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:
1 parent
742f478526
commit
f4b826fe28
9 files changed
+2020
-103
No files matched your search
@@ -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 });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user