Files
healer-man/scripts/playtest-dungeon-race-affixes.cjs
T
2026-09-27 14:31:15 -04:00

124 lines
7.8 KiB
JavaScript

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 path = require('node:path');
const out = path.resolve('playtest-artifacts/dungeon-race-affixes');
fs.mkdirSync(out, { recursive: true });
const base = process.env.RACE_TEST_URL || 'http://127.0.0.1:5173';
const cases = [
['volatile-remains', 'falling-stars', 'volatile', 'star'],
['last-stand', 'stormcall', 'last-stand', 'storm'],
['lingering-ash', 'arcane-pulse', 'ash', 'ring'],
['unstable-orb', 'predator-mark', 'orb', 'predator'],
['risky-boon', 'falling-stars', 'boon', 'star'],
];
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const errors = [];
try {
for (const [role, className, specialization] of [
['Tank', 'Warrior', 'Protection'], ['Healer', 'Priest', 'Discipline'],
['Damage', 'Hunter', 'Marksmanship'],
].filter(([role]) => !process.env.RACE_TEST_ROLE || role === process.env.RACE_TEST_ROLE)) {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
page.on('pageerror', (error) => errors.push(error.message));
try {
await page.goto(base, { waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.getByRole('button', { name: /Play offline/ }).click();
await page.getByRole('button', { name: /Create character/ }).click();
await page.locator('.class-grid .class-option').filter({ hasText: className }).first().click();
await page.locator('.creator-specializations button').filter({ hasText: specialization }).first().click();
await page.getByRole('button', { name: /Customize & name/ }).click();
await page.getByLabel('Character name').fill(`Affix${role}`);
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByRole('button', { name: /Continue|Enter world/ }).first().click({ force: true });
await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.locator('.main-menu-screen').waitFor();
for (const pool of ['authored', 'randomized']) {
await page.getByRole('button', { name: /Dungeon Race/ }).click();
await page.locator('.dungeon-race-screen').waitFor();
if (pool === 'randomized') {
await page.getByRole('button', { name: 'Randomized Bite-Sized' }).click();
await page.locator('.dungeon-race-screen__setup select').first()
.selectOption('randomized-bite-sized-stormwind-stockade-available');
} else {
await page.locator('.dungeon-race-screen__setup select').first()
.selectOption('bite-sized-stormwind-stockade');
}
await page.getByRole('group', { name: 'Your role' })
.getByRole('button', { name: role === 'Damage' ? 'DPS' : role, exact: true }).click();
await page.getByRole('button', { name: 'Start practice' }).click();
await page.locator('.dungeon-race-hud').waitFor({ timeout: 120_000 });
await page.locator('.dungeon-boss-intro').waitFor({ state: 'visible', timeout: 120_000 });
await page.locator('.dungeon-boss-intro').waitFor({ state: 'hidden', timeout: 120_000 });
await page.locator('.dungeon-race-countdown').waitFor({ state: 'visible', timeout: 30_000 });
await page.locator('.dungeon-race-countdown').waitFor({ state: 'hidden', timeout: 30_000 });
const boot = await page.evaluate(async () => {
const { useDungeonRaceStore } = await import('/src/app/dungeonRaceStore.ts');
const { dungeonRaceAffix } = await import('/src/game/dungeonRaceAffixes.ts');
const race = useDungeonRaceStore.getState();
return { dungeonId: race.dungeonId, pair: race.affixIds, status: race.status,
names: race.affixIds.map((id) => dungeonRaceAffix(id).name) };
});
assert.equal(boot.status, 'active');
assert.equal(boot.dungeonId.startsWith(pool === 'authored' ? 'bite-sized-' : 'randomized-bite-sized-'), true);
assert.equal(boot.pair.length, 2);
const timerText = await page.locator('.dungeon-race-hud').innerText();
assert.ok(timerText.includes('Mechanics:'));
for (const name of boot.names) assert.ok(timerText.includes(name), `${pool}/${role}: timer omits ${name}`);
for (const [killId, movementId, killKind, movementKind] of cases) {
const observed = await page.evaluate(async ({ killId, movementId }) => {
const { useDungeonRaceStore } = await import('/src/app/dungeonRaceStore.ts');
const { useDungeonRaceAffixStore, clearDungeonRaceAffixes, advanceDungeonRaceAffixes } =
await import('/src/game/dungeonRaceAffixRuntime.ts');
const { useCombatStore } = await import('/src/game/combatStore.ts');
const { dungeonDefinitionById } = await import('/src/game/dungeonRegistry.ts');
const { setMobPosition } = await import('/src/game/mobRuntimeRegistry.ts');
const race = useDungeonRaceStore.getState();
const definition = dungeonDefinitionById(race.dungeonId);
const trash = definition.staticSpawns.find((spawn) => !definition.bosses.some((boss) => boss.id === spawn.id));
if (!trash) throw new Error('No original trash spawn for affix playtest');
const now = Date.now();
const player = useCombatStore.getState().playerPosition;
clearDungeonRaceAffixes();
useDungeonRaceStore.setState({ affixIds: [killId, movementId] });
advanceDungeonRaceAffixes(now, []);
setMobPosition(trash.id, player);
useCombatStore.getState().registerMob(trash.id, { name: 'Affix trigger', maxHealth: 100, boss: false });
const original = useCombatStore.getState().mobs[trash.id];
useCombatStore.setState((state) => ({ mobs: { ...state.mobs,
[trash.id]: { ...original, dead: true, health: 0 } } }));
advanceDungeonRaceAffixes(now + 100, [trash.id]);
useCombatStore.setState((state) => ({ mobs: { ...state.mobs,
[trash.id]: { ...state.mobs[trash.id], dead: false, health: 100 } } }));
useDungeonRaceAffixStore.setState({ nextMovementAt: now + 101 });
advanceDungeonRaceAffixes(now + 101, []);
return { kinds: useDungeonRaceAffixStore.getState().effects.map((effect) => effect.kind),
pair: useDungeonRaceStore.getState().affixIds };
}, { killId, movementId });
if (killKind !== 'last-stand') assert.ok(observed.kinds.includes(killKind), `${pool}/${role}: ${killId}`);
assert.ok(observed.kinds.includes(movementKind), `${pool}/${role}: ${movementId}`);
assert.deepEqual(observed.pair, [killId, movementId]);
await page.waitForTimeout(150);
if (role === 'Healer' || (role === 'Damage' && pool === 'randomized')) {
await page.screenshot({ path: path.join(out, `${pool}-${role.toLowerCase()}-${killId}-${movementId}.png`) });
}
}
await page.keyboard.press('Escape');
await page.getByRole('dialog', { name: 'Paused' }).getByRole('button', { name: 'Main menu' }).click();
await page.locator('.main-menu-screen').waitFor();
}
} catch (error) {
await page.screenshot({ path: path.join(out, `failure-${role.toLowerCase()}.png`) });
console.error((await page.locator('body').innerText()).slice(0, 4000));
throw error;
} finally { await page.close(); }
}
assert.deepEqual(errors, []);
console.log(JSON.stringify({ ok: true, runs: process.env.RACE_TEST_ROLE ? 2 : 6,
mechanicsPerRun: 9, screenshots: out }));
} finally { await browser.close(); }
})().catch((error) => { console.error(error); process.exitCode = 1; });