Release Healer Man 0.1.49
This commit is contained in:
1 parent
39425f2494
commit
612170afd0
91 files changed
+2278
-263
No files matched your search
@@ -0,0 +1,240 @@
|
||||
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;
|
||||
});
|
||||
Reference in new issue
Block a user