index.mjs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  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. const weather = world.get("weather")?.weatherId ?? "unknown";
  33. emit("mod:sample-day-start", {
  34. modId: "sample-game-mod",
  35. day,
  36. season,
  37. weather,
  38. greetings: stats.greetings,
  39. mayor: { x: walkX, y: walkY },
  40. });
  41. },
  42. onPlayerIntent(ctx) {
  43. const intent = ctx.payload;
  44. if (!intent || typeof intent !== "object") return;
  45. if (intent.action !== "interact" || intent.ok !== true) return;
  46. const tile = intent.tile;
  47. if (!tile || typeof tile.x !== "number" || typeof tile.y !== "number") return;
  48. if (!isWaterTile(ctx, tile.x, tile.y)) return;
  49. const player = ctx.world.get("player");
  50. if (!player) return;
  51. const stamina = typeof player.stamina === "number" ? player.stamina : 100;
  52. const day = ctx.time.get().day;
  53. if (stamina < FISH_MIN_STAMINA) {
  54. ctx.emit("mod:fish-too-tired", { x: tile.x, y: tile.y, stamina, day });
  55. return;
  56. }
  57. const inventory = isRecord(player.inventory) ? player.inventory : {};
  58. player.inventory = inventory;
  59. const total = (inventory.fish ?? 0) + 1;
  60. inventory.fish = total;
  61. player.stamina = stamina - FISH_STAMINA_COST;
  62. const skills = isRecord(player.skills) ? player.skills : {};
  63. player.skills = skills;
  64. skills.fishing = (skills.fishing ?? 0) + 1;
  65. const stats = fishStats(ctx);
  66. stats.catches += 1;
  67. ctx.emit("item-acquire", { itemId: "fish", qty: 1, total });
  68. ctx.emit("mod:fish-caught", {
  69. fish: "fish",
  70. size: 1 + ((stats.catches - 1) % 3),
  71. x: tile.x,
  72. y: tile.y,
  73. stamina: player.stamina,
  74. catches: stats.catches,
  75. day,
  76. });
  77. ctx.world.remove(`plot:${tile.x}:${tile.y}`);
  78. },
  79. },
  80. };
  81. function isWaterTile(ctx, x, y) {
  82. const map = ctx.world.query({ type: "map" })[0];
  83. if (!map) return false;
  84. const layers = Array.isArray(map.layers) ? map.layers : [];
  85. const layer = layers.find((candidate) => candidate.name === "ground") ?? layers[0];
  86. if (!layer || !Array.isArray(layer.data)) return false;
  87. return layer.data[y]?.[x] === "water";
  88. }
  89. function fishStats(ctx) {
  90. let stats = ctx.world.get(FISH_STATS_ID);
  91. if (!stats) {
  92. stats = ctx.world.create({ id: FISH_STATS_ID, type: "mod-state", catches: 0 });
  93. }
  94. return stats;
  95. }
  96. function isRecord(value) {
  97. return typeof value === "object" && value !== null && !Array.isArray(value);
  98. }