diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 0000000..1ae78e5
--- /dev/null
+++ b/.claude/launch.json
@@ -0,0 +1,6 @@
+{
+ "version": "0.0.1",
+ "configurations": [
+ { "name": "not-tonight", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"], "port": 5199 }
+ ]
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b947077
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+dist/
diff --git a/CLAUDE.md b/CLAUDE.md
index 3d80ad1..3ce34f3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -29,6 +29,24 @@ by parallel "lanes" coordinated through files, reviewed by Fable.
branch. An unpushed session didn't happen.
- Remote is Gitea: `ssh://git@100.71.119.27:222/monster/not-tonight.git`.
+## Dev notes (Phase 0 landed 2026-07-19)
+- Gate: `npm run lint && npm run build && npm test` — all three must pass before
+ any push. Dev server: `npm run dev` (port 5199, config in `.claude/launch.json`).
+- Key APIs (read the source, it's short): `core/EventBus` (`on` returns an
+ unsubscribe fn), `core/SeededRNG` (`rng.stream(name)` — NEVER `Math.random`,
+ eslint blocks it), `core/GameClock` (feed real ms via `update(delta)` from your
+ scene; `ageOn(dob, nightDate)` is the ID math), `core/meters` (emit
+ `meters:delta`, never mutate NightState), `patrons/generator`
+ (`generatePatron(ctx, clockMin, override?)`), `patrons/dollPlan` (pure,
+ testable) + `patrons/doll` (`renderDoll(scene, patron, pose)` → texture key),
+ `rules/drunk` (`drunkStage`). `StubBeatClock` needs an owning scene calling
+ `update(delta)` each frame.
+- **Verifying in Claude's browser preview pane:** the pane tab reports
+ `document.hidden = true`, so Chrome suspends rAF and the Phaser loop freezes at
+ frame 0. Drive frames manually via the exposed debug handle:
+ `const b = performance.now(); for (let i = 1; i <= 900; i++) window.game.loop.step(b + i * 16.67)`
+ then screenshot/inspect. In a normal browser the game runs fine.
+
## Tone guard
The game is Aussie hospitality satire — affectionate, funny, with a moral undertow
(design doc §4.3). If content you're writing reads as mean-spirited rather than
diff --git a/LANEHANDOVER.md b/LANEHANDOVER.md
index c3db89f..05b1f95 100644
--- a/LANEHANDOVER.md
+++ b/LANEHANDOVER.md
@@ -46,3 +46,52 @@ BOTTOM. Push after every session so the reviewer can see state remotely.
---
*(log starts below)*
+
+---
+### SESSION — LANE-0 (executed by Fable, reviewer) — 2026-07-19 16:30
+**Branch/commits:** main (reviewer executed Phase 0 directly; no lane branch)
+**Done:**
+- Vite + strict-TS + Phaser 3.90 + Vitest + ESLint scaffold; `Math.random` banned by lint rule.
+- All CONTRACTS.md types verbatim in `src/data/types.ts`.
+- `core/`: SeededRNG (mulberry32, named streams), typed EventBus (snapshot-emit
+ semantics), GameClock (real-ms driven, `ageOn` birthday math incl. leap-day),
+ Meters (single-writer, hype multiplies positive vibe only, clicker drift),
+ StubBeatClock (128 BPM), versioned corrupt-safe localStorage save.
+- `data/`: outfit vocabulary (+PALETTE), 9 archetypes, 10 contraband items,
+ 17 Dazza texts (8 rule-bearing with ruleIds).
+- `patrons/`: generator (fake-ID tell system, DOBs clustered near the 18
+ boundary, late-arrival drunk drift), pure `dollPlan` + Phaser `renderDoll`
+ (3 poses, drunk tells baked), regulars' memory store.
+- `rules/drunk.ts`: drunkStage thresholds.
+- ParadeScene demo: rainy street, kebab-shop + venue dressing, 24-walker parade
+ with ID cards rendered from the same data, SPACE reseed / D toggle cards.
+- 49 tests green; lint/build/test gate clean.
+**How to see it:** `npm run dev` → http://localhost:5199 (in the Claude preview
+pane, drive frames per CLAUDE.md "Dev notes"). Watch for an `almost18` label
+walking under an ID card claiming 18+ — that's the whole game in one image.
+**Tests:** 49 passing / 0 failing.
+**Broke / known-wonky:**
+- ID cards overlap when the footpath is crowded — cosmetic, parade-only.
+- `window.game` debug handle exposed in main.ts (deliberate, for pane-driving).
+- Spec deviations: doll renderer split into `dollPlan.ts` (pure) + `doll.ts`
+ (Phaser) — better than spec'd, plans are pixel-testable without a GPU; added
+ a 5th fake-ID tell (expired real card, no boolean tells); no MainMenu scene
+ (Boot → Parade direct) — menu is Phase 2 shell work.
+**Contract change requests:** none (contracts implemented as written).
+**Questions for reviewer:** n/a (self-reviewed).
+**Next session should:** Phase 1 lanes are OPEN — see REVIEW block below.
+
+---
+### REVIEW — Fable — 2026-07-19
+**Reviewed:** Phase 0 (self-executed; verified via test gate + in-browser parade).
+**Merged to main:** yes (direct).
+**Contract decisions:** CONTRACTS.md now frozen. Extension requests via this file.
+**Instructions for Phase 1 (LANE-DOOR, LANE-FLOOR, LANE-JUICE):**
+1. Branch from current main into `lane/door` / `lane/floor` / `lane/juice`
+ (worktrees if sharing a machine). Read your lane file's new
+ "Phase-0 reality" section FIRST — it corrects the original spec where the
+ landed code differs.
+2. LANE-DOOR is the critical path; if only one Opus session is available, run
+ DOOR first, JUICE second, FLOOR third.
+3. Every session ends with the gate + a SESSION block here + a push. Fable
+ reviews on request ("review the lanes").
diff --git a/docs/ASSETS.md b/docs/ASSETS.md
index af36fec..a7c37bd 100644
--- a/docs/ASSETS.md
+++ b/docs/ASSETS.md
@@ -73,11 +73,20 @@ worth cloning for an `art_src/` pipeline here.
## Ground rules for the eventual art pass
+**Licensing: none needed.** This is a private tailnet sandbox game, never public —
+John's call (2026-07-19). Anything on the fleet or gdrive is fair game, including
+the audio archives and MONSTER MIXES. (If that ever changes and the game goes
+public, this section is the checklist to revisit first.)
+
1. Everything funnels through the existing contracts (`renderDoll` poses, outfit
slots) — art swaps in behind interfaces, callers never change.
2. One palette, one pixel density (32×48 queue dolls at 640×360) decided ONCE
before any batch conversion; retrofitting mixed densities is the classic trap.
-3. Provenance: only fleet-owned / John-authored / MODELBEAST-generated / CC0
- sources. Log each imported batch (source → transform → dest) in this file.
+3. Log each imported batch (source → transform → dest) in this file — not for
+ licensing, just so we can regenerate/retrace later.
4. Check the MeshGod gallery + MODELBEAST `data/` (m3ultra) before generating
anything — the inventory's rule "catalog before regenerating" applies here too.
+5. The synthesized techno engine stays the core soundtrack (the location-filter
+ trick needs a synth we control), but real archive audio is now allowed as
+ layers/easter eggs — e.g. MONSTER MIXES on a kebab-shop jukebox, real crowd
+ ambience beds under the synth.
diff --git a/docs/BUILD_PLAN.md b/docs/BUILD_PLAN.md
index aab291e..6b08d4b 100644
--- a/docs/BUILD_PLAN.md
+++ b/docs/BUILD_PLAN.md
@@ -42,12 +42,13 @@ CONTRACT CHANGE REQUEST in the handover file and reviewer sign-off — never jus
## 3. Phases
-### Phase 0 — Scaffold & Contracts (ONE lane, sequential — everything depends on it)
-`lanes/LANE0_SCAFFOLD.md`. Deliverable: repo compiles, tests green, and a **Patron
-Parade** demo scene proves the generator + paper-doll renderer (generated patrons
-walk across the screen; ID card renders from the same data). All shared types in
-`src/data/types.ts` matching CONTRACTS.md. Event bus + seeded RNG + game clock done
-and tested.
+### Phase 0 — Scaffold & Contracts — ✅ DONE 2026-07-19 (executed by Fable)
+`lanes/LANE0_SCAFFOLD.md`. Delivered: strict-TS Vite+Phaser scaffold, all
+CONTRACTS.md types in `src/data/types.ts`, core systems (`SeededRNG`, `EventBus`,
+`GameClock`+`ageOn`, `Meters`, `StubBeatClock`, `save`), patron generator +
+paper-doll renderer (pure `dollPlan` + Phaser `renderDoll`), 49 unit tests, and
+the Patron Parade demo verified in-browser. See the LANE-0 SESSION block in
+LANEHANDOVER.md for API notes Phase-1 lanes must read.
### Phase 1 — Parallel lanes (after Phase 0 review passes)
Three lanes run concurrently on separate branches/worktrees:
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..25f55c7
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,17 @@
+import js from '@eslint/js';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ rules: {
+ // All randomness must flow from SeededRNG streams — determinism is a contract.
+ 'no-restricted-properties': [
+ 'error',
+ { object: 'Math', property: 'random', message: 'Use SeededRNG streams (docs/CONTRACTS.md §6).' },
+ ],
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+ },
+ },
+);
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d612d64
--- /dev/null
+++ b/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ NOT TONIGHT
+
+
+
+
+
+
diff --git a/lanes/LANE0_SCAFFOLD.md b/lanes/LANE0_SCAFFOLD.md
index 9ba448a..35c1cfe 100644
--- a/lanes/LANE0_SCAFFOLD.md
+++ b/lanes/LANE0_SCAFFOLD.md
@@ -1,5 +1,9 @@
# LANE-0 — Scaffold & Contracts (Phase 0, single lane)
+> **✅ EXECUTED 2026-07-19 by Fable (reviewer) directly on main.** Kept for
+> reference. Deviations from the original spec are noted in the LANE-0 SESSION
+> block in LANEHANDOVER.md — that block is the accurate record.
+
**Executor:** Opus 4.8. **Branch:** `lane/scaffold`. **Prereq reading (in order):**
`docs/GAME_DESIGN.md` → `docs/BUILD_PLAN.md` → `docs/CONTRACTS.md` → `LANEHANDOVER.md`.
Everything downstream depends on this lane being solid. Favour boring correctness
diff --git a/lanes/LANE_DOOR.md b/lanes/LANE_DOOR.md
index 0612d5d..2814060 100644
--- a/lanes/LANE_DOOR.md
+++ b/lanes/LANE_DOOR.md
@@ -10,6 +10,30 @@ REVIEW blocks addressed to you).
as a Door-only night. Bias every decision toward "playable and funny" over
"architecturally complete".
+## Phase-0 reality (read before coding)
+Phase 0 landed 2026-07-19 — see CLAUDE.md "Dev notes" for the API map and the
+browser-pane verification trick. Door-relevant specifics:
+- `rules/drunk.ts` already exists (drunkStage thresholds + `hasVisibleTells`) —
+ build `dressCode.ts`, `idCheck.ts`, `judge.ts` beside it.
+- ID date math: `ageOn(dob, nightDate)` in `core/GameClock` and
+ `idAge(patron, nightDate)` in `patrons/generator` exist and are tested —
+ `idCheck.ts` should build on them, not reimplement.
+- Fake-ID tells now include a fifth: an **expired real-looking card** (fake
+ present, all tell booleans false, expiry in the past). Your `idVerdict` must
+ catch it — see `tests/generator.test.ts` "every fake has a tell".
+- Patron generation: `generatePatron({rng, nightDate}, clockMin, override?)`;
+ ids are serial (`p0…`), reset per run via `_resetPatronSerial()`.
+- `renderDoll(scene, patron, 'queue')` returns a texture key; drunk tells are
+ baked per sway-bucket (see `dollTextureKey`). For the ×3 patron-up view just
+ scale the image — pixelArt rendering keeps it crisp.
+- `ParadeScene` shows the working pattern for bus/clock/meters lifecycle
+ (`resetRun`) — copy that shape, don't invent one. `StubBeatClock` requires
+ your scene to call `update(delta)` each frame.
+- Dazza strings live in `data/strings/dazza.ts` keyed with `ruleId`s — your
+ `dressCode.ts` rule ids must match them (8 rule-bearing texts are already
+ written: noThongsSinglets, noLogos, noWhiteSneakers, noSongRequesters,
+ noBucketHats, noSunnies, noBlazers, noHandHolders).
+
## Deliverable
`npm run dev` → full Door-only night: 9:00 PM to 3:00 AM, patrons queue and step
diff --git a/lanes/LANE_FLOOR.md b/lanes/LANE_FLOOR.md
index cc147bc..edd4060 100644
--- a/lanes/LANE_FLOOR.md
+++ b/lanes/LANE_FLOOR.md
@@ -12,6 +12,21 @@ infractions (cut-off, toilet stall, pat-down) + UV stamp checks. Runs standalone
it self-populates from the patron generator; integration (Phase 2) will hand it the
real admitted-patron list instead. Code against `Patron[]` input from day one.
+## Phase-0 reality (read before coding)
+Phase 0 landed 2026-07-19 — see CLAUDE.md "Dev notes" for the API map and the
+browser-pane verification trick. Floor-relevant specifics:
+- Drunk thresholds exist in `rules/drunk.ts` (`drunkStage`, `hasVisibleTells`) —
+ no local copy needed; delete that TODO from your plan.
+- `renderDoll(scene, patron, 'floorTopDown')` already produces the 16×16
+ top-down doll (hair blob over shoulders/top colour).
+- `StubBeatClock` exists in core; instantiate it in your demo scene and call
+ `update(delta)` each frame — it emits `beat:tick` at 128 BPM.
+- `generatePatron` + `ParadeScene`'s `resetRun` lifecycle are the reference
+ patterns for self-populating your demo.
+- Register your demo scene in `src/main.ts`'s scene list; launch order stays
+ Boot → Parade. Menu keys (F for floor demo) can live in ParadeScene as a
+ scene-switch keybind — that one-line touch outside your dirs is pre-approved.
+
## Scene: the venue (top-down, ~2× screen scrollable)
Tilemap (hand-authored array is fine, no Tiled dependency): entry corridor, bar
diff --git a/lanes/LANE_JUICE.md b/lanes/LANE_JUICE.md
index 376903c..4d47214 100644
--- a/lanes/LANE_JUICE.md
+++ b/lanes/LANE_JUICE.md
@@ -15,6 +15,26 @@ plus the techno engine playing with a big on-screen low-pass cutoff slider and a
DOOR/FLOOR toggle. Beat indicator flashes on `beat:tick` and visibly matches the
audio.
+## Phase-0 reality (read before coding)
+Phase 0 landed 2026-07-19 — see CLAUDE.md "Dev notes" for the API map and the
+browser-pane verification trick. Juice-relevant specifics:
+- `core/StubBeatClock.ts` is the stub you'll replace: it's scene-update-driven
+ and emits `beat:tick {beatIndex, audioTimeMs}`. Your TechnoEngine must emit the
+ same event/payload (audio-clock authoritative) so consumers don't change.
+- `EventBus.on` returns an unsubscribe fn — widgets should keep them and detach
+ in `destroy()` (see `HudStub` for the pattern your `MeterHud` replaces).
+- `data/outfits.ts` exports `PALETTE` (name → hex) — use it for widget colours
+ so UI and dolls share one palette.
+- `door:phoneTheatre` is NOT in the EventMap yet — adding it is a worked example
+ of the CONTRACT CHANGE REQUEST flow; file it in your first handover.
+- Audio can't be heard in the Claude browser pane — verify SFX/engine by ear in
+ a real browser, and note in your handover what was ear-checked vs only
+ logic-checked.
+- License note: per docs/ASSETS.md this is a private sandbox — real audio
+ samples are ALLOWED as layers/ambience if synthesis fights you, but the
+ location-filter architecture (music through the lowpass, SFX around it) is
+ non-negotiable.
+
## Part A — Audio (`src/audio/`) — raw WebAudio, no libraries
### 1. `TechnoEngine.ts` — the looping track
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..4b83e96
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3044 @@
+{
+ "name": "not-tonight",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "not-tonight",
+ "version": "0.0.1",
+ "dependencies": {
+ "phaser": "^3.90.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.0.0",
+ "eslint": "^9.0.0",
+ "typescript": "^5.9.0",
+ "typescript-eslint": "^8.0.0",
+ "vite": "^7.0.0",
+ "vitest": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
+ "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.3.0",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz",
+ "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
+ "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.64.0",
+ "@typescript-eslint/type-utils": "8.64.0",
+ "@typescript-eslint/utils": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.64.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
+ "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.64.0",
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/typescript-estree": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
+ "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.64.0",
+ "@typescript-eslint/types": "^8.64.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
+ "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
+ "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
+ "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/typescript-estree": "8.64.0",
+ "@typescript-eslint/utils": "8.64.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
+ "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
+ "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.64.0",
+ "@typescript-eslint/tsconfig-utils": "8.64.0",
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/visitor-keys": "8.64.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+ "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
+ "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.64.0",
+ "@typescript-eslint/types": "8.64.0",
+ "@typescript-eslint/typescript-estree": "8.64.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
+ "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.64.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.7",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.7",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
+ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.6",
+ "@eslint/js": "9.39.5",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/phaser": {
+ "version": "3.90.0",
+ "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz",
+ "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^5.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.19",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
+ "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.64.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
+ "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.64.0",
+ "@typescript-eslint/parser": "8.64.0",
+ "@typescript-eslint/typescript-estree": "8.64.0",
+ "@typescript-eslint/utils": "8.64.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.7",
+ "@vitest/mocker": "3.2.7",
+ "@vitest/pretty-format": "^3.2.7",
+ "@vitest/runner": "3.2.7",
+ "@vitest/snapshot": "3.2.7",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.7",
+ "@vitest/ui": "3.2.7",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..eb06188
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "not-tonight",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc --noEmit && vite build",
+ "test": "vitest run",
+ "lint": "eslint src tests"
+ },
+ "dependencies": {
+ "phaser": "^3.90.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.0.0",
+ "eslint": "^9.0.0",
+ "typescript": "^5.9.0",
+ "typescript-eslint": "^8.0.0",
+ "vite": "^7.0.0",
+ "vitest": "^3.2.0"
+ }
+}
diff --git a/src/core/EventBus.ts b/src/core/EventBus.ts
new file mode 100644
index 0000000..653e8e7
--- /dev/null
+++ b/src/core/EventBus.ts
@@ -0,0 +1,43 @@
+import type { EventMap } from '../data/types';
+
+type Handler = (payload: EventMap[K]) => void;
+
+// Typed pub/sub. Handlers added/removed during an emit don't affect that emit
+// (we snapshot the handler list), matching what scenes need on shutdown.
+export class EventBus {
+ private handlers = new Map>>();
+
+ on(event: K, fn: Handler): () => void {
+ let set = this.handlers.get(event);
+ if (!set) {
+ set = new Set();
+ this.handlers.set(event, set);
+ }
+ set.add(fn as Handler);
+ return () => this.off(event, fn);
+ }
+
+ once(event: K, fn: Handler): () => void {
+ const off = this.on(event, (payload) => {
+ off();
+ fn(payload);
+ });
+ return off;
+ }
+
+ off(event: K, fn: Handler): void {
+ this.handlers.get(event)?.delete(fn as Handler);
+ }
+
+ emit(event: K, payload: EventMap[K]): void {
+ const set = this.handlers.get(event);
+ if (!set) return;
+ for (const fn of [...set]) {
+ if (set.has(fn)) (fn as Handler)(payload);
+ }
+ }
+
+ removeAll(): void {
+ this.handlers.clear();
+ }
+}
diff --git a/src/core/GameClock.ts b/src/core/GameClock.ts
new file mode 100644
index 0000000..cf9c4ec
--- /dev/null
+++ b/src/core/GameClock.ts
@@ -0,0 +1,78 @@
+import type { EventBus } from './EventBus';
+
+export interface ClockConfig {
+ nightClockMinutes: number; // in-game minutes per night (design: 360 = 9PM→3AM)
+ nightRealMinutes: number; // real minutes a night takes (design: ~13)
+ nightDate: string; // ISO date of the night — anchors ID birthday math
+}
+
+export const DEFAULT_CLOCK: ClockConfig = {
+ nightClockMinutes: 360,
+ nightRealMinutes: 13,
+ nightDate: '2026-07-18', // Saturday, night 1
+};
+
+// Engine-agnostic clock: feed it real elapsed ms (from Phaser's update or a
+// test harness), it emits clock:tick once per in-game minute.
+export class GameClock {
+ private elapsedRealMs = 0;
+ private emittedMin = -1;
+ private running = false;
+
+ constructor(
+ private readonly bus: EventBus,
+ public readonly config: ClockConfig = DEFAULT_CLOCK,
+ ) {}
+
+ get clockMin(): number {
+ const ratio = this.config.nightClockMinutes / (this.config.nightRealMinutes * 60_000);
+ return Math.min(this.config.nightClockMinutes, Math.floor(this.elapsedRealMs * ratio));
+ }
+
+ get isOver(): boolean {
+ return this.clockMin >= this.config.nightClockMinutes;
+ }
+
+ /** "9:47 PM" style label; clock starts at 21:00. */
+ get label(): string {
+ const total = 21 * 60 + this.clockMin;
+ const h24 = Math.floor(total / 60) % 24;
+ const m = total % 60;
+ const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
+ const ampm = h24 < 12 ? 'AM' : 'PM';
+ return `${h12}:${String(m).padStart(2, '0')} ${ampm}`;
+ }
+
+ get nightDate(): Date {
+ return new Date(`${this.config.nightDate}T00:00:00`);
+ }
+
+ start(): void {
+ this.running = true;
+ }
+
+ pause(): void {
+ this.running = false;
+ }
+
+ update(deltaMs: number): void {
+ if (!this.running || this.isOver) return;
+ this.elapsedRealMs += deltaMs;
+ const min = this.clockMin;
+ while (this.emittedMin < min) {
+ this.emittedMin++;
+ this.bus.emit('clock:tick', { clockMin: this.emittedMin });
+ }
+ }
+}
+
+/** Full years between an ISO DOB and a night date. The ID minigame's math. */
+export function ageOn(dobIso: string, nightDate: Date): number {
+ const dob = new Date(`${dobIso}T00:00:00`);
+ let age = nightDate.getFullYear() - dob.getFullYear();
+ const beforeBirthday =
+ nightDate.getMonth() < dob.getMonth() ||
+ (nightDate.getMonth() === dob.getMonth() && nightDate.getDate() < dob.getDate());
+ if (beforeBirthday) age--;
+ return age;
+}
diff --git a/src/core/SeededRNG.ts b/src/core/SeededRNG.ts
new file mode 100644
index 0000000..fc8140a
--- /dev/null
+++ b/src/core/SeededRNG.ts
@@ -0,0 +1,77 @@
+// Deterministic RNG. Every random draw in the game flows from a run seed via
+// named streams so systems can't perturb each other's sequences.
+
+export interface RngStream {
+ next(): number; // [0, 1)
+ int(min: number, max: number): number; // inclusive both ends
+ pick(arr: readonly T[]): T;
+ chance(p: number): boolean;
+ weighted(entries: ReadonlyArray): T;
+}
+
+function mulberry32(seed: number): () => number {
+ let a = seed >>> 0;
+ return () => {
+ a = (a + 0x6d2b79f5) >>> 0;
+ let t = a;
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+ };
+}
+
+// FNV-1a over the stream name, mixed with the run seed.
+function hashName(seed: number, name: string): number {
+ let h = 0x811c9dc5 ^ seed;
+ for (let i = 0; i < name.length; i++) {
+ h ^= name.charCodeAt(i);
+ h = Math.imul(h, 0x01000193);
+ }
+ return h >>> 0;
+}
+
+class Stream implements RngStream {
+ private readonly rand: () => number;
+ constructor(seed: number) {
+ this.rand = mulberry32(seed);
+ }
+ next(): number {
+ return this.rand();
+ }
+ int(min: number, max: number): number {
+ return min + Math.floor(this.rand() * (max - min + 1));
+ }
+ pick(arr: readonly T[]): T {
+ if (arr.length === 0) throw new Error('pick from empty array');
+ return arr[Math.floor(this.rand() * arr.length)] as T;
+ }
+ chance(p: number): boolean {
+ return this.rand() < p;
+ }
+ weighted(entries: ReadonlyArray): T {
+ let total = 0;
+ for (const [, w] of entries) total += w;
+ if (total <= 0) throw new Error('weighted with non-positive total weight');
+ let roll = this.rand() * total;
+ for (const [value, w] of entries) {
+ roll -= w;
+ if (roll < 0) return value;
+ }
+ return entries[entries.length - 1]![0];
+ }
+}
+
+export class SeededRNG {
+ private readonly streams = new Map();
+ constructor(public readonly seed: number) {}
+
+ /** Same (seed, name) always yields the same stream state history. */
+ stream(name: string): RngStream {
+ let s = this.streams.get(name);
+ if (!s) {
+ s = new Stream(hashName(this.seed, name));
+ this.streams.set(name, s);
+ }
+ return s;
+ }
+}
diff --git a/src/core/StubBeatClock.ts b/src/core/StubBeatClock.ts
new file mode 100644
index 0000000..e506cd0
--- /dev/null
+++ b/src/core/StubBeatClock.ts
@@ -0,0 +1,37 @@
+import type { EventBus } from './EventBus';
+
+// Placeholder beat authority: fixed-BPM beat:tick until LANE-JUICE's TechnoEngine
+// replaces it (same event, same payload shape — consumers must not care which).
+export class StubBeatClock {
+ private accMs = 0;
+ private beatIndex = 0;
+ private totalMs = 0;
+ private running = false;
+
+ constructor(
+ private readonly bus: EventBus,
+ public readonly bpm: number = 128,
+ ) {}
+
+ get beatIntervalMs(): number {
+ return 60_000 / this.bpm;
+ }
+
+ start(): void {
+ this.running = true;
+ }
+
+ stop(): void {
+ this.running = false;
+ }
+
+ update(deltaMs: number): void {
+ if (!this.running) return;
+ this.accMs += deltaMs;
+ this.totalMs += deltaMs;
+ while (this.accMs >= this.beatIntervalMs) {
+ this.accMs -= this.beatIntervalMs;
+ this.bus.emit('beat:tick', { beatIndex: this.beatIndex++, audioTimeMs: this.totalMs - this.accMs });
+ }
+ }
+}
diff --git a/src/core/meters.ts b/src/core/meters.ts
new file mode 100644
index 0000000..d0ae20e
--- /dev/null
+++ b/src/core/meters.ts
@@ -0,0 +1,67 @@
+import type { EventBus } from './EventBus';
+import type { HeatStrike, NightState } from '../data/types';
+
+export function freshNightState(venueId: string, nightIndex: number, licensed: number): NightState {
+ return {
+ venueId,
+ nightIndex,
+ vibe: 50,
+ aggro: 0,
+ heatStrikes: [],
+ hype: 1,
+ capacity: { inside: 0, licensed, clickerShown: 0 },
+ clockMin: 0,
+ location: 'door',
+ incidents: [],
+ };
+}
+
+const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
+
+// Single writer for the night's numbers. Scenes emit meters:delta / heat:strike;
+// nothing else may mutate NightState meters directly.
+export class Meters {
+ private readonly offs: Array<() => void> = [];
+
+ constructor(
+ private readonly bus: EventBus,
+ public readonly state: NightState,
+ ) {
+ this.offs.push(
+ bus.on('meters:delta', (d) => this.applyDelta(d)),
+ bus.on('heat:strike', (s) => this.applyStrike(s)),
+ bus.on('clock:tick', ({ clockMin }) => {
+ this.state.clockMin = clockMin;
+ }),
+ bus.on('night:phaseChange', ({ location }) => {
+ this.state.location = location;
+ }),
+ bus.on('door:clicker', ({ direction }) => {
+ this.state.capacity.clickerShown = Math.max(
+ 0,
+ this.state.capacity.clickerShown + (direction === 'in' ? 1 : -1),
+ );
+ }),
+ );
+ }
+
+ private applyDelta(d: { vibe?: number; aggro?: number; hype?: number }): void {
+ const s = this.state;
+ if (d.vibe !== undefined) {
+ // Hype multiplies the good news only — queue theatre never softens a hit.
+ const scaled = d.vibe > 0 ? d.vibe * s.hype : d.vibe;
+ s.vibe = clamp(s.vibe + scaled, 0, 100);
+ }
+ if (d.aggro !== undefined) s.aggro = clamp(s.aggro + d.aggro, 0, 100);
+ if (d.hype !== undefined) s.hype = clamp(s.hype + d.hype, 1, 3);
+ this.bus.emit('meters:changed', { vibe: s.vibe, aggro: s.aggro, hype: s.hype });
+ }
+
+ private applyStrike(strike: HeatStrike): void {
+ this.state.heatStrikes.push(strike);
+ }
+
+ destroy(): void {
+ for (const off of this.offs) off();
+ }
+}
diff --git a/src/core/save.ts b/src/core/save.ts
new file mode 100644
index 0000000..cbd71bf
--- /dev/null
+++ b/src/core/save.ts
@@ -0,0 +1,40 @@
+import type { GameState } from '../data/types';
+
+const KEY = 'not-tonight-save';
+
+export interface StorageLike {
+ getItem(key: string): string | null;
+ setItem(key: string, value: string): void;
+ removeItem(key: string): void;
+}
+
+export function freshGameState(seed: number): GameState {
+ return { v: 1, seed, venueIndex: 0, nightIndex: 0, heatStrikes: [], regulars: {}, pastReports: [] };
+}
+
+export function saveGame(state: GameState, storage: StorageLike = localStorage): void {
+ storage.setItem(KEY, JSON.stringify(state));
+}
+
+// Corrupt or version-mismatched saves fall back to a fresh run — never throw.
+export function loadGame(fallbackSeed: number, storage: StorageLike = localStorage): GameState {
+ try {
+ const raw = storage.getItem(KEY);
+ if (!raw) return freshGameState(fallbackSeed);
+ const parsed: unknown = JSON.parse(raw);
+ if (
+ typeof parsed === 'object' && parsed !== null &&
+ (parsed as GameState).v === 1 &&
+ typeof (parsed as GameState).seed === 'number'
+ ) {
+ return parsed as GameState;
+ }
+ return freshGameState(fallbackSeed);
+ } catch {
+ return freshGameState(fallbackSeed);
+ }
+}
+
+export function clearSave(storage: StorageLike = localStorage): void {
+ storage.removeItem(KEY);
+}
diff --git a/src/data/archetypes.ts b/src/data/archetypes.ts
new file mode 100644
index 0000000..0efb596
--- /dev/null
+++ b/src/data/archetypes.ts
@@ -0,0 +1,34 @@
+import type { Archetype } from './types';
+
+export interface ArchetypeDef {
+ key: Archetype;
+ /** Spawn weight in the general queue mix. 0 = only spawned by script/override. */
+ weight: number;
+ ageRange: [number, number];
+ toleranceRange: [number, number];
+ /** Starting intoxication range at 9 PM; drifts up with arrival time. */
+ startDrunkRange: [number, number];
+ chances: {
+ fakeId?: number;
+ contraband?: number;
+ claimsGuestList?: number;
+ onGuestList?: number;
+ knowsOwner?: number;
+ };
+}
+
+export const ARCHETYPES: readonly ArchetypeDef[] = [
+ { key: 'punter', weight: 50, ageRange: [18, 45], toleranceRange: [0.3, 0.8], startDrunkRange: [0, 0.3], chances: { fakeId: 0.02, contraband: 0.08, claimsGuestList: 0.05 } },
+ { key: 'fresh18', weight: 8, ageRange: [18, 18], toleranceRange: [0.1, 0.35], startDrunkRange: [0, 0.15], chances: {} },
+ { key: 'almost18', weight: 7, ageRange: [17, 17], toleranceRange: [0.1, 0.4], startDrunkRange: [0, 0.25], chances: { fakeId: 1 } },
+ { key: 'financeBro', weight: 10, ageRange: [23, 34], toleranceRange: [0.4, 0.75], startDrunkRange: [0.15, 0.55], chances: { contraband: 0.25, claimsGuestList: 0.15, knowsOwner: 0.02 } },
+ { key: 'influencer', weight: 7, ageRange: [19, 29], toleranceRange: [0.3, 0.6], startDrunkRange: [0, 0.25], chances: { claimsGuestList: 0.75, onGuestList: 0.2, contraband: 0.1 } },
+ { key: 'regular', weight: 8, ageRange: [25, 55], toleranceRange: [0.6, 0.95], startDrunkRange: [0.1, 0.4], chances: { onGuestList: 0.1 } },
+ { key: 'ownersMate', weight: 3, ageRange: [30, 60], toleranceRange: [0.5, 0.9], startDrunkRange: [0.1, 0.5], chances: { claimsGuestList: 0.9, knowsOwner: 0.6 } },
+ { key: 'quietMessy', weight: 6, ageRange: [20, 38], toleranceRange: [0.1, 0.3], startDrunkRange: [0.25, 0.5], chances: { contraband: 0.15 } },
+ { key: 'inspector', weight: 1, ageRange: [38, 55], toleranceRange: [0.9, 1], startDrunkRange: [0, 0], chances: {} },
+];
+
+export const ARCHETYPE_BY_KEY: ReadonlyMap = new Map(
+ ARCHETYPES.map((a) => [a.key, a]),
+);
diff --git a/src/data/contraband.ts b/src/data/contraband.ts
new file mode 100644
index 0000000..0c22f16
--- /dev/null
+++ b/src/data/contraband.ts
@@ -0,0 +1,21 @@
+export interface ContrabandDef {
+ id: string;
+ name: string;
+ /** 1 = confiscate & wave through · 2 = confiscate, warning · 3 = ejection-worthy */
+ severity: 1 | 2 | 3;
+ weight: number;
+ spriteHint: string; // placeholder-art colour/shape note until real sprites
+}
+
+export const CONTRABAND: readonly ContrabandDef[] = [
+ { id: 'baggie', name: 'Suspicious baggie', severity: 3, weight: 3, spriteHint: 'tiny white rect' },
+ { id: 'flask', name: 'Hip flask', severity: 2, weight: 5, spriteHint: 'silver rounded rect' },
+ { id: 'goonSack', name: 'Goon sack', severity: 2, weight: 3, spriteHint: 'silver pillow' },
+ { id: 'tinnies', name: 'Two warm tinnies', severity: 1, weight: 4, spriteHint: 'gold cans' },
+ { id: 'kebab', name: 'A whole kebab', severity: 1, weight: 3, spriteHint: 'foil cylinder' },
+ { id: 'someoneElsesId', name: "Someone else's ID", severity: 2, weight: 2, spriteHint: 'small card' },
+ { id: 'budgie', name: 'A live budgie', severity: 1, weight: 1, spriteHint: 'green blob (flaps)' },
+ { id: 'vapes', name: 'Fourteen vapes', severity: 1, weight: 3, spriteHint: 'neon sticks' },
+ { id: 'marker', name: 'Fat permanent marker', severity: 2, weight: 2, spriteHint: 'black stick' },
+ { id: 'snags', name: 'Raw sausages (why)', severity: 1, weight: 1, spriteHint: 'pink links' },
+];
diff --git a/src/data/outfits.ts b/src/data/outfits.ts
new file mode 100644
index 0000000..7b78ef1
--- /dev/null
+++ b/src/data/outfits.ts
@@ -0,0 +1,84 @@
+import type { OutfitSlot } from './types';
+
+// Outfit vocabulary. Types/colours the generator draws from and dress-code rules
+// predicate on. Slot taxonomy cross-checked against the 90sDJsim wardrobe catalog
+// (docs/ASSETS.md §1) so a future art pass maps 1:1.
+
+export interface OutfitTypeDef {
+ type: string;
+ weight: number;
+ colours: readonly string[];
+ canLogo?: boolean;
+ canVintage?: boolean;
+}
+
+export const PALETTE: Record = {
+ white: 0xf2f2f2,
+ black: 0x14141c,
+ grey: 0x8a8a94,
+ navy: 0x1e2a52,
+ red: 0xc03434,
+ green: 0x2e7d46,
+ blue: 0x3465c0,
+ pink: 0xe06fa4,
+ purple: 0x7a4fc0,
+ yellow: 0xd8c23a,
+ orange: 0xd07c2e,
+ brown: 0x6e4a2e,
+ cream: 0xe8ddc0,
+ denim: 0x4a6a95,
+};
+
+export const OUTFIT_VOCAB: Record = {
+ shoes: [
+ { type: 'sneaker', weight: 5, colours: ['white', 'black', 'red', 'grey'], canLogo: true, canVintage: true },
+ { type: 'boot', weight: 2, colours: ['black', 'brown'] },
+ { type: 'dress', weight: 2, colours: ['black', 'brown'] },
+ { type: 'heel', weight: 2, colours: ['black', 'red', 'pink', 'white'] },
+ { type: 'thong', weight: 1, colours: ['black', 'blue', 'yellow'] }, // the footwear, obviously
+ { type: 'loafer', weight: 1, colours: ['brown', 'black', 'cream'] },
+ { type: 'platform', weight: 1, colours: ['black', 'purple', 'pink'], canVintage: true },
+ ],
+ legs: [
+ { type: 'jeans', weight: 5, colours: ['denim', 'black', 'grey'] },
+ { type: 'chinos', weight: 3, colours: ['cream', 'navy', 'brown'] },
+ { type: 'skirt', weight: 3, colours: ['black', 'red', 'denim', 'pink'] },
+ { type: 'trackies', weight: 2, colours: ['grey', 'black', 'navy'], canLogo: true },
+ { type: 'shorts', weight: 2, colours: ['denim', 'cream', 'black'] },
+ { type: 'slacks', weight: 2, colours: ['black', 'grey', 'navy'] },
+ { type: 'cargo', weight: 1, colours: ['green', 'grey', 'brown'] },
+ ],
+ top: [
+ { type: 'tee', weight: 5, colours: ['white', 'black', 'grey', 'red', 'green', 'navy'], canLogo: true, canVintage: true },
+ { type: 'shirt', weight: 4, colours: ['white', 'blue', 'pink', 'cream'] },
+ { type: 'singlet', weight: 2, colours: ['white', 'black', 'grey'], canLogo: true },
+ { type: 'polo', weight: 2, colours: ['navy', 'white', 'green', 'red'], canLogo: true },
+ { type: 'crop', weight: 2, colours: ['white', 'black', 'pink', 'purple'] },
+ { type: 'dressShirt', weight: 2, colours: ['white', 'black'] },
+ { type: 'jersey', weight: 1, colours: ['red', 'blue', 'yellow'], canLogo: true, canVintage: true },
+ ],
+ outer: [
+ { type: 'none', weight: 6, colours: ['black'] },
+ { type: 'blazer', weight: 2, colours: ['black', 'navy', 'grey'] },
+ { type: 'bomber', weight: 2, colours: ['black', 'green', 'navy'], canLogo: true, canVintage: true },
+ { type: 'hoodie', weight: 2, colours: ['black', 'grey', 'red'], canLogo: true },
+ { type: 'leather', weight: 1, colours: ['black', 'brown'], canVintage: true },
+ { type: 'puffer', weight: 1, colours: ['black', 'orange', 'purple'], canLogo: true },
+ ],
+ hair: [
+ { type: 'short', weight: 5, colours: ['black', 'brown', 'yellow', 'grey'] },
+ { type: 'long', weight: 3, colours: ['black', 'brown', 'yellow', 'red'] },
+ { type: 'bun', weight: 2, colours: ['black', 'brown', 'yellow'] },
+ { type: 'mullet', weight: 2, colours: ['brown', 'yellow', 'black'], canVintage: true },
+ { type: 'shaved', weight: 2, colours: ['black', 'brown'] },
+ { type: 'dyed', weight: 1, colours: ['pink', 'green', 'purple', 'blue'] },
+ ],
+ accessory: [
+ { type: 'none', weight: 6, colours: ['black'] },
+ { type: 'bucketHat', weight: 2, colours: ['cream', 'black', 'green'], canLogo: true, canVintage: true },
+ { type: 'cap', weight: 2, colours: ['black', 'red', 'navy'], canLogo: true },
+ { type: 'sunnies', weight: 2, colours: ['black'] }, // sunglasses. at night. a tell in itself
+ { type: 'bumbag', weight: 1, colours: ['black', 'purple', 'yellow'], canLogo: true, canVintage: true },
+ { type: 'chain', weight: 1, colours: ['yellow', 'grey'] },
+ ],
+};
diff --git a/src/data/strings/dazza.ts b/src/data/strings/dazza.ts
new file mode 100644
index 0000000..ca86ce7
--- /dev/null
+++ b/src/data/strings/dazza.ts
@@ -0,0 +1,36 @@
+// Dazza (management) text messages. Voice: aggressive lowercase, no full stops,
+// occasionally terrified of the owner. Rule-bearing texts pair with a
+// DressCodeRule id in rules/dressCode.ts.
+
+export interface DazzaLine {
+ id: string;
+ text: string;
+ /** dress-code rule id this text announces, if any */
+ ruleId?: string;
+ /** earliest clock-min this can fire */
+ fromMin: number;
+}
+
+export const DAZZA_TEXTS: readonly DazzaLine[] = [
+ // rule-bearing escalation (design doc §3.1)
+ { id: 'rule-basics', text: 'mate its 930 no thongs no singlets we are not a beer garden', ruleId: 'noThongsSinglets', fromMin: 30 },
+ { id: 'rule-logos', text: 'seeing too many logos in there. no visible logos from NOW', ruleId: 'noLogos', fromMin: 90 },
+ { id: 'rule-sneakers', text: 'white sneakers are OVER. unless vintage. u know the difference', ruleId: 'noWhiteSneakers', fromMin: 150 },
+ { id: 'rule-songRequest', text: 'if they look like theyd request a song they dont come in. u know the look', ruleId: 'noSongRequesters', fromMin: 240 },
+ { id: 'rule-bucketHats', text: 'bucket hats r making the room feel like a festival toilet queue. gone', ruleId: 'noBucketHats', fromMin: 180 },
+ { id: 'rule-sunnies', text: 'anyone wearing sunnies inside at 11pm is a cop or a flog. neither gets in', ruleId: 'noSunnies', fromMin: 120 },
+ { id: 'rule-suits', text: 'too corporate in here tonight. no blazers til the suits ive already got leave', ruleId: 'noBlazers', fromMin: 200 },
+ { id: 'rule-couples', text: 'no more couples holding hands in the queue its making the room feel married', ruleId: 'noHandHolders', fromMin: 280 },
+
+ // vibe commentary
+ { id: 'vibe-mid', text: 'why is the floor MID rn. fix it', fromMin: 60 },
+ { id: 'vibe-good', text: 'room is going OFF right now whatever ur doing keep doing it', fromMin: 100 },
+ { id: 'vibe-queue', text: 'queue looks massive from my office window. love that. keep them out there', fromMin: 80 },
+ { id: 'vibe-empty', text: 'i can see the back wall from the bar. the BACK WALL. let some people in', fromMin: 45 },
+
+ // 2am philosophy / the owner
+ { id: 'owner-scare', text: 'if a bloke called terry shows up he gets in. do NOT stamp terry. please', fromMin: 150 },
+ { id: 'philosophy-1', text: 'a nightclub is just a room where everyone agreed to pretend. dont let them stop pretending', fromMin: 300 },
+ { id: 'philosophy-2', text: 'do u ever think about how the kebab shop outlives us all', fromMin: 320 },
+ { id: 'kayden', text: 'kayden let in a bloke wearing CROCS while u were inside. deal w kayden', fromMin: 120 },
+];
diff --git a/src/data/types.ts b/src/data/types.ts
new file mode 100644
index 0000000..4626ef0
--- /dev/null
+++ b/src/data/types.ts
@@ -0,0 +1,140 @@
+// NOT TONIGHT — shared contract types. Source of truth: docs/CONTRACTS.md.
+// Post-Phase-0 changes require a CONTRACT CHANGE REQUEST in LANEHANDOVER.md.
+
+export type Archetype =
+ | 'fresh18' | 'almost18' | 'financeBro' | 'influencer'
+ | 'regular' | 'ownersMate' | 'quietMessy' | 'inspector' | 'punter';
+
+export type DrunkStage = 'sober' | 'tipsy' | 'loose' | 'messy' | 'maggot';
+
+export type OutfitSlot = 'shoes' | 'legs' | 'top' | 'outer' | 'hair' | 'accessory';
+
+export interface OutfitLayer {
+ slot: OutfitSlot;
+ type: string;
+ colour: string;
+ logo?: boolean;
+ vintage?: boolean;
+}
+
+export interface IdFakeTells {
+ peelingLaminate?: boolean;
+ wrongHologram?: boolean;
+ jokeName?: boolean;
+ photoMismatch?: boolean;
+}
+
+export interface IdCard {
+ name: string;
+ dob: string; // ISO date
+ expiry: string; // ISO date
+ photoSeed: number;
+ fake?: IdFakeTells;
+}
+
+export interface PatronFlags {
+ contraband?: string[];
+ onGuestList?: boolean;
+ claimsGuestList?: boolean;
+ knowsOwner?: boolean;
+ uvStamped?: boolean;
+ timesDeniedBefore?: number;
+ disguised?: boolean;
+}
+
+export interface Patron {
+ id: string;
+ dollSeed: number;
+ archetype: Archetype;
+ age: number;
+ idCard: IdCard;
+ outfit: OutfitLayer[];
+ intoxication: number; // 0..1
+ tolerance: number; // 0..1
+ flags: PatronFlags;
+}
+
+// ---- rules ----
+
+export interface DressCodeRule {
+ id: string;
+ text: string;
+ activeFrom: number; // clock minutes since 21:00
+ violates: (p: Patron) => boolean;
+}
+
+export type Verdict = 'admit' | 'deny' | 'sobrietyTest' | 'patDown' | 'wait';
+
+export interface HeatStrike {
+ reason: string;
+ deferred?: boolean;
+}
+
+export interface VerdictOutcome {
+ vibeDelta: number;
+ aggroDelta: number;
+ heatStrike?: HeatStrike;
+ dazzaText?: string;
+}
+
+// ---- night / run state ----
+
+export interface IncidentRecord {
+ clockMin: number;
+ kind: string;
+ patronId?: string;
+ detail: string;
+}
+
+export interface NightState {
+ venueId: string;
+ nightIndex: number;
+ vibe: number; // 0..100
+ aggro: number; // 0..100
+ heatStrikes: HeatStrike[];
+ hype: number;
+ capacity: { inside: number; licensed: number; clickerShown: number };
+ clockMin: number; // minutes since 21:00; night ends at 360
+ location: 'door' | 'floor';
+ incidents: IncidentRecord[];
+}
+
+export interface RegularMemory {
+ patronId: string;
+ timesDenied: number;
+ lastSeenNight: number;
+ grudge: number; // 0..1
+}
+
+export interface GameState {
+ v: 1;
+ seed: number;
+ venueIndex: number;
+ nightIndex: number;
+ heatStrikes: HeatStrike[];
+ regulars: Record;
+ pastReports: IncidentRecord[][];
+}
+
+// ---- event bus ----
+
+export interface EventMap {
+ 'clock:tick': { clockMin: number };
+ 'night:phaseChange': { location: 'door' | 'floor' };
+ 'radio:call': { urgency: 1 | 2 | 3; line: string };
+
+ 'door:patronUp': { patron: Patron };
+ 'door:verdict': { patron: Patron; verdict: Verdict; outcome: VerdictOutcome };
+ 'door:clicker': { direction: 'in' | 'out' };
+ 'dazza:text': { text: string; rule?: DressCodeRule };
+
+ 'floor:infractionSpotted': { patronId: string; kind: 'drunk' | 'stall' | 'contraband' | 'noStamp' | 'fight' };
+ 'floor:ejection': { patronId: string; style: 'clean' | 'messy' };
+
+ 'beat:tick': { beatIndex: number; audioTimeMs: number };
+ 'audio:location': { location: 'door' | 'floor' };
+
+ 'meters:delta': { vibe?: number; aggro?: number; hype?: number };
+ 'meters:changed': { vibe: number; aggro: number; hype: number };
+ 'heat:strike': HeatStrike;
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..38c90af
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,20 @@
+import Phaser from 'phaser';
+import { BootScene } from './scenes/shared/BootScene';
+import { ParadeScene } from './scenes/shared/ParadeScene';
+
+const game = new Phaser.Game({
+ type: Phaser.AUTO,
+ width: 640,
+ height: 360,
+ pixelArt: true,
+ backgroundColor: '#0a0a12',
+ scale: {
+ mode: Phaser.Scale.FIT,
+ autoCenter: Phaser.Scale.CENTER_BOTH,
+ zoom: Phaser.Scale.MAX_ZOOM,
+ },
+ scene: [BootScene, ParadeScene],
+});
+
+// debug handle (harmless in prod; used by dev tooling)
+(window as unknown as { game: Phaser.Game }).game = game;
diff --git a/src/patrons/doll.ts b/src/patrons/doll.ts
new file mode 100644
index 0000000..1b98d2e
--- /dev/null
+++ b/src/patrons/doll.ts
@@ -0,0 +1,29 @@
+import Phaser from 'phaser';
+import { dollPlan, type DollPose } from './dollPlan';
+import type { Patron } from '../data/types';
+
+// Phaser half of the paper-doll renderer: paints a DollPlan into a cached
+// texture. Contract (docs/CONTRACTS.md §5): callers get a texture key and never
+// care whether the pixels came from procedural rects or real art.
+
+export function dollTextureKey(p: Patron, pose: DollPose): string {
+ // Drunk tells are baked into the texture, so intoxication stage is part of
+ // the cache key (coarse: swayPx bucket, not raw float).
+ return `doll:${p.dollSeed}:${pose}:${dollPlan(p, pose).swayPx}`;
+}
+
+export function renderDoll(scene: Phaser.Scene, p: Patron, pose: DollPose): string {
+ const key = dollTextureKey(p, pose);
+ if (scene.textures.exists(key)) return key;
+
+ const plan = dollPlan(p, pose);
+ const canvas = scene.textures.createCanvas(key, plan.width, plan.height);
+ if (!canvas) return key; // key collision race — texture already exists
+ const ctx = canvas.getContext();
+ for (const r of plan.rects) {
+ ctx.fillStyle = `#${r.colour.toString(16).padStart(6, '0')}`;
+ ctx.fillRect(r.x, r.y, r.w, r.h);
+ }
+ canvas.refresh();
+ return key;
+}
diff --git a/src/patrons/dollPlan.ts b/src/patrons/dollPlan.ts
new file mode 100644
index 0000000..ffb7f29
--- /dev/null
+++ b/src/patrons/dollPlan.ts
@@ -0,0 +1,126 @@
+// Pure half of the paper-doll renderer: Patron -> list of coloured rects.
+// No Phaser imports — fully unit-testable, deterministic per (patron, pose).
+// doll.ts paints these plans into textures; real pixel art later replaces the
+// painter while THIS data contract (and renderDoll's signature) stays put.
+
+import { PALETTE } from '../data/outfits';
+import { drunkStage } from '../rules/drunk';
+import type { Patron } from '../data/types';
+
+export type DollPose = 'queue' | 'idPhoto' | 'floorTopDown';
+
+export interface PlanRect {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ colour: number;
+}
+
+export interface DollPlan {
+ width: number;
+ height: number;
+ rects: PlanRect[];
+ /** render-time tell: sway amplitude in px (0 = steady) */
+ swayPx: number;
+}
+
+export const DOLL_SIZES: Record = {
+ queue: { width: 32, height: 48 },
+ idPhoto: { width: 24, height: 24 },
+ floorTopDown: { width: 16, height: 16 },
+};
+
+const SKIN_TONES = [0xf1c9a5, 0xd9a066, 0xa5673f, 0x6b4423, 0x8d5524, 0xe8b088] as const;
+
+// A tiny deterministic hash so body build/skin derive from dollSeed without
+// consuming RNG stream state (plans must be reproducible in isolation).
+function mix(seed: number, salt: number): number {
+ let h = (seed ^ (salt * 0x9e3779b9)) >>> 0;
+ h = Math.imul(h ^ (h >>> 16), 0x45d9f3b) >>> 0;
+ h = Math.imul(h ^ (h >>> 13), 0x45d9f3b) >>> 0;
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
+}
+
+const colourOf = (name: string): number => PALETTE[name] ?? 0xff00ff;
+
+function layerColour(p: Patron, slot: string): number | undefined {
+ const layer = p.outfit.find((l) => l.slot === slot);
+ if (!layer || layer.type === 'none') return undefined;
+ return colourOf(layer.colour);
+}
+
+export function dollPlan(p: Patron, pose: DollPose): DollPlan {
+ const { width, height } = DOLL_SIZES[pose];
+ const skin = SKIN_TONES[Math.floor(mix(p.dollSeed, 1) * SKIN_TONES.length)]!;
+ const build = 0.8 + mix(p.dollSeed, 2) * 0.4; // 0.8..1.2 body width factor
+ const stage = drunkStage(p.intoxication);
+ const swayPx = stage === 'loose' ? 1 : stage === 'messy' ? 2 : stage === 'maggot' ? 3 : 0;
+ const rects: PlanRect[] = [];
+
+ const seedForPhoto = pose === 'idPhoto' ? p.idCard.photoSeed : p.dollSeed;
+ const photoSkin = SKIN_TONES[Math.floor(mix(seedForPhoto, 1) * SKIN_TONES.length)]!;
+ const hair = layerColour(p, 'hair') ?? 0x14141c;
+ const photoHair = pose === 'idPhoto' && p.idCard.photoSeed !== p.dollSeed
+ ? (Object.values(PALETTE)[Math.floor(mix(seedForPhoto, 3) * 10)] ?? hair)
+ : hair;
+
+ if (pose === 'idPhoto') {
+ // Head-and-shoulders on a flat card background.
+ rects.push({ x: 0, y: 0, w: width, h: height, colour: 0xdad4c4 });
+ rects.push({ x: 7, y: 6, w: 10, h: 10, colour: photoSkin }); // face
+ rects.push({ x: 6, y: 3, w: 12, h: 4, colour: photoHair }); // hair
+ rects.push({ x: 5, y: 16, w: 14, h: 8, colour: layerColour(p, 'top') ?? 0x555555 }); // shoulders
+ return { width, height, rects, swayPx: 0 };
+ }
+
+ if (pose === 'floorTopDown') {
+ // Seen from above: hair blob over shoulders (top colour).
+ const top = layerColour(p, 'outer') ?? layerColour(p, 'top') ?? 0x555555;
+ rects.push({ x: 3, y: 3, w: 10, h: 10, colour: top }); // shoulders
+ rects.push({ x: 5, y: 5, w: 6, h: 6, colour: hair }); // head/hair
+ return { width, height, rects, swayPx };
+ }
+
+ // queue pose — full front-facing figure, 32x48.
+ const bw = Math.round(12 * build); // body width
+ const cx = Math.floor(width / 2);
+
+ const shoes = layerColour(p, 'shoes') ?? 0x14141c;
+ const legs = layerColour(p, 'legs') ?? 0x333344;
+ const top = layerColour(p, 'top') ?? 0x555566;
+ const outer = layerColour(p, 'outer');
+ const acc = p.outfit.find((l) => l.slot === 'accessory');
+
+ rects.push({ x: cx - 4, y: 44, w: 3, h: 4, colour: shoes }); // L shoe
+ rects.push({ x: cx + 1, y: 44, w: 3, h: 4, colour: shoes }); // R shoe
+ rects.push({ x: cx - 4, y: 30, w: 8, h: 14, colour: legs }); // legs
+ rects.push({ x: cx - Math.floor(bw / 2), y: 16, w: bw, h: 14, colour: top }); // torso
+ if (outer !== undefined) {
+ rects.push({ x: cx - Math.floor(bw / 2) - 1, y: 16, w: 3, h: 13, colour: outer }); // jacket L
+ rects.push({ x: cx + Math.floor(bw / 2) - 2, y: 16, w: 3, h: 13, colour: outer }); // jacket R
+ }
+ const topLayer = p.outfit.find((l) => l.slot === 'top');
+ if (topLayer?.logo) rects.push({ x: cx - 2, y: 20, w: 4, h: 3, colour: 0xffffff }); // visible logo
+ rects.push({ x: cx - 3, y: 6, w: 6, h: 9, colour: skin }); // face
+ rects.push({ x: cx - 4, y: 3, w: 8, h: 4, colour: hair }); // hair
+ const hairLayer = p.outfit.find((l) => l.slot === 'hair');
+ if (hairLayer?.type === 'long') rects.push({ x: cx - 5, y: 6, w: 2, h: 8, colour: hair });
+ if (hairLayer?.type === 'mullet') rects.push({ x: cx - 4, y: 12, w: 8, h: 3, colour: hair });
+
+ if (acc && acc.type !== 'none') {
+ const accColour = colourOf(acc.colour);
+ if (acc.type === 'bucketHat' || acc.type === 'cap') rects.push({ x: cx - 5, y: 2, w: 10, h: 3, colour: accColour });
+ if (acc.type === 'sunnies') rects.push({ x: cx - 3, y: 8, w: 6, h: 2, colour: 0x000000 });
+ if (acc.type === 'bumbag') rects.push({ x: cx - 5, y: 28, w: 10, h: 3, colour: accColour });
+ if (acc.type === 'chain') rects.push({ x: cx - 2, y: 16, w: 4, h: 1, colour: 0xf0d060 });
+ }
+
+ // drunk tells: bloodshot eye pixels from 'loose' up
+ if (swayPx > 0) {
+ rects.push({ x: cx - 2, y: 9, w: 1, h: 1, colour: 0xcc3333 });
+ rects.push({ x: cx + 1, y: 9, w: 1, h: 1, colour: 0xcc3333 });
+ }
+
+ return { width, height, rects, swayPx };
+}
diff --git a/src/patrons/generator.ts b/src/patrons/generator.ts
new file mode 100644
index 0000000..befb2a7
--- /dev/null
+++ b/src/patrons/generator.ts
@@ -0,0 +1,130 @@
+import type { RngStream, SeededRNG } from '../core/SeededRNG';
+import { ageOn } from '../core/GameClock';
+import { ARCHETYPES, ARCHETYPE_BY_KEY } from '../data/archetypes';
+import { CONTRABAND } from '../data/contraband';
+import { OUTFIT_VOCAB } from '../data/outfits';
+import type { Archetype, IdCard, OutfitLayer, OutfitSlot, Patron } from '../data/types';
+
+const FIRST = ['Shazza', 'Dazza', 'Bazza', 'Kylie', 'Jai', 'Tahlia', 'Lachlan', 'Brooke', 'Cooper', 'Mia', 'Ngaio', 'Con', 'Duc', 'Sofia', 'Marco', 'Aroha', 'Blake', 'Chantelle', 'Rhys', 'Imogen'] as const;
+const LAST = ['Nguyen', 'Smith', 'Papadopoulos', 'Chen', 'OBrien', 'Kowalski', 'Singh', 'Taufa', 'Romano', 'Petersen', 'Doyle', 'Vella', 'Karim', 'Marsh', 'Duffy'] as const;
+const JOKE_NAMES = ['M. Lovin', 'B. Drinkwater', 'Rusty Shackleford', 'A. Nonymous', 'J. Kebab'] as const;
+
+let serial = 0;
+/** Test hook — resets the patron id counter so runs are comparable. */
+export function _resetPatronSerial(): void {
+ serial = 0;
+}
+
+function isoDaysFrom(base: Date, days: number): string {
+ const d = new Date(base.getTime());
+ d.setDate(d.getDate() + days);
+ const y = d.getFullYear();
+ const m = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+ return `${y}-${m}-${day}`;
+}
+
+/** DOB that makes the patron `years` old, offset by `extraDays`, on nightDate. */
+function dobForAge(nightDate: Date, years: number, extraDays: number): string {
+ const d = new Date(nightDate.getTime());
+ d.setFullYear(d.getFullYear() - years);
+ return isoDaysFrom(d, -extraDays);
+}
+
+function rollOutfit(rng: RngStream): OutfitLayer[] {
+ const layers: OutfitLayer[] = [];
+ for (const slot of Object.keys(OUTFIT_VOCAB) as OutfitSlot[]) {
+ const def = rng.weighted(OUTFIT_VOCAB[slot].map((d) => [d, d.weight] as const));
+ const layer: OutfitLayer = { slot, type: def.type, colour: rng.pick(def.colours) };
+ if (def.canLogo && rng.chance(0.35)) layer.logo = true;
+ if (def.canVintage && rng.chance(0.2)) layer.vintage = true;
+ layers.push(layer);
+ }
+ return layers;
+}
+
+function rollId(rng: RngStream, nightDate: Date, trueAge: number, dollSeed: number, wantFake: boolean): IdCard {
+ const name = `${rng.pick(FIRST)} ${rng.pick(LAST)}`;
+ if (!wantFake) {
+ // Real ID. DOBs cluster near the 18 boundary so the birthday math stays live.
+ const extraDays = trueAge === 18 ? rng.int(0, 90) : rng.int(0, 364);
+ return {
+ name,
+ dob: dobForAge(nightDate, trueAge, extraDays),
+ expiry: isoDaysFrom(nightDate, rng.int(30, 365 * 4)),
+ photoSeed: dollSeed,
+ };
+ }
+ // Fake: claims 18-19ish, carries 1-2 visible tells. Occasionally the "fake" is
+ // just an expired real card — expiry IS the tell then.
+ const claimedAge = 18 + rng.int(0, 1);
+ const tellPool = ['peelingLaminate', 'wrongHologram', 'jokeName', 'photoMismatch', 'expired'] as const;
+ const first = rng.pick(tellPool);
+ const second = rng.chance(0.4) ? rng.pick(tellPool) : undefined;
+ const tells = new Set([first, second].filter((t): t is (typeof tellPool)[number] => t !== undefined));
+
+ const card: IdCard = {
+ name: tells.has('jokeName') ? rng.pick(JOKE_NAMES) : name,
+ dob: dobForAge(nightDate, claimedAge, rng.int(10, 200)),
+ expiry: tells.has('expired') ? isoDaysFrom(nightDate, -rng.int(5, 400)) : isoDaysFrom(nightDate, rng.int(30, 365 * 3)),
+ photoSeed: tells.has('photoMismatch') ? dollSeed + 7777 : dollSeed,
+ fake: {},
+ };
+ if (tells.has('peelingLaminate')) card.fake!.peelingLaminate = true;
+ if (tells.has('wrongHologram')) card.fake!.wrongHologram = true;
+ if (tells.has('jokeName')) card.fake!.jokeName = true;
+ if (tells.has('photoMismatch')) card.fake!.photoMismatch = true;
+ return card;
+}
+
+export interface GeneratorContext {
+ rng: SeededRNG;
+ nightDate: Date;
+}
+
+export function generatePatron(ctx: GeneratorContext, clockMin: number, archetypeOverride?: Archetype): Patron {
+ const rng = ctx.rng.stream('patrons');
+ const def = archetypeOverride
+ ? ARCHETYPE_BY_KEY.get(archetypeOverride)!
+ : rng.weighted(ARCHETYPES.map((a) => [a, a.weight] as const));
+
+ const dollSeed = rng.int(0, 2 ** 31 - 1);
+ const age = rng.int(def.ageRange[0], def.ageRange[1]);
+ const wantFake = rng.chance(def.chances.fakeId ?? 0);
+ const idCard = rollId(rng, ctx.nightDate, age, dollSeed, wantFake);
+
+ // Crowd arrives progressively drunker as the night wears on.
+ const lateDrift = Math.min(0.35, (clockMin / 360) * 0.45);
+ const [lo, hi] = def.startDrunkRange;
+ const intoxication = Math.min(1, lo + rng.next() * (hi - lo) + lateDrift * rng.next());
+
+ const flags: Patron['flags'] = {};
+ if (rng.chance(def.chances.contraband ?? 0)) {
+ const item = rng.weighted(CONTRABAND.map((c) => [c, c.weight] as const));
+ flags.contraband = [item.id];
+ if (rng.chance(0.2)) {
+ const extra = rng.weighted(CONTRABAND.map((c) => [c, c.weight] as const));
+ if (extra.id !== item.id) flags.contraband.push(extra.id);
+ }
+ }
+ if (rng.chance(def.chances.onGuestList ?? 0)) flags.onGuestList = true;
+ if (flags.onGuestList || rng.chance(def.chances.claimsGuestList ?? 0)) flags.claimsGuestList = true;
+ if (rng.chance(def.chances.knowsOwner ?? 0)) flags.knowsOwner = true;
+
+ return {
+ id: `p${serial++}`,
+ dollSeed,
+ archetype: def.key,
+ age,
+ idCard,
+ outfit: rollOutfit(rng),
+ intoxication,
+ tolerance: def.toleranceRange[0] + rng.next() * (def.toleranceRange[1] - def.toleranceRange[0]),
+ flags,
+ };
+}
+
+/** True iff the ID itself (not the face in front of you) reads as valid tonight. */
+export function idAge(patron: Patron, nightDate: Date): number {
+ return ageOn(patron.idCard.dob, nightDate);
+}
diff --git a/src/patrons/memory.ts b/src/patrons/memory.ts
new file mode 100644
index 0000000..55ef82e
--- /dev/null
+++ b/src/patrons/memory.ts
@@ -0,0 +1,30 @@
+import type { GameState, Patron, RegularMemory } from '../data/types';
+
+// Regulars' grudge store. Behaviour wiring (disguises, dialogue callbacks) lands
+// in Phase 2/3 — this is just the persistent bookkeeping, run-scoped in GameState.
+
+export function recordDenial(state: GameState, patron: Patron, nightIndex: number): RegularMemory {
+ const existing = state.regulars[patron.id];
+ const mem: RegularMemory = existing ?? {
+ patronId: patron.id,
+ timesDenied: 0,
+ lastSeenNight: nightIndex,
+ grudge: 0,
+ };
+ mem.timesDenied += 1;
+ mem.lastSeenNight = nightIndex;
+ mem.grudge = Math.min(1, mem.grudge + 0.34); // three denials = maximum grudge
+ state.regulars[patron.id] = mem;
+ return mem;
+}
+
+export function recordSeen(state: GameState, patronId: string, nightIndex: number): void {
+ const mem = state.regulars[patronId];
+ if (mem) mem.lastSeenNight = nightIndex;
+}
+
+/** A regular with maxed grudge comes back disguised (bad fake moustache era). */
+export function shouldReturnDisguised(state: GameState, patronId: string): boolean {
+ const mem = state.regulars[patronId];
+ return !!mem && mem.timesDenied >= 2;
+}
diff --git a/src/rules/drunk.ts b/src/rules/drunk.ts
new file mode 100644
index 0000000..d644d81
--- /dev/null
+++ b/src/rules/drunk.ts
@@ -0,0 +1,17 @@
+import type { DrunkStage } from '../data/types';
+
+// Single authority for intoxication thresholds. Renderer reads stages for tells;
+// scenes read them for gameplay; nobody hardcodes numbers.
+export function drunkStage(intoxication: number): DrunkStage {
+ if (intoxication < 0.2) return 'sober';
+ if (intoxication < 0.45) return 'tipsy';
+ if (intoxication < 0.65) return 'loose';
+ if (intoxication < 0.85) return 'messy';
+ return 'maggot';
+}
+
+/** Visible-at-the-rope tells begin at 'loose'. */
+export function hasVisibleTells(intoxication: number): boolean {
+ const s = drunkStage(intoxication);
+ return s === 'loose' || s === 'messy' || s === 'maggot';
+}
diff --git a/src/scenes/shared/BootScene.ts b/src/scenes/shared/BootScene.ts
new file mode 100644
index 0000000..b5e506e
--- /dev/null
+++ b/src/scenes/shared/BootScene.ts
@@ -0,0 +1,13 @@
+import Phaser from 'phaser';
+
+// Asset-less boot: everything v0.x is procedural, so boot just forwards to the
+// parade demo. Phase 2 integration will route to a menu/night flow here.
+export class BootScene extends Phaser.Scene {
+ constructor() {
+ super('Boot');
+ }
+
+ create(): void {
+ this.scene.start('Parade');
+ }
+}
diff --git a/src/scenes/shared/HudStub.ts b/src/scenes/shared/HudStub.ts
new file mode 100644
index 0000000..02cbefa
--- /dev/null
+++ b/src/scenes/shared/HudStub.ts
@@ -0,0 +1,23 @@
+import Phaser from 'phaser';
+import type { EventBus } from '../../core/EventBus';
+
+// Text-only meter readout. LANE-JUICE's MeterHud replaces this at integration.
+export class HudStub {
+ private readonly text: Phaser.GameObjects.Text;
+ private readonly off: () => void;
+
+ constructor(scene: Phaser.Scene, bus: EventBus) {
+ this.text = scene.add
+ .text(4, 4, 'vibe 50 aggro 0 hype 1.0', { fontFamily: 'monospace', fontSize: '8px', color: '#9fe8a0' })
+ .setDepth(1000)
+ .setScrollFactor(0);
+ this.off = bus.on('meters:changed', ({ vibe, aggro, hype }) => {
+ this.text.setText(`vibe ${Math.round(vibe)} aggro ${Math.round(aggro)} hype ${hype.toFixed(1)}`);
+ });
+ }
+
+ destroy(): void {
+ this.off();
+ this.text.destroy();
+ }
+}
diff --git a/src/scenes/shared/ParadeScene.ts b/src/scenes/shared/ParadeScene.ts
new file mode 100644
index 0000000..78aee52
--- /dev/null
+++ b/src/scenes/shared/ParadeScene.ts
@@ -0,0 +1,182 @@
+import Phaser from 'phaser';
+import { EventBus } from '../../core/EventBus';
+import { GameClock } from '../../core/GameClock';
+import { SeededRNG } from '../../core/SeededRNG';
+import { Meters, freshNightState } from '../../core/meters';
+import { drunkStage } from '../../rules/drunk';
+import { generatePatron, idAge, _resetPatronSerial } from '../../patrons/generator';
+import { renderDoll } from '../../patrons/doll';
+import { dollPlan } from '../../patrons/dollPlan';
+import type { Patron } from '../../data/types';
+import { HudStub } from './HudStub';
+
+const W = 640;
+const H = 360;
+
+interface Walker {
+ patron: Patron;
+ root: Phaser.GameObjects.Container;
+ doll: Phaser.GameObjects.Image;
+ speed: number;
+}
+
+// Phase-0 proof scene: generator + doll renderer + RNG determinism + clock all
+// visibly working. Patrons stream across a rainy street; their ID card renders
+// from the same data record. SPACE reseeds, D toggles ID cards.
+export class ParadeScene extends Phaser.Scene {
+ private bus!: EventBus;
+ private clock!: GameClock;
+ private rng!: SeededRNG;
+ private meters!: Meters;
+ private hud!: HudStub;
+ private walkers: Walker[] = [];
+ private seed = 4207;
+ private spawnAccMs = 0;
+ private showIds = true;
+ private seedText!: Phaser.GameObjects.Text;
+ private clockText!: Phaser.GameObjects.Text;
+
+ constructor() {
+ super('Parade');
+ }
+
+ create(): void {
+ this.buildWorld();
+ this.resetRun(this.seed);
+
+ this.input.keyboard?.on('keydown-SPACE', () => this.resetRun(this.seed + 1));
+ this.input.keyboard?.on('keydown-D', () => {
+ this.showIds = !this.showIds;
+ for (const w of this.walkers) {
+ (w.root.getByName('card') as Phaser.GameObjects.Container | null)?.setVisible(this.showIds);
+ }
+ });
+ }
+
+ private buildWorld(): void {
+ // street
+ this.add.rectangle(W / 2, H - 40, W, 80, 0x1a1a24); // footpath
+ this.add.rectangle(W / 2, H / 2 - 40, W, H - 160, 0x0d0d16); // wall of night
+ // kebab shop glow, stage left — the moral centre of the game
+ this.add.rectangle(70, 120, 120, 90, 0x2a1c08);
+ this.add.rectangle(70, 96, 100, 18, 0xd8a020, 0.9);
+ this.add.text(34, 90, 'KEBABS', { fontFamily: 'monospace', fontSize: '12px', color: '#3a2404' });
+ this.add.rectangle(70, 150, 100, 40, 0xe8c060, 0.12);
+ // venue door, stage right
+ this.add.rectangle(W - 60, 130, 90, 110, 0x181828);
+ this.add.rectangle(W - 60, 90, 70, 14, 0xd03470, 0.9);
+ this.add.text(W - 88, 84, 'NOT TONIGHT', { fontFamily: 'monospace', fontSize: '9px', color: '#2a0818' });
+ this.add.rectangle(W - 60, 150, 26, 54, 0x05050a);
+ // neon reflections on the footpath
+ this.add.rectangle(70, H - 60, 90, 6, 0xd8a020, 0.15);
+ this.add.rectangle(W - 60, H - 60, 60, 6, 0xd03470, 0.15);
+
+ // rain
+ const rainG = this.add.graphics();
+ rainG.fillStyle(0x8899bb, 1);
+ rainG.fillRect(0, 0, 1, 4);
+ rainG.generateTexture('rainDrop', 1, 4);
+ rainG.destroy();
+ this.add.particles(0, 0, 'rainDrop', {
+ x: { min: 0, max: W },
+ y: -6,
+ lifespan: 900,
+ speedY: { min: 220, max: 300 },
+ speedX: { min: -30, max: -10 },
+ quantity: 2,
+ alpha: { start: 0.5, end: 0.15 },
+ });
+
+ this.seedText = this.add.text(4, H - 12, '', { fontFamily: 'monospace', fontSize: '8px', color: '#667' });
+ this.clockText = this.add.text(W - 60, 4, '', { fontFamily: 'monospace', fontSize: '8px', color: '#9fe8a0' });
+ this.add.text(4, 14, 'SPACE reseed · D toggle IDs', { fontFamily: 'monospace', fontSize: '8px', color: '#556' });
+ }
+
+ private resetRun(seed: number): void {
+ this.seed = seed;
+ for (const w of this.walkers) w.root.destroy();
+ this.walkers = [];
+ _resetPatronSerial();
+
+ this.bus?.removeAll();
+ this.bus = new EventBus();
+ this.rng = new SeededRNG(seed);
+ this.clock = new GameClock(this.bus);
+ this.meters?.destroy();
+ this.meters = new Meters(this.bus, freshNightState('theRoyal', 0, 80));
+ this.hud?.destroy();
+ this.hud = new HudStub(this, this.bus);
+ this.clock.start();
+
+ this.bus.on('clock:tick', () => {
+ // idle proof that the meter pipeline works: hype breathes with the clock
+ this.bus.emit('meters:delta', { vibe: 0 });
+ });
+
+ this.seedText.setText(`seed ${seed}`);
+ }
+
+ private spawnWalker(): void {
+ const patron = generatePatron({ rng: this.rng, nightDate: this.clock.nightDate }, this.clock.clockMin);
+ const plan = dollPlan(patron, 'queue');
+ const key = renderDoll(this, patron, 'queue');
+ const y = H - 78 + (this.walkers.length % 5) * 6;
+
+ const root = this.add.container(-40, y);
+ const doll = this.add.image(0, 0, key).setOrigin(0.5, 1);
+ root.add(doll);
+
+ const stage = drunkStage(patron.intoxication);
+ const label = this.add
+ .text(0, 4, `${patron.archetype} ${patron.age} ${stage !== 'sober' ? stage : ''}`.trim(), {
+ fontFamily: 'monospace',
+ fontSize: '7px',
+ color: stage === 'maggot' || stage === 'messy' ? '#e06060' : '#556',
+ })
+ .setOrigin(0.5, 0);
+ root.add(label);
+
+ const card = this.add.container(0, -plan.height - 18);
+ card.setName('card');
+ const idKey = renderDoll(this, patron, 'idPhoto');
+ card.add(this.add.rectangle(0, 0, 58, 30, 0xdad4c4).setStrokeStyle(1, 0x555555));
+ card.add(this.add.image(-22, 0, idKey).setScale(1));
+ const fakeMark = patron.idCard.fake ? ' ?' : '';
+ card.add(
+ this.add
+ .text(-8, -11, `${patron.idCard.name}\nID ${idAge(patron, this.clock.nightDate)}${fakeMark}`, {
+ fontFamily: 'monospace',
+ fontSize: '7px',
+ color: '#222',
+ })
+ .setOrigin(0, 0),
+ );
+ card.setVisible(this.showIds);
+ root.add(card);
+
+ root.setDepth(y);
+ this.walkers.push({ patron, root, doll, speed: 26 + (patron.dollSeed % 20) - plan.swayPx * 5 });
+ }
+
+ override update(_time: number, deltaMs: number): void {
+ this.clock.update(deltaMs);
+ this.clockText.setText(this.clock.label);
+
+ this.spawnAccMs += deltaMs;
+ const spawnEvery = 700;
+ while (this.spawnAccMs > spawnEvery && this.walkers.length < 24) {
+ this.spawnAccMs -= spawnEvery;
+ this.spawnWalker();
+ }
+
+ for (const w of [...this.walkers]) {
+ w.root.x += (w.speed * deltaMs) / 1000;
+ const sway = dollPlan(w.patron, 'queue').swayPx;
+ if (sway > 0) w.doll.x = Math.sin(w.root.x / (14 - sway * 3)) * sway;
+ if (w.root.x > W + 40) {
+ w.root.destroy();
+ this.walkers.splice(this.walkers.indexOf(w), 1);
+ }
+ }
+ }
+}
diff --git a/tests/beatclock.test.ts b/tests/beatclock.test.ts
new file mode 100644
index 0000000..d140958
--- /dev/null
+++ b/tests/beatclock.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from 'vitest';
+import { EventBus } from '../src/core/EventBus';
+import { StubBeatClock } from '../src/core/StubBeatClock';
+
+describe('StubBeatClock', () => {
+ it('emits sequential beats at the configured BPM', () => {
+ const bus = new EventBus();
+ const beats: { beatIndex: number; audioTimeMs: number }[] = [];
+ bus.on('beat:tick', (b) => beats.push(b));
+ const clock = new StubBeatClock(bus, 120); // 500ms interval
+ clock.start();
+ for (let i = 0; i < 100; i++) clock.update(37); // ragged frame times
+ expect(beats.length).toBe(Math.floor((100 * 37) / 500));
+ expect(beats.map((b) => b.beatIndex)).toEqual(beats.map((_, i) => i));
+ // beat timestamps land on exact multiples of the interval
+ for (const [i, b] of beats.entries()) expect(b.audioTimeMs).toBeCloseTo((i + 1) * 500, 6);
+ });
+
+ it('does not emit when stopped', () => {
+ const bus = new EventBus();
+ let count = 0;
+ bus.on('beat:tick', () => count++);
+ const clock = new StubBeatClock(bus, 128);
+ clock.update(10_000);
+ expect(count).toBe(0);
+ });
+});
diff --git a/tests/clock.test.ts b/tests/clock.test.ts
new file mode 100644
index 0000000..090c12f
--- /dev/null
+++ b/tests/clock.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, it } from 'vitest';
+import { EventBus } from '../src/core/EventBus';
+import { GameClock, ageOn, type ClockConfig } from '../src/core/GameClock';
+
+const FAST: ClockConfig = { nightClockMinutes: 360, nightRealMinutes: 13, nightDate: '2026-07-18' };
+
+describe('GameClock', () => {
+ it('emits one tick per in-game minute, in order', () => {
+ const bus = new EventBus();
+ const ticks: number[] = [];
+ bus.on('clock:tick', ({ clockMin }) => ticks.push(clockMin));
+ const clock = new GameClock(bus, FAST);
+ clock.start();
+ const msPerClockMin = (FAST.nightRealMinutes * 60_000) / FAST.nightClockMinutes;
+ for (let i = 0; i < 50; i++) clock.update(msPerClockMin / 5); // uneven small steps
+ expect(ticks).toEqual(Array.from({ length: ticks.length }, (_, i) => i));
+ expect(clock.clockMin).toBe(ticks[ticks.length - 1]);
+ });
+
+ it('does not advance while paused or before start', () => {
+ const bus = new EventBus();
+ const clock = new GameClock(bus, FAST);
+ clock.update(10_000);
+ expect(clock.clockMin).toBe(0);
+ clock.start();
+ clock.update(5_000);
+ const at = clock.clockMin;
+ clock.pause();
+ clock.update(60_000);
+ expect(clock.clockMin).toBe(at);
+ });
+
+ it('caps at night end', () => {
+ const bus = new EventBus();
+ const clock = new GameClock(bus, FAST);
+ clock.start();
+ clock.update(FAST.nightRealMinutes * 60_000 * 3);
+ expect(clock.clockMin).toBe(360);
+ expect(clock.isOver).toBe(true);
+ });
+
+ it('formats the label from a 9 PM start', () => {
+ const bus = new EventBus();
+ const clock = new GameClock(bus, FAST);
+ expect(clock.label).toBe('9:00 PM');
+ });
+});
+
+describe('ageOn (the ID birthday math)', () => {
+ const night = new Date('2026-07-18T00:00:00');
+
+ it('18th birthday IS tonight → 18 (they get in)', () => {
+ expect(ageOn('2008-07-18', night)).toBe(18);
+ });
+
+ it('18th birthday is tomorrow → 17 (denied, so close)', () => {
+ expect(ageOn('2008-07-19', night)).toBe(17);
+ });
+
+ it('birthday was yesterday → 18', () => {
+ expect(ageOn('2008-07-17', night)).toBe(18);
+ });
+
+ it('leap-day DOB counts the year correctly', () => {
+ expect(ageOn('2008-02-29', night)).toBe(18);
+ expect(ageOn('2008-02-29', new Date('2026-02-28T00:00:00'))).toBe(17);
+ expect(ageOn('2008-02-29', new Date('2026-03-01T00:00:00'))).toBe(18);
+ });
+});
diff --git a/tests/dollplan.test.ts b/tests/dollplan.test.ts
new file mode 100644
index 0000000..14d5e68
--- /dev/null
+++ b/tests/dollplan.test.ts
@@ -0,0 +1,72 @@
+import { describe, expect, it } from 'vitest';
+import { SeededRNG } from '../src/core/SeededRNG';
+import { dollPlan, DOLL_SIZES } from '../src/patrons/dollPlan';
+import { generatePatron, _resetPatronSerial } from '../src/patrons/generator';
+import type { Patron } from '../src/data/types';
+
+const NIGHT = new Date('2026-07-18T00:00:00');
+const somePatrons = (seed: number, n: number): Patron[] => {
+ _resetPatronSerial();
+ const ctx = { rng: new SeededRNG(seed), nightDate: NIGHT };
+ return Array.from({ length: n }, () => generatePatron(ctx, 0));
+};
+
+describe('dollPlan', () => {
+ it('is deterministic per (patron, pose)', () => {
+ for (const p of somePatrons(5, 20)) {
+ expect(dollPlan(p, 'queue')).toEqual(dollPlan(p, 'queue'));
+ expect(dollPlan(p, 'idPhoto')).toEqual(dollPlan(p, 'idPhoto'));
+ expect(dollPlan(p, 'floorTopDown')).toEqual(dollPlan(p, 'floorTopDown'));
+ }
+ });
+
+ it('respects the pose size contract and stays in bounds', () => {
+ for (const p of somePatrons(9, 30)) {
+ for (const pose of ['queue', 'idPhoto', 'floorTopDown'] as const) {
+ const plan = dollPlan(p, pose);
+ expect(plan.width).toBe(DOLL_SIZES[pose].width);
+ expect(plan.height).toBe(DOLL_SIZES[pose].height);
+ for (const r of plan.rects) {
+ expect(r.x).toBeGreaterThanOrEqual(-2); // jacket edges may hug the border
+ expect(r.x + r.w).toBeLessThanOrEqual(plan.width + 2);
+ expect(r.y).toBeGreaterThanOrEqual(0);
+ expect(r.y + r.h).toBeLessThanOrEqual(plan.height);
+ }
+ }
+ }
+ });
+
+ it('drunk stages produce sway + bloodshot tells; sober does not', () => {
+ const [p] = somePatrons(3, 1);
+ const sober = dollPlan({ ...p!, intoxication: 0.1 }, 'queue');
+ const maggot = dollPlan({ ...p!, intoxication: 0.95 }, 'queue');
+ expect(sober.swayPx).toBe(0);
+ expect(maggot.swayPx).toBe(3);
+ expect(maggot.rects.length).toBeGreaterThan(sober.rects.length); // eye pixels
+ });
+
+ it('photoMismatch renders the ID photo from the WRONG seed (visible tell)', () => {
+ const [p] = somePatrons(21, 1);
+ const honest: Patron = { ...p!, idCard: { ...p!.idCard, photoSeed: p!.dollSeed, fake: undefined } };
+ const mismatched: Patron = {
+ ...p!,
+ idCard: { ...p!.idCard, photoSeed: p!.dollSeed + 7777, fake: { photoMismatch: true } },
+ };
+ expect(dollPlan(mismatched, 'idPhoto')).not.toEqual(dollPlan(honest, 'idPhoto'));
+ // and the patron themselves renders identically — only the card lies
+ expect(dollPlan(mismatched, 'queue')).toEqual(dollPlan(honest, 'queue'));
+ });
+
+ it('outfit data drives visible pixels: white vs black sneakers differ', () => {
+ const [p] = somePatrons(33, 1);
+ const white: Patron = {
+ ...p!,
+ outfit: p!.outfit.map((l) => (l.slot === 'shoes' ? { ...l, type: 'sneaker', colour: 'white' } : l)),
+ };
+ const black: Patron = {
+ ...p!,
+ outfit: p!.outfit.map((l) => (l.slot === 'shoes' ? { ...l, type: 'sneaker', colour: 'black' } : l)),
+ };
+ expect(dollPlan(white, 'queue')).not.toEqual(dollPlan(black, 'queue'));
+ });
+});
diff --git a/tests/eventbus.test.ts b/tests/eventbus.test.ts
new file mode 100644
index 0000000..590ca31
--- /dev/null
+++ b/tests/eventbus.test.ts
@@ -0,0 +1,54 @@
+import { describe, expect, it, vi } from 'vitest';
+import { EventBus } from '../src/core/EventBus';
+
+describe('EventBus', () => {
+ it('delivers typed payloads to subscribers', () => {
+ const bus = new EventBus();
+ const fn = vi.fn();
+ bus.on('clock:tick', fn);
+ bus.emit('clock:tick', { clockMin: 5 });
+ expect(fn).toHaveBeenCalledWith({ clockMin: 5 });
+ });
+
+ it('on() returns an unsubscribe function', () => {
+ const bus = new EventBus();
+ const fn = vi.fn();
+ const off = bus.on('clock:tick', fn);
+ off();
+ bus.emit('clock:tick', { clockMin: 1 });
+ expect(fn).not.toHaveBeenCalled();
+ });
+
+ it('once() fires exactly once', () => {
+ const bus = new EventBus();
+ const fn = vi.fn();
+ bus.once('clock:tick', fn);
+ bus.emit('clock:tick', { clockMin: 1 });
+ bus.emit('clock:tick', { clockMin: 2 });
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
+
+ it('unsubscribing a later handler during emit prevents its delivery', () => {
+ const bus = new EventBus();
+ const calls: string[] = [];
+ let offB: () => void = () => {};
+ bus.on('clock:tick', () => {
+ calls.push('a');
+ offB();
+ });
+ offB = bus.on('clock:tick', () => calls.push('b'));
+ bus.emit('clock:tick', { clockMin: 1 });
+ expect(calls).toEqual(['a']);
+ });
+
+ it('subscribing during emit does not deliver to the new handler this emit', () => {
+ const bus = new EventBus();
+ const calls: string[] = [];
+ bus.on('clock:tick', () => {
+ calls.push('a');
+ bus.on('clock:tick', () => calls.push('late'));
+ });
+ bus.emit('clock:tick', { clockMin: 1 });
+ expect(calls).toEqual(['a']);
+ });
+});
diff --git a/tests/generator.test.ts b/tests/generator.test.ts
new file mode 100644
index 0000000..48f21bf
--- /dev/null
+++ b/tests/generator.test.ts
@@ -0,0 +1,95 @@
+import { beforeEach, describe, expect, it } from 'vitest';
+import { SeededRNG } from '../src/core/SeededRNG';
+import { ageOn } from '../src/core/GameClock';
+import { generatePatron, _resetPatronSerial, type GeneratorContext } from '../src/patrons/generator';
+import type { Patron } from '../src/data/types';
+
+const NIGHT = new Date('2026-07-18T00:00:00');
+const ctx = (seed: number): GeneratorContext => ({ rng: new SeededRNG(seed), nightDate: NIGHT });
+
+const batch = (seed: number, n: number, clockMin = 0): Patron[] => {
+ _resetPatronSerial();
+ const c = ctx(seed);
+ return Array.from({ length: n }, () => generatePatron(c, clockMin));
+};
+
+beforeEach(() => _resetPatronSerial());
+
+describe('generatePatron', () => {
+ it('is deterministic: same seed → identical batch', () => {
+ expect(batch(42, 50)).toEqual(batch(42, 50));
+ });
+
+ it('different seeds → different batches', () => {
+ const a = batch(1, 20).map((p) => p.dollSeed);
+ const b = batch(2, 20).map((p) => p.dollSeed);
+ expect(a).not.toEqual(b);
+ });
+
+ it('real IDs never carry fake tells and their DOB matches true age', () => {
+ for (const p of batch(7, 300)) {
+ if (!p.idCard.fake) {
+ expect(ageOn(p.idCard.dob, NIGHT)).toBe(p.age);
+ expect(p.age).toBeGreaterThanOrEqual(18);
+ }
+ }
+ });
+
+ it('every fake ID has at least one detectable tell', () => {
+ const fakes = batch(11, 600).filter((p) => p.idCard.fake);
+ expect(fakes.length).toBeGreaterThan(0);
+ for (const p of fakes) {
+ const f = p.idCard.fake!;
+ const expired = new Date(`${p.idCard.expiry}T00:00:00`) < NIGHT;
+ const hasTell =
+ !!f.peelingLaminate || !!f.wrongHologram || !!f.jokeName || !!f.photoMismatch || expired;
+ expect(hasTell).toBe(true);
+ }
+ });
+
+ it('almost18s are 17 with fake IDs claiming 18+; fresh18s are genuinely 18', () => {
+ _resetPatronSerial();
+ const c = ctx(13);
+ for (let i = 0; i < 40; i++) {
+ const kid = generatePatron(c, 0, 'almost18');
+ expect(kid.age).toBe(17);
+ expect(kid.idCard.fake).toBeDefined();
+ expect(ageOn(kid.idCard.dob, NIGHT)).toBeGreaterThanOrEqual(18);
+ const fresh = generatePatron(c, 0, 'fresh18');
+ expect(fresh.age).toBe(18);
+ expect(fresh.idCard.fake).toBeUndefined();
+ }
+ });
+
+ it('photoMismatch is the only case where photoSeed differs from dollSeed', () => {
+ for (const p of batch(17, 600)) {
+ if (p.idCard.fake?.photoMismatch) expect(p.idCard.photoSeed).not.toBe(p.dollSeed);
+ else expect(p.idCard.photoSeed).toBe(p.dollSeed);
+ }
+ });
+
+ it('late crowd arrives drunker on average', () => {
+ const early = batch(23, 200, 0);
+ const late = batch(23, 200, 300);
+ const avg = (ps: Patron[]) => ps.reduce((s, p) => s + p.intoxication, 0) / ps.length;
+ expect(avg(late)).toBeGreaterThan(avg(early) + 0.05);
+ });
+
+ it('outfits fill every slot and intoxication/tolerance stay in 0..1', () => {
+ for (const p of batch(29, 100)) {
+ expect(p.outfit.map((l) => l.slot).sort()).toEqual(
+ ['accessory', 'hair', 'legs', 'outer', 'shoes', 'top'],
+ );
+ expect(p.intoxication).toBeGreaterThanOrEqual(0);
+ expect(p.intoxication).toBeLessThanOrEqual(1);
+ expect(p.tolerance).toBeGreaterThanOrEqual(0);
+ expect(p.tolerance).toBeLessThanOrEqual(1);
+ }
+ });
+
+ it('guest-list truth implies the claim (liars exist, honest listers do not hide)', () => {
+ for (const p of batch(31, 400)) {
+ if (p.flags.onGuestList) expect(p.flags.claimsGuestList).toBe(true);
+ }
+ });
+});
diff --git a/tests/memory.test.ts b/tests/memory.test.ts
new file mode 100644
index 0000000..911af2e
--- /dev/null
+++ b/tests/memory.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from 'vitest';
+import { freshGameState } from '../src/core/save';
+import { recordDenial, shouldReturnDisguised } from '../src/patrons/memory';
+import { SeededRNG } from '../src/core/SeededRNG';
+import { generatePatron, _resetPatronSerial } from '../src/patrons/generator';
+
+const patron = () => {
+ _resetPatronSerial();
+ return generatePatron({ rng: new SeededRNG(1), nightDate: new Date('2026-07-18T00:00:00') }, 0);
+};
+
+describe('regular memory', () => {
+ it('accumulates denials and grudge, capped at 1', () => {
+ const state = freshGameState(1);
+ const p = patron();
+ recordDenial(state, p, 0);
+ recordDenial(state, p, 1);
+ const mem = recordDenial(state, p, 2);
+ expect(mem.timesDenied).toBe(3);
+ expect(mem.grudge).toBe(1);
+ expect(mem.lastSeenNight).toBe(2);
+ });
+
+ it('two denials → returns disguised (the bad-moustache rule)', () => {
+ const state = freshGameState(1);
+ const p = patron();
+ expect(shouldReturnDisguised(state, p.id)).toBe(false);
+ recordDenial(state, p, 0);
+ expect(shouldReturnDisguised(state, p.id)).toBe(false);
+ recordDenial(state, p, 1);
+ expect(shouldReturnDisguised(state, p.id)).toBe(true);
+ });
+
+ it('memory survives a save/load roundtrip shape-wise', () => {
+ const state = freshGameState(1);
+ recordDenial(state, patron(), 0);
+ const json = JSON.parse(JSON.stringify(state)) as typeof state;
+ expect(json.regulars).toEqual(state.regulars);
+ });
+});
diff --git a/tests/meters.test.ts b/tests/meters.test.ts
new file mode 100644
index 0000000..cf0768d
--- /dev/null
+++ b/tests/meters.test.ts
@@ -0,0 +1,61 @@
+import { describe, expect, it, vi } from 'vitest';
+import { EventBus } from '../src/core/EventBus';
+import { Meters, freshNightState } from '../src/core/meters';
+
+const setup = () => {
+ const bus = new EventBus();
+ const state = freshNightState('theRoyal', 0, 80);
+ const meters = new Meters(bus, state);
+ return { bus, state, meters };
+};
+
+describe('Meters', () => {
+ it('applies deltas and emits meters:changed', () => {
+ const { bus, state } = setup();
+ const fn = vi.fn();
+ bus.on('meters:changed', fn);
+ bus.emit('meters:delta', { vibe: 10, aggro: 5 });
+ expect(state.vibe).toBe(60);
+ expect(state.aggro).toBe(5);
+ expect(fn).toHaveBeenCalledWith({ vibe: 60, aggro: 5, hype: 1 });
+ });
+
+ it('clamps vibe/aggro to 0..100 and hype to 1..3', () => {
+ const { bus, state } = setup();
+ bus.emit('meters:delta', { vibe: 999, aggro: -50, hype: 99 });
+ expect(state.vibe).toBe(100);
+ expect(state.aggro).toBe(0);
+ expect(state.hype).toBe(3);
+ bus.emit('meters:delta', { vibe: -999 });
+ expect(state.vibe).toBe(0);
+ });
+
+ it('hype multiplies positive vibe only — never softens a hit', () => {
+ const { bus, state } = setup();
+ bus.emit('meters:delta', { hype: 1 }); // hype -> 2
+ bus.emit('meters:delta', { vibe: 10 });
+ expect(state.vibe).toBe(70); // 50 + 10*2
+ bus.emit('meters:delta', { vibe: -10 });
+ expect(state.vibe).toBe(60); // full -10, unscaled
+ });
+
+ it('collects heat strikes and tracks clicker drift', () => {
+ const { bus, state } = setup();
+ bus.emit('heat:strike', { reason: 'minor admitted' });
+ expect(state.heatStrikes).toHaveLength(1);
+ bus.emit('door:clicker', { direction: 'in' });
+ bus.emit('door:clicker', { direction: 'in' });
+ bus.emit('door:clicker', { direction: 'out' });
+ expect(state.capacity.clickerShown).toBe(1);
+ bus.emit('door:clicker', { direction: 'out' });
+ bus.emit('door:clicker', { direction: 'out' });
+ expect(state.capacity.clickerShown).toBe(0); // clicker can't go negative
+ });
+
+ it('destroy() detaches all listeners', () => {
+ const { bus, state, meters } = setup();
+ meters.destroy();
+ bus.emit('meters:delta', { vibe: 10 });
+ expect(state.vibe).toBe(50);
+ });
+});
diff --git a/tests/rng.test.ts b/tests/rng.test.ts
new file mode 100644
index 0000000..5e89ec1
--- /dev/null
+++ b/tests/rng.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, it } from 'vitest';
+import { SeededRNG } from '../src/core/SeededRNG';
+
+describe('SeededRNG', () => {
+ it('same seed + stream name → identical sequence', () => {
+ const a = new SeededRNG(42).stream('patrons');
+ const b = new SeededRNG(42).stream('patrons');
+ for (let i = 0; i < 100; i++) expect(a.next()).toBe(b.next());
+ });
+
+ it('different seeds diverge', () => {
+ const a = new SeededRNG(1).stream('x');
+ const b = new SeededRNG(2).stream('x');
+ const seqA = Array.from({ length: 10 }, () => a.next());
+ const seqB = Array.from({ length: 10 }, () => b.next());
+ expect(seqA).not.toEqual(seqB);
+ });
+
+ it('streams are independent — draining one does not shift another', () => {
+ const rng1 = new SeededRNG(7);
+ const rng2 = new SeededRNG(7);
+ rng1.stream('a');
+ for (let i = 0; i < 500; i++) rng1.stream('a').next(); // drain a
+ const fromDrained = rng1.stream('b').next();
+ const fromFresh = rng2.stream('b').next();
+ expect(fromDrained).toBe(fromFresh);
+ });
+
+ it('stream() returns the same stream instance per name (stateful)', () => {
+ const rng = new SeededRNG(9);
+ const first = rng.stream('q').next();
+ const second = rng.stream('q').next();
+ expect(first).not.toBe(second); // continued, not restarted
+ });
+
+ it('int() covers bounds inclusively and stays in range', () => {
+ const s = new SeededRNG(3).stream('ints');
+ const seen = new Set();
+ for (let i = 0; i < 2000; i++) {
+ const v = s.int(1, 6);
+ expect(v).toBeGreaterThanOrEqual(1);
+ expect(v).toBeLessThanOrEqual(6);
+ seen.add(v);
+ }
+ expect(seen.size).toBe(6);
+ });
+
+ it('chance() distribution sanity', () => {
+ const s = new SeededRNG(11).stream('coin');
+ let hits = 0;
+ for (let i = 0; i < 10_000; i++) if (s.chance(0.3)) hits++;
+ expect(hits / 10_000).toBeGreaterThan(0.27);
+ expect(hits / 10_000).toBeLessThan(0.33);
+ });
+
+ it('weighted() respects weights and never returns zero-weight entries', () => {
+ const s = new SeededRNG(13).stream('w');
+ const counts = { a: 0, b: 0, c: 0 };
+ for (let i = 0; i < 10_000; i++) {
+ counts[s.weighted([['a', 9], ['b', 1], ['c', 0]] as const)]++;
+ }
+ expect(counts.c).toBe(0);
+ expect(counts.a).toBeGreaterThan(counts.b * 5);
+ });
+
+ it('pick/weighted throw on empty/invalid input', () => {
+ const s = new SeededRNG(1).stream('e');
+ expect(() => s.pick([])).toThrow();
+ expect(() => s.weighted([])).toThrow();
+ });
+});
diff --git a/tests/save.test.ts b/tests/save.test.ts
new file mode 100644
index 0000000..a705c0f
--- /dev/null
+++ b/tests/save.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from 'vitest';
+import { freshGameState, loadGame, saveGame, type StorageLike } from '../src/core/save';
+
+const memStorage = (): StorageLike & { data: Map } => {
+ const data = new Map();
+ return {
+ data,
+ getItem: (k) => data.get(k) ?? null,
+ setItem: (k, v) => void data.set(k, v),
+ removeItem: (k) => void data.delete(k),
+ };
+};
+
+describe('save', () => {
+ it('roundtrips game state', () => {
+ const storage = memStorage();
+ const state = freshGameState(4207);
+ state.nightIndex = 2;
+ state.regulars['p9'] = { patronId: 'p9', timesDenied: 2, lastSeenNight: 1, grudge: 0.68 };
+ saveGame(state, storage);
+ expect(loadGame(0, storage)).toEqual(state);
+ });
+
+ it('missing save → fresh state with fallback seed', () => {
+ const loaded = loadGame(77, memStorage());
+ expect(loaded).toEqual(freshGameState(77));
+ });
+
+ it('corrupt JSON → fresh state, no throw', () => {
+ const storage = memStorage();
+ storage.setItem('not-tonight-save', '{nope');
+ expect(loadGame(5, storage)).toEqual(freshGameState(5));
+ });
+
+ it('wrong version/shape → fresh state', () => {
+ const storage = memStorage();
+ storage.setItem('not-tonight-save', JSON.stringify({ v: 99, seed: 'what' }));
+ expect(loadGame(6, storage)).toEqual(freshGameState(6));
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ebe7c72
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2022", "DOM"],
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src", "tests"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..0ca6e31
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,11 @@
+///
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ base: './',
+ server: { port: 5199 },
+ test: {
+ include: ['tests/**/*.test.ts'],
+ environment: 'node',
+ },
+});