feedback5 R4: one file per operator kit — kits/tier1..6.js split into kits/ops/<chessId>-<codename>.js (129 kits), kits/shared/tier1..6.js (the helpers two or more kits of a tier use, and each tier's header notes) and kits/index.js (the registry, same keys in the same order); the tier files become re-export shims

Pure move: every line of a kit, of a helper and of the tier headers is copied verbatim (only the headers' first
line names the new file); a helper used by one kit moved into that kit's file. Glue only: the generated imports,
`export default { … }` around each registry entry, `export { … }` lists in shared/tier2..6.js, and tier4's
`(bb, chess, def)` default-argument wrapper as shared/tier4.js `withDefaults` (applied in each tier-4 kit file).
kits/index.js loads every kit file with a guarded dynamic import (browser-safe, no directory listing); tierN.js
re-export the tier registry (TIER_KITS) and their old named exports (tier1 helpers + tinmanKit, tier6
PITHST_ELEMENTS). The merged registry, every tier default and named export have the same keys, order, frozenness
and function sources as before. test/docs-consistency.test.js reads the moved code at its new paths.
Golden: GOLDEN_FULL=1 node --test test/golden.test.js unchanged.
This commit is contained in:
sganggs committed 2026-10-05 16:13:56 +08:00
1 parent 6b489b1776
commit 1169102d46
143 files changed
+12924 -11784

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
// server/sim/content/kits/index.js — the operator kit registry (DESIGN §7, docs/SIM.md §7.2): baseChessId →
// (bb, chess, def) => Kit. One file per kit in ops/ (README.md: what a kit file holds, naming, registration, tests).
//
// KIT_FILES lists every kit file, grouped by tier — the former kits/tier1.js … tier6.js, each group in that file's
// order — so the merged registry keeps the key order it always had (content/index.js resolves kits by id; the tier
// shims, tools/kit-coverage.mjs and the golden corpus see the same registry as before). A new kit file is appended to
// its tier's group.
// Each file is loaded with a guarded dynamic import, so this runs unchanged in the browser (/sim/, no Node-only
// module, no directory listing): a file that fails to load (syntax error, throwing top-level code, missing file) is
// logged and skipped — its chess falls back to the generic kit — instead of breaking the server or the page.
export const KIT_FILES = Object.freeze([
// tier 1
['chess_char_1_01-inside.js', 'chess_char_1_02-yak.js', 'chess_char_1_03-leizi.js', 'chess_char_1_04-udflow.js',
'chess_char_1_05-vigna.js', 'chess_char_1_06-vendla.js', 'chess_char_1_07-prove.js', 'chess_char_1_08-texas.js',
'chess_char_1_09-caper.js', 'chess_char_1_10-sunbr.js', 'chess_char_1_11-skgoat.js', 'chess_char_1_12-estell.js',
'chess_char_1_13-podego.js', 'chess_char_1_14-greyy.js', 'chess_char_1_16-tinman.js', 'chess_char_1_17-indigo.js',
'chess_char_1_18-utage.js', 'chess_char_1_19-wildmn.js', 'chess_char_1_20-liskam.js'],
// tier 2
['chess_char_2_01-excu.js', 'chess_char_2_02-silent.js', 'chess_char_2_03-slchan.js', 'chess_char_2_04-grabds.js',
'chess_char_2_05-harold.js', 'chess_char_2_06-papyrs.js', 'chess_char_2_07-ghost.js', 'chess_char_2_08-bubble.js',
'chess_char_2_09-humus.js', 'chess_char_2_10-rockr.js', 'chess_char_2_11-kazema.js', 'chess_char_2_12-gravel.js',
'chess_char_2_13-tippi.js', 'chess_char_2_14-flower.js', 'chess_char_2_15-akkord.js', 'chess_char_2_16-whitew.js',
'chess_char_2_17-branch.js', 'chess_char_2_18-ashlok.js', 'chess_char_2_19-tinman.js'],
// tier 3
['chess_char_3_01-angel.js', 'chess_char_3_02-ayer.js', 'chess_char_3_03-swire.js', 'chess_char_3_04-swire2.js',
'chess_char_3_05-skadi.js', 'chess_char_3_06-philae.js', 'chess_char_3_07-forcer.js', 'chess_char_3_08-mint.js',
'chess_char_3_09-haini.js', 'chess_char_3_10-pinecn.js', 'chess_char_3_11-snhunt.js', 'chess_char_3_12-blemsh.js',
'chess_char_3_13-malist.js', 'chess_char_3_14-slbell.js', 'chess_char_3_15-vodfox.js', 'chess_char_3_16-snakek.js',
'chess_char_3_17-shotst.js', 'chess_char_3_18-vulpis.js', 'chess_char_3_19-vigil.js', 'chess_char_3_20-kjera.js',
'chess_char_3_21-archet.js'],
// tier 4
['chess_char_4_01-rmixer.js', 'chess_char_4_02-mostma.js', 'chess_char_4_03-kjera.js', 'chess_char_4_04-ines.js',
'chess_char_4_05-beewax.js', 'chess_char_4_06-kroos2.js', 'chess_char_4_07-bpipe.js', 'chess_char_4_08-rosesa.js',
'chess_char_4_09-mizuki.js', 'chess_char_4_10-aroma.js', 'chess_char_4_11-cathy.js', 'chess_char_4_12-glady.js',
'chess_char_4_13-gnosis.js', 'chess_char_4_14-lionhd.js', 'chess_char_4_15-reckpr.js', 'chess_char_4_16-texas2.js',
'chess_char_4_17-hsguma.js', 'chess_char_4_18-mudrok.js', 'chess_char_4_19-flamtl.js', 'chess_char_4_20-fartth.js',
'chess_char_4_21-plosis.js', 'chess_char_4_22-svrash.js', 'chess_char_4_23-gvial2.js', 'chess_char_4_24-billro.js',
'chess_char_4_25-cetsyr.js', 'chess_char_4_26-bldsk.js'],
// tier 5
['chess_char_5_01-excu2.js', 'chess_char_5_02-titi.js', 'chess_char_5_03-blaze2.js', 'chess_char_5_04-bldsk.js',
'chess_char_5_05-ulpia.js', 'chess_char_5_06-etlchi.js', 'chess_char_5_07-surtr.js', 'chess_char_5_08-horn.js',
'chess_char_5_09-cetsyr.js', 'chess_char_5_10-lisa.js', 'chess_char_5_11-demkni.js', 'chess_char_5_12-dusk.js',
'chess_char_5_13-ghost2.js', 'chess_char_5_14-svash2.js', 'chess_char_5_15-thorn2.js', 'chess_char_5_16-plosis.js',
'chess_char_5_17-f12yin.js', 'chess_char_5_18-gvial2.js', 'chess_char_5_19-mlynar.js', 'chess_char_5_20-aglina.js',
'chess_char_5_21-sntlla.js', 'chess_char_5_22-nymph.js', 'chess_char_5_23-reckpr.js'],
// tier 6 (and 盟约·辅助干员 chess_char_1_15, a hidden tier-1 chess always registered with the tier-6 kits)
['chess_char_1_15-pithst.js', 'chess_char_6_01-lemuen.js', 'chess_char_6_02-sbell2.js', 'chess_char_6_03-yu.js',
'chess_char_6_04-skadi2.js', 'chess_char_6_05-pasngr.js', 'chess_char_6_06-pepe.js', 'chess_char_6_07-siege2.js',
'chess_char_6_08-reed2.js', 'chess_char_6_09-cello.js', 'chess_char_6_10-nymph.js', 'chess_char_6_11-mlyss.js',
'chess_char_6_12-rosmon.js', 'chess_char_6_13-angel2.js', 'chess_char_6_14-lumen.js', 'chess_char_6_15-qiubai.js',
'chess_char_6_16-halo2.js', 'chess_char_6_17-nearl2.js', 'chess_char_6_18-whitw2.js', 'chess_char_6_19-blkkgt.js',
'chess_char_6_20-agoat2.js'],
].map((group) => Object.freeze(group)));
async function loadKitFile(file) {
try {
return await import(`./ops/${file}`);
} catch (e) {
console.error(`[content] failed to load kits/ops/${file}: ${e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e}`);
return {};
}
}
const MODULES = await Promise.all(KIT_FILES.map((group) => Promise.all(group.map(loadKitFile))));
const registryOf = (m) => (m && m.default && typeof m.default === 'object' ? m.default : {});
/** The registry of each tier group (index 0 = tier 1): baseChessId → kit builder, in KIT_FILES order. */
export const TIER_KITS = Object.freeze(MODULES.map((group) => Object.assign({}, ...group.map(registryOf))));
/** The merged kit registry: baseChessId → (bb, chess, def) => Kit, tier 1 … tier 6. */
export const KITS = Object.freeze(Object.assign({}, ...TIER_KITS));
@@ -0,0 +1,61 @@
// server/sim/content/kits/ops/chess_char_1_01-inside.js — 隐现 (char_498_inside) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, up, once, skillBbOf, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_01 隐现 “解决麻烦”: ammo (attack@trigger_time) ATK+atk, base attack time +base_attack_time s, prefers ranged
// enemies, taunt −1. 火力支援: after `duration` s on the field own ammo +self_ammo (from the next cast); elite: another
// random 【拉特兰】 op's ammo +ally_ammo while that deployment lasts (one grant per source).
// Alternate S1 “不惹麻烦” (AUTO, attack SP): attack@trigger_time rounds of attack@atk_scale × ATK physical (an ammo
// skill, so 火力支援's larger magazine applies to it too).
chess_char_1_01_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const dur = num(t.duration), selfAmmo = num(t.self_ammo), allyAmmo = num(t.ally_ammo);
const s1 = skillBbOf(chess, 'skchr_inside_1');
return {
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 1),
mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess, def?.skill?.description ?? chess?.skill?.desc), taunt: -1 },
targeting: { priority: 'ranged' },
},
skills: {
skchr_inside_1: { kind: 'ammo', ammo: Math.max(1, Math.floor(num(s1['attack@trigger_time'], 4))), attack: { atkScale: num(s1['attack@atk_scale'], 1) } },
},
talents: [{ install(battle, unit) {
const onField = () => up(unit) && battle.time - unit.deployedAt + 1e-9 >= dur;
battle.on('skillStart', ({ unit: u, skill }) => {
if (u === unit && skill.kind === 'ammo' && onField() && selfAmmo > 0) skill.addAmmo(selfAmmo);
}, { owner: unit });
if (!(allyAmmo > 0)) return;
// elite: once per deployment, `duration` s after it, one random other 【拉特兰】 operator with an ammo skill gets
// +ally_ammo for as long as this deployment lasts (one grant per source — redeploying never stacks it)
battle.on('deploy', ({ unit: u }) => {
if (u !== unit) return;
const seq = unit.deploySeq;
battle.after(dur, () => {
// (the larger magazine applies from the next activation: a running skill keeps its rounds)
if (!up(unit) || unit.deploySeq !== seq) return;
const pool = battle.allyUnits.filter((a) => a !== unit && a.kind === 'op' && a.ownerId === unit.ownerId && a.alive
&& (a.def.bonds || []).includes('lateranoShip') && a.skill && a.skill.kind === 'ammo');
const pick = battle.rng.pick(pool);
if (!pick) return;
(pick.mem.insiderAmmo ??= new Map()).set(unit.id, { src: unit, seq, n: allyAmmo });
battle.fx('buff', { x: pick.x, y: pick.y, id: pick.id, kind: 'ammo' });
}, { owner: unit });
}, { owner: unit });
once(battle, 'insider:bonusAmmo', () => battle.on('skillStart', ({ unit: u, skill }) => {
const m = u.mem.insiderAmmo;
if (!m || skill.kind !== 'ammo') return;
let n = 0;
for (const [k, g] of m) {
if (up(g.src) && g.src.deploySeq === g.seq) n += g.n;
else m.delete(k);
}
if (n > 0) skill.addAmmo(n);
}));
} }],
};
},
};
@@ -0,0 +1,19 @@
// server/sim/content/kits/ops/chess_char_1_02-yak.js — 角峰 (char_199_yak) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, statBuff, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_02 角峰 抗寒体质: HP +max_hp, DEF +def, RES ×(1+magic_resistance). 雪原卫士: RES +magic_resistance (flat).
// Alternate S1 体能强化 (重装 ⇒ TAKE_DAMAGE trigger from data): HP +max_hp, +hp_recovery_per_sec HP per second
// (the 生命回复速度 attribute). Elite module PRO-Y (block 4) is a stat of the module (attr blockCnt).
chess_char_1_02_a: (bb, chess) => {
const s1 = skillBbOf(chess, 'skchr_yak_1');
return {
skill: { kind: 'duration', mods: { hpPct: num(bb.max_hp), defPct: num(bb.def), resMul: 1 + num(bb.magic_resistance) } },
skills: { skchr_yak_1: { kind: 'duration', mods: { hpPct: num(s1.max_hp), hpRegen: num(s1.hp_recovery_per_sec) } } },
talents: [{ install(battle, unit) { statBuff(battle, unit, 'talent:yak', { resFlat: num(talentBb(chess, 0).magic_resistance) }); } }],
};
},
};
@@ -0,0 +1,27 @@
// server/sim/content/kits/ops/chess_char_1_03-leizi.js — 惊蛰 (char_306_leizi) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { CHAIN_RADIUS } from '../../../constants.js';
import { num, talentBb, traitBb, onHitBy, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_03 惊蛰 初雷: ATK +atk, chain jumps lose no damage. 通流无阻: vs unblocked enemies ATK ×atk_scale (every jump).
// Alternate S1 攻击力强化·γ型: ATK +atk (the chain keeps its falloff). Elite module CHA-X (4 jumps, −10 %, 0.8 s 停顿)
// is the chain profile of the module trait (professions.js TUNE.chain).
chess_char_1_03_a: (bb, chess) => {
const scale = num(talentBb(chess, 0).atk_scale, 1);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, attack: {} },
skills: { 'skcom_atk_up[3]': { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, 'skcom_atk_up[3]').atk) } } },
talents: [{ install(battle, unit) {
if (scale !== 1) onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && !target.blockedBy) dmg.amount *= scale; });
} }],
install(battle, unit) {
// the chain shape (count / sluggish, elite module included) comes from the resolved chain profile
const ch = unit.profile.chain || { count: num(traitBb(chess)['attack@max_target'], 3), radius: CHAIN_RADIUS, sluggish: num(traitBb(chess)['attack@sluggish'], 0.5) };
if (unit.skill && unit.skill.spec.attack) unit.skill.spec.attack.chain = { ...ch, falloff: 0 };
},
};
},
};
@@ -0,0 +1,45 @@
// server/sim/content/kits/ops/chess_char_1_04-udflow.js — 深巡 (char_4137_udflow) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, moduleOn, installReveal, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_04 深巡 行动能力剥夺: longer line range (skill grid), ATK +atk, ASPD +attack_speed, fin darts pierce
// attack@max_target enemies on the line and cause attack@sluggish s of 停顿.
// 技能策略 → DEFAULT (PR #12; DESIGN §21.29): the official 下半 class row hands every MANUAL 重装 skill TAKE_DAMAGE,
// which makes a 2-2 ranged 哨戒铁卫 wait until something hits her — in practice until she blocks (GitHub issue #4). By
// the owner's deliberate deviation from that row (2026-10-03, community feedback) this offensive ranged skill takes
// the basic strategy (SP ready + about to attack + an enemy inside the initial range); her data says DEFAULT too
// (rawRule keeps the official TAKE_DAMAGE). Only the rule changes: spCost / initSp / spType still come from data.
// 细胞活性抑制剂: attacks inflict `damage` arts per `interval` s for `duration` s (damage_seamonster vs 【海怪】).
// Elite module (SPT-X): stealth of enemies inside the range is cancelled.
// Alternate S1 侵袭破坏应对 (重装 ⇒ TAKE_DAMAGE trigger from data): ATK +atk, DEF +def.
chess_char_1_04_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const s1 = skillBbOf(chess, 'skchr_udflow_1');
return {
skill: {
trigger: 'DEFAULT',
kind: 'duration', mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) },
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null, maxTargets: num(bb['attack@max_target'], 1) },
attack: { onHitStatus: { key: 'sluggish', duration: num(bb['attack@sluggish'], 1) } },
},
skills: { skchr_udflow_1: { kind: 'duration', mods: { atkPct: num(s1.atk), defPct: num(s1.def) } } },
talents: [{ install(battle, unit) {
const dur = num(t.duration), iv = num(t.interval, 1);
if (!(dur > 0) || !(num(t.damage) > 0)) return;
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg?.isAttack || ctx.target.side !== 'enemy' || !ctx.target.alive) return;
const sea = (ctx.target.def?.tags || []).includes('seamonster');
const amount = sea ? num(t.damage_seamonster, num(t.damage) * 2) : num(t.damage);
battle.addBuff(ctx.target, {
key: `udflow:dot:${unit.id}`, duration: dur, interval: iv, refresh: 'extend', source: unit, visible: false,
onTick: ({ battle: b, unit: e }) => b.dealDamage(unit, e, { amount, type: 'arts', canDodge: false, tags: ['dot'] }),
});
}, { owner: unit });
} }],
install(battle, unit) { if (moduleOn(chess) && chess.isGolden) installReveal(battle, unit); },
};
},
};
@@ -0,0 +1,33 @@
// server/sim/content/kits/ops/chess_char_1_05-vigna.js — 红豆 (char_290_vigna) kit, tier 1 (hidden).
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_05 红豆 (hidden) 槌音: base attack time +base_attack_time s, ATK +atk. 蛮力穿刺: each attack prob1 (prob2 while the skill
// runs) for ATK +atk on that attack. Elite module (CHG-Y): vs enemies below hp_ratio HP, ATK ×atk_scale.
chess_char_1_05_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess) } },
talents: [{ install(battle, unit) {
const key = 'vigna:proc';
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit) return;
const p = unit.skill?.active ? num(t.prob2) : num(t.prob1);
if (p > 0 && battle.rng.chance(p)) {
battle.addBuff(unit, { key, duration: 0.1, mods: { atkPct: num(t.atk) } });
battle.fx('crit', { x: unit.x, y: unit.y, id: unit.id });
}
}, { owner: unit });
// melee hits resolve synchronously inside the attack: the one-attack buff is dropped right after it
battle.on('attack', (ctx) => { if (ctx.attacker === unit && unit.findBuff(key)) battle.removeBuff(unit, key); }, { owner: unit });
if (tb.hp_ratio != null && num(tb.atk_scale, 1) !== 1) {
onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && target.hpRatio < num(tb.hp_ratio)) dmg.amount *= num(tb.atk_scale, 1); });
}
} }],
};
},
};
@@ -0,0 +1,73 @@
// server/sim/content/kits/ops/chess_char_1_06-vendla.js — 刺玫 (char_494_vendla) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, up, byEnemyAttack, alliesInGridOf, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_06 刺玫 荆藤庇荫: ATK +atk; the ally with the highest max HP in range gets taunt +taunt_level; whenever that ally is
// attacked, 刺玫 deals atk_scale × ATK arts to the attacker, and her trait heal (scale × damage) only goes to that ally.
// 土壤基肥改良: the highest-max-HP ally in range receives heal_scale × healing ("受到的治疗效果提升") — never its natural /
// skill HP regeneration (the 生命回复速度 attribute is "不受治疗加成影响": 宴 分神, 角峰 体能强化; PRTS).
// Alternate S1 战术咏唱·γ型: ASPD +attack_speed (the trait heal keeps going to the most injured ally in range).
// Elite module INC-X (heal 60 % of the damage) is the incantation profile of the module trait (TUNE.incantationmedic).
chess_char_1_06_a: (bb, chess) => {
const hs = num(talentBb(chess, 0).heal_scale, 1);
const tauntKey = 'vendla:taunt';
// the protégé only exists while 荆藤庇荫 runs (mem.protege is set by its onStart only)
const protege = (u) => (u.skill?.active && up(u.mem.protege) ? u.mem.protege : null);
return {
skills: { 'skcom_magic_rage[3]': { kind: 'duration', mods: { aspd: num(skillBbOf(chess, 'skcom_magic_rage[3]').attack_speed) } } },
trait: {
// 咒愈师 trait: EVERY damage she deals heals an ally for 50 % of it (professions.js `installIncantation`,
// buff_template_data `vendla_tr` = ON_AFTER_OUTPUT_DAMAGE) — while 荆藤庇荫 runs her S2 says "仅对该角色触发刺玫
// 特性", so her protégé is the target then; otherwise it is the lowest-HP ally in range.
install(battle, u) {
battle.on('damaged', (c) => {
const t = c.target;
if (c.source !== u || !u.alive || !t || t.side !== 'enemy' || !(c.amount > 0)) return;
if (c.type === 'element' || c.type === 'elemental') return;
const ally = (c.dmg && c.dmg.traitAlly) || protege(u) || battle.lowestHpAllyInRange(u);
if (ally) battle.heal(u, ally, c.amount * (u.profile.healRatio ?? 0.5), { tags: ['incantation'] });
}, { owner: u });
},
},
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit, skill }) {
const cands = alliesInGridOf(battle, unit).sort((a, b) => b.s.maxHp - a.s.maxHp || a.deploySeq - b.deploySeq);
const p = cands[0] ?? null;
unit.mem.protege = p;
if (!p) return;
battle.addBuff(p, { key: tauntKey, duration: skill.duration + 0.1, mods: { taunt: num(bb.taunt_level, 1) }, visible: true, source: unit });
battle.fx('taunt', { x: p.x, y: p.y, id: p.id });
},
onEnd({ battle, unit }) {
if (unit.mem.protege) battle.removeBuff(unit.mem.protege, tauntKey);
unit.mem.protege = null;
},
},
talents: [{ install(battle, unit) {
battle.on('damaged', (ctx) => {
const p = protege(unit);
if (!p || ctx.target !== p || !byEnemyAttack(ctx) || !ctx.source.alive || !unit.canAct) return;
// "并仅对该角色触发刺玫特性": this counter damage is healed by the trait (install above) for the protégé — the
// damage instance names her, so no separate heal here (it would double)
battle.dealDamage(unit, ctx.source, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, canDodge: false, tags: ['counter'], traitAlly: p });
battle.fx('counter', { x: ctx.source.x, y: ctx.source.y, id: unit.id });
}, { owner: unit });
if (hs !== 1) {
let cacheT = -1, top = null;
battle.on('heal', (ctx) => {
if (!up(unit) || ctx.opts?.regen) return;
if (cacheT !== battle.time) {
cacheT = battle.time;
top = alliesInGridOf(battle, unit).sort((a, b) => b.s.maxHp - a.s.maxHp || a.deploySeq - b.deploySeq)[0] ?? null;
}
if (ctx.target === top) ctx.amount *= hs;
}, { owner: unit });
}
} }],
};
},
};
@@ -0,0 +1,64 @@
// server/sim/content/kits/ops/chess_char_1_07-prove.js — 普罗旺斯 (char_145_prove) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import { frontOf } from '../../../dir.js';
import { bodyOnTile } from '../../../body.js';
import { num, talentBb, isMainHit, onHitBy, skillBbOf } from '../shared/tier1.js';
/** 普罗旺斯 杀戮嗅觉 "普通攻击不再以生命值高于80%的敌人作为目标" — the 80 % exists only in the skill text. */
const PROVE_S2_MAX_HP = 0.8;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_07 普罗旺斯 狼眼 (passive): per hp_ratio_drop of HP the target has lost, ATK +atk_scale_up against it — CONTINUOUS,
// not in 20 % steps (PRTS note: "并非以每20%为固定节点 … 攻击力倍率提升至[100%+(100%-目标生命比例)×加成量×5]", i.e.
// ×(1 + lost / hp_ratio_drop × atk_scale_up)).
// 狩猎箭头: each attack prob (prob2 when the enemy is on the tile right in front) for ATK ×atk_scale.
// 狼眼 is an ATK increase ⇒ it scales her (ATK-based) attacks only, never fixed-value damage she sources
// (叙拉古 5000+50·L true proc, fixed item/DoT ticks…).
// Alternate S2 杀戮嗅觉: ATK +atk, but normal attacks never target an enemy above 80 % HP (with only such enemies in
// range she holds her fire). Her attacks stay normal attacks. (狼眼 is S1 only: absent when S2 is carried.)
// Elite module ARC-X only changes stats (ATK/DEF, faster redeploy).
chess_char_1_07_a: (bb, chess) => {
const t = talentBb(chess, 0);
const drop = num(bb.hp_ratio_drop), upPer = num(bb.atk_scale_up);
const S2 = 'skchr_prove_2';
const huntable = (e) => e.hpRatio <= PROVE_S2_MAX_HP + 1e-9;
const s2On = (u) => !!(u.skill && u.skill.active && u.skill.id === S2);
return {
skill: { kind: 'passive' },
skills: { [S2]: { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, S2).atk) } } },
trait: {
canAttack: (battle, u) => !s2On(u) || battle.enemiesInKeys(u.rangeKeys, u, u.profile).some(huntable),
},
install(battle, unit) {
if (unit.skill?.id !== S2) return;
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !s2On(unit) || ctx.targets.every(huntable)) return;
const prof = ctx.profile || unit.profile;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, prof).filter(huntable);
sortEnemyTargets(battle, unit, cands, prof.priority);
ctx.targets = cands.slice(0, Math.max(1, ctx.targets.length));
}, { owner: unit });
},
talents: [{ install(battle, unit) {
onHitBy(battle, unit, ({ target, dmg }) => {
if (!dmg.isAttack) return;
if (drop > 0 && upPer > 0) {
const lost = Math.max(0, Math.min(1, 1 - target.hpRatio));
if (lost > 0) dmg.amount *= 1 + (lost / drop) * upPer;
}
if (!isMainHit(dmg)) return;
const [fr, fc] = frontOf(unit.tileR, unit.tileC, unit.dir);
const front = bodyOnTile(target, fr, fc);
const p = front ? num(t.prob2) : num(t.prob);
if (p > 0 && battle.rng.chance(p)) {
dmg.amount *= num(t.atk_scale, 1);
battle.fx('crit', { x: target.x, y: target.y, id: unit.id });
}
});
} }],
};
},
};
@@ -0,0 +1,43 @@
// server/sim/content/kits/ops/chess_char_1_08-texas.js — 德克萨斯 (char_102_texas) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, enemiesInGrid, skillBbOf, RING1 } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_08 德克萨斯 剑雨: +cost DP; every enemy around (skill grid) takes two hits of atk_scale × ATK arts and is
// stunned `stun` s — air units too (PRTS 备注 "※可对空"). 战术快递: +cost initial DP. (Elite hidden runtime_cost −4
// "首次部署时部署费用-4": the initial deployment is free in battle ⇒ no effect.)
// Alternate S1 冲锋号令·γ型 (AUTO, no target): +cost DP at once. An AUTO skill keeps its normal rule in this mode
// (research 03 §1.4: only MANUAL skills are converted by the trigger table) ⇒ it fires as soon as SP is full.
chess_char_1_08_a: (bb, chess, def) => ({
skills: {
'skcom_charge_cost[3]': {
kind: 'instant', trigger: 'SP_FULL',
onStart({ battle, unit }) {
const n = num(skillBbOf(chess, 'skcom_charge_cost[3]').cost);
battle.addDp(unit.ownerId, n);
battle.fx('dp', { x: unit.x, y: unit.y, n, id: unit.id });
},
},
},
skill: {
kind: 'instant',
onStart({ battle, unit }) {
battle.addDp(unit.ownerId, num(bb.cost));
battle.fx('dp', { x: unit.x, y: unit.y, n: num(bb.cost), id: unit.id });
const grid = def?.skill?.rangeGrid;
const foes = grid ? enemiesInGrid(battle, unit, grid) : battle.foesInRadius(unit.x, unit.y, RING1).filter((e) => !e.s.flags.untargetable);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 2, id: unit.id, skill: 'swordRain' });
for (const e of foes) {
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun), source: unit });
}
},
},
talents: [{ install(battle, unit) {
const n = num(talentBb(chess, 0).cost);
if (n > 0) battle.on('battleStart', () => battle.addDp(unit.ownerId, n), { owner: unit });
} }],
}),
};
@@ -0,0 +1,32 @@
// server/sim/content/kits/ops/chess_char_1_09-caper.js — 跃跃 (char_4100_caper) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, cheb, onHitBy, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_09 跃跃 (回环射手: every attack is a boomerang out to the target and back — she attacks again only once it is
// caught, professions.js loopshooter / ai.js throwBoomerang). 乐趣加倍: ATK +atk, each attack throws `cnt`
// boomerangs at the target (cnt hits); they fly the same path, so the flight carries cnt hits and all of them are
// back together ("必须回收全部回旋投掷物才可以进行下一次攻击"). 戏耍随心: prob per hit (each boomerang on its own) for
// ATK ×atk_scale. Elite module (LPS-X, trait atk_scale): vs enemies on the 8 surrounding tiles ATK ×atk_scale.
// Alternate S1 强力击·β型 (AUTO, attack SP): the next attack at atk_scale × ATK.
chess_char_1_09_a: (bb, chess) => {
const t = talentBb(chess, 0);
const near = num(traitBb(chess).atk_scale, 1);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, attack: { hits: Math.max(1, Math.floor(num(bb.cnt, 1))) } },
skills: { skchr_caper_1: { kind: 'instant', attack: { atkScale: num(skillBbOf(chess, 'skchr_caper_1').atk_scale, 1) } } },
talents: [{ install(battle, unit) {
onHitBy(battle, unit, ({ target, dmg }) => {
if (!dmg.isAttack) return;
if (near !== 1 && cheb(unit, target) <= 1) dmg.amount *= near;
if (!dmg.isSplash && num(t.prob) > 0 && battle.rng.chance(num(t.prob))) {
dmg.amount *= num(t.atk_scale, 1);
battle.fx('crit', { x: target.x, y: target.y, id: unit.id });
}
});
} }],
};
},
};
@@ -0,0 +1,85 @@
// server/sim/content/kits/ops/chess_char_1_10-sunbr.js — 古米 (char_196_sunbr) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, isMainHit, onHitBy, instantKind, skillRec } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_10 古米 备用军粮 (自动触发, ct charges — an AUTO skill: no 技能策略 applies, it keeps its own rule; PRTS 备注 "此技能
// 在存在生命值不满的可治疗角色时可触发;技能触发后古米将切换至治疗模式(普通攻击改为治疗技能范围内的一名友方单位),直至古米
// 完成一次普通攻击的治疗"): cast as soon as a healable ally of the skill range (x-4, herself included) is injured —
// no enemy needed (user playtest #6: it waited until she engaged) — then her next normal attack heals the most
// injured ally of the skill range for heal_scale × ATK instead of hitting an enemy; she makes no normal attack until
// that heal is done. Elite module (GUA-X): targets below hp_ratio HP get ×heal_scale more.
// 平底锅专精: each attack prob for ATK ×atk_scale and a `stun` s stun (only when that hit lands).
// Alternate S2 食粮烹制 (PRTS notes: 10 s 缴械 at the start, the real duration is disarm + 30 s, attacks become heals
// with the attack interval +130 %): cooking for `disarm` s — no attacks, DEF +def; then for the skill's `duration` s
// she heals the most injured ally inside the skill grid (x-4) for ATK per attack, ATK +atk, base attack time
// ×(1 + base_attack_time). The GUA-X heal bonus (below hp_ratio: ×heal_scale) applies to these heals too.
// Trigger: TAKE_DAMAGE (data: the 重装 strategy "不受技能范围影响,受到伤害时释放技能").
chess_char_1_10_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const S2 = 'skchr_sunbr_2';
const r2 = skillRec(chess, S2);
const b2 = r2?.bb ?? {};
const cook = Math.max(0, num(b2.disarm, 10));
const cookKey = 'sunbr:cook', serveKey = 'sunbr:serve';
const healGrid = def?.skill?.rangeGrid ?? null;
return {
skills: {
[S2]: {
kind: 'duration', duration: cook + Math.max(0, num(r2?.duration, 30)),
mods: { batPct: Math.max(0, num(b2.base_attack_time)) },
targeting: { rangeGrid: r2?.rangeGrid ?? def?.skill?.rangeGrid ?? null },
attack: { dmgType: 'heal', heal: { mode: 'single' } },
onStart({ battle, unit }) {
battle.addBuff(unit, { key: cookKey, duration: cook, mods: { defPct: num(b2.def) }, flags: { disarm: true }, tags: ['skill'], visible: true });
},
onTick({ battle, unit }) {
if (!unit.findBuff(cookKey) && !unit.findBuff(serveKey)) {
battle.addBuff(unit, { key: serveKey, mods: { atkPct: num(b2.atk) }, tags: ['skill'] });
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'heal' });
}
},
onEnd({ battle, unit }) {
battle.removeBuff(unit, cookKey);
battle.removeBuff(unit, serveKey);
},
},
},
install(battle, unit) {
if (tb.hp_ratio == null || num(tb.heal_scale, 1) === 1) return;
// GUA-X on her skill heals (S1's heal-mode attack, S2's attack heals while it runs — herself included, she stands
// in the grid), never her natural / self regeneration
battle.on('heal', (ctx) => {
if (ctx.source !== unit || !unit.skill?.active || ctx.opts?.regen || ctx.opts?.self) return;
if (ctx.target.hpRatio < num(tb.hp_ratio)) ctx.amount *= num(tb.heal_scale, 1);
}, { owner: unit });
},
skill: {
kind: instantKind(def),
heal: true,
...(healGrid ? { trigger: { rule: 'SKILL_RANGE', grid: healGrid, allies: true }, targeting: { rangeGrid: healGrid } } : {}),
// the heal-mode attack: the most injured ally of the skill range (acquireTargets, heal profile)
attack: { dmgType: 'heal', heal: { mode: 'single' }, healScale: num(bb.heal_scale, 1), projectile: 'none' },
onHit({ battle, target }) { if (target) battle.fx('heal', { x: target.x, y: target.y, id: target.id }); },
},
talents: [{ install(battle, unit) {
// the proc is rolled per attack hit; the stun only follows a hit that landed (not dodged / cancelled)
const procs = new WeakSet();
onHitBy(battle, unit, ({ target, dmg }) => {
if (!isMainHit(dmg) || !(num(t.prob) > 0) || !battle.rng.chance(num(t.prob))) return;
dmg.amount *= num(t.atk_scale, 1);
procs.add(dmg);
battle.fx('crit', { x: target.x, y: target.y, id: unit.id });
});
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg || !procs.has(ctx.dmg) || !ctx.target.alive) return;
procs.delete(ctx.dmg);
battle.applyStatus(ctx.target, 'stun', { duration: num(t.stun), source: unit });
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,15 @@
// server/sim/content/kits/ops/chess_char_1_11-skgoat.js — 地灵 (char_183_skgoat) kit, tier 1 (hidden).
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_11 地灵 (hidden) 攻击力强化·β型: ATK +atk. Elite 地质勘探: trait 停顿 +sluggish s (module trait 1.2 s included).
chess_char_1_11_a: (bb, chess) => {
const add = num(talentBb(chess, 0).sluggish);
const kit = { skill: { kind: 'duration', mods: { atkPct: num(bb.atk) } } };
if (add > 0) kit.trait = { onHitStatus: { key: 'sluggish', duration: num(traitBb(chess).sluggish, 0.8) + add } };
return kit;
},
};
@@ -0,0 +1,34 @@
// server/sim/content/kits/ops/chess_char_1_12-estell.js — 艾丝黛尔 (char_127_estell) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { absoluteRangeKeys } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
import { num, talentBb, talentGrid, traitBb, up, cheb, onHitOn, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_12 艾丝黛尔 舍身突击 (hurt SP): ATK +atk, cannot be healed by others. 自愈能力: an enemy dying within the talent
// grid (8 surrounding tiles) heals her hp_ratio × max HP. Elite module (CEN-Y): above hp_ratio HP, physical damage
// taken −damage_resistance.
// Alternate S1 攻击力强化·β型: ATK +atk (she stays a heal target).
chess_char_1_12_a: (bb, chess) => {
const t = talentBb(chess, 0);
const grid = talentGrid(chess, 0);
const tb = traitBb(chess);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, flags: { noHeal: true } },
skills: { 'skcom_atk_up[2]': { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, 'skcom_atk_up[2]').atk) } } },
talents: [{ install(battle, unit) {
battle.on('death', (ctx) => {
const e = ctx.unit;
if (e.side !== 'enemy' || ctx.reason !== 'killed' || !up(unit)) return;
const near = grid ? bodyInKeys(e, absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0)) : cheb(unit, e) <= 1;
if (near) battle.heal(unit, unit, unit.s.maxHp * num(t.hp_ratio), { self: true, tags: ['talent'] });
}, { owner: unit });
if (tb.damage_resistance != null) {
onHitOn(battle, unit, ({ dmg }) => { if (dmg.type === 'phys' && unit.hpRatio > num(tb.hp_ratio)) dmg.mul *= 1 - num(tb.damage_resistance); });
}
} }],
};
},
};
@@ -0,0 +1,64 @@
// server/sim/content/kits/ops/chess_char_1_13-podego.js — 波登可 (char_258_podego) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import {
num, talentBb, moduleBb, up, enemiesInGrid, enemyInRange, installAura, spTimeBonus, makeZone, skillBbOf,
} from '../shared/tier1.js';
// =================================================================================================================
// tier-local constants that exist nowhere in data
/** Radius (tiles) of 波登可's spore cloud. No blackboard key; PRTS 波登可 备注 "孢子群范围半径为0.9,可对空". */
const SPORE_RADIUS = 0.9;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_13 波登可 孢子扩散: a vial bursts on the current target into a projectile_delay_time s spore cloud (radius 0.9,
// PRTS 备注, 可对空): enemies inside are 停顿 and lose their abilities (silence) and take atk_scale × ATK arts per second.
// 园丁: every 【辅助】 operator on the field ATK +atk. Elite module (DEC-X): +sp_recovery_per_sec SP/s with an enemy in range.
// Alternate S1 花香疗法: ATK +atk, normal attacks heal the most injured ally in range instead (ATK per attack). A heal
// skill: DEFAULT fires it with an injured ally inside her initial range (research 03 §1.4) — also checked every tick,
// since she is only "about to attack" with an enemy in range.
chess_char_1_13_a: (bb, chess) => {
const t = talentBb(chess, 0);
const spBonus = num(moduleBb(chess).sp_recovery_per_sec);
const dur = num(bb.projectile_delay_time, 5);
const S1 = 'skchr_podego_1';
return {
skills: {
[S1]: { kind: 'duration', heal: true, mods: { atkPct: num(skillBbOf(chess, S1).atk) }, attack: { dmgType: 'heal', heal: { mode: 'single' } } },
},
install(battle, unit) {
if (unit.skill?.id !== S1) return;
battle.on('tick', () => {
const sk = unit.skill;
if (!up(unit) || !unit.canAct || !sk.ready || sk.active || sk.opCooling || unit.s.flags.silence) return;
if (battle.injuredAlliesInKeys(unit.baseRangeKeys || unit.rangeKeys, unit).length) sk.activate('DEFAULT');
}, { owner: unit });
},
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const tgt = enemiesInGrid(battle, unit, null, { n: 1 })[0];
const x = tgt ? tgt.x : unit.x + unit.fwd[1], y = tgt ? tgt.y : unit.y + unit.fwd[0];
const atk = unit.s.atk;
makeZone(battle, unit, {
x, y, radius: SPORE_RADIUS, duration: dur, skill: 'spores', onPulse(b) {
for (const e of b.foesInRadius(x, y, SPORE_RADIUS)) {
if (e.s.flags.untargetable) continue;
b.applyStatus(e, 'sluggish', { duration: 1.05, source: unit });
b.applyStatus(e, 'silence', { duration: 1.05, source: unit });
b.dealDamage(unit, e, { amount: atk * num(bb.atk_scale), type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'zone'] });
}
},
});
},
},
talents: [{ install(battle, unit) {
const v = num(t.atk);
if (v) installAura(battle, unit, { key: 'talent:podego', value: v, select: (a) => a.kind === 'op' && a.def.profession === 'SUPPORT', mods: { atkPct: v } });
spTimeBonus(battle, unit, spBonus, () => enemyInRange(battle, unit));
} }],
};
},
};
@@ -0,0 +1,22 @@
// server/sim/content/kits/ops/chess_char_1_14-greyy.js — 格雷伊 (char_253_greyy) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_14 格雷伊 静电释放: ASPD +attack_speed, the talent 停顿 lasts talent_scale × longer.
// 静电场: attacks cause `sluggish` s of 停顿 to the attacked target.
// Alternate S1 战术咏唱·β型: ASPD +attack_speed (the talent 停顿 keeps its length). Elite module SPC-Y: cost only.
chess_char_1_14_a: (bb, chess) => {
const sl = num(talentBb(chess, 0).sluggish);
return {
skills: { 'skcom_magic_rage[2]': { kind: 'duration', mods: { aspd: num(skillBbOf(chess, 'skcom_magic_rage[2]').attack_speed) } } },
trait: sl > 0 ? { onHitStatus: { key: 'sluggish', duration: sl } } : undefined,
skill: {
kind: 'duration', mods: { aspd: num(bb.attack_speed) },
attack: sl > 0 ? { onHitStatus: { key: 'sluggish', duration: sl * num(bb.talent_scale, 1) } } : {},
},
};
},
};
@@ -0,0 +1,54 @@
// server/sim/content/kits/ops/chess_char_1_15-pithst.js — 盟约·辅助干员 (char_616_pithst) kit, tier 1 (hidden).
// Registered with the tier-6 kits, whose conventions are in ../shared/tier6.js; kit contract and rules: ../README.md.
import { hasHp } from '../../../damage.js';
import { num, tbb, isElite, elementDmg } from '../shared/tier6.js';
// ------------------------------------------------------------------------------------------------------------------
// 盟约·辅助干员 chess_char_1_15 (巫役) — S1 战术咏唱·双型; 迭代元素
/** 迭代元素's elements in the order they are applied: 神经损伤 "(优先)", then 灼燃损伤, then 凋亡损伤. */
export const PITHST_ELEMENTS = Object.freeze(['neural', 'burn', 'apoptosis']);
/**
* 迭代元素 "攻击同时附带18%攻击力的神经损伤(优先)、灼燃损伤、凋亡损伤;优先攻击未处于损伤爆发的目标":
* * timing = every damage she deals (PRTS 盟约·辅助干员 corrects "攻击同时" to "造成伤害时"): HP damage > 0 — a dodged
* or fully absorbed hit deals none; her own element fills and the bursts they cause never re-trigger it; a killing
* blow attaches nothing (the target is dead — no burst on the corpse);
* * each such damage attaches ep_damage_ratio × ATK of ALL THREE elements, 神经 first, then 灼燃, then 凋亡 (user
* playtest #5 #3: the kit used to pick one element "not bursting" — but a burst's 爆发冷却 locks every gauge of the
* unit, so it only ever filled 神经). PRTS 元素: when one unit applies several elements that would each fill their
* gauge, the element applied first bursts — from her alone 神经 bursts (the "(优先)"), while her 灼燃 / 凋亡 add to
* the gauges the team builds (余, 塑心 …). [ASSUMED: simultaneous application — the reading of the official text]
* * elite module RIT-X "对精英和领袖敌人造成的元素损伤提升18%" = ep_damage_ratio_boss (0.2124) vs ELITE / BOSS enemies:
* a talent ratio, not an "元素损伤提升" (PRTS 备注), so it changes nothing else she deals (e.g. a 灼燃维式重锤).
* The target pick prefers enemies not in a burst (`priority: 'notBurst'`).
*/
function pithst(bb, chess, def) {
const t0 = tbb(def, 0);
const ratio = num(t0.ep_damage_ratio);
const ratioBoss = num(t0.ep_damage_ratio_boss, ratio);
return {
skill: {
kind: 'duration',
mods: { aspd: num(bb.attack_speed) },
targeting: { maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 1))) },
},
trait: { priority: 'notBurst' },
install(battle, unit) {
if (!(ratio > 0)) return;
battle.on('damaged', (ctx) => {
const t = ctx.target, d = ctx.dmg;
// hasHp: a killing blow attaches nothing (the hook runs before battle.kill, see hasHp)
if (ctx.source !== unit || !t || t.side !== 'enemy' || !hasHp(t) || !(ctx.amount > 0)) return;
if (ctx.type === 'element' || ctx.type === 'elemental' || (d && d.tags && d.tags.includes('burst'))) return;
const amount = unit.s.atk * (isElite(t) ? ratioBoss : ratio);
for (const el of PITHST_ELEMENTS) elementDmg(battle, unit, t, el, amount, ['talent', 'pithst']);
}, { owner: unit });
},
};
}
export default {
chess_char_1_15_a: pithst,
};
@@ -0,0 +1,67 @@
// server/sim/content/kits/ops/chess_char_1_16-tinman.js — 锡人 (char_4151_tinman) kit, tier 1 (hidden).
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, moduleBb, enemiesInGrid, once, spTimeBonus, makeZone, instantKind } from '../shared/tier1.js';
// =================================================================================================================
// kit builders shared by two chess ids
/**
* 锡人 “大拉里” (chess_char_1_16 hidden and chess_char_2_19): throw an alchemy unit at the current target —
* for projectile_delay_time s, ground enemies within projectile_range tiles take atk_scale × ATK arts per second,
* allies there recover hp_recovery_per_sec_ratio × ATK per second. Elite: 凋敝魂灵 (skill@damage_scale: damage-over-
* time taken by ground enemies inside a unit ×1.2) and the module's +sp_recovery_per_sec SP while a unit exists.
*/
export function tinmanKit(bb, chess, def) {
const dur = num(bb.projectile_delay_time, 10);
const radius = num(bb.projectile_range, 1.5);
const dmgScale = num(bb.atk_scale);
const healRatio = num(bb.hp_recovery_per_sec_ratio);
const wither = num(talentBb(chess, 1)['skill@damage_scale'], 1);
const spBonus = num(moduleBb(chess).sp_recovery_per_sec);
const witherKey = 'tinman:wither';
return {
skill: {
kind: instantKind(def),
onStart({ battle, unit }) {
const tgt = enemiesInGrid(battle, unit, null, { n: 1, groundOnly: true })[0] ?? enemiesInGrid(battle, unit, null, { n: 1 })[0];
const x = tgt ? tgt.x : unit.x + unit.fwd[1], y = tgt ? tgt.y : unit.y + unit.fwd[0];
const atk = unit.s.atk;
unit.mem.tinZones = (unit.mem.tinZones ?? 0) + 1;
makeZone(battle, unit, {
x, y, radius, duration: dur, skill: 'tinman', onPulse(b) {
for (const e of b.foesInRadius(x, y, radius)) {
if (e.isFlying || e.s.flags.untargetable) continue;
if (wither > 1) b.addBuff(e, { key: witherKey, duration: 1.05, data: { mul: wither }, source: unit });
b.dealDamage(unit, e, { amount: atk * dmgScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'zone'] });
}
if (healRatio > 0) for (const a of b.alliesInRadius(x, y, radius, null)) if (a.hp < a.s.maxHp) b.heal(unit, a, atk * healRatio, { tags: ['zone'] });
},
onEnd() { unit.mem.tinZones = Math.max(0, (unit.mem.tinZones ?? 1) - 1); },
});
},
},
talents: [{ install(battle, unit) {
if (wither > 1) {
// "持续伤害" = damage over time only: 'dot'-tagged ticks and the per-second ticks of a necrosis / apoptosis
// burst — never instantaneous bursts (burn/neural bursts, skill/summon bursts are also tagged 'burst')
once(battle, witherKey, () => battle.on('hit', (ctx) => {
if (!ctx.target || ctx.target.side !== 'enemy') return;
const tags = ctx.dmg.tags || [];
if (!(tags.includes('dot') || tags.includes('necrosis') || tags.includes('apoptosis'))) return;
const w = ctx.target.findBuff(witherKey);
if (w) ctx.dmg.amount *= w.data?.mul ?? 1;
}));
}
spTimeBonus(battle, unit, spBonus, () => (unit.mem.tinZones ?? 0) > 0);
} }],
};
}
export default {
// 1_15 盟约·辅助干员 (hidden, band 优等生): its kit is tier6.js `pithst` (迭代元素 — one implementation only).
// ---------------------------------------------------------------------------------------------------------------
// 1_16 锡人 (hidden tier-1 entry of chess_char_2_19): see tinmanKit.
chess_char_1_16_a: tinmanKit,
};
@@ -0,0 +1,71 @@
// server/sim/content/kits/ops/chess_char_1_17-indigo.js — 深靛 (char_469_indigo) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import { num, talentBb, skillRec, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_17 深靛 光影迷宫: attack interval ×base_attack_time, talent chance ×talent_scale, bound enemies in range take
// indigo_s_2[damage].atk_scale × ATK arts every indigo_s_2[damage].interval s.
// 柔光缚目: attacks bind the target `duration` s with `prob`; bound enemies are never chosen as targets.
// Alternate S1 灯塔守卫者: for `duration` s the skill range (4-1), attack interval ×(1 + base_attack_time) (PRTS:
// "大幅度缩短(-80%)" ⇒ ×0.2 — a flat −0.8 s on 3 s would be less than S2's "略微缩短 ×0.7"), every attack hits for
// attack@atk_scale × ATK arts. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic).
chess_char_1_17_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const dScale = num(bb['indigo_s_2[damage].atk_scale']);
const dIv = num(bb['indigo_s_2[damage].interval'], 0.5);
const bound = (e) => !!e.s.flags.bind;
const r1 = skillRec(chess, 'skchr_indigo_1');
const b1 = r1?.bb ?? {};
return {
skills: {
skchr_indigo_1: {
kind: 'duration', mods: { batPct: Math.max(-0.95, Math.min(0, num(b1.base_attack_time))) },
targeting: { rangeGrid: r1?.rangeGrid ?? null },
attack: { atkScale: num(b1['attack@atk_scale'], 1) },
},
},
trait: {
// with only bound enemies in range (or blocked by her — always her targets, Battle.blockedTargets) she holds her
// fire (the mystic trait stores the energy meanwhile)
canAttack(battle, u) {
const ok = battle.enemiesInKeys(u.rangeKeys, u, u.profile).some((e) => !bound(e)) || battle.blockedTargets(u, u.profile).some((e) => !bound(e));
if (!ok) u.trait.hadTarget = false;
return ok;
},
afterHit(battle, u, target) {
if (!target || !target.alive || target.side !== 'enemy') return;
const p = num(t.prob) * (u.skill?.active ? num(bb.talent_scale, 1) : 1);
if (p > 0 && battle.rng.chance(Math.min(1, p))) battle.applyStatus(target, 'bind', { duration: num(t.duration), source: u });
},
},
skill: {
kind: 'duration', mods: { batPct: batMod(bb.base_attack_time, chess, def?.skill?.description ?? chess?.skill?.desc) },
onStart({ unit }) { unit.mem.indigoAcc = 0; },
onTick({ battle, unit, dt }) {
if (!(dScale > 0) || !(dIv > 0)) return;
unit.mem.indigoAcc = (unit.mem.indigoAcc ?? 0) + dt;
while (unit.mem.indigoAcc >= dIv - 1e-9) {
unit.mem.indigoAcc -= dIv;
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) {
if (bound(e)) battle.dealDamage(unit, e, { amount: unit.s.atk * dScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot'] });
}
}
},
},
talents: [{ install(battle, unit) {
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !ctx.targets.some(bound)) return;
const prof = ctx.profile || unit.profile;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, prof);
for (const e of battle.blockedTargets(unit, prof)) if (!cands.includes(e)) cands.push(e);
for (let i = cands.length - 1; i >= 0; i--) if (bound(cands[i])) cands.splice(i, 1);
sortEnemyTargets(battle, unit, cands, prof.priority);
ctx.targets = cands.slice(0, Math.max(1, ctx.targets.length));
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,57 @@
// server/sim/content/kits/ops/chess_char_1_18-utage.js — 宴 (char_337_utage) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, up, onHitBy, onHitOn, skillBbOf } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_18 宴 落地斩·破门 (passive): at each deployment lose hp_ratio of current HP, then for `duration` s ATK +atk and
// attacks deal arts damage. 认真模式: 坚忍 ASPD up to +min_attack_speed, reached at min_hp_ratio HP (linear).
// Elite (module talent): below hp_ratio HP, 庇护 −damage_resistance physical/arts damage taken.
// Alternate S1 分神: stops attacking, block count 0 (her blocked enemies walk on), DEF +def, recovers
// hp_recovery_per_sec_by_max_hp_ratio × max HP per second (the 生命回复速度 attribute: works under her 武者 no-heal).
chess_char_1_18_a: (bb, chess) => {
const t = talentBb(chess, 0);
const buffKey = 'utage:s2';
const s1 = skillBbOf(chess, 'skchr_utage_1');
return {
skills: {
skchr_utage_1: {
kind: 'duration',
mods: { defPct: num(s1.def), hpRegenRatio: num(s1.hp_recovery_per_sec_by_max_hp_ratio), blockCnt: -99 },
attack: { noAttack: true },
onStart({ battle, unit }) { battle.releaseBlocked(unit); },
},
},
skill: {
kind: 'passive',
onStart({ battle, unit }) {
const loss = unit.hp * num(bb.hp_ratio);
if (loss > 0 && unit.hp - loss >= 1) battle.loseHp(unit, loss, { source: unit });
battle.addBuff(unit, { key: buffKey, duration: num(bb.duration), mods: { atkPct: num(bb.atk) }, tags: ['skill'], visible: true });
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: 'breach' });
},
},
talents: [{ install(battle, unit) {
onHitBy(battle, unit, ({ dmg }) => { if (dmg.isAttack && dmg.type === 'phys' && unit.findBuff(buffKey)) dmg.type = 'arts'; });
const maxAs = num(t.min_attack_speed), minHp = num(t.min_hp_ratio);
if (maxAs > 0 && minHp < 1) {
battle.on('tick', () => {
if (!up(unit)) return;
const v = maxAs * Math.max(0, Math.min(1, (1 - unit.hpRatio) / (1 - minHp)));
const cur = unit.findBuff('utage:serious');
if (cur && Math.abs((cur.data?.v ?? 0) - v) < 0.5) return;
if (v < 0.5) { if (cur) battle.removeBuff(unit, 'utage:serious'); return; }
battle.addBuff(unit, { key: 'utage:serious', mods: { aspd: v }, data: { v }, tags: ['talent'] });
}, { owner: unit });
}
// 庇护 (ba.protect): "受到的物理和法术伤害降低相应比例" — physical and arts damage only
if (t.damage_resistance != null && t.hp_ratio != null) {
onHitOn(battle, unit, ({ dmg }) => {
if ((dmg.type === 'phys' || dmg.type === 'arts') && unit.hpRatio < num(t.hp_ratio)) dmg.mul *= 1 - num(t.damage_resistance);
});
}
} }],
};
},
};
@@ -0,0 +1,67 @@
// server/sim/content/kits/ops/chess_char_1_19-wildmn.js — 野鬃 (char_496_wildmn) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import { num, talentBb, skillRec } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_19 野鬃 夹枪冲锋: wider range (skill grid), ATK +atk, "攻击会把目标往攻击方向中等力度地推开" (attack@force 1 = 中力) by
// the official 力度 − 重量 distance (Battle.push: weight 0 → 2.14 tiles, 1 → 1.7, 2 → 0.44, 3 → 0.12, ≥ 4 → none; user
// playtest #6 item 14). A directional push (方向力, PRTS 推与拉): the client's S2 attack ability (charpack
// char_496_wildmn, anim Skill_2) carries buff wildmn_s_2[force] of template knockback[dir] — buff_template_data:
// Knockback {_useSourceDirection: true, _decreaseForceLevelWhenNotInDirection: 2} — i.e. along her deploy direction,
// radial at 受力等级 −2 for a target > 45° off it or < 0.25 tile away (特殊修正); the radial pushes are
// knockback[relative] (琳琅诗怀雅 S3, 山 S3, 莫斯提马 S3). Her text uses the 推击手 wording "往攻击方向". So an enemy she
// grabs behind her centre (the hand-over after a push freed her block) is not thrown 1.7 tiles towards the objective
// (player feedback D2, "往攻击方向相反方向推"; it was a radial push before).
// 一致向前: after deploying (normal flag 0: first deployment only; elite flag 1: every deployment) every undeployed
// 【近卫】 operator of the player costs `value` less DP to deploy (≤ max_stack_cnt per operator until it deploys).
// Alternate S1 骑枪刺击 (PASSIVE, ON_DEPLOY): for `duration` s after every deployment ASPD +attack_speed.
// Elite module CHG-X (+2 DP per kill) is the charger profile of the module trait (TUNE.charger).
chess_char_1_19_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const force = num(bb['attack@force'], 1);
const r1 = skillRec(chess, 'skchr_wildmn_1');
return {
skills: {
skchr_wildmn_1: {
kind: 'passive',
onStart({ battle, unit }) {
const d = num(r1?.duration);
if (d > 0) battle.addBuff(unit, { key: 'wildmn:s1', duration: d, mods: { aspd: num(r1?.bb?.attack_speed) }, tags: ['skill'], visible: true });
},
},
},
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null },
attack: {
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.side !== 'enemy') return;
battle.push(target, force, { from: unit, dir: { x: unit.fwd[1], y: unit.fwd[0] } });
},
},
},
talents: [{ install(battle, unit) {
const cut = Math.abs(num(t.value, -1)), cap = num(t.max_stack_cnt, 1), every = num(t.flag) === 1;
let deploys = 0;
battle.on('deploy', ({ unit: u }) => {
if (u === unit) {
deploys++;
if (!every && deploys > 1) return;
for (const a of battle.allyUnits) {
if (a === unit || a.kind !== 'op' || a.ownerId !== unit.ownerId || a.alive || a.removed || a.def.profession !== 'WARRIOR') continue;
const done = a.mem.wildmnCut ?? 0;
const d = Math.min(cut, cap - done, a.base.cost);
if (d > 0) { a.base.cost -= d; a.mem.wildmnCut = done + d; }
}
} else if (u.mem.wildmnCut > 0) {
// the discount is consumed by this deployment
u.base.cost += u.mem.wildmnCut;
u.mem.wildmnCut = 0;
}
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,81 @@
// server/sim/content/kits/ops/chess_char_1_20-liskam.js — 雷蛇 (char_107_liskam) kit, tier 1.
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
import {
num, talentBb, talentGrid, moduleOn, up, hurtSpDamage, giveSp, onDamagedOn, alliesInGridOf, statBuff, installReveal,
skillBbOf,
} from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 1_20 雷蛇 反击电弧 (hurt SP): attack interval ×(1 + base_attack_time) (PRTS "攻击间隔增大(+70%)": a RATIO for this skill,
// 1.2 → 2.04 s — not +0.7 s), ATK +atk, arts attacks on up to attack@max_target enemies,
// attack@buff_prob to stun attack@stun s; afterwards 雷蛇 is stunned `stun` s.
// 技能策略 → DEFAULT (PR #12; DESIGN §21.29): 反击电弧 is an offensive skill (ATK +125 %, arts hits on up to 3 enemies,
// stun); the official 下半 TANK row (TAKE_DAMAGE for every MANUAL 重装 skill) made it — and 深巡's S2 — wait for a hit
// (GitHub issue #4). The owner's deliberate deviation from that row (2026-10-03, community feedback) gives both
// 哨戒铁卫 S2s the basic strategy; the data says DEFAULT too (rawRule keeps the official TAKE_DAMAGE).
// 战术防御: when attacked, +sp SP to herself and to one random ally in the talent grid. Elite 雷抗: RES +magic_resistance.
// Elite module (SPT-X): stealth of enemies inside the range is cancelled.
// Alternate S1 充能防御 (AUTO, hurt SP, "技能自动开启" — an AUTO skill takes no 技能策略: SP_FULL, so the hit that fills SP
// sets it off): PRTS "技能拥有8s的持续时间": for bb.duration s DEF +def, and the next damage instance taken is blocked
// (cancelled — a blocked hit "导致第一天赋无法触发": no 战术防御 SP, no hurt SP).
chess_char_1_20_a: (bb, chess) => {
const t = talentBb(chess, 0);
const grid = talentGrid(chess, 0);
const S1 = 'skchr_liskam_1';
const b1 = skillBbOf(chess, S1);
const blockKey = 'liskam:block';
return {
skills: {
[S1]: {
kind: 'duration', duration: num(b1.duration, 8), mods: { defPct: num(b1.def) }, trigger: 'SP_FULL',
onStart({ battle, unit, skill }) {
battle.addBuff(unit, { key: blockKey, duration: skill.duration + 0.05, tags: ['skill'], visible: true });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { battle.removeBuff(unit, blockKey); },
},
},
install(battle, unit) {
if (moduleOn(chess) && chess.isGolden) installReveal(battle, unit);
if (unit.skill?.id !== S1) return;
battle.on('hit', (ctx) => {
if (ctx.target !== unit || ctx.dmg.cancel || !(ctx.dmg.amount > 0) || !unit.findBuff(blockKey)) return;
ctx.dmg.cancel = true;
battle.removeBuff(unit, blockKey);
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit, priority: 50 });
},
skill: {
trigger: 'DEFAULT',
kind: 'duration', mods: { atkPct: num(bb.atk), batPct: Math.max(0, num(bb.base_attack_time)) },
targeting: { maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 1))) },
attack: {
dmgType: 'arts',
onHit({ battle, unit, target }) {
if (target && target.alive && battle.rng.chance(num(bb['attack@buff_prob']))) battle.applyStatus(target, 'stun', { duration: num(bb['attack@stun']), source: unit });
},
},
onEnd({ battle, unit, reason }) {
if (reason !== 'death' && unit.alive && num(bb.stun) > 0) battle.applyStatus(unit, 'stun', { duration: num(bb.stun), source: unit });
},
},
talents: [{ install(battle, unit) {
const sp = num(t.sp);
if (sp > 0) {
// PRTS 备注 "仅伤害量不为0且能够触发受击回复的伤害才能触发此天赋": any such damage, not only an enemy attack
// (a zone tick, the 源石溶剂 drain — player report D1 audit)
onDamagedOn(battle, unit, (ctx) => {
if (!hurtSpDamage(ctx) || !up(unit)) return;
giveSp(unit, sp);
const mates = alliesInGridOf(battle, unit, grid ?? [[1, 0], [0, -1], [0, 1], [-1, 0]]).filter((a) => a !== unit && a.skill && !a.skill.noSkill);
const m = battle.rng.pick(mates);
if (m) giveSp(m, sp);
});
}
statBuff(battle, unit, 'talent:liskam', { resFlat: num(talentBb(chess, 1).magic_resistance) });
} }],
};
},
};
@@ -0,0 +1,21 @@
// server/sim/content/kits/ops/chess_char_2_01-excu.js — 送葬人 (char_279_excu) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, statBuff, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_01 送葬人 最终旅程: normal attacks become double hits, base attack time +base_attack_time s (−0.5 on 2.3 s
// "少量缩短"). 终结改装: ignore def_penetrate_fixed DEF. (Trait: every enemy in range, ×atk_scale on its front row —
// reaperrange profile.)
//
// S1 铳口收束 (alt): ATK +atk; "攻击时对攻击范围内的所有敌人应用特性加成" — every enemy in range takes the trait's
// front-row multiplier (profile frontScale: trait atk_scale 1.5, elite module 1.6, elite without module 1.5).
chess_char_2_01_a: (bb, chess) => ({
skill: { kind: 'duration', mods: { batPct: batMod(bb.base_attack_time, chess) }, attack: { hits: 2 } },
skills: {
skchr_excu_1: { kind: 'duration', mods: { atkPct: num(bb.atk) }, attack: { dmgMul: (battle, u) => num(u.profile?.frontScale, 1.5) } },
},
talents: [{ install(battle, unit) { statBuff(battle, unit, 'talent:excu', { defIgnoreFlat: num(talentBb(chess, 0).def_penetrate_fixed) }); } }],
}),
};
@@ -0,0 +1,36 @@
// server/sim/content/kits/ops/chess_char_2_02-silent.js — 赫默 (char_108_silent) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { releaseSkillSummon } from '../../tokens.js';
import { num, talentBb, traitBb, installAura } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_02 赫默 医疗无人机 (AUTO, heal trigger): "获得一个医疗无人机 / 最多可库存1个无人机;无人机投入战场后治疗周围友军,
// 10秒后自动销毁" — the 医疗探机 is a hand piece the player places (user playtest #6; PRTS 卫戍协议/帮助): each cast gives
// one (stock ≤ cnt) and the placed piece takes the field on its own tile (tokens.js releaseSkillSummon: not at the
// battle start — user playtest #4; not placed ⇒ no drone); it heals around itself and self-destructs after 10 s (token
// kit). 强化注射: every 【医疗】 operator on the field ASPD +attack_speed. Elite module (PHY-Y): heals on ground units
// ×heal_scale (module 'none': no bonus).
// S1 治疗强化·γ型 (alt): ATK +atk for its duration (no drone).
chess_char_2_02_a: (bb, chess) => {
const tokId = chess?.skill?.overrideTokenKey ?? (chess?.tokens ?? [])[0] ?? 'token_10000_silent_healrb';
const cnt = Math.max(1, Math.floor(num(bb.cnt, 1)));
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: {
kind: 'instant', heal: true,
onStart({ battle, unit }) { releaseSkillSummon(battle, unit, tokId, { cap: cnt }); },
},
skills: { 'skcom_heal_up[3]': { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
const v = num(t.attack_speed);
if (v) installAura(battle, unit, { key: 'talent:silent', value: v, select: (a) => a.kind === 'op' && a.def.profession === 'MEDIC', mods: { aspd: v } });
if (num(tb.heal_scale, 1) !== 1) {
battle.on('heal', (ctx) => { if (ctx.source === unit && ctx.target.ground) ctx.amount *= num(tb.heal_scale, 1); }, { owner: unit });
}
} }],
};
},
};
@@ -0,0 +1,39 @@
// server/sim/content/kits/ops/chess_char_2_03-slchan.js — 崖心 (char_173_slchan) kit, tier 2 (hidden).
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, enemiesInGrid, toggleBuff } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_03 崖心 (hidden) 束缚链: up to max_target enemies in the large front grid are dragged (force 1 = 中力; Battle.pullToFront:
// the official 力度 − 重量 pull — weight ≤ 1 all the way in front of her, 2 a third of the way, 3 barely, ≥ 4 not at all),
// take atk_scale × ATK true damage and are stunned `stun` s. 雪境猎手: ATK/DEF +atk/+def while not blocking.
// Elite module (HOK-X, trait value/dist): dragged enemies take `value` arts per `dist` tiles travelled.
chess_char_2_03_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const force = num(bb.force, 1);
return {
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const foes = enemiesInGrid(battle, unit, def?.skill?.rangeGrid ?? null, { n: Math.max(1, Math.floor(num(bb.max_target, 1))) });
for (const e of foes) {
const sx = e.x, sy = e.y;
const moved = battle.pullToFront(e, unit, force);
battle.fx('pull', { x: e.x, y: e.y, id: e.id, fromX: sx, fromY: sy });
if (moved > 0 && num(tb.value) > 0) {
battle.dealDamage(unit, e, { amount: num(tb.value) * moved / Math.max(0.01, num(tb.dist, 1)), type: 'arts', isSkill: true, canDodge: false, tags: ['drag'] });
}
if (!e.alive) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'true', isSkill: true, tags: ['skill'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun), source: unit });
}
},
},
talents: [{ install(battle, unit) {
if (num(t.atk) || num(t.def)) toggleBuff(battle, unit, 'slchan:hunter', () => unit.blocking.length === 0, { atkPct: num(t.atk), defPct: num(t.def) });
} }],
};
},
};
@@ -0,0 +1,49 @@
// server/sim/content/kits/ops/chess_char_2_04-grabds.js — 小满 (char_4122_grabds) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import {
num, talentBb, moduleBb, traitBb, enemiesInGrid, enemyInRange, statBuff, spTimeBonus, instantKind,
} from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_04 小满 乡音沉沉: stops attacking and puts max_target enemies in range to sleep `sleep` s; afterwards ASPD
// +attack_speed and attacks max_target enemies. 好好听话: ASPD +attack_speed; vs 【野生动物】 (tag `infection`,
// "野生的被感染生物") the trait 停顿 lasts +sluggish_addition s. Elite module (DEC-X): +sp_recovery_per_sec SP/s with
// an enemy in range.
// S1 竹笛飞声 (alt, 2 charges): the next attack deals atk_scale × ATK arts and hits one more enemy (+1 target).
chess_char_2_04_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const n = Math.max(1, Math.floor(num(bb.max_target, 1)));
const sleep = num(bb.sleep);
return {
trait: {
onHitStatus: null,
canAttack: (battle, u) => !(u.skill?.active && battle.time < (u.mem.grainQuietUntil ?? -1)),
afterHit(battle, u, target) {
if (!target || !target.alive || target.side !== 'enemy') return;
const beast = (target.def?.tags || []).includes('infection');
battle.applyStatus(target, 'sluggish', { duration: num(tb.sluggish, 0.8) + (beast ? num(t.sluggish_addition) : 0), source: u });
},
},
skill: {
kind: 'duration', mods: { aspd: num(bb.attack_speed) },
targeting: { maxTargets: n, rangeGrid: def?.skill?.rangeGrid ?? null },
onStart({ battle, unit }) {
unit.mem.grainQuietUntil = battle.time + sleep;
for (const e of enemiesInGrid(battle, unit, def?.skill?.rangeGrid ?? null, { n })) {
if (battle.applyStatus(e, 'sleep', { duration: sleep, source: unit })) battle.fx('sleep', { x: e.x, y: e.y, id: e.id });
}
},
},
skills: {
skchr_grabds_1: { kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 1), dmgType: 'arts' }, mods: { maxTargets: 1 } },
},
talents: [{ install(battle, unit) {
statBuff(battle, unit, 'talent:grabds', { aspd: num(t.attack_speed) });
spTimeBonus(battle, unit, num(moduleBb(chess).sp_recovery_per_sec), () => enemyInRange(battle, unit));
} }],
};
},
};
@@ -0,0 +1,56 @@
// server/sim/content/kits/ops/chess_char_2_05-harold.js — 哈洛德 (char_4114_harold) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { COLS, ELEMENT_GAUGE_MAX } from '../../../constants.js';
import { num, talentBb, up } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
/** Largest element gauge of a unit (every element — 侵蚀 included: the bosses' attacks fill it on operators). */
const elemLoad = (a) => Math.max(a.elem.burn, a.elem.neural, a.elem.necrosis, a.elem.apoptosis, a.elem.erosion ?? 0);
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_05 哈洛德 重症优先: ASPD +attack_speed, heals the ally with the heaviest element damage first; on targets whose
// gauge is over half, the element recovery is trait_scale × (the wandermedic trait: ep_heal_ratio × ATK).
// 我即军营: allies in range whose gauge is over half take −ep_damage_resistance 元素损伤 — checked on the element hit
// itself (`elementHit`, a 元素损伤 multiplier: PRTS 元素 "…后续可应用元素损伤倍率提升/降低等效果"; not `elemTakenMul`,
// which is 元素伤害 / 元素脆弱); several 哈洛德 do not stack — the strongest cut applies.
// S1 治疗强化·γ型 (alt): ATK +atk; the 重症优先 target order belongs to S2 only.
chess_char_2_05_a: (bb, chess) => {
const t = talentBb(chess, 0);
const s2 = onDefaultSkill(chess);
const half = ELEMENT_GAUGE_MAX / 2;
const ts = num(bb.trait_scale, 1);
return {
skill: {
kind: 'duration', heal: true, mods: { aspd: num(bb.attack_speed) },
onHit({ battle, unit, target }) {
if (!target || !unit.mem.haroldHalf?.has(target.id) || !(ts > 1)) return;
battle.reduceElement(target, unit.s.atk * (unit.profile.heal?.elementHealRatio ?? 0) * (ts - 1));
},
},
skills: { 'skcom_heal_up[3]': { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
if (s2) battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active) return;
const cands = battle.injuredAlliesInKeys(unit.rangeKeys, unit, true);
cands.sort((a, b) => elemLoad(b) - elemLoad(a) || a.hpRatio - b.hpRatio || a.deploySeq - b.deploySeq);
if (cands.length) ctx.targets = cands.slice(0, Math.max(1, ctx.targets.length));
unit.mem.haroldHalf = new Set(ctx.targets.filter((a) => elemLoad(a) > half).map((a) => a.id));
}, { owner: unit });
const r = Math.max(0, Math.min(1, num(t.ep_damage_resistance)));
if (r > 0) {
battle.on('elementHit', (ctx) => {
const a = ctx.target, d = ctx.dmg;
if (!up(unit) || !a || a.side !== 'ally' || !d || d.type !== 'element' || !(elemLoad(a) > half)) return;
if (!(unit.rangeKeySet || new Set(unit.rangeKeys || [])).has(a.tileR * COLS + a.tileC)) return;
const prev = d.haroldCut || 0; // the strongest 我即军营 of the hit only
if (r <= prev) return;
d.mul *= (1 - r) / (1 - prev);
d.haroldCut = r;
}, { owner: unit, priority: 20 });
}
} }],
};
},
};
@@ -0,0 +1,85 @@
// server/sim/content/kits/ops/chess_char_2_06-papyrs.js — 莎草 (char_4139_papyrs) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { bodyInKeys } from '../../../body.js';
import { num, talentBb, up, alliesInGridOf, instantKind, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_06 莎草 巧思乍现 (AUTO, 2 charges): the next heal is heal_scale × ATK (chain jumps included) and the talent
// barrier is shield_scale_skill × stronger. 博览古卷: every heal gives the target a barrier of attack@scale × ATK for
// attack@shield_duration s. (Chain falloff incl. the elite module comes from the chainhealer profile.)
// S2 临考发挥 (alt): on start lock the ally in range (not herself, no 禁疗; any profession, summons included) with the
// highest max HP; while it runs that ally is the only main heal target, base attack time +base_attack_time s (−1.1
// on 2.85), ATK +atk, chain jumps +attack@chain.extra_value. PRTS: "无法可选单位时无法开启技能" — with no lockable
// ally the skill never activates (the talent install guards the unit's skill.activate: no skillStart, no
// 萨尔贡 / 特质 "开启技能时" gains, SP kept); the skill stops at once when the locked ally leaves the field.
chess_char_2_06_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const sc = num(t['attack@scale']), sd = num(t['attack@shield_duration'], 8), boost = num(bb.shield_scale_skill, 1);
const extra = Math.max(0, Math.floor(num(bb['attack@chain.extra_value'])));
const lockOf = (battle, unit) => alliesInGridOf(battle, unit)
.filter((a) => a !== unit && !a.s.flags.noHeal && !a.profile?.noHeal)
.sort((a, b) => b.s.maxHp - a.s.maxHp || a.deploySeq - b.deploySeq)[0] ?? null;
const release = (battle, unit) => {
if (unit.mem.papyrsHeal) { unit.profile.heal = unit.mem.papyrsHeal; unit.mem.papyrsHeal = null; }
if (unit.mem.papyrsHook) battle.off(unit.mem.papyrsHook);
unit.mem.papyrsHook = null;
unit.mem.papyrsLock = null;
unit.mem.papyrsAbort = false;
};
return {
skill: { kind: instantKind(def), heal: true, attack: { healScale: num(bb.heal_scale, 1) } },
skills: {
skchr_papyrs_2: {
kind: 'duration', heal: true, mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess) },
onStart({ battle, unit, skill }) {
release(battle, unit);
const lock = lockOf(battle, unit);
// (only reachable when a runtime bypasses the activate guard below, e.g. a copied skill: undo on the next tick)
if (!lock) { unit.mem.papyrsAbort = true; return; }
unit.mem.papyrsLock = lock;
const h = unit.profile.heal;
if (h && extra > 0) { unit.mem.papyrsHeal = h; unit.profile.heal = { ...h, count: Math.max(1, h.count || 3) + extra }; }
unit.mem.papyrsHook = battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !skill.active) return;
const L = unit.mem.papyrsLock;
if (L && up(L) && bodyInKeys(L, unit.rangeKeySet)) ctx.targets = [L];
}, { owner: unit, priority: 10 });
battle.fx('buff', { x: lock.x, y: lock.y, id: lock.id, kind: 'lock' });
},
onTick({ unit, skill }) {
if (unit.mem.papyrsAbort) {
// "无法可选单位时无法开启技能": undo the cast, keep the SP
unit.mem.papyrsAbort = false;
skill.end('noTarget');
skill.addCharge(1);
return;
}
const L = unit.mem.papyrsLock;
if (!L || !up(L)) skill.end('target');
},
onEnd({ battle, unit }) { release(battle, unit); },
},
},
talents: [{ install(battle, unit) {
const sk = unit.skill;
if (sk && sk.id === 'skchr_papyrs_2' && !Object.prototype.hasOwnProperty.call(sk, 'activate')) {
// "无法可选单位时无法开启技能": every activation path (DEFAULT heal trigger, carried skill, content) is refused
// while no ally can be locked — the charge stays, nothing starts (non-enumerable: never serialised)
const activate = sk.activate;
Object.defineProperty(sk, 'activate', {
configurable: true, writable: true, enumerable: false,
value(reason, opts) { return lockOf(battle, unit) ? activate.call(this, reason, opts) : false; },
});
}
if (!(sc > 0)) return;
battle.on('heal', (ctx) => {
if (ctx.source !== unit || ctx.target === unit && ctx.opts?.regen || !(ctx.amount > 0)) return;
const bySkill = !!(unit.skill?.active && unit.skill.pending);
battle.addBuff(ctx.target, { key: 'papyrs:shield', shield: unit.s.atk * sc * (bySkill ? boost : 1), duration: sd, source: unit, visible: true, tags: ['talent'] });
}, { owner: unit, priority: -10 });
} }],
};
},
};
@@ -0,0 +1,35 @@
// server/sim/content/kits/ops/chess_char_2_07-ghost.js — 幽灵鲨 (char_143_ghost) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, statBuff } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_07 幽灵鲨 肉斩骨断: ATK +atk and HP never drops below 1 while active; stunned `stun` s afterwards.
// Talent: max HP +max_hp (elite: + hp_recovery_per_sec_by_max_hp_ratio × max HP per s).
// Elite module (CEN-X, trait atk_scale): vs enemies it blocks ATK ×atk_scale.
// S1 攻击力强化·γ型 (alt): ATK +atk for its duration.
chess_char_2_07_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) {
if (unit.mem.undying) battle.off(unit.mem.undying);
unit.mem.undying = battle.on('fatal', (c) => { if (c.unit === unit && unit.skill?.active) c.prevented = true; }, { owner: unit, priority: 10 });
},
onEnd({ battle, unit, reason }) {
if (unit.mem.undying) battle.off(unit.mem.undying);
unit.mem.undying = null;
if (reason !== 'death' && unit.alive && num(bb.stun) > 0) battle.applyStatus(unit, 'stun', { duration: num(bb.stun), source: unit });
},
},
skills: { 'skcom_atk_up[3]': { kind: 'duration', mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
statBuff(battle, unit, 'talent:ghost', { hpPct: num(t.max_hp), hpRegenRatio: num(t.hp_recovery_per_sec_by_max_hp_ratio) });
if (num(tb.atk_scale, 1) !== 1) onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && target.blockedBy) dmg.amount *= num(tb.atk_scale, 1); });
} }],
};
},
};
@@ -0,0 +1,33 @@
// server/sim/content/kits/ops/chess_char_2_08-bubble.js — 泡泡 (char_381_bubble) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, byEnemyAttack, onDamagedOn, toggleBuff } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_08 泡泡 “挨打”: stops attacking; DEF +def, taunt +taunt_level, each attack received returns atk_scale × DEF phys
// damage to the attacker. 尖刺盾: attackers get ATK −atk for `duration` s. Elite module (PRO-X): DEF +def while blocking.
// S1 防御力强化·β型 (alt, TAKE_DAMAGE from data): DEF +def for its duration; the counter belongs to “挨打” only.
chess_char_2_08_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const s2 = onDefaultSkill(chess);
return {
skill: { kind: 'duration', mods: { defPct: num(bb.def), taunt: num(bb.taunt_level) }, attack: { noAttack: true } },
skills: { 'skcom_def_up[2]': { kind: 'duration', mods: { defPct: num(bb.def) } } },
talents: [{ install(battle, unit) {
onDamagedOn(battle, unit, (ctx) => {
if (!byEnemyAttack(ctx) || !unit.alive) return;
const src = ctx.source;
if (s2 && unit.skill?.active && src.alive) {
battle.dealDamage(unit, src, { amount: unit.s.def * num(bb.atk_scale), type: 'phys', isSkill: true, canDodge: false, tags: ['counter'] });
battle.fx('counter', { x: src.x, y: src.y, id: unit.id });
}
if (num(t.atk) < 0 && src.alive) battle.addBuff(src, { key: 'bubble:spike', duration: num(t.duration, 5), mods: { atkPct: num(t.atk) }, refresh: 'extend', source: unit });
});
if (num(tb.def)) toggleBuff(battle, unit, 'bubble:guard', () => unit.blocking.length > 0, { defPct: num(tb.def) });
} }],
};
},
};
@@ -0,0 +1,60 @@
// server/sim/content/kits/ops/chess_char_2_09-humus.js — 休谟斯 (char_491_humus) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_09 休谟斯 高效处理: block +block_cnt; above peak_1.hp_ratio HP ATK +peak_1.atk (精力充沛), above peak_2.hp_ratio
// +peak_2.atk. 再回收: healing beyond max HP becomes a barrier, at most max_hp_ratio × max HP.
// S1 固废切割 (alt, attack SP): the next attack hits at atk_scale × ATK (every enemy it reaches) and heals her `value`
// HP once (a self heal: her trait bars only healing by others; overflow feeds 再回收).
chess_char_2_09_a: (bb, chess) => {
const t = talentBb(chess, 0);
const peaks = [];
for (const [k, v] of Object.entries(bb)) {
const m = k.match(/\[(peak_\d+)\]\.peak_performance\.(atk|hp_ratio)$/);
if (!m) continue;
let p = peaks.find((x) => x.id === m[1]);
if (!p) peaks.push((p = { id: m[1], atk: 0, hp: 1 }));
if (m[2] === 'atk') p.atk = num(v); else p.hp = num(v, 1);
}
peaks.sort((a, b) => b.hp - a.hp);
const key = 'humus:peak';
const peak = ({ battle, unit }) => {
const lvl = peaks.find((p) => unit.hpRatio > p.hp)?.atk ?? 0;
const cur = unit.findBuff(key);
if ((cur?.data?.v ?? 0) === lvl) return;
if (lvl > 0) battle.addBuff(unit, { key, mods: { atkPct: lvl }, data: { v: lvl }, visible: true, tags: ['skill'] });
else battle.removeBuff(unit, key);
};
return {
skill: {
kind: 'duration', mods: { blockCnt: num(bb.block_cnt) },
onStart: peak, onTick: peak,
onEnd({ battle, unit }) { battle.removeBuff(unit, key); },
},
skills: {
skchr_humus_1: {
kind: 'instant', attack: { atkScale: num(bb.atk_scale, 1) },
onAttack({ battle, unit }) {
if (num(bb.value) > 0 && unit.alive) battle.heal(unit, unit, num(bb.value), { self: true, tags: ['skill'] });
},
},
},
talents: [{ install(battle, unit) {
const ratio = num(t.max_hp_ratio);
if (!(ratio > 0)) return;
battle.on('heal', (ctx) => {
if (ctx.target !== unit || !(ctx.amount > 0)) return;
const over = ctx.amount - (unit.s.maxHp - unit.hp);
if (!(over > 0)) return;
const cur = unit.findBuff('humus:recycle');
const have = cur ? cur.shield : 0;
const val = Math.min(unit.s.maxHp * ratio, have + over);
if (val > have + 1e-6) battle.addBuff(unit, { key: 'humus:recycle', shield: val, source: unit, visible: true, tags: ['talent'] });
}, { owner: unit, priority: -50 });
} }],
};
},
};
@@ -0,0 +1,68 @@
// server/sim/content/kits/ops/chess_char_2_10-rockr.js — 洛洛 (char_4040_rockr) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { canTargetEnemy } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
import { num, talentBb, up } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_10 洛洛 自负此轭: ASPD +attack_speed; the drones lock on their target until it dies or the skill ends.
// 过载 (term ba.overdrive "技能进行到一半时触发额外效果"): the second half of the duration — ATK +atk and the funnel cap
// ×scale; when the skill ends she is stunned as long as the overload lasted. 立于磐石: ATK +atk per `interval` s on the
// field, up to max_stack_cnt stacks.
// S1 战术咏唱·γ型 (alt): ASPD +attack_speed; the target lock belongs to 自负此轭 only.
chess_char_2_10_a: (bb, chess) => {
const t = talentBb(chess, 0);
const s2 = onDefaultSkill(chess);
const endOverload = (battle, unit) => {
if (unit.mem.rockFunnel) { unit.profile.funnel = unit.mem.rockFunnel; unit.mem.rockFunnel = null; }
battle.removeBuff(unit, 'rockr:overload');
};
return {
skill: {
kind: 'duration', mods: { aspd: num(bb.attack_speed) },
onStart({ battle, unit, skill }) {
unit.mem.rockOverAt = battle.time + skill.duration / 2;
unit.mem.rockOver = false;
unit.mem.rockLock = null;
},
onTick({ battle, unit }) {
if (unit.mem.rockOver || battle.time + 1e-9 < unit.mem.rockOverAt) return;
unit.mem.rockOver = true;
const f = unit.profile.funnel;
if (f) { unit.mem.rockFunnel = f; unit.profile.funnel = { ...f, max: f.max * num(bb.scale, 1) }; }
battle.addBuff(unit, { key: 'rockr:overload', mods: { atkPct: num(bb.atk) }, visible: true, tags: ['skill'] });
battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit, reason }) {
const over = unit.mem.rockOver ? battle.time - unit.mem.rockOverAt : 0;
endOverload(battle, unit);
unit.mem.rockOver = false;
unit.mem.rockLock = null;
if (over > 0 && reason !== 'death' && unit.alive) battle.applyStatus(unit, 'stun', { duration: over, source: unit });
},
},
skills: { 'skcom_magic_rage[3]': { kind: 'duration', mods: { aspd: num(bb.attack_speed) } } },
talents: [{ install(battle, unit) {
if (s2) battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active) return;
const L = unit.mem.rockLock;
if (L && L.alive && bodyInKeys(L, unit.rangeKeySet) && canTargetEnemy(unit, L, ctx.profile || unit.profile)) ctx.targets = [L];
else unit.mem.rockLock = ctx.targets[0] ?? null;
}, { owner: unit });
const iv = num(t.interval), max = Math.floor(num(t.max_stack_cnt)), v = num(t.atk);
if (!(iv > 0) || !(max > 0) || !v) return;
battle.on('tick', () => {
if (!up(unit)) return;
const n = Math.min(max, Math.floor((battle.time - unit.deployedAt + 1e-9) / iv));
const cur = unit.findBuff('rockr:rock');
if ((cur ? cur.stacks : 0) === n) return;
if (n > 0) battle.addBuff(unit, { key: 'rockr:rock', mods: { atkPct: v }, stacks: n, maxStacks: max, tags: ['talent'] });
else battle.removeBuff(unit, 'rockr:rock');
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,79 @@
// server/sim/content/kits/ops/chess_char_2_11-kazema.js — 风丸 (char_4016_kazema) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, up, cheb, freeTileAround } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_11 风丸 纸艺·双影: lose hp_ratio of current HP, ATK +atk, summon the <替身> (overrideTokenKey) on a free melee tile
// around her for the skill's duration [ASSUMED lifetime]. 折纸生花: whenever a <替身> appears (the summon, and the
// dollkeeper substitution) enemies on the 8 surrounding tiles take damage_scale × its ATK arts — air units too [ASSUMED:
// no 对空 note on PRTS; the <替身> itself "可对空"].
// Trait substitution: she fights with the <替身>'s stats (the engine already swaps in its HP; the kit swaps ATK/DEF
// with flat deltas so %-buffs still apply). Elite module (PUM-X, trait atk): while substituted, ATK +atk.
// S1 纸艺·迅击 (alt, attack SP): the next attack hits at atk_scale × ATK and she loses hp_ratio of her MAX HP (PRTS:
// ten casts knock her out — the loss can be fatal and so sets off the <替身> substitution).
chess_char_2_11_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const tokId = chess?.skill?.overrideTokenKey ?? (chess?.tokens ?? []).find((x) => /shadow|doll/.test(String(x))) ?? 'token_10022_kazema_shadow';
const ds = num(t.damage_scale);
const origami = (battle, src, atk) => {
battle.fx('aoe', { x: src.x, y: src.y, radius: 1.5, id: src.id, skill: 'origami' });
if (!(ds > 0)) return;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.s.flags.untargetable || cheb(src, e) > 1) continue;
battle.dealDamage(src, e, { amount: atk * ds, type: 'arts', isSkill: true, canDodge: false, tags: ['talent'] });
}
};
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) {
const loss = unit.hp * num(bb.hp_ratio);
if (loss > 0 && unit.hp - loss >= 1) battle.loseHp(unit, loss, { source: unit });
const tile = freeTileAround(battle, unit, (r, c) => battle.grid.canStand(r, c));
if (!tile) return;
const doll = battle.spawnToken(unit, tokId, tile[0], tile[1], {});
if (!doll) return;
unit.mem.doll = doll;
battle.fx('summon', { x: doll.x, y: doll.y, id: doll.id, token: tokId });
origami(battle, doll, doll.s.atk);
},
onEnd({ battle, unit }) {
const d = unit.mem.doll;
if (d && d.alive) battle.retreat(d, { reason: 'expired', permanent: true });
unit.mem.doll = null;
},
},
skills: {
skchr_kazema_1: {
kind: 'instant', attack: { atkScale: num(bb.atk_scale, 1) },
onAttack({ battle, unit }) {
const loss = unit.s.maxHp * num(bb.hp_ratio);
if (loss > 0 && unit.alive) battle.loseHp(unit, loss, { source: unit, tags: ['skill'] });
},
},
},
talents: [{ install(battle, unit) {
const bonus = num(tb.atk);
battle.on('tick', () => {
const sub = up(unit) && !!unit.findBuff('trait:substitute');
if (sub && !unit.mem.kzSub) {
unit.mem.kzSub = true;
const ts = battle.tokenDef(tokId, unit)?.stats ?? null; // the owner's module variant (DESIGN §16)
const mods = {};
if (num(ts?.atk) > 0) mods.atkFlat = num(ts.atk) - unit.base.atk;
if (ts && Number.isFinite(ts.def)) mods.defFlat = ts.def - unit.base.def;
if (bonus > 0) mods.atkPct = bonus;
battle.addBuff(unit, { key: 'kazema:doll', mods, tags: ['trait'] });
origami(battle, unit, unit.s.atk);
} else if (!sub && unit.mem.kzSub) {
unit.mem.kzSub = false;
battle.removeBuff(unit, 'kazema:doll');
}
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,65 @@
// server/sim/content/kits/ops/chess_char_2_12-gravel.js — 砾 (char_237_gravel) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, installAura } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_12 砾 鼠群 (passive): at each deployment a barrier of hp_ratio × max HP that decays to 0 over `duration` s
// (capacity stepped once per second; a damaged barrier shrinks in proportion — PRTS).
// 快速部署: own deployment cost +cost (−1). Elite 小个子支援: every unit whose initial deployment cost ≤ cond.cost
// DEF +def while 砾 is on the field. (Elite module withdraw refund: no manual retreat in battle ⇒ no effect.)
// S1 影袭 (alt, passive): at each deployment DEF +def, decaying linearly to 0 over `duration` s, updated once per
// second (PRTS: "防御力加成每秒更新一次").
chess_char_2_12_a: (bb, chess) => {
const t = talentBb(chess, 0);
return {
skill: {
kind: 'passive',
onStart({ battle, unit }) {
const total = unit.s.maxHp * num(bb.hp_ratio);
const dur = num(bb.duration, 10);
if (!(total > 0) || !(dur > 0)) return;
let k = 0;
battle.addBuff(unit, {
key: 'gravel:rats', shield: total, duration: dur, interval: 1, visible: true, tags: ['skill'],
// PRTS: "屏障最大容量每秒更新一次;受到伤害后,剩余容量根据屏障最大容量等比变化" — the capacity steps down
// once per second and what is left of the barrier scales with it (never a plain cap)
onTick: ({ unit: u, buff }) => {
const prev = Math.max(0, dur - k) / dur;
k++;
const next = Math.max(0, dur - k) / dur;
if (prev > 0 && buff.shield > 0) { buff.shield *= next / prev; u.markDirty(); }
},
});
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
},
},
skills: {
skchr_gravel_1: {
kind: 'passive',
onStart({ battle, unit }) {
const v = num(bb.def), dur = num(bb.duration);
if (!(v > 0) || !(dur > 0)) return;
let k = 0;
battle.addBuff(unit, {
key: 'gravel:shadow', duration: dur, mods: { defPct: v }, interval: 1, visible: true, tags: ['skill'],
onTick: ({ unit: u, buff }) => {
k++;
buff.mods = { defPct: v * Math.max(0, dur - k) / dur };
u.markDirty();
},
});
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'def' });
},
},
},
talents: [{ install(battle, unit) {
const c = num(t.cost);
if (c) unit.base.cost = Math.max(0, unit.base.cost + c);
const d = num(t.def), lim = t['cond.cost'];
if (d && lim != null) installAura(battle, unit, { key: 'talent:gravel', value: d, select: (a) => num(a.def?.stats?.cost, Infinity) <= num(lim), mods: { defPct: d } });
} }],
};
},
};
@@ -0,0 +1,78 @@
// server/sim/content/kits/ops/chess_char_2_13-tippi.js — 蒂比 (char_4191_tippi) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, up, onHitOn } from '../shared/tier1.js';
import { onDefaultSkill } from '../shared/tier2.js';
/**
* 起飞 of 蒂比's skills (gamedata_const ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击,可以阻挡飞行敌人"): the skill's flags
* `liftoff` (no ground enemy blocked — Battle._blockerFor; 对地规避 — targeting.js evadesGround: no ground enemy selects
* her, while what selects nobody still lands — 无视无法选择 abilities, direct picks, flying units' blasts, 无来源 damage, the
* ticks of a debuff already on her) and `blockFly` (blocks flyers at the air radius). She stays a ground unit on her tile (PRTS 行动方式 "起飞的
* 干员仍然是地面单位": `unit.ground` unchanged — 隐德来希's 血镰 still counts her; 地面干员 bonds / items read her position).
*/
const LIFTOFF_FLAGS = Object.freeze({ blockFly: true, liftoff: true });
/** 蒂比 take-off: the ground enemies she blocked walk on. */
function tippiTakeOff({ battle, unit }) {
battle.releaseBlocked(unit);
battle.fx('takeoff', { x: unit.x, y: unit.y, id: unit.id });
}
/** 蒂比 landing when the airborne skill ends: the flyers she held are released. */
function tippiLand({ battle, unit }) {
battle.releaseBlocked(unit);
}
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_13 蒂比 紧急赶场通知 (AUTO): "受到攻击后触发" is officially "受到伤害前触发" (PRTS 修正 原因 6; 备注 "在受到伤害前自动
// 触发技能…若本次伤害为物理或法术,再将本次伤害闪避") — any incoming damage instance (an attack, a zone tick, the 源石溶剂
// drain; never a 流失, which has no `hit`) sets it off (the kit is the only trigger: the engine rule is disabled) and a
// physical/arts one is dodged; takes off for the duration: skill range, ATK +atk, attacks become 3 shots, blocks flying
// (not ground) enemies. Trait "起飞后能够阻挡2个飞行敌人": flying enemies are blocked only while airborne. 片场工作指南
// ("若最近N秒内未受伤害" — 修正 原文 攻击): if no damage for stack_time s, the next physical/arts damage is dodged (prob);
// every damage instance restarts that timer (dodged or not; a 流失 does not — PRTS 备注).
// S1 专业喷绘技巧 (alt, DEFAULT trigger from data): takes off at once for its duration — skill range, ATK +atk, blocks
// flyers only; no triple shot, and incoming attacks never set it off.
// Both take off as 起飞 (LIFTOFF_FLAGS): she blocks no ground enemy and none selects her (对地规避), so a ground enemy's
// selected damage refused on the way never reaches her `hit` and neither sets off S2 nor restarts 片场工作指南.
chess_char_2_13_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const s2 = onDefaultSkill(chess);
return {
trait: { blockFly: false },
skill: {
kind: 'duration', trigger: { rule: 'CUSTOM_RANGE', grid: [] }, // never by the engine: the hit handler below
mods: { atkPct: num(bb.atk) }, flags: LIFTOFF_FLAGS,
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null },
attack: { hits: 3 },
onStart: tippiTakeOff,
onEnd: tippiLand,
},
skills: {
skchr_tippi_1: {
kind: 'duration', mods: { atkPct: num(bb.atk) }, flags: LIFTOFF_FLAGS,
targeting: { rangeGrid: def?.skill?.rangeGrid ?? null },
onStart: tippiTakeOff,
onEnd: tippiLand,
},
},
talents: [{ install(battle, unit) {
battle.removeBuff(unit, 'trait:skywalker'); // the profession grants blockFly permanently; here only airborne
const st = num(t.stack_time), p = num(t.prob, 1);
let last = -Infinity;
battle.on('deploy', ({ unit: u }) => { if (u === unit) last = battle.time - st; }, { owner: unit });
onHitOn(battle, unit, (ctx) => {
const { dmg } = ctx;
if (!dmg || dmg.cancel || !up(unit)) return;
const dodgeable = dmg.type === 'phys' || dmg.type === 'arts';
const sk = unit.skill;
let dodged = false;
if (s2 && sk && sk.ready && !sk.active && unit.canAct && !unit.s.flags.silence && sk.activate('TAKE_DAMAGE')) dodged = dodgeable;
else if (dodgeable && st > 0 && battle.time - last + 1e-9 >= st && battle.rng.chance(p)) dodged = true;
last = battle.time;
if (dodged) { dmg.cancel = true; battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id }); }
}, 5);
} }],
};
},
};
@@ -0,0 +1,29 @@
// server/sim/content/kits/ops/chess_char_2_14-flower.js — 调香师 (char_181_flower) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, moduleBb, up } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_14 调香师 精调: ASPD +attack_speed (−50), ATK +atk. 熏衣草: every ally on the field recovers
// atk_to_hp_recovery_ratio × ATK HP per second. Elite module (RIN-Y, hidden attack@max_target): heals 4 allies.
// S1 治疗强化·β型 (alt): ATK +atk for its duration.
chess_char_2_14_a: (bb, chess) => {
const t = talentBb(chess, 0);
const cnt = Math.floor(num(moduleBb(chess)['attack@max_target']));
return {
trait: cnt > 0 ? { heal: { mode: 'multi', count: cnt } } : undefined,
skill: { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) } },
skills: { 'skcom_heal_up[2]': { kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } } },
talents: [{ install(battle, unit) {
const r = num(t.atk_to_hp_recovery_ratio);
if (!(r > 0)) return;
battle.every(1, () => {
if (!up(unit)) return;
const amt = unit.s.atk * r;
for (const a of battle.alliesFor(unit)) if (a.hp < a.s.maxHp) battle.heal(unit, a, amt, { aura: true, tags: ['talent'] });
}, { owner: unit });
} }],
};
},
};
@@ -0,0 +1,43 @@
// server/sim/content/kits/ops/chess_char_2_15-akkord.js — 协律 (char_4051_akkord) kit, tier 2 (hidden).
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, alliesInGridOf, toggleBuff } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_15 协律 (hidden) 震爆调谐: ATK +atk; each attack sets off a sonic boom at every other operator in range:
// attack@aoe_atk_scale × ATK arts within attack@range_radius. 律脉同构: ATK +atk with another operator in range.
// Elite module (BLA-X): damage grows with distance, up to +damage_scale at max_dist tiles.
chess_char_2_15_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const rad = num(bb['attack@range_radius'], 0.9), sc = num(bb['attack@aoe_atk_scale']);
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk) },
onAttack({ battle, unit }) {
if (!(sc > 0)) return;
for (const a of alliesInGridOf(battle, unit)) {
if (a === unit || a.kind !== 'op') continue;
battle.fx('sonic', { x: a.x, y: a.y, radius: rad, id: unit.id });
for (const e of battle.foesInRadius(a.x, a.y, rad)) {
if (!e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * sc, type: 'arts', isSkill: true, isSplash: true, tags: ['sonic'] });
}
}
},
},
talents: [{ install(battle, unit) {
const v = num(t.atk);
if (v) toggleBuff(battle, unit, 'akkord:sync', () => alliesInGridOf(battle, unit).some((a) => a !== unit && a.kind === 'op'), { atkPct: v });
if (tb.damage_scale != null) {
const mn = num(tb.min_dist), mx = num(tb.max_dist, 4), ds = num(tb.damage_scale);
onHitBy(battle, unit, ({ target, dmg }) => {
if (!dmg.isAttack) return;
const d = Math.hypot(target.x - unit.x, target.y - unit.y);
dmg.amount *= 1 + ds * (mx > mn ? Math.max(0, Math.min(1, (d - mn) / (mx - mn))) : 1);
});
}
} }],
};
},
};
@@ -0,0 +1,47 @@
// server/sim/content/kits/ops/chess_char_2_16-whitew.js — 拉普兰德 (char_140_whitew) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitOn } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_16 拉普兰德 狼魂 (AUTO, attack SP): ATK +atk, arts damage, one extra target, ranged attacks no longer reduced.
// 精神摧毁: attacks disable the target's abilities (silence) for `duration` s. Elite module (LOR-X, trait
// atk_scale_m): attacks add atk_scale_m × ATK arts damage.
// S1 日晷 (alt, attack SP, 持续时间无限 ⇒ toggle until she falls): ATK +atk and a `prob` chance to block (抵挡: the
// damage instance is negated) each physical damage an enemy deals her.
chess_char_2_16_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
const trait = {};
if (num(t.duration) > 0) trait.onHitStatus = { key: 'silence', duration: num(t.duration) };
if (num(tb.atk_scale_m) > 0) {
trait.afterHit = (battle, u, target) => {
// PRTS 特性备注 "造成预计算的法术附加伤害": 附加伤害 (tag addition — no 叙拉古 6 roll)
if (target && target.alive && target.side === 'enemy') battle.dealDamage(u, target, { amount: u.s.atk * num(tb.atk_scale_m), type: 'arts', tags: ['module', 'addition'] });
};
}
return {
trait,
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, targeting: { maxTargets: 2 }, attack: { dmgType: 'arts', dmgMul: () => 1 } },
skills: {
skchr_whitew_1: {
kind: 'toggle', mods: { atkPct: num(bb.atk) },
onStart({ battle, unit, skill }) {
if (unit.mem.sundial) battle.off(unit.mem.sundial);
const p = num(bb.prob);
unit.mem.sundial = p > 0 ? onHitOn(battle, unit, ({ source, credit, dmg }) => { // 抵挡 is target-side: a 无来源 burst counts via its credit
const src = source || credit;
if (!skill.active || dmg.cancel || dmg.type !== 'phys' || !src || src.side !== 'enemy') return;
if (battle.rng.chance(p)) { dmg.cancel = true; battle.fx('block', { x: unit.x, y: unit.y, id: unit.id }); }
}, 5) : null;
},
onEnd({ battle, unit }) {
if (unit.mem.sundial) battle.off(unit.mem.sundial);
unit.mem.sundial = null;
},
},
},
};
},
};
@@ -0,0 +1,46 @@
// server/sim/content/kits/ops/chess_char_2_17-branch.js — 折桠 (char_4207_branch) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitOn, enemiesInGrid } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_17 折桠 生存决心: on start, ground enemies around her (skill grid) 战栗 (tremble: "被阻挡后无法进行普通攻击") for
// not_combat s; ATK +atk, DEF +def, hits every blocked enemy. 简易包扎: heals hp_ratio × max HP when the skill ends.
// Elite module (UNY-X, trait damage_scale): damage from enemies she blocks ×damage_scale.
// S1 绝境抵抗 (alt, TAKE_DAMAGE / hurt SP from data): DEF +def and 抵抗 (−one_minus_status_resistance: control
// statuses last half as long) for its duration; 简易包扎 heals at its end too.
chess_char_2_17_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: {
kind: 'duration', mods: { atkPct: num(bb.atk), defPct: num(bb.def) }, attack: { hitAllBlocked: true },
onStart({ battle, unit }) {
const grid = def?.skill?.rangeGrid;
const foes = grid ? enemiesInGrid(battle, unit, grid, { canHitFly: false, groundOnly: true }) : battle.foesInRadius(unit.x, unit.y, 1.5).filter((e) => !e.isFlying);
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'resolve' });
for (const e of foes) battle.applyStatus(e, 'tremble', { duration: num(bb.not_combat), source: unit });
},
},
skills: {
skchr_branch_1: {
kind: 'duration', mods: { defPct: num(bb.def) },
onStart({ battle, unit, skill }) {
const v = Math.min(0.95, Math.max(0, -num(bb.one_minus_status_resistance)));
if (v > 0) battle.applyStatus(unit, 'resist', { duration: skill.timeLeft, value: v, source: unit });
},
},
},
talents: [{ install(battle, unit) {
const hr = num(t.hp_ratio);
if (hr > 0) {
battle.on('skillEnd', ({ unit: u, reason }) => {
if (u === unit && reason !== 'death' && unit.alive) battle.heal(unit, unit, unit.s.maxHp * hr, { self: true, tags: ['talent'] });
}, { owner: unit });
}
if (tb.damage_scale != null) onHitOn(battle, unit, ({ source, dmg }) => { if (source && source.blockedBy === unit) dmg.mul *= num(tb.damage_scale, 1); });
} }],
};
},
};
@@ -0,0 +1,37 @@
// server/sim/content/kits/ops/chess_char_2_18-ashlok.js — 灰毫 (char_431_ashlok) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, traitBb, onHitBy, batMod } from '../shared/tier1.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_18 灰毫 攻击力强化·γ型 (DEFAULT, like S2: the owner's deliberate deviation from the 重装 TAKE_DAMAGE row, data
// TRIGGER_DEVIATIONS, DESIGN §21.29): ATK +atk. 炮术研习: ATK +atk, or +ashlok_t_1.atk when the `cnt` orthogonal
// tiles around her are all ground (LOW). Elite module (FOR-X, trait atk_scale): vs blocked enemies ATK ×atk_scale.
// S2 专注轰击 (alt): block count 0 (noBlock: releases what she holds), only ranged (splash) attacks, base attack time
// +base_attack_time s (−0.4 / −0.5 on 2.8), ATK +atk.
chess_char_2_18_a: (bb, chess) => {
const t = talentBb(chess, 0);
const tb = traitBb(chess);
return {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) } },
skills: {
skchr_ashlok_2: {
kind: 'duration', mods: { atkPct: num(bb.atk), batPct: batMod(bb.base_attack_time, chess) }, flags: { noBlock: true },
attack: { fortress: false, _fortressMelee: false },
},
},
talents: [{ install(battle, unit) {
battle.on('deploy', ({ unit: u }) => {
if (u !== unit) return;
const r = unit.tileR, c = unit.tileC;
const low = [[1, 0], [-1, 0], [0, 1], [0, -1]].filter(([dr, dc]) => battle.grid.isLow(r + dr, c + dc)).length;
const ground = low >= num(t.cnt, 4);
const v = ground ? num(t['ashlok_t_1.atk'], num(t.atk)) : num(t.atk);
if (v) battle.addBuff(unit, { key: 'talent:ashlok', mods: { atkPct: v }, data: { ground }, tags: ['talent'] });
}, { owner: unit });
if (num(tb.atk_scale, 1) !== 1) onHitBy(battle, unit, ({ target, dmg }) => { if (dmg.isAttack && target.blockedBy) dmg.amount *= num(tb.atk_scale, 1); });
} }],
};
},
};
@@ -0,0 +1,49 @@
// server/sim/content/kits/ops/chess_char_2_19-tinman.js — 锡人 (char_4151_tinman) kit, tier 2.
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
import { num, talentBb, enemiesInGrid, instantKind } from '../shared/tier1.js';
import { tinmanKit } from './chess_char_1_16-tinman.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_19 锡人 “大拉里”: see tinmanKit (tier1.js).
// S1 “老科利” (alt, attack SP, needs a target): throws an alchemy unit at the target (ground first); for
// projectile_delay_time s ground enemies within projectile_range tiles are 虚弱 (weaken −atk) and take atk_scale ×
// ATK arts per second (ATK cached at the cast; damage over time, so the elite 凋敝魂灵 ×skill@damage_scale applies;
// the zone counts for the module's SP bonus and outlives her).
chess_char_2_19_a: (bb, chess, def) => {
const k = tinmanKit(bb, chess, def);
const dur = num(bb.projectile_delay_time, 8), radius = num(bb.projectile_range, 1), dmgScale = num(bb.atk_scale);
const weak = Math.min(1, Math.max(0, -num(bb.atk)));
const wither = num(talentBb(chess, 1)['skill@damage_scale'], 1);
const STEP = 0.25; // weaken refresh cadence (damage every 1 s)
return {
...k,
skills: {
...(k.skills || {}),
skchr_tinman_1: {
kind: instantKind(def),
onStart({ battle, unit }) {
const tgt = enemiesInGrid(battle, unit, null, { n: 1, groundOnly: true })[0] ?? enemiesInGrid(battle, unit, null, { n: 1 })[0];
const x = tgt ? tgt.x : unit.x + unit.fwd[1], y = tgt ? tgt.y : unit.y + unit.fwd[0];
const atk = unit.s.atk;
const n = Math.max(1, Math.round(dur / STEP)), per = Math.max(1, Math.round(1 / STEP));
let i = 0;
unit.mem.tinZones = (unit.mem.tinZones ?? 0) + 1;
battle.fx('zone', { x, y, radius, dur, id: unit.id, skill: 'tinman1' });
battle.every(STEP, (b, sc) => {
const pulse = i % per === 0;
for (const e of b.foesInRadius(x, y, radius)) {
if (e.isFlying || e.s.flags.untargetable) continue;
if (weak > 0) b.applyStatus(e, 'weaken', { duration: STEP + 0.05, value: weak, source: unit });
if (wither > 1) b.addBuff(e, { key: 'tinman:wither', duration: STEP + 0.05, data: { mul: wither }, source: unit });
if (pulse && dmgScale > 0) b.dealDamage(unit, e, { amount: atk * dmgScale, type: 'arts', isSkill: true, canDodge: false, tags: ['dot', 'zone'] });
}
if (++i >= n) { sc.cancel(); unit.mem.tinZones = Math.max(0, (unit.mem.tinZones ?? 1) - 1); }
}, { immediate: true });
},
},
},
};
},
};
@@ -0,0 +1,49 @@
// server/sim/content/kits/ops/chess_char_3_01-angel.js — 能天使 (char_103_angel) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, instantKindOf, groundAspd, alive, fx } from '../shared/tier3.js';
export default {
// ---- 3_01 能天使 · 速射手 — S3 过载模式: 5 连射 + 攻击间隔缩短; 快速弹匣 ASPD; 天使的祝福 (self + one random ally)
// S1 冲锋模式: next attack = 3 shots × atk_scale; S2 扫射模式: 4 shots × attack@atk_scale per attack for 15 s;
// 精锐 module MAR-Y: ASPD + while a ground enemy is in range (MAR-X fly × = profession layer)
chess_char_3_01_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const bless = { atkPct: num(t1.atk), hpPct: num(t1.max_hp) };
const shots = (v) => Math.max(1, Math.floor(num(v, 1)));
const kit = {
skill: {
kind: 'duration',
mods: { batPct: num(bb.base_attack_time) },
attack: { atkScale: num(bb['attack@atk_scale'], 1), hits: Math.max(1, Math.floor(num(bb['attack@times'], 1))) },
},
skills: altSkills(chess, d, bb, {
skchr_angel_1: (s) => ({ kind: instantKindOf(s), attack: { atkScale: num(s.bb.atk_scale, 1), hits: shots(s.bb.times) } }),
skchr_angel_2: (s) => ({ kind: 'duration', attack: { atkScale: num(s.bb['attack@atk_scale'], 1), hits: shots(s.bb['attack@times']) } }),
}),
talents: [
{ install(battle, unit) { battle.addBuff(unit, { key: 'talent:angel_mag', mods: { aspd: num(t0.attack_speed) }, persist: true, allowDead: true }); } },
{ install(battle, unit) {
battle.addBuff(unit, { key: 'talent:angel_bless', mods: { ...bless }, persist: true, allowDead: true });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
// after the whole board is deployed (the initial deployment goes one unit after another)
battle.after(0, () => {
if (!alive(unit)) return;
const cands = battle.allies(unit.ownerId).filter((a) => a !== unit && a.kind === 'op' && !a.findBuff('talent:angel_bless_ally'));
const pick = battle.rng.pick(cands);
if (!pick) return;
battle.addBuff(pick, { key: 'talent:angel_bless_ally', mods: { ...bless }, persist: true, source: unit, visible: true });
fx(battle, 'buff', pick, { src: unit.id, talent: 'angel_bless' });
}, { owner: unit });
}, { owner: unit });
} },
],
};
// MAR-Y trait "范围内存在地面敌人时攻击速度+8" (bb attack_speed, cnt = ground enemies needed)
if (num(tb.attack_speed) > 0) kit.install = groundAspd('trait:angel_ground', num(tb.attack_speed), Math.floor(num(tb.cnt, 1)));
return kit;
},
};
@@ -0,0 +1,67 @@
// server/sim/content/kits/ops/chess_char_3_02-ayer.js — 断崖 (char_294_ayer) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import {
num, defOf, talentBb, traitBb, altSkills, instantKindOf, alive, gridKeys, fx, copyGrid, NINE, aura,
} from '../shared/tier3.js';
export default {
// ---- 3_02 断崖 · 领主 — S2 浮游刃启动: arts, wider range, each attack hits every enemy blocked by allies around her
chess_char_3_02_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const tGrid = copyGrid(d.talents?.[0]?.rangeGrid) ?? NINE;
const extra = num(bb.atk_scale, 1);
const moduleArts = num(tb.atk_scale_m, 0);
const skillGrid = copyGrid(d.skill?.rangeGrid);
const kit = {
skill: {
kind: 'duration',
attack: { dmgType: 'arts', atkScale: num(bb['attack@atk_scale'], 1) },
onAttack({ battle, unit }) {
// "周围8格友方单位阻挡的所有敌人": the 8 tiles around her (her own tile is not one of them — the talent
// says "自身与周围8格" when it means both)
const seen = new Set();
for (const a of battle.allyUnits) {
if (a === unit || !alive(a) || a.kind === 'device' || !a.blocking.length) continue;
if (Math.abs(a.tileR - unit.tileR) > 1 || Math.abs(a.tileC - unit.tileC) > 1) continue;
for (const e of a.blocking) {
if (!e.alive || seen.has(e)) continue;
seen.add(e);
battle.dealDamage(unit, e, { amount: unit.s.atk * extra, type: 'arts', isSkill: true, tags: ['skill', 'melee', 'ayerBlade'] });
fx(battle, 'strike', e, { src: unit.id, skill: 'ayer_2' });
}
}
},
},
// S1 多导向散射弹丸: next attack hits up to max_target enemies in range for atk_scale arts + 停顿
skills: altSkills(chess, d, bb, {
skchr_ayer_1: (s) => {
const slug = num(s.bb.sluggish, 0);
return {
kind: instantKindOf(s),
targeting: { maxTargets: Math.max(1, Math.floor(num(s.bb.max_target, 1))) },
attack: {
dmgType: 'arts', atkScale: num(s.bb.atk_scale, 1),
onEachHit({ battle, unit, target }) {
if (slug > 0 && target && target.alive && target.side === 'enemy') battle.applyStatus(target, 'sluggish', { duration: slug, source: unit });
},
},
};
},
}),
talents: [{ install(battle, unit) {
aura(battle, unit, { key: 'talent:ayer_aspd', side: 'ally', tiles: () => gridKeys(tGrid, unit), filter: (a) => a.kind === 'op', mods: { aspd: num(t0.attack_speed) } });
} }],
};
if (skillGrid) kit.skill.targeting = { rangeGrid: skillGrid };
// 精锐 module LOR-X: "攻击附带10%攻击力的法术伤害"
if (moduleArts > 0) {
kit.trait = { afterHit: (battle, unit, target) => {
if (target && target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * moduleArts, type: 'arts', tags: ['module'] });
} };
}
return kit;
},
};
@@ -0,0 +1,36 @@
// server/sim/content/kits/ops/chess_char_3_03-swire.js — 诗怀雅 (char_308_swire) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, gridKeys, copyGrid, NINE, aura } from '../shared/tier3.js';
export default {
// ---- 3_03 诗怀雅 · 教官 — S2 协同作战: ATK +, first talent ×talent_scale; 近距离作战指导 melee ATK aura
// S1 指挥调度: the talent covers the skill's range (x-1 normal / x-2 elite) at ×talent_scale while it runs
chess_char_3_03_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tGrid = copyGrid(d.talents?.[0]?.rangeGrid) ?? NINE;
const scale = num(bb.talent_scale, 1);
// "第一天赋生效范围扩大" (talent_range_flag): the selected skill's range replaces the talent's while it is active
const skillTGrid = num(bb.talent_range_flag) > 0 ? copyGrid(d.skill?.rangeGrid) : null;
const refresh = ({ unit }) => unit.mem.swireAura?.();
return {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onStart: refresh,
onEnd: refresh,
},
skills: altSkills(chess, d, bb, {
skchr_swire_1: () => ({ kind: 'duration', onStart: refresh, onEnd: refresh }),
}),
talents: [{ install(battle, unit) {
unit.mem.swireAura = aura(battle, unit, {
key: 'talent:swire_guide', side: 'ally', tiles: () => gridKeys(skillTGrid && unit.skill?.active ? skillTGrid : tGrid, unit),
filter: (a) => String(a.def?.position ?? 'MELEE').toUpperCase() === 'MELEE',
mods: () => ({ atkPct: num(t0.atk) * (unit.skill?.active ? scale : 1) }),
});
} }],
};
},
};
@@ -0,0 +1,230 @@
// server/sim/content/kits/ops/chess_char_3_04-swire2.js — 琳琅诗怀雅 (char_1033_swire2) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { bodyOnTile } from '../../../body.js';
import {
num, defOf, talentBb, moduleTalentBb, selectedId, altSkills, alive, gridKeys, fx, copyGrid, textNum, freeTile,
groundTile, enemiesOn,
} from '../shared/tier3.js';
/** 琳琅诗怀雅 S3's coin range (PRTS 备注 "前方范围2-4"; range_table "2-4", facing right). */
const SWIRE2_COIN_GRID = Object.freeze([[1, 1], [0, 0], [0, 1], [0, 2], [-1, 1]]);
/** Merchant trait with a callback on every successful DP payment (琳琅诗怀雅 大买家). Same rules as professions.js. */
function merchantInstall(onPay) {
return (battle, unit) => {
const iv = unit.profile.merchantInterval ?? 3;
const cost = unit.profile.merchantCost ?? 3;
battle.every(iv, () => {
if (!alive(unit)) return;
const pl = battle.getPlayer(unit.ownerId);
if (!pl) return;
if (pl.dp >= cost) {
battle.addDp(unit.ownerId, -cost);
onPay(battle, unit, cost);
battle.emit('merchantPay', { unit, cost });
} else battle.retreat(unit, { reason: 'merchant' });
}, { owner: unit });
};
}
export default {
// ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): each attack spends a coin to drop a champagne bomb in range;
// 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles)
// S1 仗义疏财 (passive, 2 coins): a coin heals the most injured ally (< 70 % HP) of the 8 surrounding tiles for
// attack@heal_scale × ATK — on her attack, or with no enemy to attack on her own attack timer (owner's decision
// 2026-10-04, against the official 「下一次攻击会为…」; installS1). S3 千金一掷 (持续时间无限): attacks hit twice, kills give a coin;
// closing it spends every coin on random ground enemies of range 2-4 in front and those she blocks (atk_scale phys +
// a small push, radial despite the text's 向前 — PRTS 备注 "推开效果为径向推动"; client charpack char_1033_swire2:
// the RandomGold ability (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位,
// 弹道不可对空). It does not close itself when the purse is full (PRTS 卫戍协议/帮助 「通常不会自动关闭技能」; the skill
// text is 「可随时主动关闭」). Owner 2026-10-04: at the cap (金币上限为10) she shoots once an enemy is in the skill's
// attack range (the 1-tile range, not the coin-mark 2-4 grid). Below the cap, or with nobody there, it stays open.
// 精锐 module MER-Y: ATK +4 % per trait payment (≤ 5 stacks).
chess_char_3_04_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const sel = selectedId(chess, d);
const S1 = 'skchr_swire2_1', S2 = 'skchr_swire2_2', S3 = 'skchr_swire2_3';
// "携带此技能时金币上限为N": the SELECTED skill's cap
const coinMax = num(bb.sp, 3);
const coinCost = Math.abs(num(bb['attack@sp'], -1)) || 1;
const modTal = moduleTalentBb(chess);
const scale = num(bb.atk_scale, 1);
const slug = num(bb.sluggish, 2);
const tokenId = chess?.skill?.overrideTokenKey ?? (d.tokens || []).find((t) => /gdtrap/.test(String(t))) ?? 'token_10031_swire2_gdtrap';
const skillGrid = copyGrid(d.skill?.rangeGrid);
const addCoins = (battle, unit, n) => {
const before = unit.mem.coins ?? 0;
unit.mem.coins = Math.min(coinMax, before + n);
if (unit.mem.coins > before) fx(battle, 'coin', unit, { n: unit.mem.coins });
};
const bombKit = (owner, switchT) => ({
skill: null,
trait: { noAttack: true },
install(battle, bomb) {
battle.on('tick', () => {
if (!bomb.alive || !bomb.deployed) return;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.isFlying || e.s.flags.untargetable) continue;
if (!bodyOnTile(e, bomb.tileR, bomb.tileC)) continue;
// first enemy touching it; after switchT s on the field the bomb deals its damage one extra time
const hits = battle.time - bomb.deployedAt >= switchT - 1e-9 ? 2 : 1;
for (let i = 0; i < hits && e.alive; i++) {
battle.dealDamage(owner, e, { amount: owner.s.atk * scale, type: 'phys', isSkill: true, canDodge: false, tags: ['skill', 'trap'] });
}
if (e.alive) battle.applyStatus(e, 'sluggish', { duration: slug, source: owner });
// `consumed`: the bomb is used up by its own blast — clients play its impact sound, not a death sound
fx(battle, 'explode', bomb, { src: owner.id, target: e.id, hits, consumed: true });
battle.retreat(bomb, { reason: 'expired', permanent: true });
break;
}
}, { owner: bomb });
},
});
const healRatio = textNum(d.skill?.description, /血量不足(\d+)%/, 70) / 100;
/**
* 仗义疏财 — official text 「消耗一枚金币,下一次攻击会为周围八格内血量不足70%的一名友方单位恢复相当于攻击力40%的生命」: the heal rides on an
* attack, so with no enemy around she never healed (community report #5 「琳琅诗怀雅1技能不会主动奶身边受伤的干员」). Owner's
* decision 2026-10-04 (a deliberate deviation, like §21.29's 重装 casts): she heals an injured ally beside her whether
* she attacks or not. Same target (the lowest HP ratio below 70 % of the 8 surrounding tiles, no 禁疗 / 孤立 unit, no
* device), coin and heal_scale × ATK. Cadence [ASSUMED]: at most one heal per attack cycle (her attack interval, ASPD
* included) — while she attacks, on the attack as before; when her last attack attempt found no target, on her own
* timer (the 'tick' hook runs after the attacks, so an attack due in the same tick takes it).
*/
const installS1 = (battle, unit) => {
const hs = num(bb['attack@heal_scale'], num(bb.heal_scale, 0));
let nextAt = -Infinity;
const healTarget = () => {
let best = null;
for (const a of battle.allyUnits) {
if (a === unit || !alive(a) || a.hidden || a.kind === 'device' || a.hpRatio >= healRatio) continue;
if (a.s.flags.noHeal || a.profile?.noHeal) continue; // 禁疗 / 孤立: never a heal target
if (Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) !== 1) continue; // 周围八格
if (!best || a.hpRatio < best.hpRatio || (a.hpRatio === best.hpRatio && a.deploySeq < best.deploySeq)) best = a;
}
return best;
};
const tryHeal = () => {
if (!alive(unit) || (unit.mem.coins ?? 0) < coinCost || !(hs > 0) || battle.time < nextAt - 1e-9) return;
const best = healTarget();
if (!best) return;
unit.mem.coins -= coinCost;
nextAt = battle.time + unit.s.interval;
battle.heal(unit, best, unit.s.atk * hs);
fx(battle, 'coin', unit, { n: unit.mem.coins, heal: best.id, skill: 'swire2_1' });
};
battle.on('attack', (ctx) => { if (ctx.attacker === unit) tryHeal(); }, { owner: unit, priority: -10 });
battle.on('tick', () => { if (unit.deployed && unit.canAct && !unit.trait?.hadTarget) tryHeal(); }, { owner: unit });
};
const installS3 = (battle, unit) => { // 千金一掷: "击倒敌人时获得一枚金币"
battle.on('kill', (ctx) => {
if (ctx.killer === unit && ctx.victim.side === 'enemy' && unit.skill?.active) addCoins(battle, unit, 1);
}, { owner: unit });
};
return {
skill: { kind: 'passive' },
skills: altSkills(chess, d, bb, {
[S1]: () => ({ kind: 'passive' }), // (coins → heals: installS1)
[S3]: (s) => {
const cash = num(s.bb.atk_scale, 1), force = num(s.bb.force, 0);
// the 【金币标记】 targets: ground enemies on range 2-4 in front of her (range_table "2-4") and every unit she blocks
const coinMarks = (battle, unit) => {
const list = enemiesOn(battle, unit, gridKeys(SWIRE2_COIN_GRID, unit), 0, { ...unit.profile, canHitFly: false });
for (const e of unit.blocking || []) if (e.alive && !e.isFlying && !list.includes(e)) list.push(e);
return list;
};
const purseCap = Math.max(1, Math.floor(num(s.bb.sp, 10)));
return {
kind: 'toggle',
attack: { hits: 2 },
// Owner 2026-10-04: at the cap, shoot once a ground enemy is in the skill attack range (data rangeGrid,
// the same 1-tile range as her attack). The coin marks (前方 2-4) are who the burst pays, not the trigger.
// Nobody in that range: stay open and keep the coins. Below the cap: stay open.
onTick({ battle, unit, skill }) {
if (!skill.active || (unit.mem.coins ?? 0) + 1e-9 < purseCap) return;
const keys = gridKeys(skillGrid ?? unit.rangeGrid, unit);
if (enemiesOn(battle, unit, keys, 0, { ...unit.profile, canHitFly: false }).length) skill.end('manual');
},
onEnd({ battle, unit, reason }) {
if (reason !== 'manual' || !unit.alive) return;
const n = unit.mem.coins ?? 0;
unit.mem.coins = 0;
let spent = 0;
// PRTS 备注: closing it "立即对前方范围2-4内的地面敌方单位与自身阻挡的所有单位施加【金币标记】", the coins going to
// marked units at random — 弹道(不可对空): air units (FLY, 近地悬浮, 浮空) are never paid. Until 0.1.1 the
// coins went to her attack range (1-1) instead of range 2-4.
const marked = coinMarks(battle, unit);
for (let i = 0; i < n; i++) {
const e = battle.rng.pick(marked.filter((x) => x.alive && !x.isFlying));
if (!e) break;
spent++;
battle.dealDamage(unit, e, { amount: unit.s.atk * cash, type: 'phys', isSkill: true, tags: ['skill', 'swire2Cash'] });
// "将目标小力地向前推开" — PRTS 备注 "金币弹道…推开效果为径向推动", client knockback[relative]: a radial push
// away from her centre (not along her direction, so no 45° / 0.25-tile 特殊修正), official 力度 − 重量
// distance (Battle.push)
if (e.alive) battle.push(e, force, { from: unit });
}
fx(battle, 'coin', unit, { n: 0, spent, skill: 'swire2_3' });
},
};
},
}),
trait: { install: merchantInstall((battle, unit) => {
// MER-Y "每次特性消耗费用时攻击力+4%,最多可以叠加5次" (any time, not only during the skill)
if (modTal && num(modTal.atk) > 0) {
battle.addBuff(unit, { key: 'trait:swire2_module', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(modTal.max_stack_cnt, 5)), mods: { atkPct: num(modTal.atk) } });
}
if (!unit.skill?.active) return; // "技能期间"
addCoins(battle, unit, num(t0.trait_sp, 1));
battle.addBuff(unit, { key: 'talent:swire2_buyer', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(t0.max_stack_cnt, 8)), mods: { atkPct: num(t0.atk) } });
}) },
install(battle, unit) {
if (sel === S1) { installS1(battle, unit); return; }
if (sel === S3) { installS3(battle, unit); return; }
if (sel !== S2 && sel != null) return;
const switchT = num(battle.tokenDef(tokenId, unit)?.skill?.bb?.duration_switch, 3);
battle.on('attack', (ctx) => {
if (ctx.attacker !== unit || !alive(unit) || (unit.mem.coins ?? 0) < coinCost) return;
const tiles = [];
for (const k of gridKeys(skillGrid ?? unit.rangeGrid, unit)) {
const r = (k / COLS) | 0, c = k % COLS;
// (never on the home tile of a dead operator: it could not redeploy until an enemy triggers the bomb)
if (freeTile(battle, r, c) && groundTile(battle, r, c)) tiles.push([r, c]);
}
const tile = battle.rng.pick(tiles);
if (!tile) return;
const bomb = battle.spawnToken(unit, tokenId, tile[0], tile[1], { untargetable: true, kit: bombKit(unit, switchT) });
if (!bomb) return;
unit.mem.coins -= coinCost;
fx(battle, 'summon', bomb, { src: unit.id, token: tokenId, coins: unit.mem.coins });
}, { owner: unit, priority: -10 });
},
talents: [
{ install(battle, unit) { // 大买家: "开启技能时获得1枚金币" (a passive starts at every deployment, S3 when cast)
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
unit.mem.coins = 0;
if (unit.skill?.kind === 'passive') addCoins(battle, unit, num(t0.sp, 1));
}, { owner: unit });
battle.on('skillStart', (ctx) => { if (ctx.unit === unit && unit.skill?.kind !== 'passive') addCoins(battle, unit, num(t0.sp, 1)); }, { owner: unit });
} },
{ install(battle, unit) { // 破财消灾
battle.on('fatal', (ctx) => {
if (ctx.unit !== unit || ctx.prevented) return;
const n = unit.mem.saveCount ?? 0;
const cost = Math.abs(num(t1.cost, -5)) * Math.pow(num(t1.cost_multi, 2), n);
const pl = battle.getPlayer(unit.ownerId);
if (!pl || pl.dp + 1e-9 < cost) return;
battle.addDp(unit.ownerId, -cost);
unit.mem.saveCount = n + 1;
ctx.prevented = true;
unit.hp = Math.max(1, unit.s.maxHp * num(t1.hp_ratio, 0.7));
fx(battle, 'revive', unit, { cost });
}, { owner: unit });
} },
],
};
},
};
@@ -0,0 +1,62 @@
// server/sim/content/kits/ops/chess_char_3_05-skadi.js — 斯卡蒂 (char_263_skadi) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, statSkill, fx } from '../shared/tier3.js';
/** Abyssal Hunters (character_table groupId `abyssal`, not in data/chess.json): 深海掠食者 targets. */
const ABYSSAL = new Set(['char_263_skadi', 'char_143_ghost', 'char_218_cuttle', 'char_474_glady', 'char_1023_ghost2', 'char_4145_ulpia']);
export default {
// ---- 3_05 斯卡蒂 · 无畏者 — S3 涌潮悲歌: ATK/DEF/HP +; 深海掠食者 (Abyssal Hunters ATK); 迅捷出击 (redeploy −10 s)
// 精锐 module DRE-Y: once per deployment, a lethal hit instead restores full HP with max HP −60 % and ASPD +30
// S1 迅捷打击·γ型: ATK/ASPD +; S2 跃浪击 (passive): ATK + for `duration` s after every deployment;
// 精锐 module DRE-X: ×atk_scale on blocked enemies
chess_char_3_05_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const kit = {
skill: { kind: 'duration', mods: { atkPct: num(bb.atk), defPct: num(bb.def), hpPct: num(bb.max_hp) } },
skills: altSkills(chess, d, bb, {
'skcom_quickattack[3]': statSkill,
skchr_skadi_2: (s) => ({
kind: 'passive',
onStart({ battle, unit }) { // "部署后N秒内攻击力+X" (a passive starts at every deployment)
battle.addBuff(unit, { key: 'skill:skadi_wave', duration: num(s.bb.duration, 20), mods: { atkPct: num(s.bb.atk) }, visible: true });
},
}),
}),
talents: [
{ install(battle, unit) { // "编入队伍时": every Abyssal Hunter of this player, for the whole battle
for (const a of battle.allyUnits) {
if (a.kind === 'op' && a.ownerId === unit.ownerId && ABYSSAL.has(a.def?.charId)) {
battle.addBuff(a, { key: 'talent:skadi_predator', mods: { atkPct: num(t0.atk) }, persist: true, allowDead: true, source: unit });
}
}
} },
{ install(battle, unit) {
if (unit.mem.skadiSwift) return; // base stats are per unit; never apply twice
unit.mem.skadiSwift = true;
unit.base.respawnTime = Math.max(0, unit.base.respawnTime + num(t1.respawn_time));
} },
],
};
if (tb.value != null || tb.hp_ratio != null) {
kit.install = (battle, unit) => {
battle.on('deploy', (ctx) => { if (ctx.unit === unit) unit.mem.tideUsed = false; }, { owner: unit });
battle.on('fatal', (ctx) => {
if (ctx.unit !== unit || ctx.prevented || unit.mem.tideUsed) return;
unit.mem.tideUsed = true;
ctx.prevented = true;
const hpMul = tb.value != null ? 1 - num(tb.value) : num(tb.max_hp, 0.4);
battle.addBuff(unit, { key: 'trait:skadi_tide', mods: { hpMul: Math.max(0.05, hpMul), aspd: num(tb.attack_speed) }, visible: true });
unit.hp = Math.max(1, unit.s.maxHp * num(tb.hp_ratio, 1));
fx(battle, 'revive', unit, { module: true });
}, { owner: unit, priority: -50 });
};
}
// DRE-X "攻击被阻挡的敌人时攻击力提升至115%" (blocked by anyone)
if (num(tb.atk_scale) > 0) kit.trait = { dmgMul: (battle, unit, target) => (target && target.blockedBy ? num(tb.atk_scale, 1) : 1) };
return kit;
},
};
@@ -0,0 +1,102 @@
// server/sim/content/kits/ops/chess_char_3_06-philae.js — 菲莱 (char_4148_philae) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, selectedId, altSkills, fx, NINE, giveSp } from '../shared/tier3.js';
export default {
// ---- 3_06 菲莱 · 本源铁卫 — S2 冥河诅咒: stops attacking, HP +; when attacked blasts the ground enemies of the 3×3
// around her (PRTS 备注: range x-4; arts + ep_damage_ratio × ATK apoptosis, aoe_cd); ATK + once hit by element
// damage; 神河谕使: 元素损伤 taken −damage_resistance, +SP on apoptosis
// 精锐 module PRP-X "阻挡敌人时,自身造成的元素损伤提升15%": EVERY element fill she deals (the S2 blast, a 灼燃维式重锤
// she carries …) ×ep_damage_scale while she blocks — an `elementHit` multiplier, like 余's
// S1 灵河护佑 (TAKE_DAMAGE): HP +, clears her element gauges and gives a shield_value 损伤屏障 (absorbs element
// damage — gauge fills — until spent or the skill ends)
// 神河谕使 and the barrier act on the element hit before it lands (PRTS 备注: "减伤与技力回复效果于伤害计算前处理;即使
// 受到0点的凋亡损伤依然可以回复技力", "损伤屏障于伤害计算前、第一天赋后处理"): the talent's cut and SP first (priority 20),
// then the barrier absorbs what is left (`dmg.amount × dmg.mul`). The cut is a 元素损伤 multiplier (PRTS 元素 "受到的
// 元素损伤 = 损伤值 × (1 − 损伤抵抗 × 0.01),后续可应用元素损伤倍率提升/降低等效果"), not `elemTakenMul` (元素伤害 / 元素脆弱).
chess_char_3_06_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const sel = selectedId(chess, d);
const S1 = 'skchr_philae_1', S2 = 'skchr_philae_2';
const isS2 = sel === S2 || sel == null;
const scale = num(bb.atk_scale, 1);
const ep = num(bb.ep_damage_ratio, 0);
const cd = num(bb.aoe_cd, 2);
const epScale = num(tb.ep_damage_scale, 1);
const cut = Math.max(0, Math.min(1, num(t0.damage_resistance)));
// 损伤屏障 of S1: absorbs the gauge gain an element hit would add (after 神河谕使)
const installBarrier = (battle, unit) => {
battle.on('elementHit', (ctx) => {
if (ctx.target !== unit || !(unit.mem.philaeBarrier > 0) || !unit.skill?.active) return;
const dmg = ctx.dmg;
const eff = num(dmg.amount) * num(dmg.mul, 1);
if (!(eff > 0)) return;
const take = Math.min(unit.mem.philaeBarrier, eff);
unit.mem.philaeBarrier -= take;
if (take >= eff - 1e-9) dmg.cancel = true;
else dmg.amount *= (eff - take) / eff;
if (!(unit.mem.philaeBarrier > 0)) fx(battle, 'shieldBreak', unit, { element: true });
}, { owner: unit });
};
return {
skill: {
kind: 'duration',
mods: { hpPct: num(bb.max_hp) },
attack: { noAttack: true },
onStart({ unit }) { unit.mem.philaeCd = -Infinity; },
onEnd({ battle, unit }) { battle.removeBuff(unit, 'skill:philae_rage'); },
},
skills: altSkills(chess, d, bb, {
[S1]: (s) => ({
kind: 'duration',
mods: { hpPct: num(s.bb.max_hp) },
onStart({ battle, unit }) {
battle.reduceElement(unit, 1e12); // "立刻清除自身的元素损伤" (a gauge locked by a running burst stays)
unit.mem.philaeBarrier = num(s.bb.shield_value, 0);
fx(battle, 'shield', unit, { element: true, n: unit.mem.philaeBarrier });
},
onEnd({ unit }) { unit.mem.philaeBarrier = 0; },
}),
}),
talents: [{ install(battle, unit) {
// 神河谕使 (every skill): 元素损伤 taken ×(1 − damage_resistance); "受到凋亡损伤时回复2点技力" — even for 0 damage
battle.on('elementHit', (ctx) => {
const dmg = ctx.dmg;
if (ctx.target !== unit || !dmg || dmg.type !== 'element') return;
if (dmg.element === 'apoptosis' && !unit.skill?.active) giveSp(unit, num(t0.sp), 'talent');
if (cut > 0) dmg.mul *= 1 - cut;
}, { owner: unit, priority: 20 });
} }],
install(battle, unit) {
if (sel === S1) installBarrier(battle, unit);
// module PRP-X: her element damage ×ep_damage_scale while she blocks
if (epScale > 1) {
battle.on('elementHit', (ctx) => {
if (ctx.source === unit && ctx.dmg?.type === 'element' && ctx.target?.side === 'enemy' && unit.blocking.length) ctx.dmg.mul *= epScale;
}, { owner: unit });
}
battle.on('damaged', (ctx) => {
if (ctx.target !== unit || !unit.alive) return;
const sk = unit.skill;
if (ctx.type === 'element') {
if (isS2 && sk && sk.active && !unit.findBuff('skill:philae_rage')) battle.addBuff(unit, { key: 'skill:philae_rage', mods: { atkPct: num(bb.atk) }, visible: true });
return;
}
if (!isS2 || !sk || !sk.active || !ctx.dmg?.isAttack || !ctx.source || ctx.source.side !== 'enemy') return;
if (battle.time < (unit.mem.philaeCd ?? -Infinity)) return;
unit.mem.philaeCd = battle.time + cd;
// "周围的地面敌人" = range x-4, the 3×3 tiles around her (PRTS 备注)
for (const e of battle.unitsInGrid(unit, NINE, { side: 'enemy' })) {
if (e.isFlying) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', canDodge: false, isSkill: true, tags: ['skill', 'counter'] });
if (ep > 0 && e.alive) battle.dealDamage(unit, e, { type: 'element', element: 'apoptosis', amount: unit.s.atk * ep, tags: ['skill'] });
}
fx(battle, 'aoe', unit, { radius: 1.5, tiles: 'box', dmgType: 'arts', skill: 'philae_2' }); // box: the 3×3 tiles
}, { owner: unit });
},
};
},
};
@@ -0,0 +1,72 @@
// server/sim/content/kits/ops/chess_char_3_07-forcer.js — 见行者 (char_4036_forcer) kit, tier 3 (hidden).
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { PUSH_DIRECTIONAL_MIN_DIST } from '../../../constants.js';
import { num, defOf, talentBb, traitBb, onTiles, gridKeys, fx, copyGrid } from '../shared/tier3.js';
/** Two enemy bodies touch within this distance (tiles) — 见行者 collision stun. */
const COLLIDE = 0.6;
export default {
// ---- 3_07 见行者 · 推击手 (hidden) — S2 惊爆射击: push every enemy in the skill range forward + stun (longer when
// slammed into a wall, collided enemies stunned too; air units too [ASSUMED: "范围内所有敌人", no 对空 note on
// PRTS] — a 失衡免疫 enemy or a 静态刚体 (the drones) is not pushed but still stunned); 技巧射击: ignore DEF vs heavy enemies;
// 精锐 module PUS-X: redeployed on a ranged tile ⇒ half the deployment cost back
chess_char_3_07_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const skillGrid = copyGrid(d.skill?.rangeGrid);
const force = num(bb.force, 1);
const sDirect = num(bb['forcer_s_2[hit_directly].stun'], num(bb.stun, 1));
const sWall = num(bb.stun, sDirect);
const sBrush = num(bb['forcer_s_2[brush].stun'], sDirect);
const kit = {
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const keys = new Set(gridKeys(skillGrid ?? unit.rangeGrid, unit));
const victims = battle.enemies.filter((e) => e.alive && !e.hidden && !e.s.flags.untargetable && onTiles(e, keys));
const pushed = new Set(victims);
for (const e of victims) {
// "往身前方向…推开": a directional push whose angle is fixed — PRTS 备注 "不会因为角度过大而改变推动的方向或削减力度"
// (only the angle: a target nearer than 0.25 tile still turns radial at 受力等级 −2) — by the official
// 力度 − 重量 distance of a 特效 push (PRTS 推与拉 names 见行者's skills 特效类; Battle.push / pushDistance);
// stopped short of it ⇒ it hit a wall
const near = Math.hypot(e.x - unit.x, e.y - unit.y) < PUSH_DIRECTIONAL_MIN_DIST;
const expect = battle.pushDistance(e, near ? force - 2 : force, { effect: true });
const moved = battle.push(e, force, { from: unit, dir: { x: unit.fwd[1], y: unit.fwd[0] }, fixedAngle: true, effect: true });
const wall = expect > 0 && moved + 0.05 < expect;
battle.applyStatus(e, 'stun', { duration: wall ? sWall : sDirect, source: unit });
}
for (const e of victims) {
for (const o of battle.enemiesInRadius(e.x, e.y, COLLIDE)) {
if (pushed.has(o)) continue;
pushed.add(o);
battle.applyStatus(o, 'stun', { duration: sBrush, source: unit });
}
}
fx(battle, 'push', unit, { skill: 'forcer_2', n: victims.length });
},
// "立即将范围内所有敌人…": the data rule SKILL_RANGE fires it once an enemy is inside the skill range (PRTS 技能
// 策略 "仅在技能范围内存在敌人(无视其不可选中)时释放技能"), not only when one stands on her 2-tile attack range
},
talents: [{ install(battle, unit) {
battle.on('hit', (ctx) => {
if (ctx.source === unit && ctx.target.side === 'enemy' && ctx.target.s.massLevel >= num(t0.value, 3)) ctx.dmg.defIgnoreFlat += num(t0.def_penetrate_fixed);
}, { owner: unit });
} }],
};
if (num(tb.value) > 0) {
kit.install = (battle, unit) => {
battle.on('deploy', (ctx) => {
// ranged position = not a ground tile (a high tile, or a platform that elevates it: unit.ground false)
if (ctx.unit !== unit || ctx.initial || unit.ground) return;
battle.addDp(unit.ownerId, unit.base.cost * num(tb.value));
fx(battle, 'dp', unit, { n: unit.base.cost * num(tb.value) });
}, { owner: unit });
};
}
return kit;
},
};
@@ -0,0 +1,79 @@
// server/sim/content/kits/ops/chess_char_3_08-mint.js — 薄绿 (char_388_mint) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, gridKeys, fx, copyGrid, enemiesOn, aura } from '../shared/tier3.js';
export default {
// ---- 3_08 薄绿 · 阵法术师 — S2 聚能涡旋: each attack strikes every enemy on her range (the trait's 群体法术伤害) and
// pushes each one towards her, end-of-skill burst on
// every enemy in range; 地质学者: DEF aura (skill off) / less likely targeted (skill on);
// 精锐 module PLX-X: keeps part of the guard (DEF/RES) while the skill runs
// S1 风语: wider range (skill grid), attacks every enemy on it at attack@atk_scale. Auto-cast: the data
// rule SEARCH is the 阵法术师 row (PRTS 卫戍协议/帮助 "不受基础策略影响,在初始攻击范围内存在敌人时释放技能"; it
// covers every MANUAL skill of the class — user playtest #6) = an enemy inside her INITIAL range — the engine's
// DEFAULT rule, checked every tick for a unit that does not attack while its skill is off — not any enemy on the
// field: she would burn the skill on enemies that just spawned.
chess_char_3_08_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const tGrid = copyGrid(d.talents?.[0]?.rangeGrid) ?? [[1, 0], [0, -1], [0, 0], [0, 1], [-1, 0]];
const pullForce = num(bb['attack@force'], num(bb.force, 0));
const endScale = num(bb.atk_scale, 0);
const keepDef = num(tb['soil_e_002[buff].def'], 0), keepRes = num(tb['soil_e_002[buff].magic_resistance'], 0);
// every skill: PLX-X keeps part of the guard, 地质学者 taunt −1 while it runs, DEF aura only while it does not
const guardOn = ({ battle, unit }) => {
if (keepDef || keepRes) battle.addBuff(unit, { key: 'trait:mint_keep', mods: { defPct: keepDef, resFlat: keepRes } });
battle.addBuff(unit, { key: 'talent:mint_taunt', mods: { taunt: num(t0.taunt_level, -1) } });
unit.mem.mintAura?.();
};
const guardOff = ({ battle, unit }) => {
battle.removeBuff(unit, 'trait:mint_keep');
battle.removeBuff(unit, 'talent:mint_taunt');
unit.mem.mintAura?.();
};
return {
skills: altSkills(chess, d, bb, {
skchr_mint_1: (s) => {
const grid = copyGrid(s.rangeGrid);
return {
kind: 'duration',
trigger: { rule: 'DEFAULT' },
attack: { atkScale: num(s.bb['attack@atk_scale'], num(s.bb.atk_scale, 1)) },
...(grid ? { targeting: { rangeGrid: grid } } : {}),
onStart: guardOn,
onEnd: guardOff,
};
},
}),
skill: {
kind: 'duration',
attack: {
atkScale: num(bb['attack@atk_scale'], 1),
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.side !== 'enemy') return;
// PRTS 备注: "此技能的“拖拽”机制实际为反方向(指向薄绿方向)的推开" — a radial push towards her by the
// official 力度 − 重量 push distance (小力 vs weight 1: 0.44 tiles), never past her (Battle.push inward). Her
// attack reaches air units (阵法术师 "攻击时可对空"), but the drones of the mode are 静态刚体 (PRTS 特殊机制): hit,
// never dragged (player report after 0.1.0, "飞机可以被薄绿的技能拉走")
if (battle.push(target, pullForce, { from: unit, inward: true }) > 0) fx(battle, 'pull', target, { src: unit.id });
},
},
onStart: guardOn,
onEnd(ctx) {
const { battle, unit, reason } = ctx;
if (reason !== 'death' && unit.alive && endScale > 0) {
for (const e of enemiesOn(battle, unit, unit.rangeKeys)) battle.dealDamage(unit, e, { amount: unit.s.atk * endScale, type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
fx(battle, 'aoe', unit, { radius: 2, dmgType: 'arts', skill: 'mint_2' });
}
guardOff(ctx);
},
},
talents: [{ install(battle, unit) {
unit.mem.mintAura = aura(battle, unit, {
key: 'talent:mint_geo', side: 'ally', tiles: () => gridKeys(tGrid, unit), active: () => !unit.skill?.active, mods: { defPct: num(t0.def) },
});
} }],
};
},
};
@@ -0,0 +1,57 @@
// server/sim/content/kits/ops/chess_char_3_09-haini.js — 海霓 (char_4079_haini) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, instantKindOf, alive, onTiles, aura } from '../shared/tier3.js';
/** Non-elite, non-leader enemy (海霓 "非精英和领袖敌人"). */
const isNormalEnemy = (e) => !e.isBoss && String(e.def?.rank ?? 'NORMAL').toUpperCase() === 'NORMAL';
export default {
// ---- 3_09 海霓 · 削弱者 — S2 阻滞性显色剂: ATK +, 2 targets, slows non-elite enemies in range, their deaths raise the
// talent up to max_talent_up; 测绘器材的奇用: non-elite enemies in range are fragile
chess_char_3_09_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const slow = num(bb['attack@move_speed'], 0);
const up = num(bb['attack@talent_up'], 0), upMax = num(bb['attack@max_talent_up'], 1);
const mt = Math.floor(num(bb['attack@max_target'], 1));
const kit = {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) {
unit.mem.hainiMul = 1;
if (!unit.mem.hainiSlow) {
unit.mem.hainiSlow = aura(battle, unit, { key: 'skill:haini_slow', side: 'enemy', interval: 0, tiles: () => unit.rangeKeySet, filter: isNormalEnemy, mods: { moveMul: Math.max(0, 1 + slow) } });
}
unit.mem.hainiSlow();
},
onTick({ unit }) { unit.mem.hainiSlow?.(); },
onEnd({ unit }) { unit.mem.hainiSlow?.clear(); unit.mem.hainiMul = 1; },
},
// S1 迷惑性洋流图: next attack ×atk_scale on max_target enemies at once
skills: altSkills(chess, d, bb, {
skchr_haini_1: (s) => ({
kind: instantKindOf(s),
targeting: { maxTargets: Math.max(1, Math.floor(num(s.bb.max_target, 1))) },
attack: { atkScale: num(s.bb.atk_scale, 1) },
}),
}),
talents: [{ install(battle, unit) {
unit.mem.hainiMul = 1;
aura(battle, unit, {
key: 'talent:haini_fragile', side: 'enemy', tiles: () => unit.rangeKeySet, filter: isNormalEnemy,
mods: () => ({ dmgTakenMul: 1 + (num(t0.damage_scale, 1) - 1) * (unit.mem.hainiMul ?? 1) }),
});
battle.on('kill', (ctx) => {
const v = ctx.victim;
if (!unit.skill?.active || !alive(unit) || v.side !== 'enemy' || !isNormalEnemy(v)) return;
if (!unit.rangeKeySet || !onTiles(v, unit.rangeKeySet)) return;
unit.mem.hainiMul = Math.min(upMax, (unit.mem.hainiMul ?? 1) + up);
}, { owner: unit });
} }],
};
if (mt > 1) kit.skill.targeting = { maxTargets: mt };
return kit;
},
};
@@ -0,0 +1,54 @@
// server/sim/content/kits/ops/chess_char_3_10-pinecn.js — 松果 (char_440_pinecn) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, selectedId, altSkills, instantKindOf, copyGrid } from '../shared/tier3.js';
export default {
// ---- 3_10 松果 · 散射手 — S2 电能过载: ATK + growing with each use (a→d), shorter range; 便携电源: SP regen for 60 s
chess_char_3_10_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const sel = selectedId(chess, d);
const steps = Object.keys(bb).filter((k) => /\[[a-z]\]\.atk$/.test(k)).sort().map((k) => num(bb[k]));
if (!steps.length) steps.push(num(bb.atk));
const skillGrid = copyGrid(d.skill?.rangeGrid);
const kit = {
skill: {
kind: 'duration',
onStart({ battle, unit, skill }) {
const atk = steps[Math.min(steps.length - 1, Math.max(0, skill.activations - 1))];
battle.addBuff(unit, { key: 'skill:pinecn_atk', mods: { atkPct: atk } });
},
onEnd({ battle, unit }) { battle.removeBuff(unit, 'skill:pinecn_atk'); },
},
// S1 RMA长钉 (charges): "立即以165%的攻击力进行一次攻击,无视敌人180的防御力" — an extra shot fired at once (all
// enemies in range, front-row × of the trait), on top of the normal attack it was cast before
skills: altSkills(chess, d, bb, {
skchr_pinecn_1: (s) => ({
kind: instantKindOf(s),
// (the DEF ignore rides on the shots' tag: they land after the instant skill has ended)
attack: { atkScale: num(s.bb.atk_scale, 1), tags: ['skill', 'pinecnSpike'] },
onStart({ battle, unit }) { battle.forceAttack(unit); },
}),
}),
talents: [{ install(battle, unit) {
battle.on('deploy', (ctx) => {
if (ctx.unit === unit) battle.addBuff(unit, { key: 'talent:pinecn_power', duration: num(t0.duration, 60), mods: { spRecoveryFlat: num(t0.sp_recovery_per_sec) } });
}, { owner: unit });
} }],
};
if (sel === 'skchr_pinecn_1') {
const pen = num(bb.def_penetrate_fixed, 0);
kit.install = (battle, unit) => battle.on('hit', (ctx) => {
if (ctx.source === unit && pen > 0 && ctx.dmg.tags?.includes('pinecnSpike')) ctx.dmg.defIgnoreFlat += pen;
}, { owner: unit });
}
if (skillGrid) {
kit.skill.targeting = { rangeGrid: skillGrid };
// "攻击范围缩短": offensive skills fire with an enemy inside the SKILL range (research 03 §1.4) — with the
// initial (wider) range she would open the skill on enemies the shortened range cannot reach
kit.skill.trigger = { rule: 'CUSTOM_RANGE', grid: skillGrid };
}
return kit;
},
};
@@ -0,0 +1,74 @@
// server/sim/content/kits/ops/chess_char_3_11-snhunt.js — 雪猎 (char_4211_snhunt) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import {
num, defOf, talentBb, traitBb, selectedId, altSkills, instantKindOf, alive, fx, textNum, enemiesOn,
} from '../shared/tier3.js';
export default {
// ---- 3_11 雪猎 · 猎手 — S2 风雪连弩: instant special-bullet double shot (higher vs non-moving targets), charges;
// 裂云一击: the skill also sends 裂云兽 (ATK% phys + cold); 精锐 module HUN-X: reload from empty adds extra rounds
// S1 强力击·β型: next attack ×atk_scale (a normal shot: needs a round); 裂云兽 hits the enemy it is cast on
chess_char_3_11_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const sel = selectedId(chess, d);
const S2 = 'skchr_snhunt_2';
const s1 = num(bb.atk_scale_1, 1), s2 = num(bb.atk_scale_2, s1);
const charges = Math.max(1, Math.floor(num(d.skill?.maxCharges, num(bb.ct, 1))));
const shots = textNum(d.skill?.description, /(\d+|[一二两三四五])连击/, 2);
const still = (e) => !!(e.blockedBy || e.s.flags.stun || e.s.flags.noMove || !e.moving);
const extra = num(tb.extra_add, 0);
return {
skill: {
kind: charges > 1 ? 'charges' : 'instant',
onStart({ battle, unit }) {
const t = enemiesOn(battle, unit, unit.rangeKeys, 1, unit.profile)[0] ?? null;
unit.mem.snTarget = t;
if (!t) return;
battle.addProjectile({
from: unit, target: t, speed: 16, visual: 'arrow', source: unit,
onHit: ({ target }) => {
if (!target || !target.alive) return;
const sc = still(target) ? s2 : s1;
for (let i = 0; i < shots && target.alive; i++) battle.dealDamage(unit, target, { amount: unit.s.atk * sc, type: 'phys', isSkill: true, tags: ['skill', 'snhunt'] });
},
});
fx(battle, 'volley', unit, { target: t.id, skill: 'snhunt_2' });
},
},
skills: altSkills(chess, d, bb, {
skchr_snhunt_1: (s) => ({ kind: instantKindOf(s), attack: { atkScale: num(s.bb.atk_scale, 1) } }),
}),
talents: [{ install(battle, unit) {
battle.on('skillStart', (ctx) => {
if (ctx.unit !== unit) return;
// S2 aims its special bullets itself (mem.snTarget); another skill is cast on the enemy about to be shot
const t = sel === S2 || sel == null ? unit.mem.snTarget : (enemiesOn(battle, unit, unit.rangeKeys, 1, unit.profile)[0] ?? null);
if (!t || !t.alive) return;
battle.dealDamage(unit, t, { amount: unit.s.atk * num(t0.atk_scale, 1), type: 'phys', isSkill: true, tags: ['talent', 'cloudbeast'] });
if (t.alive) battle.applyStatus(t, 'cold', { duration: num(t0.cold, 3), source: unit });
fx(battle, 'strike', t, { src: unit.id, talent: 'snhunt_beast' });
}, { owner: unit });
} }],
install(battle, unit) {
// special bullets (S2 only): the skill also fires while the magazine is empty (the attack loop cannot run then)
if (sel === S2 || sel == null) {
battle.on('tick', () => {
const sk = unit.skill;
if (!alive(unit) || !unit.canAct || !sk || !sk.ready || sk.opCooling || unit.s.flags.silence || (unit.trait.ammo ?? 1) > 0) return;
if (battle.enemiesInKeys(unit.baseRangeKeys || unit.rangeKeys, unit, unit.profile).length) sk.activate('DEFAULT');
}, { owner: unit });
}
if (extra > 0) {
battle.on('tick', () => {
const a = unit.trait.ammo ?? 0;
if (unit.mem.prevAmmo === 0 && a === 1) unit.trait.ammo = Math.min(Math.max(1, unit.profile.ammoMax ?? 8), a + extra);
unit.mem.prevAmmo = unit.trait.ammo;
}, { owner: unit });
}
},
};
},
};
@@ -0,0 +1,127 @@
// server/sim/content/kits/ops/chess_char_3_12-blemsh.js — 瑕光 (char_423_blemsh) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { bodyOnTile } from '../../../body.js';
import {
num, defOf, talentBb, traitBb, altSkills, instantKindOf, alive, onTiles, gridKeys, fx, copyGrid, NINE, isLeader,
giveSp,
} from '../shared/tier3.js';
export default {
// ---- 3_12 瑕光 · 守护者 — S3 先贤化身: ATK/DEF +, bonus arts per hit, heals another nearby ally per attack;
// 剑盾骑士: hurt-SP skills of the team also gain SP on attack; 仁慈: attacks sleeping enemies (first, ×atk_scale);
// 精锐 module GUA-Y: damage taken −15 %
// S1 光芒涌动 (自动触发 ⇒ DEFAULT, charges): next attack ×atk_scale phys + heals the most injured ally of the 3×3 (herself
// included) for heal_scale × ATK; S2 慑敌辉光: ATK +, puts every ground enemy on her own tile and every enemy she
// blocks to sleep (PRTS 备注; for the skill's duration: no own value in the data) and heals every ally of the
// skill range by ATK × ratio each second;
// 精锐 module GUA-X: her heals on allies under hp_ratio HP × heal_scale
chess_char_3_12_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const bonus = num(bb['attack@blemsh_s_3_extra_dmg[magic].atk_scale'], 0);
const heal = num(bb.heal_scale, 0);
const healGrid = copyGrid(d.skill?.rangeGrid) ?? NINE;
const mercy = num(t1.atk_scale, 1);
const sleepersInRange = (battle, unit) => {
const set = unit.rangeKeySet;
return set ? battle.enemies.filter((e) => e.alive && !e.hidden && e.s.flags.sleep && !e.s.flags.untargetable && !e.isFlying && onTiles(e, set)) : [];
};
const kit = {
trait: { hitSleep: true }, // 仁慈: 自身可以攻击…沉睡的目标 (沉睡 = 无敌 for everyone else)
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
heal: false,
attack: { onHit({ battle, unit, target }) {
if (bonus > 0 && target && target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * bonus, type: 'arts', isSkill: true, tags: ['skill', 'blemshBonus'] });
} },
onAttack({ battle, unit }) {
if (!(heal > 0)) return;
const ally = battle.injuredAlliesInKeys(new Set(gridKeys(healGrid, unit)), unit).find((a) => a !== unit);
if (ally) battle.heal(unit, ally, unit.s.atk * heal);
},
},
skills: altSkills(chess, d, bb, {
skchr_blemsh_1: (s) => {
const grid = copyGrid(s.rangeGrid) ?? NINE;
const hs = num(s.bb.heal_scale, 0);
return {
kind: instantKindOf(s),
heal: false,
attack: {
atkScale: num(s.bb.atk_scale, 1),
onHit({ battle, unit }) { // "恢复周围一名友方单位"
const ally = hs > 0 ? battle.injuredAlliesInKeys(new Set(gridKeys(grid, unit)), unit)[0] : null;
if (ally) battle.heal(unit, ally, unit.s.atk * hs);
},
},
};
},
skchr_blemsh_2: (s) => {
const grid = copyGrid(s.rangeGrid) ?? NINE;
const ratio = num(s.bb['attack@atk_to_hp_recovery_ratio'], num(s.bb.atk_to_hp_recovery_ratio, 0));
return {
kind: 'duration',
heal: false,
mods: { atkPct: num(s.bb.atk) },
onStart({ battle, unit, skill }) {
const dur = skill.timeLeft > 0 ? skill.timeLeft : Math.max(0.1, num(s.duration, 10));
let n = 0;
// PRTS 备注: "技能生效对象实际为“自身这格内的所有地面敌人及自身阻挡的敌人”,即使阻挡的是飞行敌人" — a blocked
// enemy stands at the block radius, outside her tile (Battle._checkBlock)
for (const e of battle.enemies) {
if (!e.alive || e.hidden) continue;
if (e.blockedBy !== unit && (e.isFlying || !bodyOnTile(e, unit.tileR, unit.tileC))) continue;
if (battle.applyStatus(e, 'sleep', { duration: dur, source: unit })) n++;
}
fx(battle, 'aoe', unit, { radius: 0.5, skill: 'blemsh_2', status: 'sleep', n });
unit.mem.blemshRegen = 0;
},
onTick({ battle, unit, dt }) { // "周围的所有友方单位每秒恢复相当于攻击力N%的生命值"
if (!(ratio > 0)) return;
unit.mem.blemshRegen = (unit.mem.blemshRegen ?? 0) + dt;
while (unit.mem.blemshRegen >= 1 - 1e-9) {
unit.mem.blemshRegen -= 1;
for (const a of battle.injuredAlliesInKeys(new Set(gridKeys(grid, unit)), unit)) battle.heal(unit, a, unit.s.atk * ratio);
}
},
};
},
}),
talents: [
{ install(battle, unit) {
unit.mem.blemshKnight = true;
battle.on('attack', (ctx) => {
const a = ctx.attacker;
if (!a || a.kind !== 'op' || a.ownerId !== unit.ownerId || a.skill?.spType !== 'hurt') return;
if (!alive(unit) || !isLeader(battle, unit, 'blemshKnight')) return;
giveSp(a, num(t0.sp, 1), 'talent');
}, { owner: unit });
} },
{ install(battle, unit) {
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit) return;
const s = sleepersInRange(battle, unit);
if (s.length) ctx.targets = [s[0], ...ctx.targets.filter((t) => t !== s[0])].slice(0, Math.max(1, ctx.targets.length));
}, { owner: unit });
battle.on('hit', (ctx) => {
if (ctx.source === unit && ctx.target.side === 'enemy' && ctx.target.s.flags.sleep) ctx.dmg.mul *= mercy;
}, { owner: unit });
// (a lone sleeper is found by the engine's own target search: `trait.hitSleep` makes sleepers targetable
// and damageable for her only)
} },
],
};
if (num(tb.damage_resistance) > 0) {
kit.install = (battle, unit) => battle.addBuff(unit, { key: 'trait:blemsh_guard', mods: { dmgTakenMul: 1 - num(tb.damage_resistance) }, persist: true, allowDead: true });
} else if (num(tb.heal_scale) > 0) {
// GUA-X "治疗生命值低于50%的友方单位时治疗量提升15%" (the HP before the heal)
kit.install = (battle, unit) => battle.on('heal', (ctx) => {
if (ctx.source === unit && ctx.target && ctx.target.hpRatio < num(tb.hp_ratio, 0.5)) ctx.amount *= num(tb.heal_scale, 1);
}, { owner: unit });
}
return kit;
},
};
@@ -0,0 +1,25 @@
// server/sim/content/kits/ops/chess_char_3_13-malist.js — 至简 (char_4054_malist) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, statSkill, textNum, funnelMap, installFunnelPrune } from '../shared/tier3.js';
export default {
// ---- 3_13 至简 · 驭械术师 — S2 神工意匠: next attack ATK% arts twice (charges); 忽有所悟: prob of ATK ×atk_scale per attack
chess_char_3_13_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const charges = Math.max(1, Math.floor(num(d.skill?.maxCharges, num(bb.ct, 1))));
const hits = textNum(d.skill?.description, /连续攻击(\d+|[一二两三四五])次/, 2);
const insight = (battle) => (battle.rng.chance(num(t0.prob, 0)) ? num(t0.atk_scale, 1) : 1);
return {
trait: { dmgMul: (battle, unit, target) => funnelMap(battle, unit, target) * insight(battle) },
skill: {
kind: charges > 1 ? 'charges' : 'instant',
attack: { atkScale: num(bb.atk_scale, 1), hits, dmgType: 'arts', dmgMul: (battle) => insight(battle) },
},
// S1 迅捷打击·γ型: ATK / ASPD + (the drones' ramp and 忽有所悟 stay on every attack through the trait)
skills: altSkills(chess, d, bb, { 'skcom_quickattack[3]': statSkill }),
install: installFunnelPrune,
};
},
};
@@ -0,0 +1,55 @@
// server/sim/content/kits/ops/chess_char_3_14-slbell.js — 初雪 (char_174_slbell) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, aura } from '../shared/tier3.js';
export default {
// ---- 3_14 初雪 · 削弱者 — S2 自然震慑: DEF/RES shred aura on every enemy in range; 虚弱化: low-HP enemies fragile;
// 双响: 2 targets
chess_char_3_14_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const mr = num(bb.magic_resistance, 0);
const shred = { defPct: num(bb.def) };
if (mr) {
if (Math.abs(mr) < 1) shred.resMul = Math.max(0, 1 + mr);
else shred.resFlat = mr;
}
const mt = Math.floor(num(t1['attack@max_target'], 1));
const kit = {
skill: {
kind: 'duration',
onStart({ battle, unit }) {
if (!unit.mem.slbellAura) unit.mem.slbellAura = aura(battle, unit, { key: 'skill:slbell_shred', side: 'enemy', interval: 0, tiles: () => unit.rangeKeySet, mods: shred });
unit.mem.slbellAura();
},
onTick({ unit }) { unit.mem.slbellAura?.(); },
onEnd({ unit }) { unit.mem.slbellAura?.clear(); },
},
// S1 传音回响: 2 targets, every enemy in range ASPD + attack_speed (negative) while it runs
skills: altSkills(chess, d, bb, {
skchr_slbell_1: (s) => {
const slow = { aspd: num(s.bb.attack_speed) };
return {
kind: 'duration',
targeting: { maxTargets: Math.max(1, Math.floor(num(s.bb['attack@max_target'], num(s.bb.max_target, 1)))) },
onStart({ battle, unit }) {
if (!unit.mem.slbellSlow) unit.mem.slbellSlow = aura(battle, unit, { key: 'skill:slbell_aspd', side: 'enemy', interval: 0, tiles: () => unit.rangeKeySet, mods: slow });
unit.mem.slbellSlow();
},
onTick({ unit }) { unit.mem.slbellSlow?.(); },
onEnd({ unit }) { unit.mem.slbellSlow?.clear(); },
};
},
}),
talents: [{ install(battle, unit) {
aura(battle, unit, {
key: 'talent:weak_fragile', side: 'enemy', tiles: () => unit.rangeKeySet, filter: (e) => e.hpRatio < num(t0.hp_ratio, 0.4),
mods: { dmgTakenMul: num(t0.damage_scale, 1) }, interval: 0.1,
});
} }],
};
if (mt > 1) kit.trait = { maxTargets: mt };
return kit;
},
};
@@ -0,0 +1,29 @@
// server/sim/content/kits/ops/chess_char_3_15-vodfox.js — 巫恋 (char_254_vodfox) kit, tier 3 (hidden).
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { releaseSkillSummon } from '../../tokens.js';
import { num, defOf, talentBb, aura } from '../shared/tier3.js';
export default {
// ---- 3_15 巫恋 · 削弱者 (hidden) — S2 诅咒娃娃 "获得一个诅咒娃娃(最多可库存1个)": the doll is a hand piece the player
// places (user playtest #6; PRTS 卫戍协议/帮助); each cast gives one and the placed piece takes the field on its
// own tile (tokens.js releaseSkillSummon: not at the battle start; not placed ⇒ no doll); its token kit keeps the
// 3×3 ATK/DEF aura (the token skill's bb) for 15 s; 溃败暗示: low-HP enemies fragile
chess_char_3_15_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tokenId = chess?.skill?.overrideTokenKey ?? (d.tokens || []).find((t) => /doll/.test(String(t))) ?? 'token_10006_vodfox_doll';
return {
skill: {
kind: 'instant',
onStart({ battle, unit }) { releaseSkillSummon(battle, unit, tokenId); },
},
talents: [{ install(battle, unit) {
aura(battle, unit, {
key: 'talent:weak_fragile', side: 'enemy', tiles: () => unit.rangeKeySet, filter: (e) => e.hpRatio < num(t0.hp_ratio, 0.4),
mods: { dmgTakenMul: num(t0.damage_scale, 1) }, interval: 0.1,
});
} }],
};
},
};
@@ -0,0 +1,26 @@
// server/sim/content/kits/ops/chess_char_3_16-snakek.js — 蛇屠箱 (char_150_snakek) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, traitBb, altSkills, statSkill, whileTrue } from '../shared/tier3.js';
export default {
// ---- 3_16 蛇屠箱 · 铁卫 — S2 壳状防御: stops attacking, block +1, DEF +, regen; 防御专精: DEF +;
// 精锐 module PRO-X: DEF + while blocking
chess_char_3_16_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const tb = traitBb(d);
const kit = {
skill: {
kind: 'duration',
mods: { defPct: num(bb.def), blockCnt: num(bb.block_cnt), hpRegenRatio: num(bb.hp_recovery_per_sec_by_max_hp_ratio) },
attack: { noAttack: true },
},
// S1 防御力强化·β型 (TAKE_DAMAGE): DEF + (she keeps attacking)
skills: altSkills(chess, d, bb, { 'skcom_def_up[2]': statSkill }),
talents: [{ install(battle, unit) { battle.addBuff(unit, { key: 'talent:snakek_def', mods: { defPct: num(t0.def) }, persist: true, allowDead: true }); } }],
};
if (num(tb.def) > 0) kit.install = (battle, unit) => whileTrue(battle, unit, 'trait:snakek_block', () => unit.blocking.length > 0, { defPct: num(tb.def) });
return kit;
},
};
@@ -0,0 +1,41 @@
// server/sim/content/kits/ops/chess_char_3_17-shotst.js — 流星 (char_126_shotst) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, altSkills, instantKindOf, fx, textNum, enemiesOn } from '../shared/tier3.js';
export default {
// ---- 3_17 流星 · 速射手 — S2 碎甲击·扩散: instant ATK% phys on ≤5 enemies in range + DEF shred for `duration` s;
// 空射专精: ×atk_scale vs flying (stacks with the module fly bonus)
chess_char_3_17_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const n = textNum(d.skill?.description, /至多(\d+)个/, 5);
const flyMul = (unit, t) => (t.isFlying ? (unit.profile.flyScale ?? 1) * num(t0.atk_scale, 1) : 1);
return {
trait: { dmgMul: (battle, unit, target) => flyMul(unit, target) },
skill: {
kind: 'instant',
onStart({ battle, unit }) {
const list = enemiesOn(battle, unit, unit.rangeKeys, n, unit.profile);
for (const e of list) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1) * flyMul(unit, e), type: 'phys', isSkill: true, tags: ['skill', 'burst'] });
if (e.alive) battle.addBuff(e, { key: 'skill:shotst_shred', duration: num(bb.duration, 5), refresh: 'extend', mods: { defPct: num(bb.def) }, source: unit, visible: true });
}
if (list.length) fx(battle, 'volley', unit, { targets: list.map((e) => e.id), skill: 'shotst_2' });
},
},
// S1 碎甲击: next attack ×atk_scale (fly × of the trait kept) and the target's DEF − for `duration` s
skills: altSkills(chess, d, bb, {
skchr_shotst_1: (s) => ({
kind: instantKindOf(s),
attack: {
atkScale: num(s.bb.atk_scale, 1),
onHit({ battle, unit, target }) {
if (target && target.alive && target.side === 'enemy') battle.addBuff(target, { key: 'skill:shotst_shred', duration: num(s.bb.duration, 5), refresh: 'extend', mods: { defPct: num(s.bb.def) }, source: unit, visible: true });
},
},
}),
}),
};
},
};
@@ -0,0 +1,127 @@
// server/sim/content/kits/ops/chess_char_3_18-vulpis.js — 忍冬 (char_4026_vulpis) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import {
num, defOf, talentBb, traitBb, altSkills, instantKindOf, alive, gridKeys, fx, copyGrid, enemiesOn, whileTrue,
isLeader,
} from '../shared/tier3.js';
/** 忍冬 S3's 迷彩 (until her next cast): its own buff key, never merged with another unit status. */
const CAMOU_KEY = 'vulpis:camou';
export default {
// ---- 3_18 忍冬 · 尖兵 — S3 隐狐之艺: +DP, range +1, ATK +, decaying ASPD, hits all blocked, 0.2 s stun per hit,
// after a kill during the skill 迷彩 from its end until the next cast ("进入迷彩状态,直至下一次开启技能") — the
// `camou` status under its own key (ba.camou "不阻挡时不成为敌方普通攻击的目标": only the enemy she blocks attacks
// her, as with 隐匿; it used to be the generic `stealth` = 隐匿 under the key `stealth`, shared with 伪装服's timed
// 隐匿, which the unending one then made permanent, and it counted as 隐匿 for 叙拉古 / 家族徽章 — user playtest #6
// "叙拉古阵营隐身不会结束": the bond's 隐匿 ends on time, this 迷彩 officially lasts); 追凶: bonus arts on marked
// enemies; 蓄势: DP regen + out-of-combat
// regen; 精锐 module SOL-X: ATK/DEF + while blocking
// S1 小施惩戒 (charges): next attack + extra_damage_ratio × ATK arts and +cost DP; S2 坠刃拷问 (charges, the data rule
// SKILL_RANGE: fires once an enemy is inside its own range 3-12, charges 3 s apart): +cost DP, ≤ max_target enemies of that range take atk_scale arts and
// 停顿 — the ones already 停顿 are also stunned. (SOL-Y "首次部署时部署费用-4": the initial deployment is free.)
chess_char_3_18_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const skillGrid = copyGrid(d.skill?.rangeGrid);
const aspd0 = num(bb.attack_speed, 0);
const kit = {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
attack: { hitAllBlocked: true, onHitStatus: { key: 'stun', duration: num(bb['attack@stun'], 0.2) } },
onStart({ battle, unit, skill }) {
battle.removeBuff(unit, CAMOU_KEY);
unit.mem.vulpisKill = false;
const dp = num(bb.cost, 0);
if (dp > 0) { battle.addDp(unit.ownerId, dp); fx(battle, 'dp', unit, { n: dp }); }
battle.addBuff(unit, { key: 'skill:vulpis_aspd', mods: { aspd: aspd0 } });
unit.mem.vulpisDur = skill.timeLeft;
},
onTick({ battle, unit, skill }) {
const b = unit.findBuff('skill:vulpis_aspd');
const dur = unit.mem.vulpisDur || skill.duration || 1;
if (b) { b.mods = { aspd: aspd0 * Math.max(0, skill.timeLeft) / dur }; unit.markDirty(); }
},
onEnd({ battle, unit, reason }) {
battle.removeBuff(unit, 'skill:vulpis_aspd');
if (reason !== 'death' && unit.alive && unit.mem.vulpisKill) {
battle.addBuff(unit, { key: CAMOU_KEY, flags: { camou: true }, status: 'camou', source: unit });
fx(battle, 'camouflage', unit);
}
},
},
skills: altSkills(chess, d, bb, {
skchr_vulpis_1: (s) => ({
kind: instantKindOf(s),
attack: { onHit({ battle, unit, target }) {
if (target && target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * num(s.bb.extra_damage_ratio), type: 'arts', isSkill: true, tags: ['skill', 'vulpisPunish'] });
const dp = num(s.bb.cost, 0);
if (dp > 0) { battle.addDp(unit.ownerId, dp); fx(battle, 'dp', unit, { n: dp }); }
} },
}),
skchr_vulpis_2: (s) => { // 坠刃拷问 — air units too (PRTS 备注 "※可对空"); data trigger SKILL_RANGE (its 3-12 range)
const grid = copyGrid(s.rangeGrid);
const n = Math.max(1, Math.floor(num(s.bb.max_target, 6)));
return {
kind: instantKindOf(s),
onStart({ battle, unit }) {
const dp = num(s.bb.cost, 0);
if (dp > 0) { battle.addDp(unit.ownerId, dp); fx(battle, 'dp', unit, { n: dp }); }
const list = enemiesOn(battle, unit, gridKeys(grid ?? unit.rangeGrid, unit), n);
for (const e of list) {
const was = !!e.findBuff('sluggish'); // "若敌人已经处于停顿状态" (before this cast)
battle.dealDamage(unit, e, { amount: unit.s.atk * num(s.bb.atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill', 'vulpisTorture'] });
if (!e.alive) continue;
battle.applyStatus(e, 'sluggish', { duration: num(s.bb.sluggish, 0), source: unit });
if (was && e.alive) battle.applyStatus(e, 'stun', { duration: num(s.bb.stun, 0), source: unit });
}
if (list.length) fx(battle, 'aoe', unit, { radius: 1.5, dmgType: 'arts', skill: 'vulpis_2', n: list.length });
},
};
},
}),
talents: [
{ install(battle, unit) { // 追凶
const marks = new Map();
// re-entrancy guard: content that turns a hit into HP loss credited to her (频次 enemies: every instance
// = 1 HP) would otherwise feed the bonus back into itself until the hook-depth limit
let busy = false;
battle.on('damaged', (ctx) => {
const e = ctx.target;
if (busy || ctx.source !== unit || e.side !== 'enemy' || ctx.type === 'element' || ctx.dmg?.tags?.includes('vulpisHunt')) return;
const t = marks.get(e.id);
if (t == null) { marks.set(e.id, battle.time); return; }
if (battle.time - t <= num(t0.interval, 10) + 1e-9 && e.alive) {
busy = true;
// PRTS 备注 "伤害类型为法术附加伤害": tag addition (no 叙拉古 6 roll)
try { battle.dealDamage(unit, e, { amount: unit.s.atk * num(t0.atk_scale), type: 'arts', canDodge: false, tags: ['talent', 'vulpisHunt', 'addition'] }); } finally { busy = false; }
}
}, { owner: unit });
battle.on('kill', (ctx) => { if (ctx.killer === unit && unit.skill?.active) unit.mem.vulpisKill = true; }, { owner: unit });
} },
{ install(battle, unit) { // 蓄势
unit.mem.vulpisDp = true;
const bonus = num(t1.delta_cost_increase_time, 1) - 1;
const quiet = num(t1.interval, 4);
const regen = num(t1['vulpis_t_2[heal][interval].hp_recovery_per_sec_by_max_hp_ratio'], 0);
battle.on('tick', (ctx) => {
if (!alive(unit)) return;
if (bonus > 0 && isLeader(battle, unit, 'vulpisDp')) battle.addDp(unit.ownerId, battle.flags.dpPerSec * ctx.dt * bonus);
const on = battle.time - unit.lastHitAt >= quiet && regen > 0;
const has = !!unit.findBuff('talent:vulpis_regen');
if (on && !has) battle.addBuff(unit, { key: 'talent:vulpis_regen', mods: { hpRegenRatio: regen } });
else if (!on && has) battle.removeBuff(unit, 'talent:vulpis_regen');
}, { owner: unit });
} },
],
};
if (skillGrid) kit.skill.targeting = { rangeGrid: skillGrid };
if (num(tb.atk) > 0 || num(tb.def) > 0) {
kit.install = (battle, unit) => whileTrue(battle, unit, 'trait:vulpis_block', () => unit.blocking.length > 0, { atkPct: num(tb.atk), defPct: num(tb.def) });
}
return kit;
},
};
@@ -0,0 +1,240 @@
// server/sim/content/kits/ops/chess_char_3_19-vigil.js — 伺夜 (char_427_vigil) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { num, defOf, talentBb, selectedId, altSkills, alive, fx, textNum, tacticalPoint } from '../shared/tier3.js';
/** Tokens `tokenId` summoned by / placed for `owner` (board pieces included). */
const tokensOf = (battle, owner, tokenId) => battle.allyUnits.filter((t) => t.kind === 'token' && t.defId === tokenId && t.ownerUnit === owner && !t.mem.isClone);
export default {
// ---- 3_19 伺夜 · 战术家 — the tactical reinforcement is the wolf pack (狼群领袖: 2 wolves, +1 every 25 s up to 3, each
// wolf = +1 block and one more bite, a wolf is lost instead of the pack dying); 狼群天性: DEF ignore vs pack-blocked
// enemies; S3 领袖的尊严: DP over time, 三连击, bonus arts vs pack-blocked enemies; 精锐 module: pack takes less
// damage from the enemies it blocks (token module talent). A 狼群 piece placed in the prep phase is the pack.
// S1 领袖的呼唤 (ALWAYS): +cost DP and one more “狼影” (≤ the talent's maximum); S2 领袖的馈赠: +cost DP, the pack
// recovers hp_ratio of its max HP and its next attack hits ×atk_scale — a kill by that attack gives +cost DP.
// 精锐 module TAC-Y: trait ×165 % (profession layer) and "援军阻挡的敌人更容易受到我方的攻击": the pack's token module
// talent taunt_level (+1) goes to the enemies it blocks — the enemy-side 嘲讽等级 our operators target first
// (targeting.js; research 05: "更容易受到我方的攻击" = enemy taunt.taunt_level), never to the pack itself.
chess_char_3_19_a: (bb, chess, def) => {
const d = defOf(chess, def);
const sel = selectedId(chess, d);
const t0 = d.talents?.[0] ?? {}, t1 = talentBb(d, 1);
const wolfId = t0.tokenKey ?? (d.tokens || []).find((t) => /wolf/.test(String(t))) ?? 'token_10028_vigil_wolf';
const initial = textNum(t0.description, /初始(\d+|[一二两三四五])只/, 2);
const maxWolves = textNum(t0.description, /至多(\d+|[一二两三四五])只/, 3);
const hits = textNum(d.skill?.description, /(\d+|[一二两三四五])连击/, 3);
const bonus = num(bb['attack@vigil_s_3.atk_scale'], 0);
const pen = num(t1.def_penetrate_fixed, 0);
const wolfOf = (u) => (u.trait.reinforcement && u.trait.reinforcement.alive ? u.trait.reinforcement : null);
const wolfKit = (vigil) => ({
skill: null,
trait: { hitsFn: (b, w) => Math.max(1, w.mem.wolves || 1) },
install(battle, w) {
const tal = w.def.talents || [];
const wb = tal[0]?.bb ?? {};
const per = num(wb.block_cnt, num(wb['vigil_wolf_t_1_enhance[trigger].block_cnt'], 1));
const grow = num(wb.interval, num(wb['vigil_wolf_t_1_enhance[trigger].interval'], 25));
// (mem.shadows mirrors the count for content reading tokens.js wolfShadows())
const apply = () => { w.mem.shadows = w.mem.wolves; battle.addBuff(w, { key: 'token:wolves', mods: { blockCnt: per * w.mem.wolves }, allowDead: true }); };
w.mem.wolves = Math.max(1, Math.min(maxWolves, initial));
apply();
w.mem.addWolf = () => {
if (!w.alive || w.mem.wolves >= maxWolves) return false;
w.mem.wolves++;
apply();
fx(battle, 'summon', w, { src: vigil.id, wolves: w.mem.wolves });
return true;
};
battle.every(grow, () => w.mem.addWolf(), { owner: w });
battle.on('fatal', (ctx) => {
if (ctx.unit !== w || ctx.prevented || w.mem.wolves <= 1) return;
w.mem.wolves--;
apply();
ctx.prevented = true;
w.hp = w.s.maxHp;
fx(battle, 'revive', w, { wolves: w.mem.wolves });
}, { owner: w });
const mod = tal.find((t) => t && t.bb && t.bb.damage_scale != null);
if (mod) {
battle.on('hit', (ctx) => {
if (ctx.target === w && ctx.source && ctx.source.blockedBy === w) ctx.dmg.mul *= num(mod.bb.damage_scale, 1);
}, { owner: w });
}
},
});
// TAC-Y token module talent (taunt_level) of the pack — whichever kit runs it (this one or a tokens.js board piece):
// the enemies it blocks carry that taunt level while they stay blocked (short refreshed buff: nothing lingers when
// the pack or 伺夜 leaves)
const packTaunt = (w) => {
if (w.mem.packTaunt == null) {
const t = (w.def?.talents || []).find((x) => x && x.bb && x.bb.taunt_level != null);
w.mem.packTaunt = t ? num(t.bb.taunt_level) : 0;
}
return w.mem.packTaunt;
};
const installPackMark = (battle, unit) => {
let cur = new Set();
const levelOf = (w) => (w && alive(w) && alive(unit) ? packTaunt(w) : 0);
const mark = (e, w, lvl) => battle.addBuff(e, { key: 'token:pack_mark', duration: 0.25, refresh: 'extend', mods: { taunt: lvl }, source: w, visible: true });
// at once when the pack blocks (the operators attacking this tick already see it), kept / dropped every 0.1 s
battle.on('blocked', (ctx) => {
const w = wolfOf(unit), lvl = ctx.blocker === w ? levelOf(w) : 0;
if (lvl && ctx.enemy.alive) { mark(ctx.enemy, w, lvl); cur.add(ctx.enemy); }
}, { owner: unit });
battle.every(0.1, () => {
const w = wolfOf(unit);
const lvl = levelOf(w);
const next = new Set();
if (lvl) {
for (const e of w.blocking) {
if (!e.alive || e.side !== 'enemy' || e.blockedBy !== w) continue;
mark(e, w, lvl);
next.add(e);
}
}
for (const e of cur) {
if (next.has(e)) continue;
const b = e.findBuff('token:pack_mark');
if (b) battle.removeBuff(e, b);
}
cur = next;
}, { owner: unit, immediate: true });
};
/** One more “狼影” (S1): this kit's pack, or a 狼群 board piece run by content/tokens.js (same 'wolf:shadows' buff). */
const addShadow = (battle, vigil, w) => {
if (typeof w.mem.addWolf === 'function') return w.mem.addWolf();
const n = Math.max(1, w.mem.shadows ?? 1);
if (n >= maxWolves) return false;
const wb = w.def?.talents?.[0]?.bb ?? {};
const per = num(wb['vigil_wolf_t_1_enhance[trigger].block_cnt'], num(wb.block_cnt, 1));
w.mem.shadows = n + 1;
battle.addBuff(w, { key: 'wolf:shadows', persist: true, allowDead: true, refresh: 'replace', mods: { blockCnt: w.mem.shadows * per } });
fx(battle, 'summon', w, { src: vigil.id, wolves: w.mem.shadows });
return true;
};
const dpGain = (battle, unit, n) => { if (n > 0) { battle.addDp(unit.ownerId, n); fx(battle, 'dp', unit, { n }); } };
// S2: the pack's empowered next attack (armed by the cast; ×scale on its hits; a kill pays once)
const installGift = (battle, unit) => {
battle.on('beforeAttack', (ctx) => {
const w = wolfOf(unit);
if (!w || ctx.attacker !== w || !w.mem.vigilGift) return;
w.mem.vigilGiftOn = w.mem.vigilGift;
w.mem.vigilGift = null;
}, { owner: unit, priority: -100 });
battle.on('hit', (ctx) => {
const g = ctx.source?.mem?.vigilGiftOn;
if (g && ctx.dmg.isAttack && ctx.source === wolfOf(unit)) ctx.dmg.mul *= g.scale;
}, { owner: unit });
battle.on('kill', (ctx) => {
const g = ctx.killer?.mem?.vigilGiftOn;
if (!g || g.paid || ctx.victim.side !== 'enemy' || ctx.killer !== unit.trait.reinforcement) return;
g.paid = true;
dpGain(battle, unit, g.dp);
}, { owner: unit });
battle.on('attack', (ctx) => { if (ctx.attacker?.mem?.vigilGiftOn && ctx.attacker === unit.trait.reinforcement) ctx.attacker.mem.vigilGiftOn = null; }, { owner: unit });
};
return {
trait: { install(battle, unit) {
// The pack is the tactician's 援军. The match also hands the player the 狼群 token to place in the prep phase
// (= choosing the tactical point): that board piece (tokens.js kit, owner-coupled effects left to this kit) is
// the pack when present — deployed early on its own tile if 伺夜 deploys first — never a second pack.
const spawn = () => {
if (!alive(unit) || wolfOf(unit)) return;
const pieces = tokensOf(battle, unit, wolfId);
const live = pieces.find((t) => alive(t));
if (live) { unit.trait.reinforcement = live; return; }
const waiting = pieces.find((t) => !t.alive && !t.removed);
if (waiting && battle.redeploy(waiting, { free: true })) {
unit.trait.reinforcement = waiting;
fx(battle, 'summon', waiting, { src: unit.id, token: wolfId });
return;
}
const board = pieces.find((t) => t.uid != null);
const tile = tacticalPoint(battle, unit, board ? [board.homeR, board.homeC] : null);
if (!tile) return;
const w = battle.spawnToken(unit, wolfId, tile[0], tile[1], { kit: wolfKit(unit) });
unit.trait.reinforcement = w;
if (!w) return;
fx(battle, 'summon', w, { src: unit.id, token: wolfId, wolves: w.mem.wolves });
const again = Math.max(0, w.base.respawnTime || 0);
battle.on('death', (c) => { if (c.unit === w && c.reason === 'killed') battle.after(again, spawn, { owner: unit }); }, { owner: w });
};
battle.on('deploy', (c) => { if (c.unit === unit) spawn(); }, { owner: unit });
installPackMark(battle, unit);
battle.on('death', (c) => {
if (c.unit !== unit) return;
const w = wolfOf(unit);
if (w) battle.retreat(w, { reason: 'expired', permanent: true });
}, { owner: unit });
} },
skill: {
kind: 'duration',
attack: { hits },
onStart({ unit }) { unit.mem.vigilAcc = 0; unit.mem.vigilDp = 0; },
onTick({ battle, unit, dt }) {
const iv = num(bb.interval, 1.5), step = num(bb.cost, 1), cap = num(bb.value, 0);
if (!(iv > 0) || !(step > 0)) return; // (junk blackboard: never spin)
unit.mem.vigilAcc += dt;
while (unit.mem.vigilAcc >= iv - 1e-9 && unit.mem.vigilDp + step <= cap + 1e-9) {
unit.mem.vigilAcc -= iv;
unit.mem.vigilDp += step;
battle.addDp(unit.ownerId, step);
}
},
onEnd({ battle, unit, reason }) {
// the whole `value` is granted over a full duration (精锐: 11 × 1.364 s ends a hair after 15 s)
const rest = num(bb.value, 0) - (unit.mem.vigilDp ?? 0);
if (reason === 'duration' && rest > 1e-9) { battle.addDp(unit.ownerId, rest); unit.mem.vigilDp += rest; }
fx(battle, 'dp', unit, { n: unit.mem.vigilDp ?? 0 });
},
},
skills: altSkills(chess, d, bb, {
// (自动触发: an AUTO skill takes no 技能策略 — the 战术家 row is for MANUAL skills — and this DP skill fires as soon
// as it is ready, as before)
skchr_vigil_1: (s) => ({
kind: 'instant',
trigger: 'SP_FULL',
onStart({ battle, unit }) {
dpGain(battle, unit, num(s.bb.cost, 0));
const w = wolfOf(unit);
if (w) addShadow(battle, unit, w);
},
}),
skchr_vigil_2: (s) => ({
kind: 'instant',
onStart({ battle, unit }) {
dpGain(battle, unit, num(s.bb.cost, 0));
const w = wolfOf(unit);
if (!w) return;
const hr = num(s.bb['vigil_wolf_s_2.hp_ratio'], 0);
if (hr > 0) battle.heal(w, w, w.s.maxHp * hr, { self: true });
w.mem.vigilGift = { scale: num(s.bb['vigil_wolf_s_2.atk_scale'], 1), dp: num(s.bb['vigil_wolf_s_2.cost'], 0), paid: false };
fx(battle, 'buff', w, { src: unit.id, skill: 'vigil_2' });
},
}),
}),
install(battle, unit) { if (sel === 'skchr_vigil_2') installGift(battle, unit); },
talents: [
{ install() { /* 狼群领袖: the pack itself (trait.install / wolfKit) */ } },
{ install(battle, unit) {
battle.on('hit', (ctx) => {
const w = wolfOf(unit);
// "伺夜和狼群对其的攻击无视其175防御力": their attacks only (not item procs or other non-attack damage)
if (!w || pen <= 0 || !ctx.dmg.isAttack || (ctx.source !== unit && ctx.source !== w) || ctx.target.blockedBy !== w) return;
ctx.dmg.defIgnoreFlat += pen;
}, { owner: unit });
battle.on('damaged', (ctx) => {
const w = wolfOf(unit);
if (!w || !(bonus > 0) || !unit.skill?.active || !ctx.dmg?.isAttack || (ctx.source !== unit && ctx.source !== w)) return;
const e = ctx.target;
if (e.side !== 'enemy' || !e.alive || e.blockedBy !== w) return;
// "狼群与伺夜攻击…时,额外造成…": the extra hit belongs to that attack's dealer (a bite's bonus is the pack's
// damage — never re-typed by 伺夜's 弱点伤害 特质), its size to 伺夜's ATK (same as tokens.js unmanaged mode)
battle.dealDamage(ctx.source, e, { amount: unit.s.atk * bonus, type: 'arts', canDodge: false, isSkill: true, tags: ['skill', 'vigilBonus'] });
}, { owner: unit });
} },
],
};
},
};
@@ -0,0 +1,58 @@
// server/sim/content/kits/ops/chess_char_3_20-kjera.js — 耶拉 (char_4013_kjera) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { num, defOf, talentBb, altSkills, statSkill, funnelMap, installFunnelPrune } from '../shared/tier3.js';
export default {
// ---- 3_20 耶拉 · 驭械术师 — S2 心随意动: +1 drone (2 locks; a lone enemy gets both), ATK +, cold procs; 低眉: ATK +
// (more with ≥cnt ground tiles in range)
chess_char_3_20_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0);
const extra = Math.floor(num(bb['attack@cnt'], 0));
const prob = num(bb['attack@prob'], 0), cold = num(bb['attack@cold'], 0);
const kit = {
trait: { dmgMul: funnelMap },
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
attack: { onHit({ battle, unit, target }) {
if (target && target.alive && cold > 0 && battle.rng.chance(prob)) battle.applyStatus(target, 'cold', { duration: cold, source: unit });
} },
},
// S1 攻击力强化·γ型: ATK + (one drone, the trait ramp as usual)
skills: altSkills(chess, d, bb, { 'skcom_atk_up[3]': statSkill }),
install(battle, unit) {
installFunnelPrune(battle, unit);
// "浮游单元+1": every drone locks an enemy — with fewer enemies in range than drones, the spare drone joins a
// lock (both attack it, each hit rolls the cold proc; they ramp in parallel, see funnelMap)
if (extra > 0) {
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !unit.skill?.active || !ctx.targets.length) return;
const n = 1 + extra;
if (ctx.targets.length >= n) return;
const base = ctx.targets.slice();
const out = base.slice();
for (let i = 0; out.length < n; i++) out.push(base[i % base.length]);
ctx.targets = out;
}, { owner: unit, priority: -50 });
}
},
talents: [{ install(battle, unit) {
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
let low = 0;
for (const k of unit.baseRangeKeys || []) {
const r = (k / COLS) | 0, c = k % COLS;
if (battle.grid.inRect(r, c) && battle.grid.isLow(r, c)) low++;
}
const atk = low >= num(t0.cnt, 2) ? num(t0['kjera_t_1[high].atk'], num(t0.atk)) : num(t0.atk);
battle.addBuff(unit, { key: 'talent:kjera_brow', mods: { atkPct: atk } });
}, { owner: unit });
} }],
};
if (extra > 0) kit.skill.targeting = { maxTargets: 1 + extra };
return kit;
},
};
@@ -0,0 +1,122 @@
// server/sim/content/kits/ops/chess_char_3_21-archet.js — 空弦 (char_332_archet) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { canTargetEnemy } from '../../../targeting.js';
import { bodyDist } from '../../../body.js';
import {
num, defOf, talentBb, moduleTalentBb, altSkills, instantKindOf, groundAspd, alive, fx, enemiesOn, isLeader, giveSp,
} from '../shared/tier3.js';
/** The unit's own profile damage multiplier on `target` (fastshot fly bonus …) for damage a kit deals itself. */
function profileMul(battle, unit, target) {
const f = unit.profile?.dmgMul;
const m = typeof f === 'function' ? f(battle, unit, target) : f;
return typeof m === 'number' && Number.isFinite(m) ? m : 1;
}
/** Targetable enemies within `r` tiles of (x, y), nearest first (spawn order breaks ties), excluding `skip`. */
function enemiesAround(battle, unit, x, y, r, skip = null) {
const out = battle.foesInRadius(x, y, r).filter((e) => !(skip && skip.has(e)) && canTargetEnemy(unit, e, { canHitFly: true }));
const d = (e) => bodyDist(e, x, y);
return out.sort((a, b) => d(a) - d(b) || (a.spawnSeq ?? a.id) - (b.spawnSeq ?? b.id));
}
/** 周围 (around a target) for 空弦's scatter / bounce arrows [ASSUMED radius, tiles: not in the data]. */
const AROUND_R = 1.5;
export default {
// ---- 3_21 空弦 · 速射手 — S3 箭矢·暴风: ATK +, range +1, 3 hits × 2 targets; 兰登战术: attack-SP snipers +1 SP / 2.5 s;
// 铁弦: one-hit shield at deployment, +7 SP when it breaks; 精锐 module MAR-Y: ASPD + with ground enemies in range
// S1 箭矢·散逸: next attack ×atk_scale + up to max_target−1 other enemies around the target × atk_scale_2;
// S2 箭矢·追猎 (charges): an arrow fired at once hits its target `times` times, then bounces to the nearest enemy
// around not hit yet with one hit fewer each bounce (5, 4, 3, 2, 1). 精锐 MAR-X: fly × (profession layer);
// ISW-A (集成战略 only): no range bonus here.
chess_char_3_21_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const mod = moduleTalentBb(chess);
const skill = {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
attack: { hits: Math.max(1, Math.floor(num(bb['attack@times'], 1))) },
};
const tg = {};
if (num(bb.ability_range_forward_extend) > 0) tg.rangeExtend = Math.round(bb.ability_range_forward_extend);
if (num(bb['attack@max_target']) > 1) tg.maxTargets = Math.floor(bb['attack@max_target']);
if (Object.keys(tg).length) skill.targeting = tg;
const physHit = (battle, unit, e, scale, tag) => battle.dealDamage(unit, e, { amount: unit.s.atk * scale * profileMul(battle, unit, e), type: 'phys', isSkill: true, tags: ['skill', tag] });
const kit = {
skill,
skills: altSkills(chess, d, bb, {
skchr_archet_1: (s) => {
const more = Math.max(0, Math.floor(num(s.bb.max_target, 4)) - 1);
const sc2 = num(s.bb.atk_scale_2, 1);
return {
kind: instantKindOf(s),
attack: {
atkScale: num(s.bb.atk_scale, 1),
onHit({ battle, unit, target, x, y }) {
const cx = target ? target.x : x, cy = target ? target.y : y;
if (!Number.isFinite(cx) || !Number.isFinite(cy) || !(more > 0)) return;
const others = enemiesAround(battle, unit, cx, cy, AROUND_R, new Set([target])).slice(0, more);
for (const e of others) physHit(battle, unit, e, sc2, 'archetScatter');
if (others.length) fx(battle, 'volley', unit, { targets: others.map((e) => e.id), skill: 'archet_1' });
},
},
};
},
skchr_archet_2: (s) => {
const times = Math.max(1, Math.floor(num(s.bb.times, 5)));
const sc = num(s.bb.atk_scale, 1);
return {
kind: instantKindOf(s),
onStart({ battle, unit }) {
const first = enemiesOn(battle, unit, unit.rangeKeys, 1, unit.profile)[0];
if (!first) return;
const seen = new Set();
const fire = (from, target, n) => {
seen.add(target);
battle.addProjectile({
from, target, speed: 16, visual: 'arrow', source: unit, hitDead: true,
onHit: ({ target: t, x, y }) => {
if (t && t.alive) for (let i = 0; i < n && t.alive; i++) physHit(battle, unit, t, sc, 'archetPursuit');
if (n <= 1) return;
const next = enemiesAround(battle, unit, x, y, AROUND_R, seen)[0];
if (next) fire({ x, y }, next, n - 1);
},
});
};
fire(unit, first, times);
fx(battle, 'volley', unit, { target: first.id, skill: 'archet_2' });
},
};
},
}),
talents: [
{ install(battle, unit) { // 兰登战术
unit.mem.archetTactics = true;
battle.every(num(t0.interval, 2.5), () => {
if (!alive(unit) || !isLeader(battle, unit, 'archetTactics')) return;
for (const a of battle.allies(unit.ownerId)) {
if (a.kind === 'op' && a.def?.profession === 'SNIPER' && a.skill?.spType === 'attack') giveSp(a, num(t0.sp, 1), 'talent');
}
}, { owner: unit });
} },
{ install(battle, unit) { // 铁弦
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
battle.addBuff(unit, {
key: 'talent:archet_shield', shieldHits: 1, visible: true,
onRemove: ({ unit: u, buff }) => {
if (buff.shieldHits > 0 || !u.alive) return;
giveSp(u, num(t1.sp), 'talent');
fx(battle, 'shieldBreak', u);
},
});
fx(battle, 'shield', unit);
}, { owner: unit });
} },
],
};
if (mod && num(mod.attack_speed) > 0) kit.install = groundAspd('trait:archet_ground', num(mod.attack_speed));
return kit;
},
};
@@ -0,0 +1,144 @@
// server/sim/content/kits/ops/chess_char_4_01-rmixer.js — 信仰搅拌机 (char_4194_rmixer) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { isHpLoss } from '../../../damage.js';
import { holdsUndying } from '../../items/battle.js';
import {
AURA, num, tbb, grid, nationOf, enemiesOnRange, targetsInRange, whileDeployed, reveal, skillActive, isSel, alt,
instantKind, withDefaults,
} from '../shared/tier4.js';
/** Is module `id` the active one? (elites; `'none'` / normal chess ⇒ false) */
const moduleIs = (def, id) => !!(def?.raw?.module?.active && def.raw.module.id === id);
/**
* Permanent forward range extension (攻击距离 +n): every range, skill ranges included — except a skill whose range ignores
* 攻击距离 (`targeting.noRangeExtend`). SPT-Y's "攻击距离+1(不受技能攻击范围变化影响)" reads, as PRTS 修正 it, "(部分技能不受
* 此影响)" (原因: 语序颠倒、表述模糊) — 信仰搅拌机 S3 备注 "此技能的攻击范围不受'攻击距离'属性影响".
*/
const rangeUp = (battle, unit, n = 1) => battle.addBuff(unit, { key: 'module:range', mods: { rangeExtend: n }, persist: true, allowDead: true });
export default withDefaults({
// ===== 信仰搅拌机 (shotprotector) S3 退休前布道 — ammo 30 counters; talents 扫射迎宾仪礼 / 架盾送客仪礼; module: reveal
// S1 铳骑主考官 (自动触发 ⇒ DEFAULT: next attack ×3 hits + reload an adjacent 拉特兰 ammo skill), S2 八臂电锯侠 (ammo;
// a fatal hit is blocked for `ammo_cost` bullets); module SPT-Y 老朋友: range +1 (not on S3's range, PRTS 备注)
chess_char_4_01_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const counterMax = Math.max(1, Math.floor(num(bb['attack@max_target'], 3)));
const counterRatio = num(bb.base_attack_time, 0.6); // 反击最小间隔 = 实际攻击间隔 × ratio (not a BAT change)
const g = grid(def.skill?.rangeGrid);
const S2 = isSel(def, 'skchr_rmixer_2'), S3 = isSel(def, 'skchr_rmixer_3');
return {
skills: alt(def, {
// 铳骑主考官: the next attack is a triple hit of atk_scale each and reloads `charge` bullet(s) into ONE other
// 拉特兰 operator around him (8 tiles, [ASSUMED] nearest first) whose ammo skill runs
skchr_rmixer_1: () => ({
kind: instantKind(def),
attack: {
atkScale: num(bb.atk_scale, 1.3), hits: 3,
onHit({ battle, unit }) {
const n = num(bb.charge, 1);
const cand = battle.alliesInRadius(unit.x, unit.y, 1.5, unit.ownerId)
.filter((a) => a !== unit && a.kind === 'op' && nationOf(a) === 'laterano' && a.skill && a.skill.active && a.skill.kind === 'ammo')
.sort((a, b) => Math.hypot(a.x - unit.x, a.y - unit.y) - Math.hypot(b.x - unit.x, b.y - unit.y) || a.id - b.id);
if (!cand[0] || !(n > 0)) return;
cand[0].skill.addAmmo(n);
battle.fx('reload', { x: cand[0].x, y: cand[0].y, id: cand[0].id, n });
},
},
}),
// 八臂电锯侠: ATK/DEF up, 42/45 bullets; the fatal-hit guard is installed below
skchr_rmixer_2: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 42),
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
onStart({ battle, unit }) { battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 30),
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), defPct: num(bb.def) },
// 3-13 exactly: SPT-Y's 攻击距离+1 does not reach it (PRTS 备注; reviewer of community report E1 after 0.1.0)
targeting: g ? { rangeGrid: g, noRangeExtend: true } : undefined,
attack: { noAttack: true }, // 停止主动攻击敌人 (ammo is spent by counters)
onStart({ battle, unit }) {
unit.mem.counterReady = -Infinity;
for (const a of battle.allies(unit.ownerId)) {
if (a === unit || a.kind !== 'op' || nationOf(a) !== 'laterano') continue;
if (a.skill && a.skill.active && a.skill.kind === 'ammo') { a.skill.addAmmo(num(bb.ammo, 0)); battle.fx('reload', { x: a.x, y: a.y, id: a.id, n: num(bb.ammo, 0) }); }
}
battle.fx('sermon', { x: unit.x, y: unit.y, id: unit.id });
},
},
talents: [
{ install(battle, unit) { // 扫射迎宾仪礼: every damage dealt → DEF +30 / ASPD +3 for 10 s, ≤ 3 stacks
battle.on('damaged', (c) => {
if (c.source !== unit || !unit.alive || c.target.side !== 'enemy' || !(c.amount > 0)) return;
battle.addBuff(unit, { key: 'rmixer:t1', duration: num(t0.duration, 10), refresh: 'stack', maxStacks: Math.max(1, num(t0.max_stack_cnt, 3)),
mods: { defFlat: num(t0.def), aspd: num(t0.attack_speed) } });
}, { owner: unit });
} },
{ install(battle, unit) { // 架盾送客仪礼: no active attack for 8 s → shield 15 % max HP; timer restarts when it breaks
const since = () => Math.max(unit.deployedAt, unit.mem.lastActiveAtk ?? -Infinity, unit.mem.shieldLostAt ?? -Infinity);
battle.on('attack', (c) => { if (c.attacker === unit && !unit.mem.inCounter) unit.mem.lastActiveAtk = battle.time; }, { owner: unit });
battle.on('deploy', (c) => { if (c.unit === unit) { unit.mem.lastActiveAtk = -Infinity; unit.mem.shieldLostAt = -Infinity; } }, { owner: unit });
whileDeployed(battle, unit, 0.1, () => {
if (unit.findBuff('rmixer:shield') || battle.time - since() < num(t1.interval, 8) - 1e-9) return;
battle.addBuff(unit, { key: 'rmixer:shield', shield: unit.s.maxHp * num(t1.shield, 0.15), visible: true,
onRemove: () => { unit.mem.shieldLostAt = battle.time; } });
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
});
} },
],
install(battle, unit) {
if (S2) {
// S2 "技能期间若受到致命伤害,立即消耗N发弹药抵挡这次伤害": the lethal hit is negated (HP back to its value
// before the hit) and N bullets go; with fewer left it still blocks, spends them all and the skill ends (PRTS 备注
// "弹药量不足时仍可抵挡致命伤害,此时将消耗所有剩余弹药并退出技能状态"). "自身持有不死时此效果不会生效": a 不死
// that runs before it (骑士戒律, priority 20) has already prevented the hit; 坚固维式重锤's held window
// (items/battle.js holdsUndying: this deployment's, not run out) runs last, so the guard steps aside while it lasts
const cost = Math.max(1, Math.floor(num(bb.ammo_cost, 30)));
battle.on('hit', (c) => { if (c.target === unit) unit.mem.rmixerPre = { dmg: c.dmg, hp: unit.hp }; }, { owner: unit, priority: -100 });
battle.on('fatal', (c) => {
const sk = unit.skill;
if (c.unit !== unit || c.prevented || !sk || !sk.active || sk.kind !== 'ammo' || !(sk.ammoLeft > 0)) return;
if (holdsUndying(battle, unit)) return;
c.prevented = true;
const pre = unit.mem.rmixerPre;
unit.hp = pre && pre.dmg === c.dmg ? Math.max(1, Math.min(unit.s.maxHp, pre.hp)) : 1;
const spent = Math.min(cost, sk.ammoLeft);
sk.ammoLeft -= spent;
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id, n: spent });
if (sk.ammoLeft <= 0) sk.end('ammo');
}, { owner: unit });
}
// module SPT-Y (elite, 老朋友): 攻击距离+1 — S1 / S2 keep it, S3's 3-13 does not (noRangeExtend above)
if (num(def.traitBb?.ability_range_forward_extend, 0) > 0) rangeUp(battle, unit, num(def.traitBb.ability_range_forward_extend, 1));
// module SPT-X (elite default): 攻击范围内敌人的隐匿效果失效
if (moduleIs(def, 'uniequip_002_rmixer')) whileDeployed(battle, unit, AURA, () => reveal(battle, enemiesOnRange(battle, unit)));
if (!S3) return;
// S3 counter (PRTS 备注 "反击受到任何伤害后均可触发,无需目标,视为普通攻击,受各类无法触发普通攻击效果的影响"): ANY damage
// she takes — an enemy's attack, a zone, the 无来源 源石溶剂 tick (items/battle.js periodic_damage) — fires one volley at
// ≤ 3 enemies in range (the attacker first), at most once per actual interval × ratio. With no enemy in range the
// counter still happens and spends its bullet (无需目标: the drain alone empties the skill — 莫斯提马's 特质 turns those
// bullets into 拉特兰 layers, player report D1). A 流失 (Battle.loseHp, tag 'hpLoss': it skips every damage event —
// PRTS 作战机制) and an element 损伤 never counter; stunned / disarmed: no counter
battle.on('damaged', (c) => {
const d = c.dmg;
if (c.target !== unit || !unit.canAct || unit.s.flags.disarm || !skillActive(unit) || !d || c.type === 'element') return;
if (isHpLoss(d)) return;
if (battle.time < (unit.mem.counterReady ?? -Infinity) - 1e-9) return;
const list = targetsInRange(battle, unit);
const i = c.source ? list.indexOf(c.source) : -1;
if (i > 0) { list.splice(i, 1); list.unshift(c.source); }
const targets = list.slice(0, counterMax);
unit.mem.counterReady = battle.time + unit.s.interval * counterRatio;
unit.mem.inCounter = true;
try {
if (targets.length) battle.forceAttack(unit, targets);
else { unit.stats.attacks++; unit.skill.onAttackPerformed([], true); } // a counter into nothing: its bullet goes
} finally { unit.mem.inCounter = false; }
battle.fx('counter', { x: unit.x, y: unit.y, id: unit.id, n: targets.length });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,75 @@
// server/sim/content/kits/ops/chess_char_4_02-mostma.js — 莫斯提马 (char_213_mostma) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, grid, enemiesOnRange, whileDeployed, pulse, spAura, skillActive, isSel, alt, applyModuleRange,
withDefaults,
} from '../shared/tier4.js';
const TICK_EPS = 0.01; // minimal status duration (s)
/**
* Knock-back of 力度 `force` away from `from` (a radial push, Battle.push: the official 力度 − 重量 distance — PRTS 推与拉;
* user playtest #6 item 14). Returns the tiles moved.
*/
function shove(battle, e, from, force) {
return e && e.alive ? battle.push(e, num(force, 0), { from }) : 0;
}
export default withDefaults({
// ===== 莫斯提马 (splashcaster) S3 序时之匙 — ripple hits all in range, +ATK, talent 2 ×3, small knock-back
// S1 攻击力强化·γ型; S2 荒时之锁 (every enemy in range stunned for the rest of the skill, 100 %/120 % ATK arts
// per second); module SPC-X (资深万国信使定制斗篷): 攻击范围扩大
chess_char_4_02_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const g = grid(def.skill?.rangeGrid);
const force = num(bb['attack@force'], num(bb.force, 0));
const S3 = isSel(def, 'skchr_mostma_3');
return {
skills: alt(def, {
'skcom_atk_up[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk) } }),
skchr_mostma_2: () => ({
kind: 'duration',
onStart({ battle, unit, skill }) {
unit.mem.timeLock = { acc: 0, locked: new Set() };
battle.fx('zone', { x: unit.x, y: unit.y, id: unit.id, duration: skill.duration });
},
onTick({ battle, unit, skill, dt }) {
const L = unit.mem.timeLock;
if (!L) return;
// every enemy standing in her range (entering later too) is stunned until the skill ends
for (const e of enemiesOnRange(battle, unit)) {
if (L.locked.has(e.id)) continue;
L.locked.add(e.id);
battle.applyStatus(e, 'stun', { duration: Math.max(TICK_EPS, skill.timeLeft), source: unit });
}
L.acc += dt;
while (L.acc + 1e-9 >= 1) {
L.acc -= 1;
for (const e of enemiesOnRange(battle, unit)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill', 'timeLock'] });
}
}
},
onEnd({ unit }) { unit.mem.timeLock = null; },
}),
}),
install(battle, unit) { applyModuleRange(battle, unit, def); }, // module SPC-X: 攻击范围扩大 (module range grid)
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
targeting: { ...(g ? { rangeGrid: g } : {}), allInRange: true },
attack: { projectile: 'none', splashRadius: 0, onHit({ battle, unit, target }) { if (target && target.alive) shove(battle, target, unit, force); } },
onAttack({ battle, unit }) { battle.fx('ripple', { x: unit.x, y: unit.y, id: unit.id }); },
},
talents: [
{ install(battle, unit) { spAura(battle, unit, num(t0.sp_recovery_per_sec, 0.4), (a) => a.def?.profession === 'CASTER'); } },
{ install(battle, unit) { // 主观缓时: enemies in range −15 % move speed (×talent_scale during S3)
whileDeployed(battle, unit, AURA, () => {
const v = num(t1.move_speed, -0.15) * (S3 && skillActive(unit) ? num(bb.talent_scale, 3) : 1);
for (const e of enemiesOnRange(battle, unit)) pulse(battle, e, `mostma:slow:${unit.id}`, { moveMul: Math.max(0, 1 + v) }, { status: 'slow', visible: true });
});
} },
],
};
},
});
@@ -0,0 +1,77 @@
// server/sim/content/kits/ops/chess_char_4_03-kjera.js — 耶拉 (char_4013_kjera) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { canTargetEnemy } from '../../../targeting.js';
import { num, tbb, skillActive, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 耶拉 (funnel, hidden) S2 心随意动 — +1 drone locking targets, ATK up, chance to chill; talent 低眉
chess_char_4_03_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const n = 1 + Math.max(0, Math.floor(num(bb['attack@cnt'], 1)));
const prob = num(bb['attack@prob'], 0), cold = num(bb['attack@cold'], 0);
const funnel = (unit) => unit.profile.funnel || { init: 0.2, delta: 0.15, max: 1.1 };
return {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
targeting: { maxTargets: n },
attack: {
// every drone ramps on its own locked target (trait init/delta/max): the multipliers are queued per target
// by the beforeAttack hook below (two drones on the same enemy ramp independently)
dmgMul: (battle, unit, target) => {
const q = unit.mem.kjeraQueue && unit.mem.kjeraQueue.get(target.id);
return q && q.length ? q.shift() : funnel(unit).init;
},
onHit({ battle, unit, target }) {
if (target && target.alive && cold > 0 && battle.rng.chance(prob)) battle.applyStatus(target, 'cold', { duration: cold, source: unit });
},
},
onStart({ battle, unit }) { unit.mem.kjeraDrones = []; unit.mem.kjeraQueue = new Map(); battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n }); },
onEnd({ unit }) { unit.mem.kjeraDrones = []; unit.mem.kjeraQueue = null; },
},
talents: [{ install(battle, unit) { // 低眉: ATK +10 %, +16 % with ≥ 2 ground tiles in range
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
let low = 0;
for (const k of unit.rangeKeys || []) if (battle.grid.isLow((k / COLS) | 0, k % COLS)) low++;
const v = low >= num(t0.cnt, 2) ? num(t0['kjera_t_1[high].atk'], num(t0.atk)) : num(t0.atk);
battle.addBuff(unit, { key: 'kjera:t1', mods: { atkPct: v } });
}, { owner: unit });
} }],
install(battle, unit) {
// n drones, each locked on its enemy until that enemy dies (while at least one enemy is in her range). A free
// drone takes an enemy no other drone holds, else doubles up on the best target (a lone enemy/boss gets both).
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillActive(unit)) return;
const ok = (e) => !!e && e.alive && canTargetEnemy(unit, e, { canHitFly: true });
const cands = c.targets.filter(ok);
const drones = unit.mem.kjeraDrones || (unit.mem.kjeraDrones = []);
const held = new Set();
for (let i = 0; i < n; i++) { if (drones[i] && ok(drones[i].e)) held.add(drones[i].e); else drones[i] = null; }
for (let i = 0; i < n; i++) {
if (drones[i]) continue;
const e = cands.find((x) => !held.has(x)) ?? cands[0] ?? drones.find((d) => d)?.e;
if (!e) continue;
held.add(e);
drones[i] = { e, scale: null };
}
const f = funnel(unit);
const q = unit.mem.kjeraQueue || (unit.mem.kjeraQueue = new Map());
const out = [];
for (const d of drones) {
if (!d) continue;
d.scale = d.scale == null ? f.init : Math.min(f.max, d.scale + f.delta);
out.push(d.e);
const l = q.get(d.e.id);
if (l) l.push(d.scale); else q.set(d.e.id, [d.scale]);
}
if (out.length) c.targets = out;
}, { owner: unit, priority: 10 });
// queued multipliers of shots that never landed (target died mid-flight) are dropped with the target
battle.on('death', (c) => { if (c.unit.side === 'enemy' && unit.mem.kjeraQueue) unit.mem.kjeraQueue.delete(c.unit.id); }, { owner: unit });
},
};
},
});
@@ -0,0 +1,168 @@
// server/sim/content/kits/ops/chess_char_4_04-ines.js — 伊内丝 (char_4087_ines) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyDist } from '../../../body.js';
import { canTargetEnemy } from '../../../targeting.js';
import { AURA, num, tbb, grid, enemiesOnRange, pulse, isSel, alt, instantKind, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 伊内丝 (agent) S2 暗夜无明 — stealth, +ATK, +1 DP & steal ASPD per attack; talents 影织 / 影哨
// S1 淬影突袭 (next attack: 3 s arts DoT 40 %/55 % ATK per s, not stacking, +2 DP); S3 独影归途 (passive: the
// first deployment leaves a 影哨 and retreats with the redeploy timer refreshed; every deployment: ATK up for
// the skill duration, the sentry is recalled through ≤ 4/5 enemies for 110 %/140 % phys, +1 DP per damage)
chess_char_4_04_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid);
const step = num(bb['attack@steal_atk_speed'], 5), maxSteal = num(bb['attack@steal_atk_speed_max'], 50);
const S3 = isSel(def, 'skchr_ines_3');
const clearAspd = (battle, unit) => {
for (const id of unit.mem.inesAspdVictims || []) { const e = battle.unitById(id); if (e) battle.removeBuff(e, `ines:aspd:${unit.id}`); }
unit.mem.inesAspdVictims = [];
unit.mem.inesAspd = 0;
battle.removeBuff(unit, 'ines:aspdGain');
};
const s3Dur = num(def.skill?.duration, 11);
return {
skills: alt(def, {
skchr_ines_1: () => ({
kind: instantKind(def),
attack: {
onHit({ battle, unit, target }) {
battle.addDp(unit.ownerId, num(bb.cost, 2));
if (!target || !target.alive || target.side !== 'enemy') return;
const scale = num(bb.bleed_atk_scale, 0.4);
// (不叠加): a new DoT replaces the running one
battle.addBuff(target, { key: `ines:bleed:${unit.id}`, duration: num(bb.bleed_duration, 3) + 1e-6, interval: 1, source: unit, visible: true, refresh: 'extend',
onTick: ({ unit: t }) => battle.dealDamage(unit, t, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'dot'] }) });
},
},
}),
skchr_ines_3: () => ({
kind: 'passive',
onStart({ battle, unit }) {
if (!unit.mem.inesS3Placed) {
// 首次部署: place a 影哨 (her talent's sentry, left on retreat) and leave; 立刻刷新再部署时间 — the redeploy
// (auto, paying her DP cost like every redeploy) follows as soon as it is affordable
unit.mem.inesS3Placed = true;
battle.after(0, () => {
if (!unit.alive || !unit.deployed) return;
battle.retreat(unit, { reason: 'retreat' });
unit.respawnAt = battle.time;
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
return;
}
battle.addBuff(unit, { key: 'ines:s3', duration: s3Dur, mods: { atkPct: num(bb.atk) }, visible: true });
// 立刻收回影哨: the sentry flies back to her and hits ≤ max_target enemies on its way (within
// projectile_range of the segment, [ASSUMED] nearest to its start first)
const from = unit.mem.inesSentryAt;
if (!unit.mem.sentry || !from) return;
unit.mem.sentry = null;
unit.mem.inesSentryAt = null;
const ax = from.x, ay = from.y, bx = unit.x, by = unit.y;
const L2 = (bx - ax) ** 2 + (by - ay) ** 2;
const w = num(bb.projectile_range, 1.4);
const hits = [];
for (const e of battle.aliveEnemies()) {
if (!canTargetEnemy(unit, e, { canHitFly: true })) continue;
const t = L2 > 1e-9 ? Math.max(0, Math.min(1, ((e.x - ax) * (bx - ax) + (e.y - ay) * (by - ay)) / L2)) : 0;
const d = bodyDist(e, ax + t * (bx - ax), ay + t * (by - ay));
if (d <= w + 1e-9) hits.push({ e, t, d });
}
hits.sort((a, b) => a.t - b.t || a.d - b.d || a.e.id - b.e.id);
const amount = unit.s.atk * num(bb.atk_scale, 1.1); // one projectile: ATK taken when it flies back
for (const { e } of hits.slice(0, Math.max(1, Math.floor(num(bb.max_target, 4))))) {
battle.dealDamage(unit, e, { amount, type: 'phys', isSkill: true, tags: ['skill', 'sentryRecall'] });
}
battle.fx('beam', { x: ax, y: ay, tx: bx, ty: by, id: unit.id });
},
}),
}),
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
flags: { stealth: true },
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { clearAspd(battle, unit); battle.fx('stealth', { x: unit.x, y: unit.y, id: unit.id }); },
onAttack({ battle, unit }) { battle.addDp(unit.ownerId, num(bb.cost, 1)); },
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.side !== 'enemy') return;
const room = maxSteal - (unit.mem.inesAspd ?? 0);
if (room <= 0) return;
const s = Math.min(step, room);
unit.mem.inesAspd = (unit.mem.inesAspd ?? 0) + s;
const key = `ines:aspd:${unit.id}`;
const cur = target.findBuff(key);
battle.addBuff(target, { key, mods: { aspd: (cur?.mods?.aspd ?? 0) - s }, source: unit });
if (!(unit.mem.inesAspdVictims ||= []).includes(target.id)) unit.mem.inesAspdVictims.push(target.id);
battle.addBuff(unit, { key: 'ines:aspdGain', mods: { aspd: unit.mem.inesAspd } });
},
onEnd({ battle, unit }) { clearAspd(battle, unit); },
},
talents: [
{ install(battle, unit) { // 影织: first damage on each enemy → bind 5 s + steal 90 ATK until it dies or Ines leaves
unit.mem.woven = new Set();
unit.mem.atkVictims = [];
const refresh = () => {
unit.mem.atkVictims = unit.mem.atkVictims.filter((id) => battle.unitById(id)?.alive);
const gain = Math.min(num(t0.steal_atk_max, Infinity), num(t0.steal_atk, 90) * unit.mem.atkVictims.length);
if (unit.alive) { if (gain > 0) battle.addBuff(unit, { key: 'ines:atkGain', mods: { atkFlat: gain } }); else battle.removeBuff(unit, 'ines:atkGain'); }
};
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !unit.alive || e.side !== 'enemy' || c.type === 'element' || unit.mem.woven.has(e.id)) return;
unit.mem.woven.add(e.id);
if (!e.alive) return;
battle.applyStatus(e, 'bind', { duration: num(t0.duration, 5), source: unit });
battle.addBuff(e, { key: `ines:atkSteal:${unit.id}`, mods: { atkFlat: -num(t0.steal_atk, 90) }, source: unit });
unit.mem.atkVictims.push(e.id);
refresh();
battle.fx('shadowWeave', { x: e.x, y: e.y, id: e.id });
}, { owner: unit });
battle.on('death', (c) => {
if (c.unit.side === 'enemy' && unit.mem.atkVictims.includes(c.unit.id)) refresh();
if (c.unit !== unit) return;
for (const id of unit.mem.atkVictims) { const e = battle.unitById(id); if (e) battle.removeBuff(e, `ines:atkSteal:${unit.id}`); }
unit.mem.atkVictims = [];
unit.mem.woven = new Set();
}, { owner: unit });
} },
{ install(battle, unit) { // 影哨: reveal + −30 % move speed in range; a sentry keeps it after she leaves (max 1)
const mods = { moveMul: Math.max(0, 1 + num(t1.move_speed, -0.3)) };
battle.on('death', (c) => {
if (c.unit !== unit || c.reason === 'expired') return;
unit.mem.sentry = new Set(unit.baseRangeKeys || unit.rangeKeys || []);
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
battle.every(AURA, () => {
const set = new Set();
if (unit.alive && unit.deployed) for (const k of unit.rangeKeys || []) set.add(k);
if (unit.mem.sentry) for (const k of unit.mem.sentry) set.add(k);
if (!set.size) return;
for (const e of enemiesOnRange(battle, unit, set)) pulse(battle, e, `ines:sentry:${unit.id}`, mods, { flags: { reveal: true } });
}, { owner: unit });
} },
],
install(battle, unit) {
if (S3) {
// where the talent's 影哨 stays (every leave but an expiry), for the recall of the next deployment
battle.on('death', (c) => { if (c.unit === unit && c.reason !== 'expired') unit.mem.inesSentryAt = { x: unit.x, y: unit.y }; }, { owner: unit, priority: 5 });
// 技能期间每对一个敌人造成伤害就获得1点部署费用
battle.on('damaged', (c) => {
if (c.source !== unit || c.target.side !== 'enemy' || !(c.amount > 0) || c.type === 'element' || !unit.findBuff('ines:s3')) return;
battle.addDp(unit.ownerId, num(bb.cost, 1));
}, { owner: unit });
}
// module (elite): the first retreat's redeploy time is 35 % shorter
const rt = num(tb.respawn_time, 0);
if (!rt) return;
battle.on('death', (c) => {
if (c.unit !== unit || unit.mem.firstRetreatDone || !Number.isFinite(unit.respawnAt)) return;
unit.mem.firstRetreatDone = true;
unit.respawnAt = unit.deathAt + (unit.respawnAt - unit.deathAt) * Math.max(0, 1 + rt);
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,66 @@
// server/sim/content/kits/ops/chess_char_4_05-beewax.js — 蜜蜡 (char_344_beewax) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import {
num, tbb, grid, targetsInRange, targetsInGrid, toggleBuff, skillActive, withDefaults,
} from '../shared/tier4.js';
/**
* Free tile for a summon/device: in the rect, standable, not reserved (Battle.isReservedTile: empty, no knocked-out
* operator lying there) and not the home tile of any ally (dead ones redeploy there).
*/
function freeTile(battle, r, c, { ranged = false, ground = false } = {}) {
if (!Number.isInteger(r) || !Number.isInteger(c) || !battle.grid.inRect(r, c)) return false;
if (battle.isReservedTile(r, c)) return false;
if (!battle.grid.canStand(r, c, { ranged })) return false;
if (ground && !battle.grid.groundPassable(r, c)) return false;
for (const u of battle.allyUnits) if (!u.removed && u.homeR === r && u.homeC === c && u.kind !== 'device') return false;
return true;
}
export default withDefaults({
// ===== 蜜蜡 (phalanx, hidden) S2 守卫尖碑 — obelisk on a melee tile in range: 200 % arts burst + stun, blocks 3
chess_char_4_05_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const tokenId = def.raw?.skill?.overrideTokenKey ?? (def.tokens || [])[0] ?? 'token_10011_beewax_oblisk';
const keepDef = num(tb['soil_e_002[buff].def'], 0), keepRes = num(tb['soil_e_002[buff].magic_resistance'], 0);
const obeliskKit = { skill: null, talents: [], trait: { noAttack: true } };
return {
skill: {
kind: 'duration',
onStart({ battle, unit }) {
if (keepDef || keepRes) battle.addBuff(unit, { key: 'beewax:keep', mods: { defPct: keepDef, resFlat: keepRes } });
// melee deploy tile in range: nearest to the most advanced enemy (path tile first)
const foes = targetsInRange(battle, unit);
const ref = foes[0] || battle.aliveEnemies().filter((e) => !e.hidden).sort((a, b) => battle.remainingDistance(a) - battle.remainingDistance(b))[0] || unit;
let best = null, bd = Infinity;
for (const k of unit.rangeKeys || []) {
const r = (k / COLS) | 0, c = k % COLS;
if (!freeTile(battle, r, c)) continue;
const d = Math.hypot(c - ref.x, r - ref.y) + (battle.grid.groundPassable(r, c) ? 0 : 5);
if (d < bd - 1e-9) { bd = d; best = [r, c]; }
}
if (!best) return;
const tok = battle.tokenDef(tokenId, unit); // the owner's skill / module variant (DESIGN §16)
const dur = num(tok?.talents?.[0]?.bb?.duration, num(def.skill?.duration, 20));
const ob = battle.spawnToken(unit, tokenId, best[0], best[1], { duration: dur, kit: obeliskKit });
if (!ob) return;
unit.mem.obelisk = ob;
battle.fx('obelisk', { x: ob.x, y: ob.y, id: ob.id });
const tokGrid = grid(tok?.skill?.rangeGrid) || [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]];
for (const e of targetsInGrid(battle, ob, tokGrid)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 2), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun, 1), source: unit });
}
},
onEnd({ battle, unit }) { battle.removeBuff(unit, 'beewax:keep'); },
},
talents: [{ install(battle, unit) { // 沙原的庇护: +4 % max HP/s while the skill is off
const set = () => toggleBuff(battle, unit, 'beewax:regen', unit.alive && !skillActive(unit), { hpRegenRatio: num(t0.hp_recovery_per_sec_by_max_hp_ratio, 0.04) });
for (const ev of ['deploy', 'skillStart', 'skillEnd']) battle.on(ev, (c) => { if (c.unit === unit) set(); }, { owner: unit });
} }],
};
},
});
@@ -0,0 +1,50 @@
// server/sim/content/kits/ops/chess_char_4_06-kroos2.js — 寒芒克洛丝 (char_1021_kroos2) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { num, tbb, batFlat, skillActive, isSel, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 寒芒克洛丝 (fastshot) S2 封喉 — double shot → 4 shots after 40 hits; talent 中的 (20 % ×1.5 + 0.2 s stun)
// S1 无痕 (ATK up, double shot, 迷彩 = the engine's `camou` flag: only an enemy she blocks targets her)
chess_char_4_06_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const need = num(bb['attack@max_stack_count'], 40);
const S2 = isSel(def, 'skchr_kroos2_2');
return {
skills: alt(def, {
skchr_kroos2_1: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk) },
flags: { camou: true },
attack: { hits: 2 },
onStart({ battle, unit }) { battle.fx('camouflage', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
mods: { batPct: batFlat(def, bb.base_attack_time) },
attack: { hitsFn: (battle, unit) => ((unit.mem.kroosHits ?? 0) >= need ? 4 : 2) },
onStart({ unit }) { unit.mem.kroosHits = 0; },
},
talents: [{ install(battle, unit) {
// 中的: the ×1.5 is rolled before mitigation; the stun (and the S2 "击中目标" count) only for shots that landed
const crits = new WeakSet();
battle.on('hit', (c) => {
if (c.source !== unit || !c.dmg.isAttack || c.target.side !== 'enemy') return;
if (battle.rng.chance(num(t0.prob, 0.2))) { c.dmg.mul *= num(t0.atk_scale, 1.5); crits.add(c.dmg); }
}, { owner: unit });
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg || !c.dmg.isAttack || c.target.side !== 'enemy') return;
if (S2 && skillActive(unit)) {
unit.mem.kroosHits = (unit.mem.kroosHits ?? 0) + 1;
if (unit.mem.kroosHits === need) battle.fx('quadShot', { x: unit.x, y: unit.y, id: unit.id });
}
if (!crits.has(c.dmg)) return;
crits.delete(c.dmg);
if (c.target.alive) battle.applyStatus(c.target, 'stun', { duration: num(t0.stun, 0.2), source: unit });
battle.fx('crit', { x: c.target.x, y: c.target.y, id: c.target.id });
}, { owner: unit });
} }],
};
},
});
@@ -0,0 +1,59 @@
// server/sim/content/kits/ops/chess_char_4_07-bpipe.js — 风笛 (char_222_bpipe) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import { num, tbb, batFlat, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 风笛 (charger) S2 高效冲击 — next attack 145 % ×2 (charges); talents 精密填弹 / 军事传统
// S1 迅捷打击·γ型; S3 闭膛连发 (BAT +0.7 s, block +1, ATK/DEF up, triple hits); module CHG-Y (“棍棒与口袋”):
// attacks on enemies below 40 % HP use 115 % ATK
chess_char_4_07_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
return {
skills: alt(def, {
'skcom_quickattack[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) } }),
skchr_bpipe_3: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def), blockCnt: num(bb.block_cnt, 1), batPct: batFlat(def, bb.base_attack_time) },
attack: { hits: 3 },
onStart({ battle, unit }) { battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
install(battle, unit) {
const hr = num(tb.hp_ratio, 0), sc = num(tb.atk_scale, 0);
if (!(hr > 0) || !(sc > 0)) return;
battle.on('hit', (c) => { // 攻击力提升至115% (before mitigation)
if (c.source === unit && c.dmg.isAttack && c.target.side === 'enemy' && c.target.hpRatio < hr) c.dmg.amount *= sc;
}, { owner: unit });
},
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
attack: { atkScale: num(bb.atk_scale, 1.45), hits: 2 },
onStart({ battle, unit }) { battle.fx('charge', { x: unit.x, y: unit.y, id: unit.id }); },
},
talents: [
{ install(battle, unit) { // 精密填弹: 25 % → ×1.3 and one extra target
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit) return;
unit.mem.bpBoost = battle.rng.chance(num(t0.prob, 0.25));
if (!unit.mem.bpBoost) return;
const extra = battle.enemiesInKeys(unit.rangeKeys, unit, c.profile).filter((e) => !c.targets.includes(e));
sortEnemyTargets(battle, unit, extra, c.profile?.priority ?? null);
if (extra[0]) c.targets = [...c.targets, extra[0]];
}, { owner: unit });
battle.on('hit', (c) => { if (c.source === unit && c.dmg.isAttack && unit.mem.bpBoost) c.dmg.mul *= num(t0.atk_scale, 1.3); }, { owner: unit });
battle.on('attack', (c) => { if (c.attacker === unit) unit.mem.bpBoost = false; }, { owner: unit });
} },
{ install(battle, unit) { // 军事传统: every 先锋 of the team starts with +6 SP
battle.on('deploy', (c) => {
const u = c.unit;
if (u.side !== 'ally' || u.kind !== 'op' || u.ownerId !== unit.ownerId || u.def?.profession !== 'PIONEER' || !u.skill || (u.skill.active && u.skill.isTimed)) return;
u.skill.gainSp(num(t1.sp, 6), 'init');
}, { owner: unit });
} },
],
};
},
});
@@ -0,0 +1,47 @@
// server/sim/content/kits/ops/chess_char_4_08-rosesa.js — 瑰盐 (char_4163_rosesa) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import {
AURA, num, tbb, batFlat, keySet, whileDeployed, pulse, skillActive, applyModuleRange, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 瑰盐 (ringhealer, hidden) S2 绝妙的长效药呀 — allies in range: 20 % of phys/arts damage → 5 s HP loss
chess_char_4_08_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const scale = num(bb['attack@damage_scale'], 0.8), fin = num(bb['attack@final_duration'], 5), iv = Math.max(0.1, num(bb['attack@interval'], 1));
return {
skill: { kind: 'duration', heal: true, mods: { batPct: batFlat(def, bb.base_attack_time) },
onStart({ battle, unit }) { battle.fx('saltWard', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [{ install(battle, unit) { // 最好的草药医生: own ATK −5 %, operators in range receive +15 % healing
battle.addBuff(unit, { key: 'rosesa:t1', mods: { atkPct: num(t0.atk, -0.05) }, persist: true, allowDead: true });
whileDeployed(battle, unit, AURA, () => {
for (const a of battle.alliesInGrid(unit)) if (a.kind === 'op') pulse(battle, a, `rosesa:heal:${unit.id}`, { healingTakenMul: num(t0.heal_scale, 1.15) });
});
} }],
install(battle, unit) {
const deferred = new WeakSet();
battle.on('hit', (c) => {
const t = c.target;
// "友方干员": operators only (summons/devices keep the full hit)
if (!skillActive(unit) || !unit.alive || t.side !== 'ally' || t.kind !== 'op' || (c.dmg.type !== 'phys' && c.dmg.type !== 'arts')) return;
if (!keySet(unit).has(t.tileR * COLS + t.tileC)) return;
c.dmg.mul *= scale;
deferred.add(c.dmg);
}, { owner: unit, priority: -5 });
battle.on('damaged', (c) => {
if (!deferred.has(c.dmg) || !(c.amount > 0) || !c.target.alive) return;
deferred.delete(c.dmg);
const total = (c.amount * (1 - scale)) / Math.max(1e-6, scale);
const per = total / Math.max(1, Math.round(fin / iv));
const src = c.source;
battle.addBuff(c.target, { key: 'rosesa:dot', refresh: 'independent', maxStacks: 1000, duration: fin + 1e-6, interval: iv,
onTick: ({ unit: tgt }) => battle.loseHp(tgt, per, { source: src && src.side === 'enemy' ? src : null }) });
}, { owner: unit });
// module (elite) RIN-X: 攻击范围扩大 (module range grid)
applyModuleRange(battle, unit, def);
},
};
},
});
@@ -0,0 +1,62 @@
// server/sim/content/kits/ops/chess_char_4_09-mizuki.js — 水月 (char_437_mizuki) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, moduleBb, grid, batFlat, enemiesOnRange, whileDeployed, pulse, toggleBuff, skillActive, isSel, alt,
instantKind, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 水月 (stalker) S2 囚徒困境 — faster, +ATK, talent 1 +1 target & bind; talents 创伤性癔症 / 反移情; module slow
// S1 唤醒 (charges: next attack 200 %/230 %, talent 1 ×2.0/×2.3); S3 镜花水月 (wider range, ATK up, talent 1 +2
// targets & stun; an attack hitting < 3 enemies costs 15 % max HP); modules: AMB-Y 65 % dodge (trait, profession)
chess_char_4_09_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const S1 = isSel(def, 'skchr_mizuki_1'), S2 = isSel(def, 'skchr_mizuki_2'), S3 = isSel(def, 'skchr_mizuki_3');
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
skchr_mizuki_1: () => ({ kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 2) } }),
skchr_mizuki_3: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk) },
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { battle.fx('ripple', { x: unit.x, y: unit.y, id: unit.id }); },
onAttack({ battle, unit, targets }) {
if ((targets || []).filter((e) => e.side === 'enemy').length >= 3) return;
battle.loseHp(unit, unit.s.maxHp * num(bb['attack@hp_ratio'], 0.15), { tags: ['skill', 'selfLoss'] }); // (no source: not damage dealt)
},
}),
}),
skill: { kind: 'duration', mods: { batPct: batFlat(def, bb.base_attack_time), atkPct: num(bb.atk) },
onStart({ battle, unit }) { battle.fx('dilemma', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [
{ install(battle, unit) { // 创伤性癔症: +50 % ATK arts to the lowest-HP enemy hit (S2 +1 target & bind, S3 +2 & stun, S1 ×2)
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.alive) return;
const act = skillActive(unit) && (S2 || S3);
const n = Math.max(1, Math.floor(num(t0['attack@max_target'], 1))) + (act ? Math.floor(num(bb['attack@max_target'], 1)) : 0);
const mul = S1 && c.isSkill ? num(bb.talent_scale, 2) : 1;
const list = c.targets.filter((e) => e.alive && e.side === 'enemy').sort((a, b) => a.hp - b.hp || a.id - b.id).slice(0, n);
for (const e of list) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(t0['attack@mizuki_t_1.atk_scale'], 0.5) * mul, type: 'arts', tags: ['talent'] });
if (!act || !e.alive) continue;
if (S2) battle.applyStatus(e, 'bind', { duration: num(bb['attack@unmovable'], 0.8), source: unit });
else battle.applyStatus(e, 'stun', { duration: num(bb['attack@stun'], 0.7), source: unit });
}
}, { owner: unit });
} },
{ install(battle, unit) { // 反移情: ATK +10 % while an enemy in range is below 50 % HP
whileDeployed(battle, unit, 0.1, () => {
const on = enemiesOnRange(battle, unit).some((e) => e.hpRatio < num(t1.hp_ratio, 0.5));
toggleBuff(battle, unit, 'mizuki:t2', on, { atkPct: num(t1.atk, 0.1) });
});
} },
],
install(battle, unit) {
const ms = num(mb.move_speed, 0);
if (ms) whileDeployed(battle, unit, AURA, () => { for (const e of enemiesOnRange(battle, unit)) pulse(battle, e, `mizuki:slow:${unit.id}`, { moveMul: Math.max(0, 1 + ms) }); });
},
};
},
});
@@ -0,0 +1,81 @@
// server/sim/content/kits/ops/chess_char_4_10-aroma.js — 阿罗玛 (char_446_aroma) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyInKeys } from '../../../body.js';
import { num, tbb, keySet, skillActive, isSel, alt, instantKind, withDefaults } from '../shared/tier4.js';
/** 阿罗玛's 非首次标记 (talent 起泡性能测试): one per enemy, shared by every 阿罗玛 (PRTS 备注). */
const AROMA_MARK = 'aroma:bubbled';
export default withDefaults({
// ===== 阿罗玛 (blastcaster) S2 小心地滑 — +ATK, levitated enemies take 65 % ATK arts on landing; talent 起泡性能测试
// S1 强效清洁 (charges: next attack 150 %/180 % arts, flying victims +55 %/70 % ATK arts)
chess_char_4_10_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const land = num(bb['attack@atk_scale_when_fly_finish'], 0.65);
const S2 = isSel(def, 'skchr_aroma_2');
return {
skills: alt(def, {
skchr_aroma_1: () => ({
kind: instantKind(def),
attack: {
atkScale: num(bb.atk_scale, 1.5),
onEachHit({ battle, unit, target }) { // every enemy of her line the blast strikes that flies
if (target && target.alive && target.isFlying) battle.dealDamage(unit, target, { amount: unit.s.atk * num(bb.atk_scale_to_fly, 0.55), type: 'arts', isSkill: true, tags: ['skill', 'antiAir'] });
},
},
}),
}),
skill: { kind: 'duration', mods: { atkPct: num(bb.atk) }, onStart({ battle, unit }) { battle.fx('slippery', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [{ install(battle, unit) { // first attack on each enemy: ×1.1 and levitate 2.5 s
// PRTS 阿罗玛 备注: "天赋采用施加非首次标记的方式判断是否为'首次进行攻击',自身离场时移除自身已施加的标记,不同阿罗玛之间的
// 非首次标记通用" — one mark per enemy (AROMA_MARK, the setter as source) for every 阿罗玛; hers go when she leaves
// the field (until 0.1.1 each 阿罗玛 kept her own list, so two of them both triggered on one enemy)
unit.mem.bubbled = new Set(); // the enemies she marked
battle.on('hit', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg.isAttack || e.side !== 'enemy' || e.findBuff(AROMA_MARK)) return;
battle.addBuff(e, { key: AROMA_MARK, source: unit });
unit.mem.bubbled.add(e.id);
c.dmg.mul *= num(t0.damage_scale, 1.1);
if (battle.applyStatus(e, 'levitate', { duration: num(t0.levitate_duration, 2.5), source: unit })) battle.fx('levitate', { x: e.x, y: e.y, id: e.id });
}, { owner: unit });
battle.on('death', (c) => {
if (c.unit !== unit) return;
for (const id of unit.mem.bubbled) {
const e = battle.unitById(id);
if (e && e.findBuff(AROMA_MARK)?.source === unit) battle.removeBuff(e, AROMA_MARK);
}
unit.mem.bubbled.clear();
}, { owner: unit });
} }],
install(battle, unit) {
// S2: landing damage for every levitated enemy (any source) that lands inside her range while the skill runs
const floating = new Map();
battle.on('statusApplied', (c) => { if (c.status === 'levitate' && c.target.side === 'enemy') floating.set(c.target.id, c.target); }, { owner: unit });
battle.every(1 / 30, () => {
for (const [id, e] of floating) {
if (!e.alive) { floating.delete(id); continue; }
if (e.findBuff('levitate')) continue;
floating.delete(id);
if (S2 && unit.alive && unit.deployed && skillActive(unit) && bodyInKeys(e, keySet(unit))) {
battle.dealDamage(unit, e, { amount: unit.s.atk * land, type: 'arts', isSkill: true, tags: ['skill', 'landing'] });
battle.fx('splash', { x: e.x, y: e.y, id: e.id });
}
}
}, { owner: unit });
// module (elite) trait: farther targets take more damage (up to ×1.1)
const ds = num(tb.damage_scale, 0);
if (ds > 0) {
const lo = num(tb.min_dist, 0), hi = num(tb.max_dist, 4);
battle.on('hit', (c) => {
if (c.source !== unit || !c.dmg.isAttack) return;
const d = Math.hypot(c.target.x - unit.x, c.target.y - unit.y);
c.dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo)));
}, { owner: unit });
}
},
};
},
});
@@ -0,0 +1,38 @@
// server/sim/content/kits/ops/chess_char_4_11-cathy.js — 凯瑟琳 (char_4162_cathy) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { CAT_SHIELD_KEY } from '../../tokens.js';
import { AURA, num, whileDeployed, pulse, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 凯瑟琳 (craftsman) S2 战火淬炼 — stops attacking, HP/DEF up, devices give 6 %/s shields; talent 定向支援信号
// "携带3个支援装置(最多部署2个)": the 爬行号·防护单元 are hand pieces (2 = the deploy limit) the player places and
// turns towards an operator (user playtest #6, PRTS 卫戍协议/帮助 §战斗部署); they deploy with the board and the
// token kit (tokens.js catShield) gives the shields — S2 overwrite_ratio per second while this skill runs — so
// this kit places nothing (none placed ⇒ no device).
// S1 岁月锻打 (passive: she and every other operator holding a device shield ATK/DEF +8 %/+11 %); module CRA-X
// carries one more device (talent cnt, data — a spare the hand never shows)
chess_char_4_11_a: (bb, chess, def) => ({
skills: alt(def, {
skchr_cathy_1: () => ({
kind: 'passive',
mods: { atkPct: num(bb.s1_atk), defPct: num(bb.s1_def) },
onStart({ battle, unit }) {
unit.mem.forgeAura?.cancel();
unit.mem.forgeAura = whileDeployed(battle, unit, AURA, () => {
for (const a of battle.allies(unit.ownerId)) {
if (a === unit || a.kind !== 'op' || !a.findBuff(CAT_SHIELD_KEY)) continue;
pulse(battle, a, `cathy:forge:${unit.id}`, { atkPct: num(bb.s1_atk), defPct: num(bb.s1_def) });
}
});
},
}),
}),
skill: {
kind: 'duration',
mods: { hpPct: num(bb.max_hp), defPct: num(bb.def) },
attack: { noAttack: true },
onStart({ battle, unit }) { battle.fx('overclock', { x: unit.x, y: unit.y, id: unit.id }); },
},
}),
});
@@ -0,0 +1,100 @@
// server/sim/content/kits/ops/chess_char_4_12-glady.js — 歌蕾蒂娅 (char_474_glady) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyDist } from '../../../body.js';
import {
AURA, num, tbb, moduleBb, grid, batFlat, enemyHasTag, targetsInRange, whileDeployed, pulse, alt, instantKind,
pullToFront, withDefaults,
} from '../shared/tier4.js';
const ABYSSAL = new Set(['char_143_ghost', 'char_263_skadi', 'char_474_glady', 'char_4145_ulpia', 'char_1023_ghost2']);
const isAbyssal = (u) => u?.def?.raw?.groupId === 'abyssal' || ABYSSAL.has(u?.def?.charId ?? u?.def?.raw?.charId);
export default withDefaults({
// ===== 歌蕾蒂娅 (hookmaster) S3 缺水的碎漩狂舞 — bind a far target, tornado: slow, 85 % arts pulses + pull, final pull
// S1 缺水的大洋裂断 (charges: next attack pulls the target to her front, 150 %/180 %); S2 缺水的掌握怒海 (BAT +0.5 s,
// wider range, ≤ 2 targets — blocked first — at 135 %/150 % and pulled to her front); module HOK-Y (淡金坠饰):
// a pull towards herself of an enemy farther than 2.5 tiles is one force level stronger
chess_char_4_12_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const tb = def.traitBb || {};
const R = 1.5; // [ASSUMED] tornado radius (no blackboard key)
const force = num(bb.force, num(bb['attack@force'], 0));
const iv = Math.max(0.1, num(bb.interval, 1.5));
const g = grid(def.skill?.rangeGrid);
const dragDmg = (battle, unit, e, moved) => { // module: dragged enemies take arts damage ∝ distance
if (!(moved > 0) || !(num(tb.value, 0) > 0) || !e.alive) return;
battle.dealDamage(unit, e, { amount: num(tb.value) * moved / Math.max(1e-6, num(tb.dist, 1)), type: 'arts', isSkill: true, tags: ['drag'] });
};
// module HOK-Y: "向自身拖拽较远的敌人时力度提升一个等级"
const farR = num(mb['skill@range_radius'], num(mb['attack@range_radius'], 0)), farF = num(mb['skill@delta_force'], num(mb['attack@delta_force'], 0));
const selfForce = (unit, e, f) => (farR > 0 && farF > 0 && Math.hypot(e.x - unit.x, e.y - unit.y) > farR + 1e-9 ? f + farF : f);
const pullSelf = (battle, unit, e, f) => { if (e && e.alive) dragDmg(battle, unit, e, pullToFront(battle, unit, e, selfForce(unit, e, f))); };
return {
skills: alt(def, {
skchr_glady_1: () => ({
kind: instantKind(def),
attack: { atkScale: num(bb.atk_scale, 1.5), onHit({ battle, unit, target }) { pullSelf(battle, unit, target, num(bb.force, 1)); } },
}),
skchr_glady_2: () => ({
kind: 'duration',
mods: { batPct: batFlat(def, bb.base_attack_time) },
// (target sorting already puts the enemies she blocks first)
targeting: { ...(g ? { rangeGrid: g } : {}), maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 2))) },
attack: { atkScale: num(bb['attack@atk_scale'], 1.35), onEachHit({ battle, unit, target }) { pullSelf(battle, unit, target, num(bb['attack@force'], 1)); } },
onStart({ battle, unit }) { battle.fx('wake', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
onStart({ battle, unit, skill }) {
const foes = targetsInRange(battle, unit).sort((a, b) => bodyDist(b, unit.x, unit.y) - bodyDist(a, unit.x, unit.y) || a.id - b.id);
const t = foes[0];
unit.mem.tornado = t ? { x: t.x, y: t.y, acc: 0 } : null;
if (!t) return;
battle.applyStatus(t, 'bind', { duration: skill.duration, source: unit });
battle.fx('tornado', { x: t.x, y: t.y, id: unit.id, r: R, duration: skill.duration });
},
onTick({ battle, unit, dt }) {
const T = unit.mem.tornado;
if (!T) return;
const inside = battle.foesInRadius(T.x, T.y, R);
for (const e of inside) pulse(battle, e, `glady:slow:${unit.id}`, { moveMul: Math.max(0, 1 + num(bb.move_speed, -0.5)) });
T.acc += dt;
if (T.acc + 1e-9 < iv) return;
T.acc -= iv;
for (const e of inside) {
if (!e.alive || e.s.flags.untargetable) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 0.85), type: 'arts', isSkill: true, tags: ['skill', 'tornado'] });
// "小力地拖拽至中心": a pull to the marked point (PRTS 推与拉: its 6 tornado pulls stop 0.05 from it)
if (e.alive) dragDmg(battle, unit, e, battle.pull(e, force, { to: T, stop: 0.05 }));
}
battle.fx('tornadoPulse', { x: T.x, y: T.y, id: unit.id, r: R });
},
onEnd({ battle, unit, reason }) {
const T = unit.mem.tornado;
unit.mem.tornado = null;
if (!T || reason === 'death' || !unit.alive) return;
for (const e of battle.foesInRadius(T.x, T.y, R)) pullSelf(battle, unit, e, force);
battle.fx('pull', { x: T.x, y: T.y, id: unit.id });
},
},
talents: [
{ install(battle, unit) { // 阿戈尔的波涛: 深海猎人 regen 2.5 %/s, −25 % phys/arts damage from 海怪
whileDeployed(battle, unit, AURA, () => {
for (const a of battle.allies(unit.ownerId)) if (isAbyssal(a)) pulse(battle, a, 'glady:tide', { hpRegenRatio: num(t0.hp_recovery_per_sec_by_max_hp_ratio, 0.025) });
});
battle.on('hit', (c) => {
if (!c.target.findBuff?.('glady:tide') || !enemyHasTag(c.source, 'seamonster') || (c.dmg.type !== 'phys' && c.dmg.type !== 'arts')) return;
c.dmg.mul *= 1 - num(t0.damage_resistance, 0.25);
}, { owner: unit });
} },
{ install(battle, unit) { // 弱肉强食: ×1.3 against enemies of weight ≤ 3
battle.on('hit', (c) => {
if (c.source === unit && c.dmg.isAttack && c.target.side === 'enemy' && c.target.s.massLevel <= num(t1.value, 3)) c.dmg.mul *= num(t1.atk_scale, 1.3);
}, { owner: unit });
} },
],
};
},
});
@@ -0,0 +1,112 @@
// server/sim/content/kits/ops/chess_char_4_13-gnosis.js — 灵知 (char_206_gnosis) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import {
AURA, AURA_DUR, num, tbb, nationOf, enemiesOnRange, targetsInRange, whileDeployed, toggleBuff, skillActive, isSel,
alt, instantKind, withDefaults,
} from '../shared/tier4.js';
/** Enemy leaders / elites (精英或领袖敌人). */
const isEliteEnemy = (e) => !!e && (e.isBoss || e.def?.rank === 'ELITE' || e.def?.rank === 'BOSS');
export default withDefaults({
// ===== 灵知 (underminer) S2 零度爆发 — charges: 130 % arts + cold to all in range, charged ⇒ 2nd cold; 坚冰 / 殊途同归
// S1 高速思考 (next attack: 2 × 135 %/150 % arts); S3 失温症 (ASPD up, 2 targets — unfrozen first; frozen enemies
// in range stay frozen until the skill ends, then take 300 %/400 % arts and thaw); module UMD-Y (一号项目模型):
// +0.25 SP/s while an elite / leader enemy is in range
chess_char_4_13_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const S3 = isSel(def, 'skchr_gnosis_3');
const frozen = (e) => !!e.findBuff('freeze');
return {
skills: alt(def, {
skchr_gnosis_1: () => ({ kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 1.35), hits: 2 } }),
skchr_gnosis_3: () => ({
kind: 'duration',
mods: { aspd: num(bb.attack_speed) },
targeting: { maxTargets: Math.max(1, Math.floor(num(bb.max_target, 2))) },
onStart({ battle, unit }) { unit.mem.hypothermia = new Set(); battle.fx('coldWind', { x: unit.x, y: unit.y, id: unit.id }); },
onTick({ battle, unit, skill }) {
const H = unit.mem.hypothermia;
if (!H) return;
// 范围内所有敌人的冻结延长至技能结束
for (const e of enemiesOnRange(battle, unit)) {
const f = e.findBuff('freeze');
if (!f) continue;
H.add(e.id);
if (f.timeLeft < skill.timeLeft) { f.timeLeft = skill.timeLeft; f.duration = Math.max(f.duration, skill.timeLeft); }
}
},
onEnd({ battle, unit, reason }) {
const H = unit.mem.hypothermia;
unit.mem.hypothermia = null;
if (!H || reason === 'death' || !unit.alive) return;
// 技能结束时对所有冻结的敌人造成N%的法术伤害并结束冻结 (the frozen enemies of her range)
const list = enemiesOnRange(battle, unit).filter(frozen);
for (const id of H) { const e = battle.unitById(id); if (e && e.alive && frozen(e) && !list.includes(e)) list.push(e); }
for (const e of list) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 3), type: 'arts', isSkill: true, tags: ['skill', 'shatter'] });
if (e.alive) battle.removeStatus(e, 'freeze');
}
battle.fx('iceSpike', { x: unit.x, y: unit.y, id: unit.id, n: list.length });
},
}),
}),
install(battle, unit) {
if (S3) {
// 优先攻击未冻结的单位 (while S3 runs)
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillActive(unit)) return;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, c.profile);
for (const e of battle.blockedTargets(unit, c.profile)) if (!cands.includes(e)) cands.push(e); // DESIGN §20.3
if (cands.length <= 1) return;
sortEnemyTargets(battle, unit, cands, c.profile?.priority ?? null);
const n = Math.max(1, Math.floor((c.profile?.maxTargets || 1) + unit.s.maxTargets));
c.targets = [...cands.filter((e) => !frozen(e)), ...cands.filter(frozen)].slice(0, n);
}, { owner: unit, priority: 5 });
}
const sp = num(tb.sp_recovery_per_sec, 0); // module UMD-Y
if (sp > 0) whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'gnosis:module', enemiesOnRange(battle, unit).some(isEliteEnemy), { spRecoveryFlat: sp }));
},
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
onStart({ battle, unit, skill }) {
const charged = skill.maxCharges > 1 && skill.charges + 1 >= skill.maxCharges; // cast with every charge stored
for (const e of targetsInRange(battle, unit)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.3), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
if (!e.alive) continue;
battle.applyStatus(e, 'cold', { duration: num(bb.cold, 2.5), source: unit });
if (charged && e.alive) battle.applyStatus(e, 'cold', { duration: num(bb.cold, 2.5), source: unit });
}
battle.fx('frostNova', { x: unit.x, y: unit.y, id: unit.id, charged });
},
},
talents: [
{ install(battle, unit) { // 坚冰: attacks chill 1 s; cold enemies in range fragile 25 %, frozen 50 %
battle.on('damaged', (c) => {
if (c.source === unit && c.dmg?.isAttack && !c.dmg.isSplash && c.target.side === 'enemy' && c.target.alive) battle.applyStatus(c.target, 'cold', { duration: num(t0.cold, 1), source: unit });
}, { owner: unit });
whileDeployed(battle, unit, 0.1, () => {
for (const e of enemiesOnRange(battle, unit)) {
const f = e.s.flags;
const m = f.freeze ? num(t0.damage_scale_freeze, 1.5) : f.cold ? num(t0.damage_scale_cold, 1.25) : 0;
// 同名效果取最高: one 坚冰 per enemy, the strongest — two 灵知 (a pair / 联防 partner's copy) never compound
if (m) battle.applyStrongest(e, 'gnosis:fragile', { duration: 0.15, value: m, mods: (v) => ({ dmgTakenMul: v }), source: unit });
}
});
} },
{ install(battle, unit) { // 殊途同归: 10 s after deployment every 谢拉格 operator gains 抵抗 (negative status durations ×0.5)
// engine `resist` status (applied before a new status lands, so a longer one already running is never
// shortened; 同名效果不叠加: several 灵知 — or 流明's 抵抗 — never compound)
const value = Math.min(1, Math.max(0, -num(t1.one_minus_status_resistance, -0.5)));
whileDeployed(battle, unit, AURA, () => {
if (battle.time - unit.deployedAt < num(t1.interval, 10) - 1e-9 || !(value > 0)) return;
for (const a of battle.allies(unit.ownerId)) if (a.kind === 'op' && nationOf(a) === 'kjerag') battle.applyStatus(a, 'resist', { duration: AURA_DUR, value, source: unit });
});
} },
],
};
},
});
@@ -0,0 +1,37 @@
// server/sim/content/kits/ops/chess_char_4_14-lionhd.js — 莱恩哈特 (char_373_lionhd) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
num, tbb, grid, targetsInRange, whileDeployed, toggleBuff, resCut, alt, applyModuleRange, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 莱恩哈特 (splashcaster) S2 解构与爆破 — charges: 170 % arts to all in the wider range + RES −8 % for 6 s; 破片杀伤
chess_char_4_14_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const g = grid(def.skill?.rangeGrid);
const mr = num(bb.magic_resistance, 0);
return {
skills: alt(def, { 'skcom_atk_up[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk) } }) }), // S1 攻击力强化·γ型
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) {
for (const e of targetsInRange(battle, unit)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.7), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
// 同名效果取最高: one RES cut per enemy (the strongest), never one per 莱恩哈特
if (e.alive && mr) battle.applyStrongest(e, 'lionhd:res', { duration: num(bb.duration, 6), value: mr, mods: resCut, source: unit });
}
battle.fx('explosion', { x: unit.x, y: unit.y, id: unit.id });
},
},
talents: [{ install(battle, unit) { // 破片杀伤: ATK +4 % per enemy in range (≤ 5)
whileDeployed(battle, unit, 0.1, () => {
const n = Math.min(num(t0.max_valid_stack_cnt, 5), targetsInRange(battle, unit).length);
toggleBuff(battle, unit, 'lionhd:t1', n > 0, { atkPct: num(t0.atk, 0.04) * n });
});
} }],
install(battle, unit) { applyModuleRange(battle, unit, def); }, // module SPC-X (elite default): 攻击范围扩大
};
},
});
@@ -0,0 +1,41 @@
// server/sim/content/kits/ops/chess_char_4_15-reckpr.js — 录武官 (char_4196_reckpr) kit, tier 4 (hidden).
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { isHpLoss } from '../../../damage.js';
import { num, tbb, keySet, skillActive, installLowHpHealBonus, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 录武官 (physician, hidden) S2 一点关窍 — +ATK; healed allies regain 80 HP per hit taken for 10 s; 学成于聚
chess_char_4_15_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
return {
skill: {
kind: 'duration', heal: true,
mods: { atkPct: num(bb.atk) },
onStart({ battle, unit }) { battle.fx('knack', { x: unit.x, y: unit.y, id: unit.id }); },
onHit({ battle, unit, target }) {
if (target && target.alive && target.side === 'ally' && target.kind === 'op') battle.addBuff(target, { key: `reckpr:guard:${unit.id}`, duration: num(bb['attack@buff_duration'], 10), visible: true, source: unit });
},
},
talents: [{ install(battle, unit) { // 学成于聚: an operator in range casts ⇒ +1 SP and ASPD +16 for 8 s
battle.on('skillStart', (c) => {
const a = c.unit;
if (a === unit || a.side !== 'ally' || a.kind !== 'op' || !unit.alive || !unit.deployed || !keySet(unit).has(a.tileR * COLS + a.tileC)) return;
if (!battle.rng.chance(num(t0.prob, 1))) return;
if (!skillActive(unit)) unit.skill?.gainSp(num(t0.sp, 1), 'talent');
battle.addBuff(unit, { key: 'reckpr:t1', duration: num(t0.duration, 8), mods: { aspd: num(t0.attack_speed, 16) }, maxStacks: Math.max(1, num(t0.max_stack_cnt, 1)) });
}, { owner: unit });
} }],
install(battle, unit) {
battle.on('damaged', (c) => {
const t = c.target;
if (t.side !== 'ally' || !t.alive || !(c.amount > 0) || isHpLoss(c.dmg) || !t.findBuff(`reckpr:guard:${unit.id}`)) return;
battle.heal(unit, t, num(bb['attack@fixed_heal_value'], 80));
}, { owner: unit });
installLowHpHealBonus(battle, unit, tb);
},
};
},
});
@@ -0,0 +1,115 @@
// server/sim/content/kits/ops/chess_char_4_16-texas2.js — 缄默德克萨斯 (char_1028_texas2) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import {
AURA, num, tbb, grid, targetsInGrid, whileDeployed, toggleBuff, resCut, isSel, alt, lonely, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 缄默德克萨斯 (executor) S3 剑雨滂沱 (passive) — deploy burst 2×115 % + 1.5 s stun, sword rain 1/s; talents
// S1 细雨无声 (passive, for the skill duration: ATK up; hits silence the target 5 s/8 s — 失去特殊能力 — with
// 260/320 arts per s meanwhile); S2 阵雨连绵 (passive: deploy burst 150 %/180 % arts + RES −15 %/−20 % around her;
// for the skill duration ATK up and attacks become arts double hits). Talent 德克萨斯传统 applies to every passive.
// The S3 rain hits air units (PRTS 备注 "效果可对空"); so does the S2 deploy burst [ASSUMED: no note].
chess_char_4_16_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid) || [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]];
const S1 = isSel(def, 'skchr_texas2_1'), S2 = isSel(def, 'skchr_texas2_2');
const dur = num(def.skill?.duration, S1 ? 11 : S2 ? 8 : 6);
const talentAtk = (battle, unit) => battle.addBuff(unit, { key: 'texas2:rainAtk', duration: dur, mods: { atkPct: num(t0.atk, 0.2) } });
const castS1 = (battle, unit) => {
if (!unit.alive || !unit.deployed) return;
talentAtk(battle, unit);
battle.addBuff(unit, { key: 'texas2:drizzle', duration: dur, mods: { atkPct: num(bb.atk) }, visible: true });
battle.fx('swordRain', { x: unit.x, y: unit.y, id: unit.id });
};
const castS2 = (battle, unit) => {
if (!unit.alive || !unit.deployed) return;
talentAtk(battle, unit);
battle.addBuff(unit, { key: 'texas2:shower', duration: dur, mods: { atkPct: num(bb.atk) }, visible: true });
const mr = num(bb.magic_resistance, 0);
for (const e of targetsInGrid(battle, unit, g)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.5), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
// 同名效果取最高: one RES cut per enemy (the strongest), never one per 缄默德克萨斯
if (e.alive && mr) battle.applyStrongest(e, 'texas2:resDown', { duration: num(bb.debuff_duration, 8), value: mr, mods: resCut, source: unit });
}
battle.fx('swordStorm', { x: unit.x, y: unit.y, id: unit.id });
};
const castS3 = (battle, unit) => {
if (!unit.alive || !unit.deployed) return;
talentAtk(battle, unit);
for (const e of targetsInGrid(battle, unit, g)) {
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['appear.atk_scale'], 1.15), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb['appear.stun'], 1.5), source: unit });
}
battle.fx('swordStorm', { x: unit.x, y: unit.y, id: unit.id });
unit.mem.rainUntil = battle.time + dur;
unit.mem.rainTimer?.cancel();
unit.mem.rainTimer = battle.every(Math.max(0.1, num(bb['texas2_s_3[sword].interval'], 1)), (b, sched) => {
if (!unit.alive || !unit.deployed || b.time > unit.mem.rainUntil + 1e-9) { sched.cancel(); return; }
const list = targetsInGrid(b, unit, g);
sortEnemyTargets(b, unit, list, null);
for (const e of list.slice(0, Math.max(1, Math.floor(num(bb.max_target, 2))))) {
b.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 0.85), type: 'arts', isSkill: true, tags: ['skill', 'swordRain'] });
if (e.alive) b.applyStatus(e, 'stun', { duration: num(bb.stun, 0.2), source: unit });
b.fx('swordRain', { x: e.x, y: e.y, id: e.id });
}
}, { owner: unit });
};
const cast = S1 ? castS1 : S2 ? castS2 : castS3;
return {
skill: { kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } },
skills: alt(def, {
skchr_texas2_1: () => ({ kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } }),
skchr_texas2_2: () => ({ kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } }),
}),
// S2: 攻击变为二连击 while the passive lasts
trait: S2 ? { hitsFn: (b, u) => (u.findBuff('texas2:shower') ? 2 : 1) } : undefined,
talents: [
{ install(battle, unit) { // 德克萨斯传统 (2nd half): first kill of each deployment ⇒ full heal + recast
// (the passive's deploy burst runs inside skill.reset, BEFORE the `deploy` hook: the per-deployment state is
// therefore reset when she leaves the field, so a kill by the deploy burst itself counts)
battle.on('death', (c) => { if (c.unit === unit) unit.mem.texasKilled = false; }, { owner: unit });
battle.on('kill', (c) => {
if (c.killer !== unit || !unit.alive || c.victim.side !== 'enemy' || unit.mem.texasKilled) return;
unit.mem.texasKilled = true;
battle.removeBuff(unit, 'texas2:swordplay');
battle.heal(unit, unit, unit.s.maxHp * num(t0.hp_ratio, 1), { self: true });
cast(battle, unit);
}, { owner: unit });
} },
{ install(battle, unit) { // 德克萨斯剑术: until her first kill after each deployment: ASPD +8, −25 % damage taken
battle.on('deploy', (c) => {
if (c.unit !== unit || unit.mem.texasKilled) return; // the deploy burst may already have scored the kill
battle.addBuff(unit, { key: 'texas2:swordplay', mods: { aspd: num(t1.attack_speed, 8), dmgTakenMul: 1 - num(t1.damage_resistance, 0.25) } });
}, { owner: unit });
} },
],
install(battle, unit) {
if (S1) {
// 攻击使命中目标失去特殊能力N秒,期间目标每秒受到X点法术伤害 (landed normal attacks while the passive lasts)
const sil = num(bb['attack@silence'], 5), dotDur = num(bb['attack@texas2_s_1[dot].duration'], sil);
const dot = num(bb['attack@texas2_s_1[dot].dot_damage'], 260), dotIv = Math.max(0.1, num(bb['attack@texas2_s_1[dot].interval'], 1));
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg?.isAttack || e.side !== 'enemy' || !e.alive || !unit.findBuff('texas2:drizzle')) return;
battle.applyStatus(e, 'silence', { duration: sil, source: unit });
battle.addBuff(e, { key: `texas2:drizzleDot:${unit.id}`, duration: dotDur + 1e-6, interval: dotIv, source: unit, refresh: 'extend', // a re-hit refreshes it, the per-second ticks keep their rhythm
onTick: ({ unit: t }) => battle.dealDamage(unit, t, { amount: dot, type: 'arts', isSkill: true, tags: ['skill', 'dot'] }) });
}, { owner: unit });
}
if (S2) {
// 攻击…造成法术伤害 while the passive lasts (the double hit is the kit trait's hitsFn)
battle.on('hit', (c) => {
if (c.source === unit && c.dmg.isAttack && c.dmg.type === 'phys' && unit.findBuff('texas2:shower')) c.dmg.type = 'arts';
}, { owner: unit, priority: 50 });
}
const a = num(tb.atk, 0); // module (elite): ATK +10 % with no ally on the 4 adjacent tiles
if (!a) return;
whileDeployed(battle, unit, AURA, () => toggleBuff(battle, unit, 'texas2:module', lonely(battle, unit), { atkPct: a }));
},
};
},
});
@@ -0,0 +1,50 @@
// server/sim/content/kits/ops/chess_char_4_17-hsguma.js — 星熊 (char_136_hsguma) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { AURA, num, tbb, whileDeployed, pulse, toggleBuff, isSel, alt, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 星熊 (protector) S2 荆棘 (passive) — DEF +13 %, counters every attacker for 65 % ATK phys; talents
// S1 战意 (TAKE_DAMAGE: DEF/ATK up); S3 力之锯 (ATK/DEF up, cuts every enemy on her front tile — all enemies of her
// range); module PRO-X (护身符): DEF +20 % while blocking
chess_char_4_17_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const S2 = isSel(def, 'skchr_hsguma_2');
return {
skills: alt(def, {
skchr_hsguma_1: () => ({ kind: 'duration', mods: { defPct: num(bb.def), atkPct: num(bb.atk) } }),
skchr_hsguma_3: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
targeting: { allInRange: true },
onStart({ battle, unit }) { battle.fx('overclock', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: { kind: 'passive', mods: { defPct: num(bb.def) } },
talents: [
{ install(battle, unit) { // 战术装甲: 25 % 伤害抵挡 — negates an enemy phys/arts damage instance ("抵挡一次物理或法术伤害")
battle.on('hit', (c) => { // 抵挡 is target-side: a 无来源 burst (source null) counts via the enemy credited with it
const src = c.source || c.credit;
if (c.target !== unit || !src || src.side !== 'enemy' || c.dmg.cancel || (c.dmg.type !== 'phys' && c.dmg.type !== 'arts')) return;
if (battle.rng.chance(num(t0.prob, 0.25))) { c.dmg.cancel = true; battle.fx('block', { x: unit.x, y: unit.y, id: unit.id }); }
}, { owner: unit });
} },
{ install(battle, unit) { // 特种作战策略: 重装 allies DEF +6 %
whileDeployed(battle, unit, AURA, () => { for (const a of battle.allies(unit.ownerId)) if (a.def?.profession === 'TANK') pulse(battle, a, 'hsguma:def', { defPct: num(t1.def, 0.06) }); });
} },
],
install(battle, unit) {
const bd = num(tb.def, 0); // module PRO-X: 阻挡敌人时防御力+20%
if (bd) whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'hsguma:module', unit.blocking.length > 0, { defPct: bd }));
if (!S2) return;
battle.on('damaged', (c) => {
const src = c.source;
if (c.target !== unit || !unit.alive || !src || src.side !== 'enemy' || !src.alive || !c.dmg?.isAttack) return;
battle.dealDamage(unit, src, { amount: unit.s.atk * num(bb.atk_scale, 0.65), type: 'phys', canDodge: false, isSkill: true, tags: ['counter'] });
battle.fx('thorns', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,101 @@
// server/sim/content/kits/ops/chess_char_4_18-mudrok.js — 泥岩 (char_311_mudrok) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, grid, batFlat, enemyHasTag, targetsInGrid, whileDeployed, pulse, toggleBuff, alt, lonely,
withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 泥岩 (unyield) S2 岩崩锤 — next attack: heal 4 %, 190 % phys to all ground enemies around, 30 % stun; talents
// S1 防御力强化·γ型 (TAKE_DAMAGE); S3 秽壤的血脉 (10 s dormant — cannot act, takes no damage, keeps blocking —
// with enemies around −60 % speed; then ground enemies around stunned 3 s/3.5 s and for the rest of the skill
// BAT −0.3 s, ATK/DEF up, attacks every blocked enemy); module UNY-Y (沃土的愿景): ATK/DEF +8 % with no ally
// on the 8 tiles around
chess_char_4_18_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid) || [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]];
const sleep = num(bb.sleep, 10);
return {
skills: alt(def, {
'skcom_def_up[3]': () => ({ kind: 'duration', mods: { defPct: num(bb.def) } }),
skchr_mudrok_3: () => ({
kind: 'duration',
attack: { hitAllBlocked: true },
onStart({ battle, unit }) {
unit.mem.mudS3 = { awake: false, t: 0 };
battle.addBuff(unit, { key: 'mudrok:dormant', duration: sleep + 1, flags: { invulnerable: true, disarm: true }, visible: true });
battle.fx('stone', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
const M = unit.mem.mudS3;
if (!M || M.awake) return;
M.t += dt;
for (const e of targetsInGrid(battle, unit, g)) pulse(battle, e, `mudrok:slow:${unit.id}`, { moveMul: Math.max(0, 1 + num(bb.move_speed, -0.6)) });
if (M.t + 1e-9 < sleep) return;
M.awake = true;
battle.removeBuff(unit, 'mudrok:dormant');
for (const e of targetsInGrid(battle, unit, g)) if (!e.isFlying && e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun, 3), source: unit });
battle.addBuff(unit, { key: 'mudrok:awake', mods: { atkPct: num(bb.atk), defPct: num(bb.def), batPct: batFlat(def, bb.base_attack_time) } });
battle.fx('rockfall', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) {
unit.mem.mudS3 = null;
battle.removeBuff(unit, 'mudrok:dormant');
battle.removeBuff(unit, 'mudrok:awake');
},
}),
}),
skill: {
kind: 'instant',
targeting: { rangeGrid: g, allInRange: true, canHitFly: false },
attack: {
atkScale: num(bb.atk_scale, 1.9), groundOnly: true,
onHit({ battle, unit, target }) {
if (target && target.alive && battle.rng.chance(num(bb.buff_prob, 0.3))) battle.applyStatus(target, 'stun', { duration: num(bb.stun, 0.5), source: unit });
},
},
onStart({ battle, unit }) {
battle.heal(unit, unit, unit.s.maxHp * num(bb.hp_ratio, 0.04), { self: true });
battle.fx('rockslide', { x: unit.x, y: unit.y, id: unit.id });
},
},
talents: [
{ install(battle, unit) { // 沃土予身: a hit-negating layer every 9 s (≤ 3, 1 on deploy); each broken layer heals 20 %
const setLayers = (n) => {
unit.mem.layers = n;
if (n > 0) battle.addBuff(unit, { key: 'mudrok:layers', shieldHits: n, visible: true });
else battle.removeBuff(unit, 'mudrok:layers');
};
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
setLayers(Math.min(num(t0.max_times, 3), num(t0.times, 1)));
unit.mem.layerTimer?.cancel();
unit.mem.layerTimer = whileDeployed(battle, unit, num(t0.interval, 9), () => {
if ((unit.mem.layers ?? 0) < num(t0.max_times, 3)) setLayers(Math.min(num(t0.max_times, 3), (unit.mem.layers ?? 0) + num(t0.times, 1)));
});
}, { owner: unit });
battle.on('damaged', (c) => {
if (c.target !== unit || !unit.alive) return;
const left = unit.findBuff('mudrok:layers')?.shieldHits ?? 0;
const broken = (unit.mem.layers ?? 0) - left;
if (broken <= 0) return;
unit.mem.layers = left;
battle.heal(unit, unit, unit.s.maxHp * num(t0.hp_ratio, 0.2) * broken, { self: true });
battle.fx('shieldBreak', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
} },
{ install(battle, unit) { // 手足相惜: −30 % damage from 萨卡兹 enemies
battle.on('hit', (c) => { if (c.target === unit && enemyHasTag(c.source, 'sarkaz')) c.dmg.mul *= 1 - num(t1.damage_resistance, 0.3); }, { owner: unit });
} },
],
install(battle, unit) {
const ds = num(tb.damage_scale, 0); // module (elite): −15 % damage from enemies it blocks
if (ds > 0) battle.on('hit', (c) => { if (c.target === unit && c.source && c.source.blockedBy === unit) c.dmg.mul *= ds; }, { owner: unit });
const ma = num(tb.atk, 0), md = num(tb.def, 0); // module UNY-Y: 周围8格没有友方干员时攻击力和防御力+8%
if (ma || md) whileDeployed(battle, unit, AURA, () => toggleBuff(battle, unit, 'mudrok:module', lonely(battle, unit, true), { atkPct: ma, defPct: md }));
},
};
},
});
@@ -0,0 +1,118 @@
// server/sim/content/kits/ops/chess_char_4_19-flamtl.js — 焰尾 (char_420_flamtl) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
import {
AURA, num, tbb, grid, nationOf, targetsInGrid, whileDeployed, pulse, toggleBuff, skillActive, isSel, alt,
withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 焰尾 (pioneer) S3 焰心 — 8 DP over the skill, faster, +ATK, block +1, 60 % dodge; talents 前锋剑术 / 红松骑士团团长
// S1 迅敏直觉 (+6 DP, dodges the next physical attack); S2 “红松林” (+11/12 DP; ≤ 6 enemies around: 2 × 180 %/210 %
// phys + 0.5 s stun, air units too — PRTS 备注 "※可对空"; allies around +40 %/45 % physical dodge for 10 s);
// module SOL-X (她们的未来): ATK/DEF +8 % while blocking. Her dodges (any source) feed talent 前锋剑术.
chess_char_4_19_a: (bb, chess, def) => {
const t1 = tbb(def, 1);
const tb = def.traitBb || {};
const total = Math.max(0, Math.floor(num(bb.value, 8))), per = num(bb.cost, 1);
const p = num(bb.prob, 0.6);
const S1 = isSel(def, 'skchr_flamtl_1'), S3 = isSel(def, 'skchr_flamtl_3');
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
// (自动触发: an AUTO skill takes no 技能策略 — an AUTO DP skill fires as soon as it is ready, like 伺夜 S1)
skchr_flamtl_1: () => ({
kind: 'instant',
trigger: 'SP_FULL',
onStart({ battle, unit }) {
battle.addDp(unit.ownerId, num(bb.cost, 6));
battle.addBuff(unit, { key: 'flamtl:evade', visible: true });
battle.fx('dp', { x: unit.x, y: unit.y, id: unit.id, n: num(bb.cost, 6) });
},
}),
skchr_flamtl_2: () => ({
kind: 'instant',
onStart({ battle, unit }) {
battle.addDp(unit.ownerId, num(bb.cost, 11));
const area = g || [[0, 0], [0, 1]];
const foes = targetsInGrid(battle, unit, area);
sortEnemyTargets(battle, unit, foes, null);
for (const e of foes.slice(0, Math.max(1, Math.floor(num(bb.max_target, 6))))) {
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.8), type: 'phys', isSkill: true, tags: ['skill', 'redPine'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun, 0.5), source: unit });
}
for (const a of battle.unitsInGrid(unit, area, { side: 'ally' })) {
if (a.kind === 'device' || !battle.allySelectable(a, unit)) continue;
battle.addBuff(a, { key: 'flamtl:redPine', duration: num(bb['flamtl_s_2.duration'], 10), mods: { dodgePhys: num(bb['flamtl_s_2.prob'], 0.4) }, visible: true, source: unit });
}
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id });
},
}),
}),
skill: {
kind: 'duration',
// the 60 % dodge is rolled on its own in the `hit` hook below (independent of other dodge sources such as her
// talent's 22 %: 1 − (1−p)(1−q), never an additive 80 % + 22 % = 100 % immunity)
mods: { atkPct: num(bb.atk), batPct: Math.max(-0.9, num(bb.base_attack_time, 1) - 1), blockCnt: num(bb.block_cnt, 1) },
onStart({ battle, unit, skill }) { unit.mem.flame = { acc: 0, given: 0, step: skill.duration / Math.max(1, total) }; battle.fx('flame', { x: unit.x, y: unit.y, id: unit.id }); },
onTick({ battle, unit, dt }) {
const F = unit.mem.flame;
if (!F) return;
F.acc += dt;
while (F.acc + 1e-9 >= F.step && F.given < total) { F.acc -= F.step; F.given++; battle.addDp(unit.ownerId, per); }
},
onEnd({ battle, unit, reason }) {
const F = unit.mem.flame;
unit.mem.flame = null;
if (F && reason === 'duration' && F.given < total) battle.addDp(unit.ownerId, per * (total - F.given));
},
},
install(battle, unit) {
if (S1) {
battle.on('hit', (c) => { // S1: "闪避下次物理攻击"
const d = c.dmg;
if (c.target !== unit || d.cancel || d.type !== 'phys' || !d.isAttack || !c.source || c.source.side !== 'enemy' || !unit.findBuff('flamtl:evade')) return;
battle.removeBuff(unit, 'flamtl:evade');
d.cancel = true;
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
battle.emit('dodge', { source: c.source, target: unit, dmg: d });
}, { owner: unit, priority: 20 });
}
const ma = num(tb.atk, 0), md = num(tb.def, 0); // module SOL-X: 阻挡敌人时攻击力和防御力各+8%
if (ma || md) whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'flamtl:module', unit.blocking.length > 0, { atkPct: ma, defPct: md }));
if (!S3) return;
battle.on('hit', (c) => { // S3: "获得60%的物理和法术闪避"
const d = c.dmg;
if (c.target !== unit || !skillActive(unit) || d.cancel || !d.canDodge || (d.type !== 'phys' && d.type !== 'arts')) return;
if (!battle.rng.chance(p)) return;
d.cancel = true;
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
battle.emit('dodge', { source: c.source, target: unit, dmg: d });
}, { owner: unit, priority: 20 });
},
talents: [
{ install(battle, unit) { // 前锋剑术: after a dodge the next attack hits twice and every blocked enemy
battle.on('dodge', (c) => { if (c.target === unit) unit.mem.riposte = true; }, { owner: unit });
battle.on('death', (c) => { if (c.unit === unit) unit.mem.riposte = unit.mem.riposteNow = false; }, { owner: unit });
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !unit.mem.riposte) return;
unit.mem.riposte = false;
unit.mem.riposteNow = true;
const extra = unit.blocking.filter((e) => e.alive && canTargetEnemy(unit, e, c.profile) && !c.targets.includes(e));
c.targets = [...c.targets, ...extra];
}, { owner: unit });
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.mem.riposteNow) return;
unit.mem.riposteNow = false;
for (const e of c.targets) if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * unit.s.atkScaleMul, type: 'phys', isAttack: true, tags: ['riposte'] });
battle.fx('riposte', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
} },
{ install(battle, unit) { // 红松骑士团团长: 卡西米尔 operators +22 % physical dodge
whileDeployed(battle, unit, AURA, () => { for (const a of battle.allies(unit.ownerId)) if (nationOf(a) === 'kazimierz') pulse(battle, a, 'flamtl:dodge', { dodgePhys: num(t1.prob, 0.22) }); });
} },
],
};
},
});
@@ -0,0 +1,91 @@
// server/sim/content/kits/ops/chess_char_4_20-fartth.js — 远牙 (char_430_fartth) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { bodyInKeys } from '../../../body.js';
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
import { isHpLoss } from '../../../damage.js';
import { num, tbb, whileDeployed, toggleBuff, skillActive, isSel, alt, withDefaults } from '../shared/tier4.js';
const LINE = Object.freeze(Array.from({ length: COLS }, (_, i) => Object.freeze([0, i])));
const tileKey = (u) => Math.round(u.y) * COLS + Math.round(u.x);
/** HP at 0 (boss pool: pool HP) — a `damaged` hook sees the lethal hit before the kill, while the unit is still `alive`. */
const downed = (u) => (u.bossPool ? !(u.bossPool.hp > 0) : !(u.hp > 0));
export default withDefaults({
// ===== 远牙 (longrange) S3 光羽箭 — infinite line, +ATK, ×1.25 beyond the normal range; talents 凝神 / 屏息; module
// S1 迅捷打击·γ型; S2 同盟支援 (ASPD up; enemies blocked by any ally anywhere on the field are in range — their
// tiles are extra range keys while the skill runs). Talent 屏息 (taunt −1, ignores dodge) covers every skill.
// Module DEA-Y (支持者来信): +1 SP when an attacked enemy survives the hit
chess_char_4_20_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const S3 = isSel(def, 'skchr_fartth_3');
const taunt = num(t1.taunt_level, -1);
const setAllied = (battle, unit, keys) => {
const sig = keys ? keys.join(',') : '';
if (sig === (unit.mem.alliedSig ?? '')) return;
unit.mem.alliedSig = sig;
battle.setExtraRange(unit, keys);
};
return {
skills: alt(def, {
'skcom_quickattack[3]': () => ({ kind: 'duration', mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed), taunt } }),
skchr_fartth_2: () => ({
kind: 'duration',
mods: { aspd: num(bb.attack_speed), taunt },
onStart({ battle, unit }) { battle.fx('link', { x: unit.x, y: unit.y, id: unit.id }); },
onTick({ battle, unit }) {
const keys = [];
for (const e of battle.enemies) if (e.alive && !e.hidden && e.blockedBy && e.blockedBy.side === 'ally' && e.blockedBy.alive) keys.push(tileKey(e));
keys.sort((a, b) => a - b);
setAllied(battle, unit, keys.length ? keys : null);
},
onEnd({ battle, unit }) { setAllied(battle, unit, null); },
}),
}),
skill: { kind: 'duration', mods: { atkPct: num(bb.atk), taunt: num(t1.taunt_level, -1) }, targeting: { rangeGrid: LINE },
onStart({ battle, unit }) { battle.fx('featherArrow', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [
{ install(battle, unit) { // 凝神: ATK +15 % when not hurt for 10 s
battle.on('damaged', (c) => { if (c.target === unit && c.amount > 0 && !isHpLoss(c.dmg)) unit.mem.lastHurt = battle.time; }, { owner: unit }); // (a 流失 is not 受伤害)
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.lastHurt = -Infinity; }, { owner: unit });
whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'fartth:focus', battle.time - (unit.mem.lastHurt ?? -Infinity) >= num(t0.delay, 10) - 1e-9, { atkPct: num(t0.atk, 0.15) }));
} },
{ install(battle, unit) { // 屏息 (2nd half): skill attacks ignore physical dodge
battle.on('hit', (c) => { if (c.source === unit && skillActive(unit) && c.dmg.type === 'phys') c.dmg.canDodge = false; }, { owner: unit });
} },
],
install(battle, unit) {
if (isSel(def, 'skchr_fartth_2')) {
// 同盟支援 reaches BLOCKED enemies outside her range only: the blocked enemies' tiles are extra range keys, so an
// unblocked enemy (a flyer…) standing on such a tile outside her own range is dropped for the next valid target
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillActive(unit) || !unit.extraRangeKeys) return;
const own = new Set(absoluteRangeKeys(unit.rangeGrid, unit.tileR, unit.tileC, unit.dir, unit.s.rangeExtend));
const ok = (e) => bodyInKeys(e, own) || !!(e.blockedBy && e.blockedBy.side === 'ally' && e.blockedBy.alive);
if (c.targets.every(ok)) return;
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, c.profile);
for (const e of battle.blockedTargets(unit, c.profile)) if (!cands.includes(e)) cands.push(e); // DESIGN §20.3
for (let i = cands.length - 1; i >= 0; i--) if (!ok(cands[i])) cands.splice(i, 1);
sortEnemyTargets(battle, unit, cands, c.profile?.priority ?? null);
c.targets = cands.slice(0, Math.max(1, c.targets.length));
}, { owner: unit, priority: 5 });
}
const sp = num(tb.sp, 0); // module DEA-Y: 攻击的敌人未被击倒时自身额外获得1点技力
if (sp > 0) {
// (`damaged` fires before the kill: a lethal hit leaves the target `alive` with 0 HP — that one gives no SP)
battle.on('damaged', (c) => {
if (c.source === unit && c.dmg?.isAttack && !c.dmg.isSplash && c.target.side === 'enemy' && c.target.alive && !downed(c.target)) unit.skill?.gainSp(sp, 'module');
}, { owner: unit });
}
const ds = num(tb.damage_scale, 0), lo = num(tb.min_dist, 1), hi = num(tb.max_dist, 4.5);
battle.on('hit', (c) => {
if (c.source !== unit || !c.dmg.isAttack) return;
if (S3 && skillActive(unit) && unit.baseRangeKeys && !bodyInKeys(c.target, unit.baseRangeKeys)) c.dmg.mul *= num(bb.damage_scale, 1.25);
if (ds > 0) { const d = Math.hypot(c.target.x - unit.x, c.target.y - unit.y); c.dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo))); }
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,19 @@
// server/sim/content/kits/ops/chess_char_4_21-plosis.js — 白面鸮 (char_128_plosis) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { num, tbb, grid, batFlat, spAura, alt, applyModuleRange, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 白面鸮 (ringhealer) S2 脑啡肽 — wider range, much faster heals; talent 技力光环 (+0.3 SP/s to all allies)
chess_char_4_21_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, { 'skcom_heal_up[3]': () => ({ kind: 'duration', heal: true, mods: { atkPct: num(bb.atk) } }) }), // S1 治疗强化·γ型
skill: { kind: 'duration', heal: true, mods: { batPct: batFlat(def, bb.base_attack_time) }, targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { battle.fx('healField', { x: unit.x, y: unit.y, id: unit.id }); } },
talents: [{ install(battle, unit) { spAura(battle, unit, num(t0.sp_recovery_per_sec, 0.3), (a) => a.kind !== 'device'); } }],
install(battle, unit) { applyModuleRange(battle, unit, def); }, // module RIN-X (elite default): 攻击范围扩大
};
},
});
@@ -0,0 +1,52 @@
// server/sim/content/kits/ops/chess_char_4_22-svrash.js — 银灰 (char_172_svrash) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
AURA, num, tbb, grid, enemiesOnRange, whileDeployed, reveal, alt, instantKind, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 银灰 (lord) S3 真银斩 — DEF −70 %, ATK +125 %, wider range, ≤ 4 targets at melee scale; 领袖 / 鹰眼视觉; module
// S1 强力击·γ型 (attack SP: next attack 205 %/225 %); S2 雪境生存法则 (toggle — [ASSUMED] once switched on it stays
// on for the deployment: smaller range, DEF up, 3.5 %/4 % max HP regen per s)
chess_char_4_22_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
skchr_svrash_1: () => ({ kind: instantKind(def), attack: { atkScale: num(bb.atk_scale, 2.05) } }),
skchr_svrash_2: () => ({
kind: 'toggle',
mods: { defPct: num(bb.def), hpRegenRatio: num(bb.hp_recovery_per_sec_by_max_hp_ratio) },
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) { battle.fx('snow', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
mods: { defPct: num(bb.def), atkPct: num(bb.atk) },
targeting: { ...(g ? { rangeGrid: g } : {}), maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 4))) },
attack: { projectile: 'none', dmgMul: () => 1 }, // 视为近距离攻击: no ranged ×0.8
onStart({ battle, unit }) { battle.fx('truesilver', { x: unit.x, y: unit.y, id: unit.id }); },
onAttack({ battle, unit }) { battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id }); },
},
talents: [
{ install(battle, unit) { // 领袖: ATK +10 %; every unit of the team redeploys 10 % faster
battle.addBuff(unit, { key: 'svrash:t1', mods: { atkPct: num(t0.atk, 0.1) }, persist: true, allowDead: true });
const p = battle.getPlayer(unit.ownerId);
for (const a of p ? p.units : []) if (a.kind === 'op') battle.addBuff(a, { key: 'svrash:leader', mods: { redeployMul: Math.max(0, 1 + num(t0.respawn_time, -0.1)) }, persist: true, allowDead: true });
} },
{ install(battle, unit) { whileDeployed(battle, unit, AURA, () => reveal(battle, enemiesOnRange(battle, unit))); } }, // 鹰眼视觉
],
install(battle, unit) {
const m = num(tb.atk_scale_m, 0); // module (elite): attacks add 10 % ATK arts damage
if (!(m > 0)) return;
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg?.isAttack || c.type !== 'phys' || !c.target.alive || c.target.side !== 'enemy') return;
battle.dealDamage(unit, c.target, { amount: unit.s.atk * m, type: 'arts', tags: ['module'] });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,78 @@
// server/sim/content/kits/ops/chess_char_4_23-gvial2.js — 百炼嘉维尔 (char_1026_gvial2) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
num, tbb, grid, whileDeployed, toggleBuff, skillActive, isSel, alt, pullToFront, withDefaults,
} from '../shared/tier4.js';
export default withDefaults({
// ===== 百炼嘉维尔 (centurion) S3 丛林之魂 — ATK/ASPD/block up, takes 50 % now, the rest as 20 s HP loss; talents; module
// S1 精准痛击 (ATK up, heals herself 30 %/35 % of the damage dealt); S2 链锯强袭 (wider range, ATK/DEF up, pulls
// unblocked enemies it hits to her front); module CEN-Y (好锯多磨): −20 % physical damage taken above 50 % HP
chess_char_4_23_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
const dr = Math.max(0, Math.min(0.99, num(bb.damage_resistance, 0.5)));
const fin = num(bb.final_duration, 20), iv = Math.max(1 / 30, num(bb.interval, 0.1));
const S3 = isSel(def, 'skchr_gvial2_3');
const g = grid(def.skill?.rangeGrid);
return {
skills: alt(def, {
skchr_gvial2_1: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk) },
onHit({ battle, unit, dealt }) { if (dealt > 0 && unit.alive) battle.heal(unit, unit, dealt * num(bb.heal_scale, 0.3), { self: true }); },
}),
skchr_gvial2_2: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
targeting: g ? { rangeGrid: g } : undefined,
attack: { onEachHit({ battle, unit, target }) { if (target && target.alive && !target.blockedBy) pullToFront(battle, unit, target, num(bb['attack@force'], 1)); } },
onStart({ battle, unit }) { battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id }); },
}),
}),
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed), blockCnt: num(bb.block_cnt, 2) },
onStart({ battle, unit }) { unit.mem.gvialDebt = 0; battle.fx('jungleSoul', { x: unit.x, y: unit.y, id: unit.id }); },
onEnd({ battle, unit, reason }) {
const debt = unit.mem.gvialDebt ?? 0;
unit.mem.gvialDebt = 0;
if (reason === 'death' || !unit.alive || !(debt > 0)) return;
const per = debt * iv / fin;
battle.addBuff(unit, { key: 'gvial:bleed', refresh: 'independent', maxStacks: 10, duration: fin + 1e-6, interval: iv, visible: true,
onTick: ({ unit: u }) => battle.loseHp(u, per) });
},
},
talents: [
{ install(battle, unit) { // 战地巨斧: ATK/DEF +10 %, +4 % per extra blocked enemy
whileDeployed(battle, unit, 0.1, () => {
const extra = Math.max(0, unit.blocking.length - 1);
toggleBuff(battle, unit, 'gvial:t1', true, { atkPct: num(t0.atk, 0.1) + num(t0.atk_add, 0.04) * extra, defPct: num(t0.def, 0.1) + num(t0.def_add, 0.04) * extra });
});
} },
{ install(battle, unit) { // 医学背景: healing received +20 %, +40 % below half HP
battle.on('heal', (c) => { if (c.target === unit) c.amount *= unit.hpRatio < num(t1.hp_ratio, 0.5) ? num(t1.heal_scale_2, 1.4) : num(t1.heal_scale_1, 1.2); }, { owner: unit });
} },
],
install(battle, unit) {
const s = num(tb.atk_scale, 0); // module (elite): ×1.1 against blocked enemies
if (s > 0) battle.on('hit', (c) => { if (c.source === unit && c.dmg.isAttack && c.target.blockedBy === unit) c.dmg.mul *= s; }, { owner: unit });
const yr = num(tb.hp_ratio, 0), yd = num(tb.damage_resistance, 0); // module CEN-Y: 生命值高于50%时受到的物理伤害降低20%
if (yr > 0 && yd > 0) battle.on('hit', (c) => { if (c.target === unit && c.dmg.type === 'phys' && unit.hpRatio > yr) c.dmg.mul *= 1 - yd; }, { owner: unit });
if (!S3) return;
const deferred = new WeakSet();
battle.on('hit', (c) => {
if (c.target !== unit || !skillActive(unit)) return;
c.dmg.mul *= 1 - dr;
deferred.add(c.dmg);
}, { owner: unit, priority: -5 });
battle.on('damaged', (c) => {
if (c.target !== unit || !deferred.has(c.dmg)) return;
deferred.delete(c.dmg);
if (c.amount > 0) unit.mem.gvialDebt = (unit.mem.gvialDebt ?? 0) + (c.amount * dr) / (1 - dr);
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,148 @@
// server/sim/content/kits/ops/chess_char_4_24-billro.js — 卡涅利安 (char_426_billro) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import {
num, tbb, moduleBb, grid, batFlat, enemiesOnRange, whileDeployed, toggleBuff, skillActive, isSel, alt, withDefaults,
} from '../shared/tier4.js';
/** 卡涅利安 S3 食噬之印: the ATK bonus climbs in 1 s steps over 20 s (PRTS 备注; the blackboard has no ramp time). */
const BILLRO_S3_RAMP = 20;
/** 卡涅利安 S3 charged mark: one buff per enemy, shared by every 卡涅利安 (PRTS 备注), ≤ 5 stacks (skill text). */
const BILLRO_MARK = 'billro:mark';
const BILLRO_MARK_MAX = 5;
export default withDefaults({
// ===== 卡涅利安 (phalanx) S2 沙缚镣锁 — faster attacks on every enemy in range (the trait's 群体法术伤害), 0.3 s
// sluggish on each; charged: ATK +10 % and bind; talents
// S1 沙暴守卫 (SEARCH: ATK/DEF up; charged — cast with every charge stored — "特性效果在技能期间继续生效", PRTS 备注
// "应用蓄力时:应用技能未开启时的特性": the skill-off trait stays in force, the DEF/RES guard AND 不攻击 — she makes
// no attack until the skill ends, a pure guard);
// S3 食噬之印 (wider range, ATK +0 % → +140 %/+200 % in 1 s steps over 20 s — PRTS 备注; charged: each attack adds a
// stack of the mark BEFORE its damage — PRTS 备注 "于攻击造成伤害前生效,多层效果之间加算叠加" — +20 % damage from
// her per stack, ≤ 5, so ×1.2 on the first hit and ×2.0 from the 5th, until the skill ends; one mark per enemy:
// another 卡涅利安 only adds stacks to it, the bonus is the setter's — PRTS 备注 popup). Talent 生命之餐 heals on
// every skill; module PLX-X keeps part of the guard during any skill, PLX-Y (乡音): +3 % damage per enemy in range
// (≤ 5)
chess_char_4_24_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const tb = def.traitBb || {};
const keepDef = num(tb['billro_e_002[buff].def'], 0), keepRes = num(tb['billro_e_002[buff].magic_resistance'], 0);
const S1 = isSel(def, 'skchr_billro_1'), S2 = isSel(def, 'skchr_billro_2'), S3 = isSel(def, 'skchr_billro_3');
const g = grid(def.skill?.rangeGrid);
const isCharged = (skill) => skill.maxCharges > 1 && skill.charges + 1 >= skill.maxCharges; // cast with every charge stored
return {
// charged S1: the skill-off trait's 不攻击 stays in force while the skill lasts (PRTS S1 备注) — no attack at all
trait: S1 ? { canAttack: (battle, u) => !(skillActive(u) && u.mem.billroCharged) } : null,
skills: alt(def, {
skchr_billro_1: () => ({
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
onStart({ battle, unit, skill }) {
unit.mem.billroCharged = isCharged(skill);
// 蓄力额外效果:特性效果在技能期间继续生效 (the profession removes its own guard buff when a skill starts; the
// kit trait's canAttack keeps the trait's 不攻击)
if (unit.mem.billroCharged) battle.addBuff(unit, { key: 'billro:s1guard', mods: { defPct: num(unit.profile?.guardDef, 2), resFlat: num(unit.profile?.guardRes, 20) } });
battle.fx('shell', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { unit.mem.billroCharged = false; battle.removeBuff(unit, 'billro:s1guard'); },
}),
skchr_billro_3: () => ({
kind: 'duration',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit, skill }) {
unit.mem.billroCharged = isCharged(skill);
unit.mem.billroRamp = 0;
unit.mem.billroMarked = new Set();
battle.addBuff(unit, { key: 'billro:s3atk', mods: { atkPct: 0 } });
battle.fx('devour', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ unit, dt }) { // 攻击力逐渐增至+N%: PRTS 备注 "从+0%开始在20秒内线性增加,攻击力每1秒更新1次"
unit.mem.billroRamp = (unit.mem.billroRamp ?? 0) + dt;
const steps = Math.min(BILLRO_S3_RAMP, Math.floor(unit.mem.billroRamp + 1e-9));
const b = unit.findBuff('billro:s3atk');
if (b && b.mods.atkPct !== num(bb.atk) * steps / BILLRO_S3_RAMP) { b.mods = { atkPct: num(bb.atk) * steps / BILLRO_S3_RAMP }; unit.markDirty(); }
},
onEnd({ battle, unit }) {
battle.removeBuff(unit, 'billro:s3atk');
// "持续至技能结束": the marks she set end with her skill (stacks another 卡涅利安 added to them included)
for (const id of unit.mem.billroMarked || []) {
const e = battle.unitById(id);
if (e && e.findBuff(BILLRO_MARK)?.source === unit) battle.removeBuff(e, BILLRO_MARK);
}
unit.mem.billroMarked = null;
unit.mem.billroCharged = false;
},
}),
}),
skill: {
kind: 'duration',
mods: { batPct: batFlat(def, bb.base_attack_time) },
onStart({ battle, unit, skill }) {
const charged = isCharged(skill);
unit.mem.billroCharged = charged;
if (charged) battle.addBuff(unit, { key: 'billro:charged', mods: { atkPct: num(bb.atk, 0.1) } });
battle.fx(charged ? 'sandChainsCharged' : 'sandChains', { x: unit.x, y: unit.y, id: unit.id });
},
onEnd({ battle, unit }) { unit.mem.billroCharged = false; battle.removeBuff(unit, 'billro:charged'); },
},
talents: [
{ install(battle, unit) { // 生命之餐: every skill start heals 40 % max HP (×2 when charged)
battle.on('skillStart', (c) => {
if (c.unit !== unit) return;
const charged = isCharged(c.skill);
battle.heal(unit, unit, unit.s.maxHp * (charged ? num(t0['billro_t_1[enhance].heal_scale'], 0.8) : num(t0.heal_scale, 0.4)), { self: true });
}, { owner: unit });
} },
{ install(battle, unit) { // 蓄势待发: +0.6 SP/s once a charge is stored
whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'billro:t2', !!unit.skill && unit.skill.charges >= 1 && !skillActive(unit), { spRecoveryFlat: num(t1.sp_recovery_per_sec, 0.6) }));
} },
],
install(battle, unit) {
// module PLX-X (elite default): 技能开启时保留部分效果 (any skill; a charged S1 keeps the whole guard instead)
if (keepDef || keepRes) {
battle.on('skillStart', (c) => {
if (c.unit !== unit || (S1 && unit.mem.billroCharged)) return;
battle.addBuff(unit, { key: 'billro:keep', mods: { defPct: keepDef, resFlat: keepRes } });
}, { owner: unit });
battle.on('skillEnd', (c) => { if (c.unit === unit) battle.removeBuff(unit, 'billro:keep'); }, { owner: unit });
}
// module PLX-Y: 范围内敌人越多造成的伤害越高(每个 +3 %,最多 5 个)
const per = num(mb.damage_scale, 0), cap = num(mb.max_valid_stack_cnt, 5);
if (per > 0) {
battle.on('hit', (c) => {
if (c.source !== unit || c.target.side !== 'enemy') return;
const n = Math.min(cap, enemiesOnRange(battle, unit).length);
if (n > 0) c.dmg.mul *= 1 + per * n;
}, { owner: unit });
}
if (S3) {
// charged S3 食噬之印: each attack adds a stack BEFORE its damage (PRTS 备注 "于攻击造成伤害前生效"); stacks add up,
// +20 % damage taken from the mark's setter per stack. One mark per enemy (PRTS 备注 popup: "存在则为已有的BUFF叠加
// 一次加成,不存在则给与敌人仅对此卡涅利安的伤害生效的一个食噬之印BUFF"): another 卡涅利安 stacks it without the bonus
const per = num(bb['attack@damage_scale'], 0.2);
battle.on('hit', (c) => {
const e = c.target;
if (c.source !== unit || e.side !== 'enemy') return;
let m = e.findBuff?.(BILLRO_MARK) || null;
if (c.dmg.isAttack && skillActive(unit) && unit.mem.billroCharged) {
if (m) battle.addBuff(e, { key: BILLRO_MARK, refresh: 'stack', maxStacks: BILLRO_MARK_MAX, duration: m.timeLeft });
else {
m = battle.addBuff(e, { key: BILLRO_MARK, refresh: 'stack', maxStacks: BILLRO_MARK_MAX, duration: Math.max(0.1, (unit.skill?.timeLeft ?? 0) + 0.1), source: unit, visible: true });
if (m) unit.mem.billroMarked?.add(e.id);
}
}
if (m && m.source === unit) c.dmg.mul *= 1 + per * (m.stacks || 1);
}, { owner: unit });
}
if (!S2) return;
// "每次攻击对目标造成0.3秒停顿" (charged: 束缚): every enemy her attack damages — each one on her range
battle.on('damaged', (c) => {
const e = c.target;
if (c.source !== unit || !c.dmg || !c.dmg.isAttack || e.side !== 'enemy' || !e.alive || !skillActive(unit)) return;
if (unit.mem.billroCharged) battle.applyStatus(e, 'bind', { duration: num(bb['attack@root'], 0.3), source: unit });
else battle.applyStatus(e, 'sluggish', { duration: num(bb['attack@sluggish'], 0.3), source: unit });
}, { owner: unit });
},
};
},
});
@@ -0,0 +1,150 @@
// server/sim/content/kits/ops/chess_char_4_25-cetsyr.js — 魔王 (char_4134_cetsyr) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { canTargetEnemy } from '../../../targeting.js';
import { aggregateMods } from '../../../buffs.js';
import {
AURA, AURA_DUR, num, tbb, moduleBb, grid, enemyHasTag, whileDeployed, toggleBuff, skillActive, isSel, alt,
withDefaults,
} from '../shared/tier4.js';
/**
* 鼓舞 (ba.inspire "获得额外附加的基础属性加成(同类属性取最高)"): +`val` ATK (or max HP) on top of the target's own
* percentage multipliers (the flat mod is compensated, so an ATK+% skill does not scale it), the strongest source wins
* (the ATK key `inspire` is shared with tier6's 鼓舞); units flagged `mem.noInspire` (bards: 自身不受鼓舞影响) never
* receive it. Refreshed by an aura: lapses within AURA_DUR once the source stops.
*/
function inspire(battle, target, val, src, stat = 'atk') {
if (!(val > 0) || !target || !target.alive || target.mem?.noInspire) return;
const key = stat === 'hp' ? 'inspire:hp' : 'inspire';
const cur = target.findBuff(key);
if (cur && cur.data && cur.data.src !== src.id && cur.data.val > val && cur.timeLeft > 1e-6) return;
const { add, mul } = aggregateMods(target.buffs.filter((b) => b.key !== key));
const f = stat === 'hp' ? Math.max(0, 1 + (add.hpPct ?? 0)) * (mul.hpMul ?? 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 === 'hp' ? { hpFlat: flat } : { atkFlat: flat }, duration: AURA_DUR, source: src, visible: true, data: { src: src.id, val } });
}
export default withDefaults({
// ===== 魔王 (bard) S3 编织重构现世 — wider range, trait 65 %, motes stay, 鼓舞 +65 % of her max HP, HP redistribution
// S1 往昔萦绕身旁 (SP_FULL toggle, 持续时间无限: trait 20 %/28 %, motes respawn in `talent_cool_down` s); S2 明日渺远不及
// (motes cap +3 and 6 at once, wider orbit — enemies within outside_radius — no longer touch operators: a mote
// hitting an enemy deals 220 %/260 % ATK true damage + 3 s bind and respawns like any mote [ASSUMED]; 鼓舞: other
// allies in range ATK + 65 %/80 % of her ATK)
chess_char_4_25_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1), mb = moduleBb(def);
const g = grid(def.skill?.rangeGrid);
const redistIv = Math.max(0.5, num(bb['attack@cetsyr_s_3[cal_hp_ratio].interval'], 2));
const S1 = isSel(def, 'skchr_cetsyr_1'), S2 = isSel(def, 'skchr_cetsyr_2'), S3 = isSel(def, 'skchr_cetsyr_3');
const baseCnt = Math.max(0, Math.floor(num(t0.cnt, 3)));
const MOTE_GAP = 0.5; // [ASSUMED] minimal time between two motes hitting the same enemy (s)
const setTrait = (unit, v) => { unit.mem.bardBase ??= unit.profile.auraRatio; unit.profile.auraRatio = num(v, unit.mem.bardBase); };
const resetTrait = (unit) => { if (unit.mem.bardBase != null) unit.profile.auraRatio = unit.mem.bardBase; };
return {
skills: alt(def, {
skchr_cetsyr_1: () => ({
kind: 'toggle',
trigger: 'SP_FULL', // 自动触发 (no 技能策略: the 吟游者 row is for MANUAL skills): on as soon as it is ready
onStart({ battle, unit }) { setTrait(unit, bb['attack@atk_to_hp_recovery_ratio']); battle.fx('mote', { x: unit.x, y: unit.y, id: unit.id }); },
onEnd({ unit }) { resetTrait(unit); },
}),
skchr_cetsyr_2: () => ({
kind: 'duration',
onStart({ battle, unit }) {
// “微尘”上限+3,立刻获得6枚“微尘”
unit.mem.motes = new Array(baseCnt + 3).fill(-Infinity);
unit.mem.moteHit = new Map();
battle.fx('reweave', { x: unit.x, y: unit.y, id: unit.id, n: unit.mem.motes.length });
},
onTick({ battle, unit }) { // 鼓舞: other allies in range ATK + x % of her ATK
for (const a of battle.alliesInGrid(unit)) if (a !== unit) inspire(battle, a, unit.s.atk * num(bb['attack@atk'], 0.65), unit);
},
onEnd({ battle, unit }) {
// back to the normal cap: the motes that come back first are kept
unit.mem.motes = (unit.mem.motes || []).slice().sort((a, b) => a - b).slice(0, baseCnt);
unit.mem.moteHit = null;
},
}),
}),
skill: {
kind: 'duration',
targeting: g ? { rangeGrid: g } : undefined,
onStart({ battle, unit }) {
setTrait(unit, bb['attack@atk_to_hp_recovery_ratio']);
unit.mem.redist = 0;
battle.fx('reweave', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
const others = battle.alliesInGrid(unit).filter((a) => a !== unit);
for (const a of others) inspire(battle, a, unit.s.maxHp * num(bb.max_hp, 0.65), unit, 'hp');
unit.mem.redist += dt;
if (unit.mem.redist + 1e-9 < redistIv) return;
unit.mem.redist -= redistIv;
const pool = battle.alliesInGrid(unit).filter((a) => !(a.s.flags.noHeal || a.profile?.noHeal) || a === unit);
let hp = 0, max = 0;
for (const a of pool) { hp += a.hp; max += a.s.maxHp; }
if (!(max > 0) || pool.length < 2) return;
const ratio = Math.min(1, hp / max);
for (const a of pool) a.hp = Math.max(1, Math.min(a.s.maxHp, a.s.maxHp * ratio));
battle.fx('redistribute', { x: unit.x, y: unit.y, id: unit.id, ratio: Math.round(ratio * 1000) / 1000 });
},
onEnd({ unit }) { if (unit.mem.bardBase != null) unit.profile.auraRatio = unit.mem.bardBase; },
},
talents: [
{ install(battle, unit) { // 过往尘埃: 3 motes; touching an operator ⇒ her trait heal ×1.5 on it for 6 s; mote back after 6 s
unit.mem.motes = new Array(baseCnt).fill(-Infinity);
unit.mem.noInspire = true; // trait: 自身不受鼓舞影响 (another bard's 鼓舞 skips her)
const cooldown = () => (S1 && skillActive(unit) ? num(bb.talent_cool_down, 3) : num(t0.cooldown, 6)); // S1: 重生速度加快
const takeMote = () => {
const i = unit.mem.motes.findIndex((t) => t <= battle.time + 1e-9);
if (i < 0) return false;
unit.mem.motes[i] = battle.time + cooldown();
return true;
};
whileDeployed(battle, unit, 0.25, () => {
if (S2 && skillActive(unit)) {
// S2: motes orbit wider and hit enemies instead of operators
const hitAt = unit.mem.moteHit || (unit.mem.moteHit = new Map());
const foes = battle.foesInRadius(unit.x, unit.y, num(bb.outside_radius, 2)).filter((e) => canTargetEnemy(unit, e, { canHitFly: true }))
.sort((a, b) => a.id - b.id);
for (const e of foes) {
if ((hitAt.get(e.id) ?? -Infinity) > battle.time + 1e-9) continue;
if (!takeMote()) break;
hitAt.set(e.id, battle.time + MOTE_GAP);
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 2.2), type: 'true', isSkill: true, tags: ['skill', 'mote'] });
if (e.alive) battle.applyStatus(e, 'bind', { duration: num(bb.unmoveable_duration, 3), source: unit });
battle.fx('mote', { x: e.x, y: e.y, id: e.id });
}
return;
}
const keep = S3 && skillActive(unit); // S3: “微尘”不再消失
for (const a of battle.alliesInRadius(unit.x, unit.y, num(t0.range_radius, 1.15), unit.ownerId)) {
if (a === unit || a.kind !== 'op' || a.findBuff(`cetsyr:mote:${unit.id}`)) continue;
if (!keep && !takeMote()) break;
battle.addBuff(a, { key: `cetsyr:mote:${unit.id}`, duration: num(t0.talent_duration, 6), visible: true, source: unit });
battle.fx('mote', { x: a.x, y: a.y, id: a.id });
}
});
battle.on('heal', (c) => {
if (c.source === unit && c.opts?.aura && c.target.findBuff(`cetsyr:mote:${unit.id}`)) c.amount *= num(t0['attack@trait_mul'], 1.5);
}, { owner: unit });
} },
{ install(battle, unit) { // 魔王残响: every ally takes −10 % damage from 萨卡兹 enemies (while she is in the squad)
battle.on('hit', (c) => {
if (c.target.side === 'ally' && c.target.ownerId === unit.ownerId && enemyHasTag(c.source, 'sarkaz')) c.dmg.mul *= 1 - num(t1.damage_resistance, 0.1);
}, { owner: unit });
} },
],
install(battle, unit) {
const a = num(mb.atk, 0), cnt = num(mb.cnt, 2); // module (elite): ≥ 2 other operators in her normal range ⇒ ATK +8 %
if (!a) return;
whileDeployed(battle, unit, AURA, () => {
const set = new Set(unit.baseRangeKeys || []);
const n = battle.allies(unit.ownerId).filter((o) => o !== unit && o.kind === 'op' && set.has(o.tileR * COLS + o.tileC)).length;
toggleBuff(battle, unit, 'cetsyr:module', n >= cnt, { atkPct: a });
});
},
};
},
});
@@ -0,0 +1,66 @@
// server/sim/content/kits/ops/chess_char_4_26-bldsk.js — 华法琳 (char_171_bldsk) kit, tier 4.
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { bodyInKeys } from '../../../body.js';
import { num, tbb, keySet, isSel, alt, installLowHpHealBonus, withDefaults } from '../shared/tier4.js';
export default withDefaults({
// ===== 华法琳 (physician) S1 紧急包扎 — charges: a heal on an ally below 50 % adds 15 % of its max HP; 血液样本回收
// S2 不稳定血浆 (she and one random other ally in range: ATK +45 %/+60 %, lose 3 % max HP per s, 15 s)
chess_char_4_26_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const tb = def.traitBb || {};
const S1 = isSel(def, 'skchr_bldsk_1');
const plasma = (battle, unit, a) => battle.addBuff(a, {
key: 'bldsk:plasma', duration: num(bb.duration, 15) + 1e-6, interval: Math.max(0.1, num(bb.interval, 1)), mods: { atkPct: num(bb.atk) }, visible: true, source: unit,
onTick: ({ unit: t }) => battle.loseHp(t, t.s.maxHp * num(bb.hp_ratio, 0.03), { tags: ['skill', 'plasma'] }), // (no source: not damage dealt)
});
return {
skills: alt(def, {
skchr_bldsk_2: () => ({
kind: 'instant', heal: true,
onStart({ battle, unit }) {
plasma(battle, unit, unit);
const pick = battle.rng.pick(battle.alliesInGrid(unit).filter((a) => a !== unit));
if (pick) plasma(battle, unit, pick);
battle.fx('bloodBattle', { x: unit.x, y: unit.y, id: unit.id, target: pick ? pick.id : null });
},
}),
}),
skill: {
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
heal: true,
trigger: { rule: 'CUSTOM_RANGE', grid: [] }, // never auto-cast by the engine: fired by the kit on a < 50 % heal target
},
talents: [{ install(battle, unit) { // 血液样本回收: an enemy falls in range ⇒ +2 SP to her and a random ally in range
battle.on('death', (c) => {
const e = c.unit;
if (c.reason !== 'killed' || e.side !== 'enemy' || !unit.alive || !unit.deployed || !bodyInKeys(e, keySet(unit))) return;
unit.skill?.gainSp(num(t0['bldsk_t_1[self].sp'], 2), 'talent');
// SP cannot be gained while a timed skill runs (engine gainSp ignores it): such allies are not picked
const pick = battle.rng.pick(battle.alliesInGrid(unit).filter((a) => a !== unit && a.skill && !a.skill.noSkill && a.skill.kind !== 'passive' && !(a.skill.active && a.skill.isTimed)));
if (pick) { pick.skill.gainSp(num(t0['bldsk_t_1[rand].sp'], 2), 'talent'); battle.fx('spGift', { x: pick.x, y: pick.y, id: pick.id }); }
}, { owner: unit });
} }],
install(battle, unit) {
installLowHpHealBonus(battle, unit, tb);
if (!S1) return;
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit) return;
const t = c.targets[0];
const sk = unit.skill;
if (!t || t.side !== 'ally' || t.hpRatio >= 0.5 || !sk || !sk.ready || unit.s.flags.silence) return;
if (sk.activate('DEFAULT')) unit.mem.bandage = t;
}, { owner: unit, priority: 5 });
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.mem.bandage) return;
const t = unit.mem.bandage;
unit.mem.bandage = null;
unit.mem.skipSp = true; // the skill heal recovers no attack SP (AK)
if (t.alive) { battle.heal(unit, t, t.s.maxHp * num(bb.hp_ratio, 0.15)); battle.fx('bandage', { x: t.x, y: t.y, id: t.id }); }
}, { owner: unit });
battle.on('spGain', (c) => { if (c.unit === unit && c.reason === 'attack' && unit.mem.skipSp) { unit.mem.skipSp = false; c.amount = 0; } }, { owner: unit });
},
};
},
});
@@ -0,0 +1,124 @@
// server/sim/content/kits/ops/chess_char_5_01-excu2.js — 圣约送葬人 (char_1032_excu2) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import {
num, on, talent, batPct, mods, inFaction, isOp, selectedId, lazySkills, skillRange, crowdAspd,
} from '../shared/tier5.js';
const isLaterano = (u) => inFaction(u, 'lateranoShip', ['laterano']);
/** Enemy `e` attacks `t` in melee (not a ranged shot), mirroring ai.js enemyAttack. */
function meleeAttacker(e, t) {
const melee = e.profile?.melee ?? e.def?.applyWay === 'MELEE';
const r = num(e.base?.rangeRadius, 0);
return melee || !(r > 0) || (e.blockedBy === t && r < 1);
}
export default {
// ---------------------------------------------------------------------------------------------------------------
// 圣约送葬人 — S2 近身铳斗 (ammo, attack SP): ATK/DEF +, block +1, melee hits dodged with prob (+ammo refill).
// T1 受选之人: extra attack chance (+prob_add per ammo spent in the skill). T2 铳弹共感: +ammo per Laterano op.
// S1 遗嘱执行 (ammo 8, attack SP): skill range, ATK +, ignores def_penetrate_fixed DEF.
// S3 圣约决裁 (ammo 16): skill range, BAT +0.5 s, ATK +; +attack@atk ATK per bullet spent (≤ attack@max_stack_cnt),
// reaper self-heal ×trait_ratio; at the end every enemy attacked during the skill takes attack@final_atk_scale × ATK phys.
// Module REA-Y (elite): ASPD +12 with ≥ 2 enemies in range.
chess_char_5_01_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tm = talent(chess, -1);
const sid = selectedId(chess, def);
const prob = num(bb.prob), refill = num(bb.recover_cnt, 1);
return {
skills: lazySkills({
skchr_excu2_1: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 8),
mods: mods({ atkPct: num(bb.atk), defIgnoreFlat: num(bb.def_penetrate_fixed) }),
targeting: skillRange(chess, def),
}),
skchr_excu2_3: () => ({
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 16),
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def),
attack: { onEachHit({ unit, target }) { if (target && target.side === 'enemy') unit.mem.verdict?.add(target); } },
onStart({ unit }) {
unit.mem.verdict = new Set();
// 特性的回复生命效果提高至2倍 (the reaper profile reads profile.selfHeal on every attack)
if (unit.profile) { unit.mem.verdictHeal0 ??= num(unit.profile.selfHeal, 50); unit.profile.selfHeal = unit.mem.verdictHeal0 * num(bb.trait_ratio, 1); }
},
onEnd({ battle, unit }) {
const hit = unit.mem.verdict;
unit.mem.verdict = null;
if (unit.profile && unit.mem.verdictHeal0 != null) unit.profile.selfHeal = unit.mem.verdictHeal0;
if (hit && unit.alive) {
for (const e of hit) {
if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['attack@final_atk_scale']), type: 'phys', isSkill: true, tags: ['skill', 'verdict'] });
}
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id, r: 1.5, skill: 'excu2' });
}
battle.removeBuff(unit, 'excu2:verdict');
},
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 12),
mods: mods({ atkPct: num(bb.atk), defPct: num(bb.def), blockCnt: num(bb.block_cnt) }),
},
// REA-Y: its trait part is DISPLAY-only (official battle_equip_table uniequip_003_excu2: target DISPLAY, the ASPD
// text "{value}" = 12; the ASPD rider itself is the talent −1 part). data/chess.json merges that display blackboard
// into trait.bb ({value: 12}), which the reaper profile would read as its per-hit heal: the heal stays the base
// trait's (50).
trait: tm['trigger_cnt[equip]'] != null && chess?.traitBase?.bb?.value != null ? { selfHeal: num(chess.traitBase.bb.value, 50) } : undefined,
talents: [
{ install(battle, unit) { // 受选之人
let spent = 0;
const reset = (c) => { if (c.unit === unit) spent = 0; };
battle.on('skillStart', reset, { owner: unit });
battle.on('skillEnd', reset, { owner: unit });
battle.on('ammoUsed', (c) => { if (c.unit === unit) spent++; }, { owner: unit });
// PRTS 备注: "触发时可额外回复1点攻击回复技力,不额外消耗弹药" — the extra attack is an attack (attack SP, trait
// heal) but consumes no ammo (engine forceAttack `noAmmo`: no bullet, no ammoUsed for any listener — the
// 7-bullet 特质 counter, items, bonds and the prob_add counter above)
battle.on('attack', (c) => {
if (c.attacker !== unit || unit.mem.extraAttack || !on(unit)) return;
const p = num(t0.prob) + (unit.skill?.active ? num(t0.prob_add) * spent : 0);
if (!(p > 0) || !battle.rng.chance(Math.min(1, p))) return;
const dep = unit.deploySeq;
battle.after(0, () => {
if (!on(unit) || unit.deploySeq !== dep || !unit.canAct || unit.s.flags.disarm) return;
unit.mem.extraAttack = true;
let hit = false;
try { hit = battle.forceAttack(unit, null, { noAmmo: true }); } finally { unit.mem.extraAttack = false; }
if (hit) battle.fx('extraAttack', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
}, { owner: unit });
} },
{ install(battle, unit) { // 铳弹共感
battle.on('skillStart', (c) => {
if (c.unit !== unit || c.skill.kind !== 'ammo') return;
const n = battle.allies().filter((a) => isOp(a) && isLaterano(a)).length;
const stacks = Math.min(num(t1.add_count_max_stack, 4), n);
if (stacks > 0) c.skill.addAmmo(stacks * num(t1.add_count, 1));
}, { owner: unit });
} },
],
install(battle, unit) {
crowdAspd(battle, unit, 'excu2:module', num(tm.attack_speed), num(tm['trigger_cnt[equip]']));
if (sid === 'skchr_excu2_3') { // S3: +attack@atk ATK per bullet spent
battle.on('ammoUsed', (c) => {
if (c.unit !== unit || !unit.skill?.active) return;
battle.addBuff(unit, { key: 'excu2:verdict', refresh: 'stack', maxStacks: Math.max(1, num(bb['attack@max_stack_cnt'], 30)), mods: mods({ atkPct: num(bb['attack@atk']) }) });
}, { owner: unit });
}
if (sid && sid !== 'skchr_excu2_2') return;
// S2: melee attacks dodged with `prob`, each dodge refills ammo
battle.on('hit', (c) => {
if (c.target !== unit || !unit.skill?.active || !c.source || c.source.side !== 'enemy' || !c.dmg.isAttack) return;
if (!meleeAttacker(c.source, unit) || !(prob > 0) || !battle.rng.chance(prob)) return;
c.dmg.cancel = true;
battle.fx('dodge', { x: unit.x, y: unit.y, id: unit.id });
// a dodge like the engine's (listeners of the `dodge` hook — items/bonds — see it too)
if (battle.hasHook('dodge')) battle.emit('dodge', { source: c.source, target: unit, dmg: c.dmg });
unit.skill.addAmmo(refill);
}, { owner: unit });
},
};
},
};
@@ -0,0 +1,177 @@
// server/sim/content/kits/ops/chess_char_5_02-titi.js — 缇缇 (char_4056_titi) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import {
AURA_IV, AURA_DUR, RING1, num, on, talent, mods, dist, inFaction, isOp, selectedId, lazySkills, whileOn,
} from '../shared/tier5.js';
const isSargonMinos = (u) => inFaction(u, 'sargonShip', ['sargon', 'minos']);
/** Enemy not moving along its route (blocked, stunned, rooted, waiting…). */
const isStill = (e) => !!(e.blockedBy || !e.moving || e.s.flags.stun || e.s.flags.noMove || !(e.s.moveSpeed > 0));
export default {
// ---------------------------------------------------------------------------------------------------------------
// 缇缇 — S3 旧日绽放 (25 s): ATK +, 2 targets, sleeps non-sleeping targets; during the skill every enemy waking up (or
// knocked out asleep) takes arts damage growing with the sleep time (linear min_atk_scale → max_atk_scale over `sleep` s,
// PRTS 备注) and sends the highest-aggro enemy within range_radius to sleep; other allies in range fall asleep (小睡)
// instead of dying. T1 凝固的时光: can attack sleeping enemies, +15 % ATK arts vs non-moving
// enemies, sleeping enemies take 30 % ATK arts/s. T2 勇气的报偿: Sargon/Minos ops above 50 % HP get +20 ASPD.
// S1 缓蚀 (duration): ATK +, attack@prob chance per attack to sleep the target attack@sleep s.
// S2 封护 (duration): no attacks, ATK +; she and the lowest-HP-ratio operator in her range sleep until the skill ends
// (沉睡 = invulnerable), enemies around either of them keep falling asleep; T1 ×talent_scale.
chess_char_5_02_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1);
const sid = selectedId(chess, def);
const hitSleep = num(bb['attack@sleep'], num(bb.sleep, 5));
const minS = num(bb.min_atk_scale, 1), maxS = num(bb.max_atk_scale, minS);
const chainSleep = num(bb.sleep, hitSleep), radius = num(bb.range_radius, 1.5), chainN = Math.max(1, num(bb.max_target, 1));
// S2 封护: "第一天赋效果提升至N倍" while it runs
const t0Scale = (unit) => (sid === 'skchr_titi_2' && unit.skill?.active ? num(bb.talent_scale, 1) : 1);
const ward = (battle, unit, a) => {
const entered = !a.buffs.some((b) => (b.status ?? b.key) === 'sleep');
battle.addBuff(a, { key: 'titi:ward', flags: { sleep: true }, visible: true, status: 'sleep', data: { src: unit } });
battle.releaseBlocked(a);
if (battle.hasHook('statusApplied')) battle.emit('statusApplied', { source: unit, target: a, status: 'sleep', duration: Infinity, entered });
};
return {
skills: lazySkills({
skchr_titi_1: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
attack: {
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.s.flags.sleep || !battle.rng.chance(num(bb['attack@prob']))) return;
battle.applyStatus(target, 'sleep', { duration: num(bb['attack@sleep']), source: unit });
},
},
}),
skchr_titi_2: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
attack: { noAttack: true },
onStart({ battle, unit }) {
const pick = battle.alliesInGrid(unit).filter((a) => a !== unit && isOp(a) && a.hp > 0)
.sort((a, b) => a.hpRatio - b.hpRatio || a.id - b.id)[0] ?? null;
unit.mem.titiWard = pick ? [unit, pick] : [unit];
for (const a of unit.mem.titiWard) ward(battle, unit, a);
unit.mem.titiWardAcc = AURA_IV;
battle.fx('sleepGuard', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
unit.mem.titiWardAcc += dt;
if (unit.mem.titiWardAcc < AURA_IV - 1e-9) return;
unit.mem.titiWardAcc = 0;
for (const a of unit.mem.titiWard || []) {
if (!a.alive || !a.deployed || !a.findBuff('titi:ward')) continue;
for (const e of battle.foesInRadius(a.x, a.y, RING1)) if (e.alive) battle.applyStatus(e, 'sleep', { duration: AURA_DUR, source: unit });
}
},
onEnd({ battle, unit }) {
for (const a of unit.mem.titiWard || []) {
const b = a.findBuff('titi:ward');
if (b && b.data.src === unit) battle.removeBuff(a, b);
}
unit.mem.titiWard = null;
},
}),
}),
trait: { hitSleep: true }, // 凝固的时光 (a): she targets and damages sleeping enemies (沉睡 = 无敌 for everyone else)
skill: {
kind: 'duration',
mods: mods({ atkPct: num(bb.atk) }),
attack: {
maxTargets: Math.max(1, num(bb['attack@max_target'], 2)),
onHit({ battle, unit, target }) {
if (!target || !target.alive || target.s.flags.sleep) return;
battle.applyStatus(target, 'sleep', { duration: hitSleep, source: unit });
},
},
onEnd({ battle, unit }) {
for (const a of battle.allyUnits) {
const b = a.findBuff('titi:allySleep');
if (b && b.data.src === unit) battle.removeBuff(a, b);
}
},
},
talents: [
{ install(battle, unit) { // 凝固的时光 — (a) is the `hitSleep` trait above
// (b) +extra_atk_scale arts vs non-moving enemies
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg?.isAttack || c.dmg.isSplash || c.type === 'element') return;
const e = c.target;
if (e.side !== 'enemy' || !e.alive || !isStill(e)) return;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(t0.extra_atk_scale) * t0Scale(unit), type: 'arts', tags: ['talent', 'titiStill'] });
}, { owner: unit });
// (c) sleeping enemies take damage_atk_scale × ATK arts per second
whileOn(battle, unit, 1, () => {
const amount = unit.s.atk * num(t0.damage_atk_scale) * t0Scale(unit);
for (const e of battle.enemies) {
if (e.alive && !e.hidden && e.s.flags.sleep) battle.dealDamage(unit, e, { amount, type: 'arts', tags: ['talent', 'titiDream'] });
}
});
} },
{ install(battle, unit) { // 勇气的报偿: 精力充沛
whileOn(battle, unit, AURA_IV, () => {
for (const a of battle.allies()) {
if (isOp(a) && isSargonMinos(a) && a.hpRatio > num(t1.hp_ratio, 0.5)) battle.addBuff(a, { key: 'titi:vigor', duration: AURA_DUR, mods: { aspd: num(t1.attack_speed) } });
}
});
} },
],
install(battle, unit) {
if (sid && sid !== 'skchr_titi_3') return;
// S3: wake-up burst chain (sleep applied by anyone counts)
const asleep = new Map();
// PRTS 备注: "向1.5半径内除该单位外仇恨值最高的敌方单位施加沉睡" — highest aggro (taunt) first, then the nearest
const sleepOthers = (from) => {
const cands = battle.foesInRadius(from.x, from.y, radius).filter((o) => o !== from && !o.s.flags.sleep && !o.mem?.candleOwner)
.sort((a, b) => (b.s.taunt || 0) - (a.s.taunt || 0) || dist(a, from) - dist(b, from) || a.spawnSeq - b.spawnSeq);
let n = 0;
for (const o of cands) {
if (n >= chainN) break;
if (battle.applyStatus(o, 'sleep', { duration: chainSleep, source: unit })) n++;
}
};
battle.on('statusApplied', (c) => {
if (c.status === 'sleep' && c.target.side === 'enemy' && !asleep.has(c.target)) asleep.set(c.target, battle.time);
}, { owner: unit });
battle.on('tick', () => {
if (!asleep.size) return;
for (const [e, t0s] of asleep) {
if (!e.alive) { asleep.delete(e); continue; }
if (e.s.flags.sleep) continue;
asleep.delete(e);
if (!unit.skill?.active || !on(unit)) continue;
const scale = minS + (maxS - minS) * Math.min(1, (battle.time - t0s) / Math.max(0.1, chainSleep));
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'titiWake'] });
battle.fx('wake', { x: e.x, y: e.y, id: e.id, scale: Math.round(scale * 100) / 100 });
sleepOthers(e);
}
}, { owner: unit });
battle.on('kill', (c) => {
const v = c.victim;
if (v.side !== 'enemy' || !asleep.has(v)) return;
asleep.delete(v);
if (unit.skill?.active && on(unit)) sleepOthers(v);
}, { owner: unit });
// S3: other allies in range fall asleep instead of dying, until healed to full or the skill ends
battle.on('fatal', (c) => {
const a = c.unit;
if (c.prevented || a.side !== 'ally' || a === unit) return;
const guard = a.findBuff('titi:allySleep');
if (guard) { if (guard.data.src === unit) c.prevented = true; return; }
if (!isOp(a) || !unit.skill?.active || !on(unit) || !unit.rangeKeySet?.has(a.tileR * COLS + a.tileC)) return;
c.prevented = true;
const entered = !a.buffs.some((b) => (b.status ?? b.key) === 'sleep');
battle.addBuff(a, {
key: 'titi:allySleep', flags: { sleep: true }, visible: true, status: 'sleep', data: { src: unit }, interval: 0.2,
onTick: ({ unit: x, buff }) => { if (x.hp >= x.s.maxHp - 1e-6) battle.removeBuff(x, buff); },
});
// other content (特质 "干员进入沉睡时") observes it like any sleep status
if (battle.hasHook('statusApplied')) battle.emit('statusApplied', { source: unit, target: a, status: 'sleep', duration: Infinity, entered });
battle.fx('sleepGuard', { x: a.x, y: a.y, id: a.id });
}, { owner: unit, priority: 5 });
},
};
},
};
@@ -0,0 +1,166 @@
// server/sim/content/kits/ops/chess_char_5_03-blaze2.js — 烛煌 (char_1040_blaze2) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { bodyInKeys } from '../../../body.js';
import {
AURA_IV, AURA_DUR, num, on, talent, traitBb, skillGrid, batPct, mods, isOp, selectedId, lazySkills, instantKind,
skillRange, burstSpUp, elementHit, whileOn, burstDamageUp,
} from '../shared/tier5.js';
/** 烛煌 绝处重燃 "使附近的敌人晕眩" radius (tiles). PRTS 备注: 1.7. */
const NEARBY_RADIUS = 1.7;
/** 烛煌 S3 "攻击变为群体攻击": splash radius around the target (tiles). PRTS 技能3 备注 "攻击溅射半径1.7". */
const BLAZE_S3_SPLASH = 1.7;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 烛煌 — S3 众恶的焚场 (ammo): skill range 4-11 (the targets; the DEFAULT cast still needs an enemy in 3-1), ATK +,
// BAT −1.3 s, "攻击变为群体攻击" = one target + a BLAZE_S3_SPLASH (1.7) splash around it (PRTS 备注 "攻击溅射半径1.7",
// 中点判定 — so the fire reaches past the diamond; it used to hit every enemy inside it instead, community report E1
// after 0.1.0), +attack@atk_scale ATK elemental damage to every enemy of the attack in a burn burst (main and splash),
// dealt BEFORE the attack's own damage ("于攻击造成伤害前判定元素爆发并造成元素伤害": a 'hit' hook — so a hit that kills
// or starts the burst keeps / does not get it); loses 3 % max HP/s; any burn burst on the field refills ammo_recover
// bullets, never above the skill's ammo ("补充后的弹药数量无法超过上限"). Each landed bolt shows its splash (fx 'splash' r 1.7).
// T1 熔点引爆: burn burst anywhere → 350 % ATK elemental damage to it + heal 12 % max HP. T2 绝处重燃: downed instead of
// dying (6000 shield, no attack, no heal, 3 %/s regen) → revives at full HP and stuns nearby enemies.
// Module (elite): ×damage_scale vs enemies in an element burst.
// S1 炙手之援 (instant): the operator with the highest max HP in her range carries a fire aura for max_duration s —
// every `interval` s the enemies within range_radius of it take atk_scale × her ATK arts + element_multiplier × that
// damage as 灼燃损伤. S2 沸血燎原 (duration): skill range, BAT +0.9 s, ATK +, 3 targets; each attack costs
// attack@hp_ratio of her max HP and leaves a burning tile under every target: ground enemies on it move −50 % and take
// atk_scale × ATK arts + element_damage_scale × that as 灼燃损伤 per second (the tiles burn until the skill ends
// [ASSUMED: no duration in the data]). Module PRI-Y (elite): SP +0.2/s while an enemy in range is in an element burst.
chess_char_5_03_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess), tm = talent(chess, -1);
const sid = selectedId(chess, def);
const lose = num(bb.lose_hp_scale);
const burnTiles = (battle, unit) => {
const tiles = unit.mem.blazeTiles;
if (!tiles || !tiles.size) return;
unit.mem.blazeAcc2 = (unit.mem.blazeAcc2 ?? 0) + AURA_IV;
const dmgTick = unit.mem.blazeAcc2 >= 1 - 1e-9;
if (dmgTick) unit.mem.blazeAcc2 -= 1;
for (const e of battle.enemies) {
if (!e.alive || e.hidden || e.isFlying || !bodyInKeys(e, tiles)) continue;
battle.addBuff(e, { key: `blaze2:ground:${unit.id}`, duration: AURA_DUR, mods: { moveMul: Math.max(0, 1 + num(bb.move_speed)) } });
if (!dmgTick) continue;
const d = battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill', 'blazeGround'] });
if (d > 0 && e.alive) battle.dealDamage(unit, e, { type: 'element', element: 'burn', amount: d * num(bb.element_damage_scale), tags: ['skill', 'blazeGround'] });
}
};
return {
skills: lazySkills({
skchr_blaze2_1: () => ({
kind: instantKind(chess, def),
onStart({ battle, unit }) {
const t = battle.alliesInGrid(unit).filter((a) => isOp(a)).sort((a, b) => b.s.maxHp - a.s.maxHp || a.id - b.id)[0];
if (!t) return;
const r = num(bb.range_radius, 1.5);
battle.addBuff(t, {
key: `blaze2:aid:${unit.id}`, duration: num(bb.max_duration, 20), interval: Math.max(0.1, num(bb.interval, 1)), visible: true, data: { src: unit },
onTick: ({ unit: x }) => {
for (const e of battle.foesInRadius(x.x, x.y, r)) {
const d = battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill', 'blazeAid'] });
if (d > 0 && e.alive) battle.dealDamage(unit, e, { type: 'element', element: 'burn', amount: d * num(bb.element_multiplier), tags: ['skill', 'blazeAid'] });
}
},
});
battle.fx('aoe', { x: t.x, y: t.y, id: unit.id, r, skill: 'blaze2Aid' });
},
}),
skchr_blaze2_2: () => ({
kind: 'duration',
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def, { maxTargets: Math.max(1, num(bb['attack@max_target'], 3)) }),
attack: {
onEachHit({ unit, target }) {
if (target && target.side === 'enemy') unit.mem.blazeTiles?.add(Math.round(target.y) * COLS + Math.round(target.x));
},
},
onStart({ unit }) { unit.mem.blazeTiles = new Set(); unit.mem.blazeAcc2 = 0; },
onAttack({ battle, unit }) { battle.loseHp(unit, unit.s.maxHp * num(bb['attack@hp_ratio']), { source: unit }); },
onEnd({ unit }) { unit.mem.blazeTiles = null; },
}),
}),
skill: {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 18),
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : undefined,
attack: {
splashRadius: BLAZE_S3_SPLASH,
// the fire of each landed bolt (the screen rings splash attacks by sub-profession; 本源术师 is not one): once
// per attack at the main target / its spot — from the profile it was fired with, so the last bolt too
onHit({ battle, unit, target, x, y }) {
battle.fx('splash', { x, y, ...(target && target.alive ? { id: target.id } : {}), src: unit.id, r: BLAZE_S3_SPLASH, element: 'burn' });
},
},
onStart({ unit }) { unit.mem.blazeAcc = 0; },
onTick({ battle, unit, dt }) {
if (!(lose > 0)) return;
unit.mem.blazeAcc = (unit.mem.blazeAcc ?? 0) + dt;
while (unit.mem.blazeAcc >= 1 && unit.alive) {
unit.mem.blazeAcc -= 1;
battle.loseHp(unit, unit.s.maxHp * lose, { source: unit });
}
},
},
talents: [
{ install(battle, unit) { // 熔点引爆
battle.on('elementBurst', (c) => {
if (c.element !== 'burn' || !on(unit) || c.target.side !== 'enemy') return;
elementHit(battle, unit, c.target, unit.s.atk * num(t0.ep_damage_scale), 'blazeMelt', 'burn');
// 绝处重燃: "倒地期间…无法被治疗" — the meltdown heal does not reach her while downed
if (!unit.mem.downed) battle.heal(unit, unit, unit.s.maxHp * num(t0.hp_ratio), { self: true });
battle.fx('meltdown', { x: c.target.x, y: c.target.y, id: c.target.id });
}, { owner: unit });
} },
{ install(battle, unit) { // 绝处重燃
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.downed = false; }, { owner: unit });
battle.on('fatal', (c) => {
if (c.unit !== unit || c.prevented || unit.mem.downed) return;
c.prevented = true;
unit.mem.downed = true;
if (unit.skill?.active) unit.skill.end('downed');
battle.releaseBlocked(unit);
const dep = unit.deploySeq;
battle.addBuff(unit, {
key: 'blaze2:downed', visible: true, shield: num(t1.dynamic), interval: 0.2,
flags: { disarm: true, noHeal: true },
mods: mods({ hpRegenRatio: num(t1.hp_recovery_per_sec_by_max_hp_ratio), blockCnt: -99 }),
onTick: ({ unit: u, buff }) => {
if (u.deploySeq !== dep || u.hp < u.s.maxHp - 1e-6) return;
battle.removeBuff(u, buff);
u.mem.downed = false;
for (const e of battle.foesInRadius(u.x, u.y, NEARBY_RADIUS)) if (e.alive) battle.applyStatus(e, 'stun', { duration: num(t1.stun), source: u });
battle.fx('revive', { x: u.x, y: u.y, id: u.id, r: NEARBY_RADIUS });
},
});
battle.fx('downed', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit, priority: -50 });
} },
],
install(battle, unit) {
burstDamageUp(battle, unit, num(tb.damage_scale));
burstSpUp(battle, unit, 'blaze2:module', num(tm.sp_recovery_per_sec));
if (sid === 'skchr_blaze2_2') whileOn(battle, unit, AURA_IV, () => burnTiles(battle, unit));
if (sid && sid !== 'skchr_blaze2_3') return;
// S3: each hit of her skill attack (main and splash) on an enemy in a burn burst deals the bonus first (PRTS 备注).
// Gated on the attack's own isSkill (captured when the bolt was fired), not on the live skill: her bolts land after
// the last bullet's end('ammo') / an early end (downed), and those still carry it. Only S3 runs here (sid gate).
battle.on('hit', (c) => {
const d = c.dmg, t = c.target;
if (c.source !== unit || !d.isAttack || !d.isSkill || d.cancel) return;
if (t.side === 'enemy' && t.alive && t.findBuff('burnBurst')) elementHit(battle, unit, t, unit.s.atk * num(bb['attack@atk_scale']), 'blazeBurn', 'burn');
}, { owner: unit });
const maxAmmo = num(bb['attack@trigger_time'], 18);
battle.on('elementBurst', (c) => { // S3: burn bursts refill ammo, up to the skill's ammo
const sk = unit.skill;
if (c.element !== 'burn' || !sk?.active || sk.kind !== 'ammo' || !on(unit)) return;
const n = Math.min(num(bb.ammo_recover), maxAmmo - sk.ammoLeft);
if (n > 0) sk.addAmmo(n);
}, { owner: unit });
},
};
},
};
@@ -0,0 +1,59 @@
// server/sim/content/kits/ops/chess_char_5_04-bldsk.js — 华法琳 (char_171_bldsk) kit, tier 5 (hidden).
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { HALF_HP, NEVER, num, on, inRange, talent, traitBb, lowHpHealUp } from '../shared/tier5.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 华法琳 — S1 紧急包扎 (charges, attack SP): the next heal on a target below half HP adds hp_ratio × its max HP.
// T1 血液样本回收: an enemy dying in range → +2 SP to herself and to a random ally in range.
// Module (elite): heals on allies below 50 % ×1.15.
chess_char_5_04_a: (bb, chess) => {
const t0 = talent(chess, 0), tb = traitBb(chess);
const ratio = num(bb.hp_ratio);
return {
skill: {
kind: 'charges', heal: true, trigger: NEVER,
onStart({ unit }) { unit.mem.bldskBonus = unit.mem.bldskTarget ?? null; },
},
talents: [{ install(battle, unit) { // 血液样本回收
battle.on('kill', (c) => {
const v = c.victim;
if (v.side !== 'enemy' || !on(unit) || !inRange(unit, v)) return;
unit.skill?.gainSp(num(t0['bldsk_t_1[self].sp']), 'talent');
const others = battle.alliesInGrid(unit).filter((a) => a !== unit && a.skill && !a.skill.noSkill && a.skill.kind !== 'passive');
const pick = battle.rng.pick(others);
if (pick) pick.skill.gainSp(num(t0['bldsk_t_1[rand].sp']), 'talent');
}, { owner: unit });
} }],
install(battle, unit) {
// the skill only fires for a heal target below half HP: checked right before the heal
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit) return;
unit.mem.bldskNoSp = false;
const sk = unit.skill;
if (!sk || !sk.ready || unit.s.flags.silence) return;
const t = c.targets[0];
if (!t || t.side !== 'ally' || !(t.hpRatio < HALF_HP)) return;
unit.mem.bldskTarget = t;
// the heal made by the skill recovers no attack SP (engine rule for skill attacks: cost N ⇒ every N+1 heals)
if (sk.activate('DEFAULT')) unit.mem.bldskNoSp = true;
unit.mem.bldskTarget = null;
}, { owner: unit });
battle.on('spGain', (c) => {
if (c.unit !== unit || c.reason !== 'attack' || !unit.mem.bldskNoSp) return;
unit.mem.bldskNoSp = false;
c.amount = 0;
}, { owner: unit, priority: 100 });
battle.on('heal', (c) => { // merged into the same heal
if (c.source !== unit || !unit.mem.bldskBonus || c.target !== unit.mem.bldskBonus) return;
unit.mem.bldskBonus = null;
// the bonus is part of the same heal: the healer's and the target's healing multipliers apply to it too
c.amount += c.target.s.maxHp * ratio * num(unit.s.healingDealtMul, 1) * num(c.target.s.healingTakenMul, 1);
battle.fx('healAoe', { x: c.target.x, y: c.target.y, id: c.target.id, r: 0.5 });
}, { owner: unit, priority: 10 });
lowHpHealUp(battle, unit, tb);
},
};
},
};
@@ -0,0 +1,146 @@
// server/sim/content/kits/ops/chess_char_5_05-ulpia.js — 乌尔比安 (char_4145_ulpia) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { frontOf } from '../../../dir.js';
import { isHpLoss } from '../../../damage.js';
import {
RING1, num, on, inRange, talent, traitBb, skillGrid, mods, dist, isAbyssal, isOp, selectedId, lazySkills, instantKind,
permBuff, enemiesInGrid,
} from '../shared/tier5.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 乌尔比安 — S3 必须开辟的通路 (25 s, CUSTOM_RANGE row ahead): max HP/ATK +, throws an anchor forward that stops on the
// first enemy or at max distance — on his own tile while he blocks: 135 % ATK phys + 6 s stun around it
// (projectile_range); moves onto the anchor tile when deployable and not his own (a 从不混淆的方向 marker keeps his tile)
// and returns at skill end — both 【移动】, i.e. free redeploys (Battle.moveRedeploy; the return empties his SP).
// T1 本性的坚守: heal 100 (160 below 50 %) on every hit taken. T2 血脉的哺养: per kill +120 max HP / +30 ATK (×9),
// other Abyssal Hunters +50 %. Module (elite): healing received ×1.2.
// S1 必须促成的接触 (instant): the anchor lands on the best enemy of the skill range (beyond his own range, unblocked
// first) and drags up to max_target enemies around it (RING1) in front of him (中等力度), atk_scale × ATK phys each;
// a 捕网 — ground enemies only ([ASSUMED] like 雪雉's "不对空" net). S3's anchor blast has no such note: it hits air
// units too [ASSUMED].
// S2 必须维系的界限 (toggle, 持续时间无限): T1 ×talent_scale, block +1, max HP +, ATK +.
chess_char_5_05_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess);
const sid = selectedId(chess, def);
const t0Scale = sid === 'skchr_ulpia_2' ? num(bb.talent_scale, 1) : 1;
const tokenId = chess?.skill?.overrideTokenKey ?? def?.skill?.raw?.overrideTokenKey ?? 'token_10039_ulpia_block';
const trig = def?.skill?.trigger?.grid ?? chess?.skill?.trigger?.customRangeGrid ?? [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6]];
const reach = Math.max(1, ...trig.filter((p) => p[0] === 0).map((p) => p[1]));
const radius = num(bb.projectile_range, 1.5);
return {
skills: lazySkills({
skchr_ulpia_1: () => ({
kind: instantKind(chess, def),
onStart({ battle, unit }) {
// "向前方扔出船锚": the anchor goes beyond his own reach first (pulling what he already hits is moot), then
// unblocked ground enemies, then the usual target order
const list = enemiesInGrid(battle, unit, skillGrid(chess, def) ?? unit.rangeGrid);
const reach = (e) => inRange(unit, e);
// the anchor is a 捕网 (PRTS 备注 "实际效果为捕网而非拖拽"); 雪雉's 捕网 is "不对空" (PRTS 雪雉 备注) — [ASSUMED] the same
// for this one: it never lands on or catches air units (FLY, 近地悬浮, 浮空)
const main = list.find((e) => !reach(e) && !e.blockedBy && !e.isFlying) ?? list.find((e) => !e.blockedBy && !e.isFlying) ?? list.find((e) => !e.isFlying);
if (!main) return;
const near = battle.foesInRadius(main.x, main.y, RING1).filter((e) => !e.isFlying && !e.s.flags.untargetable && !e.s.flags.sleep)
.sort((a, b) => (a === main ? -1 : b === main ? 1 : 0) || dist(a, main) - dist(b, main) || a.spawnSeq - b.spawnSeq)
.slice(0, Math.max(1, num(bb.max_target, 2)));
const force = num(bb.force, 1);
battle.fx('anchor', { x: main.x, y: main.y, id: unit.id, fromX: unit.x, fromY: unit.y, r: RING1 });
for (const e of near) {
// "中等力度地拖拽至面前": the 捕网's pull uses the 拖拽 rules (PRTS 推与拉 §捕网 "力的大小:同拖拽") —
// Battle.pullToFront, the official 力度 − 重量 pull
battle.pullToFront(e, unit, force);
if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'phys', isSkill: true, tags: ['skill', 'anchorPull'] });
}
},
}),
skchr_ulpia_2: () => ({
kind: 'toggle',
mods: mods({ atkPct: num(bb.atk), hpPct: num(bb.max_hp), blockCnt: num(bb.block_cnt) }),
}),
}),
skill: {
kind: 'duration',
mods: mods({ hpPct: num(bb.max_hp), atkPct: num(bb.atk) }),
onStart({ battle, unit }) {
// PRTS 备注 ② — the anchor's target: his own tile while he blocks an enemy (e.g. just after a 突袭 landing),
// else the nearest tile ahead in the skill range (straight along his direction) with an enemy on it, else the
// farthest one. His own tile is a candidate only while he blocks ("自身所在地块(仅阻挡敌人时)"); not taken: the
// reading of the range's own tile (6-1 starts at [0,0]) as distance 0 for the second rule (a flyer over him)
let stop = 0;
if (!unit.blocking.some((e) => e.alive && e.blockedBy === unit)) {
for (let d = 1; d <= reach; d++) {
const [r, c] = frontOf(unit.tileR, unit.tileC, unit.dir, d);
// the anchor stops at the field edge and in front of a ground obstacle (crates, roadblocks)
if (!battle.grid.inRect(r, c) || battle.grid.isObstacle(r, c)) break;
stop = d;
if (battle.enemiesInKeys([r * COLS + c], unit, { canHitFly: true }).length) break;
}
}
const [sr, sc] = frontOf(unit.tileR, unit.tileC, unit.dir, stop);
const fromX = unit.x, fromY = unit.y;
battle.fx('anchor', { x: sc, y: sr, id: unit.id, fromX, fromY, r: radius });
for (const e of battle.foesInRadius(sc, sr, radius)) {
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'phys', isSkill: true, tags: ['skill', 'anchor'] });
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun), source: unit });
}
// ③ 【移动】 — only a change of tile moves him and leaves the 从不混淆的方向 ("若目标地块不为当前地块,会在原地部署"): an
// anchor on his own tile leaves him where he stands, no marker, nothing to return from [ASSUMED: the "tile one
// beyond the landing" is not tried when the landing is his own tile]
if (stop === 0 || !unit.alive) return;
// PRTS 备注: landing tile > the tile one beyond it > his own tile (a deployable, free, unreserved melee tile)
const ok = ([r, c]) => (r !== unit.tileR || c !== unit.tileC) && battle.grid.inRect(r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c);
const dest = [[sr, sc], frontOf(unit.tileR, unit.tileC, unit.dir, stop + 1)].find(ok);
if (dest == null) return;
const home = [unit.tileR, unit.tileC];
// the 【移动】 is a redeploy on the new tile (Battle.moveRedeploy: a new deployment, deploy effects fire again,
// no exit) that keeps the running skill — "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数"; the rest of
// his buffs are kept too (owner's deviation, DESIGN §22.3). The marker is deployed after the move (备注 ③)
if (!battle.moveRedeploy(unit, dest[0], dest[1]) || !unit.alive || !unit.skill?.active) return;
const marker = battle.spawnToken(unit, tokenId, home[0], home[1], { untargetable: true, kit: { skill: null, trait: { noAttack: true } } });
unit.mem.anchorHome = { r: home[0], c: home[1], marker };
battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
},
onEnd({ battle, unit }) {
const h = unit.mem.anchorHome;
unit.mem.anchorHome = null;
if (!h) return;
if (h.marker && h.marker.alive) battle.retreat(h.marker, { reason: 'expired', permanent: true });
if (unit.alive && unit.deployed) {
// ④ 【返回】: a 【移动】 back to his tile "【返回】时将清空技力,但仍可以享受后续由其他效果提供的技力" — the SP is
// emptied before the deploy effects of the return run (迅捷作战粮, 黄沙罗盘 … still give theirs, so do skillEnd
// effects after it: 迅捷). Here in onEnd the skill's mods are still on (skills.js end) [ASSUMED: officially the
// return comes "技能结束后"; none of the deploy effects that can reach him reads his stats]
const fromX = unit.x, fromY = unit.y;
if (battle.moveRedeploy(unit, h.r, h.c, { clearSp: true })) battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
}
},
},
talents: [
{ install(battle, unit) { // 本性的坚守
battle.on('damaged', (c) => {
if (c.target !== unit || !on(unit) || unit.hp <= 0 || !(c.amount > 0) || c.type === 'element' || isHpLoss(c.dmg)) return; // (not a 流失)
const v = (unit.hpRatio < num(t0.hp_ratio, 0.5) ? num(t0.value2) : num(t0.value1)) * (unit.skill?.active ? t0Scale : 1);
if (v > 0) battle.heal(unit, unit, v, { self: true });
}, { owner: unit });
} },
{ install(battle, unit) { // 血脉的哺养
battle.on('kill', (c) => {
if (c.killer !== unit || c.victim.side !== 'enemy' || !on(unit)) return;
battle.addBuff(unit, { key: 'ulpia:blood', refresh: 'stack', maxStacks: Math.max(1, num(t1.max_stack_cnt, 9)), mods: mods({ hpFlat: num(t1.max_hp), atkFlat: num(t1.atk) }) });
const share = mods({ hpFlat: num(t1['ulpia_t_1[abyssal].max_hp']), atkFlat: num(t1['ulpia_t_1[abyssal].atk']) });
if (!Object.keys(share).length) return;
for (const a of battle.allies()) {
if (a !== unit && isOp(a) && isAbyssal(a)) battle.addBuff(a, { key: 'ulpia:bloodShare', refresh: 'stack', maxStacks: Math.max(1, num(t1['ulpia_t_1[abyssal].max_stack_cnt'], 9)), mods: share });
}
}, { owner: unit });
} },
],
install(battle, unit) {
if (num(tb.heal_scale) > 0) permBuff(battle, unit, 'ulpia:module', { healingTakenMul: num(tb.heal_scale) });
},
};
},
};
@@ -0,0 +1,183 @@
// server/sim/content/kits/ops/chess_char_5_06-etlchi.js — 隐德来希 (char_4010_etlchi) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { mitigate } from '../../../damage.js';
import {
RING1, num, on, talent, talentRec, traitBb, skillGrid, mods, dist, lazySkills, instantKind, crowdAspd,
} from '../shared/tier5.js';
/** 隐德来希 S3 "每次攻击对心烛至少造成35%攻击力的伤害". */
const CANDLE_MIN_ATK = 0.35;
/** 隐德来希 S3 心烛 enemy key (hidden talent bbStr take_extra_enemy_key; not in data/enemies.json). */
const CANDLE_KEY = 'enemy_5601_entlec';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 隐德来希 — S3 灵与欲的惜别 (20 s): skill range, ATK +, ASPD +100; summons a 心烛 for up to 3 highest-HP ground enemies
// in range (60 % of their current HP, same DEF/RES); his hits on a candle deal at least 35 % ATK and the original loses
// the same HP; only he can damage candles (other allies' attacks are redirected to the original).
// T1 萃血: each hit steals 75 max HP (≤ 1350) and applies 200 arts/s for 5 s. T2 重盈: once below 25 % HP, heal 50 %
// max HP and take −10 % phys damage afterwards. Reaper trait (heal per hit, module 60) comes from the profession.
// S1 玫影觅迹 (instant, attack SP): next attack atk_scale × ATK, twice. S2 绯红壁合 (duration): no attacks; blood sickles
// on herself and on one other ground unit (the ally with the most enemies around it) cut every enemy around them (RING1)
// for atk_scale × ATK phys every `interval` s — ground enemies only unless the carrier has taken off (PRTS 备注).
// Module REA-Y (elite): ASPD +12 with ≥ 2 enemies in range.
chess_char_5_06_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess);
const hpScale = num(bb['attack@max_hp_scale'], 0.6), defScale = num(bb['attack@def_scale'], 1), resScale = num(bb['attack@magic_resistance_scale'], 1);
const nCandles = Math.max(1, num(bb['attack@max_target'], 3));
const candleKey = talentRec(chess, 2)?.bbStr?.take_extra_enemy_key ?? CANDLE_KEY;
const aroundN = (battle, a) => battle.foesInRadius(a.x, a.y, RING1).length;
return {
skills: lazySkills({
skchr_etlchi_1: () => ({ kind: instantKind(chess, def), attack: { atkScale: num(bb.atk_scale, 1), hits: 2 } }),
skchr_etlchi_2: () => ({
kind: 'duration', attack: { noAttack: true },
onStart({ battle, unit }) {
const other = battle.alliesFor(unit).filter((a) => a !== unit && a.ground && a.hp > 0)
.sort((a, b) => aroundN(battle, b) - aroundN(battle, a) || dist(a, unit) - dist(b, unit) || a.id - b.id)[0] ?? null;
unit.mem.sickles = other ? [unit, other] : [unit];
unit.mem.sickleAcc = 0;
for (const a of unit.mem.sickles) battle.fx('aoe', { x: a.x, y: a.y, id: unit.id, r: RING1, skill: 'etlchiSickle' });
},
onTick({ battle, unit, dt }) {
const iv = Math.max(0.1, num(bb.interval, 0.5));
unit.mem.sickleAcc += dt;
while (unit.mem.sickleAcc >= iv - 1e-9) {
unit.mem.sickleAcc -= iv;
for (const a of unit.mem.sickles || []) {
if (!a.alive || !a.deployed) continue;
// PRTS 备注 "被添加血镰的单位处于起飞时,血镰可对空": a sickle on the ground spares air units (FLY, 近地悬浮, 浮空);
// 起飞 = an airborne skywalker (蒂比's skills: flag `liftoff`; still a 地面单位, so she can carry one)
const air = !!a.s.flags.liftoff;
for (const e of battle.foesInRadius(a.x, a.y, RING1)) {
if (e.isFlying && !air) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'phys', isSkill: true, tags: ['skill', 'bloodSickle'] });
}
}
}
},
onEnd({ unit }) { unit.mem.sickles = null; },
}),
}),
skill: {
kind: 'duration',
mods: mods({ atkPct: num(bb.atk), aspd: num(bb.attack_speed) }),
targeting: skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : undefined,
onStart({ battle, unit }) {
unit.mem.candles = [];
const picks = battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: false })
.filter((e) => !e.isFlying && !e.mem?.candleOwner && !e.isBoss)
.sort((a, b) => b.hp - a.hp || a.spawnSeq - b.spawnSeq).slice(0, nCandles);
for (const e of picks) {
const r = Math.round(e.y), c = Math.round(e.x);
// 心烛 is not in data/enemies.json: an inline record (neutral, never attacks, never moves, not counted)
const def = {
name: '心烛', rank: 'NORMAL', applyWay: 'NONE', motion: 'WALK', notCountInTotal: true, lifePointReduce: 0,
stats: { maxHp: Math.max(1, e.hp * hpScale), atk: 0, def: e.s.def * defScale, magicResistance: e.s.res * resScale, moveSpeed: 0, massLevel: num(e.base.massLevel, 1) },
};
const cd = battle.spawnEnemy(candleKey, {
def, pos: [e.y, e.x], route: { motion: 'WALK', start: [r, c], end: [r, c], checkpoints: [] },
countInTotal: false, ownerPlayerId: e.ownerId,
});
if (!cd) continue;
cd.name = '心烛';
Object.assign(cd.base, { maxHp: Math.max(1, e.hp * hpScale), atk: 0, def: e.s.def * defScale, res: e.s.res * resScale, moveSpeed: 0 });
cd.profile = { ...(cd.profile || {}), noAttack: true };
cd.markDirty();
cd.hp = cd.s.maxHp;
cd.mem.candleOwner = unit;
cd.mem.candleOf = e;
cd.mem.noLeak = true; // never a leak at the time limit (Battle._timeout)
battle.addBuff(cd, { key: 'etlchi:candle', flags: { unblockable: true, noMove: true } });
unit.mem.candles.push(cd);
battle.fx('candle', { x: cd.x, y: cd.y, id: cd.id, of: e.id });
}
},
onEnd({ battle, unit }) {
for (const cd of unit.mem.candles || []) if (cd.alive) battle.kill(cd, null);
unit.mem.candles = [];
},
},
talents: [
{ install(battle, unit) { // 萃血
const steal = num(t0['attack@steal_hp']), cap = num(t0['attack@steal_hp_max']);
const dot = num(t0.magic_value), dotDur = num(t0.dot_duration, 5), iv = num(t0.interval, 1);
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.stolen = 0; }, { owner: unit });
battle.on('damaged', (c) => {
if (c.source !== unit || !c.dmg?.isAttack || c.type === 'element') return;
const e = c.target;
if (e.side !== 'enemy' || !e.alive || e.mem?.candleOwner || !on(unit)) return;
if (dot > 0) {
battle.addBuff(e, { key: `etlchi:dot:${unit.id}`, duration: dotDur, interval: iv, refresh: 'extend',
onTick: ({ unit: x }) => battle.dealDamage(unit, x, { amount: dot, type: 'arts', tags: ['talent', 'dot'] }) });
}
const stolen = num(unit.mem.stolen);
// (a boss sharing the match HP pool keeps its max HP: the pool is owned by the match)
const n = Math.min(steal, cap - stolen, Math.floor(e.s.maxHp - 1)); // never below 1 max HP
if (n > 0 && !e.bossPool) {
unit.mem.stolen = stolen + n;
// the victim keeps an explicit running total (a 'stack' refresh would rescale every stack by the last n)
const key = `etlchi:steal:${unit.id}`;
const lost = num(e.findBuff(key)?.data?.total) + n;
const hp0 = e.hp;
battle.addBuff(e, { key, mods: { hpFlat: -lost }, data: { total: lost } });
// 偷取生命上限: the victim's current HP only drops where it exceeds the new maximum (the engine keeps ratios)
if (e.alive) e.hp = Math.min(hp0, e.s.maxHp);
battle.addBuff(unit, { key: 'etlchi:gain', mods: { hpFlat: unit.mem.stolen } });
}
}, { owner: unit });
} },
{ install(battle, unit) { // 重盈
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.reborn = false; }, { owner: unit });
battle.on('damaged', (c) => {
if (c.target !== unit || unit.mem.reborn || !on(unit) || unit.hp <= 0 || !(unit.hpRatio < num(t1.hp_ratio))) return;
unit.mem.reborn = true;
battle.heal(unit, unit, unit.s.maxHp * num(t1['etlchi_t_2[heal].hp_ratio']), { self: true });
if (num(t1.damage_resistance) > 0) battle.addBuff(unit, { key: 'etlchi:reborn', mods: { physTakenMul: 1 - num(t1.damage_resistance) } });
battle.fx('reborn', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit });
} },
],
install(battle, unit) {
crowdAspd(battle, unit, 'etlchi:module', num(tb.attack_speed), num(tb.cnt));
const mine = (e) => e && e.mem && e.mem.candleOwner === unit;
// "心烛只受隐德来希攻击的影响": other sources' element gauges never fill a candle either
battle.on('elementHit', (c) => { if (mine(c.target) && c.source !== unit) c.dmg.cancel = true; }, { owner: unit });
battle.on('beforeStatus', (c) => { if (mine(c.target) && c.source !== unit) c.cancel = true; }, { owner: unit });
battle.on('hit', (c) => {
if (!mine(c.target)) return;
if (c.source !== unit) { c.dmg.cancel = true; return; }
if (c.dmg.type !== 'phys' && c.dmg.type !== 'arts') return;
const ss = unit.s;
const mit = mitigate(c.dmg.amount, c.dmg.type, c.target.s, {
defIgnorePct: c.dmg.defIgnorePct + ss.defIgnorePct, defIgnoreFlat: c.dmg.defIgnoreFlat + ss.defIgnoreFlat,
resIgnorePct: c.dmg.resIgnorePct + ss.resIgnorePct, resIgnoreFlat: c.dmg.resIgnoreFlat + ss.resIgnoreFlat,
});
const floor = ss.atk * CANDLE_MIN_ATK;
if (mit < floor) { c.dmg.type = 'true'; c.dmg.amount = floor; c.dmg.canDodge = false; }
}, { owner: unit, priority: -20 });
battle.on('damaged', (c) => {
if (!mine(c.target) || !(c.amount > 0) || c.type === 'element') return;
const o = c.target.mem.candleOf;
if (o && o.alive) battle.loseHp(o, c.amount, { source: unit });
}, { owner: unit });
battle.on('death', (c) => { // the original fell or leaked: its candle goes out
if (c.unit.side !== 'enemy' || c.unit.mem?.candleOwner) return;
for (const cd of unit.mem.candles || []) if (cd.alive && cd.mem.candleOf === c.unit) battle.kill(cd, null);
}, { owner: unit });
battle.on('beforeAttack', (c) => { // other allies hit the original instead of the candle
if (c.attacker === unit || c.attacker.side !== 'ally' || !(unit.mem.candles && unit.mem.candles.length)) return;
if (!c.targets.some(mine)) return;
const out = [];
for (const t of c.targets) {
if (!mine(t)) { if (!out.includes(t)) out.push(t); continue; }
const o = t.mem.candleOf;
if (o && o.alive && !out.includes(o) && !c.targets.includes(o)) out.push(o);
}
c.targets = out;
}, { owner: unit });
},
};
},
};
@@ -0,0 +1,108 @@
// server/sim/content/kits/ops/chess_char_5_07-surtr.js — 史尔特尔 (char_350_surtr) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { num, talent, traitBb, mods, selectedId, lazySkills, instantKind, whileOn, permBuff } from '../shared/tier5.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 史尔特尔 — S3 黄昏 (toggle, 持续时间无限): full heal, ATK +, range +2, 3 targets, max HP +5000 (flat), HP loss ramping
// to 20 % max HP/s over 60 s. T1 熔火: ignores 20 RES. T2 余烬: lethal damage keeps HP ≥ 1 for 8 s (不死 + 禁疗), then she
// withdraws.
// Module (elite): ASPD +8 while not blocking.
// S1 烈焰魔剑 (instant, attack SP): next attack atk_scale × ATK; a kill refills all SP at once.
// S2 熔核巨影 (duration): ATK +, range +1, 2 targets; an attack that hits a single enemy is ×critical atk_scale.
// Module AFT-Y (elite): the enemies she blocks are 法术脆弱 +10 %.
chess_char_5_07_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess);
const sid = selectedId(chess, def);
const iv = Math.max(0.05, num(bb.interval, 0.2)), peak = num(bb.hp_ratio), ramp = Math.max(0.1, num(bb.duration, 60));
const maxHp = num(bb.max_hp);
return {
skills: lazySkills({
skchr_surtr_1: () => ({
kind: instantKind(chess, def),
attack: {
atkScale: num(bb.atk_scale, 1),
onHit({ unit, target }) {
// 将目标击倒则立即恢复所有技力 (the skill is still pending here: the refill is the next charge)
if (target && target.side === 'enemy' && !target.alive && unit.skill) unit.skill.gainSp(unit.skill.spCost, 'skill');
},
},
}),
skchr_surtr_2: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
targeting: mods({ rangeExtend: Math.round(num(bb.ability_range_forward_extend)), maxTargets: num(bb['attack@max_target']) }),
}),
}),
skill: {
kind: 'toggle',
mods: mods({ atkPct: num(bb.atk), ...(Math.abs(maxHp) > 5 ? { hpFlat: maxHp } : { hpPct: maxHp }) }),
targeting: mods({ rangeExtend: Math.round(num(bb.ability_range_forward_extend)), maxTargets: num(bb['attack@max_target']) }),
onStart({ battle, unit }) {
unit.mem.twilightT = 0;
unit.mem.twilightAcc = 0;
// "立即恢复所有生命" — PRTS 技能3 备注 "(无视禁疗)": it reaches her during 余烬 too
battle.heal(unit, unit, unit.s.maxHp, { self: true, ignoreHealFree: true });
battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id, r: 1, skill: 'surtr' });
},
onTick({ battle, unit, dt }) {
if (!(peak > 0)) return;
unit.mem.twilightT += dt;
unit.mem.twilightAcc += dt;
while (unit.mem.twilightAcc >= iv && unit.alive) {
unit.mem.twilightAcc -= iv;
const rate = peak * Math.min(1, unit.mem.twilightT / ramp);
if (rate > 0) battle.loseHp(unit, unit.s.maxHp * rate * iv, { source: unit, silent: true });
}
},
},
talents: [
{ install(battle, unit) { permBuff(battle, unit, 'surtr:magma', { resIgnoreFlat: num(t0.magic_resist_penetrate_fixed) }); } },
{ install(battle, unit) { // 余烬
// PRTS 天赋备注: "持有不死的情况下不会触发此天赋" (a 不死 that prevented the blow first: `c.prevented` — 坚固维式重锤's
// lock, PRIO_REVIVE −100, runs after this −60 on the same first lethal blow [ASSUMED order], so it never starts
// while 余烬 is unused). "触发本天赋后,获得禁疗与不死": 不死 = every later lethal blow is prevented (`mem.ember`);
// 禁疗 (异常效果 HEAL_FREE "无法成为治疗类能力的目标,且受到的治疗量变为0", an HP-regen attribute excepted) = flags
// noHeal (no heal pick, no heal from others) + healFree (her own heals too; S3's start heal "无视禁疗"), shown as
// the status 'healFree' until she leaves. "强制退出战场视为撤回干员": a retreat (Battle.retreat drops the buff) — she
// lies down where she stood and redeploys there, like every operator that leaves the field (PRTS 卫戍协议/帮助
// "干员退场后…原地留下一个“倒地干员”…自动部署至该位置"; Battle.isDown, GitHub #60).
const wait = num(t1['surtr_t_2[withdraw].interval'], 8);
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.ember = false; }, { owner: unit });
battle.on('fatal', (c) => {
if (c.unit !== unit || c.prevented) return;
c.prevented = true;
if (unit.mem.ember) return;
unit.mem.ember = true;
const dep = unit.deploySeq;
battle.addBuff(unit, { key: 'surtr:ember', status: 'healFree', flags: { noHeal: true, healFree: true } });
battle.fx('ember', { x: unit.x, y: unit.y, id: unit.id });
battle.after(wait, () => { if (unit.alive && unit.deploySeq === dep) battle.retreat(unit, { reason: 'retreat' }); }, { owner: unit });
}, { owner: unit, priority: -60 });
} },
],
install(battle, unit) {
const aspd = num(tb.attack_speed);
if (aspd) {
whileOn(battle, unit, 0.2, () => {
if (!unit.blocking.length) battle.addBuff(unit, { key: 'surtr:module', duration: 0.3, mods: { aspd } });
else battle.removeBuff(unit, 'surtr:module');
});
}
const fragile = num(tb.damage_scale, 1) - 1; // AFT-Y: 自身阻挡的敌人受到10%的法术脆弱效果
if (fragile > 0) {
whileOn(battle, unit, 0.2, () => {
for (const e of unit.blocking) if (e.alive) battle.applyStatus(e, 'artsFragile', { duration: 0.3, value: fragile, source: unit });
});
}
if (sid === 'skchr_surtr_2') { // S2: 仅攻击到一个敌人时对其攻击力提升至140%
const solo = num(bb['attack@surtr_s_2[critical].atk_scale'], 1);
battle.on('beforeAttack', (c) => { if (c.attacker === unit) unit.mem.surtrSolo = !!unit.skill?.active && c.targets.filter((t) => t && t.alive).length === 1; }, { owner: unit, priority: -100 });
battle.on('hit', (c) => {
if (c.source === unit && unit.mem.surtrSolo && unit.skill?.active && c.dmg.isAttack && !c.dmg.isSplash) c.dmg.amount *= solo;
}, { owner: unit });
}
},
};
},
};
@@ -0,0 +1,143 @@
// server/sim/content/kits/ops/chess_char_5_08-horn.js — 号角 (char_4039_horn) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { bodyInRadius } from '../../../body.js';
import {
AURA_IV, AURA_DUR, num, talent, traitBb, batPct, mods, isOp, selectedId, lazySkills, instantKind, whileOn,
} from '../shared/tier5.js';
/** 号角 S2 过载: the second half of the ammo (PRTS 备注 of S3 "技能进行到一半时"; S2 counts shots). [ASSUMED] */
const HORN_S2_OVERLOAD_AT = 0.5;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 号角 — S3 终极防线 (24 s in total, a two-segment gauge — PRTS 备注: "技能进行到一半时触发额外效果"): ATK +, BAT −1.2 s
// for the first 12 s, then 过载 for the remaining damage_duration (12) s: ATK changes to +50 %, HP loss ramping to 12 %
// max HP/s at the end. T1 军事要塞: all Defenders ATK +20 % while she is on the field.
// T2 血战: once per deployment, lethal damage → full heal, max HP −50 %, ASPD +18, DEF +18 %.
// Module (elite): ×1.1 vs blocked enemies.
// S1 照明榴弹 (instant / 2 charges elite, 自动触发 ⇒ DEFAULT): next attack atk_scale × ATK; a ranged (unblocked) one also
// splashes projectile_range and lights the impact for projectile_delay_time s (enemies within projectile_range lose
// 隐匿). S2 暴风号令 (ammo 10): every attack attack@s2.atk_scale × ATK phys splash; 过载 for the second half of the
// ammo [ASSUMED like S3]: + attack@s2.magic_atk_scale × ATK arts to every enemy hit (manual close never happens in the
// auto battle). Module FOR-Y (elite): ASPD +10 while not blocking.
// S2 and S3 cast with an enemy in range (data DEFAULT, rawRule TAKE_DAMAGE): the owner's deliberate deviation from the
// official 重装 TAKE_DAMAGE row (DESIGN §21.29, tools/build-data.mjs TRIGGER_DEVIATIONS).
chess_char_5_08_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess), tm = talent(chess, -1);
const sid = selectedId(chess, def);
const s2Ammo = Math.max(1, num(bb['attack@s2.trigger_time'], 10));
const total = Math.max(0.1, num(def?.skill?.duration, num(chess?.skill?.duration, 24)));
const ovAtk = num(bb['horn_s_3[overload_start].atk'], num(bb.atk));
const ovIv = Math.max(0.05, num(bb['horn_s_3[overload_start].interval'], 0.2));
const ovPeak = num(bb['horn_s_3[overload_start].hp_ratio']);
const ovRamp = Math.max(0.1, Math.min(total, num(bb['horn_s_3[overload_start].damage_duration'], total / 2)));
const main = total - ovRamp;
const blockedMul = num(tb.atk_scale);
return {
skills: lazySkills({
skchr_horn_1: () => ({
kind: instantKind(chess, def), // 自动触发 "下次攻击": the data rule DEFAULT (an AUTO skill takes no 技能策略)
attack: {
atkScale: num(bb.atk_scale, 1),
onHit({ battle, unit, x, y }) {
if (!unit.mem.hornFlare) return;
unit.mem.hornFlare = false;
(unit.mem.flares ??= []).push({ x, y, until: battle.time + num(bb.projectile_delay_time, 6) });
battle.fx('zone', { x, y, id: unit.id, r: num(bb.projectile_range, 1.7), duration: num(bb.projectile_delay_time, 6) });
},
},
}),
skchr_horn_2: () => ({
kind: 'ammo', ammo: s2Ammo,
attack: { atkScale: num(bb['attack@s2.atk_scale'], 1) },
onStart({ unit }) { unit.mem.hornS2Over = false; },
onEnd({ unit }) { unit.mem.hornS2Over = false; },
}),
}),
skill: {
kind: 'duration', duration: total,
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
onStart({ unit }) { unit.mem.hornT = 0; unit.mem.hornAcc = 0; unit.mem.overload = false; },
onTick({ battle, unit, dt }) {
unit.mem.hornT += dt;
if (!unit.mem.overload && unit.mem.hornT >= main - 1e-9) {
unit.mem.overload = true;
const delta = ovAtk - num(bb.atk);
battle.addBuff(unit, { key: 'horn:overload', visible: true, mods: mods({ atkPct: delta }) });
battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id });
}
if (!unit.mem.overload || !(ovPeak > 0)) return;
unit.mem.hornAcc += dt;
while (unit.mem.hornAcc >= ovIv && unit.alive) {
unit.mem.hornAcc -= ovIv;
const rate = ovPeak * Math.min(1, (unit.mem.hornT - main) / ovRamp);
if (rate > 0) battle.loseHp(unit, unit.s.maxHp * rate * ovIv, { source: unit, silent: true });
}
},
onEnd({ battle, unit }) { unit.mem.overload = false; battle.removeBuff(unit, 'horn:overload'); },
},
trait: blockedMul > 1 ? { dmgMul: (b, u, t) => (t && t.blockedBy ? blockedMul : 1) } : undefined,
talents: [
{ install(battle, unit) { // 军事要塞
const atk = num(t0.atk);
if (!atk) return;
whileOn(battle, unit, AURA_IV, () => {
for (const a of battle.allies()) if (isOp(a) && a.def?.profession === 'TANK') battle.addBuff(a, { key: 'horn:fortress', duration: AURA_DUR, mods: { atkPct: atk } });
});
} },
{ install(battle, unit) { // 血战
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.bloodBattle = false; }, { owner: unit });
battle.on('fatal', (c) => {
if (c.unit !== unit || c.prevented || unit.mem.bloodBattle) return;
c.prevented = true;
unit.mem.bloodBattle = true;
battle.addBuff(unit, { key: 'horn:bloodBattle', visible: true, mods: mods({ hpMul: 1 - num(t1.max_hp), aspd: num(t1.attack_speed), defPct: num(t1.def) }) });
unit.hp = Math.max(1, unit.hp);
battle.heal(unit, unit, unit.s.maxHp * num(t1.hp_ratio, 1), { self: true });
battle.fx('bloodBattle', { x: unit.x, y: unit.y, id: unit.id });
}, { owner: unit, priority: -40 });
} },
],
install(battle, unit) {
const aspd = num(tm.attack_speed); // FOR-Y: 不阻挡敌人时…攻击速度+10
if (aspd) whileOn(battle, unit, 0.2, () => { if (!unit.blocking.length) battle.addBuff(unit, { key: 'horn:moduleY', duration: 0.3, mods: { aspd } }); });
if (sid === 'skchr_horn_1') {
// the pending shot: a ranged (unblocked) attack widens its splash and lights the impact
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !unit.skill?.pending || !c.profile) return;
unit.mem.hornFlare = !c.profile._fortressMelee;
if (unit.mem.hornFlare) c.profile.splashRadius = Math.max(num(c.profile.splashRadius), num(bb.projectile_range, 1.7));
}, { owner: unit, priority: -100 });
battle.every(AURA_IV, () => {
const fl = unit.mem.flares;
if (!fl || !fl.length) return;
unit.mem.flares = fl.filter((f) => f.until > battle.time + 1e-9);
const r = num(bb.projectile_range, 1.7);
for (const f of unit.mem.flares) for (const e of battle.enemies) {
if (e.alive && !e.hidden && bodyInRadius(e, f.x, f.y, r)) battle.applyStatus(e, 'reveal', { duration: AURA_DUR, source: unit });
}
}, { owner: unit });
}
if (sid === 'skchr_horn_2') {
const magic = num(bb['attack@s2.magic_atk_scale']);
battle.on('beforeAttack', (c) => { // 过载: the second half of the ammo
if (c.attacker !== unit || !unit.skill?.active || !c.profile) return;
const shot = s2Ammo - num(unit.skill.ammoLeft) + 1;
const over = shot > s2Ammo * HORN_S2_OVERLOAD_AT + 1e-9;
if (over && !unit.mem.hornS2Over) battle.fx('overload', { x: unit.x, y: unit.y, id: unit.id });
unit.mem.hornS2Over = over;
if (!over || !(magic > 0)) return;
// decided at the shot (a shell still in flight when the skill ends keeps it): the effective profile of this
// attack (a per-attack copy while the skill runs) carries the extra arts splash on every enemy it hits
const prev = c.profile.onEachHit;
c.profile.onEachHit = (b, u, victim, hc) => {
if (prev) prev(b, u, victim, hc);
if (victim && victim.alive && victim.side === 'enemy') b.dealDamage(unit, victim, { amount: unit.s.atk * magic, type: 'arts', isSkill: true, isSplash: true, tags: ['skill', 'hornOverload'] });
};
}, { owner: unit, priority: -100 });
}
},
};
},
};
@@ -0,0 +1,125 @@
// server/sim/content/kits/ops/chess_char_5_09-cetsyr.js — 魔王 (char_4134_cetsyr) kit, tier 5 (hidden).
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { rotateOffset } from '../../../dir.js';
import { AURA_IV, AURA_DUR, num, talent, traitBb, skillGrid, isOp, leaderOf, whileOn } from '../shared/tier5.js';
/** 魔王 T1 微尘: collision radius of one mote (PRTS 备注 0.4); orbit radius / speed come from the talent bb. */
const MOTE_HIT_RADIUS = 0.4;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 魔王 — bard (aura heal 10 % ATK/s). S3 编织重构现世 (30 s): skill range, aura 65 %, motes never vanish, other allies in
// range get 鼓舞 = +65 % of her max HP, HP of everyone in range is equalised every 2 s.
// T1 过往尘埃: 3 motes orbit her; one colliding with an operator vanishes and gives it ×1.5 aura healing for 6 s, then
// respawns after 6 s.
// T2 魔王残响: allies take −10 % damage from Sarkaz enemies. Module (elite): ≥2 other ops in range → ATK +8 %.
chess_char_5_09_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess), tm = talent(chess, -1);
const baseRatio = num(tb['attack@atk_to_hp_recovery_ratio'], 0.1);
const skillRatio = num(bb['attack@atk_to_hp_recovery_ratio'], baseRatio);
const inspire = num(bb.max_hp);
const shareIv = Math.max(0.1, num(bb['attack@cetsyr_s_3[cal_hp_ratio].interval'], 2));
const inspireAll = (battle, unit) => {
if (!(inspire > 0)) return;
const hp = unit.s.maxHp * inspire;
for (const a of battle.alliesInGrid(unit)) {
if (a !== unit) battle.addBuff(a, { key: 'cetsyr:inspire', duration: AURA_DUR, visible: true, status: 'inspire', mods: { hpFlat: hp }, data: { src: unit } });
}
};
return {
skill: {
kind: 'duration',
targeting: skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : undefined,
onStart({ battle, unit }) {
if (unit.profile) unit.profile.auraRatio = skillRatio;
unit.mem.shareAcc = 0;
unit.mem.inspireAcc = 0;
inspireAll(battle, unit);
battle.fx('healAoe', { x: unit.x, y: unit.y, id: unit.id, r: 2.5 });
},
onTick({ battle, unit, dt }) {
unit.mem.inspireAcc += dt;
if (unit.mem.inspireAcc >= AURA_IV) { unit.mem.inspireAcc = 0; inspireAll(battle, unit); }
unit.mem.shareAcc += dt;
if (unit.mem.shareAcc < shareIv - 1e-9) return;
unit.mem.shareAcc -= shareIv;
const group = battle.alliesInGrid(unit).filter((a) => a.alive && a.kind !== 'device');
let hp = 0, max = 0;
for (const a of group) { hp += a.hp; max += a.s.maxHp; }
if (!(max > 0) || group.length < 2) return;
const ratio = hp / max;
for (const a of group) a.hp = Math.max(1, Math.min(a.s.maxHp, a.s.maxHp * ratio));
battle.fx('hpShare', { x: unit.x, y: unit.y, id: unit.id, ratio: Math.round(ratio * 1000) / 1000 });
},
onEnd({ battle, unit }) {
if (unit.profile) unit.profile.auraRatio = baseRatio;
for (const a of battle.allyUnits) {
const b = a.findBuff('cetsyr:inspire');
if (b && b.data.src === unit) battle.removeBuff(a, b);
}
},
},
talents: [
{ install(battle, unit) { // 过往尘埃: cnt motes orbit her (radius range_radius, dynamic_spd °/s, PRTS hit radius 0.4)
const cnt = Math.max(0, Math.floor(num(t0.cnt, 3))), orbit = num(t0.range_radius, 1.15);
const spd = (num(t0.dynamic_spd, 30) * Math.PI) / 180;
const cd = num(t0.cooldown, 6), dur = num(t0.talent_duration, 6), mul = num(t0['attack@trait_mul'], 1.5);
const motes = new Array(cnt).fill(0); // per orbit slot: the time its mote is back (vanished → respawn after cd)
const reach = orbit + MOTE_HIT_RADIUS + 0.01;
battle.on('deploy', (c) => { if (c.unit === unit) motes.fill(0); }, { owner: unit });
whileOn(battle, unit, AURA_IV, () => {
if (!cnt) return;
const keep = !!unit.skill?.active; // S3: "微尘"不再消失
// "微尘"碰撞友方干员: operators only; a mote never collides with an operator already under a mote effect
const cands = battle.alliesInRadius(unit.x, unit.y, reach, null).filter((a) => a !== unit && isOp(a) && !a.findBuff('cetsyr:mote'));
if (!cands.length) return;
const t = battle.time - num(unit.deployedAt);
for (let k = 0; k < cnt; k++) {
if (motes[k] > battle.time + 1e-9) continue;
const ang = spd * t + (2 * Math.PI * k) / cnt;
// the orbit turns with his direction: angle 0 = straight ahead, π/2 = his left hand
const [fr, fc] = unit.fwd, [lr, lc] = rotateOffset(1, 0, unit.dir);
const mx = unit.x + orbit * (fc * Math.cos(ang) + lc * Math.sin(ang)), my = unit.y + orbit * (fr * Math.cos(ang) + lr * Math.sin(ang));
let hit = null, hd = Infinity;
for (const a of cands) {
if (a.findBuff('cetsyr:mote')) continue;
const d = Math.hypot(a.x - mx, a.y - my);
if (d <= MOTE_HIT_RADIUS + 1e-9 && d < hd) { hd = d; hit = a; }
}
if (!hit) continue;
battle.addBuff(hit, { key: 'cetsyr:mote', duration: dur, visible: true, data: { src: unit, mul } });
if (!keep) motes[k] = battle.time + cd;
battle.fx('mote', { x: hit.x, y: hit.y, id: hit.id });
}
});
battle.on('heal', (c) => {
if (c.source !== unit || !c.opts?.aura) return;
const b = c.target.findBuff('cetsyr:mote');
if (b) c.amount *= num(b.data.mul, 1);
}, { owner: unit });
} },
{ install(battle, unit) { // 魔王残响
const dr = num(t1.damage_resistance);
if (!(dr > 0)) return;
battle.on('hit', (c) => {
if (!c.target || c.target.side !== 'ally' || !c.source || c.source.side !== 'enemy') return;
if (!(c.source.def?.tags || []).includes('sarkaz') || leaderOf(battle, unit) !== unit) return;
c.dmg.mul *= 1 - dr;
}, { owner: unit });
} },
],
install(battle, unit) { // module: ≥ cnt other ops in the (base) range → ATK +8 %
const atk = num(tm.atk), need = num(tm.cnt, 2);
if (!chess?.isGolden || !atk) return;
whileOn(battle, unit, AURA_IV, () => {
const keys = new Set(unit.baseRangeKeys || []);
let n = 0;
for (const a of battle.allies()) if (a !== unit && isOp(a) && keys.has(a.tileR * COLS + a.tileC)) n++;
if (n >= need) battle.addBuff(unit, { key: 'cetsyr:module', duration: AURA_DUR, mods: { atkPct: atk } });
});
},
};
},
};
@@ -0,0 +1,64 @@
// server/sim/content/kits/ops/chess_char_5_10-lisa.js — 铃兰 (char_358_lisa) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { AURA_IV, AURA_DUR, num, talent, skillGrid, mods, isOp, lazySkills, whileOn, spAura } from '../shared/tier5.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 铃兰 — S3 狐火渺然 (27/29 s): no attacks, skill range, every enemy in range is sluggish, allies in range heal 9/11 %
// ATK per second, T2 ×scale_delta_to_one. T1 技力光环·辅助: Supporters +0.4 SP/s (highest wins).
// T2 画地为牢: sluggish enemies in range also take +20 % damage for the same time. Module (elite): +0.2 SP/s with an
// enemy in range.
chess_char_5_10_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tm = talent(chess, -1);
const boost = num(bb.scale_delta_to_one, 1);
const healRatio = num(bb['attack@atk_to_hp_recovery_ratio']);
const fragile = num(t1.damage_scale, 1) - 1;
return {
// S1 全力以赴 (duration): ATK +, ASPD +. S2 儿时的舞乐 (toggle, 持续时间无限): ATK +, 2 targets.
skills: lazySkills({
skchr_lisa_1: () => ({ kind: 'duration', mods: mods({ atkPct: num(bb.atk), aspd: num(bb.attack_speed) }) }),
skchr_lisa_2: () => ({ kind: 'toggle', mods: mods({ atkPct: num(bb.atk) }), targeting: { maxTargets: Math.max(1, num(bb['attack@max_target'], 2)) } }),
}),
skill: {
kind: 'duration',
targeting: skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : undefined,
attack: { noAttack: true },
onStart({ unit }) { unit.mem.foxSlow = AURA_IV; unit.mem.foxHeal = 0; },
onTick({ battle, unit, dt }) {
unit.mem.foxSlow += dt;
if (unit.mem.foxSlow >= AURA_IV) {
unit.mem.foxSlow = 0;
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) battle.applyStatus(e, 'sluggish', { duration: AURA_DUR, source: unit });
}
unit.mem.foxHeal += dt;
if (unit.mem.foxHeal >= 1) {
unit.mem.foxHeal -= 1;
if (!(healRatio > 0)) return;
for (const a of battle.alliesInGrid(unit)) if (a.hp < a.s.maxHp) battle.heal(unit, a, unit.s.atk * healRatio, { aura: true });
}
},
},
talents: [
{ install(battle, unit) { spAura(battle, unit, num(t0.sp_recovery_per_sec), (a) => isOp(a) && a.def?.profession === 'SUPPORT'); } },
{ install(battle, unit) { // 画地为牢 — PRTS 备注: an aura; every enemy in her range that is 停顿 (whoever caused it)
// carries the talent's 脆弱 while it stays 停顿 and in range. It is the standard 脆弱 status (same-name: highest wins).
if (!(fragile > 0)) return;
whileOn(battle, unit, AURA_IV, () => {
const v = fragile * (unit.skill?.active ? boost : 1);
for (const e of battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true })) {
if (e.alive && e.findBuff('sluggish')) battle.applyStatus(e, 'fragile', { duration: AURA_DUR, value: v, source: unit });
}
});
} },
],
install(battle, unit) { // module
const sp = num(tm.sp_recovery_per_sec);
if (!chess?.isGolden || !(sp > 0)) return;
whileOn(battle, unit, AURA_IV, () => {
if (battle.enemiesInKeys(unit.rangeKeys, unit, { canHitFly: true }).length) battle.addBuff(unit, { key: 'lisa:module', duration: AURA_DUR, mods: { spRecoveryFlat: sp } });
});
},
};
},
};
@@ -0,0 +1,111 @@
// server/sim/content/kits/ops/chess_char_5_11-demkni.js — 塞雷娅 (char_202_demkni) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import {
HALF_HP, NEVER, AURA_IV, AURA_DUR, num, on, talent, traitBb, skillGrid, mods, selectedId, lazySkills, instantKind,
whileOn, lowHpHealUp, permBuff,
} from '../shared/tier5.js';
/**
* Activate the unit's skill whenever it is ready and `cond()` holds (NEVER-trigger skills). Consecutive casts of a
* multi-charge skill are spaced by at least one attack interval (like the attack-driven DEFAULT rule).
*/
function autoCast(battle, unit, cond) {
battle.on('tick', () => {
const sk = unit.skill;
if (!sk || sk.noSkill || !on(unit) || !unit.canAct || !sk.ready || unit.s.flags.silence) return;
if (sk.active && sk.isTimed) return;
if (battle.time - sk.lastStart < unit.s.interval - 1e-9) return;
if (cond()) sk.activate('DEFAULT');
}, { owner: unit });
}
export default {
// ---------------------------------------------------------------------------------------------------------------
// 塞雷娅 — S2 药物配置 (instant, time SP): heals every ally in the skill range for heal_scale × ATK (cast when one is
// injured). T1 莱茵充能护服: every 20 s on the field ATK +5 % / DEF +4 % (×5). T2 精神回复: +1 SP to every ally she heals.
// Module (elite): heals on allies below 50 % ×1.15.
// S1 急救 (instant / 2 charges elite, 自动触发: cast at her attack when an ally of the skill area 周围 is at ≤ half HP):
// that attack is instead a heal of the lowest such ally for heal_scale × ATK.
// S3 钙质化 (duration; the 重装 strategy TAKE_DAMAGE): allies in the skill area heal
// attack@heal_scale × ATK per second; enemies there take arts ×demkni_s_3.damage_scale and move −60 %.
// Module GUA-Y (elite): damage taken −15 %.
chess_char_5_11_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess);
const sid = selectedId(chess, def);
const grid = skillGrid(chess, def) ?? [[0, 0]];
const zone = (battle, unit) => {
const at = unit.tileR * COLS + unit.tileC;
if (unit.mem.sariaAt !== at) { unit.mem.sariaAt = at; unit.mem.sariaKeys = new Set(absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0)); }
const keys = unit.mem.sariaKeys;
return battle.allies().filter((a) => keys.has(a.tileR * COLS + a.tileC) && (a === unit || !(a.s.flags.noHeal || a.profile?.noHeal)));
};
const healable = (a, unit) => a.kind !== 'device' && (a === unit || !(a.s.flags.noHeal || a.profile?.noHeal));
return {
skills: lazySkills({
skchr_demkni_1: () => ({
// 自动触发: its own rule, no 技能策略 — PRTS 备注 "此技能仅在周围有符合血量条件的友方单位时可触发,触发时会替换当次
// 攻击" and the corrected text "血量小于等于一半": checked when she is about to attack (an enemy target, the basic
// rule), an ally of the skill area at ≤ half HP casts it and that attack becomes the heal (unlike 古米's 备用军粮,
// no heal mode is left waiting: the engine withdraws a cast whose ally was healed before the attack)
kind: instantKind(chess, def),
trigger: { rule: 'DEFAULT', grid, allies: true, hpAtMost: HALF_HP },
targeting: { rangeGrid: grid },
attack: { dmgType: 'heal', heal: { mode: 'single', hpAtMost: HALF_HP }, healScale: num(bb.heal_scale, 1), projectile: 'none' },
onHit({ battle, target }) { if (target) battle.fx('healAoe', { x: target.x, y: target.y, id: target.id, r: 0.5 }); },
}),
skchr_demkni_3: () => ({
kind: 'duration',
onStart({ unit }) { unit.mem.calcAcc = 0; unit.mem.calcAura = AURA_IV; },
onTick({ battle, unit, dt }) {
unit.mem.calcAura += dt;
if (unit.mem.calcAura >= AURA_IV - 1e-9) {
unit.mem.calcAura = 0;
const m = { artsTakenMul: num(bb['demkni_s_3.damage_scale'], 1), moveMul: Math.max(0, 1 + num(bb['demkni_s_3.move_speed'])) };
for (const e of battle.unitsInGrid(unit, grid, { side: 'enemy' })) battle.addBuff(e, { key: 'saria:calcify', duration: AURA_DUR, visible: true, mods: m });
}
unit.mem.calcAcc += dt;
if (unit.mem.calcAcc < 1 - 1e-9) return;
unit.mem.calcAcc -= 1;
const amt = unit.s.atk * num(bb['attack@heal_scale']);
if (amt > 0) for (const a of battle.unitsInGrid(unit, grid, { side: 'ally' })) if (healable(a, unit) && a.hp < a.s.maxHp - 1e-6) battle.heal(unit, a, amt);
},
}),
}),
skill: {
kind: 'instant', heal: true, trigger: NEVER,
onStart({ battle, unit }) {
const amt = unit.s.atk * num(bb.heal_scale, 1);
for (const a of zone(battle, unit)) if (a.hp < a.s.maxHp - 1e-6) battle.heal(unit, a, amt);
battle.fx('healAoe', { x: unit.x, y: unit.y, id: unit.id, r: 2.5 });
},
},
talents: [
{ install(battle, unit) { // 莱茵充能护服
const iv = Math.max(1, num(t0.interval, 20)), max = Math.max(1, num(t0.max_stack_cnt, 5));
whileOn(battle, unit, 1, () => {
const n = Math.min(max, Math.floor((battle.time - unit.deployedAt + 1e-6) / iv));
const cur = unit.findBuff('saria:suit');
if (n > 0 && (!cur || cur.stacks !== n)) battle.addBuff(unit, { key: 'saria:suit', stacks: n, maxStacks: max, mods: mods({ atkPct: num(t0.atk), defPct: num(t0.def) }) });
});
} },
{ install(battle, unit) { // 精神回复
const sp = num(t1.sp);
if (!(sp > 0)) return;
battle.on('heal', (c) => {
if (c.source !== unit || c.opts?.regen || !(c.amount > 0) || !c.target.skill) return;
c.target.skill.gainSp(sp, 'talent');
}, { owner: unit, priority: -10 });
} },
],
install(battle, unit) {
// S2 only: the NEVER-trigger skill casts itself when an ally in its area is injured
if (!sid || sid === 'skchr_demkni_2') autoCast(battle, unit, () => zone(battle, unit).some((a) => a.hp < a.s.maxHp - 1e-6));
lowHpHealUp(battle, unit, tb);
if (num(tb.damage_resistance) > 0) permBuff(battle, unit, 'saria:moduleY', { dmgTakenMul: 1 - num(tb.damage_resistance) });
},
};
},
};
@@ -0,0 +1,118 @@
// server/sim/content/kits/ops/chess_char_5_12-dusk.js — 夕 (char_2015_dusk) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import { sortEnemyTargets } from '../../../targeting.js';
import {
HALF_HP, num, on, talent, talentRec, batPct, mods, selectedId, lazySkills, skillRange, moduleRangeUp,
} from '../shared/tier5.js';
/** 夕 S1 "下一次攻击溅射范围扩大" — expanded splash radius (tiles; the splash-caster default is 1.1). PRTS: "溅射半径扩大至1.7". */
const DUSK_SPLASH_RADIUS = 1.7;
/** 夕 S3 "攻击范围与溅射范围扩大" — the expanded splash radius, the same 1.7 as S1 (PRTS). [ASSUMED for S3] */
const DUSK_S3_SPLASH = DUSK_SPLASH_RADIUS;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 夕 — S1 工笔入化 (2 charges): next attack 180 % ATK with an expanded splash. T1 化境: a kill by 夕 or 小自在 → ATK +2 %
// (×15). T2 点睛: first attack after deploying summons 小自在 (25 s) on the target's tile (deployable ground).
// Module (elite): 攻击范围扩大.
// S2 泼墨淋漓 (duration): skill range, ATK +, ASPD +, hits every enemy in range (one hit each: no splash on top);
// arts damage ×damage_scale on enemies below hp_ratio. S3 写意胜形 (duration): skill range, BAT +0.4 s, prefers
// unblocked enemies, splash 1.7, ATK +; every attack summons 小自在 on the target's tile (deployable ground) or moves /
// refreshes the one already out (deployLimit 1: "召唤/刷新一个"), 25 s (the 点睛 token duration).
chess_char_5_12_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1);
const sid = selectedId(chess, def);
const tokenId = talentRec(chess, 1)?.tokenKey ?? (chess?.tokens || [])[0] ?? 'token_10015_dusk_drgn';
const tokenDur = num(t1['attack@tokenduration'], 25);
const freeGround = (battle, r, c) => battle.grid.inRect(r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c);
// 小自在 expiry follows tk.mem.duskUntil (a refresh pushes it back)
const expire = (battle, tk) => {
if (!tk.alive) return;
const left = num(tk.mem.duskUntil) - battle.time;
if (left > 1e-6) battle.after(left, () => expire(battle, tk), { owner: tk });
else battle.retreat(tk, { reason: 'expired', permanent: true });
};
/** Summon 小自在 on (r, c), or move / refresh hers when one is out. */
const summon = (battle, unit, r, c) => {
const mine = battle.allyUnits.find((x) => x.kind === 'token' && x.ownerUnit === unit && x.defId === tokenId && x.alive && x.deployed);
if (mine) {
if (mine.tileR !== r || mine.tileC !== c) {
const fromX = mine.x, fromY = mine.y;
if (!freeGround(battle, r, c) || !battle.relocate(mine, r, c)) return null;
battle.fx('teleport', { x: mine.x, y: mine.y, id: mine.id, fromX, fromY });
}
mine.mem.duskUntil = battle.time + tokenDur;
return mine;
}
if (!freeGround(battle, r, c)) return null;
const tk = battle.spawnToken(unit, tokenId, r, c);
if (!tk) return null;
tk.mem.duskUntil = battle.time + tokenDur;
battle.after(tokenDur, () => expire(battle, tk), { owner: tk });
battle.fx('summon', { x: tk.x, y: tk.y, id: tk.id, token: tokenId });
return tk;
};
return {
skills: lazySkills({
skchr_dusk_2: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk), aspd: num(bb.attack_speed) }),
targeting: skillRange(chess, def, { allInRange: true }),
attack: { splashRadius: 0 },
}),
skchr_dusk_3: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillRange(chess, def),
attack: { splashRadius: DUSK_S3_SPLASH },
}),
}),
skill: { kind: 'charges', attack: { atkScale: num(bb.atk_scale, 1), splashRadius: DUSK_SPLASH_RADIUS } },
talents: [
{ install(battle, unit) { // 化境
battle.on('kill', (c) => {
const k = c.killer;
if (!k || c.victim.side !== 'enemy' || !(k === unit || (k.kind === 'token' && k.ownerUnit === unit)) || !on(unit)) return;
battle.addBuff(unit, { key: 'dusk:realm', refresh: 'stack', maxStacks: Math.max(1, num(t0.max_stack_cnt, 15)), mods: { atkPct: num(t0.atk) } });
}, { owner: unit });
} },
{ install(battle, unit) { // 点睛
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.duskSummoned = false; }, { owner: unit });
battle.on('attack', (c) => {
if (c.attacker !== unit || unit.mem.duskSummoned) return;
const t = c.targets.find((x) => x.side === 'enemy');
if (!t) return;
unit.mem.duskSummoned = true;
// "在目标位置(可部署地面)召唤": only on the target's own tile when it is free deployable ground — PRTS 备注:
// otherwise nothing is summoned, and the talent is spent for this deployment either way
summon(battle, unit, Math.round(t.y), Math.round(t.x));
}, { owner: unit });
} },
],
install(battle, unit) {
moduleRangeUp(battle, unit, chess);
if (sid === 'skchr_dusk_2') {
const cut = num(bb.hp_ratio, HALF_HP), mul = num(bb.damage_scale, 1);
battle.on('hit', (c) => {
if (c.source !== unit || !unit.skill?.active || c.dmg.type !== 'arts' || !c.target || c.target.side !== 'enemy') return;
if (c.target.hpRatio < cut) c.dmg.mul *= mul;
}, { owner: unit });
}
if (sid === 'skchr_dusk_3') {
// 优先攻击未阻挡的敌人
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !unit.skill?.active || !c.targets.length || !c.targets[0].blockedBy) return;
const list = battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile);
sortEnemyTargets(battle, unit, list, null);
const alt = list.find((e) => !e.blockedBy);
if (alt) c.targets = [alt, ...c.targets.slice(1).filter((x) => x !== alt)];
}, { owner: unit, priority: 10 });
battle.on('attack', (c) => {
if (c.attacker !== unit || !unit.skill?.active) return;
const t = c.targets.find((x) => x.side === 'enemy');
if (t) summon(battle, unit, Math.round(t.y), Math.round(t.x));
}, { owner: unit });
}
},
};
},
};
@@ -0,0 +1,104 @@
// server/sim/content/kits/ops/chess_char_5_13-ghost2.js — 归溟幽灵鲨 (char_1023_ghost2) kit, tier 5.
// Conventions of the tier-5 kits: ../shared/tier5.js; kit contract and rules: ../README.md.
import {
AURA_IV, AURA_DUR, num, talent, traitBb, skillGrid, batPct, mods, isAbyssal, isOp, selectedId, lazySkills, whileOn,
} from '../shared/tier5.js';
export default {
// ---------------------------------------------------------------------------------------------------------------
// 归溟幽灵鲨 — dollkeeper (professions.js installDollkeeper: the 替身 form, its switch animations, 阻回). Her <替身> makes
// no normal attack (PRTS 特性备注 "<替身>不进行普通攻击") and so casts no skill (kit trait `dollNoAttack`).
// S2 生存的渴望 (15/17 s): ATK/ASPD +, HP never below 1 (PRTS 备注: 不死 — a lethal hit does not switch her meanwhile);
// when it ends she switches to the 替身 at once (PRTS 修正 "技能结束后立刻切换为<替身>", the text's 视为被击倒: no lethal
// HP loss, so no 不死 / 复活 effect takes it). T1 拥抱自我: the 替身 slows nearby enemies −40 % and deals 40 % ATK
// arts/s to them (PRTS 备注 "伤害与减速不可对空": ground enemies only) — once it fights (not during its switch
// animation). T2 阿戈尔的深邃: Abyssal Hunters in the team max HP +20 %. Module (elite): substitute ATK +15 %.
// S1 生存的技巧 (duration): swaps HP ratios with the other operator of the skill area (周围) with the lowest HP ratio,
// ATK +. S3 生存的重压 (duration): BAT +1 s, hits every blocked enemy, ATK +, max HP +; an attacked enemy whose HP ratio
// is ≥ hers takes attack@atk_scale_ex × ATK phys more, otherwise she loses attack@hp_ratio of her max HP.
// Module PUM-Y (elite): substitute max HP +20 %.
chess_char_5_13_a: (bb, chess, def) => {
const t0 = talent(chess, 0), t1 = talent(chess, 1), tb = traitBb(chess);
const sid = selectedId(chess, def);
const aroundGrid = chess?.trait?.rangeGrid ?? [[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]];
return {
skills: lazySkills({
skchr_ghost2_1: () => ({
kind: 'duration', mods: mods({ atkPct: num(bb.atk) }),
onStart({ battle, unit }) {
const t = battle.unitsInGrid(unit, skillGrid(chess, def) ?? aroundGrid, { side: 'ally' }).filter((a) => a !== unit && isOp(a) && a.hp > 0)
.sort((a, b) => a.hpRatio - b.hpRatio || a.id - b.id)[0];
if (!t) return;
const mine = unit.hpRatio, theirs = t.hpRatio;
unit.hp = Math.max(1, Math.min(unit.s.maxHp, unit.s.maxHp * theirs));
t.hp = Math.max(1, Math.min(t.s.maxHp, t.s.maxHp * mine));
battle.fx('hpShare', { x: t.x, y: t.y, id: unit.id, to: t.id });
},
}),
skchr_ghost2_3: () => ({
kind: 'duration',
mods: mods({ atkPct: num(bb.atk), hpPct: num(bb.max_hp), batPct: batPct(bb.base_attack_time, chess) }),
attack: {
hitAllBlocked: true,
onEachHit({ battle, unit, target, kind }) {
if (kind !== 'main' || !target || target.side !== 'enemy') return;
if (unit.mem.ghostHeavy?.get(target)) {
if (target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * num(bb['attack@atk_scale_ex']), type: 'phys', isSkill: true, tags: ['skill', 'ghost2Weight'] });
} else if (unit.alive) battle.loseHp(unit, unit.s.maxHp * num(bb['attack@hp_ratio']), { source: unit });
},
},
}),
}),
skill: {
kind: 'duration',
mods: mods({ atkPct: num(bb.atk), aspd: num(bb.attack_speed) }),
// "技能结束后立刻切换为<替身>": nothing when the skill ended with her (death) or by her switch to the 替身, or she
// already is one
onEnd({ battle, unit, reason }) {
if (reason === 'death' || reason === 'substitute' || !unit.alive || !unit.deployed || unit.trait.doll) return;
battle.emit('dollSwitch', { unit, reason: 'skill', done: false });
},
},
trait: { dollNoAttack: true },
talents: [
{ install(battle, unit) { // 拥抱自我 (the 替身 fighting: not during a switch animation)
const slow = num(t0.move_speed), scale = num(t0.atk_scale);
whileOn(battle, unit, AURA_IV, () => {
if (!unit.trait.doll || unit.trait.dollSwitching || !slow) return;
for (const e of battle.unitsInGrid(unit, aroundGrid, { side: 'enemy' })) if (!e.isFlying) battle.addBuff(e, { key: 'ghost2:embrace', duration: AURA_DUR, mods: { moveMul: Math.max(0, 1 + slow) } });
});
whileOn(battle, unit, 1, () => {
if (!unit.trait.doll || unit.trait.dollSwitching || !(scale > 0)) return;
for (const e of battle.unitsInGrid(unit, aroundGrid, { side: 'enemy' })) if (!e.isFlying) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', tags: ['talent', 'embrace'] });
});
} },
{ install(battle, unit) { // 阿戈尔的深邃
const hp = num(t1.max_hp);
if (!hp) return;
battle.on('battleStart', () => {
for (const a of battle.getPlayer(unit.ownerId)?.units ?? []) if (isOp(a) && isAbyssal(a)) battle.addBuff(a, { key: 'ghost2:abyss', mods: { hpPct: hp }, persist: true, allowDead: true });
}, { owner: unit });
} },
],
install(battle, unit) {
// S2: HP never below 1 while it runs
if (!sid || sid === 'skchr_ghost2_2') battle.on('fatal', (c) => { if (c.unit === unit && unit.skill?.active) c.prevented = true; }, { owner: unit, priority: 10 });
if (sid === 'skchr_ghost2_3') { // S3: 生命比例高于或等于自身 — judged when the attack starts
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !unit.skill?.active) return;
unit.mem.ghostHeavy = new Map(c.targets.filter(Boolean).map((t) => [t, t.hpRatio >= unit.hpRatio - 1e-9]));
}, { owner: unit, priority: -100 });
}
const hpUp = num(tb.max_hp); // PUM-Y: 替身…生命值提升
if (hpUp) whileOn(battle, unit, 0.2, () => { if (unit.trait.doll) battle.addBuff(unit, { key: 'ghost2:moduleY', duration: 0.3, mods: { hpPct: hpUp } }); });
const atk = num(tb.atk);
if (atk) {
whileOn(battle, unit, 0.2, () => {
if (unit.trait.doll) battle.addBuff(unit, { key: 'ghost2:module', duration: 0.3, mods: { atkPct: atk } });
});
}
},
};
},
};
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff. Show more