feedback1 integrate: follow-ups from the workstream notes (official rules)

- an operator's radius area damage skips unblocked 隐匿 and untargetable enemies (Battle.foesInRadius;
  PRTS 作战机制 AOE伤害判定), the 逐火 embers included
- PRTS 溅射半径一览: 投掷手 0.9, 链术师 1.7, 格雷伊 1.0; 迷迭香 S2 radius 1.5
- 阿罗玛's first-attack mark is shared between 阿罗玛 and leaves with its setter (PRTS 备注)
- 奥术法阵 on the field: 炎佑's damage silences its target (PRTS 盟约记录 备注)
- skills restart from their initial cooldown when a 重生 ends (PRTS 特殊机制)
- 高准度伦蒂尼姆城防自行炮: lock in range, 3x3 area, every 0.5 s up to 10 shots, 失衡/控制免疫 meanwhile
- 琳琅诗怀雅 S3 coins go to range 2-4 in front and the units she blocks
- 阿戈尔 devour is a 物理流失 reduced by the target's DEF
This commit is contained in:
sganggs committed 2026-10-03 07:57:28 +08:00
1 parent 4e68100065
commit 75e2f02590
21 files changed
+346 -112

No files matched your search

+17 -16
View File
@@ -103,7 +103,7 @@
import { absoluteRangeKeys, sortEnemyTargets, canTargetEnemy, aggroCmp } from '../../targeting.js';
import { aggregateMods } from '../../buffs.js';
import { COLS, ROWS, PULL_STOP_RADIUS } from '../../constants.js';
import { COLS, ROWS, PULL_STOP_RADIUS, CHAIN_RADIUS } from '../../constants.js';
import { rotateOffset } from '../../dir.js';
import { bodyDist, bodyInKeys, bodyInRadius, bodyKeys } from '../../body.js';
import { hasHp } from '../../damage.js';
@@ -449,7 +449,7 @@ function lemuen(bb, chess, def) {
};
const blast = (battle, unit, atk, x, y) => {
battle.fx('bombard', { x, y, id: unit.id, r: d2 });
for (const e of battle.enemiesInRadius(x, y, d2)) {
for (const e of battle.foesInRadius(x, y, d2)) {
if (e.s.flags.untargetable) continue;
const d = bodyDist(e, x, y);
battle.dealDamage(unit, e, { amount: atk * (d <= d1 + 1e-9 ? s1 : s2), type: 'phys', isSkill: true, isSplash: true, tags: ['skill', 'bombard'] });
@@ -1078,7 +1078,7 @@ function pasngr(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {};
const skillGrid = def?.skill?.rangeGrid?.length ? def.skill.rangeGrid : null;
const strike = (battle, unit, first, scale) => {
const ch0 = unit.profile?.chain || { count: 4, falloff: 0.15, radius: 1.8 };
const ch0 = unit.profile?.chain || { count: 4, falloff: 0.15, radius: CHAIN_RADIUS };
// elite module (电磁调节器) upgrades the skill's chain too: skill@chain.atk_scale / skill@sluggish
const ch = { ...ch0, falloff: tb['skill@chain.atk_scale'] != null ? 1 - num(tb['skill@chain.atk_scale']) : num(ch0.falloff, 0.15) };
const count = Math.max(1, Math.floor(num(bb['chain.max_target'], ch.count || 4)));
@@ -1091,7 +1091,7 @@ function pasngr(bb, chess, def) {
battle.dealDamage(unit, prev, { amount: unit.s.atk * scale * Math.pow(1 - num(ch.falloff, 0.15), i), type: 'arts', isSkill: true, isAttack: true, tags: ['skill', 'storm'] });
if (slug > 0 && prev.alive) battle.applyStatus(prev, 'sluggish', { duration: slug, source: unit });
let best = null, bd = Infinity;
for (const x of battle.enemiesInRadius(prev.x, prev.y, ch.radius || 1.8)) {
for (const x of battle.foesInRadius(prev.x, prev.y, ch.radius || CHAIN_RADIUS)) {
if (hit.has(x.id) || !canTargetEnemy(unit, x, ANY)) continue;
const d = bodyDist(x, prev.x, prev.y);
if (d < bd - 1e-9) { bd = d; best = x; }
@@ -1100,7 +1100,7 @@ function pasngr(bb, chess, def) {
}
};
// the chain of her profile (trait / module: bounce count, falloff, 停顿) with skill overrides
const chainOf = (unit, o) => ({ ...(unit.profile?.chain || { count: 4, falloff: 0.15, radius: 1.8, sluggish: 0.5 }), ...o });
const chainOf = (unit, o) => ({ ...(unit.profile?.chain || { count: 4, falloff: 0.15, radius: CHAIN_RADIUS, sluggish: 0.5 }), ...o });
const skills = {
// S1 电能之触: next attack at atk_scale × ATK, bouncing over max_target enemies with a sluggish-s 停顿 (the module's
// skill@pasngr_s_1.chain.atk_scale sets its falloff)
@@ -1140,7 +1140,7 @@ function pasngr(bb, chess, def) {
const h = battle.every(iv, () => {
if (!live(unit) || unit.deploySeq !== seq) { h.cancel(); return; } // the storm ends when she leaves the field
if (++k >= n) h.cancel();
const zone = battle.enemiesInRadius(cx, cy, STORM_RADIUS).filter((e) => canTargetEnemy(unit, e, ANY));
const zone = battle.foesInRadius(cx, cy, STORM_RADIUS).filter((e) => canTargetEnemy(unit, e, ANY));
const e = battle.rng.pick(zone);
if (e) strike(battle, unit, e, scale);
}, { owner: unit });
@@ -1271,7 +1271,7 @@ function pepe(bb, chess, def) {
if (ctx.attacker !== unit) return;
const t = ctx.targets[0];
const r = num(ctx.profile?.splashRadius, 1);
unit.mem.pepeBoost = !!t && battle.enemiesInRadius(t.x, t.y, r).length >= cnt;
unit.mem.pepeBoost = !!t && battle.foesInRadius(t.x, t.y, r).length >= cnt;
}, { owner: unit });
battle.on('hit', (ctx) => { if (ctx.source === unit && ctx.dmg.isAttack && unit.mem.pepeBoost) ctx.dmg.mul *= sc; }, { owner: unit });
battle.on('attack', (ctx) => { if (ctx.attacker === unit) unit.mem.pepeBoost = false; }, { owner: unit, priority: -100 });
@@ -1505,7 +1505,7 @@ function reed2(bb, chess, def) {
battle.after(0, () => {
if (!live(unit)) return;
battle.fx('scorchBurst', { x, y, id: unit.id, r: aoeR });
for (const e of battle.enemiesInRadius(x, y, aoeR, true)) { // splash around the victim: 中点判定
for (const e of battle.foesInRadius(x, y, aoeR, true)) { // splash around the victim: 中点判定
if (!e.alive) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * aoe, type: 'arts', isSkill: true, isSplash: true, tags: ['skill', 'scorch'] });
scorch(battle, unit, e);
@@ -2024,7 +2024,8 @@ function mlyss(bb, chess, def) {
// ------------------------------------------------------------------------------------------------------------------
// 迷迭香 chess_char_6_12 (投掷手) — S2 末梢阻断; 歼灭战装备; 感知稳定
const ROSMON_SPLASH_MUL = 1.3; // [ASSUMED] "溅射范围扩大" (not in data)
/** S2 末梢阻断 "溅射范围扩大": radius 1.5 (PRTS 溅射半径一览, 技能: 迷迭香 末梢阻断 1.5; ×1.3 [ASSUMED] until 0.1.1). */
const ROSMON_S2_SPLASH = 1.5;
function rosmon(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
@@ -2075,7 +2076,7 @@ function rosmon(bb, chess, def) {
onStart({ unit }) {
const p = unit.profile;
unit.mem.rosSaved = { r: p.splashRadius, n: p.shockTimes };
p.splashRadius = num(p.splashRadius, 1) * ROSMON_SPLASH_MUL;
p.splashRadius = Math.max(num(p.splashRadius, 0.9), ROSMON_S2_SPLASH);
p.shockTimes = num(p.shockTimes, 2) + Math.floor(num(bb.add_times));
},
onEnd({ unit }) {
@@ -2176,7 +2177,7 @@ function angel2(bb, chess, def) {
if (!c) return;
const r = c.tileR, col = c.tileC;
battle.fx('airstrike', { x: col, y: r, id: unit.id, r: AIRSTRIKE_RADIUS });
for (const e of battle.enemiesInRadius(col, r, AIRSTRIKE_RADIUS)) {
for (const e of battle.foesInRadius(col, r, AIRSTRIKE_RADIUS)) {
if (e.alive && !e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['attack@cannon_atk_scale'], 1), type: 'phys', isSkill: true, isSplash: true, tags: ['skill', 'delivery'] });
}
const waiting = battle.allyUnits.filter((a) => a.kind === 'op' && a.ownerId === unit.ownerId && !a.alive && !a.removed && a !== unit
@@ -2251,7 +2252,7 @@ function angel2(bb, chess, def) {
sortEnemyTargets(battle, u, cands, null);
const c = cands[0];
battle.fx('airstrike', { x: c.x, y: c.y, id: unit.id, r: AIRSTRIKE_RADIUS });
for (const e of battle.enemiesInRadius(c.x, c.y, AIRSTRIKE_RADIUS, true)) { // splash around the target: 中点判定
for (const e of battle.foesInRadius(c.x, c.y, AIRSTRIKE_RADIUS, true)) { // splash around the target: 中点判定
if (e.alive && !e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * sc, type: 'phys', isSkill: true, isSplash: true, tags: ['talent', 'airstrike'] });
}
}, { owner: unit });
@@ -2420,7 +2421,7 @@ function qiubai(bb, chess, def) {
if (!unit.alive || unit.deploySeq !== seq) return;
const x = target.x, y = target.y; // (a fallen target keeps its last position)
battle.fx('aoe', { x, y, id: unit.id });
for (const e of battle.enemiesInRadius(x, y, 1.2, true)) { // splash around the target: 中点判定
for (const e of battle.foesInRadius(x, y, 1.2, true)) { // splash around the target: 中点判定
if (e.alive && !e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.aoe_scale, 1), type: 'arts', isSkill: true, isSplash: e !== target, tags: ['skill'] });
}
}, { owner: unit });
@@ -2507,7 +2508,7 @@ function halo2(bb, chess, def) {
let prev = target, px = target.x, py = target.y;
for (let i = 0; i < jumps; i++) {
let best = null, bd = Infinity;
for (const e of battle.enemiesInRadius(px, py, R)) {
for (const e of battle.foesInRadius(px, py, R)) {
if (e === prev || !canTargetEnemy(unit, e, ANY)) continue;
const d = bodyDist(e, px, py);
if (d < bd - 1e-9) { bd = d; best = e; }
@@ -2532,7 +2533,7 @@ function halo2(bb, chess, def) {
onHit({ battle, unit, target }) {
if (!target) return;
const R = num(bb.ability_range_radius, 2), k = Math.max(0, Math.floor(num(bb.max_target, 2)));
const near = battle.enemiesInRadius(target.x, target.y, R).filter((e) => e !== target && canTargetEnemy(unit, e, ANY))
const near = battle.foesInRadius(target.x, target.y, R).filter((e) => e !== target && canTargetEnemy(unit, e, ANY))
.sort((a, b) => bodyDist(a, target.x, target.y) - bodyDist(b, target.x, target.y) || a.spawnSeq - b.spawnSeq)
.slice(0, k);
for (const e of near) {
@@ -2870,7 +2871,7 @@ function whitw2(bb, chess, def) {
}
}
const near = new Set();
for (const d of D) for (const e of battle.enemiesInRadius(d.x, d.y, R)) if (ok(e)) near.add(e);
for (const d of D) for (const e of battle.foesInRadius(d.x, d.y, R)) if (ok(e)) near.add(e);
if (slow) for (const e of near) battle.addBuff(e, { key: 'whitw2:slow', duration: 0.2, refresh: 'replace', mods: { moveMul: Math.max(0, 1 + slow) }, source: unit });
unit.mem.droneAcc += dt;
if (unit.mem.droneAcc + 1e-9 >= 1) {