Exact source used by this build. SHA-256: 50b96f0ab74c670fdb7695acc6d5f764adbca81c8e5fdde9001540de074a8059.

1 import { z } from "zod";
2 import type { IntentResult } from "../../types";
3 import { withPlayer } from "../../state";
4 import { defineIntent, defineSystem, type IntentContext } from "../registry";
5 import { TilePositionSchema } from "../schema";
6 import { addItem } from "../inventory";
7 import { debitMoney } from "../shipping/money";
8 import { shopAccess } from "../shops";
9 import { craftingPlayerDefaults, craftingWorldDefaults, getCrafting, getCraftingContent, getPlayerCrafting, hydrateCraftingPlayer, learnRecipe, withCrafting, withPlayerCrafting } from "./model";
10 import { consumeMaterials, recipeAvailability, recipeIngredients } from "./materials";
11 import { pickupCrafted, placeCrafted } from "./placement";
12 import { announceReadyMachines, collectMachine, loadMachine, offlineMachineElapsed, reconcileTappers } from "./machines";
13 import { eat, fertilize, placeBomb, runMorningCrafting, tickBombs, tickBuffs } from "./effects";
14 
15 export * from "./model";
16 export * from "./materials";
17 export * from "./placement";
18 export * from "./machines";
19 export * from "./effects";
20 
21 function makeRecipe(context: IntentContext, recipeId: string, quantity: number, kind: "crafting" | "cooking"): IntentResult {
22   const { state, player, rules } = context, recipe = getCraftingContent(rules)?.recipes.find((entry) => entry.id === recipeId);
23   if (!recipe || recipe.kind !== kind) return { state, events: [], error: "Unknown recipe" };
24   const availability = recipeAvailability(state, player.id, rules, recipeId, quantity);
25   if (!availability.available) return { state, events: [], error: availability.reason ?? "Recipe cannot be made" };
26   const supplied = consumeMaterials(state, player, rules, recipeIngredients(player, recipe, rules), quantity)!;
27   const added = addItem(supplied.player, { id: recipe.output.itemId, quantity: recipe.output.quantity * quantity, quality: "regular" }, rules);
28   if (added.remaining) return { state, events: [], error: "Inventory is full" };
29   const saved = getPlayerCrafting(added.player), history = kind === "crafting" ? "crafted" : "cooked";
30   const updated = withPlayerCrafting(added.player, { ...saved, learnedRecipes: saved.learnedRecipes.includes(recipe.id) ? saved.learnedRecipes : [...saved.learnedRecipes, recipe.id], [history]: { ...saved[history], [recipe.id]: (saved[history][recipe.id] ?? 0) + quantity } });
31   return { state: withPlayer(supplied.state, updated), events: [{ type: history, playerId: player.id, areaId: player.areaId, data: { recipeId, itemId: recipe.output.itemId, quantity: recipe.output.quantity * quantity } }] };
32 }
33 const slot = z.number().int().min(0).max(35), objectId = z.string().min(1), quantity = z.number().int().min(1).max(99).optional();
34 export const craftingSystem = defineSystem({
35   id: "crafting", worldDefaults: () => ({ ...craftingWorldDefaults() }), playerDefaults: () => ({ ...craftingPlayerDefaults() }),
36   events: ["crafted", "cooked", "recipeLearned", "craftedPlaced", "craftedPickedUp", "machineLoaded", "machineReady", "machineCollected", "foodEaten", "fertilizerApplied", "bombPlaced", "bombExploded", "cropsEaten", "sprinklersWatered"],
37   intents: [
38     defineIntent("craft", z.object({ type: z.literal("craft"), recipeId: z.string().min(1), quantity }), (context, intent) => makeRecipe(context, intent.recipeId, intent.quantity ?? 1, "crafting")),
39     defineIntent("cook", z.object({ type: z.literal("cook"), recipeId: z.string().min(1), quantity }), (context, intent) => makeRecipe(context, intent.recipeId, intent.quantity ?? 1, "cooking")),
40     defineIntent("purchaseRecipe", z.object({ type: z.literal("purchaseRecipe"), recipeId: z.string().min(1) }), ({ state, player, rules }, intent) => {
41       const recipe = getCraftingContent(rules)?.recipes.find((entry) => entry.id === intent.recipeId);
42       if (!recipe || recipe.unlock.kind !== "purchase") return { state, events: [], error: "Recipe is not sold" };
43       if (getPlayerCrafting(player).learnedRecipes.includes(recipe.id)) return { state, events: [], error: "Recipe is already learned" };
44       const access = shopAccess(state, player, rules, recipe.unlock.shopId);
45       if ("error" in access) return { state, events: [], error: access.error };
46       const paid = debitMoney(state, player.id, recipe.unlock.price);
47       if (!paid) return { state, events: [], error: "Insufficient funds" };
48       return { state: withPlayer(paid, learnRecipe(player, recipe.id)), events: [{ type: "recipeLearned", playerId: player.id, data: { recipeId: recipe.id, price: recipe.unlock.price } }] };
49     }),
50     defineIntent("placeCrafted", z.object({ type: z.literal("placeCrafted"), slot, tile: TilePositionSchema }), (context, intent) => placeCrafted(context, intent.slot, intent.tile)),
51     defineIntent("pickupCrafted", z.object({ type: z.literal("pickupCrafted"), objectId, tool: z.enum(["axe", "pickaxe"]) }), (context, intent) => pickupCrafted(context, intent.objectId, intent.tool)),
52     defineIntent("loadMachine", z.object({ type: z.literal("loadMachine"), objectId, slot: slot.optional(), processingRecipeId: z.string().min(1).optional() }), (context, intent) => loadMachine(context, intent.objectId, intent.slot, intent.processingRecipeId)),
53     defineIntent("collectMachine", z.object({ type: z.literal("collectMachine"), objectId }), (context, intent) => collectMachine(context, intent.objectId)),
54     defineIntent("eat", z.object({ type: z.literal("eat"), slot }), (context, intent) => eat(context, intent.slot)),
55     defineIntent("fertilize", z.object({ type: z.literal("fertilize"), slot, tile: TilePositionSchema }), (context, intent) => fertilize(context, intent.slot, intent.tile)),
56     defineIntent("placeBomb", z.object({ type: z.literal("placeBomb"), slot, tile: TilePositionSchema }), (context, intent) => placeBomb(context, intent.slot, intent.tile)),
57   ],
58   onClockTick: ({ state, rules, elapsedMs }) => {
59     const bombs = tickBombs(tickBuffs(state, elapsedMs ?? 0), rules, elapsedMs ?? 0), machines = announceReadyMachines(reconcileTappers(bombs.state, rules));
60     return { state: machines.state, events: [...bombs.events, ...machines.events] };
61   },
62   onOfflineElapsed: ({ state, rules, elapsedMs }) => announceReadyMachines(offlineMachineElapsed(reconcileTappers(state, rules), rules, elapsedMs ?? 0)),
63   onOvernight: ({ state, rules }) => {
64     const morning = runMorningCrafting(state, rules), machines = announceReadyMachines(reconcileTappers(morning.state, rules));
65     for (const player of Object.values(machines.state.players)) { const hydrated = hydrateCraftingPlayer(player, rules); if (hydrated !== player) machines.state = withPlayer(machines.state, hydrated); }
66     return { state: machines.state, events: [...morning.events, ...machines.events] };
67   },
68   onPlayerAdded: ({ state, rules, playerId }) => {
69     const player = playerId && state.players[playerId], hydrated = player && hydrateCraftingPlayer(player, rules);
70     return { state: player && hydrated && hydrated !== player ? withPlayer(state, hydrated) : state, events: [] };
71   },
72   onPresenceChange: ({ state, rules, connected, playerId }) => {
73     if (connected && getCrafting(state).offlinePhaseMinutes !== null) state = withCrafting(state, { ...getCrafting(state), offlinePhaseMinutes: null });
74     const player = playerId && state.players[playerId], hydrated = connected && player && hydrateCraftingPlayer(player, rules);
75     return { state: player && hydrated && hydrated !== player ? withPlayer(state, hydrated) : state, events: [] };
76   },
77 });
78