playtest-hud.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { describe, expect, it } from "vitest";
  2. import {
  3. buildingShopAt,
  4. dialogueActiveInWorld,
  5. dialogueChoicesFromNode,
  6. inventorySummary,
  7. playerStatsFromSnapshot,
  8. seedListFromSnapshot,
  9. shopRowsFromDef,
  10. } from "../src/renderer/playtest-hud.js";
  11. describe("playerStatsFromSnapshot", () => {
  12. it("reads gold, stamina, seed and inventory from the player entity", () => {
  13. const stats = playerStatsFromSnapshot([
  14. { id: "plot:0:0", type: "plot" },
  15. { id: "player", type: "player", gold: 42, stamina: 30, selectedSeed: "carrot-seed", inventory: { carrot: 3, "carrot-seed": 1 } },
  16. ]);
  17. expect(stats.gold).toBe(42);
  18. expect(stats.stamina).toBe(30);
  19. expect(stats.selectedSeed).toBe("carrot-seed");
  20. expect(stats.inventory).toEqual({ carrot: 3, "carrot-seed": 1 });
  21. });
  22. it("defaults when the player or fields are missing", () => {
  23. const stats = playerStatsFromSnapshot([]);
  24. expect(stats.gold).toBe(0);
  25. expect(stats.stamina).toBe(0);
  26. expect(stats.selectedSeed).toBeNull();
  27. expect(stats.inventory).toEqual({});
  28. });
  29. it("drops zero/negative inventory quantities", () => {
  30. const stats = playerStatsFromSnapshot([
  31. { id: "player", type: "player", inventory: { carrot: 2, hoe: 0, weed: -1 } },
  32. ]);
  33. expect(stats.inventory).toEqual({ carrot: 2 });
  34. });
  35. });
  36. describe("inventorySummary", () => {
  37. it("joins inventory items", () => {
  38. expect(inventorySummary({ inventory: { carrot: 3, hoe: 1 }, gold: 0, stamina: 0, selectedSeed: null })).toBe(
  39. "carrot ×3 · hoe ×1",
  40. );
  41. });
  42. it("shows a placeholder for an empty inventory", () => {
  43. expect(inventorySummary({ inventory: {}, gold: 0, stamina: 0, selectedSeed: null })).toBe("items: —");
  44. });
  45. });
  46. describe("seedListFromSnapshot", () => {
  47. const world = [
  48. { id: "player", type: "player", inventory: { "carrot-seed": 2, carrot: 3, hoe: 1 } },
  49. ];
  50. const items: Array<{ id: string; name: string; kind: string }> = [
  51. { id: "carrot-seed", name: "Carrot Seeds", kind: "seed" },
  52. { id: "carrot", name: "Carrot", kind: "material" },
  53. { id: "hoe", name: "Hoe", kind: "tool" },
  54. ];
  55. it("keeps only seed-kind items when an item catalog is given", () => {
  56. expect(seedListFromSnapshot(world, items as never)).toEqual([{ id: "carrot-seed", name: "Carrot Seeds", qty: 2 }]);
  57. });
  58. it("offers every inventory item without a catalog (ws parity)", () => {
  59. expect(seedListFromSnapshot(world)).toEqual([
  60. { id: "carrot-seed", name: "carrot-seed", qty: 2 },
  61. { id: "carrot", name: "carrot", qty: 3 },
  62. { id: "hoe", name: "hoe", qty: 1 },
  63. ]);
  64. });
  65. it("returns an empty list for an empty inventory", () => {
  66. expect(seedListFromSnapshot([])).toEqual([]);
  67. });
  68. });
  69. describe("shopRowsFromDef", () => {
  70. it("maps buys/sells with the shop-viewpoint convention and names from the catalog", () => {
  71. const rows = shopRowsFromDef(
  72. {
  73. id: "general-store",
  74. name: "General Store",
  75. buys: [{ itemId: "carrot-seed", price: 20 }],
  76. sells: [{ itemId: "carrot", price: 15, stock: 5 }],
  77. } as never,
  78. { carrot: 2 },
  79. [
  80. { id: "carrot-seed", name: "Carrot Seeds", kind: "seed" },
  81. { id: "carrot", name: "Carrot", kind: "material" },
  82. ] as never,
  83. );
  84. expect(rows.shopId).toBe("general-store");
  85. expect(rows.buyRows).toEqual([{ itemId: "carrot-seed", name: "Carrot Seeds", price: 20, stock: null, owned: 0 }]);
  86. expect(rows.sellRows).toEqual([{ itemId: "carrot", name: "Carrot", price: 15, stock: 5, owned: 2 }]);
  87. });
  88. it("falls back to the item id without a catalog and handles empty lines", () => {
  89. const rows = shopRowsFromDef({ id: "s", buys: [], sells: [{ itemId: "egg", price: 40 }] } as never, undefined);
  90. expect(rows.buyRows).toEqual([]);
  91. expect(rows.sellRows).toEqual([{ itemId: "egg", name: "egg", price: 40, stock: null, owned: 0 }]);
  92. });
  93. });
  94. describe("dialogueChoicesFromNode", () => {
  95. it("maps text and choice labels", () => {
  96. const view = dialogueChoicesFromNode({
  97. dialogueId: "mayor",
  98. nodeId: "n1",
  99. text: "Hello!",
  100. choices: [{ text: "Hi", to: "n2" }, { text: "Bye", to: "n3" }],
  101. });
  102. expect(view.text).toBe("Hello!");
  103. expect(view.choices).toEqual(["Hi", "Bye"]);
  104. });
  105. it("returns empty for an absent dialogue state", () => {
  106. expect(dialogueChoicesFromNode(undefined)).toEqual({ text: "", choices: [] });
  107. });
  108. });
  109. describe("buildingShopAt", () => {
  110. const world = [
  111. { id: "building:shed:2:3", type: "building", x: 2, y: 3, w: 2, h: 2, shop: "general-store" },
  112. { id: "building:barn:0:0", type: "building", x: 0, y: 0, w: 1, h: 1 },
  113. ];
  114. it("finds the shop whose footprint contains the tile", () => {
  115. expect(buildingShopAt(world, { x: 3, y: 4 })).toEqual({ buildingId: "building:shed:2:3", shopId: "general-store" });
  116. });
  117. it("misses outside the footprint, on shopless buildings and on empty world", () => {
  118. expect(buildingShopAt(world, { x: 4, y: 4 })).toBeUndefined();
  119. expect(buildingShopAt(world, { x: 0, y: 0 })).toBeUndefined();
  120. expect(buildingShopAt([], { x: 0, y: 0 })).toBeUndefined();
  121. });
  122. });
  123. describe("dialogueActiveInWorld", () => {
  124. it("detects the dialogue:active sentinel entity", () => {
  125. expect(dialogueActiveInWorld([{ id: "dialogue:active", type: "dialogue" }])).toBe(true);
  126. expect(dialogueActiveInWorld([{ id: "player", type: "player" }])).toBe(false);
  127. expect(dialogueActiveInWorld([])).toBe(false);
  128. });
  129. });