Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_5_19-mlynar.js
T
sganggs 383fe4a49e feedback5 WF: ACTIVE_RANGE also replaces the SEARCH row and covers 深巡 S2 — a skill whose running range strictly contains the own one casts with an enemy inside it (the owner's decisions of 2026-10-05)
tools/build-data.mjs resolveTrigger: a MANUAL operator skill on DEFAULT (no row, or a DEFAULT entry of the deviation
tables) or on the SEARCH row ("在初始攻击范围内存在敌人时释放技能": 解放者 / 阵法术师 / 安洁莉娜 — ACTIVE_RANGE_OVER) whose
running range strictly contains its own is ACTIVE_RANGE on that range, rawRule the official row; validateAll accepts a
DEFAULT deviation resolved to ACTIVE_RANGE (chess and stand-in tables) and ACTIVE_RANGE over SEARCH. Data rebuilt
offline, JSON-compared: only skills[].trigger rule / customRangeGrid of 薄绿 S1 (x-2), 蜜蜡 S1 (x-2), 卡涅利安 S3 (x-2),
玛恩纳 S2 (2-3), 安洁莉娜 S3 (y-4) — rawRule SEARCH — and 深巡 S2 (3-2, rawRule TAKE_DAMAGE), normal + elite (32
leaves); backups.json unchanged. Kits: 薄绿 S1 drops its DEFAULT trigger and 深巡 S2 PR #12's DEFAULT line, both read the
data now (雷蛇 S2 keeps PR #12's line). The sim needs no change (SEARCH was the DEFAULT condition on the initial range
every tick; ACTIVE_RANGE is the same on the running range). Docs: DESIGN §5.6 and the §21.20 flip row, DATA, SIM,
PLAYING, kits README, shared tier comments, skills.js header.

Tests: data (78 records / 39 skills, the five from SEARCH, the SEARCH skills that stay), feedback5-active-range (安洁莉娜
S3 on its y-4, S2 keeps SEARCH), feedback1-tank-triggers and kits_t1t2 (深巡 S2 on its 3-2, before any hit), kits_alt_t3
(薄绿 S1 on its x-2), feedback1f-fence (the idle measure uses the interval of the cooldown still running: the S2 now casts
between two attacks), docs-consistency (one PR #12 kit line left).

Golden: roster-001 / 002 (深巡), roster-016 / 044 (玛恩纳, 安洁莉娜), roster-026 / 027 (薄绿), roster-041 / 042 (卡涅利安),
bond-egirShip-low / bond-suntShip-high (深巡), boss-boss_2-pair / boss-boss_6-pair (玛恩纳, 安洁莉娜), matches solo-NORMAL-1,
coop2-NORMAL-3, coop4-FUNNY-6, coop2-NORMAL-8-serverrun — each fields one of the six skills, which now casts when an
enemy enters its running range.
2026-10-05 18:45:13 +08:00

130 lines
8.4 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';
/** 玛恩纳 游侠 "周围存在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 attacked.
// 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;
if (a.side !== 'ally' || !isOp(a) || !isKazimierz(a) || !src || src.side !== 'enemy' || !src.alive || !c.dmg?.isAttack) 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 });
},
};
},
};