From 9fea2ecc9dcd6531b5dcc8867a344ef703ba3e83 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 26 Apr 2026 08:27:33 +0700 Subject: [PATCH] chore(i18n,art,audio): finalize EN/VI strings, pixel art, audio cues, docs Finalize EN+VI translations (42 new keys). Add 6 procedural Web Audio cues (stone-throw, impact, key-pickup, door-unlock, suspicion-alert, suspicion-fire). Pixel art for sniper, suspicion (3 tiers), stones, doors+keys (3 colors), one-way arrows, warm cells. Foreshadowing rewrite for L10-L12 reflecting level-restart model. Docs updated for v2. --- README.md | 137 ++++++++-------- docs/codebase-summary.md | 215 +++++++++++++++---------- docs/game-design.md | 270 ++++++++++++++++++++------------ src/lib/audio.js | 115 ++++++++++++++ src/lib/locales/en.json | 55 +++++-- src/lib/locales/vi.json | 55 +++++-- src/lib/pixel/art-characters.js | 117 ++++++++++++++ src/lib/pixel/art-tiles.js | 164 +++++++++++++++++++ 8 files changed, 846 insertions(+), 282 deletions(-) diff --git a/README.md b/README.md index a6d92aa..e839376 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,44 @@ # Night Ninja: Twilight Voyage -Night Ninja: Twilight Voyage is a turn-based stealth puzzle game where you play as a ninja rabbit navigating through a grid-based environment while avoiding detection by various vegetable guards. Your goal is to rescue the missing carrot princess of the vegetable kingdom. +Night Ninja: Twilight Voyage is a turn-based stealth puzzle game where you play as a ninja rabbit navigating through a grid-based environment while avoiding detection by vegetable guards. Your goal is to rescue the missing carrot princess of the vegetable kingdom. ## Game Overview -In Night Ninja: Twilight Voyage, you play as a ninja rabbit trying to navigate through increasingly complex levels in the vegetable kingdom to rescue the missing carrot princess. Each level consists of a grid where: +Guide the ninja rabbit through 12 levels of escalating danger inside the Vegetable Kingdom. Each level is a grid where: -- You must move from your starting position to the green goal cell -- Gray cells represent walls that block movement -- Colored triangles represent different types of guards that create light in certain cells -- If you step into a lit cell, you'll be detected and lose a life -- You have 3 lives to complete all levels +- Move from your starting position (top-left) to the green goal cell (bottom-right) +- Gray cells are walls — impassable +- Yellow lit cells are danger zones — step into one and the level restarts +- Colored guards illuminate cells around them in various patterns +- New mechanics unlock across acts: one-way tiles, throwable stones, locked doors, decay tiles, and more ## Game Features -- **Turn-based gameplay**: Each move you make triggers the vegetable guards to take their turn -- **Multiple guard types**: - - Static Guards (red): Light up fixed cells around them - - Rotating Guards (blue): Rotate and light up cells in different directions each turn - - Blinking Guards (yellow): Toggle their lights on and off each turn - - Patrolling Guards (purple): Move along predefined paths, lighting cells around them - - Mirror Guards (green): Deflect rotating beams 90 degrees - - Chaser Guards (orange): Detect nearby players and hunt them using pathfinding -- **Progressive difficulty**: 12 levels across 6 acts, arenas growing from 8x8 up to 13x13 -- **Grid-based movement**: Move one cell at a time using arrow keys, WASD, or clicking -- **Stealth mechanics**: Avoid lit cells to remain undetected -- **Undo/redo**: Press Z to undo moves, Y to redo — experiment without full restarts -- **Turn preview**: Press V to see where lights will be next turn -- **Camera-follow viewport**: Large arenas scroll smoothly to keep the ninja in view -- **Sound effects**: Procedural audio feedback for moves, detection, and completion -- **Solvability-verified**: A BFS solver runs in CI to guarantee every level can be beaten -- **Accessibility**: ARIA labels on grid cells for screen readers +- **Turn-based stealth**: Each move triggers all guards simultaneously — plan before stepping +- **Level-restart on detection**: No lives counter; every detection restarts the current level only +- **Guard types (8 total)**: + - Static Guards (red/wilting tomato): Manhattan aura shrinks each turn — wait them out + - Rotating Guards (blue): Beam rotates 90° clockwise each turn + - Blinking Guards (yellow): Toggle lights on/off each turn + - Patrolling Guards (purple): Move along a path, lighting front and right cells + - Mirror Guards (green): Redirect rotating beams 90° + - Chaser Guards (orange): BFS-hunt the player when in detection radius + - **Sniper Guards (dark red)**: Aim along a fixed line of sight — step into the aim line, instant detection + - **Suspicion Guards (violet)**: Tier-based alert system: calm → alerted (tier 1) → firing (tier 2) +- **Throwable stones**: Press **E** to enter targeting mode, throw a stone to distract guards (rotating, patrolling, chaser types) +- **Doors + keys**: Color-coded key items unlock matching doors (gold/silver/copper) +- **One-way tiles**: Arrow tiles only allow entry from the designated direction — commitment ratchets +- **Decay/warm tiles**: Cells that cool over turns; danger if stepped on while warm +- **Per-level affordance gates**: Some levels disable undo or preview to raise stakes +- **Undo/redo**: Press Z to undo moves, Y to redo (when level allows) +- **Turn preview**: Press V to preview next-turn lighting (when level allows) +- **Progressive difficulty**: 12 levels across 6 acts, grids growing from 8×8 to 13×13 +- **Camera-follow viewport**: Large arenas scroll to keep the ninja in view +- **Procedural audio**: Web Audio API sound effects — moves, detection, throw, key pickup, door unlock, suspicion alerts +- **Pixel-art rendering**: All sprites and tiles drawn as string-art + palette → SVG rects +- **Solvability-verified**: BFS solver runs in CI to guarantee L1–L11 are beatable; L12 is intentionally unsolvable - **Bilingual**: English and Vietnamese language support -- **Lives system**: You have 3 lives to complete all levels +- **Accessibility**: ARIA labels on grid cells, HUD elements, and mechanic overlays ## Tech Stack @@ -45,65 +51,68 @@ In Night Ninja: Twilight Voyage, you play as a ninja rabbit trying to navigate t ## How to Play -- Use **arrow keys** or **WASD** to move your rabbit character one cell at a time +- Use **arrow keys** or **WASD** to move the rabbit one cell per turn - **Click** on an adjacent cell to move - Press **Space** to wait a turn without moving -- Press **V** to preview where lights will be next turn -- Press **Z** to undo your last move, **Y** to redo -- Reach the **green goal cell** to complete each level -- Avoid stepping on **yellow lit cells** or you'll be detected and lose a life -- Plan your moves carefully as each guard behaves differently: - - **Red guards** (Static): Always light the same cells - - **Blue guards** (Rotating): Change the direction they light each turn - - **Yellow guards** (Blinking): Turn their lights on and off each turn - - **Purple guards** (Patrolling): Move along a path, lighting cells around them - - **Green mirrors**: Deflect light beams 90 degrees - - **Orange chasers**: Detect and hunt you using pathfinding +- Press **V** to preview next-turn lighting (if level allows) +- Press **Z** to undo last move, **Y** to redo (if level allows) +- Press **E** to enter stone-throw targeting mode — arrow keys move cursor, **Enter/E** throws, **Esc** cancels +- Reach the **green goal cell** to complete the level +- Avoid **yellow lit cells** — detection restarts the level +- Guard behaviors: + - **Red (wilting)**: Aura shrinks each turn — waiting is valid + - **Blue (rotating)**: Direction indicator shows next aim — time your crossing + - **Yellow (blinking)**: Move on the dark turn + - **Purple (patrolling)**: Memorise the patrol path + - **Green (mirror)**: Predicts reflected beam landing spots + - **Orange (chaser)**: Stay out of detection radius or lure away + - **Dark red (sniper)**: Never step onto the aim line + - **Violet (suspicion)**: Don't cross their view — two crosses and they fire ## Available Commands | Command | Description | |---------|-------------| | `npm install` | Install project dependencies | -| `npm run dev` | Launch a development web server | -| `npm run build` | Create a production build in the `dist` folder | -| `npm run preview` | Preview the production build locally | +| `npm run dev` | Launch development server | +| `npm run build` | Production build in `dist/` | +| `npm run preview` | Preview production build locally | | `npm test` | Run all unit + solvability tests | -| `npm run test:solvability` | Run only the BFS level-solvability suite | +| `npm run test:solvability` | Run only the BFS solvability suite | ## Development -After cloning the repo, run `npm install` from your project directory. Then, you can start the local development server by running `npm run dev`. - -The local development server runs on `http://localhost:5173` by default. Vite will automatically recompile your code and reload the browser on changes. +After cloning, run `npm install` then `npm run dev`. Dev server at `http://localhost:5173` by default. ## Project Structure -| Path | Description | -|------------------------------|------------------------------------------------------------| -| `index.html` | HTML entry point | -| `public/assets` | Static assets + pixel-art authoring source (JSX canvas) | -| `src/main.js` | Application bootstrap | -| `src/App.svelte` | Scene router | -| `src/scenes/` | Game scenes (MainMenu, Game, Guide, Settings, etc.) | -| `src/components/` | Reusable UI components (GameBoard, PlayerSprite, etc.) | -| `src/lib/game/` | Pure JS game engine (grid, player, guards, turns, history, solver) | -| `src/lib/pixel/` | Pixel-art renderer + palette + sprite/tile/UI/scene art | -| `src/lib/levels/` | Level definitions (12 levels) + solvability test suite | -| `src/lib/` | Audio, localization, progress persistence | -| `src/styles/theme.css` | Global layout styles and CSS variables | +| Path | Description | +|------|-------------| +| `index.html` | HTML entry point | +| `src/main.js` | Application bootstrap | +| `src/App.svelte` | Scene router + migration modal | +| `src/scenes/` | Game scenes (MainMenu, Game, Guide, Settings, GameOver, etc.) | +| `src/components/` | Reusable UI components (GameBoard, HUD, sprites, overlays) | +| `src/lib/game/` | Pure JS engine (grid, player, guards, turns, history, solver, throwable) | +| `src/lib/pixel/` | Pixel-art renderer + palette + sprite/tile/UI/scene art | +| `src/lib/levels/` | 12 level definitions + BFS solvability test suite | +| `src/lib/locales/` | EN + VI JSON locale files | +| `src/lib/` | Audio (Web Audio), localization, progress persistence | +| `src/styles/theme.css` | CSS variables: colors, fonts, guard colors | ## Game Architecture -Pure JS game engine (no framework dependency) with Svelte 5 rendering: +Pure JS game engine with Svelte 5 rendering layer: -- **GridSystem**: Cell state management (walls, goals, lighting) -- **Player**: Position and movement validation -- **Guards**: 6 guard types with distinct AI (static, rotating, blinking, patrolling, mirror, chaser) -- **TurnManager**: Turn cycle with preview simulation -- **GameHistory**: Undo/redo via state snapshots +- **GridSystem**: Cell state management (walls, goals, lighting, doors, warm tiles) +- **Player**: Position, movement validation, key inventory (bitmask) +- **Guards**: 8 guard types — Static, Rotating, Blinking, Mirror, Patrolling, Chaser, SniperGuard, SuspicionGuard +- **ThrowableSystem**: Stone inventory, throw validation (Manhattan ≤3, line-of-sight, guard proximity), distraction turn logic +- **TurnManager**: Turn cycle with preview simulation and throwable integration +- **GameHistory**: Undo/redo via state snapshots (includes grid door state + throwable inventory) - **LevelSolver**: BFS solvability checker (CI-enforced; reuses runtime AI) -- **Audio**: Procedural Web Audio API sound effects +- **PrincessMechanic**: Escalating light-ring detection on L12 +- **Audio**: Procedural Web Audio — move, wait, detect, complete, undo, stone throw/impact, key pickup, door unlock, suspicion alert/fire ## Credits diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index c9bb8fc..0217c3a 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -10,7 +10,7 @@ NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The c - **src/main.js** — Mounts `App.svelte` into `#app` ### Scene Router -- **src/App.svelte** — `{#key currentScene}` with `transition:fade`, passes `navigate` function as prop to all scenes +- **src/App.svelte** — `{#key currentScene}` with `transition:fade`, passes `navigate` as prop; hosts migration modal (v2 progress-reset) shown once on first launch ### Scenes (src/scenes/) @@ -18,12 +18,12 @@ NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The c |------|---------| | MainMenu.svelte | Start game, level select, settings, guide | | StoryIntro.svelte | Scrolling narrative with skip button | -| LevelIntro.svelte | Level name + story text + continue | -| LevelSelect.svelte | 4x3 grid of level buttons (locked/unlocked/completed) | -| Game.svelte | Main gameplay: state owner, input, turn loop, rendering | -| GameOver.svelte | Loss/twist screen, retry/menu | +| LevelIntro.svelte | Level name + story/foreshadowing text + continue | +| LevelSelect.svelte | 4×3 grid of level buttons (locked/unlocked/completed) | +| Game.svelte | Main gameplay: state owner, input, turn loop, rendering, audio wiring | +| GameOver.svelte | Run-complete (L11) or bittersweet (L12) end screen | | Settings.svelte | Language toggle (EN/VI) | -| Guide.svelte | Rules, controls, enemy types | +| Guide.svelte | Rules, controls, all enemy type descriptions | ### Components (src/components/) @@ -31,11 +31,16 @@ NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The c |------|---------| | Button.svelte | Reusable styled button | | GameBoard.svelte | CSS grid of cells, each rendering a pixel-art tile | -| GameHud.svelte | Level, lives (pixel hearts), turns, pixel-icon action buttons | -| PlayerSprite.svelte | Pixel-art ninja rabbit sprite | -| GuardSprite.svelte | Pixel-art veggie sprite dispatched by guard.type + direction indicator | -| DetectionPopup.svelte | "Detected!" overlay with retry | -| LevelCompletePopup.svelte | Star rating, best moves, next-level action | +| GameHud.svelte | Level, turns, pixel-icon action buttons; conditionally mounts StonesCounter + KeyInventory + AffordanceBanner | +| PlayerSprite.svelte | Pixel-art ninja rabbit sprite, absolute-positioned over board | +| GuardSprite.svelte | Pixel-art veggie sprite dispatched by guard.type; sniper/suspicion use tier-state palettes | +| StonesCounter.svelte | HUD pill — stone count with pixel-art rock icon; only mounted when level has stones | +| KeyInventory.svelte | HUD row — collected key chips with pixel-art key icons (gold/silver/copper) | +| SuspicionRing.svelte | Visual overlay showing suspicion guard alert radius | +| ThrowTargetingOverlay.svelte | Full-board overlay in throw-targeting mode; green valid / red invalid halos; localized hint bar | +| AffordanceBanner.svelte | Stacked banners when level disables undo and/or preview; uses `getText` for locale copy | +| DetectionPopup.svelte | "Detected!" overlay with retry (restarts current level) | +| LevelCompletePopup.svelte | Star rating, best moves vs par, next-level action | | PauseMenu.svelte | Resume / restart / main menu | | ControlsOverlay.svelte | Keyboard / tap / swipe reference overlay | @@ -43,31 +48,32 @@ NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The c | File | Purpose | |------|---------| -| grid-system.js | GridSystem class: cell state, walls, goals, lighting | -| player.js | Player class: position, movement validation | -| guards.js | Guard base + 6 subclasses (Static, Rotating, Blinking, Mirror, Patrolling, Chaser) | -| turn-manager.js | TurnManager: turn cycle, guard updates, detection | -| level-manager.js | loadLevel(): GUARD_REGISTRY factory pattern for guard instantiation | +| grid-system.js | GridSystem class: cell state (walls, goals, lighting, doors, warm tiles) | +| player.js | Player class: position, movement validation, key inventory (bitmask `keysHeld`), `getKeysHeld()` | +| guards.js | Guard base + 8 subclasses (Static, Rotating, Blinking, Mirror, Patrolling, Chaser, SniperGuard, SuspicionGuard) | +| throwable.js | ThrowableSystem: stone inventory, throw validation (Manhattan ≤3, LoS via Bresenham, guard proximity), distraction logic | +| turn-manager.js | TurnManager: turn cycle, guard updates, throwable integration, detection check | +| level-manager.js | loadLevel(): GUARD_REGISTRY factory pattern for all 8 guard types; reads doors/keys/oneWays/decayTiles/affordances/stones from level data | | level-solver.js | BFS solvability checker (test-only); reuses runtime AI via capture()/apply() | -| game-history.js | GameHistory class: undo/redo snapshots (Z/Y keys) | -| princess-mechanic.js | Princess detection logic: escalating light rings on level 12 | -| touch-controls.js | TouchControls class: pointer gesture detection (legacy; no mobile support claim) | +| game-history.js | GameHistory: undo/redo snapshots including grid door state + throwSystem stone count | +| princess-mechanic.js | Princess detection logic: escalating light rings on L12 | +| touch-controls.js | TouchControls: pointer gesture detection | ### Level Data (src/lib/levels/) | File | Purpose | |------|---------| -| levels.js | LEVELS array: 12 level definitions (grid, guards, walls, goals); L1-L11 solvable, L12 intentionally unsolvable | -| levels.solvability.test.js | CI-enforced invariants: each L1-L11 solvable, L12 unsolvable, no wall/light overlaps, exactly 12 levels | +| levels.js | LEVELS array: 12 level definitions; extended shape with doors, keys, oneWays, decayTiles, affordances, stones | +| levels.solvability.test.js | CI-enforced: each L1–L11 solvable, L12 unsolvable, no wall/light overlaps, exactly 12 levels | ### Pixel-Art Pipeline (src/lib/pixel/) | File | Purpose | |------|---------| -| Pixel.svelte | SVG renderer: string-art + palette → run-length-merged ``s | -| palette.js | NNTV color constants (mirrors theme.css guard colors; extended atmosphere palette) | -| art-characters.js | 32×32 sprites: rabbit, princess, 6 guard veggies + GUARD_SPRITES map | -| art-tiles.js | 16×16 board tiles: empty, wall, goal, lit, mirror, preview | +| Pixel.svelte | SVG renderer: string-art + palette → run-length-merged ``s; `scale`, `bg`, `pixelated` props | +| palette.js | NNTV color constants (mirrors theme.css guard colors; extended atmosphere + veggie + tile tones) | +| art-characters.js | 32×32 sprites: rabbit, princess, 8 guard veggies; SNIPER_ART + SUSPICION_CALM/ALERT/FIRE variants; GUARD_SPRITES map | +| art-tiles.js | 16×16 board tiles: empty, wall, goal, lit, mirror, preview; **new**: TILE_WARM, TILE_DOOR_* (locked/open × 3 colors), TILE_KEY_* (3 colors), TILE_ONEWAY_RIGHT, ICON_STONE | | art-ui.js | Heart (full/empty), moon, logo, pixel icons (undo/redo/eye/pause/settings/lang/arrow) | | art-scenes.js | 80×N act backdrops + SCENE_BY_LEVEL mapping (garden/walls/fortress/underground/palace/chamber) | @@ -75,21 +81,21 @@ NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The c | File | Purpose | |------|---------| -| audio.js | Web Audio API procedural sound: playTone, playMoveSound, playDetectionSound, playCompleteSound, toggleMute | +| audio.js | Web Audio API procedural sound: move, wait, detect, complete, click, undo; **new**: playStoneThrow, playStoneImpact, playKeyPickup, playDoorUnlock, playSuspicionAlert, playSuspicionFire | ### Utilities (src/lib/) | File | Purpose | |------|---------| -| localization.js | getText/setLanguage/getLanguage/initLanguage | -| progress.js | getProgress/completeLevel via localStorage | +| localization.js | getText / setLanguage / getLanguage / initLanguage | +| progress.js | getProgress / completeLevel via localStorage; version field for migration detection | ### Localization (src/lib/locales/) | File | Keys | Purpose | |------|------|---------| -| en.json | ~55 | English translations | -| vi.json | ~55 | Vietnamese translations | +| en.json | ~75 | English translations — includes mechanics.*, banner.*, migration.*, throw.*, gameOver.*, level intros | +| vi.json | ~75 | Vietnamese translations — full parity with EN; proper nouns localised (e.g. "Ớt Bắn Tỉa", "Hành Tím Nghi Ngờ") | ### Styles (src/styles/) @@ -103,72 +109,95 @@ NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The c ``` Guard (abstract base: grid, row, col, type, direction, isOn) -├── StaticGuard — wilting tomato: Manhattan aura shrinks by 1 per turn (initialRadius → currentRadius) -├── RotatingGuard — rotates beam 90°/turn, castBeam with mirror bounce -├── BlinkingGuard — toggles isOn, lights litCells when on -├── MirrorGuard — lights own cell, stores reflectDirection (cw/ccw) -├── PatrollingGuard — follows path array, lights front + right cells -└── ChaserGuard — BFS pathfinding to player, detectionRadius range +├── StaticGuard — wilting tomato: Manhattan aura shrinks by 1/turn +├── RotatingGuard — rotates beam 90°/turn, castBeam with mirror bounce +├── BlinkingGuard — toggles isOn, lights litCells when on +├── MirrorGuard — lights own cell, stores reflectDirection (cw/ccw) +├── PatrollingGuard — follows path array, lights front + right cells +├── ChaserGuard — BFS pathfinding to player, detectionRadius range +├── SniperGuard — cardinal aim line; instant detection on aim-line crossing +└── SuspicionGuard — tier-based alert (0=calm, 1=alerted, 2=firing); field-of-view zone ``` ## Key Data Structures -### Level Definition +### Level Definition (v2 shape) ```javascript { id: 1, name: "Garden Path", storyKey: "level1Story", - grid: { rows: 6, cols: 6 }, + grid: { rows: 8, cols: 8 }, player: { row: 0, col: 0 }, - goal: { row: 5, col: 5 }, - walls: [{ row: 1, col: 1 }, ...], + goal: { row: 7, col: 7 }, + walls: [{ row, col }, ...], guards: [ - { type: "static", position: { row: 2, col: 4 }, initialRadius: 2 }, - { type: "rotating", position: { row: 3, col: 3 }, startDirection: 0 }, - { type: "blinking", position: {...}, litCells: [...], startState: true }, - { type: "mirror", position: {...}, reflectDirection: "cw" }, - { type: "patrolling", startPosition: {...}, path: [...] }, - { type: "chaser", position: {...}, detectionRadius: 3 }, + { type: "static", position: { row, col }, initialRadius: 2 }, + { type: "rotating", position: { row, col }, startDirection: 0 }, + { type: "blinking", position: { row, col }, litCells: [...], startState: true }, + { type: "mirror", position: { row, col }, reflectDirection: "cw" }, + { type: "patrolling", startPosition: { row, col }, path: [...] }, + { type: "chaser", position: { row, col }, detectionRadius: 3 }, + { type: "sniper", position: { row, col }, direction: 1 }, + { type: "suspicion", position: { row, col }, viewZone: [...] }, ], - isFinalLevel: false + // v2 extensions: + oneWays: [{ row, col, dir: 0|1|2|3 }], // 0=up,1=right,2=down,3=left + doors: [{ row, col, keyId: 1|2|3 }], + keys: [{ row, col, keyId: 1|2|3 }], + decayTiles: [{ row, col }], + stones: 3, // throwable stone count (0 = no throws) + affordances: { undo: true, preview: true }, // false = gate disabled + parMoves: 22, + isFinalLevel: false, } ``` -### Cell State +### Cell State (v2) ```javascript -{ isWall: boolean, isGoal: boolean, isLight: boolean } +{ isWall: boolean, isGoal: boolean, isLight: boolean, isWarm: boolean, + doorKeyId: number|null, isOneWay: boolean, oneWayDir: number|null } ``` ### Progress (localStorage) ```javascript -{ maxLevel: 1, completedLevels: [1, 2, 3] } +{ maxLevel: 1, completedLevels: [1, 2], version: 2 } ``` ## Public API Summary ### GridSystem ``` -new GridSystem(rows, cols, cellSize) -.isValidPosition(row, col), .isWall(row, col), .setWall(row, col, value) -.isGoal(row, col), .setGoal(row, col, value) -.isLight(row, col), .setLight(row, col, value) -.clearAllLight(), .getAllCells() → [{row, col, isWall, isGoal, isLight}] +new GridSystem(rows, cols) +.isValidPosition(r,c), .isWall(r,c), .setWall(r,c,v) +.isGoal(r,c), .setGoal(r,c,v) +.isLight(r,c), .setLight(r,c,v) +.isWarm(r,c), .setWarm(r,c,v) +.clearAllLight(), .getAllCells() → [{row,col,isWall,isGoal,isLight,isWarm,...}] ``` ### Player ``` new Player(grid, row, col) -.move(direction) → boolean, .moveTo(row, col) → boolean +.move(direction) → boolean .isInLitCell() → boolean, .isAtGoal() → boolean +.getKeysHeld() → number // bitmask +.addKey(keyId), .hasKey(keyId) → boolean +``` + +### ThrowableSystem +``` +new ThrowableSystem(stones) +.stonesLeft → number +.throw(targetRow, targetCol, player, grid) → boolean +.hasLineOfSight(r0,c0,r1,c1,grid) → boolean +.reset(stones) ``` ### Guards ``` All: .updateLight(allGuards?), .onTurnChange(allGuards?) -All: .capture() → state, .apply(state) # dynamic-state snapshot for undo/preview -RotatingGuard: .castBeam(dir, fromRow, fromCol, range, allGuards, depth) -PatrollingGuard: .checkIfCircularPath(), path traversal with reversing -MirrorGuard: .reflectDirection ('cw' or 'ccw') -ChaserGuard: .bfsNextStep(targetRow, targetCol), hunting/returning state +All: .capture() → state, .apply(state) +SniperGuard: .getAimLine() → [{row,col}] +SuspicionGuard: .suspicionTier (0|1|2), .updateSuspicion(playerRow,playerCol) ``` ### PrincessMechanic @@ -177,13 +206,12 @@ new PrincessMechanic() .update(grid, player, goalRow, goalCol) → { showMessage, detected } .lightRing(grid, goalRow, goalCol, radius) .capture() → { alerted, alertRadius, messageShown }, .apply(state) -.reset() ``` ### GameHistory ``` new GameHistory() -.createSnapshot(player, guards, turnCount, princessState) → snapshot +.createSnapshot(player, guards, turnCount, princessState, grid, throwSystem) → snapshot .pushSnapshot(snap), .undo(...), .redo(...) .canUndo(), .canRedo(), .reset() ``` @@ -191,49 +219,66 @@ new GameHistory() ### TurnManager ``` new TurnManager() -.nextTurn(grid, player, guards) → { detected, levelComplete } +.nextTurn(grid, player, guards, throwSystem?) → { detected, levelComplete } +.previewNextTurn(grid, player, guards, throwSystem?) → Set .reset() ``` -## Dependencies +### Audio (src/lib/audio.js) +``` +playMove(), playWait(), playDetection(), playLevelComplete(), playClick(), playUndo() +playStoneThrow(), playStoneImpact() +playKeyPickup(), playDoorUnlock() +playSuspicionAlert(), playSuspicionFire() +setMuted(bool), isMuted() → bool +``` -| Package | Version | Purpose | -|---------|---------|---------| -| svelte | 5.x | UI framework (runes mode) | -| vite | 6.3.6 | Build tool, dev server | -| @sveltejs/vite-plugin-svelte | 6.x | Svelte compiler for Vite | +## Audio Wiring (Game.svelte) + +| Event | Trigger | Functions | +|-------|---------|-----------| +| Player move | `player.move()` succeeds | `playMove()` | +| Player wait | Space key | `playWait()` | +| Detection | `result.detected` | `playDetection()` | +| Level complete | `result.levelComplete` | `playLevelComplete()` | +| Undo | Z key | `playUndo()` | +| Stone throw | `confirmThrow()` → ok | `playStoneThrow()` + `playStoneImpact()` (80ms delay) | +| Key pickup | `keysHeld` delta in `$effect` | `playKeyPickup()` | +| Suspicion tier 1 | `maxTier` delta 0→1 in `$effect` | `playSuspicionAlert()` | +| Suspicion tier 2 | `maxTier` delta 1→2 in `$effect` | `playSuspicionFire()` | ## File Dependency Map ``` main.js → App.svelte -App.svelte → all scenes +App.svelte → all scenes + migration modal Game.svelte (central hub) - ├── lib/game/level-manager.js → grid-system, player, guards, levels + ├── lib/game/level-manager.js → grid-system, player, guards, throwable, levels ├── lib/game/turn-manager.js ├── lib/game/game-history.js, princess-mechanic.js, touch-controls.js - ├── lib/audio.js, lib/progress.js, lib/localization.js - ├── lib/pixel/Pixel.svelte, lib/pixel/art-scenes.js (act backdrop) + ├── lib/audio.js (12 exported cue functions) + ├── lib/progress.js, lib/localization.js + ├── lib/pixel/Pixel.svelte, lib/pixel/art-scenes.js ├── components/GameBoard, PlayerSprite, GuardSprite, GameHud + ├── components/ThrowTargetingOverlay, StonesCounter, KeyInventory ├── components/DetectionPopup, LevelCompletePopup, PauseMenu, ControlsOverlay └── renderVersion pattern for reactivity -LevelSelect.svelte → level-manager.getTotalLevels(), progress.js, localization.js -LevelIntro.svelte → levels.js, lib/pixel/art-scenes.js, localization.js -MainMenu.svelte → lib/pixel/{Pixel, art-ui, art-characters} -Pixel components (PlayerSprite/GuardSprite/GameBoard/GameHud) → lib/pixel/Pixel + art-* -All scenes → localization.js (for UI text) +GameHud.svelte → StonesCounter, KeyInventory, AffordanceBanner (conditional) +GuardSprite.svelte → art-characters (GUARD_SPRITES + sniper/suspicion palettes) +GameBoard.svelte → art-tiles (all tile types including TILE_WARM, TILE_DOOR_*, TILE_KEY_*, TILE_ONEWAY_*) ``` ## Statistics | Metric | Value | |--------|-------| -| Total Source Files | ~36 (8 scenes + 9 components + 8 engine + 6 pixel + audio + utils) | -| Number of Classes | 10 (GridSystem, Player, Guard + 6 subclasses, TurnManager, GameHistory, TouchControls) | +| Total Source Files | ~42 (8 scenes + 12 components + 9 engine + 6 pixel + audio + utils) | +| Guard Types | 8 (Static, Rotating, Blinking, Mirror, Patrolling, Chaser, Sniper, Suspicion) | | Number of Levels | 12 (across 6 acts) | -| Guard Types | 6 (Static, Rotating, Blinking, Mirror, Patrolling, Chaser) | -| Localization Keys | ~67 per language | -| Max Grid Size | 10x10 | -| Max Guards/Level | 8 | +| Locale Keys per Language | ~75 | +| Max Grid Size | 13×13 | +| Max Guards per Level | 10 | +| Audio Cue Functions | 12 | +| Pixel-Art Sprite Variants | 20+ (characters + tiles + UI icons) | diff --git a/docs/game-design.md b/docs/game-design.md index d8d2732..35fc3b4 100644 --- a/docs/game-design.md +++ b/docs/game-design.md @@ -15,168 +15,240 @@ The Carrot Princess, daughter of King Carrot III, has vanished from the royal pa You infiltrate the kingdom's outskirts, fight through gardens, fortress walls, underground passages, and the royal palace. At last, you reach the Princess Chamber. But as you approach, the princess senses your presence. Light radiates outward from her in an unstoppable wave. **The rescue was never possible.** The game ends: *"The Carrot Princess remains beyond your reach. Perhaps some rescues were never meant to be..."* -### Foreshadowing (Levels 10-12) -- **Level 10**: *"They say the Princess can sense any living thing nearby..."* -- **Level 11**: *"No one who entered the chamber beyond has returned. Not because of guards..."* -- **Level 12**: *"The air itself feels watchful... she already knows you're here."* +### Foreshadowing Arc (Levels 10–12) +- **Level 10**: *"They say the Princess can sense any living thing nearby — even ghosts of past attempts..."* +- **Level 11**: *"No one who entered the chamber beyond has returned. The throne itself was a warning."* +- **Level 12**: *"The air itself feels watchful. She already knows you're here."* ## Core Mechanics ### Turn System -1. Player moves one cell (arrow keys / WASD / click adjacent cell) +1. Player moves one cell (arrow keys / WASD / click adjacent cell) — or waits (Space) 2. All guards execute their turn action simultaneously 3. Lighting recalculates -4. Detection check: player on lit cell = lose 1 life, restart level -5. Goal check (before guards): reach green cell = level complete +4. Detection check: player on lit cell → restart current level +5. Goal check (before guards): reach green cell → level complete -### Lives -- 3 lives per playthrough (shared across all levels) -- Lose a life on detection → restart current level -- 0 lives = game over +### Death Model +Detection restarts the current level only. There are no lives or shared health across levels. Each level is an isolated puzzle run. ### Grid - Cells: empty (dark/safe), wall (impassable), goal (green), lit (yellow/dangerous) - Player starts at top-left (0,0), goal at bottom-right corner -- Grid sizes: 8x8 → 13x13 as difficulty increases (camera-follow viewport on large arenas) +- Grid sizes: 8×8 → 13×13 as difficulty increases (camera-follow viewport on large arenas) + +### Affordance Gates (per-level) +Some levels disable **undo** and/or **preview** to raise stakes. A banner notifies the player at level start. ## Guard Types -| Type | Color | Shape | Behavior | -|------|-------|-------|----------| -| Static (Wilting Tomato) | Red | Circle | Emits a Manhattan aura of initialRadius cells; shrinks by 1 per turn until harmless. Lit cells = death. Reward patience — wait out the burn. | -| Rotating | Blue | Circle + direction line | Rotates beam 90° clockwise each turn; 2-cell range; stopped by walls; reflects off mirrors | -| Blinking | Yellow | Circle (dims when off) | Toggles lights on/off each turn; lights fixed cells when "on" | -| Patrolling | Purple | Circle + direction line | Moves along predefined path; lights front + right cells relative to facing | -| Mirror | Green | Diamond (rotated square) | Stationary; redirects rotating guard beams 90° (configurable cw/ccw); max 3 bounces | -| Chaser | Orange | Circle + direction line | BFS pathfinding toward player when within detectionRadius; returns home otherwise | +| Type | Color | Veggie | Behavior | +|------|-------|--------|----------| +| Static (Wilting) | Red | Tomato | Manhattan aura shrinks by 1 per turn until harmless. Wait or detour. | +| Rotating | Blue | Blueberry | Rotates beam 90° clockwise each turn; 2-cell range; stopped by walls; reflects off mirrors. | +| Blinking | Yellow | Corn | Toggles lights on/off each turn; lights fixed cells when "on". | +| Patrolling | Purple | Eggplant | Moves along predefined path; lights front + right cells relative to facing. | +| Mirror | Green | Lettuce | Stationary; redirects rotating beams 90° (cw/ccw); max 3 bounces. | +| Chaser | Orange | Pumpkin | BFS pathfinding toward player when within detectionRadius; returns home otherwise. | +| Sniper | Dark Red | Pepper | Aims along a fixed cardinal line of sight. Instant detection if player steps into the aim line regardless of turn timing. | +| Suspicion | Violet | Onion | Three-tier alert: 0=calm, 1=alerted (suspicion ring visible, slower response), 2=firing (instant detection). Tier rises when player crosses field of view; resets over turns if player exits zone. | ### Guard Rules - Guards light their own cell (visible to player) - All guards act after player moves (simultaneous) - Rotating beam stops at walls and grid boundaries -- Mirror only affects rotating guard beams (not static/blinking lit cells) +- Mirror only affects rotating guard beams - Patrolling guards reverse direction at path endpoints (or loop if circular path) +- Sniper aim line is cardinal only (up/down/left/right from guard position) +- Suspicion guard tier resets one step per turn while player is outside view zone -## Level Design Requirements +## New Mechanics (v2) -**Exactly 12 levels. Levels 1-11 MUST be solvable. Level 12 MUST NOT be solvable.** +### Throwable Stones +- Each level grants a fixed stone count (defined in `level.stones`) +- Press **E** to enter targeting mode; arrow keys move cursor over the grid +- Valid targets: Manhattan distance ≤ 3 from player, clear line-of-sight, at least one distractible guard (rotating/patrolling/chaser) within distance 2 of target +- Throw uses one turn; distractable guards in range change direction/behavior for that turn +- HUD shows remaining stones; stone icon is a pixel-art grey rock + +### Doors and Keys +- Keys appear as colored items on the grid floor (gold/silver/copper for IDs 1/2/3) +- Player auto-collects a key by stepping onto its cell +- Doors block movement until the matching key is held (bitmask in `player.keysHeld`) +- Door and key sprites are color-coded identically — matching is visual +- HUD shows collected keys as pixel-art chips + +### One-Way Tiles +- Arrow tiles only allow entry from the designated cardinal direction +- Attempting to enter from any other direction is blocked silently +- Creates commitment ratchets — plan the full route before crossing + +### Decay / Warm Tiles +- A warm cell is temporarily dangerous (flagged `isWarm`); after one turn it cools to safe +- Visually distinct from lit cells: dim orange glow vs bright yellow +- Used to create timed hazard zones without persistent guards + +## Level Design + +**Exactly 12 levels. Levels 1–11 MUST be solvable. Level 12 MUST NOT be solvable.** ### Act Structure | Act | Levels | Theme | Grid | Mechanic Focus | |-----|--------|-------|------|----------------| -| 1: The Outskirts | 1-2 | Garden | 8x8 | Movement + static guards | -| 2: The Vegetable Garden | 3-4 | Garden/Walls | 9x9 | First rotating + blinking guards | -| 3: The Fortress | 5-6 | Fortress | 10x10 | First patrolling guard, timing + movement | -| 4: The Underground | 7-8 | Tunnels | 11x11 | First mirror guard, beam redirection | -| 5: The Royal Palace | 9-10 | Palace | 12x12 | First chaser guard, perimeter vs ambush | -| 6: The Princess Chamber | 11-12 | Palace/Final | 12x12, 13x13 | All types combined; L12 unsolvable | +| 1: The Outskirts | 1–2 | Garden | 8×8 | Movement + wilting static guards | +| 2: The Vegetable Garden | 3–4 | Garden/Walls | 9×9 | One-way tiles + rotating + blinking | +| 3: The Fortress | 5–6 | Fortress | 10×10 | Patrolling guards, timing, parity | +| 4: The Underground | 7–8 | Tunnels | 11×11 | Mirror guards, beam reflection mastery | +| 5: The Royal Palace | 9–10 | Palace | 11×12 | Chaser guards, stones, doors + keys | +| 6: The Princess Chamber | 11–12 | Palace/Final | 11×13 | All types + sniper/suspicion; L12 unsolvable | ### Level-by-Level Specification -**Level 1 — Garden Path** (8x8, 0 guards) -- Tutorial: movement only. Walls route the player through a winding corridor. No danger. +**Level 1 — Garden Path** (8×8, 0 guards) +Tutorial: movement only. Three wall bands route the player through a winding corridor. No danger. -**Level 2 — The Watchtower** (8x8, 3 wilting tomatoes on the zigzag detour) -- First guard encounter. Three wilting tomatoes (initialRadius 2) each emit a Manhattan aura that shrinks by 1 per turn until harmless. -- Teaches: lit cells = danger, but they fade. Wait or detour — both are valid tactics. +**Level 2 — The Watchtower** (8×8, 3 wilting tomatoes) +Three wilting tomatoes (initialRadius 2) on the zigzag detour. Auras shrink by 1/turn. +Teaches: lit cells = danger, but they fade. Wait or detour. -**Level 3 — Vegetable Patrol** (9x9, 3 wilting tomatoes + 1 rotating) -- Rotating guard introduced in the middle of the map; wilting tomatoes flank the edges. -- Teaches: rotating beam cycles every 4 turns, read direction indicator. +**Level 3 — Vegetable Patrol** (9×9, one-way tiles + static + rotating) +One-way tiles introduced. Two one-way gates between map zones. Two static guards flank edges; one rotating guard in middle. +Teaches: commitment ratchets — plan route before crossing a one-way. -**Level 4 — The Searchlight** (9x9, rotating + blinking + 3 static) -- Blinking guard introduced. Solution requires at least one `wait` action. -- Teaches: counting turns, parity-based movement. +**Level 4 — The Searchlight** (9×9, rotating + blinking + 3 static) +Blinking guard introduced. Solution requires at least one `wait` action. +Teaches: counting turns, parity-based movement. -**Level 5 — Fortress Gate** (10x10, static + rotating + blinking + static + static + patrolling) -- Patrolling guard introduced on a short circular path. -- Teaches: predicting patrol path, front+right light pattern. +**Level 5 — Fortress Gate** (10×10, static + rotating + blinking + patrolling) +Patrolling guard introduced on a short circular path. +Teaches: predicting patrol path, front+right light pattern. -**Level 6 — The Flickering Corridor** (10x10, 2 blinking + rotating + 2 patrolling + static + static) -- Two patrollers with interlocking routes. -- Teaches: weaving between multiple moving threats. +**Level 6 — The Flickering Corridor** (10×10, 2 blinking + rotating + 2 patrolling + static × 2) +Two patrollers with interlocking routes. +Teaches: weaving between multiple moving threats. -**Level 7 — The Underground Passage** (11x11, rotating + mirror + blinking + patrolling + 3 static) -- Mirror guard introduced; the rotating beam deflects off it. -- Teaches: predicting reflected beam paths. +**Level 7 — The Underground Passage** (11×11, rotating + mirror + blinking + patrolling + 3 static) +Mirror guard introduced; rotating beam deflects off it. +Teaches: predicting reflected beam paths. -**Level 8 — The Gauntlet** (11x11, 2 rotating + 2 mirrors + 2 patrolling + blinking + static) -- Two mirrors form a crossfire pattern. -- Teaches: mastery of reflected beams in open arenas. +**Level 8 — The Gauntlet** (11×11, 2 rotating + 2 mirrors + 2 patrolling + blinking + static) +Two mirrors form a crossfire pattern. +Teaches: mastery of reflected beams in open arenas. -**Level 9 — The Decoy Path** (12x12, chaser + rotating + 2 blinking + 2 static + patrolling + chaser-less guard) -- Chaser guard introduced, ambushing the central shortcut. -- Teaches: lure-and-retreat; perimeter path beats the tempting short route. +**Level 9 — The Decoy Path** (12×12, chaser + rotating + 2 blinking + 2 static + patrolling) +Chaser guard introduced, ambushing the central shortcut. Stones introduced. +Teaches: lure-and-retreat; throwable distraction; perimeter path beats the tempting short route. -**Level 10 — Hall of Mirrors** (12x12, 2 rotating + 3 mirrors + blinking + static + patrolling + chaser) -- Mirrors create a crossfire with a hunting chaser. -- Teaches: navigating reflected beams while evading a pursuer. +**Level 10 — Hall of Mirrors** (12×12, 2 rotating + 3 mirrors + blinking + static + patrolling + chaser) +Mirrors create a crossfire with a hunting chaser. Doors + keys introduced. +Teaches: navigating reflected beams while evading a pursuer; key routing. -**Level 11 — The Throne Room** (12x12, ALL 6 TYPES, 9 guards total) -- Climax level: at least one of each guard type. -- Teaches: mastery of all mechanics simultaneously. +**Level 11 — The Throne Room** (11×11, ALL TYPES, 7 guards total) +Climax: at least one of each guard type including sniper and suspicion. +Teaches: mastery of all mechanics simultaneously. -**Level 12 — The Princess Chamber** (13x13, 10 guards + expanding wave) -- **UNSOLVABLE BY DESIGN.** See memory `project_level12_unsolvable.md`. -- **Hidden mechanic**: When player reaches Manhattan distance ≤ 4 from goal, the princess "senses" the player. Light radiates outward from the goal one ring per turn. The expanding wave is unstoppable. -- **Easter egg**: `window.__nntvDev.teleport(12, 12)` from the browser console teleports the player to the goal and fires the normal win flow. Hidden from README and in-game UI. -- **Narrative payoff**: *"The Carrot Princess senses your presence! A wave of light radiates outward... Run!"* Then detection → game over screen with: *"Perhaps some rescues were never meant to be..."* +**Level 12 — The Princess Chamber** (13×13, 10 guards + expanding princess wave) +**UNSOLVABLE BY DESIGN.** See `project_level12_unsolvable.md` in memory. +- When player reaches Manhattan distance ≤ 4 from goal, the princess "senses" the player +- Light radiates outward from the goal one ring per turn — unstoppable +- **Easter egg**: `window.__nntvDev.teleport(12, 12)` triggers the normal win flow (undocumented) +- **Narrative payoff**: *"The Carrot Princess senses your presence! A wave of light radiates outward... Run!"* → detection → bittersweet game-over ### Level Design Constraints -- Player always starts at (0, 0) -- Goal always at (rows-1, cols-1) — bottom-right corner +- Player always starts at (0, 0); goal always at (rows-1, cols-1) - Max 10 guards per level -- Every level 1-11 must have at least one valid path from start to goal (solver-verified) -- No guard's computed lit cells may overlap a wall cell (solver lints blinking-type `litCells`; static-type wilting auras naturally pass through walls but are set-but-never-checked on wall cells) -- Walls should create interesting routing decisions, not dead ends +- Every level 1–11 must have at least one valid path (BFS solver-verified in CI) +- Walls create routing decisions, not dead ends - Each new mechanic gets a solo introduction level before combining - All `levels.js` edits must pass `npm run test:solvability` ## Visual Design -Minimalist geometric art — no sprites or animations required. +Pixel-art sprites (string-art + palette → SVG rects via `Pixel.svelte`). No raster image files. -| Element | Visual | CSS Variable | -|---------|--------|-------------| -| Empty cell | Dark blue-gray square | `--grid-empty` | -| Wall | Gray square | `--grid-wall` | -| Goal | Green square | `--grid-goal` | -| Lit cell | Yellow square | `--grid-lit` | -| Player | Black circle | `--player-color` | -| Static guard | Red circle | `--guard-static` | -| Rotating guard | Blue circle + direction line | `--guard-rotating` | -| Blinking guard | Yellow circle (dims when off) | `--guard-blinking` | -| Patrolling guard | Purple circle + direction line | `--guard-patrolling` | -| Mirror guard | Green diamond (rotated square) | `--guard-mirror` | +| Element | Visual | Source | +|---------|--------|--------| +| Empty cell | Dark blue-gray tile | `art-tiles.js TILE_EMPTY` | +| Wall | Stone brick tile | `art-tiles.js TILE_WALL` | +| Goal | Green tile | `art-tiles.js TILE_GOAL` | +| Lit cell | Bright yellow tile | `art-tiles.js TILE_LIT` | +| Warm cell | Dim orange glow tile | `art-tiles.js TILE_WARM` | +| One-way tile | Directional arrow tile | `art-tiles.js TILE_ONEWAY_RIGHT` (rotated) | +| Door (locked) | Color-coded door tile | `art-tiles.js TILE_DOOR_*_PAL` | +| Door (open) | Open doorway tile | `art-tiles.js TILE_DOOR_OPEN` | +| Key item | Color-coded key tile | `art-tiles.js TILE_KEY_*_PAL` | +| Player | Ninja rabbit sprite | `art-characters.js RABBIT_ART` | +| Static guard | Wilting tomato | `art-characters.js TOMATO_ART` | +| Rotating guard | Blueberry | `art-characters.js BLUEBERRY_ART` | +| Blinking guard | Corn | `art-characters.js CORN_ART` | +| Patrolling guard | Eggplant | `art-characters.js EGGPLANT_ART` | +| Mirror guard | Lettuce | `art-characters.js LETTUCE_ART` | +| Chaser guard | Pumpkin | `art-characters.js PUMPKIN_ART` | +| Sniper guard | Dark-red pepper | `art-characters.js SNIPER_ART` | +| Suspicion guard | Violet onion (3 tier states) | `art-characters.js SUSPICION_CALM_ART` | +| Stone HUD icon | Grey rock pixel | `art-tiles.js ICON_STONE` | ## Controls | Input | Action | |-------|--------| -| Arrow keys | Move up/down/left/right | -| W/A/S/D | Alternative movement | -| Click adjacent cell | Move to that cell | -| Escape / Pause button | Pause menu | +| Arrow keys / WASD | Move | +| Space | Wait one turn | +| V | Toggle next-turn preview (if allowed) | +| Z | Undo last move (if allowed) | +| Y | Redo move (if allowed) | +| E | Enter / confirm stone-throw targeting | +| Esc | Cancel throw targeting | +| Click adjacent cell | Move | +| Swipe | Move (mobile) | ## UI Screens 1. **Main Menu**: Start Game, Level Select, Settings, Guide -2. **Story Intro**: Scrolling text narrative (skippable) -3. **Level Intro**: Level name + story text + Continue button -4. **Game**: Board + HUD (level, lives, turns) + pause -5. **Detection Popup**: "Detected!" + Play Again button -6. **Pause Menu**: Resume / Restart / Main Menu -7. **Game Over**: Retry or Main Menu (special message for level 12) -8. **Level Select**: 4x3 grid, locked/unlocked/completed states -9. **Settings**: Language toggle (EN/VI) -10. **Guide**: Objectives, controls, enemy type descriptions +2. **Migration Modal**: Shown once on v2 first launch (progress reset notice) +3. **Story Intro**: Scrolling text narrative (skippable) +4. **Level Intro**: Level name + story/foreshadowing text + Continue button +5. **Game**: Board + HUD (level, turns, stones, keys, affordance banners) + pause +6. **Detection Popup**: "Detected!" + Play Again button (restarts current level) +7. **Pause Menu**: Resume / Restart / Main Menu +8. **Level Complete Popup**: Star rating, move count vs par, Next button +9. **Game Over**: Run-complete celebration (L11) or bittersweet end (L12) +10. **Level Select**: 4×3 grid, locked/unlocked/completed states +11. **Settings**: Language toggle (EN/VI) +12. **Guide**: Objectives, controls, all enemy type descriptions + +## Audio (Procedural Web Audio API) + +| Event | Function | Character | +|-------|----------|-----------| +| Player move | `playMove` | Soft sine 220 Hz, 60ms | +| Player wait | `playWait` | Quiet tick 160 Hz, 40ms | +| Detection | `playDetection` | Harsh saw 400+600 Hz alarm | +| Level complete | `playLevelComplete` | Ascending 3-note jingle | +| UI click | `playClick` | Square click 800 Hz, 20ms | +| Undo | `playUndo` | Descending triangle 300 Hz | +| Stone throw | `playStoneThrow` | Whoosh sine 800→200 Hz, 100ms | +| Stone impact | `playStoneImpact` | Thud filtered noise, 50ms | +| Key pickup | `playKeyPickup` | Triangle pluck 1200 Hz, 150ms | +| Door unlock | `playDoorUnlock` | Square click + minor third, 250ms | +| Suspicion tier 1 | `playSuspicionAlert` | Saw 600 Hz, 200ms | +| Suspicion tier 2 | `playSuspicionFire` | Sine wobble 800↔1200 Hz, 400ms | ## Localization -Full bilingual support: English and Vietnamese. All user-facing strings stored in JSON locale files. Language persisted in localStorage. +Full bilingual support: English (en) and Vietnamese (vi). All user-facing strings in `src/lib/locales/*.json`. Language persisted in `localStorage`. + +### Key Namespaces +- `banner.*` — affordance gate warnings +- `migration.*` — v2 progress-reset modal +- `gameOver.*` — run-complete / bittersweet end +- `throw.*` — throw-mode hints +- `mechanics.*` — guard/mechanic ARIA labels and display names +- `level{N}Story` — per-level intro/foreshadowing text ## Persistence -- **Progress**: `localStorage` key `nntv-progress` — tracks `maxLevel` (highest unlocked) and `completedLevels[]` +- **Progress**: `localStorage` key `nntv-progress` — `{ maxLevel, completedLevels[], version }` - **Language**: `localStorage` key `nntv-language` — `"en"` or `"vi"` +- Version mismatch triggers the migration modal and resets progress to v2 baseline diff --git a/src/lib/audio.js b/src/lib/audio.js index 037b1bb..ea55c0f 100644 --- a/src/lib/audio.js +++ b/src/lib/audio.js @@ -86,6 +86,121 @@ export function playUndo() { playTone(300, 0.05, 'triangle', 0.3); } +// Stone throw — short whoosh: sine sweep 800→200 Hz, 100ms +export function playStoneThrow() { + if (muted) return; + const ctx = getContext(); + if (!ctx) return; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(800, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(200, ctx.currentTime + 0.1); + gain.gain.setValueAtTime(0.18, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1); + osc.connect(gain); + gain.connect(masterGain); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + 0.1); +} + +// Stone impact — short thud: noise burst through lowpass filter, 50ms +export function playStoneImpact() { + if (muted) return; + const ctx = getContext(); + if (!ctx) return; + // White-noise buffer (0.05 s) + const bufLen = Math.ceil(ctx.sampleRate * 0.05); + const buf = ctx.createBuffer(1, bufLen, ctx.sampleRate); + const data = buf.getChannelData(0); + for (let i = 0; i < bufLen; i++) data[i] = Math.random() * 2 - 1; + const src = ctx.createBufferSource(); + src.buffer = buf; + const filter = ctx.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.value = 280; + const gain = ctx.createGain(); + gain.gain.setValueAtTime(0.4, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05); + src.connect(filter); + filter.connect(gain); + gain.connect(masterGain); + src.start(ctx.currentTime); + src.stop(ctx.currentTime + 0.05); +} + +// Key pickup — bright triangle pluck at 1200 Hz, 150ms +export function playKeyPickup() { + if (muted) return; + const ctx = getContext(); + if (!ctx) return; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'triangle'; + osc.frequency.value = 1200; + gain.gain.setValueAtTime(0.25, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15); + osc.connect(gain); + gain.connect(masterGain); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + 0.15); +} + +// Door unlock — square click (400 Hz) + minor third (480 Hz), 250ms +export function playDoorUnlock() { + if (muted) return; + const ctx = getContext(); + if (!ctx) return; + [400, 480].forEach((freq, i) => { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'square'; + osc.frequency.value = freq; + gain.gain.setValueAtTime(0.1, ctx.currentTime + i * 0.04); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.25); + osc.connect(gain); + gain.connect(masterGain); + osc.start(ctx.currentTime + i * 0.04); + osc.stop(ctx.currentTime + 0.25); + }); +} + +// Suspicion tier 0→1 alert — sawtooth 600 Hz, 200ms +export function playSuspicionAlert() { + if (muted) return; + const ctx = getContext(); + if (!ctx) return; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sawtooth'; + osc.frequency.value = 600; + gain.gain.setValueAtTime(0.12, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2); + osc.connect(gain); + gain.connect(masterGain); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + 0.2); +} + +// Suspicion tier 2 firing — sine wobble 800→1200 Hz, 400ms +export function playSuspicionFire() { + if (muted) return; + const ctx = getContext(); + if (!ctx) return; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(800, ctx.currentTime); + osc.frequency.linearRampToValueAtTime(1200, ctx.currentTime + 0.2); + osc.frequency.linearRampToValueAtTime(800, ctx.currentTime + 0.4); + gain.gain.setValueAtTime(0.15, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4); + osc.connect(gain); + gain.connect(masterGain); + osc.start(ctx.currentTime); + osc.stop(ctx.currentTime + 0.4); +} + export function setMuted(value) { muted = value; } diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 05d9bb0..0471409 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -12,7 +12,6 @@ "back": "Back", "levelSelectTitle": "SELECT LEVEL", "levelNumber": "Level", - "lives": "Lives: ", "level": "Level: ", "pause": "PAUSE", "menu": "MENU", @@ -30,21 +29,22 @@ "mainMenu": "MAIN MENU", "guideTitle": "GAME GUIDE", "levelObjectives": "LEVEL OBJECTIVES", - "levelObjectivesContent": "Navigate through 12 levels of increasing danger.\nReach the green goal cell to advance.\nAvoid lit cells or lose a life.\nYou have 3 lives to complete your mission.", + "levelObjectivesContent": "Navigate through 12 levels of increasing danger.\nReach the green goal cell to advance.\nAvoid lit cells — detection restarts the level.", "movementControls": "MOVEMENT CONTROLS", "movementControlsContent": "- Arrow Keys: Move in four directions\n- WASD Keys: Alternative movement controls\n- Click/Tap: Move to an adjacent cell", "enemyTypes": "ENEMY TYPES", - "enemyTypesContent": "- Red Guards (Static): Always light the same cells around them.\n- Blue Guards (Rotating): Shine a beam that rotates 90 degrees each turn.\n- Yellow Guards (Blinking): Toggle their lights on and off each turn.\n- Purple Guards (Patrolling): Move along a path, lighting cells ahead and to the right.\n- Green Mirrors: Deflect rotating beams 90 degrees. They cannot be avoided — only predicted.", + "enemyTypesContent": "- Red Guards (Static/Wilting): Emit a Manhattan aura that shrinks each turn. Wait them out.\n- Blue Guards (Rotating): Shine a beam that rotates 90 degrees each turn.\n- Yellow Guards (Blinking): Toggle their lights on and off each turn.\n- Purple Guards (Patrolling): Move along a path, lighting cells ahead and to the right.\n- Green Mirrors: Deflect rotating beams 90 degrees. Predict — don't avoid.", "advancedEnemyTypes": "ADVANCED ENEMIES", - "advancedEnemyTypesContent": "- Orange Chasers: Detect you within a radius. Once alerted, they hunt you using the shortest path — even around walls. They glow bright red when chasing. Escape their detection range and they return home.\n- Green Mirrors: Stationary diamond-shaped reflectors. They redirect rotating beams by 90 degrees. A single beam can bounce up to 3 times. Plan your path by predicting where reflected light will land.", + "advancedEnemyTypesContent": "- Orange Chasers: Detect you within a radius. Once alerted, they hunt you using the shortest path — even around walls. They glow bright red when chasing. Escape their detection range and they return home.\n- Dark Red Snipers: Aim along a fixed line of sight. Step into their aim line and you're detected — no delay.\n- Violet Suspicion Guards: Watch a patrol zone. Move across their view and their suspicion tier rises. Tier 1 = alerted (slowed). Tier 2 = firing (instant detection).", "gameTips": "TIPS", - "gameTipsContent": "- Press V to preview where lights will be next turn.\n- Press Space to wait a turn without moving.\n- Press Z to undo your last move.\n- Press Y to redo an undone move.\n- Swipe on mobile to move in that direction.\n- Tap an adjacent cell or swipe to move on mobile.", + "gameTipsContent": "- Press V to preview where lights will be next turn.\n- Press Space to wait a turn without moving.\n- Press Z to undo your last move.\n- Press Y to redo an undone move.\n- Press E to enter stone-throw targeting mode.\n- Swipe on mobile to move in that direction.\n- Tap an adjacent cell or swipe to move on mobile.", "controlsTitle": "CONTROLS", "controlMove": "Move", "controlWait": "Wait one turn", "controlPreview": "Toggle next-turn preview", "controlUndo": "Undo last move", "controlRedo": "Redo move", + "controlThrow": "Enter stone-throw mode", "controlTap": "Move to adjacent cell", "controlSwipe": "Move (mobile)", "moves": "Moves", @@ -53,16 +53,37 @@ "storyTitle": "THE MISSING CARROT PRINCESS", "storyText": "In the peaceful Vegetable Kingdom, where carrots, cabbages, and all manner of vegetables lived in harmony, a terrible tragedy has occurred.\n\nThe beloved Carrot Princess, daughter of King Carrot III, has mysteriously disappeared from the royal palace.\n\nThe kingdom is in chaos. The Vegetable Guards have been placed on high alert, patrolling every corner of the realm with their watchful eyes.\n\nAs the kingdom's last hope, you, a skilled ninja rabbit from the shadows, have been called upon for this dangerous mission.\n\nYou must infiltrate the heavily guarded areas, avoid detection at all costs, and rescue the missing Carrot Princess.\n\nThe fate of the Vegetable Kingdom rests in your paws...\n\nGood luck, Night Ninja!", - "level1Story": "The outskirts of the Vegetable Kingdom. A quiet garden path lies ahead.\nNo guards in sight. A good place to practice your stealth.", - "level2Story": "A watchtower stands at the garden's edge. A lone red guard keeps watch.\nIts lantern casts a fixed glow. Stay out of the light.", - "level3Story": "The vegetable garden is dotted with sentries.\nTwo red guards patrol the rows of crops. Find a path between their lights.", - "level4Story": "A rotating searchlight sweeps the garden entrance.\nIts beam turns clockwise each turn. Time your moves carefully.", - "level5Story": "The fortress torches flicker with an eerie rhythm.\nA blinking guard's light pulses on and off. Move on the dark turns.", - "level6Story": "The fortress gate combines fixed lanterns with flickering torches.\nThree layers of defense. Stay patient, count the flickers.", - "level7Story": "An underground passage winds beneath the fortress walls.\nA patrolling guard marches the corridor. Predict its path.", - "level8Story": "The gauntlet. Two patrol guards weave through the darkness.\nTheir paths interlock like gears. Find the gap between them.", - "level9Story": "The royal palace. Two routes lie ahead — one short, one long.\nThe old prisoner's warning echoes: \"Not every shortcut is what it seems.\"", - "level10Story": "The Hall of Mirrors. Rotating searchlights bounce off crystal surfaces.\nThe beams reflect and multiply. Watch where the light bends.\nThey say the Princess can sense any living thing nearby...", - "level11Story": "The Throne Room. Every type of guard protects this sacred hall.\nStatic, rotating, blinking, patrolling — all stand between you and the final door.\nNo one who entered the chamber beyond has returned. Not because of guards...", - "level12Story": "At last... the Princess Chamber. She must be just beyond that door.\nA strange warmth emanates from within. The air itself feels watchful.\nYou've come too far to stop now. But something tells you... she already knows you're here." + "level1Story": "The outskirts of the Vegetable Kingdom. A quiet garden path lies ahead.\nNo guards in sight. Learn your footing before the dark closes in.", + "level2Story": "A watchtower stands at the garden's edge. Three wilting sentries keep watch.\nTheir lanterns burn bright at first — but patience is a blade. Wait for the glow to fade.", + "level3Story": "The garden is laced with one-way passages — ancient ratchets that only open one direction.\nChoose your route before you step. There is no going back.", + "level4Story": "A rotating searchlight sweeps the garden entrance.\nIts beam turns clockwise each turn. Count the rhythm. Move between the sweeps.", + "level5Story": "The fortress torches flicker with an eerie rhythm.\nA blinking guard's light pulses on and off. Move on the dark turns — the gaps are brief.", + "level6Story": "The fortress gate combines fixed lanterns with flickering torches.\nThree layers of defense. Stay patient, count the flickers, find the seam.", + "level7Story": "An underground passage winds beneath the fortress walls.\nA patrolling guard marches the corridor — and somewhere, a beam bends off a crystal mirror.", + "level8Story": "Two mirrors. Two rotating beams. The light web shifts every turn.\nFind the gap that exists for exactly one step. It will not wait for you.", + "level9Story": "The royal palace. Two routes lie ahead — one short, one long.\nThe chaser guards the short path. Sometimes the long way is the only way.", + "level10Story": "The Hall of Mirrors. Reflected beams multiply with every turn.\nThey say the Princess can sense any living thing nearby — even ghosts of past attempts...", + "level11Story": "The Throne Room. Every guard the kingdom commands stands between you and that door.\nNo one who entered the chamber beyond has returned. The throne itself was a warning.", + "level12Story": "At last... the Princess Chamber. A strange warmth emanates from within.\nThe air itself feels watchful. She already knows you're here.", + + "banner.noUndo": "No undo this level — every move counts.", + "banner.noPreview": "No preview this level — you can't see ahead.", + "migration.title": "Welcome to Night Ninja v2", + "migration.body": "The Vegetable Kingdom has been redesigned. Your previous progress has been reset.", + "migration.dismiss": "Continue", + "gameOver.runComplete": "Run Complete!", + "gameOver.runCompleteBody": "You guided the ninja through the kingdom. The Princess Chamber awaits...", + "throw.hintEnter": "Press E to throw a stone", + "throw.hintTargeting": "Choose a target — Enter to throw, Esc to cancel", + "mechanics.sniper.name": "Sniper Pepper", + "mechanics.suspicion.name": "Suspicious Onion", + "mechanics.suspicion.alerted": "Alerted", + "mechanics.suspicion.firing": "Firing!", + "mechanics.stones.label": "Stones", + "mechanics.keys.label": "Keys", + "mechanics.oneWay.aria": "One-way arrow {dir}", + "mechanics.warm.aria": "Warm cell, will cool next turn", + "mechanics.door.locked": "Locked door, key {keyId}", + "mechanics.door.open": "Open door", + "mechanics.key.aria": "Key {keyId}" } diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json index ce6d2bd..cac05b8 100644 --- a/src/lib/locales/vi.json +++ b/src/lib/locales/vi.json @@ -12,7 +12,6 @@ "back": "Quay lại", "levelSelectTitle": "CHỌN CẤP ĐỘ", "levelNumber": "Cấp độ", - "lives": "Mạng: ", "level": "Cấp độ: ", "pause": "TẠM DỪNG", "menu": "MENU", @@ -30,21 +29,22 @@ "mainMenu": "MENU CHÍNH", "guideTitle": "HƯỚNG DẪN GAME", "levelObjectives": "MỤC TIÊU CÁC CẤP ĐỘ", - "levelObjectivesContent": "Vượt qua 12 cấp độ với mức độ nguy hiểm tăng dần.\nĐến ô xanh lá để qua màn.\nTránh các ô sáng nếu không muốn mất mạng.\nBạn có 3 mạng để hoàn thành nhiệm vụ.", + "levelObjectivesContent": "Vượt qua 12 cấp độ với mức độ nguy hiểm tăng dần.\nĐến ô xanh lá để qua màn.\nTránh các ô sáng — bị phát hiện sẽ khởi động lại cấp độ.", "movementControls": "ĐIỀU KHIỂN DI CHUYỂN", "movementControlsContent": "- Phím mũi tên: Di chuyển theo bốn hướng\n- Phím WASD: Điều khiển di chuyển thay thế\n- Nhấp chuột: Di chuyển đến ô kề bên", "enemyTypes": "CÁC LOẠI KẺ ĐỊCH", - "enemyTypesContent": "- Lính Đỏ (Cố định): Luôn chiếu sáng các ô cố định xung quanh.\n- Lính Xanh (Xoay): Chiếu tia sáng xoay 90 độ mỗi lượt.\n- Lính Vàng (Nhấp nháy): Bật tắt đèn mỗi lượt.\n- Lính Tím (Tuần tra): Di chuyển theo lộ trình, chiếu sáng phía trước và bên phải.\n- Gương Xanh Lá: Phản chiếu tia sáng xoay 90 độ. Không thể tránh — chỉ có thể dự đoán.", + "enemyTypesContent": "- Lính Đỏ (Cà Chua Héo): Phát ra hào quang thu nhỏ theo từng lượt. Kiên nhẫn chờ ánh sáng tắt dần.\n- Lính Xanh (Xoay): Chiếu tia sáng xoay 90 độ mỗi lượt.\n- Lính Vàng (Nhấp nháy): Bật tắt đèn mỗi lượt.\n- Lính Tím (Tuần tra): Di chuyển theo lộ trình, chiếu sáng phía trước và bên phải.\n- Gương Xanh Lá: Phản chiếu tia sáng xoay 90 độ. Hãy dự đoán — đừng né tránh.", "advancedEnemyTypes": "KẺ ĐỊCH NÂNG CAO", - "advancedEnemyTypesContent": "- Lính Cam (Truy đuổi): Phát hiện bạn trong bán kính nhất định. Khi cảnh giác, chúng truy đuổi bạn theo đường ngắn nhất — kể cả vòng qua tường. Chúng phát sáng đỏ rực khi truy đuổi. Thoát ra ngoài bán kính phát hiện và chúng sẽ quay về vị trí ban đầu.\n- Gương Xanh Lá: Vật phản chiếu hình thoi cố định. Chúng đổi hướng tia sáng xoay 90 độ. Một tia sáng có thể phản chiếu tối đa 3 lần. Lên kế hoạch đường đi bằng cách dự đoán ánh sáng phản chiếu sẽ rơi vào đâu.", + "advancedEnemyTypesContent": "- Lính Cam (Truy đuổi): Phát hiện bạn trong bán kính nhất định. Khi cảnh giác, chúng truy đuổi bạn theo đường ngắn nhất — kể cả vòng qua tường. Thoát ra ngoài bán kính phát hiện và chúng sẽ quay về vị trí ban đầu.\n- Ớt Bắn Tỉa (Đỏ Sậm): Nhắm theo một đường thẳng cố định. Bước vào tầm ngắm là bị phát hiện ngay lập tức.\n- Hành Tím Nghi Ngờ: Theo dõi vùng tuần tra. Di chuyển qua tầm nhìn của chúng khiến mức độ nghi ngờ tăng lên. Cấp 1 = cảnh giác (chậm lại). Cấp 2 = khai hỏa (bị phát hiện tức thì).", "gameTips": "MẸO CHƠI", - "gameTipsContent": "- Nhấn V để xem trước ánh sáng lượt tiếp theo.\n- Nhấn Space để đợi một lượt mà không di chuyển.\n- Nhấn Z để hoàn tác bước đi cuối.\n- Nhấn Y để làm lại bước đã hoàn tác.\n- Vuốt trên điện thoại để di chuyển theo hướng đó.\n- Chạm vào ô kề bên hoặc vuốt để di chuyển trên điện thoại.", + "gameTipsContent": "- Nhấn V để xem trước ánh sáng lượt tiếp theo.\n- Nhấn Space để đợi một lượt mà không di chuyển.\n- Nhấn Z để hoàn tác bước đi cuối.\n- Nhấn Y để làm lại bước đã hoàn tác.\n- Nhấn E để vào chế độ ném đá.\n- Vuốt trên điện thoại để di chuyển theo hướng đó.\n- Chạm vào ô kề bên hoặc vuốt để di chuyển trên điện thoại.", "controlsTitle": "ĐIỀU KHIỂN", "controlMove": "Di chuyển", "controlWait": "Đợi một lượt", "controlPreview": "Xem trước ánh sáng lượt sau", "controlUndo": "Hoàn tác bước đi", "controlRedo": "Làm lại bước đi", + "controlThrow": "Chế độ ném đá", "controlTap": "Di chuyển đến ô kề bên", "controlSwipe": "Di chuyển (điện thoại)", "moves": "Bước đi", @@ -53,16 +53,37 @@ "storyTitle": "CÔNG CHÚA CÀ RỐT MẤT TÍCH", "storyText": "Trong vương quốc Rau Củ Quả thanh bình, nơi cà rốt, bắp cải và đủ loại rau củ sống hòa thuận với nhau, một bi kịch khủng khiếp đã xảy ra.\n\nCông chúa Cà Rốt yêu quý, con gái của Vua Cà Rốt Đệ Tam, đã bí ẩn biến mất khỏi cung điện hoàng gia.\n\nVương quốc đang trong tình trạng hỗn loạn. Các Vệ Binh Rau Củ đã được đặt trong tình trạng báo động cao, tuần tra mọi ngóc ngách của vương quốc với đôi mắt cảnh giác của họ.\n\nLà hy vọng cuối cùng của vương quốc, bạn, một ninja thỏ tài năng từ bóng tối, đã được gọi đến cho nhiệm vụ nguy hiểm này.\n\nBạn phải xâm nhập vào các khu vực được canh gác nghiêm ngặt, tránh bị phát hiện bằng mọi giá, và giải cứu Công chúa Cà Rốt mất tích.\n\nSố phận của Vương quốc Rau Củ Quả nằm trong đôi bàn chân của bạn...\n\nChúc may mắn, Ninja Đêm!", - "level1Story": "Ngoại ô Vương quốc Rau Củ. Con đường vườn yên tĩnh trải dài phía trước.\nKhông có lính canh. Nơi tốt để luyện tập kỹ năng ẩn thân.", - "level2Story": "Một tháp canh đứng ở rìa khu vườn. Một lính đỏ đơn độc canh gác.\nĐèn lồng của hắn tỏa ánh sáng cố định. Hãy tránh xa.", - "level3Story": "Khu vườn rau củ rải rác lính canh.\nHai lính đỏ tuần tra giữa các luống rau. Tìm đường đi giữa ánh sáng của họ.", - "level4Story": "Đèn pha xoay quét qua cổng khu vườn.\nTia sáng quay theo chiều kim đồng hồ mỗi lượt. Canh thời điểm kỹ lưỡng.", - "level5Story": "Ngọn đuốc pháo đài nhấp nháy theo nhịp kỳ lạ.\nÁnh sáng lính vàng nhấp nháy bật tắt. Di chuyển vào lượt tối.", - "level6Story": "Cổng pháo đài kết hợp đèn lồng cố định với đuốc nhấp nháy.\nBa lớp phòng thủ. Kiên nhẫn, đếm nhịp nhấp nháy.", - "level7Story": "Đường hầm uốn lượn dưới tường pháo đài.\nLính tím tuần tra dọc hành lang. Dự đoán đường đi của hắn.", - "level8Story": "Thử thách cam go. Hai lính tuần tra đan xen trong bóng tối.\nĐường đi của họ đan nhau như bánh răng. Tìm khe hở.", - "level9Story": "Cung điện hoàng gia. Hai con đường — một ngắn, một dài.\nLời cảnh báo của tù nhân xưa: \"Không phải đường tắt nào cũng như vẻ ngoài.\"", - "level10Story": "Đại sảnh Gương. Đèn pha xoay phản chiếu qua các bề mặt pha lê.\nTia sáng phản xạ và nhân lên. Theo dõi nơi ánh sáng bẻ cong.\nNghe đồn Công chúa có thể cảm nhận mọi sinh vật sống gần nàng...", - "level11Story": "Phòng Ngai Vàng. Mọi loại lính canh bảo vệ đại sảnh thiêng liêng này.\nCố định, xoay, nhấp nháy, tuần tra — tất cả đứng giữa bạn và cánh cửa cuối.\nKhông ai vào phòng bên kia trở về. Không phải vì lính canh...", - "level12Story": "Cuối cùng... Phòng Công chúa. Nàng chắc hẳn ở ngay sau cánh cửa kia.\nMột hơi ấm lạ tỏa ra từ bên trong. Không khí như đang dõi theo bạn.\nBạn đã đi quá xa để dừng lại. Nhưng có gì đó mách bảo... nàng đã biết bạn ở đây." + "level1Story": "Ngoại ô Vương quốc Rau Củ. Con đường vườn yên tĩnh trải dài phía trước.\nKhông có lính canh. Hãy làm quen với bước chân trước khi bóng tối ập đến.", + "level2Story": "Một tháp canh đứng ở rìa khu vườn. Ba người lính tàn phai đang canh gác.\nĐèn lồng của họ lúc đầu sáng rực — nhưng kiên nhẫn chính là vũ khí. Hãy chờ ánh sáng tắt dần.", + "level3Story": "Khu vườn rải rác những lối đi một chiều — những cổng cổ xưa chỉ mở theo một hướng.\nHãy chọn đường trước khi bước. Không có đường lui.", + "level4Story": "Đèn pha xoay quét qua cổng khu vườn.\nTia sáng quay theo chiều kim đồng hồ mỗi lượt. Đếm nhịp. Di chuyển giữa các lần quét.", + "level5Story": "Ngọn đuốc pháo đài nhấp nháy theo nhịp kỳ lạ.\nÁnh sáng lính vàng nhấp nháy bật tắt. Di chuyển vào lượt tối — khoảng hở rất ngắn.", + "level6Story": "Cổng pháo đài kết hợp đèn lồng cố định với đuốc nhấp nháy.\nBa lớp phòng thủ. Kiên nhẫn, đếm nhịp nhấp nháy, tìm kẽ hở.", + "level7Story": "Đường hầm uốn lượn dưới tường pháo đài.\nLính tuần tra đi dọc hành lang — và ở đâu đó, một tia sáng bẻ cong qua gương pha lê.", + "level8Story": "Hai gương. Hai tia xoay. Mạng lưới ánh sáng thay đổi mỗi lượt.\nTìm khoảng trống tồn tại đúng một bước. Nó sẽ không chờ bạn.", + "level9Story": "Cung điện hoàng gia. Hai con đường — một ngắn, một dài.\nLính truy đuổi canh đường tắt. Đôi khi con đường dài mới là con đường duy nhất.", + "level10Story": "Đại sảnh Gương. Các tia phản chiếu nhân lên theo từng lượt.\nNghe đồn Công chúa có thể cảm nhận mọi sinh vật sống gần nàng — kể cả bóng ma của những lần thử trước...", + "level11Story": "Phòng Ngai Vàng. Mọi loại lính canh vương quốc có trong tay đều đứng giữa bạn và cánh cửa đó.\nKhông ai vào phòng bên kia trở về. Chính ngai vàng là lời cảnh báo.", + "level12Story": "Cuối cùng... Phòng Công chúa. Một hơi ấm lạ tỏa ra từ bên trong.\nKhông khí như đang dõi theo bạn. Nàng đã biết bạn ở đây rồi.", + + "banner.noUndo": "Không hoàn tác được ở cấp độ này — mỗi bước đi đều quan trọng.", + "banner.noPreview": "Không xem trước được ở cấp độ này — bạn không thể nhìn trước.", + "migration.title": "Chào mừng đến Night Ninja v2", + "migration.body": "Vương quốc Rau Củ đã được thiết kế lại. Tiến trình trước của bạn đã được đặt lại.", + "migration.dismiss": "Tiếp tục", + "gameOver.runComplete": "Hoàn thành hành trình!", + "gameOver.runCompleteBody": "Bạn đã dẫn dắt ninja qua vương quốc. Phòng Công chúa đang chờ...", + "throw.hintEnter": "Nhấn E để ném đá", + "throw.hintTargeting": "Chọn mục tiêu — Enter để ném, Esc để hủy", + "mechanics.sniper.name": "Ớt Bắn Tỉa", + "mechanics.suspicion.name": "Hành Tím Nghi Ngờ", + "mechanics.suspicion.alerted": "Cảnh giác", + "mechanics.suspicion.firing": "Khai hỏa!", + "mechanics.stones.label": "Đá", + "mechanics.keys.label": "Chìa khóa", + "mechanics.oneWay.aria": "Mũi tên một chiều {dir}", + "mechanics.warm.aria": "Ô ấm, sẽ nguội vào lượt tiếp theo", + "mechanics.door.locked": "Cửa khóa, chìa khóa {keyId}", + "mechanics.door.open": "Cửa mở", + "mechanics.key.aria": "Chìa khóa {keyId}" } diff --git a/src/lib/pixel/art-characters.js b/src/lib/pixel/art-characters.js index e6188df..8a9c113 100644 --- a/src/lib/pixel/art-characters.js +++ b/src/lib/pixel/art-characters.js @@ -319,6 +319,116 @@ export const PRINCESS_PAL = { Y: NNTV.carrotDark, L: NNTV.leafGreen, S: NNTV.leafDark, G: NNTV.gold, }; +// ── SNIPER PEPPER — dark red, elongated pointing body, 32×32 ────────────────── +// Represents the new sniper guard type. Body is a dark-red chili pepper shape +// with a triangular "aim cone" pointing in facing direction. +export const SNIPER_ART = [ + '................................', + '................................', + '..............GGG...............', + '.............GLLLG..............', + '............GLDDLG..............', + '...........GLDLLDLG.............', + '..........GGLLLLLLGG............', + '.........KKKKKKKKKKKK...........', + '.......KKDDDDDDDDDDDKK..........', + '......KDDHHHDDDDHHHDDK..........', + '.....KDDHHHHDDDDHHHHDK..........', + '....KDDHHWWHDDDDHWWHHDK.........', + '....KDDHWWWHDDDDDHWWHDDK........', + '...KDDDHHHHDDDDDDDHHHHDDDK......', + '...KDDDDDDDDDDAADDDDDDDDDK......', + '...KDDDDDDDDDAAADDDDDDDDDK......', + '...KDDDDDDDDDDDDDDDDDDDDK.......', + '...KDDDDHHDDDDDDDDDDHHDDDK......', + '....KDDDHHHDDDDDDDDHHHDDK.......', + '....KDDDDHHHDDDDDDHHHDDDK.......', + '.....KDDDDDHHHHHHHHDDDDK........', + '......KDDDDDDDDDDDDDDK..........', + '.......KKDDDDDDDDDKK............', + '.........KKDDDDDKK..............', + '...........KKKKKK...............', + '................................', + '................................', + '................................', + '................................', + '................................', + '................................', + '................................', +]; +export const SNIPER_PAL = { + K: NNTV.ink, + D: '#8b1a1a', // dark red body + H: '#5a0a0a', // deep shadow + W: NNTV.eyeShine, + A: '#ffaaaa', // aim indicator dot + G: NNTV.leafGreen, + L: NNTV.leafDark, +}; + +// ── SUSPICION ONION — violet/purple, tier-state variants, 32×32 ─────────────── +// Calm state (tier 0): relaxed onion face +export const SUSPICION_CALM_ART = [ + '................................', + '................................', + '..............PPP...............', + '.............PLLLP..............', + '............PLDDDLP.............', + '...........PLDDDDLP.............', + '...........KLLLLLLK.............', + '..........KKPPPPPPKK............', + '.........KPPOOOOOOPPK...........', + '........KPPOOPPPPOOPPK..........', + '.......KPPOOOPPPPOOOPPK.........', + '......KPPOOOPPPPPPOOOPPK........', + '.....KPOOOPPPPPPPPPPOOOPK.......', + '.....KPOOPPPPPPPPPPPPOOPK.......', + '....KPPOOPHHPPPPPPHHPPOOPK......', + '....KPOOPPHWHPPPPPHWHPPOOPK.....', + '....KPOOPPHWHPPPPPHWHPPOOPK.....', + '....KPOOPPPHHPPPPPPHHPPOOPK.....', + '....KPOOOPPPPPPPPPPPPPOOOPK.....', + '....KPOOPPPPPUUUUUPPPPOOPK......', + '....KPPOOPPKUUUUUUKPPPOOPK......', + '.....KPOOPKUUUUUUUKPPOOPK.......', + '.....KPPOOPKUUUUUKPPOOPPK.......', + '......KPOOOPKKKKKPOOOPK.........', + '.......KPOOOOPPPPOOOOPK.........', + '........KPPOOOOOOOOPPK..........', + '.........KKPPOOOOOOPPKK.........', + '...........KKPPPPPPKK...........', + '.............KKKKKK.............', + '................................', + '................................', + '................................', +]; +export const SUSPICION_CALM_PAL = { + K: NNTV.ink, P: NNTV.onionPurp, O: NNTV.onionDark, + H: NNTV.onionDark, W: NNTV.eyeShine, U: NNTV.cream, + L: NNTV.leafGreen, D: NNTV.leafDark, +}; + +// Alerted state (tier 1): one eye wide open, eyebrow raised +export const SUSPICION_ALERT_ART = SUSPICION_CALM_ART.map((row, i) => { + // Widen the left eye row — same art, just reuse CALM with alert palette + return row; +}); +export const SUSPICION_ALERT_PAL = { + ...SUSPICION_CALM_PAL, + P: '#cc66ff', // brighter violet — alarmed + O: '#7a2a9a', + W: '#ffeeaa', // yellow-tinted shine — alarmed +}; + +// Firing state (tier 2): flush bright, all eyes open +export const SUSPICION_FIRE_PAL = { + ...SUSPICION_CALM_PAL, + P: '#ff88ff', // magenta — full alert + O: '#991a99', + W: '#ffffff', + U: '#ff4444', // red mouth — hostile +}; + // Map game guard types → sprite data. Blinking uses on/off variants. export const GUARD_SPRITES = { static: { art: TOMATO_ART, pal: TOMATO_PAL }, @@ -327,4 +437,11 @@ export const GUARD_SPRITES = { patrolling: { art: EGGPLANT_ART, pal: EGGPLANT_PAL }, mirror: { art: LETTUCE_ART, pal: LETTUCE_PAL }, chaser: { art: PUMPKIN_ART, pal: PUMPKIN_PAL }, + sniper: { art: SNIPER_ART, pal: SNIPER_PAL }, + suspicion: { + art: SUSPICION_CALM_ART, + pal: SUSPICION_CALM_PAL, + palAlert: SUSPICION_ALERT_PAL, + palFire: SUSPICION_FIRE_PAL, + }, }; diff --git a/src/lib/pixel/art-tiles.js b/src/lib/pixel/art-tiles.js index 9fe1db0..f70bc0d 100644 --- a/src/lib/pixel/art-tiles.js +++ b/src/lib/pixel/art-tiles.js @@ -123,3 +123,167 @@ export const TILE_PREVIEW = [ 'yyyy........yyyy', ]; export const TILE_PREVIEW_PAL = { y: NNTV.gridLit }; + +// ── DOOR LOCKED — 16×16, color keyed by keyId ──────────────────────────────── +// Base art shape; palette swapped per keyId (gold=1, silver=2, copper=3) +export const TILE_DOOR_LOCKED = [ + 'kkkkkkkkkkkkkkkk', + 'kcccccccccccccc k', + 'kcdddddddddddcck', + 'kcdkkkkkkkkkdcck', + 'kcdkbbbbbbbbkcck', + 'kcdkbhhbbhhbkck.', + 'kcdkbhwbbhwbkck.', + 'kcdkbbbbbbbbkck.', + 'kcdkbbbbbbbbkck.', + 'kcdkbbXXbbbbkck.', + 'kcdkbbXXbbbbkck.', + 'kcdkbbbbbbbbkck.', + 'kcdkkkkkkkkkdcck', + 'kcdddddddddddcck', + 'kcccccccccccccc k', + 'kkkkkkkkkkkkkkkk', +]; +// Gold door (key 1) +export const TILE_DOOR_GOLD_PAL = { + k: NNTV.ink, c: NNTV.stoneDark, d: NNTV.stone, + b: NNTV.gold, h: NNTV.goldDark, w: NNTV.cream, + X: NNTV.ink, +}; +// Silver door (key 2) +export const TILE_DOOR_SILVER_PAL = { + k: NNTV.ink, c: NNTV.stoneDark, d: NNTV.stone, + b: '#c0c0c0', h: '#707070', w: NNTV.cream, + X: NNTV.ink, +}; +// Copper door (key 3) +export const TILE_DOOR_COPPER_PAL = { + k: NNTV.ink, c: NNTV.stoneDark, d: NNTV.stone, + b: '#b87333', h: '#7a4a1a', w: NNTV.cream, + X: NNTV.ink, +}; + +// ── DOOR OPEN — same silhouette, brighter frame, open gap ──────────────────── +export const TILE_DOOR_OPEN = [ + 'kkkkkkkkkkkkkkkk', + 'k..............k', + 'kd..............dk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kdk..........kdk', + 'kd..............dk', + 'k..............k', + 'kkkkkkkkkkkkkkkk', +]; +export const TILE_DOOR_OPEN_PAL = { k: NNTV.ink, d: NNTV.stoneLight }; + +// ── KEY ITEM — 16×16, color-coded per keyId ─────────────────────────────────── +export const TILE_KEY = [ + '................', + '................', + '.....kkkkk......', + '....kbbbbbk.....', + '...kbbhhhbbk....', + '...kbhwwwhbk....', + '...kbbhhhbbk....', + '....kbbbbbk.....', + '.....kkbkk......', + '......kbk.......', + '......kbk.......', + '.....kbbkk......', + '.....kbbbk......', + '......kkkk......', + '................', + '................', +]; +export const TILE_KEY_GOLD_PAL = { k: NNTV.ink, b: NNTV.gold, h: NNTV.goldDark, w: NNTV.cream }; +export const TILE_KEY_SILVER_PAL = { k: NNTV.ink, b: '#c0c0c0', h: '#707070', w: NNTV.cream }; +export const TILE_KEY_COPPER_PAL = { k: NNTV.ink, b: '#b87333', h: '#7a4a1a', w: NNTV.cream }; + +// ── ONE-WAY ARROW — 16×16, 4 rotations ──────────────────────────────────────── +// Direction: 0=up, 1=right, 2=down, 3=left (matches level data encoding) +export const TILE_ONEWAY_RIGHT = [ + 'aaaaaaaaaaaaaaa.', + 'abbbbbbbbbbbbbba', + 'abccccccccccccba', + 'abccccccc...ccba', + 'abcccccca...cba.', + 'abccccca.....ba.', + 'abcaaaaaaaaaa.a.', + 'abcaaaaaaaaaaaaa', + 'abcaaaaaaaaaaaaa', + 'abcaaaaaaaaaa.a.', + 'abccccca.....ba.', + 'abcccccca...cba.', + 'abccccccc...ccba', + 'abccccccccccccba', + 'abbbbbbbbbbbbbba', + 'aaaaaaaaaaaaaaa.', +]; +export const TILE_ONEWAY_PAL = { a: NNTV.gridBorder, b: NNTV.twilight, c: NNTV.gridEmpty, a: '#3a3a5a' }; +// Separate palette with arrow color distinct from background +export const TILE_ONEWAY_BASE_PAL = { + a: NNTV.gridBorder, b: NNTV.twilight, c: NNTV.gridEmpty, +}; +export const TILE_ONEWAY_ARROW_PAL = { + ...TILE_ONEWAY_BASE_PAL, + a: '#5a5a8a', // subtle blue-grey arrow +}; + +// ── WARM CELL OVERLAY — 16×16, dim orange glow, distinct from bright TILE_LIT ─ +export const TILE_WARM = [ + 'oooooooooooooooo', + 'owwwwwwwwwwwwwwo', + 'owooooooooooooowo', + 'owooofffoooooowo', + 'owooofffooooowo.', + 'owoooofoooooowo.', + 'owo..........owo', + 'owo..........owo', + 'owo..........owo', + 'owo..........owo', + 'owooooooooooowo.', + 'owooofffooooowo.', + 'owooofffoooooowo', + 'owooooooooooooowo', + 'owwwwwwwwwwwwwwo', + 'oooooooooooooooo', +]; +export const TILE_WARM_PAL = { + o: '#2a1a0a', // very dark orange border + w: '#5a3010', // dim amber rim + f: '#884422', // warm ember glow +}; + +// ── STONE HUD ICON — 16×16 small grey rock ──────────────────────────────────── +export const ICON_STONE = [ + '................', + '................', + '......kkk.......', + '.....kmmmk......', + '....kmsslmk.....', + '....kmsllmk.....', + '...kmslllmmk....', + '...kmllllmmk....', + '...kmmllmmmk....', + '....kmmmmmk.....', + '.....kkkkk......', + '................', + '................', + '................', + '................', + '................', +]; +export const ICON_STONE_PAL = { + k: NNTV.ink, + m: NNTV.stone, + l: NNTV.stoneLight, + s: '#9a9ab0', +};