import type { DungeonBossObjective, DungeonDefinition, DungeonEncounterSpawnGroupDefinition, } from "./dungeonTypes"; import type { DungeonEncounterDefinition, DungeonEncounterRule, } from "./dungeonEncounterTypes"; import type { DungeonWorldObjectDefinition } from "./dungeonWorldObjects"; import type { StaticMobSpawnDefinition } from "./mobPopulation"; interface ProgressionSourceReview { readonly dungeonId: string; readonly files: readonly string[]; readonly reviewedBehavior: string; readonly adaptations: readonly string[]; readonly remainingGaps: readonly string[]; } const SOURCE_ROOT = "../HealerMan-Storage/pipeline-work/dungeons/azerothcore-scripts/src/server/scripts"; const WORLD_SOURCE_ROOT = "../HealerMan-Storage/pipeline-work/dungeons/azerothcore-world"; export const WOW_PROGRESSION_OBJECT_REVIEWS: readonly ProgressionSourceReview[] = Object.freeze([ { dungeonId: "deadmines", files: [ `${SOURCE_ROOT}/EasternKingdoms/Deadmines/instance_deadmines.cpp (sha256 c13065819443a70ec431b921edd2e32bcd6e0ec66c4c84c3f2e932e9fb1cbbc9; saved Rhahk'Zor and cannon state lines 31-95)`, `${SOURCE_ROOT}/EasternKingdoms/Deadmines/deadmines.h (sha256 a510bb170ac09b78c2f095742aa668c1aac8bf7ba8b457f6923807f37ae98045; encounter and gameobject entries lines 25-45)`, `${WORLD_SOURCE_ROOT}/smart_scripts.sql (sha256 622b5865f171b0b7a4dd93ecba55afd673c393c579485c17f827b31cb6e47dd2; Rhahk'Zor rows 4777-4780, cannon rows 17399 and 43337-43343, lever rows 38147-38150)`, ], reviewedBehavior: "Rhahk'Zor opens the Factory Door; the foundry lever opens the Foundry Door; the Defias Cannon breaches the Iron Clad Door and summons two Defias Squallshapers, with the nearby inside lever retaining its database-linked door control.", adaptations: [ "The source cannon requires a Gunpowder spell hit; Healer Man exposes the cannon as a direct one-shot interaction because inventory quest-item spell casting is not part of the dungeon runtime.", "The cannon's custom animation and Mr. Smite dialogue are collapsed into the immediate breach and exact two-add reinforcement spawn.", "Source transforms and client bounds drive procedural doors, levers, and cannon collision while their native legacy models await export.", ], remainingGaps: [ "Heavy Door 1, Heavy Door 2, and the Mast Room Door are still left nonblocking because the reviewed source only persists their generic client/core state and does not bind an instance progression event.", "Cannon smoke, timed dialogue, native door/lever animation, and the original gunpowder item requirement remain unported presentation or inventory mechanics.", ], }, { dungeonId: "shadowfang-keep", files: [ `${SOURCE_ROOT}/EasternKingdoms/ShadowfangKeep/instance_shadowfang_keep.cpp (sha256 12d7568b35b30a7d2630de46729d4ffaa0d58de9c345c6c9143037bf88b59e07; saved courtyard, Fenrus, and Nandos door state lines 76-111)`, `${SOURCE_ROOT}/EasternKingdoms/ShadowfangKeep/shadowfang_keep.h (sha256 daa0ac298fe8c23c4e9353be898bec0fa6b7cdd9c35e821ef880aaa8600419fd; encounter and gameobject entries lines 25-48)`, `${WORLD_SOURCE_ROOT}/smart_scripts.sql (sha256 622b5865f171b0b7a4dd93ecba55afd673c393c579485c17f827b31cb6e47dd2; prisoner escort/unlock rows 7933-7945 and 40678-40693)`, ], reviewedBehavior: "The courtyard route opens after the Rethilgore/prisoner section, Fenrus opens the Sorcerer Door, and Wolf Master Nandos opens the final Arugal Door.", adaptations: [ "The two faction-specific prisoner escorts are collapsed into an immediate courtyard-door transition when Rethilgore dies, avoiding a mandatory faction/gossip branch that the current runtime cannot represent safely.", "The exact imported door transforms and display bounds provide procedural blockers while native legacy door animations await export.", ], remainingGaps: [ "Deathstalker Adamant and Sorcerer Ashcrombe dialogue, waypoint movement, spell visuals, faction-specific gossip, and despawn presentation remain unported.", "The seasonal Apothecary Hummel event is intentionally outside the ordinary dungeon route and has not been made a required objective.", ], }, { dungeonId: "maraudon", files: [ `${SOURCE_ROOT}/Kalimdor/Maraudon/instance_maraudon.cpp (sha256 edc4b2c0ba055f9953186bda5009ac990e1b18d206ae8d9aa60dcbf24f259d09; saved Noxxion state and spewer transition lines 31-79)`, `${SOURCE_ROOT}/Kalimdor/Maraudon/maraudon.h (sha256 81939372ae6bb38d879cda349992c1895e7281f5364ddca4e0fc235140e6cbfa; encounter and Corruption Spewer entries lines 20-33)`, ], reviewedBehavior: "Defeating Noxxion permanently opens the Corruption Spewer route transition.", adaptations: [ "The imported transform and display bounds drive a conservative procedural barrier that disappears when opened; the legacy spewer animation and particles await native model export.", ], remainingGaps: [ "Corruption spray/particle presentation, the native gameobject animation, optional quest interactions, and Celebras's Scepter shortcut are not part of this mandatory route gate.", ], }, { dungeonId: "mechanar", files: [ `${SOURCE_ROOT}/Outland/TempestKeep/Mechanar/instance_mechanar.cpp (sha256 6dac8f562fe69dd5ec06ef94ffea5399ae08c9b502eec3fa8790390a0fde90b7; DoorData lines 22-27)`, `${SOURCE_ROOT}/Outland/TempestKeep/Mechanar/mechanar.h (sha256 9c5baf35ca4b6126901d39278a289a42efce0b3d6c56d1ac1231cd8ddb6d9283; data and gameobject entries lines 29-52)`, ], reviewedBehavior: "Gatewatcher Iron-Hand and Gatewatcher Gyro-Kill each open their bound Mo'arg passage door; Sepethrea's room door closes while she is engaged and opens after her defeat.", adaptations: [ "The exact imported door transforms and display bounds drive procedural blockers while the three native MDX door models await export.", "Door motion is represented by visible/blocked and hidden/unblocked states rather than the client door animation.", ], remainingGaps: [ "Pathaleon's bridge trash death counter, bridge dialogue, Cache of the Legion rules, and elevator operation are not yet ported.", ], }, { dungeonId: "steamvault", files: [ `${SOURCE_ROOT}/Outland/CoilfangReservoir/SteamVault/instance_steam_vault.cpp (sha256 55b1bd5fd6a01233ca18a35eb9b0c098bbb996cfd220d8f4c3e51f992e3c60cf; access panels lines 36-91 and boss state lines 154-174)`, `${SOURCE_ROOT}/Outland/CoilfangReservoir/SteamVault/steam_vault.h (sha256 94d142e428a796cf4c0d36ec6b9b7fe70f059790d6378bbb55c7803bf9cda393; object entries lines 31-50)`, ], reviewedBehavior: "Hydromancer Thespia and Mekgineer Steamrigger each unlock their nearby access panel; using both panels opens the Main Chambers door to Warlord Kalithresh.", adaptations: [ "Source positions identify both panels and the main door, with compact procedural consoles and a procedural collision blocker until their client models are exported.", "The original door-controller creature's movement and emote are collapsed into the immediate two-panel door transition.", ], remainingGaps: [ "Door-controller presentation, distiller reset coupling, access-panel animation, and native object visuals remain unported.", ], }, { dungeonId: "shadow-labyrinth", files: [ `${SOURCE_ROOT}/Outland/Auchindoun/ShadowLabyrinth/instance_shadow_labyrinth.cpp (sha256 b51d655ee5918d0bcb89b4ada5d9af752c8c0395032d99e4148b9edaaf34a834; ritualist release lines 58-89 and DoorData lines 24-29)`, `${SOURCE_ROOT}/Outland/Auchindoun/ShadowLabyrinth/shadow_labyrinth.h (sha256 11f6bf85abe38fab9d4182325cc6a4693e3ea724e49f211355756345a7d44046; ritualist, Hellmaw, and door entries lines 28-44)`, ], reviewedBehavior: "The common Cabal Ritualists keep Ambassador Hellmaw banished; Blackheart and Vorpil open the Refectory and Screaming Hall passage doors on defeat.", adaptations: [ "Hellmaw is removed from the unsafe static spawn and appears only after the 18 normal/common ritualists die.", "Heroic-only duplicate ritualists do not delay release because the encounter reducer has no active-difficulty guard; they remain ordinary combat population.", "The exact imported door transforms and bounds use procedural blockers until native door models are exported.", ], remainingGaps: [ "Hellmaw's banish channel presentation, heroic-only ritualist contribution, Blackheart room aggro choreography, and native door animations remain unported.", ], }, { dungeonId: "scarlet-monastery", files: [ `${SOURCE_ROOT}/EasternKingdoms/ScarletMonastery/instance_scarlet_monastery.cpp (sha256 1bada7d72e38197e6d8d202d39940a27b69e6dbbf795a53e97744227aa8ca01e; cathedral pair and doors lines 38-69 and 132-197)`, ], reviewedBehavior: "Commander Mograine gates High Inquisitor Whitemane behind the High Inquisitor's Door as one linked cathedral encounter.", adaptations: [ "Whitemane is removed from the unsafe static spawn, the imported inner door opens on Mograine's first defeat, and Whitemane then spawns at her exact database position.", "The source resurrection duet is represented as two ordered lives because the current encounter action vocabulary cannot revive an existing combat actor.", "The Chapel Door is left open; its source Ashbringer event is optional external narrative content and must not block the ordinary cathedral route.", ], remainingGaps: [ "Whitemane's sleep/resurrection of Mograine, Mograine's post-resurrection phase, cathedral-wide social aggro, Ashbringer scene, and native door animation remain unported.", ], }, ]); const REVIEW_BY_DUNGEON = new Map( WOW_PROGRESSION_OBJECT_REVIEWS.map((review) => [review.dungeonId, review] as const), ); function entityEntry(definition: DungeonDefinition, entityId: string): number | undefined { return definition.entities[entityId]?.combat?.serverEntry; } function staticSpawnForEntry(definition: DungeonDefinition, entry: number): StaticMobSpawnDefinition { const spawn = definition.staticSpawns.find((candidate) => entityEntry(definition, candidate.entityId) === entry); if (!spawn) throw new Error(`${definition.id} is missing static creature entry ${entry}`); return spawn; } function actorIdForEntry(definition: DungeonDefinition, entry: number): string { const staticSpawn = definition.staticSpawns.find( (candidate) => entityEntry(definition, candidate.entityId) === entry, ); if (staticSpawn) return staticSpawn.id; for (const pack of definition.roamingPacks) { const member = pack.members.find((candidate) => entityEntry(definition, candidate.entityId) === entry); if (member) return `${pack.id}:${member.id}`; } throw new Error(`${definition.id} is missing runtime actor for creature entry ${entry}`); } function objectiveForEntry(definition: DungeonDefinition, entry: number): DungeonBossObjective { const actorIds = new Set(); for (const spawn of definition.staticSpawns) { if (entityEntry(definition, spawn.entityId) === entry) actorIds.add(spawn.id); } for (const pack of definition.roamingPacks) { for (const member of pack.members) { if (entityEntry(definition, member.entityId) === entry) actorIds.add(`${pack.id}:${member.id}`); } } const objective = definition.bosses.find((candidate) => actorIds.has(candidate.id)); if (!objective) throw new Error(`${definition.id} is missing boss objective for creature entry ${entry}`); return objective; } function sourceObject(definition: DungeonDefinition, templateId: number): DungeonWorldObjectDefinition { const object = definition.serverGameObjects.find((candidate) => candidate.source?.templateId === templateId); if (!object) throw new Error(`${definition.id} is missing source gameobject template ${templateId}`); return object; } function reviewedDoor( definition: DungeonDefinition, templateId: number, initialState: "closed" | "open", halfExtents?: readonly [number, number, number], ): DungeonWorldObjectDefinition { const source = sourceObject(definition, templateId); const provenance = source.source; if (!provenance) throw new Error(`${definition.id} source gameobject ${source.id} has no provenance`); return Object.freeze({ ...source, kind: "door", ...(halfExtents ? { halfExtents } : {}), initialState, states: Object.freeze([ { 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: Object.freeze({ color: "#544a3f", emissive: "#130f0a" }), source: Object.freeze({ ...provenance, fidelity: "source-backed-adapted", note: "Source transform and client bounds are retained. A procedural blocker provides reviewed gameplay collision until the native legacy door model is exported.", }), }); } function reviewedPanel( definition: DungeonDefinition, templateId: number, signalId: string, ): DungeonWorldObjectDefinition { const source = sourceObject(definition, templateId); const provenance = source.source; if (!provenance) throw new Error(`${definition.id} source gameobject ${source.id} has no provenance`); return Object.freeze({ ...source, kind: "switch", halfExtents: [0.85, 1.1, 0.45] as const, initialState: "locked", states: Object.freeze([ { id: "locked", label: "Locked", visible: true, blocksMovement: false, interactionEnabled: false }, { id: "unlocked", label: "Ready", visible: true, blocksMovement: false, interactionEnabled: true }, { id: "used", label: "Activated", visible: true, blocksMovement: false, interactionEnabled: false }, ]), interaction: Object.freeze({ prompt: "Activate access panel", range: 3, signalId, allowedStates: ["unlocked"], oneShot: true, }), visual: Object.freeze({ color: "#2f6d75", emissive: "#173f46" }), source: Object.freeze({ ...provenance, fidelity: "source-backed-adapted", note: "Source placement identifies the console. Compact procedural geometry supplies a readable interaction target until its native legacy model is exported.", }), }); } function reviewedInteractable( definition: DungeonDefinition, templateId: number, options: { readonly signalId: string; readonly prompt: string; readonly kind?: DungeonWorldObjectDefinition["kind"]; readonly initialState?: "ready" | "locked"; readonly range?: number; }, ): DungeonWorldObjectDefinition { const source = sourceObject(definition, templateId); const provenance = source.source; if (!provenance) throw new Error(`${definition.id} source gameobject ${source.id} has no provenance`); const initialState = options.initialState ?? "ready"; return Object.freeze({ ...source, kind: options.kind ?? "switch", initialState, states: Object.freeze([ { id: "locked", label: "Locked", visible: true, blocksMovement: false, interactionEnabled: false }, { id: "ready", label: "Ready", visible: true, blocksMovement: false, interactionEnabled: true }, { id: "used", label: "Activated", visible: true, blocksMovement: false, interactionEnabled: false }, ]), interaction: Object.freeze({ prompt: options.prompt, range: options.range ?? 3.2, signalId: options.signalId, allowedStates: ["ready"], oneShot: true, }), visual: Object.freeze({ color: "#786344", emissive: "#2b2114" }), source: Object.freeze({ ...provenance, fidelity: "source-backed-adapted", note: "The source transform and client display bounds are retained. Procedural geometry supplies a readable interaction target until the native legacy model is exported.", }), }); } function replaceObjects( definition: DungeonDefinition, replacements: readonly DungeonWorldObjectDefinition[], ): readonly DungeonWorldObjectDefinition[] { const byId = new Map(replacements.map((object) => [object.id, object] as const)); return Object.freeze(definition.serverGameObjects.map((object) => byId.get(object.id) ?? object)); } function sourceReview(definition: DungeonDefinition): DungeonEncounterDefinition["source"] { const review = REVIEW_BY_DUNGEON.get(definition.id); if (!review) throw new Error(`Missing progression-object source review for ${definition.id}`); return Object.freeze({ fidelity: "source-backed-adapted", scope: "instance-orchestration", files: review.files, reviewedBehavior: review.reviewedBehavior, adaptations: review.adaptations, remainingGaps: review.remainingGaps, }); } function appendWarning(definition: DungeonDefinition, warning: string): DungeonDefinition["validation"] { return Object.freeze({ ...definition.validation, warnings: Object.freeze([...new Set([...definition.validation.warnings, warning])]), }); } function buildDeadmines(definition: DungeonDefinition): DungeonDefinition { const rhahkZor = actorIdForEntry(definition, 644); const squallshaperSource = staticSpawnForEntry(definition, 1_732); const factoryDoor = reviewedDoor(definition, 13_965, "closed"); const foundryDoor = reviewedDoor(definition, 16_399, "closed"); const ironCladDoor = reviewedDoor(definition, 16_397, "closed"); const factoryLever = reviewedInteractable(definition, 101_831, { signalId: "deadmines:factory-lever", prompt: "Use the factory lever", initialState: "locked", }); const ironCladLever = reviewedInteractable(definition, 101_833, { signalId: "deadmines:iron-clad-lever", prompt: "Use the iron-clad lever", }); const foundryLever = reviewedInteractable(definition, 101_834, { signalId: "deadmines:foundry-lever", prompt: "Use the foundry lever", }); const cannon = reviewedInteractable(definition, 16_398, { signalId: "deadmines:fire-cannon", prompt: "Load and fire the Defias Cannon", kind: "quest-object", range: 4.5, }); const cannonReinforcements: DungeonEncounterSpawnGroupDefinition = Object.freeze({ id: "deadmines:cannon-reinforcements", spawns: Object.freeze([ { id: "deadmines:cannon-squallshaper-one", entityId: squallshaperSource.entityId, position: [-709.2917, 9.0828, -100.7318] as const, yaw: -1.58, spawnMask: 1, }, { id: "deadmines:cannon-squallshaper-two", entityId: squallshaperSource.entityId, position: [-708.6717, 9.0028, -90.2018] as const, yaw: -1.74, spawnMask: 1, }, ]), }); const rules: readonly DungeonEncounterRule[] = Object.freeze([ { id: "rhahk-zor-opens-factory", trigger: { kind: "entity-death", entityId: rhahkZor }, actions: [ { id: "open-factory-door", kind: "set-world-object-state", objectId: factoryDoor.id, state: "open", persistence: "run" }, { id: "mark-factory-lever-used", kind: "set-world-object-state", objectId: factoryLever.id, state: "used", persistence: "run" }, ], }, { id: "foundry-lever-opens-door", trigger: { kind: "interact", objectId: foundryLever.id }, guards: [{ kind: "world-object-state", objectId: foundryLever.id, states: ["ready"] }], actions: [ { id: "open-foundry-door", kind: "set-world-object-state", objectId: foundryDoor.id, state: "open", persistence: "run" }, { id: "mark-foundry-lever-used", kind: "set-world-object-state", objectId: foundryLever.id, state: "used", persistence: "run" }, ], }, { id: "iron-clad-lever-opens-door", trigger: { kind: "interact", objectId: ironCladLever.id }, guards: [{ kind: "world-object-state", objectId: ironCladLever.id, states: ["ready"] }], actions: [ { id: "lever-opens-iron-clad-door", kind: "set-world-object-state", objectId: ironCladDoor.id, state: "open", persistence: "run" }, { id: "mark-iron-clad-lever-used", kind: "set-world-object-state", objectId: ironCladLever.id, state: "used", persistence: "run" }, ], }, { id: "cannon-breaches-iron-clad-door", trigger: { kind: "interact", objectId: cannon.id }, guards: [{ kind: "world-object-state", objectId: cannon.id, states: ["ready"] }], actions: [ { id: "mark-defias-cannon-used", kind: "set-world-object-state", objectId: cannon.id, state: "used", persistence: "run" }, { id: "cannon-opens-iron-clad-door", kind: "set-world-object-state", objectId: ironCladDoor.id, state: "open", persistence: "run" }, { id: "spawn-cannon-reinforcements", kind: "activate-spawn-group", spawnGroupId: cannonReinforcements.id }, ], }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "deadmines:progression-objects", dungeonId: definition.id, initialPhase: "active", initialState: { worldObjectStates: { [factoryDoor.id]: "closed", [foundryDoor.id]: "closed", [ironCladDoor.id]: "closed", [factoryLever.id]: "locked", [foundryLever.id]: "ready", [ironCladLever.id]: "ready", [cannon.id]: "ready", }, }, resetPolicy: { bossWipePhase: "active", checkpointPhase: "active" }, source: sourceReview(definition), rules, }); return Object.freeze({ ...definition, encounters: Object.freeze([...(definition.encounters ?? []), encounter]), encounterSpawnGroups: Object.freeze([...(definition.encounterSpawnGroups ?? []), cannonReinforcements]), serverGameObjects: replaceObjects(definition, [ factoryDoor, foundryDoor, ironCladDoor, factoryLever, foundryLever, ironCladLever, cannon, ]), validation: appendWarning(definition, "Deadmines now runs the source Rhahk'Zor, foundry-lever, and cannon/iron-clad transitions; generic heavy and mast doors remain explicitly nonblocking."), }); } function buildShadowfangKeep(definition: DungeonDefinition): DungeonDefinition { const rethilgore = actorIdForEntry(definition, 3_914); const fenrus = actorIdForEntry(definition, 4_274); const nandos = actorIdForEntry(definition, 3_927); const courtyardDoor = reviewedDoor(definition, 18_895, "closed"); const sorcererDoor = reviewedDoor(definition, 18_972, "closed"); const arugalDoor = reviewedDoor(definition, 18_971, "closed"); const rules: readonly DungeonEncounterRule[] = Object.freeze([ { id: "rethilgore-section-opens-courtyard", trigger: { kind: "entity-death", entityId: rethilgore }, actions: [{ id: "open-courtyard-door", kind: "set-world-object-state", objectId: courtyardDoor.id, state: "open", persistence: "run" }], }, { id: "fenrus-opens-sorcerer-door", trigger: { kind: "entity-death", entityId: fenrus }, actions: [{ id: "open-sorcerer-door", kind: "set-world-object-state", objectId: sorcererDoor.id, state: "open", persistence: "run" }], }, { id: "nandos-opens-arugal-door", trigger: { kind: "entity-death", entityId: nandos }, actions: [{ id: "open-arugal-door", kind: "set-world-object-state", objectId: arugalDoor.id, state: "open", persistence: "run" }], }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "shadowfang-keep:progression-objects", dungeonId: definition.id, initialPhase: "active", initialState: { worldObjectStates: { [courtyardDoor.id]: "closed", [sorcererDoor.id]: "closed", [arugalDoor.id]: "closed", }, }, resetPolicy: { bossWipePhase: "active", checkpointPhase: "active" }, source: sourceReview(definition), rules, }); return Object.freeze({ ...definition, encounters: Object.freeze([...(definition.encounters ?? []), encounter]), serverGameObjects: replaceObjects(definition, [courtyardDoor, sorcererDoor, arugalDoor]), validation: appendWarning(definition, "Shadowfang Keep now binds all three source progression doors; the faction-specific prisoner escort is an explicit immediate post-Rethilgore adaptation."), }); } function buildMaraudon(definition: DungeonDefinition): DungeonDefinition { const noxxion = actorIdForEntry(definition, 13_282); const corruptionSpewer = reviewedDoor(definition, 178_570, "closed", [1.2, 7.7, 13.9]); const rules: readonly DungeonEncounterRule[] = Object.freeze([ { id: "noxxion-opens-corruption-spewer", trigger: { kind: "entity-death", entityId: noxxion }, actions: [{ id: "open-corruption-spewer", kind: "set-world-object-state", objectId: corruptionSpewer.id, state: "open", persistence: "run", }], }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "maraudon:progression-objects", dungeonId: definition.id, initialPhase: "active", initialState: { worldObjectStates: { [corruptionSpewer.id]: "closed" } }, resetPolicy: { bossWipePhase: "active", checkpointPhase: "active" }, source: sourceReview(definition), rules, }); return Object.freeze({ ...definition, encounters: Object.freeze([...(definition.encounters ?? []), encounter]), serverGameObjects: replaceObjects(definition, [corruptionSpewer]), validation: appendWarning(definition, "Maraudon's source Noxxion-to-Corruption-Spewer route gate is executable; native spewer presentation remains an explicit asset adaptation."), }); } function buildMechanar(definition: DungeonDefinition): DungeonDefinition { const ironHand = actorIdForEntry(definition, 19_710); const gyroKill = actorIdForEntry(definition, 19_218); const sepethrea = actorIdForEntry(definition, 19_221); const ironDoor = reviewedDoor(definition, 184_632, "closed", [4.65, 7.98, 1.1]); const gyroDoor = reviewedDoor(definition, 184_322, "closed", [4.65, 7.98, 1.1]); const sepethreaDoor = reviewedDoor(definition, 184_449, "open", [4.65, 7.98, 1.1]); const rules: readonly DungeonEncounterRule[] = Object.freeze([ { id: "iron-hand-opens-moarg-one", trigger: { kind: "entity-death", entityId: ironHand }, actions: [{ id: "open-moarg-one", kind: "set-world-object-state", objectId: ironDoor.id, state: "open", persistence: "run" }], }, { id: "gyro-kill-opens-moarg-two", trigger: { kind: "entity-death", entityId: gyroKill }, actions: [{ id: "open-moarg-two", kind: "set-world-object-state", objectId: gyroDoor.id, state: "open", persistence: "run" }], }, { id: "sepethrea-closes-room", trigger: { kind: "entity-engaged", entityId: sepethrea }, actions: [{ id: "close-sepethrea-room", kind: "set-world-object-state", objectId: sepethreaDoor.id, state: "closed" }], }, { id: "sepethrea-opens-room", trigger: { kind: "entity-death", entityId: sepethrea }, actions: [{ id: "open-sepethrea-room", kind: "set-world-object-state", objectId: sepethreaDoor.id, state: "open", persistence: "run" }], }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "mechanar:progression-objects", dungeonId: definition.id, initialPhase: "active", initialState: { worldObjectStates: { [ironDoor.id]: "closed", [gyroDoor.id]: "closed", [sepethreaDoor.id]: "open", }, }, resetPolicy: { bossWipePhase: "active", checkpointPhase: "active" }, source: sourceReview(definition), rules, }); return Object.freeze({ ...definition, encounters: Object.freeze([...(definition.encounters ?? []), encounter]), serverGameObjects: replaceObjects(definition, [ironDoor, gyroDoor, sepethreaDoor]), validation: appendWarning(definition, "Mechanar source doors are executable; bridge scripting, elevator operation, and native door assets remain documented adaptations."), }); } function buildSteamvault(definition: DungeonDefinition): DungeonDefinition { const thespia = actorIdForEntry(definition, 17_797); const steamrigger = actorIdForEntry(definition, 17_796); const hydroPanel = reviewedPanel(definition, 184_125, "steamvault:hydromancer-panel"); const mekPanel = reviewedPanel(definition, 184_126, "steamvault:mekgineer-panel"); const mainDoor = reviewedDoor(definition, 183_049, "closed", [7.2, 5.95, 0.9]); const rules: readonly DungeonEncounterRule[] = Object.freeze([ { id: "thespia-unlocks-panel", trigger: { kind: "entity-death", entityId: thespia }, actions: [{ id: "unlock-hydromancer-panel", kind: "set-world-object-state", objectId: hydroPanel.id, state: "unlocked", persistence: "run" }], }, { id: "steamrigger-unlocks-panel", trigger: { kind: "entity-death", entityId: steamrigger }, actions: [{ id: "unlock-mekgineer-panel", kind: "set-world-object-state", objectId: mekPanel.id, state: "unlocked", persistence: "run" }], }, { id: "use-hydromancer-panel", trigger: { kind: "interact", objectId: hydroPanel.id }, guards: [{ kind: "world-object-state", objectId: hydroPanel.id, states: ["unlocked"] }], actions: [ { id: "mark-hydromancer-panel-used", kind: "set-flag", flagId: "hydromancer-panel-used", value: true, persistence: "run" }, { id: "set-hydromancer-panel-used", kind: "set-world-object-state", objectId: hydroPanel.id, state: "used", persistence: "run" }, ], }, { id: "use-mekgineer-panel", trigger: { kind: "interact", objectId: mekPanel.id }, guards: [{ kind: "world-object-state", objectId: mekPanel.id, states: ["unlocked"] }], actions: [ { id: "mark-mekgineer-panel-used", kind: "set-flag", flagId: "mekgineer-panel-used", value: true, persistence: "run" }, { id: "set-mekgineer-panel-used", kind: "set-world-object-state", objectId: mekPanel.id, state: "used", persistence: "run" }, ], }, { id: "both-panels-open-main-door", trigger: { kind: "tick" }, guards: [ { kind: "flag", flagId: "hydromancer-panel-used", equals: true }, { kind: "flag", flagId: "mekgineer-panel-used", equals: true }, ], actions: [{ id: "open-main-chambers-door", kind: "set-world-object-state", objectId: mainDoor.id, state: "open", persistence: "run" }], }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "steamvault:access-panels", dungeonId: definition.id, initialPhase: "active", initialState: { flags: { "hydromancer-panel-used": false, "mekgineer-panel-used": false }, worldObjectStates: { [hydroPanel.id]: "locked", [mekPanel.id]: "locked", [mainDoor.id]: "closed", }, }, resetPolicy: { bossWipePhase: "active", checkpointPhase: "active" }, source: sourceReview(definition), rules, }); return Object.freeze({ ...definition, encounters: Object.freeze([...(definition.encounters ?? []), encounter]), serverGameObjects: replaceObjects(definition, [hydroPanel, mekPanel, mainDoor]), validation: appendWarning(definition, "Steamvault's two boss-gated access panels and Kalithresh door are executable with documented procedural presentation."), }); } function buildShadowLabyrinth(definition: DungeonDefinition): DungeonDefinition { const hellmawObjective = objectiveForEntry(definition, 18_731); const hellmawSpawn = staticSpawnForEntry(definition, 18_731); const ritualists = definition.staticSpawns .filter((spawn) => entityEntry(definition, spawn.entityId) === 18_794 && (spawn.spawnMask ?? 3) === 3) .map((spawn) => spawn.id); if (ritualists.length !== 18) { throw new Error(`shadow-labyrinth expected 18 common Cabal Ritualists, found ${ritualists.length}`); } const blackheart = actorIdForEntry(definition, 18_667); const vorpil = actorIdForEntry(definition, 18_732); const refectoryDoor = reviewedDoor(definition, 183_296, "closed", [3.5, 6, 0.8]); const screamingHallDoor = reviewedDoor(definition, 183_295, "closed", [3.5, 6, 0.8]); const group: DungeonEncounterSpawnGroupDefinition = Object.freeze({ id: "shadow-labyrinth:hellmaw", spawns: Object.freeze([hellmawSpawn]), }); const rules: readonly DungeonEncounterRule[] = Object.freeze([ { id: "release-hellmaw", trigger: { kind: "entities-dead", entityIds: ritualists }, actions: [ { id: "spawn-released-hellmaw", kind: "activate-spawn-group", spawnGroupId: group.id }, { id: "enter-hellmaw-active", kind: "set-phase", phase: "hellmaw-active" }, ], }, { id: "blackheart-opens-refectory", trigger: { kind: "entity-death", entityId: blackheart }, actions: [{ id: "open-refectory-door", kind: "set-world-object-state", objectId: refectoryDoor.id, state: "open", persistence: "run" }], }, { id: "vorpil-opens-screaming-hall", trigger: { kind: "entity-death", entityId: vorpil }, actions: [{ id: "open-screaming-hall-door", kind: "set-world-object-state", objectId: screamingHallDoor.id, state: "open", persistence: "run" }], }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "shadow-labyrinth:progression-objects", dungeonId: definition.id, initialPhase: "ritualists-active", initialState: { worldObjectStates: { [refectoryDoor.id]: "closed", [screamingHallDoor.id]: "closed", }, }, resetPolicy: { bossWipePhase: "ritualists-active", checkpointPhase: "ritualists-active" }, source: sourceReview(definition), rules, }); const eventObjective = Object.freeze({ ...hellmawObjective, eventOnly: true }); return Object.freeze({ ...definition, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => spawn.id !== hellmawSpawn.id)), bosses: Object.freeze(definition.bosses.map((boss) => boss.id === eventObjective.id ? eventObjective : boss)), objectiveOrder: Object.freeze(definition.objectiveOrder.map((boss) => boss.id === eventObjective.id ? eventObjective : boss)), encounters: Object.freeze([...(definition.encounters ?? []), encounter]), encounterSpawnGroups: Object.freeze([...(definition.encounterSpawnGroups ?? []), group]), serverGameObjects: replaceObjects(definition, [refectoryDoor, screamingHallDoor]), validation: appendWarning(definition, "Shadow Labyrinth now gates Hellmaw on the common ritualists and binds both passage doors; heroic ritualist gating and presentation remain documented adaptations."), }); } function buildScarletMonastery(definition: DungeonDefinition): DungeonDefinition { const mograine = actorIdForEntry(definition, 3_976); const whitemaneObjective = objectiveForEntry(definition, 3_977); const whitemaneSpawn = staticSpawnForEntry(definition, 3_977); const chapelDoor = reviewedDoor(definition, 104_591, "open", [4.3, 5.5, 0.8]); const inquisitorDoor = reviewedDoor(definition, 104_600, "closed", [3.6, 4.6, 0.8]); const group: DungeonEncounterSpawnGroupDefinition = Object.freeze({ id: "scarlet-monastery:whitemane", spawns: Object.freeze([whitemaneSpawn]), }); const rules: readonly DungeonEncounterRule[] = Object.freeze([ { id: "mograine-releases-whitemane", trigger: { kind: "entity-death", entityId: mograine }, actions: [ { id: "remember-mograine-defeat", kind: "set-flag", flagId: "mograine-defeated", value: true, persistence: "run" }, { id: "open-inquisitor-door", kind: "set-world-object-state", objectId: inquisitorDoor.id, state: "open", persistence: "run" }, { id: "spawn-whitemane", kind: "activate-spawn-group", spawnGroupId: group.id }, { id: "enter-whitemane-active", kind: "set-phase", phase: "whitemane-active" }, ], }, { id: "resume-whitemane-after-wipe", trigger: { kind: "tick" }, guards: [ { kind: "phase", phases: ["resume"] }, { kind: "flag", flagId: "mograine-defeated", equals: true }, { kind: "objective-complete", objectiveId: whitemaneObjective.id, equals: false }, ], actions: [ { id: "respawn-whitemane", kind: "activate-spawn-group", spawnGroupId: group.id }, { id: "resume-whitemane-active", kind: "set-phase", phase: "whitemane-active" }, ], }, ]); const encounter: DungeonEncounterDefinition = Object.freeze({ id: "scarlet-monastery:cathedral-pair", dungeonId: definition.id, initialPhase: "mograine-active", initialState: { flags: { "mograine-defeated": false }, worldObjectStates: { [chapelDoor.id]: "open", [inquisitorDoor.id]: "closed" }, }, resetPolicy: { bossWipePhase: "resume", checkpointPhase: "resume" }, source: sourceReview(definition), rules, }); const eventObjective = Object.freeze({ ...whitemaneObjective, eventOnly: true }); return Object.freeze({ ...definition, staticSpawns: Object.freeze(definition.staticSpawns.filter((spawn) => spawn.id !== whitemaneSpawn.id)), bosses: Object.freeze(definition.bosses.map((boss) => boss.id === eventObjective.id ? eventObjective : boss)), objectiveOrder: Object.freeze(definition.objectiveOrder.map((boss) => boss.id === eventObjective.id ? eventObjective : boss)), encounters: Object.freeze([...(definition.encounters ?? []), encounter]), encounterSpawnGroups: Object.freeze([...(definition.encounterSpawnGroups ?? []), group]), serverGameObjects: replaceObjects(definition, [chapelDoor, inquisitorDoor]), validation: appendWarning(definition, "Scarlet Monastery now gates Whitemane behind Mograine and the source inner door; resurrection remains a documented two-stage adaptation."), }); } export function withWowProgressionObjectContent(definition: DungeonDefinition): DungeonDefinition { if (definition.encounters?.some((encounter) => encounter.id === `${definition.id}:progression-objects`) || definition.encounters?.some((encounter) => encounter.id === `${definition.id}:access-panels`) || definition.encounters?.some((encounter) => encounter.id === `${definition.id}:cathedral-pair`)) { return definition; } if (definition.id === "deadmines") return buildDeadmines(definition); if (definition.id === "shadowfang-keep") return buildShadowfangKeep(definition); if (definition.id === "maraudon") return buildMaraudon(definition); if (definition.id === "mechanar") return buildMechanar(definition); if (definition.id === "steamvault") return buildSteamvault(definition); if (definition.id === "shadow-labyrinth") return buildShadowLabyrinth(definition); if (definition.id === "scarlet-monastery") return buildScarletMonastery(definition); return definition; }