feedback5 O23: 自选 kits for 白铁, 娜斯提 (with their 支援装置) and 死芒 (with her 悲叹的仆役); ally targets for 白铁's 铁钳号·原型机

- kits/ops/op-ironmn.js — 白铁 (char_4072_ironmn): S1 / S2 / S3, 战地工程师 (device stock 3, CRA-X +1, refilled at each
  deployment; a device that left comes back on its tile after its redeploy time, paying its cost, one stock, while he stands;
  his devices leave with him), 节约经费 (prob 0.7 / CRA-X stage 3 0.9), CRA-X (stock, costs, stage 3 SP +0.2/s), CRA-Y
  (redeploy times, stage 3 ASPD +6 on device-buffed operators); the device kits 白铁™多功能平台 (S1 ATK +12 % ×2.5 / 3 in S1;
  S2 1 SP / 3.5 s, 2.5 / 2 s in S2, 0.4 % / s 流失 ×2) and 铁钳号·原型机 (an ally target: damage cancelled, 1 SP per hit,
  a 工匠's hit heals 1 %; its skill −2.5 % then 230 / 260 % + 110 / 120 % within 1.1 of 白铁's ATK; 团结的力量 ×0.8
  physical behind it). Exports the shared 工匠 device helpers.
- kits/ops/op-nasti.js — 娜斯提 (char_4212_nasti): S1 / S2 / S3, 前方施工 (stock; CRA-X stage 3 first device −3, combined
  ranges), 注意安全 (10 % 庇护, 15 % + 1 SP / 6 s for her and our ranged operators once a ranged enemy appeared), CRA-X; the
  device kits “质检专员”, “监工专员” (3 / 8 HP, its S2 pulses of SP and her barrier) and “应急承重小组” (小工程师 + the 高台 it
  builds and levels).
- kits/ops/op-necras.js — 死芒 (char_450_necras): S1 / S2 / S3, 复燃 (summon on the kill tile or a random tile of her range,
  upgrade at 3), 回光黯淡, the trait (the enemies her summons block are her targets; SOC-X ×1.15), SOC-X stage 3; the
  悲叹的仆役 kit and its special form (S3).
- Engine (additive): Battle.setAllyTarget / allyTargetsInKeys / isAllyTarget (battle/queries.js); ai.js acquireTargets puts a
  registered ally target of the range after every enemy, performAttack flies ranged attacks to it. Nothing changes while
  none is registered: every golden family but diy unchanged. docs/SIM.md §6 documents it.
- kits/index.js: the three files appended to OPERATOR_KIT_FILES.
- Tests: test/content/op_ironmn.test.js, op_nasti.test.js, op_necras.test.js, test/sim/feedback5-o23-engine.test.js.
- Golden: only diy moved — diy-106 … diy-109 re-chunked (the new operators' configurations come before the prototypes),
  diy-110 … diy-114 new.
This commit is contained in:
sganggs committed 2026-10-06 05:28:54 +08:00
1 parent 996f5d20fe
commit 5ef47b0cf1
12 files changed
+2859 -100

No files matched your search

+405
View File
@@ -0,0 +1,405 @@
// 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. Open (data, report): the hand count of his devices is the data's deployLimit 1 — tools/build-data.mjs takes
// the attribute frame's maxDeployCount and leaves out the token's hidden talent max_deploy_count +1 (E2 "最多可部署2个").
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 });
} },
],
};
},
};