// server/sim/content/kits/ops/op-thorns.js — 棘刺 (char_293_thorns) 自选 operator kit: 6★ 领主 (近卫), an owned-6★ pick of // the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (LOR-X, LOR-Δ) at every form. // Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)"). // // Forms (data/backups.json units.char_293_thorns, 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); the client's battle // logic — buff_template_data (thorns_t_1, thorns_s_2, thorns_e_002_t / [mark] / _tr, thorns_e_003_t / _tr), his charpack // (talent 1 `_additiveActiveBuffs` thorns_t_1: triggerInterval 1, waitFirstTriggerInterval, independentCharacterSource; // talent 2 toggle `_disableWhenInAttackState` / `InCombatState`, `_restoreDelay` 2) and the skill prefabs (skchr_thorns_2 // Thorn: `_preDelay` 0.5, cooldown key `cooldown`, the trait's 0.8 scale, ground + air selector, `max_target`; // skchr_thorns_3 [a] / [b] buffs), the module parts ([uc]equips thorns_equip_1_n / 2_n: the trait buffs, and from stage 2 // the talent overrides — LOR-X `_targetFamilyMask` 31, LOR-Δ 3) — and PRTS 棘刺 (talent 备注, S2 备注, S3 备注, LOR-X 特性 // "于计算伤害时触发"). // - Trait (领主) "可以进行远程攻击,但此时攻击力降低至80%": the lord profile (professions.js: ×atk_scale unless the target // stands on his tile / the tile in front or he blocks it), range 3-12, hits air units (data canHitFly), blocks 2. // Module LOR-X “盐与沙” adds "攻击附带10%攻击力的法术伤害" (trait atk_scale_m; thorns_e_002_tr: ON_CALCULATE_DAMAGE, attack // type ADDITION — PRTS "造成预计算法术附加伤害"): atk_scale_m × ATK arts after every hit of his attacks (the trait's // afterHit, as 拉普兰德 / 断崖 / 仇白 / 银灰 do it; tags module / addition) and of S2's spikes [ASSUMED: the Thorn ability // calculates its damage like an attack; the fixed-value poison ticks do not]. Module LOR-Δ “沙蚀” adds // "造成伤害时附带相当于10%伤害的神经损伤" (trait ep_damage_ratio; thorns_e_003_tr: ON_AFTER_OUTPUT_DAMAGE, every damage // type): ep_damage_ratio × the HP damage of every damage instance he deals — attacks, spikes, LOR-X additions and the // poison ticks — as 神经损伤 (docs/SIM.md §7.2 element conventions). // - T1 神经腐蚀 "攻击使目标中毒,在3秒内每秒受到125点法术伤害(对会远程攻击的目标伤害加倍)" (full potential: 140): every // hit of his attacks (and of the S2 spikes, PRTS "所有攻击均能附加") poisons the target — one poison per 棘刺 // (independentCharacterSource) for `duration` s, ticking every second, the first a second in (waitFirstTriggerInterval), damage[normal] or // damage[ranged] (an enemy whose 攻击方式 includes 远程: data applyWay RANGED or ALL; PRTS "伤害加倍为数值提升") as a fixed // arts 持续伤害 (FixedValueDamage MAGICAL, attack type BUFF: RES applies, no dodge, tags dot). A new hit refreshes the // duration and keeps the damage value and the tick rhythm (PRTS "仅刷新持续时间而不改变伤害数值"). The ticks go on after // he leaves the field [ASSUMED, as 伊芙利特's 灼伤]. A dodged hit poisons nothing (no `damaged`; 击中目标的攻击). // LOR-X stage 2+ (thorns_e_002_t: no end of its own, cnt 1; every hit a new [mark] of `duration` s whose start adds 1 // to cnt up to max_cnt; a tick without a [mark] ends it — damage × cnt) "可叠加4次": the tick is the first application's // value × the hits so far (≤ max_cnt; PRTS "以第一层的伤害数值为基础进行翻倍"), the poison ending `duration` s after the // last hit; all his hits, spikes included (mask 31). LOR-Δ stage 2+ (thorns_e_003_t, ep_break_multi): on an enemy in its // 神经损伤 burst (the `neuralBurst` lock, FilterEPBreakRecoveryType SANITY) the tick is ×ep_break_multi 元素伤害; only his // normal attacks poison (mask 3; PRTS "仅有普通攻击能附加(意味着2技能不再附加中毒Buff)"), and that poison has no // independentCharacterSource: one per enemy whoever applies it. At stage 1 both modules leave the talent as it is // (battle_equip_table: no talent part in phase 1). // - T2 故土潮声 "如果2秒内没有主动攻击过,每秒恢复最大生命3.5%的生命" (full potential: 4%): PRTS "增加“生命回复速度(百分比)”属性, // 不受治疗加成和禁疗影响" — an hpRegenRatio buff (checklist 11, never battle.heal), on while his last attack is at least // `delay` s old (the toggle's restoreDelay after the attack states) and from his deployment on [ASSUMED: no attack yet counts as // "2秒内没有主动攻击过"]. The S2 spikes are no 主动攻击 (PRTS "不会使第二天赋效果中断"). // - S1 攻击力强化·γ型 (MANUAL, data DEFAULT): ATK +atk for `duration` s. // - S2 护身尖刺 (MANUAL, 技能范围 3-1, data SKILL_RANGE): "停止攻击" (no normal attack; PRTS "技能期间,自身丢失全部视野"), // ATK +atk, DEF +def; every 普通伤害 an enemy deals him (PRTS "指受到普通伤害(伤害类型),而非由普通攻击造成的伤害"; // thorns_s_2 FilterDamageModifer attack type NORMAL: not 溅射, not 持续 (dot / periodic), not 附加, not element damage, // not a 流失, not 无来源) sets off the Thorn ability when its `cooldown` (0.8 / 0.75 s) is ready and he can act: 0.5 s // later (its `_preDelay`) up to max_target enemies of the 3-1 — ground and air, the usual target order — each take ATK × // the trait's atk_scale (PRTS "始终受降低攻击倍率特性的影响") physical, a skill hit that is no normal attack (no `attack` // event, no SP), cancelled when the skill has ended, he left or cannot act by then [ASSUMED]. The 3-1 is not his // attack range: the card keeps his 3-12. // - S3 至高之术 (MANUAL, attack SP, data ACTIVE_RANGE on its 3-3): range 3-3, ATK +atk, ASPD +attack_speed, every attack at // full ATK ("远程攻击不再降低攻击力": the skill's attack profile has no 0.8 — dmgMul 1, as 领主·Sharp S1) for `duration` s; // "第二次及以后使用时能力加成变为最初的两倍,且持续时间无限": from the second activation of a deployment on, the // thorns_s_3[b] numbers (ATK / ASPD ×2) and no end until he leaves (a toggle; the first use counts its own 30 s down) — // PRTS "每次部署棘刺时,重新计算该技能的使用次数": the count restarts with every deployment. import { num, talentBb, traitBb, skillRec, up } from '../shared/tier1.js'; import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js'; import { hasHp, isHpLoss } from '../../../damage.js'; const S1 = 'skcom_atk_up[3]'; const S2 = 'skchr_thorns_2'; const S3 = 'skchr_thorns_3'; /** The skill ranges when a record carries none (data: 3-1, 3-3). */ const R3_1 = Object.freeze([[1, 0], [1, 1], [1, 2], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-1, 1], [-1, 2]]); const R3_3 = Object.freeze([[1, 0], [1, 1], [1, 2], [1, 3], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-1, 1], [-1, 2], [-1, 3]]); /** skchr_thorns_2 Thorn `_preDelay`: the spikes land this long after the hit that set them off. */ const SPIKE_DELAY = 0.5; /** thorns_t_1 triggerInterval: a poison tick every second. */ const POISON_IV = 1; const AIR = Object.freeze({ canHitFly: true }); const S3_BUFF = 'skill:thorns:destreza'; const REGEN_KEY = 'talent:thorns:tide'; const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {}; /** An enemy whose 攻击方式 includes 远程 (PRTS "图鉴中【攻击方式】包含远程的敌人"; data applyWay). */ const attacksRanged = (e) => { const w = String(e?.def?.applyWay ?? '').toUpperCase(); return w === 'RANGED' || w === 'ALL'; }; /** * 普通伤害 (PRTS 伤害分类; thorns_s_2 FilterDamageModifer attack type NORMAL) an enemy dealt — not 溅射, 持续 (dot / * periodic), 附加, element damage, a 流失 or 无来源 damage. */ function normalHitFromEnemy(c) { const d = c.dmg; if (!d || !c.source || c.source.side !== 'enemy' || d.sourceless || d.isSplash || isHpLoss(d)) return false; if (c.type === 'element' || c.type === 'elemental') return false; const tags = d.tags || []; return !tags.includes('dot') && !tags.includes('periodic') && !tags.includes('addition'); } export default { char_293_thorns: (bb, chess) => { const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1); const tb = traitBb(chess); const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3); const grid2 = skillRec(chess, S2)?.rangeGrid ?? R3_1, grid3 = skillRec(chess, S3)?.rangeGrid ?? R3_3; const rangedScale = num(tb.atk_scale, 0.8); const loreX = num(tb.atk_scale_m); // LOR-X: arts addition const loreD = num(tb.ep_damage_ratio); // LOR-Δ: 神经损伤 rider const maxCnt = Math.max(1, Math.floor(num(t0.max_cnt, 1))); // LOR-X stage 2+: 可叠加 const breakMul = num(t0.ep_break_multi); // LOR-Δ stage 2+: ×multi 元素伤害 in a 神经 burst const spikesPoison = !(breakMul > 0); // LOR-Δ stage 2+: only normal attacks poison /** LOR-X "攻击附带10%攻击力的法术伤害": the arts addition of one hit (PRTS: 预计算法术附加伤害). */ const addition = (battle, unit, target) => { if (loreX > 0 && target && target.alive && target.side === 'enemy' && hasHp(target)) { battle.dealDamage(unit, target, { amount: unit.s.atk * loreX, type: 'arts', tags: ['module', 'addition', 'thorns:lorx'] }); } }; /** T1 神经腐蚀: poison `e` (one per 棘刺; a new hit refreshes it, LOR-X stage 2+ adds a layer). */ const poison = (battle, unit, e) => { if (!e || !e.alive || e.side !== 'enemy' || !hasHp(e)) return; const dur = num(t0.duration, 3); const base = num(attacksRanged(e) ? t0['damage[ranged]'] : t0['damage[normal]']); if (!(dur > 0) || !(base > 0)) return; // one poison per 棘刺 (independentCharacterSource) — but the LOR-Δ stage 2+ one (thorns_e_003_t has no such flag) const key = breakMul > 0 ? 'thorns:poison' : `thorns:poison:${unit.id}`; const old = e.findBuff(key); const b = battle.addBuff(e, { key, duration: dur, refresh: 'extend', interval: POISON_IV, source: unit, tags: ['dot', 'talent'], data: { base, cnt: 1 }, onTick({ battle: bt, unit: tgt, buff }) { if (!tgt.alive || !hasHp(tgt)) return; let amount = buff.data.base * (maxCnt > 1 ? buff.data.cnt : 1); let type = 'arts'; if (breakMul > 0 && tgt.findBuff('neuralBurst')) { amount *= breakMul; type = 'elemental'; } bt.dealDamage(unit, tgt, { amount, type, element: type === 'elemental' ? 'neural' : undefined, canDodge: false, tags: ['talent', 'dot', 'thorns:poison'] }); }, }); if (old && b === old && maxCnt > 1) old.data.cnt = Math.min(maxCnt, num(old.data.cnt, 1) + 1); }; return { trait: { // LOR-X: every hit of his attacks carries the arts addition afterHit(battle, u, target) { addition(battle, u, target); }, }, skills: { [S1]: { kind: 'duration', mods: { atkPct: num(b1.atk) } }, [S2]: { kind: 'duration', mods: { atkPct: num(b2.atk), defPct: num(b2.def) }, attack: { noAttack: true }, // 停止攻击 }, [S3]: { kind: 'toggle', targeting: { rangeGrid: grid3 }, attack: { dmgMul: () => 1 }, // 远程攻击不再降低攻击力 onStart({ battle, unit, skill }) { const n = (unit.mem.thornsS3Uses = num(unit.mem.thornsS3Uses) + 1); const again = n >= 2; const atk = again ? num(b3['thorns_s_3[b].atk'], 2 * num(b3.atk)) : num(b3.atk); const aspd = again ? num(b3['thorns_s_3[b].attack_speed'], 2 * num(b3.attack_speed)) : num(b3.attack_speed); battle.addBuff(unit, { key: S3_BUFF, mods: { atkPct: atk, aspd }, tags: ['skill'] }); // the first use runs `duration` s; from the second on "持续时间无限" ([b].duration −1) skill.timeLeft = again && num(b3['thorns_s_3[b].duration'], -1) < 0 ? Infinity : Math.max(0.01, skill.duration); }, onTick({ skill, dt }) { if (!Number.isFinite(skill.timeLeft)) return; skill.timeLeft -= dt; if (skill.timeLeft <= 1e-9) skill.end('duration'); }, onEnd({ battle, unit }) { battle.removeBuff(unit, S3_BUFF); }, }, }, talents: [ { install(battle, unit) { // 神经腐蚀: his attack hits (and the spikes, but under LOR-Δ stage 2+) poison battle.on('damaged', (c) => { if (c.source !== unit || !c.target || c.target.side !== 'enemy' || !c.dmg || c.type === 'element') return; const tags = c.dmg.tags || []; if (c.dmg.isAttack || (spikesPoison && tags.includes('thorns:spike'))) poison(battle, unit, c.target); }, { owner: unit }); } }, { install(battle, unit) { // 故土潮声: 生命回复速度 while no attack of his is younger than `delay` s const r = num(t1.hp_recovery_per_sec_by_max_hp_ratio), delay = num(t1.delay, 2); if (!(r > 0)) return; unit.mem.thornsAttackAt = -Infinity; battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.thornsAttackAt = -Infinity; }, { owner: unit }); battle.on('attack', (c) => { if (c.attacker === unit) unit.mem.thornsAttackAt = battle.time; }, { owner: unit }); const apply = () => { const want = up(unit) && battle.time - unit.mem.thornsAttackAt >= delay - 1e-9; const has = unit.findBuff(REGEN_KEY); if (want && !has) battle.addBuff(unit, { key: REGEN_KEY, mods: { hpRegenRatio: r }, tags: ['talent'] }); else if (!want && has) battle.removeBuff(unit, REGEN_KEY); }; battle.on('tick', apply, { owner: unit }); battle.on('battleStart', apply, { owner: unit }); battle.on('deploy', (c) => { if (c.unit === unit && battle.started) apply(); }, { owner: unit }); } }, ], install(battle, unit) { // S3: the use count restarts with every deployment (PRTS "每次部署棘刺时,重新计算该技能的使用次数") battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.thornsS3Uses = 0; }, { owner: unit }); // LOR-Δ: 神经损伤 = ep_damage_ratio × the HP damage of every damage instance he deals if (loreD > 0) { battle.on('damaged', (c) => { if (c.source !== unit || !c.target || c.target.side !== 'enemy' || c.type === 'element' || !(c.amount > 0) || !hasHp(c.target)) return; battle.dealDamage(unit, c.target, { type: 'element', element: 'neural', amount: c.amount * loreD, tags: ['module', 'thorns:lord'] }); }, { owner: unit }); } if (unit.skill?.id !== S2) return; // S2 护身尖刺: 普通伤害 from an enemy ⇒ the Thorn ability (cooldown), 0.5 s later up to max_target spikes on the 3-1 unit.mem.thornsSpikeAt = -Infinity; const cd = num(b2.cooldown, 0.8), n = Math.max(1, Math.floor(num(b2.max_target, 4))); battle.on('damaged', (c) => { const sk = unit.skill; if (c.target !== unit || !sk || !sk.active || !up(unit) || !unit.canAct || !normalHitFromEnemy(c)) return; if (battle.time < unit.mem.thornsSpikeAt + cd - 1e-9) return; unit.mem.thornsSpikeAt = battle.time; const seq = unit.deploySeq, act = sk.activations; battle.after(SPIKE_DELAY, () => { if (!up(unit) || unit.deploySeq !== seq || !unit.canAct || !unit.skill?.active || unit.skill.activations !== act) return; const list = battle.enemiesInKeys(absoluteRangeKeys(grid2, unit.tileR, unit.tileC, unit.dir, 0), unit, AIR); sortEnemyTargets(battle, unit, list, null); const targets = list.slice(0, n); if (!targets.length) return; battle.fx('thorns', { x: unit.x, y: unit.y, id: unit.id, n: targets.length }); for (const e of targets) { if (!e.alive) continue; battle.dealDamage(unit, e, { amount: unit.s.atk * rangedScale * unit.s.atkScaleMul, type: 'phys', isSkill: true, tags: ['skill', 'thorns:spike'] }); addition(battle, unit, e); } }, { owner: unit }); }, { owner: unit }); }, }; }, };