feedback5 O18: 自选 kits for 阿, 傀影 (with his 镜中虚影) and 弑君者

Three owned-6★ 自选 picks get operator kits (kits/ops/op-haak.js, op-phatom.js, op-crosly.js; appended to
OPERATOR_KIT_FILES): every skill, both talents, the trait and every module at stage 1 / 3 in every DIY form, from the
data, PRTS (备注) and the client's battle logic (charpacks, skill / equip prefabs, buff templates).

- 阿: 怪杰 drain (profession default), 混合药物射击 (one of four per attack, GEE-X stage 2+ "all four" roll), 药剂扩散
  (healing received, GEE-Y stage 3 deploy heal on the ally ahead), S1 ASPD, S2 / S3 爆发剂 (15 hits of 500 fixed ATK on
  the ally of his range ahead, then the buff on both), GEE-X SP recovery while HP >= 80 %.
- 傀影: S1 dodge + physical-only barrier, S2 stacked ATK used per damage instance, S3 burst (x-4, ground, radial 小力
  push, one random status per cast); the twin's kit (its skill copy, killed when he leaves, back on its tile once its
  own redeploy time has run from its last deployment and he stands, 5 DP), 虚影精通, EXE-X (refund N/A), EXE-Y
  (lonely ATK, stage 3 ATK on both), ISW-A attributes only.
- 弑君者: the smoke's 命中率 cut (x-4, S3 x-1, S2 x2; battle-wide hit handler), 弑君者威名 (fresh ground enemies x1.2,
  EXE-Y stage 3 x1.35 and stun x1.5), S1 ATK / dodge, S2 no attacks + taunt -1 + end blast, S3 隐匿 + block 0 +
  2 s strikes with 6 s marks.

Tests: test/content/op_haak.test.js (10), op_phatom.test.js (8), op_crosly.test.js (8).
Golden: only diy moved — diy-094..diy-100 field the 78 new configurations (阿 24, 傀影 30, 弑君者 24; no summon is
placed by the family), diy-101..diy-104 are the 预备干员 / prototype battles re-packed after them (new index, stage
and seed). roster, bonds, fields, matches and standins are unchanged.
This commit is contained in:
sganggs committed 2026-10-06 03:04:57 +08:00
1 parent 78251f499c
commit b55d4f9d04
8 files changed
+1909 -99

No files matched your search

