feedback5 O4: 自选 operator kits for 陈, 艾丽妮 and 赫拉格 — every skill, talent, trait and module (stage 1 and 3) at every form

- op-chen.js (char_010_chen, 剑豪): S1 鞘击 one sheath strike (her Skill clip: one OnAttack) + stun; S2 赤霄·拔刀 up to
  max_target enemies of 3-12, flyers too, arts then physical (PRTS 备注); S3 赤霄·绝影 10 slashes on the nearest ground
  enemy of x-1 at the Skill_3 clip's OnAttack times, the last stuns, 2.933 s of 无敌 / 无法阻挡 / stun and freeze
  immunity, ends at once with no target before the 10th (PRTS 备注), not opened with only flyers in range [ASSUMED];
  呵斥 per-deployment timer +1 attack / hurt SP to the field's allies (SWO-X 3: every 3 s, +1 more for her); 持刀格斗术;
  SWO-X x1.1 during her skill (PRTS 修正 技能期间), SWO-Y ignores 70 DEF.
- op-irene.js (char_4009_irene, 剑豪): 审判之火 as PRTS 备注 (a 物理穿透 charge: air units always and without a draw,
  浮空 enemies count, ground at 50 %, spent only by a landing instance); SWO-X 3: 55 % and +6 SP after a skill that knocked
  out an air unit; 净化之剑 doubled while a 【海怪】 is on the field; S1 起风 strike + 浮空 + second strike; S2 裂潮 2
  charges, ground only, 重量 <= 3 float, not opened with only flyers in range [ASSUMED]; S3 判决 3.5 s (PRTS): opening
  strike + 浮空, 10 bombardments every 0.3 s, radius 1.1, air too; SWO-X x1.1 skill damage, SWO-Y 70 DEF, ISW-A
  attributes only (its talent parts are 集成战略-only).
- op-helage.js (char_188_helage, 武者): S1 新月 two hits, S2 弦月 二连击 + 75 % dodge, S3 满月 range +1 / 3 targets;
  the 武者 heal per damage instance (the kit adds one per further hit of an attack); 坚忍 ASPD (SBL-X 3: +130 at 50 %);
  运筹帷幄 生命回复速度 while not blocking [ASSUMED: no PRTS 备注], SBL-Y 3 also below 30 % HP (80 / s, never both);
  SBL-X 庇护 25 % below 50 % HP; SBL-Y once per deployment back to 30 % HP.
- kits/index.js OPERATOR_KIT_FILES: the three files appended. Tests: op_chen / op_irene / op_helage (10 each, every form x
  module x skill, numbers from data/backups.json).
- Golden: only the diy family moved — the packing now fields the new kits: diy-003 … diy-006 (the prototypes before) and
  the new diy-007 … diy-009 field 陈 / 艾丽妮 / 赫拉格 in every form x module x skill, the prototypes moved to the new
  diy-010 … diy-013; diy-001 / diy-002 (推进之王) and roster / bonds / fields / matches / standins unchanged.
  test/golden/README.md: the diy row no longer counts battles or names 推进之王 alone.
