Initial commit — PokéSQL Game
This commit is contained in:
@@ -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)
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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; }
|
||||||
Reference in New Issue
Block a user