feedback5 O16: 自选 operator kits 涤火杰西卡 (+ her 机动盾牌), 机械师 (+ her 结构性原理) and 凯尔希 (+ her Mon3tr) — every skill, talent, trait and module (stage 1 / 3) at every form

- 涤火杰西卡 (ops/op-jesca2.js): the shield piece lasts 50 s (SPT-X stage 3: 60 s), holds 禁疗 and taunt 1, she turns to face
  it and back when it leaves, she and the unit behind her DEF +15 % (孤立 too), it falls with her and comes back 30 s later
  for 5 DP; T2 +1 SP per shield hit at 50 % (SPT-Y stage 3: 60 % and its redeploy −1 s per success, ≤ 14 s); S1 SP_FULL
  toggle ATK / DEF +40 / +55 % and the shield's DEF and +20 s life; S2 ACTIVE_RANGE 2-5 (not grown by 攻击距离), 0.3 s,
  75 % dodge; S3 ACTIVE_RANGE 20 bullets, +1 range, 1.8 s, the shell (5 tiles / s, 1.7 burst, 250 % ATK, 5 s stun) while
  the shield stands next to her; SPT-X reveal, SPT-Y +1 range.
- 机械师 (ops/op-mcnist.js): 结构性原理 30 s, 禁疗, back 40 s later for 10 DP; T2 30 % 屏障 (physical / arts, 超额防护); S1
  3 bullets of 5 strikes (1.1, air too); S2 10 % 屏障 whose break bursts (200 % arts, 战栗 2 s) and buys a new one per bullet;
  S3 the 0.8 s cross (x-6, 240 % arts), the token's charge (300 % physical, redeployed on the stop tile) and its decaying
  weaken aura; SO-A: attributes only (its trait / talent are 集成战略-only).
- 凯尔希 (ops/op-kalts.js): heal order herself / Mon3tr first (with no module the operator Mon3tr too), her heals through
  Mon3tr's 禁疗 (profile healThrough); Mon3tr DEF ×0 off her range (PHY-Y stage 3: ASPD +20 / DEF +20 % inside), withdrawn
  with her, back 25 s later for 10 DP; T2 1200 true + 3 s stun on its 3×3 when knocked out (PHY-X stage 3: 1500 / 3.5 s and
  the first time it falls to half HP); S1 DEF + 物理格挡; S2 / S3 bound to Mon3tr (绑定): Mon3tr hits every blocked enemy /
  true damage with an ATK bonus decaying every second and the 50 % 流失 without a kill; PHY-X / PHY-Y heal ×1.15; ISW-A
  attributes only.
- Triggers from the data: the 重装 exception (S2 / S3 ACTIVE_RANGE for 涤火杰西卡, DEFAULT for 机械师 — rawRule TAKE_DAMAGE),
  凯尔希's MANUAL skills DEFAULT (heal skills); 涤火杰西卡 S1 (AUTO, self / shield) SP_FULL by the owner's AUTO rule.
- OPERATOR_KIT_FILES: op-jesca2.js, op-mcnist.js, op-kalts.js.
Golden: only diy moved — diy-049 … diy-052 take the new pieces into their twelve, diy-053 … diy-058 are new (58 scenarios);
roster 49, bonds 46, fields 22, matches 17, standins 10 unchanged.
Tests: test/content/op_jesca2.test.js (10), op_mcnist.test.js (9), op_kalts.test.js (9).
This commit is contained in:
sganggs committed 2026-10-06 01:34:10 +08:00
1 parent 5888f153ac
commit d7a1d92d40
8 files changed
+2360 -117

No files matched your search

