forked from phenom/healer-man
242 lines
12 KiB
JavaScript
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;
|
|
});
|