Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-leizi2.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

342 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/op-leizi2.js — 司霆惊蛰 (char_1043_leizi2) 自选 operator kit: 6★ 解放者 (近卫), an owned-6★
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (LIB-X) at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_1043_leizi2, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, LIB-X at stage 1 (tier 5) or 3 (tier 6). Full potential (the owner's decision of 2026-10-07).
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); the client's battle
// logic — buff_template_data (leizi2_tr, leizi2_e_002_tr, leizi2_s_cast, leizi2_t_1[atk_up] / [on_calcul] / [fly] /
// [trigger] / [buff_start], leizi2_s_2[try_add_atk], leizi2_s_3), her charpack (Trait buff: triggerInterval 1; the
// "thunder" ability: `_maxCount` 15, `_resetCountList` 3…7, `_rangeCastDelay` 0.25; S1 abilities s1_m / s1_l / s1_r:
// `_preDelay` 0.3, ground selectors; S3 attack: projectile_char_leizi2_s3 every `_triggerDelta` 0.28 s), the skill prefabs
// (skchr_leizi2_1: leizi2_s_1 lifeTime 0.5; skchr_leizi2_2: the flood-fill selector `_depth` 3, `checkHeightType`) and
// the projectile prefabs (projectile_char_leizi2_t_1 / [start]: lands `_delayTime` 0.15 s after its cast, ground + air;
// projectile_char_leizi2_s3: speed 1, `_lifeTime` 3.1, `_keepAlreadyHitTime` 0.6, ground only, magical, modifier key
// "leizi2", rebounds off its source), her skeleton's clips (Skill_1_Begin 0.5 s with OnAttack at 0.3; Skill_2_Begin /
// Skill_3_Begin 0.5 s; Skill_1_End / Skill_2_End 0.933 s, Skill_3_End 0.833 s) — and PRTS 司霆惊蛰 (talent / skill 备注).
// The official map of a battle is its field (`rect`): stage tiles beyond it (another player's half) are no tiles here —
// no strike falls there, S2's range and S3's currents stop at its edge [ASSUMED].
// - Trait (解放者) "通常不攻击且阻挡数为0,技能未开启时40秒内攻击力逐渐提升至最高+200%且技能结束时重置攻击力": the librator
// profile (professions.js: no attack and block 0 while the skill is off, ATK +atk / max_stack_cnt every second up to
// +atk while it is off, reset when it ends). Module LIB-X “一念” "部署后获得+100%加成": its hidden talent init_atk
// (leizi2_e_002_tr: init_atk / (atk / max_stack_cnt) stacks at each deployment) — the profile's rampInit (her DISPLAY
// trait part carries no blackboard, unlike 玛恩纳's, so the kit hands it over).
// - T1 明断 "攻击范围内每个地块每秒有10%的概率落雷对所有敌人造成相当于攻击力100%的法术伤害。未开启技能时起飞;技能期间攻击时攻击
// 力提升至107%" (full potential: 111 %):
// · the strikes (PRTS 备注's algorithm, the thunder ability's numbers): every 1.0333 s from each deployment, each tile of
// her current attack range — first seen: a counter at a random 0…14; else +1, and at 15 the tile is struck and its
// counter restarts at a random 3…7; counters outlive a tile leaving the range and die with the deployment. A strike
// lands 0.15 s after its cast: atk_scale_t × ATK arts on every enemy she can select on that tile, air units too
// (PRTS 可对空). Counting pauses while she cannot act [ASSUMED: the ability does not cast under 晕眩 / 冻结].
// · 起飞 while no skill runs (leizi2_t_1[fly] in her default mode only; gamedata_const ba.liftoff): flag `liftoff` —
// no ground enemy selects her or is blocked by her (her block count is 0 then anyway). PRTS "开启技能时的降落动作与技能
// 结束时的起飞动作…期间自身不处于起飞状态,其中降落动作计入技能总时长": she lands at the cast and makes no attack for the
// landing clip (S2 / S3 0.5 s, inside the skill's time; S1's attacks are its own), and after the skill she is up
// again only when the take-off clip is over (S1 / S2 0.933 s, S3 0.833 s) — ground enemies may select her meanwhile;
// her block count is the trait's 0 from the skill's end on.
// · leizi2_t_1[on_calcul] (AtkScaleUp on ON_CALCULATE_DAMAGE while a skill runs; PRTS "对自身技能期间造成的所有伤害生效"):
// an atkScaleMul ×atk_scale[skill_up] buff for the skill's time — her attacks (ai.js resolveHit) and every damage this
// kit computes (strikes, S1, currents) multiply it. LIB-X stage 2+: the module talent's value (stage 3: 1.17).
// - T2 追责 "开启技能时,使攻击范围内所有地面地块落雷,对所有敌人造成相当于攻击力100%的法术伤害和2秒战栗" (full potential: 3 s): at every cast (each S1
// charge too) every low tile of her attack range — the skill's, S2's flood fill included — is struck, ring by ring by
// Manhattan distance from her tile (PRTS "自内向外"), 0.25 s per ring (the thunder `_rangeCastDelay`; 0.07 s under S1 —
// leizi2_t_1[trigger] `_specialRangeCastDelay`), each landing 0.15 s after its cast: atk_scale_t2 × ATK arts and 战栗
// (`tremble`) not_combat s on every enemy she can select there, air units too. The strikes start at the cast
// [ASSUMED: leizi2_t_1[trigger] rides her landing clip's start event (≤ 0.17 s in), not modelled].
// - S1 浩气长存 (MANUAL, 可充能 3 次, data SEARCH): a 0.5 s skill (PRTS "技能持续0.5s"; skill prefab lifeTime 0.5) whose
// range is the 3-19; no normal attack in it; 0.3 s after the cast (the abilities' `_preDelay`) three attacks — a 3-2 to
// her left, her front and her right (PRTS: the 3-19 only explains the coverage) — each hit every ground enemy she can
// select on it for attack@atk_scale_s1 × ATK physical (one on her own tile takes all three). "充能耗尽前特性不重置"
// (leizi2_s_cast: the trait resets only when no charge is left): while a charge remains the ramp is kept.
// - S2 正霆摄威 (MANUAL, data SEARCH): at the cast her range becomes the low tiles reachable from hers in up to three 4-way
// steps over the field (PRTS 备注: "任意高度类型为低地的地块以及干员自己所在的地块", "不存在地块数据的地块始终视为不合法";
// a real attack range — rangeExtend applies), fixed for the skill; attacks hit attack@max_target_s2 targets for
// attack@atk_scale_s2 × ATK physical, air units too (PRTS 攻击可对空); every lightning cast of hers while it runs
// (leizi2_s_2[try_add_atk], the thunder ability's buffsToOwnerOnCast — 追责's strikes too) gives ATK +thunder_atk, up
// to thunder_max_stack_cnt stacks, gone when it ends.
// - S3 天地通明 (MANUAL, data CUSTOM_RANGE on its 2-1): range 2-1, attack interval +base_attack_time s (flat), each attack
// hits its target and every enemy within attack@range_radius of it for attack@atk_scale_s3 × ATK physical (air units
// too: PRTS 普通攻击可对空); after each attack four currents leave the target's spot — up, right, down, left (screen
// directions), 0.28 s apart (the first at the hit [ASSUMED]) — at 1 tile/s for 3.1 s, turning back before they would
// enter a high tile, her own tile or the field's edge (PRTS "只在即将进入地块时进行反弹判定"), gone when she leaves (they
// outlive the skill [ASSUMED]); each hits every ground enemy she can select on its
// tile for attack@atk_scale_current × ATK arts, each enemy at most every 0.6 s per current, and — while S3 runs
// (leizi2_s_3 ON_OUTPUT_MODIFIER, modifier key "leizi2") — with `prob` to 战栗 it not_combat s.
import { num, talentBb, moduleBb, skillRec, batMod, up } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { toLocal } from '../../../dir.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_leizi2_1';
const S2 = 'skchr_leizi2_2';
const S3 = 'skchr_leizi2_3';
/** The skill ranges when a record carries none (data: 3-19, 2-1). */
const R3_19 = Object.freeze([[3, 0], [2, 0], [1, 0], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-2, 0], [-3, 0]]);
const R2_1 = Object.freeze([[2, 0], [1, 0], [1, 1], [0, 0], [0, 1], [0, 2], [-1, 0], [-1, 1], [-2, 0]]);
/** S1's three attacks (PRTS 备注): a 3-2 to her front, her left and her right (facing-RIGHT offsets). */
const S1_ARMS = Object.freeze([
Object.freeze([[0, 0], [0, 1], [0, 2], [0, 3]]),
Object.freeze([[0, 0], [1, 0], [2, 0], [3, 0]]),
Object.freeze([[0, 0], [-1, 0], [-2, 0], [-3, 0]]),
]);
/** skchr_leizi2_1 leizi2_s_1 lifeTime (PRTS "技能持续0.5s") and the s1_* abilities' `_preDelay`. */
const S1_TIME = 0.5;
const S1_STRIKE_AT = 0.3;
/** PRTS 明断 备注: "部署后每1.0333秒对自身当前攻击范围内所有地块执行计数判定" (31 ticks). */
const COUNT_IV = 31 / 30;
/** The thunder ability: a strike at `_maxCount` 15, then a restart at one of `_resetCountList` 3…7; a new tile 0…14. */
const COUNT_MAX = 15;
const RESET_MIN = 3;
const RESET_SPAN = 5;
/** projectile_char_leizi2_t_1 / [start] `_delayTime`: a strike lands this long after its cast. */
const STRIKE_DELAY = 0.15;
/** 追责: the thunder `_rangeCastDelay` per Manhattan ring; leizi2_t_1[trigger] `_specialRangeCastDelay` under S1. */
const RING_DELAY = 0.25;
const RING_DELAY_S1 = 0.07;
/** projectile_char_leizi2_s3: speed, lifetime, one hit per enemy per this long; the S3 attack's `_triggerDelta`. */
const CURRENT_SPEED = 1;
const CURRENT_LIFE = 3.1;
const CURRENT_REHIT = 0.6;
const CURRENT_GAP = 0.28;
/** PRTS: "依次生成向上/向右/向下/向左移动的电流" — screen directions as [dRow, dCol] (row 0 is the bottom). */
const CURRENT_DIRS = Object.freeze([[1, 0], [0, 1], [-1, 0], [0, -1]]);
const FOUR = Object.freeze([[1, 0], [0, 1], [-1, 0], [0, -1]]);
const AIR = Object.freeze({ canHitFly: true });
const GROUND = Object.freeze({ canHitFly: false });
/** Her skeleton's landing clips (Skill_2_Begin / Skill_3_Begin; S1's is its own 0.5 s) and take-off clips (Skill_N_End). */
const LANDING = Object.freeze({ [S2]: 0.5, [S3]: 0.5 });
const TAKEOFF = Object.freeze({ [S1]: 0.933, [S2]: 0.933, [S3]: 0.833 });
const LIFTOFF_KEY = 'talent:leizi2:liftoff';
const SKILL_UP_KEY = 'talent:leizi2:skillUp';
const S2_ATK_KEY = 'skill:leizi2:thunderAtk';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** Absolute tile keys of a grid at the unit's tile and facing. */
const keysOf = (unit, grid) => absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0);
/** A tile of the battle's field — the official map of a battle: no lightning falls on a stage tile beyond it. */
const onField = (battle, k) => battle.grid.inRect(Math.floor(k / COLS), k % COLS);
/**
* S2's range: the tiles reachable from hers in up to three 4-way steps over low tiles (her own tile always; a tile
* without data never — off the battle's field: the official map of a battle is its field, `rect`), as facing-RIGHT
* offsets (PRTS 备注, the selector's `_depth` 3).
*/
function floodGrid(battle, unit) {
const r0 = unit.tileR, c0 = unit.tileC;
const seen = new Set([r0 * COLS + c0]);
const cells = [[r0, c0]];
let frontier = [[r0, c0]];
for (let step = 0; step < 3 && frontier.length; step++) {
const next = [];
for (const [r, c] of frontier) {
for (const [dr, dc] of FOUR) {
const rr = r + dr, cc = c + dc, k = rr * COLS + cc;
if (seen.has(k) || !battle.grid.inRect(rr, cc) || battle.grid.tile(rr, cc).height !== 'LOW') continue;
seen.add(k);
next.push([rr, cc]);
cells.push([rr, cc]);
}
}
frontier = next;
}
return cells.map(([r, c]) => toLocal(r - r0, c - c0, unit.dir));
}
export default {
char_1043_leizi2: (bb, chess) => {
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const hidden = moduleBb(chess); // LIB-X: init_atk
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s1 = skillRec(chess, S1), s3 = skillRec(chess, S3);
const skillUp = num(t0['atk_scale[skill_up]'], 1);
const s2Grid = [[0, 0]]; // this unit's S2 range, rebuilt at every cast
/** A lightning cast on tile `key` (T1's strikes, `accuse` = T2's): S2's stack at once, the strike 0.15 s later. */
const cast = (battle, unit, key, accuse) => {
const sk = unit.skill;
if (sk && sk.active && sk.id === S2 && num(b2.thunder_atk) > 0) {
battle.addBuff(unit, { key: S2_ATK_KEY, refresh: 'stack', stacks: 1, maxStacks: Math.max(1, Math.floor(num(b2.thunder_max_stack_cnt, 25))), mods: { atkPct: num(b2.thunder_atk) }, tags: ['skill'] });
}
const seq = unit.deploySeq;
battle.after(STRIKE_DELAY, () => {
if (!up(unit) || unit.deploySeq !== seq) return; // _stopWhenSourceInvalid
const r = Math.floor(key / COLS), c = key % COLS;
battle.fx('lightning', { x: c, y: r, id: unit.id, skill: accuse ? 'leizi2:accuse' : 'leizi2:thunder' });
const scale = num(accuse ? t1.atk_scale_t2 : t0.atk_scale_t, 1);
for (const e of battle.enemiesInKeys([key], unit, AIR)) {
if (!e.alive) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * scale * unit.s.atkScaleMul, type: 'arts', tags: ['talent', accuse ? 'leizi2:accuse' : 'leizi2:thunder'] });
if (accuse && e.alive && num(t1.not_combat) > 0) battle.applyStatus(e, 'tremble', { duration: num(t1.not_combat), source: unit });
}
}, { owner: unit });
};
return {
trait: {
// LIB-X “一念” 部署后获得+100%加成: the trait's start (the module's hidden talent; her trait part has no blackboard)
...(num(hidden.init_atk) > 0 ? { rampInit: num(hidden.init_atk) } : {}),
// S2 / S3: no attack while she lands (PRTS "降落动作计入技能总时长")
canAttack: (b, u) => !(num(u.mem.leiziLandUntil) > b.time + 1e-9),
},
skills: {
[S1]: {
kind: 'duration',
duration: S1_TIME,
targeting: { rangeGrid: s1?.rangeGrid ?? R3_19 },
attack: { noAttack: true }, // the three attacks below are the skill's
onStart({ battle, unit, skill }) {
const seq = unit.deploySeq, act = skill.activations;
battle.after(S1_STRIKE_AT, () => {
if (!up(unit) || unit.deploySeq !== seq || skill.activations !== act) return;
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 3, id: unit.id, skill: 'leizi2:s1' });
for (const arm of S1_ARMS) {
for (const e of battle.enemiesInKeys(keysOf(unit, arm), unit, GROUND)) {
if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * num(b1['attack@atk_scale_s1'], 1) * unit.s.atkScaleMul, type: 'phys', isSkill: true, tags: ['skill', 'leizi2:s1'] });
}
}
}, { owner: unit });
},
},
[S2]: {
kind: 'duration',
targeting: { rangeGrid: s2Grid, canHitFly: true, maxTargets: Math.max(1, Math.floor(num(b2['attack@max_target_s2'], 3))) },
attack: { atkScale: num(b2['attack@atk_scale_s2'], 1) },
onStart({ battle, unit }) {
const g = floodGrid(battle, unit);
s2Grid.length = 0;
for (const p of g) s2Grid.push(p);
battle.refreshRange(unit);
},
onEnd({ battle, unit }) { battle.removeBuff(unit, S2_ATK_KEY); },
},
[S3]: {
kind: 'duration',
mods: { batPct: batMod(b3.base_attack_time, chess) },
targeting: { rangeGrid: s3?.rangeGrid ?? R2_1, canHitFly: true },
attack: {
atkScale: num(b3['attack@atk_scale_s3'], 1),
splashRadius: num(b3['attack@range_radius'], 1.65),
splashScale: 1,
onHit({ battle, unit, x, y }) { // the currents leave the target's spot
const seq = unit.deploySeq;
CURRENT_DIRS.forEach(([dr, dc], i) => {
const spawn = () => {
if (!up(unit) || unit.deploySeq !== seq) return;
(unit.mem.leiziCurrents ??= []).push({ x, y, dr, dc, life: CURRENT_LIFE, seq, hitAt: new Map() });
};
if (i === 0) spawn();
else battle.after(CURRENT_GAP * i, spawn, { owner: unit });
});
battle.fx('pulse', { x, y, id: unit.id, skill: 'leizi2:current' });
},
},
},
},
talents: [
{ install(battle, unit) { // 明断: the strikes, 起飞 while no skill runs, ×atk_scale[skill_up] during a skill
const lift = () => { if (up(unit)) battle.addBuff(unit, { key: LIFTOFF_KEY, flags: { liftoff: true }, tags: ['talent'] }); };
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
if (!unit.skill?.active) lift();
if (c.move) return;
// the strike counters: a new list with every deployment
const seq = unit.deploySeq;
const counters = new Map();
battle.every(COUNT_IV, (b, sc) => {
if (!up(unit) || unit.deploySeq !== seq) { sc.cancel(); return; }
if (!unit.canAct) return;
for (const k of unit.rangeKeys) {
if (!onField(b, k)) continue;
if (!counters.has(k)) { counters.set(k, b.rng.int(COUNT_MAX)); continue; }
const n = counters.get(k) + 1;
if (n >= COUNT_MAX) { counters.set(k, RESET_MIN + b.rng.int(RESET_SPAN)); cast(b, unit, k, false); } else counters.set(k, n);
}
}, { owner: unit });
}, { owner: unit });
battle.on('skillStart', (c) => {
if (c.unit !== unit) return;
battle.removeBuff(unit, LIFTOFF_KEY);
unit.mem.leiziLandUntil = battle.time + (LANDING[unit.skill.id] ?? 0);
if (skillUp !== 1) battle.addBuff(unit, { key: SKILL_UP_KEY, mods: { atkScaleMul: skillUp }, tags: ['talent'] });
}, { owner: unit, priority: 50 });
battle.on('skillEnd', (c) => {
if (c.unit !== unit) return;
battle.removeBuff(unit, SKILL_UP_KEY);
unit.mem.leiziLandUntil = 0;
if (c.reason === 'death') return;
// (the trait's own handler releases what she blocked) — 起飞 again once the take-off clip is over
const seq = unit.deploySeq;
battle.after(TAKEOFF[unit.skill.id] ?? 0, () => { if (unit.deploySeq === seq && !unit.skill?.active) lift(); }, { owner: unit });
}, { owner: unit });
} },
{ install(battle, unit) { // 追责: every cast strikes the low tiles of her range, ring by ring from her tile
if (!(num(t1.atk_scale_t2) > 0)) return;
battle.on('skillStart', (c) => {
if (c.unit !== unit) return;
const gap = unit.skill?.id === S1 ? RING_DELAY_S1 : RING_DELAY;
const rings = new Map();
for (const k of unit.rangeKeys) {
const r = Math.floor(k / COLS), cc = k % COLS;
if (!onField(battle, k) || !battle.grid.isLow(r, cc)) continue;
const d = Math.abs(r - unit.tileR) + Math.abs(cc - unit.tileC);
if (!rings.has(d)) rings.set(d, []);
rings.get(d).push(k);
}
const seq = unit.deploySeq;
for (const d of [...rings.keys()].sort((a, b) => a - b)) {
const fire = () => { if (up(unit) && unit.deploySeq === seq) for (const k of rings.get(d)) cast(battle, unit, k, true); };
if (d === 0) fire();
else battle.after(d * gap, fire, { owner: unit });
}
}, { owner: unit });
} },
],
install(battle, unit) {
const id = unit.skill?.id;
if (id === S1) {
// 充能耗尽前特性不重置: the trait resets the ramp on skillEnd (priority 0) — keep it while a charge remains
battle.on('skillEnd', (c) => {
if (c.unit === unit) unit.mem.leiziRamp = c.reason !== 'death' && unit.skill.charges > 0 ? num(unit.trait.ramp) : null;
}, { owner: unit, priority: 100 });
battle.on('skillEnd', (c) => {
if (c.unit !== unit || unit.mem.leiziRamp == null) return;
unit.trait.ramp = unit.mem.leiziRamp;
unit.mem.leiziRamp = null;
if (unit.trait.ramp > 0 && unit.alive) battle.addBuff(unit, { key: 'trait:libratorRamp', mods: { atkPct: unit.trait.ramp } });
}, { owner: unit, priority: -100 });
}
if (id !== S3) return;
// S3: the currents
const scale = num(b3['attack@atk_scale_current']), prob = num(b3.prob), stop = num(b3.not_combat);
battle.on('tick', ({ dt }) => {
const list = unit.mem.leiziCurrents;
if (!list || !list.length) return;
const keep = [];
for (const cur of list) {
cur.life -= dt;
if (cur.life <= 1e-9 || !up(unit) || unit.deploySeq !== cur.seq) continue;
const nx = cur.x + cur.dc * CURRENT_SPEED * dt, ny = cur.y + cur.dr * CURRENT_SPEED * dt;
const r = Math.round(ny), c = Math.round(nx);
if (r !== Math.round(cur.y) || c !== Math.round(cur.x)) {
// about to enter (r, c): back off a high tile (高台), her own tile or the field's edge [ASSUMED]
if (!battle.grid.inRect(r, c) || battle.grid.tile(r, c).height === 'HIGH' || (r === unit.tileR && c === unit.tileC)) { cur.dr = 0 - cur.dr; cur.dc = 0 - cur.dc; } else { cur.x = nx; cur.y = ny; }
} else { cur.x = nx; cur.y = ny; }
keep.push(cur);
const s3On = !!(unit.skill?.active && unit.skill.id === S3);
for (const e of battle.enemiesInKeys([Math.round(cur.y) * COLS + Math.round(cur.x)], unit, GROUND)) {
const at = cur.hitAt.get(e);
if (!e.alive || (at != null && battle.time - at < CURRENT_REHIT - 1e-9)) continue;
cur.hitAt.set(e, battle.time);
const dealt = battle.dealDamage(unit, e, { amount: unit.s.atk * scale * unit.s.atkScaleMul, type: 'arts', isSkill: true, tags: ['skill', 'leizi2:current'] });
if (s3On && dealt > 0 && e.alive && stop > 0 && battle.rng.chance(prob)) battle.applyStatus(e, 'tremble', { duration: stop, source: unit });
}
if (!unit.alive) break;
}
unit.mem.leiziCurrents = keep;
}, { owner: unit });
},
};
},
};