nh99sysrj6-blip 7dbfe75fb3 Crafting system + festival templates + BGM + lifecycle + sample-game expansion + doc sync 2 månader sedan
..
content 7dbfe75fb3 Crafting system + festival templates + BGM + lifecycle + sample-game expansion + doc sync 2 månader sedan
mods 7dbfe75fb3 Crafting system + festival templates + BGM + lifecycle + sample-game expansion + doc sync 2 månader sedan
README.md 3c43aabf79 nEngine: top-down 2D farming game engine with MCP-first interfaces, engine-level Mod support, and enforced data-driven content 2 månader sedan

README.md

Sample Game — Pinefield Valley

A complete, small, playable farm game built entirely from engine content (YAML) plus one mod. It exists to prove the nEngine's data-driven contract: no engine code was touched to make this game; every capability is exercised through content and the MCP surface.

What the game is

Spring Day 1. You are a new farmer. The game's arc:

  1. Start — Spring Day 1, 0 gold, empty inventory.
  2. Starter gift — on the first day-start, the starter-gift trigger grants 3 parsnip seeds + 2 carrot seeds (single-shot via a flag).
  3. Quest — the starter-quest state machine activates on Day 2: harvest 3 parsnips.
  4. Grow — till, sow, and water plots; time.tick drives growth day by day (parsnip = spring crop, 2/2/1 days; carrot = summer crop, 2/3/1 days).
  5. Harvest & sell — harvest mature crops, sell them at the general store.
  6. Buy — spend gold on tools/seeds.
  7. Build — place a 3×3 coop (and fences) on the grass.
  8. Chicken — acquire a chicken, force it to laying, feed it daily; the chicken-egg state machine turns animal-produce events into eggs.
  9. Talk to the Mayor — a dialogue with choices, a condition-gated branch (only after the starter quest completes), and give-item actions on choices and nodes.
  10. Quest chain (D29) — once the starter quest completes, the carrot-quest machine activates on the next day-start (harvest a carrot); completing it activates mayor-talk-quest (finish a mayor dialogue), which pays an egg + gold and unlocks a new "fishing tip" branch in the mayor dialogue.
  11. Fishing (D29) — stamina/skills are plain player fields; the fishing Mod hooks the player's intent: interact with a pond water tile to cast (costs 5 stamina, skills.fishing +1, grants a fish), and every third catch pays a free egg via the fishing-treat machine. Exhaust your stamina below 5 and the Mod tells you you're too tired.

Along the way, weather rolls every day-start, and the mod spawns the Mayor NPC (NPCs have no content kind — they are runtime entities), walks him according to the mayor-routine state machine, reports the weather, and emits mod:sample-day-start.

Content inventory

