Exact source used by this build. SHA-256: 85f7fd3cbb7105711257e3cc010dd66461f38fd1bdcd14480af81fdcab86cd44.
1 import { z } from "zod";
2 import type { Player, SimRules, WorldState } from "../../types";
3 import { inToolRange } from "../../state";
4 import { defineIntent, defineSystem } from "../registry";
5 import { getItem, sellPrice } from "../items";
6 import { takeSlot, type ItemStack } from "../inventory";
7 import { professionSaleMultiplier } from "../skills";
8 import { creditMoney } from "./money";
9 export * from "./money";
10
11 export interface ShippingBin { id: string; areaId: string; x: number; y: number; name: string; description: string }
12 export interface ShippingEntry extends ItemStack { playerId: string; unitPrice: number; total: number; firstShipment: boolean }
13 export interface ShippingSummary { day: string; entries: ShippingEntry[]; total: number; credited: number }
14 export interface ShippingState { pending: { playerId: string; stack: ItemStack }[]; shipped: Record<string, number> }
15 export interface PlayerShippingState { summary: ShippingSummary | null; shipped: Record<string, number> }
16 export function calendarDay(state: Pick<WorldState, "clock">): string { return `${state.clock.year}:${state.clock.season}:${state.clock.day}`; }
17 export function getShipping(state: WorldState): ShippingState { return state.systems.shipping as ShippingState ?? { pending: [], shipped: {} }; }
18 export function getPlayerShipping(player: Player): PlayerShippingState { return player.systems.shipping as PlayerShippingState ?? { summary: null, shipped: {} }; }
19 export function shippingBins(rules: SimRules): readonly ShippingBin[] { return rules.content?.shipping as ShippingBin[] ?? []; }
20
21 export const shippingSystem = defineSystem({
22 id: "shipping", worldDefaults: () => ({ pending: [], shipped: {} }), playerDefaults: () => ({ summary: null, shipped: {} }), events: ["itemShipped", "shippingPaid", "shippingAcknowledged"],
23 intents: [
24 defineIntent("shipItem", z.object({ type: z.literal("shipItem"), binId: z.string().min(1), slot: z.number().int().nonnegative(), quantity: z.number().int().min(1).max(999) }), ({ state, player, playerId, rules }, intent) => {
25 const bin = shippingBins(rules).find((entry) => entry.id === intent.binId);
26 if (!bin || bin.areaId !== player.areaId || !inToolRange(player, bin)) return { state, events: [], error: "Shipping bin is out of reach" };
27 const taken = takeSlot(player, intent.slot, intent.quantity, rules);
28 if (!taken) return { state, events: [], error: "Item is not in inventory" };
29 const item = getItem(rules, taken.stack.id);
30 if (!item || item.baseSellPrice <= 0 || item.category === "tool" || item.category === "equipment") return { state, events: [], error: "Item cannot be shipped" };
31 const shipping = getShipping(state);
32 return { state: { ...state, revision: state.revision + 1, players: { ...state.players, [playerId]: taken.player }, systems: { ...state.systems, shipping: { ...shipping, pending: [...shipping.pending, { playerId, stack: taken.stack }] } } }, events: [{ type: "itemShipped", playerId, areaId: player.areaId, data: { id: taken.stack.id, quantity: taken.stack.quantity } }] };
33 }),
34 defineIntent("acknowledgeShipping", z.object({ type: z.literal("acknowledgeShipping") }), ({ state, player, playerId }) => ({
35 state: { ...state, revision: state.revision + 1, players: { ...state.players, [playerId]: { ...player, systems: { ...player.systems, shipping: { ...getPlayerShipping(player), summary: null } } } } }, events: [{ type: "shippingAcknowledged", playerId }],
36 }), { allowPaused: true }),
37 ],
38 onOvernight: ({ state, rules }) => {
39 const shipping = getShipping(state), shipped = { ...shipping.shipped }, entries: ShippingEntry[] = [], unresolved: ShippingState["pending"] = [];
40 let next = state;
41 for (const pending of shipping.pending) {
42 const player = next.players[pending.playerId], item = getItem(rules, pending.stack.id);
43 // A temporarily removed content entry or absent player must never destroy saved items.
44 if (!player || !item) { unresolved.push(pending); continue; }
45 const unitPrice = sellPrice(item, pending.stack.quality, professionSaleMultiplier(player, item, rules)), total = unitPrice * pending.stack.quantity;
46 const firstShipment = !(shipped[item.id] ?? 0);
47 shipped[item.id] = (shipped[item.id] ?? 0) + pending.stack.quantity;
48 entries.push({ ...pending.stack, playerId: player.id, unitPrice, total, firstShipment });
49 next = creditMoney(next, player.id, total);
50 }
51 const total = entries.reduce((sum, entry) => sum + entry.total, 0), day = calendarDay(state);
52 const players = Object.fromEntries(Object.entries(next.players).map(([id, player]) => {
53 const previous = getPlayerShipping(player), personal = { ...previous.shipped };
54 for (const entry of entries) if (entry.playerId === id) personal[entry.id] = (personal[entry.id] ?? 0) + entry.quantity;
55 const credited = state.wallet.mode === "shared" ? total : entries.filter((entry) => entry.playerId === id).reduce((sum, entry) => sum + entry.total, 0);
56 return [id, { ...player, systems: { ...player.systems, shipping: { shipped: personal, summary: entries.length ? { day, entries, total, credited } : previous.summary } } }];
57 }));
58 return { state: { ...next, revision: next.revision + 1, players, systems: { ...next.systems, shipping: { pending: unresolved, shipped } } }, events: entries.length ? [{ type: "shippingPaid", data: { total, entries, day } }] : [] };
59 },
60 });
61