Commit Graph
118 Commits
Author SHA1 Message Date
tiennm99 b69b4ad141 fix(deps): force postcss >= 8.5.18 to close GHSA-r28c-9q8g-f849
PostCSS auto-loaded any path named in a sourceMappingURL comment as a
previous source map, so crafted CSS could disclose arbitrary .map files.
Transitive devDependency of Vite; overridden the same way esbuild already
is, which lifts every consumer regardless of the version Vite asked for.

Resolves to 8.5.23.
2026-07-26 00:39:35 +07:00
tiennm99 dcafb100de docs: sync documentation and briefings with shipped behavior
Level briefings promised mechanics the levels did not contain: a blinking
guard on five, a patroller on seven, two mirrors and two rotating beams on
eight, a chaser on nine. A briefing is a planning input in a puzzle game,
so a wrong one makes a fair level feel unfair.

- rewrite briefings for levels four, five, seven, eight and nine in both
  locales, verified against the level definitions
- correct the design doc's level table and warm-tile behavior
- record that level twelve's unsolvability is intentional and CI-pinned
- document offline play, the unshipped-assets directory, and Node 24
- keep the audit and phase reports alongside the plan
2026-07-26 00:35:50 +07:00
tiennm99 4f9973177c ci: run the test suite before deploying to pages
243 tests existed and none of them gated a deploy.
2026-07-26 00:35:50 +07:00
tiennm99 3000146886 feat(pwa): precache the app shell so the game plays offline
There was no service worker, so a cold load with the network off failed at
the HTTP layer before any of this dependency-free code could run.

scope and start_url are relative because the site is served from a Pages
subpath, not a domain root. Icons are kept to files the precache glob does
not already sweep up: listing one twice under two revisions makes Workbox
reject its install handler, and the worker then activates with an empty
cache, which looks identical to success until the network is pulled.

Verified by serving the build from a /nntv/ subpath, killing the server,
and reloading: renders and starts a run with zero network requests.
2026-07-26 00:35:50 +07:00
tiennm99 2c8b0dc89b perf(assets): ship key art as webp and drop unreferenced media
Fifty-seven percent of the built output was reachable by nothing but a
manifest that is never fetched at runtime. The key art documented as the
menu background was never wired up; a 1.2KB placeholder was loaded instead.

- wire key art into the menu, ending screen and act four to six intros
- re-encode key art to webp: 1.63MB to 556KB at DSSIM around 0.012
- move 3.9MB of unwired voice lines and authoring sources out of the
  bundle into unshipped-assets, still tracked rather than deleted
- add 192 and 512 icons, the sizes an install prompt requires
2026-07-26 00:35:36 +07:00
tiennm99 f883c91b1e fix(audio): start background music after a blocked autoplay retry
activeTrack was assigned before play() could reject, so the retry bound to
the first user gesture hit the same-track early return and menu music never
started on any browser that blocks autoplay. Also drops crossOrigin, which
is inert for same-origin audio but forces CORS-mode fetches through the
service worker and breaks file:// use.
2026-07-26 00:35:36 +07:00
tiennm99 2a361c09b4 feat(ux): responsive board, touch controls, hints and mercy unlock
Difficulty should come from the puzzle, not the interface. The stage was
hard-coded to 1024x768 with no media queries, stone throwing was bound to
the E key with no on-screen control, and a stuck player had no way forward
because progress advanced only on a win.

- scale the board down to 360px so the 13x13 grid fits a phone
- on-screen throw entry with 44px confirm and cancel targets
- escalating hints at three, five and seven attempts; mercy unlock at eight
- level-complete popup no longer renders the literal key name
- keyboard retry is immediate instead of seven tabs through hidden buttons
- delay the detection overlay so the death flash stays visible
- suspicion range shown before the guard reaches tier one
- dialog roles, focus trap and scene-transition focus management
- localized level names and turn counter, dead aria keys removed
2026-07-26 00:35:24 +07:00
tiennm99 eeb3a711ee feat(levels): retune levels one through eleven for real difficulty
Levels were authored against guards that did nothing, so the finale fell
to a twenty-move walk down two edges. Guard tax is now non-zero on every
level with guards, and search depth ramps from 50 states on the opener to
251612 on the throne room.

