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

1 import type { Placement } from "../../../../content/src/domains/crafting/schema";
2 import type { Area, IntentResult, Player, SimRules, TilePosition, WorldState } from "../../types";
3 import { inToolRange, tileIndex, withPlayer } from "../../state";
4 import type { IntentContext } from "../registry";
5 import { addItem, addSharedChest, countItem, getInventory, getInventoryWorld, removeSharedChest, takeSlot } from "../inventory";
6 import { getDebris, toolAvailable } from "../tools";
7 import { placeFishingPot } from "../fishing";
8 import { getCrafting, getCraftingArea, getCraftingContent, getPlayerCrafting, withCrafting, withCraftingArea, type CraftingTree, type PlacedObject } from "./model";
9 import { machineDefinition, startPassiveJob } from "./machines";
10 
11 export function footprint(object: Pick<PlacedObject, "x" | "y" | "width" | "height">): TilePosition[] {
12   const positions: TilePosition[] = [];
13   for (let y = object.y; y < object.y + object.height; y++) for (let x = object.x; x < object.x + object.width; x++) positions.push({ x, y });
14   return positions;
15 }
16 export function inCraftedReach(player: Player, object: Pick<PlacedObject, "x" | "y" | "width" | "height">): boolean {
17   return inToolRange(player, { x: Math.max(object.x, Math.min(player.x, object.x + object.width - 1)), y: Math.max(object.y, Math.min(player.y, object.y + object.height - 1)) });
18 }
19 export function placementDefinition(rules: SimRules, itemId: string): Placement | undefined { return getCraftingContent(rules)?.placements.find((entry) => entry.itemId === itemId); }
20 export function craftingObject(state: WorldState, areaId: string, id: string, rules: SimRules): PlacedObject | undefined {
21   const area = state.areas[areaId], object = area && getCraftingArea(area).objects.find((entry) => entry.id === id);
22   if (object) return object;
23   const chest = getInventoryWorld(state).chests.find((entry) => entry.id === id && entry.areaId === areaId);
24   const itemId = chest?.itemId ?? getCraftingContent(rules)?.placements.find((entry) => entry.kind === "chest")?.itemId;
25   return chest && itemId ? { id: chest.id, itemId, x: chest.x, y: chest.y, width: 1, height: 1, machine: null } : undefined;
26 }
27 export function canCook(state: WorldState, player: Player, rules: SimRules): boolean {
28   const data = getCraftingContent(rules), area = state.areas[player.areaId];
29   if (!data || !area) return false;
30   if (getCraftingArea(area).objects.some((object) => placementDefinition(rules, object.itemId)?.kind === "cookout" && inToolRange(player, object))) return true;
31   if (getPlayerCrafting(player).kitchenUpgradeLevel < data.settings.kitchen.requiredUpgradeLevel) return false;
32   return area.landmarks.some((landmark) => landmark.kind === data.settings.kitchen.landmarkKind && inToolRange(player, {
33     x: Math.max(landmark.x, Math.min(player.x, landmark.x + landmark.width - 1)),
34     y: Math.max(landmark.y, Math.min(player.y, landmark.y + landmark.height - 1)),
35   }));
36 }
37 /** A typed adapter for future tree producers; existing tools debris trees are recognized too. */
38 export function registerCraftingTree(state: WorldState, areaId: string, tree: CraftingTree | { id: string; remove: true }): WorldState {
39   const area = state.areas[areaId]; if (!area) return state;
40   const saved = getCraftingArea(area), trees = saved.trees.filter((entry) => entry.id !== tree.id);
41   if (!("remove" in tree)) {
42     if (tileIndex(area, tree.x, tree.y) === null) return state;
43     trees.push({ ...tree });
44   }
45   return withCraftingArea(state, areaId, { ...saved, trees });
46 }
47 export function treeAt(area: Area, tile: TilePosition, rules: SimRules): CraftingTree | undefined {
48   const known = getCraftingArea(area).trees.find((tree) => tree.x === tile.x && tree.y === tile.y && tree.mature);
49   if (known) return known;
50   const kinds = new Set(getCraftingContent(rules)?.machines.filter((machine) => machine.kind === "tapper").flatMap((machine) => machine.recipes.flatMap((recipe) => recipe.treeKinds ?? [])));
51   const debris = getDebris(area).find((entry) => entry.x === tile.x && entry.y === tile.y && kinds.has(entry.kind));
52   return debris ? { id: debris.id, kind: debris.kind, x: debris.x, y: debris.y, mature: true } : undefined;
53 }
54 function failed(state: WorldState, error: string): IntentResult { return { state, events: [], error }; }
55 function occupiedByObject(area: Area, tile: TilePosition): boolean {
56   return getCraftingArea(area).objects.some((object) => tile.x >= object.x && tile.x < object.x + object.width && tile.y >= object.y && tile.y < object.y + object.height);
57 }
58 function onWater(area: Area, index: number, rules: SimRules): boolean {
59   const water = rules.content?.tools as { waterTileIds?: number[] } | undefined;
60   return area.tiles[index]?.ground === "water" || !!water?.waterTileIds?.includes(area.groundTiles[index] ?? 0);
61 }
62 export function placeCrafted(context: IntentContext, slot: number, tile: TilePosition): IntentResult {
63   const { state, player, rules } = context, stack = getInventory(player, rules).slots[slot], definition = stack && placementDefinition(rules, stack.id), area = state.areas[player.areaId];
64   if (!definition || !area) return failed(state, "Select a placeable item");
65   if (definition.kind === "crabPot" && rules.content?.fishing) return placeFishingPot(context, tile, stack!.id);
66   if (!inToolRange(player, tile)) return failed(state, "Tile is out of reach");
67   const tree = definition.requiresTree ? treeAt(area, tile, rules) : undefined;
68   if (definition.requiresTree && !tree) return failed(state, "A mature compatible tree is required");
69   for (const position of footprint({ ...tile, ...definition })) {
70     const index = tileIndex(area, position.x, position.y), ground = index === null ? undefined : area.tiles[index];
71     if (index === null || !ground || occupiedByObject(area, position) || ground.crop) return failed(state, "Object cannot be placed here");
72     if (definition.kind === "crabPot") { if (!onWater(area, index, rules)) return failed(state, "A crab pot needs water"); }
73     else if (!tree && (area.collision[index] || ground.objectId)) return failed(state, "Object cannot be placed here");
74     if (tree && (position.x !== tree.x || position.y !== tree.y || ground.objectId && ground.objectId !== tree.id)) return failed(state, "Tree attachment is unavailable");
75     if (area.warps.some((warp) => position.x >= warp.x && position.x < warp.x + warp.width && position.y >= warp.y && position.y < warp.y + warp.height)
76       || position.x === Math.floor(area.spawn.x) && position.y === Math.floor(area.spawn.y)
77       || Object.values(state.players).some((entry) => entry.connected && entry.areaId === area.id && Math.floor(entry.x) === position.x && Math.floor(entry.y) === position.y)) return failed(state, "Object cannot obstruct a player, spawn, or exit");
78   }
79   const taken = takeSlot(player, slot, 1, rules); if (!taken) return failed(state, "Item is not in inventory");
80   const world = getCrafting(state);
81   let next = state, id = `crafted_${world.nextObjectId}`;
82   if (definition.kind === "chest") { const added = addSharedChest(next, { areaId: area.id, ...tile }, definition.itemId); next = added.state; id = added.chest.id; }
83   let object: PlacedObject = { id, itemId: definition.itemId, ...tile, width: definition.width, height: definition.height, machine: null, ...(tree ? { tree } : {}) };
84   object = startPassiveJob(next, object, rules);
85   const saved = getCraftingArea(next.areas[area.id]!);
86   next = withCraftingArea(next, area.id, { ...saved, objects: [...saved.objects, object] });
87   const updated = next.areas[area.id]!, tiles = updated.tiles.slice();
88   if (!tree) for (const position of footprint(object)) { const index = tileIndex(updated, position.x, position.y)!; tiles[index] = { ...tiles[index]!, objectId: id, walkableObject: !definition.collision }; }
89   next = { ...next, areas: { ...next.areas, [area.id]: { ...updated, tiles } } };
90   next = withCrafting(next, { ...world, nextObjectId: world.nextObjectId + 1 });
91   return { state: withPlayer(next, taken.player), events: [{ type: "craftedPlaced", playerId: player.id, areaId: area.id, data: { objectId: id, itemId: definition.itemId } }] };
92 }
93 export function pickupCrafted(context: IntentContext, objectId: string, tool: "axe" | "pickaxe"): IntentResult {
94   const { state, player, rules } = context, area = state.areas[player.areaId], object = craftingObject(state, player.areaId, objectId, rules), definition = object && placementDefinition(rules, object.itemId);
95   if (!area || !object || !definition || !inCraftedReach(player, object)) return failed(state, "Object is out of reach");
96   if (definition.pickupTool !== tool || countItem(player, `tool_${tool}`, rules) < 1 || !toolAvailable(player, tool, rules)) return failed(state, "Carry the available matching pickup tool");
97   const sourceExists = !object.tree || treeAt(area, object, rules)?.id === object.tree.id;
98   if (object.machine && sourceExists && (machineDefinition(object, rules)?.kind !== "tapper" || object.machine.readyAt <= state.clock.machineMinutes + getCrafting(state).offlineMachineMinutes)) return failed(state, "Collect the machine before picking it up");
99   const added = addItem(player, { id: object.itemId, quality: "regular", quantity: 1 }, rules);
100   if (added.remaining) return failed(state, "Inventory is full");
101   let next = state;
102   if (definition.kind === "chest") { const removed = removeSharedChest(next, object.id); if (!removed) return failed(state, "Empty the chest before picking it up"); next = removed; }
103   const saved = getCraftingArea(next.areas[area.id]!);
104   next = withCraftingArea(next, area.id, { ...saved, objects: saved.objects.filter((entry) => entry.id !== object.id) });
105   const updated = next.areas[area.id]!, tiles = updated.tiles.map((tile) => {
106     if (tile.objectId !== object.id) return tile;
107     const { walkableObject: _, ...rest } = tile;
108     return { ...rest, objectId: null };
109   });
110   next = { ...next, areas: { ...next.areas, [area.id]: { ...updated, tiles } } };
111   return { state: withPlayer(next, added.player), events: [{ type: "craftedPickedUp", playerId: player.id, areaId: area.id, data: { objectId } }] };
112 }
113