feedback5: merge R4 — operator kits one file per chess (kits/ops/*, kits/shared/*, kits/index.js in the old registry order) + kits/README.md with the fidelity checklist; golden unchanged
# Conflicts: # .git-blame-ignore-revs # test/docs-consistency.test.js
This commit is contained in:
commit
6a7a23765a
172 files changed
+13214
-11826
No files matched your search
@@ -1,4 +1,4 @@
|
||||
// Tier 1 alternate skills & modules (DESIGN §16 operator loadouts; server/sim/content/kits/tier1.js `skills` maps).
|
||||
// Tier 1 alternate skills & modules (DESIGN §16 operator loadouts; the `skills` maps of the tier-1 kits, server/sim/content/kits/ops/).
|
||||
// Every selectable non-default skill of every visible tier-1 chess runs a real battle — normal (Lv4) and elite (Lv7) —
|
||||
// through the harness with its loadout (`skillIndex` / `moduleId` on the board entry, as a BattleSpec carries them),
|
||||
// and its signature effect is asserted with numbers from that skill's own blackboard (data/chess.json skills[]).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Operator loadouts (DESIGN §16) for the tier-2 kits (server/sim/content/kits/tier2.js): every selectable NON-default
|
||||
// Operator loadouts (DESIGN §16) for the tier-2 kits (server/sim/content/kits/ops/): every selectable NON-default
|
||||
// skill of every visible tier-2 chess is hand-authored (tools/kit-coverage.mjs) and shows its signature effect for the
|
||||
// normal (Lv4) and the elite (Lv7) chess — numbers from the selected skill's blackboard (data/chess.json skills[]) —
|
||||
// and the elite's module choice ('none' instead of the default module) changes what the kit / profile does.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tier-3 operator loadouts (DESIGN §16, server/sim/content/kits/tier3.js): every selectable NON-default skill of every
|
||||
// Tier-3 operator loadouts (DESIGN §16, server/sim/content/kits/ops/): every selectable NON-default skill of every
|
||||
// visible tier-3 chess is hand-authored (`skills[skillId]`) and proves its signature effect for the normal (Lv4) and the
|
||||
// elite (Lv7) chess with its own blackboard; non-default modules that change behaviour are exercised too.
|
||||
import { test } from 'node:test';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Tier-4 operator loadouts (DESIGN §16): every selectable NON-default skill of every visible tier-4 chess has a
|
||||
// hand-authored spec in server/sim/content/kits/tier4.js (`skills[skillId]`), proven here by its signature effect for
|
||||
// hand-authored spec in its kit file (server/sim/content/kits/ops/, `skills[skillId]`), proven here by its signature effect for
|
||||
// the normal (Lv4) and the elite (Lv7) chess; non-default modules (and 'none') change what their text says. Numbers
|
||||
// are read back from the data blackboards (the SELECTED skill's `bb`), real battles through the harness.
|
||||
import { test } from 'node:test';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tier 5 alternate skills & modules (DESIGN §16 operator loadouts; server/sim/content/kits/tier5.js `skills` maps).
|
||||
// Tier 5 alternate skills & modules (DESIGN §16 operator loadouts; the `skills` maps of the tier-5 kits, server/sim/content/kits/ops/).
|
||||
// Every selectable non-default skill of every visible tier-5 chess runs a real battle — normal (Lv4) and elite (Lv7) —
|
||||
// through the harness with its loadout (`skillIndex` / `moduleId` on the board entry, as a BattleSpec carries them),
|
||||
// and its signature effect is asserted with numbers from that skill's own blackboard (data/chess.json skills[]).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Operator loadouts (DESIGN §16) for the tier-6 kits (server/sim/content/kits/tier6.js): every selectable NON-default
|
||||
// Operator loadouts (DESIGN §16) for the tier-6 kits (server/sim/content/kits/ops/): every selectable NON-default
|
||||
// skill of every visible tier-6 chess is hand-authored (tools/kit-coverage.mjs) and shows its signature effect for the
|
||||
// normal (Lv4) and the elite (Lv7) chess — numbers from the selected skill's blackboard (data/chess.json skills[]) —
|
||||
// and the elite's non-default module choices (Y / RA modules, or 'none') change what the kit does.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// The operator kit layout (server/sim/content/kits/README.md): every file of kits/ops/ is listed once in kits/index.js
|
||||
// KIT_FILES and nothing else is; a file `<chessId>-<codename>.js` registers exactly the kit of `<chessId>_a`, whose
|
||||
// data record has that charId code name; the registry is the tier groups in order, and content/index.js serves that
|
||||
// object; no kit file imports a Node-only or server-only module or reads the clock / Math.random (the sim is
|
||||
// deterministic and also runs in the browser, /sim/).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { KIT_FILES, TIER_KITS, KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { KITS as CONTENT_KITS } from '../../server/sim/content/index.js';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const KIT_DIR = join(ROOT, 'server/sim/content/kits');
|
||||
const CHESS = JSON.parse(readFileSync(join(ROOT, 'data/chess.json'), 'utf8'));
|
||||
const jsIn = (dir) => readdirSync(join(KIT_DIR, dir)).filter((f) => f.endsWith('.js')).sort();
|
||||
|
||||
test('kits/index.js lists every kit file of kits/ops/ exactly once, and only those', () => {
|
||||
const listed = KIT_FILES.flat();
|
||||
assert.equal(new Set(listed).size, listed.length, 'a file listed twice');
|
||||
assert.deepEqual([...listed].sort(), jsIn('ops'));
|
||||
});
|
||||
|
||||
test('a kit file <chessId>-<codename>.js registers the kit of <chessId>_a (codename: its charId without char_<n>_)', async () => {
|
||||
for (const file of KIT_FILES.flat()) {
|
||||
const m = /^(chess_char_\w+?)-([a-z0-9]+)\.js$/.exec(file);
|
||||
assert.ok(m, `${file}: name`);
|
||||
const key = `${m[1]}_a`;
|
||||
const rec = CHESS[key];
|
||||
assert.ok(rec, `${file}: no data record ${key}`);
|
||||
assert.equal(String(rec.charId).replace(/^char_\d+_/, ''), m[2], `${file}: code name of ${rec.charId}`);
|
||||
const mod = await import(`../../server/sim/content/kits/ops/${file}`);
|
||||
assert.deepEqual(Object.keys(mod.default), [key], `${file}: default export`);
|
||||
assert.equal(typeof mod.default[key], 'function', `${file}: kit builder`);
|
||||
assert.equal(KITS[key], mod.default[key], `${file}: registered`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the registry is the tier groups in KIT_FILES order, one key per file; content/index.js serves the same object', () => {
|
||||
assert.deepEqual(Object.keys(KITS), TIER_KITS.flatMap((t) => Object.keys(t)));
|
||||
assert.equal(Object.keys(KITS).length, KIT_FILES.flat().length, 'one key per file, none twice');
|
||||
assert.equal(CONTENT_KITS, KITS);
|
||||
});
|
||||
|
||||
test('kit files stay browser-safe and deterministic: no Node-only or server-only import, no clock, no Math.random', () => {
|
||||
for (const f of ['index.js', ...['shared', 'ops'].flatMap((d) => jsIn(d).map((x) => `${d}/${x}`))]) {
|
||||
const src = readFileSync(join(KIT_DIR, f), 'utf8');
|
||||
assert.ok(!/from\s+['"]node:|import\(\s*['"]node:|\brequire\(|nodeData\.js/.test(src), `${f}: Node-only import`);
|
||||
const specifiers = [...src.matchAll(/^(?:import|export)\s[^;]*?\sfrom\s+['"]([^'"]+)['"]/gm), ...src.matchAll(/\bimport\(\s*['"`]([^'"`]+)['"`]/g)];
|
||||
for (const [, p] of specifiers) {
|
||||
assert.ok(p.startsWith('.'), `${f}: bare import ${p}`);
|
||||
// never server / client code, never an index module (content/index.js, kits/index.js: an import cycle)
|
||||
assert.ok(!/(^|\/)(match|net|public)\/|(^|\/)index\.js$/.test(p), `${f}: imports ${p}`);
|
||||
}
|
||||
assert.ok(!/Math\.random|Date\.now|performance\.now|setTimeout|setInterval/.test(src), `${f}: clock / Math.random (use battle.rng, battle.after / every)`);
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// Content tests for the tier 1 / tier 2 operator kits (server/sim/content/kits/tier1.js, tier2.js).
|
||||
// Content tests for the tier 1 / tier 2 operator kits (server/sim/content/kits/ops/chess_char_1_*.js, chess_char_2_*.js).
|
||||
// Every chess (normal + elite where the elite adds something) runs a real battle through the harness and the test
|
||||
// asserts its signature effect with numbers taken from the chess blackboards (data/chess.json).
|
||||
import { test } from 'node:test';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tier-3 operator kits (server/sim/content/kits/tier3.js): every chess runs a real battle through the harness and
|
||||
// Tier-3 operator kits (server/sim/content/kits/ops/chess_char_3_*.js): every chess runs a real battle through the harness and
|
||||
// its signature skill / talent / module effect is asserted with numbers taken from its own blackboards.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tier-4 operator kits (server/sim/content/kits/tier4.js): one signature test per chess (+ elite checks), real battles
|
||||
// Tier-4 operator kits (server/sim/content/kits/ops/chess_char_4_*.js): one signature test per chess (+ elite checks), real battles
|
||||
// through the harness. Numbers are read back from the data blackboards so the tests follow data changes.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tier-5 operator kits (server/sim/content/kits/tier5.js): every chess runs a real battle through the harness and its
|
||||
// Tier-5 operator kits (server/sim/content/kits/ops/chess_char_5_*.js): every chess runs a real battle through the harness and its
|
||||
// signature effect is asserted with numbers taken from its own blackboards (normal Lv4 / elite Lv7).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -998,7 +998,7 @@ test('录武官 S2: ATK +45 %; healed allies heal 80 HP per hit taken for 10 s;
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// regressions of the 2026-09-28 fidelity review (PRTS 备注 of the base operators; see the tier5.js header)
|
||||
// regressions of the 2026-09-28 fidelity review (PRTS 备注 of the base operators; see the kits/shared/tier5.js header)
|
||||
|
||||
test('圣约送葬人 T1 受选之人: the extra attack consumes no ammo and never reaches ammoUsed listeners', () => {
|
||||
const log = [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Content tests for the tier-6 kits (server/sim/content/kits/tier6.js) + 盟约·辅助干员 (chess_char_1_15).
|
||||
// Content tests for the tier-6 kits (server/sim/content/kits/ops/chess_char_6_*.js) + 盟约·辅助干员 (chess_char_1_15).
|
||||
// Every test runs a real battle through the harness and checks the signature effect of the kit.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Content tests for user playtest #4 items 11 and 12 (server/sim/content/tokens.js, kits/tier2.js):
|
||||
// Content tests for user playtest #4 items 11 and 12 (server/sim/content/tokens.js, kits/ops/…2_02-silent.js):
|
||||
// #11 赫默's 医疗无人机 comes with her S2 — since user playtest #6 it is a hand piece the player places, it deploys
|
||||
// once on that tile at the battle start (PRTS 卫戍协议/帮助 §作战阶段; settled by the user after playtest #6 —
|
||||
// shared/constants.js SKILL_SUMMON_START_DEPLOY) and re-appears there each time S2 fires
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// User playtest #6 items 1 and 2 (from a friend): 凯瑟琳's device and 赫默's drone could not be placed by hand. The
|
||||
// prep side is test/match/playtest6_summons.test.js; this file is the battle side of the placed pieces
|
||||
// (server/sim/content/tokens.js dockSkillSummons / releaseSkillSummon, Battle.start `deferDeploy`, kits tier2 / tier3 /
|
||||
// tier4, the 爬行号·防护单元 token kit):
|
||||
// (server/sim/content/tokens.js dockSkillSummons / releaseSkillSummon, Battle.start `deferDeploy`, the summoner
|
||||
// kits in kits/ops/, the 爬行号·防护单元 token kit):
|
||||
// * PRTS 卫戍协议/帮助 §作战阶段: the board summons deploy after the operators; "若战场区初始部署有召唤物,若召唤物在
|
||||
// 战斗期间退场,将在满足条件后立即原地再部署1个" — a placed piece marks the tile its summon deploys on;
|
||||
// * a skill's summon (赫默 医疗探机, 巫恋 诅咒娃娃: "获得一个…") deploys once on its tile at the battle start, for free
|
||||
|
||||
@@ -46,11 +46,14 @@ import { BAND_TURN_SECONDS } from '../server/match/Match.js';
|
||||
import { SPINE_EVICT_DELAY_MS, SPINE_QUIET_DELAY_MS } from '../public/js/assets.js';
|
||||
import { RETRY_DELAYS_MS } from '../public/js/data.js';
|
||||
import { DATA, makeMatch } from './match/harness.js';
|
||||
import { KIT_FILES } from '../server/sim/content/kits/index.js';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const doc = (p) => readFileSync(join(ROOT, p), 'utf8');
|
||||
/** server/sim/Battle.js with its method modules (server/sim/battle/*.js) as one text. */
|
||||
const battleText = () => [doc('server/sim/Battle.js'), ...readdirSync(join(ROOT, 'server/sim/battle')).sort().map((f) => doc(`server/sim/battle/${f}`))].join('\n');
|
||||
// the sources of a former kits/tierN.js: its helpers (kits/shared/tierN.js) and its kit files (kits/ops/, kits/index.js)
|
||||
const tierSources = (t) => [`server/sim/content/kits/shared/tier${t}.js`, ...KIT_FILES[t - 1].map((f) => `server/sim/content/kits/ops/${f}`)];
|
||||
const DESIGN = doc('docs/DESIGN.md');
|
||||
const META = doc('docs/META.md');
|
||||
const DATA_MD = doc('docs/DATA.md');
|
||||
@@ -255,7 +258,7 @@ test('回环射手 boomerang and 蕾缪安 S3 shells (user playtest #3 items 4
|
||||
assert.match(SIM, /`none\|arrow\|bolt\|bomb\|lob\|orb\|drone\|enemy\|boomerang\|droneBomb\|chain\|chainHeal`/);
|
||||
assert.match(DESIGN, /BOOMERANG_RETURN_SPEED/);
|
||||
// 蕾缪安: one shell every 0.3 s after the skill (PRTS), fx 'bombardShell' then 'bombard' — the kit's constants
|
||||
const kit = readFileSync(join(ROOT, 'server/sim/content/kits/tier6.js'), 'utf8');
|
||||
const kit = readFileSync(join(ROOT, 'server/sim/content/kits/ops/chess_char_6_01-lemuen.js'), 'utf8');
|
||||
assert.match(kit, /const LEMUEN_SHELL_INTERVAL = 0\.3;/);
|
||||
assert.match(kit, /battle\.fx\('bombardShell'/);
|
||||
assert.match(DESIGN, /'bombardShell' \{x, y, id: shooter, r, t: flight game s, i\}/);
|
||||
@@ -367,7 +370,7 @@ test('user playtest #5 (DESIGN §19): blocking, 联防 forced exit, huge bosses,
|
||||
assert.match(PLAYING, /自缚、无法被阻挡/);
|
||||
// #3 elements: one hasHp, the pipeline guard, 脆弱 vs 元素伤害, element healing per type
|
||||
assert.equal(typeof DAMAGE.hasHp, 'function');
|
||||
for (const f of ['server/sim/content/kits/tier5.js', 'server/sim/content/kits/tier6.js', 'server/sim/content/items/battle.js']) {
|
||||
for (const f of [...tierSources(5), ...tierSources(6), 'server/sim/content/items/battle.js']) {
|
||||
assert.ok(!/const hasHp = /.test(readFileSync(join(ROOT, f), 'utf8')), `${f}: no local hasHp copy`);
|
||||
}
|
||||
assert.match(s55, /元素伤害 takes 元素脆弱 \(`elementalTakenMul`\) alone, not `dmgTakenMul`/);
|
||||
@@ -753,7 +756,7 @@ test('batch 6 QA residuals (DESIGN §21.21–§21.25): the lock per deployment f
|
||||
assert.match(items, /function deploymentOf\(u\)/);
|
||||
assert.ok(!/S\.on\('deploy', \(c\) => \{\s*if \(c\.unit !== u \|\| c\.initial\) return;\s*hs\.undyingUsed/.test(items), 'no per-grant re-arm hook');
|
||||
assert.match(doc('server/sim/content/bands/battle.js'), /revivedInPlace\(u\)/);
|
||||
assert.match(doc('server/sim/content/kits/tier4.js'), /if \(holdsUndying\(battle, unit\)\) return;/);
|
||||
assert.match(doc('server/sim/content/kits/ops/chess_char_4_01-rmixer.js'), /if \(holdsUndying\(battle, unit\)\) return;/);
|
||||
assert.match(sub(21), /\*\*QA after the integration, fixed\*\*: \(1\) the lock lived in the hooks of the carrier's hammer grants/);
|
||||
assert.match(sub(21), /both in-place revives now call `revivedInPlace`/);
|
||||
assert.match(sub(20), /the lock belongs to the deployment, so a borrowed hammer \(萨尔贡 × 娜仁图亚\) follows the same rule \| `content\/items\/battle\.js deploymentOf` returning one key/);
|
||||
@@ -838,7 +841,7 @@ test('the deliberate trigger deviation (DESIGN §21.29): six 重装 skills DEFAU
|
||||
assert.match(r03, /下半 \(act2autochess, 2026-03-14\) added `TANK \| \| \| 0 \| TAKE_DAMAGE` for every skill index/);
|
||||
assert.match(r03, /\*\*Deliberate deviation\*\* \(the owner, 2026-10-03/);
|
||||
// PR #12's kit lines stay; their comments give this reason, not the community summary
|
||||
const t1 = doc('server/sim/content/kits/tier1.js');
|
||||
const t1 = tierSources(1).map(doc).join('\n');
|
||||
assert.equal((t1.match(/trigger: 'DEFAULT',/g) || []).length, 2, "PR #12's two kit lines");
|
||||
assert.ok(!/offensive skills activate when an enemy is in their skill range/.test(t1));
|
||||
assert.ok(!/documented for skillIndex 0/.test(t1));
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// routes RouteSpec[] (default: template routes, or flat-stage routes 0..3 = walk low, walk high, fly low, fly high)
|
||||
// timeLimit seconds (default: template maxPlayTime, else 60; boss/hidden: Infinity)
|
||||
// content 'full' | 'generic' | 'none' (default 'full')
|
||||
// kits { [baseChessId]: (bb, chess, def) => Kit } injected kits (take precedence over kits/tier*.js)
|
||||
// kits { [baseChessId]: (bb, chess, def) => Kit } injected kits (take precedence over kits/index.js)
|
||||
// extraContent [{ install(battle) }] extra content modules installed after the domain modules (content tests)
|
||||
// defs { chess: {id: record}, enemies: {key: record}, tokens: {id: record} } extra/override data records
|
||||
// flags, sharedBoss, modeId, round, setup(battle), recordEvents (default true). The harness sets
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// test/render/fxwall.test.js — 余 S3 fire wall visual (render/fx.js): the sim's `firewall` fx carries `axis` ('col' for
|
||||
// a RIGHT / LEFT facing, 'row' for UP / DOWN — sim/content/kits/tier6.js); the renderer lights that straight line of
|
||||
// a RIGHT / LEFT facing, 'row' for UP / DOWN — sim/content/kits/ops/chess_char_6_03-yu.js); the renderer lights that straight line of
|
||||
// tiles through his tile, clipped to the battle rect (it used to be a round zone whatever the facing).
|
||||
|
||||
import { test } from 'node:test';
|
||||
|
||||
@@ -222,7 +222,7 @@ test('generic text rules: 泡泡 stops attacking; 小满 sleeps ≤3 enemies onc
|
||||
});
|
||||
|
||||
test('generic element: 塑心 charges add 85 % ATK of 凋亡 (apoptosis) damage to the gauge', () => {
|
||||
// the generic kit's numbers (tier6.js has a hand-written 塑心 kit whose talent amplifies apoptosis in range)
|
||||
// the generic kit's numbers (kits/ops/chess_char_6_09-cello.js is a hand-written 塑心 kit whose talent amplifies apoptosis in range)
|
||||
const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy() } }, units: [{ chessId: 'chess_char_6_09_a', row: 10, col: 4 }], enemies: [{ key: 'enemy_dummy', pos: [10, 6] }], timeLimit: 60, content: 'generic' });
|
||||
const u = h.unit('chess_char_6_09_a');
|
||||
assert.ok(h.runUntil(() => u.skill.activations >= 1 && h.enemy('enemy_dummy').elem.apoptosis > 0, 40));
|
||||
|
||||
Reference in new issue
Block a user