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', + }, +});