Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-ifrit.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

217 lines
15 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-ifrit.js — 伊芙利特 (char_134_ifrit) 自选 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_134_ifrit, 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 伊芙利特 (炎爆 备注
// "灼伤:目标每秒受到伊芙利特当前攻击力33%的法术持续伤害,伊芙利特离场后也会生效" and "<模组相关>于伊芙利特在场时受模组基础效果影响";
// 灼地 备注 "属于维持技能状态,时间模式为跟随动画", "降低法术抗性的效果持续1秒"; 精神融解 备注 "<Δ模组>造成的元素伤害为持续伤害";
// “同熔” 特性 "目标距离自身0格或以上时应用加成,按距离线性提升,目标距离自身4格或以上时达到最大值"); PRTS 游戏数据基础 §维持技能状态;
// PRTS 分支特性信息 轰击术师 ("可对空", "普通攻击同时攻击范围内的所有敌人"); the client's battle data (battle/prefabs [uc]skills
// skchr_ifrit_1/2/3, [uc]equips ifrit_equip_*, charpack char_134_ifrit, buff_template_data: burn, atk_scale_by_distance,
// periodic_damage_by_hp_ratio[skip_modifier_fix], ifrit_e_002[sp] / [dice_sp], ifrit_e_003_tr, ifrit_e_003_ele) and her
// battle skeleton (Skill_2_Begin's OnAttack event).
// - Trait (轰击术师) "攻击造成超远距离的群体法术伤害": the profession's `rangeAoe` — every selectable enemy of her 5-1 line at
// once, air units too, the same damage near and far (professions.js blastcaster).
// - Module BLA-X “同熔” (trait min_dist 0 / max_dist 4 / damage_scale 0.1): "距离越远伤害越高,最高达到110%" — ×(1 + 0.1 ×
// clamp((d − 0) / (4 − 0), 0, 1)) by the Euclidean distance d between her and the target, on EVERY damage she outputs
// while on the field (the module's buff atk_scale_by_distance: ON_OUTPUT_DAMAGE, no damage mask) — her attacks, the 灼地
// ticks and the 炎爆 灼伤 (PRTS: "于伊芙利特在场时受模组基础效果影响"). Stage 3 also changes T2 (below).
// - Module BLA-D “热成形记忆” (trait ep_damage_ratio 0.08): "造成法术伤害时附带相当于8%伤害的灼燃损伤" (ifrit_e_003_tr: ON_AFTER_
// OUTPUT_DAMAGE, every MAGICAL damage of hers) — 8 % of the HP damage dealt as 灼燃损伤 (`type: 'element'`, docs/SIM.md §7.2
// element conventions), the 灼伤 too while she stands. Stage 3 also changes T1 (below).
// - T1 精神融解 "攻击范围内的敌军法术抗性-40%" (full potential: −44 %; magic_resistance −0.44: a ratio; charpack ifrit_t_1, removed
// when the enemy leaves): RES ×0.56 on every enemy on her range, refreshed every AURA_IV s; several 伊芙利特 keep one
// (applyStrongest — the mode's "同名效果取最高" convention; [ASSUMED] the client's independentCharacterSource flag would let
// two stack, which a lone base-game 伊芙利特 never shows). BLA-D stage 2+ "若攻击目标处于灼燃损伤爆发期间,攻击对其额外造成
// 相当于攻击力50%的元素伤害" (the module's hidden talent element_atk_scale; ifrit_e_003_ele: IsTargetInEPBreakRecovery FIRE
// ⇒ ELEMENT damage, attack type BUFF, 持续伤害): every hit of her attack abilities — normal attacks and the 灼地 ticks
// (the equip adds it to the abilities of `_targetFamilyMask` 31, [ASSUMED] every family: 1 / 3 mark the narrower equips)
// — on an enemy in its 灼燃 burst (its `burnBurst` lock) adds 50 % ATK 元素伤害 (`type: 'elemental'`), tagged 'dot'
// (PRTS: "<Δ模组>造成的元素伤害为持续伤害"); never the 灼伤 (a buff's damage).
// - T2 莱茵回路 "每6秒额外回复2点技力" (full potential: every 5.5 s; periodic_sp; BLA-X stage 2+ ifrit_e_002[sp]): +sp SP every
// `interval` s of a deployment (the first one interval in: waitFirstTriggerInterval; a gift during a running 狂热 / 灼地
// is lost — no SP during a skill). BLA-X stage 2+ "有30%概率额外回复5点技力" (ifrit_e_002[dice_sp] prob / interval / sp):
// one roll per its own interval.
// - S1 狂热 (MANUAL, data DEFAULT, 20 s): ATK +atk, ASPD +attack_speed.
// - S2 炎爆 (AUTO, 可充能 2 / 3 次, data DEFAULT — the "next attack" waits for her attack): the next attack hits her whole line
// (ground and air: its selector's targetMotion ALL) for atk_scale × ATK; every enemy it hits gets the buff ifrit_s_2
// (template burn): DEF +def (−100 / −200, "同名效果取最高") for `duration` (3.01) s and the 灼伤 — burn.atk_scale (33 %) × her
// CURRENT ATK arts every second of it (triggerInterval 1, the first a second in: 3 ticks), a 持续伤害 (no dodge) that keeps
// ticking after she leaves the field. A new 炎爆 on the same enemy restarts both.
// - S3 灼地 (MANUAL, data DEFAULT, 20 s): a 维持技能状态 — no normal attack. The ability (skchr_ifrit_3: `_triggerDelta` 1,
// ground-only selector) strikes first at the attack event of its Begin clip (SCORCH_FIRST: 0.4 s; facing UP / DOWN the
// back / down clips, 0.333 s) and then every S3_CADENCE (1) s — a fixed cadence no ASPD change touches: every ground enemy
// on her range takes atk_scale × ATK arts and the active buff ifrit_s_3, RES +magic_resistance (−7 / −10, flat) for 1 s
// (PRTS 备注) — the cut before the damage [ASSUMED order], so the tick it comes with counts it. Her own periodic_damage_by_
// hp_ratio buff takes hp_ratio (2 %) of her max HP every second of the skill (the first a second in; a 流失 — it ignores
// for SP and skips the damage events —, it can knock her out; PRTS "实际的生命流失量略低于标注值" gives no number [ASSUMED:
// the stated 2 %]). 晕眩 / 冻结 (any state that stops her acting) ends it at once (PRTS 维持技能状态).
import { num, talentBb, moduleBb, traitBb, skillRec, up, giveSp } from '../shared/tier1.js';
import { bodyInKeys } from '../../../body.js';
import { hasHp } from '../../../damage.js';
const S1 = 'skchr_ifrit_1';
const S2 = 'skchr_ifrit_2';
const S3 = 'skchr_ifrit_3';
/** T1 aura refresh period and buff length (the non-stacking aura convention of the kits). */
const AURA_IV = 0.25;
const AURA_DUR = 0.4;
/** S3 灼地: the ability's `_triggerDelta` (s) — one damage tick per second, "每秒". */
const S3_CADENCE = 1;
/** S3 灼地: the first tick at the OnAttack event of Skill_2_Begin (front 0.4 s; the back clip and Skill_Down_2_Begin 0.333 s). */
const SCORCH_FIRST = Object.freeze({ side: 0.4, vertical: 0.333 });
/** S3 灼地 RES cut length (ifrit_s_3 lifeTime 1; PRTS 备注 "降低法术抗性的效果持续1秒"). */
const S3_RES_TIME = 1;
/** S3 灼地 self 流失 period (periodic_damage_by_hp_ratio triggerInterval 1). */
const S3_LOSS_IV = 1;
const MELT_KEY = 'ifrit:meltdown';
const S2_DEF_KEY = 'ifrit:pyroDef';
const S2_BURN_KEY = 'ifrit:pyroBurn';
const S3_RES_KEY = 'ifrit:scorchRes';
const SCORCH_TAG = 'ifrit:scorch';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** RES cut mods for a blackboard magic_resistance value (applyStrongest): |v| < 1 = ×(1 + v), else flat v. */
const resCut = (v) => (Math.abs(v) < 1 ? { resMul: Math.max(0, 1 + v) } : { resFlat: v });
const defCut = (v) => ({ defFlat: v });
/** S2 炎爆 on one enemy its attack hit: DEF cut and the 灼伤 for `duration` s. */
function pyroclasm(battle, unit, e, b2) {
if (!e || !e.alive || e.side !== 'enemy') return;
const dur = num(b2.duration, 3);
const d = num(b2.def);
if (d) battle.applyStrongest(e, S2_DEF_KEY, { duration: dur, value: d, mods: defCut, source: unit });
const sc = num(b2['burn.atk_scale']);
if (!(sc > 0)) return;
battle.addBuff(e, {
key: S2_BURN_KEY, duration: dur, interval: 1, source: unit, tags: ['dot'],
// "伊芙利特当前攻击力": her ATK at each tick; it ticks on after she leaves the field (PRTS 备注)
onTick: ({ battle: b, unit: tgt }) => b.dealDamage(unit, tgt, { amount: unit.s.atk * sc, type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'skill', 'ifrit:burn'] }),
});
}
/** S3 灼地 tick: every ground enemy on her range — the RES cut, then the damage. */
function scorch(battle, unit, b3) {
const scale = num(b3.atk_scale), mr = num(b3.magic_resistance);
const victims = battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: false });
for (const e of victims) {
if (mr) battle.applyStrongest(e, S3_RES_KEY, { duration: S3_RES_TIME, value: mr, mods: resCut, source: unit });
if (e.alive && scale > 0) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', SCORCH_TAG] });
}
battle.fx('flame', { x: unit.x, y: unit.y, id: unit.id, n: victims.length, skill: SCORCH_TAG });
}
export default {
char_134_ifrit: (bb, chess) => {
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
const hidden = moduleBb(chess); // BLA-D stage 2+: element_atk_scale
const tb = traitBb(chess); // BLA-X: min_dist / max_dist / damage_scale; BLA-D: ep_damage_ratio
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
return {
skills: {
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk), aspd: num(b1.attack_speed) } },
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
attack: {
atkScale: num(b2.atk_scale, 1),
onEachHit({ battle, unit, target }) { pyroclasm(battle, unit, target, b2); },
},
onStart({ battle, unit }) { battle.fx('aoe', { x: unit.x, y: unit.y, radius: 0.5, id: unit.id, skill: 'ifrit:pyroclasm' }); },
},
[S3]: {
kind: 'duration',
attack: { noAttack: true },
onStart({ unit }) {
unit.mem.scorchAt = 0;
unit.mem.scorchNext = unit.dir === 'UP' || unit.dir === 'DOWN' ? SCORCH_FIRST.vertical : SCORCH_FIRST.side;
unit.mem.scorchLoss = 0;
},
onTick({ battle, unit, skill, dt }) {
// 维持技能状态: a hard control (晕眩 / 冻结 / 浮空 / 睡眠 — the unit cannot act) ends it at once
if (!unit.canAct) { skill.end('interrupted'); return; }
const t = (unit.mem.scorchAt = (unit.mem.scorchAt ?? 0) + dt);
while (t >= unit.mem.scorchNext - 1e-6 && unit.alive && skill.active) {
unit.mem.scorchNext += S3_CADENCE;
scorch(battle, unit, b3);
}
const ratio = num(b3.hp_ratio);
while (ratio > 0 && t >= (unit.mem.scorchLoss + 1) * S3_LOSS_IV - 1e-6 && unit.alive) {
unit.mem.scorchLoss++;
battle.loseHp(unit, unit.s.maxHp * ratio, { source: unit, tags: ['skill', SCORCH_TAG] });
}
},
},
},
talents: [
{ install(battle, unit) { // 精神融解: RES −44 % on her range; BLA-D stage 2+: +50 % ATK 元素伤害 on a bursting target
const v = num(t0.magic_resistance);
if (v) {
battle.every(AURA_IV, () => {
if (!up(unit) || !unit.rangeKeySet) return;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || !bodyInKeys(e, unit.rangeKeySet)) continue;
battle.applyStrongest(e, MELT_KEY, { duration: AURA_DUR, value: v, mods: resCut, source: unit });
}
}, { owner: unit, immediate: true });
}
const ex = num(hidden.element_atk_scale);
if (ex > 0) {
battle.on('damaged', (c) => {
const e = c.target, d = c.dmg;
if (c.source !== unit || !d || !(d.isAttack || d.tags?.includes(SCORCH_TAG)) || !e || e.side !== 'enemy' || !hasHp(e) || !e.findBuff('burnBurst')) return;
battle.dealDamage(unit, e, { amount: unit.s.atk * ex, type: 'elemental', element: 'burn', canDodge: false, tags: ['dot', 'module', 'ifrit:meltdown'] });
}, { owner: unit });
}
} },
{ install(battle, unit) { // 莱茵回路: +sp SP every interval s; BLA-X stage 2+: prob to +5 SP more
const sp = num(t1.sp), iv = num(t1.interval, 6);
const dProb = num(t1['ifrit_e_002[dice_sp].prob']), dSp = num(t1['ifrit_e_002[dice_sp].sp']);
const dIv = num(t1['ifrit_e_002[dice_sp].interval'], iv);
if (!(sp > 0 && iv > 0) && !(dProb > 0 && dSp > 0 && dIv > 0)) return;
battle.on('deploy', (c) => { if (c.unit === unit) { unit.mem.rhineAcc = 0; unit.mem.rhineDice = 0; } }, { owner: unit });
battle.on('tick', ({ dt }) => {
if (!up(unit)) return;
if (sp > 0 && iv > 0) {
unit.mem.rhineAcc = (unit.mem.rhineAcc ?? 0) + dt;
while (unit.mem.rhineAcc >= iv - 1e-6) { unit.mem.rhineAcc -= iv; giveSp(unit, sp); }
}
if (dProb > 0 && dSp > 0 && dIv > 0) {
unit.mem.rhineDice = (unit.mem.rhineDice ?? 0) + dt;
while (unit.mem.rhineDice >= dIv - 1e-6) {
unit.mem.rhineDice -= dIv;
if (battle.rng.chance(dProb)) giveSp(unit, dSp);
}
}
}, { owner: unit });
} },
],
install(battle, unit) {
// BLA-X “同熔”: farther targets take more of every damage she outputs (up to ×1 + damage_scale at max_dist tiles)
const ds = num(tb.damage_scale);
if (ds > 0 && tb.max_dist != null) {
const lo = num(tb.min_dist), hi = num(tb.max_dist, 4);
battle.on('hit', (c) => {
const e = c.target;
if (c.source !== unit || !e || e.side !== 'enemy' || c.dmg.type === 'element' || !up(unit)) return;
const d = Math.hypot(e.x - unit.x, e.y - unit.y);
c.dmg.mul *= 1 + ds * (hi > lo ? Math.max(0, Math.min(1, (d - lo) / (hi - lo))) : 1);
}, { owner: unit });
}
// BLA-D “热成形记忆”: 8 % of each arts damage she deals as 灼燃损伤
const ep = num(tb.ep_damage_ratio);
if (ep > 0) {
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || c.type !== 'arts' || !(c.amount > 0) || !e || e.side !== 'enemy' || !hasHp(e) || !up(unit)) return;
battle.dealDamage(unit, e, { type: 'element', element: 'burn', amount: c.amount * ep, tags: ['module', 'ifrit:thermal'] });
}, { owner: unit });
}
},
};
},
};