forked from phenom/healer-man
80 lines
3.5 KiB
JavaScript
80 lines
3.5 KiB
JavaScript
// This self-contained factory also runs in the browser's QA context. It only
|
|
// subscribes to committed state; it never writes the game stores.
|
|
function createWeaponEvidenceRecorder(store, options = {}) {
|
|
const capacity = options.capacity ?? 50_000;
|
|
const clock = options.clock ?? Date.now;
|
|
const initial = store.getState();
|
|
const baselineEventId = initial.combatEvents?.at(-1)?.id ?? 0;
|
|
let lastEventId = baselineEventId;
|
|
let stopped = false;
|
|
const events = [], gaps = [], mobSamples = [], auraSamples = [], errors = [];
|
|
const signatures = new Map();
|
|
let auraSignature = null;
|
|
let discardedEvents = 0, discardedStates = 0;
|
|
const retain = (array, entry, event = false) => {
|
|
if (array.length < capacity) array.push(entry);
|
|
else if (event) discardedEvents += 1;
|
|
else discardedStates += 1;
|
|
};
|
|
const observe = (state) => {
|
|
if (stopped) return;
|
|
try {
|
|
const fresh = (state.combatEvents ?? []).filter((event) => event.id > lastEventId);
|
|
for (const event of fresh) {
|
|
if (event.id > lastEventId + 1) gaps.push({ afterId: lastEventId, beforeId: event.id });
|
|
retain(events, { ...event }, true);
|
|
lastEventId = event.id;
|
|
}
|
|
const observedAt = clock();
|
|
for (const mob of Object.values(state.mobs ?? {})) {
|
|
const entry = {
|
|
id: mob.id, name: mob.name, serverEntry: mob.serverEntry ?? null,
|
|
sourceCreatureGuid: mob.sourceCreatureGuid ?? null,
|
|
sourceEquipment: mob.sourceEquipment ? JSON.parse(JSON.stringify(mob.sourceEquipment)) : null,
|
|
engaged: Boolean(mob.engaged), dead: Boolean(mob.dead),
|
|
dualWieldEnabled: mob.dualWieldEnabled ?? null,
|
|
nextEnemyOffhandAttackAt: mob.nextEnemyOffhandAttackAt ?? null,
|
|
pendingExtraAttacks: mob.pendingExtraAttacks?.map((queued) => ({ ...queued })) ?? null,
|
|
lastDamagedActorId: mob.lastDamagedActorId ?? null,
|
|
};
|
|
const signature = JSON.stringify(entry);
|
|
if (signatures.get(mob.id) !== signature) {
|
|
signatures.set(mob.id, signature);
|
|
retain(mobSamples, { observedAt, ...entry });
|
|
}
|
|
}
|
|
const active = (state.auras ?? []).filter((aura) =>
|
|
/^(?:enemy-source-spell-(?:6742|6713|17230|3419)|enemy-control:source-spell-(?:5246|19134|20511|29544))$/.test(aura.definition?.id ?? '')).map((aura) => ({
|
|
instanceId: aura.instanceId, definitionId: aura.definition.id,
|
|
sourceId: aura.sourceId, targetId: aura.targetId,
|
|
appliedAt: aura.appliedAt, expiresAt: aura.expiresAt, stacks: aura.stacks,
|
|
dispelCategory: aura.definition.dispelCategory ?? null,
|
|
}));
|
|
const nextAuraSignature = JSON.stringify(active);
|
|
if (nextAuraSignature !== auraSignature) {
|
|
auraSignature = nextAuraSignature;
|
|
retain(auraSamples, { observedAt, auras: active });
|
|
}
|
|
} catch (error) {
|
|
if (errors.length < 10) errors.push(String(error?.message ?? error));
|
|
}
|
|
};
|
|
observe(initial);
|
|
const unsubscribe = store.subscribe(observe);
|
|
return {
|
|
stop() {
|
|
if (!stopped) {
|
|
observe(store.getState());
|
|
stopped = true;
|
|
unsubscribe();
|
|
}
|
|
return { baselineEventId, lastEventId, events, gaps, mobSamples, auraSamples,
|
|
discardedEvents, discardedStates, errors,
|
|
eventCoverage: 'Committed-store subscription with event-ID gap auditing; pre-start events excluded.',
|
|
stateCoverage: 'Weapon and source-aura changes at store commits, including combat between route checkpoints.' };
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = { createWeaponEvidenceRecorder };
|