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