Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_5_19-mlynar.js
T
sganggs 5877800a67 feedback5 CB3: the "受到攻击时" counters answer every damage instance from an enemy, not its attacks only — 菲莱 S2 now blasts on 深溟巢涌者's pulse, so do 星熊 S2, 泡泡, 刺玫 S2, 余 S1, 玛恩纳 无动于衷, 年 S2 and 斩业星熊 (community report of 2026-10-06, item 30)
The report's 退行的潮涌者 is the 巢涌者 (深溟 / 富营养的巢涌者 in this mode): no normal attack, its talent hurts every operator in
range once a second. Official (ArknightsGameData buff_template_data): its pulse is an AdvancedApplyDamage with the enemy as
source (enemy_dsubrl_aoe[aura]); the counters fire on ON_TAKE_DAMAGE — philae_s_2 with no filter, inverse_damage (星熊 S2),
inverse_damage[magic] + nian_s_2 (年 S2), bubble_s_2 / bubble_t_1 (泡泡), vendla_s_2 (刺玫), yu_s_1[inverse_damage] (余 S1),
hsgma2_s_1 / hsgma2_e_003_tr_take (斩业星熊), mlynar_t_2[inverse] (玛恩纳) on a source of the other side; 棘刺 S2 (普通伤害
filter) and 信仰搅拌机 S3 already took any damage. Cause: the kits required dmg.isAttack. Now kits/shared/tier1.js
byEnemyAttack = any enemy damage instance but a 流失, an element 损伤, 无来源 damage or another counter / reflection (tags
'counter' / 'reflect'); 菲莱 S2 any instance but a 流失 or a counter. test/content/op_nian.test.js pinned "a non-attack hit
returns nothing" for 年 S2 — updated on purpose (answered now; a 流失 still is not).

Golden: 19 scenarios where an enemy's non-attack damage (skills, pulses, areas) reached one of these operators — their counter
now answers it (more damage, fx thorns / aoe): roster-002, -005, -008, -010, -012, -016, -033, -044; bond-lateranoShip-low;
fields boss-boss_2-solo, boss-boss_4-pair, boss-boss_4-solo, hidden-boss_8-pair, hidden-boss_8-solo; matches solo-FUNNY-1,
solo-FUNNY-2, coop4-ABYSS-7, coop2-ABYSS-10-boosted; diy-051. Tests: test/sim/feedback5-counters-any-damage.test.js (6; 5
fail on the parent, the first checks the 巢涌者 never attacks). Docs: docs/SIM.md (re-entrancy rule).
2026-10-07 02:30:07 +08:00

