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;
}