mirror of
https://github.com/tiennm99/nntv.git
synced 2026-05-18 23:28:50 +00:00
ba687ea130
Static guards now emit a Manhattan aura that shrinks by 1 per turn until harmless (clamped at -1 to keep solver state finite). Level data schema change: 'static' guards now take 'initialRadius' instead of 'litCells'. All 27 static-guard entries migrated in one pass. L1 and L2 redesigned with real forced-zigzag walls — solver path is now 20 moves (vs Manhattan 14), so players must move AWAY from the goal to reach it. L2 adds three wilting tomatoes parked on the zigzag path as a gentle intro to the mechanic. Tests: +3 new tests on StaticGuard wilting behavior; all 70 tests pass.
8.8 KiB
8.8 KiB
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.svelteinto#app
Scene Router
- src/App.svelte —
{#key currentScene}withtransition:fade, passesnavigatefunction as prop to all scenes
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 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 |
| Settings.svelte | Language toggle (EN/VI) |
| Guide.svelte | Rules, controls, enemy types |
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, 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 |
| 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 |
| 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 |
| 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) |
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 |
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 |
| 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: playTone, playMoveSound, playDetectionSound, playCompleteSound, toggleMute |
Utilities (src/lib/)
| File | Purpose |
|---|---|
| localization.js | getText/setLanguage/getLanguage/initLanguage |
| progress.js | getProgress/completeLevel via localStorage |
Localization (src/lib/locales/)
| File | Keys | Purpose |
|---|---|---|
| en.json | ~55 | English translations |
| vi.json | ~55 | Vietnamese translations |
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 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
Key Data Structures
Level Definition
{
id: 1, name: "Garden Path", storyKey: "level1Story",
grid: { rows: 6, cols: 6 },
player: { row: 0, col: 0 },
goal: { row: 5, col: 5 },
walls: [{ row: 1, col: 1 }, ...],
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 },
],
isFinalLevel: false
}
Cell State
{ isWall: boolean, isGoal: boolean, isLight: boolean }
Progress (localStorage)
{ maxLevel: 1, completedLevels: [1, 2, 3] }
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}]
Player
new Player(grid, row, col)
.move(direction) → boolean, .moveTo(row, col) → boolean
.isInLitCell() → boolean, .isAtGoal() → boolean
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
PrincessMechanic
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
.pushSnapshot(snap), .undo(...), .redo(...)
.canUndo(), .canRedo(), .reset()
TurnManager
new TurnManager()
.nextTurn(grid, player, guards) → { detected, levelComplete }
.reset()
Dependencies
| 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 |
File Dependency Map
main.js → App.svelte
App.svelte → all scenes
Game.svelte (central hub)
├── lib/game/level-manager.js → grid-system, player, guards, 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)
├── components/GameBoard, PlayerSprite, GuardSprite, GameHud
├── 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)
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) |
| 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 |