forked from phenom/healer-man
187 lines
7.6 KiB
TypeScript
187 lines
7.6 KiB
TypeScript
import type { DungeonDefinition, DungeonBossObjective } from "../dungeonTypes";
|
|
import type { PopulationEntityDefinition, StaticMobSpawnDefinition } from "../mobPopulation";
|
|
import type { DungeonWorldObjectDefinition } from "../dungeonWorldObjects";
|
|
import {
|
|
BITE_SIZED_STOCKADES_DUNGEON_ID,
|
|
STORMWIND_STOCKADE_DUNGEON_ID,
|
|
} from "../dungeonContentSource";
|
|
import { seededBossIndex } from "./seededBossSelection";
|
|
|
|
const BOSS_ROOM_POSITION = [-159.582, -25.6062, 1.2531] as const;
|
|
const CENTRAL_CORRIDOR_HALF_WIDTH = 15;
|
|
|
|
export interface BiteSizedStockadesBossCandidate {
|
|
readonly sourceSpawnId: string;
|
|
readonly name: string;
|
|
}
|
|
|
|
export const BITE_SIZED_STOCKADES_BOSS_CANDIDATES: readonly BiteSizedStockadesBossCandidate[] = Object.freeze([
|
|
{ sourceSpawnId: "creature-84027", name: "Targorr the Dread" },
|
|
{ sourceSpawnId: "creature-79088", name: "Kam Deepfury" },
|
|
{ sourceSpawnId: "creature-79098", name: "Hamhock" },
|
|
{ sourceSpawnId: "creature-79124", name: "Bazil Thredd" },
|
|
{ sourceSpawnId: "creature-79116", name: "Dextren Ward" },
|
|
]);
|
|
|
|
export function biteSizedStockadesBossIndex(seed: string): number {
|
|
return seededBossIndex(seed, BITE_SIZED_STOCKADES_BOSS_CANDIDATES.length);
|
|
}
|
|
|
|
function branchWall(id: string, name: string, z: number): DungeonWorldObjectDefinition {
|
|
return {
|
|
id,
|
|
name,
|
|
kind: "gate",
|
|
position: [-129.17, -22.1, z],
|
|
halfExtents: [6, 5.2, 0.65],
|
|
initialState: "sealed",
|
|
states: [{
|
|
id: "sealed",
|
|
label: "Sealed",
|
|
visible: true,
|
|
blocksMovement: true,
|
|
blocksSight: true,
|
|
interactionEnabled: false,
|
|
}],
|
|
visual: { color: "#332f2b" },
|
|
source: {
|
|
fidelity: "healer-man-authored",
|
|
files: ["src/game/dungeons/biteSizedStockades.ts"],
|
|
note: "Permanent wall for the bite-sized route; the source Stockades geometry remains unchanged.",
|
|
},
|
|
};
|
|
}
|
|
|
|
function sourceBoss(
|
|
source: DungeonDefinition,
|
|
candidate: BiteSizedStockadesBossCandidate,
|
|
): { readonly spawn: StaticMobSpawnDefinition; readonly entity: PopulationEntityDefinition } {
|
|
const spawn = source.staticSpawns.find((entry) => entry.id === candidate.sourceSpawnId);
|
|
const entity = spawn ? source.entities[spawn.entityId] : undefined;
|
|
if (!spawn || !entity || entity.kind !== "boss") {
|
|
throw new Error(`Stockades boss source is missing: ${candidate.sourceSpawnId}`);
|
|
}
|
|
return { spawn, entity };
|
|
}
|
|
|
|
/**
|
|
* Builds a one-boss Stockades variant without mutating the generated source definition.
|
|
* The seed controls only boss identity, so saves and online party members can reconstruct
|
|
* the same run while a fresh run receives a new random selection.
|
|
*/
|
|
export function createBiteSizedStockadesDefinition(
|
|
source: DungeonDefinition,
|
|
seed: string,
|
|
): DungeonDefinition {
|
|
if (source.id !== STORMWIND_STOCKADE_DUNGEON_ID) {
|
|
throw new Error(`Bite-sized Stockades requires ${STORMWIND_STOCKADE_DUNGEON_ID}.`);
|
|
}
|
|
const candidate = BITE_SIZED_STOCKADES_BOSS_CANDIDATES[biteSizedStockadesBossIndex(seed)];
|
|
const selected = sourceBoss(source, candidate);
|
|
const sourceBossIds = new Set(BITE_SIZED_STOCKADES_BOSS_CANDIDATES.map((entry) => entry.sourceSpawnId));
|
|
const corridorSpawns = source.staticSpawns.filter((spawn) => (
|
|
!sourceBossIds.has(spawn.id)
|
|
&& Math.abs(spawn.position[2]) <= CENTRAL_CORRIDOR_HALF_WIDTH
|
|
));
|
|
const usedEntityIds = new Set(corridorSpawns.map((spawn) => spawn.entityId));
|
|
const entities = Object.fromEntries([
|
|
...Object.entries(source.entities).filter(([entityId]) => usedEntityIds.has(entityId)),
|
|
[selected.spawn.entityId, selected.entity],
|
|
]);
|
|
const boss: DungeonBossObjective = {
|
|
id: candidate.sourceSpawnId,
|
|
name: candidate.name,
|
|
position: BOSS_ROOM_POSITION,
|
|
};
|
|
|
|
return {
|
|
...source,
|
|
id: BITE_SIZED_STOCKADES_DUNGEON_ID,
|
|
title: "Stormwind Stockade: Cellblock Rush",
|
|
lfgDungeonIds: [],
|
|
presentation: {
|
|
...source.presentation,
|
|
location: "Stormwind Stockade · Central Cellblock",
|
|
theme: "Bite-Sized Dungeon",
|
|
summary: "Race through the central cellblock and defeat one surprise warden chosen from the five Stockades bosses.",
|
|
loadingMessage: "Sealing the side wings and choosing a cellblock warden...",
|
|
unchartedAreaName: "Central Cellblock",
|
|
},
|
|
areas: [
|
|
{ ...source.entrance, id: "entrance", center: source.entrance.footPosition, radius: 35 },
|
|
{ id: "bite-sized-boss-room", name: `${candidate.name}'s Cell`, center: BOSS_ROOM_POSITION, radius: 32 },
|
|
],
|
|
entities,
|
|
staticSpawns: [
|
|
...corridorSpawns,
|
|
{
|
|
id: candidate.sourceSpawnId,
|
|
entityId: selected.spawn.entityId,
|
|
sourceCreatureGuid: selected.spawn.sourceCreatureGuid,
|
|
position: BOSS_ROOM_POSITION,
|
|
yaw: selected.spawn.yaw,
|
|
spawnMask: selected.spawn.spawnMask,
|
|
},
|
|
],
|
|
roamingPacks: [],
|
|
bosses: [boss],
|
|
objectiveOrder: [boss],
|
|
finalBossDurability: "standard",
|
|
// Keep the complete reviewed Stockades navigation graph connected. The
|
|
// physical branch walls, not navmesh pruning, close the unused wings.
|
|
navigationLinks: source.navigationLinks,
|
|
traversalRamps: source.traversalRamps,
|
|
encounters: [{
|
|
id: `${BITE_SIZED_STOCKADES_DUNGEON_ID}:boss-progression`,
|
|
dungeonId: BITE_SIZED_STOCKADES_DUNGEON_ID,
|
|
initialPhase: "progression",
|
|
initialState: { flags: { "dungeon-complete": false } },
|
|
resetPolicy: { bossWipePhase: "progression", checkpointPhase: "progression" },
|
|
source: {
|
|
fidelity: "healer-man-authored",
|
|
scope: "boss-progression",
|
|
files: ["src/game/dungeons/biteSizedStockades.ts"],
|
|
reviewedBehavior: "A single seeded Stockades boss completes the bite-sized run in the first boss room.",
|
|
adaptations: [
|
|
"Each seeded run retains the selected source boss objective ID so saves, loot, guides, and leaderboards report the correct variant.",
|
|
"The one-boss run uses standard boss durability instead of the campaign-finale health floor.",
|
|
"The side-wing stairs retain their navigation bridges so the central route stays on the retained navmesh component; physical walls seal both unused wings.",
|
|
],
|
|
remainingGaps: [],
|
|
},
|
|
rules: [
|
|
{
|
|
id: "boss-defeated",
|
|
trigger: { kind: "entities-dead", entityIds: [candidate.sourceSpawnId] },
|
|
actions: [
|
|
{ id: "record-boss", kind: "complete-objective", objectiveId: candidate.sourceSpawnId, persistence: "run" },
|
|
{ id: "checkpoint-boss", kind: "set-checkpoint", checkpointId: candidate.sourceSpawnId, persistence: "checkpoint" },
|
|
],
|
|
},
|
|
{
|
|
id: "complete-run",
|
|
trigger: { kind: "tick" },
|
|
guards: [{ kind: "objective-complete", objectiveId: candidate.sourceSpawnId, equals: true }],
|
|
actions: [
|
|
{ id: "mark-dungeon-complete", kind: "set-flag", flagId: "dungeon-complete", value: true, persistence: "run" },
|
|
{ id: "enter-complete-phase", kind: "set-phase", phase: "complete", persistence: "run" },
|
|
],
|
|
},
|
|
],
|
|
}],
|
|
encounterSpawnGroups: [],
|
|
serverGameObjects: [
|
|
...source.serverGameObjects.filter((object) => Math.abs(object.position[2]) <= CENTRAL_CORRIDOR_HALF_WIDTH),
|
|
branchWall("bite-sized-stockades-north-wall", "Sealed North Wing", 16.4),
|
|
branchWall("bite-sized-stockades-south-wall", "Sealed South Wing", -16.4),
|
|
],
|
|
validation: {
|
|
...source.validation,
|
|
warnings: [
|
|
...source.validation.warnings,
|
|
"Bite-sized layout and branch walls are Healer Man-authored; environment and combat assets are shared with Stormwind Stockade.",
|
|
],
|
|
},
|
|
};
|
|
}
|