feedback5 O11: 自选 operator kits 薇薇安娜, 怒潮凛冬 and 赫德雷 — every skill, talent, trait and module (stage 1 / 3) at every form

- kits/ops/op-vvana.js, op-headb2.js, op-hodrer.js (registered in OPERATOR_KIT_FILES), checked against the data, PRTS and
  the client's battle data (charpacks, [uc]skills / [uc]equips prefabs, buff templates, 怒潮凛冬's Skill_3 clip events).
- Engine (additive): ai.js enemyAttack marks a shot that flies as a projectile isProjectile (远程途径: 薇薇安娜's 散华 护盾
  blocks melee-path hits only); targeting.js priority 'elite' (薇薇安娜 S3 "优先攻击精英或领袖敌人"); docs/SIM.md §4 / §8.
- Tests: test/content/op_vvana.test.js (13), op_headb2.test.js (10), op_hodrer.test.js (8).
- Golden: only diy moved — diy-049 … 054 now field the three operators in every form × module × skill, the prototypes
  follow in diy-054 … 058 (same packing); diy-001 … 048 and every other family unchanged.
This commit is contained in:
sganggs committed 2026-10-06 01:38:08 +08:00
1 parent fef4ea3d4b
commit e0cce79f98
11 files changed
+2128 -113

No files matched your search

