feedback5 R4: paths after the kit split — DESIGN §7 and SIM.md (layout, §7.2 kit contract, kit citations), research 09, code comments (content/generic, tokens, items/battle, shared/loadoutRecord, render fx / demo), test comments, and the kits' own comments that named tierN.js; DESIGN §17–§23 history citations are left as they are

This commit is contained in:
sganggs committed 2026-10-05 16:18:58 +08:00
1 parent 7adf46b033
commit 37b1c0a644
34 files changed
+54 -51

No files matched your search

+4 -2
View File
@@ -75,7 +75,9 @@ server/
snapshot.js compact serialization for clients
content/
index.js installs all content into a Battle (kits, bonds, garrisons, items, bands, enemies, devices)
kits/tier1.js … tier6.js operator kits (skill + talents) keyed by base chessId
kits/index.js operator kit registry keyed by base chessId (guide: kits/README.md)
kits/ops/*.js one operator kit (skills + talents) per file, <chessId>-<codename>.js
kits/shared/*.js helpers the kits share (shared/tier1.js: the general ones)
tokens.js summon/token definitions
bonds.js battle side of the 23 bonds
garrisons.js battle side (IN_BATTLE) garrison effect keys
@@ -392,7 +394,7 @@ Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for
## 7. Content modules (server/sim/content + match/effectsMeta handlers)
Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js` exports `install(battle)` (battle side; called once per Battle by `content/index.js → installContent`) and `registerMeta(registry)` (prep side; called once at server boot by `match/effectsMeta.js`, API documented in `docs/META.md`). Kits live in `content/kits/tier1..6.js` (`export default { [baseChessId]: (bb, chess) => Kit }`). Content must be **data-driven from blackboards** (numbers never hard-coded when a blackboard key exists). Every effect gets at least one unit test in `test/content/*.test.js` using the `test/helpers/battleHarness.js` harness (provided by sim-core).
Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js` exports `install(battle)` (battle side; called once per Battle by `content/index.js → installContent`) and `registerMeta(registry)` (prep side; called once at server boot by `match/effectsMeta.js`, API documented in `docs/META.md`). Kits live one per file in `content/kits/ops/` (`export default { [baseChessId]: (bb, chess, def) => Kit }`), listed in `content/kits/index.js` (guide: `content/kits/README.md`). Content must be **data-driven from blackboards** (numbers never hard-coded when a blackboard key exists). Every effect gets at least one unit test in `test/content/*.test.js` using the `test/helpers/battleHarness.js` harness (provided by sim-core).
- kits: all 112 visible chess + hidden ones used by effects (盟约·辅助干员), default skill + talents + trait specifics; tokens.
- bonds: 23 (battle + meta), exact per-layer formulas from research 02.
+8 -7
View File
@@ -26,7 +26,8 @@ server/sim/
simdata.js data access + normalisation (data/*.json, research fallback)
content/index.js installContent / setupUnitKit / registerAllMeta
content/generic.js generic kit from skill blackboards
content/kits/tier1..6.js, content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js (content phase)
content/kits/index.js + kits/ops/*.js (one operator kit per file) + kits/shared/*.js,
content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js (content phase)
```
---
@@ -123,7 +124,7 @@ device or 海嗣 that was not placed never appears (the hidden 待部署区 depl
流形 still come as their 援军 on a tactical point without a piece (content/tokens.js `tacticalPoint`). Their piece stands
inside the tactician's attack range — the prep enforces "只能部署在召唤者攻击范围内" (tokens.json `ownerRange`, player
report #9 after 0.1.0) — and the kits re-use the piece's tile for a re-summoned pack only while it is a free, standable
tile of her initial range (tier3 `tacticalPoint`, tokens.js `ensureReinforcement`). The start deploy
tile of her initial range (kits/shared/tier3.js `tacticalPoint`, tokens.js `ensureReinforcement`). The start deploy
is the user's call after playtest #6 (DESIGN §20); `shared/constants.js SKILL_SUMMON_START_DEPLOY = false` would bring
back the playtest #4 reading (only with the skill) in the sim and the summon card's hint (docs/PLAYING.md §4 and this
passage must follow; test/ui/playtest6_summons.test.js checks).
@@ -768,7 +769,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
| `getPlayer(playerId)` | `{ playerId, seat, side, colOffset, mirror, dir (default unit direction: RIGHT, mirrored side LEFT), facing (its sign), bonds (live copy, layers updated by addLayers), bandId, playerEffects, lpForBoss, dp, units }` |
| `mapTile(ps, row, col, abs?)` / `mapDir(ps, dir, abs?)` | board → field tile / direction of a player (the FA right-side mirror) |
| `addDp(playerId, n)`, `retreat(unit, {reason, permanent})`, `relocate(unit, r, c)` | `relocate` only changes the tile (state kept, no event) |
| `moveRedeploy(unit, r, c, { clearSp })` | a 【移动】 (PRTS 术语释义: "不退场,以当前血量在目标位置部署", a special retreat + redeploy): `relocate`'s checks (false = refused, nothing changed), then a new deployment (`deploySeq` / `aggroSeq` / `deployedAt`) and `deploy {initial:false, move:true}` (deploy effects fire again); no `die` / `death`, timer or cost (不屈 / 阿戈尔's revive never see it); HP, buffs and a running skill are kept (owner's deviation, DESIGN §22.3); `clearSp` empties the SP before the deploy handlers run — 乌尔比安 S3's move (kits/tier5.js) and 【返回】 (`clearSp`; also the 从不混淆的方向 fallback, content/tokens.js) |
| `moveRedeploy(unit, r, c, { clearSp })` | a 【移动】 (PRTS 术语释义: "不退场,以当前血量在目标位置部署", a special retreat + redeploy): `relocate`'s checks (false = refused, nothing changed), then a new deployment (`deploySeq` / `aggroSeq` / `deployedAt`) and `deploy {initial:false, move:true}` (deploy effects fire again); no `die` / `death`, timer or cost (不屈 / 阿戈尔's revive never see it); HP, buffs and a running skill are kept (owner's deviation, DESIGN §22.3); `clearSp` empties the SP before the deploy handlers run — 乌尔比安 S3's move (kits/ops/chess_char_5_05-ulpia.js) and 【返回】 (`clearSp`; also the 从不混淆的方向 fallback, content/tokens.js) |
| `redeploy(unit, { free=true, tile, keepSp })` | immediate (re)deployment of a dead/retreated ally (full HP, `deploy {initial:false}`); `free: false` pays `base.cost` DP (refused without it); without `tile` it lands on the unit's rest tile (`restTile`: where a knocked-out operator lies, else home); `tile: [r, c]` lands on that tile once (home unchanged; refused when off-rect, occupied or a knocked-out operator's tile, no fallback); `keepSp` keeps SP/charges (保留技力), restored before `deploy` fires — 突袭 raids, 阿戈尔 / 不屈 revives where the unit lies |
| `refreshRange(unit)`, `setExtraRange(unit, keys)`, `rangeChanged(unit)` | rebuild the ranges after changing `unit.rangeGrid` (流形 copies); extra targetable tiles (absolute keys; merged into every later rebuild until set again; `null` clears; never in `baseRangeKeys`): 蕾缪安 wanted, 维娜 S3 |
| `push(enemy, force, {from, dir, fixed, fixedAngle, inward, effect})`, `pull(enemy, force, {to, center, stop})`, `pullToFront(enemy, unit, force)`, `forceLevel(enemy, force)`, `pushDistance(enemy, force, {effect})` | the official 位移 (PRTS 游戏数据基础 §重量公式 / 推与拉; user playtest #6 item 14): 受力等级 = 力度 (微小力 −1, 小力 0, 中力 1, 较大力 2, 大力 3 …) − current 重量等级 (massLevel, 失重 counts). Push distance per level (`constants.js PUSH_TILES`, PRTS 推与拉's 弹道 column): ≤ −3 → 0, −2 → 0.12, −1 → 0.44, 0 → 1.7, 1 → 2.14, 2 → 2.96, ≥ 3 → 3.53 tiles; `effect` = a 特效 push (`PUSH_TILES_EFFECT`: −2 → 0.085, −1 → 0.374, 0 → 1.562, 1 → 1.987, 2 → 2.773, ≥ 3 → 3.331 — 见行者 S1 / S2, `PUSH_EFFECT_SKILLS`; every other pusher uses the 弹道 column [ASSUMED]); radial (away from `from`; the client's buff template `knockback[relative]`: 莫斯提马 S3, 山 S3, 琳琅诗怀雅 S3 — also PRTS 备注 "推开效果为径向推动") unless `dir` (directional; template `knockback[dir]` = Knockback {`_useSourceDirection` true, `_decreaseForceLevelWhenNotInDirection` 2}: 推击手, 野鬃 S2 "往攻击方向" (charpack char_496_wildmn: buff `wildmn_s_2[force]`) — > 45° off or < 0.25 tile ⇒ radial and level −2; `fixed` waives both (圣聆初雪 S1 朝部署方向, `KnockBackWithCharacterDirection`), `fixedAngle` only the angle (见行者 S2)); `inward` = a push towards `from` (薄绿 S2), stopping at the 急停 radius. Pull: level ≥ 0 → to `to` / the 急停 radius around `center`, −1 → 35 % of the way, −2 → 0.03, ≤ −3 → 0; `pullToFront` aims at the 拉力起点 0.5 tile ahead of the unit with the 急停 radius 0.6708 around it (never moves an enemy the unit itself blocks — nor does a pull towards an ally's centre, e.g. the 流形 S3 pulse) A 静态刚体 (data `staticBody`: every air unit of the mode except “炎佑”, plus 昆图斯 — PRTS 特殊机制 "可以进入失衡状态…但物理层面上无法产生任何速度或移动", "与单位的行动方式无关") moves 0 from every source while the skills that reach it still hit it (薄绿 S2, 锏 S3, the 钩索师 …; player report after 0.1.0) |
@@ -842,7 +843,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
### 7.2 Kits
```js
// server/sim/content/kits/tierN.js
// server/sim/content/kits/ops/<chessId>-<codename>.js — one kit per file, listed in kits/index.js (kits/README.md)
export default {
[baseChessId]: (bb, chess, def) => Kit, // bb = flattened blackboard of the SELECTED skill (normal Lv4 / elite Lv7)
}; // chess = the record as the unit's loadout makes it (simdata loadoutRecord),
@@ -884,11 +885,11 @@ Element conventions of the kits (user playtest #5 #3; official term dictionary:
constraint (priority 0 — enemies carry no 损伤屏障).
- **Element attached to a damage** (a `damaged` hook: 迭代元素, 灼燃维式重锤, 炎佑, 妮芙 S2, 余 S3 火墙): the hook runs before
`battle.kill`, so a killing blow still sees the target `alive` at 0 HP — these riders check HP left (damage.js `hasHp`,
boss: pool HP; tier6 `elementDmg`) and applyElement itself refuses such a target, so nothing bursts on the corpse.
boss: pool HP; kits/shared/tier6.js `elementDmg`) and applyElement itself refuses such a target, so nothing bursts on the corpse.
塑心 S2 (安魂的弥撒) is the exception: PRTS "…凋亡损伤生效于当次触发的伤害之前,该造成的凋亡损伤的来源始终为塑心" — a late
`hit` handler (priority −1000, after any cancel), so its 凋亡 lands (and may burst) before the damage; a hit dodged or
absorbed after that still carried it [ASSUMED].
- **盟约·辅助干员 迭代元素** (tier6.js `pithst`): every damage she deals (HP damage > 0; "造成伤害时") attaches `ep_damage_ratio
- **盟约·辅助干员 迭代元素** (kits/ops/chess_char_1_15-pithst.js `pithst`): every damage she deals (HP damage > 0; "造成伤害时") attaches `ep_damage_ratio
× ATK` of 神经, then 灼燃, then 凋亡 (`PITHST_ELEMENTS`); the element applied first bursts, so alone she bursts 神经.
### 7.3 SkillSpec schema
@@ -1082,7 +1083,7 @@ const bombard = (battle, unit, locks) => {
};
```
(`blast` emits fx `'bombard'` at (x, y) and deals `proj_atk_scale_1` / `_2` × the cached ATK to every enemy within
`dist_2`, once each; the full kit is `content/kits/tier6.js lemuen`.)
`dist_2`, once each; the full kit is `content/kits/ops/chess_char_6_01-lemuen.js`.)
---
+1 -1
View File
@@ -332,7 +332,7 @@ The public mirror (ArknightsAssets2 `cn`, `ui/emoticon/theme/[uc]<themeId>/icon/
- `professions.js` front checks (the `(x − unit.x)·facing` tests)
- `skills.js` trigger grids
- `tokens.js` placement grids and Mech/device helpers
- kits tier3 (`displace` direction, 薄绿 pull tile) and tier4 device placement
- the tier-3 kits (`displace` direction, 薄绿 pull tile) and tier-4 device placement (server/sim/content/kits/ops/)
- `content/devices.js` blower relation: `equal` if the operator dir == blower dir, `opposite` if reversed, otherwise none. Today only fx is compared, so the DOWN blowers never buff anyone.
- Items 叙拉古正装 (perpendicular pair), 歌利亚头盔 (front tile), 画卷 `1-1`.
4. **`server/match/finalAssault.js`.** The right-side mirror becomes col `c→20−c` with dir RIGHT↔LEFT; UP and DOWN are kept (not a flat `facing=−1`). FA prep on the right half shows the mirrored layout, and moves there go through the wheel in mirrored coordinates.
+1 -1
View File
@@ -463,7 +463,7 @@ function fxScene(view, stageId) {
};
// `pt` kinds name the shooter in `id` and happen at an enemy's spot; a 'flame' happens at its target
const posFor = (k, ex) => unitAt(FX_KINDS[k].pt ? [21, 23, 20][i % 3] : FX_KINDS[k].a === 'flame' ? ex.target : ex.id);
// 蕾缪安 S3 as the sim plays it (sim/content/kits/tier6.js): a lock every 0.5 s, then after the skill one shell every
// 蕾缪安 S3 as the sim plays it (sim/content/kits/ops/chess_char_6_01-lemuen.js): a lock every 0.5 s, then after the skill one shell every
// 0.3 s on the locks in order, each landing 0.3 s later with its bombard — queued [game time, event]
const queue = [];
const lemuenS3 = () => {
+1 -1
View File
@@ -292,7 +292,7 @@ export function tilesAround(x, y, r, tiles) {
/**
* Tiles of a straight wall through tile (round(y), round(x)): `axis` 'col' = that column, 'row' = that row (余 S3
* fire wall: perpendicular to his facing, sim/content/kits/tier6.js). Clipped to the field `rect` (inclusive
* fire wall: perpendicular to his facing, sim/content/kits/ops/chess_char_6_03-yu.js). Clipped to the field `rect` (inclusive
* { r0, r1, c0, c1 }); without one ±4 tiles.
*/
export function wallTiles(x, y, axis, rect) {
+1 -1
View File
@@ -35,7 +35,7 @@
// 推与拉 方向力) / 朝部署方向 / 向前 / 身前方向 or for 推击手 (directional), otherwise away from the unit (radial); a skill of
// constants.js PUSH_EFFECT_SKILLS (见行者 S1) pushes by PRTS 推与拉's 特效 column. These keywords are only a fallback:
// the hand-written kits follow the client's buff templates (knockback[dir] = directional, knockback[relative] = radial),
// and the text can mislead — 琳琅诗怀雅 S3's "向前推开" is knockback[relative] (kits/tier3.js). No pool skill pushes
// and the text can mislead — 琳琅诗怀雅 S3's "向前推开" is knockback[relative] (kits/ops/chess_char_3_04-swire2.js). No pool skill pushes
// through this path with 向前 / 往攻击方向; the only generic push is 见行者 S1, a 推击手.
import { normalizeSkill } from '../simdata.js';
+1 -1
View File
@@ -253,7 +253,7 @@ export function revivedInPlace(u) { if (u && u.mem) u.mem.revives = (u.mem.reviv
* Does `u` hold 坚固维式重锤's 不死 right now — a window started in this deployment that has not run out? The window lives
* on the unit (`mem.undyingUntil`, `mem.undyingAt`) and a battle-level hook holds it (hammerAcquire), so it outlives the
* grant that started it — a lend running out mid-window leaves the 不死 for its 8 s [ASSUMED: the 异常效果 outlasts its
* source] — and ends with the deployment. 信仰搅拌机 S2 steps aside while it holds (kits/tier4.js).
* source] — and ends with the deployment. 信仰搅拌机 S2 steps aside while it holds (kits/ops/chess_char_4_01-rmixer.js).
*/
export function holdsUndying(battle, u) {
return !!u && u.mem.undyingAt != null && battle.time < u.mem.undyingUntil && u.mem.undyingAt === deploymentOf(u);
@@ -59,8 +59,6 @@ export function tinmanKit(bb, chess, def) {
}
export default {
// 1_15 盟约·辅助干员 (hidden, band 优等生): its kit is tier6.js `pithst` (迭代元素 — one implementation only).
// ---------------------------------------------------------------------------------------------------------------
// 1_16 锡人 (hidden tier-1 entry of chess_char_2_19): see tinmanKit.
chess_char_1_16_a: tinmanKit,
@@ -6,7 +6,7 @@ import { tinmanKit } from './chess_char_1_16-tinman.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_19 锡人 “大拉里”: see tinmanKit (tier1.js).
// 2_19 锡人 “大拉里”: see tinmanKit (chess_char_1_16-tinman.js).
// S1 “老科利” (alt, attack SP, needs a target): throws an alchemy unit at the target (ground first); for
// projectile_delay_time s ground enemies within projectile_range tiles are 虚弱 (weaken −atk) and take atk_scale ×
// ATK arts per second (ATK cached at the cast; damage over time, so the elite 凋敝魂灵 ×skill@damage_scale applies;
@@ -12,7 +12,7 @@ import {
/**
* 鼓舞 (ba.inspire "获得额外附加的基础属性加成(同类属性取最高)"): +`val` ATK (or max HP) on top of the target's own
* percentage multipliers (the flat mod is compensated, so an ATK+% skill does not scale it), the strongest source wins
* (the ATK key `inspire` is shared with tier6's 鼓舞); units flagged `mem.noInspire` (bards: 自身不受鼓舞影响) never
* (the ATK key `inspire` is shared with 浊心斯卡蒂's 鼓舞); units flagged `mem.noInspire` (bards: 自身不受鼓舞影响) never
* receive it. Refreshed by an aura: lapses within AURA_DUR once the source stops.
*/
function inspire(battle, target, val, src, stat = 'atk') {
@@ -22,7 +22,7 @@ export default {
// off, all allies regenerate 20 HP/s. Module (elite): longer sluggish (trait bb, profession tunables).
// S1 秘杖·速充模式 (duration, attack SP): ATK +; she attacks normally with S1 ("技能未开启时无法普通攻击" is only in the
// S2/S3 texts). S2 秘杖·微粒模式 (duration, SEARCH): attack interval ×base_attack_time ("极大幅度缩短": a positive value
// described as a shortening is the new interval ratio, tier1 batMod convention), each attack damage_scale × ATK arts.
// described as a shortening is the new interval ratio, the shared/tier1.js batMod convention), each attack damage_scale × ATK arts.
// Module DEC-X (elite): SP +0.2/s with an enemy in range.
chess_char_5_20_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tm = talent(chess, -1);
@@ -50,7 +50,7 @@ function whitw2(bb, chess, def) {
// ② the selectable enemy nearest to the drone, ties broken by the one nearest to her. [ASSUMED] distances are measured
// to every enemy's position (a huge enemy's centre, its 判定中心) — the owner's decision of 2026-10-04: the centre, so
// leader rounds stay close to 0.1.1; the sim's general convention for operator-side distance picks, the hit rectangle
// (body.js bodyDist: targeting.js sortEnemyTargets 'nearest', tier3 enemiesAround, 异客 / 溯光星源's chains), was
// (body.js bodyDist: targeting.js sortEnemyTargets 'nearest', 空弦's enemiesAround, 异客 / 溯光星源's chains), was
// considered — it made a huge leader (胄, 管) the nearest enemy of every drone around it (DESIGN §22.9)
const pickTarget = (battle, unit, d, ok) => {
let best = null, bd = Infinity, bh = Infinity;
+3 -2
View File
@@ -9,7 +9,8 @@
//
// Covered (normal + elite): 1_01 隐现 1_02 角峰 1_03 惊蛰 1_04 深巡 1_05 红豆(H) 1_06 刺玫 1_07 普罗旺斯 1_08 德克萨斯
// 1_09 跃跃 1_10 古米 1_11 地灵(H) 1_12 艾丝黛尔 1_13 波登可 1_14 格雷伊 1_16 锡人(H) 1_17 深靛
// 1_18 宴 1_19 野鬃 1_20 雷蛇. (H = hidden in the shop pool, still authored.) 1_15 盟约·辅助干员(H) lives in tier6.js.
// 1_18 宴 1_19 野鬃 1_20 雷蛇. (H = hidden in the shop pool, still authored.) 1_15 盟约·辅助干员(H) is registered
// with the tier-6 kits.
// Operator loadouts (DESIGN §16): every selectable non-default skill of the 16 visible chess is authored in the kit's
// `skills: { [skillId]: SkillSpec }` map from its own SkillRecord (skillRec / skillBbOf — Lv4 normal, Lv7 elite);
// talents / traits read the resolved record, so a module choice ('none' ⇒ traitBase / talentsBase, module.active
@@ -227,7 +228,7 @@ export const skillBbOf = (chess, skillId) => skillRec(chess, skillId)?.bb ?? {};
/**
* Skill blackboard `base_attack_time` → engine batPct of this chess's base attack time. The value is a FLAT change of
* the base attack time in seconds (AK attribute ADDITION: 送葬人 −0.5 on 2.3 s "少量缩短(-0.5)" ⇒ 1.8 s, 红豆 +0.5
* "略微增大(+0.5)" — the same convention as tier4/tier5), except a positive value described as a shortening, which is
* "略微增大(+0.5)" — the same convention as the tier-4 / tier-5 kits), except a positive value described as a shortening, which is
* the new interval ratio (深靛 0.7 "攻击间隔略微缩短" ⇒ ×0.7). The convention is per skill in AK: skills PRTS lists as
* a ratio (雷蛇 反击电弧 "+70%", 古米 食粮烹制 "+130%", 深靛 灯塔守卫者 "-80%") use batPct = value directly, not this.
*/
+3 -3
View File
@@ -1,9 +1,9 @@
// server/sim/content/kits/shared/tier2.js — helpers and notes of the Tier 2 operator kits (formerly tier2.js; the kits
// live one per file in ../ops/).
//
// export default { [baseChessId]: (bb, chess, def) => Kit } (docs/SIM.md §7.2). Shared helpers live in tier1.js (named
// exports). Numbers come from the skill / talent / trait blackboards (elite module upgrades are in the elite record's
// trait.bb and hidden index −1 talents). fx kinds: see the tier1.js header.
// export default { [baseChessId]: (bb, chess, def) => Kit } (docs/SIM.md §7.2). Shared helpers live in shared/tier1.js
// (named exports). Numbers come from the skill / talent / trait blackboards (elite module upgrades are in the elite
// record's trait.bb and hidden index −1 talents). fx kinds: see the shared/tier1.js header.
//
// Covered (normal + elite): 2_01 送葬人 2_02 赫默 2_03 崖心(H) 2_04 小满 2_05 哈洛德 2_06 莎草 2_07 幽灵鲨 2_08 泡泡
// 2_09 休谟斯 2_10 洛洛 2_11 风丸 2_12 砾 2_13 蒂比 2_14 调香师 2_15 协律(H) 2_16 拉普兰德 2_17 折桠 2_18 灰毫 2_19 锡人.
+1 -1
View File
@@ -159,7 +159,7 @@ function lowHpHealUp(battle, unit, tb) {
/**
* Modules "攻击范围扩大" (夕 SPC-X, 白面鸮 RIN-X): the range becomes the SELECTED module's own grid — the data's
* range-only talent change (talentIndex −1), e.g. SPC-X = the 3×3 caster range + ONE centre tile [0,3] — like tier4's
* range-only talent change (talentIndex −1), e.g. SPC-X = the 3×3 caster range + ONE centre tile [0,3] — like the tier-4 kits'
* 莫斯提马 / 莱恩哈特 / 白面鸮 (integration review: a flat forward +1 added a whole column, 3 tiles, and widened the skill
* ranges too). It replaces the unit's own range, so the DEFAULT trigger's initial range follows; skills with their own
* range keep it. Without such a grid in the data: one extra forward tile [ASSUMED].
+1 -1
View File
@@ -165,7 +165,7 @@ const selectedSkill = (chess, def) => chess?.skill?.skillId ?? def?.skill?.id ??
/**
* Skill blackboard `base_attack_time` → engine batPct, the rule of the AK damage calculator (akdata attributes.js,
* checked against the game): a shortening (v < 0) is a FLAT change of the base attack time in seconds (送葬人 −0.5 on
* 2.3 s ⇒ 1.8 s, the tier1 `batMod` convention), a lengthening (v > 0) scales it (迷迭香 S2 +0.5 ⇒ ×1.5, 佩佩 S3 +0.2 ⇒
* 2.3 s ⇒ 1.8 s, the shared/tier1.js `batMod` convention), a lengthening (v > 0) scales it (迷迭香 S2 +0.5 ⇒ ×1.5, 佩佩 S3 +0.2 ⇒
* ×1.2). `mul` = a skill the calculator lists as "攻击间隔缩短,但是是乘算负数" (异客 S2 −0.3 ⇒ ×0.7, 迷迭香 S3 −0.5 ⇒ ×0.5).
*/
function batOf(v, def, mul = false) {
+4 -3
View File
@@ -32,7 +32,7 @@
// its blast fx carries `consumed: true` so clients play the explosion, not a death sound)
// 从不混淆的方向 untargetable marker; when the owner's skill ends it vanishes and the owner returns to its tile
// 黄金盟誓 attacks deal true damage (trait); lasts while the owner's skill runs; 维娜 S3 places one on every
// free deployable tile around her (kits/tier6.js) — no per-owner deploy limit (SKILL_SUMMON_UNCAPPED)
// free deployable tile around her (kits/ops/chess_char_6_07-siege2.js) — no per-owner deploy limit (SKILL_SUMMON_UNCAPPED)
// 防护单元 untargetable, invulnerable device placed by the player (a hand piece, user playtest #6): shield =
// 凯瑟琳 max HP × max_shield_ratio on the operator in its range (range 1-1: the tile it faces; effects do
// not stack — `cathy:shield`, read by 凯瑟琳 S1 岁月锻打), in full whenever it takes a new operator,
@@ -691,7 +691,7 @@ function wolfPack(bb, raw, def) {
* The operator a 流形 copies ("可复制待部署区一名干员"): the nearest (Chebyshev tiles) living operator of its player on
* the field, not the summoner; ties → higher base ATK → lower id. (The remake deploys the whole board at battle start,
* so the official "operator waiting to deploy" is read as the nearest deployed operator — same pick as the 缪尔赛思
* kit, content/kits/tier6.js.) Null when there is none: the 流形 then waits with its copy skill ready.
* kit, content/kits/ops/chess_char_6_11-mlyss.js.) Null when there is none: the 流形 then waits with its copy skill ready.
*/
export function pickCopyTarget(battle, unit) {
let best = null, bs = null;
@@ -1438,7 +1438,8 @@ function deployLimitOf(u) {
/**
* Tactical point (战术点) of a tactician when the player placed no 援军 piece: `Battle.findTacticalPoint` — a free
* walkable tile of its initial range, on an enemy ground path first (where a player would put the blocker), then
* nearest to the tactician. Shared by every tactician kit (tier3 伺夜, the tokens' own fallback).
* nearest to the tactician. Shared by every tactician kit (伺夜, kits/ops/chess_char_3_19-vigil.js; the tokens' own
* fallback).
*/
export function tacticalPoint(battle, owner) {
return battle.findTacticalPoint(owner);
+3 -3
View File
@@ -106,9 +106,9 @@ export function loadoutRecord(rec, lo) {
* The attack range a (loadout-resolved) chess record fights with from its deployment — the detail card without a live
* entry, the board's range overlay and the deploy wheel (DESIGN §16), the same tiles the battle unit starts with (prep
* m.unitStats `range`): the selected skill's grid when it reads "被动效果:攻击范围扩大" (引星棘刺 S3 3-9: her own range
* while she carries it, tier5 kit); else an elite whose equipped module reads "攻击范围扩大" uses that module's own grid
* while she carries it, her kit); else an elite whose equipped module reads "攻击范围扩大" uses that module's own grid
* — its range-only talent change (talentIndex −1), e.g. SPC-X = the 3×3 caster range + the centre tile [0,3] — as the
* kits do (tier4 moduleRangeGrid, tier5 moduleRangeUp); anything else its `rangeGrid`. Then grown by the 特性's
* kits do (kits/shared/tier4.js moduleRangeGrid, shared/tier5.js moduleRangeUp); anything else its `rangeGrid`. Then grown by the 特性's
* permanent 攻击距离 (traitRangeExtend: 信仰搅拌机 SPT-Y "攻击距离+1"). A running skill's range is the live entry's.
* @param {object|null} rec loadoutRecord(…) output (or a data/chess.json record: its default module)
* @returns {number[][]|null}
@@ -131,7 +131,7 @@ export function attackRangeGrid(rec) {
/**
* The permanent 攻击距离 (ability_range_forward_extend) a record's 特性 grants — a module's, e.g. 信仰搅拌机 SPT-Y
* "攻击距离+1" (tier4 rangeUp: a persistent rangeExtend buff, s.baseRangeExtend); 0 for one that works "在集成战略中" only
* "攻击距离+1" (its kit's rangeUp: a persistent rangeExtend buff, s.baseRangeExtend); 0 for one that works "在集成战略中" only
* (空弦 ISW-A). The other 攻击距离 of the mode are skills' (their running range).
* @param {object|null} rec loadoutRecord(…) output
*/
+1 -1
View File
@@ -1,4 +1,4 @@
// Tier 1 alternate skills & modules (DESIGN §16 operator loadouts; server/sim/content/kits/tier1.js `skills` maps).
// Tier 1 alternate skills & modules (DESIGN §16 operator loadouts; the `skills` maps of the tier-1 kits, server/sim/content/kits/ops/).
// Every selectable non-default skill of every visible tier-1 chess runs a real battle — normal (Lv4) and elite (Lv7) —
// through the harness with its loadout (`skillIndex` / `moduleId` on the board entry, as a BattleSpec carries them),
// and its signature effect is asserted with numbers from that skill's own blackboard (data/chess.json skills[]).
+1 -1
View File
@@ -1,4 +1,4 @@
// Operator loadouts (DESIGN §16) for the tier-2 kits (server/sim/content/kits/tier2.js): every selectable NON-default
// Operator loadouts (DESIGN §16) for the tier-2 kits (server/sim/content/kits/ops/): every selectable NON-default
// skill of every visible tier-2 chess is hand-authored (tools/kit-coverage.mjs) and shows its signature effect for the
// normal (Lv4) and the elite (Lv7) chess — numbers from the selected skill's blackboard (data/chess.json skills[]) —
// and the elite's module choice ('none' instead of the default module) changes what the kit / profile does.
+1 -1
View File
@@ -1,4 +1,4 @@
// Tier-3 operator loadouts (DESIGN §16, server/sim/content/kits/tier3.js): every selectable NON-default skill of every
// Tier-3 operator loadouts (DESIGN §16, server/sim/content/kits/ops/): every selectable NON-default skill of every
// visible tier-3 chess is hand-authored (`skills[skillId]`) and proves its signature effect for the normal (Lv4) and the
// elite (Lv7) chess with its own blackboard; non-default modules that change behaviour are exercised too.
import { test } from 'node:test';
+1 -1
View File
@@ -1,5 +1,5 @@
// Tier-4 operator loadouts (DESIGN §16): every selectable NON-default skill of every visible tier-4 chess has a
// hand-authored spec in server/sim/content/kits/tier4.js (`skills[skillId]`), proven here by its signature effect for
// hand-authored spec in its kit file (server/sim/content/kits/ops/, `skills[skillId]`), proven here by its signature effect for
// the normal (Lv4) and the elite (Lv7) chess; non-default modules (and 'none') change what their text says. Numbers
// are read back from the data blackboards (the SELECTED skill's `bb`), real battles through the harness.
import { test } from 'node:test';
+1 -1
View File
@@ -1,4 +1,4 @@
// Tier 5 alternate skills & modules (DESIGN §16 operator loadouts; server/sim/content/kits/tier5.js `skills` maps).
// Tier 5 alternate skills & modules (DESIGN §16 operator loadouts; the `skills` maps of the tier-5 kits, server/sim/content/kits/ops/).
// Every selectable non-default skill of every visible tier-5 chess runs a real battle — normal (Lv4) and elite (Lv7) —
// through the harness with its loadout (`skillIndex` / `moduleId` on the board entry, as a BattleSpec carries them),
// and its signature effect is asserted with numbers from that skill's own blackboard (data/chess.json skills[]).
+1 -1
View File
@@ -1,4 +1,4 @@
// Operator loadouts (DESIGN §16) for the tier-6 kits (server/sim/content/kits/tier6.js): every selectable NON-default
// Operator loadouts (DESIGN §16) for the tier-6 kits (server/sim/content/kits/ops/): every selectable NON-default
// skill of every visible tier-6 chess is hand-authored (tools/kit-coverage.mjs) and shows its signature effect for the
// normal (Lv4) and the elite (Lv7) chess — numbers from the selected skill's blackboard (data/chess.json skills[]) —
// and the elite's non-default module choices (Y / RA modules, or 'none') change what the kit does.
+1 -1
View File
@@ -1,4 +1,4 @@
// Content tests for the tier 1 / tier 2 operator kits (server/sim/content/kits/tier1.js, tier2.js).
// Content tests for the tier 1 / tier 2 operator kits (server/sim/content/kits/ops/chess_char_1_*.js, chess_char_2_*.js).
// Every chess (normal + elite where the elite adds something) runs a real battle through the harness and the test
// asserts its signature effect with numbers taken from the chess blackboards (data/chess.json).
import { test } from 'node:test';
+1 -1
View File
@@ -1,4 +1,4 @@
// Tier-3 operator kits (server/sim/content/kits/tier3.js): every chess runs a real battle through the harness and
// Tier-3 operator kits (server/sim/content/kits/ops/chess_char_3_*.js): every chess runs a real battle through the harness and
// its signature skill / talent / module effect is asserted with numbers taken from its own blackboards.
import { test } from 'node:test';
import assert from 'node:assert/strict';
+1 -1
View File
@@ -1,4 +1,4 @@
// Tier-4 operator kits (server/sim/content/kits/tier4.js): one signature test per chess (+ elite checks), real battles
// Tier-4 operator kits (server/sim/content/kits/ops/chess_char_4_*.js): one signature test per chess (+ elite checks), real battles
// through the harness. Numbers are read back from the data blackboards so the tests follow data changes.
import { test } from 'node:test';
import assert from 'node:assert/strict';
+2 -2
View File
@@ -1,4 +1,4 @@
// Tier-5 operator kits (server/sim/content/kits/tier5.js): every chess runs a real battle through the harness and its
// Tier-5 operator kits (server/sim/content/kits/ops/chess_char_5_*.js): every chess runs a real battle through the harness and its
// signature effect is asserted with numbers taken from its own blackboards (normal Lv4 / elite Lv7).
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -998,7 +998,7 @@ test('录武官 S2: ATK +45 %; healed allies heal 80 HP per hit taken for 10 s;
});
// ------------------------------------------------------------------------------------------------------------------
// regressions of the 2026-09-28 fidelity review (PRTS 备注 of the base operators; see the tier5.js header)
// regressions of the 2026-09-28 fidelity review (PRTS 备注 of the base operators; see the kits/shared/tier5.js header)
test('圣约送葬人 T1 受选之人: the extra attack consumes no ammo and never reaches ammoUsed listeners', () => {
const log = [];
+1 -1
View File
@@ -1,4 +1,4 @@
// Content tests for the tier-6 kits (server/sim/content/kits/tier6.js) + 盟约·辅助干员 (chess_char_1_15).
// Content tests for the tier-6 kits (server/sim/content/kits/ops/chess_char_6_*.js) + 盟约·辅助干员 (chess_char_1_15).
// Every test runs a real battle through the harness and checks the signature effect of the kit.
import { test } from 'node:test';
import assert from 'node:assert/strict';
+1 -1
View File
@@ -1,4 +1,4 @@
// Content tests for user playtest #4 items 11 and 12 (server/sim/content/tokens.js, kits/tier2.js):
// Content tests for user playtest #4 items 11 and 12 (server/sim/content/tokens.js, kits/ops/…2_02-silent.js):
// #11 赫默's 医疗无人机 comes with her S2 — since user playtest #6 it is a hand piece the player places, it deploys
// once on that tile at the battle start (PRTS 卫戍协议/帮助 §作战阶段; settled by the user after playtest #6 —
// shared/constants.js SKILL_SUMMON_START_DEPLOY) and re-appears there each time S2 fires
+2 -2
View File
@@ -1,7 +1,7 @@
// User playtest #6 items 1 and 2 (from a friend): 凯瑟琳's device and 赫默's drone could not be placed by hand. The
// prep side is test/match/playtest6_summons.test.js; this file is the battle side of the placed pieces
// (server/sim/content/tokens.js dockSkillSummons / releaseSkillSummon, Battle.start `deferDeploy`, kits tier2 / tier3 /
// tier4, the 爬行号·防护单元 token kit):
// (server/sim/content/tokens.js dockSkillSummons / releaseSkillSummon, Battle.start `deferDeploy`, the summoner
// kits in kits/ops/, the 爬行号·防护单元 token kit):
// * PRTS 卫戍协议/帮助 §作战阶段: the board summons deploy after the operators; "若战场区初始部署有召唤物,若召唤物在
// 战斗期间退场,将在满足条件后立即原地再部署1个" — a placed piece marks the tile its summon deploys on;
// * a skill's summon (赫默 医疗探机, 巫恋 诅咒娃娃: "获得一个…") deploys once on its tile at the battle start, for free
+1 -1
View File
@@ -19,7 +19,7 @@
// routes RouteSpec[] (default: template routes, or flat-stage routes 0..3 = walk low, walk high, fly low, fly high)
// timeLimit seconds (default: template maxPlayTime, else 60; boss/hidden: Infinity)
// content 'full' | 'generic' | 'none' (default 'full')
// kits { [baseChessId]: (bb, chess, def) => Kit } injected kits (take precedence over kits/tier*.js)
// kits { [baseChessId]: (bb, chess, def) => Kit } injected kits (take precedence over kits/index.js)
// extraContent [{ install(battle) }] extra content modules installed after the domain modules (content tests)
// defs { chess: {id: record}, enemies: {key: record}, tokens: {id: record} } extra/override data records
// flags, sharedBoss, modeId, round, setup(battle), recordEvents (default true). The harness sets
+1 -1
View File
@@ -1,5 +1,5 @@
// test/render/fxwall.test.js — 余 S3 fire wall visual (render/fx.js): the sim's `firewall` fx carries `axis` ('col' for
// a RIGHT / LEFT facing, 'row' for UP / DOWN — sim/content/kits/tier6.js); the renderer lights that straight line of
// a RIGHT / LEFT facing, 'row' for UP / DOWN — sim/content/kits/ops/chess_char_6_03-yu.js); the renderer lights that straight line of
// tiles through his tile, clipped to the battle rect (it used to be a round zone whatever the facing).
import { test } from 'node:test';
+1 -1
View File
@@ -222,7 +222,7 @@ test('generic text rules: 泡泡 stops attacking; 小满 sleeps ≤3 enemies onc
});
test('generic element: 塑心 charges add 85 % ATK of 凋亡 (apoptosis) damage to the gauge', () => {
// the generic kit's numbers (tier6.js has a hand-written 塑心 kit whose talent amplifies apoptosis in range)
// the generic kit's numbers (kits/ops/chess_char_6_09-cello.js is a hand-written 塑心 kit whose talent amplifies apoptosis in range)
const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy() } }, units: [{ chessId: 'chess_char_6_09_a', row: 10, col: 4 }], enemies: [{ key: 'enemy_dummy', pos: [10, 6] }], timeLimit: 60, content: 'generic' });
const u = h.unit('chess_char_6_09_a');
assert.ok(h.runUntil(() => u.skill.activations >= 1 && h.enemy('enemy_dummy').elem.apoptosis > 0, 40));