mirror of
https://github.com/tiennm99/nntv.git
synced 2026-10-16 02:15:39 +00:00
remove completed plan and report files
Plans for Svelte 5 rewrite and gameplay improvements have been fully implemented — removing the now-obsolete plan phases and research report.
This commit is contained in:
1 parent
7233310662
commit
286edc3c1d
13 files changed
-1196
No files matched your search
@@ -1,50 +0,0 @@
|
||||
# Phase 1: Scaffold Svelte 5 Project
|
||||
|
||||
**Priority:** Critical | **Status:** pending | **Effort:** Small
|
||||
|
||||
## Overview
|
||||
Set up a fresh Svelte 5 + Vite project structure alongside existing code. Replace Phaser dependency with Svelte.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Remove Phaser and related deps from `package.json`
|
||||
2. Install Svelte 5 + vite-plugin-svelte
|
||||
```bash
|
||||
npm install svelte && npm install -D @sveltejs/vite-plugin-svelte
|
||||
```
|
||||
3. Replace Vite config files (`vite/config.dev.mjs`, `vite/config.prod.mjs`) with single `vite.config.js` using svelte plugin
|
||||
4. Create directory structure:
|
||||
```
|
||||
src/lib/ # Pure JS game logic
|
||||
src/lib/locales/ # Locale JSON files
|
||||
src/lib/levels/ # Level data
|
||||
src/lib/game/ # Game logic classes
|
||||
src/components/ # Reusable Svelte components
|
||||
src/scenes/ # Scene components
|
||||
src/styles/ # CSS
|
||||
```
|
||||
5. Create `src/main.js` — mount Svelte app to `#app`
|
||||
6. Create minimal `src/App.svelte` — renders "Hello NNTV" placeholder
|
||||
7. Create `src/styles/theme.css` — port color/font constants from `theme.js` as CSS variables
|
||||
8. Update `index.html` — remove Phaser-specific meta, keep basic structure
|
||||
9. Update `package.json` scripts to use new vite config
|
||||
10. Verify `npm run dev` serves the placeholder app
|
||||
|
||||
## Files to Create
|
||||
- `vite.config.js`
|
||||
- `src/main.js` (replace existing)
|
||||
- `src/App.svelte`
|
||||
- `src/styles/theme.css`
|
||||
|
||||
## Files to Delete
|
||||
- `vite/config.dev.mjs`
|
||||
- `vite/config.prod.mjs`
|
||||
- `vite/` directory
|
||||
- `log.js`
|
||||
- `src/game/theme.js`
|
||||
|
||||
## Success Criteria
|
||||
- `npm run dev` opens browser with "Hello NNTV"
|
||||
- `npm run build` produces working dist/
|
||||
- Zero Phaser imports remain
|
||||
- CSS variables available globally
|
||||
@@ -1,63 +0,0 @@
|
||||
# Phase 2: Port Pure Game Logic
|
||||
|
||||
**Priority:** Critical | **Status:** pending | **Effort:** Medium
|
||||
|
||||
## Overview
|
||||
Move all framework-agnostic game logic into `src/lib/`. Strip Phaser imports. Separate data/logic from rendering.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Copy as-is** (just move files):
|
||||
- `locales/en.json` → `src/lib/locales/en.json`
|
||||
- `locales/vi.json` → `src/lib/locales/vi.json`
|
||||
- `levels/Levels.js` → `src/lib/levels/levels.js`
|
||||
- `progress.js` → `src/lib/progress.js`
|
||||
- `localization.js` → `src/lib/localization.js`
|
||||
|
||||
2. **Port TurnManager** → `src/lib/game/turn-manager.js`
|
||||
- Remove `import Phaser` (line 1)
|
||||
- Remove `this.scene` references
|
||||
- Make it accept callbacks instead of calling scene methods directly
|
||||
- Constructor takes `{ onLevelComplete, onDetected, getPlayerPos, getGrid, getGuards, getLightSystem }`
|
||||
|
||||
3. **Port GridSystem** → `src/lib/game/grid-system.js`
|
||||
- Keep: grid data model, isValidPosition, setWall/setGoal/setLight, gridToPixel, pixelToGrid
|
||||
- Remove: `scene.add.graphics()`, `render()` method, all Phaser graphics calls
|
||||
- Merge LightingSystem's `clearAllLight()` into GridSystem
|
||||
- Add `getCellState(row, col)` returning `{ isWall, isGoal, isLight }`
|
||||
|
||||
4. **Port Guards** → `src/lib/game/guards.js`
|
||||
- Keep: all guard logic (updateLight, onTurnChange, patrol path, rotation, blinking)
|
||||
- Remove: `createSprite()`, `update()` (sprite positioning), `scene.add.circle/line`
|
||||
- Remove: `destroy()` sprite cleanup (no sprites to destroy)
|
||||
- Each guard exposes: `{ row, col, color, type, direction, isOn }` for rendering
|
||||
|
||||
5. **Port Player** → `src/lib/game/player.js`
|
||||
- Keep: `moveTo()`, `move()`, `isInLitCell()`, `isAtGoal()`
|
||||
- Remove: `createSprite()`, `update()` (sprite), tweens
|
||||
- Pure data: `{ row, col }` position tracking
|
||||
|
||||
6. **Port LevelManager** → `src/lib/game/level-manager.js`
|
||||
- Rewrite as a pure function: `loadLevel(levelId, grid)` → returns `{ player, guards, isFinalLevel }`
|
||||
- No scene references
|
||||
- Creates guard instances, sets up grid walls/goals
|
||||
|
||||
## Files to Create
|
||||
- `src/lib/game/grid-system.js`
|
||||
- `src/lib/game/guards.js`
|
||||
- `src/lib/game/player.js`
|
||||
- `src/lib/game/turn-manager.js`
|
||||
- `src/lib/game/level-manager.js`
|
||||
- `src/lib/localization.js`
|
||||
- `src/lib/progress.js`
|
||||
- `src/lib/locales/en.json`
|
||||
- `src/lib/locales/vi.json`
|
||||
- `src/lib/levels/levels.js`
|
||||
|
||||
## Files to Delete (after all phases complete)
|
||||
- Entire `src/game/` directory
|
||||
|
||||
## Success Criteria
|
||||
- All game logic files import zero Phaser code
|
||||
- Guard logic can be tested independently (no DOM/canvas needed)
|
||||
- `loadLevel()` returns a complete game state object
|
||||
@@ -1,87 +0,0 @@
|
||||
# Phase 3: Build Game Board + Sprite Components
|
||||
|
||||
**Priority:** Critical | **Status:** pending | **Effort:** Medium
|
||||
|
||||
## Overview
|
||||
Create Svelte components that render the game grid, player, and guards using HTML/CSS instead of Phaser canvas.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Button.svelte** — reusable styled button
|
||||
- Props: `text`, `onclick`, `small` (boolean for compact variant)
|
||||
- Styled with CSS variables from theme.css
|
||||
- Hover/active states via CSS
|
||||
|
||||
2. **GameBoard.svelte** — renders the grid
|
||||
- Props: `grid` (2D array of cell states), `rows`, `cols`, `cellSize`
|
||||
- CSS Grid layout: `grid-template-columns: repeat(cols, cellSize)`
|
||||
- Each cell is a `<div>` with class based on state: `.wall`, `.goal`, `.lit`, `.empty`
|
||||
- Click handler on cells for tap-to-move (emits `oncellclick` with row, col)
|
||||
- Cell colors from CSS variables
|
||||
|
||||
3. **PlayerSprite.svelte** — renders the player
|
||||
- Props: `row`, `col`, `cellSize`, `gridOffset`
|
||||
- Positioned absolutely over the grid
|
||||
- CSS `transition: top 100ms, left 100ms` for smooth movement (replaces Phaser tweens)
|
||||
- Black circle via `border-radius: 50%`
|
||||
|
||||
4. **GuardSprite.svelte** — renders a guard
|
||||
- Props: `guard` object `{ row, col, type, direction, isOn }`
|
||||
- Props: `cellSize`, `gridOffset`
|
||||
- Positioned absolutely over the grid
|
||||
- Circle colored by type (CSS class: `.guard-static`, `.guard-rotating`, etc.)
|
||||
- Direction indicator for rotating/patrolling: rotated CSS triangle or line
|
||||
- Blinking guard dim state: reduced opacity via class toggle
|
||||
|
||||
5. **GameHud.svelte** — top HUD bar
|
||||
- Props: `lives`, `level`, `turns`
|
||||
- Events: `onpause`, `onmenu`
|
||||
- Fixed position, not part of game board
|
||||
- Uses Button.svelte for pause/menu
|
||||
|
||||
6. **DetectionPopup.svelte** — detection overlay
|
||||
- Props: `visible`
|
||||
- Events: `onplayagain`
|
||||
- Dark overlay with centered message + button
|
||||
- Uses `transition:fade`
|
||||
|
||||
7. **PauseMenu.svelte** — pause overlay
|
||||
- Props: `visible`
|
||||
- Events: `onresume`, `onrestart`, `onmainmenu`
|
||||
- Dark overlay with 3 buttons
|
||||
- Uses `transition:fade`
|
||||
|
||||
## Rendering Strategy
|
||||
|
||||
```
|
||||
┌─ GameBoard.svelte (CSS Grid) ─────────┐
|
||||
│ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │
|
||||
│ │ │ │ │ │ │ │ │ │ │ │ │ ... │
|
||||
│ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ │
|
||||
│ (each cell = div with bg color) │
|
||||
│ │
|
||||
│ PlayerSprite (absolute positioned) │
|
||||
│ GuardSprite × N (absolute positioned)│
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## CSS Class Map
|
||||
|
||||
| Game State | CSS Class | Color Variable |
|
||||
|---|---|---|
|
||||
| Empty cell | `.cell` | `--grid-empty` |
|
||||
| Wall | `.cell.wall` | `--grid-wall` |
|
||||
| Goal | `.cell.goal` | `--grid-goal` |
|
||||
| Lit | `.cell.lit` | `--grid-lit` |
|
||||
| Static guard | `.guard.static` | `--guard-static` |
|
||||
| Rotating guard | `.guard.rotating` | `--guard-rotating` |
|
||||
| Blinking guard (on) | `.guard.blinking` | `--guard-blinking` |
|
||||
| Blinking guard (off) | `.guard.blinking.off` | `--guard-blinking-off` |
|
||||
| Patrolling guard | `.guard.patrolling` | `--guard-patrolling` |
|
||||
|
||||
## Success Criteria
|
||||
- GameBoard renders correct grid for any level (6x6 through 10x10)
|
||||
- Player circle moves smoothly with CSS transitions
|
||||
- Guards display correct colors and direction indicators
|
||||
- All overlays (detection, pause) fade in/out
|
||||
- Components accept plain JS data — no Phaser objects
|
||||
@@ -1,85 +0,0 @@
|
||||
# Phase 4: Build Scene Components
|
||||
|
||||
**Priority:** High | **Status:** pending | **Effort:** Medium
|
||||
|
||||
## Overview
|
||||
Replace all Phaser Scene classes with Svelte components. App.svelte acts as the scene router using reactive state + transitions.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **App.svelte** — scene router
|
||||
- Module-level `$state` for `currentScene` and `sceneData`
|
||||
- `{#if currentScene === 'MainMenu'}` blocks for each scene
|
||||
- Each block wrapped in `transition:fade={{ duration: 300 }}`
|
||||
- Export `navigate(scene, data)` function for child components
|
||||
- Initialize with `currentScene = 'MainMenu'`
|
||||
|
||||
2. **MainMenu.svelte**
|
||||
- Starfield background (random positioned dots via `{#each}`)
|
||||
- Title, ninja icon, 4 buttons (Start, Level Select, Guide, Settings)
|
||||
- Buttons call `navigate('StoryIntro')` etc.
|
||||
|
||||
3. **StoryIntro.svelte**
|
||||
- Dark background, scrolling story text
|
||||
- CSS animation `@keyframes scroll` moves text upward
|
||||
- Skip button → navigates to LevelIntro
|
||||
- Auto-advance after scroll completes
|
||||
|
||||
4. **LevelIntro.svelte**
|
||||
- Props via sceneData: `{ level, lives }`
|
||||
- Shows level number, name (from levels.js), story text (from localization)
|
||||
- Continue button → navigates to Game
|
||||
|
||||
5. **LevelSelect.svelte**
|
||||
- Grid of 12 level buttons (CSS Grid, 4 columns)
|
||||
- Reads progress from `getProgress()`
|
||||
- Locked/unlocked/completed states via CSS classes
|
||||
- Checkmark on completed levels
|
||||
- Click → navigate to LevelIntro with level data
|
||||
|
||||
6. **GameOver.svelte**
|
||||
- Props via sceneData: `{ level, isLastLevel }`
|
||||
- Different message for final level vs normal game over
|
||||
- Try Again + Main Menu buttons
|
||||
|
||||
7. **Settings.svelte**
|
||||
- Language toggle (EN/VI)
|
||||
- Calls `setLanguage()` from localization.js
|
||||
- Re-renders text reactively via `$derived` on language
|
||||
|
||||
8. **Guide.svelte**
|
||||
- Scrollable content area (native CSS `overflow-y: auto`)
|
||||
- Sections: objectives, controls, enemy types
|
||||
- Back button
|
||||
|
||||
## Scene Navigation Pattern
|
||||
|
||||
```svelte
|
||||
<!-- App.svelte -->
|
||||
<script>
|
||||
let currentScene = $state('MainMenu');
|
||||
let sceneData = $state({});
|
||||
|
||||
function navigate(scene, data = {}) {
|
||||
sceneData = data;
|
||||
currentScene = scene;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if currentScene === 'MainMenu'}
|
||||
<div transition:fade>
|
||||
<MainMenu {navigate} />
|
||||
</div>
|
||||
{:else if currentScene === 'Game'}
|
||||
<div transition:fade>
|
||||
<Game {navigate} level={sceneData.level} lives={sceneData.lives} />
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
- All 8 scenes render correctly
|
||||
- Scene transitions fade smoothly
|
||||
- Language switching updates all visible text
|
||||
- Level select shows correct progress state
|
||||
- Story intro scrolls and auto-advances
|
||||
@@ -1,96 +0,0 @@
|
||||
# Phase 5: Wire Game Scene + Input
|
||||
|
||||
**Priority:** Critical | **Status:** pending | **Effort:** Large
|
||||
|
||||
## Overview
|
||||
The main Game.svelte scene — connects game logic (grid, guards, turns, player) to the visual components and handles keyboard/click input.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Game.svelte** — main game orchestrator
|
||||
- `$state` for: grid, player, guards, lives, level, turns, isPaused, detected, isFinalLevel
|
||||
- `onMount`: call `loadLevel()` to get initial state, set up keyboard listener
|
||||
- `onDestroy`: clean up keyboard listener
|
||||
- Render: GameBoard + PlayerSprite + GuardSprites + GameHud + overlays
|
||||
|
||||
2. **Keyboard input**
|
||||
```js
|
||||
function onKeyDown(e) {
|
||||
if (isPaused || !inputEnabled) return;
|
||||
const dirMap = {
|
||||
ArrowUp: 'up', ArrowDown: 'down', ArrowLeft: 'left', ArrowRight: 'right',
|
||||
w: 'up', s: 'down', a: 'left', d: 'right',
|
||||
};
|
||||
const dir = dirMap[e.key];
|
||||
if (dir) handleMove(dir);
|
||||
}
|
||||
```
|
||||
- Bind to `window` via `svelte:window` directive: `<svelte:window onkeydown={onKeyDown} />`
|
||||
|
||||
3. **Click-to-move input**
|
||||
- GameBoard emits `oncellclick(row, col)`
|
||||
- Game.svelte checks if cell is adjacent to player, calls `handleMove()`
|
||||
|
||||
4. **Turn flow** (handleMove function)
|
||||
```
|
||||
1. player.move(direction)
|
||||
2. If moved successfully:
|
||||
a. Check if player at goal → handleLevelComplete()
|
||||
b. clearAllLight()
|
||||
c. Each guard.onTurnChange()
|
||||
d. grid.render state update (triggers Svelte reactivity)
|
||||
e. Check if player in lit cell → showDetection()
|
||||
f. turns++
|
||||
```
|
||||
|
||||
5. **Level 12 special mechanic**
|
||||
- On each move, check `isFinalLevel && Manhattan distance to goal <= 2`
|
||||
- If triggered: set ALL non-wall cells to `isLight = true`
|
||||
- Show princess detected message
|
||||
- This looks like normal gameplay — player won't know it's impossible until they experience it
|
||||
|
||||
6. **Detection flow**
|
||||
- Set `detected = true` → shows DetectionPopup
|
||||
- On "Play Again": `lives--`, if lives > 0 reload level, else navigate to GameOver
|
||||
|
||||
7. **Level complete flow**
|
||||
- Call `completeLevel()` from progress.js
|
||||
- Brief CSS flash animation on board
|
||||
- Navigate to LevelIntro for next level (or GameOver for final)
|
||||
|
||||
8. **Pause flow**
|
||||
- Toggle `isPaused` → shows PauseMenu
|
||||
- Resume / Restart / Main Menu actions
|
||||
|
||||
## Reactive State Shape
|
||||
|
||||
```js
|
||||
// In Game.svelte
|
||||
let grid = $state(null); // GridSystem instance
|
||||
let player = $state(null); // Player instance
|
||||
let guards = $state([]); // Guard instances
|
||||
let lives = $state(3);
|
||||
let currentLevel = $state(1);
|
||||
let turns = $state(0);
|
||||
let isPaused = $state(false);
|
||||
let detected = $state(false);
|
||||
let isFinalLevel = $state(false);
|
||||
|
||||
// Derived for rendering
|
||||
let cells = $derived(grid ? grid.getAllCells() : []);
|
||||
let playerPos = $derived(player ? { row: player.row, col: player.col } : null);
|
||||
let guardData = $derived(guards.map(g => ({
|
||||
row: g.row, col: g.col, type: g.constructor.name,
|
||||
direction: g.direction, isOn: g.isOn,
|
||||
})));
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
- Arrow keys / WASD move player through grid
|
||||
- Click adjacent cell moves player
|
||||
- Guards update correctly each turn (static lit, rotating beam, blinking toggle, patrol movement)
|
||||
- Detection popup shows when stepping on lit cell
|
||||
- Level completes when reaching goal
|
||||
- Level 12 lights up entire map near goal
|
||||
- Pause menu works
|
||||
- Lives decrement and game over triggers at 0
|
||||
@@ -1,60 +0,0 @@
|
||||
# Phase 6: Polish + Verify
|
||||
|
||||
**Priority:** High | **Status:** pending | **Effort:** Small
|
||||
|
||||
## Overview
|
||||
Final pass — visual polish, cleanup old Phaser code, update docs, verify all 12 levels.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Delete old Phaser code**
|
||||
- Remove entire `src/game/` directory
|
||||
- Remove `vite/` directory
|
||||
- Remove `log.js`
|
||||
- Remove `public/style.css` (styles now in Svelte components + theme.css)
|
||||
- Remove `phaser` from package.json dependencies
|
||||
- Remove `terser` from devDependencies (Vite handles minification)
|
||||
- Run `npm install` to clean lockfile
|
||||
|
||||
2. **Visual polish**
|
||||
- Verify dark theme renders correctly on all scenes
|
||||
- Test responsive scaling (Scale.FIT equivalent via CSS `max-width` + `aspect-ratio`)
|
||||
- Ensure grid is centered on all level sizes (6x6 through 10x10)
|
||||
- Test direction indicators on rotating/patrolling guards
|
||||
- Verify blinking guard color toggle
|
||||
|
||||
3. **Test all 12 levels**
|
||||
- Level 1: move freely, reach goal
|
||||
- Levels 2-5: static guard lit cells block correct positions
|
||||
- Level 6: rotating guard beam sweeps and blocks cells
|
||||
- Levels 7-8: mixed guard interactions
|
||||
- Level 9: blinking guard toggles on/off
|
||||
- Level 10: patrolling guard moves along path
|
||||
- Level 11: all 4 guard types simultaneously
|
||||
- Level 12: approach goal → entire map lights up (unbeatable)
|
||||
|
||||
4. **Test game flows**
|
||||
- Lives system: lose 3 lives → game over screen
|
||||
- Level progression: complete level → intro → next level
|
||||
- Level select: locked/unlocked/completed states correct
|
||||
- Progress persistence: refresh browser, progress retained
|
||||
- Language switch: EN ↔ VI updates all text
|
||||
- Pause menu: resume, restart, main menu all work
|
||||
|
||||
5. **Update docs**
|
||||
- Update `docs/system-architecture.md` — Svelte 5 architecture
|
||||
- Update `docs/codebase-summary.md` — new file listing
|
||||
- Update `docs/code-standards.md` — Svelte conventions
|
||||
- Update `README.md` — remove Phaser references, add Svelte info
|
||||
|
||||
6. **Build verification**
|
||||
- `npm run build` produces working dist/
|
||||
- Serve dist/ locally, test full game
|
||||
- Check bundle size (target: <50KB app code, down from ~1MB with Phaser)
|
||||
|
||||
## Success Criteria
|
||||
- Zero Phaser code remains
|
||||
- All 12 levels playable with correct guard behavior
|
||||
- Level 12 secret mechanic works
|
||||
- Bundle size < 50KB (excluding assets)
|
||||
- Docs updated to reflect new stack
|
||||
@@ -1,124 +0,0 @@
|
||||
---
|
||||
title: Rewrite NNTV from Phaser 3 to Svelte 5
|
||||
status: ready
|
||||
created: 2026-04-12
|
||||
branch: main
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
---
|
||||
|
||||
# Rewrite NNTV: Phaser 3 → Svelte 5 + Vite
|
||||
|
||||
## Overview
|
||||
|
||||
Replace Phaser 3 game engine (~1MB) with plain Svelte 5 + Vite for a turn-based grid puzzle game that only uses rectangles, circles, and text. All game logic (guards, turns, levels, lighting) ports as-is. Only the rendering/UI layer changes.
|
||||
|
||||
## Architecture: Before → After
|
||||
|
||||
```
|
||||
BEFORE (Phaser 3) AFTER (Svelte 5)
|
||||
───────────────── ────────────────
|
||||
Phaser.Game config → App.svelte (scene router)
|
||||
Phaser.Scene classes → Svelte components per scene
|
||||
scene.add.circle/rect/text → HTML/CSS elements
|
||||
scene.input.keyboard → window keydown events
|
||||
scene.tweens.add → CSS transitions
|
||||
this.scene.start() → reactive scene state
|
||||
camera.fadeIn/fadeOut → svelte/transition (fade)
|
||||
Phaser physics (unused) → DELETE
|
||||
```
|
||||
|
||||
## File Migration Map
|
||||
|
||||
| Current File | Action | New Location |
|
||||
|---|---|---|
|
||||
| `locales/en.json`, `vi.json` | PORT AS-IS | `src/lib/locales/` |
|
||||
| `levels/Levels.js` | PORT AS-IS | `src/lib/levels/levels.js` |
|
||||
| `progress.js` | PORT AS-IS | `src/lib/progress.js` |
|
||||
| `localization.js` | PORT AS-IS | `src/lib/localization.js` |
|
||||
| `TurnManager.js` | PORT (remove Phaser import) | `src/lib/game/turn-manager.js` |
|
||||
| `GridSystem.js` | SPLIT: data logic → pure JS, rendering → Svelte | `src/lib/game/grid-system.js` + `GameBoard.svelte` |
|
||||
| `Guard.js` | SPLIT: logic → pure JS, rendering → Svelte | `src/lib/game/guards.js` + `GuardSprite.svelte` |
|
||||
| `Player.js` | SPLIT: logic → pure JS, rendering → Svelte | `src/lib/game/player.js` + `PlayerSprite.svelte` |
|
||||
| `LightingSystem.js` | MERGE into grid-system.js | `src/lib/game/grid-system.js` |
|
||||
| `LevelManager.js` | REWRITE as pure function | `src/lib/game/level-manager.js` |
|
||||
| `theme.js` | REWRITE as CSS variables | `src/styles/theme.css` |
|
||||
| All scenes/*.js | REWRITE as Svelte components | `src/scenes/*.svelte` |
|
||||
| `main.js` | REWRITE | `src/main.js` + `src/App.svelte` |
|
||||
| `LevelTester.js` | DELETE | — |
|
||||
|
||||
## New Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.js # Mount Svelte app
|
||||
├── App.svelte # Scene router + transitions
|
||||
├── styles/
|
||||
│ └── theme.css # CSS variables (colors, fonts)
|
||||
├── lib/
|
||||
│ ├── localization.js # getText, setLanguage (ported)
|
||||
│ ├── progress.js # localStorage persistence (ported)
|
||||
│ ├── locales/
|
||||
│ │ ├── en.json # English (ported)
|
||||
│ │ └── vi.json # Vietnamese (ported)
|
||||
│ ├── levels/
|
||||
│ │ └── levels.js # Level data (ported)
|
||||
│ └── game/
|
||||
│ ├── grid-system.js # Grid data model + lighting (pure JS)
|
||||
│ ├── guards.js # Guard classes, logic only (pure JS)
|
||||
│ ├── player.js # Player position + movement (pure JS)
|
||||
│ ├── turn-manager.js # Turn logic (pure JS)
|
||||
│ └── level-manager.js # Level loading (pure JS)
|
||||
├── components/
|
||||
│ ├── GameBoard.svelte # CSS Grid rendering of cells
|
||||
│ ├── PlayerSprite.svelte # Player circle element
|
||||
│ ├── GuardSprite.svelte # Guard circle + direction indicator
|
||||
│ ├── GameHud.svelte # Lives, level, turns, pause/menu buttons
|
||||
│ ├── DetectionPopup.svelte # "You've been detected" overlay
|
||||
│ ├── PauseMenu.svelte # Pause overlay
|
||||
│ └── Button.svelte # Reusable styled button
|
||||
└── scenes/
|
||||
├── MainMenu.svelte
|
||||
├── StoryIntro.svelte
|
||||
├── LevelSelect.svelte
|
||||
├── LevelIntro.svelte
|
||||
├── Game.svelte # Main game scene
|
||||
├── GameOver.svelte
|
||||
├── Settings.svelte
|
||||
└── Guide.svelte
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Plain Svelte + Vite** (not SvelteKit) — no routing needed for SPA game
|
||||
2. **Svelte 5 runes** (`$state`, `$derived`, `$effect`) — reactive game state
|
||||
3. **CSS Grid** for game board — native, responsive, zero dependencies
|
||||
4. **CSS transitions** for player movement — replace Phaser tweens
|
||||
5. **`svelte/transition`** for scene switching — fade between screens
|
||||
6. **Pure JS game logic** — all guard/turn/level logic stays framework-agnostic
|
||||
7. **Game state as module-level `$state`** in `.svelte.js` files — shared across components
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Status | File |
|
||||
|---|-------|--------|------|
|
||||
| 1 | Scaffold Svelte 5 project | pending | [phase-01](phase-01-scaffold-project.md) |
|
||||
| 2 | Port pure game logic | pending | [phase-02](phase-02-port-game-logic.md) |
|
||||
| 3 | Build game board + sprites | pending | [phase-03](phase-03-game-board-components.md) |
|
||||
| 4 | Build scene components | pending | [phase-04](phase-04-scene-components.md) |
|
||||
| 5 | Wire game scene + input | pending | [phase-05](phase-05-game-scene-input.md) |
|
||||
| 6 | Polish + verify | pending | [phase-06](phase-06-polish-verify.md) |
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|---|---|---|
|
||||
| Grid reactivity perf on 10x10 | Low | Svelte fine-grained updates; only changed cells re-render |
|
||||
| CSS transition timing mismatch with game turns | Medium | Use fixed duration, test each guard type |
|
||||
| Level 12 special mechanic (lightUpEntireMap) | Low | Direct DOM class toggle on all cells |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Svelte 5 (latest)
|
||||
- Vite 6.x (already used)
|
||||
- No other runtime dependencies
|
||||
@@ -1,54 +0,0 @@
|
||||
## Phase 1: Wait Action
|
||||
|
||||
**Priority:** P1 | **Status:** Pending | **Effort:** 15min
|
||||
|
||||
### Overview
|
||||
|
||||
Spacebar (and tap-on-player for mobile) skips the player's move but still advances guards.
|
||||
Creates timing puzzles — player must wait for a safe window.
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. User presses Space / taps player cell
|
||||
2. `handleMove` is NOT called; instead new `handleWait()` fires
|
||||
3. `handleWait` calls `turnManager.nextTurn(grid, player, guards)` without moving player
|
||||
4. Same detection/completion checks run
|
||||
5. `renderVersion++` triggers re-render
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`src/scenes/Game.svelte`** (input handler only)
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
- [ ] 1. In `onKeyDown`, add `' '` (Space) key check before direction map lookup
|
||||
- [ ] 2. When Space detected, call `handleWait()` and `e.preventDefault()`
|
||||
- [ ] 3. Create `handleWait()` function:
|
||||
```js
|
||||
function handleWait() {
|
||||
if (!player || !grid) return;
|
||||
const result = turnManager.nextTurn(grid, player, guards);
|
||||
if (isFinalLevel && checkFinalLevel()) { renderVersion++; return; }
|
||||
renderVersion++;
|
||||
if (result.levelComplete) handleLevelComplete();
|
||||
else if (result.detected) detected = true;
|
||||
}
|
||||
```
|
||||
- [ ] 4. In `onCellClick`, if clicked cell === player position, call `handleWait()`
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Wait on level 12 still triggers princess detection expansion (covered — same `checkFinalLevel` path)
|
||||
- Wait when already at goal: `nextTurn` returns `levelComplete: true` (correct)
|
||||
- Wait counts as a turn for move counter (correct — `turnManager.turnCount++` happens in `nextTurn`)
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Space key advances turn without moving player
|
||||
- Guards rotate/patrol/blink as normal
|
||||
- Turn counter increments
|
||||
- Detection still works if guard light reaches player after wait
|
||||
|
||||
### Rollback
|
||||
|
||||
Revert single commit. No schema changes.
|
||||
@@ -1,89 +0,0 @@
|
||||
## Phase 2: Star Rating + Par Moves
|
||||
|
||||
**Priority:** P1 | **Status:** Pending | **Effort:** 2h
|
||||
|
||||
### Overview
|
||||
|
||||
Each level gets a `parMoves` value. On completion, show 1-3 star rating.
|
||||
Persist best stars + best moves in localStorage. Display stars on level select.
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Level complete → turnManager.turnCount vs level.parMoves → star rating
|
||||
→ completeLevel(levelNum, totalLevels, moves) → localStorage update
|
||||
→ LevelSelect reads stars from progress
|
||||
```
|
||||
|
||||
### Star Calculation
|
||||
|
||||
| Condition | Stars |
|
||||
|-----------|-------|
|
||||
| moves <= parMoves | 3 |
|
||||
| moves <= parMoves + 3 | 2 |
|
||||
| completed | 1 |
|
||||
|
||||
### Files to Modify
|
||||
|
||||
#### 1. `src/lib/levels/levels.js`
|
||||
- [ ] Add `parMoves` field to each level definition
|
||||
- [ ] Placeholder values (will be finalized in Phase 3 after redesign):
|
||||
- L1: 10, L2: 12, L3: 14, L4: 14, L5: 14, L6: 16
|
||||
- L7: 16, L8: 18, L9: 18, L10: 20, L11: 22, L12: 99 (unbeatable)
|
||||
|
||||
#### 2. `src/lib/progress.js`
|
||||
- [ ] Extend schema: add `levelStars: {}` and `levelBestMoves: {}` to DEFAULT_PROGRESS
|
||||
- [ ] `getProgress()`: merge missing keys for backwards compat
|
||||
- [ ] Update `completeLevel(levelNum, totalLevels, moves, parMoves)`:
|
||||
- Calculate stars
|
||||
- Store best stars (only if better than existing)
|
||||
- Store best moves (only if fewer than existing)
|
||||
- Return `{ ...progress, stars }` so caller can display
|
||||
|
||||
#### 3. `src/scenes/Game.svelte`
|
||||
- [ ] Import `LEVELS` to access `parMoves` for current level
|
||||
- [ ] Pass `turnManager.turnCount` and `parMoves` to `completeLevel()`
|
||||
- [ ] After level complete, show star rating overlay before navigating
|
||||
- [ ] Add `LevelComplete` popup component (inline or separate small component)
|
||||
- Shows 1-3 stars with animation
|
||||
- Shows move count vs par
|
||||
- "Next Level" button
|
||||
|
||||
#### 4. `src/scenes/LevelSelect.svelte`
|
||||
- [ ] Read `progress.levelStars` in addition to `completedLevels`
|
||||
- [ ] Replace checkmark with star display (1-3 stars shown as filled/empty)
|
||||
- [ ] Show best move count under level number
|
||||
|
||||
#### 5. `src/components/GameHud.svelte`
|
||||
- [ ] No changes needed — already shows turn count
|
||||
|
||||
### localStorage Schema (backwards compatible)
|
||||
|
||||
```js
|
||||
// Old format still works — missing keys get defaults
|
||||
{
|
||||
maxLevel: 5,
|
||||
completedLevels: [1, 2, 3, 4],
|
||||
// NEW — absent in old saves, defaults to {}
|
||||
levelStars: { "1": 3, "2": 2, "3": 1 },
|
||||
levelBestMoves: { "1": 8, "2": 14, "3": 20 }
|
||||
}
|
||||
```
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Old localStorage without `levelStars`/`levelBestMoves` → defaults to `{}`
|
||||
- Level 12 unbeatable → `parMoves: 99` never matters
|
||||
- Player completes level with worse score → keep existing best
|
||||
- `completeLevel` called with no moves param (old code path) → skip star calc
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Star rating appears on level complete
|
||||
- Best stars persist in localStorage
|
||||
- Level select shows stars per level
|
||||
- Old saves don't break (backwards compat)
|
||||
|
||||
### Rollback
|
||||
|
||||
Revert commit. Old localStorage data unaffected (new keys simply disappear).
|
||||
@@ -1,89 +0,0 @@
|
||||
## Phase 3: Level Redesign + Chaser Guard
|
||||
|
||||
**Priority:** P1 | **Status:** Pending | **Effort:** 3h
|
||||
|
||||
### Overview
|
||||
|
||||
Redesign existing 12 levels for more challenge. Add **Chaser guard** type.
|
||||
Finalize `parMoves` values after redesign. Level 12 remains unbeatable.
|
||||
|
||||
### New Guard Type: ChaserGuard
|
||||
|
||||
**Behavior:** Dormant until player enters Manhattan distance <= `detectionRadius` (default 3).
|
||||
Once triggered, moves 1 cell toward player's **last known position** each turn.
|
||||
Lights its own cell + adjacent cell in movement direction. Returns to start if reaches
|
||||
last known position and player not visible.
|
||||
|
||||
**Why Chaser over Sentry:** Chaser creates dynamic panic — player must change route.
|
||||
Sentry is just a fancy static guard (cone vision = static pattern). Chaser adds genuine
|
||||
reactive gameplay with minimal code.
|
||||
|
||||
#### Implementation in `src/lib/game/guards.js`
|
||||
|
||||
- [ ] Add `ChaserGuard` class extending `Guard`:
|
||||
```
|
||||
Properties: detectionRadius, startRow, startCol, targetRow, targetCol,
|
||||
isChasing, lastKnownRow, lastKnownCol
|
||||
Methods:
|
||||
- onTurnChange(allGuards, player): check distance to player
|
||||
- If within radius and not blocked by wall: set isChasing, record lastKnownPos
|
||||
- If chasing: move 1 step toward lastKnownPos (prefer row then col)
|
||||
- If reached lastKnownPos and player not in radius: return to start
|
||||
- updateLight(): light own cell + 1 cell in facing direction
|
||||
```
|
||||
- [ ] Key: `onTurnChange` needs player reference — update signature
|
||||
|
||||
#### Turn Manager Change (`src/lib/game/turn-manager.js`)
|
||||
|
||||
- [ ] Pass `player` to `guard.onTurnChange(guards, player)` — non-breaking since
|
||||
existing guards ignore extra params
|
||||
|
||||
#### Level Manager Change (`src/lib/game/level-manager.js`)
|
||||
|
||||
- [ ] Add `case 'chaser'` to guard factory switch
|
||||
|
||||
#### Guard Sprite (`src/components/GuardSprite.svelte`)
|
||||
|
||||
- [ ] Add chaser visual (red-tinted sprite, or different emoji/icon)
|
||||
- [ ] Show "alert" state when `isChasing` is true
|
||||
|
||||
### Level Redesign Principles
|
||||
|
||||
1. **Tighter corridors** — reduce open space, force specific paths
|
||||
2. **More guards per level** — current levels are sparse
|
||||
3. **Timing windows** — require waiting (Phase 1) for safe passage
|
||||
4. **Chaser placement** — put chasers near obvious paths to force detours
|
||||
5. **Level 12** — keep walls + guard placement, only verify light wave still covers all paths
|
||||
|
||||
### Level-by-Level Notes
|
||||
|
||||
- [ ] **L1-2** (tutorial): Keep simple but add 1 more wall each to reduce trivial paths
|
||||
- [ ] **L3-4** (rotating intro): Add extra static guard, narrow corridors
|
||||
- [ ] **L5-6** (blinking): Tighter timing — reduce safe windows from 2 turns to 1
|
||||
- [ ] **L7-8** (patrolling): Add walls to force player into patrol paths, require waiting
|
||||
- [ ] **L9** (decoy): Add chaser near the "obvious" short path — punishes rushing
|
||||
- [ ] **L10** (mirrors): Add more mirror bounces, tighter spaces
|
||||
- [ ] **L11** (throne room): Add chaser + extra patrolling guard
|
||||
- [ ] **L12** (princess): DO NOT CHANGE core mechanic. May add 1-2 more guards in upper sections
|
||||
- [ ] **Finalize parMoves** for each level after redesign (playtest manually)
|
||||
|
||||
### Edge Cases / Risks
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Level becomes unsolvable | Medium | High | Playtest each level; keep at least 1 valid path |
|
||||
| Chaser pathfinding hits wall | Medium | Medium | Simple Manhattan step; skip move if blocked by wall |
|
||||
| Level 12 becomes beatable | Low | High | Verify: expanding light wave from (9,9) covers all reachable cells within player's max possible steps |
|
||||
| ChaserGuard makes levels too hard | Medium | Low | Tune detectionRadius per level (2-4 range) |
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- All levels 1-11 are solvable but require thought
|
||||
- At least 3 levels benefit from wait action
|
||||
- Chaser guard creates noticeable tension when triggered
|
||||
- Level 12 remains provably unbeatable
|
||||
- parMoves set to optimal-path length for each level
|
||||
|
||||
### Rollback
|
||||
|
||||
Revert commit restores old level definitions and removes ChaserGuard class.
|
||||
@@ -1,49 +0,0 @@
|
||||
## Phase 4: Guard Vision Preview (Optional)
|
||||
|
||||
**Priority:** P3 | **Status:** Pending | **Effort:** 45min
|
||||
|
||||
### Overview
|
||||
|
||||
Show semi-transparent overlay of cells that will be lit NEXT turn.
|
||||
Helps player plan without trial-and-error. Toggle via button or hotkey.
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. Before rendering, simulate next turn's guard positions/lights on a **cloned grid**
|
||||
2. Diff current lit cells vs next-turn lit cells
|
||||
3. Render "preview" cells with distinct CSS class (e.g., `cell-preview` with 20% opacity warning color)
|
||||
|
||||
### Implementation
|
||||
|
||||
#### `src/lib/game/turn-manager.js`
|
||||
- [ ] Add `previewNextTurn(grid, guards)` method:
|
||||
- Deep-clone guard states (row, col, direction, isOn, currentPathIndex, isReversing)
|
||||
- Create temporary grid clone
|
||||
- Run `onTurnChange` on clones
|
||||
- Return Set of `"row,col"` strings for next-turn lit cells
|
||||
|
||||
#### `src/scenes/Game.svelte`
|
||||
- [ ] Add `showPreview` toggle state (default false)
|
||||
- [ ] Bind `v` key or HUD button to toggle
|
||||
- [ ] Compute `previewCells` derived from `turnManager.previewNextTurn()` when toggle on
|
||||
- [ ] Pass `previewCells` to `GameBoard`
|
||||
|
||||
#### `src/components/GameBoard.svelte`
|
||||
- [ ] Accept optional `previewCells` prop
|
||||
- [ ] Add `cell-preview` CSS class to cells in preview set
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Chaser guard preview needs player position — pass current player pos to preview
|
||||
- Level 12 expanding wave — preview should show next ring expansion
|
||||
- Performance: cloning 5-10 guards per frame is trivial for grid sizes <= 10x10
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Toggle shows/hides next-turn danger zones
|
||||
- Preview visually distinct from current lit cells (different opacity/color)
|
||||
- No performance impact on turn processing
|
||||
|
||||
### Rollback
|
||||
|
||||
Revert commit. Preview is purely additive UI — no game logic changes.
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
title: "Gameplay Improvements: Wait Action, Star Rating, Level Redesign"
|
||||
description: "Add wait mechanic, star/par rating system, and redesign levels for challenge"
|
||||
status: done
|
||||
priority: P1
|
||||
effort: 6h
|
||||
branch: main
|
||||
tags: [gameplay, levels, ux]
|
||||
created: 2026-04-13
|
||||
---
|
||||
|
||||
# Gameplay Improvements Plan
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Effort | Status | Files Modified |
|
||||
|---|-------|--------|--------|----------------|
|
||||
| 1 | [Wait Action](phase-01-wait-action.md) | 15min | Done | Game.svelte |
|
||||
| 2 | [Star Rating + Par Moves](phase-02-star-rating.md) | 2h | Done | levels.js, progress.js, Game.svelte, LevelSelect.svelte, LevelCompletePopup.svelte |
|
||||
| 3 | [Level Redesign + Chaser Guard](phase-03-level-redesign.md) | 3h | Done | levels.js, guards.js, level-manager.js, GuardSprite.svelte, theme.css |
|
||||
| 4 | [Guard Vision Preview](phase-04-vision-preview.md) | 45min | Done | GameBoard.svelte, Game.svelte, turn-manager.js, GameHud.svelte |
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 1 (Wait) ── no deps, standalone
|
||||
Phase 2 (Stars) ── no deps, standalone
|
||||
Phase 3 (Levels) ── after Phase 1 + 2 (par values depend on wait mechanic existing)
|
||||
Phase 4 (Preview) ── after Phase 3 (needs final guard configs)
|
||||
```
|
||||
|
||||
## File Ownership (no parallel conflicts)
|
||||
|
||||
- Phase 1: Game.svelte only (input handler)
|
||||
- Phase 2: progress.js, LevelSelect.svelte, GameHud.svelte, + level complete flow in Game.svelte
|
||||
- Phase 3: levels.js, guards.js, level-manager.js, GuardSprite.svelte
|
||||
- Phase 4: GameBoard.svelte (new overlay layer)
|
||||
|
||||
## Key Constraints
|
||||
|
||||
- Level 12 MUST remain unbeatable (expanding light wave from goal covers all paths)
|
||||
- No new npm dependencies
|
||||
- All state stays in pure JS classes; Svelte 5 reactivity via renderVersion bump pattern
|
||||
- localStorage schema must be backwards-compatible (existing `nntv-progress` key)
|
||||
|
||||
## Rollback
|
||||
|
||||
Each phase is a single commit. Revert commit to rollback. No data migrations needed
|
||||
(localStorage schema only adds optional fields, old data still works).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Spacebar wait works, guards advance, player stays
|
||||
2. Stars show on level complete + level select; persist across sessions
|
||||
3. Levels require more thought; at least 3 levels need wait action to solve optimally
|
||||
4. Level 12 still unbeatable (manual verification)
|
||||
5. (Optional) Ghost overlay shows next-turn danger zones
|
||||
@@ -1,293 +0,0 @@
|
||||
# Svelte 5 Research: Grid Puzzle Game Rewrite
|
||||
|
||||
**Status:** Complete | **Focus:** Runes, setup, transitions, grid patterns, state management
|
||||
**Date:** 2026-04-12 | **Scope:** Actionable patterns for turn-based grid game
|
||||
|
||||
---
|
||||
|
||||
## 1. Svelte 5 Runes Syntax — Reactive State
|
||||
|
||||
### Core Pattern: $state for Grid
|
||||
Runes are compiler keywords (not functions), prefixed with `$`. Use `$state` to declare reactive cell data.
|
||||
|
||||
**Grid state pattern (array of cells):**
|
||||
```javascript
|
||||
let grid = $state([
|
||||
{ id: 0, type: 'wall' },
|
||||
{ id: 1, type: 'floor' },
|
||||
// ... 16+ cells
|
||||
]);
|
||||
```
|
||||
|
||||
**For object-based grid:**
|
||||
```javascript
|
||||
let gridMap = $state(new Map([
|
||||
['0,0', { type: 'wall', solved: false }],
|
||||
['0,1', { type: 'puzzle', value: 5 }],
|
||||
]));
|
||||
```
|
||||
|
||||
### $derived — Computed Grid State
|
||||
Auto-updates when dependencies change (memoized). Zero side-effects allowed.
|
||||
|
||||
```javascript
|
||||
let solvedCount = $derived(grid.filter(c => c.solved).length);
|
||||
let isWon = $derived(solvedCount === grid.length);
|
||||
let gridDisplay = $derived(grid.map(c => c.type)); // safe memo
|
||||
```
|
||||
|
||||
### $effect — Side Effects (Subscriptions, DOM, Analytics)
|
||||
Runs after DOM update. Return cleanup function for teardown.
|
||||
|
||||
```javascript
|
||||
$effect(() => {
|
||||
console.log('Game state changed:', grid);
|
||||
localStorage.setItem('game', JSON.stringify(grid));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
return () => console.log('Component unmounting'); // cleanup
|
||||
});
|
||||
```
|
||||
|
||||
### Key Gotchas
|
||||
- Avoid mutations inside `$derived`: `$derived(count++)` **fails**.
|
||||
- `$state` works outside `.svelte` files (in `.svelte.js` / `.svelte.ts`).
|
||||
- Runes replace old `let x = 0; $: x = x + 1` reactive declarations.
|
||||
|
||||
---
|
||||
|
||||
## 2. Svelte 5 + Vite Setup
|
||||
|
||||
### Recommended: Use `sv` CLI (SvelteKit)
|
||||
```bash
|
||||
npx sv create my-puzzle-game
|
||||
# Select: Svelte, TypeScript, Vitest, Prettier
|
||||
cd my-puzzle-game
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Scaffold includes: Vite (pre-configured), TypeScript, dev server at `localhost:5173`.
|
||||
|
||||
### Alternative: Bare Svelte + Vite
|
||||
```bash
|
||||
npm create vite@latest my-game -- --template svelte-ts
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Simpler but misses SvelteKit's routing/build optimizations. **For a static SPA game, either works.**
|
||||
|
||||
### Vite Config (if bare Svelte)
|
||||
`vite.config.ts` auto-includes `vite-plugin-svelte`. No manual config needed for basic setup.
|
||||
|
||||
---
|
||||
|
||||
## 3. Scene Transitions (Fade/Fly/Slide)
|
||||
|
||||
### Pattern: Conditional Scenes
|
||||
```svelte
|
||||
<script>
|
||||
import { fade, fly, slide } from 'svelte/transition';
|
||||
let currentScene = $state('intro');
|
||||
</script>
|
||||
|
||||
{#if currentScene === 'intro'}
|
||||
<div transition:fade={{ duration: 300 }}>
|
||||
<h1>Intro Scene</h1>
|
||||
<button onclick={() => currentScene = 'game'}>Play</button>
|
||||
</div>
|
||||
{:else if currentScene === 'game'}
|
||||
<div transition:fly={{ x: 100, duration: 400 }}>
|
||||
<!-- Game board here -->
|
||||
</div>
|
||||
{:else if currentScene === 'win'}
|
||||
<div transition:slide={{ axis: 'y', duration: 600 }}>
|
||||
<h1>You Won!</h1>
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Available Transitions
|
||||
| Name | Use | Params |
|
||||
|------|-----|--------|
|
||||
| `fade` | Opacity 0→1 | `duration`, `delay`, `easing` |
|
||||
| `fly` | Position + opacity | `x`, `y`, `duration` |
|
||||
| `slide` | Axis-based slide | `axis` ('x'/'y'), `duration` |
|
||||
| `scale`, `blur` | Also available | config object |
|
||||
|
||||
### Bidirectional
|
||||
`transition:` works both in and out. Reverses smoothly mid-animation if toggled.
|
||||
|
||||
---
|
||||
|
||||
## 4. CSS Grid for Game Board
|
||||
|
||||
### Reactive Grid Layout
|
||||
```svelte
|
||||
<script>
|
||||
let grid = $state(Array(16).fill(null).map((_, i) => ({
|
||||
id: i,
|
||||
row: Math.floor(i / 4),
|
||||
col: i % 4,
|
||||
})));
|
||||
</script>
|
||||
|
||||
<div class="board">
|
||||
{#each grid as cell (cell.id)}
|
||||
<div class="cell" onclick={() => handleCellClick(cell.id)}>
|
||||
{cell.id}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
width: 320px;
|
||||
}
|
||||
.cell {
|
||||
aspect-ratio: 1;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Performance Note
|
||||
Svelte's fine-grained reactivity means updating a single cell **doesn't re-render all others**. Safe for responsive grids (6x6, 8x8).
|
||||
|
||||
---
|
||||
|
||||
## 5. State Management: Stores vs. Runes
|
||||
|
||||
### For Game Progress + Localization: Use Runes in `.svelte.js`
|
||||
|
||||
**gameState.svelte.js:**
|
||||
```javascript
|
||||
export const game = $state({
|
||||
level: 1,
|
||||
moves: 0,
|
||||
isWon: false,
|
||||
grid: [],
|
||||
});
|
||||
|
||||
export const i18n = $state({
|
||||
lang: 'en',
|
||||
strings: { title: 'Puzzle Game', play: 'Play' },
|
||||
});
|
||||
|
||||
export function resetLevel() {
|
||||
game.level = 1;
|
||||
game.moves = 0;
|
||||
game.isWon = false;
|
||||
}
|
||||
```
|
||||
|
||||
**App.svelte:**
|
||||
```svelte
|
||||
<script>
|
||||
import { game, i18n } from './gameState.svelte.js';
|
||||
</script>
|
||||
|
||||
<h1>{i18n.strings.title}</h1>
|
||||
<p>Level {game.level} • Moves: {game.moves}</p>
|
||||
<button onclick={() => i18n.lang = 'es'}>Español</button>
|
||||
```
|
||||
|
||||
### When to Use Stores (Deprecated Pattern)
|
||||
Only for **async data streams** (polling, WebSocket). Most games don't need this.
|
||||
|
||||
### Why Runes > Stores
|
||||
- No boilerplate (no `writable()`, `.subscribe()`).
|
||||
- Works in plain `.js` files (shared logic).
|
||||
- Memoization via `$derived`.
|
||||
|
||||
---
|
||||
|
||||
## 6. SvelteKit vs. Plain Svelte for Static Game
|
||||
|
||||
### Recommendation: **Use Plain Svelte + Vite**
|
||||
|
||||
| Criteria | Plain Svelte | SvelteKit |
|
||||
|----------|--------------|-----------|
|
||||
| **Setup time** | 2 min | 3 min |
|
||||
| **Routing** | Manual | File-based (unnecessary) |
|
||||
| **Build output** | Single HTML/JS | Server capable |
|
||||
| **Static deploy** | ✅ Simpler | ✅ Needs adapter-static |
|
||||
| **DevTools** | Vite only | Vite + SvelteKit CLI |
|
||||
| **Scaling** | Hits limits ~2-3 scenes | Extensible |
|
||||
|
||||
### For a Static SPA Game
|
||||
```bash
|
||||
npm create vite@latest my-game -- --template svelte-ts
|
||||
```
|
||||
|
||||
This is **KISS**. No file-based routing overhead, no server code, direct control.
|
||||
|
||||
### Only Use SvelteKit If
|
||||
- Game needs **server-side scoring/leaderboards** (then use SvelteKit backend).
|
||||
- Game will expand to **10+ pages** (use file-based routing).
|
||||
- Team prefers **unified framework** across multiple projects.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Phase 1: Setup
|
||||
- [ ] `npm create vite@latest` (svelte-ts template)
|
||||
- [ ] Replace Vite's default App.svelte with game component
|
||||
- [ ] Verify `npm run dev` opens at localhost:5173
|
||||
|
||||
### Phase 2: Grid & State
|
||||
- [ ] Create `gameState.svelte.js` with `$state(grid)`
|
||||
- [ ] Build `GameBoard.svelte` with CSS Grid + `{#each grid}`
|
||||
- [ ] Wire `$derived(isWon)` from grid state
|
||||
|
||||
### Phase 3: Interactions
|
||||
- [ ] Add cell click handlers → update grid state
|
||||
- [ ] Bind to `$effect` for persistence (localStorage)
|
||||
- [ ] Test fine-grained reactivity (click cell, only that cell re-renders)
|
||||
|
||||
### Phase 4: Transitions
|
||||
- [ ] Import `fade`, `fly`, `slide` from `svelte/transition`
|
||||
- [ ] Wrap scenes in conditional blocks with `transition:` directives
|
||||
- [ ] Test bidirectional transitions (scene switches)
|
||||
|
||||
### Phase 5: Deploy
|
||||
- [ ] `npm run build` → outputs `dist/` (static)
|
||||
- [ ] Upload `dist/` to any static host (Vercel, GitHub Pages, Netlify)
|
||||
|
||||
---
|
||||
|
||||
## Trade-offs & Adoption Risk
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| Svelte 5 is new (2024+) | Use LTS minor versions; runes are stable now |
|
||||
| Team unfamiliar with runes | Runes are **simpler** than Vue/React hooks; quick ramp |
|
||||
| Grid performance at scale | Fine-grained reactivity handles 100+ cells easily |
|
||||
| Transitions can stutter | Use `duration: 200–400ms`; test on target devices |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Svelte Runes Documentation](https://svelte.dev/docs/svelte/what-are-runes)
|
||||
- [Svelte Transitions](https://svelte.dev/docs/svelte/svelte-transition)
|
||||
- [Svelte Stores](https://svelte.dev/docs/svelte/stores)
|
||||
- [SvelteKit Single-Page Apps](https://svelte.dev/docs/kit/single-page-apps)
|
||||
- [Getting Started](https://svelte.dev/docs/svelte/getting-started)
|
||||
- [Mainmatter: Runes and Global State](https://mainmatter.com/blog/2025/03/11/global-state-in-svelte-5/)
|
||||
- [Frontend Masters: Game Board Grid](https://frontendmasters.com/courses/svelte-v2/game-board-grid/)
|
||||
|
||||
---
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
- **WebSocket multiplayer?** Not covered; requires SvelteKit backend or third-party service.
|
||||
- **Mobile touch events?** Standard DOM events work; no Svelte-specific guidance needed.
|
||||
- **Save file encryption?** Depends on backend; out of scope for static game.
|
||||
Reference in new issue
Block a user