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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user