import type { DungeonDefinition } from "./dungeonTypes"; import { FORSAKEN_ABBEY_DEFINITION } from "./dungeons/forsakenAbbey"; import { WAILING_CAVERNS_DEFINITION } from "./dungeons/wailingCaverns"; import availability from "./generated/dungeonAvailability.json"; import { RUNEWAKER_DUNGEON_DEFINITIONS } from "./generated/runewakerDungeonDefinitions.generated"; import compressedCampaign from "./generated/dungeonCampaignCatalog.compressed"; import { gunzipSync, strFromU8 } from "fflate"; import { DEFAULT_DUNGEON_ID as DEFAULT_DUNGEON_KEY } from "./dungeonDefaults"; export { DEFAULT_DUNGEON_ID } from "./dungeonDefaults"; function decodeBase64(value: string): Uint8Array { const decoded = atob(value); const bytes = new Uint8Array(decoded.length); for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index); return bytes; } const campaign = JSON.parse(strFromU8(gunzipSync(decodeBase64(compressedCampaign)))) as { readonly definitions: readonly DungeonDefinition[]; }; const generatedDefinitions = campaign.definitions as unknown as readonly DungeonDefinition[]; function normalizedName(value: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, ""); } /** Keep authored/server-backed WC combat; use generated kits only for unmapped fixtures. */ function enrichAuthoredDefinition( authored: DungeonDefinition, generated: DungeonDefinition | undefined, ): DungeonDefinition { if (!generated) return authored; const generatedByName = new Map( Object.values(generated.entities).map((entity) => [normalizedName(entity.name), entity]), ); return { ...authored, entities: Object.fromEntries( Object.entries(authored.entities).map(([id, entity]) => { const generatedEntity = generatedByName.get(normalizedName(entity.name)); return [ id, entity.combat ? entity : generatedEntity?.combat ? { ...entity, combat: generatedEntity.combat } : entity, ]; }), ), }; } const authoredWailingCaverns = enrichAuthoredDefinition( WAILING_CAVERNS_DEFINITION, generatedDefinitions.find((definition) => definition.id === WAILING_CAVERNS_DEFINITION.id), ); if (authoredWailingCaverns.id !== DEFAULT_DUNGEON_KEY) { throw new Error(`Default dungeon id mismatch: ${authoredWailingCaverns.id}`); } export const DUNGEON_REGISTRY: Readonly> = Object.freeze( Object.fromEntries([ [authoredWailingCaverns.id, authoredWailingCaverns], [FORSAKEN_ABBEY_DEFINITION.id, FORSAKEN_ABBEY_DEFINITION], ...RUNEWAKER_DUNGEON_DEFINITIONS.map((definition) => [definition.id, definition] as const), ...generatedDefinitions .filter((definition) => definition.id !== authoredWailingCaverns.id) .map((definition) => [definition.id, definition] as const), ]), ); export type DungeonId = string; export const DUNGEON_DEFINITIONS = Object.freeze(Object.values(DUNGEON_REGISTRY)); export function dungeonDefinitionById(id: string): DungeonDefinition | null { return DUNGEON_REGISTRY[id] ?? null; } export function requireDungeonDefinition(id: string): DungeonDefinition { const definition = dungeonDefinitionById(id); if (!definition) throw new Error(`Unknown dungeon definition: ${id}`); return definition; } /** Portable package metadata is the offline validation gate for independently generated dungeons. */ export function dungeonPackagePassesOfflineGate(definition: DungeonDefinition): boolean { return definition.availability === "available" && definition.validation.status === "green" && definition.validation.blockers.length === 0; } export function dungeonIsEnabled(id: string): boolean { const definition = dungeonDefinitionById(id); if (!definition) return false; const configured = (availability.enabled as Readonly>)[id]; if (definition.expansion !== "runewaker") return configured === true; if (!dungeonPackagePassesOfflineGate(definition)) return false; if (configured !== undefined) return configured; // The WoW and RuneWaker package generators are independent. A WoW catalog // refresh can legitimately omit RuneWaker keys, so those packages fall back // to the green validation metadata bundled into their generated definition. return true; } export function dungeonCanEnter(id: string): boolean { const definition = dungeonDefinitionById(id); return Boolean(definition && dungeonIsEnabled(id) && definition.assets.visual.length && definition.assets.collision.length); }