273 lines
20 KiB
JavaScript
273 lines
20 KiB
JavaScript
// 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 });
|
||
}
|
||
}
|