- fix the perimeter-walk exploit on levels four, eight, nine, ten, eleven
- unblock the mirrors on seven and ten, move guards off wall cells
- phase-align rotating, sniper and static cycles to arrival turns
- tighten par so three stars is earned rather than free
- restore preview on eleven while leaving undo disabled

Level twelve is untouched and remains unsolvable by design.
2026-07-26 00:35:10 +07:00
tiennm99 ab9da7666a test(levels): assert guard tax and authoring invariants
The suite asserted only that a level was solvable and within par. With no
lower bound, a level whose guards did nothing passed clean, which is how
eleven degenerate levels shipped green.

- ablation: solve with guards, then without, and require a minimum tax
- per-mechanic necessity: removing a level's named mechanic must change it
- authoring: no guard or patrol node on a wall, mirrors reachable by a beam,
  sniper beams non-trivial, par not slack enough to make three stars free
- memoized baselines keep the run near 20s despite doubling the solves
2026-07-26 00:35:10 +07:00
tiennm99 ebff1ad019 fix(engine): make guard mechanics actually execute
Four defects left the stealth mechanics inert: warm tiles were cosmetic,
mirror beams never reflected, static guards died permanently after a few
turns, and suspicion guards reset their own tier every cycle. Deleting
every guard from every level changed the optimal path by zero moves.

- warm tiles are lethal and their decay window survives the turn cycle
- rotating and sniper beams reach mirrors and reflect
- static guards regrow, suspicion tiers persist
- a mobile guard's own cell is lethal, and cell-swap counts as detection
- turn preview simulates the player's move instead of a stationary player
- a rejected one-way move no longer opens the door it was refused
- line-of-sight extracted to one module shared by engine and solver

Every rule change lands in both the turn engine and the BFS solver so CI
cannot certify a level that a human cannot play.
2026-07-26 00:34:57 +07:00
tiennm99 eb800f1ce0 fix(deps): bump vite security patch 2026-07-01 09:00:14 +07:00
tiennm99 c2c8ad0ab2 feat(assets,audio): regenerate broken keyart/welcome, add VI voices, wire BGM
Asset review + regeneration
- Regenerate keyart-palace.png to remove STUDIOSWAG watermark
- Regenerate keyart-portrait.png as true 16-bit pixel art (was 3D/cinematic)
- Regenerate vi-welcome.mp3 as a 31KB short line (was 808KB misplaced story)
- Generate 10 missing VI voice lines (levelclear, princess-help, princess-run,
  gameover, 6 act intros) in Vietnamese_kindhearted_girl
- Replace broken PHASER logo + 16x16 favicon with proper NNTV wordmark + 32x32
  rabbit sprite
- Add UI scene backgrounds (scene-menu/story/guide/settings) for in-app use
- Add 6 keyart images (1280x720) + portrait (1024x1024) + poster for store/social
- Delete orphan assets: bg.png, nntv-generated-sheet.png, scraps/sketch.napkin
- Add MANIFEST.json + MEDIA.json documenting the asset corpus

BGM integration
- New src/lib/bgm.js: HTMLAudioElement BGM manager with 600ms crossfade,
  per-track loop, autoplay-blocked fallback, persistent sfxMuted/bgmMuted/
  bgmVolume in localStorage (key: nntv-audio-settings)
- App.svelte scene router picks the track per scene; L12 uses the climactic
  chamber track, all other Game scenes use the levels loop
- audio.js: setMuted/isMuted now mirror bgm.js so GameHud's SND/MUTE button
  silences both SFX and BGM (master toggle)
- Settings.svelte: new Audio panel with SFX toggle, BGM toggle, BGM volume
  slider (0-100%, disabled when BGM is off); bilingual
- MainMenu/StoryIntro/Guide/Settings: top band scene backdrops at ~0.5 opacity
- New i18n keys: audioSettings, sfxLabel, bgmLabel, volumeLabel, on, off,
  muted, sound (en + vi)
- GameHud label localized (SND / MUTED)

