diff --git a/README.md b/README.md index 9dc5a3d..dcbe800 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ๐ŸŽฎ PokรฉSQL โ€” Learn T-SQL -A Pokรฉmon-style Game Boy Advance browser game for learning **Microsoft T-SQL**! +A Pokรฉmon-style Game Boy Advance browser game for learning **Microsoft T-SQL**! Play it on your phone, tablet, or PC โ€” progress is saved automatically to your browser. ## ๐Ÿš€ Play It Live @@ -14,7 +14,7 @@ Play it on your phone, tablet, or PC โ€” progress is saved automatically to your pokesql/ โ”œโ”€โ”€ index.html โ† Game screens & structure โ”œโ”€โ”€ style.css โ† GBA Pokรฉmon aesthetic, fully responsive -โ”œโ”€โ”€ game.js โ† Game engine + localStorage save system +โ”œโ”€โ”€ game.js โ† Game engine, controller, walking, save system โ”œโ”€โ”€ questions.json โ† 100 T-SQL questions & answers โ””โ”€โ”€ README.md ``` @@ -32,15 +32,50 @@ No server, no database, no cost. --- +## ๐ŸŽฎ GBA Controller Overlay + +On mobile, a semi-transparent Game Boy Advance-style controller overlays the bottom of the screen: + +| Control | Action | +|---|---| +| **D-pad** | Walk the player around the map | +| **A button** (red) | Talk to NPC on map ยท Confirm answer in battle ยท Advance dialog | +| **B button** (blue) | Talk to NPC on map ยท Advance dialog | +| **SELECT / START** | Decorative โ€” reserved for future use | + +The controller is hidden on desktop (โ‰ฅ900px wide) โ€” use the keyboard instead. + +--- + +## โŒจ๏ธ PC Keyboard Controls + +| Key | Action | +|---|---| +| **WASD / Arrow keys** | Walk player on map | +| **E / Enter** | Talk to NPC ยท Confirm answer ยท Advance dialog | +| **X** | Talk to NPC ยท Advance dialog | +| **1 / 2 / 3 / 4** | Quick-pick answer directly | +| **Space** | Advance dialog | + +--- + +## ๐Ÿ—บ๏ธ Map & Walking + +- The player sprite walks freely around the overworld map using the D-pad or WASD +- Walking animations change based on direction (left, right, up, down) +- Press **A**, **B**, **E**, or **Enter** at any time to talk to the NPC and start the question +- Tapping the NPC sprite directly also triggers the conversation + +--- + ## ๐Ÿ’พ Save System Progress is stored in the player's browser (`localStorage`) automatically: -- **Auto-saves** after every answered question and on map return -- **Continue screen** appears on next visit if an in-progress save exists -- Displays trainer name, question progress, and score on the continue screen -- **New Game** option is always available (prompts confirmation to prevent accidents) -- Save is cleared on game completion โ€” ready for a fresh run +- **Auto-saves** after every answered question and on map return โ€” a ๐Ÿ’พ icon flashes to confirm +- **Continue screen** appears on next visit showing trainer name, progress %, and score +- **New Game** option always available โ€” prompts confirmation before erasing save +- Save clears automatically on completion so the next run starts fresh --- @@ -48,16 +83,20 @@ Progress is stored in the player's browser (`localStorage`) automatically: | Feature | Detail | |---|---| -| GBA Pokรฉmon aesthetic | Press Start 2P font, pixel dialog boxes, scanline overlay | -| Mobile-first design | Full-screen on phones, framed device on desktop | -| 100 T-SQL questions | Progressive difficulty, multiple choice | +| GBA controller overlay | D-pad + A/B buttons, semi-transparent, mobile only | +| Walking player | Moves freely on map, directional walk animations | +| Mobile-first design | Full-screen on phones with safe-area support, framed on desktop | +| localStorage save | Auto-save with continue screen on return visits | +| 100 T-SQL questions | Progressive difficulty, shuffled answer order every time | +| Randomised answer positions | Correct answer shuffled to a random slot โ€” no pattern to exploit | +| Numbered answers | Options labelled 1/2/3/4 (not A/B/C/D) | | Name entry | On-screen pixel keyboard, just like the real game | -| Typewriter text | Authentic dialog effect with tap-to-skip | +| Typewriter text | Authentic dialog effect, tap/press to skip | | 22 NPC characters | Professor Oak, Misty, Brock, Giovanni, Lanceโ€ฆ | -| Streak bonuses | 3ร— = +150 pts ๐Ÿ”ฅ, 5ร—+ = +200 pts ๐Ÿ”ฅ | +| Streak bonuses | 3ร— correct = +150 pts ๐Ÿ”ฅ, 5ร—+ = +200 pts ๐Ÿ”ฅ | | Milestone badges | Earned at Q25, Q50, Q75, Q100 | -| Correct answer reveal | Wrong answers show the right answer + explanation | -| Keyboard shortcuts | 1โ€“4 / Aโ€“D to answer, Enter to continue | +| Correct answer reveal | Wrong answers show the right answer + full explanation | +| Scanline overlay | Authentic CRT/GBA screen effect | --- @@ -90,7 +129,7 @@ Edit `questions.json`, add to the `questions` array inside `levels[0]`: } ``` -`answer` is the **0-based index** of the correct option (0 = A, 1 = B, 2 = C, 3 = D). +`answer` is the **0-based index** of the correct option in the `options` array (0 = first option, 1 = second, etc.). The game shuffles the display order randomly at runtime, so the position shown to the player will vary each time. --- @@ -111,7 +150,7 @@ Then reference `"npc": "Your NPC Name"` in `questions.json`. ## ๐Ÿ“ฑ Mobile Tips -- Add to Home Screen on iOS/Android for a full-screen app experience -- The game uses `100dvh` and `safe-area-inset` for notched phones -- Tap anywhere on the dialog during intro to skip typewriter / advance -- Tap the NPC sprite on the map to start the question (same as the button) +- Add to Home Screen on iOS/Android for a full-screen app-like experience +- The game uses `100dvh` and `env(safe-area-inset-bottom)` for notched phones (iPhone X+) +- Tap anywhere on the dialog box during the intro to skip the typewriter and advance +- Tap the NPC sprite directly on the map as an alternative to pressing A/B diff --git a/game.js b/game.js index 160cc30..0eb6a7d 100644 --- a/game.js +++ b/game.js @@ -26,161 +26,381 @@ const NPC = { }; const INTRO_MSGS = [ - { speaker: 'PROFESSOR OAK', npc: 'Professor Oak', text: "Hello {name}! Welcome to PokรฉSQL โ€” the world of T-SQL training! My name is Professor Oak, the SQL Pokรฉmon Professor!" }, - { speaker: 'PROFESSOR OAK', npc: 'Professor Oak', text: "This world is inhabited by powerful data โ€” stored in TABLES, organised in DATABASES, and tamed with QUERIES!" }, - { speaker: 'PROFESSOR OAK', npc: 'Professor Oak', text: "Your mission? Journey through Pallet Town, challenge SQL experts, and become a T-SQL Champion!" }, - { speaker: 'PROFESSOR OAK', npc: 'Professor Oak', text: "You'll learn Microsoft T-SQL โ€” the language used by DBAs and data professionals worldwide." }, - { speaker: 'PROFESSOR OAK', npc: 'Professor Oak', text: "100 questions await you in this town alone. Each correct answer earns you SQL EXP โ€” and you'll learn something real!" }, - { speaker: 'PROFESSOR OAK', npc: 'Professor Oak', text: "Your progress is saved automatically in your browser so you can pick up right where you left off. Now, {name}... your adventure begins!" }, + { speaker:'PROFESSOR OAK', npc:'Professor Oak', text:"Hello {name}! Welcome to PokรฉSQL โ€” the world of T-SQL training! My name is Professor Oak, the SQL Pokรฉmon Professor!" }, + { speaker:'PROFESSOR OAK', npc:'Professor Oak', text:"This world is inhabited by powerful data โ€” stored in TABLES, organised in DATABASES, and tamed with QUERIES!" }, + { speaker:'PROFESSOR OAK', npc:'Professor Oak', text:"Your mission? Journey through Pallet Town, challenge SQL experts, and become a T-SQL Champion!" }, + { speaker:'PROFESSOR OAK', npc:'Professor Oak', text:"You'll learn Microsoft T-SQL โ€” the language used by DBAs and data professionals worldwide." }, + { speaker:'PROFESSOR OAK', npc:'Professor Oak', text:"100 questions await you in this town alone. Each correct answer earns you SQL EXP โ€” and you'll learn something real!" }, + { speaker:'PROFESSOR OAK', npc:'Professor Oak', text:"Your progress is saved automatically in your browser so you can pick up right where you left off. Now, {name}... your adventure begins!" }, ]; 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 MILESTONES = { - 25: { badge: '๐Ÿฅ‰ SQL INITIATE', stars: 'โ˜… โ˜… โ˜† โ˜†' }, - 50: { badge: '๐Ÿฅˆ QUERY TRAINER', stars: 'โ˜… โ˜… โ˜… โ˜†' }, - 75: { badge: '๐Ÿฅ‡ DATA WRANGLER', stars: 'โ˜… โ˜… โ˜… โ˜…' }, + 25: { badge:'๐Ÿฅ‰ SQL INITIATE', stars:'โ˜… โ˜… โ˜† โ˜†' }, + 50: { badge:'๐Ÿฅˆ QUERY TRAINER', stars:'โ˜… โ˜… โ˜… โ˜†' }, + 75: { badge:'๐Ÿฅ‡ DATA WRANGLER', stars:'โ˜… โ˜… โ˜… โ˜…' }, }; const SAVE_KEY = 'pokesql_save_v1'; /* โ”€โ”€โ”€ SAVE / LOAD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -function saveGame(state) { - try { - const data = { - playerName: state.playerName, - currentQ: state.currentQ, - score: state.score, - streak: state.streak, - maxStreak: state.maxStreak, - correct: state.correct, - wrong: state.wrong, - savedAt: Date.now(), - }; - localStorage.setItem(SAVE_KEY, JSON.stringify(data)); - } catch (_) {} +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, savedAt:Date.now() })); } 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(_){} } -function loadGame() { - try { - const raw = localStorage.getItem(SAVE_KEY); - if (!raw) return null; - return JSON.parse(raw); - } catch (_) { return null; } -} - -function clearSave() { - try { localStorage.removeItem(SAVE_KEY); } catch (_) {} -} - -/* โ”€โ”€โ”€ GAME CLASS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +/* โ”€โ”€โ”€ GAME โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ class Game { constructor() { this.state = { - screen: 'boot', - playerName: '', - questions: [], - currentQ: 0, - score: 0, - streak: 0, - maxStreak: 0, - correct: 0, - wrong: 0, - answering: false, - introStep: 0, - twTimer: null, + screen:'boot', playerName:'', questions:[], currentQ:0, + score:0, streak:0, maxStreak:0, correct:0, wrong:0, + answering:false, introStep:0, twTimer:null, + /* map movement */ + playerX:15, playerY:42, /* % of map-world */ + npcX:72, npcY:38, + moving:false, walkDir:'', + /* battle cursor */ + cursor:0, }; this.screens = {}; document.querySelectorAll('.screen').forEach(s => { - this.screens[s.id.replace('screen-', '')] = s; + this.screens[s.id.replace('screen-','')] = s; }); - this._introClickHandler = null; - this._introKeyHandler = null; + this._heldKeys = new Set(); + this._walkLoop = null; + this._introClick = null; + this._introKey = null; - this.bindGlobalKeys(); + this._bindController(); + this._bindGlobalKeys(); this.boot(); } - /* โ”€โ”€ BOOT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + CONTROLLER OVERLAY โ€” wires D-pad, A, B, SELECT, START + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ + _bindController() { + const ctrl = document.getElementById('gba-controller'); + + const dpadMap = { 'dp-up':'up', 'dp-down':'down', 'dp-left':'left', 'dp-right':'right' }; + + Object.entries(dpadMap).forEach(([id, dir]) => { + const btn = document.getElementById(id); + if (!btn) return; + + const start = (e) => { e.preventDefault(); this._dpadStart(dir); btn.classList.add('pressed'); }; + const end = (e) => { e.preventDefault(); this._dpadEnd(dir); btn.classList.remove('pressed'); }; + + btn.addEventListener('pointerdown', start, {passive:false}); + btn.addEventListener('pointerup', end, {passive:false}); + btn.addEventListener('pointerleave', end, {passive:false}); + btn.addEventListener('pointercancel',end, {passive:false}); + }); + + const aBtn = document.getElementById('btn-a'); + const bBtn = document.getElementById('btn-b'); + + aBtn.addEventListener('pointerdown', (e)=>{ e.preventDefault(); aBtn.classList.add('pressed'); this._pressA(); }); + aBtn.addEventListener('pointerup', (e)=>{ e.preventDefault(); aBtn.classList.remove('pressed'); }); + aBtn.addEventListener('pointercancel',(e)=>{ aBtn.classList.remove('pressed'); }); + + bBtn.addEventListener('pointerdown', (e)=>{ e.preventDefault(); bBtn.classList.add('pressed'); this._pressB(); }); + bBtn.addEventListener('pointerup', (e)=>{ e.preventDefault(); bBtn.classList.remove('pressed'); }); + bBtn.addEventListener('pointercancel',(e)=>{ bBtn.classList.remove('pressed'); }); + + document.getElementById('btn-start').addEventListener('pointerdown', (e)=>{ e.preventDefault(); }); + 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 === '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; } + if (s === 'battle' && this.state.answering) { return; } + if (s === 'intro') { this.advanceIntro(); return; } + 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); } + } + + _dpadEnd(dir) { + this._heldKeys.delete(dir); + 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(), 80); + } + + _stopWalkLoop() { + clearInterval(this._walkLoop); + this._walkLoop = null; + if (this.state.screen === 'map') { + const p = document.getElementById('map-player'); + if (p) { p.className = 'map-player idle'; } + } + } + + _walkTick() { + if (this.state.screen !== 'map') return; + + let dx = 0, dy = 0, dir = ''; + if (this._heldKeys.has('up')) { dy = 1.8; dir = 'up'; } + if (this._heldKeys.has('down')) { dy = -1.8; dir = 'down'; } + if (this._heldKeys.has('left')) { dx = -2.2; dir = 'left'; } + if (this._heldKeys.has('right')) { dx = 2.2; dir = 'right'; } + if (!dir) return; + + let nx = Math.max(5, Math.min(88, this.state.playerX + dx)); + let ny = Math.max(30, Math.min(72, this.state.playerY + dy)); + + this.state.playerX = nx; + this.state.playerY = ny; + + const p = document.getElementById('map-player'); + if (p) { + p.style.left = nx + '%'; + p.style.bottom = ny + '%'; + p.className = `map-player walk-${dir}`; + } + + this._checkNPCProximity(); + } + + _checkNPCProximity() { + const dx = Math.abs(this.state.playerX - this.state.npcX); + const dy = Math.abs(this.state.playerY - this.state.npcY); + const near = dx < 12 && dy < 12; + + const hint = document.getElementById('map-talk-hint'); + const bub = document.getElementById('npc-bubble'); + if (hint) { + hint.style.display = near ? 'block' : 'none'; + hint.style.left = (this.state.playerX + 4) + '%'; + hint.style.bottom= (this.state.playerY + 11) + '%'; + } + if (bub) bub.style.opacity = near ? '0' : '1'; + } + + _nearNPC() { + const dx = Math.abs(this.state.playerX - this.state.npcX); + const dy = Math.abs(this.state.playerY - this.state.npcY); + return dx < 12 && dy < 12; + } + + _talkToNPC() { this.startQuestion(); } + + /* โ”€โ”€ Position player & NPC on map โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + _placeMapSprites() { + const p = document.getElementById('map-player'); + if (p) { + p.style.left = this.state.playerX + '%'; + p.style.bottom = this.state.playerY + '%'; + p.className = 'map-player idle'; + } + const npcWrap = document.getElementById('map-npc-wrap'); + if (npcWrap) { + npcWrap.style.left = (this.state.npcX - 3) + '%'; + npcWrap.style.bottom = this.state.npcY + '%'; + } + const hint = document.getElementById('map-talk-hint'); + 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)'); + if (!btns.length) return; + let c = this.state.cursor; + if (dir === 'right') c = (c === 0) ? 1 : (c === 2) ? 3 : c; + else if (dir === 'left') c = (c === 1) ? 0 : (c === 3) ? 2 : c; + else if (dir === 'down') c = (c === 0) ? 2 : (c === 1) ? 3 : c; + else if (dir === 'up') c = (c === 2) ? 0 : (c === 3) ? 1 : c; + c = Math.min(c, btns.length - 1); + this.state.cursor = c; + this._renderCursor(); + } + + _renderCursor() { + document.querySelectorAll('.choice-btn').forEach((b,i) => { + b.classList.toggle('cursor', i === this.state.cursor); + }); + } + + _confirmCursor() { + if (this.state.answering) return; + const btns = document.querySelectorAll('.choice-btn:not(.disabled)'); + const target = btns[this.state.cursor]; + if (target) target.click(); + } + + /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + GLOBAL KEYBOARD + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ + _bindGlobalKeys() { + const DPAD_KEYS = { + 'ArrowUp':'up','ArrowDown':'down','ArrowLeft':'left','ArrowRight':'right', + 'w':'up','s':'down','a':'left','d':'right', + 'W':'up','S':'down','A':'left','D':'right', + }; + + 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(); + else if (e.key.length===1 && /[A-Z0-9.\-!?]/i.test(e.key)) this.addChar(e.key.toUpperCase()); + return; + } + + /* Title */ + if (s === 'title' && (e.key==='Enter'||e.key===' ')) { document.getElementById('screen-title').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 (DPAD_KEYS[e.key] && !e.repeat) { + const dir = DPAD_KEYS[e.key]; + if ((s==='map' || s==='battle') && !this._heldKeys.has(dir)) { + e.preventDefault(); + this._heldKeys.add(dir); + if (s==='map' && !this._walkLoop) this._startWalkLoop(); + if (s==='battle') this._moveCursor(dir); + } + 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) { + this.state.cursor = numMap[e.key]; + this._renderCursor(); + setTimeout(()=>this._confirmCursor(), 80); + } + } + }); + + document.addEventListener('keyup', (e) => { + const DPAD_KEYS = { + 'ArrowUp':'up','ArrowDown':'down','ArrowLeft':'left','ArrowRight':'right', + 'w':'up','s':'down','a':'left','d':'right', + 'W':'up','S':'down','A':'left','D':'right', + }; + const dir = DPAD_KEYS[e.key]; + if (dir) { + this._heldKeys.delete(dir); + if (this._heldKeys.size === 0) this._stopWalkLoop(); + } + }); + } + + /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + CONTROLLER VISIBILITY + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ + _showController(visible) { + const ctrl = document.getElementById('gba-controller'); + if (!ctrl) return; + if (visible) ctrl.classList.remove('hidden'); + else ctrl.classList.add('hidden'); + } + + /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + SCREENS + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ boot() { this.show('boot'); + this._showController(false); setTimeout(() => this.showTitle(), 1800); } showTitle() { this.show('title'); - const go = () => { - document.getElementById('screen-title').removeEventListener('click', go); - this.showContinueOrName(); - }; + this._showController(false); + const go = () => { document.getElementById('screen-title').removeEventListener('click',go); this.showContinueOrName(); }; document.getElementById('screen-title').addEventListener('click', go); } - /* โ”€โ”€ CONTINUE SCREEN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showContinueOrName() { const save = loadGame(); if (save && save.playerName && save.currentQ > 0 && save.currentQ < 100) { this.show('continue'); - this.populateContinueScreen(save); + this._showController(false); + const pct = Math.round((save.currentQ/100)*100); + document.getElementById('save-name-disp').textContent = save.playerName; + document.getElementById('save-prog-disp').textContent = `Q${save.currentQ}/100 (${pct}%)`; + document.getElementById('save-score-disp').textContent = save.score.toLocaleString(); - document.getElementById('btn-continue-save').onclick = () => { - this.restoreFromSave(save); - }; + document.getElementById('btn-continue-save').onclick = () => this.restoreFromSave(save); document.getElementById('btn-new-game').onclick = () => { - document.getElementById('continue-warn').textContent = - 'Starting a new game will erase your saved progress!'; - document.getElementById('btn-new-game').textContent = 'โœฆ CONFIRM NEW GAME'; - document.getElementById('btn-new-game').onclick = () => { - clearSave(); - this.startFresh(); - }; + document.getElementById('continue-warn').textContent = 'Starting a new game will erase your saved progress!'; + document.getElementById('btn-new-game').textContent = 'โœฆ CONFIRM NEW GAME'; + document.getElementById('btn-new-game').onclick = () => { clearSave(); this.startFresh(); }; }; } else { this.startFresh(); } } - populateContinueScreen(save) { - const pct = Math.round((save.currentQ / 100) * 100); - document.getElementById('save-name-disp').textContent = save.playerName; - document.getElementById('save-prog-disp').textContent = `Q${save.currentQ}/100 (${pct}%)`; - document.getElementById('save-score-disp').textContent = save.score.toLocaleString(); - } - restoreFromSave(save) { - Object.assign(this.state, { - playerName: save.playerName, - currentQ: save.currentQ, - score: save.score, - streak: save.streak, - maxStreak: save.maxStreak, - correct: save.correct, - wrong: save.wrong, - }); + Object.assign(this.state, { playerName:save.playerName, currentQ:save.currentQ, score:save.score, streak:save.streak, maxStreak:save.maxStreak, correct:save.correct, wrong:save.wrong }); this.loadQuestions(() => this.showMap()); } startFresh() { - this.state.playerName = ''; - this.state.currentQ = 0; - this.state.score = 0; - this.state.streak = 0; - this.state.maxStreak = 0; - this.state.correct = 0; - this.state.wrong = 0; + Object.assign(this.state, { playerName:'', currentQ:0, score:0, streak:0, maxStreak:0, correct:0, wrong:0 }); this.showNameEntry(); } - /* โ”€โ”€ NAME ENTRY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* โ”€โ”€ NAME โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showNameEntry() { this.show('name'); + this._showController(false); this.buildKeyboard(); - this.typeText('name-prompt-text', - "Hello there! Welcome to PokรฉSQL! My name is Professor Oak โ€” the SQL Pokรฉmon Professor. Now tell me, what is your name?"); - document.getElementById('btn-backspace').onclick = () => this.delChar(); + this.typeText('name-prompt-text', "Hello there! Welcome to PokรฉSQL! My name is Professor Oak โ€” the SQL Pokรฉmon Professor. Now tell me, what is your name?"); + document.getElementById('btn-backspace').onclick = () => this.delChar(); document.getElementById('btn-confirm-name').onclick = () => this.confirmName(); } @@ -190,50 +410,32 @@ class Game { grid.innerHTML = ''; chars.forEach(ch => { const b = document.createElement('button'); - b.className = 'key-btn'; - b.textContent = ch; - b.addEventListener('pointerdown', (e) => { e.preventDefault(); this.addChar(ch); }); + b.className = 'key-btn'; b.textContent = ch; + b.addEventListener('pointerdown', (e)=>{ e.preventDefault(); this.addChar(ch); }); grid.appendChild(b); }); } - addChar(ch) { - if (this.state.playerName.length >= 10) return; - this.state.playerName += ch; - this.refreshNameDisplay(); - } - - delChar() { - this.state.playerName = this.state.playerName.slice(0, -1); - this.refreshNameDisplay(); - } - + addChar(ch) { if (this.state.playerName.length<10){ this.state.playerName+=ch; this.refreshNameDisplay(); } } + delChar() { this.state.playerName=this.state.playerName.slice(0,-1); this.refreshNameDisplay(); } refreshNameDisplay() { - const el = document.getElementById('name-display'); - el.textContent = this.state.playerName + (this.state.playerName.length < 10 ? '_' : ''); + const el=document.getElementById('name-display'); + if(el) el.textContent=this.state.playerName+(this.state.playerName.length<10?'_':''); } + confirmName() { if(!this.state.playerName.trim()) this.state.playerName='ASH'; this.showIntro(); } - confirmName() { - if (!this.state.playerName.trim()) this.state.playerName = 'ASH'; - this.showIntro(); - } - - /* โ”€โ”€ INTRO CUTSCENE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* โ”€โ”€ INTRO โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showIntro() { this.show('intro'); + this._showController(false); this.state.introStep = 0; this.renderIntroMsg(); const advance = () => this.advanceIntro(); const introEl = document.getElementById('screen-intro'); - - if (this._introClickHandler) introEl.removeEventListener('click', this._introClickHandler); - this._introClickHandler = advance; + if (this._introClick) introEl.removeEventListener('click', this._introClick); + this._introClick = advance; introEl.addEventListener('click', advance); - - if (this._introKeyHandler) document.removeEventListener('keydown', this._introKeyHandler); - this._introKeyHandler = (e) => { if (e.key === 'Enter' || e.key === ' ') advance(); }; - document.addEventListener('keydown', this._introKeyHandler); } renderIntroMsg() { @@ -244,24 +446,23 @@ class Game { document.getElementById('intro-speaker').textContent = msg.speaker; document.getElementById('intro-arrow').style.display = 'none'; this.typeText('intro-text', - msg.text.replace(/{name}/g, this.state.playerName || 'ASH'), - () => { document.getElementById('intro-arrow').style.display = 'block'; } + msg.text.replace(/{name}/g, this.state.playerName||'ASH'), + ()=>{ document.getElementById('intro-arrow').style.display='block'; } ); } advanceIntro() { if (this.state.twTimer) { - clearInterval(this.state.twTimer); - this.state.twTimer = null; + clearInterval(this.state.twTimer); this.state.twTimer=null; const msg = INTRO_MSGS[this.state.introStep]; - document.getElementById('intro-text').textContent = - msg.text.replace(/{name}/g, this.state.playerName || 'ASH'); + document.getElementById('intro-text').textContent = msg.text.replace(/{name}/g, this.state.playerName||'ASH'); document.getElementById('intro-arrow').style.display = 'block'; return; } this.state.introStep++; if (this.state.introStep >= INTRO_MSGS.length) { - document.removeEventListener('keydown', this._introKeyHandler); + const el = document.getElementById('screen-intro'); + if (this._introClick) el.removeEventListener('click', this._introClick); this.loadQuestions(() => this.showMap()); return; } @@ -270,271 +471,211 @@ class Game { /* โ”€โ”€ LOAD QUESTIONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ loadQuestions(cb) { - if (this.state.questions.length > 0) { cb(); return; } + if (this.state.questions.length>0) { cb(); return; } fetch('questions.json') - .then(r => r.json()) - .then(data => { - this.state.questions = data.levels[0].questions; - cb(); - }) - .catch(() => this.toast('โŒ Could not load questions.json')); + .then(r=>r.json()) + .then(data=>{ this.state.questions=data.levels[0].questions; cb(); }) + .catch(()=>this.toast('โŒ Could not load questions.json')); } /* โ”€โ”€ MAP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showMap() { this.show('map'); - document.getElementById('hud-name').textContent = this.state.playerName || 'ASH'; + this._showController(true); + + /* reset player to start if fresh (currentQ 0), else keep position */ + if (this.state.currentQ === 0) { + this.state.playerX = 15; this.state.playerY = 42; + } + this.state.npcX = 72; this.state.npcY = 38; + + document.getElementById('hud-name').textContent = this.state.playerName||'ASH'; this.updateHUD(); - this.updateMapNPC(); + + const q = this.state.questions[this.state.currentQ]; + const npcEl = document.getElementById('map-npc'); + if (q && npcEl) npcEl.textContent = NPC[q.npc]||'๐Ÿง‘'; + + this._placeMapSprites(); saveGame(this.state); this.flashSaveDot(); - document.getElementById('btn-walk').onclick = () => this.startQuestion(); - const mapNpc = document.getElementById('map-npc'); - if (mapNpc) mapNpc.onclick = () => this.startQuestion(); - } - updateMapNPC() { - const q = this.state.questions[this.state.currentQ]; - if (!q) return; - const el = document.getElementById('map-npc'); - if (el) el.textContent = NPC[q.npc] || '๐Ÿง‘'; + /* click NPC directly also triggers talk */ + const npcEl2 = document.getElementById('map-npc'); + if (npcEl2) npcEl2.onclick = ()=>this._talkToNPC(); } updateHUD() { - const q = this.state.questions; - const tot = q.length || 100; - const done = this.state.currentQ; - const 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 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}`; 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 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'; } } flashSaveDot() { - const dot = document.getElementById('save-dot'); - if (!dot) return; - dot.style.opacity = '1'; - setTimeout(() => { dot.style.opacity = '.4'; }, 1500); + const dot=document.getElementById('save-dot'); + if(!dot)return; dot.style.opacity='1'; + setTimeout(()=>{ dot.style.opacity='.4'; },1500); } - /* โ”€โ”€ BATTLE / QUESTION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* โ”€โ”€ BATTLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ startQuestion() { - const q = this.state.questions[this.state.currentQ]; - if (!q) { this.showComplete(); return; } + this._stopWalkLoop(); + this._heldKeys.clear(); + const q=this.state.questions[this.state.currentQ]; + if(!q){ this.showComplete(); return; } this.show('battle'); - this.state.answering = false; + this._showController(true); + this.state.answering=false; + this.state.cursor=0; - document.getElementById('battle-npc-sprite').textContent = NPC[q.npc] || '๐Ÿง‘'; + document.getElementById('battle-npc-sprite').textContent = NPC[q.npc]||'๐Ÿง‘'; document.getElementById('battle-npc-name').textContent = q.npc.toUpperCase(); document.getElementById('battle-speaker').textContent = q.npc.toUpperCase(); - document.getElementById('battle-player-name').textContent= (this.state.playerName || 'ASH').toUpperCase(); - document.getElementById('battle-q-num').textContent = this.state.currentQ + 1; + document.getElementById('battle-player-name').textContent= (this.state.playerName||'ASH').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'; document.getElementById('battle-choices').innerHTML = ''; - this.typeText('battle-text', q.text, () => this.renderChoices(q)); + this.typeText('battle-text', q.text, ()=>this.renderChoices(q)); } renderChoices(q) { - const container = document.getElementById('battle-choices'); - container.innerHTML = ''; - ['A','B','C','D'].forEach((letter, i) => { - const btn = document.createElement('button'); - btn.className = 'choice-btn'; - btn.setAttribute('data-letter', letter); - btn.textContent = q.options[i]; - btn.addEventListener('click', () => this.pick(i, q, container)); + const container=document.getElementById('battle-choices'); + container.innerHTML=''; + this.state.cursor=0; + + /* Shuffle options so the correct answer lands in a random slot each time. + We track which shuffled index holds the correct answer. */ + const indices = [0,1,2,3]; + for (let i = indices.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [indices[i], indices[j]] = [indices[j], indices[i]]; + } + const correctShuffledIdx = indices.indexOf(q.answer); + + indices.forEach((origIdx, slotIdx) => { + const btn=document.createElement('button'); + btn.className='choice-btn'; + btn.setAttribute('data-letter', slotIdx + 1); + btn.textContent=q.options[origIdx]; + btn.addEventListener('click',()=>this.pick(slotIdx, correctShuffledIdx, container)); container.appendChild(btn); }); + this._renderCursor(); } - pick(chosen, q, container) { - if (this.state.answering) return; - this.state.answering = true; + pick(chosen, correctIdx, container) { + if(this.state.answering)return; + this.state.answering=true; - const btns = container.querySelectorAll('.choice-btn'); - const correct = chosen === q.answer; + const btns=container.querySelectorAll('.choice-btn'); + const correct=chosen===correctIdx; - btns.forEach(b => b.classList.add('disabled')); - btns[q.answer].classList.remove('disabled'); - btns[q.answer].classList.add('correct'); - if (!correct) btns[chosen].classList.add('wrong'); + btns.forEach(b=>{ b.classList.remove('cursor'); b.classList.add('disabled'); }); + btns[correctIdx].classList.remove('disabled'); btns[correctIdx].classList.add('correct'); + if(!correct) btns[chosen].classList.add('wrong'); - if (correct) { - this.state.streak++; - this.state.correct++; - this.state.maxStreak = Math.max(this.state.maxStreak, this.state.streak); - const bonus = this.state.streak >= 5 ? 200 : this.state.streak >= 3 ? 150 : 100; - this.state.score += bonus; - } else { - this.state.streak = 0; - this.state.wrong++; - } + if(correct){ + 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; + } else { this.state.streak=0; this.state.wrong++; } - setTimeout(() => this.showResult(correct, q), 850); + setTimeout(()=>this.showResult(correct,q),850); } /* โ”€โ”€ RESULT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - showResult(correct, q) { + showResult(correct,q) { this.show('result'); + this._showController(false); - const icon = document.getElementById('result-icon'); - const label = document.getElementById('result-label'); - const ansEl = document.getElementById('result-correct-ans'); - const expEl = document.getElementById('result-explanation'); - const ptsEl = document.getElementById('result-pts'); + document.getElementById('result-icon').textContent = correct?'โœ“':'โœ—'; + const lbl=document.getElementById('result-label'); + lbl.textContent = correct ? CORRECT_FB[Math.floor(Math.random()*CORRECT_FB.length)] + : WRONG_FB[Math.floor(Math.random()*WRONG_FB.length)]; + lbl.className = 'result-label '+(correct?'correct':'wrong'); - if (correct) { - icon.textContent = 'โœ“'; - label.textContent = CORRECT_FB[Math.floor(Math.random() * CORRECT_FB.length)]; - label.className = 'result-label correct'; - ptsEl.textContent = this.state.streak >= 5 ? '+200 PTS ๐Ÿ”ฅ STREAK BONUS!' - : this.state.streak >= 3 ? '+150 PTS ๐Ÿ”ฅ HOT!' - : '+100 PTS'; - ptsEl.style.display = 'block'; - ansEl.classList.remove('visible'); + const ansEl=document.getElementById('result-correct-ans'); + const ptsEl=document.getElementById('result-pts'); + 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'); } else { - icon.textContent = 'โœ—'; - label.textContent = WRONG_FB[Math.floor(Math.random() * WRONG_FB.length)]; - label.className = 'result-label wrong'; - ptsEl.style.display = 'none'; - ansEl.textContent = 'โœ“ Correct answer: ' + q.options[q.answer]; + ptsEl.style.display='none'; + ansEl.textContent='โœ“ Correct answer: '+q.options[q.answer]; ansEl.classList.add('visible'); } + document.getElementById('result-explanation').textContent=q.explanation; - expEl.textContent = q.explanation; - - document.getElementById('btn-result-cont').onclick = () => { + document.getElementById('btn-result-cont').onclick=()=>{ this.state.currentQ++; saveGame(this.state); - if (this.state.currentQ >= this.state.questions.length) { - clearSave(); - this.showComplete(); - } else if (MILESTONES[this.state.currentQ]) { - this.showLevelUp(); - } else { - this.showMap(); - } + if(this.state.currentQ>=this.state.questions.length){ clearSave(); this.showComplete(); } + else if(MILESTONES[this.state.currentQ]) this.showLevelUp(); + else this.showMap(); }; } /* โ”€โ”€ LEVEL UP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showLevelUp() { this.show('levelup'); - const ms = MILESTONES[this.state.currentQ]; - const pct = (this.state.currentQ / (this.state.questions.length || 100)) * 100; - - document.getElementById('lu-name').textContent = this.state.playerName || 'ASH'; - document.getElementById('lu-stars').textContent = `${ms.stars} BADGE EARNED! ${ms.stars}`; - document.getElementById('lu-badge').textContent = ms.badge; - document.getElementById('lu-prog-label').textContent = `${this.state.currentQ} / 100`; - - const bar = document.getElementById('lu-prog-inner'); - if (bar) bar.style.width = pct + '%'; - - document.getElementById('btn-lu-cont').onclick = () => this.showMap(); + this._showController(false); + const ms=MILESTONES[this.state.currentQ]; + const pct=(this.state.currentQ/(this.state.questions.length||100))*100; + document.getElementById('lu-name').textContent = this.state.playerName||'ASH'; + document.getElementById('lu-stars').textContent = `${ms.stars} BADGE EARNED! ${ms.stars}`; + document.getElementById('lu-badge').textContent = ms.badge; + document.getElementById('lu-prog-label').textContent=`${this.state.currentQ} / 100`; + const bar=document.getElementById('lu-prog-inner'); + if(bar)bar.style.width=pct+'%'; + document.getElementById('btn-lu-cont').onclick=()=>this.showMap(); } /* โ”€โ”€ COMPLETE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showComplete() { this.show('complete'); - const name = this.state.playerName || 'ASH'; - const tot = this.state.questions.length || 100; - const acc = tot > 0 ? Math.round((this.state.correct / tot) * 100) : 0; - - document.getElementById('complete-name').textContent = name; - document.getElementById('complete-stats').textContent = - `FINAL SCORE: ${this.state.score.toLocaleString()}\n` + - `CORRECT: ${this.state.correct} / ${tot}\n` + - `ACCURACY: ${acc}%\n` + + this._showController(false); + const name=this.state.playerName||'ASH', tot=this.state.questions.length||100; + const acc=tot>0?Math.round((this.state.correct/tot)*100):0; + document.getElementById('complete-name').textContent=name; + document.getElementById('complete-stats').textContent= + `FINAL SCORE: ${this.state.score.toLocaleString()}\n`+ + `CORRECT: ${this.state.correct} / ${tot}\n`+ + `ACCURACY: ${acc}%\n`+ `BEST STREAK: ${this.state.maxStreak} ๐Ÿ”ฅ`; - - document.getElementById('btn-play-again').onclick = () => { - clearSave(); - this.startFresh(); - }; + document.getElementById('btn-play-again').onclick=()=>{ clearSave(); this.startFresh(); }; } - /* โ”€โ”€ SCREEN SWITCHER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + /* โ”€โ”€ 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; + 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 = ''; - let i = 0; - this.state.twTimer = setInterval(() => { - if (i < text.length) { el.textContent += text[i++]; } - else { - clearInterval(this.state.twTimer); - this.state.twTimer = null; - if (onDone) onDone(); - } - }, 26); + 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=''; + let i=0; + this.state.twTimer=setInterval(()=>{ + if(i el.classList.add('hidden'), ms); - } - - /* โ”€โ”€ GLOBAL KEYBOARD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - bindGlobalKeys() { - document.addEventListener('keydown', (e) => { - const s = this.state.screen; - - if (s === 'name') { - if (e.key === 'Backspace') { e.preventDefault(); this.delChar(); } - else if (e.key === 'Enter') { this.confirmName(); } - else if (e.key.length === 1 && /[A-Z0-9.\-!?]/i.test(e.key)) { - this.addChar(e.key.toUpperCase()); - } - return; - } - - if (s === 'title' && (e.key === 'Enter' || e.key === ' ')) { - document.getElementById('screen-title').click(); - return; - } - - if (s === 'battle' && !this.state.answering) { - const map = { '1':0, '2':1, '3':2, '4':3, 'a':0, 'b':1, 'c':2, 'd':3 }; - const idx = map[e.key.toLowerCase()]; - if (idx !== undefined) { - const btns = document.querySelectorAll('.choice-btn:not(.disabled)'); - if (btns[idx]) btns[idx].click(); - } - return; - } - - if (e.key === 'Enter' || e.key === ' ') { - if (s === 'result') { document.getElementById('btn-result-cont')?.click(); } - if (s === 'levelup') { document.getElementById('btn-lu-cont')?.click(); } - if (s === 'map') { document.getElementById('btn-walk')?.click(); } - } - }); + toast(msg,ms=2600){ + const el=document.getElementById('toast'); + el.textContent=msg; el.classList.remove('hidden'); + setTimeout(()=>el.classList.add('hidden'),ms); } } -/* โ”€โ”€โ”€ INIT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -window.addEventListener('DOMContentLoaded', () => { new Game(); }); +window.addEventListener('DOMContentLoaded',()=>{ new Game(); }); diff --git a/index.html b/index.html index 71789a3..e81297e 100644 --- a/index.html +++ b/index.html @@ -27,7 +27,7 @@
Learn Microsoft T-SQL
โ˜… SQL TRAINER EDITION โ˜…
@@ -84,7 +84,7 @@
- + @@ -100,17 +100,22 @@ ๐Ÿ’พ -
+
๐ŸŒฒ๐ŸŒณ ๐ŸŒฒ๐ŸŒณ๐ŸŒฒ
+
-
-
!
+ +
+
!
-
๐Ÿงข
+ +
๐Ÿงข
+ +
TRAINER
ASH
@@ -118,7 +123,6 @@
STREAK
0
CORRECT
0/0
-
@@ -141,7 +145,7 @@
@@ -195,6 +199,33 @@ + + + diff --git a/style.css b/style.css index c24bf6e..9fd066a 100644 --- a/style.css +++ b/style.css @@ -21,11 +21,15 @@ --hp-green: #50c020; --hp-yellow: #c8b000; --font: 'Press Start 2P', monospace; - - /* layout */ - --app-w: 420px; - --app-h: 680px; + --app-w: 420px; + --app-h: 680px; --safe-bottom: env(safe-area-inset-bottom, 0px); + /* GBA controller palette */ + --gba-body: #6a5acd; + --gba-dark: #3d2f99; + --gba-ab-a: #e83030; + --gba-ab-b: #2060d0; + --gba-dpad: #1a1a2e; } /* โ”€โ”€โ”€ RESET โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ @@ -52,26 +56,15 @@ html, body { height: min(var(--app-h), 100dvh); background: var(--dark-bg); overflow: hidden; - - /* desktop: device frame */ border: 5px solid #111; border-radius: 6px; - box-shadow: - 0 0 0 2px #333, - 0 0 40px rgba(100,80,200,.55), - 0 0 90px rgba(80,60,180,.25); + box-shadow: 0 0 0 2px #333, 0 0 40px rgba(100,80,200,.55), 0 0 90px rgba(80,60,180,.25); } - -/* scanline overlay */ #app::after { content: ''; position: absolute; inset: 0; - background: repeating-linear-gradient( - 0deg, - transparent 0px, transparent 2px, - rgba(0,0,0,.045) 2px, rgba(0,0,0,.045) 4px - ); + background: repeating-linear-gradient(0deg, transparent 0px, transparent 2px, rgba(0,0,0,.045) 2px, rgba(0,0,0,.045) 4px); pointer-events: none; z-index: 900; } @@ -79,41 +72,36 @@ html, body { /* โ”€โ”€โ”€ MOBILE FULL-SCREEN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ @media (max-width: 480px), (max-height: 720px) { html, body { align-items: flex-start; } - #app { - width: 100vw; - height: 100dvh; - border: none; - border-radius: 0; - box-shadow: none; - } + #app { width: 100vw; height: 100dvh; border: none; border-radius: 0; box-shadow: none; } } /* โ”€โ”€โ”€ SCREENS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.screen { - position: absolute; - inset: 0; - display: none; - flex-direction: column; -} +.screen { position: absolute; inset: 0; display: none; flex-direction: column; } .screen.active { display: flex; } /* โ”€โ”€โ”€ ANIMATIONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -@keyframes blink { 0%,100%{opacity:1}50%{opacity:0} } -@keyframes fadeIn { from{opacity:0}to{opacity:1} } -@keyframes slideUp { from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1} } -@keyframes bounce { 0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)} } -@keyframes pop { 0%{transform:scale(.5);opacity:0}65%{transform:scale(1.08)}100%{transform:scale(1);opacity:1} } -@keyframes goldShimmer { 0%,100%{color:#f8c030;text-shadow:0 0 8px #f8c030}50%{color:#ffe878;text-shadow:0 0 18px #ffe878,0 0 36px #f8c030} } -@keyframes correctPulse{ 0%,100%{background:#c8ffc8;border-color:var(--green)}50%{background:#e0ffe0} } -@keyframes wrongShake { 0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)} } -@keyframes npcFloat { 0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)} } -@keyframes treeSway { 0%,100%{transform:rotate(-2deg)}50%{transform:rotate(2deg)} } +@keyframes blink { 0%,100%{opacity:1}50%{opacity:0} } +@keyframes fadeIn { from{opacity:0}to{opacity:1} } +@keyframes slideUp { from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1} } +@keyframes bounce { 0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)} } +@keyframes pop { 0%{transform:scale(.5);opacity:0}65%{transform:scale(1.08)}100%{transform:scale(1);opacity:1} } +@keyframes goldShimmer { 0%,100%{color:#f8c030;text-shadow:0 0 8px #f8c030}50%{color:#ffe878;text-shadow:0 0 18px #ffe878,0 0 36px #f8c030} } +@keyframes correctPulse { 0%,100%{background:#c8ffc8;border-color:var(--green)}50%{background:#e0ffe0} } +@keyframes wrongShake { 0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)} } +@keyframes npcFloat { 0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)} } +@keyframes treeSway { 0%,100%{transform:rotate(-2deg)}50%{transform:rotate(2deg)} } +@keyframes walkLeft { 0%,100%{transform:scaleX(-1) translateY(0)}50%{transform:scaleX(-1) translateY(-4px)} } +@keyframes walkRight { 0%,100%{transform:scaleX(1) translateY(0)}50%{transform:scaleX(1) translateY(-4px)} } +@keyframes walkUp { 0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)} } +@keyframes walkDown { 0%,100%{transform:translateY(0)}50%{transform:translateY(4px)} } +@keyframes dpadPress { 0%{transform:scale(1)}50%{transform:scale(.88)}100%{transform:scale(1)} } +@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)} } .blink { animation: blink 1s step-end infinite; } -/* โ”€โ”€โ”€ SHARED COMPONENTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - -/* Dialog box โ€” the heart of the Pokรฉmon look */ +/* โ”€โ”€โ”€ DIALOG BOX โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .dialog-box { background: var(--dialog-bg); border: 4px solid var(--dialog-bd); @@ -147,14 +135,9 @@ html, body { padding-top: 2px; min-height: 52px; } -.dialog-arrow { - font-size: 10px; - color: var(--black); - text-align: right; - margin-top: 4px; -} +.dialog-arrow { font-size: 10px; color: var(--black); text-align: right; margin-top: 4px; } -/* Pixel buttons */ +/* โ”€โ”€โ”€ PIXEL BUTTONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .btn-pixel { display: inline-block; font-family: var(--font); @@ -169,19 +152,13 @@ html, body { user-select: none; -webkit-tap-highlight-color: transparent; } -.btn-pixel:active { - transform: translate(3px,3px); - box-shadow: none; -} -.btn-blue { background: var(--blue); } -.btn-blue:hover { background: var(--blue-hover); } -.btn-green { background: var(--green); } -.btn-green:hover { background: var(--green-hover); } -.btn-red { background: var(--red); } -.btn-red:hover { background: var(--red-hover); } +.btn-pixel:active { transform: translate(3px,3px); box-shadow: none; } +.btn-blue { background: var(--blue); } .btn-blue:hover { background: var(--blue-hover); } +.btn-green { background: var(--green); } .btn-green:hover { background: var(--green-hover); } +.btn-red { background: var(--red); } .btn-red:hover { background: var(--red-hover); } .btn-full { width: 100%; text-align: center; } -/* Overworld background */ +/* โ”€โ”€โ”€ OVERWORLD BG โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .overworld-bg { flex: 1; position: relative; @@ -192,44 +169,151 @@ html, body { linear-gradient(180deg, #6cb84e 0%, #58a83e 100%); } -/* โ”€โ”€โ”€ BOOT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-boot { - background: var(--white); +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + GBA CONTROLLER OVERLAY + Sits outside #app, fixed to viewport bottom. + Shown only when screen === 'map' or 'battle'. + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +.gba-controller { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: clamp(110px, 22vw, 148px); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 clamp(8px,3vw,20px) max(8px, var(--safe-bottom)); + background: linear-gradient(180deg, rgba(26,10,62,.82) 0%, rgba(14,6,36,.95) 100%); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + border-top: 2px solid rgba(120,80,255,.35); + z-index: 800; + user-select: none; + touch-action: none; + pointer-events: auto; +} +.gba-controller.hidden { display: none; } + +/* Only show controller on touch / small screens by default. + On desktop we rely on keyboard โ€” but controller still shows + if screen is narrow enough. JS toggles it per screen anyway. */ +@media (min-width: 900px) { + .gba-controller { display: none !important; } +} + +/* โ”€โ”€ D-PAD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.dpad { + position: relative; + width: clamp(90px, 21vw, 120px); + height: clamp(90px, 21vw, 120px); + flex-shrink: 0; +} +.dpad-btn { + position: absolute; + background: var(--gba-dpad); + border: 2px solid rgba(255,255,255,.18); + color: rgba(255,255,255,.75); + font-size: clamp(8px, 2.2vw, 11px); + display: flex; align-items: center; justify-content: center; - animation: fadeIn .4s ease; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + touch-action: none; + transition: background .08s; } -.boot-logo { text-align: center; } -.boot-gameboy { - font-size: clamp(8px, 2.5vw, 11px); - color: var(--black); - letter-spacing: 4px; - margin-bottom: 10px; +.dpad-btn:active, .dpad-btn.pressed { + background: #3a3a6e; + animation: dpadPress .15s ease; } -.boot-ding { - font-size: 28px; - margin-bottom: 8px; - animation: goldShimmer 1.5s ease-in-out infinite; -} -.boot-copy { font-size: 6px; color: var(--gray); } -/* โ”€โ”€โ”€ TITLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-title { - background: linear-gradient(180deg, #08031a 0%, #180a44 45%, #240e6a 75%, #120538 100%); - overflow: hidden; -} -.title-bg { - position: relative; - width: 100%; - height: 100%; +/* Cross layout: 3ร—3 grid conceptually */ +.dpad-up { top:0; left:33.3%; width:33.4%; height:33.4%; border-radius:4px 4px 0 0; } +.dpad-down { bottom:0; left:33.3%; width:33.4%; height:33.4%; border-radius:0 0 4px 4px; } +.dpad-left { top:33.3%; left:0; width:33.4%; height:33.4%; border-radius:4px 0 0 4px; } +.dpad-right { top:33.3%; right:0; width:33.4%; height:33.4%; border-radius:0 4px 4px 0; } +.dpad-center{ top:33.3%; left:33.3%; width:33.4%; height:33.4%; background:#111; border:none; cursor:default; } + +/* โ”€โ”€ SELECT / START (center strip) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.gba-mid-btns { display: flex; flex-direction: column; + gap: 6px; + align-items: center; +} +.gba-mid-btn { + background: #2a1a5e; + border: 2px solid rgba(255,255,255,.2); + border-radius: 20px; + color: rgba(255,255,255,.6); + font-family: var(--font); + font-size: clamp(4.5px, 1.2vw, 6px); + padding: 4px 10px; + cursor: pointer; + letter-spacing: .5px; + -webkit-tap-highlight-color: transparent; + transition: background .08s; +} +.gba-mid-btn:active { background: #3d2f99; } + +/* โ”€โ”€ A / B BUTTONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.ab-cluster { + position: relative; + width: clamp(90px, 21vw, 120px); + height: clamp(90px, 21vw, 120px); + flex-shrink: 0; +} +.ab-btn { + position: absolute; + width: clamp(36px, 9vw, 48px); + height: clamp(36px, 9vw, 48px); + border-radius: 50%; + border: 3px solid rgba(0,0,0,.5); + font-family: var(--font); + font-size: clamp(9px, 2.5vw, 13px); + font-weight: bold; + color: var(--white); + cursor: pointer; + display: flex; align-items: center; justify-content: center; + box-shadow: 0 4px 0 rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.3); + -webkit-tap-highlight-color: transparent; + touch-action: none; + transition: transform .06s, box-shadow .06s; } -.title-stars { +.ab-btn:active, .ab-btn.pressed { + transform: translateY(3px); + box-shadow: 0 1px 0 rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.2); + animation: abPress .18s ease; +} +/* B = left, A = right (classic GBA layout) */ +.ab-b { background: radial-gradient(circle at 35% 35%, #4080ff, var(--gba-ab-b)); bottom: 14%; left: 5%; } +.ab-a { background: radial-gradient(circle at 35% 35%, #ff5555, var(--gba-ab-a)); top: 14%; right: 5%; } + +/* label under each button */ +.ab-btn::after { + content: attr(id) == 'btn-a' ? 'A' : 'B'; position: absolute; - inset: 0; + bottom: -14px; + font-size: 5px; + color: rgba(255,255,255,.4); + letter-spacing: 1px; +} + +/* โ”€โ”€โ”€ BOOT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-boot { background: var(--white); align-items: center; justify-content: center; animation: fadeIn .4s ease; } +.boot-logo { text-align: center; } +.boot-gameboy { font-size: clamp(8px,2.5vw,11px); color: var(--black); letter-spacing: 4px; margin-bottom: 10px; } +.boot-ding { font-size: 28px; margin-bottom: 8px; animation: goldShimmer 1.5s ease-in-out infinite; } +.boot-copy { font-size: 6px; color: var(--gray); } + +/* โ”€โ”€โ”€ TITLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-title { background: linear-gradient(180deg,#08031a 0%,#180a44 45%,#240e6a 75%,#120538 100%); overflow: hidden; } +.title-bg { position:relative; width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; } +.title-stars { + position: absolute; inset: 0; background-image: radial-gradient(1px 1px at 8% 12%, #fff 100%,transparent), radial-gradient(1px 1px at 22% 38%, #fff 100%,transparent), @@ -242,553 +326,193 @@ html, body { radial-gradient(2px 2px at 18% 82%, rgba(255,220,100,.8) 100%,transparent), radial-gradient(2px 2px at 78% 78%, rgba(255,220,100,.8) 100%,transparent); } -.title-main { - text-align: center; - z-index: 1; - padding: 24px 16px; -} -.title-logo { - margin-bottom: 16px; - line-height: 1; -} -.title-poke { - font-size: clamp(30px, 11vw, 50px); - 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-query { - font-size: clamp(30px, 11vw, 50px); - color: #60c8ff; - text-shadow: 4px 4px 0 #003d70, 0 0 24px rgba(96,200,255,.5); - display: inline-block; -} -.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-deco { - font-size: clamp(22px, 6vw, 32px); - margin-bottom: 28px; - display: flex; - gap: 16px; - justify-content: center; -} -.title-deco span:nth-child(1) { animation: npcFloat 2.0s ease-in-out infinite; } -.title-deco span:nth-child(2) { animation: npcFloat 2.4s ease-in-out infinite; } -.title-deco span:nth-child(3) { animation: npcFloat 2.8s ease-in-out infinite; } -.title-tap { - font-size: clamp(7px, 2vw, 9px); - color: var(--white); - letter-spacing: 1px; -} -.title-footer { - position: absolute; - bottom: max(12px, var(--safe-bottom)); - font-size: 6px; - color: rgba(255,255,255,.3); - letter-spacing: .5px; -} +.title-main { text-align:center; z-index:1; padding:24px 16px; } +.title-logo { margin-bottom:16px; line-height:1; } +.title-poke { font-size:clamp(30px,11vw,50px); 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(30px,11vw,50px); color:#60c8ff; text-shadow:4px 4px 0 #003d70,0 0 24px rgba(96,200,255,.5); display:inline-block; } +.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-deco { font-size:clamp(22px,6vw,32px); margin-bottom:28px; display:flex; gap:16px; justify-content:center; } +.title-deco span:nth-child(1){animation:npcFloat 2.0s ease-in-out infinite;} +.title-deco span:nth-child(2){animation:npcFloat 2.4s ease-in-out infinite;} +.title-deco span:nth-child(3){animation:npcFloat 2.8s ease-in-out infinite;} +.title-tap { font-size:clamp(7px,2vw,9px); color:var(--white); letter-spacing:1px; } +.title-footer { position:absolute; bottom:max(12px,var(--safe-bottom)); font-size:6px; color:rgba(255,255,255,.3); letter-spacing:.5px; } /* โ”€โ”€โ”€ CONTINUE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-continue { - background: linear-gradient(180deg, #08031a 0%, #180a44 100%); - align-items: center; - justify-content: center; -} -.continue-bg { - width: 90%; - max-width: 340px; - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; - animation: fadeIn .5s ease; -} -.continue-logo { - font-size: clamp(16px, 5vw, 22px); - color: var(--yellow); - text-shadow: 3px 3px 0 #7a5800; - animation: goldShimmer 3s ease-in-out infinite; -} -.continue-logo span { color: #60c8ff; text-shadow: 3px 3px 0 #003d70; } -.continue-card { - width: 100%; - background: var(--dialog-bg); - border: 4px solid var(--dialog-bd); - padding: 16px; - display: flex; - flex-direction: column; - gap: 10px; - box-shadow: 4px 4px 0 rgba(0,0,0,.5); -} -.continue-card-header { - font-size: 8px; - color: var(--black); - margin-bottom: 4px; - letter-spacing: 1px; -} -.save-row { - display: flex; - justify-content: space-between; - align-items: center; - font-size: clamp(6.5px, 1.8vw, 8px); -} -.save-lbl { color: var(--gray); } -.save-val { color: var(--black); } -.continue-or { - font-size: 7px; - color: rgba(255,255,255,.4); - letter-spacing: 1px; -} -.continue-warn { - font-size: 6px; - color: rgba(255,80,80,.7); - text-align: center; - line-height: 1.8; - min-height: 14px; -} +#screen-continue { background:linear-gradient(180deg,#08031a 0%,#180a44 100%); align-items:center; justify-content:center; } +.continue-bg { width:90%; max-width:340px; display:flex; flex-direction:column; align-items:center; gap:16px; animation:fadeIn .5s ease; } +.continue-logo { font-size:clamp(16px,5vw,22px); color:var(--yellow); text-shadow:3px 3px 0 #7a5800; animation:goldShimmer 3s ease-in-out infinite; } +.continue-logo span { color:#60c8ff; text-shadow:3px 3px 0 #003d70; } +.continue-card { width:100%; background:var(--dialog-bg); border:4px solid var(--dialog-bd); padding:16px; display:flex; flex-direction:column; gap:10px; box-shadow:4px 4px 0 rgba(0,0,0,.5); } +.continue-card-header { font-size:8px; color:var(--black); margin-bottom:4px; letter-spacing:1px; } +.save-row { display:flex; justify-content:space-between; align-items:center; font-size:clamp(6.5px,1.8vw,8px); } +.save-lbl { color:var(--gray); } .save-val { color:var(--black); } +.continue-or { font-size:7px; color:rgba(255,255,255,.4); letter-spacing:1px; } +.continue-warn{ font-size:6px; color:rgba(255,80,80,.7); text-align:center; line-height:1.8; min-height:14px; } /* โ”€โ”€โ”€ NAME ENTRY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-name { - background: #c8b88a; - padding: 14px 10px 10px; - gap: 10px; -} -.name-dialog { flex-shrink: 0; } -.name-entry-area { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - flex: 1; -} -.name-display-box { - background: var(--white); - border: 3px solid var(--black); - padding: 8px 20px; - font-size: clamp(14px, 4.5vw, 20px); - color: var(--black); - min-width: 180px; - text-align: center; - letter-spacing: 4px; - box-shadow: inset 2px 2px 0 rgba(0,0,0,.2); -} -.keyboard-grid { - display: grid; - grid-template-columns: repeat(10, 1fr); - gap: 3px; - width: 100%; -} -.key-btn { - background: var(--white); - border: 2px solid var(--black); - color: var(--black); - font-family: var(--font); - font-size: clamp(6px, 1.8vw, 8px); - padding: clamp(5px, 1.5vw, 8px) 2px; - cursor: pointer; - text-align: center; - box-shadow: 2px 2px 0 var(--darkgray); - user-select: none; - -webkit-tap-highlight-color: transparent; - transition: transform .05s, box-shadow .05s; -} -.key-btn:active { transform: translate(2px,2px); box-shadow: none; } -.name-controls { - display: flex; - gap: 12px; - margin-top: 4px; -} -.btn-del { background: var(--red); } -.btn-del:hover { background: var(--red-hover); } -.btn-ok { background: var(--green); } -.btn-ok:hover { background: var(--green-hover); } +#screen-name { background:#c8b88a; padding:14px 10px 10px; gap:10px; } +.name-dialog { flex-shrink:0; } +.name-entry-area { display:flex; flex-direction:column; align-items:center; gap:8px; flex:1; } +.name-display-box { background:var(--white); border:3px solid var(--black); padding:8px 20px; font-size:clamp(14px,4.5vw,20px); color:var(--black); min-width:180px; text-align:center; letter-spacing:4px; box-shadow:inset 2px 2px 0 rgba(0,0,0,.2); } +.keyboard-grid { display:grid; grid-template-columns:repeat(10,1fr); gap:3px; width:100%; } +.key-btn { background:var(--white); border:2px solid var(--black); color:var(--black); font-family:var(--font); font-size:clamp(6px,1.8vw,8px); padding:clamp(5px,1.5vw,8px) 2px; cursor:pointer; text-align:center; box-shadow:2px 2px 0 var(--darkgray); user-select:none; -webkit-tap-highlight-color:transparent; transition:transform .05s,box-shadow .05s; } +.key-btn:active { transform:translate(2px,2px); box-shadow:none; } +.name-controls { display:flex; gap:12px; margin-top:4px; } +.btn-del { background:var(--red); } .btn-del:hover { background:var(--red-hover); } +.btn-ok { background:var(--green); } .btn-ok:hover { background:var(--green-hover); } /* โ”€โ”€โ”€ INTRO โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#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-player { - position: absolute; - left: 22%; - bottom: 28%; - font-size: clamp(36px, 10vw, 52px); - animation: bounce 1s ease-in-out infinite; - filter: drop-shadow(2px 3px 0 rgba(0,0,0,.4)); -} -.tap-hint { - font-size: 6px; - color: var(--black); - background: rgba(255,255,255,.6); - text-align: center; - padding: 4px; - letter-spacing: 1px; - flex-shrink: 0; -} +#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-player { position:absolute; left:22%; bottom:28%; font-size:clamp(36px,10vw,52px); animation:bounce 1s ease-in-out infinite; filter:drop-shadow(2px 3px 0 rgba(0,0,0,.4)); } +.tap-hint { font-size:6px; color:var(--black); background:rgba(255,255,255,.6); text-align:center; padding:4px; letter-spacing:1px; flex-shrink:0; } /* โ”€โ”€โ”€ MAP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.map-header { - background: var(--dark-bg); - padding: 8px 12px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - border-bottom: 3px solid var(--black); - flex-shrink: 0; -} -.map-left { display: flex; align-items: center; gap: 6px; } -.map-pin { font-size: 12px; } -.map-town-name { font-size: clamp(5.5px, 1.6vw, 7px); color: var(--yellow); letter-spacing: .5px; } -.map-right { display: flex; align-items: center; gap: 6px; } -.prog-bar-outer { width: 80px; height: 9px; background: var(--black); border: 2px solid var(--lightgray); overflow: hidden; } -.prog-bar-inner { height: 100%; width: 0%; background: var(--hp-green); transition: width .5s ease; } -.prog-text { font-size: 6px; color: var(--white); } -.save-dot { font-size: 12px; animation: blink 3s ease-in-out infinite; } +.map-header { background:var(--dark-bg); padding:8px 12px; display:flex; align-items:center; justify-content:space-between; gap:8px; border-bottom:3px solid var(--black); flex-shrink:0; } +.map-left { display:flex; align-items:center; gap:6px; } +.map-pin { font-size:12px; } +.map-town-name { font-size:clamp(5.5px,1.6vw,7px); color:var(--yellow); letter-spacing:.5px; } +.map-right { display:flex; align-items:center; gap:6px; } +.prog-bar-outer { width:80px; height:9px; background:var(--black); border:2px solid var(--lightgray); overflow:hidden; } +.prog-bar-inner { height:100%; width:0%; background:var(--hp-green); transition:width .5s ease; } +.prog-text { font-size:6px; color:var(--white); } +.save-dot { font-size:12px; animation:blink 3s ease-in-out infinite; } -.map-world { position: relative; } -.map-trees { - position: absolute; - top: 12%; - left: 0; right: 0; - display: flex; - justify-content: space-around; - padding: 0 8px; -} -.tree { - font-size: clamp(24px, 7vw, 36px); - display: inline-block; - animation: treeSway 3s ease-in-out infinite; - filter: drop-shadow(1px 2px 0 rgba(0,0,0,.3)); -} -.tree:nth-child(2) { animation-delay: .5s; } -.tree:nth-child(3) { animation-delay: 1s; } -.tree:nth-child(4) { animation-delay: 1.5s; } -.tree:nth-child(5) { animation-delay: 2s; } -.map-ground { - position: absolute; - bottom: 0; left: 0; right: 0; - height: 32%; - background: linear-gradient(180deg, #c0a060 0%, #a89050 100%); - border-top: 3px solid #907840; -} -.map-npc-wrap { - position: absolute; - right: 20%; - bottom: 30%; - text-align: center; -} -.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); - animation: npcFloat 1.8s ease-in-out infinite; - filter: drop-shadow(2px 3px 0 rgba(0,0,0,.35)); - cursor: pointer; -} +.map-world { position:relative; } +.map-trees { position:absolute; top:12%; left:0; right:0; display:flex; justify-content:space-around; padding:0 8px; } +.tree { font-size:clamp(24px,7vw,36px); display:inline-block; animation:treeSway 3s ease-in-out infinite; filter:drop-shadow(1px 2px 0 rgba(0,0,0,.3)); } +.tree:nth-child(2){animation-delay:.5s;} .tree:nth-child(3){animation-delay:1s;} .tree:nth-child(4){animation-delay:1.5s;} .tree:nth-child(5){animation-delay:2s;} +.map-ground { position:absolute; bottom:0; left:0; right:0; height:32%; background:linear-gradient(180deg,#c0a060 0%,#a89050 100%); border-top:3px solid #907840; } + +/* NPC on map */ +.map-npc-wrap { position:absolute; text-align:center; transition: left .4s ease, bottom .4s ease; } +.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)); } + +/* โ”€โ”€ PLAYER SPRITE (JS-positioned) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .map-player { position: absolute; - left: 22%; - bottom: 28%; - font-size: clamp(32px, 9vw, 46px); - animation: bounce 1.1s ease-in-out infinite; - filter: drop-shadow(2px 3px 0 rgba(0,0,0,.35)); + font-size: clamp(28px, 8vw, 40px); + filter: drop-shadow(2px 3px 0 rgba(0,0,0,.4)); + transition: left .12s linear, bottom .12s linear; + z-index: 10; + line-height: 1; } -.map-hud { - background: var(--dialog-bg); - border-top: 4px solid var(--dialog-bd); - padding: 8px 10px; - display: flex; - gap: 0; - flex-shrink: 0; -} -.hud-cell { - flex: 1; - text-align: center; - border-right: 2px solid rgba(0,0,0,.15); - padding: 0 4px; -} -.hud-cell:last-child { border-right: none; } -.hud-lbl { font-size: 5.5px; color: var(--gray); margin-bottom: 3px; } -.hud-val { font-size: clamp(7px, 2vw, 9px); color: var(--black); } -.btn-walk { - margin: 8px 10px max(8px, var(--safe-bottom)); - width: calc(100% - 20px); - font-size: clamp(7.5px, 2.2vw, 9px); - padding: 12px; - text-align: center; - flex-shrink: 0; +.map-player.walk-left { animation: walkLeft .25s steps(1) infinite; } +.map-player.walk-right { animation: walkRight .25s steps(1) infinite; } +.map-player.walk-up { animation: walkUp .25s steps(1) infinite; } +.map-player.walk-down { animation: walkDown .25s steps(1) infinite; } +.map-player.idle { animation: none; } + +/* Talk hint that floats above player when near NPC */ +.map-talk-hint { + position: absolute; + font-family: var(--font); + font-size: 6px; + background: var(--yellow); + color: var(--black); + border: 2px solid var(--black); + padding: 3px 6px; + white-space: nowrap; + z-index: 11; + animation: talkBubble .6s ease-in-out infinite; + pointer-events: none; } +/* HUD below map */ +.map-hud { background:var(--dialog-bg); border-top:4px solid var(--dialog-bd); padding:8px 10px; display:flex; gap:0; flex-shrink:0; } +.hud-cell { flex:1; text-align:center; border-right:2px solid rgba(0,0,0,.15); padding:0 4px; } +.hud-cell:last-child { border-right:none; } +.hud-lbl { font-size:5.5px; color:var(--gray); margin-bottom:3px; } +.hud-val { font-size:clamp(7px,2vw,9px); color:var(--black); } + /* โ”€โ”€โ”€ BATTLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-battle { flex-direction: column; } +#screen-battle { flex-direction:column; } .battle-scene { - display: flex; - justify-content: space-between; - align-items: flex-end; - padding: 10px 16px 6px; - background: linear-gradient(180deg, #90c8f0 0%, #70b0e0 55%, #c8b880 55%, #b09860 100%); - flex-shrink: 0; - min-height: clamp(120px, 28vh, 180px); - position: relative; + display:flex; justify-content:space-between; align-items:flex-end; + padding:10px 16px 6px; + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#c8b880 55%,#b09860 100%); + flex-shrink:0; min-height:clamp(100px,22vh,150px); position:relative; } -.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(44px, 13vw, 64px); - filter: drop-shadow(2px 3px 0 rgba(0,0,0,.3)); - animation: npcFloat 2s ease-in-out infinite; -} -.battle-player-side .battle-sprite { animation: bounce 1s ease-in-out infinite; } -.battle-name-tag { - font-size: clamp(5.5px, 1.6vw, 7px); - background: rgba(255,255,255,.8); - border: 2px solid var(--black); - padding: 2px 6px; - color: var(--black); -} -.battle-dialog { margin: 8px; min-height: 80px; } -.battle-choices { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 5px; - padding: 5px 8px; - flex-shrink: 0; -} -.choice-btn { - background: var(--dialog-bg); - border: 3px solid var(--dialog-bd); - color: var(--black); - font-family: var(--font); - font-size: clamp(6px, 1.7vw, 7.5px); - padding: clamp(7px,2vw,10px) clamp(6px,2vw,10px); - cursor: pointer; - text-align: left; - line-height: 1.8; - box-shadow: 2px 2px 0 var(--darkgray); - word-break: break-word; - hyphens: auto; - transition: transform .06s, box-shadow .06s; - -webkit-tap-highlight-color: transparent; - position: relative; -} -.choice-btn::before { - content: attr(data-letter) ' '; - color: var(--blue); -} -.choice-btn:hover { background: #fffae0; border-color: var(--blue); transform: translate(-1px,-1px); box-shadow: 3px 3px 0 var(--darkgray); } -.choice-btn:active { transform: translate(2px,2px); box-shadow: none; } -.choice-btn.correct { background: #c8ffc8; border-color: var(--green); animation: correctPulse .5s ease 3; pointer-events: none; } -.choice-btn.wrong { background: #ffc8c8; border-color: var(--red); animation: wrongShake .35s ease; pointer-events: none; } -.choice-btn.disabled{ opacity: .5; pointer-events: none; } +.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-player-side .battle-sprite { animation:bounce 1s ease-in-out infinite; } +.battle-name-tag { font-size:clamp(5.5px,1.6vw,7px); background:rgba(255,255,255,.8); border:2px solid var(--black); padding:2px 6px; color:var(--black); } +.battle-dialog { margin:8px; min-height:72px; } +.battle-choices { display:grid; grid-template-columns:1fr 1fr; gap:5px; padding:5px 8px; flex-shrink:0; } -.battle-footer { - background: var(--dark-bg); - border-top: 3px solid var(--black); - padding: clamp(5px,1.5vw,7px) 12px max(6px, var(--safe-bottom)); - display: flex; - justify-content: space-between; - align-items: center; - font-size: clamp(6px, 1.7vw, 8px); - color: var(--yellow); - flex-shrink: 0; +/* choice button */ +.choice-btn { + background:var(--dialog-bg); + border:3px solid var(--dialog-bd); + color:var(--black); + font-family:var(--font); + font-size:clamp(6px,1.7vw,7.5px); + padding:clamp(7px,2vw,10px) clamp(6px,2vw,10px); + cursor:pointer; + text-align:left; + line-height:1.8; + box-shadow:2px 2px 0 var(--darkgray); + word-break:break-word; + hyphens:auto; + transition:transform .06s, box-shadow .06s, border-color .08s; + -webkit-tap-highlight-color:transparent; + position:relative; } -.kbd-hint { font-size: 5.5px; color: rgba(255,255,255,.35); } -@media (max-width: 480px) { .kbd-hint { display: none; } } +.choice-btn::before { content:attr(data-letter) ' '; color:var(--blue); } +.choice-btn:hover { background:#fffae0; border-color:var(--blue); transform:translate(-1px,-1px); box-shadow:3px 3px 0 var(--darkgray); } +.choice-btn:active { transform:translate(2px,2px); box-shadow:none; } + +/* D-pad / keyboard cursor highlight */ +.choice-btn.cursor { animation:choiceCursor .7s ease-in-out infinite; border-color:var(--yellow) !important; background:#fffae0 !important; } +.choice-btn.correct { background:#c8ffc8; border-color:var(--green); animation:correctPulse .5s ease 3; pointer-events:none; } +.choice-btn.wrong { background:#ffc8c8; border-color:var(--red); animation:wrongShake .35s ease; pointer-events:none; } +.choice-btn.disabled{ opacity:.5; pointer-events:none; } + +.battle-footer { background:var(--dark-bg); border-top:3px solid var(--black); padding:clamp(4px,1.2vw,6px) 12px max(4px,var(--safe-bottom)); display:flex; justify-content:space-between; align-items:center; font-size:clamp(5.5px,1.5vw,7px); color:var(--yellow); flex-shrink:0; } +.kbd-hint { font-size:5px; color:rgba(255,255,255,.3); } +@media (max-width:480px) { .kbd-hint { display:none; } } /* โ”€โ”€โ”€ RESULT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-result { - background: rgba(0,0,0,.86); - align-items: center; - justify-content: center; -} -.result-box { - background: var(--dialog-bg); - border: 4px solid var(--dialog-bd); - padding: clamp(18px,5vw,28px) clamp(14px,4vw,22px); - width: clamp(280px, 88vw, 400px); - text-align: center; - box-shadow: 6px 6px 0 rgba(0,0,0,.5); - animation: pop .4s cubic-bezier(.175,.885,.32,1.275); - display: flex; - flex-direction: column; - gap: 10px; -} -.result-icon { font-size: clamp(28px, 9vw, 42px); } -.result-label { - font-size: clamp(12px, 4vw, 16px); -} -.result-label.correct { color: var(--green); text-shadow: 2px 2px 0 #003010; } -.result-label.wrong { color: var(--red); text-shadow: 2px 2px 0 #400010; } -.result-correct-ans { - font-size: clamp(6px, 1.8vw, 7.5px); - color: var(--green); - background: rgba(0,160,30,.1); - border: 2px solid rgba(0,160,30,.3); - padding: 6px; - text-align: left; - line-height: 1.8; - display: none; -} -.result-correct-ans.visible { display: block; } -.result-explanation { - font-size: clamp(6px, 1.8vw, 7.5px); - color: var(--black); - line-height: 2; - text-align: left; - padding: 8px; - background: rgba(255,255,255,.5); - border: 2px solid rgba(0,0,0,.15); -} -.result-pts { - font-size: clamp(10px, 3vw, 13px); - color: var(--yellow); - text-shadow: 2px 2px 0 #604000; - animation: goldShimmer 2s ease-in-out infinite; -} +#screen-result { background:rgba(0,0,0,.86); align-items:center; justify-content:center; } +.result-box { background:var(--dialog-bg); border:4px solid var(--dialog-bd); padding:clamp(16px,4.5vw,26px) clamp(12px,3.5vw,20px); width:clamp(270px,88vw,390px); text-align:center; box-shadow:6px 6px 0 rgba(0,0,0,.5); animation:pop .4s cubic-bezier(.175,.885,.32,1.275); display:flex; flex-direction:column; gap:9px; } +.result-icon { font-size:clamp(26px,8vw,40px); } +.result-label { font-size:clamp(11px,3.5vw,15px); } +.result-label.correct { color:var(--green); text-shadow:2px 2px 0 #003010; } +.result-label.wrong { color:var(--red); text-shadow:2px 2px 0 #400010; } +.result-correct-ans { font-size:clamp(6px,1.8vw,7.5px); color:var(--green); background:rgba(0,160,30,.1); border:2px solid rgba(0,160,30,.3); padding:6px; text-align:left; line-height:1.8; display:none; } +.result-correct-ans.visible { display:block; } +.result-explanation { font-size:clamp(6px,1.8vw,7.5px); color:var(--black); line-height:2; text-align:left; padding:8px; background:rgba(255,255,255,.5); border:2px solid rgba(0,0,0,.15); } +.result-pts { font-size:clamp(9px,2.8vw,12px); color:var(--yellow); text-shadow:2px 2px 0 #604000; animation:goldShimmer 2s ease-in-out infinite; } /* โ”€โ”€โ”€ LEVEL UP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-levelup { - background: linear-gradient(135deg, #0f0630, #220e5e, #0a0a34); - align-items: center; - justify-content: center; -} -.levelup-box { - text-align: center; - padding: clamp(20px,5vw,32px) clamp(16px,4vw,24px); - width: 90%; - max-width: 360px; - display: flex; - flex-direction: column; - gap: 14px; - animation: fadeIn .5s ease; -} -.lu-stars { - font-size: clamp(7px, 2vw, 9px); - color: var(--yellow); - text-shadow: 0 0 8px var(--yellow); - letter-spacing: 1px; -} -.lu-sprite { font-size: clamp(56px, 16vw, 80px); animation: bounce 1s ease-in-out infinite; } -.lu-text { font-size: clamp(7.5px, 2.2vw, 9px); color: var(--white); line-height: 2; } -.lu-badge { - font-size: clamp(7px, 2vw, 9px); - color: var(--yellow); - background: rgba(255,255,255,.08); - border: 2px solid var(--yellow); - padding: clamp(8px,2.5vw,12px); -} -.lu-prog-wrap { display: flex; flex-direction: column; gap: 5px; align-items: center; } -.lu-prog-outer { width: 80%; height: 12px; background: var(--black); border: 2px solid var(--lightgray); overflow: hidden; } -.lu-prog-inner { height: 100%; background: var(--hp-green); transition: width .8s ease; } -.lu-prog-label { font-size: 7px; color: var(--white); } +#screen-levelup { background:linear-gradient(135deg,#0f0630,#220e5e,#0a0a34); align-items:center; justify-content:center; } +.levelup-box { text-align:center; padding:clamp(16px,4vw,28px) clamp(14px,3.5vw,22px); width:90%; max-width:360px; display:flex; flex-direction:column; gap:12px; animation:fadeIn .5s ease; } +.lu-stars { font-size:clamp(7px,2vw,9px); color:var(--yellow); text-shadow:0 0 8px var(--yellow); letter-spacing:1px; } +.lu-sprite { font-size:clamp(52px,15vw,74px); animation:bounce 1s ease-in-out infinite; } +.lu-text { font-size:clamp(7.5px,2.2vw,9px); color:var(--white); line-height:2; } +.lu-badge { font-size:clamp(7px,2vw,9px); color:var(--yellow); background:rgba(255,255,255,.08); border:2px solid var(--yellow); padding:clamp(8px,2.5vw,12px); } +.lu-prog-wrap{ display:flex; flex-direction:column; gap:5px; align-items:center; } +.lu-prog-outer{ width:80%; height:12px; background:var(--black); border:2px solid var(--lightgray); overflow:hidden; } +.lu-prog-inner{ height:100%; background:var(--hp-green); transition:width .8s ease; } +.lu-prog-label{ font-size:7px; color:var(--white); } /* โ”€โ”€โ”€ COMPLETE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -#screen-complete { - background: linear-gradient(180deg, #08031a 0%, #180a44 100%); - overflow: hidden; -} -.complete-scroll { overflow-y: auto; height: 100%; -webkit-overflow-scrolling: touch; } -.complete-area { - text-align: center; - padding: clamp(20px,5vw,32px) clamp(14px,4vw,20px) max(24px,var(--safe-bottom)); - display: flex; - flex-direction: column; - gap: 14px; - align-items: center; - animation: fadeIn .7s ease; -} -.complete-trophy { font-size: clamp(48px, 14vw, 68px); animation: goldShimmer 2s ease-in-out infinite; } -.complete-title { - font-size: clamp(18px, 6vw, 26px); - color: var(--yellow); - text-shadow: 3px 3px 0 #604000; - animation: goldShimmer 2s ease-in-out infinite; -} -.complete-name { font-size: clamp(10px, 3vw, 14px); color: var(--white); } -.complete-msg { - font-size: clamp(6px, 1.7vw, 7.5px); - color: rgba(255,255,255,.8); - line-height: 2.2; - max-width: 360px; - text-align: left; -} -.complete-stats { - font-size: clamp(7px, 2vw, 8.5px); - color: var(--yellow); - background: rgba(255,255,255,.08); - border: 2px solid rgba(255,255,255,.15); - padding: clamp(10px,3vw,16px); - line-height: 2.6; - width: 100%; - max-width: 340px; - text-align: left; - white-space: pre-line; -} -.complete-badges { width: 100%; max-width: 340px; display: flex; flex-direction: column; gap: 6px; } -.badge-row { - font-size: clamp(6.5px, 1.8vw, 8px); - color: var(--white); - padding: 8px 10px; - border: 2px solid rgba(248,192,48,.35); - background: rgba(255,255,255,.04); - text-align: left; -} +#screen-complete { background:linear-gradient(180deg,#08031a 0%,#180a44 100%); overflow:hidden; } +.complete-scroll { overflow-y:auto; height:100%; -webkit-overflow-scrolling:touch; } +.complete-area { text-align:center; padding:clamp(18px,4.5vw,28px) clamp(12px,3.5vw,18px) max(22px,var(--safe-bottom)); display:flex; flex-direction:column; gap:12px; align-items:center; animation:fadeIn .7s ease; } +.complete-trophy { font-size:clamp(44px,13vw,64px); animation:goldShimmer 2s ease-in-out infinite; } +.complete-title { font-size:clamp(16px,5.5vw,24px); color:var(--yellow); text-shadow:3px 3px 0 #604000; animation:goldShimmer 2s ease-in-out infinite; } +.complete-name { font-size:clamp(9px,2.8vw,13px); color:var(--white); } +.complete-msg { font-size:clamp(6px,1.7vw,7.5px); color:rgba(255,255,255,.8); line-height:2.2; max-width:360px; text-align:left; } +.complete-stats { font-size:clamp(7px,2vw,8.5px); color:var(--yellow); background:rgba(255,255,255,.08); border:2px solid rgba(255,255,255,.15); padding:clamp(10px,3vw,16px); line-height:2.6; width:100%; max-width:340px; text-align:left; white-space:pre-line; } +.complete-badges { width:100%; max-width:340px; display:flex; flex-direction:column; gap:6px; } +.badge-row { font-size:clamp(6.5px,1.8vw,8px); color:var(--white); padding:8px 10px; border:2px solid rgba(248,192,48,.35); background:rgba(255,255,255,.04); text-align:left; } /* โ”€โ”€โ”€ TOAST โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.toast { - position: absolute; - bottom: max(20px, var(--safe-bottom)); - left: 50%; - transform: translateX(-50%); - background: var(--dark-bg); - border: 3px solid var(--yellow); - color: var(--yellow); - font-family: var(--font); - font-size: clamp(6.5px, 1.8vw, 8px); - padding: 10px 18px; - z-index: 950; - animation: slideUp .3s ease; - white-space: nowrap; - max-width: 90%; - overflow: hidden; - text-overflow: ellipsis; -} -.hidden { display: none !important; } +.toast { position:absolute; bottom:max(20px,var(--safe-bottom)); left:50%; transform:translateX(-50%); background:var(--dark-bg); border:3px solid var(--yellow); color:var(--yellow); font-family:var(--font); font-size:clamp(6.5px,1.8vw,8px); padding:10px 18px; z-index:950; animation:slideUp .3s ease; white-space:nowrap; max-width:90%; overflow:hidden; text-overflow:ellipsis; } +.hidden { display:none !important; }