+184
View File
@@ -0,0 +1,184 @@
// server/sim/content/kits/ops/op-haak.js — 阿 (char_225_haak) 自选 operator kit: 6★ 怪杰 (特种), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (GEE-X 什锦果味医用箱, GEE-Y 毒医残章) at
// every form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_225_haak, 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) — the owner's decision of 2026-10-05.
// Potential 0 [ASSUMED: no account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into
// backups.json); PRTS 阿 (混合药物射击 备注 "每次攻击必定触发随机效果,4种效果的概率平均分布" / "<X模组>先判断是否“同时触发”所有
// 效果,未触发情况下再判断随机效果"; 爆发剂 备注 "进行完整的15次攻击后阿与目标获得持续时间相当于技能持续时间的增益;15次攻击均为无途径
// 攻击,但可满足近战/远程攻击相关的触发条件"); PRTS 分支特性信息 怪杰 ("可对空", "每秒流失最大生命值1%的生命值,不会致命且最低保留1
// 生命值"); the client's battle data — charpack char_225_haak (Talents/1: the additive active buff haak_t_1; Talents/2:
// heal_scale; Trait: periodic_damage_by_hp_ratio[skip_modifier_fix][undeadable] every 1 s), buff_template_data
// (haak_t_1: RandomCreateBuff of haak_t_atk (AtkScaleUp `atk_scale` on ON_CALCULATE_DAMAGE) / sluggish / stun / haak_t_heal
// (HealViaMaxHpRatio), weight 1 each; haak_e_002[dice]: Dice `prob` ⇒ all four, else the random one), the skill prefabs
// skchr_haak_2 / _3 (Attack + Fake_Attack: 15 shots — `_additionalTimes` 14, `_triggerDelta` 0.03 s — of
// instant_damage_physical[add_sp] = FixedValueDamage PHYSICAL `damage`; the target's buff fed with the last shot, his own
// by the Buff ability after the volley; ally selector `_targetSide` 1, `_excludeOwner`, `_postFilter` 21, his attack
// range) and the equips haak_equip_1_* (the hidden sp_recovery talent while his HP ratio is in [0.8, 1]) / haak_equip_2_2,
// _2_3 (heal_scale + the ability healinrange: one ally of his range, the same selector, healed healinrange.heal_scale ×
// ATK at once); Terra Wiki Aak (爆发剂: "an ally within range (prioritizes allies directly in front)", "the one in the
// frontal row/column from the closest to the furthest").
// - Trait (怪杰) "自身生命会不断流失": the profession default (professions.js geek, installHpDrain: hp_ratio × max HP every
// second, never below 1 HP), ranged physical arrows that hit air units (data canHitFly; PRTS "可对空"), blocks 1, 3-3;
// ground enemies target him (no flag). Module GEE-X adds "生命值高于80%时,技力自然回复速度+0.25/秒" (the hidden module
// talent sp_recovery_per_sec: SP recovery +0.25/s while his HP ratio is at least 80 % — the equip checker's [0.8, 1]);
// GEE-Y "再部署时间减少" is its attribute respawn_time −15 (in the stats, as its ATK / ASPD).
// - T1 混合药物射击 "每次攻击时会随机触发下列效果之一": every attack of his on an enemy rolls once, at its hit (the talent's
// additive active buff rides the attack onto the target — not damage-missable: a dodged hit still rolls), one of four
// with equal weight: heal himself hp_ratio × max HP (a 治疗: T2 scales it), that attack's damage ×atk_scale (an ATK scale,
// before mitigation — "当次攻击力提升至150%"), 停顿 `sluggish` s or 晕眩 `stun` s on the target. GEE-X stage 2+: first
// `prob` to trigger all four at once (haak_e_002[dice]), else the random one; stage 3's numbers are the module's.
// [ASSUMED] his 爆发剂 shots at an ally never roll (PRTS names no such effect); the client's haak_t_atk lingers until his
// next roll and could scale damage he deals meanwhile — here it scales the attack that rolled it only.
// - T2 药剂扩散 "自身受到的治疗量+20%": healing received ×heal_scale. GEE-Y stage 2+ (1.25 at stage 3) adds "部署后立即恢复攻击
// 范围内一名友方干员(优先选取正前方)的生命值,恢复量为阿攻击力的150%": at each deployment, after the deploy-time buffs, a heal
// of healinrange.heal_scale × ATK on the ally frontAlly picks among the healable ones [ASSUMED: injured or not — the
// selector has no HP filter; summons count, as for 爆发剂's identical selector].
// - S1 快速射击 (MANUAL, data DEFAULT): ASPD +attack_speed for its duration.
// - S2 爆发剂·γ型 / S3 爆发剂·榴莲味 (MANUAL, data DEFAULT; timed: 30 / 20 s): at the cast, `stimulant`: the ally frontAlly
// picks takes 15 physical hits of `damage` (500) fixed attack power each (FixedValueDamage: DEF applies, 5 % minimum;
// 普通伤害, skill damage — 受击回复 SP and the 重装 TAKE_DAMAGE trigger answer them like any damage taken: the
// template's "[add_sp]"); then he and — still standing — the target get max HP +max_hp / DEF +def (S2) or ATK +atk / ASPD
// +attack_speed (S3) for the skill's duration (per 阿 on the target: independentCharacterSource). [ASSUMED] the volley
// lands at once (its 0.42 s of shots and flight are not modelled); with no ally in his range the cast does nothing (the
// client's sequence stops when its attack finds no target) — his skill still runs and spends its SP: the data's DEFAULT
// trigger needs an enemy, not an ally (reported as an open question).
import { num, talentBb, moduleBb, moduleOn, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
import { toLocal } from '../../../dir.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_haak_1';
const S2 = 'skchr_haak_2';
const S3 = 'skchr_haak_3';
/** 爆发剂's shots when the text names none ("攻击15次"; skill prefab `_additionalTimes` 14 + the first). */
const SHOTS_FALLBACK = 15;
/** GEE-X "生命值高于80%时" when the trait text names none (the equip checker's `_minHpRatio`). */
const GEEX_HP_FALLBACK = 0.8;
/** 混合药物射击's four effects, in the client's RandomCreateBuff order (weight 1 each). */
const DICE = Object.freeze(['atk', 'sluggish', 'stun', 'heal']);
const TAG_STIM = 'haak:stim';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/**
* 爆发剂 / 药剂扩散's target (the client selector `_postFilter` 21; Terra Wiki Aak): an ally — an operator or a summon,
* never a device, never a 孤立 one (Battle.allySelectable) — standing in his current attack range, himself excluded; the
* ones on his straight line ahead (local row 0) first, each group nearest first; ties: the higher taunt level, then the
* latest deployed [ASSUMED: the Terra Wiki's "highest aggression level"], then the unit id. `heal` = a heal selector: no
* 禁疗 / 无法被友方治疗 ally (PRTS 选择器 "若掩码中禁疗为1,且选择器属于治疗行为,则不可选中").
*/
export function frontAlly(battle, unit, heal = false) {
const keys = unit.rangeKeySet || new Set(unit.rangeKeys || []);
let best = null, bk = null;
for (const a of battle.allyUnits) {
if (a === unit || !a.alive || !a.deployed || a.hidden || a.kind === 'device') continue;
if (!keys.has(a.tileR * COLS + a.tileC) || !battle.allySelectable(a, unit)) continue;
if (heal && (a.s.flags.noHeal || a.s.flags.healFree || a.profile?.noHeal)) continue;
const dr = a.tileR - unit.tileR, dc = a.tileC - unit.tileC;
const [lr, lc] = toLocal(dr, dc, unit.dir);
const k = [lr === 0 && lc > 0 ? 0 : 1, Math.hypot(dr, dc), -(a.s.taunt || 0), -(a.aggroSeq || a.deploySeq || 0), a.id];
let less = !bk;
for (let i = 0; !less && i < k.length; i++) {
if (k[i] === bk[i]) continue;
less = k[i] < bk[i];
break;
}
if (less) { best = a; bk = k; }
}
return best;
}
/**
* 爆发剂 at the cast: `shots` hits of `damage` fixed attack power on frontAlly's pick, then `mods` on him and on the
* target (if it still stands) for `dur` s. No ally in range: nothing [ASSUMED, header].
*/
function stimulant(battle, unit, { damage, shots, dur, mods, key }) {
const t = frontAlly(battle, unit);
if (!t) return;
for (let i = 0; i < shots && t.alive && unit.alive; i++) {
battle.dealDamage(unit, t, { amount: damage, type: 'phys', isSkill: true, tags: ['skill', TAG_STIM] });
}
battle.fx('volley', { x: t.x, y: t.y, id: unit.id, target: t.id, n: shots, skill: key });
if (!(dur > 0)) return;
if (unit.alive) battle.addBuff(unit, { key: `${key}:self`, duration: dur, mods, source: unit, tags: ['skill'] });
if (t.alive && t !== unit) {
battle.addBuff(t, { key: `${key}:${unit.id}`, duration: dur, mods, source: unit, tags: ['skill'] });
battle.fx('buff', { x: t.x, y: t.y, id: t.id, kind: key });
}
}
/**
* 混合药物射击: one roll per attack on an enemy (its `attackId`), at its first hit — `all` (GEE-X stage 2+) for the four
* at once, else one of DICE uniformly; the heal and the statuses then, the ATK scale on every damage instance of it.
*/
function installDice(battle, unit, t0) {
const hr = num(t0.hp_ratio), scale = num(t0.atk_scale, 1), slug = num(t0.sluggish), stun = num(t0.stun), all = num(t0.prob);
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !d || !d.isAttack || d.isSplash || d.type === 'element' || !t || t.side !== 'enemy') return;
const m = unit.mem;
if (!d.attackId || m.haakDiceId !== d.attackId) {
m.haakDiceId = d.attackId;
m.haakDice = all > 0 && battle.rng.chance(all) ? DICE : [DICE[battle.rng.int(DICE.length)]];
for (const e of m.haakDice) {
if (e === 'heal' && hr > 0) battle.heal(unit, unit, unit.s.maxHp * hr, { self: true });
else if (e === 'sluggish' && slug > 0) battle.applyStatus(t, 'sluggish', { duration: slug, source: unit });
else if (e === 'stun' && stun > 0) battle.applyStatus(t, 'stun', { duration: stun, source: unit });
}
if (m.haakDice.includes('atk') && scale !== 1) battle.fx('crit', { x: t.x, y: t.y, id: unit.id });
}
if (m.haakDice.includes('atk')) d.amount *= scale;
}, { owner: unit });
}
export default {
char_225_haak: (bb, chess) => {
const t0 = talentBb(chess, 0); // 混合药物射击 (GEE-X stage 2+: + prob, its own numbers)
const t1 = talentBb(chess, 1); // 药剂扩散 (GEE-Y stage 2+: heal_scale 1.25, healinrange.heal_scale)
const hidden = moduleBb(chess); // GEE-X: sp_recovery_per_sec
const b1 = bbOf(chess, S1);
const stim = (id, key, mods) => {
const rec = skillRec(chess, id);
const b = rec?.bb ?? {};
const shots = Math.max(1, Math.floor(num(+(/攻击(\d+)次/.exec(String(rec?.desc ?? ''))?.[1]), SHOTS_FALLBACK)));
const dur = num(rec?.duration, 0);
return {
kind: 'duration',
onStart({ battle, unit }) { stimulant(battle, unit, { damage: num(b.damage, 500), shots, dur, mods: mods(b), key }); },
};
};
return {
skills: {
[S1]: { kind: 'duration', mods: { aspd: num(b1.attack_speed) } },
[S2]: stim(S2, 'haak:s2', (b) => ({ hpPct: num(b.max_hp), defPct: num(b.def) })),
[S3]: stim(S3, 'haak:s3', (b) => ({ atkPct: num(b.atk), aspd: num(b.attack_speed) })),
},
talents: [
{ install(battle, unit) { installDice(battle, unit, t0); } }, // 混合药物射击
{ install(battle, unit) { // 药剂扩散: healing received ×heal_scale; GEE-Y stage 2+: the deploy heal
statBuff(battle, unit, 'talent:haak:heal', { healingTakenMul: num(t1.heal_scale, 1) });
const hs = num(t1['healinrange.heal_scale']);
if (!(hs > 0)) return;
battle.on('deploy', (c) => {
if (c.unit !== unit || c.move || !up(unit)) return;
const t = frontAlly(battle, unit, true);
if (!t) return;
battle.heal(unit, t, unit.s.atk * hs);
battle.fx('heal', { x: t.x, y: t.y, id: t.id, src: unit.id });
}, { owner: unit, priority: -10 });
} },
],
install(battle, unit) {
// GEE-X "生命值高于80%时,技力自然回复速度+0.25/秒"
const sp = num(hidden.sp_recovery_per_sec);
if (sp > 0 && moduleOn(chess)) {
const pct = +(/生命值高于(\d+)%/.exec(String(chess?.trait?.moduleDesc ?? ''))?.[1]);
const thr = Number.isFinite(pct) ? pct / 100 : GEEX_HP_FALLBACK;
toggleBuff(battle, unit, 'module:haak:sp', () => unit.hp >= unit.s.maxHp * thr - 1e-9, { spRecoveryFlat: sp });
}
},
};
},
};