Tests: 180/180 passing. Build: clean (139KB JS / 28KB CSS gzipped).
2026-06-17 17:08:50 +07:00
tiennm99 2e60dd1371 fix(deps): pin esbuild to >=0.28.1 to close GHSA-gv7w-rqvm-qjhr
Dependabot alert #16 (high, CVSS 8.1) flags esbuild < 0.28.1 for missing
SHA-256 integrity verification in the Deno download path. Even though the
npm/Vite path used by this project is not directly exploitable, bump
esbuild to the patched release via a pnpm-workspace.yaml override so the
alert closes.

esbuild 0.28.x dropped support for transforming destructuring to the
Vite 6 default browser target set (chrome87/firefox78/safari14), so
also pin build.target and esbuild.target to es2022 in vite.config.js.

Verification:
- pnpm build: success (173 modules, 1.71s)
- pnpm test: 180/180 pass
2026-06-15 09:03:32 +07:00
tiennm99 968e3179a1 feat(art): integrate generated pixel-art assets for sprites and scenes
Replace procedural pixel art with pre-generated PNGs for player, princess,
guards (8 types + suspicion tiers), tiles, keys, stones, and scenes.
Add src/lib/generated-assets.js as the central path registry and update
all sprite/scene components to consume it.
2026-05-16 14:36:36 +07:00
tiennm99 1d883d4dde fix(deps): bump svelte to 5.55.7 and devalue to 5.8.1
Resolves dependabot alerts:
- svelte <=5.55.6: XSS via DOM clobbering, SSR spread XSS,
  hydratable promise XSS, <svelte:element> ReDoS
- devalue 5.6.3-5.8.0: DoS via sparse array deserialization
2026-05-15 09:32:14 +07:00
tiennm99 f519612972 chore(ci): bump node to 24 2026-05-13 10:52:15 +07:00
tiennm99 fa6e7b068e fix(ci): bump Node.js to 22 required by pnpm@11.1.1 2026-05-13 10:33:41 +07:00
tiennm99 d3de00f98b fix(ci): remove pnpm version override conflicting with packageManager 2026-05-13 10:28:38 +07:00
tiennm99 a9d01990a1 chore: migrate from npm to pnpm 2026-05-13 10:20:14 +07:00
tiennm99 41d500fa19 chore: align package.json license with LICENSE file (Apache-2.0) 2026-05-11 20:18:48 +07:00
tiennm99 d9784582cb docs: write substantive README 2026-05-11 20:16:58 +07:00
tiennm99 e1b1842a8e chore: relicense from MIT to Apache-2.0 (sole-author repo) 2026-05-11 17:15:41 +07:00
tiennm99 1eaa9e73f7 chore(plans): remove superseded reports
All three reports are historical artifacts:
- v2 brainstorm: shipped (commit 149e07a, journal in docs/journals/)
- 260421 level-redesign brainstorm: fully superseded by v2
- 260420 full-project review: pre-v2; findings addressed by v2

Preserved in git history. Removing from working tree.
2026-04-26 08:41:43 +07:00
tiennm99 54cf809e6c chore(plans): remove completed v2 tight-12 plan + reports
The plan and all phase reports are preserved in git history (commit
149e07a). Removing from working tree as the work is fully shipped.

Adds the v2 ship journal entry under docs/journals/.
2026-04-26 08:40:06 +07:00
tiennm99 88a5c973f3 feat(audio): wire door-unlock cue on locked-door count delta
Adds a reactive effect that fires playDoorUnlock when the count of
locked doors decreases — i.e. when player.moveTo opens a door with a
matching key. Mirrors the existing keysHeld delta pattern.

