Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-nasti.js
T

421 lines
28 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-nasti.js — 娜斯提 (char_4212_nasti) 自选 operator kit: 6★ 工匠 (辅助), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait, the module CRA-X 工程师们 at every form, and the kits of her
// three <支援装置>: “质检专员” (token_10059_nasti_nstdef, S1), “监工专员” (token_10060_nasti_nstchr, S2) and “应急承重小组”
// (token_10061_nasti_nstbld, S3: the 小工程师 and the 高台 it builds). Kit contract and the 自选 rules: ../README.md ("How to
// add an operator (自选)"). The device stock / return / leave-with-the-owner rules are 白铁's (op-ironmn.js), shared.
//
// Forms (data/backups.json units.char_4212_nasti): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
// 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
// 娜斯提 (前方施工 备注 "所有装置的效果均为对自身攻击范围内所有单位生效的光环效果", "自身<支援装置>提供的效果均可叠加", "<X模组>首个
// 装置部署费用-3;…若装置攻击范围内存在另一同来源同名的装置,将使用自身攻击范围+对方攻击范围形成复合范围"; 注意安全 备注 "远程敌人出现后,
// 此天赋额外效果在娜斯提退场前永久生效", "对敌人/干员的判定无视不可选中类效果/孤立", "“高台干员”指实际可部署于远程位的干员"; S2 备注 "此
// 技能提供的屏障持续时间无限;自身退场时,清除此技能提供的所有屏障"; CRA-X: 质检 / 监工 −2, 应急承重小组 −3 部署费用), PRTS “质检专员”
// / “监工专员” / “应急承重小组” (备注: 常态持有禁疗、无敌 / 禁疗、静默; 监工 "生命值满/持有者技能开启期间,获得阻回、静默", 取缔无效流程
// "生命回复速度属性不受此影响", skill "流失生命效果于获得技力/屏障效果后紧接着处理…会先提供技力和屏障再导致装置死亡"; 应急承重小组: the two
// forms, "前方地块为可部署近战位的低地地块…则在前方地块上召唤一个高台形态的此装置。高台形态的此装置不计入部署数量上限", "仅高台形态装置
// 已激活,且生命值未满时…可对其进行治疗,仅成功治疗后流失生命值", 未激活 = 装置类 + 无敌 + 不可阻挡, 激活 = 默认类, 阻挡数+1, 高地 /
// 仅远程, 重叠部署; 第二次升级起 +1000 / +100 / +5; 最高级 阻回), PRTS 分支特性信息 工匠, PRTS 卫戍协议/帮助 (§战斗部署, §作战阶段 —
// as op-ironmn.js), and the client's battle data read from the local install — charpack char_4212_nasti (nasti_t_1
// charge_token[born]; T2: PassiveTrigger nasti_t2[res] = damage_resistance[inf], AuraTrigger on enemies of applyWay RANGED
// (`_applyWay` 2, every level and motion) ⇒ AuraBuff: OwnerBuff nasti_t2[res_plus] (overrides [res]) + nasti_t2[add_sp], and
// AuraBuffCore on our operators (professionMask 639, `_buildableType` 2, ignoring 孤立) the same two; nasti_a[die_kill_token]
// KillTokens of modes 0–4 — a 3-times-upgraded 高台 (mode 5) stays; S1 / S2 / S3 modes), the token prefabs nstdef / nstchr /
// nstbld (range abilities with the CRA-X stage-3 SharedValidator: same token id, same host, host holding nasti_e[class_3_mark];
// nstchr: EnsureBlood nasti_nstchr[blood_one], full_blood ⇒ 阻回, Special mode RecoverSp add_char_sp / update_shield /
// dec_blood; nstbld: BuildFactory create_m3 (Nstlv1Tile: buildableType melee, low ground, not tile type 16), m345_update on a
// full SP, M4–M6 modes `_keepCurrentPassableMask`), the skill prefabs skchr_nasti_1 / 2 / 3 and sktok_nasti_nstchr (add_hp
// +value, `_hpRatio` 1), the equip prefabs (trigger_charge_token +cnt at each deployment; stage 2+ nasti_e[first_token_cost_dec]
// UNTIL_NEXT_SPAWN; stage 3 nasti_e[class_3_mark]) and buff_template_data (the templates named here).
//
// - Trait (工匠) "能够阻挡两个敌人,使用<支援装置>协助作战": melee physical, 1-1, blocks 2, ground only — and the device rules of
// op-ironmn.js: the picked skill's hand pieces deploy with the board for free; her stock is cnt (3) + CRA-X's trait cnt (1)
// after each of her deployments, capped there; a device that left comes back on its tile after its redeploy time, paying
// its cost, spending one stock, while she stands; her leaving withdraws her devices (a level-3 高台 stays).
// - T1 前方施工 "可以携带3个<支援装置>(最多可部署2个)": that stock; 2 hand pieces (data deployLimit 2). Every device's effect is
// an aura on the operators of its range (1-1: its tile and the one it faces); her devices' effects stack (one buff per
// device). CRA-X stage 2+ "首个装置部署费用减少": after each of her deployments her next device deployment costs value (−3)
// less (UNTIL_NEXT_SPAWN — the battle-start deployment, free here, uses it up); stage 3 "装置可与攻击范围内其他装置共同提供效
// 果": a device with another same-name device of hers on its range also reaches that device's range (one step).
// - T2 注意安全 "获得10%庇护;远程敌人出现后,改为使自身和场上的高台干员获得15%庇护,且每6秒获得1点技力": 庇护 (ba.protect, the
// shared applyStrongest key 'protect': physical and arts damage taken ×(1 − v), the strongest wins) nasti_t2[res] on her;
// once a 远程 enemy (applyWay RANGED — ALL too [ASSUMED: the 2 bit]) is on the field, untargetable or stealthed too, until she
// leaves: [res_plus] on her and on every operator of our side whose position is 远程 (RANGED / ALL — "实际可部署于远程位"), 孤立
// ones too, and each of them +1 SP every interval s it holds it (the first one interval after; none at full SP or during a
// skill).
// - Module CRA-X “工程师们” "<支援装置>的持有上限+1且部署费用减少": stock +1, the data costs (2 / 2 / 3).
// - “质检专员” (S1): "使前方干员的防御力+25%" (Σpct); while her S1 runs: DEF +talent@def instead, block +talent@block_cnt, and
// it loses talent@hp_ratio of its max HP each second (流失; the first one 1 s into the skill).
// - “监工专员” (S2): max HP 8 (data), init_hp 3 at each deployment ("仅可通过自身技能补充生命"); its skill (AUTO, 6 SP over
// time) "开启技能时回复1点生命值" fires whenever its SP is full and its HP is not (阻回 while its HP is full or 娜斯提's skill
// runs — the client's full_blood / Special mode). Activated by her S2 (the devices on the field at the cast): every
// talent@interval s, the first 0.05 s in: each operator of its range +talent@sp SP (none during its skill) and a 屏障 of
// talent@shield_each_hp_rate × 娜斯提's max HP (one barrier per operator from her, filled up to talent@shield_max_hp_rate ×
// her max HP; it lasts until used or until she leaves), then the device loses talent@hp HP; activated devices leave when
// her skill ends.
// - “应急承重小组” (S3): the 小工程师 (invulnerable, untargetable, 禁疗, 阻回) builds the 高台 on the tile it faces when it is
// deployed, if she has none and that tile is a free low melee tile (canStand melee, low); the 高台 (one per 娜斯提, outside the
// deploy limit) gains 1 SP / s of its own (the token's INCREASE_WITH_TIME) and each 小工程师 whose range holds it gives it
// m1_sp SP each second (m2_sp while her S3 runs) while it is not at its top level; a full SP (50) upgrades it (SP cleared):
// level 1 = activated (attackable, block 1), level 2 = +1000 max HP / +100 DEF / +5 RES (m5 attribute_up), level 3 = the
// m6 values instead + 阻回. A 小工程师 heals an activated 高台 of its range that is not at full HP hp_ratio of its max HP
// each second (×2 in her S3) and then loses 2 % of its own (hp_ratio × cnt — "仅成功治疗后流失生命值", 流失). Before level 1 the
// 高台 is invulnerable, untargetable and blocks nobody. The 高台's tile stays walkable (`_keepCurrentPassableMask`) and keeps
// its look of ground; the operator bonuses of a 高台 (m4 / m5 / m6_bonus: +ATK / +max HP for the operator deployed on it)
// never apply here — nobody deploys during a battle of this mode and the 高台 is only built on a free tile [ASSUMED: N/A].
// - S1 “拱卫” (MANUAL, data DEFAULT): 38 / 40 s, ATK +atk, DEF +def, every enemy she blocks at once; +1 stock at the start.
// - S2 “执行” (MANUAL, DEFAULT): 4 s, 停止攻击; every attack@nasti_s2[update_shield].interval s a 屏障 of shield_each_hp_rate ×
// her max HP on herself, up to shield_max_hp_rate × it (infinite, gone when she leaves); activates her 监工专员; +1 stock at
// its end.
// - S3 栖脚地 (MANUAL, DEFAULT): 15 s, ATK +atk, DEF +def; her 小工程师 work twice as hard; +1 stock at the start.
// [ASSUMED] (beyond op-ironmn.js's): the 监工专员 loses its 1 HP per pulse with or without an operator in range (dec_blood is a
// passive buff of the ability); the combined range one step deep; the 小工程师's 1 s heal / SP timer runs from its deployment.
import { num, talentBb, traitBb, skillRec, up, giveSp, holdProtect } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
import { frontOf } from '../../../dir.js';
import { deviceState, devicesUp, rechargeDevices, deviceAura, opsOn, deviceReturn, devicesLeaveWithOwner } from './op-ironmn.js';
const S1 = 'skchr_nasti_1';
const S2 = 'skchr_nasti_2';
const S3 = 'skchr_nasti_3';
export const NSTDEF = 'token_10059_nasti_nstdef';
export const NSTCHR = 'token_10060_nasti_nstchr';
export const NSTBLD = 'token_10061_nasti_nstbld';
const DEVICES = new Set([NSTDEF, NSTCHR, NSTBLD]);
/** A device's 1-1 when its def carries none. */
const FRONT1 = Object.freeze([[0, 0], [0, 1]]);
/** 注意安全's 庇护 refresh: a little longer than the check period, so it never lapses while it holds. */
const T2_PERIOD = 0.25;
const PROTECT_HOLD = T2_PERIOD + 0.05;
/** 监工专员 Special mode: dec_blood `firstTriggerInterval` 0.05 s. */
const CHR_FIRST = 0.05;
/** 小工程师 talent buffs: triggerInterval 1 s. */
const BLD_EVERY = 1;
/** Enemies that count as 远程 (AuraTriggerCore `_applyWay` 2). */
const RANGED_WAYS = new Set(['RANGED', 'ALL']);
/** Operators "实际可部署于远程位" (`_buildableType` 2). */
const RANGED_POS = new Set(['RANGED', 'ALL']);
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tokBb = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null)?.bb ?? {};
const tileOf = (u) => u.tileR * COLS + u.tileC;
const ownKeys = (u) => absoluteRangeKeys(u.rangeGrid ?? u.def?.rangeGrid ?? FRONT1, u.tileR, u.tileC, u.dir, 0);
/** The barrier key of the shields her S2 devices give (one per operator from her: nasti_nstchr_m2[shield_core]). */
const shieldKey = (owner) => `nasti:shield:${owner.id}`;
const SELF_SHIELD = 'nasti:s2shield';
export default {
char_4212_nasti: (bb, chess) => {
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const cnt = num(t0.cnt, 3) + num(traitBb(chess).cnt);
const firstCut = -num(t0.value); // CRA-X stage 2+: 首个装置部署费用 −3
const combined = !!(chess?.module?.active && num(chess.module.level) >= 3); // stage 3: nasti_e[class_3_mark]
const resBase = num(t1['nasti_t2[res].damage_resistance']);
const resPlus = num(t1['nasti_t2[res_plus].damage_resistance'], resBase);
const spEvery = num(t1['nasti_t2[add_sp].interval']);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2Every = num(b2['attack@nasti_s2[update_shield].interval'], 0.5);
const s2Each = num(b2['attack@nasti_s2[update_shield].shield_each_hp_rate']);
const s2Max = num(b2['attack@nasti_s2[update_shield].shield_max_hp_rate']);
const skillOn = (owner, id) => up(owner) && !!owner.skill?.active && owner.skill.id === id;
/**
* The tiles a device's effect reaches: its own range, and with CRA-X stage 3 the range of every same-name device of hers
* standing on it ("复合范围", one step).
*/
const reachOf = (battle, dev, owner) => {
const keys = new Set(ownKeys(dev));
if (combined) {
for (const d of devicesUp(owner)) {
if (d === dev || d.defId !== dev.defId || !keys.has(tileOf(d))) continue;
for (const k of ownKeys(d)) keys.add(k);
}
}
return keys;
};
/** “质检专员”. */
const defKit = (dev, owner) => {
const def = dev.def, tb = def.skill?.bb ?? {};
const base = num(tokBb(def, 'def').def);
const s1Def = num(tb['talent@def'], num(b1['talent@def'], base));
const s1Block = num(tb['talent@block_cnt']);
const s1Loss = num(tb['talent@hp_ratio']);
return {
skill: null,
trait: { noAttack: true },
talents: [],
install(battle) {
battle.addBuff(dev, { key: 'nasti:device', flags: { untargetable: true, invulnerable: true, noHeal: true }, persist: true, allowDead: true });
// 严禁偷工减料 / S1 mode: the operators of its range
deviceAura(battle, dev, { key: `nasti:nstdef:${dev.id}`, select: () => opsOn(battle, dev, reachOf(battle, dev, owner)),
sig: () => (skillOn(owner, S1) ? 1 : 0),
mods: () => (skillOn(owner, S1) ? { defPct: s1Def, ...(s1Block ? { blockCnt: s1Block } : {}) } : { defPct: base }) });
// S1 mode: −talent@hp_ratio of its max HP each second (nasti_nstdef_t[self_damage], first after 1 s)
let next = Infinity, was = false;
battle.on('tick', () => {
const on = up(dev) && skillOn(owner, S1);
if (on && !was) next = battle.time + 1;
was = on;
if (!on || battle.time + 1e-9 < next) return;
next += 1;
if (s1Loss > 0) battle.loseHp(dev, dev.s.maxHp * s1Loss, { source: dev, tags: ['device'] });
}, { owner: dev });
},
};
};
/** “监工专员”. */
const chrKit = (dev, owner) => {
const def = dev.def, tb = def.skill?.bb ?? {};
const initHp = num(tokBb(def, 'init_hp').init_hp, 3);
const heal = num(tb.value, 1);
const sp = num(tb['talent@sp'], 1);
const each = num(tb['talent@shield_each_hp_rate'], num(b2['talent@shield_each_hp_rate']));
const cap = num(tb['talent@shield_max_hp_rate'], num(b2['talent@shield_max_hp_rate']));
const loss = num(tb['talent@hp'], 1);
return {
skill: {
kind: 'instant', trigger: 'SP_FULL',
onStart({ battle, unit }) { battle.heal(unit, unit, heal, { self: true, ignoreHealFree: true }); },
},
trait: { noAttack: true },
talents: [],
install(battle) {
battle.addBuff(dev, { key: 'nasti:device', flags: { untargetable: true, invulnerable: true, noHeal: true }, persist: true, allowDead: true });
// 取缔无效流程: deployed with init_hp of its (data) max HP
battle.on('deploy', (ctx) => { if (ctx.unit === dev) { dev.hp = Math.max(1, Math.min(dev.s.maxHp, initHp)); dev.mem.nastiActive = false; } }, { owner: dev });
// 阻回 while its HP is full or 娜斯提's skill runs (full_blood; the Special mode)
battle.on('tick', () => {
if (!up(dev)) return;
const stop = dev.hp >= dev.s.maxHp - 1e-9 || (up(owner) && !!owner.skill?.active);
const has = dev.findBuff('nasti:nstchr:stop');
if (stop && !has) battle.addBuff(dev, { key: 'nasti:nstchr:stop', flags: { noSp: true } });
else if (!stop && has) battle.removeBuff(dev, 'nasti:nstchr:stop');
}, { owner: dev, priority: 5 });
// activated by her S2: pulses (SP + her barrier to the operators of its range), then −1 HP each
dev.mem.nastiPulse = (b) => {
if (!up(dev) || !dev.mem.nastiActive) return;
for (const a of opsOn(b, dev, reachOf(b, dev, owner))) {
if (giveSp(a, sp, 'device') > 0) b.fx('spGift', { x: a.x, y: a.y, id: a.id, from: dev.id });
const hp = up(owner) ? owner.s.maxHp : 0;
const key = shieldKey(owner);
const cur = a.findBuff(key);
const have = cur ? cur.shield : 0;
const next = Math.min(hp * cap, have + hp * each);
if (next > have + 1e-9) {
if (cur) { cur.shield = next; a.markDirty(); } else b.addBuff(a, { key, shield: next, visible: true, source: owner, tags: ['skill'] });
b.fx('shield', { x: a.x, y: a.y, id: a.id, from: dev.id });
}
}
if (loss > 0) b.loseHp(dev, loss, { source: dev, tags: ['device'] });
};
},
};
};
/** “应急承重小组” — the 小工程师 (a hand piece). */
const bldKit = (dev, owner) => {
const def = dev.def;
const tb0 = tokBb(def, 'm1_sp');
const sp1 = num(tb0.m1_sp, 1), sp2 = num(tb0.m2_sp, 2 * sp1);
const heal1 = num(tb0['nasti_nstbld_m1[heal_m456].hp_ratio']), heal2 = num(tb0['nasti_nstbld_m2[heal_m456].hp_ratio'], 2 * heal1);
const cost1 = heal1 * num(tb0['nasti_nstbld_m1[heal_m456].cnt'], 1), cost2 = heal2 * num(tb0['nasti_nstbld_m2[heal_m456].cnt'], 1);
return {
skill: null,
// its heal of the 高台 ignores the 高台's 禁疗 ("_ignoreHealFree"): it heals through it (damage.js heal healThrough)
trait: { noAttack: true, healThrough: (h, t) => !!t && t.mem?.nastiPlatform === true && t.ownerUnit === owner },
talents: [],
install(battle) {
battle.addBuff(dev, { key: 'nasti:device', flags: { untargetable: true, invulnerable: true, noHeal: true, noSp: true }, persist: true, allowDead: true });
battle.on('deploy', (ctx) => {
if (ctx.unit !== dev || !up(owner)) return;
// 承重结构部署: the 高台 on the tile it faces, if she has none and the tile is a free low melee tile
const cur = owner.mem.nastiPlatform;
if (cur && (cur.alive || !cur.removed)) return;
const [r, c] = frontOf(dev.tileR, dev.tileC, dev.dir, 1);
if (!battle.grid.inRect(r, c) || battle.isReservedTile(r, c) || !battle.grid.canStand(r, c, { ranged: false }) || !battle.grid.isLow(r, c)) return;
const p = battle.spawnToken(owner, NSTBLD, r, c, { kit: platformKit(owner), dir: dev.dir, anySource: true });
if (p) battle.fx('summon', { x: p.x, y: p.y, id: p.id, src: dev.id });
}, { owner: dev });
// every second: SP to the 高台 of its range below its top level; an activated, injured one healed (then −2 % own)
battle.every(BLD_EVERY, () => {
if (!up(dev)) return;
const p = owner.mem.nastiPlatform;
if (!p || !up(p) || !reachOf(battle, dev, owner).has(tileOf(p))) return;
const m2 = skillOn(owner, S3);
const lvl = p.mem.nastiLevel ?? 0;
if (lvl < 3 && p.skill && p.skill.sp < p.skill.spCost) p.skill.gainSp(m2 ? sp2 : sp1, 'device');
if (lvl >= 1 && p.hp < p.s.maxHp - 1e-6) {
const healed = battle.heal(dev, p, p.s.maxHp * (m2 ? heal2 : heal1), { ignoreHealFree: true });
if (healed > 0) battle.loseHp(dev, dev.s.maxHp * (m2 ? cost2 : cost1), { source: dev, tags: ['device'] });
}
}, { owner: dev });
},
};
};
/** The 高台 (a spawned unit, no hand piece): levels 0 (未激活) → 3. */
const platformKit = (owner) => {
const levelUp = (battle, p) => {
const lvl = Math.min(3, (p.mem.nastiLevel ?? 0) + 1);
p.mem.nastiLevel = lvl;
const tb = p.def.skill?.bb ?? {};
if (lvl === 1) {
battle.removeBuff(p, 'nasti:platform:inert');
battle.addBuff(p, { key: 'nasti:platform:block', mods: { blockCnt: 1 }, persist: true, allowDead: true });
} else {
const m = lvl === 2 ? 'm5' : 'm6';
battle.addBuff(p, { key: 'nasti:platform:attr', persist: true, allowDead: true, mods: {
hpFlat: num(tb[`attack@nasti_nstbld_${m}[attribute_up].max_hp`]), defFlat: num(tb[`attack@nasti_nstbld_${m}[attribute_up].def`]),
resFlat: num(tb[`attack@nasti_nstbld_${m}[attribute_up].magic_resistance`]) } });
if (lvl === 3) battle.addBuff(p, { key: 'nasti:platform:top', flags: { noSp: true }, persist: true, allowDead: true });
}
battle.fx('grow', { x: p.x, y: p.y, id: p.id, n: lvl });
};
return {
skill: { kind: 'instant', trigger: 'SP_FULL', onStart({ battle, unit }) { levelUp(battle, unit); } },
trait: { noAttack: true },
talents: [],
install(battle, p) {
p.mem.nastiPlatform = true;
p.mem.nastiLevel = 0;
owner.mem.nastiPlatform = p;
// the 小工程师's "不会受到攻击" (the token text, Battle._setupUnit) is not the 高台's: untargetable only until activated
battle.removeBuff(p, 'trait:untargetable');
battle.addBuff(p, { key: 'nasti:platform', flags: { noHeal: true }, persist: true, allowDead: true }); // 常态持有禁疗
battle.addBuff(p, { key: 'nasti:platform:inert', flags: { untargetable: true, invulnerable: true }, persist: true, allowDead: true });
battle.on('death', (ctx) => { if (ctx.unit === p && owner.mem.nastiPlatform === p) owner.mem.nastiPlatform = null; }, { owner: p });
},
};
};
const kitOf = (dev, owner) => (dev.defId === NSTDEF ? defKit(dev, owner) : dev.defId === NSTCHR ? chrKit(dev, owner) : bldKit(dev, owner));
return {
skills: {
[S1]: {
kind: 'duration',
mods: { atkPct: num(b1.atk), defPct: num(b1.def) },
attack: { hitAllBlocked: true },
onStart({ unit }) { rechargeDevices(unit, 1); },
},
[S2]: {
kind: 'duration',
attack: { noAttack: true },
onStart({ battle, unit }) {
const m = unit.mem;
m.nastiS2 = { n: 0, at: battle.time };
// 立即激活场上装置: the 监工专员 on the field now
const chr = devicesUp(unit).filter((d) => d.defId === NSTCHR);
for (const d of chr) d.mem.nastiActive = true;
const seq = unit.skill.activations;
const shield = () => {
const s = m.nastiS2;
if (!s) return;
s.n++;
if (!up(unit)) return;
const cap = unit.s.maxHp * s2Max;
const cur = unit.findBuff(SELF_SHIELD);
const have = cur ? cur.shield : 0;
const next = Math.min(cap, have + unit.s.maxHp * s2Each);
if (next > have + 1e-9) {
if (cur) { cur.shield = next; unit.markDirty(); } else battle.addBuff(unit, { key: SELF_SHIELD, shield: next, visible: true, tags: ['skill'] });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
}
};
m.nastiS2.shield = shield;
battle.every(s2Every, (b, sc) => {
if (!unit.skill.active || unit.skill.activations !== seq || unit.skill.id !== S2) { sc.cancel(); return; }
shield();
}, { owner: unit });
// the devices' pulses: CHR_FIRST s in, then every talent@interval
const ev = chr.length ? num(chr[0].def.skill?.bb?.['talent@interval'], 0.5) : 0.5;
const pulse = () => { for (const d of chr) if (d.mem.nastiPulse) d.mem.nastiPulse(battle); };
battle.after(CHR_FIRST, () => {
if (!unit.skill.active || unit.skill.activations !== seq) return;
pulse();
battle.every(ev, (b, sc) => {
if (!unit.skill.active || unit.skill.activations !== seq) { sc.cancel(); return; }
pulse();
}, { owner: unit });
}, { owner: unit });
},
onEnd({ battle, unit, skill }) {
const s = unit.mem.nastiS2;
// the last shield tick that lands with the end (every interval of the duration)
if (s && s.shield && up(unit)) while (s.n < Math.floor(skill.duration / s2Every + 1e-9)) s.shield();
unit.mem.nastiS2 = null;
// 激活的装置在娜斯提技能结束时离场
for (const d of devicesUp(unit)) if (d.defId === NSTCHR && d.mem.nastiActive) battle.kill(d, null);
rechargeDevices(unit, 1);
},
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), defPct: num(b3.def) },
onStart({ unit }) { rechargeDevices(unit, 1); },
},
},
talents: [
{ install(battle, unit) {
// 前方施工: her device pieces run their kits; the stock; CRA-X stage 2+ the first device after each deployment −3 DP
const st = deviceState(unit);
st.cap = cnt;
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, kitOf(t, unit));
deviceReturn(battle, t, unit, {
costOf: (d) => Math.max(0, num(d.base.cost) - (unit.mem.nastiFirst ? firstCut : 0)),
});
battle.on('deploy', (ctx) => { if (ctx.unit === t) unit.mem.nastiFirst = false; }, { owner: t, priority: -20 });
st.devices.add(t);
}
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
rechargeDevices(unit, cnt);
unit.mem.nastiFirst = firstCut > 0;
}, { owner: unit });
devicesLeaveWithOwner(battle, unit);
// she leaves: her S2 barriers go (nasti_s2[finish_shield]); a 高台 below level 3 goes with her (KillTokens modes 0–4)
battle.on('death', (ctx) => {
if (ctx.unit !== unit) return;
for (const a of battle.allyUnits) if (a.findBuff(shieldKey(unit))) battle.removeBuff(a, shieldKey(unit));
battle.removeBuff(unit, SELF_SHIELD);
const p = unit.mem.nastiPlatform;
if (p && up(p) && (p.mem.nastiLevel ?? 0) < 3) battle.kill(p, null);
}, { owner: unit });
} },
{ install(battle, unit) {
// 注意安全: 庇护 res; once a 远程 enemy is on the field (until she leaves): res_plus on her and our 远程 operators, +1 SP
// every interval s
let triggered = false;
const timers = new Map();
battle.on('deploy', (ctx) => { if (ctx.unit === unit) { triggered = false; timers.clear(); } }, { owner: unit });
battle.every(T2_PERIOD, () => {
if (!up(unit)) { triggered = false; timers.clear(); return; }
if (!triggered) triggered = battle.enemies.some((e) => e.alive && !e.hidden && RANGED_WAYS.has(String(e.def?.applyWay ?? '').toUpperCase()));
if (!triggered) {
holdProtect(battle, unit, resBase, PROTECT_HOLD, unit); // 庇护: the shared effect (同名效果取最高)
return;
}
const who = [unit, ...battle.allyUnits.filter((a) => a !== unit && up(a) && !a.hidden && a.kind === 'op' && RANGED_POS.has(String(a.def?.position ?? '').toUpperCase()))];
for (const a of [...timers.keys()]) if (!who.includes(a)) timers.delete(a);
for (const a of who) {
holdProtect(battle, a, resPlus, PROTECT_HOLD, unit);
if (!(spEvery > 0)) continue;
if (!timers.has(a)) { timers.set(a, battle.time + spEvery); continue; }
if (battle.time + 1e-9 < timers.get(a)) continue;
timers.set(a, timers.get(a) + spEvery);
if (a.skill && a.skill.sp < a.skill.spCost && giveSp(a, 1, 'talent') > 0) battle.fx('spGift', { x: a.x, y: a.y, id: a.id, from: unit.id });
}
}, { owner: unit, immediate: true });
} },
],
};
},
};