feedback5 O2: 自选 operator kits for 艾雅法拉, 刻俄柏 and 霍尔海雅 — every skill, talent, trait and module (stages 1 and 3) at every form, with tests

- kits/ops/op-amgoat.js (艾雅法拉): S1 二重咏唱 (ATK from the second cast of a deployment), S2 点燃 (main target's RES cut
  first, ½ explosion r1.5 on all, ½ again on the main target), S3 火山 (x-3, −1.1 s, lava on random 4 / 5 enemies), 炎息
  aura (CCR-X Lv3 携带时 22 %), 乱火 deploy SP (CCR-Y Lv3 ASPD, maxima with an elite / leader in range), CCR-X RES ignore,
  CCR-Y SP per hit on an elite / leader.
- kits/ops/op-cerber.js (刻俄柏): S1 unblocked first + 束缚, S2 interval ×0.6 / ×0.4 highest DEF, S3 3-3 physical lowest DEF
  + silence, 剥壳 DEF × ratio arts (CCR-X Lv3 ramp 50 → 75 %), 独行长路 x-5 incl. PRTS's enemy-like-ally rule (CCR-Y Lv3:
  decided per deployment).
- kits/ops/op-heyak.js (霍尔海雅): S1 two targets / single-target 浮空, S2 nine random shots with 浮空 rolls, S3 three
  whirlwinds (PRTS speed 1, 1x1 box, 5 tiles, 1.77 s ramp, away rules), 传承终焉 (lifts resolve first), 曾有羽翼 失重 / CCR-Y
  Lv3 法术脆弱; ISW-A: attributes and 传承终焉 numbers only (its trait and hidden talents are validInGameTag roguelike).
- kits/index.js: the three files appended to OPERATOR_KIT_FILES (one line per file).
- Golden: only the diy family moved — diy-003 … diy-006 re-packed and diy-007 … diy-013 new, because the diy family fields
  every kitted owned 6★ in every form × module × skill (the three new operators, then the prototypes); diy-001 / 002 and
  every other family (roster, bonds, fields, matches, standins) byte-identical. test/golden/README.md counts updated
  (13 diy battles, 61 of 157 fast).
- Reported, not changed: 艾雅法拉 S3's data trigger is SKILL_RANGE (攻击范围增大 escapes build-data's ATTACK_RANGE_CHANGE).
This commit is contained in:
sganggs committed 2026-10-06 00:00:14 +08:00
1 parent 742f478526
commit f4b826fe28
9 files changed
+2020 -103

No files matched your search

+180
View File
@@ -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 });
}
},
};
},
};