| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- const FISH_STATS_ID = "mod:fish-stats";
- const FISH_STAMINA_COST = 5;
- const FISH_MIN_STAMINA = 5;
- export default {
- manifest: { id: "sample-game-mod", version: "0.1.0" },
- hooks: {
- onDayStart(ctx) {
- const { world, time, emit } = ctx;
- const { day, season } = time.get();
- let stats = world.get("mod:sample-stats");
- if (!stats) {
- stats = world.create({ id: "mod:sample-stats", type: "mod-state", greetings: 0 });
- }
- stats.greetings += 1;
- let mayor = world.get("npc:mayor");
- if (!mayor) {
- mayor = world.create({
- id: "npc:mayor",
- type: "npc",
- x: 6,
- y: 3,
- name: "Mayor",
- label: "Mayor",
- dialogue: "mayor",
- });
- }
- const routineState = world.get("sm:mayor-routine")?.state ?? "sleeping";
- const walkX = routineState === "market-day" ? 12 : 6;
- const walkY = 3 + (day % 4);
- mayor.x = walkX;
- mayor.y = walkY;
- // Mia the merchant arrives once the mayor-talk-quest is complete — the
- // restoration arc's second NPC (content/dialogues/mia.yaml). Gated so she
- // never appears before the starter arc's day-2 npc.list assertion.
- const player = world.get("player");
- const miaUnlocked = player?.questProgress?.["mayor-talk-quest"] === "complete";
- let mia = world.get("npc:mia");
- if (miaUnlocked) {
- const miaX = 14 + (day % 3);
- const miaY = 11;
- if (!mia) {
- mia = world.create({
- id: "npc:mia",
- type: "npc",
- x: miaX,
- y: miaY,
- name: "Mia",
- label: "Mia",
- dialogue: "mia",
- });
- } else {
- mia.x = miaX;
- mia.y = miaY;
- }
- }
- const weather = world.get("weather")?.weatherId ?? "unknown";
- emit("mod:sample-day-start", {
- modId: "sample-game-mod",
- day,
- season,
- weather,
- greetings: stats.greetings,
- mayor: { x: walkX, y: walkY },
- });
- },
- onPlayerIntent(ctx) {
- const intent = ctx.payload;
- if (!intent || typeof intent !== "object") return;
- if (intent.action !== "interact" || intent.ok !== true) return;
- const tile = intent.tile;
- if (!tile || typeof tile.x !== "number" || typeof tile.y !== "number") return;
- if (!isWaterTile(ctx, tile.x, tile.y)) return;
- const player = ctx.world.get("player");
- if (!player) return;
- const stamina = typeof player.stamina === "number" ? player.stamina : 100;
- const day = ctx.time.get().day;
- if (stamina < FISH_MIN_STAMINA) {
- ctx.emit("mod:fish-too-tired", { x: tile.x, y: tile.y, stamina, day });
- return;
- }
- const inventory = isRecord(player.inventory) ? player.inventory : {};
- player.inventory = inventory;
- const total = (inventory.fish ?? 0) + 1;
- inventory.fish = total;
- player.stamina = stamina - FISH_STAMINA_COST;
- const skills = isRecord(player.skills) ? player.skills : {};
- player.skills = skills;
- skills.fishing = (skills.fishing ?? 0) + 1;
- const stats = fishStats(ctx);
- stats.catches += 1;
- ctx.emit("item-acquire", { itemId: "fish", qty: 1, total });
- ctx.emit("mod:fish-caught", {
- fish: "fish",
- size: 1 + ((stats.catches - 1) % 3),
- x: tile.x,
- y: tile.y,
- stamina: player.stamina,
- catches: stats.catches,
- day,
- });
- ctx.world.remove(`plot:${tile.x}:${tile.y}`);
- },
- },
- };
- function isWaterTile(ctx, x, y) {
- const map = ctx.world.query({ type: "map" })[0];
- if (!map) return false;
- const layers = Array.isArray(map.layers) ? map.layers : [];
- const layer = layers.find((candidate) => candidate.name === "ground") ?? layers[0];
- if (!layer || !Array.isArray(layer.data)) return false;
- return layer.data[y]?.[x] === "water";
- }
- function fishStats(ctx) {
- let stats = ctx.world.get(FISH_STATS_ID);
- if (!stats) {
- stats = ctx.world.create({ id: FISH_STATS_ID, type: "mod-state", catches: 0 });
- }
- return stats;
- }
- function isRecord(value) {
- return typeof value === "object" && value !== null && !Array.isArray(value);
- }
|