卫戍协议:盟约 web remake — initial import

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).
This commit is contained in:
sganggs committed 2026-09-29 16:21:31 +08:00
commit 01a44cd288
408 files changed
+308986

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
#!/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;
}