mirror of
https://github.com/tiennm99/nntv.git
synced 2026-09-05 10:19:38 +00:00
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.
This commit is contained in:
+130
-85
@@ -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 `<rect>`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 `<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) |
|
||||
|
||||
@@ -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<string>
|
||||
.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) |
|
||||
|
||||
Reference in New Issue
Block a user