From 1e8b2acbe03ecfaeeb2588c0c88f04e61c798333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 16:12:15 +0800 Subject: [PATCH 01/12] feat(core): route explicit 3D prompts to threed_basic Add the frozen classifier/GDD contract for a minimal three.js archetype while preserving every existing Phaser route. Includes regression fixtures for explicit 3D, 2D platformer, and 2D top-down prompts. --- .../src/tools/game-type-classifier.test.ts | 60 +++++++++++++++++++ .../core/src/tools/game-type-classifier.ts | 41 ++++++++++--- packages/core/src/tools/generate-gdd.ts | 41 ++++++++++++- 3 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/tools/game-type-classifier.test.ts diff --git a/packages/core/src/tools/game-type-classifier.test.ts b/packages/core/src/tools/game-type-classifier.test.ts new file mode 100644 index 000000000..2f7cf8890 --- /dev/null +++ b/packages/core/src/tools/game-type-classifier.test.ts @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { Config } from '../config/config.js'; +import { GameTypeClassifierTool } from './game-type-classifier.js'; + +const modelConfig = { + apiKey: 'test-key', + baseUrl: 'https://classifier.test/v1', + modelName: 'test-model', +}; + +afterEach(() => vi.unstubAllGlobals()); + +describe('GameTypeClassifierTool archetype routing', () => { + it.each([ + [ + '做一个 three.js 迷宫漫游游戏', + 'threed_basic', + 'three_dimensional', + 'core3d', + ], + ['做一个 2D Phaser 横版跳跃游戏', 'platformer', 'side', 'core'], + ['做一个 2D Phaser 俯视角自由移动游戏', 'top_down', 'top_down', 'core'], + ])( + 'routes %s to %s without changing the 2D core', + async (gameDescription, archetype, perspective, coreTemplate) => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + choices: [ + { + message: { + content: JSON.stringify({ + archetype, + reasoning: 'test fixture', + physicsProfile: { + hasGravity: archetype === 'platformer', + perspective, + movementType: 'continuous', + }, + }), + }, + }, + ], + }), + }), + ); + + const tool = new GameTypeClassifierTool({} as Config, modelConfig); + const result = await tool + .build({ game_description: gameDescription }) + .execute(new AbortController().signal); + + expect(result.error).toBeUndefined(); + expect(result.llmContent).toContain(`Archetype: ${archetype}`); + expect(result.llmContent).toContain(`/templates/${coreTemplate}/*`); + }, + ); +}); diff --git a/packages/core/src/tools/game-type-classifier.ts b/packages/core/src/tools/game-type-classifier.ts index b81cf2f0a..9107f5768 100644 --- a/packages/core/src/tools/game-type-classifier.ts +++ b/packages/core/src/tools/game-type-classifier.ts @@ -44,14 +44,15 @@ export type GameArchetype = | 'top_down' | 'grid_logic' | 'tower_defense' - | 'ui_heavy'; + | 'ui_heavy' + | 'threed_basic'; export interface ClassificationResult { archetype: GameArchetype; reasoning: string; physicsProfile: { hasGravity: boolean; - perspective: 'side' | 'top_down' | 'none'; + perspective: 'side' | 'top_down' | 'three_dimensional' | 'none'; movementType: 'continuous' | 'grid' | 'path' | 'ui_only'; }; } @@ -173,12 +174,21 @@ You are a game physics analyzer. Your job is to classify games based on their PH **Key Question**: Is the game primarily UI panels and state changes? +### 6. threed_basic (Explicit 3D World) +**Physics**: No physics engine; simple geometry and manual collision bounds +**Perspective**: First-person, third-person, or freely moving camera in a three-dimensional world +**Movement**: Keyboard movement plus mouse look/steering +**Examples**: three.js maze roaming, low-poly floating-island treasure hunt, neon 3D collection runner + +**Key Question**: Does the request explicitly require 3D, three.js, a 3D camera, or movement through a three-dimensional world? +**Priority Rule**: Explicit 3D intent wins over genre words such as maze, platformer, runner, or racing. Do not classify ordinary 2D Phaser games as threed_basic. + ## Output Format Respond with ONLY a JSON object (no markdown, no explanation outside JSON): { - "archetype": "platformer" | "top_down" | "grid_logic" | "tower_defense" | "ui_heavy", + "archetype": "platformer" | "top_down" | "grid_logic" | "tower_defense" | "ui_heavy" | "threed_basic", "reasoning": "Brief explanation of why this archetype was chosen based on physics", "physicsProfile": { "hasGravity": true | false, @@ -194,6 +204,8 @@ Respond with ONLY a JSON object (no markdown, no explanation outside JSON): - Hill Climb Racing is NOT top_down (it has gravity, it's platformer) - SimCity/Factorio are grid_logic (grid-based building), not top_down - Racing games: If side-view with gravity = platformer, if top-down = top_down +- A "three.js maze roaming" request is threed_basic, not grid_logic or top_down +- A normal 2D side-view Phaser platformer remains platformer `; } @@ -202,7 +214,7 @@ Respond with ONLY a JSON object (no markdown, no explanation outside JSON): "${this.params.game_description}" -Remember: Think about GRAVITY, PERSPECTIVE, and MOVEMENT TYPE. Output JSON only.`; +Remember: Explicit 3D/three.js intent takes priority; otherwise use GRAVITY, PERSPECTIVE, and MOVEMENT TYPE. Output JSON only.`; } private async callClassifierModel( @@ -280,6 +292,7 @@ Remember: Think about GRAVITY, PERSPECTIVE, and MOVEMENT TYPE. Output JSON only. } catch { // Fallback: try to extract archetype from text const archetypes: GameArchetype[] = [ + 'threed_basic', 'platformer', 'top_down', 'grid_logic', @@ -293,7 +306,12 @@ Remember: Think about GRAVITY, PERSPECTIVE, and MOVEMENT TYPE. Output JSON only. reasoning: result, physicsProfile: { hasGravity: arch === 'platformer', - perspective: arch === 'platformer' ? 'side' : 'top_down', + perspective: + arch === 'threed_basic' + ? 'three_dimensional' + : arch === 'platformer' + ? 'side' + : 'top_down', movementType: arch === 'grid_logic' ? 'grid' : 'continuous', }, }; @@ -317,6 +335,9 @@ Remember: Think about GRAVITY, PERSPECTIVE, and MOVEMENT TYPE. Output JSON only. const templatesDir = process.env.GAME_TEMPLATES_DIR || '../../templates'; const docsDir = process.env.GAME_DOCS_DIR || '../../docs'; + const coreTemplate = + result.archetype === 'threed_basic' ? 'core3d' : 'core'; + return ` Archetype: ${result.archetype} Reasoning: ${result.reasoning} @@ -330,13 +351,13 @@ Physics Profile: GAME TYPE CLASSIFIED: **${result.archetype}** -## Next Step: Scaffold Templates (FOUR commands) +## Next Step: Scaffold Templates Run these commands NOW: \`\`\`bash # Step 1: Copy core template (creates src/, public/, config files) -cp -r ${templatesDir}/core/* ./ +cp -r ${templatesDir}/${coreTemplate}/* ./ # Step 2: Copy module-specific code INTO src/ (ADDITIVE merge) cp -r ${templatesDir}/modules/${result.archetype}/src/* ./src/ @@ -349,6 +370,9 @@ cp ${docsDir}/asset_protocol.md ${docsDir}/debug_protocol.md docs/ # Step 4: Copy module-specific documentation mkdir -p docs/modules/${result.archetype} cp -r ${docsDir}/modules/${result.archetype}/* docs/modules/${result.archetype}/ + +# three.js is pinned in core3d/package-lock.json; install only inside this game workspace. +if [ "${result.archetype}" = "threed_basic" ]; then npm ci; fi \`\`\` ## After Scaffolding: Proceed to Phase 2 (GDD Generation) @@ -369,6 +393,7 @@ Next: Call \`generate-gdd\` tool with: grid_logic: 'Grid + Static Logic (Sokoban, Fire Emblem, Match-3)', tower_defense: 'Path + Waves (Kingdom Rush, Bloons TD)', ui_heavy: 'UI Driven (Card Games, Visual Novels, Idle Clickers)', + threed_basic: '3D World + Manual Movement (three.js primitives)', }; return `**Game Type Classification** @@ -422,7 +447,7 @@ export class GameTypeClassifierTool extends BaseDeclarativeTool< super( GameTypeClassifierTool.Name, ToolDisplayNames.GAME_TYPE_CLASSIFIER, - `Classifies a game idea into an archetype based on PHYSICS and PERSPECTIVE (not genre name). Returns: platformer (side+gravity), top_down (free movement), grid_logic (discrete tiles), tower_defense (path+waves), or ui_heavy (no physics). Call this FIRST before scaffolding templates.`, + `Classifies a game idea into an archetype based on DIMENSION, PHYSICS, and PERSPECTIVE (not genre name). Returns: platformer (side+gravity), top_down (free movement), grid_logic (discrete tiles), tower_defense (path+waves), ui_heavy (no physics), or threed_basic (explicit three.js/3D world). Call this FIRST before scaffolding templates.`, Kind.Think, { type: 'object', diff --git a/packages/core/src/tools/generate-gdd.ts b/packages/core/src/tools/generate-gdd.ts index 2c8708d52..2bb20eb48 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -20,7 +20,8 @@ export type GameArchetype = | 'top_down' | 'grid_logic' | 'tower_defense' - | 'ui_heavy'; + | 'ui_heavy' + | 'threed_basic'; export interface GenerateGDDParams { /** @@ -111,11 +112,11 @@ Save content between tags to \`GAME_DESIGN.md\` ### Phase 3: Assets (use GDD Section 1) - Read \`{DOCS_DIR}/asset_protocol.md\` - Call \`generate_game_assets\` with the Asset Registry table from **GDD Section 1** -- Call \`generate_tilemap\` with ASCII maps from **GDD Section 4** (NOT for ui_heavy, tower_defense, or grid_logic -- these use code-defined grids) +- Call \`generate_tilemap\` with ASCII maps from **GDD Section 4** (NOT for ui_heavy, tower_defense, grid_logic, or threed_basic) - Read \`public/assets/asset-pack.json\` for generated texture keys ### Phase 4: Config (use GDD Section 2) -- MERGE GDD Section 2 values INTO the existing \`src/gameConfig.json\` -- add/update game-specific fields using \`{ "value": X }\` wrapper format, but NEVER delete infrastructure fields (\`screenSize\`, \`debugConfig\`, \`renderConfig\`) +- MERGE GDD Section 2 values INTO the existing \`src/gameConfig.json\` -- add/update game-specific fields using \`{ "value": X }\` wrapper format, but NEVER delete infrastructure fields (\`screenSize\`, \`renderConfig\`, and Phaser's \`debugConfig\`) ### Phase 5: Code Implementation (use GDD Sections 0, 3, 5) - **GDD Section 0** has scene keys -> update \`LevelManager.ts\` and \`main.ts\` @@ -623,6 +624,19 @@ Tower Defense does NOT use generate_tilemap -- maps are code-defined grids. - Do NOT invent new hooks (e.g., onRoundStart, onBuzzerPressed, onAnswerSelected, onTimeout do NOT exist) - If template_api.md doesn't list a hook, it DOES NOT EXIST -- design around existing hooks only +`, + threed_basic: `--- + +## Three.js Basic Rules (Built-in) + +- Runtime: pinned three.js + TypeScript + Vite; do not import Phaser. +- World: primitives/custom low-poly geometry, one PerspectiveCamera, ambient + directional light, fog, and a sky texture. +- Input: WASD/arrows + mouse; ESC uses the existing DOM pause contract. No touch controls. +- Gameplay: one short route, 5-10 collectible objectives, one reachable completion state. +- Collision: manual bounds/proximity only. Do not add a physics engine. +- Assets: use generate_game_assets only for skybox/texture/billboard/floor patch images. No models, GLB/FBX/OBJ, text-to-3D, or generate_tilemap. +- Architecture: keep main.ts RAF wiring; put declarative positions in SceneMap.ts and rendering/gameplay in GameScene.ts. +- Required verification: build, WebGL context, non-black canvas, zero console errors, and ESC pause/resume. `, }; @@ -711,6 +725,12 @@ ${this.getSection4Guidance(archetype)}`; { "type": "image", "key": "icon_tower_tabby", "description": "Icon for Spitfire Tabby tower selection UI: small version of tabby cat" } { "type": "image", "key": "tower_slot", "description": "A round stone platform with subtle moss, top-down view" } \`\`\``, + threed_basic: `**Three.js image asset rules:** +- Skybox: \`type: "background"\`, key \`skybox_texture\`, equirectangular, \`resolution: "1024*1024"\`. +- Floor patch/surface: \`type: "image"\`, key \`floor_patch\`; declare runtime \`displaySize\` in the GDD, never pass it to the tool. +- Collectible billboard: \`type: "image"\`, key \`energy_billboard\`, one centered subject. +- Keep source/display size <= 1024*1024. Colormap only matte art; never glossy/translucent/emissive/sky art. +- Every generated image must remain in asset-pack.json. Do not request models, meshes, normal maps, or text-to-3D output.`, }; return guidance[archetype] || ''; } @@ -722,6 +742,7 @@ ${this.getSection4Guidance(archetype)}`; top_down: 'Entity Architecture (Behavior Composition)', grid_logic: 'Entity Architecture (Grid Logic)', tower_defense: 'Entity Architecture (Towers & Waves)', + threed_basic: '3D Scene Architecture (SceneMap + GameScene)', }; return titles[archetype] || 'Entity Architecture'; } @@ -867,6 +888,9 @@ Refer to Design Guide Section 7 for screen shake rules, Template Capabilities Se - \`getCellsInRadius(x, y, radius, w, h)\`: cells in area (AoE effects) **CRITICAL**: Every hook name must exist in template_api.md. Do NOT invent hooks.`, + threed_basic: `Define the exact \`SceneMap.ts\` arrays (floorPatches, collectibles, obstacles), the \`GameScene\` texture-key mapping, camera start/bounds, and completion condition. + +Use only the public constructor and methods from template_api.md: \`update\`, \`setPaused\`, \`isPaused\`, and \`resize\`. Preserve main.ts RAF and DOM-screen wiring. List exact config fields and values; do not invent editor/runtime hooks.`, tower_defense: `Define every tower type, enemy type, and level scene with EXACT configurations. This maps directly to code files. **For each Tower Type** (COPY \`_TemplateTower.ts\` -> \`TowerName.ts\`): @@ -932,6 +956,7 @@ if (slowAmt && slowDur) enemy.applyStatusEffect('slow', slowAmt, slowDur, 0x4488 top_down: 'Level Design', grid_logic: 'Level & Puzzle Design (Code-Defined Grid)', tower_defense: 'Map & Wave Design', + threed_basic: 'Single-Route 3D Level Map', }; return titles[archetype] || 'Level Design'; } @@ -1161,6 +1186,14 @@ List which tower types are available in each level. Early levels may restrict to - Starting gold: 80-200 (higher = easier) - Kill rewards: 5-50g per enemy type - Wave clear bonuses: 10-100g per wave`, + threed_basic: `**Do not use generate_tilemap.** Provide one declarative route for \`initSceneMap()\`: +- 8-14 overlapping floor patches with x/z/radius values +- 5-10 reachable collectibles with id/x/y/z values +- 8-16 low-poly obstacle decorations outside the main walking line +- camera start, track bounds, pickup radius, and finish z +- a manual play path proving every collectible is reachable + +End Section 4 with: \`3D scope: primitives + generated image textures; no model generation\`.`, }; return guidance[archetype] || ''; } @@ -1271,6 +1304,7 @@ export class GenerateGDDTool extends BaseDeclarativeTool< 'grid_logic', 'tower_defense', 'ui_heavy', + 'threed_basic', ], }, config_summary: { @@ -1306,6 +1340,7 @@ export class GenerateGDDTool extends BaseDeclarativeTool< 'grid_logic', 'tower_defense', 'ui_heavy', + 'threed_basic', ]; if (!validArchetypes.includes(params.archetype)) { return `Invalid archetype: ${params.archetype}. Must be one of: ${validArchetypes.join(', ')}`; From 3c0143bbfc609d02d87c888560159a5a6b1d73fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 16:13:04 +0800 Subject: [PATCH 02/12] feat(templates): add minimal three.js game archetype Provide the core3d skeleton, threed_basic gameplay layer, pinned npm lockfile, and builder-facing manuals. The template keeps DOM lifecycle screens and the three-key ESC fallback while limiting assets to the existing image pipeline. --- agent-test/docs/asset_protocol.md | 24 + .../docs/modules/threed_basic/design_rules.md | 50 + .../docs/modules/threed_basic/template_api.md | 65 + .../docs/modules/threed_basic/threed_basic.md | 56 + agent-test/templates/core3d/.gitignore | 3 + agent-test/templates/core3d/README.md | 10 + agent-test/templates/core3d/index.html | 13 + agent-test/templates/core3d/package-lock.json | 2361 +++++++++++++++++ agent-test/templates/core3d/package.json | 23 + agent-test/templates/core3d/postcss.config.js | 3 + agent-test/templates/core3d/src/GameScene.ts | 144 + .../templates/core3d/src/LevelManager.ts | 7 + .../templates/core3d/src/gameConfig.json | 15 + agent-test/templates/core3d/src/main.ts | 88 + .../core3d/src/scenes/GameCompleteUIScene.ts | 10 + .../core3d/src/scenes/GameOverUIScene.ts | 10 + .../core3d/src/scenes/PauseUIScene.ts | 44 + .../templates/core3d/src/scenes/Preloader.ts | 48 + .../core3d/src/scenes/TitleScreen.ts | 33 + .../templates/core3d/src/scenes/UIScene.ts | 39 + .../templates/core3d/src/styles/tailwind.css | 19 + .../templates/core3d/tailwind.config.js | 10 + agent-test/templates/core3d/tsconfig.json | 16 + agent-test/templates/core3d/vite.config.js | 6 + .../modules/threed_basic/src/GameScene.ts | 173 ++ .../threed_basic/src/InputController.ts | 55 + .../modules/threed_basic/src/SceneMap.ts | 29 + .../threed_basic/src/ThreeSceneDefaults.ts | 15 + .../modules/threed_basic/src/gameConfig.json | 20 + 29 files changed, 3389 insertions(+) create mode 100644 agent-test/docs/modules/threed_basic/design_rules.md create mode 100644 agent-test/docs/modules/threed_basic/template_api.md create mode 100644 agent-test/docs/modules/threed_basic/threed_basic.md create mode 100644 agent-test/templates/core3d/.gitignore create mode 100644 agent-test/templates/core3d/README.md create mode 100644 agent-test/templates/core3d/index.html create mode 100644 agent-test/templates/core3d/package-lock.json create mode 100644 agent-test/templates/core3d/package.json create mode 100644 agent-test/templates/core3d/postcss.config.js create mode 100644 agent-test/templates/core3d/src/GameScene.ts create mode 100644 agent-test/templates/core3d/src/LevelManager.ts create mode 100644 agent-test/templates/core3d/src/gameConfig.json create mode 100644 agent-test/templates/core3d/src/main.ts create mode 100644 agent-test/templates/core3d/src/scenes/GameCompleteUIScene.ts create mode 100644 agent-test/templates/core3d/src/scenes/GameOverUIScene.ts create mode 100644 agent-test/templates/core3d/src/scenes/PauseUIScene.ts create mode 100644 agent-test/templates/core3d/src/scenes/Preloader.ts create mode 100644 agent-test/templates/core3d/src/scenes/TitleScreen.ts create mode 100644 agent-test/templates/core3d/src/scenes/UIScene.ts create mode 100644 agent-test/templates/core3d/src/styles/tailwind.css create mode 100644 agent-test/templates/core3d/tailwind.config.js create mode 100644 agent-test/templates/core3d/tsconfig.json create mode 100644 agent-test/templates/core3d/vite.config.js create mode 100644 agent-test/templates/modules/threed_basic/src/GameScene.ts create mode 100644 agent-test/templates/modules/threed_basic/src/InputController.ts create mode 100644 agent-test/templates/modules/threed_basic/src/SceneMap.ts create mode 100644 agent-test/templates/modules/threed_basic/src/ThreeSceneDefaults.ts create mode 100644 agent-test/templates/modules/threed_basic/src/gameConfig.json diff --git a/agent-test/docs/asset_protocol.md b/agent-test/docs/asset_protocol.md index 9eac1d4d4..5417ac27f 100644 --- a/agent-test/docs/asset_protocol.md +++ b/agent-test/docs/asset_protocol.md @@ -37,6 +37,30 @@ If generating \*too many assets**, split into **2 separate tool calls\*\* to avo | `image` | `key`, `description` | PNG 386\*560 (portrait) | Yes | | `audio` | `key`, `description`, `audioType`, `duration?`, `genre?`, `tempo?` | WAV (8-bit chiptune) | N/A | +### 1.2.0 3D Image Assets (`threed_basic`) + +Three.js games use the same `generate_game_assets` image pipeline. These are +2D images applied to geometry; they are not 3D models and do not create a new +tool type. + +| 3D role | Existing type | Required description/runtime contract | +|---|---|---| +| skybox | `background` | equirectangular sky texture, `1024*1024`, no background removal | +| surface texture | `image` | seamless or centered surface art, runtime display/source <= `1024*1024` | +| billboard sprite | `image` | one centered subject, transparent background/removal allowed | +| circular floor patch | `image` | top-down circular patch, transparent outside edge | + +- Asset Registry `displaySize` remains GDD/runtime metadata; never pass it as + an unsupported MCP parameter. Source textures and declared display size must + not exceed `1024*1024`. +- Keep every 3D image in the normal Asset Registry/display list and generated + `asset-pack.json`; key and URL consistency rules below are unchanged. +- `colormap` is allowed only for matte/non-glossy art. Never colormap glossy, + translucent, emissive, metallic, or skybox art. +- Allowed runtime consumers are `Texture`, `MeshStandardMaterial.map`, scene + background, and `SpriteMaterial`. Never request GLB/FBX/OBJ, normal maps, + model generation, or a text-to-3D API. + **CRITICAL — Parameter restrictions:** - `type: "image"` accepts ONLY `key` and `description`. **Do NOT pass `size`, `resolution`, or any other parameter** — the output is always 386\*560 PNG. Game code scales the image via `setScale()` or `setDisplaySize()`. Icons, projectiles, and small sprites all use the same output size; scale in code. diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md new file mode 100644 index 000000000..d8cc4e95d --- /dev/null +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -0,0 +1,50 @@ +# threed_basic Design Rules + +## 1. Product shape + +Build one short, finishable 3D route: the player moves through a single world, +collects every required item, and reaches one completion state. Use three.js, +simple geometry, lights, fog, a sky texture, and DOM overlays. + +Do not add physics, touch controls, multiplayer, imported 3D models, or any +text-to-3D service. 2D Phaser templates are unrelated and must remain unchanged. + +## 2. Required runtime contract + +| Area | Required | +|---|---| +| Renderer | `WebGLRenderer`, `PerspectiveCamera`, resize handling, visible non-black frame | +| World | primitives or custom low-poly geometry, ambient + directional light, fog | +| Input | WASD and arrow keys; mouse drag/look; ESC pause | +| HUD | DOM in `#ui-root`; canvas stays dedicated to three.js | +| Pause | resolve `gameSceneKey ?? currentLevelKey ?? LevelManager.getFirstLevelScene()` | +| Win | one explicit, reachable completion condition | + +## 3. Asset registry rules + +3D assets are still ordinary images produced by `generate_game_assets`. + +| Key role | Tool type | Max/source rule | three.js use | +|---|---|---|---| +| `skybox_texture` | `background` | `1024*1024`; `displaySize: 1024*1024` in GDD | equirectangular scene background | +| `floor_patch` | `image` | generated image; declare display size <= `1024*1024` | `CircleGeometry` material map | +| `energy_billboard` | `image` | one centered subject, transparent removal allowed | `SpriteMaterial` | +| surface texture | `image` | <= `1024*1024`; no glossy colormap | `MeshStandardMaterial.map` | + +Never request a model, mesh, GLB, FBX, normal map, or text-to-3D output. Do not +use `colormap` for glossy, translucent, emissive, or sky assets. + +## 4. Level and camera budget + +Use 8-14 floor patches, 5-10 collectibles, and 8-16 low-poly decorations. +Keep the camera far plane under 250 and cap device pixel ratio at 2. Manual +distance checks are enough for pickups; do not introduce a physics dependency. + +## 5. GDD completion notes + +The GDD must end with: + +- the actual generated keys and their runtime consumers; +- any placeholder/fallback used; +- `3D scope: primitives + generated image textures; no model generation`; +- the command evidence from build and smoke. diff --git a/agent-test/docs/modules/threed_basic/template_api.md b/agent-test/docs/modules/threed_basic/template_api.md new file mode 100644 index 000000000..2ba793f83 --- /dev/null +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -0,0 +1,65 @@ +# threed_basic Template API + +## Scaffolded files + +| File | Contract | +|---|---| +| `src/main.ts` | boots title, runtime, DOM HUD, pause, completion, render loop | +| `src/GameScene.ts` | owns renderer, scene, camera, world, manual pickup checks | +| `src/InputController.ts` | keyboard + mouse state only | +| `src/SceneMap.ts` | Editor-facing declarative positions via `initSceneMap()` | +| `src/ThreeSceneDefaults.ts` | shared light, fog, and background defaults | +| `src/gameConfig.json` | wrapped `{ value, type, description }` tuning fields | + +## `GameScene` constructor + +```ts +new GameScene(container, { + onProgress(collected, total) {}, + onComplete() {}, + onGameOver() {}, +}, preloader.textures); +``` + +Required public methods: + +| Method | Meaning | +|---|---| +| `update(deltaSeconds)` | advance input, collection, animation, and render | +| `setPaused(boolean)` | stop/resume simulation and clear held input | +| `isPaused()` | smoke/lifecycle observable pause state | +| `resize()` | update camera aspect and renderer size | + +## SceneMap + +`initSceneMap()` returns `floorPatches`, `collectibles`, and `obstacles`. +Change positions there instead of hard-coding level coordinates inside the +render loop. Add new declarative arrays at the `// EXT` point only when a real +consumer is implemented. + +## Texture keys + +`Preloader` reads Phaser-compatible `asset-pack.json` sections and loads image +entries into `Map`. The reference module recognizes: + +| Key | Fallback | +|---|---| +| `skybox_texture` | dark blue `Color` background | +| `floor_patch` | rough blue material | +| `energy_billboard` | emissive sphere primitive | + +Missing optional textures must not throw or block the game. + +## Pause data contract + +Both `UIScene.init()` and `PauseUIScene.init()` accept: + +```ts +{ gameSceneKey?: string; currentLevelKey?: string } +``` + +Resolve in this exact order: + +```ts +data.gameSceneKey ?? data.currentLevelKey ?? LevelManager.getFirstLevelScene() +``` diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md new file mode 100644 index 000000000..0826130eb --- /dev/null +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -0,0 +1,56 @@ +# threed_basic Implementation Manual + +Read this file and every scaffolded `src/` file before editing the generated +game. Keep the reference lifecycle intact; customize data, materials, text, +and the one level rather than replacing the shell. + +## Phase 5 implementation order + +| Order | Action | Done when | +|---|---|---| +| 1 | map GDD asset keys to `skybox_texture`, `floor_patch`, `energy_billboard` | every used key exists in `asset-pack.json` | +| 2 | edit `SceneMap.ts` | main route and every pickup are reachable | +| 3 | merge tuning into `gameConfig.json` | wrapper shape and core fields remain | +| 4 | theme materials and DOM text | canvas remains WebGL-only; HUD remains DOM-only | +| 5 | run build and smoke | zero errors, non-black canvas, WebGL context, ESC resume | + +## Runtime lifecycle + +```text +Preloader.load -> TitleScreen.show -> GameScene constructor + -> applyThreeSceneDefaults -> initSceneMap -> HUD.show + -> requestAnimationFrame -> GameScene.update -> renderer.render + -> all collectibles removed -> onComplete -> GameCompleteUIScene +``` + +`deltaSeconds` is capped by `main.ts`; all movement must multiply by it. +`setPaused(true)` must clear input so a key held before pause cannot continue +moving after resume. + +## Asset hookup + +Only call `generate_game_assets`. A skybox is a generated 2D equirectangular +image, floor art is a generated 2D patch/texture, and an energy marker is a +generated billboard sprite. Do not call shell image tools or any 3D model API. + +If a texture is unavailable, keep the supplied primitive fallback and append +the fallback key to the GDD Asset Degradation Log. The fallback makes the game +playable; it does not authorize skipping the required asset call. + +## Manual play check + +1. Press Enter on the title screen. +2. Move with W/A/S/D or arrow keys; drag the mouse to look. +3. Press ESC, confirm the pause overlay, then ESC again and confirm movement. +4. Follow the single route and collect all energy markers. +5. Confirm `TRAIL COMPLETE`, then verify restart. + +## Frequent failures + +| Symptom | Root cause | Fix | +|---|---|---| +| black canvas, no console error | level never rendered after title | keep the RAF loop and call `renderer.render` every active frame | +| ESC overlay opens but game stays paused | wrong scene key | use the three-key fallback contract exactly | +| image 404s | invented key or leading slash mismatch | read the generated `asset-pack.json`; use its key/url | +| movement depends on frame rate | raw per-frame displacement | multiply by capped `deltaSeconds` | +| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | diff --git a/agent-test/templates/core3d/.gitignore b/agent-test/templates/core3d/.gitignore new file mode 100644 index 000000000..8a55dd1f1 --- /dev/null +++ b/agent-test/templates/core3d/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +shots diff --git a/agent-test/templates/core3d/README.md b/agent-test/templates/core3d/README.md new file mode 100644 index 000000000..a1e970e34 --- /dev/null +++ b/agent-test/templates/core3d/README.md @@ -0,0 +1,10 @@ +# OpenGame core3d template + +Minimal three.js + TypeScript + Vite shell. It preserves the existing DOM +screen contract while keeping 3D rendering isolated from every Phaser template. + +```bash +npm ci +npm run build +npm run dev +``` diff --git a/agent-test/templates/core3d/index.html b/agent-test/templates/core3d/index.html new file mode 100644 index 000000000..432b6fdbf --- /dev/null +++ b/agent-test/templates/core3d/index.html @@ -0,0 +1,13 @@ + + + + + + OpenGame 3D + + +
+
+ + + diff --git a/agent-test/templates/core3d/package-lock.json b/agent-test/templates/core3d/package-lock.json new file mode 100644 index 000000000..0858ef515 --- /dev/null +++ b/agent-test/templates/core3d/package-lock.json @@ -0,0 +1,2361 @@ +{ + "name": "opengame-core3d-template", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opengame-core3d-template", + "version": "0.0.0", + "dependencies": { + "three": "0.179.1" + }, + "devDependencies": { + "@types/three": "0.179.0", + "autoprefixer": "10.4.21", + "postcss": "8.5.25", + "tailwindcss": "3.4.18", + "typescript": "5.8.3", + "vite": "6.4.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "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/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.179.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.179.0.tgz", + "integrity": "sha512-VgbFG2Pgsm84BqdegZzr7w2aKbQxmgzIu4Dy7/75ygiD/0P68LKmp5ie08KMPNqGTQwIge8s6D1guZf1RnZE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.71", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz", + "integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.400", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", + "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "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/node-releases": { + "version": "2.0.52", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", + "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "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": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/three": { + "version": "0.179.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.179.1.tgz", + "integrity": "sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw==", + "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/tinyglobby/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/tinyglobby/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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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_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/vite/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" + } + } + } +} diff --git a/agent-test/templates/core3d/package.json b/agent-test/templates/core3d/package.json new file mode 100644 index 000000000..7fbaf2b08 --- /dev/null +++ b/agent-test/templates/core3d/package.json @@ -0,0 +1,23 @@ +{ + "name": "opengame-core3d-template", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "typecheck": "tsc --noEmit", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "three": "0.179.1" + }, + "devDependencies": { + "@types/three": "0.179.0", + "autoprefixer": "10.4.21", + "postcss": "8.5.25", + "tailwindcss": "3.4.18", + "typescript": "5.8.3", + "vite": "6.4.3" + } +} diff --git a/agent-test/templates/core3d/postcss.config.js b/agent-test/templates/core3d/postcss.config.js new file mode 100644 index 000000000..e008c9cee --- /dev/null +++ b/agent-test/templates/core3d/postcss.config.js @@ -0,0 +1,3 @@ +export default { + plugins: { tailwindcss: {}, autoprefixer: {} }, +}; diff --git a/agent-test/templates/core3d/src/GameScene.ts b/agent-test/templates/core3d/src/GameScene.ts new file mode 100644 index 000000000..120000b7a --- /dev/null +++ b/agent-test/templates/core3d/src/GameScene.ts @@ -0,0 +1,144 @@ +import { + AmbientLight, + BoxGeometry, + Color, + DirectionalLight, + Fog, + Mesh, + MeshStandardMaterial, + PerspectiveCamera, + PlaneGeometry, + Scene, + Texture, + WebGLRenderer, +} from 'three'; +import gameConfig from './gameConfig.json'; + +export type GameSceneEvents = { + onProgress: (collected: number, total: number) => void; + onComplete: () => void; + onGameOver: () => void; +}; + +export class GameScene { + readonly renderer: WebGLRenderer; + private readonly scene = new Scene(); + private readonly camera = new PerspectiveCamera(65, 1, 0.1, 200); + private readonly keys = new Set(); + private paused = false; + private yaw = 0; + private dragging = false; + + constructor( + private readonly container: HTMLElement, + private readonly events: GameSceneEvents, + protected readonly textures: Map = new Map(), + ) { + this.renderer = new WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); + this.renderer.setPixelRatio( + Math.min(devicePixelRatio, gameConfig.renderConfig.pixelRatioCap.value), + ); + this.renderer.setSize(container.clientWidth, container.clientHeight); + container.replaceChildren(this.renderer.domElement); + this.buildWorld(); + this.bindInputs(); + this.resize(); + this.events.onProgress(0, 0); + } + + private buildWorld(): void { + this.scene.background = new Color(0x071426); + this.scene.fog = new Fog( + 0x071426, + gameConfig.renderConfig.fogNear.value, + gameConfig.renderConfig.fogFar.value, + ); + this.scene.add(new AmbientLight(0x9ccfff, 1.5)); + const sun = new DirectionalLight(0xffffff, 3); + sun.position.set(8, 14, 6); + this.scene.add(sun); + + const floor = new Mesh( + new PlaneGeometry(18, 90), + new MeshStandardMaterial({ color: 0x123765, roughness: 0.8 }), + ); + floor.rotation.x = -Math.PI / 2; + floor.position.z = -32; + this.scene.add(floor); + + const geometry = new BoxGeometry(1.5, 1.5, 1.5); + for (let i = 0; i < 14; i++) { + const marker = new Mesh( + geometry, + new MeshStandardMaterial({ color: i % 2 ? 0x22d3ee : 0xa855f7 }), + ); + marker.position.set(i % 2 ? 5 : -5, 1, -i * 6); + this.scene.add(marker); + } + this.camera.position.set(0, 2.2, 7); + } + + private bindInputs(): void { + window.addEventListener('keydown', this.onKeyDown); + window.addEventListener('keyup', this.onKeyUp); + this.renderer.domElement.addEventListener('pointerdown', this.onPointerDown); + window.addEventListener('pointerup', this.onPointerUp); + window.addEventListener('pointermove', this.onPointerMove); + } + + private readonly onKeyDown = (event: KeyboardEvent): void => { + this.keys.add(event.code); + }; + + private readonly onKeyUp = (event: KeyboardEvent): void => { + this.keys.delete(event.code); + }; + + private readonly onPointerDown = (): void => { + this.dragging = true; + }; + + private readonly onPointerUp = (): void => { + this.dragging = false; + }; + + private readonly onPointerMove = (event: PointerEvent): void => { + if (this.dragging) this.yaw -= event.movementX * gameConfig.playerConfig.mouseSensitivity.value; + }; + + update(deltaSeconds: number): void { + if (this.paused) return; + const forward = + Number(this.keys.has('KeyW') || this.keys.has('ArrowUp')) - + Number(this.keys.has('KeyS') || this.keys.has('ArrowDown')); + const strafe = + Number(this.keys.has('KeyD') || this.keys.has('ArrowRight')) - + Number(this.keys.has('KeyA') || this.keys.has('ArrowLeft')); + const speed = gameConfig.playerConfig.moveSpeed.value * deltaSeconds; + this.camera.position.x += + (Math.cos(this.yaw) * strafe + Math.sin(this.yaw) * forward) * speed; + this.camera.position.z += + (Math.sin(this.yaw) * strafe - Math.cos(this.yaw) * forward) * speed; + this.camera.position.x = Math.max(-8, Math.min(8, this.camera.position.x)); + this.camera.position.z = Math.max(-78, Math.min(8, this.camera.position.z)); + this.camera.rotation.y = this.yaw; + this.renderer.render(this.scene, this.camera); + } + + setPaused(paused: boolean): void { + this.paused = paused; + this.keys.clear(); + } + + isPaused(): boolean { + return this.paused; + } + + resize(): void { + const width = Math.max(1, this.container.clientWidth); + const height = Math.max(1, this.container.clientHeight); + this.camera.aspect = width / height; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(width, height); + } +} diff --git a/agent-test/templates/core3d/src/LevelManager.ts b/agent-test/templates/core3d/src/LevelManager.ts new file mode 100644 index 000000000..e35bb131c --- /dev/null +++ b/agent-test/templates/core3d/src/LevelManager.ts @@ -0,0 +1,7 @@ +export class LevelManager { + static readonly LEVEL_ORDER = ['GameScene']; + + static getFirstLevelScene(): string { + return LevelManager.LEVEL_ORDER[0]; + } +} diff --git a/agent-test/templates/core3d/src/gameConfig.json b/agent-test/templates/core3d/src/gameConfig.json new file mode 100644 index 000000000..f23a8fad7 --- /dev/null +++ b/agent-test/templates/core3d/src/gameConfig.json @@ -0,0 +1,15 @@ +{ + "screenSize": { + "width": { "value": 1152, "type": "number", "description": "Reference width" }, + "height": { "value": 768, "type": "number", "description": "Reference height" } + }, + "renderConfig": { + "pixelRatioCap": { "value": 2, "type": "number", "description": "Renderer pixel ratio cap" }, + "fogNear": { "value": 18, "type": "number", "description": "Fog near distance" }, + "fogFar": { "value": 85, "type": "number", "description": "Fog far distance" } + }, + "playerConfig": { + "moveSpeed": { "value": 8, "type": "number", "description": "Movement units per second" }, + "mouseSensitivity": { "value": 0.002, "type": "number", "description": "Mouse look sensitivity" } + } +} diff --git a/agent-test/templates/core3d/src/main.ts b/agent-test/templates/core3d/src/main.ts new file mode 100644 index 000000000..2067f95be --- /dev/null +++ b/agent-test/templates/core3d/src/main.ts @@ -0,0 +1,88 @@ +import './styles/tailwind.css'; +import { GameScene } from './GameScene'; +import { Preloader } from './scenes/Preloader'; +import { TitleScreen } from './scenes/TitleScreen'; +import { UIScene } from './scenes/UIScene'; +import { PauseUIScene } from './scenes/PauseUIScene'; +import { GameCompleteUIScene } from './scenes/GameCompleteUIScene'; +import { GameOverUIScene } from './scenes/GameOverUIScene'; + +declare global { + interface Window { + __opengame3d?: { isPaused: () => boolean; renderer: string }; + } +} + +class GameApp { + private readonly container = document.getElementById('game-container'); + private readonly preloader = new Preloader(); + private readonly title = new TitleScreen(); + private readonly ui = new UIScene(); + private readonly pauseUi = new PauseUIScene(); + private readonly completeUi = new GameCompleteUIScene(); + private readonly gameOverUi = new GameOverUIScene(); + private game?: GameScene; + private lastFrame = performance.now(); + + async boot(): Promise { + if (!this.container) throw new Error('Missing #game-container'); + await this.preloader.load(); + this.title.show(() => this.start()); + window.addEventListener('resize', () => this.game?.resize()); + window.addEventListener('keydown', (event) => { + if (event.code === 'Escape' && this.game) { + if (this.game.isPaused()) this.resume('GameScene'); + else this.pause('GameScene'); + } + }); + requestAnimationFrame(this.frame); + } + + private start(): void { + if (!this.container) return; + this.game = new GameScene( + this.container, + { + onProgress: (collected, total) => this.ui.update(collected, total), + onComplete: () => { + this.game?.setPaused(true); + this.completeUi.show(() => location.reload()); + }, + onGameOver: () => { + this.game?.setPaused(true); + this.gameOverUi.show(() => location.reload()); + }, + }, + this.preloader.textures, + ); + this.ui.init({ gameSceneKey: 'GameScene' }); + this.ui.show((key) => this.pause(key)); + window.__opengame3d = { + isPaused: () => this.game?.isPaused() ?? false, + renderer: 'three.js', + }; + } + + private pause(sceneKey: string): void { + if (sceneKey !== 'GameScene' || !this.game || this.game.isPaused()) return; + this.game.setPaused(true); + this.pauseUi.init({ gameSceneKey: sceneKey }); + this.pauseUi.show((key) => this.resume(key)); + } + + private resume(sceneKey: string): void { + if (sceneKey === 'GameScene') { + this.pauseUi.hide(); + this.game?.setPaused(false); + } + } + + private readonly frame = (time: number): void => { + const delta = Math.min(0.05, (time - this.lastFrame) / 1000); + this.lastFrame = time; + this.game?.update(delta); + requestAnimationFrame(this.frame); + }; +} + +new GameApp().boot(); diff --git a/agent-test/templates/core3d/src/scenes/GameCompleteUIScene.ts b/agent-test/templates/core3d/src/scenes/GameCompleteUIScene.ts new file mode 100644 index 000000000..e58889337 --- /dev/null +++ b/agent-test/templates/core3d/src/scenes/GameCompleteUIScene.ts @@ -0,0 +1,10 @@ +export class GameCompleteUIScene { + show(onRestart: () => void): void { + const root = document.getElementById('ui-root'); + if (!root) return; + root.innerHTML = `

TRAIL COMPLETE

All energy recovered.

`; + root.querySelector('#complete-btn')?.addEventListener('click', onRestart, { + once: true, + }); + } +} diff --git a/agent-test/templates/core3d/src/scenes/GameOverUIScene.ts b/agent-test/templates/core3d/src/scenes/GameOverUIScene.ts new file mode 100644 index 000000000..222b818c0 --- /dev/null +++ b/agent-test/templates/core3d/src/scenes/GameOverUIScene.ts @@ -0,0 +1,10 @@ +export class GameOverUIScene { + show(onRestart: () => void): void { + const root = document.getElementById('ui-root'); + if (!root) return; + root.innerHTML = `

GAME OVER

`; + root.querySelector('#retry-btn')?.addEventListener('click', onRestart, { + once: true, + }); + } +} diff --git a/agent-test/templates/core3d/src/scenes/PauseUIScene.ts b/agent-test/templates/core3d/src/scenes/PauseUIScene.ts new file mode 100644 index 000000000..a26e94dff --- /dev/null +++ b/agent-test/templates/core3d/src/scenes/PauseUIScene.ts @@ -0,0 +1,44 @@ +import { LevelManager } from '../LevelManager'; +import type { SceneKeyData } from './UIScene'; + +export class PauseUIScene { + private sceneKey = LevelManager.getFirstLevelScene(); + private keyHandler?: (event: KeyboardEvent) => void; + + init(data: SceneKeyData = {}): void { + this.sceneKey = + data.gameSceneKey ?? + data.currentLevelKey ?? + LevelManager.getFirstLevelScene(); + } + + show(onResume: (sceneKey: string) => void): void { + const root = document.getElementById('ui-root'); + if (!root) return; + root.insertAdjacentHTML( + 'beforeend', + `
+

GAME PAUSED

+
`, + ); + const resume = () => { + this.hide(); + onResume(this.sceneKey); + }; + root.querySelector('#resume-btn')?.addEventListener('click', resume, { + once: true, + }); + this.keyHandler = (event) => { + if (event.code === 'Space' || event.code === 'Enter') { + event.stopPropagation(); + resume(); + } + }; + document.addEventListener('keydown', this.keyHandler); + } + + hide(): void { + if (this.keyHandler) document.removeEventListener('keydown', this.keyHandler); + document.getElementById('pause-overlay')?.remove(); + } +} diff --git a/agent-test/templates/core3d/src/scenes/Preloader.ts b/agent-test/templates/core3d/src/scenes/Preloader.ts new file mode 100644 index 000000000..5c283ec02 --- /dev/null +++ b/agent-test/templates/core3d/src/scenes/Preloader.ts @@ -0,0 +1,48 @@ +import { LoadingManager, Texture, TextureLoader } from 'three'; + +export class Preloader { + readonly textures = new Map(); + + async load(entries?: Record): Promise { + const assetEntries = entries ?? (await this.readAssetPack()); + const manager = new LoadingManager(); + const loader = new TextureLoader(manager); + const loads = Object.entries(assetEntries).map( + ([key, url]) => + new Promise((resolve) => { + loader.load( + url, + (texture) => { + this.textures.set(key, texture); + resolve(); + }, + undefined, + () => resolve(), + ); + }), + ); + await Promise.all(loads); + } + + private async readAssetPack(): Promise> { + try { + const response = await fetch('assets/asset-pack.json'); + if (!response.ok) return {}; + const pack = (await response.json()) as Record< + string, + { files?: Array<{ type?: string; key?: string; url?: string }> } + >; + const entries: Record = {}; + for (const section of Object.values(pack)) { + for (const file of section.files ?? []) { + if (file.type === 'image' && file.key && file.url) { + entries[file.key] = file.url; + } + } + } + return entries; + } catch { + return {}; + } + } +} diff --git a/agent-test/templates/core3d/src/scenes/TitleScreen.ts b/agent-test/templates/core3d/src/scenes/TitleScreen.ts new file mode 100644 index 000000000..47e46e008 --- /dev/null +++ b/agent-test/templates/core3d/src/scenes/TitleScreen.ts @@ -0,0 +1,33 @@ +export class TitleScreen { + private keyHandler?: (event: KeyboardEvent) => void; + + show(onStart: () => void): void { + const root = document.getElementById('ui-root'); + if (!root) return; + root.innerHTML = ` +
+
+

OPENGAME 3D

+

PRISM TRAIL

+ +
+
`; + const start = () => { + this.hide(); + onStart(); + }; + root.querySelector('#start-btn')?.addEventListener('click', start, { + once: true, + }); + this.keyHandler = (event) => { + if (event.code === 'Enter' || event.code === 'Space') start(); + }; + document.addEventListener('keydown', this.keyHandler); + } + + hide(): void { + if (this.keyHandler) document.removeEventListener('keydown', this.keyHandler); + const root = document.getElementById('ui-root'); + if (root) root.innerHTML = ''; + } +} diff --git a/agent-test/templates/core3d/src/scenes/UIScene.ts b/agent-test/templates/core3d/src/scenes/UIScene.ts new file mode 100644 index 000000000..ea7571e53 --- /dev/null +++ b/agent-test/templates/core3d/src/scenes/UIScene.ts @@ -0,0 +1,39 @@ +import { LevelManager } from '../LevelManager'; + +export type SceneKeyData = { + gameSceneKey?: string; + currentLevelKey?: string; +}; + +export class UIScene { + private sceneKey = LevelManager.getFirstLevelScene(); + + init(data: SceneKeyData = {}): void { + this.sceneKey = + data.gameSceneKey ?? + data.currentLevelKey ?? + LevelManager.getFirstLevelScene(); + } + + show(onPause: (sceneKey: string) => void): void { + const root = document.getElementById('ui-root'); + if (!root) return; + root.innerHTML = ` +
+
+
ENERGY
+
0 / 0
+
+ +
WASD / ARROWS: MOVE · DRAG: LOOK · ESC: PAUSE
+
`; + root.querySelector('#pause-btn')?.addEventListener('click', () => + onPause(this.sceneKey), + ); + } + + update(collected: number, total: number): void { + const score = document.getElementById('score-text'); + if (score) score.textContent = `${collected} / ${total}`; + } +} diff --git a/agent-test/templates/core3d/src/styles/tailwind.css b/agent-test/templates/core3d/src/styles/tailwind.css new file mode 100644 index 000000000..0c697a00e --- /dev/null +++ b/agent-test/templates/core3d/src/styles/tailwind.css @@ -0,0 +1,19 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +#game-container { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + background: #030712; +} + +canvas { + display: block; + width: 100%; + height: 100%; +} diff --git a/agent-test/templates/core3d/tailwind.config.js b/agent-test/templates/core3d/tailwind.config.js new file mode 100644 index 000000000..4a6eff983 --- /dev/null +++ b/agent-test/templates/core3d/tailwind.config.js @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{ts,html}'], + theme: { + extend: { + fontFamily: { retro: ['ui-monospace', 'monospace'] }, + }, + }, + plugins: [], +}; diff --git a/agent-test/templates/core3d/tsconfig.json b/agent-test/templates/core3d/tsconfig.json new file mode 100644 index 000000000..1d114120b --- /dev/null +++ b/agent-test/templates/core3d/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/agent-test/templates/core3d/vite.config.js b/agent-test/templates/core3d/vite.config.js new file mode 100644 index 000000000..fcfef3886 --- /dev/null +++ b/agent-test/templates/core3d/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + server: { host: '127.0.0.1' }, +}); diff --git a/agent-test/templates/modules/threed_basic/src/GameScene.ts b/agent-test/templates/modules/threed_basic/src/GameScene.ts new file mode 100644 index 000000000..c9878650e --- /dev/null +++ b/agent-test/templates/modules/threed_basic/src/GameScene.ts @@ -0,0 +1,173 @@ +import { + CircleGeometry, + Color, + EquirectangularReflectionMapping, + IcosahedronGeometry, + Mesh, + MeshStandardMaterial, + Object3D, + PerspectiveCamera, + Scene, + SphereGeometry, + Sprite, + SpriteMaterial, + SRGBColorSpace, + Texture, + Vector3, + WebGLRenderer, +} from 'three'; +import gameConfig from './gameConfig.json'; +import { InputController } from './InputController'; +import { initSceneMap } from './SceneMap'; +import { applyThreeSceneDefaults } from './ThreeSceneDefaults'; + +export type GameSceneEvents = { + onProgress: (collected: number, total: number) => void; + onComplete: () => void; + onGameOver: () => void; +}; + +export class GameScene { + readonly renderer: WebGLRenderer; + private readonly scene = new Scene(); + private readonly camera = new PerspectiveCamera(65, 1, 0.1, 200); + private readonly input: InputController; + private readonly collectibles: Object3D[] = []; + private paused = false; + private completed = false; + private collected = 0; + private yaw = 0; + + constructor( + private readonly container: HTMLElement, + private readonly events: GameSceneEvents, + private readonly textures: Map = new Map(), + ) { + this.renderer = new WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); + this.renderer.outputColorSpace = SRGBColorSpace; + this.renderer.setPixelRatio( + Math.min(devicePixelRatio, gameConfig.renderConfig.pixelRatioCap.value), + ); + this.renderer.setSize(container.clientWidth, container.clientHeight); + container.replaceChildren(this.renderer.domElement); + this.input = new InputController(this.renderer.domElement); + this.buildWorld(); + this.resize(); + this.events.onProgress(0, this.collectibles.length); + } + + private buildWorld(): void { + applyThreeSceneDefaults(this.scene); + const skybox = this.textures.get('skybox_texture'); + if (skybox) { + skybox.mapping = EquirectangularReflectionMapping; + skybox.colorSpace = SRGBColorSpace; + this.scene.background = skybox; + } + + const map = initSceneMap(); + const floorTexture = this.textures.get('floor_patch'); + if (floorTexture) floorTexture.colorSpace = SRGBColorSpace; + const floorMaterial = new MeshStandardMaterial({ + color: floorTexture ? 0xffffff : 0x123c66, + map: floorTexture, + roughness: 0.92, + }); + for (const patch of map.floorPatches) { + const floor = new Mesh( + new CircleGeometry(patch.radius, 20), + floorMaterial, + ); + floor.rotation.x = -Math.PI / 2; + floor.position.set(patch.x, 0, patch.z); + this.scene.add(floor); + } + + const obstacleMaterial = new MeshStandardMaterial({ + color: 0x7c3aed, + emissive: new Color(0x21084f), + flatShading: true, + }); + for (const obstacle of map.obstacles) { + const mesh = new Mesh(new IcosahedronGeometry(obstacle.scale, 0), obstacleMaterial); + mesh.position.set(obstacle.x, obstacle.y, obstacle.z); + this.scene.add(mesh); + } + + const energyTexture = this.textures.get('energy_billboard'); + if (energyTexture) energyTexture.colorSpace = SRGBColorSpace; + for (const item of map.collectibles) { + const collectible = energyTexture + ? new Sprite(new SpriteMaterial({ map: energyTexture, transparent: true })) + : new Mesh( + new SphereGeometry(0.55, 12, 8), + new MeshStandardMaterial({ color: 0x67e8f9, emissive: 0x155e75 }), + ); + collectible.name = item.id; + collectible.position.set(item.x, item.y, item.z); + collectible.scale.setScalar(energyTexture ? 1.8 : 1); + this.collectibles.push(collectible); + this.scene.add(collectible); + } + this.camera.position.set(0, 2.2, 7); + } + + update(deltaSeconds: number): void { + if (this.paused || this.completed) return; + this.yaw -= + this.input.consumeLookDelta() * gameConfig.playerConfig.mouseSensitivity.value; + const { forward, strafe } = this.input.movement(); + const speed = gameConfig.playerConfig.moveSpeed.value * deltaSeconds; + this.camera.position.x += + (Math.cos(this.yaw) * strafe + Math.sin(this.yaw) * forward) * speed; + this.camera.position.z += + (Math.sin(this.yaw) * strafe - Math.cos(this.yaw) * forward) * speed; + this.camera.position.x = Math.max( + -gameConfig.levelConfig.trackHalfWidth.value, + Math.min(gameConfig.levelConfig.trackHalfWidth.value, this.camera.position.x), + ); + this.camera.position.z = Math.max( + gameConfig.levelConfig.finishZ.value, + Math.min(8, this.camera.position.z), + ); + this.camera.rotation.y = this.yaw; + + for (const collectible of [...this.collectibles]) { + collectible.rotation.y += deltaSeconds * 2; + if ( + collectible.position.distanceTo(this.camera.position) <= + gameConfig.levelConfig.collectRadius.value + ) { + this.scene.remove(collectible); + this.collectibles.splice(this.collectibles.indexOf(collectible), 1); + this.collected++; + this.events.onProgress(this.collected, this.collected + this.collectibles.length); + } + } + if (this.collectibles.length === 0) { + this.completed = true; + this.events.onComplete(); + } + this.renderer.render(this.scene, this.camera); + } + + setPaused(paused: boolean): void { + this.paused = paused; + this.input.clear(); + } + + isPaused(): boolean { + return this.paused; + } + + resize(): void { + const size = new Vector3( + Math.max(1, this.container.clientWidth), + Math.max(1, this.container.clientHeight), + 0, + ); + this.camera.aspect = size.x / size.y; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(size.x, size.y); + } +} diff --git a/agent-test/templates/modules/threed_basic/src/InputController.ts b/agent-test/templates/modules/threed_basic/src/InputController.ts new file mode 100644 index 000000000..97b237519 --- /dev/null +++ b/agent-test/templates/modules/threed_basic/src/InputController.ts @@ -0,0 +1,55 @@ +export class InputController { + private readonly keys = new Set(); + private dragging = false; + private lookDelta = 0; + + constructor(canvas: HTMLCanvasElement) { + window.addEventListener('keydown', this.onKeyDown); + window.addEventListener('keyup', this.onKeyUp); + canvas.addEventListener('mousedown', this.onMouseDown); + window.addEventListener('mouseup', this.onMouseUp); + window.addEventListener('mousemove', this.onMouseMove); + } + + movement(): { forward: number; strafe: number } { + return { + forward: + Number(this.keys.has('KeyW') || this.keys.has('ArrowUp')) - + Number(this.keys.has('KeyS') || this.keys.has('ArrowDown')), + strafe: + Number(this.keys.has('KeyD') || this.keys.has('ArrowRight')) - + Number(this.keys.has('KeyA') || this.keys.has('ArrowLeft')), + }; + } + + consumeLookDelta(): number { + const delta = this.lookDelta; + this.lookDelta = 0; + return delta; + } + + clear(): void { + this.keys.clear(); + this.lookDelta = 0; + } + + private readonly onKeyDown = (event: KeyboardEvent): void => { + this.keys.add(event.code); + }; + + private readonly onKeyUp = (event: KeyboardEvent): void => { + this.keys.delete(event.code); + }; + + private readonly onMouseDown = (): void => { + this.dragging = true; + }; + + private readonly onMouseUp = (): void => { + this.dragging = false; + }; + + private readonly onMouseMove = (event: MouseEvent): void => { + if (this.dragging) this.lookDelta += event.movementX; + }; +} diff --git a/agent-test/templates/modules/threed_basic/src/SceneMap.ts b/agent-test/templates/modules/threed_basic/src/SceneMap.ts new file mode 100644 index 000000000..edbafd736 --- /dev/null +++ b/agent-test/templates/modules/threed_basic/src/SceneMap.ts @@ -0,0 +1,29 @@ +export type SceneMap = { + floorPatches: Array<{ x: number; z: number; radius: number }>; + collectibles: Array<{ id: string; x: number; y: number; z: number }>; + obstacles: Array<{ x: number; y: number; z: number; scale: number }>; +}; + +/** Editor-facing data initialization; keep positions declarative and code-free. */ +export function initSceneMap(): SceneMap { + return { + // EXT: append authored path patches without changing GameScene. + floorPatches: Array.from({ length: 14 }, (_, i) => ({ + x: Math.sin(i * 0.8) * 2.2, + z: 2 - i * 6, + radius: 5.2, + })), + collectibles: Array.from({ length: 8 }, (_, i) => ({ + id: `energy-${i + 1}`, + x: Math.sin(i * 1.4) * 3.5, + y: 1.35, + z: -5 - i * 9, + })), + obstacles: Array.from({ length: 12 }, (_, i) => ({ + x: i % 2 ? 5.6 : -5.6, + y: 1.2, + z: -i * 6, + scale: 0.7 + (i % 3) * 0.25, + })), + }; +} diff --git a/agent-test/templates/modules/threed_basic/src/ThreeSceneDefaults.ts b/agent-test/templates/modules/threed_basic/src/ThreeSceneDefaults.ts new file mode 100644 index 000000000..900b08538 --- /dev/null +++ b/agent-test/templates/modules/threed_basic/src/ThreeSceneDefaults.ts @@ -0,0 +1,15 @@ +import { AmbientLight, Color, DirectionalLight, Fog, Scene } from 'three'; +import gameConfig from './gameConfig.json'; + +export function applyThreeSceneDefaults(scene: Scene): void { + scene.background = new Color(0x060b21); + scene.fog = new Fog( + 0x060b21, + gameConfig.renderConfig.fogNear.value, + gameConfig.renderConfig.fogFar.value, + ); + scene.add(new AmbientLight(0x7dd3fc, 1.6)); + const keyLight = new DirectionalLight(0xffffff, 3.2); + keyLight.position.set(8, 16, 10); + scene.add(keyLight); +} diff --git a/agent-test/templates/modules/threed_basic/src/gameConfig.json b/agent-test/templates/modules/threed_basic/src/gameConfig.json new file mode 100644 index 000000000..1ec3b8a9f --- /dev/null +++ b/agent-test/templates/modules/threed_basic/src/gameConfig.json @@ -0,0 +1,20 @@ +{ + "screenSize": { + "width": { "value": 1152, "type": "number", "description": "Reference width" }, + "height": { "value": 768, "type": "number", "description": "Reference height" } + }, + "renderConfig": { + "pixelRatioCap": { "value": 2, "type": "number", "description": "Renderer pixel ratio cap" }, + "fogNear": { "value": 16, "type": "number", "description": "Fog near distance" }, + "fogFar": { "value": 82, "type": "number", "description": "Fog far distance" } + }, + "playerConfig": { + "moveSpeed": { "value": 9, "type": "number", "description": "Movement units per second" }, + "mouseSensitivity": { "value": 0.002, "type": "number", "description": "Mouse look sensitivity" } + }, + "levelConfig": { + "trackHalfWidth": { "value": 7, "type": "number", "description": "Half-width of the playable track" }, + "finishZ": { "value": -78, "type": "number", "description": "End of the main path" }, + "collectRadius": { "value": 1.8, "type": "number", "description": "Manual pickup radius" } + } +} From a3f5b76fac4ab30612aea8742dd771b884a58435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 17:40:51 +0800 Subject: [PATCH 03/12] feat(templates): add threed_basic manual collision Keep the player on authored floor patches and outside explicit static obstacle circles without introducing a physics dependency. Substepped resolution prevents high-delta tunnelling and the self-check covers road, obstacle, and slide behavior. --- .../threed_basic/src/CollisionResolver.ts | 64 +++++++++++++++++++ .../modules/threed_basic/src/GameScene.ts | 36 ++++++----- .../modules/threed_basic/src/SceneMap.ts | 13 +++- .../modules/threed_basic/src/gameConfig.json | 5 +- .../threed_basic/tests/collision-selfcheck.ts | 39 +++++++++++ 5 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 agent-test/templates/modules/threed_basic/src/CollisionResolver.ts create mode 100644 agent-test/templates/modules/threed_basic/tests/collision-selfcheck.ts diff --git a/agent-test/templates/modules/threed_basic/src/CollisionResolver.ts b/agent-test/templates/modules/threed_basic/src/CollisionResolver.ts new file mode 100644 index 000000000..fe6156b5b --- /dev/null +++ b/agent-test/templates/modules/threed_basic/src/CollisionResolver.ts @@ -0,0 +1,64 @@ +export type XzPoint = { x: number; z: number }; + +export type FloorPatch = XzPoint & { radius: number }; + +export type StaticObstacle = XzPoint & { collisionRadius: number }; + +export type CollisionMap = { + floorPatches: FloorPatch[]; + obstacles: StaticObstacle[]; +}; + +const EPSILON = 1e-6; + +function isPlayable( + point: XzPoint, + playerRadius: number, + map: CollisionMap, +): boolean { + // ponytail: linear scans fit the 8-16 authored object budget; add spatial + // indexing only when measured level budgets grow beyond that ceiling. + const onFloor = map.floorPatches.some( + (patch) => + Math.hypot(point.x - patch.x, point.z - patch.z) + playerRadius <= + patch.radius + EPSILON, + ); + if (!onFloor) return false; + + return map.obstacles.every( + (obstacle) => + Math.hypot(point.x - obstacle.x, point.z - obstacle.z) + EPSILON >= + playerRadius + obstacle.collisionRadius, + ); +} + +export function resolveMovement( + position: XzPoint, + movement: XzPoint, + playerRadius: number, + map: CollisionMap, +): XzPoint { + const distance = Math.hypot(movement.x, movement.z); + if (distance === 0) return { ...position }; + + const stepCount = Math.max( + 1, + Math.ceil(distance / Math.max(0.05, playerRadius * 0.5)), + ); + const step = { x: movement.x / stepCount, z: movement.z / stepCount }; + let current = { ...position }; + + for (let index = 0; index < stepCount; index++) { + const candidates = [ + { x: current.x + step.x, z: current.z + step.z }, + { x: current.x + step.x, z: current.z }, + { x: current.x, z: current.z + step.z }, + ]; + current = + candidates.find((candidate) => + isPlayable(candidate, playerRadius, map), + ) ?? current; + } + + return current; +} diff --git a/agent-test/templates/modules/threed_basic/src/GameScene.ts b/agent-test/templates/modules/threed_basic/src/GameScene.ts index c9878650e..649507b68 100644 --- a/agent-test/templates/modules/threed_basic/src/GameScene.ts +++ b/agent-test/templates/modules/threed_basic/src/GameScene.ts @@ -17,6 +17,7 @@ import { WebGLRenderer, } from 'three'; import gameConfig from './gameConfig.json'; +import { resolveMovement } from './CollisionResolver'; import { InputController } from './InputController'; import { initSceneMap } from './SceneMap'; import { applyThreeSceneDefaults } from './ThreeSceneDefaults'; @@ -32,6 +33,7 @@ export class GameScene { private readonly scene = new Scene(); private readonly camera = new PerspectiveCamera(65, 1, 0.1, 200); private readonly input: InputController; + private readonly map = initSceneMap(); private readonly collectibles: Object3D[] = []; private paused = false; private completed = false; @@ -65,7 +67,6 @@ export class GameScene { this.scene.background = skybox; } - const map = initSceneMap(); const floorTexture = this.textures.get('floor_patch'); if (floorTexture) floorTexture.colorSpace = SRGBColorSpace; const floorMaterial = new MeshStandardMaterial({ @@ -73,7 +74,7 @@ export class GameScene { map: floorTexture, roughness: 0.92, }); - for (const patch of map.floorPatches) { + for (const patch of this.map.floorPatches) { const floor = new Mesh( new CircleGeometry(patch.radius, 20), floorMaterial, @@ -88,7 +89,7 @@ export class GameScene { emissive: new Color(0x21084f), flatShading: true, }); - for (const obstacle of map.obstacles) { + for (const obstacle of this.map.obstacles) { const mesh = new Mesh(new IcosahedronGeometry(obstacle.scale, 0), obstacleMaterial); mesh.position.set(obstacle.x, obstacle.y, obstacle.z); this.scene.add(mesh); @@ -96,7 +97,7 @@ export class GameScene { const energyTexture = this.textures.get('energy_billboard'); if (energyTexture) energyTexture.colorSpace = SRGBColorSpace; - for (const item of map.collectibles) { + for (const item of this.map.collectibles) { const collectible = energyTexture ? new Sprite(new SpriteMaterial({ map: energyTexture, transparent: true })) : new Mesh( @@ -109,7 +110,11 @@ export class GameScene { this.collectibles.push(collectible); this.scene.add(collectible); } - this.camera.position.set(0, 2.2, 7); + this.camera.position.set( + this.map.playerSpawn.x, + 2.2, + this.map.playerSpawn.z, + ); } update(deltaSeconds: number): void { @@ -118,18 +123,17 @@ export class GameScene { this.input.consumeLookDelta() * gameConfig.playerConfig.mouseSensitivity.value; const { forward, strafe } = this.input.movement(); const speed = gameConfig.playerConfig.moveSpeed.value * deltaSeconds; - this.camera.position.x += - (Math.cos(this.yaw) * strafe + Math.sin(this.yaw) * forward) * speed; - this.camera.position.z += - (Math.sin(this.yaw) * strafe - Math.cos(this.yaw) * forward) * speed; - this.camera.position.x = Math.max( - -gameConfig.levelConfig.trackHalfWidth.value, - Math.min(gameConfig.levelConfig.trackHalfWidth.value, this.camera.position.x), - ); - this.camera.position.z = Math.max( - gameConfig.levelConfig.finishZ.value, - Math.min(8, this.camera.position.z), + const nextPosition = resolveMovement( + { x: this.camera.position.x, z: this.camera.position.z }, + { + x: (Math.cos(this.yaw) * strafe + Math.sin(this.yaw) * forward) * speed, + z: (Math.sin(this.yaw) * strafe - Math.cos(this.yaw) * forward) * speed, + }, + gameConfig.playerConfig.collisionRadius.value, + this.map, ); + this.camera.position.x = nextPosition.x; + this.camera.position.z = nextPosition.z; this.camera.rotation.y = this.yaw; for (const collectible of [...this.collectibles]) { diff --git a/agent-test/templates/modules/threed_basic/src/SceneMap.ts b/agent-test/templates/modules/threed_basic/src/SceneMap.ts index edbafd736..c27afb560 100644 --- a/agent-test/templates/modules/threed_basic/src/SceneMap.ts +++ b/agent-test/templates/modules/threed_basic/src/SceneMap.ts @@ -1,12 +1,20 @@ export type SceneMap = { + playerSpawn: { x: number; z: number }; floorPatches: Array<{ x: number; z: number; radius: number }>; collectibles: Array<{ id: string; x: number; y: number; z: number }>; - obstacles: Array<{ x: number; y: number; z: number; scale: number }>; + obstacles: Array<{ + x: number; + y: number; + z: number; + scale: number; + collisionRadius: number; + }>; }; /** Editor-facing data initialization; keep positions declarative and code-free. */ export function initSceneMap(): SceneMap { return { + playerSpawn: { x: 0, z: 6.5 }, // EXT: append authored path patches without changing GameScene. floorPatches: Array.from({ length: 14 }, (_, i) => ({ x: Math.sin(i * 0.8) * 2.2, @@ -20,10 +28,11 @@ export function initSceneMap(): SceneMap { z: -5 - i * 9, })), obstacles: Array.from({ length: 12 }, (_, i) => ({ - x: i % 2 ? 5.6 : -5.6, + x: Math.sin(i * 0.8) * 2.2 + (i % 2 ? 2.2 : -2.2), y: 1.2, z: -i * 6, scale: 0.7 + (i % 3) * 0.25, + collisionRadius: 0.8 + (i % 3) * 0.2, })), }; } diff --git a/agent-test/templates/modules/threed_basic/src/gameConfig.json b/agent-test/templates/modules/threed_basic/src/gameConfig.json index 1ec3b8a9f..3bec72a8f 100644 --- a/agent-test/templates/modules/threed_basic/src/gameConfig.json +++ b/agent-test/templates/modules/threed_basic/src/gameConfig.json @@ -10,11 +10,10 @@ }, "playerConfig": { "moveSpeed": { "value": 9, "type": "number", "description": "Movement units per second" }, - "mouseSensitivity": { "value": 0.002, "type": "number", "description": "Mouse look sensitivity" } + "mouseSensitivity": { "value": 0.002, "type": "number", "description": "Mouse look sensitivity" }, + "collisionRadius": { "value": 0.45, "type": "number", "description": "Player radius for XZ collision" } }, "levelConfig": { - "trackHalfWidth": { "value": 7, "type": "number", "description": "Half-width of the playable track" }, - "finishZ": { "value": -78, "type": "number", "description": "End of the main path" }, "collectRadius": { "value": 1.8, "type": "number", "description": "Manual pickup radius" } } } diff --git a/agent-test/templates/modules/threed_basic/tests/collision-selfcheck.ts b/agent-test/templates/modules/threed_basic/tests/collision-selfcheck.ts new file mode 100644 index 000000000..5c6cab22c --- /dev/null +++ b/agent-test/templates/modules/threed_basic/tests/collision-selfcheck.ts @@ -0,0 +1,39 @@ +import { resolveMovement, type CollisionMap } from '../src/CollisionResolver.js'; + +function assert(condition: boolean, message: string): void { + if (!condition) throw new Error(message); +} + +const openFloor: CollisionMap = { + floorPatches: [{ x: 0, z: 0, radius: 5 }], + obstacles: [], +}; +const roadEdge = resolveMovement( + { x: 0, z: 0 }, + { x: 20, z: 0 }, + 0.5, + openFloor, +); +assert(roadEdge.x <= 4.5 + 1e-6, 'player escaped the authored floor'); + +const blockedFloor: CollisionMap = { + ...openFloor, + obstacles: [{ x: 0, z: 0, collisionRadius: 0.75 }], +}; +const blocked = resolveMovement( + { x: -3, z: 0 }, + { x: 6, z: 0 }, + 0.5, + blockedFloor, +); +assert(blocked.x < -1.2, 'high-delta movement tunneled through an obstacle'); + +const sliding = resolveMovement( + { x: -2, z: -1.5 }, + { x: 2, z: 1 }, + 0.5, + blockedFloor, +); +assert(sliding.z > -1.5, 'unblocked axis did not slide along the obstacle'); + +console.log('threed_basic collision self-check: PASS'); From 3a0b59a7ed55b1bd1791cfbda04f14e2ca5fe311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 17:41:02 +0800 Subject: [PATCH 04/12] docs(templates): define threed collision contract Teach the builder to preserve the pure XZ resolver, explicit collision radii, and v1 no-physics boundary so generated games consume the new capability instead of bypassing it. --- agent-test/docs/modules/threed_basic/design_rules.md | 5 ++++- agent-test/docs/modules/threed_basic/template_api.md | 8 +++++++- agent-test/docs/modules/threed_basic/threed_basic.md | 9 +++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index d8cc4e95d..82a3ed896 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -15,6 +15,7 @@ text-to-3D service. 2D Phaser templates are unrelated and must remain unchanged. |---|---| | Renderer | `WebGLRenderer`, `PerspectiveCamera`, resize handling, visible non-black frame | | World | primitives or custom low-poly geometry, ambient + directional light, fog | +| Collision | player circle stays on an authored floor patch and outside static obstacle circles | | Input | WASD and arrow keys; mouse drag/look; ESC pause | | HUD | DOM in `#ui-root`; canvas stays dedicated to three.js | | Pause | resolve `gameSceneKey ?? currentLevelKey ?? LevelManager.getFirstLevelScene()` | @@ -38,7 +39,9 @@ use `colormap` for glossy, translucent, emissive, or sky assets. Use 8-14 floor patches, 5-10 collectibles, and 8-16 low-poly decorations. Keep the camera far plane under 250 and cap device pixel ratio at 2. Manual -distance checks are enough for pickups; do not introduce a physics dependency. +distance checks are enough for pickups and static collision; do not introduce a +physics dependency. Give every obstacle an explicit `collisionRadius` instead +of deriving gameplay collision from rendered scale. ## 5. GDD completion notes diff --git a/agent-test/docs/modules/threed_basic/template_api.md b/agent-test/docs/modules/threed_basic/template_api.md index 2ba793f83..cbf674c74 100644 --- a/agent-test/docs/modules/threed_basic/template_api.md +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -6,6 +6,7 @@ |---|---| | `src/main.ts` | boots title, runtime, DOM HUD, pause, completion, render loop | | `src/GameScene.ts` | owns renderer, scene, camera, world, manual pickup checks | +| `src/CollisionResolver.ts` | pure XZ road/obstacle movement resolution with substeps | | `src/InputController.ts` | keyboard + mouse state only | | `src/SceneMap.ts` | Editor-facing declarative positions via `initSceneMap()` | | `src/ThreeSceneDefaults.ts` | shared light, fog, and background defaults | @@ -32,11 +33,16 @@ Required public methods: ## SceneMap -`initSceneMap()` returns `floorPatches`, `collectibles`, and `obstacles`. +`initSceneMap()` returns `playerSpawn`, `floorPatches`, `collectibles`, and `obstacles`. Change positions there instead of hard-coding level coordinates inside the render loop. Add new declarative arrays at the `// EXT` point only when a real consumer is implemented. +Each obstacle declares `collisionRadius` independently from visual `scale`. +`resolveMovement()` keeps the full player circle inside at least one floor +patch, subdivides long moves to prevent tunnelling, and slides along an +unblocked axis. Dynamic bodies, impulses, and gravity remain v2 concerns. + ## Texture keys `Preloader` reads Phaser-compatible `asset-pack.json` sections and loads image diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 0826130eb..f9e436747 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -9,7 +9,7 @@ and the one level rather than replacing the shell. | Order | Action | Done when | |---|---|---| | 1 | map GDD asset keys to `skybox_texture`, `floor_patch`, `energy_billboard` | every used key exists in `asset-pack.json` | -| 2 | edit `SceneMap.ts` | main route and every pickup are reachable | +| 2 | edit `SceneMap.ts` | main route and every pickup are reachable; obstacle collision radii leave a traversable lane | | 3 | merge tuning into `gameConfig.json` | wrapper shape and core fields remain | | 4 | theme materials and DOM text | canvas remains WebGL-only; HUD remains DOM-only | | 5 | run build and smoke | zero errors, non-black canvas, WebGL context, ESC resume | @@ -19,7 +19,7 @@ and the one level rather than replacing the shell. ```text Preloader.load -> TitleScreen.show -> GameScene constructor -> applyThreeSceneDefaults -> initSceneMap -> HUD.show - -> requestAnimationFrame -> GameScene.update -> renderer.render + -> requestAnimationFrame -> GameScene.update -> resolveMovement -> renderer.render -> all collectibles removed -> onComplete -> GameCompleteUIScene ``` @@ -27,6 +27,10 @@ Preloader.load -> TitleScreen.show -> GameScene constructor `setPaused(true)` must clear input so a key held before pause cannot continue moving after resume. +Keep `CollisionResolver.ts` pure. Floor patches and obstacle circles come from +`SceneMap.ts`; player radius comes from `gameConfig.json`. Do not replace this +with a physics dependency in v1. + ## Asset hookup Only call `generate_game_assets`. A skybox is a generated 2D equirectangular @@ -53,4 +57,5 @@ playable; it does not authorize skipping the required asset call. | ESC overlay opens but game stays paused | wrong scene key | use the three-key fallback contract exactly | | image 404s | invented key or leading slash mismatch | read the generated `asset-pack.json`; use its key/url | | movement depends on frame rate | raw per-frame displacement | multiply by capped `deltaSeconds` | +| player leaves the road or crosses a pylon | movement bypasses `resolveMovement` or collision radii are missing | route every XZ move through the resolver and keep SceneMap radii explicit | | huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | From d243bde307ac7c2b4e2ce0ebc626ce33c23ad2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 18:46:15 +0800 Subject: [PATCH 05/12] fix(templates): remove dead 3D screen config The three.js renderer sizes from its container and never consumes the inherited 2D reference dimensions. Removing the unused leaves gives both standalone core3d and threed_basic a zero-dead-config baseline. --- agent-test/templates/core3d/src/gameConfig.json | 4 ---- agent-test/templates/modules/threed_basic/src/gameConfig.json | 4 ---- 2 files changed, 8 deletions(-) diff --git a/agent-test/templates/core3d/src/gameConfig.json b/agent-test/templates/core3d/src/gameConfig.json index f23a8fad7..4b8593717 100644 --- a/agent-test/templates/core3d/src/gameConfig.json +++ b/agent-test/templates/core3d/src/gameConfig.json @@ -1,8 +1,4 @@ { - "screenSize": { - "width": { "value": 1152, "type": "number", "description": "Reference width" }, - "height": { "value": 768, "type": "number", "description": "Reference height" } - }, "renderConfig": { "pixelRatioCap": { "value": 2, "type": "number", "description": "Renderer pixel ratio cap" }, "fogNear": { "value": 18, "type": "number", "description": "Fog near distance" }, diff --git a/agent-test/templates/modules/threed_basic/src/gameConfig.json b/agent-test/templates/modules/threed_basic/src/gameConfig.json index 3bec72a8f..57231e400 100644 --- a/agent-test/templates/modules/threed_basic/src/gameConfig.json +++ b/agent-test/templates/modules/threed_basic/src/gameConfig.json @@ -1,8 +1,4 @@ { - "screenSize": { - "width": { "value": 1152, "type": "number", "description": "Reference width" }, - "height": { "value": 768, "type": "number", "description": "Reference height" } - }, "renderConfig": { "pixelRatioCap": { "value": 2, "type": "number", "description": "Renderer pixel ratio cap" }, "fogNear": { "value": 16, "type": "number", "description": "Fog near distance" }, From 2cf29c098a15be2988f1ff9d3f6717e2e586a27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 18:46:15 +0800 Subject: [PATCH 06/12] docs(templates): require consumed 3D config Generated v1.1 code duplicated fog, pickup, and count values. Make the builder keep one canonical path per runtime value, remove superseded aliases, and record the leaf-to-consumer map in GDD completion notes. --- agent-test/docs/modules/threed_basic/design_rules.md | 9 +++++++++ agent-test/docs/modules/threed_basic/template_api.md | 10 ++++++++++ agent-test/docs/modules/threed_basic/threed_basic.md | 9 ++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index 82a3ed896..35a60471f 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -43,11 +43,20 @@ distance checks are enough for pickups and static collision; do not introduce a physics dependency. Give every obstacle an explicit `collisionRadius` instead of deriving gameplay collision from rendered scale. +Every `gameConfig.json` leaf must have one literal runtime consumer. Do not keep +aliases for the same value: if pickup code moves from +`levelConfig.collectRadius` to another path, delete the old leaf in the same +edit. The current linear fog consumes `renderConfig.fogNear` and `fogFar`; do +not add `fogDensity` unless the implementation changes and the superseded +linear-fog leaves are removed. Derive authored counts from `SceneMap` arrays +unless a separately consumed completion threshold is required. + ## 5. GDD completion notes The GDD must end with: - the actual generated keys and their runtime consumers; +- a config leaf-to-consumer table with no duplicate or unconsumed leaf; - any placeholder/fallback used; - `3D scope: primitives + generated image textures; no model generation`; - the command evidence from build and smoke. diff --git a/agent-test/docs/modules/threed_basic/template_api.md b/agent-test/docs/modules/threed_basic/template_api.md index cbf674c74..9ca0949b7 100644 --- a/agent-test/docs/modules/threed_basic/template_api.md +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -43,6 +43,16 @@ Each obstacle declares `collisionRadius` independently from visual `scale`. patch, subdivides long moves to prevent tunnelling, and slides along an unblocked axis. Dynamic bodies, impulses, and gravity remain v2 concerns. +## Config ownership + +The shipped config is a starting contract, not a compatibility registry. Keep +one leaf per runtime value and require a literal consumer for every leaf. When +renaming or regrouping a field, update its consumer and delete the old field in +the same change. Do not preserve unused 2D infrastructure fields in a 3D game. + +Before build, search every config leaf outside `gameConfig.json`. A leaf with no +consumer is a failed implementation check, not a harmless preset. + ## Texture keys `Preloader` reads Phaser-compatible `asset-pack.json` sections and loads image diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index f9e436747..7e6c11c1a 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -10,7 +10,7 @@ and the one level rather than replacing the shell. |---|---|---| | 1 | map GDD asset keys to `skybox_texture`, `floor_patch`, `energy_billboard` | every used key exists in `asset-pack.json` | | 2 | edit `SceneMap.ts` | main route and every pickup are reachable; obstacle collision radii leave a traversable lane | -| 3 | merge tuning into `gameConfig.json` | wrapper shape and core fields remain | +| 3 | merge tuning into `gameConfig.json` | every leaf has one runtime consumer; superseded aliases are deleted | | 4 | theme materials and DOM text | canvas remains WebGL-only; HUD remains DOM-only | | 5 | run build and smoke | zero errors, non-black canvas, WebGL context, ESC resume | @@ -31,6 +31,12 @@ Keep `CollisionResolver.ts` pure. Floor patches and obstacle circles come from `SceneMap.ts`; player radius comes from `gameConfig.json`. Do not replace this with a physics dependency in v1. +Treat config as executable data. Keep the existing path when it already serves +the intended value; if a generated design chooses a new path, update the code +and remove the old path together. Never add `fogDensity`, a second pickup +radius, or a duplicate collectible count while their existing equivalents stay +in the file. Record the final leaf-to-consumer mapping in GDD completion notes. + ## Asset hookup Only call `generate_game_assets`. A skybox is a generated 2D equirectangular @@ -58,4 +64,5 @@ playable; it does not authorize skipping the required asset call. | image 404s | invented key or leading slash mismatch | read the generated `asset-pack.json`; use its key/url | | movement depends on frame rate | raw per-frame displacement | multiply by capped `deltaSeconds` | | player leaves the road or crosses a pylon | movement bypasses `resolveMovement` or collision radii are missing | route every XZ move through the resolver and keep SceneMap radii explicit | +| acceptance reports dead config | a field was copied or renamed without removing its old path | keep one canonical leaf, update its consumer, and delete the duplicate | | huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | From 74a7c9fbd21b024f7da78b8753c95cbc687c8ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 19:10:30 +0800 Subject: [PATCH 07/12] fix(core): specialize 3D config merge guidance The universal GDD reminder contradicted threed_basic zero-dead-config rules by forbidding deletion of inherited fields. Keep the Phaser reminder unchanged while requiring consumed, single-source leaves for 3D. --- packages/core/src/tools/generate-gdd.test.ts | 23 ++++++++++++++++++++ packages/core/src/tools/generate-gdd.ts | 9 +++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/tools/generate-gdd.test.ts diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts new file mode 100644 index 000000000..010a64dbd --- /dev/null +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; +import { configMergeInstruction } from './generate-gdd.js'; + +describe('configMergeInstruction', () => { + it('requires consumed config and deletion of superseded 3D leaves', () => { + const instruction = configMergeInstruction('threed_basic'); + + expect(instruction).toContain('every leaf must have a runtime consumer'); + expect(instruction).toContain('superseded leaves must be removed'); + expect(instruction).not.toContain('NEVER delete infrastructure fields'); + }); + + it.each(['platformer', 'top_down'] as const)( + 'preserves the existing Phaser config contract for %s', + (archetype) => { + const instruction = configMergeInstruction(archetype); + + expect(instruction).toContain('NEVER delete infrastructure fields'); + expect(instruction).toContain('screenSize'); + expect(instruction).toContain('debugConfig'); + }, + ); +}); diff --git a/packages/core/src/tools/generate-gdd.ts b/packages/core/src/tools/generate-gdd.ts index 2bb20eb48..c77d4c000 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -41,6 +41,13 @@ export interface GenerateGDDParams { config_summary?: string; } +export function configMergeInstruction(archetype: GameArchetype): string { + if (archetype === 'threed_basic') { + return '- MERGE GDD Section 2 values INTO the existing `src/gameConfig.json`; every leaf must have a runtime consumer, and renamed or superseded leaves must be removed in the same edit.'; + } + return '- MERGE GDD Section 2 values INTO the existing `src/gameConfig.json` -- add/update game-specific fields using `{ "value": X }` wrapper format, but NEVER delete infrastructure fields (`screenSize`, `renderConfig`, and Phaser\'s `debugConfig`)'; +} + export interface GDDModelConfig { apiKey: string; baseUrl: string; @@ -116,7 +123,7 @@ Save content between tags to \`GAME_DESIGN.md\` - Read \`public/assets/asset-pack.json\` for generated texture keys ### Phase 4: Config (use GDD Section 2) -- MERGE GDD Section 2 values INTO the existing \`src/gameConfig.json\` -- add/update game-specific fields using \`{ "value": X }\` wrapper format, but NEVER delete infrastructure fields (\`screenSize\`, \`renderConfig\`, and Phaser's \`debugConfig\`) +${configMergeInstruction(this.params.archetype)} ### Phase 5: Code Implementation (use GDD Sections 0, 3, 5) - **GDD Section 0** has scene keys -> update \`LevelManager.ts\` and \`main.ts\` From b49c27f282f0e0a6318d286e60cdfa96c19ce511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 20:02:31 +0800 Subject: [PATCH 08/12] docs(templates): keep 3D smoke bridge read-only A generated v1.3 game shipped teleport and time mutation hooks solely to make verification easy. Freeze the existing probe surface and require completion evidence through real browser input so acceptance measures gameplay instead of a debug backdoor. --- .../docs/modules/threed_basic/template_api.md | 13 +++++++++++++ .../docs/modules/threed_basic/threed_basic.md | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/agent-test/docs/modules/threed_basic/template_api.md b/agent-test/docs/modules/threed_basic/template_api.md index 9ca0949b7..2049f5e13 100644 --- a/agent-test/docs/modules/threed_basic/template_api.md +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -31,6 +31,19 @@ Required public methods: | `isPaused()` | smoke/lifecycle observable pause state | | `resize()` | update camera aspect and renderer size | +## Smoke bridge + +`window.__opengame3d` is an exact, read-only probe surface: + +```ts +{ isPaused: () => boolean; renderer: 'three.js' } +``` + +Do not add `teleport`, time setters, collectible setters, mutable `state`, or +other gameplay shortcuts. Build, pause, completion, failure, and restart must +be verified through keyboard/mouse events and the visible DOM. Test time limits +with browser clock control or a pure unit check, not a shipped runtime setter. + ## SceneMap `initSceneMap()` returns `playerSpawn`, `floorPatches`, `collectibles`, and `obstacles`. diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 7e6c11c1a..a450064da 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -12,7 +12,7 @@ and the one level rather than replacing the shell. | 2 | edit `SceneMap.ts` | main route and every pickup are reachable; obstacle collision radii leave a traversable lane | | 3 | merge tuning into `gameConfig.json` | every leaf has one runtime consumer; superseded aliases are deleted | | 4 | theme materials and DOM text | canvas remains WebGL-only; HUD remains DOM-only | -| 5 | run build and smoke | zero errors, non-black canvas, WebGL context, ESC resume | +| 5 | run build and smoke | zero errors, non-black canvas, WebGL context, ESC resume; smoke bridge remains read-only | ## Runtime lifecycle @@ -49,6 +49,10 @@ playable; it does not authorize skipping the required asset call. ## Manual play check +Keep the scaffolded `window.__opengame3d` surface exact: `isPaused` and +`renderer` only. Never add teleport, time/state setters, or collectible cheats +to make this check pass. + 1. Press Enter on the title screen. 2. Move with W/A/S/D or arrow keys; drag the mouse to look. 3. Press ESC, confirm the pause overlay, then ESC again and confirm movement. From ab0c5469f663c299ec9eeb9b2c5d00f3f7a50d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 20:31:54 +0800 Subject: [PATCH 09/12] fix(templates): stabilize 3D pause lifecycle A real v1.4 game showed repeated Escape events can toggle pause multiple times and end screens can resume gameplay. Ignore keyboard repeats and lock the runtime after completion or failure. --- agent-test/templates/core3d/src/main.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agent-test/templates/core3d/src/main.ts b/agent-test/templates/core3d/src/main.ts index 2067f95be..3616f7997 100644 --- a/agent-test/templates/core3d/src/main.ts +++ b/agent-test/templates/core3d/src/main.ts @@ -23,6 +23,7 @@ class GameApp { private readonly gameOverUi = new GameOverUIScene(); private game?: GameScene; private lastFrame = performance.now(); + private ended = false; async boot(): Promise { if (!this.container) throw new Error('Missing #game-container'); @@ -30,7 +31,7 @@ class GameApp { this.title.show(() => this.start()); window.addEventListener('resize', () => this.game?.resize()); window.addEventListener('keydown', (event) => { - if (event.code === 'Escape' && this.game) { + if (event.code === 'Escape' && this.game && !event.repeat && !this.ended) { if (this.game.isPaused()) this.resume('GameScene'); else this.pause('GameScene'); } @@ -45,10 +46,12 @@ class GameApp { { onProgress: (collected, total) => this.ui.update(collected, total), onComplete: () => { + this.ended = true; this.game?.setPaused(true); this.completeUi.show(() => location.reload()); }, onGameOver: () => { + this.ended = true; this.game?.setPaused(true); this.gameOverUi.show(() => location.reload()); }, From 4e4fdf50def3625c7271701e6d14095f68202e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 20:31:54 +0800 Subject: [PATCH 10/12] fix(templates): serve 3D favicon by path Real-browser review found the core3d shell requests an undeclared favicon. Ship a tiny standalone SVG and reference it relatively, keeping binary data out of editable HTML. --- agent-test/templates/core3d/index.html | 1 + agent-test/templates/core3d/public/favicon.svg | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 agent-test/templates/core3d/public/favicon.svg diff --git a/agent-test/templates/core3d/index.html b/agent-test/templates/core3d/index.html index 432b6fdbf..629d65596 100644 --- a/agent-test/templates/core3d/index.html +++ b/agent-test/templates/core3d/index.html @@ -3,6 +3,7 @@ + OpenGame 3D diff --git a/agent-test/templates/core3d/public/favicon.svg b/agent-test/templates/core3d/public/favicon.svg new file mode 100644 index 000000000..2bbc79a45 --- /dev/null +++ b/agent-test/templates/core3d/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + From 16aefc29ac10a93c665238f634790da02567714a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 22:06:43 +0800 Subject: [PATCH 11/12] fix(gdd): preserve ordered 3D objectives The star-lamp maze GDD weakened an explicit sequential objective into unordered collection to match the scaffold. Add a threed_basic-only fidelity rule so private GameScene state may implement the requested mechanic without widening the public API or changing Phaser prompts. --- packages/core/src/tools/generate-gdd.test.ts | 17 ++++++++++++++++- packages/core/src/tools/generate-gdd.ts | 8 ++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index 010a64dbd..3cea88072 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { configMergeInstruction } from './generate-gdd.js'; +import { + configMergeInstruction, + gameplaySemanticsInstruction, +} from './generate-gdd.js'; describe('configMergeInstruction', () => { it('requires consumed config and deletion of superseded 3D leaves', () => { @@ -21,3 +24,15 @@ describe('configMergeInstruction', () => { }, ); }); + +describe('gameplaySemanticsInstruction', () => { + it('preserves ordered 3D objectives without changing Phaser prompts', () => { + const instruction = gameplaySemanticsInstruction('threed_basic'); + + expect(instruction).toContain('ordered or sequential objectives'); + expect(instruction).toContain('private state inside `GameScene`'); + expect(instruction).toContain('never weaken the user requirement'); + expect(gameplaySemanticsInstruction('platformer')).toBe(''); + expect(gameplaySemanticsInstruction('top_down')).toBe(''); + }); +}); diff --git a/packages/core/src/tools/generate-gdd.ts b/packages/core/src/tools/generate-gdd.ts index c77d4c000..29c5d1415 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -48,6 +48,11 @@ export function configMergeInstruction(archetype: GameArchetype): string { return '- MERGE GDD Section 2 values INTO the existing `src/gameConfig.json` -- add/update game-specific fields using `{ "value": X }` wrapper format, but NEVER delete infrastructure fields (`screenSize`, `renderConfig`, and Phaser\'s `debugConfig`)'; } +export function gameplaySemanticsInstruction(archetype: GameArchetype): string { + if (archetype !== 'threed_basic') return ''; + return '5. **3D Mechanic Fidelity**: Preserve explicit gameplay semantics such as ordered or sequential objectives. Public API limits forbid new external hooks, not private state inside `GameScene`; never weaken the user requirement to match the scaffold pickup loop.'; +} + export interface GDDModelConfig { apiKey: string; baseUrl: string; @@ -237,6 +242,9 @@ You are a game design engineer. Produce a **Technical Game Design Document** — `; + const semanticsInstruction = gameplaySemanticsInstruction(archetype); + if (semanticsInstruction) prompt += `${semanticsInstruction}\n\n`; + if (coreRules) { prompt += `---\n\n## Universal GDD Rules\n\n${coreRules}\n\n`; } else { From 158924cec861e237bce47ffa625fe840cafff6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 22:06:51 +0800 Subject: [PATCH 12/12] docs(threed): bind explicit mechanic semantics Document that ordered objectives remain ordered and should use minimal private GameScene state instead of being reinterpreted as the scaffold's unordered pickup loop. --- agent-test/docs/modules/threed_basic/design_rules.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index 35a60471f..a80fe96f1 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -9,6 +9,11 @@ simple geometry, lights, fog, a sky texture, and DOM overlays. Do not add physics, touch controls, multiplayer, imported 3D models, or any text-to-3D service. 2D Phaser templates are unrelated and must remain unchanged. +Explicit prompt mechanics are binding. Never reinterpret ordered or sequential +objectives as unordered collection to match the scaffold. Keep the existing +public API and implement the requested rule with the smallest private state in +`GameScene`, such as one next-objective index over the authored `SceneMap` order. + ## 2. Required runtime contract | Area | Required |