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 = 'Lady Anacondra'; 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 Lady Anacondra'); 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 Lady Anacondra'); 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: 'Wailing Caverns — 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; });