Update Healer Man server

This commit is contained in:
phenom committed 2026-10-02 10:38:12 -04:00
1 parent 4c768fbec6
commit b16821e92e
330 files changed
+35406 -1760

No files matched your search

+17 -13
View File
@@ -254,36 +254,40 @@ function rankNumber(spell) {
return Number(spell.rankText.match(/^Rank\s+(\d+)$/i)?.[1] ?? 0);
}
function resolveDescription(spell) {
const effect = (oneBasedIndex) => spell.effects.find(
(candidate) => candidate.index === Math.max(0, Number(oneBasedIndex || 1) - 1),
);
function resolveDescription(spell, spells) {
return (spell.description || "No client description is available.")
.replace(/\$(\d+)?[sSmM](\d+)?/g, (_token, _spellId, effectIndex) => {
const value = effect(effectIndex);
.replace(/\$(\d+)?[sSmM](\d+)?/g, (_token, referencedSpellId, effectIndex) => {
const referencedSpell = referencedSpellId ? spells.get(Number(referencedSpellId)) : undefined;
const value = (referencedSpell ?? spell).effects.find(
(candidate) => candidate.index === Math.max(0, Number(effectIndex || 1) - 1),
);
if (!value) return "the listed amount";
const maximum = Math.abs(value.basePoints) + Math.max(0, Math.abs(value.dieSides) - 1);
return String(Math.round(maximum * 100) / 100);
})
.replace(/\$(\d+)?t(\d+)?/g, (_token, _spellId, effectIndex) => {
const period = effect(effectIndex)?.periodMs ?? 0;
.replace(/\$(\d+)?t(\d+)?/g, (_token, referencedSpellId, effectIndex) => {
const referencedSpell = referencedSpellId ? spells.get(Number(referencedSpellId)) : undefined;
const period = (referencedSpell ?? spell).effects.find(
(candidate) => candidate.index === Math.max(0, Number(effectIndex || 1) - 1),
)?.periodMs ?? 0;
return period > 0 ? String(period / 1000) : "the listed interval";
})
.replace(/\$(\d+)?d\b/g, () => {
const milliseconds = Math.abs(spell.durationMs);
.replace(/\$(\d+)?d\b/g, (_token, referencedSpellId) => {
const referencedSpell = referencedSpellId ? spells.get(Number(referencedSpellId)) : undefined;
const milliseconds = Math.abs((referencedSpell ?? spell).durationMs);
return milliseconds > 0 ? `${milliseconds / 1000} sec` : "the listed duration";
})
.replace(/\$[a-zA-Z0-9:;.*+/{}-]+/g, "the listed amount");
}
function slimRank(spell) {
function slimRank(spell, spells) {
return {
spellId: spell.id,
rank: rankNumber(spell),
level: spell.spellLevel,
sourceLevel: spell.baseLevel || spell.spellLevel,
maxScalingLevel: spell.maxLevel,
description: resolveDescription(spell),
description: resolveDescription(spell, spells),
castTimeMs: spell.castTimeMs,
cooldownMs: Math.max(spell.recoveryTimeMs, spell.categoryRecoveryTimeMs),
// Zero is the DBC signal for abilities that do not start a global cooldown.
@@ -391,7 +395,7 @@ try {
sourceUnlockLevel: Math.max(1, Math.min(...ranks.map((spell) => spell.spellLevel))),
...(passive ? { passive: true } : {}),
...(talentNodeId ? { talentNodeId } : {}),
ranks: ranks.map(slimRank),
ranks: ranks.map((spell) => slimRank(spell, spells)),
};
}).sort((left, right) => (
left.sourceUnlockLevel - right.sourceUnlockLevel
+3 -1
View File
@@ -419,7 +419,9 @@ if (talentDbcPath) {
const lastSpell = spells.get(rankSpellIds.at(-1));
const rankSpells = rankSpellIds.map((spellId) => spells.get(spellId)).filter(Boolean);
const rawRankDescriptions = rankSpells.map((spell) => cleanDescription(spell.description || spell.auraDescription));
const rankDescriptions = rankSpells.map((spell) => resolveDescription(spell.description || spell.auraDescription, spell));
const rankDescriptions = rankSpells.map((spell, rankIndex) => dbcTalentId === 206
? `Your Kick has a ${rankIndex === 0 ? 50 : 100}% chance to silence the target for 2 sec. Capstone Bonus: Successful interrupts with Kick restore 40 Energy and increase your attack speed by 35% for 6 sec.`
: resolveDescription(spell.description || spell.auraDescription, spell));
exactNodes.push({
id: `talent-${dbcTalentId}`,
dbcTalentId,
+102
View File
@@ -0,0 +1,102 @@
const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const output = path.resolve('artifacts/playtest/graphics-options');
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-graphics-'));
const staticDir = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-graphics-dist-'));
fs.mkdirSync(output, { recursive: true });
fs.cpSync(path.resolve('dist'), staticDir, { recursive: true });
async function enterDungeon(page, category, name) {
await page.getByRole('button', { name: /^Dungeons/ }).click();
if (category) await page.locator('.dungeon-category-tab').filter({ hasText: category }).click();
await page.locator('.dungeon-select-row').filter({ hasText: name }).first().click();
const download = page.getByRole('button', { name: 'Download and continue' });
if (await download.isVisible()) await download.click();
else await page.getByRole('button', { name: 'Choose role' }).click();
await page.locator('.dungeon-role-option--healer').click();
await page.locator('.scene-shell').waitFor();
await page.locator('.loading-screen').waitFor({ state: 'visible', timeout: 180_000 });
await page.locator('.loading-screen').waitFor({ state: 'hidden', timeout: 180_000 });
await page.locator('.dungeon-boss-intro').waitFor({ state: 'hidden', timeout: 180_000 });
await page.waitForTimeout(1000);
}
async function canvasScale(page) {
return page.locator('.scene-shell canvas').first().evaluate((canvas) =>
Number((canvas.width / canvas.clientWidth).toFixed(2)));
}
(async () => {
const { createGameServer } = await import('../server/server.mjs');
const runtime = createGameServer({ dataDir, staticDir, enableShelvedModes: false });
await new Promise((resolve) => runtime.server.listen(4198, '127.0.0.1', resolve));
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
page.setDefaultTimeout(180_000);
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
try {
await page.goto('http://127.0.0.1:4198', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: /Play offline/ }).click();
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByRole('button', { name: /Customize & name/ }).click();
await page.getByLabel('Character name').fill('GraphicsQA');
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByRole('button', { name: /Continue|Enter world/ }).first().evaluate((button) => button.click());
await page.locator('.main-menu-screen').waitFor();
await enterDungeon(page, 'WoW', 'Wailing Caverns');
const defaultScale = await canvasScale(page);
await page.screenshot({ path: path.join(output, 'wow-default.png') });
await page.keyboard.press('Escape');
await page.getByRole('dialog', { name: 'Paused' }).waitFor();
await page.getByRole('button', { name: 'Options', exact: true }).click();
await page.getByRole('dialog', { name: 'Options' }).waitFor();
const setting = (name) => page.locator('.options-panel .option-row').filter({ has: page.getByText(name, { exact: true }) }).getByRole('button').first();
for (let index = 0; index < 3; index++) await setting('Render resolution').click();
for (let index = 0; index < 2; index++) await setting('Texture filtering').click();
await setting('Shadows').click();
await setting('Ambient fill').click();
assert.equal(await setting('Render resolution').innerText(), '200%');
assert.equal(await setting('Texture filtering').innerText(), '16×');
assert.equal(await setting('Shadows').innerText(), 'High');
await page.screenshot({ path: path.join(output, 'graphics-options.png') });
await page.setViewportSize({ width: 960, height: 540 });
await page.screenshot({ path: path.join(output, 'graphics-options-thor.png') });
assert.ok(await setting('Ambient fill').isVisible());
await page.setViewportSize({ width: 1440, height: 900 });
await page.getByRole('button', { name: 'Done', exact: true }).click();
await page.getByRole('button', { name: 'Resume game' }).click();
await page.waitForTimeout(1500);
const highScale = await canvasScale(page);
assert.ok(highScale >= 1.9, `Expected 200% render scale, got ${highScale}`);
await page.screenshot({ path: path.join(output, 'wow-high.png') });
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Main menu', exact: true }).click();
await page.locator('.main-menu-screen').waitFor();
await enterDungeon(page, 'RoM', 'Forsaken Abbey');
assert.ok(await canvasScale(page) >= 1.9);
await page.screenshot({ path: path.join(output, 'rom-high.png') });
assert.deepEqual(errors, []);
const results = { defaultScale, highScale, romScale: await canvasScale(page), errors };
fs.writeFileSync(path.join(output, 'results.json'), `${JSON.stringify(results, null, 2)}\n`);
console.log(JSON.stringify(results));
} catch (error) {
await page.screenshot({ path: path.join(output, 'failure.png') });
console.error((await page.locator('body').innerText()).slice(-1500));
console.error(errors);
throw error;
} finally {
await browser.close();
await new Promise((resolve) => runtime.server.close(resolve));
assert.ok(dataDir.startsWith(path.resolve(os.tmpdir()) + path.sep));
assert.ok(staticDir.startsWith(path.resolve(os.tmpdir()) + path.sep));
fs.rmSync(dataDir, { recursive: true, force: true });
fs.rmSync(staticDir, { recursive: true, force: true });
}
})().catch((error) => { console.error(error); process.exitCode = 1; });
@@ -34,6 +34,14 @@ const CLASS_IDS = ["rom-warrior", "rom-scout", "rom-rogue", "rom-mage", "rom-pri
const CLASS_NAMES = ["Warrior", "Scout", "Rogue", "Mage", "Priest", "Knight", "Warden", "Druid", "Warlock", "Champion"];
const RESOURCES = ["rage", "focus", "energy", "mana", "mana", "mana", "mana", "mana", "focus", "rage"];
const ELITE_LEVELS = [15, 20, 25, 30, 35, 40, 45, 50, 60, 70];
const DRUID_WARRIOR_BRIAR_CHILD = {
mainClass: 8,
secondaryClass: 1,
level: 60,
keyItemId: 546833,
passiveSourceSkillId: 499955,
childSourceSkillId: 499956,
};
const RACE_MATRIX = {
"rom-human": [1, 2, 3, 4, 5, 6],
"rom-elf": [1, 2, 3, 4, 7, 8],
@@ -554,6 +562,41 @@ for (const key of [...eligiblePairs].sort()) {
icon: `/assets/ui/rom-spells/${spell.source.imageId}.png`,
iconResolution: "native-interface-fdb",
});
// RuneWaker unlocks Cross of Thorns Attacks as an additional class-8
// skill behind Briar Technique. It shares the passive's key item, so the
// usual one-candidate-per-elite-milestone lookup above would omit it.
if (mapping.mainClass === DRUID_WARRIOR_BRIAR_CHILD.mainClass
&& mapping.secondaryClass === DRUID_WARRIOR_BRIAR_CHILD.secondaryClass
&& milestone.level === DRUID_WARRIOR_BRIAR_CHILD.level
&& milestone.keyItemId === DRUID_WARRIOR_BRIAR_CHILD.keyItemId
&& candidate.sourceSkillId === DRUID_WARRIOR_BRIAR_CHILD.passiveSourceSkillId) {
const child = candidates.find((entry) => entry.classIndex === DRUID_WARRIOR_BRIAR_CHILD.mainClass
&& entry.sourceSkillId === DRUID_WARRIOR_BRIAR_CHILD.childSourceSkillId
&& entry.learnLevel === milestone.level
&& entry.keyItemId === milestone.keyItemId
&& entry.prerequisites.includes(DRUID_WARRIOR_BRIAR_CHILD.passiveSourceSkillId));
if (!child) throw new Error("Druid/Warrior Briar Technique child 499956 lost its source unlock row.");
const childSpell = sourceSpell(child.sourceSkillId, mapping.mainClass);
if (!childSpell) throw new Error("Druid/Warrior Briar Technique child 499956 has no MagicCollect row.");
skills.push({
id: `${CLASS_IDS[mapping.mainClass - 1]}-${CLASS_IDS[mapping.secondaryClass - 1]}-elite-${milestone.level}-${child.sourceSkillId}`,
classId: CLASS_IDS[mapping.mainClass - 1],
secondaryClassId: CLASS_IDS[mapping.secondaryClass - 1],
group: childSpell.passive ? "passive" : "elite",
unlockLevel: child.learnLevel,
keyItemId: child.keyItemId,
...childSpell,
ranks: [{
rank: 1,
level: child.learnLevel,
prerequisites: child.prerequisites,
...nestedRankSpell(childSpell, childSpell.sourceSkillId),
}],
icon: `/assets/ui/rom-spells/${childSpell.source.imageId}.png`,
iconResolution: "native-interface-fdb",
});
}
}
}
@@ -561,6 +604,8 @@ const eliteSkills = skills.filter((skill) => skill.secondaryClassId);
const ordinarySkills = skills.filter((skill) => !skill.secondaryClassId);
const unresolvedVisible = skills.filter((skill) => !skill.passive && !skill.effect?.kind);
const iconCoverage = await extractNativeIcons(skills);
const eliteMilestoneCount = [...eliteMappings.values()].reduce((sum, entry) => sum + entry.milestones.length, 0);
const supplementalEliteCount = eliteSkills.length - eliteMilestoneCount;
const sourceHashes = {
...Object.fromEntries(Object.entries(buffers).map(([key, value]) => [key, sha256(value)])),
interfaceFdb: await sha256File(interfaceFdb),
@@ -583,7 +628,7 @@ const audit = {
duelistExcluded: !CLASS_NAMES.includes("Duelist"),
races: Object.keys(RACE_MATRIX).length,
orderedPairs: eligiblePairs.size,
eliteMilestones: eliteSkills.length,
eliteMilestones: eliteMilestoneCount,
ordinaryFamilies: ordinarySkills.length,
sourceRankRows: ordinarySkills.reduce((sum, skill) => sum + skill.ranks.length, 0),
iconResolved: skills.filter((skill) => skill.iconResolution === "native-interface-fdb").length,
@@ -594,7 +639,12 @@ const audit = {
},
adaptations,
};
if (eliteSkills.length !== 660 || unresolvedVisible.length !== 0 || audit.coverage.iconResolved !== skills.length || iconCoverage.requested !== iconCoverage.resolved) throw new Error(`Coverage failed: ${JSON.stringify(audit.coverage)}`);
// In addition to the 660 ordinary milestones, this source emits the Druid/Warrior
// Cross of Thorns Attacks row behind its explicit learned prerequisite.
if (eliteMilestoneCount !== 660 || supplementalEliteCount !== 1 || unresolvedVisible.length !== 0
|| audit.coverage.iconResolved !== skills.length || iconCoverage.requested !== iconCoverage.resolved) {
throw new Error(`Coverage failed: ${JSON.stringify(audit.coverage)}`);
}
await mkdir(path.dirname(outputFile), { recursive: true });
await mkdir(path.dirname(auditFile), { recursive: true });
await Promise.all([
@@ -25,10 +25,10 @@
"eliteMilestones": 660,
"ordinaryFamilies": 269,
"sourceRankRows": 274,
"iconResolved": 929,
"iconResolved": 930,
"uniqueNativeIcons": 918,
"interfaceFdbEntries": 34954,
"executableOrNonCombat": 929,
"executableOrNonCombat": 930,
"unresolvedVisible": 0
},
"adaptations": [