Exact source used by this build. SHA-256: fe3fa18c5bd3ae94bc7811ad94180784cc3e0ce10166e0abeeca07c83fce0c41.
1 import { z } from "zod";
2 import { createRng } from "../../rng";
3 import type { IntentResult } from "../../types";
4 import type { IntentContext } from "../registry";
5 import { addItem, getInventory, takeSlot } from "../inventory";
6 import { debitMoney } from "../shipping/money";
7
8 const GeodesSchema = z.object({ geodes: z.array(z.object({
9 itemId: z.string(), crackPrice: z.number().int().positive(),
10 drops: z.array(z.object({ itemId: z.string(), min: z.number().int().positive(), max: z.number().int().positive(), weight: z.number().positive() })).min(1),
11 })) });
12
13 /** Shop access belongs to shops; this service owns only the mining reward transaction. */
14 export function crackMineGeode({ state, player, playerId, rules }: IntentContext, slot?: number): IntentResult {
15 const data = GeodesSchema.safeParse(rules.content?.mines);
16 if (!data.success) return { state, events: [], error: "Geode content is unavailable" };
17 const slots = getInventory(player, rules).slots;
18 const selected = slot ?? slots.findIndex((stack) => stack && data.data.geodes.some((entry) => entry.itemId === stack.id));
19 const stack = slots[selected], geode = data.data.geodes.find((entry) => entry.itemId === stack?.id);
20 if (!geode) return { state, events: [], error: "Choose a geode in your backpack" };
21 const taken = takeSlot(player, selected, 1, rules);
22 if (!taken) return { state, events: [], error: "Geode is not in inventory" };
23 const paid = debitMoney(state, playerId, geode.crackPrice);
24 if (!paid) return { state, events: [], error: "Insufficient funds" };
25 const rng = createRng(state.rngState);
26 let roll = rng.next() * geode.drops.reduce((sum, drop) => sum + drop.weight, 0);
27 const reward = geode.drops.find((drop) => { roll -= drop.weight; return roll < 0; }) ?? geode.drops[geode.drops.length - 1]!;
28 const quantity = reward.min + rng.nextInt(reward.max - reward.min + 1);
29 const added = addItem(taken.player, { id: reward.itemId, quality: "regular", quantity }, rules);
30 if (added.remaining) return { state, events: [], error: "Make room for the geode contents" };
31 return {
32 state: { ...paid, revision: state.revision + 1, rngState: rng.state, players: { ...paid.players, [playerId]: added.player } },
33 events: [{ type: "geodeCracked", playerId, areaId: player.areaId, data: { geodeId: geode.itemId, itemId: reward.itemId, quantity, cost: geode.crackPrice } }],
34 };
35 }
36