commit 30f76c447943a1b293eda2d6f56fff53373e5e5e Author: Justin Oros Date: Wed May 13 16:47:35 2026 -0700 Initial commit โ€” PokรฉSQL Game diff --git a/README.md b/README.md new file mode 100644 index 0000000..9dc5a3d --- /dev/null +++ b/README.md @@ -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) diff --git a/game.js b/game.js new file mode 100644 index 0000000..160cc30 --- /dev/null +++ b/game.js @@ -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(); }); diff --git a/index.html b/index.html new file mode 100644 index 0000000..71789a3 --- /dev/null +++ b/index.html @@ -0,0 +1,201 @@ + + + + + + + + +PokรฉSQL โ€” Learn T-SQL + + + +
+ + +
+ +
+ + +
+
+
+
+ +
Learn Microsoft T-SQL
+
โ˜… SQL TRAINER EDITION โ˜…
+
+ ๐Ÿ—„๏ธ๐Ÿ“Š๐Ÿ” +
+ +
+ +
+
+ + +
+
+ +
+
๐Ÿ“‚ CONTINUE
+
TRAINERโ€”
+
PROGRESSโ€”
+
SCOREโ€”
+ +
+
โ€” OR โ€”
+ +

+
+
+ + +
+
+
PROFESSOR OAK
+
+
+
+
_
+
+
+ + +
+
+
+ + +
+
+
๐Ÿ‘ด
+
๐Ÿงข
+
+
+
PROFESSOR OAK
+
+ +
+ +
+ + +
+
+
+ ๐Ÿ“ + PALLET TOWN +
+
+
+ 0/100 + ๐Ÿ’พ +
+
+
+
+ ๐ŸŒฒ๐ŸŒณ + ๐ŸŒฒ๐ŸŒณ๐ŸŒฒ +
+
+
+
!
+
+
+
๐Ÿงข
+
+
+
TRAINER
ASH
+
SCORE
0
+
STREAK
0
+
CORRECT
0/0
+
+ +
+ + +
+
+
+
๐Ÿ‘ด
+
OAK
+
+
+
๐Ÿงข
+
ASH
+
+
+
+
PROFESSOR OAK
+
+ +
+
+ +
+ + +
+
+
โœ“
+
CORRECT!
+
+
+
+100 PTS
+ +
+
+ + +
+
+ +
๐Ÿงข
+
ASH gained SQL EXP!
+
๐Ÿ… SQL TRAINEE
+
+
+
25 / 100
+
+ +
+
+ + +
+
+
+
๐Ÿ†
+
CHAMPION!
+
ASH
+

You've mastered Microsoft T-SQL fundamentals! You're ready to write real queries, assist DBAs, and analyse data like a pro.

