Exact source used by this build. SHA-256: d6e53b4d63585e341b31490c04c0f1f78a1593a38f2fb813d8954f29e21f6da5.

1 import json from "../../../data/crafting/crafting.json";
2 import { ITEMS, validateItemReferences, type Item } from "../items";
3 import { SHOPS } from "../shops";
4 import { TOOLS } from "../tools";
5 import type { ContentDomain } from "../types";
6 import { CraftingSchema, type CraftingContent, type Ingredient } from "./schema";
7 
8 export * from "./schema";
9 export const CRAFTING = CraftingSchema.parse(json);
10 function unique(values: readonly string[], label: string): void {
11   if (new Set(values).size !== values.length) throw new Error(`Duplicate ${label}`);
12 }
13 function matchingItems(ingredient: Ingredient, items: readonly Item[]): Item[] {
14   return items.filter((item) => ingredient.itemId ? item.id === ingredient.itemId : ingredient.tag ? item.tags.includes(ingredient.tag) : item.category === ingredient.category);
15 }
16 /** Explicit external selectors are typed integration slots, never invented item IDs. */
17 export function validateCraftingReferences(value: unknown, items: readonly Item[] = ITEMS): CraftingContent {
18   const data = CraftingSchema.parse(value);
19   unique(data.recipes.map((recipe) => recipe.id), "recipe id");
20   unique(data.placements.map((placement) => placement.itemId), "placement item");
21   unique(data.machines.map((machine) => machine.id), "machine id");
22   unique(data.machines.flatMap((machine) => machine.recipes.map((recipe) => recipe.id)), "processing recipe id");
23   unique(data.consumables.map((food) => food.itemId), "consumable item");
24   unique(data.visuals.map((visual) => visual.itemId), "visual item");
25   unique(data.visuals.flatMap((visual) => [visual.icon, ...(visual.world ? [visual.world] : [])]), "visual manifest key");
26   const validateIngredient = (ingredient: Ingredient): void => {
27     if (ingredient.itemId) validateItemReferences([ingredient.itemId], items);
28     if (ingredient.tag && !matchingItems(ingredient, items).length && !data.externalTags.some((tag) => tag === ingredient.tag)) throw new Error(`Missing ingredient tag: ${ingredient.tag}`);
29     if (ingredient.category && !matchingItems(ingredient, items).length && !data.externalCategories.some((category) => category === ingredient.category)) throw new Error(`Missing ingredient category: ${ingredient.category}`);
30   };
31   const outputs = data.recipes.map((recipe) => recipe.output.itemId);
32   for (const recipe of data.recipes) {
33     recipe.ingredients.forEach(validateIngredient);
34     const unlock = recipe.unlock;
35     if (unlock.kind === "purchase" && !SHOPS.some((shop) => shop.id === unlock.shopId)) throw new Error(`Missing recipe shop: ${unlock.shopId}`);
36     if (recipe.kind === "cooking" && !data.consumables.some((food) => food.itemId === recipe.output.itemId)) throw new Error(`Cooking output has no consumable: ${recipe.id}`);
37   }
38   for (const machine of data.machines) for (const recipe of machine.recipes) {
39     if (recipe.input) validateIngredient(recipe.input);
40     recipe.fuel.forEach(validateIngredient);
41     outputs.push(recipe.output.itemId);
42     if (recipe.treeKinds?.some((kind) => !TOOLS.debris.some((tree) => tree.id === kind))) throw new Error(`Missing tapper tree: ${recipe.id}`);
43     if (machine.kind === "tapper" && (!recipe.treeKinds?.length || recipe.input !== null)) throw new Error(`Tapper recipe requires a tree and no inserted input: ${recipe.id}`);
44     if (machine.kind !== "tapper" && !recipe.input) throw new Error(`Processing recipe requires input: ${recipe.id}`);
45     if (recipe.priceFormula) {
46       const input = recipe.input && matchingItems(recipe.input, items);
47       const output = items.find((item) => item.id === recipe.output.itemId);
48       if (!input?.length || !output || input.some((item) => Math.floor(item.baseSellPrice * recipe.priceFormula!.multiplier + recipe.priceFormula!.add) !== output.baseSellPrice)) throw new Error(`Processing price formula mismatch: ${recipe.id}`);
49     }
50   }
51   validateItemReferences([...outputs, ...data.placements.map((p) => p.itemId), ...data.consumables.map((food) => food.itemId), ...data.fertilizers.map((item) => item.itemId), ...data.bombs.map((item) => item.itemId), ...data.hooks.flatMap((item) => [item.itemId, ...(item.targetItemId ? [item.targetItemId] : [])]), ...data.visuals.map((visual) => visual.itemId)], items);
52   for (const placement of data.placements) {
53     if (placement.kind === "machine" && !data.machines.some((machine) => machine.id === placement.machineId)) throw new Error(`Missing placement machine: ${placement.itemId}`);
54     if (placement.kind === "sprinkler" && !placement.sprinklerOffsets?.length) throw new Error(`Sprinkler has no coverage: ${placement.itemId}`);
55     if (placement.kind === "scarecrow" && !placement.protectionOffsets?.length) throw new Error(`Scarecrow has no coverage: ${placement.itemId}`);
56     for (const offsets of [placement.sprinklerOffsets, placement.protectionOffsets]) if (offsets) {
57       unique(offsets.map((offset) => `${offset.x},${offset.y}`), "coverage offset");
58       if (offsets.some((offset) => offset.x === 0 && offset.y === 0)) throw new Error(`Coverage includes occupied tile: ${placement.itemId}`);
59     }
60     if (!data.visuals.some((visual) => visual.itemId === placement.itemId && visual.world === placement.visualKey)) throw new Error(`Missing world visual: ${placement.itemId}`);
61   }
62   for (const food of data.consumables) {
63     const item = items.find((item) => item.id === food.itemId)!;
64     if (!item.edibility || item.edibility.energy !== food.energy || item.edibility.health !== food.health) throw new Error(`Consumable restoration mismatch: ${food.itemId}`);
65   }
66   for (const recipe of data.recipes) if (!data.visuals.some((visual) => visual.itemId === recipe.output.itemId && visual.icon === recipe.visualKey)) throw new Error(`Missing recipe visual: ${recipe.id}`);
67   return data;
68 }
69 export const craftingDomain: ContentDomain = { id: "crafting", data: CRAFTING, validate() { validateCraftingReferences(json); } };
70 craftingDomain.validate();
71