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

+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 } : {}) };
}