Files
healer-man/scripts/playtest-stockade-enemy-mechanics.cjs
T
2026-10-06 22:25:34 -04:00

532 lines
34 KiB
JavaScript

const { spawnSync } = require('node:child_process');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const {
FULL_STOCKADE_DUNGEON_ID,
FULL_STOCKADE_DUNGEON_TITLE,
FULL_STOCKADE_BOSS,
} = require('./playtest-defaults.cjs');
const projectRoot = path.resolve(__dirname, '..');
const contentSourceDirectory = path.join(projectRoot, 'content');
const contentManifestPath = path.join(contentSourceDirectory, 'manifest.json');
const sourceHarnessPath = path.join(projectRoot, 'scripts', 'playtest-campaign-boss-pass.cjs');
const sourceHarness = fs.readFileSync(sourceHarnessPath, 'utf8');
const prepareOnly = process.argv.includes('--prepare-only');
const frozenBuildAuthorized = process.argv.includes('--frozen-build-authorized');
const runId = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', '');
const outputDirectory = path.join(projectRoot, 'playtest-artifacts', 'stockade-enemy-mechanics', runId);
const childOutputDirectory = path.join(outputDirectory, 'campaign');
const runtimeHarnessPath = path.join(__dirname, `.playtest-stockade-enemy-mechanics-${process.pid}.cjs`);
const requestedBuildDirectory = process.env.HEALER_MAN_STOCKADE_DIST?.trim()
? path.resolve(process.env.HEALER_MAN_STOCKADE_DIST.trim()) : null;
const buildDirectory = requestedBuildDirectory ?? path.join(projectRoot, 'dist');
const expectedBuildSha256Argument = process.argv.find((value) => value.startsWith('--expected-build-sha256='))
?.slice('--expected-build-sha256='.length);
const expectedBuildSha256 = expectedBuildSha256Argument
?? process.env.HEALER_MAN_STOCKADE_DIST_SHA256?.trim() ?? null;
function sha256(bytes) {
return crypto.createHash('sha256').update(bytes).digest('hex');
}
function replaceExactlyOnce(source, marker, replacement, label) {
const count = source.split(marker).length - 1;
if (count !== 1) throw new Error(`${label} must occur exactly once in the source harness; found ${count}.`);
return source.replace(marker, replacement);
}
function collectBuildManifest() {
if (!fs.existsSync(buildDirectory) || !fs.statSync(buildDirectory).isDirectory()) {
throw new Error(`Frozen build directory is missing or is not a directory: ${buildDirectory}`);
}
const files = [];
function visit(directory, relativeDirectory = '') {
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const relativePath = path.posix.join(relativeDirectory, entry.name);
const absolutePath = path.join(directory, entry.name);
if (entry.isDirectory()) visit(absolutePath, relativePath);
else if (entry.isFile()) {
const bytes = fs.readFileSync(absolutePath);
files.push({ path: relativePath, bytes: bytes.length, sha256: sha256(bytes) });
}
}
}
visit(buildDirectory);
const canonicalManifest = files.map((file) => `${file.path}\t${file.bytes}\t${file.sha256}\n`).join('');
const bundleModuleReport = files.find((file) => file.path === 'bundle-module-report.json');
return {
capturedAt: new Date().toISOString(),
directory: buildDirectory,
fileCount: files.length,
aggregateSha256: sha256(canonicalManifest),
bundleModuleReportSha256: bundleModuleReport?.sha256 ?? null,
files,
};
}
function collectContentManifestSnapshot() {
const bytes = fs.readFileSync(contentManifestPath);
const manifest = JSON.parse(bytes.toString('utf8'));
return {
capturedAt: new Date().toISOString(),
directory: contentSourceDirectory,
manifestPath: contentManifestPath,
manifestBytes: bytes.length,
manifestSha256: sha256(bytes),
manifestVersion: manifest.version ?? null,
schemaVersion: manifest.schemaVersion ?? null,
packIds: Array.isArray(manifest.packs) ? manifest.packs.map((pack) => pack.id).sort() : [],
fileCount: manifest.files && typeof manifest.files === 'object' ? Object.keys(manifest.files).length : null,
};
}
function collectLoadedContentObjectEvidence(responses) {
const objectResponses = responses.filter((response) => response?.pathname?.startsWith('/content/objects/'));
const responseCountsByPath = objectResponses.reduce((counts, response) => {
counts.set(response.pathname, (counts.get(response.pathname) ?? 0) + 1);
return counts;
}, new Map());
const uniqueObjects = [...new Map(objectResponses.map((response) => [response.pathname, {
...response,
responseCount: responseCountsByPath.get(response.pathname),
}])).values()];
return uniqueObjects.map((response) => {
const relativeObjectPath = decodeURIComponent(response.pathname.slice('/content/'.length));
const absoluteObjectPath = path.resolve(contentSourceDirectory, relativeObjectPath);
const relativeCheck = path.relative(contentSourceDirectory, absoluteObjectPath);
if (relativeCheck.startsWith('..') || path.isAbsolute(relativeCheck)) {
return { ...response, localObjectPath: null, localBytes: null, localSha256: null,
objectIntegrity: 'unsafe-path-refused' };
}
if (!fs.existsSync(absoluteObjectPath) || !fs.statSync(absoluteObjectPath).isFile()) {
return { ...response, localObjectPath: path.relative(projectRoot, absoluteObjectPath),
localBytes: null, localSha256: null, objectIntegrity: 'not-found-on-content-source' };
}
const bytes = fs.readFileSync(absoluteObjectPath);
const actualSha256 = sha256(bytes);
const urlObjectSha256 = /^([a-f0-9]{64})(?:\.|$)/i.exec(path.basename(absoluteObjectPath))?.[1]?.toLowerCase() ?? null;
return {
...response,
localObjectPath: path.relative(projectRoot, absoluteObjectPath),
localBytes: bytes.length,
localSha256: actualSha256,
urlObjectSha256,
objectIntegrity: response.status === 200 && urlObjectSha256
? actualSha256 === urlObjectSha256 ? 'served-file-matches-hash-address' : 'served-file-hash-mismatch'
: response.status === 200 ? 'served-without-hash-address' : 'http-not-successful',
};
});
}
function createInstrumentedHarness() {
if (FULL_STOCKADE_DUNGEON_ID !== 'stormwind-stockade'
|| FULL_STOCKADE_DUNGEON_TITLE !== 'Stormwind Stockade') {
throw new Error(`Full Stockade fixture changed unexpectedly: ${FULL_STOCKADE_DUNGEON_ID} / ${FULL_STOCKADE_DUNGEON_TITLE}`);
}
const mobMarker = ' schoolLockedUntil: mob.schoolLockedUntil ?? {},';
const outputMarker = "const artifactRoot = path.resolve('playtest-artifacts/campaign-boss-pass');";
const runtimeRelativeOutput = path.relative(projectRoot, childOutputDirectory).replace(/\\/g, '/');
let transformed = sourceHarness;
transformed = replaceExactlyOnce(transformed, outputMarker,
`const artifactRoot = path.resolve(${JSON.stringify(runtimeRelativeOutput)});`, 'Campaign output directory');
transformed = replaceExactlyOnce(transformed, mobMarker, ` attacks: (mob.attacks ?? []).map((attack) => ({
id: attack.id, name: attack.name, spellId: attack.spellId ?? null,
school: attack.school ?? null, castTimeMs: attack.castTimeMs ?? 0,
cooldownMs: attack.cooldownMs ?? 0, interruptible: attack.interruptible !== false,
})),
auras: mob.auras ?? [],
combatAuraTelemetry: (combatState.auras ?? []).filter((aura) => aura.targetId === mob.id).map((aura) => ({
instanceId: aura.instanceId ?? null,
definitionId: aura.definition?.id ?? null,
definitionName: aura.definition?.name ?? null,
sourceId: aura.sourceId ?? null,
targetId: aura.targetId ?? null,
appliedAt: aura.appliedAt ?? null,
expiresAt: aura.expiresAt ?? null,
stacks: aura.stacks ?? null,
definition: aura.definition ?? null,
})),
schoolLockedUntil: mob.schoolLockedUntil ?? {},`, 'Mob telemetry projection');
const distPathReplacements = [
["fs.readFileSync('dist/bundle-module-report.json', 'utf8')",
"fs.readFileSync(path.join(process.env.HEALER_MAN_STOCKADE_DIST, 'bundle-module-report.json'), 'utf8')",
'bundle module report path'],
["path.join('dist/assets', dungeonRunStoreAssetFile)",
"path.join(process.env.HEALER_MAN_STOCKADE_DIST, 'assets', dungeonRunStoreAssetFile)",
'dungeon run module verification path'],
["fs.readdirSync('dist/assets')",
"fs.readdirSync(path.join(process.env.HEALER_MAN_STOCKADE_DIST, 'assets'))",
'module asset enumeration path'],
["const runtime = createGameServer({ dataDir: dataDirectory, enableShelvedModes: false });",
"const runtime = createGameServer({ dataDir: dataDirectory, staticDir: process.env.HEALER_MAN_STOCKADE_DIST, contentDir: path.resolve('content'), enableShelvedModes: false });\n if (path.resolve(runtime.staticDir) !== path.resolve(process.env.HEALER_MAN_STOCKADE_DIST)) {\n throw new Error(`Playtest server resolved an unexpected static directory: ${runtime.staticDir}`);\n }\n if (path.resolve(runtime.contentDir) !== path.resolve('content')) {\n throw new Error(`Playtest server resolved an unexpected read-only content source: ${runtime.contentDir}`);\n }",
'static server document root'],
[" const pageErrors = [];\n const consoleErrors = [];",
" const pageErrors = [];\n const consoleErrors = [];\n const contentSourceResponses = [];",
'content source response telemetry'],
[" page.on('pageerror', (error) => pageErrors.push(error.message));",
" page.on('pageerror', (error) => pageErrors.push(error.message));\n page.on('response', (response) => {\n try {\n const url = new URL(response.url());\n if (url.pathname !== '/content/manifest.json' && !url.pathname.startsWith('/content/objects/')) return;\n const headers = response.headers();\n contentSourceResponses.push({\n observedAt: new Date().toISOString(),\n method: response.request().method(),\n url: response.url(),\n pathname: url.pathname,\n status: response.status(),\n contentType: headers['content-type'] ?? null,\n contentLength: headers['content-length'] ?? null,\n contentEncoding: headers['content-encoding'] ?? null,\n cacheControl: headers['cache-control'] ?? null,\n });\n } catch (error) {\n contentSourceResponses.push({ observedAt: new Date().toISOString(), instrumentationError: String(error) });\n }\n });",
'content source response listener'],
["Expected built game assets were not found in dist/assets: ${moduleFiles.join(', ')}",
"Expected built game assets were not found in the frozen Stockade distribution: ${moduleFiles.join(', ')}",
'asset failure message'],
];
for (const [marker, replacement, label] of distPathReplacements) {
transformed = replaceExactlyOnce(transformed, marker, replacement, label);
}
transformed = replaceExactlyOnce(transformed,
' const routeResult = await runRouteSegment(page, route, entry, profile);',
' const routeResult = await runRouteSegment(page, route, entry, profile);\n routeResult.contentSourceResponses = contentSourceResponses.slice();',
'route content source evidence');
transformed = replaceExactlyOnce(transformed,
' activeCast: mob.activeCast, position: mob.position })) });',
' activeCast: mob.activeCast, attacks: mob.attacks ?? [], auras: mob.auras ?? [],\n combatAuraTelemetry: mob.combatAuraTelemetry ?? [], abilityCastCounts: mob.abilityCastCounts ?? {},\n statuses: mob.statuses ?? [], position: mob.position })) });',
'compact route sample enemy mechanics telemetry');
transformed = replaceExactlyOnce(transformed,
' const beforePosition = current.player.position;\n const beforeDistance = distance3(beforePosition, checkpoint.position);\n const planarRemaining = distancePlanar(beforePosition, checkpoint.position);\n const speedEstimate = ordinaryMovementSpeedEstimate(result.movement);\n const durationMs = Math.max(45, Math.min(350, Math.floor(planarRemaining * 0.58 / speedEstimate * 1_000)));\n const moved = await walkToward(page, result, checkpoint.position, durationMs);\n if (!moved) {\n routeBlocked = { kind: \'no-directional-input\', checkpointIndex, checkpoint, position: current.player.position };',
' const beforePosition = current.player.position;\n const beforeDistance = distance3(beforePosition, checkpoint.position);\n const planarRemaining = distancePlanar(beforePosition, checkpoint.position);\n const speedEstimate = ordinaryMovementSpeedEstimate(result.movement);\n const recentResponseScales = result.movement.slice(-8)\n .filter((step) => Number(step.requestedDurationMs) > 0 && Number(step.durationMs) >= Number(step.requestedDurationMs)\n && Number(step.planarDisplacement) >= 0.08)\n .map((step) => Number(step.durationMs) / Number(step.requestedDurationMs)).sort((left, right) => left - right);\n const movementResponseScale = recentResponseScales.length\n ? recentResponseScales[Math.floor(recentResponseScales.length / 2)] : 1;\n const plannedDurationMs = planarRemaining * 0.58 / speedEstimate * 1_000 / movementResponseScale;\n const nearCheckpointLimitMs = planarRemaining < 1.5 ? 55 : 350;\n const durationMs = Math.max(25, Math.min(nearCheckpointLimitMs, 350, Math.floor(plannedDurationMs)));\n const moved = await walkToward(page, result, checkpoint.position, durationMs);\n if (!moved) {\n routeBlocked = { kind: \'no-directional-input\', checkpointIndex, checkpoint, position: current.player.position };',
'adaptive ordinary movement timing from measured response');
transformed = replaceExactlyOnce(transformed,
' buildAssets: moduleFiles, error: error.stack ?? error.message, pageErrors, consoleErrors,',
' buildAssets: moduleFiles, error: error.stack ?? error.message, pageErrors, consoleErrors,\n contentSourceResponses,',
'failure content source evidence');
if (/['"`]dist\/(?:assets|bundle-module-report)/.test(transformed)) {
throw new Error('A hard-coded campaign harness distribution path survived frozen-build instrumentation.');
}
return transformed;
}
function summarizeEnemyEvidence(route) {
const mobSamples = [];
const rawCombatEvents = route.observedCombatEvents ?? [];
function visit(value, sourcePath = '$') {
if (!value || typeof value !== 'object') return;
if (Array.isArray(value.mobs)) {
const at = value.at ?? value.now ?? value.observedAt ?? null;
for (const mob of value.mobs) {
if (!mob || typeof mob !== 'object') continue;
mobSamples.push({
sampledAt: at,
sourcePath,
id: mob.id ?? null,
name: mob.name ?? null,
serverEntry: mob.serverEntry ?? null,
health: mob.health ?? null,
maxHealth: mob.maxHealth ?? null,
dead: mob.dead ?? null,
engaged: mob.engaged ?? null,
activeCast: mob.activeCast ?? null,
abilityCastCounts: mob.abilityCastCounts ?? null,
attacks: mob.attacks ?? [],
auras: mob.auras ?? [],
combatAuraTelemetry: mob.combatAuraTelemetry ?? [],
statuses: mob.statuses ?? [],
});
}
}
if (Array.isArray(value)) value.forEach((child, index) => visit(child, `${sourcePath}[${index}]`));
else for (const [key, child] of Object.entries(value)) visit(child, `${sourcePath}.${key}`);
}
visit(route);
const enemyActorIds = new Set(mobSamples.map((sample) => sample.id).filter(Boolean));
const enemyNameById = new Map(mobSamples.filter((sample) => sample.id && sample.name)
.map((sample) => [sample.id, sample.name]));
const combatAuraSamples = mobSamples.filter((sample) => sample.combatAuraTelemetry.length > 0)
.map((sample) => ({ sampledAt: sample.sampledAt, sourcePath: sample.sourcePath,
enemyId: sample.id, enemyName: sample.name, auras: sample.combatAuraTelemetry }));
const enemyBuffAuraApplications = [...new Map(combatAuraSamples.flatMap((sample) => sample.auras
.filter((aura) => enemyActorIds.has(aura.sourceId) && enemyActorIds.has(aura.targetId)
&& aura.definition?.disposition === 'buff')
.map((aura) => [aura.instanceId, {
instanceId: aura.instanceId,
definitionId: aura.definitionId,
definitionName: aura.definitionName,
sourceId: aura.sourceId,
sourceName: enemyNameById.get(aura.sourceId) ?? null,
targetId: aura.targetId,
targetName: enemyNameById.get(aura.targetId) ?? null,
appliedAt: aura.appliedAt,
expiresAt: aura.expiresAt,
stacks: aura.stacks,
}]))).filter(([instanceId]) => Boolean(instanceId))].map(([, aura]) => aura);
const enemyDamageEvents = rawCombatEvents.filter((event) => enemyActorIds.has(event.sourceActorId)
&& event.kind === 'damage').map((event) => ({ ...event,
sourceEnemyName: enemyNameById.get(event.sourceActorId) ?? null }));
const rendDamageEvents = enemyDamageEvents.filter((event) => /(?:^|\D)(?:6547|16509)(?:\D|$)/
.test(String(event.abilityId ?? event.spellId ?? event.attackId ?? ''))
|| /rend/i.test(String(event.abilityName ?? event.name ?? '')));
const chainLightningCastSamples = mobSamples.filter((sample) => sample.name === FULL_STOCKADE_BOSS
&& sample.activeCast && (Number(sample.activeCast.spellId) === 421
|| /(?:^|\D)421(?:\D|$)/.test(String(sample.activeCast.attackId ?? ''))
|| /chain\s*lightning/i.test(String(sample.activeCast.name ?? ''))));
const rendCastSamples = mobSamples.filter((sample) => sample.activeCast
&& (Number(sample.activeCast.spellId) === 6547 || Number(sample.activeCast.spellId) === 16509
|| /rend/i.test(String(sample.activeCast.name ?? ''))
|| /(?:^|\D)(?:6547|16509)(?:\D|$)/.test(String(sample.activeCast.attackId ?? ''))));
const demoralizingShoutCastWindows = [...new Map(mobSamples.filter((sample) => sample.activeCast
&& (Number(sample.activeCast.spellId) === 13730 || Number(sample.activeCast.spellId) === 16244
|| /demoralizing\s*shout/i.test(String(sample.activeCast.name ?? '')))
).map((sample) => [`${sample.id}|${sample.activeCast.attackId}|${sample.activeCast.startedAt}`, {
enemyId: sample.id,
enemyName: sample.name,
attackId: sample.activeCast.attackId ?? null,
spellId: sample.activeCast.spellId ?? null,
startedAt: sample.activeCast.startedAt ?? null,
completesAt: sample.activeCast.completesAt ?? null,
interruptible: sample.activeCast.interruptible ?? null,
}])).values()];
const rendAttacks = mobSamples.flatMap((sample) => (sample.attacks ?? []).filter((attack) => (
Number(attack.spellId) === 6547 || Number(attack.spellId) === 16509
|| /rend/i.test(String(attack.name ?? ''))
|| /(?:^|\D)(?:6547|16509)(?:\D|$)/.test(String(attack.id ?? ''))
)).map((attack) => ({ enemyId: sample.id, enemyName: sample.name, ...attack })));
const enemyAuraObservations = mobSamples.filter((sample) => Array.isArray(sample.auras) && sample.auras.length > 0)
.map((sample) => ({ sampledAt: sample.sampledAt, sourcePath: sample.sourcePath,
enemyId: sample.id, enemyName: sample.name, auras: sample.auras }));
return {
definitions: {
rawCombatEvents: 'Unmodified combatStore event objects collected by the campaign harness; timestamps and amount fields retain source values.',
enemyDamageEvents: 'Raw damage events whose source actor id appeared in observed mob snapshots.',
rendDamageEvents: 'Raw enemy damage events whose source ability id/name matches Rend 6547 or 16509.',
mobSamples: 'Read-only snapshots extended with each mob attack definition and mob aura state.',
combatAuraSamples: 'Read-only combatStore auras filtered to the sampled mob id, retaining definition, owner, source, apply, expiry, and stack fields.',
enemyBuffAuraApplications: 'Unique enemy-source buff auras whose source and target actor ids both appeared in sampled mobs; deduplicated by aura instance id.',
chainLightningMatch: 'Hamhock active cast matches spellId 421, attackId containing 421, or source cast name Chain Lightning.',
rendCastMatch: 'Mob active cast matches spellId 6547/16509, attackId containing either ID, or source cast name Rend.',
demoralizingShoutCastWindows: 'Unique active cast windows for enemy spell 13730/16244 or a Demoralizing Shout-named cast, keyed by enemy id, attack id, and startedAt.',
rendMatch: 'Mob attack definition matches spellId 6547/16509, attack name Rend, or attack id containing one of those IDs.',
},
counts: {
mobSamples: mobSamples.length,
rawCombatEvents: rawCombatEvents.length,
enemyDamageEvents: enemyDamageEvents.length,
rendDamageEvents: rendDamageEvents.length,
chainLightningCastSamples: chainLightningCastSamples.length,
rendCastSamples: rendCastSamples.length,
demoralizingShoutCastWindows: demoralizingShoutCastWindows.length,
rendAttackDefinitions: rendAttacks.length,
enemyAuraObservations: enemyAuraObservations.length,
combatAuraSamples: combatAuraSamples.length,
enemyBuffAuraApplications: enemyBuffAuraApplications.length,
},
enemyDamageEvents,
rendDamageEvents,
chainLightningCastSamples,
rendCastSamples,
demoralizingShoutCastWindows,
rendAttackDefinitions: [...new Map(rendAttacks.map((attack) => [
`${attack.enemyId}|${attack.id}|${attack.spellId}`, attack,
])).values()],
enemyAuraObservations,
combatAuraSamples,
enemyBuffAuraApplications,
mobSamples,
rawCombatEvents,
};
}
function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
function main() {
const instrumentedHarness = createInstrumentedHarness();
fs.writeFileSync(runtimeHarnessPath, instrumentedHarness, 'utf8');
try {
const syntax = spawnSync(process.execPath, ['--check', runtimeHarnessPath], {
cwd: projectRoot,
encoding: 'utf8',
windowsHide: true,
});
if (syntax.status !== 0) throw new Error(`Instrumented campaign harness syntax check failed: ${syntax.stderr || syntax.error}`);
if (prepareOnly) {
console.log('Prepared scoped Stockade enemy-mechanics harness; source and transformed syntax are valid; no distribution scan, server, or browser was started.');
return;
}
if (!process.argv.includes('--full-stockade')) {
throw new Error('This source-specific full-campaign harness requires explicit --full-stockade opt-in.');
}
if (!frozenBuildAuthorized) {
throw new Error('Live mode is locked until the owner authorizes the frozen build. Use --prepare-only for static preparation.');
}
if (!requestedBuildDirectory) {
throw new Error('Live mode requires HEALER_MAN_STOCKADE_DIST to point to the immutable task-specific distribution.');
}
if (!expectedBuildSha256 || !/^[a-f0-9]{64}$/i.test(expectedBuildSha256)) {
throw new Error('Live mode requires --expected-build-sha256=<64 hex chars> or HEALER_MAN_STOCKADE_DIST_SHA256.');
}
if (!fs.existsSync(requestedBuildDirectory) || !fs.statSync(requestedBuildDirectory).isDirectory()) {
throw new Error(`Task-specific frozen distribution is missing: ${requestedBuildDirectory}`);
}
const projectDistRealPath = fs.realpathSync(path.join(projectRoot, 'dist'));
const requestedDistRealPath = fs.realpathSync(requestedBuildDirectory);
if (requestedDistRealPath === projectDistRealPath) {
throw new Error('Live mode requires the isolated frozen distribution copy, not the mutable project dist directory.');
}
fs.mkdirSync(outputDirectory, { recursive: true });
const beforeManifest = collectBuildManifest();
const contentManifestBefore = collectContentManifestSnapshot();
writeJson(path.join(outputDirectory, 'build-manifest-before.json'), beforeManifest);
writeJson(path.join(outputDirectory, 'content-manifest-before.json'), contentManifestBefore);
const runRecord = {
status: 'running',
runId,
startedAt: new Date().toISOString(),
dungeon: { id: FULL_STOCKADE_DUNGEON_ID, title: FULL_STOCKADE_DUNGEON_TITLE },
profilePlan: { creation: 'normal offline character-creation UI', classId: 'rom-priest',
secondaryClassId: 'rom-knight', isolatedLevel: 30, pairingSetup: 'existing campaign harness shell profile setup before dungeon entry' },
entryAndPlay: 'normal full Stockade picker and healer-role entry; campaign harness ordinary WASD, target selection, attack/support healing/party commands; no Boss Testing, teleport, collision bypass, enemy/damage/aura/health/cast seeding, or combat store writes',
campaignHarness: path.relative(projectRoot, sourceHarnessPath),
instrumentation: ['mob attack definitions', 'mob auras', 'combat auras filtered to each enemy id with definitions/owners/timestamps/stacks', 'raw observed combat events'],
requestedMaximumCampaignSeconds: 1800,
frozenBuildDirectory: requestedDistRealPath,
expectedBuildSha256: expectedBuildSha256.toLowerCase(),
readOnlyContentSourceDirectory: contentSourceDirectory,
contentManifestBeforeSha256: contentManifestBefore.manifestSha256,
childOutputDirectory: path.relative(projectRoot, childOutputDirectory),
buildManifestBeforeSha256: beforeManifest.aggregateSha256,
loadedBuildAssets: [],
statusNotes: [],
};
writeJson(path.join(outputDirectory, 'run-record.json'), runRecord);
if (beforeManifest.aggregateSha256.toLowerCase() !== expectedBuildSha256.toLowerCase()) {
runRecord.status = 'not-started-frozen-build-fingerprint-mismatch';
runRecord.executionStatus = 'browser-not-started';
runRecord.statusNotes.push('Expected aggregate SHA-256 did not match the supplied frozen distribution; no game server or browser was started.');
writeJson(path.join(outputDirectory, 'run-record.json'), runRecord);
console.error(`Frozen-build fingerprint mismatch; preserved preflight evidence at ${path.relative(projectRoot, outputDirectory)}`);
process.exitCode = 1;
return;
}
const child = spawnSync(process.execPath, [runtimeHarnessPath,
'--campaign-boss-route', `--route=${FULL_STOCKADE_DUNGEON_ID}`, '--max-campaign-seconds=1800'], {
cwd: projectRoot,
env: { ...process.env, HEALER_MAN_STOCKADE_DIST: requestedDistRealPath },
stdio: 'inherit',
timeout: 2_100_000,
windowsHide: true,
});
const afterManifest = collectBuildManifest();
const contentManifestAfter = collectContentManifestSnapshot();
writeJson(path.join(outputDirectory, 'build-manifest-after.json'), afterManifest);
writeJson(path.join(outputDirectory, 'content-manifest-after.json'), contentManifestAfter);
const candidateDirectories = fs.existsSync(childOutputDirectory)
? fs.readdirSync(childOutputDirectory, { withFileTypes: true })
.filter((entry) => entry.isDirectory()).map((entry) => path.join(childOutputDirectory, entry.name)).sort()
: [];
const campaignDirectory = candidateDirectories.at(-1) ?? null;
const routePath = campaignDirectory
? path.join(campaignDirectory, `${FULL_STOCKADE_DUNGEON_ID}-campaign-boss-route.json`) : null;
const routeResultPath = campaignDirectory
? path.join(campaignDirectory, `${FULL_STOCKADE_DUNGEON_ID}-route.json`) : null;
const resultsPath = campaignDirectory ? path.join(campaignDirectory, 'results.json') : null;
const route = routePath && fs.existsSync(routePath) ? JSON.parse(fs.readFileSync(routePath, 'utf8')) : null;
const routeResult = routeResultPath && fs.existsSync(routeResultPath)
? JSON.parse(fs.readFileSync(routeResultPath, 'utf8')) : null;
const results = resultsPath && fs.existsSync(resultsPath) ? JSON.parse(fs.readFileSync(resultsPath, 'utf8')) : null;
const evidence = route ? summarizeEnemyEvidence(route) : null;
if (evidence) writeJson(path.join(outputDirectory, 'enemy-mechanics-evidence.json'), evidence);
const failurePath = campaignDirectory ? path.join(campaignDirectory, 'failure.json') : null;
const failure = failurePath && fs.existsSync(failurePath)
? JSON.parse(fs.readFileSync(failurePath, 'utf8')) : null;
const contentSourceResponses = route?.contentSourceResponses ?? routeResult?.contentSourceResponses
?? failure?.contentSourceResponses ?? [];
const loadedContentObjects = collectLoadedContentObjectEvidence(contentSourceResponses);
const contentSourceEvidence = {
sourceDirectory: contentSourceDirectory,
manifestBefore: contentManifestBefore,
manifestAfter: contentManifestAfter,
manifestUnchanged: contentManifestBefore.manifestSha256 === contentManifestAfter.manifestSha256,
responses: contentSourceResponses,
loadedObjects: loadedContentObjects,
counts: {
manifestResponses: contentSourceResponses.filter((response) => response?.pathname === '/content/manifest.json').length,
successfulManifestResponses: contentSourceResponses.filter((response) => response?.pathname === '/content/manifest.json'
&& response.status === 200).length,
objectResponses: contentSourceResponses.filter((response) => response?.pathname?.startsWith('/content/objects/')).length,
successfulObjectResponses: contentSourceResponses.filter((response) => response?.pathname?.startsWith('/content/objects/')
&& response.status === 200).length,
verifiedHashAddressedObjects: loadedContentObjects.filter((object) => object.objectIntegrity === 'served-file-matches-hash-address').length,
objectIntegrityFailures: loadedContentObjects.filter((object) => object.objectIntegrity === 'served-file-hash-mismatch'
|| object.objectIntegrity === 'not-found-on-content-source' || object.objectIntegrity === 'unsafe-path-refused').length,
},
};
writeJson(path.join(outputDirectory, 'content-source-evidence.json'), contentSourceEvidence);
runRecord.endedAt = new Date().toISOString();
runRecord.executionStatus = child.error ? 'failed-to-complete' : child.status === 0 ? 'complete' : 'failed';
runRecord.childExitCode = child.status;
runRecord.childSignal = child.signal ?? null;
runRecord.childError = child.error ? { message: child.error.message, code: child.error.code ?? null } : null;
runRecord.campaignDirectory = campaignDirectory ? path.relative(projectRoot, campaignDirectory) : null;
runRecord.campaignRoutePath = routePath && fs.existsSync(routePath) ? path.relative(projectRoot, routePath) : null;
runRecord.loadedBuildAssets = results?.buildAssets ?? [];
runRecord.actualAssetUrls = route?.actualAssetUrls ?? [];
runRecord.campaignResultStatus = results?.status ?? null;
runRecord.fullClearVerified = results?.campaignFullClearVerified ?? false;
runRecord.bossProgress = route?.bossProgress ?? null;
runRecord.dungeonResult = route?.dungeonResult ?? null;
runRecord.enemyMechanicsEvidenceCounts = evidence?.counts ?? null;
runRecord.contentManifestAfterSha256 = contentManifestAfter.manifestSha256;
runRecord.contentManifestUnchanged = contentSourceEvidence.manifestUnchanged;
runRecord.contentSourceResponseCounts = contentSourceEvidence.counts;
runRecord.contentSourceEvidencePath = path.relative(projectRoot, path.join(outputDirectory, 'content-source-evidence.json'));
runRecord.buildManifestAfterSha256 = afterManifest.aggregateSha256;
runRecord.buildManifestUnchanged = beforeManifest.aggregateSha256 === afterManifest.aggregateSha256
&& afterManifest.aggregateSha256.toLowerCase() === expectedBuildSha256.toLowerCase();
runRecord.pageErrors = results?.pageErrors ?? null;
runRecord.consoleErrors = results?.consoleErrors ?? null;
runRecord.screenshots = campaignDirectory
? fs.readdirSync(campaignDirectory).filter((file) => file.toLowerCase().endsWith('.png'))
.map((file) => path.relative(projectRoot, path.join(campaignDirectory, file))).sort()
: [];
if (route && !evidence?.counts.chainLightningCastSamples) {
runRecord.statusNotes.push('Hamhock Chain Lightning live cast was not captured; inspect target cast coverage and label inconclusive if the boss was not observed or no matching cast entered sampled state.');
}
if (route && !evidence?.counts.rendAttackDefinitions && !evidence?.counts.rendDamageEvents) {
runRecord.statusNotes.push('No sampled mob exposed Rend 6547/16509 or a Rend-named attack definition; label source mechanics coverage inconclusive.');
}
if (route && !evidence?.counts.rendCastSamples) {
runRecord.statusNotes.push(evidence?.counts.rendDamageEvents
? 'Enemy Rend 6547/16509 damage was observed in the raw combat stream; no corresponding active-cast state was sampled.'
: 'No live mob Rend 6547/16509 cast entered a sampled active-cast state; attack definitions alone do not establish a cast.');
}
if (route && !evidence?.counts.enemyAuraObservations && evidence?.counts.enemyBuffAuraApplications) {
runRecord.statusNotes.push(`Mob aura snapshots remained empty, but ${evidence.counts.enemyBuffAuraApplications} unique enemy-source buff applications were captured from combat aura telemetry.`);
} else if (route && !evidence?.counts.enemyAuraObservations && !evidence?.counts.enemyBuffAuraApplications) {
runRecord.statusNotes.push('No non-empty mob aura snapshot or enemy-source buff application was observed; report enemy-buff outcome as not observed.');
}
if (!runRecord.buildManifestUnchanged) {
runRecord.statusNotes.push('The frozen distribution fingerprint changed during the run; preserve the evidence but do not attribute it to one immutable build.');
}
if (!runRecord.contentManifestUnchanged) {
runRecord.statusNotes.push('The read-only content manifest changed during the run; content source provenance is inconclusive.');
}
runRecord.status = runRecord.executionStatus === 'complete' && route ? 'complete' : 'failed';
if (!runRecord.buildManifestUnchanged && runRecord.status === 'complete') runRecord.status = 'inconclusive-build-mutated';
writeJson(path.join(outputDirectory, 'run-record.json'), runRecord);
console.log(`Stockade enemy-mechanics QA ${runRecord.status}: ${path.relative(projectRoot, outputDirectory)}`);
if (runRecord.status !== 'complete') process.exitCode = child.status || 1;
} finally {
fs.rmSync(runtimeHarnessPath, { force: true });
}
}
try {
main();
} catch (error) {
fs.rmSync(runtimeHarnessPath, { force: true });
console.error(error.stack ?? error.message);
process.exitCode = 1;
}