Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_6_04-skadi2.js
T
sganggs 5c6524e1ad feedback5 WL: 生命回复速度 is not 治疗 — the 吟游者 trait, 调香师, 瑕光 S2, 铃兰 S3 and 锡人 give an hpRegen buff, so 无法被友方治疗 / 禁疗 units get it and 治疗加成 does not scale it (GitHub #96 / #137)
PRTS: 分支特性信息 吟游者 「特性为基于自身攻击力来增加受益者的“生命回复速度”属性」 (魔王, 浊心斯卡蒂); the 备注 of 调香师 熏衣草,
瑕光 S2, 铃兰 S3 and 锡人 S2 「生命恢复的提供方式为增加目标的“生命回复速度”属性…不受治疗加成和禁疗影响」 (铃兰: none in the
first second, refreshed every second); 异常效果 禁疗 「增减生命回复速度…的效果不会被识别为治疗类能力」. These were heals from
the provider, which damage.js refused on 收割者 / 不屈者 / 武者 (noHeal) and under 禁疗 — 隐德来希 gained nothing under 魔王.
This is the separation proposed in PR #135 by @SpiritedAwayCN, written anew (the PR's patch is not used):
- professions.js bardRegen: the trait is an hpRegen buff per bard refreshed every 0.25 s; 魔王's 微尘 ×1.5 goes through
  its `bardRegen` hook (both 魔王 kits); 浊心斯卡蒂's pulse and the 海嗣 token use it too.
- 调香师 (every ally), 瑕光 S2 (the skill range, removed at its end), 铃兰 S3 (from the first full second, removed at its
  end), 锡人 (per 炼金单元, its cached ATK): hpRegen buffs instead of battle.heal.
- damage.js: a `regen` tick takes no healing multiplier and a heal hook cannot change its amount (no 治疗加成 on any
  生命回复速度); 流明's 凡人之愿 and 华法琳's S1 bonus skip a regen tick of their own.
Direct heals are unchanged (still refused / scaled); 魔王 S3's redistribution and 纯烬's HoT are untouched. Other
per-second recoveries without such a note stay heals (引星棘刺, 塞雷娅 S3, 流明 / 纯烬 HoTs; 余 闲云隐市 and 天马之盔 left open).

Golden moved (every one fields 魔王 / 浊心斯卡蒂 / 调香师 / 瑕光 S2 / 铃兰 / 锡人): roster-004, roster-005, roster-006,
roster-011, roster-013, roster-015, roster-016, roster-018, roster-023, roster-025, roster-030, roster-032, roster-035,
roster-037, roster-039, roster-041, roster-042, roster-045, roster-046; bond-deputShip-high, bond-emptyShip-low,
bond-emptyShip-high, bond-indomShip-high, bond-kazimierzShip-low, bond-kazimierzShip-high, bond-sargonShip-low,
bond-sargonShip-high, bond-skillfulShip-high; boss-boss_1-pair, boss-boss_1-solo, boss-boss_2-solo, boss-boss_3-pair,
boss-boss_3-solo, boss-boss_4-solo, boss-boss_5-pair, boss-boss_5-solo, boss-boss_6-solo, boss-boss_7-pair,
boss-boss_7-solo, hidden-boss_8-solo, hidden-boss_9-pair, hidden-boss_9-solo, hidden-boss_10-solo, unite-1;
solo-NORMAL-1, solo-HARD-1, solo-ABYSS-1, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-14-standins.
Docs: SIM.md (heal pipeline, bard row), PLAYING.md, kits/README.md (checklist item 11).
2026-10-05 21:13:15 +08:00

194 lines
12 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/chess_char_6_04-skadi2.js — 浊心斯卡蒂 (char_1012_skadi2) kit, tier 6.
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
import { aggregateMods } from '../../../buffs.js';
import { COLS } from '../../../constants.js';
import { bardRegen } from '../../../professions.js';
import {
num, bv, tbb, tdesc, moduleBb, parseN, live, enemiesIn, isTok, onDefaultSkill, selectedSkill, aura,
} from '../shared/tier6.js';
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
/** 深海猎人 (character_table groupId "abyssal" — checked against the official table; data/chess.json has no group id). */
const ABYSSAL = new Set(['char_143_ghost', 'char_263_skadi', 'char_474_glady', 'char_4145_ulpia', 'char_1023_ghost2']);
/**
* 鼓舞 (ba.inspire "获得额外附加的基础属性加成(同类属性取最高)"): +`val` ATK after the target's own multipliers (flat
* compensated), strongest source wins. Units flagged `mem.noInspire` (浊心斯卡蒂 "自身不受鼓舞影响") never receive it.
*/
function inspire(battle, target, val, src, stat = 'atk') {
if (!(val > 0) || !live(target) || target.mem.noInspire) return;
const key = stat === 'def' ? 'inspire:def' : 'inspire';
const cur = target.findBuff(key);
if (cur && cur.data && cur.data.src !== src.id && cur.data.val > val && cur.timeLeft > 0.1) return;
const { add, mul } = aggregateMods(target.buffs.filter((b) => b.key !== key));
const f = stat === 'def'
? Math.max(0, 1 + (add.defPct ?? 0)) * (mul.defMul ?? 1)
: Math.max(0, 1 + (add.atkPct ?? 0)) * (mul.atkMul ?? 1);
const flat = f > 1e-6 ? val / f : val;
battle.addBuff(target, { key, mods: stat === 'def' ? { defFlat: flat } : { atkFlat: flat }, duration: 0.75, refresh: 'replace', source: src, visible: true, data: { src: src.id, val } });
}
// ------------------------------------------------------------------------------------------------------------------
// 浊心斯卡蒂 chess_char_6_04 (吟游者) — S3 "潮涌,潮枯"; 远古血亲 (海嗣); 捕食习性; module 蜕化的残迹
function skadi2(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mod = moduleBb(chess), tb = def?.traitBb || {};
const isDef = onDefaultSkill(chess), sid = selectedSkill(chess, def);
const tokId = def?.talents?.[0]?.tokenKey || (chess?.tokens || [])[0] || 'token_10017_skadi2_dedant';
const auraRatio = num(tb['attack@atk_to_hp_recovery_ratio'], 0.1);
// S1 / S2 raise the trait ("特性效果提高至N%") while they run; S3 (default) turns it into the tide. The trait is a
// 生命回复速度 buff on the allies, not a heal (PRTS 分支特性信息 吟游者; professions.js bardRegen)
const skillRatio = num(bb['attack@atk_to_hp_recovery_ratio'], auraRatio);
const seaborns = (battle, unit) => battle.allyUnits.filter((t) => isTok(t, tokId, unit) && live(t));
const covered = (battle, unit, toks) => { // allies inside her range ∪ the seaborns' ranges
const set = new Set(battle.alliesInGrid(unit));
for (const t of toks) for (const a of battle.alliesInGrid(t)) set.add(a);
for (const t of toks) set.delete(t);
return [...set];
};
const inCover = (battle, unit, a) => {
const k = a.tileR * COLS + a.tileC;
if (unit.rangeKeySet?.has(k)) return true;
return seaborns(battle, unit).some((t) => t.rangeKeySet?.has(k));
};
const skills = {
// S1 同归殊途之吟 (SP_FULL): full self heal, max HP +max_hp, trait attack@atk_to_hp_recovery_ratio, and
// damage_resistance of the damage taken by every ally of her (+ 海嗣) range is transferred to her (install)
skchr_skadi2_1: {
kind: 'duration',
mods: { hpPct: num(bb.max_hp) },
onStart({ battle, unit }) { unit.hp = unit.s.maxHp; battle.fx('heal', { x: unit.x, y: unit.y, id: unit.id }); },
},
// S2 同葬无光之愿 (toggle): 鼓舞 ATK / DEF = atk / def × her ATK / DEF on every other ally of her (+ 海嗣) range,
// trait attack@atk_to_hp_recovery_ratio (trait pulse). 自动触发 with effects on her allies only (nothing to target):
// on as soon as it is ready (SP_FULL, like 魔王 S1 往昔萦绕身旁); until 0.2.0 the data's DEFAULT left it off until an
// enemy came into her (or a 海嗣's) range.
skchr_skadi2_2: { kind: 'toggle', trigger: 'SP_FULL' },
};
return {
skills,
skill: {
kind: 'duration', // effects run in the trait pulse below ("特性变为…")
onStart({ battle, unit }) { battle.fx('tide', { x: unit.x, y: unit.y, id: unit.id }); },
},
trait: {
install(battle, unit) { // replaces the bard aura: 生命回复速度 normally, tide (true damage + 鼓舞 + self drain) during S3
unit.mem.noInspire = true; // 自身不受鼓舞影响
const isInspire = (b) => b.key === 'inspire' || b.status === 'inspire' || b.key.startsWith('inspire:') || b.key.endsWith(':inspire');
// "海嗣的攻击范围视为自身攻击范围的延伸": an enemy in a 海嗣's range also satisfies her DEFAULT trigger
if (unit.skill) unit.skill.addTriggerRange(() => seaborns(battle, unit));
let n = 0;
battle.every(0.5, () => {
if (!unit.canAct) return;
for (let i = unit.buffs.length - 1; i >= 0; i--) if (isInspire(unit.buffs[i])) battle.removeBuff(unit, unit.buffs[i]);
n++;
const toks = seaborns(battle, unit);
const allies = covered(battle, unit, toks);
const sk = unit.skill;
const on = !!(sk && sk.active);
if (on && isDef) {
const val = unit.s.atk * num(bb.atk);
for (const a of allies) if (a !== unit) inspire(battle, a, val, unit);
if (n % 2 === 0) {
const dmg = unit.s.atk * num(bb.atk_scale);
if (dmg > 0) for (const src of [unit, ...toks]) for (const e of enemiesIn(battle, src)) {
battle.dealDamage(unit, e, { amount: dmg, type: 'true', isSkill: true, tags: ['skill', 'tide'] });
}
const loss = unit.s.maxHp * num(bb.hp_ratio);
if (loss > 0) { if (unit.hp - loss >= 1) battle.loseHp(unit, loss, { source: unit, silent: true }); else unit.hp = Math.min(unit.hp, 1); }
}
return;
}
if (on && sid === 'skchr_skadi2_2') {
const va = unit.s.atk * num(bb.atk), vd = unit.s.def * num(bb.def);
for (const a of allies) if (a !== unit) { inspire(battle, a, va, unit); inspire(battle, a, vd, unit, 'def'); }
}
// the trait over her range ∪ the 海嗣' ranges, refreshed every pulse (it lapses 0.75 s after the last one)
const v = unit.s.atk * (on ? skillRatio : num(unit.profile?.auraRatio, auraRatio));
for (const a of allies) bardRegen(battle, unit, a, v, 0.75);
}, { owner: unit });
},
},
install(battle, unit) {
// S1: "攻击范围内我方所有单位受到伤害的50%直接转移给浊心斯卡蒂承担(同类效果取最高)" — the ally takes (1 − share),
// she takes the transferred part as true damage from the same attacker
if (sid === 'skchr_skadi2_1') {
const share = clamp(num(bb.damage_resistance), 0, 1);
const TAG = `skadi2:transfer:${unit.id}`;
battle.on('hit', (ctx) => {
const t = ctx.target, d = ctx.dmg;
if (!(share > 0) || !t || t === unit || t.side !== 'ally' || t.kind === 'device' || !live(unit) || !unit.skill?.active) return;
if (d.type === 'element' || d.type === 'elemental' || d.skadiShare || d.tags?.includes('transfer') || !inCover(battle, unit, t)) return;
d.skadiShare = { share, tag: TAG }; // 同类效果取最高: one transfer per damage instance
d.mul *= 1 - share;
}, { owner: unit, priority: -20 });
battle.on('damaged', (ctx) => {
const x = ctx.dmg?.skadiShare;
if (!x || x.tag !== TAG || !(ctx.amount > 0) || !live(unit)) return;
battle.dealDamage(ctx.source, unit, { amount: ctx.amount * x.share / Math.max(1e-6, 1 - x.share), type: 'true', canDodge: false, tags: ['transfer'] });
}, { owner: unit });
}
// module 新生代: "自身技能期间,攻击范围内的友军获得30点物理与法术伤害减免" (flat reduction per hit)
const flat = -num(mod.damage_resistance);
if (flat > 0) {
battle.on('hit', (ctx) => {
const t = ctx.target, d = ctx.dmg;
if (!t || t.side !== 'ally' || t.kind === 'device' || !live(unit) || !unit.skill?.active || !inCover(battle, unit, t)) return;
if (d.type === 'phys') d.defIgnoreFlat -= flat; // +flat effective DEF ⇒ −flat damage (above the 5 % floor)
else if (d.type === 'arts') d.amount = Math.max(0, d.amount - flat / Math.max(0.05, 1 - (t.s.res ?? 0) / 100));
}, { owner: unit, priority: -10 });
}
},
talents: [
{ install(battle, unit) { // 远古血亲: the seaborn is an inert range extension with a limited life
battle.on('deploy', ({ unit: t }) => {
if (!isTok(t, tokId, unit)) return;
if (t.profile) t.profile.noAttack = true;
const dur = num(t.def?.talents?.[0]?.bb?.duration, parseN(tdesc(def, 0), /持续(\d+(?:\.\d+)?)秒/, 25));
const seq = t.deploySeq, r = t.tileR, c = t.tileC;
battle.after(dur, () => {
if (!t.alive || t.deploySeq !== seq) return;
battle.retreat(t, { reason: 'expired', permanent: true });
const wait = num(t.base.respawnTime, 30), cost = num(t.base.cost, 0);
let tries = 0;
const again = () => {
if (battle.finished || tries++ > 300) return;
const ps = battle.getPlayer(unit.ownerId);
if (!live(unit) || !ps || ps.dp + 1e-9 < cost || battle.unitAt(r, c)) { battle.after(1, again, { owner: unit }); return; }
battle.addDp(unit.ownerId, -cost);
if (!battle.spawnToken(unit, tokId, r, c)) { battle.addDp(unit.ownerId, cost); battle.after(1, again, { owner: unit }); }
};
battle.after(wait, again, { owner: unit });
}, { owner: t });
}, { owner: unit });
} },
{ install(battle, unit) { // 捕食习性 (module 新生代: ATK 9 / 20 %, DEF +8 %, +3 SP per op deployed in range) + module (≥2 other ops in range: ATK +8 %)
const a1 = num(t1['skadi2_e_003_t_2[atk][1].atk'], bv(t1, 'skadi2_t_2[atk][1].atk', 0));
const a2 = num(t1['skadi2_e_003_t_2[atk][2].atk'], bv(t1, 'skadi2_t_2[atk][2].atk', a1));
const d1 = num(t1['skadi2_e_003_t_2[def].def']), sp = num(t1.sp);
const mCnt = num(mod.cnt, Infinity), mAtk = num(mod.atk);
aura(battle, unit, 0.5, () => {
const toks = seaborns(battle, unit);
const ops = covered(battle, unit, toks).filter((a) => a !== unit && a.kind === 'op');
const val = ops.some((a) => ABYSSAL.has(a.def?.charId)) ? a2 : ops.length ? a1 : 0;
const own = battle.alliesInGrid(unit).filter((a) => a !== unit && a.kind === 'op').length;
const total = val + (own >= mCnt ? mAtk : 0);
const dPct = ops.length ? d1 : 0;
if (total > 0 || dPct > 0) battle.addBuff(unit, { key: 'skadi2:predator', mods: { atkPct: total, defPct: dPct }, duration: 0.75, refresh: 'replace' });
});
// "我方干员部署于自身或海嗣范围内后,自身立刻获得3点技力"
if (sp > 0) battle.on('deploy', ({ unit: u }) => {
if (!u || u === unit || u.kind !== 'op' || u.side !== 'ally' || !live(unit) || !unit.skill) return;
if (inCover(battle, unit, u)) unit.skill.gainSp(sp, 'talent');
}, { owner: unit });
} },
],
};
}
export default {
chess_char_6_04_a: skadi2,
};