mirror of
https://github.com/tiennm99/nntv.git
synced 2026-05-25 13:39:31 +00:00
7233310662
- Fix ChaserGuard with BFS pathfinding and proper chase/return states
- Add undo/redo system (Z/Y keys) with full state snapshots
- Add procedural audio via Web Audio API (move, wait, detection, complete)
- Add mobile swipe controls with touch gesture detection
- Add detection feedback (cell flash, player shake animation)
- Add CSS transitions on grid cells for smooth lighting changes
- Add ARIA accessibility labels on game board and cells
- Add controls overlay ("?" button) showing all keyboard/touch shortcuts
- Add mute toggle in HUD
- Update Guide scene with chaser/mirror guard descriptions and tips
- Replace guard switch statement with factory registry pattern
- Extract princess mechanic and touch controls into separate modules
- Localize all UI strings (EN/VI) including new controls and tips
- Update README for Svelte 5 architecture with all current features
- Update project docs (architecture, code standards, codebase summary)
7.9 KiB
7.9 KiB
Code Standards - Night Ninja: Twilight Voyage
Naming Conventions
Files
- Svelte components: PascalCase (
Game.svelte,GameBoard.svelte,PlayerSprite.svelte) - JS modules: kebab-case (
grid-system.js,turn-manager.js,level-manager.js) - JSON/config: kebab-case (
en.json,vi.json,theme.css)
Variables & Functions
- Classes: PascalCase (
GridSystem,Player,RotatingGuard) - Methods & Functions: camelCase (
updateLight(),nextTurn(),getText()) - Constants: UPPER_CASE for arrays (
LEVELS), camelCase for others - Svelte state: camelCase (
renderVersion,isPaused,detected) - Props: camelCase (
cellSize,oncellclick,onplayagain)
Grid Coordinates
- row: Vertical axis (0 = top, increases downward)
- col: Horizontal axis (0 = left, increases rightward)
- Position objects:
{ row: num, col: num }
Code Organization
Directory Structure
src/
├── main.js # Entry point, mounts App.svelte
├── App.svelte # Scene router ({#key} + fade)
│
├── scenes/ # Full-page scene components
│ ├── MainMenu.svelte
│ ├── StoryIntro.svelte
│ ├── LevelIntro.svelte
│ ├── LevelSelect.svelte
│ ├── Game.svelte # Main gameplay (state owner)
│ ├── GameOver.svelte
│ ├── Settings.svelte
│ └── Guide.svelte
│
├── components/ # Reusable UI components
│ ├── Button.svelte
│ ├── GameBoard.svelte # CSS grid rendering
│ ├── GameHud.svelte # Level/lives/turns display
│ ├── PlayerSprite.svelte # Positioned player div
│ ├── GuardSprite.svelte # Colored guard circle/diamond
│ ├── PauseMenu.svelte
│ └── DetectionPopup.svelte
│
├── lib/
│ ├── game/ # Pure JS game engine (no framework)
│ │ ├── grid-system.js
│ │ ├── player.js
│ │ ├── guards.js # Base + 6 guard subclasses (including ChaserGuard)
│ │ ├── turn-manager.js
│ │ ├── level-manager.js # GUARD_REGISTRY factory pattern
│ │ ├── game-history.js # Undo/redo system
│ │ ├── princess-mechanic.js # Level 12 escalating detection
│ │ └── touch-controls.js # Mobile swipe support
│ │
│ ├── levels/
│ │ └── levels.js # 12 level definitions
│ │
│ ├── locales/
│ │ ├── en.json
│ │ └── vi.json
│ ├── audio.js # Web Audio API sounds
│ ├── localization.js
│ └── progress.js
│
└── styles/
└── theme.css # CSS variables (colors, fonts)
File Size Limits
- Maximum 200 lines per file before considering split
- Scenes with heavy logic → extract to
lib/game/ - Shared UI → extract to
components/
Svelte 5 Patterns
State Management
// Primitive state
let isPaused = $state(false);
// Class instances (NOT auto-proxied — use renderVersion pattern)
let grid = $state(null);
let player = $state(null);
let renderVersion = $state(0);
// Derived values depend on renderVersion to pick up class mutations
let cells = $derived((renderVersion, grid ? grid.getAllCells() : []));
Props
let { navigate, level = 1, lives = 3 } = $props();
Event Handlers
<!-- Window-level events -->
<svelte:window onkeydown={onKeyDown} />
<!-- Component events via callback props -->
<Button text="Resume" onclick={() => isPaused = false} />
Scene Navigation
// Parent passes navigate function as prop
navigate('Game', { level: 3, lives: 2 });
// App.svelte routes via {#key currentScene}
Class & Inheritance Patterns
Guard Hierarchy
Guard (abstract base)
├── StaticGuard — fixed lit cells
├── RotatingGuard — rotating beam + mirror reflection
├── BlinkingGuard — toggle on/off
├── MirrorGuard — redirects beams
├── PatrollingGuard — path movement + directional light
└── ChaserGuard — BFS pathfinding + detection radius
Base Guard contract:
- Constructor:
grid, row, col, type updateLight(allGuards?): Set lit cells on gridonTurnChange(allGuards?): Update state then call updateLight
Level Manager Pattern:
Use GUARD_REGISTRY factory pattern in level-manager.js:
const GUARD_REGISTRY = {
static: (grid, g) => new StaticGuard(...),
chaser: (grid, g) => new ChaserGuard(...),
// ... etc
};
This eliminates switch statements and allows easy guard type registration.
Key rule: Game engine classes are pure JS with no Svelte dependency. They operate on raw object references, not proxied state.
Grid & Coordinate System
- Origin: Top-left (0, 0)
- Row: 0 = top, increases downward
- Col: 0 = left, increases rightward
- Cell Data:
grid[row][col]={ isWall, isGoal, isLight } - Pixel Position:
row * cellSize,col * cellSize
Theme & Styling
All visual constants centralized in src/styles/theme.css as CSS variables:
--bg-dark,--bg-panel— backgrounds--grid-empty,--grid-wall,--grid-goal,--grid-lit— cell colors--guard-static,--guard-rotating,--guard-blinking,--guard-patrolling,--guard-mirror— guard colors--font-title,--font-body,--font-button— typography--text-primary,--text-accent,--text-danger— text colors
Component-scoped <style> blocks reference these variables. No inline color values.
Error Handling
- Try-catch: localStorage operations (
progress.js,localization.js) - Null checks:
if (!player || !grid) returnin input handlers - Validation: Grid bounds checked before all cell operations
- Fallback: Missing translations return key string itself
Localization Pattern
import { getText } from '../lib/localization.js';
const message = getText('level1Story'); // Returns EN or VI string
Key naming: camelCase matching JSON structure (levelSelectTitle, enemyTypesContent).
Git & Commit Conventions
- Format: Conventional commits (
feat:,fix:,refactor:,test:,docs:) - Branch:
mainfor production - Messages: Descriptive, explain "why" not just "what"
- Example:
fix: resolve Svelte 5 reactivity for class instances
New Patterns
Undo/Redo System
import { GameHistory } from '../lib/game/game-history.js';
const history = new GameHistory();
// Before player move
history.snapshot(player, guards, turnCount, princessAlerted, alertRadius);
// Z/Y key handlers
if (event.key === 'z') history.undo(player, guards);
if (event.key === 'y') history.redo(player, guards);
Mobile Touch Controls
import { TouchControls } from '../lib/game/touch-controls.js';
const touch = new TouchControls();
// In Game.svelte
<svelte:window ontouchstart={e => touch.onTouchStart(e)}
ontouchend={e => { const dir = touch.onTouchEnd(e); handleMove(dir); }} />
Audio Feedback
import * as audio from '../lib/audio.js';
audio.playMoveSound();
audio.playDetectionSound();
audio.toggleMute();
Code Review Checklist
- Follows naming conventions (PascalCase components, kebab-case JS modules)
- No dead code or commented-out blocks
- Class mutations followed by
renderVersion++ - No hardcoded colors (use CSS variables)
- Localization keys used for all user-facing strings
- File size under 200 lines
- Pure JS game logic has no Svelte imports
- Guard types registered in GUARD_REGISTRY (not switch statements)
- Touch input debounced/throttled if needed
- Audio context lazily initialized (autoplay policy compliance)