index.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. const FISH_STATS_ID = "mod:fish-stats";
  2. const FISH_STAMINA_COST = 5;
  3. const FISH_MIN_STAMINA = 5;
  4. export default {
  5. manifest: { id: "sample-game-mod", version: "0.1.0" },
  6. hooks: {
  7. onDayStart(ctx) {
  8. const { world, time, emit } = ctx;
  9. const { day, season } = time.get();
  10. let stats = world.get("mod:sample-stats");
  11. if (!stats) {
  12. stats = world.create({ id: "mod:sample-stats", type: "mod-state", greetings: 0 });
  13. }
  14. stats.greetings += 1;
  15. let mayor = world.get("npc:mayor");
  16. if (!mayor) {
  17. mayor = world.create({
  18. id: "npc:mayor",
  19. type: "npc",
  20. x: 6,
  21. y: 3,
  22. name: "Mayor",
  23. label: "Mayor",
  24. dialogue: "mayor",
  25. });
  26. }
  27. const routineState = world.get("sm:mayor-routine")?.state ?? "sleeping";
  28. const walkX = routineState === "market-day" ? 12 : 6;
  29. const walkY = 3 + (day % 4);
  30. mayor.x = walkX;
  31. mayor.y = walkY;
  32. // Mia the merchant arrives once the mayor-talk-quest is complete — the
  33. // restoration arc's second NPC (content/dialogues/mia.yaml). Gated so she
  34. // never appears before the starter arc's day-2 npc.list assertion.
  35. const player = world.get("player");
  36. const miaUnlocked = player?.questProgress?.["mayor-talk-quest"] === "complete";
  37. let mia = world.get("npc:mia");
  38. if (miaUnlocked) {
  39. const miaX = 14 + (day % 3);
  40. const miaY = 11;
  41. if (!mia) {
  42. mia = world.create({
  43. id: "npc:mia",
  44. type: "npc",
  45. x: miaX,
  46. y: miaY,
  47. name: "Mia",
  48. label: "Mia",
  49. dialogue: "mia",
  50. });
  51. } else {
  52. mia.x = miaX;
  53. mia.y = miaY;
  54. }
  55. }
  56. const weather = world.get("weather")?.weatherId ?? "unknown";
  57. emit("mod:sample-day-start", {
  58. modId: "sample-game-mod",
  59. day,
  60. season,
  61. weather,
  62. greetings: stats.greetings,
  63. mayor: { x: walkX, y: walkY },
  64. });
  65. },
  66. onPlayerIntent(ctx) {
  67. const intent = ctx.payload;
  68. if (!intent || typeof intent !== "object") return;
  69. if (intent.action !== "interact" || intent.ok !== true) return;
  70. const tile = intent.tile;
  71. if (!tile || typeof tile.x !== "number" || typeof tile.y !== "number") return;
  72. if (!isWaterTile(ctx, tile.x, tile.y)) return;
  73. const player = ctx.world.get("player");
  74. if (!player) return;
  75. const stamina = typeof player.stamina === "number" ? player.stamina : 100;
  76. const day = ctx.time.get().day;
  77. if (stamina < FISH_MIN_STAMINA) {
  78. ctx.emit("mod:fish-too-tired", { x: tile.x, y: tile.y, stamina, day });
  79. return;
  80. }
  81. const inventory = isRecord(player.inventory) ? player.inventory : {};
  82. player.inventory = inventory;
  83. const total = (inventory.fish ?? 0) + 1;
  84. inventory.fish = total;
  85. player.stamina = stamina - FISH_STAMINA_COST;
  86. const skills = isRecord(player.skills) ? player.skills : {};
  87. player.skills = skills;
  88. skills.fishing = (skills.fishing ?? 0) + 1;
  89. const stats = fishStats(ctx);
  90. stats.catches += 1;
  91. ctx.emit("item-acquire", { itemId: "fish", qty: 1, total });
  92. ctx.emit("mod:fish-caught", {
  93. fish: "fish",
  94. size: 1 + ((stats.catches - 1) % 3),
  95. x: tile.x,
  96. y: tile.y,
  97. stamina: player.stamina,
  98. catches: stats.catches,
  99. day,
  100. });
  101. ctx.world.remove(`plot:${tile.x}:${tile.y}`);
  102. },
  103. },
  104. };
  105. function isWaterTile(ctx, x, y) {
  106. const map = ctx.world.query({ type: "map" })[0];
  107. if (!map) return false;
  108. const layers = Array.isArray(map.layers) ? map.layers : [];
  109. const layer = layers.find((candidate) => candidate.name === "ground") ?? layers[0];
  110. if (!layer || !Array.isArray(layer.data)) return false;
  111. return layer.data[y]?.[x] === "water";
  112. }
  113. function fishStats(ctx) {
  114. let stats = ctx.world.get(FISH_STATS_ID);
  115. if (!stats) {
  116. stats = ctx.world.create({ id: FISH_STATS_ID, type: "mod-state", catches: 0 });
  117. }
  118. return stats;
  119. }
  120. function isRecord(value) {
  121. return typeof value === "object" && value !== null && !Array.isArray(value);
  122. }