Files
healer-man/src/game/ascensionCustomEncounterCatalog.ts
T
2026-09-09 11:21:33 -04:00

1430 lines
50 KiB
TypeScript

import type {
DungeonDefinition,
DungeonEncounterSpawnGroupDefinition,
Vector3Tuple,
} from "./dungeonTypes";
import type { DungeonEncounterDefinition } from "./dungeonEncounterTypes";
import type { DungeonWorldObjectDefinition } from "./dungeonWorldObjects";
import type { EnemyAttackDefinition, EnemyCombatDefinition } from "./enemyAttacks";
import type {
PopulationDefinitionMap,
PopulationEntityDefinition,
StaticMobSpawnDefinition,
} from "./mobPopulation";
const RECOVERY_MANIFEST = "dungeon-pipeline/ascension/ASCENSION_CUSTOM_DUNGEON_RECOVERY.json";
const RECOVERY_NOTES = "dungeon-pipeline/ascension/ASCENSION_CUSTOM_DUNGEON_RECOVERY.md";
const MANASTORM_CATALOG = "src/game/generated/manastormRuntimeCatalog.json";
const ASCENSION_SPELL_DBC = "../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc (sha256 2f77f18b91ffbae32a19568e4bc05ec78ee384b6ba8dd2a6e05f0e97abbb2dce)";
const RECOVERY_FILES = Object.freeze([
RECOVERY_MANIFEST,
RECOVERY_NOTES,
MANASTORM_CATALOG,
ASCENSION_SPELL_DBC,
] as const);
export type AscensionCustomBindingConfidence =
| "exact"
| "spell-linked"
| "candidate-unbound"
| "authored";
export interface AscensionCustomEncounterEvidence {
readonly dungeonId: string;
readonly fidelity: "source-backed-adapted" | "healer-man-authored";
readonly exactCreatureIds: readonly number[];
readonly spellLinkedCreatureIds: readonly number[];
readonly spellLinkedGameObjectIds: readonly number[];
readonly exactSpellFamilies: Readonly<Record<string, readonly number[]>>;
readonly candidateUnboundSpellFamilies: Readonly<Record<string, readonly number[]>>;
readonly authoredDecisions: readonly string[];
readonly remainingGaps: readonly string[];
}
/**
* Durable evidence boundary for the three Ascension-only packages. A spell id
* appearing in candidateUnboundSpellFamilies is never presented as an exact
* creature binding by the runtime adapter.
*/
export const ASCENSION_CUSTOM_ENCOUNTER_EVIDENCE = Object.freeze([
Object.freeze({
dungeonId: "frozen-reach",
fidelity: "source-backed-adapted",
exactCreatureIds: Object.freeze([97_700, 97_701]),
spellLinkedCreatureIds: Object.freeze([97_703, 97_731, 97_770]),
spellLinkedGameObjectIds: Object.freeze([]),
exactSpellFamilies: Object.freeze({
alvaRanged: Object.freeze([255_347, 255_349, 255_352, 255_358, 255_401, 255_420, 255_421]),
arktos: Object.freeze([255_360, 255_374, 255_379, 255_380, 255_399, 255_417, 255_434]),
alvaVengeance: Object.freeze([255_389, 255_390, 255_393, 255_394, 255_397, 255_410, 255_419, 255_432]),
}),
candidateUnboundSpellFamilies: Object.freeze({}),
authoredDecisions: Object.freeze([
"Normal mode uses recovered D0 spell rows; the surviving mode ids do not establish public difficulty labels.",
"Cast cadence, target selection, Arktos placement, Alvajar formation, and the exact death transition are HealerMan adaptations.",
"Arktos is an attempt-scoped encounter actor so a boss wipe restores the duo and can replay his death transition.",
"The unsupported fifteen-creature procedural hallway population is removed.",
]),
remainingGaps: Object.freeze([
"The source server script, creature templates, display ids, and authoritative placements were not recovered.",
"Blast Arrow cannot currently retarget a friendly encounter creature, so the Set Ablaze interaction with Arktos remains documented but unexecutable.",
"Trap and area-trigger summons use bounded combat proxies until dedicated hazard actors are available.",
]),
}),
Object.freeze({
dungeonId: "forgotten-mine",
fidelity: "source-backed-adapted",
exactCreatureIds: Object.freeze([254_272]),
spellLinkedCreatureIds: Object.freeze([254_278]),
spellLinkedGameObjectIds: Object.freeze([254_051]),
exactSpellFamilies: Object.freeze({
stonebreaker: Object.freeze([354_000, 354_021, 354_022, 354_023, 354_024]),
quake: Object.freeze([354_001, 354_009, 354_010]),
stoneShield: Object.freeze([354_002, 354_011, 354_012]),
geospike: Object.freeze([354_004, 354_005, 354_006, 354_025, 354_026]),
resonance: Object.freeze([354_013, 354_014]),
geodeBarrage: Object.freeze([354_015, 354_016, 354_027, 354_028, 354_029, 354_030]),
rupturedEarth: Object.freeze([354_017, 354_018]),
stoneskin: Object.freeze([354_019, 354_020]),
resonite: Object.freeze([354_031, 354_032]),
}),
candidateUnboundSpellFamilies: Object.freeze({}),
authoredDecisions: Object.freeze([
"The fifteen existing route spawns remain as five conceptual three-creature packs with authored identities and cadence.",
"The 50 percent phase threshold, fixed Geospike proxy location, Resonite offsets, generous interaction bounds, and thirty-second recycle are HealerMan adaptations.",
"A consumed Resonite shard suppresses Resonance for eight seconds because the exact server-side cleanse rule did not survive.",
]),
remainingGaps: Object.freeze([
"Authoritative trash entries, creature stats, display ids, paths, and spawn coordinates were not recovered.",
"Geode Barrage is represented as one telegraphed cast rather than its unknown source projectile sequence.",
"Resonite Cleanse is source-constrained but its exact protection removal and prevention semantics remain unavailable.",
]),
}),
Object.freeze({
dungeonId: "tinkertech-showdown",
fidelity: "healer-man-authored",
exactCreatureIds: Object.freeze([663_012]),
spellLinkedCreatureIds: Object.freeze([]),
spellLinkedGameObjectIds: Object.freeze([]),
exactSpellFamilies: Object.freeze({}),
candidateUnboundSpellFamilies: Object.freeze({
rocket: Object.freeze([257_438]),
gatlingMissileBarrage: Object.freeze([257_459, 257_460, 257_461]),
incinerationBeam: Object.freeze([257_470]),
electrifiedZone: Object.freeze([257_472, 257_473]),
}),
authoredDecisions: Object.freeze([
"The machine spell cluster is a low-confidence client-side candidate and is deliberately classified as candidate-unbound, not exact Mechatron data.",
"All trash identities, positions, attacks, boss cadence, telegraphs, and the 35 percent reinforcement phase are HealerMan-authored.",
"The existing fifteen-spawn route is retained as five conceptual three-creature packs.",
]),
remainingGaps: Object.freeze([
"No server row links spells 257438 or 257459-257473 to Mechatron 663012.",
"No authoritative trash, gameobject, platform-destruction, display, loot, or placement data was recovered.",
"The provisional map anchors still require a live traversal and collision playtest.",
]),
}),
] as const satisfies readonly AscensionCustomEncounterEvidence[]);
const EVIDENCE_BY_DUNGEON: ReadonlyMap<string, AscensionCustomEncounterEvidence> = new Map(
ASCENSION_CUSTOM_ENCOUNTER_EVIDENCE.map((entry) => [entry.dungeonId, entry] as const),
);
function frozen<T>(value: T): Readonly<T> {
return Object.freeze(value);
}
function attack(value: EnemyAttackDefinition): EnemyAttackDefinition {
return frozen(value);
}
function encounterCondition(encounterId: string, states: readonly string[]) {
return frozen({ kind: "encounter-state" as const, encounterId, states: frozen([...states]) });
}
function combatWith(
source: PopulationEntityDefinition,
attacks: readonly EnemyAttackDefinition[],
mechanicIds: readonly string[],
overrides: Partial<EnemyCombatDefinition> = {},
): EnemyCombatDefinition {
return frozen({
...source.combat,
...overrides,
mechanicIds: frozen([...mechanicIds]),
attacks: frozen([...attacks]),
});
}
function appendWarnings(
definition: DungeonDefinition,
additions: readonly string[],
removeContaining: readonly string[] = [],
): DungeonDefinition["validation"] {
const warnings = definition.validation.warnings.filter(
(warning) => !removeContaining.some((part) => warning.includes(part)),
);
for (const addition of additions) if (!warnings.includes(addition)) warnings.push(addition);
return frozen({ ...definition.validation, warnings: frozen(warnings) });
}
function sourceReview(
evidence: AscensionCustomEncounterEvidence,
reviewedBehavior: string,
): NonNullable<DungeonEncounterDefinition["source"]> {
return frozen({
fidelity: evidence.fidelity,
scope: "instance-orchestration",
files: RECOVERY_FILES,
reviewedBehavior,
adaptations: evidence.authoredDecisions,
remainingGaps: evidence.remainingGaps,
});
}
function replaceBaselineReview(
definition: DungeonDefinition,
evidence: AscensionCustomEncounterEvidence,
): readonly DungeonEncounterDefinition[] {
return frozen((definition.encounters ?? []).map((encounter) => {
if (encounter.id !== `${definition.id}:boss-progression`) return encounter;
return frozen({
...encounter,
source: frozen({
fidelity: evidence.fidelity,
scope: "boss-progression" as const,
files: RECOVERY_FILES,
reviewedBehavior: evidence.fidelity === "healer-man-authored"
? "Mechatron's exact encounter identity gates a fully authored completion objective and checkpoint."
: "Recovered Ascension encounter identities gate persistent completion and a post-boss checkpoint.",
adaptations: frozen([
"HealerMan owns completion, checkpoint, loot, and reset semantics because the Ascension instance script was not recovered.",
]),
remainingGaps: frozen([
...evidence.remainingGaps,
"An end-to-end traversal, wipe, checkpoint reload, HUD, collision, and completion playtest is still required before validation.",
]),
}),
});
}));
}
function withoutKeys(
entities: PopulationDefinitionMap,
removedIds: ReadonlySet<string>,
): Record<string, PopulationEntityDefinition> {
return Object.fromEntries(Object.entries(entities).filter(([id]) => !removedIds.has(id)));
}
function offset(position: Vector3Tuple, x: number, z: number, y = 0): Vector3Tuple {
return frozen([position[0] + x, position[1] + y, position[2] + z]);
}
function requireEntity(definition: DungeonDefinition, id: string): PopulationEntityDefinition {
const entity = definition.entities[id];
if (!entity) throw new Error(`${definition.id} is missing required entity ${id}`);
return entity;
}
function requireSpawn(definition: DungeonDefinition, id: string): StaticMobSpawnDefinition {
const spawn = definition.staticSpawns.find((entry) => entry.id === id);
if (!spawn) throw new Error(`${definition.id} is missing required spawn ${id}`);
return spawn;
}
function buildFrozenReach(definition: DungeonDefinition): DungeonDefinition {
const markerId = "frozen-reach:alva-and-arktos";
if (definition.encounters?.some((encounter) => encounter.id === markerId)) return definition;
const evidence = EVIDENCE_BY_DUNGEON.get(definition.id)!;
const alvaSource = requireEntity(definition, "entry-97700");
const alvaSpawn = requireSpawn(definition, "encounter-2168");
const condition = (states: readonly string[]) => encounterCondition(markerId, states);
const alvaAttacks = frozen([
attack({
id: "auto-shot-255347",
name: "Auto Shot",
spellId: 255_347,
delivery: "projectile",
target: "primary",
school: "physical",
animation: "attack",
range: 35,
cooldownMs: 2_200,
damageMultiplier: 0.8,
}),
attack({
id: "rain-of-arrows-255349",
name: "Rain of Arrows",
spellId: 255_349,
delivery: "area",
target: "random-party",
school: "physical",
animation: "cast",
castTimeMs: 2_000,
range: 35,
radius: 7,
cooldownMs: 12_000,
initialCooldownMs: [5_000, 7_000],
damageMultiplier: 0.85,
telegraph: { shape: "circle", anchor: "target", warningMs: 2_000, radius: 7 },
condition: condition(["duo-active"]),
}),
attack({
id: "unbridled-arrows-255352",
name: "Unbridled Arrows",
spellId: 255_352,
delivery: "area",
target: "nearby-party",
school: "physical",
animation: "cast",
castTimeMs: 2_100,
range: 28,
cooldownMs: 20_000,
initialCooldownMs: [9_000, 11_000],
damageMultiplier: 0.72,
telegraph: { shape: "cone", anchor: "source", warningMs: 2_100, length: 28, angleDegrees: 75 },
condition: condition(["duo-active"]),
}),
attack({
id: "blast-arrow-255358",
name: "Blast Arrow",
spellId: 255_358,
delivery: "area",
target: "random-party",
school: "fire",
animation: "cast",
castTimeMs: 1_750,
range: 38,
radius: 5.5,
cooldownMs: 15_000,
initialCooldownMs: [7_000, 9_000],
maximumCasts: 3,
damageMultiplier: 1.05,
telegraph: { shape: "circle", anchor: "target", warningMs: 1_750, radius: 5.5 },
condition: condition(["duo-active"]),
}),
attack({
id: "blast-arrow-trigger-255363",
name: "Blast Arrow Ground Trigger",
spellId: 255_363,
delivery: "area",
target: "self",
school: "fire",
animation: "cast",
range: 0,
cooldownMs: 15_000,
initialCooldownMs: [7_000, 9_000],
damageMultiplier: 0,
effect: {
kind: "summon",
creatureEntry: 97_703,
count: 1,
entityId: "frozen-reach:blast-arrow-trigger",
spawnGroupId: "frozen-reach:blast-arrow-summons",
},
condition: condition(["duo-active"]),
}),
attack({
id: "cluster-shot-255401",
name: "Cluster Shot",
spellId: 255_401,
delivery: "area",
target: "random-party",
school: "physical",
animation: "cast",
castTimeMs: 1_800,
range: 38,
radius: 6.5,
cooldownMs: 18_000,
damageMultiplier: 0.95,
telegraph: { shape: "circle", anchor: "target", warningMs: 1_800, radius: 6.5 },
condition: condition(["duo-active"]),
}),
attack({
id: "explosive-trap-255421",
name: "Throw Explosive Trap",
spellId: 255_421,
delivery: "area",
target: "self",
school: "fire",
animation: "cast",
castTimeMs: 1_500,
range: 0,
cooldownMs: 16_000,
initialCooldownMs: [10_000, 12_000],
maximumCasts: 3,
damageMultiplier: 0,
effect: {
kind: "summon",
creatureEntry: 97_731,
count: 1,
entityId: "frozen-reach:explosive-trap",
spawnGroupId: "frozen-reach:trap-summons",
},
condition: condition(["duo-active"]),
}),
attack({
id: "vengeful-bolt-255394",
name: "Vengeful Bolt",
spellId: 255_394,
delivery: "projectile",
target: "random-party",
school: "shadow",
animation: "cast",
castTimeMs: 1_400,
range: 40,
cooldownMs: 7_000,
damageMultiplier: 0.95,
condition: condition(["vengeance"]),
}),
attack({
id: "shadow-veil-lance-255397",
name: "Shadow Veil Lance",
spellId: 255_397,
delivery: "area",
target: "nearby-party",
school: "shadow",
animation: "cast",
castTimeMs: 2_000,
range: 34,
cooldownMs: 12_000,
damageMultiplier: 1.1,
telegraph: { shape: "line", anchor: "source", warningMs: 2_000, length: 34, width: 4 },
condition: condition(["vengeance"]),
}),
attack({
id: "sphere-grief-wrath-255393",
name: "Sphere of Grief and Wrath",
spellId: 255_393,
delivery: "area",
target: "random-party",
school: "shadow",
animation: "cast",
castTimeMs: 2_100,
range: 38,
radius: 8,
cooldownMs: 20_000,
damageMultiplier: 1.15,
telegraph: { shape: "circle", anchor: "target", warningMs: 2_100, radius: 8 },
condition: condition(["vengeance"]),
}),
attack({
id: "alvas-grief-255410",
name: "Alva's Grief",
spellId: 255_410,
delivery: "area",
target: "nearby-party",
school: "shadow",
animation: "cast",
range: 10,
radius: 10,
cooldownMs: 10_000,
damageMultiplier: 0.62,
condition: condition(["vengeance"]),
}),
attack({
id: "alvas-wrath-255419",
name: "Alva's Wrath",
spellId: 255_419,
delivery: "area",
target: "self",
school: "shadow",
animation: "cast",
range: 0,
cooldownMs: 25_000,
damageMultiplier: 0,
effect: { kind: "aura", aura: "alvas-wrath", durationMs: 0, magnitude: 0.15 },
condition: condition(["vengeance"]),
}),
]);
const alva = frozen({
...alvaSource,
name: "Alva",
title: "Source-backed adapted encounter",
combat: combatWith(alvaSource, alvaAttacks, alvaAttacks.map((entry) => entry.id), {
serverEntry: 97_700,
level: 60,
healthMultiplier: 1.35,
damageMultiplier: 1.08,
}),
} as PopulationEntityDefinition);
const arktosAttacks = frozen([
attack({
id: "carnivorous-bite-255379",
name: "Carnivorous Bite",
spellId: 255_379,
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
range: 5,
cooldownMs: 2_200,
damageMultiplier: 1,
}),
attack({
id: "pact-of-the-hunt-255360",
name: "Pact of the Hunt",
spellId: 255_360,
delivery: "area",
target: "self",
school: "frost",
animation: "cast",
range: 0,
cooldownMs: 60_000,
initialCooldownMs: [0, 0],
maximumCasts: 1,
damageMultiplier: 0,
effect: { kind: "aura", aura: "pact-of-the-hunt", durationMs: 0, magnitude: 0.1 },
condition: condition(["duo-active"]),
}),
attack({
id: "ice-rock-255374",
name: "Ice Rock",
spellId: 255_374,
delivery: "projectile",
target: "random-party",
school: "frost",
animation: "cast",
castTimeMs: 1_600,
range: 32,
cooldownMs: 12_000,
damageMultiplier: 0.9,
condition: condition(["duo-active"]),
}),
attack({
id: "frost-roar-255399",
name: "Frost Roar",
spellId: 255_399,
delivery: "area",
target: "nearby-party",
school: "frost",
animation: "cast",
castTimeMs: 1_800,
range: 15,
radius: 15,
cooldownMs: 18_000,
damageMultiplier: 0.75,
telegraph: { shape: "circle", anchor: "source", warningMs: 1_800, radius: 15 },
condition: condition(["duo-active"]),
}),
attack({
id: "lacerate-255434",
name: "Lacerate",
spellId: 255_434,
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
range: 5,
cooldownMs: 8_000,
damageMultiplier: 0,
effect: { kind: "target-aura", aura: "lacerate", durationMs: 12_000 },
condition: condition(["duo-active"]),
}),
attack({
id: "frost-shield-255380",
name: "Frost Shield",
spellId: 255_380,
delivery: "area",
target: "self",
school: "frost",
animation: "cast",
range: 0,
cooldownMs: 22_000,
damageMultiplier: 0,
effect: { kind: "aura", aura: "frost-shield", durationMs: 8_000, magnitude: 0.2 },
condition: condition(["duo-active"]),
}),
]);
const arktos: PopulationEntityDefinition = frozen({
...alvaSource,
id: "frozen-reach:arktos",
kind: "mob",
name: "Arktos",
hasLoot: false,
combat: combatWith(alvaSource, arktosAttacks, arktosAttacks.map((entry) => entry.id), {
serverEntry: 97_701,
level: 60,
healthMultiplier: 0.95,
damageMultiplier: 0.95,
}),
visual: frozen({
...alvaSource.visual,
primaryColor: "#6c8194",
accentColor: "#c8efff",
scale: Math.max(1, alvaSource.visual.scale * 0.95),
}),
});
const alvajar: PopulationEntityDefinition = frozen({
...alvaSource,
id: "frozen-reach:alvajar",
kind: "mob",
name: "Alvajar",
hasLoot: false,
combat: frozen({
serverEntry: 97_770,
level: 60,
healthMultiplier: 0.32,
damageMultiplier: 0.55,
moveSpeed: 3.5,
leashRange: 55,
mechanicIds: frozen(["authored-alvajar-strike"]),
attacks: frozen([
attack({
id: "authored-alvajar-strike",
name: "Alvajar Strike",
delivery: "melee",
target: "primary",
school: "frost",
animation: "attack",
range: 4.5,
cooldownMs: 2_100,
damageMultiplier: 0.75,
}),
]),
}),
visual: frozen({
...alvaSource.visual,
primaryColor: "#312d4b",
accentColor: "#a9d8ff",
scale: Math.max(0.78, alvaSource.visual.scale * 0.7),
}),
});
const removed = new Set([
"frozen-reach-guardian",
"frozen-reach-caster",
"frozen-reach-brute",
]);
const entities: PopulationDefinitionMap = frozen({
...withoutKeys(definition.entities, removed),
"entry-97700": alva,
[arktos.id]: arktos,
[alvajar.id]: alvajar,
});
const arktosSpawn: StaticMobSpawnDefinition = frozen({
id: "frozen-reach:arktos",
entityId: arktos.id,
position: offset(alvaSpawn.position, -6.5, 5.5),
yaw: Math.PI * 0.75,
spawnMask: alvaSpawn.spawnMask,
});
const arktosGroup: DungeonEncounterSpawnGroupDefinition = frozen({
id: "frozen-reach:arktos-attempt",
spawns: frozen([arktosSpawn]),
});
const alvajarSpawns = frozen([
[-8, -7],
[8, -7],
[-8, 7],
[8, 7],
].map(([x, z], index): StaticMobSpawnDefinition => frozen({
id: `frozen-reach:alvajar-${index + 1}`,
entityId: alvajar.id,
position: offset(alvaSpawn.position, x, z),
yaw: Math.atan2(-x, -z),
spawnMask: alvaSpawn.spawnMask,
})));
const alvajarGroup: DungeonEncounterSpawnGroupDefinition = frozen({
id: "frozen-reach:rebirth-alvajar",
spawns: alvajarSpawns,
});
const encounter: DungeonEncounterDefinition = frozen({
id: markerId,
dungeonId: definition.id,
initialPhase: "duo-active",
initialState: { flags: { "arktos-defeated": false } },
resetPolicy: { bossWipePhase: "duo-active", checkpointPhase: "duo-active" },
source: sourceReview(
evidence,
"Alva and an attempt-scoped Arktos begin together with recovered D0 ability families. Arktos's death moves Alva to the recovered vengeance family and activates four spell-linked Alvajar proxies; a wipe restores the duo.",
),
rules: frozen([
frozen({
id: "spawn-arktos-attempt",
trigger: { kind: "tick" },
guards: [
{ kind: "phase", phases: ["duo-active"] },
{ kind: "spawn-group-active", spawnGroupId: arktosGroup.id, equals: false },
],
actions: [
{ id: "activate-arktos-attempt", kind: "activate-spawn-group", spawnGroupId: arktosGroup.id },
],
}),
frozen({
id: "arktos-defeated",
trigger: { kind: "entity-death", entityId: arktosSpawn.id },
guards: [{ kind: "phase", phases: ["duo-active"] }],
actions: [
{ id: "mark-arktos-defeated", kind: "set-flag", flagId: "arktos-defeated", value: true },
{ id: "deactivate-arktos-attempt", kind: "deactivate-spawn-group", spawnGroupId: arktosGroup.id },
{ id: "activate-rebirth-alvajar", kind: "activate-spawn-group", spawnGroupId: alvajarGroup.id },
{ id: "enter-alva-vengeance", kind: "set-phase", phase: "vengeance" },
],
}),
frozen({
id: "alvajar-defeated",
trigger: { kind: "entities-dead", entityIds: alvajarSpawns.map((spawn) => spawn.id) },
guards: [{ kind: "phase", phases: ["vengeance"] }],
actions: [{ id: "deactivate-rebirth-alvajar", kind: "deactivate-spawn-group", spawnGroupId: alvajarGroup.id }],
}),
frozen({
id: "alva-defeated",
trigger: { kind: "entity-death", entityId: alvaSpawn.id },
actions: [
{ id: "cleanup-rebirth-alvajar", kind: "deactivate-spawn-group", spawnGroupId: alvajarGroup.id },
{ id: "complete-alva-encounter", kind: "set-phase", phase: "complete", persistence: "run" },
],
}),
]),
});
const staticSpawns = frozen([
frozen({ ...alvaSpawn, entityId: alva.id }),
]);
return frozen({
...definition,
entities,
staticSpawns,
roamingPacks: frozen([]),
encounters: frozen([...replaceBaselineReview(definition, evidence), encounter]),
encounterSpawnGroups: frozen([
...(definition.encounterSpawnGroups ?? []).filter((group) => (
group.id !== arktosGroup.id && group.id !== alvajarGroup.id
)),
arktosGroup,
alvajarGroup,
]),
validation: appendWarnings(definition, [
"Frozen Reach now uses the recovered Alva/Arktos encounter families; all cadence, placement, and hazard behavior remains source-backed adaptation pending live playtest.",
], ["Trash identities and placements are procedural"]),
});
}
function tinkerAttackKit(phase: "active" | "overdrive", cooldownScale: number): readonly EnemyAttackDefinition[] {
const condition = encounterCondition("tinkertech-showdown:mechatron", [phase]);
const scaled = (milliseconds: number) => Math.round(milliseconds * cooldownScale);
const suffix = phase === "active" ? "normal" : "overdrive";
return frozen([
attack({
id: `rocket-257438-${suffix}`,
name: "Rocket",
spellId: 257_438,
delivery: "area",
target: "random-party",
school: "fire",
animation: "cast",
castTimeMs: 2_000,
range: 42,
radius: 7,
cooldownMs: scaled(8_000),
damageMultiplier: 1.05,
telegraph: { shape: "circle", anchor: "target", warningMs: 2_000, radius: 7 },
condition,
}),
attack({
id: `gatling-missile-barrage-257459-${suffix}`,
name: "Gatling Missile Barrage",
spellId: 257_459,
delivery: "area",
target: "nearby-party",
school: "fire",
animation: "cast",
castTimeMs: 3_000,
range: 38,
cooldownMs: scaled(14_000),
damageMultiplier: 0.92,
telegraph: { shape: "line", anchor: "source", warningMs: 3_000, length: 38, width: 6 },
condition,
}),
attack({
id: `incineration-beam-257470-${suffix}`,
name: "Incineration Beam",
spellId: 257_470,
delivery: "area",
target: "nearby-party",
school: "fire",
animation: "cast",
castTimeMs: 2_500,
range: 45,
cooldownMs: scaled(20_000),
damageMultiplier: 1.25,
telegraph: { shape: "line", anchor: "source", warningMs: 2_500, length: 45, width: 4.5 },
condition,
}),
attack({
id: `electrified-zone-257472-${suffix}`,
name: "Electrified Zone",
spellId: 257_472,
delivery: "area",
target: "random-party",
school: "nature",
animation: "cast",
castTimeMs: 1_500,
range: 40,
radius: 8,
cooldownMs: scaled(16_000),
damageMultiplier: 0,
telegraph: { shape: "circle", anchor: "target", warningMs: 1_500, radius: 8 },
effect: { kind: "control", mechanic: "stun", durationMs: 1_500 },
condition,
}),
]);
}
function buildTinkertechShowdown(definition: DungeonDefinition): DungeonDefinition {
const markerId = "tinkertech-showdown:mechatron";
if (definition.encounters?.some((encounter) => encounter.id === markerId)) return definition;
const evidence = EVIDENCE_BY_DUNGEON.get(definition.id)!;
const bossSource = requireEntity(definition, "entry-663012");
const bossSpawn = requireSpawn(definition, "encounter-9013");
const guardbotSource = requireEntity(definition, "tinkertech-showdown-guardian");
const coilcasterSource = requireEntity(definition, "tinkertech-showdown-caster");
const demolisherSource = requireEntity(definition, "tinkertech-showdown-brute");
const normalAttacks = tinkerAttackKit("active", 1);
const overdriveAttacks = tinkerAttackKit("overdrive", 0.8);
const mechatron = frozen({
...bossSource,
name: "Mechatron",
title: "HealerMan-authored machine encounter",
combat: combatWith(
bossSource,
frozen([
attack({
id: "basic-attack",
name: "Piston Slam",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
range: 5,
cooldownMs: 2_000,
damageMultiplier: 1,
}),
...normalAttacks,
...overdriveAttacks,
]),
[...normalAttacks, ...overdriveAttacks].map((entry) => entry.id),
{ serverEntry: 663_012, healthMultiplier: 1.4, damageMultiplier: 1.08 },
),
} as PopulationEntityDefinition);
const guardbotAttacks = frozen([
attack({
id: "servo-strike",
name: "Servo Strike",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
range: 4.5,
cooldownMs: 1_900,
damageMultiplier: 1,
}),
attack({
id: "authored-clamping-field",
name: "Clamping Field",
delivery: "area",
target: "nearby-party",
school: "nature",
animation: "cast",
castTimeMs: 1_500,
range: 8,
radius: 8,
cooldownMs: 11_000,
damageMultiplier: 0,
effect: { kind: "control", mechanic: "root", durationMs: 1_500 },
}),
]);
const coilcasterAttacks = frozen([
attack({
id: "coil-discharge-257473",
name: "Coil Discharge",
spellId: 257_473,
delivery: "projectile",
target: "random-party",
school: "nature",
animation: "cast",
castTimeMs: 1_400,
range: 30,
cooldownMs: 5_500,
damageMultiplier: 0.9,
}),
]);
const demolisherAttacks = frozen([
attack({
id: "demolition-rocket-257438",
name: "Demolition Rocket",
spellId: 257_438,
delivery: "area",
target: "random-party",
school: "fire",
animation: "cast",
castTimeMs: 2_000,
range: 32,
radius: 5,
cooldownMs: 8_000,
damageMultiplier: 1,
telegraph: { shape: "circle", anchor: "target", warningMs: 2_000, radius: 5 },
}),
]);
const guardbot = frozen({
...guardbotSource,
name: "Tinkertech Guardbot",
combat: combatWith(guardbotSource, guardbotAttacks, guardbotAttacks.map((entry) => entry.id)),
} as PopulationEntityDefinition);
const coilcaster = frozen({
...coilcasterSource,
name: "Tinkertech Coilcaster",
combat: combatWith(coilcasterSource, coilcasterAttacks, coilcasterAttacks.map((entry) => entry.id)),
} as PopulationEntityDefinition);
const demolisher = frozen({
...demolisherSource,
name: "Tinkertech Demolisher",
combat: combatWith(demolisherSource, demolisherAttacks, demolisherAttacks.map((entry) => entry.id)),
} as PopulationEntityDefinition);
const entities: PopulationDefinitionMap = frozen({
...definition.entities,
"entry-663012": mechatron,
"tinkertech-showdown-guardian": guardbot,
"tinkertech-showdown-caster": coilcaster,
"tinkertech-showdown-brute": demolisher,
});
const reinforcementGroup: DungeonEncounterSpawnGroupDefinition = frozen({
id: "tinkertech-showdown:overdrive-reinforcements",
spawns: frozen([
frozen({
id: "tinkertech-showdown:reinforcement-a",
entityId: coilcaster.id,
position: offset(bossSpawn.position, -9, -14),
yaw: 0,
spawnMask: bossSpawn.spawnMask,
}),
frozen({
id: "tinkertech-showdown:reinforcement-b",
entityId: coilcaster.id,
position: offset(bossSpawn.position, 9, -14),
yaw: 0,
spawnMask: bossSpawn.spawnMask,
}),
]),
});
const encounter: DungeonEncounterDefinition = frozen({
id: markerId,
dungeonId: definition.id,
initialPhase: "active",
initialState: { flags: { overdrive: false } },
resetPolicy: { bossWipePhase: "active", checkpointPhase: "active" },
source: sourceReview(
evidence,
"Mechatron keeps its exact recovered identity but uses an explicitly authored machine kit. At 35 percent health it enters a 20-percent-faster overdrive kit and calls two authored Coilcaster reinforcements.",
),
rules: frozen([
frozen({
id: "mechatron-overdrive",
trigger: { kind: "entity-health", entityId: bossSpawn.id, atOrBelowPercent: 35 },
guards: [{ kind: "phase", phases: ["active"] }],
actions: [
{ id: "mark-mechatron-overdrive", kind: "set-flag", flagId: "overdrive", value: true },
{ id: "activate-mechatron-reinforcements", kind: "activate-spawn-group", spawnGroupId: reinforcementGroup.id },
{ id: "enter-mechatron-overdrive", kind: "set-phase", phase: "overdrive" },
],
}),
frozen({
id: "mechatron-defeated",
trigger: { kind: "entity-death", entityId: bossSpawn.id },
actions: [
{ id: "cleanup-mechatron-reinforcements", kind: "deactivate-spawn-group", spawnGroupId: reinforcementGroup.id },
{ id: "complete-mechatron-encounter", kind: "set-phase", phase: "complete", persistence: "run" },
],
}),
]),
});
return frozen({
...definition,
entities,
encounters: frozen([...replaceBaselineReview(definition, evidence), encounter]),
encounterSpawnGroups: frozen([
...(definition.encounterSpawnGroups ?? []).filter((group) => group.id !== reinforcementGroup.id),
reinforcementGroup,
]),
validation: appendWarnings(definition, [
"Tinkertech population and Mechatron mechanics are explicitly HealerMan-authored; candidate spell ids are not authoritative creature bindings.",
], ["Trash identities and placements are procedural"]),
});
}
const FORGOTTEN_MINE_MARKER_ID = "forgotten-mine:geolord-vek";
const FORGOTTEN_MINE_GEOSPIKE_GROUP_ID = "forgotten-mine:geospike";
const FORGOTTEN_MINE_SHARD_IDS = Object.freeze([
"forgotten-mine:resonite-shard-1",
"forgotten-mine:resonite-shard-2",
"forgotten-mine:resonite-shard-3",
] as const);
const FORGOTTEN_MINE_SHARD_ID_SET: ReadonlySet<string> = new Set(FORGOTTEN_MINE_SHARD_IDS);
function forgottenMineWorldObjects(bossPosition: Vector3Tuple): readonly DungeonWorldObjectDefinition[] {
const offsets = [
[-12, 0],
[6, 10.392],
[6, -10.392],
] as const;
return frozen(offsets.map(([x, z], index): DungeonWorldObjectDefinition => frozen({
id: FORGOTTEN_MINE_SHARD_IDS[index],
name: "Resonite Shard",
kind: "resource",
position: offset(bossPosition, x, z),
halfExtents: [2.25, 2.4, 2.25],
initialState: "dormant",
states: frozen([
frozen({ id: "dormant", label: "Dormant", visible: false, blocksMovement: false, interactionEnabled: false }),
frozen({ id: "ready", label: "Charged", visible: true, blocksMovement: false, interactionEnabled: true }),
frozen({ id: "consumed", label: "Consumed", visible: false, blocksMovement: false, interactionEnabled: false }),
]),
interaction: frozen({
prompt: "Consume Resonite Shard",
range: 4.5,
signalId: `forgotten-mine:consume-resonite-${index + 1}`,
allowedStates: ["ready"],
oneShot: false,
instructions: "Consume a shard to suppress Geolord Vek's next Resonance window.",
}),
visual: frozen({ color: "#76dbe8", emissive: "#176778", scale: [1.35, 2.2, 1.35] }),
source: frozen({
fidelity: "source-backed-adapted",
files: RECOVERY_FILES,
templateId: 254_051,
note: "Template 254051 is spell-linked exactly by Create Resonite Shard 354032. Placement, bounds, procedural visual, state cycle, and cleanse behavior are authored.",
}),
})));
}
function buildForgottenMine(definition: DungeonDefinition): DungeonDefinition {
if (definition.encounters?.some((encounter) => encounter.id === FORGOTTEN_MINE_MARKER_ID)) return definition;
const evidence = EVIDENCE_BY_DUNGEON.get(definition.id)!;
const bossSource = requireEntity(definition, "entry-254272");
const bossSpawn = requireSpawn(definition, "encounter-5069");
const condition = (states: readonly string[]) => encounterCondition(FORGOTTEN_MINE_MARKER_ID, states);
const allCombatPhases = [
"boss-active",
"pre-resonance-geospike",
"resonance-active",
"resonance-cleansed",
"resonance-recharge",
"resonance-geospike",
] as const;
const phaseTwoDamagePhases = ["resonance-active", "resonance-recharge"] as const;
const vekAttacks = frozen([
attack({
id: "basic-attack",
name: "Stone Fist",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
range: 5,
cooldownMs: 2_000,
damageMultiplier: 1,
}),
attack({
id: "stone-shield-354002",
name: "Stone Shield",
spellId: 354_002,
delivery: "area",
target: "self",
school: "nature",
animation: "cast",
range: 0,
cooldownMs: 30_000,
initialCooldownMs: [0, 0],
damageMultiplier: 0,
effect: { kind: "aura", aura: "stone-shield", durationMs: 10_000, magnitude: 0.2 },
condition: condition(allCombatPhases),
}),
attack({
id: "stonebreaker-354000",
name: "Stonebreaker",
spellId: 354_000,
delivery: "area",
target: "nearby-party",
school: "physical",
animation: "cast",
castTimeMs: 1_700,
range: 12,
cooldownMs: 6_500,
damageMultiplier: 1.15,
telegraph: { shape: "cone", anchor: "source", warningMs: 1_700, length: 12, angleDegrees: 85 },
condition: condition(allCombatPhases),
}),
attack({
id: "quake-354001",
name: "Quake",
spellId: 354_001,
delivery: "area",
target: "nearby-party",
school: "nature",
animation: "cast",
castTimeMs: 1_800,
range: 18,
radius: 18,
cooldownMs: 18_000,
damageMultiplier: 0.82,
telegraph: { shape: "circle", anchor: "source", warningMs: 1_800, radius: 18 },
condition: condition(allCombatPhases),
}),
attack({
id: "geode-barrage-354015",
name: "Geode Barrage",
spellId: 354_015,
delivery: "projectile",
target: "random-party",
school: "nature",
animation: "cast",
castTimeMs: 1_600,
range: 38,
cooldownMs: 11_000,
damageMultiplier: 1.02,
condition: condition(allCombatPhases),
}),
attack({
id: "raise-geospike-354004",
name: "Geospike",
spellId: 354_004,
delivery: "area",
target: "self",
school: "nature",
animation: "cast",
castTimeMs: 1_200,
range: 0,
cooldownMs: 15_000,
initialCooldownMs: [8_000, 10_000],
damageMultiplier: 0,
effect: { kind: "encounter-event", eventId: "forgotten-mine:geospike-cast" },
condition: condition(["boss-active", ...phaseTwoDamagePhases]),
}),
attack({
id: "resonance-354013",
name: "Resonance",
spellId: 354_013,
delivery: "area",
target: "nearby-party",
school: "nature",
animation: "cast",
castTimeMs: 2_000,
range: 28,
radius: 28,
cooldownMs: 14_000,
damageMultiplier: 1.15,
telegraph: { shape: "circle", anchor: "source", warningMs: 2_000, radius: 28 },
condition: condition(phaseTwoDamagePhases),
}),
attack({
id: "ruptured-earth-354017",
name: "Ruptured Earth",
spellId: 354_017,
delivery: "area",
target: "random-party",
school: "nature",
animation: "cast",
castTimeMs: 2_200,
range: 38,
radius: 7,
cooldownMs: 22_000,
damageMultiplier: 1.25,
telegraph: { shape: "circle", anchor: "target", warningMs: 2_200, radius: 7 },
condition: condition(["resonance-active", "resonance-cleansed", "resonance-recharge"]),
}),
]);
const vek: PopulationEntityDefinition = frozen({
...bossSource,
name: "Geolord Vek",
title: "Source-backed adapted encounter",
combat: combatWith(bossSource, vekAttacks, vekAttacks.map((entry) => entry.id), {
serverEntry: 254_272,
healthMultiplier: 1.4,
damageMultiplier: 1.08,
}),
});
const stoneguardSource = requireEntity(definition, "forgotten-mine-guardian");
const geomancerSource = requireEntity(definition, "forgotten-mine-caster");
const bruteSource = requireEntity(definition, "forgotten-mine-brute");
const stoneguard = frozen({
...stoneguardSource,
name: "Forgotten Mine Stoneguard",
combat: combatWith(stoneguardSource, frozen([
attack({
id: "stoneguard-strike",
name: "Stoneguard Strike",
delivery: "melee",
target: "primary",
school: "physical",
animation: "attack",
range: 4.5,
cooldownMs: 1_900,
damageMultiplier: 1,
}),
attack({
id: "authored-stone-guard",
name: "Stone Guard",
delivery: "area",
target: "self",
school: "nature",
animation: "cast",
range: 0,
cooldownMs: 16_000,
damageMultiplier: 0,
effect: { kind: "aura", aura: "authored-stone-guard", durationMs: 5_000, magnitude: 0.15 },
}),
]), ["stoneguard-strike", "authored-stone-guard"]),
} as PopulationEntityDefinition);
const geomancer = frozen({
...geomancerSource,
name: "Forgotten Mine Geomancer",
combat: combatWith(geomancerSource, frozen([
attack({
id: "authored-geode-bolt",
name: "Geode Bolt",
delivery: "projectile",
target: "random-party",
school: "nature",
animation: "cast",
castTimeMs: 1_500,
range: 30,
cooldownMs: 5_500,
damageMultiplier: 0.9,
}),
]), ["authored-geode-bolt"]),
} as PopulationEntityDefinition);
const quarryBrute = frozen({
...bruteSource,
name: "Forgotten Mine Quarry Brute",
combat: combatWith(bruteSource, frozen([
attack({
id: "authored-quarry-cleave",
name: "Quarry Cleave",
delivery: "area",
target: "nearby-party",
school: "physical",
animation: "attack",
range: 9,
cooldownMs: 6_000,
damageMultiplier: 1.08,
telegraph: { shape: "cone", anchor: "source", warningMs: 1_300, length: 9, angleDegrees: 90 },
}),
]), ["authored-quarry-cleave"]),
} as PopulationEntityDefinition);
const geospike: PopulationEntityDefinition = frozen({
...bossSource,
id: "forgotten-mine:geospike",
kind: "mob",
name: "Geospike",
hasLoot: false,
combat: frozen({
serverEntry: 254_278,
level: bossSource.combat?.level,
healthMultiplier: 0.5,
damageMultiplier: 0.8,
moveSpeed: 0.1,
leashRange: 6,
mechanicIds: frozen(["geospike-eruption-354005"]),
attacks: frozen([
attack({
id: "geospike-eruption-354005",
name: "Geospike Eruption",
spellId: 354_005,
delivery: "area",
target: "nearby-party",
school: "nature",
animation: "cast",
castTimeMs: 1_800,
range: 7,
radius: 7,
cooldownMs: 60_000,
initialCooldownMs: [0, 0],
maximumCasts: 1,
damageMultiplier: 1.15,
telegraph: { shape: "circle", anchor: "source", warningMs: 1_800, radius: 7 },
}),
]),
}),
visual: frozen({
...bossSource.visual,
primaryColor: "#705a40",
accentColor: "#f7c867",
scale: Math.max(0.9, bossSource.visual.scale * 0.82),
}),
});
const entities: PopulationDefinitionMap = frozen({
...definition.entities,
"entry-254272": vek,
"forgotten-mine-guardian": stoneguard,
"forgotten-mine-caster": geomancer,
"forgotten-mine-brute": quarryBrute,
[geospike.id]: geospike,
});
const geospikeSpawn: StaticMobSpawnDefinition = frozen({
id: "forgotten-mine:geospike",
entityId: geospike.id,
position: offset(bossSpawn.position, 0, -7),
yaw: 0,
spawnMask: bossSpawn.spawnMask,
});
const geospikeGroup: DungeonEncounterSpawnGroupDefinition = frozen({
id: FORGOTTEN_MINE_GEOSPIKE_GROUP_ID,
spawns: frozen([geospikeSpawn]),
});
const worldObjects = forgottenMineWorldObjects(bossSpawn.position);
const revealShardActions = worldObjects.map((object, index) => ({
id: `reveal-resonite-${index + 1}`,
kind: "set-world-object-state" as const,
objectId: object.id,
state: "ready",
}));
const encounter: DungeonEncounterDefinition = frozen({
id: FORGOTTEN_MINE_MARKER_ID,
dungeonId: definition.id,
initialPhase: "boss-active",
initialState: {
counters: { "resonite-cleanses": 0 },
worldObjectStates: Object.fromEntries(worldObjects.map((object) => [object.id, "dormant"])),
},
resetPolicy: { bossWipePhase: "boss-active", checkpointPhase: "boss-active" },
source: sourceReview(
evidence,
"Geolord Vek uses the recovered earth family, enters Resonance at 50 percent, creates a short-lived spell-linked Geospike proxy, and exposes three spell-linked Resonite Shards with authored cleanse/recharge semantics.",
),
rules: frozen([
frozen({
id: "enter-resonance-phase",
trigger: { kind: "entity-health", entityId: bossSpawn.id, atOrBelowPercent: 50 },
actions: [
{ id: "cleanup-geospike-for-resonance", kind: "deactivate-spawn-group", spawnGroupId: geospikeGroup.id },
...revealShardActions,
{ id: "enter-vek-resonance", kind: "set-phase", phase: "resonance-active" },
],
}),
frozen({
id: "raise-geospike-before-resonance",
trigger: { kind: "signal", signalId: "forgotten-mine:geospike-cast" },
guards: [{ kind: "phase", phases: ["boss-active"] }],
actions: [
{ id: "activate-pre-resonance-geospike", kind: "activate-spawn-group", spawnGroupId: geospikeGroup.id },
{ id: "enter-pre-resonance-geospike", kind: "set-phase", phase: "pre-resonance-geospike" },
],
once: false,
}),
frozen({
id: "clear-geospike-before-resonance",
trigger: { kind: "elapsed", afterMs: 3_500 },
guards: [{ kind: "phase", phases: ["pre-resonance-geospike"] }],
actions: [
{ id: "deactivate-pre-resonance-geospike", kind: "deactivate-spawn-group", spawnGroupId: geospikeGroup.id },
{ id: "resume-vek-before-resonance", kind: "set-phase", phase: "boss-active" },
],
once: false,
}),
frozen({
id: "raise-geospike-during-resonance",
trigger: { kind: "signal", signalId: "forgotten-mine:geospike-cast" },
guards: [{ kind: "phase", phases: ["resonance-active", "resonance-recharge"] }],
actions: [
{ id: "activate-resonance-geospike", kind: "activate-spawn-group", spawnGroupId: geospikeGroup.id },
{ id: "enter-resonance-geospike", kind: "set-phase", phase: "resonance-geospike" },
],
once: false,
}),
frozen({
id: "clear-geospike-during-resonance",
trigger: { kind: "elapsed", afterMs: 3_500 },
guards: [{ kind: "phase", phases: ["resonance-geospike"] }],
actions: [
{ id: "deactivate-resonance-geospike", kind: "deactivate-spawn-group", spawnGroupId: geospikeGroup.id },
{ id: "resume-vek-resonance", kind: "set-phase", phase: "resonance-recharge" },
],
once: false,
}),
...worldObjects.map((object, index) => frozen({
id: `consume-resonite-shard-${index + 1}`,
trigger: { kind: "interact" as const, objectId: object.id },
guards: [
{ kind: "phase" as const, phases: ["resonance-active", "resonance-cleansed", "resonance-recharge"] },
{ kind: "world-object-state" as const, objectId: object.id, states: ["ready"] },
],
actions: [
{
id: `consume-resonite-object-${index + 1}`,
kind: "set-world-object-state" as const,
objectId: object.id,
state: "consumed",
},
{ id: `count-resonite-cleanse-${index + 1}`, kind: "increment-counter" as const, counterId: "resonite-cleanses" },
{ id: `apply-resonite-cleanse-${index + 1}`, kind: "set-phase" as const, phase: "resonance-cleansed" },
],
once: false,
})),
frozen({
id: "expire-resonite-cleanse",
trigger: { kind: "elapsed", afterMs: 8_000 },
guards: [{ kind: "phase", phases: ["resonance-cleansed"] }],
actions: [{ id: "enter-resonite-recharge", kind: "set-phase", phase: "resonance-recharge" }],
once: false,
}),
frozen({
id: "recharge-resonite-shards",
trigger: { kind: "elapsed", afterMs: 22_000 },
guards: [{ kind: "phase", phases: ["resonance-recharge"] }],
actions: [
...worldObjects.map((object, index) => ({
id: `recharge-resonite-${index + 1}`,
kind: "set-world-object-state" as const,
objectId: object.id,
state: "ready",
})),
{ id: "complete-resonite-recharge", kind: "set-phase", phase: "resonance-active" },
],
once: false,
}),
frozen({
id: "geolord-vek-defeated",
trigger: { kind: "entity-death", entityId: bossSpawn.id },
actions: [
{ id: "cleanup-final-geospike", kind: "deactivate-spawn-group", spawnGroupId: geospikeGroup.id },
...worldObjects.map((object, index) => ({
id: `hide-final-resonite-${index + 1}`,
kind: "set-world-object-state" as const,
objectId: object.id,
state: "consumed",
})),
{ id: "complete-geolord-vek-encounter", kind: "set-phase", phase: "complete", persistence: "run" },
],
}),
]),
});
return frozen({
...definition,
entities,
encounters: frozen([...replaceBaselineReview(definition, evidence), encounter]),
encounterSpawnGroups: frozen([
...(definition.encounterSpawnGroups ?? []).filter((group) => group.id !== geospikeGroup.id),
geospikeGroup,
]),
serverGameObjects: frozen([
...definition.serverGameObjects.filter((object) => !FORGOTTEN_MINE_SHARD_ID_SET.has(object.id)),
...worldObjects,
]),
validation: appendWarnings(definition, [
"Forgotten Mine uses recovered Vek, Geospike, and Resonite families; trash, cadence, placement, and cleanse behavior remain explicitly authored pending live playtest.",
], ["Trash identities and placements are procedural"]),
});
}
/**
* Applies only the three reviewed Ascension custom-dungeon packages. Generated
* campaign data remains immutable and unrelated definitions are returned by
* identity so this can be composed safely with the WoW and RuneWaker adapters.
*/
export function withAscensionCustomEncounterContent(definition: DungeonDefinition): DungeonDefinition {
if (definition.id === "frozen-reach") return buildFrozenReach(definition);
if (definition.id === "forgotten-mine") return buildForgottenMine(definition);
if (definition.id === "tinkertech-showdown") return buildTinkertechShowdown(definition);
return definition;
}