Community report relayed by the owner: 炎祐好像吃到了不该吃到的地面伤害. 炎佑 is a flying ally (PRTS “炎佑” 行动方式 飞行). The ground terrain (活性源石), 烹泉 / 沏虹, 庞贝 and 集团军重型火炮 already skipped it, but these 不可对空 effects (PRTS, read for every enemy of data/enemies.json) went through areaAllies / fieldAllies / targetsNear, which exclude 起飞 allies only: - 死亡爆炸 of 高能源石虫 / 冰爆源石虫 / 卷心籽 (archetypes deathBoom: `noAir`, on by default; 寒冷 too); - 水遁忍者 漩涡形态 "每秒对半径1.0范围内的所有我方单位造成…(无视迷彩,不可对空)"; - 鼎沸 "攻击范围内的所有我方单位每秒受到…法术持续伤害与…灼燃损伤(不可对空)"; - “萨科塔昂首” 祈祷邀约 "令全场我方单位(不可对空,无视迷彩)获得15s【受邀祈祷】攻击速度-30"; - “斩胄之剑” / “破胄之锤” "普通攻击对攻击范围内的所有我方单位造成…物理普通伤害,不可对空" (its target rule too). A ground enemy's normal attack and the 不可对空-free effects still reach it (PRTS: enemies that hit air target it). Tests: test/content/feedback7-yanyou-noair.test.js (8; all fail on the parent).
141 lines
9.9 KiB
JavaScript
141 lines
9.9 KiB
JavaScript
// server/sim/content/enemies/dot.js — DOT 持续 kits (pulses, 污染秽蚀 / burning zones, bleeding) and their part of KITS
|
||
// (split from content/enemies.js).
|
||
|
||
import { SHELL_SPLASH_RADIUS, num, T, elem, hurt, areaAllies } from './helpers.js';
|
||
import { splashAttack, noAirTargets, resist, immuneTo, bleed, pollution, dmgZone, kitPolluted } from './archetypes.js';
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// constants (numbers that exist nowhere in the data)
|
||
|
||
/** 萨卡兹枯朽战车 秽蚀轰击: radius of the 【污染秽蚀】 it leaves at its target (PRTS 萨卡兹枯朽战车 技能 "在目标位置生成半径1.7,
|
||
* 持续10s的【污染秽蚀】" — the skill blackboard's range_radius 2.2 is its trigger range; the zone's radius is in no table). */
|
||
const TANK_ZONE_RADIUS = 1.7;
|
||
|
||
/** 集团军重型火炮's 【燃烧区域】: PRTS "碰撞半径1.5" with the source note "1.5倍可变半径" — ×1.5 on the blackboard's
|
||
* ProjectileBoomRange.attack@projectile_range (1). Until 0.1.3 the zone took the blackboard's 1. */
|
||
const UACANN_ZONE_SCALE = 1.5;
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// kits
|
||
|
||
/** 深溟巢涌者 / 富营养的巢涌者: one pulse every second (PRTS "每秒"; not its attack interval, so 攻速 changes leave it alone). */
|
||
const NEST_PULSE_INTERVAL = 1;
|
||
|
||
const NEST_PULSE_TAG = 'nestPulse';
|
||
|
||
/**
|
||
* 深溟巢涌者 / 富营养的巢涌者 (PRTS 天赋): "不进行普通攻击", 抵抗 (一减状态抵抗率 −0.5) and 停顿免疫; "未处于消失状态时,令攻击范围
|
||
* 内的所有我方单位每秒受到攻击力100%的无途径法术伤害" and "每次输出伤害时,再造成攻击力5%的神经损伤" (`EpDamage.ep_damage_ratio`).
|
||
* A talent aura, not an attack: `noAttack`, so it walks on while it hurts (ai.js attackStand holds only attackers — until
|
||
* 0.1.3 the pulse was its normal attack, which made it stand for its whole 1 s clip whenever an ally was in range:
|
||
* community report 「…错误的设置了攻击时不移动导致卡在原地」, GitHub #93), needs no target and pauses while it is hidden on a
|
||
* DISAPPEAR leg (消失). The pulse is an area selection (areaAllies: a 迷彩 ally and a flying one — the 炎佑 dragon — are
|
||
* hit, a 隐匿 one is not) of radius `rangeRadius` around its centre, like 鼎沸's. [ASSUMED] the pulse can be dodged (闪避:
|
||
* PRTS names no attack type, which the site reads as 普通伤害 — the 烹泉 splash rule) and a dodged one adds no 神经.
|
||
*/
|
||
function kitDsubrl(ab, e) {
|
||
const ratio = T(ab, 'EpDamage.ep_damage_ratio') ?? 0;
|
||
return [resist(['sluggish']), {
|
||
spawn(b, e2) {
|
||
e2.profile.noAttack = true;
|
||
// 每次输出伤害时: every pulse that reaches the unit (not dodged, not cancelled — the old attack's `dealt` rule)
|
||
if (ratio > 0) b.on('damaged', (c) => { if (c.source === e2 && c.dmg && c.dmg.tags.includes(NEST_PULSE_TAG)) elem(b, e2, c.target, 'neural', e2.s.atk * ratio); }, { owner: e2 });
|
||
},
|
||
iv: NEST_PULSE_INTERVAL,
|
||
tick(b, e2) {
|
||
if (e2.hidden) return;
|
||
const r = e2.base.rangeRadius || 1.6;
|
||
const l = areaAllies(b, e2, e2.x, e2.y, r);
|
||
if (!l.length) return;
|
||
b.fx('pulse', { x: e2.x, y: e2.y, r, id: e2.id, element: 'neural' });
|
||
for (const u of l) hurt(b, e2, u, e2.s.atk, 'arts', { canDodge: true, tags: [NEST_PULSE_TAG] });
|
||
},
|
||
}];
|
||
}
|
||
|
||
/**
|
||
* 萨卡兹枯朽战车 / 尖端 (PRTS; enemy_database spData "初始技力 2 / 技力上限 2 / 攻击回复"): an attack made with full SP is the
|
||
* skill 秽蚀轰击 — "对目标造成100%物理普通伤害,同时在目标位置生成半径1.7,持续10s的【污染秽蚀】" — and empties the SP; every
|
||
* other attack gains 1. Initial SP = max, so the FIRST attack is the skill, then every 3rd (attacks 1, 4, 7 …). Talent:
|
||
* "普通攻击只攻击位于低地的我方单位,且不会攻击飞行单位" (a candidate filter, ai.js enemyAttack: a FLY ally such as the 炎佑 dragon on a
|
||
* low tile is no target either); "被阻挡时进行近战攻击,造成攻击力200%的伤害" — normal attacks on its blocker ×chuang_atk_scale.
|
||
* [ASSUMED] the skill also fires on its blocker and deals the skill's own 100 % there (PRTS names no blocked exception:
|
||
* enemy SP skills fire as soon as they are ready and its condition — a low-ground non-flying ally within 2.2 — holds for
|
||
* the blocker), so a blocked tank hits 100 / 200 / 200 % … (v2.4.1: 200 % on every attack, the skill included).
|
||
* Reach: the 2.2 range circle takes an ally whose 0.25 collider touches it (ai.js enemyAttack, constants.js
|
||
* ALLY_COLLIDER_RADIUS; PRTS 作战机制 §碰撞体积) — 2.45 from the ally's centre, 21 tiles around it instead of 13 (user
|
||
* playtest #6 follow-up: the user remembers a long-reaching, hard-hitting ranged attack; the data scale stays 100 %).
|
||
*/
|
||
function kitTank(ab, e) {
|
||
const melee = T(ab, 'Empty.attack@chuang_atk_scale') ?? 1;
|
||
const sk = ab.sk.PollutedRangedAtk || null;
|
||
const s = sk ? sk.bb : {};
|
||
const spd = (e.def.raw && e.def.raw.sp) || null;
|
||
const max = Math.max(1, num(spd && spd.maxSp, 0) || (sk && sk.sp) || (T(ab, 'Empty.sp') ?? 2));
|
||
const init = Math.min(max, Math.max(0, num(spd && spd.initSp, 0)));
|
||
return [{
|
||
spawn(b, e2, a) { a.sp = init; a.skill = false; e2.profile.canTarget = (u) => !!u.ground && !u.isFlying; },
|
||
// SP full: this attack is 秽蚀轰击 (a blocked hit lands at once, inside this attack, while `a.skill` is set)
|
||
before(c, b, e2, a) { a.skill = a.sp >= max; },
|
||
hitOut(c, b, e2, a) { if (c.dmg.isAttack && !a.skill && c.target === e2.blockedBy) c.dmg.amount *= melee; },
|
||
attack(c, b, e2, a) {
|
||
if (!a.skill) { a.sp = Math.min(max, a.sp + 1); return; }
|
||
a.skill = false;
|
||
a.sp = 0;
|
||
const t = c.targets[0];
|
||
if (t) pollution(b, e2, t.x, t.y, TANK_ZONE_RADIUS, s.projectile_life_time ?? 0, s.polluted_damage_low ?? 0, s.polluted_damage_high ?? 0);
|
||
},
|
||
}];
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------
|
||
// this family's part of KITS (content/enemies.js spreads the parts in this order)
|
||
|
||
export const DOT_KITS = Object.freeze({
|
||
// --- DOT 持续
|
||
enemy_1234_dsubrl: kitDsubrl, // 深溟巢涌者 · no attack: an arts pulse on every ally in range each second + neural; 抵抗, immune 停顿
|
||
enemy_1234_dsubrl_2: kitDsubrl, // 富营养的巢涌者 · same
|
||
enemy_1267_nhpbr: kitPolluted, // 萨卡兹枯朽战士 · death: 污染秽蚀 zone (50 / 25 true per second)
|
||
enemy_1267_nhpbr_2: kitPolluted, // 萨卡兹枯朽战士组长 · same
|
||
enemy_1270_nhstlk: (ab) => [bleed(ab)], // 逐腐兽 · bleeding (arts/s, cleared by healing)
|
||
enemy_1270_nhstlk_2: (ab) => [bleed(ab)], // 疯狂的逐腐兽 · same
|
||
enemy_1272_nhtank: kitTank, // 萨卡兹枯朽战车 · ground targets only, melee ×2, 秽蚀轰击 on attacks 1, 4, 7 …
|
||
enemy_1272_nhtank_2: kitTank, // 尖端萨卡兹枯朽战车 · same
|
||
enemy_9006_actoxi: (ab) => [{ // 假想敌:蚀裂 · death: poison cloud on its killer
|
||
sil: true,
|
||
death(c, b, e) {
|
||
if (c.reason !== 'killed') return;
|
||
const k = c.killer && c.killer.side === 'ally' && c.killer.alive ? c.killer : null;
|
||
const atk = e.s.atk;
|
||
// a sourceless zone (`src` null): its ticks skip a 隐匿 / untargetable / sleeping operator (PRTS 假想敌:蚀裂: no 无视
|
||
// 可选性 note — PRTS 作战机制's "无来源的毒雾" that reaches 隐匿 units is the stage hazard), not an airborne 起飞 one
|
||
const cloud = (x, y) => dmgZone(b, null, x, y, T(ab, '1.projectile_range') ?? 0.8, T(ab, '1.projectile_life_time') ?? 0, T(ab, '1.interval') ?? 1, atk * (T(ab, '1.damage_atk_scale') ?? 0), 'arts', 'poison');
|
||
if (k) b.addProjectile({ from: e, target: k, speed: 8, visual: 'lob', source: null, hitDead: true, onHit: (h) => cloud(h.x ?? k.x, h.y ?? k.y) });
|
||
else cloud(e.x, e.y);
|
||
},
|
||
}],
|
||
// 集团军重型火炮 · PRTS 天赋: "不会攻击飞行单位"; the shell hits every unit within 1.0 of its target for 100 % ATK ("此弹道会强制
|
||
// 击中主目标,碰撞无视迷彩,不可对空"; no splash until 0.1.3) and each hit on the main target leaves a 3 s 【燃烧区域】 of radius
|
||
// projectile_range × UACANN_ZONE_SCALE
|
||
enemy_10122_uacann_2: (ab) => [noAirTargets(), splashAttack({ unblocked: false, radius: SHELL_SPLASH_RADIUS, noAir: true, fxKind: 'shell' }), {
|
||
dealt(c, b, e) {
|
||
// 【燃烧区域】 "不可对空": a flying ally (the 炎佑 dragon) standing in it takes nothing (until 0.1.3 it burned)
|
||
dmgZone(b, e, c.target.x, c.target.y, (T(ab, 'ProjectileBoomRange.attack@projectile_range') ?? 1) * UACANN_ZONE_SCALE, T(ab, 'ProjectileBoomRange.attack@projectile_life_time') ?? 0, 1, T(ab, 'ProjectileBoomRange.attack@value') ?? 0, 'arts', 'burning', null, 0, { noAir: true });
|
||
},
|
||
}],
|
||
enemy_10054_cjhot: (ab, e) => [immuneTo('sluggish'), { // 鼎沸 · pulses arts + burn around itself; immune 停顿
|
||
iv: e.base.bat || 1,
|
||
tick(b, e2) {
|
||
const r = (e2.def.raw.stats && e2.def.raw.stats.rawRangeRadius) || 1.6;
|
||
b.fx('explode', { x: e2.x, y: e2.y, r, kind: 'boil' });
|
||
// PRTS 鼎沸 "攻击范围内的所有我方单位每秒受到…法术持续伤害与…灼燃损伤(不可对空)": never a flying ally (the 炎佑 dragon;
|
||
// until 0.2.1 it burned)
|
||
for (const u of areaAllies(b, e2, e2.x, e2.y, r)) {
|
||
if (u.isFlying) continue;
|
||
hurt(b, e2, u, e2.s.atk * (T(ab, 'aoe.atk_scale') ?? 1), 'arts');
|
||
elem(b, e2, u, 'burn', e2.s.atk * (T(ab, 'aoe.ep_damage_ratio') ?? 0));
|
||
}
|
||
},
|
||
}],
|
||
});
|