+322
View File
@@ -0,0 +1,322 @@
// server/sim/content/kits/ops/op-jesca2.js — 涤火杰西卡 (char_1034_jesca2) 自选 operator kit: 6★ 哨戒铁卫 (重装), an owned-6★
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (SPT-X “私家英雄”, SPT-Y
// 未曾风化) at every form, and the kit of her summon 机动盾牌 (token_10032_jesca2_jckshd). Kit contract and the 自选 rules:
// ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_1034_jesca2): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7,
// the picked module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no
// account]. Sources: character_table / skill_table / battle_equip_table / token_table (zh_CN, as built into backups.json);
// PRTS 涤火杰西卡 (灵活应变 备注 "转向行为不受异常效果影响,仅在机动盾牌持续时间内生效;机动盾牌退场后,涤火杰西卡转回原方向",
// "本天赋的防御增加效果无视孤立"; 蓄能释放 备注 "受到任何伤害时均可触发(包括无法触发受击回复的伤害)"; S2 备注 "此技能的攻击范围
// 不受“攻击距离”属性影响"; S3 备注 "仅技能持续期间可触发发射炮弹的行为,技能期间多次部署机动盾牌可触发多次发射炮弹行为(同一时间内最多
// 生效1次)", "发射炮弹行为视为不消耗弹药的普通攻击,受缴械类效果影响时该行为延迟至此类效果消失后进行", "炮弹飞行终点为自身当前攻击
// 范围内正前方最远一格中点,碰撞半径0.5,爆炸半径1.7"; SPT-Y 修正 "攻击距离+1(部分技能不受此影响)"); PRTS 机动盾牌 (备注 "持有
// 禁疗", "无视野范围", "退场时返还1个可部署的机动盾牌"; 嘲讽等级 1, 阻挡数 2); the client's battle data (charpack char_1034_jesca2:
// Talents/1/DefAura range b-1 ignoring 孤立, Talents/2 x-5, die_to_kill_token, charge_token[born]; token prefab
// token_10032_jesca2_jckshd: jckshd_t = 禁疗 (abnormal flag 7) + taunt for `duration` s, HostMark b-1, charge_token[finish];
// skill prefabs skchr_jesca2_1 JckshdBuff / skchr_jesca2_3 JckshdBuffS3 (auras on her x-5); projectile
// projectile_chr_jesca2_s3_bomb (speed 5, first enemy it passes, ground and air) → _explosion (every enemy in range, air
// too); buff_template_data jesca2_jckshd_dir_set / dir_ctrl, jesca2_t_2[sp], jesca2_e_003[t_sp] / [extend], jesca2_s_3[bomb]).
// - Trait (哨戒铁卫) "能够阻挡三个敌人,可以进行远程攻击": the profession default — ranged physical, blocks 3, hits air units
// (PRTS 分支特性信息 哨戒铁卫 "可对空"), ground enemies target her normally.
// - Module SPT-X “私家英雄”: trait addition "攻击范围内敌人的隐匿效果失效" (jesca2_e_002_t: 隐匿 immunity on the enemies of her
// range): reveal (stages 1 and 3). Stage 3 changes T1: the shield lasts 60 s and is stronger (the token's module
// attributes and talent — data byModule). Stats: the module attributes.
// - Module SPT-Y 未曾风化: "攻击距离+1(部分技能不受此影响)" — a permanent forward range extension (jesca2_e_003[extend]); S2's
// 2-5 ignores it (its 备注), S3 grows on it. Stage 3 changes T2 (below). Stats: the module attributes.
// - T1 灵活应变: her 机动盾牌 is a hand piece the player places next to her ("只能放置于涤火杰西卡四周" — the prep's placement
// rule; a piece on any other tile gets no turn, mark or aura here) and deploys with the board. Each deployment: it lasts
// the token talent's `duration` (50 s; SPT-X stage 3: 60 s), holds 禁疗 and taunt level `taunt_level` (its hidden talent:
// "更容易受到攻击"), makes no attack and blocks 2 (its stats); she turns to face it (her range turns with her) and turns
// back to the direction she had when it came, once it leaves — whatever the reason, stunned or not (PRTS 备注; the
// client's dir_set / dir_ctrl[reset]); while it stands next to her, she and the unit on the tile behind her (range b-1,
// any ally unit, 孤立 ones too) DEF +def. It leaves when she leaves (die_to_kill_token: killed), and comes back on its tile
// its respawnTime (30 s) after it left, paying its cost (5 DP), only while she stands — her (re)deployment makes a waiting
// one ready at once (charge_token[born]) [ASSUMED: the 卫戍 auto redeploy of a placed summon, as 鸿雪's 打字机 / 谬因's 中继器].
// - T2 蓄能释放: every damage instance the shield takes — any damage, 无来源 / DoT included, not a 流失 or an element 损伤 —
// while it stands on her x-5: `prob` to give her `sp` SP (none while a skill of hers runs: AK). SPT-Y stage 3: prob 0.6 and
// each success also cuts the shield's own next redeploy time by respawn_time (1 s), at most respawn_time_max (14 s) per
// shield deployment (jesca2_e_003[t_sp]: the card buff lasts until the next spawn; AlwaysNext — the cut comes with the
// roll even when no SP can be gained).
// - S1 坚守阵线 (AUTO, 持续时间无限 — `toggle`): ATK +atk, DEF +def; every shield on her x-5 DEF +def while it runs (JckshdBuff
// aura) and +duration s of life once per shield deployment (jckshd_t, the overriding lifetime buff of the aura). An AUTO
// skill acting on herself and her shield fires at full SP (`trigger: 'SP_FULL'`, the owner's AUTO rule, kits/README.md
// checklist 5; skchr_jesca2_1 `_allowNoTarget`).
// - S2 掩蔽护卫 (MANUAL, 15 s, data ACTIVE_RANGE on its 2-5 — the 重装 exception of 742f478): range 2-5 (not grown by
// 攻击距离), ATK +atk, attack interval base_attack_time s (−0.9 on 1.2 s), 75 % physical and arts dodge (the `evade`
// template, prob).
// - S3 饱和迸射 (MANUAL, 20 发弹药, data ACTIVE_RANGE on its 2-2 + 1): 攻击距离 +ability_range_forward_extend, attack interval
// +base_attack_time (0.6 s), ATK +atk, DEF +jesca2_s_3[def].def; her shields on x-5 DEF +jesca2_s_3_token[def].def. While
// a shield stands next to her (the mark: at the cast, or a shield deployed during the skill — at most one pending), she
// fires a shell as soon as she is not stunned / frozen / asleep / disarmed: an attack that spends no bullet and restarts
// her attack timer [ASSUMED: the template clears and restarts the attack cooldown]; it flies at BOMB_SPEED from her
// position to the centre of the farthest tile straight ahead of her current range, hits the first enemy it touches
// (BOMB_HIT_RADIUS, ground or air) or the end point, and explodes: every selectable enemy within
// attack@extrabomb.projectile_range takes attack@extrabomb.atk_scale × her ATK at the launch, physical (普通伤害, dodgeable),
// and is stunned attack@extrabomb.stun s (not when it dodged the hit [ASSUMED]). The skill's end drops a pending shell.
import { num, talentBb, traitBb, moduleOn, skillRec, batMod, up, giveSp, installReveal } from '../shared/tier1.js';
import { offsetTile, dirFromDelta, dirVec } from '../../../dir.js';
import { isHpLoss } from '../../../damage.js';
const S1 = 'skchr_jesca2_1';
const S2 = 'skchr_jesca2_2';
const S3 = 'skchr_jesca2_3';
/** Her summon 机动盾牌 (talent 灵活应变). */
export const SHIELD = 'token_10032_jesca2_jckshd';
/** S3 shell (PRTS 备注 / projectile_chr_jesca2_s3_bomb): flight speed (tiles / s) and collision radius. */
const BOMB_SPEED = 5;
const BOMB_HIT_RADIUS = 0.5;
/** The shell's explosion radius when the blackboard lacks it (PRTS "爆炸半径1.7"). */
const BOMB_BLAST_FALLBACK = 1.7;
/** Aura refresh period and buff life (s): the kits' aura convention (a unit leaving the tile loses it within one refresh). */
const AURA = 0.2;
const AURA_DUR = 0.25;
/** How often a waiting shield tries to come back once ready (her on the field, its tile free, DP paid). */
const RETRY = 0.1;
const DEF_KEY = 'talent:jesca2:def';
const SHIELD_SKILL_KEY = 'skill:jesca2:shield';
const SHIELD_ABNORMAL_KEY = 'jesca2:shield:abnormal';
const TAG_BOMB = 'jesca2:bomb';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A 机动盾牌 of `unit`. */
const isShieldOf = (t, unit) => !!t && t.kind === 'token' && t.defId === SHIELD && t.ownerUnit === unit;
const shieldsOf = (battle, unit) => battle.allyUnits.filter((t) => isShieldOf(t, unit));
/** `t` stands on one of the four tiles next to `unit` (the placement rule "四周"; her x-5 auras reach it there). */
const besideOf = (unit, t) => Math.abs(t.tileR - unit.tileR) + Math.abs(t.tileC - unit.tileC) === 1;
/** The shield of hers standing next to her (the mark jesca2_jckshd_s_3[mark]: its HostMark b-1 holds her), or null. */
const markOf = (battle, unit) => shieldsOf(battle, unit).find((t) => up(t) && besideOf(unit, t)) ?? null;
const skillOn = (unit, id) => !!(unit.skill && unit.skill.active && unit.skill.id === id);
/** Face `dir` (the range turns with her: Battle.refreshRange; a SkillRuntime trigger grid follows `unit.dir`). */
function turnTo(battle, unit, dir) {
if (unit.dir === dir) return;
unit.dir = dir;
if (unit.alive && unit.deployed) battle.refreshRange(unit);
}
/** The farthest local column straight ahead (local row 0) of a relative grid (her current range). */
function farthestAhead(grid) {
let n = 0;
for (const p of grid ?? []) if (Array.isArray(p) && p[0] === 0 && p[1] > n) n = p[1];
return n;
}
/**
* 灵活应变's piece: the shield's kit (lifetime = its talent's `duration`, 禁疗 + taunt, her turn, its redeploy). `owner` =
* 涤火杰西卡, `s1Ext` = the life her S1 adds (bb.duration).
*/
function shieldKit(owner, { s1Ext }) {
return {
skill: null,
trait: { noAttack: true }, // "无视野范围" (and ATK 0)
talents: [],
install(battle, t) {
const tal = (t.def?.talents ?? []).find((x) => x && x.bb && x.bb.duration != null)?.bb ?? {};
const life = num(tal.duration, 50);
const taunt = num(tal.taunt_level, 1);
// 持有禁疗 (PRTS; jckshd_t abnormal flag 7 — the data's `abnormal` lacks it [ASSUMED: given here]) and "更容易受到攻击"
battle.addBuff(t, { key: SHIELD_ABNORMAL_KEY, flags: { noHeal: true }, mods: { taunt }, persist: true, allowDead: true });
battle.on('deploy', (c) => {
if (c.unit !== t) return;
t.mem.lifeEnd = battle.time + life;
t.mem.s1Done = false;
t.mem.respawnCut = 0;
// she turns to face it; it faces away from her (dir_set: AssignRelativeDirectionToBB, SetBodyDirection)
if (up(owner) && besideOf(owner, t)) {
const dir = dirFromDelta(t.tileR - owner.tileR, t.tileC - owner.tileC, owner.dir);
if (owner.mem.jescaOldDir == null) owner.mem.jescaOldDir = owner.dir;
t.dir = dir;
turnTo(battle, owner, dir);
battle.fx('buff', { x: owner.x, y: owner.y, id: owner.id, kind: 'turn' });
// S3: a shield taking the field during the skill fires a shell (at most one pending)
if (skillOn(owner, S3)) owner.mem.jescaBomb = true;
}
}, { owner: t });
battle.on('tick', () => {
if (!up(t)) return;
// S1's aura gives a shield on her x-5 +s1Ext s of life, once per deployment
if (!t.mem.s1Done && s1Ext > 0 && skillOn(owner, S1) && up(owner) && besideOf(owner, t)) {
t.mem.s1Done = true;
t.mem.lifeEnd += s1Ext;
}
if (battle.time + 1e-9 >= t.mem.lifeEnd) battle.retreat(t, { reason: 'expired', permanent: true });
}, { owner: t });
battle.on('death', (c) => {
if (c.unit !== t || battle.finished) return;
// dir_ctrl[reset]: she turns back to the direction she had when it came — stunned or not, on the field or not
if (owner.mem.jescaOldDir != null) {
turnTo(battle, owner, owner.mem.jescaOldDir);
owner.mem.jescaOldDir = null;
}
// the piece stays (退场时返还1个可部署的机动盾牌): back on its tile once ready, while she stands
t.removed = false;
t.mem.readyAt = battle.time + Math.max(0, num(t.base.respawnTime) - num(t.mem.respawnCut));
t.mem.respawnCut = 0;
if (t.mem.retry) return;
t.mem.retry = battle.every(RETRY, (b, sched) => {
const stop = () => { sched.cancel(); t.mem.retry = null; };
if (t.alive || t.removed || b.finished) { stop(); return; }
if (!up(owner) || b.time + 1e-9 < t.mem.readyAt) return;
if (b.redeploy(t, { free: false })) stop();
}, { owner: t });
}, { owner: t, priority: -10 });
},
};
}
/**
* S3's shell: from her tile straight ahead to the centre of the farthest tile of her current range, at BOMB_SPEED; the
* first selectable enemy within BOMB_HIT_RADIUS (ground or air) or the end point sets it off.
*/
function fireBomb(battle, unit, b3) {
const [fr, fc] = dirVec(unit.dir);
const k = farthestAhead(unit.liveRangeGrid);
const bomb = {
x: unit.x, y: unit.y, tx: unit.tileC + fc * k, ty: unit.tileR + fr * k,
atk: unit.s.atk, // the launch's ATK (_cachedAtkKey)
scale: num(b3['attack@extrabomb.atk_scale'], 2.5),
radius: num(b3['attack@extrabomb.projectile_range'], BOMB_BLAST_FALLBACK),
stun: num(b3['attack@extrabomb.stun'], 5),
};
(unit.mem.jescaShells ??= []).push(bomb);
unit.atkCd = unit.s.interval; // the shell is her attack: the timer restarts [ASSUMED]
battle.fx('bombardShell', { x: bomb.tx, y: bomb.ty, id: unit.id, r: bomb.radius, t: Math.hypot(bomb.tx - bomb.x, bomb.ty - bomb.y) / BOMB_SPEED });
}
/** Move the shells one tick; a shell touching an enemy or reaching its end point explodes. */
function flyShells(battle, unit, dt) {
const list = unit.mem.jescaShells;
if (!list || !list.length) return;
const keep = [];
for (const s of list) {
const dx = s.tx - s.x, dy = s.ty - s.y;
const d = Math.hypot(dx, dy);
const step = BOMB_SPEED * dt;
let hit = battle.foesInRadius(s.x, s.y, BOMB_HIT_RADIUS).length > 0;
if (!hit) {
if (d <= step) { s.x = s.tx; s.y = s.ty; hit = true; } else { s.x += (dx / d) * step; s.y += (dy / d) * step; }
if (!hit && battle.foesInRadius(s.x, s.y, BOMB_HIT_RADIUS).length > 0) hit = true;
}
if (hit) explode(battle, unit, s); else keep.push(s);
}
unit.mem.jescaShells = keep;
}
function explode(battle, unit, s) {
battle.fx('bombard', { x: s.x, y: s.y, id: unit.id, r: s.radius });
const dodged = (unit.mem.jescaDodged = new Set());
for (const e of battle.foesInRadius(s.x, s.y, s.radius)) {
if (!e.alive) continue;
battle.dealDamage(unit, e, { amount: s.atk * s.scale, type: 'phys', isSkill: true, tags: ['skill', TAG_BOMB] });
if (e.alive && !dodged.has(e)) battle.applyStatus(e, 'stun', { duration: s.stun, source: unit });
}
unit.mem.jescaDodged = null;
}
export default {
char_1034_jesca2: (bb, chess) => {
const t0 = talentBb(chess, 0); // 灵活应变: def (+15 %)
const t1 = talentBb(chess, 1); // 蓄能释放: sp, prob (SPT-Y stage 3: + respawn_time, respawn_time_max)
const tb = traitBb(chess); // SPT-Y: ability_range_forward_extend
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
const mod = moduleOn(chess) ? chess.module.id : null;
const shieldDef = (unit) => (skillOn(unit, S1) ? num(b1.def) : 0) + (skillOn(unit, S3) ? num(b3['jesca2_s_3_token[def].def']) : 0);
return {
skills: {
[S1]: { kind: 'toggle', trigger: 'SP_FULL', mods: { atkPct: num(b1.atk), defPct: num(b1.def) } },
[S2]: {
kind: 'duration',
// 2-5, never grown by 攻击距离 (PRTS 备注)
targeting: { rangeGrid: s2?.rangeGrid ?? null, noRangeExtend: true },
mods: { atkPct: num(b2.atk), batPct: batMod(b2.base_attack_time, chess), dodgePhys: num(b2.prob), dodgeArts: num(b2.prob) },
},
[S3]: {
kind: 'ammo', ammo: num(b3['attack@trigger_time'], 20),
targeting: { rangeExtend: num(b3.ability_range_forward_extend, 1) },
mods: { atkPct: num(b3.atk), defPct: num(b3['jesca2_s_3[def].def']), batPct: batMod(b3.base_attack_time, chess) },
onStart({ battle, unit }) { unit.mem.jescaBomb = !!markOf(battle, unit); },
onEnd({ unit }) { unit.mem.jescaBomb = false; },
},
},
talents: [
{ install(battle, unit) { // 灵活应变: her shield pieces run the shield's kit (set up before the battle starts)
const kit = () => shieldKit(unit, { s1Ext: num(b1.duration) });
for (const t of battle.allyUnits) {
if (!isShieldOf(t, unit) || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t); // a piece set up before her: drop its generic kit's hooks
battle._setupUnit(t, kit());
}
// die_to_kill_token: her shields fall when she leaves; charge_token[born]: her (re)deployment readies a waiting one
battle.on('death', (c) => {
if (c.unit !== unit) return;
for (const t of shieldsOf(battle, unit)) if (t.alive) battle.kill(t, null);
}, { owner: unit });
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
for (const t of shieldsOf(battle, unit)) if (!t.alive && !t.removed) t.mem.readyAt = battle.time;
}, { owner: unit });
// she and the unit on the tile behind her (b-1, 孤立 ones too: ignoreAllyTargetFree) DEF +def while her shield
// stands next to her
const d = num(t0.def);
if (!(d > 0)) return;
const aura = () => {
if (!up(unit) || !markOf(battle, unit)) return;
const [br, bc] = offsetTile(unit.tileR, unit.tileC, 0, -1, unit.dir);
for (const a of [unit, battle.unitAt(br, bc)]) {
if (!a || a.side !== 'ally' || a.kind === 'device' || !a.alive) continue;
const cur = a.findBuff(DEF_KEY);
if (cur && cur.source !== unit && (cur.data?.v ?? 0) > d && cur.timeLeft > 0.05) continue;
battle.addBuff(a, { key: DEF_KEY, duration: AURA_DUR, mods: { defPct: d }, source: unit, data: { v: d }, tags: ['talent', 'aura'] });
}
};
battle.every(AURA, aura, { owner: unit, immediate: true });
battle.on('deploy', (c) => { if (isShieldOf(c.unit, unit)) aura(); }, { owner: unit });
} },
{ install(battle, unit) { // 蓄能释放: the shield hurt ⇒ prob: +sp SP to her (SPT-Y stage 3: its next redeploy −1 s, ≤ 14 s)
const prob = num(t1.prob), sp = num(t1.sp);
const cut = num(t1.respawn_time), cutMax = num(t1.respawn_time_max);
if (!(prob > 0)) return;
battle.on('damaged', (c) => {
const t = c.target;
if (!isShieldOf(t, unit) || !up(unit) || !besideOf(unit, t)) return;
if (!c.dmg || isHpLoss(c.dmg) || c.type === 'element') return;
if (!battle.rng.chance(prob)) return;
giveSp(unit, sp, 'talent');
if (cut > 0 && cutMax > 0) t.mem.respawnCut = Math.min(cutMax, num(t.mem.respawnCut) + Math.abs(cut));
}, { owner: unit });
} },
],
install(battle, unit) {
// SPT-X “私家英雄”: 攻击范围内敌人的隐匿效果失效
if (mod === 'uniequip_002_jesca2') installReveal(battle, unit);
// SPT-Y 未曾风化: 攻击距离+1 — a permanent extension (her initial range too); S2's 2-5 ignores it (noRangeExtend)
const ext = num(tb.ability_range_forward_extend);
if (ext > 0) battle.addBuff(unit, { key: 'module:jesca2:range', mods: { rangeExtend: ext }, persist: true, allowDead: true });
// S1 / S3: her shields on x-5 DEF +def while the skill runs (JckshdBuff / JckshdBuffS3 auras)
battle.on('tick', (c) => {
for (const t of shieldsOf(battle, unit)) {
if (!t.alive) continue;
const want = up(unit) && besideOf(unit, t) ? shieldDef(unit) : 0;
const has = t.findBuff(SHIELD_SKILL_KEY);
if (want > 0 && (!has || has.mods?.defPct !== want)) {
battle.addBuff(t, { key: SHIELD_SKILL_KEY, mods: { defPct: want }, source: unit, tags: ['skill'] });
} else if (!(want > 0) && has) battle.removeBuff(t, SHIELD_SKILL_KEY);
}
// S3's shell: fired as soon as she can attack (not stunned / frozen / asleep, not disarmed), shells in flight move
if (unit.mem.jescaBomb && skillOn(unit, S3) && unit.canAct && !unit.s.flags.disarm) {
unit.mem.jescaBomb = false;
fireBomb(battle, unit, b3);
}
flyShells(battle, unit, c.dt ?? battle.dt);
}, { owner: unit });
// a shell's explosion that is dodged carries no stun [ASSUMED]
battle.on('dodge', (c) => {
if (c.source === unit && unit.mem.jescaDodged && c.dmg?.tags?.includes(TAG_BOMB)) unit.mem.jescaDodged.add(c.target);
}, { owner: unit });
},
};
},
};