Update Healer Man server

This commit is contained in:
phenom committed 2026-09-27 21:48:00 -04:00
1 parent 15a5254380
commit 9d7e0e2f9e
71 files changed
+3490 -81

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"dungeons":[{"id":"bite-sized-stormwind-stockade","sourceDungeonId":"stormwind-stockade","title":"Stormwind Stockade","expansion":"classic","bounds":{"min":[-237.771,-55.1896,-181.922],"max":[-9.23,1.66,192.931]},"difficultyIds":["normal"],"legacyBlockers":[{"position":[-129.17,-22.1,16.4],"yaw":0,"halfExtents":[6,5.2,0.65]},{"position":[-129.17,-22.1,-16.4],"yaw":0,"halfExtents":[6,5.2,0.65]}],"bossCandidates":[{"sourceDungeonId":"stormwind-stockade","entityId":"entry-1696","objectiveId":"creature-84027","name":"Targorr the Dread"},{"sourceDungeonId":"stormwind-stockade","entityId":"entry-1666","objectiveId":"creature-79088","name":"Kam Deepfury"},{"sourceDungeonId":"stormwind-stockade","entityId":"entry-1717","objectiveId":"creature-79098","name":"Hamhock"},{"sourceDungeonId":"stormwind-stockade","entityId":"entry-1716","objectiveId":"creature-79124","name":"Bazil Thredd"},{"sourceDungeonId":"stormwind-stockade","entityId":"entry-1663","objectiveId":"creature-79116","name":"Dextren Ward"}]},{"id":"bite-sized-wailing-caverns","sourceDungeonId":"wailing-caverns","title":"Wailing Caverns","expansion":"classic","bounds":{"min":[-191.6667,-119.1605,-370.8333],"max":[483.4794,56.469,554.3612]},"difficultyIds":["normal"],"legacyBlockers":[],"bossCandidates":[{"sourceDungeonId":"wailing-caverns","entityId":"lady-anacondra","objectiveId":"lady-anacondra-spawn","name":"Lady Anacondra"},{"sourceDungeonId":"wailing-caverns","entityId":"kresh","objectiveId":"kresh-patrol:kresh-roaming","name":"Kresh"},{"sourceDungeonId":"wailing-caverns","entityId":"lord-cobrahn","objectiveId":"lord-cobrahn-spawn","name":"Lord Cobrahn"},{"sourceDungeonId":"wailing-caverns","entityId":"lord-pythas","objectiveId":"lord-pythas-spawn","name":"Lord Pythas"},{"sourceDungeonId":"wailing-caverns","entityId":"skum","objectiveId":"skum-spawn","name":"Skum"},{"sourceDungeonId":"wailing-caverns","entityId":"lord-serpentis","objectiveId":"lord-serpentis-spawn","name":"Lord Serpentis"},{"sourceDungeonId":"wailing-caverns","entityId":"verdan-the-everliving","objectiveId":"verdan-spawn","name":"Verdan the Everliving"},{"sourceDungeonId":"wailing-caverns","entityId":"mutanus-the-devourer","objectiveId":"mutanus-spawn","name":"Mutanus the Devourer"}]},{"id":"bite-sized-deadmines-ship","sourceDungeonId":"deadmines","title":"The Deadmines","expansion":"classic","bounds":{"min":[-964.0937,-17.4997,-358.1248],"max":[-304.6317,81.9028,45.0756]},"difficultyIds":["normal"],"legacyBlockers":[],"bossCandidates":[{"sourceDungeonId":"deadmines","entityId":"entry-644","objectiveId":"creature-79168","name":"Rhahk'zor"},{"sourceDungeonId":"deadmines","entityId":"entry-642","objectiveId":"creature-79223","name":"Sneed"},{"sourceDungeonId":"deadmines","entityId":"entry-1763","objectiveId":"creature-79206","name":"Gilnid"},{"sourceDungeonId":"deadmines","entityId":"entry-646","objectiveId":"creature-79337","name":"Mr. Smite"},{"sourceDungeonId":"deadmines","entityId":"entry-645","objectiveId":"creature-79344","name":"Cookie"},{"sourceDungeonId":"deadmines","entityId":"entry-647","objectiveId":"creature-79333","name":"Captain Greenskin"},{"sourceDungeonId":"deadmines","entityId":"entry-639","objectiveId":"creature-79336","name":"Edwin VanCleef"}]},{"id":"bite-sized-utgarde-pinnacle-skadi","sourceDungeonId":"utgarde-pinnacle","title":"Utgarde Pinnacle","expansion":"wrath","bounds":{"min":[-552.325,55.0607,179.479],"max":[-190.62,139.777,629.117]},"difficultyIds":["normal","heroic"],"legacyBlockers":[],"bossCandidates":[{"sourceDungeonId":"utgarde-pinnacle","entityId":"entry-26668","objectiveId":"encounter-577","name":"Svala Sorrowgrave"},{"sourceDungeonId":"utgarde-pinnacle","entityId":"entry-26687","objectiveId":"creature-126102","name":"Gortok Palehoof"},{"sourceDungeonId":"utgarde-pinnacle","entityId":"entry-26693","objectiveId":"creature-126103","name":"Skadi the Ruthless"},{"sourceDungeonId":"utgarde-pinnacle","entityId":"entry-26861","objectiveId":"creature-126255","name":"King Ymiron"}]},{"id":"bite-sized-forsaken-abbey-ancalon","sourceDungeonId":"forsaken-abbey","title":"Forsaken Abbey","expansion":"runewaker","bounds":{"min":[-29.38274,-21.74707,-110.44788],"max":[416.5376,34.07087,85.36886]},"difficultyIds":["normal"],"legacyBlockers":[],"bossCandidates":[{"sourceDungeonId":"forsaken-abbey","entityId":"rw-fa-100070","objectiveId":"rw-fa-spawn-7384","name":"Necromage Keeper 1"},{"sourceDungeonId":"forsaken-abbey","entityId":"rw-fa-100070","objectiveId":"rw-fa-spawn-7426","name":"Necromage Keeper 2"},{"sourceDungeonId":"forsaken-abbey","entityId":"rw-fa-100070","objectiveId":"rw-fa-spawn-7379","name":"Necromage Keeper 3"},{"sourceDungeonId":"forsaken-abbey","entityId":"rw-fa-100070","objectiveId":"rw-fa-spawn-7425","name":"Necromage Keeper 4"},{"sourceDungeonId":"forsaken-abbey","entityId":"rw-fa-100561","objectiveId":"rw-fa-spawn-7472","name":"Flowing Chaos"},{"sourceDungeonId":"forsaken-abbey","entityId":"rw-fa-100562","objectiveId":"rw-fa-spawn-7755","name":Line truncated
File diff suppressed because it is too large. Load diff
+33
View File
@@ -0,0 +1,33 @@
export type LayoutVector = readonly [number, number, number];
export interface LayoutSpawn { readonly footPosition: LayoutVector; readonly yaw: number }
export interface BiteSizedLayoutBlocker { readonly position: LayoutVector; readonly yaw: number; readonly halfExtents: LayoutVector }
export interface BiteSizedMobPlacement { readonly id: string; readonly sourceDungeonId: string; readonly entityId: string; readonly position: LayoutVector; readonly yaw: number; readonly spawnMask?: number; readonly spawnChance?: number }
export interface BiteSizedSpot {
readonly id: string; readonly dungeonId: string; readonly sourceDungeonId: string; readonly name: string; readonly revision: number;
readonly legacyLayout: boolean; readonly partySpawn: LayoutSpawn; readonly mobs: readonly BiteSizedMobPlacement[];
readonly bossSpawn: LayoutSpawn; readonly bossActivation: "immediate" | "after-mobs";
}
export interface BiteSizedConfig { readonly schemaVersion: 1; readonly spots: readonly BiteSizedSpot[] }
export interface BiteSizedCreatureRef { readonly sourceDungeonId: string; readonly entityId: string }
export interface BiteSizedBossRef extends BiteSizedCreatureRef { readonly objectiveId: string; readonly name: string }
export interface BiteSizedRunSelection {
readonly legacyRandomized?: true;
readonly schemaVersion: 1; readonly dungeonId: string; readonly spot: BiteSizedSpot; readonly boss: BiteSizedBossRef;
readonly mobs: readonly (BiteSizedCreatureRef & { readonly placementId: string })[];
}
export interface PublishedBiteSizedConfig { readonly revision: number; readonly updatedAt: number | null; readonly config: BiteSizedConfig }
export const BITE_SIZED_DEFAULT_CONFIG: BiteSizedConfig;
export const BITE_SIZED_LAYOUT_CATALOG: {
readonly dungeons: readonly { readonly id: string; readonly sourceDungeonId: string; readonly title: string; readonly expansion: string;
readonly bounds: { readonly min: LayoutVector; readonly max: LayoutVector }; readonly difficultyIds: readonly string[]; readonly bossCandidates: readonly BiteSizedBossRef[]; readonly legacyBlockers: readonly BiteSizedLayoutBlocker[] }[];
readonly creatures: readonly (BiteSizedCreatureRef & { readonly kind: "mob" | "boss"; readonly name: string; readonly dungeonTitle: string; readonly modelUrl: string | null })[];
readonly pools: Readonly<Record<string, readonly (BiteSizedBossRef & { readonly kind: "mob" | "boss"; readonly key: string })[]>>;
};
export function normalizeBiteSizedSpot(value: unknown): BiteSizedSpot;
export function normalizeBiteSizedConfig(value: unknown): BiteSizedConfig;
export function normalizeBiteSizedSelection(value: unknown): BiteSizedRunSelection;
export function biteSizedGameplayKey(value: BiteSizedSpot): string;
export function biteSizedSeedHash(seed: string): number;
export function isLayoutBiteSizedDungeon(id: string): boolean;
export function defaultBiteSizedBoardScope(id: string): { readonly spotId: string | null; readonly spotRevision: number | null };
export function selectBiteSizedRun(config: BiteSizedConfig, dungeonId: string, seed: string, options?: { readonly eligibleDungeonIds?: readonly string[]; readonly spotId?: string }): BiteSizedRunSelection;
+124
View File
@@ -0,0 +1,124 @@
import bundled from "./bite-sized-defaults.json" with { type: "json" };
import catalog from "./bite-sized-catalog.json" with { type: "json" };
export const BITE_SIZED_DEFAULT_CONFIG = bundled;
export const BITE_SIZED_LAYOUT_CATALOG = catalog;
const dungeonBySource = new Map(catalog.dungeons.map((d) => [d.sourceDungeonId, d]));
const creatureByRef = new Map(catalog.creatures.map((c) => [`${c.sourceDungeonId}:${c.entityId}`, c]));
const fail = (message) => { throw new Error(message); };
const identifier = (value) => typeof value === "string" && /^[A-Za-z0-9_:.-]{1,100}$/.test(value);
const object = (value) => value && typeof value === "object" && !Array.isArray(value);
function vector(value, label) {
if (!Array.isArray(value) || value.length !== 3 || !value.every((n) => typeof n === "number" && Number.isFinite(n))) fail(`${label} needs three finite coordinates.`);
return [...value];
}
function spawn(value, label) {
if (!object(value) || !Number.isFinite(value.yaw)) fail(`${label} needs a finite facing.`);
return { footPosition: vector(value.footPosition, label), yaw: value.yaw };
}
export function normalizeBiteSizedSpot(value) {
if (!object(value) || !identifier(value.id)) fail("A spot needs a valid stable ID.");
const dungeon = dungeonBySource.get(value.sourceDungeonId);
if (!dungeon || value.dungeonId !== dungeon.id) fail("Choose a supported Bite-Sized parent dungeon.");
if (typeof value.name !== "string" || !value.name.trim() || value.name.length > 100) fail("Name the spot using 1–100 characters.");
if (!Number.isSafeInteger(value.revision) || value.revision < 0) fail("Invalid spot revision.");
if (!["immediate", "after-mobs"].includes(value.bossActivation)) fail("Choose when the boss appears.");
if (!Array.isArray(value.mobs) || value.mobs.length > 500) fail("A spot supports up to 500 mob placements.");
const ids = new Set();
const mobs = value.mobs.map((m) => {
if (!object(m) || !identifier(m.id) || ids.has(m.id)) fail("Mob placement IDs must be unique.");
ids.add(m.id);
if (creatureByRef.get(`${m.sourceDungeonId}:${m.entityId}`)?.kind !== "mob") fail("Choose a known non-boss creature for each mob placement.");
if (m.yaw !== undefined && !Number.isFinite(m.yaw)) fail("Mob facing must be finite.");
if (m.spawnMask !== undefined && (!Number.isInteger(m.spawnMask) || m.spawnMask < 1 || m.spawnMask > 255)) fail("Invalid mob difficulty mask.");
if (m.spawnChance !== undefined && (!Number.isFinite(m.spawnChance) || m.spawnChance < 0 || m.spawnChance > 1)) fail("Invalid mob spawn chance.");
return { id: m.id, sourceDungeonId: m.sourceDungeonId, entityId: m.entityId, position: vector(m.position, "Mob spawn"), yaw: m.yaw ?? 0,
...(m.spawnMask !== undefined ? { spawnMask: m.spawnMask } : {}), ...(m.spawnChance !== undefined ? { spawnChance: m.spawnChance } : {}) };
});
const partySpawn = spawn(value.partySpawn, "Party spawn");
const bossSpawn = spawn(value.bossSpawn, "Boss spawn");
for (const point of [partySpawn.footPosition, bossSpawn.footPosition, ...mobs.map((m) => m.position)]) {
if (point.some((n, axis) => n < dungeon.bounds.min[axis] - 25 || n > dungeon.bounds.max[axis] + 25)) fail("Spawn points must stay inside the parent dungeon.");
}
return { id: value.id, dungeonId: dungeon.id, sourceDungeonId: dungeon.sourceDungeonId, name: value.name.trim(), revision: value.revision,
legacyLayout: value.legacyLayout === true, partySpawn, mobs, bossSpawn, bossActivation: value.bossActivation };
}
export function normalizeBiteSizedConfig(value) {
if (!object(value) || value.schemaVersion !== 1 || !Array.isArray(value.spots) || value.spots.length > 960) fail("Invalid Bite-Sized layout configuration.");
const spots = value.spots.map(normalizeBiteSizedSpot);
if (new Set(spots.map((s) => s.id)).size !== spots.length) fail("Spot IDs must be unique.");
for (const dungeon of catalog.dungeons) {
const count = spots.filter((s) => s.sourceDungeonId === dungeon.sourceDungeonId).length;
if (count < 1 || count > 80) fail(`${dungeon.title} needs between 1 and 80 spots.`);
}
return { schemaVersion: 1, spots };
}
export function biteSizedGameplayKey(value) {
const { name: _name, revision: _revision, ...gameplay } = normalizeBiteSizedSpot(value);
return JSON.stringify(gameplay);
}
export function biteSizedSeedHash(seed) {
let hash = 2166136261;
for (let i = 0; i < seed.length; i++) { hash ^= seed.charCodeAt(i); hash = Math.imul(hash, 16777619); }
return hash >>> 0;
}
export function isLayoutBiteSizedDungeon(id) {
return catalog.dungeons.some((d) => d.id === id) || Object.hasOwn(catalog.pools, id);
}
export function defaultBiteSizedBoardScope(dungeonId) {
const id = Object.hasOwn(catalog.pools, dungeonId) ? "bite-sized-stormwind-stockade" : dungeonId;
const spot = bundled.spots.find((s) => s.dungeonId === id);
return spot ? { spotId: spot.id, spotRevision: 0 } : { spotId: null, spotRevision: null };
}
export function selectBiteSizedRun(config, dungeonId, seed, options = {}) {
const randomized = Object.hasOwn(catalog.pools, dungeonId);
const allowed = options.eligibleDungeonIds ?? catalog.dungeons.map((d) => d.id);
const parents = catalog.dungeons.filter((d) => allowed.includes(d.id) && (randomized || d.id === dungeonId)).sort((a, b) => a.id.localeCompare(b.id));
if (!parents.length) fail("No eligible Bite-Sized dungeon has a published spot.");
const parent = parents[biteSizedSeedHash(`${seed}:location`) % parents.length];
const spots = config.spots.filter((s) => s.dungeonId === parent.id).sort((a, b) => a.id.localeCompare(b.id));
const spot = options.spotId ? spots.find((s) => s.id === options.spotId) : spots[biteSizedSeedHash(`${seed}:spot`) % spots.length];
if (!spot) fail("That Bite-Sized spot is unavailable.");
const bossPool = randomized ? catalog.pools[dungeonId].filter((c) => c.kind === "boss") : parent.bossCandidates;
const boss = bossPool[biteSizedSeedHash(randomized ? `${seed}:boss` : seed) % bossPool.length];
const mobPool = randomized ? catalog.pools[dungeonId].filter((c) => c.kind === "mob") : [];
let remaining = [...mobPool];
const mobs = spot.mobs.map((placement, index) => {
let ref = placement;
if (randomized) {
if (!remaining.length) remaining = [...mobPool];
const entry = /(?:entry-|^rw-.*-)(\d+)$/.exec(placement.entityId)?.[1];
const originalKey = entry ? `${placement.entityId.startsWith("rw-") ? "rom" : "wow"}:mob:${entry}` : null;
const choices = remaining.filter((c) => c.key !== originalKey);
// Preserve the existing deal without replacement. Large authored spots
// start another deterministic deal only after exhausting the pool.
const candidates = choices.length ? choices : remaining;
ref = candidates[biteSizedSeedHash(`${seed}:mob:${placement.id}:${index}`) % candidates.length];
remaining.splice(remaining.indexOf(ref), 1);
}
return { placementId: placement.id, sourceDungeonId: ref.sourceDungeonId, entityId: ref.entityId };
});
return normalizeBiteSizedSelection({ schemaVersion: 1, dungeonId, spot, boss, mobs });
}
export function normalizeBiteSizedSelection(value) {
if (!object(value) || value.schemaVersion !== 1 || !isLayoutBiteSizedDungeon(value.dungeonId)) fail("Invalid Bite-Sized run selection.");
const spot = normalizeBiteSizedSpot(value.spot);
const randomized = Object.hasOwn(catalog.pools, value.dungeonId);
if (!randomized && spot.dungeonId !== value.dungeonId) fail("The spot belongs to a different dungeon.");
const parent = dungeonBySource.get(spot.sourceDungeonId);
const pool = randomized ? catalog.pools[value.dungeonId].filter((c) => c.kind === "boss") : parent.bossCandidates;
const boss = pool.find((c) => c.sourceDungeonId === value.boss?.sourceDungeonId && c.entityId === value.boss?.entityId && c.objectiveId === value.boss?.objectiveId);
if (!boss || spot.mobs.some((m) => m.id === boss.objectiveId)) fail("Invalid selected boss or conflicting placement ID.");
if (!Array.isArray(value.mobs) || value.mobs.length !== spot.mobs.length) fail("The run is missing mob assignments.");
const mobs = spot.mobs.map((placement) => {
const entries = value.mobs.filter((m) => m.placementId === placement.id);
const ref = entries[0];
if (entries.length !== 1 || creatureByRef.get(`${ref.sourceDungeonId}:${ref.entityId}`)?.kind !== "mob") fail("Invalid selected mob.");
if (randomized ? !catalog.pools[value.dungeonId].some((c) => c.kind === "mob" && c.sourceDungeonId === ref.sourceDungeonId && c.entityId === ref.entityId)
: ref.sourceDungeonId !== placement.sourceDungeonId || ref.entityId !== placement.entityId) fail("The selected mob is outside this run's creature pool.");
return { placementId: placement.id, sourceDungeonId: ref.sourceDungeonId, entityId: ref.entityId };
});
if (value.legacyRandomized && (!randomized || spot.id !== "bite-sized-stormwind-stockade:default" || spot.revision !== 0)) fail("Invalid legacy randomized layout.");
return { schemaVersion: 1, dungeonId: value.dungeonId, spot, boss: { sourceDungeonId: boss.sourceDungeonId, entityId: boss.entityId, objectiveId: boss.objectiveId, name: boss.name }, mobs,
...(value.legacyRandomized ? { legacyRandomized: true } : {}) };
}