Files
ark-stronghold-protocol/server/sim/content/kits/ops/op-shining.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

145 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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). Full potential (the owner's decision of 2026-10-07).
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json) and PRTS 闪灵 (S2 备注
// "多个屏障吸收量可叠加,持续时间独立计算,优先消耗先生成的屏障,防御力提升效果不可叠加"; 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 at or below hp_ratio of its max HP before the heal — the client's
// shining_e_003_tr is heal_scale_up[hpratio][LE] (as 华法琳's PHY-X; 录武官's reckpr_e_002_tr is LT, strictly below).
// - T1 黑恶魔的庇护 "攻击范围内的友方单位防御力+60" (full potential: +65): every ally (operators and summons: 友方单位)
// standing in her current attack range — her own tile included — DEF +def (flat). PHY-Y stage 2+ "防御力+100,地面单位防御力
// 额外+40" (full potential: +105; talent change def / def_lowland): def_lowland more on a 地面 tile. Refreshed every AURA s;
// two sources keep the strongest.
// - T2 法典 "攻击速度+10" (full potential: +13): ASPD +attack_speed. PHY-X stage 2+ "攻击速度+15,装备技能2时获得+25%攻击力,
// 装备技能3时技力自然回复速度+0.6/秒" (full potential: ASPD +18; 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 105, def_lowland)
const t1 = talentBb(chess, 1); // 法典: attack_speed (PHY-X stage 2+: 18)
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) + 1e-9 : 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 });
},
};
},
};