feedback5: merge O10 — 自选 kits for 棘刺, 司霆惊蛰 and 赤刃明霄陈; golden: only diy moved
This commit is contained in:
commit
eba4f53e87
8 files changed
+2336
-122
No files matched your search
@@ -115,6 +115,9 @@ export const OPERATOR_KIT_FILES = Object.freeze([
|
||||
'op-cqbw.js',
|
||||
'op-phenxi.js',
|
||||
'op-wisdel.js',
|
||||
'op-thorns.js',
|
||||
'op-chen3.js',
|
||||
'op-leizi2.js',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
// server/sim/content/kits/ops/op-chen3.js — 赤刃明霄陈 (char_1050_chen3) 自选 operator kit: 6★ 术战者 (近卫), an owned-6★
|
||||
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (AFT-X) at every form.
|
||||
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_1050_chen3, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
|
||||
// elite = E2 Lv60, rank 7, AFT-X at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
|
||||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); the client's battle
|
||||
// logic — buff_template_data (chen3_t2 / [timer] / [evade], chen3_s1[derived_silence], chen3_s2[dead_listener] /
|
||||
// [record_position] / [try_resapwn] / [respawn_buff], chen3_s3[ensure_dmg] / [finish_projectile]), her charpack (talent 1
|
||||
// chen3_t1 + common_weak_damage; S1 attack `_additionalTimes` 1; S3 attack `_additionalTimes` 2, ground selector), the
|
||||
// skill prefabs (skchr_chen3_2 S2MultiMeleeAttack: `_triggerDelta` 0.4, `s2_extend_time` 0.4 / `s2_times_add_one` 1 per
|
||||
// lost target; skchr_chen3_3: the sword wave) and projectile_chr_chen3_s3 (speed 1.5, no lifetime of its own — ended by
|
||||
// the skill —, `_hitDistance` 0.25, turns at high ground / map edge / 侵入点 / 保护目标, clears its hit list when it turns)
|
||||
// — and PRTS 赤刃明霄陈 (talent 1 修正 "造成的物理和法术伤害变为弱点伤害"; talent 2 备注 "闪避优先级为-1000,不可叠加…二技能
|
||||
// 移动时可继承闪避效果"; S2 / S3 备注), Terra wiki Ch'en the Dawnstreak (the wave's enemy hitbox 1.3).
|
||||
// - Trait (术战者) "攻击造成法术伤害": the artsfghter profile (melee arts, ground only), range 1-1, blocks 1. Module AFT-X
|
||||
// “记忆残页” adds "未阻挡敌人时攻击速度+8" (trait attack_speed): ASPD + while she blocks nobody.
|
||||
// - T1 形意洞照 "攻击力+13%,攻击速度+13,造成的物理和法术伤害变为弱点伤害" (PRTS 修正): a permanent ATK / ASPD buff, and
|
||||
// every physical or arts damage instance she deals becomes 弱点伤害 (ba.weaknessatk "根据目标防御力和法术抗性变更伤害类
|
||||
// 型"): the type the target resists less (items/battle.js weaknessRetype, as 双模机械臂 / 陈's band 以己之长).
|
||||
// - T2 寒暑觉知 "未受到伤害时,每N秒随机治疗自身一定(攻击力的X%~Y%)生命值,并闪避下次物理与法术攻击": a counter +1 every
|
||||
// second (chen3_t2[timer]: triggerInterval 1, the first a second in), back to 0 with every damage instance she takes
|
||||
// (ON_APPLIED_MODIFIER IsDamage: not a 流失, not an element fill; a dodged hit lands nothing); at stack_time it restarts and
|
||||
// she heals herself ATK × a random whole percent in [heal_atk_scale_min, heal_atk_scale_max) (RandomSetter, PRTS
|
||||
// "最小单位为1%") and holds one dodge (chen3_t2[evade]: STACK, maxStackCnt 1 — "不可叠加") that cancels the next physical
|
||||
// or arts damage instance that can be dodged (Evade PHYSICAL_AND_MAGICAL), then is spent. Its priority is −1000 (PRTS;
|
||||
// onEventPriority LOW_PRIORITY): her other dodges (S2's) are rolled first, and the held dodge is kept when one of them
|
||||
// wins. S2's 【移动】 keeps it (PRTS "二技能移动时可继承闪避效果") — and the counter, as the engine's 【移动】 keeps the rest
|
||||
// (DESIGN §22.3). AFT-X stage 2+: the module talent's numbers (stage 3: 6 s, 50 %–200 %).
|
||||
// - S1 赤霄·奔夜 (MANUAL, data DEFAULT): ATK +atk for `duration` s, every attack hits twice (二连击) and 特殊能力失效 — 沉默 —
|
||||
// its target until the skill ends (chen3_s1[derived_silence]: derived from the skill's holder buff, independentCharacter
|
||||
// Source): the status for the skill's time left; an early end (she leaves) lifts the ones she set.
|
||||
// - S2 赤霄·绝影-驰 (MANUAL, 技能范围 x-1, data SKILL_RANGE): she slashes the nearest enemy she can select in the x-1 (air units
|
||||
// too) `times` (10) times, 0.4 s apart, for atk_scale × ATK arts each (the first at the cast [ASSUMED], so the slashing lasts
|
||||
// 0.4 s × its slashes); a target that leaves the field (or hides [ASSUMED]) hands the slashes to the selectable enemy
|
||||
// nearest it within 1.7 of it, else in her x-1, and adds one slash (+0.4 s); with nobody to select the slashing ends.
|
||||
// Meanwhile (PRTS): 无敌, 不可阻挡 (she blocks nothing), 阻回, 静默, stun / freeze immunity, no normal attack. Then always a
|
||||
// 【移动】 (Battle.moveRedeploy): onto the tile its last target stood on at its last slash (record_position) when that target
|
||||
// still stands and the tile takes a melee operator, else back onto her own tile (PRTS "否则返回原位置"); the move's own
|
||||
// travel and deploy clip (Start_2, 1 s) are not modelled [ASSUMED: instant, as every deployment of the engine]. From
|
||||
// then the `duration` (6 s, "不包括斩击所耗时间"): ATK +respawn_buff.atk and respawn_buff.prob physical and arts dodge.
|
||||
// - S3 赤霄·天喟 (MANUAL, data ACTIVE_RANGE on its 3-12): range 3-12, every attack hits up to attack@max_target ground enemies
|
||||
// three times (`_additionalTimes` 2) for attack@atk_scale × ATK arts, `duration` s. At the cast a sword wave leaves her
|
||||
// tile in her direction at 1.5 tiles/s and roams until the skill ends (FinishManagedProjectiles) or she leaves: when its
|
||||
// leading edge (0.25 ahead) would enter a high tile, the map's edge — the battle's field [ASSUMED: a battle's map is its
|
||||
// field, the stage tiles beyond it are another player's half] —, an 侵入点 or a 保护目标 it turns 90° clockwise (four
|
||||
// turns at most per tick; boxed in on every side it waits) and forgets whom it hit; each enemy it can select within 1.3
|
||||
// (air too) is hit once per straight run for the larger of hp_ratio × its current HP (the unit's own HP, PRTS) and
|
||||
// projectile_min_atk_scale × her ATK at the hit, arts (RES applies — PRTS "非伤害保底或无视法术抗性"; 普通伤害). Her
|
||||
// begin clips (S1 0.2 s, S3 0.433 s) are not modelled [ASSUMED: the engine starts skills at once].
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
|
||||
import { bodyDist } from '../../../body.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
import { weaknessRetype } from '../../items/battle.js';
|
||||
|
||||
const S1 = 'skchr_chen3_1';
|
||||
const S2 = 'skchr_chen3_2';
|
||||
const S3 = 'skchr_chen3_3';
|
||||
/** The skill ranges when a record carries none (data: x-1, 3-12). */
|
||||
const X_1 = Object.freeze([[2, 0], [1, -1], [1, 0], [1, 1], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, -1], [-1, 0], [-1, 1], [-2, 0]]);
|
||||
const R3_12 = Object.freeze([[1, 0], [1, 1], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-1, 1]]);
|
||||
/** skchr_chen3_2 S2MultiMeleeAttack `_triggerDelta` / dead_listener `s2_extend_time`: the time per slash. */
|
||||
const SLASH_IV = 0.4;
|
||||
/** PRTS S2 备注: a lost target hands over to "该目标半径1.7范围内的另一个目标". */
|
||||
const RETARGET_RADIUS = 1.7;
|
||||
/** projectile_chr_chen3_s3: speed; its terrain probe (`_hitDistance`); Terra wiki: its enemy hitbox. */
|
||||
const WAVE_SPEED = 1.5;
|
||||
const WAVE_PROBE = 0.25;
|
||||
const WAVE_HIT_RADIUS = 1.3;
|
||||
/** S1's early end: a silence of hers that ends within this of the skill's planned end ended with it. */
|
||||
const SILENCE_SLACK = 0.1;
|
||||
const AIR = Object.freeze({ canHitFly: true });
|
||||
const DASH_KEY = 'skill:chen3:dash';
|
||||
const AFTER_KEY = 'skill:chen3:respawn';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** Absolute tile keys of a skill range at the unit's tile and facing (a 技能范围 ignores 攻击距离). */
|
||||
const keysOf = (unit, grid) => absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0);
|
||||
/** An S2 target that left the field — dead, removed or hidden [ASSUMED: a hidden one counts as gone]. */
|
||||
const gone = (e) => !e || !e.alive || e.removed || e.hidden;
|
||||
/** The enemy of `list` nearest to (x, y) (body distance; ties by spawn order), or null. */
|
||||
function nearest(list, x, y) {
|
||||
let best = null, bd = Infinity;
|
||||
for (const e of list) {
|
||||
const d = bodyDist(e, x, y);
|
||||
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && best && e.spawnSeq < best.spawnSeq)) { bd = d; best = e; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
/** A tile S2's 【移动】 may land on: in the field, melee-deployable, free (her own tile aside), no operator lying there. */
|
||||
function landable(battle, unit, r, c) {
|
||||
if (!Number.isInteger(r) || !Number.isInteger(c) || !battle.grid.inRect(r, c) || !battle.grid.canStand(r, c)) return false;
|
||||
if (battle.downOn(r, c)) return false;
|
||||
return (r === unit.tileR && c === unit.tileC) || !battle.isReservedTile(r, c);
|
||||
}
|
||||
/**
|
||||
* S3's wave turns before a high tile, the map's edge — the battle's field (`rect`): the official map of a battle is that
|
||||
* field, the stage tiles beyond it (another player's half) are not part of it —, an 侵入点 or a 保护目标.
|
||||
*/
|
||||
function waveBlocked(battle, w) {
|
||||
const r = Math.round(w.y + w.dr * WAVE_PROBE), c = Math.round(w.x + w.dc * WAVE_PROBE);
|
||||
if (!battle.grid.inRect(r, c)) return true;
|
||||
const t = battle.grid.tile(r, c);
|
||||
return t.height === 'HIGH' || t.special === 'start' || t.special === 'end';
|
||||
}
|
||||
|
||||
export default {
|
||||
char_1050_chen3: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const tb = traitBb(chess);
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const grid2 = skillRec(chess, S2)?.rangeGrid ?? X_1, grid3 = skillRec(chess, S3)?.rangeGrid ?? R3_12;
|
||||
const slashes = Math.max(1, Math.floor(num(b2.times, 10)));
|
||||
|
||||
/** S2: the selectable enemies (air too) she may slash next — around the lost target, else in her x-1. */
|
||||
const nextTarget = (battle, unit, from) => {
|
||||
const near = battle.foesInRadius(from.x, from.y, RETARGET_RADIUS).filter((e) => e !== from.e && canTargetEnemy(unit, e, AIR));
|
||||
if (near.length) return nearest(near, from.x, from.y);
|
||||
const inRange = battle.enemiesInKeys(keysOf(unit, grid2), unit, AIR).filter((e) => e !== from.e);
|
||||
return nearest(inRange, from.x, from.y);
|
||||
};
|
||||
/** S2: the slashing is over — the 【移动】, then the 6 s state. */
|
||||
const landS2 = (battle, unit, skill, m) => {
|
||||
battle.removeBuff(unit, DASH_KEY);
|
||||
m.phase = 2;
|
||||
const keep = m.target && !m.lost && !gone(m.target) && m.at;
|
||||
const [r, c] = keep && landable(battle, unit, m.at[0], m.at[1]) ? m.at : [unit.tileR, unit.tileC];
|
||||
battle.moveRedeploy(unit, r, c);
|
||||
battle.fx('blink', { x: unit.x, y: unit.y, id: unit.id, skill: 'chen3:respawn' });
|
||||
if (!unit.alive || !skill.active) return;
|
||||
const p = num(b2['chen3_s2[respawn_buff].prob']);
|
||||
battle.addBuff(unit, { key: AFTER_KEY, mods: { atkPct: num(b2['chen3_s2[respawn_buff].atk']), dodgePhys: p, dodgeArts: p }, tags: ['skill'] });
|
||||
skill.timeLeft = Math.max(0.01, skill.duration);
|
||||
};
|
||||
/** S2: one slash on the current target; a target that falls hands over (+1 slash) or ends the slashing. */
|
||||
const slash = (battle, unit, m) => {
|
||||
const e = m.target;
|
||||
m.n++;
|
||||
m.at = [Math.round(e.y), Math.round(e.x)]; // record_position (each slash)
|
||||
battle.fx('slash', { x: e.x, y: e.y, id: unit.id, n: 1, skill: 'chen3:slash' });
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(b2.atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill', 'chen3:slash'] });
|
||||
};
|
||||
/** S2: the current target left — retarget (+1 slash, +0.4 s) or end the slashing (it was lost). */
|
||||
const handOver = (m, battle, unit) => {
|
||||
const from = { e: m.target, x: m.target.x, y: m.target.y };
|
||||
const next = nextTarget(battle, unit, from);
|
||||
if (!next) { m.lost = true; return false; }
|
||||
m.target = next;
|
||||
m.lost = false;
|
||||
m.hits++;
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
// S2's slashes replace her attacks
|
||||
trait: { canAttack: (b, u) => !(u.mem.chen3S2 && u.mem.chen3S2.phase === 1) },
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b1.atk) },
|
||||
attack: {
|
||||
hits: 2, // 二连击
|
||||
onHit({ battle, unit, skill, target }) { // 特殊能力失效 until the skill ends
|
||||
if (!target || !target.alive || target.side !== 'enemy' || !skill.active) return;
|
||||
if (battle.applyStatus(target, 'silence', { duration: Math.max(0.01, skill.timeLeft), source: unit })) unit.mem.chen3S1Silenced?.add(target);
|
||||
},
|
||||
},
|
||||
onStart({ battle, unit, skill }) { unit.mem.chen3S1End = battle.time + skill.timeLeft; unit.mem.chen3S1Silenced = new Set(); },
|
||||
onEnd({ battle, unit, reason }) {
|
||||
const hit = unit.mem.chen3S1Silenced;
|
||||
unit.mem.chen3S1Silenced = null;
|
||||
if (reason === 'duration' || !hit) return;
|
||||
// an early end lifts the silences she set (derived from the skill: _finishDerivedBuffIfParentFinish) — the ones
|
||||
// that would have ended with it (a tick of slack: the status and the skill count down in different phases)
|
||||
const left = num(unit.mem.chen3S1End) - battle.time + SILENCE_SLACK;
|
||||
for (const e of hit) {
|
||||
for (const s of [...e.buffs]) if (s.key === 'silence' && s.source === unit && s.timeLeft <= left) battle.removeBuff(e, s);
|
||||
}
|
||||
},
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
onStart({ battle, unit, skill }) {
|
||||
const m = unit.mem.chen3S2 = { phase: 1, t: 0, n: 0, hits: slashes, target: null, at: null, lost: false };
|
||||
battle.addBuff(unit, { key: DASH_KEY, flags: { invulnerable: true, noBlock: true, noSp: true, silence: true }, tags: ['skill'] });
|
||||
battle.releaseBlocked(unit);
|
||||
m.target = nearest(battle.enemiesInKeys(keysOf(unit, grid2), unit, AIR), unit.x, unit.y);
|
||||
battle.fx('dash', { x: unit.x, y: unit.y, id: unit.id, skill: 'chen3:dash' });
|
||||
if (!m.target) landS2(battle, unit, skill, m);
|
||||
},
|
||||
onTick({ battle, unit, skill, dt }) {
|
||||
const m = unit.mem.chen3S2;
|
||||
if (!m || m.phase !== 1) return;
|
||||
skill.timeLeft = Math.max(0.01, skill.duration) + dt; // the 6 s start after the move
|
||||
m.t += dt;
|
||||
if (gone(m.target) && !handOver(m, battle, unit)) { landS2(battle, unit, skill, m); return; }
|
||||
while (m.n < m.hits && m.t + 1e-9 >= SLASH_IV * m.n) {
|
||||
slash(battle, unit, m);
|
||||
if (!unit.alive || !skill.active) return;
|
||||
if (gone(m.target) && !handOver(m, battle, unit)) { landS2(battle, unit, skill, m); return; }
|
||||
}
|
||||
if (m.n >= m.hits && m.t + 1e-9 >= SLASH_IV * m.hits) landS2(battle, unit, skill, m);
|
||||
},
|
||||
onEnd({ battle, unit }) {
|
||||
battle.removeBuff(unit, DASH_KEY);
|
||||
battle.removeBuff(unit, AFTER_KEY);
|
||||
unit.mem.chen3S2 = null;
|
||||
},
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
targeting: { rangeGrid: grid3, maxTargets: Math.max(1, Math.floor(num(b3['attack@max_target'], 3))) },
|
||||
attack: { atkScale: num(b3['attack@atk_scale'], 1), hits: 3 }, // `_additionalTimes` 2
|
||||
onStart({ battle, unit, skill }) {
|
||||
const [dr, dc] = unit.fwd;
|
||||
unit.mem.chen3Wave = { x: unit.x, y: unit.y, dr, dc, hit: new Set(), act: skill.activations, seq: unit.deploySeq };
|
||||
battle.fx('dash', { x: unit.x, y: unit.y, id: unit.id, skill: 'chen3:wave' });
|
||||
},
|
||||
onEnd({ unit }) { unit.mem.chen3Wave = null; },
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 形意洞照: ATK / ASPD; every physical / arts damage of hers is 弱点伤害
|
||||
statBuff(battle, unit, 'talent:chen3:insight', { atkPct: num(t0.atk), aspd: num(t0.attack_speed) });
|
||||
battle.on('hit', (c) => { if (c.source === unit && c.target && c.target.side === 'enemy') weaknessRetype(c.dmg, unit, c.target); }, { owner: unit, priority: 5 });
|
||||
} },
|
||||
{ install(battle, unit) { // 寒暑觉知: every stack_time s without damage: a random self-heal and one held dodge
|
||||
const st = Math.max(1, Math.floor(num(t1.stack_time, 7))), lo = num(t1.heal_atk_scale_min), hi = num(t1.heal_atk_scale_max, lo + 1);
|
||||
const fresh = () => ({ count: 0, acc: 0, evade: false });
|
||||
unit.mem.chen3T2 = fresh();
|
||||
// a 【移动】 (S2) keeps it — the dodge by PRTS, the counter as the engine's 【移动】 keeps the rest
|
||||
battle.on('deploy', (c) => { if (c.unit === unit && !c.move) unit.mem.chen3T2 = fresh(); }, { owner: unit });
|
||||
battle.on('tick', ({ dt }) => {
|
||||
const m = unit.mem.chen3T2;
|
||||
if (!up(unit) || !m) return;
|
||||
m.acc += dt;
|
||||
while (m.acc >= 1 - 1e-9) {
|
||||
m.acc -= 1;
|
||||
if (++m.count < st) continue;
|
||||
m.count = 0;
|
||||
m.evade = true;
|
||||
const pct = lo + battle.rng.int(Math.max(1, Math.floor(hi - lo)));
|
||||
battle.heal(unit, unit, unit.s.atk * pct / 100, { self: true });
|
||||
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id, skill: 'chen3:evade' });
|
||||
}
|
||||
}, { owner: unit });
|
||||
battle.on('damaged', (c) => {
|
||||
const m = unit.mem.chen3T2;
|
||||
if (c.target !== unit || !m || !c.dmg || isHpLoss(c.dmg) || c.type === 'element') return;
|
||||
m.count = 0;
|
||||
m.acc = 0;
|
||||
}, { owner: unit });
|
||||
// the held dodge: after her other dodges (priority −1000), on a dodgeable physical / arts damage instance
|
||||
battle.on('hit', (c) => {
|
||||
const m = unit.mem.chen3T2, d = c.dmg;
|
||||
if (c.target !== unit || !m || !m.evade || !d || d.cancel || !d.canDodge || (d.type !== 'phys' && d.type !== 'arts')) return;
|
||||
const p = d.type === 'phys' ? unit.s.dodgePhys : unit.s.dodgeArts;
|
||||
if (!(p > 0 && battle.rng() < p)) m.evade = false; // one of her other dodges first: then it is kept
|
||||
d.cancel = true;
|
||||
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
|
||||
battle.emit('dodge', { source: c.credit ?? c.source, target: unit, dmg: d });
|
||||
}, { owner: unit, priority: -1000 });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// AFT-X “记忆残页”: 未阻挡敌人时攻击速度+8
|
||||
const as = num(tb.attack_speed);
|
||||
if (as) toggleBuff(battle, unit, 'trait:chen3:unblocked', () => unit.blocking.length === 0, { aspd: as });
|
||||
// S2: 眩晕 / 冻结 immunity while she slashes
|
||||
battle.on('beforeStatus', (c) => {
|
||||
if (c.target === unit && unit.mem.chen3S2 && unit.mem.chen3S2.phase === 1 && (c.status === 'stun' || c.status === 'freeze')) c.cancel = true;
|
||||
}, { owner: unit });
|
||||
// S3: the sword wave
|
||||
if (unit.skill?.id !== S3) return;
|
||||
const ratio = num(b3.hp_ratio), minScale = num(b3.projectile_min_atk_scale);
|
||||
battle.on('tick', ({ dt }) => {
|
||||
const w = unit.mem.chen3Wave;
|
||||
if (!w) return;
|
||||
const sk = unit.skill;
|
||||
if (!up(unit) || unit.deploySeq !== w.seq || !sk || !sk.active || sk.activations !== w.act) { unit.mem.chen3Wave = null; return; }
|
||||
for (let i = 0; i < 4 && waveBlocked(battle, w); i++) { // turn 90° clockwise (row up, col right)
|
||||
const dr = w.dr;
|
||||
w.dr = 0 - w.dc; // (`0 - x`: never a negative zero)
|
||||
w.dc = dr;
|
||||
w.hit.clear();
|
||||
}
|
||||
if (!waveBlocked(battle, w)) { w.x += w.dc * WAVE_SPEED * dt; w.y += w.dr * WAVE_SPEED * dt; }
|
||||
for (const e of battle.foesInRadius(w.x, w.y, WAVE_HIT_RADIUS)) {
|
||||
if (w.hit.has(e) || !canTargetEnemy(unit, e, AIR)) continue;
|
||||
w.hit.add(e);
|
||||
const amount = Math.max(ratio * e.hp, minScale * unit.s.atk);
|
||||
battle.fx('slash', { x: e.x, y: e.y, id: unit.id, n: 1, skill: 'chen3:wave' });
|
||||
battle.dealDamage(unit, e, { amount, type: 'arts', isSkill: true, tags: ['skill', 'chen3:wave'] });
|
||||
if (!unit.alive || unit.mem.chen3Wave !== w) return;
|
||||
}
|
||||
}, { owner: unit });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,341 @@
|
||||
// server/sim/content/kits/ops/op-leizi2.js — 司霆惊蛰 (char_1043_leizi2) 自选 operator kit: 6★ 解放者 (近卫), an owned-6★
|
||||
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (LIB-X) at every form.
|
||||
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_1043_leizi2, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
|
||||
// elite = E2 Lv60, rank 7, LIB-X at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
|
||||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); the client's battle
|
||||
// logic — buff_template_data (leizi2_tr, leizi2_e_002_tr, leizi2_s_cast, leizi2_t_1[atk_up] / [on_calcul] / [fly] /
|
||||
// [trigger] / [buff_start], leizi2_s_2[try_add_atk], leizi2_s_3), her charpack (Trait buff: triggerInterval 1; the
|
||||
// "thunder" ability: `_maxCount` 15, `_resetCountList` 3…7, `_rangeCastDelay` 0.25; S1 abilities s1_m / s1_l / s1_r:
|
||||
// `_preDelay` 0.3, ground selectors; S3 attack: projectile_char_leizi2_s3 every `_triggerDelta` 0.28 s), the skill prefabs
|
||||
// (skchr_leizi2_1: leizi2_s_1 lifeTime 0.5; skchr_leizi2_2: the flood-fill selector `_depth` 3, `checkHeightType`) and
|
||||
// the projectile prefabs (projectile_char_leizi2_t_1 / [start]: lands `_delayTime` 0.15 s after its cast, ground + air;
|
||||
// projectile_char_leizi2_s3: speed 1, `_lifeTime` 3.1, `_keepAlreadyHitTime` 0.6, ground only, magical, modifier key
|
||||
// "leizi2", rebounds off its source), her skeleton's clips (Skill_1_Begin 0.5 s with OnAttack at 0.3; Skill_2_Begin /
|
||||
// Skill_3_Begin 0.5 s; Skill_1_End / Skill_2_End 0.933 s, Skill_3_End 0.833 s) — and PRTS 司霆惊蛰 (talent / skill 备注).
|
||||
// The official map of a battle is its field (`rect`): stage tiles beyond it (another player's half) are no tiles here —
|
||||
// no strike falls there, S2's range and S3's currents stop at its edge [ASSUMED].
|
||||
// - Trait (解放者) "通常不攻击且阻挡数为0,技能未开启时40秒内攻击力逐渐提升至最高+200%且技能结束时重置攻击力": the librator
|
||||
// profile (professions.js: no attack and block 0 while the skill is off, ATK +atk / max_stack_cnt every second up to
|
||||
// +atk while it is off, reset when it ends). Module LIB-X “一念” "部署后获得+100%加成": its hidden talent init_atk
|
||||
// (leizi2_e_002_tr: init_atk / (atk / max_stack_cnt) stacks at each deployment) — the profile's rampInit (her DISPLAY
|
||||
// trait part carries no blackboard, unlike 玛恩纳's, so the kit hands it over).
|
||||
// - T1 明断 "攻击范围内每个地块每秒有10%的概率落雷对所有敌人造成相当于攻击力100%的法术伤害。未开启技能时起飞;技能期间攻击时攻击
|
||||
// 力提升至107%":
|
||||
// · the strikes (PRTS 备注's algorithm, the thunder ability's numbers): every 1.0333 s from each deployment, each tile of
|
||||
// her current attack range — first seen: a counter at a random 0…14; else +1, and at 15 the tile is struck and its
|
||||
// counter restarts at a random 3…7; counters outlive a tile leaving the range and die with the deployment. A strike
|
||||
// lands 0.15 s after its cast: atk_scale_t × ATK arts on every enemy she can select on that tile, air units too
|
||||
// (PRTS 可对空). Counting pauses while she cannot act [ASSUMED: the ability does not cast under 晕眩 / 冻结].
|
||||
// · 起飞 while no skill runs (leizi2_t_1[fly] in her default mode only; gamedata_const ba.liftoff): flag `liftoff` —
|
||||
// no ground enemy selects her or is blocked by her (her block count is 0 then anyway). PRTS "开启技能时的降落动作与技能
|
||||
// 结束时的起飞动作…期间自身不处于起飞状态,其中降落动作计入技能总时长": she lands at the cast and makes no attack for the
|
||||
// landing clip (S2 / S3 0.5 s, inside the skill's time; S1's attacks are its own), and after the skill she is up
|
||||
// again only when the take-off clip is over (S1 / S2 0.933 s, S3 0.833 s) — ground enemies may select her meanwhile;
|
||||
// her block count is the trait's 0 from the skill's end on.
|
||||
// · leizi2_t_1[on_calcul] (AtkScaleUp on ON_CALCULATE_DAMAGE while a skill runs; PRTS "对自身技能期间造成的所有伤害生效"):
|
||||
// an atkScaleMul ×atk_scale[skill_up] buff for the skill's time — her attacks (ai.js resolveHit) and every damage this
|
||||
// kit computes (strikes, S1, currents) multiply it. LIB-X stage 2+: the module talent's value (stage 3: 1.13).
|
||||
// - T2 追责 "开启技能时,使攻击范围内所有地面地块落雷,对所有敌人造成相当于攻击力100%的法术伤害和2秒战栗": at every cast (each S1
|
||||
// charge too) every low tile of her attack range — the skill's, S2's flood fill included — is struck, ring by ring by
|
||||
// Manhattan distance from her tile (PRTS "自内向外"), 0.25 s per ring (the thunder `_rangeCastDelay`; 0.07 s under S1 —
|
||||
// leizi2_t_1[trigger] `_specialRangeCastDelay`), each landing 0.15 s after its cast: atk_scale_t2 × ATK arts and 战栗
|
||||
// (`tremble`) not_combat s on every enemy she can select there, air units too. The strikes start at the cast
|
||||
// [ASSUMED: leizi2_t_1[trigger] rides her landing clip's start event (≤ 0.17 s in), not modelled].
|
||||
// - S1 浩气长存 (MANUAL, 可充能 3 次, data SEARCH): a 0.5 s skill (PRTS "技能持续0.5s"; skill prefab lifeTime 0.5) whose
|
||||
// range is the 3-19; no normal attack in it; 0.3 s after the cast (the abilities' `_preDelay`) three attacks — a 3-2 to
|
||||
// her left, her front and her right (PRTS: the 3-19 only explains the coverage) — each hit every ground enemy she can
|
||||
// select on it for attack@atk_scale_s1 × ATK physical (one on her own tile takes all three). "充能耗尽前特性不重置"
|
||||
// (leizi2_s_cast: the trait resets only when no charge is left): while a charge remains the ramp is kept.
|
||||
// - S2 正霆摄威 (MANUAL, data SEARCH): at the cast her range becomes the low tiles reachable from hers in up to three 4-way
|
||||
// steps over the field (PRTS 备注: "任意高度类型为低地的地块以及干员自己所在的地块", "不存在地块数据的地块始终视为不合法";
|
||||
// a real attack range — rangeExtend applies), fixed for the skill; attacks hit attack@max_target_s2 targets for
|
||||
// attack@atk_scale_s2 × ATK physical, air units too (PRTS 攻击可对空); every lightning cast of hers while it runs
|
||||
// (leizi2_s_2[try_add_atk], the thunder ability's buffsToOwnerOnCast — 追责's strikes too) gives ATK +thunder_atk, up
|
||||
// to thunder_max_stack_cnt stacks, gone when it ends.
|
||||
// - S3 天地通明 (MANUAL, data CUSTOM_RANGE on its 2-1): range 2-1, attack interval +base_attack_time s (flat), each attack
|
||||
// hits its target and every enemy within attack@range_radius of it for attack@atk_scale_s3 × ATK physical (air units
|
||||
// too: PRTS 普通攻击可对空); after each attack four currents leave the target's spot — up, right, down, left (screen
|
||||
// directions), 0.28 s apart (the first at the hit [ASSUMED]) — at 1 tile/s for 3.1 s, turning back before they would
|
||||
// enter a high tile, her own tile or the field's edge (PRTS "只在即将进入地块时进行反弹判定"), gone when she leaves (they
|
||||
// outlive the skill [ASSUMED]); each hits every ground enemy she can select on its
|
||||
// tile for attack@atk_scale_current × ATK arts, each enemy at most every 0.6 s per current, and — while S3 runs
|
||||
// (leizi2_s_3 ON_OUTPUT_MODIFIER, modifier key "leizi2") — with `prob` to 战栗 it not_combat s.
|
||||
|
||||
import { num, talentBb, moduleBb, skillRec, batMod, up } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { toLocal } from '../../../dir.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
|
||||
const S1 = 'skchr_leizi2_1';
|
||||
const S2 = 'skchr_leizi2_2';
|
||||
const S3 = 'skchr_leizi2_3';
|
||||
/** The skill ranges when a record carries none (data: 3-19, 2-1). */
|
||||
const R3_19 = Object.freeze([[3, 0], [2, 0], [1, 0], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-2, 0], [-3, 0]]);
|
||||
const R2_1 = Object.freeze([[2, 0], [1, 0], [1, 1], [0, 0], [0, 1], [0, 2], [-1, 0], [-1, 1], [-2, 0]]);
|
||||
/** S1's three attacks (PRTS 备注): a 3-2 to her front, her left and her right (facing-RIGHT offsets). */
|
||||
const S1_ARMS = Object.freeze([
|
||||
Object.freeze([[0, 0], [0, 1], [0, 2], [0, 3]]),
|
||||
Object.freeze([[0, 0], [1, 0], [2, 0], [3, 0]]),
|
||||
Object.freeze([[0, 0], [-1, 0], [-2, 0], [-3, 0]]),
|
||||
]);
|
||||
/** skchr_leizi2_1 leizi2_s_1 lifeTime (PRTS "技能持续0.5s") and the s1_* abilities' `_preDelay`. */
|
||||
const S1_TIME = 0.5;
|
||||
const S1_STRIKE_AT = 0.3;
|
||||
/** PRTS 明断 备注: "部署后每1.0333秒对自身当前攻击范围内所有地块执行计数判定" (31 ticks). */
|
||||
const COUNT_IV = 31 / 30;
|
||||
/** The thunder ability: a strike at `_maxCount` 15, then a restart at one of `_resetCountList` 3…7; a new tile 0…14. */
|
||||
const COUNT_MAX = 15;
|
||||
const RESET_MIN = 3;
|
||||
const RESET_SPAN = 5;
|
||||
/** projectile_char_leizi2_t_1 / [start] `_delayTime`: a strike lands this long after its cast. */
|
||||
const STRIKE_DELAY = 0.15;
|
||||
/** 追责: the thunder `_rangeCastDelay` per Manhattan ring; leizi2_t_1[trigger] `_specialRangeCastDelay` under S1. */
|
||||
const RING_DELAY = 0.25;
|
||||
const RING_DELAY_S1 = 0.07;
|
||||
/** projectile_char_leizi2_s3: speed, lifetime, one hit per enemy per this long; the S3 attack's `_triggerDelta`. */
|
||||
const CURRENT_SPEED = 1;
|
||||
const CURRENT_LIFE = 3.1;
|
||||
const CURRENT_REHIT = 0.6;
|
||||
const CURRENT_GAP = 0.28;
|
||||
/** PRTS: "依次生成向上/向右/向下/向左移动的电流" — screen directions as [dRow, dCol] (row 0 is the bottom). */
|
||||
const CURRENT_DIRS = Object.freeze([[1, 0], [0, 1], [-1, 0], [0, -1]]);
|
||||
const FOUR = Object.freeze([[1, 0], [0, 1], [-1, 0], [0, -1]]);
|
||||
const AIR = Object.freeze({ canHitFly: true });
|
||||
const GROUND = Object.freeze({ canHitFly: false });
|
||||
/** Her skeleton's landing clips (Skill_2_Begin / Skill_3_Begin; S1's is its own 0.5 s) and take-off clips (Skill_N_End). */
|
||||
const LANDING = Object.freeze({ [S2]: 0.5, [S3]: 0.5 });
|
||||
const TAKEOFF = Object.freeze({ [S1]: 0.933, [S2]: 0.933, [S3]: 0.833 });
|
||||
const LIFTOFF_KEY = 'talent:leizi2:liftoff';
|
||||
const SKILL_UP_KEY = 'talent:leizi2:skillUp';
|
||||
const S2_ATK_KEY = 'skill:leizi2:thunderAtk';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** Absolute tile keys of a grid at the unit's tile and facing. */
|
||||
const keysOf = (unit, grid) => absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0);
|
||||
/** A tile of the battle's field — the official map of a battle: no lightning falls on a stage tile beyond it. */
|
||||
const onField = (battle, k) => battle.grid.inRect(Math.floor(k / COLS), k % COLS);
|
||||
|
||||
/**
|
||||
* S2's range: the tiles reachable from hers in up to three 4-way steps over low tiles (her own tile always; a tile
|
||||
* without data never — off the battle's field: the official map of a battle is its field, `rect`), as facing-RIGHT
|
||||
* offsets (PRTS 备注, the selector's `_depth` 3).
|
||||
*/
|
||||
function floodGrid(battle, unit) {
|
||||
const r0 = unit.tileR, c0 = unit.tileC;
|
||||
const seen = new Set([r0 * COLS + c0]);
|
||||
const cells = [[r0, c0]];
|
||||
let frontier = [[r0, c0]];
|
||||
for (let step = 0; step < 3 && frontier.length; step++) {
|
||||
const next = [];
|
||||
for (const [r, c] of frontier) {
|
||||
for (const [dr, dc] of FOUR) {
|
||||
const rr = r + dr, cc = c + dc, k = rr * COLS + cc;
|
||||
if (seen.has(k) || !battle.grid.inRect(rr, cc) || battle.grid.tile(rr, cc).height !== 'LOW') continue;
|
||||
seen.add(k);
|
||||
next.push([rr, cc]);
|
||||
cells.push([rr, cc]);
|
||||
}
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
return cells.map(([r, c]) => toLocal(r - r0, c - c0, unit.dir));
|
||||
}
|
||||
|
||||
export default {
|
||||
char_1043_leizi2: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const hidden = moduleBb(chess); // LIB-X: init_atk
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s1 = skillRec(chess, S1), s3 = skillRec(chess, S3);
|
||||
const skillUp = num(t0['atk_scale[skill_up]'], 1);
|
||||
const s2Grid = [[0, 0]]; // this unit's S2 range, rebuilt at every cast
|
||||
|
||||
/** A lightning cast on tile `key` (T1's strikes, `accuse` = T2's): S2's stack at once, the strike 0.15 s later. */
|
||||
const cast = (battle, unit, key, accuse) => {
|
||||
const sk = unit.skill;
|
||||
if (sk && sk.active && sk.id === S2 && num(b2.thunder_atk) > 0) {
|
||||
battle.addBuff(unit, { key: S2_ATK_KEY, refresh: 'stack', stacks: 1, maxStacks: Math.max(1, Math.floor(num(b2.thunder_max_stack_cnt, 25))), mods: { atkPct: num(b2.thunder_atk) }, tags: ['skill'] });
|
||||
}
|
||||
const seq = unit.deploySeq;
|
||||
battle.after(STRIKE_DELAY, () => {
|
||||
if (!up(unit) || unit.deploySeq !== seq) return; // _stopWhenSourceInvalid
|
||||
const r = Math.floor(key / COLS), c = key % COLS;
|
||||
battle.fx('lightning', { x: c, y: r, id: unit.id, skill: accuse ? 'leizi2:accuse' : 'leizi2:thunder' });
|
||||
const scale = num(accuse ? t1.atk_scale_t2 : t0.atk_scale_t, 1);
|
||||
for (const e of battle.enemiesInKeys([key], unit, AIR)) {
|
||||
if (!e.alive) continue;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * scale * unit.s.atkScaleMul, type: 'arts', tags: ['talent', accuse ? 'leizi2:accuse' : 'leizi2:thunder'] });
|
||||
if (accuse && e.alive && num(t1.not_combat) > 0) battle.applyStatus(e, 'tremble', { duration: num(t1.not_combat), source: unit });
|
||||
}
|
||||
}, { owner: unit });
|
||||
};
|
||||
|
||||
return {
|
||||
trait: {
|
||||
// LIB-X “一念” 部署后获得+100%加成: the trait's start (the module's hidden talent; her trait part has no blackboard)
|
||||
...(num(hidden.init_atk) > 0 ? { rampInit: num(hidden.init_atk) } : {}),
|
||||
// S2 / S3: no attack while she lands (PRTS "降落动作计入技能总时长")
|
||||
canAttack: (b, u) => !(num(u.mem.leiziLandUntil) > b.time + 1e-9),
|
||||
},
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'duration',
|
||||
duration: S1_TIME,
|
||||
targeting: { rangeGrid: s1?.rangeGrid ?? R3_19 },
|
||||
attack: { noAttack: true }, // the three attacks below are the skill's
|
||||
onStart({ battle, unit, skill }) {
|
||||
const seq = unit.deploySeq, act = skill.activations;
|
||||
battle.after(S1_STRIKE_AT, () => {
|
||||
if (!up(unit) || unit.deploySeq !== seq || skill.activations !== act) return;
|
||||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 3, id: unit.id, skill: 'leizi2:s1' });
|
||||
for (const arm of S1_ARMS) {
|
||||
for (const e of battle.enemiesInKeys(keysOf(unit, arm), unit, GROUND)) {
|
||||
if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * num(b1['attack@atk_scale_s1'], 1) * unit.s.atkScaleMul, type: 'phys', isSkill: true, tags: ['skill', 'leizi2:s1'] });
|
||||
}
|
||||
}
|
||||
}, { owner: unit });
|
||||
},
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
targeting: { rangeGrid: s2Grid, canHitFly: true, maxTargets: Math.max(1, Math.floor(num(b2['attack@max_target_s2'], 3))) },
|
||||
attack: { atkScale: num(b2['attack@atk_scale_s2'], 1) },
|
||||
onStart({ battle, unit }) {
|
||||
const g = floodGrid(battle, unit);
|
||||
s2Grid.length = 0;
|
||||
for (const p of g) s2Grid.push(p);
|
||||
battle.refreshRange(unit);
|
||||
},
|
||||
onEnd({ battle, unit }) { battle.removeBuff(unit, S2_ATK_KEY); },
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { batPct: batMod(b3.base_attack_time, chess) },
|
||||
targeting: { rangeGrid: s3?.rangeGrid ?? R2_1, canHitFly: true },
|
||||
attack: {
|
||||
atkScale: num(b3['attack@atk_scale_s3'], 1),
|
||||
splashRadius: num(b3['attack@range_radius'], 1.65),
|
||||
splashScale: 1,
|
||||
onHit({ battle, unit, x, y }) { // the currents leave the target's spot
|
||||
const seq = unit.deploySeq;
|
||||
CURRENT_DIRS.forEach(([dr, dc], i) => {
|
||||
const spawn = () => {
|
||||
if (!up(unit) || unit.deploySeq !== seq) return;
|
||||
(unit.mem.leiziCurrents ??= []).push({ x, y, dr, dc, life: CURRENT_LIFE, seq, hitAt: new Map() });
|
||||
};
|
||||
if (i === 0) spawn();
|
||||
else battle.after(CURRENT_GAP * i, spawn, { owner: unit });
|
||||
});
|
||||
battle.fx('pulse', { x, y, id: unit.id, skill: 'leizi2:current' });
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 明断: the strikes, 起飞 while no skill runs, ×atk_scale[skill_up] during a skill
|
||||
const lift = () => { if (up(unit)) battle.addBuff(unit, { key: LIFTOFF_KEY, flags: { liftoff: true }, tags: ['talent'] }); };
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
if (!unit.skill?.active) lift();
|
||||
if (c.move) return;
|
||||
// the strike counters: a new list with every deployment
|
||||
const seq = unit.deploySeq;
|
||||
const counters = new Map();
|
||||
battle.every(COUNT_IV, (b, sc) => {
|
||||
if (!up(unit) || unit.deploySeq !== seq) { sc.cancel(); return; }
|
||||
if (!unit.canAct) return;
|
||||
for (const k of unit.rangeKeys) {
|
||||
if (!onField(b, k)) continue;
|
||||
if (!counters.has(k)) { counters.set(k, b.rng.int(COUNT_MAX)); continue; }
|
||||
const n = counters.get(k) + 1;
|
||||
if (n >= COUNT_MAX) { counters.set(k, RESET_MIN + b.rng.int(RESET_SPAN)); cast(b, unit, k, false); } else counters.set(k, n);
|
||||
}
|
||||
}, { owner: unit });
|
||||
}, { owner: unit });
|
||||
battle.on('skillStart', (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
battle.removeBuff(unit, LIFTOFF_KEY);
|
||||
unit.mem.leiziLandUntil = battle.time + (LANDING[unit.skill.id] ?? 0);
|
||||
if (skillUp !== 1) battle.addBuff(unit, { key: SKILL_UP_KEY, mods: { atkScaleMul: skillUp }, tags: ['talent'] });
|
||||
}, { owner: unit, priority: 50 });
|
||||
battle.on('skillEnd', (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
battle.removeBuff(unit, SKILL_UP_KEY);
|
||||
unit.mem.leiziLandUntil = 0;
|
||||
if (c.reason === 'death') return;
|
||||
// (the trait's own handler releases what she blocked) — 起飞 again once the take-off clip is over
|
||||
const seq = unit.deploySeq;
|
||||
battle.after(TAKEOFF[unit.skill.id] ?? 0, () => { if (unit.deploySeq === seq && !unit.skill?.active) lift(); }, { owner: unit });
|
||||
}, { owner: unit });
|
||||
} },
|
||||
{ install(battle, unit) { // 追责: every cast strikes the low tiles of her range, ring by ring from her tile
|
||||
if (!(num(t1.atk_scale_t2) > 0)) return;
|
||||
battle.on('skillStart', (c) => {
|
||||
if (c.unit !== unit) return;
|
||||
const gap = unit.skill?.id === S1 ? RING_DELAY_S1 : RING_DELAY;
|
||||
const rings = new Map();
|
||||
for (const k of unit.rangeKeys) {
|
||||
const r = Math.floor(k / COLS), cc = k % COLS;
|
||||
if (!onField(battle, k) || !battle.grid.isLow(r, cc)) continue;
|
||||
const d = Math.abs(r - unit.tileR) + Math.abs(cc - unit.tileC);
|
||||
if (!rings.has(d)) rings.set(d, []);
|
||||
rings.get(d).push(k);
|
||||
}
|
||||
const seq = unit.deploySeq;
|
||||
for (const d of [...rings.keys()].sort((a, b) => a - b)) {
|
||||
const fire = () => { if (up(unit) && unit.deploySeq === seq) for (const k of rings.get(d)) cast(battle, unit, k, true); };
|
||||
if (d === 0) fire();
|
||||
else battle.after(d * gap, fire, { owner: unit });
|
||||
}
|
||||
}, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
const id = unit.skill?.id;
|
||||
if (id === S1) {
|
||||
// 充能耗尽前特性不重置: the trait resets the ramp on skillEnd (priority 0) — keep it while a charge remains
|
||||
battle.on('skillEnd', (c) => {
|
||||
if (c.unit === unit) unit.mem.leiziRamp = c.reason !== 'death' && unit.skill.charges > 0 ? num(unit.trait.ramp) : null;
|
||||
}, { owner: unit, priority: 100 });
|
||||
battle.on('skillEnd', (c) => {
|
||||
if (c.unit !== unit || unit.mem.leiziRamp == null) return;
|
||||
unit.trait.ramp = unit.mem.leiziRamp;
|
||||
unit.mem.leiziRamp = null;
|
||||
if (unit.trait.ramp > 0 && unit.alive) battle.addBuff(unit, { key: 'trait:libratorRamp', mods: { atkPct: unit.trait.ramp } });
|
||||
}, { owner: unit, priority: -100 });
|
||||
}
|
||||
if (id !== S3) return;
|
||||
// S3: the currents
|
||||
const scale = num(b3['attack@atk_scale_current']), prob = num(b3.prob), stop = num(b3.not_combat);
|
||||
battle.on('tick', ({ dt }) => {
|
||||
const list = unit.mem.leiziCurrents;
|
||||
if (!list || !list.length) return;
|
||||
const keep = [];
|
||||
for (const cur of list) {
|
||||
cur.life -= dt;
|
||||
if (cur.life <= 1e-9 || !up(unit) || unit.deploySeq !== cur.seq) continue;
|
||||
const nx = cur.x + cur.dc * CURRENT_SPEED * dt, ny = cur.y + cur.dr * CURRENT_SPEED * dt;
|
||||
const r = Math.round(ny), c = Math.round(nx);
|
||||
if (r !== Math.round(cur.y) || c !== Math.round(cur.x)) {
|
||||
// about to enter (r, c): back off a high tile (高台), her own tile or the field's edge [ASSUMED]
|
||||
if (!battle.grid.inRect(r, c) || battle.grid.tile(r, c).height === 'HIGH' || (r === unit.tileR && c === unit.tileC)) { cur.dr = 0 - cur.dr; cur.dc = 0 - cur.dc; } else { cur.x = nx; cur.y = ny; }
|
||||
} else { cur.x = nx; cur.y = ny; }
|
||||
keep.push(cur);
|
||||
const s3On = !!(unit.skill?.active && unit.skill.id === S3);
|
||||
for (const e of battle.enemiesInKeys([Math.round(cur.y) * COLS + Math.round(cur.x)], unit, GROUND)) {
|
||||
const at = cur.hitAt.get(e);
|
||||
if (!e.alive || (at != null && battle.time - at < CURRENT_REHIT - 1e-9)) continue;
|
||||
cur.hitAt.set(e, battle.time);
|
||||
const dealt = battle.dealDamage(unit, e, { amount: unit.s.atk * scale * unit.s.atkScaleMul, type: 'arts', isSkill: true, tags: ['skill', 'leizi2:current'] });
|
||||
if (s3On && dealt > 0 && e.alive && stop > 0 && battle.rng.chance(prob)) battle.applyStatus(e, 'tremble', { duration: stop, source: unit });
|
||||
}
|
||||
if (!unit.alive) break;
|
||||
}
|
||||
unit.mem.leiziCurrents = keep;
|
||||
}, { owner: unit });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,229 @@
|
||||
// server/sim/content/kits/ops/op-thorns.js — 棘刺 (char_293_thorns) 自选 operator kit: 6★ 领主 (近卫), an owned-6★ pick of
|
||||
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (LOR-X, LOR-Δ) at every form.
|
||||
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_293_thorns, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
|
||||
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
|
||||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); the client's battle
|
||||
// logic — buff_template_data (thorns_t_1, thorns_s_2, thorns_e_002_t / [mark] / _tr, thorns_e_003_t / _tr), his charpack
|
||||
// (talent 1 `_additiveActiveBuffs` thorns_t_1: triggerInterval 1, waitFirstTriggerInterval, independentCharacterSource;
|
||||
// talent 2 toggle `_disableWhenInAttackState` / `InCombatState`, `_restoreDelay` 2) and the skill prefabs (skchr_thorns_2
|
||||
// Thorn: `_preDelay` 0.5, cooldown key `cooldown`, the trait's 0.8 scale, ground + air selector, `max_target`;
|
||||
// skchr_thorns_3 [a] / [b] buffs), the module parts ([uc]equips thorns_equip_1_n / 2_n: the trait buffs, and from stage 2
|
||||
// the talent overrides — LOR-X `_targetFamilyMask` 31, LOR-Δ 3) — and PRTS 棘刺 (talent 备注, S2 备注, S3 备注, LOR-X 特性
|
||||
// "于计算伤害时触发").
|
||||
// - Trait (领主) "可以进行远程攻击,但此时攻击力降低至80%": the lord profile (professions.js: ×atk_scale unless the target
|
||||
// stands on his tile / the tile in front or he blocks it), range 3-12, hits air units (data canHitFly), blocks 2.
|
||||
// Module LOR-X “盐与沙” adds "攻击附带10%攻击力的法术伤害" (trait atk_scale_m; thorns_e_002_tr: ON_CALCULATE_DAMAGE, attack
|
||||
// type ADDITION — PRTS "造成预计算法术附加伤害"): atk_scale_m × ATK arts after every hit of his attacks (the trait's
|
||||
// afterHit, as 拉普兰德 / 断崖 / 仇白 / 银灰 do it; tags module / addition) and of S2's spikes [ASSUMED: the Thorn ability
|
||||
// calculates its damage like an attack; the fixed-value poison ticks do not]. Module LOR-Δ “沙蚀” adds
|
||||
// "造成伤害时附带相当于10%伤害的神经损伤" (trait ep_damage_ratio; thorns_e_003_tr: ON_AFTER_OUTPUT_DAMAGE, every damage
|
||||
// type): ep_damage_ratio × the HP damage of every damage instance he deals — attacks, spikes, LOR-X additions and the
|
||||
// poison ticks — as 神经损伤 (docs/SIM.md §7.2 element conventions).
|
||||
// - T1 神经腐蚀 "攻击使目标中毒,在3秒内每秒受到125点法术伤害(对会远程攻击的目标伤害加倍)": every hit of his attacks (and
|
||||
// of the S2 spikes, PRTS "所有攻击均能附加") poisons the target — one poison per 棘刺 (independentCharacterSource) for
|
||||
// `duration` s, ticking every second, the first a second in (waitFirstTriggerInterval), damage[normal] or
|
||||
// damage[ranged] (an enemy whose 攻击方式 includes 远程: data applyWay RANGED or ALL; PRTS "伤害加倍为数值提升") as a fixed
|
||||
// arts 持续伤害 (FixedValueDamage MAGICAL, attack type BUFF: RES applies, no dodge, tags dot). A new hit refreshes the
|
||||
// duration and keeps the damage value and the tick rhythm (PRTS "仅刷新持续时间而不改变伤害数值"). The ticks go on after
|
||||
// he leaves the field [ASSUMED, as 伊芙利特's 灼伤]. A dodged hit poisons nothing (no `damaged`; 击中目标的攻击).
|
||||
// LOR-X stage 2+ (thorns_e_002_t: no end of its own, cnt 1; every hit a new [mark] of `duration` s whose start adds 1
|
||||
// to cnt up to max_cnt; a tick without a [mark] ends it — damage × cnt) "可叠加4次": the tick is the first application's
|
||||
// value × the hits so far (≤ max_cnt; PRTS "以第一层的伤害数值为基础进行翻倍"), the poison ending `duration` s after the
|
||||
// last hit; all his hits, spikes included (mask 31). LOR-Δ stage 2+ (thorns_e_003_t, ep_break_multi): on an enemy in its
|
||||
// 神经损伤 burst (the `neuralBurst` lock, FilterEPBreakRecoveryType SANITY) the tick is ×ep_break_multi 元素伤害; only his
|
||||
// normal attacks poison (mask 3; PRTS "仅有普通攻击能附加(意味着2技能不再附加中毒Buff)"), and that poison has no
|
||||
// independentCharacterSource: one per enemy whoever applies it. At stage 1 both modules leave the talent as it is
|
||||
// (battle_equip_table: no talent part in phase 1).
|
||||
// - T2 故土潮声 "如果2秒内没有主动攻击过,每秒恢复最大生命3.5%的生命": PRTS "增加“生命回复速度(百分比)”属性,不受治疗加成和禁疗
|
||||
// 影响" — an hpRegenRatio buff (checklist 11, never battle.heal), on while his last attack is at least `delay` s old
|
||||
// (the toggle's restoreDelay after the attack states) and from his deployment on [ASSUMED: no attack yet counts as
|
||||
// "2秒内没有主动攻击过"]. The S2 spikes are no 主动攻击 (PRTS "不会使第二天赋效果中断").
|
||||
// - S1 攻击力强化·γ型 (MANUAL, data DEFAULT): ATK +atk for `duration` s.
|
||||
// - S2 护身尖刺 (MANUAL, 技能范围 3-1, data SKILL_RANGE): "停止攻击" (no normal attack; PRTS "技能期间,自身丢失全部视野"),
|
||||
// ATK +atk, DEF +def; every 普通伤害 an enemy deals him (PRTS "指受到普通伤害(伤害类型),而非由普通攻击造成的伤害";
|
||||
// thorns_s_2 FilterDamageModifer attack type NORMAL: not 溅射, not 持续 (dot / periodic), not 附加, not element damage,
|
||||
// not a 流失, not 无来源) sets off the Thorn ability when its `cooldown` (0.8 / 0.75 s) is ready and he can act: 0.5 s
|
||||
// later (its `_preDelay`) up to max_target enemies of the 3-1 — ground and air, the usual target order — each take ATK ×
|
||||
// the trait's atk_scale (PRTS "始终受降低攻击倍率特性的影响") physical, a skill hit that is no normal attack (no `attack`
|
||||
// event, no SP), cancelled when the skill has ended, he left or cannot act by then [ASSUMED]. The 3-1 is not his
|
||||
// attack range: the card keeps his 3-12.
|
||||
// - S3 至高之术 (MANUAL, attack SP, data ACTIVE_RANGE on its 3-3): range 3-3, ATK +atk, ASPD +attack_speed, every attack at
|
||||
// full ATK ("远程攻击不再降低攻击力": the skill's attack profile has no 0.8 — dmgMul 1, as 领主·Sharp S1) for `duration` s;
|
||||
// "第二次及以后使用时能力加成变为最初的两倍,且持续时间无限": from the second activation of a deployment on, the
|
||||
// thorns_s_3[b] numbers (ATK / ASPD ×2) and no end until he leaves (a toggle; the first use counts its own 30 s down) —
|
||||
// PRTS "每次部署棘刺时,重新计算该技能的使用次数": the count restarts with every deployment.
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, up } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { hasHp, isHpLoss } from '../../../damage.js';
|
||||
|
||||
const S1 = 'skcom_atk_up[3]';
|
||||
const S2 = 'skchr_thorns_2';
|
||||
const S3 = 'skchr_thorns_3';
|
||||
/** The skill ranges when a record carries none (data: 3-1, 3-3). */
|
||||
const R3_1 = Object.freeze([[1, 0], [1, 1], [1, 2], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-1, 1], [-1, 2]]);
|
||||
const R3_3 = Object.freeze([[1, 0], [1, 1], [1, 2], [1, 3], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-1, 1], [-1, 2], [-1, 3]]);
|
||||
/** skchr_thorns_2 Thorn `_preDelay`: the spikes land this long after the hit that set them off. */
|
||||
const SPIKE_DELAY = 0.5;
|
||||
/** thorns_t_1 triggerInterval: a poison tick every second. */
|
||||
const POISON_IV = 1;
|
||||
const AIR = Object.freeze({ canHitFly: true });
|
||||
const S3_BUFF = 'skill:thorns:destreza';
|
||||
const REGEN_KEY = 'talent:thorns:tide';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** An enemy whose 攻击方式 includes 远程 (PRTS "图鉴中【攻击方式】包含远程的敌人"; data applyWay). */
|
||||
const attacksRanged = (e) => { const w = String(e?.def?.applyWay ?? '').toUpperCase(); return w === 'RANGED' || w === 'ALL'; };
|
||||
/**
|
||||
* 普通伤害 (PRTS 伤害分类; thorns_s_2 FilterDamageModifer attack type NORMAL) an enemy dealt — not 溅射, 持续 (dot /
|
||||
* periodic), 附加, element damage, a 流失 or 无来源 damage.
|
||||
*/
|
||||
function normalHitFromEnemy(c) {
|
||||
const d = c.dmg;
|
||||
if (!d || !c.source || c.source.side !== 'enemy' || d.sourceless || d.isSplash || isHpLoss(d)) return false;
|
||||
if (c.type === 'element' || c.type === 'elemental') return false;
|
||||
const tags = d.tags || [];
|
||||
return !tags.includes('dot') && !tags.includes('periodic') && !tags.includes('addition');
|
||||
}
|
||||
|
||||
export default {
|
||||
char_293_thorns: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const tb = traitBb(chess);
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const grid2 = skillRec(chess, S2)?.rangeGrid ?? R3_1, grid3 = skillRec(chess, S3)?.rangeGrid ?? R3_3;
|
||||
const rangedScale = num(tb.atk_scale, 0.8);
|
||||
const loreX = num(tb.atk_scale_m); // LOR-X: arts addition
|
||||
const loreD = num(tb.ep_damage_ratio); // LOR-Δ: 神经损伤 rider
|
||||
const maxCnt = Math.max(1, Math.floor(num(t0.max_cnt, 1))); // LOR-X stage 2+: 可叠加
|
||||
const breakMul = num(t0.ep_break_multi); // LOR-Δ stage 2+: ×multi 元素伤害 in a 神经 burst
|
||||
const spikesPoison = !(breakMul > 0); // LOR-Δ stage 2+: only normal attacks poison
|
||||
|
||||
/** LOR-X "攻击附带10%攻击力的法术伤害": the arts addition of one hit (PRTS: 预计算法术附加伤害). */
|
||||
const addition = (battle, unit, target) => {
|
||||
if (loreX > 0 && target && target.alive && target.side === 'enemy' && hasHp(target)) {
|
||||
battle.dealDamage(unit, target, { amount: unit.s.atk * loreX, type: 'arts', tags: ['module', 'addition', 'thorns:lorx'] });
|
||||
}
|
||||
};
|
||||
|
||||
/** T1 神经腐蚀: poison `e` (one per 棘刺; a new hit refreshes it, LOR-X stage 2+ adds a layer). */
|
||||
const poison = (battle, unit, e) => {
|
||||
if (!e || !e.alive || e.side !== 'enemy' || !hasHp(e)) return;
|
||||
const dur = num(t0.duration, 3);
|
||||
const base = num(attacksRanged(e) ? t0['damage[ranged]'] : t0['damage[normal]']);
|
||||
if (!(dur > 0) || !(base > 0)) return;
|
||||
// one poison per 棘刺 (independentCharacterSource) — but the LOR-Δ stage 2+ one (thorns_e_003_t has no such flag)
|
||||
const key = breakMul > 0 ? 'thorns:poison' : `thorns:poison:${unit.id}`;
|
||||
const old = e.findBuff(key);
|
||||
const b = battle.addBuff(e, {
|
||||
key, duration: dur, refresh: 'extend', interval: POISON_IV, source: unit, tags: ['dot', 'talent'],
|
||||
data: { base, cnt: 1 },
|
||||
onTick({ battle: bt, unit: tgt, buff }) {
|
||||
if (!tgt.alive || !hasHp(tgt)) return;
|
||||
let amount = buff.data.base * (maxCnt > 1 ? buff.data.cnt : 1);
|
||||
let type = 'arts';
|
||||
if (breakMul > 0 && tgt.findBuff('neuralBurst')) { amount *= breakMul; type = 'elemental'; }
|
||||
bt.dealDamage(unit, tgt, { amount, type, element: type === 'elemental' ? 'neural' : undefined, canDodge: false, tags: ['talent', 'dot', 'thorns:poison'] });
|
||||
},
|
||||
});
|
||||
if (old && b === old && maxCnt > 1) old.data.cnt = Math.min(maxCnt, num(old.data.cnt, 1) + 1);
|
||||
};
|
||||
|
||||
return {
|
||||
trait: {
|
||||
// LOR-X: every hit of his attacks carries the arts addition
|
||||
afterHit(battle, u, target) { addition(battle, u, target); },
|
||||
},
|
||||
skills: {
|
||||
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk) } },
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b2.atk), defPct: num(b2.def) },
|
||||
attack: { noAttack: true }, // 停止攻击
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'toggle',
|
||||
targeting: { rangeGrid: grid3 },
|
||||
attack: { dmgMul: () => 1 }, // 远程攻击不再降低攻击力
|
||||
onStart({ battle, unit, skill }) {
|
||||
const n = (unit.mem.thornsS3Uses = num(unit.mem.thornsS3Uses) + 1);
|
||||
const again = n >= 2;
|
||||
const atk = again ? num(b3['thorns_s_3[b].atk'], 2 * num(b3.atk)) : num(b3.atk);
|
||||
const aspd = again ? num(b3['thorns_s_3[b].attack_speed'], 2 * num(b3.attack_speed)) : num(b3.attack_speed);
|
||||
battle.addBuff(unit, { key: S3_BUFF, mods: { atkPct: atk, aspd }, tags: ['skill'] });
|
||||
// the first use runs `duration` s; from the second on "持续时间无限" ([b].duration −1)
|
||||
skill.timeLeft = again && num(b3['thorns_s_3[b].duration'], -1) < 0 ? Infinity : Math.max(0.01, skill.duration);
|
||||
},
|
||||
onTick({ skill, dt }) {
|
||||
if (!Number.isFinite(skill.timeLeft)) return;
|
||||
skill.timeLeft -= dt;
|
||||
if (skill.timeLeft <= 1e-9) skill.end('duration');
|
||||
},
|
||||
onEnd({ battle, unit }) { battle.removeBuff(unit, S3_BUFF); },
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 神经腐蚀: his attack hits (and the spikes, but under LOR-Δ stage 2+) poison
|
||||
battle.on('damaged', (c) => {
|
||||
if (c.source !== unit || !c.target || c.target.side !== 'enemy' || !c.dmg || c.type === 'element') return;
|
||||
const tags = c.dmg.tags || [];
|
||||
if (c.dmg.isAttack || (spikesPoison && tags.includes('thorns:spike'))) poison(battle, unit, c.target);
|
||||
}, { owner: unit });
|
||||
} },
|
||||
{ install(battle, unit) { // 故土潮声: 生命回复速度 while no attack of his is younger than `delay` s
|
||||
const r = num(t1.hp_recovery_per_sec_by_max_hp_ratio), delay = num(t1.delay, 2);
|
||||
if (!(r > 0)) return;
|
||||
unit.mem.thornsAttackAt = -Infinity;
|
||||
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.thornsAttackAt = -Infinity; }, { owner: unit });
|
||||
battle.on('attack', (c) => { if (c.attacker === unit) unit.mem.thornsAttackAt = battle.time; }, { owner: unit });
|
||||
const apply = () => {
|
||||
const want = up(unit) && battle.time - unit.mem.thornsAttackAt >= delay - 1e-9;
|
||||
const has = unit.findBuff(REGEN_KEY);
|
||||
if (want && !has) battle.addBuff(unit, { key: REGEN_KEY, mods: { hpRegenRatio: r }, tags: ['talent'] });
|
||||
else if (!want && has) battle.removeBuff(unit, REGEN_KEY);
|
||||
};
|
||||
battle.on('tick', apply, { owner: unit });
|
||||
battle.on('battleStart', apply, { owner: unit });
|
||||
battle.on('deploy', (c) => { if (c.unit === unit && battle.started) apply(); }, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// S3: the use count restarts with every deployment (PRTS "每次部署棘刺时,重新计算该技能的使用次数")
|
||||
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.thornsS3Uses = 0; }, { owner: unit });
|
||||
// LOR-Δ: 神经损伤 = ep_damage_ratio × the HP damage of every damage instance he deals
|
||||
if (loreD > 0) {
|
||||
battle.on('damaged', (c) => {
|
||||
if (c.source !== unit || !c.target || c.target.side !== 'enemy' || c.type === 'element' || !(c.amount > 0) || !hasHp(c.target)) return;
|
||||
battle.dealDamage(unit, c.target, { type: 'element', element: 'neural', amount: c.amount * loreD, tags: ['module', 'thorns:lord'] });
|
||||
}, { owner: unit });
|
||||
}
|
||||
if (unit.skill?.id !== S2) return;
|
||||
// S2 护身尖刺: 普通伤害 from an enemy ⇒ the Thorn ability (cooldown), 0.5 s later up to max_target spikes on the 3-1
|
||||
unit.mem.thornsSpikeAt = -Infinity;
|
||||
const cd = num(b2.cooldown, 0.8), n = Math.max(1, Math.floor(num(b2.max_target, 4)));
|
||||
battle.on('damaged', (c) => {
|
||||
const sk = unit.skill;
|
||||
if (c.target !== unit || !sk || !sk.active || !up(unit) || !unit.canAct || !normalHitFromEnemy(c)) return;
|
||||
if (battle.time < unit.mem.thornsSpikeAt + cd - 1e-9) return;
|
||||
unit.mem.thornsSpikeAt = battle.time;
|
||||
const seq = unit.deploySeq, act = sk.activations;
|
||||
battle.after(SPIKE_DELAY, () => {
|
||||
if (!up(unit) || unit.deploySeq !== seq || !unit.canAct || !unit.skill?.active || unit.skill.activations !== act) return;
|
||||
const list = battle.enemiesInKeys(absoluteRangeKeys(grid2, unit.tileR, unit.tileC, unit.dir, 0), unit, AIR);
|
||||
sortEnemyTargets(battle, unit, list, null);
|
||||
const targets = list.slice(0, n);
|
||||
if (!targets.length) return;
|
||||
battle.fx('thorns', { x: unit.x, y: unit.y, id: unit.id, n: targets.length });
|
||||
for (const e of targets) {
|
||||
if (!e.alive) continue;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * rangedScale * unit.s.atkScaleMul, type: 'phys', isSkill: true, tags: ['skill', 'thorns:spike'] });
|
||||
addition(battle, unit, e);
|
||||
}
|
||||
}, { owner: unit });
|
||||
}, { owner: unit });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user