| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
- import { tmpdir } from "node:os";
- import { join } from "node:path";
- import { describe, expect, it } from "vitest";
- import { createEngine } from "@nengine/core";
- import { connectLocal } from "../src/sync/local.js";
- import type { RenderSnapshot, ViewportHandle } from "../src/viewport.js";
- const TIME_YAML = [
- "id: default",
- "dayLengthTicks: 480",
- "seasons:",
- " - { name: spring, dayCount: 28 }",
- "start: { day: 1, season: spring }",
- "",
- ].join("\n");
- const FARM_YAML = [
- "id: farm",
- "width: 5",
- "height: 5",
- 'tilesetRef: "tiles"',
- "spawn: { x: 0, y: 0 }",
- "layers:",
- " - name: ground",
- " data:",
- " - [grass, grass, water, water, water]",
- " - [grass, grass, water, water, water]",
- " - [grass, grass, grass, grass, grass]",
- " - [grass, grass, grass, grass, grass]",
- " - [grass, grass, grass, grass, grass]",
- "",
- ].join("\n");
- function fakeViewport(): { viewport: ViewportHandle; renders: RenderSnapshot[] } {
- const renders: RenderSnapshot[] = [];
- const viewport: ViewportHandle = {
- render(snapshot?: RenderSnapshot): void {
- if (snapshot) renders.push(snapshot);
- },
- tileAt(): { x: number; y: number } | null {
- return null;
- },
- resize(): void {},
- destroy(): void {},
- };
- return { viewport, renders };
- }
- interface Setup {
- engine: ReturnType<typeof createEngine>;
- close: () => Promise<void>;
- }
- async function setup(): Promise<Setup> {
- const tmp = await mkdtemp(join(tmpdir(), "farm-sync-local-"));
- await mkdir(join(tmp, "maps"), { recursive: true });
- await writeFile(join(tmp, "time.yaml"), TIME_YAML);
- await writeFile(join(tmp, "maps", "farm.yaml"), FARM_YAML);
- const engine = createEngine({ contentDir: tmp });
- await engine.start();
- return {
- engine,
- close: async () => {
- await engine.stop();
- await rm(tmp, { recursive: true, force: true });
- },
- };
- }
- describe("connectLocal sync handle", () => {
- it("renders a full snapshot (map + world entities) on connect", async () => {
- const { engine, close } = await setup();
- try {
- const { viewport, renders } = fakeViewport();
- connectLocal(engine, viewport);
- expect(renders).toHaveLength(1);
- expect(renders[0].map).toMatchObject({ id: "farm", width: 5, height: 5 });
- const ids = renders[0].world.map((entity) => entity.id);
- expect(ids).toContain("player");
- } finally {
- await close();
- }
- });
- it("intent() delegates to the engine IntentSystem and re-renders the viewport", async () => {
- const { engine, close } = await setup();
- try {
- const { viewport, renders } = fakeViewport();
- const sync = connectLocal(engine, viewport);
- const before = renders.length;
- sync.intent("move", { tile: { x: 3, y: 3 } });
- expect(engine.world.get("player")).toMatchObject({ x: 3, y: 3 });
- expect(renders.length).toBeGreaterThan(before);
- const last = renders[renders.length - 1];
- const player = last.world.find((entity) => entity.id === "player") as { x?: number; y?: number } | undefined;
- expect(player).toMatchObject({ x: 3, y: 3 });
- } finally {
- await close();
- }
- });
- it("setTimeLabel reports the current time state", async () => {
- const { engine, close } = await setup();
- try {
- const { viewport } = fakeViewport();
- const sync = connectLocal(engine, viewport);
- const labels: string[] = [];
- sync.setTimeLabel((label) => labels.push(label));
- expect(labels[labels.length - 1]).toBe("Day 1, spring");
- } finally {
- await close();
- }
- });
- it("input() maps key:Arrow* to a move intent one tile from the player", async () => {
- const { engine, close } = await setup();
- try {
- const { viewport, renders } = fakeViewport();
- const sync = connectLocal(engine, viewport);
- const before = renders.length;
- sync.input("key:ArrowDown");
- expect(engine.world.get("player")).toMatchObject({ x: 0, y: 1 });
- expect(renders.length).toBeGreaterThan(before);
- sync.input("key:ArrowRight");
- expect(engine.world.get("player")).toMatchObject({ x: 1, y: 1 });
- } finally {
- await close();
- }
- });
- it("input() ignores non-arrow inputs", async () => {
- const { engine, close } = await setup();
- try {
- const { viewport } = fakeViewport();
- const sync = connectLocal(engine, viewport);
- sync.input("key:Space");
- sync.input("pointer:click:1:2");
- expect(engine.world.get("player")).toMatchObject({ x: 0, y: 0 });
- } finally {
- await close();
- }
- });
- it("intent() passes the extended actions through (select-seed sets player.selectedSeed)", async () => {
- const { engine, close } = await setup();
- try {
- engine.registry.register("item", { id: "parsnip-seed", name: "Parsnip Seed", kind: "seed", stackable: true });
- const { viewport, renders } = fakeViewport();
- const sync = connectLocal(engine, viewport);
- const before = renders.length;
- sync.intent("select-seed", { seedItemId: "parsnip-seed" });
- expect(engine.world.get("player")).toMatchObject({ selectedSeed: "parsnip-seed" });
- expect(renders.length).toBeGreaterThan(before);
- sync.intent("buy", { shopId: "nope", itemId: "parsnip-seed" });
- expect(renders.length).toBeGreaterThan(before);
- } finally {
- await close();
- }
- });
- });
- describe("connectLocal sound wiring (bug-fix round)", () => {
- it("forwards bgm-play and bgm-stop engine events to the sound player", async () => {
- const { engine, close } = await setup();
- try {
- const calls: { event: string; payload: unknown }[] = [];
- const sound = {
- setCatalog(): void {},
- setAssets(): void {},
- onEvent: (frame: { event: string; payload: unknown }) => calls.push(frame),
- } as never;
- connectLocal(engine, fakeViewport().viewport, { sound });
- engine.events.emit("bgm-play", { soundId: "bgm-spring", fadeMs: 1500 });
- engine.events.emit("bgm-stop", { fadeMs: 800 });
- expect(calls).toEqual([
- { event: "bgm-play", payload: { soundId: "bgm-spring", fadeMs: 1500 } },
- { event: "bgm-stop", payload: { fadeMs: 800 } },
- ]);
- } finally {
- await close();
- }
- });
- });
|