|
|
2 meses atrás | |
|---|---|---|
| .. | ||
| content | 2 meses atrás | |
| mods | 2 meses atrás | |
| README.md | 2 meses atrás | |
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.
Spring Day 1. You are a new farmer. The game's arc:
Spring Day 1, 0 gold, empty inventory.day-start, the starter-gift trigger
grants 3 parsnip seeds + 2 carrot seeds (single-shot via a flag).starter-quest state machine activates on Day 2:
harvest 3 parsnips.time.tick drives growth day by
day (parsnip = spring crop, 2/2/1 days; carrot = summer crop, 2/3/1 days).chicken-egg state machine turns animal-produce events into eggs.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.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.
| 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) |
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/.
Connect any MCP client over stdio. Key tool calls for the full loop:
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).time.advance {ticks: 480} — first day-start: starter gift lands
(3 parsnip-seed + 2 carrot-seed), quest activates, weather rolls, mod
spawns npc:mayor.item.query_inventory — see the gift.npc.list — see npc:mayor (spawned by the mod, not by the engine).weather.get — see the rolled weather.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).time.tick {ticks: 480} — grow the crop one day (repeat + re-water).crop.harvest {x, y} once mature (crop.stage {x, y, stageIndex: 2} or
time.tick growth) → quest completes (behavior.statemachine.status
{machineId: starter-quest}).shop.sell {shopId: general-store, itemId: parsnip, qty: 3} → gold.shop.buy {shopId: general-store, itemId: carrot-seed, qty: 1}.placement.place {buildingId: coop, x: 10, y: 4} (+ fence).animal.spawn {animalId: chicken, x: 9, y: 3} — chickens are runtime
entities; the shop sells items only.animal.stage {id: animal:chicken:1, stageIndex: 2} + animal.feed
(feed each day).time.tick {ticks: 480} → animal-produce fires → the chicken-egg
machine grants an egg; see it in item.query_inventory.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.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.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.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.
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
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:
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.
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).
{"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.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),harvested <crop>),select-seed intent (sowed <seed>; falls back to the first seed in inventory; no seed in inventory otherwise),crop parsnip is not ready to harvest).time.advance/time.tick or auto-tick), so the button is disabled.The dev app adds four DOM panels driven through the same intent mechanism:
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 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: "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).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.
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?}.
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).