Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-ironmn.js
T
sganggs 99d8807328 feedback5 WE2: 自选 summon hand counts include the tokens' own deploy talents (#21)
tools/build-data.mjs tokenTalentDeckBonus adds a summon's talent max_deploy_count /
max_deck_stack_cnt (the hidden "TOKEN数" talent; a module token part of the same talent
replaces it, matched by prefabKey) to its variant deployLimit / deckStack. Regenerated
offline: only data/backups.json tokens of 麦哲伦 (drones), 令 (souls), 白铁 (devices) and
夜莺 (幻影) changed — deployLimit 3 / 4 with SUM-Y stage 3, 2, 3; deckStack 5 (8), 3 (4).
The hand count (player/diy.js placeableTokens) now matches the official one for every
自选 summoner (PRTS 卫戍协议/帮助 "根据召唤物部署数量上限", the owners' talents, PRTS 幻影 备注).
The summoner kits read deckStack / deployLimit as they are (same decks as before).
Golden: no scenario moved (the diy family places no summon pieces).
2026-10-06 15:01:46 +08:00

406 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/op-ironmn.js — 白铁 (char_4072_ironmn) 自选 operator kit: 6★ 工匠 (辅助), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, both modules (CRA-X 铁钳号·爬行者, CRA-Y 海布里印记)
// at every form, and the kits of his three <支援装置>: 白铁™多功能平台 (token_10027_ironmn_pile1, 极致火力 / pile2, 高效补给)
// and 铁钳号·原型机 (token_10027_ironmn_pile3). Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4072_ironmn): 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 / the token rows (zh_CN, as built into backups.json), PRTS
// 白铁 (S1 / S2 备注 "支援装置的效果可叠加", S2 "携带该技能时,支援装置每秒流失最大生命值0.4%的生命值", S3 "此时的支援装置为敌对
// 阵营的召唤物单位"; 节约经费 "手动撤回白铁周围8格的装置也可以触发本天赋"; the module pages: CRA-X 白铁™多功能平台 −1 / 铁钳号 −4
// 部署费用, CRA-Y −5 s / −10 s 再部署时间), PRTS 白铁™多功能平台 / 铁钳号·原型机 (备注: 持有禁疗、无敌 / "该召唤物阵营为敌方…提供
// 我方视野", 禁疗 孤立 不可阻挡 失衡免疫 元素免疫, 晕眩 寒冷 冻结 浮空 束缚 沉默 沉睡免疫, "受到伤害时取消此伤害,若该伤害来源为非
// 无职业的辅助职业的工匠单位,治疗自身相当于最大生命值1%的生命值"; 团结的力量 "实际范围为自身及身后一格…多个支援装置的天赋效果可
// 叠加"; skill "伤害半径1.1,造成物理普通伤害…可对空", "仅能选择不持有不可选中/隐匿/无敌的敌方单位(无视孤立)"), PRTS 分支特性信息
// 工匠 ("干员部署后,按天赋描述数量补充支援装置持有数", "干员离场后,附属的支援装置随之消失"), PRTS 卫戍协议/帮助 (§战斗部署 a hand
// summon comes in its deploy-limit count; §作战阶段 "所有手动部署的召唤物…作战开始时立即部署一次", "若战场区初始部署有召唤物,若召唤
// 物在战斗期间退场,将在满足条件后立即原地再部署1个"), and the client's battle data read from the local install — charpack
// char_4072_ironmn (ironmn_t_1 charge_token[born]; ironmn_t_2 on his own tokens of his x-4: ON_OWNER_FINISH ⇒ Dice(prob) ⇒
// RechargeToken(cnt); ironmn_c die_to_kill_token), the token prefabs pile1 / 2 / 3 (talent auras on ally operators —
// professionMask 639 — of the device range, `disableOverride` = one buff per device; pile2 modify_sp[trigger] + the
// ironmn_pile_bleed timer of 1 s; pile3: Trait ironmn_pile3_trait, 团结的力量 damage_resistance[phy] on targetSide 2 = our
// operators of its b-1, `ignoreTargetFree`), the skill prefabs (skchr_ironmn_1 / 2 / 3: devices to their skill mode,
// RechargeToken at S1 start / S2 end / S3 start, KillTokens at S1 end; sktok_ironmn_pile3: one target (`_maxTarget` 1),
// projectile_chr_ironmn_pile3 at 10 tiles/s, `_exceptTraceTarget` splash, the bleed on the cast event), the equip prefabs
// (CRA-X: trigger_charge_token +cnt at each deployment, stage 2+ ironmn_e_002[sp_recover] on the device's x-4; CRA-Y stage
// 2+: ironmn_e_003_t[attr] ATTACK_SPEED on the device talent's targets, its own buff per device) and buff_template_data
// (ironmn_s_1 / 2 / 3, ironmn_t_2, ironmn_pile_bleed / pile3_bleed: PURE DamageViaMaxHpRatio `_skipModifierEvent` — a 流失 —,
// ironmn_pile3_s[main] / [splash]: PHYSICAL AdvancedApplyDamage on the host's ATK, attackType NORMAL, charge_token[born],
// trigger_charge_token, die_to_kill_token).
//
// - Trait (工匠) "能够阻挡两个敌人,使用<支援装置>协助作战": melee physical (professions.js craftsman), 1-1, blocks 2, ground only;
// the enemies he blocks are always his targets (Battle.blockedTargets, PRTS 分支特性信息 "可以且优先攻击自身阻挡的单位").
// - <支援装置> = the hand pieces of the picked skill's device (data `overrideTokenKey`: S1 pile1, S2 pile2, S3 pile3 — the
// variant `sources`, server/match/player/diy.js placeableTokens). Every placed piece deploys with the board at the battle
// start for free (the mode's rule). His 持有 (stock) is cnt (T1, 3) + CRA-X's trait cnt (1) after each of his deployments
// (charge_token[born] + trigger_charge_token), capped there (持有上限). A device that leaves the field comes back on its own
// tile — after its redeploy time (data respawnTime, CRA-Y less), paying its cost (data, CRA-X less) and spending one of the
// stock — while he stands; when he leaves, his devices go with him (die_to_kill_token) and come back the same way after his
// return (stock refilled). The devices never attack, block nobody, are 禁疗; 白铁™多功能平台 is untargetable and invulnerable
// ("不会受到攻击"; PRTS 持有无敌).
// - T1 战地工程师 "可以携带3个<支援装置>(最多可部署2个)": the stock above; the 2 deployable are the hand pieces. CRA-Y stage 2+
// "自身装置天赋生效的干员攻击速度+6(此效果不受技能影响)": every operator a device's talent reaches ASPD +attack_speed (the
// token's module talent; one per device), never scaled by S1.
// - T2 节约经费 "当白铁周围8格的自身装置损毁时,有70%的几率回收使白铁额外获得1个装置": a device of his leaving the field (killed,
// destroyed by S1, withdrawn) while on his x-4 (talent range) ⇒ prob ⇒ stock +cnt (capped) — not when he leaves himself
// (his talents go first). CRA-X stage 2+ adds "当白铁周围8格存在自身装置时技力回复速度+0.2/秒" (the token's module talent
// sp_recovery_per_sec; one effect however many devices — the client's ironmn_e_002[sp_recover], one buff key) and prob 0.9.
// - Module CRA-X “铁钳号·爬行者” "<支援装置>的持有上限+1且部署费用减少": the stock +1 (trait cnt) and the device costs of the data
// (2 / 2 / 6). CRA-Y “海布里印记” "<支援装置>的再部署时间减少": the data's respawn times (5 / 5 / 10 s).
// - 白铁™多功能平台 (S1): "使攻击范围内一名友方干员的攻击力+12%" — every operator on its range (1-1: its tile and the one it
// faces) ATK +atk (Σpct); while 白铁's S1 runs ×talent_scale (2.5 / 3: "装置效果提升至…倍").
// - 白铁™多功能平台 (S2): "使攻击范围内一名友方干员每3.5秒额外获得1点技力,每秒流失0.4%生命" — every operator on its range gets 1
// SP each `default.interval` s it stays there (the first one interval after it came; none while its skill runs — AK: no SP
// during a skill); the device loses hp_ratio of its max HP every second (a 流失: invulnerable as it is, it runs out). While
// 白铁's S2 runs: s2.interval s and s2.hp_ratio (both timers restart when the mode changes, as the client re-creates the
// buffs on its mode switch).
// - 铁钳号·原型机 (S3), an enemy-camp summon: our attacks select it like an enemy, after every real one (嘲讽等级 −2;
// Battle.setAllyTarget — the engine extension of this kit), enemies never (untargetable); every damage it takes is cancelled
// and gives it 1 SP (受击回复), and +hp_ratio of its max HP when the source is a 工匠 operator (trait, ignores 禁疗). 团结的力
// 量: the operator on its b-1 (its tile and the one behind it) takes physical damage ×(1 − damage_resistance) — one effect per
// device. Its skill (AUTO, 5 SP, data DEFAULT — it never attacks, so the engine checks it every tick: an enemy, flyers too, in
// its 3-13): it loses hp_ratio of its max HP (流失), and a projectile (10 tiles/s) to the first enemy of its range deals
// 白铁's ATK (at the cast) × atk_scale physical to it and × atk_scale_2 to the other enemies within 1.1 of it (中点判定;
// not that one: `_exceptTraceTarget`), 普通伤害, air units too. Immune to the control statuses PRTS lists, never displaced.
// - S1 极致火力 (MANUAL, data DEFAULT): 16 / 17 s, every attack attack@atk_scale × ATK; +1 stock at the start; at its end every
// device of his on the field is destroyed (KillTokens — T2 may recover them).
// - S2 高效补给 (MANUAL, DEFAULT): 30 s, ATK +atk, DEF +def, every enemy he blocks at once (hitAllBlocked); +1 stock at its end.
// - S3 铁钳号·原型机 (MANUAL, DEFAULT): 30 s, ATK +atk, ASPD +attack_speed; +1 stock at the start.
// [ASSUMED] the stock refilled by +cnt on each deployment, capped at cnt + the CRA-X +1; a RechargeToken beyond the cap is lost;
// the crab charges only from the attacks that select it (an ally's area skill that picks enemies does not see it); summons
// attack it too. The hand count of his devices is the data's deployLimit 2 (the attribute frame's 1 + the token's hidden talent
// max_deploy_count 1 — E2 "最多可部署2个"; tools/build-data.mjs tokenTalentDeckBonus, 0.2.0).
import { num, talentBb, talentGrid, traitBb, skillRec, up, giveSp } from '../shared/tier1.js';
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
import { frontOf } from '../../../dir.js';
const S1 = 'skchr_ironmn_1';
const S2 = 'skchr_ironmn_2';
const S3 = 'skchr_ironmn_3';
export const PILE1 = 'token_10027_ironmn_pile1';
export const PILE2 = 'token_10027_ironmn_pile2';
export const CRAB = 'token_10027_ironmn_pile3';
const DEVICES = new Set([PILE1, PILE2, CRAB]);
/** 节约经费's range when the data carries none: his tile and the 8 around it (x-4). */
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
/** A device's 1-1 when its def carries none: its tile and the one it faces. */
const FRONT1 = Object.freeze([[0, 0], [0, 1]]);
/** 铁钳号·原型机's skill: PRTS 备注 "伤害半径1.1"; projectile_chr_ironmn_pile3 `_speed` 10. */
const CRAB_SPLASH = 1.1;
const CRAB_SPEED = 10;
/** Statuses 铁钳号·原型机 is immune to (PRTS 备注: 晕眩 寒冷 冻结 浮空 束缚 沉默 沉睡免疫). */
const CRAB_IMMUNE = new Set(['stun', 'cold', 'freeze', 'levitate', 'bind', 'silence', 'sleep']);
/** Seconds between two tries of a device waiting to come back (timer, stock, DP, tile). */
const RETRY = 0.25;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** First talent of a normalised token def holding `key` (token talents keep no index). */
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
const tokBb = (def, key) => tokTalent(def, key)?.bb ?? {};
/** An operator of our side a device's talent may reach (professionMask 639: operators, not summons). */
const isOp = (a) => !!a && a.kind === 'op' && a.side === 'ally';
/**
* A 工匠's <支援装置> state for the battle: `stock` (持有), `cap` (持有上限), `devices` (its pieces). Shared with 娜斯提
* (op-nasti.js).
*/
export const deviceState = (unit) => unit.mem.craftsman ?? (unit.mem.craftsman = { stock: 0, cap: 0, devices: new Set() });
/** The owner's devices standing on the field. */
export const devicesUp = (unit) => [...deviceState(unit).devices].filter((d) => up(d));
/** Stock +n, capped at the 持有上限 (RechargeToken). */
export function rechargeDevices(unit, n) {
const st = deviceState(unit);
st.stock = Math.min(st.cap, st.stock + Math.max(0, n));
}
/**
* Keep buff `key` (one per device: `disableOverride`) on every operator `select` returns, exactly while it does; `mods(a)`
* re-read whenever `sig()` changes (a skill switching the device's mode). Removed from everyone when the device leaves.
*/
export function deviceAura(battle, dev, { key, select, mods, sig = () => 0 }) {
const held = new Map(); // operator → signature it was given
const drop = (a) => { if (a.findBuff(key)) battle.removeBuff(a, key); held.delete(a); };
battle.on('tick', () => {
if (!up(dev)) { for (const a of [...held.keys()]) drop(a); return; }
const want = select();
const s = sig();
for (const a of [...held.keys()]) if (!want.includes(a) || !up(a)) drop(a);
for (const a of want) {
if (!up(a)) continue;
if (held.get(a) === s && a.findBuff(key)) continue;
const m = mods(a);
if (!Object.keys(m).length) { drop(a); continue; }
battle.addBuff(a, { key, mods: m, source: dev, tags: ['talent', 'device'] });
held.set(a, s);
}
}, { owner: dev });
battle.on('death', (ctx) => { if (ctx.unit === dev) for (const a of [...held.keys()]) drop(a); }, { owner: dev, priority: 10 });
}
/** Operators of our side standing on `keys` (absolute tile keys), the device itself excluded. */
export function opsOn(battle, dev, keys, { ignoreSelect = false } = {}) {
const set = keys instanceof Set ? keys : new Set(keys);
const out = [];
for (const a of battle.allyUnits) {
if (!up(a) || a.hidden || !isOp(a) || !set.has(a.tileR * COLS + a.tileC)) continue;
if (!ignoreSelect && !battle.allySelectable(a, dev)) continue;
out.push(a);
}
return out;
}
/**
* A device piece's return (PRTS 卫戍协议/帮助 "若战场区初始部署有召唤物…退场,将在满足条件后立即原地再部署1个"): once it left the
* field it comes back on its own tile after its redeploy time (data respawnTime), paying its cost (`costOf(dev)`, default its
* data cost) and spending one of the owner's stock, while the owner stands. `onLeave(dev)` runs as it leaves (节约经费 —
* not when it leaves with its owner: `dev.mem.withOwner`), `onBack(dev)` when it is back. Shared with 娜斯提 (op-nasti.js).
*/
export function deviceReturn(battle, dev, owner, { onLeave = null, costOf = null, onBack = null } = {}) {
battle.on('death', (ctx) => {
if (ctx.unit !== dev || battle.finished) return;
if (!dev.mem.withOwner && onLeave) onLeave(dev);
dev.mem.withOwner = false;
dev.removed = false; // the piece stays its tile's (content/tokens.js enableRespawn): back there later
const at = battle.time + Math.max(0, num(dev.base.respawnTime));
const seq = (dev.mem.returnSeq ?? 0) + 1;
dev.mem.returnSeq = seq;
const tryBack = () => {
if (battle.finished || dev.alive || dev.removed || dev.mem.returnSeq !== seq) return;
const st = deviceState(owner);
if (battle.time + 1e-9 >= at && up(owner) && st.stock > 0) {
const cost = Math.max(0, costOf ? costOf(dev) : num(dev.base.cost));
const ps = battle.getPlayer(dev.ownerId);
if (ps && ps.dp + 1e-9 >= cost && battle.redeploy(dev, { free: true })) {
ps.dp = Math.max(0, ps.dp - cost);
st.stock--;
battle.fx('summon', { x: dev.x, y: dev.y, id: dev.id, src: owner.id });
if (onBack) onBack(dev);
return;
}
}
battle.after(Math.max(RETRY, at - battle.time), tryBack, { owner: dev });
};
battle.after(Math.max(0, at - battle.time), tryBack, { owner: dev });
}, { owner: dev, priority: -10 });
}
/** The owner leaving withdraws its devices on the field (die_to_kill_token / KillTokens), but those `keep(dev)` spares. */
export function devicesLeaveWithOwner(battle, owner, keep = null) {
battle.on('death', (ctx) => {
if (ctx.unit !== owner) return;
for (const d of devicesUp(owner)) {
if (keep && keep(d)) continue;
d.mem.withOwner = true;
battle.retreat(d, { reason: 'expired' });
}
}, { owner });
}
export default {
char_4072_ironmn: (bb, chess) => {
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const grid1 = talentGrid(chess, 1) ?? X4;
const extraCnt = num(traitBb(chess).cnt); // CRA-X: 持有上限+1 (and trigger_charge_token +1)
const cnt = num(t0.cnt, 3) + extraCnt;
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
/** Device kit (`dev` = the piece, `owner` = 白铁). */
const deviceKit = (dev, owner) => {
const def = dev.def;
const sk = def.skill ?? null;
const tb = sk?.bb ?? {};
const aspd = num(tokBb(def, 'attack_speed').attack_speed); // CRA-Y stage 2+ (the token's module talent)
const grid = dev.rangeGrid ?? def.rangeGrid ?? FRONT1;
const keys = () => absoluteRangeKeys(grid, dev.tileR, dev.tileC, dev.dir, 0);
const s1On = () => up(owner) && !!owner.skill?.active && owner.skill.id === S1;
const s2On = () => up(owner) && !!owner.skill?.active && owner.skill.id === S2;
const common = (battle) => {
battle.addBuff(dev, { key: 'ironmn:device', flags: { untargetable: true, noHeal: true, ...(dev.defId === CRAB ? { isolated: true, noDisplace: true } : { invulnerable: true }) }, persist: true, allowDead: true });
if (aspd) {
// CRA-Y: "自身装置天赋生效的干员攻击速度+6(此效果不受技能影响)" — on the operators the device's talent reaches
const reach = dev.defId === CRAB
? () => opsOn(battle, dev, [dev.tileR * COLS + dev.tileC, (([r, c]) => r * COLS + c)(frontOf(dev.tileR, dev.tileC, dev.dir, -1))], { ignoreSelect: true })
: () => opsOn(battle, dev, keys());
deviceAura(battle, dev, { key: `ironmn:aspd:${dev.id}`, select: reach, mods: () => ({ aspd }) });
}
};
if (dev.defId === PILE1) {
const atk = num(tokBb(def, 'atk').atk);
const scale = num(tb.talent_scale, num(b1.fake_scale, 1));
return {
skill: null,
trait: { noAttack: true },
talents: [],
install(battle) {
common(battle);
// 支援火力: ATK +atk on the operators of its 1-1 (×talent_scale while 白铁's S1 runs)
deviceAura(battle, dev, { key: `ironmn:pile1:${dev.id}`, select: () => opsOn(battle, dev, keys()),
sig: () => (s1On() ? 1 : 0), mods: () => ({ atkPct: atk * (s1On() ? scale : 1) }) });
},
};
}
if (dev.defId === PILE2) {
const iDef = num(tokBb(def, 'default.interval')['default.interval'], 3.5);
const iS2 = num(tb['s2.interval'], num(b2.fake_interval, iDef));
const hpDef = num(tokBb(def, 'hp_ratio').hp_ratio);
const hpS2 = num(tb['s2.hp_ratio'], hpDef * 2);
return {
skill: null,
trait: { noAttack: true },
talents: [],
install(battle) {
common(battle);
const m = { mode: null, next: new Map(), bleedAt: Infinity };
battle.on('deploy', (ctx) => { if (ctx.unit === dev) { m.mode = null; m.next.clear(); } }, { owner: dev });
battle.on('tick', () => {
if (!up(dev)) return;
const mode = s2On() ? 'S2' : 'default';
if (mode !== m.mode) { m.mode = mode; m.next.clear(); m.bleedAt = battle.time + 1; } // the buffs re-made on a mode switch
const every = mode === 'S2' ? iS2 : iDef;
// 战地补给: 1 SP every `every` s to each operator staying on its 1-1
const now = opsOn(battle, dev, keys());
for (const a of [...m.next.keys()]) if (!now.includes(a)) m.next.delete(a);
for (const a of now) {
if (!m.next.has(a)) { m.next.set(a, battle.time + every); continue; }
if (battle.time + 1e-9 < m.next.get(a)) continue;
m.next.set(a, m.next.get(a) + every);
if (giveSp(a, 1, 'device') > 0) battle.fx('spGift', { x: a.x, y: a.y, id: a.id, from: dev.id });
}
// 每秒流失0.4%生命 (S2: ×2): a 流失, invulnerable or not
if (battle.time + 1e-9 >= m.bleedAt) {
m.bleedAt += 1;
const r = mode === 'S2' ? hpS2 : hpDef;
if (r > 0) battle.loseHp(dev, dev.s.maxHp * r, { source: dev, tags: ['device'] });
}
}, { owner: dev });
},
};
}
// 铁钳号·原型机
const dr = num(tokBb(def, 'damage_resistance').damage_resistance);
const healRatio = num(def.traitBb?.hp_ratio, 0.01);
const atkScale = num(tb.atk_scale, 1), atkScale2 = num(tb.atk_scale_2, 0), loss = num(tb.hp_ratio);
const skillGrid = sk?.rangeGrid ?? grid;
return {
skill: {
kind: 'instant',
onStart({ battle, unit }) {
// 技能开启时自身流失2.5%的生命 (PURE DamageViaMaxHpRatio, `_skipModifierEvent`: a 流失 its trait does not cancel)
const atk = up(owner) ? owner.s.atk : 0;
const keysNow = absoluteRangeKeys(skillGrid, unit.tileR, unit.tileC, unit.dir, 0);
const list = battle.enemiesInKeys(keysNow, unit, { canHitFly: true });
sortEnemyTargets(battle, unit, list, null);
const target = list[0] ?? null;
if (loss > 0) battle.loseHp(unit, unit.s.maxHp * loss, { source: unit, tags: ['device'] });
if (!target || !(atk > 0)) return;
battle.addProjectile({ from: unit, target, speed: CRAB_SPEED, visual: 'bomb', source: unit, hitDead: true,
onHit: ({ target: t, x, y }) => {
if (t && t.alive) battle.dealDamage(unit, t, { amount: atk * atkScale, type: 'phys', isSkill: true, tags: ['summon', 'skill'] });
const cx = t ? t.x : x, cy = t ? t.y : y;
if (atkScale2 > 0) {
for (const e of battle.foesInRadius(cx, cy, CRAB_SPLASH, true)) {
if (e === target || !e.alive) continue;
battle.dealDamage(unit, e, { amount: atk * atkScale2, type: 'phys', isSkill: true, tags: ['summon', 'skill'] });
}
}
battle.fx('aoe', { x: cx, y: cy, radius: CRAB_SPLASH, id: unit.id, skill: 'ironmn:crab' });
} });
},
},
trait: { noAttack: true, canHitFly: true },
talents: [],
install(battle) {
common(battle);
battle.on('deploy', (ctx) => { if (ctx.unit === dev) battle.setAllyTarget(dev, true); }, { owner: dev });
battle.on('death', (ctx) => { if (ctx.unit === dev) battle.setAllyTarget(dev, false); }, { owner: dev, priority: 5 });
// trait: every damage cancelled; 受击回复 +1 SP; a 工匠 operator's damage heals it hp_ratio of its max HP
battle.on('hit', (ctx) => {
if (ctx.target !== dev || !up(dev)) return;
ctx.dmg.cancel = true;
if (dev.skill && !dev.skill.active) dev.skill.gainSp(1, 'hurt');
const src = ctx.source ?? ctx.credit;
if (src && src.kind === 'op' && src.def?.profession === 'SUPPORT' && src.profile?.sub === 'craftsman' && healRatio > 0) {
battle.heal(dev, dev, dev.s.maxHp * healRatio, { self: true, ignoreHealFree: true });
}
}, { owner: dev, priority: 100 });
battle.on('elementHit', (ctx) => { if (ctx.target === dev) ctx.dmg.cancel = true; }, { owner: dev }); // 元素免疫
battle.on('beforeStatus', (ctx) => { if (ctx.target === dev && CRAB_IMMUNE.has(ctx.status)) ctx.cancel = true; }, { owner: dev });
// 团结的力量: the operator on its tile / the one behind it — physical damage taken ×(1 − damage_resistance), per device
if (dr > 0) {
const behind = () => {
const [r, c] = frontOf(dev.tileR, dev.tileC, dev.dir, -1);
return opsOn(battle, dev, [dev.tileR * COLS + dev.tileC, r * COLS + c], { ignoreSelect: true });
};
deviceAura(battle, dev, { key: `ironmn:crab:${dev.id}`, select: behind, mods: () => ({ physTakenMul: 1 - dr }) });
}
},
};
};
return {
skills: {
[S1]: {
kind: 'duration',
attack: { atkScale: num(b1['attack@atk_scale'], 1) },
onStart({ unit }) { rechargeDevices(unit, 1); },
onEnd({ battle, unit }) {
// 技能结束时所有场上的装置被销毁 (KillTokens)
for (const d of devicesUp(unit)) battle.kill(d, null);
},
},
[S2]: {
kind: 'duration',
mods: { atkPct: num(b2.atk), defPct: num(b2.def) },
attack: { hitAllBlocked: true },
onEnd({ unit }) { rechargeDevices(unit, 1); },
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), aspd: num(b3.attack_speed) },
onStart({ unit }) { rechargeDevices(unit, 1); },
},
},
talents: [
{ install(battle, unit) {
// 战地工程师: his device pieces run the device kits (set up here, before the battle starts — as op-bgsnow.js); the
// stock refilled on each of his deployments; his devices leave with him (die_to_kill_token)
const st = deviceState(unit);
st.cap = cnt;
const prob = num(t1.prob), gain = num(t1.cnt, 1);
// 节约经费: a device of his leaving his x-4 ⇒ prob ⇒ +cnt
const onLeave = (dev) => {
if (!up(unit) || !absoluteRangeKeys(grid1, unit.tileR, unit.tileC, unit.dir, 0).includes(dev.tileR * COLS + dev.tileC)) return;
if (!(prob > 0) || !battle.rng.chance(prob)) return;
rechargeDevices(unit, gain);
battle.fx('spGift', { x: unit.x, y: unit.y, id: unit.id, from: dev.id });
};
for (const t of battle.allyUnits) {
if (t.kind !== 'token' || !DEVICES.has(t.defId) || t.ownerUnit !== unit || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t);
battle._setupUnit(t, deviceKit(t, unit));
deviceReturn(battle, t, unit, { onLeave });
st.devices.add(t);
}
battle.on('deploy', (ctx) => { if (ctx.unit === unit) rechargeDevices(unit, cnt); }, { owner: unit });
devicesLeaveWithOwner(battle, unit);
} },
{ install(battle, unit) {
// 节约经费 (the roll is the device lifecycle's, above); CRA-X stage 2+: SP +0.2/s while a device of his stands on
// his x-4 (one effect)
const spDef = [...deviceState(unit).devices].map((d) => num(tokBb(d.def, 'sp_recovery_per_sec').sp_recovery_per_sec)).find((v) => v > 0) ?? 0;
if (!(spDef > 0)) return;
const key = 'talent:ironmn:spRecover';
battle.on('tick', () => {
const area = new Set(absoluteRangeKeys(grid1, unit.tileR, unit.tileC, unit.dir, 0));
const want = up(unit) && devicesUp(unit).some((d) => area.has(d.tileR * COLS + d.tileC));
const has = unit.findBuff(key);
if (want && !has) battle.addBuff(unit, { key, mods: { spRecoveryFlat: spDef }, tags: ['talent'] });
else if (!want && has) battle.removeBuff(unit, key);
}, { owner: unit });
} },
],
};
},
};