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

273 lines
20 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-slent2.js — 淬羽赫默 (char_1031_slent2) 自选 operator kit: 6★ 护佑者 (辅助), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, both modules (BLS-X 难得清醒, BLS-Y 可控动量) at every
// form, and the kit of her summon 夜灯 (token_10029_slent2_protrb). Kit contract and the 自选 rules: ../README.md ("How to add
// an operator (自选)").
//
// Forms (data/backups.json units.char_1031_slent2): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
// picked module 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 (zh_CN, as built into backups.json), PRTS 淬羽赫默 (无声砥柱 备注
// "根据其已损失的生命值比例(0%~70%,以1%为最小单位)线性提升…每当庇护数值变化时,重新为该友方单位附加庇护BUFF"; 丰润羽翼 备注
// on the BLS-Y barrier; 俯瞰视界 备注 "技能期间撤回夜灯时,夜灯立刻返回待部署区并进入再部署时间"; 无畏者协议 备注 "只在没有持有
// 不死的我方干员正在应用致命伤害时…触发…该效果不会因淬羽赫默的离场消失"), PRTS 夜灯, PRTS 衍生生命 (致命伤害: "无畏者协议提供的
// 不死效果仅在目标没有不死效果时才会触发"), and the client's battle data read from the local install — charpack
// char_1031_slent2 (talent abilities: slent2_t_1 on the allies of her range every 0.1 s, removed when they leave; slent2_t_2
// likewise, its selector ignoring 禁疗 — ignoreHealFree; the S1 / S2 / S3 modes: attacks become heals of max_target injured
// allies, the lowest HP share first) and buff_template_data slent2_t_1 / slent2_t_2 / slent2_s_2 / slent2_s_3 /
// slent2_e_002[max_hp] / slent2_e_003_t2 (+ [shield]); the template atk_to_hp_recovery is the one PRTS documents as
// 「生命回复速度」 for 絮雨 天赋 and 琴柳 S2 ("生命恢复的提供方式为增加目标的“生命回复速度”属性,不受治疗加成和禁疗影响").
// - Trait (护佑者) "攻击造成法术伤害,技能开启后改为治疗友方单位(治疗量相当于75%攻击力)": ranged arts bolts (the SUPPORT
// default; 3×3-and-more y-6, hits air units), blocks 1, ground enemies target her (no flag); while ANY of her skills runs
// every attack heals the most injured ally of her range instead (trait heal_scale × ATK; allies = operators and summons).
// BLS-X “难得清醒”: heal_scale 1 (100 %). BLS-Y “可控动量” "攻击对2个目标造成法术伤害,技能开启后改为治疗2个友方单位"
// (trait attack@max_target 2): two targets / two heals.
// Her skills are no heal skills for the 技能策略: a 辅助's basic strategy casts on an enemy she is about to attack (PRTS
// 卫戍协议/帮助: only 医疗干员 attack the units needing a heal), so every skill keeps the data's DEFAULT.
// - T1 无声砥柱 "使攻击范围内的友军获得10%的庇护…(低于30%生命时获得最大24%的庇护)": every ally of her range (herself
// included) holds 庇护 (ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)"; the client's damage_resistance[inf],
// the 庇护 the other kits share as `protect` — battle.applyStrongest) of damage_resistance_base × (1 + resistance_scale × n),
// n = its lost HP in whole hp_ratio (1 %) steps, at most 1 − min_hp_ratio (70 steps ⇒ 10 % × 2.4 = 24 %), re-read every
// 0.1 s (the client's trigger) and gone at once when it leaves her range (a lower value takes over within one refresh).
// S3 / the 夜灯 scale damage_resistance_base (the client's `_scaleCertainKeyList`), i.e. the whole value.
// - T2 丰润羽翼 "攻击范围内生命低于50%的友军每秒恢复相当于淬羽赫默攻击力5%的生命,【莱茵生命】干员的恢复效果翻倍": while an
// ally of her range is below hp_ratio of its max HP it gains 生命回复速度 (an hpRegen buff, checklist 11 — the template
// atk_to_hp_recovery, see above: 禁疗 does not stop it, no 治疗加成) of atk_to_hp_recovery_ratio × her ATK, from 1 s after it
// dropped below (waitFirstTriggerInterval) and re-read every 1 s; a 【莱茵生命】 operator (character_table groupId "rhine")
// gets it twice (the client's [normal] + [rhine] buffs). One buff per 淬羽赫默 (independentCharacterSource).
// BLS-X stage 2+ (hidden max_hp; slent2_e_002[max_hp]) "攻击范围内友军的最大生命值+10%": allies of her range max HP
// +max_hp (MAX_HP MULTIPLIER = Σpct), ×2 for a 【莱茵生命】 operator; and 7 % ATK per second (the talent change's ratio).
// BLS-Y stage 2+ (hidden hp_ratio / scale; slent2_e_003_t2) "其中每个干员低于50%生命时仅一次立刻获得淬羽赫默生命上限50%的屏障":
// an operator of her range gets a 屏障 (absorbs every type) of scale × her max HP (×2 for 【莱茵生命】, the client's
// multi 2) once per deployment of hers (the card mark, cleared when she leaves): when it enters her range below hp_ratio, or
// on a damage instance that would leave it below hp_ratio of its max HP ("在每次“受到伤害前”进行预判"): the barrier takes
// that very hit (an `hpDamage` handler run first, after the other shields — PRTS: "能以更早的时间点触发…一定程度抵挡可能致命
// 的伤害"). Summons get no barrier ("其中每个干员") [ASSUMED: the text; the client data shows one validator].
// - S1 进取之心 (MANUAL, DEFAULT, 25 s): ATK +atk; heals (trait).
// - S2 俯瞰视界 (MANUAL, DEFAULT, 12 s): ASPD +attack_speed; heals; "技能期间可以使用一个辅助无人机…技能结束或淬羽赫默离场时
// 销毁" (slent2_s_2: WithdrawTokens at its end, the token count recharged): her placed 夜灯 piece takes the field on its tile
// when the skill starts (content/tokens.js releaseSkillSummon; like every placed skill summon it also deploys once, free, at
// the battle start — PRTS 卫戍协议/帮助) and is withdrawn when the skill ends (into its 5 s redeploy time, PRTS 备注) or when
// she leaves the field; a piece already up when the skill starts serves as its drone [ASSUMED: the start deploy lasts until
// the first end]. 夜灯 (token kit below): untargetable ("不会受到攻击", data), no attack (ATK 0), blocks nothing; its passive
// "使周围八格友军享受2.1倍的淬羽赫默第一天赋效果": the allies of its x-4 (its own tile and the 8 around) hold her T1 庇护 ×
// damage_resistance_scale (its skill blackboard: 2.1 / 2.4) — the same shared 庇护, so the stronger value holds.
// - S3 无畏者协议 (MANUAL, DEFAULT, 60 s, skill_max_trigger_time 2 uses per battle — the 3rd never comes: no SP after the
// 2nd; "可主动关闭" is a manual stop the 技能策略 never makes, so it runs its full time): ATK +atk, T1 ×talent_scale, heals;
// once per cast the first operator of her range taking lethal damage (not its own — CheckEntitySuicide; not while it already
// holds 不死 — "_dontConsumeWhenUndeadable") keeps ≥ 1 HP and holds 不死 for grave_duration s (slent2_shallow_grave, buff
// flag `undying`; it outlasts her leaving). Order of the `fatal` step (items/battle.js): after the kits' own savers, a
// 傀儡师's switch and 坚固维式重锤's lock (PRIO_REVIVE −100), before the 复活 (PRIO_RESPAWN −101); the window it opens is
// held like the hammer's (PRIO_UNDYING_HELD −99).
// Statuses: none applied to enemies. Damage: arts normal attacks only.
import { num, up, traitBb, moduleBb, talentBb, skillRec, once, holdProtect } from '../shared/tier1.js';
import { releaseSkillSummon } from '../../tokens.js';
import { holdsUndying, PRIO_UNDYING_HELD, PRIO_REVIVE, PRIO_RESPAWN } from '../../items/battle.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_slent2_1';
const S2 = 'skchr_slent2_2';
const S3 = 'skchr_slent2_3';
export const LAMP = 'token_10029_slent2_protrb';
/** The client's trigger period of slent2_t_1 / slent2_t_2 (talent abilities, triggerInterval 0.1). */
export const TALENT_IV = 0.1;
/** Lifetime of a refreshed effect: a little longer than the refresh period, so it never lapses while it holds. */
const HOLD = TALENT_IV + 0.05;
/** atk_to_hp_recovery: its trigger interval (1 s, the first after one interval). */
const REGEN_IV = 1;
/** BLS-X stage 2+ max HP (slent2_e_002[max_hp]): one "同名" effect per unit. */
export const MAXHP_KEY = 'slent2:maxhp';
/** S3's 不死 (slent2_shallow_grave). */
export const GRAVE_KEY = 'slent2:grave';
/** S3's place in the `fatal` step: after the items' 不死 lock (−100), before their 复活 (−101) — see the header. */
const PRIO_GRAVE = (PRIO_REVIVE + PRIO_RESPAWN) / 2;
/** 夜灯's area when its record carries no grid: its tile and the 8 around (x-4). */
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
/** 【莱茵生命】 (character_table groupId "rhine": every member, so a 自选 pick counts too). */
export const RHINE = Object.freeze(new Set([
'char_128_plosis', 'char_135_halo', 'char_242_otter', 'char_108_silent', 'char_249_mlyss', 'char_134_ifrit',
'char_1047_halo2', 'char_248_mgllan', 'char_1031_slent2', 'char_4212_nasti', 'char_4048_doroth', 'char_202_demkni',
]));
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tileKey = (r, c) => r * COLS + c;
/** An operator of the 莱茵生命 group (summons are no 干员). */
export const isRhine = (a) => !!a && a.kind === 'op' && RHINE.has(a.def?.charId ?? a.defId);
const inKeys = (keys, a) => !!keys && keys.has(tileKey(a.tileR, a.tileC));
/**
* 无声砥柱's 庇护 for an ally at its HP: base × scale × (1 + rs × n), n = its lost HP in whole `step` steps (at most
* 1 − min_hp_ratio) — the client's HpRatioTrigger count, PRTS 备注 "以1%为最小单位".
*/
export function shelterOf(t, ally, scale = 1) {
const base = num(t.damage_resistance_base) * scale;
if (!(base > 0)) return 0;
const step = num(t.hp_ratio, 0.01) || 0.01;
const lost = Math.min(Math.max(0, 1 - ally.hpRatio), Math.max(0, 1 - num(t.min_hp_ratio, 0.3)));
const n = Math.floor(lost / step + 1e-9);
return Math.min(0.95, base * (1 + num(t.resistance_scale) * n));
}
/** The allies (operators and summons; never a 孤立 one) on absolute tile keys `keys`. */
function alliesOn(battle, by, keys) {
return battle.allyUnits.filter((a) => a.alive && a.deployed && !a.hidden && a.kind !== 'device' && inKeys(keys, a) && battle.allySelectable(a, by));
}
/**
* 夜灯's kit: untargetable ("不会受到攻击": the def's flag, Battle._setupUnit), no attack (ATK 0 ⇒ resolveProfile noAttack),
* no skill to cast; while it stands, every TALENT_IV s the allies of its x-4 hold her T1 庇护 × damage_resistance_scale.
* `owner` = 淬羽赫默, `t1` = her T1 blackboard (fallback when the token def carries none).
*/
function lampKit(t, owner, t1) {
const def = t.def;
const tal = (def?.talents ?? []).find((x) => x && x.bb && x.bb.damage_resistance_base != null) ?? null;
const tb = tal?.bb ?? t1;
const grid = tal?.rangeGrid ?? def?.rangeGrid ?? X4;
const scale = num(def?.skill?.bb?.damage_resistance_scale, num(owner.skill?.bb?.damage_resistance_scale, 1));
return {
skill: null,
trait: { noAttack: true },
install(battle, lamp) {
battle.every(TALENT_IV, () => {
if (!up(lamp)) return;
const keys = new Set(absoluteRangeKeys(grid, lamp.tileR, lamp.tileC, lamp.dir, 0));
for (const a of alliesOn(battle, lamp, keys)) {
const v = shelterOf(tb, a, scale);
holdProtect(battle, a, v, HOLD, lamp); // 庇护 (the client's damage_resistance[inf]): the shared effect
}
}, { owner: lamp, immediate: true });
},
};
}
export default {
char_1031_slent2: (bb, chess) => {
const tb = traitBb(chess);
const t1 = talentBb(chess, 0), t2 = talentBb(chess, 1), hidden = moduleBb(chess);
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const targets = Math.max(1, Math.floor(num(tb['attack@max_target'], 1)));
const healAttack = Object.freeze({
dmgType: 'heal', heal: targets > 1 ? { mode: 'multi', count: targets } : { mode: 'single' },
healScale: num(tb.heal_scale, 0.75), projectile: 'none',
});
const regenRatio = num(t2.atk_to_hp_recovery_ratio), regenBelow = num(t2.hp_ratio, 0.5);
const maxHp = num(hidden.max_hp); // BLS-X stage 2+
const barrierScale = num(hidden.scale), barrierBelow = num(hidden.hp_ratio, 0.5); // BLS-Y stage 2+
const graveDur = num(b3.grave_duration), s3Uses = Math.max(1, Math.floor(num(b3.skill_max_trigger_time, 2)));
const talentScale = num(b3.talent_scale, 1);
return {
trait: targets > 1 ? { maxTargets: targets } : null,
skills: {
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk) }, attack: healAttack },
[S2]: {
kind: 'duration', mods: { aspd: num(b2.attack_speed) }, attack: healAttack,
onStart({ battle, unit }) {
// a 夜灯 already up (the battle-start deploy) is this skill's drone; else her placed piece takes the field
if (!battle.allyUnits.some((t) => t.kind === 'token' && t.defId === LAMP && t.ownerUnit === unit && t.alive)) releaseSkillSummon(battle, unit, LAMP);
},
onEnd({ battle, unit }) { withdrawLamps(battle, unit); },
},
[S3]: {
kind: 'duration', mods: { atkPct: num(b3.atk) }, attack: healAttack,
onStart({ unit }) { unit.mem.slent2Grave = true; }, // the one 不死 of this cast (slent2_shallow_grave[self])
onEnd({ unit }) { unit.mem.slent2Grave = false; },
},
},
talents: [
{ install(battle, unit) { // 无声砥柱 (+ S3 ×talent_scale) and 丰润羽翼 (+ BLS-X max HP, BLS-Y barrier on entering)
const regenKey = `talent:slent2:regen#${unit.id}`;
const regen = new Map(); // ally → { next: battle time of its next atk_to_hp_recovery trigger }
let prevIn = new Set(); // the allies of her range at the previous scan (slent2_t_2 started on them)
const stopRegen = (a) => { regen.delete(a); if (a.findBuff(regenKey)) battle.removeBuff(a, regenKey); };
battle.on('deploy', (c) => { if (c.unit === unit) { unit.mem.slent2Marks = new Set(); prevIn = new Set(); } }, { owner: unit });
battle.every(TALENT_IV, () => {
if (!up(unit)) {
for (const a of [...regen.keys()]) stopRegen(a);
prevIn = new Set();
return;
}
const scale = unit.skill?.id === S3 && unit.skill.active ? talentScale : 1;
const allies = alliesOn(battle, unit, unit.rangeKeySet);
const now = new Set(allies);
for (const a of allies) {
const v = shelterOf(t1, a, scale);
holdProtect(battle, a, v, HOLD, unit);
const mult = isRhine(a) ? 2 : 1;
if (maxHp > 0) battle.applyStrongest(a, MAXHP_KEY, { duration: HOLD, value: maxHp * mult, mods: (x) => ({ hpPct: x }), source: unit });
if (regenRatio > 0 && a.hpRatio < regenBelow - 1e-9) {
const st = regen.get(a) ?? { next: battle.time + REGEN_IV };
regen.set(a, st);
if (battle.time + 1e-9 >= st.next) {
st.next += REGEN_IV;
battle.addBuff(a, { key: regenKey, mods: { hpRegen: unit.s.atk * regenRatio * mult }, source: unit, tags: ['talent'] });
}
} else if (regen.has(a)) stopRegen(a);
if (barrierScale > 0 && !prevIn.has(a) && a.hpRatio < barrierBelow - 1e-9) giveBarrier(battle, unit, a, null);
}
for (const a of [...regen.keys()]) if (!now.has(a) || !a.alive) stopRegen(a);
prevIn = now;
}, { owner: unit, immediate: true });
// BLS-Y: a damage instance that would leave an operator of her range below barrierBelow — the barrier takes it
if (barrierScale > 0) {
battle.on('hpDamage', (c) => {
const a = c.target;
if (!up(unit) || !a || a.side !== 'ally' || !(c.amount > 0) || !inKeys(unit.rangeKeySet, a)) return;
if (a.hp - c.amount < a.s.maxHp * barrierBelow - 1e-9) giveBarrier(battle, unit, a, c);
}, { owner: unit, priority: 100 });
}
/** BLS-Y barrier on operator `a` (once per deployment of hers); `c` = the hpDamage ctx it takes, if any. */
function giveBarrier(b, u, a, c) {
const marks = u.mem.slent2Marks ?? (u.mem.slent2Marks = new Set());
if (a.kind !== 'op' || marks.has(a.id)) return;
marks.add(a.id);
let cap = u.s.maxHp * barrierScale * (isRhine(a) ? 2 : 1);
if (c) { const took = Math.min(cap, c.amount); c.amount -= took; cap -= took; }
if (cap > 1e-6) b.addBuff(a, { key: `slent2:barrier#${u.id}`, shield: cap, source: u, visible: true, tags: ['talent'] });
b.fx('shield', { x: a.x, y: a.y, id: a.id });
}
} },
{ install(battle, unit) { // 夜灯: her placed pieces run the drone's kit; withdrawn when she leaves the field
for (const t of battle.allyUnits) {
if (t.kind !== 'token' || t.defId !== LAMP || t.ownerUnit !== unit || t.alive || t.deployed) continue;
// (no offOwner for a piece set up before her: its hooks are content/tokens.js's dock hooks — the generic token
// kit registers none — and they must stay)
battle._setupUnit(t, lampKit(t, unit, t1));
}
battle.on('death', (c) => { if (c.unit === unit) withdrawLamps(battle, unit); }, { owner: unit });
} },
],
install(battle, unit) {
if (unit.skill?.id !== S3) return;
// 同一次作战中最多使用2次: after the last use no SP comes back (activations count every deployment of the battle)
battle.on('spGain', (c) => {
if (c.unit === unit && !unit.skill.active && unit.skill.activations >= s3Uses) c.amount = 0;
}, { owner: unit });
// the one 不死 of each cast: the first operator of her range taking lethal damage (see the header)
battle.on('fatal', (c) => {
const a = c.unit;
if (c.prevented || !unit.mem.slent2Grave || !unit.skill?.active || !up(unit)) return;
if (!a || a.side !== 'ally' || a.kind !== 'op' || !inKeys(unit.rangeKeySet, a)) return;
if (c.source === a || c.credit === a || holdsUndying(battle, a)) return;
c.prevented = true;
unit.mem.slent2Grave = false;
battle.addBuff(a, { key: GRAVE_KEY, duration: graveDur, flags: { undying: true }, source: unit, visible: true, tags: ['skill'] });
battle.fx('undying', { x: a.x, y: a.y, id: a.id, duration: graveDur });
}, { owner: unit, priority: PRIO_GRAVE });
// the 不死 window, held whoever opened it (one battle-wide hook, like 坚固维式重锤's running windows)
once(battle, 'slent2:grave', () => {
battle.on('fatal', (c) => { if (!c.prevented && c.unit && c.unit.findBuff(GRAVE_KEY)) c.prevented = true; }, { priority: PRIO_UNDYING_HELD });
});
},
};
},
};
/** 俯瞰视界's end or her leaving: her 夜灯 leave the field (into their redeploy time) and the drone in stock is gone. */
function withdrawLamps(battle, unit) {
const stock = unit.mem.summonStock;
if (stock) stock[LAMP] = 0;
for (const t of battle.allyUnits) {
if (t.kind === 'token' && t.defId === LAMP && t.ownerUnit === unit && t.alive) battle.retreat(t, { reason: 'expired', permanent: true });
}
}