Compare commits

...
10 Commits
9 changed files with 104 additions and 49 deletions

No files matched your search

+7
View File
@@ -15,6 +15,7 @@ pokesql/
├── index.html ← Game screens & structure ├── index.html ← Game screens & structure
├── style.css ← GBA Pokémon aesthetic, fully responsive ├── style.css ← GBA Pokémon aesthetic, fully responsive
├── game.js ← Game engine, controller, walking, save system ├── game.js ← Game engine, controller, walking, save system
├── config.js ← Game-specific config (name, text, badges, milestones)
├── questions.json ← 100 T-SQL questions & answers ├── questions.json ← 100 T-SQL questions & answers
├── player.png ← Player sprite sheet (24 frames, 16×32px) ├── player.png ← Player sprite sheet (24 frames, 16×32px)
├── oak.png ← Professor Oak NPC sprite ├── oak.png ← Professor Oak NPC sprite
@@ -184,6 +185,12 @@ Then reference `"npc": "Your NPC Name"` in `questions.json`. PNG files are auto-
--- ---
## ⚙️ Customising the Game
All game-specific text lives in **`config.js`** — the only file that differs between PokéSQL and its sister game PokéAzure. Edit it to change the game name, town name, intro dialog, badge names, battle move names, and more without touching `game.js` or `index.html`.
---
## 📱 Mobile Tips ## 📱 Mobile Tips
- Add to Home Screen on iOS/Android for a full-screen app-like experience - Add to Home Screen on iOS/Android for a full-screen app-like experience
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