Closes the door-audio gap documented in plans/260425-1907-tight-12-design-uplift/.
2026-04-26 08:36:50 +07:00
tiennm99 305d904613 docs(plans): add v2 plan + phase reports
Brainstorm-to-ship plan with 6 phase files + 04.5 patch report + code review
report.
2026-04-26 08:27:35 +07:00
tiennm99 9fea2ecc9d 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
tiennm99 4e7b58405a feat(ui): add throw-targeting, affordance gates, suspicion overlays, key inventory
Render new tile types (door/key/one-way/warm) and new guard sprites (sniper
triangle+beam, suspicion ring). Five new HUD components. Throw-targeting state
machine (E enter, arrows/click move, Enter/click confirm, Esc cancel). Per-level
affordance gates wired to Z/Y/V key handlers. GameHistory snapshots extended to
round-trip keysHeld + key/door cell state for correct undo.
2026-04-26 08:27:30 +07:00
tiennm99 bb8d3a61d0 feat(levels): redesign 11 solvable levels around new mechanics
Full L1-L11 overhaul around 6-mechanic palette. One mechanic intro per level
(one-way L3, suspicion L4, doors+keys L5, decay L6, mirror L7, sniper L8,
stones L9). L10-L11 compounding. L12 byte-identical (unsolvable narrative
preserved). All BFS-verified under 2M nodes.
2026-04-26 08:27:28 +07:00
tiennm99 f498284c9c feat(player): enforce doors, one-ways, key auto-collection on movement
Player.moveTo enforces wall/door/one-way validation with direction-of-approach
matching. keysHeld bitmask auto-collects on stepping on key cell. Capture/apply
includes inventory state.
2026-04-26 08:27:26 +07:00
tiennm99 8f22138774 refactor(death): replace lives counter with level-restart on detection
Drop run-wide lives state. Detection routes to level-restart instead of
game-over. Repurpose GameOver scene for run-complete (L11) and bittersweet
(L12) flows. One-shot save migration wipes legacy lives field with welcome
modal.
2026-04-26 08:27:25 +07:00
tiennm99 e550600218 feat(solver): canonicalize state hash, extend BFS for new mechanics, 2M node cap
Replace JSON.stringify with stable canonical state hash. Extend captureState
with stones, keys, key/door cell snapshots, warm timers. Enumerate throw-target
actions with eligibility pruning. MAX_BFS_NODES = 2_000_000 with par-based path
pruning. All 11 solvable levels under cap, L12 unsolvable preserved.
2026-04-26 08:27:23 +07:00
tiennm99 09f804c2ea feat(engine): add sniper, suspicion, throwable, doors/keys/one-way cells
New cell flags (door, key, one-way, warm). SniperGuard with line-of-sight beam
+ mirror reflection. SuspicionGuard with 3-tier meter. ThrowableSystem with
stone-throw distraction resolved before guard turn. Capture/apply round-trip
preserves all new state.
2026-04-26 08:27:21 +07:00
tiennm99 89943829c0 chore(levels): tighten L1/L2 par values to optimal+2/+4
L1 par 26\u219222 (slack 2). L2 par 30\u219224 (slack 4, leaves room for
wait-strategy on wilting tomatoes). Other levels already at spec.
2026-04-21 23:29:21 +07:00
tiennm99 ba687ea130 feat(game): wilting tomato mechanic replaces static light
Static guards now emit a Manhattan aura that shrinks by 1 per turn until
harmless (clamped at -1 to keep solver state finite). Level data schema
change: 'static' guards now take 'initialRadius' instead of 'litCells'.
All 27 static-guard entries migrated in one pass.

L1 and L2 redesigned with real forced-zigzag walls — solver path is now
20 moves (vs Manhattan 14), so players must move AWAY from the goal to
reach it. L2 adds three wilting tomatoes parked on the zigzag path as
a gentle intro to the mechanic.

