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