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).
406 lines
27 KiB
JavaScript
406 lines
27 KiB
JavaScript
// 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 });
|
||
} },
|
||
],
|
||
};
|
||
},
|
||
};
|