237 lines
15 KiB
JavaScript
237 lines
15 KiB
JavaScript
// server/sim/content/kits/ops/op-lmlee.js — 老鲤 (char_322_lmlee) 自选 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_322_lmlee, 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). Full potential (the owner's decision of 2026-10-07).
|
||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json) and PRTS 老鲤 (有备无患
|
||
// 备注 "“费用充足”指的是当前费用≥触发天赋所需要消耗的费用 / 自身已拥有本天赋的抵消异常效果时,额外消耗费用效果不会重复触发。本天赋
|
||
// 不会对我方单位造成晕眩"; S2 备注 "标记爆炸半径1,可对空 / 撤退老鲤时,如果老鲤没有阻挡存在标记的目标,则标记会消失且不产生任何
|
||
// 效果 / …①老鲤被击倒 ②老鲤在阻挡被标记的目标时撤退 ③在标记消失前再次释放本技能"; S3 备注 "闪避效果以伤害来源当前位置为准,
|
||
// 无来源伤害始终被视作“来自范围外”"), PRTS 分支特性信息 行商 ("可以且优先攻击自身阻挡的单位" — the engine's blocker rule).
|
||
// - Trait (行商) "在场时每3秒消耗3点部署费用(不足时自动撤退)": the profession default (professions.js installMerchant, trait
|
||
// bb interval / cost). Module MER-X “无问吉凶” overrides it with 2 DP (trait bb cost −2). Module MER-Y “但思善恶” adds
|
||
// "每次特性消耗费用时攻击力+4%,最多可以叠加5次" (hidden module talent atk / max_stack_cnt): every trait payment +atk ATK,
|
||
// up to max_stack_cnt stacks, until he leaves the field. "再部署时间减少" is in the stats; "撤退时不返还部署费用" has no
|
||
// battle effect here (no manual retreat).
|
||
// - T1 和气生财 "老鲤阻挡目标时,使其攻击速度-14,且自身攻击速度+14;当周围八格内仅存在一个敌人时效果翻倍" (full
|
||
// potential: ∓15; lmlee_t_1[enemy] / [self].attack_speed, cnt): while he blocks, the blocked enemy ASPD −15 and his own +15, both ×2
|
||
// while exactly `cnt` enemy stands on the 3 × 3 around him (his tile included [ASSUMED: the enemy he blocks stands
|
||
// there], flyers too). MER-Y stage 3: ∓21.
|
||
// - T2 有备无患 "特性消耗费用时,若费用足够则改为消耗5费用,抵消自身受到的下一次晕眩/冻结,并使攻击来源晕眩3秒" (extra_cost,
|
||
// stun): a trait payment while DP ≥ |extra_cost| and no guard held pays |extra_cost| instead (the `merchantPay` hook) and
|
||
// gives him the guard; the guard cancels the next 晕眩 / 冻结 on him (a cold-on-cold 冻结 too) and stuns its source for
|
||
// `stun` s — an enemy only (PRTS: never an ally). MER-X stage 3: 4 DP, 4 s.
|
||
// - S1 小惩大诫 (AUTO, 持续时间无限): ATK +atk, 法术闪避 prob. An AUTO skill acting on himself fires as soon as its SP is
|
||
// full (`trigger: 'SP_FULL'`, the owner's AUTO rule, kits/README.md checklist 5); the data's DEFAULT would wait for an
|
||
// attack.
|
||
// - S2 驱凶辟邪 (MANUAL, data DEFAULT): passive ASPD +attack_speed. Cast as he is about to attack: the target of that
|
||
// attack is marked (taunt_level: our units select it first — "使其更易受我方攻击"); paper_duration s later the mark bursts
|
||
// for (default_atk_scale + n × factor_atk_scale) × his ATK arts damage on every enemy within MARK_RADIUS of it (air
|
||
// units too; 中点判定 around the target, the selectable enemies), n = the damage instances our side dealt to the target
|
||
// meanwhile (≤ max_stack_cnt; at the cap, or when the target is knocked out, it bursts at once). PRTS 备注: it also
|
||
// bursts when he is knocked out, when he leaves while blocking the marked enemy, and when he casts again; when he leaves
|
||
// otherwise it vanishes. Counted: every `damaged` instance of an ally (credit included) on it but 元素损伤 and 流失
|
||
// [ASSUMED: a hit absorbed by a shield counts]. The burst after a knock-out uses his ATK of the last tick on the field
|
||
// [ASSUMED].
|
||
// - S3 贵客盈门 (AUTO, 持续时间无限; SP_FULL as S1): range x-4, ATK / DEF +atk / +def, taunt +taunt_level; each attack pushes
|
||
// every other enemy of his range with attack@force (小力 = 0) at attack@prob_knockback (radial from him [ASSUMED: the text
|
||
// gives no direction — knockback[relative]]); prob to dodge each physical / arts damage whose source stands outside his
|
||
// range at that moment (无来源 damage always counts as from outside).
|
||
|
||
import { num, talentBb, moduleBb, skillRec, statBuff, up } from '../shared/tier1.js';
|
||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||
import { bodyInKeys } from '../../../body.js';
|
||
import { isHpLoss } from '../../../damage.js';
|
||
import { acquireTargets, effectiveProfile } from '../../../ai.js';
|
||
|
||
const S1 = 'skchr_lmlee_1';
|
||
const S2 = 'skchr_lmlee_2';
|
||
const S3 = 'skchr_lmlee_3';
|
||
/** The 3 × 3 around him (range_table x-4): 和气生财's 周围八格 and S3's range when the data carries no grid. */
|
||
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
|
||
/** S2 驱凶辟邪's burst radius (PRTS 备注 "标记爆炸半径1"; no blackboard key). */
|
||
export const MARK_RADIUS = 1;
|
||
/** 有备无患's guard on him (cancels the next 晕眩 / 冻结). */
|
||
export const GUARD_KEY = 'lmlee:guard';
|
||
/** Tag of S2's burst (not counted as a damage instance on the mark). */
|
||
const BURST_TAG = 'lmlee:burst';
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
const markKey = (unit) => `lmlee:mark:${unit.id}`;
|
||
|
||
/** End the mark `m` without a burst (he left without blocking the marked enemy, the target left the field). */
|
||
function dropMark(battle, unit, m) {
|
||
m.alive = false;
|
||
if (m.target) battle.removeBuff(m.target, markKey(unit));
|
||
}
|
||
|
||
/** S2's burst of mark `m`: (scale + n × factor) × ATK arts on the selectable enemies within MARK_RADIUS of the target. */
|
||
function burstMark(battle, unit, m, b2, atk = null) {
|
||
if (!m.alive) return;
|
||
m.alive = false;
|
||
const t = m.target;
|
||
battle.removeBuff(t, markKey(unit));
|
||
const x = t.x, y = t.y;
|
||
const amount = (atk ?? unit.s.atk) * (num(b2.default_atk_scale) + m.n * num(b2.factor_atk_scale));
|
||
battle.fx('explode', { x, y, r: MARK_RADIUS, id: unit.id, dmgType: 'arts', skill: 'lmlee_2', n: m.n });
|
||
if (!(amount > 0)) return;
|
||
for (const e of battle.foesInRadius(x, y, MARK_RADIUS, true)) {
|
||
if (e.alive) battle.dealDamage(unit, e, { amount, type: 'arts', isSkill: true, tags: ['skill', BURST_TAG] });
|
||
}
|
||
}
|
||
|
||
export default {
|
||
char_322_lmlee: (bb, chess) => {
|
||
const t0 = talentBb(chess, 0); // 和气生财 (MER-Y stage 2+: its change)
|
||
const t1 = talentBb(chess, 1); // 有备无患 (MER-X stage 2+: its change)
|
||
const hidden = moduleBb(chess); // MER-Y: atk / max_stack_cnt per trait payment
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
const s3 = skillRec(chess, S3);
|
||
const tauntLv = num(b2.taunt_level, 1);
|
||
const maxN = Math.max(1, Math.floor(num(b2.max_stack_cnt, 20)));
|
||
return {
|
||
skills: {
|
||
[S1]: { kind: 'toggle', trigger: { rule: 'SP_FULL' }, mods: { atkPct: num(b1.atk), dodgeArts: num(b1.prob) } },
|
||
[S2]: {
|
||
kind: 'instant',
|
||
// "主动开启:标记目标": the target of the attack he is about to make (the basic strategy casts on that attack)
|
||
onStart({ battle, unit }) {
|
||
const t = acquireTargets(battle, unit, effectiveProfile(unit)).find((e) => e && e.side === 'enemy' && e.alive);
|
||
if (!t) return;
|
||
const prev = unit.mem.lmleeMark;
|
||
if (prev && prev.alive) burstMark(battle, unit, prev, b2); // ③ cast again before the mark ends
|
||
const m = { target: t, n: 0, alive: true, atk: unit.s.atk };
|
||
unit.mem.lmleeMark = m;
|
||
const dur = num(b2.paper_duration, 5);
|
||
battle.addBuff(t, { key: markKey(unit), duration: dur + 0.1, mods: tauntLv ? { taunt: tauntLv } : null, source: unit, tags: ['skill'] });
|
||
battle.fx('mark', { x: t.x, y: t.y, id: t.id, src: unit.id, skill: 'lmlee_2' });
|
||
battle.after(dur, () => burstMark(battle, unit, m, b2), { owner: unit });
|
||
},
|
||
},
|
||
[S3]: {
|
||
kind: 'toggle',
|
||
trigger: { rule: 'SP_FULL' },
|
||
mods: { atkPct: num(b3.atk), defPct: num(b3.def), taunt: num(b3.taunt_level) },
|
||
targeting: { rangeGrid: s3?.rangeGrid ?? X4 },
|
||
attack: {
|
||
// "攻击小力度推开范围内除当前目标外的敌人"
|
||
onHit({ battle, unit, target }) {
|
||
const p = num(b3['attack@prob_knockback'], 1), force = num(b3['attack@force'], 0);
|
||
if (!(p > 0) || (p < 1 && !battle.rng.chance(p))) return;
|
||
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) {
|
||
if (e !== target && e.alive) battle.push(e, force, { from: unit });
|
||
}
|
||
},
|
||
},
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 和气生财: while blocking — the blocked enemy ASPD −, his own +; ×2 with one enemy around
|
||
const foe = num(t0['lmlee_t_1[enemy].attack_speed']), self = num(t0['lmlee_t_1[self].attack_speed']);
|
||
const alone = Math.max(1, Math.floor(num(t0.cnt, 1)));
|
||
if (!foe && !self) return;
|
||
const selfKey = 'talent:lmlee:self', foeKey = `talent:lmlee:foe:${unit.id}`;
|
||
battle.on('tick', () => {
|
||
const blocked = up(unit) ? unit.blocking.filter((e) => e && e.alive) : [];
|
||
if (!blocked.length) {
|
||
if (unit.findBuff(selfKey)) battle.removeBuff(unit, selfKey);
|
||
return;
|
||
}
|
||
const around = new Set(absoluteRangeKeys(X4, unit.tileR, unit.tileC, unit.dir, 0));
|
||
let n = 0;
|
||
for (const e of battle.enemies) if (e.alive && !e.hidden && bodyInKeys(e, around)) n++;
|
||
const k = n === alone ? 2 : 1;
|
||
const cur = unit.findBuff(selfKey);
|
||
if (self && (!cur || cur.mods?.aspd !== self * k)) battle.addBuff(unit, { key: selfKey, mods: { aspd: self * k }, tags: ['talent'] });
|
||
if (foe) for (const e of blocked) battle.addBuff(e, { key: foeKey, duration: 0.1, mods: { aspd: foe * k }, source: unit, tags: ['talent'] });
|
||
}, { owner: unit });
|
||
} },
|
||
{ install(battle, unit) { // 有备无患: pay |extra_cost| for the guard; the guard cancels the next 晕眩 / 冻结
|
||
const pay = Math.abs(num(t1.extra_cost)), stun = num(t1.stun);
|
||
if (!(pay > 0)) return;
|
||
battle.on('merchantPay', (ctx) => {
|
||
if (ctx.unit !== unit || ctx.cancel || unit.findBuff(GUARD_KEY)) return;
|
||
const pl = battle.getPlayer(unit.ownerId);
|
||
if (!pl || pl.dp + 1e-9 < pay) return; // "费用充足": DP ≥ the talent's cost
|
||
ctx.cost = pay;
|
||
battle.addBuff(unit, { key: GUARD_KEY, tags: ['talent'] });
|
||
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id, kind: 'lmlee' });
|
||
}, { owner: unit, priority: 10 });
|
||
battle.on('beforeStatus', (ctx) => {
|
||
if (ctx.target !== unit || ctx.cancel || (ctx.status !== 'stun' && ctx.status !== 'freeze') || !unit.findBuff(GUARD_KEY)) return;
|
||
ctx.cancel = true;
|
||
battle.removeBuff(unit, GUARD_KEY);
|
||
const src = ctx.source;
|
||
if (stun > 0 && src && src.side === 'enemy' && src.alive) battle.applyStatus(src, 'stun', { duration: stun, source: unit });
|
||
battle.fx('counter', { x: unit.x, y: unit.y, id: unit.id });
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
// MER-Y “但思善恶”: every trait payment ATK +atk (≤ max_stack_cnt) — counted when the payment goes through (after
|
||
// 有备无患 set its cost)
|
||
const ma = num(hidden.atk);
|
||
if (ma > 0) {
|
||
const cap = Math.max(1, Math.floor(num(hidden.max_stack_cnt, 5)));
|
||
battle.on('merchantPay', (ctx) => {
|
||
if (ctx.unit !== unit || ctx.cancel) return;
|
||
const pl = battle.getPlayer(unit.ownerId);
|
||
const c = Number.isFinite(ctx.cost) ? Math.max(0, ctx.cost) : 0;
|
||
if (!pl || pl.dp < c) return;
|
||
battle.addBuff(unit, { key: 'trait:lmlee:merY', refresh: 'stack', stacks: 1, maxStacks: cap, mods: { atkPct: ma }, tags: ['trait'] });
|
||
}, { owner: unit, priority: -10 });
|
||
}
|
||
const sid = unit.skill?.id;
|
||
if (sid === S2) {
|
||
statBuff(battle, unit, 'skill:lmlee:s2passive', { aspd: num(b2.attack_speed) }); // "被动效果:攻击速度+15"
|
||
// the mark: every damage instance of our side on it, its target's knock-out, his leaving the field
|
||
battle.on('damaged', (ctx) => {
|
||
const m = unit.mem.lmleeMark;
|
||
if (!m || !m.alive || ctx.target !== m.target) return;
|
||
const who = ctx.source ?? ctx.credit;
|
||
const d = ctx.dmg;
|
||
if (!who || who.side !== 'ally' || !d || ctx.type === 'element' || isHpLoss(d) || d.tags?.includes(BURST_TAG)) return;
|
||
m.n = Math.min(maxN, m.n + 1);
|
||
if (m.n >= maxN) burstMark(battle, unit, m, b2);
|
||
}, { owner: unit, priority: -20 });
|
||
battle.on('tick', () => {
|
||
const m = unit.mem.lmleeMark;
|
||
if (!m || !m.alive || !up(unit)) return;
|
||
m.blocked = m.target.blockedBy === unit; // read when he leaves (his block list is gone by then)
|
||
m.atk = unit.s.atk;
|
||
}, { owner: unit });
|
||
battle.on('death', (ctx) => {
|
||
const m = unit.mem.lmleeMark;
|
||
if (!m || !m.alive) return;
|
||
if (ctx.unit === m.target) {
|
||
if (ctx.reason === 'killed') burstMark(battle, unit, m, b2);
|
||
else dropMark(battle, unit, m);
|
||
} else if (ctx.unit === unit) {
|
||
// ① knocked out, ② leaving while blocking the marked enemy ⇒ bursts; else it vanishes
|
||
if (ctx.reason === 'killed' || m.blocked) burstMark(battle, unit, m, b2, m.atk);
|
||
else dropMark(battle, unit, m);
|
||
}
|
||
}, { owner: unit, priority: 5 });
|
||
}
|
||
if (sid === S3) {
|
||
// "有55%概率闪避来自范围外的物理或法术伤害" (the source's position now; 无来源 = from outside)
|
||
const p = num(b3.prob);
|
||
if (p > 0) {
|
||
battle.on('hit', (ctx) => {
|
||
const d = ctx.dmg;
|
||
if (ctx.target !== unit || !unit.skill?.active || !d || d.cancel || !d.canDodge || (d.type !== 'phys' && d.type !== 'arts')) return;
|
||
const src = ctx.source;
|
||
if (src && bodyInKeys(src, unit.rangeKeySet ?? new Set(unit.rangeKeys ?? []))) return;
|
||
if (battle.rng() >= p) return;
|
||
d.cancel = true;
|
||
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
|
||
if (battle.hasHook('dodge')) battle.emit('dodge', { source: ctx.credit ?? src, target: unit, dmg: d });
|
||
}, { owner: unit, priority: -50 });
|
||
}
|
||
}
|
||
},
|
||
};
|
||
},
|
||
};
|