forked from phenom/healer-man
785 lines
38 KiB
TypeScript
785 lines
38 KiB
TypeScript
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<string>();
|
|
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;
|
|
}
|