From e03ebffc2f050c2729a37d187ecfcc02c6d537a1 Mon Sep 17 00:00:00 2001 From: Sterling Archer Date: Wed, 27 May 2026 10:23:10 -0700 Subject: [PATCH] Improve fun factor (added pokemon to game) --- game.js | 812 +++++++++++++++++++++++++++++++++++++++++------------ index.html | 105 ++++++- style.css | 487 +++++++++++++++++++++++++++++++- 3 files changed, 1206 insertions(+), 198 deletions(-) diff --git a/game.js b/game.js index 485c62b..6436bc6 100644 --- a/game.js +++ b/game.js @@ -1,21 +1,71 @@ -/* ─── NPC ROSTER ─────────────────────────────────────────── */ const CONFIG = { gameName: 'PokéSQL', subject: 'SQL', examName: 'SQL', townName: 'SQL Town', - saveKey: 'pokesql_save_v1', + saveKey: 'pokesql_save_v2', introLines: [ "Hello {name}! Welcome to PokéSQL, the world of SQL training! My name is Professor Oak, the SQL Professor!", "This world is powered by Databases, Tables, Queries, Indexes, and Transactions, mastered through knowledge!", "Your mission? Journey through each town, challenge SQL masters, and conquer every database concept!", + "But first... every trainer needs a partner Pokémon! I have three special Pokémon waiting for you in my lab!", "Start in SQL Town then travel the SQL world to master JOINs, Aggregations, Indexing, and beyond!", "100 questions await you. Each correct answer earns you SQL EXP and you'll learn something real!", - "Your progress is saved automatically in your browser so you can pick up right where you left off. Now, {name}... your adventure begins!", + "Your progress is saved automatically in your browser so you can pick up right where you left off. Now, {name}... let's choose your partner!", ], 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 = [ + { name:'Charmander', emoji:'🔥', type:'Fire' }, + { name:'Squirtle', emoji:'💧', type:'Water' }, + { name:'Bulbasaur', emoji:'🌿', type:'Grass' }, + { name:'Pikachu', emoji:'⚡', type:'Electric' }, + { name:'Eevee', emoji:'🦊', type:'Normal' }, + { name:'Jigglypuff', emoji:'🎀', type:'Fairy' }, + { name:'Gengar', emoji:'👻', type:'Ghost' }, + { name:'Snorlax', emoji:'😴', type:'Normal' }, + { name:'Machamp', emoji:'💪', type:'Fighting' }, + { name:'Alakazam', emoji:'🔮', type:'Psychic' }, + { name:'Gyarados', emoji:'🐉', type:'Dragon' }, + { name:'Lapras', emoji:'🐋', type:'Water' }, + { name:'Dragonite', emoji:'🐲', type:'Dragon' }, + { name:'Mewtwo', emoji:'🧬', type:'Psychic' }, + { name:'Arcanine', emoji:'🐕', type:'Fire' }, + { name:'Rapidash', emoji:'🦄', type:'Fire' }, + { name:'Geodude', emoji:'🪨', type:'Rock' }, + { name:'Onix', emoji:'🐍', type:'Rock' }, + { name:'Haunter', emoji:'😈', type:'Ghost' }, + { name:'Abra', emoji:'🧘', type:'Psychic' }, + { name:'Pidgeot', emoji:'🦅', type:'Flying' }, + { name:'Butterfree', emoji:'🦋', type:'Bug' }, + { name:'Sandslash', emoji:'🦔', type:'Ground' }, + { name:'Ninetales', emoji:'🦊', type:'Fire' }, + { name:'Tentacruel', emoji:'🐙', type:'Water' }, + { name:'Magnemite', emoji:'🧲', type:'Electric' }, + { name:'Cloyster', emoji:'🐚', type:'Ice' }, + { name:'Hitmonlee', emoji:'🦵', type:'Fighting' }, + { name:'Starmie', emoji:'⭐', type:'Water' }, + { name:'Scyther', emoji:'🦗', type:'Bug' }, + { name:'Magikarp', emoji:'🐟', type:'Water' }, + { name:'Ditto', emoji:'🟣', type:'Normal' }, + { name:'Voltorb', emoji:'🔴', type:'Electric' }, + { name:'Cubone', emoji:'🦴', type:'Ground' }, + { name:'Chansey', emoji:'🥚', type:'Normal' }, + { name:'Kangaskhan', emoji:'🦘', type:'Normal' }, + { name:'Pinsir', emoji:'🪲', type:'Bug' }, + { name:'Tauros', emoji:'🐂', type:'Normal' }, + { name:'Vaporeon', emoji:'💎', type:'Water' }, + { name:'Flareon', emoji:'🌋', type:'Fire' }, + { name:'Jolteon', emoji:'⚡', type:'Electric' }, +]; + +const STARTER_POOL = [ + { name:'Charmander', emoji:'🔥', type:'Fire' }, + { name:'Squirtle', emoji:'💧', type:'Water' }, + { name:'Bulbasaur', emoji:'🌿', type:'Grass' }, +]; + function setNPC(el, val) { if (!el) return; if (val && val.endsWith('.png')) { @@ -68,14 +118,18 @@ const MILESTONES = { const SAVE_KEY = CONFIG.saveKey; -/* ─── SAVE / LOAD ────────────────────────────────────────── */ function saveGame(s) { - try { localStorage.setItem(SAVE_KEY, JSON.stringify({ playerName:s.playerName, currentQ:s.currentQ, score:s.score, streak:s.streak, maxStreak:s.maxStreak, correct:s.correct, wrong:s.wrong, questionOrder:s.questions.map(q=>q.id), savedAt:Date.now() })); } catch(_){} + try { localStorage.setItem(SAVE_KEY, JSON.stringify({ + playerName:s.playerName, currentQ:s.currentQ, score:s.score, + streak:s.streak, maxStreak:s.maxStreak, correct:s.correct, wrong:s.wrong, + questionOrder:s.questions.map(q=>q.id), savedAt:Date.now(), + party:s.party, pokeballs:s.pokeballs, playerLevel:s.playerLevel, + caughtCount:s.caughtCount, + })); } catch(_){} } function loadGame() { try { const r=localStorage.getItem(SAVE_KEY); return r?JSON.parse(r):null; } catch(_){return null;} } function clearSave() { try { localStorage.removeItem(SAVE_KEY); } catch(_){} } -/* ─── BACKGROUND MUSIC ───────────────────────────────────── */ const Music = (() => { let audio = null; let rival = null; @@ -151,7 +205,6 @@ const Music = (() => { }; })(); -/* ─── CHIPTUNE SOUND ENGINE ──────────────────────────────── */ const SFX = (() => { let ctx = null; @@ -173,7 +226,6 @@ const SFX = (() => { } }); - /* Core: play a tone with envelope */ function tone(freq, type, vol, attack, sustain, release, when) { const c = getCtx(); const t = when ?? c.currentTime; @@ -191,7 +243,6 @@ const SFX = (() => { osc.stop(t + attack + sustain + release + 0.01); } - /* Sequence of notes: [{f, d}...] */ function seq(notes, type='square', vol=0.18) { const c = getCtx(); let t = c.currentTime + 0.02; @@ -202,89 +253,43 @@ const SFX = (() => { } return { - /* Boot jingle — classic rising fanfare */ - boot() { - seq([ - {f:262,d:.1},{f:330,d:.1},{f:392,d:.1},{f:523,d:.25} - ], 'square', 0.2); - }, - - /* Overworld bgm loop — simple cheerful melody */ - overworldStart() { - seq([ - {f:392,d:.12},{f:440,d:.12},{f:494,d:.12},{f:523,d:.18}, - {f:494,d:.1}, {f:440,d:.1}, {f:392,d:.18}, - {f:330,d:.12},{f:392,d:.12},{f:440,d:.12},{f:392,d:.24}, - ], 'square', 0.12); - }, - - /* Footstep click — tiny blip each walk tick */ - step() { - tone(180, 'square', 0.04, 0.005, 0.02, 0.03); - }, - - /* NPC encounter — ascending two-note ding */ - encounter() { - seq([{f:523,d:.1},{f:659,d:.18}], 'square', 0.22); - }, - - /* Typewriter blip per character */ - type() { - tone(880 + Math.random()*200, 'square', 0.03, 0.005, 0.01, 0.02); - }, - - /* Select / menu move */ - select() { - const c = getCtx(); - tone(440, 'square', 0.12, 0.005, 0.03, 0.04, c.currentTime + 0.05); - }, - - /* Correct answer — happy ascending chord */ - correct() { - seq([ - {f:523,d:.08},{f:659,d:.08},{f:784,d:.08},{f:1047,d:.2} - ], 'square', 0.18); - }, - - /* Streak bonus — extra flourish */ - 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 answer — descending buzz */ - wrong() { - seq([ - {f:330,d:.1},{f:277,d:.1},{f:233,d:.18} - ], 'sawtooth', 0.15); - }, - - /* Badge / level up fanfare */ - levelUp() { - seq([ - {f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:659,d:.1}, - {f:784,d:.1},{f:1047,d:.3} - ], 'square', 0.2); - }, - - /* Champion / completion — full fanfare */ - complete() { - seq([ - {f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:1047,d:.1}, - {f:784,d:.08},{f:880,d:.08},{f:1047,d:.08},{f:1319,d:.4} - ], 'square', 0.2); - }, - - /* Menu confirm (name entry OK) */ - confirm() { - seq([{f:523,d:.08},{f:784,d:.15}], 'square', 0.18); - }, + boot() { seq([{f:262,d:.1},{f:330,d:.1},{f:392,d:.1},{f:523,d:.25}], 'square', 0.2); }, + overworldStart() { seq([{f:392,d:.12},{f:440,d:.12},{f:494,d:.12},{f:523,d:.18},{f:494,d:.1},{f:440,d:.1},{f:392,d:.18},{f:330,d:.12},{f:392,d:.12},{f:440,d:.12},{f:392,d:.24}], 'square', 0.12); }, + step() { tone(180, 'square', 0.04, 0.005, 0.02, 0.03); }, + 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); }, + select() { const c = getCtx(); tone(440, 'square', 0.12, 0.005, 0.03, 0.04, c.currentTime + 0.05); }, + 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); }, + wrong() { seq([{f:330,d:.1},{f:277,d:.1},{f:233,d:.18}], 'sawtooth', 0.15); }, + levelUp() { seq([{f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:659,d:.1},{f:784,d:.1},{f:1047,d:.3}], 'square', 0.2); }, + complete() { seq([{f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:1047,d:.1},{f:784,d:.08},{f:880,d:.08},{f:1047,d:.08},{f:1319,d:.4}], 'square', 0.2); }, + confirm() { seq([{f:523,d:.08},{f:784,d:.15}], 'square', 0.18); }, + catch() { seq([{f:330,d:.08},{f:440,d:.08},{f:523,d:.08},{f:659,d:.15},{f:784,d:.25}], 'square', 0.2); }, + pokeball() { seq([{f:600,d:.06},{f:500,d:.06},{f:400,d:.06},{f:300,d:.1}], 'triangle', 0.15); }, + escape() { seq([{f:400,d:.08},{f:350,d:.08},{f:250,d:.15}], 'sawtooth', 0.12); }, }; })(); -/* ─── GAME ───────────────────────────────────────────────── */ +function getPlayerLevel(state) { + return Math.max(1, Math.floor(state.correct * 1.2) + 1); +} + +function randomPokemonForNPC() { + const pool = ALL_POKEMON.filter(p => !STARTER_POOL.find(s => s.name === p.name)); + const pick = pool[Math.floor(Math.random() * pool.length)]; + const level = Math.floor(Math.random() * 15) + 1; + return { ...pick, level }; +} + +function randomWildPokemon(playerLevel) { + const pick = ALL_POKEMON[Math.floor(Math.random() * ALL_POKEMON.length)]; + const minLvl = Math.max(1, playerLevel - 3); + const maxLvl = playerLevel + 5; + const level = Math.floor(Math.random() * (maxLvl - minLvl + 1)) + minLvl; + return { ...pick, level }; +} + class Game { constructor() { this.state = { @@ -297,6 +302,15 @@ class Game { pathDir: 'right', lastDir: 'right', cursor:0, + party: [], + pokeballs: 0, + playerLevel: 1, + caughtCount: 0, + activePokemon: null, + wildWorldX: 0, wildWorldY: 0, + wildSpawned: false, + currentWild: null, + currentNPCPokemon: null, }; this.screens = {}; @@ -316,9 +330,6 @@ class Game { this.boot(); } - /* ══════════════════════════════════════════════════════════ - CONTROLLER OVERLAY — wires D-pad, A, B, SELECT, START - ══════════════════════════════════════════════════════════ */ _bindController() { const dpadMap = { 'dp-up':'up', 'dp-down':'down', 'dp-left':'left', 'dp-right':'right' }; @@ -350,17 +361,17 @@ class Game { document.getElementById('btn-select').addEventListener('pointerdown', (e)=>{ e.preventDefault(); }); } - /* A — map: talk to NPC | battle: confirm cursor | elsewhere: advance */ _pressA() { const s = this.state.screen; if (s === 'map') { this._talkToNPC(); return; } if (s === 'battle' && !this.state.answering) { this._confirmCursor(); return; } if (s === 'intro') { this.advanceIntro(); return; } + if (s === 'starter-confirm') { document.getElementById('btn-starter-continue')?.click(); return; } + if (s === 'catch-result') { document.getElementById('btn-catch-continue')?.click(); return; } if (s === 'result') { document.getElementById('btn-result-cont')?.click(); return; } if (s === 'levelup') { document.getElementById('btn-lu-cont')?.click(); return; } } - /* B — map: talk to NPC (same as A) | battle: no-op while answering | intro/result: advance */ _pressB() { const s = this.state.screen; if (s === 'map') { this._talkToNPC(); return; } @@ -369,11 +380,9 @@ class Game { if (s === 'result') { document.getElementById('btn-result-cont')?.click(); return; } } - /* D-pad start hold */ _dpadStart(dir) { this._heldKeys.add(dir); if (!this._walkLoop) this._startWalkLoop(); - const s = this.state.screen; if (s === 'battle') { this._moveCursor(dir); } } @@ -383,9 +392,6 @@ class Game { if (this._heldKeys.size === 0) this._stopWalkLoop(); } - /* ══════════════════════════════════════════════════════════ - MAP WALKING — 16-step loop, collision, NPC proximity - ══════════════════════════════════════════════════════════ */ _startWalkLoop() { if (this._walkLoop) return; this._walkLoop = setInterval(() => this._walkTick(), 30); @@ -429,6 +435,31 @@ class Game { this.state.worldX += dx; this.state.worldY += dy; + + if (this._rocks && this._rocks.length > 0) { + for (const rock of this._rocks) { + const rdx = this.state.worldX - rock.wx; + const rdy = this.state.worldY - rock.wy; + if (Math.abs(rdx) < rock.r && Math.abs(rdy) < rock.r) { + this.state.worldX -= dx; + this.state.worldY -= dy; + return; + } + } + } + + if (this._trees && this._trees.length > 0) { + for (const tree of this._trees) { + const tdx = this.state.worldX - tree.wx; + const tdy = this.state.worldY - tree.wy; + if (Math.abs(tdx) < tree.r && Math.abs(tdy) < tree.r) { + this.state.worldX -= dx; + this.state.worldY -= dy; + return; + } + } + } + this.state.lastDir = dir; this._applyCamera(); @@ -444,6 +475,7 @@ class Game { if (bub) { bub.style.left = (cx - 4) + 'px'; bub.style.bottom = (bot + 66) + 'px'; } } this._checkNPCProximity(); + this._checkWildProximity(); } _applyCamera() { @@ -454,6 +486,7 @@ class Game { const ty = -(spread + this.state.worldY - viewH / 2); if (inner) inner.style.transform = `translate(${tx}px,${ty}px)`; this._positionNPCOnScreen(); + this._positionWildOnScreen(); } _positionNPCOnScreen() { @@ -467,6 +500,18 @@ class Game { } } + _positionWildOnScreen() { + const {viewW, viewH} = this._getViewport(); + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap && this.state.wildSpawned) { + const screenX = viewW / 2 + (this.state.wildWorldX - this.state.worldX) - 24; + const screenY = viewH / 2 + (this.state.wildWorldY - this.state.worldY) - 24; + wildWrap.style.left = screenX + 'px'; + wildWrap.style.bottom = (viewH - screenY - 48) + 'px'; + wildWrap.style.display = 'block'; + } + } + _playerScreenOffset() { const {viewW, viewH} = this._getViewport(); return { px: viewW / 2, py: viewH - (viewH * 0.45) - 32 }; @@ -480,6 +525,15 @@ class Game { return Math.sqrt((npcSX - px) ** 2 + (npcSY - py) ** 2); } + _wildScreenDist() { + if (!this.state.wildSpawned) return Infinity; + const {viewW, viewH} = this._getViewport(); + const {px, py} = this._playerScreenOffset(); + const wSX = viewW / 2 + (this.state.wildWorldX - this.state.worldX); + const wSY = viewH / 2 + (this.state.wildWorldY - this.state.worldY); + return Math.sqrt((wSX - px) ** 2 + (wSY - py) ** 2); + } + _checkNPCProximity() { const {viewW, viewH} = this._getViewport(); const dist = this._npcScreenDist(); @@ -498,10 +552,21 @@ class Game { this._updateDirArrow(near); } + _checkWildProximity() { + if (!this.state.wildSpawned || !this.state.currentWild) return; + const dist = this._wildScreenDist(); + if (dist < 80) { + this._stopWalkLoop(); + this._heldKeys.clear(); + SFX.encounter(); + this.showCatchScreen(); + } + } + _updateDirArrow(nearNPC) { const arrow = document.getElementById('map-dir-arrow'); if (!arrow) return; - if (nearNPC) { arrow.style.display = 'none'; arrow.style.animation = 'none'; return; } + if (nearNPC) { arrow.style.display = 'none'; arrow.style.animation = 'none'; this._stopArrowCycle(); return; } const {viewW, viewH} = this._getViewport(); const dx = this.state.npcWorldX - this.state.worldX; const dy = this.state.npcWorldY - this.state.worldY; @@ -519,8 +584,12 @@ class Game { const glyphs = { right:'▶', left:'◀', up:'▲', down:'▼' }; arrow.textContent = glyphs[pd]; - arrow.style.display = 'block'; - arrow.style.animation = 'arrowPulse .7s ease-in-out infinite'; + if (this._arrowDir !== pd) { + this._arrowDir = pd; + arrow.style.display = 'block'; + arrow.style.animation = 'arrowPulse .7s ease-in-out infinite'; + this._startArrowCycle(arrow); + } if (pd === 'right') { arrow.style.removeProperty('left'); arrow.style.right = '12px'; @@ -540,6 +609,28 @@ class Game { } } + _startArrowCycle(arrow) { + this._stopArrowCycle(); + arrow.style.opacity = '0'; + const cycle = () => { + if (!arrow || arrow.style.display === 'none') return; + arrow.style.opacity = '1'; + this._arrowHideTimer = setTimeout(() => { + arrow.style.opacity = '0'; + }, 3000); + }; + cycle(); + this._arrowInterval = setInterval(cycle, 15000); + } + + _stopArrowCycle() { + clearInterval(this._arrowInterval); + clearTimeout(this._arrowHideTimer); + this._arrowInterval = null; + this._arrowHideTimer = null; + this._arrowDir = null; + } + _nearNPC() { if (!this.state.npcSpawned) return false; return this._npcScreenDist() < 100; @@ -558,7 +649,7 @@ class Game { return; } const arrow = document.getElementById('map-dir-arrow'); - if (arrow) { arrow.style.display = 'none'; arrow.style.animation = 'none'; } + if (arrow) { arrow.style.display = 'none'; arrow.style.animation = 'none'; this._stopArrowCycle(); } const bub = document.getElementById('player-bubble'); if (bub) bub.classList.remove('visible'); SFX.encounter(); @@ -584,9 +675,6 @@ class Game { if (hint) hint.style.display = 'none'; } - /* ══════════════════════════════════════════════════════════ - BATTLE CURSOR — navigate 2×2 grid with D-pad / WASD - ══════════════════════════════════════════════════════════ */ _moveCursor(dir) { const btns = document.querySelectorAll('.choice-btn:not(.disabled):not(.correct):not(.wrong):not(.confirm-inactive)'); if (!btns.length) return; @@ -616,9 +704,6 @@ class Game { if (target) target.click(); } - /* ══════════════════════════════════════════════════════════ - GLOBAL KEYBOARD - ══════════════════════════════════════════════════════════ */ _bindGlobalKeys() { const DPAD_KEYS = { 'ArrowUp':'up','ArrowDown':'down','ArrowLeft':'left','ArrowRight':'right', @@ -629,7 +714,6 @@ class Game { document.addEventListener('keydown', (e) => { const s = this.state.screen; - /* Name screen — character input */ if (s === 'name') { if (e.key==='Backspace') { e.preventDefault(); this.delChar(); } else if (e.key==='Enter') this.confirmName(); @@ -637,16 +721,69 @@ class Game { return; } - /* Title */ if (s === 'title' && (e.key==='Enter'||e.key===' ')) { document.getElementById('screen-title').click(); return; } - - /* Continue screen */ if (s === 'continue' && (e.key==='Enter'||e.key===' ')) { document.getElementById('btn-continue-save')?.click(); return; } - - /* Intro */ if (s === 'intro' && (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E')) { this.advanceIntro(); return; } - /* D-pad direction keys */ + if (s === 'starter') { + if (e.key==='1'||e.key==='2'||e.key==='3') { + this._starterCursor = parseInt(e.key) - 1; + this._renderStarterCursor(); + setTimeout(() => this.pickStarter(this._starterCursor), 150); + return; + } + if (e.key==='ArrowLeft'||e.key==='a'||e.key==='A') { + this._starterCursor = Math.max(0, (this._starterCursor||0) - 1); + this._renderStarterCursor(); + return; + } + if (e.key==='ArrowRight'||e.key==='d'||e.key==='D') { + this._starterCursor = Math.min(2, (this._starterCursor||0) + 1); + this._renderStarterCursor(); + return; + } + if (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E') { + this.pickStarter(this._starterCursor||0); + return; + } + return; + } + + if (s === 'starter-confirm' && (e.key==='Enter'||e.key===' ')) { + document.getElementById('btn-starter-continue')?.click(); + return; + } + + if (s === 'catch') { + if (e.key==='1'||e.key==='2') { + const btns = document.querySelectorAll('.catch-action-btn'); + if (e.key==='1' && btns[0]) btns[0].click(); + if (e.key==='2' && btns[1]) btns[1].click(); + return; + } + if (e.key==='ArrowLeft'||e.key==='a'||e.key==='A') { + this._catchCursor = 0; + this._renderCatchCursor(); + return; + } + if (e.key==='ArrowRight'||e.key==='d'||e.key==='D') { + this._catchCursor = 1; + this._renderCatchCursor(); + return; + } + if (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E') { + const btns = document.querySelectorAll('.catch-action-btn'); + if (btns[this._catchCursor||0]) btns[this._catchCursor||0].click(); + return; + } + return; + } + + if (s === 'catch-result' && (e.key==='Enter'||e.key===' ')) { + document.getElementById('btn-catch-continue')?.click(); + return; + } + if (DPAD_KEYS[e.key] && !e.repeat) { const dir = DPAD_KEYS[e.key]; if ((s==='map' || s==='battle') && !this._heldKeys.has(dir)) { @@ -658,17 +795,14 @@ class Game { return; } - /* A-button equivalent on keyboard */ if ((e.key==='Enter'||e.key==='e'||e.key==='E') && !e.repeat) { e.preventDefault(); this._pressA(); return; } - /* B-button */ if ((e.key==='x'||e.key==='X') && !e.repeat) { this._pressB(); return; } - /* Quick-pick numbers on battle */ if (s==='battle' && !this.state.answering) { const numMap={'1':0,'2':1,'3':2,'4':3}; if (numMap[e.key]!==undefined) { @@ -693,21 +827,6 @@ class Game { }); } - /* ══════════════════════════════════════════════════════════ - XBOX / GAMEPAD SUPPORT (Gamepad API) - Polls every animation frame. Works with Xbox, PS, and most - USB/Bluetooth gamepads that the browser recognises. - - Xbox mapping: - Axes 0/1 = left stick X/Y - Button 0 = A (confirm / talk) - Button 1 = B (talk / back) - Button 12 = D-pad up - Button 13 = D-pad down - Button 14 = D-pad left - Button 15 = D-pad right - Button 9 = Start (confirm) - ══════════════════════════════════════════════════════════ */ _bindGamepad() { this._gpPrev = {}; this._gpActive = false; @@ -716,8 +835,6 @@ class Game { console.log('Gamepad connected:', e.gamepad.id); }); - /* Start polling immediately — don't wait for the event. - The poll function checks if any gamepad is present each frame. */ this._startGpLoop(); } @@ -751,7 +868,6 @@ class Game { const s = this.state.screen; - /* ── NAME SCREEN — D-pad navigates keyboard grid, A selects, B deletes, Start confirms ── */ if (s === 'name') { const letterKeys = Array.from(document.querySelectorAll('.key-btn')); const delBtn = document.getElementById('btn-backspace'); @@ -760,14 +876,11 @@ class Game { const total = keys.length; const COLS = 10; - /* Highlight current key — works for letter keys, DEL and OK */ keys.forEach((k, i) => { k.style.outline = i === this._gpKeyCursor ? '3px solid #f8c030' : ''; k.style.outlineOffset = i === this._gpKeyCursor ? '-2px' : ''; }); - /* D-pad navigation with repeat delay */ - const now = Date.now(); const moved = Object.entries(gpDirs).find(([d, active]) => active && !this._gpPrev[`axis_${d}`]); if (moved) { const [dir] = moved; @@ -783,7 +896,7 @@ class Game { }); } - if (justPressed(0) || justPressed(1)) { /* A or B — activate highlighted key */ + if (justPressed(0) || justPressed(1)) { const key = keys[this._gpKeyCursor]; if (!key) return; const isLetterKey = key.classList.contains('key-btn'); @@ -795,17 +908,15 @@ class Game { this.confirmName(); } } - if (justPressed(2)) { /* X — delete */ + if (justPressed(2)) { this.delChar(); } - if (justPressed(9) || justPressed(8)) { /* Start or Select — confirm name */ + if (justPressed(9) || justPressed(8)) { this.confirmName(); } - /* ── ALL OTHER SCREENS — directional movement + button actions ── */ } else { - /* ── CONTINUE SCREEN — D-pad up/down switches between Continue and New Game ── */ if (s === 'continue') { if (this._gpContCursor === undefined) this._gpContCursor = 0; const contBtn = document.getElementById('btn-continue-save'); @@ -817,12 +928,10 @@ class Game { this._gpContCursor = this._gpContCursor === 0 ? 1 : 0; } - /* Highlight selected button */ if (contBtn) contBtn.style.outline = this._gpContCursor === 0 ? '3px solid #f8c030' : ''; if (newBtn) newBtn.style.outline = this._gpContCursor === 1 ? '3px solid #f8c030' : ''; } - /* Walk / battle cursor movement */ Object.entries(gpDirs).forEach(([dir, active]) => { const key = `axis_${dir}`; if (active && !this._gpPrev[key]) { @@ -831,18 +940,24 @@ class Game { if (!this._walkLoop) this._startWalkLoop(); } if (s === 'battle') this._moveCursor(dir); + if (s === 'starter' && (dir === 'left' || dir === 'right')) { + this._starterCursor = dir === 'left' ? Math.max(0, (this._starterCursor||0) - 1) : Math.min(2, (this._starterCursor||0) + 1); + this._renderStarterCursor(); + } + if (s === 'catch' && (dir === 'left' || dir === 'right')) { + this._catchCursor = dir === 'left' ? 0 : 1; + this._renderCatchCursor(); + } } else if (!active && this._gpPrev[key]) { this._heldKeys.delete(dir); if (this._heldKeys.size === 0) this._stopWalkLoop(); } }); - /* A / Start — context action on every screen */ if (justPressed(0) || justPressed(9)) { - if (s === 'boot') { /* wait for title */ } + if (s === 'boot') { } else if (s === 'title') { document.getElementById('screen-title')?.click(); } else if (s === 'continue') { - /* 0 = CONTINUE highlighted, 1 = NEW GAME highlighted */ if (this._gpContCursor === 0) document.getElementById('btn-continue-save')?.click(); else document.getElementById('btn-new-game')?.click(); } @@ -851,10 +966,13 @@ class Game { else if (s === 'result') { document.getElementById('btn-result-cont')?.click(); } else if (s === 'levelup') { document.getElementById('btn-lu-cont')?.click(); } else if (s === 'complete') { document.getElementById('btn-play-again')?.click(); } + else if (s === 'starter-confirm') { document.getElementById('btn-starter-continue')?.click(); } + else if (s === 'starter') { this.pickStarter(this._starterCursor||0); } + else if (s === 'catch') { const cbtns = document.querySelectorAll('.catch-action-btn'); if (cbtns[this._catchCursor||0]) cbtns[this._catchCursor||0].click(); } + else if (s === 'catch-result') { document.getElementById('btn-catch-continue')?.click(); } else { this._pressA(); } } - /* B — also confirms answer in battle */ if (justPressed(1)) { if (s === 'battle' && !this.state.answering) { this._confirmCursor(); } else if (s === 'intro') { this.advanceIntro(); } @@ -862,7 +980,6 @@ class Game { else { this._pressB(); } } - /* Quick-pick answers X=2, Y=3, LB=4, RB=5 */ if (s === 'battle' && !this.state.answering) { const quickMap = { 2:0, 3:1, 4:2, 5:3 }; Object.entries(quickMap).forEach(([btn, idx]) => { @@ -875,12 +992,12 @@ class Game { } } - /* Store all states for next frame */ Object.entries(gpDirs).forEach(([d, v]) => this._gpPrev[`axis_${d}`] = v); for (let i = 0; i < btns.length; i++) { this._gpPrev[`btn_${i}`] = btns[i]?.pressed; } } + _showController(visible) { ['gba-left', 'gba-right'].forEach(id => { const el = document.getElementById(id); @@ -890,9 +1007,6 @@ class Game { }); } - /* ══════════════════════════════════════════════════════════ - SCREENS - ══════════════════════════════════════════════════════════ */ boot() { this.show('boot'); this._showController(false); @@ -913,7 +1027,7 @@ class Game { showContinueOrName() { const save = loadGame(); - if (save && save.playerName && save.currentQ > 0 && save.currentQ < 100) { + if (save && save.playerName && save.currentQ > 0 && save.currentQ < 100 && save.party && save.party.length > 0) { this.show('continue'); this._showController(false); const pct = Math.round((save.currentQ/100)*100); @@ -927,13 +1041,13 @@ class Game { document.getElementById('btn-new-game').textContent = '✦ CONFIRM NEW GAME'; document.getElementById('btn-new-game').onclick = () => { clearSave(); this.startFresh(); }; }; - /* Reset controller cursor to CONTINUE button */ this._gpContCursor = 0; setTimeout(() => { const c = document.getElementById('btn-continue-save'); if (c) c.style.outline = '3px solid #f8c030'; }, 50); } else { + clearSave(); this.startFresh(); } } @@ -944,6 +1058,11 @@ class Game { score:save.score, streak:save.streak, maxStreak:save.maxStreak, correct:save.correct, wrong:save.wrong, worldX:0, worldY:0, npcWorldX:0, npcWorldY:0, npcSpawned:false, + party: save.party || [], + pokeballs: save.pokeballs || 0, + playerLevel: save.playerLevel || 1, + caughtCount: save.caughtCount || 0, + activePokemon: save.party && save.party.length > 0 ? save.party[0] : null, }); this.loadQuestions(() => { if (save.questionOrder && save.questionOrder.length === this.state.questions.length) { @@ -958,11 +1077,13 @@ class Game { } startFresh() { - Object.assign(this.state, { playerName:'', currentQ:0, score:0, streak:0, maxStreak:0, correct:0, wrong:0 }); + Object.assign(this.state, { + playerName:'', currentQ:0, score:0, streak:0, maxStreak:0, correct:0, wrong:0, + party:[], pokeballs:0, playerLevel:1, caughtCount:0, activePokemon:null, + }); this.showNameEntry(); } - /* ── NAME ─────────────────────────────────────────────── */ showNameEntry() { this.show('name'); this._showController(false); @@ -993,7 +1114,6 @@ class Game { } confirmName() { if(!this.state.playerName.trim()) this.state.playerName='ASH'; SFX.confirm(); this.showIntro(); } - /* ── INTRO ────────────────────────────────────────────── */ showIntro() { this.show('intro'); this._showController(false); @@ -1032,13 +1152,197 @@ class Game { if (this.state.introStep >= INTRO_MSGS.length) { const el = document.getElementById('screen-intro'); if (this._introClick) el.removeEventListener('click', this._introClick); - this.loadQuestions(() => this.showMap()); + this.showStarterSelection(); return; } this.renderIntroMsg(); } - /* ── LOAD QUESTIONS ───────────────────────────────────── */ + showStarterSelection() { + this.show('starter'); + this._showController(true); + this._starterCursor = 0; + const oakEl = document.getElementById('starter-oak'); + if (oakEl) setNPC(oakEl, NPC['Professor Oak']); + + this.typeText('starter-text', `Now, ${this.state.playerName||'ASH'}! Choose your partner Pokémon! Each one is special in its own way!`); + + const container = document.getElementById('starter-choices'); + container.innerHTML = ''; + + STARTER_POOL.forEach((poke, i) => { + const btn = document.createElement('button'); + btn.className = 'starter-btn'; + btn.innerHTML = ` + ${poke.emoji} + ${poke.name} + ${poke.type} · Lv5 + `; + btn.addEventListener('click', () => this.pickStarter(i)); + container.appendChild(btn); + }); + this._renderStarterCursor(); + } + + _renderStarterCursor() { + const btns = document.querySelectorAll('.starter-btn'); + btns.forEach((btn, i) => { + btn.classList.toggle('starter-btn-selected', i === (this._starterCursor||0)); + }); + } + + _renderCatchCursor() { + const btns = document.querySelectorAll('.catch-action-btn'); + btns.forEach((btn, i) => { + btn.classList.toggle('catch-btn-selected', i === (this._catchCursor||0)); + }); + } + + pickStarter(index) { + const chosen = { ...STARTER_POOL[index], level: 5 }; + this.state.party = [chosen]; + this.state.activePokemon = chosen; + this.state.pokeballs = 5; + + SFX.catch(); + + this.show('starter-confirm'); + this._showController(false); + + document.getElementById('starter-chosen-emoji').textContent = chosen.emoji; + document.getElementById('starter-chosen-name').textContent = chosen.name.toUpperCase(); + document.getElementById('starter-chosen-level').textContent = `Lv ${chosen.level} · ${chosen.type}`; + + const others = STARTER_POOL.filter((_, i) => i !== index); + const fleeText = `${others[0].emoji} ${others[0].name} and ${others[1].emoji} ${others[1].name} ran away!`; + document.getElementById('starter-flee-text').textContent = fleeText; + + this.typeText('starter-confirm-text', + `Great choice! ${chosen.name} looks excited to join you! The other Pokémon scurried away into the wild. You also received 5 Pokéballs!` + ); + + document.getElementById('btn-starter-continue').onclick = () => { + this.loadQuestions(() => this.showMap()); + }; + } + + _spawnWildPokemon() { + if (this.state.pokeballs <= 0) { this.state.wildSpawned = false; return; } + if (Math.random() > 0.90) { this.state.wildSpawned = false; return; } + + const pLevel = getPlayerLevel(this.state); + const wild = randomWildPokemon(pLevel); + this.state.currentWild = wild; + + const {viewW, viewH} = this._getViewport(); + const npcDx = this.state.npcWorldX; + const npcDy = this.state.npcWorldY; + + const t = 0.2 + Math.random() * 0.5; + let wx = npcDx * t + (Math.random() - 0.5) * viewW * 1.5; + let wy = npcDy * t + (Math.random() - 0.5) * viewH * 1.5; + + const minDist = 150; + if (Math.sqrt(wx*wx + wy*wy) < minDist) { + wx += (wx >= 0 ? 1 : -1) * minDist; + wy += (wy >= 0 ? 1 : -1) * minDist; + } + + this.state.wildWorldX = wx; + this.state.wildWorldY = wy; + this.state.wildSpawned = true; + + const wildEl = document.getElementById('map-wild'); + if (wildEl) wildEl.textContent = wild.emoji; + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap) wildWrap.style.display = 'block'; + + this._positionWildOnScreen(); + } + + showCatchScreen() { + const wild = this.state.currentWild; + if (!wild) return; + + this.show('catch'); + this._showController(true); + this._catchCursor = 0; + Music.pause(); + + document.getElementById('catch-wild-pokemon').textContent = wild.emoji; + document.getElementById('catch-wild-name').textContent = wild.name.toUpperCase(); + document.getElementById('catch-wild-level').textContent = `Lv ${wild.level}`; + + const pLevel = getPlayerLevel(this.state); + const canCatch = pLevel >= wild.level; + + this.typeText('catch-text', `A wild ${wild.name} appeared!`); + + const actions = document.getElementById('catch-actions'); + actions.innerHTML = ''; + + const throwBtn = document.createElement('button'); + throwBtn.className = 'catch-action-btn btn-pixel btn-red'; + throwBtn.textContent = `⚪ THROW BALL (${this.state.pokeballs})`; + throwBtn.disabled = this.state.pokeballs <= 0; + if (this.state.pokeballs <= 0) throwBtn.style.opacity = '0.4'; + throwBtn.addEventListener('click', () => this.throwPokeball(wild, canCatch)); + actions.appendChild(throwBtn); + + const runBtn = document.createElement('button'); + runBtn.className = 'catch-action-btn btn-pixel btn-blue'; + runBtn.textContent = '🏃 RUN AWAY'; + runBtn.addEventListener('click', () => { + this.state.wildSpawned = false; + this.state.currentWild = null; + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap) wildWrap.style.display = 'none'; + this.showMap(); + }); + actions.appendChild(runBtn); + } + + throwPokeball(wild, canCatch) { + this.state.pokeballs--; + SFX.pokeball(); + + this.show('catch-result'); + this._showController(false); + + const resultEmoji = document.getElementById('catch-result-emoji'); + const resultMsg = document.getElementById('catch-result-msg'); + + if (canCatch) { + setTimeout(() => SFX.catch(), 500); + resultEmoji.textContent = wild.emoji; + resultMsg.innerHTML = ` +
GOTCHA!
+
${wild.name} (Lv ${wild.level}) was caught!
+
Added to your party!
+ `; + this.state.party.push({ ...wild }); + this.state.caughtCount++; + } else { + setTimeout(() => SFX.escape(), 300); + resultEmoji.textContent = '💨'; + resultMsg.innerHTML = ` +
OH NO!
+
${wild.name} (Lv ${wild.level}) broke free and escaped!
+
The Pokémon was too strong! Train more to catch stronger Pokémon.
+ `; + } + + this.state.wildSpawned = false; + this.state.currentWild = null; + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap) wildWrap.style.display = 'none'; + + document.getElementById('btn-catch-continue').onclick = () => { + saveGame(this.state); + this.showMap(); + }; + } + loadQuestions(cb) { if (this.state.questions.length>0) { cb(); return; } fetch('./questions.json') @@ -1055,9 +1359,6 @@ class Game { .catch(()=>this.toast('❌ Could not load questions.json')); } - /* Generate randomly placed trees across the full scrollable world */ - /* Generate trees for the current journey leg — from world origin to just past the NPC. - Called fresh each time so trees always fill the visible path ahead. */ _pickNextDir(currentDir) { const opposite = { right:'left', left:'right', up:'down', down:'up' }; const opp = opposite[currentDir]; @@ -1077,6 +1378,8 @@ class Game { const cy = spread; const npcX = cx + this.state.npcWorldX; const npcY = cy + this.state.npcWorldY; + const occupied = []; + this._trees = []; for (let i = 0; i < 300; i++) { const span = document.createElement('span'); span.className = 'tree'; @@ -1087,6 +1390,8 @@ class Game { y = cy + (Math.random() - 0.5) * spread * 1.8; tries++; } while (tries < 10 && Math.abs(x - npcX) < 80 && Math.abs(y - npcY) < 80); + occupied.push({ x, y, r: size * 0.6 }); + this._trees.push({ wx: x - cx, wy: y - cy, r: size * 0.35 }); span.textContent = '🌳'; span.style.fontSize = size + 'px'; span.style.left = x + 'px'; @@ -1095,6 +1400,50 @@ class Game { span.style.animationDuration = (2.5 + Math.random() * 2).toFixed(1) + 's'; container.appendChild(span); } + const grassEmojis = ['🌾']; + for (let i = 0; i < 360; i++) { + let x, y, tries = 0, tooClose; + do { + x = cx + (Math.random() - 0.5) * spread * 1.8; + y = cy + (Math.random() - 0.5) * spread * 1.8; + tries++; + tooClose = occupied.some(t => Math.abs(x - t.x) < t.r && Math.abs(y - t.y) < t.r); + } while (tries < 12 && (tooClose || (Math.abs(x - npcX) < 80 && Math.abs(y - npcY) < 80))); + if (tooClose) continue; + const span = document.createElement('span'); + span.className = 'tall-grass'; + const size = 12 + Math.random() * 8; + span.textContent = grassEmojis[Math.floor(Math.random() * grassEmojis.length)]; + span.style.fontSize = size + 'px'; + span.style.left = x + 'px'; + span.style.top = y + 'px'; + span.style.animationDelay = (Math.random() * 4).toFixed(2) + 's'; + span.style.animationDuration = (2 + Math.random() * 2).toFixed(1) + 's'; + container.appendChild(span); + } + this._rocks = []; + for (let i = 0; i < 90; i++) { + let x, y, tries = 0, tooClose; + do { + x = cx + (Math.random() - 0.5) * spread * 1.8; + y = cy + (Math.random() - 0.5) * spread * 1.8; + tries++; + tooClose = occupied.some(t => Math.abs(x - t.x) < t.r && Math.abs(y - t.y) < t.r); + } while (tries < 12 && (tooClose || (Math.abs(x - npcX) < 80 && Math.abs(y - npcY) < 80))); + if (tooClose) continue; + occupied.push({ x, y, r: 20 }); + const wx = x - cx; + const wy = y - cy; + this._rocks.push({ wx, wy, r: 22 }); + const span = document.createElement('span'); + span.className = 'map-rock'; + const size = 22 + Math.random() * 12; + span.textContent = '🪨'; + span.style.fontSize = size + 'px'; + span.style.left = x + 'px'; + span.style.top = y + 'px'; + container.appendChild(span); + } } _preGenerateNextLeg() { @@ -1140,6 +1489,7 @@ class Game { this.state.npcSpawned = true; this._generateTrees(); + this._spawnWildPokemon(); document.getElementById('hud-name').textContent = this.state.playerName||'ASH'; this.updateHUD(); @@ -1164,15 +1514,23 @@ class Game { }); } - updateHUD() { const tot=this.state.questions.length||100, done=this.state.currentQ, pct=(done/tot)*100; document.getElementById('hud-score').textContent = this.state.score.toLocaleString(); document.getElementById('hud-streak').textContent = this.state.streak+(this.state.streak>=3?'🔥':''); - document.getElementById('hud-correct').textContent = `${this.state.correct}/${done}`; + const partyEl = document.getElementById('hud-party'); + if (partyEl) { + partyEl.textContent = this.state.party.map(p => p.emoji).join(''); + if (this.state.party.length === 0) partyEl.textContent = '—'; + } document.getElementById('map-prog-text').textContent = `${done}/${tot}`; const bar=document.getElementById('map-prog-bar'); if(bar){ bar.style.width=pct+'%'; bar.style.background=pct<40?'var(--hp-green)':pct<75?'var(--hp-yellow)':'#60c8ff'; } + + const pbEl = document.getElementById('pokeball-count'); + if (pbEl) pbEl.textContent = `⚪ ${this.state.pokeballs}`; + + this.state.playerLevel = getPlayerLevel(this.state); } flashSaveDot() { @@ -1181,23 +1539,40 @@ class Game { setTimeout(()=>{ dot.style.opacity='.4'; },1500); } - /* ── BATTLE ───────────────────────────────────────────── */ + _getActivePokemon() { + if (this.state.party.length === 0) return { name:'MissingNo', emoji:'❓', type:'???', level:1 }; + return this.state.party[0]; + } + startQuestion() { this._stopWalkLoop(); this._heldKeys.clear(); const q=this.state.questions[this.state.currentQ]; if(!q){ this.showComplete(); return; } + const playerPoke = this._getActivePokemon(); + const npcPoke = randomPokemonForNPC(); + this.state.currentNPCPokemon = npcPoke; + this.show('battle'); this._showController(true); Music.playBattle(); this.state.answering=false; this.state.cursor=0; - setNPC(document.getElementById('battle-npc-sprite'), NPC[q.npc]||'🧑'); + document.getElementById('battle-npc-pokemon').textContent = npcPoke.emoji; + document.getElementById('battle-npc-pokemon-name').textContent = npcPoke.name.toUpperCase(); + document.getElementById('battle-npc-pokemon-level').textContent = `Lv ${npcPoke.level}`; document.getElementById('battle-npc-name').textContent = q.npc.toUpperCase(); - document.getElementById('battle-speaker').textContent = q.npc.toUpperCase(); + const npcTrainer = document.getElementById('battle-npc-trainer'); + if (npcTrainer) setNPC(npcTrainer, NPC[q.npc] || '🧑'); + + document.getElementById('battle-player-pokemon').textContent = playerPoke.emoji; + document.getElementById('battle-player-pokemon-name').textContent = playerPoke.name.toUpperCase(); + document.getElementById('battle-player-pokemon-level').textContent = `Lv ${playerPoke.level}`; document.getElementById('battle-player-name').textContent= (this.state.playerName||'ASH').toUpperCase(); + + document.getElementById('battle-speaker').textContent = q.npc.toUpperCase(); document.getElementById('battle-q-num').textContent = this.state.currentQ+1; document.getElementById('battle-pts').textContent = this.state.score.toLocaleString(); document.getElementById('battle-arrow').style.display = 'none'; @@ -1301,12 +1676,15 @@ class Game { this.state.streak++; this.state.correct++; this.state.maxStreak=Math.max(this.state.maxStreak,this.state.streak); this.state.score+=this.state.streak>=5?200:this.state.streak>=3?150:100; + this.state.pokeballs += 3; if(this.state.streak>=3) SFX.streak(); else SFX.correct(); } else { this.state.streak=0; this.state.wrong++; SFX.wrong(); } + this.state.playerLevel = getPlayerLevel(this.state); + setTimeout(()=>{ this._preGenerateNextLeg(); - this.showResult(correct,q); + this.showBattleAnim(correct, q); },120); } @@ -1331,16 +1709,80 @@ class Game { this.state.streak++; this.state.correct++; this.state.maxStreak=Math.max(this.state.maxStreak,this.state.streak); this.state.score+=this.state.streak>=5?200:this.state.streak>=3?150:100; + this.state.pokeballs += 3; if(this.state.streak>=3) SFX.streak(); else SFX.correct(); } else { this.state.streak=0; this.state.wrong++; SFX.wrong(); } + this.state.playerLevel = getPlayerLevel(this.state); + setTimeout(()=>{ this._preGenerateNextLeg(); - this.showResult(correct,q); + this.showBattleAnim(correct, q); },120); } - /* ── RESULT ───────────────────────────────────────────── */ + showBattleAnim(correct, q) { + this.show('battle-anim'); + this._showController(false); + + const playerPoke = this._getActivePokemon(); + const npcPoke = this.state.currentNPCPokemon || { name:'???', emoji:'❓', level:1 }; + + document.getElementById('anim-player-pokemon-name').textContent = playerPoke.name.toUpperCase(); + document.getElementById('anim-player-pokemon').textContent = playerPoke.emoji; + document.getElementById('anim-npc-pokemon-name').textContent = npcPoke.name.toUpperCase(); + document.getElementById('anim-npc-pokemon').textContent = npcPoke.emoji; + + const playerHp = document.getElementById('anim-player-hp'); + const npcHp = document.getElementById('anim-npc-hp'); + playerHp.style.width = '100%'; + npcHp.style.width = '100%'; + playerHp.style.background = 'var(--hp-green)'; + npcHp.style.background = 'var(--hp-green)'; + + const playerSprite = document.getElementById('anim-player-pokemon'); + const npcSprite = document.getElementById('anim-npc-pokemon'); + playerSprite.style.opacity = '1'; + playerSprite.style.transform = ''; + npcSprite.style.opacity = '1'; + npcSprite.style.transform = ''; + + const msg = document.getElementById('battle-anim-msg'); + + if (correct) { + msg.textContent = `${playerPoke.name} used SQL KNOWLEDGE!`; + setTimeout(() => { + npcHp.style.width = '0%'; + npcHp.style.background = 'var(--red)'; + msg.textContent = `It's super effective! ${npcPoke.name} fainted!`; + npcSprite.style.opacity = '0.3'; + npcSprite.style.transform = 'translateY(20px) scale(0.5)'; + }, 800); + setTimeout(() => { + npcSprite.style.opacity = '1'; + npcSprite.style.transform = ''; + this.showResult(true, q); + }, 2200); + } else { + msg.textContent = `${playerPoke.name} used SQL QUERY... but it missed!`; + setTimeout(() => { + msg.textContent = `${npcPoke.name} used COUNTER ATTACK!`; + }, 800); + setTimeout(() => { + playerHp.style.width = '0%'; + playerHp.style.background = 'var(--red)'; + msg.textContent = `${playerPoke.name} fainted!`; + playerSprite.style.opacity = '0.3'; + playerSprite.style.transform = 'translateY(20px) scale(0.5)'; + }, 1400); + setTimeout(() => { + playerSprite.style.opacity = '1'; + playerSprite.style.transform = ''; + this.showResult(false, q); + }, 2800); + } + } + showResult(correct,q) { this.show('result'); this._showController(false); @@ -1355,11 +1797,16 @@ class Game { const ansEl=document.getElementById('result-correct-ans'); const ptsEl=document.getElementById('result-pts'); + const pbEl=document.getElementById('result-pokeballs'); + if(correct){ ptsEl.textContent=this.state.streak>=5?'+200 PTS 🔥 STREAK!':this.state.streak>=3?'+150 PTS 🔥 HOT!':'+100 PTS'; ptsEl.style.display='block'; ansEl.classList.remove('visible'); + pbEl.textContent = `⚪ +3 Pokéballs! (Total: ${this.state.pokeballs})`; + pbEl.style.display = 'block'; } else { ptsEl.style.display='none'; + pbEl.style.display='none'; const correctText = q._correctDisplayText || (q.answers ? q.answers.map(a=>q.options[a]).join(' AND ') : q.options[q.answer]); ansEl.textContent='✓ Correct answer'+(q.answers?'s':'')+': '+correctText; ansEl.classList.add('visible'); @@ -1375,11 +1822,8 @@ class Game { else this.showMap(); }; contBtn.onclick = doNext; - const resultScreen = document.getElementById('screen-result'); - resultScreen.addEventListener('pointerdown', doNext, { once: true }); } - /* ── LEVEL UP ─────────────────────────────────────────── */ showLevelUp() { this.show('levelup'); SFX.levelUp(); @@ -1395,7 +1839,6 @@ class Game { document.getElementById('btn-lu-cont').onclick=()=>this.showMap(); } - /* ── COMPLETE ─────────────────────────────────────────── */ showComplete() { this.show('complete'); SFX.complete(); @@ -1407,18 +1850,24 @@ class Game { `FINAL SCORE: ${this.state.score.toLocaleString()}\n`+ `CORRECT: ${this.state.correct} / ${tot}\n`+ `ACCURACY: ${acc}%\n`+ - `BEST STREAK: ${this.state.maxStreak} 🔥`; + `BEST STREAK: ${this.state.maxStreak} 🔥\n`+ + `POKÉMON CAUGHT: ${this.state.caughtCount}`; + + const pokeEl = document.getElementById('complete-pokemon'); + if (pokeEl && this.state.party.length > 0) { + pokeEl.innerHTML = '
YOUR PARTY
' + + this.state.party.map(p => `${p.emoji} ${p.name} Lv${p.level}`).join(''); + } + document.getElementById('btn-play-again').onclick=()=>{ clearSave(); this.startFresh(); }; } - /* ── SCREEN SWITCH ────────────────────────────────────── */ show(name) { Object.values(this.screens).forEach(s=>s.classList.remove('active')); const t=this.screens[name]; if(t)t.classList.add('active'); this.state.screen=name; } - /* ── TYPEWRITER ───────────────────────────────────────── */ typeText(id,text,onDone) { if(this.state.twTimer){clearInterval(this.state.twTimer);this.state.twTimer=null;} const el=document.getElementById(id); if(!el)return; el.textContent=''; @@ -1429,7 +1878,6 @@ class Game { },26); } - /* ── TOAST ────────────────────────────────────────────── */ toast(msg,ms=2600){ const el=document.getElementById('toast'); el.textContent=msg; el.classList.remove('hidden'); diff --git a/index.html b/index.html index 305c444..8b66907 100644 --- a/index.html +++ b/index.html @@ -73,9 +73,8 @@
-
-
-
+
+
PROFESSOR OAK
@@ -85,6 +84,54 @@
+ +
+
+
+
+
+
PROFESSOR OAK
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
@@ -97,6 +144,7 @@ 0/100
+ ⚪ 0 🎵
@@ -104,16 +152,17 @@
- -
!
- + +
...
-
@@ -121,7 +170,7 @@
TRAINER
ASH
SCORE
0
STREAK
0
-
CORRECT
0/0
+
PARTY
0
@@ -129,11 +178,21 @@
-
👴
+
+
+
+
🐉
+
+
OAK
-
+
+
+
+
+
🔥
+
ASH
@@ -150,6 +209,28 @@
+ +
+
+
+
+
+
+
+
+
+
⚔️
+
+
+
+
+
+
+
+
+
+
+
@@ -158,6 +239,7 @@
+100 PTS
+
@@ -186,6 +268,7 @@
ASH

You've conquered SQL Town and mastered Microsoft T-SQL! Your SQL journey is just beginning — more towns and exams await!

+
🥉 SQL EXPLORER — Q25
🥈 SQL APPRENTICE — Q50
@@ -219,7 +302,7 @@
- + diff --git a/style.css b/style.css index 9d6451c..146dad2 100644 --- a/style.css +++ b/style.css @@ -110,7 +110,7 @@ html, body { @keyframes abPress { 0%{transform:scale(1)}40%{transform:scale(.82)}100%{transform:scale(1)} } @keyframes choiceCursor { 0%,100%{box-shadow:0 0 0 3px var(--yellow), 2px 2px 0 var(--darkgray)}50%{box-shadow:0 0 0 3px #ffe878, 2px 2px 0 var(--darkgray)} } @keyframes talkBubble { 0%{transform:translateY(0)}50%{transform:translateY(-3px)} } -@keyframes arrowPulse { 0%,100%{transform:scale(1) translateX(0);opacity:1} 50%{transform:scale(1.25) translateX(4px);opacity:.7} } +@keyframes arrowPulse { 0%,100%{transform:scale(1) translateX(0)} 50%{transform:scale(1.25) translateX(4px)} } .blink { animation: blink 1s step-end infinite; } @@ -414,6 +414,22 @@ html, body { #screen-intro { flex-direction:column-reverse; } .intro-dialog { margin:10px; min-height:90px; } .intro-npc { position:absolute; right:22%; bottom:30%; font-size:clamp(44px,13vw,64px); animation:npcFloat 2s ease-in-out infinite; filter:drop-shadow(2px 3px 0 rgba(0,0,0,.4)); } +.intro-npc.intro-npc-centered { + right:auto !important; + left:50% !important; + bottom:50% !important; + transform:translate(-50%, 50%); + font-size:clamp(80px,25vw,120px); + text-align:center; +} +.intro-npc.intro-npc-centered img { + height:clamp(120px,35vw,180px) !important; + margin:0 auto; + display:block; +} +.intro-oak-bg { + display:flex; align-items:center; justify-content:center; +} .intro-player { position: absolute; left: 22%; @@ -423,7 +439,7 @@ html, body { background-image: url('./player.png'); background-repeat: no-repeat; background-size: 768px 64px; - background-position-x: -32px; /* idle down frame */ + background-position-x: -32px; background-position-y: 0; image-rendering: pixelated; animation: bounce 1s ease-in-out infinite; @@ -479,6 +495,20 @@ html, body { z-index: 60; } .tree:nth-child(even) { } +.tall-grass { + position: absolute; + display: inline-block; + filter: drop-shadow(0 1px 0 rgba(0,0,0,.2)); + line-height: 1; + z-index: 5; + opacity: .85; + animation: grassSway 2s ease-in-out infinite alternate; +} +@keyframes grassSway { + 0% { transform: rotate(-3deg) scaleY(1); } + 50% { transform: rotate(2deg) scaleY(1.05); } + 100% { transform: rotate(-3deg) scaleY(1); } +} /* Directional nudge arrow */ .map-dir-arrow { position: absolute; @@ -488,6 +518,7 @@ html, body { pointer-events: none; z-index: 50; filter: drop-shadow(0 0 4px rgba(248,192,48,.6)); + transition: opacity .5s ease; display: none; bottom: -9999px; } @@ -496,6 +527,7 @@ html, body { .npc-bubble { font-size:14px; font-family:var(--font); color:var(--black); background:var(--yellow); border:2px solid var(--black); width:20px; height:20px; display:flex; align-items:center; justify-content:center; margin:0 auto 2px; animation:bounce .7s ease-in-out infinite; } .map-npc { font-size:clamp(36px,10vw,52px); filter:drop-shadow(2px 3px 0 rgba(0,0,0,.35)); cursor:pointer; animation:npcFloat 2s ease-in-out infinite; } .intro-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } +.intro-npc.intro-npc-centered img { height:clamp(120px,35vw,180px) !important; } .map-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } /* Player is fixed to the viewport centre horizontally; only Y moves */ /* "..." bubble above player when pressing talk away from NPC */ @@ -591,8 +623,31 @@ html, body { } .battle-scene::after { content:''; position:absolute; top:55%; left:0; right:0; height:2px; background:rgba(0,0,0,.18); } .battle-npc-side, .battle-player-side { display:flex; flex-direction:column; align-items:center; gap:4px; } -.battle-sprite { font-size:clamp(38px,11vw,56px); filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); animation:npcFloat 2s ease-in-out infinite; } -.battle-sprite img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } +.battle-npc-row, .battle-player-row { display:flex; align-items:flex-end; gap:2px; } +.battle-npc-trainer, .battle-player-trainer { + font-size:clamp(38px,11vw,56px); + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); + align-self:flex-end; + line-height:1; +} +.battle-npc-trainer img, .battle-player-trainer img { + height:clamp(56px,16vw,80px); + width:auto; + image-rendering:pixelated; + display:block; +} +.battle-player-trainer { + width:32px; + height:64px; + background-image:url('./player.png'); + background-repeat:no-repeat; + background-size:768px 64px; + background-position:-32px 0; + image-rendering:pixelated; + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); +} +.battle-sprite { font-size:clamp(14px,4vw,20px); filter:drop-shadow(1px 2px 0 rgba(0,0,0,.3)); animation:npcFloat 2s ease-in-out infinite; } +.battle-sprite img { height:clamp(18px,5vw,24px); width:auto; image-rendering:pixelated; display:block; } .battle-player-side .battle-sprite { animation:bounce 1s ease-in-out infinite; } /* Player sprite on battle screen — use sprite sheet facing down-idle */ .battle-player-sprite { @@ -703,7 +758,7 @@ html, body { .hidden { display:none !important; } /* ─── LANDSCAPE CONTROLLER LAYOUT ───────────────────────── */ -@media (orientation: landscape) and (max-height: 500px) { +@media (orientation: landscape) and (max-height: 600px) { html, body { align-items: flex-start; justify-content: flex-start; } #app { @@ -751,4 +806,426 @@ html, body { #gba-right .ab-a { top: 14%; right: 5%; transform: none; } .gba-mid-btns { flex-direction: column; gap: 5px; } .gba-mid-btn { min-width: 44px; width: 44px; font-size: 5px; padding: 6px 4px; } + + .intro-npc-centered img { height: clamp(80px, 25vh, 120px) !important; } + .intro-npc-centered { font-size: clamp(50px, 18vh, 80px); } + .starter-choices { gap: 6px; padding: 4px 6px; } + .starter-btn { padding: 6px 4px; } + .starter-btn .starter-emoji { font-size: clamp(24px, 8vh, 36px); } + .catch-wild-pokemon { font-size: clamp(40px, 14vh, 60px); } + .catch-scene { padding: 8px 12px 4px; } + .battle-anim-npc-sprite, .battle-anim-player-sprite { font-size: clamp(32px, 10vh, 48px); } + .battle-scene { min-height: clamp(70px, 18vh, 110px); } +} + +/* ─── STARTER SELECTION ──────────────────────────────────── */ +#screen-starter { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#c8b880 55%,#b09860 100%); + flex-direction:column; + justify-content:flex-start; +} +.starter-dialog { + margin:0 8px; + min-height:60px; + flex-shrink:0; +} +.starter-choices { + display:flex; + justify-content:center; + gap:clamp(6px,2vw,12px); + padding:8px 10px; + flex-shrink:0; +} +.starter-btn { + display:flex; + flex-direction:column; + align-items:center; + gap:6px; + padding:clamp(10px,3vw,16px) clamp(8px,2.5vw,14px); + background:var(--dialog-bg); + border:4px solid var(--dialog-bd); + cursor:pointer; + font-family:var(--font); + transition:transform .15s, box-shadow .15s; + box-shadow:4px 4px 0 rgba(0,0,0,.4); + flex:1; + max-width:130px; +} +.starter-btn:hover, +.starter-btn:active { + transform:translateY(-3px); + box-shadow:4px 7px 0 rgba(0,0,0,.4); + background:#fff8e0; +} +.starter-btn .starter-emoji { + font-size:clamp(32px,10vw,48px); + animation:npcFloat 2s ease-in-out infinite; + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); +} +.starter-btn .starter-name { + font-size:clamp(5.5px,1.6vw,7px); + color:var(--black); +} +.starter-btn .starter-type { + font-size:clamp(5px,1.3vw,6px); + color:var(--lightgray); +} + +/* ─── STARTER CONFIRM ────────────────────────────────────── */ +#screen-starter-confirm { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#c8b880 55%,#b09860 100%); + align-items:center; + justify-content:center; +} +.starter-confirm-bg { + display:flex; + flex-direction:column; + align-items:center; + gap:10px; + padding:16px; + width:100%; +} +.starter-chosen-emoji { + font-size:clamp(56px,18vw,80px); + animation:bounce 1s ease-in-out infinite; + filter:drop-shadow(3px 4px 0 rgba(0,0,0,.35)); +} +.starter-chosen-name { + font-size:clamp(11px,3.5vw,15px); + color:var(--black); + font-family:var(--font); + text-shadow:1px 1px 0 rgba(255,255,255,.6); +} +.starter-chosen-level { + font-size:clamp(7px,2vw,9px); + color:var(--gray); + font-family:var(--font); +} +.starter-confirm-dialog { + margin:0 12px; + width:calc(100% - 24px); +} +.starter-flee-text { + font-family:var(--font); + font-size:clamp(6px,1.7vw,7.5px); + color:var(--gray); + text-align:center; + line-height:2; + padding:0 16px; + animation:fadeIn .8s ease; +} + +/* ─── CATCH SCREEN ───────────────────────────────────────── */ +#screen-catch { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#60a840 55%,#408830 100%); + flex-direction:column; + justify-content:flex-start; +} +.catch-scene { + display:flex; + flex-direction:column; + align-items:center; + gap:6px; + padding:clamp(16px,5vw,28px) 16px 10px; + flex-shrink:0; +} +.catch-wild-pokemon { + font-size:clamp(56px,18vw,80px); + animation:npcFloat 2s ease-in-out infinite; + filter:drop-shadow(3px 4px 0 rgba(0,0,0,.35)); +} +.catch-wild-name { + font-family:var(--font); + font-size:clamp(9px,2.8vw,12px); + color:var(--black); + text-shadow:1px 1px 0 rgba(255,255,255,.6); +} +.catch-wild-level { + font-family:var(--font); + font-size:clamp(6.5px,1.8vw,8px); + color:var(--gray); +} +.catch-dialog { + margin:8px; + min-height:50px; + flex-shrink:0; +} +.catch-actions { + display:flex; + gap:8px; + padding:6px 12px; + justify-content:center; + flex-shrink:0; +} +.catch-action-btn { + flex:1; + max-width:160px; +} + +/* ─── CATCH RESULT ───────────────────────────────────────── */ +#screen-catch-result { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#60a840 55%,#408830 100%); + align-items:center; + justify-content:center; +} +.catch-result-bg { + display:flex; + flex-direction:column; + align-items:center; + gap:14px; + padding:20px; + width:100%; +} +.catch-result-emoji { + font-size:clamp(48px,15vw,72px); + filter:drop-shadow(3px 4px 0 rgba(0,0,0,.35)); +} +.catch-result-msg { + font-family:var(--font); + font-size:clamp(8px,2.5vw,11px); + color:var(--black); + text-align:center; + line-height:2; + text-shadow:1px 1px 0 rgba(255,255,255,.6); + padding:0 16px; +} +.catch-result-msg.success { color:var(--green); } +.catch-result-msg.fail { color:var(--red); } + +/* ─── BATTLE POKEMON LABELS ──────────────────────────────── */ +.battle-npc-pokemon-name, +.battle-player-pokemon-name { + font-family:var(--font); + font-size:clamp(5px,1.4vw,6.5px); + color:var(--black); + background:rgba(255,255,255,.7); + border:2px solid rgba(0,0,0,.2); + padding:1px 5px; + text-align:center; +} +.battle-npc-pokemon-level, +.battle-player-pokemon-level { + font-family:var(--font); + font-size:clamp(4.5px,1.2vw,5.5px); + color:var(--gray); + text-align:center; +} + +/* ─── BATTLE ANIMATION SCREEN ────────────────────────────── */ +#screen-battle-anim { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#c8b880 55%,#b09860 100%); + flex-direction:column; + justify-content:center; + align-items:center; +} +.battle-anim-scene { + display:flex; + justify-content:space-around; + align-items:center; + width:100%; + padding:10px 16px; + gap:8px; +} +.battle-anim-npc, +.battle-anim-player { + display:flex; + flex-direction:column; + align-items:center; + gap:6px; + flex:1; +} +.battle-anim-npc-name, +.battle-anim-player-name { + font-family:var(--font); + font-size:clamp(6px,1.8vw,8px); + color:var(--black); + text-shadow:1px 1px 0 rgba(255,255,255,.5); +} +.battle-anim-npc-sprite, +.battle-anim-player-sprite { + font-size:clamp(42px,13vw,64px); + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); + transition:opacity .6s ease, transform .6s ease; +} +.battle-anim-vs { + font-size:clamp(20px,6vw,32px); + animation:bounce .8s ease-in-out infinite; + filter:drop-shadow(2px 2px 0 rgba(0,0,0,.3)); +} +.battle-anim-hp-bar { + width:clamp(60px,20vw,100px); + height:10px; + background:var(--black); + border:2px solid var(--lightgray); + overflow:hidden; +} +.battle-anim-hp-fill { + height:100%; + width:100%; + background:var(--hp-green); + transition:width 1s ease; +} +.battle-anim-msg { + font-family:var(--font); + font-size:clamp(8px,2.5vw,11px); + color:var(--black); + text-align:center; + padding:12px 16px; + line-height:2; + text-shadow:1px 1px 0 rgba(255,255,255,.5); + min-height:40px; +} + +/* ─── WILD POKEMON ON MAP ────────────────────────────────── */ +.map-wild-wrap { + position:absolute; + text-align:center; + z-index:25; + cursor:pointer; +} +.wild-bubble { + font-size:12px; + font-family:var(--font); + color:var(--black); + background:var(--green); + border:2px solid var(--black); + width:18px; + height:18px; + display:flex; + align-items:center; + justify-content:center; + margin:0 auto 2px; + animation:bounce .7s ease-in-out infinite; + color:#fff; +} +.map-wild { + font-size:clamp(28px,8vw,40px); + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); + animation:npcFloat 2s ease-in-out infinite; +} + +/* ─── POKEBALL COUNT IN MAP HEADER ───────────────────────── */ +.pokeball-count { + font-size:clamp(6px,1.6vw,7.5px); + color:var(--white); + text-shadow:1px 1px 0 rgba(0,0,0,.5); + white-space:nowrap; +} + +/* ─── RESULT POKEBALLS REWARD ────────────────────────────── */ +.result-pokeballs { + font-size:clamp(7px,2vw,9px); + color:var(--blue); + text-shadow:1px 1px 0 rgba(0,0,0,.15); + animation:blueShimmer 2s ease-in-out infinite; +} + +/* ─── COMPLETE SCREEN PARTY ──────────────────────────────── */ +.complete-party-title { + font-size:clamp(9px,2.8vw,12px); + color:var(--yellow); + text-shadow:2px 2px 0 #604000; + margin-top:8px; +} +.complete-party-grid { + display:grid; + grid-template-columns:repeat(auto-fill, minmax(80px, 1fr)); + gap:6px; + width:100%; + max-width:340px; +} +.complete-party-entry { + display:flex; + flex-direction:column; + align-items:center; + gap:2px; + background:rgba(255,255,255,.06); + border:2px solid rgba(248,192,48,.25); + padding:6px 4px; + font-family:var(--font); +} +.complete-party-entry .party-emoji { + font-size:clamp(20px,6vw,28px); +} +.complete-party-entry .party-name { + font-size:clamp(4.5px,1.2vw,5.5px); + color:var(--white); +} +.complete-party-entry .party-level { + font-size:clamp(4px,1vw,5px); + color:var(--lightgray); +} + +/* ─── POKEBALL SHAKE ANIMATION ───────────────────────────── */ +@keyframes pokeballShake { + 0%,100% { transform:rotate(0deg); } + 20% { transform:rotate(-15deg); } + 40% { transform:rotate(15deg); } + 60% { transform:rotate(-10deg); } + 80% { transform:rotate(10deg); } +} +.pokeball-shaking { + animation:pokeballShake .6s ease-in-out; + display:inline-block; +} + +/* ─── CATCH SUCCESS FLASH ────────────────────────────────── */ +@keyframes catchFlash { + 0% { background:rgba(255,255,255,0); } + 50% { background:rgba(255,255,255,.6); } + 100% { background:rgba(255,255,255,0); } +} +.catch-flash { + animation:catchFlash .4s ease; +} + +/* ─── CATCH RESULT TEXT SUB-STYLES ───────────────────────── */ +.catch-success-title { + font-size:clamp(12px,4vw,16px); + color:var(--green); + text-shadow:2px 2px 0 #003010; + margin-bottom:4px; +} +.catch-fail-title { + font-size:clamp(12px,4vw,16px); + color:var(--red); + text-shadow:2px 2px 0 #400010; + margin-bottom:4px; +} +.catch-success-detail, +.catch-fail-detail { + font-size:clamp(7px,2vw,9px); + line-height:2; +} +.catch-success-sub { + font-size:clamp(6px,1.6vw,7.5px); + color:var(--green); + margin-top:4px; +} +.catch-fail-sub { + font-size:clamp(6px,1.6vw,7.5px); + color:var(--lightgray); + margin-top:4px; +} + +/* ─── STARTER / CATCH CURSOR SELECTION ───────────────────── */ +.starter-btn-selected { + outline:3px solid var(--yellow); + outline-offset:-2px; + background:#fff8e0; + transform:translateY(-3px); + box-shadow:4px 7px 0 rgba(0,0,0,.4); +} +.catch-btn-selected { + outline:3px solid var(--yellow); + outline-offset:-2px; +} + +/* ─── MAP ROCKS ──────────────────────────────────────────── */ +.map-rock { + position: absolute; + display: inline-block; + filter: drop-shadow(2px 2px 0 rgba(0,0,0,.4)); + line-height: 1; + z-index: 15; }