diff --git a/README.md b/README.md index c0e8478..9b2da01 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,7 @@ -# ๐ŸŽฎ PokรฉAzure โ€” Azure you ready? +# ๐ŸŽฎ PokรฉAzure โ€” Learn Azure Cloud -A Pokรฉmon-style Game Boy Advance browser game for **studying Microsoft Azure certification exams**! -Journey through towns, each covering a different Azure exam. Play on your phone, tablet, or PC โ€” progress saves automatically. - -> *"Gotta cert 'em all!"* - -**Current towns:** -| Town | Exam | Status | -|---|---|---| -| ๐Ÿ˜๏ธ AZ-900 Town | AZ-900: Azure Fundamentals | โœ… Available | -| ๐Ÿ™๏ธ Administrator City | AZ-104: Azure Administrator | ๐Ÿ”œ Coming soon | -| ๐Ÿ—๏ธ Developer District | AZ-204: Azure Developer | ๐Ÿ”œ Coming soon | -| ๐Ÿ”’ Security Stronghold | AZ-500: Azure Security | ๐Ÿ”œ Coming soon | - ---- +A Pokรฉmon-style Game Boy Advance browser game for learning **Microsoft Azure** and preparing for the **AZ-900** exam! +Play it on your phone, tablet, or PC โ€” progress is saved automatically to your browser. ## ๐Ÿš€ Play It Live ๐Ÿ‘‰ [Launch PokรฉAzure](https://justinoros.github.io/pokeazure) @@ -27,15 +15,20 @@ pokeazure/ โ”œโ”€โ”€ index.html โ† Game screens & structure โ”œโ”€โ”€ style.css โ† GBA Pokรฉmon aesthetic, fully responsive โ”œโ”€โ”€ game.js โ† Game engine, controller, walking, save system -โ”œโ”€โ”€ questions.json โ† Questions & answers (organised by town/exam) -โ”œโ”€โ”€ music.mp3 โ† Overworld background music (looping) -โ”œโ”€โ”€ music-rival.mp3 โ† Battle/question screen music (looping) -โ”œโ”€โ”€ player.png โ† Player sprite sheet +โ”œโ”€โ”€ config.js โ† Game-specific config (name, text, badges, milestones) +โ”œโ”€โ”€ questions.json โ† 100 AZ-900 questions & answers +โ”œโ”€โ”€ player.png โ† Player sprite sheet (24 frames, 16ร—32px) โ”œโ”€โ”€ oak.png โ† Professor Oak NPC sprite โ”œโ”€โ”€ gary.png โ† Rival Gary NPC sprite โ”œโ”€โ”€ joy.png โ† Nurse Joy NPC sprite โ”œโ”€โ”€ jenny.png โ† Officer Jenny NPC sprite -โ”œโ”€โ”€ favicon.ico โ† Browser tab icon +โ”œโ”€โ”€ brock.png โ† Brock NPC sprite +โ”œโ”€โ”€ misty.png โ† Misty NPC sprite +โ”œโ”€โ”€ taro.png โ† Hiker Taro NPC sprite +โ”œโ”€โ”€ ralph.png โ† Fisherman Ralph NPC sprite +โ”œโ”€โ”€ music.mp3 โ† Background music (overworld) +โ”œโ”€โ”€ music-rival.mp3 โ† Background music (rival/battle) +โ”œโ”€โ”€ favicon.ico โ† Browser tab icon / Pokรฉball โ””โ”€โ”€ README.md ``` @@ -44,7 +37,7 @@ pokeazure/ ## ๐Ÿ•น๏ธ Host on GitHub Pages (free) 1. Create a GitHub repo named **`pokeazure`** -2. Upload all files to the root of the repo +2. Upload all files listed above 3. Go to **Settings โ†’ Pages โ†’ Source: `main` branch, `/ (root)` folder** 4. Hit **Save** โ€” live in ~60 seconds at `https://justinoros.github.io/pokeazure` @@ -52,55 +45,76 @@ No server, no database, no cost. --- -## ๐ŸŽฎ Controls +## ๐ŸŽฎ GBA Controller Overlay -### Mobile Portrait -A semi-transparent GBA-style controller overlays the bottom of the screen, split into two panels: -- **Left panel**: D-pad + SELECT/START -- **Right panel**: A and B buttons +On mobile, a semi-transparent Game Boy Advance-style controller overlays the bottom of the screen: -### Mobile Landscape -The controller moves to side panels flanking the game screen: -- **Left panel**: D-pad + SELECT/START (stacked vertically) -- **Right panel**: A and B buttons (diagonal layout) +| Control | Action | +|---|---| +| **D-pad** | Walk the player around the map | +| **A button** (red) | Talk to NPC ยท Confirm answer ยท Advance dialog | +| **B button** (blue) | Talk to NPC ยท Advance dialog | +| **SELECT / START** | Decorative | + +The controller is hidden on desktop (โ‰ฅ900px wide) โ€” use the keyboard instead. + +--- + +## ๐Ÿ•น๏ธ Xbox / Gamepad Support + +Plug in any Xbox, PlayStation, or USB gamepad and it works automatically: + +| Input | Action | +|---|---| +| **Left stick / D-pad** | Walk on map | +| **A button** | Talk to NPC ยท Confirm answer ยท Advance dialog | +| **B button** | Talk to NPC ยท Advance dialog | +| **Start** | Confirm / advance on all screens | +| **X / Y / LB / RB** | Quick-pick answers 1/2/3/4 in battle | + +--- + +## โŒจ๏ธ PC Keyboard Controls -### PC Keyboard | Key | Action | |---|---| | **WASD / Arrow keys** | Walk player on map | | **E / Enter** | Talk to NPC ยท Confirm answer ยท Advance dialog | | **X** | Talk to NPC ยท Advance dialog | -| **1 / 2 / 3 / 4** | Quick-pick answer | +| **1 / 2 / 3 / 4** | Quick-pick answer directly | | **Space** | Advance dialog | --- -## ๐Ÿ—บ๏ธ Map & Gameplay +## ๐Ÿ—บ๏ธ Map & Walking -- Player walks freely in all 4 directions across a large 2D world -- A directional arrow indicates where the next NPC is โ€” hides when the NPC is on screen -- Walk up to an NPC and press E/A to start a question -- After answering, the next NPC spawns in a random direction (never the opposite of the last) -- Trees render above the player for depth -- NPCs float with a gentle animation - ---- - -## ๐ŸŽต Music - -- **Overworld** (`music.mp3`): plays while walking the map -- **Battle** (`music-rival.mp3`): plays during question/answer screen -- **Result screen**: silent -- Toggle mute with the ๐ŸŽต button in the HUD +- The player sprite walks freely around the overworld map using the D-pad, WASD, arrow keys, or Xbox controller +- Walking animations use the `player.png` sprite sheet with directional frames +- A blinking `โ–ถ` arrow on the right edge points toward the next NPC +- A `...` speech bubble appears above the player if you press talk before reaching the NPC +- Press **A**, **B**, **E**, or **Enter** when near the NPC to start the question +- Tapping the NPC sprite directly also triggers the conversation --- ## ๐Ÿ’พ Save System -- **Auto-saves** after every answered question -- **Continue screen** on return visits showing trainer name, progress, and score -- **Shuffled question order** is saved so continuing always resumes at the correct question -- **New Game** option prompts confirmation before erasing +Progress is stored in the player's browser (`localStorage`) automatically: + +- **Auto-saves** after every answered question and on map return +- **Shuffled question order** is saved so continuing a game resumes the exact same sequence +- **Continue screen** appears on next visit showing trainer name, progress %, and score +- **New Game** option always available โ€” prompts confirmation before erasing save +- Save clears automatically on completion so the next run starts fresh + +--- + +## ๐ŸŽต Music + +- Background music plays automatically on the map (`music.mp3`) +- Music pauses during questions and resumes on return to the map +- **๐ŸŽต / ๐Ÿ”‡ toggle** in the map header to mute/unmute +- Music resumes correctly after switching browser tabs on mobile --- @@ -108,62 +122,48 @@ The controller moves to side panels flanking the game screen: | Feature | Detail | |---|---| -| Free 2D movement | Walk in any direction, camera follows player | -| Random NPC directions | Next NPC spawns in a random direction each leg (no U-turns) | -| PNG NPC sprites | NPCs use pixel art PNG files; emoji fallback for unassigned NPCs | -| Mobile portrait & landscape | Fully optimised controller layout for both orientations | -| Single & multi-select questions | "Choose two" shows CONFIRM button; single select is immediate | -| Shuffled question order | Different order every new game | +| GBA controller overlay | D-pad + A/B buttons, semi-transparent, mobile only | +| Xbox / gamepad support | Full navigation on all screens including name entry | +| Walking player | Pixel sprite sheet, directional walk animations | +| Mobile-first design | Full-screen on phones with safe-area support, framed on desktop | +| localStorage save | Auto-save with shuffled order preserved on continue | +| Starter Pokรฉmon selection | Choose Charmander, Squirtle, or Bulbasaur at the start | +| Wild Pokรฉmon encounters | Wild Pokรฉmon spawn on the map โ€” walk into them to catch | +| Pokรฉball & catching system | Throw Pokรฉballs to catch wild Pokรฉmon and build your party | +| Pokรฉmon party | Manage a party of caught Pokรฉmon, switch your active battler | +| 100 AZ-900 questions | Randomised order every new game, shuffled answer slots | +| Multi-select questions | Some questions require choosing two correct answers | +| Numbered answers | Options labelled 1/2/3/4 โ€” no A/B/C/D pattern to exploit | +| PNG NPC sprites | NPCs can use image files or emoji | +| Name entry | On-screen pixel keyboard navigable by gamepad | +| Typewriter text | Authentic dialog effect, tap/press/gamepad to skip | +| `...` proximity bubble | Shows above player when pressing talk too far from NPC | +| Background music | Loops on map, pauses during questions, mute toggle | | Streak bonuses | 3ร— correct = +150 pts ๐Ÿ”ฅ, 5ร—+ = +200 pts ๐Ÿ”ฅ | -| Milestone badges | Cloud Explorer (Q25), Azure Apprentice (Q50), Cloud Practitioner (Q75), AZ-900 Champion (Q100) | +| Milestone badges | Earned at Q25, Q50, Q75, Q100 | | Correct answer reveal | Wrong answers show the right answer + full explanation | -| Tap anywhere to continue | Result screen advances on any tap/click, not just CONTINUE button | -| Rival battle music | Separate music track plays during questions | -| Player faces last direction | Idle sprite faces the last direction walked | +| Scanline overlay | Authentic CRT/GBA screen effect | --- -## ๐Ÿ“š AZ-900 Topics Covered (AZ-900 Town) +## ๐Ÿ“š Topics Covered -| Domain | Topics | +| Questions | Topics | |---|---| -| Cloud Concepts | IaaS / PaaS / SaaS, public / private / hybrid cloud, CapEx vs OpEx, elasticity, scalability, high availability, disaster recovery | -| Core Azure Services | Regions, availability zones, resource groups, subscriptions, management groups, VMs, App Service, Azure Functions, Blob/File storage tiers, VNet, VPN Gateway, ExpressRoute, Load Balancer, Cosmos DB, Azure SQL | -| Security & Identity | Microsoft Entra ID, RBAC, MFA, Conditional Access, SSO, Key Vault, NSGs, Azure Firewall, DDoS Protection, Microsoft Defender for Cloud, defence in depth, shared responsibility model | -| Management & Governance | Azure Policy, Blueprints, Resource locks, Tags, ARM templates, Azure CLI, Azure PowerShell, Cloud Shell, Azure Portal, Azure Arc, Azure Migrate, Microsoft Purview | -| Monitoring & Health | Azure Monitor, Azure Advisor, Azure Service Health, Application Insights, health advisories, RCA reports | -| Pricing & Cost Management | Pricing Calculator, TCO Calculator, Azure Reservations, Spot VMs, Cost Management + Billing, budgets, pay-as-you-go | - ---- - -## โž• Adding a New Town (Exam) - -Add a new level object to the `levels` array in `questions.json`: - -```json -{ - "levels": [ - { - "id": 1, - "name": "AZ-900 Town", - "exam": "AZ-900", - "questions": [ ... ] - }, - { - "id": 2, - "name": "Administrator City", - "exam": "AZ-104", - "questions": [ ... ] - } - ] -} -``` +| 1โ€“15 | Regions, availability zones, VPNs, virtual networks, resource tags, reservations, locks | +| 16โ€“25 | Azure Cloud Shell, CLI tools, Advisor, Monitor, Service Health, Application Insights | +| 26โ€“35 | Subscriptions, management groups, VMs, serverless, Blob storage tiers | +| 36โ€“50 | Defense in depth, SSO, Conditional Access, RBAC, MFA, cloud models (public/private/hybrid) | +| 51โ€“65 | IaaS / PaaS / SaaS, cloud pricing, Cost Management, regions, resource groups, Entra ID | +| 66โ€“80 | NSGs, ExpressRoute, Load Balancer, App Service, Cosmos DB, CapEx vs OpEx, Well-Architected Framework | +| 81โ€“100 | ARM Templates, Azure Portal, CLI, least privilege, Defender, shared responsibility, Blueprints, GRS, CAF, Private Link, Arc, DevOps, Sentinel, MFA, elasticity | --- ## โž• Adding Questions -**Single-answer:** +Edit `questions.json`, add to the `questions` array inside `levels[0]`: + ```json { "id": 101, @@ -175,58 +175,47 @@ Add a new level object to the `levels` array in `questions.json`: } ``` -**Multi-select (choose two):** +For a multi-select question (choose two), use `answers` instead of `answer`: + ```json { "id": 102, - "npc": "Nurse Joy", - "text": "Which two options are correct? (Choose two)", - "options": ["Option A", "Option B", "Option C", "Option D"], + "npc": "Misty", + "text": "Which two of these are Azure compute services? (Choose two)", + "options": ["Azure VMs", "Azure Blob", "Azure Functions", "Azure DNS"], "answers": [0, 2], - "explanation": "Options A and C are correct because..." + "explanation": "Azure VMs and Azure Functions are both compute services." } ``` +`answer` / `answers` use the **0-based index** of the correct option(s). The game shuffles display order randomly at runtime. + --- -## ๐Ÿง‘ Adding NPC Sprites +## ๐Ÿง‘ Adding NPCs -In `game.js`, update the `NPC` object at the top: +In `game.js`, add to the `NPC` object: ```js const NPC = { - 'Professor Oak': 'oak.png', // PNG file in repo root - 'Nurse Joy': 'joy.png', - 'Brock': '๐Ÿง—', // emoji fallback still works + 'Your NPC Name': 'npc.png', // PNG sprite file in repo root + 'Another NPC': '๐Ÿง‘', // or an emoji }; ``` -- PNG filenames are relative to the repo root (no path needed) -- Any NPC not in the object falls back to `๐Ÿง‘` +Then reference `"npc": "Your NPC Name"` in `questions.json`. PNG files are auto-prefixed with `./` so just use the filename. --- -## ๐Ÿ”ง Rebranding (CONFIG object) +## โš™๏ธ Customising the Game -To create a version for a different exam set, update the `CONFIG` object at the top of `game.js`: - -```js -const CONFIG = { - gameName: 'PokรฉAzure', - subject: 'Azure', - examName: 'AZ-900', - townName: 'AZ-900 Town', - saveKey: 'pokeazure_save_v1', - introLines: [ ... ], - namePrompt: '...', -}; -``` +All game-specific text lives in **`config.js`** โ€” the only file that differs between PokรฉAzure and its sister game PokรฉSQL. Edit it to change the game name, town name, intro dialog, badge names, battle move names, and more without touching `game.js` or `index.html`. --- ## ๐Ÿ“ฑ Mobile Tips - Add to Home Screen on iOS/Android for a full-screen app-like experience -- The game uses `100dvh` and `env(safe-area-inset-*)` for notched phones -- Tap anywhere on the result screen to continue (no need to find the CONTINUE button) -- Rotate to landscape for a wider game view with side-panel controls +- The game uses `100dvh` and `env(safe-area-inset-bottom)` for notched phones (iPhone X+) +- Tap anywhere on the dialog box during the intro to skip the typewriter and advance +- Tap the NPC sprite directly on the map as an alternative to pressing A/B diff --git a/config.js b/config.js new file mode 100644 index 0000000..cfb188d --- /dev/null +++ b/config.js @@ -0,0 +1,33 @@ +const CONFIG = { + gameName: 'PokรฉAzure', + subject: 'Azure', + examName: 'AZ-900', + townName: 'Azure Town', + saveKey: 'pokeazure_save_v2', + titleText: 'AZURE', + titleEdition: 'โ˜… AZURE TRAINER โ˜…', + titleFooter: 'PokรฉAZURE v1.0 ยท AZ-900 ยท github.com/JustinOros', + townNameUpper:'AZURE TOWN', + continueLogoSuffix: 'AZURE', + expLabel: 'AZURE EXP', + battleMove: 'AZURE KNOWLEDGE', + battleMiss: 'Azure Cloud', + completeMsg: "You've conquered Azure Town and mastered AZ-900! Your Azure journey is just beginning โ€” more towns and exams await!", + badges: [ + '๐Ÿฅ‰ AZURE EXPLORER โ€” Q25', + '๐Ÿฅˆ AZURE APPRENTICE โ€” Q50', + '๐Ÿฅ‡ AZURE PRACTITIONER โ€” Q75', + '๐Ÿ† AZURE CHAMPION โ€” Q100', + ], + milestones: { + 25: { badge: '๐Ÿฅ‰ AZURE EXPLORER', stars: 'โ˜… โ˜… โ˜† โ˜†' }, + 50: { badge: '๐Ÿฅˆ AZURE APPRENTICE', stars: 'โ˜… โ˜… โ˜… โ˜†' }, + 75: { badge: '๐Ÿฅ‡ AZURE PRACTITIONER', stars: 'โ˜… โ˜… โ˜… โ˜…' }, + }, + introLines: [ + "Hello {name}! Welcome to PokรฉAzure โ€” the world of Azure certification training! My name is Professor Oak, the Azure Pokรฉmon Professor!", + "Your mission? Journey through AZ-900 Town, challenge Azure experts, and conquer 100 certification questions to become the ultimate Azure Trainer!", + "But first... every trainer needs a partner Pokรฉmon! Let's choose yours, {name}!", + ], + namePrompt: "Hello there! Welcome to PokรฉAzure! My name is Professor Oak โ€” the Azure Pokรฉmon Professor. Now tell me, what is your name?", +}; diff --git a/game.js b/game.js index f69778a..4c7af7d 100644 --- a/game.js +++ b/game.js @@ -1,21 +1,53 @@ -/* โ”€โ”€โ”€ NPC ROSTER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -const CONFIG = { - gameName: 'PokรฉAzure', - subject: 'Azure', - examName: 'AZ-900', - townName: 'AZ-900 Town', - saveKey: 'pokeazure_save_v1', - introLines: [ - "Hello {name}! Welcome to PokรฉAzure โ€” the world of Azure certification training! My name is Professor Oak, the Azure Pokรฉmon Professor!", - "This world is inhabited by powerful cloud services โ€” Compute, Storage, Networking, and Security โ€” mastered with knowledge!", - "Your mission? Journey through each town, challenge Azure experts, and conquer every certification exam!", - "Start in AZ-900 Town with AZ-900 โ€” then travel the Azure world to tackle Administrator, Developer, Security, and beyond!", - "100 questions await you. Each correct answer earns you Azure EXP โ€” and you'll learn something real!", - "Your progress is saved automatically in your browser so you can pick up right where you left off. Now, {name}... your adventure begins!", - ], - namePrompt: "Hello there! Welcome to PokรฉAzure! My name is Professor Oak โ€” the Azure Pokรฉmon Professor. Now tell me, what is your name?", -}; +const ALL_POKEMON = [ + { name:'Charmander', emoji:'๐Ÿ”ฅ', type:'Fire' }, + { name:'Squirtle', emoji:'๐Ÿ’ง', type:'Water' }, + { name:'Bulbasaur', emoji:'๐ŸŒฟ', type:'Grass' }, + { name:'Pikachu', emoji:'โšก', type:'Electric' }, + { name:'Eevee', emoji:'๐ŸฆŠ', type:'Normal' }, + { name:'Jigglypuff', emoji:'๐ŸŽ€', type:'Fairy' }, + { name:'Gengar', emoji:'๐Ÿ‘ป', type:'Ghost' }, + { name:'Snorlax', emoji:'๐Ÿ˜ด', type:'Normal' }, + { name:'Machamp', emoji:'๐Ÿ’ช', type:'Fighting' }, + { name:'Alakazam', emoji:'๐Ÿ”ฎ', type:'Psychic' }, + { name:'Gyarados', emoji:'๐Ÿ‰', type:'Dragon' }, + { name:'Lapras', emoji:'๐Ÿ‹', type:'Water' }, + { name:'Dragonite', emoji:'๐Ÿฒ', type:'Dragon' }, + { name:'Mewtwo', emoji:'๐Ÿงฌ', type:'Psychic' }, + { name:'Arcanine', emoji:'๐Ÿ•', type:'Fire' }, + { name:'Rapidash', emoji:'๐Ÿฆ„', type:'Fire' }, + { name:'Geodude', emoji:'๐Ÿชจ', type:'Rock' }, + { name:'Onix', emoji:'๐Ÿ', type:'Rock' }, + { name:'Haunter', emoji:'๐Ÿ˜ˆ', type:'Ghost' }, + { name:'Abra', emoji:'๐Ÿง˜', type:'Psychic' }, + { name:'Pidgeot', emoji:'๐Ÿฆ…', type:'Flying' }, + { name:'Butterfree', emoji:'๐Ÿฆ‹', type:'Bug' }, + { name:'Sandslash', emoji:'๐Ÿฆ”', type:'Ground' }, + { name:'Ninetales', emoji:'๐ŸฆŠ', type:'Fire' }, + { name:'Tentacruel', emoji:'๐Ÿ™', type:'Water' }, + { name:'Magnemite', emoji:'๐Ÿงฒ', type:'Electric' }, + { name:'Cloyster', emoji:'๐Ÿš', type:'Ice' }, + { name:'Hitmonlee', emoji:'๐Ÿฆต', type:'Fighting' }, + { name:'Starmie', emoji:'โญ', type:'Water' }, + { name:'Scyther', emoji:'๐Ÿฆ—', type:'Bug' }, + { name:'Magikarp', emoji:'๐ŸŸ', type:'Water' }, + { name:'Ditto', emoji:'๐ŸŸฃ', type:'Normal' }, + { name:'Voltorb', emoji:'๐Ÿ”ด', type:'Electric' }, + { name:'Cubone', emoji:'๐Ÿฆด', type:'Ground' }, + { name:'Chansey', emoji:'๐Ÿฅš', type:'Normal' }, + { name:'Kangaskhan', emoji:'๐Ÿฆ˜', type:'Normal' }, + { name:'Pinsir', emoji:'๐Ÿชฒ', type:'Bug' }, + { name:'Tauros', emoji:'๐Ÿ‚', type:'Normal' }, + { name:'Vaporeon', emoji:'๐Ÿ’Ž', type:'Water' }, + { name:'Flareon', emoji:'๐ŸŒ‹', type:'Fire' }, + { name:'Jolteon', emoji:'โšก', type:'Electric' }, +]; + +const STARTER_POOL = [ + { name:'Charmander', emoji:'๐Ÿ”ฅ', type:'Fire' }, + { name:'Squirtle', emoji:'๐Ÿ’ง', type:'Water' }, + { name:'Bulbasaur', emoji:'๐ŸŒฟ', type:'Grass' }, +]; function setNPC(el, val) { if (!el) return; @@ -61,22 +93,22 @@ const INTRO_MSGS = CONFIG.introLines.map(text => ({ speaker:'PROFESSOR OAK', npc 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:'๐Ÿฅ‰ CLOUD EXPLORER', stars:'โ˜… โ˜… โ˜† โ˜†' }, - 50: { badge:'๐Ÿฅˆ AZURE APPRENTICE', stars:'โ˜… โ˜… โ˜… โ˜†' }, - 75: { badge:'๐Ÿฅ‡ CLOUD PRACTITIONER', stars:'โ˜… โ˜… โ˜… โ˜…' }, -}; const SAVE_KEY = CONFIG.saveKey; +const MILESTONES = CONFIG.milestones; -/* โ”€โ”€โ”€ SAVE / LOAD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ function saveGame(s) { - try { localStorage.setItem(SAVE_KEY, JSON.stringify({ playerName:s.playerName, currentQ:s.currentQ, score:s.score, streak:s.streak, maxStreak:s.maxStreak, correct:s.correct, wrong:s.wrong, questionOrder:s.questions.map(q=>q.id), savedAt:Date.now() })); } catch(_){} + try { localStorage.setItem(SAVE_KEY, JSON.stringify({ + playerName:s.playerName, currentQ:s.currentQ, score:s.score, + streak:s.streak, maxStreak:s.maxStreak, correct:s.correct, wrong:s.wrong, + questionOrder:s.questions.map(q=>q.id), savedAt:Date.now(), + party:s.party, pokeballs:s.pokeballs, playerLevel:s.playerLevel, + caughtCount:s.caughtCount, + })); } catch(_){} } function loadGame() { try { const r=localStorage.getItem(SAVE_KEY); return r?JSON.parse(r):null; } catch(_){return null;} } function clearSave() { try { localStorage.removeItem(SAVE_KEY); } catch(_){} } -/* โ”€โ”€โ”€ BACKGROUND MUSIC โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ const Music = (() => { let audio = null; let rival = null; @@ -87,7 +119,7 @@ const Music = (() => { if (!audio) { audio = new Audio('./music.mp3'); audio.loop = true; - audio.volume = 0.5; + audio.volume = 0.375; audio.preload = 'auto'; audio.addEventListener('canplaythrough', () => { if (wantPlay && !muted) audio.play().catch(() => {}); @@ -100,7 +132,7 @@ const Music = (() => { if (!rival) { rival = new Audio('./music-rival.mp3'); rival.loop = true; - rival.volume = 0.5; + rival.volume = 0.375; rival.preload = 'auto'; } return rival; @@ -152,7 +184,6 @@ const Music = (() => { }; })(); -/* โ”€โ”€โ”€ CHIPTUNE SOUND ENGINE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ const SFX = (() => { let ctx = null; @@ -174,7 +205,6 @@ const SFX = (() => { } }); - /* Core: play a tone with envelope */ function tone(freq, type, vol, attack, sustain, release, when) { const c = getCtx(); const t = when ?? c.currentTime; @@ -192,7 +222,6 @@ const SFX = (() => { osc.stop(t + attack + sustain + release + 0.01); } - /* Sequence of notes: [{f, d}...] */ function seq(notes, type='square', vol=0.18) { const c = getCtx(); let t = c.currentTime + 0.02; @@ -203,89 +232,48 @@ const SFX = (() => { } return { - /* Boot jingle โ€” classic rising fanfare */ - boot() { - seq([ - {f:262,d:.1},{f:330,d:.1},{f:392,d:.1},{f:523,d:.25} - ], 'square', 0.2); - }, - - /* Overworld bgm loop โ€” simple cheerful melody */ - overworldStart() { - seq([ - {f:392,d:.12},{f:440,d:.12},{f:494,d:.12},{f:523,d:.18}, - {f:494,d:.1}, {f:440,d:.1}, {f:392,d:.18}, - {f:330,d:.12},{f:392,d:.12},{f:440,d:.12},{f:392,d:.24}, - ], 'square', 0.12); - }, - - /* Footstep click โ€” tiny blip each walk tick */ - step() { - tone(180, 'square', 0.04, 0.005, 0.02, 0.03); - }, - - /* NPC encounter โ€” ascending two-note ding */ - encounter() { - seq([{f:523,d:.1},{f:659,d:.18}], 'square', 0.22); - }, - - /* Typewriter blip per character */ - type() { - tone(880 + Math.random()*200, 'square', 0.03, 0.005, 0.01, 0.02); - }, - - /* Select / menu move */ - select() { - const c = getCtx(); - tone(440, 'square', 0.12, 0.005, 0.03, 0.04, c.currentTime + 0.05); - }, - - /* Correct answer โ€” happy ascending chord */ - correct() { - seq([ - {f:523,d:.08},{f:659,d:.08},{f:784,d:.08},{f:1047,d:.2} - ], 'square', 0.18); - }, - - /* Streak bonus โ€” extra flourish */ - streak() { - seq([ - {f:523,d:.06},{f:659,d:.06},{f:784,d:.06}, - {f:1047,d:.06},{f:1319,d:.2} - ], 'square', 0.18); - }, - - /* Wrong answer โ€” descending buzz */ - wrong() { - seq([ - {f:330,d:.1},{f:277,d:.1},{f:233,d:.18} - ], 'sawtooth', 0.15); - }, - - /* Badge / level up fanfare */ - levelUp() { - seq([ - {f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:659,d:.1}, - {f:784,d:.1},{f:1047,d:.3} - ], 'square', 0.2); - }, - - /* Champion / completion โ€” full fanfare */ - complete() { - seq([ - {f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:1047,d:.1}, - {f:784,d:.08},{f:880,d:.08},{f:1047,d:.08},{f:1319,d:.4} - ], 'square', 0.2); - }, - - /* Menu confirm (name entry OK) */ - confirm() { - seq([{f:523,d:.08},{f:784,d:.15}], 'square', 0.18); - }, + boot() { seq([{f:262,d:.1},{f:330,d:.1},{f:392,d:.1},{f:523,d:.25}], 'square', 0.2); }, + overworldStart() { seq([{f:392,d:.12},{f:440,d:.12},{f:494,d:.12},{f:523,d:.18},{f:494,d:.1},{f:440,d:.1},{f:392,d:.18},{f:330,d:.12},{f:392,d:.12},{f:440,d:.12},{f:392,d:.24}], 'square', 0.12); }, + step() { tone(180, 'square', 0.04, 0.005, 0.02, 0.03); }, + encounter() { seq([{f:523,d:.1},{f:659,d:.18}], 'square', 0.22); }, + type() { tone(880 + Math.random()*200, 'square', 0.03, 0.005, 0.01, 0.02); }, + select() { const c = getCtx(); tone(440, 'square', 0.12, 0.005, 0.03, 0.04, c.currentTime + 0.05); }, + backspace() { seq([{f:350,d:.05},{f:250,d:.08}], 'triangle', 0.12); }, + correct() { seq([{f:523,d:.08},{f:659,d:.08},{f:784,d:.08},{f:1047,d:.2}], 'square', 0.18); }, + streak() { seq([{f:523,d:.06},{f:659,d:.06},{f:784,d:.06},{f:1047,d:.06},{f:1319,d:.2}], 'square', 0.18); }, + wrong() { seq([{f:330,d:.1},{f:277,d:.1},{f:233,d:.18}], 'sawtooth', 0.15); }, + levelUp() { seq([{f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:659,d:.1},{f:784,d:.1},{f:1047,d:.3}], 'square', 0.2); }, + complete() { seq([{f:523,d:.1},{f:659,d:.1},{f:784,d:.1},{f:1047,d:.1},{f:784,d:.08},{f:880,d:.08},{f:1047,d:.08},{f:1319,d:.4}], 'square', 0.2); }, + confirm() { seq([{f:523,d:.08},{f:784,d:.15}], 'square', 0.18); }, + catch() { seq([{f:330,d:.08},{f:440,d:.08},{f:523,d:.08},{f:659,d:.15},{f:784,d:.25}], 'square', 0.2); }, + pokeball() { seq([{f:600,d:.06},{f:500,d:.06},{f:400,d:.06},{f:300,d:.1}], 'triangle', 0.15); }, + pokeballBounce() { seq([{f:300,d:.04},{f:500,d:.06},{f:200,d:.08}], 'triangle', 0.12); }, + escape() { seq([{f:400,d:.08},{f:350,d:.08},{f:250,d:.15}], 'sawtooth', 0.12); }, + attack() { seq([{f:200,d:.04},{f:400,d:.04},{f:600,d:.04},{f:800,d:.03},{f:1000,d:.03},{f:600,d:.06}], 'sawtooth', 0.2); }, + battleWin() { seq([{f:523,d:.1},{f:659,d:.1},{f:784,d:.12},{f:1047,d:.15},{f:1319,d:.25}], 'square', 0.22); }, + battleLose() { seq([{f:400,d:.12},{f:350,d:.12},{f:300,d:.14},{f:200,d:.25}], 'sawtooth', 0.18); }, }; })(); -/* โ”€โ”€โ”€ GAME โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +function getPlayerLevel(state) { + return Math.max(1, Math.floor(state.correct * 1.2) + 1); +} + +function randomPokemonForNPC() { + const pool = ALL_POKEMON.filter(p => !STARTER_POOL.find(s => s.name === p.name)); + const pick = pool[Math.floor(Math.random() * pool.length)]; + const level = Math.floor(Math.random() * 15) + 1; + return { ...pick, level }; +} + +function randomWildPokemon(playerLevel) { + const pick = ALL_POKEMON[Math.floor(Math.random() * ALL_POKEMON.length)]; + const minLvl = Math.max(1, playerLevel - 3); + const maxLvl = playerLevel + 5; + const level = Math.floor(Math.random() * (maxLvl - minLvl + 1)) + minLvl; + return { ...pick, level }; +} + class Game { constructor() { this.state = { @@ -298,6 +286,15 @@ class Game { pathDir: 'right', lastDir: 'right', cursor:0, + party: [], + pokeballs: 0, + playerLevel: 1, + caughtCount: 0, + activePokemon: null, + wildWorldX: 0, wildWorldY: 0, + wildSpawned: false, + currentWild: null, + currentNPCPokemon: null, }; this.screens = {}; @@ -317,9 +314,6 @@ class Game { this.boot(); } - /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - CONTROLLER OVERLAY โ€” wires D-pad, A, B, SELECT, START - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ _bindController() { const dpadMap = { 'dp-up':'up', 'dp-down':'down', 'dp-left':'left', 'dp-right':'right' }; @@ -347,36 +341,97 @@ class Game { bBtn.addEventListener('pointerup', (e)=>{ e.preventDefault(); bBtn.classList.remove('pressed'); }); bBtn.addEventListener('pointercancel',(e)=>{ bBtn.classList.remove('pressed'); }); - document.getElementById('btn-start').addEventListener('pointerdown', (e)=>{ e.preventDefault(); }); + document.getElementById('btn-start').addEventListener('pointerdown', (e)=>{ e.preventDefault(); if (this.state.screen === 'name') { this.confirmName(); } else { this._pressA(); } }); document.getElementById('btn-select').addEventListener('pointerdown', (e)=>{ e.preventDefault(); }); } - /* A โ€” map: talk to NPC | battle: confirm cursor | elsewhere: advance */ _pressA() { const s = this.state.screen; + if (s === 'title') { document.getElementById('screen-title')?.click(); return; } + if (s === 'name') { this._nameKeyConfirm(); return; } + if (s === 'continue') { + if (this._contCursor === 1) document.getElementById('btn-new-game')?.click(); + else document.getElementById('btn-continue-save')?.click(); + return; + } if (s === 'map') { this._talkToNPC(); return; } if (s === 'battle' && !this.state.answering) { this._confirmCursor(); return; } if (s === 'intro') { this.advanceIntro(); return; } + if (s === 'party-select') { this._partySelectConfirm(); return; } + if (s === 'starter') { this.pickStarter(this._starterCursor||0); return; } + if (s === 'starter-confirm') { document.getElementById('btn-starter-continue')?.click(); return; } + if (s === 'catch') { const btns = document.querySelectorAll('.catch-action-btn'); if (btns[this._catchCursor||0]) btns[this._catchCursor||0].click(); return; } + if (s === 'catch-result') { document.getElementById('btn-catch-continue')?.click(); return; } if (s === 'result') { document.getElementById('btn-result-cont')?.click(); return; } if (s === 'levelup') { document.getElementById('btn-lu-cont')?.click(); return; } } - /* B โ€” map: talk to NPC (same as A) | battle: no-op while answering | intro/result: advance */ _pressB() { const s = this.state.screen; + if (s === 'name') { this.delChar(); return; } if (s === 'map') { this._talkToNPC(); return; } if (s === 'battle' && this.state.answering) { return; } + if (s === 'battle' && !this.state.answering) { this._confirmCursor(); return; } if (s === 'intro') { this.advanceIntro(); return; } + if (s === 'catch') { const btns = document.querySelectorAll('.catch-action-btn'); if (btns[this._catchCursor||0]) btns[this._catchCursor||0].click(); return; } if (s === 'result') { document.getElementById('btn-result-cont')?.click(); return; } + if (s === 'starter-confirm') { document.getElementById('btn-starter-continue')?.click(); return; } + if (s === 'catch-result') { document.getElementById('btn-catch-continue')?.click(); return; } + if (s === 'levelup') { document.getElementById('btn-lu-cont')?.click(); return; } } - /* D-pad start hold */ _dpadStart(dir) { + const s = this.state.screen; + if (s === 'name') { + const COLS = 10; + const keys = this._getNameKeys(); + const total = keys.length; + let c = this._gpKeyCursor; + const row = Math.floor(c / COLS); + const col = c % COLS; + const totalRows = Math.ceil(total / COLS); + if (dir === 'right') { + c = (col + 1 >= COLS || c + 1 >= total) ? row * COLS : c + 1; + } + if (dir === 'left') { + c = (col - 1 < 0) ? Math.min(row * COLS + COLS - 1, total - 1) : c - 1; + } + if (dir === 'down') { + c = c + COLS >= total ? col : c + COLS; + } + if (dir === 'up') { + c = c - COLS < 0 ? Math.min((totalRows - 1) * COLS + col, total - 1) : c - COLS; + } + this._gpKeyCursor = c; + this._renderNameCursor(); + SFX.select(); + return; + } + if (s === 'party-select' && (dir === 'up' || dir === 'down')) { + const max = (this._partySelectOrdered || []).length - 1; + this._partySelectCursor = dir === 'up' ? Math.max(0, this._partySelectCursor - 1) : Math.min(max, this._partySelectCursor + 1); + this._renderPartySelectCursor(); + SFX.select(); + return; + } + if (s === 'continue' && (dir === 'up' || dir === 'down')) { + this._contCursor = this._contCursor === 0 ? 1 : 0; + this._renderContCursor(); + return; + } + if (s === 'starter' && (dir === 'left' || dir === 'right')) { + this._starterCursor = dir === 'left' ? Math.max(0, (this._starterCursor||0) - 1) : Math.min(2, (this._starterCursor||0) + 1); + this._renderStarterCursor(); + return; + } + if (s === 'catch' && (dir === 'left' || dir === 'right')) { + this._catchCursor = dir === 'left' ? 0 : 1; + this._renderCatchCursor(); + return; + } + if (s === 'battle') { this._moveCursor(dir); } this._heldKeys.add(dir); if (!this._walkLoop) this._startWalkLoop(); - - const s = this.state.screen; - if (s === 'battle') { this._moveCursor(dir); } } _dpadEnd(dir) { @@ -384,9 +439,6 @@ class Game { if (this._heldKeys.size === 0) this._stopWalkLoop(); } - /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - MAP WALKING โ€” 16-step loop, collision, NPC proximity - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ _startWalkLoop() { if (this._walkLoop) return; this._walkLoop = setInterval(() => this._walkTick(), 30); @@ -430,6 +482,7 @@ class Game { this.state.worldX += dx; this.state.worldY += dy; + this.state.lastDir = dir; this._applyCamera(); @@ -445,6 +498,7 @@ class Game { if (bub) { bub.style.left = (cx - 4) + 'px'; bub.style.bottom = (bot + 66) + 'px'; } } this._checkNPCProximity(); + this._checkWildProximity(); } _applyCamera() { @@ -455,6 +509,7 @@ class Game { const ty = -(spread + this.state.worldY - viewH / 2); if (inner) inner.style.transform = `translate(${tx}px,${ty}px)`; this._positionNPCOnScreen(); + this._positionWildOnScreen(); } _positionNPCOnScreen() { @@ -468,6 +523,18 @@ class Game { } } + _positionWildOnScreen() { + const {viewW, viewH} = this._getViewport(); + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap && this.state.wildSpawned) { + const screenX = viewW / 2 + (this.state.wildWorldX - this.state.worldX) - 24; + const screenY = viewH / 2 + (this.state.wildWorldY - this.state.worldY) - 24; + wildWrap.style.left = screenX + 'px'; + wildWrap.style.bottom = (viewH - screenY - 48) + 'px'; + wildWrap.style.display = 'block'; + } + } + _playerScreenOffset() { const {viewW, viewH} = this._getViewport(); return { px: viewW / 2, py: viewH - (viewH * 0.45) - 32 }; @@ -481,6 +548,15 @@ class Game { return Math.sqrt((npcSX - px) ** 2 + (npcSY - py) ** 2); } + _wildScreenDist() { + if (!this.state.wildSpawned) return Infinity; + const {viewW, viewH} = this._getViewport(); + const {px, py} = this._playerScreenOffset(); + const wSX = viewW / 2 + (this.state.wildWorldX - this.state.worldX); + const wSY = viewH / 2 + (this.state.wildWorldY - this.state.worldY); + return Math.sqrt((wSX - px) ** 2 + (wSY - py) ** 2); + } + _checkNPCProximity() { const {viewW, viewH} = this._getViewport(); const dist = this._npcScreenDist(); @@ -499,10 +575,21 @@ class Game { this._updateDirArrow(near); } + _checkWildProximity() { + if (!this.state.wildSpawned || !this.state.currentWild) return; + const dist = this._wildScreenDist(); + if (dist < 80) { + this._stopWalkLoop(); + this._heldKeys.clear(); + SFX.encounter(); + this.showCatchScreen(); + } + } + _updateDirArrow(nearNPC) { const arrow = document.getElementById('map-dir-arrow'); if (!arrow) return; - if (nearNPC) { arrow.style.display = 'none'; arrow.style.animation = 'none'; return; } + if (nearNPC) { arrow.style.display = 'none'; arrow.style.animation = 'none'; this._stopArrowCycle(); return; } const {viewW, viewH} = this._getViewport(); const dx = this.state.npcWorldX - this.state.worldX; const dy = this.state.npcWorldY - this.state.worldY; @@ -520,8 +607,12 @@ class Game { const glyphs = { right:'โ–ถ', left:'โ—€', up:'โ–ฒ', down:'โ–ผ' }; arrow.textContent = glyphs[pd]; - arrow.style.display = 'block'; - arrow.style.animation = 'arrowPulse .7s ease-in-out infinite'; + if (this._arrowDir !== pd) { + this._arrowDir = pd; + arrow.style.display = 'block'; + arrow.style.animation = 'arrowPulse .7s ease-in-out infinite'; + this._startArrowCycle(arrow); + } if (pd === 'right') { arrow.style.removeProperty('left'); arrow.style.right = '12px'; @@ -541,6 +632,28 @@ class Game { } } + _startArrowCycle(arrow) { + this._stopArrowCycle(); + arrow.style.opacity = '0'; + const cycle = () => { + if (!arrow || arrow.style.display === 'none') return; + arrow.style.opacity = '1'; + this._arrowHideTimer = setTimeout(() => { + arrow.style.opacity = '0'; + }, 3000); + }; + cycle(); + this._arrowInterval = setInterval(cycle, 15000); + } + + _stopArrowCycle() { + clearInterval(this._arrowInterval); + clearTimeout(this._arrowHideTimer); + this._arrowInterval = null; + this._arrowHideTimer = null; + this._arrowDir = null; + } + _nearNPC() { if (!this.state.npcSpawned) return false; return this._npcScreenDist() < 100; @@ -559,11 +672,90 @@ class Game { return; } const arrow = document.getElementById('map-dir-arrow'); - if (arrow) { arrow.style.display = 'none'; arrow.style.animation = 'none'; } + if (arrow) { arrow.style.display = 'none'; arrow.style.animation = 'none'; this._stopArrowCycle(); } const bub = document.getElementById('player-bubble'); if (bub) bub.classList.remove('visible'); SFX.encounter(); - this.startQuestion(); + const conscious = this.state.party.filter(p => !p.fainted); + if (conscious.length > 1) { + this.showPartySelect(); + } else { + this.state.activePokemon = conscious[0] || this.state.party[0]; + this.startQuestion(); + } + } + + showPartySelect() { + this.show('party-select'); + this._showController(true); + this._partySelectCursor = 0; + + const list = document.getElementById('party-select-list'); + list.innerHTML = ''; + + const ordered = [...this.state.party].reverse().sort((a, b) => (a.fainted ? 1 : 0) - (b.fainted ? 1 : 0)); + + ordered.forEach((poke, i) => { + const entry = document.createElement('div'); + entry.className = 'party-select-entry' + (poke.fainted ? ' fainted' : ''); + if (i === 0 && !poke.fainted) entry.classList.add('party-selected'); + + const emojiWrap = document.createElement('div'); + emojiWrap.className = 'party-select-emoji'; + emojiWrap.textContent = poke.emoji; + if (poke.fainted) { + const zzz = document.createElement('span'); + zzz.className = 'party-select-zzz'; + zzz.textContent = 'zZz'; + emojiWrap.appendChild(zzz); + } + + const info = document.createElement('div'); + info.className = 'party-select-info'; + info.innerHTML = `
${poke.name.toUpperCase()}
${poke.type} ยท Lv ${poke.level}
`; + + const badge = document.createElement('div'); + badge.className = 'party-select-badge' + (poke.fainted ? ' badge-fainted' : ''); + badge.textContent = poke.fainted ? 'FAINTED' : 'READY'; + + entry.appendChild(emojiWrap); + entry.appendChild(info); + entry.appendChild(badge); + + if (!poke.fainted) { + entry.addEventListener('click', () => { + this.state.activePokemon = poke; + SFX.confirm(); + this.startQuestion(); + }); + } + + list.appendChild(entry); + }); + + this._partySelectOrdered = ordered; + this._renderPartySelectCursor(); + } + + _renderPartySelectCursor() { + const entries = document.querySelectorAll('.party-select-entry'); + entries.forEach((e, i) => { + e.classList.toggle('party-selected', i === this._partySelectCursor && !e.classList.contains('fainted')); + if (i === this._partySelectCursor) { + e.scrollIntoView({ block:'nearest', behavior:'smooth' }); + } + }); + } + + _partySelectConfirm() { + const ordered = this._partySelectOrdered; + if (!ordered) return; + const poke = ordered[this._partySelectCursor]; + if (poke && !poke.fainted) { + this.state.activePokemon = poke; + SFX.confirm(); + this.startQuestion(); + } } _placeMapSprites() { @@ -585,9 +777,6 @@ class Game { if (hint) hint.style.display = 'none'; } - /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - BATTLE CURSOR โ€” navigate 2ร—2 grid with D-pad / WASD - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ _moveCursor(dir) { const btns = document.querySelectorAll('.choice-btn:not(.disabled):not(.correct):not(.wrong):not(.confirm-inactive)'); if (!btns.length) return; @@ -617,9 +806,6 @@ class Game { if (target) target.click(); } - /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - GLOBAL KEYBOARD - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ _bindGlobalKeys() { const DPAD_KEYS = { 'ArrowUp':'up','ArrowDown':'down','ArrowLeft':'left','ArrowRight':'right', @@ -630,7 +816,6 @@ class Game { document.addEventListener('keydown', (e) => { const s = this.state.screen; - /* Name screen โ€” character input */ if (s === 'name') { if (e.key==='Backspace') { e.preventDefault(); this.delChar(); } else if (e.key==='Enter') this.confirmName(); @@ -638,16 +823,101 @@ class Game { return; } - /* Title */ if (s === 'title' && (e.key==='Enter'||e.key===' ')) { document.getElementById('screen-title').click(); return; } - - /* Continue screen */ - if (s === 'continue' && (e.key==='Enter'||e.key===' ')) { document.getElementById('btn-continue-save')?.click(); return; } - - /* Intro */ + if (s === 'party-select') { + if (e.key==='ArrowUp'||e.key==='w'||e.key==='W') { + this._partySelectCursor = Math.max(0, this._partySelectCursor - 1); + this._renderPartySelectCursor(); + SFX.select(); + return; + } + if (e.key==='ArrowDown'||e.key==='s'||e.key==='S') { + const max = (this._partySelectOrdered || []).length - 1; + this._partySelectCursor = Math.min(max, this._partySelectCursor + 1); + this._renderPartySelectCursor(); + SFX.select(); + return; + } + if (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E') { + this._partySelectConfirm(); + return; + } + return; + } + if (s === 'continue') { + if (e.key==='ArrowUp'||e.key==='w'||e.key==='W'||e.key==='ArrowDown'||e.key==='s'||e.key==='S') { + this._contCursor = this._contCursor === 0 ? 1 : 0; + this._renderContCursor(); + return; + } + if (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E') { + if (this._contCursor === 1) document.getElementById('btn-new-game')?.click(); + else document.getElementById('btn-continue-save')?.click(); + return; + } + return; + } if (s === 'intro' && (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E')) { this.advanceIntro(); return; } - /* D-pad direction keys */ + if (s === 'starter') { + if (e.key==='1'||e.key==='2'||e.key==='3') { + this._starterCursor = parseInt(e.key) - 1; + this._renderStarterCursor(); + setTimeout(() => this.pickStarter(this._starterCursor), 150); + return; + } + if (e.key==='ArrowLeft'||e.key==='a'||e.key==='A') { + this._starterCursor = Math.max(0, (this._starterCursor||0) - 1); + this._renderStarterCursor(); + return; + } + if (e.key==='ArrowRight'||e.key==='d'||e.key==='D') { + this._starterCursor = Math.min(2, (this._starterCursor||0) + 1); + this._renderStarterCursor(); + return; + } + if (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E') { + this.pickStarter(this._starterCursor||0); + return; + } + return; + } + + if (s === 'starter-confirm' && (e.key==='Enter'||e.key===' ')) { + document.getElementById('btn-starter-continue')?.click(); + return; + } + + if (s === 'catch') { + if (e.key==='1'||e.key==='2') { + const btns = document.querySelectorAll('.catch-action-btn'); + if (e.key==='1' && btns[0]) btns[0].click(); + if (e.key==='2' && btns[1]) btns[1].click(); + return; + } + if (e.key==='ArrowLeft'||e.key==='a'||e.key==='A') { + this._catchCursor = 0; + this._renderCatchCursor(); + return; + } + if (e.key==='ArrowRight'||e.key==='d'||e.key==='D') { + this._catchCursor = 1; + this._renderCatchCursor(); + return; + } + if (e.key==='Enter'||e.key===' '||e.key==='e'||e.key==='E') { + const btns = document.querySelectorAll('.catch-action-btn'); + if (btns[this._catchCursor||0]) btns[this._catchCursor||0].click(); + return; + } + return; + } + + if (s === 'catch-result' && (e.key==='Enter'||e.key===' ')) { + document.getElementById('btn-catch-continue')?.click(); + return; + } + if (DPAD_KEYS[e.key] && !e.repeat) { const dir = DPAD_KEYS[e.key]; if ((s==='map' || s==='battle') && !this._heldKeys.has(dir)) { @@ -659,17 +929,14 @@ class Game { return; } - /* A-button equivalent on keyboard */ if ((e.key==='Enter'||e.key==='e'||e.key==='E') && !e.repeat) { e.preventDefault(); this._pressA(); return; } - /* B-button */ if ((e.key==='x'||e.key==='X') && !e.repeat) { this._pressB(); return; } - /* Quick-pick numbers on battle */ if (s==='battle' && !this.state.answering) { const numMap={'1':0,'2':1,'3':2,'4':3}; if (numMap[e.key]!==undefined) { @@ -694,21 +961,6 @@ class Game { }); } - /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - XBOX / GAMEPAD SUPPORT (Gamepad API) - Polls every animation frame. Works with Xbox, PS, and most - USB/Bluetooth gamepads that the browser recognises. - - Xbox mapping: - Axes 0/1 = left stick X/Y - Button 0 = A (confirm / talk) - Button 1 = B (talk / back) - Button 12 = D-pad up - Button 13 = D-pad down - Button 14 = D-pad left - Button 15 = D-pad right - Button 9 = Start (confirm) - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ _bindGamepad() { this._gpPrev = {}; this._gpActive = false; @@ -717,8 +969,6 @@ class Game { console.log('Gamepad connected:', e.gamepad.id); }); - /* Start polling immediately โ€” don't wait for the event. - The poll function checks if any gamepad is present each frame. */ this._startGpLoop(); } @@ -752,7 +1002,6 @@ class Game { const s = this.state.screen; - /* โ”€โ”€ NAME SCREEN โ€” D-pad navigates keyboard grid, A selects, B deletes, Start confirms โ”€โ”€ */ if (s === 'name') { const letterKeys = Array.from(document.querySelectorAll('.key-btn')); const delBtn = document.getElementById('btn-backspace'); @@ -761,14 +1010,11 @@ class Game { const total = keys.length; const COLS = 10; - /* Highlight current key โ€” works for letter keys, DEL and OK */ keys.forEach((k, i) => { k.style.outline = i === this._gpKeyCursor ? '3px solid #f8c030' : ''; k.style.outlineOffset = i === this._gpKeyCursor ? '-2px' : ''; }); - /* D-pad navigation with repeat delay */ - const now = Date.now(); const moved = Object.entries(gpDirs).find(([d, active]) => active && !this._gpPrev[`axis_${d}`]); if (moved) { const [dir] = moved; @@ -784,7 +1030,7 @@ class Game { }); } - if (justPressed(0) || justPressed(1)) { /* A or B โ€” activate highlighted key */ + if (justPressed(0) || justPressed(1)) { const key = keys[this._gpKeyCursor]; if (!key) return; const isLetterKey = key.classList.contains('key-btn'); @@ -796,34 +1042,24 @@ class Game { this.confirmName(); } } - if (justPressed(2)) { /* X โ€” delete */ + if (justPressed(2)) { this.delChar(); } - if (justPressed(9) || justPressed(8)) { /* Start or Select โ€” confirm name */ + if (justPressed(9) || justPressed(8)) { this.confirmName(); } - /* โ”€โ”€ ALL OTHER SCREENS โ€” directional movement + button actions โ”€โ”€ */ } else { - /* โ”€โ”€ CONTINUE SCREEN โ€” D-pad up/down switches between Continue and New Game โ”€โ”€ */ if (s === 'continue') { - if (this._gpContCursor === undefined) this._gpContCursor = 0; - const contBtn = document.getElementById('btn-continue-save'); - const newBtn = document.getElementById('btn-new-game'); - const movedUp = gpDirs.up && !this._gpPrev['axis_up']; const movedDown = gpDirs.down && !this._gpPrev['axis_down']; if (movedUp || movedDown) { - this._gpContCursor = this._gpContCursor === 0 ? 1 : 0; + this._contCursor = this._contCursor === 0 ? 1 : 0; + this._renderContCursor(); } - - /* Highlight selected button */ - if (contBtn) contBtn.style.outline = this._gpContCursor === 0 ? '3px solid #f8c030' : ''; - if (newBtn) newBtn.style.outline = this._gpContCursor === 1 ? '3px solid #f8c030' : ''; } - /* Walk / battle cursor movement */ Object.entries(gpDirs).forEach(([dir, active]) => { const key = `axis_${dir}`; if (active && !this._gpPrev[key]) { @@ -832,19 +1068,31 @@ class Game { if (!this._walkLoop) this._startWalkLoop(); } if (s === 'battle') this._moveCursor(dir); + if (s === 'party-select' && (dir === 'up' || dir === 'down')) { + const max = (this._partySelectOrdered || []).length - 1; + this._partySelectCursor = dir === 'up' ? Math.max(0, this._partySelectCursor - 1) : Math.min(max, this._partySelectCursor + 1); + this._renderPartySelectCursor(); + SFX.select(); + } + if (s === 'starter' && (dir === 'left' || dir === 'right')) { + this._starterCursor = dir === 'left' ? Math.max(0, (this._starterCursor||0) - 1) : Math.min(2, (this._starterCursor||0) + 1); + this._renderStarterCursor(); + } + if (s === 'catch' && (dir === 'left' || dir === 'right')) { + this._catchCursor = dir === 'left' ? 0 : 1; + this._renderCatchCursor(); + } } else if (!active && this._gpPrev[key]) { this._heldKeys.delete(dir); if (this._heldKeys.size === 0) this._stopWalkLoop(); } }); - /* A / Start โ€” context action on every screen */ if (justPressed(0) || justPressed(9)) { - if (s === 'boot') { /* wait for title */ } + if (s === 'boot') { } else if (s === 'title') { document.getElementById('screen-title')?.click(); } else if (s === 'continue') { - /* 0 = CONTINUE highlighted, 1 = NEW GAME highlighted */ - if (this._gpContCursor === 0) document.getElementById('btn-continue-save')?.click(); + if (this._contCursor === 0) document.getElementById('btn-continue-save')?.click(); else document.getElementById('btn-new-game')?.click(); } else if (s === 'intro') { this.advanceIntro(); } @@ -852,10 +1100,14 @@ class Game { else if (s === 'result') { document.getElementById('btn-result-cont')?.click(); } else if (s === 'levelup') { document.getElementById('btn-lu-cont')?.click(); } else if (s === 'complete') { document.getElementById('btn-play-again')?.click(); } + else if (s === 'starter-confirm') { document.getElementById('btn-starter-continue')?.click(); } + else if (s === 'party-select') { this._partySelectConfirm(); } + else if (s === 'starter') { this.pickStarter(this._starterCursor||0); } + else if (s === 'catch') { const cbtns = document.querySelectorAll('.catch-action-btn'); if (cbtns[this._catchCursor||0]) cbtns[this._catchCursor||0].click(); } + else if (s === 'catch-result') { document.getElementById('btn-catch-continue')?.click(); } else { this._pressA(); } } - /* B โ€” also confirms answer in battle */ if (justPressed(1)) { if (s === 'battle' && !this.state.answering) { this._confirmCursor(); } else if (s === 'intro') { this.advanceIntro(); } @@ -863,7 +1115,6 @@ class Game { else { this._pressB(); } } - /* Quick-pick answers X=2, Y=3, LB=4, RB=5 */ if (s === 'battle' && !this.state.answering) { const quickMap = { 2:0, 3:1, 4:2, 5:3 }; Object.entries(quickMap).forEach(([btn, idx]) => { @@ -876,12 +1127,12 @@ class Game { } } - /* Store all states for next frame */ Object.entries(gpDirs).forEach(([d, v]) => this._gpPrev[`axis_${d}`] = v); for (let i = 0; i < btns.length; i++) { this._gpPrev[`btn_${i}`] = btns[i]?.pressed; } } + _showController(visible) { ['gba-left', 'gba-right'].forEach(id => { const el = document.getElementById(id); @@ -891,19 +1142,16 @@ class Game { }); } - /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - SCREENS - โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ boot() { this.show('boot'); - this._showController(false); + this._showController(true); setTimeout(() => this.showTitle(), 1800); } showTitle() { this.show('title'); SFX.boot(); - this._showController(false); + this._showController(true); const go = () => { document.getElementById('screen-title').removeEventListener('click', go); Music.unblock(); @@ -914,9 +1162,10 @@ class Game { showContinueOrName() { const save = loadGame(); - if (save && save.playerName && save.currentQ > 0 && save.currentQ < 100) { + if (save && save.playerName && save.currentQ > 0 && save.currentQ < 100 && save.party && save.party.length > 0) { this.show('continue'); - this._showController(false); + this._showController(true); + this._contCursor = 0; 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}%)`; @@ -928,13 +1177,13 @@ class Game { document.getElementById('btn-new-game').textContent = 'โœฆ CONFIRM NEW GAME'; document.getElementById('btn-new-game').onclick = () => { clearSave(); this.startFresh(); }; }; - /* Reset controller cursor to CONTINUE button */ - this._gpContCursor = 0; + this._contCursor = 0; setTimeout(() => { const c = document.getElementById('btn-continue-save'); if (c) c.style.outline = '3px solid #f8c030'; }, 50); } else { + clearSave(); this.startFresh(); } } @@ -945,6 +1194,11 @@ class Game { score:save.score, streak:save.streak, maxStreak:save.maxStreak, correct:save.correct, wrong:save.wrong, worldX:0, worldY:0, npcWorldX:0, npcWorldY:0, npcSpawned:false, + party: save.party || [], + pokeballs: save.pokeballs || 0, + playerLevel: save.playerLevel || 1, + caughtCount: save.caughtCount || 0, + activePokemon: save.party && save.party.length > 0 ? save.party[0] : null, }); this.loadQuestions(() => { if (save.questionOrder && save.questionOrder.length === this.state.questions.length) { @@ -959,14 +1213,16 @@ class Game { } startFresh() { - Object.assign(this.state, { playerName:'', currentQ:0, score:0, streak:0, maxStreak:0, correct:0, wrong:0 }); + Object.assign(this.state, { + playerName:'', currentQ:0, score:0, streak:0, maxStreak:0, correct:0, wrong:0, + party:[], pokeballs:0, playerLevel:1, caughtCount:0, activePokemon:null, + }); this.showNameEntry(); } - /* โ”€โ”€ NAME โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showNameEntry() { this.show('name'); - this._showController(false); + this._showController(true); this._gpKeyCursor = 0; this.buildKeyboard(); this.typeText('name-prompt-text', CONFIG.namePrompt); @@ -984,20 +1240,49 @@ class Game { b.addEventListener('pointerdown', (e)=>{ e.preventDefault(); this.addChar(ch); }); grid.appendChild(b); }); + this._gpKeyCursor = 0; + this._renderNameCursor(); + } + + _getNameKeys() { + const letterKeys = Array.from(document.querySelectorAll('.key-btn')); + const delBtn = document.getElementById('btn-backspace'); + const okBtn = document.getElementById('btn-confirm-name'); + return [...letterKeys, delBtn, okBtn].filter(Boolean); + } + + _renderNameCursor() { + const keys = this._getNameKeys(); + keys.forEach((k, i) => { + k.style.outline = i === this._gpKeyCursor ? '3px solid #f8c030' : ''; + k.style.outlineOffset = i === this._gpKeyCursor ? '-2px' : ''; + }); + } + + _nameKeyConfirm() { + const keys = this._getNameKeys(); + const key = keys[this._gpKeyCursor]; + if (!key) return; + if (key.classList.contains('key-btn')) { + this.addChar(key.textContent); + } else if (key.id === 'btn-backspace') { + this.delChar(); + } else if (key.id === 'btn-confirm-name') { + this.confirmName(); + } } addChar(ch) { if (this.state.playerName.length<10){ this.state.playerName+=ch; this.refreshNameDisplay(); SFX.select(); } } - delChar() { this.state.playerName=this.state.playerName.slice(0,-1); this.refreshNameDisplay(); } + delChar() { this.state.playerName=this.state.playerName.slice(0,-1); this.refreshNameDisplay(); SFX.backspace(); } refreshNameDisplay() { const el=document.getElementById('name-display'); if(el) el.textContent=this.state.playerName+(this.state.playerName.length<10?'_':''); } confirmName() { if(!this.state.playerName.trim()) this.state.playerName='ASH'; SFX.confirm(); this.showIntro(); } - /* โ”€โ”€ INTRO โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showIntro() { this.show('intro'); - this._showController(false); + this._showController(true); this.state.introStep = 0; this.renderIntroMsg(); @@ -1033,13 +1318,280 @@ class Game { if (this.state.introStep >= INTRO_MSGS.length) { const el = document.getElementById('screen-intro'); if (this._introClick) el.removeEventListener('click', this._introClick); - this.loadQuestions(() => this.showMap()); + this.showStarterSelection(); return; } this.renderIntroMsg(); } - /* โ”€โ”€ LOAD QUESTIONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + showStarterSelection() { + this.show('starter'); + this._showController(true); + this._starterCursor = 0; + const oakEl = document.getElementById('starter-oak'); + if (oakEl) setNPC(oakEl, NPC['Professor Oak']); + + this.typeText('starter-text', `Now, ${this.state.playerName||'ASH'}! Choose your partner Pokรฉmon! Each one is special in its own way!`); + + const container = document.getElementById('starter-choices'); + container.innerHTML = ''; + + STARTER_POOL.forEach((poke, i) => { + const btn = document.createElement('button'); + btn.className = 'starter-btn'; + btn.innerHTML = ` + ${poke.emoji} + ${poke.name} + ${poke.type} ยท Lv5 + `; + btn.addEventListener('click', () => this.pickStarter(i)); + container.appendChild(btn); + }); + this._renderStarterCursor(); + } + + _renderStarterCursor() { + SFX.select(); + const btns = document.querySelectorAll('.starter-btn'); + btns.forEach((btn, i) => { + btn.classList.toggle('starter-btn-selected', i === (this._starterCursor||0)); + }); + } + + _renderCatchCursor() { + const btns = document.querySelectorAll('.catch-action-btn'); + btns.forEach((btn, i) => { + btn.classList.toggle('catch-btn-selected', i === (this._catchCursor||0)); + }); + } + + _renderContCursor() { + const contBtn = document.getElementById('btn-continue-save'); + const newBtn = document.getElementById('btn-new-game'); + if (contBtn) contBtn.style.outline = this._contCursor === 0 ? '3px solid #f8c030' : ''; + if (newBtn) newBtn.style.outline = this._contCursor === 1 ? '3px solid #f8c030' : ''; + } + + pickStarter(index) { + const chosen = { ...STARTER_POOL[index], level: 5 }; + this.state.party = [chosen]; + this.state.activePokemon = chosen; + this.state.pokeballs = 5; + + SFX.catch(); + + this.show('starter-confirm'); + this._showController(true); + + document.getElementById('starter-chosen-emoji').textContent = chosen.emoji; + document.getElementById('starter-chosen-name').textContent = chosen.name.toUpperCase(); + document.getElementById('starter-chosen-level').textContent = `Lv ${chosen.level} ยท ${chosen.type}`; + + const others = STARTER_POOL.filter((_, i) => i !== index); + const fleeText = `${others[0].emoji} ${others[0].name} and ${others[1].emoji} ${others[1].name} ran away!`; + document.getElementById('starter-flee-text').textContent = fleeText; + + this.typeText('starter-confirm-text', + `Great choice! ${chosen.name} looks excited to join you! The other Pokรฉmon scurried away into the wild. You also received 5 Pokรฉballs!` + ); + + document.getElementById('btn-starter-continue').onclick = () => { + if (this.state.twTimer) { clearInterval(this.state.twTimer); this.state.twTimer = null; } + this.loadQuestions(() => this.showMap()); + }; + } + + _spawnWildPokemon() { + if (this.state.pokeballs <= 0) { this.state.wildSpawned = false; return; } + if (Math.random() > 0.90) { this.state.wildSpawned = false; return; } + + const pLevel = getPlayerLevel(this.state); + const wild = randomWildPokemon(pLevel); + this.state.currentWild = wild; + + const {viewW, viewH} = this._getViewport(); + const npcDx = this.state.npcWorldX; + const npcDy = this.state.npcWorldY; + + const t = 0.2 + Math.random() * 0.5; + let wx = npcDx * t + (Math.random() - 0.5) * viewW * 1.5; + let wy = npcDy * t + (Math.random() - 0.5) * viewH * 1.5; + + const minDist = 150; + if (Math.sqrt(wx*wx + wy*wy) < minDist) { + wx += (wx >= 0 ? 1 : -1) * minDist; + wy += (wy >= 0 ? 1 : -1) * minDist; + } + + this.state.wildWorldX = wx; + this.state.wildWorldY = wy; + this.state.wildSpawned = true; + + const wildEl = document.getElementById('map-wild'); + if (wildEl) wildEl.textContent = wild.emoji; + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap) wildWrap.style.display = 'block'; + + this._positionWildOnScreen(); + } + + showCatchScreen() { + const wild = this.state.currentWild; + if (!wild) return; + this._throwing = false; + + this.show('catch'); + this._showController(true); + this._catchCursor = 0; + Music.pause(); + + document.getElementById('catch-wild-pokemon').textContent = wild.emoji; + document.getElementById('catch-wild-name').textContent = wild.name.toUpperCase(); + document.getElementById('catch-wild-level').textContent = `Lv ${wild.level}`; + + const pLevel = getPlayerLevel(this.state); + const canCatch = pLevel >= wild.level; + + this.typeText('catch-text', `A wild ${wild.name} appeared!`); + + const actions = document.getElementById('catch-actions'); + actions.innerHTML = ''; + + const throwBtn = document.createElement('button'); + throwBtn.className = 'catch-action-btn btn-pixel btn-red'; + throwBtn.innerHTML = ' THROW BALL'; + throwBtn.disabled = this.state.pokeballs <= 0; + if (this.state.pokeballs <= 0) throwBtn.style.opacity = '0.4'; + throwBtn.addEventListener('click', () => this.throwPokeball(wild, canCatch)); + actions.appendChild(throwBtn); + + const runBtn = document.createElement('button'); + runBtn.className = 'catch-action-btn btn-pixel btn-blue'; + runBtn.textContent = '๐Ÿƒ RUN AWAY'; + runBtn.addEventListener('click', () => { + this.state.wildSpawned = false; + this.state.currentWild = null; + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap) wildWrap.style.display = 'none'; + this.showMap(); + }); + actions.appendChild(runBtn); + this._renderCatchCursor(); + } + + throwPokeball(wild, canCatch) { + if (this._throwing) return; + if (this.state.pokeballs <= 0) return; + this._throwing = true; + this.state.pokeballs--; + SFX.pokeball(); + + const catchScreen = document.getElementById('screen-catch'); + const pokemonEl = document.getElementById('catch-wild-pokemon'); + const nameEl = document.getElementById('catch-wild-name'); + const levelEl = document.getElementById('catch-wild-level'); + + const ball = document.createElement('img'); + ball.src = './favicon.ico'; + ball.className = 'pokeball-throw'; + catchScreen.appendChild(ball); + + setTimeout(() => { + const pokeRect = pokemonEl.getBoundingClientRect(); + const screenRect = catchScreen.getBoundingClientRect(); + const burstX = pokeRect.left - screenRect.left + pokeRect.width / 2; + const burstY = pokeRect.top - screenRect.top + pokeRect.height * 0.25; + + const burst = document.createElement('div'); + burst.className = 'pokeball-burst'; + burst.style.left = burstX + 'px'; + burst.style.top = burstY + 'px'; + catchScreen.appendChild(burst); + + ball.remove(); + pokemonEl.style.opacity = '0'; + pokemonEl.style.transform = 'scale(0)'; + pokemonEl.style.transition = 'opacity .2s, transform .2s'; + if (nameEl) nameEl.style.display = 'none'; + if (levelEl) levelEl.style.display = 'none'; + + const restBall = document.createElement('img'); + restBall.src = './favicon.ico'; + restBall.className = 'pokeball-rest'; + restBall.style.left = (burstX - 20) + 'px'; + restBall.style.top = (burstY + 20) + 'px'; + catchScreen.appendChild(restBall); + + setTimeout(() => burst.remove(), 500); + + let bounceCount = 0; + const doBounce = () => { + const maxBounces = canCatch ? 3 : 2; + if (bounceCount >= maxBounces) { + restBall.remove(); + pokemonEl.style.opacity = '1'; + pokemonEl.style.transform = ''; + pokemonEl.style.transition = ''; + if (nameEl) nameEl.style.display = ''; + if (levelEl) levelEl.style.display = ''; + this._showCatchResult(wild, canCatch); + return; + } + bounceCount++; + SFX.pokeballBounce(); + restBall.classList.remove('pokeball-bounce-anim'); + void restBall.offsetWidth; + restBall.classList.add('pokeball-bounce-anim'); + setTimeout(doBounce, 600); + }; + + setTimeout(doBounce, 600); + }, 700); + } + + _showCatchResult(wild, canCatch) { + this._throwing = false; + this.show('catch-result'); + this._showController(true); + + const resultEmoji = document.getElementById('catch-result-emoji'); + const resultMsg = document.getElementById('catch-result-msg'); + const continueBtn = document.getElementById('btn-catch-continue'); + if (continueBtn) continueBtn.style.display = 'none'; + + if (canCatch) { + setTimeout(() => SFX.catch(), 500); + resultEmoji.textContent = wild.emoji; + resultMsg.innerHTML = ` +
GOTCHA!
+
${wild.name} (Lv ${wild.level}) was caught!
+
Added to your party!
+ `; + this.state.party.push({ ...wild }); + this.state.caughtCount++; + } else { + setTimeout(() => SFX.escape(), 300); + resultEmoji.textContent = '๐Ÿ’จ'; + resultMsg.innerHTML = ` +
OH NO!
+
${wild.name} (Lv ${wild.level}) broke free and escaped!
+
The Pokรฉmon was too strong! Train more to catch stronger Pokรฉmon.
+ `; + } + + this.state.wildSpawned = false; + this.state.currentWild = null; + const wildWrap = document.getElementById('map-wild-wrap'); + if (wildWrap) wildWrap.style.display = 'none'; + + setTimeout(() => { + if (this.state.screen === 'catch-result') { + saveGame(this.state); + this.showMap(); + } + }, 3000); + } + loadQuestions(cb) { if (this.state.questions.length>0) { cb(); return; } fetch('./questions.json') @@ -1056,9 +1608,6 @@ class Game { .catch(()=>this.toast('โŒ Could not load questions.json')); } - /* Generate randomly placed trees across the full scrollable world */ - /* Generate trees for the current journey leg โ€” from world origin to just past the NPC. - Called fresh each time so trees always fill the visible path ahead. */ _pickNextDir(currentDir) { const opposite = { right:'left', left:'right', up:'down', down:'up' }; const opp = opposite[currentDir]; @@ -1078,6 +1627,8 @@ class Game { const cy = spread; const npcX = cx + this.state.npcWorldX; const npcY = cy + this.state.npcWorldY; + const occupied = []; + this._trees = []; for (let i = 0; i < 300; i++) { const span = document.createElement('span'); span.className = 'tree'; @@ -1088,6 +1639,8 @@ class Game { y = cy + (Math.random() - 0.5) * spread * 1.8; tries++; } while (tries < 10 && Math.abs(x - npcX) < 80 && Math.abs(y - npcY) < 80); + occupied.push({ x, y, r: size * 0.6 }); + this._trees.push({ wx: x - cx, wy: y - cy, r: size * 0.35 }); span.textContent = '๐ŸŒณ'; span.style.fontSize = size + 'px'; span.style.left = x + 'px'; @@ -1096,6 +1649,51 @@ class Game { span.style.animationDuration = (2.5 + Math.random() * 2).toFixed(1) + 's'; container.appendChild(span); } + const grassEmojis = ['๐ŸŒพ']; + for (let i = 0; i < 2400; i++) { + let x, y, tries = 0, tooClose; + do { + x = cx + (Math.random() - 0.5) * spread * 1.8; + y = cy + (Math.random() - 0.5) * spread * 1.8; + tries++; + tooClose = occupied.some(t => Math.abs(x - t.x) < t.r && Math.abs(y - t.y) < t.r); + } while (tries < 12 && (tooClose || (Math.abs(x - npcX) < 80 && Math.abs(y - npcY) < 80))); + if (tooClose) continue; + const span = document.createElement('span'); + span.className = 'tall-grass'; + const size = 5 + Math.random() * 4; + span.textContent = grassEmojis[Math.floor(Math.random() * grassEmojis.length)]; + span.style.fontSize = size + 'px'; + span.style.left = x + 'px'; + span.style.top = y + 'px'; + span.style.animationDelay = (Math.random() * 4).toFixed(2) + 's'; + span.style.animationDuration = (2 + Math.random() * 2).toFixed(1) + 's'; + if (Math.random() > 0.5) span.classList.add('tall-grass-flip'); + container.appendChild(span); + } + this._rocks = []; + for (let i = 0; i < 90; i++) { + let x, y, tries = 0, tooClose; + do { + x = cx + (Math.random() - 0.5) * spread * 1.8; + y = cy + (Math.random() - 0.5) * spread * 1.8; + tries++; + tooClose = occupied.some(t => Math.abs(x - t.x) < t.r && Math.abs(y - t.y) < t.r); + } while (tries < 12 && (tooClose || (Math.abs(x - npcX) < 80 && Math.abs(y - npcY) < 80))); + if (tooClose) continue; + occupied.push({ x, y, r: 20 }); + const wx = x - cx; + const wy = y - cy; + this._rocks.push({ wx, wy, r: 22 }); + const span = document.createElement('span'); + span.className = 'map-rock'; + const size = 11 + Math.random() * 6; + span.textContent = '๐Ÿชจ'; + span.style.fontSize = size + 'px'; + span.style.left = x + 'px'; + span.style.top = y + 'px'; + container.appendChild(span); + } } _preGenerateNextLeg() { @@ -1141,6 +1739,7 @@ class Game { this.state.npcSpawned = true; this._generateTrees(); + this._spawnWildPokemon(); document.getElementById('hud-name').textContent = this.state.playerName||'ASH'; this.updateHUD(); @@ -1165,15 +1764,23 @@ class Game { }); } - updateHUD() { const tot=this.state.questions.length||100, done=this.state.currentQ, pct=(done/tot)*100; document.getElementById('hud-score').textContent = this.state.score.toLocaleString(); document.getElementById('hud-streak').textContent = this.state.streak+(this.state.streak>=3?'๐Ÿ”ฅ':''); - document.getElementById('hud-correct').textContent = `${this.state.correct}/${done}`; + const partyEl = document.getElementById('hud-party'); + if (partyEl) { + partyEl.textContent = this.state.party.map(p => p.emoji).join(''); + if (this.state.party.length === 0) partyEl.textContent = 'โ€”'; + } document.getElementById('map-prog-text').textContent = `${done}/${tot}`; const bar=document.getElementById('map-prog-bar'); if(bar){ bar.style.width=pct+'%'; bar.style.background=pct<40?'var(--hp-green)':pct<75?'var(--hp-yellow)':'#60c8ff'; } + + const pbEl = document.getElementById('pokeball-count'); + if (pbEl) pbEl.innerHTML = ` ${this.state.pokeballs}`; + + this.state.playerLevel = getPlayerLevel(this.state); } flashSaveDot() { @@ -1182,23 +1789,43 @@ class Game { setTimeout(()=>{ dot.style.opacity='.4'; },1500); } - /* โ”€โ”€ BATTLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + _getActivePokemon() { + if (this.state.activePokemon && !this.state.activePokemon.fainted) return this.state.activePokemon; + const conscious = this.state.party.filter(p => !p.fainted); + if (conscious.length > 0) return conscious[0]; + if (this.state.party.length === 0) return { name:'MissingNo', emoji:'โ“', type:'???', level:1 }; + return this.state.party[0]; + } + startQuestion() { this._stopWalkLoop(); this._heldKeys.clear(); const q=this.state.questions[this.state.currentQ]; if(!q){ this.showComplete(); return; } + const playerPoke = this._getActivePokemon(); + const npcPoke = randomPokemonForNPC(); + this.state.currentNPCPokemon = npcPoke; + this.show('battle'); this._showController(true); Music.playBattle(); this.state.answering=false; this.state.cursor=0; - setNPC(document.getElementById('battle-npc-sprite'), NPC[q.npc]||'๐Ÿง‘'); + document.getElementById('battle-npc-pokemon').textContent = npcPoke.emoji; + document.getElementById('battle-npc-pokemon-name').textContent = npcPoke.name.toUpperCase(); + document.getElementById('battle-npc-pokemon-level').textContent = `Lv ${npcPoke.level}`; document.getElementById('battle-npc-name').textContent = q.npc.toUpperCase(); - document.getElementById('battle-speaker').textContent = q.npc.toUpperCase(); + const npcTrainer = document.getElementById('battle-npc-trainer'); + if (npcTrainer) setNPC(npcTrainer, NPC[q.npc] || '๐Ÿง‘'); + + document.getElementById('battle-player-pokemon').textContent = playerPoke.emoji; + document.getElementById('battle-player-pokemon-name').textContent = playerPoke.name.toUpperCase(); + document.getElementById('battle-player-pokemon-level').textContent = `Lv ${playerPoke.level}`; document.getElementById('battle-player-name').textContent= (this.state.playerName||'ASH').toUpperCase(); + + document.getElementById('battle-speaker').textContent = q.npc.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'; @@ -1302,12 +1929,15 @@ class Game { this.state.streak++; this.state.correct++; this.state.maxStreak=Math.max(this.state.maxStreak,this.state.streak); this.state.score+=this.state.streak>=5?200:this.state.streak>=3?150:100; + this.state.pokeballs += 3; if(this.state.streak>=3) SFX.streak(); else SFX.correct(); } else { this.state.streak=0; this.state.wrong++; SFX.wrong(); } + this.state.playerLevel = getPlayerLevel(this.state); + setTimeout(()=>{ this._preGenerateNextLeg(); - this.showResult(correct,q); + this.showBattleAnim(correct, q); },120); } @@ -1332,19 +1962,101 @@ class Game { this.state.streak++; this.state.correct++; this.state.maxStreak=Math.max(this.state.maxStreak,this.state.streak); this.state.score+=this.state.streak>=5?200:this.state.streak>=3?150:100; + this.state.pokeballs += 3; if(this.state.streak>=3) SFX.streak(); else SFX.correct(); } else { this.state.streak=0; this.state.wrong++; SFX.wrong(); } + this.state.playerLevel = getPlayerLevel(this.state); + setTimeout(()=>{ this._preGenerateNextLeg(); - this.showResult(correct,q); + this.showBattleAnim(correct, q); },120); } - /* โ”€โ”€ RESULT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + showBattleAnim(correct, q) { + this.show('battle-anim'); + this._showController(false); + + const playerPoke = this._getActivePokemon(); + const npcPoke = this.state.currentNPCPokemon || { name:'???', emoji:'โ“', level:1 }; + + const npcTrainer = document.getElementById('anim-npc-trainer'); + if (npcTrainer) { + const npcQ = this.state.questions[this.state.currentQ]; + setNPC(npcTrainer, NPC[npcQ?.npc] || '๐Ÿง‘'); + } + const playerTrainer = document.getElementById('anim-player-trainer'); + if (playerTrainer) playerTrainer.innerHTML = '
'; + + document.getElementById('anim-player-pokemon-name').textContent = playerPoke.name.toUpperCase(); + document.getElementById('anim-player-pokemon').textContent = playerPoke.emoji; + document.getElementById('anim-npc-pokemon-name').textContent = npcPoke.name.toUpperCase(); + document.getElementById('anim-npc-pokemon').textContent = npcPoke.emoji; + + const playerHp = document.getElementById('anim-player-hp'); + const npcHp = document.getElementById('anim-npc-hp'); + playerHp.style.width = '100%'; + npcHp.style.width = '100%'; + playerHp.style.background = 'var(--hp-green)'; + npcHp.style.background = 'var(--hp-green)'; + + const playerSprite = document.getElementById('anim-player-pokemon'); + const npcSprite = document.getElementById('anim-npc-pokemon'); + playerSprite.style.opacity = '1'; + playerSprite.style.transform = ''; + npcSprite.style.opacity = '1'; + npcSprite.style.transform = ''; + + const msg = document.getElementById('battle-anim-msg'); + + if (correct) { + msg.textContent = `${playerPoke.name} used ${CONFIG.battleMove}!`; + SFX.attack(); + setTimeout(() => { + npcHp.style.width = '0%'; + npcHp.style.background = 'var(--red)'; + msg.textContent = `It's super effective! ${npcPoke.name} fainted!`; + npcSprite.style.opacity = '0.3'; + npcSprite.style.transform = 'translateY(20px) scale(0.5)'; + SFX.battleWin(); + }, 800); + setTimeout(() => { + npcSprite.style.opacity = '1'; + npcSprite.style.transform = ''; + this.showResult(true, q); + }, 2200); + } else { + msg.textContent = `${playerPoke.name} used ${CONFIG.battleMiss}... but it missed!`; + SFX.attack(); + setTimeout(() => { + msg.textContent = `${npcPoke.name} used COUNTER ATTACK!`; + SFX.attack(); + }, 800); + setTimeout(() => { + playerHp.style.width = '0%'; + playerHp.style.background = 'var(--red)'; + msg.textContent = `${playerPoke.name} fainted!`; + playerSprite.style.opacity = '0.3'; + playerSprite.style.transform = 'translateY(20px) scale(0.5)'; + SFX.battleLose(); + playerPoke.fainted = true; + const conscious = this.state.party.filter(p => !p.fainted); + if (conscious.length === 0) { + this.state.party[0].fainted = false; + } + }, 1400); + setTimeout(() => { + playerSprite.style.opacity = '1'; + playerSprite.style.transform = ''; + this.showResult(false, q); + }, 2800); + } + } + showResult(correct,q) { this.show('result'); - this._showController(false); + this._showController(true); Music.stopBattle(); Music.pause(); @@ -1356,11 +2068,16 @@ class Game { const ansEl=document.getElementById('result-correct-ans'); const ptsEl=document.getElementById('result-pts'); + const pbEl=document.getElementById('result-pokeballs'); + if(correct){ ptsEl.textContent=this.state.streak>=5?'+200 PTS ๐Ÿ”ฅ STREAK!':this.state.streak>=3?'+150 PTS ๐Ÿ”ฅ HOT!':'+100 PTS'; ptsEl.style.display='block'; ansEl.classList.remove('visible'); + pbEl.textContent = `โšช +3 Pokรฉballs! (Total: ${this.state.pokeballs})`; + pbEl.style.display = 'block'; } else { ptsEl.style.display='none'; + pbEl.style.display='none'; const correctText = q._correctDisplayText || (q.answers ? q.answers.map(a=>q.options[a]).join(' AND ') : q.options[q.answer]); ansEl.textContent='โœ“ Correct answer'+(q.answers?'s':'')+': '+correctText; ansEl.classList.add('visible'); @@ -1376,11 +2093,8 @@ class Game { else this.showMap(); }; contBtn.onclick = doNext; - const resultScreen = document.getElementById('screen-result'); - resultScreen.addEventListener('pointerdown', doNext, { once: true }); } - /* โ”€โ”€ LEVEL UP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showLevelUp() { this.show('levelup'); SFX.levelUp(); @@ -1396,7 +2110,6 @@ class Game { document.getElementById('btn-lu-cont').onclick=()=>this.showMap(); } - /* โ”€โ”€ COMPLETE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ showComplete() { this.show('complete'); SFX.complete(); @@ -1408,18 +2121,24 @@ class Game { `FINAL SCORE: ${this.state.score.toLocaleString()}\n`+ `CORRECT: ${this.state.correct} / ${tot}\n`+ `ACCURACY: ${acc}%\n`+ - `BEST STREAK: ${this.state.maxStreak} ๐Ÿ”ฅ`; + `BEST STREAK: ${this.state.maxStreak} ๐Ÿ”ฅ\n`+ + `POKร‰MON CAUGHT: ${this.state.caughtCount}`; + + const pokeEl = document.getElementById('complete-pokemon'); + if (pokeEl && this.state.party.length > 0) { + pokeEl.innerHTML = '
YOUR PARTY
' + + this.state.party.map(p => `${p.emoji} ${p.name} Lv${p.level}`).join(''); + } + document.getElementById('btn-play-again').onclick=()=>{ clearSave(); this.startFresh(); }; } - /* โ”€โ”€ SCREEN SWITCH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ show(name) { Object.values(this.screens).forEach(s=>s.classList.remove('active')); const t=this.screens[name]; if(t)t.classList.add('active'); this.state.screen=name; } - /* โ”€โ”€ 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=''; @@ -1430,7 +2149,6 @@ class Game { },26); } - /* โ”€โ”€ TOAST โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ toast(msg,ms=2600){ const el=document.getElementById('toast'); el.textContent=msg; el.classList.remove('hidden'); diff --git a/index.html b/index.html index 8fd3a2a..690806f 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,12 @@ - + -PokรฉAzure โ€” Study for Azure Certifications +PokรฉAzure โ€” Learn Azure Cloud @@ -17,9 +17,8 @@
@@ -29,19 +28,19 @@
-
โ˜… AZURE CERT TRAINER โ˜…
+
โ˜… AZURE TRAINER โ˜…
- +
- +
๐Ÿ“‚ CONTINUE
TRAINERโ€”
@@ -73,9 +72,8 @@
-
-
-
+
+
PROFESSOR OAK
@@ -85,18 +83,73 @@
+ +
+
+
+
+
+
PROFESSOR OAK
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ +
+
+ + +
+
CHOOSE YOUR POKร‰MON!
+
+
+
๐Ÿ“ - AZ-900 TOWN + AZURE TOWN
0/100
+ 0 ๐ŸŽต
@@ -104,16 +157,17 @@
- -
!
- + +
...
-
@@ -121,7 +175,7 @@
TRAINER
ASH
SCORE
0
STREAK
0
-
CORRECT
0/0
+
PARTY
0
@@ -129,17 +183,30 @@
-
๐Ÿ‘ด
+
+
+
+
+
๐Ÿ‰
+
+
+
OAK
-
+
+
+
+
+
+
๐Ÿ”ฅ
+
+
ASH
-
@@ -151,6 +218,30 @@
+ +
+
+
+
+
+
+
+
+
+
+
+
โš”๏ธ
+
+
+
+
+
+
+
+
+
+
+
@@ -159,6 +250,7 @@
+100 PTS
+
@@ -168,7 +260,7 @@
-
ASH gained Azure EXP!
+
ASH gained AZURE EXP!
๐Ÿ… CLOUD TRAINEE
@@ -185,13 +277,14 @@
๐Ÿ†
CHAMPION!
ASH
-

You've conquered AZ-900 Town and mastered AZ-900! Your Azure journey is just beginning โ€” more towns and exams await!

+

You've conquered Azure Town and mastered AZ-900! Your Azure journey is just beginning โ€” more towns and exams await!

+
-
๐Ÿฅ‰ CLOUD EXPLORER โ€” Q25
-
๐Ÿฅˆ AZURE APPRENTICE โ€” Q50
-
๐Ÿฅ‡ CLOUD PRACTITIONER โ€” Q75
-
๐Ÿ† AZ-900 CHAMPION โ€” Q100
+
๐Ÿฅ‰ AZURE EXPLORER โ€” Q25
+
๐Ÿฅˆ AZURE APPRENTICE โ€” Q50
+
๐Ÿฅ‡ AZURE PRACTITIONER โ€” Q75
+
๐Ÿ† AZURE CHAMPION โ€” Q100
๐Ÿ—บ๏ธ More towns & exams coming soon...
@@ -220,9 +313,28 @@
-
+
+ + + diff --git a/music.mp3 b/music.mp3 index f736949..e64e447 100644 Binary files a/music.mp3 and b/music.mp3 differ diff --git a/style.css b/style.css index b9a663b..0524b52 100644 --- a/style.css +++ b/style.css @@ -42,6 +42,7 @@ html, body, #app { user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; + touch-action: manipulation; } /* โ”€โ”€โ”€ BODY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ @@ -110,7 +111,7 @@ html, body { @keyframes abPress { 0%{transform:scale(1)}40%{transform:scale(.82)}100%{transform:scale(1)} } @keyframes choiceCursor { 0%,100%{box-shadow:0 0 0 3px var(--yellow), 2px 2px 0 var(--darkgray)}50%{box-shadow:0 0 0 3px #ffe878, 2px 2px 0 var(--darkgray)} } @keyframes talkBubble { 0%{transform:translateY(0)}50%{transform:translateY(-3px)} } -@keyframes arrowPulse { 0%,100%{transform:scale(1) translateX(0);opacity:1} 50%{transform:scale(1.25) translateX(4px);opacity:.7} } +@keyframes arrowPulse { 0%,100%{transform:scale(1) translateX(0)} 50%{transform:scale(1.25) translateX(4px)} } .blink { animation: blink 1s step-end infinite; } @@ -291,14 +292,16 @@ html, body { color: rgba(255,255,255,.6); font-family: var(--font); font-size: clamp(4.5px, 1.2vw, 6px); - padding: clamp(8px, 2vw, 11px) clamp(10px, 2.5vw, 14px); + padding: clamp(8px, 2vw, 11px) clamp(6px, 1.5vw, 10px); cursor: pointer; letter-spacing: .5px; -webkit-tap-highlight-color: transparent; transition: background .08s; - min-width: 52px; - width: 52px; + min-width: 56px; text-align: center; + display: flex; + align-items: center; + justify-content: center; } .gba-mid-btn:active { background: #3d2f99; } @@ -350,11 +353,11 @@ html, body { } /* โ”€โ”€โ”€ 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); } +#screen-boot { background: #f8f0ff; align-items: center; justify-content: center; animation: fadeIn .4s ease; } +.boot-logo { text-align: center; display:flex; flex-direction:column; align-items:center; gap:12px; } +.boot-gameboy { font-size: clamp(22px,7vw,36px); font-weight:bold; color: #4a0e8f; letter-spacing: 3px; text-shadow: 2px 2px 0 rgba(0,0,0,.1); } +.boot-gameboy sup { font-size:50%; vertical-align:super; } +.boot-ding { font-size: 28px; animation: goldShimmer 1.5s ease-in-out infinite; } /* โ”€โ”€โ”€ TITLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ #screen-title { background: linear-gradient(180deg,#08031a 0%,#180a44 45%,#240e6a 75%,#120538 100%); overflow: hidden; } @@ -376,7 +379,7 @@ html, body { .title-main { text-align:center; z-index:1; padding:24px 16px; } .title-logo { margin-bottom:16px; line-height:1; } .title-poke { font-size:clamp(22px,7.5vw,38px); color:var(--yellow); text-shadow:4px 4px 0 #7a5800,0 0 24px rgba(248,192,48,.5); animation:goldShimmer 3s ease-in-out infinite; display:inline-block; } -.title-sql { font-size:clamp(22px,7.5vw,38px); color:#60c8ff; text-shadow:4px 4px 0 #003d70,0 0 24px rgba(96,200,255,.5); display:inline-block; animation:blueShimmer 3s ease-in-out infinite; } +.title-game { font-size:clamp(22px,7.5vw,38px); color:#60c8ff; text-shadow:4px 4px 0 #003d70,0 0 24px rgba(96,200,255,.5); display:inline-block; animation:blueShimmer 3s ease-in-out infinite; } .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; } @@ -414,8 +417,22 @@ html, body { #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-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } -.intro-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } +.intro-npc.intro-npc-centered { + right:auto !important; + left:50% !important; + bottom:20% !important; + transform:translate(-50%, 50%); + font-size:clamp(80px,25vw,120px); + text-align:center; +} +.intro-npc.intro-npc-centered img { + height:clamp(120px,35vw,180px) !important; + margin:0 auto; + display:block; +} +.intro-oak-bg { + display:flex; align-items:center; justify-content:center; +} .intro-player { position: absolute; left: 22%; @@ -425,7 +442,7 @@ html, body { background-image: url('./player.png'); background-repeat: no-repeat; background-size: 768px 64px; - background-position-x: -32px; /* idle down frame */ + background-position-x: -32px; background-position-y: 0; image-rendering: pixelated; animation: bounce 1s ease-in-out infinite; @@ -478,9 +495,31 @@ html, body { display: inline-block; filter: drop-shadow(1px 2px 0 rgba(0,0,0,.3)); line-height: 1; - z-index: 60; + z-index: 80; } .tree:nth-child(even) { } +.tall-grass { + position: absolute; + display: inline-block; + filter: drop-shadow(0 1px 0 rgba(0,0,0,.2)); + line-height: 1; + z-index: 5; + opacity: .85; + animation: grassSway 2s ease-in-out infinite alternate; +} +@keyframes grassSway { + 0% { transform: rotate(-3deg) scaleY(1); } + 50% { transform: rotate(2deg) scaleY(1.05); } + 100% { transform: rotate(-3deg) scaleY(1); } +} +.tall-grass-flip { + animation: grassSwayFlip 2s ease-in-out infinite alternate; +} +@keyframes grassSwayFlip { + 0% { transform: scaleX(-1) rotate(-3deg) scaleY(1); } + 50% { transform: scaleX(-1) rotate(2deg) scaleY(1.05); } + 100% { transform: scaleX(-1) rotate(-3deg) scaleY(1); } +} /* Directional nudge arrow */ .map-dir-arrow { position: absolute; @@ -490,17 +529,17 @@ html, body { pointer-events: none; z-index: 50; filter: drop-shadow(0 0 4px rgba(248,192,48,.6)); + transition: opacity .5s ease; display: none; bottom: -9999px; } /* NPC lives inside .map-inner, positioned in world-px via JS */ -.map-npc-wrap { position:absolute; text-align:center; z-index: 30; } +.map-npc-wrap { position:absolute; text-align:center; z-index: 70; } .npc-bubble { font-size:14px; font-family:var(--font); color:var(--black); background:var(--yellow); border:2px solid var(--black); width:20px; height:20px; display:flex; align-items:center; justify-content:center; margin:0 auto 2px; animation:bounce .7s ease-in-out infinite; } .map-npc { font-size:clamp(36px,10vw,52px); filter:drop-shadow(2px 3px 0 rgba(0,0,0,.35)); cursor:pointer; animation:npcFloat 2s ease-in-out infinite; } -.map-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } -.battle-sprite img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } -.map-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } -.battle-sprite img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } +.intro-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } +.intro-npc.intro-npc-centered img { height:clamp(120px,35vw,180px) !important; } +.map-npc img { height:clamp(56px,16vw,80px); width:auto; image-rendering:pixelated; display:block; } /* Player is fixed to the viewport centre horizontally; only Y moves */ /* "..." bubble above player when pressing talk away from NPC */ .player-bubble { @@ -544,7 +583,7 @@ html, body { background-size: 768px 64px; image-rendering: pixelated; filter: drop-shadow(2px 3px 0 rgba(0,0,0,.4)); - z-index: 20; + z-index: 70; font-size: 0; color: transparent; left: -9999px; @@ -595,7 +634,31 @@ html, body { } .battle-scene::after { content:''; position:absolute; top:55%; left:0; right:0; height:2px; background:rgba(0,0,0,.18); } .battle-npc-side, .battle-player-side { display:flex; flex-direction:column; align-items:center; gap:4px; } -.battle-sprite { font-size:clamp(38px,11vw,56px); filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); animation:npcFloat 2s ease-in-out infinite; } +.battle-npc-row, .battle-player-row { display:flex; align-items:flex-end; gap:2px; } +.battle-npc-trainer, .battle-player-trainer { + font-size:clamp(38px,11vw,56px); + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); + align-self:flex-end; + line-height:1; +} +.battle-npc-trainer img, .battle-player-trainer img { + height:clamp(56px,16vw,80px); + width:auto; + image-rendering:pixelated; + display:block; +} +.battle-player-trainer { + width:32px; + height:64px; + background-image:url('./player.png'); + background-repeat:no-repeat; + background-size:768px 64px; + background-position:-32px 0; + image-rendering:pixelated; + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); +} +.battle-sprite { font-size:clamp(14px,4vw,20px); filter:drop-shadow(1px 2px 0 rgba(0,0,0,.3)); animation:npcFloat 2s ease-in-out infinite; } +.battle-sprite img { height:clamp(18px,5vw,24px); width:auto; image-rendering:pixelated; display:block; } .battle-player-side .battle-sprite { animation:bounce 1s ease-in-out infinite; } /* Player sprite on battle screen โ€” use sprite sheet facing down-idle */ .battle-player-sprite { @@ -706,7 +769,7 @@ html, body { .hidden { display:none !important; } /* โ”€โ”€โ”€ LANDSCAPE CONTROLLER LAYOUT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -@media (orientation: landscape) and (max-height: 500px) { +@media (orientation: landscape) and (max-height: 600px) { html, body { align-items: flex-start; justify-content: flex-start; } #app { @@ -754,4 +817,633 @@ html, body { #gba-right .ab-a { top: 14%; right: 5%; transform: none; } .gba-mid-btns { flex-direction: column; gap: 5px; } .gba-mid-btn { min-width: 44px; width: 44px; font-size: 5px; padding: 6px 4px; } + + .intro-npc-centered img { height: clamp(60px, 20vh, 100px) !important; } + .intro-npc-centered { font-size: clamp(40px, 14vh, 70px); bottom: 30% !important; } + .starter-choices { gap: 6px; padding: 4px 6px; } + .starter-btn { padding: 6px 4px; } + .starter-btn .starter-emoji { font-size: clamp(24px, 8vh, 36px); } + .catch-wild-pokemon { font-size: clamp(40px, 14vh, 60px); } + .catch-scene { padding: 8px 12px 4px; } + .battle-anim-npc-sprite, .battle-anim-player-sprite { font-size: clamp(32px, 10vh, 48px); } + .battle-scene { min-height: clamp(70px, 18vh, 110px); } +} + +/* โ”€โ”€โ”€ STARTER SELECTION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-starter { + background:linear-gradient(180deg, #6cb84e 0%, #58a83e 100%); + flex-direction:column; + justify-content:flex-start; + overflow-y:auto; + -webkit-overflow-scrolling:touch; +} +#screen-starter .overworld-bg { + flex:0 0 auto; + min-height:30%; + max-height:38%; +} +.starter-dialog { + margin:0 8px; + min-height:60px; + flex-shrink:0; +} +.starter-choices { + display:flex; + justify-content:center; + gap:clamp(6px,2vw,12px); + padding:8px 10px; + padding-bottom:max(16px, calc(var(--safe-bottom) + 160px)); + flex-shrink:0; +} +.starter-btn { + display:flex; + flex-direction:column; + align-items:center; + gap:6px; + padding:clamp(10px,3vw,16px) clamp(8px,2.5vw,14px); + background:var(--dialog-bg); + border:4px solid var(--dialog-bd); + cursor:pointer; + font-family:var(--font); + transition:transform .15s, box-shadow .15s; + box-shadow:4px 4px 0 rgba(0,0,0,.4); + flex:1; + max-width:130px; +} +.starter-btn:hover, +.starter-btn:active { + transform:translateY(-3px); + box-shadow:4px 7px 0 rgba(0,0,0,.4); + background:#fff8e0; +} +.starter-btn .starter-emoji { + font-size:clamp(32px,10vw,48px); + animation:npcFloat 2s ease-in-out infinite; + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); +} +.starter-btn .starter-name { + font-size:clamp(5.5px,1.6vw,7px); + color:var(--black); +} +.starter-btn .starter-type { + font-size:clamp(5px,1.3vw,6px); + color:var(--lightgray); +} + +/* โ”€โ”€โ”€ STARTER CONFIRM โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-starter-confirm { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#c8b880 55%,#b09860 100%); + align-items:center; + justify-content:center; +} +.starter-confirm-bg { + display:flex; + flex-direction:column; + align-items:center; + gap:10px; + padding:16px; + width:100%; +} +.starter-chosen-emoji { + font-size:clamp(56px,18vw,80px); + animation:bounce 1s ease-in-out infinite; + filter:drop-shadow(3px 4px 0 rgba(0,0,0,.35)); +} +.starter-chosen-name { + font-size:clamp(11px,3.5vw,15px); + color:var(--black); + font-family:var(--font); + text-shadow:1px 1px 0 rgba(255,255,255,.6); +} +.starter-chosen-level { + font-size:clamp(7px,2vw,9px); + color:var(--gray); + font-family:var(--font); +} +.starter-confirm-dialog { + margin:0 12px; + width:calc(100% - 24px); +} +.starter-flee-text { + font-family:var(--font); + font-size:clamp(6px,1.7vw,7.5px); + color:var(--gray); + text-align:center; + line-height:2; + padding:0 16px; + animation:fadeIn .8s ease; +} + +/* โ”€โ”€โ”€ CATCH SCREEN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-catch { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#60a840 55%,#408830 100%); + flex-direction:column; + justify-content:flex-start; +} +.catch-scene { + display:flex; + flex-direction:column; + align-items:center; + gap:6px; + padding:clamp(16px,5vw,28px) 16px 10px; + flex-shrink:0; +} +.catch-wild-pokemon { + font-size:clamp(56px,18vw,80px); + animation:npcFloat 2s ease-in-out infinite; + filter:drop-shadow(3px 4px 0 rgba(0,0,0,.35)); +} +.catch-wild-name { + font-family:var(--font); + font-size:clamp(9px,2.8vw,12px); + color:var(--black); + text-shadow:1px 1px 0 rgba(255,255,255,.6); +} +.catch-wild-level { + font-family:var(--font); + font-size:clamp(6.5px,1.8vw,8px); + color:var(--gray); +} +.catch-dialog { + margin:8px; + min-height:50px; + flex-shrink:0; +} +.catch-actions { + display:flex; + gap:8px; + padding:6px 12px; + padding-bottom:max(16px, calc(var(--safe-bottom) + 160px)); + justify-content:center; + flex-shrink:0; +} +.catch-action-btn { + flex:1; + max-width:160px; +} + +/* โ”€โ”€โ”€ CATCH RESULT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-catch-result { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#60a840 55%,#408830 100%); + align-items:center; + justify-content:center; +} +.catch-result-bg { + display:flex; + flex-direction:column; + align-items:center; + gap:14px; + padding:20px; + width:100%; +} +.catch-result-emoji { + font-size:clamp(48px,15vw,72px); + filter:drop-shadow(3px 4px 0 rgba(0,0,0,.35)); +} +.catch-result-msg { + font-family:var(--font); + font-size:clamp(8px,2.5vw,11px); + color:var(--black); + text-align:center; + line-height:2; + text-shadow:1px 1px 0 rgba(255,255,255,.6); + padding:0 16px; +} +.catch-result-msg.success { color:var(--green); } +.catch-result-msg.fail { color:var(--red); } + +/* โ”€โ”€โ”€ BATTLE POKEMON LABELS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.battle-npc-pokemon-name, +.battle-player-pokemon-name { + font-family:var(--font); + font-size:clamp(5px,1.4vw,6.5px); + color:var(--black); + background:rgba(255,255,255,.7); + border:2px solid rgba(0,0,0,.2); + padding:1px 5px; + text-align:center; +} +.battle-npc-pokemon-level, +.battle-player-pokemon-level { + font-family:var(--font); + font-size:clamp(4.5px,1.2vw,5.5px); + color:var(--gray); + text-align:center; +} + +/* โ”€โ”€โ”€ BATTLE ANIMATION SCREEN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-battle-anim { + background:linear-gradient(180deg,#90c8f0 0%,#70b0e0 55%,#c8b880 55%,#b09860 100%); + flex-direction:column; + justify-content:center; + align-items:center; +} +.battle-anim-scene { + display:flex; + justify-content:space-around; + align-items:center; + width:100%; + padding:10px 16px; + gap:8px; +} +.battle-anim-npc, +.battle-anim-player { + display:flex; + flex-direction:column; + align-items:center; + gap:6px; + flex:1; +} +.battle-anim-npc-name, +.battle-anim-player-name { + font-family:var(--font); + font-size:clamp(6px,1.8vw,8px); + color:var(--black); + text-shadow:1px 1px 0 rgba(255,255,255,.5); +} +.battle-anim-npc-sprite, +.battle-anim-player-sprite { + font-size:clamp(42px,13vw,64px); + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); + transition:opacity .6s ease, transform .6s ease; +} +.battle-anim-vs { + font-size:clamp(20px,6vw,32px); + animation:bounce .8s ease-in-out infinite; + filter:drop-shadow(2px 2px 0 rgba(0,0,0,.3)); +} +.battle-anim-hp-bar { + width:clamp(60px,20vw,100px); + height:10px; + background:var(--black); + border:2px solid var(--lightgray); + overflow:hidden; +} +.battle-anim-hp-fill { + height:100%; + width:100%; + background:var(--hp-green); + transition:width 1s ease; +} +.battle-anim-msg { + font-family:var(--font); + font-size:clamp(8px,2.5vw,11px); + color:var(--black); + text-align:center; + padding:12px 16px; + line-height:2; + text-shadow:1px 1px 0 rgba(255,255,255,.5); + min-height:40px; +} + +/* โ”€โ”€โ”€ WILD POKEMON ON MAP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.map-wild-wrap { + position:absolute; + text-align:center; + z-index:70; + cursor:pointer; +} +.wild-bubble { + font-size:12px; + font-family:var(--font); + color:var(--black); + background:var(--green); + border:2px solid var(--black); + width:18px; + height:18px; + display:flex; + align-items:center; + justify-content:center; + margin:0 auto 2px; + animation:bounce .7s ease-in-out infinite; + color:#fff; +} +.map-wild { + font-size:clamp(28px,8vw,40px); + filter:drop-shadow(2px 3px 0 rgba(0,0,0,.3)); + animation:npcFloat 2s ease-in-out infinite; +} + +/* โ”€โ”€โ”€ POKEBALL COUNT IN MAP HEADER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.pokeball-count { + font-size:clamp(6px,1.6vw,7.5px); + color:var(--white); + text-shadow:1px 1px 0 rgba(0,0,0,.5); + white-space:nowrap; +} + +/* โ”€โ”€โ”€ RESULT POKEBALLS REWARD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.result-pokeballs { + font-size:clamp(7px,2vw,9px); + color:var(--blue); + text-shadow:1px 1px 0 rgba(0,0,0,.15); + animation:blueShimmer 2s ease-in-out infinite; +} + +/* โ”€โ”€โ”€ COMPLETE SCREEN PARTY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.complete-party-title { + font-size:clamp(9px,2.8vw,12px); + color:var(--yellow); + text-shadow:2px 2px 0 #604000; + margin-top:8px; +} +.complete-party-grid { + display:grid; + grid-template-columns:repeat(auto-fill, minmax(80px, 1fr)); + gap:6px; + width:100%; + max-width:340px; +} +.complete-party-entry { + display:flex; + flex-direction:column; + align-items:center; + gap:2px; + background:rgba(255,255,255,.06); + border:2px solid rgba(248,192,48,.25); + padding:6px 4px; + font-family:var(--font); +} +.complete-party-entry .party-emoji { + font-size:clamp(20px,6vw,28px); +} +.complete-party-entry .party-name { + font-size:clamp(4.5px,1.2vw,5.5px); + color:var(--white); +} +.complete-party-entry .party-level { + font-size:clamp(4px,1vw,5px); + color:var(--lightgray); +} + +/* โ”€โ”€โ”€ POKEBALL SHAKE ANIMATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +@keyframes pokeballShake { + 0%,100% { transform:rotate(0deg); } + 20% { transform:rotate(-15deg); } + 40% { transform:rotate(15deg); } + 60% { transform:rotate(-10deg); } + 80% { transform:rotate(10deg); } +} +.pokeball-shaking { + animation:pokeballShake .6s ease-in-out; + display:inline-block; +} + +/* โ”€โ”€โ”€ CATCH SUCCESS FLASH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +@keyframes catchFlash { + 0% { background:rgba(255,255,255,0); } + 50% { background:rgba(255,255,255,.6); } + 100% { background:rgba(255,255,255,0); } +} +.catch-flash { + animation:catchFlash .4s ease; +} + +/* โ”€โ”€โ”€ CATCH RESULT TEXT SUB-STYLES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.catch-success-title { + font-size:clamp(12px,4vw,16px); + color:var(--green); + text-shadow:2px 2px 0 #003010; + margin-bottom:4px; +} +.catch-fail-title { + font-size:clamp(12px,4vw,16px); + color:var(--red); + text-shadow:2px 2px 0 #400010; + margin-bottom:4px; +} +.catch-success-detail, +.catch-fail-detail { + font-size:clamp(7px,2vw,9px); + line-height:2; +} +.catch-success-sub { + font-size:clamp(6px,1.6vw,7.5px); + color:var(--green); + margin-top:4px; +} +.catch-fail-sub { + font-size:clamp(6px,1.6vw,7.5px); + color:var(--lightgray); + margin-top:4px; +} + +/* โ”€โ”€โ”€ STARTER / CATCH CURSOR SELECTION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.starter-btn-selected { + outline:3px solid var(--yellow); + outline-offset:-2px; + background:#fff8e0; + transform:translateY(-3px); + box-shadow:4px 7px 0 rgba(0,0,0,.4); +} +.catch-btn-selected { + outline:3px solid var(--yellow); + outline-offset:-2px; +} + +/* โ”€โ”€โ”€ MAP ROCKS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.map-rock { + position: absolute; + display: inline-block; + filter: drop-shadow(2px 2px 0 rgba(0,0,0,.4)); + line-height: 1; + z-index: 15; +} + +/* โ”€โ”€โ”€ BATTLE POKEMON FLOAT (name+level above sprite) โ”€โ”€โ”€โ”€โ”€ */ +.battle-pokemon-float { + display:flex; + flex-direction:column; + align-items:center; + gap:1px; +} + +/* โ”€โ”€โ”€ POKEBALL ICON โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.pokeball-icon { + width:14px; + height:14px; + vertical-align:middle; + image-rendering:pixelated; + display:inline-block; + margin-right:2px; +} + +/* โ”€โ”€โ”€ POKEBALL THROW ANIMATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.pokeball-throw { + position:absolute; + width:32px; + height:32px; + bottom:15%; + right:8%; + z-index:200; + image-rendering:pixelated; + offset-path:path('M 0 0 C -80 -180, -140 -340, -160 -460'); + animation:pokeThrow .7s cubic-bezier(.2,.8,.3,1) forwards; +} +@keyframes pokeThrow { + 0% { offset-distance:0%; transform:rotate(0deg) scale(1); opacity:1; } + 100% { offset-distance:100%; transform:rotate(-540deg) scale(0.7); opacity:1; } +} + +.pokeball-burst { + position:absolute; + width:64px; + height:64px; + margin-left:-32px; + margin-top:-32px; + z-index:200; + border-radius:50%; + background:radial-gradient(circle, rgba(255,255,255,1) 0%, rgba(255,255,200,.8) 30%, rgba(255,200,100,.4) 60%, transparent 80%); + animation:burstAnim .6s ease-out forwards; + pointer-events:none; +} +@keyframes burstAnim { + 0% { transform:scale(0.3); opacity:1; } + 40% { transform:scale(2.5); opacity:.9; } + 100% { transform:scale(4); opacity:0; } +} + +/* โ”€โ”€โ”€ BATTLE ANIM TRAINER PEEK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.battle-anim-trainer-npc, +.battle-anim-trainer-player { + position:absolute; + top:50%; + transform:translateY(-50%); + z-index:5; + opacity:.7; + font-size:clamp(50px,15vw,72px); +} +.battle-anim-trainer-npc { + left:4px; +} +.battle-anim-trainer-npc img { + height:clamp(72px,20vw,100px); + width:auto; + image-rendering:pixelated; + display:block; +} +.battle-anim-trainer-player { + right:4px; +} +.anim-player-sprite-sheet { + width:48px; + height:96px; + background-image:url('./player.png'); + background-repeat:no-repeat; + background-size:1152px 96px; + background-position:-48px 0; + image-rendering:pixelated; +} + +/* โ”€โ”€โ”€ PARTY SELECT SCREEN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +#screen-party-select { + background:linear-gradient(180deg,#1a1a2e 0%,#16213e 100%); + flex-direction:column; + align-items:center; + padding:12px 10px; + overflow-y:auto; + -webkit-overflow-scrolling:touch; +} +.party-select-header { + font-family:var(--font); + font-size:clamp(10px,3vw,14px); + color:var(--yellow); + text-shadow:2px 2px 0 #604000; + text-align:center; + padding:8px 0 12px; + flex-shrink:0; +} +.party-select-list { + display:flex; + flex-direction:column; + gap:6px; + width:100%; + max-width:380px; + padding-bottom:max(16px, calc(var(--safe-bottom) + 160px)); +} +.party-select-entry { + display:flex; + align-items:center; + gap:10px; + padding:10px 12px; + background:var(--dialog-bg); + border:3px solid var(--dialog-bd); + cursor:pointer; + font-family:var(--font); + transition:transform .1s, box-shadow .1s; + box-shadow:3px 3px 0 rgba(0,0,0,.4); + position:relative; +} +.party-select-entry:hover, +.party-select-entry.party-selected { + transform:translateX(6px); + box-shadow:3px 3px 0 rgba(0,0,0,.4); + border-color:var(--yellow); + background:#fff8e0; +} +.party-select-entry.fainted { + opacity:.5; + cursor:not-allowed; +} +.party-select-entry.fainted:hover { + transform:none; + border-color:var(--dialog-bd); + background:var(--dialog-bg); +} +.party-select-emoji { + font-size:clamp(28px,8vw,40px); + position:relative; + flex-shrink:0; +} +.party-select-zzz { + position:absolute; + top:-8px; + right:-8px; + font-family:var(--font); + font-size:clamp(6px,1.8vw,8px); + color:var(--lightgray); + animation:bounce 1.5s ease-in-out infinite; +} +.party-select-info { + display:flex; + flex-direction:column; + gap:2px; +} +.party-select-name { + font-size:clamp(8px,2.5vw,11px); + color:var(--black); +} +.party-select-detail { + font-size:clamp(5.5px,1.5vw,7px); + color:var(--gray); +} +.party-select-badge { + margin-left:auto; + font-size:clamp(5px,1.3vw,6.5px); + color:var(--green); + font-family:var(--font); +} +.party-select-badge.badge-fainted { + color:var(--red); +} + +/* โ”€โ”€โ”€ POKEBALL REST + BOUNCE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.pokeball-rest { + position:absolute; + width:40px; + height:40px; + z-index:200; + image-rendering:pixelated; +} +.pokeball-bounce-anim { + animation:pokeballBounce .5s ease-out; +} +@keyframes pokeballBounce { + 0% { transform:translateY(0); } + 20% { transform:translateY(-24px) rotate(-10deg); } + 40% { transform:translateY(0) rotate(0deg); } + 55% { transform:translateY(-12px) rotate(8deg); } + 70% { transform:translateY(0) rotate(0deg); } + 82% { transform:translateY(-5px) rotate(-4deg); } + 100% { transform:translateY(0) rotate(0deg); } }