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

12 KiB
Raw Permalink Blame History

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)

{
  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)

{ isWall: boolean, isGoal: boolean, isLight: boolean, isWarm: boolean,
  doorKeyId: number|null, isOneWay: boolean, oneWayDir: number|null }

Progress (localStorage)

{ 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)