Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_4_24-billro.js
T
sganggs 69d791f9c6 feedback7 R22-OSIM: a skill that 白铁's 铁钳号 opens now acts on it — 蕾缪安 S3 / 艾丽妮 S3 / 陈 S2 / 引星棘刺 S2 spend on it, skills that pick enemies only stay closed; golden: unchanged
Grok's review of the 铁钳号 change (HIGH): the device opened every non-heal skill, but kits that pick their victims
with enemiesInKeys only were spent on nothing — 蕾缪安 S3 went active with 0 locks, 艾丽妮 S3 and 陈 S2 struck nothing,
引星棘刺 S2 threw at the empty far tile. Audit: every skill of every chess (loadouts) and of the 自选 pool, placed with
only the device in range; of the 351 skills the device opened, 67 never touched it.

- skills.js `allyTargetsOk`: the device counts for a skill's start only if the skill acts on it — SkillSpec
  `allyTargets` (true / false) from the kit, else [ASSUMED] a skill acting through the unit's own attacks (timed and
  still attacking, or a cast that is the next attack) on a unit that attacks. An onStart-only cast or a no-attack timed
  skill picks enemies itself and waits for one (德克萨斯 S2 剑雨, 锡人 S1/S2, 铃兰 S3 … about 50 skills).
- Spend on the device (allyTargets: true; its kit cancels the hits, its 受击回复 counts): 蕾缪安 S3 locks it and spends
  her bullets (ammoUsed, so the per-bullet layers come) and shells it; 艾丽妮 S3 strikes it; 陈 S2 draws on it after
  every enemy; 引星棘刺 S2 throws onto it and its pulses hit it. queries.js allyTargetsInRadius for their areas.
- Explicitly closed ([ASSUMED], allyTargets: false): 普罗旺斯 S2 (never shoots above 80 % HP), 卡涅利安 S1 (charged: no
  attack), 山 S2 (blocked enemies), 余 S2 / S3, 迷迭香 S3 (blocked only), 荒芜拉普兰德 S1 / S2 (drone locks), 淬羽赫默 S1–S3
  and 遥 S1–S3 (heal attacks), 机械师 S3, 珊比 S2 / S3, 陈 S3 / 艾丽妮 S2 (their guards already refused).
- 雪猎's empty-magazine cast back to enemies only (its S2 does not act on the device).
Re-audit: 281 skills open on the device alone and all act on it (蕾缪安 S3 through her locks); none of those that
acted on it before stopped. Docs: SIM.md, design/engine.md, PLAYING.md.

Tests: test/content/feedback7-allytarget-kits.test.js (5; all fail on the parent).
2026-10-08 20:53:24 +08:00

