feedback5: merge R4 — operator kits one file per chess (kits/ops/*, kits/shared/*, kits/index.js in the old registry order) + kits/README.md with the fidelity checklist; golden unchanged

# Conflicts:
#	.git-blame-ignore-revs
#	test/docs-consistency.test.js
This commit is contained in:
sganggs committed 2026-10-05 16:30:29 +08:00
commit 6a7a23765a
172 files changed
+13214 -11826

No files matched your search

+2
View File
@@ -9,3 +9,5 @@ b48a67caec5ba0699097e21f3db4d4dbb35a2aed
13224f9483d94f65089e289ff2be63f50a69b1cb
# feedback5 R3: server/sim/content/enemies.js → server/sim/content/enemies/*.js (helpers, archetypes, kit families)
560328cacc863c26d07780096527e255ebf62bc4
# feedback5 R4: kits/tier1..6.js → kits/ops/ (one file per kit), kits/shared/, kits/index.js
1169102d46742474bf0f0189d467c791984d9e05
+4 -2
View File
@@ -87,7 +87,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
@@ -406,7 +408,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
@@ -30,7 +30,8 @@ server/sim/
content/index.js installContent / setupUnitKit / registerAllMeta
content/enemies/ helpers.js, archetypes.js, one kit file per special type + leaders.js (content/enemies.js builds KITS)
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)
```
---
@@ -127,7 +128,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).
@@ -772,7 +773,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) |
@@ -846,7 +847,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),
@@ -888,11 +889,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
@@ -1086,7 +1087,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';
+4 -4
View File
@@ -1,7 +1,7 @@
// server/sim/content/index.js — installs all content into a Battle (DESIGN §7).
//
// installContent(battle, { mode }) — applies the per-battle loadout data view (simdata withUnitLoadouts; DESIGN §16),
// then mode 'full' (default): hand-authored kits (kits/tier1..6) with generic
// then mode 'full' (default): hand-authored kits (kits/index.js: one file per kit in kits/ops/) with generic
// fallback + every domain module's install(battle); 'generic': generic kits only, no domain modules;
// 'none': no kits (units never cast skills) and no modules. Every module install runs inside try/catch:
// a faulty content module is logged and skipped, never crashing the battle.
@@ -33,13 +33,13 @@ async function safeImport(path) {
}
}
const TIERS = await Promise.all([1, 2, 3, 4, 5, 6].map((t) => safeImport(`./kits/tier${t}.js`)));
const KIT_REGISTRY = await safeImport('./kits/index.js');
const DOMAIN_NAMES = ['tokens', 'devices', 'enemies', 'bosses', 'bonds', 'garrisons', 'items', 'bands', 'choices'];
const DOMAINS = await Promise.all(DOMAIN_NAMES.map((n) => safeImport(`./${n}.js`)));
const tokens = DOMAINS[0];
/** Merged kit registry: baseChessId → (bb, chess, def) => Kit */
export const KITS = Object.freeze(Object.assign({}, ...TIERS.map((m) => (m && m.default && typeof m.default === 'object' ? m.default : {}))));
/** Merged kit registry: baseChessId → (bb, chess, def) => Kit (kits/index.js; each kit file is loaded guarded too) */
export const KITS = KIT_REGISTRY.KITS ?? Object.freeze({});
/** Domain modules in install order: tokens, devices, enemies, bosses, bonds, garrisons, items, bands, choices. */
export const MODULES = Object.freeze(DOMAIN_NAMES.map((n, i) => [n, DOMAINS[i]]));
+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);
+185
View File
@@ -0,0 +1,185 @@
# Operator kits
A kit is the battle behaviour of one chess (棋子): its skills, talents, trait specifics and module effects, on top of
the profession defaults of `server/sim/professions.js`. Kits run inside the deterministic battle simulation that the
server and every browser share, so the same seed and inputs give the same battle everywhere. New kits — the stand-in
operators, the self-select 6★ operators, contributions from GitHub issue #136 — go here, one file each.
## Layout
| path | what |
|---|---|
| `index.js` | the registry: `KIT_FILES` lists every kit file, grouped by tier; `KITS` (base chess id → kit builder) and `TIER_KITS` (one registry per tier group) are built from it |
| `ops/<chessId>-<codename>.js` | one kit per file, with the helpers and constants only that kit uses |
| `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).
## Naming
`ops/<base chess id without _a>-<codename>.js`, where the code name is the operator's official `charId` without its
`char_<n>_` prefix (`data/chess.json` `charId`): `chess_char_3_04_a` (琳琅诗怀雅, `char_1033_swire2`) →
`ops/chess_char_3_04-swire2.js`.
- The chess id is the registry key and the one unique name: one operator can be two chess (锡人 `chess_char_1_16` and
`chess_char_2_19`, 耶拉 3_20 / 4_03, 华法琳 4_26 / 5_04 …), so a code name alone collides. One file ⇔ one registry
entry, and a directory listing sorts like the data: by tier, then shop order.
- The code name finds the operator by name; the docs and the code already call kits by it (`lemuen`, `whitw2`,
`sbell2`, `pithst`). It is ASCII and stable across languages — no Unicode file names (the macOS NFD / Windows / URL
encoding differences of `/sim/` in the browser) — and the Chinese name is in the file's first line, so
`grep -l 玛恩纳 ops/` works too.
- `-` separates the two parts, which both contain `_`.
## A kit file
```js
// server/sim/content/kits/ops/chess_char_6_21-codename.js — 名字 (char_xxxx_codename) kit, tier 6.
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
import { num, talentBb, statBuff } from '../shared/tier1.js'; // shared helpers, engine modules (relative paths)
/** 名字 S2 "…": the radius exists only in the PRTS 备注 (no blackboard key). */
const SOME_RADIUS = 1.5; // helpers / constants of this kit only
export default {
// 6_21 名字 — S3 …: what each skill / talent / module does, which blackboard key gives each number, [ASSUMED] where
// no source says.
chess_char_6_21_a: (bb, chess, def) => ({ skill: { … }, skills: { … }, talents: [ … ], trait: { … }, install() {} }),
};
```
- **The default export** is `{ [baseChessId]: (bb, chess, def) => Kit }` with exactly one key, the `_a` id; the same kit
serves the elite `_b` record. `bb` = the SELECTED skill's blackboard at the chess's level (normal Lv4, elite Lv7),
`chess` = the record as the unit's loadout makes it (talents, `trait.bb`, `module`), `def` = the normalised def. The
Kit fields (`skill`, `skills: { [skillId]: SkillSpec }`, `talents`, `trait`, `install`) and how the loader picks the
selected skill's spec: docs/SIM.md §7.2; the SkillSpec schema: §7.3; worked examples with real operators: §7.5.
- Small real files to start from: `ops/chess_char_1_11-skgoat.js`, `ops/chess_char_1_02-yak.js`; one with every
selectable skill: `ops/chess_char_3_04-swire2.js`; a large one with its own helpers: `ops/chess_char_6_18-whitw2.js`.
- The tier-4 files wrap their entry in `withDefaults(…)` (`shared/tier4.js`: it rebuilds `def` for callers that pass
`(bb, chess)` only); `content/index.js` always passes all three, so a new kit needs no wrapper.
## What a kit may use
- **Helpers**: `../shared/tier1.js` first — `num` / `talentBb` / `talentGrid` / `moduleBb` / `traitBb` / `moduleOn`,
`skillRec` / `skillBbOf` (another selectable skill's record and blackboard), `instantKind`, `batMod`, `up` / `cheb`,
`onHitBy` / `onHitOn` / `onDamagedOn`, `enemiesInGrid` / `alliesInGridOf` / `enemyInRange`, `statBuff` / `toggleBuff`
/ `installAura` / `spTimeBonus` / `installReveal`, `makeZone`, `freeTileAround`, `giveSp`, `once`. The other
`../shared/tierN.js` are usable too, but same-named helpers are not interchangeable: `num` of tier 1 / 4 also reads
numeric strings, those of tiers 3 / 5 / 6 do not; `toggleBuff`, `talentBb`, `moduleBb`, `instantKind` take different
arguments per tier. Read the helper before using it.
- **The battle API** documented in docs/SIM.md: units (§2), buffs / mods / statuses (§3), the damage & heal pipeline
(§4), the hook bus and its re-entrancy rule (§5), the engine helpers content must use (§6), the skill runtime (§7.1);
engine modules by relative path (`../../../constants.js`, `targeting.js`, `body.js` for huge enemies, `dir.js` for
facing, `damage.js` `hasHp` / `isHpLoss` / `mitigate`, `buffs.js`, `simdata.js` `normalizeChess`); content modules
(`../../tokens.js` summons, `../../items/battle.js`).
- **Never**: Node-only modules (`node:*`, `server/sim/nodeData.js`) or anything of `server/match`, `server/net`,
`public/` — the sim is served to browsers as-is (`/sim/`, no bundler); `Math.random`, `Date.now`, timers — use
`battle.rng()` (`rng.int / range / chance / pick / shuffle / weighted`), `battle.after` / `battle.every`; state kept
across battles in module variables — key it by battle (`WeakMap`) or keep it in `unit.mem`; mutating `bb`, `def`,
`chess` or range grids (deep-frozen, shared by every battle: copy them). Register hooks with `{ owner: unit }` and
check `unit.alive` in handlers. `test/content/kits_layout.test.js` enforces the import / clock rules.
- Order matters: hooks run by priority, then registration order, and every random draw comes from one stream — the
golden corpus (`test/golden/`) notices any change in either.
## Registering a kit
1. The chess needs its data record first: `data/chess.json` is generated from the official tables by
`tools/build-data.mjs` (docs/DATA.md) — skills, talents, trait, modules, ranges, tokens and the resolved auto-cast
trigger of every skill.
2. Add the kit file to `ops/`, named as above.
3. Append its file name to its tier's group in `index.js` `KIT_FILES` (never reorder the existing entries). Each file
is loaded with a guarded dynamic import: a file that fails to load is logged and its chess falls back to the
generic kit, so check the server log when a kit seems to do nothing.
4. `node --test test/content/kits_layout.test.js` checks the name, the registry entry and the imports;
`node tools/kit-coverage.mjs --missing` lists selectable skills that still fall back to the generic spec.
## Testing a kit
- The harness is `test/helpers/battleHarness.js` (`makeBattle`, `runUntil`, `hooksOf`, `eventsOf`, `checkInvariants`;
docs/SIM.md §10 has an example and every option). Seed everything, use synthetic `enemyRec` targets for exact
numbers, assert on ids, end with `checkInvariants(h.b)`.
- Patterns: `test/content/kits_t1t2.test.js` … `kits_t6.test.js` (the default skill and the talents of every chess),
`test/content/kits_alt_t1.test.js` … `kits_alt_t6.test.js` (every selectable non-default skill and every module;
`kits_alt_t4.test.js` also runs every chess × skill × module against a mixed wave),
`test/content/feedback1b_kits.test.js` (real-match checks).
A new kit gets its own test file (for example `test/content/kits_<codename>.test.js`).
- Golden results: a new or changed kit moves the digests of the scenarios that field its chess. Run
`npm run golden:update` and commit `test/golden/*.json` in the same change, saying which scenarios moved and why
(`test/golden/README.md`). A refactor never changes them.
- Run `node --test test/content/kits_layout.test.js test/golden.test.js <your test file>`, then
`GOLDEN_FULL=1 node --test test/golden.test.js` before the pull request.
## The fidelity rule
Every official skill, talent and module is implemented exactly as the game has it — no approximation.
- Sources: the official tables (`.cache/gamedata/excel/*.json`, which `tools/build-data.mjs` downloads: character_table,
skill_table, battle_equip_table / uniequip_table, activity_table → act2autochess) as built into `data/chess.json`;
PRTS (prts.wiki: the operator page, its 备注, 卫戍协议 pages) for mechanics the data does not carry — cite it.
- Numbers come from the blackboards whenever a key exists; a number that only exists in the text is parsed from it
(with a fallback) or a named constant with its source in the comment.
- When the text and the blackboard seem to disagree, find out what the game does and say so in the comment (DESIGN
§19.9: 至简's text says 购买价格为1 while `bb.price` is 2 — the blackboard is the discount off the tier price 3).
- If the engine cannot express a mechanic, extend the engine in a reviewed change of its own instead of faking it.
- Anything no source backs is marked `[ASSUMED]` in the comment and in the pull request; a decision of the owner is
cited as "owner's decision YYYY-MM-DD".
## Fidelity checklist (one per operator)
Each item is a mistake this project already made once. Tick every one for every operator in the pull request.
- [ ] **1. Skills** — every effect of each skill at the level the chess uses (normal Lv4, elite Lv7: the skill_table
blackboard and the text, reconciled), durations, charges, ammo, SP type (time / attack / hit) and initial SP (the
data's `spType`, `initSp`; SkillSpec overrides need a reason). Examples: ammo — 隐现 `ops/chess_char_1_01-inside.js`,
`kits_t1t2.test.js` "1_01 隐现: ammo skill"; charges — 松果 S1, `kits_alt_t3.test.js` "3_10 松果 S1 RMA长钉 (charges)";
hit SP — `feedback1d-solvent.test.js` (the drain feeds 受击回复 SP).
- [ ] **2. Talents** — every talent at the chess's level / potential (the talent blackboards), conditional ones too.
Example: `kits_t1t2.test.js` "1_01 隐现 elite: +self_ammo after `duration` s on field; a random other 【拉特兰】 ammo
operator +ally_ammo".
- [ ] **3. Modules** — for every module the chess offers (loadout, DESIGN §16): the trait override
(battle_equip_table `overrideTraitDataBundle`, the trait text), the talent changes (`addOrOverrideTalentDataBundle`)
and the stat bonuses (`attributeBlackboard`); `chess` is already the loadout-resolved record. Examples:
`kits_alt_t4.test.js` "信仰搅拌机 modules: SPT-Y 老朋友 = range +1 …" and "莫斯提马 SPC-X: the range becomes the module
grid …", and its "every chess × selectable skill × module" run.
- [ ] **4. Attack range** — the base range, the range while a skill that changes it is on, and back when it ends; a
skill's own 技能范围. Examples: 烛煌 S3, `test/sim/feedback1e-skillrange.test.js`; DESIGN §21.16 (the live range on the
card); 见行者 / 松果, `kits_t3.test.js` ("fires on an enemy inside the skill range only").
- [ ] **5. Auto-cast trigger** — the data resolves it (`tools/build-data.mjs resolveTrigger`, `data/chess.json`
`skills[].trigger`); check it, and change the data rule rather than hand-coding a trigger in the kit. The owner's
rules (0.2.0): a skill whose attack range is larger than the normal one casts as soon as an enemy is inside the
SKILL range (a deliberate deviation from the official 技能策略, like the 重装 exception; existing kits that predate
the rule, such as 烛煌 S3 in `feedback1e-skillrange.test.js`, are not examples of it); an AUTO skill that acts on
allies or itself fires at full SP (引星棘刺 S1, `kits_alt_t5.test.js` "… fires as soon as its SP is full, no enemy
needed (GitHub #124)"); the 重装 exception list (`TRIGGER_DEVIATIONS`: 深巡 S2, 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2,
余 S2; the 预备干员-重装 / Mechanist stand-ins join it), DESIGN §21.29 / §22.10,
`test/sim/feedback1-tank-triggers.test.js`.
- [ ] **6. Targeting** — can it hit air units (`canHitFly`), ground-only attacks, block count, target priority; can
ground enemies hit it (起飞 / 对地规避, stealth, camouflage flags). Examples: `test/sim/professions.test.js` "fortress
(号角 / 灰毫) is ground-only and never fires at FLY enemies" (DESIGN §22.13); `feedback1b_kits.test.js` "B4 玛恩纳 S3 …
hits the air units" vs "S1 / S2 … stay ground-only"; 蒂比 `ops/chess_char_2_13-tippi.js` (`LIFTOFF_FLAGS`, DESIGN
§21.22); `kits_t3.test.js` "忍冬 迷彩: … ranged enemies stop targeting her".
- [ ] **7. Damage typing** — physical / arts / true / element (元素损伤 vs 元素伤害: docs/SIM.md §7.2), splash vs
normal (普通伤害), dodgeable or not, `isSkill`, and what triggers on-hit effects (a trait that heals on any damage
dealt, e.g. 咒愈师). Examples: `enemies_bosses.test.js` "its attack splash (法术普通伤害) can be dodged; its death blast
(法术溅射伤害) cannot"; DESIGN §23.29 (叙拉古 rolls on every 普通伤害 hit); `test/sim/professions.test.js` "incantation
medics heal an ally for 50 % of damage dealt"; the re-entrancy rule of docs/SIM.md §5.
- [ ] **8. Summons / tokens** — placement rules (the owner's range, deployable tiles), redeploy, what they inherit from
the owner's skill / module (`data/tokens.json` variants, `battle.tokenDef`). Examples:
`test/match/feedback1-placement.test.js` "#9 data: 狼群 and 流形 are owner-range summons" (DESIGN §20.1, §21.3);
`test/content/playtest6_summons.test.js`, `tokens_summons.test.js`; `feedback1b_kits.test.js` "B3 维娜·维多利亚 S3".
- [ ] **9. Status effects** — every status applied, with its official duration and stacking (寒冷 on 寒冷 ⇒ 冻结 for the
longer cold, 沉默 removes abilities, 恐惧, 睡眠, 浮空 …; "同名效果取最高" = `applyStrongest`). Examples:
`test/sim/combat.test.js` "cold on cold ⇒ freeze for the longer cold …", "sleep: target is untargetable and
inactive; fear stops enemy attacks", "statuses: sleep = invulnerable except `hitSleep` attackers; levitate …";
`enemies_bosses.test.js` "沉默: exactly the abilities whose handbook line is SILENCE-flagged can be silenced".
- [ ] **10. Tests** — at least one per skill and per talent, plus the trigger rule, the range toggle, anti-air and
every module; the golden files updated in the same pull request (an intended change).
## Old references
DESIGN, the changelog and issues cite kits as `kits/tierN.js <name>` (before 0.2.0). That kit is now
`ops/<chess id>-<codename>.js` — search `ops/` for the chess id, the code name or the Chinese name — and the helpers of
that file are in `shared/tierN.js`.
+72
View File
@@ -0,0 +1,72 @@
// server/sim/content/kits/index.js — the operator kit registry (DESIGN §7, docs/SIM.md §7.2): baseChessId →
// (bb, chess, def) => Kit. One file per kit in ops/ (README.md: what a kit file holds, naming, registration, tests).
//
// KIT_FILES lists every kit file, grouped by tier — the former kits/tier1.js … tier6.js, each group in that file's
// order — so the merged registry keeps the key order it always had (content/index.js resolves kits by id; the tier
// shims, tools/kit-coverage.mjs and the golden corpus see the same registry as before). A new kit file is appended to
// its tier's group.
// Each file is loaded with a guarded dynamic import, so this runs unchanged in the browser (/sim/, no Node-only
// module, no directory listing): a file that fails to load (syntax error, throwing top-level code, missing file) is
// logged and skipped — its chess falls back to the generic kit — instead of breaking the server or the page.
export const KIT_FILES = Object.freeze([
// tier 1
['chess_char_1_01-inside.js', 'chess_char_1_02-yak.js', 'chess_char_1_03-leizi.js', 'chess_char_1_04-udflow.js',
'chess_char_1_05-vigna.js', 'chess_char_1_06-vendla.js', 'chess_char_1_07-prove.js', 'chess_char_1_08-texas.js',
'chess_char_1_09-caper.js', 'chess_char_1_10-sunbr.js', 'chess_char_1_11-skgoat.js', 'chess_char_1_12-estell.js',
'chess_char_1_13-podego.js', 'chess_char_1_14-greyy.js', 'chess_char_1_16-tinman.js', 'chess_char_1_17-indigo.js',
'chess_char_1_18-utage.js', 'chess_char_1_19-wildmn.js', 'chess_char_1_20-liskam.js'],
// tier 2
['chess_char_2_01-excu.js', 'chess_char_2_02-silent.js', 'chess_char_2_03-slchan.js', 'chess_char_2_04-grabds.js',
'chess_char_2_05-harold.js', 'chess_char_2_06-papyrs.js', 'chess_char_2_07-ghost.js', 'chess_char_2_08-bubble.js',
'chess_char_2_09-humus.js', 'chess_char_2_10-rockr.js', 'chess_char_2_11-kazema.js', 'chess_char_2_12-gravel.js',
'chess_char_2_13-tippi.js', 'chess_char_2_14-flower.js', 'chess_char_2_15-akkord.js', 'chess_char_2_16-whitew.js',
'chess_char_2_17-branch.js', 'chess_char_2_18-ashlok.js', 'chess_char_2_19-tinman.js'],
// tier 3
['chess_char_3_01-angel.js', 'chess_char_3_02-ayer.js', 'chess_char_3_03-swire.js', 'chess_char_3_04-swire2.js',
'chess_char_3_05-skadi.js', 'chess_char_3_06-philae.js', 'chess_char_3_07-forcer.js', 'chess_char_3_08-mint.js',
'chess_char_3_09-haini.js', 'chess_char_3_10-pinecn.js', 'chess_char_3_11-snhunt.js', 'chess_char_3_12-blemsh.js',
'chess_char_3_13-malist.js', 'chess_char_3_14-slbell.js', 'chess_char_3_15-vodfox.js', 'chess_char_3_16-snakek.js',
'chess_char_3_17-shotst.js', 'chess_char_3_18-vulpis.js', 'chess_char_3_19-vigil.js', 'chess_char_3_20-kjera.js',
'chess_char_3_21-archet.js'],
// tier 4
['chess_char_4_01-rmixer.js', 'chess_char_4_02-mostma.js', 'chess_char_4_03-kjera.js', 'chess_char_4_04-ines.js',
'chess_char_4_05-beewax.js', 'chess_char_4_06-kroos2.js', 'chess_char_4_07-bpipe.js', 'chess_char_4_08-rosesa.js',
'chess_char_4_09-mizuki.js', 'chess_char_4_10-aroma.js', 'chess_char_4_11-cathy.js', 'chess_char_4_12-glady.js',
'chess_char_4_13-gnosis.js', 'chess_char_4_14-lionhd.js', 'chess_char_4_15-reckpr.js', 'chess_char_4_16-texas2.js',
'chess_char_4_17-hsguma.js', 'chess_char_4_18-mudrok.js', 'chess_char_4_19-flamtl.js', 'chess_char_4_20-fartth.js',
'chess_char_4_21-plosis.js', 'chess_char_4_22-svrash.js', 'chess_char_4_23-gvial2.js', 'chess_char_4_24-billro.js',
'chess_char_4_25-cetsyr.js', 'chess_char_4_26-bldsk.js'],
// tier 5
['chess_char_5_01-excu2.js', 'chess_char_5_02-titi.js', 'chess_char_5_03-blaze2.js', 'chess_char_5_04-bldsk.js',
'chess_char_5_05-ulpia.js', 'chess_char_5_06-etlchi.js', 'chess_char_5_07-surtr.js', 'chess_char_5_08-horn.js',
'chess_char_5_09-cetsyr.js', 'chess_char_5_10-lisa.js', 'chess_char_5_11-demkni.js', 'chess_char_5_12-dusk.js',
'chess_char_5_13-ghost2.js', 'chess_char_5_14-svash2.js', 'chess_char_5_15-thorn2.js', 'chess_char_5_16-plosis.js',
'chess_char_5_17-f12yin.js', 'chess_char_5_18-gvial2.js', 'chess_char_5_19-mlynar.js', 'chess_char_5_20-aglina.js',
'chess_char_5_21-sntlla.js', 'chess_char_5_22-nymph.js', 'chess_char_5_23-reckpr.js'],
// tier 6 (and 盟约·辅助干员 chess_char_1_15, a hidden tier-1 chess always registered with the tier-6 kits)
['chess_char_1_15-pithst.js', 'chess_char_6_01-lemuen.js', 'chess_char_6_02-sbell2.js', 'chess_char_6_03-yu.js',
'chess_char_6_04-skadi2.js', 'chess_char_6_05-pasngr.js', 'chess_char_6_06-pepe.js', 'chess_char_6_07-siege2.js',
'chess_char_6_08-reed2.js', 'chess_char_6_09-cello.js', 'chess_char_6_10-nymph.js', 'chess_char_6_11-mlyss.js',
'chess_char_6_12-rosmon.js', 'chess_char_6_13-angel2.js', 'chess_char_6_14-lumen.js', 'chess_char_6_15-qiubai.js',
'chess_char_6_16-halo2.js', 'chess_char_6_17-nearl2.js', 'chess_char_6_18-whitw2.js', 'chess_char_6_19-blkkgt.js',
'chess_char_6_20-agoat2.js'],
].map((group) => Object.freeze(group)));
async function loadKitFile(file) {
try {
return await import(`./ops/${file}`);
} catch (e) {
console.error(`[content] failed to load kits/ops/${file}: ${e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e}`);
return {};
}
}
const MODULES = await Promise.all(KIT_FILES.map((group) => Promise.all(group.map(loadKitFile))));
const registryOf = (m) => (m && m.default && typeof m.default === 'object' ? m.default : {});
/** The registry of each tier group (index 0 = tier 1): baseChessId → kit builder, in KIT_FILES order. */
export const TIER_KITS = Object.freeze(MODULES.map((group) => Object.assign({}, ...group.map(registryOf))));
/** The merged kit registry: baseChessId → (bb, chess, def) => Kit, tier 1 … tier 6. */
export const KITS = Object.freeze(Object.assign({}, ...TIER_KITS));
@@ -0,0 +1,61 @@
// server/sim/content/kits/ops/chess_char_1_01-inside.js — 隐现 (char_498_inside) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, up, once, skillBbOf, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_01 隐现 “解决麻烦”: ammo (attack@trigger_time) ATK+atk, base attack time +base_attack_time s, prefers ranged
// enemies, taunt −1. 火力支援: after `duration` s on the field own ammo +self_ammo (from the next cast); elite: another
// random 【拉特兰】 op's ammo +ally_ammo while that deployment lasts (one grant per source).
// Alternate S1 “不惹麻烦” (AUTO, attack SP): attack@trigger_time rounds of attack@atk_scale × ATK physical (an ammo
// skill, so 火力支援's larger magazine applies to it too).
chess_char_1_01_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const dur = num(t.duration), selfAmmo = num(t.self_ammo), allyAmmo = num(t.ally_ammo);
const s1 = skillBbOf(chess, 'skchr_inside_1');
return {
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 1),
mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess, def?.skill?.description ?? chess?.skill?.desc), taunt: -1 },
targeting: { priority: 'ranged' },
},
skills: {
skchr_inside_1: { kind: 'ammo', ammo: Math.max(1, Math.floor(num(s1['attack@trigger_time'], 4))), attack: { atkScale: num(s1['attack@atk_scale'], 1) } },
},
talents: [{ install(battle, unit) {
const onField = () => up(unit) && battle.time - unit.deployedAt + 1e-9 >= dur;
battle.on('skillStart', ({ unit: u, skill }) => {
if (u === unit && skill.kind === 'ammo' && onField() && selfAmmo > 0) skill.addAmmo(selfAmmo);
}, { owner: unit });
if (!(allyAmmo > 0)) return;
// elite: once per deployment, `duration` s after it, one random other 【拉特兰】 operator with an ammo skill gets
// +ally_ammo for as long as this deployment lasts (one grant per source — redeploying never stacks it)
battle.on('deploy', ({ unit: u }) => {
if (u !== unit) return;
const seq = unit.deploySeq;
battle.after(dur, () => {
// (the larger magazine applies from the next activation: a running skill keeps its rounds)
if (!up(unit) || unit.deploySeq !== seq) return;
const pool = battle.allyUnits.filter((a) => a !== unit && a.kind === 'op' && a.ownerId === unit.ownerId && a.alive
&& (a.def.bonds || []).includes('lateranoShip') && a.skill && a.skill.kind === 'ammo');
const pick = battle.rng.pick(pool);
if (!pick) return;
(pick.mem.insiderAmmo ??= new Map()).set(unit.id, { src: unit, seq, n: allyAmmo });
battle.fx('buff', { x: pick.x, y: pick.y, id: pick.id, kind: 'ammo' });
}, { owner: unit });
}, { owner: unit });
once(battle, 'insider:bonusAmmo', () => battle.on('skillStart', ({ unit: u, skill }) => {
const m = u.mem.insiderAmmo;
if (!m || skill.kind !== 'ammo') return;
let n = 0;
for (const [k, g] of m) {
if (up(g.src) && g.src.deploySeq === g.seq) n += g.n;
else m.delete(k);
}
if (n > 0) skill.addAmmo(n);
}));
} }],
};
},
};
@@ -0,0 +1,19 @@
// server/sim/content/kits/ops/chess_char_1_02-yak.js — 角峰 (char_199_yak) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, statBuff, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_02 角峰 抗寒体质: HP +max_hp, DEF +def, RES ×(1+magic_resistance). 雪原卫士: RES +magic_resistance (flat).
// Alternate S1 体能强化 (重装 ⇒ TAKE_DAMAGE trigger from data): HP +max_hp, +hp_recovery_per_sec HP per second
// (the 生命回复速度 attribute). Elite module PRO-Y (block 4) is a stat of the module (attr blockCnt).
chess_char_1_02_a: (bb, chess) => {
const s1 = skillBbOf(chess, 'skchr_yak_1');
return {
skill: { kind: 'duration', mods: { hpPct: num(bb.max_hp), defPct: num(bb.def), resMul: 1 + num(bb.magic_resistance) } },
skills: { skchr_yak_1: { kind: 'duration', mods: { hpPct: num(s1.max_hp), hpRegen: num(s1.hp_recovery_per_sec) } } },
talents: [{ install(battle, unit) { statBuff(battle, unit, 'talent:yak', { resFlat: num(talentBb(chess, 0).magic_resistance) }); } }],
};
},
};
@@ -0,0 +1,27 @@
// server/sim/content/kits/ops/chess_char_1_03-leizi.js — 惊蛰 (char_306_leizi) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { CHAIN_RADIUS } from '../../../constants.js';
import { num, talentBb, traitBb, onHitBy, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_03 惊蛰 初雷: ATK +atk, chain jumps lose no damage. 通流无阻: vs unblocked enemies ATK ×atk_scale (every jump).
// Alternate S1 攻击力强化·γ型: ATK +atk (the chain keeps its falloff). Elite module CHA-X (4 jumps, −10 %, 0.8 s 停顿)
// is the chain profile of the module trait (professions.js TUNE.chain).
chess_char_1_03_a: (bb, chess) => {
const scale = num(talentBb(chess, 0).atk_scale, 1);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, attack: {} },
skills: { 'skcom_atk_up[3]': { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, 'skcom_atk_up[3]').atk) } } },
talents: [{ install(battle, unit) {
if (scale !== 1) onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && !target.blockedBy) dmg.amount *= scale; });
} }],
install(battle, unit) {
// the chain shape (count / sluggish, elite module included) comes from the resolved chain profile
const ch = unit.profile.chain || { count: num(traitBb(chess)['attack@max_target'], 3), radius: CHAIN_RADIUS, sluggish: num(traitBb(chess)['attack@sluggish'], 0.5) };
if (unit.skill && unit.skill.spec.attack) unit.skill.spec.attack.chain = { ...ch, falloff: 0 };
},
};
},
};
@@ -0,0 +1,45 @@
// server/sim/content/kits/ops/chess_char_1_04-udflow.js — 深巡 (char_4137_udflow) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, moduleOn, installReveal, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_04 深巡 行动能力剥夺: longer line range (skill grid), ATK +atk, ASPD +attack_speed, fin darts pierce
// attack@max_target enemies on the line and cause attack@sluggish s of 停顿.
// 技能策略 → DEFAULT (PR #12; DESIGN §21.29): the official 下半 class row hands every MANUAL 重装 skill TAKE_DAMAGE,
// which makes a 2-2 ranged 哨戒铁卫 wait until something hits her — in practice until she blocks (GitHub issue #4). By
// the owner's deliberate deviation from that row (2026-10-03, community feedback) this offensive ranged skill takes
// the basic strategy (SP ready + about to attack + an enemy inside the initial range); her data says DEFAULT too
// (rawRule keeps the official TAKE_DAMAGE). Only the rule changes: spCost / initSp / spType still come from data.
// 细胞活性抑制剂: attacks inflict `damage` arts per `interval` s for `duration` s (damage_seamonster vs 【海怪】).
// Elite module (SPT-X): stealth of enemies inside the range is cancelled.
// Alternate S1 侵袭破坏应对 (重装 ⇒ TAKE_DAMAGE trigger from data): ATK +atk, DEF +def.
chess_char_1_04_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const s1 = skillBbOf(chess, 'skchr_udflow_1');
return {
skill: {
trigger: 'DEFAULT',
kind: 'duration', mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) },
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null, maxTargets: num(bb['attack@max_target'], 1) },
attack: { onHitStatus: { key: 'sluggish', duration: num(bb['attack@sluggish'], 1) } },
},
skills: { skchr_udflow_1: { kind: 'duration', mods: { atkPct: num(s1.atk), defPct: num(s1.def) } } },
talents: [{ install(battle, unit) {
const dur = num(t.duration), iv = num(t.interval, 1);
if (!(dur > 0) || !(num(t.damage) > 0)) return;
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg?.isAttack || ctx.target.side !== 'enemy' || !ctx.target.alive) return;
const sea = (ctx.target.def?.tags || []).includes('seamonster');
const amount = sea ? num(t.damage_seamonster, num(t.damage) * 2) : num(t.damage);
battle.addBuff(ctx.target, {
key: `udflow:dot:${unit.id}`, duration: dur, interval: iv, refresh: 'extend', source: unit, visible: false,
onTick: ({ battle: b, unit: e }) => b.dealDamage(unit, e, { amount, type: 'arts', canDodge: false, tags: ['dot'] }),
});
}, { owner: unit });
} }],
install(battle, unit) { if (moduleOn(chess) && chess.isGolden) installReveal(battle, unit); },
};
},
};
@@ -0,0 +1,33 @@
// server/sim/content/kits/ops/chess_char_1_05-vigna.js — 红豆 (char_290_vigna) kit, tier 1 (hidden).
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_05 红豆 (hidden) 槌音: base attack time +base_attack_time s, ATK +atk. 蛮力穿刺: each attack prob1 (prob2 while the skill
// runs) for ATK +atk on that attack. Elite module (CHG-Y): vs enemies below hp_ratio HP, ATK ×atk_scale.
chess_char_1_05_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess) } },
talents: [{ install(battle, unit) {
const key = 'vigna:proc';
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit) return;
const p = unit.skill?.active ? num(t.prob2) : num(t.prob1);
if (p > 0 && battle.rng.chance(p)) {
battle.addBuff(unit, { key, duration: 0.1, mods: { atkPct: num(t.atk) } });
battle.fx('crit', { x: unit.x, y: unit.y, id: unit.id });
}
}, { owner: unit });
// melee hits resolve synchronously inside the attack: the one-attack buff is dropped right after it
battle.on('attack', (ctx) => { if (ctx.attacker === unit && unit.findBuff(key)) battle.removeBuff(unit, key); }, { owner: unit });
if (tb.hp_ratio != null && num(tb.atk_scale, 1) !== 1) {
onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && target.hpRatio < num(tb.hp_ratio)) dmg.amount *= num(tb.atk_scale, 1); });
}
} }],
};
},
};
@@ -0,0 +1,73 @@
// server/sim/content/kits/ops/chess_char_1_06-vendla.js — 刺玫 (char_494_vendla) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, up, byEnemyAttack, alliesInGridOf, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_06 刺玫 荆藤庇荫: ATK +atk; the ally with the highest max HP in range gets taunt +taunt_level; whenever that ally is
// attacked, 刺玫 deals atk_scale × ATK arts to the attacker, and her trait heal (scale × damage) only goes to that ally.
// 土壤基肥改良: the highest-max-HP ally in range receives heal_scale × healing ("受到的治疗效果提升") — never its natural /
// skill HP regeneration (the 生命回复速度 attribute is "不受治疗加成影响": 宴 分神, 角峰 体能强化; PRTS).
// Alternate S1 战术咏唱·γ型: ASPD +attack_speed (the trait heal keeps going to the most injured ally in range).
// Elite module INC-X (heal 60 % of the damage) is the incantation profile of the module trait (TUNE.incantationmedic).
chess_char_1_06_a: (bb, chess) => {
const hs = num(talentBb(chess, 0).heal_scale, 1);
const tauntKey = 'vendla:taunt';
// the protégé only exists while 荆藤庇荫 runs (mem.protege is set by its onStart only)
const protege = (u) => (u.skill?.active && up(u.mem.protege) ? u.mem.protege : null);
return {
skills: { 'skcom_magic_rage[3]': { kind: 'duration', mods: { aspd: num(skillBbOf(chess, 'skcom_magic_rage[3]').attack_speed) } } },
trait: {
// 咒愈师 trait: EVERY damage she deals heals an ally for 50 % of it (professions.js `installIncantation`,
// buff_template_data `vendla_tr` = ON_AFTER_OUTPUT_DAMAGE) — while 荆藤庇荫 runs her S2 says "仅对该角色触发刺玫
// 特性", so her protégé is the target then; otherwise it is the lowest-HP ally in range.
install(battle, u) {
battle.on('damaged', (c) => {
const t = c.target;
if (c.source !== u || !u.alive || !t || t.side !== 'enemy' || !(c.amount > 0)) return;
if (c.type === 'element' || c.type === 'elemental') return;
const ally = (c.dmg && c.dmg.traitAlly) || protege(u) || battle.lowestHpAllyInRange(u);
if (ally) battle.heal(u, ally, c.amount * (u.profile.healRatio ?? 0.5), { tags: ['incantation'] });
}, { owner: u });
},
},
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit, skill }) {
const cands = alliesInGridOf(battle, unit).sort((a, b) => b.s.maxHp - a.s.maxHp || a.deploySeq - b.deploySeq);
const p = cands[0] ?? null;
unit.mem.protege = p;
if (!p) return;
battle.addBuff(p, { key: tauntKey, duration: skill.duration + 0.1, mods: { taunt: num(bb.taunt_level, 1) }, visible: true, source: unit });
battle.fx('taunt', { x: p.x, y: p.y, id: p.id });
},
onEnd({ battle, unit }) {
if (unit.mem.protege) battle.removeBuff(unit.mem.protege, tauntKey);
unit.mem.protege = null;
},
},
talents: [{ install(battle, unit) {
battle.on('damaged', (ctx) => {
const p = protege(unit);
if (!p || ctx.target !== p || !byEnemyAttack(ctx) || !ctx.source.alive || !unit.canAct) return;
// "并仅对该角色触发刺玫特性": this counter damage is healed by the trait (install above) for the protégé — the
// damage instance names her, so no separate heal here (it would double)
battle.dealDamage(unit, ctx.source, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, canDodge: false, tags: ['counter'], traitAlly: p });
battle.fx('counter', { x: ctx.source.x, y: ctx.source.y, id: unit.id });
}, { owner: unit });
if (hs !== 1) {
let cacheT = -1, top = null;
battle.on('heal', (ctx) => {
if (!up(unit) || ctx.opts?.regen) return;
if (cacheT !== battle.time) {
cacheT = battle.time;
top = alliesInGridOf(battle, unit).sort((a, b) => b.s.maxHp - a.s.maxHp || a.deploySeq - b.deploySeq)[0] ?? null;
}
if (ctx.target === top) ctx.amount *= hs;
}, { owner: unit });
}
} }],
};
},
};
@@ -0,0 +1,64 @@
// server/sim/content/kits/ops/chess_char_1_07-prove.js — 普罗旺斯 (char_145_prove) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import { frontOf } from '../../../dir.js';
import { bodyOnTile } from '../../../body.js';
import { num, talentBb, isMainHit, onHitBy, skillBbOf } from '../shared/tier1.js';
/** 普罗旺斯 杀戮嗅觉 "普通攻击不再以生命值高于80%的敌人作为目标" — the 80 % exists only in the skill text. */
const PROVE_S2_MAX_HP = 0.8;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_07 普罗旺斯 狼眼 (passive): per hp_ratio_drop of HP the target has lost, ATK +atk_scale_up against it — CONTINUOUS,
// not in 20 % steps (PRTS note: "并非以每20%为固定节点 … 攻击力倍率提升至[100%+(100%-目标生命比例)×加成量×5]", i.e.
// ×(1 + lost / hp_ratio_drop × atk_scale_up)).
// 狩猎箭头: each attack prob (prob2 when the enemy is on the tile right in front) for ATK ×atk_scale.
// 狼眼 is an ATK increase ⇒ it scales her (ATK-based) attacks only, never fixed-value damage she sources
// (叙拉古 5000+50·L true proc, fixed item/DoT ticks…).
// Alternate S2 杀戮嗅觉: ATK +atk, but normal attacks never target an enemy above 80 % HP (with only such enemies in
// range she holds her fire). Her attacks stay normal attacks. (狼眼 is S1 only: absent when S2 is carried.)
// Elite module ARC-X only changes stats (ATK/DEF, faster redeploy).
chess_char_1_07_a: (bb, chess) => {
const t = talentBb(chess, 0);
const drop = num(bb.hp_ratio_drop), upPer = num(bb.atk_scale_up);
const S2 = 'skchr_prove_2';
const huntable = (e) => e.hpRatio <= PROVE_S2_MAX_HP + 1e-9;
const s2On = (u) => !!(u.skill && u.skill.active && u.skill.id === S2);
return {
skill: { kind: 'passive' },
skills: { [S2]: { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, S2).atk) } } },
trait: {
canAttack: (battle, u) => !s2On(u) || battle.enemiesInKeys(u.rangeKeys, u, u.profile).some(huntable),
},
install(battle, unit) {
if (unit.skill?.id !== S2) return;
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !s2On(unit) || ctx.targets.every(huntable)) return;
const prof = ctx.profile || unit.profile;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, prof).filter(huntable);
sortEnemyTargets(battle, unit, cands, prof.priority);
ctx.targets = cands.slice(0, Math.max(1, ctx.targets.length));
}, { owner: unit });
},
talents: [{ install(battle, unit) {
onHitBy(battle, unit, ({ target, dmg }) => {
if (!dmg.isAttack) return;
if (drop > 0 && upPer > 0) {
const lost = Math.max(0, Math.min(1, 1 - target.hpRatio));
if (lost > 0) dmg.amount *= 1 + (lost / drop) * upPer;
}
if (!isMainHit(dmg)) return;
const [fr, fc] = frontOf(unit.tileR, unit.tileC, unit.dir);
const front = bodyOnTile(target, fr, fc);
const p = front ? num(t.prob2) : num(t.prob);
if (p > 0 && battle.rng.chance(p)) {
dmg.amount *= num(t.atk_scale, 1);
battle.fx('crit', { x: target.x, y: target.y, id: unit.id });
}
});
} }],
};
},
};
@@ -0,0 +1,43 @@
// server/sim/content/kits/ops/chess_char_1_08-texas.js — 德克萨斯 (char_102_texas) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, enemiesInGrid, skillBbOf, RING1 } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_08 德克萨斯 剑雨: +cost DP; every enemy around (skill grid) takes two hits of atk_scale × ATK arts and is
// stunned `stun` s — air units too (PRTS 备注 "※可对空"). 战术快递: +cost initial DP. (Elite hidden runtime_cost −4
// "首次部署时部署费用-4": the initial deployment is free in battle ⇒ no effect.)
// Alternate S1 冲锋号令·γ型 (AUTO, no target): +cost DP at once. An AUTO skill keeps its normal rule in this mode
// (research 03 §1.4: only MANUAL skills are converted by the trigger table) ⇒ it fires as soon as SP is full.
chess_char_1_08_a: (bb, chess, def) => ({
skills: {
'skcom_charge_cost[3]': {
kind: 'instant', trigger: 'SP_FULL',
onStart({ battle, unit }) {
const n = num(skillBbOf(chess, 'skcom_charge_cost[3]').cost);
battle.addDp(unit.ownerId, n);
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
},
},
},
skill: {
kind: 'instant',
onStart({ battle, unit }) {
battle.addDp(unit.ownerId, num(bb.cost));
battle.fx('dp', { x: unit.x, y: unit.y, n: num(bb.cost), id: unit.id });
const grid = def?.skill?.rangeGrid;
const foes = grid ? enemiesInGrid(battle, unit, grid) : battle.foesInRadius(unit.x, unit.y, RING1).filter((e) => !e.s.flags.untargetable);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 2, id: unit.id, skill: 'swordRain' });
for (const e of foes) {
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun), source: unit });
}
},
},
talents: [{ install(battle, unit) {
const n = num(talentBb(chess, 0).cost);
if (n > 0) battle.on('battleStart', () => battle.addDp(unit.ownerId, n), { owner: unit });
} }],
}),
};
@@ -0,0 +1,32 @@
// server/sim/content/kits/ops/chess_char_1_09-caper.js — 跃跃 (char_4100_caper) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, cheb, onHitBy, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_09 跃跃 (回环射手: every attack is a boomerang out to the target and back — she attacks again only once it is
// caught, professions.js loopshooter / ai.js throwBoomerang). 乐趣加倍: ATK +atk, each attack throws `cnt`
// boomerangs at the target (cnt hits); they fly the same path, so the flight carries cnt hits and all of them are
// back together ("必须回收全部回旋投掷物才可以进行下一次攻击"). 戏耍随心: prob per hit (each boomerang on its own) for
// ATK ×atk_scale. Elite module (LPS-X, trait atk_scale): vs enemies on the 8 surrounding tiles ATK ×atk_scale.
// Alternate S1 强力击·β型 (AUTO, attack SP): the next attack at atk_scale × ATK.
chess_char_1_09_a: (bb, chess) => {
const t = talentBb(chess, 0);
const near = num(traitBb(chess).atk_scale, 1);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, attack: { hits: Math.max(1, Math.floor(num(bb.cnt, 1))) } },
skills: { skchr_caper_1: { kind: 'instant', attack: { atkScale: num(skillBbOf(chess, 'skchr_caper_1').atk_scale, 1) } } },
talents: [{ install(battle, unit) {
onHitBy(battle, unit, ({ target, dmg }) => {
if (!dmg.isAttack) return;
if (near !== 1 && cheb(unit, target) <= 1) dmg.amount *= near;
if (!dmg.isSplash && num(t.prob) > 0 && battle.rng.chance(num(t.prob))) {
dmg.amount *= num(t.atk_scale, 1);
battle.fx('crit', { x: target.x, y: target.y, id: unit.id });
}
});
} }],
};
},
};
@@ -0,0 +1,85 @@
// server/sim/content/kits/ops/chess_char_1_10-sunbr.js — 古米 (char_196_sunbr) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, isMainHit, onHitBy, instantKind, skillRec } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_10 古米 备用军粮 (自动触发, ct charges — an AUTO skill: no 技能策略 applies, it keeps its own rule; PRTS 备注 "此技能
// 在存在生命值不满的可治疗角色时可触发;技能触发后古米将切换至治疗模式(普通攻击改为治疗技能范围内的一名友方单位),直至古米
// 完成一次普通攻击的治疗"): cast as soon as a healable ally of the skill range (x-4, herself included) is injured —
// no enemy needed (user playtest #6: it waited until she engaged) — then her next normal attack heals the most
// injured ally of the skill range for heal_scale × ATK instead of hitting an enemy; she makes no normal attack until
// that heal is done. Elite module (GUA-X): targets below hp_ratio HP get ×heal_scale more.
// 平底锅专精: each attack prob for ATK ×atk_scale and a `stun` s stun (only when that hit lands).
// Alternate S2 食粮烹制 (PRTS notes: 10 s 缴械 at the start, the real duration is disarm + 30 s, attacks become heals
// with the attack interval +130 %): cooking for `disarm` s — no attacks, DEF +def; then for the skill's `duration` s
// she heals the most injured ally inside the skill grid (x-4) for ATK per attack, ATK +atk, base attack time
// ×(1 + base_attack_time). The GUA-X heal bonus (below hp_ratio: ×heal_scale) applies to these heals too.
// Trigger: TAKE_DAMAGE (data: the 重装 strategy "不受技能范围影响,受到伤害时释放技能").
chess_char_1_10_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const S2 = 'skchr_sunbr_2';
const r2 = skillRec(chess, S2);
const b2 = r2?.bb ?? {};
const cook = Math.max(0, num(b2.disarm, 10));
const cookKey = 'sunbr:cook', serveKey = 'sunbr:serve';
const healGrid = def?.skill?.rangeGrid ?? null;
return {
skills: {
[S2]: {
kind: 'duration', duration: cook + Math.max(0, num(r2?.duration, 30)),
mods: { batPct: Math.max(0, num(b2.base_attack_time)) },
targeting: { rangeGrid: r2?.rangeGrid ?? def?.skill?.rangeGrid ?? null },
attack: { dmgType: 'heal', heal: { mode: 'single' } },
onStart({ battle, unit }) {
battle.addBuff(unit, { key: cookKey, duration: cook, mods: { defPct: num(b2.def) }, flags: { disarm: true }, tags: ['skill'], visible: true });
},
onTick({ battle, unit }) {
if (!unit.findBuff(cookKey) && !unit.findBuff(serveKey)) {
battle.addBuff(unit, { key: serveKey, mods: { atkPct: num(b2.atk) }, tags: ['skill'] });
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'heal' });
}
},
onEnd({ battle, unit }) {
battle.removeBuff(unit, cookKey);
battle.removeBuff(unit, serveKey);
},
},
},
install(battle, unit) {
if (tb.hp_ratio == null || num(tb.heal_scale, 1) === 1) return;
// GUA-X on her skill heals (S1's heal-mode attack, S2's attack heals while it runs — herself included, she stands
// in the grid), never her natural / self regeneration
battle.on('heal', (ctx) => {
if (ctx.source !== unit || !unit.skill?.active || ctx.opts?.regen || ctx.opts?.self) return;
if (ctx.target.hpRatio < num(tb.hp_ratio)) ctx.amount *= num(tb.heal_scale, 1);
}, { owner: unit });
},
skill: {
kind: instantKind(def),
heal: true,
...(healGrid ? { trigger: { rule: 'SKILL_RANGE', grid: healGrid, allies: true }, targeting: { rangeGrid: healGrid } } : {}),
// the heal-mode attack: the most injured ally of the skill range (acquireTargets, heal profile)
attack: { dmgType: 'heal', heal: { mode: 'single' }, healScale: num(bb.heal_scale, 1), projectile: 'none' },
onHit({ battle, target }) { if (target) battle.fx('heal', { x: target.x, y: target.y, id: target.id }); },
},
talents: [{ install(battle, unit) {
// the proc is rolled per attack hit; the stun only follows a hit that landed (not dodged / cancelled)
const procs = new WeakSet();
onHitBy(battle, unit, ({ target, dmg }) => {
if (!isMainHit(dmg) || !(num(t.prob) > 0) || !battle.rng.chance(num(t.prob))) return;
dmg.amount *= num(t.atk_scale, 1);
procs.add(dmg);
battle.fx('crit', { x: target.x, y: target.y, id: unit.id });
});
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg || !procs.has(ctx.dmg) || !ctx.target.alive) return;
procs.delete(ctx.dmg);
battle.applyStatus(ctx.target, 'stun', { duration: num(t.stun), source: unit });
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,15 @@
// server/sim/content/kits/ops/chess_char_1_11-skgoat.js — 地灵 (char_183_skgoat) kit, tier 1 (hidden).
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_11 地灵 (hidden) 攻击力强化·β型: ATK +atk. Elite 地质勘探: trait 停顿 +sluggish s (module trait 1.2 s included).
chess_char_1_11_a: (bb, chess) => {
const add = num(talentBb(chess, 0).sluggish);
const kit = { skill: { kind: 'duration', mods: { atkPct: num(bb.atk) } } };
if (add > 0) kit.trait = { onHitStatus: { key: 'sluggish', duration: num(traitBb(chess).sluggish, 0.8) + add } };
return kit;
},
};
@@ -0,0 +1,34 @@
// server/sim/content/kits/ops/chess_char_1_12-estell.js — 艾丝黛尔 (char_127_estell) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { absoluteRangeKeys } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
import { num, talentBb, talentGrid, traitBb, up, cheb, onHitOn, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_12 艾丝黛尔 舍身突击 (hurt SP): ATK +atk, cannot be healed by others. 自愈能力: an enemy dying within the talent
// grid (8 surrounding tiles) heals her hp_ratio × max HP. Elite module (CEN-Y): above hp_ratio HP, physical damage
// taken −damage_resistance.
// Alternate S1 攻击力强化·β型: ATK +atk (she stays a heal target).
chess_char_1_12_a: (bb, chess) => {
const t = talentBb(chess, 0);
const grid = talentGrid(chess, 0);
const tb = traitBb(chess);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, flags: { noHeal: true } },
skills: { 'skcom_atk_up[2]': { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, 'skcom_atk_up[2]').atk) } } },
talents: [{ install(battle, unit) {
battle.on('death', (ctx) => {
const e = ctx.unit;
if (e.side !== 'enemy' || ctx.reason !== 'killed' || !up(unit)) return;
const near = grid ? bodyInKeys(e, absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0)) : cheb(unit, e) <= 1;
if (near) battle.heal(unit, unit, unit.s.maxHp * num(t.hp_ratio), { self: true, tags: ['talent'] });
}, { owner: unit });
if (tb.damage_resistance != null) {
onHitOn(battle, unit, ({ dmg }) => { if (dmg.type === 'phys' && unit.hpRatio > num(tb.hp_ratio)) dmg.mul *= 1 - num(tb.damage_resistance); });
}
} }],
};
},
};
@@ -0,0 +1,64 @@
// server/sim/content/kits/ops/chess_char_1_13-podego.js — 波登可 (char_258_podego) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import {
num, talentBb, moduleBb, up, enemiesInGrid, enemyInRange, installAura, spTimeBonus, makeZone, skillBbOf,
} from '../shared/tier1.js';
// =================================================================================================================
// tier-local constants that exist nowhere in data
/** Radius (tiles) of 波登可's spore cloud. No blackboard key; PRTS 波登可 备注 "孢子群范围半径为0.9,可对空". */
const SPORE_RADIUS = 0.9;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_13 波登可 孢子扩散: a vial bursts on the current target into a projectile_delay_time s spore cloud (radius 0.9,
// PRTS 备注, 可对空): enemies inside are 停顿 and lose their abilities (silence) and take atk_scale × ATK arts per second.
// 园丁: every 【辅助】 operator on the field ATK +atk. Elite module (DEC-X): +sp_recovery_per_sec SP/s with an enemy in range.
// Alternate S1 花香疗法: ATK +atk, normal attacks heal the most injured ally in range instead (ATK per attack). A heal
// skill: DEFAULT fires it with an injured ally inside her initial range (research 03 §1.4) — also checked every tick,
// since she is only "about to attack" with an enemy in range.
chess_char_1_13_a: (bb, chess) => {
const t = talentBb(chess, 0);
const spBonus = num(moduleBb(chess).sp_recovery_per_sec);
const dur = num(bb.projectile_delay_time, 5);
const S1 = 'skchr_podego_1';
return {
skills: {
[S1]: { kind: 'duration', heal: true, mods: { atkPct: num(skillBbOf(chess, S1).atk) }, attack: { dmgType: 'heal', heal: { mode: 'single' } } },
},
install(battle, unit) {
if (unit.skill?.id !== S1) return;
battle.on('tick', () => {
const sk = unit.skill;
if (!up(unit) || !unit.canAct || !sk.ready || sk.active || sk.opCooling || unit.s.flags.silence) return;
if (battle.injuredAlliesInKeys(unit.baseRangeKeys || unit.rangeKeys, unit).length) sk.activate('DEFAULT');
}, { owner: unit });
},
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const tgt = enemiesInGrid(battle, unit, null, { n: 1 })[0];
const x = tgt ? tgt.x : unit.x + unit.fwd[1], y = tgt ? tgt.y : unit.y + unit.fwd[0];
const atk = unit.s.atk;
makeZone(battle, unit, {
x, y, radius: SPORE_RADIUS, duration: dur, skill: 'spores', onPulse(b) {
for (const e of b.foesInRadius(x, y, SPORE_RADIUS)) {
if (e.s.flags.untargetable) continue;
b.applyStatus(e, 'sluggish', { duration: 1.05, source: unit });
b.applyStatus(e, 'silence', { duration: 1.05, source: unit });
b.dealDamage(unit, e, { amount: atk * num(bb.atk_scale), type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'zone'] });
}
},
});
},
},
talents: [{ install(battle, unit) {
const v = num(t.atk);
if (v) installAura(battle, unit, { key: 'talent:podego', value: v, select: (a) => a.kind === 'op' && a.def.profession === 'SUPPORT', mods: { atkPct: v } });
spTimeBonus(battle, unit, spBonus, () => enemyInRange(battle, unit));
} }],
};
},
};
@@ -0,0 +1,22 @@
// server/sim/content/kits/ops/chess_char_1_14-greyy.js — 格雷伊 (char_253_greyy) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_14 格雷伊 静电释放: ASPD +attack_speed, the talent 停顿 lasts talent_scale × longer.
// 静电场: attacks cause `sluggish` s of 停顿 to the attacked target.
// Alternate S1 战术咏唱·β型: ASPD +attack_speed (the talent 停顿 keeps its length). Elite module SPC-Y: cost only.
chess_char_1_14_a: (bb, chess) => {
const sl = num(talentBb(chess, 0).sluggish);
return {
skills: { 'skcom_magic_rage[2]': { kind: 'duration', mods: { aspd: num(skillBbOf(chess, 'skcom_magic_rage[2]').attack_speed) } } },
trait: sl > 0 ? { onHitStatus: { key: 'sluggish', duration: sl } } : undefined,
skill: {
kind: 'duration', mods: { aspd: num(bb.attack_speed) },
attack: sl > 0 ? { onHitStatus: { key: 'sluggish', duration: sl * num(bb.talent_scale, 1) } } : {},
},
};
},
};
@@ -0,0 +1,54 @@
// server/sim/content/kits/ops/chess_char_1_15-pithst.js — 盟约·辅助干员 (char_616_pithst) kit, tier 1 (hidden).
// Registered with the tier-6 kits, whose conventions are in ../shared/tier6.js; kit contract and rules: ../README.md.
import { hasHp } from '../../../damage.js';
import { num, tbb, isElite, elementDmg } from '../shared/tier6.js';
// ------------------------------------------------------------------------------------------------------------------
// 盟约·辅助干员 chess_char_1_15 (巫役) — S1 战术咏唱·双型; 迭代元素
/** 迭代元素's elements in the order they are applied: 神经损伤 "(优先)", then 灼燃损伤, then 凋亡损伤. */
export const PITHST_ELEMENTS = Object.freeze(['neural', 'burn', 'apoptosis']);
/**
* 迭代元素 "攻击同时附带18%攻击力的神经损伤(优先)、灼燃损伤、凋亡损伤;优先攻击未处于损伤爆发的目标":
* * timing = every damage she deals (PRTS 盟约·辅助干员 corrects "攻击同时" to "造成伤害时"): HP damage > 0 — a dodged
* or fully absorbed hit deals none; her own element fills and the bursts they cause never re-trigger it; a killing
* blow attaches nothing (the target is dead — no burst on the corpse);
* * each such damage attaches ep_damage_ratio × ATK of ALL THREE elements, 神经 first, then 灼燃, then 凋亡 (user
* playtest #5 #3: the kit used to pick one element "not bursting" — but a burst's 爆发冷却 locks every gauge of the
* unit, so it only ever filled 神经). PRTS 元素: when one unit applies several elements that would each fill their
* gauge, the element applied first bursts — from her alone 神经 bursts (the "(优先)"), while her 灼燃 / 凋亡 add to
* the gauges the team builds (余, 塑心 …). [ASSUMED: simultaneous application — the reading of the official text]
* * elite module RIT-X "对精英和领袖敌人造成的元素损伤提升18%" = ep_damage_ratio_boss (0.2124) vs ELITE / BOSS enemies:
* a talent ratio, not an "元素损伤提升" (PRTS 备注), so it changes nothing else she deals (e.g. a 灼燃维式重锤).
* The target pick prefers enemies not in a burst (`priority: 'notBurst'`).
*/
function pithst(bb, chess, def) {
const t0 = tbb(def, 0);
const ratio = num(t0.ep_damage_ratio);
const ratioBoss = num(t0.ep_damage_ratio_boss, ratio);
return {
skill: {
kind: 'duration',
mods: { aspd: num(bb.attack_speed) },
targeting: { maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 1))) },
},
trait: { priority: 'notBurst' },
install(battle, unit) {
if (!(ratio > 0)) return;
battle.on('damaged', (ctx) => {
const t = ctx.target, d = ctx.dmg;
// hasHp: a killing blow attaches nothing (the hook runs before battle.kill, see hasHp)
if (ctx.source !== unit || !t || t.side !== 'enemy' || !hasHp(t) || !(ctx.amount > 0)) return;
if (ctx.type === 'element' || ctx.type === 'elemental' || (d && d.tags && d.tags.includes('burst'))) return;
const amount = unit.s.atk * (isElite(t) ? ratioBoss : ratio);
for (const el of PITHST_ELEMENTS) elementDmg(battle, unit, t, el, amount, ['talent', 'pithst']);
}, { owner: unit });
},
};
}
export default {
chess_char_1_15_a: pithst,
};
@@ -0,0 +1,65 @@
// server/sim/content/kits/ops/chess_char_1_16-tinman.js — 锡人 (char_4151_tinman) kit, tier 1 (hidden).
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, moduleBb, enemiesInGrid, once, spTimeBonus, makeZone, instantKind } from '../shared/tier1.js';
// =================================================================================================================
// kit builders shared by two chess ids
/**
* 锡人 “大拉里” (chess_char_1_16 hidden and chess_char_2_19): throw an alchemy unit at the current target —
* for projectile_delay_time s, ground enemies within projectile_range tiles take atk_scale × ATK arts per second,
* allies there recover hp_recovery_per_sec_ratio × ATK per second. Elite: 凋敝魂灵 (skill@damage_scale: damage-over-
* time taken by ground enemies inside a unit ×1.2) and the module's +sp_recovery_per_sec SP while a unit exists.
*/
export function tinmanKit(bb, chess, def) {
const dur = num(bb.projectile_delay_time, 10);
const radius = num(bb.projectile_range, 1.5);
const dmgScale = num(bb.atk_scale);
const healRatio = num(bb.hp_recovery_per_sec_ratio);
const wither = num(talentBb(chess, 1)['skill@damage_scale'], 1);
const spBonus = num(moduleBb(chess).sp_recovery_per_sec);
const witherKey = 'tinman:wither';
return {
skill: {
kind: instantKind(def),
onStart({ battle, unit }) {
const tgt = enemiesInGrid(battle, unit, null, { n: 1, groundOnly: true })[0] ?? enemiesInGrid(battle, unit, null, { n: 1 })[0];
const x = tgt ? tgt.x : unit.x + unit.fwd[1], y = tgt ? tgt.y : unit.y + unit.fwd[0];
const atk = unit.s.atk;
unit.mem.tinZones = (unit.mem.tinZones ?? 0) + 1;
makeZone(battle, unit, {
x, y, radius, duration: dur, skill: 'tinman', onPulse(b) {
for (const e of b.foesInRadius(x, y, radius)) {
if (e.isFlying || e.s.flags.untargetable) continue;
if (wither > 1) b.addBuff(e, { key: witherKey, duration: 1.05, data: { mul: wither }, source: unit });
b.dealDamage(unit, e, { amount: atk * dmgScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'zone'] });
}
if (healRatio > 0) for (const a of b.alliesInRadius(x, y, radius, null)) if (a.hp < a.s.maxHp) b.heal(unit, a, atk * healRatio, { tags: ['zone'] });
},
onEnd() { unit.mem.tinZones = Math.max(0, (unit.mem.tinZones ?? 1) - 1); },
});
},
},
talents: [{ install(battle, unit) {
if (wither > 1) {
// "持续伤害" = damage over time only: 'dot'-tagged ticks and the per-second ticks of a necrosis / apoptosis
// burst — never instantaneous bursts (burn/neural bursts, skill/summon bursts are also tagged 'burst')
once(battle, witherKey, () => battle.on('hit', (ctx) => {
if (!ctx.target || ctx.target.side !== 'enemy') return;
const tags = ctx.dmg.tags || [];
if (!(tags.includes('dot') || tags.includes('necrosis') || tags.includes('apoptosis'))) return;
const w = ctx.target.findBuff(witherKey);
if (w) ctx.dmg.amount *= w.data?.mul ?? 1;
}));
}
spTimeBonus(battle, unit, spBonus, () => (unit.mem.tinZones ?? 0) > 0);
} }],
};
}
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_16 锡人 (hidden tier-1 entry of chess_char_2_19): see tinmanKit.
chess_char_1_16_a: tinmanKit,
};
@@ -0,0 +1,71 @@
// server/sim/content/kits/ops/chess_char_1_17-indigo.js — 深靛 (char_469_indigo) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import { num, talentBb, skillRec, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_17 深靛 光影迷宫: attack interval ×base_attack_time, talent chance ×talent_scale, bound enemies in range take
// indigo_s_2[damage].atk_scale × ATK arts every indigo_s_2[damage].interval s.
// 柔光缚目: attacks bind the target `duration` s with `prob`; bound enemies are never chosen as targets.
// Alternate S1 灯塔守卫者: for `duration` s the skill range (4-1), attack interval ×(1 + base_attack_time) (PRTS:
// "大幅度缩短(-80%)" ⇒ ×0.2 — a flat −0.8 s on 3 s would be less than S2's "略微缩短 ×0.7"), every attack hits for
// attack@atk_scale × ATK arts. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic).
chess_char_1_17_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const dScale = num(bb['indigo_s_2[damage].atk_scale']);
const dIv = num(bb['indigo_s_2[damage].interval'], 0.5);
const bound = (e) => !!e.s.flags.bind;
const r1 = skillRec(chess, 'skchr_indigo_1');
const b1 = r1?.bb ?? {};
return {
skills: {
skchr_indigo_1: {
kind: 'duration', mods: { batPct: Math.max(-0.95, Math.min(0, num(b1.base_attack_time))) },
targeting: { rangeGrid: r1?.rangeGrid ?? null },
attack: { atkScale: num(b1['attack@atk_scale'], 1) },
},
},
trait: {
// with only bound enemies in range (or blocked by her — always her targets, Battle.blockedTargets) she holds her
// fire (the mystic trait stores the energy meanwhile)
canAttack(battle, u) {
const ok = battle.enemiesInKeys(u.rangeKeys, u, u.profile).some((e) => !bound(e)) || battle.blockedTargets(u, u.profile).some((e) => !bound(e));
if (!ok) u.trait.hadTarget = false;
return ok;
},
afterHit(battle, u, target) {
if (!target || !target.alive || target.side !== 'enemy') return;
const p = num(t.prob) * (u.skill?.active ? num(bb.talent_scale, 1) : 1);
if (p > 0 && battle.rng.chance(Math.min(1, p))) battle.applyStatus(target, 'bind', { duration: num(t.duration), source: u });
},
},
skill: {
kind: 'duration', mods: { batPct: batMod(bb.base_attack_time, chess, def?.skill?.description ?? chess?.skill?.desc) },
onStart({ unit }) { unit.mem.indigoAcc = 0; },
onTick({ battle, unit, dt }) {
if (!(dScale > 0) || !(dIv > 0)) return;
unit.mem.indigoAcc = (unit.mem.indigoAcc ?? 0) + dt;
while (unit.mem.indigoAcc >= dIv - 1e-9) {
unit.mem.indigoAcc -= dIv;
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) {
if (bound(e)) battle.dealDamage(unit, e, { amount: unit.s.atk * dScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot'] });
}
}
},
},
talents: [{ install(battle, unit) {
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !ctx.targets.some(bound)) return;
const prof = ctx.profile || unit.profile;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, prof);
for (const e of battle.blockedTargets(unit, prof)) if (!cands.includes(e)) cands.push(e);
for (let i = cands.length - 1; i >= 0; i--) if (bound(cands[i])) cands.splice(i, 1);
sortEnemyTargets(battle, unit, cands, prof.priority);
ctx.targets = cands.slice(0, Math.max(1, ctx.targets.length));
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,57 @@
// server/sim/content/kits/ops/chess_char_1_18-utage.js — 宴 (char_337_utage) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, up, onHitBy, onHitOn, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_18 宴 落地斩·破门 (passive): at each deployment lose hp_ratio of current HP, then for `duration` s ATK +atk and
// attacks deal arts damage. 认真模式: 坚忍 ASPD up to +min_attack_speed, reached at min_hp_ratio HP (linear).
// Elite (module talent): below hp_ratio HP, 庇护 −damage_resistance physical/arts damage taken.
// Alternate S1 分神: stops attacking, block count 0 (her blocked enemies walk on), DEF +def, recovers
// hp_recovery_per_sec_by_max_hp_ratio × max HP per second (the 生命回复速度 attribute: works under her 武者 no-heal).
chess_char_1_18_a: (bb, chess) => {
const t = talentBb(chess, 0);
const buffKey = 'utage:s2';
const s1 = skillBbOf(chess, 'skchr_utage_1');
return {
skills: {
skchr_utage_1: {
kind: 'duration',
mods: { defPct: num(s1.def), hpRegenRatio: num(s1.hp_recovery_per_sec_by_max_hp_ratio), blockCnt: -99 },
attack: { noAttack: true },
onStart({ battle, unit }) { battle.releaseBlocked(unit); },
},
},
skill: {
kind: 'passive',
onStart({ battle, unit }) {
const loss = unit.hp * num(bb.hp_ratio);
if (loss > 0 && unit.hp - loss >= 1) battle.loseHp(unit, loss, { source: unit });
battle.addBuff(unit, { key: buffKey, duration: num(bb.duration), mods: { atkPct: num(bb.atk) }, tags: ['skill'], visible: true });
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: 'breach' });
},
},
talents: [{ install(battle, unit) {
onHitBy(battle, unit, ({ dmg }) => { if (dmg.isAttack && dmg.type === 'phys' && unit.findBuff(buffKey)) dmg.type = 'arts'; });
const maxAs = num(t.min_attack_speed), minHp = num(t.min_hp_ratio);
if (maxAs > 0 && minHp < 1) {
battle.on('tick', () => {
if (!up(unit)) return;
const v = maxAs * Math.max(0, Math.min(1, (1 - unit.hpRatio) / (1 - minHp)));
const cur = unit.findBuff('utage:serious');
if (cur && Math.abs((cur.data?.v ?? 0) - v) < 0.5) return;
if (v < 0.5) { if (cur) battle.removeBuff(unit, 'utage:serious'); return; }
battle.addBuff(unit, { key: 'utage:serious', mods: { aspd: v }, data: { v }, tags: ['talent'] });
}, { owner: unit });
}
// 庇护 (ba.protect): "受到的物理和法术伤害降低相应比例" — physical and arts damage only
if (t.damage_resistance != null && t.hp_ratio != null) {
onHitOn(battle, unit, ({ dmg }) => {
if ((dmg.type === 'phys' || dmg.type === 'arts') && unit.hpRatio < num(t.hp_ratio)) dmg.mul *= 1 - num(t.damage_resistance);
});
}
} }],
};
},
};
@@ -0,0 +1,67 @@
// server/sim/content/kits/ops/chess_char_1_19-wildmn.js — 野鬃 (char_496_wildmn) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, skillRec } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_19 野鬃 夹枪冲锋: wider range (skill grid), ATK +atk, "攻击会把目标往攻击方向中等力度地推开" (attack@force 1 = 中力) by
// the official 力度 − 重量 distance (Battle.push: weight 0 → 2.14 tiles, 1 → 1.7, 2 → 0.44, 3 → 0.12, ≥ 4 → none; user
// playtest #6 item 14). A directional push (方向力, PRTS 推与拉): the client's S2 attack ability (charpack
// char_496_wildmn, anim Skill_2) carries buff wildmn_s_2[force] of template knockback[dir] — buff_template_data:
// Knockback {_useSourceDirection: true, _decreaseForceLevelWhenNotInDirection: 2} — i.e. along her deploy direction,
// radial at 受力等级 −2 for a target > 45° off it or < 0.25 tile away (特殊修正); the radial pushes are
// knockback[relative] (琳琅诗怀雅 S3, 山 S3, 莫斯提马 S3). Her text uses the 推击手 wording "往攻击方向". So an enemy she
// grabs behind her centre (the hand-over after a push freed her block) is not thrown 1.7 tiles towards the objective
// (player feedback D2, "往攻击方向相反方向推"; it was a radial push before).
// 一致向前: after deploying (normal flag 0: first deployment only; elite flag 1: every deployment) every undeployed
// 【近卫】 operator of the player costs `value` less DP to deploy (≤ max_stack_cnt per operator until it deploys).
// Alternate S1 骑枪刺击 (PASSIVE, ON_DEPLOY): for `duration` s after every deployment ASPD +attack_speed.
// Elite module CHG-X (+2 DP per kill) is the charger profile of the module trait (TUNE.charger).
chess_char_1_19_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const force = num(bb['attack@force'], 1);
const r1 = skillRec(chess, 'skchr_wildmn_1');
return {
skills: {
skchr_wildmn_1: {
kind: 'passive',
onStart({ battle, unit }) {
const d = num(r1?.duration);
if (d > 0) battle.addBuff(unit, { key: 'wildmn:s1', duration: d, mods: { aspd: num(r1?.bb?.attack_speed) }, tags: ['skill'], visible: true });
},
},
},
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null },
attack: {
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.side !== 'enemy') return;
battle.push(target, force, { from: unit, dir: { x: unit.fwd[1], y: unit.fwd[0] } });
},
},
},
talents: [{ install(battle, unit) {
const cut = Math.abs(num(t.value, -1)), cap = num(t.max_stack_cnt, 1), every = num(t.flag) === 1;
let deploys = 0;
battle.on('deploy', ({ unit: u }) => {
if (u === unit) {
deploys++;
if (!every && deploys > 1) return;
for (const a of battle.allyUnits) {
if (a === unit || a.kind !== 'op' || a.ownerId !== unit.ownerId || a.alive || a.removed || a.def.profession !== 'WARRIOR') continue;
const done = a.mem.wildmnCut ?? 0;
const d = Math.min(cut, cap - done, a.base.cost);
if (d > 0) { a.base.cost -= d; a.mem.wildmnCut = done + d; }
}
} else if (u.mem.wildmnCut > 0) {
// the discount is consumed by this deployment
u.base.cost += u.mem.wildmnCut;
u.mem.wildmnCut = 0;
}
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,81 @@
// server/sim/content/kits/ops/chess_char_1_20-liskam.js — 雷蛇 (char_107_liskam) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import {
num, talentBb, talentGrid, moduleOn, up, hurtSpDamage, giveSp, onDamagedOn, alliesInGridOf, statBuff, installReveal,
skillBbOf,
} from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_20 雷蛇 反击电弧 (hurt SP): attack interval ×(1 + base_attack_time) (PRTS "攻击间隔增大(+70%)": a RATIO for this skill,
// 1.2 → 2.04 s — not +0.7 s), ATK +atk, arts attacks on up to attack@max_target enemies,
// attack@buff_prob to stun attack@stun s; afterwards 雷蛇 is stunned `stun` s.
// 技能策略 → DEFAULT (PR #12; DESIGN §21.29): 反击电弧 is an offensive skill (ATK +125 %, arts hits on up to 3 enemies,
// stun); the official 下半 TANK row (TAKE_DAMAGE for every MANUAL 重装 skill) made it — and 深巡's S2 — wait for a hit
// (GitHub issue #4). The owner's deliberate deviation from that row (2026-10-03, community feedback) gives both
// 哨戒铁卫 S2s the basic strategy; the data says DEFAULT too (rawRule keeps the official TAKE_DAMAGE).
// 战术防御: when attacked, +sp SP to herself and to one random ally in the talent grid. Elite 雷抗: RES +magic_resistance.
// Elite module (SPT-X): stealth of enemies inside the range is cancelled.
// Alternate S1 充能防御 (AUTO, hurt SP, "技能自动开启" — an AUTO skill takes no 技能策略: SP_FULL, so the hit that fills SP
// sets it off): PRTS "技能拥有8s的持续时间": for bb.duration s DEF +def, and the next damage instance taken is blocked
// (cancelled — a blocked hit "导致第一天赋无法触发": no 战术防御 SP, no hurt SP).
chess_char_1_20_a: (bb, chess) => {
const t = talentBb(chess, 0);
const grid = talentGrid(chess, 0);
const S1 = 'skchr_liskam_1';
const b1 = skillBbOf(chess, S1);
const blockKey = 'liskam:block';
return {
skills: {
[S1]: {
kind: 'duration', duration: num(b1.duration, 8), mods: { defPct: num(b1.def) }, trigger: 'SP_FULL',
onStart({ battle, unit, skill }) {
battle.addBuff(unit, { key: blockKey, duration: skill.duration + 0.05, tags: ['skill'], visible: true });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { battle.removeBuff(unit, blockKey); },
},
},
install(battle, unit) {
if (moduleOn(chess) && chess.isGolden) installReveal(battle, unit);
if (unit.skill?.id !== S1) return;
battle.on('hit', (ctx) => {
if (ctx.target !== unit || ctx.dmg.cancel || !(ctx.dmg.amount > 0) || !unit.findBuff(blockKey)) return;
ctx.dmg.cancel = true;
battle.removeBuff(unit, blockKey);
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit, priority: 50 });
},
skill: {
trigger: 'DEFAULT',
kind: 'duration', mods: { atkPct: num(bb.atk), batPct: Math.max(0, num(bb.base_attack_time)) },
targeting: { maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 1))) },
attack: {
dmgType: 'arts',
onHit({ battle, unit, target }) {
if (target && target.alive && battle.rng.chance(num(bb['attack@buff_prob']))) battle.applyStatus(target, 'stun', { duration: num(bb['attack@stun']), source: unit });
},
},
onEnd({ battle, unit, reason }) {
if (reason !== 'death' && unit.alive && num(bb.stun) > 0) battle.applyStatus(unit, 'stun', { duration: num(bb.stun), source: unit });
},
},
talents: [{ install(battle, unit) {
const sp = num(t.sp);
if (sp > 0) {
// PRTS 备注 "仅伤害量不为0且能够触发受击回复的伤害才能触发此天赋": any such damage, not only an enemy attack
// (a zone tick, the 源石溶剂 drain — player report D1 audit)
onDamagedOn(battle, unit, (ctx) => {
if (!hurtSpDamage(ctx) || !up(unit)) return;
giveSp(unit, sp);
const mates = alliesInGridOf(battle, unit, grid ?? [[1, 0], [0, -1], [0, 1], [-1, 0]]).filter((a) => a !== unit && a.skill && !a.skill.noSkill);
const m = battle.rng.pick(mates);
if (m) giveSp(m, sp);
});
}
statBuff(battle, unit, 'talent:liskam', { resFlat: num(talentBb(chess, 1).magic_resistance) });
} }],
};
},
};
@@ -0,0 +1,21 @@
// server/sim/content/kits/ops/chess_char_2_01-excu.js — 送葬人 (char_279_excu) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, statBuff, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_01 送葬人 最终旅程: normal attacks become double hits, base attack time +base_attack_time s (−0.5 on 2.3 s
// "少量缩短"). 终结改装: ignore def_penetrate_fixed DEF. (Trait: every enemy in range, ×atk_scale on its front row —
// reaperrange profile.)
//
// S1 铳口收束 (alt): ATK +atk; "攻击时对攻击范围内的所有敌人应用特性加成" — every enemy in range takes the trait's
// front-row multiplier (profile frontScale: trait atk_scale 1.5, elite module 1.6, elite without module 1.5).
chess_char_2_01_a: (bb, chess) => ({
skill: { kind: 'duration', mods: { batPct: batMod(bb.base_attack_time, chess) }, attack: { hits: 2 } },
skills: {
skchr_excu_1: { kind: 'duration', mods: { atkPct: num(bb.atk) }, attack: { dmgMul: (battle, u) => num(u.profile?.frontScale, 1.5) } },
},
talents: [{ install(battle, unit) { statBuff(battle, unit, 'talent:excu', { defIgnoreFlat: num(talentBb(chess, 0).def_penetrate_fixed) }); } }],
}),
};
@@ -0,0 +1,36 @@
// server/sim/content/kits/ops/chess_char_2_02-silent.js — 赫默 (char_108_silent) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { releaseSkillSummon } from '../../tokens.js';
import { num, talentBb, traitBb, installAura } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_02 赫默 医疗无人机 (AUTO, heal trigger): "获得一个医疗无人机 / 最多可库存1个无人机;无人机投入战场后治疗周围友军,
// 10秒后自动销毁" — the 医疗探机 is a hand piece the player places (user playtest #6; PRTS 卫戍协议/帮助): each cast gives
// one (stock ≤ cnt) and the placed piece takes the field on its own tile (tokens.js releaseSkillSummon: not at the
// battle start — user playtest #4; not placed ⇒ no drone); it heals around itself and self-destructs after 10 s (token
// kit). 强化注射: every 【医疗】 operator on the field ASPD +attack_speed. Elite module (PHY-Y): heals on ground units
// ×heal_scale (module 'none': no bonus).
// S1 治疗强化·γ型 (alt): ATK +atk for its duration (no drone).
chess_char_2_02_a: (bb, chess) => {
const tokId = chess?.skill?.overrideTokenKey ?? (chess?.tokens ?? [])[0] ?? 'token_10000_silent_healrb';
const cnt = Math.max(1, Math.floor(num(bb.cnt, 1)));
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: {
kind: 'instant', heal: true,
onStart({ battle, unit }) { releaseSkillSummon(battle, unit, tokId, { cap: cnt }); },
},
skills: { 'skcom_heal_up[3]': { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
const v = num(t.attack_speed);
if (v) installAura(battle, unit, { key: 'talent:silent', value: v, select: (a) => a.kind === 'op' && a.def.profession === 'MEDIC', mods: { aspd: v } });
if (num(tb.heal_scale, 1) !== 1) {
battle.on('heal', (ctx) => { if (ctx.source === unit && ctx.target.ground) ctx.amount *= num(tb.heal_scale, 1); }, { owner: unit });
}
} }],
};
},
};
@@ -0,0 +1,39 @@
// server/sim/content/kits/ops/chess_char_2_03-slchan.js — 崖心 (char_173_slchan) kit, tier 2 (hidden).
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, enemiesInGrid, toggleBuff } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_03 崖心 (hidden) 束缚链: up to max_target enemies in the large front grid are dragged (force 1 = 中力; Battle.pullToFront:
// the official 力度 − 重量 pull — weight ≤ 1 all the way in front of her, 2 a third of the way, 3 barely, ≥ 4 not at all),
// take atk_scale × ATK true damage and are stunned `stun` s. 雪境猎手: ATK/DEF +atk/+def while not blocking.
// Elite module (HOK-X, trait value/dist): dragged enemies take `value` arts per `dist` tiles travelled.
chess_char_2_03_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const force = num(bb.force, 1);
return {
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const foes = enemiesInGrid(battle, unit, def?.skill?.rangeGrid ?? null, { n: Math.max(1, Math.floor(num(bb.max_target, 1))) });
for (const e of foes) {
const sx = e.x, sy = e.y;
const moved = battle.pullToFront(e, unit, force);
battle.fx('pull', { x: e.x, y: e.y, id: e.id, fromX: sx, fromY: sy });
if (moved > 0 && num(tb.value) > 0) {
battle.dealDamage(unit, e, { amount: num(tb.value) * moved / Math.max(0.01, num(tb.dist, 1)), type: 'arts', isSkill: true, canDodge: false, tags: ['drag'] });
}
if (!e.alive) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'true', isSkill: true, tags: ['skill'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun), source: unit });
}
},
},
talents: [{ install(battle, unit) {
if (num(t.atk) || num(t.def)) toggleBuff(battle, unit, 'slchan:hunter', () => unit.blocking.length === 0, { atkPct: num(t.atk), defPct: num(t.def) });
} }],
};
},
};
@@ -0,0 +1,49 @@
// server/sim/content/kits/ops/chess_char_2_04-grabds.js — 小满 (char_4122_grabds) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import {
num, talentBb, moduleBb, traitBb, enemiesInGrid, enemyInRange, statBuff, spTimeBonus, instantKind,
} from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_04 小满 乡音沉沉: stops attacking and puts max_target enemies in range to sleep `sleep` s; afterwards ASPD
// +attack_speed and attacks max_target enemies. 好好听话: ASPD +attack_speed; vs 【野生动物】 (tag `infection`,
// "野生的被感染生物") the trait 停顿 lasts +sluggish_addition s. Elite module (DEC-X): +sp_recovery_per_sec SP/s with
// an enemy in range.
// S1 竹笛飞声 (alt, 2 charges): the next attack deals atk_scale × ATK arts and hits one more enemy (+1 target).
chess_char_2_04_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const n = Math.max(1, Math.floor(num(bb.max_target, 1)));
const sleep = num(bb.sleep);
return {
trait: {
onHitStatus: null,
canAttack: (battle, u) => !(u.skill?.active && battle.time < (u.mem.grainQuietUntil ?? -1)),
afterHit(battle, u, target) {
if (!target || !target.alive || target.side !== 'enemy') return;
const beast = (target.def?.tags || []).includes('infection');
battle.applyStatus(target, 'sluggish', { duration: num(tb.sluggish, 0.8) + (beast ? num(t.sluggish_addition) : 0), source: u });
},
},
skill: {
kind: 'duration', mods: { aspd: num(bb.attack_speed) },
targeting: { maxTargets: n, rangeGrid: def?.skill?.rangeGrid ?? null },
onStart({ battle, unit }) {
unit.mem.grainQuietUntil = battle.time + sleep;
for (const e of enemiesInGrid(battle, unit, def?.skill?.rangeGrid ?? null, { n })) {
if (battle.applyStatus(e, 'sleep', { duration: sleep, source: unit })) battle.fx('sleep', { x: e.x, y: e.y, id: e.id });
}
},
},
skills: {
skchr_grabds_1: { kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 1), dmgType: 'arts' }, mods: { maxTargets: 1 } },
},
talents: [{ install(battle, unit) {
statBuff(battle, unit, 'talent:grabds', { aspd: num(t.attack_speed) });
spTimeBonus(battle, unit, num(moduleBb(chess).sp_recovery_per_sec), () => enemyInRange(battle, unit));
} }],
};
},
};
@@ -0,0 +1,56 @@
// server/sim/content/kits/ops/chess_char_2_05-harold.js — 哈洛德 (char_4114_harold) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { COLS, ELEMENT_GAUGE_MAX } from '../../../constants.js';
import { num, talentBb, up } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
/** Largest element gauge of a unit (every element — 侵蚀 included: the bosses' attacks fill it on operators). */
const elemLoad = (a) => Math.max(a.elem.burn, a.elem.neural, a.elem.necrosis, a.elem.apoptosis, a.elem.erosion ?? 0);
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_05 哈洛德 重症优先: ASPD +attack_speed, heals the ally with the heaviest element damage first; on targets whose
// gauge is over half, the element recovery is trait_scale × (the wandermedic trait: ep_heal_ratio × ATK).
// 我即军营: allies in range whose gauge is over half take −ep_damage_resistance 元素损伤 — checked on the element hit
// itself (`elementHit`, a 元素损伤 multiplier: PRTS 元素 "…后续可应用元素损伤倍率提升/降低等效果"; not `elemTakenMul`,
// which is 元素伤害 / 元素脆弱); several 哈洛德 do not stack — the strongest cut applies.
// S1 治疗强化·γ型 (alt): ATK +atk; the 重症优先 target order belongs to S2 only.
chess_char_2_05_a: (bb, chess) => {
const t = talentBb(chess, 0);
const s2 = onDefaultSkill(chess);
const half = ELEMENT_GAUGE_MAX / 2;
const ts = num(bb.trait_scale, 1);
return {
skill: {
kind: 'duration', heal: true, mods: { aspd: num(bb.attack_speed) },
onHit({ battle, unit, target }) {
if (!target || !unit.mem.haroldHalf?.has(target.id) || !(ts > 1)) return;
battle.reduceElement(target, unit.s.atk * (unit.profile.heal?.elementHealRatio ?? 0) * (ts - 1));
},
},
skills: { 'skcom_heal_up[3]': { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
if (s2) battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active) return;
const cands = battle.injuredAlliesInKeys(unit.rangeKeys, unit, true);
cands.sort((a, b) => elemLoad(b) - elemLoad(a) || a.hpRatio - b.hpRatio || a.deploySeq - b.deploySeq);
if (cands.length) ctx.targets = cands.slice(0, Math.max(1, ctx.targets.length));
unit.mem.haroldHalf = new Set(ctx.targets.filter((a) => elemLoad(a) > half).map((a) => a.id));
}, { owner: unit });
const r = Math.max(0, Math.min(1, num(t.ep_damage_resistance)));
if (r > 0) {
battle.on('elementHit', (ctx) => {
const a = ctx.target, d = ctx.dmg;
if (!up(unit) || !a || a.side !== 'ally' || !d || d.type !== 'element' || !(elemLoad(a) > half)) return;
if (!(unit.rangeKeySet || new Set(unit.rangeKeys || [])).has(a.tileR * COLS + a.tileC)) return;
const prev = d.haroldCut || 0; // the strongest 我即军营 of the hit only
if (r <= prev) return;
d.mul *= (1 - r) / (1 - prev);
d.haroldCut = r;
}, { owner: unit, priority: 20 });
}
} }],
};
},
};
@@ -0,0 +1,85 @@
// server/sim/content/kits/ops/chess_char_2_06-papyrs.js — 莎草 (char_4139_papyrs) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { bodyInKeys } from '../../../body.js';
import { num, talentBb, up, alliesInGridOf, instantKind, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_06 莎草 巧思乍现 (AUTO, 2 charges): the next heal is heal_scale × ATK (chain jumps included) and the talent
// barrier is shield_scale_skill × stronger. 博览古卷: every heal gives the target a barrier of attack@scale × ATK for
// attack@shield_duration s. (Chain falloff incl. the elite module comes from the chainhealer profile.)
// S2 临考发挥 (alt): on start lock the ally in range (not herself, no 禁疗; any profession, summons included) with the
// highest max HP; while it runs that ally is the only main heal target, base attack time +base_attack_time s (−1.1
// on 2.85), ATK +atk, chain jumps +attack@chain.extra_value. PRTS: "无法可选单位时无法开启技能" — with no lockable
// ally the skill never activates (the talent install guards the unit's skill.activate: no skillStart, no
// 萨尔贡 / 特质 "开启技能时" gains, SP kept); the skill stops at once when the locked ally leaves the field.
chess_char_2_06_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const sc = num(t['attack@scale']), sd = num(t['attack@shield_duration'], 8), boost = num(bb.shield_scale_skill, 1);
const extra = Math.max(0, Math.floor(num(bb['attack@chain.extra_value'])));
const lockOf = (battle, unit) => alliesInGridOf(battle, unit)
.filter((a) => a !== unit && !a.s.flags.noHeal && !a.profile?.noHeal)
.sort((a, b) => b.s.maxHp - a.s.maxHp || a.deploySeq - b.deploySeq)[0] ?? null;
const release = (battle, unit) => {
if (unit.mem.papyrsHeal) { unit.profile.heal = unit.mem.papyrsHeal; unit.mem.papyrsHeal = null; }
if (unit.mem.papyrsHook) battle.off(unit.mem.papyrsHook);
unit.mem.papyrsHook = null;
unit.mem.papyrsLock = null;
unit.mem.papyrsAbort = false;
};
return {
skill: { kind: instantKind(def), heal: true, attack: { healScale: num(bb.heal_scale, 1) } },
skills: {
skchr_papyrs_2: {
kind: 'duration', heal: true, mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess) },
onStart({ battle, unit, skill }) {
release(battle, unit);
const lock = lockOf(battle, unit);
// (only reachable when a runtime bypasses the activate guard below, e.g. a copied skill: undo on the next tick)
if (!lock) { unit.mem.papyrsAbort = true; return; }
unit.mem.papyrsLock = lock;
const h = unit.profile.heal;
if (h && extra > 0) { unit.mem.papyrsHeal = h; unit.profile.heal = { ...h, count: Math.max(1, h.count || 3) + extra }; }
unit.mem.papyrsHook = battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !skill.active) return;
const L = unit.mem.papyrsLock;
if (L && up(L) && bodyInKeys(L, unit.rangeKeySet)) ctx.targets = [L];
}, { owner: unit, priority: 10 });
battle.fx('buff', { x: lock.x, y: lock.y, id: lock.id, kind: 'lock' });
},
onTick({ unit, skill }) {
if (unit.mem.papyrsAbort) {
// "无法可选单位时无法开启技能": undo the cast, keep the SP
unit.mem.papyrsAbort = false;
skill.end('noTarget');
skill.addCharge(1);
return;
}
const L = unit.mem.papyrsLock;
if (!L || !up(L)) skill.end('target');
},
onEnd({ battle, unit }) { release(battle, unit); },
},
},
talents: [{ install(battle, unit) {
const sk = unit.skill;
if (sk && sk.id === 'skchr_papyrs_2' && !Object.prototype.hasOwnProperty.call(sk, 'activate')) {
// "无法可选单位时无法开启技能": every activation path (DEFAULT heal trigger, carried skill, content) is refused
// while no ally can be locked — the charge stays, nothing starts (non-enumerable: never serialised)
const activate = sk.activate;
Object.defineProperty(sk, 'activate', {
configurable: true, writable: true, enumerable: false,
value(reason, opts) { return lockOf(battle, unit) ? activate.call(this, reason, opts) : false; },
});
}
if (!(sc > 0)) return;
battle.on('heal', (ctx) => {
if (ctx.source !== unit || ctx.target === unit && ctx.opts?.regen || !(ctx.amount > 0)) return;
const bySkill = !!(unit.skill?.active && unit.skill.pending);
battle.addBuff(ctx.target, { key: 'papyrs:shield', shield: unit.s.atk * sc * (bySkill ? boost : 1), duration: sd, source: unit, visible: true, tags: ['talent'] });
}, { owner: unit, priority: -10 });
} }],
};
},
};
@@ -0,0 +1,35 @@
// server/sim/content/kits/ops/chess_char_2_07-ghost.js — 幽灵鲨 (char_143_ghost) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, statBuff } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_07 幽灵鲨 肉斩骨断: ATK +atk and HP never drops below 1 while active; stunned `stun` s afterwards.
// Talent: max HP +max_hp (elite: + hp_recovery_per_sec_by_max_hp_ratio × max HP per s).
// Elite module (CEN-X, trait atk_scale): vs enemies it blocks ATK ×atk_scale.
// S1 攻击力强化·γ型 (alt): ATK +atk for its duration.
chess_char_2_07_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) {
if (unit.mem.undying) battle.off(unit.mem.undying);
unit.mem.undying = battle.on('fatal', (c) => { if (c.unit === unit && unit.skill?.active) c.prevented = true; }, { owner: unit, priority: 10 });
},
onEnd({ battle, unit, reason }) {
if (unit.mem.undying) battle.off(unit.mem.undying);
unit.mem.undying = null;
if (reason !== 'death' && unit.alive && num(bb.stun) > 0) battle.applyStatus(unit, 'stun', { duration: num(bb.stun), source: unit });
},
},
skills: { 'skcom_atk_up[3]': { kind: 'duration', mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
statBuff(battle, unit, 'talent:ghost', { hpPct: num(t.max_hp), hpRegenRatio: num(t.hp_recovery_per_sec_by_max_hp_ratio) });
if (num(tb.atk_scale, 1) !== 1) onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && target.blockedBy) dmg.amount *= num(tb.atk_scale, 1); });
} }],
};
},
};
@@ -0,0 +1,33 @@
// server/sim/content/kits/ops/chess_char_2_08-bubble.js — 泡泡 (char_381_bubble) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, byEnemyAttack, onDamagedOn, toggleBuff } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_08 泡泡 “挨打”: stops attacking; DEF +def, taunt +taunt_level, each attack received returns atk_scale × DEF phys
// damage to the attacker. 尖刺盾: attackers get ATK −atk for `duration` s. Elite module (PRO-X): DEF +def while blocking.
// S1 防御力强化·β型 (alt, TAKE_DAMAGE from data): DEF +def for its duration; the counter belongs to “挨打” only.
chess_char_2_08_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const s2 = onDefaultSkill(chess);
return {
skill: { kind: 'duration', mods: { defPct: num(bb.def), taunt: num(bb.taunt_level) }, attack: { noAttack: true } },
skills: { 'skcom_def_up[2]': { kind: 'duration', mods: { defPct: num(bb.def) } } },
talents: [{ install(battle, unit) {
onDamagedOn(battle, unit, (ctx) => {
if (!byEnemyAttack(ctx) || !unit.alive) return;
const src = ctx.source;
if (s2 && unit.skill?.active && src.alive) {
battle.dealDamage(unit, src, { amount: unit.s.def * num(bb.atk_scale), type: 'phys', isSkill: true, canDodge: false, tags: ['counter'] });
battle.fx('counter', { x: src.x, y: src.y, id: unit.id });
}
if (num(t.atk) < 0 && src.alive) battle.addBuff(src, { key: 'bubble:spike', duration: num(t.duration, 5), mods: { atkPct: num(t.atk) }, refresh: 'extend', source: unit });
});
if (num(tb.def)) toggleBuff(battle, unit, 'bubble:guard', () => unit.blocking.length > 0, { defPct: num(tb.def) });
} }],
};
},
};
@@ -0,0 +1,60 @@
// server/sim/content/kits/ops/chess_char_2_09-humus.js — 休谟斯 (char_491_humus) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_09 休谟斯 高效处理: block +block_cnt; above peak_1.hp_ratio HP ATK +peak_1.atk (精力充沛), above peak_2.hp_ratio
// +peak_2.atk. 再回收: healing beyond max HP becomes a barrier, at most max_hp_ratio × max HP.
// S1 固废切割 (alt, attack SP): the next attack hits at atk_scale × ATK (every enemy it reaches) and heals her `value`
// HP once (a self heal: her trait bars only healing by others; overflow feeds 再回收).
chess_char_2_09_a: (bb, chess) => {
const t = talentBb(chess, 0);
const peaks = [];
for (const [k, v] of Object.entries(bb)) {
const m = k.match(/\[(peak_\d+)\]\.peak_performance\.(atk|hp_ratio)$/);
if (!m) continue;
let p = peaks.find((x) => x.id === m[1]);
if (!p) peaks.push((p = { id: m[1], atk: 0, hp: 1 }));
if (m[2] === 'atk') p.atk = num(v); else p.hp = num(v, 1);
}
peaks.sort((a, b) => b.hp - a.hp);
const key = 'humus:peak';
const peak = ({ battle, unit }) => {
const lvl = peaks.find((p) => unit.hpRatio > p.hp)?.atk ?? 0;
const cur = unit.findBuff(key);
if ((cur?.data?.v ?? 0) === lvl) return;
if (lvl > 0) battle.addBuff(unit, { key, mods: { atkPct: lvl }, data: { v: lvl }, visible: true, tags: ['skill'] });
else battle.removeBuff(unit, key);
};
return {
skill: {
kind: 'duration', mods: { blockCnt: num(bb.block_cnt) },
onStart: peak, onTick: peak,
onEnd({ battle, unit }) { battle.removeBuff(unit, key); },
},
skills: {
skchr_humus_1: {
kind: 'instant', attack: { atkScale: num(bb.atk_scale, 1) },
onAttack({ battle, unit }) {
if (num(bb.value) > 0 && unit.alive) battle.heal(unit, unit, num(bb.value), { self: true, tags: ['skill'] });
},
},
},
talents: [{ install(battle, unit) {
const ratio = num(t.max_hp_ratio);
if (!(ratio > 0)) return;
battle.on('heal', (ctx) => {
if (ctx.target !== unit || !(ctx.amount > 0)) return;
const over = ctx.amount - (unit.s.maxHp - unit.hp);
if (!(over > 0)) return;
const cur = unit.findBuff('humus:recycle');
const have = cur ? cur.shield : 0;
const val = Math.min(unit.s.maxHp * ratio, have + over);
if (val > have + 1e-6) battle.addBuff(unit, { key: 'humus:recycle', shield: val, source: unit, visible: true, tags: ['talent'] });
}, { owner: unit, priority: -50 });
} }],
};
},
};
@@ -0,0 +1,68 @@
// server/sim/content/kits/ops/chess_char_2_10-rockr.js — 洛洛 (char_4040_rockr) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { canTargetEnemy } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
import { num, talentBb, up } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_10 洛洛 自负此轭: ASPD +attack_speed; the drones lock on their target until it dies or the skill ends.
// 过载 (term ba.overdrive "技能进行到一半时触发额外效果"): the second half of the duration — ATK +atk and the funnel cap
// ×scale; when the skill ends she is stunned as long as the overload lasted. 立于磐石: ATK +atk per `interval` s on the
// field, up to max_stack_cnt stacks.
// S1 战术咏唱·γ型 (alt): ASPD +attack_speed; the target lock belongs to 自负此轭 only.
chess_char_2_10_a: (bb, chess) => {
const t = talentBb(chess, 0);
const s2 = onDefaultSkill(chess);
const endOverload = (battle, unit) => {
if (unit.mem.rockFunnel) { unit.profile.funnel = unit.mem.rockFunnel; unit.mem.rockFunnel = null; }
battle.removeBuff(unit, 'rockr:overload');
};
return {
skill: {
kind: 'duration', mods: { aspd: num(bb.attack_speed) },
onStart({ battle, unit, skill }) {
unit.mem.rockOverAt = battle.time + skill.duration / 2;
unit.mem.rockOver = false;
unit.mem.rockLock = null;
},
onTick({ battle, unit }) {
if (unit.mem.rockOver || battle.time + 1e-9 < unit.mem.rockOverAt) return;
unit.mem.rockOver = true;
const f = unit.profile.funnel;
if (f) { unit.mem.rockFunnel = f; unit.profile.funnel = { ...f, max: f.max * num(bb.scale, 1) }; }
battle.addBuff(unit, { key: 'rockr:overload', mods: { atkPct: num(bb.atk) }, visible: true, tags: ['skill'] });
battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit, reason }) {
const over = unit.mem.rockOver ? battle.time - unit.mem.rockOverAt : 0;
endOverload(battle, unit);
unit.mem.rockOver = false;
unit.mem.rockLock = null;
if (over > 0 && reason !== 'death' && unit.alive) battle.applyStatus(unit, 'stun', { duration: over, source: unit });
},
},
skills: { 'skcom_magic_rage[3]': { kind: 'duration', mods: { aspd: num(bb.attack_speed) } } },
talents: [{ install(battle, unit) {
if (s2) battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active) return;
const L = unit.mem.rockLock;
if (L && L.alive && bodyInKeys(L, unit.rangeKeySet) && canTargetEnemy(unit, L, ctx.profile || unit.profile)) ctx.targets = [L];
else unit.mem.rockLock = ctx.targets[0] ?? null;
}, { owner: unit });
const iv = num(t.interval), max = Math.floor(num(t.max_stack_cnt)), v = num(t.atk);
if (!(iv > 0) || !(max > 0) || !v) return;
battle.on('tick', () => {
if (!up(unit)) return;
const n = Math.min(max, Math.floor((battle.time - unit.deployedAt + 1e-9) / iv));
const cur = unit.findBuff('rockr:rock');
if ((cur ? cur.stacks : 0) === n) return;
if (n > 0) battle.addBuff(unit, { key: 'rockr:rock', mods: { atkPct: v }, stacks: n, maxStacks: max, tags: ['talent'] });
else battle.removeBuff(unit, 'rockr:rock');
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,79 @@
// server/sim/content/kits/ops/chess_char_2_11-kazema.js — 风丸 (char_4016_kazema) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, up, cheb, freeTileAround } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_11 风丸 纸艺·双影: lose hp_ratio of current HP, ATK +atk, summon the <替身> (overrideTokenKey) on a free melee tile
// around her for the skill's duration [ASSUMED lifetime]. 折纸生花: whenever a <替身> appears (the summon, and the
// dollkeeper substitution) enemies on the 8 surrounding tiles take damage_scale × its ATK arts — air units too [ASSUMED:
// no 对空 note on PRTS; the <替身> itself "可对空"].
// Trait substitution: she fights with the <替身>'s stats (the engine already swaps in its HP; the kit swaps ATK/DEF
// with flat deltas so %-buffs still apply). Elite module (PUM-X, trait atk): while substituted, ATK +atk.
// S1 纸艺·迅击 (alt, attack SP): the next attack hits at atk_scale × ATK and she loses hp_ratio of her MAX HP (PRTS:
// ten casts knock her out — the loss can be fatal and so sets off the <替身> substitution).
chess_char_2_11_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const tokId = chess?.skill?.overrideTokenKey ?? (chess?.tokens ?? []).find((x) => /shadow|doll/.test(String(x))) ?? 'token_10022_kazema_shadow';
const ds = num(t.damage_scale);
const origami = (battle, src, atk) => {
battle.fx('aoe', { x: src.x, y: src.y, radius: 1.5, id: src.id, skill: 'origami' });
if (!(ds > 0)) return;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.s.flags.untargetable || cheb(src, e) > 1) continue;
battle.dealDamage(src, e, { amount: atk * ds, type: 'arts', isSkill: true, canDodge: false, tags: ['talent'] });
}
};
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) {
const loss = unit.hp * num(bb.hp_ratio);
if (loss > 0 && unit.hp - loss >= 1) battle.loseHp(unit, loss, { source: unit });
const tile = freeTileAround(battle, unit, (r, c) => battle.grid.canStand(r, c));
if (!tile) return;
const doll = battle.spawnToken(unit, tokId, tile[0], tile[1], {});
if (!doll) return;
unit.mem.doll = doll;
battle.fx('summon', { x: doll.x, y: doll.y, id: doll.id, token: tokId });
origami(battle, doll, doll.s.atk);
},
onEnd({ battle, unit }) {
const d = unit.mem.doll;
if (d && d.alive) battle.retreat(d, { reason: 'expired', permanent: true });
unit.mem.doll = null;
},
},
skills: {
skchr_kazema_1: {
kind: 'instant', attack: { atkScale: num(bb.atk_scale, 1) },
onAttack({ battle, unit }) {
const loss = unit.s.maxHp * num(bb.hp_ratio);
if (loss > 0 && unit.alive) battle.loseHp(unit, loss, { source: unit, tags: ['skill'] });
},
},
},
talents: [{ install(battle, unit) {
const bonus = num(tb.atk);
battle.on('tick', () => {
const sub = up(unit) && !!unit.findBuff('trait:substitute');
if (sub && !unit.mem.kzSub) {
unit.mem.kzSub = true;
const ts = battle.tokenDef(tokId, unit)?.stats ?? null; // the owner's module variant (DESIGN §16)
const mods = {};
if (num(ts?.atk) > 0) mods.atkFlat = num(ts.atk) - unit.base.atk;
if (ts && Number.isFinite(ts.def)) mods.defFlat = ts.def - unit.base.def;
if (bonus > 0) mods.atkPct = bonus;
battle.addBuff(unit, { key: 'kazema:doll', mods, tags: ['trait'] });
origami(battle, unit, unit.s.atk);
} else if (!sub && unit.mem.kzSub) {
unit.mem.kzSub = false;
battle.removeBuff(unit, 'kazema:doll');
}
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,65 @@
// server/sim/content/kits/ops/chess_char_2_12-gravel.js — 砾 (char_237_gravel) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, installAura } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_12 砾 鼠群 (passive): at each deployment a barrier of hp_ratio × max HP that decays to 0 over `duration` s
// (capacity stepped once per second; a damaged barrier shrinks in proportion — PRTS).
// 快速部署: own deployment cost +cost (−1). Elite 小个子支援: every unit whose initial deployment cost ≤ cond.cost
// DEF +def while 砾 is on the field. (Elite module withdraw refund: no manual retreat in battle ⇒ no effect.)
// S1 影袭 (alt, passive): at each deployment DEF +def, decaying linearly to 0 over `duration` s, updated once per
// second (PRTS: "防御力加成每秒更新一次").
chess_char_2_12_a: (bb, chess) => {
const t = talentBb(chess, 0);
return {
skill: {
kind: 'passive',
onStart({ battle, unit }) {
const total = unit.s.maxHp * num(bb.hp_ratio);
const dur = num(bb.duration, 10);
if (!(total > 0) || !(dur > 0)) return;
let k = 0;
battle.addBuff(unit, {
key: 'gravel:rats', shield: total, duration: dur, interval: 1, visible: true, tags: ['skill'],
// PRTS: "屏障最大容量每秒更新一次;受到伤害后,剩余容量根据屏障最大容量等比变化" — the capacity steps down
// once per second and what is left of the barrier scales with it (never a plain cap)
onTick: ({ unit: u, buff }) => {
const prev = Math.max(0, dur - k) / dur;
k++;
const next = Math.max(0, dur - k) / dur;
if (prev > 0 && buff.shield > 0) { buff.shield *= next / prev; u.markDirty(); }
},
});
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
},
},
skills: {
skchr_gravel_1: {
kind: 'passive',
onStart({ battle, unit }) {
const v = num(bb.def), dur = num(bb.duration);
if (!(v > 0) || !(dur > 0)) return;
let k = 0;
battle.addBuff(unit, {
key: 'gravel:shadow', duration: dur, mods: { defPct: v }, interval: 1, visible: true, tags: ['skill'],
onTick: ({ unit: u, buff }) => {
k++;
buff.mods = { defPct: v * Math.max(0, dur - k) / dur };
u.markDirty();
},
});
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'def' });
},
},
},
talents: [{ install(battle, unit) {
const c = num(t.cost);
if (c) unit.base.cost = Math.max(0, unit.base.cost + c);
const d = num(t.def), lim = t['cond.cost'];
if (d && lim != null) installAura(battle, unit, { key: 'talent:gravel', value: d, select: (a) => num(a.def?.stats?.cost, Infinity) <= num(lim), mods: { defPct: d } });
} }],
};
},
};
@@ -0,0 +1,78 @@
// server/sim/content/kits/ops/chess_char_2_13-tippi.js — 蒂比 (char_4191_tippi) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, up, onHitOn } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
/**
* 起飞 of 蒂比's skills (gamedata_const ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击,可以阻挡飞行敌人"): the skill's flags
* `liftoff` (no ground enemy blocked — Battle._blockerFor; 对地规避 — targeting.js evadesGround: no ground enemy selects
* her, while what selects nobody still lands — 无视无法选择 abilities, direct picks, flying units' blasts, 无来源 damage, the
* ticks of a debuff already on her) and `blockFly` (blocks flyers at the air radius). She stays a ground unit on her tile (PRTS 行动方式 "起飞的
* 干员仍然是地面单位": `unit.ground` unchanged — 隐德来希's 血镰 still counts her; 地面干员 bonds / items read her position).
*/
const LIFTOFF_FLAGS = Object.freeze({ blockFly: true, liftoff: true });
/** 蒂比 take-off: the ground enemies she blocked walk on. */
function tippiTakeOff({ battle, unit }) {
battle.releaseBlocked(unit);
battle.fx('takeoff', { x: unit.x, y: unit.y, id: unit.id });
}
/** 蒂比 landing when the airborne skill ends: the flyers she held are released. */
function tippiLand({ battle, unit }) {
battle.releaseBlocked(unit);
}
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_13 蒂比 紧急赶场通知 (AUTO): "受到攻击后触发" is officially "受到伤害前触发" (PRTS 修正 原因 6; 备注 "在受到伤害前自动
// 触发技能…若本次伤害为物理或法术,再将本次伤害闪避") — any incoming damage instance (an attack, a zone tick, the 源石溶剂
// drain; never a 流失, which has no `hit`) sets it off (the kit is the only trigger: the engine rule is disabled) and a
// physical/arts one is dodged; takes off for the duration: skill range, ATK +atk, attacks become 3 shots, blocks flying
// (not ground) enemies. Trait "起飞后能够阻挡2个飞行敌人": flying enemies are blocked only while airborne. 片场工作指南
// ("若最近N秒内未受伤害" — 修正 原文 攻击): if no damage for stack_time s, the next physical/arts damage is dodged (prob);
// every damage instance restarts that timer (dodged or not; a 流失 does not — PRTS 备注).
// S1 专业喷绘技巧 (alt, DEFAULT trigger from data): takes off at once for its duration — skill range, ATK +atk, blocks
// flyers only; no triple shot, and incoming attacks never set it off.
// Both take off as 起飞 (LIFTOFF_FLAGS): she blocks no ground enemy and none selects her (对地规避), so a ground enemy's
// selected damage refused on the way never reaches her `hit` and neither sets off S2 nor restarts 片场工作指南.
chess_char_2_13_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const s2 = onDefaultSkill(chess);
return {
trait: { blockFly: false },
skill: {
kind: 'duration', trigger: { rule: 'CUSTOM_RANGE', grid: [] }, // never by the engine: the hit handler below
mods: { atkPct: num(bb.atk) }, flags: LIFTOFF_FLAGS,
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null },
attack: { hits: 3 },
onStart: tippiTakeOff,
onEnd: tippiLand,
},
skills: {
skchr_tippi_1: {
kind: 'duration', mods: { atkPct: num(bb.atk) }, flags: LIFTOFF_FLAGS,
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null },
onStart: tippiTakeOff,
onEnd: tippiLand,
},
},
talents: [{ install(battle, unit) {
battle.removeBuff(unit, 'trait:skywalker'); // the profession grants blockFly permanently; here only airborne
const st = num(t.stack_time), p = num(t.prob, 1);
let last = -Infinity;
battle.on('deploy', ({ unit: u }) => { if (u === unit) last = battle.time - st; }, { owner: unit });
onHitOn(battle, unit, (ctx) => {
const { dmg } = ctx;
if (!dmg || dmg.cancel || !up(unit)) return;
const dodgeable = dmg.type === 'phys' || dmg.type === 'arts';
const sk = unit.skill;
let dodged = false;
if (s2 && sk && sk.ready && !sk.active && unit.canAct && !unit.s.flags.silence && sk.activate('TAKE_DAMAGE')) dodged = dodgeable;
else if (dodgeable && st > 0 && battle.time - last + 1e-9 >= st && battle.rng.chance(p)) dodged = true;
last = battle.time;
if (dodged) { dmg.cancel = true; battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id }); }
}, 5);
} }],
};
},
};
@@ -0,0 +1,29 @@
// server/sim/content/kits/ops/chess_char_2_14-flower.js — 调香师 (char_181_flower) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, moduleBb, up } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_14 调香师 精调: ASPD +attack_speed (−50), ATK +atk. 熏衣草: every ally on the field recovers
// atk_to_hp_recovery_ratio × ATK HP per second. Elite module (RIN-Y, hidden attack@max_target): heals 4 allies.
// S1 治疗强化·β型 (alt): ATK +atk for its duration.
chess_char_2_14_a: (bb, chess) => {
const t = talentBb(chess, 0);
const cnt = Math.floor(num(moduleBb(chess)['attack@max_target']));
return {
trait: cnt > 0 ? { heal: { mode: 'multi', count: cnt } } : undefined,
skill: { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) } },
skills: { 'skcom_heal_up[2]': { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
const r = num(t.atk_to_hp_recovery_ratio);
if (!(r > 0)) return;
battle.every(1, () => {
if (!up(unit)) return;
const amt = unit.s.atk * r;
for (const a of battle.alliesFor(unit)) if (a.hp < a.s.maxHp) battle.heal(unit, a, amt, { aura: true, tags: ['talent'] });
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,43 @@
// server/sim/content/kits/ops/chess_char_2_15-akkord.js — 协律 (char_4051_akkord) kit, tier 2 (hidden).
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, alliesInGridOf, toggleBuff } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_15 协律 (hidden) 震爆调谐: ATK +atk; each attack sets off a sonic boom at every other operator in range:
// attack@aoe_atk_scale × ATK arts within attack@range_radius. 律脉同构: ATK +atk with another operator in range.
// Elite module (BLA-X): damage grows with distance, up to +damage_scale at max_dist tiles.
chess_char_2_15_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const rad = num(bb['attack@range_radius'], 0.9), sc = num(bb['attack@aoe_atk_scale']);
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onAttack({ battle, unit }) {
if (!(sc > 0)) return;
for (const a of alliesInGridOf(battle, unit)) {
if (a === unit || a.kind !== 'op') continue;
battle.fx('sonic', { x: a.x, y: a.y, radius: rad, id: unit.id });
for (const e of battle.foesInRadius(a.x, a.y, rad)) {
if (!e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * sc, type: 'arts', isSkill: true, isSplash: true, tags: ['sonic'] });
}
}
},
},
talents: [{ install(battle, unit) {
const v = num(t.atk);
if (v) toggleBuff(battle, unit, 'akkord:sync', () => alliesInGridOf(battle, unit).some((a) => a !== unit && a.kind === 'op'), { atkPct: v });
if (tb.damage_scale != null) {
const mn = num(tb.min_dist), mx = num(tb.max_dist, 4), ds = num(tb.damage_scale);
onHitBy(battle, unit, ({ target, dmg }) => {
if (!dmg.isAttack) return;
const d = Math.hypot(target.x - unit.x, target.y - unit.y);
dmg.amount *= 1 + ds * (mx > mn ? Math.max(0, Math.min(1, (d - mn) / (mx - mn))) : 1);
});
}
} }],
};
},
};
@@ -0,0 +1,47 @@
// server/sim/content/kits/ops/chess_char_2_16-whitew.js — 拉普兰德 (char_140_whitew) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitOn } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_16 拉普兰德 狼魂 (AUTO, attack SP): ATK +atk, arts damage, one extra target, ranged attacks no longer reduced.
// 精神摧毁: attacks disable the target's abilities (silence) for `duration` s. Elite module (LOR-X, trait
// atk_scale_m): attacks add atk_scale_m × ATK arts damage.
// S1 日晷 (alt, attack SP, 持续时间无限 ⇒ toggle until she falls): ATK +atk and a `prob` chance to block (抵挡: the
// damage instance is negated) each physical damage an enemy deals her.
chess_char_2_16_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const trait = {};
if (num(t.duration) > 0) trait.onHitStatus = { key: 'silence', duration: num(t.duration) };
if (num(tb.atk_scale_m) > 0) {
trait.afterHit = (battle, u, target) => {
// PRTS 特性备注 "造成预计算的法术附加伤害": 附加伤害 (tag addition — no 叙拉古 6 roll)
if (target && target.alive && target.side === 'enemy') battle.dealDamage(u, target, { amount: u.s.atk * num(tb.atk_scale_m), type: 'arts', tags: ['module', 'addition'] });
};
}
return {
trait,
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, targeting: { maxTargets: 2 }, attack: { dmgType: 'arts', dmgMul: () => 1 } },
skills: {
skchr_whitew_1: {
kind: 'toggle', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit, skill }) {
if (unit.mem.sundial) battle.off(unit.mem.sundial);
const p = num(bb.prob);
unit.mem.sundial = p > 0 ? onHitOn(battle, unit, ({ source, credit, dmg }) => { // 抵挡 is target-side: a 无来源 burst counts via its credit
const src = source || credit;
if (!skill.active || dmg.cancel || dmg.type !== 'phys' || !src || src.side !== 'enemy') return;
if (battle.rng.chance(p)) { dmg.cancel = true; battle.fx('block', { x: unit.x, y: unit.y, id: unit.id }); }
}, 5) : null;
},
onEnd({ battle, unit }) {
if (unit.mem.sundial) battle.off(unit.mem.sundial);
unit.mem.sundial = null;
},
},
},
};
},
};
@@ -0,0 +1,46 @@
// server/sim/content/kits/ops/chess_char_2_17-branch.js — 折桠 (char_4207_branch) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitOn, enemiesInGrid } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_17 折桠 生存决心: on start, ground enemies around her (skill grid) 战栗 (tremble: "被阻挡后无法进行普通攻击") for
// not_combat s; ATK +atk, DEF +def, hits every blocked enemy. 简易包扎: heals hp_ratio × max HP when the skill ends.
// Elite module (UNY-X, trait damage_scale): damage from enemies she blocks ×damage_scale.
// S1 绝境抵抗 (alt, TAKE_DAMAGE / hurt SP from data): DEF +def and 抵抗 (−one_minus_status_resistance: control
// statuses last half as long) for its duration; 简易包扎 heals at its end too.
chess_char_2_17_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk), defPct: num(bb.def) }, attack: { hitAllBlocked: true },
onStart({ battle, unit }) {
const grid = def?.skill?.rangeGrid;
const foes = grid ? enemiesInGrid(battle, unit, grid, { canHitFly: false, groundOnly: true }) : battle.foesInRadius(unit.x, unit.y, 1.5).filter((e) => !e.isFlying);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'resolve' });
for (const e of foes) battle.applyStatus(e, 'tremble', { duration: num(bb.not_combat), source: unit });
},
},
skills: {
skchr_branch_1: {
kind: 'duration', mods: { defPct: num(bb.def) },
onStart({ battle, unit, skill }) {
const v = Math.min(0.95, Math.max(0, -num(bb.one_minus_status_resistance)));
if (v > 0) battle.applyStatus(unit, 'resist', { duration: skill.timeLeft, value: v, source: unit });
},
},
},
talents: [{ install(battle, unit) {
const hr = num(t.hp_ratio);
if (hr > 0) {
battle.on('skillEnd', ({ unit: u, reason }) => {
if (u === unit && reason !== 'death' && unit.alive) battle.heal(unit, unit, unit.s.maxHp * hr, { self: true, tags: ['talent'] });
}, { owner: unit });
}
if (tb.damage_scale != null) onHitOn(battle, unit, ({ source, dmg }) => { if (source && source.blockedBy === unit) dmg.mul *= num(tb.damage_scale, 1); });
} }],
};
},
};
@@ -0,0 +1,37 @@
// server/sim/content/kits/ops/chess_char_2_18-ashlok.js — 灰毫 (char_431_ashlok) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_18 灰毫 攻击力强化·γ型 (DEFAULT, like S2: the owner's deliberate deviation from the 重装 TAKE_DAMAGE row, data
// TRIGGER_DEVIATIONS, DESIGN §21.29): ATK +atk. 炮术研习: ATK +atk, or +ashlok_t_1.atk when the `cnt` orthogonal
// tiles around her are all ground (LOW). Elite module (FOR-X, trait atk_scale): vs blocked enemies ATK ×atk_scale.
// S2 专注轰击 (alt): block count 0 (noBlock: releases what she holds), only ranged (splash) attacks, base attack time
// +base_attack_time s (−0.4 / −0.5 on 2.8), ATK +atk.
chess_char_2_18_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) } },
skills: {
skchr_ashlok_2: {
kind: 'duration', mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess) }, flags: { noBlock: true },
attack: { fortress: false, _fortressMelee: false },
},
},
talents: [{ install(battle, unit) {
battle.on('deploy', ({ unit: u }) => {
if (u !== unit) return;
const r = unit.tileR, c = unit.tileC;
const low = [[1, 0], [-1, 0], [0, 1], [0, -1]].filter(([dr, dc]) => battle.grid.isLow(r + dr, c + dc)).length;
const ground = low >= num(t.cnt, 4);
const v = ground ? num(t['ashlok_t_1.atk'], num(t.atk)) : num(t.atk);
if (v) battle.addBuff(unit, { key: 'talent:ashlok', mods: { atkPct: v }, data: { ground }, tags: ['talent'] });
}, { owner: unit });
if (num(tb.atk_scale, 1) !== 1) onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && target.blockedBy) dmg.amount *= num(tb.atk_scale, 1); });
} }],
};
},
};
@@ -0,0 +1,49 @@
// server/sim/content/kits/ops/chess_char_2_19-tinman.js — 锡人 (char_4151_tinman) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, enemiesInGrid, instantKind } from '../shared/tier1.js';
import { tinmanKit } from './chess_char_1_16-tinman.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 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;
// the zone counts for the module's SP bonus and outlives her).
chess_char_2_19_a: (bb, chess, def) => {
const k = tinmanKit(bb, chess, def);
const dur = num(bb.projectile_delay_time, 8), radius = num(bb.projectile_range, 1), dmgScale = num(bb.atk_scale);
const weak = Math.min(1, Math.max(0, -num(bb.atk)));
const wither = num(talentBb(chess, 1)['skill@damage_scale'], 1);
const STEP = 0.25; // weaken refresh cadence (damage every 1 s)
return {
...k,
skills: {
...(k.skills || {}),
skchr_tinman_1: {
kind: instantKind(def),
onStart({ battle, unit }) {
const tgt = enemiesInGrid(battle, unit, null, { n: 1, groundOnly: true })[0] ?? enemiesInGrid(battle, unit, null, { n: 1 })[0];
const x = tgt ? tgt.x : unit.x + unit.fwd[1], y = tgt ? tgt.y : unit.y + unit.fwd[0];
const atk = unit.s.atk;
const n = Math.max(1, Math.round(dur / STEP)), per = Math.max(1, Math.round(1 / STEP));
let i = 0;
unit.mem.tinZones = (unit.mem.tinZones ?? 0) + 1;
battle.fx('zone', { x, y, radius, dur, id: unit.id, skill: 'tinman1' });
battle.every(STEP, (b, sc) => {
const pulse = i % per === 0;
for (const e of b.foesInRadius(x, y, radius)) {
if (e.isFlying || e.s.flags.untargetable) continue;
if (weak > 0) b.applyStatus(e, 'weaken', { duration: STEP + 0.05, value: weak, source: unit });
if (wither > 1) b.addBuff(e, { key: 'tinman:wither', duration: STEP + 0.05, data: { mul: wither }, source: unit });
if (pulse && dmgScale > 0) b.dealDamage(unit, e, { amount: atk * dmgScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'zone'] });
}
if (++i >= n) { sc.cancel(); unit.mem.tinZones = Math.max(0, (unit.mem.tinZones ?? 1) - 1); }
}, { immediate: true });
},
},
},
};
},
};
@@ -0,0 +1,49 @@
// server/sim/content/kits/ops/chess_char_3_01-angel.js — 能天使 (char_103_angel) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, instantKindOf, groundAspd, alive, fx } from '../shared/tier3.js';
export default {
// ---- 3_01 能天使 · 速射手 — S3 过载模式: 5 连射 + 攻击间隔缩短; 快速弹匣 ASPD; 天使的祝福 (self + one random ally)
// S1 冲锋模式: next attack = 3 shots × atk_scale; S2 扫射模式: 4 shots × attack@atk_scale per attack for 15 s;
// 精锐 module MAR-Y: ASPD + while a ground enemy is in range (MAR-X fly × = profession layer)
chess_char_3_01_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const bless = { atkPct: num(t1.atk), hpPct: num(t1.max_hp) };
const shots = (v) => Math.max(1, Math.floor(num(v, 1)));
const kit = {
skill: {
kind: 'duration',
mods: { batPct: num(bb.base_attack_time) },
attack: { atkScale: num(bb['attack@atk_scale'], 1), hits: Math.max(1, Math.floor(num(bb['attack@times'], 1))) },
},
skills: altSkills(chess, d, bb, {
skchr_angel_1: (s) => ({ kind: instantKindOf(s), attack: { atkScale: num(s.bb.atk_scale, 1), hits: shots(s.bb.times) } }),
skchr_angel_2: (s) => ({ kind: 'duration', attack: { atkScale: num(s.bb['attack@atk_scale'], 1), hits: shots(s.bb['attack@times']) } }),
}),
talents: [
{ install(battle, unit) { battle.addBuff(unit, { key: 'talent:angel_mag', mods: { aspd: num(t0.attack_speed) }, persist: true, allowDead: true }); } },
{ install(battle, unit) {
battle.addBuff(unit, { key: 'talent:angel_bless', mods: { ...bless }, persist: true, allowDead: true });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
// after the whole board is deployed (the initial deployment goes one unit after another)
battle.after(0, () => {
if (!alive(unit)) return;
const cands = battle.allies(unit.ownerId).filter((a) => a !== unit && a.kind === 'op' && !a.findBuff('talent:angel_bless_ally'));
const pick = battle.rng.pick(cands);
if (!pick) return;
battle.addBuff(pick, { key: 'talent:angel_bless_ally', mods: { ...bless }, persist: true, source: unit, visible: true });
fx(battle, 'buff', pick, { src: unit.id, talent: 'angel_bless' });
}, { owner: unit });
}, { owner: unit });
} },
],
};
// MAR-Y trait "范围内存在地面敌人时攻击速度+8" (bb attack_speed, cnt = ground enemies needed)
if (num(tb.attack_speed) > 0) kit.install = groundAspd('trait:angel_ground', num(tb.attack_speed), Math.floor(num(tb.cnt, 1)));
return kit;
},
};
@@ -0,0 +1,67 @@
// server/sim/content/kits/ops/chess_char_3_02-ayer.js — 断崖 (char_294_ayer) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import {
num, defOf, talentBb, traitBb, altSkills, instantKindOf, alive, gridKeys, fx, copyGrid, NINE, aura,
} from '../shared/tier3.js';
export default {
// ---- 3_02 断崖 · 领主 — S2 浮游刃启动: arts, wider range, each attack hits every enemy blocked by allies around her
chess_char_3_02_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const tGrid = copyGrid(d.talents?.[0]?.rangeGrid) ?? NINE;
const extra = num(bb.atk_scale, 1);
const moduleArts = num(tb.atk_scale_m, 0);
const skillGrid = copyGrid(d.skill?.rangeGrid);
const kit = {
skill: {
kind: 'duration',
attack: { dmgType: 'arts', atkScale: num(bb['attack@atk_scale'], 1) },
onAttack({ battle, unit }) {
// "周围8格友方单位阻挡的所有敌人": the 8 tiles around her (her own tile is not one of them — the talent
// says "自身与周围8格" when it means both)
const seen = new Set();
for (const a of battle.allyUnits) {
if (a === unit || !alive(a) || a.kind === 'device' || !a.blocking.length) continue;
if (Math.abs(a.tileR - unit.tileR) > 1 || Math.abs(a.tileC - unit.tileC) > 1) continue;
for (const e of a.blocking) {
if (!e.alive || seen.has(e)) continue;
seen.add(e);
battle.dealDamage(unit, e, { amount: unit.s.atk * extra, type: 'arts', isSkill: true, tags: ['skill', 'melee', 'ayerBlade'] });
fx(battle, 'strike', e, { src: unit.id, skill: 'ayer_2' });
}
}
},
},
// S1 多导向散射弹丸: next attack hits up to max_target enemies in range for atk_scale arts + 停顿
skills: altSkills(chess, d, bb, {
skchr_ayer_1: (s) => {
const slug = num(s.bb.sluggish, 0);
return {
kind: instantKindOf(s),
targeting: { maxTargets: Math.max(1, Math.floor(num(s.bb.max_target, 1))) },
attack: {
dmgType: 'arts', atkScale: num(s.bb.atk_scale, 1),
onEachHit({ battle, unit, target }) {
if (slug > 0 && target && target.alive && target.side === 'enemy') battle.applyStatus(target, 'sluggish', { duration: slug, source: unit });
},
},
};
},
}),
talents: [{ install(battle, unit) {
aura(battle, unit, { key: 'talent:ayer_aspd', side: 'ally', tiles: () => gridKeys(tGrid, unit), filter: (a) => a.kind === 'op', mods: { aspd: num(t0.attack_speed) } });
} }],
};
if (skillGrid) kit.skill.targeting = { rangeGrid: skillGrid };
// 精锐 module LOR-X: "攻击附带10%攻击力的法术伤害"
if (moduleArts > 0) {
kit.trait = { afterHit: (battle, unit, target) => {
if (target && target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * moduleArts, type: 'arts', tags: ['module'] });
} };
}
return kit;
},
};
@@ -0,0 +1,36 @@
// server/sim/content/kits/ops/chess_char_3_03-swire.js — 诗怀雅 (char_308_swire) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, gridKeys, copyGrid, NINE, aura } from '../shared/tier3.js';
export default {
// ---- 3_03 诗怀雅 · 教官 — S2 协同作战: ATK +, first talent ×talent_scale; 近距离作战指导 melee ATK aura
// S1 指挥调度: the talent covers the skill's range (x-1 normal / x-2 elite) at ×talent_scale while it runs
chess_char_3_03_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tGrid = copyGrid(d.talents?.[0]?.rangeGrid) ?? NINE;
const scale = num(bb.talent_scale, 1);
// "第一天赋生效范围扩大" (talent_range_flag): the selected skill's range replaces the talent's while it is active
const skillTGrid = num(bb.talent_range_flag) > 0 ? copyGrid(d.skill?.rangeGrid) : null;
const refresh = ({ unit }) => unit.mem.swireAura?.();
return {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onStart: refresh,
onEnd: refresh,
},
skills: altSkills(chess, d, bb, {
skchr_swire_1: () => ({ kind: 'duration', onStart: refresh, onEnd: refresh }),
}),
talents: [{ install(battle, unit) {
unit.mem.swireAura = aura(battle, unit, {
key: 'talent:swire_guide', side: 'ally', tiles: () => gridKeys(skillTGrid && unit.skill?.active ? skillTGrid : tGrid, unit),
filter: (a) => String(a.def?.position ?? 'MELEE').toUpperCase() === 'MELEE',
mods: () => ({ atkPct: num(t0.atk) * (unit.skill?.active ? scale : 1) }),
});
} }],
};
},
};
@@ -0,0 +1,230 @@
// server/sim/content/kits/ops/chess_char_3_04-swire2.js — 琳琅诗怀雅 (char_1033_swire2) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { bodyOnTile } from '../../../body.js';
import {
num, defOf, talentBb, moduleTalentBb, selectedId, altSkills, alive, gridKeys, fx, copyGrid, textNum, freeTile,
groundTile, enemiesOn,
} from '../shared/tier3.js';
/** 琳琅诗怀雅 S3's coin range (PRTS 备注 "前方范围2-4"; range_table "2-4", facing right). */
const SWIRE2_COIN_GRID = Object.freeze([[1, 1], [0, 0], [0, 1], [0, 2], [-1, 1]]);
/** Merchant trait with a callback on every successful DP payment (琳琅诗怀雅 大买家). Same rules as professions.js. */
function merchantInstall(onPay) {
return (battle, unit) => {
const iv = unit.profile.merchantInterval ?? 3;
const cost = unit.profile.merchantCost ?? 3;
battle.every(iv, () => {
if (!alive(unit)) return;
const pl = battle.getPlayer(unit.ownerId);
if (!pl) return;
if (pl.dp >= cost) {
battle.addDp(unit.ownerId, -cost);
onPay(battle, unit, cost);
battle.emit('merchantPay', { unit, cost });
} else battle.retreat(unit, { reason: 'merchant' });
}, { owner: unit });
};
}
export default {
// ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): each attack spends a coin to drop a champagne bomb in range;
// 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles)
// S1 仗义疏财 (passive, 2 coins): a coin heals the most injured ally (< 70 % HP) of the 8 surrounding tiles for
// attack@heal_scale × ATK — on her attack, or with no enemy to attack on her own attack timer (owner's decision
// 2026-10-04, against the official 「下一次攻击会为…」; installS1). S3 千金一掷 (持续时间无限): attacks hit twice, kills give a coin;
// closing it spends every coin on random ground enemies of range 2-4 in front and those she blocks (atk_scale phys +
// a small push, radial despite the text's 向前 — PRTS 备注 "推开效果为径向推动"; client charpack char_1033_swire2:
// the RandomGold ability (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位,
// 弹道不可对空). It does not close itself when the purse is full (PRTS 卫戍协议/帮助 「通常不会自动关闭技能」; the skill
// text is 「可随时主动关闭」). Owner 2026-10-04: at the cap (金币上限为10) she shoots once an enemy is in the skill's
// attack range (the 1-tile range, not the coin-mark 2-4 grid). Below the cap, or with nobody there, it stays open.
// 精锐 module MER-Y: ATK +4 % per trait payment (≤ 5 stacks).
chess_char_3_04_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const sel = selectedId(chess, d);
const S1 = 'skchr_swire2_1', S2 = 'skchr_swire2_2', S3 = 'skchr_swire2_3';
// "携带此技能时金币上限为N": the SELECTED skill's cap
const coinMax = num(bb.sp, 3);
const coinCost = Math.abs(num(bb['attack@sp'], -1)) || 1;
const modTal = moduleTalentBb(chess);
const scale = num(bb.atk_scale, 1);
const slug = num(bb.sluggish, 2);
const tokenId = chess?.skill?.overrideTokenKey ?? (d.tokens || []).find((t) => /gdtrap/.test(String(t))) ?? 'token_10031_swire2_gdtrap';
const skillGrid = copyGrid(d.skill?.rangeGrid);
const addCoins = (battle, unit, n) => {
const before = unit.mem.coins ?? 0;
unit.mem.coins = Math.min(coinMax, before + n);
if (unit.mem.coins > before) fx(battle, 'coin', unit, { n: unit.mem.coins });
};
const bombKit = (owner, switchT) => ({
skill: null,
trait: { noAttack: true },
install(battle, bomb) {
battle.on('tick', () => {
if (!bomb.alive || !bomb.deployed) return;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.isFlying || e.s.flags.untargetable) continue;
if (!bodyOnTile(e, bomb.tileR, bomb.tileC)) continue;
// first enemy touching it; after switchT s on the field the bomb deals its damage one extra time
const hits = battle.time - bomb.deployedAt >= switchT - 1e-9 ? 2 : 1;
for (let i = 0; i < hits && e.alive; i++) {
battle.dealDamage(owner, e, { amount: owner.s.atk * scale, type: 'phys', isSkill: true, canDodge: false, tags: ['skill', 'trap'] });
}
if (e.alive) battle.applyStatus(e, 'sluggish', { duration: slug, source: owner });
// `consumed`: the bomb is used up by its own blast — clients play its impact sound, not a death sound
fx(battle, 'explode', bomb, { src: owner.id, target: e.id, hits, consumed: true });
battle.retreat(bomb, { reason: 'expired', permanent: true });
break;
}
}, { owner: bomb });
},
});
const healRatio = textNum(d.skill?.description, /血量不足(\d+)%/, 70) / 100;
/**
* 仗义疏财 — official text 「消耗一枚金币,下一次攻击会为周围八格内血量不足70%的一名友方单位恢复相当于攻击力40%的生命」: the heal rides on an
* attack, so with no enemy around she never healed (community report #5 「琳琅诗怀雅1技能不会主动奶身边受伤的干员」). Owner's
* decision 2026-10-04 (a deliberate deviation, like §21.29's 重装 casts): she heals an injured ally beside her whether
* she attacks or not. Same target (the lowest HP ratio below 70 % of the 8 surrounding tiles, no 禁疗 / 孤立 unit, no
* device), coin and heal_scale × ATK. Cadence [ASSUMED]: at most one heal per attack cycle (her attack interval, ASPD
* included) — while she attacks, on the attack as before; when her last attack attempt found no target, on her own
* timer (the 'tick' hook runs after the attacks, so an attack due in the same tick takes it).
*/
const installS1 = (battle, unit) => {
const hs = num(bb['attack@heal_scale'], num(bb.heal_scale, 0));
let nextAt = -Infinity;
const healTarget = () => {
let best = null;
for (const a of battle.allyUnits) {
if (a === unit || !alive(a) || a.hidden || a.kind === 'device' || a.hpRatio >= healRatio) continue;
if (a.s.flags.noHeal || a.profile?.noHeal) continue; // 禁疗 / 孤立: never a heal target
if (Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) !== 1) continue; // 周围八格
if (!best || a.hpRatio < best.hpRatio || (a.hpRatio === best.hpRatio && a.deploySeq < best.deploySeq)) best = a;
}
return best;
};
const tryHeal = () => {
if (!alive(unit) || (unit.mem.coins ?? 0) < coinCost || !(hs > 0) || battle.time < nextAt - 1e-9) return;
const best = healTarget();
if (!best) return;
unit.mem.coins -= coinCost;
nextAt = battle.time + unit.s.interval;
battle.heal(unit, best, unit.s.atk * hs);
fx(battle, 'coin', unit, { n: unit.mem.coins, heal: best.id, skill: 'swire2_1' });
};
battle.on('attack', (ctx) => { if (ctx.attacker === unit) tryHeal(); }, { owner: unit, priority: -10 });
battle.on('tick', () => { if (unit.deployed && unit.canAct && !unit.trait?.hadTarget) tryHeal(); }, { owner: unit });
};
const installS3 = (battle, unit) => { // 千金一掷: "击倒敌人时获得一枚金币"
battle.on('kill', (ctx) => {
if (ctx.killer === unit && ctx.victim.side === 'enemy' && unit.skill?.active) addCoins(battle, unit, 1);
}, { owner: unit });
};
return {
skill: { kind: 'passive' },
skills: altSkills(chess, d, bb, {
[S1]: () => ({ kind: 'passive' }), // (coins → heals: installS1)
[S3]: (s) => {
const cash = num(s.bb.atk_scale, 1), force = num(s.bb.force, 0);
// the 【金币标记】 targets: ground enemies on range 2-4 in front of her (range_table "2-4") and every unit she blocks
const coinMarks = (battle, unit) => {
const list = enemiesOn(battle, unit, gridKeys(SWIRE2_COIN_GRID, unit), 0, { ...unit.profile, canHitFly: false });
for (const e of unit.blocking || []) if (e.alive && !e.isFlying && !list.includes(e)) list.push(e);
return list;
};
const purseCap = Math.max(1, Math.floor(num(s.bb.sp, 10)));
return {
kind: 'toggle',
attack: { hits: 2 },
// Owner 2026-10-04: at the cap, shoot once a ground enemy is in the skill attack range (data rangeGrid,
// the same 1-tile range as her attack). The coin marks (前方 2-4) are who the burst pays, not the trigger.
// Nobody in that range: stay open and keep the coins. Below the cap: stay open.
onTick({ battle, unit, skill }) {
if (!skill.active || (unit.mem.coins ?? 0) + 1e-9 < purseCap) return;
const keys = gridKeys(skillGrid ?? unit.rangeGrid, unit);
if (enemiesOn(battle, unit, keys, 0, { ...unit.profile, canHitFly: false }).length) skill.end('manual');
},
onEnd({ battle, unit, reason }) {
if (reason !== 'manual' || !unit.alive) return;
const n = unit.mem.coins ?? 0;
unit.mem.coins = 0;
let spent = 0;
// PRTS 备注: closing it "立即对前方范围2-4内的地面敌方单位与自身阻挡的所有单位施加【金币标记】", the coins going to
// marked units at random — 弹道(不可对空): air units (FLY, 近地悬浮, 浮空) are never paid. Until 0.1.1 the
// coins went to her attack range (1-1) instead of range 2-4.
const marked = coinMarks(battle, unit);
for (let i = 0; i < n; i++) {
const e = battle.rng.pick(marked.filter((x) => x.alive && !x.isFlying));
if (!e) break;
spent++;
battle.dealDamage(unit, e, { amount: unit.s.atk * cash, type: 'phys', isSkill: true, tags: ['skill', 'swire2Cash'] });
// "将目标小力地向前推开" — PRTS 备注 "金币弹道…推开效果为径向推动", client knockback[relative]: a radial push
// away from her centre (not along her direction, so no 45° / 0.25-tile 特殊修正), official 力度 − 重量
// distance (Battle.push)
if (e.alive) battle.push(e, force, { from: unit });
}
fx(battle, 'coin', unit, { n: 0, spent, skill: 'swire2_3' });
},
};
},
}),
trait: { install: merchantInstall((battle, unit) => {
// MER-Y "每次特性消耗费用时攻击力+4%,最多可以叠加5次" (any time, not only during the skill)
if (modTal && num(modTal.atk) > 0) {
battle.addBuff(unit, { key: 'trait:swire2_module', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(modTal.max_stack_cnt, 5)), mods: { atkPct: num(modTal.atk) } });
}
if (!unit.skill?.active) return; // "技能期间"
addCoins(battle, unit, num(t0.trait_sp, 1));
battle.addBuff(unit, { key: 'talent:swire2_buyer', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(t0.max_stack_cnt, 8)), mods: { atkPct: num(t0.atk) } });
}) },
install(battle, unit) {
if (sel === S1) { installS1(battle, unit); return; }
if (sel === S3) { installS3(battle, unit); return; }
if (sel !== S2 && sel != null) return;
const switchT = num(battle.tokenDef(tokenId, unit)?.skill?.bb?.duration_switch, 3);
battle.on('attack', (ctx) => {
if (ctx.attacker !== unit || !alive(unit) || (unit.mem.coins ?? 0) < coinCost) return;
const tiles = [];
for (const k of gridKeys(skillGrid ?? unit.rangeGrid, unit)) {
const r = (k / COLS) | 0, c = k % COLS;
// (never on the home tile of a dead operator: it could not redeploy until an enemy triggers the bomb)
if (freeTile(battle, r, c) && groundTile(battle, r, c)) tiles.push([r, c]);
}
const tile = battle.rng.pick(tiles);
if (!tile) return;
const bomb = battle.spawnToken(unit, tokenId, tile[0], tile[1], { untargetable: true, kit: bombKit(unit, switchT) });
if (!bomb) return;
unit.mem.coins -= coinCost;
fx(battle, 'summon', bomb, { src: unit.id, token: tokenId, coins: unit.mem.coins });
}, { owner: unit, priority: -10 });
},
talents: [
{ install(battle, unit) { // 大买家: "开启技能时获得1枚金币" (a passive starts at every deployment, S3 when cast)
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
unit.mem.coins = 0;
if (unit.skill?.kind === 'passive') addCoins(battle, unit, num(t0.sp, 1));
}, { owner: unit });
battle.on('skillStart', (ctx) => { if (ctx.unit === unit && unit.skill?.kind !== 'passive') addCoins(battle, unit, num(t0.sp, 1)); }, { owner: unit });
} },
{ install(battle, unit) { // 破财消灾
battle.on('fatal', (ctx) => {
if (ctx.unit !== unit || ctx.prevented) return;
const n = unit.mem.saveCount ?? 0;
const cost = Math.abs(num(t1.cost, -5)) * Math.pow(num(t1.cost_multi, 2), n);
const pl = battle.getPlayer(unit.ownerId);
if (!pl || pl.dp + 1e-9 < cost) return;
battle.addDp(unit.ownerId, -cost);
unit.mem.saveCount = n + 1;
ctx.prevented = true;
unit.hp = Math.max(1, unit.s.maxHp * num(t1.hp_ratio, 0.7));
fx(battle, 'revive', unit, { cost });
}, { owner: unit });
} },
],
};
},
};
@@ -0,0 +1,62 @@
// server/sim/content/kits/ops/chess_char_3_05-skadi.js — 斯卡蒂 (char_263_skadi) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, statSkill, fx } from '../shared/tier3.js';
/** Abyssal Hunters (character_table groupId `abyssal`, not in data/chess.json): 深海掠食者 targets. */
const ABYSSAL = new Set(['char_263_skadi', 'char_143_ghost', 'char_218_cuttle', 'char_474_glady', 'char_1023_ghost2', 'char_4145_ulpia']);
export default {
// ---- 3_05 斯卡蒂 · 无畏者 — S3 涌潮悲歌: ATK/DEF/HP +; 深海掠食者 (Abyssal Hunters ATK); 迅捷出击 (redeploy −10 s)
// 精锐 module DRE-Y: once per deployment, a lethal hit instead restores full HP with max HP −60 % and ASPD +30
// S1 迅捷打击·γ型: ATK/ASPD +; S2 跃浪击 (passive): ATK + for `duration` s after every deployment;
// 精锐 module DRE-X: ×atk_scale on blocked enemies
chess_char_3_05_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const kit = {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk), defPct: num(bb.def), hpPct: num(bb.max_hp) } },
skills: altSkills(chess, d, bb, {
'skcom_quickattack[3]': statSkill,
skchr_skadi_2: (s) => ({
kind: 'passive',
onStart({ battle, unit }) { // "部署后N秒内攻击力+X" (a passive starts at every deployment)
battle.addBuff(unit, { key: 'skill:skadi_wave', duration: num(s.bb.duration, 20), mods: { atkPct: num(s.bb.atk) }, visible: true });
},
}),
}),
talents: [
{ install(battle, unit) { // "编入队伍时": every Abyssal Hunter of this player, for the whole battle
for (const a of battle.allyUnits) {
if (a.kind === 'op' && a.ownerId === unit.ownerId && ABYSSAL.has(a.def?.charId)) {
battle.addBuff(a, { key: 'talent:skadi_predator', mods: { atkPct: num(t0.atk) }, persist: true, allowDead: true, source: unit });
}
}
} },
{ install(battle, unit) {
if (unit.mem.skadiSwift) return; // base stats are per unit; never apply twice
unit.mem.skadiSwift = true;
unit.base.respawnTime = Math.max(0, unit.base.respawnTime + num(t1.respawn_time));
} },
],
};
if (tb.value != null || tb.hp_ratio != null) {
kit.install = (battle, unit) => {
battle.on('deploy', (ctx) => { if (ctx.unit === unit) unit.mem.tideUsed = false; }, { owner: unit });
battle.on('fatal', (ctx) => {
if (ctx.unit !== unit || ctx.prevented || unit.mem.tideUsed) return;
unit.mem.tideUsed = true;
ctx.prevented = true;
const hpMul = tb.value != null ? 1 - num(tb.value) : num(tb.max_hp, 0.4);
battle.addBuff(unit, { key: 'trait:skadi_tide', mods: { hpMul: Math.max(0.05, hpMul), aspd: num(tb.attack_speed) }, visible: true });
unit.hp = Math.max(1, unit.s.maxHp * num(tb.hp_ratio, 1));
fx(battle, 'revive', unit, { module: true });
}, { owner: unit, priority: -50 });
};
}
// DRE-X "攻击被阻挡的敌人时攻击力提升至115%" (blocked by anyone)
if (num(tb.atk_scale) > 0) kit.trait = { dmgMul: (battle, unit, target) => (target && target.blockedBy ? num(tb.atk_scale, 1) : 1) };
return kit;
},
};
@@ -0,0 +1,102 @@
// server/sim/content/kits/ops/chess_char_3_06-philae.js — 菲莱 (char_4148_philae) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, selectedId, altSkills, fx, NINE, giveSp } from '../shared/tier3.js';
export default {
// ---- 3_06 菲莱 · 本源铁卫 — S2 冥河诅咒: stops attacking, HP +; when attacked blasts the ground enemies of the 3×3
// around her (PRTS 备注: range x-4; arts + ep_damage_ratio × ATK apoptosis, aoe_cd); ATK + once hit by element
// damage; 神河谕使: 元素损伤 taken −damage_resistance, +SP on apoptosis
// 精锐 module PRP-X "阻挡敌人时,自身造成的元素损伤提升15%": EVERY element fill she deals (the S2 blast, a 灼燃维式重锤
// she carries …) ×ep_damage_scale while she blocks — an `elementHit` multiplier, like 余's
// S1 灵河护佑 (TAKE_DAMAGE): HP +, clears her element gauges and gives a shield_value 损伤屏障 (absorbs element
// damage — gauge fills — until spent or the skill ends)
// 神河谕使 and the barrier act on the element hit before it lands (PRTS 备注: "减伤与技力回复效果于伤害计算前处理;即使
// 受到0点的凋亡损伤依然可以回复技力", "损伤屏障于伤害计算前、第一天赋后处理"): the talent's cut and SP first (priority 20),
// then the barrier absorbs what is left (`dmg.amount × dmg.mul`). The cut is a 元素损伤 multiplier (PRTS 元素 "受到的
// 元素损伤 = 损伤值 × (1 − 损伤抵抗 × 0.01),后续可应用元素损伤倍率提升/降低等效果"), not `elemTakenMul` (元素伤害 / 元素脆弱).
chess_char_3_06_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const sel = selectedId(chess, d);
const S1 = 'skchr_philae_1', S2 = 'skchr_philae_2';
const isS2 = sel === S2 || sel == null;
const scale = num(bb.atk_scale, 1);
const ep = num(bb.ep_damage_ratio, 0);
const cd = num(bb.aoe_cd, 2);
const epScale = num(tb.ep_damage_scale, 1);
const cut = Math.max(0, Math.min(1, num(t0.damage_resistance)));
// 损伤屏障 of S1: absorbs the gauge gain an element hit would add (after 神河谕使)
const installBarrier = (battle, unit) => {
battle.on('elementHit', (ctx) => {
if (ctx.target !== unit || !(unit.mem.philaeBarrier > 0) || !unit.skill?.active) return;
const dmg = ctx.dmg;
const eff = num(dmg.amount) * num(dmg.mul, 1);
if (!(eff > 0)) return;
const take = Math.min(unit.mem.philaeBarrier, eff);
unit.mem.philaeBarrier -= take;
if (take >= eff - 1e-9) dmg.cancel = true;
else dmg.amount *= (eff - take) / eff;
if (!(unit.mem.philaeBarrier > 0)) fx(battle, 'shieldBreak', unit, { element: true });
}, { owner: unit });
};
return {
skill: {
kind: 'duration',
mods: { hpPct: num(bb.max_hp) },
attack: { noAttack: true },
onStart({ unit }) { unit.mem.philaeCd = -Infinity; },
onEnd({ battle, unit }) { battle.removeBuff(unit, 'skill:philae_rage'); },
},
skills: altSkills(chess, d, bb, {
[S1]: (s) => ({
kind: 'duration',
mods: { hpPct: num(s.bb.max_hp) },
onStart({ battle, unit }) {
battle.reduceElement(unit, 1e12); // "立刻清除自身的元素损伤" (a gauge locked by a running burst stays)
unit.mem.philaeBarrier = num(s.bb.shield_value, 0);
fx(battle, 'shield', unit, { element: true, n: unit.mem.philaeBarrier });
},
onEnd({ unit }) { unit.mem.philaeBarrier = 0; },
}),
}),
talents: [{ install(battle, unit) {
// 神河谕使 (every skill): 元素损伤 taken ×(1 − damage_resistance); "受到凋亡损伤时回复2点技力" — even for 0 damage
battle.on('elementHit', (ctx) => {
const dmg = ctx.dmg;
if (ctx.target !== unit || !dmg || dmg.type !== 'element') return;
if (dmg.element === 'apoptosis' && !unit.skill?.active) giveSp(unit, num(t0.sp), 'talent');
if (cut > 0) dmg.mul *= 1 - cut;
}, { owner: unit, priority: 20 });
} }],
install(battle, unit) {
if (sel === S1) installBarrier(battle, unit);
// module PRP-X: her element damage ×ep_damage_scale while she blocks
if (epScale > 1) {
battle.on('elementHit', (ctx) => {
if (ctx.source === unit && ctx.dmg?.type === 'element' && ctx.target?.side === 'enemy' && unit.blocking.length) ctx.dmg.mul *= epScale;
}, { owner: unit });
}
battle.on('damaged', (ctx) => {
if (ctx.target !== unit || !unit.alive) return;
const sk = unit.skill;
if (ctx.type === 'element') {
if (isS2 && sk && sk.active && !unit.findBuff('skill:philae_rage')) battle.addBuff(unit, { key: 'skill:philae_rage', mods: { atkPct: num(bb.atk) }, visible: true });
return;
}
if (!isS2 || !sk || !sk.active || !ctx.dmg?.isAttack || !ctx.source || ctx.source.side !== 'enemy') return;
if (battle.time < (unit.mem.philaeCd ?? -Infinity)) return;
unit.mem.philaeCd = battle.time + cd;
// "周围的地面敌人" = range x-4, the 3×3 tiles around her (PRTS 备注)
for (const e of battle.unitsInGrid(unit, NINE, { side: 'enemy' })) {
if (e.isFlying) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', canDodge: false, isSkill: true, tags: ['skill', 'counter'] });
if (ep > 0 && e.alive) battle.dealDamage(unit, e, { type: 'element', element: 'apoptosis', amount: unit.s.atk * ep, tags: ['skill'] });
}
fx(battle, 'aoe', unit, { radius: 1.5, tiles: 'box', dmgType: 'arts', skill: 'philae_2' }); // box: the 3×3 tiles
}, { owner: unit });
},
};
},
};
@@ -0,0 +1,72 @@
// server/sim/content/kits/ops/chess_char_3_07-forcer.js — 见行者 (char_4036_forcer) kit, tier 3 (hidden).
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { PUSH_DIRECTIONAL_MIN_DIST } from '../../../constants.js';
import { num, defOf, talentBb, traitBb, onTiles, gridKeys, fx, copyGrid } from '../shared/tier3.js';
/** Two enemy bodies touch within this distance (tiles) — 见行者 collision stun. */
const COLLIDE = 0.6;
export default {
// ---- 3_07 见行者 · 推击手 (hidden) — S2 惊爆射击: push every enemy in the skill range forward + stun (longer when
// slammed into a wall, collided enemies stunned too; air units too [ASSUMED: "范围内所有敌人", no 对空 note on
// PRTS] — a 失衡免疫 enemy or a 静态刚体 (the drones) is not pushed but still stunned); 技巧射击: ignore DEF vs heavy enemies;
// 精锐 module PUS-X: redeployed on a ranged tile ⇒ half the deployment cost back
chess_char_3_07_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const skillGrid = copyGrid(d.skill?.rangeGrid);
const force = num(bb.force, 1);
const sDirect = num(bb['forcer_s_2[hit_directly].stun'], num(bb.stun, 1));
const sWall = num(bb.stun, sDirect);
const sBrush = num(bb['forcer_s_2[brush].stun'], sDirect);
const kit = {
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const keys = new Set(gridKeys(skillGrid ?? unit.rangeGrid, unit));
const victims = battle.enemies.filter((e) => e.alive && !e.hidden && !e.s.flags.untargetable && onTiles(e, keys));
const pushed = new Set(victims);
for (const e of victims) {
// "往身前方向…推开": a directional push whose angle is fixed — PRTS 备注 "不会因为角度过大而改变推动的方向或削减力度"
// (only the angle: a target nearer than 0.25 tile still turns radial at 受力等级 −2) — by the official
// 力度 − 重量 distance of a 特效 push (PRTS 推与拉 names 见行者's skills 特效类; Battle.push / pushDistance);
// stopped short of it ⇒ it hit a wall
const near = Math.hypot(e.x - unit.x, e.y - unit.y) < PUSH_DIRECTIONAL_MIN_DIST;
const expect = battle.pushDistance(e, near ? force - 2 : force, { effect: true });
const moved = battle.push(e, force, { from: unit, dir: { x: unit.fwd[1], y: unit.fwd[0] }, fixedAngle: true, effect: true });
const wall = expect > 0 && moved + 0.05 < expect;
battle.applyStatus(e, 'stun', { duration: wall ? sWall : sDirect, source: unit });
}
for (const e of victims) {
for (const o of battle.enemiesInRadius(e.x, e.y, COLLIDE)) {
if (pushed.has(o)) continue;
pushed.add(o);
battle.applyStatus(o, 'stun', { duration: sBrush, source: unit });
}
}
fx(battle, 'push', unit, { skill: 'forcer_2', n: victims.length });
},
// "立即将范围内所有敌人…": the data rule SKILL_RANGE fires it once an enemy is inside the skill range (PRTS 技能
// 策略 "仅在技能范围内存在敌人(无视其不可选中)时释放技能"), not only when one stands on her 2-tile attack range
},
talents: [{ install(battle, unit) {
battle.on('hit', (ctx) => {
if (ctx.source === unit && ctx.target.side === 'enemy' && ctx.target.s.massLevel >= num(t0.value, 3)) ctx.dmg.defIgnoreFlat += num(t0.def_penetrate_fixed);
}, { owner: unit });
} }],
};
if (num(tb.value) > 0) {
kit.install = (battle, unit) => {
battle.on('deploy', (ctx) => {
// ranged position = not a ground tile (a high tile, or a platform that elevates it: unit.ground false)
if (ctx.unit !== unit || ctx.initial || unit.ground) return;
battle.addDp(unit.ownerId, unit.base.cost * num(tb.value));
fx(battle, 'dp', unit, { n: unit.base.cost * num(tb.value) });
}, { owner: unit });
};
}
return kit;
},
};
@@ -0,0 +1,79 @@
// server/sim/content/kits/ops/chess_char_3_08-mint.js — 薄绿 (char_388_mint) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, gridKeys, fx, copyGrid, enemiesOn, aura } from '../shared/tier3.js';
export default {
// ---- 3_08 薄绿 · 阵法术师 — S2 聚能涡旋: each attack strikes every enemy on her range (the trait's 群体法术伤害) and
// pushes each one towards her, end-of-skill burst on
// every enemy in range; 地质学者: DEF aura (skill off) / less likely targeted (skill on);
// 精锐 module PLX-X: keeps part of the guard (DEF/RES) while the skill runs
// S1 风语: wider range (skill grid), attacks every enemy on it at attack@atk_scale. Auto-cast: the data
// rule SEARCH is the 阵法术师 row (PRTS 卫戍协议/帮助 "不受基础策略影响,在初始攻击范围内存在敌人时释放技能"; it
// covers every MANUAL skill of the class — user playtest #6) = an enemy inside her INITIAL range — the engine's
// DEFAULT rule, checked every tick for a unit that does not attack while its skill is off — not any enemy on the
// field: she would burn the skill on enemies that just spawned.
chess_char_3_08_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const tGrid = copyGrid(d.talents?.[0]?.rangeGrid) ?? [[1, 0], [0, -1], [0, 0], [0, 1], [-1, 0]];
const pullForce = num(bb['attack@force'], num(bb.force, 0));
const endScale = num(bb.atk_scale, 0);
const keepDef = num(tb['soil_e_002[buff].def'], 0), keepRes = num(tb['soil_e_002[buff].magic_resistance'], 0);
// every skill: PLX-X keeps part of the guard, 地质学者 taunt −1 while it runs, DEF aura only while it does not
const guardOn = ({ battle, unit }) => {
if (keepDef || keepRes) battle.addBuff(unit, { key: 'trait:mint_keep', mods: { defPct: keepDef, resFlat: keepRes } });
battle.addBuff(unit, { key: 'talent:mint_taunt', mods: { taunt: num(t0.taunt_level, -1) } });
unit.mem.mintAura?.();
};
const guardOff = ({ battle, unit }) => {
battle.removeBuff(unit, 'trait:mint_keep');
battle.removeBuff(unit, 'talent:mint_taunt');
unit.mem.mintAura?.();
};
return {
skills: altSkills(chess, d, bb, {
skchr_mint_1: (s) => {
const grid = copyGrid(s.rangeGrid);
return {
kind: 'duration',
trigger: { rule: 'DEFAULT' },
attack: { atkScale: num(s.bb['attack@atk_scale'], num(s.bb.atk_scale, 1)) },
...(grid ? { targeting: { rangeGrid: grid } } : {}),
onStart: guardOn,
onEnd: guardOff,
};
},
}),
skill: {
kind: 'duration',
attack: {
atkScale: num(bb['attack@atk_scale'], 1),
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.side !== 'enemy') return;
// PRTS 备注: "此技能的“拖拽”机制实际为反方向(指向薄绿方向)的推开" — a radial push towards her by the
// official 力度 − 重量 push distance (小力 vs weight 1: 0.44 tiles), never past her (Battle.push inward). Her
// attack reaches air units (阵法术师 "攻击时可对空"), but the drones of the mode are 静态刚体 (PRTS 特殊机制): hit,
// never dragged (player report after 0.1.0, "飞机可以被薄绿的技能拉走")
if (battle.push(target, pullForce, { from: unit, inward: true }) > 0) fx(battle, 'pull', target, { src: unit.id });
},
},
onStart: guardOn,
onEnd(ctx) {
const { battle, unit, reason } = ctx;
if (reason !== 'death' && unit.alive && endScale > 0) {
for (const e of enemiesOn(battle, unit, unit.rangeKeys)) battle.dealDamage(unit, e, { amount: unit.s.atk * endScale, type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
fx(battle, 'aoe', unit, { radius: 2, dmgType: 'arts', skill: 'mint_2' });
}
guardOff(ctx);
},
},
talents: [{ install(battle, unit) {
unit.mem.mintAura = aura(battle, unit, {
key: 'talent:mint_geo', side: 'ally', tiles: () => gridKeys(tGrid, unit), active: () => !unit.skill?.active, mods: { defPct: num(t0.def) },
});
} }],
};
},
};
@@ -0,0 +1,57 @@
// server/sim/content/kits/ops/chess_char_3_09-haini.js — 海霓 (char_4079_haini) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, instantKindOf, alive, onTiles, aura } from '../shared/tier3.js';
/** Non-elite, non-leader enemy (海霓 "非精英和领袖敌人"). */
const isNormalEnemy = (e) => !e.isBoss && String(e.def?.rank ?? 'NORMAL').toUpperCase() === 'NORMAL';
export default {
// ---- 3_09 海霓 · 削弱者 — S2 阻滞性显色剂: ATK +, 2 targets, slows non-elite enemies in range, their deaths raise the
// talent up to max_talent_up; 测绘器材的奇用: non-elite enemies in range are fragile
chess_char_3_09_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const slow = num(bb['attack@move_speed'], 0);
const up = num(bb['attack@talent_up'], 0), upMax = num(bb['attack@max_talent_up'], 1);
const mt = Math.floor(num(bb['attack@max_target'], 1));
const kit = {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) {
unit.mem.hainiMul = 1;
if (!unit.mem.hainiSlow) {
unit.mem.hainiSlow = aura(battle, unit, { key: 'skill:haini_slow', side: 'enemy', interval: 0, tiles: () => unit.rangeKeySet, filter: isNormalEnemy, mods: { moveMul: Math.max(0, 1 + slow) } });
}
unit.mem.hainiSlow();
},
onTick({ unit }) { unit.mem.hainiSlow?.(); },
onEnd({ unit }) { unit.mem.hainiSlow?.clear(); unit.mem.hainiMul = 1; },
},
// S1 迷惑性洋流图: next attack ×atk_scale on max_target enemies at once
skills: altSkills(chess, d, bb, {
skchr_haini_1: (s) => ({
kind: instantKindOf(s),
targeting: { maxTargets: Math.max(1, Math.floor(num(s.bb.max_target, 1))) },
attack: { atkScale: num(s.bb.atk_scale, 1) },
}),
}),
talents: [{ install(battle, unit) {
unit.mem.hainiMul = 1;
aura(battle, unit, {
key: 'talent:haini_fragile', side: 'enemy', tiles: () => unit.rangeKeySet, filter: isNormalEnemy,
mods: () => ({ dmgTakenMul: 1 + (num(t0.damage_scale, 1) - 1) * (unit.mem.hainiMul ?? 1) }),
});
battle.on('kill', (ctx) => {
const v = ctx.victim;
if (!unit.skill?.active || !alive(unit) || v.side !== 'enemy' || !isNormalEnemy(v)) return;
if (!unit.rangeKeySet || !onTiles(v, unit.rangeKeySet)) return;
unit.mem.hainiMul = Math.min(upMax, (unit.mem.hainiMul ?? 1) + up);
}, { owner: unit });
} }],
};
if (mt > 1) kit.skill.targeting = { maxTargets: mt };
return kit;
},
};
@@ -0,0 +1,54 @@
// server/sim/content/kits/ops/chess_char_3_10-pinecn.js — 松果 (char_440_pinecn) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, selectedId, altSkills, instantKindOf, copyGrid } from '../shared/tier3.js';
export default {
// ---- 3_10 松果 · 散射手 — S2 电能过载: ATK + growing with each use (a→d), shorter range; 便携电源: SP regen for 60 s
chess_char_3_10_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const sel = selectedId(chess, d);
const steps = Object.keys(bb).filter((k) => /\[[a-z]\]\.atk$/.test(k)).sort().map((k) => num(bb[k]));
if (!steps.length) steps.push(num(bb.atk));
const skillGrid = copyGrid(d.skill?.rangeGrid);
const kit = {
skill: {
kind: 'duration',
onStart({ battle, unit, skill }) {
const atk = steps[Math.min(steps.length - 1, Math.max(0, skill.activations - 1))];
battle.addBuff(unit, { key: 'skill:pinecn_atk', mods: { atkPct: atk } });
},
onEnd({ battle, unit }) { battle.removeBuff(unit, 'skill:pinecn_atk'); },
},
// S1 RMA长钉 (charges): "立即以165%的攻击力进行一次攻击,无视敌人180的防御力" — an extra shot fired at once (all
// enemies in range, front-row × of the trait), on top of the normal attack it was cast before
skills: altSkills(chess, d, bb, {
skchr_pinecn_1: (s) => ({
kind: instantKindOf(s),
// (the DEF ignore rides on the shots' tag: they land after the instant skill has ended)
attack: { atkScale: num(s.bb.atk_scale, 1), tags: ['skill', 'pinecnSpike'] },
onStart({ battle, unit }) { battle.forceAttack(unit); },
}),
}),
talents: [{ install(battle, unit) {
battle.on('deploy', (ctx) => {
if (ctx.unit === unit) battle.addBuff(unit, { key: 'talent:pinecn_power', duration: num(t0.duration, 60), mods: { spRecoveryFlat: num(t0.sp_recovery_per_sec) } });
}, { owner: unit });
} }],
};
if (sel === 'skchr_pinecn_1') {
const pen = num(bb.def_penetrate_fixed, 0);
kit.install = (battle, unit) => battle.on('hit', (ctx) => {
if (ctx.source === unit && pen > 0 && ctx.dmg.tags?.includes('pinecnSpike')) ctx.dmg.defIgnoreFlat += pen;
}, { owner: unit });
}
if (skillGrid) {
kit.skill.targeting = { rangeGrid: skillGrid };
// "攻击范围缩短": offensive skills fire with an enemy inside the SKILL range (research 03 §1.4) — with the
// initial (wider) range she would open the skill on enemies the shortened range cannot reach
kit.skill.trigger = { rule: 'CUSTOM_RANGE', grid: skillGrid };
}
return kit;
},
};
@@ -0,0 +1,74 @@
// server/sim/content/kits/ops/chess_char_3_11-snhunt.js — 雪猎 (char_4211_snhunt) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import {
num, defOf, talentBb, traitBb, selectedId, altSkills, instantKindOf, alive, fx, textNum, enemiesOn,
} from '../shared/tier3.js';
export default {
// ---- 3_11 雪猎 · 猎手 — S2 风雪连弩: instant special-bullet double shot (higher vs non-moving targets), charges;
// 裂云一击: the skill also sends 裂云兽 (ATK% phys + cold); 精锐 module HUN-X: reload from empty adds extra rounds
// S1 强力击·β型: next attack ×atk_scale (a normal shot: needs a round); 裂云兽 hits the enemy it is cast on
chess_char_3_11_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const sel = selectedId(chess, d);
const S2 = 'skchr_snhunt_2';
const s1 = num(bb.atk_scale_1, 1), s2 = num(bb.atk_scale_2, s1);
const charges = Math.max(1, Math.floor(num(d.skill?.maxCharges, num(bb.ct, 1))));
const shots = textNum(d.skill?.description, /(\d+|[一二两三四五])连击/, 2);
const still = (e) => !!(e.blockedBy || e.s.flags.stun || e.s.flags.noMove || !e.moving);
const extra = num(tb.extra_add, 0);
return {
skill: {
kind: charges > 1 ? 'charges' : 'instant',
onStart({ battle, unit }) {
const t = enemiesOn(battle, unit, unit.rangeKeys, 1, unit.profile)[0] ?? null;
unit.mem.snTarget = t;
if (!t) return;
battle.addProjectile({
from: unit, target: t, speed: 16, visual: 'arrow', source: unit,
onHit: ({ target }) => {
if (!target || !target.alive) return;
const sc = still(target) ? s2 : s1;
for (let i = 0; i < shots && target.alive; i++) battle.dealDamage(unit, target, { amount: unit.s.atk * sc, type: 'phys', isSkill: true, tags: ['skill', 'snhunt'] });
},
});
fx(battle, 'volley', unit, { target: t.id, skill: 'snhunt_2' });
},
},
skills: altSkills(chess, d, bb, {
skchr_snhunt_1: (s) => ({ kind: instantKindOf(s), attack: { atkScale: num(s.bb.atk_scale, 1) } }),
}),
talents: [{ install(battle, unit) {
battle.on('skillStart', (ctx) => {
if (ctx.unit !== unit) return;
// S2 aims its special bullets itself (mem.snTarget); another skill is cast on the enemy about to be shot
const t = sel === S2 || sel == null ? unit.mem.snTarget : (enemiesOn(battle, unit, unit.rangeKeys, 1, unit.profile)[0] ?? null);
if (!t || !t.alive) return;
battle.dealDamage(unit, t, { amount: unit.s.atk * num(t0.atk_scale, 1), type: 'phys', isSkill: true, tags: ['talent', 'cloudbeast'] });
if (t.alive) battle.applyStatus(t, 'cold', { duration: num(t0.cold, 3), source: unit });
fx(battle, 'strike', t, { src: unit.id, talent: 'snhunt_beast' });
}, { owner: unit });
} }],
install(battle, unit) {
// special bullets (S2 only): the skill also fires while the magazine is empty (the attack loop cannot run then)
if (sel === S2 || sel == null) {
battle.on('tick', () => {
const sk = unit.skill;
if (!alive(unit) || !unit.canAct || !sk || !sk.ready || sk.opCooling || unit.s.flags.silence || (unit.trait.ammo ?? 1) > 0) return;
if (battle.enemiesInKeys(unit.baseRangeKeys || unit.rangeKeys, unit, unit.profile).length) sk.activate('DEFAULT');
}, { owner: unit });
}
if (extra > 0) {
battle.on('tick', () => {
const a = unit.trait.ammo ?? 0;
if (unit.mem.prevAmmo === 0 && a === 1) unit.trait.ammo = Math.min(Math.max(1, unit.profile.ammoMax ?? 8), a + extra);
unit.mem.prevAmmo = unit.trait.ammo;
}, { owner: unit });
}
},
};
},
};
@@ -0,0 +1,127 @@
// server/sim/content/kits/ops/chess_char_3_12-blemsh.js — 瑕光 (char_423_blemsh) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { bodyOnTile } from '../../../body.js';
import {
num, defOf, talentBb, traitBb, altSkills, instantKindOf, alive, onTiles, gridKeys, fx, copyGrid, NINE, isLeader,
giveSp,
} from '../shared/tier3.js';
export default {
// ---- 3_12 瑕光 · 守护者 — S3 先贤化身: ATK/DEF +, bonus arts per hit, heals another nearby ally per attack;
// 剑盾骑士: hurt-SP skills of the team also gain SP on attack; 仁慈: attacks sleeping enemies (first, ×atk_scale);
// 精锐 module GUA-Y: damage taken −15 %
// S1 光芒涌动 (自动触发 ⇒ DEFAULT, charges): next attack ×atk_scale phys + heals the most injured ally of the 3×3 (herself
// included) for heal_scale × ATK; S2 慑敌辉光: ATK +, puts every ground enemy on her own tile and every enemy she
// blocks to sleep (PRTS 备注; for the skill's duration: no own value in the data) and heals every ally of the
// skill range by ATK × ratio each second;
// 精锐 module GUA-X: her heals on allies under hp_ratio HP × heal_scale
chess_char_3_12_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const bonus = num(bb['attack@blemsh_s_3_extra_dmg[magic].atk_scale'], 0);
const heal = num(bb.heal_scale, 0);
const healGrid = copyGrid(d.skill?.rangeGrid) ?? NINE;
const mercy = num(t1.atk_scale, 1);
const sleepersInRange = (battle, unit) => {
const set = unit.rangeKeySet;
return set ? battle.enemies.filter((e) => e.alive && !e.hidden && e.s.flags.sleep && !e.s.flags.untargetable && !e.isFlying && onTiles(e, set)) : [];
};
const kit = {
trait: { hitSleep: true }, // 仁慈: 自身可以攻击…沉睡的目标 (沉睡 = 无敌 for everyone else)
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
heal: false,
attack: { onHit({ battle, unit, target }) {
if (bonus > 0 && target && target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * bonus, type: 'arts', isSkill: true, tags: ['skill', 'blemshBonus'] });
} },
onAttack({ battle, unit }) {
if (!(heal > 0)) return;
const ally = battle.injuredAlliesInKeys(new Set(gridKeys(healGrid, unit)), unit).find((a) => a !== unit);
if (ally) battle.heal(unit, ally, unit.s.atk * heal);
},
},
skills: altSkills(chess, d, bb, {
skchr_blemsh_1: (s) => {
const grid = copyGrid(s.rangeGrid) ?? NINE;
const hs = num(s.bb.heal_scale, 0);
return {
kind: instantKindOf(s),
heal: false,
attack: {
atkScale: num(s.bb.atk_scale, 1),
onHit({ battle, unit }) { // "恢复周围一名友方单位"
const ally = hs > 0 ? battle.injuredAlliesInKeys(new Set(gridKeys(grid, unit)), unit)[0] : null;
if (ally) battle.heal(unit, ally, unit.s.atk * hs);
},
},
};
},
skchr_blemsh_2: (s) => {
const grid = copyGrid(s.rangeGrid) ?? NINE;
const ratio = num(s.bb['attack@atk_to_hp_recovery_ratio'], num(s.bb.atk_to_hp_recovery_ratio, 0));
return {
kind: 'duration',
heal: false,
mods: { atkPct: num(s.bb.atk) },
onStart({ battle, unit, skill }) {
const dur = skill.timeLeft > 0 ? skill.timeLeft : Math.max(0.1, num(s.duration, 10));
let n = 0;
// PRTS 备注: "技能生效对象实际为“自身这格内的所有地面敌人及自身阻挡的敌人”,即使阻挡的是飞行敌人" — a blocked
// enemy stands at the block radius, outside her tile (Battle._checkBlock)
for (const e of battle.enemies) {
if (!e.alive || e.hidden) continue;
if (e.blockedBy !== unit && (e.isFlying || !bodyOnTile(e, unit.tileR, unit.tileC))) continue;
if (battle.applyStatus(e, 'sleep', { duration: dur, source: unit })) n++;
}
fx(battle, 'aoe', unit, { radius: 0.5, skill: 'blemsh_2', status: 'sleep', n });
unit.mem.blemshRegen = 0;
},
onTick({ battle, unit, dt }) { // "周围的所有友方单位每秒恢复相当于攻击力N%的生命值"
if (!(ratio > 0)) return;
unit.mem.blemshRegen = (unit.mem.blemshRegen ?? 0) + dt;
while (unit.mem.blemshRegen >= 1 - 1e-9) {
unit.mem.blemshRegen -= 1;
for (const a of battle.injuredAlliesInKeys(new Set(gridKeys(grid, unit)), unit)) battle.heal(unit, a, unit.s.atk * ratio);
}
},
};
},
}),
talents: [
{ install(battle, unit) {
unit.mem.blemshKnight = true;
battle.on('attack', (ctx) => {
const a = ctx.attacker;
if (!a || a.kind !== 'op' || a.ownerId !== unit.ownerId || a.skill?.spType !== 'hurt') return;
if (!alive(unit) || !isLeader(battle, unit, 'blemshKnight')) return;
giveSp(a, num(t0.sp, 1), 'talent');
}, { owner: unit });
} },
{ install(battle, unit) {
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit) return;
const s = sleepersInRange(battle, unit);
if (s.length) ctx.targets = [s[0], ...ctx.targets.filter((t) => t !== s[0])].slice(0, Math.max(1, ctx.targets.length));
}, { owner: unit });
battle.on('hit', (ctx) => {
if (ctx.source === unit && ctx.target.side === 'enemy' && ctx.target.s.flags.sleep) ctx.dmg.mul *= mercy;
}, { owner: unit });
// (a lone sleeper is found by the engine's own target search: `trait.hitSleep` makes sleepers targetable
// and damageable for her only)
} },
],
};
if (num(tb.damage_resistance) > 0) {
kit.install = (battle, unit) => battle.addBuff(unit, { key: 'trait:blemsh_guard', mods: { dmgTakenMul: 1 - num(tb.damage_resistance) }, persist: true, allowDead: true });
} else if (num(tb.heal_scale) > 0) {
// GUA-X "治疗生命值低于50%的友方单位时治疗量提升15%" (the HP before the heal)
kit.install = (battle, unit) => battle.on('heal', (ctx) => {
if (ctx.source === unit && ctx.target && ctx.target.hpRatio < num(tb.hp_ratio, 0.5)) ctx.amount *= num(tb.heal_scale, 1);
}, { owner: unit });
}
return kit;
},
};
@@ -0,0 +1,25 @@
// server/sim/content/kits/ops/chess_char_3_13-malist.js — 至简 (char_4054_malist) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, statSkill, textNum, funnelMap, installFunnelPrune } from '../shared/tier3.js';
export default {
// ---- 3_13 至简 · 驭械术师 — S2 神工意匠: next attack ATK% arts twice (charges); 忽有所悟: prob of ATK ×atk_scale per attack
chess_char_3_13_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const charges = Math.max(1, Math.floor(num(d.skill?.maxCharges, num(bb.ct, 1))));
const hits = textNum(d.skill?.description, /连续攻击(\d+|[一二两三四五])次/, 2);
const insight = (battle) => (battle.rng.chance(num(t0.prob, 0)) ? num(t0.atk_scale, 1) : 1);
return {
trait: { dmgMul: (battle, unit, target) => funnelMap(battle, unit, target) * insight(battle) },
skill: {
kind: charges > 1 ? 'charges' : 'instant',
attack: { atkScale: num(bb.atk_scale, 1), hits, dmgType: 'arts', dmgMul: (battle) => insight(battle) },
},
// S1 迅捷打击·γ型: ATK / ASPD + (the drones' ramp and 忽有所悟 stay on every attack through the trait)
skills: altSkills(chess, d, bb, { 'skcom_quickattack[3]': statSkill }),
install: installFunnelPrune,
};
},
};
@@ -0,0 +1,55 @@
// server/sim/content/kits/ops/chess_char_3_14-slbell.js — 初雪 (char_174_slbell) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, aura } from '../shared/tier3.js';
export default {
// ---- 3_14 初雪 · 削弱者 — S2 自然震慑: DEF/RES shred aura on every enemy in range; 虚弱化: low-HP enemies fragile;
// 双响: 2 targets
chess_char_3_14_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const mr = num(bb.magic_resistance, 0);
const shred = { defPct: num(bb.def) };
if (mr) {
if (Math.abs(mr) < 1) shred.resMul = Math.max(0, 1 + mr);
else shred.resFlat = mr;
}
const mt = Math.floor(num(t1['attack@max_target'], 1));
const kit = {
skill: {
kind: 'duration',
onStart({ battle, unit }) {
if (!unit.mem.slbellAura) unit.mem.slbellAura = aura(battle, unit, { key: 'skill:slbell_shred', side: 'enemy', interval: 0, tiles: () => unit.rangeKeySet, mods: shred });
unit.mem.slbellAura();
},
onTick({ unit }) { unit.mem.slbellAura?.(); },
onEnd({ unit }) { unit.mem.slbellAura?.clear(); },
},
// S1 传音回响: 2 targets, every enemy in range ASPD + attack_speed (negative) while it runs
skills: altSkills(chess, d, bb, {
skchr_slbell_1: (s) => {
const slow = { aspd: num(s.bb.attack_speed) };
return {
kind: 'duration',
targeting: { maxTargets: Math.max(1, Math.floor(num(s.bb['attack@max_target'], num(s.bb.max_target, 1)))) },
onStart({ battle, unit }) {
if (!unit.mem.slbellSlow) unit.mem.slbellSlow = aura(battle, unit, { key: 'skill:slbell_aspd', side: 'enemy', interval: 0, tiles: () => unit.rangeKeySet, mods: slow });
unit.mem.slbellSlow();
},
onTick({ unit }) { unit.mem.slbellSlow?.(); },
onEnd({ unit }) { unit.mem.slbellSlow?.clear(); },
};
},
}),
talents: [{ install(battle, unit) {
aura(battle, unit, {
key: 'talent:weak_fragile', side: 'enemy', tiles: () => unit.rangeKeySet, filter: (e) => e.hpRatio < num(t0.hp_ratio, 0.4),
mods: { dmgTakenMul: num(t0.damage_scale, 1) }, interval: 0.1,
});
} }],
};
if (mt > 1) kit.trait = { maxTargets: mt };
return kit;
},
};
@@ -0,0 +1,29 @@
// server/sim/content/kits/ops/chess_char_3_15-vodfox.js — 巫恋 (char_254_vodfox) kit, tier 3 (hidden).
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { releaseSkillSummon } from '../../tokens.js';
import { num, defOf, talentBb, aura } from '../shared/tier3.js';
export default {
// ---- 3_15 巫恋 · 削弱者 (hidden) — S2 诅咒娃娃 "获得一个诅咒娃娃(最多可库存1个)": the doll is a hand piece the player
// places (user playtest #6; PRTS 卫戍协议/帮助); each cast gives one and the placed piece takes the field on its
// own tile (tokens.js releaseSkillSummon: not at the battle start; not placed ⇒ no doll); its token kit keeps the
// 3×3 ATK/DEF aura (the token skill's bb) for 15 s; 溃败暗示: low-HP enemies fragile
chess_char_3_15_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tokenId = chess?.skill?.overrideTokenKey ?? (d.tokens || []).find((t) => /doll/.test(String(t))) ?? 'token_10006_vodfox_doll';
return {
skill: {
kind: 'instant',
onStart({ battle, unit }) { releaseSkillSummon(battle, unit, tokenId); },
},
talents: [{ install(battle, unit) {
aura(battle, unit, {
key: 'talent:weak_fragile', side: 'enemy', tiles: () => unit.rangeKeySet, filter: (e) => e.hpRatio < num(t0.hp_ratio, 0.4),
mods: { dmgTakenMul: num(t0.damage_scale, 1) }, interval: 0.1,
});
} }],
};
},
};
@@ -0,0 +1,26 @@
// server/sim/content/kits/ops/chess_char_3_16-snakek.js — 蛇屠箱 (char_150_snakek) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, statSkill, whileTrue } from '../shared/tier3.js';
export default {
// ---- 3_16 蛇屠箱 · 铁卫 — S2 壳状防御: stops attacking, block +1, DEF +, regen; 防御专精: DEF +;
// 精锐 module PRO-X: DEF + while blocking
chess_char_3_16_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const kit = {
skill: {
kind: 'duration',
mods: { defPct: num(bb.def), blockCnt: num(bb.block_cnt), hpRegenRatio: num(bb.hp_recovery_per_sec_by_max_hp_ratio) },
attack: { noAttack: true },
},
// S1 防御力强化·β型 (TAKE_DAMAGE): DEF + (she keeps attacking)
skills: altSkills(chess, d, bb, { 'skcom_def_up[2]': statSkill }),
talents: [{ install(battle, unit) { battle.addBuff(unit, { key: 'talent:snakek_def', mods: { defPct: num(t0.def) }, persist: true, allowDead: true }); } }],
};
if (num(tb.def) > 0) kit.install = (battle, unit) => whileTrue(battle, unit, 'trait:snakek_block', () => unit.blocking.length > 0, { defPct: num(tb.def) });
return kit;
},
};
@@ -0,0 +1,41 @@
// server/sim/content/kits/ops/chess_char_3_17-shotst.js — 流星 (char_126_shotst) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, instantKindOf, fx, textNum, enemiesOn } from '../shared/tier3.js';
export default {
// ---- 3_17 流星 · 速射手 — S2 碎甲击·扩散: instant ATK% phys on ≤5 enemies in range + DEF shred for `duration` s;
// 空射专精: ×atk_scale vs flying (stacks with the module fly bonus)
chess_char_3_17_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const n = textNum(d.skill?.description, /至多(\d+)个/, 5);
const flyMul = (unit, t) => (t.isFlying ? (unit.profile.flyScale ?? 1) * num(t0.atk_scale, 1) : 1);
return {
trait: { dmgMul: (battle, unit, target) => flyMul(unit, target) },
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const list = enemiesOn(battle, unit, unit.rangeKeys, n, unit.profile);
for (const e of list) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1) * flyMul(unit, e), type: 'phys', isSkill: true, tags: ['skill', 'burst'] });
if (e.alive) battle.addBuff(e, { key: 'skill:shotst_shred', duration: num(bb.duration, 5), refresh: 'extend', mods: { defPct: num(bb.def) }, source: unit, visible: true });
}
if (list.length) fx(battle, 'volley', unit, { targets: list.map((e) => e.id), skill: 'shotst_2' });
},
},
// S1 碎甲击: next attack ×atk_scale (fly × of the trait kept) and the target's DEF − for `duration` s
skills: altSkills(chess, d, bb, {
skchr_shotst_1: (s) => ({
kind: instantKindOf(s),
attack: {
atkScale: num(s.bb.atk_scale, 1),
onHit({ battle, unit, target }) {
if (target && target.alive && target.side === 'enemy') battle.addBuff(target, { key: 'skill:shotst_shred', duration: num(s.bb.duration, 5), refresh: 'extend', mods: { defPct: num(s.bb.def) }, source: unit, visible: true });
},
},
}),
}),
};
},
};
@@ -0,0 +1,127 @@
// server/sim/content/kits/ops/chess_char_3_18-vulpis.js — 忍冬 (char_4026_vulpis) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import {
num, defOf, talentBb, traitBb, altSkills, instantKindOf, alive, gridKeys, fx, copyGrid, enemiesOn, whileTrue,
isLeader,
} from '../shared/tier3.js';
/** 忍冬 S3's 迷彩 (until her next cast): its own buff key, never merged with another unit status. */
const CAMOU_KEY = 'vulpis:camou';
export default {
// ---- 3_18 忍冬 · 尖兵 — S3 隐狐之艺: +DP, range +1, ATK +, decaying ASPD, hits all blocked, 0.2 s stun per hit,
// after a kill during the skill 迷彩 from its end until the next cast ("进入迷彩状态,直至下一次开启技能") — the
// `camou` status under its own key (ba.camou "不阻挡时不成为敌方普通攻击的目标": only the enemy she blocks attacks
// her, as with 隐匿; it used to be the generic `stealth` = 隐匿 under the key `stealth`, shared with 伪装服's timed
// 隐匿, which the unending one then made permanent, and it counted as 隐匿 for 叙拉古 / 家族徽章 — user playtest #6
// "叙拉古阵营隐身不会结束": the bond's 隐匿 ends on time, this 迷彩 officially lasts); 追凶: bonus arts on marked
// enemies; 蓄势: DP regen + out-of-combat
// regen; 精锐 module SOL-X: ATK/DEF + while blocking
// S1 小施惩戒 (charges): next attack + extra_damage_ratio × ATK arts and +cost DP; S2 坠刃拷问 (charges, the data rule
// SKILL_RANGE: fires once an enemy is inside its own range 3-12, charges 3 s apart): +cost DP, ≤ max_target enemies of that range take atk_scale arts and
// 停顿 — the ones already 停顿 are also stunned. (SOL-Y "首次部署时部署费用-4": the initial deployment is free.)
chess_char_3_18_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const skillGrid = copyGrid(d.skill?.rangeGrid);
const aspd0 = num(bb.attack_speed, 0);
const kit = {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
attack: { hitAllBlocked: true, onHitStatus: { key: 'stun', duration: num(bb['attack@stun'], 0.2) } },
onStart({ battle, unit, skill }) {
battle.removeBuff(unit, CAMOU_KEY);
unit.mem.vulpisKill = false;
const dp = num(bb.cost, 0);
if (dp > 0) { battle.addDp(unit.ownerId, dp); fx(battle, 'dp', unit, { n: dp }); }
battle.addBuff(unit, { key: 'skill:vulpis_aspd', mods: { aspd: aspd0 } });
unit.mem.vulpisDur = skill.timeLeft;
},
onTick({ battle, unit, skill }) {
const b = unit.findBuff('skill:vulpis_aspd');
const dur = unit.mem.vulpisDur || skill.duration || 1;
if (b) { b.mods = { aspd: aspd0 * Math.max(0, skill.timeLeft) / dur }; unit.markDirty(); }
},
onEnd({ battle, unit, reason }) {
battle.removeBuff(unit, 'skill:vulpis_aspd');
if (reason !== 'death' && unit.alive && unit.mem.vulpisKill) {
battle.addBuff(unit, { key: CAMOU_KEY, flags: { camou: true }, status: 'camou', source: unit });
fx(battle, 'camouflage', unit);
}
},
},
skills: altSkills(chess, d, bb, {
skchr_vulpis_1: (s) => ({
kind: instantKindOf(s),
attack: { onHit({ battle, unit, target }) {
if (target && target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * num(s.bb.extra_damage_ratio), type: 'arts', isSkill: true, tags: ['skill', 'vulpisPunish'] });
const dp = num(s.bb.cost, 0);
if (dp > 0) { battle.addDp(unit.ownerId, dp); fx(battle, 'dp', unit, { n: dp }); }
} },
}),
skchr_vulpis_2: (s) => { // 坠刃拷问 — air units too (PRTS 备注 "※可对空"); data trigger SKILL_RANGE (its 3-12 range)
const grid = copyGrid(s.rangeGrid);
const n = Math.max(1, Math.floor(num(s.bb.max_target, 6)));
return {
kind: instantKindOf(s),
onStart({ battle, unit }) {
const dp = num(s.bb.cost, 0);
if (dp > 0) { battle.addDp(unit.ownerId, dp); fx(battle, 'dp', unit, { n: dp }); }
const list = enemiesOn(battle, unit, gridKeys(grid ?? unit.rangeGrid, unit), n);
for (const e of list) {
const was = !!e.findBuff('sluggish'); // "若敌人已经处于停顿状态" (before this cast)
battle.dealDamage(unit, e, { amount: unit.s.atk * num(s.bb.atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill', 'vulpisTorture'] });
if (!e.alive) continue;
battle.applyStatus(e, 'sluggish', { duration: num(s.bb.sluggish, 0), source: unit });
if (was && e.alive) battle.applyStatus(e, 'stun', { duration: num(s.bb.stun, 0), source: unit });
}
if (list.length) fx(battle, 'aoe', unit, { radius: 1.5, dmgType: 'arts', skill: 'vulpis_2', n: list.length });
},
};
},
}),
talents: [
{ install(battle, unit) { // 追凶
const marks = new Map();
// re-entrancy guard: content that turns a hit into HP loss credited to her (频次 enemies: every instance
// = 1 HP) would otherwise feed the bonus back into itself until the hook-depth limit
let busy = false;
battle.on('damaged', (ctx) => {
const e = ctx.target;
if (busy || ctx.source !== unit || e.side !== 'enemy' || ctx.type === 'element' || ctx.dmg?.tags?.includes('vulpisHunt')) return;
const t = marks.get(e.id);
if (t == null) { marks.set(e.id, battle.time); return; }
if (battle.time - t <= num(t0.interval, 10) + 1e-9 && e.alive) {
busy = true;
// PRTS 备注 "伤害类型为法术附加伤害": tag addition (no 叙拉古 6 roll)
try { battle.dealDamage(unit, e, { amount: unit.s.atk * num(t0.atk_scale), type: 'arts', canDodge: false, tags: ['talent', 'vulpisHunt', 'addition'] }); } finally { busy = false; }
}
}, { owner: unit });
battle.on('kill', (ctx) => { if (ctx.killer === unit && unit.skill?.active) unit.mem.vulpisKill = true; }, { owner: unit });
} },
{ install(battle, unit) { // 蓄势
unit.mem.vulpisDp = true;
const bonus = num(t1.delta_cost_increase_time, 1) - 1;
const quiet = num(t1.interval, 4);
const regen = num(t1['vulpis_t_2[heal][interval].hp_recovery_per_sec_by_max_hp_ratio'], 0);
battle.on('tick', (ctx) => {
if (!alive(unit)) return;
if (bonus > 0 && isLeader(battle, unit, 'vulpisDp')) battle.addDp(unit.ownerId, battle.flags.dpPerSec * ctx.dt * bonus);
const on = battle.time - unit.lastHitAt >= quiet && regen > 0;
const has = !!unit.findBuff('talent:vulpis_regen');
if (on && !has) battle.addBuff(unit, { key: 'talent:vulpis_regen', mods: { hpRegenRatio: regen } });
else if (!on && has) battle.removeBuff(unit, 'talent:vulpis_regen');
}, { owner: unit });
} },
],
};
if (skillGrid) kit.skill.targeting = { rangeGrid: skillGrid };
if (num(tb.atk) > 0 || num(tb.def) > 0) {
kit.install = (battle, unit) => whileTrue(battle, unit, 'trait:vulpis_block', () => unit.blocking.length > 0, { atkPct: num(tb.atk), defPct: num(tb.def) });
}
return kit;
},
};
@@ -0,0 +1,240 @@
// server/sim/content/kits/ops/chess_char_3_19-vigil.js — 伺夜 (char_427_vigil) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, selectedId, altSkills, alive, fx, textNum, tacticalPoint } from '../shared/tier3.js';
/** Tokens `tokenId` summoned by / placed for `owner` (board pieces included). */
const tokensOf = (battle, owner, tokenId) => battle.allyUnits.filter((t) => t.kind === 'token' && t.defId === tokenId && t.ownerUnit === owner && !t.mem.isClone);
export default {
// ---- 3_19 伺夜 · 战术家 — the tactical reinforcement is the wolf pack (狼群领袖: 2 wolves, +1 every 25 s up to 3, each
// wolf = +1 block and one more bite, a wolf is lost instead of the pack dying); 狼群天性: DEF ignore vs pack-blocked
// enemies; S3 领袖的尊严: DP over time, 三连击, bonus arts vs pack-blocked enemies; 精锐 module: pack takes less
// damage from the enemies it blocks (token module talent). A 狼群 piece placed in the prep phase is the pack.
// S1 领袖的呼唤 (ALWAYS): +cost DP and one more “狼影” (≤ the talent's maximum); S2 领袖的馈赠: +cost DP, the pack
// recovers hp_ratio of its max HP and its next attack hits ×atk_scale — a kill by that attack gives +cost DP.
// 精锐 module TAC-Y: trait ×165 % (profession layer) and "援军阻挡的敌人更容易受到我方的攻击": the pack's token module
// talent taunt_level (+1) goes to the enemies it blocks — the enemy-side 嘲讽等级 our operators target first
// (targeting.js; research 05: "更容易受到我方的攻击" = enemy taunt.taunt_level), never to the pack itself.
chess_char_3_19_a: (bb, chess, def) => {
const d = defOf(chess, def);
const sel = selectedId(chess, d);
const t0 = d.talents?.[0] ?? {}, t1 = talentBb(d, 1);
const wolfId = t0.tokenKey ?? (d.tokens || []).find((t) => /wolf/.test(String(t))) ?? 'token_10028_vigil_wolf';
const initial = textNum(t0.description, /初始(\d+|[一二两三四五])只/, 2);
const maxWolves = textNum(t0.description, /至多(\d+|[一二两三四五])只/, 3);
const hits = textNum(d.skill?.description, /(\d+|[一二两三四五])连击/, 3);
const bonus = num(bb['attack@vigil_s_3.atk_scale'], 0);
const pen = num(t1.def_penetrate_fixed, 0);
const wolfOf = (u) => (u.trait.reinforcement && u.trait.reinforcement.alive ? u.trait.reinforcement : null);
const wolfKit = (vigil) => ({
skill: null,
trait: { hitsFn: (b, w) => Math.max(1, w.mem.wolves || 1) },
install(battle, w) {
const tal = w.def.talents || [];
const wb = tal[0]?.bb ?? {};
const per = num(wb.block_cnt, num(wb['vigil_wolf_t_1_enhance[trigger].block_cnt'], 1));
const grow = num(wb.interval, num(wb['vigil_wolf_t_1_enhance[trigger].interval'], 25));
// (mem.shadows mirrors the count for content reading tokens.js wolfShadows())
const apply = () => { w.mem.shadows = w.mem.wolves; battle.addBuff(w, { key: 'token:wolves', mods: { blockCnt: per * w.mem.wolves }, allowDead: true }); };
w.mem.wolves = Math.max(1, Math.min(maxWolves, initial));
apply();
w.mem.addWolf = () => {
if (!w.alive || w.mem.wolves >= maxWolves) return false;
w.mem.wolves++;
apply();
fx(battle, 'summon', w, { src: vigil.id, wolves: w.mem.wolves });
return true;
};
battle.every(grow, () => w.mem.addWolf(), { owner: w });
battle.on('fatal', (ctx) => {
if (ctx.unit !== w || ctx.prevented || w.mem.wolves <= 1) return;
w.mem.wolves--;
apply();
ctx.prevented = true;
w.hp = w.s.maxHp;
fx(battle, 'revive', w, { wolves: w.mem.wolves });
}, { owner: w });
const mod = tal.find((t) => t && t.bb && t.bb.damage_scale != null);
if (mod) {
battle.on('hit', (ctx) => {
if (ctx.target === w && ctx.source && ctx.source.blockedBy === w) ctx.dmg.mul *= num(mod.bb.damage_scale, 1);
}, { owner: w });
}
},
});
// TAC-Y token module talent (taunt_level) of the pack — whichever kit runs it (this one or a tokens.js board piece):
// the enemies it blocks carry that taunt level while they stay blocked (short refreshed buff: nothing lingers when
// the pack or 伺夜 leaves)
const packTaunt = (w) => {
if (w.mem.packTaunt == null) {
const t = (w.def?.talents || []).find((x) => x && x.bb && x.bb.taunt_level != null);
w.mem.packTaunt = t ? num(t.bb.taunt_level) : 0;
}
return w.mem.packTaunt;
};
const installPackMark = (battle, unit) => {
let cur = new Set();
const levelOf = (w) => (w && alive(w) && alive(unit) ? packTaunt(w) : 0);
const mark = (e, w, lvl) => battle.addBuff(e, { key: 'token:pack_mark', duration: 0.25, refresh: 'extend', mods: { taunt: lvl }, source: w, visible: true });
// at once when the pack blocks (the operators attacking this tick already see it), kept / dropped every 0.1 s
battle.on('blocked', (ctx) => {
const w = wolfOf(unit), lvl = ctx.blocker === w ? levelOf(w) : 0;
if (lvl && ctx.enemy.alive) { mark(ctx.enemy, w, lvl); cur.add(ctx.enemy); }
}, { owner: unit });
battle.every(0.1, () => {
const w = wolfOf(unit);
const lvl = levelOf(w);
const next = new Set();
if (lvl) {
for (const e of w.blocking) {
if (!e.alive || e.side !== 'enemy' || e.blockedBy !== w) continue;
mark(e, w, lvl);
next.add(e);
}
}
for (const e of cur) {
if (next.has(e)) continue;
const b = e.findBuff('token:pack_mark');
if (b) battle.removeBuff(e, b);
}
cur = next;
}, { owner: unit, immediate: true });
};
/** One more “狼影” (S1): this kit's pack, or a 狼群 board piece run by content/tokens.js (same 'wolf:shadows' buff). */
const addShadow = (battle, vigil, w) => {
if (typeof w.mem.addWolf === 'function') return w.mem.addWolf();
const n = Math.max(1, w.mem.shadows ?? 1);
if (n >= maxWolves) return false;
const wb = w.def?.talents?.[0]?.bb ?? {};
const per = num(wb['vigil_wolf_t_1_enhance[trigger].block_cnt'], num(wb.block_cnt, 1));
w.mem.shadows = n + 1;
battle.addBuff(w, { key: 'wolf:shadows', persist: true, allowDead: true, refresh: 'replace', mods: { blockCnt: w.mem.shadows * per } });
fx(battle, 'summon', w, { src: vigil.id, wolves: w.mem.shadows });
return true;
};
const dpGain = (battle, unit, n) => { if (n > 0) { battle.addDp(unit.ownerId, n); fx(battle, 'dp', unit, { n }); } };
// S2: the pack's empowered next attack (armed by the cast; ×scale on its hits; a kill pays once)
const installGift = (battle, unit) => {
battle.on('beforeAttack', (ctx) => {
const w = wolfOf(unit);
if (!w || ctx.attacker !== w || !w.mem.vigilGift) return;
w.mem.vigilGiftOn = w.mem.vigilGift;
w.mem.vigilGift = null;
}, { owner: unit, priority: -100 });
battle.on('hit', (ctx) => {
const g = ctx.source?.mem?.vigilGiftOn;
if (g && ctx.dmg.isAttack && ctx.source === wolfOf(unit)) ctx.dmg.mul *= g.scale;
}, { owner: unit });
battle.on('kill', (ctx) => {
const g = ctx.killer?.mem?.vigilGiftOn;
if (!g || g.paid || ctx.victim.side !== 'enemy' || ctx.killer !== unit.trait.reinforcement) return;
g.paid = true;
dpGain(battle, unit, g.dp);
}, { owner: unit });
battle.on('attack', (ctx) => { if (ctx.attacker?.mem?.vigilGiftOn && ctx.attacker === unit.trait.reinforcement) ctx.attacker.mem.vigilGiftOn = null; }, { owner: unit });
};
return {
trait: { install(battle, unit) {
// The pack is the tactician's 援军. The match also hands the player the 狼群 token to place in the prep phase
// (= choosing the tactical point): that board piece (tokens.js kit, owner-coupled effects left to this kit) is
// the pack when present — deployed early on its own tile if 伺夜 deploys first — never a second pack.
const spawn = () => {
if (!alive(unit) || wolfOf(unit)) return;
const pieces = tokensOf(battle, unit, wolfId);
const live = pieces.find((t) => alive(t));
if (live) { unit.trait.reinforcement = live; return; }
const waiting = pieces.find((t) => !t.alive && !t.removed);
if (waiting && battle.redeploy(waiting, { free: true })) {
unit.trait.reinforcement = waiting;
fx(battle, 'summon', waiting, { src: unit.id, token: wolfId });
return;
}
const board = pieces.find((t) => t.uid != null);
const tile = tacticalPoint(battle, unit, board ? [board.homeR, board.homeC] : null);
if (!tile) return;
const w = battle.spawnToken(unit, wolfId, tile[0], tile[1], { kit: wolfKit(unit) });
unit.trait.reinforcement = w;
if (!w) return;
fx(battle, 'summon', w, { src: unit.id, token: wolfId, wolves: w.mem.wolves });
const again = Math.max(0, w.base.respawnTime || 0);
battle.on('death', (c) => { if (c.unit === w && c.reason === 'killed') battle.after(again, spawn, { owner: unit }); }, { owner: w });
};
battle.on('deploy', (c) => { if (c.unit === unit) spawn(); }, { owner: unit });
installPackMark(battle, unit);
battle.on('death', (c) => {
if (c.unit !== unit) return;
const w = wolfOf(unit);
if (w) battle.retreat(w, { reason: 'expired', permanent: true });
}, { owner: unit });
} },
skill: {
kind: 'duration',
attack: { hits },
onStart({ unit }) { unit.mem.vigilAcc = 0; unit.mem.vigilDp = 0; },
onTick({ battle, unit, dt }) {
const iv = num(bb.interval, 1.5), step = num(bb.cost, 1), cap = num(bb.value, 0);
if (!(iv > 0) || !(step > 0)) return; // (junk blackboard: never spin)
unit.mem.vigilAcc += dt;
while (unit.mem.vigilAcc >= iv - 1e-9 && unit.mem.vigilDp + step <= cap + 1e-9) {
unit.mem.vigilAcc -= iv;
unit.mem.vigilDp += step;
battle.addDp(unit.ownerId, step);
}
},
onEnd({ battle, unit, reason }) {
// the whole `value` is granted over a full duration (精锐: 11 × 1.364 s ends a hair after 15 s)
const rest = num(bb.value, 0) - (unit.mem.vigilDp ?? 0);
if (reason === 'duration' && rest > 1e-9) { battle.addDp(unit.ownerId, rest); unit.mem.vigilDp += rest; }
fx(battle, 'dp', unit, { n: unit.mem.vigilDp ?? 0 });
},
},
skills: altSkills(chess, d, bb, {
// (自动触发: an AUTO skill takes no 技能策略 — the 战术家 row is for MANUAL skills — and this DP skill fires as soon
// as it is ready, as before)
skchr_vigil_1: (s) => ({
kind: 'instant',
trigger: 'SP_FULL',
onStart({ battle, unit }) {
dpGain(battle, unit, num(s.bb.cost, 0));
const w = wolfOf(unit);
if (w) addShadow(battle, unit, w);
},
}),
skchr_vigil_2: (s) => ({
kind: 'instant',
onStart({ battle, unit }) {
dpGain(battle, unit, num(s.bb.cost, 0));
const w = wolfOf(unit);
if (!w) return;
const hr = num(s.bb['vigil_wolf_s_2.hp_ratio'], 0);
if (hr > 0) battle.heal(w, w, w.s.maxHp * hr, { self: true });
w.mem.vigilGift = { scale: num(s.bb['vigil_wolf_s_2.atk_scale'], 1), dp: num(s.bb['vigil_wolf_s_2.cost'], 0), paid: false };
fx(battle, 'buff', w, { src: unit.id, skill: 'vigil_2' });
},
}),
}),
install(battle, unit) { if (sel === 'skchr_vigil_2') installGift(battle, unit); },
talents: [
{ install() { /* 狼群领袖: the pack itself (trait.install / wolfKit) */ } },
{ install(battle, unit) {
battle.on('hit', (ctx) => {
const w = wolfOf(unit);
// "伺夜和狼群对其的攻击无视其175防御力": their attacks only (not item procs or other non-attack damage)
if (!w || pen <= 0 || !ctx.dmg.isAttack || (ctx.source !== unit && ctx.source !== w) || ctx.target.blockedBy !== w) return;
ctx.dmg.defIgnoreFlat += pen;
}, { owner: unit });
battle.on('damaged', (ctx) => {
const w = wolfOf(unit);
if (!w || !(bonus > 0) || !unit.skill?.active || !ctx.dmg?.isAttack || (ctx.source !== unit && ctx.source !== w)) return;
const e = ctx.target;
if (e.side !== 'enemy' || !e.alive || e.blockedBy !== w) return;
// "狼群与伺夜攻击…时,额外造成…": the extra hit belongs to that attack's dealer (a bite's bonus is the pack's
// damage — never re-typed by 伺夜's 弱点伤害 特质), its size to 伺夜's ATK (same as tokens.js unmanaged mode)
battle.dealDamage(ctx.source, e, { amount: unit.s.atk * bonus, type: 'arts', canDodge: false, isSkill: true, tags: ['skill', 'vigilBonus'] });
}, { owner: unit });
} },
],
};
},
};
@@ -0,0 +1,58 @@
// server/sim/content/kits/ops/chess_char_3_20-kjera.js — 耶拉 (char_4013_kjera) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { num, defOf, talentBb, altSkills, statSkill, funnelMap, installFunnelPrune } from '../shared/tier3.js';
export default {
// ---- 3_20 耶拉 · 驭械术师 — S2 心随意动: +1 drone (2 locks; a lone enemy gets both), ATK +, cold procs; 低眉: ATK +
// (more with ≥cnt ground tiles in range)
chess_char_3_20_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const extra = Math.floor(num(bb['attack@cnt'], 0));
const prob = num(bb['attack@prob'], 0), cold = num(bb['attack@cold'], 0);
const kit = {
trait: { dmgMul: funnelMap },
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
attack: { onHit({ battle, unit, target }) {
if (target && target.alive && cold > 0 && battle.rng.chance(prob)) battle.applyStatus(target, 'cold', { duration: cold, source: unit });
} },
},
// S1 攻击力强化·γ型: ATK + (one drone, the trait ramp as usual)
skills: altSkills(chess, d, bb, { 'skcom_atk_up[3]': statSkill }),
install(battle, unit) {
installFunnelPrune(battle, unit);
// "浮游单元+1": every drone locks an enemy — with fewer enemies in range than drones, the spare drone joins a
// lock (both attack it, each hit rolls the cold proc; they ramp in parallel, see funnelMap)
if (extra > 0) {
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active || !ctx.targets.length) return;
const n = 1 + extra;
if (ctx.targets.length >= n) return;
const base = ctx.targets.slice();
const out = base.slice();
for (let i = 0; out.length < n; i++) out.push(base[i % base.length]);
ctx.targets = out;
}, { owner: unit, priority: -50 });
}
},
talents: [{ install(battle, unit) {
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
let low = 0;
for (const k of unit.baseRangeKeys || []) {
const r = (k / COLS) | 0, c = k % COLS;
if (battle.grid.inRect(r, c) && battle.grid.isLow(r, c)) low++;
}
const atk = low >= num(t0.cnt, 2) ? num(t0['kjera_t_1[high].atk'], num(t0.atk)) : num(t0.atk);
battle.addBuff(unit, { key: 'talent:kjera_brow', mods: { atkPct: atk } });
}, { owner: unit });
} }],
};
if (extra > 0) kit.skill.targeting = { maxTargets: 1 + extra };
return kit;
},
};
@@ -0,0 +1,122 @@
// server/sim/content/kits/ops/chess_char_3_21-archet.js — 空弦 (char_332_archet) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { canTargetEnemy } from '../../../targeting.js';
import { bodyDist } from '../../../body.js';
import {
num, defOf, talentBb, moduleTalentBb, altSkills, instantKindOf, groundAspd, alive, fx, enemiesOn, isLeader, giveSp,
} from '../shared/tier3.js';
/** The unit's own profile damage multiplier on `target` (fastshot fly bonus …) for damage a kit deals itself. */
function profileMul(battle, unit, target) {
const f = unit.profile?.dmgMul;
const m = typeof f === 'function' ? f(battle, unit, target) : f;
return typeof m === 'number' && Number.isFinite(m) ? m : 1;
}
/** Targetable enemies within `r` tiles of (x, y), nearest first (spawn order breaks ties), excluding `skip`. */
function enemiesAround(battle, unit, x, y, r, skip = null) {
const out = battle.foesInRadius(x, y, r).filter((e) => !(skip && skip.has(e)) && canTargetEnemy(unit, e, { canHitFly: true }));
const d = (e) => bodyDist(e, x, y);
return out.sort((a, b) => d(a) - d(b) || (a.spawnSeq ?? a.id) - (b.spawnSeq ?? b.id));
}
/** 周围 (around a target) for 空弦's scatter / bounce arrows [ASSUMED radius, tiles: not in the data]. */
const AROUND_R = 1.5;
export default {
// ---- 3_21 空弦 · 速射手 — S3 箭矢·暴风: ATK +, range +1, 3 hits × 2 targets; 兰登战术: attack-SP snipers +1 SP / 2.5 s;
// 铁弦: one-hit shield at deployment, +7 SP when it breaks; 精锐 module MAR-Y: ASPD + with ground enemies in range
// S1 箭矢·散逸: next attack ×atk_scale + up to max_target−1 other enemies around the target × atk_scale_2;
// S2 箭矢·追猎 (charges): an arrow fired at once hits its target `times` times, then bounces to the nearest enemy
// around not hit yet with one hit fewer each bounce (5, 4, 3, 2, 1). 精锐 MAR-X: fly × (profession layer);
// ISW-A (集成战略 only): no range bonus here.
chess_char_3_21_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const mod = moduleTalentBb(chess);
const skill = {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
attack: { hits: Math.max(1, Math.floor(num(bb['attack@times'], 1))) },
};
const tg = {};
if (num(bb.ability_range_forward_extend) > 0) tg.rangeExtend = Math.round(bb.ability_range_forward_extend);
if (num(bb['attack@max_target']) > 1) tg.maxTargets = Math.floor(bb['attack@max_target']);
if (Object.keys(tg).length) skill.targeting = tg;
const physHit = (battle, unit, e, scale, tag) => battle.dealDamage(unit, e, { amount: unit.s.atk * scale * profileMul(battle, unit, e), type: 'phys', isSkill: true, tags: ['skill', tag] });
const kit = {
skill,
skills: altSkills(chess, d, bb, {
skchr_archet_1: (s) => {
const more = Math.max(0, Math.floor(num(s.bb.max_target, 4)) - 1);
const sc2 = num(s.bb.atk_scale_2, 1);
return {
kind: instantKindOf(s),
attack: {
atkScale: num(s.bb.atk_scale, 1),
onHit({ battle, unit, target, x, y }) {
const cx = target ? target.x : x, cy = target ? target.y : y;
if (!Number.isFinite(cx) || !Number.isFinite(cy) || !(more > 0)) return;
const others = enemiesAround(battle, unit, cx, cy, AROUND_R, new Set([target])).slice(0, more);
for (const e of others) physHit(battle, unit, e, sc2, 'archetScatter');
if (others.length) fx(battle, 'volley', unit, { targets: others.map((e) => e.id), skill: 'archet_1' });
},
},
};
},
skchr_archet_2: (s) => {
const times = Math.max(1, Math.floor(num(s.bb.times, 5)));
const sc = num(s.bb.atk_scale, 1);
return {
kind: instantKindOf(s),
onStart({ battle, unit }) {
const first = enemiesOn(battle, unit, unit.rangeKeys, 1, unit.profile)[0];
if (!first) return;
const seen = new Set();
const fire = (from, target, n) => {
seen.add(target);
battle.addProjectile({
from, target, speed: 16, visual: 'arrow', source: unit, hitDead: true,
onHit: ({ target: t, x, y }) => {
if (t && t.alive) for (let i = 0; i < n && t.alive; i++) physHit(battle, unit, t, sc, 'archetPursuit');
if (n <= 1) return;
const next = enemiesAround(battle, unit, x, y, AROUND_R, seen)[0];
if (next) fire({ x, y }, next, n - 1);
},
});
};
fire(unit, first, times);
fx(battle, 'volley', unit, { target: first.id, skill: 'archet_2' });
},
};
},
}),
talents: [
{ install(battle, unit) { // 兰登战术
unit.mem.archetTactics = true;
battle.every(num(t0.interval, 2.5), () => {
if (!alive(unit) || !isLeader(battle, unit, 'archetTactics')) return;
for (const a of battle.allies(unit.ownerId)) {
if (a.kind === 'op' && a.def?.profession === 'SNIPER' && a.skill?.spType === 'attack') giveSp(a, num(t0.sp, 1), 'talent');
}
}, { owner: unit });
} },
{ install(battle, unit) { // 铁弦
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
battle.addBuff(unit, {
key: 'talent:archet_shield', shieldHits: 1, visible: true,
onRemove: ({ unit: u, buff }) => {
if (buff.shieldHits > 0 || !u.alive) return;
giveSp(u, num(t1.sp), 'talent');
fx(battle, 'shieldBreak', u);
},
});
fx(battle, 'shield', unit);
}, { owner: unit });
} },
],
};
if (mod && num(mod.attack_speed) > 0) kit.install = groundAspd('trait:archet_ground', num(mod.attack_speed));
return kit;
},
};
@@ -0,0 +1,144 @@
// server/sim/content/kits/ops/chess_char_4_01-rmixer.js — 信仰搅拌机 (char_4194_rmixer) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { isHpLoss } from '../../../damage.js';
import { holdsUndying } from '../../items/battle.js';
import {
AURA, num, tbb, grid, nationOf, enemiesOnRange, targetsInRange, whileDeployed, reveal, skillActive, isSel, alt,
instantKind, withDefaults,
} from '../shared/tier4.js';
/** Is module `id` the active one? (elites; `'none'` / normal chess ⇒ false) */
const moduleIs = (def, id) => !!(def?.raw?.module?.active && def.raw.module.id === id);
/**
* Permanent forward range extension (攻击距离 +n): every range, skill ranges included — except a skill whose range ignores
* 攻击距离 (`targeting.noRangeExtend`). SPT-Y's "攻击距离+1(不受技能攻击范围变化影响)" reads, as PRTS 修正 it, "(部分技能不受
* 此影响)" (原因: 语序颠倒、表述模糊) — 信仰搅拌机 S3 备注 "此技能的攻击范围不受'攻击距离'属性影响".
*/
const rangeUp = (battle, unit, n = 1) => battle.addBuff(unit, { key: 'module:range', mods: { rangeExtend: n }, persist: true, allowDead: true });
export default withDefaults({
// ===== 信仰搅拌机 (shotprotector) S3 退休前布道 — ammo 30 counters; talents 扫射迎宾仪礼 / 架盾送客仪礼; module: reveal
// S1 铳骑主考官 (自动触发 ⇒ DEFAULT: next attack ×3 hits + reload an adjacent 拉特兰 ammo skill), S2 八臂电锯侠 (ammo;
// a fatal hit is blocked for `ammo_cost` bullets); module SPT-Y 老朋友: range +1 (not on S3's range, PRTS 备注)
chess_char_4_01_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const counterMax = Math.max(1, Math.floor(num(bb['attack@max_target'], 3)));
const counterRatio = num(bb.base_attack_time, 0.6); // 反击最小间隔 = 实际攻击间隔 × ratio (not a BAT change)
const g = grid(def.skill?.rangeGrid);
const S2 = isSel(def, 'skchr_rmixer_2'), S3 = isSel(def, 'skchr_rmixer_3');
return {
skills: alt(def, {
// 铳骑主考官: the next attack is a triple hit of atk_scale each and reloads `charge` bullet(s) into ONE other
// 拉特兰 operator around him (8 tiles, [ASSUMED] nearest first) whose ammo skill runs
skchr_rmixer_1: () => ({
kind: instantKind(def),
attack: {
atkScale: num(bb.atk_scale, 1.3), hits: 3,
onHit({ battle, unit }) {
const n = num(bb.charge, 1);
const cand = battle.alliesInRadius(unit.x, unit.y, 1.5, unit.ownerId)
.filter((a) => a !== unit && a.kind === 'op' && nationOf(a) === 'laterano' && a.skill && a.skill.active && a.skill.kind === 'ammo')
.sort((a, b) => Math.hypot(a.x - unit.x, a.y - unit.y) - Math.hypot(b.x - unit.x, b.y - unit.y) || a.id - b.id);
if (!cand[0] || !(n > 0)) return;
cand[0].skill.addAmmo(n);
battle.fx('reload', { x: cand[0].x, y: cand[0].y, id: cand[0].id, n });
},
},
}),
// 八臂电锯侠: ATK/DEF up, 42/45 bullets; the fatal-hit guard is installed below
skchr_rmixer_2: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 42),
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
onStart({ battle, unit }) { battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 30),
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), defPct: num(bb.def) },
// 3-13 exactly: SPT-Y's 攻击距离+1 does not reach it (PRTS 备注; reviewer of community report E1 after 0.1.0)
targeting: g ? { rangeGrid: g, noRangeExtend: true } : undefined,
attack: { noAttack: true }, // 停止主动攻击敌人 (ammo is spent by counters)
onStart({ battle, unit }) {
unit.mem.counterReady = -Infinity;
for (const a of battle.allies(unit.ownerId)) {
if (a === unit || a.kind !== 'op' || nationOf(a) !== 'laterano') continue;
if (a.skill && a.skill.active && a.skill.kind === 'ammo') { a.skill.addAmmo(num(bb.ammo, 0)); battle.fx('reload', { x: a.x, y: a.y, id: a.id, n: num(bb.ammo, 0) }); }
}
battle.fx('sermon', { x: unit.x, y: unit.y, id: unit.id });
},
},
talents: [
{ install(battle, unit) { // 扫射迎宾仪礼: every damage dealt → DEF +30 / ASPD +3 for 10 s, ≤ 3 stacks
battle.on('damaged', (c) => {
if (c.source !== unit || !unit.alive || c.target.side !== 'enemy' || !(c.amount > 0)) return;
battle.addBuff(unit, { key: 'rmixer:t1', duration: num(t0.duration, 10), refresh: 'stack', maxStacks: Math.max(1, num(t0.max_stack_cnt, 3)),
mods: { defFlat: num(t0.def), aspd: num(t0.attack_speed) } });
}, { owner: unit });
} },
{ install(battle, unit) { // 架盾送客仪礼: no active attack for 8 s → shield 15 % max HP; timer restarts when it breaks
const since = () => Math.max(unit.deployedAt, unit.mem.lastActiveAtk ?? -Infinity, unit.mem.shieldLostAt ?? -Infinity);
battle.on('attack', (c) => { if (c.attacker === unit && !unit.mem.inCounter) unit.mem.lastActiveAtk = battle.time; }, { owner: unit });
battle.on('deploy', (c) => { if (c.unit === unit) { unit.mem.lastActiveAtk = -Infinity; unit.mem.shieldLostAt = -Infinity; } }, { owner: unit });
whileDeployed(battle, unit, 0.1, () => {
if (unit.findBuff('rmixer:shield') || battle.time - since() < num(t1.interval, 8) - 1e-9) return;
battle.addBuff(unit, { key: 'rmixer:shield', shield: unit.s.maxHp * num(t1.shield, 0.15), visible: true,
onRemove: () => { unit.mem.shieldLostAt = battle.time; } });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
});
} },
],
install(battle, unit) {
if (S2) {
// S2 "技能期间若受到致命伤害,立即消耗N发弹药抵挡这次伤害": the lethal hit is negated (HP back to its value
// before the hit) and N bullets go; with fewer left it still blocks, spends them all and the skill ends (PRTS 备注
// "弹药量不足时仍可抵挡致命伤害,此时将消耗所有剩余弹药并退出技能状态"). "自身持有不死时此效果不会生效": a 不死
// that runs before it (骑士戒律, priority 20) has already prevented the hit; 坚固维式重锤's held window
// (items/battle.js holdsUndying: this deployment's, not run out) runs last, so the guard steps aside while it lasts
const cost = Math.max(1, Math.floor(num(bb.ammo_cost, 30)));
battle.on('hit', (c) => { if (c.target === unit) unit.mem.rmixerPre = { dmg: c.dmg, hp: unit.hp }; }, { owner: unit, priority: -100 });
battle.on('fatal', (c) => {
const sk = unit.skill;
if (c.unit !== unit || c.prevented || !sk || !sk.active || sk.kind !== 'ammo' || !(sk.ammoLeft > 0)) return;
if (holdsUndying(battle, unit)) return;
c.prevented = true;
const pre = unit.mem.rmixerPre;
unit.hp = pre && pre.dmg === c.dmg ? Math.max(1, Math.min(unit.s.maxHp, pre.hp)) : 1;
const spent = Math.min(cost, sk.ammoLeft);
sk.ammoLeft -= spent;
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id, n: spent });
if (sk.ammoLeft <= 0) sk.end('ammo');
}, { owner: unit });
}
// module SPT-Y (elite, 老朋友): 攻击距离+1 — S1 / S2 keep it, S3's 3-13 does not (noRangeExtend above)
if (num(def.traitBb?.ability_range_forward_extend, 0) > 0) rangeUp(battle, unit, num(def.traitBb.ability_range_forward_extend, 1));
// module SPT-X (elite default): 攻击范围内敌人的隐匿效果失效
if (moduleIs(def, 'uniequip_002_rmixer')) whileDeployed(battle, unit, AURA, () => reveal(battle, enemiesOnRange(battle, unit)));
if (!S3) return;
// S3 counter (PRTS 备注 "反击受到任何伤害后均可触发,无需目标,视为普通攻击,受各类无法触发普通攻击效果的影响"): ANY damage
// she takes — an enemy's attack, a zone, the 无来源 源石溶剂 tick (items/battle.js periodic_damage) — fires one volley at
// ≤ 3 enemies in range (the attacker first), at most once per actual interval × ratio. With no enemy in range the
// counter still happens and spends its bullet (无需目标: the drain alone empties the skill — 莫斯提马's 特质 turns those
// bullets into 拉特兰 layers, player report D1). A 流失 (Battle.loseHp, tag 'hpLoss': it skips every damage event —
// PRTS 作战机制) and an element 损伤 never counter; stunned / disarmed: no counter
battle.on('damaged', (c) => {
const d = c.dmg;
if (c.target !== unit || !unit.canAct || unit.s.flags.disarm || !skillActive(unit) || !d || c.type === 'element') return;
if (isHpLoss(d)) return;
if (battle.time < (unit.mem.counterReady ?? -Infinity) - 1e-9) return;
const list = targetsInRange(battle, unit);
const i = c.source ? list.indexOf(c.source) : -1;
if (i > 0) { list.splice(i, 1); list.unshift(c.source); }
const targets = list.slice(0, counterMax);
unit.mem.counterReady = battle.time + unit.s.interval * counterRatio;
unit.mem.inCounter = true;
try {
if (targets.length) battle.forceAttack(unit, targets);
else { unit.stats.attacks++; unit.skill.onAttackPerformed([], true); } // a counter into nothing: its bullet goes
} finally { unit.mem.inCounter = false; }
battle.fx('counter', { x: unit.x, y: unit.y, id: unit.id, n: targets.length });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,75 @@
// server/sim/content/kits/ops/chess_char_4_02-mostma.js — 莫斯提马 (char_213_mostma) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, grid, enemiesOnRange, whileDeployed, pulse, spAura, skillActive, isSel, alt, applyModuleRange,
withDefaults,
} from '../shared/tier4.js';
const TICK_EPS = 0.01; // minimal status duration (s)
/**
* Knock-back of 力度 `force` away from `from` (a radial push, Battle.push: the official 力度 − 重量 distance — PRTS 推与拉;
* user playtest #6 item 14). Returns the tiles moved.
*/
function shove(battle, e, from, force) {
return e && e.alive ? battle.push(e, num(force, 0), { from }) : 0;
}
export default withDefaults({
// ===== 莫斯提马 (splashcaster) S3 序时之匙 — ripple hits all in range, +ATK, talent 2 ×3, small knock-back
// S1 攻击力强化·γ型; S2 荒时之锁 (every enemy in range stunned for the rest of the skill, 100 %/120 % ATK arts
// per second); module SPC-X (资深万国信使定制斗篷): 攻击范围扩大
chess_char_4_02_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const g = grid(def.skill?.rangeGrid);
const force = num(bb['attack@force'], num(bb.force, 0));
const S3 = isSel(def, 'skchr_mostma_3');
return {
skills: alt(def, {
'skcom_atk_up[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk) } }),
skchr_mostma_2: () => ({
kind: 'duration',
onStart({ battle, unit, skill }) {
unit.mem.timeLock = { acc: 0, locked: new Set() };
battle.fx('zone', { x: unit.x, y: unit.y, id: unit.id, duration: skill.duration });
},
onTick({ battle, unit, skill, dt }) {
const L = unit.mem.timeLock;
if (!L) return;
// every enemy standing in her range (entering later too) is stunned until the skill ends
for (const e of enemiesOnRange(battle, unit)) {
if (L.locked.has(e.id)) continue;
L.locked.add(e.id);
battle.applyStatus(e, 'stun', { duration: Math.max(TICK_EPS, skill.timeLeft), source: unit });
}
L.acc += dt;
while (L.acc + 1e-9 >= 1) {
L.acc -= 1;
for (const e of enemiesOnRange(battle, unit)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill', 'timeLock'] });
}
}
},
onEnd({ unit }) { unit.mem.timeLock = null; },
}),
}),
install(battle, unit) { applyModuleRange(battle, unit, def); }, // module SPC-X: 攻击范围扩大 (module range grid)
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
targeting: { ...(g ? { rangeGrid: g } : {}), allInRange: true },
attack: { projectile: 'none', splashRadius: 0, onHit({ battle, unit, target }) { if (target && target.alive) shove(battle, target, unit, force); } },
onAttack({ battle, unit }) { battle.fx('ripple', { x: unit.x, y: unit.y, id: unit.id }); },
},
talents: [
{ install(battle, unit) { spAura(battle, unit, num(t0.sp_recovery_per_sec, 0.4), (a) => a.def?.profession === 'CASTER'); } },
{ install(battle, unit) { // 主观缓时: enemies in range −15 % move speed (×talent_scale during S3)
whileDeployed(battle, unit, AURA, () => {
const v = num(t1.move_speed, -0.15) * (S3 && skillActive(unit) ? num(bb.talent_scale, 3) : 1);
for (const e of enemiesOnRange(battle, unit)) pulse(battle, e, `mostma:slow:${unit.id}`, { moveMul: Math.max(0, 1 + v) }, { status: 'slow', visible: true });
});
} },
],
};
},
});
@@ -0,0 +1,77 @@
// server/sim/content/kits/ops/chess_char_4_03-kjera.js — 耶拉 (char_4013_kjera) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { canTargetEnemy } from '../../../targeting.js';
import { num, tbb, skillActive, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 耶拉 (funnel, hidden) S2 心随意动 — +1 drone locking targets, ATK up, chance to chill; talent 低眉
chess_char_4_03_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const n = 1 + Math.max(0, Math.floor(num(bb['attack@cnt'], 1)));
const prob = num(bb['attack@prob'], 0), cold = num(bb['attack@cold'], 0);
const funnel = (unit) => unit.profile.funnel || { init: 0.2, delta: 0.15, max: 1.1 };
return {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
targeting: { maxTargets: n },
attack: {
// every drone ramps on its own locked target (trait init/delta/max): the multipliers are queued per target
// by the beforeAttack hook below (two drones on the same enemy ramp independently)
dmgMul: (battle, unit, target) => {
const q = unit.mem.kjeraQueue && unit.mem.kjeraQueue.get(target.id);
return q && q.length ? q.shift() : funnel(unit).init;
},
onHit({ battle, unit, target }) {
if (target && target.alive && cold > 0 && battle.rng.chance(prob)) battle.applyStatus(target, 'cold', { duration: cold, source: unit });
},
},
onStart({ battle, unit }) { unit.mem.kjeraDrones = []; unit.mem.kjeraQueue = new Map(); battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n }); },
onEnd({ unit }) { unit.mem.kjeraDrones = []; unit.mem.kjeraQueue = null; },
},
talents: [{ install(battle, unit) { // 低眉: ATK +10 %, +16 % with ≥ 2 ground tiles in range
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
let low = 0;
for (const k of unit.rangeKeys || []) if (battle.grid.isLow((k / COLS) | 0, k % COLS)) low++;
const v = low >= num(t0.cnt, 2) ? num(t0['kjera_t_1[high].atk'], num(t0.atk)) : num(t0.atk);
battle.addBuff(unit, { key: 'kjera:t1', mods: { atkPct: v } });
}, { owner: unit });
} }],
install(battle, unit) {
// n drones, each locked on its enemy until that enemy dies (while at least one enemy is in her range). A free
// drone takes an enemy no other drone holds, else doubles up on the best target (a lone enemy/boss gets both).
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillActive(unit)) return;
const ok = (e) => !!e && e.alive && canTargetEnemy(unit, e, { canHitFly: true });
const cands = c.targets.filter(ok);
const drones = unit.mem.kjeraDrones || (unit.mem.kjeraDrones = []);
const held = new Set();
for (let i = 0; i < n; i++) { if (drones[i] && ok(drones[i].e)) held.add(drones[i].e); else drones[i] = null; }
for (let i = 0; i < n; i++) {
if (drones[i]) continue;
const e = cands.find((x) => !held.has(x)) ?? cands[0] ?? drones.find((d) => d)?.e;
if (!e) continue;
held.add(e);
drones[i] = { e, scale: null };
}
const f = funnel(unit);
const q = unit.mem.kjeraQueue || (unit.mem.kjeraQueue = new Map());
const out = [];
for (const d of drones) {
if (!d) continue;
d.scale = d.scale == null ? f.init : Math.min(f.max, d.scale + f.delta);
out.push(d.e);
const l = q.get(d.e.id);
if (l) l.push(d.scale); else q.set(d.e.id, [d.scale]);
}
if (out.length) c.targets = out;
}, { owner: unit, priority: 10 });
// queued multipliers of shots that never landed (target died mid-flight) are dropped with the target
battle.on('death', (c) => { if (c.unit.side === 'enemy' && unit.mem.kjeraQueue) unit.mem.kjeraQueue.delete(c.unit.id); }, { owner: unit });
},
};
},
});
@@ -0,0 +1,168 @@
// server/sim/content/kits/ops/chess_char_4_04-ines.js — 伊内丝 (char_4087_ines) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyDist } from '../../../body.js';
import { canTargetEnemy } from '../../../targeting.js';
import { AURA, num, tbb, grid, enemiesOnRange, pulse, isSel, alt, instantKind, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 伊内丝 (agent) S2 暗夜无明 — stealth, +ATK, +1 DP & steal ASPD per attack; talents 影织 / 影哨
// S1 淬影突袭 (next attack: 3 s arts DoT 40 %/55 % ATK per s, not stacking, +2 DP); S3 独影归途 (passive: the
// first deployment leaves a 影哨 and retreats with the redeploy timer refreshed; every deployment: ATK up for
// the skill duration, the sentry is recalled through ≤ 4/5 enemies for 110 %/140 % phys, +1 DP per damage)
chess_char_4_04_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid);
const step = num(bb['attack@steal_atk_speed'], 5), maxSteal = num(bb['attack@steal_atk_speed_max'], 50);
const S3 = isSel(def, 'skchr_ines_3');
const clearAspd = (battle, unit) => {
for (const id of unit.mem.inesAspdVictims || []) { const e = battle.unitById(id); if (e) battle.removeBuff(e, `ines:aspd:${unit.id}`); }
unit.mem.inesAspdVictims = [];
unit.mem.inesAspd = 0;
battle.removeBuff(unit, 'ines:aspdGain');
};
const s3Dur = num(def.skill?.duration, 11);
return {
skills: alt(def, {
skchr_ines_1: () => ({
kind: instantKind(def),
attack: {
onHit({ battle, unit, target }) {
battle.addDp(unit.ownerId, num(bb.cost, 2));
if (!target || !target.alive || target.side !== 'enemy') return;
const scale = num(bb.bleed_atk_scale, 0.4);
// (不叠加): a new DoT replaces the running one
battle.addBuff(target, { key: `ines:bleed:${unit.id}`, duration: num(bb.bleed_duration, 3) + 1e-6, interval: 1, source: unit, visible: true, refresh: 'extend',
onTick: ({ unit: t }) => battle.dealDamage(unit, t, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'dot'] }) });
},
},
}),
skchr_ines_3: () => ({
kind: 'passive',
onStart({ battle, unit }) {
if (!unit.mem.inesS3Placed) {
// 首次部署: place a 影哨 (her talent's sentry, left on retreat) and leave; 立刻刷新再部署时间 — the redeploy
// (auto, paying her DP cost like every redeploy) follows as soon as it is affordable
unit.mem.inesS3Placed = true;
battle.after(0, () => {
if (!unit.alive || !unit.deployed) return;
battle.retreat(unit, { reason: 'retreat' });
unit.respawnAt = battle.time;
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
return;
}
battle.addBuff(unit, { key: 'ines:s3', duration: s3Dur, mods: { atkPct: num(bb.atk) }, visible: true });
// 立刻收回影哨: the sentry flies back to her and hits ≤ max_target enemies on its way (within
// projectile_range of the segment, [ASSUMED] nearest to its start first)
const from = unit.mem.inesSentryAt;
if (!unit.mem.sentry || !from) return;
unit.mem.sentry = null;
unit.mem.inesSentryAt = null;
const ax = from.x, ay = from.y, bx = unit.x, by = unit.y;
const L2 = (bx - ax) ** 2 + (by - ay) ** 2;
const w = num(bb.projectile_range, 1.4);
const hits = [];
for (const e of battle.aliveEnemies()) {
if (!canTargetEnemy(unit, e, { canHitFly: true })) continue;
const t = L2 > 1e-9 ? Math.max(0, Math.min(1, ((e.x - ax) * (bx - ax) + (e.y - ay) * (by - ay)) / L2)) : 0;
const d = bodyDist(e, ax + t * (bx - ax), ay + t * (by - ay));
if (d <= w + 1e-9) hits.push({ e, t, d });
}
hits.sort((a, b) => a.t - b.t || a.d - b.d || a.e.id - b.e.id);
const amount = unit.s.atk * num(bb.atk_scale, 1.1); // one projectile: ATK taken when it flies back
for (const { e } of hits.slice(0, Math.max(1, Math.floor(num(bb.max_target, 4))))) {
battle.dealDamage(unit, e, { amount, type: 'phys', isSkill: true, tags: ['skill', 'sentryRecall'] });
}
battle.fx('beam', { x: ax, y: ay, tx: bx, ty: by, id: unit.id });
},
}),
}),
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
flags: { stealth: true },
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { clearAspd(battle, unit); battle.fx('stealth', { x: unit.x, y: unit.y, id: unit.id }); },
onAttack({ battle, unit }) { battle.addDp(unit.ownerId, num(bb.cost, 1)); },
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.side !== 'enemy') return;
const room = maxSteal - (unit.mem.inesAspd ?? 0);
if (room <= 0) return;
const s = Math.min(step, room);
unit.mem.inesAspd = (unit.mem.inesAspd ?? 0) + s;
const key = `ines:aspd:${unit.id}`;
const cur = target.findBuff(key);
battle.addBuff(target, { key, mods: { aspd: (cur?.mods?.aspd ?? 0) - s }, source: unit });
if (!(unit.mem.inesAspdVictims ||= []).includes(target.id)) unit.mem.inesAspdVictims.push(target.id);
battle.addBuff(unit, { key: 'ines:aspdGain', mods: { aspd: unit.mem.inesAspd } });
},
onEnd({ battle, unit }) { clearAspd(battle, unit); },
},
talents: [
{ install(battle, unit) { // 影织: first damage on each enemy → bind 5 s + steal 90 ATK until it dies or Ines leaves
unit.mem.woven = new Set();
unit.mem.atkVictims = [];
const refresh = () => {
unit.mem.atkVictims = unit.mem.atkVictims.filter((id) => battle.unitById(id)?.alive);
const gain = Math.min(num(t0.steal_atk_max, Infinity), num(t0.steal_atk, 90) * unit.mem.atkVictims.length);
if (unit.alive) { if (gain > 0) battle.addBuff(unit, { key: 'ines:atkGain', mods: { atkFlat: gain } }); else battle.removeBuff(unit, 'ines:atkGain'); }
};
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !unit.alive || e.side !== 'enemy' || c.type === 'element' || unit.mem.woven.has(e.id)) return;
unit.mem.woven.add(e.id);
if (!e.alive) return;
battle.applyStatus(e, 'bind', { duration: num(t0.duration, 5), source: unit });
battle.addBuff(e, { key: `ines:atkSteal:${unit.id}`, mods: { atkFlat: -num(t0.steal_atk, 90) }, source: unit });
unit.mem.atkVictims.push(e.id);
refresh();
battle.fx('shadowWeave', { x: e.x, y: e.y, id: e.id });
}, { owner: unit });
battle.on('death', (c) => {
if (c.unit.side === 'enemy' && unit.mem.atkVictims.includes(c.unit.id)) refresh();
if (c.unit !== unit) return;
for (const id of unit.mem.atkVictims) { const e = battle.unitById(id); if (e) battle.removeBuff(e, `ines:atkSteal:${unit.id}`); }
unit.mem.atkVictims = [];
unit.mem.woven = new Set();
}, { owner: unit });
} },
{ install(battle, unit) { // 影哨: reveal + −30 % move speed in range; a sentry keeps it after she leaves (max 1)
const mods = { moveMul: Math.max(0, 1 + num(t1.move_speed, -0.3)) };
battle.on('death', (c) => {
if (c.unit !== unit || c.reason === 'expired') return;
unit.mem.sentry = new Set(unit.baseRangeKeys || unit.rangeKeys || []);
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
battle.every(AURA, () => {
const set = new Set();
if (unit.alive && unit.deployed) for (const k of unit.rangeKeys || []) set.add(k);
if (unit.mem.sentry) for (const k of unit.mem.sentry) set.add(k);
if (!set.size) return;
for (const e of enemiesOnRange(battle, unit, set)) pulse(battle, e, `ines:sentry:${unit.id}`, mods, { flags: { reveal: true } });
}, { owner: unit });
} },
],
install(battle, unit) {
if (S3) {
// where the talent's 影哨 stays (every leave but an expiry), for the recall of the next deployment
battle.on('death', (c) => { if (c.unit === unit && c.reason !== 'expired') unit.mem.inesSentryAt = { x: unit.x, y: unit.y }; }, { owner: unit, priority: 5 });
// 技能期间每对一个敌人造成伤害就获得1点部署费用
battle.on('damaged', (c) => {
if (c.source !== unit || c.target.side !== 'enemy' || !(c.amount > 0) || c.type === 'element' || !unit.findBuff('ines:s3')) return;
battle.addDp(unit.ownerId, num(bb.cost, 1));
}, { owner: unit });
}
// module (elite): the first retreat's redeploy time is 35 % shorter
const rt = num(tb.respawn_time, 0);
if (!rt) return;
battle.on('death', (c) => {
if (c.unit !== unit || unit.mem.firstRetreatDone || !Number.isFinite(unit.respawnAt)) return;
unit.mem.firstRetreatDone = true;
unit.respawnAt = unit.deathAt + (unit.respawnAt - unit.deathAt) * Math.max(0, 1 + rt);
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,66 @@
// server/sim/content/kits/ops/chess_char_4_05-beewax.js — 蜜蜡 (char_344_beewax) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import {
num, tbb, grid, targetsInRange, targetsInGrid, toggleBuff, skillActive, withDefaults,
} from '../shared/tier4.js';
/**
* Free tile for a summon/device: in the rect, standable, not reserved (Battle.isReservedTile: empty, no knocked-out
* operator lying there) and not the home tile of any ally (dead ones redeploy there).
*/
function freeTile(battle, r, c, { ranged = false, ground = false } = {}) {
if (!Number.isInteger(r) || !Number.isInteger(c) || !battle.grid.inRect(r, c)) return false;
if (battle.isReservedTile(r, c)) return false;
if (!battle.grid.canStand(r, c, { ranged })) return false;
if (ground && !battle.grid.groundPassable(r, c)) return false;
for (const u of battle.allyUnits) if (!u.removed && u.homeR === r && u.homeC === c && u.kind !== 'device') return false;
return true;
}
export default withDefaults({
// ===== 蜜蜡 (phalanx, hidden) S2 守卫尖碑 — obelisk on a melee tile in range: 200 % arts burst + stun, blocks 3
chess_char_4_05_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const tokenId = def.raw?.skill?.overrideTokenKey ?? (def.tokens || [])[0] ?? 'token_10011_beewax_oblisk';
const keepDef = num(tb['soil_e_002[buff].def'], 0), keepRes = num(tb['soil_e_002[buff].magic_resistance'], 0);
const obeliskKit = { skill: null, talents: [], trait: { noAttack: true } };
return {
skill: {
kind: 'duration',
onStart({ battle, unit }) {
if (keepDef || keepRes) battle.addBuff(unit, { key: 'beewax:keep', mods: { defPct: keepDef, resFlat: keepRes } });
// melee deploy tile in range: nearest to the most advanced enemy (path tile first)
const foes = targetsInRange(battle, unit);
const ref = foes[0] || battle.aliveEnemies().filter((e) => !e.hidden).sort((a, b) => battle.remainingDistance(a) - battle.remainingDistance(b))[0] || unit;
let best = null, bd = Infinity;
for (const k of unit.rangeKeys || []) {
const r = (k / COLS) | 0, c = k % COLS;
if (!freeTile(battle, r, c)) continue;
const d = Math.hypot(c - ref.x, r - ref.y) + (battle.grid.groundPassable(r, c) ? 0 : 5);
if (d < bd - 1e-9) { bd = d; best = [r, c]; }
}
if (!best) return;
const tok = battle.tokenDef(tokenId, unit); // the owner's skill / module variant (DESIGN §16)
const dur = num(tok?.talents?.[0]?.bb?.duration, num(def.skill?.duration, 20));
const ob = battle.spawnToken(unit, tokenId, best[0], best[1], { duration: dur, kit: obeliskKit });
if (!ob) return;
unit.mem.obelisk = ob;
battle.fx('obelisk', { x: ob.x, y: ob.y, id: ob.id });
const tokGrid = grid(tok?.skill?.rangeGrid) || [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]];
for (const e of targetsInGrid(battle, ob, tokGrid)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 2), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun, 1), source: unit });
}
},
onEnd({ battle, unit }) { battle.removeBuff(unit, 'beewax:keep'); },
},
talents: [{ install(battle, unit) { // 沙原的庇护: +4 % max HP/s while the skill is off
const set = () => toggleBuff(battle, unit, 'beewax:regen', unit.alive && !skillActive(unit), { hpRegenRatio: num(t0.hp_recovery_per_sec_by_max_hp_ratio, 0.04) });
for (const ev of ['deploy', 'skillStart', 'skillEnd']) battle.on(ev, (c) => { if (c.unit === unit) set(); }, { owner: unit });
} }],
};
},
});
@@ -0,0 +1,50 @@
// server/sim/content/kits/ops/chess_char_4_06-kroos2.js — 寒芒克洛丝 (char_1021_kroos2) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { num, tbb, batFlat, skillActive, isSel, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 寒芒克洛丝 (fastshot) S2 封喉 — double shot → 4 shots after 40 hits; talent 中的 (20 % ×1.5 + 0.2 s stun)
// S1 无痕 (ATK up, double shot, 迷彩 = the engine's `camou` flag: only an enemy she blocks targets her)
chess_char_4_06_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const need = num(bb['attack@max_stack_count'], 40);
const S2 = isSel(def, 'skchr_kroos2_2');
return {
skills: alt(def, {
skchr_kroos2_1: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk) },
flags: { camou: true },
attack: { hits: 2 },
onStart({ battle, unit }) { battle.fx('camouflage', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
mods: { batPct: batFlat(def, bb.base_attack_time) },
attack: { hitsFn: (battle, unit) => ((unit.mem.kroosHits ?? 0) >= need ? 4 : 2) },
onStart({ unit }) { unit.mem.kroosHits = 0; },
},
talents: [{ install(battle, unit) {
// 中的: the ×1.5 is rolled before mitigation; the stun (and the S2 "击中目标" count) only for shots that landed
const crits = new WeakSet();
battle.on('hit', (c) => {
if (c.source !== unit || !c.dmg.isAttack || c.target.side !== 'enemy') return;
if (battle.rng.chance(num(t0.prob, 0.2))) { c.dmg.mul *= num(t0.atk_scale, 1.5); crits.add(c.dmg); }
}, { owner: unit });
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg || !c.dmg.isAttack || c.target.side !== 'enemy') return;
if (S2 && skillActive(unit)) {
unit.mem.kroosHits = (unit.mem.kroosHits ?? 0) + 1;
if (unit.mem.kroosHits === need) battle.fx('quadShot', { x: unit.x, y: unit.y, id: unit.id });
}
if (!crits.has(c.dmg)) return;
crits.delete(c.dmg);
if (c.target.alive) battle.applyStatus(c.target, 'stun', { duration: num(t0.stun, 0.2), source: unit });
battle.fx('crit', { x: c.target.x, y: c.target.y, id: c.target.id });
}, { owner: unit });
} }],
};
},
});
@@ -0,0 +1,59 @@
// server/sim/content/kits/ops/chess_char_4_07-bpipe.js — 风笛 (char_222_bpipe) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import { num, tbb, batFlat, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 风笛 (charger) S2 高效冲击 — next attack 145 % ×2 (charges); talents 精密填弹 / 军事传统
// S1 迅捷打击·γ型; S3 闭膛连发 (BAT +0.7 s, block +1, ATK/DEF up, triple hits); module CHG-Y (“棍棒与口袋”):
// attacks on enemies below 40 % HP use 115 % ATK
chess_char_4_07_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
return {
skills: alt(def, {
'skcom_quickattack[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) } }),
skchr_bpipe_3: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def), blockCnt: num(bb.block_cnt, 1), batPct: batFlat(def, bb.base_attack_time) },
attack: { hits: 3 },
onStart({ battle, unit }) { battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
install(battle, unit) {
const hr = num(tb.hp_ratio, 0), sc = num(tb.atk_scale, 0);
if (!(hr > 0) || !(sc > 0)) return;
battle.on('hit', (c) => { // 攻击力提升至115% (before mitigation)
if (c.source === unit && c.dmg.isAttack && c.target.side === 'enemy' && c.target.hpRatio < hr) c.dmg.amount *= sc;
}, { owner: unit });
},
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
attack: { atkScale: num(bb.atk_scale, 1.45), hits: 2 },
onStart({ battle, unit }) { battle.fx('charge', { x: unit.x, y: unit.y, id: unit.id }); },
},
talents: [
{ install(battle, unit) { // 精密填弹: 25 % → ×1.3 and one extra target
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit) return;
unit.mem.bpBoost = battle.rng.chance(num(t0.prob, 0.25));
if (!unit.mem.bpBoost) return;
const extra = battle.enemiesInKeys(unit.rangeKeys, unit, c.profile).filter((e) => !c.targets.includes(e));
sortEnemyTargets(battle, unit, extra, c.profile?.priority ?? null);
if (extra[0]) c.targets = [...c.targets, extra[0]];
}, { owner: unit });
battle.on('hit', (c) => { if (c.source === unit && c.dmg.isAttack && unit.mem.bpBoost) c.dmg.mul *= num(t0.atk_scale, 1.3); }, { owner: unit });
battle.on('attack', (c) => { if (c.attacker === unit) unit.mem.bpBoost = false; }, { owner: unit });
} },
{ install(battle, unit) { // 军事传统: every 先锋 of the team starts with +6 SP
battle.on('deploy', (c) => {
const u = c.unit;
if (u.side !== 'ally' || u.kind !== 'op' || u.ownerId !== unit.ownerId || u.def?.profession !== 'PIONEER' || !u.skill || (u.skill.active && u.skill.isTimed)) return;
u.skill.gainSp(num(t1.sp, 6), 'init');
}, { owner: unit });
} },
],
};
},
});
@@ -0,0 +1,47 @@
// server/sim/content/kits/ops/chess_char_4_08-rosesa.js — 瑰盐 (char_4163_rosesa) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import {
AURA, num, tbb, batFlat, keySet, whileDeployed, pulse, skillActive, applyModuleRange, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 瑰盐 (ringhealer, hidden) S2 绝妙的长效药呀 — allies in range: 20 % of phys/arts damage → 5 s HP loss
chess_char_4_08_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const scale = num(bb['attack@damage_scale'], 0.8), fin = num(bb['attack@final_duration'], 5), iv = Math.max(0.1, num(bb['attack@interval'], 1));
return {
skill: { kind: 'duration', heal: true, mods: { batPct: batFlat(def, bb.base_attack_time) },
onStart({ battle, unit }) { battle.fx('saltWard', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [{ install(battle, unit) { // 最好的草药医生: own ATK −5 %, operators in range receive +15 % healing
battle.addBuff(unit, { key: 'rosesa:t1', mods: { atkPct: num(t0.atk, -0.05) }, persist: true, allowDead: true });
whileDeployed(battle, unit, AURA, () => {
for (const a of battle.alliesInGrid(unit)) if (a.kind === 'op') pulse(battle, a, `rosesa:heal:${unit.id}`, { healingTakenMul: num(t0.heal_scale, 1.15) });
});
} }],
install(battle, unit) {
const deferred = new WeakSet();
battle.on('hit', (c) => {
const t = c.target;
// "友方干员": operators only (summons/devices keep the full hit)
if (!skillActive(unit) || !unit.alive || t.side !== 'ally' || t.kind !== 'op' || (c.dmg.type !== 'phys' && c.dmg.type !== 'arts')) return;
if (!keySet(unit).has(t.tileR * COLS + t.tileC)) return;
c.dmg.mul *= scale;
deferred.add(c.dmg);
}, { owner: unit, priority: -5 });
battle.on('damaged', (c) => {
if (!deferred.has(c.dmg) || !(c.amount > 0) || !c.target.alive) return;
deferred.delete(c.dmg);
const total = (c.amount * (1 - scale)) / Math.max(1e-6, scale);
const per = total / Math.max(1, Math.round(fin / iv));
const src = c.source;
battle.addBuff(c.target, { key: 'rosesa:dot', refresh: 'independent', maxStacks: 1000, duration: fin + 1e-6, interval: iv,
onTick: ({ unit: tgt }) => battle.loseHp(tgt, per, { source: src && src.side === 'enemy' ? src : null }) });
}, { owner: unit });
// module (elite) RIN-X: 攻击范围扩大 (module range grid)
applyModuleRange(battle, unit, def);
},
};
},
});
@@ -0,0 +1,62 @@
// server/sim/content/kits/ops/chess_char_4_09-mizuki.js — 水月 (char_437_mizuki) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, moduleBb, grid, batFlat, enemiesOnRange, whileDeployed, pulse, toggleBuff, skillActive, isSel, alt,
instantKind, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 水月 (stalker) S2 囚徒困境 — faster, +ATK, talent 1 +1 target & bind; talents 创伤性癔症 / 反移情; module slow
// S1 唤醒 (charges: next attack 200 %/230 %, talent 1 ×2.0/×2.3); S3 镜花水月 (wider range, ATK up, talent 1 +2
// targets & stun; an attack hitting < 3 enemies costs 15 % max HP); modules: AMB-Y 65 % dodge (trait, profession)
chess_char_4_09_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const S1 = isSel(def, 'skchr_mizuki_1'), S2 = isSel(def, 'skchr_mizuki_2'), S3 = isSel(def, 'skchr_mizuki_3');
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
skchr_mizuki_1: () => ({ kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 2) } }),
skchr_mizuki_3: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk) },
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { battle.fx('ripple', { x: unit.x, y: unit.y, id: unit.id }); },
onAttack({ battle, unit, targets }) {
if ((targets || []).filter((e) => e.side === 'enemy').length >= 3) return;
battle.loseHp(unit, unit.s.maxHp * num(bb['attack@hp_ratio'], 0.15), { tags: ['skill', 'selfLoss'] }); // (no source: not damage dealt)
},
}),
}),
skill: { kind: 'duration', mods: { batPct: batFlat(def, bb.base_attack_time), atkPct: num(bb.atk) },
onStart({ battle, unit }) { battle.fx('dilemma', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [
{ install(battle, unit) { // 创伤性癔症: +50 % ATK arts to the lowest-HP enemy hit (S2 +1 target & bind, S3 +2 & stun, S1 ×2)
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.alive) return;
const act = skillActive(unit) && (S2 || S3);
const n = Math.max(1, Math.floor(num(t0['attack@max_target'], 1))) + (act ? Math.floor(num(bb['attack@max_target'], 1)) : 0);
const mul = S1 && c.isSkill ? num(bb.talent_scale, 2) : 1;
const list = c.targets.filter((e) => e.alive && e.side === 'enemy').sort((a, b) => a.hp - b.hp || a.id - b.id).slice(0, n);
for (const e of list) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(t0['attack@mizuki_t_1.atk_scale'], 0.5) * mul, type: 'arts', tags: ['talent'] });
if (!act || !e.alive) continue;
if (S2) battle.applyStatus(e, 'bind', { duration: num(bb['attack@unmovable'], 0.8), source: unit });
else battle.applyStatus(e, 'stun', { duration: num(bb['attack@stun'], 0.7), source: unit });
}
}, { owner: unit });
} },
{ install(battle, unit) { // 反移情: ATK +10 % while an enemy in range is below 50 % HP
whileDeployed(battle, unit, 0.1, () => {
const on = enemiesOnRange(battle, unit).some((e) => e.hpRatio < num(t1.hp_ratio, 0.5));
toggleBuff(battle, unit, 'mizuki:t2', on, { atkPct: num(t1.atk, 0.1) });
});
} },
],
install(battle, unit) {
const ms = num(mb.move_speed, 0);
if (ms) whileDeployed(battle, unit, AURA, () => { for (const e of enemiesOnRange(battle, unit)) pulse(battle, e, `mizuki:slow:${unit.id}`, { moveMul: Math.max(0, 1 + ms) }); });
},
};
},
});
@@ -0,0 +1,81 @@
// server/sim/content/kits/ops/chess_char_4_10-aroma.js — 阿罗玛 (char_446_aroma) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyInKeys } from '../../../body.js';
import { num, tbb, keySet, skillActive, isSel, alt, instantKind, withDefaults } from '../shared/tier4.js';
/** 阿罗玛's 非首次标记 (talent 起泡性能测试): one per enemy, shared by every 阿罗玛 (PRTS 备注). */
const AROMA_MARK = 'aroma:bubbled';
export default withDefaults({
// ===== 阿罗玛 (blastcaster) S2 小心地滑 — +ATK, levitated enemies take 65 % ATK arts on landing; talent 起泡性能测试
// S1 强效清洁 (charges: next attack 150 %/180 % arts, flying victims +55 %/70 % ATK arts)
chess_char_4_10_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const land = num(bb['attack@atk_scale_when_fly_finish'], 0.65);
const S2 = isSel(def, 'skchr_aroma_2');
return {
skills: alt(def, {
skchr_aroma_1: () => ({
kind: instantKind(def),
attack: {
atkScale: num(bb.atk_scale, 1.5),
onEachHit({ battle, unit, target }) { // every enemy of her line the blast strikes that flies
if (target && target.alive && target.isFlying) battle.dealDamage(unit, target, { amount: unit.s.atk * num(bb.atk_scale_to_fly, 0.55), type: 'arts', isSkill: true, tags: ['skill', 'antiAir'] });
},
},
}),
}),
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, onStart({ battle, unit }) { battle.fx('slippery', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [{ install(battle, unit) { // first attack on each enemy: ×1.1 and levitate 2.5 s
// PRTS 阿罗玛 备注: "天赋采用施加非首次标记的方式判断是否为'首次进行攻击',自身离场时移除自身已施加的标记,不同阿罗玛之间的
// 非首次标记通用" — one mark per enemy (AROMA_MARK, the setter as source) for every 阿罗玛; hers go when she leaves
// the field (until 0.1.1 each 阿罗玛 kept her own list, so two of them both triggered on one enemy)
unit.mem.bubbled = new Set(); // the enemies she marked
battle.on('hit', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg.isAttack || e.side !== 'enemy' || e.findBuff(AROMA_MARK)) return;
battle.addBuff(e, { key: AROMA_MARK, source: unit });
unit.mem.bubbled.add(e.id);
c.dmg.mul *= num(t0.damage_scale, 1.1);
if (battle.applyStatus(e, 'levitate', { duration: num(t0.levitate_duration, 2.5), source: unit })) battle.fx('levitate', { x: e.x, y: e.y, id: e.id });
}, { owner: unit });
battle.on('death', (c) => {
if (c.unit !== unit) return;
for (const id of unit.mem.bubbled) {
const e = battle.unitById(id);
if (e && e.findBuff(AROMA_MARK)?.source === unit) battle.removeBuff(e, AROMA_MARK);
}
unit.mem.bubbled.clear();
}, { owner: unit });
} }],
install(battle, unit) {
// S2: landing damage for every levitated enemy (any source) that lands inside her range while the skill runs
const floating = new Map();
battle.on('statusApplied', (c) => { if (c.status === 'levitate' && c.target.side === 'enemy') floating.set(c.target.id, c.target); }, { owner: unit });
battle.every(1 / 30, () => {
for (const [id, e] of floating) {
if (!e.alive) { floating.delete(id); continue; }
if (e.findBuff('levitate')) continue;
floating.delete(id);
if (S2 && unit.alive && unit.deployed && skillActive(unit) && bodyInKeys(e, keySet(unit))) {
battle.dealDamage(unit, e, { amount: unit.s.atk * land, type: 'arts', isSkill: true, tags: ['skill', 'landing'] });
battle.fx('splash', { x: e.x, y: e.y, id: e.id });
}
}
}, { owner: unit });
// module (elite) trait: farther targets take more damage (up to ×1.1)
const ds = num(tb.damage_scale, 0);
if (ds > 0) {
const lo = num(tb.min_dist, 0), hi = num(tb.max_dist, 4);
battle.on('hit', (c) => {
if (c.source !== unit || !c.dmg.isAttack) return;
const d = Math.hypot(c.target.x - unit.x, c.target.y - unit.y);
c.dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo)));
}, { owner: unit });
}
},
};
},
});
@@ -0,0 +1,38 @@
// server/sim/content/kits/ops/chess_char_4_11-cathy.js — 凯瑟琳 (char_4162_cathy) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { CAT_SHIELD_KEY } from '../../tokens.js';
import { AURA, num, whileDeployed, pulse, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 凯瑟琳 (craftsman) S2 战火淬炼 — stops attacking, HP/DEF up, devices give 6 %/s shields; talent 定向支援信号
// "携带3个支援装置(最多部署2个)": the 爬行号·防护单元 are hand pieces (2 = the deploy limit) the player places and
// turns towards an operator (user playtest #6, PRTS 卫戍协议/帮助 §战斗部署); they deploy with the board and the
// token kit (tokens.js catShield) gives the shields — S2 overwrite_ratio per second while this skill runs — so
// this kit places nothing (none placed ⇒ no device).
// S1 岁月锻打 (passive: she and every other operator holding a device shield ATK/DEF +8 %/+11 %); module CRA-X
// carries one more device (talent cnt, data — a spare the hand never shows)
chess_char_4_11_a: (bb, chess, def) => ({
skills: alt(def, {
skchr_cathy_1: () => ({
kind: 'passive',
mods: { atkPct: num(bb.s1_atk), defPct: num(bb.s1_def) },
onStart({ battle, unit }) {
unit.mem.forgeAura?.cancel();
unit.mem.forgeAura = whileDeployed(battle, unit, AURA, () => {
for (const a of battle.allies(unit.ownerId)) {
if (a === unit || a.kind !== 'op' || !a.findBuff(CAT_SHIELD_KEY)) continue;
pulse(battle, a, `cathy:forge:${unit.id}`, { atkPct: num(bb.s1_atk), defPct: num(bb.s1_def) });
}
});
},
}),
}),
skill: {
kind: 'duration',
mods: { hpPct: num(bb.max_hp), defPct: num(bb.def) },
attack: { noAttack: true },
onStart({ battle, unit }) { battle.fx('overclock', { x: unit.x, y: unit.y, id: unit.id }); },
},
}),
});
@@ -0,0 +1,100 @@
// server/sim/content/kits/ops/chess_char_4_12-glady.js — 歌蕾蒂娅 (char_474_glady) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyDist } from '../../../body.js';
import {
AURA, num, tbb, moduleBb, grid, batFlat, enemyHasTag, targetsInRange, whileDeployed, pulse, alt, instantKind,
pullToFront, withDefaults,
} from '../shared/tier4.js';
const ABYSSAL = new Set(['char_143_ghost', 'char_263_skadi', 'char_474_glady', 'char_4145_ulpia', 'char_1023_ghost2']);
const isAbyssal = (u) => u?.def?.raw?.groupId === 'abyssal' || ABYSSAL.has(u?.def?.charId ?? u?.def?.raw?.charId);
export default withDefaults({
// ===== 歌蕾蒂娅 (hookmaster) S3 缺水的碎漩狂舞 — bind a far target, tornado: slow, 85 % arts pulses + pull, final pull
// S1 缺水的大洋裂断 (charges: next attack pulls the target to her front, 150 %/180 %); S2 缺水的掌握怒海 (BAT +0.5 s,
// wider range, ≤ 2 targets — blocked first — at 135 %/150 % and pulled to her front); module HOK-Y (淡金坠饰):
// a pull towards herself of an enemy farther than 2.5 tiles is one force level stronger
chess_char_4_12_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const tb = def.traitBb || {};
const R = 1.5; // [ASSUMED] tornado radius (no blackboard key)
const force = num(bb.force, num(bb['attack@force'], 0));
const iv = Math.max(0.1, num(bb.interval, 1.5));
const g = grid(def.skill?.rangeGrid);
const dragDmg = (battle, unit, e, moved) => { // module: dragged enemies take arts damage ∝ distance
if (!(moved > 0) || !(num(tb.value, 0) > 0) || !e.alive) return;
battle.dealDamage(unit, e, { amount: num(tb.value) * moved / Math.max(1e-6, num(tb.dist, 1)), type: 'arts', isSkill: true, tags: ['drag'] });
};
// module HOK-Y: "向自身拖拽较远的敌人时力度提升一个等级"
const farR = num(mb['skill@range_radius'], num(mb['attack@range_radius'], 0)), farF = num(mb['skill@delta_force'], num(mb['attack@delta_force'], 0));
const selfForce = (unit, e, f) => (farR > 0 && farF > 0 && Math.hypot(e.x - unit.x, e.y - unit.y) > farR + 1e-9 ? f + farF : f);
const pullSelf = (battle, unit, e, f) => { if (e && e.alive) dragDmg(battle, unit, e, pullToFront(battle, unit, e, selfForce(unit, e, f))); };
return {
skills: alt(def, {
skchr_glady_1: () => ({
kind: instantKind(def),
attack: { atkScale: num(bb.atk_scale, 1.5), onHit({ battle, unit, target }) { pullSelf(battle, unit, target, num(bb.force, 1)); } },
}),
skchr_glady_2: () => ({
kind: 'duration',
mods: { batPct: batFlat(def, bb.base_attack_time) },
// (target sorting already puts the enemies she blocks first)
targeting: { ...(g ? { rangeGrid: g } : {}), maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 2))) },
attack: { atkScale: num(bb['attack@atk_scale'], 1.35), onEachHit({ battle, unit, target }) { pullSelf(battle, unit, target, num(bb['attack@force'], 1)); } },
onStart({ battle, unit }) { battle.fx('wake', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
onStart({ battle, unit, skill }) {
const foes = targetsInRange(battle, unit).sort((a, b) => bodyDist(b, unit.x, unit.y) - bodyDist(a, unit.x, unit.y) || a.id - b.id);
const t = foes[0];
unit.mem.tornado = t ? { x: t.x, y: t.y, acc: 0 } : null;
if (!t) return;
battle.applyStatus(t, 'bind', { duration: skill.duration, source: unit });
battle.fx('tornado', { x: t.x, y: t.y, id: unit.id, r: R, duration: skill.duration });
},
onTick({ battle, unit, dt }) {
const T = unit.mem.tornado;
if (!T) return;
const inside = battle.foesInRadius(T.x, T.y, R);
for (const e of inside) pulse(battle, e, `glady:slow:${unit.id}`, { moveMul: Math.max(0, 1 + num(bb.move_speed, -0.5)) });
T.acc += dt;
if (T.acc + 1e-9 < iv) return;
T.acc -= iv;
for (const e of inside) {
if (!e.alive || e.s.flags.untargetable) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 0.85), type: 'arts', isSkill: true, tags: ['skill', 'tornado'] });
// "小力地拖拽至中心": a pull to the marked point (PRTS 推与拉: its 6 tornado pulls stop 0.05 from it)
if (e.alive) dragDmg(battle, unit, e, battle.pull(e, force, { to: T, stop: 0.05 }));
}
battle.fx('tornadoPulse', { x: T.x, y: T.y, id: unit.id, r: R });
},
onEnd({ battle, unit, reason }) {
const T = unit.mem.tornado;
unit.mem.tornado = null;
if (!T || reason === 'death' || !unit.alive) return;
for (const e of battle.foesInRadius(T.x, T.y, R)) pullSelf(battle, unit, e, force);
battle.fx('pull', { x: T.x, y: T.y, id: unit.id });
},
},
talents: [
{ install(battle, unit) { // 阿戈尔的波涛: 深海猎人 regen 2.5 %/s, −25 % phys/arts damage from 海怪
whileDeployed(battle, unit, AURA, () => {
for (const a of battle.allies(unit.ownerId)) if (isAbyssal(a)) pulse(battle, a, 'glady:tide', { hpRegenRatio: num(t0.hp_recovery_per_sec_by_max_hp_ratio, 0.025) });
});
battle.on('hit', (c) => {
if (!c.target.findBuff?.('glady:tide') || !enemyHasTag(c.source, 'seamonster') || (c.dmg.type !== 'phys' && c.dmg.type !== 'arts')) return;
c.dmg.mul *= 1 - num(t0.damage_resistance, 0.25);
}, { owner: unit });
} },
{ install(battle, unit) { // 弱肉强食: ×1.3 against enemies of weight ≤ 3
battle.on('hit', (c) => {
if (c.source === unit && c.dmg.isAttack && c.target.side === 'enemy' && c.target.s.massLevel <= num(t1.value, 3)) c.dmg.mul *= num(t1.atk_scale, 1.3);
}, { owner: unit });
} },
],
};
},
});
@@ -0,0 +1,112 @@
// server/sim/content/kits/ops/chess_char_4_13-gnosis.js — 灵知 (char_206_gnosis) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import {
AURA, AURA_DUR, num, tbb, nationOf, enemiesOnRange, targetsInRange, whileDeployed, toggleBuff, skillActive, isSel,
alt, instantKind, withDefaults,
} from '../shared/tier4.js';
/** Enemy leaders / elites (精英或领袖敌人). */
const isEliteEnemy = (e) => !!e && (e.isBoss || e.def?.rank === 'ELITE' || e.def?.rank === 'BOSS');
export default withDefaults({
// ===== 灵知 (underminer) S2 零度爆发 — charges: 130 % arts + cold to all in range, charged ⇒ 2nd cold; 坚冰 / 殊途同归
// S1 高速思考 (next attack: 2 × 135 %/150 % arts); S3 失温症 (ASPD up, 2 targets — unfrozen first; frozen enemies
// in range stay frozen until the skill ends, then take 300 %/400 % arts and thaw); module UMD-Y (一号项目模型):
// +0.25 SP/s while an elite / leader enemy is in range
chess_char_4_13_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const S3 = isSel(def, 'skchr_gnosis_3');
const frozen = (e) => !!e.findBuff('freeze');
return {
skills: alt(def, {
skchr_gnosis_1: () => ({ kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 1.35), hits: 2 } }),
skchr_gnosis_3: () => ({
kind: 'duration',
mods: { aspd: num(bb.attack_speed) },
targeting: { maxTargets: Math.max(1, Math.floor(num(bb.max_target, 2))) },
onStart({ battle, unit }) { unit.mem.hypothermia = new Set(); battle.fx('coldWind', { x: unit.x, y: unit.y, id: unit.id }); },
onTick({ battle, unit, skill }) {
const H = unit.mem.hypothermia;
if (!H) return;
// 范围内所有敌人的冻结延长至技能结束
for (const e of enemiesOnRange(battle, unit)) {
const f = e.findBuff('freeze');
if (!f) continue;
H.add(e.id);
if (f.timeLeft < skill.timeLeft) { f.timeLeft = skill.timeLeft; f.duration = Math.max(f.duration, skill.timeLeft); }
}
},
onEnd({ battle, unit, reason }) {
const H = unit.mem.hypothermia;
unit.mem.hypothermia = null;
if (!H || reason === 'death' || !unit.alive) return;
// 技能结束时对所有冻结的敌人造成N%的法术伤害并结束冻结 (the frozen enemies of her range)
const list = enemiesOnRange(battle, unit).filter(frozen);
for (const id of H) { const e = battle.unitById(id); if (e && e.alive && frozen(e) && !list.includes(e)) list.push(e); }
for (const e of list) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 3), type: 'arts', isSkill: true, tags: ['skill', 'shatter'] });
if (e.alive) battle.removeStatus(e, 'freeze');
}
battle.fx('iceSpike', { x: unit.x, y: unit.y, id: unit.id, n: list.length });
},
}),
}),
install(battle, unit) {
if (S3) {
// 优先攻击未冻结的单位 (while S3 runs)
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillActive(unit)) return;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, c.profile);
for (const e of battle.blockedTargets(unit, c.profile)) if (!cands.includes(e)) cands.push(e); // DESIGN §20.3
if (cands.length <= 1) return;
sortEnemyTargets(battle, unit, cands, c.profile?.priority ?? null);
const n = Math.max(1, Math.floor((c.profile?.maxTargets || 1) + unit.s.maxTargets));
c.targets = [...cands.filter((e) => !frozen(e)), ...cands.filter(frozen)].slice(0, n);
}, { owner: unit, priority: 5 });
}
const sp = num(tb.sp_recovery_per_sec, 0); // module UMD-Y
if (sp > 0) whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'gnosis:module', enemiesOnRange(battle, unit).some(isEliteEnemy), { spRecoveryFlat: sp }));
},
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
onStart({ battle, unit, skill }) {
const charged = skill.maxCharges > 1 && skill.charges + 1 >= skill.maxCharges; // cast with every charge stored
for (const e of targetsInRange(battle, unit)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.3), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
if (!e.alive) continue;
battle.applyStatus(e, 'cold', { duration: num(bb.cold, 2.5), source: unit });
if (charged && e.alive) battle.applyStatus(e, 'cold', { duration: num(bb.cold, 2.5), source: unit });
}
battle.fx('frostNova', { x: unit.x, y: unit.y, id: unit.id, charged });
},
},
talents: [
{ install(battle, unit) { // 坚冰: attacks chill 1 s; cold enemies in range fragile 25 %, frozen 50 %
battle.on('damaged', (c) => {
if (c.source === unit && c.dmg?.isAttack && !c.dmg.isSplash && c.target.side === 'enemy' && c.target.alive) battle.applyStatus(c.target, 'cold', { duration: num(t0.cold, 1), source: unit });
}, { owner: unit });
whileDeployed(battle, unit, 0.1, () => {
for (const e of enemiesOnRange(battle, unit)) {
const f = e.s.flags;
const m = f.freeze ? num(t0.damage_scale_freeze, 1.5) : f.cold ? num(t0.damage_scale_cold, 1.25) : 0;
// 同名效果取最高: one 坚冰 per enemy, the strongest — two 灵知 (a pair / 联防 partner's copy) never compound
if (m) battle.applyStrongest(e, 'gnosis:fragile', { duration: 0.15, value: m, mods: (v) => ({ dmgTakenMul: v }), source: unit });
}
});
} },
{ install(battle, unit) { // 殊途同归: 10 s after deployment every 谢拉格 operator gains 抵抗 (negative status durations ×0.5)
// engine `resist` status (applied before a new status lands, so a longer one already running is never
// shortened; 同名效果不叠加: several 灵知 — or 流明's 抵抗 — never compound)
const value = Math.min(1, Math.max(0, -num(t1.one_minus_status_resistance, -0.5)));
whileDeployed(battle, unit, AURA, () => {
if (battle.time - unit.deployedAt < num(t1.interval, 10) - 1e-9 || !(value > 0)) return;
for (const a of battle.allies(unit.ownerId)) if (a.kind === 'op' && nationOf(a) === 'kjerag') battle.applyStatus(a, 'resist', { duration: AURA_DUR, value, source: unit });
});
} },
],
};
},
});
@@ -0,0 +1,37 @@
// server/sim/content/kits/ops/chess_char_4_14-lionhd.js — 莱恩哈特 (char_373_lionhd) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
num, tbb, grid, targetsInRange, whileDeployed, toggleBuff, resCut, alt, applyModuleRange, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 莱恩哈特 (splashcaster) S2 解构与爆破 — charges: 170 % arts to all in the wider range + RES −8 % for 6 s; 破片杀伤
chess_char_4_14_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const g = grid(def.skill?.rangeGrid);
const mr = num(bb.magic_resistance, 0);
return {
skills: alt(def, { 'skcom_atk_up[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk) } }) }), // S1 攻击力强化·γ型
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) {
for (const e of targetsInRange(battle, unit)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.7), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
// 同名效果取最高: one RES cut per enemy (the strongest), never one per 莱恩哈特
if (e.alive && mr) battle.applyStrongest(e, 'lionhd:res', { duration: num(bb.duration, 6), value: mr, mods: resCut, source: unit });
}
battle.fx('explosion', { x: unit.x, y: unit.y, id: unit.id });
},
},
talents: [{ install(battle, unit) { // 破片杀伤: ATK +4 % per enemy in range (≤ 5)
whileDeployed(battle, unit, 0.1, () => {
const n = Math.min(num(t0.max_valid_stack_cnt, 5), targetsInRange(battle, unit).length);
toggleBuff(battle, unit, 'lionhd:t1', n > 0, { atkPct: num(t0.atk, 0.04) * n });
});
} }],
install(battle, unit) { applyModuleRange(battle, unit, def); }, // module SPC-X (elite default): 攻击范围扩大
};
},
});
@@ -0,0 +1,41 @@
// server/sim/content/kits/ops/chess_char_4_15-reckpr.js — 录武官 (char_4196_reckpr) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { isHpLoss } from '../../../damage.js';
import { num, tbb, keySet, skillActive, installLowHpHealBonus, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 录武官 (physician, hidden) S2 一点关窍 — +ATK; healed allies regain 80 HP per hit taken for 10 s; 学成于聚
chess_char_4_15_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
return {
skill: {
kind: 'duration', heal: true,
mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) { battle.fx('knack', { x: unit.x, y: unit.y, id: unit.id }); },
onHit({ battle, unit, target }) {
if (target && target.alive && target.side === 'ally' && target.kind === 'op') battle.addBuff(target, { key: `reckpr:guard:${unit.id}`, duration: num(bb['attack@buff_duration'], 10), visible: true, source: unit });
},
},
talents: [{ install(battle, unit) { // 学成于聚: an operator in range casts ⇒ +1 SP and ASPD +16 for 8 s
battle.on('skillStart', (c) => {
const a = c.unit;
if (a === unit || a.side !== 'ally' || a.kind !== 'op' || !unit.alive || !unit.deployed || !keySet(unit).has(a.tileR * COLS + a.tileC)) return;
if (!battle.rng.chance(num(t0.prob, 1))) return;
if (!skillActive(unit)) unit.skill?.gainSp(num(t0.sp, 1), 'talent');
battle.addBuff(unit, { key: 'reckpr:t1', duration: num(t0.duration, 8), mods: { aspd: num(t0.attack_speed, 16) }, maxStacks: Math.max(1, num(t0.max_stack_cnt, 1)) });
}, { owner: unit });
} }],
install(battle, unit) {
battle.on('damaged', (c) => {
const t = c.target;
if (t.side !== 'ally' || !t.alive || !(c.amount > 0) || isHpLoss(c.dmg) || !t.findBuff(`reckpr:guard:${unit.id}`)) return;
battle.heal(unit, t, num(bb['attack@fixed_heal_value'], 80));
}, { owner: unit });
installLowHpHealBonus(battle, unit, tb);
},
};
},
});
@@ -0,0 +1,115 @@
// server/sim/content/kits/ops/chess_char_4_16-texas2.js — 缄默德克萨斯 (char_1028_texas2) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import {
AURA, num, tbb, grid, targetsInGrid, whileDeployed, toggleBuff, resCut, isSel, alt, lonely, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 缄默德克萨斯 (executor) S3 剑雨滂沱 (passive) — deploy burst 2×115 % + 1.5 s stun, sword rain 1/s; talents
// S1 细雨无声 (passive, for the skill duration: ATK up; hits silence the target 5 s/8 s — 失去特殊能力 — with
// 260/320 arts per s meanwhile); S2 阵雨连绵 (passive: deploy burst 150 %/180 % arts + RES −15 %/−20 % around her;
// for the skill duration ATK up and attacks become arts double hits). Talent 德克萨斯传统 applies to every passive.
// The S3 rain hits air units (PRTS 备注 "效果可对空"); so does the S2 deploy burst [ASSUMED: no note].
chess_char_4_16_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid) || [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]];
const S1 = isSel(def, 'skchr_texas2_1'), S2 = isSel(def, 'skchr_texas2_2');
const dur = num(def.skill?.duration, S1 ? 11 : S2 ? 8 : 6);
const talentAtk = (battle, unit) => battle.addBuff(unit, { key: 'texas2:rainAtk', duration: dur, mods: { atkPct: num(t0.atk, 0.2) } });
const castS1 = (battle, unit) => {
if (!unit.alive || !unit.deployed) return;
talentAtk(battle, unit);
battle.addBuff(unit, { key: 'texas2:drizzle', duration: dur, mods: { atkPct: num(bb.atk) }, visible: true });
battle.fx('swordRain', { x: unit.x, y: unit.y, id: unit.id });
};
const castS2 = (battle, unit) => {
if (!unit.alive || !unit.deployed) return;
talentAtk(battle, unit);
battle.addBuff(unit, { key: 'texas2:shower', duration: dur, mods: { atkPct: num(bb.atk) }, visible: true });
const mr = num(bb.magic_resistance, 0);
for (const e of targetsInGrid(battle, unit, g)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.5), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
// 同名效果取最高: one RES cut per enemy (the strongest), never one per 缄默德克萨斯
if (e.alive && mr) battle.applyStrongest(e, 'texas2:resDown', { duration: num(bb.debuff_duration, 8), value: mr, mods: resCut, source: unit });
}
battle.fx('swordStorm', { x: unit.x, y: unit.y, id: unit.id });
};
const castS3 = (battle, unit) => {
if (!unit.alive || !unit.deployed) return;
talentAtk(battle, unit);
for (const e of targetsInGrid(battle, unit, g)) {
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['appear.atk_scale'], 1.15), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb['appear.stun'], 1.5), source: unit });
}
battle.fx('swordStorm', { x: unit.x, y: unit.y, id: unit.id });
unit.mem.rainUntil = battle.time + dur;
unit.mem.rainTimer?.cancel();
unit.mem.rainTimer = battle.every(Math.max(0.1, num(bb['texas2_s_3[sword].interval'], 1)), (b, sched) => {
if (!unit.alive || !unit.deployed || b.time > unit.mem.rainUntil + 1e-9) { sched.cancel(); return; }
const list = targetsInGrid(b, unit, g);
sortEnemyTargets(b, unit, list, null);
for (const e of list.slice(0, Math.max(1, Math.floor(num(bb.max_target, 2))))) {
b.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 0.85), type: 'arts', isSkill: true, tags: ['skill', 'swordRain'] });
if (e.alive) b.applyStatus(e, 'stun', { duration: num(bb.stun, 0.2), source: unit });
b.fx('swordRain', { x: e.x, y: e.y, id: e.id });
}
}, { owner: unit });
};
const cast = S1 ? castS1 : S2 ? castS2 : castS3;
return {
skill: { kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } },
skills: alt(def, {
skchr_texas2_1: () => ({ kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } }),
skchr_texas2_2: () => ({ kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } }),
}),
// S2: 攻击变为二连击 while the passive lasts
trait: S2 ? { hitsFn: (b, u) => (u.findBuff('texas2:shower') ? 2 : 1) } : undefined,
talents: [
{ install(battle, unit) { // 德克萨斯传统 (2nd half): first kill of each deployment ⇒ full heal + recast
// (the passive's deploy burst runs inside skill.reset, BEFORE the `deploy` hook: the per-deployment state is
// therefore reset when she leaves the field, so a kill by the deploy burst itself counts)
battle.on('death', (c) => { if (c.unit === unit) unit.mem.texasKilled = false; }, { owner: unit });
battle.on('kill', (c) => {
if (c.killer !== unit || !unit.alive || c.victim.side !== 'enemy' || unit.mem.texasKilled) return;
unit.mem.texasKilled = true;
battle.removeBuff(unit, 'texas2:swordplay');
battle.heal(unit, unit, unit.s.maxHp * num(t0.hp_ratio, 1), { self: true });
cast(battle, unit);
}, { owner: unit });
} },
{ install(battle, unit) { // 德克萨斯剑术: until her first kill after each deployment: ASPD +8, −25 % damage taken
battle.on('deploy', (c) => {
if (c.unit !== unit || unit.mem.texasKilled) return; // the deploy burst may already have scored the kill
battle.addBuff(unit, { key: 'texas2:swordplay', mods: { aspd: num(t1.attack_speed, 8), dmgTakenMul: 1 - num(t1.damage_resistance, 0.25) } });
}, { owner: unit });
} },
],
install(battle, unit) {
if (S1) {
// 攻击使命中目标失去特殊能力N秒,期间目标每秒受到X点法术伤害 (landed normal attacks while the passive lasts)
const sil = num(bb['attack@silence'], 5), dotDur = num(bb['attack@texas2_s_1[dot].duration'], sil);
const dot = num(bb['attack@texas2_s_1[dot].dot_damage'], 260), dotIv = Math.max(0.1, num(bb['attack@texas2_s_1[dot].interval'], 1));
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg?.isAttack || e.side !== 'enemy' || !e.alive || !unit.findBuff('texas2:drizzle')) return;
battle.applyStatus(e, 'silence', { duration: sil, source: unit });
battle.addBuff(e, { key: `texas2:drizzleDot:${unit.id}`, duration: dotDur + 1e-6, interval: dotIv, source: unit, refresh: 'extend', // a re-hit refreshes it, the per-second ticks keep their rhythm
onTick: ({ unit: t }) => battle.dealDamage(unit, t, { amount: dot, type: 'arts', isSkill: true, tags: ['skill', 'dot'] }) });
}, { owner: unit });
}
if (S2) {
// 攻击…造成法术伤害 while the passive lasts (the double hit is the kit trait's hitsFn)
battle.on('hit', (c) => {
if (c.source === unit && c.dmg.isAttack && c.dmg.type === 'phys' && unit.findBuff('texas2:shower')) c.dmg.type = 'arts';
}, { owner: unit, priority: 50 });
}
const a = num(tb.atk, 0); // module (elite): ATK +10 % with no ally on the 4 adjacent tiles
if (!a) return;
whileDeployed(battle, unit, AURA, () => toggleBuff(battle, unit, 'texas2:module', lonely(battle, unit), { atkPct: a }));
},
};
},
});
@@ -0,0 +1,50 @@
// server/sim/content/kits/ops/chess_char_4_17-hsguma.js — 星熊 (char_136_hsguma) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { AURA, num, tbb, whileDeployed, pulse, toggleBuff, isSel, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 星熊 (protector) S2 荆棘 (passive) — DEF +13 %, counters every attacker for 65 % ATK phys; talents
// S1 战意 (TAKE_DAMAGE: DEF/ATK up); S3 力之锯 (ATK/DEF up, cuts every enemy on her front tile — all enemies of her
// range); module PRO-X (护身符): DEF +20 % while blocking
chess_char_4_17_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const S2 = isSel(def, 'skchr_hsguma_2');
return {
skills: alt(def, {
skchr_hsguma_1: () => ({ kind: 'duration', mods: { defPct: num(bb.def), atkPct: num(bb.atk) } }),
skchr_hsguma_3: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
targeting: { allInRange: true },
onStart({ battle, unit }) { battle.fx('overclock', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: { kind: 'passive', mods: { defPct: num(bb.def) } },
talents: [
{ install(battle, unit) { // 战术装甲: 25 % 伤害抵挡 — negates an enemy phys/arts damage instance ("抵挡一次物理或法术伤害")
battle.on('hit', (c) => { // 抵挡 is target-side: a 无来源 burst (source null) counts via the enemy credited with it
const src = c.source || c.credit;
if (c.target !== unit || !src || src.side !== 'enemy' || c.dmg.cancel || (c.dmg.type !== 'phys' && c.dmg.type !== 'arts')) return;
if (battle.rng.chance(num(t0.prob, 0.25))) { c.dmg.cancel = true; battle.fx('block', { x: unit.x, y: unit.y, id: unit.id }); }
}, { owner: unit });
} },
{ install(battle, unit) { // 特种作战策略: 重装 allies DEF +6 %
whileDeployed(battle, unit, AURA, () => { for (const a of battle.allies(unit.ownerId)) if (a.def?.profession === 'TANK') pulse(battle, a, 'hsguma:def', { defPct: num(t1.def, 0.06) }); });
} },
],
install(battle, unit) {
const bd = num(tb.def, 0); // module PRO-X: 阻挡敌人时防御力+20%
if (bd) whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'hsguma:module', unit.blocking.length > 0, { defPct: bd }));
if (!S2) return;
battle.on('damaged', (c) => {
const src = c.source;
if (c.target !== unit || !unit.alive || !src || src.side !== 'enemy' || !src.alive || !c.dmg?.isAttack) return;
battle.dealDamage(unit, src, { amount: unit.s.atk * num(bb.atk_scale, 0.65), type: 'phys', canDodge: false, isSkill: true, tags: ['counter'] });
battle.fx('thorns', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,101 @@
// server/sim/content/kits/ops/chess_char_4_18-mudrok.js — 泥岩 (char_311_mudrok) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, grid, batFlat, enemyHasTag, targetsInGrid, whileDeployed, pulse, toggleBuff, alt, lonely,
withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 泥岩 (unyield) S2 岩崩锤 — next attack: heal 4 %, 190 % phys to all ground enemies around, 30 % stun; talents
// S1 防御力强化·γ型 (TAKE_DAMAGE); S3 秽壤的血脉 (10 s dormant — cannot act, takes no damage, keeps blocking —
// with enemies around −60 % speed; then ground enemies around stunned 3 s/3.5 s and for the rest of the skill
// BAT −0.3 s, ATK/DEF up, attacks every blocked enemy); module UNY-Y (沃土的愿景): ATK/DEF +8 % with no ally
// on the 8 tiles around
chess_char_4_18_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid) || [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]];
const sleep = num(bb.sleep, 10);
return {
skills: alt(def, {
'skcom_def_up[3]': () => ({ kind: 'duration', mods: { defPct: num(bb.def) } }),
skchr_mudrok_3: () => ({
kind: 'duration',
attack: { hitAllBlocked: true },
onStart({ battle, unit }) {
unit.mem.mudS3 = { awake: false, t: 0 };
battle.addBuff(unit, { key: 'mudrok:dormant', duration: sleep + 1, flags: { invulnerable: true, disarm: true }, visible: true });
battle.fx('stone', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
const M = unit.mem.mudS3;
if (!M || M.awake) return;
M.t += dt;
for (const e of targetsInGrid(battle, unit, g)) pulse(battle, e, `mudrok:slow:${unit.id}`, { moveMul: Math.max(0, 1 + num(bb.move_speed, -0.6)) });
if (M.t + 1e-9 < sleep) return;
M.awake = true;
battle.removeBuff(unit, 'mudrok:dormant');
for (const e of targetsInGrid(battle, unit, g)) if (!e.isFlying && e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun, 3), source: unit });
battle.addBuff(unit, { key: 'mudrok:awake', mods: { atkPct: num(bb.atk), defPct: num(bb.def), batPct: batFlat(def, bb.base_attack_time) } });
battle.fx('rockfall', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) {
unit.mem.mudS3 = null;
battle.removeBuff(unit, 'mudrok:dormant');
battle.removeBuff(unit, 'mudrok:awake');
},
}),
}),
skill: {
kind: 'instant',
targeting: { rangeGrid: g, allInRange: true, canHitFly: false },
attack: {
atkScale: num(bb.atk_scale, 1.9), groundOnly: true,
onHit({ battle, unit, target }) {
if (target && target.alive && battle.rng.chance(num(bb.buff_prob, 0.3))) battle.applyStatus(target, 'stun', { duration: num(bb.stun, 0.5), source: unit });
},
},
onStart({ battle, unit }) {
battle.heal(unit, unit, unit.s.maxHp * num(bb.hp_ratio, 0.04), { self: true });
battle.fx('rockslide', { x: unit.x, y: unit.y, id: unit.id });
},
},
talents: [
{ install(battle, unit) { // 沃土予身: a hit-negating layer every 9 s (≤ 3, 1 on deploy); each broken layer heals 20 %
const setLayers = (n) => {
unit.mem.layers = n;
if (n > 0) battle.addBuff(unit, { key: 'mudrok:layers', shieldHits: n, visible: true });
else battle.removeBuff(unit, 'mudrok:layers');
};
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
setLayers(Math.min(num(t0.max_times, 3), num(t0.times, 1)));
unit.mem.layerTimer?.cancel();
unit.mem.layerTimer = whileDeployed(battle, unit, num(t0.interval, 9), () => {
if ((unit.mem.layers ?? 0) < num(t0.max_times, 3)) setLayers(Math.min(num(t0.max_times, 3), (unit.mem.layers ?? 0) + num(t0.times, 1)));
});
}, { owner: unit });
battle.on('damaged', (c) => {
if (c.target !== unit || !unit.alive) return;
const left = unit.findBuff('mudrok:layers')?.shieldHits ?? 0;
const broken = (unit.mem.layers ?? 0) - left;
if (broken <= 0) return;
unit.mem.layers = left;
battle.heal(unit, unit, unit.s.maxHp * num(t0.hp_ratio, 0.2) * broken, { self: true });
battle.fx('shieldBreak', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
} },
{ install(battle, unit) { // 手足相惜: −30 % damage from 萨卡兹 enemies
battle.on('hit', (c) => { if (c.target === unit && enemyHasTag(c.source, 'sarkaz')) c.dmg.mul *= 1 - num(t1.damage_resistance, 0.3); }, { owner: unit });
} },
],
install(battle, unit) {
const ds = num(tb.damage_scale, 0); // module (elite): −15 % damage from enemies it blocks
if (ds > 0) battle.on('hit', (c) => { if (c.target === unit && c.source && c.source.blockedBy === unit) c.dmg.mul *= ds; }, { owner: unit });
const ma = num(tb.atk, 0), md = num(tb.def, 0); // module UNY-Y: 周围8格没有友方干员时攻击力和防御力+8%
if (ma || md) whileDeployed(battle, unit, AURA, () => toggleBuff(battle, unit, 'mudrok:module', lonely(battle, unit, true), { atkPct: ma, defPct: md }));
},
};
},
});
@@ -0,0 +1,118 @@
// server/sim/content/kits/ops/chess_char_4_19-flamtl.js — 焰尾 (char_420_flamtl) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
import {
AURA, num, tbb, grid, nationOf, targetsInGrid, whileDeployed, pulse, toggleBuff, skillActive, isSel, alt,
withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 焰尾 (pioneer) S3 焰心 — 8 DP over the skill, faster, +ATK, block +1, 60 % dodge; talents 前锋剑术 / 红松骑士团团长
// S1 迅敏直觉 (+6 DP, dodges the next physical attack); S2 “红松林” (+11/12 DP; ≤ 6 enemies around: 2 × 180 %/210 %
// phys + 0.5 s stun, air units too — PRTS 备注 "※可对空"; allies around +40 %/45 % physical dodge for 10 s);
// module SOL-X (她们的未来): ATK/DEF +8 % while blocking. Her dodges (any source) feed talent 前锋剑术.
chess_char_4_19_a: (bb, chess, def) => {
const t1 = tbb(def, 1);
const tb = def.traitBb || {};
const total = Math.max(0, Math.floor(num(bb.value, 8))), per = num(bb.cost, 1);
const p = num(bb.prob, 0.6);
const S1 = isSel(def, 'skchr_flamtl_1'), S3 = isSel(def, 'skchr_flamtl_3');
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
// (自动触发: an AUTO skill takes no 技能策略 — an AUTO DP skill fires as soon as it is ready, like 伺夜 S1)
skchr_flamtl_1: () => ({
kind: 'instant',
trigger: 'SP_FULL',
onStart({ battle, unit }) {
battle.addDp(unit.ownerId, num(bb.cost, 6));
battle.addBuff(unit, { key: 'flamtl:evade', visible: true });
battle.fx('dp', { x: unit.x, y: unit.y, id: unit.id, n: num(bb.cost, 6) });
},
}),
skchr_flamtl_2: () => ({
kind: 'instant',
onStart({ battle, unit }) {
battle.addDp(unit.ownerId, num(bb.cost, 11));
const area = g || [[0, 0], [0, 1]];
const foes = targetsInGrid(battle, unit, area);
sortEnemyTargets(battle, unit, foes, null);
for (const e of foes.slice(0, Math.max(1, Math.floor(num(bb.max_target, 6))))) {
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.8), type: 'phys', isSkill: true, tags: ['skill', 'redPine'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun, 0.5), source: unit });
}
for (const a of battle.unitsInGrid(unit, area, { side: 'ally' })) {
if (a.kind === 'device' || !battle.allySelectable(a, unit)) continue;
battle.addBuff(a, { key: 'flamtl:redPine', duration: num(bb['flamtl_s_2.duration'], 10), mods: { dodgePhys: num(bb['flamtl_s_2.prob'], 0.4) }, visible: true, source: unit });
}
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id });
},
}),
}),
skill: {
kind: 'duration',
// the 60 % dodge is rolled on its own in the `hit` hook below (independent of other dodge sources such as her
// talent's 22 %: 1 − (1−p)(1−q), never an additive 80 % + 22 % = 100 % immunity)
mods: { atkPct: num(bb.atk), batPct: Math.max(-0.9, num(bb.base_attack_time, 1) - 1), blockCnt: num(bb.block_cnt, 1) },
onStart({ battle, unit, skill }) { unit.mem.flame = { acc: 0, given: 0, step: skill.duration / Math.max(1, total) }; battle.fx('flame', { x: unit.x, y: unit.y, id: unit.id }); },
onTick({ battle, unit, dt }) {
const F = unit.mem.flame;
if (!F) return;
F.acc += dt;
while (F.acc + 1e-9 >= F.step && F.given < total) { F.acc -= F.step; F.given++; battle.addDp(unit.ownerId, per); }
},
onEnd({ battle, unit, reason }) {
const F = unit.mem.flame;
unit.mem.flame = null;
if (F && reason === 'duration' && F.given < total) battle.addDp(unit.ownerId, per * (total - F.given));
},
},
install(battle, unit) {
if (S1) {
battle.on('hit', (c) => { // S1: "闪避下次物理攻击"
const d = c.dmg;
if (c.target !== unit || d.cancel || d.type !== 'phys' || !d.isAttack || !c.source || c.source.side !== 'enemy' || !unit.findBuff('flamtl:evade')) return;
battle.removeBuff(unit, 'flamtl:evade');
d.cancel = true;
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
battle.emit('dodge', { source: c.source, target: unit, dmg: d });
}, { owner: unit, priority: 20 });
}
const ma = num(tb.atk, 0), md = num(tb.def, 0); // module SOL-X: 阻挡敌人时攻击力和防御力各+8%
if (ma || md) whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'flamtl:module', unit.blocking.length > 0, { atkPct: ma, defPct: md }));
if (!S3) return;
battle.on('hit', (c) => { // S3: "获得60%的物理和法术闪避"
const d = c.dmg;
if (c.target !== unit || !skillActive(unit) || d.cancel || !d.canDodge || (d.type !== 'phys' && d.type !== 'arts')) return;
if (!battle.rng.chance(p)) return;
d.cancel = true;
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
battle.emit('dodge', { source: c.source, target: unit, dmg: d });
}, { owner: unit, priority: 20 });
},
talents: [
{ install(battle, unit) { // 前锋剑术: after a dodge the next attack hits twice and every blocked enemy
battle.on('dodge', (c) => { if (c.target === unit) unit.mem.riposte = true; }, { owner: unit });
battle.on('death', (c) => { if (c.unit === unit) unit.mem.riposte = unit.mem.riposteNow = false; }, { owner: unit });
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !unit.mem.riposte) return;
unit.mem.riposte = false;
unit.mem.riposteNow = true;
const extra = unit.blocking.filter((e) => e.alive && canTargetEnemy(unit, e, c.profile) && !c.targets.includes(e));
c.targets = [...c.targets, ...extra];
}, { owner: unit });
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.mem.riposteNow) return;
unit.mem.riposteNow = false;
for (const e of c.targets) if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * unit.s.atkScaleMul, type: 'phys', isAttack: true, tags: ['riposte'] });
battle.fx('riposte', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
} },
{ install(battle, unit) { // 红松骑士团团长: 卡西米尔 operators +22 % physical dodge
whileDeployed(battle, unit, AURA, () => { for (const a of battle.allies(unit.ownerId)) if (nationOf(a) === 'kazimierz') pulse(battle, a, 'flamtl:dodge', { dodgePhys: num(t1.prob, 0.22) }); });
} },
],
};
},
});
@@ -0,0 +1,91 @@
// server/sim/content/kits/ops/chess_char_4_20-fartth.js — 远牙 (char_430_fartth) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { bodyInKeys } from '../../../body.js';
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
import { isHpLoss } from '../../../damage.js';
import { num, tbb, whileDeployed, toggleBuff, skillActive, isSel, alt, withDefaults } from '../shared/tier4.js';
const LINE = Object.freeze(Array.from({ length: COLS }, (_, i) => Object.freeze([0, i])));
const tileKey = (u) => Math.round(u.y) * COLS + Math.round(u.x);
/** HP at 0 (boss pool: pool HP) — a `damaged` hook sees the lethal hit before the kill, while the unit is still `alive`. */
const downed = (u) => (u.bossPool ? !(u.bossPool.hp > 0) : !(u.hp > 0));
export default withDefaults({
// ===== 远牙 (longrange) S3 光羽箭 — infinite line, +ATK, ×1.25 beyond the normal range; talents 凝神 / 屏息; module
// S1 迅捷打击·γ型; S2 同盟支援 (ASPD up; enemies blocked by any ally anywhere on the field are in range — their
// tiles are extra range keys while the skill runs). Talent 屏息 (taunt −1, ignores dodge) covers every skill.
// Module DEA-Y (支持者来信): +1 SP when an attacked enemy survives the hit
chess_char_4_20_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const S3 = isSel(def, 'skchr_fartth_3');
const taunt = num(t1.taunt_level, -1);
const setAllied = (battle, unit, keys) => {
const sig = keys ? keys.join(',') : '';
if (sig === (unit.mem.alliedSig ?? '')) return;
unit.mem.alliedSig = sig;
battle.setExtraRange(unit, keys);
};
return {
skills: alt(def, {
'skcom_quickattack[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed), taunt } }),
skchr_fartth_2: () => ({
kind: 'duration',
mods: { aspd: num(bb.attack_speed), taunt },
onStart({ battle, unit }) { battle.fx('link', { x: unit.x, y: unit.y, id: unit.id }); },
onTick({ battle, unit }) {
const keys = [];
for (const e of battle.enemies) if (e.alive && !e.hidden && e.blockedBy && e.blockedBy.side === 'ally' && e.blockedBy.alive) keys.push(tileKey(e));
keys.sort((a, b) => a - b);
setAllied(battle, unit, keys.length ? keys : null);
},
onEnd({ battle, unit }) { setAllied(battle, unit, null); },
}),
}),
skill: { kind: 'duration', mods: { atkPct: num(bb.atk), taunt: num(t1.taunt_level, -1) }, targeting: { rangeGrid: LINE },
onStart({ battle, unit }) { battle.fx('featherArrow', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [
{ install(battle, unit) { // 凝神: ATK +15 % when not hurt for 10 s
battle.on('damaged', (c) => { if (c.target === unit && c.amount > 0 && !isHpLoss(c.dmg)) unit.mem.lastHurt = battle.time; }, { owner: unit }); // (a 流失 is not 受伤害)
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.lastHurt = -Infinity; }, { owner: unit });
whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'fartth:focus', battle.time - (unit.mem.lastHurt ?? -Infinity) >= num(t0.delay, 10) - 1e-9, { atkPct: num(t0.atk, 0.15) }));
} },
{ install(battle, unit) { // 屏息 (2nd half): skill attacks ignore physical dodge
battle.on('hit', (c) => { if (c.source === unit && skillActive(unit) && c.dmg.type === 'phys') c.dmg.canDodge = false; }, { owner: unit });
} },
],
install(battle, unit) {
if (isSel(def, 'skchr_fartth_2')) {
// 同盟支援 reaches BLOCKED enemies outside her range only: the blocked enemies' tiles are extra range keys, so an
// unblocked enemy (a flyer…) standing on such a tile outside her own range is dropped for the next valid target
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillActive(unit) || !unit.extraRangeKeys) return;
const own = new Set(absoluteRangeKeys(unit.rangeGrid, unit.tileR, unit.tileC, unit.dir, unit.s.rangeExtend));
const ok = (e) => bodyInKeys(e, own) || !!(e.blockedBy && e.blockedBy.side === 'ally' && e.blockedBy.alive);
if (c.targets.every(ok)) return;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, c.profile);
for (const e of battle.blockedTargets(unit, c.profile)) if (!cands.includes(e)) cands.push(e); // DESIGN §20.3
for (let i = cands.length - 1; i >= 0; i--) if (!ok(cands[i])) cands.splice(i, 1);
sortEnemyTargets(battle, unit, cands, c.profile?.priority ?? null);
c.targets = cands.slice(0, Math.max(1, c.targets.length));
}, { owner: unit, priority: 5 });
}
const sp = num(tb.sp, 0); // module DEA-Y: 攻击的敌人未被击倒时自身额外获得1点技力
if (sp > 0) {
// (`damaged` fires before the kill: a lethal hit leaves the target `alive` with 0 HP — that one gives no SP)
battle.on('damaged', (c) => {
if (c.source === unit && c.dmg?.isAttack && !c.dmg.isSplash && c.target.side === 'enemy' && c.target.alive && !downed(c.target)) unit.skill?.gainSp(sp, 'module');
}, { owner: unit });
}
const ds = num(tb.damage_scale, 0), lo = num(tb.min_dist, 1), hi = num(tb.max_dist, 4.5);
battle.on('hit', (c) => {
if (c.source !== unit || !c.dmg.isAttack) return;
if (S3 && skillActive(unit) && unit.baseRangeKeys && !bodyInKeys(c.target, unit.baseRangeKeys)) c.dmg.mul *= num(bb.damage_scale, 1.25);
if (ds > 0) { const d = Math.hypot(c.target.x - unit.x, c.target.y - unit.y); c.dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo))); }
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,19 @@
// server/sim/content/kits/ops/chess_char_4_21-plosis.js — 白面鸮 (char_128_plosis) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { num, tbb, grid, batFlat, spAura, alt, applyModuleRange, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 白面鸮 (ringhealer) S2 脑啡肽 — wider range, much faster heals; talent 技力光环 (+0.3 SP/s to all allies)
chess_char_4_21_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, { 'skcom_heal_up[3]': () => ({ kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } }) }), // S1 治疗强化·γ型
skill: { kind: 'duration', heal: true, mods: { batPct: batFlat(def, bb.base_attack_time) }, targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { battle.fx('healField', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [{ install(battle, unit) { spAura(battle, unit, num(t0.sp_recovery_per_sec, 0.3), (a) => a.kind !== 'device'); } }],
install(battle, unit) { applyModuleRange(battle, unit, def); }, // module RIN-X (elite default): 攻击范围扩大
};
},
});
@@ -0,0 +1,52 @@
// server/sim/content/kits/ops/chess_char_4_22-svrash.js — 银灰 (char_172_svrash) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, grid, enemiesOnRange, whileDeployed, reveal, alt, instantKind, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 银灰 (lord) S3 真银斩 — DEF −70 %, ATK +125 %, wider range, ≤ 4 targets at melee scale; 领袖 / 鹰眼视觉; module
// S1 强力击·γ型 (attack SP: next attack 205 %/225 %); S2 雪境生存法则 (toggle — [ASSUMED] once switched on it stays
// on for the deployment: smaller range, DEF up, 3.5 %/4 % max HP regen per s)
chess_char_4_22_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
skchr_svrash_1: () => ({ kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 2.05) } }),
skchr_svrash_2: () => ({
kind: 'toggle',
mods: { defPct: num(bb.def), hpRegenRatio: num(bb.hp_recovery_per_sec_by_max_hp_ratio) },
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { battle.fx('snow', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
mods: { defPct: num(bb.def), atkPct: num(bb.atk) },
targeting: { ...(g ? { rangeGrid: g } : {}), maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 4))) },
attack: { projectile: 'none', dmgMul: () => 1 }, // 视为近距离攻击: no ranged ×0.8
onStart({ battle, unit }) { battle.fx('truesilver', { x: unit.x, y: unit.y, id: unit.id }); },
onAttack({ battle, unit }) { battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id }); },
},
talents: [
{ install(battle, unit) { // 领袖: ATK +10 %; every unit of the team redeploys 10 % faster
battle.addBuff(unit, { key: 'svrash:t1', mods: { atkPct: num(t0.atk, 0.1) }, persist: true, allowDead: true });
const p = battle.getPlayer(unit.ownerId);
for (const a of p ? p.units : []) if (a.kind === 'op') battle.addBuff(a, { key: 'svrash:leader', mods: { redeployMul: Math.max(0, 1 + num(t0.respawn_time, -0.1)) }, persist: true, allowDead: true });
} },
{ install(battle, unit) { whileDeployed(battle, unit, AURA, () => reveal(battle, enemiesOnRange(battle, unit))); } }, // 鹰眼视觉
],
install(battle, unit) {
const m = num(tb.atk_scale_m, 0); // module (elite): attacks add 10 % ATK arts damage
if (!(m > 0)) return;
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg?.isAttack || c.type !== 'phys' || !c.target.alive || c.target.side !== 'enemy') return;
battle.dealDamage(unit, c.target, { amount: unit.s.atk * m, type: 'arts', tags: ['module'] });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,78 @@
// server/sim/content/kits/ops/chess_char_4_23-gvial2.js — 百炼嘉维尔 (char_1026_gvial2) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
num, tbb, grid, whileDeployed, toggleBuff, skillActive, isSel, alt, pullToFront, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 百炼嘉维尔 (centurion) S3 丛林之魂 — ATK/ASPD/block up, takes 50 % now, the rest as 20 s HP loss; talents; module
// S1 精准痛击 (ATK up, heals herself 30 %/35 % of the damage dealt); S2 链锯强袭 (wider range, ATK/DEF up, pulls
// unblocked enemies it hits to her front); module CEN-Y (好锯多磨): −20 % physical damage taken above 50 % HP
chess_char_4_23_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const dr = Math.max(0, Math.min(0.99, num(bb.damage_resistance, 0.5)));
const fin = num(bb.final_duration, 20), iv = Math.max(1 / 30, num(bb.interval, 0.1));
const S3 = isSel(def, 'skchr_gvial2_3');
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
skchr_gvial2_1: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onHit({ battle, unit, dealt }) { if (dealt > 0 && unit.alive) battle.heal(unit, unit, dealt * num(bb.heal_scale, 0.3), { self: true }); },
}),
skchr_gvial2_2: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
targeting: g ? { rangeGrid: g } : undefined,
attack: { onEachHit({ battle, unit, target }) { if (target && target.alive && !target.blockedBy) pullToFront(battle, unit, target, num(bb['attack@force'], 1)); } },
onStart({ battle, unit }) { battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed), blockCnt: num(bb.block_cnt, 2) },
onStart({ battle, unit }) { unit.mem.gvialDebt = 0; battle.fx('jungleSoul', { x: unit.x, y: unit.y, id: unit.id }); },
onEnd({ battle, unit, reason }) {
const debt = unit.mem.gvialDebt ?? 0;
unit.mem.gvialDebt = 0;
if (reason === 'death' || !unit.alive || !(debt > 0)) return;
const per = debt * iv / fin;
battle.addBuff(unit, { key: 'gvial:bleed', refresh: 'independent', maxStacks: 10, duration: fin + 1e-6, interval: iv, visible: true,
onTick: ({ unit: u }) => battle.loseHp(u, per) });
},
},
talents: [
{ install(battle, unit) { // 战地巨斧: ATK/DEF +10 %, +4 % per extra blocked enemy
whileDeployed(battle, unit, 0.1, () => {
const extra = Math.max(0, unit.blocking.length - 1);
toggleBuff(battle, unit, 'gvial:t1', true, { atkPct: num(t0.atk, 0.1) + num(t0.atk_add, 0.04) * extra, defPct: num(t0.def, 0.1) + num(t0.def_add, 0.04) * extra });
});
} },
{ install(battle, unit) { // 医学背景: healing received +20 %, +40 % below half HP
battle.on('heal', (c) => { if (c.target === unit) c.amount *= unit.hpRatio < num(t1.hp_ratio, 0.5) ? num(t1.heal_scale_2, 1.4) : num(t1.heal_scale_1, 1.2); }, { owner: unit });
} },
],
install(battle, unit) {
const s = num(tb.atk_scale, 0); // module (elite): ×1.1 against blocked enemies
if (s > 0) battle.on('hit', (c) => { if (c.source === unit && c.dmg.isAttack && c.target.blockedBy === unit) c.dmg.mul *= s; }, { owner: unit });
const yr = num(tb.hp_ratio, 0), yd = num(tb.damage_resistance, 0); // module CEN-Y: 生命值高于50%时受到的物理伤害降低20%
if (yr > 0 && yd > 0) battle.on('hit', (c) => { if (c.target === unit && c.dmg.type === 'phys' && unit.hpRatio > yr) c.dmg.mul *= 1 - yd; }, { owner: unit });
if (!S3) return;
const deferred = new WeakSet();
battle.on('hit', (c) => {
if (c.target !== unit || !skillActive(unit)) return;
c.dmg.mul *= 1 - dr;
deferred.add(c.dmg);
}, { owner: unit, priority: -5 });
battle.on('damaged', (c) => {
if (c.target !== unit || !deferred.has(c.dmg)) return;
deferred.delete(c.dmg);
if (c.amount > 0) unit.mem.gvialDebt = (unit.mem.gvialDebt ?? 0) + (c.amount * dr) / (1 - dr);
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,148 @@
// server/sim/content/kits/ops/chess_char_4_24-billro.js — 卡涅利安 (char_426_billro) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
num, tbb, moduleBb, grid, batFlat, enemiesOnRange, whileDeployed, toggleBuff, skillActive, isSel, alt, withDefaults,
} from '../shared/tier4.js';
/** 卡涅利安 S3 食噬之印: the ATK bonus climbs in 1 s steps over 20 s (PRTS 备注; the blackboard has no ramp time). */
const BILLRO_S3_RAMP = 20;
/** 卡涅利安 S3 charged mark: one buff per enemy, shared by every 卡涅利安 (PRTS 备注), ≤ 5 stacks (skill text). */
const BILLRO_MARK = 'billro:mark';
const BILLRO_MARK_MAX = 5;
export default withDefaults({
// ===== 卡涅利安 (phalanx) S2 沙缚镣锁 — faster attacks on every enemy in range (the trait's 群体法术伤害), 0.3 s
// sluggish on each; charged: ATK +10 % and bind; talents
// S1 沙暴守卫 (SEARCH: ATK/DEF up; charged — cast with every charge stored — "特性效果在技能期间继续生效", PRTS 备注
// "应用蓄力时:应用技能未开启时的特性": the skill-off trait stays in force, the DEF/RES guard AND 不攻击 — she makes
// no attack until the skill ends, a pure guard);
// S3 食噬之印 (wider range, ATK +0 % → +140 %/+200 % in 1 s steps over 20 s — PRTS 备注; charged: each attack adds a
// stack of the mark BEFORE its damage — PRTS 备注 "于攻击造成伤害前生效,多层效果之间加算叠加" — +20 % damage from
// her per stack, ≤ 5, so ×1.2 on the first hit and ×2.0 from the 5th, until the skill ends; one mark per enemy:
// another 卡涅利安 only adds stacks to it, the bonus is the setter's — PRTS 备注 popup). Talent 生命之餐 heals on
// every skill; module PLX-X keeps part of the guard during any skill, PLX-Y (乡音): +3 % damage per enemy in range
// (≤ 5)
chess_char_4_24_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const tb = def.traitBb || {};
const keepDef = num(tb['billro_e_002[buff].def'], 0), keepRes = num(tb['billro_e_002[buff].magic_resistance'], 0);
const S1 = isSel(def, 'skchr_billro_1'), S2 = isSel(def, 'skchr_billro_2'), S3 = isSel(def, 'skchr_billro_3');
const g = grid(def.skill?.rangeGrid);
const isCharged = (skill) => skill.maxCharges > 1 && skill.charges + 1 >= skill.maxCharges; // cast with every charge stored
return {
// charged S1: the skill-off trait's 不攻击 stays in force while the skill lasts (PRTS S1 备注) — no attack at all
trait: S1 ? { canAttack: (battle, u) => !(skillActive(u) && u.mem.billroCharged) } : null,
skills: alt(def, {
skchr_billro_1: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
onStart({ battle, unit, skill }) {
unit.mem.billroCharged = isCharged(skill);
// 蓄力额外效果:特性效果在技能期间继续生效 (the profession removes its own guard buff when a skill starts; the
// kit trait's canAttack keeps the trait's 不攻击)
if (unit.mem.billroCharged) battle.addBuff(unit, { key: 'billro:s1guard', mods: { defPct: num(unit.profile?.guardDef, 2), resFlat: num(unit.profile?.guardRes, 20) } });
battle.fx('shell', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { unit.mem.billroCharged = false; battle.removeBuff(unit, 'billro:s1guard'); },
}),
skchr_billro_3: () => ({
kind: 'duration',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit, skill }) {
unit.mem.billroCharged = isCharged(skill);
unit.mem.billroRamp = 0;
unit.mem.billroMarked = new Set();
battle.addBuff(unit, { key: 'billro:s3atk', mods: { atkPct: 0 } });
battle.fx('devour', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ unit, dt }) { // 攻击力逐渐增至+N%: PRTS 备注 "从+0%开始在20秒内线性增加,攻击力每1秒更新1次"
unit.mem.billroRamp = (unit.mem.billroRamp ?? 0) + dt;
const steps = Math.min(BILLRO_S3_RAMP, Math.floor(unit.mem.billroRamp + 1e-9));
const b = unit.findBuff('billro:s3atk');
if (b && b.mods.atkPct !== num(bb.atk) * steps / BILLRO_S3_RAMP) { b.mods = { atkPct: num(bb.atk) * steps / BILLRO_S3_RAMP }; unit.markDirty(); }
},
onEnd({ battle, unit }) {
battle.removeBuff(unit, 'billro:s3atk');
// "持续至技能结束": the marks she set end with her skill (stacks another 卡涅利安 added to them included)
for (const id of unit.mem.billroMarked || []) {
const e = battle.unitById(id);
if (e && e.findBuff(BILLRO_MARK)?.source === unit) battle.removeBuff(e, BILLRO_MARK);
}
unit.mem.billroMarked = null;
unit.mem.billroCharged = false;
},
}),
}),
skill: {
kind: 'duration',
mods: { batPct: batFlat(def, bb.base_attack_time) },
onStart({ battle, unit, skill }) {
const charged = isCharged(skill);
unit.mem.billroCharged = charged;
if (charged) battle.addBuff(unit, { key: 'billro:charged', mods: { atkPct: num(bb.atk, 0.1) } });
battle.fx(charged ? 'sandChainsCharged' : 'sandChains', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { unit.mem.billroCharged = false; battle.removeBuff(unit, 'billro:charged'); },
},
talents: [
{ install(battle, unit) { // 生命之餐: every skill start heals 40 % max HP (×2 when charged)
battle.on('skillStart', (c) => {
if (c.unit !== unit) return;
const charged = isCharged(c.skill);
battle.heal(unit, unit, unit.s.maxHp * (charged ? num(t0['billro_t_1[enhance].heal_scale'], 0.8) : num(t0.heal_scale, 0.4)), { self: true });
}, { owner: unit });
} },
{ install(battle, unit) { // 蓄势待发: +0.6 SP/s once a charge is stored
whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'billro:t2', !!unit.skill && unit.skill.charges >= 1 && !skillActive(unit), { spRecoveryFlat: num(t1.sp_recovery_per_sec, 0.6) }));
} },
],
install(battle, unit) {
// module PLX-X (elite default): 技能开启时保留部分效果 (any skill; a charged S1 keeps the whole guard instead)
if (keepDef || keepRes) {
battle.on('skillStart', (c) => {
if (c.unit !== unit || (S1 && unit.mem.billroCharged)) return;
battle.addBuff(unit, { key: 'billro:keep', mods: { defPct: keepDef, resFlat: keepRes } });
}, { owner: unit });
battle.on('skillEnd', (c) => { if (c.unit === unit) battle.removeBuff(unit, 'billro:keep'); }, { owner: unit });
}
// module PLX-Y: 范围内敌人越多造成的伤害越高(每个 +3 %,最多 5 个)
const per = num(mb.damage_scale, 0), cap = num(mb.max_valid_stack_cnt, 5);
if (per > 0) {
battle.on('hit', (c) => {
if (c.source !== unit || c.target.side !== 'enemy') return;
const n = Math.min(cap, enemiesOnRange(battle, unit).length);
if (n > 0) c.dmg.mul *= 1 + per * n;
}, { owner: unit });
}
if (S3) {
// charged S3 食噬之印: each attack adds a stack BEFORE its damage (PRTS 备注 "于攻击造成伤害前生效"); stacks add up,
// +20 % damage taken from the mark's setter per stack. One mark per enemy (PRTS 备注 popup: "存在则为已有的BUFF叠加
// 一次加成,不存在则给与敌人仅对此卡涅利安的伤害生效的一个食噬之印BUFF"): another 卡涅利安 stacks it without the bonus
const per = num(bb['attack@damage_scale'], 0.2);
battle.on('hit', (c) => {
const e = c.target;
if (c.source !== unit || e.side !== 'enemy') return;
let m = e.findBuff?.(BILLRO_MARK) || null;
if (c.dmg.isAttack && skillActive(unit) && unit.mem.billroCharged) {
if (m) battle.addBuff(e, { key: BILLRO_MARK, refresh: 'stack', maxStacks: BILLRO_MARK_MAX, duration: m.timeLeft });
else {
m = battle.addBuff(e, { key: BILLRO_MARK, refresh: 'stack', maxStacks: BILLRO_MARK_MAX, duration: Math.max(0.1, (unit.skill?.timeLeft ?? 0) + 0.1), source: unit, visible: true });
if (m) unit.mem.billroMarked?.add(e.id);
}
}
if (m && m.source === unit) c.dmg.mul *= 1 + per * (m.stacks || 1);
}, { owner: unit });
}
if (!S2) return;
// "每次攻击对目标造成0.3秒停顿" (charged: 束缚): every enemy her attack damages — each one on her range
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg || !c.dmg.isAttack || e.side !== 'enemy' || !e.alive || !skillActive(unit)) return;
if (unit.mem.billroCharged) battle.applyStatus(e, 'bind', { duration: num(bb['attack@root'], 0.3), source: unit });
else battle.applyStatus(e, 'sluggish', { duration: num(bb['attack@sluggish'], 0.3), source: unit });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,150 @@
// server/sim/content/kits/ops/chess_char_4_25-cetsyr.js — 魔王 (char_4134_cetsyr) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { canTargetEnemy } from '../../../targeting.js';
import { aggregateMods } from '../../../buffs.js';
import {
AURA, AURA_DUR, num, tbb, moduleBb, grid, enemyHasTag, whileDeployed, toggleBuff, skillActive, isSel, alt,
withDefaults,
} from '../shared/tier4.js';
/**
* 鼓舞 (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 浊心斯卡蒂'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') {
if (!(val > 0) || !target || !target.alive || target.mem?.noInspire) return;
const key = stat === 'hp' ? 'inspire:hp' : 'inspire';
const cur = target.findBuff(key);
if (cur && cur.data && cur.data.src !== src.id && cur.data.val > val && cur.timeLeft > 1e-6) return;
const { add, mul } = aggregateMods(target.buffs.filter((b) => b.key !== key));
const f = stat === 'hp' ? Math.max(0, 1 + (add.hpPct ?? 0)) * (mul.hpMul ?? 1) : Math.max(0, 1 + (add.atkPct ?? 0)) * (mul.atkMul ?? 1);
const flat = f > 1e-6 ? val / f : val;
battle.addBuff(target, { key, mods: stat === 'hp' ? { hpFlat: flat } : { atkFlat: flat }, duration: AURA_DUR, source: src, visible: true, data: { src: src.id, val } });
}
export default withDefaults({
// ===== 魔王 (bard) S3 编织重构现世 — wider range, trait 65 %, motes stay, 鼓舞 +65 % of her max HP, HP redistribution
// S1 往昔萦绕身旁 (SP_FULL toggle, 持续时间无限: trait 20 %/28 %, motes respawn in `talent_cool_down` s); S2 明日渺远不及
// (motes cap +3 and 6 at once, wider orbit — enemies within outside_radius — no longer touch operators: a mote
// hitting an enemy deals 220 %/260 % ATK true damage + 3 s bind and respawns like any mote [ASSUMED]; 鼓舞: other
// allies in range ATK + 65 %/80 % of her ATK)
chess_char_4_25_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const g = grid(def.skill?.rangeGrid);
const redistIv = Math.max(0.5, num(bb['attack@cetsyr_s_3[cal_hp_ratio].interval'], 2));
const S1 = isSel(def, 'skchr_cetsyr_1'), S2 = isSel(def, 'skchr_cetsyr_2'), S3 = isSel(def, 'skchr_cetsyr_3');
const baseCnt = Math.max(0, Math.floor(num(t0.cnt, 3)));
const MOTE_GAP = 0.5; // [ASSUMED] minimal time between two motes hitting the same enemy (s)
const setTrait = (unit, v) => { unit.mem.bardBase ??= unit.profile.auraRatio; unit.profile.auraRatio = num(v, unit.mem.bardBase); };
const resetTrait = (unit) => { if (unit.mem.bardBase != null) unit.profile.auraRatio = unit.mem.bardBase; };
return {
skills: alt(def, {
skchr_cetsyr_1: () => ({
kind: 'toggle',
trigger: 'SP_FULL', // 自动触发 (no 技能策略: the 吟游者 row is for MANUAL skills): on as soon as it is ready
onStart({ battle, unit }) { setTrait(unit, bb['attack@atk_to_hp_recovery_ratio']); battle.fx('mote', { x: unit.x, y: unit.y, id: unit.id }); },
onEnd({ unit }) { resetTrait(unit); },
}),
skchr_cetsyr_2: () => ({
kind: 'duration',
onStart({ battle, unit }) {
// “微尘”上限+3,立刻获得6枚“微尘”
unit.mem.motes = new Array(baseCnt + 3).fill(-Infinity);
unit.mem.moteHit = new Map();
battle.fx('reweave', { x: unit.x, y: unit.y, id: unit.id, n: unit.mem.motes.length });
},
onTick({ battle, unit }) { // 鼓舞: other allies in range ATK + x % of her ATK
for (const a of battle.alliesInGrid(unit)) if (a !== unit) inspire(battle, a, unit.s.atk * num(bb['attack@atk'], 0.65), unit);
},
onEnd({ battle, unit }) {
// back to the normal cap: the motes that come back first are kept
unit.mem.motes = (unit.mem.motes || []).slice().sort((a, b) => a - b).slice(0, baseCnt);
unit.mem.moteHit = null;
},
}),
}),
skill: {
kind: 'duration',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) {
setTrait(unit, bb['attack@atk_to_hp_recovery_ratio']);
unit.mem.redist = 0;
battle.fx('reweave', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
const others = battle.alliesInGrid(unit).filter((a) => a !== unit);
for (const a of others) inspire(battle, a, unit.s.maxHp * num(bb.max_hp, 0.65), unit, 'hp');
unit.mem.redist += dt;
if (unit.mem.redist + 1e-9 < redistIv) return;
unit.mem.redist -= redistIv;
const pool = battle.alliesInGrid(unit).filter((a) => !(a.s.flags.noHeal || a.profile?.noHeal) || a === unit);
let hp = 0, max = 0;
for (const a of pool) { hp += a.hp; max += a.s.maxHp; }
if (!(max > 0) || pool.length < 2) return;
const ratio = Math.min(1, hp / max);
for (const a of pool) a.hp = Math.max(1, Math.min(a.s.maxHp, a.s.maxHp * ratio));
battle.fx('redistribute', { x: unit.x, y: unit.y, id: unit.id, ratio: Math.round(ratio * 1000) / 1000 });
},
onEnd({ unit }) { if (unit.mem.bardBase != null) unit.profile.auraRatio = unit.mem.bardBase; },
},
talents: [
{ install(battle, unit) { // 过往尘埃: 3 motes; touching an operator ⇒ her trait heal ×1.5 on it for 6 s; mote back after 6 s
unit.mem.motes = new Array(baseCnt).fill(-Infinity);
unit.mem.noInspire = true; // trait: 自身不受鼓舞影响 (another bard's 鼓舞 skips her)
const cooldown = () => (S1 && skillActive(unit) ? num(bb.talent_cool_down, 3) : num(t0.cooldown, 6)); // S1: 重生速度加快
const takeMote = () => {
const i = unit.mem.motes.findIndex((t) => t <= battle.time + 1e-9);
if (i < 0) return false;
unit.mem.motes[i] = battle.time + cooldown();
return true;
};
whileDeployed(battle, unit, 0.25, () => {
if (S2 && skillActive(unit)) {
// S2: motes orbit wider and hit enemies instead of operators
const hitAt = unit.mem.moteHit || (unit.mem.moteHit = new Map());
const foes = battle.foesInRadius(unit.x, unit.y, num(bb.outside_radius, 2)).filter((e) => canTargetEnemy(unit, e, { canHitFly: true }))
.sort((a, b) => a.id - b.id);
for (const e of foes) {
if ((hitAt.get(e.id) ?? -Infinity) > battle.time + 1e-9) continue;
if (!takeMote()) break;
hitAt.set(e.id, battle.time + MOTE_GAP);
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 2.2), type: 'true', isSkill: true, tags: ['skill', 'mote'] });
if (e.alive) battle.applyStatus(e, 'bind', { duration: num(bb.unmoveable_duration, 3), source: unit });
battle.fx('mote', { x: e.x, y: e.y, id: e.id });
}
return;
}
const keep = S3 && skillActive(unit); // S3: “微尘”不再消失
for (const a of battle.alliesInRadius(unit.x, unit.y, num(t0.range_radius, 1.15), unit.ownerId)) {
if (a === unit || a.kind !== 'op' || a.findBuff(`cetsyr:mote:${unit.id}`)) continue;
if (!keep && !takeMote()) break;
battle.addBuff(a, { key: `cetsyr:mote:${unit.id}`, duration: num(t0.talent_duration, 6), visible: true, source: unit });
battle.fx('mote', { x: a.x, y: a.y, id: a.id });
}
});
battle.on('heal', (c) => {
if (c.source === unit && c.opts?.aura && c.target.findBuff(`cetsyr:mote:${unit.id}`)) c.amount *= num(t0['attack@trait_mul'], 1.5);
}, { owner: unit });
} },
{ install(battle, unit) { // 魔王残响: every ally takes −10 % damage from 萨卡兹 enemies (while she is in the squad)
battle.on('hit', (c) => {
if (c.target.side === 'ally' && c.target.ownerId === unit.ownerId && enemyHasTag(c.source, 'sarkaz')) c.dmg.mul *= 1 - num(t1.damage_resistance, 0.1);
}, { owner: unit });
} },
],
install(battle, unit) {
const a = num(mb.atk, 0), cnt = num(mb.cnt, 2); // module (elite): ≥ 2 other operators in her normal range ⇒ ATK +8 %
if (!a) return;
whileDeployed(battle, unit, AURA, () => {
const set = new Set(unit.baseRangeKeys || []);
const n = battle.allies(unit.ownerId).filter((o) => o !== unit && o.kind === 'op' && set.has(o.tileR * COLS + o.tileC)).length;
toggleBuff(battle, unit, 'cetsyr:module', n >= cnt, { atkPct: a });
});
},
};
},
});
@@ -0,0 +1,66 @@
// server/sim/content/kits/ops/chess_char_4_26-bldsk.js — 华法琳 (char_171_bldsk) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyInKeys } from '../../../body.js';
import { num, tbb, keySet, isSel, alt, installLowHpHealBonus, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 华法琳 (physician) S1 紧急包扎 — charges: a heal on an ally below 50 % adds 15 % of its max HP; 血液样本回收
// S2 不稳定血浆 (she and one random other ally in range: ATK +45 %/+60 %, lose 3 % max HP per s, 15 s)
chess_char_4_26_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const S1 = isSel(def, 'skchr_bldsk_1');
const plasma = (battle, unit, a) => battle.addBuff(a, {
key: 'bldsk:plasma', duration: num(bb.duration, 15) + 1e-6, interval: Math.max(0.1, num(bb.interval, 1)), mods: { atkPct: num(bb.atk) }, visible: true, source: unit,
onTick: ({ unit: t }) => battle.loseHp(t, t.s.maxHp * num(bb.hp_ratio, 0.03), { tags: ['skill', 'plasma'] }), // (no source: not damage dealt)
});
return {
skills: alt(def, {
skchr_bldsk_2: () => ({
kind: 'instant', heal: true,
onStart({ battle, unit }) {
plasma(battle, unit, unit);
const pick = battle.rng.pick(battle.alliesInGrid(unit).filter((a) => a !== unit));
if (pick) plasma(battle, unit, pick);
battle.fx('bloodBattle', { x: unit.x, y: unit.y, id: unit.id, target: pick ? pick.id : null });
},
}),
}),
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
heal: true,
trigger: { rule: 'CUSTOM_RANGE', grid: [] }, // never auto-cast by the engine: fired by the kit on a < 50 % heal target
},
talents: [{ install(battle, unit) { // 血液样本回收: an enemy falls in range ⇒ +2 SP to her and a random ally in range
battle.on('death', (c) => {
const e = c.unit;
if (c.reason !== 'killed' || e.side !== 'enemy' || !unit.alive || !unit.deployed || !bodyInKeys(e, keySet(unit))) return;
unit.skill?.gainSp(num(t0['bldsk_t_1[self].sp'], 2), 'talent');
// SP cannot be gained while a timed skill runs (engine gainSp ignores it): such allies are not picked
const pick = battle.rng.pick(battle.alliesInGrid(unit).filter((a) => a !== unit && a.skill && !a.skill.noSkill && a.skill.kind !== 'passive' && !(a.skill.active && a.skill.isTimed)));
if (pick) { pick.skill.gainSp(num(t0['bldsk_t_1[rand].sp'], 2), 'talent'); battle.fx('spGift', { x: pick.x, y: pick.y, id: pick.id }); }
}, { owner: unit });
} }],
install(battle, unit) {
installLowHpHealBonus(battle, unit, tb);
if (!S1) return;
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit) return;
const t = c.targets[0];
const sk = unit.skill;
if (!t || t.side !== 'ally' || t.hpRatio >= 0.5 || !sk || !sk.ready || unit.s.flags.silence) return;
if (sk.activate('DEFAULT')) unit.mem.bandage = t;
}, { owner: unit, priority: 5 });
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.mem.bandage) return;
const t = unit.mem.bandage;
unit.mem.bandage = null;
unit.mem.skipSp = true; // the skill heal recovers no attack SP (AK)
if (t.alive) { battle.heal(unit, t, t.s.maxHp * num(bb.hp_ratio, 0.15)); battle.fx('bandage', { x: t.x, y: t.y, id: t.id }); }
}, { owner: unit });
battle.on('spGain', (c) => { if (c.unit === unit && c.reason === 'attack' && unit.mem.skipSp) { unit.mem.skipSp = false; c.amount = 0; } }, { owner: unit });
},
};
},
});
@@ -0,0 +1,124 @@
// server/sim/content/kits/ops/chess_char_5_01-excu2.js — 圣约送葬人 (char_1032_excu2) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import {
num, on, talent, batPct, mods, inFaction, isOp, selectedId, lazySkills, skillRange, crowdAspd,
} from '../shared/tier5.js';
const isLaterano = (u) => inFaction(u, 'lateranoShip', ['laterano']);
/** Enemy `e` attacks `t` in melee (not a ranged shot), mirroring ai.js enemyAttack. */
function meleeAttacker(e, t) {
const melee = e.profile?.melee ?? e.def?.applyWay === 'MELEE';
const r = num(e.base?.rangeRadius, 0);
return melee || !(r > 0) || (e.blockedBy === t && r < 1);
}
export default {
// ---------------------------------------------------------------------------------------------------------------
// 圣约送葬人 — S2 近身铳斗 (ammo, attack SP): ATK/DEF +, block +1, melee hits dodged with prob (+ammo refill).
// T1 受选之人: extra attack chance (+prob_add per ammo spent in the skill). T2 铳弹共感: +ammo per Laterano op.
// S1 遗嘱执行 (ammo 8, attack SP): skill range, ATK +, ignores def_penetrate_fixed DEF.
// S3 圣约决裁 (ammo 16): skill range, BAT +0.5 s, ATK +; +attack@atk ATK per bullet spent (≤ attack@max_stack_cnt),
// reaper self-heal ×trait_ratio; at the end every enemy attacked during the skill takes attack@final_atk_scale × ATK phys.
// Module REA-Y (elite): ASPD +12 with ≥ 2 enemies in range.
chess_char_5_01_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tm = talent(chess, -1);
const sid = selectedId(chess, def);
const prob = num(bb.prob), refill = num(bb.recover_cnt, 1);
return {
skills: lazySkills({
skchr_excu2_1: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 8),
mods: mods({ atkPct: num(bb.atk), defIgnoreFlat: num(bb.def_penetrate_fixed) }),
targeting: skillRange(chess, def),
}),
skchr_excu2_3: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 16),
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def),
attack: { onEachHit({ unit, target }) { if (target && target.side === 'enemy') unit.mem.verdict?.add(target); } },
onStart({ unit }) {
unit.mem.verdict = new Set();
// 特性的回复生命效果提高至2倍 (the reaper profile reads profile.selfHeal on every attack)
if (unit.profile) { unit.mem.verdictHeal0 ??= num(unit.profile.selfHeal, 50); unit.profile.selfHeal = unit.mem.verdictHeal0 * num(bb.trait_ratio, 1); }
},
onEnd({ battle, unit }) {
const hit = unit.mem.verdict;
unit.mem.verdict = null;
if (unit.profile && unit.mem.verdictHeal0 != null) unit.profile.selfHeal = unit.mem.verdictHeal0;
if (hit && unit.alive) {
for (const e of hit) {
if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['attack@final_atk_scale']), type: 'phys', isSkill: true, tags: ['skill', 'verdict'] });
}
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id, r: 1.5, skill: 'excu2' });
}
battle.removeBuff(unit, 'excu2:verdict');
},
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 12),
mods: mods({ atkPct: num(bb.atk), defPct: num(bb.def), blockCnt: num(bb.block_cnt) }),
},
// REA-Y: its trait part is DISPLAY-only (official battle_equip_table uniequip_003_excu2: target DISPLAY, the ASPD
// text "{value}" = 12; the ASPD rider itself is the talent −1 part). data/chess.json merges that display blackboard
// into trait.bb ({value: 12}), which the reaper profile would read as its per-hit heal: the heal stays the base
// trait's (50).
trait: tm['trigger_cnt[equip]'] != null && chess?.traitBase?.bb?.value != null ? { selfHeal: num(chess.traitBase.bb.value, 50) } : undefined,
talents: [
{ install(battle, unit) { // 受选之人
let spent = 0;
const reset = (c) => { if (c.unit === unit) spent = 0; };
battle.on('skillStart', reset, { owner: unit });
battle.on('skillEnd', reset, { owner: unit });
battle.on('ammoUsed', (c) => { if (c.unit === unit) spent++; }, { owner: unit });
// PRTS 备注: "触发时可额外回复1点攻击回复技力,不额外消耗弹药" — the extra attack is an attack (attack SP, trait
// heal) but consumes no ammo (engine forceAttack `noAmmo`: no bullet, no ammoUsed for any listener — the
// 7-bullet 特质 counter, items, bonds and the prob_add counter above)
battle.on('attack', (c) => {
if (c.attacker !== unit || unit.mem.extraAttack || !on(unit)) return;
const p = num(t0.prob) + (unit.skill?.active ? num(t0.prob_add) * spent : 0);
if (!(p > 0) || !battle.rng.chance(Math.min(1, p))) return;
const dep = unit.deploySeq;
battle.after(0, () => {
if (!on(unit) || unit.deploySeq !== dep || !unit.canAct || unit.s.flags.disarm) return;
unit.mem.extraAttack = true;
let hit = false;
try { hit = battle.forceAttack(unit, null, { noAmmo: true }); } finally { unit.mem.extraAttack = false; }
if (hit) battle.fx('extraAttack', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
}, { owner: unit });
} },
{ install(battle, unit) { // 铳弹共感
battle.on('skillStart', (c) => {
if (c.unit !== unit || c.skill.kind !== 'ammo') return;
const n = battle.allies().filter((a) => isOp(a) && isLaterano(a)).length;
const stacks = Math.min(num(t1.add_count_max_stack, 4), n);
if (stacks > 0) c.skill.addAmmo(stacks * num(t1.add_count, 1));
}, { owner: unit });
} },
],
install(battle, unit) {
crowdAspd(battle, unit, 'excu2:module', num(tm.attack_speed), num(tm['trigger_cnt[equip]']));
if (sid === 'skchr_excu2_3') { // S3: +attack@atk ATK per bullet spent
battle.on('ammoUsed', (c) => {
if (c.unit !== unit || !unit.skill?.active) return;
battle.addBuff(unit, { key: 'excu2:verdict', refresh: 'stack', maxStacks: Math.max(1, num(bb['attack@max_stack_cnt'], 30)), mods: mods({ atkPct: num(bb['attack@atk']) }) });
}, { owner: unit });
}
if (sid && sid !== 'skchr_excu2_2') return;
// S2: melee attacks dodged with `prob`, each dodge refills ammo
battle.on('hit', (c) => {
if (c.target !== unit || !unit.skill?.active || !c.source || c.source.side !== 'enemy' || !c.dmg.isAttack) return;
if (!meleeAttacker(c.source, unit) || !(prob > 0) || !battle.rng.chance(prob)) return;
c.dmg.cancel = true;
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
// a dodge like the engine's (listeners of the `dodge` hook — items/bonds — see it too)
if (battle.hasHook('dodge')) battle.emit('dodge', { source: c.source, target: unit, dmg: c.dmg });
unit.skill.addAmmo(refill);
}, { owner: unit });
},
};
},
};
@@ -0,0 +1,177 @@
// server/sim/content/kits/ops/chess_char_5_02-titi.js — 缇缇 (char_4056_titi) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import {
AURA_IV, AURA_DUR, RING1, num, on, talent, mods, dist, inFaction, isOp, selectedId, lazySkills, whileOn,
} from '../shared/tier5.js';
const isSargonMinos = (u) => inFaction(u, 'sargonShip', ['sargon', 'minos']);
/** Enemy not moving along its route (blocked, stunned, rooted, waiting…). */
const isStill = (e) => !!(e.blockedBy || !e.moving || e.s.flags.stun || e.s.flags.noMove || !(e.s.moveSpeed > 0));
export default {
// ---------------------------------------------------------------------------------------------------------------
// 缇缇 — S3 旧日绽放 (25 s): ATK +, 2 targets, sleeps non-sleeping targets; during the skill every enemy waking up (or
// knocked out asleep) takes arts damage growing with the sleep time (linear min_atk_scale → max_atk_scale over `sleep` s,
// PRTS 备注) and sends the highest-aggro enemy within range_radius to sleep; other allies in range fall asleep (小睡)
// instead of dying. T1 凝固的时光: can attack sleeping enemies, +15 % ATK arts vs non-moving
// enemies, sleeping enemies take 30 % ATK arts/s. T2 勇气的报偿: Sargon/Minos ops above 50 % HP get +20 ASPD.
// S1 缓蚀 (duration): ATK +, attack@prob chance per attack to sleep the target attack@sleep s.
// S2 封护 (duration): no attacks, ATK +; she and the lowest-HP-ratio operator in her range sleep until the skill ends
// (沉睡 = invulnerable), enemies around either of them keep falling asleep; T1 ×talent_scale.
chess_char_5_02_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1);
const sid = selectedId(chess, def);
const hitSleep = num(bb['attack@sleep'], num(bb.sleep, 5));
const minS = num(bb.min_atk_scale, 1), maxS = num(bb.max_atk_scale, minS);
const chainSleep = num(bb.sleep, hitSleep), radius = num(bb.range_radius, 1.5), chainN = Math.max(1, num(bb.max_target, 1));
// S2 封护: "第一天赋效果提升至N倍" while it runs
const t0Scale = (unit) => (sid === 'skchr_titi_2' && unit.skill?.active ? num(bb.talent_scale, 1) : 1);
const ward = (battle, unit, a) => {
const entered = !a.buffs.some((b) => (b.status ?? b.key) === 'sleep');
battle.addBuff(a, { key: 'titi:ward', flags: { sleep: true }, visible: true, status: 'sleep', data: { src: unit } });
battle.releaseBlocked(a);
if (battle.hasHook('statusApplied')) battle.emit('statusApplied', { source: unit, target: a, status: 'sleep', duration: Infinity, entered });
};
return {
skills: lazySkills({
skchr_titi_1: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
attack: {
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.s.flags.sleep || !battle.rng.chance(num(bb['attack@prob']))) return;
battle.applyStatus(target, 'sleep', { duration: num(bb['attack@sleep']), source: unit });
},
},
}),
skchr_titi_2: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
attack: { noAttack: true },
onStart({ battle, unit }) {
const pick = battle.alliesInGrid(unit).filter((a) => a !== unit && isOp(a) && a.hp > 0)
.sort((a, b) => a.hpRatio - b.hpRatio || a.id - b.id)[0] ?? null;
unit.mem.titiWard = pick ? [unit, pick] : [unit];
for (const a of unit.mem.titiWard) ward(battle, unit, a);
unit.mem.titiWardAcc = AURA_IV;
battle.fx('sleepGuard', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
unit.mem.titiWardAcc += dt;
if (unit.mem.titiWardAcc < AURA_IV - 1e-9) return;
unit.mem.titiWardAcc = 0;
for (const a of unit.mem.titiWard || []) {
if (!a.alive || !a.deployed || !a.findBuff('titi:ward')) continue;
for (const e of battle.foesInRadius(a.x, a.y, RING1)) if (e.alive) battle.applyStatus(e, 'sleep', { duration: AURA_DUR, source: unit });
}
},
onEnd({ battle, unit }) {
for (const a of unit.mem.titiWard || []) {
const b = a.findBuff('titi:ward');
if (b && b.data.src === unit) battle.removeBuff(a, b);
}
unit.mem.titiWard = null;
},
}),
}),
trait: { hitSleep: true }, // 凝固的时光 (a): she targets and damages sleeping enemies (沉睡 = 无敌 for everyone else)
skill: {
kind: 'duration',
mods: mods({ atkPct: num(bb.atk) }),
attack: {
maxTargets: Math.max(1, num(bb['attack@max_target'], 2)),
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.s.flags.sleep) return;
battle.applyStatus(target, 'sleep', { duration: hitSleep, source: unit });
},
},
onEnd({ battle, unit }) {
for (const a of battle.allyUnits) {
const b = a.findBuff('titi:allySleep');
if (b && b.data.src === unit) battle.removeBuff(a, b);
}
},
},
talents: [
{ install(battle, unit) { // 凝固的时光 — (a) is the `hitSleep` trait above
// (b) +extra_atk_scale arts vs non-moving enemies
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg?.isAttack || c.dmg.isSplash || c.type === 'element') return;
const e = c.target;
if (e.side !== 'enemy' || !e.alive || !isStill(e)) return;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(t0.extra_atk_scale) * t0Scale(unit), type: 'arts', tags: ['talent', 'titiStill'] });
}, { owner: unit });
// (c) sleeping enemies take damage_atk_scale × ATK arts per second
whileOn(battle, unit, 1, () => {
const amount = unit.s.atk * num(t0.damage_atk_scale) * t0Scale(unit);
for (const e of battle.enemies) {
if (e.alive && !e.hidden && e.s.flags.sleep) battle.dealDamage(unit, e, { amount, type: 'arts', tags: ['talent', 'titiDream'] });
}
});
} },
{ install(battle, unit) { // 勇气的报偿: 精力充沛
whileOn(battle, unit, AURA_IV, () => {
for (const a of battle.allies()) {
if (isOp(a) && isSargonMinos(a) && a.hpRatio > num(t1.hp_ratio, 0.5)) battle.addBuff(a, { key: 'titi:vigor', duration: AURA_DUR, mods: { aspd: num(t1.attack_speed) } });
}
});
} },
],
install(battle, unit) {
if (sid && sid !== 'skchr_titi_3') return;
// S3: wake-up burst chain (sleep applied by anyone counts)
const asleep = new Map();
// PRTS 备注: "向1.5半径内除该单位外仇恨值最高的敌方单位施加沉睡" — highest aggro (taunt) first, then the nearest
const sleepOthers = (from) => {
const cands = battle.foesInRadius(from.x, from.y, radius).filter((o) => o !== from && !o.s.flags.sleep && !o.mem?.candleOwner)
.sort((a, b) => (b.s.taunt || 0) - (a.s.taunt || 0) || dist(a, from) - dist(b, from) || a.spawnSeq - b.spawnSeq);
let n = 0;
for (const o of cands) {
if (n >= chainN) break;
if (battle.applyStatus(o, 'sleep', { duration: chainSleep, source: unit })) n++;
}
};
battle.on('statusApplied', (c) => {
if (c.status === 'sleep' && c.target.side === 'enemy' && !asleep.has(c.target)) asleep.set(c.target, battle.time);
}, { owner: unit });
battle.on('tick', () => {
if (!asleep.size) return;
for (const [e, t0s] of asleep) {
if (!e.alive) { asleep.delete(e); continue; }
if (e.s.flags.sleep) continue;
asleep.delete(e);
if (!unit.skill?.active || !on(unit)) continue;
const scale = minS + (maxS - minS) * Math.min(1, (battle.time - t0s) / Math.max(0.1, chainSleep));
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'titiWake'] });
battle.fx('wake', { x: e.x, y: e.y, id: e.id, scale: Math.round(scale * 100) / 100 });
sleepOthers(e);
}
}, { owner: unit });
battle.on('kill', (c) => {
const v = c.victim;
if (v.side !== 'enemy' || !asleep.has(v)) return;
asleep.delete(v);
if (unit.skill?.active && on(unit)) sleepOthers(v);
}, { owner: unit });
// S3: other allies in range fall asleep instead of dying, until healed to full or the skill ends
battle.on('fatal', (c) => {
const a = c.unit;
if (c.prevented || a.side !== 'ally' || a === unit) return;
const guard = a.findBuff('titi:allySleep');
if (guard) { if (guard.data.src === unit) c.prevented = true; return; }
if (!isOp(a) || !unit.skill?.active || !on(unit) || !unit.rangeKeySet?.has(a.tileR * COLS + a.tileC)) return;
c.prevented = true;
const entered = !a.buffs.some((b) => (b.status ?? b.key) === 'sleep');
battle.addBuff(a, {
key: 'titi:allySleep', flags: { sleep: true }, visible: true, status: 'sleep', data: { src: unit }, interval: 0.2,
onTick: ({ unit: x, buff }) => { if (x.hp >= x.s.maxHp - 1e-6) battle.removeBuff(x, buff); },
});
// other content (特质 "干员进入沉睡时") observes it like any sleep status
if (battle.hasHook('statusApplied')) battle.emit('statusApplied', { source: unit, target: a, status: 'sleep', duration: Infinity, entered });
battle.fx('sleepGuard', { x: a.x, y: a.y, id: a.id });
}, { owner: unit, priority: 5 });
},
};
},
};
@@ -0,0 +1,166 @@
// server/sim/content/kits/ops/chess_char_5_03-blaze2.js — 烛煌 (char_1040_blaze2) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { bodyInKeys } from '../../../body.js';
import {
AURA_IV, AURA_DUR, num, on, talent, traitBb, skillGrid, batPct, mods, isOp, selectedId, lazySkills, instantKind,
skillRange, burstSpUp, elementHit, whileOn, burstDamageUp,
} from '../shared/tier5.js';
/** 烛煌 绝处重燃 "使附近的敌人晕眩" radius (tiles). PRTS 备注: 1.7. */
const NEARBY_RADIUS = 1.7;
/** 烛煌 S3 "攻击变为群体攻击": splash radius around the target (tiles). PRTS 技能3 备注 "攻击溅射半径1.7". */
const BLAZE_S3_SPLASH = 1.7;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 烛煌 — S3 众恶的焚场 (ammo): skill range 4-11 (the targets; the DEFAULT cast still needs an enemy in 3-1), ATK +,
// BAT −1.3 s, "攻击变为群体攻击" = one target + a BLAZE_S3_SPLASH (1.7) splash around it (PRTS 备注 "攻击溅射半径1.7",
// 中点判定 — so the fire reaches past the diamond; it used to hit every enemy inside it instead, community report E1
// after 0.1.0), +attack@atk_scale ATK elemental damage to every enemy of the attack in a burn burst (main and splash),
// dealt BEFORE the attack's own damage ("于攻击造成伤害前判定元素爆发并造成元素伤害": a 'hit' hook — so a hit that kills
// or starts the burst keeps / does not get it); loses 3 % max HP/s; any burn burst on the field refills ammo_recover
// bullets, never above the skill's ammo ("补充后的弹药数量无法超过上限"). Each landed bolt shows its splash (fx 'splash' r 1.7).
// T1 熔点引爆: burn burst anywhere → 350 % ATK elemental damage to it + heal 12 % max HP. T2 绝处重燃: downed instead of
// dying (6000 shield, no attack, no heal, 3 %/s regen) → revives at full HP and stuns nearby enemies.
// Module (elite): ×damage_scale vs enemies in an element burst.
// S1 炙手之援 (instant): the operator with the highest max HP in her range carries a fire aura for max_duration s —
// every `interval` s the enemies within range_radius of it take atk_scale × her ATK arts + element_multiplier × that
// damage as 灼燃损伤. S2 沸血燎原 (duration): skill range, BAT +0.9 s, ATK +, 3 targets; each attack costs
// attack@hp_ratio of her max HP and leaves a burning tile under every target: ground enemies on it move −50 % and take
// atk_scale × ATK arts + element_damage_scale × that as 灼燃损伤 per second (the tiles burn until the skill ends
// [ASSUMED: no duration in the data]). Module PRI-Y (elite): SP +0.2/s while an enemy in range is in an element burst.
chess_char_5_03_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess), tm = talent(chess, -1);
const sid = selectedId(chess, def);
const lose = num(bb.lose_hp_scale);
const burnTiles = (battle, unit) => {
const tiles = unit.mem.blazeTiles;
if (!tiles || !tiles.size) return;
unit.mem.blazeAcc2 = (unit.mem.blazeAcc2 ?? 0) + AURA_IV;
const dmgTick = unit.mem.blazeAcc2 >= 1 - 1e-9;
if (dmgTick) unit.mem.blazeAcc2 -= 1;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.isFlying || !bodyInKeys(e, tiles)) continue;
battle.addBuff(e, { key: `blaze2:ground:${unit.id}`, duration: AURA_DUR, mods: { moveMul: Math.max(0, 1 + num(bb.move_speed)) } });
if (!dmgTick) continue;
const d = battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill', 'blazeGround'] });
if (d > 0 && e.alive) battle.dealDamage(unit, e, { type: 'element', element: 'burn', amount: d * num(bb.element_damage_scale), tags: ['skill', 'blazeGround'] });
}
};
return {
skills: lazySkills({
skchr_blaze2_1: () => ({
kind: instantKind(chess, def),
onStart({ battle, unit }) {
const t = battle.alliesInGrid(unit).filter((a) => isOp(a)).sort((a, b) => b.s.maxHp - a.s.maxHp || a.id - b.id)[0];
if (!t) return;
const r = num(bb.range_radius, 1.5);
battle.addBuff(t, {
key: `blaze2:aid:${unit.id}`, duration: num(bb.max_duration, 20), interval: Math.max(0.1, num(bb.interval, 1)), visible: true, data: { src: unit },
onTick: ({ unit: x }) => {
for (const e of battle.foesInRadius(x.x, x.y, r)) {
const d = battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill', 'blazeAid'] });
if (d > 0 && e.alive) battle.dealDamage(unit, e, { type: 'element', element: 'burn', amount: d * num(bb.element_multiplier), tags: ['skill', 'blazeAid'] });
}
},
});
battle.fx('aoe', { x: t.x, y: t.y, id: unit.id, r, skill: 'blaze2Aid' });
},
}),
skchr_blaze2_2: () => ({
kind: 'duration',
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def, { maxTargets: Math.max(1, num(bb['attack@max_target'], 3)) }),
attack: {
onEachHit({ unit, target }) {
if (target && target.side === 'enemy') unit.mem.blazeTiles?.add(Math.round(target.y) * COLS + Math.round(target.x));
},
},
onStart({ unit }) { unit.mem.blazeTiles = new Set(); unit.mem.blazeAcc2 = 0; },
onAttack({ battle, unit }) { battle.loseHp(unit, unit.s.maxHp * num(bb['attack@hp_ratio']), { source: unit }); },
onEnd({ unit }) { unit.mem.blazeTiles = null; },
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 18),
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : undefined,
attack: {
splashRadius: BLAZE_S3_SPLASH,
// the fire of each landed bolt (the screen rings splash attacks by sub-profession; 本源术师 is not one): once
// per attack at the main target / its spot — from the profile it was fired with, so the last bolt too
onHit({ battle, unit, target, x, y }) {
battle.fx('splash', { x, y, ...(target && target.alive ? { id: target.id } : {}), src: unit.id, r: BLAZE_S3_SPLASH, element: 'burn' });
},
},
onStart({ unit }) { unit.mem.blazeAcc = 0; },
onTick({ battle, unit, dt }) {
if (!(lose > 0)) return;
unit.mem.blazeAcc = (unit.mem.blazeAcc ?? 0) + dt;
while (unit.mem.blazeAcc >= 1 && unit.alive) {
unit.mem.blazeAcc -= 1;
battle.loseHp(unit, unit.s.maxHp * lose, { source: unit });
}
},
},
talents: [
{ install(battle, unit) { // 熔点引爆
battle.on('elementBurst', (c) => {
if (c.element !== 'burn' || !on(unit) || c.target.side !== 'enemy') return;
elementHit(battle, unit, c.target, unit.s.atk * num(t0.ep_damage_scale), 'blazeMelt', 'burn');
// 绝处重燃: "倒地期间…无法被治疗" — the meltdown heal does not reach her while downed
if (!unit.mem.downed) battle.heal(unit, unit, unit.s.maxHp * num(t0.hp_ratio), { self: true });
battle.fx('meltdown', { x: c.target.x, y: c.target.y, id: c.target.id });
}, { owner: unit });
} },
{ install(battle, unit) { // 绝处重燃
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.downed = false; }, { owner: unit });
battle.on('fatal', (c) => {
if (c.unit !== unit || c.prevented || unit.mem.downed) return;
c.prevented = true;
unit.mem.downed = true;
if (unit.skill?.active) unit.skill.end('downed');
battle.releaseBlocked(unit);
const dep = unit.deploySeq;
battle.addBuff(unit, {
key: 'blaze2:downed', visible: true, shield: num(t1.dynamic), interval: 0.2,
flags: { disarm: true, noHeal: true },
mods: mods({ hpRegenRatio: num(t1.hp_recovery_per_sec_by_max_hp_ratio), blockCnt: -99 }),
onTick: ({ unit: u, buff }) => {
if (u.deploySeq !== dep || u.hp < u.s.maxHp - 1e-6) return;
battle.removeBuff(u, buff);
u.mem.downed = false;
for (const e of battle.foesInRadius(u.x, u.y, NEARBY_RADIUS)) if (e.alive) battle.applyStatus(e, 'stun', { duration: num(t1.stun), source: u });
battle.fx('revive', { x: u.x, y: u.y, id: u.id, r: NEARBY_RADIUS });
},
});
battle.fx('downed', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit, priority: -50 });
} },
],
install(battle, unit) {
burstDamageUp(battle, unit, num(tb.damage_scale));
burstSpUp(battle, unit, 'blaze2:module', num(tm.sp_recovery_per_sec));
if (sid === 'skchr_blaze2_2') whileOn(battle, unit, AURA_IV, () => burnTiles(battle, unit));
if (sid && sid !== 'skchr_blaze2_3') return;
// S3: each hit of her skill attack (main and splash) on an enemy in a burn burst deals the bonus first (PRTS 备注).
// Gated on the attack's own isSkill (captured when the bolt was fired), not on the live skill: her bolts land after
// the last bullet's end('ammo') / an early end (downed), and those still carry it. Only S3 runs here (sid gate).
battle.on('hit', (c) => {
const d = c.dmg, t = c.target;
if (c.source !== unit || !d.isAttack || !d.isSkill || d.cancel) return;
if (t.side === 'enemy' && t.alive && t.findBuff('burnBurst')) elementHit(battle, unit, t, unit.s.atk * num(bb['attack@atk_scale']), 'blazeBurn', 'burn');
}, { owner: unit });
const maxAmmo = num(bb['attack@trigger_time'], 18);
battle.on('elementBurst', (c) => { // S3: burn bursts refill ammo, up to the skill's ammo
const sk = unit.skill;
if (c.element !== 'burn' || !sk?.active || sk.kind !== 'ammo' || !on(unit)) return;
const n = Math.min(num(bb.ammo_recover), maxAmmo - sk.ammoLeft);
if (n > 0) sk.addAmmo(n);
}, { owner: unit });
},
};
},
};
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff. Show more