+283
View File
@@ -0,0 +1,283 @@
// server/sim/content/kits/ops/op-vvana.js — 薇薇安娜 (char_4098_vvana) 自选 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_4098_vvana, 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 薇薇安娜 (备注 of
// 散华, 光影迅捷剑 and “明灭”, the AFT-Δ trait note); gamedata_const ba.charged 蓄力, ba.shield 护盾, ba.steal 偷取,
// ba.magicfragile 法术脆弱; PRTS 作战机制 (伤害流程, 远近途径 "有弹道的攻击固定为10,无弹道的攻击固定为01"); range_table 3-2; the
// client's battle data read from the local install — charpack char_4098_vvana, battle/prefabs [uc]skills skchr_vvana_1/2/3
// and [uc]equips vvana_equip_*, buff_template_data vvana_* (the templates are named below).
// - Trait (术战者) "攻击造成法术伤害": the profession (melee arts, ground only, block 1, 1.25 s).
// - Module AFT-D 目不能及之处: trait "攻击灼燃损伤爆发期间的目标时额外造成攻击力15%的元素伤害" (trait bb ep_damage_ratio; PRTS
// "※于任意非本模组特性的伤害的输出伤害时触发"; vvana_e_002_trait: ON_OUTPUT_DAMAGE, not its own damage, the target in its FIRE
// 爆发冷却 → ELEMENT damage ep_damage_ratio × ATK): every damage instance she outputs on an enemy holding the `burnBurst` lock
// (docs/SIM.md §3) — at the output (the `hit` step, before the target's dodge / cancel) — adds ep_damage_ratio × ATK
// 元素伤害. Stage 3: 燃烛施明's 灼燃损伤 (below).
// - Module AFT-Y “最后一行”: trait "自身阻挡的敌人受到10%的法术脆弱效果" (trait bb damage_scale 1.1; vvana_equip_2_*: weak[magic]
// to every blockee): the 法术脆弱 status (同名效果取最高) on each enemy she blocks, renewed every 0.2 s for 0.3 s (as 史尔特尔's
// AFT-Y). Stage 3: 散华 25 %, 2 layers (below).
// - T1 燃烛施明 "造成的法术伤害+8%,受到的物理和法术伤害-8%。攻击范围内存在精英或领袖敌人时,该效果提升至2倍" (damage_scale_m,
// damage_resistance_pm, super_scale; vvana_t_1[self] / [aura] / [super]: an aura on the enemies of her current range —
// targetMotion ALL, flyers too, no untargetable one — and on those she blocks; an ELITE / BOSS one swaps [self] for [super],
// ×super_scale): artsDealtMul 1 + 0.08 f, phys / artsTakenMul 1 − 0.08 f, f = super_scale while such an enemy is there, else
// 1 — re-read every tick and before every hit she deals or takes. AFT-D stage 3 "攻击附带相当于9%伤害的灼燃损伤"
// (ep_damage_ratio_m; the templates' ON_AFTER_OUTPUT_DAMAGE on her non-continuous arts damage, ApplyElementDamageBasedOnDamageValue
// FIRE): each arts damage instance of hers that removes HP carries ep × f × that damage as 灼燃损伤.
// - T2 散华 "攻击精英或领袖敌人时,有18%概率获得一层仅抵挡近战攻击的护盾(最多1层)" (prob; AFT-Y stage 3: 25 %, max_stack_cnt 2;
// S3: × talent_scale; vvana_t_2: ON_OUTPUT_DAMAGE on an ELITE_AND_BOSS target, Dice prob, CreateBuff vvana_t_2[block_melee] —
// UNIQUE without max_stack_cnt, STACK up to it with — so a roll at the cap changes nothing, PRTS "※自身未存在本天赋的护盾时,每对
// 符合条件的目标造成一次伤害判定一次本天赋"): one roll per damage instance she outputs on an elite / leader (any damage — the AFT-D
// rider too — at the output, before the target's dodge / cancel). vvana_t_2[block_melee]: ON_TAKE_DAMAGE from an ENEMY source
// with apply way MELEE → BlockDamage, one layer spent (ba.shield "每层护盾可以抵挡一次伤害"; PRTS "※仅来源于敌方单位的近战途径伤害
// 可触发本天赋的护盾"): an enemy's damage that did not fly as a projectile (DamageInfo `isProjectile`, ai.js enemyAttack) is
// negated whole in a late `hit` handler (cancel, as the enemies' 护盾 layers in content/enemies.js); 无来源 damage has no enemy
// source. The enemy content's own projectiles (invisible / leaders / fly kits) are not marked and count as melee [ASSUMED].
// - S1 光影迅捷剑 (AUTO, 2 charges, data DEFAULT — a "next attack" waits for her attack): the next attack at atk_scale × ATK,
// S1_HITS hits (S1_Attack2, `_additionalTimes` 1). 蓄力 (ba.charged "回复至上限2倍时进入蓄力状态…任何时候开启均消耗全部技力"; PRTS
// 备注 "未满足蓄力所需技力时…仅消耗技能最低所需技力" / "“蓄力”模式将在下一次技能结束时结束" / "“蓄力”模式期间…攻击范围变为3-2";
// the skill's toggle at SP ratio 1 puts vvana_s_1[mode] — mode 1, range 3-2 — whose ON_SKILL_START clears the SP and
// ON_SKILL_FINISH ends it; S1_Attack, `_additionalTimes` 2 on 3-2, needs its mark): with both charges stored her own range is
// 3-2 (the DEFAULT trigger's initial range and the card follow it) until the next end of the skill; a cast then hits
// S1_CHARGED_HITS times and spends every charge, a cast below that spends one charge.
// - S2 烛燃影息 (MANUAL, data DEFAULT, 28 / 30 s; mode 2): ATK +atk, DEF +def, block +block_cnt, every attack hits all the
// enemies she blocks; each attack picks Attack_Once / Attack_Twice (attack@prob_once / attack@prob_twice: one roll per
// attack), the double hit at attack@atk_scale_twice × ATK carrying vvana_s_2[steal] (ON_BEFORE_TARGET_APPLY_MODIFIER of each
// damage → vvana_steal_attack_speed: ba.steal "减少目标的基础属性作为自身加成,目标减少和自身加成的属性不超过指定上限(同类属性取最高)"):
// each enemy it hits loses attack@steal_atk_speed ASPD (at most attack@steal_atk_speed_max per enemy, the largest of several
// thieves) and she gains as much (at most the max in all) — "持续至技能结束或薇薇安娜离场"; a dodged hit steals too (the step
// comes before the target's checks).
// - S3 “明灭” (MANUAL, data DEFAULT, 15 s): attack interval +base_attack_time s (flat), ATK +atk, DEF +def, RES
// +magic_resistance, S3_HITS hits per attack, 散华 × talent_scale, elite / leader enemies first (targeting priority 'elite',
// after the enemies she blocks — the engine's order). "第二次及以后使用时,攻击距离+2,攻击变为三连击,持续时间延长至25秒" (PRTS
// 备注 "每次部署薇薇安娜时,重新计算该技能的使用次数" / "首次技能结束后,本技能的技能范围永久扩大至3-2"; vvana_s_3[check]: a
// skill triggered before ⇒ the enhance mark and duration_plus = enhance_duration, vvana_s_3: mode 4 instead of 3): from her
// second cast of a deployment the skill runs enhance_duration s with S3_LATER_HITS hits on the 3-2 range (her own grid while
// it runs: the card shows it). The trigger stays the data's DEFAULT on her own range for every cast — the later casts'
// larger running range is no data range (reported, not changed: kits/README.md checklist 5).
import { num, talentBb, traitBb, skillRec, up, batMod } from '../shared/tier1.js';
import { isElite, elementDmg, enemiesIn } from '../shared/tier6.js';
import { hasHp } from '../../../damage.js';
const S1 = 'skchr_vvana_1';
const S2 = 'skchr_vvana_2';
const S3 = 'skchr_vvana_3';
/** range_table 3-2: the 蓄力 range (PRTS S1 备注) and S3's range from its second cast (PRTS S3 备注). */
const RANGE_3_2 = Object.freeze([[0, 0], [0, 1], [0, 2], [0, 3]].map((p) => Object.freeze(p)));
/** S1 "并连续攻击两次" / 蓄力 "改为连续攻击三次"; S2 "二连击"; S3 "攻击变为二连击" / later "攻击变为三连击" — text only. */
const S1_HITS = 2, S1_CHARGED_HITS = 3, S2_TWICE_HITS = 2, S3_HITS = 2, S3_LATER_HITS = 3;
/** AFT-Y: the 法术脆弱 renewal (as 史尔特尔's AFT-Y, kits/ops/chess_char_5_07-surtr.js). */
const FRAGILE_IV = 0.2, FRAGILE_DUR = 0.3;
const AFTD_TAG = 'vvanaAftD';
const CANDLE_KEY = 'talent:vvana:candle';
const SHIELD_KEY = 'talent:vvana:shield';
const LOOT_KEY = 'skill:vvana:loot';
const STOLEN_KEY = 'skill:vvana:stolen';
/** `hit` order: 燃烛施明 re-reads its factor first; the output effects (AFT-D, 散华's roll) come before the target-side
* handlers (priority 0: the enemies' own 护盾, guards); 散华's 护盾 itself last (a guard that cancelled first spends none). */
const CANDLE_PRIORITY = 100, OUTPUT_PRIORITY = 50, SHIELD_PRIORITY = -500;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tagged = (d, tag) => !!d && Array.isArray(d.tags) && d.tags.includes(tag);
/** Her own range grid (the form's 1-1), or the range she switches to. */
function setGrid(battle, unit, grid) {
if (!grid || unit.rangeGrid === grid) return;
unit.rangeGrid = grid;
battle.refreshRange(unit);
}
// ---- S2 偷取: the losses of the enemies, one battle-wide table (同类属性取最高 over several thieves) ---------------------
/** battle → Map(enemy → Map(thief id → ASPD stolen)). */
const STOLEN = new WeakMap();
const stolenOf = (battle) => { let m = STOLEN.get(battle); if (!m) STOLEN.set(battle, (m = new Map())); return m; };
function applyLoss(battle, e, per) {
let a = 0;
for (const v of per.values()) a = Math.max(a, v);
if (!(a > 0)) { battle.removeBuff(e, STOLEN_KEY); return; }
battle.addBuff(e, { key: STOLEN_KEY, mods: { aspd: -a }, tags: ['steal'] });
}
function steal(battle, unit, e, n, cap) {
if (!(n > 0) || !e || !e.alive || e.side !== 'enemy') return;
const losses = stolenOf(battle);
let per = losses.get(e);
if (!per) losses.set(e, (per = new Map()));
per.set(unit.id, Math.min(cap, (per.get(unit.id) ?? 0) + n));
applyLoss(battle, e, per);
(unit.mem.vvanaVictims ??= new Set()).add(e);
unit.mem.vvanaLoot = Math.min(cap, (unit.mem.vvanaLoot ?? 0) + n);
battle.addBuff(unit, { key: LOOT_KEY, mods: { aspd: unit.mem.vvanaLoot }, tags: ['skill'] });
}
/** "持续至技能结束或薇薇安娜离场": she gives everything back. */
function returnLoot(battle, unit) {
battle.removeBuff(unit, LOOT_KEY);
unit.mem.vvanaLoot = 0;
const losses = stolenOf(battle);
for (const e of unit.mem.vvanaVictims ?? []) {
const per = losses.get(e);
if (!per || !per.delete(unit.id)) continue;
if (e.alive) applyLoss(battle, e, per);
}
unit.mem.vvanaVictims = new Set();
}
export default {
char_4098_vvana: (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 twice = num(b2['attack@atk_scale_twice'], 1);
return {
skills: {
[S1]: {
kind: 'charges',
attack: { atkScale: num(b1.atk_scale, 1), hitsFn: (b, u) => (u.mem.vvanaCast === 'charged' ? S1_CHARGED_HITS : S1_HITS) },
onStart({ unit, skill }) {
// after activate's one charge: still ≥ max − 1 ⇒ every charge was stored (蓄力) — a charged cast spends them all
const charged = !!unit.mem.vvanaCharged || (skill.maxCharges > 1 && skill.charges >= skill.maxCharges - 1);
unit.mem.vvanaCast = charged ? 'charged' : 'plain';
if (charged) { skill.charges = 0; skill.sp = 0; }
},
onEnd({ unit }) { unit.mem.vvanaCast = null; },
},
[S2]: {
kind: 'duration',
mods: { atkPct: num(b2.atk), defPct: num(b2.def), blockCnt: num(b2.block_cnt) },
attack: {
hitAllBlocked: true,
hitsFn: (b, u) => (u.mem.vvanaTwice ? S2_TWICE_HITS : 1),
dmgMul: (b, u) => (u.mem.vvanaTwice ? twice : 1),
onEachHit({ battle, unit, target, kind }) {
if (unit.mem.vvanaTwice && kind === 'main') steal(battle, unit, target, num(b2['attack@steal_atk_speed']), num(b2['attack@steal_atk_speed_max'], Infinity));
},
},
onStart({ battle, unit }) { returnLoot(battle, unit); },
onEnd({ battle, unit }) { unit.mem.vvanaTwice = false; returnLoot(battle, unit); },
},
[S3]: {
kind: 'duration',
mods: { batPct: batMod(b3.base_attack_time, chess), atkPct: num(b3.atk), defPct: num(b3.def), resFlat: num(b3.magic_resistance) },
targeting: { priority: 'elite' },
attack: { hitsFn: (b, u) => (u.mem.vvanaS3Later ? S3_LATER_HITS : S3_HITS) },
onStart({ battle, unit, skill }) {
const later = (unit.mem.vvanaS3Casts ?? 0) >= 1;
unit.mem.vvanaS3Casts = (unit.mem.vvanaS3Casts ?? 0) + 1;
unit.mem.vvanaS3Later = later;
if (!later) return;
skill.timeLeft = Math.max(0.01, num(b3.enhance_duration, skill.duration));
setGrid(battle, unit, RANGE_3_2);
},
onEnd({ battle, unit }) {
if (unit.mem.vvanaS3Later) setGrid(battle, unit, unit.def?.rangeGrid);
unit.mem.vvanaS3Later = false;
},
},
},
talents: [
{ install(battle, unit) { // 燃烛施明: arts dealt +, phys / arts taken −, ×super_scale with an elite / leader there
const dsm = num(t0.damage_scale_m), drp = num(t0.damage_resistance_pm), sup = num(t0.super_scale, 1), ep = num(t0.ep_damage_ratio_m);
const factor = () => (enemiesIn(battle, unit, unit.rangeKeys).some(isElite) || unit.blocking.some(isElite) ? sup : 1);
const check = () => {
if (!up(unit)) return;
const f = factor();
if (unit.mem.vvanaCandle === f && unit.findBuff(CANDLE_KEY)) return;
unit.mem.vvanaCandle = f;
battle.addBuff(unit, { key: CANDLE_KEY, mods: { artsDealtMul: 1 + dsm * f, physTakenMul: 1 - drp * f, artsTakenMul: 1 - drp * f }, data: { f }, tags: ['talent'] });
};
battle.on('tick', check, { owner: unit });
battle.on('battleStart', check, { owner: unit });
battle.on('deploy', (c) => { if (c.unit === unit) { unit.mem.vvanaCandle = null; check(); } }, { owner: unit });
// exact at the damage: re-read before any hit she deals or takes is resolved (the pipeline reads both units'
// stats after the `hit` hook)
battle.on('hit', (c) => { if (c.source === unit || c.target === unit) check(); }, { owner: unit, priority: CANDLE_PRIORITY });
if (!(ep > 0)) return;
// AFT-D stage 3: 攻击附带相当于9%伤害的灼燃损伤 (× the same factor) — her arts damage that removed HP
battle.on('damaged', (c) => {
const d = c.dmg;
if (c.source !== unit || c.type !== 'arts' || tagged(d, 'dot') || !(c.amount > 0) || !c.target || c.target.side !== 'enemy') return;
elementDmg(battle, unit, c.target, 'burn', c.amount * ep * (unit.mem.vvanaCandle ?? 1), ['talent']);
}, { owner: unit });
} },
{ install(battle, unit) { // 散华: a melee-only 护盾 layer from her damage on elite / leader enemies
const p = num(t1.prob), max = Math.max(1, Math.floor(num(t1.max_stack_cnt, 1)));
const s3Scale = num(b3.talent_scale, 1);
const layers = () => unit.findBuff(SHIELD_KEY)?.data?.n ?? 0;
const setLayers = (n) => {
if (n > 0) battle.addBuff(unit, { key: SHIELD_KEY, data: { n }, visible: true, tags: ['talent'] });
else battle.removeBuff(unit, SHIELD_KEY);
};
battle.on('hit', (c) => {
if (c.source !== unit || !isElite(c.target) || !up(unit)) return;
const have = layers();
if (have >= max) return;
const prob = p * (unit.skill?.active && unit.skill.id === S3 ? s3Scale : 1);
if (!(prob > 0) || !battle.rng.chance(prob)) return;
setLayers(have + 1);
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit, priority: OUTPUT_PRIORITY });
battle.on('hit', (c) => {
if (c.target !== unit || c.dmg.cancel) return;
const s = c.source;
if (!s || s.side !== 'enemy' || c.dmg.isProjectile) return;
const have = layers();
if (!(have > 0)) return;
c.dmg.cancel = true;
setLayers(have - 1);
battle.fx('shieldBreak', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit, priority: SHIELD_PRIORITY });
} },
],
install(battle, unit) {
const own = unit.def?.rangeGrid;
// S1's 蓄力 mode: both charges stored ⇒ her range is 3-2 until the next end of the skill
if (unit.skill?.id === S1) {
const leave = () => { unit.mem.vvanaCharged = false; setGrid(battle, unit, own); };
battle.on('tick', () => {
const sk = unit.skill;
if (!up(unit) || unit.mem.vvanaCharged || !sk || !(sk.maxCharges > 1) || sk.charges < sk.maxCharges) return;
unit.mem.vvanaCharged = true;
setGrid(battle, unit, RANGE_3_2);
}, { owner: unit });
battle.on('skillEnd', (c) => { if (c.unit === unit && c.skill.id === S1 && unit.mem.vvanaCharged) leave(); }, { owner: unit });
battle.on('deploy', (c) => { if (c.unit === unit) leave(); }, { owner: unit });
}
// S2: one Attack_Once / Attack_Twice pick per attack while it runs
if (unit.skill?.id === S2) {
const prob = num(b2['attack@prob_twice']);
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit) return;
unit.mem.vvanaTwice = !!(unit.skill?.active && unit.skill.id === S2) && prob > 0 && battle.rng.chance(prob);
}, { owner: unit });
battle.on('death', (c) => { if (c.unit === unit) returnLoot(battle, unit); }, { owner: unit });
}
// S3: the cast count of a deployment ("每次部署…重新计算"), her own range back
if (unit.skill?.id === S3) {
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
unit.mem.vvanaS3Casts = 0;
unit.mem.vvanaS3Later = false;
setGrid(battle, unit, own);
}, { owner: unit });
}
// AFT-D: 元素伤害 at the output of every damage of hers on an enemy in its 灼燃 爆发冷却 (not on this rider's own)
const er = num(tb.ep_damage_ratio);
if (er > 0) {
battle.on('hit', (c) => {
const t = c.target;
if (c.source !== unit || !t || t.side !== 'enemy' || tagged(c.dmg, AFTD_TAG) || !hasHp(t) || !t.findBuff('burnBurst')) return;
battle.dealDamage(unit, t, { amount: unit.s.atk * er, type: 'elemental', element: 'burn', canDodge: false, tags: [AFTD_TAG, 'dot'] });
}, { owner: unit, priority: OUTPUT_PRIORITY });
}
// AFT-Y: 自身阻挡的敌人受到10%的法术脆弱效果
const fragile = num(tb.damage_scale, 1) - 1;
if (fragile > 0) {
battle.every(FRAGILE_IV, () => {
if (!up(unit)) return;
for (const e of unit.blocking) if (e.alive) battle.applyStatus(e, 'artsFragile', { duration: FRAGILE_DUR, value: fragile, source: unit });
}, { owner: unit });
}
},
};
},
};