Exact source used by this build. SHA-256: 2d95055cd072a224a0b79bad0a57980b24ca6d115a501c55aed8f919d51b7a43.
1 import json from "../../../data/animals/animals.json";
2 import { ITEMS, validateItemReferences } from "../items";
3 import { AREA_MAPS, isMapBlocked } from "../maps";
4 import { validateShopReferences } from "../shops";
5 import type { ContentDomain } from "../types";
6 import { AnimalsSchema } from "./schema";
7 import type { AnimalsContent } from "./schema";
8
9 export * from "./schema";
10 /** Services follow authored map targets when layouts change. */
11 export function loadAnimalsContent(value: unknown): AnimalsContent {
12 const data = AnimalsSchema.parse(value);
13 return { ...data, shops: data.shops.map((shop) => {
14 const poi = AREA_MAPS[shop.areaId]?.pois.find((entry) => entry.id === shop.poiId);
15 if (!poi) throw new Error(`Missing animal shop POI: ${shop.poiId}`);
16 if (isMapBlocked(AREA_MAPS[shop.areaId]!, poi.x, poi.y)) throw new Error(`Animal shop POI is blocked: ${shop.poiId}`);
17 return { ...shop, x: poi.x, y: poi.y };
18 }) };
19 }
20 export const ANIMALS = loadAnimalsContent(json);
21
22 function unique(ids: readonly string[], label: string): void {
23 if (new Set(ids).size !== ids.length) throw new Error(`Duplicate ${label}`);
24 }
25
26 /** Every produced, consumed, purchased and illustrated item belongs to the shared catalog. */
27 export function validateAnimalsReferences(value: unknown, items: readonly { id: string }[] = ITEMS): AnimalsContent {
28 const data = loadAnimalsContent(value);
29 unique(data.buildings.map((entry) => entry.id), "building id");
30 unique(data.species.map((entry) => entry.id), "animal species id");
31 unique(data.species.map((entry) => entry.role), "animal role");
32 unique(data.tools.map((entry) => entry.itemId), "animal tool item");
33 unique(data.pets.choices.map((entry) => entry.id), "pet choice");
34 const itemIds = [data.hayItemId, data.grassStarterItemId, data.heaterItemId, data.autoGrabberItemId,
35 ...data.buildings.flatMap((entry) => entry.materials.map((material) => material.itemId)),
36 ...data.species.flatMap((entry) => [entry.productItemId, entry.largeProductItemId, entry.bonusProductItemId, entry.toolItemId].filter((id): id is string => id !== null)),
37 ...data.tools.map((entry) => entry.itemId), ...Object.keys(data.itemVisuals)];
38 validateItemReferences(itemIds, items);
39 validateShopReferences(data.shops, items.map((item) => item.id));
40 for (const id of [data.carpenterShopId, data.ranchShopId]) if (!data.shops.some((shop) => shop.id === id)) throw new Error(`Missing animal shop ${id}`);
41 if (data.carpenterShopId === data.ranchShopId) throw new Error("Animal shops must have different ids");
42 for (const building of data.buildings) {
43 unique(building.materials.map((entry) => entry.itemId), `material for ${building.id}`);
44 if (building.kind !== building.id) throw new Error(`Building ${building.id} must match its kind`);
45 const housing = building.family !== "utility";
46 if (building.capacity !== building.interior.troughs) throw new Error(`Building ${building.id} trough count must match capacity`);
47 if (housing && building.capacity !== building.tier * 4) throw new Error(`Building ${building.id} capacity must match its tier`);
48 if (!housing && building.capacity !== 0) throw new Error(`Utility ${building.id} cannot house livestock`);
49 if (building.interior.width < building.interior.troughs + 2) throw new Error(`Building ${building.id} troughs do not fit`);
50 if (building.autoFeed && (!housing || building.tier !== 3)) throw new Error(`Building ${building.id} cannot auto-feed`);
51 if (building.interior.incubator && (building.family !== "coop" || building.tier < 2)) throw new Error(`Building ${building.id} cannot have an incubator`);
52 if (building.upgradeFrom) {
53 const previous = data.buildings.find((entry) => entry.id === building.upgradeFrom);
54 if (!previous || previous.family !== building.family || previous.tier + 1 !== building.tier) throw new Error(`Invalid upgrade prerequisite for ${building.id}`);
55 if (previous.width !== building.width || previous.height !== building.height) throw new Error(`Upgrade ${building.id} must preserve its footprint`);
56 } else if (building.tier !== 1) throw new Error(`Building ${building.id} is missing its upgrade prerequisite`);
57 }
58 for (const animal of data.species) {
59 if (!data.buildings.some((building) => building.family === animal.buildingFamily && building.tier >= animal.minTier)) throw new Error(`Missing housing for ${animal.id}`);
60 if (animal.toolItemId && !data.tools.some((tool) => tool.itemId === animal.toolItemId)) throw new Error(`Missing harvest tool for ${animal.id}`);
61 if (animal.incubationMinutes !== null && animal.buildingFamily !== "coop") throw new Error(`Invalid incubator species ${animal.id}`);
62 if (animal.largeProductItemId && animal.largeProductItemId === animal.productItemId) throw new Error(`Large product must differ for ${animal.id}`);
63 }
64 const manifestKeys = [...data.buildings.map((entry) => entry.manifestKey), ...data.species.map((entry) => entry.manifestKey),
65 ...data.pets.choices.map((entry) => entry.manifestKey), data.pets.bowl.manifestKey, data.horse.manifestKey, data.grass.manifestKey,
66 ...data.tools.map((entry) => entry.manifestKey), ...Object.values(data.itemVisuals), ...data.shops.map((shop) => `${shop.id}.counter`)];
67 for (const key of manifestKeys) if (!data.visuals[key]) throw new Error(`Missing animal visual ${key}`);
68 const farm = AREA_MAPS[data.farmAreaId];
69 if (!farm || !farm.outdoor) throw new Error("Animals require an outdoor farm area");
70 const placement = farm.pois.find((poi) => poi.id === data.buildingPlacementPoiId);
71 if (!placement || placement.kind !== "building_placement" || placement.width < 1 || placement.height < 1) throw new Error("Missing farm building placement POI");
72 if (data.buildings.some((building) => building.width > placement.width || building.height + 1 > placement.height)) throw new Error("Building does not fit farm placement POI");
73 const positions = [data.pets.bowl, data.pets.start, ...data.grass.initialPatches];
74 for (const position of positions) {
75 if (position.x >= farm.width || position.y >= farm.height || isMapBlocked(farm, position.x, position.y)) throw new Error("Animal initial position must be on accessible farm tile");
76 if (farm.warps.some((warp) => position.x >= warp.x && position.x < warp.x + warp.width && position.y >= warp.y && position.y < warp.y + warp.height)) throw new Error("Animal initial position overlaps a warp");
77 }
78 unique(data.grass.initialPatches.map(({ x, y }) => `${x}:${y}`), "initial grass tile");
79 if (data.grass.initialPatches.some(({ x, y }) => !farm.tillableTiles[y * farm.width + x])) throw new Error("Initial animal grass must occupy growable farm soil");
80 const grass = data.grass;
81 if (grass.initialPatches.some((patch) => patch.tufts > grass.maxTufts) || grass.starterTufts > grass.maxTufts
82 || grass.growMin > grass.growMax || grass.extraGrowMin > grass.extraGrowMax || grass.spreadMin > grass.spreadMax
83 || grass.growMax > grass.maxTufts || grass.extraGrowMax > grass.maxTufts || grass.spreadMax > grass.maxTufts) throw new Error("Invalid grass growth limits");
84 if (data.care.initialFriendship > data.care.friendshipMax || data.care.initialMood > data.care.moodMax) throw new Error("Invalid initial animal care values");
85 if (data.care.returnMinute >= data.care.sleepMinute || data.care.qualityMoodThreshold > data.care.moodMax || data.care.productionMoodThreshold > data.care.qualityMoodThreshold) throw new Error("Invalid animal care thresholds");
86 return data;
87 }
88
89 export const animalsDomain: ContentDomain = { id: "animals", data: ANIMALS, validate() { validateAnimalsReferences(json); } };
90 animalsDomain.validate();
91