Update Healer Man server
This commit is contained in:
1 parent
f90ba7d989
commit
189b94d6b7
51 files changed
+3448
-154
No files matched your search
@@ -216,6 +216,8 @@ function catalogPayload(source: PartyCatalogSource, level: number) {
|
||||
id: build.id,
|
||||
source: build.source,
|
||||
classId: build.classId,
|
||||
raceId: build.raceId ?? null,
|
||||
secondaryClassId: build.secondaryClassId ?? null,
|
||||
className: classById(build.classId).name,
|
||||
role: build.role,
|
||||
specialization: build.orientation,
|
||||
@@ -229,7 +231,9 @@ function catalogPayload(source: PartyCatalogSource, level: number) {
|
||||
talentBuildSourcePointsRetained: build.talentBuildSourcePointsRetained ?? null,
|
||||
talentBuildFallbackPointCount: build.talentBuildFallbackPointCount ?? null,
|
||||
defaultTalentBuild: build.defaultTalentBuild ?? true,
|
||||
label: `${classById(build.classId).name} — ${build.orientation} (${build.role})${build.talentBuildName ? ` — ${build.talentBuildName}` : ""}`,
|
||||
label: build.secondaryClassId
|
||||
? `${classById(build.classId).name}/${classById(build.secondaryClassId).name} — ${build.characterName ?? build.orientation} (${build.role})`
|
||||
: `${classById(build.classId).name} — ${build.orientation} (${build.role})${build.talentBuildName ? ` — ${build.talentBuildName}` : ""}`,
|
||||
gear: resolvedGearSummary(build),
|
||||
})),
|
||||
bosses: bossCatalogRows(),
|
||||
@@ -268,6 +272,7 @@ function configuredCompanion(
|
||||
return createConfiguredPartyMember({
|
||||
name: build.characterName ?? `${classById(build.classId).name} ${build.orientation}`,
|
||||
classId: build.classId,
|
||||
raceId: build.raceId,
|
||||
secondaryClassId: build.secondaryClassId,
|
||||
specialization: build.orientation,
|
||||
specializationId: specialization?.id ?? null,
|
||||
@@ -362,6 +367,8 @@ function serializedMember(
|
||||
talentBuildSourcePointsRetained: metrics.build.talentBuildSourcePointsRetained ?? null,
|
||||
talentBuildFallbackPointCount: metrics.build.talentBuildFallbackPointCount ?? null,
|
||||
classId: metrics.build.classId,
|
||||
raceId: metrics.build.raceId ?? null,
|
||||
secondaryClassId: metrics.build.secondaryClassId ?? null,
|
||||
className: classById(metrics.build.classId).name,
|
||||
specialization: metrics.build.orientation,
|
||||
role: metrics.build.role,
|
||||
@@ -563,6 +570,7 @@ async function runBoss(
|
||||
const combat = useCombatStore.getState();
|
||||
combat.initializeCharacter({
|
||||
classId: activeBuild.classId,
|
||||
raceId: activeBuild.raceId,
|
||||
secondaryClassId: activeBuild.secondaryClassId,
|
||||
specializationId: specialization?.id,
|
||||
role: activeBuild.role,
|
||||
@@ -776,6 +784,8 @@ async function simulateConfig(config: BalanceLabConfig, output: string): Promise
|
||||
talentBuildSourcePointsRetained: build.talentBuildSourcePointsRetained ?? null,
|
||||
talentBuildFallbackPointCount: build.talentBuildFallbackPointCount ?? null,
|
||||
classId: build.classId,
|
||||
raceId: build.raceId ?? null,
|
||||
secondaryClassId: build.secondaryClassId ?? null,
|
||||
className: classById(build.classId).name,
|
||||
specialization: build.orientation,
|
||||
role: build.role,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
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 directory = path.resolve(process.env.ROM_FIVE_PLAYTEST_DIR || 'artifacts/playtest/rom-five');
|
||||
const port = Number(process.env.ROM_FIVE_PLAYTEST_PORT || 4193);
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
const runtimeData = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-rom-five-ui-'));
|
||||
|
||||
(async () => {
|
||||
const { createGameServer } = await import('../server/server.mjs');
|
||||
const runtime = createGameServer({ dataDir: runtimeData, enableShelvedModes: false });
|
||||
await new Promise((resolve) => runtime.server.listen(port, '127.0.0.1', resolve));
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(120_000);
|
||||
const pageErrors = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
|
||||
try {
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByLabel('Username').fill('RomFiveQA');
|
||||
await page.getByLabel('Password').fill('rom-five-playtest');
|
||||
await page.getByRole('button', { name: /Create account/ }).click();
|
||||
await page.locator('.character-select-screen').waitFor();
|
||||
await page.getByRole('button', { name: /Create character/ }).click();
|
||||
await page.locator('.creator-category-tabs button').filter({ hasText: 'RoM' }).click();
|
||||
await page.locator('.class-option').filter({ hasText: 'Rogue' }).click();
|
||||
await page.getByText('Skills unlock by level').waitFor();
|
||||
await page.getByRole('button', { name: /Customize & name/ }).click();
|
||||
await page.getByLabel('Character name').fill('RomRogueQA');
|
||||
await page.getByRole('button', { name: /Create character/ }).click();
|
||||
await page.locator('.character-select-screen').waitFor();
|
||||
await page.locator('.character-preview canvas').waitFor({ timeout: 180_000 });
|
||||
await page.waitForTimeout(3_000);
|
||||
await page.screenshot({ path: path.join(directory, 'roster-preview.png') });
|
||||
|
||||
await page.getByRole('button', { name: /Create character/ }).click();
|
||||
await page.locator('.creator-category-tabs button').filter({ hasText: 'RoM' }).click();
|
||||
await page.locator('.race-option').filter({ hasText: 'Dwarf' }).click();
|
||||
await page.getByRole('button', { name: 'Mage Damage' }).click();
|
||||
await page.locator('.creator-preview-stage canvas').waitFor({ timeout: 180_000 });
|
||||
await page.waitForTimeout(3_000);
|
||||
await page.screenshot({ path: path.join(directory, 'dwarf-mage-creator.png') });
|
||||
await page.getByRole('button', { name: /Customize & name/ }).click();
|
||||
await page.getByLabel('Character name').fill('RomDwarfMageQA');
|
||||
await page.getByRole('button', { name: /Create character/ }).click();
|
||||
await page.locator('.character-select-screen').waitFor();
|
||||
await page.locator('.character-preview canvas').waitFor({ timeout: 180_000 });
|
||||
await page.waitForTimeout(3_000);
|
||||
await page.screenshot({ path: path.join(directory, 'dwarf-mage-roster.png') });
|
||||
await page.locator('.roster-character').filter({ hasText: 'RomRogueQA' }).click();
|
||||
|
||||
// The dual-display toggle overlays the narrow roster footer at this viewport.
|
||||
await page.locator('.roster-enter').evaluate((button) => button.click());
|
||||
await page.locator('.main-menu-screen').waitFor();
|
||||
await page.getByRole('button', { name: /Dungeons/ }).click();
|
||||
await page.locator('.dungeon-select-screen').waitFor();
|
||||
await page.locator('.dungeon-category-tab').filter({ hasText: 'WoW' }).click();
|
||||
await page.locator('.dungeon-select-row').filter({ hasText: 'Wailing Caverns' }).click();
|
||||
await page.getByRole('button', { name: /Choose role/ }).click();
|
||||
await page.locator('.dungeon-role-option--damage').click();
|
||||
await page.locator('.party-frames').waitFor({ timeout: 180_000 });
|
||||
await page.locator('.loading-screen').waitFor({ state: 'detached', timeout: 180_000 });
|
||||
assert.equal(await page.locator('.party-frame').count(), 5);
|
||||
await page.screenshot({ path: path.join(directory, 'dungeon-rogue.png') });
|
||||
await page.getByRole('button', { name: /Tactical/ }).click();
|
||||
await page.getByRole('button', { name: 'Skills', exact: true }).click();
|
||||
await page.locator('.rom-skills-panel').waitFor();
|
||||
assert.doesNotMatch(await page.locator('.rom-skills-panel').innerText(), /\bRank\s+\d+\b/);
|
||||
await page.screenshot({ path: path.join(directory, 'skills-no-ranks.png') });
|
||||
await page.locator('.rom-skills-panel').getByRole('button', { name: 'Close' }).click();
|
||||
await page.getByRole('button', { name: /Tactical/ }).click();
|
||||
await page.getByRole('button', { name: 'Spells', exact: true }).click();
|
||||
await page.locator('.spellbook-panel').waitFor();
|
||||
assert.doesNotMatch(await page.locator('.spellbook-panel').innerText(), /\bRank\s+\d+\b/);
|
||||
await page.screenshot({ path: path.join(directory, 'spellbook-no-ranks.png') });
|
||||
assert.deepEqual(pageErrors, []);
|
||||
console.log(`RoM Rogue roster and dungeon screenshots: ${directory}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await new Promise((resolve) => runtime.server.close(resolve));
|
||||
fs.rmSync(runtimeData, { recursive: true, force: true });
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { projectRoot, sourceRootFor } from "./lib/recipe.mjs";
|
||||
|
||||
const sourceRoot = sourceRootFor({
|
||||
source: { rootEnvironmentVariable: "RUNEWAKER_ROOT", relativeDefault: "../rom-pvt-server-backups/Runewaker" },
|
||||
});
|
||||
const dataRoot = path.join(sourceRoot, "Resource", "data");
|
||||
const outputPath = path.join(projectRoot, "src", "game", "generated", "romLevelingGearPackages.json");
|
||||
const files = ["questdetailobject.db", "itemobject.db", "armorobject.db", "suitobject.db", "string_enus.db"];
|
||||
const buffers = Object.fromEntries(await Promise.all(files.map(async (name) => (
|
||||
[name, await readFile(path.join(dataRoot, name))]
|
||||
))));
|
||||
|
||||
const DATA_START = 140;
|
||||
const COUNT_OFFSET = 132;
|
||||
const layouts = {
|
||||
"questdetailobject.db": { size: 1656, type: 7 },
|
||||
"itemobject.db": { size: 1776, type: 2 },
|
||||
"armorobject.db": { size: 1776, type: 2 },
|
||||
"suitobject.db": { size: 1284, type: 17 },
|
||||
};
|
||||
const indexes = Object.fromEntries(Object.entries(layouts).map(([name, layout]) => {
|
||||
const buffer = buffers[name];
|
||||
const count = buffer.readInt32LE(COUNT_OFFSET);
|
||||
if (count < 1 || DATA_START + count * layout.size > buffer.length) {
|
||||
throw new Error(`${name} record count or layout drifted.`);
|
||||
}
|
||||
const index = new Map();
|
||||
for (let row = 0; row < count; row += 1) {
|
||||
const offset = DATA_START + row * layout.size;
|
||||
if (buffer.readInt32LE(offset + 4) !== layout.type) {
|
||||
throw new Error(`${name} object type drifted at row ${row}.`);
|
||||
}
|
||||
index.set(buffer.readInt32LE(offset), offset);
|
||||
}
|
||||
return [name, index];
|
||||
}));
|
||||
|
||||
function record(name, id) {
|
||||
const offset = indexes[name].get(id);
|
||||
if (offset === undefined) throw new Error(`${name} is missing source ID ${id}.`);
|
||||
return buffers[name].subarray(offset, offset + layouts[name].size);
|
||||
}
|
||||
|
||||
const names = new Map();
|
||||
function sourceName(id) {
|
||||
if (names.has(id)) return names.get(id);
|
||||
const buffer = buffers["string_enus.db"];
|
||||
const key = Buffer.from(`Sys${id}_name\0`, "ascii");
|
||||
const position = buffer.indexOf(key);
|
||||
if (position < 0) throw new Error(`Missing English name for source ID ${id}.`);
|
||||
for (let offset = position + key.length; offset < Math.min(buffer.length - 4, position + key.length + 600); offset += 1) {
|
||||
const length = buffer.readInt32LE(offset);
|
||||
if (length < 2 || length > 300 || offset + 4 + length > buffer.length) continue;
|
||||
const value = buffer.subarray(offset + 4, offset + 3 + length);
|
||||
if (buffer[offset + 3 + length] !== 0 || value.includes(0)) continue;
|
||||
try {
|
||||
const name = new TextDecoder("utf-8", { fatal: true }).decode(value);
|
||||
names.set(id, name);
|
||||
return name;
|
||||
} catch { /* continue past metadata */ }
|
||||
}
|
||||
throw new Error(`Could not decode English name for source ID ${id}.`);
|
||||
}
|
||||
|
||||
// The quest rewards are source-linked. Suit matches are curated from the
|
||||
// source's armor category, required levels, names, and suit membership. Package
|
||||
// expansion itself is not present in these DB records and remains unverified.
|
||||
const milestones = [
|
||||
{ level: 50, questId: 424596, choices: [
|
||||
["plate", 240384, 240385, 240386, 610363],
|
||||
["chain", 240387, 240388, 240389, 610364],
|
||||
["leather", 240390, 240391, 240392, 610365],
|
||||
["cloth", 240393, 240394, 240395, 610366],
|
||||
] },
|
||||
{ level: 55, questId: 424597, choices: [
|
||||
["plate", 240353, 240354, 240355, 610395],
|
||||
["chain", 240356, 240357, 240358, 610396],
|
||||
["leather", 240359, 240360, 240361, 610397],
|
||||
["cloth", 240362, 240363, 240364, 610398],
|
||||
] },
|
||||
{ level: 60, questId: 424598, choices: [
|
||||
["plate", 240341, 240342, 240343, 610391],
|
||||
["chain", 240344, 240345, 240346, 610392],
|
||||
["leather", 240347, 240348, 240349, 610393],
|
||||
["cloth", 240350, 240351, 240352, 610394],
|
||||
] },
|
||||
{ level: 65, questId: 424213, choices: [
|
||||
["plate", 240304, 240302, 240303, 610387],
|
||||
["chain", 240307, 240305, 240306, 610388],
|
||||
["leather", 240310, 240308, 240309, 610389],
|
||||
["cloth", 240313, 240311, 240312, 610390],
|
||||
] },
|
||||
];
|
||||
|
||||
const extracted = milestones.map(({ level, questId, choices }) => {
|
||||
const quest = record("questdetailobject.db", questId);
|
||||
if (quest.readInt32LE(0xc0) !== level) throw new Error(`Quest ${questId} level drifted.`);
|
||||
const rewardIds = Array.from({ length: 4 }, (_, index) => quest.readInt32LE(0x34c + index * 4));
|
||||
const packages = choices.map(([armorCategory, packageId, weaponPackageId, armorPackageId, matchingSuitId], index) => {
|
||||
if (rewardIds[index] !== packageId) throw new Error(`Quest ${questId} reward order drifted.`);
|
||||
for (const id of [packageId, weaponPackageId, armorPackageId]) record("itemobject.db", id);
|
||||
const suit = record("suitobject.db", matchingSuitId);
|
||||
const matchingArmorItems = Array.from({ length: 10 }, (_, member) => suit.readInt32LE(1224 + member * 4))
|
||||
.filter((id) => id > 0)
|
||||
.map((itemId) => {
|
||||
const item = record("armorobject.db", itemId);
|
||||
if (item.readInt32LE(0x1a8) !== matchingSuitId) {
|
||||
throw new Error(`Armor ${itemId} lost suit ${matchingSuitId}.`);
|
||||
}
|
||||
const requiredLevel = item.readInt32LE(0x78);
|
||||
if (requiredLevel < 1 || requiredLevel > level) {
|
||||
throw new Error(`Armor ${itemId} has unexpected required level ${requiredLevel}.`);
|
||||
}
|
||||
return { itemId, name: sourceName(itemId), requiredLevel };
|
||||
});
|
||||
if (matchingArmorItems.length !== 7) throw new Error(`Suit ${matchingSuitId} should have seven armor pieces.`);
|
||||
return {
|
||||
armorCategory,
|
||||
packageId,
|
||||
packageName: sourceName(packageId),
|
||||
weaponPackageId,
|
||||
weaponPackageName: sourceName(weaponPackageId),
|
||||
armorPackageId,
|
||||
armorPackageName: sourceName(armorPackageId),
|
||||
matchingSuitId,
|
||||
matchingSuitName: sourceName(matchingSuitId),
|
||||
matchingArmorItems,
|
||||
};
|
||||
});
|
||||
return { level, questId, questName: sourceName(questId), packages };
|
||||
});
|
||||
|
||||
const output = {
|
||||
schemaVersion: 1,
|
||||
contentStatus: "quest-rewards-confirmed-package-expansion-unverified",
|
||||
sourceFiles: Object.fromEntries(files.map((name) => [name, createHash("sha256").update(buffers[name]).digest("hex")])),
|
||||
milestones: extracted,
|
||||
};
|
||||
await writeFile(outputPath, `${JSON.stringify(output, null, 2)}\n`);
|
||||
console.log(`Exported ${extracted.length} RoM leveling milestones and ${extracted.flatMap((entry) => entry.packages).length} package choices.`);
|
||||
Reference in new issue
Block a user