forked from phenom/healer-man
Release Healer Man 0.1.22
This commit is contained in:
1 parent
f6f2375145
commit
7f6001723a
144 files changed
+54107
-690
No files matched your search
@@ -0,0 +1,784 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user