Upgraded codebase, added starter/catch/party system, and introduced config.js for easy multi-game maintenance

This commit is contained in:
Sterling Archer committed 2026-05-31 12:44:00 -07:00
1 parent d16753f013
commit 5b18f9bda1
6 files changed
+1940 -396

No files matched your search

+114 -125
View File
@@ -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
+33
View File
@@ -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?",
};
+941 -223
View File
File diff suppressed because it is too large. Load diff
+139 -27
View File
@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0a0520">
<title>PokéAzure — Study for Azure Certifications</title>
<title id="page-title">PokéAzure — Learn Azure Cloud</title>
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="style.css">
</head>
@@ -17,9 +17,8 @@
<!-- BOOT SPLASH -->
<div id="screen-boot" class="screen active">
<div class="boot-logo">
<div class="boot-gameboy">GAME BOY ADVANCE</div>
<div class="boot-gameboy">GAME BOY<sup>™</sup></div>
<div class="boot-ding">♪</div>
<div class="boot-copy">© 2026 PokéAzure</div>
</div>
</div>
@@ -29,19 +28,19 @@
<div class="title-stars"></div>
<div class="title-main">
<div class="title-logo">
<span class="title-poke">POKÉ</span><span class="title-sql">AZURE</span>
<span class="title-poke">POKÉ</span><span class="title-game" id="title-game-text">AZURE</span>
</div>
<div class="title-edition">★ AZURE CERT TRAINER ★</div>
<div class="title-edition" id="title-edition-text">★ AZURE TRAINER ★</div>
<div class="title-tap blink">PRESS START</div>
</div>
<div class="title-footer">PokéAzure v1.0 · AZ-900 · github.com/JustinOros</div>
<div class="title-footer" id="title-footer-text">PokéAZURE v1.0 · Azure · github.com/JustinOros</div>
</div>
</div>
<!-- CONTINUE OR NEW GAME -->
<div id="screen-continue" class="screen">
<div class="continue-bg">
<div class="continue-logo">POKÉ<span>AZURE</span></div>
<div class="continue-logo">POKÉ<span id="continue-logo-text">AZURE</span></div>
<div class="continue-card" id="continue-card">
<div class="continue-card-header">📂 CONTINUE</div>
<div class="save-row"><span class="save-lbl">TRAINER</span><span class="save-val" id="save-name-disp">—</span></div>
@@ -73,9 +72,8 @@
<!-- INTRO CUTSCENE -->
<div id="screen-intro" class="screen">
<div class="overworld-bg">
<div class="intro-npc" id="intro-npc"></div>
<div class="intro-player"></div>
<div class="overworld-bg intro-oak-bg">
<div class="intro-npc intro-npc-centered" id="intro-npc"></div>
</div>
<div class="dialog-box intro-dialog">
<div class="dialog-speaker" id="intro-speaker">PROFESSOR OAK</div>
@@ -85,18 +83,73 @@
<div class="tap-hint blink"><span class="hint-pc">PRESS E TO CONTINUE</span><span class="hint-touch">TAP / PRESS A TO CONTINUE</span></div>
</div>
<!-- STARTER POKEMON SELECTION -->
<div id="screen-starter" class="screen">
<div class="overworld-bg intro-oak-bg">
<div class="intro-npc intro-npc-centered" id="starter-oak"></div>
</div>
<div class="dialog-box starter-dialog">
<div class="dialog-speaker">PROFESSOR OAK</div>
<div class="dialog-text" id="starter-text"></div>
</div>
<div class="starter-choices" id="starter-choices"></div>
</div>
<!-- STARTER CONFIRM -->
<div id="screen-starter-confirm" class="screen">
<div class="starter-confirm-bg">
<div class="starter-chosen-emoji" id="starter-chosen-emoji"></div>
<div class="starter-chosen-name" id="starter-chosen-name"></div>
<div class="starter-chosen-level" id="starter-chosen-level"></div>
<div class="dialog-box starter-confirm-dialog">
<div class="dialog-text" id="starter-confirm-text"></div>
</div>
<div class="starter-flee-text" id="starter-flee-text"></div>
<button class="btn-pixel btn-green btn-full" id="btn-starter-continue">LET'S GO! ▶</button>
</div>
</div>
<!-- CATCH SCREEN -->
<div id="screen-catch" class="screen">
<div class="catch-scene">
<div class="catch-wild-pokemon" id="catch-wild-pokemon"></div>
<div class="catch-wild-name" id="catch-wild-name"></div>
<div class="catch-wild-level" id="catch-wild-level"></div>
</div>
<div class="dialog-box catch-dialog">
<div class="dialog-text" id="catch-text"></div>
</div>
<div class="catch-actions" id="catch-actions"></div>
</div>
<!-- CATCH RESULT -->
<div id="screen-catch-result" class="screen">
<div class="catch-result-bg">
<div class="catch-result-emoji" id="catch-result-emoji"></div>
<div class="catch-result-msg" id="catch-result-msg"></div>
<button class="btn-pixel btn-blue btn-full" id="btn-catch-continue">CONTINUE ▶</button>
</div>
</div>
<!-- PARTY SELECT -->
<div id="screen-party-select" class="screen">
<div class="party-select-header">CHOOSE YOUR POKÉMON!</div>
<div class="party-select-list" id="party-select-list"></div>
</div>
<!-- OVERWORLD MAP -->
<div id="screen-map" class="screen">
<div class="map-header">
<div class="map-left">
<span class="map-pin">📍</span>
<span class="map-town-name">AZ-900 TOWN</span>
<span class="map-town-name" id="map-town-name-text">AZURE TOWN</span>
</div>
<div class="map-center">
<div class="prog-bar-outer"><div class="prog-bar-inner" id="map-prog-bar"></div></div>
<span class="prog-text" id="map-prog-text">0/100</span>
</div>
<div class="map-right">
<span class="pokeball-count" id="pokeball-count" title="Pokéballs"><img src="./favicon.ico" class="pokeball-icon"> 0</span>
<span class="music-toggle" id="music-toggle" title="Mute/Unmute Music">🎵</span>
</div>
</div>
@@ -104,16 +157,17 @@
<div class="map-inner" id="map-inner">
<div class="map-trees" id="map-trees"></div>
</div>
<!-- Path stays fixed to the viewport — does not scroll -->
<!-- NPC in viewport layer — JS positions it using screen-space coords -->
<div class="map-npc-wrap" id="map-npc-wrap">
<div class="npc-bubble" id="npc-bubble">!</div>
<div class="map-npc" id="map-npc"></div>
</div>
<!-- Player stays fixed to viewport, only Y moves -->
<!-- Wild pokemon spawns -->
<div class="map-wild-wrap" id="map-wild-wrap" style="display:none">
<div class="wild-bubble">?</div>
<div class="map-wild" id="map-wild"></div>
</div>
<div class="player-bubble" id="player-bubble">...</div>
<div class="map-player" id="map-player"></div>
<!-- Hints overlay the viewport, not the world -->
<div class="map-talk-hint" id="map-talk-hint" style="display:none"><span class="hint-pc">Press E</span><span class="hint-touch">Press A</span></div>
<div class="map-dir-arrow" id="map-dir-arrow" style="display:none">▶</div>
</div>
@@ -121,7 +175,7 @@
<div class="hud-cell"><div class="hud-lbl">TRAINER</div><div class="hud-val" id="hud-name">ASH</div></div>
<div class="hud-cell"><div class="hud-lbl">SCORE</div><div class="hud-val" id="hud-score">0</div></div>
<div class="hud-cell"><div class="hud-lbl">STREAK</div><div class="hud-val" id="hud-streak">0</div></div>
<div class="hud-cell"><div class="hud-lbl">CORRECT</div><div class="hud-val" id="hud-correct">0/0</div></div>
<div class="hud-cell"><div class="hud-lbl">PARTY</div><div class="hud-val" id="hud-party">0</div></div>
</div>
</div>
@@ -129,17 +183,30 @@
<div id="screen-battle" class="screen">
<div class="battle-scene">
<div class="battle-npc-side">
<div class="battle-sprite" id="battle-npc-sprite">👴</div>
<div class="battle-npc-row">
<div class="battle-pokemon-float">
<div class="battle-npc-pokemon-name" id="battle-npc-pokemon-name"></div>
<div class="battle-npc-pokemon-level" id="battle-npc-pokemon-level"></div>
<div class="battle-sprite" id="battle-npc-pokemon">🐉</div>
</div>
<div class="battle-npc-trainer" id="battle-npc-trainer"></div>
</div>
<div class="battle-name-tag" id="battle-npc-name">OAK</div>
</div>
<div class="battle-player-side">
<div class="battle-player-sprite"></div>
<div class="battle-player-row">
<div class="battle-player-trainer" id="battle-player-trainer"></div>
<div class="battle-pokemon-float">
<div class="battle-player-pokemon-name" id="battle-player-pokemon-name"></div>
<div class="battle-player-pokemon-level" id="battle-player-pokemon-level"></div>
<div class="battle-sprite" id="battle-player-pokemon">🔥</div>
</div>
</div>
<div class="battle-name-tag" id="battle-player-name">ASH</div>
</div>
</div>
<div class="dialog-box battle-dialog">
<div class="dialog-speaker" id="battle-speaker" style="display:none">PROFESSOR OAK</div>
<div class="dialog-text" id="battle-text"></div>
<div class="dialog-arrow blink" id="battle-arrow" style="display:none">▼</div>
</div>
@@ -151,6 +218,30 @@
</div>
</div>
<!-- BATTLE RESULT ANIMATION -->
<div id="screen-battle-anim" class="screen">
<div class="battle-anim-trainer-npc" id="anim-npc-trainer"></div>
<div class="battle-anim-trainer-player" id="anim-player-trainer"></div>
<div class="battle-anim-scene">
<div class="battle-anim-npc">
<div class="battle-anim-npc-name" id="anim-npc-pokemon-name"></div>
<div class="battle-anim-npc-sprite" id="anim-npc-pokemon"></div>
<div class="battle-anim-hp-bar">
<div class="battle-anim-hp-fill" id="anim-npc-hp"></div>
</div>
</div>
<div class="battle-anim-vs">⚔️</div>
<div class="battle-anim-player">
<div class="battle-anim-player-name" id="anim-player-pokemon-name"></div>
<div class="battle-anim-player-sprite" id="anim-player-pokemon"></div>
<div class="battle-anim-hp-bar">
<div class="battle-anim-hp-fill" id="anim-player-hp"></div>
</div>
</div>
</div>
<div class="battle-anim-msg" id="battle-anim-msg"></div>
</div>
<!-- RESULT -->
<div id="screen-result" class="screen">
<div class="result-box">
@@ -159,6 +250,7 @@
<div class="result-correct-ans" id="result-correct-ans"></div>
<div class="result-explanation" id="result-explanation"></div>
<div class="result-pts" id="result-pts">+100 PTS</div>
<div class="result-pokeballs" id="result-pokeballs" style="display:none"></div>
<button class="btn-pixel btn-blue btn-full" id="btn-result-cont">CONTINUE ▶</button>
</div>
</div>
@@ -168,7 +260,7 @@
<div class="levelup-box">
<div class="lu-stars blink" id="lu-stars">★ BADGE EARNED! ★</div>
<div class="lu-sprite"><div class="lu-player-sprite"></div></div>
<div class="lu-text"><span id="lu-name">ASH</span> gained Azure EXP!</div>
<div class="lu-text"><span id="lu-name">ASH</span> gained <span id="exp-label-text">AZURE EXP</span>!</div>
<div class="lu-badge" id="lu-badge">🏅 CLOUD TRAINEE</div>
<div class="lu-prog-wrap">
<div class="lu-prog-outer"><div class="lu-prog-inner" id="lu-prog-inner"></div></div>
@@ -185,13 +277,14 @@
<div class="complete-trophy">🏆</div>
<div class="complete-title">CHAMPION!</div>
<div class="complete-name" id="complete-name">ASH</div>
<p class="complete-msg">You've conquered AZ-900 Town and mastered AZ-900! Your Azure journey is just beginning — more towns and exams await!</p>
<p class="complete-msg"><span id="complete-msg-text">You've conquered Azure Town and mastered AZ-900! Your Azure journey is just beginning — more towns and exams await!</span></p>
<div class="complete-stats" id="complete-stats"></div>
<div class="complete-pokemon" id="complete-pokemon"></div>
<div class="complete-badges">
<div class="badge-row">🥉 CLOUD EXPLORER — Q25</div>
<div class="badge-row">🥈 AZURE APPRENTICE — Q50</div>
<div class="badge-row">🥇 CLOUD PRACTITIONER — Q75</div>
<div class="badge-row">🏆 AZ-900 CHAMPION — Q100</div>
<div class="badge-row"><span id="badge-1">🥉 AZURE EXPLORER — Q25</span></div>
<div class="badge-row"><span id="badge-2">🥈 AZURE APPRENTICE — Q50</span></div>
<div class="badge-row"><span id="badge-3">🥇 AZURE PRACTITIONER — Q75</span></div>
<div class="badge-row"><span id="badge-4">🏆 AZURE CHAMPION — Q100</span></div>
<div class="badge-row">🗺️ More towns & exams coming soon...</div>
</div>
<button class="btn-pixel btn-red btn-full" id="btn-play-again">▶ PLAY AGAIN</button>
@@ -220,9 +313,28 @@
</div>
</div>
</div><!-- /#app -->
</div>
<div id="toast" class="toast hidden"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
set('title-game-text', CONFIG.titleText);
set('title-edition-text', CONFIG.titleEdition);
set('title-footer-text', CONFIG.titleFooter);
set('continue-logo-text', CONFIG.continueLogoSuffix);
set('map-town-name-text', CONFIG.townNameUpper);
set('exp-label-text', CONFIG.expLabel);
set('complete-msg-text', CONFIG.completeMsg);
if (CONFIG.badges) {
CONFIG.badges.forEach((b, i) => set(`badge-${i+1}`, b));
}
set('page-title', CONFIG.gameName + ' — Learn ' + CONFIG.subject);
document.title = CONFIG.gameName + ' — Learn ' + CONFIG.subject;
});
</script>
<script src="config.js"></script>
<script src="game.js"></script>
</body>
</html>
BIN
View File
Binary file not shown.
+713 -21
View File
@@ -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; }
.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; }
.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; }
/* 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); }
}