# AGENTS.md **nEngine** — top-down 2D farming game engine (not a game). TypeScript + PixiJS. Built around three non-negotiable architectural pillars: **MCP-first interfaces**, **engine-level Mod support**, **enforced data-driven content**. ## What This Project Is An engine whose core value is *controllability by external tools/agents*, *extensibility*, and *content-as-data* — not raw rendering performance. The engine provides logic systems; creators provide only data (YAML content files). No engine logic hardcodes content. ## Non-Negotiable Pillars 1. **Every subsystem exposes MCP** — a central MCP gateway with per-subsystem namespaces (`item.*`, `crop.*`, `map.*`, `save.*`). "MCP for every feature" is an architectural constraint (enforced by the registration layer), not a process count. 2. **Engine-level Mod support** — Mods register to typed hook points (`onDayStart(ctx)`, `onCropHarvest(ctx)`); they touch only context objects, never engine internals. 3. **Data/logic separation, enforced** — game creation happens only through data. Content files (YAML) contain no code; engine logic contains no hardcoded content. ## Design Principles (derived from docs/decisions.md — READ IT) - **MCP-first**: when designing any interface, design the MCP surface first (tools/resources/prompts), then derive the engine API from it. Engine internals exist to serve the MCP surface. - Engine provides **mechanisms**, never **strategies** (save *format* is a mechanism; save *policy* is creator content). - Mechanisms over policies; declarative data language over code; Hooks over deep imports. - Mods serve gameplay only. MCP serves engine-native capabilities only. **No Mod↔MCP bridge.** - Headless Node core is the authority; browsers are render viewports. - **Desktop-first (D40)**: distribution target is desktop (Electron, unchanged Web stack). Browsers/WS/static-site remain as dev/debug aids — kept, no longer extended. ## Document Map (keep in sync) | Path | Purpose | |---|---|---| | `docs/decisions.md` | All architecture decisions, with rationale and user corrections. **Source of truth for WHY.** | | `docs/architecture.md` | Top-level architecture, package boundaries, directory layout, core API overview. | | `docs/mcp.md` | **MCP-first interface spec**: per-subsystem namespace tools/resources/prompts. The interface contract. | | `docs/api.md` | Engine API reference, organized by MCP service. **The living API doc.** | | `docs/boundaries.md` | Engine/game boundary: what is engine-built vs creator content, with the tri-criterion. | | `docs/editor.md` | **Editor architecture** (D40): D40 decision summary, `apps/editor` structure, main/preload/renderer responsibilities, IPC API table, `project.yaml` schema, template contents, round roadmap E1–E7. | ## Work Conventions (MANDATORY) 1. **MCP-first design**: when adding or changing a subsystem, write its MCP namespace spec (tools/resources/prompts) *before* implementing engine internals. The engine API must be derivable from the MCP surface. 2. **API docs are living documents**: ANY change to an interface (MCP tool signature, engine API, data schema) MUST be reflected in `docs/mcp.md` / `docs/api.md` **in the same change**. A change without its doc update is incomplete. 3. Every new subsystem follows the plugin template: `add schema → add system → register MCP namespace → declare hook points`. 4. Content data is authored in **YAML**, validated after normalization to internal JSON (zod). The MCP surface only ever sees JSON. 5. Data hot-reload on content change; code Mod changes restart the headless core. 6. Type safety: never suppress errors (`as any`, `@ts-ignore`, `@ts-expect-error`). ## Current Status **Walking skeleton + Tier 1 complete; Tier 2 complete (behavior language v2 + dialogue + weather + placement + NPC grid movement + tick loop + animal age + multi-viewport sync); player input built (D25); presentation pipeline complete (D26 data/protocol + D27 render half: sprite drawing + Web Audio playback + keyboard input); deterministic seeded RNG built (D28 — the ONE LÖVE gap adopted); Tier 3 content round complete (D29 — stamina/skills field-level + `onPlayerIntent` hook + multi-quest chain + fishing minigame Mod); **game packaging built (D30 — content bundle + self-contained static site via `apps/pack` `nengine-pack`, bundle-mode `createEngine({bundle})` + `ContentLoader.loadBundle` + `apps/dev` `?bundle=` mode; no Mods in packaged output at D30, closed by D36's `--mods`; no Tauri yet, no `pack.*` MCP namespace — all documented)**; **asset pipeline built (D31 — binary assets as data-driven content: `content/assets/` `.png`/`.ogg` files → asset table (`registry.assets`/`engine.assets`, base64; NOT a content kind, no MCP namespace), `bundle.assets` + WS-snapshot `assets` field (BUNDLE_VERSION stays 1), sprite `image` layer (`src` = asset id), map `tiles` tile-id→asset-id mapping, `sound.src` audio playback, render texture/buffer caches keyed by asset id with async load + redraw-on-ready; sample-game adopts 13 Kenney tiles + 8 interface sounds)**; **desktop packaging built (D32 — Electron over Tauri: Tauri needs `webkit2gtk-4.1` system deps (no sudo here), Electron is npm-only; `nengine-pack --desktop [--name ]` writes `out/desktop/` (`main.cjs` + `package.json` + `README-desktop.md`): main.cjs serves the out dir over a local `node:http` server (127.0.0.1, ephemeral port, GET-only, MIME map, path-traversal guard, exact files only, `SITE_ROOT = resolve(__dirname, "..")` → any cwd) and loads it in a 960×640 BrowserWindow (title = product name via `page-title-updated` prevention, `contextIsolation: true`/`nodeIntegration: false`); `npm i && npm start` in `desktop/` runs it; no installer/electron-builder, default icon, sidecar headless core (Mods + multiplayer) reserved v2 — all documented; E2E-verified in real Electron via playwright `_electron`; `pnpm pack:example:desktop` packages the sample game)**; **core mechanism completions built (D33 — A* grid pathfinding: pure `findPath(map, start, goal, isBlocked)` (4-neighbor, Manhattan, array-based open set — maps are ~16×16) + `MovementSystem.findPath(npcId, to)` (blocks = buildings + other grid entities, never moves the npc) + MCP debug `npc.path` (101st tool); data-driven map walkability: optional `MapDef.walkable?: Record` with a single `isWalkableTile` source of truth (declared value wins, else the `UNWALKABLE_TILES` fallback — existing maps keep water blocked, zero content change) consumed by movement/intent/placement(`buildable:"map"`)/pathfinding; behavior-language `add` action (`{action:"add", path, value}` — incremental, missing field = 0, `set-state` stays absolute, closing the D29 gap); new single-file content kind `interact` (`content/interact.yaml` `{id, order:[harvest,water,sow,till]}` — the player interact priority ORDER is content policy, applicability stays engine mechanism, no MCP tools); `save.*` covers TimeSystem (optional additive `time: {day, season, tickInDay}` in `WorldSnapshot`, version stays 1, `TimeSystem.set(day, season, tickInDay=0)`, restore re-sets the clock)**; **render-layer completions built (D34 — 5 rendering mechanisms, all render-layer + content-kind only, no new engine systems/hooks: (1) `particle` content kind — event-triggered bursts via the existing behavior-language `emit-event` action (`event:"particle", payload:{particleId, x?, y?, entityId?}`; x/y tile coords, absent → the named entity or `player`), pure `particles.ts` (spawn ranges, px/s physics, gravity, expiry, fade approximated by size shrink over lifetime — documented); (2) `shader` content kind — parameterized BUILT-IN filters (blur/tint/grayscale/sepia/hue/brightness over PixiJS BlurFilter/ColorMatrixFilter), **NO GLSL in content**; optional `SpriteDef.filters` + whole-map filter via weather visuals; (3) sprite-sheet support — optional `frameX/frameY/frameW/frameH` source-pixel crop rects on the sprite `image` layer → viewport sub-textures (cached by asset+rect); (4) weather visual presentation — pure `weather.ts` built-in mapping (rain/storm → rain particles + rain-tint filter, wind → wind particles, storm → +overlay color) applied to the map container with periodic particle bursts; (5) entity text labels — plain `label` entity field (content-able; the mayor spawn carries `label:"Mayor"`), pure `labels.ts` position math + Pixi Text in the viewport. MCP 18→20 namespaces (`particle.*`/`shader.*`, 5 lifecycle tools each + catalog + `{id}` resources + create-prompt).** **22 MCP namespaces live** (`server`/`item`/`crop`/`map`/`time`/`shop`/`save`/`behavior`/`dialogue`/`weather`/`mod`/`placement`/`npc`/`animal`/`player`/`sprite`/`sound`/`rng`/`particle`/`shader`/`animation`/`scene` — 124 tools / 36 resources / 17 prompts), 18 content kinds (…`particle`/`shader` are the 15th+16th, `animation` is the 17th, `scene` is the 18th), 12 auto-boot systems (Time/Inventory/Planting/Growth/Economy/Save/Trigger/Weather/Dialogue/Placement/Movement/Intent — TriggerSystem is the v2 behavior engine driving both triggers and state machines; WeatherSystem rolls a weighted pick on day-start; DialogueSystem drives authored node/choice documents; PlacementSystem does grid footprint/overlap/buildability from map data; MovementSystem does grid `moveTo` + blocking + the D33 `findPath` A* query; GrowthSystem advances both crops and animals, driven by the new `engine.tick(dt)` tick loop with `time-tick` intra-day events and optional auto-tick; `onSchedule` supports `at: {tickInDay}`; IntentSystem is the single player-input mechanism — `handle(action, args)` for `move`/`interact` (the D33 interact ORDER is content-ized via the `interact` kind; applicability stays engine mechanism) plus D35's `buy`/`sell`/`talk`/`choose`/`select-seed` (economy/dialogue/seed-selection actions; sow prefers `player.selectedSeed` when it names an owned seed, else the first inventory seed), emitting `player-intent` per call — behind MCP `player.intent`, the WS `{"type":"intent"}` message, and `connectLocal.intent`), 16 hook points wired (added `onStateChange`, `onDialogueStart`, `onDialogueEnd`, `onWeatherChange`, `onPlacementChange`, `onAnimalGrow`, `onAnimalProduce`, `onPlayerIntent`). **Seeded RNG (D28)**: `packages/core/src/random.ts` `RandomSource`/`seedRandom`/`randomSeed` (mulberry32-style, zero deps, mirroring LÖVE's `love.math.RandomGenerator`); `createEngine({seed?})` exposes `engine.rng` injected into systems via `SystemInitCtx.rng` — WeatherSystem day-start rolls and PlantingSystem harvest drop chances draw from it (same seed → identical world randomness); MCP `rng.*` debug surface (`rng.get`/`rng.set`/`rng://state`); `save.*` snapshots carry `rngState {seed, calls}` and restore re-anchors the seed (`calls` reset, no draw replay). **Multi-viewport sync built (D24)**: `apps/mcp-server --ws-port `/`NENGINE_WS_PORT` broadcasts world state over WebSocket (snapshot on connect + `{"type":"sync"}`, event frames per bus emission — NOT an MCP namespace); `packages/render/src/sync/ws.ts` `connectWs` (browser-native, same handle shape as `connectLocal`); `viewport.render(snapshot?)` draws map + basic entity shapes (pure `shapes.ts` geometry — plots/buildings/dots, no art); `apps/dev` parameterized via `?contentDir=`/`?modsDir=`/`?sync=ws&url=ws://...`. **Presentation data/protocol (D26)**: two new content kinds `sprite` (`content/sprites/`, programmatic draw instructions — rect/circle layers + `#rrggbb` fills, zod strict, rect/circle/image (D31) layers) and `sound` (`content/sounds/`, synth specs — wave/freq/duration/gain/slideTo, zod strict, `src` reserved, no audio files v1), MCP `sprite.*`/`sound.*` (16th+17th namespaces — lifecycle tools mirroring `item.*`), optional `spriteRef` appearance hints on crop/animal stages, buildings, and the player (maintained by sow/growth/place/spawn/seed so the snapshot always carries current appearance; missing refs fall back to shape drawing), `play-sound` emitting the **`sound-play`** event `{soundId}`, the WS snapshot carrying `sprites`/`sounds` catalogs and (D31) the `assets` table, and a `{"type":"input","input":string}` WS message → `input` bus event with a built-in `key:Arrow*` → `IntentSystem.move` mapping (player-centered, adapter-level). **Player input (D25)**: snapshot frame carries `time` (HUD day/season on connect); `SyncHandle.intent(action, args)` is uniform across local + ws (both modes playable); `viewport.tileAt(clientX, clientY)` converts screen→tile; the dev app has an Interact/Move toolbar + canvas-click intents + HUD (seeds/gold/feedback). **Render half (D27)**: `packages/render` draws entities by `spriteRef` (pure `sprite-draw.ts` — `durationTicks`-weighted animation clock at a documented default 10 fps, tile-space→pixel command mapping, building footprint scaling; `image` commands (D31) resolve asset dims from loaded textures + source-crop rects; `shapes.ts` fallback unchanged for missing refs/unknown ids), map tiles draw via `map.tiles` → image textures (palette `tileColor` fallback), `sound.ts` `createSoundPlayer` synthesizes `sound` defs on `sound-play` (Web Audio, lazy AudioContext behind a user gesture) or decodes `src` audio assets via `decodeAudioData` (buffers cached by id; `src` takes precedence over `synth`), both sync handles gained `input(input)` (ws: raw message; local: client-side arrow mapping) and an optional `sound` player, and `apps/dev` adds arrow-key movement + audio priming. **In-game UI built (D35)** — the dev app gains DOM panels wired through the intent mechanism: a seed picker (`select-seed` intent → `player.selectedSeed`), a shop panel (discovered from `BuildingDef.shop?`/`building.shop` entities + the additive WS-snapshot `shops` catalog; `buy`/`sell` intents), a dialogue panel (visibility from the `dialogue:active` entity, content from `talk`/`choose` intent results), and an inventory bar (gold/stamina/items/seed) — pure display logic in node-tested `apps/dev/src/ui.ts`, thin DOM wiring in `main.ts`, both local and ws modes. **Mod packaging + browser Mod execution + installers built (D36)** — `nengine-pack --mods ` packages Mods into the bundle (`ContentBundle.mods?: Record` — additive, BUNDLE_VERSION stays 1; ModLoader-convention scan, per-mod loud failures); `ModLoader.loadFromSource(source, id, version?)` executes packaged Mod code via a base64 `data:text/javascript` dynamic import (browser + Node parity, zero fs, same validation/binding as file mods) so bundle mode boots `bundle.mods` (modsDir wins when both) and `apps/dev ?bundle=`/desktop need no change; `nengine-pack --installer` (implies `--desktop`) generates `electron-builder.yml` + `dist`/`dist:win` scripts + copies site files into `desktop/` — **Linux AppImage build verified end-to-end** (Windows NSIS documented config); `pnpm pack:example:full` exercises all flags. **Scalability fixes built (D37) — WS snapshot assets on-demand + `--split-assets`**: the WS snapshot frame's `assets` field (D31 — full base64 in EVERY snapshot) is replaced by a **metadata-only `assetsIndex`** (`{kind, mime, size}` — no bytes); new client→server `{"type":"assets-request","ids"}` (bounded: client `AssetCoordinator` batches ≤64 ids, server truncates over the cap, unknown ids ignored, empty request → empty response) is answered per-client with `{"type":"assets","assets":Record}`; `connectWs` derives wanted ids from snapshot presentation (pure `snapshotAssetRefs` — map `tiles` + sprite `image` layers + `sound.src`), dedupes via the pure `AssetCoordinator` (wanted/loaded/pending), and feeds arrivals into viewport/sound (re-render on ready + `setAssets`); `nengine-pack --split-assets` writes `out/nengine-assets/.` (one file per asset) + a metadata-only bundle.json (`assetsIndex` with `file` paths, no base64) while inline (default) stays byte-identical plus the additive `assetsIndex`; `AssetEntry.data` is now OPTIONAL (**the engine asset table is a metadata index — bytes are a transport concern**; split entries carry `size`/`file`); `createViewport({assetsBase?})`/`createSoundPlayer(factory?, assetsBase?)` fetch split entries via the pure `resolveAssetUrl` (pack bakes `__NENGINE_ASSETS_BASE__ = "./nengine-assets"`; dev `?bundle=` derives it from the bundle URL's dir); installer copies `nengine-assets/`; root script `pack:example:split`. **727 tests green** (D38 adds 35 — core animation-schema 10, mcp animation-namespace 9, render tween 15, client-surface/ws-broadcast/sample-game 1); D37 adds 22 — ws-broadcast 4, render asset-coordinator/refs/url 11, pack 5, core bundle 2; D36 adds 16; D35 adds 25; D34 adds 54; D33 adds 43; D31 added 28); **D37 kept the MCP surface at 20/111/32/14** (both fixes are protocol/offline-tool changes — WS transport is NOT an MCP namespace; packaging is an offline build-time tool); **D38 raises it to 21 namespaces / 116 tools / 34 resources / 15 prompts** (the additive `animation.*` namespace); **D40 foundation raises it to 21 namespaces / 119 tools / 34 resources / 16 prompts + 736 tests green** (the shared-authoring extraction refactors all twelve authoring namespaces onto `packages/mcp/src/authoring.ts` with zero behavior change, and `map.*` gains `validate`/`create`/`update` + `map.create-prompt` — D40.4's map-writing capability; the D40 shared authoring layer is BUILT, the GUI editor consumes `validateDef`/`writeContentFile`/`updateContentFile` directly); **D40.9 scene round raises it to 22 namespaces / 124 tools / 36 resources / 17 prompts + 758 tests green** (the `scene` content kind — `content/scenes/.yaml` initial-entity list, zod strict discriminated union — is instantiated at seed time by a new `seedScenes` engine step AFTER `initSystems`/BEFORE Mods: buildings via `PlacementSystem.place` with overlap/buildability validation, NPCs as entities with dialogue/label, animals via `GrowthSystem.spawnAnimal`, all using the runtime entity id formats; bad references/conflicts/duplicate npc ids FAIL boot loudly; the `scene.*` MCP namespace (22nd) goes through the shared authoring layer + `scene.list`/`scene.get` + catalog/`{id}` resources + `scene.create-prompt`; **no scene content added to examples/sample-game or content/** — demonstration lives in tests + docs). **Tier 3 content round (D29)**: stamina/skills are seeded player fields (`seedWorld`: `stamina: 100`, `skills: {}` — no StaminaSystem/SkillSystem, D19); the `player-intent` bus event gained the **`onPlayerIntent` hook** (16th — reverses D25.4's bus-only stance: Mods are hooks-only, D6/D13, and the intent payload is transient, so the fishing Mod can settle the minigame; the ONLY hook whose `HookContext.payload` is set); the sample-game now chains three quest state machines (starter-quest → carrot-quest → mayor-talk-quest), demonstrates a `player.stamina` condition + day-start restore (`daily-stamina`) and the Mod→content bridge (`fishing-treat` matches `mod:fish-caught`); the fishing Mod hooks `onPlayerIntent` (water-tile catch: fish ×1, stamina −5, `skills.fishing` +1, `mod:fish-caught`/`mod:fish-too-tired` events, stamina < 5 → too tired). Known language limitation closed by D33: `set-state` stays absolute (D20) but the new `add` incremental action now expresses the mayor-talk-quest gold reward (`add player.gold 100` — an increment action deferred in D29 is now built). Tier 2 is fully complete; Tier 3's multiplayer gameplay remains reserved (see below). **Editor design locked (D40); foundation built (D40); `apps/editor` scaffolded (D40/E1)** — a desktop-first graphical creation tool: a single Electron app, editor + runner in one (visual map editing, scene placement, schema-driven content forms, embedded playtesting, embedded MCP server for AI-assisted design sharing one engine instance + **the shared authoring layer — `packages/mcp/src/authoring.ts`, extracted from the MCP creation namespaces and already BUILT (all twelve authoring namespaces refactored onto it with zero behavior change, and `map.*` gained `validate`/`create`/`update` + `map.create-prompt` so visual map editing can write map YAML through the same path)**), packaging via nengine-pack; new `scene` content kind (initial entity list, seeded at engine start); implemented as a new `apps/editor` package in vertical slices — **E1 (project layer) BUILT (2026-08-13): the package scaffold + Electron main/preload/renderer, project OPEN/NEW (built-in minimal playable template boot-validated by `createEngine`)/RECENT-OPEN, `project.yaml` read-write, `project:listContent`/`project:stats`, 10 strictly-typed IPC channels, 28 new node tests (786 total green); E2 (map editing) BUILT (2026-08-13): a pure `src/renderer/map-model.ts` (createMapModel/paint/erase/setSpawn/toggleWalkable/addTileId/serialize/tileColor/collectTileIds — deep-cloned working def, grid ops never mutate the input, serialize re-validates via `mapSchema.safeParse` and rejects invalid edits, erase=grass→first-palette→"", spawn non-integers rejected + out-of-range clamped, walkable toggle sets false / deletes key to restore the D33 fallback, editor palette colors NOT real assets) + a thin 32px canvas-2D `map-editor.ts` DOM shell (paint/erase/spawn/walkable tools, right-click erase, palette column + add-tile-id, red-hatch walkable viz, Save = serialize → stringify → `project:writeYaml` with dirty-state button; no map resize in v1); the editor renderer imports runtime core via editor-only synthetic facets `@nengine/core-schema`/`@nengine/core-systems` (the core barrel is browser-unsafe — loader/mods use node:fs), 31 new node tests (817 total green); E3 (content forms) BUILT (2026-08-13): a **schema-driven universal form generator** — pure `src/renderer/form-schema.ts` (deriveFormSchema walks zod `_def` defensively → recursive FormSchema: field/label/type string|number|boolean|enum|ref|object|array|union, optional flags, enum values, array elements, discriminatedUnion branches + discriminator, cross-kind reference heuristics — names ending Ref/Id/Src/SeedItem → their kind, e.g. spriteRef→sprite ids; heuristic table extensible; unsupported constructs reported not mis-rendered) + pure `form-value.ts` (initialValue defaults / applyValue lossless overlay from the loaded def so unknown fields survive save / coerceForValidate) + thin DOM `form-panel.ts` (recursive: text/number/checkbox/select/ref-dropdowns fed by per-kind id lists via IPC, nested fieldsets, add/remove/reorder array editors, union-branch select; Save = merge → `contentSchemas[kind].safeParse` via the `@nengine/core-schema` facet → stringify → `project:writeYaml`; zod issues in status bar; per-kind **+ (new)** create flow, duplicate-id error, id first and read-only when editing) — **all 18 kinds formable, schema changes need no form code**; 53 new node tests (870 total green)**; **scene placement editor built (E4 — 2026-08-13): the scene kind opens a placement editor over the SAME canvas as the E2 map editor with an Edit Scene/Edit Map mode toggle (one shared canvas, map from the project's first map file, each model with its own dirty state, one Save writes both); pure `src/renderer/scene-model.ts` (`createSceneModel` + building/npc/animal CRUD with footprint-aware overlap — unknown building ids, duplicate building-at-coords, duplicate npc ids, non-integer/out-of-bounds coords, and any footprint overlap are REJECTED since `seedScenes` fails boot on placement conflicts, deliberately stricter than seed-time so authored scenes always boot; npc/animal are 1×1, buildings use a footprint table read from the project's building defs; `serialize` re-validates via `sceneSchema.safeParse` → null on invalid, invalid never writes; empty scenes valid) + thin DOM `scene-editor.ts` (building/animal palette with footprint tooltips, npc free-id form with optional dialogue/label, click-to-place, entity list with select/delete/click-to-move, Save = serialize → stringify → `project:writeYaml` to `content/scenes/.yaml`; entities render as plain colored shapes — NO asset rendering in v1, playtest territory E5); 43 new node tests (934 total green — 913 + 21 editor playtest tests)**; **embedded MCP server built (E6 — 2026-08-13, D40 #5/#16): the editor embeds an in-process MCP gateway serving the editor's OWN engine instance (same contentDir + registry + shared authoring layer) — created on project open (`createEditEngine(root)` = `createEngine({contentDir, modsDir?})` full fs mode in main), stopped on switch/close/quit (`project:close` IPC added); **streamable HTTP primary** (persistent `node:http` server on 127.0.0.1, ephemeral port, `/mcp` GET-SSE/POST/DELETE + OPTIONS CORS preflight via the SDK `StreamableHTTPServerTransport`; URL stable for the whole session, top bar shows it with a Copy MCP URL button; `attach`/`detach` swap the gateway+transport per project) + **`--mcp-stdio` headless secondary** (`node apps/editor/dist/main/mcp-stdio.js --dir ` pure-Node entry or `electron . --mcp-stdio --dir ` — the editor binary as a drop-in stdio MCP server, mirroring apps/mcp-server); dual-channel same world (decision #16, full exposure — no permission narrowing): AI edits via MCP write disk + registry through the shared authoring layer → GUI reads disk; GUI saves (`project:writeYaml`) re-register into the engine registry via `syncContentWrite` → MCP sees them immediately; the GUI never goes through the MCP protocol, so namespace toggling by an AI client can't destabilize it. Same 22 namespaces / 124 tools / 36 resources / 17 prompts wired to the editor's instance. New deps `@nengine/mcp` + `@modelcontextprotocol/sdk` (main-process-only); 14 new node tests (948 total green — 934 + 14: mcp-server HTTP +12, mcp-stdio spawn +2, real SDK clients over both transports); **package dialog built (E7 — 2026-08-13, D40 #8: a Package button in the editor top bar opens a small DOM dialog that runs the existing nengine-pack pipeline in main — `pack:run` IPC → `runEditorPack` (`src/pack.ts`) → `packGame` from `@nengine/pack` (library call not CLI, stateless like E5's `compileBundle`; `packGame` exposes no progress callbacks, so `pack:run` is a single blocking call with a dialog busy state — documented) — fields (product name, icon path, desktop, installer, include Mods, split assets, output directory default `/dist-pack`) pre-filled from `project.yaml` (D40.6 packaging metadata: product name = `pack.name` → top-level `name` → display-name chain; `pack.{installer,mods,splitAssets}`; `icon`) and saved back into `project.yaml` on run (persistent fields only; `desktop`/`outDir` stay transient run options, installer implies desktop); the merge is a pure shared module `src/pack-options.ts` (`dialogDefaultsFromProject`/`projectYamlFromPack`/`resolvePackRequest`/`packHints`, zero node imports) used by both the renderer dialog (`renderer/pack-dialog.ts`) and the main step; success shows the output path + hints (`npm install && npm run dist` in `desktop/` for installers), failures return `{ok:false,error}` — content compile failures with per-def messages, vite build errors, desktop/installer generation errors (installer mode only *writes* `electron-builder.yml`, so a missing electron-builder surfaces at `npm run dist` time — documented); `icon` is stored in project.yaml only — the v1 pipeline still ships the default Electron icon (D32.6), pack logic untouched; Mods package only when the dialog asks AND `/mods` exists; output layout = the unchanged pipeline (`index.html` + `assets/` + `bundle.json`, `nengine-assets/` when split, `desktop/` for desktop/installer); 13 new node tests (editor 190 → 203: pack-options merge 9 + real `runEditorPack` over a mkdtemp fixture 4 — site, desktop shell, installer config + site copy, compile-failure error) — 948 → **961 total green**; the Mods enable/disable panel remains future work)**. Distribution is now **desktop-first** (browsers/WS/static-site kept as dev/debug aids, no longer extended). Full detail in `docs/decisions.md` D40 + `docs/editor.md`. ## Milestone Plan 1. **Docs + interface freeze** — done (decisions/architecture/mcp/api). 2. **Walking skeleton** — done (data/render/MCP/Mod chains end-to-end). 3. **Horizontal subsystems (Tier 1)** — done: time, planting, inventory/items, economy, persistence + behavior v1 + mod introspection. 4. **Tier 2** — done: behavior language v2 (state machines + `onSchedule` timing, D20), dialogue/story, weather, buildings/placement, NPC grid movement, tick loop (`engine.tick(dt)` + `time-tick` + `at: {tickInDay}`, D23), GrowthSystem→animal `age` (animal content kind + `fed` gate + `animal-grow`/`animal-produce`, D23), multi-viewport sync (`render/sync`, D24 — WS broadcast channel + `connectWs` client + snapshot-driven viewport drawing). **Tier 2 fully complete.** 5. **Tier 3** — quests/events, stamina/skills, minigames are now **content-round complete (D29)** (quests/events via behavior v2 state machines — the mechanism; stamina/skills as seeded fields + behavior language + Mod; the fishing minigame as the Mod escape-hatch demo). **Animation state machines + smooth movement (D38)** — done: an `animation` content kind (`content/animations/` — state machine over sprite defs, zod strict; `animation.*` MCP namespace) drives the render layer's per-entity sprite selection (explicit `player.anim` field set/cleared by the behavior language, or walk/idle motion inference, or `initial`) with per-state fps/loop; smooth movement is a render-layer tween of consecutive tile coords (`MOVE_DURATION_MS` 250 ms, easeOutQuad) over the rAF loop — **the engine stays the authoritative instant grid-jump** (`moveTo`/`IntentSystem.move` untouched); no new systems/hooks. **Multiplayer gameplay remains reserved** (the multi-viewport sync mechanism is built, D24; gameplay rules are creator content). 6. **Packaging (D30/D32)** — done: `apps/pack` `nengine-pack` compiles content → `bundle.json` + static site; `createEngine({bundle})` + `loadBundle` are the engine mechanisms; `apps/dev` `?bundle=` mode reuses the same UI. **Desktop (D32)**: `nengine-pack --desktop` writes a runnable Electron shell (`out/desktop/`, `npm i && npm start`) over the same static site. Mods in packaged output, desktop installers (electron-builder), a sidecar headless core, and a `pack.*` MCP namespace are all documented future paths (not built). 7. **Editor (D40)** — done (**E1 project layer built 2026-08-13**; **E2 map editing built 2026-08-13**; **E3 forms built 2026-08-13**; **E4 scene placement built 2026-08-13**; **E5 embedded playtest built (2026-08-13 — D40 #13: a Play button in the editor top bar → save every dirty model (each editor mount registers an `onFlush` flush collector) → main compiles the project via `playtest:start` IPC (`compileBundle(/content, /mods)` from `@nengine/pack`, stateless — returns `{bundle}`/`{error}`) → the renderer boots an isolated `createEngine({bundle})` + Pixi viewport + `connectLocal` with arrow-key move / click interact / HUD time-inventory (dev-app flow), Exit destroys engine+viewport+listeners and returns to edit mode; **bundle-mode-in-renderer via node shims** (the dev-app-proven `src/shim/` aliases for `node:fs`/`node:fs/promises`/`node:path`/`node:url`, scoped to non-test builds — the full `@nengine/core` barrel now imports into the renderer, emitted chunk verified `node:fs`-free; bundle mode does zero fs calls D30/D36); pure orchestratable layer `playtest-session.ts` (saveAllFlushes + phase state machine + keydownInput) + `playtest-hud.ts` node-tested; fixture-proven playtest-visible correctness — a compiled fixture project (content+scene+mod) boots in bundle mode seeding the scene's building/npc/animal via `seedScenes` and fires a Mod hook on `engine.tick(480)`; playable v1 = move+interact+HUD; E5 (playtesting) BUILT): a **desktop-first graphical creation tool** — a single Electron app, editor + runner in one (RPG-Maker style): visual map editing, scene entity placement, schema-driven content forms (all kinds), embedded playtesting, embedded MCP server for AI-assisted design (shared engine instance + shared authoring layer), packaging via the nengine-pack pipeline. New `scene` content kind (initial entity list, seeded at engine start) — **built (D40.9)**: schema + `seedScenes` instantiation + `scene.*` MCP namespace live; the editor's scene placement vertical slice writes `content/scenes/.yaml` through the shared authoring layer. **E6 embedded MCP built (2026-08-13 — D40 #5/#16: the editor embeds an in-process MCP gateway serving the editor's own engine instance — streamable HTTP primary** (persistent server, stable URL shown in the top bar with a copy button, per-project attach/detach) **+ `--mcp-stdio` headless secondary** (pure-Node entry `node apps/editor/dist/main/mcp-stdio.js --dir `, or `electron . --mcp-stdio --dir ` — the editor binary as a drop-in stdio MCP server); engine lifecycle in main (create on open, stop on switch/close/quit); dual-channel same world — AI MCP authoring writes disk + registry (shared authoring layer) and GUI saves re-register via `syncContentWrite`, full exposure no permission narrowing; `mcp:status` IPC + top-bar URL; 14 new node tests, 948 total green); **E7 package dialog built (2026-08-13 — D40 #8: Package button → dialog pre-filled from project.yaml → `pack:run` IPC → `runEditorPack` → `packGame` in main (single blocking call, busy state) → result + installer hints; merge is the pure `pack-options.ts` shared by renderer + main; 13 new node tests, 961 total green; the Mods enable/disable panel remains future work)**. Implemented as a new `apps/editor` package in vertical slices (project layer → map editing → form generator → scene placement → playtesting → embedded MCP → package dialog). Full detail in `docs/decisions.md` D40 + `docs/editor.md`. **Editor (D40) milestone COMPLETE.**