Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_5_19-mlynar.js
T
sganggs 1169102d46 feedback5 R4: one file per operator kit — kits/tier1..6.js split into kits/ops/<chessId>-<codename>.js (129 kits), kits/shared/tier1..6.js (the helpers two or more kits of a tier use, and each tier's header notes) and kits/index.js (the registry, same keys in the same order); the tier files become re-export shims
Pure move: every line of a kit, of a helper and of the tier headers is copied verbatim (only the headers' first
line names the new file); a helper used by one kit moved into that kit's file. Glue only: the generated imports,
`export default { … }` around each registry entry, `export { … }` lists in shared/tier2..6.js, and tier4's
`(bb, chess, def)` default-argument wrapper as shared/tier4.js `withDefaults` (applied in each tier-4 kit file).
kits/index.js loads every kit file with a guarded dynamic import (browser-safe, no directory listing); tierN.js
re-export the tier registry (TIER_KITS) and their old named exports (tier1 helpers + tinmanKit, tier6
PITHST_ELEMENTS). The merged registry, every tier default and named export have the same keys, order, frozenness
and function sources as before. test/docs-consistency.test.js reads the moved code at its new paths.
Golden: GOLDEN_FULL=1 node --test test/golden.test.js unchanged.
2026-10-05 16:13:56 +08:00

129 lines
8.2 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.
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 });
},
};
},
};