Files
healer-man/scripts/playtest-boss-testing.cjs
2026-10-06 22:25:34 -04:00

243 lines
14 KiB
JavaScript

const { DEFAULT_BOSS_TEST_BOSS } = require('./playtest-defaults.cjs');
const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const selectedBossName = process.env.BOSS_TEST_BOSS || DEFAULT_BOSS_TEST_BOSS;
const historicalWailingBosses = [
'Lady Anacondra', 'Lord Cobrahn', 'Kresh', 'Lord Pythas', 'Skum',
'Lord Serpentis', 'Verdan the Everliving', 'Mutanus the Devourer',
];
if (historicalWailingBosses.includes(selectedBossName) && !process.argv.includes('--historical-wailing')) {
throw new Error('A Wailing boss override requires an explicit owner request and --historical-wailing. Isolated Boss Testing defaults to Hamhock.');
}
const directory = path.resolve(process.env.BOSS_TEST_ARTIFACT_DIR || path.join(
'playtest-artifacts',
selectedBossName === 'Celebras the Cursed' ? 'boss-testing-scaling' : 'boss-testing',
));
fs.mkdirSync(directory, { recursive: true });
const base = process.env.BOSS_TEST_URL || 'http://127.0.0.1:4187';
(async () => {
const { createGameServer } = await import('../server/server.mjs');
const runtime = createGameServer({ dataDir: path.join(directory, 'test-database'), enableShelvedModes: false });
await new Promise((resolve) => runtime.server.listen(4187, '127.0.0.1', resolve));
console.log('Started isolated production test server');
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
const page = await context.newPage();
page.setDefaultTimeout(60000);
const errors = [];
const assetFiles = fs.readdirSync(path.resolve('dist/assets'));
const mainModule = `/assets/${assetFiles.find((file) => /^index-.*\.js$/.test(file))}`;
const combatModule = `/assets/${assetFiles.find((file) => /^combatStore-.*\.js$/.test(file))}`;
await page.addInitScript(({ mainModule, combatModule }) => {
window.bossTestingStores = async () => {
const main = Object.values(await import(mainModule));
const combat = Object.values(await import(combatModule));
return {
useGameStore: main.find((value) => value?.getState && 'bossTest' in value.getState()),
useShellStore: main.find((value) => value?.getState && 'characters' in value.getState()),
useCombatStore: combat.find((value) => value?.getState && 'damageMob' in value.getState()),
};
};
}, { mainModule, combatModule });
page.on('pageerror', (error) => errors.push(error.message));
const capture = async (name) => {
await page.screenshot({ path: path.join(directory, `${name}.png`) });
console.log(`Captured ${name}`);
};
const canvasHash = async () => {
const canvas = page.locator('canvas');
await canvas.evaluate((element) => {
const hidden = [];
let current = element;
while (current.parentElement) {
for (const sibling of current.parentElement.children) {
if (sibling === current || !(sibling instanceof HTMLElement)) continue;
hidden.push([sibling, sibling.style.visibility]);
sibling.style.visibility = 'hidden';
}
current = current.parentElement;
}
window.__restoreCanvasCapture = () => {
for (const [node, visibility] of hidden) node.style.visibility = visibility;
delete window.__restoreCanvasCapture;
};
});
try {
return crypto.createHash('sha256').update(await canvas.screenshot()).digest('hex');
} finally {
await page.evaluate(() => window.__restoreCanvasCapture?.());
}
};
try {
await page.goto(base, { waitUntil: 'domcontentloaded' });
console.log('Production menu loaded');
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('Bosstester');
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByRole('button', { name: /Continue|Enter world/ }).first().click();
await page.locator('.main-menu-screen').waitFor();
await capture('01-main-menu');
await page.getByRole('button', { name: /Boss Testing/ }).click();
await page.getByRole('heading', { name: 'Boss Testing', exact: true }).waitFor();
const picker = page.getByLabel(/Choose a boss/);
const option = await picker.locator('option').evaluateAll((options, name) => options.find((entry) => entry.textContent.startsWith(`${name} ·`))?.value, selectedBossName);
assert(option, `${selectedBossName} is selectable`);
await picker.selectOption(option);
await page.getByRole('button', { name: 'Test selected boss', exact: true }).waitFor({ state: 'visible' });
await capture('02-boss-selection');
await page.getByRole('button', { name: 'Test selected boss', exact: true }).click();
if (await page.getByRole('alert').count()) throw new Error(await page.getByRole('alert').innerText());
await page.locator('canvas').waitFor();
await page.waitForFunction(async () => {
const { useGameStore, useCombatStore } = await window.bossTestingStores();
const game = useGameStore.getState();
return game.assetStatus === 'ready' && game.bossTest?.runtimeIds.every((id) => useCombatStore.getState().mobs[id]);
}, null, { timeout: 90000 });
await page.waitForTimeout(4000);
const bossFrameBefore = await canvasHash();
await page.waitForTimeout(500);
assert.notEqual(
await canvasHash(),
bossFrameBefore,
'Boss Testing redraws the WebGL scene independently from its HUD and nameplates',
);
await capture('03-empty-room');
const before = await page.evaluate(async () => {
const { useGameStore, useCombatStore, useShellStore } = await window.bossTestingStores();
const game = useGameStore.getState();
const combat = useCombatStore.getState();
const boss = game.bossTest;
return { boss, xp: combat.xp, saved: JSON.stringify(useShellStore.getState().characters), count: Object.keys(combat.mobs).length,
classId: combat.classId, level: combat.level, bossLevel: combat.mobs[boss.runtimeIds[0]].level,
bossMaxHealth: combat.mobs[boss.runtimeIds[0]].maxHealth };
});
assert.equal(before.count, 1, 'Only the selected boss spawns initially');
if (selectedBossName === 'Celebras the Cursed') {
assert.equal(before.classId, 'priest');
assert.equal(before.level, 1);
assert.equal(before.bossLevel, 2);
assert.equal(before.bossMaxHealth, 575, 'Level-one Celebras uses scaled native health without campaign floors');
}
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Boss readiness & report', exact: true }).click();
await page.getByRole('button', { name: 'Mark not ready', exact: true }).click();
await page.getByRole('status').filter({ hasText: 'Marked not ready.' }).waitFor();
await page.getByRole('button', { name: 'Mark needs testing', exact: true }).click();
await page.getByRole('status').filter({ hasText: 'Marked needs testing.' }).waitFor();
await capture('04-pause-review');
await page.getByRole('button', { name: 'Submit a boss report', exact: true }).click();
await page.getByLabel('Describe the issue', { exact: true }).fill('Boss Testing playtest: verify mechanic telegraphs.');
await page.getByRole('button', { name: 'Submit report', exact: true }).click();
await page.getByText('Report submitted.', { exact: false }).waitFor();
await capture('05-boss-report');
await page.getByRole('button', { name: 'Back to boss review', exact: true }).click();
await page.getByRole('button', { name: 'Fight this boss again', exact: true }).click();
await page.waitForTimeout(1000);
await page.evaluate(async () => {
const { useGameStore, useCombatStore } = await window.bossTestingStores();
const game = useGameStore.getState();
const combat = useCombatStore.getState();
combat.selectMob(game.bossTest.runtimeIds[0]);
combat.damageMob(game.bossTest.runtimeIds[0], 1000000, Date.now());
});
await page.getByText('Boss defeated', { exact: true }).waitFor();
await page.getByRole('button', { name: 'Mark ready', exact: true }).click();
await page.getByRole('status').filter({ hasText: 'Marked ready.' }).waitFor();
await capture('06-victory-review');
const after = await page.evaluate(async () => {
const { useCombatStore, useShellStore } = await window.bossTestingStores();
return { xp: useCombatStore.getState().xp, saved: JSON.stringify(useShellStore.getState().characters) };
});
assert.equal(after.xp, before.xp);
assert.equal(after.saved, before.saved);
const reports = runtime.database.listDungeonReports().reports;
assert.equal(reports[0].bossKey, before.boss.key);
assert.equal(runtime.database.listBossTestingReviews().reviews[0].status, 'ready');
await page.evaluate(async (previousKey) => {
window.previousBossTestingKey = previousKey;
window.liveBossTestingStores = await window.bossTestingStores();
}, before.boss.key);
await page.getByRole('button', { name: 'Fight another random boss', exact: true }).click();
await page.waitForFunction(() => {
const { useGameStore, useCombatStore } = window.liveBossTestingStores;
const game = useGameStore.getState();
return game.assetStatus === 'ready' && game.bossTest?.key !== window.previousBossTestingKey
&& game.bossTest?.runtimeIds.every((id) => useCombatStore.getState().mobs[id]);
}, null, { timeout: 120000 });
const nextBoss = await page.evaluate(() => {
const { useGameStore } = window.liveBossTestingStores;
return useGameStore.getState().bossTest;
});
assert.notEqual(nextBoss.key, before.boss.key, 'Post-fight random selection must choose another boss');
await page.waitForTimeout(1000);
await capture('06b-another-random-boss');
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Return to main menu', exact: true }).click();
await page.locator('.main-menu-screen').waitFor();
await page.getByRole('button', { name: /Boss Testing/ }).click();
await picker.selectOption(option);
await page.getByRole('article').getByText('Ready', { exact: true }).waitFor();
await page.setViewportSize({ width: 390, height: 844 });
await capture('07-mobile-selection');
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'No mobile horizontal overflow');
await page.setViewportSize({ width: 1440, height: 1000 });
await page.getByLabel('Search bosses').fill(selectedBossName);
await page.getByRole('button', { name: 'Fight a random boss', exact: true }).click();
await page.locator('canvas').waitFor();
await page.waitForTimeout(3000);
await capture('08-random-fight');
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Return to main menu', exact: true }).click();
await page.getByRole('button', { name: /Boss Testing/ }).click();
const missing = await picker.locator('option').evaluateAll((options) => options.find((entry) => /Taragaman the Hungerer.*Ragefire Chasm/.test(entry.textContent))?.value);
assert(missing, 'An uninstalled boss is selectable');
await picker.selectOption(missing);
await capture('09-automatic-download');
await page.waitForFunction(() => {
const button = [...document.querySelectorAll('button')].find((entry) => entry.textContent === 'Test selected boss');
const error = document.querySelector('[role="alert"]');
if (error) throw new Error(error.textContent);
return button && !button.disabled;
}, null, { timeout: 120000 });
const installed = await page.evaluate(() => JSON.parse(localStorage.getItem('healer-man.content-state.v2') || '{}'));
assert(installed.packs['dungeon:ragefire-chasm'], 'The selected boss pack downloads automatically');
await page.getByRole('button', { name: 'Test selected boss', exact: true }).click();
await page.waitForFunction(async () => {
const { useGameStore, useCombatStore } = await window.bossTestingStores();
const game = useGameStore.getState();
return game.assetStatus === 'ready' && game.bossTest?.runtimeIds.every((id) => useCombatStore.getState().mobs[id]);
}, null, { timeout: 90000 });
await page.waitForTimeout(1500);
await capture('10-downloaded-boss-room');
const bottom = await page.context().newPage();
bottom.on('pageerror', (error) => errors.push(error.message));
await bottom.setViewportSize({ width: 1240, height: 1080 });
await bottom.goto(`${base}/?display=bottom&nativeLayout=1`, { waitUntil: 'domcontentloaded' });
await bottom.getByRole('button', { name: 'Boss review', exact: true }).waitFor();
await bottom.screenshot({ path: path.join(directory, '11-bottom-display.png') });
await bottom.getByRole('button', { name: 'Boss review', exact: true }).click();
await page.getByRole('heading', { name: 'Taragaman the Hungerer', exact: true }).waitFor();
await capture('12-bottom-display-review');
await bottom.close();
assert.deepEqual(errors, [], 'No browser runtime errors');
fs.writeFileSync(path.join(directory, 'results.json'), `${JSON.stringify({ passed: true, boss: before.boss, classId: before.classId,
characterLevel: before.level, bossLevel: before.bossLevel, bossMaxHealth: before.bossMaxHealth,
initialMobCount: before.count, nextRandomBoss: nextBoss, xpUnchanged: true, profileUnchanged: true,
automaticDownload: 'dungeon:ragefire-chasm', report: reports[0], errors }, null, 2)}\n`);
console.log('Boss Testing live flow passed');
} catch (error) {
await capture('failure');
console.log((await page.locator('body').innerText()).slice(-2000));
console.log('Browser errors:', errors);
throw error;
} finally {
await browser.close();
await new Promise((resolve) => runtime.server.close(resolve));
}
})().catch((error) => { console.error(error); process.exitCode = 1; });