// 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; });