- op-shining.js 闪灵 (医师): S1 信条; S2 自动掩护 — the next heal gives its target a 屏障 of atk_scale × ATK for duration s (every damage type) and DEF +def while one of her barriers holds (PRTS 备注: barriers add up, own timers, older first, the DEF bonus does not stack); S3 教条力场 — allies in range DEF +def %; T1 黑恶魔的庇护 (allies in range DEF +60; PHY-Y stage 3: +100, 地面 units +40 more); T2 法典 (ASPD +10; PHY-X stage 3: +15, ATK +25 % with S2, SP +0.6/s with S3); PHY-Y heals on 地面 units ×1.15, PHY-X heals on allies below 50 % ×1.15. - op-cgbird.js 夜莺 (群愈师): S1 治疗强化·γ型; S2 法术护盾 — every target of the next heal gets an arts-only 屏障 (shieldType 'arts') and RES +flat; S3 圣域 — range y-4 (data ACTIVE_RANGE), allies in it RES ×(1 + v) and arts dodge; T1 白恶魔的庇护 (RES +15; RIN-X stage 3: healing received ×1.05); RIN-X range = its module grid, RIN-Y 4 heals; T2 幻影: its own talent (30 % / RIN-Y stage 3 40 % physical dodge, 3 % max HP 流失 per second), her stock (+2 / RIN-Y stage 3 +3 per deployment, at most 3 — PRTS 备注), a fallen board 幻影 back on its tile after its redeploy time using one and its DP. - op-saga.js 嵯峨 (尖兵): S1 冲锋号令·γ型 (SP_FULL); S2 除恶 — +4 DP, up to 6 ground enemies of the x-6 cross (重伤 ones included), the ones still 重伤 0.7 s later killed by her (PRTS 备注); S3 怒目 — +1 DP/s (20), +0.5 s interval, range +1, every blocked enemy, one more attack on a target below half HP; T1 劝善 — her damage is never lethal, an enemy at ≤ 1 HP becomes 重伤 (PRTS 术语释义: move −80 %, 禁用近战, 禁疗, no regeneration, a forced 无来源 death 10 s later, none while 消失), its killer +2 SP, she never selects a 重伤 enemy (profile skipEnemy); SOL-X stage 3 +15 % damage below half HP; T2 清明 (once per deployment below 40 %: 70 % physical dodge, 生命回复速度 5 % / SOL-Y stage 3 6.5 % — its blackboard — for 15 / 18 s); SOL-X block ATK / DEF +8 %; SOL-Y's 首次部署 cost cut has no battle effect. - kits/index.js OPERATOR_KIT_FILES: the three files, one line each. Golden (diy only; every other family unchanged): diy-003…008 are new — 闪灵, 夜莺 and 嵯峨 in every form × module × skill at tiers 5 and 6; the prototype battles moved from diy-003…006 to diy-009…012 (same pieces; their seed follows the index); diy-001 / 002 (推进之王) unchanged. Tests: test/content/op_shining.test.js (9), op_cgbird.test.js (10), op_saga.test.js (15).
232 lines
16 KiB
JavaScript
232 lines
16 KiB
JavaScript
// server/sim/content/kits/ops/op-saga.js — 嵯峨 (char_362_saga) 自选 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_362_saga, 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); gamedata_const ba.dying
|
||
// "重伤: 移速下降且无法被阻挡,10秒后自然死亡,被击杀后使击杀者回复数点技力"; PRTS 嵯峨 (劝善 备注 "本天赋第一部分实际效果为:
|
||
// 自身造成的伤害不会致命(至多使目标生命值降至1点),成功造成伤害后若目标生命值小于等于1则为目标附加重伤"; 清明 备注 "嵯峨每次
|
||
// 部署都可以触发一次本天赋"; S2 备注 "可选择重伤单位", "攻击附加延迟检测Buff:0.7s后进行检测,强制击杀持有重伤的目标(击杀来源为
|
||
// 嵯峨自身),随后解除目标的重伤"; S3 备注 "造成伤害前判断目标生命值"); PRTS 术语释义 重伤 ("重伤Buff包含:移动速度-80%,禁疗、
|
||
// 禁用近战,阻挡数、生命回复速度、生命回复速度(百分比)归零,从消失状态变为出现的瞬间立刻死亡(消失状态期间不会因重伤自然死亡)",
|
||
// "重伤造成的自然死亡为强制死亡(也即可以杀死持有不死的单位),视为无来源击杀", "重伤期间的单位无法再被施加重伤"); PRTS 异常效果
|
||
// 禁用近战 "敌人类单位无法进行阻挡攻击,无法被阻挡".
|
||
// - Trait (尖兵) "能够阻挡两个敌人": the profession default (block 2). SOL-X 何处寻驮? "阻挡敌人时攻击力和防御力各+8%"
|
||
// (trait bb atk / def): +atk / +def while she blocks an enemy. SOL-Y 身向云泥 "首次部署时部署费用-4" (hidden
|
||
// runtime_cost −4): the first deployment of a battle costs nothing in this mode ⇒ no effect (as 推进之王's SOL-Y).
|
||
// - T1 劝善: her damage is never lethal to an enemy (a `fatal` hook keeps it at 1 HP — every damage of hers: attacks, 除恶;
|
||
// not a 无来源 one, which has no source); after a damage of hers an enemy at ≤ 1 HP becomes 重伤 (the DYING buff, once:
|
||
// none while it holds one): move speed ×(1 + move_speed) (−80 %), unblockable (禁用近战: released by its blocker, no
|
||
// block attack), 禁疗 (healFree / noHeal), its HP regeneration zeroed; it dies `interval` s later (10) — a forced, 无来源
|
||
// death (battle.kill with no killer: no SP, 不死 does not hold it) — not while it is 消失 (hidden), and at once when it
|
||
// comes back from 消失. Whoever kills a 重伤 enemy gets `sp` SP (2; "击杀者技力+2", giveSp: not during a timed skill).
|
||
// "嵯峨不攻击重伤单位": the kit trait `skipEnemy` (targeting.js canTargetEnemy) — no normal attack, block target or
|
||
// trigger target of hers is a 重伤 enemy. SOL-X stage 2+ "对生命值低于一半的单位造成的伤害提升15%" (hidden hp_ratio /
|
||
// damage_scale): ×damage_scale on every damage of hers whose target is below hp_ratio before it.
|
||
// A shared-pool leader (boss rounds: its HP is the pool's, damage.js runs no `fatal` step for it) is outside 劝善
|
||
// [ASSUMED: her damage there is ordinary].
|
||
// - T2 清明 "生命值低于40%时,仅一次获得70%物理闪避和每秒回复5%的最大生命,持续15秒": once per deployment (PRTS 备注), the first
|
||
// time a damage leaves her below hp_ratio of her max HP: dodgePhys prob and hpRegenRatio for `duration` s — the
|
||
// blackboard key hp_recovery_per_sec_by_max_hp_ratio is the 生命回复速度(百分比) attribute (PRTS 异常效果 禁疗 / 术语释义
|
||
// 重伤 name it), as 宴 S1 / 山 / 幽灵鲨 / 蜜蜡 / 烛煌 apply it [ASSUMED: no 备注 of 清明 says so; 余's 闲云隐市 with the same
|
||
// key is still open, DESIGN 0.2.0.l3]. SOL-Y stage 2+: the talent change's numbers — at stage 3 the text reads 6 % (its
|
||
// potential-5 text 7 %) while the blackboard is 0.065: the game applies the blackboard, so 6.5 % per second.
|
||
// - S1 冲锋号令·γ型 (AUTO, no target): +cost DP at once; an AUTO skill acting on nobody fires as soon as its SP is full
|
||
// (`trigger: 'SP_FULL'`, as 推进之王's / 德克萨斯's kits cast the same skill — the owner's AUTO rule).
|
||
// - S2 除恶 (MANUAL, 2 charges, data SKILL_RANGE on its x-6 cross: any enemy there, 无视其不可选中): +cost DP; the up to
|
||
// S2_MAX (6, the text's — no blackboard key) selectable ground enemies of the cross ("地面敌人"; a 重伤 one included:
|
||
// "可选择重伤单位"), best targets first (her priority) [ASSUMED order], take atk_scale × ATK physical (a skill damage;
|
||
// 劝善 applies); S2_CHECK s later each of them still 重伤 is killed (killer 嵯峨: she gets its SP, stats and bounty; a
|
||
// forced kill — battle.kill, no `fatal` step), then its 重伤 is lifted (should a kill handler have revived it).
|
||
// - S3 怒目 (MANUAL, 20 s, data ACTIVE_RANGE on her 1-1 + 1): +cost DP every interval s while it runs (value in all; the
|
||
// rest of value at its natural end), attack interval +base_attack_time s (+0.5 on her 1.05), 攻击距离 +1
|
||
// (targeting.rangeExtend), ATK +atk, every enemy she blocks at once (hitAllBlocked; one target in range while she
|
||
// blocks none), and one more attack on each target that was below attack@hp_ratio before the hit (PRTS 备注: an HP
|
||
// snapshot at beforeAttack; the extra attack is a forced one a tick later, no 重伤 target).
|
||
// Melee physical, ground-only (canHitFly false), ground enemies target her normally (no 起飞 / 迷彩 / 隐匿).
|
||
|
||
import { num, talentBb, moduleBb, traitBb, skillRec, toggleBuff, batMod, giveSp, up } from '../shared/tier1.js';
|
||
import { absoluteRangeKeys, canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||
|
||
const S1 = 'skcom_charge_cost[3]';
|
||
const S2 = 'skchr_saga_2';
|
||
const S3 = 'skchr_saga_3';
|
||
/** The 重伤 buff (gamedata_const ba.dying): one per enemy — "重伤期间的单位无法再被施加重伤". */
|
||
export const DYING = 'saga:dying';
|
||
/** S2 除恶 备注 "攻击附加延迟检测Buff:0.7s后进行检测" (no blackboard key). */
|
||
export const S2_CHECK = 0.7;
|
||
/** S2 除恶 "对十字范围内最多6名地面敌人" — the text's number when it cannot be read (no blackboard key). */
|
||
const S2_MAX = 6;
|
||
/** The x-6 cross (skill range) when the data carries no grid. */
|
||
const X6 = Object.freeze([[2, 0], [1, 0], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, 0], [-2, 0]]);
|
||
/** 除恶's selection: ground enemies only ("地面敌人"), 重伤 ones included (no skipEnemy). */
|
||
const GROUND = Object.freeze({ canHitFly: false, groundOnly: true });
|
||
|
||
/** Is `e` 重伤 (whoever put it there)? */
|
||
export const isDying = (e) => !!(e && typeof e.findBuff === 'function' && e.findBuff(DYING));
|
||
|
||
/**
|
||
* 重伤 on enemy `e` from `saga` (PRTS 术语释义 重伤): move speed ×(1 + ms), unblockable (禁用近战), 禁疗, no HP regeneration;
|
||
* a forced, 无来源 death `life` s later — never while 消失, at once when it comes back from 消失. The killer of a 重伤
|
||
* enemy gets `sp` SP (the talent's kill hook reads `data.sp`). Nothing when it already holds one.
|
||
*/
|
||
function applyDying(battle, saga, e, { sp, life, ms }) {
|
||
if (!e || !e.alive || isDying(e)) return;
|
||
battle.addBuff(e, {
|
||
key: DYING, source: saga, visible: true,
|
||
mods: { moveMul: Math.max(0, 1 + ms), hpRegen: -1e9 },
|
||
flags: { unblockable: true, healFree: true, noHeal: true },
|
||
data: { sp, until: battle.time + life, hidden: !!e.hidden },
|
||
onTick: ({ battle: b, unit: x, buff }) => {
|
||
const d = buff.data;
|
||
if ((d.hidden && !x.hidden) || (!x.hidden && b.time + 1e-9 >= d.until)) { b.kill(x, null); return; }
|
||
d.hidden = !!x.hidden;
|
||
},
|
||
});
|
||
battle.fx('dying', { x: e.x, y: e.y, id: e.id, from: saga.id });
|
||
}
|
||
|
||
export default {
|
||
char_362_saga: (bb, chess) => {
|
||
const b1 = skillRec(chess, S1)?.bb ?? {}, b2 = skillRec(chess, S2)?.bb ?? {}, b3 = skillRec(chess, S3)?.bb ?? {};
|
||
const s2 = skillRec(chess, S2);
|
||
const t0 = talentBb(chess, 0); // 劝善: move_speed, sp, interval
|
||
const t1 = talentBb(chess, 1); // 清明: hp_ratio, hp_recovery_per_sec_by_max_hp_ratio, prob, duration
|
||
const hidden = moduleBb(chess); // SOL-X stage 2+: hp_ratio, damage_scale; SOL-Y: runtime_cost (no effect)
|
||
const tb = traitBb(chess); // SOL-X: atk, def while blocking
|
||
const picked = chess?.skill?.skillId ?? null;
|
||
const maxHit = Math.max(1, Math.floor(num(/最多(\d+)名/.exec(String(s2?.desc ?? ''))?.[1], S2_MAX)));
|
||
return {
|
||
// 劝善 "嵯峨不攻击重伤单位"
|
||
trait: { skipEnemy: isDying },
|
||
skills: {
|
||
[S1]: {
|
||
kind: 'instant', trigger: 'SP_FULL',
|
||
onStart({ battle, unit }) {
|
||
const n = num(b1.cost);
|
||
battle.addDp(unit.ownerId, n);
|
||
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
|
||
},
|
||
},
|
||
[S2]: {
|
||
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||
onStart({ battle, unit }) {
|
||
const n = num(b2.cost);
|
||
battle.addDp(unit.ownerId, n);
|
||
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
|
||
const keys = absoluteRangeKeys(s2?.rangeGrid ?? X6, unit.tileR, unit.tileC, unit.dir, 0);
|
||
const list = battle.enemiesInKeys(keys, unit, GROUND);
|
||
sortEnemyTargets(battle, unit, list, unit.profile?.priority ?? null);
|
||
const hit = list.slice(0, maxHit);
|
||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 2, id: unit.id, skill: 'saga:cleanse', n: hit.length });
|
||
for (const e of hit) {
|
||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(b2.atk_scale, 1), type: 'phys', isSkill: true, tags: ['saga:s2'] });
|
||
if (!e.alive) continue;
|
||
battle.addBuff(e, {
|
||
key: 'saga:s2check', refresh: 'independent', maxStacks: 99, duration: S2_CHECK, source: unit,
|
||
onExpire: ({ battle: b, unit: x }) => {
|
||
if (!x.alive || !isDying(x)) return;
|
||
b.kill(x, unit);
|
||
if (x.alive) b.removeBuff(x, DYING);
|
||
},
|
||
});
|
||
}
|
||
},
|
||
},
|
||
[S3]: {
|
||
kind: 'duration',
|
||
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
|
||
targeting: { rangeExtend: num(b3.ability_range_forward_extend) },
|
||
attack: { hitAllBlocked: true },
|
||
onStart({ unit }) { unit.mem.sagaDp = { acc: 0, got: 0 }; },
|
||
onTick({ battle, unit, dt }) {
|
||
const st = unit.mem.sagaDp;
|
||
const iv = num(b3.interval, 1), n = num(b3.cost), total = num(b3.value);
|
||
if (!st || !(iv > 0) || !(n > 0)) return;
|
||
st.acc += dt;
|
||
while (st.acc + 1e-9 >= iv && st.got + n <= total + 1e-9) {
|
||
st.acc -= iv;
|
||
st.got += n;
|
||
battle.addDp(unit.ownerId, n);
|
||
}
|
||
},
|
||
onEnd({ battle, unit, reason }) {
|
||
const st = unit.mem.sagaDp;
|
||
unit.mem.sagaDp = null;
|
||
// the last grant falls on the very end of its 20 s
|
||
if (st && reason === 'duration' && num(b3.value) - st.got > 1e-9) battle.addDp(unit.ownerId, num(b3.value) - st.got);
|
||
},
|
||
},
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 劝善
|
||
const sp = num(t0.sp), life = num(t0.interval, 10), ms = num(t0.move_speed, -0.8);
|
||
// "自身造成的伤害不会致命(至多使目标生命值降至1点)" — before any other saver: it never was lethal
|
||
battle.on('fatal', (c) => {
|
||
if (c.source === unit && c.unit && c.unit.side === 'enemy' && !c.prevented) c.prevented = true;
|
||
}, { owner: unit, priority: 100 });
|
||
// "成功造成伤害后若目标生命值小于等于1则为目标附加重伤"
|
||
battle.on('damaged', (c) => {
|
||
const e = c.target;
|
||
if (c.source !== unit || !e || e.side !== 'enemy' || !e.alive || e.bossPool || c.type === 'element' || e.hp > 1) return;
|
||
applyDying(battle, unit, e, { sp, life, ms });
|
||
}, { owner: unit });
|
||
// "被击杀后使击杀者回复数点技力" — the killer of a 重伤 enemy she marked (none for the 无来源 natural death)
|
||
battle.on('kill', (c) => {
|
||
const d = c.victim && typeof c.victim.findBuff === 'function' ? c.victim.findBuff(DYING) : null;
|
||
if (!d || d.source !== unit || !c.killer || c.killer.side !== 'ally') return;
|
||
giveSp(c.killer, num(d.data?.sp));
|
||
}, { owner: unit });
|
||
// SOL-X stage 2+: "对生命值低于一半的单位造成的伤害提升15%"
|
||
const hr = num(hidden.hp_ratio), ds = num(hidden.damage_scale, 1);
|
||
if (hr > 0 && ds !== 1) {
|
||
battle.on('hit', (c) => {
|
||
if (c.source === unit && c.target && c.target.side === 'enemy' && c.target.hpRatio < hr) c.dmg.mul *= ds;
|
||
}, { owner: unit });
|
||
}
|
||
} },
|
||
{ install(battle, unit) { // 清明: once per deployment below hp_ratio — dodgePhys prob, hpRegenRatio, `duration` s
|
||
const hr = num(t1.hp_ratio), p = num(t1.prob), regen = num(t1.hp_recovery_per_sec_by_max_hp_ratio), dur = num(t1.duration);
|
||
if (!(hr > 0) || !(dur > 0)) return;
|
||
battle.on('damaged', (c) => {
|
||
if (c.target !== unit || !up(unit) || unit.mem.qingmingAt === unit.deploySeq) return;
|
||
if (!(unit.hp < unit.s.maxHp * hr)) return;
|
||
unit.mem.qingmingAt = unit.deploySeq;
|
||
battle.addBuff(unit, { key: 'talent:saga:qingming', duration: dur, mods: { dodgePhys: p, hpRegenRatio: regen }, visible: true, tags: ['talent'] });
|
||
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'saga:qingming' });
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
// SOL-X 何处寻驮?: 阻挡敌人时攻击力和防御力各+8%
|
||
const ma = num(tb.atk), md = num(tb.def);
|
||
if (ma || md) toggleBuff(battle, unit, 'trait:saga:block', () => unit.blocking.length > 0, { atkPct: ma, defPct: md });
|
||
// 怒目: one more attack on each target below attack@hp_ratio (checked before the hit) — a forced attack a tick later
|
||
if (picked !== S3) return;
|
||
const hp3 = num(b3['attack@hp_ratio']);
|
||
if (!(hp3 > 0)) return;
|
||
let half = null;
|
||
battle.on('beforeAttack', (c) => {
|
||
if (c.attacker !== unit || unit.mem.sagaExtra || !unit.skill?.active) return;
|
||
half = (c.targets || []).filter((e) => e && e.side === 'enemy' && e.hpRatio < hp3);
|
||
}, { owner: unit, priority: -100 });
|
||
battle.on('attack', (c) => {
|
||
if (c.attacker !== unit || unit.mem.sagaExtra) return;
|
||
const list = half;
|
||
half = null;
|
||
if (!list || !list.length) return;
|
||
const dep = unit.deploySeq;
|
||
battle.after(0, () => {
|
||
if (!up(unit) || unit.deploySeq !== dep || !unit.canAct || unit.s.flags.disarm) return;
|
||
const t = list.filter((e) => e.alive && canTargetEnemy(unit, e, unit.profile));
|
||
if (!t.length) return;
|
||
unit.mem.sagaExtra = true;
|
||
try { battle.forceAttack(unit, t, { noAmmo: true }); } finally { unit.mem.sagaExtra = false; }
|
||
}, { owner: unit });
|
||
}, { owner: unit });
|
||
},
|
||
};
|
||
},
|
||
};
|