Exact source used by this build. SHA-256: 92336bf9e175f6130a93b318dc74d3bf69a5ced3935fad2550ebd5979c7ca7c3.
1 import type { Area, IntentResult, Player, SimRules, TilePosition, WorldState } from "../../types";
2 import { inToolRange, tileIndex } from "../../state";
3 import { getInventoryWorld, removeItem } from "../inventory";
4 import { shopAccess, getShops } from "../shops";
5 import { shippingBins } from "../shipping";
6 import { debitMoney } from "../shipping/money";
7 import { animalDefaults, buildingDoor, getAnimals, getAnimalsContent, type AnimalsState, type Building, type BuildingDefinition, type InteriorFeatures } from "./types";
8
9 export function failure(state: WorldState, error: string): IntentResult { return { state, events: [], error }; }
10 export function saveAnimals(state: WorldState, value: AnimalsState): WorldState { return { ...state, revision: state.revision + 1, systems: { ...state.systems, animals: value } }; }
11 export function animalResult(state: WorldState, value: AnimalsState, type = "animalsChanged", playerId?: string): IntentResult { return { state: saveAnimals(state, value), events: [{ type, ...(playerId ? { playerId } : {}) }] }; }
12 export function updatePlayer(state: WorldState, player: Player): WorldState { return { ...state, players: { ...state.players, [player.id]: player } }; }
13 export function definitionFor(building: Building, rules: SimRules): BuildingDefinition | undefined { return getAnimalsContent(rules)?.buildings.find((entry) => entry.id === building.definitionId); }
14 export function buildingAccessible(state: WorldState, player: Player, building: Building, rules: SimRules): boolean {
15 const def = definitionFor(building, rules);
16 return player.areaId === building.interiorAreaId || !!def && player.areaId === building.areaId && inToolRange(player, buildingDoor(building, def));
17 }
18 export function siloCapacity(value: AnimalsState, rules: SimRules): number { return Object.values(value.buildings).reduce((sum, building) => sum + (!building.construction ? definitionFor(building, rules)?.siloCapacity ?? 0 : 0), 0); }
19 export interface PlacementTile extends TilePosition { valid: boolean; reason?: string }
20 export interface PlacementPreview { valid: boolean; tiles: PlacementTile[]; door: PlacementTile | null }
21 function inside(tile: TilePosition, rect: { x: number; y: number; width: number; height: number }): boolean { return tile.x >= rect.x && tile.x < rect.x + rect.width && tile.y >= rect.y && tile.y < rect.y + rect.height; }
22 export function buildingPlacementPreview(state: WorldState, rules: SimRules, definitionId: string, tile: TilePosition, movingBuildingId?: string): PlacementPreview {
23 const content = getAnimalsContent(rules), def = content?.buildings.find((entry) => entry.id === definitionId), area = content && state.areas[content.farmAreaId];
24 if (!def || !area) return { valid: false, tiles: [], door: null };
25 const placement = area.pois?.find((poi) => poi.id === content!.buildingPlacementPoiId);
26 const saved = getAnimals(state), own = movingBuildingId && saved.buildings[movingBuildingId];
27 const ownDef = own ? definitionFor(own, rules) : undefined;
28 const validate = (position: TilePosition): PlacementTile => {
29 const index = tileIndex(area, position.x, position.y), value = index === null ? undefined : area.tiles[index];
30 let reason: string | undefined;
31 const oldFootprint = !!own && !!ownDef && inside(position, { ...own, width: ownDef.width, height: ownDef.height });
32 if (index === null || !value) reason = "Outside farm";
33 else if (!placement || !inside(position, placement)) reason = "Outside building placement area";
34 else if (!oldFootprint && (area.collision[index] || value.objectId || value.crop || value.tilled)) reason = "Occupied tile";
35 else if (Object.values(saved.buildings).some((building) => { const definition = definitionFor(building, rules); const door = definition && buildingDoor(building, definition); return building.id !== movingBuildingId && building.areaId === area.id && door?.x === position.x && door?.y === position.y; })) reason = "Building doorway";
36 else if (area.warps.some((warp) => inside(position, warp)) || area.landmarks.some((landmark) => inside(position, landmark)) || area.pois?.some((poi) => poi.id !== placement.id && inside(position, poi)) || Math.floor(area.spawn.x) === position.x && Math.floor(area.spawn.y) === position.y) reason = "Reserved tile";
37 else if ([...getShops(rules), ...shippingBins(rules)].some((object) => object.areaId === area.id && Math.floor(object.x) === position.x && Math.floor(object.y) === position.y)) reason = "Service tile";
38 else if (Math.floor(content!.pets.bowl.x) === position.x && Math.floor(content!.pets.bowl.y) === position.y) reason = "Pet bowl";
39 else if (Object.values(state.players).some((player) => player.areaId === area.id && Math.floor(player.x) === position.x && Math.floor(player.y) === position.y)) reason = "Player present";
40 else if (Object.values(saved.animals).some((animal) => animal.areaId === area.id && Math.floor(animal.x) === position.x && Math.floor(animal.y) === position.y)) reason = "Animal present";
41 else if (saved.horse?.areaId === area.id && Math.floor(saved.horse.x) === position.x && Math.floor(saved.horse.y) === position.y) reason = "Horse present";
42 else if (saved.pet?.areaId === area.id && Math.floor(saved.pet.x) === position.x && Math.floor(saved.pet.y) === position.y) reason = "Pet present";
43 else if (Object.values(saved.products).some((product) => product.areaId === area.id && product.x === position.x && product.y === position.y) || getInventoryWorld(state).drops.some((drop) => drop.areaId === area.id && Math.floor(drop.x) === position.x && Math.floor(drop.y) === position.y)) reason = "Item present";
44 else if (saved.grass[`${position.x},${position.y}`]) reason = "Grass present";
45 return { ...position, valid: !reason, ...(reason ? { reason } : {}) };
46 };
47 const tiles: PlacementTile[] = [];
48 for (let y = tile.y; y < tile.y + def.height; y++) for (let x = tile.x; x < tile.x + def.width; x++) tiles.push(validate({ x, y }));
49 const door = validate({ x: tile.x + Math.floor(def.width / 2), y: tile.y + def.height });
50 return { valid: tiles.every((entry) => entry.valid) && door.valid, tiles, door };
51 }
52 export function createAnimalInterior(building: Building, def: BuildingDefinition): Area {
53 const interior = def.interior!, width = interior.width, height = interior.height, exit = { x: Math.floor(width / 2), y: height - 1 };
54 const features: InteriorFeatures = {
55 buildingId: building.id, troughs: Array.from({ length: interior.troughs }, (_, at) => ({ x: 1 + at % (width - 2), y: 1 + Math.floor(at / (width - 2)) })),
56 hayHopper: interior.hayHopper ? { x: 1, y: 3 } : null, heaterSlot: interior.heaterSlot ? { x: width - 2, y: 3 } : null,
57 incubator: interior.incubator ? { x: width - 2, y: 4 } : null, autoGrabberSlot: interior.autoGrabberSlot ? { x: 1, y: 4 } : null,
58 exit, manifestKey: `${def.id}.interior`,
59 };
60 const fixtures = [...features.troughs, features.hayHopper, features.heaterSlot, features.incubator, features.autoGrabberSlot].filter((entry): entry is TilePosition => !!entry);
61 const length = width * height;
62 const collision = Array.from({ length }, (_, at) => {
63 const x = at % width, y = Math.floor(at / width);
64 return (x === 0 || x === width - 1 || y === 0 || y === height - 1 || fixtures.some((position) => position.x === x && position.y === y)) && !(x === exit.x && y === exit.y);
65 });
66 const door = buildingDoor(building, def);
67 return {
68 id: building.interiorAreaId, name: "TBD", width, height, clockRateMsPerGameMinute: 700, outdoor: false, locked: false,
69 spawn: { x: exit.x, y: exit.y - 1 }, landmarks: [], collision, groundTiles: Array<number>(length).fill(1), decorTiles: Array<number>(length).fill(0),
70 tiles: Array.from({ length }, () => ({ ground: "floor", tillable: false, tilled: false, watered: false, objectId: null, crop: null })),
71 warps: [{ id: `${building.id}_exit`, ...exit, width: 1, height: 1, targetAreaId: building.areaId, targetX: door.x, targetY: door.y + 0.25, locked: false, label: "TBD", kind: "door" }],
72 systems: { animals: features },
73 };
74 }
75 export function stampBuilding(state: WorldState, building: Building, def: BuildingDefinition, remove = false): WorldState {
76 const area = state.areas[building.areaId]; if (!area) return state;
77 const tiles = area.tiles.slice(), collision = area.collision.slice();
78 for (let y = building.y; y < building.y + def.height; y++) for (let x = building.x; x < building.x + def.width; x++) {
79 const at = tileIndex(area, x, y); if (at === null || !tiles[at]) continue;
80 if (remove && tiles[at]!.objectId !== building.id) continue;
81 tiles[at] = { ...tiles[at]!, objectId: remove ? null : building.id }; collision[at] = !remove;
82 }
83 return { ...state, areas: { ...state.areas, [area.id]: { ...area, tiles, collision } } };
84 }
85 export function carpenterError(state: WorldState, player: Player, rules: SimRules, shopId: string): string | undefined {
86 const data = getAnimalsContent(rules); if (!data || shopId !== data.carpenterShopId) return "Choose the carpenter shop";
87 const access = shopAccess(state, player, rules, shopId); return "error" in access ? access.error : undefined;
88 }
89 export function payForBuilding(state: WorldState, player: Player, rules: SimRules, def: BuildingDefinition): WorldState | null {
90 const paid = debitMoney(state, player.id, def.cost); if (!paid) return null;
91 let supplied = player;
92 for (const material of def.materials) { const next = removeItem(supplied, material.itemId, material.quantity, rules); if (!next) return null; supplied = next; }
93 return updatePlayer(paid, supplied);
94 }
95 export function buildingHasOccupants(state: WorldState, building: Building): boolean { return Object.values(state.players).some((player) => player.areaId === building.interiorAreaId); }
96 export function buildingHasContents(state: WorldState, building: Building): boolean {
97 const saved = getAnimals(state), inventory = getInventoryWorld(state);
98 return buildingHasOccupants(state, building) || Object.values(saved.animals).some((animal) => animal.buildingId === building.id)
99 || Object.values(saved.products).some((product) => product.buildingId === building.id) || building.storedProducts.length > 0 || !!building.incubation
100 || building.heater || building.autoGrabber || building.trough > 0 || inventory.chests.some((chest) => chest.areaId === building.interiorAreaId)
101 || inventory.drops.some((drop) => drop.areaId === building.interiorAreaId) || !!state.areas[building.interiorAreaId]?.tiles.some((tile) => tile.objectId || tile.crop);
102 }
103 /** Initializes only valid grass locations. New systems never replace existing map objects. */
104 export function initializeAnimals(state: WorldState, rules: SimRules): WorldState {
105 const saved = getAnimals(state), data = getAnimalsContent(rules); if (!data || saved.initialized) return state;
106 const farm = state.areas[data.farmAreaId], grass = { ...saved.grass };
107 if (farm) for (const patch of data.grass.initialPatches) {
108 const at = tileIndex(farm, patch.x, patch.y), tile = at === null ? undefined : farm.tiles[at];
109 if (at !== null && tile && !farm.collision[at] && !tile.objectId && !tile.crop && !tile.tilled) grass[`${patch.x},${patch.y}`] = { ...patch, manifestKey: data.grass.manifestKey };
110 }
111 return saveAnimals(state, { ...animalDefaults(), ...saved, grass, initialized: true });
112 }
113