Files
healer-man/scripts/playtest-vow-live-final.cjs
2026-10-04 22:51:21 -04:00

798 lines
46 KiB
JavaScript

// Historical Lady Anacondra reproduction only. New routine Vow QA uses Stockade.
if (!process.argv.includes('--historical-wailing')) {
throw new Error('This historical Wailing harness requires an explicit owner request and --historical-wailing. Use the Stockade campaign/mechanics harness for routine QA.');
}
const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
// UI-driven, final-build Boss Testing proof for Vow of Silence against the
// source-scheduled Lady Anacondra Lightning Bolt. Store access is read-only
// during combat; only the isolated profile's level and secondary class are set.
const artifactRoot = path.resolve('playtest-artifacts/vow-live-final');
const stamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', '');
const outputDirectory = path.join(artifactRoot, `final-build-${stamp}`);
const dataDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-vow-live-final-'));
const liveFightBudgetMs = 80_000;
const vowSilenceWindowMs = 8_400;
const actionSlots = [
['primary:face-bottom', '1'], ['primary:face-left', '2'], ['primary:face-top', '3'],
['primary:face-right', '4'], ['primary:r1', '5'], ['primary:r2', '6'],
['primary:dpad-left', '7'], ['primary:dpad-right', '8'],
];
fs.mkdirSync(outputDirectory, { recursive: true });
function flatEffects(effects = []) {
return effects.flatMap((effect) => effect.kind === 'composite' ? flatEffects(effect.effects) : [effect]);
}
function actionPlanFromSnapshot(snapshot, vowId) {
const abilities = new Map(snapshot.abilities.map((ability) => [ability.id, ability]));
const bound = actionSlots.flatMap(([bindingId, key]) => {
const id = snapshot.actionBindings[bindingId];
const ability = id ? abilities.get(id) : null;
return ability && ability.id !== vowId && !ability.passive
&& (ability.unlockLevel ?? 1) <= snapshot.player.level
? [{ key, ability, effects: flatEffects(ability.effects) }] : [];
});
const healing = bound.find(({ ability, effects }) => ability.target === 'friendly'
&& effects.some((effect) => effect.kind === 'heal')) ?? null;
const damage = bound.filter(({ ability, effects }) => ability.target === 'hostile'
&& effects.some((effect) => effect.kind === 'damage'))
.sort((left, right) => Number(right.ability.range?.max ?? 0) - Number(left.ability.range?.max ?? 0))[0] ?? null;
return {
healing: healing ? { key: healing.key, id: healing.ability.id, name: healing.ability.name,
range: healing.ability.range ?? null } : null,
damage: damage ? { key: damage.key, id: damage.ability.id, name: damage.ability.name,
range: damage.ability.range ?? null } : null,
bound: bound.map(({ key, ability }) => ({ key, id: ability.id, name: ability.name,
target: ability.target, range: ability.range ?? null })),
};
}
async function readLiveState(page, includeActionSetup = false) {
return page.evaluate((includeSetup) => window.vowFinalStores().then(({ game, combat, party,
getMobPosition, getMobTransform, getPartyPosition }) => {
const gameState = game.getState();
const combatState = combat.getState();
return {
now: Date.now(),
game: {
activeDungeonId: gameState.activeDungeonId,
gameMode: gameState.gameMode,
gmTestMode: gameState.gmTestMode,
bossTest: gameState.bossTest,
assetStatus: gameState.assetStatus,
activeSpawn: gameState.activeSpawn,
playerPosition: gameState.playerPosition,
cameraYaw: gameState.cameraYaw ?? null,
overlay: gameState.overlay,
dungeonIntroActive: gameState.dungeonIntroActive,
},
player: {
classId: combatState.classId,
secondaryClassId: combatState.secondaryClassId,
statRuleset: combatState.statRuleset,
level: combatState.level,
experience: combatState.xp,
health: combatState.health,
maxHealth: combatState.maxHealth,
resource: combatState.resource,
maxResource: combatState.maxResource,
resourceType: combatState.resourceType,
resourceName: combatState.resourceName,
position: gameState.playerPosition,
cameraYaw: gameState.cameraYaw ?? null,
selectedTargetId: combatState.selectedTargetId,
activeCast: combatState.activeCast?.abilityId ?? null,
globalCooldownEndsAt: combatState.globalCooldownEndsAt,
cooldowns: combatState.cooldowns,
},
party: party.getState().members.map((member) => ({
id: member.id,
name: member.name,
classId: member.classId,
secondaryClassId: member.secondaryClassId,
role: member.role,
statRuleset: member.statRuleset,
health: member.health,
maxHealth: member.maxHealth,
resource: member.resource,
maxResource: member.maxResource,
resourceType: member.resourceType,
status: member.status,
activeCast: member.activeCast?.abilityId ?? null,
position: getPartyPosition?.(member.id) ?? null,
})),
mobs: Object.values(combatState.mobs).filter((mob) => !mob.despawned).map((mob) => ({
id: mob.id,
name: mob.name,
serverEntry: mob.serverEntry,
runewakerNpcId: mob.runewakerNpcId ?? null,
creatureFamily: mob.creatureFamily,
level: mob.level,
health: mob.health,
maxHealth: mob.maxHealth,
dead: mob.dead,
diedAt: mob.diedAt ?? null,
engaged: mob.engaged,
hasLoot: mob.hasLoot,
pendingLootCount: mob.pendingLoot?.length ?? 0,
position: getMobPosition?.(mob.id) ?? mob.position ?? mob.homePosition,
transform: getMobTransform?.(mob.id) ?? null,
attackRevision: mob.attackRevision,
abilityCastCounts: mob.abilityCastCounts,
abilityReadyAt: mob.abilityReadyAt,
schoolLockedUntil: mob.schoolLockedUntil,
mechanicImmunities: mob.mechanicImmunities,
schoolImmunities: mob.schoolImmunities,
auras: mob.auras,
activeCast: mob.activeCast ? {
name: mob.activeCast.name,
attackId: mob.activeCast.attackId,
spellId: mob.activeCast.spellId ?? null,
school: mob.activeCast.school,
startedAt: mob.activeCast.startedAt,
completesAt: mob.activeCast.completesAt,
interruptible: mob.activeCast.interruptible,
} : null,
statuses: mob.statuses,
attacks: mob.attacks.map((attack) => ({ id: attack.id, name: attack.name,
spellId: attack.spellId ?? null, castTimeMs: attack.castTimeMs ?? 0,
cooldownMs: attack.cooldownMs ?? 0, school: attack.school,
interruptible: attack.interruptible !== false })),
boss: mob.boss,
})),
...(includeSetup ? { abilities: combatState.abilities, actionBindings: combatState.actionBindings } : {}),
feedback: combatState.feedback ? { kind: combatState.feedback.kind,
message: combatState.feedback.message, abilityId: combatState.feedback.abilityId ?? null,
targetId: combatState.feedback.targetId ?? null } : null,
events: combatState.combatEvents.slice(-200),
presentationEvents: window.vowFinalPresentationEvents.slice(-500),
presentationSubscriptionFound: window.vowFinalPresentationSubscriptionFound ?? false,
presentationSubscribeExportName: window.vowFinalPresentationSubscribeExportName ?? null,
presentationExportNames: window.vowFinalPresentationExportNames ?? [],
presentationSubscriptionCandidates: window.vowFinalPresentationSubscriptionCandidates ?? [],
};
}), includeActionSetup);
}
async function assignVowThroughAbilityControls(page, outputDirectory) {
const before = await readLiveState(page, true);
const vow = before.abilities.find((ability) => ability.dbcSpellId === 490275);
if (!vow) throw new Error('The UI-created Priest/Knight did not learn DBC spell 490275 at the prepared level.');
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await page.getByRole('button', { name: 'Ability controls' }).click();
await page.locator('.ability-bindings__diagram').waitFor({ timeout: 15_000 });
await page.locator('.ability-bindings__layers button').nth(1).click();
await page.locator('button[aria-label*="Shift+5:"]').click();
await page.getByRole('button', { name: new RegExp(`^${vow.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:,|$)`) }).click();
await page.screenshot({ path: path.join(outputDirectory, 'vow-binding-ui.png') });
const after = await readLiveState(page, true);
const binding = Object.entries(after.actionBindings).find(([, id]) => id === vow.id)?.[0] ?? null;
await page.getByRole('button', { name: 'Close button assignments' }).click();
await page.getByRole('button', { name: 'Resume game' }).click();
assert.equal(binding, 'secondary:r1', 'Vow is bound through the UI to Shift+Digit5.');
return { id: vow.id, name: vow.name, dbcSpellId: vow.dbcSpellId,
unlockLevel: vow.unlockLevel, playerLevel: after.player.level, target: vow.target,
range: vow.range ?? null, radius: vow.radius ?? null, targeting: vow.targeting ?? null,
castMode: vow.castMode, castTimeMs: vow.castTimeMs, cooldownMs: vow.cooldownMs,
gcdMs: vow.gcdMs, cost: vow.cost, effects: vow.effects,
binding, input: 'Shift+Digit5' };
}
async function selectBossWithTab(page, bossId, attempts = 8) {
let snapshot = await readLiveState(page);
for (let attempt = 0; attempt < attempts && snapshot.player.selectedTargetId !== bossId; attempt += 1) {
await page.keyboard.press('Tab');
await page.waitForTimeout(70);
snapshot = await readLiveState(page);
}
return { selected: snapshot.player.selectedTargetId === bossId, snapshot };
}
async function selectPartyMember(page, member) {
const label = new RegExp(`^Target ${member.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')},`);
await page.getByRole('button', { name: label }).click();
}
async function selectPlayerFrame(page, playerName) {
const label = new RegExp(`^Target ${playerName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}, you$`);
await page.getByRole('button', { name: label }).click();
}
async function performCast(page, action, kind, intendedTargetId, reason) {
const before = await readLiveState(page);
await page.keyboard.press(action.key);
const startedAt = Date.now();
let after = await readLiveState(page);
const eventsFor = (state) => state.events.filter((event) => event.occurredAt >= before.now - 1
&& event.abilityId === action.id && event.sourceActorId === '__player__'
&& (kind === 'damage' ? event.kind === 'damage' : event.kind === 'healing'));
while (Date.now() - startedAt < 2_600) {
if (eventsFor(after).some((event) => event.effectiveAmount > 0)
|| (!after.player.activeCast && Date.now() - startedAt >= 650)) break;
await page.waitForTimeout(120);
after = await readLiveState(page);
}
const memberBefore = before.party.find((member) => member.id === intendedTargetId);
const memberAfter = after.party.find((member) => member.id === intendedTargetId);
return {
at: before.now,
key: action.key,
abilityId: action.id,
abilityName: action.name,
kind,
reason,
intendedTargetId,
selectedTargetIdBefore: before.player.selectedTargetId,
targetHealthBefore: kind === 'healing'
? memberBefore?.health ?? (intendedTargetId === '__player__' ? before.player.health : null)
: before.mobs.find((mob) => mob.id === intendedTargetId)?.health ?? null,
targetHealthAfter: kind === 'healing'
? memberAfter?.health ?? (intendedTargetId === '__player__' ? after.player.health : null)
: after.mobs.find((mob) => mob.id === intendedTargetId)?.health ?? null,
resolvedEvents: eventsFor(after),
actionResolved: eventsFor(after).some((event) => event.effectiveAmount > 0),
playerHealthBefore: before.player.health,
playerHealthAfter: after.player.health,
playerResourceBefore: before.player.resource,
playerResourceAfter: after.player.resource,
feedback: after.feedback,
atAfter: after.now,
};
}
function playerVitals(snapshot) {
return { health: snapshot.player.health, maxHealth: snapshot.player.maxHealth,
resource: snapshot.player.resource, maxResource: snapshot.player.maxResource,
resourceType: snapshot.player.resourceType, resourceName: snapshot.player.resourceName };
}
function partyVitals(snapshot) {
return snapshot.party.map((member) => ({ id: member.id, name: member.name,
classId: member.classId, secondaryClassId: member.secondaryClassId, role: member.role,
statRuleset: member.statRuleset, status: member.status,
health: member.health, maxHealth: member.maxHealth, resource: member.resource,
maxResource: member.maxResource, resourceType: member.resourceType }));
}
async function runFight(page, result, vowSetup, initial) {
const bossId = initial.game.bossTest.runtimeIds[0];
let current = initial;
const target = await selectBossWithTab(page, bossId);
if (!target.selected) throw new Error('Boss Testing UI targeting did not select the live Lady Anacondra.');
current = target.snapshot;
await page.keyboard.press('F1');
result.fight.startedAt = Date.now();
result.fight.partyAttackInput = { at: result.fight.startedAt, key: 'F1', command: 'Attack', targetId: bossId };
const wallDeadline = result.fight.startedAt + liveFightBudgetMs;
let vowAttempts = 0;
let nextDamageAt = 0;
let nextHealAt = 0;
let observedCastCountAtWindowStart = null;
let lastSampleAt = 0;
let lastPresentationCursor = current.presentationEvents.at(-1)?.id ?? 0;
let previousCastIdentity = null;
while (Date.now() < wallDeadline) {
current = await readLiveState(page);
const boss = current.mobs.find((mob) => mob.id === bossId);
if (!boss || boss.dead || boss.health <= 0) {
result.fight.outcome = boss?.dead || boss?.health <= 0 ? 'boss-defeated' : 'boss-despawned';
result.fight.bossAtEnd = boss ?? null;
break;
}
if (current.player.health <= 0) { result.fight.outcome = 'player-dead'; break; }
if (lastSampleAt === 0 || current.now - lastSampleAt >= 350) {
result.fight.samples.push({ at: current.now, bossHealth: boss.health, bossCast: boss.activeCast,
lightningBoltCount: boss.abilityCastCounts?.['spell-9532'] ?? null,
natureLockedUntil: boss.schoolLockedUntil?.nature ?? 0,
player: playerVitals(current), party: partyVitals(current) });
lastSampleAt = current.now;
}
for (const event of current.presentationEvents) {
if (event.id <= lastPresentationCursor) continue;
result.fight.presentationEvents.push(event);
lastPresentationCursor = Math.max(lastPresentationCursor, event.id);
}
if (current.player.selectedTargetId !== bossId) {
const selected = await selectBossWithTab(page, bossId, 4);
if (selected.selected) current = selected.snapshot;
}
const liveBoss = current.mobs.find((mob) => mob.id === bossId);
const liveCast = liveBoss?.activeCast;
const castIdentity = liveCast ? `${liveCast.attackId}:${liveCast.startedAt}` : null;
if (castIdentity !== previousCastIdentity) {
result.fight.castTimeline.push({ at: current.now, activeCast: liveCast });
previousCastIdentity = castIdentity;
}
const sourceLightning = liveBoss?.attacks.find((attack) => attack.spellId === 9532
|| attack.id === 'spell-9532') ?? null;
if (!sourceLightning || sourceLightning.castTimeMs <= 0) {
throw new Error('The live Lady Anacondra has no source-scheduled interruptible Lightning Bolt (9532) definition.');
}
if (!result.fight.sourceLightningDefinition) result.fight.sourceLightningDefinition = sourceLightning;
if (!result.fight.vowSuccess && vowAttempts < 4 && liveCast
&& liveCast.attackId === sourceLightning.id && liveCast.spellId === 9532
&& liveCast.interruptible && liveCast.completesAt - current.now > 450
&& current.player.selectedTargetId === bossId
&& current.player.globalCooldownEndsAt <= current.now
&& !current.player.activeCast) {
vowAttempts += 1;
const beforeCast = liveCast;
const presentationBefore = current.presentationEvents.at(-1)?.id ?? 0;
const eventsBefore = current.events.map((event) => event.id);
const attempt = { attempt: vowAttempts, observedAt: current.now,
castStartedAt: beforeCast.startedAt, scheduledCompletionAt: beforeCast.completesAt,
remainingMs: beforeCast.completesAt - current.now, activeCastBefore: beforeCast,
sourceAttack: sourceLightning, input: 'Shift+Digit5', selectedTargetId: current.player.selectedTargetId,
lightningBoltCountBefore: liveBoss.abilityCastCounts?.[sourceLightning.id] ?? null,
natureLockedUntilBefore: liveBoss.schoolLockedUntil?.nature ?? 0,
playerOriginBefore: current.player.position ?? null,
playerCameraYawBefore: current.player.cameraYaw ?? null,
bossPositionBefore: liveBoss.position ?? null,
bossTransformBefore: liveBoss.transform ?? null,
vowTargetingMetadata: { target: vowSetup.target, range: vowSetup.range,
radius: vowSetup.radius, targeting: vowSetup.targeting } };
result.fight.vowAttempts.push(attempt);
await page.screenshot({ path: path.join(outputDirectory, `lightning-bolt-pre-vow-${vowAttempts}.png`) });
await page.keyboard.press('Shift+Digit5');
attempt.inputAt = Date.now();
await page.waitForTimeout(65);
const afterVow = await readLiveState(page);
const afterBoss = afterVow.mobs.find((mob) => mob.id === bossId);
const cancelEvent = afterVow.presentationEvents.find((event) => event.id > presentationBefore
&& event.phase === 'cast-cancel' && event.actorGroup === 'enemy'
&& event.sourceActorId === bossId && event.abilityId === beforeCast.attackId
&& (event.sourceSpellId === 9532 || event.name === sourceLightning.name));
const vowEvents = afterVow.events.filter((event) => !eventsBefore.includes(event.id)
&& event.abilityId === vowSetup.id && event.sourceActorId === '__player__');
const schoolLock = afterBoss?.schoolLockedUntil?.nature ?? 0;
const sameObservedCastStillActive = afterBoss?.activeCast?.attackId === beforeCast.attackId
&& afterBoss.activeCast.startedAt === beforeCast.startedAt;
const sourceCastEndedOnTime = Boolean(!sameObservedCastStillActive && afterVow.now < beforeCast.completesAt);
const appliedSilence = afterBoss?.statuses.find((status) => status.kind === 'silence'
&& status.sourceAbilityId === 'aura:rom-priest-vow-of-silence'
&& status.endsAt > afterVow.now) ?? null;
const silenceAura = afterBoss?.auras.find((aura) => aura.id === 'rom-priest-vow-of-silence'
&& aura.endsAt > afterVow.now) ?? null;
const silenceEndsAt = appliedSilence?.endsAt ?? silenceAura?.endsAt ?? schoolLock;
const silenced = silenceEndsAt >= afterVow.now + 7_900;
attempt.afterReadAt = afterVow.now;
attempt.presentationSubscriptionFound = afterVow.presentationSubscriptionFound;
attempt.presentationSubscribeExportName = afterVow.presentationSubscribeExportName;
attempt.presentationExportNames = afterVow.presentationExportNames;
attempt.presentationSubscriptionCandidates = afterVow.presentationSubscriptionCandidates;
attempt.activeCastAfter = afterBoss?.activeCast ?? null;
attempt.sourceCastEndedBeforeScheduledCompletion = sourceCastEndedOnTime;
attempt.sameObservedCastStillActive = sameObservedCastStillActive;
attempt.castIdentityChangedBeforeScheduledCompletion = Boolean(afterBoss?.activeCast
&& (afterBoss.activeCast.attackId !== beforeCast.attackId
|| afterBoss.activeCast.startedAt !== beforeCast.startedAt)
&& sourceCastEndedOnTime);
attempt.targetSourceIdentityAfter = afterBoss ? { serverEntry: afterBoss.serverEntry,
runewakerNpcId: afterBoss.runewakerNpcId, creatureFamily: afterBoss.creatureFamily } : null;
attempt.targetPositionAfter = afterBoss?.position ?? null;
attempt.targetTransformAfter = afterBoss?.transform ?? null;
attempt.playerOriginBefore = current.player.position ?? null;
attempt.playerCameraYawBefore = current.player.cameraYaw ?? null;
attempt.targetMechanicImmunitiesAfter = afterBoss?.mechanicImmunities ?? [];
attempt.targetSchoolImmunitiesAfter = afterBoss?.schoolImmunities ?? [];
attempt.targetStatusesAfter = afterBoss?.statuses ?? [];
attempt.targetAurasAfter = afterBoss?.auras ?? [];
attempt.castCancelPresentationEvent = cancelEvent ?? null;
attempt.playerVowCombatEvents = vowEvents;
attempt.natureLockedUntil = schoolLock;
attempt.silenceEndsAt = silenceEndsAt;
attempt.silenceDurationMsObserved = silenceEndsAt > 0 ? silenceEndsAt - afterVow.now : 0;
attempt.silenceEvidence = { appliedMobStatus: appliedSilence, appliedMobAura: silenceAura,
schoolLockUntil: schoolLock };
attempt.silenceConfirmed = silenced;
attempt.vowPresentationEvents = afterVow.presentationEvents.filter((event) => event.id > presentationBefore
&& event.actorGroup === 'player' && event.sourceActorId === '__player__'
&& event.abilityId === vowSetup.id);
attempt.presentationEventsAfterInput = afterVow.presentationEvents.filter((event) => event.id > presentationBefore);
attempt.vowEffectEvents = afterVow.events.filter((event) => event.abilityId === vowSetup.id
&& event.sourceActorId === '__player__' && event.occurredAt >= attempt.inputAt);
attempt.confirmedEffectTargets = [{ actorId: afterBoss?.id ?? null,
status: appliedSilence, aura: silenceAura }].filter((effect) => effect.status || effect.aura);
await page.screenshot({ path: path.join(outputDirectory, `lightning-bolt-post-vow-${vowAttempts}.png`) });
current = afterVow;
if (sourceCastEndedOnTime && silenced && vowEvents.some((event) => event.targetActorId === bossId)) {
result.fight.vowSuccess = true;
result.fight.vowSuccessAt = Math.min(...vowEvents.map((event) => event.occurredAt));
result.fight.silenceEndsAt = silenceEndsAt;
result.fight.cancellationEvidence = { liveSourceCastBefore: beforeCast,
activeCastAfter: afterBoss?.activeCast ?? null, sourceCastEndedOnTime,
cancelPresentationEvent: cancelEvent ?? null,
statusAppliedToBoss: appliedSilence, silenceEndsAt };
result.fight.silenceWindow = { startedAt: result.fight.vowSuccessAt, expectedEndsAt: silenceEndsAt,
lightningBoltAttackId: sourceLightning.id,
lightningBoltCountAtStart: afterBoss.abilityCastCounts?.[sourceLightning.id] ?? null,
enemyLightningBoltStarts: [],
statusSamples: [{ at: afterVow.now, active: Boolean(appliedSilence && appliedSilence.endsAt > afterVow.now),
endsAt: appliedSilence?.endsAt ?? silenceEndsAt, activeCast: afterBoss?.activeCast ?? null }],
nonPhysicalCastSamplesDuringActiveStatus: appliedSilence && afterBoss?.activeCast
&& afterBoss.activeCast.school !== 'physical'
? [{ at: afterVow.now, statusEndsAt: appliedSilence.endsAt, activeCast: afterBoss.activeCast }]
: [],
sourceNatureCastResumptionsAfterExpiry: [] };
observedCastCountAtWindowStart = afterBoss.abilityCastCounts?.[sourceLightning.id] ?? null;
await page.keyboard.press('F2');
result.fight.partyDefendInput = { at: Date.now(), key: 'F2', command: 'Defend',
reason: 'hold the actual encounter while observing Vow silence duration' };
}
}
if (result.fight.vowSuccess && current.now < result.fight.silenceEndsAt) {
const silenceSampleBoss = current.mobs.find((mob) => mob.id === bossId);
const silenceSampleCast = silenceSampleBoss?.activeCast;
const activeSilenceStatus = silenceSampleBoss?.statuses.find((status) => status.kind === 'silence'
&& status.sourceAbilityId === 'aura:rom-priest-vow-of-silence' && status.endsAt > current.now) ?? null;
result.fight.silenceWindow.statusSamples.push({ at: current.now,
active: Boolean(activeSilenceStatus), endsAt: activeSilenceStatus?.endsAt ?? null,
activeCast: silenceSampleCast ?? null });
if (activeSilenceStatus && silenceSampleCast && silenceSampleCast.school !== 'physical') {
result.fight.silenceWindow.nonPhysicalCastSamplesDuringActiveStatus.push({ at: current.now,
statusEndsAt: activeSilenceStatus.endsAt, activeCast: silenceSampleCast });
}
const startsDuringSilence = current.presentationEvents.filter((event) => event.actorGroup === 'enemy'
&& event.sourceActorId === bossId && event.abilityId === result.fight.silenceWindow.lightningBoltAttackId
&& event.phase === 'cast-start' && event.occurredAt >= result.fight.vowSuccessAt
&& event.occurredAt < result.fight.silenceEndsAt);
result.fight.silenceWindow.enemyLightningBoltStarts = startsDuringSilence;
const sampleCount = silenceSampleBoss?.abilityCastCounts?.[result.fight.silenceWindow.lightningBoltAttackId] ?? null;
result.fight.silenceWindow.lightningBoltCountAtLastSample = sampleCount;
if (sampleCount !== observedCastCountAtWindowStart || silenceSampleCast?.attackId === result.fight.silenceWindow.lightningBoltAttackId) {
result.fight.silenceWindow.suppressionFailedAt = current.now;
result.fight.outcome = 'lightning-bolt-started-during-silence';
break;
}
const injured = current.party.filter((member) => member.health > 0
&& member.health < member.maxHealth * 0.72 && member.position)
.sort((left, right) => left.health / left.maxHealth - right.health / right.maxHealth)[0] ?? null;
const selfInjured = current.player.health < current.player.maxHealth * 0.72;
if ((injured || selfInjured) && result.actionPlan.healing && Date.now() - nextHealAt > 1_100) {
const self = selfInjured && (!injured || current.player.health / current.player.maxHealth
< injured.health / injured.maxHealth);
if (self) await selectPlayerFrame(page, result.profile.name);
else await selectPartyMember(page, injured);
const cast = await performCast(page, result.actionPlan.healing, 'healing', self ? '__player__' : injured.id,
self ? 'Boss Testing Vow silence window survival' : `Boss Testing triage for ${injured.name}`);
result.fight.healingActions.push(cast);
nextHealAt = Date.now();
}
await page.waitForTimeout(100);
continue;
}
if (result.fight.vowSuccess && !result.fight.silenceWindow.verifiedAt
&& current.now >= result.fight.silenceEndsAt) {
const finalBoss = current.mobs.find((mob) => mob.id === bossId);
const durationStarts = current.presentationEvents.filter((event) => event.actorGroup === 'enemy'
&& event.sourceActorId === bossId && event.abilityId === result.fight.silenceWindow.lightningBoltAttackId
&& event.phase === 'cast-start' && event.occurredAt >= result.fight.vowSuccessAt
&& event.occurredAt < result.fight.silenceEndsAt);
result.fight.silenceWindow.enemyLightningBoltStarts = durationStarts;
result.fight.silenceWindow.lightningBoltCountAtEnd = finalBoss?.abilityCastCounts?.[result.fight.silenceWindow.lightningBoltAttackId] ?? null;
result.fight.silenceWindow.verifiedAt = current.now;
result.fight.silenceWindow.durationMsObserved = Math.min(current.now, result.fight.silenceEndsAt)
- result.fight.vowSuccessAt;
result.fight.silenceWindow.verificationLagMs = current.now - result.fight.silenceEndsAt;
result.fight.silenceWindow.nonPhysicalCastStartsDuringSilence = result.fight.castTimeline.filter(({ activeCast }) => (
activeCast && activeCast.school !== 'physical'
&& activeCast.startedAt >= result.fight.vowSuccessAt
&& activeCast.startedAt < result.fight.silenceEndsAt
));
result.fight.silenceWindow.fullSilenceSuppressionPassed = result.fight.silenceWindow.statusSamples.some((sample) => sample.active)
&& result.fight.silenceWindow.nonPhysicalCastSamplesDuringActiveStatus.length === 0
&& result.fight.silenceWindow.nonPhysicalCastStartsDuringSilence.length === 0;
result.fight.silenceWindow.suppressedNextCast = durationStarts.length === 0
&& result.fight.silenceWindow.lightningBoltCountAtEnd === observedCastCountAtWindowStart;
if (!result.fight.silenceWindow.suppressedNextCast) {
result.fight.outcome = 'lightning-bolt-not-suppressed-through-silence';
break;
}
await page.keyboard.press('F1');
result.fight.partyAttackResumedAt = Date.now();
}
if (result.fight.silenceWindow?.verifiedAt && current.now >= result.fight.silenceEndsAt) {
const expiredWindowBoss = current.mobs.find((mob) => mob.id === bossId);
const resumedCast = expiredWindowBoss?.activeCast;
const resumedAttack = resumedCast && expiredWindowBoss.attacks.find((attack) => attack.id === resumedCast.attackId);
if (resumedCast && resumedCast.startedAt >= result.fight.silenceEndsAt
&& (resumedCast.school === 'nature' || resumedAttack?.school === 'nature')
&& !result.fight.silenceWindow.sourceNatureCastResumptionsAfterExpiry.some((entry) => (
entry.activeCast.attackId === resumedCast.attackId && entry.activeCast.startedAt === resumedCast.startedAt
))) {
result.fight.silenceWindow.sourceNatureCastResumptionsAfterExpiry.push({ at: current.now,
activeCast: resumedCast, sourceAttack: resumedAttack ?? null });
}
}
const injured = current.party.filter((member) => member.health > 0
&& member.health < member.maxHealth * 0.72 && member.position)
.sort((left, right) => left.health / left.maxHealth - right.health / right.maxHealth)[0] ?? null;
const selfInjured = current.player.health < current.player.maxHealth * 0.72;
if ((injured || selfInjured) && result.actionPlan.healing && Date.now() - nextHealAt > 1_100) {
const self = selfInjured && (!injured || current.player.health / current.player.maxHealth
< injured.health / injured.maxHealth);
if (self) await selectPlayerFrame(page, result.profile.name);
else await selectPartyMember(page, injured);
const cast = await performCast(page, result.actionPlan.healing, 'healing', self ? '__player__' : injured.id,
self ? 'Boss Testing live-fight survival' : `Boss Testing triage for ${injured.name}`);
result.fight.healingActions.push(cast);
nextHealAt = Date.now();
continue;
}
if (result.fight.vowSuccess && result.fight.silenceWindow.suppressedNextCast
&& result.actionPlan.damage && current.player.selectedTargetId === bossId
&& !current.player.activeCast && current.player.globalCooldownEndsAt <= current.now
&& Date.now() - nextDamageAt > 1_300) {
const cast = await performCast(page, result.actionPlan.damage, 'damage', bossId,
'normal keyboard damage in Lady Anacondra Boss Testing');
result.fight.damageActions.push(cast);
nextDamageAt = Date.now();
continue;
}
await page.waitForTimeout(90);
}
if (result.fight.outcome === 'in-progress') result.fight.outcome = '80-second-live-fight-budget-ended';
const finalState = await readLiveState(page);
const finalBoss = finalState.mobs.find((mob) => mob.id === bossId) ?? null;
result.fight.endedAt = Date.now();
result.fight.durationMs = result.fight.endedAt - result.fight.startedAt;
result.fight.finalVitals = { player: playerVitals(finalState), party: partyVitals(finalState) };
result.fight.bossAtFinalRead = finalBoss;
result.fight.bossFate = finalBoss?.dead || finalBoss?.health <= 0 ? 'defeated'
: finalBoss ? 'alive-at-budget-end' : 'despawned-or-unavailable';
result.fight.partySurvivors = finalState.party.filter((member) => member.health > 0).map((member) => member.name);
result.fight.partyDeaths = finalState.party.filter((member) => member.health <= 0).map((member) => member.name);
result.fight.playerSurvived = finalState.player.health > 0;
result.fight.finalPlayerTargetId = finalState.player.selectedTargetId;
result.fight.experienceBefore = result.fight.preFight.experience;
result.fight.experienceAfter = finalState.player.experience;
result.fight.noExperienceAwardObserved = finalState.player.experience === result.fight.preFight.experience;
result.fight.noLootObserved = Boolean(finalBoss
&& !finalBoss.hasLoot && finalBoss.pendingLootCount === 0);
result.fight.finalEvents = finalState.events;
await page.screenshot({ path: path.join(outputDirectory, 'fight-end.png') });
return finalState;
}
(async () => {
let runtime = null;
let browser = null;
const pageErrors = [];
const consoleErrors = [];
const assetFiles = fs.readdirSync('dist/assets')
.filter((file) => /^(?:index|GameRuntime|combatStore|partyStore|PartyFrames|romAbilityCatalog)-.*\.js$/.test(file)).sort();
const cssAssetFiles = fs.readdirSync('dist/assets')
.filter((file) => /^index-.*\.css$/.test(file)).sort();
const assetSnapshotAtStart = [...assetFiles, ...cssAssetFiles].map((file) => {
const absolute = path.resolve('dist/assets', file);
const bytes = fs.readFileSync(absolute);
return { file, bytes: bytes.length, sha256: crypto.createHash('sha256').update(bytes).digest('hex') };
});
const moduleUrls = assetFiles.map((file) => `/assets/${file}`);
const result = {
status: 'running',
buildCapturedAtScriptStart: new Date().toISOString(),
snapshotKind: 'final production dist; dedicated Boss Testing fight only',
outputDirectory,
assetSnapshotAtStart,
liveFightBudgetMs,
scope: { mode: 'Boss Testing', boss: 'Lady Anacondra', mechanic: 'Vow of Silence (DBC 490275) interrupts source Lightning Bolt (9532)',
combatStoreWritesDuringLiveFight: false, stagedEnemyCast: false, allowedSetup: ['UI-created RoM Priest', 'level 30', 'RoM secondary class Knight',
'Vow bound through Ability controls UI to Shift+Digit5'], lootExpected: false, readinessChanges: false },
profile: null,
partyAtEntry: [],
vowSetup: null,
actionPlan: null,
fight: { outcome: 'in-progress', vowAttempts: [], vowSuccess: false,
damageActions: [], healingActions: [], samples: [], castTimeline: [], presentationEvents: [], pageErrors: [] },
pageErrors,
consoleErrors,
};
try {
if (assetFiles.length < 3) throw new Error(`Expected final production assets are missing: ${assetFiles.join(', ')}`);
const { createGameServer } = await import('../server/server.mjs');
runtime = createGameServer({ dataDir: dataDirectory, enableShelvedModes: false });
await new Promise((resolve, reject) => {
runtime.server.once('error', reject);
runtime.server.listen(0, '127.0.0.1', resolve);
});
const address = runtime.server.address();
browser = await chromium.launch({ channel: 'msedge', headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
page.setDefaultTimeout(120_000);
page.on('pageerror', (error) => pageErrors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push({ text: message.text(), location: message.location() });
});
await page.addInitScript(({ urls }) => {
window.vowFinalPresentationEvents = [];
window.vowFinalStores = async () => {
const modules = await Promise.all(urls.map((url) => import(url)));
const namedExports = modules.flatMap((module) => Object.entries(module));
const exports = namedExports.map(([, value]) => value);
const stores = exports.filter((value) => value && typeof value.getState === 'function');
const subscribe = namedExports.find(([name, value]) => name === 'subscribeCombatPresentation'
&& typeof value === 'function');
window.vowFinalPresentationExportNames = namedExports.map(([name]) => name)
.filter((name) => name.toLowerCase().includes('presentation'));
if (subscribe && !window.vowFinalUnsubscribe) {
window.vowFinalPresentationSubscriptionFound = true;
window.vowFinalPresentationSubscribeExportName = subscribe[0];
window.vowFinalUnsubscribe = subscribe[1]((message) => {
if (message?.kind === 'event') window.vowFinalPresentationEvents.push(message.event);
if (window.vowFinalPresentationEvents.length > 500) window.vowFinalPresentationEvents.shift();
});
}
const transformExport = namedExports.find(([, value]) => {
if (typeof value !== 'function') return false;
const source = Function.prototype.toString.call(value);
return source.includes('.mounted') && source.includes('.value') && source.includes('position:')
&& source.includes('yaw:') && source.includes('null');
});
const partyPositionExport = namedExports.find(([, value]) => {
if (typeof value !== 'function') return false;
const source = Function.prototype.toString.call(value);
return /\.get\(/.test(source)
&& /return\s+[A-Za-z_$][\w$]*\s*\?\s*\[\s*\.\.\.[A-Za-z_$][\w$]*\s*\]\s*:\s*null/.test(source);
});
const presentationSubscriptionCandidates = namedExports.filter(([, value]) => {
if (typeof value !== 'function') return false;
const source = Function.prototype.toString.call(value);
return source.includes('.add(') && source.includes('.delete(') && source.includes('=>');
}).map(([name, value]) => ({ name, source: Function.prototype.toString.call(value) }));
window.vowFinalPresentationSubscriptionCandidates = presentationSubscriptionCandidates;
return {
getMobPosition: transformExport?.[1] ? (id) => transformExport[1](id)?.position ?? null : null,
getMobTransform: transformExport?.[1] ?? null,
getPartyPosition: partyPositionExport?.[1] ?? null,
game: stores.find((store) => typeof store.getState()?.relocatePlayer === 'function'),
shell: stores.find((store) => typeof store.getState()?.saveActiveProgression === 'function'),
combat: stores.find((store) => typeof store.getState()?.castAbility === 'function'),
party: stores.find((store) => typeof store.getState()?.setCommand === 'function'
&& Array.isArray(store.getState()?.members)),
};
};
}, { urls: moduleUrls });
await page.goto(`http://127.0.0.1:${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: 'RoM' }).click();
await page.locator('.class-option').filter({ hasText: /^Priest/ }).click();
await page.getByRole('button', { name: /Customize & name/ }).click();
await page.getByLabel('Character name').fill('VowLiveFinalQA');
await page.getByRole('button', { name: /Create character/ }).click();
await page.locator('.character-select-screen').waitFor();
const profile = await page.evaluate(async () => {
const { shell } = await window.vowFinalStores();
const state = shell.getState();
const character = state.characters.find((entry) => entry.id === state.selectedCharacterId);
if (!character || character.classId !== 'rom-priest') throw new Error('UI-created RoM Priest profile is missing.');
shell.setState({ activeCharacter: character });
shell.getState().saveActiveProgression(30, 0, character.talentRanks);
shell.setState({ activeCharacter: null });
if (!shell.getState().chooseRomSecondaryClass('rom-knight')) throw new Error('Priest/Knight pairing was not accepted.');
const updated = shell.getState().characters.find((entry) => entry.id === character.id);
return { id: updated.id, name: updated.name, classId: updated.classId,
secondaryClassId: updated.secondaryClassId, level: updated.level, categoryId: updated.categoryId };
});
assert.equal(profile.classId, 'rom-priest');
assert.equal(profile.secondaryClassId, 'rom-knight');
assert.equal(profile.level, 30);
result.profile = profile;
await page.waitForTimeout(1_000);
await page.screenshot({ path: path.join(outputDirectory, 'roster-priest-knight-level-30.png') });
await page.locator('.roster-enter').click();
await page.locator('.main-menu-screen').waitFor();
await page.getByRole('button', { name: /^GM Menu/ }).click();
await page.getByRole('button', { name: /Boss Testing/ }).click();
await page.getByRole('heading', { name: 'Boss Testing', exact: true }).waitFor();
const picker = page.getByLabel(/Choose a boss/);
const option = await picker.locator('option').evaluateAll((options) => options
.find((entry) => entry.textContent.startsWith('Lady Anacondra ·'))?.value ?? null);
assert(option, 'Lady Anacondra appears in the dedicated Boss Testing catalog.');
await picker.selectOption(option);
const selectedArticle = page.locator('.boss-testing-selected');
await selectedArticle.getByRole('heading', { name: 'Lady Anacondra', exact: true }).waitFor();
await page.screenshot({ path: path.join(outputDirectory, 'boss-testing-lady-selection.png') });
await page.getByRole('button', { name: 'Test selected boss', exact: true }).click();
await page.locator('.party-frames').waitFor({ timeout: 180_000 });
await page.locator('.loading-screen').waitFor({ state: 'detached', timeout: 180_000 }).catch(() => {});
await page.waitForFunction(async () => {
const { game, combat } = await window.vowFinalStores();
const current = game.getState();
const boss = current.bossTest;
return current.assetStatus === 'ready' && boss?.name === 'Lady Anacondra'
&& boss.runtimeIds.every((id) => combat.getState().mobs[id]);
}, null, { timeout: 240_000 });
await page.locator('.dungeon-boss-intro').waitFor({ state: 'detached', timeout: 15_000 }).catch(() => {});
await page.evaluate(() => document.activeElement instanceof HTMLElement && document.activeElement.blur());
let entry = await readLiveState(page, true);
assert.equal(entry.game.bossTest?.name, 'Lady Anacondra');
assert.equal(entry.game.gmTestMode, true, 'Selected room is actual Boss Testing, through its UI flow.');
assert.equal(entry.player.classId, 'rom-priest');
assert.equal(entry.player.secondaryClassId, 'rom-knight');
assert.equal(entry.party.length, 4, 'Boss Testing generated four owned NPC companions.');
assert(entry.party.every((member) => member.classId.startsWith('rom-') && member.statRuleset === 'rom'),
'All four NPCs have native RoM identities.');
result.partyAtEntry = partyVitals(entry);
result.partyIdentityAtEntry = entry.party.map((member) => ({ id: member.id, name: member.name,
classId: member.classId, secondaryClassId: member.secondaryClassId, role: member.role,
statRuleset: member.statRuleset }));
await page.screenshot({ path: path.join(outputDirectory, 'lady-anacondra-before-vow.png') });
result.vowSetup = await assignVowThroughAbilityControls(page, outputDirectory);
entry = await readLiveState(page, true);
result.actionPlan = actionPlanFromSnapshot(entry, result.vowSetup.id);
if (!result.actionPlan.healing) throw new Error('No normal keyboard-bound Priest healing action is available.');
const boss = entry.mobs.find((mob) => mob.id === entry.game.bossTest.runtimeIds[0]);
if (!boss || !boss.boss || boss.name !== 'Lady Anacondra') throw new Error('Boss Testing did not spawn the expected boss.');
result.fight.preFight = { at: entry.now, bossId: boss.id, boss: { id: boss.id, name: boss.name,
level: boss.level, health: boss.health, maxHealth: boss.maxHealth,
attacks: boss.attacks, abilityCastCounts: boss.abilityCastCounts },
player: playerVitals(entry), party: partyVitals(entry), game: entry.game,
experience: entry.player.experience };
await page.screenshot({ path: path.join(outputDirectory, 'lady-anacondra-fight-start.png') });
await runFight(page, result, result.vowSetup, entry);
assert.equal(result.fight.vowSuccess, true,
'A live scheduled Lightning Bolt ended early and Vow applied its full-duration silence status.');
assert.equal(result.fight.silenceWindow.suppressedNextCast, true,
'No subsequent Lightning Bolt began during the observed silence window.');
assert.equal(result.fight.silenceWindow.fullSilenceSuppressionPassed, true,
'No nonphysical Lady Anacondra cast became active during Vow silence.');
assert.equal(result.fight.noLootObserved, true, 'Boss Testing did not create boss loot.');
assert.equal(result.fight.noExperienceAwardObserved, true, 'Boss Testing did not award XP.');
assert.deepEqual(pageErrors, [], `No browser page exceptions: ${JSON.stringify(pageErrors)}`);
result.status = 'complete';
result.pageErrors = pageErrors;
result.consoleErrors = consoleErrors;
result.fight.pageErrors = pageErrors;
result.completedAt = new Date().toISOString();
fs.writeFileSync(path.join(outputDirectory, 'results.json'), `${JSON.stringify(result, null, 2)}\n`);
console.log(`Live Vow of Silence QA complete: ${outputDirectory}`);
} catch (error) {
result.status = 'failed';
result.error = error.stack ?? error.message;
result.pageErrors = pageErrors;
result.consoleErrors = consoleErrors;
result.fight.pageErrors = pageErrors;
result.completedAt = new Date().toISOString();
if (browser) {
const contexts = browser.contexts();
const page = contexts[0]?.pages()[0];
if (page) await page.screenshot({ path: path.join(outputDirectory, 'failure.png') }).catch(() => {});
if (page) result.failureBodyText = (await page.locator('body').innerText().catch(() => '')).slice(-3_000);
}
fs.writeFileSync(path.join(outputDirectory, 'results.json'), `${JSON.stringify(result, null, 2)}\n`);
console.error(`Live Vow of Silence QA failed: ${outputDirectory}`);
throw error;
} finally {
if (browser) await browser.close();
if (runtime) await new Promise((resolve) => runtime.server.close(resolve));
const target = fs.realpathSync(dataDirectory);
if (path.dirname(target) !== fs.realpathSync(os.tmpdir())
|| !path.basename(target).startsWith('healer-man-vow-live-final-')) {
throw new Error('Unsafe temporary playtest database cleanup path.');
}
fs.rmSync(target, { recursive: true, force: true });
}
})().catch((error) => {
console.error(error.stack ?? error.message);
process.exitCode = 1;
});