133 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/chess_char_5_19-mlynar.js — 玛恩纳 (char_4064_mlynar) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import {
num, on, inRange, talent, skillGrid, batPct, mods, inFaction, isOp, selectedId, lazySkills, skillRange, leaderOf,
whileOn, permBuff,
} from '../shared/tier5.js';
import { byEnemyAttack } from '../shared/tier1.js';
/** 玛恩纳 游侠 "周围存在3名及以上敌人" radius (8-neighbourhood). */
const AROUND_RADIUS = 1.5;
const isKazimierz = (u) => inFaction(u, 'kazimierzShip', ['kazimierz']);
export default {
// ---------------------------------------------------------------------------------------------------------------
// 玛恩纳 — librator (ramp to +200 % ATK while idle). S3 未照耀的荣光 (26 s, CUSTOM_RANGE): skill range, trait ×2, 5
// targets at 125/150 % ATK phys, air units too (PRTS 备注 "※可对空"; player report B4 after 0.1.0: on a flying wave the
// cast hit nothing for 26 s); enemies in range take +10/11 % of his ATK true damage from every Kazimierz attack. Per
// PRTS 备注 the trait bonus drops by 10 % (per_kill_reduce, absolute: +400 % → +390 %) for each enemy knocked out by
// his own attack (or the damage it carries — not 无动于衷's reflection, not a mark another operator's attack set off),
// settled after that attack, never below +0 %. T1 游侠: ×1.1 ATK on attacks (×1.15 and −15 % damage taken with ≥3
// enemies around). T2 无动于衷: taunt +1, Kazimierz ops reflect 15 % of his ATK as true damage when an enemy damages them.
// S1 未声张的怒火 (duration, SEARCH): attacks attack@atk_scale × ATK, DEF +. S2 未宽解的悲哀 (duration): skill range,
// BAT +0.3 s, attacks attack@atk_scale × ATK twice; a kill of his own attacks during the skill keeps the trait ramp
// when it ends. S1 / S2 have no 对空 note: ground only, like his trait. S2's data rule is ACTIVE_RANGE on its 2-3 (the
// owner's rule of 2026-10-05 over the 解放者 SEARCH row: an enemy he can target inside the 2-3 casts it).
chess_char_5_19_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1);
const sid = selectedId(chess, def);
const up = num(bb.trait_up, 1), perKill = num(bb.per_kill_reduce);
// S3: the trait bonus = ramp × trait_up + per_kill_reduce × kills, ≥ 0 — the trait's own ramp buff stays, the
// difference goes into `mlynar:traitUp` (negative once the kills take the bonus below the ramp)
const applyUp = (battle, unit) => {
const ramp = num(unit.trait.ramp);
const extra = unit.mem.mlyUp ? Math.max(0, ramp * up + perKill * num(unit.mem.mlyKills)) - ramp : 0;
if (extra) battle.addBuff(unit, { key: 'mlynar:traitUp', mods: { atkPct: extra } });
else battle.removeBuff(unit, 'mlynar:traitUp');
};
/**
* "仅自身普通攻击(与该次攻击附带的伤害)击倒非角色类单位" (PRTS S2 / S3 备注): `onKill(victim)` for each enemy he
* knocks out, while his skill runs, that his current attack hit (or the mark it set off, tagged 'mlynarOwn') — so a
* kill by the damage that attack carries (天马之枪, the 卡西米尔 bond's true damage, the mark) counts too, in any
* hook order; `onAttack()` after each of his attacks ("加成降低于当次攻击后统一结算"), which also closes the set.
* 无动于衷's reflection and a mark another operator's attack set off happen outside his attack: never counted.
*/
const ownKills = (battle, unit, onKill, onAttack = null) => {
const hit = new Set();
battle.on('damaged', (c) => {
if (c.source === unit && (c.dmg?.isAttack || (c.dmg?.tags || []).includes('mlynarOwn'))) hit.add(c.target);
}, { owner: unit, priority: 1000 });
battle.on('kill', (c) => { if (c.killer === unit && hit.has(c.victim) && c.victim.side === 'enemy' && unit.skill?.active) onKill(c.victim); }, { owner: unit });
battle.on('attack', (c) => { if (c.attacker !== unit) return; hit.clear(); if (onAttack) onAttack(); }, { owner: unit });
};
return {
skills: lazySkills({
skchr_mlynar_1: () => ({ kind: 'duration', mods: mods({ defPct: num(bb.def) }), attack: { atkScale: num(bb['attack@atk_scale'], 1) } }),
skchr_mlynar_2: () => ({
kind: 'duration', mods: mods({ batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def),
attack: { atkScale: num(bb['attack@atk_scale'], 1), hits: 2 },
}),
}),
skill: {
kind: 'duration',
targeting: { ...(skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : {}), canHitFly: true },
attack: { atkScale: num(bb['attack@atk_scale'], 1), maxTargets: Math.max(1, num(bb['attack@max_target'], 1)) },
onStart({ battle, unit }) {
unit.mem.mlyUp = true; unit.mem.mlyKills = 0; unit.mem.mlyPending = 0;
applyUp(battle, unit);
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id, r: 2, skill: 'mlynar' });
},
onEnd({ battle, unit }) { unit.mem.mlyUp = false; unit.mem.mlyPending = 0; battle.removeBuff(unit, 'mlynar:traitUp'); },
},
trait: { dmgMul: (b, u) => (num(u.mem.mlyNear) >= num(t0.cnt, 3) ? num(t0.atk_scale_up, 1) : num(t0.atk_scale_base, 1)) },
talents: [
{ install(battle, unit) { // 游侠
const dr = num(t0.damage_resistance);
whileOn(battle, unit, 0.2, () => {
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 } });
});
} },
{ install(battle, unit) { // 无动于衷
permBuff(battle, unit, 'mlynar:unmoved', { taunt: num(t1.taunt_level) });
const scale = num(t1.atk_scale);
if (!(scale > 0)) return;
battle.on('damaged', (c) => {
const a = c.target, src = c.source;
// the official mlynar_t_2[inverse] (ON_TAKE_DAMAGE, InverseDamage from an enemy source): every enemy damage instance
// a Kazimierz operator takes, not its attacks only (tier1 byEnemyAttack)
if (a.side !== 'ally' || !isOp(a) || !isKazimierz(a) || !byEnemyAttack(c) || !src.alive) return;
if (!on(unit) || leaderOf(battle, unit) !== unit) return;
battle.dealDamage(unit, src, { amount: unit.s.atk * scale, type: 'true', canDodge: false, tags: ['talent', 'reflect'] });
}, { owner: unit });
} },
],
install(battle, unit) {
if (sid === 'skchr_mlynar_2') {
// 技能期间若击倒敌人,技能结束时特性效果不重置: the librator trait resets the ramp on skillEnd (priority 0) —
// remember it before and put it back after
battle.on('skillStart', (c) => { if (c.unit === unit) unit.mem.mlyKeep = false; }, { owner: unit });
ownKills(battle, unit, () => { unit.mem.mlyKeep = true; });
battle.on('skillEnd', (c) => {
if (c.unit === unit) unit.mem.mlyRamp = unit.mem.mlyKeep && c.reason !== 'death' ? num(unit.trait.ramp) : null;
}, { owner: unit, priority: 100 });
battle.on('skillEnd', (c) => {
if (c.unit !== unit || unit.mem.mlyRamp == null) return;
unit.trait.ramp = unit.mem.mlyRamp;
unit.mem.mlyRamp = null;
if (unit.trait.ramp > 0 && unit.alive) battle.addBuff(unit, { key: 'trait:libratorRamp', mods: { atkPct: unit.trait.ramp } });
}, { owner: unit, priority: -100 });
}
if (sid && sid !== 'skchr_mlynar_3') return;
ownKills(battle, unit, () => { unit.mem.mlyPending = num(unit.mem.mlyPending) + 1; }, () => {
if (!unit.mem.mlyPending || !unit.mem.mlyUp) return;
unit.mem.mlyKills = num(unit.mem.mlyKills) + unit.mem.mlyPending;
unit.mem.mlyPending = 0;
applyUp(battle, unit);
});
const extra = num(bb.atk_scale);
if (!(extra > 0)) return;
battle.on('damaged', (c) => {
const src = c.source, e = c.target;
if (!src || src.side !== 'ally' || !isOp(src) || !isKazimierz(src) || !c.dmg?.isAttack || c.type === 'element') return;
if (e.side !== 'enemy' || !e.alive || !unit.skill?.active || !on(unit) || !inRange(unit, e)) return;
const tags = src === unit ? ['skill', 'mlynarMark', 'mlynarOwn'] : ['skill', 'mlynarMark'];
battle.dealDamage(unit, e, { amount: unit.s.atk * extra, type: 'true', canDodge: false, isSkill: true, tags });
}, { owner: unit });
},
};
},
};