feedback5: merge O2 — 自选 kits for 艾雅法拉, 刻俄柏 and 霍尔海雅; OPERATOR_KIT_FILES rebuilt as one list; golden: only diy moved
This commit is contained in:
commit
67d0993fa0
8 files changed
+2009
-119
No files matched your search
@@ -78,13 +78,17 @@ export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sh
|
||||
* The 自选 operator kit files (`ops/op-<codename>.js`, registry key = the operator's charId, an owned-6★ pick of
|
||||
* data/backups.json; a new file is appended). An operator without a file is not offered as a 自选 pick (KITTED_CHARS).
|
||||
*/
|
||||
export const OPERATOR_KIT_FILES = Object.freeze(['op-siege.js',
|
||||
export const OPERATOR_KIT_FILES = Object.freeze([
|
||||
'op-siege.js',
|
||||
'op-chen.js',
|
||||
'op-irene.js',
|
||||
'op-helage.js',
|
||||
'op-shining.js',
|
||||
'op-cgbird.js',
|
||||
'op-saga.js',
|
||||
'op-amgoat.js',
|
||||
'op-cerber.js',
|
||||
'op-heyak.js',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
// server/sim/content/kits/ops/op-amgoat.js — 艾雅法拉 (char_180_amgoat) 自选 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_180_amgoat, 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; every module part is
|
||||
// valid outside 集成战略 — no `validInGameTag`) and PRTS 艾雅法拉 (S1 备注 "每次部署艾雅法拉时,重新计算该技能的使用次数";
|
||||
// S2 备注 "爆炸范围为半径1.5", "技能实际效果为先削减主攻击目标的法术抗性,再对爆炸范围内所有敌人造成½倍率的伤害(包括主攻击目标),
|
||||
// 最后对主攻击目标额外造成一次½倍率的伤害", 法术抗性 "{{**|80%|-20%}}" = a multiplier; S3 "攻击间隔大幅度缩短(-1.1)";
|
||||
// 乱火's hidden 备注 "在最小值与最大值之间随机生成一个浮点数后向下取整").
|
||||
// - Trait (中坚术师) "攻击造成法术伤害": the profession default (ranged arts, hits air, block 1). Module CCR-X “错过的声音”
|
||||
// adds "无视目标10点法术抗性" (trait bb magic_resist_penetrate_fixed: a permanent resIgnoreFlat), CCR-Y “宠物大赛第一名”
|
||||
// "普通攻击命中精英或领袖敌人时获得1点技力" (trait bb sp: per hit of one of her attacks on an elite / leader — the S2
|
||||
// explosion's splash and its second ½ are not further 命中 [ASSUMED]; a running timed skill takes no SP, as always).
|
||||
// - T1 炎息 "在场时,所有友方【术师】职业干员的攻击力+14%": an aura on every 【术师】 operator of the field (her included) while
|
||||
// she is deployed (installAura, the kits' convention for field-wide talents; several sources keep the strongest). CCR-X
|
||||
// stage 2+ turns it into "携带时…+22%" (the module talent's bb): the same aura whether she is on the field or not — she
|
||||
// is carried while she belongs to the battle, knocked out or waiting to redeploy included.
|
||||
// - T2 乱火 "部署后立即随机获得7~15点技力" (bb sp_min 7, sp_max 16): floor(uniform [sp_min, sp_max)) SP at every deployment
|
||||
// (PRTS hidden 备注). CCR-Y stage 3 adds "并随机提升6~15的攻击速度" (attack_speed_min / _max, the same draw) for that
|
||||
// deployment [ASSUMED: no duration given — until she leaves the field], and with `factor` "部署后范围内存在精英或领袖敌人时,
|
||||
// 获得的技力和提升的攻击速度取最大值": an elite or leader enemy in her attack range at that deployment gives the text's
|
||||
// maxima, 15 SP / 15 ASPD — the blackboard maxima are the exclusive bounds of the draw (stage 2's attack_speed_max 1
|
||||
// with no ASPD in its text says so) [ASSUMED: 取最大值 = the largest value the draw yields; checked at the deployment
|
||||
// instant — at the battle start no enemy is on the field yet].
|
||||
// - S1 二重咏唱 (MANUAL, 25 s, data DEFAULT): ASPD +[a].attack_speed; from the second cast of a deployment on, ASPD
|
||||
// +[b].attack_speed and ATK +[b].atk (PRTS 备注: the count restarts at every deployment).
|
||||
// - S2 点燃 (AUTO, 可充能 2, data DEFAULT): the next attack, on impact — the main target's RES ×(1 + magic_resistance) for
|
||||
// `duration` s first, then an explosion of radius 1.5 around it (a 中点判定 splash) dealing atk_scale × ATK arts to
|
||||
// every enemy in it, the main target included, each of the others then getting the same RES cut, and last
|
||||
// atk_scale_2 × ATK arts to the main target again (the PRTS 备注's order; it names the main target's cut only as
|
||||
// coming first, so the others' cut follows their hit [ASSUMED]; the cut is "同名效果取最高" through applyStrongest).
|
||||
// The explosion reaches air units and skips enemies an area effect cannot select (foesInRadius); like the engine's
|
||||
// profession splash it can be dodged.
|
||||
// - S3 火山 (MANUAL, 15 s): ATK +atk, attack interval base_attack_time (−1.1 s flat on her 1.6 s), the attack range x-3
|
||||
// while it runs, each attack throws lava at attack@max_target enemies of that range drawn at random (all of them when
|
||||
// fewer) — one projectile each, no splash [ASSUMED: the text names none]. Trigger: the data's SKILL_RANGE on
|
||||
// x-3 — its text says "攻击范围增大", which tools/build-data.mjs ATTACK_RANGE_CHANGE does not list, so the owner's
|
||||
// ACTIVE_RANGE rule did not reach it (reported; the kit keeps the data rule).
|
||||
// - No summon (the forms list no token).
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, installAura, batMod, giveSp, onHitBy } from '../shared/tier1.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { canTargetEnemy } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
|
||||
const S1 = 'skchr_amgoat_1';
|
||||
const S2 = 'skchr_amgoat_2';
|
||||
const S3 = 'skchr_amgoat_3';
|
||||
/** S2 点燃: the explosion radius (PRTS 备注 "爆炸范围为半径1.5"; no blackboard key). */
|
||||
const IGNITE_RADIUS = 1.5;
|
||||
/** S3 火山's 技能范围 x-3 when the data carries no grid (range_table x-3: the tiles within 3 steps). */
|
||||
const X3 = Object.freeze([[3, 0], [2, -1], [2, 0], [2, 1], [1, -2], [1, -1], [1, 0], [1, 1], [1, 2], [0, -3], [0, -2], [0, -1],
|
||||
[0, 0], [0, 1], [0, 2], [0, 3], [-1, -2], [-1, -1], [-1, 0], [-1, 1], [-1, 2], [-2, -1], [-2, 0], [-2, 1], [-3, 0]]);
|
||||
/** Damage of the S2 explosion and its second ½ (not a further 命中 of the attack: CCR-Y counts the first). */
|
||||
const IGNITE_TAG = 'amgoat:ignite';
|
||||
const AURA_KEY = 'talent:amgoat:casters';
|
||||
const AURA_EVERY = 0.5;
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** A 【术师】 operator (summons are never operators). */
|
||||
const isCaster = (a) => !!a && a.kind === 'op' && a.def?.profession === 'CASTER';
|
||||
/** An elite or leader enemy (精英或领袖敌人). */
|
||||
const isEliteEnemy = (e) => !!e && e.side === 'enemy' && (e.isBoss || e.def?.rank === 'ELITE' || e.def?.rank === 'BOSS');
|
||||
const tagged = (d, tag) => !!(d && d.tags && d.tags.includes(tag));
|
||||
/** A module trait that works 在集成战略中 only (ISW modules): nothing of it applies in this mode. */
|
||||
const roguelikeOnly = (chess) => /集成战略/.test(String(chess?.trait?.moduleDesc ?? ''));
|
||||
/** Named talent `i` of the record (its desc tells the module stage's wording). */
|
||||
const talentRec = (chess, i) => (chess?.talents ?? []).filter((t) => t && t.index !== -1)[i] ?? null;
|
||||
|
||||
/** "携带时": the aura of installAura, held while she belongs to the battle — on the field or not. */
|
||||
function carriedAura(battle, unit, { key, select, mods, value }) {
|
||||
battle.every(AURA_EVERY, () => {
|
||||
for (const a of battle.alliesFor(unit)) {
|
||||
if (!select(a)) continue;
|
||||
const cur = a.findBuff(key);
|
||||
if (cur && cur.source !== unit && (cur.data?.v ?? 0) > value && cur.timeLeft > 0.05) continue;
|
||||
battle.addBuff(a, { key, duration: AURA_EVERY + 0.1, mods, source: unit, data: { v: value }, tags: ['aura'] });
|
||||
}
|
||||
}, { owner: unit, immediate: true });
|
||||
}
|
||||
|
||||
export default {
|
||||
char_180_amgoat: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const carried = /携带时/.test(String(talentRec(chess, 0)?.desc ?? ''));
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
|
||||
const mr = num(b2.magic_resistance), resDur = num(b2.duration);
|
||||
const resCut = (battle, unit, e) => {
|
||||
if (e && e.alive && mr) battle.applyStrongest(e, 'amgoat:ignite', { duration: resDur, value: mr, mods: (v) => ({ resMul: Math.max(0, 1 + v) }), source: unit });
|
||||
};
|
||||
const maxLava = Math.max(1, Math.floor(num(b3['attack@max_target'], 1)));
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'duration',
|
||||
onStart({ battle, unit }) {
|
||||
// the casts of this deployment (PRTS 备注: "每次部署…重新计算该技能的使用次数")
|
||||
const n = unit.mem.amgoatDuetSeq === unit.deploySeq ? num(unit.mem.amgoatDuetN) + 1 : 1;
|
||||
unit.mem.amgoatDuetSeq = unit.deploySeq;
|
||||
unit.mem.amgoatDuetN = n;
|
||||
const mods = n >= 2
|
||||
? { aspd: num(b1['amgoat_s_1[b].attack_speed']), atkPct: num(b1['amgoat_s_1[b].atk']) }
|
||||
: { aspd: num(b1['amgoat_s_1[a].attack_speed']) };
|
||||
battle.addBuff(unit, { key: 'skill:amgoat:duet', mods, tags: ['skill'] });
|
||||
},
|
||||
onEnd({ battle, unit }) { battle.removeBuff(unit, 'skill:amgoat:duet'); },
|
||||
},
|
||||
[S2]: {
|
||||
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||||
attack: {
|
||||
// the engine's hit on the main target is its share of the explosion (½); the RES cut came first (install)
|
||||
atkScale: num(b2.atk_scale, 1),
|
||||
onEachHit({ battle, unit, target, kind }) {
|
||||
if (kind !== 'main' || !target) return;
|
||||
const atk = unit.s.atk, x = target.x, y = target.y;
|
||||
for (const e of battle.foesInRadius(x, y, IGNITE_RADIUS, true)) {
|
||||
if (e === target) continue;
|
||||
battle.dealDamage(unit, e, { amount: atk * num(b2.atk_scale, 1), type: 'arts', isAttack: true, isSplash: true, isSkill: true, tags: [IGNITE_TAG] });
|
||||
resCut(battle, unit, e);
|
||||
}
|
||||
if (hasHp(target)) battle.dealDamage(unit, target, { amount: atk * num(b2.atk_scale_2, num(b2.atk_scale, 1)), type: 'arts', isAttack: true, isSkill: true, tags: [IGNITE_TAG] });
|
||||
battle.fx('explosion', { x, y, id: unit.id, r: IGNITE_RADIUS, dmgType: 'arts' });
|
||||
},
|
||||
},
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
|
||||
// every enemy of x-3 is a candidate; install draws the lava targets among them
|
||||
targeting: { rangeGrid: s3?.rangeGrid ?? X3, allInRange: true },
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 炎息: the field's 【术师】 ATK +atk — while she is deployed, or (CCR-X stage 2+) carried
|
||||
const a = num(t0.atk);
|
||||
if (!(a > 0)) return;
|
||||
const opts = { key: AURA_KEY, value: a, select: isCaster, mods: { atkPct: a } };
|
||||
if (carried) carriedAura(battle, unit, opts);
|
||||
else installAura(battle, unit, { ...opts, interval: AURA_EVERY });
|
||||
} },
|
||||
{ install(battle, unit) { // 乱火: SP (and with CCR-Y stage 3 ASPD) at every deployment; the maxima with an elite in range
|
||||
const spLo = num(t1.sp_min), spHi = num(t1.sp_max), asLo = num(t1.attack_speed_min), asHi = num(t1.attack_speed_max);
|
||||
const maxRule = num(t1.factor) > 0;
|
||||
battle.on('deploy', (ctx) => {
|
||||
if (ctx.unit !== unit || !unit.alive) return;
|
||||
// (a scan of the enemies: the tile index of enemiesInKeys is the last tick's)
|
||||
const top = maxRule && battle.enemies.some((e) => isEliteEnemy(e) && canTargetEnemy(unit, e, unit.profile) && bodyInKeys(e, unit.rangeKeySet));
|
||||
// floor of a uniform draw in [lo, hi); "取最大值" = the largest value the draw yields (hi − 1 for integer bounds)
|
||||
const draw = (lo, hi) => (hi > lo ? (top ? Math.ceil(hi) - 1 : Math.floor(battle.rng.range(lo, hi))) : 0);
|
||||
const sp = draw(spLo, spHi);
|
||||
if (sp > 0) giveSp(unit, sp);
|
||||
const as = draw(asLo, asHi);
|
||||
if (as > 0) battle.addBuff(unit, { key: 'talent:amgoat:wildfire', mods: { aspd: as }, tags: ['talent'] });
|
||||
}, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// module traits (none on the normal form): CCR-X 无视目标10点法术抗性, CCR-Y SP per hit on an elite / leader
|
||||
if (!roguelikeOnly(chess)) {
|
||||
const tb = traitBb(chess);
|
||||
const pen = num(tb.magic_resist_penetrate_fixed);
|
||||
if (pen > 0) statBuff(battle, unit, 'trait:amgoat:resIgnore', { resIgnoreFlat: pen });
|
||||
const sp = num(tb.sp);
|
||||
if (sp > 0) {
|
||||
battle.on('damaged', (ctx) => {
|
||||
const d = ctx.dmg;
|
||||
if (ctx.source !== unit || !d || !d.isAttack || d.isSplash || tagged(d, IGNITE_TAG) || !isEliteEnemy(ctx.target)) return;
|
||||
giveSp(unit, sp, 'trait');
|
||||
}, { owner: unit });
|
||||
}
|
||||
}
|
||||
const sid = unit.skill?.id;
|
||||
if (sid === S2) {
|
||||
// 点燃: "先削减主攻击目标的法术抗性" — the main target's RES cut lands before the first damage of the attack
|
||||
onHitBy(battle, unit, (ctx) => {
|
||||
const d = ctx.dmg;
|
||||
if (d.isAttack && d.isSkill && !d.isSplash && !tagged(d, IGNITE_TAG)) resCut(battle, unit, ctx.target);
|
||||
}, 50);
|
||||
} else if (sid === S3) {
|
||||
// 火山: "随机对攻击范围内至多N个敌人发射熔岩" — a random draw among every enemy of the range
|
||||
battle.on('beforeAttack', (ctx) => {
|
||||
if (ctx.attacker !== unit || !unit.skill?.active || ctx.targets.length <= maxLava) return;
|
||||
ctx.targets = battle.rng.shuffle(ctx.targets.slice()).slice(0, maxLava);
|
||||
}, { owner: unit });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
// server/sim/content/kits/ops/op-cerber.js — 刻俄柏 (char_2013_cerber) 自选 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_2013_cerber, 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; every module part is
|
||||
// valid outside 集成战略 — no `validInGameTag`) and PRTS 刻俄柏 (S2 "攻击间隔缩短(*0.6)" / "较大幅度缩短(*0.4)": a ratio of the
|
||||
// interval; 剥壳 备注 "处理敌人防御力时不考虑物理穿透", "<X模组>每次攻击与上一次攻击相同的目标时,将提升5%的比例值(第五次后达到
|
||||
// 最大值,于剥壳伤害前处理);攻击不同目标时比例值立刻重置"; 独行长路 能力生效范围 x-5 and 备注 "天赋判定范围包括刻俄柏所在格,不判定
|
||||
// 目标的可选性、实体类型等;天赋生效期间检测到敌人类我方单位时不会主动失效,但失效期间检测到敌人类我方单位后,直到该敌人类我方单位退场前
|
||||
// 无法重新生效", "<Y模组>本天赋不再通过原本的判定方式生效/失效…部署时检测到敌人类我方单位也会失效").
|
||||
// - Trait (中坚术师) "攻击造成法术伤害": the profession default (ranged arts, hits air, block 1). Module CCR-X “很干的面包”
|
||||
// adds "无视目标10点法术抗性" (trait bb magic_resist_penetrate_fixed: a permanent resIgnoreFlat), CCR-Y “我打的刀”
|
||||
// "普通攻击命中精英或领袖敌人时获得1点技力" (trait bb sp: per hit of one of her attacks on an elite / leader; a running timed
|
||||
// skill takes no SP, as always).
|
||||
// - T1 剥壳 "攻击时对目标额外造成相当于其防御力40%的法术伤害": each hit of her attacks (S3's physical spear too) is followed by
|
||||
// atk_scale × the target's DEF (its current DEF, her own penetration ignored — she has none) as arts damage, a damage
|
||||
// instance of the talent (not an attack: no further on-hit effects). A target with no DEF takes none (PRTS: she "tries"
|
||||
// and deals nothing; no flat arts bonus exists in this mode that a zero hit could carry). CCR-X stage 2+ (bb
|
||||
// basic_atk_scale / delta_atk_scale / max_atk_scale): 50 % +5 % per consecutive hit on the same target, ≤ 75 %, back to
|
||||
// 50 % on another target (PRTS 备注), counted from the start of each deployment [ASSUMED].
|
||||
// - T2 独行长路 "当周围四格内没有其他友方单位时,攻击力+8%,攻击速度+8": while no other ally unit (operator, summon or device —
|
||||
// PRTS: no selectability / entity check) stands on her tile or the four next to it (x-5), with the 敌人类我方单位 rule
|
||||
// of the 备注 (炎佑: an ally built from an enemy record; it does not switch the talent off, but one seen while it is off
|
||||
// keeps it off until that unit leaves the field). CCR-Y stage 2+ ("部署时若周围四格内没有其他友方单位,攻击力+15%,攻击速度
|
||||
// +15"): checked once per deployment — an ally of either kind there switches it off for that deployment; the battle's
|
||||
// initial deployment counts as one moment, every board piece already placed [ASSUMED: the pieces of the mode are on the
|
||||
// board together before the battle starts; the official deploys one at a time].
|
||||
// - S1 “很冰的斧” (AUTO, 可充能 1 / 2, data DEFAULT): the next attack at atk_scale × ATK, on an enemy that nobody blocks first
|
||||
// (the usual order among those, then the blocked ones), binding (束缚) it for `duration` s.
|
||||
// - S2 “很热的刀” (MANUAL, 33 / 36 s, data DEFAULT): attack interval × base_attack_time (0.6 / 0.4: PRTS "(*0.4)"), the
|
||||
// highest-DEF enemy first.
|
||||
// - S3 “很重的枪” (MANUAL, 56 / 57 s, data ACTIVE_RANGE on 3-3): attack range 3-3 while it runs, ATK +atk, physical damage,
|
||||
// the lowest-DEF enemy first, each hit silences (失去特殊能力) for attack@silence s.
|
||||
// - No summon (the forms list no token).
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, batMod, giveSp, up } from '../shared/tier1.js';
|
||||
import { sortEnemyTargets } from '../../../targeting.js';
|
||||
import { offsetTile } from '../../../dir.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
|
||||
const S1 = 'skchr_cerber_1';
|
||||
const S2 = 'skchr_cerber_2';
|
||||
const S3 = 'skchr_cerber_3';
|
||||
/** 独行长路's area: her tile and the four next to it (PRTS 能力生效范围 x-5; the data carries no talent grid). */
|
||||
const X5 = Object.freeze([[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]);
|
||||
/** S3's 3-3 when the data carries no grid. */
|
||||
const R33 = 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]]);
|
||||
/** 剥壳's extra damage (a talent instance: never itself followed by 剥壳). */
|
||||
const SHELL_TAG = 'cerber:shell';
|
||||
const LONE_KEY = 'talent:cerber:lone';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** An elite or leader enemy (精英或领袖敌人). */
|
||||
const isEliteEnemy = (e) => !!e && e.side === 'enemy' && (e.isBoss || e.def?.rank === 'ELITE' || e.def?.rank === 'BOSS');
|
||||
const tagged = (d, tag) => !!(d && d.tags && d.tags.includes(tag));
|
||||
/** A module trait that works 在集成战略中 only (ISW modules): nothing of it applies in this mode. */
|
||||
const roguelikeOnly = (chess) => /集成战略/.test(String(chess?.trait?.moduleDesc ?? ''));
|
||||
/** Named talent `i` of the record (its desc tells the module stage's wording). */
|
||||
const talentRec = (chess, i) => (chess?.talents ?? []).filter((t) => t && t.index !== -1)[i] ?? null;
|
||||
/** PRTS's 敌人类我方单位: an ally built from an enemy record (炎佑 enemy_9012_acloon, content/items/battle.js). */
|
||||
const enemyLike = (a) => /^enemy_/.test(String(a?.defId ?? a?.def?.id ?? ''));
|
||||
|
||||
/** The other ally units on her x-5 (any kind — PRTS: "不判定目标的可选性、实体类型等"). */
|
||||
function othersAround(battle, unit) {
|
||||
const out = [];
|
||||
for (const [dr, dc] of X5) {
|
||||
const [r, c] = offsetTile(unit.tileR, unit.tileC, dr, dc, unit.dir);
|
||||
const o = battle.grid.inBounds(r, c) ? battle.unitAt(r, c) : null;
|
||||
if (o && o !== unit && o.side === 'ally' && !out.includes(o)) out.push(o);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default {
|
||||
char_2013_cerber: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const onDeploy = /部署时/.test(String(talentRec(chess, 1)?.desc ?? '')); // CCR-Y stage 2+
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s1 = skillRec(chess, S1), s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||||
attack: { atkScale: num(b1.atk_scale, 1), onHitStatus: { key: 'bind', duration: num(b1.duration) } },
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
mods: { batPct: batMod(b2.base_attack_time, chess, s2?.desc) },
|
||||
targeting: { priority: 'highDef' },
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b3.atk) },
|
||||
targeting: { rangeGrid: s3?.rangeGrid ?? R33, priority: 'lowDef' },
|
||||
attack: { dmgType: 'phys', onHitStatus: { key: 'silence', duration: num(b3['attack@silence']) } },
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 剥壳: DEF × ratio arts after each hit; CCR-X stage 2+: the ratio climbs on the same target
|
||||
const base = num(t0.basic_atk_scale, num(t0.atk_scale)), delta = num(t0.delta_atk_scale), top = num(t0.max_atk_scale, base);
|
||||
if (!(base > 0)) return;
|
||||
battle.on('deploy', (ctx) => { if (ctx.unit === unit) { unit.mem.cerberShellTarget = null; unit.mem.cerberShellScale = base; } }, { owner: unit });
|
||||
battle.on('damaged', (ctx) => {
|
||||
const t = ctx.target, d = ctx.dmg;
|
||||
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d || !d.isAttack || d.isSplash || tagged(d, SHELL_TAG)) return;
|
||||
let sc = base;
|
||||
if (delta > 0) {
|
||||
// "于剥壳伤害前处理": the same target as her previous hit +delta (≤ max), another one back to the base
|
||||
sc = unit.mem.cerberShellTarget === t ? Math.min(top, num(unit.mem.cerberShellScale, base) + delta) : base;
|
||||
unit.mem.cerberShellTarget = t;
|
||||
unit.mem.cerberShellScale = sc;
|
||||
}
|
||||
const def = t.s.def;
|
||||
if (def > 0 && hasHp(t)) battle.dealDamage(unit, t, { amount: def * sc, type: 'arts', tags: [SHELL_TAG] });
|
||||
}, { owner: unit });
|
||||
} },
|
||||
{ install(battle, unit) { // 独行长路
|
||||
const mods = { atkPct: num(t1.atk), aspd: num(t1.attack_speed) };
|
||||
if (!mods.atkPct && !mods.aspd) return;
|
||||
if (onDeploy) {
|
||||
// CCR-Y stage 2+: the deployment's check, for the whole deployment
|
||||
const check = () => {
|
||||
if (!up(unit)) return;
|
||||
if (othersAround(battle, unit).length) battle.removeBuff(unit, LONE_KEY);
|
||||
else battle.addBuff(unit, { key: LONE_KEY, mods, tags: ['talent'] });
|
||||
};
|
||||
battle.on('battleStart', check, { owner: unit });
|
||||
battle.on('deploy', (ctx) => { if (ctx.unit === unit && battle.started) check(); }, { owner: unit });
|
||||
return;
|
||||
}
|
||||
// the base talent, checked every tick: an ordinary ally switches it off; an enemy-like one seen while it is off
|
||||
// keeps it off until that unit leaves the field
|
||||
let on = false;
|
||||
const locks = new Set();
|
||||
battle.on('tick', () => {
|
||||
if (!up(unit)) { on = false; locks.clear(); return; }
|
||||
const near = othersAround(battle, unit);
|
||||
if (on && near.some((a) => !enemyLike(a))) on = false;
|
||||
if (!on) {
|
||||
for (const a of near) if (enemyLike(a)) locks.add(a);
|
||||
for (const a of locks) if (!a.alive || !a.deployed) locks.delete(a);
|
||||
if (!near.length && !locks.size) on = true;
|
||||
}
|
||||
}, { owner: unit, priority: 1 });
|
||||
toggleBuff(battle, unit, LONE_KEY, () => on, mods);
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// module traits (none on the normal form): CCR-X 无视目标10点法术抗性, CCR-Y SP per hit on an elite / leader
|
||||
if (!roguelikeOnly(chess)) {
|
||||
const tb = traitBb(chess);
|
||||
const pen = num(tb.magic_resist_penetrate_fixed);
|
||||
if (pen > 0) statBuff(battle, unit, 'trait:cerber:resIgnore', { resIgnoreFlat: pen });
|
||||
const sp = num(tb.sp);
|
||||
if (sp > 0) {
|
||||
battle.on('damaged', (ctx) => {
|
||||
const d = ctx.dmg;
|
||||
if (ctx.source !== unit || !d || !d.isAttack || d.isSplash || !isEliteEnemy(ctx.target)) return;
|
||||
giveSp(unit, sp, 'trait');
|
||||
}, { owner: unit });
|
||||
}
|
||||
}
|
||||
if (unit.skill?.id === S1) {
|
||||
// “很冰的斧” "优先攻击没有被阻挡的目标": the pending attack re-picks its target, unblocked enemies first
|
||||
battle.on('beforeAttack', (ctx) => {
|
||||
if (ctx.attacker !== unit || !ctx.isSkill || !ctx.targets.length) return;
|
||||
const prof = ctx.profile;
|
||||
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, prof);
|
||||
for (const e of battle.blockedTargets(unit, prof)) if (!cands.includes(e)) cands.push(e);
|
||||
if (!cands.length) return;
|
||||
sortEnemyTargets(battle, unit, cands, prof.priority);
|
||||
ctx.targets = [cands.find((e) => !e.blockedBy) ?? cands[0]];
|
||||
}, { owner: unit });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
// server/sim/content/kits/ops/op-heyak.js — 霍尔海雅 (char_4027_heyak) 自选 operator kit: 6★ 中坚术师 (术师), an owned-6★
|
||||
// pick of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and the three modules at every form.
|
||||
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_4027_heyak, 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) and PRTS 霍尔海雅
|
||||
// (传承终焉 备注 "技能触发的浮空效果于本天赋结算前进行结算"; S1 备注 "“仅攻击到1名目标”仅判定实际发射的弹道数"; S3 备注 "攻击以自身格
|
||||
// 中心处为准,于此处、左1.0、右1.0格在0~0.3s内随机延迟后生成三股旋风", "移动速度1.0,碰撞范围为1×1的正方形,旋风移动至5格远处中心点
|
||||
// 自然消失,造成伤害时实时应用霍尔海雅的当前攻击力", "旋风在生成后1.77s内,攻击倍率由最低值线性递增至最大值", "霍尔海雅离场时已存在的旋风
|
||||
// 不会因此立刻消失,但其命中敌人时不享受模组加成,不触发第一天赋").
|
||||
// - Trait (中坚术师) "攻击造成法术伤害": the profession default (ranged arts, hits air, block 1). Module CCR-X “图书馆” adds
|
||||
// "无视目标10点法术抗性" (trait bb magic_resist_penetrate_fixed: a permanent resIgnoreFlat), CCR-Y “羽蛇的时间博物馆”
|
||||
// "普通攻击命中精英或领袖敌人时获得1点技力" (trait bb sp: per hit of one of her attacks on an elite / leader; a running timed
|
||||
// skill takes no SP). ISW-A “霍尔海雅特限证章”: its trait "在集成战略中,无视目标10点法术抗性,每个敌人首次进入目标点所在地块时,恢复
|
||||
// 自身30%的技力" and its hidden talents (首次部署 1 点临时目标生命值, the goal-tile 浮空 15 s, stage 3's radius-1.1 skill
|
||||
// splash "自身技能会对敌人及其周围的目标同时造成浮空效果和伤害效果") are battle_equip_table parts with `validInGameTag:
|
||||
// roguelike` — N/A here; what applies is its stats and, at stage 2+, 传承终焉's new numbers (a TALENT_DATA_ONLY part with
|
||||
// no game tag: 145 % / 5 s at stage 3).
|
||||
// - T1 传承终焉 "攻击空中目标时攻击力提升至120%,并使其特殊能力失效3秒": every hit of her attacks on an air unit (flying, 近地悬浮
|
||||
// or 浮空) at the hit is × atk_scale and silences it for `silence` s. Her skills' 浮空 of that very hit resolve first (PRTS
|
||||
// 备注), so a target her S1 / S2 / S3 lifts takes the ×atk_scale of that hit: the S1 / S2 lifts land in the hit's `hit`
|
||||
// step, ahead of this talent (a dodged hit has lifted already [ASSUMED]). CCR-X stage 3: 135 % / 5 s, ISW-A stage 3:
|
||||
// 145 % / 5 s (the talent's merged bb).
|
||||
// - T2 曾有羽翼 "攻击范围内所有生命值高于80%的敌人失重": every enemy of her current attack range (a running S3's included) above
|
||||
// hp_ratio of its HP is 失重 (weight −1 level, 同名效果不可叠加: none of hers while another source's 失重 is on it), off
|
||||
// again at once below it or outside the range. CCR-Y stage 3 lowers it to 50 % and adds "生命值低于50%的敌人获得20%的
|
||||
// 【法术脆弱】" (its hidden talent: damage_scale 1.2, hp_ratio 0.5): the 法术脆弱 status, renewed every 0.2 s for 0.3 s
|
||||
// (as 史尔特尔 AFT-Y's).
|
||||
// - S1 但为求索 (AUTO, 可充能 1 / 2, data DEFAULT): the next attack hits one more target (2), atk_scale × ATK arts each; when
|
||||
// it fires at one target only it lifts it (浮空) `levitate` s — counted at the shot (PRTS 备注).
|
||||
// - S2 群星逶迤 (MANUAL, 16 s, data DEFAULT): each attack becomes SHOTS (9, the text's "9连发"; no blackboard key) shots of
|
||||
// attack@atk_scale × ATK, each at a random enemy of her range (independent draws), each with attack@prob to lift it
|
||||
// attack@levitate s [ASSUMED: the nine leave with the attack; the burst's spacing is not modelled].
|
||||
// - S3 博览者的狂语 (MANUAL, 45 s, data ACTIVE_RANGE on 3-10): attack range 3-10 while it runs, attack interval +1.4 s
|
||||
// (base_attack_time, flat); each attack (an enemy in that range, her attack timer) releases three whirlwinds from the
|
||||
// centre of her tile and the tiles left and right of it, each after its own random 0–0.3 s delay. A whirlwind flies
|
||||
// straight forward at 1 tile/s over 5 tiles (PRTS; a 1×1 square), hits the first enemy it touches — an enemy her
|
||||
// attacks can select, air units too [ASSUMED] — lifting it attack@levitate s and then dealing (min + (max − min) ×
|
||||
// min(1, age / 1.77 s)) × her current ATK arts (attack@min_atk_scale → attack@max_atk_scale), and is gone; the nearest
|
||||
// of several touched at once is hit first [ASSUMED]. A whirlwind out when she leaves the field flies on, without 传承终焉
|
||||
// and without her module's RES ignore [ASSUMED: "模组加成" = the module's trait; its attributes stay in her ATK].
|
||||
// (wiki.gg reads the end of the flight as the far end of her range, 4 tiles; PRTS's 5 is kept.)
|
||||
// - No summon (the forms list no token).
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, batMod, giveSp, onHitBy, up } from '../shared/tier1.js';
|
||||
import { canTargetEnemy } from '../../../targeting.js';
|
||||
import { dirVec, rotateOffset } from '../../../dir.js';
|
||||
import { bodyDist, hitRect } from '../../../body.js';
|
||||
|
||||
const S1 = 'skchr_heyak_1';
|
||||
const S2 = 'skchr_heyak_2';
|
||||
const S3 = 'skchr_heyak_3';
|
||||
/** S1 "额外攻击一个目标": the next attack's targets (no blackboard key; `cnt` is the charge count). */
|
||||
const S1_TARGETS = 2;
|
||||
/** S2 "9连发": shots per attack when the text gives no number (no blackboard key). */
|
||||
const SHOTS = 9;
|
||||
/** S3 whirlwinds (PRTS 备注): lanes (facing-RIGHT row offsets: her row, "左1.0", "右1.0"), spawn delay, speed, flight, box. */
|
||||
const WIND_LANES = Object.freeze([0, 1, -1]);
|
||||
const WIND_DELAY = 0.3;
|
||||
const WIND_SPEED = 1;
|
||||
const WIND_RANGE = 5;
|
||||
const WIND_HALF = 0.5;
|
||||
/** "旋风在生成后1.77s内,攻击倍率由最低值线性递增至最大值". */
|
||||
const WIND_RAMP = 1.77;
|
||||
/** How often a flying whirlwind is drawn again (fx 'tornado'). */
|
||||
const WIND_FX_EVERY = 0.5;
|
||||
/** S3's 3-10 when the data carries no grid. */
|
||||
const R310 = Object.freeze([[1, 0], [1, 1], [1, 2], [1, 3], [1, 4], [0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [-1, 0], [-1, 1], [-1, 2], [-1, 3], [-1, 4]]);
|
||||
/** A whirlwind hit after she left the field: no 传承终焉, no module trait (PRTS 备注). */
|
||||
const AWAY_TAG = 'heyak:away';
|
||||
const WEIGHTLESS_KEY = 'heyak:weightless';
|
||||
const FRAGILE_EVERY = 0.2;
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
/** An elite or leader enemy (精英或领袖敌人). */
|
||||
const isEliteEnemy = (e) => !!e && e.side === 'enemy' && (e.isBoss || e.def?.rank === 'ELITE' || e.def?.rank === 'BOSS');
|
||||
const tagged = (d, tag) => !!(d && d.tags && d.tags.includes(tag));
|
||||
/** A module trait that works 在集成战略中 only (ISW-A): nothing of it applies in this mode. */
|
||||
const roguelikeOnly = (chess) => /集成战略/.test(String(chess?.trait?.moduleDesc ?? ''));
|
||||
/** The record's active module, or null. */
|
||||
const activeModule = (chess) => (chess?.module?.active ? (chess.modules ?? []).find((m) => m && m.uniEquipId === chess.module.id) ?? null : null);
|
||||
/** Does the enemy's body touch the 1×1 square centred on (x, y)? */
|
||||
function inBox(e, x, y) {
|
||||
const R = hitRect(e);
|
||||
if (!R) return Math.abs(e.x - x) <= WIND_HALF + 1e-9 && Math.abs(e.y - y) <= WIND_HALF + 1e-9;
|
||||
return R.x0 <= x + WIND_HALF && R.x1 >= x - WIND_HALF && R.y0 <= y + WIND_HALF && R.y1 >= y - WIND_HALF;
|
||||
}
|
||||
|
||||
export default {
|
||||
char_4027_heyak: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const isw = roguelikeOnly(chess);
|
||||
const mod = activeModule(chess);
|
||||
// CCR-Y's hidden part (the 【法术脆弱】 rider); an ISW module's hidden parts are 集成战略-only
|
||||
const rider = isw || !mod ? {} : Object.assign({}, ...(mod.talentChanges ?? []).filter((t) => t && t.talentIndex === -1).map((t) => t.bb ?? {}));
|
||||
const tb = isw ? {} : traitBb(chess);
|
||||
const pen = num(tb.magic_resist_penetrate_fixed);
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s1 = skillRec(chess, S1), s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
|
||||
const shots = Math.max(1, Math.floor(num(+(/(\d+)连发/.exec(String(s2?.desc ?? ''))?.[1]), SHOTS)));
|
||||
|
||||
/** S3: release one attack's three whirlwinds. */
|
||||
function releaseWinds(battle, unit, winds) {
|
||||
const seq = unit.deploySeq;
|
||||
const [fr, fc] = dirVec(unit.dir);
|
||||
const x0 = unit.tileC, y0 = unit.tileR;
|
||||
for (const lane of WIND_LANES) {
|
||||
const [dr, dc] = rotateOffset(lane, 0, unit.dir);
|
||||
const delay = battle.rng.range(0, WIND_DELAY);
|
||||
battle.after(delay, () => {
|
||||
// a whirlwind not yet out when she left is never released [ASSUMED: "已存在的旋风" only fly on]
|
||||
if (!(unit.alive && unit.deployed && unit.deploySeq === seq)) return;
|
||||
const w = { x: x0 + dc, y: y0 + dr, dx: fc, dy: fr, left: WIND_RANGE, age: 0, fxAge: 0, seq };
|
||||
battle.fx('tornado', { x: w.x, y: w.y, id: unit.id, r: WIND_HALF, dur: WIND_FX_EVERY + 0.1 });
|
||||
if (!windHit(battle, unit, w)) winds.push(w);
|
||||
}, { owner: unit });
|
||||
}
|
||||
}
|
||||
/** The first enemy the whirlwind touches, struck: 浮空 first (PRTS: before 传承终焉), then the damage. True when it hit. */
|
||||
function windHit(battle, unit, w) {
|
||||
let best = null, bd = Infinity;
|
||||
for (const e of battle.enemies) {
|
||||
if (!e.alive || !inBox(e, w.x, w.y) || !canTargetEnemy(unit, e, unit.profile)) continue;
|
||||
const d = bodyDist(e, w.x, w.y);
|
||||
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && best && e.spawnSeq < best.spawnSeq)) { bd = d; best = e; }
|
||||
}
|
||||
if (!best) return false;
|
||||
const away = !(unit.alive && unit.deployed && unit.deploySeq === w.seq);
|
||||
const lev = num(b3['attack@levitate']);
|
||||
if (lev > 0) battle.applyStatus(best, 'levitate', { duration: lev, source: unit });
|
||||
const lo = num(b3['attack@min_atk_scale']), hi = num(b3['attack@max_atk_scale'], lo);
|
||||
const scale = lo + (hi - lo) * Math.min(1, w.age / WIND_RAMP);
|
||||
if (best.alive) {
|
||||
battle.dealDamage(unit, best, {
|
||||
amount: unit.s.atk * scale, type: 'arts', isAttack: true, isSkill: true,
|
||||
...(away ? { resIgnoreFlat: -pen, tags: [AWAY_TAG] } : null),
|
||||
});
|
||||
}
|
||||
battle.fx('tornadoPulse', { x: w.x, y: w.y, id: unit.id, r: WIND_HALF });
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||||
targeting: { maxTargets: S1_TARGETS },
|
||||
attack: { atkScale: num(b1.atk_scale, 1) },
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
attack: { atkScale: num(b2['attack@atk_scale'], 1) },
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { batPct: batMod(b3.base_attack_time, chess) },
|
||||
targeting: { rangeGrid: s3?.rangeGrid ?? R310 },
|
||||
// the attack becomes the whirlwinds (onTick): her attack timer, an enemy of the skill's range, no projectile
|
||||
attack: { noAttack: true },
|
||||
onTick({ battle, unit }) {
|
||||
if (unit.atkCd > 0 || !unit.canAct || unit.s.flags.disarm) return;
|
||||
const foes = battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile);
|
||||
if (!foes.length) return;
|
||||
unit.atkCd = unit.s.interval;
|
||||
unit.lastAttackAt = battle.time;
|
||||
unit.stats.attacks++;
|
||||
releaseWinds(battle, unit, unit.mem.heyakWinds);
|
||||
// an attack was performed ("攻击时" content), the whirlwinds still on their way
|
||||
if (battle.hasHook('attack')) battle.emit('attack', { attacker: unit, targets: foes.slice(0, 1), isSkill: true });
|
||||
},
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 传承终焉: × atk_scale and 沉默 on an air unit hit by her attack
|
||||
const sc = num(t0.atk_scale, 1), sil = num(t0.silence);
|
||||
onHitBy(battle, unit, (ctx) => {
|
||||
const d = ctx.dmg, t = ctx.target;
|
||||
if (!d.isAttack || tagged(d, AWAY_TAG) || !t.isFlying) return;
|
||||
if (sc !== 1) d.amount *= sc;
|
||||
if (sil > 0 && t.alive) battle.applyStatus(t, 'silence', { duration: sil, source: unit });
|
||||
});
|
||||
} },
|
||||
{ install(battle, unit) { // 曾有羽翼: 失重 above hp_ratio in her range; CCR-Y stage 3: 【法术脆弱】 below its hp_ratio
|
||||
const hiRatio = num(t1.hp_ratio, 1);
|
||||
battle.on('tick', () => {
|
||||
const on = up(unit);
|
||||
const inRange = on ? new Set(battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) : null;
|
||||
for (const e of battle.enemies) {
|
||||
if (!e.alive) continue;
|
||||
// (another 霍尔海雅's 失重 carries the same key: it is hers to keep or drop)
|
||||
const mine = e.findBuff(WEIGHTLESS_KEY);
|
||||
const want = on && inRange.has(e) && e.hpRatio > hiRatio + 1e-9
|
||||
&& !e.buffs.some((b) => b.status === 'weightless' && b.key !== WEIGHTLESS_KEY);
|
||||
if (want && !mine) battle.addBuff(e, { key: WEIGHTLESS_KEY, status: 'weightless', visible: true, mods: { massFlat: -1 }, source: unit });
|
||||
else if (!want && mine && mine.source === unit) battle.removeBuff(e, mine);
|
||||
}
|
||||
}, { owner: unit });
|
||||
const fragile = Math.round((num(rider.damage_scale, 1) - 1) * 1e6) / 1e6, loRatio = num(rider.hp_ratio); // 1.2 → 0.2
|
||||
if (fragile > 0) {
|
||||
battle.every(FRAGILE_EVERY, () => {
|
||||
if (!up(unit)) return;
|
||||
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) {
|
||||
if (e.hpRatio < loRatio - 1e-9) battle.applyStatus(e, 'artsFragile', { duration: FRAGILE_EVERY + 0.1, value: fragile, source: unit });
|
||||
}
|
||||
}, { owner: unit, immediate: true });
|
||||
}
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// module traits (none on the normal form, nothing of ISW-A's): CCR-X RES ignore, CCR-Y SP per hit on an elite / leader
|
||||
if (pen > 0) statBuff(battle, unit, 'trait:heyak:resIgnore', { resIgnoreFlat: pen });
|
||||
const sp = num(tb.sp);
|
||||
if (sp > 0) {
|
||||
battle.on('damaged', (ctx) => {
|
||||
const d = ctx.dmg;
|
||||
if (ctx.source !== unit || !d || !d.isAttack || d.isSplash || tagged(d, AWAY_TAG) || !isEliteEnemy(ctx.target)) return;
|
||||
giveSp(unit, sp, 'trait');
|
||||
}, { owner: unit });
|
||||
}
|
||||
const sid = unit.skill?.id;
|
||||
if (sid === S1) {
|
||||
// 但为求索: the shot count decides the 浮空 (PRTS: the shots fired, whatever happens in flight)
|
||||
const lev = num(b1.levitate);
|
||||
battle.on('beforeAttack', (ctx) => {
|
||||
if (ctx.attacker !== unit || !ctx.isSkill) return;
|
||||
unit.mem.heyakLift = ctx.targets.length === 1 ? ctx.targets[0] : null;
|
||||
}, { owner: unit, priority: -10 });
|
||||
onHitBy(battle, unit, (ctx) => { // before 传承终焉 (priority 0): the lift is settled first
|
||||
const d = ctx.dmg;
|
||||
if (!d.isAttack || !d.isSkill || unit.mem.heyakLift !== ctx.target) return;
|
||||
unit.mem.heyakLift = null;
|
||||
if (lev > 0) battle.applyStatus(ctx.target, 'levitate', { duration: lev, source: unit });
|
||||
}, 10);
|
||||
} else if (sid === S2) {
|
||||
// 群星逶迤: the nine shots, each at a random enemy of her range, each rolling its 浮空 before 传承终焉
|
||||
const prob = num(b2['attack@prob']), lev = num(b2['attack@levitate']);
|
||||
battle.on('beforeAttack', (ctx) => {
|
||||
if (ctx.attacker !== unit || !unit.skill?.active || !ctx.targets.length) return;
|
||||
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, ctx.profile);
|
||||
for (const e of battle.blockedTargets(unit, ctx.profile)) if (!cands.includes(e)) cands.push(e);
|
||||
if (!cands.length) return;
|
||||
const out = [];
|
||||
for (let i = 0; i < shots; i++) out.push(battle.rng.pick(cands));
|
||||
ctx.targets = out;
|
||||
}, { owner: unit });
|
||||
onHitBy(battle, unit, (ctx) => {
|
||||
const d = ctx.dmg;
|
||||
if (!d.isAttack || !d.isSkill || !(prob > 0) || !(lev > 0)) return;
|
||||
if (battle.rng.chance(prob)) battle.applyStatus(ctx.target, 'levitate', { duration: lev, source: unit });
|
||||
}, 10);
|
||||
} else if (sid === S3) {
|
||||
// 博览者的狂语: the whirlwinds in flight (also after she left the field)
|
||||
const winds = (unit.mem.heyakWinds = []);
|
||||
battle.on('tick', ({ dt }) => {
|
||||
for (let i = winds.length - 1; i >= 0; i--) {
|
||||
const w = winds[i];
|
||||
const step = Math.min(WIND_SPEED * dt, w.left);
|
||||
w.x += w.dx * step;
|
||||
w.y += w.dy * step;
|
||||
w.left -= step;
|
||||
w.age += dt;
|
||||
if (windHit(battle, unit, w) || w.left <= 1e-9) { winds.splice(i, 1); continue; }
|
||||
w.fxAge += dt;
|
||||
if (w.fxAge + 1e-9 >= WIND_FX_EVERY) {
|
||||
w.fxAge = 0;
|
||||
battle.fx('tornado', { x: w.x, y: w.y, id: unit.id, r: WIND_HALF, dur: WIND_FX_EVERY + 0.1 });
|
||||
}
|
||||
}
|
||||
}, { owner: unit });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user