Exact source used by this build. SHA-256: 2025a647fa6202482bfb2297cb307743161533f8309b759a13a362552e9f0db6.
1 import { z } from "zod";
2 import type { IntentResult, Player, SimRules, TilePosition, WorldState } from "../../types";
3 import { inToolRange, tileIndex } from "../../state";
4 import { createRng } from "../../rng";
5 import { defineIntent, defineSystem, type IntentContext } from "../registry";
6 import { TilePositionSchema } from "../schema";
7 import { addItem, removeItem } from "../inventory";
8 import { farmingAnimalModifiers, grantXp, skillLevel } from "../skills";
9 import { calendarDay } from "../shipping";
10 import { creditMoney } from "../shipping/money";
11 import { shopAccess } from "../shops";
12 import { consumeToolWater, refillToolAtSource } from "../tools";
13 import { forageQuality } from "../forage";
14 import { hasProfessionHook } from "../skills";
15 import { absoluteDay, animalDefaults, animalSellPrice, buildingDoor, getAnimalPlayer, getAnimals, getAnimalsContent, getInterior, grassKey, type AnimalSpecies, type AnimalsContent, type AnimalsState, type Building, type FarmAnimal } from "./types";
16 import { animalResult, buildingAccessible, buildingHasContents, buildingHasOccupants, buildingPlacementPreview, carpenterError, definitionFor, failure, initializeAnimals, payForBuilding, saveAnimals, siloCapacity, stampBuilding, updatePlayer } from "./buildings";
17 import { makeAnimal, overnightAnimals, tickAnimals } from "./lifecycle";
18 export * from "./types";
19 export { buildingPlacementPreview, createAnimalInterior, initializeAnimals, siloCapacity, stampBuilding } from "./buildings";
20 export { overnightAnimals, tickAnimals } from "./lifecycle";
21
22 const Id = z.string().min(1), Name = z.string().trim().min(1).max(32), Quantity = z.number().int().min(1).max(999);
23 function animalReach(player: Player, animal: FarmAnimal): boolean { return player.areaId === animal.areaId && inToolRange(player, animal); }
24 function readyBuilding(state: WorldState, building: Building): boolean { return !!state.areas[building.interiorAreaId] && !state.areas[building.interiorAreaId]!.locked; }
25 function clearDestination(state: WorldState, areaId: string, tile: TilePosition): boolean {
26 const area = state.areas[areaId], at = area && tileIndex(area, Math.floor(tile.x), Math.floor(tile.y));
27 return !!area && !area.locked && at !== null && at !== undefined && !area.collision[at] && !area.tiles[at]?.objectId;
28 }
29 function animalChanged(state: WorldState, value: AnimalsState, animal: FarmAnimal, playerId: string): IntentResult { return animalResult(state, { ...value, animals: { ...value.animals, [animal.id]: animal } }, "animalsChanged", playerId); }
30 function speciesFor(data: AnimalsContent, animal: FarmAnimal): AnimalSpecies | undefined { return data.species.find((species) => species.id === animal.speciesId); }
31 function fixtureAccess(state: WorldState, player: Player, building: Building, kind: "hayHopper" | "heaterSlot" | "incubator" | "autoGrabberSlot"): boolean {
32 const area = state.areas[building.interiorAreaId], target = area && getInterior(area)?.[kind];
33 return !!target && player.areaId === building.interiorAreaId && inToolRange(player, target);
34 }
35 function insideOrDoor(state: WorldState, player: Player, building: Building, rules: SimRules): boolean { return readyBuilding(state, building) && buildingAccessible(state, player, building, rules); }
36 function fillBowl(context: IntentContext): IntentResult {
37 const { state, player, playerId, rules } = context, data = getAnimalsContent(rules), value = getAnimals(state);
38 if (!data || !value.pet) return failure(state, "Choose a pet first");
39 if (player.areaId !== data.farmAreaId || !inToolRange(player, data.pets.bowl)) return failure(state, "Pet bowl is out of reach");
40 if (value.pet.bowlWateredDay === calendarDay(state)) return failure(state, "Pet bowl is already full");
41 const watered = consumeToolWater(player, rules, 1); if (!watered) return failure(state, "A filled watering can is required");
42 return animalResult(updatePlayer(state, watered), { ...value, pet: { ...value.pet, bowlWateredDay: calendarDay(state) } }, "animalsChanged", playerId);
43 }
44 function useAnimalItemTool(context: IntentContext, use: { itemId: string; tile: TilePosition; targetId?: string }): IntentResult | undefined {
45 const { state, player, playerId, rules } = context, data = getAnimalsContent(rules); if (!data) return undefined;
46 const value = getAnimals(state);
47 if (use.itemId === "tool_wateringCan") {
48 if (player.areaId === data.farmAreaId && use.tile.x === data.pets.bowl.x && use.tile.y === data.pets.bowl.y) return fillBowl(context);
49 const well = Object.values(value.buildings).find((building) => definitionFor(building, rules)?.kind === "well" && readyBuilding(state, building) && player.areaId === building.areaId && inToolRange(player, buildingDoor(building, definitionFor(building, rules)!)) && (use.targetId === building.id || use.tile.x >= building.x && use.tile.x < building.x + definitionFor(building, rules)!.width && use.tile.y >= building.y && use.tile.y <= building.y + definitionFor(building, rules)!.height));
50 if (well) return animalResult(updatePlayer(state, refillToolAtSource(player, rules)), value, "animalsChanged", playerId);
51 return undefined;
52 }
53 if (use.itemId === "tool_scythe") {
54 const key = grassKey(use.tile), grass = value.grass[key];
55 if (player.areaId !== data.farmAreaId || !grass) return undefined;
56 const patches = { ...value.grass }; delete patches[key];
57 const rng = createRng(state.rngState), chance = state.clock.season === "winter" ? data.grass.winterHayChance : data.grass.hayChance;
58 let hay = value.hay;
59 for (let tuft = 0; tuft < grass.tufts; tuft++) if (rng.next() < chance && hay < siloCapacity(value, rules)) hay++;
60 return animalResult({ ...state, rngState: rng.state }, { ...value, grass: patches, hay }, "animalsChanged", playerId);
61 }
62 const tool = data.tools.find((entry) => entry.itemId === use.itemId); if (!tool) return undefined;
63 const animal = use.targetId ? value.animals[use.targetId] : Object.values(value.animals).find((entry) => entry.areaId === player.areaId && Math.floor(entry.x) === use.tile.x && Math.floor(entry.y) === use.tile.y);
64 if (!animal || !animalReach(player, animal) || Math.floor(animal.x) !== use.tile.x || Math.floor(animal.y) !== use.tile.y) return failure(state, "Animal is out of reach");
65 const species = speciesFor(data, animal);
66 if (!species || species.toolItemId !== use.itemId) return failure(state, "This tool does not match the animal");
67 if (animal.sleeping) return failure(state, "Animal is sleeping");
68 if (!animal.readyProduct) return failure(state, "Animal has no product ready");
69 if (player.energy < tool.energyCost) return failure(state, "Not enough energy");
70 const added = addItem(player, { id: animal.readyProduct.itemId, quality: animal.readyProduct.quality, quantity: 1 }, rules);
71 if (added.remaining) return failure(state, "Inventory is full");
72 const updated = grantXp({ ...added.player, energy: player.energy - tool.energyCost }, "farming", species.harvestXp, rules);
73 return animalChanged(updatePlayer(state, updated), value, { ...animal, readyProduct: null, lastProductDay: species.role === "wool" ? absoluteDay(state) : animal.lastProductDay, friendship: Math.min(data.care.friendshipMax, animal.friendship + data.care.harvestFriendship) }, playerId);
74 }
75 export function animalsMovementMultiplier(state: WorldState, player: Player, rules: SimRules): number {
76 const horse = getAnimals(state).horse;
77 return horse?.riderId === player.id && getAnimalPlayer(player).mounted ? getAnimalsContent(rules)?.horse.speedMultiplier ?? 1 : 1;
78 }
79
80 export const animalsSystem = defineSystem({
81 id: "animals", worldDefaults: () => ({ ...animalDefaults() }), playerDefaults: () => ({ mounted: false }),
82 events: ["animalsChanged", "animalsMorning", "areaChanged"],
83 intents: [
84 defineIntent("animalsBuild", z.object({ type: z.literal("animalsBuild"), definitionId: Id, tile: TilePositionSchema, shopId: Id }), ({ state: incoming, player, playerId, rules }, intent) => {
85 const error = carpenterError(incoming, player, rules, intent.shopId); if (error) return failure(incoming, error);
86 const state = initializeAnimals(incoming, rules), value = getAnimals(state), data = getAnimalsContent(rules)!, def = data.buildings.find((entry) => entry.id === intent.definitionId);
87 if (!def || def.upgradeFrom) return failure(incoming, "Choose a base building");
88 if (Object.values(value.buildings).some((building) => building.construction)) return failure(incoming, "Construction is already underway");
89 if (def.kind === "stable" && Object.values(value.buildings).some((building) => definitionFor(building, rules)?.kind === "stable")) return failure(incoming, "The farm already has a stable");
90 if (!buildingPlacementPreview(state, rules, def.id, intent.tile).valid) return failure(incoming, "Building footprint or doorway is blocked");
91 const paid = payForBuilding(state, player, rules, def); if (!paid) return failure(incoming, "Insufficient funds or building materials");
92 const id = `building_${value.nextId}`, building: Building = { id, definitionId: def.id, ...intent.tile, areaId: data.farmAreaId, interiorAreaId: `animals_${id}`, construction: { definitionId: def.id, morningsRemaining: def.buildDays }, doorOpen: false, trough: 0, heater: false, autoGrabber: false, storedProducts: [], incubation: null };
93 return animalResult(stampBuilding(paid, building, def), { ...value, nextId: value.nextId + 1, buildings: { ...value.buildings, [id]: building } }, "animalsChanged", playerId);
94 }),
95 defineIntent("animalsUpgrade", z.object({ type: z.literal("animalsUpgrade"), buildingId: Id, definitionId: Id, shopId: Id }), ({ state, player, playerId, rules }, intent) => {
96 const error = carpenterError(state, player, rules, intent.shopId); if (error) return failure(state, error);
97 const value = getAnimals(state), building = value.buildings[intent.buildingId], data = getAnimalsContent(rules)!, def = data.buildings.find((entry) => entry.id === intent.definitionId);
98 if (!building || !def || def.upgradeFrom !== building.definitionId || !readyBuilding(state, building)) return failure(state, "Building upgrade is unavailable");
99 if (Object.values(value.buildings).some((entry) => entry.construction)) return failure(state, "Construction is already underway");
100 if (buildingHasOccupants(state, building)) return failure(state, "Players must leave before upgrading");
101 const oldDef = definitionFor(building, rules)!;
102 if (oldDef.width !== def.width || oldDef.height !== def.height) return failure(state, "Upgrade footprint must match the existing building");
103 const paid = payForBuilding(state, player, rules, def); if (!paid) return failure(state, "Insufficient funds or building materials");
104 return animalResult(paid, { ...value, buildings: { ...value.buildings, [building.id]: { ...building, construction: { definitionId: def.id, morningsRemaining: def.buildDays } } } }, "animalsChanged", playerId);
105 }),
106 defineIntent("animalsMoveBuilding", z.object({ type: z.literal("animalsMoveBuilding"), buildingId: Id, tile: TilePositionSchema, shopId: Id }), ({ state, player, playerId, rules }, intent) => {
107 const error = carpenterError(state, player, rules, intent.shopId); if (error) return failure(state, error);
108 const value = getAnimals(state), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules);
109 if (!building || !def || building.construction) return failure(state, "Building cannot be moved during construction");
110 if (buildingHasOccupants(state, building) || value.horse?.buildingId === building.id && value.horse.riderId) return failure(state, "Building has an occupant");
111 if (!buildingPlacementPreview(state, rules, def.id, intent.tile, building.id).valid) return failure(state, "Building footprint or doorway is blocked");
112 const moved = { ...building, ...intent.tile }, next = stampBuilding(stampBuilding(state, building, def, true), moved, def), interior = next.areas[moved.interiorAreaId], door = buildingDoor(moved, def);
113 const updated = interior ? { ...next, areas: { ...next.areas, [interior.id]: { ...interior, warps: interior.warps.map((warp) => ({ ...warp, targetX: door.x, targetY: door.y + 0.25 })) } } } : next;
114 const horse = value.horse?.buildingId === moved.id ? { ...value.horse, ...door } : value.horse;
115 return animalResult(updated, { ...value, horse, buildings: { ...value.buildings, [moved.id]: moved } }, "animalsChanged", playerId);
116 }),
117 defineIntent("animalsDemolish", z.object({ type: z.literal("animalsDemolish"), buildingId: Id, shopId: Id }), ({ state, player, playerId, rules }, intent) => {
118 const error = carpenterError(state, player, rules, intent.shopId); if (error) return failure(state, error);
119 const value = getAnimals(state), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules);
120 if (!building || !def || building.construction) return failure(state, "Building cannot be demolished during construction");
121 if (buildingHasContents(state, building) || value.horse?.buildingId === building.id && value.horse.riderId) return failure(state, "Empty the building before demolition");
122 const buildings = { ...value.buildings }; delete buildings[building.id];
123 if (value.hay > siloCapacity({ ...value, buildings }, rules)) return failure(state, "Silo still contains hay");
124 const cleared = stampBuilding(state, building, def, true), areas = { ...cleared.areas };
125 delete areas[building.interiorAreaId];
126 return animalResult({ ...cleared, areas }, { ...value, buildings, horse: value.horse?.buildingId === building.id ? null : value.horse }, "animalsChanged", playerId);
127 }),
128 defineIntent("animalsDoor", z.object({ type: z.literal("animalsDoor"), buildingId: Id, open: z.boolean() }), ({ state, player, playerId, rules }, intent) => {
129 const value = getAnimals(state), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules);
130 if (!building || !def || !def.capacity || !insideOrDoor(state, player, building, rules)) return failure(state, "Animal door is out of reach");
131 return animalResult(state, { ...value, buildings: { ...value.buildings, [building.id]: { ...building, doorOpen: intent.open } } }, "animalsChanged", playerId);
132 }),
133 defineIntent("animalsEnter", z.object({ type: z.literal("animalsEnter"), buildingId: Id }), ({ state, player, playerId, rules }, intent) => {
134 const value = getAnimals(state), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules), area = building && state.areas[building.interiorAreaId];
135 if (!building || !def || !area || player.areaId !== building.areaId || !inToolRange(player, buildingDoor(building, def))) return failure(state, "Building door is out of reach or under construction");
136 if (getAnimalPlayer(player).mounted) return failure(state, "Dismount before entering");
137 if (!clearDestination(state, area.id, area.spawn)) return failure(state, "Interior entrance is blocked; clear it before entering");
138 return animalResult(updatePlayer(state, { ...player, areaId: area.id, ...area.spawn }), value, "areaChanged", playerId);
139 }),
140 defineIntent("animalsLeave", z.object({ type: z.literal("animalsLeave") }), ({ state, player, playerId, rules }) => {
141 const value = getAnimals(state), building = Object.values(value.buildings).find((entry) => entry.interiorAreaId === player.areaId), def = building && definitionFor(building, rules), area = state.areas[player.areaId], exit = area && getInterior(area)?.exit;
142 if (!building || !def || !exit || !inToolRange(player, exit)) return failure(state, "Exit is out of reach");
143 const door = buildingDoor(building, def);
144 if (!clearDestination(state, building.areaId, door)) return failure(state, "Exterior doorway is blocked; clear it before leaving");
145 return animalResult(updatePlayer(state, { ...player, areaId: building.areaId, ...door }), value, "areaChanged", playerId);
146 }),
147 defineIntent("animalsBuy", z.object({ type: z.literal("animalsBuy"), speciesId: Id, buildingId: Id, name: Name, shopId: Id }), ({ state, player, playerId, rules }, intent) => {
148 const value = getAnimals(state), data = getAnimalsContent(rules); if (!data || intent.shopId !== data.ranchShopId) return failure(state, "Choose the ranch shop");
149 const access = shopAccess(state, player, rules, intent.shopId); if ("error" in access) return failure(state, access.error);
150 const species = data.species.find((entry) => entry.id === intent.speciesId), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules);
151 if (!species || !building || !def || !readyBuilding(state, building) || species.buildingFamily !== def.family || species.minTier > def.tier) return failure(state, "Animal requires a suitable completed building");
152 if (Object.values(value.animals).filter((entry) => entry.buildingId === building.id).length >= def.capacity) return failure(state, "Building is full");
153 const paid = payForBuilding(state, player, rules, { ...def, cost: species.purchasePrice, materials: [] }); if (!paid) return failure(state, "Insufficient funds");
154 const animal = makeAnimal(state, value, data, species, building, playerId, intent.name);
155 return animalResult(paid, { ...value, nextId: value.nextId + 1, animals: { ...value.animals, [animal.id]: animal } }, "animalsChanged", playerId);
156 }),
157 defineIntent("animalsName", z.object({ type: z.literal("animalsName"), animalId: Id, name: Name }), ({ state, player, playerId }, intent) => {
158 const value = getAnimals(state), animal = value.animals[intent.animalId];
159 return animal && animalReach(player, animal) ? animalChanged(state, value, { ...animal, name: intent.name }, playerId) : failure(state, "Animal is out of reach");
160 }),
161 defineIntent("animalsSell", z.object({ type: z.literal("animalsSell"), animalId: Id }), ({ state, player, playerId, rules }, intent) => {
162 const value = getAnimals(state), animal = value.animals[intent.animalId], data = getAnimalsContent(rules), species = data && animal && speciesFor(data, animal);
163 if (!animal || !species || !animalReach(player, animal)) return failure(state, "Animal is out of reach");
164 const animals = { ...value.animals }; delete animals[animal.id];
165 return animalResult(creditMoney(state, playerId, animalSellPrice(species, animal.friendship)), { ...value, animals }, "animalsChanged", playerId);
166 }),
167 defineIntent("animalsPet", z.object({ type: z.literal("animalsPet"), animalId: Id }), ({ state, player, playerId, rules }, intent) => {
168 const value = getAnimals(state), animal = value.animals[intent.animalId], data = getAnimalsContent(rules), species = data && animal && speciesFor(data, animal);
169 if (!animal || !species || !data || !animalReach(player, animal)) return failure(state, "Animal is out of reach");
170 if (animal.sleeping || state.clock.minuteOfDay >= data.care.sleepMinute) return failure(state, "Animal is sleeping");
171 const day = calendarDay(state); if (animal.pettedDay === day) return failure(state, "Animal has already been petted today");
172 const modifiers = farmingAnimalModifiers(player, species.buildingFamily, rules);
173 const updated = grantXp(player, "farming", data.care.petXp, rules);
174 return animalChanged(updatePlayer(state, updated), value, { ...animal, pettedDay: day, friendship: Math.min(data.care.friendshipMax, animal.friendship + data.care.petFriendship * modifiers.friendshipMultiplier), mood: Math.min(data.care.moodMax, animal.mood + species.petMoodGain) }, playerId);
175 }),
176 defineIntent("animalsFeed", z.object({ type: z.literal("animalsFeed"), buildingId: Id, quantity: Quantity, source: z.enum(["inventory", "silo"]) }), ({ state, player, playerId, rules }, intent) => {
177 const value = getAnimals(state), data = getAnimalsContent(rules), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules);
178 if (!data || !building || !def?.interior?.troughs || !fixtureAccess(state, player, building, "hayHopper")) return failure(state, "Hay hopper is out of reach");
179 if (building.trough + intent.quantity > def.interior.troughs) return failure(state, "Trough is full");
180 if (intent.source === "silo" && value.hay < intent.quantity) return failure(state, "Silo has insufficient hay");
181 const supplied = intent.source === "inventory" ? removeItem(player, data.hayItemId, intent.quantity, rules) : player;
182 if (!supplied) return failure(state, "Hay is not in inventory");
183 return animalResult(updatePlayer(state, supplied), { ...value, hay: value.hay - (intent.source === "silo" ? intent.quantity : 0), buildings: { ...value.buildings, [building.id]: { ...building, trough: building.trough + intent.quantity } } }, "animalsChanged", playerId);
184 }),
185 defineIntent("animalsStoreHay", z.object({ type: z.literal("animalsStoreHay"), buildingId: Id, quantity: Quantity }), ({ state, player, playerId, rules }, intent) => {
186 const value = getAnimals(state), data = getAnimalsContent(rules), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules);
187 if (!data || !building || !def?.siloCapacity || !insideOrDoor(state, player, building, rules)) return failure(state, "Silo is out of reach");
188 if (value.hay + intent.quantity > siloCapacity(value, rules)) return failure(state, "Silo is full");
189 const supplied = removeItem(player, data.hayItemId, intent.quantity, rules); if (!supplied) return failure(state, "Hay is not in inventory");
190 return animalResult(updatePlayer(state, supplied), { ...value, hay: value.hay + intent.quantity }, "animalsChanged", playerId);
191 }),
192 defineIntent("animalsTakeHay", z.object({ type: z.literal("animalsTakeHay"), buildingId: Id, quantity: Quantity, source: z.enum(["silo", "trough"]) }), ({ state, player, playerId, rules }, intent) => {
193 const value = getAnimals(state), data = getAnimalsContent(rules), building = value.buildings[intent.buildingId];
194 if (!data || !building || !(fixtureAccess(state, player, building, "hayHopper") || definitionFor(building, rules)?.siloCapacity && insideOrDoor(state, player, building, rules))) return failure(state, "Hay is out of reach");
195 if ((intent.source === "silo" ? value.hay : building.trough) < intent.quantity) return failure(state, "Insufficient hay");
196 const added = addItem(player, { id: data.hayItemId, quality: "regular", quantity: intent.quantity }, rules); if (added.remaining) return failure(state, "Inventory is full");
197 return animalResult(updatePlayer(state, added.player), { ...value, hay: value.hay - (intent.source === "silo" ? intent.quantity : 0), buildings: { ...value.buildings, [building.id]: { ...building, trough: building.trough - (intent.source === "trough" ? intent.quantity : 0) } } }, "animalsChanged", playerId);
198 }),
199 defineIntent("animalsCollect", z.object({ type: z.literal("animalsCollect"), productId: Id }), ({ state, player, playerId, rules }, intent) => {
200 const value = getAnimals(state), product = value.products[intent.productId], data = getAnimalsContent(rules);
201 if (!product || !data || player.areaId !== product.areaId || !inToolRange(player, product)) return failure(state, "Product is out of reach");
202 const species = data.species.find((entry) => entry.id === value.animals[product.animalId]?.speciesId) ?? data.species.find((entry) => entry.productItemId === product.itemId || entry.largeProductItemId === product.itemId || entry.bonusProductItemId === product.itemId);
203 const rng = createRng(state.rngState), truffle = species?.outdoorOnly === true;
204 const quality = truffle ? hasProfessionHook(player, "foraging_05", rules) ? "iridium" : forageQuality(skillLevel(player, "foraging"), rng.next(), rng.next()) : product.quality;
205 const added = addItem(player, { id: product.itemId, quality, quantity: 1 }, rules); if (added.remaining) return failure(state, "Inventory is full");
206 const products = { ...value.products }; delete products[product.id];
207 return animalResult(updatePlayer({ ...state, rngState: rng.state }, grantXp(added.player, truffle ? "foraging" : "farming", species?.harvestXp ?? 0, rules)), { ...value, products }, "animalsChanged", playerId);
208 }),
209 defineIntent("animalsCollectStored", z.object({ type: z.literal("animalsCollectStored"), buildingId: Id }), ({ state, player, playerId, rules }, intent) => {
210 const value = getAnimals(state), building = value.buildings[intent.buildingId];
211 if (!building || !fixtureAccess(state, player, building, "autoGrabberSlot") || !building.autoGrabber) return failure(state, "Auto-grabber is out of reach");
212 let updated = player;
213 for (const product of building.storedProducts) { const added = addItem(updated, { id: product.itemId, quality: product.quality, quantity: 1 }, rules); if (added.remaining) return failure(state, "Inventory is full"); updated = added.player; }
214 if (!building.storedProducts.length) return failure(state, "Auto-grabber is empty");
215 return animalResult(updatePlayer(state, updated), { ...value, buildings: { ...value.buildings, [building.id]: { ...building, storedProducts: [] } } }, "animalsChanged", playerId);
216 }),
217 defineIntent("animalsEquipment", z.object({ type: z.literal("animalsEquipment"), buildingId: Id, itemId: Id }), ({ state, player, playerId, rules }, intent) => {
218 const value = getAnimals(state), data = getAnimalsContent(rules), building = value.buildings[intent.buildingId];
219 if (!data || !building) return failure(state, "Unknown building");
220 const heater = intent.itemId === data.heaterItemId, auto = intent.itemId === data.autoGrabberItemId;
221 if (!heater && !auto || !fixtureAccess(state, player, building, heater ? "heaterSlot" : "autoGrabberSlot")) return failure(state, "Equipment slot is out of reach");
222 const installed = heater ? building.heater : building.autoGrabber;
223 if (!installed && auto && skillLevel(player, "farming") < data.care.autoGrabberFarmingLevel) return failure(state, "Farming level is too low");
224 if (installed && auto && building.storedProducts.length) return failure(state, "Empty the auto-grabber first");
225 const added = installed ? addItem(player, { id: intent.itemId, quality: "regular", quantity: 1 }, rules) : null;
226 const supplied = installed ? added!.remaining ? null : added!.player : removeItem(player, intent.itemId, 1, rules);
227 if (!supplied) return failure(state, installed ? "Inventory is full" : "Equipment is not in inventory");
228 return animalResult(updatePlayer(state, supplied), { ...value, buildings: { ...value.buildings, [building.id]: { ...building, ...(heater ? { heater: !installed } : { autoGrabber: !installed }) } } }, "animalsChanged", playerId);
229 }),
230 defineIntent("animalsIncubate", z.object({ type: z.literal("animalsIncubate"), buildingId: Id, itemId: Id }), ({ state, player, playerId, rules }, intent) => {
231 const value = getAnimals(state), data = getAnimalsContent(rules), building = value.buildings[intent.buildingId], def = building && definitionFor(building, rules);
232 if (!data || !building || !def || !fixtureAccess(state, player, building, "incubator")) return failure(state, "Incubator is out of reach");
233 if (building.incubation) return failure(state, "Incubator is occupied");
234 const species = data.species.find((entry) => entry.incubationMinutes && (entry.productItemId === intent.itemId || entry.largeProductItemId === intent.itemId));
235 if (!species || species.buildingFamily !== def.family || species.minTier > def.tier) return failure(state, "Item cannot be incubated here");
236 const supplied = removeItem(player, intent.itemId, 1, rules); if (!supplied) return failure(state, "Egg is not in inventory");
237 const minutes = Math.ceil(species.incubationMinutes! * farmingAnimalModifiers(player, species.buildingFamily, rules).incubationMultiplier);
238 return animalResult(updatePlayer(state, supplied), { ...value, buildings: { ...value.buildings, [building.id]: { ...building, incubation: { speciesId: species.id, ownerId: playerId, morningsRemaining: Math.ceil(minutes / 1600), readyAtMachineMinute: state.clock.machineMinutes + minutes } } } }, "animalsChanged", playerId);
239 }),
240 defineIntent("animalsPlantGrass", z.object({ type: z.literal("animalsPlantGrass"), tile: TilePositionSchema }), ({ state, player, playerId, rules }, intent) => {
241 const value = getAnimals(state), data = getAnimalsContent(rules), area = data && state.areas[data.farmAreaId], index = area && tileIndex(area, intent.tile.x, intent.tile.y), tile = area && index !== undefined && index !== null ? area.tiles[index] : undefined;
242 if (!data || !area || index === null || index === undefined || !tile || player.areaId !== area.id || !inToolRange(player, intent.tile) || area.collision[index] || tile.objectId || tile.crop || tile.tilled || !tile.tillable || value.grass[grassKey(intent.tile)]) return failure(state, "Grass cannot be planted here");
243 const supplied = removeItem(player, data.grassStarterItemId, 1, rules); if (!supplied) return failure(state, "Grass starter is not in inventory");
244 return animalResult(updatePlayer(state, supplied), { ...value, grass: { ...value.grass, [grassKey(intent.tile)]: { ...intent.tile, tufts: data.grass.starterTufts, manifestKey: data.grass.manifestKey } } }, "animalsChanged", playerId);
245 }),
246 defineIntent("animalsChoosePet", z.object({ type: z.literal("animalsChoosePet"), kind: z.enum(["cat", "dog"]), name: Name }), ({ state, player, playerId, rules }, intent) => {
247 const value = getAnimals(state), data = getAnimalsContent(rules), choice = data?.pets.choices.find((entry) => entry.id === intent.kind);
248 if (!data || !choice || player.areaId !== data.farmAreaId) return failure(state, "Choose a pet on the farm");
249 if (value.pet) return failure(state, "The farm already has a pet");
250 return animalResult(state, { ...value, pet: { kind: intent.kind, name: intent.name, friendship: 0, pettedDay: "", bowlWateredDay: "", areaId: data.farmAreaId, ...data.pets.start, manifestKey: choice.manifestKey } }, "animalsChanged", playerId);
251 }),
252 defineIntent("animalsPetCompanion", z.object({ type: z.literal("animalsPetCompanion") }), ({ state, player, playerId, rules }) => {
253 const value = getAnimals(state), data = getAnimalsContent(rules), pet = value.pet;
254 if (!data || !pet || player.areaId !== pet.areaId || !inToolRange(player, pet)) return failure(state, "Pet is out of reach");
255 if (pet.pettedDay === calendarDay(state)) return failure(state, "Pet has already been petted today");
256 return animalResult(state, { ...value, pet: { ...pet, friendship: Math.min(data.pets.friendshipMax, pet.friendship + data.pets.petFriendship), pettedDay: calendarDay(state) } }, "animalsChanged", playerId);
257 }),
258 defineIntent("animalsFillPetBowl", z.object({ type: z.literal("animalsFillPetBowl") }), fillBowl),
259 defineIntent("animalsMount", z.object({ type: z.literal("animalsMount"), mounted: z.boolean() }), ({ state, player, playerId }, intent) => {
260 const value = getAnimals(state), horse = value.horse;
261 if (!horse) return failure(state, "Build a stable first");
262 if (intent.mounted && (horse.riderId || horse.areaId !== player.areaId || !inToolRange(player, horse) || !state.areas[player.areaId]?.outdoor)) return failure(state, "Horse is unavailable or out of reach");
263 if (!intent.mounted && horse.riderId !== playerId) return failure(state, "Player is not mounted");
264 return animalResult(updatePlayer(state, { ...player, systems: { ...player.systems, animals: { mounted: intent.mounted } } }), { ...value, horse: { ...horse, riderId: intent.mounted ? playerId : null, areaId: player.areaId, x: player.x, y: player.y } }, "animalsChanged", playerId);
265 }),
266 ],
267 onItemTool: useAnimalItemTool,
268 movementSpeedMultiplier: animalsMovementMultiplier,
269 onPlayerAdded: ({ state, rules }) => ({ state: initializeAnimals(state, rules), events: [] }),
270 onPresenceChange: ({ state, rules, playerId, connected }) => {
271 const updated = initializeAnimals(state, rules), value = getAnimals(updated);
272 if (connected || !playerId || value.horse?.riderId !== playerId) return { state: updated, events: [] };
273 const player = updated.players[playerId];
274 return { state: saveAnimals(player ? updatePlayer(updated, { ...player, systems: { ...player.systems, animals: { mounted: false } } }) : updated, { ...value, horse: { ...value.horse, riderId: null, ...(player ? { areaId: player.areaId, x: player.x, y: player.y } : {}) } }), events: [] };
275 },
276 onOvernight: ({ state, rules, weatherOverride }) => overnightAnimals(state, rules, weatherOverride),
277 onClockTick: ({ state, rules, elapsedMs }) => tickAnimals(state, rules, elapsedMs),
278 });
279