feedback5 O1: 自选 operator kits 伊芙利特, 谬因 (with her 中继器) and 林 — every skill, talent, trait and module (stage 1 / 3) at every form

- op-ifrit.js: 轰击术师 line; 精神融解 RES ×0.6 aura, BLA-Δ Lv3 +50 % ATK 元素伤害 on burn-bursting targets (attacks and 灼地 ticks);
  莱茵回路 +2 SP / 6 s, BLA-X Lv3 30 % +5; S1 ATK / ASPD; S2 charges, DEF cut + 灼伤 (current ATK, lasts after she leaves);
  S3 维持技能状态 (no attack, ground-only ticks from the Begin clip's event then every 1 s, RES cut, 2 %/s 流失, ended by stun);
  BLA-X distance scaling on all her damage while she stands, BLA-Δ 8 % 灼燃损伤 rider.
- op-aphris.js: her range is the extension path (5 tiles, +2 and turned by her 中继器; S2 unbounded and turned by allied
  operators); the 中继器's lifetime / marks / redeploy (leaves and returns with her, cleared by S3), ATK +25 / 35 % while it
  stands, 取样优化 法术穿透 on allied operators of her path; S1 SP_FULL toggle; S2 beam every 0.5 s (no attack); S3 ammo 20,
  3 s 强制缴械, extra hit + longer 停顿 past a 中继器; BLA-Y cost −8, Lv3 35 s / 20 s / +35 %.
- op-lin.js: 琉璃璧 shield (absorbs ≤ 200, breaks above: stun + burst on her current range, regrows 8 / 6 s), 韬光 per hit
  taken; S1 状态切换 toggle (停顿), S2 琉璃璧 copies for allies in range, S3 x-2 + threshold ×2 / ×2.5 + kill refresh;
  PLX-X keeps DEF +100 % / RES +10 in skills, PLX-Y +3 % per enemy in range (≤ 5).
- Sources: data/backups.json, PRTS (备注), the client's battle data (charpacks, battle/prefabs skills / equips, buff templates)
  and the battle skeletons' clip events; [ASSUMED] items in the kit headers.
- kits/index.js OPERATOR_KIT_FILES += op-ifrit.js, op-aphris.js, op-lin.js; tests test/content/op_ifrit / op_aphris / op_lin
  (10 each).
- Golden: diy only — diy-003 … diy-006 now pack the new kits (they held the prototypes), diy-007 … diy-012 are new (the rest of
  林, then the prototypes repacked into diy-008 … diy-012); diy-001 / 002 (推进之王) and every other family unchanged.
This commit is contained in:
sganggs committed 2026-10-06 00:20:37 +08:00
1 parent 742f478526
commit 541048fed6
8 files changed
+1945 -100

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
// 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). Potential 0 [ASSUMED: no account].
// 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; PLX-Y stage 3: 110 %, 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点技力" (PLX-X stage 3: 75 %, 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 });
}
},
};
},
};