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:
1 parent
db7a177164
commit
c1bf395207
8 files changed
+1828
-113
No files matched your search
@@ -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 (冲锋号令, 攻击力 / 防御力 / 治疗强化, 战术咏唱, 一击即退 —
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user