sync-local.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
  2. import { tmpdir } from "node:os";
  3. import { join } from "node:path";
  4. import { describe, expect, it } from "vitest";
  5. import { createEngine } from "@nengine/core";
  6. import { connectLocal } from "../src/sync/local.js";
  7. import type { RenderSnapshot, ViewportHandle } from "../src/viewport.js";
  8. const TIME_YAML = [
  9. "id: default",
  10. "dayLengthTicks: 480",
  11. "seasons:",
  12. " - { name: spring, dayCount: 28 }",
  13. "start: { day: 1, season: spring }",
  14. "",
  15. ].join("\n");
  16. const FARM_YAML = [
  17. "id: farm",
  18. "width: 5",
  19. "height: 5",
  20. 'tilesetRef: "tiles"',
  21. "spawn: { x: 0, y: 0 }",
  22. "layers:",
  23. " - name: ground",
  24. " data:",
  25. " - [grass, grass, water, water, water]",
  26. " - [grass, grass, water, water, water]",
  27. " - [grass, grass, grass, grass, grass]",
  28. " - [grass, grass, grass, grass, grass]",
  29. " - [grass, grass, grass, grass, grass]",
  30. "",
  31. ].join("\n");
  32. function fakeViewport(): { viewport: ViewportHandle; renders: RenderSnapshot[] } {
  33. const renders: RenderSnapshot[] = [];
  34. const viewport: ViewportHandle = {
  35. render(snapshot?: RenderSnapshot): void {
  36. if (snapshot) renders.push(snapshot);
  37. },
  38. tileAt(): { x: number; y: number } | null {
  39. return null;
  40. },
  41. resize(): void {},
  42. destroy(): void {},
  43. };
  44. return { viewport, renders };
  45. }
  46. interface Setup {
  47. engine: ReturnType<typeof createEngine>;
  48. close: () => Promise<void>;
  49. }
  50. async function setup(): Promise<Setup> {
  51. const tmp = await mkdtemp(join(tmpdir(), "farm-sync-local-"));
  52. await mkdir(join(tmp, "maps"), { recursive: true });
  53. await writeFile(join(tmp, "time.yaml"), TIME_YAML);
  54. await writeFile(join(tmp, "maps", "farm.yaml"), FARM_YAML);
  55. const engine = createEngine({ contentDir: tmp });
  56. await engine.start();
  57. return {
  58. engine,
  59. close: async () => {
  60. await engine.stop();
  61. await rm(tmp, { recursive: true, force: true });
  62. },
  63. };
  64. }
  65. describe("connectLocal sync handle", () => {
  66. it("renders a full snapshot (map + world entities) on connect", async () => {
  67. const { engine, close } = await setup();
  68. try {
  69. const { viewport, renders } = fakeViewport();
  70. connectLocal(engine, viewport);
  71. expect(renders).toHaveLength(1);
  72. expect(renders[0].map).toMatchObject({ id: "farm", width: 5, height: 5 });
  73. const ids = renders[0].world.map((entity) => entity.id);
  74. expect(ids).toContain("player");
  75. } finally {
  76. await close();
  77. }
  78. });
  79. it("intent() delegates to the engine IntentSystem and re-renders the viewport", async () => {
  80. const { engine, close } = await setup();
  81. try {
  82. const { viewport, renders } = fakeViewport();
  83. const sync = connectLocal(engine, viewport);
  84. const before = renders.length;
  85. sync.intent("move", { tile: { x: 3, y: 3 } });
  86. expect(engine.world.get("player")).toMatchObject({ x: 3, y: 3 });
  87. expect(renders.length).toBeGreaterThan(before);
  88. const last = renders[renders.length - 1];
  89. const player = last.world.find((entity) => entity.id === "player") as { x?: number; y?: number } | undefined;
  90. expect(player).toMatchObject({ x: 3, y: 3 });
  91. } finally {
  92. await close();
  93. }
  94. });
  95. it("setTimeLabel reports the current time state", async () => {
  96. const { engine, close } = await setup();
  97. try {
  98. const { viewport } = fakeViewport();
  99. const sync = connectLocal(engine, viewport);
  100. const labels: string[] = [];
  101. sync.setTimeLabel((label) => labels.push(label));
  102. expect(labels[labels.length - 1]).toBe("Day 1, spring");
  103. } finally {
  104. await close();
  105. }
  106. });
  107. it("input() maps key:Arrow* to a move intent one tile from the player", async () => {
  108. const { engine, close } = await setup();
  109. try {
  110. const { viewport, renders } = fakeViewport();
  111. const sync = connectLocal(engine, viewport);
  112. const before = renders.length;
  113. sync.input("key:ArrowDown");
  114. expect(engine.world.get("player")).toMatchObject({ x: 0, y: 1 });
  115. expect(renders.length).toBeGreaterThan(before);
  116. sync.input("key:ArrowRight");
  117. expect(engine.world.get("player")).toMatchObject({ x: 1, y: 1 });
  118. } finally {
  119. await close();
  120. }
  121. });
  122. it("input() ignores non-arrow inputs", async () => {
  123. const { engine, close } = await setup();
  124. try {
  125. const { viewport } = fakeViewport();
  126. const sync = connectLocal(engine, viewport);
  127. sync.input("key:Space");
  128. sync.input("pointer:click:1:2");
  129. expect(engine.world.get("player")).toMatchObject({ x: 0, y: 0 });
  130. } finally {
  131. await close();
  132. }
  133. });
  134. it("intent() passes the extended actions through (select-seed sets player.selectedSeed)", async () => {
  135. const { engine, close } = await setup();
  136. try {
  137. engine.registry.register("item", { id: "parsnip-seed", name: "Parsnip Seed", kind: "seed", stackable: true });
  138. const { viewport, renders } = fakeViewport();
  139. const sync = connectLocal(engine, viewport);
  140. const before = renders.length;
  141. sync.intent("select-seed", { seedItemId: "parsnip-seed" });
  142. expect(engine.world.get("player")).toMatchObject({ selectedSeed: "parsnip-seed" });
  143. expect(renders.length).toBeGreaterThan(before);
  144. sync.intent("buy", { shopId: "nope", itemId: "parsnip-seed" });
  145. expect(renders.length).toBeGreaterThan(before);
  146. } finally {
  147. await close();
  148. }
  149. });
  150. });