Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
103 lines
5.1 KiB
JavaScript
103 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
||
// tools/kit-coverage.mjs — which selectable skills of every visible chess have a hand-authored skill spec (DESIGN §16).
|
||
//
|
||
// Operator loadouts let the player pick any skill unlocked for both the normal and the elite chess (shared/protocol.js
|
||
// loadoutOptions — exactly what room.loadout accepts). The kit loader (server/sim/content/index.js selectSkillSpec) uses, for the SELECTED skill:
|
||
// skills[skillId] of the kit → 'skills' (hand-authored per skill)
|
||
// kit.skill, default skill only → 'kit' (hand-authored default)
|
||
// otherwise → 'generic' (content/generic.js from the blackboard — an approximation)
|
||
// A skill counts as covered when both its normal and its elite def get a hand-authored spec ('skills' | 'kit').
|
||
//
|
||
// Usage: node tools/kit-coverage.mjs [--json] [--missing] [--tier N] [--strict]
|
||
// --json machine-readable report { summary, chess: [{ chessId, name, tier, skills: [{ index, skillId, name,
|
||
// isDefault, normal, elite, covered }] }] } on stdout
|
||
// --missing list only the chess with at least one uncovered skill
|
||
// --tier N only tier N (1–6)
|
||
// --strict exit code 1 when any selectable skill is uncovered (CI once every kit is authored)
|
||
// Unknown options are errors (exit code 2).
|
||
|
||
import { getDefaultSource } from '../server/sim/simdata.js';
|
||
import { loadoutOptions } from '../shared/protocol.js';
|
||
import { KITS, skillSpecSource } from '../server/sim/content/index.js';
|
||
|
||
const USAGE = 'usage: node tools/kit-coverage.mjs [--json] [--missing] [--tier N] [--strict]';
|
||
|
||
function parseArgs(argv) {
|
||
const o = { json: false, missing: false, tier: null, strict: false };
|
||
for (let i = 0; i < argv.length; i++) {
|
||
const a = argv[i];
|
||
if (a === '--json') o.json = true;
|
||
else if (a === '--missing') o.missing = true;
|
||
else if (a === '--strict') o.strict = true;
|
||
else if (a === '--tier') {
|
||
const t = Number(argv[++i]);
|
||
if (!Number.isInteger(t) || t < 1 || t > 6) return null;
|
||
o.tier = t;
|
||
} else return null;
|
||
}
|
||
return o;
|
||
}
|
||
|
||
/**
|
||
* Coverage report of every visible chess (optionally one tier).
|
||
* @param {{ tier?: number|null, kits?: object, ds?: object }} [opts]
|
||
*/
|
||
export function kitCoverage({ tier = null, kits = KITS, ds = getDefaultSource() } = {}) {
|
||
const chessMap = ds.raw?.chess ?? {};
|
||
const rows = [];
|
||
const bases = Object.values(chessMap)
|
||
.filter((c) => c && !c.isGolden && c.visible && (tier == null || c.tier === tier))
|
||
.sort((a, b) => a.tier - b.tier || String(a.chessId).localeCompare(String(b.chessId), 'en', { numeric: true }));
|
||
for (const base of bases) {
|
||
if (!Array.isArray(base.skills)) continue;
|
||
const choices = loadoutOptions(base, base.goldenId ? chessMap[base.goldenId] ?? null : null);
|
||
const skills = choices.skills.map((index) => {
|
||
const rec = base.skills.find((s) => s.index === index);
|
||
const src = (id) => {
|
||
const def = chessMap[id] ? ds.getChess(id, { skillIndex: index }) : null;
|
||
return def ? skillSpecSource(def, kits) : 'none';
|
||
};
|
||
const normal = src(base.chessId);
|
||
const elite = base.goldenId ? src(base.goldenId) : normal;
|
||
const authored = (s) => s === 'skills' || s === 'kit';
|
||
return { index, skillId: rec.skillId, name: rec.name, isDefault: !!rec.isDefault, normal, elite, covered: authored(normal) && authored(elite) };
|
||
});
|
||
rows.push({ chessId: base.chessId, name: base.name, tier: base.tier, skills });
|
||
}
|
||
const all = rows.flatMap((r) => r.skills);
|
||
const summary = {
|
||
chess: rows.length,
|
||
skills: all.length,
|
||
covered: all.filter((s) => s.covered).length,
|
||
defaultCovered: all.filter((s) => s.isDefault && s.covered).length,
|
||
defaults: all.filter((s) => s.isDefault).length,
|
||
chessFullyCovered: rows.filter((r) => r.skills.every((s) => s.covered)).length,
|
||
};
|
||
return { summary, chess: rows };
|
||
}
|
||
|
||
const isMain = import.meta.url === new URL(process.argv[1] ?? '', 'file://').href || process.argv[1]?.endsWith('kit-coverage.mjs');
|
||
if (isMain) {
|
||
const o = parseArgs(process.argv.slice(2));
|
||
if (!o) {
|
||
console.error(USAGE);
|
||
process.exit(2);
|
||
}
|
||
const rep = kitCoverage({ tier: o.tier });
|
||
const list = o.missing ? rep.chess.filter((r) => r.skills.some((s) => !s.covered)) : rep.chess;
|
||
if (o.json) {
|
||
process.stdout.write(JSON.stringify({ summary: rep.summary, chess: list }, null, 1) + '\n');
|
||
} else {
|
||
const mark = (s) => (s.covered ? '✓' : s.normal === s.elite ? `· ${s.normal}` : `· ${s.normal}/${s.elite}`);
|
||
for (const r of list) {
|
||
const cells = r.skills.map((s) => `S${s.index + 1}${s.isDefault ? '*' : ''} ${s.name} ${mark(s)}`);
|
||
console.log(`T${r.tier} ${r.name.padEnd(8, ' ')} ${r.chessId.padEnd(18)} ${cells.join(' | ')}`);
|
||
}
|
||
const s = rep.summary;
|
||
console.log(`\n${s.covered}/${s.skills} selectable skills hand-authored (defaults ${s.defaultCovered}/${s.defaults}); ` +
|
||
`${s.chessFullyCovered}/${s.chess} chess fully covered. (* = default skill; ✓ = normal+elite authored; ` +
|
||
`· = falls back to: generic | none (no kit))`);
|
||
}
|
||
if (o.strict && rep.summary.covered < rep.summary.skills) process.exitCode = 1;
|
||
}
|