Tests: +3 new tests on StaticGuard wilting behavior; all 70 tests pass.
2026-04-21 22:40:53 +07:00
tiennm99 05cc053cb6 docs: sync README and docs with redesigned levels
Reflect new grid sizes (8x8-13x13), chaser guard type, L12 easter egg,
and solvability CI guard. Remove lingering mobile-support claims.
Delete completed level-redesign plan directory (preserved in git history).
2026-04-21 21:41:33 +07:00
tiennm99 90e6958d41 docs(nntv): document solver, viewport, and L12 easter egg
Enable wall-light overlap lint (previously skipped); clean up remaining
overlaps in L4, L5, L8, L11. Add solver and easter-egg sections to
system-architecture.md. Extend code-standards.md review checklist with
solvability requirements.
2026-04-21 21:35:26 +07:00
tiennm99 02f85ef115 feat(levels): redesign L12 Princess Chamber and add console easter egg
L12 widened to 13x13 with 10 guards including 2 chasers and a mirror.
Remains unsolvable by normal play (BFS-verified). window.__nntvDev.teleport
exposed as an intentional escape hatch — console-savvy users who invoke it
win the game. Hidden from README and in-game UI.
2026-04-21 21:31:14 +07:00
tiennm99 2f122d1896 feat(levels): redesign L11 Throne Room on 12x12 grid
Climactic Act 6 level — 9 guards covering ALL six guard types
(static, rotating, blinking, patrolling, mirror, chaser). BFS-proven
solvable in 22 moves. All playable levels (L1-L11) now verified
solvable; only L12 remains unsolvable by design.
2026-04-21 21:29:20 +07:00
tiennm99 f3394bd15f feat(levels): redesign L9 and L10 on 12x12 grid
Act 5 redesign — chaser guard introduced. L9 has 8 guards with the
chaser ambushing the short route, forcing a perimeter detour. L10 is
the Hall of Mirrors with 9 guards (2 rotating + 3 mirrors + chaser).
Both solvable under 50 ms via BFS.
2026-04-21 21:28:03 +07:00
tiennm99 014a55199d feat(levels): redesign L7 and L8 on 11x11 grid
Act 4 redesign — mirror guard introduced. L7 has 7 guards including
one mirror that deflects a rotating beam. L8 has 8 guards with two
mirrors creating a crossfire pattern.
2026-04-21 21:26:32 +07:00
tiennm99 aa3ea39b71 feat(levels): redesign L5 and L6 on 10x10 grid
Act 3 redesign — patrolling guard introduced in L5 (6 guards total).
L6 widens with two patrollers, two blinkers, and a rotating guard
(7 guards). Both solvable via BFS solver.
2026-04-21 21:23:57 +07:00
tiennm99 0aad28e81d feat(levels): redesign L3 and L4 on 9x9 grid
Act 2 redesign — rotating guard (L3) and blinking guard (L4) introduced.
Both levels widened to 9x9. L4 solution requires a wait action to
synchronize with the blinking cycle, teaching the timing mechanic.
2026-04-21 21:22:30 +07:00
tiennm99 33f6c98a14 fix(levels): redesign L1 and L2 on 8x8 grid
L2 right-side region was disconnected — player could not reach the goal
via any sequence of moves (verified via BFS). New L2 places three static
guards with two viable paths. L1 widens to 8x8 as a gentle Act 1 intro.
Preserves id/name/storyKey/isFinalLevel for progress persistence.
2026-04-21 21:20:25 +07:00
tiennm99 bfed64d871 test(levels): add solvability suite with metadata invariants
Asserts L1/5-8/10 solvable, L12 unsolvable (easter-egg invariant), and
locks the 12-level count. Known-broken levels (L2/3/4/9/11) tracked
explicitly and auto-unskip when their redesign phases flip them.
2026-04-21 21:20:19 +07:00
tiennm99 19e3e9cb67 feat(game): add BFS level solver for solvability validation
Pure BFS over (player, guard snapshots, princess state). Reuses guards.js
and princess-mechanic.js so solver verdicts match runtime behavior. State
budget cap prevents runaway search on impossible levels.
2026-04-21 21:20:12 +07:00
tiennm99 0bf72c50d8 feat(game): scrollable viewport with camera-follow for large grids
Fix cellSize at 50 and wrap board-container with overflow:auto + smooth
scrollTo effect centered on player. Unlocks grid sizes beyond 9x9 without
shrinking cells below readable size.
2026-04-21 21:20:04 +07:00
tiennm99 6e2114162f docs(plans): add level redesign brainstorm and 11-phase plan
Fixes L2 unsolvable bug and widens grids across all 12 levels.
L12 preserved as unsolvable-by-normal-play (easter egg).
2026-04-21 21:19:56 +07:00
dependabot[bot] 692d90cafb build(deps): bump rollup from 4.40.0 to 4.60.1 (#7)
Bumps [rollup](https://github.com/rollup/rollup) from 4.40.0 to 4.60.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.40.0...v4.60.1)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.60.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 22:55:58 +07:00
dependabot[bot] 11860bcda3 build(deps-dev): bump vite from 6.3.6 to 6.4.2 (#6)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 22:54:45 +07:00