This commit is contained in:
sganggs committed 2026-10-05 23:51:27 +08:00
1 parent 742f478526
commit a62728c188
9 files changed
+1798 -102

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
// server/sim/content/kits/ops/op-irene.js — 艾丽妮 (char_4009_irene) 自选 operator kit: 6★ 剑豪 (近卫), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and all three modules at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4009_irene, 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 艾丽妮 (审判之火
// 备注: "计算伤害前有概率(地面)/必定(空中单位)获取永久的物理穿透(百分比)Buff(不可叠加,直接加算),该效果仅在成功造成伤害后
// 消耗", popup "对空中单位造成物理伤害时,不会占用随机数"; S3 备注 "技能范围内仅存在飞行单位时,该技能也可开启 / 技能动画为固定时长
// (3.5秒),不受攻击速度影响 / 快速轰击可对空,溅射半径1.1"), PRTS 分支特性信息 §剑豪, gamedata_const ba.levitate ("变为空中单位").
// - Trait (剑豪) "普通攻击连续造成两次伤害": the profession default (hits 2), ground-only melee on her 1-1. SWO-X “审判官口粮”
// "技能造成的伤害提升10%" (trait bb damage_scale): her skill damage ×1.1; SWO-Y “厚重的经卷” "攻击时无视敌人70点的防御力"
// (def_penetrate_fixed): defIgnoreFlat on all her damage; ISW-A “艾丽妮特限证章”: only its attributes act here — its
// trait text and every talent part are 集成战略-only (battle_equip_table validInGameTag roguelike: 再部署 −60 %, +8 SP on
// deploy, the stage-3 range effects) ⇒ N/A; its stage-3 审判之火 text keeps the base 50 % / 50 %.
// - T1 审判之火 "对敌人造成物理伤害时有50%概率无视其50%防御力,对空中单位概率提升至100%": per PRTS 备注, before each physical
// damage instance she deals (normal or skill) she holds a 物理穿透 charge or draws one — always against an air unit (no
// random draw; 浮空 enemies are air units, ba.levitate), with `prob` against a ground one — and the charge adds
// def_penetrate to that instance's defIgnorePct; it is spent only by a damage instance that lands (a dodge or a cancel
// keeps it, and while she holds one she draws nothing: 不可叠加). SWO-X stage 3: 55 % and "技能期间若击倒空中单位,技能结束
// 时获得6点技力" — an air unit she knocks out while her skill is active (S1's strike, S2's cut, S3's 3.5 s) gives her +sp SP
// once that skill ends.
// - T2 净化之剑 "攻击速度+18,场上有【海怪】敌人时效果翻倍": ASPD +attack_speed; SWO-Y stage 3 adds ATK +5 % (the module talent
// change's atk); both doubled while a living 【海怪】 enemy (data tag seamonster) is on the field.
// - S1 起风 (AUTO, attack SP, DEFAULT): the next attack strikes its target once for atk_scale × ATK physical and levitates it
// `levitate` s, then hits it again for atk_scale × ATK physical (her skeleton's Skill_1 clip: two OnAttack) — the second
// hit meets an air unit when the levitation took.
// - S2 裂潮 (MANUAL, attack SP, 2 charges, 技能范围 3-12, data SKILL_RANGE): at once, up to max_target ground enemies of the
// skill range take atk_scale × ATK physical, and those of 重量 ≤ value (3) are levitated `levitate` s. It cannot be opened
// while its range holds no ground enemy at all [ASSUMED: PRTS's S3 note "仅存在飞行单位时,该技能也可开启" marks the opposite
// as the rule for this ground-only skill; the strategy's SKILL_RANGE still counts unselectable ones, "无视其不可选中"].
// - S3 判决 (MANUAL, attack SP, 技能范围 x-1, data SKILL_RANGE — PRTS: opens with only flyers in range): every ground enemy of
// the skill range takes atk_scale × ATK physical and is levitated `levitate` s, then multi_times (10) bombardments, one
// every multi_hit_interval (0.3) s, each on a random enemy of the skill range (flyers included) hitting every enemy within
// 1.1 of it (中点判定; PRTS 溅射半径1.1, 可对空) for multi_atk_scale × ATK physical; the skill runs PRTS's fixed 3.5 s with
// no normal attack [ASSUMED: the first bombardment one interval after the opening strike; a bombardment with no enemy in
// the skill range is skipped].
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, giveSp, onHitBy } from '../shared/tier1.js';
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
const S1 = 'skchr_irene_1';
const S2 = 'skchr_irene_2';
const S3 = 'skchr_irene_3';
/** PRTS S3 备注: "技能动画为固定时长(3.5秒),不受攻击速度影响". */
const S3_DURATION = 3.5;
/** PRTS S3 备注: "快速轰击可对空,溅射半径1.1". */
const S3_SPLASH = 1.1;
/** The skill ranges when a record carries none (data: 3-12 and x-1). */
const R3_12 = Object.freeze([[1, 0], [1, 1], [0, 0], [0, 1], [0, 2], [0, 3], [-1, 0], [-1, 1]]);
const X_1 = Object.freeze([[2, 0], [1, -1], [1, 0], [1, 1], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, -1], [-1, 0], [-1, 1], [-2, 0]]);
const AIR = Object.freeze({ canHitFly: true });
const GROUND = Object.freeze({ canHitFly: false });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** Absolute tile keys of a skill range at the unit's tile and facing (a 技能范围 ignores 攻击距离: no extend). */
const keysOf = (unit, grid) => absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0);
/** A living 【海怪】 enemy on the field. */
const seaMonsterOnField = (battle) => battle.enemies.some((e) => e.alive && !e.hidden && Array.isArray(e.def?.tags) && e.def.tags.includes('seamonster'));
/**
* A skill that strikes ground enemies only cannot be opened while its range holds no ground enemy: every activation path
* (the strategy's SKILL_RANGE, content) is refused and the charge stays — the pattern of 莎草 S2 (ops/chess_char_2_06-papyrs.js,
* non-enumerable: never serialised).
*/
function guardActivation(battle, unit, grid) {
const sk = unit.skill;
if (!sk || Object.prototype.hasOwnProperty.call(sk, 'activate')) return;
const activate = sk.activate;
const ok = () => battle.unitsInGrid(unit, grid, { side: 'enemy' }).some((e) => !e.isFlying);
Object.defineProperty(sk, 'activate', {
configurable: true, writable: true, enumerable: false,
value(reason, opts) { return ok() ? activate.call(this, reason, opts) : false; },
});
}
export default {
char_4009_irene: (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 s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
const grid2 = s2?.rangeGrid ?? R3_12, grid3 = s3?.rangeGrid ?? X_1;
const levitate = (battle, unit, e, s) => { if (e.alive && s > 0) battle.applyStatus(e, 'levitate', { duration: s, source: unit }); };
return {
skills: {
[S1]: {
kind: num(skillRec(chess, S1)?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
attack: {
atkScale: num(b1.atk_scale, 1),
hits: 1, // the first strike; the follow-up below (Skill_1 clip: two OnAttack)
onEachHit({ battle, unit, target, kind, attackId }) {
if (kind !== 'main' || !target || target.side !== 'enemy' || !target.alive) return;
levitate(battle, unit, target, num(b1.levitate));
if (!target.alive || !unit.alive) return;
battle.dealDamage(unit, target, { amount: unit.s.atk * num(b1.atk_scale, 1) * unit.s.atkScaleMul, type: 'phys', isAttack: true, isSkill: true, attackId, tags: ['skill'] });
},
},
},
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
onStart({ battle, unit }) {
const list = battle.enemiesInKeys(keysOf(unit, grid2), unit, GROUND);
sortEnemyTargets(battle, unit, list, null);
const v = list.slice(0, Math.max(1, Math.floor(num(b2.max_target, 1))));
battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length, skill: 'irene:tide' });
for (const e of v) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(b2.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
// "使其中重量小于等于3的目标浮空"
if (e.alive && e.weight <= num(b2.value, 3)) levitate(battle, unit, e, num(b2.levitate));
}
},
},
[S3]: {
kind: 'duration',
duration: S3_DURATION,
attack: { noAttack: true },
onStart({ battle, unit }) {
unit.mem.ireneS3 = { t: 0, n: 0 };
const v = battle.enemiesInKeys(keysOf(unit, grid3), unit, GROUND);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'irene:judgment' });
for (const e of v) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(b3.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
levitate(battle, unit, e, num(b3.levitate));
}
},
onTick({ battle, unit, dt }) {
const m = unit.mem.ireneS3;
if (!m) return;
m.t += dt;
const iv = Math.max(0.05, num(b3.multi_hit_interval, 0.3)), times = Math.max(0, Math.floor(num(b3.multi_times, 10)));
while (m.n < times && m.t + 1e-9 >= (m.n + 1) * iv) {
m.n++;
const cands = battle.enemiesInKeys(keysOf(unit, grid3), unit, AIR);
if (!cands.length) continue;
const c = battle.rng.pick(cands);
battle.fx('aoe', { x: c.x, y: c.y, radius: S3_SPLASH, id: unit.id, skill: 'irene:judgment' });
for (const e of battle.foesInRadius(c.x, c.y, S3_SPLASH, true)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(b3.multi_atk_scale, 1), type: 'phys', isSkill: true, isSplash: e !== c, tags: ['skill'] });
}
if (!unit.alive || !unit.skill?.active) return;
}
},
onEnd({ unit }) { unit.mem.ireneS3 = null; },
},
},
talents: [
{ install(battle, unit) { // 审判之火: a 物理穿透 charge per physical instance (air: always, ground: prob), spent on landing
const prob = num(t0.prob), pen = num(t0.def_penetrate), sp = num(t0.sp);
if (pen > 0) {
onHitBy(battle, unit, (ctx) => {
const d = ctx.dmg, m = unit.mem;
if (d.type !== 'phys') return;
if (!m.ireneCharge) m.ireneCharge = ctx.target.isFlying || (prob > 0 && battle.rng() < prob);
if (!m.ireneCharge) return;
d.defIgnorePct += pen;
m.ireneChargeDmg = d;
});
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg || c.dmg !== unit.mem.ireneChargeDmg) return;
unit.mem.ireneCharge = false;
unit.mem.ireneChargeDmg = null;
}, { owner: unit });
}
// SWO-X stage 3: an air unit knocked out during her skill ⇒ +sp SP when the skill ends
if (sp > 0) {
battle.on('death', (c) => {
if (c.reason === 'killed' && c.killer === unit && c.unit?.side === 'enemy' && c.unit.isFlying && unit.skill?.active) unit.mem.ireneAirKill = true;
}, { owner: unit });
battle.on('skillEnd', (c) => {
if (c.unit !== unit || !unit.mem.ireneAirKill) return;
unit.mem.ireneAirKill = false;
if (unit.alive) giveSp(unit, sp);
}, { owner: unit });
}
} },
{ install(battle, unit) { // 净化之剑: ASPD (SWO-Y stage 3: ATK too), doubled while a 【海怪】 enemy is on the field
const mods = { aspd: num(t1.attack_speed), atkPct: num(t1.atk) };
statBuff(battle, unit, 'talent:irene:sword', mods);
if (mods.aspd || mods.atkPct) {
const extra = {};
for (const [k, v] of Object.entries(mods)) if (v) extra[k] = v;
toggleBuff(battle, unit, 'talent:irene:seamonster', () => seaMonsterOnField(battle), extra);
}
} },
],
install(battle, unit) {
// SWO-Y “厚重的经卷”: 攻击时无视敌人70点的防御力
statBuff(battle, unit, 'trait:irene:pierce', { defIgnoreFlat: num(tb.def_penetrate_fixed) });
// SWO-X “审判官口粮”: 技能造成的伤害提升10%
const skillMul = num(tb.damage_scale, 1);
if (skillMul !== 1) onHitBy(battle, unit, ({ dmg }) => { if (dmg.isSkill) dmg.mul *= skillMul; });
if (unit.skill?.id === S2) guardActivation(battle, unit, grid2);
},
};
},
};