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

242 lines
12 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 assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const directory = path.resolve('playtest-artifacts', 'rotation-overhaul');
fs.mkdirSync(directory, { recursive: true });
const databaseDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-rotation-playtest-'));
const partySeed = 'rotation-boss-test-0';
const bossName = DEFAULT_BOSS_TEST_BOSS;
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
(async () => {
const { createGameServer } = await import('../server/server.mjs');
const runtime = createGameServer({ dataDir: databaseDirectory, enableShelvedModes: false });
await new Promise((resolve) => runtime.server.listen(4191, '127.0.0.1', resolve));
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const context = await browser.newContext({ viewport: { width: 1600, height: 1000 } });
const page = await context.newPage();
page.setDefaultTimeout(90_000);
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
const moduleUrls = fs.readdirSync('dist/assets')
.filter((file) => /^(?:index|GameRuntime|combatStore)-.*\.js$/.test(file))
.map((file) => `/assets/${file}`);
await page.addInitScript((urls) => {
const runtimeStores = async () => {
const values = (await Promise.all(urls.map((url) => import(url)))).flatMap(Object.values);
const stores = values.filter((value) => value && typeof value.getState === 'function');
const partyStore = stores.find((store) => {
const state = store.getState();
return Array.isArray(state?.members) && typeof state?.initializeParty === 'function'
&& typeof state?.setCommand === 'function';
});
const combatStore = stores.find((store) => {
const state = store.getState();
return state?.mobs && state?.actors && Array.isArray(state?.combatEvents)
&& typeof state?.synchronizeActors === 'function';
});
const gameStore = stores.find((store) => {
const state = store.getState();
return 'bossTest' in (state ?? {}) && 'activeDungeonId' in (state ?? {});
});
if (!partyStore || !combatStore || !gameStore) throw new Error('Required runtime stores were not found.');
return { partyStore, combatStore, gameStore };
};
window.configureRotationBossParty = async (seed) => {
const { partyStore, combatStore } = await runtimeStores();
const combat = combatStore.getState();
partyStore.getState().initializeParty(seed, combat.level, combat.health, 'damage', 5, 'wow');
partyStore.getState().setCommand('stop');
combatStore.getState().synchronizeActors();
combatStore.getState().applyDefaultPartyBuffs();
combatStore.getState().applyPrecombatPreparation();
};
window.startRotationBossParty = async () => {
const { partyStore } = await runtimeStores();
partyStore.getState().setCommand('attack');
};
window.readRotationPlaytestState = async () => {
const { partyStore, combatStore, gameStore } = await runtimeStores();
const party = partyStore.getState();
const combat = combatStore.getState();
const game = gameStore.getState();
const runtimeIds = game.bossTest?.runtimeIds ?? [];
const boss = runtimeIds.map((id) => combat.mobs[id]).find(Boolean)
?? Object.values(combat.mobs).find((mob) => mob.boss);
const currentActorIds = new Set(['__player__', ...party.members.map((member) => member.id)]);
return {
activeDungeonId: game.activeDungeonId,
boss: boss ? {
id: boss.id,
name: boss.name,
health: boss.health,
maxHealth: boss.maxHealth,
engaged: boss.engaged,
dead: boss.dead,
} : null,
party: party.members.map((member) => ({
id: member.id,
name: member.name,
classId: member.classId,
specialization: member.specialization,
role: member.role,
health: member.health,
maxHealth: member.maxHealth,
status: member.status,
})),
auras: combat.auras.filter((aura) => currentActorIds.has(aura.targetId)).map((aura) => ({
name: aura.definition.name,
exclusiveGroup: aura.definition.exclusiveGroup ?? null,
sourceId: aura.sourceId,
targetId: aura.targetId,
expiresAt: aura.expiresAt,
})),
summons: combat.summons.filter((summon) => currentActorIds.has(summon.ownerActorId)).map((summon) => ({
name: summon.name,
kind: summon.kind,
ownerActorId: summon.ownerActorId,
sourceAbilityId: summon.sourceAbilityId,
expiresAt: summon.expiresAt,
})),
events: combat.combatEvents.map((event) => ({
id: event.id,
kind: event.kind,
sourceActorId: event.sourceActorId,
targetActorId: event.targetActorId,
abilityId: event.abilityId,
effectiveAmount: event.effectiveAmount,
})),
};
};
}, moduleUrls);
try {
await page.goto('http://127.0.0.1:4191', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Username').fill('Phenom');
await page.getByLabel('Password').fill('rotation-overhaul-boss-run');
await page.getByRole('button', { name: /Create account/ }).click();
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByLabel('Specialization').getByRole('button', { name: /Shadow/ }).click();
await page.getByRole('button', { name: /Customize & name/ }).click();
await page.getByLabel('Character name').fill('Rotationwatcher');
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByRole('button', { name: /Continue/ }).first().click();
await page.getByRole('button', { name: /Boss Testing/ }).click();
await page.getByRole('heading', { name: 'Boss Testing' }).waitFor();
await page.getByLabel('Search bosses').fill(bossName);
const bossPicker = page.locator('.boss-testing-picker select');
await bossPicker.selectOption({ index: 1 });
await page.getByRole('button', { name: 'Test selected boss' }).click();
await page.locator('canvas').waitFor();
await page.waitForTimeout(3_000);
await page.evaluate((seed) => window.configureRotationBossParty(seed), partySeed);
const initial = await page.evaluate(() => window.readRotationPlaytestState());
assert.equal(initial.boss?.name, bossName, 'the requested boss is active in Boss Testing');
assert.equal(initial.party.length, 4, 'the player has a complete four-companion party');
assert(initial.party.some((member) => member.role === 'tank'), 'party includes a tank');
assert(initial.party.some((member) => member.role === 'healer'), 'party includes a healer');
const damage = initial.party.filter((member) => member.role === 'damage');
const ranged = damage.filter((member) => (
['hunter', 'mage', 'priest', 'warlock'].includes(member.classId)
|| (member.classId === 'druid' && member.specialization === 'Balance')
|| (member.classId === 'shaman' && member.specialization === 'Elemental')
));
const melee = damage.filter((member) => !ranged.some((candidate) => candidate.id === member.id));
assert(ranged.length > 0, `party includes ranged damage: ${JSON.stringify(damage)}`);
assert(melee.length > 0, `party includes melee damage: ${JSON.stringify(damage)}`);
const paladin = initial.party.find((member) => member.classId === 'paladin');
assert(paladin, `party includes the expected paladin setup check: ${JSON.stringify(initial.party)}`);
assert(initial.auras.some((aura) => (
aura.targetId === paladin.id && aura.exclusiveGroup === 'paladin:seal'
)), `paladin seal is active before the attack command: ${JSON.stringify(initial.auras)}`);
await page.evaluate(() => window.startRotationBossParty());
const eventKeys = new Set(initial.events.map((event) => `${event.id}:${event.sourceActorId}:${event.abilityId}`));
const observedEvents = [];
const collect = (state) => {
for (const event of state.events) {
const key = `${event.id}:${event.sourceActorId}:${event.abilityId}`;
if (eventKeys.has(key)) continue;
eventKeys.add(key);
observedEvents.push(event);
}
};
let engaged = null;
const engagementDeadline = Date.now() + 30_000;
while (Date.now() < engagementDeadline) {
const state = await page.evaluate(() => window.readRotationPlaytestState());
collect(state);
if (state.boss?.engaged && !state.boss.dead && state.boss.health < state.boss.maxHealth) {
engaged = state;
break;
}
await sleep(50);
}
assert(engaged, `the companion party engages ${bossName}`);
await page.screenshot({ path: path.join(directory, 'boss-engaged.png'), fullPage: true });
const canvasImage = await page.locator('canvas').screenshot({ path: path.join(directory, 'boss-engaged-canvas.png') });
assert(canvasImage.length > 10_000, 'boss screenshot contains a rendered WebGL scene');
let completed = null;
const completionDeadline = Date.now() + 90_000;
while (Date.now() < completionDeadline) {
const state = await page.evaluate(() => window.readRotationPlaytestState());
collect(state);
if (state.boss?.dead || state.boss?.health === 0) {
completed = state;
break;
}
await sleep(50);
}
assert(completed, `the companion party defeats ${bossName}`);
await page.waitForTimeout(250);
await page.screenshot({ path: path.join(directory, 'boss-defeated.png'), fullPage: true });
const partyIds = new Set(initial.party.map((member) => member.id));
const partyEvents = observedEvents.filter((event) => partyIds.has(event.sourceActorId));
for (const member of [...initial.party.filter((candidate) => candidate.role === 'tank'), ...melee, ...ranged]) {
assert(partyEvents.some((event) => event.sourceActorId === member.id && event.kind === 'damage'
&& event.abilityId && event.abilityId !== 'party-auto-attack'), `${member.name} uses a damaging ability`);
}
for (const healer of initial.party.filter((member) => member.role === 'healer')) {
assert(partyEvents.some((event) => event.sourceActorId === healer.id
&& ['damage', 'healing'].includes(event.kind) && event.abilityId), `${healer.name} takes an active combat action`);
}
assert.deepEqual(errors, [], `No browser page errors: ${errors.join('; ')}`);
const results = {
dungeon: `${DEFAULT_BOSS_TEST_BOSS} — Boss Testing`,
boss: bossName,
partySeed,
engagedBoss: engaged.boss,
defeatedBoss: completed.boss,
party: initial.party,
precombatAuras: initial.auras,
precombatSummons: initial.summons,
observedAbilityEvents: partyEvents.filter((event) => event.abilityId !== 'party-auto-attack'),
browserErrors: errors,
};
fs.writeFileSync(path.join(directory, 'results.json'), `${JSON.stringify(results, null, 2)}\n`);
console.log(JSON.stringify(results, null, 2));
console.log('Rotation-overhaul dungeon boss playtest passed.');
} finally {
await browser.close();
await new Promise((resolve) => runtime.server.close(resolve));
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});