const { DEFAULT_TEST_DUNGEON_TITLE, DEFAULT_TEST_DUNGEON_CATEGORY, openSelectedDungeonRoles, } = 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 output = path.resolve('playtest-artifacts/party-equipment', process.argv.includes('--basic-only') ? 'basic' : '.'); const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-equipment-')); fs.mkdirSync(output, { recursive: true }); async function capture(page, name) { await page.screenshot({ path: path.join(output, `${name}.png`) }); } (async () => { const { createGameServer } = await import('../server/server.mjs'); const runtime = createGameServer({ dataDir, staticDir: path.resolve('dist'), contentDir: path.resolve('content'), enableShelvedModes: false }); await new Promise((resolve) => runtime.server.listen(0, '127.0.0.1', resolve)); console.log({ cwd: process.cwd(), staticDir: runtime.staticDir, indexExists: fs.existsSync(path.join(runtime.staticDir, 'index.html')), port: runtime.server.address().port }); const browser = await chromium.launch({ channel: 'msedge', headless: true }); const results = {}; try { for (const category of process.argv.includes('--rom-only') ? ['RoM'] : ['WoW', 'RoM']) { const context = await browser.newContext({ viewport: { width: 1600, height: 1000 } }); const page = await context.newPage(); // Test-only observation of the existing canvas; no alternate character renderer. await page.route(/\/assets\/SkeletonUtils-.*\.js$/, async (route) => { const response = await route.fetch(); const source = await response.text(); const instrumented = source.replace(/onCreated:([\w$]+)=>\{(?=\1\.events\.connect)/, 'onCreated:$1=>{(window.__equipmentQaScenes??=[]).push($1);'); assert.notEqual(instrumented, source, 'The built canvas creation hook must be observable.'); await route.fulfill({ response, body: instrumented }); }); page.setDefaultTimeout(60000); const errors = []; let inspectedParty = []; let bowFacing = null; page.on('pageerror', (error) => errors.push(error.message)); page.on('console', (message) => { if (message.type() === 'error' && !message.text().startsWith('Failed to load resource:')) errors.push(message.text()); }); page.on('response', (response) => { if (response.status() >= 400 && !response.url().endsWith('/favicon.ico')) errors.push(`${response.status()}: ${response.url()}`); }); try { await page.goto(`http://127.0.0.1:${runtime.server.address().port}`, { waitUntil: 'domcontentloaded' }); await page.getByRole('button', { name: /Play offline/ }).click(); await page.getByRole('button', { name: /Create character/ }).click(); await page.locator('.creator-category-tabs button').filter({ hasText: category }).click(); await page.locator('.class-option').filter({ hasText: 'Priest' }).click(); await page.getByRole('button', { name: /Customize & name/ }).click(); await page.getByLabel('Character name', { exact: true }).fill(category === 'WoW' ? 'Wowgear' : 'Romgear'); await page.getByRole('button', { name: /Create character/ }).click(); await page.locator('.character-select-screen').waitFor(); await page.waitForTimeout(4000); await capture(page, `${category}-roster-basic`); await page.getByRole('button', { name: /Continue as/ }).click({ position: { x: 24, y: 30 } }); await page.locator('.main-menu-screen').waitFor(); if (!process.argv.includes('--basic-only')) { await page.getByRole('button', { name: /Downloads/ }).click(); const pack = page.locator('.content-pack-row').filter({ hasText: category === 'WoW' ? 'Extra Wrath Character Models & Skins' : 'RuneWaker Character Models & Skins' }); await pack.getByRole('button', { name: 'Download', exact: true }).click(); await pack.getByText('Installed — offline ready', { exact: true }).waitFor({ timeout: 180000 }); await page.getByRole('button', { name: 'Back', exact: true }).click(); await page.getByRole('button', { name: 'Change character', exact: true }).click(); await page.locator('.character-select-screen').waitFor(); await page.waitForTimeout(5000); await capture(page, `${category}-roster`); await page.getByRole('button', { name: /Continue as/ }).click({ position: { x: 24, y: 30 } }); await page.locator('.main-menu-screen').waitFor(); } await page.getByRole('button', { name: /^Dungeons/ }).click(); await page.locator('.dungeon-category-tab').filter({ hasText: DEFAULT_TEST_DUNGEON_CATEGORY }).click(); await page.locator('.dungeon-select-row').filter({ hasText: DEFAULT_TEST_DUNGEON_TITLE }).first().click(); const download = page.getByRole('button', { name: 'Download and continue', exact: true }); if (await download.isVisible()) { await download.click(); await page.locator('.dungeon-role-option').first().waitFor({ timeout: 180000 }); } else { await openSelectedDungeonRoles(page); } await page.locator('.dungeon-role-option--healer').click(); await page.locator('.scene-shell').waitFor(); await page.locator('.loading-screen').waitFor({ state: 'hidden' }); await page.locator('.party-frames').waitFor(); await page.waitForTimeout(12000); await page.locator('.dungeon-boss-intro').waitFor({ state: 'hidden' }); await capture(page, `${category}-dungeon`); if (category === 'RoM') { inspectedParty = await page.evaluate(() => { const canvas = document.querySelector('.scene-shell canvas'); const state = window.__equipmentQaScenes?.find((entry) => entry.gl.domElement === canvas); if (!state) throw new Error('Could not inspect the live shared scene.'); window.equipmentQaScene = state; const actors = state.scene.getObjectByName('party-population').children; return actors.map((actor) => { const weapons = []; const sourceMeshes = []; actor.traverse((node) => { if (node.name.startsWith('equipped-')) weapons.push(node.name); if (/^(blade01|hammer01|shield01|staff00|bow03|dagger01)_/.test(node.name)) sourceMeshes.push(node.name); }); if (!sourceMeshes.length) throw new Error(`${actor.name} must carry original RoM weapon meshes.`); return { name: actor.name, position: actor.position.toArray(), weapons, sourceMeshes }; }); }); console.log(JSON.stringify({ inspection: inspectedParty }, null, 2)); await page.evaluate(() => { const scene = window.equipmentQaScene.scene; const actors = scene.getObjectByName('party-population').children; const scout = actors.find((actor) => { let found = false; actor.traverse((node) => { if (node.name.startsWith('bow03_')) found = true; }); return found; }); if (!scout) throw new Error('Caelan must carry the original bow03 RoM mesh.'); const state = scout.__r3f.root.getState(); window.equipmentQaScene = state; let bow; scout.traverse((node) => { if (node.name.startsWith('bow03_')) bow = node; }); let prop = bow; while (prop && !prop.name.startsWith('equipped-')) prop = prop.parent; window.equipmentQaBow = { bow, prop }; const vector = scout.position.clone(); const eye = state.camera.position.clone(); window.equipmentQaOriginalFov = state.camera.fov; const view = { ref: { current: () => { const center = scout.getWorldPosition(vector.clone()); center.y += 0.95; // Keep the normal camera's collision-safe location in the corridor. state.camera.position.copy(eye); state.camera.fov = 40; state.camera.updateProjectionMatrix(); state.camera.lookAt(center); } }, priority: 0, store: scout.__r3f.root, }; state.internal.subscribers.push(view); state.internal.subscribers.sort((left, right) => left.priority - right.priority); window.equipmentQaCameraView = view; }); await page.waitForTimeout(1200); bowFacing = await page.evaluate(() => { window.equipmentQaBowFacing = () => { const { bow, prop } = window.equipmentQaBow; bow.updateWorldMatrix(true, true); const center = bow.position.clone().set(0, 0, 0); const tips = center.clone(); const upper = center.clone(); const lower = center.clone(); let upperCount = 0; let lowerCount = 0; let centerCount = 0; let tipCount = 0; const positions = bow.geometry.attributes.position; for (let index = 0; index < positions.count; index++) { const y = Math.abs(positions.getY(index)); const point = bow.localToWorld(bow.getVertexPosition(index, center.clone().set(0, 0, 0))); if (y < 0.2) { center.add(point); centerCount++; } if (y > 0.8) { tips.add(point); tipCount++; } if (positions.getY(index) > 0.8) { upper.add(point); upperCount++; } if (positions.getY(index) < -0.8) { lower.add(point); lowerCount++; } } center.divideScalar(centerCount); tips.divideScalar(tipCount); upper.divideScalar(upperCount); lower.divideScalar(lowerCount); const actor = window.equipmentQaScene.scene.getObjectByName('party-population').children.find((node) => { let found = false; node.traverse((child) => { if (child === bow) found = true; }); return found; }); const chest = actor.localToWorld(center.clone().set(0, 1.1, 0)); const facing = tips.sub(center).normalize().dot(chest.sub(prop.getWorldPosition(center.clone())).normalize()); return { stringSideTowardTorso: facing, longAxisUp: Math.abs(upper.sub(lower).normalize().y), grip: prop.getWorldPosition(center.clone()).toArray() }; }; return window.equipmentQaBowFacing(); }); console.log(JSON.stringify({ bowFacing })); assert.ok(bowFacing.stringSideTowardTorso > 0.1, 'The original bow string side must face Caelan’s torso.'); await capture(page, process.argv.includes('--basic-only') ? 'RoM-Caelan-basic' : 'RoM-Caelan-native'); await page.evaluate(() => { const subscribers = window.equipmentQaScene.internal.subscribers; const index = subscribers.indexOf(window.equipmentQaCameraView); if (index >= 0) subscribers.splice(index, 1); window.equipmentQaScene.camera.fov = window.equipmentQaOriginalFov; window.equipmentQaScene.camera.updateProjectionMatrix(); }); } // Rotate the camera with the normal held-right-mouse gesture to show the party fronts. const bounds = await page.locator('.scene-shell canvas').boundingBox(); await page.mouse.move(bounds.x + bounds.width * 0.5, bounds.y + bounds.height * 0.55); await page.mouse.down({ button: 'right' }); await page.mouse.move(bounds.x + bounds.width * 0.5 + 440, bounds.y + bounds.height * 0.55, { steps: 40 }); await page.mouse.up({ button: 'right' }); await page.waitForTimeout(1200); await capture(page, `${category}-party-front`); if (category === 'RoM') { await page.locator('.scene-shell').focus(); await page.keyboard.down('s'); await page.waitForTimeout(2000); await page.keyboard.up('s'); await page.mouse.wheel(700, 700); await page.waitForTimeout(2500); await capture(page, `${category}-party-open`); } assert.deepEqual(errors, []); results[category] = { dungeon: DEFAULT_TEST_DUNGEON_TITLE, installedNativeCharacterPack: !process.argv.includes('--basic-only'), screenshots: [`${category}-roster-basic.png`, ...(!process.argv.includes('--basic-only') ? [`${category}-roster.png`] : []), `${category}-dungeon.png`, `${category}-party-front.png`, ...(category === 'RoM' ? [`${category}-party-open.png`, process.argv.includes('--basic-only') ? 'RoM-Caelan-basic.png' : 'RoM-Caelan-native.png'] : [])], inspectedParty, bowFacing, errors, }; } catch (error) { await capture(page, `${category}-failure`); console.error((await page.locator('body').innerText()).slice(-2500)); console.error(errors); throw error; } finally { await context.close(); } } fs.writeFileSync(path.join(output, 'results.json'), `${JSON.stringify(results, null, 2)}\n`); console.log(JSON.stringify(results, null, 2)); } finally { await browser.close(); await new Promise((resolve) => runtime.server.close(resolve)); assert.equal(path.dirname(path.resolve(dataDir)), path.resolve(os.tmpdir())); assert.ok(path.basename(dataDir).startsWith('healer-man-equipment-')); fs.rmSync(dataDir, { recursive: true, force: true }); } })().catch((error) => { console.error(error); process.exitCode = 1; });