From 171efe72de6f431f3bee612ced1488d9eb556c6f Mon Sep 17 00:00:00 2001 From: sganggs Date: Mon, 5 Oct 2026 20:16:57 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20WH:=20docs=20for=20=E8=87=AA?= =?UTF-8?q?=E9=80=89=20=E2=80=94=20kits/README.md=20"How=20to=20add=20an?= =?UTF-8?q?=20operator=20(=E8=87=AA=E9=80=89)"=20(who=20still=20lacks=20a?= =?UTF-8?q?=20kit,=20the=20built=20data,=20file=20naming=20and=20registrat?= =?UTF-8?q?ion,=20the=20kit=20contract,=20the=20fidelity=20checklist,=20th?= =?UTF-8?q?e=20test=20pattern,=20golden,=20art);=20DATA.md=20=C2=A716?= =?UTF-8?q?=E2=80=93=C2=A718=20(72=20owned=20units=20and=20their=20summons?= =?UTF-8?q?,=20diy.locked=20/=20excluded,=20shared/diy.js,=20the=20counts)?= =?UTF-8?q?,=20SIM.md=20=C2=A77.2=20/=20=C2=A79=20/=20=C2=A710=20/=20?= =?UTF-8?q?=C2=A712=20(=E8=87=AA=E9=80=89=20defs,=20kits,=20summons,=20har?= =?UTF-8?q?ness,=20UnitInfo.diy),=20DESIGN's=20v1=20scope=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DATA.md | 70 ++++++++++++++++++---------- docs/DESIGN.md | 2 +- docs/SIM.md | 23 +++++++++- server/sim/content/kits/README.md | 76 +++++++++++++++++++++++++++++-- 4 files changed, 142 insertions(+), 29 deletions(-) diff --git a/docs/DATA.md b/docs/DATA.md index 0a9e4a81..5d2788c5 100644 --- a/docs/DATA.md +++ b/docs/DATA.md @@ -501,8 +501,9 @@ Glyph legend (`rows`): `…_eagle1/3` stay listed (`sources:["display"]`, belong to S1/S3). With another skill selected (DESIGN §16) the talent follows that skill's eagle (`simdata loadoutRecord`; `variants[o].bySkill[i].sources`). 2. **DIY chess** (`chess_char_5_diy1/2`, `chess_char_6_diy1/2`, `_a` and `_b` = 8 records): no `charId`, no stats/skill; `visible:false`, - name placeholder `甄选干员`, not in the shop. Their picks and bond rule are in `backups.json diy` (§18); the gameplay - is not implemented yet. + name placeholder `甄选干员`, not in the shop. Their picks and bond rule are in `backups.json diy` (§18); a slot fights + only as a 自选 piece — a PlayerBattleInput entry with its `diy` pick (docs/SIM.md §12; shared/diy.js) — and a slot + without a legal pick fields nothing. The picker and the per-player shop are not implemented yet. 3. **Module-less chess**: 蒂比 (`chess_char_2_13`) and 凛御银灰 (`chess_char_5_14`) have no module; their golden record has `module:{id:null,active:false}` and no module stat bonus. 4. **Hidden chess (17)** are kept with `visible:false`; several operators exist in two tiers with one hidden @@ -562,14 +563,16 @@ Glyph legend (`rows`): `chess 266 (112 visible; 283 selectable skills over the visible chess, 184 module choices over 129 goldens; 74 PRESET / 55 NORMAL / 4 DIY base chess)`, `bonds 23`, `garrisons 249 (43 effect keys)`, `items 115`, `bands 40`, `effects 361`, `enemies 249`, `factions 67 entries`, `waves 38`, `stages 11 (8 active)`, `bosses 10`, `tokens 22`, `choice events 109`, -`bounty cards 129`, `tactic cards 43`, `backups: 17 stand-in units (43 forms), 4 DIY slots, 15 / 9 prototype picks (tier 5 / 6), 78 owned-6★ picks`. +`bounty cards 129`, `tactic cards 43`, `backups: 89 units (17 stand-ins, 72 owned-6★ picks; 259 forms), 38 自选 summons, 4 DIY slots, 15 / 9 prototype picks (tier 5 / 6), 72 owned-6★ picks (6 collab operators excluded)`. ## 17. Integrity guarantees (checked by the builder and `test/data.test.js`) Every PRESET chess is its own backup and the stand-in builder gives it back field for field (§18); every NORMAL chess (both forms) composes into its stand-in with an unlocked backup skill and, on the elite form, its module at that level; -every DIY prototype pick has a form for both slot statuses; derived DIY bonds exist (`test/backups.test.js` also -re-derives the backup fields, the stand-in numbers and the faction ids from the raw tables). +every DIY prototype pick has a form for both slot statuses and its locked skill / module there; every owned pick has +both slot forms with its three skills, every module of the character at the elite's stage and a `tokens` variant per +summon; derived DIY bonds exist; no 自选 summon id is a tokens.json record (`test/backups.test.js` also re-derives the +backup fields, the stand-in numbers, the owned pool and the faction ids from the raw tables). Chess bonds/garrisons/tokens/base/golden ids resolve; talent tokens are in `chess.tokens`; every non-DIY chess has `skills[]` with exactly one default equal to `skill` (and every skill token listed in `chess.tokens`); golden module choices are consistent (one default iff `module.active`, base fields present; composing the default reproduces the @@ -589,13 +592,14 @@ included) × allowed entry, and `test/sim/pathing-crosscheck.test.js` re-impleme whole map for every walkable tile of the normal / 联防 / boss fields of the 8 stages (extended by the road-over-floor preference; against the pure official algorithm: identical route lengths, never more non-blockable tiles crossed). -## 18. `backups.json` — 补位 stand-ins and 自选 (DIY) data — `{ units, diy }` +## 18. `backups.json` — 补位 stand-ins and 自选 (DIY) data — `{ units, tokens, diy }` -The data of two official features whose gameplay is not implemented yet (DESIGN 0.2.0 draft): **补位** — a NORMAL chess -whose operator the player does not own is fielded as its official stand-in (原型干员) — and **自选编队** — two tier-5 and -two tier-6 DIY slots, each filled with a 6★ the player owns or a prototype. Built by `tools/build-data.mjs buildBackups`; -`shared/standIn.js` composes it into chess-shaped records. The rules in the data (activity_table act2autochess -`charShopChessDatas`; PRTS 卫戍协议, 卫戍协议:盟约 下半/PRTS盟约记录): +The data of two official features (DESIGN 0.2.0 draft): **补位** — a NORMAL chess whose operator the player does not own +is fielded as its official stand-in (原型干员) — and **自选编队** — two tier-5 and two tier-6 DIY slots, each filled with a +6★ the player owns or a prototype. Built by `tools/build-data.mjs buildBackups`; `shared/standIn.js` composes it into +chess-shaped records, `shared/diy.js` adds the 自选 rules on top. The sim fields both (docs/SIM.md §12); the 自选 picker +and per-player shop come later. The rules in the data (activity_table act2autochess `charShopChessDatas`; PRTS 卫戍协议, +卫戍协议:盟约 下半/PRTS盟约记录): - PRESET (74, 特许干员) always fields the real operator (`backup.charId` = itself); NORMAL (55) names one of 17 stand-ins — the 4★ 预备干员 `char_600–607`, the 6★ `char_608–615` and 领主·Sharp `char_617` (the "其它分支"); DIY (4) has none. @@ -604,21 +608,22 @@ two tier-6 DIY slots, each filled with a 6★ the player owns or a prototype. Bu character takes S2 on one chess and S3 on another), module `backup.uniEquipId` (null at tiers 3–4 and for every 4★, so even their elite form has none) and potential 0 (`potRank` 0 on all 55). -`units[charId]` — the 17 stand-ins (no unit for a PRESET or DIY chess): +`units[charId]` — first the 17 stand-ins, then the 72 owned-6★ 自选 picks (`diy.ownedPool`); no unit for a PRESET or DIY +chess: | Field | Example (`char_611_acnipe`) | Meaning | |---|---|---| -| `charId`, `name`, `appellation`, `rarity`, `profession`, `subProfessionId`, `subProfessionName`, `position`, `nationId`, `isNotObtainable` | `"char_611_acnipe"`, `"Stormeye"`, `"Stormeye"`, `6`, `"SNIPER"`, `"fastshot"`, `"速射手"`, `"RANGED"`, `null`, `true` | as on a chess record | -| `assets` | `{"avatar":"char_611_acnipe","avatarGolden":"char_611_acnipe","portrait":"char_611_acnipe_1","portraitGolden":"char_611_acnipe_1","spine":"char_611_acnipe","subProfIcon":"sub_fastshot_icon"}` | art ids (URLs in `data/assets.json`, which already carries all 17); the elite form takes the E2 art when it exists — only 领主·Sharp has it | +| `charId`, `name`, `appellation`, `rarity`, `profession`, `subProfessionId`, `subProfessionName`, `position`, `nationId`, `isNotObtainable` | `"char_611_acnipe"`, `"Stormeye"`, `"Stormeye"`, `6`, `"SNIPER"`, `"fastshot"`, `"速射手"`, `"RANGED"`, `null`, `true` | as on a chess record (`isNotObtainable` false for the owned picks) | +| `assets` | `{"avatar":"char_611_acnipe","avatarGolden":"char_611_acnipe","portrait":"char_611_acnipe_1","portraitGolden":"char_611_acnipe_1","spine":"char_611_acnipe","subProfIcon":"sub_fastshot_icon"}` | art ids (URLs in `data/assets.json`, which carries all 89 — the owned picks since 0.2.0, ASSETS.md); the elite form takes the E2 art when it exists — of the stand-ins only 领主·Sharp has it, every owned pick does | | `moduleNames` | `{"uniequip_001_acnipe":{"name":"Stormeye证章","typeName":"ORIGINAL"},"uniequip_002_acnipe":{"name":"Stormeye证章","typeName":"MAR-X"}}` | every module of the character (a composed record names its module on the normal form too) | -| `standsIn[]` | `["chess_char_3_21_a","chess_char_4_02_a","chess_char_5_18_a","chess_char_5_22_a","chess_char_6_01_a","chess_char_6_05_a"]` | the NORMAL base chess it replaces | -| `forms[statusKey]` | keys `"2/1/4/0"`, `"2/60/7/1"`, `"2/60/7/3"` | the character at every status it fights at — of the chess it stands in for and of the DIY slots it may fill (`statusKey(status)` = `phase/level/skillLevel/equipLevel`): 3 forms per 6★, 2 per 4★ | +| `standsIn[]` | `["chess_char_3_21_a","chess_char_4_02_a","chess_char_5_18_a","chess_char_5_22_a","chess_char_6_01_a","chess_char_6_05_a"]` | the NORMAL base chess it replaces (`[]` for an owned pick) | +| `forms[statusKey]` | keys `"2/1/4/0"`, `"2/60/7/1"`, `"2/60/7/3"` | the character at every status it fights at — of the chess it stands in for and of the DIY slots it may fill (`statusKey(status)` = `phase/level/skillLevel/equipLevel`): 3 forms per 6★ (an owned pick: exactly the three DIY slot statuses — E2 Lv1 skill rank 4 without a module, E2 Lv60 rank 7 with every module at stage 1 and at stage 3), 2 per 4★ | A **form** holds the operator fields of a chess record with **nothing selected**: `status`; `stats`, `trait`, `talents` **without** a module; `immunities`, `rangeId`, `rangeGrid`, `dmgType`, `attackKind`, `projectile`, `canHitFly`, `targetPriority`; `skills[]` — every skill unlocked at the status, at its skill level, `trigger` resolved per skill, no -`isDefault`; `displayTokens` / `tokens` (summons — none of the 17 has one); at `equipLevel > 0` `modules[]` (§2.2 shape -without `isDefault`). `buildUnitForm` uses buildChess's helpers and rules, and the build fails when `buildUnitForm` + +`isDefault`; `displayTokens` / `tokens` (summons — none of the 17 stand-ins has one; 27 of the owned picks do, their +records in `tokens` below); at `equipLevel > 0` `modules[]` (§2.2 shape without `isDefault`). `buildUnitForm` uses buildChess's helpers and rules, and the build fails when `buildUnitForm` + `composeUnitRecord` do not give back every PRESET chess field for field (each is its own backup), so a later change to buildChess that the stand-ins would miss stops the build. The chess trigger deviations (§2.2, DESIGN §21.29) name chess and never apply to a form; the 重装 stand-ins have their own, by charId (`tools/build-data.mjs STANDIN_TRIGGER_DEVIATIONS`, @@ -634,8 +639,18 @@ elite `statsBase` / `traitBase` / `talentsBase` / `modules[]`), so `normalizeChe backup selection) and `loadoutRecord` read it unchanged; null for a PRESET or DIY chess. **In battle** a PlayerBattleInput entry with `standIn: true` is fielded as `getChess(chessId, { standIn: true })` (docs/SIM.md §12: this record, normalised), and its kit is found by its `charId` (`server/sim/content/index.js kitOf`; kits/README.md "Stand-in kits") — never by -the chess id it keeps, which names the replaced operator's kit. `diyRecord(slot, charId, backups, { skillIndex, moduleId })` fills a DIY slot with a prototype (the slot's -tier and price, no 特质, the pick's derived bonds); which skill a prototype carries in a slot is not in the data. +the chess id it keeps, which names the replaced operator's kit. A 自选 piece is composed by `shared/diy.js`: +`checkDiyPick(slotId, pick, data)` checks one pick `{ charId, skillIndex?, uniEquipId? }` against a slot (a pick of the +slot's tier; a prototype takes its `diy.locked` selection, another skill is refused; an owned pick names one of its three +skills and optionally a module of its elite form at the slot's stage), `diyRecord(slotId, pick, { elite, data })` / +`diyRecordOf(slot, pick, data)` give the record — the slot's identity (tier, price, merge, status; no 特质), the pick's +derived bonds, the operator's form at the slot's status with that skill and module (active on the elite only), plus +`diyFor` = the slot's base id —, `diyPool(tier, { data, kitted })` the legal picks of a tier (prototypes, then the owned +pool; with `kitted`, only operators with a kit: server/sim/content/kits/index.js `KITTED_CHARS`) and +`validateDiyPicks(picks, { data, kitted })` a roster (a prototype may fill a tier-5 and a tier-6 slot, an owned operator +one slot, the picks of a tier differ). **In battle** a PlayerBattleInput entry of a DIY slot carries `diy` (the pick) and +is fielded as `getChess(slotId, { diy })` (docs/SIM.md §12); its kit is `KITS[charId]` (kits/README.md "How to add an +operator (自选)"). (`shared/standIn.js diyRecord(slot, charId, backups, sel)` is the older prototype-only form.) `diy`: @@ -643,9 +658,18 @@ tier and price, no 特质, the pick's derived bonds); which skill a prototype ca |---|---|---| | `slots[slotId]` | `{"tier":5,"goldenId":"chess_char_5_diy1_b","shopLevel":5,"requirement":"TIER_6"}` | the four DIY chess `chess_char_5_diy1/2_a`, `chess_char_6_diy1/2_a` (chess.json: price 4, sell 1, `diyRequirement`, empty `bonds` / `garrisonIds`); `shopLevel` = the 调度中心 level whose `shopLevelDisplayDataDict.charChessDiySlotIdList` lists the slot | | `prototypes[tier]` | `{"5":[…15],"6":[…9]}` | the legal prototype picks: the nine 6★ at both tiers, at tier 5 also the six 4★ that are not 先锋 / 特种 ("第5阶可额外从6名四星原型干员(先锋、特种职业除外)中选取"; `DIY_EXTRA_PROTOTYPES`). A prototype may fill a tier-5 and a tier-6 slot ("原型干员可于5、6阶之间重复选取") | -| `ownedPool[]` | 78 charIds | the owned 6★ a player may slot: obtainable, rarity = the requirement, and no chess names it — hidden chess included ("不可甄选加入已在名单中的固定干员"); each at most once per roster ("玩家已拥有干员不可重复选取"). The data does not exclude collab operators | +| `locked[tier][charId]` | `{"skillIndex":2,"uniEquipId":"uniequip_002_acguad","from":["chess_char_5_06_a","chess_char_5_13_a"]}` | the skill and module a prototype carries in a slot of that tier — "技能携带规则与系统补位时一致" (PRTS 卫戍协议), read as [ASSUMED] (the owner's decision of 2026-10-05) the selection of its 补位 rows at that tier (`from`): the eight 6★ elites S3 with their own module, 领主·Sharp S1, the reserves S3 without a module; 预备干员-医疗 has no tier-5 row: S3 by analogy (`from` `[]`, `DIY_PROTOTYPE_FALLBACK_SKILL`) | +| `ownedPool[]` | 72 charIds | the owned 6★ a player may slot: obtainable, rarity = the requirement, and no chess names it — hidden chess included ("不可甄选加入已在名单中的固定干员"); each at most once per roster ("玩家已拥有干员不可重复选取"). Not the collab operators (`excluded`) | +| `excluded[]` | `["char_456_ash","char_1029_yato2","char_4123_ela","char_4141_marcil","char_4182_oblvns","char_4217_makoto"]` | the 6 obtainable 6★ outside the pool whose `mainPower` / `subPower` team is a collab (`DIY_EXCLUDED_TEAMS`: rainbow, action4, mujica, sees, laios — 灰烬, 麒麟R夜刀, 艾拉, 玛露西尔, 丰川祥子, 结城理): left out of the data and the pool by the owner's decision of 2026-10-05 (copyright); the excel does not exclude them | | `operators[charId]` | `{"name":"煌","rarity":6,"profession":"WARRIOR","subProfessionId":"centurion","obtainable":true,"powers":["rhodes","elite","yan","victoria"],"bonds":["yanShip","victoriaShip"]}` | every pick (owned pool + prototypes): `powers` = the `nationId` / `groupId` / `teamId` of `mainPower` and of every `subPower` (隐藏势力); `bonds` = the core bonds whose `powerIdList` meets them — one or several — else `economy.fallbackBondId` 协防干员 ("甄选加入的干员会根据其实际阵营所属分配核心盟约,若没有可匹配的则改为分配协防干员盟约"); every prototype gets `["emptyShip"]` | +`tokens[tokenId]` — the summons of the owned picks (38 tokens of 27 operators): a tokens.json record (§14: name, text, +`placeable`, `ownerRange`, `abnormal`, `assets`) whose `variants` are keyed by the owner FORM `@` +(`owners` = those keys; shared/diy.js `diyTokenOwner`) instead of a chess id — a DIY piece is a slot, and two players may +fill one slot with different operators. A variant is the token at the owner's status for its first skill and no module, +`bySkill[i]` the token skill / count / sources under each other skill, `byModule[id]` each module's token attributes, +trait and talents (`tools/build-data.mjs buildDiyTokens`); simdata `getToken` merges them with the pick as the owner's +loadout. No id is also a tokens.json record. + Not in this data (the gameplay workstreams): a player's ownership roster, 助战 borrows (`borrowCount` 20), DIY stock, the -owned-operator training bonus (`prepareStateDict`), forms and kits of owned 6★ picks (the same `buildUnitForm` builds -them once kits exist). +owned-operator training bonus (`prepareStateDict`), potential (0 for every 自选 pick, [ASSUMED]: no account). diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 57211fcf..14df90bf 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -21,7 +21,7 @@ In scope: - Rendering with the real Spine battle chibis (PixiJS 7 + pixi-spine 4), procedural tiles, VFX, damage numbers, real BGM/SFX, emotes, broadcast ticker. - Reconnect, AI take-over of disconnected players, robust validation of every client intent. -Out of scope v1: matchmaking queue, training/tutorial, DIY (甄选) slots (the 4 DIY chess are removed from the pool), trophies/progression persistence, reporting. +Out of scope v1: matchmaking queue, training/tutorial, DIY (甄选) slots (the 4 DIY chess are removed from the pool — 0.2.0 adds their data and battle side: DATA.md §18, shared/diy.js), trophies/progression persistence, reporting. --- diff --git a/docs/SIM.md b/docs/SIM.md index 1a74353c..b1205204 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -876,6 +876,11 @@ true })`, §12): the def keeps the chess's ids, so `content/index.js kitOf` look stand-in's own, `kits/ops/standin-.js` registered under that charId (kits/README.md "Stand-in kits") — never by the chess id, which names the replaced operator's kit. A stand-in has no default skill: its kit authors each skill in `skills` (`skill` is ignored); without a kit it gets the generic kit plus `genericTalents(def)` (§7.4). +自选 pieces (DATA.md §18; a PlayerBattleInput entry of a DIY slot with `diy: { charId, skillIndex, uniEquipId }` ⇒ the def +of `getChess(slotId, { diy })`, §12) take their kit the same way, `KITS[def.charId]` only (`content/index.js isDiyDef`): an +owned 6★'s `kits/ops/op-.js` (kits/index.js `OPERATOR_KIT_FILES`, kits/README.md "How to add an operator +(自选)"), a prototype's stand-in kit, a 预备干员's generic kit; no default skill either (the pick chooses any of the +three). `KITTED_CHARS` lists who has one — shared/diy.js offers no other pick. Operator loadouts (DESIGN §16): the unit's def is `getChess(chessId, { skillIndex, moduleId })` of its PlayerBattleInput entry (no loadout fields = the default). The skill spec is `kit.skills[selectedSkillId]` when authored, else `kit.skill` only when the selected skill is the default one, else the GENERIC spec of the selected skill (its generic install is @@ -1200,7 +1205,7 @@ Unknown subprofessions fall back to the profession default (test `professions.te `['fx', kind, x, y, extra]` (`hitCap` `{ id, n }`: a leader's hit cancelled by 限伤 — the renderer draws nothing; `extra.form` = the unit's model form from then on — an enemy's `content/enemies/helpers.js setForm`, a 傀儡师's 替身 — `shared/protocol.js fxForm`), `['layer', playerId, bondId, n]` (n = the layers actually added, capped at 999), `['bounty', playerId, coins]`. -- `UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?, form?, skillIndex?, moduleId?, items?, standInFor? }` (`standInFor`: a 补位 stand-in's replaced operator charId — `spine` / `avatar` / `name` are the stand-in's, DATA.md §18; `skillIndex`: an ally's equipped skill, DESIGN §16; `form`: the unit's current model form — `content/enemies/helpers.js setForm`: 掠海漂移体 `'crawl'`, 暴鸰 `'bombed'` after its drop, 转译基底·α's forms …; a 傀儡师 fighting as its 替身 `'doll'` — so a view built mid-battle from `fieldMeta()` starts on that clip set; `items`: an ally operator's equipped item ids — a 变形同构体 wearer is a member of the bond it grants on the client too, the bond popup and the detail card's chips) +- `UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?, form?, skillIndex?, moduleId?, items?, standInFor?, diy? }` (`standInFor`: a 补位 stand-in's replaced operator charId — `spine` / `avatar` / `name` are the stand-in's, DATA.md §18; `diy`: a 自选 piece's pick `{ charId, skillIndex, uniEquipId }` — `defId` is its slot, `spine` / `avatar` / `name` the operator's; `skillIndex`: an ally's equipped skill, DESIGN §16; `form`: the unit's current model form — `content/enemies/helpers.js setForm`: 掠海漂移体 `'crawl'`, 暴鸰 `'bombed'` after its drop, 转译基底·α's forms …; a 傀儡师 fighting as its 替身 `'doll'` — so a view built mid-battle from `fieldMeta()` starts on that clip set; `items`: an ally operator's equipped item ids — a 变形同构体 wearer is a member of the bond it grants on the client too, the bond popup and the detail card's chips) (`dir` = the unit direction, allies meaningful, enemies 'RIGHT'; `facing` = its horizontal sign for sprite flipping) (`spine`/`avatar` are asset ids from data). - flags: UF bits (blocked 1, stunned 2, frozen 4, stealth 8 — 隐匿 (an enemy's only while not blocked / revealed and not within 3 s of a block's end) or an ally's 迷彩 — skill 16, shield 32, invuln 64, cold 128, sleep 256, flying 512); @@ -1239,7 +1244,10 @@ or `players`; `enemies: `timeLimit`; `content` (`'full'|'generic'|'none'`); `kits` (inject `{ baseId: kitFn }`; a stand-in's by its charId); a unit's `standIn: true` fields a NORMAL chess as its 补位 stand-in (the production path, the chess's backup skill / module; `_a` = normal, `_b` = elite), `standIn: { skillIndex?, moduleId? }` another skill / module of it (the composed record `standInRec(chessId, -sel)` replaces that chess's record for the battle; kits/README.md "How to test a stand-in kit"); `defs: { chess, enemies, tokens }` +sel)` replaces that chess's record for the battle; kits/README.md "How to test a stand-in kit"); a unit `{ diy: { slot, +charId, skillIndex?, uniEquipId? }, elite?, row, col }` fields a 自选 piece the production way (`slot` = a DIY slot's base +id or its tier 5 / 6, `elite` = its `_b` form; `getChess(slotId, { diy })`, kits/README.md "How to add an operator +(自选)"); `defs: { chess, enemies, tokens }` (extra/override records; build them with `chessRec({...})` / `enemyRec({...})`); `bonds`, `bandId`, `playerEffects`, `flags`, `sharedBoss`, `setup(battle)`, `hooks` (names to capture; `captureNoisy` to keep tick/hit/damaged/heal/spGain/ attack contexts), `autoFinish` (default: true when enemies are scheduled). @@ -1298,6 +1306,17 @@ the replaced operator's, `loadout` the backup selection (`standIn: true`); its s ("对于补位干员其技能不可更改", shared/standIn.js). A PRESET / DIY chess or a source without backups (a browser that did not fetch backups.json) gives the chess's own def. A PlayerBattleInput entry carries it as `standIn: true` (spec.js keeps nothing else); the per-battle view maps id-only lookups of that chess id to the stand-in. +自选 (DATA.md §18): `getChess(slotId, { diy })` = `getDiy(slotId, pick)` — shared/diy.js `diyRecordOf` of the DIY slot +record (`_a` normal, `_b` elite) with the pick over this source, normalised: `id` / `baseId` / `golden` / `tier` of the +slot, `bonds` the pick's derived ones, `raw.garrisonIds` `[]` (no 特质), every combat field of the operator at the slot's +status with the pick's skill and module (a prototype's locked ones), `charId` the operator's, `diyFor` the slot's base +id, `loadout` the selection with `diy` = the checked pick `{ charId, skillIndex, uniEquipId }`, `tokenOwner` = +`@`; null for an illegal pick (shared/diy.js `checkDiyPick`); cached per (id, pick). Its summons: +`getToken(id, slotId, def.loadout)` reads `rawBackups().tokens[id].variants[tokenOwner]` with `bySkill[skillIndex]` / +`byModule[uniEquipId]` merged (`getDiyToken`); `rawToken(id)` falls back to `backups.tokens`, and content/tokens.js / +`Battle.producesToken` key a 自选 owner's variant by `def.tokenOwner`. A PlayerBattleInput entry carries the pick as +`diy` (spec.js keeps `charId` / `skillIndex` / `uniEquipId` when well-formed); Battle fields a DIY slot only with a legal +pick (none ⇒ no unit), and each unit resolves its own pick — two players filling one slot id differently are exact. Assumptions taken here (documented choices): element burst numbers for necrosis/apoptosis; unspawned enemies at timeout are dropped (not leaks); geek drain is non-lethal; tactician reinforcement stats; displacement is instantaneous (the official 失衡 slide over ≈ 0.4–1.2 s is not modelled — only its distance, `push` / `pull`); pushes PRTS does not classify use the 弹道 distance column (`PUSH_TILES`); diff --git a/server/sim/content/kits/README.md b/server/sim/content/kits/README.md index 768a9f06..55d1c057 100644 --- a/server/sim/content/kits/README.md +++ b/server/sim/content/kits/README.md @@ -9,17 +9,18 @@ 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, 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 | +| `index.js` | the registry: `KIT_FILES` lists every kit file, grouped by tier, `STANDIN_KIT_FILES` the 补位 stand-in kits and `OPERATOR_KIT_FILES` the 自选 operator kits; `KITS` (base chess id → kit builder, then stand-in charId → kit builder, then operator charId → kit builder), `TIER_KITS` (one registry per tier group), `STANDIN_KITS`, `OPERATOR_KITS` and `KITTED_CHARS` (who a 自选 slot may field) are built from them | | `ops/-.js` | one kit per file, with the helpers and constants only that kit uses | | `ops/standin-.js` | one 补位 stand-in kit per file (below: "Stand-in kits") | +| `ops/op-.js` | one 自选 operator kit per file (below: "How to add an operator (自选)") | | `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). 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. +and a 自选 piece's kit is `KITS[def.charId]` and nothing else (`content/index.js kitOf`): it keeps the chess's (the DIY +slot's) ids, which name the replaced operator's kit (a slot has none). ## Naming @@ -132,6 +133,75 @@ stand-in's, `def.standInFor` the replaced operator's charId. - `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. +## How to add an operator (自选) + +The 自选 (self-select) slots — two at tier 5, two at tier 6 — field a 6★ the player owns that is not in the chess pool, +or a prototype (DATA.md §18, `shared/diy.js`). The prototypes run their stand-in kits, the 4★ 预备干员 the generic kit; +an owned 6★ can be picked only once it has a kit of its own: one file per operator, the same rules as every kit above. +Contributions are welcome — one operator per pull request is easiest to review. + +**Who.** The owned-6★ picks are `data/backups.json diy.ownedPool` (72 operators; the collab operators are not included, +the owner's decision of 2026-10-05). The ones still without a kit: + +```sh +node --input-type=module -e "import { readFileSync } from 'node:fs'; import { KITTED_CHARS } from './server/sim/content/kits/index.js'; +const b = JSON.parse(readFileSync('data/backups.json', 'utf8')); for (const id of b.diy.ownedPool) if (!KITTED_CHARS.includes(id)) console.log(id, b.units[id].name);" +``` + +**The data is already built** — `data/backups.json units[charId]` (no chess record exists for these operators): +- `forms['2/1/4/0']` = the normal form (E2 Lv1, skills at rank 4, no module), `forms['2/60/7/1']` = the tier-5 elite (E2 + Lv60, rank 7, modules at stage 1), `forms['2/60/7/3']` = the tier-6 elite (modules at stage 3). Each holds `stats`, + `rangeGrid`, `trait`, `talents` (no module), all three `skills` (blackboard `bb`, SP data, `rangeGrid`, the resolved + auto-cast `trigger`) and on the elites `modules[]` (`attr`, `traitOverride`, `talentChanges` — the official module + parts at that stage). +- `tokens[tokenId]` = its summons, per owner form (`variants['@']`, with `bySkill` / `byModule`). +- Verify the numbers against the official tables (`.cache/gamedata/excel`) and PRTS like any kit. If the data itself is + wrong or lacks something — a trigger deviation, a summon's abnormal effect — fix `tools/build-data.mjs` + (`STANDIN_TRIGGER_DEVIATIONS` is keyed by charId and applies to these units too; `TOKEN_ABNORMAL`), rebuild with + `node tools/build-data.mjs --offline` and check with a JSON compare that only the intended data changed. + +**File and registration.** +1. `ops/op-.js`, the code name being the charId without `char__` (`op-siege.js` = 推进之王, + `char_112_siege`); the Chinese name in the first line. +2. The default export has exactly one key, the charId: `export default { char_112_siege: (bb, chess, def) => Kit }`. +3. Append the file name to `OPERATOR_KIT_FILES` in `index.js`. The operator becomes a legal pick (`KITTED_CHARS`; + `shared/diy.js diyPool` / `validateDiyPicks` with `kitted`) — never register it under a chess or slot id. +4. `node --test test/content/kits_layout.test.js` checks the name, the key (an owned pick), the registry and that every + skill of every form is authored under `skills`. + +**The kit contract** (as for a stand-in): `bb` = the blackboard of the picked skill at the slot's rank; `chess` = the +composed record (`shared/diy.js diyRecordOf`): `skills` (all three at that rank), `talents` with the picked module's +changes, `trait` (the module's override), `module` (`{ id, level: 1 | 3, active }` — active on the elite only), and on an +elite with a module stage `statsBase` / `traitBase` / `talentsBase` / `modules[]` (a module talent that adds to a base +talent: read the base from `talentsBase` and the module's own part from the module's `talentChanges`, as `op-siege.js` +万兽之王); `def.tier` (5 / 6), `def.golden`, `def.charId`, `def.diyFor` (the slot). There is **no default skill**: the +pick chooses any of the three, so write every skill under `skills: { [skillId]: SkillSpec }` (`skill` is ignored); +`talents`, `trait` and `install` apply under every skill. Read every number from `chess` / `bb` — the same file serves +both tiers, both forms and both module stages. A 自选 piece has no 特质 and its bonds come from its factions: neither is +the kit's business. Summons: `battle.tokenDef(tokenId, unit)` / `battle.spawnToken(unit, …)` resolve the variant of the +pick (the hand pieces of 自选 summons come with the per-player shop). Potential is 0 [ASSUMED: no account]. + +**The fidelity rule and the checklist** above apply item by item: every skill at rank 4 and 7, every talent, every +module at stages 1 and 3 (trait override, talent changes, stats), the range while a skill runs, the auto-cast trigger +(the data's; the owner's rules of checklist item 5 — an AUTO skill acting on nobody fires at full SP), anti-air and +targetability, damage typing, summons, statuses. Mark what no source settles `[ASSUMED]` in the comment and the pull +request. + +**Tests** — `test/content/op_.test.js`, the pattern of `test/content/op_siege.test.js`: +- field the operator the production way: `makeBattle({ units: [{ diy: { slot: 5, charId: 'char_112_siege', + skillIndex: 2, uniEquipId: 'uniequip_002_siege' }, elite: true, row: 10, col: 5 }] })` — `slot` = a DIY slot id or its + tier (5 / 6 ⇒ that tier's first slot), `elite` = the `_b` form, `uniEquipId` null / omitted = no module; +- loop over every form (`[5, false]`, `[6, false]`, and both tiers' elites with no module and with each module) and every + skill; assert that the unit is yours (`u.def.charId`, `!u.kit.generic`, `u.kit.skillSource === 'skills'`), then the + checklist; read the expected numbers from `data/backups.json` forms, never from memory; +- seed everything, use synthetic `enemyRec` targets for exact numbers, end with `checkInvariants(h.b)`. + +**Golden** — the `diy` family of `tools/golden.mjs` fields every operator of `OPERATOR_KIT_FILES` in every form × module × +skill: a new kit adds its scenarios. Run `npm run golden:update`, check that only `test/golden/diy.json` changed (new +scenarios; no other family moves), and commit it with the kit (`test/golden/README.md`). + +**Art** comes with the data: `tools/fetch-assets.mjs` plans every unit of `data/backups.json` (ASSETS.md) — nothing to do. + ## Testing a kit - The harness is `test/helpers/battleHarness.js` (`makeBattle`, `runUntil`, `hooksOf`, `eventsOf`, `checkInvariants`;