import type { DungeonBossObjective, DungeonDefinition, DungeonEncounterSpawnGroupDefinition, Vector3Tuple, } from "./dungeonTypes"; import type { DungeonEncounterAction, DungeonEncounterDefinition, DungeonEncounterRule, } from "./dungeonEncounterTypes"; import type { DungeonWorldObjectDefinition } from "./dungeonWorldObjects"; import type { PopulationDefinitionMap, PopulationEntityDefinition, StaticMobSpawnDefinition, } from "./mobPopulation"; import type { EnemyAttackDefinition } from "./enemyAttacks"; export interface WowWrathEncounterSourceFile { readonly path: string; readonly sha256: string; readonly anchors: readonly string[]; } export interface WowWrathEncounterContentReview { readonly dungeonId: "halls-of-reflection" | "the-oculus" | "trial-of-the-champion"; readonly fidelity: "source-backed-adapted"; readonly sourceFiles: readonly WowWrathEncounterSourceFile[]; readonly reviewedBehavior: string; readonly adaptations: readonly string[]; readonly remainingGaps: readonly string[]; } interface AdaptedEntitySpec { readonly id?: string; readonly entry: number; readonly name: string; readonly level: number; readonly role: "melee" | "caster"; readonly kind?: "mob" | "boss"; readonly title?: string; readonly hasLoot?: boolean; readonly attacks?: readonly EnemyAttackDefinition[]; readonly basicAttackStyle?: "melee" | "spell-proxy"; } const SOURCE_ROOT = "../HealerMan-Storage/pipeline-work/dungeons/azerothcore-scripts/src/server/scripts"; const HALLS_OF_REFLECTION_FILES = Object.freeze([ `${SOURCE_ROOT}/Northrend/FrozenHalls/HallsOfReflection/instance_halls_of_reflection.cpp`, `${SOURCE_ROOT}/Northrend/FrozenHalls/HallsOfReflection/halls_of_reflection.cpp`, `${SOURCE_ROOT}/Northrend/FrozenHalls/HallsOfReflection/halls_of_reflection.h`, ]); const OCULUS_FILES = Object.freeze([ `${SOURCE_ROOT}/Northrend/Nexus/Oculus/instance_oculus.cpp`, `${SOURCE_ROOT}/Northrend/Nexus/Oculus/boss_urom.cpp`, `${SOURCE_ROOT}/Northrend/Nexus/Oculus/boss_eregos.cpp`, `${SOURCE_ROOT}/Northrend/Nexus/Oculus/oculus.h`, ]); const TRIAL_OF_THE_CHAMPION_FILES = Object.freeze([ `${SOURCE_ROOT}/Northrend/CrusadersColiseum/TrialOfTheChampion/instance_trial_of_the_champion.cpp`, `${SOURCE_ROOT}/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_grand_champions.cpp`, `${SOURCE_ROOT}/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_argent_challenge.cpp`, `${SOURCE_ROOT}/Northrend/CrusadersColiseum/TrialOfTheChampion/boss_black_knight.cpp`, `${SOURCE_ROOT}/Northrend/CrusadersColiseum/TrialOfTheChampion/trial_of_the_champion.h`, ]); export const WOW_WRATH_ENCOUNTER_CONTENT_REVIEWS: readonly WowWrathEncounterContentReview[] = Object.freeze([ { dungeonId: "halls-of-reflection", fidelity: "source-backed-adapted", sourceFiles: [ { path: HALLS_OF_REFLECTION_FILES[0], sha256: "4b274ef9a3042c478e54e2b2ed87bd7902e4304d2c6ade342ae7635916c00e66", anchors: [ "OnGameObjectCreate front/Arthas doors (lines 317-331)", "SetBossState Falric pause and Marwyn unlock (lines 337-376)", "StartNextWave ten-wave ordering (lines 713-779)", "HandleWaveWipe and resume (lines 781-817)", ], }, { path: HALLS_OF_REFLECTION_FILES[1], sha256: "483de89e772ab41667ace738fc3ece73b1532aa0ca80a523a4350318e85b5dce", anchors: [ "escape wall completion lifecycle (lines 1531-1543)", "Lich King pursuit failure pressure (lines 1617-1653)", "four ordered escape summon schedules (lines 1690-1721)", ], }, { path: HALLS_OF_REFLECTION_FILES[2], sha256: "023932e8ced7532f857516c3d8688462ef961028ded128b52ae963a3cfa22452", anchors: [ "wave creature and door ids (lines 55-106)", "allowedCompositions (lines 316-325)", "WP_STOP and PathWaypoints (lines 353-376)", ], }, ], reviewedBehavior: "A finite event preserves the source 4 trash waves, Falric, 4 trash waves, Marwyn, Frostsworn General, and four ordered escape-wall stands before completion.", adaptations: [ "The deterministic composition order is one valid outcome of AzerothCore's paired random selection and shuffle.", "The Lich King chase is represented by four bounded combat stands and exact source wall-stop coordinates; clearing a stand advances the whole party because protected escort/path pursuit primitives are not available.", "Source front/Arthas doors are cloned as visible procedural blockers and dynamic ice walls use source template 201385 at source stop coordinates; the boxes are explicit temporary geometry, not exported client models.", "The Frostsworn General and five Spiritual Reflections fight together instead of copying current player appearances after engagement.", ], remainingGaps: [ "Jaina/Sylvanas escort movement, Lich King distance pressure, leader health/failure, Remorseless Winter, and behind-the-pursuer instant death need protected-escort and moving-hazard support.", "Faction-specific intro/outro, dialogue, gunship transport, quest credit, achievements, loot presentation, and native door/ice-wall models remain unported.", ], }, { dungeonId: "the-oculus", fidelity: "source-backed-adapted", sourceFiles: [ { path: OCULUS_FILES[0], sha256: "55329af928c05aecf8d1b03887651dea82812dd1ffb32f887eb85f0f5ae2bc54", anchors: [ "Drakos opens three dragon cage doors (lines 89-107)", "Drakos enables ten-construct count (lines 116-149)", "Varos/Urom/Eregos attackability gates (lines 151-180)", "ten Centrifuge Constructs remove Varos shield (lines 182-194)", ], }, { path: OCULUS_FILES[1], sha256: "e09f7bd1ad478467bd41de119088d4e2aab5da4cfa7e3ddee4fb54c43a232238", anchors: [ "three exact menagerie compositions (lines 36-76)", "three outer platform and final Urom coordinates (lines 78-87)", "summon, disappear, and next-platform lifecycle (lines 144-188, 241-274)", ], }, { path: OCULUS_FILES[2], sha256: "d0bab24e54ad84484eaaed0dfe37d6f9b7bb962faa848e621fd56dc2077544b1", anchors: [ "Urom prerequisite and Eregos reset gate (lines 84-96)", "Eregos victory/cache handoff (lines 128-141)", "Planar Shift and whelp schedules (lines 190-223)", ], }, { path: OCULUS_FILES[3], sha256: "0e51c1b9fde0bb7b408e5b96557e2c96fe491e747297ce16c02b02735f7f1f29", anchors: ["boss, construct, drake, cage-door, and cache ids (lines 32-86)"], }, ], reviewedBehavior: "Drakos gates the exact ten imported Centrifuge Constructs and cage doors; Varos gates three exact Urom menageries; Urom gates Eregos.", adaptations: [ "Party teleports between exact source platform coordinates replace player-controlled drake vehicles and free vertical flight.", "Urom's three summon-and-disappear platform interactions are finite spawn groups followed by his final ground fight.", "The three exact imported dragon cage doors are cloned as visible procedural blockers with source closed/open semantics; all unrelated imported objects remain untouched and hidden/nonblocking.", "Eregos remains a direct party fight, so drake role abilities and void-achievement loadouts are not required to make the instance completable.", ], remainingGaps: [ "Ruby/Emerald/Amber drake vehicles, free flight, player-selected drake composition, and vehicle-specific Eregos tuning require a vehicle runtime.", "Varos construct beam visuals, Urom teleport presentation, Eregos Planar Shift/anomalies, dialogue, achievements, and cache loot presentation remain incomplete.", ], }, { dungeonId: "trial-of-the-champion", fidelity: "source-backed-adapted", sourceFiles: [ { path: TRIAL_OF_THE_CHAMPION_FILES[0], sha256: "85b569490763e207366e9a04741dfd3bba5e176725b2b953416d395759bca7a0", anchors: [ "source summon position and known source TODOs (lines 25-35)", "three distinct random champion selection (lines 467-505)", "three minion waves, mounted champions, then foot champions (lines 550-626, 819-968)", "nine Argent soldiers then Eadric/Paletress (lines 984-1083)", "Black Knight arrival and attack scene (lines 1122-1217)", ], }, { path: TRIAL_OF_THE_CHAMPION_FILES[1], sha256: "c7c517be4a355099574fee16c7e9ad1d0668f3d96f2213edb350629dd928bcb5", anchors: [ "mounted Charge, Shield Breaker, and Thrust scheduling (lines 339-357, 724-785)", "Mokra, Eressea, and Runok foot-combat schedules (lines 413-449, 790-926)", ], }, { path: TRIAL_OF_THE_CHAMPION_FILES[2], sha256: "45376430d55e09cb7c37e1faea5493b154c028cd539d87dba290ead8716afb81", anchors: [ "Eadric Vengeance and initial Radiance/Hammer schedule (lines 108-117)", "Radiance and Hammer repeat schedules (lines 156-180)", ], }, { path: TRIAL_OF_THE_CHAMPION_FILES[3], sha256: "e7d16a6973e3bfe56f4eaf70ee8f0bebddc193ee20f7dd8eafd0dd3b5fe1827a", anchors: [ "three phase spell sets and model ids (lines 28-82)", "lethal damage becomes resurrection for phases one/two (lines 122-146)", "skeleton then ghost transitions (lines 175-216)", "phase-specific initial and repeating combat schedules (lines 160-273)", ], }, { path: TRIAL_OF_THE_CHAMPION_FILES[4], sha256: "4e6078e4dea2c3de5910649d044bda941d2033968cd88e9c20e493d716d4da7e", anchors: [ "three encounter and ordered instance-progress states (lines 27-61)", "champion/minion/Argent/Black Knight ids (lines 104-147)", "main and arena gate ids (lines 167-181)", ], }, ], reviewedBehavior: "A deterministic valid Horde champion roster runs three 3-add opening waves, a mounted-combat proxy, the same three champions on foot, nine Argent soldiers, Eadric, and all three Black Knight forms.", adaptations: [ "Mokra, Eressea, and Runok are a deterministic valid three-of-five source roster because party faction/seed selection is not available in dungeon definitions.", "Mounted combat is a separate champion combat pass using ordinary actors with the source Charge, Shield Breaker, and Thrust schedules; defeating it advances to the source foot-combat pass without replacement mounts or player jousting.", "Eadric is the deterministic valid Argent Challenge branch and uses his source Vengeance, Radiance, and hammer schedule; Vengeance preserves the source 66865 -> 66889 chain, where each successful main-hand auto grants a six-second, one-charge +100% critical chance modifier for the next main-hand auto; the triggered Hammer of Justice plus Hammer of the Righteous pair is one compound damage/control release on the same target, collapsing the source's pre-cast stun staging into the hammer resolution.", "Black Knight resurrection/model changes are three finite combat actors at the exact source combat position, each with the source phase-specific spell schedule, preserving phase order without in-place creature transformation.", "The exact imported main/north gates are cloned as visible procedural blockers with reviewed state changes; the boxes are temporary geometry pending native models.", ], remainingGaps: [ "Player mounts, lance abilities, defend layers, mount replacement, faction-derived roster selection, mounted threat resets, and spectators require vehicle and presentation systems.", "Radiance's source facing check and blind/confuse wandering path are represented by an all-nearby generic confuse action lock; Eadric hammer throw-back, Black Knight disease/ground-area/stack exactness and ghoul AI, Paletress/Memory seeded selection, full announcer RP, the gryphon/announcer murder scene, achievements, and encounter chests remain unported.", ], }, ]); const REVIEW_BY_DUNGEON = new Map( WOW_WRATH_ENCOUNTER_CONTENT_REVIEWS.map((review) => [review.dungeonId, review] as const), ); const PACK_OFFSETS = Object.freeze([ [0, 0], [4, 0], [-4, 0], [0, 4], [0, -4], [4, 4], [-4, 4], [4, -4], [-4, -4], ] as const); function sourceMapPosition(source: readonly [x: number, y: number, z: number]): Vector3Tuple { return [source[1], source[2], source[0]]; } function assertSourceMapTransform(definition: DungeonDefinition): void { if (definition.provenance.coordinateTransform !== "three(x,y,z)=(wow.y,wow.z,wow.x)") { throw new Error( `Wrath encounter content for ${definition.id} requires three(x,y,z)=(wow.y,wow.z,wow.x), got ${definition.provenance.coordinateTransform}`, ); } } function entityIdForEntry(entities: PopulationDefinitionMap, entry: number): string | undefined { return Object.entries(entities).find(([, entity]) => entity.combat?.serverEntry === entry)?.[0]; } function createAdaptedEntity(spec: AdaptedEntitySpec): PopulationEntityDefinition { const caster = spec.role === "caster"; const spellProxy = spec.basicAttackStyle === "spell-proxy" || (spec.basicAttackStyle === undefined && caster); const base = { id: spec.id ?? `wrath-event-entry-${spec.entry}`, name: spec.name, hasLoot: spec.hasLoot ?? false, combat: { serverEntry: spec.entry, level: spec.level, healthMultiplier: spec.kind === "boss" ? 4.2 : caster ? 0.9 : 1.08, damageMultiplier: spec.kind === "boss" ? 1.18 : 0.92, moveSpeed: 3.65, aggroRange: 32, leashRange: 90, mechanicIds: Object.freeze((spec.attacks ?? []).map((attack) => attack.id)), attacks: Object.freeze([ { id: "basic-attack", name: spellProxy ? "Source Spell Proxy" : "Basic Attack", delivery: spellProxy ? "projectile" as const : "melee" as const, target: "primary" as const, school: spellProxy ? "arcane" as const : "physical" as const, animation: spellProxy ? "cast" as const : "attack" as const, range: spellProxy ? 28 : 4.5, cooldownMs: spellProxy ? 3_000 : 2_000, damageMultiplier: spellProxy ? 0.82 : 1, }, ...(spec.attacks ?? []), ]), }, visual: { primaryColor: caster ? "#536a8e" : "#684b54", accentColor: caster ? "#a8d7f4" : "#d7a08f", scale: spec.kind === "boss" ? 1.16 : 1, archetype: "humanoid" as const, }, }; if (spec.kind === "boss") { return Object.freeze({ ...base, kind: "boss" as const, title: spec.title ?? "Dungeon Boss" }); } return Object.freeze({ ...base, kind: "mob" as const }); } function ensureEntities( definition: DungeonDefinition, specs: readonly AdaptedEntitySpec[], ): PopulationDefinitionMap { const entities: Record = { ...definition.entities }; for (const spec of specs) { const id = spec.id ?? `wrath-event-entry-${spec.entry}`; if (entities[id]) continue; if (!spec.id) { const existing = entityIdForEntry(entities, spec.entry); if (existing) continue; } entities[id] = createAdaptedEntity(spec); } return Object.freeze(entities); } function entryForSpawn(definition: DungeonDefinition, spawn: StaticMobSpawnDefinition): number | undefined { return definition.entities[spawn.entityId]?.combat?.serverEntry; } function objectiveForEntry(definition: DungeonDefinition, entry: number): DungeonBossObjective { const spawnIds = new Set(definition.staticSpawns .filter((spawn) => entryForSpawn(definition, spawn) === entry) .map((spawn) => spawn.id)); const objective = definition.bosses.find((boss) => spawnIds.has(boss.id)); if (!objective) throw new Error(`${definition.id} is missing boss objective for creature entry ${entry}`); return objective; } function objectiveForName(definition: DungeonDefinition, name: string): DungeonBossObjective { const objective = definition.bosses.find((boss) => boss.name.toLowerCase() === name.toLowerCase()); if (!objective) throw new Error(`${definition.id} is missing boss objective ${name}`); return objective; } function spawnAt( id: string, entityId: string, center: Vector3Tuple, offset: readonly [x: number, z: number] = [0, 0], ): StaticMobSpawnDefinition { const position: Vector3Tuple = [center[0] + offset[0], center[1], center[2] + offset[1]]; return Object.freeze({ id, entityId, position, yaw: 0, spawnMask: 3, }); } function startObject( id: string, name: string, prompt: string, position: Vector3Tuple, sourceFiles: readonly string[], initialState: "ready" | "hidden" = "ready", ): DungeonWorldObjectDefinition { return Object.freeze({ id, name, kind: "npc", position, halfExtents: [0.8, 1.7, 0.8] as const, initialState, states: [ { id: "hidden", label: "Unavailable", visible: false, blocksMovement: false, interactionEnabled: false }, { id: "ready", label: "Ready", visible: true, blocksMovement: false, interactionEnabled: true }, { id: "active", label: "Event in progress", visible: true, blocksMovement: false, interactionEnabled: false }, { id: "complete", label: "Complete", visible: true, blocksMovement: false, interactionEnabled: false }, ], interaction: { prompt, range: 4, signalId: `${id}:start`, allowedStates: ["ready"], oneShot: true, }, visual: { color: "#a78bd4", emissive: "#302044" }, source: { fidelity: "source-backed-adapted" as const, files: sourceFiles, note: "A compact procedural interaction represents the source proximity/gossip start.", }, }); } function cloneReviewedDoors( definition: DungeonDefinition, templateIds: readonly number[], prefix: string, sourceFiles: readonly string[], ): readonly DungeonWorldObjectDefinition[] { const templates = new Set(templateIds); return Object.freeze(definition.serverGameObjects .filter((object) => object.source?.templateId !== undefined && templates.has(object.source.templateId)) .map((object, index): DungeonWorldObjectDefinition => { const source = object.source!; return Object.freeze({ ...object, id: `${prefix}-${index + 1}`, states: [ { id: "closed", label: "Closed", visible: true, blocksMovement: true, blocksSight: true, interactionEnabled: false }, { id: "open", label: "Open", visible: false, blocksMovement: false, blocksSight: false, interactionEnabled: false }, ], visual: { color: "#65758a", emissive: "#111b29" }, source: { ...source, fidelity: "source-backed-adapted" as const, files: sourceFiles, note: "Exact source transform and bounds are retained. A visible procedural blocker temporarily replaces the unexported client model.", }, }); })); } function withoutReviewedDoorSources( definition: DungeonDefinition, reviewedDoors: readonly DungeonWorldObjectDefinition[], ): readonly DungeonWorldObjectDefinition[] { const reviewedSpawnIds = new Set(reviewedDoors .map((door) => door.source?.spawnId) .filter((spawnId): spawnId is number => spawnId !== undefined)); return definition.serverGameObjects.filter( (object) => object.source?.spawnId === undefined || !reviewedSpawnIds.has(object.source.spawnId), ); } function authoredIceWall( index: number, position: Vector3Tuple, ): DungeonWorldObjectDefinition { return Object.freeze({ id: `halls-of-reflection:ice-wall-${index}`, name: `Ice Wall ${index}`, kind: "door", position, halfExtents: [7, 5, 1.2] as const, initialState: "hidden", states: [ { id: "hidden", label: "Not summoned", visible: false, blocksMovement: false, blocksSight: false }, { id: "closed", label: "Blocking the escape", visible: true, blocksMovement: true, blocksSight: true }, { id: "open", label: "Destroyed", visible: false, blocksMovement: false, blocksSight: false }, ], visual: { color: "#a6d7e8", emissive: "#244c61" }, source: { fidelity: "source-backed-adapted" as const, files: HALLS_OF_REFLECTION_FILES, templateId: 201_385, note: "Dynamic GO_ICE_WALL is represented by a procedural collision box at the exact source escort stop.", }, }); } function sourceReview( review: WowWrathEncounterContentReview, ): DungeonEncounterDefinition["source"] { return { fidelity: review.fidelity, scope: "instance-orchestration", files: review.sourceFiles.map((file) => `${file.path} (sha256 ${file.sha256})`), reviewedBehavior: review.reviewedBehavior, adaptations: review.adaptations, remainingGaps: review.remainingGaps, }; } function withoutBaselineBossProgression( definition: DungeonDefinition, ): readonly DungeonEncounterDefinition[] { return (definition.encounters ?? []).filter((encounter) => ( encounter.source?.scope !== "boss-progression" && encounter.id !== `${definition.id}:boss-progression` && encounter.id !== `${definition.id}:wrath-instance` )); } function appendValidationWarnings( definition: DungeonDefinition, warnings: readonly string[], ): DungeonDefinition["validation"] { return Object.freeze({ ...definition.validation, warnings: Object.freeze([...new Set([...definition.validation.warnings, ...warnings])]), }); } function deactivateAndWaitActions( prefix: string, groupId: string, nextPhase: string, ): DungeonEncounterAction[] { return [ { id: `${prefix}-deactivate`, kind: "deactivate-spawn-group", spawnGroupId: groupId }, { id: `${prefix}-wait`, kind: "set-phase", phase: nextPhase }, ]; } const HOR_WAVE_ENTRIES = Object.freeze({ priest: 38_175, mage: 38_172, mercenary: 38_177, footman: 38_173, rifleman: 38_176, }); const HOR_WAVE_COMPOSITIONS = Object.freeze([ [HOR_WAVE_ENTRIES.mercenary, HOR_WAVE_ENTRIES.priest, HOR_WAVE_ENTRIES.rifleman], [HOR_WAVE_ENTRIES.mage, HOR_WAVE_ENTRIES.rifleman, HOR_WAVE_ENTRIES.footman], [HOR_WAVE_ENTRIES.mercenary, HOR_WAVE_ENTRIES.priest, HOR_WAVE_ENTRIES.footman, HOR_WAVE_ENTRIES.footman], [HOR_WAVE_ENTRIES.mage, HOR_WAVE_ENTRIES.priest, HOR_WAVE_ENTRIES.footman, HOR_WAVE_ENTRIES.footman], [HOR_WAVE_ENTRIES.mercenary, HOR_WAVE_ENTRIES.mage, HOR_WAVE_ENTRIES.rifleman, HOR_WAVE_ENTRIES.footman, HOR_WAVE_ENTRIES.footman], [HOR_WAVE_ENTRIES.mercenary, HOR_WAVE_ENTRIES.mage, HOR_WAVE_ENTRIES.priest, HOR_WAVE_ENTRIES.rifleman, HOR_WAVE_ENTRIES.rifleman], [HOR_WAVE_ENTRIES.mercenary, HOR_WAVE_ENTRIES.mage, HOR_WAVE_ENTRIES.priest, HOR_WAVE_ENTRIES.rifleman, HOR_WAVE_ENTRIES.footman], [HOR_WAVE_ENTRIES.mercenary, HOR_WAVE_ENTRIES.mage, HOR_WAVE_ENTRIES.priest, HOR_WAVE_ENTRIES.footman, HOR_WAVE_ENTRIES.footman], ] as const); const HOR_ESCAPE_STOPS = Object.freeze([ sourceMapPosition([5558.182617, 2103.950928, 731.263]), sourceMapPosition([5513.573242, 1996.611206, 735.115723]), sourceMapPosition([5456.632324, 1902.801025, 747.220886]), sourceMapPosition([5340.560059, 1772.791016, 766.478149]), ]); const HOR_ESCAPE_COMPOSITIONS = Object.freeze([ [36_940, 36_940, 36_940, 36_940, 36_940, 36_940, 36_941], [37_069, 36_940, 36_940, 36_940, 36_940, 36_940, 36_940, 36_941, 36_941], [37_069, 37_069, 36_940, 36_940, 36_940, 36_940, 36_940, 36_940, 36_941, 36_941], [37_069, 37_069, 37_069, 36_940, 36_940, 36_940, 36_940, 36_940, 36_940, 36_941, 36_941, 36_941, 36_941], ] as const); function buildHallsOfReflection(definition: DungeonDefinition): DungeonDefinition { assertSourceMapTransform(definition); const review = REVIEW_BY_DUNGEON.get("halls-of-reflection")!; const falric = objectiveForEntry(definition, 38_112); const marwyn = objectiveForEntry(definition, 38_113); const waveStart = startObject( "halls-of-reflection:wave-start", "The Hall's Restless Spirits", "Begin the reflection waves", sourceMapPosition([5309.459473, 2006.478516, 711.595459]), HALLS_OF_REFLECTION_FILES, ); const escapeStart = startObject( "halls-of-reflection:escape-start", "Jaina / Sylvanas", "Begin the escape from the Lich King", sourceMapPosition([5576.80566, 2235.55004, 733.012268]), HALLS_OF_REFLECTION_FILES, "hidden", ); const doors = cloneReviewedDoors( definition, [201_976, 197_341], "halls-of-reflection:reviewed-door", HALLS_OF_REFLECTION_FILES, ); const frontDoors = doors.filter((door) => door.source?.templateId === 201_976); const arthasDoors = doors.filter((door) => door.source?.templateId === 197_341); const iceWalls = HOR_ESCAPE_STOPS.map((position, index) => authoredIceWall(index + 1, position)); const entities = ensureEntities(definition, [ { entry: 38_175, name: "Phantom Priest", level: 80, role: "caster" }, { entry: 38_172, name: "Phantom Mage", level: 80, role: "caster" }, { entry: 38_177, name: "Shadowy Mercenary", level: 80, role: "melee" }, { entry: 38_173, name: "Spectral Footman", level: 80, role: "melee" }, { entry: 38_176, name: "Tortured Rifleman", level: 80, role: "caster" }, { entry: 36_940, name: "Raging Ghoul", level: 80, role: "melee" }, { entry: 36_941, name: "Risen Witch Doctor", level: 80, role: "caster" }, { entry: 37_069, name: "Lumbering Abomination", level: 80, role: "melee" }, ]); const idFor = (entry: number): string => { const id = entityIdForEntry(entities, entry); if (!id) throw new Error(`halls-of-reflection is missing event creature entry ${entry}`); return id; }; const groups: DungeonEncounterSpawnGroupDefinition[] = []; const waveGroupIds: string[] = []; const hallCenter = sourceMapPosition([5309.459473, 2006.478516, 711.595459]); HOR_WAVE_COMPOSITIONS.forEach((entries, index) => { const sourceWave = index < 4 ? index + 1 : index + 2; const groupId = `halls-of-reflection:wave-${String(sourceWave).padStart(2, "0")}`; waveGroupIds.push(groupId); groups.push(Object.freeze({ id: groupId, spawns: Object.freeze(entries.map((entry, memberIndex) => spawnAt( `${groupId}:mob-${memberIndex + 1}`, idFor(entry), hallCenter, PACK_OFFSETS[memberIndex], ))), })); }); const falricGroupId = "halls-of-reflection:falric"; const marwynGroupId = "halls-of-reflection:marwyn"; groups.push( Object.freeze({ id: falricGroupId, spawns: Object.freeze([spawnAt(falric.id, idFor(38_112), falric.position)]) }), Object.freeze({ id: marwynGroupId, spawns: Object.freeze([spawnAt(marwyn.id, idFor(38_113), marwyn.position)]) }), ); const generalSpawns = definition.staticSpawns.filter((spawn) => { const entry = entryForSpawn(definition, spawn); return entry === 36_723 || entry === 37_068; }); if (!generalSpawns.some((spawn) => entryForSpawn(definition, spawn) === 36_723)) { throw new Error("halls-of-reflection is missing the source Frostsworn General spawn"); } const generalGroupId = "halls-of-reflection:frostsworn-general"; groups.push(Object.freeze({ id: generalGroupId, spawns: Object.freeze(generalSpawns) })); const escapeGroupIds: string[] = []; HOR_ESCAPE_COMPOSITIONS.forEach((entries, index) => { const groupId = `halls-of-reflection:escape-${index + 1}`; escapeGroupIds.push(groupId); groups.push(Object.freeze({ id: groupId, spawns: Object.freeze(entries.map((entry, memberIndex) => spawnAt( `${groupId}:mob-${memberIndex + 1}`, idFor(entry), HOR_ESCAPE_STOPS[index], PACK_OFFSETS[memberIndex % PACK_OFFSETS.length], ))), })); }); const escapeObjective: DungeonBossObjective = Object.freeze({ id: "halls-of-reflection:escape-objective", name: "Escape from the Lich King", position: sourceMapPosition([5262.773926, 1669.980103, 784.301697]), eventOnly: true, }); const updatedFalric = Object.freeze({ ...falric, eventOnly: true }); const updatedMarwyn = Object.freeze({ ...marwyn, eventOnly: true }); const objectiveReplacements = new Map([ [falric.id, updatedFalric], [marwyn.id, updatedMarwyn], ]); const rules: DungeonEncounterRule[] = [ { id: "start-reflection-waves", trigger: { kind: "interact", objectId: waveStart.id }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "mark-wave-start-active", kind: "set-world-object-state", objectId: waveStart.id, state: "active" }, ...frontDoors.map((door, index) => ({ id: `close-front-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "closed" })), { id: "activate-hor-wave-01", kind: "activate-spawn-group", spawnGroupId: waveGroupIds[0] }, { id: "enter-hor-wave-01", kind: "set-phase", phase: "wave-01-active" }, ], }, { id: "resume-hor-fresh", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: falric.id, equals: false }, ], actions: [ { id: "resume-mark-wave-start-active", kind: "set-world-object-state", objectId: waveStart.id, state: "active" }, ...frontDoors.map((door, index) => ({ id: `resume-close-front-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "closed" })), { id: "resume-activate-hor-wave-01", kind: "activate-spawn-group", spawnGroupId: waveGroupIds[0] }, { id: "resume-enter-hor-wave-01", kind: "set-phase", phase: "wave-01-active" }, ], }, { id: "resume-hor-after-falric", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: falric.id, equals: true }, { kind: "objective-complete", objectiveId: marwyn.id, equals: false }, ], actions: [ { id: "resume-falric-mark-wave-start-active", kind: "set-world-object-state", objectId: waveStart.id, state: "active" }, ...frontDoors.map((door, index) => ({ id: `resume-falric-close-front-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "closed" })), { id: "resume-activate-hor-wave-06", kind: "activate-spawn-group", spawnGroupId: waveGroupIds[4] }, { id: "resume-enter-hor-wave-06", kind: "set-phase", phase: "wave-06-active" }, ], }, { id: "resume-hor-general", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: marwyn.id, equals: true }, { kind: "flag", flagId: "frostsworn-general-complete", equals: false }, ], actions: [ { id: "resume-mark-wave-start-complete", kind: "set-world-object-state", objectId: waveStart.id, state: "complete" }, ...frontDoors.map((door, index) => ({ id: `resume-open-front-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "open" })), ...arthasDoors.map((door, index) => ({ id: `resume-open-arthas-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "open" })), { id: "resume-activate-frostsworn-general", kind: "activate-spawn-group", spawnGroupId: generalGroupId }, { id: "resume-enter-frostsworn-general", kind: "set-phase", phase: "general-active" }, ], }, { id: "resume-hor-escape", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "flag", flagId: "frostsworn-general-complete", equals: true }, { kind: "objective-complete", objectiveId: escapeObjective.id, equals: false }, ], actions: [ { id: "resume-ready-escape-start", kind: "set-world-object-state", objectId: escapeStart.id, state: "ready" }, { id: "resume-enter-escape-ready", kind: "set-phase", phase: "escape-ready" }, ], }, ]; const sourceWaveNumbers = [1, 2, 3, 4, 6, 7, 8, 9] as const; sourceWaveNumbers.forEach((wave, index) => { const groupId = waveGroupIds[index]; const group = groups.find((candidate) => candidate.id === groupId)!; const bossNext = wave === 4 || wave === 9; const nextWave = wave === 4 ? 5 : wave === 9 ? 10 : wave + 1; const nextPhase = `wave-${String(nextWave).padStart(2, "0")}-wait`; rules.push({ id: `clear-hor-wave-${String(wave).padStart(2, "0")}`, trigger: { kind: "entities-dead", entityIds: group.spawns.map((spawn) => spawn.id), timeoutMs: 600_000 }, guards: [{ kind: "phase", phases: [`wave-${String(wave).padStart(2, "0")}-active`] }], actions: deactivateAndWaitActions(`clear-hor-wave-${String(wave).padStart(2, "0")}`, groupId, nextPhase), }); const nextGroupId = bossNext ? (wave === 4 ? falricGroupId : marwynGroupId) : waveGroupIds[index + 1]; rules.push({ id: `start-hor-wave-${String(nextWave).padStart(2, "0")}`, trigger: { kind: "elapsed", afterMs: 5_000 }, guards: [{ kind: "phase", phases: [nextPhase] }], actions: [ { id: `activate-hor-wave-${String(nextWave).padStart(2, "0")}`, kind: "activate-spawn-group", spawnGroupId: nextGroupId }, { id: `enter-hor-wave-${String(nextWave).padStart(2, "0")}`, kind: "set-phase", phase: `wave-${String(nextWave).padStart(2, "0")}-active` }, ], }); }); rules.push( { id: "clear-falric", trigger: { kind: "entities-dead", entityIds: [falric.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["wave-05-active"] }], actions: [ { id: "deactivate-falric", kind: "deactivate-spawn-group", spawnGroupId: falricGroupId }, { id: "complete-falric", kind: "complete-objective", objectiveId: falric.id, persistence: "run" }, { id: "checkpoint-falric", kind: "set-checkpoint", checkpointId: falric.id, persistence: "checkpoint" }, { id: "wait-source-falric-pause", kind: "set-phase", phase: "wave-06-source-pause" }, ], }, { id: "finish-source-falric-pause", trigger: { kind: "elapsed", afterMs: 60_000 }, guards: [{ kind: "phase", phases: ["wave-06-source-pause"] }], actions: [ { id: "activate-post-falric-wave-06", kind: "activate-spawn-group", spawnGroupId: waveGroupIds[4] }, { id: "enter-post-falric-wave-06", kind: "set-phase", phase: "wave-06-active" }, ], }, { id: "clear-marwyn", trigger: { kind: "entities-dead", entityIds: [marwyn.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["wave-10-active"] }], actions: [ { id: "deactivate-marwyn", kind: "deactivate-spawn-group", spawnGroupId: marwynGroupId }, { id: "complete-marwyn", kind: "complete-objective", objectiveId: marwyn.id, persistence: "run" }, { id: "checkpoint-marwyn", kind: "set-checkpoint", checkpointId: marwyn.id, persistence: "checkpoint" }, { id: "complete-wave-start", kind: "set-world-object-state", objectId: waveStart.id, state: "complete", persistence: "checkpoint" }, ...frontDoors.map((door, index) => ({ id: `open-front-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "open", persistence: "checkpoint" as const })), ...arthasDoors.map((door, index) => ({ id: `open-arthas-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "open", persistence: "checkpoint" as const })), { id: "activate-frostsworn-general", kind: "activate-spawn-group", spawnGroupId: generalGroupId }, { id: "enter-frostsworn-general", kind: "set-phase", phase: "general-active" }, ], }, { id: "clear-frostsworn-general", trigger: { kind: "entities-dead", entityIds: generalSpawns.map((spawn) => spawn.id), timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["general-active"] }], actions: [ { id: "deactivate-frostsworn-general", kind: "deactivate-spawn-group", spawnGroupId: generalGroupId }, { id: "mark-frostsworn-general-complete", kind: "set-flag", flagId: "frostsworn-general-complete", value: true, persistence: "checkpoint" }, { id: "checkpoint-frostsworn-general", kind: "set-checkpoint", checkpointId: generalGroupId, persistence: "checkpoint" }, { id: "ready-escape-start", kind: "set-world-object-state", objectId: escapeStart.id, state: "ready", persistence: "checkpoint" }, { id: "enter-escape-ready", kind: "set-phase", phase: "escape-ready" }, ], }, { id: "start-lich-king-escape", trigger: { kind: "interact", objectId: escapeStart.id }, guards: [{ kind: "phase", phases: ["escape-ready"] }], actions: [ { id: "mark-escape-start-active", kind: "set-world-object-state", objectId: escapeStart.id, state: "active" }, { id: "close-escape-wall-1", kind: "set-world-object-state", objectId: iceWalls[0].id, state: "closed" }, { id: "activate-escape-wall-1", kind: "activate-spawn-group", spawnGroupId: escapeGroupIds[0] }, { id: "teleport-to-escape-wall-1", kind: "teleport-party", destination: HOR_ESCAPE_STOPS[0] }, { id: "enter-escape-wall-1", kind: "set-phase", phase: "escape-1-active" }, ], }, ); escapeGroupIds.forEach((groupId, index) => { const wall = index + 1; const group = groups.find((candidate) => candidate.id === groupId)!; const final = wall === escapeGroupIds.length; const actions: DungeonEncounterAction[] = [ { id: `deactivate-escape-wall-${wall}`, kind: "deactivate-spawn-group", spawnGroupId: groupId }, { id: `open-escape-wall-${wall}`, kind: "set-world-object-state", objectId: iceWalls[index].id, state: "open" }, ]; if (final) { actions.push( { id: "complete-lich-king-escape", kind: "complete-objective", objectiveId: escapeObjective.id, persistence: "run" }, { id: "checkpoint-lich-king-escape", kind: "set-checkpoint", checkpointId: escapeObjective.id, persistence: "run" }, { id: "mark-halls-of-reflection-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "mark-escape-start-complete", kind: "set-world-object-state", objectId: escapeStart.id, state: "complete", persistence: "run" }, { id: "enter-halls-of-reflection-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ); } else { actions.push({ id: `wait-for-escape-wall-${wall + 1}`, kind: "set-phase", phase: `escape-${wall + 1}-wait` }); } rules.push({ id: `clear-escape-wall-${wall}`, trigger: { kind: "entities-dead", entityIds: group.spawns.map((spawn) => spawn.id), timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: [`escape-${wall}-active`] }], actions, }); if (!final) { const nextIndex = index + 1; const delayMs = wall === 3 ? 11_000 : 7_500; rules.push({ id: `start-escape-wall-${wall + 1}`, trigger: { kind: "elapsed", afterMs: delayMs }, guards: [{ kind: "phase", phases: [`escape-${wall + 1}-wait`] }], actions: [ { id: `close-escape-wall-${wall + 1}`, kind: "set-world-object-state", objectId: iceWalls[nextIndex].id, state: "closed" }, { id: `activate-escape-wall-${wall + 1}`, kind: "activate-spawn-group", spawnGroupId: escapeGroupIds[nextIndex] }, { id: `teleport-to-escape-wall-${wall + 1}`, kind: "teleport-party", destination: HOR_ESCAPE_STOPS[nextIndex] }, { id: `enter-escape-wall-${wall + 1}`, kind: "set-phase", phase: `escape-${wall + 1}-active` }, ], }); } }); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "halls-of-reflection:wrath-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { flags: { "frostsworn-general-complete": false, "dungeon-complete": false }, worldObjectStates: Object.fromEntries([ [waveStart.id, "ready"], [escapeStart.id, "hidden"], ...frontDoors.map((door) => [door.id, "open"] as const), ...arthasDoors.map((door) => [door.id, "closed"] as const), ...iceWalls.map((wall) => [wall.id, "hidden"] as const), ]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review), rules: Object.freeze(rules), }); const gatedEntries = new Set([36_723, 36_954, 37_068, 37_225, 37_226, 37_554, 37_779, 38_112, 38_113]); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => !gatedEntries.has(entryForSpawn(definition, spawn) ?? -1))), bosses: Object.freeze([ ...definition.bosses.map((boss) => objectiveReplacements.get(boss.id) ?? boss), escapeObjective, ]), objectiveOrder: Object.freeze([ ...definition.objectiveOrder.map((boss) => objectiveReplacements.get(boss.id) ?? boss), escapeObjective, ]), encounters: Object.freeze([...withoutBaselineBossProgression(definition), encounter]), encounterSpawnGroups: Object.freeze([ ...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("halls-of-reflection:")), ...groups, ]), serverGameObjects: Object.freeze([ ...withoutReviewedDoorSources(definition, doors), waveStart, escapeStart, ...doors, ...iceWalls, ]), validation: appendValidationWarnings(definition, [ "Halls of Reflection now has finite source-ordered wave, boss, General, and four-wall escape orchestration; escort pursuit is explicitly adapted to checkpoint-safe party transitions.", ]), }); } const UROM_MENAGERIES = Object.freeze([ [27_650, 27_650, 27_653, 27_651], [27_647, 27_647, 27_648, 27_649], [27_645, 27_645, 27_642, 27_644], ] as const); const UROM_PLATFORM_POSITIONS = Object.freeze([ sourceMapPosition([1177.47, 937.722, 527.405]), sourceMapPosition([968.66, 1042.53, 527.32]), sourceMapPosition([1164.02, 1170.85, 527.321]), ]); const UROM_FINAL_POSITION = sourceMapPosition([1118.31, 1080.377, 508.361]); function buildOculus(definition: DungeonDefinition): DungeonDefinition { assertSourceMapTransform(definition); const review = REVIEW_BY_DUNGEON.get("the-oculus")!; const drakos = objectiveForEntry(definition, 27_654); const varos = objectiveForEntry(definition, 27_447); const urom = objectiveForEntry(definition, 27_655); const eregos = objectiveForEntry(definition, 27_656); const start = startObject( "the-oculus:expedition-start", "Oculus Expedition", "Begin the ascent through the Oculus", definition.entrance.footPosition, OCULUS_FILES, ); const cageDoors = cloneReviewedDoors(definition, [193_995], "the-oculus:reviewed-cage-door", OCULUS_FILES); if (cageDoors.length !== 3) throw new Error(`the-oculus expected 3 source dragon cage doors, found ${cageDoors.length}`); const entities = ensureEntities(definition, [ { entry: 27_650, name: "Phantasmal Air", level: 80, role: "caster" }, { entry: 27_651, name: "Phantasmal Fire", level: 80, role: "caster" }, { entry: 27_653, name: "Phantasmal Water", level: 80, role: "caster" }, { entry: 27_647, name: "Phantasmal Ogre", level: 80, role: "melee" }, { entry: 27_648, name: "Phantasmal Naga", level: 80, role: "melee" }, { entry: 27_649, name: "Phantasmal Murloc", level: 80, role: "caster" }, { entry: 27_645, name: "Phantasmal Cloudscraper", level: 80, role: "melee" }, { entry: 27_642, name: "Phantasmal Mammoth", level: 80, role: "melee" }, { entry: 27_644, name: "Phantasmal Wolf", level: 80, role: "melee" }, ]); const idFor = (entry: number): string => { const id = entityIdForEntry(entities, entry); if (!id) throw new Error(`the-oculus is missing event creature entry ${entry}`); return id; }; const constructs = definition.staticSpawns.filter((spawn) => entryForSpawn(definition, spawn) === 27_641); if (constructs.length !== 10) throw new Error(`the-oculus expected 10 Centrifuge Constructs, found ${constructs.length}`); const groups: DungeonEncounterSpawnGroupDefinition[] = [ Object.freeze({ id: "the-oculus:drakos", spawns: Object.freeze([spawnAt(drakos.id, idFor(27_654), drakos.position)]) }), Object.freeze({ id: "the-oculus:centrifuge-constructs", spawns: Object.freeze(constructs) }), Object.freeze({ id: "the-oculus:varos", spawns: Object.freeze([spawnAt(varos.id, idFor(27_447), varos.position)]) }), ]; UROM_MENAGERIES.forEach((entries, index) => { const groupId = `the-oculus:urom-menagerie-${index + 1}`; groups.push(Object.freeze({ id: groupId, spawns: Object.freeze(entries.map((entry, memberIndex) => spawnAt( `${groupId}:mob-${memberIndex + 1}`, idFor(entry), UROM_PLATFORM_POSITIONS[index], [memberIndex % 2 ? 4 : -4, memberIndex < 2 ? 4 : -4], ))), })); }); groups.push( Object.freeze({ id: "the-oculus:urom", spawns: Object.freeze([spawnAt(urom.id, idFor(27_655), UROM_FINAL_POSITION)]) }), Object.freeze({ id: "the-oculus:eregos", spawns: Object.freeze([spawnAt(eregos.id, idFor(27_656), eregos.position)]) }), ); const updatedObjectives = new Map([ [drakos.id, Object.freeze({ ...drakos, eventOnly: true })], [varos.id, Object.freeze({ ...varos, eventOnly: true })], [urom.id, Object.freeze({ ...urom, position: UROM_FINAL_POSITION, eventOnly: true })], [eregos.id, Object.freeze({ ...eregos, eventOnly: true })], ]); const rules: DungeonEncounterRule[] = [ { id: "start-oculus-expedition", trigger: { kind: "interact", objectId: start.id }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "mark-oculus-start-active", kind: "set-world-object-state", objectId: start.id, state: "active" }, { id: "activate-drakos", kind: "activate-spawn-group", spawnGroupId: "the-oculus:drakos" }, { id: "enter-drakos", kind: "set-phase", phase: "drakos-active" }, ], }, { id: "resume-oculus-drakos", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: drakos.id, equals: false }, ], actions: [ { id: "resume-mark-oculus-start-active", kind: "set-world-object-state", objectId: start.id, state: "active" }, { id: "resume-activate-drakos", kind: "activate-spawn-group", spawnGroupId: "the-oculus:drakos" }, { id: "resume-enter-drakos", kind: "set-phase", phase: "drakos-active" }, ], }, { id: "resume-oculus-constructs", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: drakos.id, equals: true }, { kind: "objective-complete", objectiveId: varos.id, equals: false }, ], actions: [ { id: "resume-activate-constructs", kind: "activate-spawn-group", spawnGroupId: "the-oculus:centrifuge-constructs" }, { id: "resume-teleport-construct-ring", kind: "teleport-party", destination: constructs[0].position }, { id: "resume-enter-constructs", kind: "set-phase", phase: "constructs-active" }, ], }, { id: "resume-oculus-urom-menageries", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: varos.id, equals: true }, { kind: "objective-complete", objectiveId: urom.id, equals: false }, ], actions: [ { id: "resume-activate-urom-menagerie-1", kind: "activate-spawn-group", spawnGroupId: "the-oculus:urom-menagerie-1" }, { id: "resume-teleport-urom-platform-1", kind: "teleport-party", destination: UROM_PLATFORM_POSITIONS[0] }, { id: "resume-enter-urom-menagerie-1", kind: "set-phase", phase: "urom-menagerie-1-active" }, ], }, { id: "resume-oculus-eregos", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: urom.id, equals: true }, { kind: "objective-complete", objectiveId: eregos.id, equals: false }, ], actions: [ { id: "resume-activate-eregos", kind: "activate-spawn-group", spawnGroupId: "the-oculus:eregos" }, { id: "resume-teleport-eregos", kind: "teleport-party", destination: eregos.position }, { id: "resume-enter-eregos", kind: "set-phase", phase: "eregos-active" }, ], }, { id: "clear-drakos", trigger: { kind: "entities-dead", entityIds: [drakos.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["drakos-active"] }], actions: [ { id: "deactivate-drakos", kind: "deactivate-spawn-group", spawnGroupId: "the-oculus:drakos" }, { id: "complete-drakos", kind: "complete-objective", objectiveId: drakos.id, persistence: "run" }, { id: "checkpoint-drakos", kind: "set-checkpoint", checkpointId: drakos.id, persistence: "checkpoint" }, ...cageDoors.map((door, index) => ({ id: `open-oculus-cage-door-${index + 1}`, kind: "set-world-object-state" as const, objectId: door.id, state: "open", persistence: "checkpoint" as const })), { id: "activate-centrifuge-constructs", kind: "activate-spawn-group", spawnGroupId: "the-oculus:centrifuge-constructs" }, { id: "teleport-to-construct-ring", kind: "teleport-party", destination: constructs[0].position }, { id: "enter-centrifuge-constructs", kind: "set-phase", phase: "constructs-active" }, ], }, { id: "clear-centrifuge-constructs", trigger: { kind: "entities-dead", entityIds: constructs.map((spawn) => spawn.id), timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["constructs-active"] }], actions: [ { id: "deactivate-centrifuge-constructs", kind: "deactivate-spawn-group", spawnGroupId: "the-oculus:centrifuge-constructs" }, { id: "activate-varos", kind: "activate-spawn-group", spawnGroupId: "the-oculus:varos" }, { id: "teleport-to-varos", kind: "teleport-party", destination: varos.position }, { id: "enter-varos", kind: "set-phase", phase: "varos-active" }, ], }, { id: "clear-varos", trigger: { kind: "entities-dead", entityIds: [varos.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["varos-active"] }], actions: [ { id: "deactivate-varos", kind: "deactivate-spawn-group", spawnGroupId: "the-oculus:varos" }, { id: "complete-varos", kind: "complete-objective", objectiveId: varos.id, persistence: "run" }, { id: "checkpoint-varos", kind: "set-checkpoint", checkpointId: varos.id, persistence: "checkpoint" }, { id: "activate-urom-menagerie-1", kind: "activate-spawn-group", spawnGroupId: "the-oculus:urom-menagerie-1" }, { id: "teleport-to-urom-platform-1", kind: "teleport-party", destination: UROM_PLATFORM_POSITIONS[0] }, { id: "enter-urom-menagerie-1", kind: "set-phase", phase: "urom-menagerie-1-active" }, ], }, ]; UROM_MENAGERIES.forEach((_, index) => { const platform = index + 1; const groupId = `the-oculus:urom-menagerie-${platform}`; const group = groups.find((candidate) => candidate.id === groupId)!; const final = platform === UROM_MENAGERIES.length; rules.push({ id: `clear-urom-menagerie-${platform}`, trigger: { kind: "entities-dead", entityIds: group.spawns.map((spawn) => spawn.id), timeoutMs: 600_000 }, guards: [{ kind: "phase", phases: [`urom-menagerie-${platform}-active`] }], actions: final ? [ { id: "deactivate-urom-menagerie-3", kind: "deactivate-spawn-group", spawnGroupId: groupId }, { id: "activate-urom", kind: "activate-spawn-group", spawnGroupId: "the-oculus:urom" }, { id: "teleport-to-urom-final", kind: "teleport-party", destination: UROM_FINAL_POSITION }, { id: "enter-urom", kind: "set-phase", phase: "urom-active" }, ] : [ { id: `deactivate-urom-menagerie-${platform}`, kind: "deactivate-spawn-group", spawnGroupId: groupId }, { id: `activate-urom-menagerie-${platform + 1}`, kind: "activate-spawn-group", spawnGroupId: `the-oculus:urom-menagerie-${platform + 1}` }, { id: `teleport-to-urom-platform-${platform + 1}`, kind: "teleport-party", destination: UROM_PLATFORM_POSITIONS[index + 1] }, { id: `enter-urom-menagerie-${platform + 1}`, kind: "set-phase", phase: `urom-menagerie-${platform + 1}-active` }, ], }); }); rules.push( { id: "clear-urom", trigger: { kind: "entities-dead", entityIds: [urom.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["urom-active"] }], actions: [ { id: "deactivate-urom", kind: "deactivate-spawn-group", spawnGroupId: "the-oculus:urom" }, { id: "complete-urom", kind: "complete-objective", objectiveId: urom.id, persistence: "run" }, { id: "checkpoint-urom", kind: "set-checkpoint", checkpointId: urom.id, persistence: "checkpoint" }, { id: "activate-eregos", kind: "activate-spawn-group", spawnGroupId: "the-oculus:eregos" }, { id: "teleport-to-eregos", kind: "teleport-party", destination: eregos.position }, { id: "enter-eregos", kind: "set-phase", phase: "eregos-active" }, ], }, { id: "clear-eregos", trigger: { kind: "entities-dead", entityIds: [eregos.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["eregos-active"] }], actions: [ { id: "deactivate-eregos", kind: "deactivate-spawn-group", spawnGroupId: "the-oculus:eregos" }, { id: "complete-eregos", kind: "complete-objective", objectiveId: eregos.id, persistence: "run" }, { id: "checkpoint-eregos", kind: "set-checkpoint", checkpointId: eregos.id, persistence: "run" }, { id: "mark-oculus-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "mark-oculus-start-complete", kind: "set-world-object-state", objectId: start.id, state: "complete", persistence: "run" }, { id: "enter-oculus-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ], }, ); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "the-oculus:wrath-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { flags: { "dungeon-complete": false }, worldObjectStates: Object.fromEntries([ [start.id, "ready"], ...cageDoors.map((door) => [door.id, "closed"] as const), ]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review), rules: Object.freeze(rules), }); const gatedEntries = new Set([27_447, 27_641, 27_654, 27_655, 27_656]); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => !gatedEntries.has(entryForSpawn(definition, spawn) ?? -1))), bosses: Object.freeze(definition.bosses.map((boss) => updatedObjectives.get(boss.id) ?? boss)), objectiveOrder: Object.freeze(definition.objectiveOrder.map((boss) => updatedObjectives.get(boss.id) ?? boss)), encounters: Object.freeze([...withoutBaselineBossProgression(definition), encounter]), encounterSpawnGroups: Object.freeze([ ...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("the-oculus:")), ...groups, ]), serverGameObjects: Object.freeze([ ...withoutReviewedDoorSources(definition, cageDoors), start, ...cageDoors, ]), validation: appendValidationWarnings(definition, [ "The Oculus now gates Drakos, all ten source constructs, Varos, three exact Urom menageries, Urom, and Eregos; source platform travel is a deterministic party-teleport adaptation.", ]), }); } const TOC_MOUNTED_ATTACKS: readonly EnemyAttackDefinition[] = Object.freeze([ Object.freeze({ id: "mounted-charge-63010", name: "Mounted Charge", spellId: 63_010, delivery: "melee", target: "random-party", school: "physical", animation: "attack", range: 25, cooldownMs: 5_250, initialCooldownMs: [2_500, 4_000] as const, repeatCooldownMs: [4_500, 6_000] as const, damageMultiplier: 1.15, telegraph: { shape: "line", anchor: "source", warningMs: 750, length: 25, width: 3 } as const, }), Object.freeze({ id: "shield-breaker-68504", name: "Shield Breaker", spellId: 68_504, delivery: "projectile", target: "random-party", school: "physical", animation: "attack", range: 30, cooldownMs: 7_000, initialCooldownMs: [5_000, 8_000] as const, repeatCooldownMs: [6_000, 8_000] as const, damageMultiplier: 0.65, }), Object.freeze({ id: "thrust-68505", name: "Thrust", spellId: 68_505, delivery: "melee", target: "primary", school: "physical", animation: "attack", range: 6, cooldownMs: 4_000, initialCooldownMs: [3_000, 5_000] as const, repeatCooldownMs: [3_000, 5_000] as const, damageMultiplier: 0.8, }), ]); const EADRIC_ATTACKS: readonly EnemyAttackDefinition[] = Object.freeze([ Object.freeze({ id: "vengeance-66865", name: "Vengeance", spellId: 66_865, delivery: "area", target: "self", school: "holy", animation: "cast", castTimeMs: 0, range: 0, cooldownMs: 1, initialCooldownMs: [0, 0] as const, repeatCooldownMs: [1, 1] as const, maximumCasts: 1, damageMultiplier: 0, effect: { kind: "aura", aura: "vengeance", durationMs: 0, meleeAutoCriticalProc: { chance: 1, triggeredAura: "vengeance-next-melee-critical-66889", durationMs: 6_000, criticalChancePercent: 100, charges: 1, }, } as const, }), Object.freeze({ id: "radiance-66935", name: "Radiance", spellId: 66_935, delivery: "area", target: "nearby-party", school: "holy", animation: "cast", range: 45, radius: 45, cooldownMs: 16_000, initialCooldownMs: [16_000, 16_000] as const, repeatCooldownMs: [16_000, 16_000] as const, damageMultiplier: 0.9, effect: { kind: "control", mechanic: "confuse", durationMs: 2_000, damageMultiplier: 0.9 } as const, telegraph: { shape: "circle", anchor: "source", warningMs: 1_500, radius: 45 } as const, }), Object.freeze({ id: "hammer-of-justice-and-righteous-66863-66867", name: "Hammer of Justice + Hammer of the Righteous", spellId: 66_867, delivery: "projectile", target: "random-party", school: "holy", animation: "cast", range: 55, cooldownMs: 25_000, initialCooldownMs: [25_000, 25_000] as const, repeatCooldownMs: [25_000, 25_000] as const, damageMultiplier: 1.3, effect: { kind: "control", mechanic: "stun", durationMs: 6_000, damageMultiplier: 1.3 } as const, }), ]); const BLACK_KNIGHT_PHASE_ONE_ATTACKS: readonly EnemyAttackDefinition[] = Object.freeze([ Object.freeze({ id: "plague-strike-67724", name: "Plague Strike", spellId: 67_724, delivery: "melee", target: "primary", school: "physical", animation: "attack", range: 5, cooldownMs: 11_000, initialCooldownMs: [7_000, 9_000] as const, repeatCooldownMs: [10_000, 12_000] as const, damageMultiplier: 0.85 }), Object.freeze({ id: "icy-touch-67718", name: "Icy Touch", spellId: 67_718, delivery: "projectile", target: "primary", school: "frost", animation: "cast", range: 30, cooldownMs: 5_500, initialCooldownMs: [3_500, 7_000] as const, repeatCooldownMs: [5_000, 6_000] as const, damageMultiplier: 0.75 }), Object.freeze({ id: "death-respite-67745", name: "Death's Respite", spellId: 67_745, delivery: "projectile", target: "random-party", school: "shadow", animation: "cast", range: 50, cooldownMs: 14_000, initialCooldownMs: [13_000, 15_000] as const, repeatCooldownMs: [13_000, 15_000] as const, damageMultiplier: 1.05 }), Object.freeze({ id: "obliterate-67725", name: "Obliterate", spellId: 67_725, delivery: "melee", target: "primary", school: "physical", animation: "attack", range: 5, cooldownMs: 16_000, initialCooldownMs: [11_000, 19_000] as const, repeatCooldownMs: [15_000, 17_000] as const, damageMultiplier: 1.35 }), ]); const BLACK_KNIGHT_PHASE_TWO_ATTACKS: readonly EnemyAttackDefinition[] = Object.freeze([ ...BLACK_KNIGHT_PHASE_ONE_ATTACKS.filter((attack) => attack.id !== "death-respite-67745"), Object.freeze({ id: "desecration-67778", name: "Desecration", spellId: 67_778, delivery: "area", target: "random-party", school: "shadow", animation: "cast", range: 50, radius: 7, cooldownMs: 15_500, initialCooldownMs: [2_000, 3_000] as const, repeatCooldownMs: [14_000, 17_000] as const, damageMultiplier: 0.95, telegraph: { shape: "circle", anchor: "target", warningMs: 1_500, radius: 7 } as const, }), ]); const BLACK_KNIGHT_PHASE_THREE_ATTACKS: readonly EnemyAttackDefinition[] = Object.freeze([ Object.freeze({ id: "death-bite-67808", name: "Death's Bite", spellId: 67_808, delivery: "area", target: "nearby-party", school: "shadow", animation: "cast", range: 45, radius: 45, cooldownMs: 3_000, initialCooldownMs: [2_000, 2_000] as const, repeatCooldownMs: [2_000, 4_000] as const, damageMultiplier: 0.6, telegraph: { shape: "circle", anchor: "source", warningMs: 1_000, radius: 45 } as const, }), Object.freeze({ id: "marked-for-death-67823", name: "Marked for Death", spellId: 67_823, delivery: "projectile", target: "random-party", school: "shadow", animation: "cast", range: 50, cooldownMs: 9_000, initialCooldownMs: [1_000, 1_000] as const, repeatCooldownMs: [9_000, 9_000] as const, damageMultiplier: 0, effect: { kind: "target-aura", aura: "marked-for-death", durationMs: 9_000 } as const, }), ]); const TOC_CENTER = sourceMapPosition([748.309, 619.448, 411.3]); const TOC_CHAMPION_POSITIONS = Object.freeze([ sourceMapPosition([736.695, 650.02, 412.4]), sourceMapPosition([756.32, 650.05, 412.4]), sourceMapPosition([746.5, 650.65, 411.7]), ]); const TOC_EADRIC_POSITION = sourceMapPosition([746.881, 635.263, 411.7]); const TOC_BLACK_KNIGHT_POSITION = sourceMapPosition([745.016, 631.506, 411.575]); function buildTrialOfTheChampion(definition: DungeonDefinition): DungeonDefinition { assertSourceMapTransform(definition); const review = REVIEW_BY_DUNGEON.get("trial-of-the-champion")!; const championObjectives = [35_572, 35_569, 35_571].map((entry) => objectiveForEntry(definition, entry)); const argentObjective = objectiveForName(definition, "Argent Champion"); const blackKnightObjective = objectiveForEntry(definition, 35_451); const start = startObject( "trial-of-the-champion:announcer-start", "Coliseum Announcer", "Begin the Trial of the Champion", sourceMapPosition([735.81, 661.92, 412.39]), TRIAL_OF_THE_CHAMPION_FILES, ); const gates = cloneReviewedDoors( definition, [195_647, 195_650], "trial-of-the-champion:reviewed-gate", TRIAL_OF_THE_CHAMPION_FILES, ); const mainGates = gates.filter((gate) => gate.source?.templateId === 195_647); const arenaGates = gates.filter((gate) => gate.source?.templateId === 195_650); if (mainGates.length !== 1 || arenaGates.length !== 1) { throw new Error(`trial-of-the-champion expected one main and one north gate, found ${mainGates.length}/${arenaGates.length}`); } const entities = ensureEntities(definition, [ { entry: 35_314, name: "Orgrimmar Champion", level: 80, role: "melee" }, { entry: 35_326, name: "Silvermoon Champion", level: 80, role: "caster" }, { entry: 35_325, name: "Thunder Bluff Champion", level: 80, role: "melee" }, { entry: 35_309, name: "Argent Lightwielder", level: 80, role: "caster" }, { entry: 35_305, name: "Argent Monk", level: 80, role: "melee" }, { entry: 35_307, name: "Argent Priestess", level: 80, role: "caster" }, { id: "wrath-event-eadric", entry: 35_119, name: "Eadric the Pure", level: 80, role: "melee", kind: "boss", title: "Argent Champion", hasLoot: true, attacks: EADRIC_ATTACKS }, { id: "wrath-event-mounted-mokra", entry: 35_572, name: "Mounted Mokra the Skullcrusher", level: 80, role: "melee", kind: "boss", title: "Grand Champion", attacks: TOC_MOUNTED_ATTACKS }, { id: "wrath-event-mounted-eressea", entry: 35_569, name: "Mounted Eressea Dawnsinger", level: 80, role: "caster", basicAttackStyle: "melee", kind: "boss", title: "Grand Champion", attacks: TOC_MOUNTED_ATTACKS }, { id: "wrath-event-mounted-runok", entry: 35_571, name: "Mounted Runok Wildmane", level: 80, role: "melee", kind: "boss", title: "Grand Champion", attacks: TOC_MOUNTED_ATTACKS }, { id: "wrath-event-black-knight-phase-1", entry: 35_451, name: "The Black Knight", level: 80, role: "melee", kind: "boss", title: "Death Knight Form", attacks: BLACK_KNIGHT_PHASE_ONE_ATTACKS }, { id: "wrath-event-black-knight-phase-2", entry: 35_451, name: "The Black Knight", level: 80, role: "melee", kind: "boss", title: "Skeleton Form", attacks: BLACK_KNIGHT_PHASE_TWO_ATTACKS }, { id: "wrath-event-black-knight-phase-3", entry: 35_451, name: "The Black Knight", level: 80, role: "caster", basicAttackStyle: "melee", kind: "boss", title: "Ghost Form", hasLoot: true, attacks: BLACK_KNIGHT_PHASE_THREE_ATTACKS }, ]); const idFor = (entry: number): string => { const id = entityIdForEntry(entities, entry); if (!id) throw new Error(`trial-of-the-champion is missing event creature entry ${entry}`); return id; }; const groups: DungeonEncounterSpawnGroupDefinition[] = []; const minionWaves = [ { id: "silvermoon", entry: 35_326 }, { id: "orgrimmar", entry: 35_314 }, { id: "thunder-bluff", entry: 35_325 }, ] as const; minionWaves.forEach((wave, index) => { const groupId = `trial-of-the-champion:minions-${index + 1}-${wave.id}`; groups.push(Object.freeze({ id: groupId, spawns: Object.freeze([0, 1, 2].map((member) => spawnAt( `${groupId}:mob-${member + 1}`, idFor(wave.entry), TOC_CENTER, PACK_OFFSETS[member], ))), })); }); groups.push(Object.freeze({ id: "trial-of-the-champion:mounted-champions", spawns: Object.freeze([ spawnAt("trial-of-the-champion:mounted-mokra", "wrath-event-mounted-mokra", TOC_CENTER, PACK_OFFSETS[1]), spawnAt("trial-of-the-champion:mounted-eressea", "wrath-event-mounted-eressea", TOC_CENTER, PACK_OFFSETS[2]), spawnAt("trial-of-the-champion:mounted-runok", "wrath-event-mounted-runok", TOC_CENTER, PACK_OFFSETS[0]), ]), })); groups.push(Object.freeze({ id: "trial-of-the-champion:foot-champions", spawns: Object.freeze(championObjectives.map((objective, index) => spawnAt( objective.id, idFor([35_572, 35_569, 35_571][index]), TOC_CHAMPION_POSITIONS[index], ))), })); const argentSoldierSpawns: StaticMobSpawnDefinition[] = []; for (let row = -1; row <= 1; row += 1) { const sourceX = 746.67 + row * 10; argentSoldierSpawns.push( spawnAt(`trial-of-the-champion:argent-${row + 2}-lightwielder`, idFor(35_309), sourceMapPosition([sourceX - 2, 684.08, 412.5])), spawnAt(`trial-of-the-champion:argent-${row + 2}-monk`, idFor(35_305), sourceMapPosition([sourceX, 684.08, 412.5])), spawnAt(`trial-of-the-champion:argent-${row + 2}-priestess`, idFor(35_307), sourceMapPosition([sourceX + 2, 684.08, 412.5])), ); } groups.push( Object.freeze({ id: "trial-of-the-champion:argent-soldiers", spawns: Object.freeze(argentSoldierSpawns) }), Object.freeze({ id: "trial-of-the-champion:eadric", spawns: Object.freeze([spawnAt(argentObjective.id, "wrath-event-eadric", TOC_EADRIC_POSITION)]) }), Object.freeze({ id: "trial-of-the-champion:black-knight-phase-1", spawns: Object.freeze([spawnAt("trial-of-the-champion:black-knight-form-1", "wrath-event-black-knight-phase-1", TOC_BLACK_KNIGHT_POSITION)]) }), Object.freeze({ id: "trial-of-the-champion:black-knight-phase-2", spawns: Object.freeze([spawnAt("trial-of-the-champion:black-knight-form-2", "wrath-event-black-knight-phase-2", TOC_BLACK_KNIGHT_POSITION)]) }), Object.freeze({ id: "trial-of-the-champion:black-knight-phase-3", spawns: Object.freeze([spawnAt(blackKnightObjective.id, "wrath-event-black-knight-phase-3", TOC_BLACK_KNIGHT_POSITION)]) }), ); const objectiveReplacements = new Map([ ...championObjectives.map((objective, index) => [ objective.id, Object.freeze({ ...objective, position: TOC_CHAMPION_POSITIONS[index], eventOnly: true }), ] as const), [argentObjective.id, Object.freeze({ ...argentObjective, name: "Eadric the Pure", position: TOC_EADRIC_POSITION, eventOnly: true })], [blackKnightObjective.id, Object.freeze({ ...blackKnightObjective, position: TOC_BLACK_KNIGHT_POSITION, eventOnly: true })], ]); const rules: DungeonEncounterRule[] = [ { id: "start-trial", trigger: { kind: "interact", objectId: start.id }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "mark-trial-start-active", kind: "set-world-object-state", objectId: start.id, state: "active" }, ...arenaGates.map((gate, index) => ({ id: `close-trial-arena-gate-${index + 1}`, kind: "set-world-object-state" as const, objectId: gate.id, state: "closed" })), { id: "activate-champion-minions-1", kind: "activate-spawn-group", spawnGroupId: groups[0].id }, { id: "enter-champion-minions-1", kind: "set-phase", phase: "champion-minions-1-active" }, ], }, { id: "resume-trial-champions", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, ...championObjectives.map((objective) => ({ kind: "objective-complete" as const, objectiveId: objective.id, equals: false })), ], actions: [ { id: "resume-mark-trial-start-active", kind: "set-world-object-state", objectId: start.id, state: "active" }, ...arenaGates.map((gate, index) => ({ id: `resume-close-trial-arena-gate-${index + 1}`, kind: "set-world-object-state" as const, objectId: gate.id, state: "closed" })), { id: "resume-activate-champion-minions-1", kind: "activate-spawn-group", spawnGroupId: groups[0].id }, { id: "resume-enter-champion-minions-1", kind: "set-phase", phase: "champion-minions-1-active" }, ], }, { id: "resume-trial-argent", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, ...championObjectives.map((objective) => ({ kind: "objective-complete" as const, objectiveId: objective.id, equals: true })), { kind: "objective-complete", objectiveId: argentObjective.id, equals: false }, ], actions: [ { id: "resume-activate-argent-soldiers", kind: "activate-spawn-group", spawnGroupId: "trial-of-the-champion:argent-soldiers" }, { id: "resume-enter-argent-soldiers", kind: "set-phase", phase: "argent-soldiers-active" }, ], }, { id: "resume-trial-black-knight", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: argentObjective.id, equals: true }, { kind: "objective-complete", objectiveId: blackKnightObjective.id, equals: false }, ], actions: [ { id: "resume-activate-black-knight-phase-1", kind: "activate-spawn-group", spawnGroupId: "trial-of-the-champion:black-knight-phase-1" }, { id: "resume-enter-black-knight-phase-1", kind: "set-phase", phase: "black-knight-phase-1-active" }, ], }, ]; minionWaves.forEach((_, index) => { const wave = index + 1; const group = groups[index]; const nextGroupId = wave < 3 ? groups[index + 1].id : "trial-of-the-champion:mounted-champions"; const nextPhase = wave < 3 ? `champion-minions-${wave + 1}-active` : "mounted-champions-active"; rules.push({ id: `clear-champion-minions-${wave}`, trigger: { kind: "entities-dead", entityIds: group.spawns.map((spawn) => spawn.id), timeoutMs: 600_000 }, guards: [{ kind: "phase", phases: [`champion-minions-${wave}-active`] }], actions: [ { id: `deactivate-champion-minions-${wave}`, kind: "deactivate-spawn-group", spawnGroupId: group.id }, { id: wave < 3 ? `activate-champion-minions-${wave + 1}` : "activate-mounted-champions", kind: "activate-spawn-group", spawnGroupId: nextGroupId }, { id: wave < 3 ? `enter-champion-minions-${wave + 1}` : "enter-mounted-champions", kind: "set-phase", phase: nextPhase }, ], }); }); const mountedGroup = groups.find((group) => group.id === "trial-of-the-champion:mounted-champions")!; const footGroup = groups.find((group) => group.id === "trial-of-the-champion:foot-champions")!; rules.push( { id: "clear-mounted-champions", trigger: { kind: "entities-dead", entityIds: mountedGroup.spawns.map((spawn) => spawn.id), timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["mounted-champions-active"] }], actions: [ { id: "deactivate-mounted-champions", kind: "deactivate-spawn-group", spawnGroupId: mountedGroup.id }, { id: "activate-foot-champions", kind: "activate-spawn-group", spawnGroupId: footGroup.id }, { id: "enter-foot-champions", kind: "set-phase", phase: "foot-champions-active" }, ], }, { id: "clear-foot-champions", trigger: { kind: "entities-dead", entityIds: footGroup.spawns.map((spawn) => spawn.id), timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["foot-champions-active"] }], actions: [ { id: "deactivate-foot-champions", kind: "deactivate-spawn-group", spawnGroupId: footGroup.id }, ...championObjectives.map((objective, index) => ({ id: `complete-grand-champion-${index + 1}`, kind: "complete-objective" as const, objectiveId: objective.id, persistence: "run" as const })), { id: "checkpoint-grand-champions", kind: "set-checkpoint", checkpointId: "trial-of-the-champion:grand-champions", persistence: "checkpoint" }, { id: "activate-argent-soldiers", kind: "activate-spawn-group", spawnGroupId: "trial-of-the-champion:argent-soldiers" }, { id: "enter-argent-soldiers", kind: "set-phase", phase: "argent-soldiers-active" }, ], }, { id: "clear-argent-soldiers", trigger: { kind: "entities-dead", entityIds: argentSoldierSpawns.map((spawn) => spawn.id), timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["argent-soldiers-active"] }], actions: [ { id: "deactivate-argent-soldiers", kind: "deactivate-spawn-group", spawnGroupId: "trial-of-the-champion:argent-soldiers" }, { id: "activate-eadric", kind: "activate-spawn-group", spawnGroupId: "trial-of-the-champion:eadric" }, { id: "enter-eadric", kind: "set-phase", phase: "eadric-active" }, ], }, { id: "clear-eadric", trigger: { kind: "entities-dead", entityIds: [argentObjective.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["eadric-active"] }], actions: [ { id: "deactivate-eadric", kind: "deactivate-spawn-group", spawnGroupId: "trial-of-the-champion:eadric" }, { id: "complete-eadric", kind: "complete-objective", objectiveId: argentObjective.id, persistence: "run" }, { id: "checkpoint-eadric", kind: "set-checkpoint", checkpointId: argentObjective.id, persistence: "checkpoint" }, { id: "activate-black-knight-phase-1", kind: "activate-spawn-group", spawnGroupId: "trial-of-the-champion:black-knight-phase-1" }, { id: "enter-black-knight-phase-1", kind: "set-phase", phase: "black-knight-phase-1-active" }, ], }, { id: "clear-black-knight-phase-1", trigger: { kind: "entities-dead", entityIds: ["trial-of-the-champion:black-knight-form-1"], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["black-knight-phase-1-active"] }], actions: [ { id: "deactivate-black-knight-phase-1", kind: "deactivate-spawn-group", spawnGroupId: "trial-of-the-champion:black-knight-phase-1" }, { id: "activate-black-knight-phase-2", kind: "activate-spawn-group", spawnGroupId: "trial-of-the-champion:black-knight-phase-2" }, { id: "enter-black-knight-phase-2", kind: "set-phase", phase: "black-knight-phase-2-active" }, ], }, { id: "clear-black-knight-phase-2", trigger: { kind: "entities-dead", entityIds: ["trial-of-the-champion:black-knight-form-2"], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["black-knight-phase-2-active"] }], actions: [ { id: "deactivate-black-knight-phase-2", kind: "deactivate-spawn-group", spawnGroupId: "trial-of-the-champion:black-knight-phase-2" }, { id: "activate-black-knight-phase-3", kind: "activate-spawn-group", spawnGroupId: "trial-of-the-champion:black-knight-phase-3" }, { id: "enter-black-knight-phase-3", kind: "set-phase", phase: "black-knight-phase-3-active" }, ], }, { id: "clear-black-knight-phase-3", trigger: { kind: "entities-dead", entityIds: [blackKnightObjective.id], timeoutMs: 900_000 }, guards: [{ kind: "phase", phases: ["black-knight-phase-3-active"] }], actions: [ { id: "deactivate-black-knight-phase-3", kind: "deactivate-spawn-group", spawnGroupId: "trial-of-the-champion:black-knight-phase-3" }, { id: "complete-black-knight", kind: "complete-objective", objectiveId: blackKnightObjective.id, persistence: "run" }, { id: "checkpoint-black-knight", kind: "set-checkpoint", checkpointId: blackKnightObjective.id, persistence: "run" }, ...arenaGates.map((gate, index) => ({ id: `open-trial-arena-gate-${index + 1}`, kind: "set-world-object-state" as const, objectId: gate.id, state: "open", persistence: "run" as const })), { id: "mark-trial-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "mark-trial-start-complete", kind: "set-world-object-state", objectId: start.id, state: "complete", persistence: "run" }, { id: "enter-trial-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ], }, ); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "trial-of-the-champion:wrath-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { flags: { "dungeon-complete": false }, worldObjectStates: Object.fromEntries([ [start.id, "ready"], ...mainGates.map((gate) => [gate.id, "closed"] as const), ...arenaGates.map((gate) => [gate.id, "open"] as const), ]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review), rules: Object.freeze(rules), }); const gatedEntries = new Set([30_188, 35_451, 35_569, 35_571, 35_572]); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => !gatedEntries.has(entryForSpawn(definition, spawn) ?? -1))), bosses: Object.freeze(definition.bosses.map((boss) => objectiveReplacements.get(boss.id) ?? boss)), objectiveOrder: Object.freeze(definition.objectiveOrder.map((boss) => objectiveReplacements.get(boss.id) ?? boss)), encounters: Object.freeze([...withoutBaselineBossProgression(definition), encounter]), encounterSpawnGroups: Object.freeze([ ...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("trial-of-the-champion:")), ...groups, ]), serverGameObjects: Object.freeze([ ...withoutReviewedDoorSources(definition, gates), start, ...gates, ]), validation: appendValidationWarnings(definition, [ "Trial of the Champion now has a complete finite announcer sequence with a deterministic valid champion roster, explicit mounted proxy, Eadric branch, and all three Black Knight forms.", ]), }); } /** * Adds checkpoint-safe source-reviewed adapters for Wrath instances whose * retail vehicle, escort, or in-place transformation primitives are absent. * Apply this after the general WoW catalog and before RuneWaker content. */ export function withWowWrathEncounterContent(definition: DungeonDefinition): DungeonDefinition { if (definition.encounters?.some((encounter) => encounter.id === `${definition.id}:wrath-instance`)) { return definition; } if (definition.id === "halls-of-reflection") return buildHallsOfReflection(definition); if (definition.id === "the-oculus") return buildOculus(definition); if (definition.id === "trial-of-the-champion") return buildTrialOfTheChampion(definition); return definition; }