159 lines
11 KiB
JavaScript
159 lines
11 KiB
JavaScript
// server/sim/content/kits/ops/op-cgbird.js — 夜莺 (char_179_cgbird) 自选 operator kit: 6★ 群愈师 (医疗), an owned-6★ pick of
|
||
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, both modules at every form and her summon 幻影.
|
||
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||
//
|
||
// Forms (data/backups.json units.char_179_cgbird, 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; the 幻影 token record
|
||
// backups.json tokens.token_10003_cgbird_bird with its owner-form / module variants) and PRTS 夜莺 (S2 备注
|
||
// "多个屏障吸收量可叠加,持续时间独立计算,优先消耗先生成的屏障,法术抗性增加效果不可叠加"; 转瞬即逝的幻影 备注 "幻影的持有上限为3个,
|
||
// 每次部署夜莺时补充2个幻影 / Y模组2级时…补充3个 / 夜莺离场时,已部署的幻影会留在场上"); PRTS 卫戍协议/帮助 §作战阶段 (summons:
|
||
// "所有手动部署的召唤物,无视所属干员的持有状态,不消耗持有数量,作战开始时立即部署一次", "若战场区初始部署有召唤物,若召唤物在
|
||
// 战斗期间退场,将在满足条件后立即原地再部署1个").
|
||
// - Trait (群愈师) "同时恢复三个友方单位的生命": the profession default (3 heals). Module RIN-Y “莺歌” "同时恢复四个友方单位的
|
||
// 生命" (hidden module talent attack@max_target / skill@max_target 4): 4 heals (kit trait, as 调香师's RIN-Y). RIN-X
|
||
// “封闭的希望” "攻击范围扩大": her range becomes the module's own grid (its range-only talent change, talentIndex −1: y-2
|
||
// plus the centre tile [0,3] — PRTS y-3), as shared/loadoutRecord.js attackRangeGrid draws it; 圣域 keeps its own y-4.
|
||
// - T1 白恶魔的庇护 "攻击范围内的友方单位法术抗性+15" (full potential: +17): every ally (operators and summons) standing in
|
||
// her current attack range — her own tile included — RES +magic_resistance (flat). RIN-X stage 2+ "…且受到的治疗效果提升5%" (talent change
|
||
// heal_scale): also healing received ×heal_scale there. Refreshed every 0.2 s; two sources keep the strongest.
|
||
// - T2 转瞬即逝的幻影 (summon token_10003_cgbird_bird: no attack, blocks nothing, RES 75, taunt level 1 — its stats — and
|
||
// its talent "30%的物理闪避…每秒流失3%的最大生命": dodgePhys prob, a 流失 of hp_ratio × its max HP every second; RIN-Y
|
||
// stage 3 "40%的物理闪避": the module variant's talent). The 幻影 is a hand piece (the match's per-player shop): a placed
|
||
// one deploys with the board at the battle start for free and without using her stock. Each deployment of 夜莺 adds
|
||
// cnt (2; RIN-Y stage 2+: 3) to her stock, at most PHANTOM_HOLD_CAP; a placed 幻影 that falls comes back on its own tile
|
||
// once its redeploy time (respawnTime) has passed and she holds one — using one, paying its DP cost as a deployment
|
||
// does [ASSUMED: "满足条件" = a 幻影 held, its redeploy time, the DP], whether 夜莺 is on the field or not (deployed
|
||
// 幻影 stay when she leaves: PRTS 备注). No tokens.js kit: its talent and its return are this kit's (owner-coupled,
|
||
// as tokens.js "Managed mode"), applied at each of its deployments.
|
||
// - S1 治疗强化·γ型 (MANUAL, 30 s, data DEFAULT — heal skill): ATK +atk.
|
||
// - S2 法术护盾 (AUTO, 2 charges, data DEFAULT — a "next heal" waits for her heal, as 闪灵's S2): "下次治疗使所有目标获得一个
|
||
// 持续N秒的屏障,屏障能吸收相当于夜莺攻击力X%的法术伤害,同时使目标法术抗性+Y" — every target of that heal (3 / 4) gets a
|
||
// 屏障 of atk_scale × her ATK for duration s that absorbs arts damage only (buff shieldType 'arts', damage.js
|
||
// absorbShields) and RES +magic_resistance (flat) while one of her barriers lasts (op-shining.js grantBarrier: each
|
||
// barrier its own timer, older ones first, the RES bonus not stacking) [ASSUMED: the bonus goes with the last barrier].
|
||
// - S3 圣域 (MANUAL, 60 s, data ACTIVE_RANGE on its y-4 — the owner's rule: an injured ally inside it): range y-4 while it
|
||
// runs, ATK +atk; every ally in that range RES +magic_resistance (×(1 + v): "法术抗性+105%") and dodgeArts prob
|
||
// ("获得20%的法术闪避"), refreshed every 0.2 s.
|
||
// Medic: heals only (dmgType heal), ranged, no anti-air question; ground enemies target her normally (no 起飞 / 迷彩 / 隐匿).
|
||
|
||
import { num, talentBb, moduleBb, moduleOn, skillRec } from '../shared/tier1.js';
|
||
import { rangeAura, grantBarrier } from './op-shining.js';
|
||
|
||
const S1 = 'skcom_heal_up[3]';
|
||
const S2 = 'skchr_cgbird_2';
|
||
const S3 = 'skchr_cgbird_3';
|
||
export const PHANTOM = 'token_10003_cgbird_bird';
|
||
/** PRTS 转瞬即逝的幻影 备注 "幻影的持有上限为3个" (no blackboard key). */
|
||
export const PHANTOM_HOLD_CAP = 3;
|
||
/** Seconds between two tries of a fallen 幻影 to come back (tile busy, DP short, none held). */
|
||
const RETRY = 0.25;
|
||
|
||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||
|
||
/**
|
||
* RIN-X "攻击范围扩大": the picked module's own range grid (its range-only talent change, talentIndex −1), or null — the
|
||
* rule of shared/loadoutRecord.js attackRangeGrid (the card / board range) and shared/tier5.js moduleRangeUp.
|
||
*/
|
||
function moduleRange(chess) {
|
||
if (!chess?.isGolden || !moduleOn(chess) || !/攻击范围扩大/.test(String(chess?.trait?.moduleDesc ?? ''))) return null;
|
||
const rec = (chess.modules ?? []).find((m) => m && m.uniEquipId === chess.module.id);
|
||
const g = (rec?.talentChanges ?? []).find((t) => t && t.talentIndex === -1 && Array.isArray(t.rangeGrid) && t.rangeGrid.length)?.rangeGrid;
|
||
return g ? g.map((p) => [p[0], p[1]]) : null;
|
||
}
|
||
|
||
/**
|
||
* The 幻影's own talent blackboard (`prob`, `hp_ratio`) from its def at the owner's form and module: the last talent
|
||
* holding `prob` — a module's upgrade (RIN-Y stage 3: 40 %) follows the base talent in the variant data.
|
||
* @param {object} def normalised token def
|
||
*/
|
||
export function phantomTalent(def) {
|
||
let out = {};
|
||
for (const t of def?.talents ?? []) if (t && t.bb && t.bb.prob != null) out = t.bb;
|
||
return out;
|
||
}
|
||
|
||
export default {
|
||
char_179_cgbird: (bb, chess) => {
|
||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
|
||
const t0 = talentBb(chess, 0); // 白恶魔的庇护: magic_resistance (RIN-X stage 2+: heal_scale)
|
||
const t1 = talentBb(chess, 1); // 转瞬即逝的幻影: cnt (RIN-Y stage 2+: 3)
|
||
const hidden = moduleBb(chess); // RIN-Y: attack@max_target
|
||
const heals = Math.floor(num(hidden['attack@max_target']));
|
||
const grid = moduleRange(chess);
|
||
const picked = chess?.skill?.skillId ?? null;
|
||
const tokenId = (chess?.talents ?? []).find((t) => t && t.tokenKey)?.tokenKey ?? PHANTOM;
|
||
return {
|
||
trait: heals > 0 ? { heal: { mode: 'multi', count: heals } } : undefined,
|
||
skills: {
|
||
[S1]: { kind: 'duration', heal: true, mods: { atkPct: num(b1.atk) } },
|
||
[S2]: {
|
||
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||
heal: true,
|
||
attack: { healScale: 1 }, // the next heal (ai.js doHeal calls onHit for every heal target)
|
||
onHit({ battle, unit, target }) {
|
||
if (!target || target.side !== 'ally') return;
|
||
grantBarrier(battle, unit, target, {
|
||
key: 'cgbird:barrier', statKey: 'cgbird:barrierRes', shield: unit.s.atk * num(b2.atk_scale),
|
||
duration: num(b2.duration), mods: { resFlat: num(b2.magic_resistance) }, shieldType: 'arts',
|
||
});
|
||
},
|
||
},
|
||
[S3]: { kind: 'duration', heal: true, mods: { atkPct: num(b3.atk) }, targeting: s3?.rangeGrid ? { rangeGrid: s3.rangeGrid } : undefined },
|
||
},
|
||
talents: [
|
||
{ install(battle, unit) { // 白恶魔的庇护: allies in her attack range RES +magic_resistance; RIN-X stage 2+: healing ×heal_scale
|
||
const r = num(t0.magic_resistance), hs = num(t0.heal_scale, 1);
|
||
if (!(r > 0) && hs === 1) return;
|
||
const mods = { ...(r ? { resFlat: r } : null), ...(hs !== 1 ? { healingTakenMul: hs } : null) };
|
||
rangeAura(battle, unit, { key: 'talent:cgbird:res', value: r + (hs - 1) * 100, mods });
|
||
} },
|
||
{ install(battle, unit) { // 转瞬即逝的幻影: the 幻影's talent, her stock, a fallen 幻影's return
|
||
const cnt = Math.max(0, Math.floor(num(t1.cnt)));
|
||
if (!(cnt > 0)) return;
|
||
unit.mem.phantomStock = 0;
|
||
const mine = (t) => !!t && t.kind === 'token' && t.defId === tokenId && t.ownerUnit === unit;
|
||
battle.on('deploy', (ctx) => {
|
||
const u = ctx.unit;
|
||
if (u === unit) {
|
||
unit.mem.phantomStock = Math.min(PHANTOM_HOLD_CAP, (unit.mem.phantomStock ?? 0) + cnt);
|
||
return;
|
||
}
|
||
if (!mine(u)) return;
|
||
const tal = phantomTalent(u.def);
|
||
const p = num(tal.prob), hr = num(tal.hp_ratio);
|
||
if (p > 0) battle.addBuff(u, { key: 'cgbird:phantomDodge', mods: { dodgePhys: p }, tags: ['talent'] });
|
||
if (hr > 0) {
|
||
battle.addBuff(u, {
|
||
key: 'cgbird:phantomDrain', interval: 1, tags: ['talent'],
|
||
onTick: ({ unit: t }) => battle.loseHp(t, t.s.maxHp * hr, { silent: true, tags: ['cgbird:phantom'] }),
|
||
});
|
||
}
|
||
}, { owner: unit });
|
||
battle.on('death', (ctx) => {
|
||
const t = ctx.unit;
|
||
if (!mine(t) || ctx.reason !== 'killed' || battle.finished) return;
|
||
t.removed = false; // the piece stays (Battle.redeploy accepts it; its hooks are kept)
|
||
const at = battle.time + Math.max(0, num(t.base.respawnTime));
|
||
battle.every(RETRY, (b, sched) => {
|
||
if (t.alive || t.removed || b.finished) { sched.cancel(); return; }
|
||
if (b.time + 1e-9 < at || !((unit.mem.phantomStock ?? 0) > 0)) return;
|
||
if (b.redeploy(t, { free: false })) { unit.mem.phantomStock--; sched.cancel(); }
|
||
}, { owner: unit });
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
// RIN-X: the module's range is her own (the initial range of the DEFAULT trigger too); 圣域 runs its y-4
|
||
if (grid) { unit.rangeGrid = grid; battle.refreshRange(unit); }
|
||
// 圣域: allies in her (y-4) range RES ×(1 + magic_resistance), arts dodge prob, while the skill runs
|
||
if (picked === S3) {
|
||
const mr = num(b3.magic_resistance), p = num(b3.prob);
|
||
if (mr > 0 || p > 0) rangeAura(battle, unit, { key: 'skill:cgbird:sanctuary', value: mr, mods: { resMul: 1 + mr, dodgeArts: p }, on: () => !!unit.skill?.active });
|
||
}
|
||
},
|
||
};
|
||
},
|
||
};
|