import type { DungeonDefinition } from "../dungeonTypes"; import type { PopulationEntityDefinition } from "../mobPopulation"; import { RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, STORMWIND_STOCKADE_DUNGEON_ID, } from "../dungeonContentSource"; import { BITE_SIZED_STOCKADES_BOSS_CANDIDATES, createBiteSizedStockadesDefinition, } from "./biteSizedStockades"; export type RandomizedCreatureKind = "mob" | "boss"; export interface RandomizedBiteSizedCreature { readonly key: string; readonly sourceDungeonId: string; readonly sourceDungeonTitle: string; readonly entityId: string; readonly name: string; readonly kind: RandomizedCreatureKind; readonly serverEntry: number | null; } function seedHash(seed: string): number { let hash = 0x811c9dc5; for (let index = 0; index < seed.length; index += 1) { hash ^= seed.charCodeAt(index); hash = Math.imul(hash, 0x01000193); } return hash >>> 0; } function placedEntityIds(source: DungeonDefinition): ReadonlySet { return new Set([ ...source.staticSpawns.map((spawn) => spawn.entityId), ...source.roamingPacks.flatMap((pack) => pack.members.map((member) => member.entityId)), ]); } /** The immutable review catalog used by both seeded generation and the GM readiness screen. */ export function randomizedBiteSizedCreatureCatalog( source: DungeonDefinition, ): readonly RandomizedBiteSizedCreature[] { if (source.id !== STORMWIND_STOCKADE_DUNGEON_ID) { throw new Error(`Randomized Bite-Sized Stockades requires ${STORMWIND_STOCKADE_DUNGEON_ID}.`); } const placed = placedEntityIds(source); const bossEntityIds = new Set(BITE_SIZED_STOCKADES_BOSS_CANDIDATES.map((candidate) => { const spawn = source.staticSpawns.find((entry) => entry.id === candidate.sourceSpawnId); if (!spawn) throw new Error(`Stockades boss source is missing: ${candidate.sourceSpawnId}`); return spawn.entityId; })); return Object.values(source.entities) .filter((entity) => placed.has(entity.id) && (entity.kind === "mob" || bossEntityIds.has(entity.id))) .map((entity) => ({ key: `${source.id}:${entity.id}`, sourceDungeonId: source.id, sourceDungeonTitle: source.title, entityId: entity.id, name: entity.name, kind: entity.kind, serverEntry: entity.combat?.serverEntry ?? null, })) .sort((left, right) => left.kind.localeCompare(right.kind) || left.name.localeCompare(right.name)); } /** * Rerolls every ordinary corridor spawn from the complete placed Stockades mob * pool. The existing bite-sized builder remains the authority for geometry, * navigation, sealed wings, and the independently seeded five-boss roll. */ export function createRandomizedBiteSizedStockadesDefinition( source: DungeonDefinition, seed: string, ): DungeonDefinition { const base = createBiteSizedStockadesDefinition(source, seed); const catalog = randomizedBiteSizedCreatureCatalog(source); const mobCandidates = catalog.filter((candidate) => candidate.kind === "mob"); if (!mobCandidates.length) throw new Error("Randomized Bite-Sized Stockades has no ordinary mob candidates."); const bossSpawnId = base.bosses[0]?.id; const randomizedSpawns = base.staticSpawns.map((spawn) => { if (spawn.id === bossSpawnId) return spawn; const candidate = mobCandidates[seedHash(`${seed}:${spawn.id}`) % mobCandidates.length]; return { ...spawn, entityId: candidate.entityId }; }); const usedEntityIds = new Set(randomizedSpawns.map((spawn) => spawn.entityId)); const entities = Object.fromEntries( Object.entries(source.entities).filter(([entityId]) => usedEntityIds.has(entityId)), ) as Readonly>; return { ...base, id: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, title: "Randomized Bite-Sized: Cellblock Shuffle", presentation: { ...base.presentation, location: "Stormwind Stockade ยท Shifting Cellblock", theme: "Randomized Bite-Sized", summary: "Every enemy slot and the final warden are rerolled for a fast, unpredictable one-boss run.", loadingMessage: "Shuffling prisoners, guards, and the final warden...", }, entities, staticSpawns: randomizedSpawns, encounters: (base.encounters ?? []).map((encounter) => ({ ...encounter, id: `${RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID}:boss-progression`, dungeonId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, source: encounter.source ? { ...encounter.source, files: ["src/game/dungeons/randomizedBiteSizedStockades.ts"], reviewedBehavior: "Every corridor slot and the final Stockades boss are deterministic rolls from the run seed.", adaptations: [ ...encounter.source.adaptations, "Each ordinary spawn slot independently selects a placed Stockades mob archetype from the immutable review catalog.", ], } : encounter.source, })), validation: { ...base.validation, warnings: [ ...base.validation.warnings, "Randomized creature identities are Healer Man-authored and deterministic per run seed.", ], }, }; }