Add GBA controller overlay, walking player, D-pad movement, shuffle answers, 1-4 labels, A/B talk to NPC, E key, localStorage save/resume, continue screen, mobile-first responsive, PokéSQL rebrand
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -46,50 +46,25 @@ const MILESTONES = {
|
||||
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 = {};
|
||||
@@ -97,89 +72,334 @@ class Game {
|
||||
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('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('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?");
|
||||
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.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 ? '_' : '');
|
||||
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() {
|
||||
@@ -251,17 +453,16 @@ class Game {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -273,65 +474,64 @@ class Game {
|
||||
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();
|
||||
})
|
||||
.then(data=>{ this.state.questions=data.levels[0].questions; cb(); })
|
||||
.catch(()=>this.toast('❌ Could not load questions.json'));
|
||||
}
|
||||
|
||||
/* ── MAP ──────────────────────────────────────────────── */
|
||||
showMap() {
|
||||
this.show('map');
|
||||
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;
|
||||
|
||||
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';
|
||||
}
|
||||
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';
|
||||
if(!dot)return; dot.style.opacity='1';
|
||||
setTimeout(()=>{ dot.style.opacity='.4'; },1500);
|
||||
}
|
||||
|
||||
/* ── BATTLE / QUESTION ────────────────────────────────── */
|
||||
/* ── BATTLE ───────────────────────────────────────────── */
|
||||
startQuestion() {
|
||||
this._stopWalkLoop();
|
||||
this._heldKeys.clear();
|
||||
const q=this.state.questions[this.state.currentQ];
|
||||
if(!q){ this.showComplete(); return; }
|
||||
|
||||
this.show('battle');
|
||||
this._showController(true);
|
||||
this.state.answering=false;
|
||||
this.state.cursor=0;
|
||||
|
||||
document.getElementById('battle-npc-sprite').textContent = NPC[q.npc]||'🧑';
|
||||
document.getElementById('battle-npc-name').textContent = q.npc.toUpperCase();
|
||||
@@ -348,38 +548,44 @@ class Game {
|
||||
renderChoices(q) {
|
||||
const container=document.getElementById('battle-choices');
|
||||
container.innerHTML='';
|
||||
['A','B','C','D'].forEach((letter, i) => {
|
||||
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', letter);
|
||||
btn.textContent = q.options[i];
|
||||
btn.addEventListener('click', () => this.pick(i, q, container));
|
||||
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) {
|
||||
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 correct=chosen===correctIdx;
|
||||
|
||||
btns.forEach(b => b.classList.add('disabled'));
|
||||
btns[q.answer].classList.remove('disabled');
|
||||
btns[q.answer].classList.add('correct');
|
||||
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.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++;
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -387,154 +593,89 @@ class Game {
|
||||
/* ── RESULT ───────────────────────────────────────────── */
|
||||
showResult(correct,q) {
|
||||
this.show('result');
|
||||
this._showController(false);
|
||||
|
||||
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');
|
||||
|
||||
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');
|
||||
|
||||
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');
|
||||
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];
|
||||
ansEl.classList.add('visible');
|
||||
}
|
||||
|
||||
expEl.textContent = q.explanation;
|
||||
document.getElementById('result-explanation').textContent=q.explanation;
|
||||
|
||||
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');
|
||||
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;
|
||||
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');
|
||||
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 = '';
|
||||
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();
|
||||
}
|
||||
else{clearInterval(this.state.twTimer);this.state.twTimer=null;if(onDone)onDone();}
|
||||
},26);
|
||||
}
|
||||
|
||||
/* ── TOAST ────────────────────────────────────────────── */
|
||||
toast(msg,ms=2600){
|
||||
const el=document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.remove('hidden');
|
||||
el.textContent=msg; el.classList.remove('hidden');
|
||||
setTimeout(()=>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(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── INIT ───────────────────────────────────────────────── */
|
||||
window.addEventListener('DOMContentLoaded',()=>{ new Game(); });
|
||||
|
||||
+39
-8
@@ -27,7 +27,7 @@
|
||||
<div class="title-stars"></div>
|
||||
<div class="title-main">
|
||||
<div class="title-logo">
|
||||
<span class="title-poke">POKÉ</span><span class="title-query">SQL</span>
|
||||
<span class="title-poke">POKÉ</span><span class="title-sql">SQL</span>
|
||||
</div>
|
||||
<div class="title-sub">Learn Microsoft T-SQL</div>
|
||||
<div class="title-edition">★ SQL TRAINER EDITION ★</div>
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="dialog-text" id="intro-text"></div>
|
||||
<div class="dialog-arrow blink" id="intro-arrow" style="display:none">▼</div>
|
||||
</div>
|
||||
<div class="tap-hint blink">TAP TO CONTINUE</div>
|
||||
<div class="tap-hint blink">TAP / PRESS A TO CONTINUE</div>
|
||||
</div>
|
||||
|
||||
<!-- OVERWORLD MAP -->
|
||||
@@ -100,17 +100,22 @@
|
||||
<span class="save-dot" id="save-dot" title="Auto-saved">💾</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overworld-bg map-world">
|
||||
<div class="overworld-bg map-world" id="map-world">
|
||||
<div class="map-trees">
|
||||
<span class="tree">🌲</span><span class="tree">🌳</span>
|
||||
<span class="tree">🌲</span><span class="tree">🌳</span><span class="tree">🌲</span>
|
||||
</div>
|
||||
<!-- Tiled path floor -->
|
||||
<div class="map-ground"></div>
|
||||
<div class="map-npc-wrap">
|
||||
<div class="npc-bubble">!</div>
|
||||
<!-- NPC with ! bubble -->
|
||||
<div class="map-npc-wrap" id="map-npc-wrap">
|
||||
<div class="npc-bubble" id="npc-bubble">!</div>
|
||||
<div class="map-npc" id="map-npc"></div>
|
||||
</div>
|
||||
<div class="map-player">🧢</div>
|
||||
<!-- Pixel player — position driven by JS -->
|
||||
<div class="map-player" id="map-player">🧢</div>
|
||||
<!-- interaction speech bubble -->
|
||||
<div class="map-talk-hint" id="map-talk-hint" style="display:none">Press A</div>
|
||||
</div>
|
||||
<div class="map-hud">
|
||||
<div class="hud-cell"><div class="hud-lbl">TRAINER</div><div class="hud-val" id="hud-name">ASH</div></div>
|
||||
@@ -118,7 +123,6 @@
|
||||
<div class="hud-cell"><div class="hud-lbl">STREAK</div><div class="hud-val" id="hud-streak">0</div></div>
|
||||
<div class="hud-cell"><div class="hud-lbl">CORRECT</div><div class="hud-val" id="hud-correct">0/0</div></div>
|
||||
</div>
|
||||
<button class="btn-pixel btn-green btn-walk" id="btn-walk">▶ TALK TO NPC</button>
|
||||
</div>
|
||||
|
||||
<!-- BATTLE / QUESTION -->
|
||||
@@ -141,7 +145,7 @@
|
||||
<div class="battle-choices" id="battle-choices"></div>
|
||||
<div class="battle-footer">
|
||||
<span>Q <span id="battle-q-num">1</span>/100</span>
|
||||
<span class="kbd-hint">1–4 keys to answer</span>
|
||||
<span class="kbd-hint">Click or tap an answer · 1–4 keys</span>
|
||||
<span>PTS: <span id="battle-pts">0</span></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,6 +199,33 @@
|
||||
|
||||
</div><!-- /#app -->
|
||||
|
||||
<!-- ═══ GBA CONTROLLER OVERLAY ═══════════════════════════════
|
||||
Shown over map + battle screens. Hidden on all others. -->
|
||||
<div id="gba-controller" class="gba-controller hidden">
|
||||
|
||||
<!-- D-PAD (left) -->
|
||||
<div class="dpad" id="dpad">
|
||||
<button class="dpad-btn dpad-up" id="dp-up" aria-label="Up">▲</button>
|
||||
<button class="dpad-btn dpad-left" id="dp-left" aria-label="Left">◀</button>
|
||||
<button class="dpad-btn dpad-center" aria-label="Center"></button>
|
||||
<button class="dpad-btn dpad-right" id="dp-right" aria-label="Right">▶</button>
|
||||
<button class="dpad-btn dpad-down" id="dp-down" aria-label="Down">▼</button>
|
||||
</div>
|
||||
|
||||
<!-- SELECT / START (center) -->
|
||||
<div class="gba-mid-btns">
|
||||
<button class="gba-mid-btn" id="btn-select">SELECT</button>
|
||||
<button class="gba-mid-btn" id="btn-start">START</button>
|
||||
</div>
|
||||
|
||||
<!-- A / B BUTTONS (right) -->
|
||||
<div class="ab-cluster">
|
||||
<button class="ab-btn ab-b" id="btn-b">B</button>
|
||||
<button class="ab-btn ab-a" id="btn-a">A</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
<script src="game.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -21,11 +21,15 @@
|
||||
--hp-green: #50c020;
|
||||
--hp-yellow: #c8b000;
|
||||
--font: 'Press Start 2P', monospace;
|
||||
|
||||
/* layout */
|
||||
--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,22 +72,11 @@ 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 ─────────────────────────────────────────── */
|
||||
@@ -108,12 +90,18 @@ html, body {
|
||||
@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;
|
||||
}
|
||||
.dpad-btn:active, .dpad-btn.pressed {
|
||||
background: #3a3a6e;
|
||||
animation: dpadPress .15s ease;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
.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;
|
||||
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-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;
|
||||
}
|
||||
#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;
|
||||
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,218 +326,52 @@ 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-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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
.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); }
|
||||
.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;
|
||||
}
|
||||
.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-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; }
|
||||
@@ -464,134 +382,70 @@ html, body {
|
||||
.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-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;
|
||||
.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); }
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ─── BATTLE ─────────────────────────────────────────────── */
|
||||
#screen-battle { flex-direction:column; }
|
||||
.battle-scene {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
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(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: 80px; }
|
||||
.battle-choices {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5px;
|
||||
padding: 5px 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.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; }
|
||||
|
||||
/* choice button */
|
||||
.choice-btn {
|
||||
background:var(--dialog-bg);
|
||||
border:3px solid var(--dialog-bd);
|
||||
@@ -605,190 +459,60 @@ html, body {
|
||||
box-shadow:2px 2px 0 var(--darkgray);
|
||||
word-break:break-word;
|
||||
hyphens:auto;
|
||||
transition: transform .06s, box-shadow .06s;
|
||||
transition:transform .06s, box-shadow .06s, border-color .08s;
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
position:relative;
|
||||
}
|
||||
.choice-btn::before {
|
||||
content: attr(data-letter) ' ';
|
||||
color: var(--blue);
|
||||
}
|
||||
.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(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;
|
||||
}
|
||||
.kbd-hint { font-size: 5.5px; color: rgba(255,255,255,.35); }
|
||||
.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);
|
||||
}
|
||||
#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 { 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;
|
||||
}
|
||||
.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; }
|
||||
#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-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;
|
||||
}
|
||||
#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-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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user