diff --git a/.DS_Store b/.DS_Store index cf0f0d7..0ac3bf3 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..67d6ccc --- /dev/null +++ b/_config.yml @@ -0,0 +1,5 @@ +# GitHub Pages Configuration +theme: null +plugins: [] +include: [".gitignore"] +exclude: [".DS_Store", "node_modules", ".git"] diff --git a/assets/.DS_Store b/assets/.DS_Store index da3c127..09ffb1f 100644 Binary files a/assets/.DS_Store and b/assets/.DS_Store differ diff --git a/assets/audio/ambient.mp3 b/assets/audio/ambient.mp3 new file mode 100644 index 0000000..6bd46c7 Binary files /dev/null and b/assets/audio/ambient.mp3 differ diff --git a/home_page.css b/home_page.css index 5871c71..3689b40 100644 --- a/home_page.css +++ b/home_page.css @@ -1,4 +1,5 @@ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + * { margin: 0; padding: 0; @@ -19,19 +20,20 @@ body { justify-content: center; background: radial-gradient(circle at center, - #0e1629 0%, - #0a1120 40%, - #050a14 70%, - #000000 100% - ); + #0e1629 0%, + #0a1120 40%, + #050a14 70%, + #000000 100%); background-attachment: fixed; } /* Stars Container */ .stars { position: fixed; - left: 0; top: 0; - width: 100%; height: 100%; + left: 0; + top: 0; + width: 100%; + height: 100%; pointer-events: none; z-index: 0; } @@ -52,21 +54,28 @@ body { width: 8px; height: 8px; background: white; - clip-path: polygon( - 40% 0%, 60% 0%, - 60% 40%, 100% 40%, - 100% 60%, 60% 60%, - 60% 100%, 40% 100%, - 40% 60%, 0% 60%, - 0% 40%, 40% 40% - ); + clip-path: polygon(40% 0%, 60% 0%, + 60% 40%, 100% 40%, + 100% 60%, 60% 60%, + 60% 100%, 40% 100%, + 40% 60%, 0% 60%, + 0% 40%, 40% 40%); opacity: 0.9; animation: twinkle 1.4s infinite ease-in-out; } @keyframes twinkle { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.3; transform: scale(0.5); } + + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.3; + transform: scale(0.5); + } } /* TITLE & TEXT */ @@ -74,7 +83,8 @@ body { .game-title { font-size: 25px; margin-bottom: 10px; - color: #33f6ff; /* Minecraft diamond color */ + color: #33f6ff; + /* Minecraft diamond color */ text-shadow: 4px 4px #0a4750, 0 0 15px #33f6ff; z-index: 5; position: relative; @@ -90,6 +100,7 @@ body { z-index: 5; text-align: center; } + /* Optional typewriter (if JS uses it) */ .typewriter { overflow: hidden; @@ -100,12 +111,19 @@ body { } @keyframes typing { - from { width: 0; } - to { width: 100%; } + from { + width: 0; + } + + to { + width: 100%; + } } @keyframes blink { - 50% { border-color: transparent; } + 50% { + border-color: transparent; + } } /* Home Wrapper - Centers all content */ @@ -123,9 +141,11 @@ body { /* Dashboard */ .outer-box { - width: 460px; + width: 100%; + max-width: 460px; background: rgba(20, 20, 20, 0.92); - border: 6px solid #00c244; /* Minecraft emerald border */ + border: 6px solid #00c244; + /* Minecraft emerald border */ padding: 20px; box-shadow: 0 0 20px #00c244, inset 0 0 20px #00c244; animation: floatBox 3s ease-in-out infinite; @@ -134,9 +154,17 @@ body { } @keyframes floatBox { - 0% { transform: translateY(0px); } - 50% { transform: translateY(-10px); } - 100% { transform: translateY(0px); } + 0% { + transform: translateY(0px); + } + + 50% { + transform: translateY(-10px); + } + + 100% { + transform: translateY(0px); + } } .menu-box { @@ -163,7 +191,7 @@ body { padding: 14px; font-size: 14px; cursor: pointer; - background: #00c244; + background: #00c244; color: #002910; border: 4px solid #002910; box-shadow: 4px 4px #001a0b; @@ -173,9 +201,17 @@ body { } @keyframes pulse { - 0% { box-shadow: 0 0 6px #00c244; } - 50% { box-shadow: 0 0 20px #00c244; } - 100% { box-shadow: 0 0 6px #00c244; } + 0% { + box-shadow: 0 0 6px #00c244; + } + + 50% { + box-shadow: 0 0 20px #00c244; + } + + 100% { + box-shadow: 0 0 6px #00c244; + } } .menu-btn:hover { @@ -214,6 +250,16 @@ body { border-color: #ffffff; } +/* Accessibility: Focus styles */ +.menu-btn:focus-visible, +.menu-select:focus-visible, +.volume-slider:focus-visible, +.close-btn:focus-visible { + outline: 3px solid #ffffff; + outline-offset: 2px; + box-shadow: 0 0 15px #33f6ff; +} + /* FOOTER */ @@ -230,13 +276,23 @@ footer:hover { } @keyframes walkAcross { - 0% { left: -60px; } - 100% { left: 110%; } + 0% { + left: -60px; + } + + 100% { + left: 110%; + } } @keyframes spriteWalk { - from { background-position: 0 0; } - to { background-position: -192px 0; } + from { + background-position: 0 0; + } + + to { + background-position: -192px 0; + } } /* @@ -245,8 +301,10 @@ footer:hover { #screen-transition { position: fixed; - top: 0; left: 0; - width: 100%; height: 100%; + top: 0; + left: 0; + width: 100%; + height: 100%; background: black; z-index: 5000; opacity: 0; @@ -265,13 +323,16 @@ footer:hover { width: 90%; padding: 15px; } + .menu-box { padding: 12px; } + .game-title { font-size: 20px; } } + /* Knight Pixel Character */ /* Dust container FIXED (only one copy) */ #dust-container { @@ -284,31 +345,40 @@ footer:hover { z-index: 5; } +/* Dust particle */ /* Dust particle */ .dust { position: absolute; - width: 6px; - height: 6px; - background: rgba(255, 255, 255, 0.8); - border-radius: 50%; + width: 4px; + height: 4px; + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 0 4px rgba(255, 255, 255, 0.5); + border-radius: 0; + /* Square pixels */ opacity: 1; image-rendering: pixelated; - transform: translateY(0px) scale(1); + pointer-events: none; + + /* Variables for randomization */ + --drift-x: -10px; + --drift-y: -10px; - animation: dustFade 0.45s ease-out forwards; + animation: dustFade 0.6s ease-out forwards; } /* Dust fade animation */ @keyframes dustFade { 0% { opacity: 1; - transform: translateY(-4px) scale(1); + transform: translate(0, 0) scale(1); } + 100% { opacity: 0; - transform: translateY(-16px) scale(0.3); + transform: translate(var(--drift-x), var(--drift-y)) scale(0); } } + /* Knight Wrapper = movement + scaling */ /* Knight container handles movement + direction */ .knight-wrapper { @@ -320,8 +390,19 @@ footer:hover { z-index: 20; transform-origin: bottom left; - animation: knightWalkAcross 14s linear infinite; - --scale: 2.1; /* You can change size globally here */ + animation: knightWalkAcross 14s linear infinite, knightBob 0.4s infinite alternate ease-in-out; + --scale: 2.1; + /* You can change size globally here */ +} + +@keyframes knightBob { + from { + margin-bottom: 0px; + } + + to { + margin-bottom: 4px; + } } /* Knight sprite sheet frames */ @@ -330,7 +411,8 @@ footer:hover { height: 64px; background: url("./assets/sprites/blue_knight.png") no-repeat; - background-size: 256px 64px; /* 4 walking frames */ + background-size: 256px 64px; + /* 4 walking frames */ image-rendering: pixelated; animation: knightWalkFrames 0.8s steps(4) infinite; @@ -338,24 +420,54 @@ footer:hover { /* Walking frames */ @keyframes knightWalkFrames { - from { background-position: 0 0; } - to { background-position: -256px 0; } + from { + background-position: 0 0; + } + + to { + background-position: -256px 0; + } } /* WALK ACROSS + AUTOMATIC FLIP */ @keyframes knightWalkAcross { - 0% { left: -80px; transform: scale(var(--scale)) scaleX(1); } - 47% { left: calc(100% - 160px); transform: scale(var(--scale)) scaleX(1); } - 50% { transform: scale(var(--scale)) scaleX(-1); } - 97% { left: -80px; transform: scale(var(--scale)) scaleX(-1); } - 100% { transform: scale(var(--scale)) scaleX(1); } + 0% { + left: -80px; + transform: scale(var(--scale)) scaleX(1); + } + + 47% { + left: calc(100% - 160px); + transform: scale(var(--scale)) scaleX(1); + } + + 50% { + transform: scale(var(--scale)) scaleX(-1); + } + + 97% { + left: -80px; + transform: scale(var(--scale)) scaleX(-1); + } + + 100% { + transform: scale(var(--scale)) scaleX(1); + } } /* Sword swing (2 frames only) */ @keyframes knightSwordSwing { - from { background-position: 0 -64px; } - to { background-position: -128px -64px; } /* 2 sword frames */ + from { + background-position: 0 -64px; + } + + to { + background-position: -128px -64px; + } + + /* 2 sword frames */ } + /* For better readability of the written content */ .storyline { max-width: 700px; @@ -365,13 +477,16 @@ footer:hover { letter-spacing: 0.4px; text-shadow: 1px 1px #003f2c; } + .how-to-play-box ul li { margin-bottom: 10px; line-height: 1; } + .how-to-play-box h2 { margin-bottom: 10px; } + .game-title { margin-bottom: 18px; } @@ -379,30 +494,33 @@ footer:hover { .subtitle { margin-bottom: 20px; } + .shooting-star { position: absolute; width: 3px; height: 80px; - background: linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,0)); + background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); border-radius: 2px; transform-origin: left top; transform: rotate(-35deg); opacity: 0.95; - filter: drop-shadow(0 0 6px rgba(255,255,255,0.7)); + filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.7)); z-index: 2; animation: shooting 1.2s linear forwards; } @keyframes shooting { from { - transform: translate(0,0) rotate(-35deg) scaleY(1); + transform: translate(0, 0) rotate(-35deg) scaleY(1); opacity: 1; } + to { - transform: translate(240px,150px) rotate(-35deg) scaleY(0.2); + transform: translate(240px, 150px) rotate(-35deg) scaleY(0.2); opacity: 0; } } + /* Multiline Typewriter Cursor */ .type-cursor { display: inline-block; @@ -414,15 +532,25 @@ footer:hover { } @keyframes cursorBlink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0; + } } + .hidden { display: none !important; } + #story-text { white-space: pre-line; } + .type-cursor { position: relative; top: -2px; @@ -455,7 +583,7 @@ footer:hover { max-height: 70vh; overflow-y: auto; box-shadow: 0 0 25px rgba(0, 234, 255, 0.4), - inset 0 0 15px rgba(0, 234, 255, 0.1); + inset 0 0 15px rgba(0, 234, 255, 0.1); position: relative; } @@ -564,10 +692,10 @@ footer:hover { flex: 1; height: 6px; border-radius: 3px; - background: linear-gradient(to right, - #00eaff 0%, - #00ff88 50%, - #00eaff 100%); + background: linear-gradient(to right, + #00eaff 0%, + #00ff88 50%, + #00eaff 100%); border: 2px solid rgba(0, 234, 255, 0.4); outline: none; -webkit-appearance: none; @@ -709,4 +837,190 @@ footer:hover { .controls-table td:last-child { color: #d8ffe6; +} + +/* ========== PROFILE AVATAR BUTTON (TOP RIGHT) ========== */ + +.profile-avatar-btn { + position: fixed; + top: 20px; + right: 20px; + width: 60px; + height: 60px; + border-radius: 50%; + background: rgba(0, 234, 255, 0.2); + border: 3px solid #00eaff; + font-size: 32px; + cursor: pointer; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + box-shadow: 0 0 15px rgba(0, 234, 255, 0.5); +} + +.profile-avatar-btn:hover { + background: rgba(0, 234, 255, 0.4); + box-shadow: 0 0 25px rgba(0, 234, 255, 0.8); + transform: scale(1.1); +} + +.profile-avatar-btn:active { + transform: scale(0.95); +} + +#avatarDisplay { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +/* ========== USER PROFILE POPUP ========== */ + +.profile-content { + display: flex; + flex-direction: column; + gap: 20px; + margin: 20px 0; +} + +.profile-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.profile-section label { + color: #00eaff; + font-weight: bold; + font-size: 12px; +} + +.profile-input { + padding: 10px; + background: rgba(0, 234, 255, 0.1); + border: 2px solid #00eaff; + color: #00ff88; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + border-radius: 4px; + text-align: center; +} + +.profile-input::placeholder { + color: rgba(0, 255, 136, 0.5); +} + +.profile-input:focus { + outline: none; + background: rgba(0, 234, 255, 0.2); + box-shadow: 0 0 10px #00eaff; +} + +.error-msg { + color: #ff6b6b; + font-size: 10px; + text-align: center; +} + +.error-msg.hidden { + display: none; +} + +/* Avatar Grid */ +.avatar-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.avatar-btn { + background: rgba(0, 234, 255, 0.1); + border: 2px solid rgba(0, 234, 255, 0.4); + font-size: 28px; + padding: 15px; + border-radius: 6px; + cursor: pointer; + transition: 0.3s ease; + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-btn:hover { + background: rgba(0, 234, 255, 0.3); + border-color: #00eaff; + box-shadow: 0 0 10px rgba(0, 234, 255, 0.5); + transform: scale(1.05); +} + +.avatar-btn.selected { + background: #00eaff; + border-color: #00ff88; + box-shadow: 0 0 15px #00eaff; + transform: scale(1.1); +} + +/* Profile Stats Display */ +.stats-display { + background: rgba(0, 234, 255, 0.1); + border: 1px solid rgba(0, 234, 255, 0.3); + border-radius: 4px; + padding: 12px; + font-size: 11px; +} + +.stats-display p { + margin: 6px 0; + color: #d8ffe6; +} + +.stats-display strong { + color: #00ff88; +} + +/* Profile Buttons */ +.profile-buttons { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 15px; +} + +.profile-buttons button { + flex: 1; + padding: 10px; + border: 2px solid; + border-radius: 4px; + font-family: 'Press Start 2P', monospace; + font-size: 10px; + font-weight: bold; + cursor: pointer; + transition: 0.3s ease; +} + +.confirm-btn { + background: #00ff88; + color: #000; + border-color: #00ff88; +} + +.confirm-btn:hover { + background: #00dd66; + box-shadow: 0 0 10px #00ff88; +} + +.cancel-btn { + background: rgba(255, 100, 100, 0.2); + color: #ff6b6b; + border-color: #ff6b6b; +} + +.cancel-btn:hover { + background: rgba(255, 100, 100, 0.4); + box-shadow: 0 0 10px rgba(255, 100, 100, 0.5); } \ No newline at end of file diff --git a/home_page.html b/home_page.html index d23bb89..08dd679 100644 --- a/home_page.html +++ b/home_page.html @@ -12,6 +12,11 @@ + + +
@@ -51,7 +56,7 @@

