feedback1 integrate: WR 阵法术师 / 轰击术师 attacks hit every enemy in range

# Conflicts:
#	docs/SIM.md
This commit is contained in:
sganggs committed 2026-10-03 07:04:07 +08:00
commit fcbccd4a32
13 files changed
+535 -59

No files matched your search

+3 -2
View File
@@ -773,10 +773,11 @@ const KITS = {
return kit;
},
// ---- 3_08 薄绿 · 阵法术师 — S2 聚能涡旋: each hit pushes the target towards her (splash arts), end-of-skill burst on
// ---- 3_08 薄绿 · 阵法术师 — S2 聚能涡旋: each attack strikes every enemy on her range (the trait's 群体法术伤害) and
// pushes each one towards her, end-of-skill burst on
// every enemy in range; 地质学者: DEF aura (skill off) / less likely targeted (skill on);
// 精锐 module PLX-X: keeps part of the guard (DEF/RES) while the skill runs
// S1 风语: wider range (skill grid), attacks at attack@atk_scale (群体 arts splash of the trait). Auto-cast: the data
// S1 风语: wider range (skill grid), attacks every enemy on it at attack@atk_scale. Auto-cast: the data
// rule SEARCH is the 阵法术师 row (PRTS 卫戍协议/帮助 "不受基础策略影响,在初始攻击范围内存在敌人时释放技能"; it
// covers every MANUAL skill of the class — user playtest #6) = an enemy inside her INITIAL range — the engine's
// DEFAULT rule, checked every tick for a unit that does not attack while its skill is off — not any enemy on the
+47 -24
View File
@@ -16,8 +16,9 @@
// groupId `abyssal` (data/chess.json has no groupId: charIds from docs/research/03-operators.json).
// - "友方干员" effects touch operators only (summons/devices excluded); SP gifts skip units whose timed skill runs
// (AK: no SP gain during a skill — the engine's gainSp enforces it too; the kits skip such units when picking).
// - On-hit statuses of AoE skill attacks (卡涅利安) apply to every enemy the attack damages (a SkillSpec
// `attack.onHit` only sees the main target), via a `damaged` hook.
// - On-hit effects of 卡涅利安's attacks land on every enemy the attack strikes — the 阵法术师 trait strikes every enemy
// on her range at once (professions.js `allInRange` / `rangeAoe`, community report E3): S2's 停顿 / 束缚 via a
// `damaged` hook, the charged S3 mark via a `hit` hook (it applies before the damage, PRTS 备注).
// - Dodge from a skill that adds to other dodge sources (焰尾 S3) is rolled independently in a `hit` hook, so the
// sources combine as 1 − Π(1 − p) (the same rule the engine now applies to stacked dodge mods).
// - Operator loadouts (DESIGN §16): every selectable non-default skill of the 22 visible chess is authored in the kit's
@@ -38,6 +39,11 @@ const AURA = 0.2; // aura refresh period (s)
const AURA_DUR = 0.25; // aura buff lifetime (s): lapses ~1 tick after the source stops refreshing it
const ABYSSAL = new Set(['char_143_ghost', 'char_263_skadi', 'char_474_glady', 'char_4145_ulpia', 'char_1023_ghost2']);
const LINE = Object.freeze(Array.from({ length: COLS }, (_, i) => Object.freeze([0, i])));
/** 卡涅利安 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;
// ---------------------------------------------------------------------------------------------------------------
// helpers
@@ -849,7 +855,7 @@ const kits = {
kind: instantKind(def),
attack: {
atkScale: num(bb.atk_scale, 1.5),
onEachHit({ battle, unit, target }) { // every victim of the blast (main + splash) that flies
onEachHit({ battle, unit, target }) { // every enemy of her line the blast strikes that flies
if (target && target.alive && target.isFlying) battle.dealDamage(unit, target, { amount: unit.s.atk * num(bb.atk_scale_to_fly, 0.55), type: 'arts', isSkill: true, tags: ['skill', 'antiAir'] });
},
},
@@ -1727,11 +1733,17 @@ const kits = {
};
},
// ===== 卡涅利安 (phalanx) S2 沙缚镣锁 — faster AoE, 0.3 s sluggish; charged: ATK +10 % and bind; talents
// S1 沙暴守卫 (SEARCH: ATK/DEF up; charged — cast with every charge stored — the trait's DEF/RES guard stays on);
// S3 食噬之印 (wider range, ATK ramps to +140 %/+200 % over the skill; charged: every hit marks the target, +20 %
// damage from her per mark, ≤ 5, until the skill ends). 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)
// ===== 卡涅利安 (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, 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 || {};
@@ -1739,15 +1751,17 @@ const kits = {
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
const markKey = (unit) => `billro:mark:${unit.id}`;
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',
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 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 });
},
@@ -1763,14 +1777,19 @@ const kits = {
battle.addBuff(unit, { key: 'billro:s3atk', mods: { atkPct: 0 } });
battle.fx('devour', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ unit, skill, dt }) { // 攻击力逐渐增至+N% (linear over the skill duration)
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) * Math.min(1, unit.mem.billroRamp / Math.max(0.1, skill.duration)) }; unit.markDirty(); }
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');
for (const id of unit.mem.billroMarked || []) { const e = battle.unitById(id); if (e) battle.removeBuff(e, markKey(unit)); }
// "持续至技能结束": 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;
},
@@ -1818,22 +1837,26 @@ const kits = {
}, { owner: unit });
}
if (S3) {
// charged S3: each hit adds a mark (≤ 5) — +20 % damage taken from her per mark until the skill ends
// 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) => {
if (c.source !== unit) return;
const m = c.target.findBuff?.(markKey(unit));
if (m) c.dmg.mul *= 1 + num(bb['attack@damage_scale'], 0.2) * (m.stacks || 1);
}, { owner: unit });
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg?.isAttack || e.side !== 'enemy' || !e.alive || !skillActive(unit) || !unit.mem.billroCharged) return;
battle.addBuff(e, { key: markKey(unit), refresh: 'stack', maxStacks: 5, duration: Math.max(0.1, (unit.skill?.timeLeft ?? 0) + 0.1), source: unit, visible: true });
unit.mem.billroMarked?.add(e.id);
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 damaged by her AoE skill attack — the splash victims
// too (a SkillSpec attack.onHit only sees the main target)
// "每次攻击对目标造成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;
+2 -1
View File
@@ -1754,7 +1754,8 @@ function copyInto(battle, t, src, scale, ranged) {
const p = t.profile;
if (p) {
p.attack = ranged ? 'ranged' : 'melee';
p.projectile = ranged ? (sp.projectile && sp.projectile !== 'none' && sp.projectile !== 'orb' ? sp.projectile : 'bolt') : 'none';
// (a 阵法术师 / 轰击术师's instant 'beam' is their every-enemy-in-range shape: the copy fires a plain bolt)
p.projectile = ranged ? (sp.projectile && sp.projectile !== 'none' && sp.projectile !== 'orb' && sp.projectile !== 'beam' ? sp.projectile : 'bolt') : 'none';
p.canHitFly = ranged ? true : !!sp.canHitFly;
if (!(sp.dmgType === 'heal' || sp.dmgType === 'none' || sp.noAttack || sp.noAttackUnlessSkill)) p.dmgType = sp.dmgType;
p.heal = null;
+3 -1
View File
@@ -730,7 +730,9 @@ function applyManifoldCopy(battle, unit, target, scale) {
const p = unit.profile;
if (p) {
p.attack = ranged ? 'ranged' : 'melee';
p.projectile = ranged ? (tp.projectile && tp.projectile !== 'none' && tp.projectile !== 'orb' ? tp.projectile : 'bolt') : 'none';
// the talent's list of copied attributes (生命上限 … 伤害类型) names no attack shape: a 阵法术师 / 轰击术师's instant
// 'beam' (rangeAoe: every enemy in range) becomes a plain single-target bolt, like a healer's orb
p.projectile = ranged ? (tp.projectile && tp.projectile !== 'none' && tp.projectile !== 'orb' && tp.projectile !== 'beam' ? tp.projectile : 'bolt') : 'none';
p.canHitFly = ranged ? true : !!tp.canHitFly;
// 初始伤害类型(不攻击、治疗类型则不继承)
if (!(tp.dmgType === 'heal' || tp.dmgType === 'none' || tp.noAttack || tp.noAttackUnlessSkill)) p.dmgType = tp.dmgType;
+23 -5
View File
@@ -5,10 +5,15 @@
// data fields (dmgType/attackKind/projectile/canHitFly/targetPriority from data/chess.json) → kit.trait overrides.
// Profile fields:
// attack 'melee'|'ranged' dmgType 'phys'|'arts'|'true'|'heal'|'none'
// projectile 'none'|'arrow'|'bolt'|'bomb'|'lob'|'orb'|'drone'|'boomerang' ('boomerang': out to the target and back to
// the thrower, ai.js throwBoomerang) boomerang bool (回环射手: keeps 'boomerang')
// projectile 'none'|'beam'|'arrow'|'bolt'|'bomb'|'lob'|'orb'|'drone'|'boomerang' ('beam': an instant hit drawn as a
// line; 'boomerang': out to the target and back to the thrower, ai.js throwBoomerang)
// boomerang bool (回环射手: keeps 'boomerang')
// canHitFly bool maxTargets n (≥1) hitAllBlocked bool (attack every blocked enemy)
// allInRange bool splashRadius tiles splashScale (× damage for splash victims)
// allInRange bool (every enemy on the range at once) splashRadius tiles (around the struck target)
// rangeAoe bool (a 锁定攻击范围 AoE without a projectile — SUB table or kit trait, applied by resolveProfile after
// every override: allInRange + instant 'beam' hits on a ranged profile; only selectable enemies are
// struck — a stealthed one is not, unless revealed or blocked; PRTS 作战机制 §AOE伤害判定)
// splashScale (× damage for splash victims)
// splashOthersOnly bool groundOnly bool hits n (damage instances per attack)
// chain {count, falloff, radius, sluggish} heal {mode:'single'|'multi'|'chain', count, falloff, farMul, elementHealRatio}
// priority 'fly'|'lowDef'|'ranged'|'lowestHp'|'highestHp'|'nearest'|'farthest'|'notBurst'|null
@@ -297,8 +302,14 @@ export const SUB = Object.freeze({
return front ? (unit.profile.frontScale ?? 1.5) : 1;
} }),
// --- CASTER
// "群体法术伤害" names two shapes: the 扩散术师 splash 1.1 tiles around the struck target (PRTS 溅射半径一览; Arknights
// Terra Wiki, Splash Caster), while the 轰击术师 ("超远距离的群体法术伤害") and the 阵法术师 strike every enemy inside the
// attack range at once, the same damage near and far — community report E3 after 0.1.0. Primary for the 轰击术师: PRTS
// 作战机制 §AOE伤害判定 names 伊芙利特's 炎爆 a 锁定攻击范围 AoE, and 炎爆 is her next-attack skill ("下次攻击造成…",
// PRTS 伊芙利特 S2); for the 阵法术师: Terra Wiki, Phalanx Caster (secondary) and PRTS 林 S3 备注. PRTS 溅射半径一览
// documents no splash radius for either (supporting only: it omits the 撼地者 too)
splashcaster: P({ splashRadius: 1.1 }),
blastcaster: P({ splashRadius: 1.1, projectile: 'bomb' }),
blastcaster: P({ rangeAoe: true }),
chain: P({ chain: { count: 3, falloff: 0.15, radius: 1.8, sluggish: 0.5 } }),
funnel: P({ projectile: 'drone', install: installFunnel,
dmgMul: (battle, unit, target) => {
@@ -309,7 +320,7 @@ export const SUB = Object.freeze({
} }),
mystic: P({ install: installMystic,
hitsFn: (battle, unit) => { const n = 1 + (unit.trait.stored ?? 0); unit.trait.stored = 0; return n; } }),
phalanx: P({ noAttackUnlessSkill: true, splashRadius: 1.1, install: installPhalanx }),
phalanx: P({ noAttackUnlessSkill: true, rangeAoe: true, install: installPhalanx }),
primcaster: P({}),
corecaster: P({}),
// --- MEDIC
@@ -484,6 +495,13 @@ export function resolveProfile(def, kitTrait = null) {
if (p.dmgType !== 'heal' && p.heal) p.heal = null;
if (p.dmgType === 'none') p.noAttack = true;
if (kitTrait) Object.assign(p, kitTrait);
// a 锁定攻击范围 AoE (阵法术师, 轰击术师) strikes every enemy on its range and has no projectile: they are struck at the
// same moment ('beam' = instant hits, drawn as a line to each victim — PRTS 作战机制 "在攻击前摇结束时选取范围内的全体
// 目标,同时造成伤害"); the data's generic ranged projectile ('bolt') would land them one by one
if (p.rangeAoe) {
p.allInRange = true;
if (p.attack === 'ranged') p.projectile = 'beam';
}
return p;
}