| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
- import { tmpdir } from "node:os";
- import { join, resolve } from "node:path";
- import { describe, expect, it } from "vitest";
- import {
- ContentLoader,
- ContentRegistry,
- type AnimalDef,
- type BuildingDef,
- type CropDef,
- type DialogueDef,
- type ItemDef,
- type MapDef,
- type ShopDef,
- type StateMachineDef,
- type TimeDef,
- type TriggerDef,
- type WeatherDef,
- } from "../src/index.js";
- const repoRoot = resolve(process.cwd(), "../..");
- const contentDir = resolve(repoRoot, "content");
- describe("ContentLoader", () => {
- it("loads the real content dir and registers map+time with correct ids", async () => {
- const registry = new ContentRegistry();
- const loader = new ContentLoader(registry);
- const report = await loader.load(contentDir);
- expect(report.ok).toBe(true);
- expect(report.items.length).toBe(18);
- expect(report.items.every((i) => i.status === "ok")).toBe(true);
- expect(registry.ids("map")).toEqual(["farm"]);
- expect(registry.ids("time")).toEqual(["default"]);
- expect(registry.ids("item")).toEqual(["egg", "hoe", "parsnip-seed", "parsnip"]);
- expect(registry.ids("crop")).toEqual(["parsnip"]);
- expect(registry.ids("animal")).toEqual(["chicken"]);
- expect(registry.ids("shop")).toEqual(["general-store"]);
- expect(registry.ids("behavior")).toEqual(["season-welcome", "spring-gift"]);
- expect(registry.ids("statemachine")).toEqual(["chicken-egg", "daily-routine", "spring-quest"]);
- expect(registry.ids("dialogue")).toEqual(["welcome"]);
- expect(registry.ids("weather")).toEqual(["default"]);
- expect(registry.ids("building")).toEqual(["fence", "shed"]);
- const map = registry.get<MapDef>("map", "farm");
- expect(map?.id).toBe("farm");
- expect(map?.width).toBe(12);
- expect(map?.height).toBe(12);
- expect(map?.spawn).toEqual({ x: 2, y: 2 });
- expect(map?.layers).toHaveLength(1);
- expect(map?.layers[0]?.data).toHaveLength(12);
- expect(map?.layers[0]?.data[0]).toHaveLength(12);
- const time = registry.get<TimeDef>("time", "default");
- expect(time?.dayLengthTicks).toBe(480);
- expect(time?.seasons).toHaveLength(4);
- expect(time?.seasons[0]?.name).toBe("spring");
- expect(time?.start).toEqual({ day: 1, season: "spring" });
- const hoe = registry.get<ItemDef>("item", "hoe");
- expect(hoe).toEqual({ id: "hoe", name: "Hoe", kind: "tool", stackable: false, price: 500, use: "tills a soil tile for planting" });
- const seed = registry.get<ItemDef>("item", "parsnip-seed");
- expect(seed?.kind).toBe("seed");
- expect(seed?.stackable).toBe(true);
- const parsnip = registry.get<ItemDef>("item", "parsnip");
- expect(parsnip?.kind).toBe("material");
- expect(parsnip?.price).toBe(35);
- const crop = registry.get<CropDef>("crop", "parsnip");
- expect(crop).toEqual({
- id: "parsnip",
- seedItem: "parsnip-seed",
- stages: [
- { name: "sprout", days: 2, waterRequired: true },
- { name: "grown", days: 2, waterRequired: true },
- { name: "mature", days: 1, waterRequired: false },
- ],
- seasons: ["spring"],
- drops: [{ itemId: "parsnip", qty: 1 }],
- });
- const egg = registry.get<ItemDef>("item", "egg");
- expect(egg).toEqual({ id: "egg", name: "Egg", kind: "material", stackable: true, price: 15, use: "a chicken's daily produce" });
- const chicken = registry.get<AnimalDef>("animal", "chicken");
- expect(chicken).toEqual({
- id: "chicken",
- stages: [
- { name: "chick", days: 2, feedRequired: false },
- { name: "grown", days: 2, feedRequired: true },
- { name: "laying", days: 1, feedRequired: true },
- ],
- produces: [{ itemId: "egg", qty: 1 }],
- });
- const chickenEgg = registry.get<StateMachineDef>("statemachine", "chicken-egg");
- expect(chickenEgg).toEqual({
- id: "chicken-egg",
- initial: "waiting",
- states: [
- { id: "waiting" },
- { id: "laid", onEnter: [{ action: "give-item", itemId: "egg", qty: 1 }] },
- ],
- transitions: [
- { from: "waiting", to: "laid", on: "animal-produce", when: { op: "eq", path: "payload.animalDefId", value: "chicken" } },
- { from: "laid", to: "waiting", on: "animal-produce", when: { op: "eq", path: "payload.animalDefId", value: "chicken" } },
- ],
- });
- const shop = registry.get<ShopDef>("shop", "general-store");
- expect(shop).toEqual({
- id: "general-store",
- buys: [{ itemId: "parsnip", price: 30 }],
- sells: [
- { itemId: "hoe", price: 500 },
- { itemId: "parsnip-seed", price: 20, stock: 99 },
- ],
- });
- const springGift = registry.get<TriggerDef>("behavior", "spring-gift");
- expect(springGift).toEqual({
- id: "spring-gift",
- when: { op: "gte", path: "time.day", value: 5 },
- fire: [{ action: "give-item", itemId: "parsnip-seed", qty: 1 }],
- });
- const seasonWelcome = registry.get<TriggerDef>("behavior", "season-welcome");
- expect(seasonWelcome?.when).toEqual({
- and: [
- { op: "eq", path: "time.day", value: 1 },
- {
- or: [
- { op: "eq", path: "time.season", value: "summer" },
- { op: "eq", path: "time.season", value: "autumn" },
- ],
- },
- { not: { op: "exists", path: "player.flag" } },
- ],
- });
- expect(seasonWelcome?.fire).toEqual([
- { action: "emit-event", event: "behavior:season-welcome", payload: { season: "welcome" } },
- { action: "set-state", path: "player.flag", value: "welcomed" },
- ]);
- const dailyRoutine = registry.get<StateMachineDef>("statemachine", "daily-routine");
- expect(dailyRoutine).toEqual({
- id: "daily-routine",
- initial: "sleeping",
- states: [
- { id: "sleeping" },
- {
- id: "working",
- onEnter: [{ action: "emit-event", event: "behavior:daily-routine-working", payload: { state: "working" } }],
- },
- { id: "resting" },
- ],
- transitions: [{ from: "working", to: "resting", on: "item-acquire" }],
- onSchedule: [
- { at: "day-start", to: "resting", when: { op: "gte", path: "payload.day", value: 28 } },
- { at: "day-start", to: "working" },
- ],
- });
- const springQuest = registry.get<StateMachineDef>("statemachine", "spring-quest");
- expect(springQuest?.initial).toBe("inactive");
- expect(springQuest?.states).toHaveLength(3);
- expect(springQuest?.states[1]?.onEnter).toEqual([
- { action: "give-item", itemId: "parsnip-seed", qty: 1 },
- ]);
- expect(springQuest?.transitions).toHaveLength(2);
- expect(springQuest?.transitions[1]?.when).toEqual({ op: "gte", path: "player.inventory.parsnip", value: 5 });
- const welcome = registry.get<DialogueDef>("dialogue", "welcome");
- expect(welcome).toEqual({
- id: "welcome",
- startNode: "hello",
- nodes: [
- {
- id: "hello",
- text: "Welcome to your farm! A new season is about to begin.",
- choices: [
- {
- text: "Thanks! What should I plant first?",
- to: "seed-tip",
- actions: [{ action: "give-item", itemId: "parsnip-seed", qty: 1 }],
- },
- { text: "You remember me from last season?", to: "returning", when: { op: "exists", path: "player.flag" } },
- { text: "I am just exploring today.", to: "farewell" },
- ],
- },
- { id: "seed-tip", text: "Parsnip seeds grow well in spring — here is one to start with.", to: "farewell" },
- { id: "returning", text: "Of course! Welcome back to the farm.", to: "farewell" },
- { id: "farewell", text: "See you around!" },
- ],
- });
- const weather = registry.get<WeatherDef>("weather", "default");
- expect(weather).toEqual({
- id: "default",
- defaults: {
- sunny: { name: "Sunny" },
- rain: { name: "Rain" },
- wind: { name: "Wind" },
- storm: { name: "Storm" },
- },
- seasons: [
- { season: "spring", weathers: [
- { id: "sunny", weight: 50 },
- { id: "rain", weight: 30 },
- { id: "wind", weight: 15 },
- { id: "storm", weight: 5 },
- ] },
- { season: "summer", weathers: [
- { id: "sunny", weight: 70 },
- { id: "rain", weight: 15 },
- { id: "wind", weight: 10 },
- { id: "storm", weight: 5 },
- ] },
- { season: "autumn", weathers: [
- { id: "sunny", weight: 40 },
- { id: "rain", weight: 35 },
- { id: "wind", weight: 20 },
- { id: "storm", weight: 5 },
- ] },
- { season: "winter", weathers: [
- { id: "sunny", weight: 20 },
- { id: "rain", weight: 25 },
- { id: "wind", weight: 40 },
- { id: "storm", weight: 15 },
- ] },
- ],
- });
- const shed = registry.get<BuildingDef>("building", "shed");
- expect(shed).toEqual({
- id: "shed",
- name: "Shed",
- footprint: { w: 2, h: 2 },
- buildable: "map",
- blocksMovement: true,
- description: "Stores tools and harvests.",
- });
- expect(registry.get<BuildingDef>("building", "fence")).toEqual({
- id: "fence",
- name: "Fence",
- footprint: { w: 1, h: 1 },
- buildable: { tiles: ["grass"] },
- blocksMovement: true,
- description: "A wooden fence post.",
- });
- });
- it("reports an invalid item as error without aborting siblings", async () => {
- const tmp = await mkdtemp(join(tmpdir(), "farm-content-"));
- try {
- await mkdir(join(tmp, "maps"), { recursive: true });
- await writeFile(
- join(tmp, "maps", "good.yaml"),
- [
- "id: good",
- "width: 2",
- "height: 2",
- 'tilesetRef: "tiles"',
- "spawn: { x: 0, y: 0 }",
- "layers:",
- " - name: ground",
- " data:",
- " - [a, b]",
- " - [b, a]",
- "",
- ].join("\n"),
- );
- await writeFile(
- join(tmp, "maps", "bad.yaml"),
- ["id: bad", "width: not-a-number", "height: 2", ""].join("\n"),
- );
- const registry = new ContentRegistry();
- const loader = new ContentLoader(registry);
- const report = await loader.load(tmp);
- expect(report.ok).toBe(false);
- const ok = report.items.find((i) => i.id === "good");
- const bad = report.items.find((i) => i.id === "bad");
- expect(ok?.status).toBe("ok");
- expect(bad?.status).toBe("error");
- expect(bad?.errors?.length).toBeGreaterThan(0);
- expect(registry.get("map", "good")).toBeDefined();
- expect(registry.get("map", "bad")).toBeUndefined();
- } finally {
- await rm(tmp, { recursive: true, force: true });
- }
- });
- });
|