Update Healer Man server

This commit is contained in:
phenom committed 2026-09-27 14:31:15 -04:00
1 parent ab1434a3b8
commit e284f4fc76
60 files changed
+2704 -118

No files matched your search

+38 -2
View File
@@ -49,8 +49,10 @@ describe("RoM bite-sized finales", () => {
expect(dungeonContentPackId(id)).toBe(`dungeon:${source.id}`);
expect(slice.assets).toBe(source.assets);
expect(slice.staticSpawns).toHaveLength(id === "bite-sized-barren-caves-anglo" ? 4
: id === "bite-sized-kawaks-tomb-linobia" || id === "bite-sized-hall-of-survivors-pursuer" ? 5 : 6);
expect(slice.staticSpawns.every((spawn) => source.staticSpawns.includes(spawn))).toBe(true);
: id === "bite-sized-kawaks-tomb-linobia" || id === "bite-sized-hall-of-survivors-pursuer" ? 5
: id === "bite-sized-hoto-teeth" ? 10 : 6);
expect(slice.staticSpawns.every((spawn) => source.staticSpawns.some((candidate) =>
candidate.id === spawn.id && candidate.entityId === spawn.entityId))).toBe(true);
expect(slice.bosses).toHaveLength(1);
expect(slice.bosses[0].eventOnly).toBe(true);
expect(slice.staticSpawns).not.toContainEqual(expect.objectContaining({ id: slice.bosses[0].id }));
@@ -117,4 +119,38 @@ describe("RoM bite-sized finales", () => {
}), `${id} route to ${spawn.id}`).not.toBeNull();
}
}, 30_000);
it("stages both Hall packs and the finale in the twin-statue chamber", () => {
const slice = DUNGEON_REGISTRY["bite-sized-hall-of-survivors-pursuer"];
const guardians = slice.staticSpawns.filter((spawn) => spawn.id.includes("59919"));
const hunters = slice.staticSpawns.filter((spawn) => spawn.id.includes("119628"));
expect(guardians).toHaveLength(3);
expect(hunters).toHaveLength(2);
// The western side room lies beyond x=-185; the statue chamber is to its east.
expect([...slice.staticSpawns, slice.encounterSpawnGroups![0].spawns[0]].every(
(spawn) => spawn.position[0] > -185 && spawn.position[0] < -158)).toBe(true);
expect(Math.min(...hunters.map((spawn) => spawn.position[2]))
- Math.max(...guardians.map((spawn) => spawn.position[2]))).toBeGreaterThan(10);
});
it("fills Hoto with three readable source Ayam clusters and a cooler cavern palette", () => {
const slice = DUNGEON_REGISTRY["bite-sized-hoto-teeth"];
const first = slice.staticSpawns.filter((spawn) => spawn.position[2] > 40);
const middle = slice.staticSpawns.filter((spawn) => spawn.position[2] > 8 && spawn.position[2] < 25);
const last = slice.staticSpawns.filter((spawn) => spawn.position[2] < 2);
expect([first.length, middle.length, last.length]).toEqual([3, 4, 3]);
const entrance = slice.entrance.footPosition;
expect(first.every((spawn) => Math.hypot(spawn.position[0] - entrance[0],
spawn.position[2] - entrance[2]) < 16)).toBe(true);
expect(first.every((spawn) => first.every((other) => Math.hypot(
spawn.position[0] - other.position[0], spawn.position[2] - other.position[2]) < 8))).toBe(true);
expect(slice.presentation.ambientLight.intensity).toBe(0.48);
expect(slice.presentation.ambientLight.color).toBe("#91acb0");
expect(slice.presentation.hemisphereLight.groundColor).toBe("#244451");
expect(slice.presentation.directionalLight.color).toBe("#f3c28b");
expect(slice.presentation.pointLights).toHaveLength(3);
expect(slice.presentation.pointLights?.map((light) => light.color)).toEqual([
"#58afc6", "#ffc06d", "#68bfad",
]);
});
});
+56 -15
View File
@@ -26,10 +26,21 @@ interface RoMSlice {
readonly start: Vector3Tuple;
readonly forward: Vector3Tuple;
readonly finaleBossName: string;
readonly finalePosition?: Vector3Tuple;
readonly mobSpawnIds: readonly string[];
readonly mobSpawnPositions?: Readonly<Record<string, Vector3Tuple>>;
readonly pointLights?: NonNullable<DungeonDefinition["presentation"]["pointLights"]>;
readonly summary: string;
readonly adaptation: string;
readonly lighting?: { readonly ambient: number; readonly hemisphere: number; readonly directional: number };
readonly lighting?: {
readonly ambient: number;
readonly hemisphere: number;
readonly directional: number;
readonly ambientColor?: string;
readonly skyColor?: string;
readonly groundColor?: string;
readonly directionalColor?: string;
};
}
const ROM_SLICES: readonly RoMSlice[] = [
@@ -130,12 +141,29 @@ const ROM_SLICES: readonly RoMSlice[] = [
forward: [0.70710678, 0, -0.70710678],
finaleBossName: "Teeth of the Reef",
mobSpawnIds: [
"rw-hoto-spawn-5978644", "rw-hoto-spawn-5978652", "rw-hoto-spawn-5978647",
"rw-hoto-spawn-5978485", "rw-hoto-spawn-5978483",
"rw-hoto-spawn-5978486", "rw-hoto-spawn-5978484",
"rw-hoto-spawn-5978631", "rw-hoto-spawn-5979808",
"rw-hoto-spawn-5978631", "rw-hoto-spawn-5979808", "rw-hoto-spawn-5978629",
],
// Keep all three within the 16 m entry aggro circle and close enough for
// three-target companion AoE; the old staggered positions pulled one at a time.
mobSpawnPositions: {
"rw-hoto-spawn-5978644": [120, -33.6, 53],
"rw-hoto-spawn-5978652": [123, -33.6, 54],
"rw-hoto-spawn-5978647": [126, -33.6, 55],
},
summary: "Clear three Ayam packs across the reef cavern, then defeat one surprise Hoto boss.",
adaptation: "The approach guard, mage, and warrior are moved onto the short route. Jiasha, earlier boss rooms, and unrelated Hoto population are outside this run.",
lighting: {
ambient: 0.48, hemisphere: 0.7, directional: 0.88,
ambientColor: "#91acb0", skyColor: "#c2c6ad", groundColor: "#244451", directionalColor: "#f3c28b",
},
pointLights: [
{ position: [123, -30.1, 50], color: "#58afc6", intensity: 30, distance: 17 },
{ position: [132, -34, 16], color: "#ffc06d", intensity: 27, distance: 16 },
{ position: [135, -33, -4], color: "#68bfad", intensity: 30, distance: 16 },
],
summary: "Clear the Ayam guards, casters, and defenders, then defeat one surprise Hoto boss.",
adaptation: "Jiasha, earlier boss rooms, and unrelated Hoto population are outside this short run.",
},
{
sourceId: "kawaks-tomb",
@@ -146,6 +174,8 @@ const ROM_SLICES: readonly RoMSlice[] = [
start: [-85, -9.4, -243],
forward: [-1, 0, 0],
finaleBossName: "Linobia",
// Move Linobia off the source room's raised northwest edge onto its open center floor.
finalePosition: [-170, -9.295, -242],
mobSpawnIds: [
"rw-kawaks-tomb-spawn-41889142", "rw-kawaks-tomb-spawn-41889139",
"rw-kawaks-tomb-spawn-41892423", "rw-kawaks-tomb-spawn-41890637",
@@ -163,13 +193,20 @@ const ROM_SLICES: readonly RoMSlice[] = [
start: [-155, -17.55, 35],
forward: [-1, 0, 0],
finaleBossName: "Divine Punishment Pursuer",
finalePosition: [-171, -17.55, 31],
mobSpawnIds: [
"rw-hall-of-survivors-spawn-11962803", "rw-hall-of-survivors-spawn-11962817",
"rw-hall-of-survivors-spawn-5991940", "rw-hall-of-survivors-spawn-5991941",
"rw-hall-of-survivors-spawn-5991939",
"rw-hall-of-survivors-spawn-11962803", "rw-hall-of-survivors-spawn-11962817",
],
summary: "Clear the hunters and Rune Guardians, then defeat one surprise Hall boss.",
adaptation: "The other Pursuer and Seeker bosses, earlier rooms, and their combined source scripts are outside this short run.",
// The two Hunters originally stood in the western side room. Reposition them
// as a distinct pack at the far end of the open chamber with the statues.
mobSpawnPositions: {
"rw-hall-of-survivors-spawn-11962803": [-165, -17.55, 38],
"rw-hall-of-survivors-spawn-11962817": [-175, -17.55, 38],
},
summary: "Clear two packs around the ward's twin statues, then defeat one surprise Hall boss in the chamber.",
adaptation: "The Hunters and selected boss are moved out of the western side room into the statue chamber. Other Pursuer and Seeker bosses, earlier rooms, and combined source scripts are outside this short run.",
},
];
@@ -180,18 +217,20 @@ function createRoMSlice(source: DungeonDefinition, slice: RoMSlice, seed: string
throw new Error(`${slice.sourceId} is missing ${slice.finaleBossName} or its boss list.`);
}
const boss = source.bosses[seededBossIndex(seed, source.bosses.length)];
const finalePosition = slice.finalePosition ?? finale.position;
const sourceBossSpawn = source.staticSpawns.find((spawn) => spawn.id === boss.id);
if (!sourceBossSpawn || source.entities[sourceBossSpawn.entityId]?.kind !== "boss") {
throw new Error(`${slice.sourceId} is missing source boss ${boss.name}.`);
}
const bossSpawn = { ...sourceBossSpawn, position: finale.position,
const bossSpawn = { ...sourceBossSpawn, position: finalePosition,
spawnMask: source.difficultyVariants.reduce((mask, difficulty) => mask | difficulty.spawnMask, 0) };
const mobSpawns = slice.mobSpawnIds.map((id) => {
const spawn = source.staticSpawns.find((candidate) => candidate.id === id);
if (!spawn || source.entities[spawn.entityId]?.kind === "boss" || !source.entities[spawn.entityId]) {
throw new Error(`${slice.sourceId} is missing source mob ${id}.`);
}
return spawn;
const position = slice.mobSpawnPositions?.[id];
return position ? { ...spawn, position } : spawn;
});
const bossGroup: DungeonEncounterSpawnGroupDefinition = {
id: `${slice.id}:boss`,
@@ -234,12 +273,12 @@ function createRoMSlice(source: DungeonDefinition, slice: RoMSlice, seed: string
},
],
};
const objective: DungeonBossObjective = { ...boss, position: finale.position, optional: false, eventOnly: true };
const objective: DungeonBossObjective = { ...boss, position: finalePosition, optional: false, eventOnly: true };
const entityIds = new Set([...mobSpawns, bossSpawn].map((spawn) => spawn.entityId));
const entities = Object.fromEntries(Object.entries(source.entities).filter(([id]) => entityIds.has(id)));
const areas = [
{ id: "entrance", name: slice.entranceName, center: slice.start, radius: 22 },
{ id: "boss-room", name: `${boss.name}'s Room`, center: finale.position, radius: 32 },
{ id: "boss-room", name: `${boss.name}'s Room`, center: finalePosition, radius: 32 },
];
return {
@@ -252,11 +291,13 @@ function createRoMSlice(source: DungeonDefinition, slice: RoMSlice, seed: string
presentation: { ...source.presentation, location: slice.location, theme: "Bite-Sized Dungeon",
summary: slice.summary, loadingMessage: `Entering ${slice.entranceName}...`,
unchartedAreaName: slice.entranceName,
ambientLight: { color: "#e2dccc", intensity: slice.lighting?.ambient ?? 0.9 },
hemisphereLight: { skyColor: "#ded9d0", groundColor: "#5a5350", intensity: slice.lighting?.hemisphere ?? 1.2 },
ambientLight: { color: slice.lighting?.ambientColor ?? "#e2dccc", intensity: slice.lighting?.ambient ?? 0.9 },
hemisphereLight: { skyColor: slice.lighting?.skyColor ?? "#ded9d0",
groundColor: slice.lighting?.groundColor ?? "#5a5350", intensity: slice.lighting?.hemisphere ?? 1.2 },
directionalLight: { ...source.presentation.directionalLight,
color: slice.lighting ? source.presentation.directionalLight.color : "#fff0d9",
intensity: slice.lighting?.directional ?? 1.35 } },
color: slice.lighting?.directionalColor ?? (slice.lighting ? source.presentation.directionalLight.color : "#fff0d9"),
intensity: slice.lighting?.directional ?? 1.35 },
pointLights: slice.pointLights ?? source.presentation.pointLights },
areas,
entities,
staticSpawns: mobSpawns,