From 8f4f08955334f65dc1283c62b17cc88f353d7ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Tue, 4 Aug 2026 16:08:38 +0800 Subject: [PATCH 01/43] fix(docs): remove corrupted text characters Normalize copied Unicode spacing and mixed punctuation that rendered as garbled text. Add a repository-wide UTF-8 and mojibake regression check so the issue cannot silently return. --- docs/users/configuration/settings.md | 2 +- docs/users/features/approval-mode.md | 30 +++++++++--------- docs/users/features/sub-agents.md | 26 ++++++++-------- docs/users/integration-github-action.md | 2 +- scripts/tests/text-encoding.test.js | 41 +++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 scripts/tests/text-encoding.test.js diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 389883b72..907794db0 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -32,7 +32,7 @@ OpenGame uses JSON settings files for persistent configuration. There are four l | System defaults file | Linux: `/etc/qwen-code/system-defaults.json`
Windows: `C:\ProgramData\qwen-code\system-defaults.json`
macOS: `/Library/Application Support/QwenCode/system-defaults.json`
The path can be overridden using the `QWEN_CODE_SYSTEM_DEFAULTS_PATH` environment variable. | Provides a base layer of system-wide default settings. These settings have the lowest precedence and are intended to be overridden by user, project, or system override settings. | | User settings file | `~/.qwen/settings.json` (where `~` is your home directory). | Applies to all OpenGame sessions for the current user. | | Project settings file | `.qwen/settings.json` within your project's root directory. | Applies only when running OpenGame from that specific project. Project settings override user settings. | -| System settings file | Linux: `/etc/qwen-code/settings.json`
Windows: `C:\ProgramData\qwen-code\settings.json`
macOS: `/Library/Application Support/QwenCode/settings.json`
The path can be overridden using the `QWEN_CODE_SYSTEM_SETTINGS_PATH` environment variable. | Applies to all OpenGame sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' OpenGame setups. | +| System settings file | Linux: `/etc/qwen-code/settings.json`
Windows: `C:\ProgramData\qwen-code\settings.json`
macOS: `/Library/Application Support/QwenCode/settings.json`
The path can be overridden using the `QWEN_CODE_SYSTEM_SETTINGS_PATH` environment variable. | Applies to all OpenGame sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' OpenGame setups. | > [!note] > diff --git a/docs/users/features/approval-mode.md b/docs/users/features/approval-mode.md index 00754a26a..7ccfa17c6 100644 --- a/docs/users/features/approval-mode.md +++ b/docs/users/features/approval-mode.md @@ -4,12 +4,12 @@ OpenGame offers three distinct permission modes that allow you to flexibly contr ## Permission Modes Comparison -| Mode | File Editing | Shell Commands | Best For | Risk Level | -| -------------- | --------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | ---------- | -| **Plan**​ | ❌ Read-only analysis only | ❌ Not executed | • Code exploration
• Planning complex changes
• Safe code review | Lowest | -| **Default**​ | ✅ Manual approval required | ✅ Manual approval required | • New/unfamiliar codebases
• Critical systems
• Team collaboration
• Learning and teaching | Low | -| **Auto-Edit**​ | ✅ Auto-approved | ❌ Manual approval required | • Daily development tasks
• Refactoring and code improvements
• Safe automation | Medium | -| **YOLO**​ | ✅ Auto-approved | ✅ Auto-approved | • Trusted personal projects
• Automated scripts/CI/CD
• Batch processing tasks | Highest | +| Mode | File Editing | Shell Commands | Best For | Risk Level | +| ------------- | --------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | ---------- | +| **Plan** | ❌ Read-only analysis only | ❌ Not executed | • Code exploration
• Planning complex changes
• Safe code review | Lowest | +| **Default** | ✅ Manual approval required | ✅ Manual approval required | • New/unfamiliar codebases
• Critical systems
• Team collaboration
• Learning and teaching | Low | +| **Auto-Edit** | ✅ Auto-approved | ❌ Manual approval required | • Daily development tasks
• Refactoring and code improvements
• Safe automation | Medium | +| **YOLO** | ✅ Auto-approved | ✅ Auto-approved | • Trusted personal projects
• Automated scripts/CI/CD
• Batch processing tasks | Highest | ### Quick Reference Guide @@ -20,7 +20,7 @@ OpenGame offers three distinct permission modes that allow you to flexibly contr > [!tip] > -> You can quickly cycle through modes during a session using **Shift+Tab**. The terminal status bar shows your current mode, so you always know what permissions OpenGame has. +> You can quickly cycle through modes during a session using **Shift+Tab**. The terminal status bar shows your current mode, so you always know what permissions OpenGame has. ## 1. Use Plan Mode for safe code analysis @@ -100,7 +100,7 @@ Default Mode is the standard way to work with OpenGame. In this mode, you mainta **Turn on Default Mode during a session** -You can switch into Default Mode during a session using **Shift+Tab**​ to cycle through permission modes. If you're in any other mode, pressing **Shift+Tab**​ will eventually cycle back to Default Mode, indicated by the absence of any mode indicator at the bottom of the terminal. +You can switch into Default Mode during a session using **Shift+Tab** to cycle through permission modes. If you're in any other mode, pressing **Shift+Tab** will eventually cycle back to Default Mode, indicated by the absence of any mode indicator at the bottom of the terminal. **Start a new session in Default Mode** @@ -171,8 +171,8 @@ Shift+Tab # Switch from other modes 1. You ask OpenGame to refactor a function 2. AI analyzes the code and proposes changes -3. **Automatically**​ applies all file changes without confirmation -4. If tests need to be run, it will **request approval**​ to execute `npm test` +3. **Automatically** applies all file changes without confirmation +4. If tests need to be run, it will **request approval** to execute `npm test` ## 4. YOLO Mode - Full Automation @@ -235,7 +235,7 @@ opengame --prompt "Run the test suite, fix all failing tests, then commit change ### Keyboard Shortcut Switching -During a OpenGame session, use **Shift+Tab**​ to quickly cycle through the three modes: +During a OpenGame session, use **Shift+Tab** to quickly cycle through the three modes: ``` Default Mode → Auto-Edit Mode → YOLO Mode → Plan Mode → Default Mode @@ -257,7 +257,7 @@ Default Mode → Auto-Edit Mode → YOLO Mode → Plan Mode → Default Mode ### Mode Usage Recommendations -1. **New to codebase**: Start with **Plan Mode**​ for safe exploration -2. **Daily development tasks**: Use **Auto-Accept Edits**​ (default mode), efficient and safe -3. **Automated scripts**: Use **YOLO Mode**​ in controlled environments for full automation -4. **Complex refactoring**: Use **Plan Mode**​ first for detailed planning, then switch to appropriate mode for execution +1. **New to codebase**: Start with **Plan Mode** for safe exploration +2. **Daily development tasks**: Use **Auto-Accept Edits** (default mode), efficient and safe +3. **Automated scripts**: Use **YOLO Mode** in controlled environments for full automation +4. **Complex refactoring**: Use **Plan Mode** first for detailed planning, then switch to appropriate mode for execution diff --git a/docs/users/features/sub-agents.md b/docs/users/features/sub-agents.md index 1de80741f..16ec84a4b 100644 --- a/docs/users/features/sub-agents.md +++ b/docs/users/features/sub-agents.md @@ -6,11 +6,11 @@ Subagents are specialized AI assistants that handle specific types of tasks with Subagents are independent AI assistants that: -- **Specialize in specific tasks** - Each Subagent is configured with a focused system prompt for particular types of work -- **Have separate context** - They maintain their own conversation history, separate from your main chat -- **Use controlled tools** - You can configure which tools each Subagent has access to -- **Work autonomously** - Once given a task, they work independently until completion or failure -- **Provide detailed feedback** - You can see their progress, tool usage, and execution statistics in real-time +- **Specialize in specific tasks** - Each Subagent is configured with a focused system prompt for particular types of work +- **Have separate context** - They maintain their own conversation history, separate from your main chat +- **Use controlled tools** - You can configure which tools each Subagent has access to +- **Work autonomously** - Once given a task, they work independently until completion or failure +- **Provide detailed feedback** - You can see their progress, tool usage, and execution statistics in real-time ## Key Benefits @@ -59,18 +59,18 @@ AI: I'll delegate this to your testing specialist Subagents. ### CLI Commands -Subagents are managed through the `/agents` slash command and its subcommands: +Subagents are managed through the `/agents` slash command and its subcommands: -**Usage:**:`/agents create`。Creates a new Subagent through a guided step wizard. +**Usage:** `/agents create`. Creates a new Subagent through a guided step wizard. -**Usage:**:`/agents manage`。Opens an interactive management dialog for viewing and managing existing Subagents. +**Usage:** `/agents manage`. Opens an interactive management dialog for viewing and managing existing Subagents. ### Storage Locations Subagents are stored as Markdown files in two locations: -- **Project-level**: `.qwen/agents/` (takes precedence) -- **User-level**: `~/.qwen/agents/` (fallback) +- **Project-level**: `.qwen/agents/` (takes precedence) +- **User-level**: `~/.qwen/agents/` (fallback) This allows you to have both project-specific agents and personal agents that work across all projects. @@ -398,7 +398,7 @@ description: Helps with testing, documentation, code review, and deployment --- ``` -**Why:** Focused agents produce better results and are easier to maintain. +**Why:** Focused agents produce better results and are easier to maintain. #### Clear Specialization @@ -422,7 +422,7 @@ description: Works on frontend development tasks --- ``` -**Why:** Specific expertise leads to more targeted and effective assistance. +**Why:** Specific expertise leads to more targeted and effective assistance. #### Actionable Descriptions @@ -440,7 +440,7 @@ description: Reviews code for security vulnerabilities, performance issues, and description: A helpful code reviewer ``` -**Why:** Clear descriptions help the main AI choose the right agent for each task. +**Why:** Clear descriptions help the main AI choose the right agent for each task. ### Configuration Best Practices diff --git a/docs/users/integration-github-action.md b/docs/users/integration-github-action.md index 8c6cbf0a9..5e72c10de 100644 --- a/docs/users/integration-github-action.md +++ b/docs/users/integration-github-action.md @@ -1,4 +1,4 @@ -# Github Actions:qwen-code-action +# Github Actions: qwen-code-action ## Overview diff --git a/scripts/tests/text-encoding.test.js b/scripts/tests/text-encoding.test.js new file mode 100644 index 000000000..b4604cb5f --- /dev/null +++ b/scripts/tests/text-encoding.test.js @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execFileSync } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; +import { describe, expect, it } from 'vitest'; + +describe('repository text encoding', () => { + it('contains valid UTF-8 without mojibake or invisible spacing', async () => { + const files = execFileSync('git', ['ls-files', '-z']) + .toString() + .split('\0') + .filter(Boolean); + const decoder = new TextDecoder('utf-8', { fatal: true }); + const suspicious = + /[\u00A0\u200B\u202A-\u202E\u2060\u2066-\u2069\uFEFF\uFFFD]|\u951F\u65A4\u62F7|\u70EB{3}|\u5C6F{3}|\u00EF\u00BF\u00BD|\u00C3[\u0080-\u00FF]|\u00C2[\u0080-\u00FF]|\u00E2[\u0080-\u00FF]/u; + const errors = []; + + for (const file of files) { + const bytes = await readFile(file); + if (bytes.includes(0)) continue; + + let content; + try { + content = decoder.decode(bytes); + } catch { + errors.push(`${file}: invalid UTF-8`); + continue; + } + + content.split('\n').forEach((line, index) => { + if (suspicious.test(line)) errors.push(`${file}:${index + 1}`); + }); + } + + expect(errors).toEqual([]); + }); +}); 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 02/43] 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 03/43] 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 04/43] 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 05/43] 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 06/43] 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 07/43] 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 08/43] 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 09/43] 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 10/43] 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 11/43] 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 12/43] 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 13/43] 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 | From 2104e1dcc0a43b78b75c5eb909781697f6e7ddb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 01:56:04 +0800 Subject: [PATCH 14/43] fix(gdd): keep 3D verification faithful Prevent generated acceptance criteria from strengthening declared win conditions or relying on instrumented artifacts, so production evidence reflects the shipped game. --- packages/core/src/tools/generate-gdd.test.ts | 8 ++++++++ packages/core/src/tools/generate-gdd.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index 3cea88072..60f7b9694 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -32,6 +32,14 @@ describe('gameplaySemanticsInstruction', () => { expect(instruction).toContain('ordered or sequential objectives'); expect(instruction).toContain('private state inside `GameScene`'); expect(instruction).toContain('never weaken the user requirement'); + expect(instruction).toContain( + 'never strengthen, replace, or contradict it', + ); + expect(instruction).toContain( + 'optional objectives must not become pass gates', + ); + expect(instruction).toContain('final uninstrumented artifact'); + expect(instruction).toContain('probes must not be a prerequisite for PASS'); 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 29c5d1415..51e6108e9 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -50,7 +50,7 @@ export function configMergeInstruction(archetype: GameArchetype): string { 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.'; + 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. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; } export interface GDDModelConfig { From f5f5090ccb2170f4ef8386677e8014c3274f8d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 01:56:14 +0800 Subject: [PATCH 15/43] docs(threed): require path-only final acceptance Keep real media out of generated source and session context, and require complete playthrough evidence from the final uninstrumented artifact. --- .../docs/modules/threed_basic/design_rules.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index a80fe96f1..ed5c11144 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -39,6 +39,9 @@ public API and implement the requested rule with the smallest private state in 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. +Keep every image, audio file, and screenshot as a separate file inside the game +workspace. Reference runtime assets through `asset-pack.json` and relative paths; +never inline real media as Base64 or a Data URL in source, config, HTML, or GDD. ## 4. Level and camera budget @@ -65,3 +68,15 @@ The GDD must end with: - any placeholder/fallback used; - `3D scope: primitives + generated image textures; no model generation`; - the command evidence from build and smoke. + +## 6. Verification integrity + +Acceptance must test the declared win condition without strengthening or +replacing it. Optional collectibles, checkpoints, and side objectives cannot +become pass gates unless the prompt explicitly makes them required. + +A complete-playthrough PASS must run against the final uninstrumented artifact. +Read-only diagnostic probes may explain a failure, but they cannot be required +for the final PASS. Save screenshots as files and report their paths; use the +official smoke pixel/WebGL assertions instead of reading screenshots back into +the generation session. From e49cdde4da0d3d73d81501d03a2f10a999944c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 02:31:13 +0800 Subject: [PATCH 16/43] docs(debug): keep screenshot evidence path-only Prevent generation sessions from reading screenshots as Base64; automated checks use smoke and visible DOM while human review opens the reported file path. --- agent-test/docs/debug_protocol.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/agent-test/docs/debug_protocol.md b/agent-test/docs/debug_protocol.md index 0666272d1..541ae04a4 100644 --- a/agent-test/docs/debug_protocol.md +++ b/agent-test/docs/debug_protocol.md @@ -170,6 +170,18 @@ asset-pack.json → animations.json → Character.ts animKeys 3. Use browser tools to verify visual behavior 4. Continue with other tasks while server runs +### 2.2 Screenshot Evidence Is Path-Only + +- Save screenshots as files under the current game workspace and report their + relative paths. Never embed real media in source, config, HTML, Markdown, or + chat/tool output. +- Never call `ReadMediaFile` or another media-read tool for screenshots, even + after compression or through a sub-agent. That still injects Base64/Data URLs + into the generation session. +- Use smoke's WebGL, non-blank pixel, error-array, and visible-DOM assertions for + automated verification. Human visual review happens outside the generation + session by opening the reported path. + --- ## 3. Test Infrastructure From 1247c2c38a0a0641beb4cbdf24d54e594fe2a3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 04:32:32 +0800 Subject: [PATCH 17/43] fix(templates): close 3d restart lifecycle Align W movement with the three.js camera forward vector and make in-page restart dispose input listeners plus scene-owned GPU resources. The source-level self-check locks both contracts without adding runtime test hooks. --- agent-test/templates/core3d/src/GameScene.ts | 21 +++++++++- agent-test/templates/core3d/src/main.ts | 12 +++++- .../modules/threed_basic/src/GameScene.ts | 21 +++++++++- .../threed_basic/src/InputController.ts | 13 +++++- .../tests/runtime-contract-selfcheck.mjs | 42 +++++++++++++++++++ 5 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs diff --git a/agent-test/templates/core3d/src/GameScene.ts b/agent-test/templates/core3d/src/GameScene.ts index 120000b7a..3ad09ff7d 100644 --- a/agent-test/templates/core3d/src/GameScene.ts +++ b/agent-test/templates/core3d/src/GameScene.ts @@ -4,6 +4,7 @@ import { Color, DirectionalLight, Fog, + Material, Mesh, MeshStandardMaterial, PerspectiveCamera, @@ -116,7 +117,7 @@ export class GameScene { 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; + (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)); @@ -134,6 +135,24 @@ export class GameScene { return this.paused; } + dispose(): void { + window.removeEventListener('keydown', this.onKeyDown); + window.removeEventListener('keyup', this.onKeyUp); + this.renderer.domElement.removeEventListener('pointerdown', this.onPointerDown); + window.removeEventListener('pointerup', this.onPointerUp); + window.removeEventListener('pointermove', this.onPointerMove); + this.scene.traverse((object) => { + if (!(object instanceof Mesh)) return; + object.geometry.dispose(); + const materials: Material[] = Array.isArray(object.material) + ? object.material + : [object.material]; + for (const material of materials) material.dispose(); + }); + this.renderer.dispose(); + this.renderer.domElement.remove(); + } + resize(): void { const width = Math.max(1, this.container.clientWidth); const height = Math.max(1, this.container.clientHeight); diff --git a/agent-test/templates/core3d/src/main.ts b/agent-test/templates/core3d/src/main.ts index 3616f7997..50966a1eb 100644 --- a/agent-test/templates/core3d/src/main.ts +++ b/agent-test/templates/core3d/src/main.ts @@ -41,6 +41,7 @@ class GameApp { private start(): void { if (!this.container) return; + this.ended = false; this.game = new GameScene( this.container, { @@ -48,12 +49,12 @@ class GameApp { onComplete: () => { this.ended = true; this.game?.setPaused(true); - this.completeUi.show(() => location.reload()); + this.completeUi.show(() => this.restart()); }, onGameOver: () => { this.ended = true; this.game?.setPaused(true); - this.gameOverUi.show(() => location.reload()); + this.gameOverUi.show(() => this.restart()); }, }, this.preloader.textures, @@ -80,6 +81,13 @@ class GameApp { } } + private restart(): void { + this.pauseUi.hide(); + this.game?.dispose(); + this.game = undefined; + this.start(); + } + private readonly frame = (time: number): void => { const delta = Math.min(0.05, (time - this.lastFrame) / 1000); this.lastFrame = time; diff --git a/agent-test/templates/modules/threed_basic/src/GameScene.ts b/agent-test/templates/modules/threed_basic/src/GameScene.ts index 649507b68..e12d77e6a 100644 --- a/agent-test/templates/modules/threed_basic/src/GameScene.ts +++ b/agent-test/templates/modules/threed_basic/src/GameScene.ts @@ -3,6 +3,7 @@ import { Color, EquirectangularReflectionMapping, IcosahedronGeometry, + Material, Mesh, MeshStandardMaterial, Object3D, @@ -123,11 +124,13 @@ export class GameScene { this.input.consumeLookDelta() * gameConfig.playerConfig.mouseSensitivity.value; const { forward, strafe } = this.input.movement(); const speed = gameConfig.playerConfig.moveSpeed.value * deltaSeconds; + const sin = Math.sin(this.yaw); + const cos = Math.cos(this.yaw); 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, + x: (-sin * forward + cos * strafe) * speed, + z: (-cos * forward - sin * strafe) * speed, }, gameConfig.playerConfig.collisionRadius.value, this.map, @@ -164,6 +167,20 @@ export class GameScene { return this.paused; } + dispose(): void { + this.input.dispose(); + this.scene.traverse((object) => { + if (object instanceof Mesh) object.geometry.dispose(); + if (!(object instanceof Mesh || object instanceof Sprite)) return; + const materials: Material[] = Array.isArray(object.material) + ? object.material + : [object.material]; + for (const material of materials) material.dispose(); + }); + this.renderer.dispose(); + this.renderer.domElement.remove(); + } + resize(): void { const size = new Vector3( Math.max(1, this.container.clientWidth), diff --git a/agent-test/templates/modules/threed_basic/src/InputController.ts b/agent-test/templates/modules/threed_basic/src/InputController.ts index 97b237519..6afa5662b 100644 --- a/agent-test/templates/modules/threed_basic/src/InputController.ts +++ b/agent-test/templates/modules/threed_basic/src/InputController.ts @@ -3,10 +3,10 @@ export class InputController { private dragging = false; private lookDelta = 0; - constructor(canvas: HTMLCanvasElement) { + constructor(private readonly canvas: HTMLCanvasElement) { window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); - canvas.addEventListener('mousedown', this.onMouseDown); + this.canvas.addEventListener('mousedown', this.onMouseDown); window.addEventListener('mouseup', this.onMouseUp); window.addEventListener('mousemove', this.onMouseMove); } @@ -33,6 +33,15 @@ export class InputController { this.lookDelta = 0; } + dispose(): void { + window.removeEventListener('keydown', this.onKeyDown); + window.removeEventListener('keyup', this.onKeyUp); + this.canvas.removeEventListener('mousedown', this.onMouseDown); + window.removeEventListener('mouseup', this.onMouseUp); + window.removeEventListener('mousemove', this.onMouseMove); + this.clear(); + } + private readonly onKeyDown = (event: KeyboardEvent): void => { this.keys.add(event.code); }; diff --git a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs new file mode 100644 index 000000000..625025f07 --- /dev/null +++ b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const moduleScene = await readFile( + new URL('../src/GameScene.ts', import.meta.url), + 'utf8', +); +const moduleInput = await readFile( + new URL('../src/InputController.ts', import.meta.url), + 'utf8', +); +const coreScene = await readFile( + new URL('../../../core3d/src/GameScene.ts', import.meta.url), + 'utf8', +); +const coreMain = await readFile( + new URL('../../../core3d/src/main.ts', import.meta.url), + 'utf8', +); + +const movement = (yaw, forward, strafe) => ({ + x: -Math.sin(yaw) * forward + Math.cos(yaw) * strafe, + z: -Math.cos(yaw) * forward - Math.sin(yaw) * strafe, +}); +for (const yaw of [-Math.PI, -Math.PI / 2, 0, Math.PI / 2, Math.PI]) { + const forward = movement(yaw, 1, 0); + assert(Math.abs(forward.x + Math.sin(yaw)) < 1e-12); + assert(Math.abs(forward.z + Math.cos(yaw)) < 1e-12); +} + +assert.match(moduleScene, /x: \(-sin \* forward \+ cos \* strafe\)/); +assert.match(moduleScene, /z: \(-cos \* forward - sin \* strafe\)/); +assert.match(coreScene, /Math\.cos\(this\.yaw\) \* strafe - Math\.sin\(this\.yaw\) \* forward/); +assert.doesNotMatch(moduleScene, /\+ Math\.sin\(this\.yaw\) \* forward/); +assert.match(moduleInput, /dispose\(\): void/); +assert.match(moduleScene, /this\.input\.dispose\(\)/); +assert.match(moduleScene, /object\.geometry\.dispose\(\)/); +assert.match(moduleScene, /material\.dispose\(\)/); +assert.match(coreMain, /this\.game\?\.dispose\(\)/); +assert.match(coreScene, /this\.renderer\.dispose\(\)/); + +console.log('threed_basic runtime contract self-check: PASS'); From ad46d70acf71e2c0245c75b7d5b6dab15b3cf621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 04:32:45 +0800 Subject: [PATCH 18/43] docs(threed): require final GDD reconciliation Phase 6 changes must correct stale values and consumers in the GDD body instead of leaving contradictory appendices. Document the restart resource-ownership contract exposed by the template fix. --- agent-test/docs/modules/threed_basic/design_rules.md | 5 +++++ agent-test/docs/modules/threed_basic/template_api.md | 5 +++++ agent-test/docs/modules/threed_basic/threed_basic.md | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index ed5c11144..6b48fb00d 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -69,6 +69,11 @@ The GDD must end with: - `3D scope: primitives + generated image textures; no model generation`; - the command evidence from build and smoke. +After Phase 6 changes, reconcile the GDD body itself against the final +`SceneMap`, config, asset-pack, and uninstrumented playthrough. Correct stale +numbers and consumers in place; an appendix may explain why a value changed, +but must not leave an older contradictory value as a second truth. + ## 6. Verification integrity Acceptance must test the declared win condition without strengthening or diff --git a/agent-test/docs/modules/threed_basic/template_api.md b/agent-test/docs/modules/threed_basic/template_api.md index 2049f5e13..62be78f3f 100644 --- a/agent-test/docs/modules/threed_basic/template_api.md +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -29,8 +29,13 @@ Required public methods: | `update(deltaSeconds)` | advance input, collection, animation, and render | | `setPaused(boolean)` | stop/resume simulation and clear held input | | `isPaused()` | smoke/lifecycle observable pause state | +| `dispose()` | unbind input and release scene-owned geometry, material, renderer, and canvas resources before instance replacement | | `resize()` | update camera aspect and renderer size | +`GameScene.dispose()` owns input listeners, geometry, materials, renderer state, +and its canvas. `Preloader` owns the shared texture map, so scene replacement +must not dispose those textures before the next scene reuses them. + ## Smoke bridge `window.__opengame3d` is an exact, read-only probe surface: diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index a450064da..b52b3953f 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -13,6 +13,7 @@ and the one level rather than replacing the shell. | 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; smoke bridge remains read-only | +| 6 | reconcile `GAME_DESIGN.md` after the final uninstrumented playthrough | the GDD body, config, asset-pack, SceneMap, and observed win condition state one consistent truth | ## Runtime lifecycle @@ -27,6 +28,10 @@ Preloader.load -> TitleScreen.show -> GameScene constructor `setPaused(true)` must clear input so a key held before pause cannot continue moving after resume. +When replacing a `GameScene` instance, call `dispose()` before constructing the +next one. It unbinds input and releases scene-owned geometry, materials, +renderer state, and canvas; shared Preloader textures remain owned by the app. + 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. @@ -67,6 +72,9 @@ to make this check pass. | 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` | +| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | +| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | | 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 | +| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | | huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | From 0067c6607cb2aa4aa6ac71a7a8614e53794270a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 04:34:31 +0800 Subject: [PATCH 19/43] fix(templates): align camera-relative strafe The core shell still used the opposite Z sign for the camera-right vector. Lock both components in the runtime contract self-check so yawed movement cannot regress independently. --- agent-test/templates/core3d/src/GameScene.ts | 2 +- .../modules/threed_basic/tests/runtime-contract-selfcheck.mjs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/agent-test/templates/core3d/src/GameScene.ts b/agent-test/templates/core3d/src/GameScene.ts index 3ad09ff7d..992583119 100644 --- a/agent-test/templates/core3d/src/GameScene.ts +++ b/agent-test/templates/core3d/src/GameScene.ts @@ -119,7 +119,7 @@ export class GameScene { 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; + (-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; diff --git a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs index 625025f07..eacd27905 100644 --- a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs +++ b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs @@ -31,6 +31,7 @@ for (const yaw of [-Math.PI, -Math.PI / 2, 0, Math.PI / 2, Math.PI]) { assert.match(moduleScene, /x: \(-sin \* forward \+ cos \* strafe\)/); assert.match(moduleScene, /z: \(-cos \* forward - sin \* strafe\)/); assert.match(coreScene, /Math\.cos\(this\.yaw\) \* strafe - Math\.sin\(this\.yaw\) \* forward/); +assert.match(coreScene, /-Math\.sin\(this\.yaw\) \* strafe - Math\.cos\(this\.yaw\) \* forward/); assert.doesNotMatch(moduleScene, /\+ Math\.sin\(this\.yaw\) \* forward/); assert.match(moduleInput, /dispose\(\): void/); assert.match(moduleScene, /this\.input\.dispose\(\)/); From 58975c490adabd110a7b69cce51924a7c958c22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 05:28:07 +0800 Subject: [PATCH 20/43] docs(threed): anchor final claims to source v1.9 production still named runtime state and texture behavior that did not exist. Require unstable implementation details to be copied from the final consumer or removed, so the GDD cannot invent a plausible second truth. --- .../docs/modules/threed_basic/design_rules.md | 34 ++++++++------- .../docs/modules/threed_basic/threed_basic.md | 41 ++++++++++--------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index 6b48fb00d..34f0c6cfc 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -16,26 +16,26 @@ public API and implement the requested rule with the smallest private state in ## 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 | +| Area | Required | +| --------- | ---------------------------------------------------------------------------------- | +| 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()` | -| Win | one explicit, reachable completion condition | +| 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` | +| 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. @@ -74,6 +74,12 @@ After Phase 6 changes, reconcile the GDD body itself against the final numbers and consumers in place; an appendix may explain why a value changed, but must not leave an older contradictory value as a second truth. +Implementation details need source anchors, not memory. Before keeping an +internal variable name, numeric mesh position, or texture behavior such as UV +repeat in the GDD, copy it from the final runtime source and verify that exact +consumer still exists. Otherwise state only the stable player-visible behavior; +delete invented implementation detail instead of preserving a plausible claim. + ## 6. Verification integrity Acceptance must test the declared win condition without strengthening or diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index b52b3953f..8892246fb 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -6,14 +6,14 @@ 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; 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; smoke bridge remains read-only | -| 6 | reconcile `GAME_DESIGN.md` after the final uninstrumented playthrough | the GDD body, config, asset-pack, SceneMap, and observed win condition state one consistent truth | +| 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; 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; smoke bridge remains read-only | +| 6 | reconcile `GAME_DESIGN.md` after the final uninstrumented playthrough | the GDD body, config, asset-pack, SceneMap, and observed win condition state one consistent truth | ## Runtime lifecycle @@ -66,15 +66,16 @@ to make this check pass. ## 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` | -| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | -| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | -| 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 | -| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | -| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | +| 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` | +| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | +| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | +| 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 | +| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | +| GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | +| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | From f1a6d767a2c273c505e0305f967fe16c554c6620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 05:38:15 +0800 Subject: [PATCH 21/43] fix(assets): normalize generated image canvases Some providers ignore the requested image size and returned 1254x1254 files for a 1024x1024 contract. Normalize backgrounds, sprites, and animation frames immediately before writing so asset-pack paths always point at the requested canvas without exposing inline media. --- .../tools/generate-assets-normalize.test.ts | 62 ++++++++++++++ packages/core/src/tools/generate-assets.ts | 84 +++++++++++++++++-- 2 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 packages/core/src/tools/generate-assets-normalize.test.ts diff --git a/packages/core/src/tools/generate-assets-normalize.test.ts b/packages/core/src/tools/generate-assets-normalize.test.ts new file mode 100644 index 000000000..452395f4e --- /dev/null +++ b/packages/core/src/tools/generate-assets-normalize.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; +import sharp from 'sharp'; +import { normalizeImageBuffer, parseImageSize } from './generate-assets.js'; + +async function fixture(width: number, height: number): Promise { + return sharp({ + create: { + width, + height, + channels: 4, + background: { r: 20, g: 120, b: 220, alpha: 1 }, + }, + }) + .png() + .toBuffer(); +} + +describe('generated image normalization', () => { + it('parses provider size notation', () => { + expect(parseImageSize('1024*1024')).toEqual({ width: 1024, height: 1024 }); + expect(parseImageSize('1536x1024')).toEqual({ width: 1536, height: 1024 }); + expect(() => parseImageSize('bad')).toThrow('Invalid image size'); + }); + + it('shrinks an ignored 1024 request to the required sprite canvas', async () => { + const output = await normalizeImageBuffer(await fixture(1254, 1254), { + width: 1024, + height: 1024, + fit: 'contain', + }); + const metadata = await sharp(output).metadata(); + expect(metadata).toMatchObject({ + width: 1024, + height: 1024, + format: 'png', + }); + }); + + it('keeps an already-correct PNG byte-identical', async () => { + const input = await fixture(1024, 1024); + const output = await normalizeImageBuffer(input, { + width: 1024, + height: 1024, + fit: 'contain', + }); + expect(output).toBe(input); + }); + + it('normalizes an opaque background to its requested canvas', async () => { + const output = await normalizeImageBuffer(await fixture(1254, 1254), { + width: 1536, + height: 1024, + fit: 'cover', + }); + const metadata = await sharp(output).metadata(); + expect(metadata).toMatchObject({ + width: 1536, + height: 1024, + format: 'png', + }); + }); +}); diff --git a/packages/core/src/tools/generate-assets.ts b/packages/core/src/tools/generate-assets.ts index e1c61e4a3..063b59f72 100644 --- a/packages/core/src/tools/generate-assets.ts +++ b/packages/core/src/tools/generate-assets.ts @@ -20,6 +20,7 @@ import { import type { Config } from '../config/config.js'; import * as fs from 'fs/promises'; import * as path from 'path'; +import sharp from 'sharp'; import type { ModelRouter } from '../services/assetModelRouter.js'; import { createModelRouter } from '../services/assetModelRouter.js'; @@ -42,6 +43,51 @@ import { TilesetProcessor } from '../services/tileset-processor.js'; // ============== Constants ============== const MAX_CONCURRENCY = 2; +const DEFAULT_IMAGE_SIZE = '1024*1024'; + +export type ImageNormalization = { + width: number; + height: number; + fit: 'contain' | 'cover'; +}; + +export function parseImageSize( + size: string, +): Pick { + const match = /^(\d+)[*x](\d+)$/.exec(size); + if (!match) throw new Error(`Invalid image size: ${size}`); + return { width: Number(match[1]), height: Number(match[2]) }; +} + +export async function normalizeImageBuffer( + buffer: Buffer, + spec: ImageNormalization, +): Promise { + const image = sharp(buffer); + const metadata = await image.metadata(); + if ( + metadata.format === 'png' && + metadata.width === spec.width && + metadata.height === spec.height + ) { + return buffer; + } + return image + .resize(spec.width, spec.height, { + fit: spec.fit, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .png({ + compressionLevel: 9, + adaptiveFiltering: true, + }) + .toBuffer(); +} + +const DEFAULT_SPRITE_NORMALIZATION: ImageNormalization = { + ...parseImageSize(DEFAULT_IMAGE_SIZE), + fit: 'contain', +}; // ============== Invocation Class ============== @@ -262,12 +308,13 @@ CODING INSTRUCTION (Phaser 3): PURE SCENERY ONLY - landscape, buildings, environment, sky, etc. `; - const imageUrl = await this.modelRouter.generateImage( - prompt, - req.resolution || '1024*1024', - ); + const resolution = req.resolution || DEFAULT_IMAGE_SIZE; + const imageUrl = await this.modelRouter.generateImage(prompt, resolution); const buffer = await this.downloadImage(imageUrl); - const cdnUrl = await this.saveAsset(buffer, req.key, assetsDir); + const cdnUrl = await this.saveAsset(buffer, req.key, assetsDir, 'png', { + ...parseImageSize(resolution), + fit: 'cover', + }); this.updateAssetPack(assetPack, req.key, 'background', cdnUrl); } @@ -291,12 +338,18 @@ CODING INSTRUCTION (Phaser 3): const imageUrl = await this.modelRouter.generateImage( prompt, - req.size || '1024*1024', + req.size || DEFAULT_IMAGE_SIZE, ); // Remove background const buffer = await this.bgRemovalService.removeBackgroundSafe(imageUrl); - const cdnUrl = await this.saveAsset(buffer, req.key, assetsDir); + const cdnUrl = await this.saveAsset( + buffer, + req.key, + assetsDir, + 'png', + DEFAULT_SPRITE_NORMALIZATION, + ); this.updateAssetPack(assetPack, req.key, 'image', cdnUrl); } @@ -322,14 +375,20 @@ CODING INSTRUCTION (Phaser 3): const baseImageUrl = await this.modelRouter.generateImage( basePrompt, - '1024*1024', + DEFAULT_IMAGE_SIZE, ); // Save base/idle frame const baseKey = `${req.key}_idle_01`; const baseBuffer = await this.bgRemovalService.removeBackgroundSafe(baseImageUrl); - const baseCdnUrl = await this.saveAsset(baseBuffer, baseKey, assetsDir); + const baseCdnUrl = await this.saveAsset( + baseBuffer, + baseKey, + assetsDir, + 'png', + DEFAULT_SPRITE_NORMALIZATION, + ); this.updateAssetPack(assetPack, baseKey, 'animation', baseCdnUrl); // Step 2: Generate animation frames @@ -480,6 +539,8 @@ CODING INSTRUCTION (Phaser 3): processedBuffer, frameKey, assetsDir, + 'png', + DEFAULT_SPRITE_NORMALIZATION, ); this.updateAssetPack(assetPack, frameKey, 'animation', frameCdnUrl); @@ -648,6 +709,8 @@ CODING INSTRUCTION (Phaser 3): frameBuffer, frameKey, assetsDir, + 'png', + DEFAULT_SPRITE_NORMALIZATION, ); this.updateAssetPack(assetPack, frameKey, 'animation', frameCdnUrl); @@ -894,7 +957,10 @@ isometric, 3D, perspective, vignette, dark corners. key: string, assetsDir: string, extension: string = 'png', + normalization?: ImageNormalization, ): Promise { + if (normalization) + buffer = await normalizeImageBuffer(buffer, normalization); const filename = `${key}.${extension}`; const filePath = path.join(assetsDir, filename); await fs.writeFile(filePath, buffer); From 6c7494967aeb551ba79b724eb9b012f43c068a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 06:39:13 +0800 Subject: [PATCH 22/43] docs(threed): protect generated runtime contracts Generated games replaced correct camera movement and disposal despite the scaffold already providing both. Make those shell sections immutable in v1 and keep media references path-only, with the existing runtime self-check pinning the manual contract. --- agent-test/docs/modules/threed_basic/threed_basic.md | 7 +++++++ .../threed_basic/tests/runtime-contract-selfcheck.mjs | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 8892246fb..10ae67bdf 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -4,6 +4,11 @@ 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. +The scaffolded camera-relative movement block, `InputController.dispose()`, +`GameScene.dispose()`, and the `main.ts` restart call are protected runtime +contracts. Keep them verbatim while customizing a v1 game; edit `SceneMap`, +config, materials, text, and win logic around them. + ## Phase 5 implementation order | Order | Action | Done when | @@ -47,6 +52,8 @@ in the file. Record the final leaf-to-consumer mapping in GDD completion notes. 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. +Keep returned media as workspace files and consume the `asset-pack.json` +relative URL; never copy media bytes or Base64/Data URLs into editable files. 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 diff --git a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs index eacd27905..2019660ca 100644 --- a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs +++ b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs @@ -17,6 +17,10 @@ const coreMain = await readFile( new URL('../../../core3d/src/main.ts', import.meta.url), 'utf8', ); +const manual = await readFile( + new URL('../../../../docs/modules/threed_basic/threed_basic.md', import.meta.url), + 'utf8', +); const movement = (yaw, forward, strafe) => ({ x: -Math.sin(yaw) * forward + Math.cos(yaw) * strafe, @@ -39,5 +43,7 @@ assert.match(moduleScene, /object\.geometry\.dispose\(\)/); assert.match(moduleScene, /material\.dispose\(\)/); assert.match(coreMain, /this\.game\?\.dispose\(\)/); assert.match(coreScene, /this\.renderer\.dispose\(\)/); +assert.match(manual, /protected runtime\s+contracts/); +assert.match(manual, /asset-pack\.json[\s\S]*relative URL/); console.log('threed_basic runtime contract self-check: PASS'); From b42ebe6a3372386d30c483604cdfe77cc5507d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 07:52:08 +0800 Subject: [PATCH 23/43] docs(threed): require baseline evidence for provenance Generated completion notes blamed a dispose defect on a frozen template whose protected block was already correct. Require path, SHA-256, and diff evidence so production feedback cannot promote a self-authored transient edit into a pipeline defect. --- agent-test/docs/modules/threed_basic/threed_basic.md | 8 ++++++++ .../threed_basic/tests/runtime-contract-selfcheck.mjs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 10ae67bdf..b67c9c521 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -20,6 +20,13 @@ config, materials, text, and win logic around them. | 5 | run build and smoke | zero errors, non-black canvas, WebGL context, ESC resume; smoke bridge remains read-only | | 6 | reconcile `GAME_DESIGN.md` after the final uninstrumented playthrough | the GDD body, config, asset-pack, SceneMap, and observed win condition state one consistent truth | +Do not attribute a defect or fix to the scaffold, template, or frozen baseline +from memory. Compare the final file with the exact scaffold source first. Any +such GDD claim must include `Baseline evidence:` with the canonical source +path, its SHA-256, and the relevant diff result. If the protected block already +matches, describe the incident as a generated edit that was reverted, not a +template defect. + ## Runtime lifecycle ```text @@ -85,4 +92,5 @@ to make this check pass. | 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 | | completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | | GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | +| GDD blames a template block that already matches the frozen baseline | Phase 6 inferred provenance from its own edit history instead of a baseline diff | record path + SHA-256 + diff evidence, or attribute the reverted edit to this generated game | | huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | diff --git a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs index 2019660ca..73398c78a 100644 --- a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs +++ b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs @@ -45,5 +45,7 @@ assert.match(coreMain, /this\.game\?\.dispose\(\)/); assert.match(coreScene, /this\.renderer\.dispose\(\)/); assert.match(manual, /protected runtime\s+contracts/); assert.match(manual, /asset-pack\.json[\s\S]*relative URL/); +assert.match(manual, /Baseline evidence:[\s\S]*SHA-256[\s\S]*diff result/); +assert.match(manual, /generated edit that was reverted, not a[\s\S]*template defect/); console.log('threed_basic runtime contract self-check: PASS'); From 867428b7c013b80965f8f8af4891ee250e3f181f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 08:38:08 +0800 Subject: [PATCH 24/43] docs(threed): require final spawn and tuning evidence Generated GDDs can claim reconciliation while retaining stale tuning aliases or spawn yaw values. Require direct final source/config and SceneMap comparison before completion. --- .../docs/modules/threed_basic/threed_basic.md | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index b67c9c521..24b3e87c4 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -27,6 +27,11 @@ path, its SHA-256, and the relevant diff result. If the protected block already matches, describe the incident as a generated edit that was reverted, not a template defect. +Before declaring reconciliation complete, search every backticked tuning name +in the final source/config and compare every player/camera spawn yaw claim with +the final `SceneMap.playerSpawn.yaw`. Remove stale aliases and old spawn values +from the GDD body; a completion note saying they were fixed is not evidence. + ## Runtime lifecycle ```text @@ -80,17 +85,17 @@ to make this check pass. ## 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` | -| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | -| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | -| 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 | -| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | -| GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | +| 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` | +| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | +| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | +| 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 | +| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | +| GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | | GDD blames a template block that already matches the frozen baseline | Phase 6 inferred provenance from its own edit history instead of a baseline diff | record path + SHA-256 + diff evidence, or attribute the reverted edit to this generated game | -| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | +| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | From 16e1fdb13fe031935bb101211d1879d83f1c98f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 08:38:52 +0800 Subject: [PATCH 25/43] style(threed): preserve existing manual formatting The pre-commit formatter changed an unrelated table while committing the source-truth rule. Restore the untouched lines so the final review diff stays scoped. --- .../docs/modules/threed_basic/threed_basic.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 24b3e87c4..a85952df3 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -85,17 +85,17 @@ to make this check pass. ## 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` | -| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | -| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | -| 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 | -| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | -| GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | +| 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` | +| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | +| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | +| 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 | +| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | +| GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | | GDD blames a template block that already matches the frozen baseline | Phase 6 inferred provenance from its own edit history instead of a baseline diff | record path + SHA-256 + diff evidence, or attribute the reverted edit to this generated game | -| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | +| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | From 4fb99d8d9063cf216a79177ed850ec4583a96e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 09:17:52 +0800 Subject: [PATCH 26/43] docs(threed): require final render config truth Generated GDDs retained stale renderConfig instructions and claimed a camera-far consumer that did not exist. Require final values and a named runtime leaf so production reviews can reject those false claims. --- agent-test/docs/modules/threed_basic/threed_basic.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index a85952df3..d530f4536 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -32,6 +32,11 @@ in the final source/config and compare every player/camera spawn yaw claim with the final `SceneMap.playerSpawn.yaw`. Remove stale aliases and old spawn values from the GDD body; a completion note saying they were fixed is not evidence. +Replace roadmap instructions such as "keep renderConfig untouched" with the +final values and consumers after any tuning edit. Describe a camera far plane +or renderer setting as config-backed only when its final runtime consumer reads +that named leaf; a nearby fog or DPR field is not evidence for camera config. + ## Runtime lifecycle ```text From 45080bcd5791ba60bbd3ed76562a26bf133dc82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 11:27:45 +0800 Subject: [PATCH 27/43] docs(threed): require stable route clearance Mathematical overlap let generated routes pass while leaving player-radius corridors and finish triggers on collision boundaries. Define measurable clearance and target-center rules, and pin final config descriptions to their real consumers. --- .../docs/modules/threed_basic/design_rules.md | 13 +++++++++++++ .../docs/modules/threed_basic/template_api.md | 6 ++++++ .../docs/modules/threed_basic/threed_basic.md | 11 ++++++++++- .../tests/runtime-contract-selfcheck.mjs | 2 ++ 4 files changed, 31 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 34f0c6cfc..da921dcfc 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -51,6 +51,14 @@ 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. +Mathematical overlap is not a playable lane. Shrink every floor patch by the +player collision radius, then require each authored neighbor pair to keep an +intersection half-width of at least two player radii. The spawn, every required +pickup center, and the finish trigger center must themselves be playable after +that shrink and must sit outside every expanded obstacle circle. Do not place a +finish center inside a lighthouse or other blocker and rely on the trigger +radius reaching a thin boundary crescent. + 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 @@ -74,6 +82,11 @@ After Phase 6 changes, reconcile the GDD body itself against the final numbers and consumers in place; an appendix may explain why a value changed, but must not leave an older contradictory value as a second truth. +Treat each `gameConfig.json` description as part of that final truth. If a +consumer changes from a config leaf to a literal or another leaf, correct the +description and every GDD consumer table instead of leaving the old consumer +claim behind. + Implementation details need source anchors, not memory. Before keeping an internal variable name, numeric mesh position, or texture behavior such as UV repeat in the GDD, copy it from the final runtime source and verify that exact diff --git a/agent-test/docs/modules/threed_basic/template_api.md b/agent-test/docs/modules/threed_basic/template_api.md index 62be78f3f..696480c07 100644 --- a/agent-test/docs/modules/threed_basic/template_api.md +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -61,6 +61,12 @@ 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. +For a single authored route, neighboring patches are ordered path segments, not +an arbitrary set. After subtracting the player radius from both patch radii, +their intersection half-width must remain at least two player radii. Spawn, +required pickup centers, and the finish center must pass the same floor and +expanded-obstacle predicate used by `resolveMovement()`. + ## Config ownership The shipped config is a starting contract, not a compatibility registry. Keep diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index d530f4536..1a2d01b50 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -58,11 +58,19 @@ 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. +Before the final playthrough, validate the authored route with the same player +radius: shrink adjacent floor patches by that radius and require their +intersection half-width to be at least two player radii. The spawn, required +pickup centers, and finish center must be playable points outside expanded +obstacle circles. A trigger radius overlapping a playable boundary does not +make a finish center embedded in solid collision acceptable. + 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. +in the file. Record the final leaf-to-consumer mapping in GDD completion notes, +and reconcile each config leaf's `description` against the same final consumer. ## Asset hookup @@ -99,6 +107,7 @@ to make this check pass. | W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | | a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | | 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 | +| route works only after repeated edge nudges | floor patches merely touch after player-radius shrink, or a target sits in collision | widen/reposition the authored patches and keep spawn, pickups, and finish centers playable | | 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 | | completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | | GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | diff --git a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs index 73398c78a..08844399d 100644 --- a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs +++ b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs @@ -47,5 +47,7 @@ assert.match(manual, /protected runtime\s+contracts/); assert.match(manual, /asset-pack\.json[\s\S]*relative URL/); assert.match(manual, /Baseline evidence:[\s\S]*SHA-256[\s\S]*diff result/); assert.match(manual, /generated edit that was reverted, not a[\s\S]*template defect/); +assert.match(manual, /intersection half-width[\s\S]*two player radii/); +assert.match(manual, /config leaf's `description`[\s\S]*final consumer/); console.log('threed_basic runtime contract self-check: PASS'); From cb63c1caca47da1c0b317430045120d3681b4462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 11:47:54 +0800 Subject: [PATCH 28/43] fix(gdd): require a real 3d gameplay loop The 3D GDD producer hard-coded a single-route collectible plan, so generated games could only reskin movement and pickup. Require verbs, pressure, feedback, escalation, win/lose states, and runtime consumers while leaving Phaser guidance untouched. --- packages/core/src/tools/generate-gdd.test.ts | 9 ++++++++- packages/core/src/tools/generate-gdd.ts | 21 +++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index 60f7b9694..19f1004ac 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -26,9 +26,16 @@ describe('configMergeInstruction', () => { }); describe('gameplaySemanticsInstruction', () => { - it('preserves ordered 3D objectives without changing Phaser prompts', () => { + it('requires a complete 3D core loop without changing Phaser prompts', () => { const instruction = gameplaySemanticsInstruction('threed_basic'); + expect(instruction).toContain('Do not default to the scaffold pickup loop'); + expect(instruction).toContain('repeatable 20-60 second core loop'); + expect(instruction).toContain('meaningful decision or skill demand'); + expect(instruction).toContain('observable failure condition'); + expect(instruction).toContain('three escalating beats'); + expect(instruction).toContain('exact win/lose conditions'); + expect(instruction).toContain('final `GameScene` or DOM consumer'); expect(instruction).toContain('ordered or sequential objectives'); expect(instruction).toContain('private state inside `GameScene`'); expect(instruction).toContain('never weaken the user requirement'); diff --git a/packages/core/src/tools/generate-gdd.ts b/packages/core/src/tools/generate-gdd.ts index 51e6108e9..cff7ec6f9 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -50,7 +50,7 @@ export function configMergeInstruction(archetype: GameArchetype): string { 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. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; + return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; } export interface GDDModelConfig { @@ -647,7 +647,7 @@ Tower Defense does NOT use generate_tilemap -- maps are code-defined grids. - 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. +- Gameplay: one short, explicit core loop with player verbs, pressure/failure, feedback, escalation, and reachable win/lose states; collection is only the reference fallback. - 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. @@ -743,7 +743,7 @@ ${this.getSection4Guidance(archetype)}`; 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. +- Interactive/objective billboard: \`type: "image"\`, one centered subject and a semantic key; \`energy_billboard\` is only the collection scaffold fallback. - 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.`, }; @@ -903,7 +903,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. + threed_basic: `Start with a Core Gameplay Contract table: player verbs, 20-60 second repeated loop, decision/skill demand, pressure, failure and recovery, visible feedback, three escalating beats, and exact win/lose conditions. Do not use collection unless the prompt calls for it. + +Define the exact \`SceneMap.ts\` arrays required by that loop (floorPatches and obstacles plus only real objective/hazard/trigger arrays), the \`GameScene\` state transitions and consumers, texture-key mapping, camera start/bounds, and completion/failure conditions. Every declared state and array needs a final runtime consumer. 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. @@ -971,7 +973,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', + threed_basic: '3D Level & Gameplay Loop', }; return titles[archetype] || 'Level Design'; } @@ -1201,12 +1203,13 @@ 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()\`: + threed_basic: `**Do not use generate_tilemap.** Provide one declarative level for \`initSceneMap()\` that executes the Section 3 Core Gameplay Contract: - 8-14 overlapping floor patches with x/z/radius values -- 5-10 reachable collectibles with id/x/y/z values +- only the objective, hazard, switch, gate, patrol, or trigger arrays actually consumed by the chosen loop; collectibles are optional - 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 +- camera start, track bounds, exact gameplay radii/timings, and reachable win/lose positions +- at least three authored beats showing how pressure or decisions escalate without changing the core verbs +- a manual play path proving the declared loop, failure/recovery, and win condition through keyboard/mouse input End Section 4 with: \`3D scope: primitives + generated image textures; no model generation\`.`, }; From 9b9354faf45013136a1c188fc42b3f5be672c602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 11:48:09 +0800 Subject: [PATCH 29/43] docs(threed): make collection a fallback The module manual defined threed_basic as collect-everything on one route, which prevented the builder from planning distinct play. Define the minimal core-loop contract and allow only semantic SceneMap arrays and path-referenced image keys with real consumers. --- .../docs/modules/threed_basic/design_rules.md | 40 ++++++++++++++----- .../docs/modules/threed_basic/template_api.md | 17 +++++--- .../docs/modules/threed_basic/threed_basic.md | 15 +++---- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index da921dcfc..50c007c50 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -2,9 +2,26 @@ ## 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. +Build one short, finishable 3D experience around an explicit core loop. Use +three.js, simple geometry, lights, fog, a sky texture, and DOM overlays. +Collection is the reference scaffold fallback, not the archetype definition. + +Before assets or level coordinates, declare a Core Gameplay Contract: + +| Contract | Required evidence | +| ---------- | ---------------------------------------------------------------------------- | +| Verbs | what the player repeatedly does through keyboard/mouse input | +| Loop | one 20-60 second action-feedback-decision cycle that repeats at least twice | +| Demand | one meaningful choice, timing, navigation, observation, or resource tradeoff | +| Pressure | an observable fail condition plus recovery or restart behavior | +| Feedback | visible DOM/world response for progress, danger, success, and failure | +| Escalation | at least three authored beats that intensify the same loop | +| Outcome | exact reachable win and lose conditions | + +Do not reskin move-and-collect when the prompt asks for avoidance, timing, +switching, delivery, pursuit, defense, or another mechanic. Every private state +field and declarative SceneMap array in the contract must name its final runtime +consumer; do not invent a generic gameplay framework. 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. @@ -30,12 +47,12 @@ public API and implement the requested rule with the smallest private state in 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` | +| 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 | +| objective billboard | `image` | semantic key, 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. @@ -45,7 +62,10 @@ never inline real media as Base64 or a Data URL in source, config, HTML, or GDD. ## 4. Level and camera budget -Use 8-14 floor patches, 5-10 collectibles, and 8-16 low-poly decorations. +Use 8-14 floor patches and 8-16 low-poly decorations. Add only the objective, +hazard, switch, gate, patrol, trigger, or collectible entries consumed by the +chosen loop; do not keep the scaffold collectible budget when collection is not +the mechanic. Keep the camera far plane under 250 and cap device pixel ratio at 2. Manual distance checks are enough for pickups and static collision; do not introduce a physics dependency. Give every obstacle an explicit `collisionRadius` instead diff --git a/agent-test/docs/modules/threed_basic/template_api.md b/agent-test/docs/modules/threed_basic/template_api.md index 696480c07..40ca6ea47 100644 --- a/agent-test/docs/modules/threed_basic/template_api.md +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -5,7 +5,7 @@ | 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/GameScene.ts` | owns renderer, scene, camera, world, and private gameplay state | | `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()` | @@ -51,10 +51,13 @@ with browser clock control or a pure unit check, not a shipped runtime setter. ## SceneMap -`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. +The reference `initSceneMap()` returns `playerSpawn`, `floorPatches`, +`collectibles`, and `obstacles`; its collection loop is a working fallback, not +a required genre. Change positions there instead of hard-coding level +coordinates inside the render loop. Add or replace declarative arrays at the +`// EXT` point only when the chosen Core Gameplay Contract has a real consumer. +Keep the public lifecycle API while implementing the smallest private state +needed by that loop. Each obstacle declares `collisionRadius` independently from visual `scale`. `resolveMovement()` keeps the full player circle inside at least one floor @@ -80,7 +83,9 @@ consumer is a failed implementation check, not a harmless preset. ## Texture keys `Preloader` reads Phaser-compatible `asset-pack.json` sections and loads image -entries into `Map`. The reference module recognizes: +entries into `Map`. The reference module recognizes +these fallback keys. A chosen loop may add semantic image keys when the final +`GameScene` consumes each one: | Key | Fallback | |---|---| diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 1a2d01b50..6c88aec6d 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -13,8 +13,8 @@ config, materials, text, and win logic around them. | 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; obstacle collision radii leave a traversable lane | +| 1 | map each semantic GDD asset key to its final texture consumer | every used key exists in `asset-pack.json`; scaffold fallback keys are not mandatory | +| 2 | edit `SceneMap.ts` | the chosen loop's targets and win/lose positions are reachable; collision leaves 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; smoke bridge remains read-only | @@ -43,7 +43,7 @@ that named leaf; a nearby fog or DPR field is not evidence for camera config. Preloader.load -> TitleScreen.show -> GameScene constructor -> applyThreeSceneDefaults -> initSceneMap -> HUD.show -> requestAnimationFrame -> GameScene.update -> resolveMovement -> renderer.render - -> all collectibles removed -> onComplete -> GameCompleteUIScene + -> declared gameplay loop reaches its win state -> onComplete -> GameCompleteUIScene ``` `deltaSeconds` is capped by `main.ts`; all movement must multiply by it. @@ -75,8 +75,9 @@ and reconcile each config leaf's `description` against the same final consumer. ## 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. +image, floor art is a generated 2D patch/texture, and interactive world art is +a generated billboard sprite with a semantic key. Do not call shell image +tools or any 3D model API. Keep returned media as workspace files and consume the `asset-pack.json` relative URL; never copy media bytes or Base64/Data URLs into editable files. @@ -93,8 +94,8 @@ 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. -4. Follow the single route and collect all energy markers. -5. Confirm `TRAIL COMPLETE`, then verify restart. +4. Exercise the GDD Core Gameplay Contract through at least two cycles and all three escalation beats. +5. Trigger the visible failure/recovery path, then reach the declared win state and verify restart. ## Frequent failures From 9d3640dfedd97ce938f3fb95de4e32d33b15c53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 12:05:00 +0800 Subject: [PATCH 30/43] fix(gdd): preserve no-fail 3d prompts A mandatory lose state would contradict peaceful game requests. Keep the gameplay contract strict while allowing an explicitly declared no-fail tension and progression response. --- agent-test/docs/modules/threed_basic/design_rules.md | 7 ++++++- agent-test/docs/modules/threed_basic/threed_basic.md | 2 +- packages/core/src/tools/generate-gdd.test.ts | 2 ++ packages/core/src/tools/generate-gdd.ts | 6 +++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index 50c007c50..921a1069e 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -13,7 +13,7 @@ Before assets or level coordinates, declare a Core Gameplay Contract: | Verbs | what the player repeatedly does through keyboard/mouse input | | Loop | one 20-60 second action-feedback-decision cycle that repeats at least twice | | Demand | one meaningful choice, timing, navigation, observation, or resource tradeoff | -| Pressure | an observable fail condition plus recovery or restart behavior | +| Pressure | an observable fail condition plus recovery, or an explicit no-fail tension/progression rule | | Feedback | visible DOM/world response for progress, danger, success, and failure | | Escalation | at least three authored beats that intensify the same loop | | Outcome | exact reachable win and lose conditions | @@ -23,6 +23,11 @@ switching, delivery, pursuit, defense, or another mechanic. Every private state field and declarative SceneMap array in the contract must name its final runtime consumer; do not invent a generic gameplay framework. +When the prompt explicitly requests peaceful or no-fail play, preserve that +semantics: state that there is no lose condition and specify how tension, +recovery, and progression still make the loop legible. Never add game over just +to fill the contract table. + 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. diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 6c88aec6d..952d92f77 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -95,7 +95,7 @@ to make this check pass. 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. Exercise the GDD Core Gameplay Contract through at least two cycles and all three escalation beats. -5. Trigger the visible failure/recovery path, then reach the declared win state and verify restart. +5. Trigger the visible failure/recovery path (or the declared no-fail pressure response), then reach the win state and verify restart. ## Frequent failures diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index 19f1004ac..f472b78c1 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -33,6 +33,8 @@ describe('gameplaySemanticsInstruction', () => { expect(instruction).toContain('repeatable 20-60 second core loop'); expect(instruction).toContain('meaningful decision or skill demand'); expect(instruction).toContain('observable failure condition'); + expect(instruction).toContain('explicitly requests a no-fail experience'); + expect(instruction).toContain('instead of inventing game over'); expect(instruction).toContain('three escalating beats'); expect(instruction).toContain('exact win/lose conditions'); expect(instruction).toContain('final `GameScene` or DOM consumer'); diff --git a/packages/core/src/tools/generate-gdd.ts b/packages/core/src/tools/generate-gdd.ts index cff7ec6f9..8cfe81902 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -50,7 +50,7 @@ export function configMergeInstruction(archetype: GameArchetype): string { export function gameplaySemanticsInstruction(archetype: GameArchetype): string { if (archetype !== 'threed_basic') return ''; - return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; + return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. If the user explicitly requests a no-fail experience, state that constraint and define tension plus recovery/progression instead of inventing game over. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; } export interface GDDModelConfig { @@ -903,7 +903,7 @@ 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: `Start with a Core Gameplay Contract table: player verbs, 20-60 second repeated loop, decision/skill demand, pressure, failure and recovery, visible feedback, three escalating beats, and exact win/lose conditions. Do not use collection unless the prompt calls for it. + threed_basic: `Start with a Core Gameplay Contract table: player verbs, 20-60 second repeated loop, decision/skill demand, pressure, failure and recovery, visible feedback, three escalating beats, and exact win/lose conditions. If the prompt explicitly requires no-fail play, record that constraint and specify tension plus recovery/progression instead. Do not use collection unless the prompt calls for it. Define the exact \`SceneMap.ts\` arrays required by that loop (floorPatches and obstacles plus only real objective/hazard/trigger arrays), the \`GameScene\` state transitions and consumers, texture-key mapping, camera start/bounds, and completion/failure conditions. Every declared state and array needs a final runtime consumer. @@ -1209,7 +1209,7 @@ List which tower types are available in each level. Early levels may restrict to - 8-16 low-poly obstacle decorations outside the main walking line - camera start, track bounds, exact gameplay radii/timings, and reachable win/lose positions - at least three authored beats showing how pressure or decisions escalate without changing the core verbs -- a manual play path proving the declared loop, failure/recovery, and win condition through keyboard/mouse input +- a manual play path proving the declared loop, failure/recovery (or declared no-fail pressure response), and win condition through keyboard/mouse input End Section 4 with: \`3D scope: primitives + generated image textures; no model generation\`.`, }; From e2716cc8e49d581c69d1430c8c35f0d9ecea6d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 12:46:35 +0800 Subject: [PATCH 31/43] fix(core): require 3d decision source truth Repeated generated games mislabeled camera far as config-backed and proved only one side of a claimed route choice. Put those checks in the shared GDD producer so later games must ground camera ownership and branch coverage in final code. --- packages/core/src/tools/generate-gdd.test.ts | 6 ++++++ packages/core/src/tools/generate-gdd.ts | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index f472b78c1..12526e2cb 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -41,6 +41,12 @@ describe('gameplaySemanticsInstruction', () => { expect(instruction).toContain('ordered or sequential objectives'); expect(instruction).toContain('private state inside `GameScene`'); expect(instruction).toContain('never weaken the user requirement'); + expect(instruction).toContain( + 'fog or DPR leaves do not make camera far config-backed', + ); + expect(instruction).toContain('forbidden cross-lane transitions'); + expect(instruction).toContain('play every branch'); + expect(instruction).toContain('attempt any claimed blocked switch'); expect(instruction).toContain( 'never strengthen, replace, or contradict it', ); diff --git a/packages/core/src/tools/generate-gdd.ts b/packages/core/src/tools/generate-gdd.ts index 8cfe81902..a6ed258b7 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -50,7 +50,7 @@ export function configMergeInstruction(archetype: GameArchetype): string { export function gameplaySemanticsInstruction(archetype: GameArchetype): string { if (archetype !== 'threed_basic') return ''; - return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. If the user explicitly requests a no-fail experience, state that constraint and define tension plus recovery/progression instead of inventing game over. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; + return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. If the user explicitly requests a no-fail experience, state that constraint and define tension plus recovery/progression instead of inventing game over. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Treat camera near/far as source literals unless the final code reads a named config leaf; fog or DPR leaves do not make camera far config-backed. For a branched choice, list intended neighbor pairs and forbidden cross-lane transitions under the player-radius-shrunk collision predicate, then play every branch and attempt any claimed blocked switch before calling the decision meaningful. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; } export interface GDDModelConfig { @@ -907,6 +907,8 @@ Refer to Design Guide Section 7 for screen shake rules, Template Capabilities Se Define the exact \`SceneMap.ts\` arrays required by that loop (floorPatches and obstacles plus only real objective/hazard/trigger arrays), the \`GameScene\` state transitions and consumers, texture-key mapping, camera start/bounds, and completion/failure conditions. Every declared state and array needs a final runtime consumer. +For branched routes, list intended neighboring floor-patch pairs and forbidden cross-lane transitions, then require the final uninstrumented play check to complete every branch and attempt any claimed blocked switch. Never call a route choice committed when the final collision geometry permits lane switching. Treat camera near/far as source literals unless final code reads an exact named config leaf; fog and DPR config are not camera-far consumers. + 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. @@ -1209,7 +1211,8 @@ List which tower types are available in each level. Early levels may restrict to - 8-16 low-poly obstacle decorations outside the main walking line - camera start, track bounds, exact gameplay radii/timings, and reachable win/lose positions - at least three authored beats showing how pressure or decisions escalate without changing the core verbs -- a manual play path proving the declared loop, failure/recovery (or declared no-fail pressure response), and win condition through keyboard/mouse input +- for a branched route, intended neighbor pairs and forbidden cross-lane transitions after player-radius shrink +- manual play paths proving every branch, any claimed blocked switch, failure/recovery (or declared no-fail pressure response), and the win condition through keyboard/mouse input End Section 4 with: \`3D scope: primitives + generated image textures; no model generation\`.`, }; From 8b18789213fae4fbd4e3e4efe5d0f4007a72ceb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 12:46:54 +0800 Subject: [PATCH 32/43] docs(threed): require branch coverage evidence The first non-collection game claimed a committed fast/safe choice while only exercising one route. Require intended and forbidden adjacency checks plus final play coverage for every branch. --- agent-test/docs/modules/threed_basic/design_rules.md | 9 +++++++++ agent-test/docs/modules/threed_basic/template_api.md | 5 +++++ agent-test/docs/modules/threed_basic/threed_basic.md | 1 + 3 files changed, 15 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index 921a1069e..01afe7ea3 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -84,6 +84,12 @@ that shrink and must sit outside every expanded obstacle circle. Do not place a finish center inside a lighthouse or other blocker and rely on the trigger radius reaching a thin boundary crescent. +For a branched route, list intended neighbor pairs and forbidden cross-lane +transitions after the same shrink. A claimed committed choice is false if the +final floor union still lets the player switch lanes. The final uninstrumented +play check must finish every branch and attempt each claimed blocked switch; +testing only one branch does not prove a meaningful route decision. + 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 @@ -91,6 +97,9 @@ 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. +Camera near/far values are source literals unless the final camera constructor +or update path reads an exact named config leaf. Fog distance and DPR fields do +not make a camera far plane config-backed. ## 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 40ca6ea47..1a3c14058 100644 --- a/agent-test/docs/modules/threed_basic/template_api.md +++ b/agent-test/docs/modules/threed_basic/template_api.md @@ -70,6 +70,11 @@ their intersection half-width must remain at least two player radii. Spawn, required pickup centers, and the finish center must pass the same floor and expanded-obstacle predicate used by `resolveMovement()`. +For a branched route, array order no longer declares adjacency. Record intended +neighbor pairs and forbidden cross-lane pairs in the GDD, then test them against +the final floor union and expanded obstacles. If a supposedly committed branch +can cross into another lane, the decision contract is not implemented. + ## Config ownership The shipped config is a starting contract, not a compatibility registry. Keep diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 952d92f77..818b4d11e 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -96,6 +96,7 @@ to make this check pass. 3. Press ESC, confirm the pause overlay, then ESC again and confirm movement. 4. Exercise the GDD Core Gameplay Contract through at least two cycles and all three escalation beats. 5. Trigger the visible failure/recovery path (or the declared no-fail pressure response), then reach the win state and verify restart. +6. If the level branches, finish every branch and attempt each transition the GDD claims is blocked; one successful route is not branch coverage. ## Frequent failures From 765f22b4f892142d0028d9b1579a5effc88b016a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 13:59:18 +0800 Subject: [PATCH 33/43] fix(gdd): require 3d feasibility ledger A prose-only core loop can combine individually plausible timings and resource values into an unwinnable game. Require winning and losing traces with derived deadlines, state deltas, and authored-event closure before a 3D design is called playable. --- packages/core/src/tools/generate-gdd.test.ts | 12 ++++++++++++ packages/core/src/tools/generate-gdd.ts | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index 12526e2cb..cd6cfdc72 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -55,6 +55,18 @@ describe('gameplaySemanticsInstruction', () => { ); expect(instruction).toContain('final uninstrumented artifact'); expect(instruction).toContain('probes must not be a prerequisite for PASS'); + expect(instruction).toContain( + 'cue -> player action -> decision -> state change -> feedback -> repeat', + ); + expect(instruction).toContain('Gameplay Feasibility Ledger'); + expect(instruction).toContain('one complete winning trace'); + expect(instruction).toContain('derived traversal/action time'); + expect(instruction).toContain( + 'handled-action counts must equal the authored event count', + ); + expect(instruction).toContain( + 'Recompute the ledger after every tuning edit', + ); 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 a6ed258b7..00cdff3d4 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -50,7 +50,7 @@ export function configMergeInstruction(archetype: GameArchetype): string { export function gameplaySemanticsInstruction(archetype: GameArchetype): string { if (archetype !== 'threed_basic') return ''; - return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. If the user explicitly requests a no-fail experience, state that constraint and define tension plus recovery/progression instead of inventing game over. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Treat camera near/far as source literals unless the final code reads a named config leaf; fog or DPR leaves do not make camera far config-backed. For a branched choice, list intended neighbor pairs and forbidden cross-lane transitions under the player-radius-shrunk collision predicate, then play every branch and attempt any claimed blocked switch before calling the decision meaningful. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; + return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. Write the loop as cue -> player action -> decision -> state change -> feedback -> repeat, not as theme prose. If the user explicitly requests a no-fail experience, state that constraint and define tension plus recovery/progression instead of inventing game over. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Treat camera near/far as source literals unless the final code reads a named config leaf; fog or DPR leaves do not make camera far config-backed. For a branched choice, list intended neighbor pairs and forbidden cross-lane transitions under the player-radius-shrunk collision predicate, then play every branch and attempt any claimed blocked switch before calling the decision meaningful. Before calling the loop playable, add a Gameplay Feasibility Ledger with one complete winning trace and one losing or counterfactual trace. Each row must name the cue/deadline, required input or route, derived traversal/action time, resource or health delta, and resulting state. Derive the budget from final SceneMap distances/timings and config speed/damage/recovery values; handled-action counts must equal the authored event count. Recompute the ledger after every tuning edit and reconcile the GDD body against the final uninstrumented playthrough. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; } export interface GDDModelConfig { @@ -905,11 +905,11 @@ Refer to Design Guide Section 7 for screen shake rules, Template Capabilities Se **CRITICAL**: Every hook name must exist in template_api.md. Do NOT invent hooks.`, threed_basic: `Start with a Core Gameplay Contract table: player verbs, 20-60 second repeated loop, decision/skill demand, pressure, failure and recovery, visible feedback, three escalating beats, and exact win/lose conditions. If the prompt explicitly requires no-fail play, record that constraint and specify tension plus recovery/progression instead. Do not use collection unless the prompt calls for it. -Define the exact \`SceneMap.ts\` arrays required by that loop (floorPatches and obstacles plus only real objective/hazard/trigger arrays), the \`GameScene\` state transitions and consumers, texture-key mapping, camera start/bounds, and completion/failure conditions. Every declared state and array needs a final runtime consumer. +Define the exact \`SceneMap.ts\` arrays required by that loop (floorPatches and obstacles plus only real objective/hazard/trigger arrays), the \`GameScene\` state transitions and consumers, texture-key mapping, camera start/bounds, and completion/failure conditions. Every declared state and array needs a final runtime consumer. Add a Gameplay Feasibility Ledger for one winning trace and one losing/counterfactual trace: cue/deadline, required input/route, derived action time, resource delta, and resulting state. Derive it from the final authored distances/timings and config speed/damage/recovery values, and make handled-action totals equal the authored event count. For branched routes, list intended neighboring floor-patch pairs and forbidden cross-lane transitions, then require the final uninstrumented play check to complete every branch and attempt any claimed blocked switch. Never call a route choice committed when the final collision geometry permits lane switching. Treat camera near/far as source literals unless final code reads an exact named config leaf; fog and DPR config are not camera-far consumers. -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.`, +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. After any tuning edit, recompute the ledger and reconcile its counts, terminal state, and GDD body against the final uninstrumented playthrough.`, 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\`): From 58e28376e198455f2f357d2b9a32390a6237dd68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 13:59:29 +0800 Subject: [PATCH 34/43] docs(threed): close gameplay budget before acceptance Make the builder derive route timing and state deltas from final authored data, then reconcile event totals and terminal states after tuning. This keeps build and smoke from standing in for actual playability. --- .../docs/modules/threed_basic/design_rules.md | 33 ++++++++--- .../docs/modules/threed_basic/threed_basic.md | 56 +++++++++++-------- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index 01afe7ea3..154609927 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -8,15 +8,31 @@ Collection is the reference scaffold fallback, not the archetype definition. Before assets or level coordinates, declare a Core Gameplay Contract: -| Contract | Required evidence | -| ---------- | ---------------------------------------------------------------------------- | -| Verbs | what the player repeatedly does through keyboard/mouse input | -| Loop | one 20-60 second action-feedback-decision cycle that repeats at least twice | -| Demand | one meaningful choice, timing, navigation, observation, or resource tradeoff | +| Contract | Required evidence | +| ---------- | ------------------------------------------------------------------------------------------- | +| Verbs | what the player repeatedly does through keyboard/mouse input | +| Loop | one 20-60 second action-feedback-decision cycle that repeats at least twice | +| Demand | one meaningful choice, timing, navigation, observation, or resource tradeoff | | Pressure | an observable fail condition plus recovery, or an explicit no-fail tension/progression rule | -| Feedback | visible DOM/world response for progress, danger, success, and failure | -| Escalation | at least three authored beats that intensify the same loop | -| Outcome | exact reachable win and lose conditions | +| Feedback | visible DOM/world response for progress, danger, success, and failure | +| Escalation | at least three authored beats that intensify the same loop | +| Outcome | exact reachable win and lose conditions | + +Immediately follow the contract with a Gameplay Feasibility Ledger. This is a +small arithmetic table, not a new runtime framework: + +| Evidence | Required | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Loop signature | cue -> player action -> decision -> state change -> feedback -> repeat | +| Winning trace | each required transition, its deadline, derived action/travel time, resource delta, and resulting state through the exact win condition | +| Losing trace | one missed/low-priority/counterfactual decision whose same rules reach the declared failure or recovery response | +| Closure | handled events equal the authored event count; terminal health/time/progress satisfies the exact outcome predicate | + +Derive travel time from final authored distance and movement speed, and derive +state deltas from the final damage, recovery, timer, and threshold consumers. +If the ledger does not close, tune the authored data before calling the design +playable. Recompute it after every Phase 6 tuning edit; do not leave aggregate +counts that disagree with the final playthrough. Do not reskin move-and-collect when the prompt asks for avoidance, timing, switching, delivery, pursuit, defense, or another mechanic. Every private state @@ -110,6 +126,7 @@ The GDD must end with: - any placeholder/fallback used; - `3D scope: primitives + generated image textures; no model generation`; - the command evidence from build and smoke. +- the final feasibility ledger totals and observed win/lose terminal states. After Phase 6 changes, reconcile the GDD body itself against the final `SceneMap`, config, asset-pack, and uninstrumented playthrough. Correct stale diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 818b4d11e..23ffe6718 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -11,14 +11,14 @@ config, materials, text, and win logic around them. ## Phase 5 implementation order -| Order | Action | Done when | -| ----- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| 1 | map each semantic GDD asset key to its final texture consumer | every used key exists in `asset-pack.json`; scaffold fallback keys are not mandatory | -| 2 | edit `SceneMap.ts` | the chosen loop's targets and win/lose positions are reachable; collision leaves 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; smoke bridge remains read-only | -| 6 | reconcile `GAME_DESIGN.md` after the final uninstrumented playthrough | the GDD body, config, asset-pack, SceneMap, and observed win condition state one consistent truth | +| Order | Action | Done when | +| ----- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| 1 | map each semantic GDD asset key to its final texture consumer | every used key exists in `asset-pack.json`; scaffold fallback keys are not mandatory | +| 2 | edit `SceneMap.ts` and close the Gameplay Feasibility Ledger | the chosen loop's route/action times fit its deadlines; resource deltas reach both declared outcomes | +| 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; smoke bridge remains read-only | +| 6 | reconcile `GAME_DESIGN.md` after the final uninstrumented playthrough | the GDD body, config, asset-pack, SceneMap, and observed win condition state one consistent truth | Do not attribute a defect or fix to the scaffold, template, or frozen baseline from memory. Compare the final file with the exact scaffold source first. Any @@ -65,6 +65,15 @@ pickup centers, and finish center must be playable points outside expanded obstacle circles. A trigger radius overlapping a playable boundary does not make a finish center embedded in solid collision acceptable. +Then execute the GDD Gameplay Feasibility Ledger against final data. For every +required decision, compare its cue-to-impact/action window with the derived +route or action time, apply the exact config damage/recovery/progress delta, and +carry the resulting state into the next row. The winning trace must satisfy the +exact win predicate, the losing/counterfactual trace must reach its declared +failure or recovery response, and handled-event totals must equal the authored +array count. Recompute this table after tuning; a build or smoke PASS cannot +substitute for a closed gameplay budget. + 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 @@ -97,21 +106,22 @@ to make this check pass. 4. Exercise the GDD Core Gameplay Contract through at least two cycles and all three escalation beats. 5. Trigger the visible failure/recovery path (or the declared no-fail pressure response), then reach the win state and verify restart. 6. If the level branches, finish every branch and attempt each transition the GDD claims is blocked; one successful route is not branch coverage. +7. Compare observed deadlines, handled-event count, state deltas, and terminal state with every row of the final Gameplay Feasibility Ledger. ## 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` | -| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | -| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | -| 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 | -| route works only after repeated edge nudges | floor patches merely touch after player-radius shrink, or a target sits in collision | widen/reposition the authored patches and keep spawn, pickups, and finish centers playable | -| 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 | -| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | -| GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | -| GDD blames a template block that already matches the frozen baseline | Phase 6 inferred provenance from its own edit history instead of a baseline diff | record path + SHA-256 + diff evidence, or attribute the reverted edit to this generated game | -| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | +| 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` | +| W moves opposite the camera's horizontal X direction after mouse look | the forward vector uses `+sin(yaw)` while three.js camera forward is `-sin(yaw)` | use `(-sin(yaw), -cos(yaw))` for forward and `(cos(yaw), -sin(yaw))` for right | +| a second run accumulates listeners or GPU resources | the previous scene instance was replaced without `dispose()` | call `dispose()` before rebuilding and keep textures under Preloader ownership | +| 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 | +| route works only after repeated edge nudges | floor patches merely touch after player-radius shrink, or a target sits in collision | widen/reposition the authored patches and keep spawn, pickups, and finish centers playable | +| 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 | +| completion notes contradict the GDD body | Phase 6 appended differences without correcting stale design values | reconcile the body in place, then keep the appendix only as change rationale | +| GDD names a Set, mesh coordinate, or UV behavior absent from source | Phase 6 rewrote prose from memory instead of the final consumer | copy the detail from final source or remove it and keep only stable player-visible behavior | +| GDD blames a template block that already matches the frozen baseline | Phase 6 inferred provenance from its own edit history instead of a baseline diff | record path + SHA-256 + diff evidence, or attribute the reverted edit to this generated game | +| huge GPU cost | uncapped DPR or oversized textures | DPR <= 2; texture/display size <= 1024 squared | From 6f9901f630704fa3dd4afa105795997f23cb5d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 15:03:36 +0800 Subject: [PATCH 35/43] fix(gdd): distinguish derived and observed gameplay evidence Prevent inferred or substitute play traces from being reported as exact-strategy acceptance evidence. Preserve the existing no-op behavior for 2D archetypes. --- packages/core/src/tools/generate-gdd.test.ts | 9 +++++++++ packages/core/src/tools/generate-gdd.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index cd6cfdc72..b710608b2 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -67,6 +67,15 @@ describe('gameplaySemanticsInstruction', () => { expect(instruction).toContain( 'Recompute the ledger after every tuning edit', ); + expect(instruction).toContain('Label every trace DERIVED or OBSERVED'); + expect(instruction).toContain('exact route, strategy, and terminal state'); + expect(instruction).toContain( + 'must not upgrade the target trace to observed', + ); + expect(instruction).toContain('carry state across escalation thresholds'); + expect(instruction).toContain( + 'observed terminal values override conflicting estimates', + ); 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 00cdff3d4..1e160fcfb 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -50,7 +50,7 @@ export function configMergeInstruction(archetype: GameArchetype): string { export function gameplaySemanticsInstruction(archetype: GameArchetype): string { if (archetype !== 'threed_basic') return ''; - return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. Write the loop as cue -> player action -> decision -> state change -> feedback -> repeat, not as theme prose. If the user explicitly requests a no-fail experience, state that constraint and define tension plus recovery/progression instead of inventing game over. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Treat camera near/far as source literals unless the final code reads a named config leaf; fog or DPR leaves do not make camera far config-backed. For a branched choice, list intended neighbor pairs and forbidden cross-lane transitions under the player-radius-shrunk collision predicate, then play every branch and attempt any claimed blocked switch before calling the decision meaningful. Before calling the loop playable, add a Gameplay Feasibility Ledger with one complete winning trace and one losing or counterfactual trace. Each row must name the cue/deadline, required input or route, derived traversal/action time, resource or health delta, and resulting state. Derive the budget from final SceneMap distances/timings and config speed/damage/recovery values; handled-action counts must equal the authored event count. Recompute the ledger after every tuning edit and reconcile the GDD body against the final uninstrumented playthrough. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; + return '5. **3D Gameplay Contract**: Do not default to the scaffold pickup loop unless the user asks for collection. Declare the player verbs, a repeatable 20-60 second core loop, one meaningful decision or skill demand, pressure and an observable failure condition, recovery/restart feedback, at least three escalating beats, and exact win/lose conditions. Write the loop as cue -> player action -> decision -> state change -> feedback -> repeat, not as theme prose. If the user explicitly requests a no-fail experience, state that constraint and define tension plus recovery/progression instead of inventing game over. Map every gameplay state and authored array to a final `GameScene` or DOM consumer. Public API limits forbid new external hooks, not private state inside `GameScene`; preserve explicit mechanics such as ordered or sequential objectives, and never weaken the user requirement to match the scaffold. Treat camera near/far as source literals unless the final code reads a named config leaf; fog or DPR leaves do not make camera far config-backed. For a branched choice, list intended neighbor pairs and forbidden cross-lane transitions under the player-radius-shrunk collision predicate, then play every branch and attempt any claimed blocked switch before calling the decision meaningful. Before calling the loop playable, add a Gameplay Feasibility Ledger with one complete winning trace and one losing or counterfactual trace. Each row must name the cue/deadline, required input or route, derived traversal/action time, resource or health delta, and resulting state. Label every trace DERIVED or OBSERVED; only the exact route, strategy, and terminal state exercised through keyboard/mouse input on the final uninstrumented artifact may be OBSERVED PASS. A substitute route, stand-in mechanic, or partial cycle proves only the shared transitions it actually exercised and must not upgrade the target trace to observed. Derived traces must carry state across escalation thresholds and recompute later timing/damage with the resulting beat or phase, rather than freezing the initial rate. Derive the budget from final SceneMap distances/timings and config speed/damage/recovery values; handled-action counts must equal the authored event count. Recompute the ledger after every tuning edit and reconcile the GDD body against the final uninstrumented playthrough; observed terminal values override conflicting estimates. Acceptance must preserve the declared win condition: never strengthen, replace, or contradict it, and optional objectives must not become pass gates. A complete-playthrough PASS must run against the final uninstrumented artifact; diagnostic probes must not be a prerequisite for PASS.'; } export interface GDDModelConfig { From 846c8d79c299ee8de560a121585869df87090b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 15:03:45 +0800 Subject: [PATCH 36/43] docs(threed): require exact-strategy playthrough provenance Keep feasibility evidence honest across arbitrary 3D mechanics by separating derived traces from exact observed playthroughs and carrying state through escalation thresholds. --- agent-test/docs/modules/threed_basic/design_rules.md | 6 ++++++ agent-test/docs/modules/threed_basic/threed_basic.md | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/agent-test/docs/modules/threed_basic/design_rules.md b/agent-test/docs/modules/threed_basic/design_rules.md index 154609927..b9453e45b 100644 --- a/agent-test/docs/modules/threed_basic/design_rules.md +++ b/agent-test/docs/modules/threed_basic/design_rules.md @@ -27,9 +27,15 @@ small arithmetic table, not a new runtime framework: | Winning trace | each required transition, its deadline, derived action/travel time, resource delta, and resulting state through the exact win condition | | Losing trace | one missed/low-priority/counterfactual decision whose same rules reach the declared failure or recovery response | | Closure | handled events equal the authored event count; terminal health/time/progress satisfies the exact outcome predicate | +| Provenance | label each trace `DERIVED` or `OBSERVED`; only the exact played route, strategy, and terminal state may be `OBSERVED PASS` | Derive travel time from final authored distance and movement speed, and derive state deltas from the final damage, recovery, timer, and threshold consumers. +Carry the resulting state across every escalation threshold and recompute later +rows with the active beat or phase. A substitute route, stand-in mechanic, or +partial cycle proves only the transitions it exercised; it cannot upgrade the +target trace to observed. Final observed terminal values override conflicting +estimates. If the ledger does not close, tune the authored data before calling the design playable. Recompute it after every Phase 6 tuning edit; do not leave aggregate counts that disagree with the final playthrough. diff --git a/agent-test/docs/modules/threed_basic/threed_basic.md b/agent-test/docs/modules/threed_basic/threed_basic.md index 23ffe6718..7d8d00eb6 100644 --- a/agent-test/docs/modules/threed_basic/threed_basic.md +++ b/agent-test/docs/modules/threed_basic/threed_basic.md @@ -74,6 +74,13 @@ failure or recovery response, and handled-event totals must equal the authored array count. Recompute this table after tuning; a build or smoke PASS cannot substitute for a closed gameplay budget. +Label each ledger trace `DERIVED` or `OBSERVED`. Only the exact route, strategy, +and terminal state exercised through normal input on the final uninstrumented +artifact may be `OBSERVED PASS`. A substitute route, stand-in mechanic, or +partial cycle proves only its shared transitions. Derived traces must carry +state across escalation thresholds and recompute later rows with the resulting +beat or phase; final observed terminal values override conflicting estimates. + 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 From d1f2b7c35195eb0c5858734949c0c4704d6ba2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 15:03:55 +0800 Subject: [PATCH 37/43] docs: record workspace test routing pitfall Document the correct package-relative focused-test invocation so future loops do not mistake workspace broadcast failures for product regressions. --- docs/mistakes.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/mistakes.md diff --git a/docs/mistakes.md b/docs/mistakes.md new file mode 100644 index 000000000..aaf06707d --- /dev/null +++ b/docs/mistakes.md @@ -0,0 +1,3 @@ +| 日期 | 现象 | 根因 | 正解 | 预防规则 | +| ---------- | ------------------------------------------------------ | --------------------------------------- | ---------------------------------------------------- | ------------------------------------------ | +| 2026-08-05 | focused Vitest 被广播到全部 workspaces,均报找不到测试 | 从仓库根运行带文件参数的聚合 `npm test` | 在 `packages/core` workspace 内用相对路径运行 Vitest | 传测试文件过滤器前先确认根脚本是否广播参数 | From ffdfdd2fb92c1c8f6813f75e5d2b9b3daf6f05a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 17:49:49 +0800 Subject: [PATCH 38/43] docs(pixelforge): add integration execution pack Consolidate the accepted cross-repository requirements, minimal architecture, dependency choices, rejected alternatives, ten-step delivery plan, and production acceptance gates so the next integration work starts from one versioned source of truth. --- docs/pixelforge-integration/README.md | 92 +++++++ .../acceptance-and-risks.md | 185 ++++++++++++++ .../architecture-and-selections.md | 205 +++++++++++++++ docs/pixelforge-integration/execution-plan.md | 180 +++++++++++++ docs/pixelforge-integration/requirements.md | 236 ++++++++++++++++++ 5 files changed, 898 insertions(+) create mode 100644 docs/pixelforge-integration/README.md create mode 100644 docs/pixelforge-integration/acceptance-and-risks.md create mode 100644 docs/pixelforge-integration/architecture-and-selections.md create mode 100644 docs/pixelforge-integration/execution-plan.md create mode 100644 docs/pixelforge-integration/requirements.md diff --git a/docs/pixelforge-integration/README.md b/docs/pixelforge-integration/README.md new file mode 100644 index 000000000..c12426a28 --- /dev/null +++ b/docs/pixelforge-integration/README.md @@ -0,0 +1,92 @@ +--- +title: PixelForge × OpenGame 融合执行包 +status: accepted-working-plan +snapshot_date: 2026-08-05 +owners: OpenGame / opengame-server / pixelforge-web +--- + +# PixelForge × OpenGame 融合执行包 + +**结论:本目录是下一阶段前后端融合的唯一执行入口。OpenGame 的目标是支撑 PixelForge 全业务,3D 只是其中一条生产管线。** + +本目录只保留当前有效结论。旧调研可作为证据来源,但不得覆盖这里的选型、顺序和验收门。 + +## 1. 文档阅读顺序 + +| 顺序 | 文档 | 用途 | 读完后应能回答 | +| ---- | ---------------------------------------------------------------- | ------------------------ | -------------------------------------- | +| 1 | [requirements.md](requirements.md) | 前端功能全下穿与后端合同 | 每个页面、动作、状态需要后端提供什么 | +| 2 | [architecture-and-selections.md](architecture-and-selections.md) | 架构、依赖、选型与排除项 | 用什么、为什么、复杂度和退出条件是什么 | +| 3 | [execution-plan.md](execution-plan.md) | 10 步施工单 | 先做什么、产物是什么、怎样回滚 | +| 4 | [acceptance-and-risks.md](acceptance-and-risks.md) | P0/P1、风险与放行门 | 什么情况下能联调、灰度和上线 | + +## 2. 当前唯一有效决策 + +| 领域 | 已采纳结论 | 边界 | +| ------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| 产品 | **OpenGame 支撑 PixelForge 全业务** | Create、Mod、Runtime Tuning、Operations Admin、发布、媒体、Feed、社区、账户与用量都要有后端认知 | +| 沙箱 | **BoxLite** | 不依赖腾讯沙箱;生产启用前必须等网络修复进入正式 release 并完成安全验收 | +| 生产主库 | **PostgreSQL** | 保存租户、账户、作品、Revision、Job、Ops、媒体、社区、订单与账本 | +| SQLite | **仅本地开发/单机运行账本** | 不承担 5–10 万 DAU 的社区主数据、队列或收入权威 | +| 文件 | **对象存储 + CDN** | DB 只存不可变对象键、URL、hash、来源、尺寸和状态,不存大文件 | +| 任务编排 | **现有 Job/worker 合同 + PostgreSQL lease** | 不引入 Temporal;Redis 只做短期状态和广播,不做正确性权威 | +| 美术模型 | **复用 OpenGame provider adapter** | 通义、豆包、OpenAI-compatible;不新建美术微服务 | +| 图片处理 | **把 sharp 提升为 `packages/core` 直接依赖** | 复用现有实现;负责裁切、缩略、格式转换和确定性文字叠加 | +| 视频处理 | **固定 FFmpeg capability** | Step 4 为现有 I2V/抽帧做 preflight,无能力则关 I2V;Step 8 产 H.264 MP4;首版不做 HLS | +| 媒体版本 | **`MediaAssetSet` 绑定 `revisionId + artifactHash`** | `Game.publishedMediaAssetSetId` 与 published Revision 原子切换;旧任务不得覆盖新版 | +| 封面策略 | **横、竖两个无字母版** | thumbnail/OG 确定性派生;Hero 只为精选游戏生成,禁止每次修改都重生图 | +| Runtime Tuning(GM 参数) | **`POST /api/ops/apply` 是唯一权威写入口** | 退役 `/tuning` 双写;写入对目标 RevisionHead 做 CAS | +| Mod | **CAS → BoxLite → QA → 新 Revision → 发布** | 不原地覆盖文件,不绕过沙箱和 QA | +| 3D v1 | **primitive + 2D texture/billboard** | GLB/FBX、text-to-3D、骨骼换装与模型 Mod 另立里程碑 | +| 移动 Feed | **安全 attract / MP4 / 竖封面三级降级** | 当前卡才运行;邻卡只预取 poster/metadata;缺策略不得默认放行 | +| 商业基础 | **不整体迁入 ShipAny** | 可购买作账户、订单、积分、管理台的代码参考;核心权威仍在 Go + PostgreSQL | +| 实时状态 | **REST event cursor + SSE** | v1 不同时维护 WS 公共合同;SSE 断线按 `Last-Event-ID` 恢复 | + +## 3. 三个仓库的责任边界 + +| 仓库 | 负责 | 不负责 | +| ----------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------- | +| `pixelforge-web` | Web/iOS/Android 交互、客户端状态、上传发起、Feed/播放/调参/运营/Mod 操作面 | 业务权威、假回执、生产 Mock、直接改游戏文件 | +| `opengame-server` | 公共 API、鉴权授权、租户、Revision、Job、CAS、审核、发布、用量、社区与账本 | 在主进程直接执行不可信游戏、把 SQLite 当生产社区库 | +| `OpenGame` | 生成引擎、GDD、游戏内资产、provider adapter 与版本化 worker I/O 合同 | 用户账户、社区主数据、支付权威、在宿主直接落盘/执行不可信项目 | +| BoxLite | 承载 OpenGame 项目落盘、build、Mod、QA、截图/转码等写文件或执行进程的步骤 | 媒体主数据、任务编排、业务授权;命令不得来自用户字符串 | +| 对象存储/CDN | immutable artifact、图片、视频与下载分发 | 版本选择、审核状态和授权判断 | + +## 4. 已核实的当前差距 + +| 事实 | 当前证据 | 影响 | +| ----------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------- | +| Web 启动即启用 MSW | `pixelforge-web/web/src/main.tsx:50-52` | 前端旅程不能证明真后端已闭环 | +| Go 仅有 7 条基础 `/v1` 路由 | `opengame-server/internal/api/api.go:52-58` | 目录、Revision、Job、Mod、Ops、媒体、账户、社区均未实现 | +| Go 生产原型仍是 SQLite | `opengame-server/go.mod:9`、`internal/store/store.go:1-16` | 不能直接按 5–10 万 DAU 上线 | +| Workspace 默认继承当前进程环境 | `opengame-server/internal/workspace/workspace.go:168-176` | 沙箱接入前有密钥外泄风险 | +| 媒体上传只绑定 game | `pixelforge-web/web/src/components/create/preview/uploads.ts:22-73` | 发布、并发修改和回滚会错配媒体 | +| Web 视频播放受“未静音”控制 | `RuntimeSurfaces.tsx:264-271` | 静音时反而不播放静音预览,策略含义错误 | +| Android 缺策略时允许 attract | `FeedAttractPreviewPolicy.kt:3-15` | 未经安全认证的游戏可在浏览态运行 | +| iOS/Android DTO 缺 cover 三字段 | `MobileGame.swift:105-161`、`MobileContracts.kt:411-447` | 原生端无法消费横/竖/Hero 封面 | +| GM 仍保留双写 | `use-tuning-surface.ts:83-90` | 两条状态链可能生成不同 Revision | +| OpenGame manifest 只有 type/key/url | `generate-assets-types.ts:136-145` | 缺 hash、来源、Revision、许可、审核和 lineage | +| 3D v1 明确不支持模型 | `generate-gdd.ts:629-640` | 不得把基础 3D 宣称为模型生成/换装能力 | + +## 5. 执行纪律 + +| 规则 | 要求 | +| -------------- | ---------------------------------------------------------------------------- | +| 单一真相 | 新决策直接修改本目录对应条目,不新建“v2/最终版/新版”副本 | +| 事实与目标分离 | **当前已实现**必须有代码证据;**目标态**必须有完成判定 | +| 一步一门 | 上一步验收未通过,不启动依赖它的发布面施工 | +| 单写入口 | 同一业务动作只能有一个权威写入口;兼容路由只调用同一 service,不复制业务逻辑 | +| 可回滚 | 每步通过 feature flag、兼容读或旧 Revision 保留回滚路径 | +| 依赖克制 | 先复用现有代码、stdlib、已安装依赖;只有验收指标证明不足才升级组件 | +| 分支与提交 | 一项可独立回滚的合同或能力一个 Conventional Commit;不得混入现有 3D WIP | + +## 6. 下一步从哪里开工 + +| 顺序 | 首批动作 | 完成判定 | +| ---- | ------------------------------------------------------------------------- | --------------------------------------------------------------- | +| 1 | 冻结 PixelForge-facing API/DTO/OpenAPI | Web、iOS、Android、Go 共用字段表与错误码,Mock 也从同一合同生成 | +| 2 | 给 MSW 增加环境闸和真后端 smoke | `production` 绝不注册 MSW;测试环境可显式开启 | +| 3 | 在 PostgreSQL 建 Game/RevisionHead/Revision/Artifact/Job/Event 的最小脊柱 | QA 后才创建不可变 Revision;重试不重复烧任务 | +| 4 | BoxLite adapter 与环境白名单 PoC | 沙箱内看不到 DB/Auth/对象存储主密钥,网络规则 fail-closed | + +完整顺序见 [execution-plan.md](execution-plan.md)。 diff --git a/docs/pixelforge-integration/acceptance-and-risks.md b/docs/pixelforge-integration/acceptance-and-risks.md new file mode 100644 index 000000000..b4dc2fc62 --- /dev/null +++ b/docs/pixelforge-integration/acceptance-and-risks.md @@ -0,0 +1,185 @@ +--- +title: PixelForge × OpenGame 验收门、预警与冲突 +status: accepted-working-plan +snapshot_date: 2026-08-05 +--- + +# PixelForge × OpenGame 验收门、预警与冲突 + +**结论:当前可以开始施工,但不能宣称“已融合”或直接承接生产流量;真 API、PostgreSQL、Revision、BoxLite、CAS、媒体版本和跨端 Feed 安全都是 P0。** + +## 1. 当前断点与源码证据 + +| # | 断点 | 当前证据 | 严重度 | 关闭标准 | +| --- | ------------------------------------- | -------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------- | +| 1 | Web 无环境闸启动 MSW | `pixelforge-web/web/src/main.tsx:50-52` | P0 | production bundle/启动不注册 MSW,真后端 smoke 命中 Go API | +| 2 | Go 只有基础生成/读取面 | `opengame-server/internal/api/api.go:52-58` | P0 | Catalog/Revision/Job/Mod/Ops/Media/Auth 最小合同落地 | +| 3 | SQLite 是当前 store | `opengame-server/internal/store/store.go:1-16`、`go.mod:9` | P0 | production 强制 PostgreSQL;迁移与恢复演练通过 | +| 4 | 默认继承主进程环境 | `workspace.go:168-176` | P0 | sandbox env 从空白安全基线构造,只加任务级白名单 | +| 5 | BoxLite 尚未接入 | 当前 workspace 仍组装本机目录并启动 engine;无 BoxLite runner | P0 | 固定安全 release,隔离矩阵全绿,生产开关才能启用 | +| 6 | preview/thumbnail 只绑定 gameId | `uploads.ts:22-73` | P0 | 上传返回 MediaAsset,强制绑定 revisionId/artifactHash | +| 7 | GM 仍走 `/tuning` + `/ops/apply` | `use-tuning-surface.ts:83-90` | P0 | 只保留 `/api/ops/apply` 权威写入,旧路径不产生 Revision | +| 8 | Web 视频与静音错误耦合 | `RuntimeSurfaces.tsx:264-271` | P0 | `mode=video`/preview 决定播放,元素固定 muted;静音开关不阻止视频 | +| 9 | Android preview policy fail-open | `FeedAttractPreviewPolicy.kt:3-15` | P0 | 缺策略默认 false;只有显式 attract+safe 才执行 | +| 10 | 原生 DTO 缺 cover 三字段 | `MobileGame.swift:105-161`、`MobileContracts.kt:411-447` | P0 | iOS/Android 解码并按方向消费 cover/coverPortrait/heroCover | +| 11 | Web 静态 Feed 只优先 thumbnail | `gameViews.ts:56-65` | P0 | Web/iOS/Android 同 fixture:portrait MP4→coverPortrait→thumbnail→plate | +| 12 | OpenGame manifest 信息不足 | `generate-assets-types.ts:136-145` | P0 | 加 hash/size/source/license/lineage/revision/status 或转成 Artifact manifest | +| 13 | asset key 未形成安全文件名合同 | `generate-assets-types.ts:47-50`、`generate-assets.ts:1453-1455` | P0 | slug 校验 + resolved path containment 负例通过 | +| 14 | 损坏 manifest 被当空对象 | `generate-assets.ts:1424-1430` | P0 | 不存在与损坏分开;临时文件 fsync/rename 原子替换 | +| 15 | 部分失败仍返回成功式结果 | `generate-assets.ts:493-508,532-535` | P0 | machine-readable item results;required 失败使 Job/Revision 不 ready | +| 16 | 3D v1 只支持基础几何和贴图 | `generate-gdd.ts:629-640` | 已接受边界 | 文案/合同不承诺 GLB/text-to-3D;独立里程碑跟踪 | +| 17 | output_dir 与 provider URL 缺完整边界 | `generate-assets.ts:416-420,1433-1438,1545-1547` | P0 | output/key containment、重复 key、host/MIME/size allowlist 与 SSRF 负例通过 | +| 18 | provider 取消与结构化账单不贯通 | `assetModelRouter.ts:125-173` | P0 | AbortSignal 贯穿;item result 含 requestId/taskId/usage/cost/failureCode | +| 19 | OpenGame 尚无 MediaJob/宣传图生产者 | 现有请求仅 background/image/animation/audio/tileset:`generate-assets-types.ts:47-110` | P0 | 新 MediaJob 从截图证据产 set/assets/attempts,并通过媒体角色验收 | +| 20 | FFmpeg 是隐式能力而非部署门 | I2V/抽帧/音频路径依赖 FFmpeg:`generate-assets.ts:658-755,823-849` | P0 | 固定版本/checksum/codec probe;缺失时 I2V 关闭且状态可见 | +| 21 | Vite SPA 无动态 OG HTML 生产面 | 当前只有客户端壳;无 `/g/:slug` 服务端 HTML | P0 | Go/edge 返回 crawler HTML,发布/回滚 purge 后映射正确 hash | + +“当前代码未发现”只代表 2026-08-05 这三个 checkout 的快照;施工前仍需在目标 branch 复核。 + +## 2. P0 放行门 + +| Gate | 必须完成 | 可执行验收 | 不通过时 | +| --------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------- | +| G0 合同 | OpenAPI/DTO/错误码/完整媒体字段/Mock 同源 | Go/TS/Swift/Kotlin 同 fixture decode 与降级决策 | 禁止跨仓并行实现 | +| G1 真后端 | MSW 环境闸、CORS/auth、real smoke | production 请求命中真实 API,Mock 请求数为 0 | 不做 UI“联调完成”结论 | +| G2 数据 | PostgreSQL、迁移、owner、幂等 | 多实例并发写、备份恢复、生产 SQLite 拒启 | 不接公开用户 | +| G3 文件/Revision | candidate/head/immutable Revision、双发布指针、对象存储、CAS | QA 前无 Revision;路径/hash/租户/并发/回滚负例全过 | 不开放 Mod/发布 | +| G4 Job/Event | lease、REST cursor+SSE、worker I/O、provider cancel/usage、FFmpeg preflight | kill/断线/过期 cursor 可恢复;required 失败不假成功 | 不开放付费生成 | +| G5 沙箱 | BoxLite 固定版、profile operation、OpenGame 落盘/执行入沙箱、env/network/resource 隔离 | 用户 command、逃逸、DNS/TCP/UDP/alias、fork bomb 全拒绝 | 生产 sandbox flag 保持关闭 | +| G6 Create | 2D+3D v1 真生成/QA/预览 | 从 prompt 到 ready Revision 可重放 | 不开放 Create | +| G7 Mod/Runtime Tuning | single write、head CAS、带 session/nonce/origin 的 ack | stale base/假 ack/重放/并发修改负例通过 | 只读展示,不开放保存 | +| G8 发布/媒体 | 新 MediaJob、Attempt、审核、双指针原子切换、`/g/:slug` | 失败/驳回/回滚仍服务上一批准版本和 OG hash | 不展示新 Revision | +| G9 跨端 Feed | 策略、完整媒体 DTO、MP4/封面降级 | Web/iOS/Android 执行同一 `safe attract→MP4→portrait→thumbnail→plate` | 三端退全静态 | +| G10 账务 | order/webhook/ledger/usage 对账 | 重放 10 次 webhook/Job 仍只入账一次 | 停充值与付费任务 | +| G11 运维 | metrics、告警、PITR、runbook | 故障演练和值班人员按文档恢复 | 不超过内部灰度 | + +## 3. P1 完整度门 + +| 领域 | P1 能力 | 完成判定 | +| ---------------- | ------------------------------------------ | ------------------------------------------------------------ | +| 搜索/发现 | 标签、搜索、精选策略、稳定游标 | 下架不可检索;分页不重不漏 | +| 社区 | 点赞、收藏、关注、评论、举报、通知 | 幂等、限流、审核、已读游标通过 | +| 美术工作台 | regenerate/focal/safe-area/lock commands | artist 不可 approve/publish;每次生成 attempt 可追溯 | +| Operations Admin | approve/reject/feature/takedown/refund/ban | 与 Runtime Tuning 分权;操作人、原因、版本、前后 hash 可审计 | +| 前端性能 | `srcset/sizes`、按尺寸解码、缓存 | Feed 100 卡无内存持续增长,弱网可用 | +| 推荐 | 先规则/精选/热度 | 只有业务指标证明需要才上复杂推荐系统 | +| Hero | 精选游戏 1800×600、3:1±2%、≤500KiB | 404 退 cover;旧 fixture/资产版权清理;普通作品不阻塞 | + +## 4. 必跑端到端旅程 + +| J | 旅程 | 通过条件 | +| --- | --------------------------------------------- | ------------------------------------------------------------------------------- | +| J1 | 新用户登录→Create→刷新→预览 | QA 前只有 candidate;通过后同一 Job 创建一个 Revision,未重复扣费 | +| J2 | 两个用户并发 Create | workspace、事件、文件、用量完全隔离 | +| J3 | 重复点击/网络重试 Create | 同幂等键只执行一次 provider 调用 | +| J4 | Create 中取消/超时/worker 崩溃 | 无残留进程;状态可恢复且余额正确 | +| J5 | 3D v1 创建 | primitive+texture 游戏可玩,触控不被错误承诺 | +| J6 | 两人从同一 RevisionHead/version 同时 Mod | 一人成功、一人明确 conflict;自然语言 patch 重跑,不静默自动重放 | +| J7 | Runtime Tuning→伪 origin/重放 ack→真 ack→保存 | session/nonce/seq/expiry/origin 任一不合 fail-closed;真保存返回 nextRevisionId | +| J8 | Art Swap 使用跨租户/篡改 assetId | 403/422,沙箱未启动、无扣费 | +| J9 | QA 失败或宣传图审核拒绝 | 线上仍是旧 Revision/旧媒体,无半成品泄漏 | +| J10 | 发布→首页/详情→`/g/:slug`→Feed | Web/iOS/Android/无 JS crawler 引用同一 Revision/MediaAssetSet | +| J11 | 回滚 | 两个 published 指针、artifact、封面、视频、OG 一起回到原 hash | +| J12 | Android 缺 PreviewPolicy | 不加载 attract,显示 MP4 或静态兜底 | +| J13 | Reduce Motion/省流/弱网 | 不自动运行;video 失败退 portrait/thumbnail | +| J14 | 支付 webhook 重放/乱序/退款 | 账本唯一、余额正确、订单状态可对账 | +| J15 | Redis/SSE 故障 | REST cursor 能查进度和终态,业务正确性不丢 | +| J16 | FFmpeg/provider 不支持所需能力 | I2V 明确关闭或 Job 结构化失败,不静默生成错误格式 | +| J17 | Hero/cover URL 404 | Hero→cover→品牌底板;Feed→portrait→thumbnail→plate,无空白卡 | + +## 5. 5–10 万 DAU 容量门 + +容量模型先按 **DAU 50,000–100,000、峰值同时在线 5%、即 2,500–5,000 在线**;实测用户曲线出来后替换假设。 + +| 指标 | 放行线 | 测试方式 | 超标优先动作 | +| ---------------- | ------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------- | +| Catalog/Feed API | p95 < 300ms,p99 < 800ms | 真实分页/权限/计数数据压测 | 索引、批量查询、缓存热门页 | +| 写 API | p95 < 500ms(异步任务仅入队) | like/save/create/order 混合流量 | 幂等索引、连接池、减少事务范围 | +| 错误率 | < 0.5%,账务/发布错误为 0 | 稳态+故障注入 | 回滚、熔断昂贵入口 | +| DB | 无长事务/锁风暴,连接低于上限 80% | `pg_stat_statements`、慢查询门 | 索引/查询修复,再考虑只读副本 | +| Job 入队 | p95 < 200ms | Create 20/min、Mod 80/min、Media 10/min 持续;10min 突发 60/200/30 | worker 扩容,不在 API 同步执行 | +| Job 正确性 | 0 丢失、0 重复扣费 | kill/restart/lease expiry/重复投递 | 修幂等和 lease,不靠人工对账兜底 | +| Event 恢复 | 断线后 5s 内恢复;≤200 events/job、单条≤32KiB | Last-Event-ID、慢消费者、7 天过期/410 snapshot | 回源 REST cursor/snapshot | +| Feed 视频并发 | 每客户端最多当前卡 1 路 | 连滑 100 卡、前后台切换 | 取消旧请求、释放 video/iframe | +| Feed 首屏 | poster 可见 p75 < 1.5s(目标网络档) | 冷 CDN、弱网、低端机 | 图片尺寸、CDN、预连接 | +| CDN 命中 | immutable media > 95% | 真实 key 和回源日志 | cache header/key 策略 | +| 沙箱/Provider | worker 数按到达量×p95秒数公式;quota≤80%;队列 ETA 可见 | 每 tenant Create 2、Mod 4 并发;provider/FFmpeg 分池 | 公平队列、限额,不静默换模型 | +| 视频/流量 | 每方向 MP4≤8MiB;API 0 热视频代理 | CDN 冷/热缓存、范围请求、回源日志 | 调码率/尺寸/CDN,不加 HLS 首发 | +| Soak | 峰值 2h + 50% 峰值 24h 无 lease/RSS/DB/event 持续增长 | worker kill/restart、Redis/SSE/Provider 抖动 | 不扩流,修泄漏/retention | +| 恢复 | PostgreSQL RPO≤5min、RTO≤60min(首发目标) | 备份恢复演练 | 达不到则不接商业流量 | + +## 6. 安全验收 + +| 边界 | 必测 | 通过标准 | +| ------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------- | +| API | JWT 伪造、过期、错 audience、IDOR、CORS、CSRF | 所有写入先认证授权;错误不泄露内部路径 | +| Upload | MIME 欺骗、超限、zip bomb、hash、路径、跨租户 | 校验失败不落公开对象、不启动 Job | +| Artifact | output_dir/key 的 `../`、绝对路径、symlink、重复 key、恶意 HTML/CSP | resolved containment;原子 manifest;入口只加载允许资源并运行于隔离域 | +| BoxLite | 用户 command/入口替换、文件/设备/进程/网络/host alias/DNS/UDP | 仅 profileId+operation;默认拒绝,策略不可由 prompt 放宽 | +| Provider | SSRF URL、假 MIME、超大响应、取消失效、错误体含密钥 | host/MIME/size allowlist;AbortSignal 生效;日志脱敏 | +| Ops/Mod | schema 绕过、旧 Revision、越权 asset | CAS/owner/schema 任一失败则 0 写入 | +| Media | NSFW、侵权、提示词注入、未审核 URL | 公开集合只接受 approved;lineage 可追责 | +| Payment | 假 webhook、重放、金额篡改、乱序退款、Job 冻结失败 | 验签+providerEventId 唯一;Job+reservation+outbox 同事务;账本可对账 | +| Admin | 越权精选/下架/退款/封禁 | RBAC、二次确认、immutable audit log | +| Supply chain | BoxLite/FFmpeg/sharp/image checksum/SBOM | 固定版本和 checksum;高危漏洞有升级路径 | + +## 7. 风险登记 + +| 风险 | 概率 | 影响 | 预警信号 | 预防/缓解 | 回滚 | +| ---------------------------- | ---- | ---- | -------------------------------------- | ------------------------------------------------------ | ------------------------------- | +| BoxLite 修复迟迟未发布 | 中 | 高 | 最新 release 不含 #1090/#1106 | adapter 先行、生产开关关闭、持续看 release | 不开放不可信任务 | +| 沙箱网络策略仍可绕过 | 中 | 极高 | UDP/alias/DNS 负例成功 | 默认无网、代理票据、红队门 | 全局关闭 sandbox | +| SQLite→PG 语义漂移 | 中 | 高 | 测试绿但生产 SQL/事务失败 | 生产同引擎集成测试、一次性迁移 | 回应用版本,保留 PG 数据 | +| API `/api` 与 `/v1` 双逻辑 | 高 | 高 | 同动作产生不同状态/Revision | 两套路由调用同一 service;新功能只写一次 | 关闭兼容路由写面 | +| Runtime Tuning 假/重放 ack | 高 | 高 | UI 显示 applied 但玩法无变化 | session+nonce+origin+seq+expiry+metrics 全校验 | 禁用保存,只留预览 | +| 并行 Mod 覆盖 | 高 | 高 | head/version 与 base 不一致仍成功 | head CAS;仅 typed Ops 重放;自然语言 patch 重跑 | 保留旧 Revision,撤回新 head | +| 媒体与 Revision 错配 | 高 | 高 | 回滚后封面/视频仍是新版 | 两个 published 指针同事务并校验 revisionId | 固定上一 approved set | +| Provider 部分失败/取消假成功 | 高 | 高 | Errors>0、cancel 后仍计费或进入 ready | required gate、AbortSignal、结构化 item/usage | candidate 失败,不创建 Revision | +| OpenGame 在宿主落盘/执行 | 中 | 极高 | npm/build/Mod 出现在主进程树 | 只下发 BoxLite profile/operation,宿主验 manifest/hash | 关闭生产 runner | +| FFmpeg 能力漂移 | 中 | 高 | 开发可用、镜像缺 codec,I2V/MP4 才失败 | 固定版本/checksum/codec probe;无能力关 I2V | 退静态媒体 | +| Feed 自动运行不安全作品 | 中 | 极高 | 缺 policy 仍创建 iframe | 所有端 fail-closed、server 下发 policy | 全局 attract kill switch | +| 移动视频带宽/内存过高 | 中 | 高 | 邻卡下载、OOM、掉帧 | 单视频、poster 邻卡、生命周期释放 | 全局退静态封面 | +| 充值重复入账 | 中 | 极高 | webhook 重放余额增长 | providerEventId/ledger 幂等唯一约束 | 停新单、冻结异常余额 | +| ShipAny 整体融合诱发双后端 | 中 | 高 | Next.js/Go 同时写用户订单 | 只移植 UI/流程,不运行第二权威 | 删除未接入模块 | +| 过早引入 Redis/队列平台 | 中 | 中 | 正确性依赖缓存、运维增多 | PG 先行,指标触发升级 | 关闭缓存回源 PG | +| 封面生成成本失控 | 中 | 中 | 每次调参触发多图生成 | 仅 publish candidate;横竖两母版 | 停 AI,退实机截图 | +| 3D 能力宣传超出现状 | 高 | 中 | 需求出现 GLB/换装但仍排 v1 | 明示边界,独立里程碑 | 降级到 primitive+texture | + +## 8. 需要与用户讨论的冲突/门 + +| 决策门 | 当前建议 | 何时必须确认 | 不确认的默认动作 | +| ----------------------- | ----------------------------------- | ----------------------- | ----------------------------------------------------------- | +| 首发市场与身份 provider | 标准 OIDC,按首发地区选托管商 | Step 2 开始前 | 仅测试 issuer 做内部联调;真实 OIDC 未接前不开放公共 Create | +| 充值渠道 | 海外默认 Stripe;大陆需另做合规选型 | Step 9 账务施工前 | 不开放充值,只保留免费额度/人工赠送测试账 | +| BoxLite release 时间 | 等包含 #1090/#1106 的正式 release | Step 5 生产开关前 | adapter 合并但始终关闭 | +| 3D 模型资产里程碑 | 与当前融合解耦 | 产品开始承诺 GLB/换装前 | 继续维持 v1 primitive+texture 边界 | +| Redis 是否首发 | 由容量压测决定 | Step 10 | 不引入;REST cursor + PG 正确运行 | +| 创作者分成/提现 | 另行法务、税务、KYC 方案 | 任何提现/转赠需求前 | 积分只能站内消费,不可提现/转赠 | + +其余核心选择已冻结;不得在施工中重新比较腾讯沙箱、Temporal、重型 DAM 或整体迁入 ShipAny。 + +## 9. 灰度 Go/No-Go + +| 阶段 | Go 条件 | No-Go 条件 | +| ----------------------- | --------------------------- | ----------------------------------------------------------- | +| 开发联调 | G0–G4 | MSW 仍无闸、PG/幂等未完成 | +| 内部 Create | G0–G6 | BoxLite 安全负例任一失败 | +| 内部 Mod/Runtime Tuning | G0–G7 | 双写、假/重放 ack、head CAS 覆盖任一存在 | +| 媒体/Feed 灰度 | G0–G9 | MediaJob 缺失、双指针错版、Android fail-open、三端 DTO 漂移 | +| 商业灰度 | G0–G11 + 账务对账 | 重复扣费/入账、备份未演练、无告警 | +| 5–10 万 DAU 扩流 | 容量门全过,连续观察期无 P0 | 靠手工扩容、单机 SQLite、API 代理视频 | + +## 10. 每次验收必须留下的证据 + +| 类别 | 最小证据 | +| ---- | -------------------------------------------------------------- | +| 合同 | schema SHA、四端 fixture decode 结果 | +| 代码 | commit SHA、目标测试、build/lint 输出 | +| 旅程 | final-SHA 真 API E2E、requestId/jobId/revisionId | +| 沙箱 | BoxLite release/checksum、负例矩阵、资源指标 | +| 数据 | migration version、行数/hash 对账、恢复演练时间 | +| 媒体 | revision/artifact/media hash、审核结果、多端截图/视频 metadata | +| 账务 | provider event/order/ledger/usage 四方对账 | +| 容量 | 测试模型、数据量、并发、p50/p95/p99、错误率和瓶颈证据 | + +聚焦测试、Mock E2E 或“agent 报告完成”都不能替代 final-SHA 真后端全旅程证据。 diff --git a/docs/pixelforge-integration/architecture-and-selections.md b/docs/pixelforge-integration/architecture-and-selections.md new file mode 100644 index 000000000..2983d0b55 --- /dev/null +++ b/docs/pixelforge-integration/architecture-and-selections.md @@ -0,0 +1,205 @@ +--- +title: PixelForge × OpenGame 架构与选型 +status: accepted-working-plan +snapshot_date: 2026-08-05 +--- + +# PixelForge × OpenGame 架构与选型 + +**结论:采用“一个 Go 控制面 + 一个 OpenGame 生成内核 + BoxLite 隔离执行 + PostgreSQL/对象存储权威”的最小缝合,不再叠加工作流平台、美术微服务或第二套 SaaS 后端。** + +## 1. 目标架构 + +```mermaid +flowchart LR + C[Web / iOS / Android] -->|/api + REST cursor/SSE| G[opengame-server] + G --> P[(PostgreSQL)] + G --> R[(Redis 可选\n缓存/限流/pubsub)] + G --> O[(S3-compatible\n对象存储 + CDN)] + G --> J[PostgreSQL Job Lease Worker] + G --> A[Scoped Provider Proxy] + subgraph B[BoxLite micro-VM] + OG[OpenGame fixed operation\n生成/落盘/build/Mod/QA/媒体] + end + J -->|profileId + structured args| OG + OG -->|manifest/hash/evidence| J + OG -->|短期 provider ticket| A + A --> AI[通义 / 豆包 / OpenAI-compatible] + OG -->|单 Job upload ticket| O + O -->|immutable artifact/media| C + PAY[OIDC / Payment Provider] -->|token / signed webhook| G +``` + +| 设计原则 | 落法 | +| -------------------- | ------------------------------------------------------------------------------------------------------- | +| 业务权威只有一处 | PostgreSQL 保存指针、状态、关系、收入与审计;对象存储保存大文件 | +| 不可信代码不进主进程 | OpenGame 的项目落盘、依赖安装、build、Mod、QA、截图/转码均在 BoxLite;Go 只下发固定 operation 并验 hash | +| 实时不是权威 | v1 固定 REST cursor + SSE;Redis 只加速广播,Event 可从 PostgreSQL 重放 | +| 媒体跟版本走 | `publishedRevisionId + publishedMediaAssetSetId` 必须同事务切换且属于同一 Revision | +| 兼容不复制业务 | `/api/*` 为 PixelForge 合同;旧 `/v1/*` 只适配到同一 service | +| 缓存可失效 | Redis 故障只影响速度/实时性,不影响 Job、账本和发布正确性 | + +## 2. 已采纳选型总表 + +| 领域 | 选型 | 复用/新增 | 复杂度 | 主要风险 | 控制办法 | +| ----------- | -------------------------------------------- | ---------------------------- | ------ | ---------------------------------------- | -------------------------------------------------------------- | +| HTTP 控制面 | Go `net/http` + 现有 opengame-server | 复用 | 低 | 路由增多后 handler 膨胀 | 按 domain service 分包,不换 Web 框架 | +| 公共合同 | OpenAPI/JSON Schema + 生成 DTO | 新增合同 | 中 | Web/iOS/Android 漂移 | CI 比较 schema 和 fixture;Mock 同源 | +| 生产数据库 | PostgreSQL | 新增生产 adapter | 中 | 双库迁移、索引不足 | SQLite 仅 dev;一次性迁移,生产无双写 | +| 开发账本 | SQLite | 保留 | 低 | 被误用于生产 | 配置在 production 拒绝 SQLite DSN | +| Job 队列 | PostgreSQL `FOR UPDATE SKIP LOCKED` lease | 新增最小表/worker | 中 | 长任务 lease、重复执行 | heartbeat、幂等键、attempt、可恢复终态 | +| 缓存/实时 | Redis,**仅生产按压测启用** | 条件新增 | 中 | 变成隐藏权威 | 仅 rate limit/cache/pubsub;所有状态可回源 | +| 沙箱 | BoxLite | 新增 adapter | 中高 | release 尚未含已合并网络修复 | feature flag;固定版本;安全验收后启用 | +| 文件 | S3-compatible 对象存储 + CDN | 新增生产配置 | 中 | 越权、脏对象、热链路成本 | 预签名、tenant prefix、hash、生命周期策略 | +| 图片 | `sharp`,补为 `packages/core` 直接依赖 | 复用现有实现 | 低 | 当前只由 `@imgly` 间接带入,部署可能缺包 | 固定 manifest/lockfile;CI 做一张真实图片转换 | +| 视频 | 固定 FFmpeg CLI/镜像 capability | 补齐运行时声明 | 中 | 当前 I2V/抽帧已依赖但未形成部署门 | Step 4 preflight;缺失则关 I2V;Step 8 固定 H.264 profile | +| AI 美术 | OpenGame 通义/豆包/OpenAI-compatible adapter | 复用并补 capability metadata | 中 | provider 差异、静默降级、取消不贯通 | sizes/edit/video/cancel 显式匹配,记录 requestId/成本/降级原因 | +| 媒体编排 | 现有 Job/worker 增加 media job | 复用扩展 | 中 | 旧任务覆盖新版本 | Revision+hash+policyVersion 幂等键 | +| 鉴权 | 托管 OIDC 优先,Go 验 JWT | 新增边界 | 中 | 地区可用性、vendor lock | 标准 OIDC;User 只镜像 subject,不存 provider 密码 | +| 充值 | 首发单一 Payment Provider + Go webhook | 新增边界 | 中高 | 重复入账、退款、合规 | 验签、幂等、追加式 ledger、服务端价格表 | +| 社区 | PostgreSQL 关系表 + 游标 | 新增 | 中 | 热计数、刷量 | 幂等唯一键、异步聚合、rate limit | +| 可观测性 | 结构化日志 + OpenTelemetry/Prometheus | 复用 Go 生态 | 中 | 高基数/敏感数据 | request/job/tenant 三主键;字段白名单 | + +## 3. BoxLite 决策 + +| 项目 | 结论 | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 选型 | **BoxLite**,Apache-2.0,面向 agent 的嵌入式 micro-VM | +| 官方仓库 | | +| 2026-08-05 已发布版本 | `v0.9.7`,发布时间 2026-07-01 | +| 已合并但未进该 release 的修复 | [PR #1090 UDP egress allow_net](https://github.com/boxlite-ai/boxlite/pull/1090)、[PR #1106 host alias allow_net](https://github.com/boxlite-ai/boxlite/pull/1106) | +| 当前施工策略 | 先实现 profile-based BoxLite runner、策略和验收;生产开关保持关闭 | +| 生产放行条件 | 正式 release 包含两项修复;固定 checksum;网络/密钥/资源/逃逸测试全绿 | +| 回退 | 禁止不可信远端任务;开发环境可使用受限本机 runner,但不得称为安全沙箱 | + +### BoxLite 最小接入合同 + +| 输入 | 输出 | 强制策略 | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------- | +| `jobId, tenantId, inputRefs, profileId, operation, structuredArgs, timeout, networkPolicy` | `exit, stdoutRef, stderrRef, outputManifest, metrics, failureCode` | 不接收用户 shell command;CPU/内存/PID/磁盘/墙钟限制 | +| 只读基础镜像 + 独立 writable workspace | 仅上传 manifest 声明的产物 | 禁止挂载宿主仓库、Docker socket、用户目录 | +| provider 短期凭据或代理票据 | 凭据不出现在日志和 artifact | DB/Auth/对象存储主密钥永不注入 | +| 默认无网 | 精确域名/IP/端口 allowlist | DNS、TCP、UDP、host alias 都必须测试 | + +## 4. 数据、任务与实时状态 + +### 4.1 PostgreSQL 与 SQLite + +| 选项 | 优点 | 代价 | 建议 | +| ------------------- | ----------------------------------------------- | -------------------------------------- | ------------------------------ | +| PostgreSQL 生产权威 | 并发、事务、索引、JSON、`SKIP LOCKED`、备份成熟 | 需要迁移与运维 | **采用** | +| SQLite 生产主库 | 单机简单 | 多实例、热写、备份、队列、社区关系受限 | **排除** | +| SQLite 本地模式 | 单二进制、测试快 | 与生产 SQL 有差异 | **保留,但生产配置 fail-fast** | + +生产不做 SQLite→PostgreSQL 长期双写。迁移期先停止写入、导入、校验计数/hash,再切 DSN;回滚只回服务版本,不回到双主。 + +### 4.2 Job 与事件 + +| 能力 | 最小实现 | 何时升级 | +| ---------- | --------------------------------------------- | ------------------------------ | +| Job 领取 | PostgreSQL lease + `SKIP LOCKED` | 单库成为实测瓶颈时再拆专用队列 | +| Job 恢复 | heartbeat/leaseUntil/attempt/maxAttempts | 不引入全量工作流 DSL | +| 事件 | events 表顺序号 + REST cursor | Redis pubsub 只为低延迟广播 | +| 跨表一致性 | 同事务写业务行 + outbox/event | 外部 webhook/对象删除异步消费 | +| 取消 | Job cancel_requested,worker 协作终止 BoxLite | 终态后取消返回幂等结果 | + +## 5. 媒体、美术与视频 + +| 问题 | 当前最小方案 | 不做的事 | +| ---------- | ----------------------------------------------- | ---------------------------- | +| 多比例 | AI 产横/竖两个无字母版;sharp 派生 thumbnail/OG | 不独立调用 AI 生成四张图 | +| 标题乱码 | 确定性文字叠加、本地化字体和 safe area | 不让 AI 直接画关键文字 | +| Hero 成本 | 仅精选游戏触发 3:1,允许人工锁版 | 不为每个发布版本生成 Hero | +| 实机一致性 | 最终 QA 后取标题页/关键场景截图 + creative spec | 不只根据最初 Prompt 生宣传图 | +| 修改频率 | 仅 publish candidate 或人工重生触发 | 不随每次实时试调重生 | +| 视频 | FFmpeg 产 4–6 秒 H.264 MP4、Fast Start | 首版不做 HLS,不要求 WebM | +| 审核 | prompt 前审 + 生成后图像/视频审 + lineage | 不先公开 URL 再补审核 | + +OpenGame 当前由 `@imgly/background-removal-node` 间接带入 `sharp ~0.32.4`(lockfile 为 `0.32.6`),源码却已直接 import。实施时必须把 `sharp` 写入 `packages/core/package.json` 并锁版本;这是修正依赖声明,不另选图片库。 + +当前 OpenGame 没有 cover/hero/OG/screenshot/`MediaAssetSet` 生产者,下面是**必须新增**的最小 MediaJob,不得把目标合同写成现状能力。 + +| MediaJob | 合同 | +| --------- | ------------------------------------------------------------------------------------------------------ | +| 输入 | `revisionId, artifactHash, screenshotEvidence[], creativeSpec, roles[], policyVersion, idempotencyKey` | +| 处理 | BoxLite 内截图/FFmpeg;provider adapter 产横竖母版;sharp 派生;逐项审核 | +| 输出 | `MediaAssetSet + MediaAsset[] + MediaGenerationAttempt[] + itemResults[]` | +| 必需/可选 | cover、coverPortrait、thumbnail、OG、portrait MP4 为发布必需;Hero 仅精选必需;landscape MP4 可选 | +| 原子性 | Job 只产 draft/approved set;发布事务校验 set.revisionId 后同时切两个 published 指针 | + +动态 OG 不由 Vite SPA 猜测。反向代理把 `/g/:slug` 交给 Go/edge 输出可抓取 HTML,并在发布/回滚时按 slug purge;HTML 再加载或跳转 PixelForge 客户端页面。 + +## 6. 账户、充值与 ShipAny + +PixelForge 是 **Vite + React 19 SPA**,opengame-server 是 Go。ShipAny One 当前是 **Next.js 15 + NextAuth + Drizzle + Stripe/Creem**,整体合并会制造第二套服务端和数据层。 + +| 方案 | 优点 | 代价/冲突 | 建议 | +| ------------------------ | ----------------------------------------- | --------------------------------------------------- | ---------------------------- | +| 整体迁入 ShipAny | 账户、支付、后台页面现成 | Vite→Next 迁移;Go/Drizzle 双权威;路由和 UI 大重构 | **排除** | +| 购买后选择性移植 | 可借表结构、页面、邮件、订单/webhook 流程 | 仍需改成 PixelForge API 与设计系统 | **采用为参考,不作运行依赖** | +| Go 自研密码登录 | 单栈 | 安全、找回、MFA、风控维护成本高 | **排除** | +| 标准 OIDC + Go 用户镜像 | 三端通用、可替换 provider | 仍需选地区可用 provider | **采用** | +| 单支付 provider + ledger | 最小上线面、易对账 | 第二地区支付需后续扩展 | **采用** | + +采购前只核对能否合法复用代码、UI 与数据库设计;不要因为“已购买”而迁入无调用方的模块。 + +### 必须在商业施工前确认 + +| 决策 | 默认值 | 改变默认值的条件 | +| ------------- | -------------------- | -------------------------------------------------------- | +| 首发市场 | 海外优先 | 若首发中国大陆,支付和身份 provider 必须重选并做合规评审 | +| 支付 provider | Stripe(海外默认) | 商务明确要求 Creem/微信/支付宝时再替换,不首发多接 | +| 虚拟积分 | 仅平台内消费,不提现 | 若涉及转赠/提现/创作者分成,先做法务、税务和 KYC 方案 | + +## 7. Feed 与 5–10 万 DAU + +| 层 | 选型 | 复杂度控制 | +| --------- | ------------------------------------------------- | ------------------------------------------ | +| Feed 查询 | PostgreSQL cursor + 预计算计数;热门页短 TTL 缓存 | 不首发推荐系统平台 | +| 静态媒体 | CDN + 响应式图片 | API 不转发视频字节 | +| 视频 | 当前卡播放,前后卡只 poster/metadata | 不预取相邻 iframe/runtime | +| attract | 显式 `mode=attract && attractSafe=true` | 缺策略/省流/Reduce Motion 退静态 | +| 实时事件 | v1 固定 REST cursor + SSE;多实例时 Redis pubsub | WebSocket 后置,避免同时维护两套客户端合同 | +| 限流 | user/IP/tenant/job-type 令牌桶 | Redis 不可用时本机保守限流或拒绝昂贵任务 | + +5–10 万 DAU 不要求首日上 Kafka、服务网格或多区域数据库;它要求先把无状态 API、CDN、PostgreSQL 索引、幂等和压测做实。 + +## 8. 候选与排除项 + +| 候选 | 状态 | 排除/降级理由 | 重新评估触发器 | +| ------------------------- | -------------- | ------------------------------------------------ | ------------------------------------------------ | +| 腾讯云 AGSX / CubeSandbox | **排除** | 用户明确不依赖腾讯;供应商锁定和部署边界不合要求 | 无,除非产品战略主动改变 | +| Docker 单独作安全沙箱 | **排除** | 容器不是足够的不可信代码安全边界 | 仅本地可信构建可用 | +| Firecracker / Kata | **排除首发** | 运维、镜像、网络和平台复杂度高 | BoxLite 无法满足隔离/吞吐验收 | +| gVisor | **备选** | Linux 容器栈复杂度高于当前方案 | BoxLite release/性能长期不达标 | +| E2B/远程沙箱 SaaS | **排除首发** | 外部依赖、成本、数据驻留和 vendor lock | 自建容量成为主要瓶颈且合规允许 | +| nsjail/bubblewrap | **不作主方案** | Linux 专用、能力拼装多、macOS 开发体验割裂 | 只作为 CI Linux 补充防线 | +| Temporal | **暂不引入** | 当前 Job 状态机可由 PG lease 覆盖 | 跨天人工审批/补偿流程显著增多且 PG 方案失控 | +| Kafka/NATS/RabbitMQ | **暂不引入** | 首发没有需要独立消息平台的实测吞吐 | outbox 消费延迟/积压超过 SLO | +| ComfyUI 生产集群 | **暂不引入** | 现有 provider adapter 足够;新增 GPU 编排过重 | 节点化可复现工作流成为明确产品需求 | +| 独立美术微服务 / 重型 DAM | **排除首发** | 版本、审核、任务已能在现有脊柱表达 | 专职美术团队和资产规模证明需要独立治理 | +| GitLab 作 Game/Mod 权威 | **排除** | 业务指针、lineage、收入和权限更适合 PostgreSQL | 多文件 diff/merge/audit 成为硬需求时只做异步镜像 | +| Redis 作 Job/余额权威 | **排除** | 故障恢复和事务边界不适合 | 无;Redis 始终只做可丢状态 | +| 每次修改重生宣传图 | **排除** | 成本高、抖动、旧任务覆盖风险 | 只允许用户显式重生或 publish candidate 触发 | +| 每版四张独立 AI 图 | **排除** | 风格漂移和成本放大 | 横/竖母版无法满足某特殊渠道时单独加 role | +| 短预览 HLS | **排除首发** | 4–6 秒 MP4 足够,切片/清单/缓存复杂度无收益 | 预览变成长视频或直播 | +| 全作品人工 GM 审批 | **排除** | 吞吐与成本不可扩展 | 高风险、举报、精选、商业活动才转人工 | + +## 9. 复杂度预算 + +| 阶段 | 允许新增的基础设施 | 明确禁止 | +| ---------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------- | +| 首个真闭环 | PostgreSQL、S3-compatible storage、真实 OIDC、BoxLite adapter、FFmpeg preflight(或明确关 I2V) | Redis、支付可暂不阻塞内部 Create | +| 移动媒体上线 | 固定 FFmpeg H.264 profile、CDN 转码队列 | HLS、独立媒体平台 | +| 5–10 万 DAU 压测 | Redis(若指标需要)、只读副本(若慢查询需要) | 未经压测直接上 Kafka/服务网格 | +| 商业上线 | 单支付 provider、ledger、对账 | 第二套 Next.js 后端、多支付并行首发 | + +## 10. 外部资料快照 + +| 来源 | 用途 | 注意 | +| ---------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------- | +| | BoxLite 版本、许可、release 与修复 | 能力必须绑定具体 release,不以 main 代替发布版 | +| | 腾讯方案对比 | 已排除,不是运行依赖 | +| | CubeSandbox 对比 | 已排除,不是范例实现 | +| | 账户、订单、积分、后台参考 | 只选择性移植,不整体并入 | +| | ShipAny 技术栈核验 | VIP/登录授权仓;未认证访问返回 404,采购/移植时核对许可并重新锁版本 | diff --git a/docs/pixelforge-integration/execution-plan.md b/docs/pixelforge-integration/execution-plan.md new file mode 100644 index 000000000..cb43b8ac6 --- /dev/null +++ b/docs/pixelforge-integration/execution-plan.md @@ -0,0 +1,180 @@ +--- +title: PixelForge × OpenGame 十步融合施工计划 +status: accepted-working-plan +snapshot_date: 2026-08-05 +--- + +# PixelForge × OpenGame 十步融合施工计划 + +**结论:严格按 1→10 推进;先统一合同和数据脊柱,再接生成与沙箱,最后才开放媒体、社区、充值和规模化流量。** + +## 1. 十步总表 + +| 步骤 | 解决的独立问题 | 依赖 | 主要输出 | 完成判定 | 回滚点 | +| ---- | ---------------------------------------- | ---- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------- | +| 1 | 真 API 合同与 Mock 分流 | 无 | OpenAPI/Schema、DTO、错误码、MSW 环境闸 | production 不注册 MSW;Web/iOS/Android/Go 合同测试一致 | 前端显式 `mock` 环境仍可工作 | +| 2 | 多用户、PostgreSQL、鉴权与幂等 | 1 | PG store、User/Session/Tenant/Game/Job 基表、真实 OIDC middleware | 吊销写请求立即拒绝;双用户并发不串;同幂等键只建一个 Job | 只回应用版本;迁移 forward-only,保留导出快照 | +| 3 | Revision、Artifact、上传与对象存储 | 2 | RevisionCandidate/Head/Revision/Artifact/Upload/Manifest、对象存储 | QA 后才建 Revision;产物按 hash 回读;跨租户拒绝 | 读旧本机 artifact adapter;新上传保持私有 | +| 4 | Job/worker、SSE 与 OpenGame 接线 | 2,3 | PG lease、REST cursor+SSE、版本化 worker I/O、provider capability/取消、FFmpeg preflight | 重启续跑;SSE 可恢复;required 失败不假成功;无 FFmpeg 时 I2V 明确关闭 | 停 worker,不影响已发布作品读取 | +| 5 | BoxLite 隔离执行 | 4 | profile-based runner、策略、资源/网络/密钥验收 | OpenGame 落盘/build/Mod/QA 在沙箱;宿主不接收用户 command | feature flag 关闭不可信远程执行 | +| 6 | Create→预览真闭环 | 4,5 | Generate API、阶段状态、QA、draft Revision、runtime URL | 关闭 MSW 后完整创建一款 2D 和一款 3D v1 游戏并可玩 | 新建流量回旧 Create;已产 Revision 可读 | +| 7 | Mod、Art Swap、Runtime Tuning 与直播状态 | 6 | ModJob、OpBatch、TuningSchema、head CAS、单写 `/api/ops/apply` | 冲突不写;有效 ack 才保存;成功产生可回滚 Revision | 关闭 Mod/Tuning 写开关,保留只读历史 | +| 8 | 媒体、宣传图、视频、审核与发布 | 6,7 | **新增 MediaJob**、MediaAssetSet/Attempt、sharp/FFmpeg、`/g/:slug` HTML、双指针发布 | 必需/可选角色清楚;媒体与 Revision 同发布同回滚 | 继续服务上一 approved 媒体集 | +| 9 | Feed、原生端、社区、账户与充值 | 2,8 | 移动 DTO/策略、social API、credit ledger、单支付 provider | 三端同数据;账务 0 重复;Feed 降级链通过 | 分模块 feature flag;支付停新单不影响余额查询 | +| 10 | 5–10 万 DAU、灾备、安全与切流 | 1–9 | 压测、安全报告、备份恢复、灰度看板、runbook | 全部 P0 和容量门通过,灰度可回滚且数据可对账 | 按 tenant/百分比切回旧读面与旧 published Revision | + +## 2. Step 1:合同冻结与 MSW 环境闸 + +| 子任务 | 仓库 | 输出 | 完成判定 | +| --------------------------------------- | --------------- | --------------------------------------------------------- | ---------------------------------------------------- | +| 盘点所有 `/api` 调用、DTO、Mock handler | pixelforge-web | endpoint/consumer 清单 | 每个前端写动作都映射到一个权威后端 command | +| 定义核心 Schema | opengame-server | Game/Revision/Job/Event/Op/Media/PreviewPolicy | 示例 JSON 可被 Go、TS、Swift、Kotlin 解码 | +| 统一错误与状态词表 | 三仓 | `code, retryable, stage, requestId` | UI 不再靠错误字符串猜状态 | +| 冻结实时合同 | 三仓 | REST cursor + SSE、Last-Event-ID、heartbeat、410 snapshot | 断线、慢消费者、cursor 过期均有确定恢复路径;WS 后置 | +| 给 MSW 加环境闸 | pixelforge-web | `mock/real` 启动策略 | build/production 中 Mock bundle 不接管请求 | +| 建合同 CI | 三仓 | schema diff + fixture decode | 任一端删改必填字段即 CI 失败 | + +**提交节点:**合同文件、Web MSW 闸、各端 DTO 生成/校验分三个可回滚 commit。 + +## 3. Step 2:生产数据脊柱与身份 + +| 子任务 | 最小实现 | 完成判定 | +| ------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- | +| PostgreSQL adapter | 复用 store/domain 边界,生产 DSN 强制 PG | production 配 SQLite 时启动失败并给明确错误 | +| User/Session/Tenant | 真实 OIDC subject 镜像 + status/roles/tokenVersion;access token≤15min | JWT 校验失败即 401;吊销 session 后下一次写请求拒绝 | +| 所有权 | Game/Job/Upload 带 tenantId/ownerId | 双租户负例无法枚举或读取对方资源 | +| 幂等 | endpoint scope + actor + Idempotency-Key 唯一 | 并发十次相同 Create 只产生一条收费 Job | +| 审计 | actor/action/target/before-after hash | 发布、封禁、退款、Runtime Tuning 保存均可追溯 | + +SQLite 仅保留 `dev` adapter,不实施长期双写。 + +## 4. Step 3:不可变 Revision 与文件链 + +| 子任务 | 最小实现 | 完成判定 | +| ---------------------- | -------------------------------------------------- | ------------------------------------------------------------ | +| 对象存储 | tenant/game/revision 内容寻址对象键 | 相同 hash 去重;上传后不可覆盖 | +| Upload | 预签名或服务端流式上传、MIME/大小/hash 校验 | 伪扩展名、超限、跨租户、hash 不符全部拒绝 | +| RevisionCandidate/Head | Create 先建 candidate;分支 head 带 version | QA 失败不产生 Revision;Mod/Ops CAS 精确比较 head version | +| Revision | parent/kind/status/artifactId | artifact+QA 通过后一次性创建;ready 后不可变 | +| 发布指针 | `publishedRevisionId + publishedMediaAssetSetId` | 两个指针同事务切换且媒体集属于该 Revision;回滚恢复同组 hash | +| Artifact manifest | entry、文件 hash/size/MIME、engine、build metadata | manifest 缺文件或越界引用不能 ready | +| 生命周期 | draft 私有、published 保留、孤儿延迟回收 | Job 失败不会生成公开 URL | + +## 5. Step 4:异步任务与 OpenGame + +| 子任务 | 最小实现 | 完成判定 | +| ------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------- | +| PG lease worker | queued→running→terminal、heartbeat、attempt | worker 异常退出后 lease 到期可恢复 | +| Event cursor/SSE | `(jobId, seq)` 唯一、Last-Event-ID、15s heartbeat、7 天保留、410 snapshot | 断线/慢消费者/cursor 过期均可恢复,不重复终态通知 | +| OpenGame 输入 | versioned `profileId+operation+structuredArgs`、refs、budget、deadline | 同输入合同可重放;不允许用户 shell command | +| OpenGame 输出 | itemResults、artifact manifest、usage、requestId/taskId、evidence、failureCode | required 失败时 Job 失败,不返回假成功 | +| Provider capability | sizes/edit/video/audio/cancel/maxBytes | 调用前匹配;不支持给明确降级,不静默换模型 | +| 取消/超时 | AbortSignal 贯穿 provider/download/FFmpeg/子进程,再终止 BoxLite | 超时后无残留 provider task/进程,状态固定为 timeout | +| FFmpeg preflight | 固定版本/checksum 和 `ffmpeg -version`/codec probe | 有 capability 才开 I2V;无则 Step 6 样本显式禁用 I2V | + +不引入 Temporal、Kafka 或新的 workflow DSL。 + +## 6. Step 5:BoxLite + +| 子任务 | 验收负例 | 完成判定 | +| ---------------------- | ------------------------------------------------- | -------------------------------------------------------------- | +| profile/operation 映射 | 传任意 shell、替换入口、注入结构化参数 | 只有服务端注册的固定入口可执行,用户无法提供 command | +| 环境白名单 | 读取 DB/Auth/S3 主密钥 | 沙箱中变量不存在,日志中也不出现 | +| 文件隔离 | output_dir/key 的 `../`/绝对路径/symlink/重复 key | resolved containment 生效,无法读写 workspace 外或覆盖同名产物 | +| 网络策略 | DNS/TCP/UDP/host alias 绕过 | 默认拒绝;只允许声明目标 | +| 下载/SSRF | provider 返回内网 URL、超大体、假 MIME | host/MIME/size allowlist 生效,响应不合规即失败 | +| 资源治理 | fork bomb、磁盘填满、无限循环 | PID/CPU/内存/磁盘/墙钟限制生效 | +| 版本供应链 | 篡改 binary/image/checksum | 固定 release/checksum/SBOM;来源可审计 | + +BoxLite 正式 release 未包含已合并修复前,只合入关闭状态的 adapter 和测试,不承接生产不可信任务。 + +## 7. Step 6:Create 真闭环 + +| 旅程阶段 | 前端表现 | 后端/worker | 完成判定 | +| -------- | ---------------------- | ------------------------------------------------------------------------ | -------------------------------------- | +| 提交 | 立即进入 queued | 202 + gameId + jobId + candidateId;**不创建 Revision** | 重试不重复创建 | +| 生产 | stage/percent/log 摘要 | OpenGame 生成并持续写 Event | 刷新/换端仍显示相同进度 | +| 失败 | 精确 stage、可重试提示 | failureCode/retryable/attempt | provider 配额与代码失败能区分 | +| QA/建版 | 显示验证步骤 | BoxLite build/smoke/gameplay;通过后一次性创建 Revision 并 CAS 推进 head | 必需门失败不生成任何 Revision | +| 预览 | 只加载 ready URL | 短期 runtime ticket | 2D 和 3D v1 均可交互、暂停、回报 ready | + +首批样本必须包含:2D、3D v1、provider 失败、取消、超时、重复提交和刷新恢复。 + +## 8. Step 7:Mod、Art Swap 与 Runtime Tuning + +| 子任务 | 规则 | 完成判定 | +| ------------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| TuningSchema | pointer/type/range/step/runtimeKey/requiresRebuild | 每个可见参数有真实 runtime reader | +| Runtime Tuning ack | 带 revisionId/candidateId/runtimeSessionId/nonce/schemaVersion/ackSeq/expiresAt/applied keys/metrics | 校验 iframe source/origin/ticket/actor;假、旧、重放、空 metrics 均不能保存 | +| 单写收口 | 只写 `/api/ops/apply` | `/tuning` 只读兼容或删除,绝不二次生成 Revision | +| ModJob | targetHeadId + expectedHeadVersion + expectedRevisionId + prompt/ops | stale base 返回 `revision_conflict`,无文件改写 | +| Art Swap | assetId + target + oldHash | 许可、hash、schema、smoke 全通过才 ready | +| 并行创作 | 独立 candidate + head CAS | 只有确定性 typed Ops 可重放;自然语言/code patch 冲突必须重跑 | +| 发布/回滚 CAS | 比较当前 publishedRevisionId | 代码、资产和同 Revision MediaAssetSet 两指针一起切换 | + +## 9. Step 8:宣传媒体与发布 + +| 顺序 | 任务 | 完成判定 | +| ---- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | +| 1 | **新增 MediaJob worker contract** | 输入 revision/artifact/screenshots/creativeSpec/roles/policyVersion;输出 set/assets/attempts/itemResults | +| 2 | QA 后在 BoxLite 截标题页/关键场景 | evidence 带 revision/artifact hash 与 scene-ready 时间 | +| 3 | 固化 creative spec/attempt | provider/model/promptHash/requestId/cost/license/safeArea/审批信息可回读 | +| 4 | 生成 1920×1080 横版与 1080×1920 竖版无字母版 | 两母版通过相关性、安全、比例、字节预算门 | +| 5 | sharp 派生 thumbnail/OG/srcset,确定性叠字 | 规格/字节预算通过;多语言标题不越 safe area | +| 6 | FFmpeg 产必需 portrait MP4;universal 双端 QA 通过才产可选 landscape MP4 | 4–6 秒、静音、H.264/yuv420p、Fast Start≤8MiB;缺 landscape 不阻塞 | +| 7 | 候选重生/焦点/锁版 + 自动审核;风险/精选转人工 | artist 不能 approve/publish;approve/reject/feature 均写审计 | +| 8 | 事务切 `publishedRevisionId + publishedMediaAssetSetId` | set.revisionId 匹配;失败继续服务上一 approved set | +| 9 | Go/edge `/g/:slug` HTML + CDN purge | crawler 读 canonical/title/description/OG;`s-maxage≤300s`,回滚 purge 后命中旧 hash | + +Hero 仅精选游戏触发,固定 1800×600、3:1±2%、WebP≤500KiB;404 退 cover。普通作品不因缺 Hero 阻塞发布,现有 fixture/Hero 资产上线前须替换或完成版权审核。 + +## 10. Step 9:Feed、社区与商业基础 + +| 子任务 | 最小实现 | 完成判定 | +| ---------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Web Feed 修正 | `mode=video` 独立于静音;竖屏消费 coverPortrait | 三端同 fixture 执行 `safe attract→portrait MP4→coverPortrait→thumbnail→plate` | +| Android 安全策略 | 缺 PreviewPolicy 默认拒绝 attract | 只有显式 `attractSafe=true` 才加载 runtime | +| 三端 DTO | thumbnail/cover/coverPortrait/heroCover/previewPortrait/previewLandscape/previewPolicy | Go/TS/Swift/Kotlin fixture decode、null、poster metadata 和优先级一致 | +| Feed 预取 | 当前卡视频;邻卡 poster/metadata | 连续滑 100 卡无邻卡 iframe/视频并发堆积 | +| 社区 | like/save/follow/comment/report/notification | 幂等、游标、限流、审核负例通过 | +| 钱包/积分 | WalletAccount、CreditReservation、ledger、结算/释放 | Job+冻结+outbox 同事务;重试不重复扣费 | +| 支付 | Order/PaymentAttempt/PaymentEvent/Refund + 单 provider webhook | providerEventId 唯一;验签、金额、乱序、退款对账通过 | +| 美术/管理台 | regenerate/focal/lock/approve/reject/feature/takedown/refund/ban | artist 不可发布;Operations Admin 与 Runtime Tuning 分权并全审计 | + +ShipAny 采购件只在此步选择性移植;每个移植模块必须先有明确调用方和适配清单。 + +## 11. Step 10:容量、安全与切流 + +| 门 | 验收 | 失败动作 | +| -------- | -------------------------------------------------------------------------------------- | -------------------------------------------- | +| 容量 | 2,500–5,000 在线模型下 Catalog p95<300ms、错误率<0.5% | 优先修查询/CDN/连接池,再决定 Redis/只读副本 | +| 昂贵 Job | Create 20/min、Mod 80/min、Media 10/min 持续;突发见需求合同;按实测 p95 秒数算 worker | 关新建、保留排队/ETA,不丢任务或重复扣费 | +| Soak | 峰值 2h + 50% 峰值 24h;无 lease/RSS/DB/event 持续泄漏 | 不扩流,先修泄漏和 retention | +| 沙箱 | 逃逸、网络、密钥、资源耗尽负例全绿 | BoxLite 生产开关保持关闭 | +| 数据 | PITR/备份恢复演练,RPO/RTO 达标 | 不切商业流量 | +| 媒体 | CDN 回源、404 降级、旧 Revision 回滚 | 固定上一 approved set | +| 账务 | 订单/provider/ledger/usage 四方对账为零差异 | 停新单与付费任务,保留只读查询 | +| 灰度 | 内部→1%→5%→25%→100%,每级观察 | 自动/手动回退到上一比例 | + +## 12. 仓库与提交切分 + +| 规则 | 执行方式 | +| ------------- | ---------------------------------------------------------------------- | +| 一仓一 writer | 同一 worktree 同时只允许一个写入者;其他 agent 只读评审 | +| 一合同一提交 | schema、storage、job、sandbox、create、mod、media、billing 分开提交 | +| 分支 | `feat/pixelforge-`;修复用 `fix/pixelforge-` | +| 验证 | 每个 commit 至少跑目标测试;里程碑跑三端合同 + 真后端旅程 | +| 合流 | 先合合同,再合后端,再合客户端消费者;不得靠 cherry-pick 未知 WIP 拼装 | +| IWE/CodeGraph | 只在里程碑开工/收尾做一次事实盘点与索引同步,不开多条并行 IWE 旅程 | + +## 13. 首个可领取施工包 + +| 包 | 内容 | 验收 | +| --- | ------------------------------------------------------------- | ---------------------------------------------- | +| C1 | 核心 OpenAPI/Schema + 完整媒体/Preview fixture | Go/TS/Swift/Kotlin 四端 decode 与降级决策一致 | +| C2 | MSW 环境闸 + real-backend smoke | production 不注册 worker,smoke 命中 Go health | +| C3 | PG Game/RevisionCandidate/Head/Revision/Job + idempotency | QA 前无 Revision;并发重试只有一个 Job | +| C4 | S3-compatible Upload/Artifact manifest | 跨租户与 hash 负例通过 | +| C5 | BoxLite profile runner + FFmpeg/provider capability preflight | 默认关闭;无用户 command;安全矩阵可自动执行 | + +领取前先核对对应仓库的分支、远端差异、WIP 与 `docs/mistakes.md`,不得覆盖现有 3D 开发改动。 diff --git a/docs/pixelforge-integration/requirements.md b/docs/pixelforge-integration/requirements.md new file mode 100644 index 000000000..de8f8c753 --- /dev/null +++ b/docs/pixelforge-integration/requirements.md @@ -0,0 +1,236 @@ +--- +title: PixelForge 全功能下穿与后端需求合同 +status: accepted-working-plan +snapshot_date: 2026-08-05 +--- + +# PixelForge 全功能下穿与后端需求合同 + +**结论:前端不是“接一个生成接口”即可;它要求一条从账户、Create、Revision、Mod、Runtime Tuning、运营审核、媒体、发布,到首页、Feed、社区和计费的可追溯生产链。** + +## 1. 统一术语 + +| 术语 | 当前定义 | 禁止混用 | +| ------------- | ---------------------------------------------------------- | ---------------------------- | +| Game | 作品的长期身份和公开目录条目 | 不等同某次构建目录 | +| Revision | 一次不可变、可运行、可回滚的作品版本 | 不原地覆盖 | +| Artifact | Revision 的构建产物与 manifest | 不用本机绝对路径作为公开引用 | +| Job | Create/Mod/QA/截图/媒体/转码的异步任务 | 不以浏览器内存作为任务真相 | +| Op | 对 config/code/asset 的结构化修改指令 | 不把自然语言直接当已应用结果 | +| MediaAssetSet | 绑定一个 Revision 的宣传媒体集合 | 不只存 game 级 URL | +| PreviewPolicy | Feed 对实时 attract、视频、静态图和 inline play 的安全策略 | 缺字段不得默认允许执行 | +| CreditLedger | 充值、赠送、冻结、消费、退款的追加式账本 | 不只维护可覆盖余额字段 | + +## 2. 前端功能全下穿矩阵 + +| ID | 前端功能/动作 | 后端必须提供 | OpenGame/沙箱职责 | 当前判定 | 完成判定 | +| ----- | ----------------------------- | ---------------------------------------------------------- | --------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------- | +| PF-01 | 注册、登录、退出、找回 | OIDC/JWT 校验、Session/tokenVersion、User/Profile、封禁态 | 无 | **缺失 P0** | 三端同一身份;写请求查 session/tokenVersion,吊销后立即拒绝;纯 access token 最大 TTL 15 分钟 | +| PF-02 | 个人资料、头像、作品列表 | Profile CRUD、公开/私有字段、游标分页 | 无 | **缺失 P1** | 修改可回读;他人只见公开字段 | +| PF-03 | 首页 Hero/精选/最新 | 目录查询、精选位、排序版本、屏幕适配字段 | 无 | **前端有壳 P0** | 真 API 返回且分页稳定;Hero 只引用 approved 媒体 | +| PF-04 | 搜索、标签、分类 | 索引字段、过滤、游标、空态 | 无 | **后端缺失 P1** | 相同 cursor 不重不漏;下架作品不可搜到 | +| PF-05 | Web/移动 Feed 滑屏 | feed cursor、PreviewPolicy、封面/视频、统计摘要 | 仅安全准入的 attract runtime | **部分 P0** | 当前卡运行,邻卡只预取 poster/metadata,降级链一致 | +| PF-06 | 游戏详情、分享 | Game+published Revision+MediaAssetSet、Go `/g/:slug` HTML | 提供可运行 artifact | **未闭环 P0** | 不执行 JS 的 crawler 获得 canonical/title/description/当前 approved OG;回滚后缓存失效 | +| PF-07 | 在线玩、暂停、音量、输入 | runtime URL、短期票据、运行桥事件 | BoxLite/静态产物运行;ready/score/over bridge | **部分 P0** | 无 cookie 隔离域可运行,暂停/音量/ready 可验证 | +| PF-08 | Prompt Create | 幂等创建、Job、事件流、取消、配额 | 生成 GDD、代码、游戏内资产、build | **仅基础后端 P0** | 202+jobId;断线后按 cursor 续看;重复请求只执行一次 | +| PF-09 | Create 过程状态 | stage、percent、event cursor、结构化失败 | 输出阶段与机器可读结果 | **协议未合流 P0** | 刷新/换端后重建相同状态;失败能准确重试 | +| PF-10 | 实时预览 | draft Revision、runtime ticket、刷新/ready | 构建可预览 artifact | **前端有壳 P0** | 仅 ready Revision 可加载;旧任务不得替换新预览 | +| PF-11 | 截图与录屏 | 上传签名、MediaAsset、Revision 绑定 | 给出 scene-ready/录制窗口 | **game 级上传 P0** | 上传失败无脏公开 URL;回滚恢复同版本 poster/video | +| PF-12 | 资产上传/素材包 | 预签名上传、MIME/大小/hash/许可/租户校验 | 消费受信 assetId | **未闭环 P0** | 客户端只提交 assetId;跨租户和篡改 hash 被拒绝 | +| PF-13 | Fork | lineage、权限、许可快照、初始 Revision | 可从指定 artifact 重建 | **后端缺失 P1** | 来源链可查;下架/不可 fork 策略生效 | +| PF-14 | Mod It 自然语言修改 | ModJob、expectedRevisionId、冲突和事件 | BoxLite 内 patch/build/test/QA | **后端缺失 P0** | CAS 冲突不写;成功产生新 Revision,旧版可回滚 | +| PF-15 | Art Swap | 结构化 asset op、旧/新 hash、审核 | 在沙箱中替换并验证引用 | **后端缺失 P0** | 替换只作用新 Revision;许可和运行检查通过 | +| PF-16 | Runtime Tuning(GM 参数)试调 | TuningSchema、candidate、runtime ack+metrics | runtime 真正读取并应用参数 | **假闭环 P0** | 每个 runtimeKey 有真实读者;无有效 ack 禁止保存 | +| PF-17 | Runtime Tuning 保存 | 单一 `/api/ops/apply`、head CAS、审计 | 根据 Op 重建或热更 | **双写 P0** | `/tuning` 不再写;响应返回 nextRevisionId 与 applied Ops | +| PF-18 | QA | 自动 build/smoke/gameplay/media/moderation 结果 | 执行真实检查并产 evidence | **缺失 P0** | required gate 失败绝不进入 published | +| PF-19 | 发布、撤回、回滚 | 原子切换 publishedRevisionId、审计、幂等 | 无 | **缺失 P0** | Revision、artifact、媒体同切;失败继续服务上一版 | +| PF-20 | 宣传图生成 | MediaJob、MediaAssetSet、审核、锁版 | 截图、creative spec、provider adapter | **生产者缺失 P0** | 横/竖母版 + 派生图可回读,均绑定 artifactHash | +| PF-21 | 移动预览录像 | 转码任务、posterAssetId、codec/时长/方向/Fast Start 元数据 | 输出安全录制窗口 | **未闭环 P0** | portrait MP4 必需;landscape 仅 universal 双端 QA 通过时可选,缺失不阻塞普通发布 | +| PF-22 | 点赞、收藏、关注 | 幂等关系写入、聚合计数、鉴权 | 无 | **Mock/壳 P1** | 重试不重复计数;读写一致且有反滥用限流 | +| PF-23 | 评论、举报、审核 | 评论树、游标、举报、封禁/下架、审计 | 可提供内容风险信号 | **后端缺失 P1** | 被审核内容按权限隐藏;操作可追责 | +| PF-24 | 通知 | durable notification、已读游标、去重 | Job 终态产生事件 | **缺失 P1** | 同一业务终态不重复轰炸;跨端已读一致 | +| PF-25 | 用量与积分 | UsageCost、CreditLedger、预授权、结算、退款 | 输出 provider usage/requestId | **仅 token 原型 P0** | 每笔消费追到 user/game/job/provider;重复 webhook 不重复入账 | +| PF-26 | 充值与订单 | Order、PaymentAttempt、验签 webhook、退款 | 无 | **缺失 P1** | 服务端验签、幂等入账、金额/币种不可由客户端决定 | +| PF-27 | Creator/美术外接臂 | 候选、重生、裁切焦点、安全区、锁版、角色权限 | 复用图像 provider 与截图 | **缺失 P1** | 美术不能发布;锁版资产仍需 reviewer/自动门通过 | +| PF-28 | Operations Admin(运营后台) | 精选、下架、审核、退款、封禁、审计 | 无 | **缺失 P1** | 与 Runtime Tuning 分开授权;高风险动作二次确认并写 immutable audit log | +| PF-29 | 可观测性/客服排障 | requestId、jobId、stage、failureCode、trace | 透传 provider/沙箱诊断但清洗密钥 | **部分 P0** | 用户错误可读,内部 trace 可定位,敏感值不下发 | +| PF-30 | 多租户/多用户并行创作 | owner、角色、资源限额、CAS、租户隔离 | 每 Job 独立 BoxLite/工作目录 | **缺失 P0** | 并发 Mod 不互踩;跨租户文件、事件、密钥均不可见 | + +## 3. 最小领域合同 + +| 对象 | 必须字段 | 不变量 | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| User/Session | `id, externalSubject, status, tokenVersion, sessionId, revokedAt` | `externalSubject` 唯一;写请求检查用户/会话状态;封禁用户不能新建付费任务 | +| Game | `id, ownerId, slug, title, visibility, defaultHeadId, publishedRevisionId, publishedMediaAssetSetId` | 两个 published 指针必须指向同一 Revision;slug 唯一;公开读不暴露本机目录 | +| RevisionCandidate | `id, gameId, jobId, baseRevisionId, status` | Create 202 只创建 candidate;失败 candidate 不得冒充 Revision | +| RevisionHead | `id, gameId, name, headRevisionId, version` | Mod/Ops CAS 比较 head+version;自然语言/code patch 冲突必须重跑,不自动重放 | +| Revision | `id, gameId, parentRevisionId, kind, artifactId, qaEvidenceId, createdBy, createdAt` | artifact+必需 QA 通过后才创建;内容不可变;发布状态由 Game 指针表达 | +| Artifact | `id, manifestUrl, manifestHash, entryUrl, sizeBytes, engine, createdAt` | 内容寻址;URL immutable;入口只能引用允许的对象 | +| Job | `id, tenantId, gameId, baseRevisionId, type, status, stage, idempotencyKey, leaseUntil` | 同业务幂等键唯一;终态不可倒退 | +| Event | `id, jobId, seq, type, stage, payload, createdAt` | `(jobId, seq)` 唯一;客户端按 cursor 重放 | +| OpBatch | `id, gameId, headId, expectedHeadVersion, expectedRevisionId, ops, actorId, reason` | CAS 失败不产生 Revision;只有确定性 typed Ops 可重放,所有 Op 经过 schema 校验 | +| MediaAssetSet | `id, gameId, revisionId, artifactHash, policyVersion, status` | `(revisionId, artifactHash, policyVersion)` 幂等;整组发布/回滚 | +| MediaAsset | `id, role, url, hash, width, height, format, byteSize, focalPoint, source, moderationStatus, codec?, durationMs?, orientation?, fastStart?, posterAssetId?` | 公开 URL 只能来自 approved set;视频 poster 必须指向同 Revision 图片;landscape preview 可空 | +| MediaGenerationAttempt | `mediaSetId, provider, model, promptHash, requestId, cost, license, safeArea, lockedBy, approvedBy, rejectionReason` | 每次重生独立留痕;锁版不绕过审核;失败 attempt 不改 published pointer | +| PreviewPolicy | `mode, attractSafe, inlinePlay, fallback, version` | `attract` 必须显式 `attractSafe=true`;缺失时 fail-closed | +| UsageCost | `jobId, provider, requestId, units, amount, currency, status` | provider request 可对账;金额只由服务端价格表计算 | +| Wallet/CreditReservation | `walletId, userId, balance`;`jobId, amount, status, expiresAt` | Job、信用冻结、outbox 同事务;终态按真实 usage 结算或释放 | +| Order/Payment | `Order, PaymentAttempt, PaymentEvent(providerEventId), Refund` | providerEventId 唯一;验签后推进订单;退款关联原支付 | +| CreditLedger | `id, walletId, orderId?, jobId?, type, delta, balanceAfter, idempotencyKey` | 追加写;余额不得为负;充值、消费、释放、退款均可关联 | +| AuditLog | `actorId, action, targetType, targetId, beforeHash, afterHash, reason, createdAt` | 高风险后台操作不可删除或覆盖 | + +## 4. API 家族与单写入口 + +| 家族 | 最小端点/通道 | 权威规则 | +| ------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Auth/Profile | `/api/session`、`/api/me`、`/api/users/:id` | 身份由服务端 token 验证,不信客户端 userId | +| Catalog | `GET /api/games`、`GET /api/games/:id` | cursor 分页;只投影当前可见 Revision | +| Create | `POST /api/generate`、`GET/DELETE /api/jobs/:id` | 返回 202;Idempotency-Key 必填 | +| Events | `GET /api/jobs/:id/events` + SSE | REST cursor 是真相;SSE 使用 `Last-Event-ID`、15s heartbeat、背压断开重连;v1 不开放 WS 公共合同 | +| Runtime | `GET /sandbox/r/:revisionId/` + ticket | cookieless origin;短期票据;尾斜杠和静态头一致 | +| Upload | `POST /api/uploads` 或预签名 | 返回 immutable assetId;校验 MIME/大小/hash/owner | +| Mod | `POST /api/games/:id/mods` | `expectedRevisionId` 必填;实际执行进入 BoxLite | +| Ops | `POST /api/ops/apply` | **唯一 Runtime Tuning/结构化修改写入口** | +| QA/Publish | `GET /api/revisions/:id/qa`、`POST /api/games/:id/publish` | 发布事务同时切 Revision 与 MediaAssetSet | +| Media | `POST /api/revisions/:id/media-jobs`、`GET /api/media-sets/:id` | 所有产物绑定 Revision 和 artifactHash | +| Share HTML | `GET /g/:slug` | Go/edge 输出可抓取 HTML;带 canonical/title/description/approved `og:image`;`s-maxage≤300s` 且发布/回滚主动 purge | +| Social | like/save/follow/comment/report/notifications | 每种关系有幂等键、权限和反滥用限流 | +| Billing | orders/payment-webhook/credits/usage | webhook 验签后入账;账本追加写 | +| Admin | review/feature/takedown/refund/ban | RBAC + 审计;不复用普通用户写接口绕过策略 | + +现有 `/v1/*` 可保留为控制面兼容入口,但必须调用同一 domain service;不得形成第二套写逻辑。 + +SSE 事件保留期首发为 7 天;cursor 过期返回 `410 event_cursor_expired` 并附当前 Job snapshot,客户端从 snapshot 继续,而不是从头猜状态。 + +## 5. Create 生产过程 + +| 阶段 | 输入 | 产物/事件 | 失败处理 | 完成判定 | +| ----------------- | ------------------------------ | ------------------------------------------------------ | ------------------------ | ------------------------------------------------------- | +| 1. 准入 | user、prompt、目标屏幕、幂等键 | `gameId + jobId + candidateId`,Job queued | 配额不足不启动 provider | 同键返回原 Job;此时尚无 Revision | +| 2. 设计 | prompt | GDD、engine、tuningSchema 草案 | 结构错误可重试一次 | schema 和目标能力匹配 | +| 3. 资产 | GDD/style anchor | 游戏内资产、来源、hash | 必需资产失败则 Job 失败 | 不以“部分成功文案”放行 | +| 4. 构建 | code+assets | immutable artifact | 编译错误进入可诊断 stage | build 绿且 manifest 完整 | +| 5. 沙箱 QA | artifact | smoke/gameplay/security evidence | 超时/越权终止沙箱 | 必需门全绿 | +| 6. Ready Revision | artifact+evidence | 一次性创建 immutable Revision,并推进目标 RevisionHead | 事务失败不暴露半成品 | 刷新后可回读同一 Revision;失败 candidate 永无 Revision | +| 7. 媒体 | ready Revision | screenshot/video/MediaAssetSet | 保留上一 approved set | 媒体状态可独立观察 | +| 8. 发布 | Revision+approved media | published pointer | 原子失败保持旧版 | 三端看到同一版本 | + +## 6. Mod、实时修改与 Runtime Tuning 过程 + +| 场景 | 统一流程 | 关键并发规则 | 用户可见状态 | +| ------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | --------------------------------------------------------- | +| 自然语言 Mod | 解析为 Op/patch → BoxLite → build → QA → Revision | `expectedRevisionId` CAS | queued/patching/building/validating/ready/conflict/failed | +| Art Swap | assetId+目标槽 → 许可校验 → 替换 → smoke → Revision | oldAssetHash 必须匹配 | uploading/validating/applying/ready/rejected | +| Runtime Tuning 试调 | TuningSchema → runtime candidate → 真 applied ack+metrics | ack 校验 `runtimeSessionId, nonce, schemaVersion, ackSeq, expiresAt, origin, ticket, actor` | previewing/applied/ack_missing | +| Runtime Tuning 保存 | `/api/ops/apply` → target head CAS → build/QA → nextRevisionId | 禁止 `/tuning` 双写 | saving/applied/conflict/failed | +| 多人并行 | 每人从 baseRevision 建 candidate;typed Ops 可重放 | Mod/Ops 比较 target RevisionHead;自然语言/code patch 冲突后重跑 | stale/rebase_required/conflict | +| 发布/回滚 | 比较 publishedRevisionId,并原子切 Revision+MediaAssetSet 两个指针 | 不重新生成媒体 | rolling_back/published | + +“直播修改状态”是事件流,不等于在主进程热改文件。不能安全热更的字段明确标为 `requiresRebuild=true`。 + +### OpenGame/BoxLite 版本化执行合同 + +| 方向 | 字段 | 规则 | +| ------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Worker→Runner | `contractVersion, profileId, operation, structuredArgs, jobId, tenantId, candidateId, baseRevisionId, deadline` | 不接受用户提供的 shell command;服务端把 operation 映射到固定入口 | +| 输入引用 | `prompt/typedOps, assetIds, artifactManifest, capabilityTicket` | 只传 immutable refs;provider 通过短期代理票据访问,不注入主密钥 | +| OpenGame→Worker | `itemResults[], artifactManifest, evidenceRefs[], usage[], providerRequests[], failureCode` | 每项含 success/required/taskId/requestId/cost;required 失败使整个 Job 失败 | +| 取消 | worker 标记 cancel + runner signal | AbortSignal 贯穿 provider、下载、FFmpeg 与子进程;终态后无残留任务 | +| Provider capability | `imageSizes, edit, video, audio, cancel, maxBytes` | 路由前显式匹配;不支持必须返回结构化降级原因,禁止静默换能力 | +| 产物交接 | output manifest + hash,或单 Job 预签名上传票据 | 宿主复核 hash/MIME/size/path 后入对象存储;沙箱不能覆盖已发布对象 | + +## 7. 宣传媒体与移动预览 + +| `MediaAsset.role` / DTO 字段 | 固定规格/预算 | 生产策略 | 消费与回退 | +| ---------------------------- | ------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------- | +| `cover` | 1920×1080、16:9、WebP≤350KiB | QA 后从实机截图+creative spec 生成无字母版 | Web 横卡;404 退同 Revision screenshot | +| `coverPortrait` | 1080×1920、9:16、WebP≤450KiB | 独立构图,不从横图硬裁 | 移动 Feed;失败退 thumbnail/品牌底板 | +| `thumbnail` | 640×360、WebP≤100KiB | sharp 从合适母版确定性派生;另产 320/640/960/1280 `srcset` | 列表兜底;客户端不得下载原母版充小卡 | +| `ogImage` | 1200×630、WebP/JPEG≤300KiB | 母版裁切 + 确定性标题/品牌叠加 | `/g/:slug` crawler;失败退 cover | +| `heroCover` | 1800×600、3:1±2%、WebP≤500KiB | **仅精选游戏**生成/人工锁版 | URL 404 退 cover+安全背景;普通作品可空 | +| `previewPortrait` | 720×1280、4–6s、静音 H.264/yuv420p MP4≤8MiB | FFmpeg 转码,Fast Start;`posterAssetId` 指 coverPortrait | 移动默认;失败退 coverPortrait | +| `previewLandscape` | 1280×720、4–6s、静音 H.264/yuv420p MP4≤8MiB | **可选**;仅 universal 且双端 QA 通过才生成 | 横屏卡;缺失不阻塞普通发布;首版不做 HLS | + +Feed 决策顺序固定为:**显式安全 attract → H.264 MP4 → coverPortrait → thumbnail → 确定性品牌底板**。 + +### PixelForge `GameDTO` 媒体字段表 + +| 字段 | 类型/空值 | 语义 | 兼容规则 | +| ------------------ | ------------------------- | ------------------------------------------------------------- | --------------------------------------------- | +| `thumbnail` | `string or null` | 640×360 列表兜底 | 保留现字段 | +| `cover` | `string or null` | 横版 cover,唯一名称;不再使用 `coverLandscape` | 保留现字段 | +| `coverPortrait` | `string or null` | 竖版静态首选 | Web/iOS/Android 同名 | +| `heroCover` | `string or null` | 精选 Hero;普通作品允许 null | 404 必须有 cover fallback | +| `previewPortrait` | `PreviewMediaDTO or null` | `url, posterUrl, codec, durationMs, width, height, fastStart` | 移动 Feed 首选;posterUrl 必须来自同 Revision | +| `previewLandscape` | `PreviewMediaDTO or null` | 同上,横版可选 | 只在布局为 landscape 时消费 | +| `preview` | `string or null` | **过渡只读别名** | Step 9 后删除;新代码不得写入 | +| `previewPolicy` | `PreviewPolicy or null` | attract/inline/fallback 安全合同 | null 等同 fail-closed,不允许 attract | + +Go、TypeScript、Swift、Kotlin 必须用同一 fixture 验证字段、null、metadata 和降级顺序。 + +## 8. 账户、社区与充值 + +| 领域 | 首发必须 | 可后置 | 关键约束 | +| ------ | --------------------------------- | ----------------- | -------------------------------------------------- | +| 账户 | OIDC 登录、用户镜像、封禁态、角色 | 多身份绑定、组织 | 不自研密码存储;JWT 必须验签和校验 audience/issuer | +| 社区 | 点赞、收藏、关注、评论、举报 | 推荐图谱、勋章 | 幂等、游标、限流、审核与下架 | +| 积分 | 余额、冻结、消费、退款、用量明细 | 促销券、等级 | 追加式账本;生成前预授权,终态结算 | +| 充值 | 单一支付 provider、订单、webhook | 第二支付 provider | 金额/币种服务端生成;webhook 验签+幂等 | +| 管理台 | 用户、作品、审核、订单、账本查询 | 复杂 BI | RBAC、二次确认、全审计 | + +ShipAny 只作为上述表结构、页面和 webhook 流程的参考来源,不引入第二个 Next.js 业务后端。 + +### 美术与 Operations Admin 外接臂 + +| Command | 允许角色 | 结果 | 强制约束 | +| --------------------------------------------- | ------------------------- | -------------------------------------------- | ------------------------------------------------------------- | +| `POST /api/media-sets/:id/regenerations` | creator、artist | 新 MediaGenerationAttempt/候选,不覆盖原资产 | 绑定 Revision/policyVersion,受预算和限流 | +| `PATCH /api/media-assets/:id/focal-point` | creator、artist | 新焦点/safe-area 版本 | 只改 draft set;锁版后需显式 unlock 审计 | +| `POST /api/media-sets/:id/lock` | creator、artist | 锁定候选 | 锁定不等于审核通过,不得发布 | +| `POST /api/media-sets/:id/approve` | reviewer/Operations Admin | approved | 记录 approvedBy、版权/安全结果 | +| `POST /api/media-sets/:id/reject` | reviewer/Operations Admin | rejected | rejectionReason 必填,公开指针不变 | +| `POST /api/games/:id/feature` | Operations Admin | featured/unfeatured | Hero 缺失时先排 Hero Job;全操作审计 | +| `POST /api/admin/` | Operations Admin 分权角色 | 管理动作 | action 服务端白名单;与 Runtime Tuning 权限完全分开,二次确认 | + +美术角色不能 approve/publish;creator 不能 feature/takedown;Operations Admin 不通过文件系统直接改游戏。 + +## 9. 5–10 万 DAU 的非功能要求 + +以下容量是**验收假设**,不是当前流量事实:按峰值 5% 同时在线,即 2,500–5,000 在线用户压测。 + +| 领域 | 首发验收线 | 设计要求 | +| ------------ | ---------------------------------- | ---------------------------------------------------- | +| Catalog/Feed | p95 < 300ms,错误率 < 0.5% | cursor 分页、CDN、批量聚合、避免 N+1 | +| Feed 媒体 | 当前卡视频;邻卡只 poster/metadata | 按网络/省流/Reduce Motion 降级,不预加载邻卡 runtime | +| API | 无状态横向扩展 | session 不驻单机内存;限流键可分布式 | +| Job | 重启不丢、重复投递不重复扣费 | PostgreSQL lease + 幂等键 + outbox/event cursor | +| 数据库 | 无 SQLite 单写瓶颈 | PostgreSQL 连接池、索引、慢查询门、必要时只读副本 | +| 实时状态 | 断线可恢复 | REST event cursor 为真相;Redis/pubsub 只加速 | +| 文件 | API 不代理大文件热流量 | 对象存储直传、CDN immutable、短期签名下载 | +| 沙箱 | 多租户资源硬隔离 | CPU/内存/PID/时限/网络 allowlist、环境白名单 | +| 账务 | 0 重复入账/扣费 | webhook、Job 和 ledger 全部幂等并可对账 | +| 可用性 | Redis/实时通道失败仍可查终态 | 正确性不依赖缓存;旧 published Revision 始终可服务 | + +### 默认昂贵任务压测模型 + +这些是首轮容量假设,实测产品漏斗后必须重算;worker 数量按 `ceil(每分钟到达量 × p95任务秒数 / 60 × 1.3)`,再受 provider 配额上限约束。 + +| 负载 | 持续目标 | 10 分钟突发 | 公平/大小约束 | +| ---------------- | ------------------------------- | ---------------------- | --------------------------------------------------- | +| Create | 20 jobs/min | 60 jobs/min | 每 tenant 同时 2 个;p95 sandbox≤8min、硬上限 20min | +| Mod/Runtime save | 80 jobs/min | 200 jobs/min | 每 tenant 同时 4 个;typed no-op 不启动 sandbox | +| MediaJob | 10 jobs/min | 30 jobs/min | provider/FFmpeg 分池;previewLandscape 可跳过 | +| Provider | 按实测 p95 计算并发 | 不超过供应商 quota 80% | 超额排队可见,不静默换 provider/模型 | +| Event | ≤200 events/job、单 event≤32KiB | cursor 7 天保留 | 过期返回 snapshot;禁止把原始二进制/完整日志入表 | +| 视频 | MP4≤8MiB/方向、图片预算见上表 | CDN 直传/直读 | API 不代理热视频;生命周期按 Revision | +| Soak | 峰值 2 小时 | 50% 峰值 24 小时 | 无 lease 泄漏、DB 膨胀失控、worker/RSS 持续增长 | + +## 10. 明确不在当前里程碑 + +| 能力 | 处理 | +| ------------------------------------- | ---------------------------------------------------- | +| GLB/FBX/OBJ、text-to-3D、骨骼动画换装 | 另立 3D Asset/Mod 里程碑 | +| 多人实时协同编辑 CRDT | 先用 Revision+CAS+Ops 重放;真实同屏协同有数据再评估 | +| HLS 短预览 | 4–6 秒 MP4 已覆盖,长直播/长视频出现再评估 | +| GitLab 作为业务主库 | 排除;PostgreSQL 保存 lineage 和收入权威 | +| 全量人工 GM 审批 | 排除;默认自动门,风险/精选才转人工 | From c4d8c08205c08b41f8326f143981f99f890ab751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 20:31:44 +0800 Subject: [PATCH 39/43] fix(core): prevent mirrored authored count config Generated 3D games can make SceneMap array lengths and config count leaves co-own the same value while using the config only in an equality assertion. State the single-truth rule at GDD production so counts remain authored data unless gameplay consumes a separate threshold. --- packages/core/src/tools/generate-gdd.test.ts | 7 +++++++ packages/core/src/tools/generate-gdd.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tools/generate-gdd.test.ts b/packages/core/src/tools/generate-gdd.test.ts index b710608b2..e1f0eba21 100644 --- a/packages/core/src/tools/generate-gdd.test.ts +++ b/packages/core/src/tools/generate-gdd.test.ts @@ -10,6 +10,13 @@ describe('configMergeInstruction', () => { expect(instruction).toContain('every leaf must have a runtime consumer'); expect(instruction).toContain('superseded leaves must be removed'); + expect(instruction).toContain( + 'Derive authored counts from `SceneMap` arrays', + ); + expect(instruction).toContain( + 'do not add a config count leaf merely to mirror or assert', + ); + expect(instruction).toContain('separate completion threshold'); expect(instruction).not.toContain('NEVER delete infrastructure fields'); }); diff --git a/packages/core/src/tools/generate-gdd.ts b/packages/core/src/tools/generate-gdd.ts index 1e160fcfb..cef601e16 100644 --- a/packages/core/src/tools/generate-gdd.ts +++ b/packages/core/src/tools/generate-gdd.ts @@ -43,7 +43,7 @@ export interface GenerateGDDParams { 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`; every leaf must have a runtime consumer, and renamed or superseded leaves must be removed in the same edit. Derive authored counts from `SceneMap` arrays: do not add a config count leaf merely to mirror or assert an authored array `.length`; keep a count in config only when gameplay consumes it as a separate completion threshold.'; } 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`)'; } From fed1ff11417ae4a2e86588d8c4bf42be4a0e0968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Wed, 5 Aug 2026 20:32:01 +0800 Subject: [PATCH 40/43] docs: record patch isolation lapse Capture the repeated failure to keep apply_patch operations file-scoped so future work checks for multiple Update File headers before applying. --- docs/mistakes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/mistakes.md b/docs/mistakes.md index aaf06707d..7896c2920 100644 --- a/docs/mistakes.md +++ b/docs/mistakes.md @@ -1,3 +1,4 @@ | 日期 | 现象 | 根因 | 正解 | 预防规则 | | ---------- | ------------------------------------------------------ | --------------------------------------- | ---------------------------------------------------- | ------------------------------------------ | | 2026-08-05 | focused Vitest 被广播到全部 workspaces,均报找不到测试 | 从仓库根运行带文件参数的聚合 `npm test` | 在 `packages/core` workspace 内用相对路径运行 Vitest | 传测试文件过滤器前先确认根脚本是否广播参数 | +| 2026-08-05 | 管线收口时又把 producer 与测试合进同一补丁 | 只关注补丁原子语义,忽略既有文件级编辑隔离规则 | 补丁虽成功但立即登记;后续严格每次只改一个文件 | `apply_patch` 输入出现两个 `Update File` 即停止并拆分 | From e37c6244fdb6afa353cb89ef33180aa7da643672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Thu, 6 Aug 2026 11:01:26 +0800 Subject: [PATCH 41/43] fix(core3d): preserve initial HUD progress Create the DOM HUD before GameScene emits its constructor-time progress event. This prevents generated games from overwriting a real initial total with the UIScene default and locks the ordering with the existing runtime contract self-check. --- agent-test/templates/core3d/src/main.ts | 4 ++-- .../modules/threed_basic/tests/runtime-contract-selfcheck.mjs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/agent-test/templates/core3d/src/main.ts b/agent-test/templates/core3d/src/main.ts index 50966a1eb..137898ff5 100644 --- a/agent-test/templates/core3d/src/main.ts +++ b/agent-test/templates/core3d/src/main.ts @@ -42,6 +42,8 @@ class GameApp { private start(): void { if (!this.container) return; this.ended = false; + this.ui.init({ gameSceneKey: 'GameScene' }); + this.ui.show((key) => this.pause(key)); this.game = new GameScene( this.container, { @@ -59,8 +61,6 @@ class GameApp { }, 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', diff --git a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs index 08844399d..ec35131df 100644 --- a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs +++ b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs @@ -42,6 +42,10 @@ assert.match(moduleScene, /this\.input\.dispose\(\)/); assert.match(moduleScene, /object\.geometry\.dispose\(\)/); assert.match(moduleScene, /material\.dispose\(\)/); assert.match(coreMain, /this\.game\?\.dispose\(\)/); +assert( + coreMain.indexOf('this.ui.show') < coreMain.indexOf('this.game = new GameScene'), + 'HUD must exist before GameScene emits its initial progress event', +); assert.match(coreScene, /this\.renderer\.dispose\(\)/); assert.match(manual, /protected runtime\s+contracts/); assert.match(manual, /asset-pack\.json[\s\S]*relative URL/); From e4873356aee481926df46de815f96968fa4df98b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Thu, 6 Aug 2026 11:02:07 +0800 Subject: [PATCH 42/43] docs: record repeated patch isolation lapse Document the repeated multi-file apply_patch mistake so future goal turns enforce the existing one-file edit rule mechanically. --- docs/mistakes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/mistakes.md b/docs/mistakes.md index 7896c2920..4c8be899f 100644 --- a/docs/mistakes.md +++ b/docs/mistakes.md @@ -2,3 +2,4 @@ | ---------- | ------------------------------------------------------ | --------------------------------------- | ---------------------------------------------------- | ------------------------------------------ | | 2026-08-05 | focused Vitest 被广播到全部 workspaces,均报找不到测试 | 从仓库根运行带文件参数的聚合 `npm test` | 在 `packages/core` workspace 内用相对路径运行 Vitest | 传测试文件过滤器前先确认根脚本是否广播参数 | | 2026-08-05 | 管线收口时又把 producer 与测试合进同一补丁 | 只关注补丁原子语义,忽略既有文件级编辑隔离规则 | 补丁虽成功但立即登记;后续严格每次只改一个文件 | `apply_patch` 输入出现两个 `Update File` 即停止并拆分 | +| 2026-08-06 | 同类错误再次发生:core3d 修复与自检被合进一个补丁 | 执行前未落实已登记的 `Update File` 数量检查 | 补丁完整成功后逐文件复核并再次登记;此后每次补丁只允许一个目标文件 | 调用 `apply_patch` 前机械检查仅有一个 `Update File`,否则禁止执行 | From c6d580e7cf604a5b64b25dc32850d72f69b344fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9D=89=E6=81=AF?= Date: Thu, 6 Aug 2026 11:35:55 +0800 Subject: [PATCH 43/43] test(core3d): require HUD order anchors Make the ordering guard mutation-honest by requiring both UIScene.show and GameScene construction to exist in the expected order; deleting either anchor can no longer pass through indexOf(-1). --- .../threed_basic/tests/runtime-contract-selfcheck.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs index ec35131df..8330f4006 100644 --- a/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs +++ b/agent-test/templates/modules/threed_basic/tests/runtime-contract-selfcheck.mjs @@ -42,8 +42,9 @@ assert.match(moduleScene, /this\.input\.dispose\(\)/); assert.match(moduleScene, /object\.geometry\.dispose\(\)/); assert.match(moduleScene, /material\.dispose\(\)/); assert.match(coreMain, /this\.game\?\.dispose\(\)/); -assert( - coreMain.indexOf('this.ui.show') < coreMain.indexOf('this.game = new GameScene'), +assert.match( + coreMain, + /this\.ui\.show[\s\S]*this\.game = new GameScene/, 'HUD must exist before GameScene emits its initial progress event', ); assert.match(coreScene, /this\.renderer\.dispose\(\)/);