152 lines
9.6 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_24-billro.js — 卡涅利安 (char_426_billro) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
num, tbb, moduleBb, grid, batFlat, enemiesOnRange, whileDeployed, toggleBuff, skillActive, isSel, alt, withDefaults,
} from '../shared/tier4.js';
/** 卡涅利安 S3 食噬之印: the ATK bonus climbs in 1 s steps over 20 s (PRTS 备注; the blackboard has no ramp time). */
const BILLRO_S3_RAMP = 20;
/** 卡涅利安 S3 charged mark: one buff per enemy, shared by every 卡涅利安 (PRTS 备注), ≤ 5 stacks (skill text). */
const BILLRO_MARK = 'billro:mark';
const BILLRO_MARK_MAX = 5;
export default withDefaults({
// ===== 卡涅利安 (phalanx) S2 沙缚镣锁 — faster attacks on every enemy in range (the trait's 群体法术伤害), 0.3 s
// sluggish on each; charged: ATK +10 % and bind; talents
// S1 沙暴守卫 (SEARCH: ATK/DEF up; charged — cast with every charge stored — "特性效果在技能期间继续生效", PRTS 备注
// "应用蓄力时:应用技能未开启时的特性": the skill-off trait stays in force, the DEF/RES guard AND 不攻击 — she makes
// no attack until the skill ends, a pure guard);
// S3 食噬之印 (wider range — data rule ACTIVE_RANGE on its x-2, the owner's rule of 2026-10-05 over the 阵法术师
// SEARCH row —, ATK +0 % → +140 %/+200 % in 1 s steps over 20 s — PRTS 备注; charged: each attack adds a
// stack of the mark BEFORE its damage — PRTS 备注 "于攻击造成伤害前生效,多层效果之间加算叠加" — +20 % damage from
// her per stack, ≤ 5, so ×1.2 on the first hit and ×2.0 from the 5th, until the skill ends; one mark per enemy:
// another 卡涅利安 only adds stacks to it, the bonus is the setter's — PRTS 备注 popup). Talent 生命之餐 heals on
// every skill; module PLX-X keeps part of the guard during any skill, PLX-Y (乡音): +3 % damage per enemy in range
// (≤ 5)
chess_char_4_24_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const tb = def.traitBb || {};
const keepDef = num(tb['billro_e_002[buff].def'], 0), keepRes = num(tb['billro_e_002[buff].magic_resistance'], 0);
const S1 = isSel(def, 'skchr_billro_1'), S2 = isSel(def, 'skchr_billro_2'), S3 = isSel(def, 'skchr_billro_3');
const g = grid(def.skill?.rangeGrid);
const isCharged = (skill) => skill.maxCharges > 1 && skill.charges + 1 >= skill.maxCharges; // cast with every charge stored
return {
// charged S1: the skill-off trait's 不攻击 stays in force while the skill lasts (PRTS S1 备注) — no attack at all
trait: S1 ? { canAttack: (battle, u) => !(skillActive(u) && u.mem.billroCharged) } : null,
skills: alt(def, {
skchr_billro_1: () => ({
kind: 'duration',
// [ASSUMED] a charged cast makes no attack at all: 白铁's 铁钳号 alone does not open it (skills.js allyTargetsOk)
allyTargets: false,
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
onStart({ battle, unit, skill }) {
unit.mem.billroCharged = isCharged(skill);
// 蓄力额外效果:特性效果在技能期间继续生效 (the profession removes its own guard buff when a skill starts; the
// kit trait's canAttack keeps the trait's 不攻击)
if (unit.mem.billroCharged) battle.addBuff(unit, { key: 'billro:s1guard', mods: { defPct: num(unit.profile?.guardDef, 2), resFlat: num(unit.profile?.guardRes, 20) } });
battle.fx('shell', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { unit.mem.billroCharged = false; battle.removeBuff(unit, 'billro:s1guard'); },
}),
skchr_billro_3: () => ({
kind: 'duration',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit, skill }) {
unit.mem.billroCharged = isCharged(skill);
unit.mem.billroRamp = 0;
unit.mem.billroMarked = new Set();
battle.addBuff(unit, { key: 'billro:s3atk', mods: { atkPct: 0 } });
battle.fx('devour', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ unit, dt }) { // 攻击力逐渐增至+N%: PRTS 备注 "从+0%开始在20秒内线性增加,攻击力每1秒更新1次"
unit.mem.billroRamp = (unit.mem.billroRamp ?? 0) + dt;
const steps = Math.min(BILLRO_S3_RAMP, Math.floor(unit.mem.billroRamp + 1e-9));
const b = unit.findBuff('billro:s3atk');
if (b && b.mods.atkPct !== num(bb.atk) * steps / BILLRO_S3_RAMP) { b.mods = { atkPct: num(bb.atk) * steps / BILLRO_S3_RAMP }; unit.markDirty(); }
},
onEnd({ battle, unit }) {
battle.removeBuff(unit, 'billro:s3atk');
// "持续至技能结束": the marks she set end with her skill (stacks another 卡涅利安 added to them included)
for (const id of unit.mem.billroMarked || []) {
const e = battle.unitById(id);
if (e && e.findBuff(BILLRO_MARK)?.source === unit) battle.removeBuff(e, BILLRO_MARK);
}
unit.mem.billroMarked = null;
unit.mem.billroCharged = false;
},
}),
}),
skill: {
kind: 'duration',
mods: { batPct: batFlat(def, bb.base_attack_time) },
onStart({ battle, unit, skill }) {
const charged = isCharged(skill);
unit.mem.billroCharged = charged;
if (charged) battle.addBuff(unit, { key: 'billro:charged', mods: { atkPct: num(bb.atk, 0.1) } });
battle.fx(charged ? 'sandChainsCharged' : 'sandChains', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { unit.mem.billroCharged = false; battle.removeBuff(unit, 'billro:charged'); },
},
talents: [
{ install(battle, unit) { // 生命之餐: every skill start heals 40 % max HP (×2 when charged)
battle.on('skillStart', (c) => {
if (c.unit !== unit) return;
const charged = isCharged(c.skill);
battle.heal(unit, unit, unit.s.maxHp * (charged ? num(t0['billro_t_1[enhance].heal_scale'], 0.8) : num(t0.heal_scale, 0.4)), { self: true });
}, { owner: unit });
} },
{ install(battle, unit) { // 蓄势待发: +0.6 SP/s once a charge is stored
whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'billro:t2', !!unit.skill && unit.skill.charges >= 1 && !skillActive(unit), { spRecoveryFlat: num(t1.sp_recovery_per_sec, 0.6) }));
} },
],
install(battle, unit) {
// module PLX-X (elite default): 技能开启时保留部分效果 (any skill; a charged S1 keeps the whole guard instead)
if (keepDef || keepRes) {
battle.on('skillStart', (c) => {
if (c.unit !== unit || (S1 && unit.mem.billroCharged)) return;
battle.addBuff(unit, { key: 'billro:keep', mods: { defPct: keepDef, resFlat: keepRes } });
}, { owner: unit });
battle.on('skillEnd', (c) => { if (c.unit === unit) battle.removeBuff(unit, 'billro:keep'); }, { owner: unit });
}
// module PLX-Y: 范围内敌人越多造成的伤害越高(每个 +3 %,最多 5 个)
const per = num(mb.damage_scale, 0), cap = num(mb.max_valid_stack_cnt, 5);
if (per > 0) {
battle.on('hit', (c) => {
if (c.source !== unit || c.target.side !== 'enemy') return;
const n = Math.min(cap, enemiesOnRange(battle, unit).length);
if (n > 0) c.dmg.mul *= 1 + per * n;
}, { owner: unit });
}
if (S3) {
// charged S3 食噬之印: each attack adds a stack BEFORE its damage (PRTS 备注 "于攻击造成伤害前生效"); stacks add up,
// +20 % damage taken from the mark's setter per stack. One mark per enemy (PRTS 备注 popup: "存在则为已有的BUFF叠加
// 一次加成,不存在则给与敌人仅对此卡涅利安的伤害生效的一个食噬之印BUFF"): another 卡涅利安 stacks it without the bonus
const per = num(bb['attack@damage_scale'], 0.2);
battle.on('hit', (c) => {
const e = c.target;
if (c.source !== unit || e.side !== 'enemy') return;
let m = e.findBuff?.(BILLRO_MARK) || null;
if (c.dmg.isAttack && skillActive(unit) && unit.mem.billroCharged) {
if (m) battle.addBuff(e, { key: BILLRO_MARK, refresh: 'stack', maxStacks: BILLRO_MARK_MAX, duration: m.timeLeft });
else {
m = battle.addBuff(e, { key: BILLRO_MARK, refresh: 'stack', maxStacks: BILLRO_MARK_MAX, duration: Math.max(0.1, (unit.skill?.timeLeft ?? 0) + 0.1), source: unit, visible: true });
if (m) unit.mem.billroMarked?.add(e.id);
}
}
if (m && m.source === unit) c.dmg.mul *= 1 + per * (m.stacks || 1);
}, { owner: unit });
}
if (!S2) return;
// "每次攻击对目标造成0.3秒停顿" (charged: 束缚): every enemy her attack damages — each one on her range
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg || !c.dmg.isAttack || e.side !== 'enemy' || !e.alive || !skillActive(unit)) return;
if (unit.mem.billroCharged) battle.applyStatus(e, 'bind', { duration: num(bb['attack@root'], 0.3), source: unit });
else battle.applyStatus(e, 'sluggish', { duration: num(bb['attack@sluggish'], 0.3), source: unit });
}, { owner: unit });
},
};
},
});