feedback5: merge O24 — 自选 kits for 凯尔希·思衡托 (with her 战术锚点) and Mon3tr (with her 重构体); engine (additive): mod blockRadiusScale widens the air-block radius (0 for every other unit); golden: only diy moved — all 71 自选 operators now have kits

This commit is contained in:
sganggs committed 2026-10-06 14:18:10 +08:00
commit a009d77d71
11 files changed
+1695 -177

No files matched your search

+2
View File
@@ -148,6 +148,8 @@ export const OPERATOR_KIT_FILES = Object.freeze([
'op-ironmn.js',
'op-nasti.js',
'op-necras.js',
'op-kalts2.js',
'op-monstr.js',
]);
/**
+359
View File
@@ -0,0 +1,359 @@
// server/sim/content/kits/ops/op-kalts2.js — 凯尔希·思衡托 (char_1052_kalts2) 自选 operator kit: 6★ 守望者 (医疗), an owned-6★
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents and the trait at every form (she has no module),
// and the kit of her summon 战术锚点 (token_10068_kalts2_mtship). Kit contract and the 自选 rules: ../README.md ("How to add
// an operator (自选)").
//
// Forms (data/backups.json units.char_1052_kalts2): normal = E2 Lv1, skills at rank 4; elite = E2 Lv60, rank 7 (no module,
// so tiers 5 and 6 field the same elite) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / token_table (zh_CN, as built into backups.json); PRTS 凯尔希·思衡托 (T1 备注
// "治疗效果无视禁疗…除非该目标为Mon3tr(凯尔希的召唤物)", "阻挡范围加成为提升自身的“阻挡半径倍率”属性"; T2 备注 "无视孤立",
// "“进入”的方式包括…部署时攻击范围碰撞到单位、单位部署在…攻击范围内…2技能攻击范围扩大时额外区域碰撞到的单位…3技能移动前发射的弹道
// 的碰撞", "来自此天赋的护盾最高1层"; S1 备注 "仅判断目标是否持有对地规避"; S2 备注 "伤害与治疗溅射半径均为1.5,可对空;伤害为真实普
// 通伤害", the enemy / ally target order and effect order; S3 备注: the operator choice, 【待投放】, the 3.5-speed 2.2-radius flight,
// its 无敌 / 强制缴械 / 不可阻挡, the paused skill time, the landing facing); PRTS 战术锚点 ("常态持有无敌"); PRTS 卫戍协议/帮助
// (§战斗部署 placed summons, §作战阶段 auto redeploy "满足再部署条件时…自动部署至该位置"); gamedata_const ba.liftoff (起飞
// "阻挡模式变为空中阻挡…阻挡半径变为0.8944"); PRTS 游戏数据基础 §阻挡半径 ("飞行阻挡半径 = 0.8944 × 阻挡半径倍率"); the client's battle
// data: charpack char_1052_kalts2 (attack triggers `_ignoreHealFree` + `_mon3terId` token_10002_kalts_mon3tr; talent "1":
// kalts2_t_1 [abnormal 35] + kalts2_t_1[buff] (MAX_HP / DEF MULTIPLIER, BLOCK_CNT ADDITION) + block_radius_scaler[inf]; talent
// "2" aura kalts2_t_2 (professionMask 639 = operators, ignoreTargetFree, `_removeBuffWhenTargetLeave`); mode s1 Aura
// block_radius_scaler[inf] on allies holding abnormal 35, `_selfOption` 2, `_interval` 0.3; mode s2 AttackEnemy (PURE,
// activeBuff sluggish, targetMotion 3) / HealAlly abilities, 10-count; mode s3 trigger `_maxNum` 2; StartTransporting
// (projectile_chr_kalts2_s3_2, activeBuff kalts2_t_2); S3OperatorSelector (`_maxNum` 2, `_checkPlayerSideSameWithOwner`);
// Invincible abnormal [5, 18, 3]); buff_template_data (kalts2_t_1, kalts2_t_2, kalts2_t_2[hp_recover] HP_RECOVERY_PER_SEC
// ×rhodes_bonus for group rhodes, kalts2_t_2[shield] BlockDamage once, INFINITY; kalts2_s_3 / [core] / [token_ctrl]
// RechargeToken cnt 1 then WithdrawTokens + cnt_minus 0; kalts2_s_3[on_token_born], kalts2_s3_trigger_transporting
// (every 0.1 s), kalts2_s_3[withdraw] (0.6 s, cost_scale 0, Withdraw); token_mtship_t_1 [abnormal 5] → on_token_born on its
// host); the projectile prefabs (projectile_chr_kalts2_s2_dmg / _heal speed 12 + the 1.5 sub-ranges; _s3_2 speed 3.5).
// - Trait (守望者) "恢复友方单位生命,并且可以起飞": the MEDIC profile (one heal per attack on the lowest-HP-ratio injured ally of
// her y-6). Her heals and their selection pass 凯尔希's Mon3tr (token_10002_kalts_mon3tr) through its 禁疗 (`healThrough`);
// other 禁疗 units stay out of the selection, and "治疗效果无视禁疗" changes nothing else here (no heal of hers reaches an
// unselected 禁疗 unit). 起飞 itself is T1's.
// - T1 遗尘守望: max HP / DEF +max_hp / +def (0.25), block +block_cnt, 阻挡半径倍率 +block_radius_scale (mod
// `blockRadiusScale`: her air-block radius 0.8944 × 1.23, Battle._checkBlock), and 起飞 from her deployment (flags
// `liftoff` + `blockFly`: no ground enemy selects or is blocked by her, she blocks flyers — kalts2_t_1 ChangeCharBlockMode
// FLY, from a ranged tile too). The 生命修复单元 is her model.
// - T2 医者丰碑: every other friendly OPERATOR (no summon: professionMask 639; 孤立 ones too) that enters her attack range —
// her current one, S2's y-11 included — gets 1 护盾 layer (one damage instance blocked whole, no expiry, at most one from
// this talent) and 生命回复速度 +hp_recovery_per_sec (×rhodes_bonus for a 罗德岛 operator, nationId rhodes) for buff_duration
// s (an hpRegen buff: HP_RECOVERY_PER_SEC, never a heal — kits/README.md item 11; 不可叠加: the strongest, a new entry refreshes
// it). An entry = not in her range on the tick before: her (re)deployment and her S3 landing (everyone in range), a unit
// deployed / redeployed / moved into it, the range growing; S3's flight touches more (below).
// - S1 应急肃正防线 (MANUAL, data DEFAULT — a heal skill: cast as she is about to heal): ATK +atk, ASPD +attack_speed for its
// 35 s; every OTHER friendly operator of the field holding 起飞 (flag `liftoff`: 蒂比's skills, 司霆惊蛰) gets 阻挡半径倍率
// +attack@block_radius_scale while it runs (refreshed every 0.3 s; 同名效果取最高 — nothing on a unit whose own 倍率 is at
// least as high) [ASSUMED: the whole field — the aura has no range selector and the text says 所有].
// - S2 保护性拒止 (MANUAL, data ACTIVE_RANGE on its y-11 — the owner's rule for a heal skill: an injured ally inside the y-11;
// 10 发弹药): range y-11, ATK +atk. Each attack fires a 医疗单元: at an enemy she can select (air units too; the ones she
// blocks first, then the usual order) — on impact every enemy within 1.5 of it takes attack@atk_scale × ATK true damage
// (普通伤害: an attack hit, not 溅射) and attack@sluggish s 停顿, then every ally within 1.5 (summons too: the heal range
// selector has no profession mask; no 孤立 one) is healed for attack@heal_scale × ATK; with no enemy to select, at the
// lowest-HP-ratio injured ally of the y-11 — heal first, then the damage, no 停顿. The shot flies at the engine's orb speed
// (10 tiles/s; the official 12) [ASSUMED] and fizzles when its enemy dies on the way [ASSUMED]. "可以随时停止技能": the
// automatic battle never stops it.
// - S3 破梏重生 (MANUAL, data DEFAULT, 35 s): ATK +atk, base attack time base_attack_time (a flat −1.55 s on her 2.85 s), one
// more heal target (2). "立刻获得战术锚点": one anchor in stock (content/tokens.js releaseSkillSummon — the placed piece
// deploys on its tile if it is off the field and ready). With her anchor standing (one deployed while she stood:
// kalts2_s_3[on_token_born] on her, lost when she leaves — `k2link`), 0.1 s after the cast (kalts2_s3_trigger_transporting),
// or at once when an anchor deploys while S3 runs:
// ① up to 2 other operators of her player inside her attack range — the latest deployed first (PRTS "部署时间点最晚的";
// the battle-start board deploys in one tick here, so its order — `deploySeq` — stands for the official one-by-one
// deployment times) — are withdrawn 0.6 s later into 【待投放】 (部署费用 ×0, 再部署时间 0, only inside her
// pre-move range): the automatic battle redeploys them at once on their tiles, free — a fresh deployment (full HP, initial
// SP) [ASSUMED: the 卫戍 auto redeploy of a 待投放 operator, PRTS 卫戍协议/帮助 "满足再部署条件时…自动部署至该位置"];
// ② she leaves the field visually (hidden: 无敌, 缴械, 不可阻挡, no status) and flies from her tile to the anchor at 3.5
// tiles/s, her skill time paused; every operator within 2.2 of the flying point gets T2's entry (once per flight), and
// her range stays on her tile until she lands (the 待投放 operators redeployed there enter it);
// ③ on arrival the anchor is withdrawn (her old instance's token_ctrl ends: WithdrawTokens, stock 0) and she 【移动】s
// (Battle.moveRedeploy) onto its tile, facing the anchor's direction; the skill goes on with its remaining time. Should
// the move be refused, she comes back on her own tile [ASSUMED; officially she is knocked out].
// At the skill's end any anchor of hers on the field is withdrawn and the stock emptied (token_ctrl). The flight's begin
// clip and its 生命回复速度 ×0 are not modelled [ASSUMED].
// - 战术锚点 (token_10068_kalts2_mtship, placed in prep — "仅可以部署在凯尔希·思衡托攻击范围外的远程位" is the match's
// placement rule): 无敌 and never targeted ("不会受到攻击"), no attack, blocks nothing; its range (hers) is only shown.
// A placed piece is docked by content/tokens.js (a skill's summon: deployed for free at the battle start, then only when
// her S3 gives one and its 70 s redeploy time has passed, on its own tile).
import { num, talentBb, skillRec, batMod, up } from '../shared/tier1.js';
import { releaseSkillSummon } from '../../tokens.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_kalts2_1';
const S2 = 'skchr_kalts2_2';
const S3 = 'skchr_kalts2_3';
/** Her summon 战术锚点 (S3). */
export const ANCHOR = 'token_10068_kalts2_mtship';
/** 凯尔希's Mon3tr: her heals and their selection pass its 禁疗 (charpack `_mon3terId`). */
const KALTS_MON3TR = 'token_10002_kalts_mon3tr';
/** S2's damage / heal radius around the struck unit (PRTS S2 备注 "伤害与治疗溅射半径均为1.5"). */
const S2_RADIUS = 1.5;
/** S1's aura refresh (charpack mode s1 Aura `_interval`). */
const S1_AURA_IV = 0.3;
/** S3: the first transport check after the cast (kalts2_s3_trigger_transporting triggerInterval). */
const TRANSPORT_CHECK = 0.1;
/** S3: the flight speed (projectile_chr_kalts2_s3_2 `_speed`) and its collision radius (PRTS S3 备注). */
const TRANSPORT_SPEED = 3.5;
const TRANSPORT_RADIUS = 2.2;
/** S3: the operators chosen are withdrawn this long after the flight starts (kalts2_s_3[withdraw] firstTriggerInterval). */
const WITHDRAW_DELAY = 0.6;
/** S3: at most this many operators (S3OperatorSelector `_maxNum`). */
const WITHDRAW_MAX = 2;
const T1_KEY = 'talent:kalts2:t1';
const T2_SHIELD = 'talent:kalts2:shield';
const T2_REGEN = 'talent:kalts2:regen';
const S1_KEY = 'skill:kalts2:blockRadius';
const ANCHOR_KEY = 'kalts2:anchor';
const TAG_S2 = 'kalts2:s2';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const skillOn = (unit, id) => !!(unit.skill && unit.skill.active && unit.skill.id === id);
const isAnchorOf = (t, unit) => !!t && t.kind === 'token' && t.defId === ANCHOR && t.ownerUnit === unit;
const anchorsOf = (battle, unit) => battle.allyUnits.filter((t) => isAnchorOf(t, unit));
const isRhodes = (a) => (a?.def?.raw?.nationId ?? a?.def?.nationId) === 'rhodes';
/** A friendly operator other than `unit` standing on the field. */
const otherOp = (a, unit) => a !== unit && a.kind === 'op' && up(a) && !a.hidden;
/** The anchor stock content/tokens.js releaseSkillSummon keeps on its owner. */
function setStock(unit, n) {
const s = unit.mem.summonStock || (unit.mem.summonStock = {});
s[ANCHOR] = n;
}
/** The 战术锚点's kit: 无敌, never targeted, no attack, blocks nothing. */
function anchorKit() {
return {
skill: null,
talents: [],
trait: { noAttack: true },
install(battle, t) {
battle.addBuff(t, { key: ANCHOR_KEY, flags: { invulnerable: true, untargetable: true, noBlock: true }, persist: true, allowDead: true });
},
};
}
/** T2's entry for operator `a`: 1 护盾 layer unless one from this talent is there, the 生命回复速度 buff (strongest). */
function enterT2(battle, unit, a, t1) {
if (!a.findBuff(T2_SHIELD)) battle.addBuff(a, { key: T2_SHIELD, shieldHits: 1, source: unit, tags: ['talent'] });
const v = num(t1.hp_recovery_per_sec) * (isRhodes(a) ? num(t1.rhodes_bonus, 1) : 1);
const d = num(t1.buff_duration);
if (v > 0 && d > 0) battle.applyStrongest(a, T2_REGEN, { duration: d, value: v, mods: (x) => ({ hpRegen: x }), source: unit });
battle.fx('shield', { x: a.x, y: a.y, id: a.id });
}
/**
* S2's 医疗单元 landing on `target` (an enemy, or an injured ally when no enemy could be selected): true damage + 停顿 on
* the enemies within 1.5, a heal on the allies within 1.5 — damage first on an enemy, heal first (and no 停顿) on an ally.
*/
function s2Burst(battle, unit, target, attackId, b2) {
const x = target.x, y = target.y;
const atk = unit.s.atk;
const dmgAmt = atk * num(b2['attack@atk_scale'], 1);
const healAmt = atk * num(b2['attack@heal_scale'], 1);
const slug = num(b2['attack@sluggish']);
const onEnemy = target.side === 'enemy';
const hurt = () => {
for (const e of battle.foesInRadius(x, y, S2_RADIUS, true)) {
if (!e.alive) continue;
battle.dealDamage(unit, e, { amount: dmgAmt, type: 'true', isAttack: true, isSkill: true, attackId, tags: [TAG_S2] });
if (onEnemy && slug > 0 && e.alive) battle.applyStatus(e, 'sluggish', { duration: slug, source: unit });
}
};
const mend = () => {
for (const a of battle.alliesInRadius(x, y, S2_RADIUS)) if (battle.allySelectable(a, unit)) battle.heal(unit, a, healAmt, { ignoreHealFree: true });
};
battle.fx('aoe', { x, y, radius: S2_RADIUS, id: unit.id, skill: TAG_S2 });
battle.fx('healAoe', { x, y, radius: S2_RADIUS, id: unit.id });
if (onEnemy) { hurt(); mend(); } else { mend(); hurt(); }
}
/**
* S2's HealAlly ability: with no enemy to select in her range (blocked ones included) she fires at the lowest-HP-ratio
* injured ally instead (an attack in every respect: Battle.forceAttack, one bullet). Returns false so the engine's attack
* loop makes no attack of its own then.
*/
function s2CanAttack(battle, unit) {
const prof = { canHitFly: true };
if (battle.enemiesInKeys(unit.rangeKeys, unit, prof).length || battle.blockedTargets(unit, prof).length) return true;
const ally = battle.injuredAlliesInKeys(unit.rangeKeys, unit)[0];
if (ally && battle.forceAttack(unit, [ally])) unit.atkCd = Math.max(unit.atkCd, unit.s.interval);
return false;
}
/** The flight's point at the current time. */
function flightPoint(battle, f) {
const k = f.T > 0 ? Math.min(1, Math.max(0, (battle.time - f.t0) / f.T)) : 1;
return { x: f.from.x + (f.to.x - f.from.x) * k, y: f.from.y + (f.to.y - f.from.y) * k };
}
/** The flight's collisions: T2's entry for every operator within 2.2 of its point (once per flight). */
function collide(battle, unit, f) {
const enter = unit.mem.kalts2Enter;
if (!enter) return;
const p = flightPoint(battle, f);
const r2 = TRANSPORT_RADIUS * TRANSPORT_RADIUS + 1e-9;
for (const a of battle.allyUnits) {
if (!otherOp(a, unit) || f.hit.has(a)) continue;
if ((a.x - p.x) * (a.x - p.x) + (a.y - p.y) * (a.y - p.y) > r2) continue;
f.hit.add(a);
enter(a);
}
}
/** S3 ③: the arrival — the anchor withdrawn, the stock emptied, a 【移动】 onto its tile facing its direction. */
function land(battle, unit) {
const f = unit.mem.k2flight;
if (!f) return;
unit.mem.k2flight = null;
unit.hidden = false;
if (!unit.alive || !unit.deployed) return;
collide(battle, unit, f);
unit.mem.k2landed = true;
setStock(unit, 0);
if (f.anchor.alive) battle.retreat(f.anchor, { reason: 'expired' });
const dir0 = unit.dir;
unit.dir = f.dir;
if (!battle.moveRedeploy(unit, f.r, f.c)) {
unit.dir = dir0;
battle.refreshRange(unit);
}
battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX: f.from.x, fromY: f.from.y });
}
/** S3 ①②: the 待投放 operators and the flight to `anchor`. */
function startTransport(battle, unit, anchor) {
const picks = battle.allyUnits
.filter((a) => otherOp(a, unit) && a.ownerId === unit.ownerId && unit.rangeKeySet?.has(a.tileR * COLS + a.tileC))
.sort((a, b) => b.deploySeq - a.deploySeq)
.slice(0, WITHDRAW_MAX);
for (const a of picks) {
const seq = a.deploySeq;
battle.after(WITHDRAW_DELAY, () => {
// kalts2_s_3[withdraw]: CheckUnitAlive(her) + CheckCharSkillAffecting(her), then Withdraw — and back at once
if (!up(a) || a.deploySeq !== seq || !unit.alive || !skillOn(unit, S3)) return;
battle.retreat(a, { reason: 'retreat' });
if (a.alive) return;
if (!battle.redeploy(a, { free: true })) a.respawnAt = Math.min(a.respawnAt, battle.time); // 再部署时间视为0
}, { owner: unit });
}
const from = { x: unit.x, y: unit.y }, to = { x: anchor.x, y: anchor.y };
const T = Math.hypot(to.x - from.x, to.y - from.y) / TRANSPORT_SPEED;
const f = { t0: battle.time, T, from, to, hit: new Set(), anchor, r: anchor.tileR, c: anchor.tileC, dir: anchor.dir };
unit.mem.k2flight = f;
battle.releaseBlocked(unit);
unit.hidden = true;
battle.fx('disappear', { x: unit.x, y: unit.y, id: unit.id });
collide(battle, unit, f);
battle.after(T, () => land(battle, unit), { owner: unit });
}
/** S3: start the transport when it can (S3 on, not done yet this cast, her linked anchor standing). */
function tryTransport(battle, unit) {
const m = unit.mem;
if (!skillOn(unit, S3) || m.k2flight || m.k2landed || !up(unit) || unit.hidden) return;
const a = m.k2link;
if (!a || !up(a) || !isAnchorOf(a, unit)) return;
startTransport(battle, unit, a);
}
export default {
char_1052_kalts2: (bb, chess) => {
const t0 = talentBb(chess, 0); // 遗尘守望: max_hp, def, block_cnt, block_radius_scale
const t1 = talentBb(chess, 1); // 医者丰碑: buff_duration, hp_recovery_per_sec, rhodes_bonus
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
const picked = chess?.skill?.skillId ?? null;
return {
trait: { healThrough: (healer, a) => !!a && a.kind === 'token' && a.defId === KALTS_MON3TR },
skills: {
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk), aspd: num(b1.attack_speed) } },
[S2]: {
kind: 'ammo',
ammo: Math.max(1, Math.floor(num(b2['attack@trigger_time'], 10))),
mods: { atkPct: num(b2.atk) },
targeting: s2?.rangeGrid ? { rangeGrid: s2.rangeGrid } : undefined,
attack: {
dmgType: 'true', canHitFly: true,
hitsFn: () => 0, // the damage is the 医疗单元's burst (s2Burst), not a hit on the target itself
canAttack: (battle, unit) => s2CanAttack(battle, unit),
onEachHit: (ctx) => { if (ctx.kind === 'main' && ctx.target) s2Burst(ctx.battle, ctx.unit, ctx.target, ctx.attackId, b2); },
},
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
targeting: { maxTargets: 2 }, // 额外治疗1个目标 (her s3 heal trigger `_maxNum` 2)
onStart({ battle, unit }) {
unit.mem.k2landed = false;
releaseSkillSummon(battle, unit, ANCHOR); // 立刻获得战术锚点 (a docked piece off the field deploys now when ready)
battle.after(TRANSPORT_CHECK, () => tryTransport(battle, unit), { owner: unit });
},
onTick({ unit, skill, dt }) { if (unit.mem.k2flight) skill.timeLeft += dt; }, // 技能剩余时间暂停消耗 while flying
onEnd({ battle, unit }) {
// kalts2_s_3[token_ctrl] ON_BUFF_FINISH: WithdrawTokens, RechargeToken cnt_minus 0 (refresh)
setStock(unit, 0);
for (const t of anchorsOf(battle, unit)) if (t.alive) battle.retreat(t, { reason: 'expired' });
if (unit.mem.k2flight) { unit.mem.k2flight = null; unit.hidden = false; }
},
},
},
talents: [
{ install(battle, unit) { // 遗尘守望
const mods = {};
for (const [k, v] of [['hpPct', num(t0.max_hp)], ['defPct', num(t0.def)], ['blockCnt', num(t0.block_cnt)], ['blockRadiusScale', num(t0.block_radius_scale)]]) if (v) mods[k] = v;
battle.addBuff(unit, { key: T1_KEY, mods, flags: { liftoff: true, blockFly: true }, persist: true, allowDead: true, tags: ['talent'] });
battle.on('deploy', (c) => { if (c.unit === unit && !c.move) battle.fx('takeoff', { x: unit.x, y: unit.y, id: unit.id }); }, { owner: unit });
} },
{ install(battle, unit) { // 医者丰碑: the operators entering her attack range
if (!(num(t1.buff_duration) > 0)) return;
const enter = (a) => enterT2(battle, unit, a, t1);
unit.mem.kalts2Enter = enter; // S3's flight collisions
let inside = new Set();
battle.on('deploy', (c) => { if (c.unit === unit) inside = new Set(); }, { owner: unit });
battle.on('tick', () => {
// (also while S3's flight hides her: her range stays on her tile until the landing — the 待投放 operators
// redeployed there enter it)
if (!up(unit) || !unit.rangeKeySet) return;
const now = new Set();
for (const a of battle.allyUnits) {
if (!otherOp(a, unit) || !unit.rangeKeySet.has(a.tileR * COLS + a.tileC)) continue;
now.add(a);
if (!inside.has(a)) enter(a);
}
inside = now;
}, { owner: unit });
} },
],
install(battle, unit) {
// her 战术锚点 pieces run the anchor kit (set up before content/tokens.js docks them: its install runs after the kits)
for (const t of battle.allyUnits) {
if (!isAnchorOf(t, unit) || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t);
battle._setupUnit(t, anchorKit());
}
// kalts2_s_3[on_token_born]: an anchor deployed while she stands links to her until it or she leaves; while S3 runs
// its deployment starts the transport at once
battle.on('deploy', (c) => {
if (!isAnchorOf(c.unit, unit)) return;
if (up(unit)) unit.mem.k2link = c.unit;
tryTransport(battle, unit);
}, { owner: unit });
battle.on('death', (c) => { if (c.unit === unit || c.unit === unit.mem.k2link) unit.mem.k2link = null; }, { owner: unit });
// S3's flight: its collisions every tick
battle.on('tick', () => { const f = unit.mem.k2flight; if (f && unit.alive) collide(battle, unit, f); }, { owner: unit });
// S1: every other 起飞 operator's 阻挡半径倍率 +attack@block_radius_scale while it runs (同名效果取最高)
const v1 = num(b1['attack@block_radius_scale']);
if (picked === S1 && v1 > 0) {
battle.every(S1_AURA_IV, () => {
if (!up(unit) || !skillOn(unit, S1)) return;
for (const a of battle.allyUnits) {
if (!otherOp(a, unit) || !a.s.flags.liftoff) continue;
const cur = a.findBuff(S1_KEY);
const own = (a.s.blockRadiusScale || 0) - (cur ? num(cur.mods?.blockRadiusScale) : 0);
if (own >= v1 - 1e-9 || (cur && cur.source !== unit && num(cur.data?.v) > v1 && cur.timeLeft > 0.05)) continue;
battle.addBuff(a, { key: S1_KEY, duration: S1_AURA_IV + 0.1, mods: { blockRadiusScale: v1 }, source: unit, data: { v: v1 }, tags: ['skill'] });
}
}, { owner: unit, immediate: true });
}
},
};
},
};
+337
View File
@@ -0,0 +1,337 @@
// server/sim/content/kits/ops/op-monstr.js — Mon3tr (char_4179_monstr) 自选 operator kit: 6★ 链愈师 (医疗), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (XAH-X 记忆存档) at every form, and the
// kit of her summon 重构体 (token_10050_monstr_prosts). Kit contract and the 自选 rules: ../README.md ("How to add an operator
// (自选)").
//
// Forms (data/backups.json units.char_4179_monstr): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7,
// XAH-X 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 Mon3tr
// (T1 备注 "治疗效果无视禁疗,但对象选取受禁疗制约,可以强行选择自身的重构体作为自身普通攻击的治疗目标", "攻击力加成效果同名效果取最高";
// T2 备注 "该效果重复触发刷新持续时间,不会因自身退场而消失"; S1 备注 "技能范围与技能期间治疗范围无关"; S2 备注 "可选择满生命值的重构体
// 进行治疗;重构体受到Mon3tr普通攻击治疗的0.6s后,Mon3tr再次以重构体为目标进行一次跳跃治疗", "仅技能期间治疗触发的第二天赋效果提升;
// …叠加时取最高"; S3 备注: the start (撤退重构体, 【移动】, the 路标形态 marker), 不死 ("期间受到致命伤害结算后立即结束技能;技能结束后
// 返回…以不高于1点的生命值在场;因返回而部署时获得1s不死"), "期间对自身进行治疗时,治疗跳跃不会衰减且不占用跳跃数", the return); PRTS
// 重构体 (备注 "持有禁疗(可以被Mon3tr无视)、不可阻挡", "嘲讽等级-1", "Mon3tr因移动之外的原因退场时强制撤退场上的重构体", 通常形态
// "阻挡数强制归0,每0.1s流失8生命值…退场时返还1个可部署的重构体"); PRTS 卫戍协议/帮助 (§战斗部署 placed summons, §作战阶段
// "若召唤物在战斗期间退场,将在满足条件后立即原地再部署1个"); the client's battle data: charpack char_4179_monstr (attacks
// `_ignoreHealFree`; talent "1" monstr_t_1[born_charge] / [die_to_kil_token]; talent "2" monstr_t_2[atk_spd], scaled in the S2
// mode by `_applyTalentScale` on attack_speed; modes S3_Reborn (Combat / Attack: PURE, targetMotion 1, `_maxNum` 3
// `_limitedMaxTargetNumToBlockedCnt`; ExtraHeal projectile_chr_monstr_s3 on every attack; talents monstr[free_jump],
// monstr_s_3[bleeding] (damage_per_second × 0.1 every 0.1 s), [reborned_passive], [sync_hp]); `_config` mapping S3's atk /
// damage_per_second / max_hp / block_cnt / base_attack_time and T1's attack@chain.extra_cnt); the skill prefabs (skchr_monstr_1
// next heal; skchr_monstr_2 its attack's activeBuff monstr_s_2[proj] lifeTime 0.6 → projectile_chr_monstr_s2_extra from the
// reconstruct; skchr_monstr_3 `_onlyAvailableWhenTokenValid`, the Respawn / RespawnEnd abilities, monstr_s_3[end_undeadable_
// restore] 1 s); the projectile prefabs (every heal projectile: `_atkScale` 0.75 = chain.atk_scale, the jump Selector
// `_rangeId` x-4 `_maxTarget` 3 lowest HP ratio `_useAdditionalTargetCount`, `_freeJumpValidator` monstr[free_jump]); the
// reconstruct's prefab (monstr[block_free] abnormal [3, 7] + taunt_level −1; prosts[atk_up] aura from the host's `atk`;
// prosts_t_1[born_charge] BLOCK_CNT ×0, interval 0.1, firstTriggerInterval 1, FixedValueDamage PURE `damage` on itself);
// buff_template_data (monstr_t_2[atk_spd] ON_OUTPUT_MODIFIER IsHeal; monstr_s_3[undead] ON_BEFORE_TRY_SET_HP_ZERO).
// - Trait (链愈师) "恢复友方单位生命,且会在3个友方单位间跳跃,每次跳跃治疗量降低25%" — the chain is this kit's (the profession's
// 2.5-tile chain is replaced: heal mode single + `chainFrom`), as her heal projectiles have it: after the heal on the main
// target the heal jumps to the lowest-HP-ratio injured ally of the 3×3 around the last one healed (x-4), each jump ×
// attack@chain.atk_scale (0.75; XAH-X 0.85), up to attack@chain.max_target units healed (3). A 重构体 (and she herself while S3
// runs: monstr[free_jump]) is a free link: healing it uses no target of the count and the next jump does not decay (XAH-X
// stage 3: the next attack@chain.extra_cnt jumps after that neither). Her heals pass 禁疗 ("治疗效果无视禁疗"); her selection
// takes her own 重构体 through its 禁疗 (`healThrough`), no other 禁疗 unit [ASSUMED: the jumps too].
// - Module XAH-X 记忆存档: the trait's 0.85 per jump (traitOverride); stage 3 changes T1 (below); ATK / DEF from the attributes.
// - T1 自我修复 (cnt 1): her 重构体 is a hand piece the player places (the talent's summon). "可以在攻击范围内的地面使用" — a
// ground tile of her attack range — is not in the token data (`ownerRange` false), so the match does not hold the piece to
// it and the battle takes it where it stands [open question]. Its kit: 禁疗 (only her heals reach it), blocks nothing, taunt
// −1, loses 8 HP every 0.1 s from 1 s after it lands (流失, Battle.loseHp — PRTS "每0.1s流失8生命值"; trait damage_per_second
// 80); every ally of its
// 3×3 but itself (no 孤立 one) ATK +atk (0.15; XAH-X stage 3 0.25; 同名效果取最高). Gone (destroyed or withdrawn) ⇒ back on its
// tile its redeploy time (15 s) after, paying its cost (3 DP), only while she stands — her deployment (not a 【移动】)
// readies a waiting one at once (monstr_t_1[born_charge]) [ASSUMED: the 卫戍 auto redeploy of a placed summon, as 凯尔希's
// Mon3tr]; withdrawn when she leaves the field (not on her S3 moves).
// - T2 战术协同: every heal of hers (chain jumps included) gives its target and herself ASPD +attack_speed for buff_duration s —
// ×talent_scale (S2) for a heal made while S2 runs; one effect per unit, the strongest (同名效果取最高 — a weaker one that
// outlasts it resumes afterwards: Battle.applyStrongest); a new one refreshes it.
// - S1 策略:超压链接 (AUTO, attack SP, data DEFAULT — a "next heal" waits for her heal, as 闪灵's S2): that heal is heal_scale ×
// ATK and heals up to chain.max_target (4) units.
// - S2 策略:超负荷 (MANUAL, data SKILL_RANGE on its y-2 — an enemy there; 30 s): her own 重构体 inside her range is her heal
// target, even at full HP (with nobody injured she heals it all the same: Battle.forceAttack); 0.6 s after each heal of her
// attack on it a second chain heal starts on it (her ATK × 1); T2 ×talent_scale.
// - S3 策略:熔毁 (MANUAL, data DEFAULT, 25 s; only while her 重构体 stands — the cast is refused otherwise): the 重构体 is withdrawn
// (its piece waits: back once its tile is free again) and she 【移动】s onto its tile; meanwhile range x-4, ATK +atk, base attack
// time base_attack_time (a flat −1.5 s on 2.85 s), block +block_cnt, max HP +max_hp (the HP ratio kept), 流失 of
// damage_per_second HP/s (8 every 0.1 s); her attacks hit every ground enemy she blocks (one in range when she blocks none —
// [ASSUMED] as 凯尔希's Mon3tr S2) for ATK true damage, and each attack starts a chain heal on herself at attack@heal_scale × ATK
// (she is a free link). 不死: a hit or 流失 that would knock her out leaves her at 1 HP, untargetable and blocking nothing, and
// the skill ends on the next tick. At the end she 【返回】s to the tile she left (her SP emptied; the max HP drop keeps the ratio,
// so after a fatal blow she stands at ≤ 1 HP) with 1 s of 不死. The 路标形态 marker on her tile is not modelled: should another
// unit stand there, she stays where she is [ASSUMED; officially she is knocked out].
import { num, talentBb, traitBb, skillRec, batMod, installAura, up } from '../shared/tier1.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_monstr_1';
const S2 = 'skchr_monstr_2';
const S3 = 'skchr_monstr_3';
/** Her summon 重构体 (T1). */
export const PROSTS = 'token_10050_monstr_prosts';
/** S2: the second chain starts this long after a heal of hers on her 重构体 (monstr_s_2[proj] lifeTime). */
const S2_EXTRA_DELAY = 0.6;
/** The 重构体's 流失: every 0.1 s, from 1 s after it lands (prosts_t_1[born_charge] interval / firstTriggerInterval). */
const PROSTS_BLEED_IV = 0.1;
const PROSTS_BLEED_FIRST = 1;
/** The 重构体's ATK aura refresh [ASSUMED: the aura's interval is not in the data]. */
const PROSTS_AURA_IV = 0.1;
/** S3's 流失 period (monstr_s_3[bleeding] interval). */
const S3_BLEED_IV = 0.1;
/** S3: 不死 after the return (monstr_s_3[end_undeadable_restore] lifeTime). */
const RETURN_UNDEAD = 1;
/** How often a waiting 重构体 tries to come back once ready. */
const RETRY = 0.1;
const T2_KEY = 'talent:monstr:aspd';
const PROSTS_ATK_KEY = 'talent:monstr:prostsAtk';
const PROSTS_ABN_KEY = 'monstr:prosts:abnormal';
const S3_UNDEAD_KEY = 'skill:monstr:undead';
const TAG_PROSTS_BLEED = 'monstr:prostsBleed';
const TAG_S3_BLEED = 'monstr:meltdownBleed';
/** Her heals "无视禁疗" (every heal ability of hers: `_ignoreHealFree`). */
const HEAL_OPTS = Object.freeze({ ignoreHealFree: true });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const skillOn = (unit, id) => !!(unit.skill && unit.skill.active && unit.skill.id === id);
const isProstsOf = (t, unit) => !!t && t.kind === 'token' && t.defId === PROSTS && t.ownerUnit === unit;
const prostsOf = (battle, unit) => battle.allyUnits.filter((t) => isProstsOf(t, unit));
/** Her 重构体 standing inside her current attack range. */
const prostsInRange = (battle, unit) => prostsOf(battle, unit).find((t) => up(t) && !!unit.rangeKeySet && unit.rangeKeySet.has(t.tileR * COLS + t.tileC)) ?? null;
/** `a` stands on the 3×3 around `b` (x-4: a heal jump's reach, the 重构体's aura). */
const nearX4 = (a, b) => Math.abs(a.tileR - b.tileR) <= 1 && Math.abs(a.tileC - b.tileC) <= 1;
/**
* The next link of a chain heal from `cur`: the lowest-HP-ratio injured ally of its 3×3 not healed yet by this chain (her
* own 重构体 even at full HP when `fullProsts`); never a 禁疗 unit but her 重构体.
*/
function nextLink(battle, unit, cur, seen, fullProsts) {
let best = null;
for (const a of battle.allyUnits) {
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device' || seen.has(a.id) || !nearX4(a, cur)) continue;
const mine = isProstsOf(a, unit);
if (a !== unit && ((a.s.flags.noHeal && !mine) || (a.profile && a.profile.noHeal))) continue;
if (!(a.hp < a.s.maxHp - 1e-6) && !(fullProsts && mine)) continue;
if (!best || a.hpRatio < best.hpRatio - 1e-12 || (Math.abs(a.hpRatio - best.hpRatio) <= 1e-12 && a.deploySeq < best.deploySeq)) best = a;
}
return best;
}
/**
* A chain heal of hers from `main` (already healed by her attack when `mainDone`): up to `count` units healed, × `step` per
* jump; a free link (her 重构体; herself with `selfFree`) uses no target of the count and the next 1 + `extra` jumps keep the
* amount.
*/
function chainFrom(battle, unit, main, base, { count, step, extra, fullProsts = false, mainDone = false, selfFree = false }) {
const free = (t) => isProstsOf(t, unit) || (selfFree && t === unit);
let amount = base;
if (!mainDone) battle.heal(unit, main, amount, HEAL_OPTS);
let counted = free(main) ? 0 : 1;
let calm = free(main) ? 1 + extra : 0;
const seen = new Set([main.id]);
let cur = main;
while (counted < count) {
const next = nextLink(battle, unit, cur, seen, fullProsts);
if (!next) break;
seen.add(next.id);
if (calm > 0) calm--; else amount *= step;
battle._ev(['atk', cur.id, next.id, 'chainHeal']);
battle.heal(unit, next, amount, HEAL_OPTS);
if (free(next)) calm = 1 + extra; else counted++;
cur = next;
}
}
/** The 重构体's kit (owner = Mon3tr): 禁疗, no block, taunt −1, its 流失, the ATK aura, its return. */
function prostsKit(owner, { atk }) {
return {
skill: null,
talents: [],
trait: { noAttack: true },
install(battle, t) {
const dps = num(t.def?.traitBb?.damage_per_second, 80);
battle.addBuff(t, { key: PROSTS_ABN_KEY, flags: { noHeal: true, noBlock: true }, mods: { taunt: -1 }, persist: true, allowDead: true });
if (atk > 0) installAura(battle, t, { key: PROSTS_ATK_KEY, select: (a) => a !== t && nearX4(a, t), mods: { atkPct: atk }, value: atk, interval: PROSTS_AURA_IV });
battle.on('deploy', (c) => {
if (c.unit !== t) return;
const seq = t.deploySeq;
battle.after(PROSTS_BLEED_FIRST, () => {
if (t.deploySeq !== seq || !up(t)) return;
battle.every(PROSTS_BLEED_IV, (b, sched) => {
if (t.deploySeq !== seq || !up(t)) { sched.cancel(); return; }
b.loseHp(t, dps * PROSTS_BLEED_IV, { source: t, tags: [TAG_PROSTS_BLEED] });
}, { owner: t, immediate: true });
}, { owner: t });
}, { owner: t });
// gone (destroyed or withdrawn): the piece stays and comes back on its tile once ready, while she stands
battle.on('death', (c) => {
if (c.unit !== t || battle.finished) return;
t.removed = false;
t.mem.readyAt = battle.time + Math.max(0, num(t.base.respawnTime));
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 });
},
};
}
/** S2: with nobody injured in her range she heals her 重构体 there all the same ("可选择满生命值的重构体进行治疗"). */
function s2CanAttack(battle, unit) {
const r = prostsInRange(battle, unit);
if (!r || battle.injuredAlliesInKeys(unit.rangeKeys, unit).length) return true;
if (battle.forceAttack(unit, [r])) unit.atkCd = Math.max(unit.atkCd, unit.s.interval);
return false;
}
export default {
char_4179_monstr: (bb, chess) => {
const tb = traitBb(chess); // attack@chain.max_target (3), attack@chain.atk_scale (0.75; XAH-X 0.85)
const t0 = talentBb(chess, 0); // 自我修复: cnt, atk (XAH-X stage 3: 0.25 + attack@chain.extra_cnt 1)
const t1 = talentBb(chess, 1); // 战术协同: buff_duration, attack_speed
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s1 = skillRec(chess, S1), s3 = skillRec(chess, S3);
const chain = {
count: Math.max(1, Math.floor(num(tb['attack@chain.max_target'], 3))),
step: num(tb['attack@chain.atk_scale'], 0.75),
extra: Math.max(0, Math.floor(num(t0['attack@chain.extra_cnt']))),
};
const s1Count = Math.max(1, Math.floor(num(b1['chain.max_target'], chain.count)));
const picked = chess?.skill?.skillId ?? null;
return {
// the chain is chainFrom's (heal mode single: no profession chain); her 重构体 is selectable through its 禁疗
trait: { heal: { mode: 'single' }, healThrough: (healer, a) => isProstsOf(a, healer) },
skills: {
[S1]: { kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant', attack: { healScale: num(b1.heal_scale, 1) } },
[S2]: { kind: 'duration', attack: { canAttack: (battle, unit) => s2CanAttack(battle, unit) } },
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess), blockCnt: num(b3.block_cnt), hpFlat: num(b3.max_hp) },
targeting: s3?.rangeGrid ? { rangeGrid: s3.rangeGrid } : undefined,
attack: { dmgType: 'true', attack: 'melee', projectile: 'none', canHitFly: false, groundOnly: true, hitAllBlocked: true, maxTargets: 1 },
onStart({ battle, unit, skill }) {
const r = prostsOf(battle, unit).find(up);
if (!r) { skill.end('noToken'); skill.addCharge(1); return; } // (refused before: the activate guard)
const home = { r: unit.tileR, c: unit.tileC, dir: unit.dir };
const to = [r.tileR, r.tileC];
battle.retreat(r, { reason: 'retreat', permanent: true }); // 撤退场上的重构体(返回待部署区)
const fromX = unit.x, fromY = unit.y;
if (!battle.moveRedeploy(unit, to[0], to[1])) { skill.end('blocked'); skill.addCharge(1); return; } // 强制结束技能并返还技力
unit.mem.mHome = home;
battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
},
onTick({ unit, skill }) { if (unit.mem.mFatal) skill.end('fatal'); }, // 受到致命伤害结算后立即结束技能
onEnd({ battle, unit }) {
const h = unit.mem.mHome;
unit.mem.mHome = null;
unit.mem.mFatal = false;
battle.removeBuff(unit, S3_UNDEAD_KEY);
if (!h || !unit.alive || !unit.deployed) return;
// 【返回】 to the tile she left: SP emptied (S3_Restore ModifySp −100 %), 1 s 不死
const fromX = unit.x, fromY = unit.y, dir0 = unit.dir;
unit.dir = h.dir;
if (battle.moveRedeploy(unit, h.r, h.c, { clearSp: true })) battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
else unit.dir = dir0;
unit.mem.mUndeadUntil = battle.time + RETURN_UNDEAD;
},
},
},
talents: [
{ install(battle, unit) { // 自我修复: her pieces run the 重构体's kit; withdrawn when she leaves; readied by her deployment
for (const t of battle.allyUnits) {
if (!isProstsOf(t, unit) || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t);
battle._setupUnit(t, prostsKit(unit, { atk: num(t0.atk) }));
}
battle.on('death', (c) => {
if (c.unit !== unit) return;
for (const t of prostsOf(battle, unit)) if (t.alive) battle.retreat(t, { reason: 'retreat', permanent: true });
}, { owner: unit });
battle.on('deploy', (c) => {
if (c.unit !== unit || c.move) return;
for (const t of prostsOf(battle, unit)) if (!t.alive && !t.removed) t.mem.readyAt = battle.time;
}, { owner: unit });
} },
{ install(battle, unit) { // 战术协同: every heal of hers ⇒ target and herself ASPD + (×talent_scale under S2)
const d = num(t1.buff_duration), v0 = num(t1.attack_speed);
if (!(d > 0) || !(v0 > 0)) return;
battle.on('heal', (c) => {
if (c.source !== unit || c.opts?.regen || !c.target || c.target.side !== 'ally' || !(c.amount > 0)) return;
const v = v0 * (skillOn(unit, S2) ? num(b2.talent_scale, 1) : 1);
const give = (x) => battle.applyStrongest(x, T2_KEY, { duration: d, value: v, mods: (a) => ({ aspd: a }), source: unit });
give(c.target);
if (c.target !== unit && unit.alive) give(unit);
}, { owner: unit });
} },
],
install(battle, unit) {
// the chain heals: after each heal attack of hers a chain from its target(s); under S3 every attack starts one on her
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.alive) return;
const prof = battle.effectiveProfile(unit);
if (prof.heal && prof.dmgType === 'heal') {
const sk = unit.skill;
const s1on = !!(sk && sk.id === S1 && sk.active && sk.pending);
const s2on = skillOn(unit, S2);
const base = unit.s.atk * (prof.atkScale ?? 1) * (prof.healScale ?? 1) * unit.s.atkScaleMul;
for (const t of c.targets || []) {
if (!t || t.side !== 'ally') continue;
chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, fullProsts: s2on, mainDone: true });
// S2: 0.6 s after her attack healed her 重构体, a second chain on it (her ATK × 1)
if (s2on && isProstsOf(t, unit)) {
battle.after(S2_EXTRA_DELAY, () => {
if (!unit.alive || !up(t)) return;
chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, { ...chain, fullProsts: skillOn(unit, S2) });
}, { owner: unit });
}
}
} else if (skillOn(unit, S3) && unit.mem.mHome && !unit.mem.mFatal) {
chainFrom(battle, unit, unit, unit.s.atk * num(b3['attack@heal_scale'], 0.5) * unit.s.atkScaleMul, { ...chain, selfFree: true });
}
}, { owner: unit });
// S2: her 重构体 inside her range is the heal target
if (picked === S2) {
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillOn(unit, S2) || !c.profile || c.profile.dmgType !== 'heal') return;
const r = prostsInRange(battle, unit);
if (r) c.targets = [r];
}, { owner: unit, priority: 10 });
}
if (picked !== S3) return;
// S3 `_onlyAvailableWhenTokenValid`: every activation path is refused while her 重构体 is not on the field
const sk = unit.skill;
if (sk && sk.id === S3 && !Object.prototype.hasOwnProperty.call(sk, 'activate')) {
const activate = sk.activate;
Object.defineProperty(sk, 'activate', {
configurable: true, writable: true, enumerable: false,
value(reason, opts) { return prostsOf(battle, unit).some(up) ? activate.call(this, reason, opts) : false; },
});
}
// S3's 流失
battle.every(S3_BLEED_IV, () => {
if (!skillOn(unit, S3) || !unit.mem.mHome || unit.mem.mFatal || !up(unit)) return;
battle.loseHp(unit, num(b3.damage_per_second) * S3_BLEED_IV, { source: unit, tags: [TAG_S3_BLEED] });
}, { owner: unit });
// S3 不死; the 1 s 不死 after the return
battle.on('fatal', (c) => {
if (c.unit !== unit) return;
if (skillOn(unit, S3) && unit.mem.mHome) {
c.prevented = true;
if (!unit.mem.mFatal) {
unit.mem.mFatal = true;
battle.addBuff(unit, { key: S3_UNDEAD_KEY, flags: { untargetable: true, noBlock: true }, tags: ['skill'] });
battle.releaseBlocked(unit);
battle.fx('undying', { x: unit.x, y: unit.y, id: unit.id });
}
return;
}
if (battle.time < num(unit.mem.mUndeadUntil, -Infinity) - 1e-9) c.prevented = true;
}, { owner: unit, priority: 10 });
},
};
},
};