Exact source used by this build. SHA-256: 71d3676634fd11ab2c3bc2819fed6e0da9f52d09cca32e4f3c11f5c905807f69.

1 import json from "../../../data/fishing/fishing.json";
2 import { ITEMS, validateItemReferences } from "../items";
3 import { AREA_MAPS, type AreaMap } from "../maps";
4 import { SHOPS } from "../shops";
5 import { FishingSchema } from "./schema";
6 import type { ContentDomain } from "../types";
7 import type { FishingContent, FishingWater } from "./schema";
8 
9 export * from "./schema";
10 /** Tileset tags classify every authored water tile, including later map extensions. */
11 export function fishingMapWater(maps: Readonly<Record<string, AreaMap>>): Pick<FishingContent, "waters" | "waterTileIds"> {
12   const waters: FishingWater[] = [];
13   for (const map of Object.values(maps)) for (const waterTag of new Set(Object.values(map.waterTags))) {
14     const waterTileIds = Object.entries(map.waterTags).filter(([, tag]) => tag === waterTag).map(([gid]) => Number(gid));
15     if (!map.groundTiles.some((gid) => waterTileIds.includes(gid))) continue;
16     waters.push({ areaId: map.id, waterTag, x: 0, y: 0, width: map.width, height: map.height,
17       waterTileIds, manifestKey: map.tileManifest[waterTileIds[0]!] ?? `fishing.water.${waterTag}` });
18   }
19   return { waters, waterTileIds: [...new Set(waters.flatMap((water) => water.waterTileIds!))] };
20 }
21 export const FISHING = FishingSchema.parse({ ...json, ...fishingMapWater(AREA_MAPS) });
22 
23 function unique(values: readonly string[], label: string): void {
24   if (new Set(values).size !== values.length) throw new Error(`Duplicate ${label}`);
25 }
26 
27 /** A rectangle classifies existing water tiles; it never creates water on land. */
28 export function validateFishingReferences(value: unknown, items: readonly { id: string; category: string; baseSellPrice: number; qualityAllowed: boolean }[] = ITEMS): FishingContent {
29   const data = FishingSchema.parse(value);
30   for (const [label, entries] of Object.entries({ rod: data.rods, bait: data.baits, tackle: data.tackles, fish: data.fish, crab: data.crabCreatures })) {
31     unique(entries.map((entry) => entry.id), `${label} id`);
32     unique(entries.map((entry) => entry.itemId), `${label} item`);
33   }
34   unique(data.tables.map((table) => `${table.areaId}:${table.waterTag}`), "fishing area/water table");
35   unique(data.waterTileIds.map(String), "water tile id");
36   unique(data.shopIds, "fishing shop id");
37   validateItemReferences([
38     ...data.rods.map((entry) => entry.itemId), ...data.baits.map((entry) => entry.itemId), ...data.tackles.map((entry) => entry.itemId),
39     ...data.fish.map((entry) => entry.itemId), ...data.crabCreatures.map((entry) => entry.itemId),
40     ...data.trash.map((entry) => entry.itemId), ...data.treasure.map((entry) => entry.itemId),
41     data.crabPot.itemId, data.crabPot.recipeItemId,
42     ...data.crabPot.ingredients.map((entry) => entry.itemId), ...data.crabPot.trapperIngredients.map((entry) => entry.itemId),
43   ], items);
44   for (const creature of [...data.fish, ...data.crabCreatures]) {
45     const item = items.find((entry) => entry.id === creature.itemId)!;
46     if (item.category !== "fish" || !item.qualityAllowed) throw new Error(`Fishing catch ${creature.id} requires a fish item supporting quality`);
47     if (item.baseSellPrice !== creature.basePrice) throw new Error(`Fishing catch ${creature.id} price disagrees with item catalog`);
48   }
49   for (const rod of data.rods) if (items.find((entry) => entry.id === rod.itemId)?.category !== "tool") throw new Error(`Fishing rod ${rod.id} must use a tool item`);
50   for (const shopId of data.shopIds) if (!SHOPS.some((shop) => shop.id === shopId)) throw new Error(`Missing fishing shop ${shopId}`);
51   const fishIds = new Set(data.fish.map((fish) => fish.id)), crabIds = new Set(data.crabCreatures.map((crab) => crab.id));
52   for (const table of data.tables) {
53     if (table.areaId !== "*" && !AREA_MAPS[table.areaId]) throw new Error(`Missing fishing area ${table.areaId}`);
54     unique(table.fishIds, "fish in fishing table");
55     unique(table.crabCreatureIds, "crab creature in fishing table");
56     for (const fishId of table.fishIds) if (!fishIds.has(fishId)) throw new Error(`Missing fishing table fish ${fishId}`);
57     for (const crabId of table.crabCreatureIds) if (!crabIds.has(crabId)) throw new Error(`Missing fishing table crab ${crabId}`);
58   }
59   for (const creature of data.fish) if (!data.tables.some((table) => table.fishIds.includes(creature.id))) throw new Error(`Unreachable fish ${creature.id}`);
60   for (const creature of data.crabCreatures) if (!data.tables.some((table) => table.crabCreatureIds.includes(creature.id))) throw new Error(`Unreachable crab creature ${creature.id}`);
61   for (const water of data.waters) {
62     const area = AREA_MAPS[water.areaId];
63     if (!area || water.x + water.width > area.width || water.y + water.height > area.height) throw new Error(`Fishing water outside area ${water.areaId}`);
64     if (!data.tables.some((table) => (table.areaId === water.areaId || table.areaId === "*") && table.waterTag === water.waterTag)) throw new Error(`Fishing water has no matching table ${water.areaId}:${water.waterTag}`);
65     let containsWater = false;
66     for (let y = water.y; y < water.y + water.height; y++) for (let x = water.x; x < water.x + water.width; x++) {
67       if ((water.waterTileIds ?? data.waterTileIds).includes(area.groundTiles[y * area.width + x] ?? 0)) containsWater = true;
68     }
69     if (!containsWater) throw new Error(`Fishing water metadata contains no real water ${water.areaId}`);
70   }
71   for (let at = 0; at < data.waters.length; at++) for (const other of data.waters.slice(at + 1)) {
72     const water = data.waters[at]!;
73     const sharedTiles = (water.waterTileIds ?? data.waterTileIds).some((gid) => (other.waterTileIds ?? data.waterTileIds).includes(gid));
74     if (sharedTiles && water.areaId === other.areaId && water.x < other.x + other.width && water.x + water.width > other.x && water.y < other.y + other.height && water.y + water.height > other.y) throw new Error(`Overlapping fishing water metadata ${water.areaId}`);
75   }
76   return data;
77 }
78 
79 export const fishingDomain: ContentDomain = {
80   id: "fishing", data: FISHING,
81   validate() { validateFishingReferences(FISHING); },
82 };
83 fishingDomain.validate();
84