feedback5 WH: 自选 pieces in the sim — shared/diy.js (diyPool, checkDiyPick, diyRecord / diyRecordOf, validateDiyPicks: the legal picks per tier, prototypes skill-locked to their 补位 rows, owned picks any skill / module at the slot's stage, no 特质, derived bonds, one slot per owned operator, distinct picks per tier, unkitted operators refused); simdata getChess(slotId, { diy }) builds the 自选 def (diyFor, charId, loadout.diy, tokenOwner) and getToken resolves its summons from backups.json tokens; Battle, BattleSpec and the loadout view carry the PlayerBattleInput diy pick (a DIY slot without a legal pick fields nothing); kitOf takes KITS[charId] for a 自选 piece as for a stand-in; kits/index.js OPERATOR_KIT_FILES (op-<codename>.js, empty for now), GENERIC_KIT_CHARS, KITTED_CHARS; the harness fields units[].diy

No golden scenario moved (none fields a 自选 piece).
This commit is contained in:
sganggs committed 2026-10-05 19:51:40 +08:00
1 parent c278eafa52
commit 2b72d4deb0
13 files changed
+791 -42

No files matched your search

+34 -10
View File
@@ -1,15 +1,18 @@
// The operator kit layout (server/sim/content/kits/README.md): every file of kits/ops/ is listed once in kits/index.js
// KIT_FILES or STANDIN_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; a 补位 file `standin-<codename>.js` registers exactly the
// kit of the stand-in character whose charId has that code name (data/backups.json `units`); the registry is the tier
// groups in order, then the stand-ins, 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/).
// KIT_FILES, STANDIN_KIT_FILES or OPERATOR_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; a 补位 file `standin-<codename>.js`
// registers exactly the kit of the stand-in character whose charId has that code name (data/backups.json `units`); a
// 自选 file `op-<codename>.js` registers exactly the kit of the owned-6★ pick with that code name (data/backups.json
// `diy.ownedPool`) and writes every skill of the operator under `skills`; the registry is the tier groups in order, then
// the stand-ins, then the 自选 operators, and content/index.js serves that object; KITTED_CHARS = the 预备干员 + every
// registered stand-in / operator charId; 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, STANDIN_KIT_FILES, STANDIN_KITS } from '../../server/sim/content/kits/index.js';
import { KIT_FILES, TIER_KITS, KITS, STANDIN_KIT_FILES, STANDIN_KITS, OPERATOR_KIT_FILES, OPERATOR_KITS, GENERIC_KIT_CHARS, KITTED_CHARS } 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)), '../..');
@@ -19,7 +22,7 @@ const BACKUPS = JSON.parse(readFileSync(join(ROOT, 'data/backups.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(), ...STANDIN_KIT_FILES];
const listed = [...KIT_FILES.flat(), ...STANDIN_KIT_FILES, ...OPERATOR_KIT_FILES];
assert.equal(new Set(listed).size, listed.length, 'a file listed twice');
assert.deepEqual([...listed].sort(), jsIn('ops'));
});
@@ -39,6 +42,27 @@ test('a stand-in kit file standin-<codename>.js registers the kit of that stand-
}
});
test('a 自选 kit file op-<codename>.js registers the kit of that owned-6★ pick (data/backups.json diy.ownedPool), every skill under `skills`', async () => {
const pool = BACKUPS.diy.ownedPool;
for (const file of OPERATOR_KIT_FILES) {
const m = /^op-([a-z0-9]+)\.js$/.exec(file);
assert.ok(m, `${file}: name`);
const charId = pool.find((id) => id.replace(/^char_\d+_/, '') === m[1]);
assert.ok(charId, `${file}: no owned-6★ pick with the code name ${m[1]}`);
const mod = await import(`../../server/sim/content/kits/ops/${file}`);
assert.deepEqual(Object.keys(mod.default), [charId], `${file}: default export`);
assert.equal(typeof mod.default[charId], 'function', `${file}: kit builder`);
assert.equal(OPERATOR_KITS[charId], mod.default[charId], `${file}: registered`);
assert.equal(KITS[charId], mod.default[charId], `${file}: in the merged registry`);
// every skill of every form is authored (a 自选 piece has no default skill: the pick chooses any)
for (const [key, form] of Object.entries(BACKUPS.units[charId].forms)) {
const kit = mod.default[charId](form.skills[0].bb, { ...form, skill: form.skills[0], charId }, {});
assert.deepEqual(Object.keys(kit.skills || {}).sort(), form.skills.map((s) => s.skillId).sort(), `${file}@${key}: skills`);
}
}
assert.deepEqual([...KITTED_CHARS].sort(), [...new Set([...GENERIC_KIT_CHARS, ...Object.keys(STANDIN_KITS), ...Object.keys(OPERATOR_KITS)])].sort());
});
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);
@@ -54,9 +78,9 @@ test('a kit file <chessId>-<codename>.js registers the kit of <chessId>_a (coden
}
});
test('the registry is the tier groups in KIT_FILES order, then the stand-ins, one key per file; content/index.js serves the same object', () => {
assert.deepEqual(Object.keys(KITS), [...TIER_KITS.flatMap((t) => Object.keys(t)), ...Object.keys(STANDIN_KITS)]);
assert.equal(Object.keys(KITS).length, KIT_FILES.flat().length + STANDIN_KIT_FILES.length, 'one key per file, none twice');
test('the registry is the tier groups in KIT_FILES order, then the stand-ins, then the 自选 operators, one key per file; content/index.js serves the same object', () => {
assert.deepEqual(Object.keys(KITS), [...TIER_KITS.flatMap((t) => Object.keys(t)), ...Object.keys(STANDIN_KITS), ...Object.keys(OPERATOR_KITS)]);
assert.equal(Object.keys(KITS).length, KIT_FILES.flat().length + STANDIN_KIT_FILES.length + OPERATOR_KIT_FILES.length, 'one key per file, none twice');
assert.equal(CONTENT_KITS, KITS);
});