Exact source used by this build. SHA-256: fe9efbc10f293cc845d1d05d8e4d12b408dd86203009705071e10ad26d51a616.
1 import { z } from "zod";
2 import type { IntentResult, WorldState } from "../../types";
3 import { defineIntent, defineSystem } from "../registry";
4
5 /** Switch wallet modes deterministically. An odd shared balance gives the extra unit to the first player id. */
6 export function setWalletMode(state: WorldState, mode: WorldState["wallet"]["mode"]): WorldState {
7 if (state.wallet.mode === mode) return state;
8 const ids = Object.keys(state.players).sort();
9 if (ids.length === 0) return state;
10 if (mode === "separate") {
11 const base = ids.length ? Math.floor(state.wallet.shared / ids.length) : 0;
12 let remainder = ids.length ? state.wallet.shared % ids.length : 0;
13 const balances: Record<string, number> = {};
14 for (const id of ids) { balances[id] = base + (remainder-- > 0 ? 1 : 0); }
15 return { ...state, revision: state.revision + 1, wallet: { mode, shared: 0, balances, ledger: [] } };
16 }
17 const shared = Object.values(state.wallet.balances).reduce((sum, value) => sum + value, 0);
18 return { ...state, revision: state.revision + 1, wallet: { mode, shared, balances: Object.fromEntries(ids.map((id) => [id, 0])), ledger: [] } };
19 }
20
21 export function transferMoney(state: WorldState, fromPlayerId: string, toPlayerId: string, amount: number): IntentResult {
22 if (state.wallet.mode !== "separate") return { state, events: [], error: "Transfers require separate wallets" };
23 if (!Number.isSafeInteger(amount) || amount <= 0 || fromPlayerId === toPlayerId || !state.players[toPlayerId]) return { state, events: [], error: "Invalid transfer" };
24 const from = state.wallet.balances[fromPlayerId] ?? 0;
25 if (from < amount) return { state, events: [], error: "Insufficient funds" };
26 const ledger = [...state.wallet.ledger, { fromPlayerId, toPlayerId, amount }].slice(-100);
27 const wallet = { ...state.wallet, balances: { ...state.wallet.balances, [fromPlayerId]: from - amount, [toPlayerId]: (state.wallet.balances[toPlayerId] ?? 0) + amount }, ledger };
28 return { state: { ...state, revision: state.revision + 1, wallet }, events: [{ type: "transfer", playerId: fromPlayerId }] };
29 }
30
31
32 export function chargePassOut(state: WorldState, playerIds: readonly string[]): WorldState {
33 let wallet = state.wallet;
34 for (const id of playerIds) {
35 if (!state.players[id]) continue;
36 if (wallet.mode === "shared") wallet = { ...wallet, shared: wallet.shared - Math.min(1000, Math.floor(wallet.shared * 0.1)) };
37 else { const balance = wallet.balances[id] ?? 0; wallet = { ...wallet, balances: { ...wallet.balances, [id]: balance - Math.min(1000, Math.floor(balance * 0.1)) } }; }
38 }
39 return { ...state, wallet };
40 }
41 export const walletSystem = defineSystem({
42 id: "wallet", worldDefaults: () => ({}), playerDefaults: () => ({}), events: ["transfer"],
43 initializeWorld: () => ({ wallet: { mode: "shared", shared: 0, balances: {}, ledger: [] } }),
44 intents: [defineIntent("transfer", z.object({ type: z.literal("transfer"), toPlayerId: z.string().min(1), amount: z.number().int().positive() }),
45 ({ state, playerId }, intent) => transferMoney(state, playerId, intent.toPlayerId, intent.amount), { allowPaused: true, allowSleeping: true })],
46 onPlayerAdded: ({ state, playerId }) => ({ state: { ...state, wallet: { ...state.wallet, balances: { ...state.wallet.balances, [playerId!]: 0 } } }, events: [] }),
47 });
48