Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-thorns.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

230 lines
17 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-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 });
},
};
},
};