+33
View File
@@ -0,0 +1,33 @@
const CONFIG = {
gameName: 'PokéSQL',
subject: 'SQL',
examName: 'SQL',
townName: 'SQL Town',
saveKey: 'pokesql_save_v2',
titleText: 'SQL',
titleEdition: '★ SQL TRAINER ★',
titleFooter: 'PokéSQL v1.0 · SQL · github.com/JustinOros',
townNameUpper:'SQL TOWN',
continueLogoSuffix: 'SQL',
expLabel: 'SQL EXP',
battleMove: 'SQL KNOWLEDGE',
battleMiss: 'SQL QUERY',
completeMsg: "You've conquered SQL Town and mastered Microsoft T-SQL! Your SQL journey is just beginning — more towns and exams await!",
badges: [
'🥉 SQL EXPLORER — Q25',
'🥈 SQL APPRENTICE — Q50',
'🥇 SQL PRACTITIONER — Q75',
'🏆 SQL CHAMPION — Q100',
],
milestones: {
25: { badge: '🥉 SQL EXPLORER', stars: '★ ★ ☆ ☆' },
50: { badge: '🥈 SQL APPRENTICE', stars: '★ ★ ★ ☆' },
75: { badge: '🥇 SQL PRACTITIONER', stars: '★ ★ ★ ★' },
},
introLines: [
"Hello {name}! Welcome to PokéSQL, the world of SQL training! My name is Professor Oak, the SQL Professor!",
"Your mission? Journey through SQL Town, challenge SQL masters, and conquer 100 database questions to become the ultimate SQL Trainer!",
"But first... every trainer needs a partner Pokémon! Let's choose yours, {name}!",
],
namePrompt: "Hello there! Welcome to PokéSQL! My name is Professor Oak — the SQL Professor. Now tell me, what is your name?",
};
+30 -35
View File
@@ -1,16 +1,3 @@
const CONFIG = {
gameName: 'PokéSQL',
subject: 'SQL',
examName: 'SQL',
townName: 'SQL Town',
saveKey: 'pokesql_save_v2',
introLines: [
"Hello {name}! Welcome to PokéSQL, the world of SQL training! My name is Professor Oak, the SQL Professor!",
"Your mission? Journey through SQL Town, challenge SQL masters, and conquer 100 database questions to become the ultimate SQL Trainer!",
"But first... every trainer needs a partner Pokémon! Let's choose yours, {name}!",
],
namePrompt: "Hello there! Welcome to PokéSQL! My name is Professor Oak — the SQL Professor. Now tell me, what is your name?",
};
const ALL_POKEMON = [ const ALL_POKEMON = [
{ name:'Charmander', emoji:'🔥', type:'Fire' }, { name:'Charmander', emoji:'🔥', type:'Fire' },
@@ -81,13 +68,13 @@ const NPC = {
'Old Man Bob': 'bob.png', 'Old Man Bob': 'bob.png',
'Nurse Joy': 'joy.png', 'Nurse Joy': 'joy.png',
'Officer Jenny': 'jenny.png', 'Officer Jenny': 'jenny.png',
'Brock': '🧑‍🍳', 'Brock': 'brock.png',
'Misty': '🧜‍♀️', 'Misty': 'misty.png',
'Giovanni': '😈', 'Giovanni': '😈',
'Team Rocket Grunt': '🚀', 'Team Rocket Grunt': '🚀',
'Scientist Bill': '🔬', 'Scientist Bill': '🔬',
'Fisherman Ralph': 'ralph.png', 'Fisherman Ralph': 'ralph.png',
'Hiker Taro': '🧗', 'Hiker Taro': 'taro.png',
'Gym Leader Surge': '⚡', 'Gym Leader Surge': '⚡',
'Erika': '🌸', 'Erika': '🌸',
'Koga': '🥷', 'Koga': '🥷',
@@ -106,13 +93,9 @@ const INTRO_MSGS = CONFIG.introLines.map(text => ({ speaker:'PROFESSOR OAK', npc
const CORRECT_FB = ["That's right!","Excellent work!","Perfect!","Outstanding!","Correct!","You're a natural!","Great answer!","Spot on!","Impressive!","Well done!"]; const CORRECT_FB = ["That's right!","Excellent work!","Perfect!","Outstanding!","Correct!","You're a natural!","Great answer!","Spot on!","Impressive!","Well done!"];
const WRONG_FB = ["Not quite...","Hmm, that's not it.","Try again next time!","Almost...","Not this time!"]; const WRONG_FB = ["Not quite...","Hmm, that's not it.","Try again next time!","Almost...","Not this time!"];
const MILESTONES = {
25: { badge:'🥉 CLOUD EXPLORER', stars:'★ ★ ☆ ☆' },
50: { badge:'🥈 AZURE APPRENTICE', stars:'★ ★ ★ ☆' },
75: { badge:'🥇 CLOUD PRACTITIONER', stars:'★ ★ ★ ★' },
};
const SAVE_KEY = CONFIG.saveKey; const SAVE_KEY = CONFIG.saveKey;
const MILESTONES = CONFIG.milestones;
function saveGame(s) { function saveGame(s) {
try { localStorage.setItem(SAVE_KEY, JSON.stringify({ try { localStorage.setItem(SAVE_KEY, JSON.stringify({
@@ -255,6 +238,7 @@ const SFX = (() => {
encounter() { seq([{f:523,d:.1},{f:659,d:.18}], 'square', 0.22); }, encounter() { seq([{f:523,d:.1},{f:659,d:.18}], 'square', 0.22); },
type() { tone(880 + Math.random()*200, 'square', 0.03, 0.005, 0.01, 0.02); }, type() { tone(880 + Math.random()*200, 'square', 0.03, 0.005, 0.01, 0.02); },
select() { const c = getCtx(); tone(440, 'square', 0.12, 0.005, 0.03, 0.04, c.currentTime + 0.05); }, select() { const c = getCtx(); tone(440, 'square', 0.12, 0.005, 0.03, 0.04, c.currentTime + 0.05); },
backspace() { seq([{f:350,d:.05},{f:250,d:.08}], 'triangle', 0.12); },
correct() { seq([{f:523,d:.08},{f:659,d:.08},{f:784,d:.08},{f:1047,d:.2}], 'square', 0.18); }, correct() { seq([{f:523,d:.08},{f:659,d:.08},{f:784,d:.08},{f:1047,d:.2}], 'square', 0.18); },
streak() { seq([{f:523,d:.06},{f:659,d:.06},{f:784,d:.06},{f:1047,d:.06},{f:1319,d:.2}], 'square', 0.18); }, streak() { seq([{f:523,d:.06},{f:659,d:.06},{f:784,d:.06},{f:1047,d:.06},{f:1319,d:.2}], 'square', 0.18); },
wrong() { seq([{f:330,d:.1},{f:277,d:.1},{f:233,d:.18}], 'sawtooth', 0.15); }, wrong() { seq([{f:330,d:.1},{f:277,d:.1},{f:233,d:.18}], 'sawtooth', 0.15); },
@@ -403,10 +387,21 @@ class Game {
const keys = this._getNameKeys(); const keys = this._getNameKeys();
const total = keys.length; const total = keys.length;
let c = this._gpKeyCursor; let c = this._gpKeyCursor;
if (dir === 'right') c = Math.min(c + 1, total - 1); const row = Math.floor(c / COLS);
if (dir === 'left') c = Math.max(c - 1, 0); const col = c % COLS;
if (dir === 'down') c = Math.min(c + COLS, total - 1); const totalRows = Math.ceil(total / COLS);
if (dir === 'up') c = Math.max(c - COLS, 0); if (dir === 'right') {
c = (col + 1 >= COLS || c + 1 >= total) ? row * COLS : c + 1;
}
if (dir === 'left') {
c = (col - 1 < 0) ? Math.min(row * COLS + COLS - 1, total - 1) : c - 1;
}
if (dir === 'down') {
c = c + COLS >= total ? col : c + COLS;
}
if (dir === 'up') {
c = c - COLS < 0 ? Math.min((totalRows - 1) * COLS + col, total - 1) : c - COLS;
}
this._gpKeyCursor = c; this._gpKeyCursor = c;
this._renderNameCursor(); this._renderNameCursor();
SFX.select(); SFX.select();
@@ -1150,21 +1145,16 @@ class Game {
boot() { boot() {
this.show('boot'); this.show('boot');
this._showController(true); this._showController(true);
try {
const bootAudio = new Audio('./loading.mp3');
bootAudio.volume = 0.5;
bootAudio.play().catch(() => {});
} catch(e) {}
setTimeout(() => this.showTitle(), 1800); setTimeout(() => this.showTitle(), 1800);
} }
showTitle() { showTitle() {
this.show('title'); this.show('title');
SFX.boot();
this._showController(true); this._showController(true);
const go = () => { const go = () => {
document.getElementById('screen-title').removeEventListener('click', go); document.getElementById('screen-title').removeEventListener('click', go);
Music.unblock(); Music.play();
SFX.boot();
this.showContinueOrName(); this.showContinueOrName();
}; };
document.getElementById('screen-title').addEventListener('click', go); document.getElementById('screen-title').addEventListener('click', go);
@@ -1283,7 +1273,7 @@ class Game {
} }
addChar(ch) { if (this.state.playerName.length<10){ this.state.playerName+=ch; this.refreshNameDisplay(); SFX.select(); } } addChar(ch) { if (this.state.playerName.length<10){ this.state.playerName+=ch; this.refreshNameDisplay(); SFX.select(); } }
delChar() { this.state.playerName=this.state.playerName.slice(0,-1); this.refreshNameDisplay(); } delChar() { this.state.playerName=this.state.playerName.slice(0,-1); this.refreshNameDisplay(); SFX.backspace(); }
refreshNameDisplay() { refreshNameDisplay() {
const el=document.getElementById('name-display'); const el=document.getElementById('name-display');
if(el) el.textContent=this.state.playerName+(this.state.playerName.length<10?'_':''); if(el) el.textContent=this.state.playerName+(this.state.playerName.length<10?'_':'');
@@ -1448,6 +1438,7 @@ class Game {
showCatchScreen() { showCatchScreen() {
const wild = this.state.currentWild; const wild = this.state.currentWild;
if (!wild) return; if (!wild) return;
this._throwing = false;
this.show('catch'); this.show('catch');
this._showController(true); this._showController(true);
@@ -1489,6 +1480,9 @@ class Game {
} }
throwPokeball(wild, canCatch) { throwPokeball(wild, canCatch) {
if (this._throwing) return;
if (this.state.pokeballs <= 0) return;
this._throwing = true;
this.state.pokeballs--; this.state.pokeballs--;
SFX.pokeball(); SFX.pokeball();
@@ -1556,6 +1550,7 @@ class Game {
} }
_showCatchResult(wild, canCatch) { _showCatchResult(wild, canCatch) {
this._throwing = false;
this.show('catch-result'); this.show('catch-result');
this._showController(true); this._showController(true);
@@ -2016,7 +2011,7 @@ class Game {
const msg = document.getElementById('battle-anim-msg'); const msg = document.getElementById('battle-anim-msg');
if (correct) { if (correct) {
msg.textContent = `${playerPoke.name} used SQL KNOWLEDGE!`; msg.textContent = `${playerPoke.name} used ${CONFIG.battleMove}!`;
SFX.attack(); SFX.attack();
setTimeout(() => { setTimeout(() => {
npcHp.style.width = '0%'; npcHp.style.width = '0%';
@@ -2032,7 +2027,7 @@ class Game {
this.showResult(true, q); this.showResult(true, q);
}, 2200); }, 2200);
} else { } else {
msg.textContent = `${playerPoke.name} used SQL QUERY... but it missed!`; msg.textContent = `${playerPoke.name} used ${CONFIG.battleMiss}... but it missed!`;
SFX.attack(); SFX.attack();
setTimeout(() => { setTimeout(() => {
msg.textContent = `${npcPoke.name} used COUNTER ATTACK!`; msg.textContent = `${npcPoke.name} used COUNTER ATTACK!`;
+32 -13
View File
@@ -2,12 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0a0520"> <meta name="theme-color" content="#0a0520">
<title>PokéSQL — Lean SQL</title> <title id="page-title">PokéSQL — Learn SQL</title>
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
@@ -28,19 +28,19 @@
<div class="title-stars"></div> <div class="title-stars"></div>
<div class="title-main"> <div class="title-main">
<div class="title-logo"> <div class="title-logo">
<span class="title-poke">POKÉ</span><span class="title-sql">SQL</span> <span class="title-poke">POKÉ</span><span class="title-game" id="title-game-text">SQL</span>
</div> </div>
<div class="title-edition">★ SQL TRAINER ★</div> <div class="title-edition" id="title-edition-text">★ SQL TRAINER ★</div>
<div class="title-tap blink">PRESS START</div> <div class="title-tap blink">PRESS START</div>
</div> </div>
<div class="title-footer">PokéSQL v1.0 · SQL · github.com/JustinOros</div> <div class="title-footer" id="title-footer-text">PokéSQL v1.0 · SQL · github.com/JustinOros</div>
</div> </div>
</div> </div>
<!-- CONTINUE OR NEW GAME --> <!-- CONTINUE OR NEW GAME -->
<div id="screen-continue" class="screen"> <div id="screen-continue" class="screen">
<div class="continue-bg"> <div class="continue-bg">
<div class="continue-logo">POKÉ<span>SQL</span></div> <div class="continue-logo">POKÉ<span id="continue-logo-text">SQL</span></div>
<div class="continue-card" id="continue-card"> <div class="continue-card" id="continue-card">
<div class="continue-card-header">📂 CONTINUE</div> <div class="continue-card-header">📂 CONTINUE</div>
<div class="save-row"><span class="save-lbl">TRAINER</span><span class="save-val" id="save-name-disp">—</span></div> <div class="save-row"><span class="save-lbl">TRAINER</span><span class="save-val" id="save-name-disp">—</span></div>
@@ -142,7 +142,7 @@
<div class="map-header"> <div class="map-header">
<div class="map-left"> <div class="map-left">
<span class="map-pin">📍</span> <span class="map-pin">📍</span>
<span class="map-town-name">SQL TOWN</span> <span class="map-town-name" id="map-town-name-text">SQL TOWN</span>
</div> </div>
<div class="map-center"> <div class="map-center">
<div class="prog-bar-outer"><div class="prog-bar-inner" id="map-prog-bar"></div></div> <div class="prog-bar-outer"><div class="prog-bar-inner" id="map-prog-bar"></div></div>
@@ -260,7 +260,7 @@
<div class="levelup-box"> <div class="levelup-box">
<div class="lu-stars blink" id="lu-stars">★ BADGE EARNED! ★</div> <div class="lu-stars blink" id="lu-stars">★ BADGE EARNED! ★</div>
<div class="lu-sprite"><div class="lu-player-sprite"></div></div> <div class="lu-sprite"><div class="lu-player-sprite"></div></div>
<div class="lu-text"><span id="lu-name">ASH</span> gained SQL EXP!</div> <div class="lu-text"><span id="lu-name">ASH</span> gained <span id="exp-label-text">SQL EXP</span>!</div>
<div class="lu-badge" id="lu-badge">🏅 CLOUD TRAINEE</div> <div class="lu-badge" id="lu-badge">🏅 CLOUD TRAINEE</div>
<div class="lu-prog-wrap"> <div class="lu-prog-wrap">
<div class="lu-prog-outer"><div class="lu-prog-inner" id="lu-prog-inner"></div></div> <div class="lu-prog-outer"><div class="lu-prog-inner" id="lu-prog-inner"></div></div>
@@ -277,14 +277,14 @@
<div class="complete-trophy">🏆</div> <div class="complete-trophy">🏆</div>
<div class="complete-title">CHAMPION!</div> <div class="complete-title">CHAMPION!</div>
<div class="complete-name" id="complete-name">ASH</div> <div class="complete-name" id="complete-name">ASH</div>
<p class="complete-msg">You've conquered SQL Town and mastered Microsoft T-SQL! Your SQL journey is just beginning — more towns and exams await!</p> <p class="complete-msg"><span id="complete-msg-text">You've conquered SQL Town and mastered Microsoft T-SQL! Your SQL journey is just beginning — more towns and exams await!</span></p>
<div class="complete-stats" id="complete-stats"></div> <div class="complete-stats" id="complete-stats"></div>
<div class="complete-pokemon" id="complete-pokemon"></div> <div class="complete-pokemon" id="complete-pokemon"></div>
<div class="complete-badges"> <div class="complete-badges">
<div class="badge-row">🥉 SQL EXPLORER — Q25</div> <div class="badge-row"><span id="badge-1">🥉 SQL EXPLORER — Q25</span></div>
<div class="badge-row">🥈 SQL APPRENTICE — Q50</div> <div class="badge-row"><span id="badge-2">🥈 SQL APPRENTICE — Q50</span></div>
<div class="badge-row">🥇 SQL PRACTITIONER — Q75</div> <div class="badge-row"><span id="badge-3">🥇 SQL PRACTITIONER — Q75</span></div>
<div class="badge-row">🏆 SQL CHAMPION — Q100</div> <div class="badge-row"><span id="badge-4">🏆 SQL CHAMPION — Q100</span></div>
<div class="badge-row">🗺️ More towns & exams coming soon...</div> <div class="badge-row">🗺️ More towns & exams coming soon...</div>
</div> </div>
<button class="btn-pixel btn-red btn-full" id="btn-play-again">▶ PLAY AGAIN</button> <button class="btn-pixel btn-red btn-full" id="btn-play-again">▶ PLAY AGAIN</button>
@@ -316,6 +316,25 @@
</div> </div>
<div id="toast" class="toast hidden"></div> <div id="toast" class="toast hidden"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
set('title-game-text', CONFIG.titleText);
set('title-edition-text', CONFIG.titleEdition);
set('title-footer-text', CONFIG.titleFooter);
set('continue-logo-text', CONFIG.continueLogoSuffix);
set('map-town-name-text', CONFIG.townNameUpper);
set('exp-label-text', CONFIG.expLabel);
set('complete-msg-text', CONFIG.completeMsg);
if (CONFIG.badges) {
CONFIG.badges.forEach((b, i) => set(`badge-${i+1}`, b));
}
set('page-title', CONFIG.gameName + ' — Learn ' + CONFIG.subject);
document.title = CONFIG.gameName + ' — Learn ' + CONFIG.subject;
});
</script>
<script src="config.js"></script>
<script src="game.js"></script> <script src="game.js"></script>
</body> </body>
</html> </html>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

+2 -1
View File
@@ -42,6 +42,7 @@ html, body, #app {
user-select: none; user-select: none;
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
touch-action: manipulation;
} }
/* ─── BODY ───────────────────────────────────────────────── */ /* ─── BODY ───────────────────────────────────────────────── */
@@ -378,7 +379,7 @@ html, body {
.title-main { text-align:center; z-index:1; padding:24px 16px; } .title-main { text-align:center; z-index:1; padding:24px 16px; }
.title-logo { margin-bottom:16px; line-height:1; } .title-logo { margin-bottom:16px; line-height:1; }
.title-poke { font-size:clamp(22px,7.5vw,38px); color:var(--yellow); text-shadow:4px 4px 0 #7a5800,0 0 24px rgba(248,192,48,.5); animation:goldShimmer 3s ease-in-out infinite; display:inline-block; } .title-poke { font-size:clamp(22px,7.5vw,38px); color:var(--yellow); text-shadow:4px 4px 0 #7a5800,0 0 24px rgba(248,192,48,.5); animation:goldShimmer 3s ease-in-out infinite; display:inline-block; }
.title-sql { font-size:clamp(22px,7.5vw,38px); color:#60c8ff; text-shadow:4px 4px 0 #003d70,0 0 24px rgba(96,200,255,.5); display:inline-block; animation:blueShimmer 3s ease-in-out infinite; } .title-game { font-size:clamp(22px,7.5vw,38px); color:#60c8ff; text-shadow:4px 4px 0 #003d70,0 0 24px rgba(96,200,255,.5); display:inline-block; animation:blueShimmer 3s ease-in-out infinite; }
.title-sub { font-size:clamp(6px,1.8vw,8px); color:rgba(255,255,255,.75); letter-spacing:1px; margin:10px 0 6px; } .title-sub { font-size:clamp(6px,1.8vw,8px); color:rgba(255,255,255,.75); letter-spacing:1px; margin:10px 0 6px; }
.title-edition { font-size:clamp(5.5px,1.6vw,7px); color:var(--yellow); margin-bottom:24px; animation:goldShimmer 3s ease-in-out infinite; } .title-edition { font-size:clamp(5.5px,1.6vw,7px); color:var(--yellow); margin-bottom:24px; animation:goldShimmer 3s ease-in-out infinite; }
.title-deco { font-size:clamp(22px,6vw,32px); margin-bottom:28px; display:flex; gap:16px; justify-content:center; } .title-deco { font-size:clamp(22px,6vw,32px); margin-bottom:28px; display:flex; gap:16px; justify-content:center; }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B