Files
nntv/docs/codebase-summary.md
tiennm99 11371c36b3 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.
2026-04-26 08:27:33 +07:00

285 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Codebase Summary - Night Ninja: Twilight Voyage
## Overview
NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The codebase separates pure JS game engine (classes in `lib/game/`) from Svelte rendering (scenes + components). State flows one-way: game classes mutate internally, then `renderVersion++` triggers Svelte re-derivation.
## Module Inventory
### Entry Point
- **src/main.js** — Mounts `App.svelte` into `#app`
### Scene Router
- **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/)
| File | Purpose |
|------|---------|
| MainMenu.svelte | Start game, level select, settings, guide |
| StoryIntro.svelte | Scrolling narrative with skip button |
| 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, all enemy type descriptions |
### Components (src/components/)
| File | Purpose |
|------|---------|
| Button.svelte | Reusable styled button |
| GameBoard.svelte | CSS grid of cells, each rendering a pixel-art tile |
| 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 |
### Game Engine (src/lib/game/) — Pure JS, no Svelte
| File | Purpose |
|------|---------|
| 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: 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; extended shape with doors, keys, oneWays, decayTiles, affordances, stones |
| levels.solvability.test.js | CI-enforced: each L1L11 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 `<rect>`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) |
### Audio System (src/lib/)
| File | Purpose |
|------|---------|
| 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; version field for migration detection |
### Localization (src/lib/locales/)
| File | Keys | Purpose |
|------|------|---------|
| 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/)
| File | Purpose |
|------|---------|
| theme.css | CSS variables: colors, fonts, guard colors, grid colors |
## Class Hierarchy
### Guard Inheritance
```
Guard (abstract base: grid, row, col, type, direction, isOn)
├── 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 (v2 shape)
```javascript
{
id: 1, name: "Garden Path", storyKey: "level1Story",
grid: { rows: 8, cols: 8 },
player: { row: 0, col: 0 },
goal: { row: 7, col: 7 },
walls: [{ row, col }, ...],
guards: [
{ 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: [...] },
],
// 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 (v2)
```javascript
{ isWall: boolean, isGoal: boolean, isLight: boolean, isWarm: boolean,
doorKeyId: number|null, isOneWay: boolean, oneWayDir: number|null }
```
### Progress (localStorage)
```javascript
{ maxLevel: 1, completedLevels: [1, 2], version: 2 }
```
## Public API Summary
### GridSystem
```
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
.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)
SniperGuard: .getAimLine() → [{row,col}]
SuspicionGuard: .suspicionTier (0|1|2), .updateSuspicion(playerRow,playerCol)
```
### PrincessMechanic
```
new PrincessMechanic()
.update(grid, player, goalRow, goalCol) → { showMessage, detected }
.lightRing(grid, goalRow, goalCol, radius)
.capture() → { alerted, alertRadius, messageShown }, .apply(state)
```
### GameHistory
```
new GameHistory()
.createSnapshot(player, guards, turnCount, princessState, grid, throwSystem) → snapshot
.pushSnapshot(snap), .undo(...), .redo(...)
.canUndo(), .canRedo(), .reset()
```
### TurnManager
```
new TurnManager()
.nextTurn(grid, player, guards, throwSystem?) → { detected, levelComplete }
.previewNextTurn(grid, player, guards, throwSystem?) → Set<string>
.reset()
```
### Audio (src/lib/audio.js)
```
playMove(), playWait(), playDetection(), playLevelComplete(), playClick(), playUndo()
playStoneThrow(), playStoneImpact()
playKeyPickup(), playDoorUnlock()
playSuspicionAlert(), playSuspicionFire()
setMuted(bool), isMuted() → bool
```
## 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 + migration modal
Game.svelte (central hub)
├── 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 (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
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 | ~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) |
| 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) |