95 lines
4.0 KiB
JavaScript
95 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const catalog = JSON.parse(fs.readFileSync(
|
|
path.join(projectRoot, "src/game/generated/dungeonCampaignCatalog.json"),
|
|
"utf8",
|
|
));
|
|
const manifest = JSON.parse(fs.readFileSync(
|
|
path.join(projectRoot, "public/assets/game/combat-effects/manifest.json"),
|
|
"utf8",
|
|
));
|
|
|
|
const highConfidenceEffectName = /\b(?:dominat\w*|polymorph|hex|silence|slow|hamstring|frost nova|chill nova|summon|raise dead|ward|frost armor|drain life|death coil|vampiric touch|leech poison|pierce armor|sunder armor|dark shell|shock barrier|damage immunity)\b/i;
|
|
const auxiliarySpellIds = new Set([21_708, 47_669, 54_888]);
|
|
const manifestBySpellId = new Map();
|
|
for (const entry of manifest.entries ?? []) manifestBySpellId.set(Number(entry.spellId), entry);
|
|
|
|
const bossCasts = [];
|
|
for (const dungeon of catalog.definitions ?? []) {
|
|
for (const entity of Object.values(dungeon.entities ?? {})) {
|
|
if (entity.kind !== "boss") continue;
|
|
for (const attack of entity.combat?.attacks ?? []) {
|
|
if (!attack.spellId) continue;
|
|
bossCasts.push({ dungeonId: dungeon.id, boss: entity.name, ...attack });
|
|
}
|
|
}
|
|
}
|
|
|
|
const effectCasts = bossCasts.filter((attack) => attack.effect);
|
|
const suspicious = bossCasts.filter((attack) => (
|
|
!attack.effect && highConfidenceEffectName.test(attack.name)
|
|
));
|
|
const auxiliaryLeaks = bossCasts.filter((attack) => auxiliarySpellIds.has(attack.spellId));
|
|
const invalidComposites = effectCasts.filter((attack) => (
|
|
attack.effect.kind === "composite" && !attack.effect.effects?.length
|
|
));
|
|
const atomicEffects = (attack) => attack.effect?.kind === "composite"
|
|
? attack.effect.effects
|
|
: attack.effect ? [attack.effect] : [];
|
|
const harmful = (attack) => atomicEffects(attack).some((effect) => (
|
|
["damage", "leech", "target-aura", "control", "pull"].includes(effect.kind)
|
|
));
|
|
const inferredGroundTelegraph = (attack) => harmful(attack) && (
|
|
attack.delivery === "area"
|
|
|| attack.radius !== undefined
|
|
|| /\b(?:breath|cleave|frontal|sweep|cone|beam|charge|line|lance)\b/i.test(attack.name)
|
|
);
|
|
const presentation = effectCasts.map((attack) => {
|
|
const entry = manifestBySpellId.get(attack.spellId);
|
|
const originalVisual = Boolean(entry && Object.values(entry.phases ?? {}).some(
|
|
(assets) => Array.isArray(assets) && assets.length > 0,
|
|
));
|
|
const originalAudio = Boolean(entry?.sounds?.some((sound) => sound.url));
|
|
return { ...attack, originalVisual, originalAudio };
|
|
});
|
|
|
|
const issues = [...suspicious, ...auxiliaryLeaks, ...invalidComposites].map((attack) => (
|
|
`${attack.dungeonId}:${attack.boss}:${attack.spellId}:${attack.name}`
|
|
));
|
|
const report = {
|
|
status: issues.length ? "failed" : "complete",
|
|
bosses: new Set(bossCasts.map((attack) => `${attack.dungeonId}:${attack.boss}`)).size,
|
|
spellCasts: bossCasts.length,
|
|
explicitEffectCasts: effectCasts.length,
|
|
presentation: {
|
|
originalVisual: presentation.filter((attack) => attack.originalVisual).length,
|
|
generatedVisualFallback: presentation.filter((attack) => !attack.originalVisual).length,
|
|
originalAudio: presentation.filter((attack) => attack.originalAudio).length,
|
|
generatedAudioFallback: presentation.filter((attack) => !attack.originalAudio).length,
|
|
},
|
|
indicators: {
|
|
castBars: effectCasts.filter((attack) => (
|
|
(attack.castTimeMs ?? 0) > 0
|
|
|| attack.animation === "cast"
|
|
|| inferredGroundTelegraph(attack)
|
|
)).length,
|
|
groundTelegraphs: effectCasts.filter((attack) => (
|
|
attack.telegraph || inferredGroundTelegraph(attack)
|
|
)).length,
|
|
releaseOrImpactCues: effectCasts.length,
|
|
auraOrControlHud: effectCasts.filter((attack) => {
|
|
return atomicEffects(attack).some((effect) => (
|
|
["aura", "target-aura", "control"].includes(effect.kind)
|
|
));
|
|
}).length,
|
|
},
|
|
issues,
|
|
};
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
if (issues.length) process.exitCode = 1;
|