230 lines
16 KiB
JavaScript
230 lines
16 KiB
JavaScript
// server/sim/content/kits/ops/op-lin.js — 林 (char_4080_lin) 自选 operator kit: 6★ 阵法术师 (术师), an owned-6★ pick of the
|
||
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules at every form.
|
||
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||
//
|
||
// Forms (data/backups.json units.char_4080_lin, the DIY slot statuses): 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). Full potential (the owner's decision of 2026-10-07).
|
||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); PRTS 林 (计出万全 备注:
|
||
// "通常情况下琉璃璧生效优先级为+1000", "琉璃璧破碎效果于满足条件的伤害应用前瞬间触发" — so a lethal hit still sets it off —,
|
||
// "其破碎时的影响范围以持有该效果的单位为中心,与林的当前攻击范围一致,先造成晕眩后造成伤害", "可造成琉璃璧破碎的伤害结算时,伤害量减少
|
||
// 相当于琉璃璧吸收伤害上限的数值"; 玲珑 备注 "非初始状态下…技能开启时的效果均会生效,但此状态下并不实际属于“技能期间”"; 荫庇 备注
|
||
// "每个干员单独获得琉璃壁,单独计算琉璃壁破碎与恢复时间", "伤害计算使用林的攻击力,伤害来源为林自身"; 流光乍裂 备注 "第一天赋生效的
|
||
// 优先级从+1000变为±0,琉璃璧破碎效果于满足条件的伤害应用后瞬间触发", "仅自身普通攻击造成击杀时可触发琉璃璧破碎/生成效果,单次普攻
|
||
// 最多触发1次效果"); PRTS 卫戍协议/帮助 §技能操作 ("携带状态切换类技能的干员…每次部署后仅开启一次技能"; 阵法术师 row "在初始攻击范围内
|
||
// 存在敌人时释放技能"; 林【流光乍裂】 x-2 "在自定义范围内存在敌人时释放技能"); PRTS 分支特性信息 阵法术师 ("攻击时可对空"); the client's
|
||
// battle data (charpack char_4080_lin, battle/prefabs [uc]skills skchr_lin_1/2/3, buff_template_data lin_t_1 / lin_t_1[damage]
|
||
// / lin_t_1_s_3 / lin_t_1_s_3[refresh] / lin_t_2 / lin_e_002[half_attribute] / lin_e_002_trait).
|
||
// - Trait (阵法术师) "通常时不攻击且防御力和法术抗性大幅度提升,技能开启时攻击造成群体法术伤害": the profession (professions.js
|
||
// phalanx — DEF +def ×, RES +magic_resistance while no skill runs; while one runs, `rangeAoe`: every selectable enemy of
|
||
// her range at once, air units too). Module PLX-X “七窍玲珑” overrides it with "更大幅度提升" (def 2.15 / magic_resistance
|
||
// 25, the profession's trait tunables) and "技能开启时保留部分效果" (lin_e_002[buff] def 1 / magic_resistance 10): DEF +100 %
|
||
// and RES +10 while any skill runs — 玲珑's switched state too (PRTS: "技能开启时的效果均会生效"). Module PLX-Y “情与义”:
|
||
// "范围内敌人越多造成的伤害越高(最高提升15%)" — the hidden module talent: +damage_scale (3 %) per enemy on her range, at most
|
||
// max_valid_stack_cnt (5), on every damage she deals (the 琉璃璧 bursts too), counted like 卡涅利安's PLX-Y (every living
|
||
// enemy standing on her current range).
|
||
// - T1 计出万全 (lin_t_1, HIGH_PRIORITY: ON_TAKE_DAMAGE → BlockDamage of a fixed `value`, a break when the damage left it
|
||
// negative): a 琉璃璧 on her from every deployment — an HP shield of `value` (200) that every damage instance refills
|
||
// (`absorbShields` absorbs it after DEF / RES and the damage multipliers): a hit of at most 200 is absorbed whole; a hit
|
||
// over 200 ("单次超过200点伤害") goes through less 200 (PRTS: "伤害量减少相当于琉璃璧吸收伤害上限的数值") and breaks it — the
|
||
// burst (lin_t_1[damage] on the targets of Lin's "Attack" selector, i.e. her CURRENT attack range, centred on the holder):
|
||
// stun `stun` s on every enemy there, then atk_scale × Lin's ATK arts (source Lin) — and lin_t_1[disable] brings it back
|
||
// `interval` s later (8 s; full potential: 110 %; PLX-Y stage 3: 120 %, 6 s). A 流失 or an element 损伤 never touches it.
|
||
// [ASSUMED] the shield sits in the buff order (HIGH_PRIORITY puts it first; no other shield usually stands on her), and
|
||
// the burst comes right after the hit has landed — before a knock-out, as PRTS's lethal-hit note says — in the 流光乍裂
|
||
// order too (PRTS: "伤害应用后").
|
||
// - T2 韬光 "受到攻击时,有50%几率回复1点技力" (full potential: 55 %; PLX-X stage 3: 80 %, 2 SP): the client's lin_t_2
|
||
// (ON_TAKE_DAMAGE → IsIgnoreForSp → Dice prob → ModifySp sp, no damage-value filter): one roll per damage instance she
|
||
// takes that could give 受击回复 SP — a hit the 琉璃璧 absorbs whole included (it is still taken, at 0); not a 流失 or an
|
||
// element 损伤. No SP while a skill runs (玲珑's state included: it stays on).
|
||
// - S1 玲珑 (MANUAL, data SEARCH, a 状态切换类 skill: switched on once per deployment and kept — the official rule, `toggle`):
|
||
// attack interval +base_attack_time (+1.0 s on her 2.0 s), ATK +atk, every enemy each attack hits 停顿 attack@sluggish s.
|
||
// PRTS's "并不实际属于技能期间" changes nothing here: the state is never switched back in the auto battle.
|
||
// - S2 荫庇 (MANUAL, data SEARCH, 25 s): ASPD +attack_speed, taunt +taunt_level (−1: "自身不容易受到敌人攻击"); every allied
|
||
// unit on her range (the client's validator: ground allies — every ally of this mode but the 孤立 “炎佑”, which no ally
|
||
// ability selects) gets its own lin_t_1 — the plain T1 (threshold `value`, Lin's ATK and source, its own break and
|
||
// `interval` regrowth; independentCharacterSource, not on Lin herself) — while the skill runs (removed when the ability
|
||
// detaches), checked every SHELTER_IV s; an ally that leaves the field loses it (its buffs go).
|
||
// - S3 流光乍裂 (MANUAL, data CUSTOM_RANGE on its x-2, 26 / 28 s): ATK +atk; range x-2 (the burst range follows: it is her
|
||
// current range: lin_t_1_s_3_selector); her own 琉璃璧 threshold ×talent_scale (2 / 2.5) while it runs (lin_t_1_s_3);
|
||
// each kill by her "Attack" ability (ON_TARGET_KILLED, one per attack) runs lin_t_1_s_3[refresh]: a standing 琉璃璧 bursts
|
||
// and stays, a broken one comes back at once. The auto battle never switches it off early.
|
||
|
||
import { num, talentBb, moduleBb, traitBb, skillRec, up, giveSp, batMod } from '../shared/tier1.js';
|
||
import { COLS } from '../../../constants.js';
|
||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||
import { bodyInKeys } from '../../../body.js';
|
||
import { isHpLoss } from '../../../damage.js';
|
||
|
||
const S1 = 'skchr_lin_1';
|
||
const S2 = 'skchr_lin_2';
|
||
const S3 = 'skchr_lin_3';
|
||
/** The 琉璃璧 shield buff (one per holder: Lin's own, and the S2 copies on her allies). */
|
||
const GLASS_KEY = 'lin:glass';
|
||
/** PLX-X: the part of the trait kept while a skill runs. */
|
||
const KEEP_KEY = 'lin:plxKeep';
|
||
/** S2 荫庇: how often the allies on her range are checked for a 琉璃璧. */
|
||
const SHELTER_IV = 0.1;
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
const s3On = (lin) => !!(lin.skill && lin.skill.active && lin.skill.id === S3);
|
||
|
||
export default {
|
||
char_4080_lin: (bb, chess) => {
|
||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||
const hidden = moduleBb(chess); // PLX-Y: damage_scale per enemy, max_valid_stack_cnt
|
||
const tb = traitBb(chess); // PLX-X: lin_e_002[buff] def / magic_resistance
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
const g3 = skillRec(chess, S3)?.rangeGrid ?? null;
|
||
const glass = { value: num(t0.value, 200), atkScale: num(t0.atk_scale, 1), stun: num(t0.stun, 1), interval: num(t0.interval, 8) };
|
||
const s3Scale = num(b3.talent_scale, 1);
|
||
|
||
/** Threshold (= shield) of a holder's 琉璃璧: Lin's own ×talent_scale while 流光乍裂 runs, the plain value otherwise. */
|
||
const thresholdOf = (lin, holder) => (holder === lin && s3On(lin) ? glass.value * s3Scale : glass.value);
|
||
/** (Re)raise the 琉璃璧 of `holder`, full. `mods: {}` keeps the buff when an instance drains it exactly (absorbShields). */
|
||
const raise = (battle, lin, holder) => {
|
||
if (!holder.alive) return;
|
||
const r = holder.mem.linGlassRegen;
|
||
if (r) { r.cancel(); holder.mem.linGlassRegen = null; }
|
||
battle.addBuff(holder, { key: GLASS_KEY, shield: thresholdOf(lin, holder), mods: {}, source: lin, tags: ['talent'] });
|
||
battle.fx('shield', { x: holder.x, y: holder.y, id: holder.id, skill: 'lin:glass' });
|
||
};
|
||
/** The break: Lin's current attack range centred on the holder — stun first, then the damage (PRTS 备注). */
|
||
const burst = (battle, lin, holder) => {
|
||
const grid = lin.liveRangeGrid || lin.rangeGrid || [[0, 0]];
|
||
const keys = absoluteRangeKeys(grid, holder.tileR, holder.tileC, lin.dir, 0);
|
||
const victims = battle.enemiesInKeys(keys, lin, { canHitFly: true });
|
||
if (glass.stun > 0) for (const e of victims) battle.applyStatus(e, 'stun', { duration: glass.stun, source: lin });
|
||
for (const e of victims) {
|
||
if (e.alive && glass.atkScale > 0) battle.dealDamage(lin, e, { amount: lin.s.atk * glass.atkScale, type: 'arts', tags: ['talent', 'lin:glass'] });
|
||
}
|
||
battle.fx('aoe', { x: holder.x, y: holder.y, radius: 1.5, id: lin.id, skill: 'lin:glass' });
|
||
};
|
||
/** Break `holder`'s 琉璃璧; it grows back `interval` s later (an S2 copy only while 荫庇 still covers it), or at once. */
|
||
const shatter = (battle, lin, holder, now = false) => {
|
||
battle.removeBuff(holder, GLASS_KEY);
|
||
burst(battle, lin, holder);
|
||
if (now) { raise(battle, lin, holder); return; }
|
||
const seq = holder.deploySeq;
|
||
holder.mem.linGlassRegen = battle.after(glass.interval, () => {
|
||
holder.mem.linGlassRegen = null;
|
||
if (!holder.alive || holder.deploySeq !== seq || holder.findBuff(GLASS_KEY)) return;
|
||
if (holder !== lin && !lin.mem.shelter?.has(holder.id)) return;
|
||
raise(battle, lin, holder);
|
||
}, { owner: lin });
|
||
};
|
||
/** Drop an S2 copy (the ally left her range, or 荫庇 ended). */
|
||
const dropCopy = (battle, holder) => {
|
||
const g = holder.findBuff(GLASS_KEY);
|
||
if (g) battle.removeBuff(holder, g);
|
||
const r = holder.mem.linGlassRegen;
|
||
if (r) { r.cancel(); holder.mem.linGlassRegen = null; }
|
||
};
|
||
/** S2: the allied units (not Lin, not devices) on her range get / keep / lose their 琉璃璧. */
|
||
const shelterTick = (battle, lin) => {
|
||
const st = lin.mem.shelter;
|
||
if (!st || !lin.rangeKeySet) return;
|
||
const now = new Set();
|
||
for (const a of battle.alliesFor(lin)) {
|
||
if (a === lin || a.kind === 'device' || !lin.rangeKeySet.has(a.tileR * COLS + a.tileC)) continue;
|
||
now.add(a.id);
|
||
if (!st.has(a.id)) { st.add(a.id); raise(battle, lin, a); }
|
||
}
|
||
for (const id of [...st]) {
|
||
if (now.has(id)) continue;
|
||
st.delete(id);
|
||
const a = battle.unitById(id);
|
||
if (a) dropCopy(battle, a);
|
||
}
|
||
};
|
||
const endShelter = (battle, lin) => {
|
||
const st = lin.mem.shelter;
|
||
lin.mem.shelter = null;
|
||
for (const id of st || []) { const a = battle.unitById(id); if (a) dropCopy(battle, a); }
|
||
};
|
||
|
||
return {
|
||
skills: {
|
||
[S1]: {
|
||
kind: 'toggle',
|
||
mods: { atkPct: num(b1.atk), batPct: batMod(b1.base_attack_time, chess) },
|
||
attack: { onHitStatus: { key: 'sluggish', duration: num(b1['attack@sluggish']) } },
|
||
},
|
||
[S2]: {
|
||
kind: 'duration',
|
||
mods: { aspd: num(b2.attack_speed), taunt: num(b2.taunt_level) },
|
||
onStart({ battle, unit }) { unit.mem.shelter = new Set(); shelterTick(battle, unit); },
|
||
onTick({ battle, unit, dt }) {
|
||
unit.mem.shelterAcc = (unit.mem.shelterAcc ?? 0) + dt;
|
||
if (unit.mem.shelterAcc + 1e-9 < SHELTER_IV) return;
|
||
unit.mem.shelterAcc = 0;
|
||
shelterTick(battle, unit);
|
||
},
|
||
onEnd({ battle, unit }) { unit.mem.shelterAcc = 0; endShelter(battle, unit); },
|
||
},
|
||
[S3]: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(b3.atk) },
|
||
targeting: g3 ? { rangeGrid: g3 } : undefined,
|
||
onStart({ battle, unit }) { // the threshold ×talent_scale: a standing 琉璃璧 grows to it
|
||
const g = unit.findBuff(GLASS_KEY);
|
||
if (g) { g.shield = thresholdOf(unit, unit); unit.markDirty(); }
|
||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 2, id: unit.id, skill: 'lin:s3' });
|
||
},
|
||
onAttack({ battle, unit, targets }) { // a knock-out by this attack: break and raise again, once per attack
|
||
if (!(targets || []).some((t) => t && t.side === 'enemy' && !t.alive)) return;
|
||
if (unit.findBuff(GLASS_KEY)) shatter(battle, unit, unit, true);
|
||
else raise(battle, unit, unit);
|
||
},
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 计出万全: her 琉璃璧 (and the S2 copies — the same hook: any holder of one she gave)
|
||
battle.on('deploy', (c) => { if (c.unit === unit) raise(battle, unit, unit); }, { owner: unit });
|
||
battle.on('damaged', (c) => {
|
||
const h = c.target;
|
||
const g = h && h.findBuff(GLASS_KEY);
|
||
if (!g || g.source !== unit || c.type === 'element' || isHpLoss(c.dmg)) return;
|
||
if (g.shield > 1e-9 || !(c.amount > 0)) { // absorbed whole: full again for the next instance
|
||
g.shield = thresholdOf(unit, h);
|
||
h.markDirty();
|
||
return;
|
||
}
|
||
if (h === unit || unit.alive) shatter(battle, unit, h);
|
||
}, { owner: unit, priority: 50 });
|
||
battle.on('skillEnd', (c) => { // 流光乍裂 over: a standing 琉璃璧 shrinks to the plain threshold
|
||
if (c.unit !== unit || c.skill.id !== S3) return;
|
||
const g = unit.findBuff(GLASS_KEY);
|
||
if (g) { g.shield = thresholdOf(unit, unit); unit.markDirty(); }
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 韬光: prob to +sp SP per damage instance taken (absorbed or not) that could give 受击回复 SP
|
||
const p = num(t1.prob), sp = num(t1.sp);
|
||
if (!(p > 0 && sp > 0)) return;
|
||
battle.on('damaged', (c) => {
|
||
if (c.target !== unit || !c.dmg || c.dmg.noSp || c.type === 'element' || isHpLoss(c.dmg) || !up(unit)) return;
|
||
if (battle.rng.chance(p)) giveSp(unit, sp);
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
// PLX-X “七窍玲珑”: 技能开启时保留部分效果 (any skill; 玲珑's switched state included)
|
||
const keepDef = num(tb['lin_e_002[buff].def']), keepRes = num(tb['lin_e_002[buff].magic_resistance']);
|
||
if (keepDef || keepRes) {
|
||
battle.on('skillStart', (c) => { if (c.unit === unit) battle.addBuff(unit, { key: KEEP_KEY, mods: { defPct: keepDef, resFlat: keepRes }, tags: ['module'] }); }, { owner: unit });
|
||
battle.on('skillEnd', (c) => { if (c.unit === unit) battle.removeBuff(unit, KEEP_KEY); }, { owner: unit });
|
||
}
|
||
// PLX-Y “情与义”: +damage_scale per enemy on her range (≤ max_valid_stack_cnt), every damage she deals
|
||
const per = num(hidden.damage_scale), cap = Math.max(0, Math.floor(num(hidden.max_valid_stack_cnt, 5)));
|
||
if (per > 0 && cap > 0) {
|
||
battle.on('hit', (c) => {
|
||
if (c.source !== unit || !c.target || c.target.side !== 'enemy' || c.dmg.type === 'element' || !unit.rangeKeySet) return;
|
||
let n = 0;
|
||
for (const e of battle.enemies) if (e.alive && !e.hidden && bodyInKeys(e, unit.rangeKeySet) && ++n >= cap) break;
|
||
if (n > 0) c.dmg.mul *= 1 + per * n;
|
||
}, { owner: unit });
|
||
}
|
||
},
|
||
};
|
||
},
|
||
};
|