Files
healer-man/src/game/dungeonRegistry.ts
T
2026-08-14 15:56:39 -04:00

115 lines
4.5 KiB
TypeScript

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<Record<string, DungeonDefinition>> = 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<Record<string, boolean>>)[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);
}