+
+
+
๐Ÿฅ‰ SQL INITIATE โ€” Q25
+
๐Ÿฅˆ QUERY TRAINER โ€” Q50
+
๐Ÿฅ‡ DATA WRANGLER โ€” Q75
+
๐Ÿ† SQL CHAMPION โ€” Q100
+
+ +
+
+
+ +
+ + + + + diff --git a/questions.json b/questions.json new file mode 100644 index 0000000..45f7187 --- /dev/null +++ b/questions.json @@ -0,0 +1,1091 @@ +{ + "levels": [ + { + "id": 1, + "name": "Pallet Town", + "description": "The beginning of your SQL journey", + "questions": [ + { + "id": 1, + "npc": "Professor Oak", + "text": "Welcome, young trainer! Every great DBA starts here. In SQL, what command do we use to retrieve data from a table?", + "options": ["SELECT", "GET", "FETCH", "RETRIEVE"], + "answer": 0, + "explanation": "SELECT is the fundamental command for retrieving data from a SQL table. You'll use it in almost every query you write!" + }, + { + "id": 2, + "npc": "Professor Oak", + "text": "Excellent! Now, if I want to see ALL columns from a table called Employees, what would I write after SELECT?", + "options": ["ALL", "EVERY", "*", "#"], + "answer": 2, + "explanation": "The asterisk (*) is a wildcard that means 'all columns'. SELECT * FROM Employees returns every column in the table." + }, + { + "id": 3, + "npc": "Professor Oak", + "text": "Brilliant! After SELECT and the columns, which keyword tells SQL which table to look in?", + "options": ["IN", "FROM", "AT", "USING"], + "answer": 1, + "explanation": "FROM tells SQL which table to pull data from. A basic query looks like: SELECT * FROM TableName" + }, + { + "id": 4, + "npc": "Rival Gary", + "text": "Oh, you're learning SQL too? Fine. What keyword do we use to filter rows in a query? I bet you don't know!", + "options": ["FILTER", "SEARCH", "WHERE", "LIMIT"], + "answer": 2, + "explanation": "WHERE is used to filter rows based on a condition. Example: SELECT * FROM Employees WHERE Department = 'Sales'" + }, + { + "id": 5, + "npc": "Rival Gary", + "text": "Lucky guess! What operator checks if a value EQUALS something in a WHERE clause?", + "options": ["==", "=>", ":=", "="], + "answer": 3, + "explanation": "In T-SQL, a single equals sign (=) is used for equality comparison in WHERE clauses. Unlike most programming languages, SQL doesn't use ==." + }, + { + "id": 6, + "npc": "Old Man Bob", + "text": "Hmm, I remember learning SQL back in my day. Do you know how to sort results from lowest to highest? That's... what order?", + "options": ["ASCENDING", "ASC", "INCREASING", "LOW"], + "answer": 1, + "explanation": "ASC (short for ASCENDING) sorts results from lowest to highest. Example: SELECT * FROM Products ORDER BY Price ASC" + }, + { + "id": 7, + "npc": "Old Man Bob", + "text": "And from highest to lowest would be... DESC, right? What does DESC stand for?", + "options": ["Description", "Decrease", "Descending", "Destination"], + "answer": 2, + "explanation": "DESC stands for DESCENDING. It sorts results from highest to lowest. Example: SELECT * FROM Sales ORDER BY Amount DESC" + }, + { + "id": 8, + "npc": "Nurse Joy", + "text": "Hello! When sorting, which keyword do you use before ASC or DESC?", + "options": ["SORT BY", "ORDER BY", "ARRANGE BY", "GROUP BY"], + "answer": 1, + "explanation": "ORDER BY is the keyword used to sort results. Example: SELECT * FROM Employees ORDER BY LastName ASC" + }, + { + "id": 9, + "npc": "Nurse Joy", + "text": "What keyword limits how many rows are returned in T-SQL (Microsoft SQL Server)?", + "options": ["LIMIT", "ROWCOUNT", "TOP", "MAX"], + "answer": 2, + "explanation": "In T-SQL, TOP limits rows returned. Example: SELECT TOP 10 * FROM Employees. Note: MySQL uses LIMIT instead, but T-SQL uses TOP!" + }, + { + "id": 10, + "npc": "Officer Jenny", + "text": "I'm checking identities here! Which SQL keyword means 'is not equal to'?", + "options": ["!=", "<>", "NOT =", "Both A and B"], + "answer": 3, + "explanation": "In T-SQL, both != and <> mean 'not equal to'. <> is the ANSI standard and older style, while != also works. Both are valid!" + }, + { + "id": 11, + "npc": "Officer Jenny", + "text": "What keyword checks if a value falls within a range? For example: Price ___ 10 AND 50", + "options": ["BETWEEN", "WITHIN", "RANGE", "FROM"], + "answer": 0, + "explanation": "BETWEEN checks if a value is within a range (inclusive on both ends). Example: WHERE Price BETWEEN 10 AND 50" + }, + { + "id": 12, + "npc": "Brock", + "text": "I'm a Gym Leader AND a DBA hobbyist! What keyword searches for a pattern in text data?", + "options": ["SEARCH", "FIND", "LIKE", "MATCH"], + "answer": 2, + "explanation": "LIKE is used for pattern matching. The % wildcard means 'any characters'. Example: WHERE LastName LIKE 'Sm%' finds names starting with 'Sm'." + }, + { + "id": 13, + "npc": "Brock", + "text": "In a LIKE pattern, what wildcard character represents any SINGLE character?", + "options": ["%", "*", "_", "?"], + "answer": 2, + "explanation": "The underscore (_) represents exactly ONE character in a LIKE pattern. Example: WHERE Name LIKE '_ohn' matches 'John', 'Bohn', etc. % matches any number of characters." + }, + { + "id": 14, + "npc": "Brock", + "text": "What keyword checks if a value is in a list? Example: WHERE Color ___ ('Red', 'Blue', 'Green')", + "options": ["CONTAINS", "WITHIN", "IN", "FROM"], + "answer": 2, + "explanation": "IN checks if a value matches any value in a list. Example: WHERE Department IN ('Sales', 'HR', 'Finance'). This is cleaner than multiple OR conditions." + }, + { + "id": 15, + "npc": "Misty", + "text": "Water-type queries are my specialty! What is a NULL value in SQL?", + "options": ["Zero (0)", "An empty string", "No value / unknown", "False"], + "answer": 2, + "explanation": "NULL means the absence of any value โ€” it's unknown or missing data. NULL is NOT zero, NOT an empty string, and NOT false. It's the complete absence of a value." + }, + { + "id": 16, + "npc": "Misty", + "text": "How do you correctly check if a column IS NULL in a WHERE clause?", + "options": ["WHERE Column = NULL", "WHERE Column IS NULL", "WHERE Column == NULL", "WHERE NULL(Column)"], + "answer": 1, + "explanation": "You must use IS NULL, not = NULL. NULL cannot be compared with = because NULL is unknown. WHERE Column = NULL will never return rows!" + }, + { + "id": 17, + "npc": "Misty", + "text": "And to find rows where a column has a value (is NOT empty), you use?", + "options": ["WHERE Column != NULL", "WHERE Column <> NULL", "WHERE Column IS NOT NULL", "WHERE NOT NULL(Column)"], + "answer": 2, + "explanation": "IS NOT NULL checks that a column has an actual value. Example: WHERE Email IS NOT NULL returns only rows where Email has been filled in." + }, + { + "id": 18, + "npc": "Giovanni", + "text": "...You dare enter my domain? Tell me โ€” what function counts the number of rows returned?", + "options": ["TOTAL()", "SUM()", "COUNT()", "NUMBER()"], + "answer": 2, + "explanation": "COUNT() counts rows. COUNT(*) counts all rows including NULLs. COUNT(ColumnName) counts only non-NULL values in that column." + }, + { + "id": 19, + "npc": "Giovanni", + "text": "What function returns the total SUM of numeric values in a column?", + "options": ["TOTAL()", "ADD()", "SUM()", "CALC()"], + "answer": 2, + "explanation": "SUM() adds up all values in a numeric column. Example: SELECT SUM(Salary) FROM Employees returns the total payroll." + }, + { + "id": 20, + "npc": "Giovanni", + "text": "What function returns the AVERAGE value of a numeric column?", + "options": ["MEAN()", "AVG()", "MID()", "AVERAGE()"], + "answer": 1, + "explanation": "AVG() calculates the average. Example: SELECT AVG(Salary) FROM Employees. AVG ignores NULL values automatically." + }, + { + "id": 21, + "npc": "Team Rocket Grunt", + "text": "Prepare for trouble! What function returns the largest value in a column?", + "options": ["HIGHEST()", "TOP()", "MAX()", "LARGEST()"], + "answer": 2, + "explanation": "MAX() returns the maximum value. Works on numbers, dates, and text. Example: SELECT MAX(Salary) FROM Employees" + }, + { + "id": 22, + "npc": "Team Rocket Grunt", + "text": "Make it double! What function returns the SMALLEST value?", + "options": ["LOWEST()", "BOTTOM()", "MIN()", "FIRST()"], + "answer": 2, + "explanation": "MIN() returns the minimum value. Example: SELECT MIN(HireDate) FROM Employees returns the earliest hire date." + }, + { + "id": 23, + "npc": "Scientist Bill", + "text": "I've been studying GROUP BY! What does GROUP BY do in a query?", + "options": ["Sorts results", "Groups rows with same values for aggregate functions", "Joins tables together", "Filters results"], + "answer": 1, + "explanation": "GROUP BY groups rows that have the same values in specified columns, allowing aggregate functions (COUNT, SUM, AVG) to be applied to each group." + }, + { + "id": 24, + "npc": "Scientist Bill", + "text": "When using GROUP BY, what keyword filters GROUPS (not individual rows)?", + "options": ["WHERE", "FILTER", "HAVING", "GROUPWHERE"], + "answer": 2, + "explanation": "HAVING filters groups after GROUP BY. WHERE filters rows BEFORE grouping. Example: SELECT Dept, COUNT(*) FROM Employees GROUP BY Dept HAVING COUNT(*) > 5" + }, + { + "id": 25, + "npc": "Scientist Bill", + "text": "What is the correct ORDER of these SQL clauses? SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY", + "options": [ + "SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY", + "FROM, SELECT, WHERE, GROUP BY, HAVING, ORDER BY", + "SELECT, WHERE, FROM, GROUP BY, HAVING, ORDER BY", + "SELECT, FROM, GROUP BY, WHERE, HAVING, ORDER BY" + ], + "answer": 0, + "explanation": "The correct SQL clause order is: SELECT โ†’ FROM โ†’ WHERE โ†’ GROUP BY โ†’ HAVING โ†’ ORDER BY. Think of it as: what, from where, filtered how, grouped by, group filtered by, sorted by." + }, + { + "id": 26, + "npc": "Fisherman Ralph", + "text": "I catch data like fish! What is an ALIAS in SQL?", + "options": ["A table backup", "A temporary name for a column or table", "A type of JOIN", "A stored procedure"], + "answer": 1, + "explanation": "An alias gives a column or table a temporary name in your query. Example: SELECT FirstName AS 'First Name' FROM Employees. The AS keyword is optional but makes it readable." + }, + { + "id": 27, + "npc": "Fisherman Ralph", + "text": "What keyword is used to create an alias for a column?", + "options": ["NAMED", "CALLED", "AS", "ALIAS"], + "answer": 2, + "explanation": "AS creates an alias. Example: SELECT COUNT(*) AS TotalEmployees FROM Employees. You can also omit AS: SELECT COUNT(*) TotalEmployees โ€” both work in T-SQL." + }, + { + "id": 28, + "npc": "Hiker Taro", + "text": "Climbing data mountains! What does DISTINCT do in a SELECT query?", + "options": ["Sorts results alphabetically", "Removes duplicate rows from results", "Selects only NULL values", "Limits to 1 row"], + "answer": 1, + "explanation": "DISTINCT removes duplicate rows. Example: SELECT DISTINCT Department FROM Employees returns each department name only once, even if many employees share a department." + }, + { + "id": 29, + "npc": "Hiker Taro", + "text": "What is the purpose of a JOIN in SQL?", + "options": ["To delete rows", "To combine rows from two or more tables based on a related column", "To group data together", "To sort multiple columns"], + "answer": 1, + "explanation": "JOIN combines rows from two or more tables based on a related column (usually a key). This is how relational databases link related data together." + }, + { + "id": 30, + "npc": "Hiker Taro", + "text": "What type of JOIN returns only rows where there is a MATCH in BOTH tables?", + "options": ["LEFT JOIN", "RIGHT JOIN", "FULL JOIN", "INNER JOIN"], + "answer": 3, + "explanation": "INNER JOIN (or just JOIN) returns only rows where the join condition matches in BOTH tables. Rows with no match on either side are excluded." + }, + { + "id": 31, + "npc": "Gym Leader Surge", + "text": "SHOCKING! What does a LEFT JOIN return?", + "options": [ + "Only rows with matches in both tables", + "All rows from the LEFT table, plus matching rows from the right (NULLs if no match)", + "All rows from the RIGHT table only", + "All rows from both tables" + ], + "answer": 1, + "explanation": "LEFT JOIN returns ALL rows from the left table, and matching rows from the right table. If no match exists on the right side, NULLs fill those columns." + }, + { + "id": 32, + "npc": "Gym Leader Surge", + "text": "What does a RIGHT JOIN return?", + "options": [ + "Only matching rows", + "All rows from the LEFT table", + "All rows from the RIGHT table, plus matching left rows (NULLs if no match)", + "Only NULL rows" + ], + "answer": 2, + "explanation": "RIGHT JOIN returns ALL rows from the right table, and matching rows from the left table. NULLs appear for unmatched left-side columns. RIGHT JOIN is less common โ€” most prefer LEFT JOIN for readability." + }, + { + "id": 33, + "npc": "Gym Leader Surge", + "text": "What does a FULL OUTER JOIN return?", + "options": [ + "Only matching rows", + "All rows from the left table only", + "All rows from both tables, with NULLs where there's no match", + "Only non-matching rows" + ], + "answer": 2, + "explanation": "FULL OUTER JOIN returns ALL rows from BOTH tables. Where there's no match, NULLs fill the missing side. It combines LEFT and RIGHT JOIN results." + }, + { + "id": 34, + "npc": "Erika", + "text": "Grass-type patience is needed here. What is a PRIMARY KEY?", + "options": [ + "The first column in a table", + "A column that uniquely identifies each row and cannot be NULL", + "A password for the database", + "The most important column" + ], + "answer": 1, + "explanation": "A PRIMARY KEY uniquely identifies each row in a table. It must be unique and cannot be NULL. A table can only have ONE primary key (though it can span multiple columns)." + }, + { + "id": 35, + "npc": "Erika", + "text": "What is a FOREIGN KEY?", + "options": [ + "A key from another database system", + "A column that links to the PRIMARY KEY of another table", + "A backup primary key", + "An encrypted column" + ], + "answer": 1, + "explanation": "A FOREIGN KEY in one table points to the PRIMARY KEY of another table. This creates a relationship between tables. Example: Orders.CustomerID is a FK referencing Customers.CustomerID." + }, + { + "id": 36, + "npc": "Koga", + "text": "The art of subtlety... What data type stores whole numbers in T-SQL?", + "options": ["NUMBER", "INT", "WHOLE", "DECIMAL"], + "answer": 1, + "explanation": "INT (integer) stores whole numbers. T-SQL also has TINYINT (0-255), SMALLINT, BIGINT for different ranges. INT is the most commonly used for IDs and counts." + }, + { + "id": 37, + "npc": "Koga", + "text": "What T-SQL data type stores text of variable length, commonly used for names and descriptions?", + "options": ["TEXT", "CHAR", "VARCHAR", "STRING"], + "answer": 2, + "explanation": "VARCHAR(n) stores variable-length text up to n characters. Example: VARCHAR(255) stores up to 255 characters but only uses space for actual characters. NVARCHAR supports Unicode." + }, + { + "id": 38, + "npc": "Koga", + "text": "What T-SQL data type is best for storing money / currency values?", + "options": ["FLOAT", "CURRENCY", "MONEY", "DECIMAL"], + "answer": 2, + "explanation": "MONEY is a T-SQL-specific type for currency (precise to 4 decimal places). DECIMAL(p,s) is also used. Avoid FLOAT for money โ€” it can introduce rounding errors!" + }, + { + "id": 39, + "npc": "Sabrina", + "text": "I can see your future queries! What type stores a date AND time combined in T-SQL?", + "options": ["DATE", "TIME", "DATETIME", "TIMESTAMP"], + "answer": 2, + "explanation": "DATETIME stores both date and time. T-SQL also has DATE (date only), TIME (time only), and DATETIME2 (more precision). GETDATE() returns the current DATETIME." + }, + { + "id": 40, + "npc": "Sabrina", + "text": "What T-SQL function returns the current date and time?", + "options": ["NOW()", "CURRENT_DATE()", "GETDATE()", "TODAY()"], + "answer": 2, + "explanation": "GETDATE() returns the current date and time in T-SQL. Example: SELECT GETDATE() returns something like '2024-03-15 14:23:45.000'. This is T-SQL specific โ€” MySQL uses NOW()." + }, + { + "id": 41, + "npc": "Blaine", + "text": "Hot like a fire quiz! What SQL statement is used to INSERT new data into a table?", + "options": ["ADD INTO", "INSERT INTO", "PUT INTO", "CREATE ROW"], + "answer": 1, + "explanation": "INSERT INTO adds new rows. Example: INSERT INTO Employees (FirstName, LastName) VALUES ('Ash', 'Ketchum'). You specify columns, then values." + }, + { + "id": 42, + "npc": "Blaine", + "text": "What SQL statement MODIFIES existing data in a table?", + "options": ["MODIFY", "CHANGE", "UPDATE", "EDIT"], + "answer": 2, + "explanation": "UPDATE modifies existing rows. Example: UPDATE Employees SET Salary = 50000 WHERE EmployeeID = 1. ALWAYS use WHERE with UPDATE or you'll change EVERY row!" + }, + { + "id": 43, + "npc": "Blaine", + "text": "What SQL statement REMOVES rows from a table?", + "options": ["REMOVE", "ERASE", "DROP", "DELETE"], + "answer": 3, + "explanation": "DELETE removes rows. Example: DELETE FROM Employees WHERE EmployeeID = 1. Like UPDATE, ALWAYS use WHERE with DELETE โ€” without it, you'll delete ALL rows in the table!" + }, + { + "id": 44, + "npc": "Giovanni", + "text": "You've returned. What's the difference between DELETE and TRUNCATE?", + "options": [ + "They are identical", + "DELETE removes specific rows with WHERE; TRUNCATE removes ALL rows and is faster (cannot be rolled back easily)", + "TRUNCATE uses WHERE; DELETE doesn't", + "DELETE is for tables; TRUNCATE is for databases" + ], + "answer": 1, + "explanation": "DELETE removes specific rows (can use WHERE) and is logged row-by-row. TRUNCATE removes ALL rows instantly, is faster, and resets identity columns. TRUNCATE cannot be WHERE filtered." + }, + { + "id": 45, + "npc": "Lorelei", + "text": "Ice-cold logic! What is a VIEW in SQL?", + "options": [ + "A visual chart of the data", + "A saved SELECT query that acts like a virtual table", + "A backup copy of a table", + "A report export" + ], + "answer": 1, + "explanation": "A VIEW is a saved SELECT query stored in the database. It acts like a virtual table. Example: CREATE VIEW HighEarners AS SELECT * FROM Employees WHERE Salary > 100000" + }, + { + "id": 46, + "npc": "Lorelei", + "text": "What is the benefit of using a VIEW?", + "options": [ + "Stores data permanently", + "Simplifies complex queries, improves security by hiding columns, and can be reused", + "Makes queries run faster always", + "Replaces the need for tables" + ], + "answer": 1, + "explanation": "VIEWs simplify complex queries (hide joins/logic), improve security (hide sensitive columns like salaries), and can be reused across applications. They don't store data โ€” they run the underlying query each time." + }, + { + "id": 47, + "npc": "Bruno", + "text": "Strong queries come from practice! What is a Stored Procedure?", + "options": [ + "A saved SELECT query", + "A precompiled set of SQL statements saved in the database that can accept parameters", + "A type of table", + "A database backup process" + ], + "answer": 1, + "explanation": "A Stored Procedure (SP) is precompiled SQL code saved in the database. It can accept input/output parameters, contain logic, and be executed with EXEC ProcName. Common in T-SQL!" + }, + { + "id": 48, + "npc": "Bruno", + "text": "How do you EXECUTE a stored procedure called 'GetEmployees' in T-SQL?", + "options": ["RUN GetEmployees", "CALL GetEmployees", "EXEC GetEmployees", "START GetEmployees"], + "answer": 2, + "explanation": "EXEC (or EXECUTE) runs a stored procedure. Example: EXEC GetEmployeesByDept @DeptName = 'Sales'. T-SQL uses EXEC; other databases use CALL." + }, + { + "id": 49, + "npc": "Agatha", + "text": "Ghosts from the past! What is a TRANSACTION in SQL?", + "options": [ + "A financial record in the database", + "A single SQL statement", + "A group of SQL operations treated as one unit โ€” all succeed or all fail", + "A stored procedure" + ], + "answer": 2, + "explanation": "A TRANSACTION groups multiple SQL statements so they all succeed or all fail together. This protects data integrity. Example: transferring money requires deducting from one account AND adding to another โ€” both must succeed." + }, + { + "id": 50, + "npc": "Agatha", + "text": "What command SAVES a transaction permanently?", + "options": ["SAVE", "APPLY", "COMMIT", "END"], + "answer": 2, + "explanation": "COMMIT saves all changes made in the transaction permanently. Pattern: BEGIN TRANSACTION โ†’ (your SQL statements) โ†’ COMMIT. If something goes wrong, use ROLLBACK instead." + }, + { + "id": 51, + "npc": "Lance", + "text": "Dragon power! What command UNDOES all changes in a transaction?", + "options": ["UNDO", "REVERT", "ROLLBACK", "CANCEL"], + "answer": 2, + "explanation": "ROLLBACK undoes all changes since BEGIN TRANSACTION. This is your emergency undo button. If an error occurs mid-transaction, ROLLBACK returns the database to its previous state." + }, + { + "id": 52, + "npc": "Lance", + "text": "What T-SQL keyword is used to declare a variable?", + "options": ["VAR", "SET", "DECLARE", "DIM"], + "answer": 2, + "explanation": "DECLARE creates a variable in T-SQL. Example: DECLARE @EmployeeCount INT. T-SQL variables always start with @. Then SET @EmployeeCount = 100 or SELECT @EmployeeCount = COUNT(*) FROM Employees." + }, + { + "id": 53, + "npc": "Lance", + "text": "In T-SQL, what symbol MUST prefix a variable name?", + "options": ["$", "#", "%", "@"], + "answer": 3, + "explanation": "T-SQL variables must start with @. Example: @TotalSales, @EmployeeID, @StartDate. Double @@ means a system variable like @@ROWCOUNT or @@SERVERNAME." + }, + { + "id": 54, + "npc": "Professor Oak", + "text": "You've grown so much! What does @@ROWCOUNT return in T-SQL?", + "options": [ + "Total rows in the table", + "Number of rows affected by the last SQL statement", + "Number of tables in the database", + "Maximum row count allowed" + ], + "answer": 1, + "explanation": "@@ROWCOUNT returns the number of rows affected by the most recent statement. Useful after UPDATE or DELETE to verify how many rows were changed. Example: DELETE ... then IF @@ROWCOUNT = 0 PRINT 'Nothing deleted'" + }, + { + "id": 55, + "npc": "Nurse Joy", + "text": "What does ISNULL() do in T-SQL?", + "options": [ + "Checks if a value is NULL and returns 1 or 0", + "Replaces NULL with a specified replacement value", + "Deletes NULL rows", + "Converts a column to nullable" + ], + "answer": 1, + "explanation": "ISNULL(column, replacement) replaces NULL with the replacement value. Example: SELECT ISNULL(MiddleName, 'N/A') FROM Employees โ€” shows 'N/A' instead of NULL for employees with no middle name." + }, + { + "id": 56, + "npc": "Officer Jenny", + "text": "What is the difference between ISNULL() and COALESCE()?", + "options": [ + "They are identical", + "ISNULL takes 2 arguments; COALESCE takes multiple and returns the first non-NULL", + "COALESCE is faster", + "ISNULL only works on text" + ], + "answer": 1, + "explanation": "ISNULL(a, b) takes exactly 2 args. COALESCE(a, b, c, ...) takes multiple and returns the FIRST non-NULL value. COALESCE is ANSI standard and works across databases. Both are widely used in T-SQL." + }, + { + "id": 57, + "npc": "Brock", + "text": "What does the CAST() function do in T-SQL?", + "options": [ + "Removes decimal places", + "Converts a value from one data type to another", + "Rounds a number", + "Converts text to uppercase" + ], + "answer": 1, + "explanation": "CAST converts data types. Example: CAST('123' AS INT) converts text '123' to the number 123. Or CAST(Salary AS VARCHAR(20)) to display a number as text. CONVERT() is a T-SQL alternative with more formatting options." + }, + { + "id": 58, + "npc": "Misty", + "text": "What does LEN() return in T-SQL?", + "options": [ + "The lowest value in a column", + "The number of characters in a string", + "The length of a table", + "The line number in a script" + ], + "answer": 1, + "explanation": "LEN(string) returns the number of characters. Example: LEN('Hello') returns 5. LEN ignores trailing spaces. Useful for validation โ€” checking if an email address is too long, etc." + }, + { + "id": 59, + "npc": "Misty", + "text": "What function converts text to ALL UPPERCASE in T-SQL?", + "options": ["UPPER()", "UPPERCASE()", "CAPS()", "UCASE()"], + "answer": 0, + "explanation": "UPPER(string) converts text to uppercase. LOWER(string) converts to lowercase. Example: WHERE UPPER(LastName) = 'SMITH' โ€” useful for case-insensitive searches (though SQL Server collation also affects this)." + }, + { + "id": 60, + "npc": "Gary", + "text": "Still keeping up? What does SUBSTRING() do?", + "options": [ + "Removes part of a string", + "Extracts part of a string starting at a position", + "Checks if one string contains another", + "Splits a string into rows" + ], + "answer": 1, + "explanation": "SUBSTRING(string, start, length) extracts characters. Example: SUBSTRING('Hello World', 1, 5) returns 'Hello'. Note: T-SQL string positions start at 1, not 0!" + }, + { + "id": 61, + "npc": "Scientist Bill", + "text": "What does CONCAT() do in T-SQL?", + "options": [ + "Counts the concatenated results", + "Joins two or more strings together", + "Compares two strings", + "Splits a string" + ], + "answer": 1, + "explanation": "CONCAT() joins strings together. Example: SELECT CONCAT(FirstName, ' ', LastName) AS FullName FROM Employees. The + operator also concatenates in T-SQL: FirstName + ' ' + LastName." + }, + { + "id": 62, + "npc": "Scientist Bill", + "text": "What function extracts the YEAR from a date in T-SQL?", + "options": ["GETYEAR()", "YEAR()", "DATEPART(year,...)", "Both B and C"], + "answer": 3, + "explanation": "Both YEAR(date) and DATEPART(year, date) extract the year. Example: YEAR(HireDate) or DATEPART(year, HireDate). Similarly, MONTH() and DAY() or DATEPART(month/day,...) work the same way." + }, + { + "id": 63, + "npc": "Fisherman Ralph", + "text": "What does DATEDIFF() calculate in T-SQL?", + "options": [ + "Adds days to a date", + "Formats a date as text", + "The difference between two dates in a specified unit", + "The current date minus a number" + ], + "answer": 2, + "explanation": "DATEDIFF(unit, startdate, enddate) calculates the difference between dates. Example: DATEDIFF(year, BirthDate, GETDATE()) gives someone's age in years. Units: year, month, day, hour, minute, second." + }, + { + "id": 64, + "npc": "Fisherman Ralph", + "text": "What does DATEADD() do in T-SQL?", + "options": [ + "Returns the current date", + "Adds or subtracts a time interval from a date", + "Formats a date", + "Converts text to a date" + ], + "answer": 1, + "explanation": "DATEADD(unit, number, date) adds/subtracts time. Example: DATEADD(day, 30, GETDATE()) returns a date 30 days from now. Use negative numbers to go back: DATEADD(month, -6, GETDATE())." + }, + { + "id": 65, + "npc": "Hiker Taro", + "text": "What is a subquery?", + "options": [ + "A query that runs after the main query", + "A query nested inside another query", + "A secondary database", + "A query with multiple JOINs" + ], + "answer": 1, + "explanation": "A subquery is a SELECT statement nested inside another query. Example: SELECT * FROM Employees WHERE Salary > (SELECT AVG(Salary) FROM Employees) โ€” finds employees earning above average." + }, + { + "id": 66, + "npc": "Hiker Taro", + "text": "What is a CTE (Common Table Expression)?", + "options": [ + "A type of table", + "A temporary named result set defined with WITH that can be referenced in the main query", + "A stored procedure", + "A type of JOIN" + ], + "answer": 1, + "explanation": "A CTE uses WITH to create a named temporary result. Example: WITH HighEarners AS (SELECT * FROM Employees WHERE Salary > 100000) SELECT * FROM HighEarners. CTEs improve readability over subqueries." + }, + { + "id": 67, + "npc": "Gym Leader Surge", + "text": "What does ROUND() do in T-SQL?", + "options": [ + "Removes decimal places entirely", + "Rounds a number to a specified number of decimal places", + "Rounds up always", + "Rounds down always" + ], + "answer": 1, + "explanation": "ROUND(number, decimals) rounds to a specified decimal place. Example: ROUND(15.456, 2) returns 15.46. ROUND(15.456, 0) returns 15. Use CEILING() to always round up, FLOOR() to always round down." + }, + { + "id": 68, + "npc": "Gym Leader Surge", + "text": "What does CASE WHEN do in T-SQL?", + "options": [ + "Handles errors", + "Defines conditional logic โ€” like IF/ELSE inside a SELECT statement", + "Encrypts data", + "Creates a new column permanently" + ], + "answer": 1, + "explanation": "CASE WHEN provides conditional logic. Example: SELECT CASE WHEN Salary > 100000 THEN 'High' WHEN Salary > 50000 THEN 'Medium' ELSE 'Low' END AS SalaryBand FROM Employees" + }, + { + "id": 69, + "npc": "Erika", + "text": "What is an INDEX in SQL?", + "options": [ + "A list of all tables in the database", + "A database object that speeds up data retrieval on a column", + "The first row of a table", + "A numbered list of columns" + ], + "answer": 1, + "explanation": "An INDEX speeds up queries by creating a data structure that SQL Server can search quickly โ€” like a book's index. Without indexes, SQL Server scans every row (slow on large tables)." + }, + { + "id": 70, + "npc": "Erika", + "text": "What is the difference between a CLUSTERED and NON-CLUSTERED index?", + "options": [ + "Clustered physically sorts the table data; non-clustered creates a separate lookup structure", + "Non-clustered is faster always", + "Clustered is for text; non-clustered is for numbers", + "There is no difference" + ], + "answer": 0, + "explanation": "A CLUSTERED index physically sorts and stores the table data in order (only one per table โ€” usually the PK). A NON-CLUSTERED index is a separate structure pointing to the data rows. A table can have many non-clustered indexes." + }, + { + "id": 71, + "npc": "Koga", + "text": "What does the UNION operator do?", + "options": [ + "Joins two tables with a foreign key", + "Combines result sets of two SELECT queries (removing duplicates)", + "Returns only matching rows from two queries", + "Multiplies two result sets" + ], + "answer": 1, + "explanation": "UNION combines results from two SELECT statements and removes duplicates. Both queries must have the same number of columns and compatible data types. Use UNION ALL to keep duplicates (faster)." + }, + { + "id": 72, + "npc": "Koga", + "text": "What is the difference between UNION and UNION ALL?", + "options": [ + "No difference", + "UNION removes duplicates; UNION ALL keeps all rows including duplicates (and is faster)", + "UNION ALL removes duplicates; UNION keeps them", + "UNION ALL adds a total row" + ], + "answer": 1, + "explanation": "UNION removes duplicate rows (slower โ€” has to check). UNION ALL keeps ALL rows including duplicates and runs faster. Use UNION ALL when you know there are no duplicates or don't mind them." + }, + { + "id": 73, + "npc": "Sabrina", + "text": "What does INTERSECT do?", + "options": [ + "Returns all rows from both queries", + "Returns only rows that appear in BOTH query results", + "Returns rows in the first query but not the second", + "Joins two tables" + ], + "answer": 1, + "explanation": "INTERSECT returns only rows that exist in BOTH query results. Like a Venn diagram intersection. Less common but useful for finding common records between two datasets." + }, + { + "id": 74, + "npc": "Sabrina", + "text": "What does EXCEPT do in T-SQL?", + "options": [ + "Returns all rows from both queries", + "Returns rows from the first query that do NOT exist in the second query", + "Excludes a specific column", + "Removes NULL rows" + ], + "answer": 1, + "explanation": "EXCEPT returns rows from the FIRST query that don't appear in the SECOND query. Useful for finding records in one dataset that are missing from another. (MySQL uses MINUS instead.)" + }, + { + "id": 75, + "npc": "Blaine", + "text": "What is a TRIGGER in SQL?", + "options": [ + "A shortcut key in SQL Server Management Studio", + "Automatically executed SQL code when INSERT, UPDATE, or DELETE occurs on a table", + "A type of stored procedure that returns a value", + "A way to schedule queries" + ], + "answer": 1, + "explanation": "A TRIGGER automatically fires when a specified event (INSERT, UPDATE, DELETE) happens on a table. Used for auditing, enforcing business rules, or cascading changes. Use carefully โ€” they can slow performance." + }, + { + "id": 76, + "npc": "Lorelei", + "text": "What is a SCHEMA in SQL Server?", + "options": [ + "The visual diagram of a database", + "A container/namespace that groups related database objects together", + "A type of backup", + "The database itself" + ], + "answer": 1, + "explanation": "A schema is a namespace that organizes database objects. Example: dbo.Employees (schema 'dbo', table 'Employees'). Common schemas: dbo (default), HR, Finance, Sales. Like folders for database objects." + }, + { + "id": 77, + "npc": "Lorelei", + "text": "In T-SQL, what does SELECT TOP 1 ... ORDER BY Date DESC return?", + "options": [ + "The oldest record", + "All records sorted by date", + "The most recent (latest) single record", + "A random record" + ], + "answer": 2, + "explanation": "TOP 1 limits to 1 row. ORDER BY Date DESC sorts newest first. Combined, this returns the SINGLE most recent record. This pattern is very common in real-world T-SQL queries!" + }, + { + "id": 78, + "npc": "Bruno", + "text": "What does SELECT INTO do in T-SQL?", + "options": [ + "Moves data from one table to another permanently", + "Creates a NEW table and inserts the query results into it", + "Inserts data into an existing table", + "Creates a view" + ], + "answer": 1, + "explanation": "SELECT ... INTO NewTable FROM ... creates a NEW table and inserts query results. Example: SELECT * INTO Employees_Backup FROM Employees. The new table structure matches the query result." + }, + { + "id": 79, + "npc": "Bruno", + "text": "What is INSERT INTO ... SELECT used for?", + "options": [ + "Creates a new table", + "Copies data from one query result into an EXISTING table", + "Updates existing rows", + "Creates a view from a table" + ], + "answer": 1, + "explanation": "INSERT INTO ExistingTable SELECT ... FROM ... copies data INTO an EXISTING table. Different from SELECT INTO (which CREATES a new table). The target table must already exist and match the column structure." + }, + { + "id": 80, + "npc": "Agatha", + "text": "From the shadows... What is a TEMP TABLE in T-SQL?", + "options": [ + "A table that deletes itself after 1 hour", + "A temporary table stored in tempdb that exists only for the current session", + "A view that looks like a table", + "A table with no indexes" + ], + "answer": 1, + "explanation": "Temp tables (#TableName) are stored in tempdb and exist only for your session (or procedure). They're great for storing intermediate results. ## prefix makes it a global temp table visible to all sessions." + }, + { + "id": 81, + "npc": "Agatha", + "text": "What is a TABLE VARIABLE in T-SQL and how does it differ from a temp table?", + "options": [ + "They are identical", + "A table variable (@TableName) exists only within a batch/procedure and is stored in memory (usually)", + "Table variables are permanent", + "Table variables use disk; temp tables use memory" + ], + "answer": 1, + "explanation": "A table variable (DECLARE @MyTable TABLE(...)) is scoped to the current batch/procedure. They often live in memory (small data), are faster for small datasets, but can't have indexes easily. Temp tables are better for large datasets." + }, + { + "id": 82, + "npc": "Lance", + "text": "What does TRY...CATCH do in T-SQL?", + "options": [ + "Tests query performance", + "Handles errors gracefully โ€” runs TRY block, and if error occurs, executes CATCH block", + "Validates data before insert", + "Encrypts sensitive data" + ], + "answer": 1, + "explanation": "TRY...CATCH is T-SQL error handling. Code in TRY block runs normally. If an error occurs, execution jumps to CATCH where you can log the error, ROLLBACK a transaction, or return a friendly message." + }, + { + "id": 83, + "npc": "Lance", + "text": "What does @@ERROR return in T-SQL?", + "options": [ + "The error message text", + "The error number of the last statement (0 if no error)", + "The line number of the error", + "A list of all recent errors" + ], + "answer": 1, + "explanation": "@@ERROR returns the error number from the last T-SQL statement. 0 means success. Older code uses @@ERROR; modern T-SQL prefers TRY...CATCH with ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_LINE() functions." + }, + { + "id": 84, + "npc": "Rival Gary", + "text": "OK you're actually pretty good. What is SQL Server Management Studio (SSMS)?", + "options": [ + "A type of SQL Server database", + "Microsoft's free GUI tool for connecting to, managing, and querying SQL Server databases", + "A stored procedure for managing users", + "A cloud version of SQL Server" + ], + "answer": 1, + "explanation": "SSMS is Microsoft's free IDE for SQL Server. You use it to write and run T-SQL queries, manage databases, create tables/views/procedures, check performance, and administer SQL Server. Every DBA uses it daily!" + }, + { + "id": 85, + "npc": "Nurse Joy", + "text": "What does a query execution plan show you in SQL Server?", + "options": [ + "The query results in a graphical format", + "How SQL Server plans to physically execute your query โ€” showing operations, costs, and bottlenecks", + "A list of scheduled queries", + "The query history" + ], + "answer": 1, + "explanation": "An Execution Plan (view with Ctrl+M in SSMS) shows HOW SQL Server will execute your query โ€” table scans, index seeks, joins, sorts, and their relative cost %. Essential for query performance tuning!" + }, + { + "id": 86, + "npc": "Officer Jenny", + "text": "What does the NOLOCK hint do in a SELECT query?", + "options": [ + "Prevents other users from reading the table", + "Allows reading data without taking a lock โ€” faster but may read uncommitted/dirty data", + "Encrypts the query results", + "Prevents the query from timing out" + ], + "answer": 1, + "explanation": "WITH (NOLOCK) hint allows dirty reads โ€” reading data even if another transaction hasn't committed yet. Makes reads faster and reduces blocking, but risks reading data that gets rolled back. Common in reporting queries." + }, + { + "id": 87, + "npc": "Professor Oak", + "text": "What is a DEADLOCK in SQL Server?", + "options": [ + "When a query takes too long", + "When two processes each wait for the other to release a lock, causing an infinite wait", + "When the database runs out of storage", + "When an index is corrupt" + ], + "answer": 1, + "explanation": "A deadlock occurs when Process A holds a lock that Process B wants, and Process B holds a lock that Process A wants โ€” neither can proceed. SQL Server detects this and kills one process (the 'deadlock victim')." + }, + { + "id": 88, + "npc": "Scientist Bill", + "text": "What does ROW_NUMBER() do in T-SQL?", + "options": [ + "Counts the total rows in a table", + "Assigns a sequential number to each row within a result set partition", + "Returns the row's physical position in the table", + "Adds an ID column permanently" + ], + "answer": 1, + "explanation": "ROW_NUMBER() is a window function that numbers rows. Example: SELECT ROW_NUMBER() OVER (ORDER BY Salary DESC) AS Rank, * FROM Employees. Used for ranking, pagination, and removing duplicates." + }, + { + "id": 89, + "npc": "Scientist Bill", + "text": "What is the difference between ROW_NUMBER(), RANK(), and DENSE_RANK()?", + "options": [ + "They are identical", + "ROW_NUMBER gives unique sequential numbers; RANK skips numbers on ties; DENSE_RANK does not skip on ties", + "RANK is the fastest; others are aliases", + "ROW_NUMBER requires a PARTITION BY" + ], + "answer": 1, + "explanation": "For tied values: ROW_NUMBER gives 1,2,3 (always unique). RANK gives 1,1,3 (skips 2 after tie). DENSE_RANK gives 1,1,2 (no skipping). All are window functions used with OVER(ORDER BY ...)." + }, + { + "id": 90, + "npc": "Erika", + "text": "What does PARTITION BY do inside a window function?", + "options": [ + "Splits the database into sections", + "Resets the window function calculation for each group defined by the partition column(s)", + "Creates a backup partition", + "Divides a number evenly" + ], + "answer": 1, + "explanation": "PARTITION BY divides rows into groups for window functions. Example: ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) numbers employees 1,2,3... within EACH department separately." + }, + { + "id": 91, + "npc": "Koga", + "text": "What does the PIVOT operation do in T-SQL?", + "options": [ + "Rotates rows into columns โ€” converts unique values from one column into multiple columns", + "Sorts data clockwise", + "Transposes the entire table", + "Converts columns to rows" + ], + "answer": 0, + "explanation": "PIVOT rotates rows into columns. Example: turning a Sales table with rows for each month into a summary with columns Jan, Feb, Mar... Very useful for reporting and creating crosstab summaries." + }, + { + "id": 92, + "npc": "Blaine", + "text": "Scorching hot question! What is the IDENTITY property in T-SQL?", + "options": [ + "Stores the user's login name", + "An auto-incrementing number column โ€” SQL Server automatically generates a unique value for each new row", + "A unique constraint on a column", + "The database's license ID" + ], + "answer": 1, + "explanation": "IDENTITY(seed, increment) auto-generates values. Example: EmployeeID INT IDENTITY(1,1) starts at 1 and increments by 1 for each new row. Common for primary keys. Use @@IDENTITY or SCOPE_IDENTITY() to get the last generated value." + }, + { + "id": 93, + "npc": "Lorelei", + "text": "What is the difference between CHAR and VARCHAR data types?", + "options": [ + "No difference", + "CHAR is fixed-length (always uses full allocated space); VARCHAR is variable-length (uses only space needed)", + "VARCHAR is faster for all use cases", + "CHAR is for numbers; VARCHAR is for text" + ], + "answer": 1, + "explanation": "CHAR(10) always uses 10 characters of storage (pads with spaces). VARCHAR(10) uses only the space the actual value needs. CHAR is efficient for fixed-length data (like state codes 'CA', 'NY'). VARCHAR is better for variable-length text." + }, + { + "id": 94, + "npc": "Bruno", + "text": "What is the NVARCHAR data type used for?", + "options": [ + "Negative numbers stored as text", + "Unicode text โ€” supports international characters like Chinese, Arabic, emoji", + "Numbers with variable decimal places", + "Very long text over 8000 characters" + ], + "answer": 1, + "explanation": "NVARCHAR stores Unicode text (N stands for National). It uses 2 bytes per character, supporting any language's characters. Use NVARCHAR when your data may contain non-English characters. Prefix Unicode strings with N: N'Hรฉllo'." + }, + { + "id": 95, + "npc": "Agatha", + "text": "What does a CHECK constraint do?", + "options": [ + "Checks if the table has data", + "Enforces that column values meet a specified condition before INSERT/UPDATE", + "Verifies the database connection", + "Checks for duplicate rows" + ], + "answer": 1, + "explanation": "CHECK constraints enforce data rules. Example: CHECK (Age >= 18) prevents inserting anyone younger than 18. CHECK (Salary > 0) ensures salary is positive. They protect data integrity at the database level." + }, + { + "id": 96, + "npc": "Lance", + "text": "What is database NORMALIZATION?", + "options": [ + "Converting all text to lowercase", + "The process of organizing tables to reduce data redundancy and improve integrity", + "Sorting all tables alphabetically", + "Defragmenting the database" + ], + "answer": 1, + "explanation": "Normalization organizes data to minimize redundancy. 1NF: eliminate repeating groups. 2NF: remove partial dependencies. 3NF: remove transitive dependencies. Well-normalized databases are easier to maintain and have fewer update anomalies." + }, + { + "id": 97, + "npc": "Rival Gary", + "text": "What is the purpose of the sp_help stored procedure in SQL Server?", + "options": [ + "Returns help documentation in a browser", + "Returns information about a database object โ€” its columns, types, constraints, and indexes", + "Lists all stored procedures", + "Shows the SQL Server error log" + ], + "answer": 1, + "explanation": "EXEC sp_help 'TableName' returns detailed info about a table: columns, data types, nullability, constraints, indexes. Great for quickly understanding an unfamiliar table's structure without opening Object Explorer." + }, + { + "id": 98, + "npc": "Professor Oak", + "text": "Nearly there, trainer! What does sp_who2 return in SQL Server?", + "options": [ + "A list of all stored procedures", + "Information about current user sessions and processes โ€” who is connected and what they're running", + "The database owner information", + "All SQL Server logins" + ], + "answer": 1, + "explanation": "EXEC sp_who2 shows active connections: login names, status, blocking info, CPU, disk usage, and current command. DBAs use it to find blocking queries, idle connections, or who is running slow queries." + }, + { + "id": 99, + "npc": "Champion", + "text": "One question from glory! What is the difference between a clustered index SEEK and a TABLE SCAN in execution plans?", + "options": [ + "No difference in performance", + "Index Seek directly finds matching rows (fast); Table Scan reads every row in the table (slow on large tables)", + "Table Scan uses indexes; Index Seek does not", + "Index Seek is only for text columns" + ], + "answer": 1, + "explanation": "Index SEEK is like using a book index โ€” goes directly to the data. Table SCAN reads every single row. Seeks are almost always faster on large tables. Seeing Table Scans in execution plans often signals a missing index." + }, + { + "id": 100, + "npc": "Champion", + "text": "FINAL QUESTION! You are writing a query for management to show total sales by region, only for regions with over $1,000,000 in sales, sorted highest first. Which query structure is correct?", + "options": [ + "SELECT Region, SUM(Amount) AS TotalSales FROM Sales WHERE SUM(Amount) > 1000000 GROUP BY Region ORDER BY TotalSales DESC", + "SELECT Region, SUM(Amount) AS TotalSales FROM Sales GROUP BY Region HAVING SUM(Amount) > 1000000 ORDER BY TotalSales DESC", + "SELECT Region, SUM(Amount) AS TotalSales FROM Sales GROUP BY Region WHERE TotalSales > 1000000 ORDER BY TotalSales DESC", + "SELECT Region, SUM(Amount) AS TotalSales FROM Sales HAVING SUM(Amount) > 1000000 GROUP BY Region ORDER BY TotalSales DESC" + ], + "answer": 1, + "explanation": "Correct! GROUP BY Region groups the data, HAVING filters the groups (you can't use WHERE with aggregate functions), and ORDER BY TotalSales DESC sorts highest first. This is a real-world query pattern used every day by analysts and DBAs!" + } + ] + } + ] +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..c24bf6e --- /dev/null +++ b/style.css @@ -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; }