Initial commit — PokéSQL Game

This commit is contained in:
justin authored and GitHub committed 2026-05-13 16:47:35 -07:00
commit 30f76c4479
5 files changed
+2743

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
# 🎮 PokéSQL — Learn 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
**`https://justinoros.github.io/pokesql`**
---
## 🗂️ Files
```
pokesql/
├── index.html ← Game screens & structure
├── style.css ← GBA Pokémon aesthetic, fully responsive
├── game.js ← Game engine + localStorage save system
├── questions.json ← 100 T-SQL questions & answers
└── README.md
```
---
## 🕹️ Host on GitHub Pages (free)
1. Create a GitHub repo named **`pokesql`**
2. Upload all **4 files**: `index.html`, `style.css`, `game.js`, `questions.json`
3. Go to **Settings → Pages → Source: `main` branch, `/ (root)` folder**
4. Hit **Save** — live in ~60 seconds at `https://justinoros.github.io/pokesql`
No server, no database, no cost.
---
## 💾 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
---
## 🎯 Features
| 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 |
| Name entry | On-screen pixel keyboard, just like the real game |
| Typewriter text | Authentic dialog effect with tap-to-skip |
| 22 NPC characters | Professor Oak, Misty, Brock, Giovanni, Lance… |
| Streak bonuses | 3× = +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 |
---
## 📚 Topics Covered
| Questions | Topics |
|---|---|
| 1–15 | SELECT, FROM, WHERE, ORDER BY, TOP, BETWEEN, LIKE, IN, NULL |
| 16–25 | COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING, DISTINCT |
| 26–35 | Aliases, JOINs (INNER, LEFT, RIGHT, FULL OUTER), Keys |
| 36–50 | Data types, GETDATE(), INSERT, UPDATE, DELETE, TRUNCATE, Views, Stored Procedures |
| 51–65 | Transactions, Variables, @@ROWCOUNT, ISNULL, COALESCE, CAST, String functions, Date functions |
| 66–80 | CTEs, Subqueries, ROUND, CASE WHEN, Indexes, UNION, INTERSECT, EXCEPT, Triggers, Schemas |
| 81–100 | Temp tables, Table variables, TRY…CATCH, @@ERROR, SSMS, Execution plans, NOLOCK, Deadlocks, Window functions, PIVOT, IDENTITY, Normalization |
---
## ➕ Adding Questions
Edit `questions.json`, add to the `questions` array inside `levels[0]`:
```json
{
"id": 101,
"npc": "Professor Oak",
"text": "Your question here?",
"options": ["Option A", "Option B", "Option C", "Option D"],
"answer": 0,
"explanation": "Why Option A is correct."
}
```
`answer` is the **0-based index** of the correct option (0 = A, 1 = B, 2 = C, 3 = D).
---
## 🧑 Adding NPCs
In `game.js`, add to the `NPC` object at the top:
```js
const NPC = {
'Your NPC Name': '🧑',
...
};
```
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)
+540
View File
@@ -0,0 +1,540 @@
/* ─── NPC ROSTER ─────────────────────────────────────────── */
const NPC = {
'Professor Oak': '👴',
'Rival Gary': '😤',
'Old Man Bob': '🧓',
'Nurse Joy': '👩‍⚕️',
'Officer Jenny': '👮‍♀️',
'Brock': '🧑‍🍳',
'Misty': '🧜‍♀️',
'Giovanni': '😈',
'Team Rocket Grunt': '🚀',
'Scientist Bill': '🔬',
'Fisherman Ralph': '🎣',
'Hiker Taro': '🧗',
'Gym Leader Surge': '⚡',
'Erika': '🌸',
'Koga': '🥷',
'Sabrina': '🔮',
'Blaine': '🌋',
'Lorelei': '❄️',
'Bruno': '💪',
'Agatha': '👻',
'Lance': '🐉',
'Champion': '🏆',
'Gary': '😤',
};
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!" },
];
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: '★ ★ ★ ★' },
};
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 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 ─────────────────────────────────────────── */
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,
};
this.screens = {};
document.querySelectorAll('.screen').forEach(s => {
this.screens[s.id.replace('screen-', '')] = s;
});
this._introClickHandler = null;
this._introKeyHandler = null;
this.bindGlobalKeys();
this.boot();
}
/* ── BOOT ─────────────────────────────────────────────── */
boot() {
this.show('boot');
setTimeout(() => this.showTitle(), 1800);
}
showTitle() {
this.show('title');
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);
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();
};
};
} 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,
});
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;
this.showNameEntry();
}
/* ── NAME ENTRY ───────────────────────────────────────── */
showNameEntry() {
this.show('name');
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();
document.getElementById('btn-confirm-name').onclick = () => this.confirmName();
}
buildKeyboard() {
const grid = document.getElementById('keyboard-grid');
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-!?'.split('');
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); });
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();
}
refreshNameDisplay() {
const el = document.getElementById('name-display');
el.textContent = this.state.playerName + (this.state.playerName.length < 10 ? '_' : '');
}
confirmName() {
if (!this.state.playerName.trim()) this.state.playerName = 'ASH';
this.showIntro();
}
/* ── INTRO CUTSCENE ───────────────────────────────────── */
showIntro() {
this.show('intro');
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;
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() {
const msg = INTRO_MSGS[this.state.introStep];
if (!msg) return;
const npcEl = document.getElementById('intro-npc');
if (npcEl) npcEl.textContent = NPC[msg.npc] || '👴';
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'; }
);
}
advanceIntro() {
if (this.state.twTimer) {
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-arrow').style.display = 'block';
return;
}
this.state.introStep++;
if (this.state.introStep >= INTRO_MSGS.length) {
document.removeEventListener('keydown', this._introKeyHandler);
this.loadQuestions(() => this.showMap());
return;
}
this.renderIntroMsg();
}
/* ── LOAD QUESTIONS ───────────────────────────────────── */
loadQuestions(cb) {
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'));
}
/* ── MAP ──────────────────────────────────────────────── */
showMap() {
this.show('map');
document.getElementById('hud-name').textContent = this.state.playerName || 'ASH';
this.updateHUD();
this.updateMapNPC();
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] || '🧑';
}
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}`;
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';
}
}
flashSaveDot() {
const dot = document.getElementById('save-dot');
if (!dot) return;
dot.style.opacity = '1';
setTimeout(() => { dot.style.opacity = '.4'; }, 1500);
}
/* ── BATTLE / QUESTION ────────────────────────────────── */
startQuestion() {
const q = this.state.questions[this.state.currentQ];
if (!q) { this.showComplete(); return; }
this.show('battle');
this.state.answering = false;
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-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));
}
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));
container.appendChild(btn);
});
}
pick(chosen, q, container) {
if (this.state.answering) return;
this.state.answering = true;
const btns = container.querySelectorAll('.choice-btn');
const correct = chosen === q.answer;
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');
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++;
}
setTimeout(() => this.showResult(correct, q), 850);
}
/* ── RESULT ───────────────────────────────────────────── */
showResult(correct, q) {
this.show('result');
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');
} 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('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();
}
};
}
/* ── 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();
}
/* ── 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` +
`BEST STREAK: ${this.state.maxStreak} 🔥`;
document.getElementById('btn-play-again').onclick = () => {
clearSave();
this.startFresh();
};
}
/* ── SCREEN SWITCHER ──────────────────────────────────── */
show(name) {
Object.values(this.screens).forEach(s => s.classList.remove('active'));
const t = this.screens[name];
if (t) t.classList.add('active');
this.state.screen = name;
}
/* ── TYPEWRITER ───────────────────────────────────────── */
typeText(id, text, onDone) {
if (this.state.twTimer) { clearInterval(this.state.twTimer); this.state.twTimer = null; }
const el = document.getElementById(id);
if (!el) return;
el.textContent = '';
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);
}
/* ── TOAST ────────────────────────────────────────────── */
toast(msg, ms = 2600) {
const el = document.getElementById('toast');
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(); });
+201
View File
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0a0520">
<title>PokéSQL — Learn T-SQL</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<!-- BOOT SPLASH -->
<div id="screen-boot" class="screen active">
<div class="boot-logo">
<div class="boot-gameboy">GAME BOY ADVANCE</div>
<div class="boot-ding">♪</div>
<div class="boot-copy">© 2025 PokéSQL</div>
</div>
</div>
<!-- TITLE -->
<div id="screen-title" class="screen">
<div class="title-bg">
<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>
</div>
<div class="title-sub">Learn Microsoft T-SQL</div>
<div class="title-edition">★ SQL TRAINER EDITION ★</div>
<div class="title-deco">
<span>🗄️</span><span>📊</span><span>🔍</span>
</div>
<div class="title-tap blink">TAP / CLICK TO START</div>
</div>
<div class="title-footer">PokéSQL v1.0 · 100 Questions</div>
</div>
</div>
<!-- CONTINUE OR NEW GAME -->
<div id="screen-continue" class="screen">
<div class="continue-bg">
<div class="continue-logo">POKÉ<span>SQL</span></div>
<div class="continue-card" id="continue-card">
<div class="continue-card-header">📂 CONTINUE</div>
<div class="save-row"><span class="save-lbl">TRAINER</span><span class="save-val" id="save-name-disp">—</span></div>
<div class="save-row"><span class="save-lbl">PROGRESS</span><span class="save-val" id="save-prog-disp">—</span></div>
<div class="save-row"><span class="save-lbl">SCORE</span><span class="save-val" id="save-score-disp">—</span></div>
<button class="btn-pixel btn-green" id="btn-continue-save">▶ CONTINUE</button>
</div>
<div class="continue-or">— OR —</div>
<button class="btn-pixel btn-red" id="btn-new-game">✦ NEW GAME</button>
<p class="continue-warn" id="continue-warn"></p>
</div>
</div>
<!-- NAME ENTRY -->
<div id="screen-name" class="screen">
<div class="dialog-box name-dialog">
<div class="dialog-speaker">PROFESSOR OAK</div>
<div class="dialog-text" id="name-prompt-text"></div>
</div>
<div class="name-entry-area">
<div class="name-display-box"><span id="name-display">_</span></div>
<div class="keyboard-grid" id="keyboard-grid"></div>
<div class="name-controls">
<button class="btn-pixel btn-del" id="btn-backspace">← DEL</button>
<button class="btn-pixel btn-ok" id="btn-confirm-name">OK ▶</button>
</div>
</div>
</div>
<!-- INTRO CUTSCENE -->
<div id="screen-intro" class="screen">
<div class="overworld-bg">
<div class="intro-npc" id="intro-npc">👴</div>
<div class="intro-player">🧢</div>
</div>
<div class="dialog-box intro-dialog">
<div class="dialog-speaker" id="intro-speaker">PROFESSOR OAK</div>
<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>
<!-- OVERWORLD MAP -->
<div id="screen-map" class="screen">
<div class="map-header">
<div class="map-left">
<span class="map-pin">📍</span>
<span class="map-town-name">PALLET TOWN</span>
</div>
<div class="map-right">
<div class="prog-bar-outer"><div class="prog-bar-inner" id="map-prog-bar"></div></div>
<span class="prog-text" id="map-prog-text">0/100</span>
<span class="save-dot" id="save-dot" title="Auto-saved">💾</span>
</div>
</div>
<div class="overworld-bg 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>
<div class="map-ground"></div>
<div class="map-npc-wrap">
<div class="npc-bubble">!</div>
<div class="map-npc" id="map-npc"></div>
</div>
<div class="map-player">🧢</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>
<div class="hud-cell"><div class="hud-lbl">SCORE</div><div class="hud-val" id="hud-score">0</div></div>
<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 -->
<div id="screen-battle" class="screen">
<div class="battle-scene">
<div class="battle-npc-side">
<div class="battle-sprite" id="battle-npc-sprite">👴</div>
<div class="battle-name-tag" id="battle-npc-name">OAK</div>
</div>
<div class="battle-player-side">
<div class="battle-sprite">🧢</div>
<div class="battle-name-tag" id="battle-player-name">ASH</div>
</div>
</div>
<div class="dialog-box battle-dialog">
<div class="dialog-speaker" id="battle-speaker">PROFESSOR OAK</div>
<div class="dialog-text" id="battle-text"></div>
<div class="dialog-arrow blink" id="battle-arrow" style="display:none">▼</div>
</div>
<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>PTS: <span id="battle-pts">0</span></span>
</div>
</div>
<!-- RESULT -->
<div id="screen-result" class="screen">
<div class="result-box">
<div class="result-icon" id="result-icon">✓</div>
<div class="result-label" id="result-label">CORRECT!</div>
<div class="result-correct-ans" id="result-correct-ans"></div>
<div class="result-explanation" id="result-explanation"></div>
<div class="result-pts" id="result-pts">+100 PTS</div>
<button class="btn-pixel btn-blue btn-full" id="btn-result-cont">CONTINUE ▶</button>
</div>
</div>
<!-- LEVEL-UP / MILESTONE -->
<div id="screen-levelup" class="screen">
<div class="levelup-box">
<div class="lu-stars blink" id="lu-stars">★ BADGE EARNED! ★</div>
<div class="lu-sprite">🧢</div>
<div class="lu-text"><span id="lu-name">ASH</span> gained SQL EXP!</div>
<div class="lu-badge" id="lu-badge">🏅 SQL TRAINEE</div>
<div class="lu-prog-wrap">
<div class="lu-prog-outer"><div class="lu-prog-inner" id="lu-prog-inner"></div></div>
<div class="lu-prog-label" id="lu-prog-label">25 / 100</div>
</div>
<button class="btn-pixel btn-green btn-full" id="btn-lu-cont">CONTINUE ▶</button>
</div>
</div>
<!-- COMPLETION -->
<div id="screen-complete" class="screen">
<div class="complete-scroll">
<div class="complete-area">
<div class="complete-trophy">🏆</div>
<div class="complete-title">CHAMPION!</div>
<div class="complete-name" id="complete-name">ASH</div>
<p class="complete-msg">You've mastered Microsoft T-SQL fundamentals! You're ready to write real queries, assist DBAs, and analyse data like a pro.</p>
<div class="complete-stats" id="complete-stats"></div>
<div class="complete-badges">
<div class="badge-row">🥉 SQL INITIATE — Q25</div>
<div class="badge-row">🥈 QUERY TRAINER — Q50</div>
<div class="badge-row">🥇 DATA WRANGLER — Q75</div>
<div class="badge-row">🏆 SQL CHAMPION — Q100</div>
</div>
<button class="btn-pixel btn-red btn-full" id="btn-play-again">▶ PLAY AGAIN</button>
</div>
</div>
</div>
</div><!-- /#app -->
<div id="toast" class="toast hidden"></div>
<script src="game.js"></script>
</body>
</html>
+1091
View File
File diff suppressed because it is too large. Load diff
+794
View File
@@ -0,0 +1,794 @@
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
/* ─── TOKENS ─────────────────────────────────────────────── */
:root {
--black: #0f0f0f;
--darkgray: #2d2d2d;
--gray: #4a4a4a;
--lightgray: #8a8a8a;
--white: #e8e8e8;
--cream: #f0e8c8;
--dark-bg: #1a1a2e;
--red: #c8003a;
--red-hover: #e0004a;
--blue: #0050a0;
--blue-hover: #0068c8;
--green: #008a1e;
--green-hover:#00b026;
--yellow: #f8c030;
--dialog-bg: #f0e8c8;
--dialog-bd: #202040;
--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);
}
/* ─── RESET ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ─── BODY ───────────────────────────────────────────────── */
html, body {
height: 100%;
overflow: hidden;
background: #12082a;
background-image:
radial-gradient(ellipse at 20% 50%, rgba(80,0,120,.35) 0%, transparent 55%),
radial-gradient(ellipse at 80% 50%, rgba(0,40,120,.35) 0%, transparent 55%);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font);
}
/* ─── APP SHELL ──────────────────────────────────────────── */
#app {
position: relative;
width: min(var(--app-w), 100vw);
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);
}
/* 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
);
pointer-events: none;
z-index: 900;
}
/* ─── 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;
}
}
/* ─── SCREENS ────────────────────────────────────────────── */
.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)} }
.blink { animation: blink 1s step-end infinite; }
/* ─── SHARED COMPONENTS ──────────────────────────────────── */
/* Dialog box — the heart of the Pokémon look */
.dialog-box {
background: var(--dialog-bg);
border: 4px solid var(--dialog-bd);
position: relative;
padding: 16px 14px 12px;
flex-shrink: 0;
}
.dialog-box::before {
content: '';
position: absolute;
inset: 3px;
border: 1px solid rgba(255,255,255,.45);
pointer-events: none;
}
.dialog-speaker {
font-size: 7px;
color: var(--black);
background: var(--dialog-bg);
border: 3px solid var(--dialog-bd);
padding: 3px 8px;
position: absolute;
top: -16px;
left: 10px;
white-space: nowrap;
letter-spacing: .5px;
}
.dialog-text {
font-size: clamp(6.5px, 1.8vw, 8px);
line-height: 2;
color: var(--black);
padding-top: 2px;
min-height: 52px;
}
.dialog-arrow {
font-size: 10px;
color: var(--black);
text-align: right;
margin-top: 4px;
}
/* Pixel buttons */
.btn-pixel {
display: inline-block;
font-family: var(--font);
font-size: clamp(7px, 2vw, 9px);
padding: 10px 16px;
border: 3px solid var(--black);
cursor: pointer;
box-shadow: 3px 3px 0 var(--black);
transition: transform .05s, box-shadow .05s;
letter-spacing: .5px;
color: var(--white);
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-full { width: 100%; text-align: center; }
/* Overworld background */
.overworld-bg {
flex: 1;
position: relative;
overflow: hidden;
background:
repeating-linear-gradient(0deg, transparent 0,transparent 31px, rgba(0,80,0,.12) 31px, rgba(0,80,0,.12) 32px),
repeating-linear-gradient(90deg, transparent 0,transparent 31px, rgba(0,80,0,.12) 31px, rgba(0,80,0,.12) 32px),
linear-gradient(180deg, #6cb84e 0%, #58a83e 100%);
}
/* ─── 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),
radial-gradient(1px 1px at 43% 8%, #fff 100%,transparent),
radial-gradient(1px 1px at 58% 28%, #fff 100%,transparent),
radial-gradient(1px 1px at 73% 14%, #fff 100%,transparent),
radial-gradient(1px 1px at 87% 42%, #fff 100%,transparent),
radial-gradient(1px 1px at 33% 62%, rgba(255,255,255,.5) 100%,transparent),
radial-gradient(1px 1px at 52% 72%, rgba(255,255,255,.5) 100%,transparent),
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;
}
/* ─── 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;
}
/* ─── 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); }
/* ─── 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;
}
/* ─── 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-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-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));
}
.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;
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;
}
.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-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); }
@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;
}
/* ─── 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); }
/* ─── 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;
}
/* ─── 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; }