diff --git a/docs/SIM.md b/docs/SIM.md index 1960727a..0611df7b 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -899,7 +899,7 @@ table: `hitSleep` (targets and damages sleeping enemies — "可以攻击沉睡 | closerange, underminer, primcaster, corecaster, ritualist, summoner, counsellor, pioneer, fearless, fighter, protector, guardian, primprotector, executor, duelist | plain profile (numbers from data; skills/talents via kits). underminer module: weaken 10 % ATK 2 s on hit | | longrange | lowest DEF first | | aoesniper / splashcaster | splash 1.1 tiles around the struck target at full damage (PRTS 溅射半径一览: 扩散术师 1.1; Arknights Terra Wiki, Splash Caster). [OPEN] the same table gives 炮手 1.0 (none in the pool) and 格雷伊 1.0 | -| blastcaster | `rangeAoe`: every selectable enemy on its line at once, the same damage near and far, instant (`'beam'`) — "超远距离的群体法术伤害" is the whole line, not a splash (PRTS 溅射半径一览 lists no splash for it; Terra Wiki, Blast Caster; PRTS 作战机制 §AOE伤害判定: 伊芙利特's 炎爆 is a 锁定攻击范围 AoE; community report E3). A stealthed enemy is not struck unless it is revealed or blocked (a 锁定范围 AoE cannot hit a 隐匿 unit, PRTS 作战机制) | +| blastcaster | `rangeAoe`: every selectable enemy on its line at once, the same damage near and far, instant (`'beam'`) — "超远距离的群体法术伤害" is the whole line, not a splash (primary: PRTS 作战机制 §AOE伤害判定 names 伊芙利特's 炎爆 a 锁定攻击范围 AoE, and 炎爆 is her next-attack skill "下次攻击造成…" (PRTS 伊芙利特 S2), so a 轰击术师 normal attack; secondary: Terra Wiki, Blast Caster; supporting: PRTS 溅射半径一览 documents no splash radius for it; community report E3). A stealthed enemy is not struck unless it is revealed or blocked (a 锁定范围 AoE cannot hit a 隐匿 unit, PRTS 作战机制) | | bombarder | ground-only splash 1.0 + aftershock(s) at 50 % ATK (bb append_atk_scale / times). [OPEN] PRTS 溅射半径一览 gives 投掷手 0.9 | | hunter | 8 bullets (bb value), ×1.2 ATK (bb atk_scale), reloads 1/s after 1 s without attacking; can't attack when empty | | loopshooter | 回环射手 (user playtest #3): every attack throws a boomerang (`ai.js throwBoomerang`, projectile `'boomerang'`) out to the target at 15 tiles/s — it hits on arrival — and back to the thrower's current position at 3.75 tiles/s without damage (PRTS 跃跃 "投射物飞行速度15,返回时飞行速度3.75"); attacks only while holding it (every boomerang thrown caught — "必须回收全部回旋投掷物才可以进行下一次攻击", `unit.trait.boomerangsOut`) and with the attack cooldown ready, so the real interval is the longer of the two; a target dead mid-flight is not hit (it still flies to the last position and back); knocked out / withdrawn ⇒ lost, a redeployed thrower holds a fresh one; 跃跃 S2's extra boomerangs share the one flight (cnt hits) | @@ -907,7 +907,7 @@ table: `hitSleep` (targets and damages sleeping enemies — "可以攻击沉睡 | chain | chain N (trait text/bb max_target) with −15 % per jump (bb chain.atk_scale), 1.8-tile jumps, sluggish on each hit. [OPEN] PRTS 溅射半径一览 gives 链术师 1.7 | | funnel | drone damage 20 % → +15 %/hit on the same target → 110 % (bb init/delta/max) | | mystic | stores up to 3 (bb times) attacks while idle, fires them all at once | -| phalanx | no attack & DEF +200 %, RES +20 (bb) while the skill is off; while on, `rangeAoe`: each attack strikes every selectable enemy on its range at once (blocked enemies included; a stealthed one only when revealed or blocked), the same damage near and far, instant (`'beam'`) — "群体法术伤害" (PRTS 溅射半径一览 lists no splash for it; Terra Wiki, Phalanx Caster: "attacks hit all enemies within their range"; PRTS 林 S3 备注 "单次普攻最多触发1次效果"; community report E3: it used to be one bolt + a 1.1 splash). 卡涅利安's charged S1 keeps the skill-off trait, 不攻击 included (kit `canAttack`, PRTS 备注) | +| phalanx | no attack & DEF +200 %, RES +20 (bb) while the skill is off; while on, `rangeAoe`: each attack strikes every selectable enemy on its range at once (blocked enemies included; a stealthed one only when revealed or blocked), the same damage near and far, instant (`'beam'`) — "群体法术伤害" (secondary: Terra Wiki, Phalanx Caster: "attacks hit all enemies within their range"; supporting: PRTS 林 S3 备注 "单次普攻最多触发1次效果" — one normal attack can kill several — the same 锁定攻击范围 shape as the 轰击术师, and PRTS 溅射半径一览 documents no splash radius for it (it omits the 撼地者 too, so this is not proof); no primary source states a target cap; community report E3: it used to be one bolt + a 1.1 splash). 卡涅利安's charged S1 keeps the skill-off trait, 不攻击 included (kit `canAttack`, PRTS 备注) | | physician | heal the lowest HP% injured ally in range (a skill `targeting.maxTargets` widens any heal profile) | | ringhealer | heal 3 allies | | chainhealer | heal bounces 3× (−25 %, bb chain.*) within 2.5 tiles | diff --git a/server/sim/professions.js b/server/sim/professions.js index 9c9659be..b1522e59 100644 --- a/server/sim/professions.js +++ b/server/sim/professions.js @@ -302,10 +302,12 @@ 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 溅射半径一览 — which lists - // no splash for the 阵法术师 / 轰击术师 — and Arknights Terra Wiki, Splash Caster), while the 轰击术师 ("超远距离的群体法术 - // 伤害") and the 阵法术师 strike every enemy inside the attack range at once, the same damage near and far (Terra Wiki, - // Blast / Phalanx Caster; PRTS 作战机制 §AOE伤害判定: 伊芙利特's 炎爆 is a 锁定攻击范围 AoE) — community report E3 after 0.1.0 + // "群体法术伤害" 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({ rangeAoe: true }), chain: P({ chain: { count: 3, falloff: 0.15, radius: 1.8, sluggish: 0.5 } }), diff --git a/test/sim/feedback1e-aoe.test.js b/test/sim/feedback1e-aoe.test.js index b678e5e7..3bb1f1b8 100644 --- a/test/sim/feedback1e-aoe.test.js +++ b/test/sim/feedback1e-aoe.test.js @@ -3,10 +3,11 @@ // enemies within their range … equal damage to all enemies in range, regardless of distance"; PRTS 作战机制 §AOE伤害判定 // "对攻击范围内的每个可以被选中的敌人进行判定"; PRTS 林 S3 备注 "单次普攻最多触发1次效果" — one normal attack can kill // several), not one target plus a 1.1-tile splash. The same holds for the 轰击术师 line ("超远距离的群体法术伤害": every -// enemy on the line — Terra Wiki Blast Caster; PRTS 作战机制: 伊芙利特's 炎爆 is a 锁定攻击范围 AoE), while the 扩散术师 -// "群体法术伤害" stays a splash of 1.1 tiles around the struck target (PRTS 溅射半径一览 — no row for the 阵法术师 / -// 轰击术师 — and Terra Wiki Splash Caster). Real battles with the real chess (every selectable attacking skill, normal + -// elite), counting the enemies each attack damages. Her kit per PRTS 卡涅利安 备注: a charged S1 keeps the skill-off trait +// enemy on the line — PRTS 作战机制: 伊芙利特's 炎爆, her next-attack skill, is a 锁定攻击范围 AoE; Terra Wiki Blast +// Caster), while the 扩散术师 "群体法术伤害" stays a splash of 1.1 tiles around the struck target (PRTS 溅射半径一览, which +// documents no splash radius for the 阵法术师 / 轰击术师 — supporting only — and Terra Wiki Splash Caster). Real +// battles with the real chess (every selectable attacking skill, normal + elite), counting the enemies each attack +// damages. Her kit per PRTS 卡涅利安 备注: a charged S1 keeps the skill-off trait // (不攻击 + the guard), the charged S3 mark stacks before the damage and is one buff per enemy (the setter's bonus); a // 流形 copy takes no attack shape ('beam' → bolt); `rangeAoe` from any source means every enemy in range, instant. import { test } from 'node:test'; @@ -128,7 +129,7 @@ test('E3 audit: a 扩散术师 still splashes 1.1 tiles around its target — no } }); -test('E3 real stage + real wave: 卡涅利安 targets every enemy on her range with each attack (act2 m01, round 10)', () => { +test('E3 real stage + real wave: 卡涅利安 targets AND damages every enemy on her range with each attack (act2 m01, round 10)', () => { const data = getData({ log: { warn() {}, error() {}, info() {} } }); const mode = data.config.modes.mode_multi_normal; const round = '10'; @@ -150,19 +151,31 @@ test('E3 real stage + real wave: 卡涅利安 targets every enemy on her range w }); const u = h.unit(id); const rows = []; - // (beforeAttack, after every content hook: the targets of the attack and the enemies on her range at that moment) + const hit = new Map(); // attackId → the enemies its damage reached + // (beforeAttack, after every content hook: the targets of the attack and the enemies on her range at that moment; + // the attack that follows takes the next attackId) h.b.on('beforeAttack', (c) => { if (c.attacker !== u) return; const p = effectiveProfile(u); const want = new Set(h.b.enemiesInKeys(u.rangeKeys, u, p)); for (const e of h.b.blockedTargets(u, p)) want.add(e); - rows.push({ got: c.targets.length, want: want.size, all: [...want].every((e) => c.targets.includes(e)) }); + rows.push({ attackId: h.b._attackSeq + 1, got: c.targets.length, want, all: [...want].every((e) => c.targets.includes(e)) }); + }, { priority: -2000 }); + h.b.on('damaged', (c) => { + if (c.source !== u || !c.dmg?.isAttack) return; + if (!hit.has(c.dmg.attackId)) hit.set(c.dmg.attackId, new Set()); + hit.get(c.dmg.attackId).add(c.target); }, { priority: -2000 }); h.runToEnd(rc.combatTimeLimit + 5); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); - const crowded = rows.filter((r) => r.want >= 3); + const crowded = rows.filter((r) => r.want.size >= 3); assert.ok(crowded.length >= 1, `S${skillIndex + 1}: attacks into a crowd (${crowded.length}/${rows.length})`); - for (const r of rows) assert.ok(r.all && r.got === r.want, `S${skillIndex + 1}: ${r.got} targets of ${r.want} enemies on her range`); + for (const r of rows) { + assert.ok(r.all && r.got === r.want.size, `S${skillIndex + 1}: ${r.got} targets of ${r.want.size} enemies on her range`); + // the damage itself: one instant hit on every one of them, in the same attack (no projectile, no splash) + const got = hit.get(r.attackId) ?? new Set(); + assert.ok(got.size === r.want.size && [...r.want].every((e) => got.has(e)), `S${skillIndex + 1}: attack ${r.attackId} damaged ${got.size} of the ${r.want.size} enemies on her range`); + } } });