From 05854ad351d57e3a176b045359051cc366d81a22 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 21:45:40 +0000 Subject: [PATCH 01/10] Fix orphan gate false positives, un-red main CI, surface red main as an issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Orphan check now unions skill tokens across all skills: a primitive surfaced in any SKILL.md or adopted anywhere passes everywhere. Kills the 31 false orphans created when curated core barrels re-listed deep symbols under the main jgengine skill (whose docs never name them) — the failure that has kept main's checks job red and could not be baselined (ratchet is shrink-only). - gen-skill-api hard-fails when any extracted package lacks dist instead of silently dropping modules and corrupting api.md/baselines. - Gen mode exits 2 on gate failures so the safe wrapper keeps valid generated files instead of rolling back legitimate regeneration. - PR quick job now builds and runs check-skill-api so this failure class can no longer merge unseen; red main auto-files/updates a 'CI red on main' issue. - tsconfig.build excludes cover .test.tsx (first .tsx test broke builds). - Prune 97 orphan-baseline entries resolved by the union fix; regen stale capabilities.md; resync AGENTS.md with CLAUDE.md. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- .../skills/jgengine-gameplay/capabilities.md | 1 + .github/workflows/ci.yml | 14 +++ AGENTS.md | 5 +- CLAUDE.md | 4 +- packages/assets/tsconfig.build.json | 2 +- packages/convex/tsconfig.build.json | 2 +- packages/core/tsconfig.build.json | 2 +- packages/editor/tsconfig.build.json | 2 +- packages/github/tsconfig.build.json | 2 +- packages/jgengine/tsconfig.build.json | 2 +- packages/node/tsconfig.build.json | 2 +- packages/react/tsconfig.build.json | 2 +- packages/shell/tsconfig.build.json | 2 +- packages/sql/tsconfig.build.json | 2 +- packages/ws/tsconfig.build.json | 2 +- scripts/api-doc-baseline.json | 63 ------------ scripts/api-orphan-baseline.json | 97 ------------------- scripts/gen-skill-api-safe.ts | 8 +- scripts/gen-skill-api.ts | 16 ++- 19 files changed, 51 insertions(+), 179 deletions(-) diff --git a/.claude/skills/jgengine-gameplay/capabilities.md b/.claude/skills/jgengine-gameplay/capabilities.md index 5945873b6..a6da7d08b 100644 --- a/.claude/skills/jgengine-gameplay/capabilities.md +++ b/.claude/skills/jgengine-gameplay/capabilities.md @@ -19,6 +19,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p ## decay-meter — survival meters that drain/refill over game time (hunger, water, oxygen, stamina) - `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/survival/decayMeter"` + ## dialogue-bridge — open/close the talkable→DialogueBox flow with no per-game store or command glue - `createGameDialogue` (function) · `import { createGameDialogue } from "@jgengine/core/game/dialogue"` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f9c2bd44..c4d773ce8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,8 @@ jobs: - run: bun run check-artifacts - run: bun run check-stage-skills - run: bun run check-skills + - run: bun run build + - run: bun run check-skill-api checks: if: github.event_name == 'push' @@ -89,7 +91,19 @@ jobs: if: always() runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + issues: write steps: - run: | ok() { test "$1" = "success" || test "$1" = "skipped"; } test "${{ needs.quick.result }}" = "success" && ok "${{ needs.checks.result }}" && ok "${{ needs.web-build.result }}" && ok "${{ needs.smoke.result }}" + - name: Report red main as an issue + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + title="CI red on main" + body="[Run ${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) failed on \`${{ github.sha }}\`. Fix forward from origin/main on a fresh branch, then close this." + n=$(gh issue list --state open --search "\"$title\" in:title" --json number --jq '.[0].number' || true) + if [ -n "$n" ]; then gh issue comment "$n" --body "$body"; else gh issue create --title "$title" --body "$body"; fi diff --git a/AGENTS.md b/AGENTS.md index eed384dfa..7de13b7e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,8 @@ Genre-agnostic pure-TypeScript game engine SDK plus its agent skills. Published - **Editor-first for scene, placement, and asset work.** Building or changing what's in a world — placing spawns/objects/props, laying paths/roads/zones, painting terrain/materials, scattering foliage, authoring assets — is done **through the scene editor** (its RPC/CLI `bun packages/editor/src/mcp/cli.ts` or the GUI) and saved into the scene document (`editor.scene.json`), consumed at runtime by engine primitives (`` etc.). The builder should make this easy and look good with zero tuning. When the editor *can't* do something you need, **file a `[FEATURE]` issue for the missing editor/engine capability first**, then fall back to code and note the gap in the PR. Never hardcode level geometry, waypoint arrays, or bespoke per-game placement/render code when the editor could own it — that's the smell (see Design principles → "Author scenes in the editor"). - **Every session is its own ephemeral cloud container.** The **main session** works directly on its assigned `claude/...` branch — no worktrees, no branch juggling. Commit and push early: `git push -u origin ` on its own line (never piped through a filter — a non-zero grep silently drops the push). `warn-unpushed` Stop hook catches strandings. Exception, and only here: parallel **shipping subagents** each run in their own isolated git worktree (`Agent({ isolation: "worktree" })`) so N tasks ship N PRs at once without stomping the shared tree — see `fan-out`. Main never juggles worktrees; the subagents do, and they auto-clean. - **Ship = push → PR → subscribe → stop. Never merge.** When work is done and clean: push, open the PR (`create_pull_request`, ready for review), `subscribe_pr_activity`, report the link, **end the turn** — no waiting or polling. **One PR per branch, ever**: before creating, check none exists (`list_pull_requests` with `head`); if it does, the push already updated it — never open a second. -- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~30s `quick` job; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. -- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` surfaces on the next PR; fix forward from `origin/main` on a fresh branch). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. +- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~1min `quick` job incl. `check-skill-api`; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. +- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` auto-files/updates a "CI red on main" issue; fix forward from `origin/main` on a fresh branch and close it). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. - **One task, one PR — never chunk.** A task ships as a single PR, however big. Never slice one request into separately-PR'd parts "to keep them small." Keep working the same branch until the whole task is done, then ship once. - **New task in the same session = fresh branch off `origin/main`, new PR.** The previous branch stays parked under its PR — never add to it, never reset it. `git fetch origin main && git checkout -b claude/ origin/main`. Many small parked PRs are the steady state. - **Git ceremony happens once per task, at the end.** No mid-task branch resets, restarts, or checkouts. Never stack new work on merged history — that's where conflicts came from; the session-start hook restarts a clean branch from `origin/main` automatically. The only mid-task restart is a fix-forward after a red merge, and even that is one restart. @@ -72,6 +72,7 @@ Delegation policy lives in the **`fan-out`** skill (`.claude/skills/fan-out`) ## Communication **Telegraph style everywhere** — chat, statuses, quips, briefs. Fragments beat sentences; cut courtesies, hedges, recaps, transitions, play-by-play. Target ~20% of polite prose. If a word gives the reader nothing, cut it. +- **Plain words for the root thing, always — code-level included.** Every explanation — an issue, a bug, a status, a next step, a PR description, even a walk through the actual code — leads with what's actually broken/true in ordinary language. "The engine doesn't auto-place props from the scene file" beats "AuthoredScene can't place catalog objects from editor doc." Naming the file/function/type (the "where") is fine and often needed; convoluted phrasing never is. If a sentence needs re-reading to parse, rewrite it, jargon or not. - **Hard cap: a normal reply fits one phone screen (~8 lines).** The user reads on mobile. One reply per turn — no interleaved narration between tool calls. If it doesn't change what the user does next, don't write it. - Result + decision only. Reasoning stays internal unless asked. diff --git a/CLAUDE.md b/CLAUDE.md index 532baf814..7de13b7e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,8 @@ Genre-agnostic pure-TypeScript game engine SDK plus its agent skills. Published - **Editor-first for scene, placement, and asset work.** Building or changing what's in a world — placing spawns/objects/props, laying paths/roads/zones, painting terrain/materials, scattering foliage, authoring assets — is done **through the scene editor** (its RPC/CLI `bun packages/editor/src/mcp/cli.ts` or the GUI) and saved into the scene document (`editor.scene.json`), consumed at runtime by engine primitives (`` etc.). The builder should make this easy and look good with zero tuning. When the editor *can't* do something you need, **file a `[FEATURE]` issue for the missing editor/engine capability first**, then fall back to code and note the gap in the PR. Never hardcode level geometry, waypoint arrays, or bespoke per-game placement/render code when the editor could own it — that's the smell (see Design principles → "Author scenes in the editor"). - **Every session is its own ephemeral cloud container.** The **main session** works directly on its assigned `claude/...` branch — no worktrees, no branch juggling. Commit and push early: `git push -u origin ` on its own line (never piped through a filter — a non-zero grep silently drops the push). `warn-unpushed` Stop hook catches strandings. Exception, and only here: parallel **shipping subagents** each run in their own isolated git worktree (`Agent({ isolation: "worktree" })`) so N tasks ship N PRs at once without stomping the shared tree — see `fan-out`. Main never juggles worktrees; the subagents do, and they auto-clean. - **Ship = push → PR → subscribe → stop. Never merge.** When work is done and clean: push, open the PR (`create_pull_request`, ready for review), `subscribe_pr_activity`, report the link, **end the turn** — no waiting or polling. **One PR per branch, ever**: before creating, check none exists (`list_pull_requests` with `head`); if it does, the push already updated it — never open a second. -- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~30s `quick` job; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. -- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` surfaces on the next PR; fix forward from `origin/main` on a fresh branch). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. +- **Silence is green.** The subscription delivers CI failures as chat events (PRs run only the ~1min `quick` job incl. `check-skill-api`; the local gate proved the rest). Failure event → fix on the **same branch**, push, end turn. Never `merge_pull_request`/`enable_pr_auto_merge` unless the user asked this session. Never arm `send_later`/triggers/remote sessions to babysit CI. +- **The user owns merging.** PRs sit parked until they say so in chat. When asked to merge: squash-merge, report, done — no post-merge babysitting of `main` (a red `main` auto-files/updates a "CI red on main" issue; fix forward from `origin/main` on a fresh branch and close it). One exit before green: a red run whose fix lives outside the repo — report it, hand off as a browser-agent prompt (see Communication), stop. - **One task, one PR — never chunk.** A task ships as a single PR, however big. Never slice one request into separately-PR'd parts "to keep them small." Keep working the same branch until the whole task is done, then ship once. - **New task in the same session = fresh branch off `origin/main`, new PR.** The previous branch stays parked under its PR — never add to it, never reset it. `git fetch origin main && git checkout -b claude/ origin/main`. Many small parked PRs are the steady state. - **Git ceremony happens once per task, at the end.** No mid-task branch resets, restarts, or checkouts. Never stack new work on merged history — that's where conflicts came from; the session-start hook restarts a clean branch from `origin/main` automatically. The only mid-task restart is a fix-forward after a red merge, and even that is one restart. diff --git a/packages/assets/tsconfig.build.json b/packages/assets/tsconfig.build.json index 9ee6ad87c..d821eeed6 100644 --- a/packages/assets/tsconfig.build.json +++ b/packages/assets/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/packages/convex/tsconfig.build.json b/packages/convex/tsconfig.build.json index 3fa3c21a7..620f51669 100644 --- a/packages/convex/tsconfig.build.json +++ b/packages/convex/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json index 9fa850010..4c71f4c49 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -10,7 +10,7 @@ "src" ], "exclude": [ - "src/**/*.test.ts", + "src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts" ] } \ No newline at end of file diff --git a/packages/editor/tsconfig.build.json b/packages/editor/tsconfig.build.json index bd7b2340c..64754d8ac 100644 --- a/packages/editor/tsconfig.build.json +++ b/packages/editor/tsconfig.build.json @@ -11,7 +11,7 @@ }, "include": ["src"], "exclude": [ - "src/**/*.test.ts", + "src/**/*.test.ts", "src/**/*.test.tsx", "src/mcp/cli.ts", "src/mcp/bridgeServer.node.ts", "src/mcp/stdioServer.ts", diff --git a/packages/github/tsconfig.build.json b/packages/github/tsconfig.build.json index 91a4a7c9d..ec0832c63 100644 --- a/packages/github/tsconfig.build.json +++ b/packages/github/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/packages/jgengine/tsconfig.build.json b/packages/jgengine/tsconfig.build.json index eb871faba..fb5726cbe 100644 --- a/packages/jgengine/tsconfig.build.json +++ b/packages/jgengine/tsconfig.build.json @@ -7,5 +7,5 @@ "rootDir": "src" }, "include": ["src"], - "exclude": ["src/**/*.test.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/packages/node/tsconfig.build.json b/packages/node/tsconfig.build.json index 8a839cc22..978b94534 100644 --- a/packages/node/tsconfig.build.json +++ b/packages/node/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/react/tsconfig.build.json b/packages/react/tsconfig.build.json index a16565534..53f9e4fe4 100644 --- a/packages/react/tsconfig.build.json +++ b/packages/react/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/shell/tsconfig.build.json b/packages/shell/tsconfig.build.json index 8b6b48b64..7572c95dd 100644 --- a/packages/shell/tsconfig.build.json +++ b/packages/shell/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/cartridge/testkit.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/cartridge/testkit.ts"] } diff --git a/packages/sql/tsconfig.build.json b/packages/sql/tsconfig.build.json index ed459ae9b..b8d88e557 100644 --- a/packages/sql/tsconfig.build.json +++ b/packages/sql/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/packages/ws/tsconfig.build.json b/packages/ws/tsconfig.build.json index 8a5f5a472..d4cad0062 100644 --- a/packages/ws/tsconfig.build.json +++ b/packages/ws/tsconfig.build.json @@ -10,5 +10,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/testFixtures.ts"] + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/testFixtures.ts"] } diff --git a/scripts/api-doc-baseline.json b/scripts/api-doc-baseline.json index 668655335..b9931a41e 100644 --- a/scripts/api-doc-baseline.json +++ b/scripts/api-doc-baseline.json @@ -2773,23 +2773,14 @@ "@jgengine/shell/drivers/FrameDriver#POSTER_SETTLE_SECONDS", "@jgengine/shell/drivers/HudOnlyDriver#HudOnlyDriver", "@jgengine/shell/environment#DaylightCycleConfig", - "@jgengine/shell/environment#DaylightCycleConfig", - "@jgengine/shell/environment#DaylightProps", "@jgengine/shell/environment#DaylightProps", "@jgengine/shell/environment#DaylightState", - "@jgengine/shell/environment#DaylightState", - "@jgengine/shell/environment#EnvironmentScene", "@jgengine/shell/environment#EnvironmentScene", "@jgengine/shell/environment#EnvironmentSceneProps", - "@jgengine/shell/environment#EnvironmentSceneProps", - "@jgengine/shell/environment#SKY_PRESET_DAY_FRACTION", "@jgengine/shell/environment#SKY_PRESET_DAY_FRACTION", "@jgengine/shell/environment#SkyDaylightProps", - "@jgengine/shell/environment#SkyDaylightProps", - "@jgengine/shell/environment#SkyDomeProps", "@jgengine/shell/environment#SkyDomeProps", "@jgengine/shell/environment#TimeOfDayDaylightProps", - "@jgengine/shell/environment#TimeOfDayDaylightProps", "@jgengine/shell/environment/Daylight#DaylightProps", "@jgengine/shell/environment/Daylight#SkyDaylightProps", "@jgengine/shell/environment/Daylight#SkyDomeProps", @@ -2859,22 +2850,13 @@ "@jgengine/shell/settings/settingsController#SettingsControllerInput", "@jgengine/shell/settings/settingsController#useSettingsCategories", "@jgengine/shell/structures#BuildingBlock", - "@jgengine/shell/structures#BuildingBlock", "@jgengine/shell/structures#BuildingBlockProps", - "@jgengine/shell/structures#BuildingBlockProps", - "@jgengine/shell/structures#GeneratedBuilding", "@jgengine/shell/structures#GeneratedBuilding", "@jgengine/shell/structures#GeneratedBuildingProps", - "@jgengine/shell/structures#GeneratedBuildingProps", - "@jgengine/shell/structures#InstancedBuildingPlacement", "@jgengine/shell/structures#InstancedBuildingPlacement", "@jgengine/shell/structures#InstancedBuildings", - "@jgengine/shell/structures#InstancedBuildings", - "@jgengine/shell/structures#InstancedBuildingsProps", "@jgengine/shell/structures#InstancedBuildingsProps", "@jgengine/shell/structures#PlacementGhost", - "@jgengine/shell/structures#PlacementGhost", - "@jgengine/shell/structures#PlacementGhostProps", "@jgengine/shell/structures#PlacementGhostProps", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlock", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlockProps", @@ -2892,55 +2874,30 @@ "@jgengine/shell/structures/PlacementGhost#PlacementGhost", "@jgengine/shell/structures/PlacementGhost#PlacementGhostProps", "@jgengine/shell/terrain#CarvedTerrainProps", - "@jgengine/shell/terrain#CarvedTerrainProps", - "@jgengine/shell/terrain#DEFAULT_GRASS_WIND", "@jgengine/shell/terrain#DEFAULT_GRASS_WIND", "@jgengine/shell/terrain#EditableGround", - "@jgengine/shell/terrain#EditableGround", - "@jgengine/shell/terrain#EditableGroundProps", "@jgengine/shell/terrain#EditableGroundProps", "@jgengine/shell/terrain#FieldGroundOptions", - "@jgengine/shell/terrain#FieldGroundOptions", - "@jgengine/shell/terrain#GrassBladeGeometryOptions", "@jgengine/shell/terrain#GrassBladeGeometryOptions", "@jgengine/shell/terrain#GrassField", - "@jgengine/shell/terrain#GrassField", - "@jgengine/shell/terrain#GrassFieldProps", "@jgengine/shell/terrain#GrassFieldProps", "@jgengine/shell/terrain#GrassMaterialHandle", - "@jgengine/shell/terrain#GrassMaterialHandle", - "@jgengine/shell/terrain#GrassMaterialOptions", "@jgengine/shell/terrain#GrassMaterialOptions", "@jgengine/shell/terrain#GrassRange", - "@jgengine/shell/terrain#GrassRange", - "@jgengine/shell/terrain#GrassShaderUniforms", "@jgengine/shell/terrain#GrassShaderUniforms", "@jgengine/shell/terrain#GrassWindOptions", - "@jgengine/shell/terrain#GrassWindOptions", - "@jgengine/shell/terrain#ProceduralGround", "@jgengine/shell/terrain#ProceduralGround", "@jgengine/shell/terrain#ProceduralGroundProps", - "@jgengine/shell/terrain#ProceduralGroundProps", - "@jgengine/shell/terrain#ProceduralTerrainConfig", "@jgengine/shell/terrain#ProceduralTerrainConfig", "@jgengine/shell/terrain#ResolvedGrassBladeGeometryOptions", - "@jgengine/shell/terrain#ResolvedGrassBladeGeometryOptions", - "@jgengine/shell/terrain#ResolvedTerrainSegments", "@jgengine/shell/terrain#ResolvedTerrainSegments", "@jgengine/shell/terrain#ResolvedTerrainSize", - "@jgengine/shell/terrain#ResolvedTerrainSize", - "@jgengine/shell/terrain#TerraformBrushCursor", "@jgengine/shell/terrain#TerraformBrushCursor", "@jgengine/shell/terrain#TerraformBrushCursorProps", - "@jgengine/shell/terrain#TerraformBrushCursorProps", - "@jgengine/shell/terrain#TerrainArea", "@jgengine/shell/terrain#TerrainArea", "@jgengine/shell/terrain#TerrainHeightSampler", - "@jgengine/shell/terrain#TerrainHeightSampler", - "@jgengine/shell/terrain#TerrainSeed", "@jgengine/shell/terrain#TerrainSeed", "@jgengine/shell/terrain#TerrainVertexColorOptions", - "@jgengine/shell/terrain#TerrainVertexColorOptions", "@jgengine/shell/terrain/CarvedTerrain#CarvedTerrainProps", "@jgengine/shell/terrain/EditableGround#EditableGround", "@jgengine/shell/terrain/EditableGround#EditableGroundProps", @@ -2981,45 +2938,25 @@ "@jgengine/shell/vision/RevealVision#RevealVisionOptions", "@jgengine/shell/vision/frustumSampleEqual#frustumSampleDisplayEqual", "@jgengine/shell/water#DEFAULT_OCEAN_CONFIG", - "@jgengine/shell/water#DEFAULT_OCEAN_CONFIG", "@jgengine/shell/water#MAX_OCEAN_WAVES", - "@jgengine/shell/water#MAX_OCEAN_WAVES", - "@jgengine/shell/water#OCEAN_QUALITY_PRESETS", "@jgengine/shell/water#OCEAN_QUALITY_PRESETS", "@jgengine/shell/water#Ocean", - "@jgengine/shell/water#Ocean", - "@jgengine/shell/water#OceanColorConfig", "@jgengine/shell/water#OceanColorConfig", "@jgengine/shell/water#OceanConfig", - "@jgengine/shell/water#OceanConfig", - "@jgengine/shell/water#OceanDirectionVector", "@jgengine/shell/water#OceanDirectionVector", "@jgengine/shell/water#OceanFoamConfig", - "@jgengine/shell/water#OceanFoamConfig", - "@jgengine/shell/water#OceanMaterialUniforms", "@jgengine/shell/water#OceanMaterialUniforms", "@jgengine/shell/water#OceanProps", - "@jgengine/shell/water#OceanProps", - "@jgengine/shell/water#OceanQualityPreset", "@jgengine/shell/water#OceanQualityPreset", "@jgengine/shell/water#OceanShaderMaterial", - "@jgengine/shell/water#OceanShaderMaterial", - "@jgengine/shell/water#OceanWaveConfig", "@jgengine/shell/water#OceanWaveConfig", "@jgengine/shell/water#OceanWaveDirection", - "@jgengine/shell/water#OceanWaveDirection", - "@jgengine/shell/water#ResolvedOceanColorConfig", "@jgengine/shell/water#ResolvedOceanColorConfig", "@jgengine/shell/water#ResolvedOceanConfig", - "@jgengine/shell/water#ResolvedOceanConfig", - "@jgengine/shell/water#ResolvedOceanFoamConfig", "@jgengine/shell/water#ResolvedOceanFoamConfig", "@jgengine/shell/water#ResolvedOceanWaveConfig", - "@jgengine/shell/water#ResolvedOceanWaveConfig", - "@jgengine/shell/water#createOceanMaterial", "@jgengine/shell/water#createOceanMaterial", "@jgengine/shell/water#syncOceanMaterial", - "@jgengine/shell/water#syncOceanMaterial", "@jgengine/shell/water/Ocean#Ocean", "@jgengine/shell/water/Ocean#OceanProps", "@jgengine/shell/water/OceanConfig#DEFAULT_OCEAN_CONFIG", diff --git a/scripts/api-orphan-baseline.json b/scripts/api-orphan-baseline.json index 603579691..43b5b697a 100644 --- a/scripts/api-orphan-baseline.json +++ b/scripts/api-orphan-baseline.json @@ -29,7 +29,6 @@ "@jgengine/assets/verify#verifyData", "@jgengine/assets/verify#verifyManifest", "@jgengine/convex#createConvexChatSync", - "@jgengine/convex#createConvexChatTransport", "@jgengine/convex#createConvexFeedWrites", "@jgengine/convex#createConvexGameFeeds", "@jgengine/convex#createConvexGameTransport", @@ -39,7 +38,6 @@ "@jgengine/convex#defaultConvexGameApi", "@jgengine/convex#randomConvexPlayerId", "@jgengine/convex#watchConvexQuery", - "@jgengine/convex/convexChatTransport#createConvexChatTransport", "@jgengine/convex/convexPresenceTransport#createConvexPresenceTransport", "@jgengine/convex/createConvexGameTransport#createConvexChatSync", "@jgengine/convex/createConvexGameTransport#createConvexFeedWrites", @@ -52,19 +50,12 @@ "@jgengine/convex/resolveConvexMultiplayer#randomConvexPlayerId", "@jgengine/core/audio/synth#patchDuration", "@jgengine/core/cards/cardPile#countIn", - "@jgengine/core/cards/cardPile#shuffleZone", "@jgengine/core/cards/cardPile#zoneOf", - "@jgengine/core/cards/modifierPipeline#createModifierPipeline", "@jgengine/core/combat/attackTags#hasAnyTag", - "@jgengine/core/combat/attackTags#hasTag", "@jgengine/core/combat/comboString#stepById", "@jgengine/core/combat/death#deathReasonFromEffect", "@jgengine/core/combat/death#normalizeOnDeath", - "@jgengine/core/combat/defensiveWindow#iframeActiveAt", "@jgengine/core/combat/defensiveWindow#totalWindowMs", - "@jgengine/core/combat/defensiveWindow#windowActiveAt", - "@jgengine/core/combat/effects#resolveAreaTargets", - "@jgengine/core/combat/hitReaction#applyImpulse", "@jgengine/core/combat/resistance#UnknownResistanceCategoryError", "@jgengine/core/combat/resistance#UnknownResistancePropertyError", "@jgengine/core/combat/shotOrigin#aimDirection", @@ -72,22 +63,9 @@ "@jgengine/core/commands/commandRegistry#createCommandRegistry", "@jgengine/core/crafting/production#acceptsInput", "@jgengine/core/crafting/recipe#hasRecipeInputs", - "@jgengine/core/crafting/recipe#missingInputs", - "@jgengine/core/data/dataSource#createDataSource", - "@jgengine/core/data/devProxy#parseDevProxyTable", - "@jgengine/core/data/devProxy#proxiedUrl", - "@jgengine/core/data/fetchJson#HttpStatusError", - "@jgengine/core/data/fetchJson#JsonParseError", - "@jgengine/core/data/fetchJson#fetchJson", - "@jgengine/core/data/jsonDataSource#createJsonDataSource", "@jgengine/core/devtools/devtools#formatLogMessage", "@jgengine/core/devtools/devtools#measureProfile", - "@jgengine/core/economy/wallet#canAfford", "@jgengine/core/format/duration#padNumber", - "@jgengine/core/game/chat#createChat", - "@jgengine/core/game/chat#whisperChannelId", - "@jgengine/core/game/chatFilter#createChatFilter", - "@jgengine/core/game/chatFilter#normalizeChatText", "@jgengine/core/game/connectedPlayers#createConnectedPlayers", "@jgengine/core/game/controlGate#setPlayControlsActive", "@jgengine/core/game/feed#appendFeedEntry", @@ -96,31 +74,19 @@ "@jgengine/core/game/keyValueStore#defaultKeyValueStorage", "@jgengine/core/game/lootTable#grantDrops", "@jgengine/core/game/objectives#evaluateObjectives", - "@jgengine/core/game/ping#classifyPing", "@jgengine/core/game/quest#applyQuestRewards", "@jgengine/core/game/quest#createQuestEvaluator", - "@jgengine/core/game/race#everyoneFinishes", - "@jgengine/core/game/race#lastStanding", - "@jgengine/core/game/race#topK", - "@jgengine/core/game/runDraft#createRunModifierStack", "@jgengine/core/game/unlocks#grantUnlock", "@jgengine/core/game/unlocks#hasUnlock", "@jgengine/core/game/unlocks#unlockTree", "@jgengine/core/input/bindingOverrides#bindingOverridesStorageKey", "@jgengine/core/input/bindingOverrides#clearAllBindingOverrides", "@jgengine/core/input/lookChannel#createLookChannel", - "@jgengine/core/input/pointer#createDragCapture", - "@jgengine/core/input/pointer#groundOf", - "@jgengine/core/input/pointer#moveTargetFromHit", "@jgengine/core/input/pointerAxis#pointerAxisValue", - "@jgengine/core/input/touchScheme#touchActionLabel", "@jgengine/core/input/touchScheme#touchButtonKind", "@jgengine/core/interaction/proximityPrompt#positionedPromptsEqual", "@jgengine/core/interaction/proximityPrompt#promptCommandsEqual", "@jgengine/core/interaction/proximityPrompt#promptDisplaysEqual", - "@jgengine/core/interaction/qte#qteProgress", - "@jgengine/core/interaction/skillCheck#skillCheckMarkerPosition", - "@jgengine/core/inventory/storageTier#tierOf", "@jgengine/core/item/durability#canRepairAt", "@jgengine/core/item/durability#isBroken", "@jgengine/core/item/durability#wearAmount", @@ -131,24 +97,17 @@ "@jgengine/core/movement/avatarGait#gaitSwayAngle", "@jgengine/core/movement/playerMovement#forgetPlayerMovement", "@jgengine/core/movement/playerMovement#playerMovementHeading", - "@jgengine/core/movement/playerMovement#resolvePhysicsTuning", "@jgengine/core/movement/steering#steerToward", "@jgengine/core/movement/steering#yawForward", "@jgengine/core/movement/steering#yawRight", "@jgengine/core/movement/voxelController#advanceVoxelPlayer", "@jgengine/core/movement/voxelController#createVoxelPlayerBody", - "@jgengine/core/multiplayer/chatContract#createLocalChatTransport", - "@jgengine/core/multiplayer/matchmaking#generateJoinCode", "@jgengine/core/multiplayer/matchmaking#hasSpace", - "@jgengine/core/multiplayer/matchmaking#matchesFilter", "@jgengine/core/multiplayer/presenceContract#createLocalPresenceTransport", "@jgengine/core/nav/corridors#createCorridorField", - "@jgengine/core/nav/navGrid#smoothPath", "@jgengine/core/nav/railGraph#createRailGraph", "@jgengine/core/nav/railGraph#createRailRider", "@jgengine/core/nav/timetable#createRouteTimetable", - "@jgengine/core/physics/buoyancy#BuoyantBody", - "@jgengine/core/physics/damageZones#DamageModel", "@jgengine/core/physics/flowTube#combineFlowVelocity", "@jgengine/core/physics/flowTube#createFlowTube", "@jgengine/core/physics/forceVolume#applyVolumeForce", @@ -156,42 +115,19 @@ "@jgengine/core/physics/physicsWorld#cellCoord", "@jgengine/core/physics/physicsWorld#cellIndex", "@jgengine/core/physics/radialImpulse#radialImpulse", - "@jgengine/core/physics/ragdoll#Ragdoll", - "@jgengine/core/physics/vehicleBody#VehicleBody", - "@jgengine/core/random/nameGen#fillTemplate", - "@jgengine/core/random/nameGen#pickFrom", "@jgengine/core/random/rng#hashString", "@jgengine/core/random/rng#randomSeedFrom", "@jgengine/core/random/rng#stepRandomSeed", "@jgengine/core/scene/assetPreload#createSceneAssetPreloader", - "@jgengine/core/scene/autoTarget#createAutoTargeter", - "@jgengine/core/scene/behaviors#promptable", - "@jgengine/core/scene/captureCheck#captureChance", - "@jgengine/core/scene/captureCheck#rollCapture", - "@jgengine/core/scene/entityStore#createEntityStore", - "@jgengine/core/scene/entityStore#movedWhileFrozen", - "@jgengine/core/scene/form#createForms", - "@jgengine/core/scene/movementSpeed#applyStatDrivenSpeed", "@jgengine/core/scene/movementSpeed#deriveWalkSpeed", "@jgengine/core/scene/objectQuery#intersectAabb", "@jgengine/core/scene/objectQuery#normalizeDirection", - "@jgengine/core/scene/objectStore#createObjectStore", - "@jgengine/core/scene/paintLayer#createPaintLayer", - "@jgengine/core/scene/possession#createPossession", - "@jgengine/core/scene/roster#createRoster", "@jgengine/core/scene/sceneRaycast#createSceneRaycast", - "@jgengine/core/scene/selection#rectContainsPoint", - "@jgengine/core/scene/spatial#createSpatialApi", - "@jgengine/core/scene/spatial#distanceBetween", - "@jgengine/core/scene/stationClaim#StationClaim", "@jgengine/core/scene/targeting#createTargeting", "@jgengine/core/settings/settingsModel#loadSettingValue", "@jgengine/core/settings/settingsModel#saveSettingValue", "@jgengine/core/settings/settingsModel#settingStorageKey", - "@jgengine/core/time/gameClock#computeGameDay", - "@jgengine/core/time/gameClock#getScaledElapsedMs", "@jgengine/core/time/serverTick#planServerTick", - "@jgengine/core/time/simClock#createSimClock", "@jgengine/core/time/stateSchedule#createStateSchedule", "@jgengine/core/time/stateSchedule#nextClearWindow", "@jgengine/core/ui/hudScale#rectOverflow", @@ -211,23 +147,13 @@ "@jgengine/core/visibility/spatialIndex#createSpatialIndex", "@jgengine/core/world/cellStates#createCellStateGrid", "@jgengine/core/world/connectors#collectWorldSockets", - "@jgengine/core/world/features#island", "@jgengine/core/world/features#padFlattenMasks", "@jgengine/core/world/gridInstances#resolveGridCells", - "@jgengine/core/world/interiors#createInteriors", "@jgengine/core/world/mapLayers#pointInMapZone", "@jgengine/core/world/massing#composeMassing", "@jgengine/core/world/massing#massingFloorCount", - "@jgengine/core/world/placement#footprintObstacle", - "@jgengine/core/world/roads#isOnRoad", - "@jgengine/core/world/roads#nearestOnPath", - "@jgengine/core/world/roads#pathLength", - "@jgengine/core/world/scatter#scatterAabb", "@jgengine/core/world/segment#circleVsSegment", "@jgengine/core/world/segment#closestPointOnSegment", - "@jgengine/core/world/streets#sidewalkPaths", - "@jgengine/core/world/terraform#brushWeight", - "@jgengine/core/world/water#synthesizeWaves", "@jgengine/core/world/windZones#createWindZones", "@jgengine/editor#AssetBrowser", "@jgengine/editor#EditorLayerOverlays", @@ -262,16 +188,10 @@ "@jgengine/node/testFixtures#createTestRuntime", "@jgengine/node/webHandler#toWebRequest", "@jgengine/shell/GamePhaseStamp#GamePhaseStamp", - "@jgengine/shell/audio/AudioComponents#AudioListener", - "@jgengine/shell/audio/AudioComponents#EntityAudioEmitters", - "@jgengine/shell/audio/AudioComponents#ObjectAudioEmitters", "@jgengine/shell/audio/audioEngine#createAudioEngine", - "@jgengine/shell/audio/musicDirector#MusicDirector", "@jgengine/shell/audio/musicVoices#playMusicNote", "@jgengine/shell/audio/synthEngine#createNoiseBuffer", "@jgengine/shell/audio/synthEngine#realizeSynthPatch", - "@jgengine/shell/behaviour#attachObject3D", - "@jgengine/shell/behaviourAttach#attachObject3D", "@jgengine/shell/camera#GameCameraRig", "@jgengine/shell/camera#GameFirstPersonCamera", "@jgengine/shell/camera#GameInspectionCamera", @@ -305,12 +225,10 @@ "@jgengine/shell/devtools/devtoolsOverrides#readStoredOverrides", "@jgengine/shell/devtools/panelAtoms#SectionLabel", "@jgengine/shell/devtools/panelAtoms#StatRow", - "@jgengine/shell/devtools/panelAtoms#ms", "@jgengine/shell/devtools/perfDiagnose#diagnose", "@jgengine/shell/diagnostics/RuntimeDiagnostics#DiagnosticOverlay", "@jgengine/shell/diagnostics/RuntimeDiagnostics#GameUiErrorBoundary", "@jgengine/shell/diagnostics/RuntimeDiagnostics#logRuntimeError", - "@jgengine/shell/drivers/FrameDriver#FrameDriver", "@jgengine/shell/drivers/HudOnlyDriver#HudOnlyDriver", "@jgengine/shell/environment/GroundPad#GroundPad", "@jgengine/shell/environment/RoadRibbons#RoadRibbons", @@ -321,7 +239,6 @@ "@jgengine/shell/pointer/PointerProbe#PointerProbe", "@jgengine/shell/postfx/PostProcessing#PostProcessing", "@jgengine/shell/postfx/gradeShader#createGradePass", - "@jgengine/shell/registry#resolveGameLoader", "@jgengine/shell/render/SceneLighting#BackdropFog", "@jgengine/shell/render/SceneLighting#ConfiguredLighting", "@jgengine/shell/render/SceneModels#EntityModel", @@ -336,46 +253,32 @@ "@jgengine/shell/settings/appliedSettings#useSettingsRevision", "@jgengine/shell/settings/settingsController#useSettingsCategories", "@jgengine/shell/structures#BuildingBlock", - "@jgengine/shell/structures#GeneratedBuilding", "@jgengine/shell/structures#InstancedBuildings", "@jgengine/shell/structures/GeneratedBuilding#BuildingBlock", - "@jgengine/shell/structures/GeneratedBuilding#GeneratedBuilding", "@jgengine/shell/structures/GeneratedBuilding#InstancedBuildings", "@jgengine/shell/touch/OrientationHint#OrientationHint", "@jgengine/shell/touch/TouchControlsOverlay#TouchControlsDock", "@jgengine/shell/touch/TouchControlsOverlay#primaryButtonOffsets", "@jgengine/shell/touch/TouchControlsOverlay#touchDockClearance", "@jgengine/shell/useShellMultiplayerSync#useShellMultiplayerSync", - "@jgengine/shell/visibility/CullingProvider#CullingProvider", "@jgengine/shell/visibility/CullingProvider#useRenderVisibility", "@jgengine/shell/vision/FrustumSensorHud#frustumSampleDisplayEqual", "@jgengine/shell/vision/FrustumSensorHud#useFrustumSensor", "@jgengine/shell/vision/RevealVision#useRevealHits", "@jgengine/shell/vision/frustumSampleEqual#frustumSampleDisplayEqual", - "@jgengine/shell/water#Ocean", "@jgengine/shell/water#createOceanMaterial", "@jgengine/shell/water#syncOceanMaterial", - "@jgengine/shell/water/Ocean#Ocean", "@jgengine/shell/water/OceanMaterial#createOceanMaterial", "@jgengine/shell/water/OceanMaterial#syncOceanMaterial", "@jgengine/shell/weather#LightningStrike", - "@jgengine/shell/weather#RainField", - "@jgengine/shell/weather#SnowField", "@jgengine/shell/weather#WeatherLayer", "@jgengine/shell/weather/LightningStrike#LightningStrike", - "@jgengine/shell/weather/RainField#RainField", - "@jgengine/shell/weather/SnowField#SnowField", "@jgengine/shell/weather/WeatherLayer#WeatherLayer", "@jgengine/shell/world/DataObjects#DataObjects", "@jgengine/shell/world/GridWorldScene#GridWorldScene", - "@jgengine/shell/world/InstancedBodies#InstancedBodies", - "@jgengine/shell/world/InstancedJoints#InstancedJoints", - "@jgengine/shell/world/SpriteBatch#SpriteBatch", "@jgengine/shell/world/WorldHud#ProjectileTracers", "@jgengine/shell/world/WorldHud#WorldFloatText", "@jgengine/shell/world/WorldHud#WorldTelegraphs", - "@jgengine/shell/world/WorldItems#WorldItems", - "@jgengine/shell/world/WorldScene#RemotePlayers", "@jgengine/shell/world/WorldScene#WorldView", "@jgengine/ws#computeVoiceGain", "@jgengine/ws/voiceChannel#computeVoiceGain" diff --git a/scripts/gen-skill-api-safe.ts b/scripts/gen-skill-api-safe.ts index 14e8de911..7008275b0 100644 --- a/scripts/gen-skill-api-safe.ts +++ b/scripts/gen-skill-api-safe.ts @@ -35,7 +35,8 @@ function restore(): void { for (const [path, content] of before) writeFileSync(path, content); } -if (result.status !== 0) { +const generationCompleted = result.status === 0 || result.status === 2; +if (!generationCompleted) { restore(); process.exit(result.status ?? 1); } @@ -65,3 +66,8 @@ try { console.error(`skill-api generation validation failed: ${String(error)}`); process.exit(1); } + +if (result.status === 2) { + console.error("skill-api: generated files kept; gate failures above still need fixing"); + process.exit(2); +} diff --git a/scripts/gen-skill-api.ts b/scripts/gen-skill-api.ts index 557f4520b..3665a0ef0 100644 --- a/scripts/gen-skill-api.ts +++ b/scripts/gen-skill-api.ts @@ -75,9 +75,10 @@ const ORPHAN_GATED_KINDS = new Set(["function", "class"]); function collectOrphans(root: string, skills: SkillModules): string[] { const adoption = collectAdoption(root); + const tokens = new Set(); + for (const skill of SKILL_DIRS) for (const t of collectSkillTokens(root, skill)) tokens.add(t); const orphans: string[] = []; - for (const [skill, refs] of skills) { - const tokens = collectSkillTokens(root, skill); + for (const refs of skills.values()) { for (const ref of refs) { if (adoption.namespaceModules.has(ref.importPath)) continue; for (const e of ref.exports) { @@ -96,6 +97,15 @@ function main(): void { const root = fileURLToPath(new URL("..", import.meta.url)); const failures: string[] = []; + const extractedPackages = ["core", ...Object.keys(PACKAGE_SKILLS)]; + const missingDist = extractedPackages.filter((pkg) => !existsSync(join(root, "packages", pkg, "dist"))); + if (missingDist.length > 0) { + console.error( + `skill-api refused: missing dist for ${missingDist.join(", ")} — run \`bun run build\` first (extraction silently drops modules that resolve through unbuilt packages)`, + ); + process.exit(1); + } + const { skills, undocumented } = collectSkillModules(root); const undocumentedSet = new Set(undocumented); const baseline = readBaseline(root, BASELINE_PATH); @@ -176,7 +186,7 @@ function main(): void { if (failures.length > 0) { console.error(`\ncheck-skill-api failed:\n${failures.map((f) => ` ${f}`).join("\n")}\n`); - process.exit(1); + process.exit(check ? 1 : 2); } const total = [...skills.values()].reduce((n, m) => n + m.length, 0); console.log( From 3044e3292cef2f41a91042548cd4308d213760fe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 21:51:00 +0000 Subject: [PATCH 02/10] Fix barrel generator's retired-skill reference and drifted gameplay barrel - gen-barrels/barrels.test still referenced jgengine-procedural after #883 retired the skill dir, crashing the sync tests (ENOENT); procedural.ts stays as a frozen compat barrel. - Regen gameplay barrel: survival symbols now route there, and document the four survival types the barrel newly surfaces. These test failures were invisible for the same reason as the orphan gate: main's CI dies at check-types before tests run. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- .claude/skills/jgengine-gameplay/api.md | 8 ++++---- .claude/skills/jgengine/api.md | 16 ++++++++++++---- packages/core/src/gameplay.ts | 3 +++ packages/core/src/survival/decayMeter.ts | 1 + packages/core/src/survival/moodle.ts | 2 ++ packages/core/src/survival/regionHealth.ts | 1 + scripts/api-doc-baseline.json | 8 -------- scripts/barrels.test.ts | 1 - scripts/gen-barrels.ts | 1 - 9 files changed, 23 insertions(+), 18 deletions(-) diff --git a/.claude/skills/jgengine-gameplay/api.md b/.claude/skills/jgengine-gameplay/api.md index acdd708f9..d5ba14c8e 100644 --- a/.claude/skills/jgengine-gameplay/api.md +++ b/.claude/skills/jgengine-gameplay/api.md @@ -992,7 +992,7 @@ ## @jgengine/core/survival/decayMeter - `DecayMeterConfig` (interface): interface DecayMeterConfig — ⚠ undocumented -- `DecayMeterSet` (interface): interface DecayMeterSet — ⚠ undocumented +- `DecayMeterSet` (interface): interface DecayMeterSet — Live handle over a set of survival meters (hunger, thirst, oxygen) that drain or refill as game time advances. - `DecayMeterState` (interface): interface DecayMeterState — ⚠ undocumented - `MeterThreshold` (interface): interface MeterThreshold — ⚠ undocumented - `createDecayMeterSet` (function): function createDecayMeterSet(configs: readonly DecayMeterConfig[]): DecayMeterSet — Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains (or recovers) on game-time `dt` at a configurable rate, refills from consumables or actions, and raises moodle statuses at thresholds. Rate modifiers let the environment drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads an environment field then calls `setRateModifier`. @@ -1000,10 +1000,10 @@ ## @jgengine/core/survival/moodle - `MOODLE_SEVERITY_ORDER` (const): const MOODLE_SEVERITY_ORDER: Record — ⚠ undocumented -- `Moodle` (interface): interface Moodle — ⚠ undocumented +- `Moodle` (interface): interface Moodle — One status icon (buff/debuff) with severity tiers, shown while its trigger condition holds. - `MoodleSeverity` (type): type MoodleSeverity = "good" | "neutral" | "warning" | "critical" — ⚠ undocumented - `MoodleSource` (type): type MoodleSource = "meter" | "ailment" | "buff" — ⚠ undocumented -- `MoodleStack` (interface): interface MoodleStack — ⚠ undocumented +- `MoodleStack` (interface): interface MoodleStack — Evaluates all registered moodles against current stats and returns the active, severity-ordered set. - `TimedMoodleInput` (interface): interface TimedMoodleInput — ⚠ undocumented - `createMoodleStack` (function): function createMoodleStack(): MoodleStack — A stateful holder for timed status moodles (food buffs, temporary shelter, warmth). Meters and multi-region health derive their own moodles on read; combine all three through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display. - `stackMoodles` (function): function stackMoodles(...groups: readonly (readonly Moodle[])[]): Moodle[] — Merge any number of moodle groups into one stack — meters, ailments, and buffs share this display. Same-id moodles fold together (stacks add, worst severity wins); the result is ordered worst-first so the HUD reads critical statuses at a glance. @@ -1014,7 +1014,7 @@ - `AilmentInstance` (interface): interface AilmentInstance — ⚠ undocumented - `DamageResult` (interface): interface DamageResult — ⚠ undocumented - `HealthRegionConfig` (interface): interface HealthRegionConfig — ⚠ undocumented -- `MultiRegionHealth` (interface): interface MultiRegionHealth — ⚠ undocumented +- `MultiRegionHealth` (interface): interface MultiRegionHealth — Health tracked per body region (head, torso, limbs), each with its own damage, bleed, and treatment state. - `MultiRegionHealthConfig` (interface): interface MultiRegionHealthConfig — ⚠ undocumented - `RegionHealthState` (interface): interface RegionHealthState — ⚠ undocumented - `TreatResult` (interface): interface TreatResult — ⚠ undocumented diff --git a/.claude/skills/jgengine/api.md b/.claude/skills/jgengine/api.md index 55ffbea47..451914f35 100644 --- a/.claude/skills/jgengine/api.md +++ b/.claude/skills/jgengine/api.md @@ -243,6 +243,7 @@ - `DEFAULT_PICKUP_RADIUS` (const): const DEFAULT_PICKUP_RADIUS: 2 — ⚠ undocumented - `DEFAULT_PING_CATEGORIES` (const): const DEFAULT_PING_CATEGORIES: Record — Content-agnostic default ping wheel: enemy / loot / location / danger. - `DEFAULT_TOUCH_STYLE` (const): const DEFAULT_TOUCH_STYLE: TouchStyle — Skin used when neither the game nor the player picks one. +- `DecayMeterSet` (interface): interface DecayMeterSet — Live handle over a set of survival meters (hunger, thirst, oxygen) that drain or refill as game time advances. - `DeliveryEntry` (interface): interface DeliveryEntry — ⚠ undocumented - `DeliveryQueue` (interface): interface DeliveryQueue — ⚠ undocumented - `DirectionalLightingConfig` (interface): interface DirectionalLightingConfig — ⚠ undocumented @@ -291,7 +292,10 @@ - `ModelMaterialMaps` (interface): interface ModelMaterialMaps — Real PBR map URLs (e.g. `buildMaterialCatalog(...).resolve(id)!.maps` from `@jgengine/assets`) layered onto a model's material — the seam for texturing an otherwise-flat/untextured GLB. Any role may be omitted to keep the model's own map. - `ModelMaterialOverride` (interface): interface ModelMaterialOverride — Per-entity PBR material override (#151.3) applied to every `MeshStandardMaterial` in the model's cloned scene graph. - `ModularItemDef` (interface): interface ModularItemDef — ⚠ undocumented +- `Moodle` (interface): interface Moodle — One status icon (buff/debuff) with severity tiers, shown while its trigger condition holds. +- `MoodleStack` (interface): interface MoodleStack — Evaluates all registered moodles against current stats and returns the active, severity-ordered set. - `MountSlotDef` (interface): interface MountSlotDef — ⚠ undocumented +- `MultiRegionHealth` (interface): interface MultiRegionHealth — Health tracked per body region (head, torso, limbs), each with its own damage, bleed, and treatment state. - `NEUTRAL_AXIS` (const): const NEUTRAL_AXIS: AxisInput — ⚠ undocumented - `ObjectStyle` (interface): interface ObjectStyle — ⚠ undocumented - `ObserverCameraConfig` (interface): interface ObserverCameraConfig — Detached spectator/photo cam (#120) — binds to any entity or fixed point, never reads player input. @@ -379,6 +383,7 @@ - `createChatRateLimiter` (function): function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter — ⚠ undocumented - `createCommitController` (function): function createCommitController(config: CommitControllerConfig): CommitController — ⚠ undocumented - `createCosmetics` (function): function createCosmetics(deps: CosmeticsDeps = {}): Cosmetics — Equip cosmetic skins and customizations by slot, independent of gameplay stats. +- `createDecayMeterSet` (function): function createDecayMeterSet(configs: readonly DecayMeterConfig[]): DecayMeterSet — Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains (or recovers) on game-time `dt` at a configurable rate, refills from consumables or actions, and raises moodle statuses at thresholds. Rate modifiers let the environment drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads an environment field then calls `setRateModifier`. - `createDeliveryQueue` (function): function createDeliveryQueue(): DeliveryQueue — ⚠ undocumented - `createDurability` (function): function createDurability(spec: DurabilitySpec): DurabilityState — ⚠ undocumented - `createDurabilityTracker` (function): function createDurabilityTracker(): DurabilityTracker — ⚠ undocumented @@ -398,6 +403,8 @@ - `createLoadouts` (function): function createLoadouts(deps: LoadoutDeps): Loadouts — Save, name, and swap equipment loadouts. - `createLootRegistry` (function): function createLootRegistry(): LootRegistry — Register named loot tables and roll weighted randomized drops from them. - `createModularItem` (function): function createModularItem(def: ModularItemDef, initial: readonly InstalledPart[] = []): ModularItem — ⚠ undocumented +- `createMoodleStack` (function): function createMoodleStack(): MoodleStack — A stateful holder for timed status moodles (food buffs, temporary shelter, warmth). Meters and multi-region health derive their own moodles on read; combine all three through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display. +- `createMultiRegionHealth` (function): function createMultiRegionHealth(config: MultiRegionHealthConfig): MultiRegionHealth — Per-region/limb health tracked separately, so each body part takes and heals damage on its own. - `createNameGenerator` (function): function createNameGenerator(options: NameGeneratorOptions): NameGenerator — Generate procedural names from templates and word banks with an injected random source. - `createPingSystem` (function): function createPingSystem(deps: PingSystemDeps): PingSystem — Contextual ping/marker communication between teammates, classified by what was pinged. - `createProductionState` (function): function createProductionState(): ProductionState — A production building that converts input items into outputs over time — factory/crafting station. @@ -477,6 +484,7 @@ - `shuffleWithRng` (function): function shuffleWithRng(values: readonly T[], rng: () => number): T[] — ⚠ undocumented - `slotAccepts` (function): function slotAccepts(slot: MountSlotDef, category: string): boolean — Attach parts into an item's mount slots and resolve the combined stats. - `splitSegments` (function): function splitSegments(splits: readonly number[], start = 0): number[] — Per-segment durations from a cumulative split book (`splits[i]` = elapsed time at checkpoint `i`): `segments[i] = splits[i] − splits[i−1]`, the first measured from `start` (default 0). Turns the cumulative splits {@link RacerProgress} records into the individual leg times a results screen shows. +- `stackMoodles` (function): function stackMoodles(...groups: readonly (readonly Moodle[])[]): Moodle[] — Merge any number of moodle groups into one stack — meters, ailments, and buffs share this display. Same-id moodles fold together (stacks add, worst severity wins); the result is ordered worst-first so the HUD reads critical statuses at a glance. - `startRaceCountdown` (function): function startRaceCountdown(options?: RaceCountdownOptions): RaceSessionState — Drop the lights: return a fresh `countdown` session of `seconds` (default 3). A non-positive length skips straight to `racing` for a standing start with no countdown. - `stationSatisfied` (function): function stationSatisfied(recipe: RecipeDef, context: CraftContext): boolean — ⚠ undocumented - `tickProduction` (function): function tickProduction(def: ProductionBuildingDef, state: ProductionState, input: ProductionTickInput): ProductionState — ⚠ undocumented @@ -536,10 +544,10 @@ ## @jgengine/core/procedural -- `DecayMeterSet` (interface): interface DecayMeterSet — ⚠ undocumented -- `Moodle` (interface): interface Moodle — ⚠ undocumented -- `MoodleStack` (interface): interface MoodleStack — ⚠ undocumented -- `MultiRegionHealth` (interface): interface MultiRegionHealth — ⚠ undocumented +- `DecayMeterSet` (interface): interface DecayMeterSet — Live handle over a set of survival meters (hunger, thirst, oxygen) that drain or refill as game time advances. +- `Moodle` (interface): interface Moodle — One status icon (buff/debuff) with severity tiers, shown while its trigger condition holds. +- `MoodleStack` (interface): interface MoodleStack — Evaluates all registered moodles against current stats and returns the active, severity-ordered set. +- `MultiRegionHealth` (interface): interface MultiRegionHealth — Health tracked per body region (head, torso, limbs), each with its own damage, bleed, and treatment state. - `createDecayMeterSet` (function): function createDecayMeterSet(configs: readonly DecayMeterConfig[]): DecayMeterSet — Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains (or recovers) on game-time `dt` at a configurable rate, refills from consumables or actions, and raises moodle statuses at thresholds. Rate modifiers let the environment drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads an environment field then calls `setRateModifier`. - `createMoodleStack` (function): function createMoodleStack(): MoodleStack — A stateful holder for timed status moodles (food buffs, temporary shelter, warmth). Meters and multi-region health derive their own moodles on read; combine all three through `stackMoodles(stack.list(), meterMoodles, ailmentMoodles)` for one display. - `createMultiRegionHealth` (function): function createMultiRegionHealth(config: MultiRegionHealthConfig): MultiRegionHealth — Per-region/limb health tracked separately, so each body part takes and heals damage on its own. diff --git a/packages/core/src/gameplay.ts b/packages/core/src/gameplay.ts index 5542c6f34..62e9e64c1 100644 --- a/packages/core/src/gameplay.ts +++ b/packages/core/src/gameplay.ts @@ -253,6 +253,9 @@ export { seededStreams } from "./random/rng"; export { createRing, ringSampleAt, type Ring, type RingConfig, type RingPhase } from "./session/ring"; export { type RoleSpec } from "./session/roles"; export { type RoundConfig, type RoundSnapshot } from "./session/roundState"; +export { createDecayMeterSet, type DecayMeterSet } from "./survival/decayMeter"; +export { createMoodleStack, stackMoodles, type Moodle, type MoodleStack } from "./survival/moodle"; +export { createMultiRegionHealth, type MultiRegionHealth } from "./survival/regionHealth"; export { createCommitController } from "./turn/commit"; export { createIntentBoard } from "./turn/intent"; export { createTurnLoop, type TurnLoop } from "./turn/turnLoop"; diff --git a/packages/core/src/survival/decayMeter.ts b/packages/core/src/survival/decayMeter.ts index 4efb0c3a0..c6868049d 100644 --- a/packages/core/src/survival/decayMeter.ts +++ b/packages/core/src/survival/decayMeter.ts @@ -36,6 +36,7 @@ export interface DecayMeterState { fraction: number; } +/** Live handle over a set of survival meters (hunger, thirst, oxygen) that drain or refill as game time advances. */ export interface DecayMeterSet { /** Drain/fill every meter by `rate * rateModifier * dt`. Call once per game tick. */ tick(dt: number): void; diff --git a/packages/core/src/survival/moodle.ts b/packages/core/src/survival/moodle.ts index 0577b3ccc..55522701c 100644 --- a/packages/core/src/survival/moodle.ts +++ b/packages/core/src/survival/moodle.ts @@ -2,6 +2,7 @@ export type MoodleSeverity = "good" | "neutral" | "warning" | "critical"; export type MoodleSource = "meter" | "ailment" | "buff"; +/** One status icon (buff/debuff) with severity tiers, shown while its trigger condition holds. */ export interface Moodle { id: string; label: string; @@ -72,6 +73,7 @@ interface TimedMoodleState { total: number | null; } +/** Evaluates all registered moodles against current stats and returns the active, severity-ordered set. */ export interface MoodleStack { /** Add or refresh a timed moodle (Valheim food buff, a temporary shelter status). */ add(input: TimedMoodleInput): void; diff --git a/packages/core/src/survival/regionHealth.ts b/packages/core/src/survival/regionHealth.ts index 10c3dfb2c..b250e9421 100644 --- a/packages/core/src/survival/regionHealth.ts +++ b/packages/core/src/survival/regionHealth.ts @@ -57,6 +57,7 @@ export interface TreatResult { treated: readonly string[]; } +/** Health tracked per body region (head, torso, limbs), each with its own damage, bleed, and treatment state. */ export interface MultiRegionHealth { damage(regionId: string, amount: number): DamageResult; heal(regionId: string, amount: number): RegionHealthState; diff --git a/scripts/api-doc-baseline.json b/scripts/api-doc-baseline.json index b9931a41e..286ff07ef 100644 --- a/scripts/api-doc-baseline.json +++ b/scripts/api-doc-baseline.json @@ -1307,10 +1307,6 @@ "@jgengine/core/physics/vehicleBody#WheelSpec", "@jgengine/core/physics/vehicleBody#WheelState", "@jgengine/core/physics/vehicleBody#createVehicleBody", - "@jgengine/core/procedural#DecayMeterSet", - "@jgengine/core/procedural#Moodle", - "@jgengine/core/procedural#MoodleStack", - "@jgengine/core/procedural#MultiRegionHealth", "@jgengine/core/puzzle/cellGrid#CellGrid", "@jgengine/core/puzzle/cellGrid#CellRun", "@jgengine/core/puzzle/fallingPiece#FallingPiece", @@ -1655,20 +1651,16 @@ "@jgengine/core/stats/statModifiers#StatModifierSet", "@jgengine/core/stats/statModifiers#Stats", "@jgengine/core/survival/decayMeter#DecayMeterConfig", - "@jgengine/core/survival/decayMeter#DecayMeterSet", "@jgengine/core/survival/decayMeter#DecayMeterState", "@jgengine/core/survival/decayMeter#MeterThreshold", "@jgengine/core/survival/moodle#MOODLE_SEVERITY_ORDER", - "@jgengine/core/survival/moodle#Moodle", "@jgengine/core/survival/moodle#MoodleSeverity", "@jgengine/core/survival/moodle#MoodleSource", - "@jgengine/core/survival/moodle#MoodleStack", "@jgengine/core/survival/moodle#TimedMoodleInput", "@jgengine/core/survival/regionHealth#AilmentConfig", "@jgengine/core/survival/regionHealth#AilmentInstance", "@jgengine/core/survival/regionHealth#DamageResult", "@jgengine/core/survival/regionHealth#HealthRegionConfig", - "@jgengine/core/survival/regionHealth#MultiRegionHealth", "@jgengine/core/survival/regionHealth#MultiRegionHealthConfig", "@jgengine/core/survival/regionHealth#RegionHealthState", "@jgengine/core/survival/regionHealth#TreatResult", diff --git a/scripts/barrels.test.ts b/scripts/barrels.test.ts index 4c2e29edd..0da828f71 100644 --- a/scripts/barrels.test.ts +++ b/scripts/barrels.test.ts @@ -36,7 +36,6 @@ const KEY_SYMBOL: Record = { gameplay: "defineGame", multiplayer: "ChatTransport", ui: "formatDuration", - procedural: "createDecayMeterSet", }; const pkg = JSON.parse(readFileSync(join(root, "packages/core/package.json"), "utf8")) as { diff --git a/scripts/gen-barrels.ts b/scripts/gen-barrels.ts index a4cf28917..59af590d7 100644 --- a/scripts/gen-barrels.ts +++ b/scripts/gen-barrels.ts @@ -31,7 +31,6 @@ export const CORE_BARRELS: readonly BarrelDomain[] = [ { skill: "jgengine-gameplay", barrel: "gameplay" }, { skill: "jgengine-multiplayer", barrel: "multiplayer" }, { skill: "jgengine-ui", barrel: "ui" }, - { skill: "jgengine-procedural", barrel: "procedural" }, ] as const; export interface Reexport { From 53422e449c3f7faf3b724cbe28c98fb13dfac791 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 21:53:13 +0000 Subject: [PATCH 03/10] Regen main-skill capabilities.md for barrel-surfaced survival primitives Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- .claude/skills/jgengine/capabilities.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/jgengine/capabilities.md b/.claude/skills/jgengine/capabilities.md index 806e7d0a0..af321bfcc 100644 --- a/.claude/skills/jgengine/capabilities.md +++ b/.claude/skills/jgengine/capabilities.md @@ -59,7 +59,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p ## decay-meter — survival meters that drain/refill over game time (hunger, water, oxygen, stamina) -- `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/procedural"` +- `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/gameplay"` ## default-look — one field that lights a scene like a shipped game (opt out with "flat") @@ -163,7 +163,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p ## limb-health — per-body-part/region health tracked separately -- `createMultiRegionHealth` (function) · `import { createMultiRegionHealth } from "@jgengine/core/procedural"` +- `createMultiRegionHealth` (function) · `import { createMultiRegionHealth } from "@jgengine/core/gameplay"` ## listing-book — player-driven marketplace listings with a house cut, expiry sweep, and seller collection box From 23f6a985f5e25c5504eff85e6c121de036a8ae12 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:05:08 +0000 Subject: [PATCH 04/10] Fix compile breakage merged into main today (#899/#900/#904/#942 interactions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream PRs merged red-on-red and never compiled together — invisible for the same reason this PR exists (PR CI runs no build; main CI red, unseen): - core: EditorDocument lacked the catalogs field and the ParamSchema import its own editor code referenced; three document constructors updated. - shell: AuthoredScene lost the placeObjects prop/render in the liveDocument refactor but kept its consts; defineGame loop hooks now satisfy the Required shape from #942. - editor: missing seedEditorCatalogs/EditorCatalogDefinition/CatalogsPanel imports, undefined catalogById map, readonly-array push, frozen editor loop missing onReset/onDispose. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- .claude/skills/jgengine-ui/api.md | 4 ++-- packages/core/src/editor/commands.ts | 1 + packages/core/src/editor/document.ts | 3 +++ packages/core/src/editor/types.ts | 3 +++ packages/editor/src/EditorApp.tsx | 2 ++ packages/editor/src/EditorChrome.tsx | 1 + packages/editor/src/mcp/loadGameCatalogs.ts | 4 ++-- packages/editor/src/session.ts | 3 +++ packages/shell/src/defineGame.tsx | 4 ++-- packages/shell/src/scene/AuthoredScene.tsx | 10 ++++++++++ 10 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.claude/skills/jgengine-ui/api.md b/.claude/skills/jgengine-ui/api.md index 254f6a0ed..0e9dc4b44 100644 --- a/.claude/skills/jgengine-ui/api.md +++ b/.claude/skills/jgengine-ui/api.md @@ -1211,7 +1211,7 @@ - `AuthoredObjectsProps` (interface): interface AuthoredObjectsProps — Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. - `AuthoredPaths` (function): function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): React.JSX.Element — Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes. - `AuthoredPathsProps` (interface): interface AuthoredPathsProps — Props for {@link AuthoredPaths}: the document, the ground field to drape over, and a kind filter. -- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. +- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. - `AuthoredSceneProps` (interface): interface AuthoredSceneProps — Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. ## @jgengine/shell/scene/AuthoredScene @@ -1220,7 +1220,7 @@ - `AuthoredObjectsProps` (interface): interface AuthoredObjectsProps — Props for {@link AuthoredObjects}: document, ground field, and optional lift / onExisting. - `AuthoredPaths` (function): function AuthoredPaths({ document, field, kinds }: AuthoredPathsProps): React.JSX.Element — Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes. - `AuthoredPathsProps` (interface): interface AuthoredPathsProps — Props for {@link AuthoredPaths}: the document, the ground field to drape over, and a kind filter. -- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. +- `AuthoredScene` (function): function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, }: AuthoredSceneProps): React.JSX.Element — Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor: drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke render code. When a live-sync bus is installed (editor host), document patches stream in and re-render automatically — document is authoritative; runtime overrides stay ephemeral unless written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`); place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to real catalog GLBs; unmapped items keep the stylized proxy. - `AuthoredSceneProps` (interface): interface AuthoredSceneProps — Props for {@link AuthoredScene}: the document to render and the ground field to drape/ground on. ## @jgengine/shell/scene/GeneratedAssetRenderer diff --git a/packages/core/src/editor/commands.ts b/packages/core/src/editor/commands.ts index be2d2e159..af1ebcdc5 100644 --- a/packages/core/src/editor/commands.ts +++ b/packages/core/src/editor/commands.ts @@ -528,6 +528,7 @@ function applyMutating(state: EditorSessionState, command: EditorCommand): Edito annotations: state.document.annotations, prefabs: state.document.prefabs, collections: state.document.collections, + catalogs: state.document.catalogs, ...(state.document.ui === undefined ? {} : { ui: state.document.ui }), }; return { ...state, document: nextDoc }; diff --git a/packages/core/src/editor/document.ts b/packages/core/src/editor/document.ts index 532430fdc..c6febb3b1 100644 --- a/packages/core/src/editor/document.ts +++ b/packages/core/src/editor/document.ts @@ -22,12 +22,14 @@ import type { export function editorDocumentExtras(doc: EditorDocument): { prefabs: EditorPrefab[]; collections: EditorCollection[]; + catalogs: EditorCatalogData[]; terrain?: EditorTerrain; ui?: EditorDocument["ui"]; } { return { prefabs: doc.prefabs, collections: doc.collections, + catalogs: doc.catalogs, ...(doc.terrain === undefined ? {} : { terrain: doc.terrain }), ...(doc.ui === undefined ? {} : { ui: doc.ui }), }; @@ -782,6 +784,7 @@ export function applyEditorDocumentOverlay( annotations: upsertById(base.annotations, overlay.annotations), prefabs: upsertById(base.prefabs, overlay.prefabs), collections: upsertById(base.collections, overlay.collections), + catalogs: upsertCatalogs(base.catalogs, overlay.catalogs), ...(terrain === undefined ? {} : { terrain }), ...(ui === undefined ? {} : { ui }), }; diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 9989f92fb..2f610472e 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -1,3 +1,4 @@ +import type { ParamSchema } from "../scene/sceneKinds"; import type { EditorUiDocument } from "../ui/hudDocument"; import type { TerraformSnapshot } from "../world/terraform"; @@ -159,6 +160,8 @@ export interface EditorDocument { prefabs: EditorPrefab[]; /** Named selection sets / production groups — restore, add-to, lock, color, visibility. */ collections: EditorCollection[]; + /** Persisted gameplay catalog values; schemas come from the game's `editorCatalogs` export. */ + catalogs: EditorCatalogData[]; /** * HUD layout owned by the scene document — panel id → anchor/offset/size/visibility. * Canvas mode (F2+C) and `canvas_move_panel` / `canvas_resize_panel` write here; HudPanel reads it. diff --git a/packages/editor/src/EditorApp.tsx b/packages/editor/src/EditorApp.tsx index 7a036c5e9..d7d0266a4 100644 --- a/packages/editor/src/EditorApp.tsx +++ b/packages/editor/src/EditorApp.tsx @@ -350,6 +350,8 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip } // Placement/walk views freeze combat/AI so the frame isn't burned on sim. }, onPlayerLeave: playable.loop.onPlayerLeave, + onReset: playable.loop.onReset, + onDispose: playable.loop.onDispose, }; if (mode === "play") { diff --git a/packages/editor/src/EditorChrome.tsx b/packages/editor/src/EditorChrome.tsx index 457016110..764de2f82 100644 --- a/packages/editor/src/EditorChrome.tsx +++ b/packages/editor/src/EditorChrome.tsx @@ -14,6 +14,7 @@ import { listSceneKinds } from "@jgengine/core/scene/sceneKinds"; import { AssetBrowser, type EditorAssetEntry } from "./AssetBrowser"; import { AgentPanel } from "./agent/AgentPanel"; +import { CatalogsPanel } from "./CatalogsPanel"; import { CollectionsPanel } from "./CollectionsPanel"; import { EditorContextMenu } from "./EditorContextMenu"; import { OutlinerPanel } from "./OutlinerPanel"; diff --git a/packages/editor/src/mcp/loadGameCatalogs.ts b/packages/editor/src/mcp/loadGameCatalogs.ts index d742b15f7..8f4b5fc3a 100644 --- a/packages/editor/src/mcp/loadGameCatalogs.ts +++ b/packages/editor/src/mcp/loadGameCatalogs.ts @@ -1,4 +1,4 @@ -import type { EditorCatalogDefinition, EditorCatalogsInput } from "@jgengine/core/editor/index"; +import type { EditorCatalogDefinition, EditorCatalogEntry, EditorCatalogsInput } from "@jgengine/core/editor/index"; import type { ParamSchema } from "@jgengine/core/scene/sceneKinds"; /** Result of {@link loadGameCatalogs}: validated definitions, or diagnostics when the export is malformed. */ @@ -55,7 +55,7 @@ export function decodeGameCatalogs(resolved: unknown): LoadGameCatalogsResult { return; } if (typeof item.id !== "string" || typeof item.label !== "string" || schema === null) return; - const entries: EditorCatalogDefinition["entries"] = []; + const entries: EditorCatalogEntry[] = []; item.entries.forEach((entry, entryIndex) => { const entryPath = `${path}.entries[${entryIndex}]`; if (!isPlainObject(entry) || typeof entry.id !== "string") { diff --git a/packages/editor/src/session.ts b/packages/editor/src/session.ts index 95781f121..b2a9c6bc4 100644 --- a/packages/editor/src/session.ts +++ b/packages/editor/src/session.ts @@ -17,10 +17,12 @@ import { planRuntimeInspectorSet, runtimeEntityMetaWriteBackCommand, runtimeEntityWriteBackCommand, + seedEditorCatalogs, summarizeEditorSession, summarizeRuntimeInspector, type DocumentLiveSync, type DocumentPatch, + type EditorCatalogDefinition, type EditorCommand, type EditorDocument, type EditorKindVisibility, @@ -301,6 +303,7 @@ export function createEditorHost(options: { dispose: () => void; } { const catalogDefinitions = options.catalogs ?? []; + const catalogById = new Map(catalogDefinitions.map((definition) => [definition.id, definition])); const document = seedEditorCatalogs(normalizeEditorLayers(options.layers), catalogDefinitions); const session = createEditorSession(document); const liveSync = createDocumentLiveSync(document); diff --git a/packages/shell/src/defineGame.tsx b/packages/shell/src/defineGame.tsx index d8d790e55..159c2ecd1 100644 --- a/packages/shell/src/defineGame.tsx +++ b/packages/shell/src/defineGame.tsx @@ -106,8 +106,8 @@ export function defineGame( onNewPlayer: withPhaseSync(composed?.onNewPlayer), onTick: withPhaseSync(composed?.onTick), onPlayerLeave: composed?.onPlayerLeave ?? noop, - onReset: composed?.onReset, - onDispose: composed?.onDispose, + onReset: composed?.onReset?.bind(composed) ?? noop, + onDispose: composed?.onDispose?.bind(composed) ?? noop, }, GameUI: GameUI ?? emptyUi, environment: diff --git a/packages/shell/src/scene/AuthoredScene.tsx b/packages/shell/src/scene/AuthoredScene.tsx index 1efa8a1ff..f113a3258 100644 --- a/packages/shell/src/scene/AuthoredScene.tsx +++ b/packages/shell/src/scene/AuthoredScene.tsx @@ -262,6 +262,12 @@ export interface AuthoredSceneProps { * the `document` prop (tests, one-shot previews). */ live?: boolean; + /** + * Place the document's catalog-id markers into the object + * store — WorldScene renders them via the game's `objectModels` seam. Omit when the game places + * props itself in onInit with `placeAuthoredObjects`. + */ + placeObjects?: boolean | { verticalOffset?: number }; } /** @@ -281,6 +287,7 @@ export function AuthoredScene({ scatterModels, assets, live = true, + placeObjects, }: AuthoredSceneProps) { const liveDocument = useLiveEditorDocument(document, live); const instances = useMemo(() => resolveScatter(liveDocument, field), [liveDocument, field]); @@ -299,6 +306,9 @@ export function AuthoredScene({ context={{ document: liveDocument, field, ...(assets === undefined ? {} : { assets }) }} /> + {shouldPlaceObjects ? ( + + ) : null} ); } From 70a044480093487ebc71f7e4ea82fee1b3da55c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:07:12 +0000 Subject: [PATCH 05/10] Fix compile breakage merged into main today (#899/#900/#904/#942 interactions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream PRs merged red-on-red and never compiled together — invisible for the same reason this PR exists (PR CI runs no build; main CI red, unseen): - core: EditorDocument lacked the catalogs field and the ParamSchema import its own editor code referenced; three document constructors updated. - shell: AuthoredScene lost the placeObjects prop/render in the liveDocument refactor but kept its consts; defineGame loop hooks now satisfy the Required shape from #942. - editor: missing seedEditorCatalogs/EditorCatalogDefinition/CatalogsPanel imports, undefined catalogById map, readonly-array push, frozen editor loop missing onReset/onDispose; new CLI/RPC plumbing exports marked @internal (no external adopters). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- .claude/skills/jgengine-editor/api.md | 11 ----------- packages/editor/src/mcp/cli.ts | 1 + packages/editor/src/mcp/rpcPayload.ts | 11 ++++++----- packages/editor/src/session.ts | 4 ++-- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.claude/skills/jgengine-editor/api.md b/.claude/skills/jgengine-editor/api.md index 3cf12c0d5..44bd27da3 100644 --- a/.claude/skills/jgengine-editor/api.md +++ b/.claude/skills/jgengine-editor/api.md @@ -194,11 +194,9 @@ - `blankWorld` (function): function blankWorld(seed = "standalone"): EnvironmentWorldFeature — The default flat-ground world the standalone editor opens on when the host supplies none. - `createBlankPlayable` (function): function createBlankPlayable(options: BlankPlayableOptions = {}): PlayableGame — Builds a minimal gameless `PlayableGame` — a flat world plus an asset catalog — for the editor to mount over. - `createDefaultAgentEndpoint` (function): function createDefaultAgentEndpoint(config: AgentEndpointConfig = resolveAgentEndpointConfig()): AgentEndpoint — Picks HTTP endpoint when `JGENGINE_EDITOR_AGENT_URL` (or config.url) is set, otherwise the offline local agent. -- `createEditorHost` (function): function createEditorHost(options: { gameId: string; layers: EditorLayersInput | undefined; /** Game-exported gameplay catalog definitions (schemas + defaults); seeds document.catalogs. */ catalogs?: readonly EditorCatalogDefinition[]; assets?: readonly EditorAssetInfo[]; onFocus?: (target: { x: num… — Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. - `createEditorUiStore` (function): function createEditorUiStore(): EditorUiStore — Creates the shared UI store the editor chrome and viewport both drive. - `createHttpAgentEndpoint` (function): function createHttpAgentEndpoint(config: { url: string; apiKey?: string; fetchImpl?: typeof fetch; }): AgentEndpoint — HTTP POST agent endpoint: `{ messages, context, tools }` → `{ message?, toolCalls? }`. Bearer auth from `apiKey` when provided (`JGENGINE_EDITOR_AGENT_KEY` / `ANTHROPIC_API_KEY`). - `downloadSaver` (function): function downloadSaver(filename = "editor.scene.json"): EditorSaveFn — A save fn that hands the scene JSON back to the browser as a downloaded file — the exit path when no dev server is listening. -- `getEditorHost` (function): function getEditorHost(): EditorHostApi | null — Retrieves the globally installed editor host, or null if none is mounted. - `installEditorHost` (function): function installEditorHost(api: EditorHostApi): () => void — Publishes an editor host globally so devtools and MCP agents can reach it; returns a cleanup fn. - `newPlacementId` (function): function newPlacementId(prefix: string): string — Generates a fresh scene-object id for a placement tool click. - `packAgentContext` (function): function packAgentContext(api: EditorHostApi): AgentEditorContext — Packs the live host's selection, mode, focus, and document counts for agent prompts. Injected into every embedded-panel turn so the agent shares the human's current view. @@ -326,7 +324,6 @@ ## @jgengine/editor/mcp/cli - `EditorCliOptions` (type): type EditorCliOptions = { gameId: string; port: number; rpcSource: RpcPayloadSource | null; serve: boolean; stdio: boolean; } — Parsed CLI flags for the headless editor control plane. -- `parseEditorCliArgs` (function): function parseEditorCliArgs(argv: string[]): EditorCliOptions — Parses argv into editor-mcp flags. `--rpc -` and `--rpc-file` both set a non-inline {@link RpcPayloadSource} so large documents never ride a shell argument. ## @jgengine/editor/mcp/loadGameCatalogs @@ -342,12 +339,6 @@ - `RpcPayloadResult` (type): type RpcPayloadResult = | { ok: true; value: unknown; raw: string; sourceLabel: string } | { ok: false; error: string } — Result of reading or JSON-decoding an RPC payload for the CLI. - `RpcPayloadSource` (type): type RpcPayloadSource = | { kind: "inline"; raw: string } | { kind: "file"; path: string } | { kind: "stdin" } — Where an RPC JSON body is read from for the headless editor CLI. -- `formatRpcParseError` (function): function formatRpcParseError(raw: string, error: unknown, source: RpcPayloadSource): string — Builds a clear diagnostic when JSON.parse fails on an RPC body — names the source, size, and (for inline args) points agents at `--rpc-file` / `--rpc -` instead of a bare SyntaxError. -- `loadRpcPayload` (function): function loadRpcPayload(source: RpcPayloadSource, readStdin?: () => Promise): Promise — Load + parse an RPC payload from the resolved CLI source. -- `looksTruncatedJson` (function): function looksTruncatedJson(raw: string): boolean — True when braces/brackets/quotes look cut off — common when a shell truncates a long --rpc arg. -- `parseRpcJson` (function): function parseRpcJson(raw: string, source: RpcPayloadSource): RpcPayloadResult — JSON.parse with a source-aware diagnostic (never throws). -- `readRpcText` (function): function readRpcText(source: RpcPayloadSource, readStdin: () => Promise = defaultReadStdin): Promise<{ ok: true; raw: string } | { ok: false; error: string }> — Reads the raw RPC text from an inline arg, file path, or stdin. -- `rpcSourceLabel` (function): function rpcSourceLabel(source: RpcPayloadSource): string — Human-readable label for error messages (and tests). ## @jgengine/editor/mcp/rpcRequest @@ -390,8 +381,6 @@ - `EditorRunMode` (type): type EditorRunMode = "edit" | "walk" | "play" — How the editor hosts the game: frozen placement view, roamable world, or the real game. - `EditorSession` (interface): interface EditorSession — Stateful, undoable handle for driving scene edits from UI or an MCP agent. - `EditorSessionState` (interface): interface EditorSessionState — The document plus current selection at a point in editor history. -- `createEditorHost` (function): function createEditorHost(options: { gameId: string; layers: EditorLayersInput | undefined; /** Game-exported gameplay catalog definitions (schemas + defaults); seeds document.catalogs. */ catalogs?: readonly EditorCatalogDefinition[]; assets?: readonly EditorAssetInfo[]; onFocus?: (target: { x: num… — Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. -- `getEditorHost` (function): function getEditorHost(): EditorHostApi | null — Retrieves the globally installed editor host, or null if none is mounted. - `installEditorHost` (function): function installEditorHost(api: EditorHostApi): () => void — Publishes an editor host globally so devtools and MCP agents can reach it; returns a cleanup fn. ## @jgengine/editor/uiStore diff --git a/packages/editor/src/mcp/cli.ts b/packages/editor/src/mcp/cli.ts index efe9aa957..02898a333 100644 --- a/packages/editor/src/mcp/cli.ts +++ b/packages/editor/src/mcp/cli.ts @@ -43,6 +43,7 @@ export type EditorCliOptions = { /** * Parses argv into editor-mcp flags. `--rpc -` and `--rpc-file` both set a non-inline * {@link RpcPayloadSource} so large documents never ride a shell argument. + * @internal */ export function parseEditorCliArgs(argv: string[]): EditorCliOptions { let gameId = "the-robots"; diff --git a/packages/editor/src/mcp/rpcPayload.ts b/packages/editor/src/mcp/rpcPayload.ts index 2236ac7b3..a8b5ac1ef 100644 --- a/packages/editor/src/mcp/rpcPayload.ts +++ b/packages/editor/src/mcp/rpcPayload.ts @@ -11,14 +11,14 @@ export type RpcPayloadResult = | { ok: true; value: unknown; raw: string; sourceLabel: string } | { ok: false; error: string }; -/** Human-readable label for error messages (and tests). */ +/** Human-readable label for error messages (and tests). @internal */ export function rpcSourceLabel(source: RpcPayloadSource): string { if (source.kind === "inline") return "inline --rpc"; if (source.kind === "file") return `--rpc-file ${source.path}`; return "stdin (--rpc -)"; } -/** True when braces/brackets/quotes look cut off — common when a shell truncates a long --rpc arg. */ +/** True when braces/brackets/quotes look cut off — common when a shell truncates a long --rpc arg. @internal */ export function looksTruncatedJson(raw: string): boolean { let depth = 0; let inString = false; @@ -49,6 +49,7 @@ export function looksTruncatedJson(raw: string): boolean { /** * Builds a clear diagnostic when JSON.parse fails on an RPC body — names the source, size, and * (for inline args) points agents at `--rpc-file` / `--rpc -` instead of a bare SyntaxError. + * @internal */ export function formatRpcParseError(raw: string, error: unknown, source: RpcPayloadSource): string { const detail = error instanceof Error ? error.message : String(error); @@ -67,7 +68,7 @@ export function formatRpcParseError(raw: string, error: unknown, source: RpcPayl return parts.join(". "); } -/** JSON.parse with a source-aware diagnostic (never throws). */ +/** JSON.parse with a source-aware diagnostic (never throws). @internal */ export function parseRpcJson(raw: string, source: RpcPayloadSource): RpcPayloadResult { if (raw.length === 0) { return { @@ -82,7 +83,7 @@ export function parseRpcJson(raw: string, source: RpcPayloadSource): RpcPayloadR } } -/** Reads the raw RPC text from an inline arg, file path, or stdin. */ +/** Reads the raw RPC text from an inline arg, file path, or stdin. @internal */ export async function readRpcText( source: RpcPayloadSource, readStdin: () => Promise = defaultReadStdin, @@ -106,7 +107,7 @@ export async function readRpcText( } } -/** Load + parse an RPC payload from the resolved CLI source. */ +/** Load + parse an RPC payload from the resolved CLI source. @internal */ export async function loadRpcPayload( source: RpcPayloadSource, readStdin?: () => Promise, diff --git a/packages/editor/src/session.ts b/packages/editor/src/session.ts index b2a9c6bc4..4df0f06c0 100644 --- a/packages/editor/src/session.ts +++ b/packages/editor/src/session.ts @@ -283,13 +283,13 @@ export function installEditorHost(api: EditorHostApi): () => void { }; } -/** Retrieves the globally installed editor host, or null if none is mounted. */ +/** Retrieves the globally installed editor host, or null if none is mounted. @internal */ export function getEditorHost(): EditorHostApi | null { const root = globalThis as typeof globalThis & { [GLOBAL_KEY]?: EditorHostApi }; return root[GLOBAL_KEY] ?? null; } -/** Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. */ +/** Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. @internal */ export function createEditorHost(options: { gameId: string; layers: EditorLayersInput | undefined; From f0958b00f71492b8539c9f3cf9de7ebc4c49c580 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:08:32 +0000 Subject: [PATCH 06/10] Trim jgengine-gameplay SKILL.md back under the size ratchet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The systems paragraph from #942 grew it 2 lines past baseline — another red-main-invisible failure; merged into the barrel-import paragraph. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- .claude/skills/jgengine-gameplay/SKILL.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.claude/skills/jgengine-gameplay/SKILL.md b/.claude/skills/jgengine-gameplay/SKILL.md index 5187dc070..99a2b784b 100644 --- a/.claude/skills/jgengine-gameplay/SKILL.md +++ b/.claude/skills/jgengine-gameplay/SKILL.md @@ -5,9 +5,7 @@ description: Gameplay systems: items, quests, economy, crafting, turns, objectiv # jgengine-gameplay -**Import from the curated barrel** `@jgengine/core/gameplay` (stable, re-exports this domain's public API) — deep paths `@jgengine/core//` still work for anything not re-exported. - -**Composable systems** — `defineSystem` / `composeGameLoop` / `compileSystemSchedule` / `DEFAULT_FIXED_STAGES` / `DEFAULT_FRAME_STAGES` / `SystemDefinition` / `SystemTick` / `SystemEventHandlers` / `CompiledSystemSchedule`: list capabilities in `defineGame({ systems })` instead of a manual `onTick` fan-out. Full contract: [reference-systems.md](reference-systems.md). +**Import from the curated barrel** `@jgengine/core/gameplay` (stable, re-exports this domain's public API) — deep paths `@jgengine/core//` still work for anything not re-exported. **Composable systems** — `defineSystem` / `composeGameLoop` / `compileSystemSchedule` / `DEFAULT_FIXED_STAGES` / `DEFAULT_FRAME_STAGES` / `SystemDefinition` / `SystemTick` / `SystemEventHandlers` / `CompiledSystemSchedule`: list capabilities in `defineGame({ systems })` instead of a manual `onTick` fan-out. Full contract: [reference-systems.md](reference-systems.md). ## Content catalogs From d9745955d917bd4b26c3f7842aa0ee2efbabefc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:10:42 +0000 Subject: [PATCH 07/10] Move tower-guard editorCatalogs under src/game/ per game-shape gate Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- Games/tower-guard/src/editorLayers.ts | 2 +- Games/tower-guard/src/{ => game}/editorCatalogs.ts | 0 Games/tower-guard/src/index.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename Games/tower-guard/src/{ => game}/editorCatalogs.ts (100%) diff --git a/Games/tower-guard/src/editorLayers.ts b/Games/tower-guard/src/editorLayers.ts index 70f90e73a..dbda70eda 100644 --- a/Games/tower-guard/src/editorLayers.ts +++ b/Games/tower-guard/src/editorLayers.ts @@ -8,7 +8,7 @@ import type { AvoidZone } from "@jgengine/core/world/geometry"; import { clearanceZonesFrom } from "@jgengine/core/world/scatterRegion"; import { createEditableTerrain, migrateTerrainSnapshot, type TerraformSnapshot } from "@jgengine/core/world/terraform"; -import { editorCatalogs } from "./editorCatalogs"; +import { editorCatalogs } from "./game/editorCatalogs"; import sceneJson from "./editor.scene.json"; type Vec2 = readonly [number, number]; diff --git a/Games/tower-guard/src/editorCatalogs.ts b/Games/tower-guard/src/game/editorCatalogs.ts similarity index 100% rename from Games/tower-guard/src/editorCatalogs.ts rename to Games/tower-guard/src/game/editorCatalogs.ts diff --git a/Games/tower-guard/src/index.tsx b/Games/tower-guard/src/index.tsx index cef99ce83..0fbb9d2f8 100644 --- a/Games/tower-guard/src/index.tsx +++ b/Games/tower-guard/src/index.tsx @@ -1,3 +1,3 @@ export { game } from "./game.config"; -export { editorCatalogs } from "./editorCatalogs"; +export { editorCatalogs } from "./game/editorCatalogs"; export { editorLayers } from "./editorLayers"; From ad115094d6ac1931aba209bd3d85340668539878 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:13:17 +0000 Subject: [PATCH 08/10] Fix consumers of the #942 Required loop contract and the game-shape move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/dev demos and the-robots/tower-guard were never updated for today's upstream contract changes (required onReset/onDispose, EditorDocument catalogs) — same red-main blindness; all 29 workspaces typecheck again. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- Games/the-robots/src/editorLayers.ts | 1 + Games/tower-guard/src/game/editorCatalogs.ts | 2 +- apps/dev/src/demo/builderDemo.tsx | 2 +- apps/dev/src/demo/demoGame.tsx | 2 +- apps/dev/src/demo/mapDemo.tsx | 2 +- apps/dev/src/demo/pointerDemo.tsx | 2 +- apps/dev/src/demo/sensorShowcase.tsx | 2 +- apps/dev/src/demo/survivalDemo.tsx | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Games/the-robots/src/editorLayers.ts b/Games/the-robots/src/editorLayers.ts index b05b38881..021ed0954 100644 --- a/Games/the-robots/src/editorLayers.ts +++ b/Games/the-robots/src/editorLayers.ts @@ -294,6 +294,7 @@ export function buildTheRobotsEditorLayers(): EditorDocument { annotations: [], prefabs: [], collections: [], + catalogs: [], }; } diff --git a/Games/tower-guard/src/game/editorCatalogs.ts b/Games/tower-guard/src/game/editorCatalogs.ts index 4b9172328..3171a4d60 100644 --- a/Games/tower-guard/src/game/editorCatalogs.ts +++ b/Games/tower-guard/src/game/editorCatalogs.ts @@ -1,7 +1,7 @@ import type { EditorCatalogDefinition } from "@jgengine/core/editor/index"; import type { ParamSchema } from "@jgengine/core/scene/sceneKinds"; -import { TOWER_CATALOG, TOWER_IDS } from "./game/entities/towers/catalog"; +import { TOWER_CATALOG, TOWER_IDS } from "./entities/towers/catalog"; /** Tunable tower combat/economy fields — drives the editor Data panel via SchemaInspector. */ export const TOWER_SCHEMA: ParamSchema = { diff --git a/apps/dev/src/demo/builderDemo.tsx b/apps/dev/src/demo/builderDemo.tsx index df70260c6..602cbf70c 100644 --- a/apps/dev/src/demo/builderDemo.tsx +++ b/apps/dev/src/demo/builderDemo.tsx @@ -297,7 +297,7 @@ function BuilderUI() { export const builderDemoGame: PlayableGame = { game, content: {}, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: BuilderUI, environment: BuilderScene, camera: { diff --git a/apps/dev/src/demo/demoGame.tsx b/apps/dev/src/demo/demoGame.tsx index 39981b201..c153779d5 100644 --- a/apps/dev/src/demo/demoGame.tsx +++ b/apps/dev/src/demo/demoGame.tsx @@ -310,6 +310,6 @@ export const demoGame: PlayableGame = { itemById: (itemId) => itemCatalog[itemId] ?? null, entityById: (catalogId) => entityCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: DemoGameUI, }; diff --git a/apps/dev/src/demo/mapDemo.tsx b/apps/dev/src/demo/mapDemo.tsx index 1ddac0402..ee5eda3df 100644 --- a/apps/dev/src/demo/mapDemo.tsx +++ b/apps/dev/src/demo/mapDemo.tsx @@ -285,7 +285,7 @@ export const mapDemoGame: PlayableGame = { entityById: (catalogId) => entityCatalog[catalogId] ?? null, objectById: (catalogId) => objectCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: MapUI, environment: () => , WorldOverlay: () => , diff --git a/apps/dev/src/demo/pointerDemo.tsx b/apps/dev/src/demo/pointerDemo.tsx index 6a48f16d0..17e643f61 100644 --- a/apps/dev/src/demo/pointerDemo.tsx +++ b/apps/dev/src/demo/pointerDemo.tsx @@ -191,7 +191,7 @@ export const pointerDemoGame: PlayableGame = { entityById: (catalogId) => entityCatalog[catalogId] ?? null, objectById: (catalogId) => objectCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: CommanderUI, pointer: { select: true, diff --git a/apps/dev/src/demo/sensorShowcase.tsx b/apps/dev/src/demo/sensorShowcase.tsx index 2a9628a93..b3ca09d99 100644 --- a/apps/dev/src/demo/sensorShowcase.tsx +++ b/apps/dev/src/demo/sensorShowcase.tsx @@ -181,7 +181,7 @@ export const sensorShowcaseGame: PlayableGame = { content: { entityById: (catalogId) => entityCatalog[catalogId] ?? null, }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: SensorHud, WorldOverlay: SensorWorldOverlay, camera: { diff --git a/apps/dev/src/demo/survivalDemo.tsx b/apps/dev/src/demo/survivalDemo.tsx index e9feb2e44..65a1b4f18 100644 --- a/apps/dev/src/demo/survivalDemo.tsx +++ b/apps/dev/src/demo/survivalDemo.tsx @@ -457,7 +457,7 @@ export const survivalDemoGame: PlayableGame = { entityById: (catalogId) => entityCatalog[catalogId] ?? null, itemById: (itemId) => (itemId in itemCatalog ? itemCatalog[itemId as keyof typeof itemCatalog] : null), }, - loop: { onInit, onNewPlayer, onTick }, + loop: { onInit, onNewPlayer, onTick, onReset: () => {}, onDispose: () => {} }, GameUI: SurvivalGameUI, environment: SurvivalWorld, camera: { minDistance: 8, maxDistance: 40, initialDistance: 26, targetHeight: 1.4 }, From c76235ba91717d994b15115ee8236a3ad51ca665 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:17:21 +0000 Subject: [PATCH 09/10] Regen stale export manifest; drive claudecraft headless tests via composed loop Both pre-existing on merged main: #942 moved claudecraft combat into systems but its headless harness still ticked the raw loop export, and the export manifest was never regenerated for today's new modules. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- Games/claudecraft/src/game/gameplay.test.ts | 3 ++- scripts/export-manifest.json | 23 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Games/claudecraft/src/game/gameplay.test.ts b/Games/claudecraft/src/game/gameplay.test.ts index 5db92a6f0..956c3adc0 100644 --- a/Games/claudecraft/src/game/gameplay.test.ts +++ b/Games/claudecraft/src/game/gameplay.test.ts @@ -3,7 +3,8 @@ import { createGameContext, type GameContext } from "@jgengine/core/runtime/game import { evaluateSkillCheck } from "@jgengine/core/interaction/skillCheck"; import { game } from "../game.config"; -import { loop } from "../loop"; + +const loop = game.loop; import type { AuctionView } from "./auction/systems"; import { classById } from "./classes/catalog"; import { applyMobCc, isMobInstance, mobCount, mobRuntimeOf } from "./ai/mobs"; diff --git a/scripts/export-manifest.json b/scripts/export-manifest.json index 295e1ee5d..88a4c5095 100644 --- a/scripts/export-manifest.json +++ b/scripts/export-manifest.json @@ -67,6 +67,8 @@ "./editor/commands", "./editor/document", "./editor/index", + "./editor/liveSync", + "./editor/runtimeInspector", "./editor/types", "./faction/factions", "./faction/reputation", @@ -79,6 +81,7 @@ "./game/controlGate", "./game/cosmetics", "./game/defineGame", + "./game/defineSystem", "./game/dialogue", "./game/events", "./game/feed", @@ -102,6 +105,8 @@ "./game/snapshotHistory", "./game/social", "./game/spawnPoints", + "./game/systemRuntime", + "./game/systemSchedule", "./game/talents", "./game/toasts", "./game/trade", @@ -222,6 +227,7 @@ "./scene/assetCatalog", "./scene/assetGenerator", "./scene/assetPreload", + "./scene/authoredTriggers", "./scene/autoTarget", "./scene/behaviorRuntime", "./scene/behaviors", @@ -290,6 +296,7 @@ "./turn/turnLoop", "./ui", "./ui/gameLayout", + "./ui/hudDocument", "./ui/hudLayout", "./ui/hudScale", "./ui/orientation", @@ -307,6 +314,7 @@ "./visibility/spatialIndex", "./visibility/visibilitySystem", "./world", + "./world/authoredObjects", "./world/buildPermissions", "./world/buildingGenerator", "./world/buildingIndex", @@ -330,6 +338,7 @@ "./world/massing", "./world/minimap", "./world/pathInstances", + "./world/placeAsset", "./world/placedStructureStore", "./world/placement", "./world/placementController", @@ -551,6 +560,7 @@ "./structures", "./structures/GeneratedBuilding", "./structures/PlacementGhost", + "./structures/TransformGizmo", "./structures/index", "./terrain", "./terrain/CarvedTerrain", @@ -610,26 +620,36 @@ "@jgengine/editor": [ ".", "./AssetBrowser", + "./CatalogsPanel", "./CollectionsPanel", "./DebugDraw", "./EditorApp", "./EditorCameraDriver", "./EditorChrome", + "./EditorContextMenu", "./InspectorPanel", "./MaterialDropZone", "./OutlinerPanel", "./PerfProbe", "./PrefabsPanel", + "./RuntimePlayBridge", "./ScatterPreview", "./SchemaInspector", "./SelectionGizmo", "./StandaloneEditor", "./TerrainPanel", "./TerrainSculpt", + "./TriggerInspector", + "./agent/AgentPanel", + "./agent/context", + "./agent/endpoint", + "./agent/toolBridge", + "./agent/turn", "./chromeFields", "./chromeStyles", "./index", "./mcp/bridgeServer", + "./mcp/loadGameCatalogs", "./mcp/rpcRequest", "./mcp/tools", "./outlinerModel", @@ -637,7 +657,8 @@ "./session", "./uiStore", "./useF2Chord", - "./useStoreSelector" + "./useStoreSelector", + "./viewportContextMenu" ], "@jgengine/assets": [ ".", From 47f3109760132df3fa96d64ae831936babfc5058 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:21:40 +0000 Subject: [PATCH 10/10] Wire rpcPayload into the editor CLI; restore editorCatalogs convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More half-landed upstream: cli.ts referenced an undefined options var and never used parseEditorCliArgs/loadRpcPayload, so --rpc-file and stdin paths (and their tests) were dead. The catalog loader convention is src/editorCatalogs.ts — check-game-shape now allows that skeleton file instead of forcing it under src/game/ where the loader can't find it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GjYn1uP6nf8maGvAvHFNjB --- .../src/{game => }/editorCatalogs.ts | 2 +- Games/tower-guard/src/editorLayers.ts | 2 +- Games/tower-guard/src/index.tsx | 2 +- packages/editor/src/mcp/cli.ts | 69 +++++++------------ scripts/check-game-shape.ts | 2 + 5 files changed, 30 insertions(+), 47 deletions(-) rename Games/tower-guard/src/{game => }/editorCatalogs.ts (95%) diff --git a/Games/tower-guard/src/game/editorCatalogs.ts b/Games/tower-guard/src/editorCatalogs.ts similarity index 95% rename from Games/tower-guard/src/game/editorCatalogs.ts rename to Games/tower-guard/src/editorCatalogs.ts index 3171a4d60..4b9172328 100644 --- a/Games/tower-guard/src/game/editorCatalogs.ts +++ b/Games/tower-guard/src/editorCatalogs.ts @@ -1,7 +1,7 @@ import type { EditorCatalogDefinition } from "@jgengine/core/editor/index"; import type { ParamSchema } from "@jgengine/core/scene/sceneKinds"; -import { TOWER_CATALOG, TOWER_IDS } from "./entities/towers/catalog"; +import { TOWER_CATALOG, TOWER_IDS } from "./game/entities/towers/catalog"; /** Tunable tower combat/economy fields — drives the editor Data panel via SchemaInspector. */ export const TOWER_SCHEMA: ParamSchema = { diff --git a/Games/tower-guard/src/editorLayers.ts b/Games/tower-guard/src/editorLayers.ts index dbda70eda..70f90e73a 100644 --- a/Games/tower-guard/src/editorLayers.ts +++ b/Games/tower-guard/src/editorLayers.ts @@ -8,7 +8,7 @@ import type { AvoidZone } from "@jgengine/core/world/geometry"; import { clearanceZonesFrom } from "@jgengine/core/world/scatterRegion"; import { createEditableTerrain, migrateTerrainSnapshot, type TerraformSnapshot } from "@jgengine/core/world/terraform"; -import { editorCatalogs } from "./game/editorCatalogs"; +import { editorCatalogs } from "./editorCatalogs"; import sceneJson from "./editor.scene.json"; type Vec2 = readonly [number, number]; diff --git a/Games/tower-guard/src/index.tsx b/Games/tower-guard/src/index.tsx index 0fbb9d2f8..cef99ce83 100644 --- a/Games/tower-guard/src/index.tsx +++ b/Games/tower-guard/src/index.tsx @@ -1,3 +1,3 @@ export { game } from "./game.config"; -export { editorCatalogs } from "./game/editorCatalogs"; +export { editorCatalogs } from "./editorCatalogs"; export { editorLayers } from "./editorLayers"; diff --git a/packages/editor/src/mcp/cli.ts b/packages/editor/src/mcp/cli.ts index 02898a333..02c95e61e 100644 --- a/packages/editor/src/mcp/cli.ts +++ b/packages/editor/src/mcp/cli.ts @@ -91,25 +91,7 @@ async function main(argv: string[]): Promise { return 0; } - let gameId = "the-robots"; - let port = 17373; - const rpcRaws: string[] = []; - let serve = true; - let stdio = false; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]!; - if (arg === "--game") gameId = argv[++i] ?? gameId; - else if (arg === "--port") port = Number(argv[++i] ?? port); - else if (arg === "--rpc") { - const raw = argv[++i]; - if (raw !== undefined) rpcRaws.push(raw); - serve = false; - } else if (arg === "--stdio") { - stdio = true; - serve = false; - } else if (arg === "--serve") serve = true; - } + const { gameId, port, rpcSource, serve, stdio } = parseEditorCliArgs(argv); if (stdio) { await runEditorMcpStdio({ gameId }); @@ -119,7 +101,7 @@ async function main(argv: string[]): Promise { const [layers, catalogs] = await Promise.all([loadGameLayers(gameId), loadGameCatalogs(gameId)]); if (!layers.ok) { console.error( - `invalid editorLayers for ${options.gameId}: ${layers.errors.map((e) => `${e.path} ${e.message}`).join("; ")}`, + `invalid editorLayers for ${gameId}: ${layers.errors.map((e) => `${e.path} ${e.message}`).join("; ")}`, ); return 1; } @@ -128,40 +110,39 @@ async function main(argv: string[]): Promise { return 1; } const { api, dispose } = createEditorHost({ - gameId: options.gameId, + gameId: gameId, layers: layers.document, catalogs: catalogs.catalogs, }); - if (rpcRaws.length > 0) { - let allOk = true; - for (const rpcRaw of rpcRaws) { - const decoded = decodeEditorBridgeRequest(JSON.parse(rpcRaw)); - if (!decoded.ok) { - console.log( - JSON.stringify( - { ok: false, error: decoded.errors.map((e) => `${e.path} ${e.message}`).join("; ") }, - null, - 2, - ), - ); - allOk = false; - break; - } - const response = api.handle(decoded.request); - console.log(JSON.stringify(response, null, 2)); - if (!response.ok) { - allOk = false; - break; - } + if (rpcSource !== null) { + const payload = await loadRpcPayload(rpcSource); + if (!payload.ok) { + console.error(payload.error); + dispose(); + return 1; + } + const decoded = decodeEditorBridgeRequest(payload.value); + if (!decoded.ok) { + console.log( + JSON.stringify( + { ok: false, error: decoded.errors.map((e) => `${e.path} ${e.message}`).join("; ") }, + null, + 2, + ), + ); + dispose(); + return 1; } + const response = api.handle(decoded.request); + console.log(JSON.stringify(response, null, 2)); dispose(); - return allOk ? 0 : 1; + return response.ok ? 0 : 1; } if (options.serve) { const server = startEditorBridgeServerNode({ host: api, port: options.port }); - console.log(`editor bridge for ${options.gameId} at ${server.url}`); + console.log(`editor bridge for ${gameId} at ${server.url}`); console.log(`POST ${server.url}/rpc body: {"method":"scene_summary"}`); console.log("tools:", EDITOR_MCP_TOOLS.map((tool) => tool.name).join(", ")); await new Promise(() => undefined); diff --git a/scripts/check-game-shape.ts b/scripts/check-game-shape.ts index deef525bc..aebbd2e65 100644 --- a/scripts/check-game-shape.ts +++ b/scripts/check-game-shape.ts @@ -11,6 +11,8 @@ const SKELETON_FILES = new Set([ "index.css", "style.css", "editorLayers.ts", + "editorCatalogs.ts", + "editorCatalogs.test.ts", "editorLayers.test.ts", "editor.scene.json", ]);