import type { DungeonDefinition, DungeonBossObjective } from "../dungeonTypes"; import type { PopulationEntityDefinition, StaticMobSpawnDefinition } from "../mobPopulation"; import type { DungeonWorldObjectDefinition } from "../dungeonWorldObjects"; import { BITE_SIZED_STOCKADES_DUNGEON_ID, STORMWIND_STOCKADE_DUNGEON_ID, } from "../dungeonContentSource"; import { seededBossIndex } from "./seededBossSelection"; const BOSS_ROOM_POSITION = [-159.582, -25.6062, 1.2531] as const; const CENTRAL_CORRIDOR_HALF_WIDTH = 15; export interface BiteSizedStockadesBossCandidate { readonly sourceSpawnId: string; readonly name: string; } export const BITE_SIZED_STOCKADES_BOSS_CANDIDATES: readonly BiteSizedStockadesBossCandidate[] = Object.freeze([ { sourceSpawnId: "creature-84027", name: "Targorr the Dread" }, { sourceSpawnId: "creature-79088", name: "Kam Deepfury" }, { sourceSpawnId: "creature-79098", name: "Hamhock" }, { sourceSpawnId: "creature-79124", name: "Bazil Thredd" }, { sourceSpawnId: "creature-79116", name: "Dextren Ward" }, ]); export function biteSizedStockadesBossIndex(seed: string): number { return seededBossIndex(seed, BITE_SIZED_STOCKADES_BOSS_CANDIDATES.length); } function branchWall(id: string, name: string, z: number): DungeonWorldObjectDefinition { return { id, name, kind: "gate", position: [-129.17, -22.1, z], halfExtents: [6, 5.2, 0.65], initialState: "sealed", states: [{ id: "sealed", label: "Sealed", visible: true, blocksMovement: true, blocksSight: true, interactionEnabled: false, }], visual: { color: "#332f2b" }, source: { fidelity: "healer-man-authored", files: ["src/game/dungeons/biteSizedStockades.ts"], note: "Permanent wall for the bite-sized route; the source Stockades geometry remains unchanged.", }, }; } function sourceBoss( source: DungeonDefinition, candidate: BiteSizedStockadesBossCandidate, ): { readonly spawn: StaticMobSpawnDefinition; readonly entity: PopulationEntityDefinition } { const spawn = source.staticSpawns.find((entry) => entry.id === candidate.sourceSpawnId); const entity = spawn ? source.entities[spawn.entityId] : undefined; if (!spawn || !entity || entity.kind !== "boss") { throw new Error(`Stockades boss source is missing: ${candidate.sourceSpawnId}`); } return { spawn, entity }; } /** * Builds a one-boss Stockades variant without mutating the generated source definition. * The seed controls only boss identity, so saves and online party members can reconstruct * the same run while a fresh run receives a new random selection. */ export function createBiteSizedStockadesDefinition( source: DungeonDefinition, seed: string, ): DungeonDefinition { if (source.id !== STORMWIND_STOCKADE_DUNGEON_ID) { throw new Error(`Bite-sized Stockades requires ${STORMWIND_STOCKADE_DUNGEON_ID}.`); } const candidate = BITE_SIZED_STOCKADES_BOSS_CANDIDATES[biteSizedStockadesBossIndex(seed)]; const selected = sourceBoss(source, candidate); const sourceBossIds = new Set(BITE_SIZED_STOCKADES_BOSS_CANDIDATES.map((entry) => entry.sourceSpawnId)); const corridorSpawns = source.staticSpawns.filter((spawn) => ( !sourceBossIds.has(spawn.id) && Math.abs(spawn.position[2]) <= CENTRAL_CORRIDOR_HALF_WIDTH )); const usedEntityIds = new Set(corridorSpawns.map((spawn) => spawn.entityId)); const entities = Object.fromEntries([ ...Object.entries(source.entities).filter(([entityId]) => usedEntityIds.has(entityId)), [selected.spawn.entityId, selected.entity], ]); const boss: DungeonBossObjective = { id: candidate.sourceSpawnId, name: candidate.name, position: BOSS_ROOM_POSITION, }; return { ...source, id: BITE_SIZED_STOCKADES_DUNGEON_ID, title: "Stormwind Stockade: Cellblock Rush", lfgDungeonIds: [], presentation: { ...source.presentation, location: "Stormwind Stockade ยท Central Cellblock", theme: "Bite-Sized Dungeon", summary: "Race through the central cellblock and defeat one surprise warden chosen from the five Stockades bosses.", loadingMessage: "Sealing the side wings and choosing a cellblock warden...", unchartedAreaName: "Central Cellblock", }, areas: [ { ...source.entrance, id: "entrance", center: source.entrance.footPosition, radius: 35 }, { id: "bite-sized-boss-room", name: `${candidate.name}'s Cell`, center: BOSS_ROOM_POSITION, radius: 32 }, ], entities, staticSpawns: [ ...corridorSpawns, { id: candidate.sourceSpawnId, entityId: selected.spawn.entityId, sourceCreatureGuid: selected.spawn.sourceCreatureGuid, position: BOSS_ROOM_POSITION, yaw: selected.spawn.yaw, spawnMask: selected.spawn.spawnMask, }, ], roamingPacks: [], bosses: [boss], objectiveOrder: [boss], finalBossDurability: "standard", // Keep the complete reviewed Stockades navigation graph connected. The // physical branch walls, not navmesh pruning, close the unused wings. navigationLinks: source.navigationLinks, traversalRamps: source.traversalRamps, encounters: [{ id: `${BITE_SIZED_STOCKADES_DUNGEON_ID}:boss-progression`, dungeonId: BITE_SIZED_STOCKADES_DUNGEON_ID, initialPhase: "progression", initialState: { flags: { "dungeon-complete": false } }, resetPolicy: { bossWipePhase: "progression", checkpointPhase: "progression" }, source: { fidelity: "healer-man-authored", scope: "boss-progression", files: ["src/game/dungeons/biteSizedStockades.ts"], reviewedBehavior: "A single seeded Stockades boss completes the bite-sized run in the first boss room.", adaptations: [ "Each seeded run retains the selected source boss objective ID so saves, loot, guides, and leaderboards report the correct variant.", "The one-boss run uses standard boss durability instead of the campaign-finale health floor.", "The side-wing stairs retain their navigation bridges so the central route stays on the retained navmesh component; physical walls seal both unused wings.", ], remainingGaps: [], }, rules: [ { id: "boss-defeated", trigger: { kind: "entities-dead", entityIds: [candidate.sourceSpawnId] }, actions: [ { id: "record-boss", kind: "complete-objective", objectiveId: candidate.sourceSpawnId, persistence: "run" }, { id: "checkpoint-boss", kind: "set-checkpoint", checkpointId: candidate.sourceSpawnId, persistence: "checkpoint" }, ], }, { id: "complete-run", trigger: { kind: "tick" }, guards: [{ kind: "objective-complete", objectiveId: candidate.sourceSpawnId, equals: true }], actions: [ { id: "mark-dungeon-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "enter-complete-phase", kind: "set-phase", phase: "complete", persistence: "run" }, ], }, ], }], encounterSpawnGroups: [], serverGameObjects: [ ...source.serverGameObjects.filter((object) => Math.abs(object.position[2]) <= CENTRAL_CORRIDOR_HALF_WIDTH), branchWall("bite-sized-stockades-north-wall", "Sealed North Wing", 16.4), branchWall("bite-sized-stockades-south-wall", "Sealed South Wing", -16.4), ], validation: { ...source.validation, warnings: [ ...source.validation.warnings, "Bite-sized layout and branch walls are Healer Man-authored; environment and combat assets are shared with Stormwind Stockade.", ], }, }; }