Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-vvana.js
T
sganggs 1559614e8a feedback5 WE2: larger-range triggers the data cannot carry — 谬因 S2's beam path, 薇薇安娜 S3's 3-2 from its second cast (#16)
The owner's ACTIVE_RANGE rule (a MANUAL skill whose running range strictly contains the
operator's own) for two skills whose running range is no data range (O1, O11 reports):
op-aphris.js adds the beam's path as a content trigger range; op-vvana.js switches S3 to
ACTIVE_RANGE on 3-2 once its first cast of a deployment ended (DEFAULT again on redeploy)
through a new additive SkillRuntime.setTrigger(rule, grid). The scan of the wave 2-3
reports and the 41 candidate skills is in the doc draft (others: target-centred, already
fixed, or owner questions). Golden: diy-045 (谬因 S2 fires for enemies beyond her 6 tiles)
and diy-078 (薇薇安娜 S3: skill timing in hooks / snapshots only) moved.
2026-10-06 16:01:48 +08:00

289 lines
20 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-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). Trigger: the data's DEFAULT on her own range for the first cast of a deployment; once it
// has ended the owner's ACTIVE_RANGE rule (2026-10-05: a MANUAL skill whose running range strictly contains her own casts
// with an enemy inside that range) on the 3-2 — no data range carries it, so the kit sets it (skill.setTrigger, 0.2.0 WE2,
// follow-up #16; O11's report) — and DEFAULT again at her next deployment.
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, skill }) {
if (unit.mem.vvanaS3Later) setGrid(battle, unit, unit.def?.rangeGrid);
unit.mem.vvanaS3Later = false;
// its later casts run on the 3-2: from now the owner's ACTIVE_RANGE rule there (see the header)
if ((unit.mem.vvanaS3Casts ?? 0) >= 1) skill.setTrigger('ACTIVE_RANGE', RANGE_3_2);
},
},
},
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);
if (unit.skill?.id === S3) unit.skill.setTrigger('DEFAULT', null); // the first cast of a deployment: the data's rule
}, { 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 });
}
},
};
},
};