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

+76 -1
View File
@@ -5,7 +5,7 @@ import os from "node:os";
import path from "node:path";
import { openGameDatabase } from "./database.mjs";
import { createDungeonRaceService } from "./dungeon-race-service.mjs";
import { botOpponent, botProgress, botTierForRecord, hashRaceSeed } from "./dungeon-race-rules.mjs";
import { botOpponent, botProgress, botTierForRecord, hashRaceSeed, raceAffixPace, rollRaceAffixes } from "./dungeon-race-rules.mjs";
import { createGameServer } from "./server.mjs";
import { createDungeonRaceRepository } from "./dungeon-races.mjs";
import { DatabaseSync } from "node:sqlite";
@@ -20,6 +20,19 @@ const bossIdsFor = (match) => {
? `${match.seed}:boss` : match.seed;
return [ids[hashRaceSeed(seed) % ids.length]];
};
const combatLogFor = (match, completed) => ({
version: 1, runId: match.seed, dungeonId: match.dungeonId, durationMs: 1_000, completed,
events: [{ atMs: 100, kind: "damage", source: "player", target: "mob-1",
ability: "test-aoe", raw: 100, effective: 90, overheal: 0, absorbed: 10, critical: false }],
droppedEventCount: 0,
kills: [{ id: "mob-1", name: "Mob One", boss: false, maxHealth: 90,
engagedAtMs: 100, firstDamageAtMs: 100, diedAtMs: 500, durationMs: 400 }],
packs: [{ startedAtMs: 100, endedAtMs: 500, durationMs: 400, mobIds: ["mob-1"],
partyDamage: [{ actorId: "player", damage: 90 }] }],
droppedKillCount: 0,
party: [{ actorId: "player", label: "Player", classId: "priest", level: 60, role: "healer" }],
partyDamage: [{ actorId: "player", damage: 90 }],
});
test("NPC tiers follow rolling records with seeded adjacent variation and milestones", () => {
for (const seed of ["race-tier-a", "race-tier-b", "race-tier-c", "race-tier-d"]) {
@@ -34,6 +47,17 @@ test("NPC tiers follow rolling records with seeded adjacent variation and milest
}
});
test("a shared seed rolls one stable pair and scales an NPC's entire progress model", () => {
const seed = "race-affix-parity";
const pair = rollRaceAffixes(seed);
assert.deepEqual(pair, rollRaceAffixes(seed));
assert.equal(pair.length, 2);
const plain = botOpponent(seed, "regular", "wow");
const tuned = botOpponent(seed, "regular", "wow", pair);
assert.equal(tuned.finishMs, Math.round(plain.finishMs * raceAffixPace(pair)));
assert.equal(tuned.milestones.at(-1).atMs, tuned.finishMs);
});
test("migrates existing race matches onto their seeded boss boards", () => {
const database = new DatabaseSync(":memory:");
try {
@@ -58,6 +82,7 @@ test("migrates existing race matches onto their seeded boss boards", () => {
const seed = `race-old-${dungeonId}`;
assert.equal(database.prepare("SELECT boss_id FROM dungeon_race_matches WHERE id = ?").get(seed).boss_id,
bossIdsFor({ dungeonId, difficultyId: dungeon.defaultDifficultyId, seed })[0]);
assert.equal(database.prepare("SELECT affix_ids_json FROM dungeon_race_matches WHERE id = ?").get(seed).affix_ids_json, "[]");
}
} finally { database.close(); }
});
@@ -87,6 +112,8 @@ test("random and specific tickets pair across WoW and RoM, then a later losing c
dungeonId: dungeon.id });
assert.equal(paired.status, "preparing");
assert.equal(service.state(wow).seed, paired.seed);
assert.deepEqual(service.state(wow).affixIds, paired.affixIds);
assert.deepEqual(paired.affixIds, rollRaceAffixes(paired.seed));
assert.equal(paired.opponent.source, "wow");
service.ready(wow, paired.matchId);
assert.equal(service.ready(rom, paired.matchId).status, "countdown");
@@ -104,6 +131,8 @@ test("random and specific tickets pair across WoW and RoM, then a later losing c
assert.equal(database.listDungeonRaceLeaderboard({ view: "wins", period: "30d" }, time()).entries[0].username, "race_wow");
assert.equal(database.listDungeonRaceLeaderboard({ view: "times", period: "all", dungeonId: dungeon.id,
bossId: bossIds[0] }, time()).total, 2);
assert.deepEqual(database.listDungeonRaceLeaderboard({ view: "times", period: "all", dungeonId: dungeon.id,
bossId: bossIds[0] }, time()).entries[0].affixIds, paired.affixIds);
const otherBossId = dungeon.difficulties[0].finalBossIds.find((id) => id !== bossIds[0]);
assert.equal(database.listDungeonRaceLeaderboard({ view: "times", period: "all", dungeonId: dungeon.id,
bossId: otherBossId }, time()).total, 0);
@@ -112,6 +141,34 @@ test("random and specific tickets pair across WoW and RoM, then a later losing c
assert.equal(service.finish(rom, paired.matchId, { runId: paired.seed, bossIds }).clearDurationMs, late.clearDurationMs);
}));
test("records a partial race log, upgrades it on clear, and scopes review to its participant", async () => fixture(async ({ database, service, player, advance, time }) => {
const owner = await player("race_log_owner");
const stranger = await player("race_log_stranger");
service.queue(owner, { characterId: "race_log_owner-character", role: "healer", pool: "authored",
dungeonId: dungeon.id });
advance(5_000);
const match = service.state(owner);
service.ready(owner, match.matchId);
advance(3_000);
const partial = combatLogFor(match, false);
assert.deepEqual(database.savePartialDungeonRaceCombatLog(match.matchId, owner.id, partial, time()),
{ ok: true, matchId: match.matchId, completed: false });
assert.equal(database.getDungeonRaceCombatLog(owner, match.matchId).completed, false);
assert.equal(database.getDungeonRaceCombatLog(owner, match.matchId).log.packs[0].durationMs, 400);
assert.throws(() => database.getDungeonRaceCombatLog(stranger, match.matchId),
{ code: "race_combat_log_not_found" });
assert.throws(() => database.listDungeonRaceCombatLogs(owner), { code: "gm_required" });
assert.throws(() => database.savePartialDungeonRaceCombatLog(match.matchId, stranger.id,
partial, time()), { code: "race_not_found" });
assert.throws(() => service.finish(owner, match.matchId, { runId: match.seed,
bossIds: bossIdsFor(match), combatLog: { ...combatLogFor(match, true), events: [{}] } }),
{ code: "invalid_race_combat_log" });
service.finish(owner, match.matchId, { runId: match.seed, bossIds: bossIdsFor(match),
combatLog: combatLogFor(match, true) });
assert.equal(database.getDungeonRaceCombatLog(owner, match.matchId).completed, true);
assert.equal(database.getDungeonRaceCombatLog(owner, match.matchId).log.events[0].ability, "test-aoe");
}));
test("both randomized creature pools validate and rank their cross-dungeon final boss", async () => fixture(async ({ database, service, player, advance, time }) => {
for (const [index, dungeonId] of ["randomized-bite-sized-stormwind-stockade",
"randomized-bite-sized-stormwind-stockade-available"].entries()) {
@@ -224,6 +281,24 @@ test("production HTTP routes expose ranked queue and public boards without shelv
assert.equal((await fetch(`${base}/api/dungeon-races/state`, { headers })).status, 200);
assert.equal((await fetch(`${base}/api/dungeon-races/leaderboard?period=30d`)).status, 200);
assert.equal((await fetch(`${base}/api/dungeon-races/queue`, { method: "DELETE", headers })).status, 200);
const matchId = "race-http-combat-log";
runtime.database.createDungeonRaceMatch({ id: matchId, dungeonId: dungeon.id, seed: matchId,
difficultyId: dungeon.defaultDifficultyId, createdAt: Date.now() - 2_000,
participants: [{ side: 0, accountId: auth.account.id, characterId: "http-character",
username: auth.account.username, source: "wow", role: "healer" },
{ side: 1, accountId: null, characterId: null, username: "Bot", source: "wow", role: null }],
bot: null });
runtime.database.startDungeonRace(matchId, Date.now() - 1_000);
const logResponse = await fetch(`${base}/api/dungeon-races/${matchId}/combat-log`, {
method: "POST", headers, body: JSON.stringify({ combatLog: combatLogFor({
seed: matchId, dungeonId: dungeon.id }, false) }),
});
assert.equal(logResponse.status, 200);
const review = await fetch(`${base}/api/dungeon-races/${matchId}/combat-log`, { headers });
assert.equal(review.status, 200);
assert.equal((await review.json()).log.kills[0].durationMs, 400);
assert.equal((await fetch(`${base}/api/dungeon-races/${matchId}/combat-log`)).status, 401);
assert.equal((await fetch(`${base}/api/gm/dungeon-races/combat-logs`, { headers })).status, 403);
} finally {
await new Promise((resolve) => runtime.server.close(resolve));
rmSync(dataDir, { recursive: true, force: true });