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