feedback5 O8: 自选 operator kits 闪灵, 夜莺 (+ her 幻影) and 嵯峨 — every skill, talent, trait and module at every form

- 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).
This commit is contained in:
sganggs committed 2026-10-05 23:58:00 +08:00
1 parent db7a177164
commit c1bf395207
8 files changed
+1828 -113

No files matched your search

+5 -1
View File
@@ -78,7 +78,11 @@ export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sh
* The 自选 operator kit files (`ops/op-<codename>.js`, registry key = the operator's charId, an owned-6★ pick of
* data/backups.json; a new file is appended). An operator without a file is not offered as a 自选 pick (KITTED_CHARS).
*/
export const OPERATOR_KIT_FILES = Object.freeze(['op-siege.js']);
export const OPERATOR_KIT_FILES = Object.freeze(['op-siege.js',
'op-shining.js',
'op-cgbird.js',
'op-saga.js',
]);
/**
* The 预备干员 whose every skill the generic kit covers exactly (冲锋号令, 攻击力 / 防御力 / 治疗强化, 战术咏唱, 一击即退 —
+158
View File
@@ -0,0 +1,158 @@
// 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). Potential 0 [ASSUMED: no account].
// 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": 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 });
}
},
};
},
};
+231
View File
@@ -0,0 +1,231 @@
// 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 });
},
};
},
};
+142
View File
@@ -0,0 +1,142 @@
// server/sim/content/kits/ops/op-shining.js — 闪灵 (char_147_shining) 自选 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_147_shining, 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) and PRTS 闪灵 (S2 备注
// "多个屏障吸收量可叠加,持续时间独立计算,优先消耗先生成的屏障,防御力提升效果不可叠加"; PRTS 术语释义 屏障 "若无特殊说明,
// 屏障可吸收全种类伤害").
// - Trait (医师) "恢复友方单位生命": the profession default (one heal on the lowest-HP% injured ally of her 3-3 range).
// Module PHY-Y 干枯剑鞘 "治疗地面单位时治疗量提升15%" (trait bb heal_scale): ×heal_scale on a heal whose target stands on
// a 地面 tile (unit.ground, as 赫默's PHY-Y); PHY-X “使徒” "治疗生命值低于50%的友方单位时治疗量提升15%" (heal_scale,
// hp_ratio): ×heal_scale when the target is below hp_ratio of its max HP before the heal (as 录武官's / 华法琳's PHY-X).
// - T1 黑恶魔的庇护 "攻击范围内的友方单位防御力+60": every ally (operators and summons: 友方单位) standing in her current
// attack range — her own tile included — DEF +def (flat). PHY-Y stage 2+ "防御力+100,地面单位防御力额外+40" (talent
// change def / def_lowland): def_lowland more on a 地面 tile. Refreshed every AURA s; two sources keep the strongest.
// - T2 法典 "攻击速度+10": ASPD +attack_speed. PHY-X stage 2+ "攻击速度+15,装备技能2时获得+25%攻击力,装备技能3时技力自然
// 回复速度+0.6/秒" (talent change attack_speed; the hidden module talent's atk / sp_recovery_per_sec): ATK +atk while 自动
// 掩护 is her picked skill, SP +sp_recovery_per_sec per second while 教条力场 is.
// - S1 信条 (MANUAL, 20 s, data DEFAULT — a heal skill: cast as she is about to heal an injured ally in range): ATK +atk,
// ASPD +attack_speed.
// - S2 自动掩护 (AUTO, 1 charge at rank 4 / 2 at rank 7, data DEFAULT): "下次治疗使目标获得一个持续N秒的屏障,屏障可以吸收相当于
// 闪灵攻击力X%的伤害,同时使目标防御力+Y%" — a "next heal" waits for her heal, as 推进之王's S2 waits for his attack (the
// owner's SP_FULL rule is for AUTO skills that act on nobody). The heal's target gets a 屏障 of atk_scale × her ATK for
// duration s (every damage type) and DEF +def while it holds: each barrier is its own buff (its own timer; older ones
// absorb first — buff order), the DEF bonus one buff per target, kept while one of her barriers lasts [ASSUMED: it goes
// with the last barrier, broken or expired — the 备注 only says it does not stack].
// - S3 教条力场 (MANUAL, 60 s, data DEFAULT): ATK +atk; every ally in her attack range DEF +def (%), refreshed every AURA s.
// Medic: heals only (dmgType heal), ranged, no anti-air question; ground enemies target her normally (no 起飞 / 迷彩 / 隐匿).
import { num, talentBb, moduleBb, traitBb, moduleOn, skillRec, statBuff, up } from '../shared/tier1.js';
const S1 = 'skchr_shining_1';
const S2 = 'skchr_shining_2';
const S3 = 'skchr_shining_3';
const PHY_Y = 'uniequip_002_shining';
const PHY_X = 'uniequip_003_shining';
/** Aura refresh period and buff life (s): an ally leaving the range loses the buff within one refresh (tier-4 AURA). */
const AURA = 0.2;
const AURA_DUR = 0.25;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const pickedSkill = (chess) => chess?.skill?.skillId ?? null;
/**
* Keep buff `key` on every ally standing in `unit`'s current attack range (operators and summons, its own tile included;
* never a 孤立 unit — Battle.alliesInGrid) while `on()` holds, refreshed every AURA s; it lapses AURA_DUR s after the
* source stops refreshing it. Several sources of the same effect keep the strongest `value` (same-named buffs).
* Shared with 夜莺's kit (op-cgbird.js).
* @param {object} battle
* @param {object} unit the source
* @param {{ key: string, value: number, mods: object|((ally: object) => object), on?: () => boolean }} o
*/
export function rangeAura(battle, unit, { key, value, mods, on = () => true }) {
battle.every(AURA, () => {
if (!up(unit) || !on()) return;
for (const a of battle.alliesInGrid(unit)) {
const cur = a.findBuff(key);
if (cur && cur.source !== unit && cur.source?.alive && (cur.data?.v ?? 0) > value && cur.timeLeft > 0.05) continue;
battle.addBuff(a, { key, duration: AURA_DUR, mods: typeof mods === 'function' ? mods(a) : mods, source: unit, data: { v: value }, tags: ['aura'] });
}
}, { owner: unit, immediate: true });
}
/**
* A 屏障 on `target` (闪灵 S2 自动掩护, 夜莺 S2 法术护盾): `shield` HP for `duration` s — of `shieldType` damage only when
* given (damage.js absorbShields), else every type — plus the buff `statKey` (`mods`) while any barrier `key` lasts. PRTS
* 备注 of both skills: "多个屏障吸收量可叠加,持续时间独立计算,优先消耗先生成的屏障,…效果不可叠加" — each barrier is its own
* buff (refresh 'independent': its own timer; absorbShields walks the buffs oldest first), the stat bonus one buff whose
* life follows the barriers (it ends when the last one breaks or expires). Shared with 夜莺's kit (op-cgbird.js).
* @param {object} battle
* @param {object} source the operator
* @param {object} target
* @param {{ key: string, statKey: string, shield: number, duration: number, mods: object, shieldType?: string|null }} o
*/
export function grantBarrier(battle, source, target, { key, statKey, shield, duration, mods, shieldType = null }) {
if (!target || !target.alive || !(shield > 0) || !(duration > 0)) return;
const sync = () => { if (!target.buffs.some((b) => b.key === key)) battle.removeBuff(target, statKey); };
battle.addBuff(target, {
key, refresh: 'independent', maxStacks: 99, duration, shield, shieldType, source, visible: true, onRemove: sync, onExpire: sync,
});
battle.addBuff(target, { key: statKey, refresh: 'extend', duration, mods, source });
battle.fx('shield', { x: target.x, y: target.y, id: target.id, from: source.id });
}
export default {
char_147_shining: (bb, chess) => {
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
const t0 = talentBb(chess, 0); // 黑恶魔的庇护: def (PHY-Y stage 2+: def 100, def_lowland)
const t1 = talentBb(chess, 1); // 法典: attack_speed (PHY-X stage 2+: 15)
const hidden = moduleBb(chess); // PHY-X stage 2+: atk (with S2 picked), sp_recovery_per_sec (with S3 picked)
const tb = traitBb(chess); // PHY-Y: heal_scale; PHY-X: heal_scale, hp_ratio
const mod = moduleOn(chess) ? chess.module.id : null;
const picked = pickedSkill(chess);
return {
skills: {
[S1]: { kind: 'duration', heal: true, mods: { atkPct: num(b1.atk), aspd: num(b1.attack_speed) } },
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
heal: true,
attack: { healScale: 1 }, // the next heal (ai.js doHeal calls onHit per heal target)
onHit({ battle, unit, target }) {
if (!target || target.side !== 'ally') return;
grantBarrier(battle, unit, target, {
key: 'shining:barrier', statKey: 'shining:barrierDef', shield: unit.s.atk * num(b2.atk_scale),
duration: num(b2.duration), mods: { defPct: num(b2.def) },
});
},
},
[S3]: { kind: 'duration', heal: true, mods: { atkPct: num(b3.atk) } },
},
talents: [
{ install(battle, unit) { // 黑恶魔的庇护: allies in her attack range DEF +def; PHY-Y stage 2+: 地面 units +def_lowland
const d = num(t0.def), low = num(t0.def_lowland);
if (!(d > 0) && !(low > 0)) return;
rangeAura(battle, unit, { key: 'talent:shining:def', value: d + low, mods: (a) => ({ defFlat: d + (a.ground ? low : 0) }) });
} },
{ install(battle, unit) { // 法典: ASPD +attack_speed; PHY-X stage 2+: ATK +atk with S2, SP +0.6/s with S3
statBuff(battle, unit, 'talent:shining:aspd', { aspd: num(t1.attack_speed) });
if (picked === S2) statBuff(battle, unit, 'talent:shining:s2', { atkPct: num(hidden.atk) });
if (picked === S3) statBuff(battle, unit, 'talent:shining:s3', { spRecoveryFlat: num(hidden.sp_recovery_per_sec) });
} },
],
install(battle, unit) {
// 教条力场: allies in her attack range DEF +def % while the skill runs
if (picked === S3 && num(b3.def) > 0) {
rangeAura(battle, unit, { key: 'skill:shining:def', value: num(b3.def), mods: { defPct: num(b3.def) }, on: () => !!unit.skill?.active });
}
// module traits: PHY-Y heals on 地面 units, PHY-X heals on allies below hp_ratio, ×heal_scale
const hs = num(tb.heal_scale, 1);
if (!mod || hs === 1) return;
const boosted = mod === PHY_X ? (t) => t.hpRatio < num(tb.hp_ratio) : mod === PHY_Y ? (t) => !!t.ground : null;
if (!boosted) return;
battle.on('heal', (ctx) => {
if (ctx.source !== unit || ctx.opts?.regen || !ctx.target || !boosted(ctx.target)) return;
ctx.amount *= hs;
}, { owner: unit });
},
};
},
};