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:
1 parent
4e68100065
commit
75e2f02590
21 files changed
+346
-112
No files matched your search
@@ -1567,6 +1567,26 @@ export class Battle {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The enemies within `r` (as enemiesInRadius) an ally-side area effect can select — PRTS 作战机制 §AOE伤害判定 "AOE的判定是
|
||||
* 对攻击范围内的每个可以被选中的敌人进行判定", 隐匿 "隐匿状态下的单位一般无法被敌方的索敌机制和Buff选择器选中为目标": no
|
||||
* untargetable enemy and no 隐匿 one unless revealed or blocked (tile selectors — enemiesInKeys / canTargetEnemy — already
|
||||
* skip them). Flying and asleep enemies stay the caller's choice. Enemy-side effects on other enemies (auras, heals) and
|
||||
* physical collisions keep enemiesInRadius. Player report #8 after 0.1.0 (the 逐火 余烬): until 0.1.1 profession splash
|
||||
* and skill circles still reached an unblocked 隐匿 enemy.
|
||||
*/
|
||||
foesInRadius(x, y, r, centre = false) {
|
||||
const out = this.enemiesInRadius(x, y, r, centre);
|
||||
let n = 0;
|
||||
for (const e of out) {
|
||||
const f = e.s.flags;
|
||||
if (f.untargetable || (f.stealth && !f.reveal && !e.blockedBy)) continue;
|
||||
out[n++] = e;
|
||||
}
|
||||
out.length = n;
|
||||
return out;
|
||||
}
|
||||
|
||||
alliesInRadius(x, y, r, ownerId = null, { includeDevices = false } = {}) {
|
||||
const out = [];
|
||||
const r2 = r * r + 1e-9;
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@
|
||||
// flutters inside its own tile); an `attract` (诱导) status walks it to the status point instead (moveAttracted);
|
||||
// both re-plan the route when released (恐惧 outranks 诱导).
|
||||
|
||||
import { ATTACK_PAUSE, ALLY_COLLIDER_RADIUS, MOVE_SCALE, PROJECTILE_SPEEDS, PROJECTILE_SPEED, BOOMERANG_RETURN_SPEED, COLS } from './constants.js';
|
||||
import { ATTACK_PAUSE, ALLY_COLLIDER_RADIUS, MOVE_SCALE, PROJECTILE_SPEEDS, PROJECTILE_SPEED, BOOMERANG_RETURN_SPEED, COLS, CHAIN_RADIUS } from './constants.js';
|
||||
import { sortEnemyTargets, sortAllyTargets, canTargetEnemy, canTargetAlly, tileKeyOf } from './targeting.js';
|
||||
import { reduceElement } from './damage.js';
|
||||
import { straightClear } from './grid.js';
|
||||
@@ -235,7 +235,7 @@ export function resolveHit(b, u, prof, target, info, x, y) {
|
||||
if (prof.splashRadius > 0) {
|
||||
const r = prof.splashRadius;
|
||||
const sc = prof.splashScale ?? 1;
|
||||
for (const e of b.enemiesInRadius(x, y, r, true)) {
|
||||
for (const e of b.foesInRadius(x, y, r, true)) {
|
||||
if (e === target) continue;
|
||||
if (prof.groundOnly && e.isFlying) continue;
|
||||
if (!prof.canHitFly && e.isFlying && !prof.splashHitsFly) continue;
|
||||
@@ -252,7 +252,7 @@ export function resolveHit(b, u, prof, target, info, x, y) {
|
||||
const n = Math.max(1, prof.chain.count || 3);
|
||||
for (let k = 1; k < n; k++) {
|
||||
let best = null, bd = Infinity;
|
||||
for (const e of b.enemiesInRadius(prev.x, prev.y, prof.chain.radius || 1.8)) {
|
||||
for (const e of b.enemiesInRadius(prev.x, prev.y, prof.chain.radius || CHAIN_RADIUS)) {
|
||||
if (hit.has(e.id) || !canTargetEnemy(u, e, prof)) continue;
|
||||
const d = Math.hypot(e.x - prev.x, e.y - prev.y);
|
||||
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && best && e.spawnSeq < best.spawnSeq)) { bd = d; best = e; }
|
||||
|
||||
@@ -216,3 +216,6 @@ export const BOSS_POOL_MIN_HP = 1;
|
||||
* the base attributes) stay multipliers.
|
||||
*/
|
||||
export const DIRECT_BONUS_STACKING = 'add';
|
||||
|
||||
/** 链术师 jump radius (PRTS 溅射半径一览, 特性: "链术师 … 1.7"; 1.8 until 0.1.1). */
|
||||
export const CHAIN_RADIUS = 1.7;
|
||||
@@ -36,6 +36,7 @@
|
||||
// death 10), layerGain −100 (after 魔王-style modifiers).
|
||||
|
||||
import * as S from '../support/index.js';
|
||||
import { mitigate } from '../../damage.js';
|
||||
import { spawnYanyou } from '../tokens.js';
|
||||
import { kjeragColdWind } from '../devices.js';
|
||||
import * as items from '../items.js';
|
||||
@@ -335,8 +336,10 @@ function installLaterano(battle, pid, bb, members) {
|
||||
* members the tiles in front of
|
||||
* those (chain); never themselves, a unit already marked by them or a unit that marked them. The marker gains the base
|
||||
* ATK (atkFlat) and block count of everything it marked; then each mark makes its target lose damage_value HP as a
|
||||
* 流失 (battle.loseHp: no DEF, shields, dodge or damage multipliers — research "5000-point physical 流失"; the kill is
|
||||
* credited to the marker) in marking order. A unit that died cancels its remaining marks (as target and as marker).
|
||||
* 物理流失 (PRTS 盟约记录: "造成5000点物理流失", 修正 "【吞噬】的物理流失来源为被付与目标自身;单位被【吞噬】击杀时,击杀来源始终为
|
||||
* 对应标记的付与来源"; PRTS 作战机制: a 物理流失 "会受到目标当前防御力…影响而相应衰减") — less the target's DEF as a physical hit
|
||||
* (its own source: no DEF ignore), then battle.loseHp: no shields, dodge or damage multipliers (DEF-free until 0.1.1); the
|
||||
* kill is credited to the marker — in marking order. A unit that died cancels its remaining marks (as target and as marker).
|
||||
* Each devoured operator adds its tier to 阿戈尔 once (IN_BATTLE gain, disabled in 联防 / boss fields).
|
||||
* Tokens / devices / empty tiles are never devoured.
|
||||
*/
|
||||
@@ -382,7 +385,7 @@ function devour(battle, pid, bb, members) {
|
||||
for (const [m, t] of marks) {
|
||||
if (!t.alive || !m.alive) continue;
|
||||
S.fxOn(battle, 'devour', t, 'bond:egirShip', 'devour', { from: m.id });
|
||||
if (amount > 0) battle.loseHp(t, amount, { source: m, tags: ['bond:egir:devour'] });
|
||||
if (amount > 0) battle.loseHp(t, mitigate(amount, 'phys', t.s), { source: m, tags: ['bond:egir:devour'] });
|
||||
if (!layered.has(t)) {
|
||||
layered.add(t);
|
||||
S.gainLayers(battle, { playerId: pid, bonds: 'egirShip', n: S.tierOf(t), source: m, reason: 'bond' });
|
||||
@@ -479,7 +482,7 @@ function installKazimierz(battle, pid, bb, members) {
|
||||
battle.every(iv, () => {
|
||||
for (const m of members) {
|
||||
if (!S.onField(m) || !m.blocking || !m.blocking.length) continue;
|
||||
const hits = battle.enemiesInRadius(m.x, m.y, radius);
|
||||
const hits = battle.foesInRadius(m.x, m.y, radius);
|
||||
if (!hits.length) continue;
|
||||
const amount = scale * m.s.atk;
|
||||
for (const e of hits) {
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
// gauge whose burst is the official one (termDescription ba.dt.erosion: "永久降低100点防御力并受到800点物理伤害").
|
||||
// DOT — damage zones (污染秽蚀: true damage, one tick per second however many cover a unit; 毒雾, 燃烧区域),
|
||||
// bleeding (removed by healing), pulsing auras.
|
||||
// INVISIBLE — permanent `stealth` flag (engine: untargetable unless blocked or revealed). Radius area damage
|
||||
// (Battle.enemiesInRadius: profession splash around a struck target, many skills' circles) still hits
|
||||
// an unblocked one — a known deviation: officially an AoE only judges the enemies it can select (PRTS
|
||||
// 作战机制 §AOE伤害判定; ba.invisible vs ba.camou); tile selectors (enemiesInKeys) already skip it.
|
||||
// INVISIBLE — permanent `stealth` flag (engine: untargetable unless blocked or revealed). An operator's radius area
|
||||
// damage skips an unblocked one too (Battle.foesInRadius: profession splash around a struck target,
|
||||
// skill circles — PRTS 作战机制 §AOE伤害判定 "对攻击范围内的每个可以被选中的敌人进行判定"; until 0.1.1 it
|
||||
// still hit it); tile selectors (enemiesInKeys) always skipped it.
|
||||
// REFLECTION — 折射 (ba.refraction "生效时,法术抗性+70"): RES +refracting.magic_resistance while NOT silenced
|
||||
// (the ability line is SILENCE-flagged: silencing turns it off); 镜膜 also gets max HP +100 % while on.
|
||||
// SPECIAL — mostly stats; prisoners, 穿刺手, 暴虐兵长, 镜卫, 动力装甲 … below.
|
||||
@@ -68,7 +68,7 @@
|
||||
// hidden tabs (shared/protocol.js fxForm).
|
||||
// Custom hook: 'lpLoss' {amount, reason, source} — leader "扣除目标生命" effects; also summed into result.lpLoss.
|
||||
|
||||
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE, PROJECTILE_SPEEDS } from '../constants.js';
|
||||
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE, PROJECTILE_SPEEDS, ALLY_COLLIDER_RADIUS } from '../constants.js';
|
||||
import { canTargetAlly, sortAllyTargets, aggroCmp } from '../targeting.js';
|
||||
import { mitigate, periodicDamage } from '../damage.js';
|
||||
|
||||
@@ -123,8 +123,11 @@ const GRAB_RADIUS_SCALE = 2.5, GRAB_MAX_PREY = 3, GRAB_DELAY = 0.5;
|
||||
/** 孽罪奇美拉 污染模式 aura (PRTS 天赋 "自身半径1.2范围内的所有单位…每0.5秒受到50真实持续伤害(同类效果取最高)"): radius,
|
||||
* damage period (s). */
|
||||
const CHIMERA_AURA_RADIUS = 1.2, CHIMERA_AURA_EVERY = 0.5;
|
||||
/** Bombardments per 自行炮 Cannon cast ("数次") [ASSUMED]. */
|
||||
const CANNON_SHOTS = 3;
|
||||
/** 自行炮 Cannon (PRTS 高准度伦蒂尼姆城防自行炮 技能 "以攻击范围内生命上限最高的我方单位为中心,生成9格的炮击区域,6s内每0.5s对炮击
|
||||
* 区域中生命比例最高的我方单位进行攻击…(最多进行10次攻击)"): shot interval (s), shot cap; 3 shots 1 s apart [ASSUMED] until
|
||||
* 0.1.1. 备注: during the skill animation 失衡免疫 + 晕眩 / 冻结 / 浮空 / 沉睡免疫 — held over the bombardment [ASSUMED length]. */
|
||||
const CANNON_EVERY = 0.5, CANNON_SHOTS = 10;
|
||||
const CANNON_IMMUNE = Object.freeze(['stun', 'freeze', 'cold', 'levitate', 'sleep']);
|
||||
/** 枯朽之种 summoned per BornBugs cast ("数个") [ASSUMED]. */
|
||||
const BUGS_PER_CAST = 3;
|
||||
/** Radius of 骸骨拷打者's "周围" death sensing when its talent has no Attack.range_radius [ASSUMED]. */
|
||||
@@ -737,6 +740,14 @@ function freeAllPrisoners(b) {
|
||||
* knocked out while feared (叙拉古 / 妮芙: 恐惧 makes it unblockable) stayed unblockable — so, 隐匿, untargetable — as an
|
||||
* ember until the fear ran out.
|
||||
*/
|
||||
/**
|
||||
* The end of a 重生 (PRTS 特殊机制 §重生 "重生结束时,重置自身的通用技能与当前形态的技能冷却为初始冷却"): every ability with a
|
||||
* cooldown starts again from its initial cooldown (until 0.1.1 the countdowns ran on through the 重生).
|
||||
*/
|
||||
function rebirthCooldowns(e) {
|
||||
for (const s of (e.mem.ab && e.mem.ab.list) || []) if (s.cd != null) s.left = num(s.icd, 0);
|
||||
}
|
||||
|
||||
function rebirthCleanse(b, e) {
|
||||
for (const x of e.buffs.slice()) {
|
||||
if (x.persist || (x.source && x.source.side === 'enemy')) continue;
|
||||
@@ -764,6 +775,7 @@ function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = nu
|
||||
if (invincible > 0) b.addBuff(e, { key: `${key}:inv`, duration: invincible, visible: true, flags: { invulnerable: true } });
|
||||
if (e.route) e.route.pts = null;
|
||||
e.atkCd = 0;
|
||||
rebirthCooldowns(e);
|
||||
setForm(b, e, 'form2', 'revive', { kind: 'reborn' });
|
||||
};
|
||||
return {
|
||||
@@ -900,7 +912,7 @@ function husk({ hits, delay, stealthy = true, unblock = false, onHusk = null, ke
|
||||
b.addBuff(e, { key: `${key}:reborn`, duration: HUSK_REBIRTH, flags: { invulnerable: true, untargetable: true, unblockable: true, noMove: true, noDisplace: true } });
|
||||
if (e.route) e.route.pts = null;
|
||||
setForm(b, e, 'husk', 'ember', { hits, dur: HUSK_REBIRTH + delay });
|
||||
if (onHusk) b.after(HUSK_REBIRTH, () => { if (e.alive && a.state === 'husk') onHusk(b, e); }, { owner: e });
|
||||
b.after(HUSK_REBIRTH, () => { if (e.alive && a.state === 'husk') { rebirthCooldowns(e); if (onHusk) onHusk(b, e); } }, { owner: e });
|
||||
b.after(HUSK_REBIRTH + delay, () => revive(b, e, a), { owner: e });
|
||||
return true;
|
||||
},
|
||||
@@ -926,6 +938,7 @@ function statue(ab) {
|
||||
a.done = true;
|
||||
a.noAtk = e.profile.noAttack;
|
||||
rebirthCleanse(b, e); // "进行重生。重生瞬间完成"
|
||||
rebirthCooldowns(e);
|
||||
e.profile.noAttack = true;
|
||||
e.hp = e.s.maxHp;
|
||||
ab2.immune = new Set([...(ab2.immune || []), 'levitate']);
|
||||
@@ -2503,17 +2516,28 @@ export const KITS = Object.freeze({
|
||||
enemy_1026_aghost: () => [unblockable()], // 幽灵组长 · unblockable
|
||||
enemy_1062_rager_2: (ab) => [{ iv: 1, tick(b, e) { const v = T(ab, 'periodic_damage.damage') ?? 0; if (v > 0) b.dealDamage(null, e, periodicDamage(v)); } }], // 狂暴宿主组长 · "自身每秒受到500无来源真实伤害" (damage, not 流失)
|
||||
enemy_1183_mlasrt: (ab) => [ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0), nthAttackPower(nthOf(ab.sk.PowerAttack), (ab.sk.PowerAttack && ab.sk.PowerAttack.bb.atk_scale) || 1)], // 无胄盟清扫小队 · erosion; every 4th attack ×1.5
|
||||
enemy_1273_stmgun_2: (ab) => [skill(ab.sk.Cannon, (b, e) => { // 高准度伦蒂尼姆城防自行炮 · locks the highest max-HP unit, bombards the highest HP% around it
|
||||
const lock = allTargets(b, e).sort((p, q) => q.s.maxHp - p.s.maxHp)[0];
|
||||
if (!lock) return;
|
||||
const x = lock.x, y = lock.y;
|
||||
b.fx('telegraph', { x, y, r: 1, dur: CANNON_SHOTS, kind: 'cannon' });
|
||||
for (let i = 1; i <= CANNON_SHOTS; i++) b.after(i, () => {
|
||||
if (!e.alive) return;
|
||||
const t = b.alliesInRadius(x, y, 1).sort((p, q) => q.hpRatio - p.hpRatio)[0];
|
||||
if (t) { b.fx('explode', { x: t.x, y: t.y, r: 0.5, kind: 'cannon' }); hurt(b, e, t, e.s.atk * (ab.sk.Cannon.bb.atk_scale ?? 0), 'arts'); }
|
||||
}, { owner: e });
|
||||
}, { cond: (b, e) => allTargets(b, e).length > 0 })],
|
||||
enemy_1273_stmgun_2: (ab) => { // 高准度伦蒂尼姆城防自行炮 · locks the highest max-HP unit in range, bombards the highest HP% on its 9 tiles
|
||||
// the allies in its range (the engine's ranged reach: radius + the ally collider)
|
||||
const inRange = (b, e) => allTargets(b, e).filter((a) => Math.hypot(a.x - e.x, a.y - e.y) <= (e.base.rangeRadius || 0) + ALLY_COLLIDER_RADIUS + 1e-9);
|
||||
return [skill(ab.sk.Cannon, (b, e) => {
|
||||
const lock = inRange(b, e).sort((p, q) => q.s.maxHp - p.s.maxHp)[0];
|
||||
if (!lock) return;
|
||||
const r0 = Math.round(lock.y), c0 = Math.round(lock.x);
|
||||
const dur = CANNON_SHOTS * CANNON_EVERY;
|
||||
b.fx('telegraph', { x: c0, y: r0, r: 1.5, dur, kind: 'cannon' });
|
||||
b.addBuff(e, { key: 'ab:cannon', duration: dur, flags: { noDisplace: true } });
|
||||
const ab2 = e.mem.ab;
|
||||
const had = new Set(ab2.immune || []);
|
||||
ab2.immune = new Set([...had, ...CANNON_IMMUNE]);
|
||||
for (let i = 1; i <= CANNON_SHOTS; i++) b.after(i * CANNON_EVERY, () => {
|
||||
if (!e.alive) return;
|
||||
if (i === CANNON_SHOTS) ab2.immune = had;
|
||||
const t = b.allies().filter((a) => canTargetAlly(e, a, true) && Math.abs(Math.round(a.y) - r0) <= 1 && Math.abs(Math.round(a.x) - c0) <= 1)
|
||||
.sort((p, q) => q.hpRatio - p.hpRatio)[0];
|
||||
if (t) { b.fx('explode', { x: t.x, y: t.y, r: 0.5, kind: 'cannon' }); hurt(b, e, t, e.s.atk * (ab.sk.Cannon.bb.atk_scale ?? 0), 'arts'); }
|
||||
}, { owner: e });
|
||||
}, { cond: (b, e) => inRange(b, e).length > 0 })];
|
||||
},
|
||||
// 萨卡兹骸骨拷打者 · heals + ATK stack when a unit within Attack.range_radius is knocked out; leaves 2 血珀 on death — they
|
||||
// stay where it fell and wait for a 唤血祭坛 that this mode has none of (inert, not counted)
|
||||
enemy_1364_spnaxe_2: (ab) => [{
|
||||
|
||||
@@ -368,7 +368,7 @@ function installGeneric(spec) {
|
||||
if (!src || src.side !== 'enemy' || !ctx.dmg || !ctx.dmg.isAttack) return;
|
||||
readyAt = battle.time + c.cooldown;
|
||||
const amount = (c.stat === 'def' ? unit.s.def : unit.s.atk) * c.scale;
|
||||
const victims = c.around ? battle.enemiesInRadius(unit.x, unit.y, 1.5).filter((e) => !(c.groundOnly && e.isFlying)) : (src.alive ? [src] : []);
|
||||
const victims = c.around ? battle.foesInRadius(unit.x, unit.y, 1.5).filter((e) => !(c.groundOnly && e.isFlying)) : (src.alive ? [src] : []);
|
||||
for (const e of victims) {
|
||||
const dealt = amount > 0 ? battle.dealDamage(unit, e, { amount, type: c.type, canDodge: false, isSkill: true, tags: ['counter'] }) : 0;
|
||||
if (c.applyElement) c.applyElement(battle, unit, e, dealt);
|
||||
|
||||
@@ -339,10 +339,20 @@ const BY_BUFF = {
|
||||
}
|
||||
} });
|
||||
},
|
||||
// 奥术法阵: 攻击使目标失去特殊能力 silence s
|
||||
// 奥术法阵: 攻击使目标失去特殊能力 silence s. PRTS 盟约记录 备注 (as 源石溶剂's): "携带后,全场范围内的所有敌人类我方单位也会获得
|
||||
// 此装备的“造成伤害时使目标失去特殊能力5秒”效果(该效果的付与为我方阵营索敌,可对空,无视目标可选性)" — the damage of every
|
||||
// enemy-record unit on our side (炎佑, a partner's too) silences its target while a carrier is on the field [ASSUMED: it
|
||||
// ends with its carriers]; one effect per damage instance however many carriers.
|
||||
silence_attachment(battle, u, p, S) {
|
||||
const d = num(p.silence);
|
||||
if (d > 0) onAttackEnemies(S, u, (t) => battle.applyStatus(t, 'silence', { duration: d, source: u }));
|
||||
if (!(d > 0)) return;
|
||||
onAttackEnemies(S, u, (t) => battle.applyStatus(t, 'silence', { duration: d, source: u }));
|
||||
S.on('damaged', (c) => {
|
||||
const a = c.source, t = c.target;
|
||||
if (!a || a === u || a.side !== 'ally' || !ENEMY_RECORD.test(a.defId ?? '') || !t || t.side !== 'enemy' || !t.alive || !onField(u)) return;
|
||||
if (c.dmg) { if (c.dmg.arcaneSilence) return; c.dmg.arcaneSilence = true; }
|
||||
battle.applyStatus(t, 'silence', { duration: d, source: a });
|
||||
});
|
||||
},
|
||||
// 海沟实验体: 伤害减免 value per damage instance (after DEF/RES)
|
||||
halfidle_block_fixed_damage(battle, u, p, S) {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
// counter {id} · crit {id} · dp {n, id} · heal {id} · taunt {id} · summon {id, token} · pull {id} · sonic {radius} ·
|
||||
// shield {id} · overload {id} · takeoff {id} · sleep {id} · buff {id, kind} · reveal {id} · dodge (engine kind).
|
||||
|
||||
import { COLS } from '../../constants.js';
|
||||
import { COLS, CHAIN_RADIUS } from '../../constants.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../targeting.js';
|
||||
import { frontOf, offsetTile } from '../../dir.js';
|
||||
import { bodyInKeys, bodyOnTile, bodyTileReach } from '../../body.js';
|
||||
@@ -276,7 +276,7 @@ export function tinmanKit(bb, chess, def) {
|
||||
unit.mem.tinZones = (unit.mem.tinZones ?? 0) + 1;
|
||||
makeZone(battle, unit, {
|
||||
x, y, radius, duration: dur, skill: 'tinman', onPulse(b) {
|
||||
for (const e of b.enemiesInRadius(x, y, radius)) {
|
||||
for (const e of b.foesInRadius(x, y, radius)) {
|
||||
if (e.isFlying || e.s.flags.untargetable) continue;
|
||||
if (wither > 1) b.addBuff(e, { key: witherKey, duration: 1.05, data: { mul: wither }, source: unit });
|
||||
b.dealDamage(unit, e, { amount: atk * dmgScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'zone'] });
|
||||
@@ -390,7 +390,7 @@ export default {
|
||||
} }],
|
||||
install(battle, unit) {
|
||||
// the chain shape (count / sluggish, elite module included) comes from the resolved chain profile
|
||||
const ch = unit.profile.chain || { count: num(traitBb(chess)['attack@max_target'], 3), radius: 1.8, sluggish: num(traitBb(chess)['attack@sluggish'], 0.5) };
|
||||
const ch = unit.profile.chain || { count: num(traitBb(chess)['attack@max_target'], 3), radius: CHAIN_RADIUS, sluggish: num(traitBb(chess)['attack@sluggish'], 0.5) };
|
||||
if (unit.skill && unit.skill.spec.attack) unit.skill.spec.attack.chain = { ...ch, falloff: 0 };
|
||||
},
|
||||
};
|
||||
@@ -590,7 +590,7 @@ export default {
|
||||
battle.addDp(unit.ownerId, num(bb.cost));
|
||||
battle.fx('dp', { x: unit.x, y: unit.y, n: num(bb.cost), id: unit.id });
|
||||
const grid = def?.skill?.rangeGrid;
|
||||
const foes = grid ? enemiesInGrid(battle, unit, grid) : battle.enemiesInRadius(unit.x, unit.y, RING1).filter((e) => !e.s.flags.untargetable);
|
||||
const foes = grid ? enemiesInGrid(battle, unit, grid) : battle.foesInRadius(unit.x, unit.y, RING1).filter((e) => !e.s.flags.untargetable);
|
||||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 2, id: unit.id, skill: 'swordRain' });
|
||||
for (const e of foes) {
|
||||
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill'] });
|
||||
@@ -776,7 +776,7 @@ export default {
|
||||
const atk = unit.s.atk;
|
||||
makeZone(battle, unit, {
|
||||
x, y, radius: SPORE_RADIUS, duration: dur, skill: 'spores', onPulse(b) {
|
||||
for (const e of b.enemiesInRadius(x, y, SPORE_RADIUS)) {
|
||||
for (const e of b.foesInRadius(x, y, SPORE_RADIUS)) {
|
||||
if (e.s.flags.untargetable) continue;
|
||||
b.applyStatus(e, 'sluggish', { duration: 1.05, source: unit });
|
||||
b.applyStatus(e, 'silence', { duration: 1.05, source: unit });
|
||||
|
||||
@@ -681,7 +681,7 @@ export default {
|
||||
for (const a of alliesInGridOf(battle, unit)) {
|
||||
if (a === unit || a.kind !== 'op') continue;
|
||||
battle.fx('sonic', { x: a.x, y: a.y, radius: rad, id: unit.id });
|
||||
for (const e of battle.enemiesInRadius(a.x, a.y, rad)) {
|
||||
for (const e of battle.foesInRadius(a.x, a.y, rad)) {
|
||||
if (!e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * sc, type: 'arts', isSkill: true, isSplash: true, tags: ['sonic'] });
|
||||
}
|
||||
}
|
||||
@@ -756,7 +756,7 @@ export default {
|
||||
kind: 'duration', mods: { atkPct: num(bb.atk), defPct: num(bb.def) }, attack: { hitAllBlocked: true },
|
||||
onStart({ battle, unit }) {
|
||||
const grid = def?.skill?.rangeGrid;
|
||||
const foes = grid ? enemiesInGrid(battle, unit, grid, { canHitFly: false, groundOnly: true }) : battle.enemiesInRadius(unit.x, unit.y, 1.5).filter((e) => !e.isFlying);
|
||||
const foes = grid ? enemiesInGrid(battle, unit, grid, { canHitFly: false, groundOnly: true }) : battle.foesInRadius(unit.x, unit.y, 1.5).filter((e) => !e.isFlying);
|
||||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'resolve' });
|
||||
for (const e of foes) battle.applyStatus(e, 'tremble', { duration: num(bb.not_combat), source: unit });
|
||||
},
|
||||
@@ -840,7 +840,7 @@ export default {
|
||||
battle.fx('zone', { x, y, radius, dur, id: unit.id, skill: 'tinman1' });
|
||||
battle.every(STEP, (b, sc) => {
|
||||
const pulse = i % per === 0;
|
||||
for (const e of b.enemiesInRadius(x, y, radius)) {
|
||||
for (const e of b.foesInRadius(x, y, radius)) {
|
||||
if (e.isFlying || e.s.flags.untargetable) continue;
|
||||
if (weak > 0) b.applyStatus(e, 'weaken', { duration: STEP + 0.05, value: weak, source: unit });
|
||||
if (wither > 1) b.addBuff(e, { key: 'tinman:wither', duration: STEP + 0.05, data: { mul: wither }, source: unit });
|
||||
|
||||
@@ -82,7 +82,7 @@ function profileMul(battle, unit, target) {
|
||||
}
|
||||
/** Targetable enemies within `r` tiles of (x, y), nearest first (spawn order breaks ties), excluding `skip`. */
|
||||
function enemiesAround(battle, unit, x, y, r, skip = null) {
|
||||
const out = battle.enemiesInRadius(x, y, r).filter((e) => !(skip && skip.has(e)) && canTargetEnemy(unit, e, { canHitFly: true }));
|
||||
const out = battle.foesInRadius(x, y, r).filter((e) => !(skip && skip.has(e)) && canTargetEnemy(unit, e, { canHitFly: true }));
|
||||
const d = (e) => bodyDist(e, x, y);
|
||||
return out.sort((a, b) => d(a) - d(b) || (a.spawnSeq ?? a.id) - (b.spawnSeq ?? b.id));
|
||||
}
|
||||
@@ -113,6 +113,8 @@ const gridKeys = (grid, u, ext = 0) => absoluteRangeKeys(grid || [[0, 0]], u.til
|
||||
const fx = (battle, kind, u, extra = {}) => battle.fx(kind, { x: u.x, y: u.y, id: u.id, ...extra });
|
||||
const copyGrid = (g) => (Array.isArray(g) && g.length ? g.map((p) => [p[0], p[1]]) : null);
|
||||
const NINE = [[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]];
|
||||
/** 琳琅诗怀雅 S3's coin range (PRTS 备注 "前方范围2-4"; range_table "2-4", facing right). */
|
||||
const SWIRE2_COIN_GRID = Object.freeze([[1, 1], [0, 0], [0, 1], [0, 2], [-1, 1]]);
|
||||
/** Two enemy bodies touch within this distance (tiles) — 见行者 collision stun. */
|
||||
const COLLIDE = 0.6;
|
||||
/** 忍冬 S3's 迷彩 (until her next cast): its own buff key, never merged with another unit status. */
|
||||
@@ -401,11 +403,11 @@ const KITS = {
|
||||
// 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles)
|
||||
// S1 仗义疏财 (passive, 2 coins): an attack spends a coin to heal the most injured ally (< 70 % HP) of the 8
|
||||
// surrounding tiles for attack@heal_scale × ATK. S3 千金一掷 (持续时间无限): attacks hit twice, kills give a coin;
|
||||
// closing it spends every coin on random ground enemies of the front range (atk_scale phys + a small push, radial
|
||||
// despite the text's 向前 — PRTS 备注 "推开效果为径向推动"; client charpack char_1033_swire2: the RandomGold ability
|
||||
// (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位, 弹道不可对空).
|
||||
// Auto-close (the mode casts everything itself; the player's "主动关闭" is not available): once the purse is full
|
||||
// (10) and an enemy stands in range. 精锐 module MER-Y: ATK +4 % per trait payment (≤ 5 stacks).
|
||||
// closing it spends every coin on random ground enemies of range 2-4 in front and those she blocks (atk_scale phys +
|
||||
// a small push, radial despite the text's 向前 — PRTS 备注 "推开效果为径向推动"; client charpack char_1033_swire2:
|
||||
// the RandomGold ability (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位,
|
||||
// 弹道不可对空). Auto-close (the mode casts everything itself; the player's "主动关闭" is not available): once the
|
||||
// purse is full (10) and a coin target stands there. 精锐 module MER-Y: ATK +4 % per trait payment (≤ 5 stacks).
|
||||
chess_char_3_04_a: (bb, chess, def) => {
|
||||
const d = defOf(chess, def);
|
||||
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
|
||||
@@ -476,22 +478,29 @@ const KITS = {
|
||||
[S1]: () => ({ kind: 'passive' }), // (coins → heals: installS1)
|
||||
[S3]: (s) => {
|
||||
const cash = num(s.bb.atk_scale, 1), force = num(s.bb.force, 0), full = num(s.bb.sp, 10);
|
||||
// the 【金币标记】 targets: ground enemies on range 2-4 in front of her (range_table "2-4") and every unit she blocks
|
||||
const coinMarks = (battle, unit) => {
|
||||
const list = enemiesOn(battle, unit, gridKeys(SWIRE2_COIN_GRID, unit), 0, { ...unit.profile, canHitFly: false });
|
||||
for (const e of unit.blocking || []) if (e.alive && !e.isFlying && !list.includes(e)) list.push(e);
|
||||
return list;
|
||||
};
|
||||
return {
|
||||
kind: 'toggle',
|
||||
attack: { hits: 2 },
|
||||
onTick({ battle, unit, skill }) {
|
||||
if ((unit.mem.coins ?? 0) >= full && enemiesOn(battle, unit, unit.rangeKeys, 0, { ...unit.profile, canHitFly: false }).length) skill.end('manual');
|
||||
if ((unit.mem.coins ?? 0) >= full && coinMarks(battle, unit).length) skill.end('manual');
|
||||
},
|
||||
onEnd({ battle, unit, reason }) {
|
||||
if (reason !== 'manual' || !unit.alive) return;
|
||||
const n = unit.mem.coins ?? 0;
|
||||
unit.mem.coins = 0;
|
||||
let spent = 0;
|
||||
// PRTS 备注: 【金币标记】 goes on "前方范围内的地面敌方单位与自身阻挡的所有单位" and the coins are "弹道(不可对空)" —
|
||||
// air units (FLY, 近地悬浮, 浮空) are never paid
|
||||
const ground = { ...unit.profile, canHitFly: false };
|
||||
// PRTS 备注: closing it "立即对前方范围2-4内的地面敌方单位与自身阻挡的所有单位施加【金币标记】", the coins going to
|
||||
// marked units at random — 弹道(不可对空): air units (FLY, 近地悬浮, 浮空) are never paid. Until 0.1.1 the
|
||||
// coins went to her attack range (1-1) instead of range 2-4.
|
||||
const marked = coinMarks(battle, unit);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const e = battle.rng.pick(enemiesOn(battle, unit, unit.rangeKeys, 0, ground));
|
||||
const e = battle.rng.pick(marked.filter((x) => x.alive && !x.isFlying));
|
||||
if (!e) break;
|
||||
spent++;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * cash, type: 'phys', isSkill: true, tags: ['skill', 'swire2Cash'] });
|
||||
|
||||
@@ -43,6 +43,8 @@ const LINE = Object.freeze(Array.from({ length: COLS }, (_, i) => Object.freeze(
|
||||
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';
|
||||
/** 阿罗玛's 非首次标记 (talent 起泡性能测试): one per enemy, shared by every 阿罗玛 (PRTS 备注). */
|
||||
const AROMA_MARK = 'aroma:bubbled';
|
||||
const BILLRO_MARK_MAX = 5;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -863,14 +865,26 @@ const kits = {
|
||||
}),
|
||||
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, onStart({ battle, unit }) { battle.fx('slippery', { x: unit.x, y: unit.y, id: unit.id }); } },
|
||||
talents: [{ install(battle, unit) { // first attack on each enemy: ×1.1 and levitate 2.5 s
|
||||
unit.mem.bubbled = new Set();
|
||||
// PRTS 阿罗玛 备注: "天赋采用施加非首次标记的方式判断是否为'首次进行攻击',自身离场时移除自身已施加的标记,不同阿罗玛之间的
|
||||
// 非首次标记通用" — one mark per enemy (AROMA_MARK, the setter as source) for every 阿罗玛; hers go when she leaves
|
||||
// the field (until 0.1.1 each 阿罗玛 kept her own list, so two of them both triggered on one enemy)
|
||||
unit.mem.bubbled = new Set(); // the enemies she marked
|
||||
battle.on('hit', (c) => {
|
||||
const e = c.target;
|
||||
if (c.source !== unit || !c.dmg.isAttack || e.side !== 'enemy' || unit.mem.bubbled.has(e.id)) return;
|
||||
if (c.source !== unit || !c.dmg.isAttack || e.side !== 'enemy' || e.findBuff(AROMA_MARK)) return;
|
||||
battle.addBuff(e, { key: AROMA_MARK, source: unit });
|
||||
unit.mem.bubbled.add(e.id);
|
||||
c.dmg.mul *= num(t0.damage_scale, 1.1);
|
||||
if (battle.applyStatus(e, 'levitate', { duration: num(t0.levitate_duration, 2.5), source: unit })) battle.fx('levitate', { x: e.x, y: e.y, id: e.id });
|
||||
}, { owner: unit });
|
||||
battle.on('death', (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
for (const id of unit.mem.bubbled) {
|
||||
const e = battle.unitById(id);
|
||||
if (e && e.findBuff(AROMA_MARK)?.source === unit) battle.removeBuff(e, AROMA_MARK);
|
||||
}
|
||||
unit.mem.bubbled.clear();
|
||||
}, { owner: unit });
|
||||
} }],
|
||||
install(battle, unit) {
|
||||
// S2: landing damage for every levitated enemy (any source) that lands inside her range while the skill runs
|
||||
@@ -979,7 +993,7 @@ const kits = {
|
||||
onTick({ battle, unit, dt }) {
|
||||
const T = unit.mem.tornado;
|
||||
if (!T) return;
|
||||
const inside = battle.enemiesInRadius(T.x, T.y, R);
|
||||
const inside = battle.foesInRadius(T.x, T.y, R);
|
||||
for (const e of inside) pulse(battle, e, `glady:slow:${unit.id}`, { moveMul: Math.max(0, 1 + num(bb.move_speed, -0.5)) });
|
||||
T.acc += dt;
|
||||
if (T.acc + 1e-9 < iv) return;
|
||||
@@ -996,7 +1010,7 @@ const kits = {
|
||||
const T = unit.mem.tornado;
|
||||
unit.mem.tornado = null;
|
||||
if (!T || reason === 'death' || !unit.alive) return;
|
||||
for (const e of battle.enemiesInRadius(T.x, T.y, R)) pullSelf(battle, unit, e, force);
|
||||
for (const e of battle.foesInRadius(T.x, T.y, R)) pullSelf(battle, unit, e, force);
|
||||
battle.fx('pull', { x: T.x, y: T.y, id: unit.id });
|
||||
},
|
||||
},
|
||||
@@ -1946,7 +1960,7 @@ const kits = {
|
||||
if (S2 && skillActive(unit)) {
|
||||
// S2: motes orbit wider and hit enemies instead of operators
|
||||
const hitAt = unit.mem.moteHit || (unit.mem.moteHit = new Map());
|
||||
const foes = battle.enemiesInRadius(unit.x, unit.y, num(bb.outside_radius, 2)).filter((e) => canTargetEnemy(unit, e, { canHitFly: true }))
|
||||
const foes = battle.foesInRadius(unit.x, unit.y, num(bb.outside_radius, 2)).filter((e) => canTargetEnemy(unit, e, { canHitFly: true }))
|
||||
.sort((a, b) => a.id - b.id);
|
||||
for (const e of foes) {
|
||||
if ((hitAt.get(e.id) ?? -Infinity) > battle.time + 1e-9) continue;
|
||||
|
||||
@@ -446,7 +446,7 @@ const KITS = {
|
||||
unit.mem.titiWardAcc = 0;
|
||||
for (const a of unit.mem.titiWard || []) {
|
||||
if (!a.alive || !a.deployed || !a.findBuff('titi:ward')) continue;
|
||||
for (const e of battle.enemiesInRadius(a.x, a.y, RING1)) if (e.alive) battle.applyStatus(e, 'sleep', { duration: AURA_DUR, source: unit });
|
||||
for (const e of battle.foesInRadius(a.x, a.y, RING1)) if (e.alive) battle.applyStatus(e, 'sleep', { duration: AURA_DUR, source: unit });
|
||||
}
|
||||
},
|
||||
onEnd({ battle, unit }) {
|
||||
@@ -507,7 +507,7 @@ const KITS = {
|
||||
const asleep = new Map();
|
||||
// PRTS 备注: "向1.5半径内除该单位外仇恨值最高的敌方单位施加沉睡" — highest aggro (taunt) first, then the nearest
|
||||
const sleepOthers = (from) => {
|
||||
const cands = battle.enemiesInRadius(from.x, from.y, radius).filter((o) => o !== from && !o.s.flags.sleep && !o.mem?.candleOwner)
|
||||
const cands = battle.foesInRadius(from.x, from.y, radius).filter((o) => o !== from && !o.s.flags.sleep && !o.mem?.candleOwner)
|
||||
.sort((a, b) => (b.s.taunt || 0) - (a.s.taunt || 0) || dist(a, from) - dist(b, from) || a.spawnSeq - b.spawnSeq);
|
||||
let n = 0;
|
||||
for (const o of cands) {
|
||||
@@ -604,7 +604,7 @@ const KITS = {
|
||||
battle.addBuff(t, {
|
||||
key: `blaze2:aid:${unit.id}`, duration: num(bb.max_duration, 20), interval: Math.max(0.1, num(bb.interval, 1)), visible: true, data: { src: unit },
|
||||
onTick: ({ unit: x }) => {
|
||||
for (const e of battle.enemiesInRadius(x.x, x.y, r)) {
|
||||
for (const e of battle.foesInRadius(x.x, x.y, r)) {
|
||||
const d = battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill', 'blazeAid'] });
|
||||
if (d > 0 && e.alive) battle.dealDamage(unit, e, { type: 'element', element: 'burn', amount: d * num(bb.element_multiplier), tags: ['skill', 'blazeAid'] });
|
||||
}
|
||||
@@ -676,7 +676,7 @@ const KITS = {
|
||||
if (u.deploySeq !== dep || u.hp < u.s.maxHp - 1e-6) return;
|
||||
battle.removeBuff(u, buff);
|
||||
u.mem.downed = false;
|
||||
for (const e of battle.enemiesInRadius(u.x, u.y, NEARBY_RADIUS)) if (e.alive) battle.applyStatus(e, 'stun', { duration: num(t1.stun), source: u });
|
||||
for (const e of battle.foesInRadius(u.x, u.y, NEARBY_RADIUS)) if (e.alive) battle.applyStatus(e, 'stun', { duration: num(t1.stun), source: u });
|
||||
battle.fx('revive', { x: u.x, y: u.y, id: u.id, r: NEARBY_RADIUS });
|
||||
},
|
||||
});
|
||||
@@ -793,7 +793,7 @@ const KITS = {
|
||||
// for this one: it never lands on or catches air units (FLY, 近地悬浮, 浮空)
|
||||
const main = list.find((e) => !reach(e) && !e.blockedBy && !e.isFlying) ?? list.find((e) => !e.blockedBy && !e.isFlying) ?? list.find((e) => !e.isFlying);
|
||||
if (!main) return;
|
||||
const near = battle.enemiesInRadius(main.x, main.y, RING1).filter((e) => !e.isFlying && !e.s.flags.untargetable && !e.s.flags.sleep)
|
||||
const near = battle.foesInRadius(main.x, main.y, RING1).filter((e) => !e.isFlying && !e.s.flags.untargetable && !e.s.flags.sleep)
|
||||
.sort((a, b) => (a === main ? -1 : b === main ? 1 : 0) || dist(a, main) - dist(b, main) || a.spawnSeq - b.spawnSeq)
|
||||
.slice(0, Math.max(1, num(bb.max_target, 2)));
|
||||
const force = num(bb.force, 1);
|
||||
@@ -827,7 +827,7 @@ const KITS = {
|
||||
const [sr, sc] = frontOf(unit.tileR, unit.tileC, unit.dir, stop ?? 0);
|
||||
const fromX = unit.x, fromY = unit.y;
|
||||
battle.fx('anchor', { x: sc, y: sr, id: unit.id, fromX, fromY, r: radius });
|
||||
for (const e of battle.enemiesInRadius(sc, sr, radius)) {
|
||||
for (const e of battle.foesInRadius(sc, sr, radius)) {
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'phys', isSkill: true, tags: ['skill', 'anchor'] });
|
||||
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun), source: unit });
|
||||
}
|
||||
@@ -894,7 +894,7 @@ const KITS = {
|
||||
const hpScale = num(bb['attack@max_hp_scale'], 0.6), defScale = num(bb['attack@def_scale'], 1), resScale = num(bb['attack@magic_resistance_scale'], 1);
|
||||
const nCandles = Math.max(1, num(bb['attack@max_target'], 3));
|
||||
const candleKey = talentRec(chess, 2)?.bbStr?.take_extra_enemy_key ?? CANDLE_KEY;
|
||||
const aroundN = (battle, a) => battle.enemiesInRadius(a.x, a.y, RING1).length;
|
||||
const aroundN = (battle, a) => battle.foesInRadius(a.x, a.y, RING1).length;
|
||||
return {
|
||||
skills: lazySkills({
|
||||
skchr_etlchi_1: () => ({ kind: instantKind(chess, def), attack: { atkScale: num(bb.atk_scale, 1), hits: 2 } }),
|
||||
@@ -917,7 +917,7 @@ const KITS = {
|
||||
// PRTS 备注 "被添加血镰的单位处于起飞时,血镰可对空": a sickle on the ground spares air units (FLY, 近地悬浮, 浮空);
|
||||
// 起飞 = an airborne skywalker (蒂比's skill: off the ground, flag blockFly)
|
||||
const air = !a.ground && !!a.s.flags.blockFly;
|
||||
for (const e of battle.enemiesInRadius(a.x, a.y, RING1)) {
|
||||
for (const e of battle.foesInRadius(a.x, a.y, RING1)) {
|
||||
if (e.isFlying && !air) continue;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'phys', isSkill: true, tags: ['skill', 'bloodSickle'] });
|
||||
}
|
||||
@@ -1939,7 +1939,7 @@ const KITS = {
|
||||
if (z.anchor && z.anchor.alive && z.anchor.deployed) { z.x = z.anchor.x; z.y = z.anchor.y; }
|
||||
const iv = Math.max(0.1, num(bb.interval, 1));
|
||||
const steps = Math.min(Math.max(0, num(bb.max_stack_cnt, 15)), Math.floor((z.t - AURA_IV + 1e-9) / iv));
|
||||
const foes = battle.enemiesInRadius(z.x, z.y, RING1);
|
||||
const foes = battle.foesInRadius(z.x, z.y, RING1);
|
||||
for (const e of foes) seaHits.set(e, Math.max(seaHits.get(e) ?? -1, steps));
|
||||
if (z.acc >= iv - 1e-9) {
|
||||
z.acc -= iv;
|
||||
@@ -2037,7 +2037,7 @@ const KITS = {
|
||||
z.x = Math.max(R.c0, Math.min(R.c1, z.x + z.vx * AURA_IV));
|
||||
z.y = Math.max(R.r0, Math.min(R.r1, z.y + z.vy * AURA_IV));
|
||||
const r = r0 + grow * z.t;
|
||||
const foes = battle.enemiesInRadius(z.x, z.y, r).filter((e) => !e.isFlying);
|
||||
const foes = battle.foesInRadius(z.x, z.y, r).filter((e) => !e.isFlying);
|
||||
for (const e of foes) battle.addBuff(e, { key: 'thorn2:rot', duration: AURA_DUR, mods: { healingTakenMul: healMul } });
|
||||
if (z.acc >= 1 - 1e-9) {
|
||||
z.acc -= 1;
|
||||
@@ -2252,7 +2252,7 @@ const KITS = {
|
||||
{ install(battle, unit) { // 游侠
|
||||
const dr = num(t0.damage_resistance);
|
||||
whileOn(battle, unit, 0.2, () => {
|
||||
unit.mem.mlyNear = battle.enemiesInRadius(unit.x, unit.y, AROUND_RADIUS).length;
|
||||
unit.mem.mlyNear = battle.foesInRadius(unit.x, unit.y, AROUND_RADIUS).length;
|
||||
if (dr > 0 && unit.mem.mlyNear >= num(t0.cnt, 3)) battle.addBuff(unit, { key: 'mlynar:ranger', duration: 0.3, mods: { dmgTakenMul: 1 - dr } });
|
||||
});
|
||||
} },
|
||||
@@ -2399,7 +2399,7 @@ const KITS = {
|
||||
battle.fx('iceSpike', { x: c, y: r, id: unit.id, r: ICICLE_RADIUS });
|
||||
battle.after(ICICLE_DELAY, () => {
|
||||
const hit = [];
|
||||
for (const e of battle.enemiesInRadius(c, r, ICICLE_RADIUS)) {
|
||||
for (const e of battle.foesInRadius(c, r, ICICLE_RADIUS)) {
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', isAttack: true, isSkill: true, isSplash: true, tags: ['skill', 'iceSpike'] });
|
||||
if (e.alive && cold > 0) battle.applyStatus(e, 'cold', { duration: cold, source: unit });
|
||||
if (e.alive) hit.push(e);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1106,7 +1106,7 @@ function yanyouKit(bb, raw) {
|
||||
if (f.acc < 1 - 1e-9) return;
|
||||
f.acc -= 1;
|
||||
// centre-point AoE around the locked target, searched again every second (the target itself included)
|
||||
const hit = flameR > 0 ? battle.enemiesInRadius(t.x, t.y, flameR) : [];
|
||||
const hit = flameR > 0 ? battle.foesInRadius(t.x, t.y, flameR) : [];
|
||||
if (!hit.includes(t)) hit.unshift(t);
|
||||
for (const e of hit) battle.dealDamage(unit, e, { amount: unit.s.atk * flameScale, type: 'arts', isSkill: true, tags: ['flame'] });
|
||||
battle.fx('yanyouFlame', { x: t.x, y: t.y, id: unit.id, target: t.id, r: flameR, n: hit.length, dur: 1 });
|
||||
@@ -1152,7 +1152,7 @@ function yanyouKit(bb, raw) {
|
||||
auraAcc += dt;
|
||||
if (fragMul > 1 && auraAcc >= YANYOU_AURA_EVERY - 1e-9) {
|
||||
auraAcc = 0;
|
||||
for (const e of battle.enemiesInRadius(unit.x, unit.y, YANYOU_FRAGILE_RADIUS)) {
|
||||
for (const e of battle.foesInRadius(unit.x, unit.y, YANYOU_FRAGILE_RADIUS)) {
|
||||
battle.applyStatus(e, 'elemFragile', { duration: 2 * YANYOU_AURA_EVERY, value: fragMul - 1, source: unit });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
import { toLocal, frontOf } from './dir.js';
|
||||
import { absoluteRangeKeys } from './targeting.js';
|
||||
import { bodyInKeys, bodyKeys, bodyOnTile } from './body.js';
|
||||
import { COLS } from './constants.js';
|
||||
import { COLS, CHAIN_RADIUS } from './constants.js';
|
||||
|
||||
const P = (o) => Object.freeze(o);
|
||||
const num = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
|
||||
@@ -276,13 +276,14 @@ export const SUB = Object.freeze({
|
||||
closerange: P({}),
|
||||
longrange: P({ priority: 'lowDef' }),
|
||||
aoesniper: P({ splashRadius: 1.1, projectile: 'bomb' }),
|
||||
bombarder: P({ splashRadius: 1.0, projectile: 'bomb', groundOnly: true, canHitFly: false,
|
||||
// PRTS 溅射半径一览 (特性): 投掷手 0.9, 扩散术师 1.1 (格雷伊 1.0, TUNE below), 链术师 1.7 jumps; 炮手 1.0 (none in the pool)
|
||||
bombarder: P({ splashRadius: 0.9, projectile: 'bomb', groundOnly: true, canHitFly: false,
|
||||
afterHit: (battle, unit, target, info) => {
|
||||
// aftershocks: (times − 1) extra hits at append_atk_scale × ATK (default one hit at 50 %)
|
||||
const n = Math.max(1, (unit.profile.shockTimes ?? 2) - 1);
|
||||
for (let i = 1; i <= n; i++) {
|
||||
battle.after(0.3 * i, () => {
|
||||
for (const e of battle.enemiesInRadius(info.x, info.y, unit.profile.splashRadius || 1, true)) { // splash: 中点判定
|
||||
for (const e of battle.foesInRadius(info.x, info.y, unit.profile.splashRadius || 1, true)) { // splash: 中点判定
|
||||
if (e.isFlying) continue;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * (unit.profile.shockScale ?? 0.5), type: 'phys', isSplash: true, tags: ['aftershock'] });
|
||||
}
|
||||
@@ -310,7 +311,7 @@ export const SUB = Object.freeze({
|
||||
// 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 } }),
|
||||
chain: P({ chain: { count: 3, falloff: 0.15, radius: CHAIN_RADIUS, sluggish: 0.5 } }),
|
||||
funnel: P({ projectile: 'drone', install: installFunnel,
|
||||
dmgMul: (battle, unit, target) => {
|
||||
const f = unit.profile.funnel || { init: 0.2, delta: 0.15, max: 1.1 };
|
||||
@@ -397,6 +398,8 @@ export const SUB = Object.freeze({
|
||||
const TUNE = {
|
||||
fastshot: (tb) => ({ flyScale: num(tb.atk_scale, 1) }),
|
||||
bombarder: (tb) => ({ shockScale: num(tb['attack@append_atk_scale'], 0.5), shockTimes: num(tb['attack@times'], 2) }),
|
||||
// PRTS 溅射半径一览 特殊: 格雷伊 1.0 (the branch's 1.1 otherwise)
|
||||
splashcaster: (tb, def) => ((def.charId ?? def.raw?.charId) === 'char_253_greyy' ? { splashRadius: 1.0 } : {}),
|
||||
hunter: (tb) => ({ ammoMax: num(tb.value, 8), ammoScale: num(tb.atk_scale, 1.2) }),
|
||||
reaperrange: (tb, def) => ({ frontScale: num(tb.atk_scale, 1.5), frontGrid: def.raw?.trait?.rangeGrid ?? null }),
|
||||
funnel: (tb) => ({ funnel: { init: num(tb.init_atk_scale, 0.2), delta: num(tb.delta_atk_scale, 0.15), max: num(tb.max_atk_scale, 1.1) } }),
|
||||
|
||||
Reference in new issue
Block a user