๐Ÿงฉ Code Quest: Debug the Matrix

- High Score: + ๐Ÿ”ฅ Highest Score: 0 @@ -196,7 +201,7 @@

๐ŸŽฎ Game Controls

- +
- Accuracy - 100% + ๐Ÿ”ฅ Highest Score + 0
diff --git a/pages/final_score.js b/pages/final_score.js index 132efd4..68cabf9 100644 --- a/pages/final_score.js +++ b/pages/final_score.js @@ -5,6 +5,7 @@ // Load data from localStorage let score = Number(localStorage.getItem("codequestScore")) || 0; const difficulty = localStorage.getItem("codequestDifficulty") || "easy"; +const highestScore = Number(localStorage.getItem("codequestHighScore")) || 0; // Elements const scoreEl = document.getElementById("finalScore"); @@ -13,6 +14,7 @@ const scoreMessage = document.getElementById("scoreMessage"); const motivationalText = document.getElementById("motivationalText"); const levelsCompletedEl = document.getElementById("levelsCompleted"); const difficultyDisplay = document.getElementById("difficultyDisplay"); +const highestScoreEl = document.getElementById("highestScore"); const achievementsList = document.getElementById("achievementsList"); // Set max score @@ -21,6 +23,9 @@ const maxScore = 250; // Display score scoreEl.textContent = score + " / " + maxScore; +// Display highest score +if (highestScoreEl) highestScoreEl.textContent = highestScore; + // Convert score โ†’ star rating function getStars(score) { if (score >= 230) return "โญโญโญโญโญ"; diff --git a/pages/pages_style/game.css b/pages/pages_style/game.css index 680a59c..e45d569 100644 --- a/pages/pages_style/game.css +++ b/pages/pages_style/game.css @@ -90,6 +90,17 @@ body { white-space: pre-wrap; text-shadow: 0 0 10px #00ffcc; } + +/* Highlight buggy line after final hint */ +.highlight-hint-line { + background: rgba(255, 255, 0, 0.25); + border-left: 3px solid #ffff00; + padding-left: 6px; + display: block; + box-shadow: inset 0 0 15px rgba(255, 255, 0, 0.3); + border-radius: 3px; +} + .game-container { display: flex; flex-direction: column; @@ -749,7 +760,7 @@ body { } #showAnswerPopup .cancel-btn { - background: #666; + background: #666; color: white; } @@ -832,39 +843,97 @@ body { /* ===== Summary Popup Styling ===== */ .summary-box { - max-height: 70vh; + max-height: 85vh; + height: auto; overflow-y: auto; - width: 380px; + width: 650px; + max-width: 90vw; + background: linear-gradient(135deg, rgba(0, 20, 50, 0.95) 0%, rgba(10, 30, 60, 0.95) 100%); + border: 3px solid #00ffcc; + border-radius: 8px; + box-shadow: + 0 0 30px rgba(0, 255, 204, 0.5), + inset 0 0 20px rgba(0, 255, 204, 0.1), + 0 0 60px rgba(0, 100, 200, 0.3); + padding: 24px; + position: relative; +} + +/* Pixel-themed corner decorations */ +.summary-box::before, +.summary-box::after { + content: ''; + position: absolute; + width: 12px; + height: 12px; + background: #00ffcc; + box-shadow: 0 0 10px #00ffcc; +} + +.summary-box::before { + top: -6px; + left: -6px; +} + +.summary-box::after { + bottom: -6px; + right: -6px; +} + +.summary-box h2 { + color: #00ffcc; + font-size: 1.8rem; + margin-bottom: 16px; + text-shadow: 0 0 15px rgba(0, 255, 204, 0.6); + text-align: center; + letter-spacing: 2px; } .summary-content { text-align: left; - line-height: 1.6; - font-size: 14px; - color: #e0e0e0; - margin: 16px 0; + line-height: 1.8; + font-size: 15px; + color: #d0d8ff; + margin: 20px 0; } .summary-content strong { - color: #00eaff; + color: #00ffff; display: block; - margin-top: 12px; - margin-bottom: 8px; + margin-top: 16px; + margin-bottom: 10px; + font-size: 1.1rem; + text-shadow: 0 0 10px rgba(0, 255, 255, 0.4); + letter-spacing: 1px; } .summary-content ul { - margin-left: 20px; - margin-bottom: 12px; + margin-left: 24px; + margin-bottom: 14px; + background: rgba(0, 50, 100, 0.3); + padding: 12px 16px; + border-left: 4px solid #00ff88; + border-radius: 4px; } .summary-content li { - margin-bottom: 6px; + margin-bottom: 8px; + position: relative; + padding-left: 8px; +} + +.summary-content li::before { + content: 'โ–ธ'; + color: #00ff88; + margin-right: 8px; } .summary-content li strong { color: #00ff88; display: inline; margin: 0; + font-size: 1rem; + text-shadow: 0 0 8px rgba(0, 255, 136, 0.4); } /* ===== FIX: Done button always visible ===== */ diff --git a/pages_script/audio_manager.js b/pages_script/audio_manager.js new file mode 100644 index 0000000..3bb3d9f --- /dev/null +++ b/pages_script/audio_manager.js @@ -0,0 +1,11 @@ +export const bgMusic = new Audio("./assets/audio/ambient.mp3"); + +bgMusic.volume = 0.4; +bgMusic.loop = true; +bgMusic.preload = "auto"; + +window.addEventListener("load", () => { + bgMusic.play().catch(() => { + document.addEventListener("click", () => bgMusic.play(), { once: true }); + }); +}); \ No newline at end of file diff --git a/pages_script/game.js b/pages_script/game.js index 87aa4a0..96df6ab 100644 --- a/pages_script/game.js +++ b/pages_script/game.js @@ -1,6 +1,33 @@ console.log("game.js loaded"); +// ===== GLOBAL PLAYTIME TRACKING ===== +let globalPlaytimeInterval = null; + +function startGlobalPlaytimeTracking() { + // Start tracking playtime in 1-second intervals + globalPlaytimeInterval = setInterval(() => { + const profile = JSON.parse(localStorage.getItem("codequestUserProfile")) || { + username: "Player", + avatar: "knight", + playtimeSeconds: 0, + createdAt: Date.now(), + preferredDifficulty: "easy" + }; + + profile.playtimeSeconds += 1; + localStorage.setItem("codequestUserProfile", JSON.stringify(profile)); + console.log(`โฑ๏ธ Playtime: ${profile.playtimeSeconds}s`); + }, 1000); +} + +function stopGlobalPlaytimeTracking() { + if (globalPlaytimeInterval) { + clearInterval(globalPlaytimeInterval); + globalPlaytimeInterval = null; + } +} + // Optional: save progress (not heavily used yet, but kept) function saveGameProgress(level, score, difficulty) { const data = { @@ -67,15 +94,32 @@ document.addEventListener("DOMContentLoaded", () => { let isPaused = false; let isTimeUp = false; - // ========================================================== + // ===== BADGE TRACKING VARIABLES ===== + let hintsUsedThisLevel = 0; + let totalSubmissions = 0; + let levelAttempts = 0; + let pauseCountSession = 0; + let hackerCodeTriggered = false; + let settingsViewedThisSession = false; + let timeRemaining = 60; + + // Start playtime tracking + startGlobalPlaytimeTracking(); + + // Check and unlock Code Explorer badge on first game start + if (!localStorage.getItem("codeQuestFirstStart")) { + localStorage.setItem("codeQuestFirstStart", "true"); + } + // LEVELS BY DIFFICULTY - // ========================================================== + const levelsByDifficulty = { // ---------------------- EASY ---------------------- easy: [ { number: 1, question: "Fix the code: Arrays are 0-indexed. Change items[3] to the correct index and use the correct function to get the list length.", + highlightLines: [2, 3, 3], snippet: `items = ["pen", "book", "bag"] print(items[3]) print(items.length)`, @@ -108,6 +152,7 @@ print(len(items))`, { number: 2, question: "Fix the code: Add proper indentation to the loop, fix the variable name typo, and ensure the print statement uses the correct variable name.", + highlightLines: [5, 5, 7, 4, 5], snippet: `numbers = [1, 2, 3, 4] total = 0 @@ -149,6 +194,7 @@ print(total)`, { number: 3, question: "Fix the code: Complete the function definition with proper syntax, fix variable name typos, remove inappropriate modifications, and use the correct return variable.", + highlightLines: [1, 3, 5, 6, 10], snippet: `def sumList(nums) total = 0 for i in range(len(num)): @@ -197,6 +243,7 @@ print(sumList(numbers))`, { number: 4, question: "Fix the code: Replace the incorrect assignment with the correct list method to add words to the collection. Use .append() instead of replacing the list.", + highlightLines: [9, 9, 9, 9], snippet: `def collect_unique_words(text): words = text.split() unique = [] @@ -253,6 +300,7 @@ print(collect_unique_words(sentence))`, { number: 5, question: "Fix the code: The function arguments are in the wrong order. Look at the function definition and call it with the correct parameter order.", + highlightLines: [11, 11, 11, 11], snippet: `def get_user_age(users, name): for user in users: if user["name"] == name: @@ -303,6 +351,7 @@ print(get_user_age(users, "Alice"))`, { number: 1, question: "Fix the code: In the if statement condition, use the comparison operator '==' instead of the assignment operator '=' to check if numbers are even.", + highlightLines: [3, 3, 3], snippet: `numbers = [2, 4, 6, 8] for n in numbers: if n % 2 = 0: @@ -335,6 +384,7 @@ for n in numbers: { number: 2, question: "Fix the code: Add proper indentation to the function body and provide both required arguments when calling the function.", + highlightLines: [2, 4, 4], snippet: `def multiply(a, b): return a * b @@ -368,6 +418,7 @@ print(multiply(5, 3))`, { number: 3, question: "Fix the code: The 'gender' key doesn't exist in the dictionary. Use the safe .get() method instead of direct indexing to avoid a KeyError.", + highlightLines: [2, 2, 2], snippet: `user = {"name": "Ava", "age": 20} print(user["gender"])`, hints: [ @@ -396,6 +447,7 @@ print(user.get("gender", "Not specified"))`, { number: 4, question: "Fix the code: Initialize max with the first element of the list to handle negative numbers correctly, and rename the variable to avoid shadowing built-in functions.", + highlightLines: [2, 2, 2], snippet: `def find_max(nums): max = 0 for n in nums: @@ -438,6 +490,7 @@ print(find_max([-5, -10, -3]))`, { number: 5, question: "Fix the code: Add the required colon at the end of the for loop statement. Python requires a colon before any indented block.", + highlightLines: [1, 1], snippet: `for i in range(1, 5) print(i)`, hints: [ @@ -469,6 +522,7 @@ print(find_max([-5, -10, -3]))`, { number: 1, question: "Fix the code: Replace the mutable default argument [] with None to avoid data persisting across function calls. Create a new list inside the function instead.", + highlightLines: [1, 1, 1, 1], snippet: `def add_item(item, items=[]): items.append(item) return items @@ -510,6 +564,7 @@ print(add_item("banana"))`, { number: 2, question: "Fix the code: The base case for recursion is wrong. In mathematics, 0! equals 1, not 0. Fix the return statement for the base case.", + highlightLines: [3, 3, 3, 3], snippet: `def factorial(n): if n == 0: return 0 @@ -551,6 +606,7 @@ print(factorial(0))`, { number: 3, question: "Fix the code: The file.close() method is missing parentheses and won't execute. Use a 'with' statement (context manager) for proper file handling instead.", + highlightLines: [5, 5, 5, 5], snippet: `file = open("data.txt", "r") lines = file.readlines() for line in lines: @@ -586,6 +642,7 @@ file.close`, { number: 4, question: "Fix the code: The variable name in the list comprehension doesn't match the expression. Use 'n' in the loop variable to match the 'n * n' expression.", + highlightLines: [2, 2, 2, 2], snippet: `nums = [1, 2, 3, 4] squares = [n * n for i in nums] print(squares)`, @@ -617,6 +674,7 @@ print(squares)`, { number: 5, question: "Fix the code: Replace the bare 'except:' clause with a specific exception type (ZeroDivisionError). Bare except clauses hide bugs and catch all exceptions.", + highlightLines: [4, 4, 4, 4], snippet: `def safe_divide(a, b): try: return a / b @@ -670,10 +728,23 @@ print(safe_divide(10, 0))`, // Score stored per difficulty const scoreKey = `codequestScore_${difficulty}`; + + // Reset score when starting a fresh game (Level 1 with no prior session) + if (currentLevel === 1 && !localStorage.getItem("codeQuestGameInProgress")) { + localStorage.setItem(scoreKey, "0"); + localStorage.setItem("codeQuestGameInProgress", "true"); + } + + // Load accumulated score from localStorage (persists across levels) let totalScore = Number(localStorage.getItem(scoreKey)) || 0; + + // Initialize score display + if (scoreDisplay) { + scoreDisplay.textContent = `Score: ${totalScore}`; + } function getHintCost() { - return 10 * (hintStep + 1); + return 10 ; } function updateHintCostUI() { @@ -687,9 +758,9 @@ print(safe_divide(10, 0))`, }); } - // ========================================================== + // LOAD LEVEL - // ========================================================== + function loadLevel(levelNum) { const level = levels[levelNum - 1]; if (!level) return; @@ -745,9 +816,9 @@ print(safe_divide(10, 0))`, }, 1000); } - // ========================================================== + // TIME UP HANDLER - // ========================================================== + function handleTimeUp() { isTimeUp = true; @@ -794,9 +865,8 @@ print(safe_divide(10, 0))`, loadLevel(currentLevel); } - // ========================================================== - // CHECK ANSWER - // ========================================================== + + // CHECK ANSWER function checkAnswer() { if (isPaused || isTimeUp) { gameMessage.textContent = isTimeUp @@ -805,6 +875,9 @@ print(safe_divide(10, 0))`, return; } + // Track total submissions for Bug Sniper badge + totalSubmissions++; + // ๐Ÿ”’ Prevent answering after showing the answer if (answerShown) { gameMessage.textContent = "โš ๏ธ Submit disabled after showing the answer."; @@ -855,9 +928,9 @@ print(safe_divide(10, 0))`, nextLevelBtn.classList.remove("hidden"); } - // ========================================================== + // HINTS - // ========================================================== + function showHint() { if (!gameStarted) { gameMessage.textContent = "โ— Start the game first!"; @@ -889,14 +962,74 @@ print(safe_divide(10, 0))`, totalScore -= getHintCost(); localStorage.setItem(scoreKey, totalScore); scoreDisplay.textContent = `Score: ${totalScore}`; + + // Track hint usage for badges + hintsUsedThisLevel++; hintTextEl.textContent = hints[hintStep]; hintTextEl.classList.remove("hidden"); + // Highlight buggy line after any hint is shown + if (level.highlightLines && level.highlightLines[hintStep] && !isPaused && !isTimeUp && !answerShown) { + highlightBuggyLine(level.highlightLines[hintStep]); + } + hintStep++; updateHintCostUI(); } + // ========================================================== + // HIGHLIGHT BUGGY LINE + // + // Highlights the specified line of the code snippet with a visual indicator + function highlightBuggyLine(lineNumber) { + const code = codeSnippetEl.textContent; + const lines = code.split('\n'); + + // Validate line number (1-based index) + if (lineNumber < 1 || lineNumber > lines.length) return; + + // Get the buggy line (convert to 0-based index) + const buggyLineIndex = lineNumber - 1; + + // Build HTML with highlights + let htmlContent = ''; + for (let i = 0; i < lines.length; i++) { + if (i === buggyLineIndex) { + // Wrap the buggy line in highlight span + htmlContent += `${escapeHtml(lines[i])}`; + } else { + htmlContent += escapeHtml(lines[i]); + } + + // Add newline after each line except the last + if (i < lines.length - 1) { + htmlContent += '\n'; + } + } + + // Update the code snippet with highlighted line + codeSnippetEl.innerHTML = htmlContent; + + // Re-enable editing after setting innerHTML + if (gameStarted && !answerShown && !levelCompleted) { + codeSnippetEl.contentEditable = "true"; + codeSnippetEl.style.pointerEvents = "auto"; + } + } + + // Helper function to escape HTML special characters + function escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, m => map[m]); + } + // ========================================================== // PAUSE / RESUME // ========================================================== @@ -905,6 +1038,8 @@ print(safe_divide(10, 0))`, if (isPaused) return; isPaused = true; + stopGlobalPlaytimeTracking(); // Pause playtime tracking + if (gameContainer) gameContainer.classList.add("paused-blur"); if (pausePopup) { pausePopup.classList.remove("hidden"); @@ -920,6 +1055,7 @@ print(safe_divide(10, 0))`, function resumeGame() { if (!isPaused || isTimeUp) return; isPaused = false; + startGlobalPlaytimeTracking(); // Resume playtime tracking if (gameContainer) gameContainer.classList.remove("paused-blur"); if (pausePopup) { @@ -951,6 +1087,11 @@ print(safe_divide(10, 0))`, // ===== HOME BUTTON ===== if (homeBtn) { homeBtn.addEventListener("click", () => { + stopGlobalPlaytimeTracking(); // Stop tracking playtime when leaving + + // Clear the game in progress flag so next game starts fresh + localStorage.removeItem("codeQuestGameInProgress"); + // Save current level and game state const gameProgress = { level: currentLevel, @@ -1045,10 +1186,20 @@ print(safe_divide(10, 0))`, // NEXT LEVEL NAVIGATION // ========================================================== function goToNextLevel() { + stopGlobalPlaytimeTracking(); // Stop tracking when moving to next level + const next = Number(window.currentLevel) + 1; // After Level 5 โ†’ Final Score page if (next > 5) { + // ๐Ÿ”ฅ Update highest score if current score is higher + const currentScore = Number(localStorage.getItem(`codequestScore_${localStorage.getItem("codequestDifficulty")}`)) || 0; + const highestScore = Number(localStorage.getItem("codequestHighScore")) || 0; + + if (currentScore > highestScore) { + localStorage.setItem("codequestHighScore", currentScore); + } + window.location.href = "../final_score.html"; return; }