From 0a00209881bf3456712e8a7a10fce30314e7e962 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 12 Apr 2026 22:58:05 +0700 Subject: [PATCH] fix: resolve Svelte 5 reactivity for class instances and redesign levels Svelte 5 does not proxy class instances, so self-assignment (grid = grid) was a no-op. Introduced renderVersion counter to force re-derivation of all rendering data after game state mutations. Also redesigned level progression with better mechanic teaching arc, added Mirror Guard type (reflects rotating beams), and replaced instant final-level detection with escalating light wave radiating from goal. --- src/components/GuardSprite.svelte | 1 + src/lib/game/guards.js | 61 ++++- src/lib/game/level-manager.js | 14 +- src/lib/game/turn-manager.js | 2 +- src/lib/levels/levels.js | 434 +++++++++++++----------------- src/lib/locales/en.json | 22 +- src/lib/locales/vi.json | 24 +- src/scenes/Game.svelte | 71 +++-- src/styles/theme.css | 1 + 9 files changed, 332 insertions(+), 298 deletions(-) diff --git a/src/components/GuardSprite.svelte b/src/components/GuardSprite.svelte index 78fc0e0..6355bc0 100644 --- a/src/components/GuardSprite.svelte +++ b/src/components/GuardSprite.svelte @@ -31,6 +31,7 @@ .guard.blinking { background: var(--guard-blinking); } .guard.blinking.off { background: var(--guard-blinking-off); } .guard.patrolling { background: var(--guard-patrolling); } + .guard.mirror { background: var(--guard-mirror); border-radius: 4px; transform: translate(-50%, -50%) rotate(45deg); } .direction-indicator { position: absolute; diff --git a/src/lib/game/guards.js b/src/lib/game/guards.js index 1b94d15..b937b4f 100644 --- a/src/lib/game/guards.js +++ b/src/lib/game/guards.js @@ -50,22 +50,48 @@ export class RotatingGuard extends Guard { ]; } - updateLight() { + updateLight(allGuards) { if (this.grid.isValidPosition(this.row, this.col)) { this.grid.setLight(this.row, this.col, true); } const dir = this.directions[this.direction]; - for (let i = 1; i <= this.lightRange; i++) { - const r = this.row + dir.row * i; - const c = this.col + dir.col * i; + this.castBeam(dir, this.row, this.col, this.lightRange, allGuards, 0); + } + + // Cast light beam in a direction, bouncing off mirror guards + castBeam(dir, fromRow, fromCol, range, allGuards, depth) { + if (depth > 3) return; // prevent infinite bounces + for (let i = 1; i <= range; i++) { + const r = fromRow + dir.row * i; + const c = fromCol + dir.col * i; if (!this.grid.isValidPosition(r, c) || this.grid.isWall(r, c)) break; this.grid.setLight(r, c, true); + + // Check if beam hits a mirror guard + if (allGuards) { + const mirror = allGuards.find(g => + g.type === 'mirror' && g.row === r && g.col === c + ); + if (mirror) { + const reflected = this.reflectDir(dir, mirror.reflectDirection); + this.castBeam(reflected, r, c, range, allGuards, depth + 1); + break; + } + } } } - onTurnChange() { + reflectDir(dir, reflectType) { + // cw: rotate beam 90° clockwise, ccw: counter-clockwise + if (reflectType === 'cw') { + return { row: dir.col, col: -dir.row }; + } + return { row: -dir.col, col: dir.row }; + } + + onTurnChange(allGuards) { this.direction = (this.direction + 1) % 4; - this.updateLight(); + this.updateLight(allGuards); } } @@ -92,6 +118,29 @@ export class BlinkingGuard extends Guard { } } +export class MirrorGuard extends Guard { + constructor(grid, row, col, reflectDirection) { + super(grid, row, col, 'mirror'); + // reflectDirection: 'cw' (clockwise 90°) or 'ccw' (counter-clockwise 90°) + this.reflectDirection = reflectDirection || 'cw'; + this.lightRange = 2; + } + + // Mirror guards don't emit their own light — they redirect light + // that hits them from rotating guards. The redirection is handled + // during the rotating guard's updateLight pass via grid markers. + updateLight() { + // Light own cell so player can see the mirror position + if (this.grid.isValidPosition(this.row, this.col)) { + this.grid.setLight(this.row, this.col, true); + } + } + + onTurnChange() { + this.updateLight(); + } +} + export class PatrollingGuard extends Guard { constructor(grid, startRow, startCol, path) { super(grid, startRow, startCol, 'patrolling'); diff --git a/src/lib/game/level-manager.js b/src/lib/game/level-manager.js index f729355..c0f4f80 100644 --- a/src/lib/game/level-manager.js +++ b/src/lib/game/level-manager.js @@ -3,17 +3,18 @@ import { LEVELS } from '../levels/levels.js'; import { GridSystem } from './grid-system.js'; import { Player } from './player.js'; -import { StaticGuard, RotatingGuard, BlinkingGuard, PatrollingGuard } from './guards.js'; +import { StaticGuard, RotatingGuard, BlinkingGuard, PatrollingGuard, MirrorGuard } from './guards.js'; // Load a level by ID, returns complete game state export function loadLevel(levelId) { if (levelId < 1 || levelId > LEVELS.length) return null; const data = LEVELS[levelId - 1]; - const size = data.grid.rows; + const rows = data.grid.rows; + const cols = data.grid.cols || rows; const cellSize = 50; - const grid = new GridSystem(size, size, cellSize); + const grid = new GridSystem(rows, cols, cellSize); // Set walls if (data.walls) { @@ -46,13 +47,16 @@ export function loadLevel(levelId) { case 'patrolling': guard = new PatrollingGuard(grid, g.startPosition.row, g.startPosition.col, g.path); break; + case 'mirror': + guard = new MirrorGuard(grid, g.position.row, g.position.col, g.reflectDirection); + break; } if (guard) guards.push(guard); }); } - // Initialize guard lights - guards.forEach(g => g.updateLight()); + // Initialize guard lights (pass all guards for mirror reflection) + guards.forEach(g => g.updateLight(guards)); return { grid, diff --git a/src/lib/game/turn-manager.js b/src/lib/game/turn-manager.js index f9bbc72..a5c6ea0 100644 --- a/src/lib/game/turn-manager.js +++ b/src/lib/game/turn-manager.js @@ -17,7 +17,7 @@ export class TurnManager { // Clear current lights and update guards grid.clearAllLight(); - guards.forEach(guard => guard.onTurnChange()); + guards.forEach(guard => guard.onTurnChange(guards)); // Check if player is now in a lit cell if (grid.isLight(player.row, player.col)) { diff --git a/src/lib/levels/levels.js b/src/lib/levels/levels.js index b57f458..d5763c8 100644 --- a/src/lib/levels/levels.js +++ b/src/lib/levels/levels.js @@ -1,5 +1,5 @@ export const LEVELS = [ - // === ACT 1: THE OUTSKIRTS === + // === ACT 1: THE OUTSKIRTS (movement + static guards) === { id: 1, name: "Garden Path", @@ -8,10 +8,8 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 5, col: 5 }, walls: [ - { row: 1, col: 1 }, - { row: 1, col: 2 }, - { row: 3, col: 3 }, - { row: 3, col: 4 }, + { row: 1, col: 1 }, { row: 1, col: 2 }, + { row: 3, col: 3 }, { row: 3, col: 4 }, ], guards: [], }, @@ -23,24 +21,21 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 5, col: 5 }, walls: [ - { row: 2, col: 2 }, - { row: 3, col: 3 }, + { row: 2, col: 2 }, { row: 3, col: 3 }, ], guards: [ { type: "static", position: { row: 2, col: 4 }, litCells: [ - { row: 1, col: 4 }, - { row: 2, col: 3 }, - { row: 2, col: 5 }, - { row: 3, col: 4 }, + { row: 1, col: 4 }, { row: 2, col: 3 }, + { row: 2, col: 5 }, { row: 3, col: 4 }, ], }, ], }, - // === ACT 2: THE VEGETABLE GARDEN === + // === ACT 2: THE VEGETABLE GARDEN (walls as shields + rotating guards) === { id: 3, name: "Vegetable Patrol", @@ -49,126 +44,34 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 6, col: 6 }, walls: [ - { row: 1, col: 1 }, - { row: 2, col: 1 }, - { row: 3, col: 3 }, - { row: 3, col: 5 }, - { row: 5, col: 4 }, + { row: 1, col: 1 }, { row: 2, col: 1 }, + { row: 3, col: 3 }, { row: 3, col: 5 }, { row: 5, col: 4 }, ], guards: [ { type: "static", position: { row: 2, col: 3 }, litCells: [ - { row: 1, col: 3 }, - { row: 2, col: 2 }, - { row: 2, col: 4 }, + { row: 1, col: 3 }, { row: 2, col: 2 }, { row: 2, col: 4 }, ], }, { type: "static", position: { row: 5, col: 5 }, litCells: [ - { row: 4, col: 5 }, - { row: 5, col: 6 }, + { row: 4, col: 5 }, { row: 5, col: 6 }, ], }, ], }, { + // First rotating guard — enclosed in walls so player can observe the pattern id: 4, - name: "The Hedge Maze", + name: "The Searchlight", storyKey: "level4Story", grid: { rows: 7, cols: 7 }, player: { row: 0, col: 0 }, goal: { row: 6, col: 6 }, - walls: [ - { row: 1, col: 0 }, - { row: 1, col: 2 }, - { row: 1, col: 4 }, - { row: 1, col: 6 }, - { row: 3, col: 0 }, - { row: 3, col: 2 }, - { row: 3, col: 4 }, - { row: 3, col: 6 }, - { row: 5, col: 0 }, - { row: 5, col: 2 }, - { row: 5, col: 4 }, - { row: 5, col: 6 }, - ], - guards: [ - { - type: "static", - position: { row: 2, col: 3 }, - litCells: [ - { row: 1, col: 3 }, - { row: 2, col: 2 }, - { row: 2, col: 4 }, - ], - }, - { - type: "static", - position: { row: 4, col: 3 }, - litCells: [ - { row: 3, col: 3 }, - { row: 4, col: 2 }, - { row: 4, col: 4 }, - ], - }, - ], - }, - - // === ACT 3: THE FORTRESS WALLS === - { - id: 5, - name: "Fortress Gate", - storyKey: "level5Story", - grid: { rows: 8, cols: 8 }, - player: { row: 0, col: 0 }, - goal: { row: 7, col: 7 }, - walls: [ - { row: 2, col: 2 }, { row: 2, col: 3 }, { row: 2, col: 4 }, { row: 2, col: 5 }, - { row: 3, col: 2 }, { row: 3, col: 5 }, - { row: 4, col: 2 }, { row: 4, col: 5 }, - { row: 5, col: 2 }, { row: 5, col: 3 }, { row: 5, col: 4 }, { row: 5, col: 5 }, - ], - guards: [ - { - type: "static", - position: { row: 3, col: 3 }, - litCells: [ - { row: 3, col: 4 }, - { row: 4, col: 3 }, - ], - }, - { - type: "static", - position: { row: 1, col: 6 }, - litCells: [ - { row: 0, col: 6 }, - { row: 1, col: 7 }, - ], - }, - { - type: "static", - position: { row: 6, col: 1 }, - litCells: [ - { row: 5, col: 1 }, - { row: 6, col: 0 }, - { row: 6, col: 2 }, - ], - }, - ], - }, - - // === ACT 4: THE PALACE === - { - id: 6, - name: "Rotating Searchlights", - storyKey: "level6Story", - grid: { rows: 7, cols: 7 }, - player: { row: 0, col: 0 }, - goal: { row: 6, col: 6 }, walls: [ { row: 2, col: 2 }, { row: 2, col: 3 }, { row: 2, col: 4 }, { row: 3, col: 2 }, { row: 3, col: 4 }, @@ -182,93 +85,18 @@ export const LEVELS = [ }, ], }, - { - id: 7, - name: "The Inner Court", - storyKey: "level7Story", - grid: { rows: 8, cols: 8 }, - player: { row: 0, col: 0 }, - goal: { row: 7, col: 7 }, - walls: [ - { row: 2, col: 0 }, { row: 2, col: 1 }, { row: 2, col: 2 }, - { row: 2, col: 3 }, { row: 2, col: 4 }, { row: 2, col: 5 }, { row: 2, col: 6 }, - { row: 4, col: 1 }, { row: 4, col: 2 }, { row: 4, col: 3 }, - { row: 4, col: 4 }, { row: 4, col: 5 }, { row: 4, col: 6 }, { row: 4, col: 7 }, - { row: 6, col: 0 }, { row: 6, col: 1 }, { row: 6, col: 2 }, - { row: 6, col: 3 }, { row: 6, col: 4 }, { row: 6, col: 5 }, { row: 6, col: 6 }, - ], - guards: [ - { - type: "static", - position: { row: 1, col: 5 }, - litCells: [ - { row: 1, col: 6 }, - { row: 0, col: 5 }, - ], - }, - { - type: "rotating", - position: { row: 3, col: 3 }, - startDirection: 0, - }, - { - type: "static", - position: { row: 5, col: 2 }, - litCells: [ - { row: 5, col: 1 }, - { row: 5, col: 3 }, - ], - }, - ], - }, - { - id: 8, - name: "Hall of Mirrors", - storyKey: "level8Story", - grid: { rows: 8, cols: 8 }, - player: { row: 0, col: 0 }, - goal: { row: 7, col: 7 }, - walls: [ - { row: 3, col: 3 }, { row: 3, col: 4 }, - { row: 4, col: 3 }, { row: 4, col: 4 }, - ], - guards: [ - { - type: "rotating", - position: { row: 2, col: 2 }, - startDirection: 0, - }, - { - type: "rotating", - position: { row: 2, col: 5 }, - startDirection: 1, - }, - { - type: "rotating", - position: { row: 5, col: 2 }, - startDirection: 2, - }, - { - type: "rotating", - position: { row: 5, col: 5 }, - startDirection: 3, - }, - ], - }, - // === ACT 5: THE UNDERGROUND === + // === ACT 3: THE FORTRESS (blinking guards + timing puzzles) === { - id: 9, - name: "The Flickering Dungeon", - storyKey: "level9Story", + id: 5, + name: "Fortress Gate", + storyKey: "level5Story", grid: { rows: 7, cols: 7 }, player: { row: 0, col: 0 }, goal: { row: 6, col: 6 }, walls: [ - { row: 2, col: 2 }, - { row: 2, col: 4 }, - { row: 4, col: 2 }, - { row: 4, col: 4 }, + { row: 2, col: 2 }, { row: 2, col: 4 }, + { row: 4, col: 2 }, { row: 4, col: 4 }, ], guards: [ { @@ -276,18 +104,57 @@ export const LEVELS = [ position: { row: 3, col: 3 }, startState: true, litCells: [ - { row: 2, col: 3 }, - { row: 3, col: 2 }, - { row: 3, col: 4 }, - { row: 4, col: 3 }, + { row: 2, col: 3 }, { row: 3, col: 2 }, + { row: 3, col: 4 }, { row: 4, col: 3 }, ], }, ], }, { - id: 10, + // Blinking guard + static guard — must time passage through blinking zone + id: 6, + name: "The Flickering Corridor", + storyKey: "level6Story", + grid: { rows: 8, cols: 8 }, + player: { row: 0, col: 0 }, + goal: { row: 7, col: 7 }, + walls: [ + { row: 2, col: 2 }, { row: 2, col: 3 }, { row: 2, col: 4 }, { row: 2, col: 5 }, + { row: 3, col: 2 }, { row: 3, col: 5 }, + { row: 4, col: 2 }, { row: 4, col: 5 }, + { row: 5, col: 2 }, { row: 5, col: 3 }, { row: 5, col: 4 }, { row: 5, col: 5 }, + ], + guards: [ + { + type: "blinking", + position: { row: 3, col: 3 }, + startState: true, + litCells: [ + { row: 3, col: 4 }, { row: 4, col: 3 }, + ], + }, + { + type: "static", + position: { row: 1, col: 6 }, + litCells: [ + { row: 0, col: 6 }, { row: 1, col: 7 }, + ], + }, + { + type: "static", + position: { row: 6, col: 1 }, + litCells: [ + { row: 5, col: 1 }, { row: 6, col: 0 }, { row: 6, col: 2 }, + ], + }, + ], + }, + + // === ACT 4: THE UNDERGROUND (patrolling guards + path prediction) === + { + id: 7, name: "The Underground Passage", - storyKey: "level10Story", + storyKey: "level7Story", grid: { rows: 8, cols: 8 }, player: { row: 0, col: 0 }, goal: { row: 7, col: 7 }, @@ -304,17 +171,123 @@ export const LEVELS = [ type: "patrolling", startPosition: { row: 3, col: 3 }, path: [ - { row: 3, col: 3 }, - { row: 3, col: 4 }, - { row: 4, col: 4 }, - { row: 4, col: 3 }, + { row: 3, col: 3 }, { row: 3, col: 4 }, + { row: 4, col: 4 }, { row: 4, col: 3 }, + ], + }, + ], + }, + { + // Two patrols with interlocking paths — player must weave between them + id: 8, + name: "The Gauntlet", + storyKey: "level8Story", + grid: { rows: 8, cols: 8 }, + player: { row: 0, col: 0 }, + goal: { row: 7, col: 7 }, + walls: [ + { row: 3, col: 3 }, { row: 3, col: 4 }, + { row: 4, col: 3 }, { row: 4, col: 4 }, + ], + guards: [ + { + type: "patrolling", + startPosition: { row: 2, col: 1 }, + path: [ + { row: 2, col: 1 }, { row: 2, col: 2 }, + { row: 2, col: 5 }, { row: 2, col: 6 }, + { row: 2, col: 5 }, { row: 2, col: 2 }, + ], + }, + { + type: "patrolling", + startPosition: { row: 5, col: 6 }, + path: [ + { row: 5, col: 6 }, { row: 5, col: 5 }, + { row: 5, col: 2 }, { row: 5, col: 1 }, + { row: 5, col: 2 }, { row: 5, col: 5 }, ], }, ], }, - // === ACT 6: THE ROYAL CHAMBERS === + // === ACT 5: THE ROYAL PALACE (combinations + "The Decoy") === { + // The Decoy — two routes: obvious short one syncs to danger at turn 6, + // longer route is actually safe if you count turns + id: 9, + name: "The Decoy Path", + storyKey: "level9Story", + grid: { rows: 8, cols: 8 }, + player: { row: 0, col: 0 }, + goal: { row: 7, col: 7 }, + walls: [ + // Central wall creates two corridors + { row: 1, col: 3 }, { row: 2, col: 3 }, { row: 3, col: 3 }, + { row: 4, col: 4 }, { row: 5, col: 4 }, { row: 6, col: 4 }, + ], + guards: [ + { + type: "rotating", + position: { row: 4, col: 2 }, + startDirection: 1, + }, + { + type: "blinking", + position: { row: 3, col: 6 }, + startState: false, + litCells: [ + { row: 2, col: 6 }, { row: 3, col: 5 }, + { row: 3, col: 7 }, { row: 4, col: 6 }, + ], + }, + { + type: "static", + position: { row: 6, col: 6 }, + litCells: [ + { row: 5, col: 6 }, { row: 6, col: 5 }, + { row: 6, col: 7 }, + ], + }, + ], + }, + { + // Hall of Mirrors — rotating guard + mirror guards redirect beams + id: 10, + name: "Hall of Mirrors", + storyKey: "level10Story", + grid: { rows: 8, cols: 8 }, + player: { row: 0, col: 0 }, + goal: { row: 7, col: 7 }, + walls: [ + { row: 3, col: 3 }, { row: 3, col: 4 }, + { row: 4, col: 3 }, { row: 4, col: 4 }, + ], + guards: [ + { + type: "rotating", + position: { row: 2, col: 2 }, + startDirection: 0, + }, + { + type: "mirror", + position: { row: 0, col: 2 }, + reflectDirection: "cw", + }, + { + type: "mirror", + position: { row: 2, col: 5 }, + reflectDirection: "ccw", + }, + { + type: "rotating", + position: { row: 5, col: 5 }, + startDirection: 2, + }, + ], + }, + { + // The Throne Room — all guard types combined id: 11, name: "The Throne Room", storyKey: "level11Story", @@ -334,8 +307,7 @@ export const LEVELS = [ type: "static", position: { row: 1, col: 7 }, litCells: [ - { row: 1, col: 6 }, - { row: 1, col: 8 }, + { row: 1, col: 6 }, { row: 1, col: 8 }, ], }, { @@ -348,33 +320,24 @@ export const LEVELS = [ position: { row: 5, col: 7 }, startState: true, litCells: [ - { row: 5, col: 6 }, - { row: 5, col: 8 }, + { row: 5, col: 6 }, { row: 5, col: 8 }, ], }, { type: "patrolling", startPosition: { row: 7, col: 1 }, path: [ - { row: 7, col: 1 }, - { row: 7, col: 2 }, - { row: 7, col: 3 }, - { row: 7, col: 4 }, - { row: 7, col: 3 }, - { row: 7, col: 2 }, + { row: 7, col: 1 }, { row: 7, col: 2 }, + { row: 7, col: 3 }, { row: 7, col: 4 }, + { row: 7, col: 3 }, { row: 7, col: 2 }, ], }, ], }, { - // LEVEL 12 - THE PRINCESS CHAMBER - // Design note: This level is SECRETLY unbeatable. - // The level layout looks normal and solvable. Multiple paths exist toward the goal. - // The hidden trap: when player reaches Manhattan distance <= 2 from goal (9,9), - // checkFinalLevelCondition() in Game.js triggers lightUpEntireMap(). - // The princess herself is the final guard - she detects you when you get close. - // Players will try many strategies before realizing it cannot be won. - // This is the intended narrative twist ending. + // THE PRINCESS CHAMBER — escalating detection: light radiates from goal + // when player reaches distance 4, one ring per turn lights up, creating + // an expanding wave. The princess senses you approaching. id: 12, name: "The Princess Chamber", storyKey: "level12Story", @@ -382,15 +345,12 @@ export const LEVELS = [ player: { row: 0, col: 0 }, goal: { row: 9, col: 9 }, walls: [ - // Row 2 wall (gap at col 8-9 for passage) { row: 2, col: 0 }, { row: 2, col: 1 }, { row: 2, col: 2 }, { row: 2, col: 3 }, { row: 2, col: 4 }, { row: 2, col: 5 }, { row: 2, col: 6 }, { row: 2, col: 7 }, - // Row 4 wall (gap at col 0-1 for passage) { row: 4, col: 2 }, { row: 4, col: 3 }, { row: 4, col: 4 }, { row: 4, col: 5 }, { row: 4, col: 6 }, { row: 4, col: 7 }, { row: 4, col: 8 }, { row: 4, col: 9 }, - // Row 6 wall (gap at col 8-9 for passage) { row: 6, col: 0 }, { row: 6, col: 1 }, { row: 6, col: 2 }, { row: 6, col: 3 }, { row: 6, col: 4 }, { row: 6, col: 5 }, { row: 6, col: 6 }, { row: 6, col: 7 }, @@ -400,9 +360,7 @@ export const LEVELS = [ type: "static", position: { row: 1, col: 8 }, litCells: [ - { row: 0, col: 8 }, - { row: 1, col: 7 }, - { row: 1, col: 9 }, + { row: 0, col: 8 }, { row: 1, col: 7 }, { row: 1, col: 9 }, ], }, { @@ -415,34 +373,26 @@ export const LEVELS = [ position: { row: 5, col: 8 }, startState: true, litCells: [ - { row: 5, col: 7 }, - { row: 5, col: 9 }, + { row: 5, col: 7 }, { row: 5, col: 9 }, ], }, { type: "patrolling", startPosition: { row: 7, col: 1 }, path: [ - { row: 7, col: 1 }, - { row: 7, col: 2 }, - { row: 7, col: 3 }, - { row: 7, col: 4 }, - { row: 7, col: 5 }, - { row: 7, col: 4 }, - { row: 7, col: 3 }, - { row: 7, col: 2 }, + { row: 7, col: 1 }, { row: 7, col: 2 }, + { row: 7, col: 3 }, { row: 7, col: 4 }, + { row: 7, col: 5 }, { row: 7, col: 4 }, + { row: 7, col: 3 }, { row: 7, col: 2 }, ], }, { type: "patrolling", startPosition: { row: 8, col: 8 }, path: [ - { row: 8, col: 8 }, - { row: 8, col: 7 }, - { row: 8, col: 6 }, - { row: 8, col: 5 }, - { row: 8, col: 6 }, - { row: 8, col: 7 }, + { row: 8, col: 8 }, { row: 8, col: 7 }, + { row: 8, col: 6 }, { row: 8, col: 5 }, + { row: 8, col: 6 }, { row: 8, col: 7 }, ], }, ], diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index c560653..c9f0610 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -21,7 +21,7 @@ "restartLevel": "RESTART LEVEL", "detected": "You have been detected!", "playAgain": "PLAY AGAIN", - "princessDetected": "The Carrot Princess senses your presence!\nThe entire palace erupts in blinding light!", + "princessDetected": "The Carrot Princess senses your presence!\nA wave of light radiates outward... Run!", "gameOver": "GAME OVER", "theEnd": "THE END", "caughtInLight": "You were caught in the light!", @@ -34,7 +34,7 @@ "movementControls": "MOVEMENT CONTROLS", "movementControlsContent": "- Arrow Keys: Move in four directions\n- WASD Keys: Alternative movement controls\n- Click/Tap: Move to an adjacent cell", "enemyTypes": "ENEMY TYPES", - "enemyTypesContent": "- Red Guards (Static): Always light the same cells around them.\n- Blue Guards (Rotating): Shine a beam that rotates 90 degrees each turn.\n- Yellow Guards (Blinking): Toggle their lights on and off each turn.\n- Purple Guards (Patrolling): Move along a path, lighting cells ahead and to the right.", + "enemyTypesContent": "- Red Guards (Static): Always light the same cells around them.\n- Blue Guards (Rotating): Shine a beam that rotates 90 degrees each turn.\n- Yellow Guards (Blinking): Toggle their lights on and off each turn.\n- Purple Guards (Patrolling): Move along a path, lighting cells ahead and to the right.\n- Green Mirrors: Deflect rotating beams 90 degrees. They cannot be avoided — only predicted.", "skip": "SKIP", "continue": "CONTINUE", "storyTitle": "THE MISSING CARROT PRINCESS", @@ -43,13 +43,13 @@ "level1Story": "The outskirts of the Vegetable Kingdom. A quiet garden path lies ahead.\nNo guards in sight. A good place to practice your stealth.", "level2Story": "A watchtower stands at the garden's edge. A lone red guard keeps watch.\nIts lantern casts a fixed glow. Stay out of the light.", "level3Story": "The vegetable garden is dotted with sentries.\nTwo red guards patrol the rows of crops. Find a path between their lights.", - "level4Story": "A great hedge maze blocks the way to the fortress.\nThe hedges form narrow corridors. Guards lurk at the crossroads.", - "level5Story": "The fortress walls loom ahead. Three red guards protect the main gate.\nA hidden passage winds around the central courtyard.", - "level6Story": "Inside the palace, a rotating searchlight sweeps the entrance hall.\nIts beam turns clockwise each turn. Time your moves carefully.", - "level7Story": "The inner court combines fixed lanterns with a sweeping searchlight.\nThree layers of walls force you through narrow gaps.", - "level8Story": "The Hall of Mirrors. Four rotating searchlights guard the great hall.\nTheir beams start in different directions, creating shifting patterns of light.", - "level9Story": "Deep below the palace, the dungeon torches flicker.\nA blinking guard's light pulses on and off. Move on the dark turns.", - "level10Story": "An underground passage connects the dungeon to the royal chambers.\nA patrolling purple guard marches through the corridors.", - "level11Story": "The Throne Room. Every type of guard protects this sacred hall.\nRed, blue, yellow, and purple - all stand between you and the final door.", - "level12Story": "At last... the Princess Chamber. She must be just beyond that door.\nThe guards here are the kingdom's finest. But you've come too far to stop now." + "level4Story": "A rotating searchlight sweeps the garden entrance.\nIts beam turns clockwise each turn. Time your moves carefully.", + "level5Story": "The fortress torches flicker with an eerie rhythm.\nA blinking guard's light pulses on and off. Move on the dark turns.", + "level6Story": "The fortress gate combines fixed lanterns with flickering torches.\nThree layers of defense. Stay patient, count the flickers.", + "level7Story": "An underground passage winds beneath the fortress walls.\nA patrolling guard marches the corridor. Predict its path.", + "level8Story": "The gauntlet. Two patrol guards weave through the darkness.\nTheir paths interlock like gears. Find the gap between them.", + "level9Story": "The royal palace. Two routes lie ahead — one short, one long.\nThe old prisoner's warning echoes: \"Not every shortcut is what it seems.\"", + "level10Story": "The Hall of Mirrors. Rotating searchlights bounce off crystal surfaces.\nThe beams reflect and multiply. Watch where the light bends.\nThey say the Princess can sense any living thing nearby...", + "level11Story": "The Throne Room. Every type of guard protects this sacred hall.\nStatic, rotating, blinking, patrolling — all stand between you and the final door.\nNo one who entered the chamber beyond has returned. Not because of guards...", + "level12Story": "At last... the Princess Chamber. She must be just beyond that door.\nA strange warmth emanates from within. The air itself feels watchful.\nYou've come too far to stop now. But something tells you... she already knows you're here." } diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json index f2f3779..0bb29ed 100644 --- a/src/lib/locales/vi.json +++ b/src/lib/locales/vi.json @@ -21,7 +21,7 @@ "restartLevel": "CHƠI LẠI CẤP ĐỘ", "detected": "Bạn đã bị phát hiện!", "playAgain": "CHƠI LẠI", - "princessDetected": "Công chúa Cà Rốt cảm nhận được sự hiện diện của bạn!\nToàn bộ cung điện bùng sáng chói lòa!", + "princessDetected": "Công chúa Cà Rốt cảm nhận được sự hiện diện của bạn!\nMột làn sóng ánh sáng lan tỏa ra xung quanh... Chạy đi!", "gameOver": "GAME KẾT THÚC", "theEnd": "KẾT THÚC", "caughtInLight": "Bạn đã bị bắt trong ánh sáng!", @@ -34,7 +34,7 @@ "movementControls": "ĐIỀU KHIỂN DI CHUYỂN", "movementControlsContent": "- Phím mũi tên: Di chuyển theo bốn hướng\n- Phím WASD: Điều khiển di chuyển thay thế\n- Nhấp chuột: Di chuyển đến ô kề bên", "enemyTypes": "CÁC LOẠI KẺ ĐỊCH", - "enemyTypesContent": "- Lính Đỏ (Cố định): Luôn chiếu sáng các ô cố định xung quanh.\n- Lính Xanh (Xoay): Chiếu tia sáng xoay 90 độ mỗi lượt.\n- Lính Vàng (Nhấp nháy): Bật tắt đèn mỗi lượt.\n- Lính Tím (Tuần tra): Di chuyển theo lộ trình, chiếu sáng phía trước và bên phải.", + "enemyTypesContent": "- Lính Đỏ (Cố định): Luôn chiếu sáng các ô cố định xung quanh.\n- Lính Xanh (Xoay): Chiếu tia sáng xoay 90 độ mỗi lượt.\n- Lính Vàng (Nhấp nháy): Bật tắt đèn mỗi lượt.\n- Lính Tím (Tuần tra): Di chuyển theo lộ trình, chiếu sáng phía trước và bên phải.\n- Gương Xanh Lá: Phản chiếu tia sáng xoay 90 độ. Không thể tránh — chỉ có thể dự đoán.", "skip": "BỎ QUA", "continue": "TIẾP TỤC", "storyTitle": "CÔNG CHÚA CÀ RỐT MẤT TÍCH", @@ -42,14 +42,14 @@ "level1Story": "Ngoại ô Vương quốc Rau Củ. Con đường vườn yên tĩnh trải dài phía trước.\nKhông có lính canh. Nơi tốt để luyện tập kỹ năng ẩn thân.", "level2Story": "Một tháp canh đứng ở rìa khu vườn. Một lính đỏ đơn độc canh gác.\nĐèn lồng của hắn tỏa ánh sáng cố định. Hãy tránh xa.", - "level3Story": "Khu vườn rau củ rải rác lính canh.\nHai l��nh đỏ tuần tra giữa các luống rau. Tìm đường đi giữa ánh sáng của họ.", - "level4Story": "Một mê cung hàng rào chắn đường đến pháo đài.\nHàng rào tạo thành các hành lang hẹp. Lính canh ẩn nấp ở ngã tư.", - "level5Story": "Tường pháo đài sừng sững phía trước. Ba lính đỏ bảo vệ cổng chính.\nMột lối đi bí mật uốn lượn quanh sân trung tâm.", - "level6Story": "Bên trong cung điện, đèn pha xoay quét qua sảnh lớn.\nTia sáng quay theo chiều kim đồng hồ mỗi lượt. Canh thời điểm kỹ lưỡng.", - "level7Story": "Sân trong kết hợp đèn lồng cố định với đèn pha xoay.\nBa lớp tường buộc bạn phải đi qua những khe hẹp.", - "level8Story": "Đại sảnh Gương. Bốn đèn pha xoay canh giữ đại sảnh.\nTia sáng bắt đầu từ các hướng khác nhau, tạo ra các mẫu ánh sáng luân chuyển.", - "level9Story": "Sâu dưới cung điện, ngọn đuốc ngục tối nhấp nháy.\nÁnh sáng của lính vàng nhấp nháy bật tắt. Di chuyển vào lượt tối.", - "level10Story": "Một đường hầm nối ngục tối đến các phòng hoàng gia.\nMột lính tím tuần tra dọc các hành lang.", - "level11Story": "Phòng Ngai Vàng. Mọi loại lính canh bảo vệ đại sảnh thiêng liêng này.\nĐỏ, xanh, vàng, tím - tất cả đứng giữa bạn và cánh cửa cuối cùng.", - "level12Story": "Cuối cùng... Phòng Công chúa. Nàng chắc hẳn ở ngay sau cánh cửa kia.\nLính canh ở đây là tinh nhuệ nhất vương quốc. Nhưng bạn đã đi quá xa để dừng lại." + "level3Story": "Khu vườn rau củ rải rác lính canh.\nHai lính đỏ tuần tra giữa các luống rau. Tìm đường đi giữa ánh sáng của họ.", + "level4Story": "Đèn pha xoay quét qua cổng khu vườn.\nTia sáng quay theo chiều kim đồng hồ mỗi lượt. Canh thời điểm kỹ lưỡng.", + "level5Story": "Ngọn đuốc pháo đài nhấp nháy theo nhịp kỳ lạ.\nÁnh sáng lính vàng nhấp nháy bật tắt. Di chuyển vào lượt tối.", + "level6Story": "Cổng pháo đài kết hợp đèn lồng cố định với đuốc nhấp nháy.\nBa lớp phòng thủ. Kiên nhẫn, đếm nhịp nhấp nháy.", + "level7Story": "Đường hầm uốn lượn dưới tường pháo đài.\nLính tím tuần tra dọc hành lang. Dự đoán đường đi của hắn.", + "level8Story": "Thử thách cam go. Hai lính tuần tra đan xen trong bóng tối.\nĐường đi của họ đan nhau như bánh răng. Tìm khe hở.", + "level9Story": "Cung điện hoàng gia. Hai con đường — một ngắn, một dài.\nLời cảnh báo của tù nhân xưa: \"Không phải đường tắt nào cũng như vẻ ngoài.\"", + "level10Story": "Đại sảnh Gương. Đèn pha xoay phản chiếu qua các bề mặt pha lê.\nTia sáng phản xạ và nhân lên. Theo dõi nơi ánh sáng bẻ cong.\nNghe đồn Công chúa có thể cảm nhận mọi sinh vật sống gần nàng...", + "level11Story": "Phòng Ngai Vàng. Mọi loại lính canh bảo vệ đại sảnh thiêng liêng này.\nCố định, xoay, nhấp nháy, tuần tra — tất cả đứng giữa bạn và cánh cửa cuối.\nKhông ai vào phòng bên kia trở về. Không phải vì lính canh...", + "level12Story": "Cuối cùng... Phòng Công chúa. Nàng chắc hẳn ở ngay sau cánh cửa kia.\nMột hơi ấm lạ tỏa ra từ bên trong. Không khí như đang dõi theo bạn.\nBạn đã đi quá xa để dừng lại. Nhưng có gì đó mách bảo... nàng đã biết bạn ở đây." } diff --git a/src/scenes/Game.svelte b/src/scenes/Game.svelte index 066527c..46be84b 100644 --- a/src/scenes/Game.svelte +++ b/src/scenes/Game.svelte @@ -30,10 +30,19 @@ let showFlash = $state(false); let finalMessage = $state(false); - // Derived rendering data - let cells = $derived(grid ? grid.getAllCells() : []); - let turns = $derived(turnManager.turnCount); + // Render version counter — incremented after each state mutation to force + // Svelte 5 to re-derive rendering data (class instances are not proxied) + let renderVersion = $state(0); + + // Derived rendering data (depend on renderVersion to pick up class mutations) + let cells = $derived((renderVersion, grid ? grid.getAllCells() : [])); + let turns = $derived((renderVersion, turnManager.turnCount)); let cellSize = $derived(grid ? Math.min(50, Math.floor(500 / grid.rows)) : 50); + let playerRow = $derived((renderVersion, player ? player.row : 0)); + let playerCol = $derived((renderVersion, player ? player.col : 0)); + let guardSnapshots = $derived((renderVersion, guards.map(g => ({ + row: g.row, col: g.col, type: g.type, direction: g.direction, isOn: g.isOn + })))); // Initialize level function initLevel() { @@ -50,6 +59,8 @@ isPaused = false; finalMessage = false; showFlash = false; + princessAlerted = false; + alertRadius = 0; } onMount(() => { initLevel(); }); @@ -83,15 +94,16 @@ if (!player || !grid) return; if (!player.move(direction)) return; - // Check final level proximity trigger BEFORE turn processing - if (isFinalLevel && checkFinalLevel()) return; - const result = turnManager.nextTurn(grid, player, guards); - // Force reactivity by reassigning - grid = grid; - guards = [...guards]; - player = player; + // Escalating princess detection — expands light wave after guard updates + if (isFinalLevel && checkFinalLevel()) { + renderVersion++; + return; + } + + // Bump version to trigger re-derivation of cells/turns/guard positions + renderVersion++; if (result.levelComplete) { handleLevelComplete(); @@ -100,25 +112,42 @@ } } + // Escalating detection: light radiates outward from goal one ring per turn + let princessAlerted = $state(false); + let alertRadius = $state(0); + function checkFinalLevel() { const distance = Math.abs(player.row - goalRow) + Math.abs(player.col - goalCol); - if (distance <= 2) { - lightUpEntireMap(); - return true; + if (distance <= 4 && !princessAlerted) { + princessAlerted = true; + finalMessage = true; + alertRadius = 1; + lightRing(alertRadius); + renderVersion++; + return false; // don't block — let the wave chase the player + } + if (princessAlerted) { + alertRadius++; + lightRing(alertRadius); + renderVersion++; + // Check if expanding light reached the player + if (grid.isLight(player.row, player.col)) { + detected = true; + return true; + } } return false; } - function lightUpEntireMap() { - finalMessage = true; + function lightRing(radius) { for (let r = 0; r < grid.rows; r++) { for (let c = 0; c < grid.cols; c++) { - if (!grid.isWall(r, c)) grid.setLight(r, c, true); + const dist = Math.abs(r - goalRow) + Math.abs(c - goalCol); + if (dist <= radius && !grid.isWall(r, c)) { + grid.setLight(r, c, true); + } } } - grid = grid; // trigger reactivity - // Auto-detect after brief pause - setTimeout(() => { detected = true; }, 1500); } function handleLevelComplete() { @@ -170,8 +199,8 @@ {cellSize} oncellclick={onCellClick} /> - - {#each guards as guard} + + {#each guardSnapshots as guard} {/each} diff --git a/src/styles/theme.css b/src/styles/theme.css index c906a79..b6ecd14 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -29,6 +29,7 @@ --guard-blinking: #ffdd44; --guard-blinking-off: #887722; --guard-patrolling: #bb44ff; + --guard-mirror: #44ddaa; /* Player */ --player-color: #111111;