forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
ab1434a3b8
commit
e284f4fc76
60 files changed
+2704
-118
No files matched your search
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user