feedback5: merge W5H — 自选 data (72 owned 6★ without the 6 collab operators, every DIY form, summons, prototypes' locked skills), shared/diy.js, the sim fields DIY picks, OPERATOR_KIT_FILES with 推进之王 as the first operator kit, golden family diy, the art plan (--add-only)
Conflicts: tools/golden.mjs / test/golden/README.md take both new families (standins from W5G, diy from W5H); DATA.md / SIM.md keep both texts. data/ equals an offline build of the merged tools/build-data.mjs. Golden regenerated on the merged code: against feedback5 nothing moved, diy.json is new; against fb5-diy the W5G / W5J / W5L scenarios moved, and diy-004 / diy-006 (the prototype battles with Touch, changed by W5L) — diy-001/002/003/005 identical.
This commit is contained in:
commit
1006254beb
32 files changed
+2229
-174
No files matched your search
+33
-14
@@ -11,6 +11,9 @@
|
||||
// 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).
|
||||
// 自选 pieces (isDiyDef: the def of simdata getDiy — a DIY slot's ids, the picked operator's body) take their kit the
|
||||
// same way: kits[def.charId] (an owned 6★'s kits/ops/op-<codename>.js, a prototype's standin-<codename>.js), else the
|
||||
// generic kit plus genericTalents; no default skill either (the pick chooses any of the three).
|
||||
// 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,
|
||||
@@ -60,21 +63,33 @@ export function isStandInDef(def) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Whether `def` is a 自选 piece: a DIY slot fielded with a picked operator's body (simdata getDiy — `diyFor` = the slot's
|
||||
* base id, `charId` = the operator's).
|
||||
*/
|
||||
export function isDiyDef(def) {
|
||||
return !!(def && def.diyFor && def.charId);
|
||||
}
|
||||
|
||||
/** A def whose body is another character than its chess id names (补位 stand-in, 自选 piece): its kit is keyed by charId. */
|
||||
const isOtherBody = (def) => isStandInDef(def) || isDiyDef(def);
|
||||
|
||||
/**
|
||||
* The kit builder of an operator def in `reg` ({ [key]: (bb, chess, def) => Kit }), or undefined. A stand-in's and a
|
||||
* 自选 piece's kit is keyed by its charId (`char_609_acguad`, `char_112_siege`) and by nothing else: the chess id it keeps
|
||||
* names the replaced operator's kit (a DIY slot has none). 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;
|
||||
if (isOtherBody(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. */
|
||||
/** The generic kit of an operator def; a stand-in's / 自选 piece'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;
|
||||
if (!isOtherBody(def)) return k;
|
||||
const talents = genericTalents(def);
|
||||
return talents.length ? { ...k, talents: [...(k.talents ?? []), ...talents] } : k;
|
||||
}
|
||||
@@ -102,7 +117,7 @@ export function setupUnitKit(battle, unit, mode = 'full') {
|
||||
const k = f(bb, raw, def);
|
||||
if (k) return selectSkillSpec(k, bb, raw, def);
|
||||
} catch (e) {
|
||||
battle._handlerError(`kit:${isStandInDef(def) ? def.charId : def.baseId}`, unit, e);
|
||||
battle._handlerError(`kit:${isOtherBody(def) ? def.charId : def.baseId}`, unit, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,15 +135,18 @@ 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). `standIn: true` rides along (补位: the stand-in def whatever the skill / module fields say).
|
||||
* 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), as
|
||||
* does `diy` (自选: the pick of a DIY slot's piece). Null when the unit has no input entry.
|
||||
*/
|
||||
function inputLoadout(unit) {
|
||||
const x = inputEntry(unit);
|
||||
if (!x) return 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 };
|
||||
const extra = {
|
||||
...(x.standIn === true ? { standIn: true } : null),
|
||||
...(x.diy && typeof x.diy === 'object' ? { diy: x.diy } : null),
|
||||
};
|
||||
if (x.skillIndex == null && x.moduleId == null) return extra;
|
||||
return { skillIndex: x.skillIndex ?? null, moduleId: x.moduleId ?? null, ...extra };
|
||||
}
|
||||
|
||||
/** Put a def on a not-yet-deployed ally (the fields Battle._makeAlly takes from the def). */
|
||||
@@ -179,9 +197,10 @@ const selectedSkillId = (def) => def?.skill?.id ?? def?.raw?.skill?.skillId ?? n
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
* the skill it fields is the one its chess names (S2 on one chess, S3 on another), so its kit authors each in `skills`;
|
||||
* nor for a 自选 piece (the pick chooses any of its skills).
|
||||
*/
|
||||
const skillIsDefault = (def) => !isStandInDef(def) && (!def?.loadout || def.loadout.skillIsDefault !== false);
|
||||
const skillIsDefault = (def) => !isOtherBody(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;
|
||||
|
||||
@@ -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/<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") |
|
||||
| `ops/op-<codename>.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
|
||||
|
||||
@@ -135,6 +136,75 @@ every active backup skill is cast).
|
||||
- `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['<charId>@<statusKey>']`, 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-<codename>.js`, the code name being the charId without `char_<n>_` (`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_<codename>.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`;
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
// 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.
|
||||
//
|
||||
// 自选 operator kits (DATA.md §18, README.md "How to add an operator (自选)"): OPERATOR_KIT_FILES lists
|
||||
// `ops/op-<codename>.js`, one per owned-6★ pick of data/backups.json `diy.ownedPool` (codename = its charId without
|
||||
// `char_<n>_`: `op-siege.js` = 推进之王, char_112_siege), each registering exactly that charId and writing every skill under
|
||||
// `skills`. A 自选 piece keeps its DIY slot's ids, so kitOf finds its kit by `def.charId` — a prototype pick runs its
|
||||
// stand-in kit, a 4★ reserve the generic kit (GENERIC_KIT_CHARS). OPERATOR_KITS is merged after the stand-ins.
|
||||
// KITTED_CHARS = every character a 自选 pick may field with a faithful kit; an operator outside it is not offered
|
||||
// (shared/diy.js diyPool / validateDiyPicks `kitted`).
|
||||
|
||||
export const KIT_FILES = Object.freeze([
|
||||
// tier 1
|
||||
@@ -66,6 +74,19 @@ export const KIT_FILES = Object.freeze([
|
||||
*/
|
||||
export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sharp2.js', 'standin-acspec.js', 'standin-acmedc.js', 'standin-acfend.js', 'standin-acpion.js', 'standin-acnipe.js', 'standin-accast.js', 'standin-acsupo.js']);
|
||||
|
||||
/**
|
||||
* The 自选 operator kit files (`ops/op-<codename>.js`, registry key = the operator's charId, an owned-6★ pick of
|
||||
* data/backups.json; a new file is appended). An operator without a file is not offered as a 自选 pick (KITTED_CHARS).
|
||||
*/
|
||||
export const OPERATOR_KIT_FILES = Object.freeze(['op-siege.js']);
|
||||
|
||||
/**
|
||||
* The 预备干员 whose every skill the generic kit covers exactly (冲锋号令, 攻击力 / 防御力 / 治疗强化, 战术咏唱, 一击即退 —
|
||||
* test/content/standin.test.js): fielded with no kit file, as 补位 stand-ins and as tier-5 自选 picks.
|
||||
*/
|
||||
export const GENERIC_KIT_CHARS = Object.freeze(['char_600_cpione', 'char_601_cguard', 'char_602_cdfend', 'char_603_csnipe',
|
||||
'char_604_ccast', 'char_605_cmedic', 'char_606_csuppo', 'char_607_cspec']);
|
||||
|
||||
async function loadKitFile(file) {
|
||||
try {
|
||||
return await import(`./ops/${file}`);
|
||||
@@ -77,6 +98,7 @@ 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 OPERATOR_MODULES = await Promise.all(OPERATOR_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. */
|
||||
@@ -85,5 +107,18 @@ export const TIER_KITS = Object.freeze(MODULES.map((group) => Object.assign({},
|
||||
/** 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));
|
||||
/** The 自选 operator kits: owned-6★ charId → kit builder, in OPERATOR_KIT_FILES order. */
|
||||
export const OPERATOR_KITS = Object.freeze(Object.assign({}, ...OPERATOR_MODULES.map(registryOf)));
|
||||
|
||||
/**
|
||||
* The merged kit registry: baseChessId → (bb, chess, def) => Kit, tier 1 … tier 6, then the stand-ins' charIds, then
|
||||
* the 自选 operators' charIds.
|
||||
*/
|
||||
export const KITS = Object.freeze(Object.assign({}, ...TIER_KITS, STANDIN_KITS, OPERATOR_KITS));
|
||||
|
||||
/**
|
||||
* Every character a 自选 pick may field with a faithful kit: the 预备干员 (GENERIC_KIT_CHARS), the stand-ins with a kit
|
||||
* file and the operators of OPERATOR_KIT_FILES whose file loaded. The `kitted` option of shared/diy.js diyPool /
|
||||
* validateDiyPicks.
|
||||
*/
|
||||
export const KITTED_CHARS = Object.freeze([...new Set([...GENERIC_KIT_CHARS, ...Object.keys(STANDIN_KITS), ...Object.keys(OPERATOR_KITS)])]);
|
||||
@@ -0,0 +1,127 @@
|
||||
// server/sim/content/kits/ops/op-siege.js — 推进之王 (char_112_siege) 自选 operator kit: 6★ 尖兵 (先锋), an owned-6★ pick
|
||||
// of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules at every form.
|
||||
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_112_siege, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
|
||||
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
|
||||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json) and PRTS 推进之王
|
||||
// (talent 修正 "在场时"; 粉碎 备注 "本天赋无视自身的阻回,可响应飞行单位的死亡"; S2 备注 "技能固定动画不受攻击速度影响";
|
||||
// S3 "攻击间隔增大(+1.0)").
|
||||
// - Trait (尖兵) "能够阻挡两个敌人": the profession default (block 2). Module SOL-X “糖果盒” overrides it with
|
||||
// "阻挡敌人时攻击力和防御力各+8%" (trait bb atk / def): +atk / +def while she blocks at least one enemy.
|
||||
// - T1 万兽之王 "在场时,所有【先锋】职业干员的攻击力和防御力各+8%": an aura on every 【先锋】 operator of the field (her
|
||||
// included) while she is deployed (installAura: the kits' convention for field-wide talents — teammates' pioneers in a
|
||||
// shared field too; several sources keep the strongest). SOL-X stage 2+ adds "自身攻击力和防御力额外+N%" (the module
|
||||
// talent change's bb, +8 % at stage 3): a self buff on top; the aura value stays the base talent's (talentsBase).
|
||||
// - T2 粉碎 "周围四格内有敌人倒下时获得1点技力" (talent range x-5: her tile and the four next to it): +sp SP when an enemy
|
||||
// dies there — flying enemies too, and under her own 阻回 (PRTS 备注) — never while a timed skill runs (AK: no SP during
|
||||
// a skill). SOL-Y stage 2+ raises it to sp (3 at stage 3) "场上随机另一名【先锋】职业干员获得1点技力": the hidden module
|
||||
// talent's sp to one other 【先锋】 operator of the field, drawn uniformly [ASSUMED: any deployed other pioneer, a draw that
|
||||
// cannot take SP wastes it].
|
||||
// - Module SOL-Y “初诺” "首次部署时部署费用-4" (hidden runtime_cost −4): the first deployment of a battle costs nothing in
|
||||
// this mode ⇒ no effect (as 德克萨斯's 战术快递 elite note); its HP / ATK are in the stats.
|
||||
// - S1 冲锋号令·γ型 (AUTO, no target): +cost DP at once; an AUTO skill acting on nobody fires as soon as its SP is full
|
||||
// (`trigger: 'SP_FULL'`, as 德克萨斯's kit casts the same skill — the owner's AUTO rule, kits/README.md checklist 5).
|
||||
// - S2 跃空锤 (AUTO, 可充能 2 / 3 次): the next attack hits every enemy around her — the skill range x-5 — for atk_scale × ATK
|
||||
// physical and gives cost DP; the data's DEFAULT trigger (an AUTO "next attack" waits for her attack). The AoE is the
|
||||
// attack itself, so it reaches the ground enemies a melee attack can [ASSUMED: no source says it reaches air]. The fixed
|
||||
// skill animation (PRTS 备注) does not change the sim's attack timing.
|
||||
// - S3 碎颅击 (MANUAL, 18 / 21 s): attack interval +base_attack_time s (a flat +1.0 on her 1.05 s), each attack at
|
||||
// attack@atk_scale × ATK with attack@buff_prob to stun the target attack@stun s.
|
||||
|
||||
import { num, talentBb, talentGrid, moduleBb, traitBb, moduleOn, skillRec, statBuff, toggleBuff, installAura, batMod,
|
||||
up, skillBusy, giveSp } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
|
||||
const S1 = 'skcom_charge_cost[3]';
|
||||
const S2 = 'skchr_siege_2';
|
||||
const S3 = 'skchr_siege_3';
|
||||
/** The four tiles around her and her own (talent / skill range x-5) when the data carries no grid. */
|
||||
const X5 = Object.freeze([[1, 0], [0, -1], [0, 0], [0, 1], [-1, 0]]);
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** A 【先锋】 operator (summons are never 【先锋】 operators). */
|
||||
const isPioneer = (a) => !!a && a.kind === 'op' && a.def?.profession === 'PIONEER';
|
||||
|
||||
/** The module the record fights with (its active one), or null. */
|
||||
function activeModule(chess) {
|
||||
if (!moduleOn(chess)) return null;
|
||||
return (chess.modules ?? []).find((m) => m && m.uniEquipId === chess.module.id) ?? null;
|
||||
}
|
||||
|
||||
export default {
|
||||
char_112_siege: (bb, chess) => {
|
||||
const base0 = talentBb({ talents: chess?.talentsBase ?? chess?.talents }, 0); // 万兽之王 without the module change
|
||||
const mod = activeModule(chess);
|
||||
const self0 = mod?.talentChanges?.find((t) => t && t.talentIndex === 0)?.bb ?? {}; // SOL-X stage 2+: 自身额外
|
||||
const t1 = talentBb(chess, 1);
|
||||
const grid1 = talentGrid(chess, 1) ?? X5;
|
||||
const hidden = moduleBb(chess); // SOL-Y: runtime_cost (no effect), sp to another pioneer (stage 2+)
|
||||
const tb = traitBb(chess);
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s2 = skillRec(chess, S2);
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'instant', trigger: 'SP_FULL',
|
||||
onStart({ battle, unit }) {
|
||||
const n = num(b1.cost);
|
||||
battle.addDp(unit.ownerId, n);
|
||||
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
|
||||
},
|
||||
},
|
||||
[S2]: {
|
||||
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||||
targeting: { rangeGrid: s2?.rangeGrid ?? X5, allInRange: true },
|
||||
attack: { atkScale: num(b2.atk_scale, 1) },
|
||||
onStart({ battle, unit }) {
|
||||
const n = num(b2.cost);
|
||||
battle.addDp(unit.ownerId, n);
|
||||
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
|
||||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: 'siege:hammer' });
|
||||
},
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { batPct: batMod(b3.base_attack_time, chess) },
|
||||
attack: {
|
||||
atkScale: num(b3['attack@atk_scale'], 1),
|
||||
onHit({ battle, unit, target }) {
|
||||
if (target && target.alive && target.side === 'enemy' && battle.rng.chance(num(b3['attack@buff_prob']))) {
|
||||
battle.applyStatus(target, 'stun', { duration: num(b3['attack@stun']), source: unit });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 万兽之王: the field's 【先锋】 ATK / DEF +atk / +def; SOL-X stage 2+: her own extra
|
||||
const a = num(base0.atk), d = num(base0.def);
|
||||
if (a || d) installAura(battle, unit, { key: 'talent:siege:pioneers', value: a, select: isPioneer, mods: { atkPct: a, defPct: d } });
|
||||
statBuff(battle, unit, 'talent:siege:self', { atkPct: num(self0.atk), defPct: num(self0.def) });
|
||||
} },
|
||||
{ install(battle, unit) { // 粉碎: an enemy falls on her x-5 ⇒ +sp SP (阻回 ignored); SOL-Y: +1 SP to another pioneer
|
||||
const sp = num(t1.sp), other = num(hidden.sp);
|
||||
if (!(sp > 0)) return;
|
||||
battle.on('death', (ctx) => {
|
||||
const e = ctx.unit;
|
||||
if (e.side !== 'enemy' || ctx.reason !== 'killed' || !up(unit) || !unit.skill) return;
|
||||
if (!bodyInKeys(e, new Set(absoluteRangeKeys(grid1, unit.tileR, unit.tileC, unit.dir, 0)))) return;
|
||||
// "无视自身的阻回": the 'init' reason passes 阻回; a running timed skill still takes no SP
|
||||
if (!skillBusy(unit)) unit.skill.gainSp(sp, 'init');
|
||||
if (other > 0) {
|
||||
const mates = battle.alliesFor(unit).filter((a) => a !== unit && isPioneer(a));
|
||||
if (mates.length) giveSp(battle.rng.pick(mates), other);
|
||||
}
|
||||
}, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// SOL-X “糖果盒”: 阻挡敌人时攻击力和防御力各+8%
|
||||
const ma = num(tb.atk), md = num(tb.def);
|
||||
if (ma || md) toggleBuff(battle, unit, 'trait:siege:block', () => unit.blocking.length > 0, { atkPct: ma, defPct: md });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -144,10 +144,13 @@ function withLoadout(v, lo) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The owner's own variant of a token record (chess id, else its normal `_a` sibling) with its loadout; null if none. */
|
||||
/**
|
||||
* The owner's own variant of a token record (chess id, else its normal `_a` sibling; a 自选 piece's: its owner form,
|
||||
* `def.tokenOwner`) with its loadout; null if none.
|
||||
*/
|
||||
function ownVariant(raw, owner) {
|
||||
const vs = raw?.variants;
|
||||
const oid = owner?.defId;
|
||||
const oid = owner?.def?.tokenOwner ?? owner?.defId;
|
||||
if (!vs || typeof vs !== 'object' || !oid) return null;
|
||||
const v = vs[oid] ?? vs[String(oid).replace(/_b$/, '_a')] ?? null;
|
||||
return v ? withLoadout(v, owner.def?.loadout) : null;
|
||||
|
||||
Reference in new issue
Block a user