Files
ark-stronghold-protocol/server/sim/content/kits/tier2.js
T
sganggs 987126cc1a feedback1 integrate: WT 蒂比's 起飞 is 对地规避 - ground enemies no longer select or hit her while airborne
Conflicts resolved keeping both sides:
- buffs.js FLAG_KEYS: liftoff beside the integration's selfBound.
- Battle.js: targeting import union (extendedGrid + evadesGround); _blockerFor keeps the
  fenced-tile gate (WU) and adds liftoff; applyStatus keeps the 无敌 + 无法选中 refusal and
  adds the 对地规避 refusal.
- enemies.js: header DOT entry from WT with the integration's INVISIBLE entry; import union;
  enemy_1273_stmgun_2 keeps the integration's rewrite (it filters through canTargetAlly,
  which now includes liftoff).
- tier2.js 蒂比 comment: the integration's 受到伤害前触发 text plus the 起飞 line.
- tier5.js 隐德来希 血镰: liftoff flag with the integration's foesInRadius.
- SIM.md: blocking keeps the fenced-tile passage and adds air-only blocking while airborne;
  the 污染秽蚀 / 燃烧区域 sentence goes before the chimera aura; statuses keep both refusals.
2026-10-03 13:36:17 +08:00

863 lines
49 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/tier2.js — Tier 2 operator kits.
//
// export default { [baseChessId]: (bb, chess, def) => Kit } (docs/SIM.md §7.2). Shared helpers live in tier1.js (named
// exports). Numbers come from the skill / talent / trait blackboards (elite module upgrades are in the elite record's
// trait.bb and hidden index −1 talents). fx kinds: see the tier1.js header.
//
// Covered (normal + elite): 2_01 送葬人 2_02 赫默 2_03 崖心(H) 2_04 小满 2_05 哈洛德 2_06 莎草 2_07 幽灵鲨 2_08 泡泡
// 2_09 休谟斯 2_10 洛洛 2_11 风丸 2_12 砾 2_13 蒂比 2_14 调香师 2_15 协律(H) 2_16 拉普兰德 2_17 折桠 2_18 灰毫 2_19 锡人.
//
// Operator loadouts (DESIGN §16): every visible chess also authors its selectable NON-default skill in `skills`
// ({ [skillId]: SkillSpec }, official Lv4 / Lv7 blackboards — the kit function receives the SELECTED skill's `bb`, so
// every spec is built from `bb`; the trigger rule comes from that skill's data). Talent / install hooks that belong to
// the default skill only (target locks, counters, hit triggers) check onDefaultSkill(chess). Module choices need no
// code here: `chess` is the loadout-resolved record (trait.bb / talents of the selected module, or none).
import { COLS, ELEMENT_GAUGE_MAX } from '../../constants.js';
import { canTargetEnemy } from '../../targeting.js';
import { bodyInKeys } from '../../body.js';
import {
num, talentBb, moduleBb, traitBb, up, cheb, byEnemyAttack, onHitBy, onHitOn, onDamagedOn, enemiesInGrid,
alliesInGridOf, enemyInRange, statBuff, toggleBuff, installAura, spTimeBonus, freeTileAround, instantKind,
tinmanKit, batMod,
} from './tier1.js';
import { releaseSkillSummon } from '../tokens.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);
/**
* Whether the SELECTED skill of a loadout-resolved chess record is its default skill (DESIGN §16: `chess.skill` is the
* selected SkillRecord, `chess.skills[]` flags the default one). Records without skill choices count as default.
*/
function onDefaultSkill(chess) {
const d = (chess?.skills ?? []).find((s) => s && s.isDefault);
return !d || !chess?.skill || d.skillId === chess.skill.skillId;
}
/**
* 起飞 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 血镰, 地面干员 bonds / items still count her).
*/
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_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) }); } }],
}),
// ---------------------------------------------------------------------------------------------------------------
// 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 });
}
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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) });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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));
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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 });
}
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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 });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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); });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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) });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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 });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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 });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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 });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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 } });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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);
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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 });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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);
});
}
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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) => {
if (target && target.alive && target.side === 'enemy') battle.dealDamage(u, target, { amount: u.s.atk * num(tb.atk_scale_m), type: 'arts', tags: ['module'] });
};
}
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;
},
},
},
};
},
// ---------------------------------------------------------------------------------------------------------------
// 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); });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 2_18 灰毫 攻击力强化·γ型 (TAKE_DAMAGE): 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); });
} }],
};
},
// ---------------------------------------------------------------------------------------------------------------
// 2_19 锡人 “大拉里”: see tinmanKit (tier1.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 });
},
},
},
};
},
};