80 lines
4.1 KiB
JavaScript
80 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
||
import { writeFile } from "node:fs/promises";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import {
|
||
BOSS_PRESSURE_PARTY_SIZE,
|
||
BOSS_PRESSURE_WINDOW_MS,
|
||
rankBossPartyPressure,
|
||
} from "../../src/game/bossPressure";
|
||
|
||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||
const playerLevel = Math.max(1, Math.min(80, Number(process.env.BOSS_PRESSURE_LEVEL) || 80));
|
||
const { DUNGEON_DEFINITIONS } = await import("../../src/game/dungeonRegistry");
|
||
const wowDefinitions = DUNGEON_DEFINITIONS.filter((dungeon) => dungeon.expansion !== "runewaker");
|
||
const ranking = rankBossPartyPressure(wowDefinitions, playerLevel);
|
||
const median = ranking.length ? ranking[Math.floor(ranking.length / 2)].partyDamagePerSecond : 0;
|
||
const entries = ranking.map((entry, index) => ({
|
||
rank: index + 1,
|
||
pressureVsMedian: median > 0 ? entry.partyDamagePerSecond / median : 0,
|
||
...entry,
|
||
}));
|
||
const eck = entries.find((entry) => entry.serverEntry === 29_932) ?? null;
|
||
const report = {
|
||
schemaVersion: 1,
|
||
generatedAt: new Date().toISOString(),
|
||
assumptions: {
|
||
playerLevel,
|
||
enemyLevelOffset: 2,
|
||
durationSeconds: BOSS_PRESSURE_WINDOW_MS / 1_000,
|
||
partySize: BOSS_PRESSURE_PARTY_SIZE,
|
||
partyLayout: "all five actors inside nearby-party effects",
|
||
metric: "unmitigated direct damage from the live boss attack catalog",
|
||
excludes: [
|
||
"armor and resistance",
|
||
"avoidable movement and interrupts",
|
||
"summoned creature damage",
|
||
"damage-amplifying debuffs",
|
||
"healing and absorbs",
|
||
"mutually exclusive phase uptime",
|
||
],
|
||
},
|
||
bosses: entries.length,
|
||
medianPartyDamagePerSecond: median,
|
||
eck,
|
||
entries,
|
||
};
|
||
|
||
const number = (value: number): string => Math.round(value).toLocaleString("en-US");
|
||
const decimal = (value: number): string => value.toFixed(1);
|
||
const top = entries.slice(0, 30);
|
||
const markdown = `# WoW boss party-pressure audit
|
||
|
||
Generated ${report.generatedAt}. This is a deterministic comparison at player level ${playerLevel}: ${BOSS_PRESSURE_PARTY_SIZE} grouped party members, ${BOSS_PRESSURE_WINDOW_MS / 1_000} seconds, and live boss attack definitions. It is a pressure ceiling, not a combat-log prediction: mitigation, movement, interrupts, summons, debuff amplification, and phase uptime are excluded.
|
||
|
||
| Rank | Boss | Dungeon | Party DPS | Tank DPS | Random DPS | Groupwide DPS | vs median |
|
||
| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||
${top.map((entry) => `| ${entry.rank} | ${entry.boss} | ${entry.dungeonTitle} | ${number(entry.partyDamagePerSecond)} | ${number(entry.tankDamage / entry.windowSeconds)} | ${number(entry.randomTargetDamage / entry.windowSeconds)} | ${number(entry.groupwideDamage / entry.windowSeconds)} | ${decimal(entry.pressureVsMedian)}x |`).join("\n")}
|
||
|
||
## Eck the Ferocious
|
||
|
||
${eck ? `Eck ranks **${eck.rank} of ${entries.length}** at **${number(eck.partyDamagePerSecond)} raw party DPS** (**${decimal(eck.pressureVsMedian)}x** the median). His corrected schedule contains one 2-second melee swing, Bite every 8–12 seconds, Spit every 8–12 seconds after its delayed opener, Spring every 10–24 seconds against a random party member, and a non-damaging Berserk at 90 seconds.` : "Eck was not found in the runtime catalog."}
|
||
|
||
The complete machine-readable ranking and per-attack breakdown are in \`dungeon-pipeline/WOW_BOSS_PRESSURE_REPORT.json\`.
|
||
`;
|
||
|
||
const jsonPath = path.join(projectRoot, "dungeon-pipeline", "WOW_BOSS_PRESSURE_REPORT.json");
|
||
const markdownPath = path.join(projectRoot, "dungeon-pipeline", "WOW_BOSS_PRESSURE_REPORT.md");
|
||
await Promise.all([
|
||
writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"),
|
||
writeFile(markdownPath, markdown, "utf8"),
|
||
]);
|
||
console.log(JSON.stringify({
|
||
status: "complete",
|
||
bosses: entries.length,
|
||
playerLevel,
|
||
medianPartyDamagePerSecond: Number(median.toFixed(2)),
|
||
eck: eck ? { rank: eck.rank, partyDamagePerSecond: Number(eck.partyDamagePerSecond.toFixed(2)), pressureVsMedian: Number(eck.pressureVsMedian.toFixed(2)) } : null,
|
||
outputs: [path.relative(projectRoot, jsonPath), path.relative(projectRoot, markdownPath)],
|
||
}, null, 2));
|