File Demonstrates
content/maps/farm.yaml 16×16 map, variety (pond water, pre-tilled bed, open grass), spawn point
content/time.yaml 4 seasons × 28 days, 480 ticks/day, Spring Day 1 start
content/weather.yaml 4-season weighted weather table (sunny/rain/wind/storm)
content/items/hoe.yaml tool, non-stackable
content/items/watering-can.yaml tool (decorative use in this slice)
content/items/parsnip-seed.yaml / carrot-seed.yaml seeds
content/items/parsnip.yaml / carrot.yaml / egg.yaml / fish.yaml harvest materials + produce + the fishing catch (granted by the Mod, D29)
content/crops/parsnip.yaml spring crop, 3 stages, water-gated, drops parsnip
content/crops/carrot.yaml summer crop, different days (2/3/1) + same water gating
content/animals/chicken.yaml chick→grown→laying, feedRequired from grown, produces egg
content/sprites/*.yaml procedural sprite draw instructions (D26) + image layers (D31): player + chicken stages stay programmatic; parsnip/carrot stages, the coop, and the fence draw Kenney assets from content/assets/ (image layers with src), some with programmatic root/leaf accents layered on top
content/sounds/*.yaml sound specs (D26): till/water/harvest/coin now play audio assets (src: drop-1/pluck-1/confirm-1/click-1, D31)
content/assets/ binary asset table (D31): 13 Kenney Tiny Farm tiles (.png) + 8 Kenney interface sounds (.ogg), CC0 — self-descriptive ids (grass-1, water-1, tilled-1, plant-sprout, plant-grown, plant-mature, carrot-sprout, carrot-grown, carrot-mature, coop-1, coop-2, fence-1, grass-2; click-1, confirm-1, error-1, back-1, drop-1, pluck-1, switch-1, bong-1). NOT a content kind — the loader builds the asset table (engine.assets); YAML references assets by id. Keep assets small: the table is base64-inlined in bundle.json and every WS snapshot frame
content/buildings/coop.yaml 3×3 footprint, grass-only, blocks movement
content/buildings/fence.yaml 1×1 footprint, grass-only, blocks movement
content/shops/general-store.yaml buys parsnip/carrot/egg; sells tools + seeds
content/behaviors/starter-gift.yaml single-shot trigger (flag re-arm pattern) with give-item + set-state
content/statemachines/starter-quest.yaml quest state machine (day-start activate, crop-harvest complete, reward)
content/statemachines/carrot-quest.yaml quest chain 2 (D29): activates when the starter quest completes, completes on a carrot harvest
content/statemachines/mayor-talk-quest.yaml quest chain 3 (D29): activates when the carrot quest completes, completes on a mayor dialogue-end, pays egg + gold
content/statemachines/chicken-egg.yaml production rule: animal-produce event → egg grant
content/statemachines/mayor-routine.yaml NPC daily routine via onSchedule day-start entries
content/statemachines/daily-stamina.yaml stamina rules (D29): a condition on player.stamina + a day-start restore
content/statemachines/fishing-treat.yaml Mod→content bridge (D29): matches the Mod's mod:fish-caught event, every third catch pays an egg
content/dialogues/mayor.yaml choices, condition-gated branches (incl. the D29 quest-gated fishing tip), give-item actions on choice + node
mods/sample-game-mod/index.mjs the Mod escape hatch (D6/D9/D29): spawns + walks the Mayor NPC, reports weather, emits an event, and settles the fishing minigame on onPlayerIntent (fish grant, stamina drain, skills.fishing, mod:fish-caught/mod:fish-too-tired)

Run the MCP server against this game

Build first (the entry ships from dist):

pnpm -r build

Then boot the stdio server pointing at this game's content/mods:

node apps/mcp-server/dist/index.js \
  --content-dir examples/sample-game/content \
  --mods-dir examples/sample-game/mods

Equivalent via environment variables (CLI still wins if both are set):

NENGINE_CONTENT_DIR=examples/sample-game/content \
NENGINE_MODS_DIR=examples/sample-game/mods \
node apps/mcp-server/dist/index.js

Zero-arg invocation keeps serving the monorepo's own content//mods/.

Drive the game with an MCP client

Connect any MCP client over stdio. Key tool calls for the full loop:

  1. server.info — confirm the loaded game content (map farm, crops carrot+parsnip, animals chicken, buildings coop+fence, dialogue mayor, behaviors starter-gift, statemachines carrot-quest/chicken-egg/daily-stamina/fishing-treat/mayor-routine/mayor-talk-quest/starter-quest, shop general-store).
  2. time.advance {ticks: 480} — first day-start: starter gift lands (3 parsnip-seed + 2 carrot-seed), quest activates, weather rolls, mod spawns npc:mayor.
  3. item.query_inventory — see the gift.
  4. npc.list — see npc:mayor (spawned by the mod, not by the engine).
  5. weather.get — see the rolled weather.
  6. Till/sow/water plots — crop.till {x, y} then crop.sow {x, y, seedItemId: parsnip-seed} (consumes 1 seed from the player inventory), then crop.water {x, y} (sets the plot watered; mirrors PlantingSystem.water, safe to call repeatedly — watering an already-watered plot keeps it watered, so a waterRequired stage never flips back mid-loop).
  7. time.tick {ticks: 480} — grow the crop one day (repeat + re-water).
  8. crop.harvest {x, y} once mature (crop.stage {x, y, stageIndex: 2} or time.tick growth) → quest completes (behavior.statemachine.status {machineId: starter-quest}).
  9. shop.sell {shopId: general-store, itemId: parsnip, qty: 3} → gold.
  10. shop.buy {shopId: general-store, itemId: carrot-seed, qty: 1}.
  11. placement.place {buildingId: coop, x: 10, y: 4} (+ fence).
  12. animal.spawn {animalId: chicken, x: 9, y: 3} — chickens are runtime entities; the shop sells items only.
  13. animal.stage {id: animal:chicken:1, stageIndex: 2} + animal.feed (feed each day).
  14. time.tick {ticks: 480} → animal-produce fires → the chicken-egg machine grants an egg; see it in item.query_inventory.
  15. dialogue.start {dialogueId: mayor} → dialogue.choose {index: 2} ("Got anything for me?", only present after the quest) → +2 eggs; then dialogue.choose {index: 0} to leave.
  16. Quest chain (D29) — after the starter quest completes, advance a day (time.tick {ticks: 480}) to activate carrot-quest; harvest a carrot (grow carrot-seed — 2/3/1 days — then crop.harvest) → +2 carrot seeds. Advance a day again → mayor-talk-quest activates; start + finish the mayor dialogue (dialogue.choose to the terminal node) → +1 egg + gold, and a new "Tell me about fishing." branch appears.
  17. Fishing (D29) — the pond is the top-left 4×3 water block. Use player.intent {action: interact, tile: {x: 3, y: 2}} (or the dev app's canvas) to cast: +1 fish (sellable at the general store), −5 stamina, player.skills.fishing +1. Every third catch pays a free egg (fishing-treat). Fish with player.stamina < 5 and the Mod emits mod:fish-too-tired instead of a catch.

Run the dev render app against this game

The dev render app (apps/dev) is parameterized via URL query — it boots an engine in the browser for its own content by default, or renders a headless core's world over WebSocket. To view THIS game's world, start the headless server with a WS channel and open the app in ws mode:

# Terminal 1 — headless core + MCP + WS broadcast on 8787
pnpm -r build
node apps/mcp-server/dist/index.js \
  --content-dir examples/sample-game/content \
  --mods-dir examples/sample-game/mods \
  --ws-port 8787

# Terminal 2 — the dev app
pnpm dev

Then open http://localhost:5173/apps/dev/?sync=ws&url=ws://localhost:8787. The browser renders snapshots from the headless core; the HUD shows day/season, seed counts, and gold.

Package this game (D30)

The engine ships a packaging CLI (apps/pack, nengine-pack) that compiles a game's content (YAML) into a self-contained static site: bundle.json (the content plus the asset table — base64-inlined content/assets/ binaries; the single source of truth for packaged output, no YAML files and no separate binary files ship) plus index.html + assets/ (the built dev app in ?bundle= mode). The packaged game runs entirely in the browser via createEngine({bundle}) + LocalSync — no dev server, no headless core.

# build first (the CLI ships from dist)
pnpm -r build

# package this game
pnpm pack:example
# equivalent:
node apps/pack/dist/index.js \
  --content examples/sample-game/content \
  --out examples/sample-game/dist-pack

Then serve the output folder statically and open it — no URL params needed (the built index.html defaults to the bundle):

npx serve examples/sample-game/dist-pack
# or: python3 -m http.server 8000 --directory examples/sample-game/dist-pack
# open http://localhost:3000 (or :8000)

The packaged game keeps the full dev-app playability (Interact/Move toolbar, arrow keys, HUD with day/season + inventory + gold, sprite image layers + map tile textures + src audio sounds). Since D36 you can package the Mod too with --mods — the Mayor NPC and the fishing minigame then run in the packaged game exactly as in dev (the engine executes the bundled Mod source via a data-URL import, browser + desktop):

pnpm pack:example:full
# equivalent:
node apps/pack/dist/index.js \
  --content examples/sample-game/content \
  --mods examples/sample-game/mods \
  --out examples/sample-game/dist-pack

pack:example (no --mods) still packages the content-only game, unchanged. Desktop (D32) + installers (D36): see below.

Split-assets packaging (D37): by default the 21 binary assets are base64-inlined in bundle.json (self-contained single file). Pass --split-assets to write them as separate files instead — the packaged site then serves out/nengine-assets/<id>.png/.ogg (one file per asset) with a metadata-only bundle.json (the renderer fetches the bytes on demand):

pnpm pack:example:split
# equivalent:
node apps/pack/dist/index.js \
  --content examples/sample-game/content \
  --out examples/sample-game/dist-pack \
  --split-assets

The game renders identically in both modes (the renderer fetches from the sibling nengine-assets/ dir when entries carry file instead of inline base64); inline stays the default (zero-regression). The dev-app ?bundle= mode derives the fetch base from the bundle's directory, so pointing it at a split dist-pack/bundle.json also works.

To try the same bundle in the dev app (over the vite dev server), pass ?bundle= with a repo-relative path:

http://localhost:5173/apps/dev/?bundle=examples/sample-game/dist-pack/bundle.json

Run this game as a desktop app (D32)

nengine-pack --desktop writes a runnable Electron app into out/desktop/ next to the static site. It serves the folder over a local loopback HTTP server and opens the same bundle-mode page in a 960×640 window.

# package with the desktop shell (window title "Pinefield Valley")
pnpm pack:example:desktop
# equivalent:
node apps/pack/dist/index.js \
  --content examples/sample-game/content \
  --out examples/sample-game/dist-pack \
  --desktop --name "Pinefield Valley"

# run it (installs Electron once, ~100 MB)
cd examples/sample-game/dist-pack/desktop
npm install
npm start

The game window is fully playable exactly like the browser package: Interact/Move toolbar, arrow-key movement, HUD, textures and sounds. Electron was chosen over Tauri because it needs no system packages (npm-only, bundles Chromium); Tauri requires webkit2gtk-4.1 system deps. v1 ships no installer (electron-builder is not a dependency), uses the default Electron icon, and does not bundle a sidecar headless core (server-side Mods + multiplayer stay a documented future path) — see docs/decisions.md D32. Desktop + Mods + installers (D36): pass --mods and --installer to package the Mod into the game and generate installer config:

Build installers (D36)

nengine-pack --installer (implies --desktop) writes an electron-builder.yml into out/desktop/ (and copies the site files in so the builder can package them), plus dist/dist:win scripts:

pnpm pack:example:full
# equivalent:
node apps/pack/dist/index.js \
  --content examples/sample-game/content \
  --mods examples/sample-game/mods \
  --out examples/sample-game/dist-pack \
  --desktop --installer --name "Pinefield Valley"

# build the installer (installs electron-builder once)
cd examples/sample-game/dist-pack/desktop
npm install
npm run dist       # builds dist/*.AppImage (Linux)
npm run dist:win   # builds a Windows NSIS installer (run on Windows)

The Linux AppImage build is verified end-to-end in this repo (dist/Pinefield Valley-0.1.0.AppImage); the Windows NSIS target is documented config (needs a Windows host). The packaged game includes the Mod (--mods), so the Mayor and the fishing minigame work in the installed app too.

How to play

The dev app is playable in both sync modes (?sync=ws&url=ws://... connects to a headless core; the default local mode boots the engine in the browser — use ?contentDir=examples/sample-game/content&modsDir=examples/sample-game/mods to boot THIS game's content locally).

  1. Choose a tool from the HUD toolbar: Interact (default) or Move.
  2. Use the arrow keys (↑ ↓ ← →) to walk the player one tile in that direction — works in both sync modes: ws mode sends the raw {"type":"input","input":"key:Arrow<…>"} message (the headless core maps it to a player move), local mode maps the same arrow client-side. Holding a key repeats.
  3. Click a tile on the map — the canvas click becomes an intent:
    • Move — walk the player to the clicked tile (grass only; water and buildings block).
    • Interact — a smart context action on the clicked plot tile, in priority order:
      • a water tile is fished by the sample-game Mod (onPlayerIntent): +1 fish, −5 stamina, skills.fishing +1 (the engine's default till-on-interact is overridden on the pond; with stamina < 5 the Mod reports "too tired" instead),
      • a mature crop is harvested (the HUD feedback shows harvested <crop>),
      • a planted, un-watered plot is watered,
      • a tilled, empty plot is sown with the selected seed — the HUD seed picker (D35) sets it via the select-seed intent (sowed <seed>; falls back to the first seed in inventory; no seed in inventory otherwise),
      • an un-tilled tile is tilled (creates the plot),
      • anything else reports an error message (e.g. crop parsnip is not ready to harvest).
  4. Read the HUD — day/season, gold, stamina, every inventory item, the selected seed, and the last intent's feedback message (auto-clears after a few seconds). The inventory bar (bottom) lists items with counts.
  5. Advance time — in local mode the HUD Advance day button drives a full day (480 ticks); in ws mode time is driven by the headless core (via MCP time.advance/time.tick or auto-tick), so the button is disabled.

In-game UI panels (D35)

The dev app adds four DOM panels driven through the same intent mechanism:

  • Seed picker (HUD): one button per owned seed (local mode filters to seed-kind items via the registry; ws mode offers every owned item — the snapshot carries no item defs — and the engine validates). Clicking sends player.intent select-seed {seedItemId}; the selected button is highlighted and interact's sow prefers that seed while it is owned.
  • Shop panel: clicking a building that carries a shop field (the sample game's shop building def → shop: general-store; place it with placement.place {buildingId:"shop", x, y}) opens a panel with the shop's buy rows (Shop sells — click Buy) and sell rows (Shop buys — click Sell, priced from your owned stock). Rows come from the shop def (registry in local mode, the WS snapshot's shops catalog in ws mode) and owned counts refresh on each snapshot.
  • Dialogue panel: talking to an NPC (the mayor Mod spawn carries dialogue: "mayor") — via player.intent talk {npcId:"npc:mayor"} or by clicking the mayor's tile — opens a panel with the current node text and choice buttons. Choosing sends player.intent choose {index}; the panel hides when the dialogue ends (the dialogue:active entity leaves the world).
  • Inventory bar (bottom): all inventory items with counts, gold, stamina, and the current seed selection — updated from every snapshot.

Presentation (D26/D27/D31)

The world is drawn from the game's sprite content (content/sprites/): the player, crop stages (parsnip/carrot × sprout/grown/mature), chicken stages, and the coop/fence buildings each render as their sprite def (buildings scaled to their footprint); entities without a sprite ref fall back to the engine's shape drawing. Since D31, crop-stage/coop/fence sprites use image layers that draw Kenney assets from content/assets/ as PixiJS textures (the map's tiles mapping draws grass/water/tilled from the same assets), while the player and chickens stay programmatic — both paths coexist. Game events play sound (content/sounds/): the four defs now decode audio assets (src), e.g. the starter-gift day-start plays the coin ping from click-1.ogg; audio starts on the first keypress/click (browser autoplay policy).

Game loop reminder: the starter-gift trigger fires on the first day-start (3 parsnip-seed + 2 carrot-seed). Till → sow → water the central bed, advance days to grow (parsnips 2/2/1 in spring), harvest, sell at the general store, and follow the quest.

Agents can play too

The same intent mechanism is exposed as MCP player.intent — an agent drives the identical IntentSystem a human drives from the canvas, with the same validation and events (D25): move/interact take {action, tile}; since D35 buy/sell take {action, shopId, itemId, qty?}, talk takes {action, npcId|tile}, choose takes {action, index}, and select-seed takes {action, seedItemId?}.

Test coverage

apps/mcp-server/test/sample-game.test.ts boots this exact game through the parameterized entry (resolveDirs → createEngine), asserts every content kind loads, the mod spawned the Mayor, the starter gift fires on the first day-start, the full gameplay arc runs (till/sow/water/tick/harvest/ sell/buy/place/feed/egg + quest + dialogue), the asset table loads (D31 — 21 assets: 13 tiles + 8 sounds, referenced by the map tiles mapping, sprite image layers, and sound src ids) and — since D29 — the Tier 3 round: the chained quests (carrot-quest + mayor-talk-quest complete with rewards), the seeded stamina/skills fields, and the fishing minigame flow (water-tile interact → fish + stamina drain + skills.fishing + mod:fish-caught → fishing-treat egg on the third catch → too-tired gate).