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:
1 parent
286a34064f
commit
ed2b679f83
14 files changed
+656
-58
No files matched your search
+2
-2
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// Every `*.json` file in the data directory becomes a top-level key named after its basename
|
||||
// (config, chess, bonds, garrisons, items, bands, effects, choices, enemies, factions, waves, stages,
|
||||
// bosses, tokens, assets, …). The result is deep-frozen so no module can mutate shared data by accident.
|
||||
// bosses, tokens, assets, backups, …). The result is deep-frozen so no module can mutate shared data by accident.
|
||||
// Missing or unparsable files are tolerated with a warning (data is generated in parallel with the
|
||||
// server); consumers must cope with an absent key.
|
||||
//
|
||||
@@ -26,7 +26,7 @@ export const DATA_DIR = path.join(ROOT, 'data');
|
||||
/** Files the game expects (a warning lists the missing ones). */
|
||||
export const DATA_FILES = Object.freeze([
|
||||
'config', 'chess', 'bonds', 'garrisons', 'items', 'bands', 'effects', 'choices',
|
||||
'enemies', 'factions', 'waves', 'stages', 'bosses', 'tokens', 'assets',
|
||||
'enemies', 'factions', 'waves', 'stages', 'bosses', 'tokens', 'assets', 'backups',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -102,8 +102,11 @@ export class BattlePlayers {
|
||||
return u;
|
||||
}
|
||||
// the unit's own loadout (DESIGN §16): an entry without loadout fields is the DEFAULT — never another player's
|
||||
// choice for the same chess id in a multi-player field (the per-battle data view maps id-only lookups)
|
||||
const def = this.data.getChess(inp.chessId, { skillIndex: inp.skillIndex ?? null, moduleId: inp.moduleId ?? null });
|
||||
// choice for the same chess id in a multi-player field (the per-battle data view maps id-only lookups); `standIn:
|
||||
// true` fields the chess as its 补位 stand-in (simdata getStandIn: the stand-in's body, the chess's identity)
|
||||
const lo = { skillIndex: inp.skillIndex ?? null, moduleId: inp.moduleId ?? null };
|
||||
if (inp.standIn === true) lo.standIn = true;
|
||||
const def = this.data.getChess(inp.chessId, lo);
|
||||
if (!def) { this.log(`unknown chess ${inp.chessId}`); return null; }
|
||||
const u = this._makeAlly(ps, def, 'op', r, c, { uid: inp.uid, dir });
|
||||
u.items = [...(inp.items ?? [])];
|
||||
|
||||
+54
-18
@@ -7,6 +7,10 @@
|
||||
// a faulty content module is logged and skipped, never crashing the battle.
|
||||
// setupUnitKit(battle, unit, mode) — resolves a unit's Kit: kits[baseChessId]?.(bb, chess, def) ?? generic.
|
||||
// Battle opts.kits ({ [baseChessId]: kitFn }) take precedence (tests / sandboxes).
|
||||
// 补位 stand-ins (isStandInDef: the def of simdata getStandIn — the chess's ids, another character's body): the kit is
|
||||
// kits[def.charId] (the stand-in's own, kits/ops/standin-<codename>.js), never the chess id's — that is the replaced
|
||||
// operator's kit; without one, the generic kit plus the talents it can apply exactly (generic.js genericTalents).
|
||||
// A stand-in has no default skill: its kit authors every skill it supports in `skills` (selectSkillSpec).
|
||||
// Operator loadouts (DESIGN §16): the def is resolved for the unit's selected skill / module (simdata getChess), so
|
||||
// the kit receives `chess.skill` = the SELECTED skill record and `bb` = its blackboard (talents / trait / module of
|
||||
// the selected module). Kit contract (backward compatible): `{ skill?, skills?: { [skillId]: SkillSpec }, talents,
|
||||
@@ -19,7 +23,7 @@
|
||||
// are resolved by Battle (getToken(id, owner.defId, owner.def.loadout)), not here.
|
||||
// registerAllMeta(registry) — calls each domain module's registerMeta(registry) (prep side, server boot).
|
||||
|
||||
import { genericKit } from './generic.js';
|
||||
import { genericKit, genericTalents } from './generic.js';
|
||||
import { withUnitLoadouts } from '../simdata.js';
|
||||
|
||||
// Content files are loaded with guarded dynamic imports: a module that fails to load (syntax error, throwing
|
||||
@@ -38,12 +42,43 @@ const DOMAIN_NAMES = ['tokens', 'devices', 'enemies', 'bosses', 'bonds', 'garris
|
||||
const DOMAINS = await Promise.all(DOMAIN_NAMES.map((n) => safeImport(`./${n}.js`)));
|
||||
const tokens = DOMAINS[0];
|
||||
|
||||
/** Merged kit registry: baseChessId → (bb, chess, def) => Kit (kits/index.js; each kit file is loaded guarded too) */
|
||||
/**
|
||||
* Merged kit registry: baseChessId → (bb, chess, def) => Kit, then the 补位 stand-in kits: stand-in charId → builder
|
||||
* (kits/index.js; each kit file is loaded guarded too)
|
||||
*/
|
||||
export const KITS = KIT_REGISTRY.KITS ?? Object.freeze({});
|
||||
|
||||
/** Domain modules in install order: tokens, devices, enemies, bosses, bonds, garrisons, items, bands, choices. */
|
||||
export const MODULES = Object.freeze(DOMAIN_NAMES.map((n, i) => [n, DOMAINS[i]]));
|
||||
|
||||
/**
|
||||
* Whether `def` is a 补位 stand-in: a chess fielded with another character's body (simdata getStandIn — `standInFor` =
|
||||
* the replaced operator's charId, `charId` = the stand-in's, which differ).
|
||||
*/
|
||||
export function isStandInDef(def) {
|
||||
return !!(def && def.standInFor && def.charId && def.charId !== def.standInFor);
|
||||
}
|
||||
|
||||
/**
|
||||
* The kit builder of an operator def in `reg` ({ [key]: (bb, chess, def) => Kit }), or undefined. A stand-in's kit is
|
||||
* keyed by its charId (`char_609_acguad`) and by nothing else: the chess id it keeps names the replaced operator's kit.
|
||||
* Any other def: baseId (`…_a`, data/SIM.md), the exact id, or the suffix-less id of DESIGN §5.6's example.
|
||||
*/
|
||||
export function kitOf(def, reg) {
|
||||
if (!def || !reg) return undefined;
|
||||
if (isStandInDef(def)) return Object.prototype.hasOwnProperty.call(reg, def.charId) ? reg[def.charId] : undefined;
|
||||
const bare = String(def.baseId ?? def.id ?? '').replace(/_[ab]$/, '');
|
||||
return reg[def.baseId] ?? reg[def.id] ?? reg[bare];
|
||||
}
|
||||
|
||||
/** The generic kit of an operator def; a stand-in's also carries the talents generic.js can apply exactly. */
|
||||
function fallbackKit(bb, raw, def) {
|
||||
const k = genericKit(bb, raw, def);
|
||||
if (!isStandInDef(def)) return k;
|
||||
const talents = genericTalents(def);
|
||||
return talents.length ? { ...k, talents: [...(k.talents ?? []), ...talents] } : k;
|
||||
}
|
||||
|
||||
/** Resolve the Kit of an ally unit. Never throws (falls back to the generic kit). */
|
||||
export function setupUnitKit(battle, unit, mode = 'full') {
|
||||
try { syncUnitLoadout(battle, unit); } catch (e) { battle._handlerError?.('loadout', unit, e); }
|
||||
@@ -61,20 +96,17 @@ export function setupUnitKit(battle, unit, mode = 'full') {
|
||||
if (unit.kind !== 'op') return {};
|
||||
const injected = battle.opts && battle.opts.kits;
|
||||
if (mode === 'full' || injected) {
|
||||
// DESIGN §5.6's example keys kits by the suffix-less id (`chess_char_1_01`), data/SIM.md by baseId (`…_a`): accept both
|
||||
const bare = String(def.baseId ?? def.id ?? '').replace(/_[ab]$/, '');
|
||||
const pick = (reg) => reg?.[def.baseId] ?? reg?.[def.id] ?? reg?.[bare];
|
||||
const f = pick(injected) ?? (mode === 'full' ? pick(KITS) : undefined);
|
||||
const f = kitOf(def, injected) ?? (mode === 'full' ? kitOf(def, KITS) : undefined);
|
||||
if (typeof f === 'function') {
|
||||
try {
|
||||
const k = f(bb, raw, def);
|
||||
if (k) return selectSkillSpec(k, bb, raw, def);
|
||||
} catch (e) {
|
||||
battle._handlerError(`kit:${def.baseId}`, unit, e);
|
||||
battle._handlerError(`kit:${isStandInDef(def) ? def.charId : def.baseId}`, unit, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return genericKit(bb, raw, def);
|
||||
return fallbackKit(bb, raw, def);
|
||||
}
|
||||
|
||||
/** A unit's own PlayerBattleInput entry (operators: kind ≠ 'token'; tokens: kind 'token'), or null. */
|
||||
@@ -88,13 +120,15 @@ function inputEntry(unit) {
|
||||
/**
|
||||
* The loadout `{ skillIndex, moduleId }` a unit's PlayerBattleInput entry gives; an entry without loadout fields means
|
||||
* the DEFAULT loadout (`{}`), never "whatever the data view maps this chess id to" (another player's choice in a
|
||||
* multi-player field). Null when the unit has no input entry.
|
||||
* multi-player field). `standIn: true` rides along (补位: the stand-in def whatever the skill / module fields say).
|
||||
* Null when the unit has no input entry.
|
||||
*/
|
||||
function inputLoadout(unit) {
|
||||
const x = inputEntry(unit);
|
||||
if (!x) return null;
|
||||
if (x.skillIndex == null && x.moduleId == null) return {};
|
||||
return { skillIndex: x.skillIndex ?? null, moduleId: x.moduleId ?? null };
|
||||
const standIn = x.standIn === true ? { standIn: true } : null;
|
||||
if (x.skillIndex == null && x.moduleId == null) return standIn ?? {};
|
||||
return { skillIndex: x.skillIndex ?? null, moduleId: x.moduleId ?? null, ...standIn };
|
||||
}
|
||||
|
||||
/** Put a def on a not-yet-deployed ally (the fields Battle._makeAlly takes from the def). */
|
||||
@@ -143,13 +177,16 @@ export function syncUnitLoadout(battle, unit) {
|
||||
/** Id of the selected skill of an operator def (null without a skill). */
|
||||
const selectedSkillId = (def) => def?.skill?.id ?? def?.raw?.skill?.skillId ?? null;
|
||||
|
||||
/** True when the def's selected skill is the chess's default skill (no loadout info ⇒ default). */
|
||||
const skillIsDefault = (def) => !def?.loadout || def.loadout.skillIsDefault !== false;
|
||||
/**
|
||||
* True when the def's selected skill is the chess's default skill (no loadout info ⇒ default). Never for a stand-in:
|
||||
* the skill it fields is the one its chess names (S2 on one chess, S3 on another), so its kit authors each in `skills`.
|
||||
*/
|
||||
const skillIsDefault = (def) => !isStandInDef(def) && (!def?.loadout || def.loadout.skillIsDefault !== false);
|
||||
|
||||
/**
|
||||
* The kit with the skill spec of the SELECTED skill (see header): `kit.skills[id]` → `kit.skill` (default skill only)
|
||||
* → the generic spec. Returns the kit itself when nothing changes; `kit.skillSource` tells which one was used
|
||||
* ('skills' | 'kit' | 'generic').
|
||||
* The kit with the skill spec of the SELECTED skill (see header): `kit.skills[id]` → `kit.skill` (default skill only;
|
||||
* never for a stand-in) → the generic spec. Returns the kit itself when nothing changes; `kit.skillSource` tells which
|
||||
* one was used ('skills' | 'kit' | 'generic').
|
||||
* @param {object} kit hand-authored kit
|
||||
*/
|
||||
export function selectSkillSpec(kit, bb, raw, def) {
|
||||
@@ -172,8 +209,7 @@ export function selectSkillSpec(kit, bb, raw, def) {
|
||||
* @returns {'skills'|'kit'|'generic'|'none'} where the spec comes from ('none' = no kit function at all)
|
||||
*/
|
||||
export function skillSpecSource(def, kits = KITS) {
|
||||
const bare = String(def?.baseId ?? def?.id ?? '').replace(/_[ab]$/, '');
|
||||
const f = kits?.[def?.baseId] ?? kits?.[def?.id] ?? kits?.[bare];
|
||||
const f = kitOf(def, kits);
|
||||
if (typeof f !== 'function') return 'none';
|
||||
let k = null;
|
||||
try { k = f(def.skill?.bb ?? {}, def.raw ?? def, def); } catch { return 'generic'; }
|
||||
|
||||
@@ -9,14 +9,17 @@ operators, the self-select 6★ operators, contributions from GitHub issue #136
|
||||
|
||||
| path | what |
|
||||
|---|---|
|
||||
| `index.js` | the registry: `KIT_FILES` lists every kit file, grouped by tier; `KITS` (base chess id → kit builder) and `TIER_KITS` (one registry per tier group) are built from it |
|
||||
| `index.js` | the registry: `KIT_FILES` lists every kit file, grouped by tier, and `STANDIN_KIT_FILES` the 补位 stand-in kits; `KITS` (base chess id → kit builder, then stand-in charId → kit builder), `TIER_KITS` (one registry per tier group) and `STANDIN_KITS` are built from them |
|
||||
| `ops/<chessId>-<codename>.js` | one kit per file, with the helpers and constants only that kit uses |
|
||||
| `ops/standin-<codename>.js` | one 补位 stand-in kit per file (below: "Stand-in kits") |
|
||||
| `shared/tier1.js` | the general kit helpers (blackboard readers, unit predicates, hit hooks, area queries, buffs, zones, free tiles, skill records) and the notes of the tier-1 kits |
|
||||
| `shared/tier2.js` … `tier6.js` | helpers two or more kits of that tier use, and that tier's notes (conventions, simplifications, fx kinds) |
|
||||
| `tier1.js` … `tier6.js` | re-export shims for the old import paths; removed after the 0.2.0 refactor |
|
||||
|
||||
`content/index.js` takes `KITS` from `index.js`: a unit's kit is `KITS[def.baseId]` (also the exact or the suffix-less
|
||||
id), else the generic kit built from the skill blackboard (`content/generic.js`, docs/SIM.md §7.4).
|
||||
id), else the generic kit built from the skill blackboard (`content/generic.js`, docs/SIM.md §7.4). A 补位 stand-in's
|
||||
kit is `KITS[def.charId]` and nothing else (`content/index.js kitOf`): it keeps the chess's ids, which name the replaced
|
||||
operator's kit.
|
||||
|
||||
## Naming
|
||||
|
||||
@@ -96,6 +99,36 @@ export default {
|
||||
4. `node --test test/content/kits_layout.test.js` checks the name, the registry entry and the imports;
|
||||
`node tools/kit-coverage.mjs --missing` lists selectable skills that still fall back to the generic spec.
|
||||
|
||||
## Stand-in kits (补位)
|
||||
|
||||
A NORMAL chess whose operator the player does not own fights as its official stand-in (原型干员, data/backups.json,
|
||||
docs/DATA.md §18): the chess's ids, bonds, 特质, tier and price, the stand-in's body — stats, range, trait, talents, the
|
||||
skill `backup.skillIndex` and the module `backup.uniEquipId` the chess names. The sim gets that def from
|
||||
`battle.data.getChess(chessId, { standIn: true })` (a PlayerBattleInput entry with `standIn: true`); `def.charId` is the
|
||||
stand-in's, `def.standInFor` the replaced operator's charId.
|
||||
|
||||
- **File and key**: `ops/standin-<codename>.js`, the code name being the stand-in's charId without `char_<n>_`
|
||||
(`standin-acguad.js` = Sharp, `char_609_acguad`); the default export has exactly one key, that charId:
|
||||
`export default { char_609_acguad: (bb, chess, def) => Kit }`. Append the file name to `STANDIN_KIT_FILES` in
|
||||
`index.js`. Never register a stand-in under a chess id: that is the replaced operator's kit, which the stand-in must
|
||||
not run.
|
||||
- **Arguments**: `bb` = the blackboard of the skill the chess names, at the chess's level (normal Lv4, elite Lv7);
|
||||
`chess` = the composed record (shared/standIn.js `standInRecord`: talents, `trait.bb`, `module` of the stand-in);
|
||||
`def.tier` / `def.golden` tell which chess it stands in for.
|
||||
- **No default skill**: one stand-in takes S2 on one chess and S3 on another (Sharp: S2 for 银灰, S3 for 隐德来希), so the
|
||||
kit authors every skill it supports in `skills: { [skillId]: SkillSpec }`; `skill` is ignored for a stand-in (a skill
|
||||
missing from `skills` gets the generic spec). `talents`, `trait` and `install` apply as for any kit.
|
||||
- **Without a file** the stand-in fights with the generic kit plus the talents it can apply exactly
|
||||
(`generic.js genericTalents`: unconditional stat lines like "攻击力+8%"). The eight 预备干员 need no file: their skills
|
||||
(攻击力 / 防御力 / 治疗强化, 战术咏唱, 冲锋号令, 一击即退) are covered by the generic spec
|
||||
(`test/content/standin.test.js`).
|
||||
- **Auto-cast**: the trigger comes from the data like any chess's (checklist item 5). The 重装 exception for the
|
||||
stand-ins is `tools/build-data.mjs STANDIN_TRIGGER_DEVIATIONS` (预备干员-重装, Mechanist: every skill `DEFAULT`,
|
||||
`rawRule` `TAKE_DAMAGE`).
|
||||
- **Summons**: no stand-in has one (DATA.md §18).
|
||||
- `node --test test/content/kits_layout.test.js` checks the file name, the key (a charId of data/backups.json `units`)
|
||||
and the registry entry.
|
||||
|
||||
## Testing a kit
|
||||
|
||||
- The harness is `test/helpers/battleHarness.js` (`makeBattle`, `runUntil`, `hooksOf`, `eventsOf`, `checkInvariants`;
|
||||
@@ -112,6 +145,22 @@ export default {
|
||||
- Run `node --test test/content/kits_layout.test.js test/golden.test.js <your test file>`, then
|
||||
`GOLDEN_FULL=1 node --test test/golden.test.js` before the pull request.
|
||||
|
||||
### How to test a stand-in kit
|
||||
|
||||
- Field the chess as its stand-in: `makeBattle({ units: [{ chessId: 'chess_char_5_13_b', row: 10, col: 4, standIn: true
|
||||
}] })` — the production path (data/backups.json through `getChess(id, { standIn: true })`): `_a` = the normal form
|
||||
(Lv4), `_b` = the elite (Lv7, its module), with the chess's backup skill. Pick one chess per skill the stand-in
|
||||
fields (data/chess.json `backup`; `backups.json units[charId].standsIn` lists them).
|
||||
- Another skill or module of the stand-in (a skill no chess names, a 自选 prototype): `standIn: { skillIndex: 0 }` or
|
||||
`standIn: { skillIndex: 2, moduleId: 'none' }` — the harness composes that record (`standInRec`) and puts it under the
|
||||
chess id for the whole battle, so do not field the real operator of that chess in the same battle.
|
||||
- Inject a kit for a quick experiment with `kits: { char_609_acguad: (bb, chess, def) => Kit }` (keyed by the charId).
|
||||
- Assert that the unit is the stand-in (`u.def.charId`, `u.def.standInFor`, `u.skill.id`) and that its kit is yours, not
|
||||
the generic one (`!u.kit.generic`); then the checklist below as for any operator. Examples:
|
||||
`test/content/standin.test.js` (the plumbing and the eight 预备干员).
|
||||
- A stand-in kit is new gameplay for stand-ins only: the golden corpus fields no stand-in, so its digests stay as they
|
||||
are.
|
||||
|
||||
## The fidelity rule
|
||||
|
||||
Every official skill, talent and module is implemented exactly as the game has it — no approximation.
|
||||
@@ -154,7 +203,7 @@ Each item is a mistake this project already made once. Tick every one for every
|
||||
the rule, such as 烛煌 S3 in `feedback1e-skillrange.test.js`, are not examples of it); an AUTO skill that acts on
|
||||
allies or itself fires at full SP (引星棘刺 S1, `kits_alt_t5.test.js` "… fires as soon as its SP is full, no enemy
|
||||
needed (GitHub #124)"); the 重装 exception list (`TRIGGER_DEVIATIONS`: 深巡 S2, 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2,
|
||||
余 S2; the 预备干员-重装 / Mechanist stand-ins join it), DESIGN §21.29 / §22.10,
|
||||
余 S2; the 预备干员-重装 / Mechanist stand-ins: `STANDIN_TRIGGER_DEVIATIONS`, every skill), DESIGN §21.29 / §22.10,
|
||||
`test/sim/feedback1-tank-triggers.test.js`.
|
||||
- [ ] **6. Targeting** — can it hit air units (`canHitFly`), ground-only attacks, block count, target priority; can
|
||||
ground enemies hit it (起飞 / 对地规避, stealth, camouflage flags). Examples: `test/sim/professions.test.js` "fortress
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
// Each file is loaded with a guarded dynamic import, so this runs unchanged in the browser (/sim/, no Node-only
|
||||
// module, no directory listing): a file that fails to load (syntax error, throwing top-level code, missing file) is
|
||||
// logged and skipped — its chess falls back to the generic kit — instead of breaking the server or the page.
|
||||
//
|
||||
// 补位 stand-in kits (DATA.md §18): STANDIN_KIT_FILES lists `ops/standin-<codename>.js`, one per stand-in character
|
||||
// (codename = its charId without `char_<n>_`: `standin-acguad.js` = Sharp, char_609_acguad); each registers exactly one
|
||||
// key, that charId. A stand-in keeps the ids of the chess it replaces, so content/index.js kitOf finds its kit by
|
||||
// `def.charId` only, never by the chess id (the replaced operator's kit). STANDIN_KITS is merged into KITS after every
|
||||
// tier group: the chess keys keep their order, and `char_…` keys never meet `chess_char_…` ones.
|
||||
|
||||
export const KIT_FILES = Object.freeze([
|
||||
// tier 1
|
||||
@@ -53,6 +59,13 @@ export const KIT_FILES = Object.freeze([
|
||||
'chess_char_6_20-agoat2.js'],
|
||||
].map((group) => Object.freeze(group)));
|
||||
|
||||
/**
|
||||
* The 补位 stand-in kit files (`ops/standin-<codename>.js`, registry key = the stand-in's charId; a new file is appended).
|
||||
* A stand-in without a file fights with the generic kit (content/generic.js) plus its unconditional stat talents
|
||||
* (genericTalents) — the eight 预备干员 need no file.
|
||||
*/
|
||||
export const STANDIN_KIT_FILES = Object.freeze([]);
|
||||
|
||||
async function loadKitFile(file) {
|
||||
try {
|
||||
return await import(`./ops/${file}`);
|
||||
@@ -63,10 +76,14 @@ async function loadKitFile(file) {
|
||||
}
|
||||
|
||||
const MODULES = await Promise.all(KIT_FILES.map((group) => Promise.all(group.map(loadKitFile))));
|
||||
const STANDIN_MODULES = await Promise.all(STANDIN_KIT_FILES.map(loadKitFile));
|
||||
const registryOf = (m) => (m && m.default && typeof m.default === 'object' ? m.default : {});
|
||||
|
||||
/** The registry of each tier group (index 0 = tier 1): baseChessId → kit builder, in KIT_FILES order. */
|
||||
export const TIER_KITS = Object.freeze(MODULES.map((group) => Object.assign({}, ...group.map(registryOf))));
|
||||
|
||||
/** The merged kit registry: baseChessId → (bb, chess, def) => Kit, tier 1 … tier 6. */
|
||||
export const KITS = Object.freeze(Object.assign({}, ...TIER_KITS));
|
||||
/** The 补位 stand-in kits: stand-in charId → kit builder, in STANDIN_KIT_FILES order. */
|
||||
export const STANDIN_KITS = Object.freeze(Object.assign({}, ...STANDIN_MODULES.map(registryOf)));
|
||||
|
||||
/** The merged kit registry: baseChessId → (bb, chess, def) => Kit, tier 1 … tier 6, then the stand-ins' charIds. */
|
||||
export const KITS = Object.freeze(Object.assign({}, ...TIER_KITS, STANDIN_KITS));
|
||||
+46
-4
@@ -16,6 +16,11 @@
|
||||
// match / UI (implemented in shared/loadoutRecord.js, re-exported here): resolveLoadout, loadoutRecord (the resolved
|
||||
// chess record), composeStats / composeTalents. (Which choices a player may make is shared/protocol.js
|
||||
// loadoutOptions / checkLoadout.)
|
||||
// 补位 stand-ins (DATA.md §18): `getChess(id, { standIn: true })` = the def of a NORMAL chess fielded as its official
|
||||
// stand-in (shared/standIn.js standInRecord over data/backups.json: the chess's ids, bonds, 特质, tier; the stand-in's
|
||||
// body, skill `backup.skillIndex` and module `backup.uniEquipId` — any other skill / module choice is ignored), with
|
||||
// `def.standInFor` = the replaced operator's charId and `def.charId` = the stand-in's (content/index.js looks its kit
|
||||
// up by that charId). A PRESET / DIY chess, or a source without backups.json, gives the chess's own def instead.
|
||||
//
|
||||
// This module is pure ESM shared with browsers (served at /sim/): no Node API is imported here. Under Node the default
|
||||
// loader (./nodeData.js) is imported dynamically at module evaluation; browsers never load it.
|
||||
@@ -25,6 +30,7 @@
|
||||
// getSimData() the injected data (browser) or the generated data (Node), or null.
|
||||
|
||||
import { resolveRecordLoadout, composeStats, composeTalents, loadoutRecord } from '../../shared/loadoutRecord.js';
|
||||
import { standInRecord } from '../../shared/standIn.js';
|
||||
import { normHitArea } from './body.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -193,6 +199,8 @@ export function normalizeChess(rec) {
|
||||
tokens: rec.tokens ?? [],
|
||||
spine: rec.assets?.spine ?? rec.spine ?? rec.charId ?? null,
|
||||
avatar: rec.assets?.avatar ?? rec.avatar ?? rec.charId ?? null,
|
||||
// a 补位 stand-in (shared/standIn.js): the replaced operator's charId — `charId` is the stand-in's (absent otherwise)
|
||||
...(rec.standInFor ? { standInFor: rec.standInFor } : null),
|
||||
raw: rec,
|
||||
};
|
||||
}
|
||||
@@ -509,7 +517,8 @@ export function freezeDef(d) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A DataSource resolves ids to normalised defs. `raw` is `{ chess, enemies, tokens, stages, waves }`.
|
||||
* A DataSource resolves ids to normalised defs. `raw` is `{ chess, enemies, tokens, stages, waves, backups? }`
|
||||
* (`backups` = data/backups.json `{ units, diy }`, the 补位 stand-ins: getChess(id, { standIn: true })).
|
||||
*/
|
||||
export class DataSource {
|
||||
constructor(raw = {}, fallback = null) {
|
||||
@@ -519,6 +528,7 @@ export class DataSource {
|
||||
tokens: asMap(unwrap(raw.tokens, 'tokens'), 'tokenId') ?? {},
|
||||
stages: asMap(unwrap(raw.stages, 'stages'), 'id') ?? {},
|
||||
waves: asMap(unwrap(raw.waves, 'waves'), 'id') ?? {},
|
||||
backups: raw.backups && typeof raw.backups === 'object' && raw.backups.units ? raw.backups : null,
|
||||
};
|
||||
this.fallback = fallback;
|
||||
this._chess = new Map();
|
||||
@@ -534,13 +544,22 @@ export class DataSource {
|
||||
rawToken(id) { return this.raw.tokens[id] ?? this.fallback?.rawToken(id) ?? null; }
|
||||
rawStage(id) { return this.raw.stages[id] ?? this.fallback?.rawStage(id) ?? null; }
|
||||
rawWave(id) { return this.raw.waves[id] ?? this.fallback?.rawWave(id) ?? null; }
|
||||
/** data/backups.json (`{ units, diy }`) of this source or its fallback, or null. */
|
||||
rawBackups() { return this.raw.backups ?? (typeof this.fallback?.rawBackups === 'function' ? this.fallback.rawBackups() : null); }
|
||||
|
||||
/**
|
||||
* Chess def for the default loadout, or for `loadout` `{ skillIndex, moduleId }` (DESIGN §16; resolved with
|
||||
* resolveLoadout — illegal choices fall back to the default). `def.loadout` = the resolved loadout (null for records
|
||||
* without skill choices); `def.id` is always the chess id. Defs are cached per (id, resolved loadout).
|
||||
* `loadout.standIn === true`: the 补位 def of the chess (getStandIn) — its skill and module are the chess's backup
|
||||
* selection whatever `skillIndex` / `moduleId` say; a chess without a stand-in (PRESET, DIY, no backups data) gives
|
||||
* its own def for that loadout.
|
||||
*/
|
||||
getChess(id, loadout = null) {
|
||||
if (loadout && loadout.standIn === true) {
|
||||
const sd = this.getStandIn(id);
|
||||
if (sd) return sd;
|
||||
}
|
||||
if (loadout == null && this._chess.has(id)) return this._chess.get(id);
|
||||
const r = this.rawChess(id);
|
||||
const lo = r ? resolveLoadout(r, loadout) : null;
|
||||
@@ -555,6 +574,27 @@ export class DataSource {
|
||||
this._chess.set(key, d);
|
||||
return d;
|
||||
}
|
||||
/**
|
||||
* The 补位 def of chess `id` (DATA.md §18): shared/standIn.js standInRecord of its record over rawBackups(), normalised
|
||||
* — `def.id` / `baseId` / `golden` / `tier` / `bonds` / `raw.garrisonIds` of the chess, every combat field of the
|
||||
* stand-in at the chess's status, `def.charId` the stand-in's, `def.standInFor` the replaced operator's charId,
|
||||
* `def.loadout` the backup selection (`isDefault`, `standIn: true`). Null for a chess without a stand-in (PRESET,
|
||||
* DIY) or when the data lacks it. Cached per id.
|
||||
*/
|
||||
getStandIn(id) {
|
||||
const key = `${id}|standIn`;
|
||||
if (this._chess.has(key)) return this._chess.get(key);
|
||||
const r = this.rawChess(id);
|
||||
const rec = r ? standInRecord(r, this.rawBackups()) : null;
|
||||
let d = null;
|
||||
if (rec) {
|
||||
d = normalizeChess({ ...rec, chessId: id });
|
||||
d.loadout = { ...resolveLoadout(rec, null), standIn: true };
|
||||
freezeDef(d);
|
||||
}
|
||||
this._chess.set(key, d);
|
||||
return d;
|
||||
}
|
||||
getEnemy(key) {
|
||||
if (this._enemy.has(key)) return this._enemy.get(key);
|
||||
const r = this.rawEnemy(key);
|
||||
@@ -691,7 +731,8 @@ export function toDataSource(data) {
|
||||
* lookups (`view.loadoutConflicts` lists them) — callers that pass the unit's own loadout (`getChess(id, { skillIndex,
|
||||
* moduleId })`, `getToken(id, owner, ownerDef.loadout)`) are always exact. Returns `ds` itself when no unit carries a
|
||||
* loadout, and an existing view unchanged (`isLoadoutView`). Used by spec.js createBattleFromSpec and by
|
||||
* content/index.js installContent (every Battle, however constructed).
|
||||
* content/index.js installContent (every Battle, however constructed). An entry with `standIn: true` (补位) maps its
|
||||
* chess id to the stand-in def (`getChess(id, { standIn: true })`) the same way.
|
||||
* @param {object} ds DataSource
|
||||
* @param {object[]} players PlayerBattleInput[] (spec players)
|
||||
*/
|
||||
@@ -703,11 +744,12 @@ export function withUnitLoadouts(ds, players) {
|
||||
for (const p of Array.isArray(players) ? players : []) {
|
||||
for (const u of (p && Array.isArray(p.units) ? p.units : [])) {
|
||||
if (!u || u.kind === 'token' || typeof u.chessId !== 'string') continue;
|
||||
if (u.skillIndex != null || u.moduleId != null) any = true;
|
||||
if (u.skillIndex != null || u.moduleId != null || u.standIn === true) any = true;
|
||||
const lo = { skillIndex: u.skillIndex ?? null, moduleId: u.moduleId ?? null };
|
||||
if (u.standIn === true) lo.standIn = true;
|
||||
const prev = map.get(u.chessId);
|
||||
if (!prev) map.set(u.chessId, lo);
|
||||
else if (prev.skillIndex !== lo.skillIndex || prev.moduleId !== lo.moduleId) conflicts.add(u.chessId);
|
||||
else if (prev.skillIndex !== lo.skillIndex || prev.moduleId !== lo.moduleId || !!prev.standIn !== !!lo.standIn) conflicts.add(u.chessId);
|
||||
}
|
||||
}
|
||||
if (!any) return ds;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//
|
||||
// b.snap = { fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total }
|
||||
// UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?,
|
||||
// form?, skillIndex?, moduleId?, items? } (form = the unit's current model form — an enemy's, content/enemies/helpers.js setForm:
|
||||
// form?, skillIndex?, moduleId?, items?, standInFor? } (standInFor = the replaced operator's charId of a 补位 stand-in;
|
||||
// form = the unit's current model form — an enemy's, content/enemies/helpers.js setForm:
|
||||
// 掠海漂移体 'crawl', 暴鸰 'bombed', 转译基底·α's forms …; a 傀儡师 fighting as its 替身 'doll', professions.js — a view built
|
||||
// after the change, a field opened mid-battle, draws it: render/units.js FORMS)
|
||||
// dir = 'UP'|'RIGHT'|'DOWN'|'LEFT' (allies: the deploy direction, sim/dir.js); facing = its horizontal sign (±1).
|
||||
@@ -51,6 +52,9 @@ export function unitInfo(u) {
|
||||
// an ally operator's equipped item ids (display: a 变形同构体 wearer counts for the bond it grants — the bond popup's
|
||||
// member list and the detail card's bond chips of a teammate's unit)
|
||||
items: u.side === 'ally' && u.kind === 'op' && Array.isArray(u.items) && u.items.length ? [...u.items] : undefined,
|
||||
// 补位: the replaced operator's charId when the chess fights as its stand-in (spine / avatar are the stand-in's; the
|
||||
// detail card composes the stand-in record, shared/standIn.js standInRecord)
|
||||
standInFor: u.side === 'ally' && u.kind === 'op' && typeof d.standInFor === 'string' ? d.standInFor : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -20,6 +20,8 @@
|
||||
// the default). createBattleFromSpec hands the Battle a per-battle data view (withUnitLoadouts) that resolves each
|
||||
// operator def — and its summons — for the loadout of its chess (simdata getChess(id, loadout) / getToken(id, owner,
|
||||
// ownerLoadout)); an explicit loadout argument always wins over the view's per-chess lookup.
|
||||
// 补位 (DATA.md §18): an operator entry with `standIn: true` (kept only when exactly `true`) is fielded as its chess's
|
||||
// stand-in (simdata getChess(id, { standIn: true }); its skill / module are the chess's backup selection).
|
||||
|
||||
import { Battle } from './Battle.js';
|
||||
import { toDataSource, withUnitLoadouts } from './simdata.js';
|
||||
@@ -76,11 +78,15 @@ export function buildBattleSpec(o = {}) {
|
||||
|
||||
const LOADOUT_ID = /^[A-Za-z0-9_\-]{1,64}$/;
|
||||
|
||||
/** Keep a unit's `skillIndex` / `moduleId` only when well-formed (the data layer checks legality). Mutates `u`. */
|
||||
/**
|
||||
* Keep a unit's `skillIndex` / `moduleId` only when well-formed (the data layer checks legality) and `standIn` only
|
||||
* when exactly `true` (补位). Mutates `u`.
|
||||
*/
|
||||
export function sanitizeUnitLoadout(u) {
|
||||
if ('skillIndex' in u && !(Number.isInteger(u.skillIndex) && u.skillIndex >= 0 && u.skillIndex <= 9)) delete u.skillIndex;
|
||||
if ('moduleId' in u && !(typeof u.moduleId === 'string' && LOADOUT_ID.test(u.moduleId))) delete u.moduleId;
|
||||
if (u.kind === 'token') { delete u.skillIndex; delete u.moduleId; }
|
||||
if ('standIn' in u && u.standIn !== true) delete u.standIn;
|
||||
if (u.kind === 'token') { delete u.skillIndex; delete u.moduleId; delete u.standIn; }
|
||||
return u;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user