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"; export interface WowEncounterSourceFile { readonly path: string; readonly sha256: string; readonly anchors: readonly string[]; } export interface WowEncounterBossMapping { readonly entry: number; readonly name: string; readonly sourceSemantics: string; readonly healerManSemantics: string; } export interface WowEncounterContentReview { readonly dungeonId: string; readonly fidelity: "source-backed-adapted"; readonly sourceFiles: readonly WowEncounterSourceFile[]; readonly bossMappings: readonly WowEncounterBossMapping[]; readonly reviewedBehavior: string; readonly adaptations: readonly string[]; readonly remainingGaps: readonly string[]; } export interface WowEncounterDeferredAdapter { readonly dungeonId: string; readonly sourceFiles: readonly string[]; readonly nextAdapter: string; readonly runtimeGap: string; } interface AdaptedMobSpec { readonly entry: number; readonly name: string; readonly level: number; readonly role: "melee" | "caster"; } const SOURCE_ROOT = "../HealerMan-Storage/pipeline-work/dungeons/azerothcore-scripts/src/server/scripts"; const BLACK_MORASS_FILES = Object.freeze([ `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/TheBlackMorass/instance_the_black_morass.cpp`, `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/TheBlackMorass/the_black_morass.cpp`, `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/TheBlackMorass/the_black_morass.h`, ]); const VIOLET_HOLD_FILES = Object.freeze([ `${SOURCE_ROOT}/Northrend/VioletHold/instance_violet_hold.cpp`, `${SOURCE_ROOT}/Northrend/VioletHold/violet_hold.cpp`, `${SOURCE_ROOT}/Northrend/VioletHold/violet_hold.h`, ]); const CULLING_FILES = Object.freeze([ `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/CullingOfStratholme/culling_of_stratholme.cpp`, `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/CullingOfStratholme/culling_of_stratholme.h`, ]); const DURNHOLDE_FILES = Object.freeze([ `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/EscapeFromDurnholdeKeep/instance_old_hillsbrad.cpp`, `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/EscapeFromDurnholdeKeep/old_hillsbrad.cpp`, `${SOURCE_ROOT}/Kalimdor/CavernsOfTime/EscapeFromDurnholdeKeep/old_hillsbrad.h`, ]); const ARCATRAZ_FILES = Object.freeze([ `${SOURCE_ROOT}/Outland/TempestKeep/arcatraz/arcatraz.cpp`, `${SOURCE_ROOT}/Outland/TempestKeep/arcatraz/arcatraz.h`, ]); const BLACKROCK_SPIRE_FILES = Object.freeze([ `${SOURCE_ROOT}/EasternKingdoms/BlackrockMountain/BlackrockSpire/instance_blackrock_spire.cpp`, `${SOURCE_ROOT}/EasternKingdoms/BlackrockMountain/BlackrockSpire/blackrock_spire.h`, ]); /** * The original deferred trio now lives in wowWrathEncounterContentCatalog. * Keep this explicit empty ledger so a stale "next batch" cannot contradict * the executable registry or the functionality audit. */ export const WOW_ENCOUNTER_NEXT_BATCH: readonly WowEncounterDeferredAdapter[] = Object.freeze([]); export const WOW_ENCOUNTER_CONTENT_REVIEWS: readonly WowEncounterContentReview[] = Object.freeze([ { dungeonId: "black-morass", fidelity: "source-backed-adapted", sourceFiles: [ { path: BLACK_MORASS_FILES[0], sha256: "a89db0207748acb461198d3b6539767880b15aef96a7a7f457ab42b3c37fbf37", anchors: [ "PortalLocation (lines 24-30)", "SetBossState boss pause (lines 95-175)", "ScheduleNextPortal (lines 189-235)", "OnCreatureRemove next-rift scheduling (lines 267-286)", "DATA_MEDIVH starts the event (lines 303-316)", ], }, { path: BLACK_MORASS_FILES[1], sha256: "25287098851d05ff713fecf8e554f34583b36039a59be67486fd8b71ee34479e", anchors: [ "firstWave/secondWave/thirdWave (lines 48-50)", "npc_time_rift reset and add schedule (lines 271-292)", "DoSelectSummon (lines 313-348)", "boss selection at rifts 6/12/18 (lines 367-391)", ], }, { path: BLACK_MORASS_FILES[2], sha256: "9d25d617b19fa01aa39a65ed071d9cd149d40ba346e64079d8a0a9cef40b9985", anchors: ["CreatureIds (lines 55-85)"], }, ], bossMappings: [ { entry: 17_879, name: "Chrono Lord Deja", sourceSemantics: "Summoned by the sixth time rift; the next rift is held for 150 seconds after victory.", healerManSemantics: "Removed from static population and spawned only in deterministic rift wave 6, followed by the source 150-second pause.", }, { entry: 17_880, name: "Temporus", sourceSemantics: "Summoned by the twelfth time rift; the next rift is held for 150 seconds after victory.", healerManSemantics: "Removed from static population and spawned only in deterministic rift wave 12, followed by the source 150-second pause.", }, { entry: 17_881, name: "Aeonus", sourceSemantics: "Summoned by the eighteenth time rift and ends the defense when defeated.", healerManSemantics: "Removed from static population and spawned only in deterministic rift wave 18; victory completes the dungeon objective.", }, ], reviewedBehavior: "An explicit start begins 18 ordered rifts. Source creature entries and portal centers are preserved, and bosses are gated to rifts 6, 12, and 18.", adaptations: [ "A deterministic round-robin portal order replaces AzerothCore random selection while remaining a valid no-immediate-repeat source outcome.", "Each rift is a finite keeper/add pack. AzerothCore's repeating 15-second add stream is bounded so the current static spawn-group runtime cannot create an unbounded lifecycle.", "Random ten-metre summon points are fixed offsets around the exact four source portal centers.", "Added source creature entries use conservative HealerMan melee/caster placeholders until their template stats, spells, and client models are imported.", ], remainingGaps: [ "Medivh is not yet a friendly damageable actor, so corruption, shield percentage, shield failure, and attacker pathing to Medivh are not executable.", "Time-rift channels, repeated add lifetimes, heroic already-defeated boss substitutes, dialogue, quest credit, and the outro remain unported.", ], }, { dungeonId: "violet-hold", fidelity: "source-backed-adapted", sourceFiles: [ { path: VIOLET_HOLD_FILES[0], sha256: "10baaf7673fc819a912892a956a9e2864127a11ef4064fb54db39a75f495a0a1", anchors: [ "SetBossState boss-wave continuation (lines 138-188)", "ACTION_START_INSTANCE/ACTION_PORTAL_DEFEATED (lines 200-219)", "StartBossEncounter cell release (lines 278-345)", "EVENT_SUMMON_PORTAL and boss selection (lines 405-437)", "Cyanigosa transform/attack gate (lines 438-455)", ], }, { path: VIOLET_HOLD_FILES[1], sha256: "432cb8b8bbe17bf43ff47c3b5e54142148f1b2de273daf3138c6af976e0f7153", anchors: [ "npc_vh_teleportation_portal deterministic candidates (lines 28-128)", "portal trash escort and seal attack (lines 145-280)", "saboteur releases selected boss (lines 915-936)", ], }, { path: VIOLET_HOLD_FILES[2], sha256: "f83b0145e6be2a6ce85d7ef15ef346d7b72b33ddb6817c73a7c3757997a85d4a", anchors: [ "VHCreatures and six-boss pool (lines 60-135)", "boss and portal positions (lines 187-219)", ], }, ], bossMappings: [ ...[ [29_316, "Moragg"], [29_315, "Erekem"], [29_313, "Ichoron"], [29_312, "Lavanthor"], [29_266, "Xevozz"], [29_314, "Zuramat the Obliterator"], ].map(([entry, name]) => ({ entry: entry as number, name: name as string, sourceSemantics: "One of six static, non-attackable prisoners selected without replacement for wave 6 or 12, then released by a saboteur.", healerManSemantics: entry === 29_315 ? "Selected deterministically for wave 6 and released with both Erekem guards." : entry === 29_316 ? "Selected deterministically for wave 12." : "Kept out of the deterministic baseline until its entity/template asset is imported; it is not fabricated as a static boss.", })), { entry: 31_134, name: "Cyanigosa", sourceSemantics: "Summoned at wave 18, transformed after ten seconds, and made attackable 2.5 seconds later.", healerManSemantics: "Removed from static population and spawned only at wave 18; the non-combat transformation presentation is shortened.", }, ], reviewedBehavior: "An explicit Sinclari start drives 18 ordered waves, deterministic source trash choices, two distinct prisoner releases at waves 6/12, and Cyanigosa at wave 18.", adaptations: [ "Erekem then Moragg is a deterministic valid selection from the source six-boss pool; the other four prisoners remain documented and are never fabricated as static objectives.", "Portal locations and source trash entries are deterministic. Portal incubation and saboteur travel are collapsed into the outer 3/35-second wave pauses.", "Keeper portals receive one bounded add pack rather than repeating adds every 20 seconds.", "Reviewed main and prison doors use visible procedural geometry that blocks movement/sight while closed and becomes nonblocking when opened.", "Added source creature entries use conservative HealerMan melee/caster placeholders until their template stats, spells, paths, and client models are imported.", ], remainingGaps: [ "The prison seal is not yet a friendly damageable objective, so trash pathing, periodic seal damage, seal health, and the zero-health failure are not executable.", "Defense crystals, portal channels, boss-specific saboteur routes, Cyanigosa's transformation presentation, dialogue, achievements, and full six-boss seeded selection remain unported.", ], }, { dungeonId: "culling-of-stratholme", fidelity: "source-backed-adapted", sourceFiles: [ { path: CULLING_FILES[0], sha256: "0a3194683ae2f6780165dba00451a7401a208dd34ff8de5f062f6fdd312dbaec", anchors: [ "eight city-wave tables and five town-hall rifts (lines 163-293)", "Arthas gossip progression (lines 295-335)", "Epoch and final Mal'ganis attack gates (lines 1095-1153)", "SummonNextWave/SendNextWave boss gates (lines 1229-1344)", ], }, { path: CULLING_FILES[1], sha256: "403d8576076ebc7e043f40bfbf21305e6bb693c3033b273f4e95afce93e82904", anchors: [ "boss creature entries (lines 49-61)", "scripted boss and event positions (lines 145-180)", ], }, ], bossMappings: [ [26_529, "Meathook", "Summoned after city trash wave 4.", "Spawned only as event wave 5."], [26_530, "Salramm the Fleshcrafter", "Summoned after city trash wave 9.", "Spawned only as event wave 10."], [26_532, "Chrono-Lord Epoch", "Summoned after five rifts across three town-hall stops, then made attackable after his intro.", "Spawned after three finite groups containing the five exact source rift packs."], [26_533, "Mal'Ganis", "Activated after Arthas reaches the final escort leg.", "Spawned only after Epoch and the adapted final-leg pause."], ].map(([entry, name, sourceSemantics, healerManSemantics]) => ({ entry: entry as number, name: name as string, sourceSemantics: sourceSemantics as string, healerManSemantics: healerManSemantics as string, })), reviewedBehavior: "The source city-wave compositions gate Meathook/Salramm, five town-hall rifts gate Epoch, and Epoch gates the final Mal'Ganis fight.", adaptations: [ "Arthas gossip starts one deterministic ordered state machine; escort travel and scene pauses are represented by finite waits between exact source combat gates.", "Five town-hall rifts are grouped at the source's one/two/two waypoint cadence, with their combatants preserved and the cosmetic rift creatures represented as world foci.", "Event-only bosses use their scripted encounter positions rather than the compiler's fabricated static positions.", ], remainingGaps: [ "Arthas is not yet a protected friendly escort, so waypoint travel, combat participation, gossip pauses between every act, escort death, and checkpoint relocation are approximated.", "Civilian infection scenes, the timed Infinite Corruptor challenge, dialogue, chest/quest credit, and Mal'Ganis escape presentation remain unported.", ], }, { dungeonId: "escape-from-durnholde", fidelity: "source-backed-adapted", sourceFiles: [ { path: DURNHOLDE_FILES[0], sha256: "2fd7f2572e9759044ed9077d99824b7673f135a98db42710d6b01cc83e6220b3", anchors: [ "Drake summon position (lines 25-39)", "five-barrel threshold and 12-second finale (lines 154-161)", "Lieutenant Drake summon (lines 217-246)", ], }, { path: DURNHOLDE_FILES[1], sha256: "41f74fc2fc3ce9146cb70419d7ec645a9aeb0f4d88ed9a5723074cc531bcbe0c", anchors: [ "Skarloc summon at Thrall waypoint 28 (lines 358-359)", "Tarren Mill guard packs (lines 700-711)", "Epoch summon, three add waves, and activation (lines 721-815)", ], }, { path: DURNHOLDE_FILES[2], sha256: "ad56a3f77a49ee8c1ef3270cc1ab80b763e21eb12008b0d1880535e0b116b6cb", anchors: ["boss, escort, and barrel entries (lines 27-67)"], }, ], bossMappings: [ { entry: 17_848, name: "Lieutenant Drake", sourceSemantics: "Summoned twelve seconds after the fifth incendiary barrel is planted.", healerManSemantics: "Removed from static population and spawned at the exact source point after any five imported barrels are used.", }, { entry: 17_862, name: "Captain Skarloc", sourceSemantics: "Summoned when Thrall reaches escort waypoint 28.", healerManSemantics: "Spawned only after Drake and an explicit Thrall escort interaction, at the exact source summon point.", }, { entry: 18_096, name: "Epoch Hunter", sourceSemantics: "Held immune while three add waves fight, then activated against Thrall's party.", healerManSemantics: "Spawned only after the exact deterministic add-wave compositions are cleared.", }, ], reviewedBehavior: "Any five source barrels gate Drake; Drake unlocks Thrall, Thrall gates Skarloc, and the Tarren Mill/Epoch add sequence gates Epoch Hunter.", adaptations: [ "A Thrall interaction stands in for the waypoint escort segment between Drake and Skarloc.", "Source add compositions and summon coordinates are retained as finite fight groups; travel time between escort stops is shortened.", ], remainingGaps: [ "Thrall and Taretha are not protected friendly actors, so escort pathing, ambush motion, friendly combat, death/failure, and the source attempt counter remain unported.", "Barrel flame chains, mounted travel, dialogue, disguises, quest credit, and Epoch's immunity/intro presentation remain unported.", ], }, { dungeonId: "arcatraz", fidelity: "source-backed-adapted", sourceFiles: [ { path: ARCATRAZ_FILES[0], sha256: "c462f5780463b0f837d79c0855c4ff856e13de0abf4e392e4c98d38d4cb746ec", anchors: [ "deterministic candidate entries (lines 272-280)", "pod release sequence and waits (lines 437-543)", "Skyriss summon and attackability gate (lines 545-577)", ], }, { path: ARCATRAZ_FILES[1], sha256: "845bd2d050ff15577f07a03794a13b57f5b4770452ce363cdbe130cc693720c3", anchors: ["Warden data, creature ids, and five pod templates (lines 27-63)"], }, ], bossMappings: [{ entry: 20_912, name: "Harbinger Skyriss", sourceSemantics: "Summoned only after Warden Mellichar releases four earlier pods, including three hostile prisoners and Millhouse Manastorm.", healerManSemantics: "Removed from static population and spawned after three deterministic hostile prisoner fights plus the timed Millhouse presentation step.", }], reviewedBehavior: "Warden Mellichar opens all five imported pod objects in source order and gates Skyriss behind the three hostile prisoner deaths.", adaptations: [ "The first/third/fourth random pods choose Trickster, Akkiris, and Twilight Drakonaar respectively, each a valid deterministic source branch.", "Millhouse's friendly dialogue/combat-support segment is a timed pod transition because the runtime has no friendly encounter actor primitive.", "Long non-combat speeches are shortened while preserving the source pod order and death gates.", "Reviewed pods and Warden's Shield use visible procedural geometry with closed blocking/open nonblocking states.", "Draft map provenance prevents a provably exact raw-coordinate transform, so imported pod and boss gameplay coordinates are retained.", ], remainingGaps: [ "Millhouse support, Warden feign death, Skyriss' long intro/mind rend presentation, randomized alternate prisoners, dialogue, and quest credit remain unported.", ], }, { dungeonId: "blackrock-spire", fidelity: "source-backed-adapted", sourceFiles: [ { path: BLACKROCK_SPIRE_FILES[0], sha256: "0b3b2f51fa7acd6e17dfa5ec4fe4f50c9f582b730bdb2e781e8b3c44ee996ee1", anchors: [ "five-wave count, 30-second cadence, source positions (lines 35-52)", "wave cleanup/reset (lines 342-355)", "guardian/hatcher and Solakar summons (lines 402-420)", "Father Flame start hook (lines 714-727)", ], }, { path: BLACKROCK_SPIRE_FILES[1], sha256: "40ae38809110acd1cf0e1f848e62cd81c0c3a1958a3c7c40c0b7afa7f475f8c1", anchors: ["Solakar, Rookery Guardian, and Rookery Hatcher entries (lines 55, 86-88)"], }, ], bossMappings: [{ entry: 10_264, name: "Solakar Flamewreath", sourceSemantics: "Father Flame starts five overlapping guardian/hatcher waves at 30-second intervals, then summons Solakar on the sixth event tick.", healerManSemantics: "Removed from static population and spawned after the exact five-pair cadence; killing Solakar despawns any surviving event adds.", }], reviewedBehavior: "The imported Father Flame object starts five timed guardian/hatcher pairs and then the event-only Solakar fight.", adaptations: [ "Draft map provenance prevents a provably exact raw-coordinate transform, so the imported Father Flame and objective-space coordinates anchor the event.", "Source entries use conservative combat placeholders until their creature templates and client models are imported.", ], remainingGaps: [ "Hatcher egg behavior, first-hatcher dialogue, original movement paths, and Solakar-specific spell AI remain unported.", ], }, ]); const REVIEW_BY_DUNGEON = new Map( WOW_ENCOUNTER_CONTENT_REVIEWS.map((review) => [review.dungeonId, review] as const), ); function sourceMapPosition(source: readonly [x: number, y: number, z: number]): Vector3Tuple { // The imported AzerothCore packages record this map-space transform in provenance. 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( `WoW encounter content for ${definition.id} requires three(x,y,z)=(wow.y,wow.z,wow.x), got ${definition.provenance.coordinateTransform}`, ); } } function adaptedEntityId(entry: number): string { return `wow-event-entry-${entry}`; } function findEntityIdByEntry( entities: PopulationDefinitionMap, entry: number, ): string | undefined { return Object.entries(entities).find(([, entity]) => entity.combat?.serverEntry === entry)?.[0]; } function createAdaptedMob(spec: AdaptedMobSpec): PopulationEntityDefinition { const caster = spec.role === "caster"; const entity: PopulationEntityDefinition = { id: adaptedEntityId(spec.entry), kind: "mob", name: spec.name, hasLoot: false, combat: { serverEntry: spec.entry, level: spec.level, healthMultiplier: caster ? 0.92 : 1.08, damageMultiplier: 0.92, moveSpeed: 3.65, aggroRange: 32, leashRange: 85, attacks: [ { id: "basic-attack", name: caster ? "Source Spell Proxy" : "Basic Attack", delivery: caster ? "projectile" : "melee", target: "primary", school: caster ? "arcane" : "physical", animation: caster ? "cast" : "attack", range: caster ? 25 : 4.5, cooldownMs: caster ? 3_000 : 2_000, damageMultiplier: caster ? 0.8 : 1, }, ], }, visual: { primaryColor: caster ? "#4d5f82" : "#61434d", accentColor: caster ? "#98c9ef" : "#d49a88", scale: 1, archetype: "humanoid", }, }; return Object.freeze(entity); } function ensureAdaptedMobs( definition: DungeonDefinition, specs: readonly AdaptedMobSpec[], ): { readonly entities: PopulationDefinitionMap; readonly idsByEntry: ReadonlyMap } { const entities: Record = { ...definition.entities }; const idsByEntry = new Map(); for (const spec of specs) { const existingId = findEntityIdByEntry(entities, spec.entry); if (existingId) { idsByEntry.set(spec.entry, existingId); continue; } const entity = createAdaptedMob(spec); if (entities[entity.id]) throw new Error(`Duplicate adapted entity ${entity.id}`); entities[entity.id] = entity; idsByEntry.set(spec.entry, entity.id); } return { entities: Object.freeze(entities), idsByEntry }; } function entityEntry(definition: DungeonDefinition, spawn: StaticMobSpawnDefinition): number | undefined { return definition.entities[spawn.entityId]?.combat?.serverEntry; } function objectiveForEntry( definition: DungeonDefinition, entry: number, names: readonly string[], ): DungeonBossObjective { const spawnIds = new Set( definition.staticSpawns .filter((spawn) => entityEntry(definition, spawn) === entry) .map((spawn) => spawn.id), ); const normalizedNames = new Set(names.map((name) => name.toLowerCase())); const objective = definition.bosses.find((boss) => ( spawnIds.has(boss.id) || normalizedNames.has(boss.name.toLowerCase()) )); if (!objective) throw new Error(`${definition.id} is missing the boss objective for creature entry ${entry}`); return objective; } function spawnAt( id: string, entityId: string, center: Vector3Tuple, offset: readonly [x: number, z: number], ): StaticMobSpawnDefinition { const spawn: StaticMobSpawnDefinition = { id, entityId, position: [center[0] + offset[0], center[1], center[2] + offset[1]], yaw: 0, spawnMask: 3, }; return Object.freeze(spawn); } const PACK_OFFSETS = Object.freeze([ [6, 0], [-3, 5.2], [-3, -5.2], [0, 0], [4.5, 4.5], ] as const); function eventFocusObject( id: string, name: string, position: Vector3Tuple, sourceFiles: readonly string[], color: string, ): DungeonWorldObjectDefinition { const object: DungeonWorldObjectDefinition = { id, name, kind: "trigger", position, halfExtents: [1.4, 2, 1.4], initialState: "hidden", states: [ { id: "hidden", label: "Dormant", visible: false, blocksMovement: false, interactionEnabled: false }, { id: "active", label: "Active", visible: true, blocksMovement: false, interactionEnabled: false }, ], visual: { color, emissive: color, scale: [1.4, 2.4, 1.4] }, source: { fidelity: "source-backed-adapted", files: sourceFiles, note: "The source position/state is retained; HealerMan uses a procedural focus until the client effect and dynamic creature lifecycle are available.", }, }; return Object.freeze(object); } function startObject( id: string, name: string, prompt: string, position: Vector3Tuple, signalId: string, sourceFiles: readonly string[], ): DungeonWorldObjectDefinition { const object: DungeonWorldObjectDefinition = { id, name, kind: "npc", position, halfExtents: [0.9, 1.8, 0.9], initialState: "ready", 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: 3, signalId, allowedStates: ["ready"], oneShot: true, }, visual: { color: "#a98bd4", emissive: "#302044", scale: [0.9, 1.8, 0.9] }, source: { fidelity: "source-backed-adapted", files: sourceFiles, note: "The interaction represents the source proximity/gossip start; its compact procedural body is a HealerMan presentation adapter.", }, }; return Object.freeze(object); } function sourceReview( review: WowEncounterContentReview, scope: "boss-progression" | "instance-orchestration" = "instance-orchestration", ): DungeonEncounterDefinition["source"] { return { fidelity: review.fidelity, scope, 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` )); } function appendValidationWarnings( definition: DungeonDefinition, warnings: readonly string[], ): DungeonDefinition["validation"] { return Object.freeze({ ...definition.validation, warnings: Object.freeze([...new Set([...definition.validation.warnings, ...warnings])]), }); } const BLACK_MORASS_PORTALS = Object.freeze([ sourceMapPosition([-2030.8318, 7024.9443, 23.071817]), sourceMapPosition([-1961.7335, 7029.528, 21.811401]), sourceMapPosition([-1887.695, 7106.557, 22.0495]), sourceMapPosition([-1930.9106, 7183.597, 23.007639]), ]); const BLACK_MORASS_MOBS = Object.freeze([ { entry: 21_104, name: "Rift Keeper (Warlock)", level: 70, role: "caster" }, { entry: 21_148, name: "Rift Keeper (Mage)", level: 70, role: "caster" }, { entry: 17_839, name: "Rift Lord", level: 70, role: "melee" }, { entry: 21_140, name: "Rift Lord", level: 70, role: "melee" }, { entry: 17_835, name: "Infinite Assassin", level: 70, role: "melee" }, { entry: 21_818, name: "Infinite Whelp", level: 68, role: "melee" }, { entry: 17_892, name: "Infinite Chronomancer", level: 70, role: "caster" }, { entry: 18_994, name: "Infinite Executioner", level: 70, role: "melee" }, { entry: 18_995, name: "Infinite Vanquisher", level: 70, role: "melee" }, ] as const satisfies readonly AdaptedMobSpec[]); const BLACK_MORASS_KEEPERS = Object.freeze([21_104, 21_148, 17_839, 21_140] as const); const BLACK_MORASS_PHASE_ADDS = Object.freeze([ [17_835, 21_818, 17_892], [18_994, 17_892, 21_818, 17_835], [18_994, 18_995, 17_892, 17_835], ] as const); function blackMorassAddEntries(rift: number): readonly number[] { const phase = rift < 6 ? 0 : rift < 12 ? 1 : 2; const sequence = BLACK_MORASS_PHASE_ADDS[phase]; const first = sequence[(rift - 1) % sequence.length]; if (first === 21_818) return [first, first, first]; return [first, sequence[rift % sequence.length]]; } function buildBlackMorass(definition: DungeonDefinition): DungeonDefinition { assertSourceMapTransform(definition); const review = REVIEW_BY_DUNGEON.get(definition.id)!; const { entities, idsByEntry } = ensureAdaptedMobs(definition, BLACK_MORASS_MOBS); const bossEntries = [17_879, 17_880, 17_881] as const; const bossRifts = new Map([[6, 17_879], [12, 17_880], [18, 17_881]]); const objectiveByEntry = new Map(bossEntries.map((entry) => [ entry, objectiveForEntry(definition, entry, [ entry === 17_879 ? "Chrono Lord Deja" : entry === 17_880 ? "Temporus" : "Aeonus", ]), ] as const)); const bossEntityByEntry = new Map(bossEntries.map((entry) => { const id = findEntityIdByEntry(entities, entry); if (!id) throw new Error(`${definition.id} is missing creature entry ${entry}`); return [entry, id] as const; })); const portalObjects = BLACK_MORASS_PORTALS.map((position, index) => eventFocusObject( `black-morass:rift-focus-${index + 1}`, `Time Rift ${index + 1}`, position, BLACK_MORASS_FILES, "#7553b8", )); const startId = "black-morass:medivh-defense-start"; const start = startObject( startId, "Medivh Defense", "Begin the Black Morass defense", [7086.156575, 22.485089, -1952.792725], "black-morass:start", BLACK_MORASS_FILES, ); const groups: DungeonEncounterSpawnGroupDefinition[] = []; const rules: DungeonEncounterRule[] = [ { id: "start-defense", trigger: { kind: "interact", objectId: startId }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "mark-defense-active", kind: "set-world-object-state", objectId: startId, state: "active" }, { id: "initialize-rift-counter", kind: "set-counter", counterId: "rift", value: 0 }, { id: "wait-for-rift-01", kind: "set-phase", phase: "rift-01-wait" }, ], }, { id: "resume-completed-defense", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "flag", flagId: "dungeon-complete", equals: true }, ], once: false, priority: 20, actions: [{ id: "resume-black-morass-complete", kind: "set-phase", phase: "complete" }], }, { id: "resume-after-temporus", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: objectiveByEntry.get(17_880)!.id, equals: true }, { kind: "objective-complete", objectiveId: objectiveByEntry.get(17_881)!.id, equals: false }, ], once: false, actions: [ { id: "resume-after-temporus-active", kind: "set-world-object-state", objectId: startId, state: "active" }, { id: "resume-after-temporus-counter", kind: "set-counter", counterId: "rift", value: 12 }, { id: "resume-at-rift-13", kind: "set-phase", phase: "rift-13-wait" }, ], }, { id: "resume-after-deja", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: objectiveByEntry.get(17_879)!.id, equals: true }, { kind: "objective-complete", objectiveId: objectiveByEntry.get(17_880)!.id, equals: false }, ], once: false, actions: [ { id: "resume-after-deja-active", kind: "set-world-object-state", objectId: startId, state: "active" }, { id: "resume-after-deja-counter", kind: "set-counter", counterId: "rift", value: 6 }, { id: "resume-at-rift-07", kind: "set-phase", phase: "rift-07-wait" }, ], }, { id: "resume-fresh-defense", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: objectiveByEntry.get(17_879)!.id, equals: false }, ], once: false, actions: [{ id: "resume-black-morass-ready", kind: "set-phase", phase: "ready" }], }, ]; for (let rift = 1; rift <= 18; rift += 1) { const suffix = String(rift).padStart(2, "0"); const groupId = `black-morass:rift-${suffix}`; const portalIndex = (rift - 1) % BLACK_MORASS_PORTALS.length; const portalId = portalObjects[portalIndex].id; const center = BLACK_MORASS_PORTALS[portalIndex]; const bossEntry = bossRifts.get(rift); const entries = bossEntry === 17_881 ? [] : blackMorassAddEntries(rift); const spawns: StaticMobSpawnDefinition[] = []; if (bossEntry) { const objective = objectiveByEntry.get(bossEntry)!; spawns.push(spawnAt(objective.id, bossEntityByEntry.get(bossEntry)!, center, PACK_OFFSETS[0])); } else { const keeperEntry = BLACK_MORASS_KEEPERS[(rift - 1) % BLACK_MORASS_KEEPERS.length]; spawns.push(spawnAt( `${groupId}:keeper`, idsByEntry.get(keeperEntry)!, center, PACK_OFFSETS[0], )); } entries.forEach((entry, index) => spawns.push(spawnAt( `${groupId}:add-${index + 1}`, idsByEntry.get(entry)!, center, PACK_OFFSETS[(index + 1) % PACK_OFFSETS.length], ))); groups.push(Object.freeze({ id: groupId, spawns: Object.freeze(spawns) })); const priorWasBoss = rift === 7 || rift === 13; const delayMs = rift === 1 ? 9_000 : priorWasBoss ? 156_000 : 7_000; rules.push({ id: `open-rift-${suffix}`, trigger: { kind: "elapsed", afterMs: delayMs }, guards: [{ kind: "phase", phases: [`rift-${suffix}-wait`] }], actions: [ { id: `show-rift-${suffix}`, kind: "set-world-object-state", objectId: portalId, state: "active" }, { id: `activate-rift-${suffix}`, kind: "activate-spawn-group", spawnGroupId: groupId }, { id: `record-rift-${suffix}`, kind: "set-counter", counterId: "rift", value: rift }, { id: `enter-rift-${suffix}`, kind: "set-phase", phase: `rift-${suffix}-active` }, ], }); const clearActions: DungeonEncounterAction[] = [ { id: `hide-rift-${suffix}`, kind: "set-world-object-state", objectId: portalId, state: "hidden" }, { id: `deactivate-rift-${suffix}`, kind: "deactivate-spawn-group", spawnGroupId: groupId }, ]; if (bossEntry) { clearActions.push({ id: `complete-rift-boss-${suffix}`, kind: "complete-objective", objectiveId: objectiveByEntry.get(bossEntry)!.id, persistence: "run", }); if (rift === 6 || rift === 12) { clearActions.push({ id: `checkpoint-after-rift-${suffix}`, kind: "set-checkpoint", checkpointId: rift === 6 ? "after-deja" : "after-temporus", persistence: "run", }); } } if (rift < 18) { const next = String(rift + 1).padStart(2, "0"); clearActions.push({ id: `wait-for-rift-${next}`, kind: "set-phase", phase: `rift-${next}-wait` }); } else { clearActions.push( { id: "mark-black-morass-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "complete-medivh-defense", kind: "set-world-object-state", objectId: startId, state: "complete", persistence: "run" }, { id: "enter-black-morass-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ); } rules.push({ id: `clear-rift-${suffix}`, trigger: { kind: "entities-dead", entityIds: spawns.map((spawn) => spawn.id) }, guards: [{ kind: "phase", phases: [`rift-${suffix}-active`] }], actions: clearActions, }); } const encounter: DungeonEncounterDefinition = Object.freeze({ id: "black-morass:wow-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { flags: { "dungeon-complete": false }, counters: { rift: 0 }, worldObjectStates: Object.fromEntries([ [startId, "ready"], ...portalObjects.map((object) => [object.id, "hidden"] as const), ]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review, "instance-orchestration"), rules: Object.freeze(rules), }); const updatedBosses = definition.bosses.map((boss) => { const match = [...objectiveByEntry.entries()].find(([, objective]) => objective.id === boss.id); if (!match) return boss; const [entry] = match; const rift = entry === 17_879 ? 6 : entry === 17_880 ? 12 : 18; const center = BLACK_MORASS_PORTALS[(rift - 1) % BLACK_MORASS_PORTALS.length]; return Object.freeze({ ...boss, position: spawnAt("position", "position", center, PACK_OFFSETS[0]).position, eventOnly: true }); }); const objectiveById = new Map(updatedBosses.map((boss) => [boss.id, boss] as const)); const gatedEntries = new Set(bossEntries); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => !gatedEntries.has(entityEntry(definition, spawn) ?? -1))), bosses: Object.freeze(updatedBosses), objectiveOrder: Object.freeze(definition.objectiveOrder.map((objective) => objectiveById.get(objective.id) ?? objective)), encounters: Object.freeze([...withoutBaselineBossProgression(definition), encounter]), encounterSpawnGroups: Object.freeze([ ...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("black-morass:rift-")), ...groups, ]), serverGameObjects: Object.freeze([ ...definition.serverGameObjects, start, ...portalObjects, ]), validation: appendValidationWarnings(definition, [ "Black Morass bosses are source-gated to rifts 6/12/18; the bounded event is playable but Medivh shield failure is not yet implemented.", ]), }); } const VIOLET_HOLD_PORTALS = Object.freeze([ sourceMapPosition([1877.51, 850.104, 44.6599]), sourceMapPosition([1918.37, 853.437, 47.1624]), sourceMapPosition([1936.07, 803.198, 53.3749]), sourceMapPosition([1927.61, 758.436, 51.4533]), sourceMapPosition([1890.64, 753.471, 48.7224]), sourceMapPosition([1908.31, 809.657, 38.7037]), ]); const VIOLET_HOLD_CENTER_PORTAL = sourceMapPosition([1896.622925, 804.854126, 38.504772]); const VIOLET_HOLD_MIDDLE = sourceMapPosition([1892.29126, 805.696838, 38.438862]); const VIOLET_HOLD_BOSS_POSITIONS = Object.freeze({ erekem: sourceMapPosition([1875.17395, 860.832703, 43.333565]), erekemGuard1: sourceMapPosition([1858.854614, 855.071411, 43.333565]), erekemGuard2: sourceMapPosition([1891.926636, 863.388977, 43.333565]), moragg: sourceMapPosition([1894.684448, 739.390503, 47.668003]), }); const VIOLET_HOLD_PORTAL_SEQUENCE = Object.freeze([0, 2, 4, 1, 3, 5] as const); const VIOLET_HOLD_MOBS = Object.freeze([ { entry: 30_660, name: "Portal Guardian", level: 77, role: "melee" }, { entry: 30_695, name: "Portal Keeper", level: 77, role: "caster" }, { entry: 30_893, name: "Portal Keeper", level: 77, role: "caster" }, { entry: 30_661, name: "Azure Invader", level: 76, role: "melee" }, { entry: 30_961, name: "Azure Invader", level: 80, role: "melee" }, { entry: 30_662, name: "Azure Spellbreaker", level: 76, role: "melee" }, { entry: 30_962, name: "Azure Spellbreaker", level: 80, role: "melee" }, { entry: 30_663, name: "Azure Binder", level: 76, role: "caster" }, { entry: 30_918, name: "Azure Binder", level: 80, role: "caster" }, { entry: 30_664, name: "Azure Mage Slayer", level: 76, role: "melee" }, { entry: 30_963, name: "Azure Mage Slayer", level: 80, role: "melee" }, { entry: 30_666, name: "Azure Captain", level: 77, role: "melee" }, { entry: 30_667, name: "Azure Sorceror", level: 77, role: "caster" }, { entry: 30_668, name: "Azure Raider", level: 77, role: "melee" }, { entry: 32_191, name: "Azure Stalker", level: 77, role: "melee" }, ] as const satisfies readonly AdaptedMobSpec[]); function objectIdsForTemplate(definition: DungeonDefinition, templateId: number): readonly string[] { return definition.serverGameObjects .filter((object) => object.source?.templateId === templateId) .map((object) => object.id); } function objectStateActions( actionPrefix: string, objectIds: readonly string[], state: string, ): DungeonEncounterAction[] { return objectIds.map((objectId, index) => ({ id: `${actionPrefix}-${index + 1}`, kind: "set-world-object-state", objectId, state, })); } function reviewedGateFallback( object: DungeonWorldObjectDefinition, sourceFiles: readonly string[], note: string, ): DungeonWorldObjectDefinition { const states = object.states.map((state) => { if (state.id === "closed") { return Object.freeze({ ...state, visible: true, blocksMovement: true, blocksSight: true, interactionEnabled: false, }); } if (state.id === "open") { return Object.freeze({ ...state, visible: true, blocksMovement: false, blocksSight: false, interactionEnabled: false, }); } return state; }); return Object.freeze({ ...object, states: Object.freeze(states), visual: Object.freeze({ color: "#6f7180", emissive: "#171923", scale: object.visual?.scale ?? ([1, 1, 1] as const), }), source: Object.freeze({ ...object.source, fidelity: "source-backed-adapted" as const, files: sourceFiles, note, }), }); } function withReviewedGateFallbacks( objects: readonly DungeonWorldObjectDefinition[], objectIds: ReadonlySet, sourceFiles: readonly string[], note: string, ): readonly DungeonWorldObjectDefinition[] { return Object.freeze(objects.map((object) => ( objectIds.has(object.id) ? reviewedGateFallback(object, sourceFiles, note) : object ))); } function reviewedInteractableFallback( object: DungeonWorldObjectDefinition, sourceFiles: readonly string[], prompt: string, signalId: string, allowedState: string, inactiveState: string, note: string, ): DungeonWorldObjectDefinition { return Object.freeze({ ...object, states: Object.freeze(object.states.map((state) => Object.freeze({ ...state, visible: state.id !== inactiveState, blocksMovement: false, blocksSight: false, interactionEnabled: state.id === allowedState, }))), interaction: Object.freeze({ prompt, range: 3, signalId, allowedStates: [allowedState], oneShot: true, }), visual: Object.freeze({ color: "#8a6642", emissive: "#24180e", scale: object.visual?.scale ?? ([1, 1, 1] as const) }), source: Object.freeze({ ...object.source, fidelity: "source-backed-adapted" as const, files: sourceFiles, note, }), }); } interface OrderedEventStage { readonly id: string; readonly group: DungeonEncounterSpawnGroupDefinition; readonly delayMs: number; readonly objective?: DungeonBossObjective; readonly checkpointId?: string; readonly openActions?: readonly DungeonEncounterAction[]; readonly clearActions?: readonly DungeonEncounterAction[]; readonly nextPhase?: string; } interface PositionedEventSpawn { readonly entry: number; readonly position: Vector3Tuple; readonly id?: string; } function eventSpawnGroup( id: string, idsByEntry: ReadonlyMap, specs: readonly PositionedEventSpawn[], ): DungeonEncounterSpawnGroupDefinition { return Object.freeze({ id, spawns: Object.freeze(specs.map((spec, index) => { const entityId = idsByEntry.get(spec.entry); if (!entityId) throw new Error(`${id} is missing creature entry ${spec.entry}`); return spawnAt(spec.id ?? `${id}:mob-${index + 1}`, entityId, spec.position, [0, 0]); })), }); } function appendOrderedEventStages( rules: DungeonEncounterRule[], prefix: string, stages: readonly OrderedEventStage[], finishActions: readonly DungeonEncounterAction[], ): void { stages.forEach((stage, index) => { rules.push({ id: `${prefix}-open-${stage.id}`, trigger: { kind: "elapsed", afterMs: stage.delayMs }, guards: [{ kind: "phase", phases: [`${stage.id}-wait`] }], actions: [ ...(stage.openActions ?? []), { id: `${prefix}-activate-${stage.id}`, kind: "activate-spawn-group", spawnGroupId: stage.group.id }, { id: `${prefix}-record-${stage.id}`, kind: "set-counter", counterId: "event-stage", value: index + 1 }, { id: `${prefix}-enter-${stage.id}`, kind: "set-phase", phase: `${stage.id}-active` }, ], }); const actions: DungeonEncounterAction[] = [ { id: `${prefix}-deactivate-${stage.id}`, kind: "deactivate-spawn-group", spawnGroupId: stage.group.id }, ...(stage.clearActions ?? []), ]; if (stage.objective) { actions.push({ id: `${prefix}-complete-${stage.id}`, kind: "complete-objective", objectiveId: stage.objective.id, persistence: "run", }); } if (stage.checkpointId) { actions.push({ id: `${prefix}-checkpoint-${stage.id}`, kind: "set-checkpoint", checkpointId: stage.checkpointId, persistence: "run", }); } const nextPhase = stage.nextPhase ?? (index + 1 < stages.length ? `${stages[index + 1].id}-wait` : undefined); if (nextPhase) { actions.push({ id: `${prefix}-next-${stage.id}`, kind: "set-phase", phase: nextPhase }); } else { actions.push(...finishActions); } rules.push({ id: `${prefix}-clear-${stage.id}`, trigger: { kind: "entities-dead", entityIds: stage.group.spawns.map((spawn) => spawn.id) }, guards: [{ kind: "phase", phases: [`${stage.id}-active`] }], actions, }); }); } function eventBossObjectives( definition: DungeonDefinition, specs: readonly { readonly entry: number; readonly names: readonly string[]; readonly position: Vector3Tuple; readonly name?: string }[], ): { readonly byEntry: ReadonlyMap; readonly bosses: readonly DungeonBossObjective[]; readonly objectiveOrder: readonly DungeonBossObjective[]; } { const replacements = new Map(); const byEntry = new Map(); for (const spec of specs) { const original = objectiveForEntry(definition, spec.entry, spec.names); const replacement = Object.freeze({ ...original, ...(spec.name ? { name: spec.name } : {}), position: spec.position, eventOnly: true, }); replacements.set(original.id, replacement); byEntry.set(spec.entry, replacement); } return { byEntry, bosses: Object.freeze(definition.bosses.map((boss) => replacements.get(boss.id) ?? boss)), objectiveOrder: Object.freeze(definition.objectiveOrder.map((objective) => replacements.get(objective.id) ?? objective)), }; } function eventBossEntityIds( entities: PopulationDefinitionMap, entries: readonly number[], ): ReadonlyMap { const result = new Map(); for (const entry of entries) { const id = findEntityIdByEntry(entities, entry); if (!id) throw new Error(`Missing event boss creature entry ${entry}`); result.set(entry, id); } return result; } function buildVioletHold(definition: DungeonDefinition): DungeonDefinition { assertSourceMapTransform(definition); const review = REVIEW_BY_DUNGEON.get(definition.id)!; const { entities, idsByEntry } = ensureAdaptedMobs(definition, VIOLET_HOLD_MOBS); const erekem = objectiveForEntry(definition, 29_315, ["First Prisoner", "Erekem"]); const moragg = objectiveForEntry(definition, 29_316, ["Second Prisoner", "Moragg"]); const cyanigosa = objectiveForEntry(definition, 31_134, ["Cyanigosa"]); const bossEntityIds = new Map([29_315, 29_316, 31_134].map((entry) => { const id = findEntityIdByEntry(entities, entry); if (!id) throw new Error(`${definition.id} is missing creature entry ${entry}`); return [entry, id] as const; })); const guardSpawns = definition.staticSpawns .filter((spawn) => entityEntry(definition, spawn) === 29_395) .sort((left, right) => left.id.localeCompare(right.id)); const guardEntityId = findEntityIdByEntry(entities, 29_395); if (!guardEntityId || guardSpawns.length < 2) { throw new Error("violet-hold requires both source Erekem guards"); } const portalObjects = VIOLET_HOLD_PORTALS.map((position, index) => eventFocusObject( `violet-hold:portal-focus-${index + 1}`, `Teleportation Portal ${index + 1}`, position, VIOLET_HOLD_FILES, "#6a82d7", )); const centerPortal = eventFocusObject( "violet-hold:portal-focus-center", "Saboteur Portal", VIOLET_HOLD_CENTER_PORTAL, VIOLET_HOLD_FILES, "#6a82d7", ); const startId = "violet-hold:sinclari-start"; const start = startObject( startId, "Lieutenant Sinclari", "Begin the Violet Hold defense", sourceMapPosition([1817.315674, 804.060608, 44.363998]), "violet-hold:start", VIOLET_HOLD_FILES, ); const mainDoorIds = objectIdsForTemplate(definition, 191_723); const erekemDoorIds = [191_564, 191_563, 191_562].flatMap((entry) => objectIdsForTemplate(definition, entry)); const moraggDoorIds = objectIdsForTemplate(definition, 191_606); const reviewedDoorIds = new Set([...mainDoorIds, ...erekemDoorIds, ...moraggDoorIds]); const reviewedObjects = withReviewedGateFallbacks( definition.serverGameObjects, reviewedDoorIds, VIOLET_HOLD_FILES, "Reviewed Violet Hold main/prison door binding. Procedural visible geometry is blocking while closed and nonblocking while open until its client model is approved.", ); const groups: DungeonEncounterSpawnGroupDefinition[] = []; const rules: DungeonEncounterRule[] = [ { id: "start-defense", trigger: { kind: "interact", objectId: startId }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "mark-violet-defense-active", kind: "set-world-object-state", objectId: startId, state: "active" }, ...objectStateActions("close-violet-main-door", mainDoorIds, "closed"), { id: "initialize-violet-wave", kind: "set-counter", counterId: "wave", value: 0 }, { id: "wait-for-violet-wave-01", kind: "set-phase", phase: "wave-01-wait" }, ], }, { id: "resume-fresh-attempt", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: erekem.id, equals: false }, { kind: "objective-complete", objectiveId: moragg.id, equals: false }, ], actions: [{ id: "resume-violet-ready", kind: "set-phase", phase: "ready" }], }, { id: "resume-after-first-prisoner", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: erekem.id, equals: true }, { kind: "objective-complete", objectiveId: moragg.id, equals: false }, ], actions: [ { id: "resume-first-mark-active", kind: "set-world-object-state", objectId: startId, state: "active" }, ...objectStateActions("resume-first-close-main-door", mainDoorIds, "closed"), { id: "resume-at-violet-wave-07", kind: "set-phase", phase: "wave-07-wait" }, ], }, { id: "resume-after-second-prisoner", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: moragg.id, equals: true }, ], actions: [ { id: "resume-second-mark-active", kind: "set-world-object-state", objectId: startId, state: "active" }, ...objectStateActions("resume-second-close-main-door", mainDoorIds, "closed"), { id: "resume-at-violet-wave-13", kind: "set-phase", phase: "wave-13-wait" }, ], }, ]; for (let wave = 1; wave <= 18; wave += 1) { const suffix = String(wave).padStart(2, "0"); const groupId = `violet-hold:wave-${suffix}`; const bossWave = wave === 6 || wave === 12 || wave === 18; const portal = bossWave ? centerPortal : portalObjects[VIOLET_HOLD_PORTAL_SEQUENCE[(wave - 1) % 6]]; const center = bossWave ? VIOLET_HOLD_CENTER_PORTAL : portal.position; const spawns: StaticMobSpawnDefinition[] = []; if (wave === 6) { spawns.push( spawnAt(erekem.id, bossEntityIds.get(29_315)!, VIOLET_HOLD_BOSS_POSITIONS.erekem, [0, 0]), spawnAt(guardSpawns[0].id, guardEntityId, VIOLET_HOLD_BOSS_POSITIONS.erekemGuard1, [0, 0]), spawnAt(guardSpawns[1].id, guardEntityId, VIOLET_HOLD_BOSS_POSITIONS.erekemGuard2, [0, 0]), ); } else if (wave === 12) { spawns.push(spawnAt(moragg.id, bossEntityIds.get(29_316)!, VIOLET_HOLD_BOSS_POSITIONS.moragg, [0, 0])); } else if (wave === 18) { spawns.push(spawnAt(cyanigosa.id, bossEntityIds.get(31_134)!, VIOLET_HOLD_MIDDLE, [0, 0])); } else { const late = wave >= 12; if (wave % 2 === 1) { const keeperEntries = [30_660, 30_695, 30_893] as const; const addEntries = late ? [30_961, 30_962, 30_918, 30_963] : [30_661, 30_662, 30_663]; const entries = [keeperEntries[wave % keeperEntries.length], ...addEntries]; entries.forEach((entry, index) => spawns.push(spawnAt( `${groupId}:mob-${index + 1}`, idsByEntry.get(entry)!, center, PACK_OFFSETS[index % PACK_OFFSETS.length], ))); } else { const eliteEntries = [30_666, 30_667, 30_668, 32_191] as const; const count = late ? 3 : 2; for (let index = 0; index < count; index += 1) { const entry = eliteEntries[(wave + index) % eliteEntries.length]; spawns.push(spawnAt( `${groupId}:mob-${index + 1}`, idsByEntry.get(entry)!, center, PACK_OFFSETS[index % PACK_OFFSETS.length], )); } } } groups.push(Object.freeze({ id: groupId, spawns: Object.freeze(spawns) })); const priorWasBoss = wave === 7 || wave === 13; const delayMs = wave === 1 ? 19_000 : priorWasBoss ? 35_000 : 3_000; const openActions: DungeonEncounterAction[] = [ { id: `show-violet-portal-${suffix}`, kind: "set-world-object-state", objectId: portal.id, state: "active" }, { id: `activate-violet-wave-${suffix}`, kind: "activate-spawn-group", spawnGroupId: groupId }, { id: `record-violet-wave-${suffix}`, kind: "set-counter", counterId: "wave", value: wave }, ]; if (wave === 6) openActions.push(...objectStateActions("open-erekem-cell", erekemDoorIds, "open")); if (wave === 12) openActions.push(...objectStateActions("open-moragg-cell", moraggDoorIds, "open")); openActions.push({ id: `enter-violet-wave-${suffix}`, kind: "set-phase", phase: `wave-${suffix}-active` }); rules.push({ id: `open-violet-wave-${suffix}`, trigger: { kind: "elapsed", afterMs: delayMs }, guards: [{ kind: "phase", phases: [`wave-${suffix}-wait`] }], actions: openActions, }); const clearActions: DungeonEncounterAction[] = [ { id: `hide-violet-portal-${suffix}`, kind: "set-world-object-state", objectId: portal.id, state: "hidden" }, { id: `deactivate-violet-wave-${suffix}`, kind: "deactivate-spawn-group", spawnGroupId: groupId }, ]; const objective = wave === 6 ? erekem : wave === 12 ? moragg : wave === 18 ? cyanigosa : undefined; if (objective) clearActions.push( { id: `complete-violet-objective-${suffix}`, kind: "complete-objective", objectiveId: objective.id, persistence: "run", }, { id: `save-violet-checkpoint-${suffix}`, kind: "set-checkpoint", checkpointId: objective.id, persistence: "checkpoint", }, ); if (wave < 18) { const next = String(wave + 1).padStart(2, "0"); clearActions.push({ id: `wait-for-violet-wave-${next}`, kind: "set-phase", phase: `wave-${next}-wait` }); } else { clearActions.push( ...objectStateActions("open-violet-main-door-complete", mainDoorIds, "open").map((action) => ({ ...action, persistence: "run" as const })), { id: "mark-violet-hold-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "complete-violet-defense", kind: "set-world-object-state", objectId: startId, state: "complete", persistence: "run" }, { id: "enter-violet-hold-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ); } rules.push({ id: `clear-violet-wave-${suffix}`, trigger: { kind: "entities-dead", entityIds: spawns.map((spawn) => spawn.id) }, guards: [{ kind: "phase", phases: [`wave-${suffix}-active`] }], actions: clearActions, }); } const encounter: DungeonEncounterDefinition = Object.freeze({ id: "violet-hold:wow-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { flags: { "dungeon-complete": false }, counters: { wave: 0 }, worldObjectStates: Object.fromEntries([ [startId, "ready"], ...portalObjects.map((object) => [object.id, "hidden"] as const), [centerPortal.id, "hidden"], ]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review, "instance-orchestration"), rules: Object.freeze(rules), }); const replacements = new Map([ [erekem.id, Object.freeze({ ...erekem, name: "Erekem", position: VIOLET_HOLD_BOSS_POSITIONS.erekem, eventOnly: true })], [moragg.id, Object.freeze({ ...moragg, name: "Moragg", position: VIOLET_HOLD_BOSS_POSITIONS.moragg, eventOnly: true })], [cyanigosa.id, Object.freeze({ ...cyanigosa, position: VIOLET_HOLD_MIDDLE, eventOnly: true })], ]); const updatedBosses = definition.bosses.map((boss) => replacements.get(boss.id) ?? boss); const gatedEntries = new Set([29_315, 29_316, 29_395, 31_134]); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => !gatedEntries.has(entityEntry(definition, spawn) ?? -1))), bosses: Object.freeze(updatedBosses), objectiveOrder: Object.freeze(definition.objectiveOrder.map((objective) => replacements.get(objective.id) ?? objective)), encounters: Object.freeze([...withoutBaselineBossProgression(definition), encounter]), encounterSpawnGroups: Object.freeze([ ...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("violet-hold:wave-")), ...groups, ]), serverGameObjects: Object.freeze([ ...reviewedObjects, start, ...portalObjects, centerPortal, ]), validation: appendValidationWarnings(definition, [ "Violet Hold now has a playable 18-wave source-backed adaptation; prison-seal damage/failure and the remaining four random prisoners still require runtime support and imported entities.", ]), }); } const CULLING_MOBS = Object.freeze([ { entry: 27_737, name: "Risen Zombie", level: 78, role: "melee" }, { entry: 28_249, name: "Devouring Ghoul", level: 78, role: "melee" }, { entry: 28_200, name: "Dark Necromancer", level: 79, role: "caster" }, { entry: 28_199, name: "Tomb Stalker", level: 79, role: "melee" }, { entry: 27_734, name: "Crypt Fiend", level: 78, role: "melee" }, { entry: 28_201, name: "Bile Golem", level: 79, role: "melee" }, { entry: 27_729, name: "Enraging Ghoul", level: 78, role: "melee" }, { entry: 27_736, name: "Patchwork Construct", level: 79, role: "melee" }, { entry: 27_742, name: "Infinite Adversary", level: 79, role: "melee" }, { entry: 27_743, name: "Infinite Hunter", level: 79, role: "caster" }, { entry: 27_744, name: "Infinite Agent", level: 79, role: "caster" }, ] as const satisfies readonly AdaptedMobSpec[]); const CULLING_CITY_WAVES: readonly (readonly PositionedEventSpawn[])[] = Object.freeze([ [[27_737, 2164.698975, 1255.392944, 135.040878], [27_737, 2183.501465, 1263.079102, 134.859055], [28_249, 2177.512939, 1247.313843, 135.846695], [28_249, 2171.991943, 1246.615845, 135.745026]], [[28_249, 2254.434326, 1163.427612, 138.055038], [28_249, 2254.703613, 1158.867798, 138.212234], [28_249, 2257.615723, 1162.310913, 138.091202], [28_200, 2258.258057, 1157.250732, 138.272873]], [[28_199, 2348.120117, 1202.30249, 130.491104], [28_249, 2352.863525, 1207.819092, 130.424271], [28_249, 2343.59375, 1207.915039, 130.781311], [28_200, 2348.257324, 1212.202515, 130.670135]], [[28_199, 2139.825195, 1356.2771, 132.199615], [28_249, 2137.073486, 1362.464844, 132.271637], [28_249, 2134.075684, 1354.148071, 131.885864], [28_200, 2133.302246, 1358.907837, 132.037689]], [[28_200, 2164.698975, 1255.392944, 135.040878], [28_249, 2183.501465, 1263.079102, 134.859055], [28_199, 2177.512939, 1247.313843, 135.846695], [27_734, 2171.991943, 1246.615845, 135.745026]], [[28_201, 2349.70166, 1188.436646, 130.428864], [28_249, 2349.90918, 1194.582642, 130.417816], [27_729, 2354.662598, 1185.692017, 130.552032], [27_729, 2354.716797, 1191.61438, 130.53981]], [[27_736, 2145.212891, 1355.288086, 132.288773], [28_200, 2137.078613, 1357.612671, 132.17334], [27_729, 2139.4021, 1352.541626, 132.127518], [27_729, 2142.408447, 1360.76062, 132.321564]], [[28_249, 2172.686279, 1259.618164, 134.391754], [27_734, 2177.64917, 1256.061157, 135.096512], [27_736, 2170.782959, 1253.594849, 134.973022], [28_200, 2175.595703, 1249.041992, 135.603531]], ].map((wave) => Object.freeze(wave.map(([entry, x, y, z]) => Object.freeze({ entry, position: sourceMapPosition([x, y, z]), }))))); const CULLING_RIFT_PACKS: readonly (readonly PositionedEventSpawn[])[] = Object.freeze([ [[27_742, 2433.85791, 1185.612061, 148.075974], [27_742, 2437.577881, 1188.241089, 148.075974], [27_744, 2437.165527, 1192.294922, 148.075974], [27_743, 2434.98999, 1197.679565, 148.075974]], [[27_744, 2403.676758, 1171.49585, 148.075607], [27_743, 2407.691162, 1172.16272, 148.075607]], [[27_742, 2416.024658, 1139.456177, 148.076431], [27_743, 2410.866699, 1139.680542, 148.076431]], [[27_742, 2426.947998, 1107.47168, 148.076019], [27_743, 2432.94458, 1108.896362, 148.20816]], [[27_742, 2438.190674, 1118.368164, 148.076172], [27_744, 2435.861328, 1113.402954, 148.169327]], ].map((wave) => Object.freeze(wave.map(([entry, x, y, z]) => Object.freeze({ entry, position: sourceMapPosition([x, y, z]), }))))); const CULLING_RIFT_POSITIONS = Object.freeze([ [2431.790039, 1190.670044, 148.076004], [2403.954834, 1178.81543, 148.075943], [2414.217041, 1133.446167, 148.07605], [2433.081543, 1099.869751, 148.076157], [2444.077637, 1114.366089, 148.076157], ].map(([x, y, z]) => sourceMapPosition([x, y, z]))); function buildCullingOfStratholme(definition: DungeonDefinition): DungeonDefinition { assertSourceMapTransform(definition); const review = REVIEW_BY_DUNGEON.get(definition.id)!; const { entities, idsByEntry } = ensureAdaptedMobs(definition, CULLING_MOBS); const bossEntries = [26_529, 26_530, 26_532, 26_533] as const; const objectiveState = eventBossObjectives(definition, [ { entry: 26_529, names: ["Meathook"], position: sourceMapPosition([2351.45, 1197.81, 130.45]) }, { entry: 26_530, names: ["Salram the Fleshcrafter", "Salramm the Fleshcrafter"], name: "Salramm the Fleshcrafter", position: sourceMapPosition([2351.45, 1197.81, 130.45]) }, { entry: 26_532, names: ["Chrono-Lord Epoch"], position: sourceMapPosition([2451.809, 1112.901, 149.22]) }, { entry: 26_533, names: ["Mal'ganis", "Mal'Ganis"], name: "Mal'Ganis", position: sourceMapPosition([2298.25, 1500.56, 128.37]) }, ]); const allIds = new Map([...idsByEntry, ...eventBossEntityIds(entities, bossEntries)]); const meathook = objectiveState.byEntry.get(26_529)!; const salramm = objectiveState.byEntry.get(26_530)!; const epoch = objectiveState.byEntry.get(26_532)!; const malganis = objectiveState.byEntry.get(26_533)!; const startId = "culling-of-stratholme:arthas-start"; const start = startObject( startId, "Prince Arthas", "Begin the Culling of Stratholme", [1287.12, 142.935, 1920.87], "culling-of-stratholme:start", CULLING_FILES, ); const riftObjects = CULLING_RIFT_POSITIONS.map((position, index) => eventFocusObject( `culling-of-stratholme:time-rift-${index + 1}`, `Town Hall Time Rift ${index + 1}`, position, CULLING_FILES, "#6e67c7", )); const groups: DungeonEncounterSpawnGroupDefinition[] = []; const stages: OrderedEventStage[] = []; let cityTrashIndex = 0; for (let wave = 1; wave <= 10; wave += 1) { const id = `culling-city-${String(wave).padStart(2, "0")}`; let group: DungeonEncounterSpawnGroupDefinition; let objective: DungeonBossObjective | undefined; if (wave === 5 || wave === 10) { objective = wave === 5 ? meathook : salramm; const entry = wave === 5 ? 26_529 : 26_530; group = eventSpawnGroup(id, allIds, [{ entry, position: objective.position, id: objective.id }]); } else { group = eventSpawnGroup(id, allIds, CULLING_CITY_WAVES[cityTrashIndex]); cityTrashIndex += 1; } groups.push(group); stages.push({ id, group, delayMs: wave === 1 ? 10_000 : 0, ...(objective ? { objective, checkpointId: objective.id } : {}), }); } const townPackIndexes = [[0], [1, 2], [3, 4]] as const; townPackIndexes.forEach((indexes, index) => { const id = `culling-town-${String(index + 1).padStart(2, "0")}`; const group = eventSpawnGroup(id, allIds, indexes.flatMap((rift) => CULLING_RIFT_PACKS[rift])); groups.push(group); stages.push({ id, group, delayMs: index === 0 ? 5_000 : 0, openActions: indexes.map((rift) => ({ id: `culling-show-town-rift-${rift + 1}`, kind: "set-world-object-state" as const, objectId: riftObjects[rift].id, state: "active", })), clearActions: indexes.map((rift) => ({ id: `culling-hide-town-rift-${rift + 1}`, kind: "set-world-object-state" as const, objectId: riftObjects[rift].id, state: "hidden", })), }); }); const epochGroup = eventSpawnGroup("culling-epoch", allIds, [{ entry: 26_532, position: epoch.position, id: epoch.id }]); const malganisGroup = eventSpawnGroup("culling-malganis", allIds, [{ entry: 26_533, position: malganis.position, id: malganis.id }]); groups.push(epochGroup, malganisGroup); stages.push( { id: "culling-epoch", group: epochGroup, delayMs: 32_000, objective: epoch, checkpointId: epoch.id }, { id: "culling-malganis", group: malganisGroup, delayMs: 5_000, objective: malganis, checkpointId: malganis.id }, ); const rules: DungeonEncounterRule[] = [ { id: "culling-start", trigger: { kind: "interact", objectId: startId }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "culling-mark-start-active", kind: "set-world-object-state", objectId: startId, state: "active" }, { id: "culling-enter-city-01", kind: "set-phase", phase: "culling-city-01-wait" }, ], }, { id: "culling-resume-complete", trigger: { kind: "tick" }, once: false, priority: 20, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "flag", flagId: "dungeon-complete", equals: true }], actions: [{ id: "culling-recover-complete", kind: "set-phase", phase: "complete" }], }, { id: "culling-resume-after-epoch", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: epoch.id, equals: true }, { kind: "objective-complete", objectiveId: malganis.id, equals: false }], actions: [{ id: "culling-recover-epoch-active", kind: "set-world-object-state", objectId: startId, state: "active" }, { id: "culling-recover-at-malganis", kind: "set-phase", phase: "culling-malganis-wait" }], }, { id: "culling-resume-after-salramm", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: salramm.id, equals: true }, { kind: "objective-complete", objectiveId: epoch.id, equals: false }], actions: [{ id: "culling-recover-salramm-active", kind: "set-world-object-state", objectId: startId, state: "active" }, { id: "culling-recover-at-town", kind: "set-phase", phase: "culling-town-01-wait" }], }, { id: "culling-resume-after-meathook", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: meathook.id, equals: true }, { kind: "objective-complete", objectiveId: salramm.id, equals: false }], actions: [{ id: "culling-recover-meathook-active", kind: "set-world-object-state", objectId: startId, state: "active" }, { id: "culling-recover-at-city-06", kind: "set-phase", phase: "culling-city-06-wait" }], }, { id: "culling-resume-fresh", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: meathook.id, equals: false }], actions: [{ id: "culling-recover-ready", kind: "set-phase", phase: "ready" }], }, ]; appendOrderedEventStages(rules, "culling", stages, [ { id: "culling-mark-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "culling-complete-start", kind: "set-world-object-state", objectId: startId, state: "complete", persistence: "run" }, { id: "culling-enter-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "culling-of-stratholme:wow-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { flags: { "dungeon-complete": false }, counters: { "event-stage": 0 }, worldObjectStates: Object.fromEntries([[startId, "ready"], ...riftObjects.map((object) => [object.id, "hidden"] as const)]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review, "instance-orchestration"), rules: Object.freeze(rules), }); const gated = new Set(bossEntries); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => !gated.has(entityEntry(definition, spawn) ?? -1))), bosses: objectiveState.bosses, objectiveOrder: objectiveState.objectiveOrder, encounters: Object.freeze([...withoutBaselineBossProgression(definition), encounter]), encounterSpawnGroups: Object.freeze([...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("culling-")), ...groups]), serverGameObjects: Object.freeze([...definition.serverGameObjects, start, ...riftObjects]), validation: appendValidationWarnings(definition, [ "Culling bosses are now source-gated behind the city/town-hall sequence; the playable adapter intentionally abbreviates Arthas escort travel and friendly-actor failure.", ]), }); } const DURNHOLDE_MOBS = Object.freeze([ { entry: 18_092, name: "Tarren Mill Guardsman", level: 66, role: "melee" }, { entry: 18_093, name: "Tarren Mill Protector", level: 66, role: "melee" }, { entry: 18_094, name: "Tarren Mill Lookout", level: 66, role: "caster" }, { entry: 23_175, name: "Epoch Guardsman", level: 67, role: "melee" }, { entry: 23_179, name: "Epoch Protector", level: 67, role: "melee" }, { entry: 23_177, name: "Epoch Lookout", level: 67, role: "caster" }, { entry: 18_171, name: "Infinite Defiler", level: 67, role: "caster" }, { entry: 18_172, name: "Infinite Saboteur", level: 67, role: "melee" }, { entry: 18_170, name: "Infinite Slayer", level: 67, role: "melee" }, ] as const satisfies readonly AdaptedMobSpec[]); function sourceFightPack(rows: readonly (readonly [number, number, number, number])[]): readonly PositionedEventSpawn[] { return Object.freeze(rows.map(([entry, x, y, z]) => Object.freeze({ entry, position: sourceMapPosition([x, y, z]), }))); } function buildEscapeFromDurnholde(definition: DungeonDefinition): DungeonDefinition { assertSourceMapTransform(definition); const review = REVIEW_BY_DUNGEON.get(definition.id)!; const { entities, idsByEntry } = ensureAdaptedMobs(definition, DURNHOLDE_MOBS); const bossEntries = [17_848, 17_862, 18_096] as const; const objectiveState = eventBossObjectives(definition, [ { entry: 17_848, names: ["Lieutenant Drake"], position: sourceMapPosition([2172.76, 149.54, 87.981]) }, { entry: 17_862, names: ["Captain Skarloc"], position: sourceMapPosition([1995.78, 277.46, 66.64]) }, { entry: 18_096, names: ["Epoch Hunter"], position: sourceMapPosition([2634.79, 672.964, 54.8577]) }, ]); const allIds = new Map([...idsByEntry, ...eventBossEntityIds(entities, bossEntries)]); const drake = objectiveState.byEntry.get(17_848)!; const skarloc = objectiveState.byEntry.get(17_862)!; const epoch = objectiveState.byEntry.get(18_096)!; const barrelIds = objectIdsForTemplate(definition, 182_589); if (barrelIds.length < 5) throw new Error("escape-from-durnholde requires at least five source barrels"); const barrelSet = new Set(barrelIds); const prisonDoorIds = objectIdsForTemplate(definition, 184_393); const thrallStartId = "escape-from-durnholde:thrall-escort"; const thrallStart = startObject( thrallStartId, "Thrall", "Continue Thrall's escape", [119.956, 82.2979, 2231.89], "escape-from-durnholde:thrall-start", DURNHOLDE_FILES, ); const reviewedBarrels = definition.serverGameObjects.map((object) => ( barrelSet.has(object.id) ? reviewedInteractableFallback( object, DURNHOLDE_FILES, "Plant an incendiary bomb", `escape-from-durnholde:barrel:${object.id}`, "available", "inactive", "Reviewed source barrel. Visible procedural geometry and a one-shot interaction implement the source five-barrel threshold.", ) : object )); const reviewedObjects = withReviewedGateFallbacks( reviewedBarrels, new Set(prisonDoorIds), DURNHOLDE_FILES, "Reviewed Durnholde prison-door binding. Procedural visible geometry is blocking while closed and nonblocking while open.", ); const groups: DungeonEncounterSpawnGroupDefinition[] = []; const drakeGroup = eventSpawnGroup("durnholde-drake", allIds, [{ entry: 17_848, position: drake.position, id: drake.id }]); const skarlocGroup = eventSpawnGroup("durnholde-skarloc", allIds, [{ entry: 17_862, position: skarloc.position, id: skarloc.id }]); const barnGroup = eventSpawnGroup("durnholde-barn", allIds, sourceFightPack([ [18_093, 2652.71, 660.31, 61.93], [18_094, 2648.96, 662.59, 61.93], [18_092, 2657.36, 662.34, 61.93], [18_092, 2656.39, 659.77, 61.93], ])); const churchGroup = eventSpawnGroup("durnholde-church", allIds, sourceFightPack([ [18_093, 2630.75, 664.8, 54.28], [18_094, 2632.2, 661.98, 54.3], [18_092, 2630.02, 662.75, 54.28], [18_092, 2632.86, 664.05, 54.31], ])); const epochWave1 = eventSpawnGroup("durnholde-epoch-wave-1", allIds, sourceFightPack([ [23_177, 2647.57, 701.17, 56.215], [23_175, 2629.46, 704.76, 56.286], [23_179, 2640.14, 709.44, 56.135], ])); const epochWave2 = eventSpawnGroup("durnholde-epoch-wave-2", allIds, sourceFightPack([ [18_172, 2599.57, 683.72, 55.975], [18_170, 2599.57, 677, 55.975], [18_171, 2592.57, 680, 55.975], ])); const epochWave3 = eventSpawnGroup("durnholde-epoch-wave-3", allIds, sourceFightPack([ [18_170, 2642.62, 701.43, 55.965], [18_170, 2638.62, 701.43, 55.965], [18_172, 2638.62, 705.43, 55.965], [18_171, 2642.62, 705.43, 55.965], ])); const epochGroup = eventSpawnGroup("durnholde-epoch", allIds, [{ entry: 18_096, position: epoch.position, id: epoch.id }]); groups.push(drakeGroup, skarlocGroup, barnGroup, churchGroup, epochWave1, epochWave2, epochWave3, epochGroup); const stages: OrderedEventStage[] = [ { id: "durnholde-drake", group: drakeGroup, delayMs: 12_000, objective: drake, checkpointId: drake.id, nextPhase: "durnholde-await-thrall", clearActions: [ ...objectStateActions("durnholde-open-prison", prisonDoorIds, "open").map((action) => ({ ...action, persistence: "run" as const })), { id: "durnholde-enable-thrall", kind: "set-world-object-state", objectId: thrallStartId, state: "ready" }, ], }, { id: "durnholde-skarloc", group: skarlocGroup, delayMs: 0, objective: skarloc, checkpointId: skarloc.id }, { id: "durnholde-barn", group: barnGroup, delayMs: 3_000 }, { id: "durnholde-church", group: churchGroup, delayMs: 3_000 }, { id: "durnholde-epoch-wave-1", group: epochWave1, delayMs: 3_000 }, { id: "durnholde-epoch-wave-2", group: epochWave2, delayMs: 0 }, { id: "durnholde-epoch-wave-3", group: epochWave3, delayMs: 0 }, { id: "durnholde-epoch", group: epochGroup, delayMs: 8_000, objective: epoch, checkpointId: epoch.id }, ]; const rules: DungeonEncounterRule[] = barrelIds.map((objectId, index) => ({ id: `durnholde-plant-barrel-${index + 1}`, trigger: { kind: "interact", objectId }, guards: [ { kind: "phase", phases: ["durnholde-barrels"] }, { kind: "counter", counterId: "barrels", comparison: "at-most", value: 4 }, ], actions: [ { id: `durnholde-use-barrel-${index + 1}`, kind: "set-world-object-state", objectId, state: "inactive" }, { id: `durnholde-count-barrel-${index + 1}`, kind: "increment-counter", counterId: "barrels" }, ], })); rules.push( { id: "durnholde-start-after-five-barrels", trigger: { kind: "tick" }, guards: [{ kind: "phase", phases: ["durnholde-barrels"] }, { kind: "counter", counterId: "barrels", comparison: "at-least", value: 5 }], actions: [ ...objectStateActions("durnholde-disable-unused-barrels", barrelIds, "inactive"), { id: "durnholde-wait-for-drake", kind: "set-phase", phase: "durnholde-drake-wait" }, ], }, { id: "durnholde-continue-with-thrall", trigger: { kind: "interact", objectId: thrallStartId }, guards: [{ kind: "phase", phases: ["durnholde-await-thrall"] }], actions: [ { id: "durnholde-mark-thrall-active", kind: "set-world-object-state", objectId: thrallStartId, state: "active" }, { id: "durnholde-wait-for-skarloc", kind: "set-phase", phase: "durnholde-skarloc-wait" }, ], }, { id: "durnholde-resume-complete", trigger: { kind: "tick" }, once: false, priority: 20, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "flag", flagId: "dungeon-complete", equals: true }], actions: [{ id: "durnholde-recover-complete", kind: "set-phase", phase: "complete" }], }, { id: "durnholde-resume-after-skarloc", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: skarloc.id, equals: true }, { kind: "objective-complete", objectiveId: epoch.id, equals: false }], actions: [ { id: "durnholde-recover-skarloc-thrall", kind: "set-world-object-state", objectId: thrallStartId, state: "active" }, { id: "durnholde-recover-at-barn", kind: "set-phase", phase: "durnholde-barn-wait" }, ], }, { id: "durnholde-resume-after-drake", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: drake.id, equals: true }, { kind: "objective-complete", objectiveId: skarloc.id, equals: false }], actions: [ ...objectStateActions("durnholde-recover-open-prison", prisonDoorIds, "open"), { id: "durnholde-recover-enable-thrall", kind: "set-world-object-state", objectId: thrallStartId, state: "ready" }, { id: "durnholde-recover-await-thrall", kind: "set-phase", phase: "durnholde-await-thrall" }, ], }, { id: "durnholde-resume-fresh", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: drake.id, equals: false }], actions: [{ id: "durnholde-recover-barrels", kind: "set-phase", phase: "durnholde-barrels" }], }, ); appendOrderedEventStages(rules, "durnholde", stages, [ { id: "durnholde-mark-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" }, { id: "durnholde-complete-thrall", kind: "set-world-object-state", objectId: thrallStartId, state: "complete", persistence: "run" }, { id: "durnholde-enter-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "escape-from-durnholde:wow-instance", dungeonId: definition.id, initialPhase: "durnholde-barrels", initialState: { flags: { "dungeon-complete": false }, counters: { barrels: 0, "event-stage": 0 }, worldObjectStates: Object.fromEntries([ ...barrelIds.map((id) => [id, "available"] as const), ...prisonDoorIds.map((id) => [id, "closed"] as const), [thrallStartId, "hidden"], ]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review, "instance-orchestration"), rules: Object.freeze(rules), }); const gated = new Set(bossEntries); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => !gated.has(entityEntry(definition, spawn) ?? -1))), bosses: objectiveState.bosses, objectiveOrder: objectiveState.objectiveOrder, encounters: Object.freeze([...withoutBaselineBossProgression(definition), encounter]), encounterSpawnGroups: Object.freeze([...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("durnholde-")), ...groups]), serverGameObjects: Object.freeze([...reviewedObjects, thrallStart]), validation: appendValidationWarnings(definition, [ "Durnholde bosses are source-gated by five barrels, Thrall, and the Epoch wave sequence; protected escort/failure behavior remains an explicit adaptation gap.", ]), }); } const ARCATRAZ_EVENT_MOBS = Object.freeze([ { entry: 20_905, name: "Arcatraz Trickster", level: 70, role: "caster" }, { entry: 20_908, name: "Akkiris Lightning-Waker", level: 70, role: "caster" }, { entry: 20_910, name: "Twilight Drakonaar", level: 70, role: "melee" }, ] as const satisfies readonly AdaptedMobSpec[]); function buildArcatraz(definition: DungeonDefinition): DungeonDefinition { const review = REVIEW_BY_DUNGEON.get(definition.id)!; const { entities, idsByEntry } = ensureAdaptedMobs(definition, ARCATRAZ_EVENT_MOBS); const originalSkyriss = objectiveForEntry(definition, 20_912, ["Harbinger Skyriss"]); const objectiveState = eventBossObjectives(definition, [{ entry: 20_912, names: ["Harbinger Skyriss"], position: originalSkyriss.position, }]); const skyriss = objectiveState.byEntry.get(20_912)!; const allIds = new Map([...idsByEntry, ...eventBossEntityIds(entities, [20_912])]); const podTemplates = [183_961, 183_963, 183_964, 183_962, 183_965] as const; const podIds = podTemplates.map((templateId) => { const ids = objectIdsForTemplate(definition, templateId); if (ids.length !== 1) throw new Error(`arcatraz requires one source pod ${templateId}`); return ids[0]; }); const podObjects = podIds.map((id) => definition.serverGameObjects.find((object) => object.id === id)!); const shieldIds = objectIdsForTemplate(definition, 184_802); const reviewedIds = new Set([...podIds, ...shieldIds]); const reviewedObjects = withReviewedGateFallbacks( definition.serverGameObjects, reviewedIds, ARCATRAZ_FILES, "Reviewed Arcatraz pod/shield binding. Visible procedural geometry blocks movement and sight while closed, then becomes nonblocking when the source sequence opens it.", ); const startId = "arcatraz:warden-start"; const start = startObject( startId, "Warden Mellichar", "Begin the containment release", [-445.803, 43.6442, -169.007], "arcatraz:warden-start", ARCATRAZ_FILES, ); const alphaGroup = eventSpawnGroup("arcatraz-pod-alpha", allIds, [{ entry: 20_905, position: podObjects[0].position }]); const deltaGroup = eventSpawnGroup("arcatraz-pod-delta", allIds, [{ entry: 20_908, position: podObjects[2].position }]); const gammaGroup = eventSpawnGroup("arcatraz-pod-gamma", allIds, [{ entry: 20_910, position: podObjects[3].position }]); const skyrissGroup = eventSpawnGroup("arcatraz-skyriss", allIds, [{ entry: 20_912, position: skyriss.position, id: skyriss.id }]); const groups = Object.freeze([alphaGroup, deltaGroup, gammaGroup, skyrissGroup]); const rules: DungeonEncounterRule[] = [ { id: "arcatraz-start", trigger: { kind: "interact", objectId: startId }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "arcatraz-mark-warden-active", kind: "set-world-object-state", objectId: startId, state: "active" }, ...objectStateActions("arcatraz-close-shield", shieldIds, "closed"), { id: "arcatraz-wait-alpha", kind: "set-phase", phase: "arcatraz-alpha-wait" }, ], }, { id: "arcatraz-open-alpha", trigger: { kind: "elapsed", afterMs: 5_000 }, guards: [{ kind: "phase", phases: ["arcatraz-alpha-wait"] }], actions: [ { id: "arcatraz-open-alpha-pod", kind: "set-world-object-state", objectId: podIds[0], state: "open" }, { id: "arcatraz-activate-alpha", kind: "activate-spawn-group", spawnGroupId: alphaGroup.id }, { id: "arcatraz-enter-alpha", kind: "set-phase", phase: "arcatraz-alpha-active" }, ], }, { id: "arcatraz-clear-alpha", trigger: { kind: "entities-dead", entityIds: alphaGroup.spawns.map((spawn) => spawn.id) }, guards: [{ kind: "phase", phases: ["arcatraz-alpha-active"] }], actions: [ { id: "arcatraz-deactivate-alpha", kind: "deactivate-spawn-group", spawnGroupId: alphaGroup.id }, { id: "arcatraz-wait-beta", kind: "set-phase", phase: "arcatraz-beta-wait" }, ], }, { id: "arcatraz-release-millhouse", trigger: { kind: "elapsed", afterMs: 5_000 }, guards: [{ kind: "phase", phases: ["arcatraz-beta-wait"] }], actions: [ { id: "arcatraz-open-beta-pod", kind: "set-world-object-state", objectId: podIds[1], state: "open" }, { id: "arcatraz-wait-delta", kind: "set-phase", phase: "arcatraz-delta-wait" }, ], }, { id: "arcatraz-open-delta", trigger: { kind: "elapsed", afterMs: 10_000 }, guards: [{ kind: "phase", phases: ["arcatraz-delta-wait"] }], actions: [ { id: "arcatraz-open-delta-pod", kind: "set-world-object-state", objectId: podIds[2], state: "open" }, { id: "arcatraz-activate-delta", kind: "activate-spawn-group", spawnGroupId: deltaGroup.id }, { id: "arcatraz-enter-delta", kind: "set-phase", phase: "arcatraz-delta-active" }, ], }, { id: "arcatraz-clear-delta", trigger: { kind: "entities-dead", entityIds: deltaGroup.spawns.map((spawn) => spawn.id) }, guards: [{ kind: "phase", phases: ["arcatraz-delta-active"] }], actions: [ { id: "arcatraz-deactivate-delta", kind: "deactivate-spawn-group", spawnGroupId: deltaGroup.id }, { id: "arcatraz-wait-gamma", kind: "set-phase", phase: "arcatraz-gamma-wait" }, ], }, { id: "arcatraz-open-gamma", trigger: { kind: "elapsed", afterMs: 8_000 }, guards: [{ kind: "phase", phases: ["arcatraz-gamma-wait"] }], actions: [ { id: "arcatraz-open-gamma-pod", kind: "set-world-object-state", objectId: podIds[3], state: "open" }, { id: "arcatraz-activate-gamma", kind: "activate-spawn-group", spawnGroupId: gammaGroup.id }, { id: "arcatraz-enter-gamma", kind: "set-phase", phase: "arcatraz-gamma-active" }, ], }, { id: "arcatraz-clear-gamma", trigger: { kind: "entities-dead", entityIds: gammaGroup.spawns.map((spawn) => spawn.id) }, guards: [{ kind: "phase", phases: ["arcatraz-gamma-active"] }], actions: [ { id: "arcatraz-deactivate-gamma", kind: "deactivate-spawn-group", spawnGroupId: gammaGroup.id }, { id: "arcatraz-wait-omega", kind: "set-phase", phase: "arcatraz-omega-wait" }, ], }, { id: "arcatraz-open-omega", trigger: { kind: "elapsed", afterMs: 12_000 }, guards: [{ kind: "phase", phases: ["arcatraz-omega-wait"] }], actions: [ { id: "arcatraz-open-omega-pod", kind: "set-world-object-state", objectId: podIds[4], state: "open" }, ...objectStateActions("arcatraz-open-shield", shieldIds, "open"), { id: "arcatraz-activate-skyriss", kind: "activate-spawn-group", spawnGroupId: skyrissGroup.id }, { id: "arcatraz-enter-skyriss", kind: "set-phase", phase: "arcatraz-skyriss-active" }, ], }, { id: "arcatraz-clear-skyriss", trigger: { kind: "entities-dead", entityIds: [skyriss.id] }, guards: [{ kind: "phase", phases: ["arcatraz-skyriss-active"] }], actions: [ { id: "arcatraz-deactivate-skyriss", kind: "deactivate-spawn-group", spawnGroupId: skyrissGroup.id }, { id: "arcatraz-complete-skyriss", kind: "complete-objective", objectiveId: skyriss.id, persistence: "run" }, { id: "arcatraz-complete-warden", kind: "set-world-object-state", objectId: startId, state: "complete", persistence: "run" }, { id: "arcatraz-enter-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ], }, { id: "arcatraz-resume-complete", trigger: { kind: "tick" }, once: false, priority: 20, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: skyriss.id, equals: true }], actions: [{ id: "arcatraz-recover-complete", kind: "set-phase", phase: "complete" }], }, { id: "arcatraz-resume-fresh", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: skyriss.id, equals: false }], actions: [{ id: "arcatraz-recover-ready", kind: "set-phase", phase: "ready" }], }, ]; const encounter: DungeonEncounterDefinition = Object.freeze({ id: "arcatraz:wow-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { worldObjectStates: Object.fromEntries([ [startId, "ready"], ...podIds.map((id) => [id, "closed"] as const), ...shieldIds.map((id) => [id, "open"] as const), ]), }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review), rules: Object.freeze(rules), }); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => entityEntry(definition, spawn) !== 20_912)), bosses: objectiveState.bosses, objectiveOrder: objectiveState.objectiveOrder, encounters: Object.freeze([...(definition.encounters ?? []).filter((value) => value.id !== encounter.id), encounter]), encounterSpawnGroups: Object.freeze([...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("arcatraz-")), ...groups]), serverGameObjects: Object.freeze([...reviewedObjects, start]), validation: appendValidationWarnings(definition, [ "Harbinger Skyriss is now source-gated behind all five visible, blocking Arcatraz pod transitions; friendly Millhouse behavior remains a timed presentation adapter.", ]), }); } const BLACKROCK_SPIRE_EVENT_MOBS = Object.freeze([ { entry: 10_258, name: "Rookery Guardian", level: 57, role: "melee" }, { entry: 10_683, name: "Rookery Hatcher", level: 57, role: "caster" }, ] as const satisfies readonly AdaptedMobSpec[]); function buildBlackrockSpire(definition: DungeonDefinition): DungeonDefinition { const review = REVIEW_BY_DUNGEON.get(definition.id)!; const { entities, idsByEntry } = ensureAdaptedMobs(definition, BLACKROCK_SPIRE_EVENT_MOBS); const originalSolakar = objectiveForEntry(definition, 10_264, ["Solakar Flamewreath"]); const fatherFlameIds = objectIdsForTemplate(definition, 175_245); if (fatherFlameIds.length !== 1) throw new Error("blackrock-spire requires one imported Father Flame object"); const fatherFlameId = fatherFlameIds[0]; const fatherFlame = definition.serverGameObjects.find((object) => object.id === fatherFlameId)!; const eventCenter: Vector3Tuple = [fatherFlame.position[0], fatherFlame.position[1], fatherFlame.position[2] + 8]; const objectiveState = eventBossObjectives(definition, [{ entry: 10_264, names: ["Solakar Flamewreath"], position: eventCenter, }]); const solakar = objectiveState.byEntry.get(10_264)!; const allIds = new Map([...idsByEntry, ...eventBossEntityIds(entities, [10_264])]); const reviewedObjects = definition.serverGameObjects.map((object) => ( object.id === fatherFlameId ? reviewedInteractableFallback( object, BLACKROCK_SPIRE_FILES, "Activate Father Flame", "blackrock-spire:father-flame-start", "available", "inactive", "Reviewed Father Flame start hook. Visible procedural geometry exposes the source event interaction until its client display is approved.", ) : object )); const waveGroups = Array.from({ length: 5 }, (_, index) => eventSpawnGroup( `blackrock-spire:solakar-wave-${index + 1}`, allIds, [ { entry: 10_258, position: [eventCenter[0] - 6, eventCenter[1], eventCenter[2]], id: `blackrock-spire:solakar-wave-${index + 1}:guardian` }, { entry: 10_683, position: [eventCenter[0] + 6, eventCenter[1], eventCenter[2]], id: `blackrock-spire:solakar-wave-${index + 1}:hatcher` }, ], )); const solakarGroup = eventSpawnGroup("blackrock-spire:solakar", allIds, [{ entry: 10_264, position: eventCenter, id: solakar.id }]); const groups = Object.freeze([...waveGroups, solakarGroup]); const rules: DungeonEncounterRule[] = [ { id: "blackrock-spire-start-solakar", trigger: { kind: "interact", objectId: fatherFlameId }, guards: [{ kind: "phase", phases: ["ready"] }], actions: [ { id: "blackrock-spire-disable-father-flame", kind: "set-world-object-state", objectId: fatherFlameId, state: "inactive" }, { id: "blackrock-spire-wait-solakar-wave-1", kind: "set-phase", phase: "solakar-wave-1-wait" }, ], }, { id: "blackrock-spire-resume-solakar-complete", trigger: { kind: "tick" }, once: false, priority: 20, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: solakar.id, equals: true }], actions: [{ id: "blackrock-spire-recover-solakar-complete", kind: "set-phase", phase: "complete" }], }, { id: "blackrock-spire-resume-solakar-fresh", trigger: { kind: "tick" }, once: false, guards: [{ kind: "phase", phases: ["resume"] }, { kind: "objective-complete", objectiveId: solakar.id, equals: false }], actions: [{ id: "blackrock-spire-recover-solakar-ready", kind: "set-phase", phase: "ready" }], }, ]; waveGroups.forEach((group, index) => { const wave = index + 1; rules.push({ id: `blackrock-spire-open-solakar-wave-${wave}`, trigger: { kind: "elapsed", afterMs: wave === 1 ? 500 : 30_000 }, guards: [{ kind: "phase", phases: [`solakar-wave-${wave}-wait`] }], actions: [ { id: `blackrock-spire-activate-solakar-wave-${wave}`, kind: "activate-spawn-group", spawnGroupId: group.id }, { id: `blackrock-spire-record-solakar-wave-${wave}`, kind: "set-counter", counterId: "solakar-wave", value: wave }, { id: `blackrock-spire-wait-after-solakar-wave-${wave}`, kind: "set-phase", phase: wave < 5 ? `solakar-wave-${wave + 1}-wait` : "solakar-boss-wait" }, ], }); }); rules.push( { id: "blackrock-spire-open-solakar-boss", trigger: { kind: "elapsed", afterMs: 30_000 }, guards: [{ kind: "phase", phases: ["solakar-boss-wait"] }], actions: [ { id: "blackrock-spire-activate-solakar", kind: "activate-spawn-group", spawnGroupId: solakarGroup.id }, { id: "blackrock-spire-enter-solakar", kind: "set-phase", phase: "solakar-active" }, ], }, { id: "blackrock-spire-clear-solakar", trigger: { kind: "entities-dead", entityIds: [solakar.id] }, guards: [{ kind: "phase", phases: ["solakar-active"] }], actions: [ { id: "blackrock-spire-deactivate-solakar", kind: "deactivate-spawn-group", spawnGroupId: solakarGroup.id }, ...waveGroups.map((group, index) => ({ id: `blackrock-spire-cleanup-solakar-wave-${index + 1}`, kind: "deactivate-spawn-group" as const, spawnGroupId: group.id, })), { id: "blackrock-spire-complete-solakar", kind: "complete-objective", objectiveId: solakar.id, persistence: "run" }, { id: "blackrock-spire-complete-father-flame", kind: "set-world-object-state", objectId: fatherFlameId, state: "inactive", persistence: "run" }, { id: "blackrock-spire-enter-solakar-complete", kind: "set-phase", phase: "complete", persistence: "run" }, ], }, ); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "blackrock-spire:wow-instance", dungeonId: definition.id, initialPhase: "ready", initialState: { counters: { "solakar-wave": 0 }, worldObjectStates: { [fatherFlameId]: "available" }, }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(review), rules: Object.freeze(rules), }); return Object.freeze({ ...definition, entities, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => entityEntry(definition, spawn) !== 10_264)), bosses: objectiveState.bosses, objectiveOrder: objectiveState.objectiveOrder, encounters: Object.freeze([...(definition.encounters ?? []).filter((value) => value.id !== encounter.id), encounter]), encounterSpawnGroups: Object.freeze([...(definition.encounterSpawnGroups ?? []).filter((group) => !group.id.startsWith("blackrock-spire:solakar")), ...groups]), serverGameObjects: Object.freeze(reviewedObjects), validation: appendValidationWarnings(definition, [ "Solakar is now source-gated behind five overlapping 30-second Rookery waves; killing him explicitly cleans up surviving event mobs.", ]), }); } /** * Replaces unsafe static event-boss fallbacks with reviewed playable WoW * instance adapters. Generated package files remain immutable. */ export function withWowEncounterContent(definition: DungeonDefinition): DungeonDefinition { if (definition.encounters?.some((encounter) => encounter.id === `${definition.id}:wow-instance`)) { return definition; } if (definition.id === "black-morass") return buildBlackMorass(definition); if (definition.id === "violet-hold") return buildVioletHold(definition); if (definition.id === "culling-of-stratholme") return buildCullingOfStratholme(definition); if (definition.id === "escape-from-durnholde") return buildEscapeFromDurnholde(definition); if (definition.id === "arcatraz") return buildArcatraz(definition); if (definition.id === "blackrock-spire") return buildBlackrockSpire(definition); return definition; }