Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-monstr.js
T

326 lines
22 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/op-monstr.js — Mon3tr (char_4179_monstr) 自选 operator kit: 6★ 链愈师 (医疗), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and her module (XAH-X 记忆存档) at every form, and the
// kit of her summon 重构体 (token_10050_monstr_prosts). Kit contract and the 自选 rules: ../README.md ("How to add an operator
// (自选)").
//
// Forms (data/backups.json units.char_4179_monstr): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7,
// XAH-X at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table / token_table (zh_CN, as built into backups.json); PRTS Mon3tr
// (T1 备注 "治疗效果无视禁疗,但对象选取受禁疗制约,可以强行选择自身的重构体作为自身普通攻击的治疗目标", "攻击力加成效果同名效果取最高";
// T2 备注 "该效果重复触发刷新持续时间,不会因自身退场而消失"; S1 备注 "技能范围与技能期间治疗范围无关"; S2 备注 "可选择满生命值的重构体
// 进行治疗;重构体受到Mon3tr普通攻击治疗的0.6s后,Mon3tr再次以重构体为目标进行一次跳跃治疗", "仅技能期间治疗触发的第二天赋效果提升;
// …叠加时取最高"; S3 备注: the start (撤退重构体, 【移动】, the 路标形态 marker), 不死 ("期间受到致命伤害结算后立即结束技能;技能结束后
// 返回…以不高于1点的生命值在场;因返回而部署时获得1s不死"), "期间对自身进行治疗时,治疗跳跃不会衰减且不占用跳跃数", the return); PRTS
// 重构体 (备注 "持有禁疗(可以被Mon3tr无视)、不可阻挡", "嘲讽等级-1", "Mon3tr因移动之外的原因退场时强制撤退场上的重构体", 通常形态
// "阻挡数强制归0,每0.1s流失8生命值…退场时返还1个可部署的重构体"); PRTS 卫戍协议/帮助 (§战斗部署 placed summons, §作战阶段
// "若召唤物在战斗期间退场,将在满足条件后立即原地再部署1个"); the client's battle data: charpack char_4179_monstr (attacks
// `_ignoreHealFree`; talent "1" monstr_t_1[born_charge] / [die_to_kil_token]; talent "2" monstr_t_2[atk_spd], scaled in the S2
// mode by `_applyTalentScale` on attack_speed; modes S3_Reborn (Combat / Attack: PURE, targetMotion 1, `_maxNum` 3
// `_limitedMaxTargetNumToBlockedCnt`; ExtraHeal projectile_chr_monstr_s3 on every attack; talents monstr[free_jump],
// monstr_s_3[bleeding] (damage_per_second × 0.1 every 0.1 s), [reborned_passive], [sync_hp]); `_config` mapping S3's atk /
// damage_per_second / max_hp / block_cnt / base_attack_time and T1's attack@chain.extra_cnt); the skill prefabs (skchr_monstr_1
// next heal; skchr_monstr_2 its attack's activeBuff monstr_s_2[proj] lifeTime 0.6 → projectile_chr_monstr_s2_extra from the
// reconstruct; skchr_monstr_3 `_onlyAvailableWhenTokenValid`, the Respawn / RespawnEnd abilities, monstr_s_3[end_undeadable_
// restore] 1 s); the projectile prefabs (every heal projectile: `_atkScale` 0.75 = chain.atk_scale, the jump Selector
// `_rangeId` x-4 `_maxTarget` 3 lowest HP ratio `_useAdditionalTargetCount`, `_freeJumpValidator` monstr[free_jump]); the
// reconstruct's prefab (monstr[block_free] abnormal [3, 7] + taunt_level −1; prosts[atk_up] aura from the host's `atk`;
// prosts_t_1[born_charge] BLOCK_CNT ×0, interval 0.1, firstTriggerInterval 1, FixedValueDamage PURE `damage` on itself);
// buff_template_data (monstr_t_2[atk_spd] ON_OUTPUT_MODIFIER IsHeal; monstr_s_3[undead] ON_BEFORE_TRY_SET_HP_ZERO).
// - Trait (链愈师) "恢复友方单位生命,且会在3个友方单位间跳跃,每次跳跃治疗量降低25%" — the chain is this kit's (the profession's
// chain is replaced: heal mode single + `chainFrom`, its links the profession's — ai.js chainHealNext): after the heal on
// the main target the heal jumps to the lowest-HP-ratio ally (a full-HP one too — PRTS 分支特性信息 链愈师; ties: the latest
// deployed) of the 3×3 around the last one healed (x-4), each jump ×
// attack@chain.atk_scale (0.75; XAH-X 0.85), up to attack@chain.max_target units healed (3). A 重构体 (and she herself while S3
// runs: monstr[free_jump]) is a free link: healing it uses no target of the count and the next jump does not decay (XAH-X
// stage 3: the next attack@chain.extra_cnt jumps after that neither). Her heals pass 禁疗 ("治疗效果无视禁疗"); her selection
// takes her own 重构体 through its 禁疗 (`healThrough`), no other 禁疗 unit [ASSUMED: the jumps too].
// - Module XAH-X 记忆存档: the trait's 0.85 per jump (traitOverride); stage 3 changes T1 (below); ATK / DEF from the attributes.
// - T1 自我修复 (cnt 1): her 重构体 is a hand piece the player places (the talent's summon) on a ground tile of her attack
// range — "可以在攻击范围内的地面使用": the data's `ownerRange` from that talent text (tools/build-data.mjs summonRecord,
// 0.2.0 WE2) and its MELEE position, enforced by the match. Its kit: 禁疗 (only her heals reach it), blocks nothing, taunt
// −1, loses 8 HP every 0.1 s from 1 s after it lands (流失, Battle.loseHp — PRTS "每0.1s流失8生命值"; trait damage_per_second
// 80); every ally of its
// 3×3 but itself (no 孤立 one) ATK +atk (0.15; XAH-X stage 3 0.25; 同名效果取最高). Gone (destroyed or withdrawn) ⇒ back on its
// tile its redeploy time (15 s) after, paying its cost (3 DP), only while she stands — her deployment (not a 【移动】)
// readies a waiting one at once (monstr_t_1[born_charge]) [ASSUMED: the 卫戍 auto redeploy of a placed summon, as 凯尔希's
// Mon3tr]; withdrawn when she leaves the field (not on her S3 moves).
// - T2 战术协同: every heal of hers (chain jumps included) gives its target and herself ASPD +attack_speed for buff_duration s —
// ×talent_scale (S2) for a heal made while S2 runs; one effect per unit, the strongest (同名效果取最高 — a weaker one that
// outlasts it resumes afterwards: Battle.applyStrongest); a new one refreshes it.
// - S1 策略:超压链接 (AUTO, attack SP, data DEFAULT — a "next heal" waits for her heal, as 闪灵's S2): that heal is heal_scale ×
// ATK and heals up to chain.max_target (4) units.
// - S2 策略:超负荷 (MANUAL, data SKILL_RANGE on its y-2 — an enemy there; 30 s): her own 重构体 inside her range is her heal
// target, even at full HP (with nobody injured she heals it all the same: Battle.forceAttack); 0.6 s after each heal of her
// attack on it a second chain heal starts on it (her ATK × 1); T2 ×talent_scale.
// - S3 策略:熔毁 (MANUAL, data DEFAULT, 25 s; only while her 重构体 stands — the cast is refused otherwise): the 重构体 is withdrawn
// (its piece waits: back once its tile is free again) and she 【移动】s onto its tile; meanwhile range x-4, ATK +atk, base attack
// time base_attack_time (a flat −1.5 s on 2.85 s), block +block_cnt, max HP +max_hp (the HP ratio kept), 流失 of
// damage_per_second HP/s (8 every 0.1 s); "同时攻击阻挡的所有敌人" (her selector's _limitedMaxTargetNumToBlockedCnt): each attack
// takes up to her block count of ground enemies of her range, the blocked ones first (ai.js `hitAllBlocked`, the 强攻手 rule
// of PRTS 分支特性信息) for ATK true damage, and each attack starts a chain heal on herself at attack@heal_scale × ATK
// (she is a free link). 不死: a hit or 流失 that would knock her out leaves her at 1 HP, untargetable and blocking nothing, and
// the skill ends on the next tick. At the end she 【返回】s to the tile she left (her SP emptied; the max HP drop keeps the ratio,
// so after a fatal blow she stands at ≤ 1 HP) with 1 s of 不死. The 路标形态 marker on her tile is not modelled: should another
// unit stand there, she stays where she is [ASSUMED; officially she is knocked out].
import { num, talentBb, traitBb, skillRec, batMod, installAura, up } from '../shared/tier1.js';
import { COLS } from '../../../constants.js';
import { chainHealNext } from '../../../ai.js';
const S1 = 'skchr_monstr_1';
const S2 = 'skchr_monstr_2';
const S3 = 'skchr_monstr_3';
/** Her summon 重构体 (T1). */
export const PROSTS = 'token_10050_monstr_prosts';
/** S2: the second chain starts this long after a heal of hers on her 重构体 (monstr_s_2[proj] lifeTime). */
const S2_EXTRA_DELAY = 0.6;
/** The 重构体's 流失: every 0.1 s, from 1 s after it lands (prosts_t_1[born_charge] interval / firstTriggerInterval). */
const PROSTS_BLEED_IV = 0.1;
const PROSTS_BLEED_FIRST = 1;
/** The 重构体's ATK aura refresh [ASSUMED: the aura's interval is not in the data]. */
const PROSTS_AURA_IV = 0.1;
/** S3's 流失 period (monstr_s_3[bleeding] interval). */
const S3_BLEED_IV = 0.1;
/** S3: 不死 after the return (monstr_s_3[end_undeadable_restore] lifeTime). */
const RETURN_UNDEAD = 1;
/** How often a waiting 重构体 tries to come back once ready. */
const RETRY = 0.1;
const T2_KEY = 'talent:monstr:aspd';
const PROSTS_ATK_KEY = 'talent:monstr:prostsAtk';
const PROSTS_ABN_KEY = 'monstr:prosts:abnormal';
const S3_UNDEAD_KEY = 'skill:monstr:undead';
const TAG_PROSTS_BLEED = 'monstr:prostsBleed';
const TAG_S3_BLEED = 'monstr:meltdownBleed';
/** Her heals "无视禁疗" (every heal ability of hers: `_ignoreHealFree`). */
const HEAL_OPTS = Object.freeze({ ignoreHealFree: true });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const skillOn = (unit, id) => !!(unit.skill && unit.skill.active && unit.skill.id === id);
const isProstsOf = (t, unit) => !!t && t.kind === 'token' && t.defId === PROSTS && t.ownerUnit === unit;
const prostsOf = (battle, unit) => battle.allyUnits.filter((t) => isProstsOf(t, unit));
/** Her 重构体 standing inside her current attack range. */
const prostsInRange = (battle, unit) => prostsOf(battle, unit).find((t) => up(t) && !!unit.rangeKeySet && unit.rangeKeySet.has(t.tileR * COLS + t.tileC)) ?? null;
/** `a` stands on the 3×3 around `b` (x-4: a heal jump's reach, the 重构体's aura). */
const nearX4 = (a, b) => Math.abs(a.tileR - b.tileR) <= 1 && Math.abs(a.tileC - b.tileC) <= 1;
/**
* A chain heal of hers from `main` (already healed by her attack when `mainDone`): up to `count` units healed, × `step` per
* jump; a free link (her 重构体; herself with `selfFree`) uses no target of the count and the next 1 + `extra` jumps keep the
* amount. Each link is the 链愈师 one (ai.js chainHealNext — the 3×3 around the last one healed, the lowest HP ratio a
* full-HP ally included, then the latest deployed; no 禁疗 unit but her 重构体, her `healThrough`).
*/
function chainFrom(battle, unit, main, base, { count, step, extra, mainDone = false, selfFree = false }) {
const free = (t) => isProstsOf(t, unit) || (selfFree && t === unit);
let amount = base;
if (!mainDone) battle.heal(unit, main, amount, HEAL_OPTS);
let counted = free(main) ? 0 : 1;
let calm = free(main) ? 1 + extra : 0;
const seen = new Set([main.id]);
let cur = main;
while (counted < count) {
const next = chainHealNext(battle, unit, cur, seen);
if (!next) break;
seen.add(next.id);
if (calm > 0) calm--; else amount *= step;
battle._ev(['atk', cur.id, next.id, 'chainHeal']);
battle.heal(unit, next, amount, HEAL_OPTS);
if (free(next)) calm = 1 + extra; else counted++;
cur = next;
}
}
/** The 重构体's kit (owner = Mon3tr): 禁疗, no block, taunt −1, its 流失, the ATK aura, its return. */
function prostsKit(owner, { atk }) {
return {
skill: null,
talents: [],
trait: { noAttack: true },
install(battle, t) {
const dps = num(t.def?.traitBb?.damage_per_second, 80);
battle.addBuff(t, { key: PROSTS_ABN_KEY, flags: { noHeal: true, noBlock: true }, mods: { taunt: -1 }, persist: true, allowDead: true });
if (atk > 0) installAura(battle, t, { key: PROSTS_ATK_KEY, select: (a) => a !== t && nearX4(a, t), mods: { atkPct: atk }, value: atk, interval: PROSTS_AURA_IV });
battle.on('deploy', (c) => {
if (c.unit !== t) return;
const seq = t.deploySeq;
battle.after(PROSTS_BLEED_FIRST, () => {
if (t.deploySeq !== seq || !up(t)) return;
battle.every(PROSTS_BLEED_IV, (b, sched) => {
if (t.deploySeq !== seq || !up(t)) { sched.cancel(); return; }
b.loseHp(t, dps * PROSTS_BLEED_IV, { source: t, tags: [TAG_PROSTS_BLEED] });
}, { owner: t, immediate: true });
}, { owner: t });
}, { owner: t });
// gone (destroyed or withdrawn): the piece stays and comes back on its tile once ready, while she stands
battle.on('death', (c) => {
if (c.unit !== t || battle.finished) return;
t.removed = false;
t.mem.readyAt = battle.time + Math.max(0, num(t.base.respawnTime));
if (t.mem.retry) return;
t.mem.retry = battle.every(RETRY, (b, sched) => {
const stop = () => { sched.cancel(); t.mem.retry = null; };
if (t.alive || t.removed || b.finished) { stop(); return; }
if (!up(owner) || b.time + 1e-9 < t.mem.readyAt) return;
if (b.redeploy(t, { free: false })) stop();
}, { owner: t });
}, { owner: t, priority: -10 });
},
};
}
/** S2: with nobody injured in her range she heals her 重构体 there all the same ("可选择满生命值的重构体进行治疗"). */
function s2CanAttack(battle, unit) {
const r = prostsInRange(battle, unit);
if (!r || battle.injuredAlliesInKeys(unit.rangeKeys, unit).length) return true;
if (battle.forceAttack(unit, [r])) unit.atkCd = Math.max(unit.atkCd, unit.s.interval);
return false;
}
export default {
char_4179_monstr: (bb, chess) => {
const tb = traitBb(chess); // attack@chain.max_target (3), attack@chain.atk_scale (0.75; XAH-X 0.85)
const t0 = talentBb(chess, 0); // 自我修复: cnt, atk (XAH-X stage 3: 0.25 + attack@chain.extra_cnt 1)
const t1 = talentBb(chess, 1); // 战术协同: buff_duration, attack_speed
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s1 = skillRec(chess, S1), s3 = skillRec(chess, S3);
const chain = {
count: Math.max(1, Math.floor(num(tb['attack@chain.max_target'], 3))),
step: num(tb['attack@chain.atk_scale'], 0.75),
extra: Math.max(0, Math.floor(num(t0['attack@chain.extra_cnt']))),
};
const s1Count = Math.max(1, Math.floor(num(b1['chain.max_target'], chain.count)));
const picked = chess?.skill?.skillId ?? null;
return {
// the chain is chainFrom's (heal mode single: no profession chain); her 重构体 is selectable through its 禁疗
trait: { heal: { mode: 'single' }, healThrough: (healer, a) => isProstsOf(a, healer) },
skills: {
[S1]: { kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant', attack: { healScale: num(b1.heal_scale, 1) } },
[S2]: { kind: 'duration', attack: { canAttack: (battle, unit) => s2CanAttack(battle, unit) } },
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess), blockCnt: num(b3.block_cnt), hpFlat: num(b3.max_hp) },
targeting: s3?.rangeGrid ? { rangeGrid: s3.rangeGrid } : undefined,
attack: { dmgType: 'true', attack: 'melee', projectile: 'none', canHitFly: false, groundOnly: true, hitAllBlocked: true },
onStart({ battle, unit, skill }) {
const r = prostsOf(battle, unit).find(up);
if (!r) { skill.end('noToken'); skill.addCharge(1); return; } // (refused before: the activate guard)
const home = { r: unit.tileR, c: unit.tileC, dir: unit.dir };
const to = [r.tileR, r.tileC];
battle.retreat(r, { reason: 'retreat', permanent: true }); // 撤退场上的重构体(返回待部署区)
const fromX = unit.x, fromY = unit.y;
if (!battle.moveRedeploy(unit, to[0], to[1])) { skill.end('blocked'); skill.addCharge(1); return; } // 强制结束技能并返还技力
unit.mem.mHome = home;
battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
},
onTick({ unit, skill }) { if (unit.mem.mFatal) skill.end('fatal'); }, // 受到致命伤害结算后立即结束技能
onEnd({ battle, unit }) {
const h = unit.mem.mHome;
unit.mem.mHome = null;
unit.mem.mFatal = false;
battle.removeBuff(unit, S3_UNDEAD_KEY);
if (!h || !unit.alive || !unit.deployed) return;
// 【返回】 to the tile she left: SP emptied (S3_Restore ModifySp −100 %), 1 s 不死
const fromX = unit.x, fromY = unit.y, dir0 = unit.dir;
unit.dir = h.dir;
if (battle.moveRedeploy(unit, h.r, h.c, { clearSp: true })) battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
else unit.dir = dir0;
unit.mem.mUndeadUntil = battle.time + RETURN_UNDEAD;
},
},
},
talents: [
{ install(battle, unit) { // 自我修复: her pieces run the 重构体's kit; withdrawn when she leaves; readied by her deployment
for (const t of battle.allyUnits) {
if (!isProstsOf(t, unit) || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t);
battle._setupUnit(t, prostsKit(unit, { atk: num(t0.atk) }));
}
battle.on('death', (c) => {
if (c.unit !== unit) return;
for (const t of prostsOf(battle, unit)) if (t.alive) battle.retreat(t, { reason: 'retreat', permanent: true });
}, { owner: unit });
battle.on('deploy', (c) => {
if (c.unit !== unit || c.move) return;
for (const t of prostsOf(battle, unit)) if (!t.alive && !t.removed) t.mem.readyAt = battle.time;
}, { owner: unit });
} },
{ install(battle, unit) { // 战术协同: every heal of hers ⇒ target and herself ASPD + (×talent_scale under S2)
const d = num(t1.buff_duration), v0 = num(t1.attack_speed);
if (!(d > 0) || !(v0 > 0)) return;
battle.on('heal', (c) => {
if (c.source !== unit || c.opts?.regen || !c.target || c.target.side !== 'ally' || !(c.amount > 0)) return;
const v = v0 * (skillOn(unit, S2) ? num(b2.talent_scale, 1) : 1);
const give = (x) => battle.applyStrongest(x, T2_KEY, { duration: d, value: v, mods: (a) => ({ aspd: a }), source: unit });
give(c.target);
if (c.target !== unit && unit.alive) give(unit);
}, { owner: unit });
} },
],
install(battle, unit) {
// the chain heals: after each heal attack of hers a chain from its target(s); under S3 every attack starts one on her
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.alive) return;
const prof = battle.effectiveProfile(unit);
if (prof.heal && prof.dmgType === 'heal') {
const sk = unit.skill;
const s1on = !!(sk && sk.id === S1 && sk.active && sk.pending);
const s2on = skillOn(unit, S2);
const base = unit.s.atk * (prof.atkScale ?? 1) * (prof.healScale ?? 1) * unit.s.atkScaleMul;
for (const t of c.targets || []) {
if (!t || t.side !== 'ally') continue;
chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, mainDone: true });
// S2: 0.6 s after her attack healed her 重构体, a second chain on it (her ATK × 1)
if (s2on && isProstsOf(t, unit)) {
battle.after(S2_EXTRA_DELAY, () => {
if (!unit.alive || !up(t)) return;
chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, chain);
}, { owner: unit });
}
}
} else if (skillOn(unit, S3) && unit.mem.mHome && !unit.mem.mFatal) {
chainFrom(battle, unit, unit, unit.s.atk * num(b3['attack@heal_scale'], 0.5) * unit.s.atkScaleMul, { ...chain, selfFree: true });
}
}, { owner: unit });
// S2: her 重构体 inside her range is the heal target
if (picked === S2) {
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillOn(unit, S2) || !c.profile || c.profile.dmgType !== 'heal') return;
const r = prostsInRange(battle, unit);
if (r) c.targets = [r];
}, { owner: unit, priority: 10 });
}
if (picked !== S3) return;
// S3 `_onlyAvailableWhenTokenValid`: every activation path is refused while her 重构体 is not on the field
const sk = unit.skill;
if (sk && sk.id === S3 && !Object.prototype.hasOwnProperty.call(sk, 'activate')) {
const activate = sk.activate;
Object.defineProperty(sk, 'activate', {
configurable: true, writable: true, enumerable: false,
value(reason, opts) { return prostsOf(battle, unit).some(up) ? activate.call(this, reason, opts) : false; },
});
}
// S3's 流失
battle.every(S3_BLEED_IV, () => {
if (!skillOn(unit, S3) || !unit.mem.mHome || unit.mem.mFatal || !up(unit)) return;
battle.loseHp(unit, num(b3.damage_per_second) * S3_BLEED_IV, { source: unit, tags: [TAG_S3_BLEED] });
}, { owner: unit });
// S3 不死; the 1 s 不死 after the return
battle.on('fatal', (c) => {
if (c.unit !== unit) return;
if (skillOn(unit, S3) && unit.mem.mHome) {
c.prevented = true;
if (!unit.mem.mFatal) {
unit.mem.mFatal = true;
battle.addBuff(unit, { key: S3_UNDEAD_KEY, flags: { untargetable: true, noBlock: true }, tags: ['skill'] });
battle.releaseBlocked(unit);
battle.fx('undying', { x: unit.x, y: unit.y, id: unit.id });
}
return;
}
if (battle.time < num(unit.mem.mUndeadUntil, -Infinity) - 1e-9) c.prevented = true;
}, { owner: unit, priority: 10 });
},
};
},
};