Exact source used by this build. SHA-256: 49f71b9c6c54df4eca9046917bcf2687e70c35c55eda2d50c132b2f48511eb40.

1 import type { Ingredient, Recipe } from "../../../../content/src/domains/crafting/schema";
2 import type { Player, SimRules, WorldState } from "../../types";
3 import { addItem, getInventory, getInventoryWorld, replaceChestSlots, takeSlot, type ItemStack } from "../inventory";
4 import { getItem } from "../items";
5 import { craftingItemHook, getCraftingContent, recipeKnown } from "./model";
6 import { crabPotIngredients } from "../fishing/integration";
7 import type { FishingContent } from "../fishing/model";
8 import { canCook } from "./placement";
9 
10 interface Supply { stack: ItemStack; slot: number; chestId?: string }
11 export function recipeIngredients(player: Player, recipe: Recipe, rules: SimRules): readonly Ingredient[] {
12   const hook = craftingItemHook(rules, recipe.output.itemId), fishing = rules.content?.fishing as FishingContent | undefined;
13   return hook?.kind === "crabPot" && hook.ownerSystem === "fishing" && fishing && hook.targetItemId === fishing.crabPot.itemId
14     ? crabPotIngredients(fishing, player, rules) : recipe.ingredients;
15 }
16 export function ingredientMatches(stack: ItemStack, ingredient: Ingredient, rules: SimRules): boolean {
17   const item = getItem(rules, stack.id);
18   return !!item && (ingredient.itemId !== undefined ? item.id === ingredient.itemId : ingredient.tag !== undefined ? item.tags.includes(ingredient.tag) : item.category === ingredient.category);
19 }
20 export function nearbyCraftingChests(state: WorldState, player: Player, rules: SimRules) {
21   const radius = getCraftingContent(rules)?.settings.nearbyChestRadius ?? 0;
22   return getInventoryWorld(state).chests.filter((chest) => chest.areaId === player.areaId && Math.hypot(player.x - chest.x, player.y - chest.y) <= radius).sort((a, b) => a.id.localeCompare(b.id));
23 }
24 function supplies(state: WorldState, player: Player, rules: SimRules, includeChests: boolean): Supply[] {
25   const result: Supply[] = [];
26   getInventory(player, rules).slots.forEach((stack, slot) => { if (stack) result.push({ stack, slot }); });
27   if (includeChests) for (const chest of nearbyCraftingChests(state, player, rules)) chest.slots.forEach((stack, slot) => { if (stack) result.push({ stack, slot, chestId: chest.id }); });
28   return result;
29 }
30 export function ingredientCount(state: WorldState, player: Player, rules: SimRules, ingredient: Ingredient, includeChests = true): number {
31   return supplies(state, player, rules, includeChests).reduce((total, supply) => total + (ingredientMatches(supply.stack, ingredient, rules) ? supply.stack.quantity : 0), 0);
32 }
33 /** Max-flow allocation prevents a broad tag consuming the only item needed by an exact selector. */
34 function allocate(stock: Supply[], ingredients: readonly Ingredient[], quantity: number, rules: SimRules): number[] | null {
35   const count = stock.length + ingredients.length + 2, sink = count - 1;
36   const capacities = Array.from({ length: count }, () => new Map<number, number>());
37   const add = (a: number, b: number, value: number) => { capacities[a]!.set(b, value); capacities[b]!.set(a, 0); };
38   stock.forEach((supply, index) => {
39     add(0, index + 1, supply.stack.quantity);
40     ingredients.forEach((ingredient, at) => { if (ingredientMatches(supply.stack, ingredient, rules)) add(index + 1, 1 + stock.length + at, supply.stack.quantity); });
41   });
42   let required = 0;
43   ingredients.forEach((ingredient, index) => { const amount = ingredient.quantity * quantity; required += amount; add(1 + stock.length + index, sink, amount); });
44   let flow = 0;
45   while (flow < required) {
46     const parents = Array<number>(count).fill(-1), queue = [0]; parents[0] = 0;
47     for (let at = 0; at < queue.length && parents[sink] === -1; at++) for (const [to, capacity] of capacities[queue[at]!]!) if (capacity > 0 && parents[to] === -1) { parents[to] = queue[at]!; queue.push(to); }
48     if (parents[sink] === -1) return null;
49     let amount = required - flow;
50     for (let node = sink; node !== 0; node = parents[node]!) amount = Math.min(amount, capacities[parents[node]!]!.get(node)!);
51     for (let node = sink; node !== 0; node = parents[node]!) { const from = parents[node]!; capacities[from]!.set(node, capacities[from]!.get(node)! - amount); capacities[node]!.set(from, capacities[node]!.get(from)! + amount); }
52     flow += amount;
53   }
54   return stock.map((_, index) => capacities[index + 1]!.get(0)!);
55 }
56 /** A speculative transaction: callers commit both returned values only after output insertion succeeds. */
57 export function consumeMaterials(state: WorldState, player: Player, rules: SimRules, ingredients: readonly Ingredient[], quantity = 1, includeChests = true): { state: WorldState; player: Player } | null {
58   const stock = supplies(state, player, rules, includeChests), allocation = allocate(stock, ingredients, quantity, rules);
59   if (!allocation) return null;
60   const changed = new Map<string, (ItemStack | null)[]>();
61   for (let index = 0; index < stock.length; index++) {
62     const amount = allocation[index]!, supply = stock[index]!; if (!amount) continue;
63     if (supply.chestId) {
64       const chest = getInventoryWorld(state).chests.find((entry) => entry.id === supply.chestId)!;
65       const slots = changed.get(chest.id) ?? chest.slots.slice();
66       slots[supply.slot] = amount === supply.stack.quantity ? null : { ...supply.stack, quantity: supply.stack.quantity - amount };
67       changed.set(chest.id, slots);
68     } else { const taken = takeSlot(player, supply.slot, amount, rules); if (!taken) return null; player = taken.player; }
69   }
70   for (const [id, slots] of changed) state = replaceChestSlots(state, id, slots);
71   return { state, player };
72 }
73 export interface RecipeAvailability { known: boolean; available: boolean; reason?: string; ingredients: { ingredient: Ingredient; available: number; required: number }[] }
74 export function recipeAvailability(state: WorldState, playerId: string, rules: SimRules, recipeId: string, quantity = 1): RecipeAvailability {
75   const player = state.players[playerId], recipe = getCraftingContent(rules)?.recipes.find((entry) => entry.id === recipeId);
76   if (!player || !recipe || !Number.isSafeInteger(quantity) || quantity < 1 || quantity > 99) return { known: false, available: false, reason: "Unknown recipe", ingredients: [] };
77   const needed = recipeIngredients(player, recipe, rules);
78   const known = recipeKnown(player, recipe), ingredients = needed.map((ingredient) => ({ ingredient, available: ingredientCount(state, player, rules, ingredient), required: ingredient.quantity * quantity }));
79   const base = { known, ingredients, available: false };
80   if (!known) return { ...base, reason: "Recipe is not learned" };
81   if (recipe.kind === "cooking" && !canCook(state, player, rules)) return { ...base, reason: "A kitchen or nearby cookout kit is required" };
82   const consumed = consumeMaterials(state, player, rules, needed, quantity);
83   if (!consumed) return { ...base, reason: "Missing ingredients" };
84   if (addItem(consumed.player, { id: recipe.output.itemId, quantity: recipe.output.quantity * quantity, quality: "regular" }, rules).remaining) return { ...base, reason: "Inventory is full" };
85   return { ...base, available: true };
86 }
87