feedback5 WD: the sim fields a 补位 stand-in — getChess(id, { standIn: true }) over data/backups.json, PlayerBattleInput standIn: true, kits by the stand-in's charId (never the replaced operator's), kits/ops/standin-<codename>.js registry, harness standIn

- simdata: DataSource keeps `backups` (rawBackups follows the fallback chain); getStandIn = shared/standIn.js
  standInRecord normalised (the chess's ids / bonds / 特质 / tier, the stand-in's body, def.standInFor); a PRESET / DIY
  chess or a source without backups fields itself; the per-battle view maps id-only lookups.
- spec.js keeps `standIn` only when exactly true (never on a token); Battle passes it to getChess; syncUnitLoadout
  carries it; UnitInfo `standInFor`.
- content/index.js kitOf: a stand-in's kit is KITS[def.charId] only; no default skill (a stand-in kit writes `skills`);
  without a kit the generic kit plus genericTalents. kits/index.js STANDIN_KIT_FILES / STANDIN_KITS, merged after the
  tier groups (old keys keep their order; golden unchanged).
- server/data.js and tools/setup.mjs expect backups.json.
- test/content/standin.test.js: the plumbing and the eight 预备干员 on all 28 chess records they replace.
This commit is contained in:
sganggs committed 2026-10-05 17:44:58 +08:00
1 parent 286a34064f
commit ed2b679f83
14 files changed
+656 -58

No files matched your search

+49 -5
View File
@@ -11,15 +11,21 @@
// stage explicit stage object (overrides stageId)
// kind 'normal' | 'unite' | 'boss' | 'hidden' (default 'normal')
// seed uint32 (default 1)
// units [{ chessId, row, col, items?, uid?, carryState?, abs? } | { kind:'token', tokenId, row, col }]
// board coordinates (rows 9–12, cols 2–10) for player 'p1'
// units [{ chessId, row, col, items?, uid?, carryState?, abs?, standIn? } | { kind:'token', tokenId, row, col }]
// board coordinates (rows 9–12, cols 2–10) for player 'p1'. `standIn: true` fields a NORMAL chess as its
// 补位 stand-in (data/backups.json through simdata getChess(id, { standIn: true }) — the production
// path: the chess's ids / bonds / 特质 / tier, the stand-in's body, its kit by charId); the `_a` id is the
// normal form, `_b` the elite, skill / module are the chess's backup selection. `standIn: { skillIndex?,
// moduleId? }` fields it with another of the stand-in's skills / modules (a kit test of a skill no chess
// names): the composed record (standInRec) replaces that chess id's record for the whole battle
// players full PlayerBattleInput[] (overrides `units`)
// enemies [{ key, time=0, route=0 | RouteSpec, pos?, count?, interval?, mods?, tag?, bounty?, sourcePlayerId? }]
// waveTemplate wave id (data/waves.json) or template object → routes + spawns + timeLimit
// 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/index.js)
// kits { [baseChessId | standInCharId]: (bb, chess, def) => Kit } injected kits (take precedence over
// kits/index.js; a stand-in's kit is keyed by its charId only)
// 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
@@ -36,6 +42,7 @@
import { Battle } from '../../server/sim/Battle.js';
import { DataSource, getDefaultSource, spawnsFromTemplate } from '../../server/sim/simdata.js';
import { TICK } from '../../server/sim/constants.js';
import { standInRecord, composeUnitRecord, unitForm } from '../../shared/standIn.js';
export const ALL_HOOKS = Object.freeze([
'battleStart', 'deploy', 'tick', 'beforeAttack', 'attack', 'hit', 'damaged', 'heal', 'kill', 'death',
@@ -141,15 +148,52 @@ export function enemyRec(o = {}) {
};
}
/**
* The 补位 record of a NORMAL chess (data/chess.json `_a` normal / `_b` elite) fielded as its stand-in — shared/standIn.js
* standInRecord: the chess's backup skill and module — or, with `skillIndex` / `moduleId` (null / 'none' = no module),
* another selection of the same stand-in at the chess's status. Throws when the chess has no stand-in (PRESET, DIY) or
* the stand-in lacks that skill / module.
* @param {string} chessId
* @param {{ skillIndex?: number, moduleId?: string|null }} [sel]
* @param {object} [data] DataSource (default: the sim's default source)
*/
export function standInRec(chessId, sel = {}, data = getDefaultSource()) {
const chess = data.rawChess(chessId);
const backups = typeof data.rawBackups === 'function' ? data.rawBackups() : null;
const b = chess?.backup;
if (!b || !backups || chess.chessType !== 'NORMAL') throw new Error(`${chessId}: no 补位 stand-in (PRESET / DIY chess, or no backups.json)`);
const custom = sel && (sel.skillIndex != null || sel.moduleId !== undefined);
const rec = !custom ? standInRecord(chess, backups) : composeUnitRecord(chess, backups.units?.[b.charId], unitForm(backups, b.charId, chess.status), {
skillIndex: sel.skillIndex ?? b.skillIndex,
moduleId: sel.moduleId === undefined ? b.uniEquipId ?? null : (sel.moduleId === 'none' ? null : sel.moduleId),
standInFor: chess.charId,
});
if (!rec || !rec.skill) throw new Error(`${chessId}: stand-in ${b.charId} has no skill ${sel?.skillIndex ?? b.skillIndex}`);
const mod = sel?.moduleId;
if (custom && mod && mod !== 'none' && !rec.module?.active) throw new Error(`${chessId}: stand-in ${b.charId} has no module ${mod} at ${chessId}`);
return rec;
}
function buildData(defs) {
if (!defs) return getDefaultSource();
return new DataSource({ chess: defs.chess ?? {}, enemies: defs.enemies ?? {}, tokens: defs.tokens ?? {}, stages: defs.stages ?? {}, waves: defs.waves ?? {} }, getDefaultSource());
}
/** `units[].standIn` objects → the composed records for `defs.chess` (null when there are none). */
function standInDefs(units) {
let out = null;
for (const u of units ?? []) {
if (!u || !u.standIn || typeof u.standIn !== 'object') continue;
(out ??= {})[u.chessId] = standInRec(u.chessId, u.standIn);
}
return out;
}
/** Create a battle + harness. See header for options. */
export function makeBattle(opts = {}) {
const kind = opts.kind ?? 'normal';
const data = opts.data ?? buildData(opts.defs);
const custom = opts.data ? null : standInDefs(opts.units);
const data = opts.data ?? buildData(custom ? { ...opts.defs, chess: { ...(opts.defs?.chess ?? {}), ...custom } } : opts.defs);
let stage = opts.stage ?? null;
if (!stage) stage = !opts.stageId || opts.stageId === 'flat' ? flatStage(opts.flat || {}) : data.getStage(opts.stageId);
if (!stage) throw new Error(`unknown stage ${opts.stageId}`);
@@ -181,7 +225,7 @@ export function makeBattle(opts = {}) {
if (extraRoutes.length) routes = routes.concat(extraRoutes);
const players = opts.players ?? [{
playerId: 'p1', seat: 0, side: 'L', colOffset: 0,
units: (opts.units ?? []).map((u, i) => ({ uid: u.uid ?? i + 1, kind: u.kind ?? 'chess', ...u })),
units: (opts.units ?? []).map((u, i) => ({ uid: u.uid ?? i + 1, kind: u.kind ?? 'chess', ...u, ...(u.standIn ? { standIn: true } : null) })),
bonds: opts.bonds ?? {}, bandId: opts.bandId ?? null, playerEffects: opts.playerEffects ?? [],
}];
const hookNames = opts.hooks ?? ALL_HOOKS;