Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_4_01-rmixer.js
T
sganggs 7e8b435f7c feedback7 R22-GHSIM: 信仰搅拌机 S1 reloads the latest-deployed 拉特兰 ammo user on his 8 surrounding tiles, not the nearest (GitHub #325, PR #329 by @IceCodeNew, re-implemented); golden: unchanged
PRTS 信仰搅拌机 S1 备注: 「补弹范围为周围8格,仅选择可补弹的干员为目标,优先选择最后部署的干员」. The candidates (his own
拉特兰 operators within 1.5 tiles — the 8 surrounding tiles — whose ammo skill runs) are now sorted by the battle-wide
deploySeq, latest first (a redeploy is a new deployment), ties by unit id; until now by distance [ASSUMED]. The unused
detmath hypot import goes. Golden: unchanged (no corpus battle has two such candidates in a different order).
2026-10-08 19:13:30 +08:00

147 lines
10 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/chess_char_4_01-rmixer.js — 信仰搅拌机 (char_4194_rmixer) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { isHpLoss } from '../../../damage.js';
import { holdsUndying } from '../../items/battle.js';
import {
AURA, num, tbb, grid, nationOf, enemiesOnRange, targetsInRange, whileDeployed, reveal, skillActive, isSel, alt,
instantKind, withDefaults,
} from '../shared/tier4.js';
/** Is module `id` the active one? (elites; `'none'` / normal chess ⇒ false) */
const moduleIs = (def, id) => !!(def?.raw?.module?.active && def.raw.module.id === id);
/**
* Permanent forward range extension (攻击距离 +n): every range, skill ranges included — except a skill whose range ignores
* 攻击距离 (`targeting.noRangeExtend`). SPT-Y's "攻击距离+1(不受技能攻击范围变化影响)" reads, as PRTS 修正 it, "(部分技能不受
* 此影响)" (原因: 语序颠倒、表述模糊) — 信仰搅拌机 S3 备注 "此技能的攻击范围不受'攻击距离'属性影响".
*/
const rangeUp = (battle, unit, n = 1) => battle.addBuff(unit, { key: 'module:range', mods: { rangeExtend: n }, persist: true, allowDead: true });
export default withDefaults({
// ===== 信仰搅拌机 (shotprotector) S3 退休前布道 — ammo 30 counters; talents 扫射迎宾仪礼 / 架盾送客仪礼; module: reveal
// S1 铳骑主考官 (自动触发 ⇒ DEFAULT: next attack ×3 hits + reload an adjacent 拉特兰 ammo skill), S2 八臂电锯侠 (ammo;
// a fatal hit is blocked for `ammo_cost` bullets); module SPT-Y 老朋友: range +1 (not on S3's range, PRTS 备注)
chess_char_4_01_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const counterMax = Math.max(1, Math.floor(num(bb['attack@max_target'], 3)));
const counterRatio = num(bb.base_attack_time, 0.6); // 反击最小间隔 = 实际攻击间隔 × ratio (not a BAT change)
const g = grid(def.skill?.rangeGrid);
const S2 = isSel(def, 'skchr_rmixer_2'), S3 = isSel(def, 'skchr_rmixer_3');
return {
skills: alt(def, {
// 铳骑主考官: the next attack is a triple hit of atk_scale each and reloads `charge` bullet(s) into ONE other
// 拉特兰 operator around him whose ammo skill runs — PRTS 备注 「补弹范围为周围8格,仅选择可补弹的干员为目标,优先选择最后
// 部署的干员」: the latest deployment first (battle-wide deploySeq: a redeploy is a new deployment). Until 0.2.2 the
// nearest first [ASSUMED] (GitHub #325, PR #329)
skchr_rmixer_1: () => ({
kind: instantKind(def),
attack: {
atkScale: num(bb.atk_scale, 1.3), hits: 3,
onHit({ battle, unit }) {
const n = num(bb.charge, 1);
const cand = battle.alliesInRadius(unit.x, unit.y, 1.5, unit.ownerId)
.filter((a) => a !== unit && a.kind === 'op' && nationOf(a) === 'laterano' && a.skill && a.skill.active && a.skill.kind === 'ammo')
.sort((a, b) => b.deploySeq - a.deploySeq || a.id - b.id);
if (!cand[0] || !(n > 0)) return;
cand[0].skill.addAmmo(n);
battle.fx('reload', { x: cand[0].x, y: cand[0].y, id: cand[0].id, n });
},
},
}),
// 八臂电锯侠: ATK/DEF up, 42/45 bullets; the fatal-hit guard is installed below
skchr_rmixer_2: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 42),
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
onStart({ battle, unit }) { battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 30),
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), defPct: num(bb.def) },
// 3-13 exactly: SPT-Y's 攻击距离+1 does not reach it (PRTS 备注; reviewer of community report E1 after 0.1.0)
targeting: g ? { rangeGrid: g, noRangeExtend: true } : undefined,
attack: { noAttack: true }, // 停止主动攻击敌人 (ammo is spent by counters)
onStart({ battle, unit }) {
unit.mem.counterReady = -Infinity;
for (const a of battle.allies(unit.ownerId)) {
if (a === unit || a.kind !== 'op' || nationOf(a) !== 'laterano') continue;
if (a.skill && a.skill.active && a.skill.kind === 'ammo') { a.skill.addAmmo(num(bb.ammo, 0)); battle.fx('reload', { x: a.x, y: a.y, id: a.id, n: num(bb.ammo, 0) }); }
}
battle.fx('sermon', { x: unit.x, y: unit.y, id: unit.id });
},
},
talents: [
{ install(battle, unit) { // 扫射迎宾仪礼: every damage dealt → DEF +30 / ASPD +3 for 10 s, ≤ 3 stacks
battle.on('damaged', (c) => {
if (c.source !== unit || !unit.alive || c.target.side !== 'enemy' || !(c.amount > 0)) return;
battle.addBuff(unit, { key: 'rmixer:t1', duration: num(t0.duration, 10), refresh: 'stack', maxStacks: Math.max(1, num(t0.max_stack_cnt, 3)),
mods: { defFlat: num(t0.def), aspd: num(t0.attack_speed) } });
}, { owner: unit });
} },
{ install(battle, unit) { // 架盾送客仪礼: no active attack for 8 s → shield 15 % max HP; timer restarts when it breaks
const since = () => Math.max(unit.deployedAt, unit.mem.lastActiveAtk ?? -Infinity, unit.mem.shieldLostAt ?? -Infinity);
battle.on('attack', (c) => { if (c.attacker === unit && !unit.mem.inCounter) unit.mem.lastActiveAtk = battle.time; }, { owner: unit });
battle.on('deploy', (c) => { if (c.unit === unit) { unit.mem.lastActiveAtk = -Infinity; unit.mem.shieldLostAt = -Infinity; } }, { owner: unit });
whileDeployed(battle, unit, 0.1, () => {
if (unit.findBuff('rmixer:shield') || battle.time - since() < num(t1.interval, 8) - 1e-9) return;
battle.addBuff(unit, { key: 'rmixer:shield', shield: unit.s.maxHp * num(t1.shield, 0.15), visible: true,
onRemove: () => { unit.mem.shieldLostAt = battle.time; } });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
});
} },
],
install(battle, unit) {
if (S2) {
// S2 "技能期间若受到致命伤害,立即消耗N发弹药抵挡这次伤害": the lethal hit is negated (HP back to its value
// before the hit) and N bullets go; with fewer left it still blocks, spends them all and the skill ends (PRTS 备注
// "弹药量不足时仍可抵挡致命伤害,此时将消耗所有剩余弹药并退出技能状态"). "自身持有不死时此效果不会生效": a 不死
// that runs before it (骑士戒律, priority 20) has already prevented the hit; 坚固维式重锤's held window
// (items/battle.js holdsUndying: this deployment's, not run out) runs last, so the guard steps aside while it lasts
const cost = Math.max(1, Math.floor(num(bb.ammo_cost, 30)));
battle.on('hit', (c) => { if (c.target === unit) unit.mem.rmixerPre = { dmg: c.dmg, hp: unit.hp }; }, { owner: unit, priority: -100 });
battle.on('fatal', (c) => {
const sk = unit.skill;
if (c.unit !== unit || c.prevented || !sk || !sk.active || sk.kind !== 'ammo' || !(sk.ammoLeft > 0)) return;
if (holdsUndying(battle, unit)) return;
c.prevented = true;
const pre = unit.mem.rmixerPre;
unit.hp = pre && pre.dmg === c.dmg ? Math.max(1, Math.min(unit.s.maxHp, pre.hp)) : 1;
const spent = Math.min(cost, sk.ammoLeft);
sk.ammoLeft -= spent;
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id, n: spent });
if (sk.ammoLeft <= 0) sk.end('ammo');
}, { owner: unit });
}
// module SPT-Y (elite, 老朋友): 攻击距离+1 — S1 / S2 keep it, S3's 3-13 does not (noRangeExtend above)
if (num(def.traitBb?.ability_range_forward_extend, 0) > 0) rangeUp(battle, unit, num(def.traitBb.ability_range_forward_extend, 1));
// module SPT-X (elite default): 攻击范围内敌人的隐匿效果失效
if (moduleIs(def, 'uniequip_002_rmixer')) whileDeployed(battle, unit, AURA, () => reveal(battle, enemiesOnRange(battle, unit)));
if (!S3) return;
// S3 counter (PRTS 备注 "反击受到任何伤害后均可触发,无需目标,视为普通攻击,受各类无法触发普通攻击效果的影响"): ANY damage
// she takes — an enemy's attack, a zone, the 无来源 源石溶剂 tick (items/battle.js periodic_damage) — fires one volley at
// ≤ 3 enemies in range (the attacker first), at most once per actual interval × ratio. With no enemy in range the
// counter still happens and spends its bullet (无需目标: the drain alone empties the skill — 莫斯提马's 特质 turns those
// bullets into 拉特兰 layers, player report D1). A 流失 (Battle.loseHp, tag 'hpLoss': it skips every damage event —
// PRTS 作战机制) and an element 损伤 never counter; stunned / disarmed: no counter
battle.on('damaged', (c) => {
const d = c.dmg;
if (c.target !== unit || !unit.canAct || unit.s.flags.disarm || !skillActive(unit) || !d || c.type === 'element') return;
if (isHpLoss(d)) return;
if (battle.time < (unit.mem.counterReady ?? -Infinity) - 1e-9) return;
const list = targetsInRange(battle, unit);
const i = c.source ? list.indexOf(c.source) : -1;
if (i > 0) { list.splice(i, 1); list.unshift(c.source); }
const targets = list.slice(0, counterMax);
unit.mem.counterReady = battle.time + unit.s.interval * counterRatio;
unit.mem.inCounter = true;
try {
if (targets.length) battle.forceAttack(unit, targets);
else { unit.stats.attacks++; unit.skill.onAttackPerformed([], true); } // a counter into nothing: its bullet goes
} finally { unit.mem.inCounter = false; }
battle.fx('counter', { x: unit.x, y: unit.y, id: unit.id, n: targets.length });
}, { owner: unit });
},
};
},
});