Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_6_13-angel2.js
T
sganggs 1169102d46 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.
2026-10-05 16:13:56 +08:00

181 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/chess_char_6_13-angel2.js — 新约能天使 (char_1041_angel2) kit, tier 6.
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
import { sortEnemyTargets, aggroCmp } from '../../../targeting.js';
import { summonToken, TOKEN_IDS } from '../../tokens.js';
import { num, bv, tbb, live, hasBond, ANY, isTok, selectedSkill, batOf, aura } from '../shared/tier6.js';
/** Barrier of `total` HP decaying linearly to 0 over `dur` seconds. */
function decayingShield(battle, u, key, total, dur) {
if (!(total > 0)) return null;
const decays = dur > 0;
return battle.addBuff(u, {
key, shield: total, duration: decays ? dur : Infinity, visible: true, interval: decays ? 0.5 : 0,
onTick: decays ? ({ unit, buff }) => { buff.shield = Math.max(0, buff.shield - (total * 0.5) / dur); unit.markDirty(); } : null,
});
}
// ------------------------------------------------------------------------------------------------------------------
// 新约能天使 chess_char_6_13 (怪杰) — S2 开火成瘾症; 火力电台; 铳弹协约; module 新朋友圣城生活套组
// S2 (PRTS 备注): the ally is the friendly operator of her attack range with the highest 仇恨值 ("选择的友方干员为攻击范围内
// 仇恨值最高的我方干员": highest taunt level, then the latest deployed — targeting.js aggroCmp, the order enemies attack
// in; it used to be the highest ASPD, so the shield often went to a back-row shooter instead of the operator in front);
// both barriers are shield_max_hp_ratio × the holder's OWN max HP ("获得的屏障均以自身生命上限为标准计算"), losing
// initial/shield_max_duration per second ("屏障每秒衰减量为:初始屏障量/30"); a new one replaces the old one ("重复获得此
// 屏障时,重置屏障量与衰减速度").
const AIRSTRIKE_RADIUS = 1; // [ASSUMED] bombardment splash radius (not in data)
function angel2(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {};
const sid = selectedSkill(chess, def);
const steal = num(bb.steal), extra = Math.floor(num(bb.addtional_ammo_each));
const shieldRatio = num(bb.shield_max_hp_ratio), shieldDur = num(bb.shield_max_duration);
const coordId = (chess?.tokens || []).find((t) => /angel2_target/.test(String(t))) || TOKEN_IDS.deliveryTarget;
const perAttack = 5; // S3 "每次攻击消耗5发" (the 5 连击)
const coord = (battle, unit) => battle.allyUnits.find((t) => isTok(t, coordId, unit) && t.alive) ?? null;
const placeCoord = (battle, unit) => {
unit.mem.angelCoord = false;
if (coord(battle, unit)) return;
const t = summonToken(battle, unit, coordId, 'melee');
if (t) battle.fx('anchor', { x: t.x, y: t.y, id: t.id, src: unit.id });
};
const skills = {
// S1 天空大扫除: 8 bullets at attack@atk_scale × ATK, flyers first; stopped by hand ⇒ the remaining bullets are fired
// at random enemies of her range
skchr_angel2_1: {
kind: 'ammo',
ammo: Math.max(1, Math.floor(num(bb['attack@trigger_time'], 8))),
attack: { atkScale: num(bb['attack@atk_scale'], 1) },
targeting: { priority: 'fly' },
onAttack({ unit, skill }) { unit.mem.angelLeft = skill.ammoLeft - 1; },
onEnd({ battle, unit, skill, reason }) {
const left = Math.max(0, Math.floor(num(unit.mem.angelLeft)));
unit.mem.angelLeft = 0;
if (reason !== 'stopped' || !unit.alive || !left) return;
for (let i = 0; i < left; i++) {
const c = battle.enemiesInKeys(unit.rangeKeys, unit, ANY);
const e = battle.rng.pick(c);
if (!e) break;
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['attack@atk_scale'], 1), type: 'phys', isAttack: true, isSkill: true, tags: ['skill', 'volley'] });
battle.emit('ammoUsed', { unit, left: left - i - 1, skill });
}
},
},
// S3 使命必达!: ATK +atk; attacks are 5 hits of attack@atk_scale × ATK spending 5 of the 50 bullets; at the start,
// with a 投递坐标 (placed after each deployment, install): attack@cannon_atk_scale × ATK physical splash there and the
// knocked-out ground operator with the longest remaining redeploy time lands on it with attack@sp SP
skchr_angel2_3: {
kind: 'ammo',
ammo: Math.max(1, Math.floor(num(bb['attack@trigger_time'], 50))),
mods: { atkPct: num(bb.atk) },
attack: { atkScale: num(bb['attack@atk_scale'], 1), hits: perAttack },
onStart({ battle, unit }) {
if (unit.mem.angelCoord) placeCoord(battle, unit);
const c = coord(battle, unit);
if (!c) return;
const r = c.tileR, col = c.tileC;
battle.fx('airstrike', { x: col, y: r, id: unit.id, r: AIRSTRIKE_RADIUS });
for (const e of battle.foesInRadius(col, r, AIRSTRIKE_RADIUS)) {
if (e.alive && !e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['attack@cannon_atk_scale'], 1), type: 'phys', isSkill: true, isSplash: true, tags: ['skill', 'delivery'] });
}
const waiting = battle.allyUnits.filter((a) => a.kind === 'op' && a.ownerId === unit.ownerId && !a.alive && !a.removed && a !== unit
&& a.def?.position !== 'RANGED' && battle.grid.canStand(r, col, { ranged: false }));
if (!waiting.length) return;
waiting.sort((a, b) => (b.respawnAt ?? 0) - (a.respawnAt ?? 0) || b.base.respawnTime - a.base.respawnTime || a.id - b.id);
const a = waiting[0];
battle.retreat(c, { reason: 'expired', permanent: true });
if (battle.redeploy(a, { free: true, tile: [r, col] })) {
if (a.skill) a.skill.gainSp(num(bb['attack@sp']), 'skill');
battle.fx('appear', { x: col, y: r, id: a.id, src: unit.id });
}
},
onAttack({ battle, unit, skill }) { // bullets 2..5 of the 5 连击 (the engine spends the 1st)
for (let i = 1; i < perAttack && skill.ammoLeft > 1; i++) {
skill.ammoLeft--;
battle.emit('ammoUsed', { unit, left: skill.ammoLeft, skill });
}
},
},
};
return {
skills,
install(battle, unit) {
if (sid === 'skchr_angel2_1') { // bullets loaded at the start (after every skillStart ammo bonus: 逃犯引渡手续 …)
battle.on('skillStart', ({ unit: u, skill }) => { if (u === unit) unit.mem.angelLeft = skill.ammoLeft; }, { owner: unit, priority: -500 });
}
if (sid !== 'skchr_angel2_3') return;
// "部署后获得投递坐标": one coordinate per deployment, placed (the player's choice in the original) on a ground tile
// of her range on the enemy path next to the first enemy that enters her range (at the latest when S3 starts)
battle.on('deploy', ({ unit: u }) => { if (u === unit) unit.mem.angelCoord = !coord(battle, unit); }, { owner: unit });
battle.on('tick', () => {
if (!unit.mem.angelCoord || !live(unit) || !battle.enemiesInKeys(unit.rangeKeys, unit, ANY).length) return;
placeCoord(battle, unit);
}, { owner: unit });
},
skill: {
kind: 'ammo',
ammo: Math.max(1, Math.floor(num(bb['attack@trigger_time'], 10))),
mods: { batPct: batOf(bb.base_attack_time, def) },
attack: { atkScale: num(bb['attack@atk_scale'], 1) },
onStart({ battle, unit, skill }) {
const cands = battle.alliesInGrid(unit).filter((a) => a !== unit && a.kind === 'op' && live(a));
cands.sort(aggroCmp);
const victim = steal > 0 ? cands[0] ?? null : null;
unit.mem.angelVictim = victim;
decayingShield(battle, unit, 'angel2:barrier', unit.s.maxHp * shieldRatio, shieldDur);
if (victim) {
battle.addBuff(victim, { key: 'angel2:stolen', mods: { aspd: -steal }, source: unit, visible: true });
battle.addBuff(unit, { key: 'angel2:steal', mods: { aspd: steal } });
decayingShield(battle, victim, 'angel2:barrier', victim.s.maxHp * shieldRatio, shieldDur);
if (extra > 0) skill.addAmmo(extra);
battle.fx('steal', { x: victim.x, y: victim.y, id: victim.id, src: unit.id });
}
},
onEnd({ battle, unit }) {
const v = unit.mem.angelVictim;
unit.mem.angelVictim = null;
if (v) battle.removeBuff(v, 'angel2:stolen');
battle.removeBuff(unit, 'angel2:steal');
},
},
talents: [
{ install(battle, unit) { // 火力电台
const hr = num(t0.hp_ratio), prob = num(t0.prob), sc = num(t0.aoe_atk_scale, num(t0.damage_scale));
battle.on('ammoUsed', ({ unit: u }) => {
if (!live(unit) || !u || u.side !== 'ally') return;
if (hr > 0) battle.heal(unit, unit, unit.s.maxHp * hr, { self: true, silent: true });
if (!(prob > 0) || !(sc > 0) || battle.rng() >= prob) return;
const cands = battle.enemiesInKeys(u.rangeKeys || [], u, ANY);
if (!cands.length) return;
sortEnemyTargets(battle, u, cands, null);
const c = cands[0];
battle.fx('airstrike', { x: c.x, y: c.y, id: unit.id, r: AIRSTRIKE_RADIUS });
for (const e of battle.foesInRadius(c.x, c.y, AIRSTRIKE_RADIUS, true)) { // splash around the target: 中点判定
if (e.alive && !e.s.flags.untargetable) battle.dealDamage(unit, e, { amount: unit.s.atk * sc, type: 'phys', isSkill: true, isSplash: true, tags: ['talent', 'airstrike'] });
}
}, { owner: unit });
} },
{ install(battle, unit) { // 铳弹协约
const atk = num(t1.atk), mult = num(t1.mult, 1);
if (atk) aura(battle, unit, 0.5, () => {
for (const a of battle.allyUnits) {
if (!live(a) || a.kind !== 'op' || !a.skill || a.skill.kind !== 'ammo') continue;
battle.addBuff(a, { key: 'angel2:covenant', mods: { atkPct: atk * (hasBond(a, 'lateranoShip') ? mult : 1) }, duration: 0.75, refresh: 'replace' });
}
});
} },
{ install(battle, unit) { // elite module: HP > 80 % ⇒ SP +0.25/s
const thr = bv(tb, 'hp_ratio', 0), spr = bv(tb, 'sp_recovery_per_sec', 0);
if (spr > 0 && tb['angel2_tr[e].hp_ratio'] != null) aura(battle, unit, 0.25, () => {
if (unit.hpRatio > num(tb['angel2_tr[e].hp_ratio'], thr)) battle.addBuff(unit, { key: 'angel2:calm', mods: { spRecoveryFlat: spr }, duration: 0.4, refresh: 'replace' });
});
} },
],
};
}
export default {
chess_char_6_13_a: angel2,
};