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

1 import json from "../../../data/mines/mines.json";
2 import { ITEMS, validateItemReferences } from "../items";
3 import { AREA_MAPS, isMapBlocked, type AreaMap } from "../maps";
4 import { SHOPS } from "../shops";
5 import type { ContentDomain } from "../types";
6 import { MinesSchema } from "./schema";
7 
8 export * from "./schema";
9 const MinesSourceSchema = MinesSchema.extend({ config: MinesSchema.shape.config.extend({
10   entrance: MinesSchema.shape.config.shape.entrance.omit({ x: true, y: true }),
11 }) });
12 
13 /** Resolve the entrance from its authored POI/warp on every content load. */
14 export function loadMinesContent(input: unknown, maps: Readonly<Record<string, AreaMap>> = AREA_MAPS) {
15   const source = MinesSourceSchema.parse(input), entrance = source.config.entrance;
16   const area = maps[entrance.areaId];
17   if (!area) throw new Error("Mine entrance references missing area");
18   const anchor = [...area.warps, ...area.pois, ...area.markers].find((entry) => entry.id === entrance.warpId);
19   if (!anchor) throw new Error("Mine entrance references missing warp or marker/POI");
20   const position = { x: anchor.x + anchor.width / 2, y: anchor.y + anchor.height / 2 };
21   if (isMapBlocked(area, position.x, position.y)) throw new Error("Mine entrance must be walkable");
22   return MinesSchema.parse({ ...source, config: { ...source.config, entrance: { ...entrance, ...position } } });
23 }
24 export const MINES = loadMinesContent(json);
25 
26 /** Validate producers, equipment, floor coverage and movable entrance references together. */
27 export function validateMines(input: unknown, items: readonly { id: string }[] = ITEMS): void {
28   const data = MinesSchema.parse(input), config = data.config;
29   const unique = (ids: readonly string[], kind: string): Set<string> => {
30     const result = new Set(ids);
31     if (result.size !== ids.length) throw new Error(`Duplicate mine ${kind} id`);
32     return result;
33   };
34   const nodes = unique(data.nodes.map((entry) => entry.id), "node");
35   const monsters = unique(data.monsters.map((entry) => entry.id), "monster");
36   const weapons = unique(data.weapons.map((entry) => entry.id), "weapon");
37   unique(data.bands.map((entry) => entry.id), "band");
38   unique(data.rings.map((entry) => entry.id), "ring");
39   unique(data.boots.map((entry) => entry.id), "boots");
40   unique(data.minerals.map((entry) => entry.id), "mineral");
41   unique(data.geodes.map((entry) => entry.itemId), "geode");
42   unique(data.treasure.map((entry) => String(entry.floor)), "treasure floor");
43   if (!weapons.has(config.starterWeaponId)) throw new Error("Unknown starter weapon");
44   if (config.mvpContentFloors > config.maxFloor) throw new Error("MVP floor count exceeds mine depth");
45   if (config.rooms.min > config.rooms.max || config.rooms.minSize > config.rooms.maxSize || config.rooms.maxSize + 2 > Math.min(config.width, config.height)) throw new Error("Mine room bounds are invalid");
46   if (config.knockout.maxGoldRate < config.knockout.minGoldRate) throw new Error("Knockout loss range is inverted");
47   if (Math.abs(Object.values(config.nodeWeights).reduce((sum, value) => sum + value, 0) - 1) > 0.000001) throw new Error("Mine node weights must sum to one");
48   if (data.mushrooms.max < data.mushrooms.min) throw new Error("Mushroom count range is inverted");
49   for (let floor = 1; floor <= config.maxFloor; floor++) {
50     if (data.bands.filter((band) => floor >= band.minFloor && floor <= band.maxFloor).length !== 1) throw new Error(`Mine floor ${floor} must belong to exactly one band`);
51     if (floor % config.treasureEvery === 0 && !data.treasure.some((entry) => entry.floor === floor)) throw new Error(`Mine floor ${floor} has no treasure reward`);
52   }
53   for (const band of data.bands) {
54     if (band.maxFloor > config.maxFloor || band.minFloor > band.maxFloor) throw new Error("Mine band bounds are invalid");
55     for (const id of [band.rockNodeId, band.oreNodeId, band.geodeNodeId, ...band.gemNodeIds]) if (!nodes.has(id)) throw new Error(`Unknown mine node ${id}`);
56     for (const id of band.monsterIds) if (!monsters.has(id)) throw new Error(`Unknown mine monster ${id}`);
57     for (const [id, kind] of [[band.rockNodeId, "rock"], [band.oreNodeId, "ore"], [band.geodeNodeId, "geode"], ...band.gemNodeIds.map((id) => [id, "gem"])] as [string, string][]) {
58       if (data.nodes.find((node) => node.id === id)?.kind !== kind) throw new Error(`Mine band node ${id} has wrong kind`);
59     }
60   }
61   for (const node of data.nodes) if (node.hitsByTier.some((hits, index) => index > 0 && hits > node.hitsByTier[index - 1]!)) throw new Error(`Mine node ${node.id} becomes harder with upgrades`);
62   for (const weapon of data.weapons) {
63     const expected = { sword: "block", dagger: "flurry", club: "slam" }[weapon.type];
64     if (weapon.special.kind !== expected) throw new Error(`Mine weapon ${weapon.id} has wrong special move`);
65   }
66   for (const geode of data.geodes) for (const drop of geode.drops) if (drop.max < drop.min) throw new Error("Geode drop range is inverted");
67   for (const chest of data.treasure) if (chest.floor > config.maxFloor || chest.floor % config.treasureEvery !== 0) throw new Error("Treasure must be on a reward floor");
68   validateItemReferences([
69     config.mushroomItemId,
70     ...data.nodes.flatMap((node) => node.drops.map((drop) => drop.itemId)),
71     ...data.monsters.flatMap((monster) => monster.drops.map((drop) => drop.itemId)),
72     ...data.weapons.map((weapon) => weapon.itemId), ...data.rings.map((ring) => ring.itemId),
73     ...data.boots.map((boots) => boots.itemId), ...data.minerals.map((mineral) => mineral.itemId),
74     ...data.geodes.flatMap((geode) => [geode.itemId, ...geode.drops.map((drop) => drop.itemId)]),
75     ...data.treasure.flatMap((chest) => chest.items.map((item) => item.itemId)), ...data.mushrooms.itemIds,
76   ], items);
77   const area = AREA_MAPS[config.entrance.areaId];
78   if (!area) throw new Error("Mine entrance references missing area");
79   const resolved = loadMinesContent(data).config.entrance;
80   if (resolved.x !== config.entrance.x || resolved.y !== config.entrance.y) throw new Error("Mine entrance coordinates disagree with map metadata");
81   if (!SHOPS.some((shop) => shop.geodeCracking)) throw new Error("Mines require a geode-cracking shop");
82 }
83 
84 export const minesDomain: ContentDomain = { id: "mines", data: MINES, validate() { validateMines(MINES); } };
85 minesDomain.validate();
86