feedback5: merge K3 — stand-in kits 郁金香, Mechanist, Touch (Touch shares the 外勤医疗 code; every skill on every form)
# Conflicts: # server/sim/content/kits/index.js
This commit is contained in:
commit
acbb70dc6b
9 files changed
+1058
-11
No files matched your search
@@ -126,6 +126,9 @@ stand-in's, `def.standInFor` the replaced operator's charId.
|
||||
stand-ins is `tools/build-data.mjs STANDIN_TRIGGER_DEVIATIONS` (预备干员-重装, Mechanist: every skill `DEFAULT`,
|
||||
`rawRule` `TAKE_DAMAGE`).
|
||||
- **Summons**: no stand-in has one (DATA.md §18).
|
||||
- **One character, one implementation**: a stand-in the mode also fields elsewhere reuses that code through a named
|
||||
export, never through another registry: Touch (`standin-acmedc.js`) runs the 外勤医疗 strategy's Touch S3 and talents,
|
||||
`content/tokens.js` `touchGospel` / `mapCharTalents`.
|
||||
- `node --test test/content/kits_layout.test.js` checks the file name, the key (a charId of data/backups.json `units`)
|
||||
and the registry entry.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export const KIT_FILES = Object.freeze([
|
||||
* A stand-in without a file fights with the generic kit (content/generic.js) plus its unconditional stat talents
|
||||
* (genericTalents) — the eight 预备干员 need no file.
|
||||
*/
|
||||
export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sharp2.js', 'standin-acspec.js']);
|
||||
export const STANDIN_KIT_FILES = Object.freeze(['standin-acguad.js', 'standin-sharp2.js', 'standin-acspec.js', 'standin-acmedc.js', 'standin-acfend.js', 'standin-acpion.js']);
|
||||
|
||||
async function loadKitFile(file) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// server/sim/content/kits/ops/standin-acfend.js — Mechanist (char_610_acfend) 补位 stand-in kit: 4 星熊 (S2), 5 号角 /
|
||||
// 6 余 (S3 + module PRO-X on the elite). Stand-in contract and rules: ../README.md "Stand-in kits".
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
|
||||
|
||||
const S1 = 'skchr_acfend_1', S2 = 'skchr_acfend_2', S3 = 'skchr_acfend_3';
|
||||
/** 反馈装甲 on an enemy she blocks, and its S2 version (×talent_mult): one buff per target whatever applies it (同名). */
|
||||
const FEEDBACK_KEY = 'acfend:feedback';
|
||||
const FEEDBACK_S2_KEY = 'acfend:feedback2';
|
||||
/** PRTS Mechanist(卫戍协议) 天赋备注: 精研材料's HP ratio and 反馈装甲's block are checked every 0.1 s. */
|
||||
const CHECK_INTERVAL = 0.1;
|
||||
/** S3 应力倒置 "每秒…": one pulse per second of the skill, the first 1 s after the cast [ASSUMED: no pulse at the cast]. */
|
||||
const STRESS_INTERVAL = 1;
|
||||
|
||||
export default {
|
||||
// Mechanist (铁卫, blocks 3) — numbers from data/backups.json (normal E2 Lv1 rank 4, elite E2 Lv60 rank 7; skill_table and
|
||||
// PRTS Mechanist(卫戍协议) agree):
|
||||
// 精研材料: DEF +def; below hp_ratio HP another +`acfend_t_1[extra].def` (module PRO-X Lv3: 0.2) — PRTS 备注 "生命比例每0.1秒
|
||||
// 检测一次".
|
||||
// 反馈装甲: enemies she blocks ASPD `attack_speed` (−7). PRTS 备注: applied every 0.1 s to the enemies she blocks; then the
|
||||
// TARGET checks every 0.1 s whether anyone still blocks it — it lasts until nobody blocks it (whoever blocked it), her
|
||||
// S2 starts, or her S2 ends (a buff with its own 0.1 s check here, so it outlives her knock-out the same way).
|
||||
// S1 结构稳定 (40 s): max HP +max_hp, DEF +def.
|
||||
// S2 不变性原理 (20 s): DEF +def, 反馈装甲 ×talent_mult (−14 / −21). PRTS 备注: at the cast the talent's effect on the
|
||||
// enemies she blocks ends and the enhanced one is applied; at the end every enhanced one she applied ends (the
|
||||
// 0.1 s talent check then gives the enemies she still blocks the plain one again).
|
||||
// S3 应力倒置 (30 s): DEF +def, block +block_cnt, every second ATK × atk_scale arts damage to each enemy she blocks.
|
||||
// Module PRO-X (elite, trait addition): "阻挡敌人时防御力+20%" — DEF +def (trait blackboard) while she blocks.
|
||||
// Triggers (data, nothing set here): every skill DEFAULT with the official TAKE_DAMAGE as rawRule — the owner's 重装
|
||||
// exception for the stand-ins (tools/build-data.mjs STANDIN_TRIGGER_DEVIATIONS): she casts with an enemy in range.
|
||||
char_610_acfend: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1), tb = traitBb(chess);
|
||||
const r1 = skillRec(chess, S1), r2 = skillRec(chess, S2), r3 = skillRec(chess, S3);
|
||||
const fbAspd = num(t1.attack_speed, 0);
|
||||
const fbMult = num(r2?.bb?.talent_mult, 1);
|
||||
/** Her S2 runs (the enhanced 反馈装甲). */
|
||||
const s2On = (unit) => !!(unit.skill && unit.skill.active && unit.skill.id === S2);
|
||||
/** A 反馈装甲 buff: it ends at the first 0.1 s check of its target that finds nobody blocking it. */
|
||||
const feedback = (battle, unit, e, key, aspd) => battle.addBuff(e, {
|
||||
key, mods: { aspd }, source: unit, data: { v: aspd }, tags: ['talent'], interval: CHECK_INTERVAL,
|
||||
onTick({ battle: b, unit: t, buff }) { if (!t.blockedBy) b.removeBuff(t, buff); },
|
||||
});
|
||||
/** Apply 反馈装甲 (its S2 version while S2 runs) to every enemy she blocks — the talent's 0.1 s application. */
|
||||
const applyFeedback = (battle, unit) => {
|
||||
if (!fbAspd || !up(unit)) return;
|
||||
const enhanced = s2On(unit);
|
||||
for (const e of unit.blocking) {
|
||||
if (!e.alive || e.blockedBy !== unit) continue;
|
||||
if (enhanced) {
|
||||
if (e.findBuff(FEEDBACK_S2_KEY)) continue;
|
||||
battle.removeBuff(e, FEEDBACK_KEY);
|
||||
feedback(battle, unit, e, FEEDBACK_S2_KEY, fbAspd * fbMult);
|
||||
} else if (!e.findBuff(FEEDBACK_S2_KEY) && !e.findBuff(FEEDBACK_KEY)) {
|
||||
feedback(battle, unit, e, FEEDBACK_KEY, fbAspd);
|
||||
}
|
||||
}
|
||||
};
|
||||
const skills = {};
|
||||
if (r1) skills[S1] = { kind: 'duration', mods: { hpPct: num(r1.bb?.max_hp), defPct: num(r1.bb?.def) } };
|
||||
if (r2) {
|
||||
skills[S2] = {
|
||||
kind: 'duration',
|
||||
mods: { defPct: num(r2.bb?.def) },
|
||||
onStart({ battle, unit }) { applyFeedback(battle, unit); },
|
||||
onEnd({ battle, unit }) {
|
||||
for (const e of battle.enemies) {
|
||||
const b = e.findBuff(FEEDBACK_S2_KEY);
|
||||
if (b && b.source === unit) battle.removeBuff(e, b);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
if (r3) {
|
||||
const scale = num(r3.bb?.atk_scale, 0);
|
||||
skills[S3] = {
|
||||
kind: 'duration',
|
||||
mods: { defPct: num(r3.bb?.def), blockCnt: num(r3.bb?.block_cnt) },
|
||||
onStart({ unit }) { unit.mem.acfendStress = 0; },
|
||||
onTick({ battle, unit, dt }) {
|
||||
unit.mem.acfendStress = (unit.mem.acfendStress ?? 0) + dt;
|
||||
while (unit.mem.acfendStress + 1e-6 >= STRESS_INTERVAL) {
|
||||
unit.mem.acfendStress -= STRESS_INTERVAL;
|
||||
const v = battle.blockedTargets(unit, { canHitFly: true });
|
||||
if (v.length) battle.fx('aoe', { x: unit.x, y: unit.y, id: unit.id, radius: 0.5, skill: S3 });
|
||||
for (const e of v) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'arts', isSkill: true, tags: ['skill', 'stress'] });
|
||||
}
|
||||
},
|
||||
onEnd({ unit }) { unit.mem.acfendStress = 0; },
|
||||
};
|
||||
}
|
||||
return {
|
||||
skills,
|
||||
talents: [
|
||||
{ install(battle, unit) { // 精研材料
|
||||
statBuff(battle, unit, 'acfend:t1', { defPct: num(t0.def) });
|
||||
const extra = num(t0['acfend_t_1[extra].def']), ratio = num(t0.hp_ratio, 0);
|
||||
if (!extra || !(ratio > 0)) return;
|
||||
const check = () => {
|
||||
const want = up(unit) && unit.hpRatio < ratio - 1e-9;
|
||||
const has = !!unit.findBuff('acfend:t1:extra');
|
||||
if (want && !has) battle.addBuff(unit, { key: 'acfend:t1:extra', mods: { defPct: extra }, tags: ['talent'] });
|
||||
else if (!want && has) battle.removeBuff(unit, 'acfend:t1:extra');
|
||||
};
|
||||
battle.every(CHECK_INTERVAL, check, { owner: unit });
|
||||
} },
|
||||
{ install(battle, unit) { // 反馈装甲
|
||||
if (fbAspd) battle.every(CHECK_INTERVAL, () => applyFeedback(battle, unit), { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
const md = num(tb.def, 0); // module PRO-X: 阻挡敌人时防御力+20%
|
||||
if (md) toggleBuff(battle, unit, 'acfend:pro', () => unit.blocking.length > 0, { defPct: md });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
// server/sim/content/kits/ops/standin-acmedc.js — Touch (char_613_acmedc) 补位 stand-in kit: 5 塞雷娅, 6 塑心 / 纯烬艾雅法拉
|
||||
// (all S3 恳切福音 + module PHY-X on the elite). Stand-in contract and rules: ../README.md "Stand-in kits".
|
||||
|
||||
import { num, traitBb, skillRec } from '../shared/tier1.js';
|
||||
import { mapCharTalents, touchGospel } from '../../tokens.js';
|
||||
|
||||
const S1 = 'skchr_acmedc_1', S2 = 'skchr_acmedc_2', S3 = 'skchr_acmedc_3';
|
||||
/** S2 宛如天启's "所有医疗干员攻击力+X%" on every medic operator of hers while the skill runs (one per target: 同名 buff). */
|
||||
const APOCALYPSE_KEY = 'acmedc:apocalypse';
|
||||
|
||||
const isMedicOp = (a) => a.kind === 'op' && a.alive && a.deployed && !a.hidden && a.def?.profession === 'MEDIC';
|
||||
|
||||
export default {
|
||||
// Touch (医师) — skills from data/backups.json (normal E2 Lv1 skill rank 4, elite E2 Lv60 rank 7; skill_table and PRTS
|
||||
// Touch(卫戍协议) agree):
|
||||
// S1 慨赠 (20 s): ATK +atk, each heal has an `attack@prob` chance of one extra heal [ASSUMED: of the same target, the
|
||||
// same amount — the text names no other target].
|
||||
// S2 宛如天启 (30 s): her range becomes the 3-10 grid (PRTS 修正: "攻击距离+1" means 攻击范围扩大), she heals
|
||||
// `attack@max_target` targets, every 医疗 operator of hers ATK +`attack@atk` herself included [ASSUMED: her own
|
||||
// player's operators, as 余's "全场所有干员"; summons not].
|
||||
// S3 恳切福音 (40 s): content/tokens.js touchGospel — the one implementation the 外勤医疗 strategy's Touch runs too
|
||||
// (5-2 grid, ATK +atk, 2 targets, an extra heal of 30 % of the main one on the target or an orthogonal neighbour,
|
||||
// ×heal_scale on allies at ≤ hp_ratio HP).
|
||||
// Talents 攫升 / 超脱: content/tokens.js mapCharTalents (the strategy's Touch too); the module's 超脱 upgrade (8 SP at
|
||||
// module Lv3) comes with the record's talents.
|
||||
// Module PHY-X (elite, trait addition): "治疗生命值低于50%的友方单位时治疗量提升15%" — every heal of hers on an ally
|
||||
// below `hp_ratio` HP (strictly: "低于") ×`heal_scale` (the trait blackboard). With S3's ×1.35 / ×1.40 it
|
||||
// multiplies (×1.61 at Lv7): both are `heal_scale` blackboards, the 治疗倍率, and PRTS 游戏数据基础 §倍率 says "同种
|
||||
// 倍率间叠乘" — no Touch-specific note or buff template exists. It runs before S3's hook, so S3's 30 % extra heal is
|
||||
// a share of the main heal with both bonuses and, like S3's ×1.35 / ×1.40 in touchGospel, never multiplies that
|
||||
// share again [ASSUMED].
|
||||
// Triggers (data, nothing set here): S1 DEFAULT (an injured ally in her range); S2 / S3 ACTIVE_RANGE — the owner's
|
||||
// rule of 2026-10-05 read for a heal skill: an injured ally inside the running 3-10 / 5-2 range casts.
|
||||
char_613_acmedc: (bb, chess, def) => {
|
||||
const tb = traitBb(chess);
|
||||
const phyScale = num(tb.heal_scale, 1), phyRatio = num(tb.hp_ratio, 0);
|
||||
const r1 = skillRec(chess, S1), r2 = skillRec(chess, S2), r3 = skillRec(chess, S3);
|
||||
const gospel = r3 ? touchGospel(r3.bb ?? {}, r3) : null;
|
||||
const skills = {};
|
||||
if (r1) {
|
||||
const b1 = r1.bb ?? {};
|
||||
const p = num(b1['attack@prob'], 0);
|
||||
skills[S1] = {
|
||||
kind: 'duration', heal: true,
|
||||
mods: { atkPct: num(b1.atk) },
|
||||
onHit({ battle, unit, target, heal }) {
|
||||
if (!(p > 0) || !target || !(heal > 0) || !battle.rng.chance(p)) return;
|
||||
battle.heal(unit, target, heal);
|
||||
battle.fx('heal', { x: target.x, y: target.y, id: unit.id });
|
||||
},
|
||||
};
|
||||
}
|
||||
if (r2) {
|
||||
const b2 = r2.bb ?? {};
|
||||
const v = num(b2['attack@atk'], 0);
|
||||
const grant = (battle, unit) => {
|
||||
for (const a of battle.alliesFor(unit, unit.ownerId)) {
|
||||
if (!isMedicOp(a)) continue;
|
||||
const cur = a.findBuff(APOCALYPSE_KEY);
|
||||
if (cur && (cur.source === unit || (cur.data?.v ?? 0) >= v)) continue; // another Touch's equal or stronger one
|
||||
battle.addBuff(a, { key: APOCALYPSE_KEY, mods: { atkPct: v }, source: unit, data: { v }, tags: ['skill'] });
|
||||
}
|
||||
};
|
||||
skills[S2] = {
|
||||
kind: 'duration', heal: true,
|
||||
targeting: { rangeGrid: r2.rangeGrid, maxTargets: Math.max(1, Math.floor(num(b2['attack@max_target'], 2))) },
|
||||
onStart({ battle, unit }) { if (v) grant(battle, unit); },
|
||||
// medics deployed (or redeployed) while it runs get it too
|
||||
onTick({ battle, unit }) { if (v) grant(battle, unit); },
|
||||
onEnd({ battle, unit }) {
|
||||
for (const a of battle.allyUnits) if (a.findBuff(APOCALYPSE_KEY)?.source === unit) battle.removeBuff(a, APOCALYPSE_KEY);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (gospel) skills[S3] = gospel.skill;
|
||||
return {
|
||||
skills,
|
||||
talents: mapCharTalents(def),
|
||||
install(battle, unit) {
|
||||
if (phyScale !== 1 && phyRatio > 0) {
|
||||
battle.on('heal', (ctx) => {
|
||||
// (S3's extra heal is a share of the main heal, which already carries it: touchGospel's `touchExtra`)
|
||||
if (ctx.source !== unit || !ctx.target || ctx.opts?.regen || unit.mem.touchExtra) return;
|
||||
if (ctx.target.hpRatio < phyRatio - 1e-9) ctx.amount *= phyScale;
|
||||
}, { owner: unit, priority: 10 });
|
||||
}
|
||||
if (gospel && def?.skill?.id === S3) gospel.install(battle, unit);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
// server/sim/content/kits/ops/standin-acpion.js — 郁金香 (char_608_acpion) 补位 stand-in kit: 3 瑕光 (S3), 4 焰尾 (S2),
|
||||
// 5 凛御银灰 / 6 圣聆初雪 / 缪尔赛思 / 耀骑士临光 (S3 + module SOL-X on the elite). Stand-in contract: ../README.md.
|
||||
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
|
||||
|
||||
const S1 = 'skchr_acpion_1', S2 = 'skchr_acpion_2', S3 = 'skchr_acpion_3';
|
||||
/**
|
||||
* S3 只余芬芳's slashes: her Spine `Skill` clip (char_608_acpion.skel, 1.567 s) carries 8 OnAttack events — one per
|
||||
* slash, `times` 8 — at 0.400 s and then every 1/6 s (0.567 … 1.567 s); the `Skill_End` clip adds 0.233 s. The skill
|
||||
* holds its effects (below) from the cast to the end of `Skill_End` [ASSUMED: the clips play at 1×, the effects last the
|
||||
* two clips].
|
||||
*/
|
||||
const SLASH_FIRST = 0.4;
|
||||
const SLASH_GAP = 1 / 6;
|
||||
const SLASH_END_CLIP = 0.233;
|
||||
/** S3's buff while it plays: 无敌, 无法阻挡, no normal attack (the skill clip plays) — stun / freeze refused by a hook. */
|
||||
const FRAGRANCE_KEY = 'acpion:fragrance';
|
||||
|
||||
/** Enemies S3 slashes: every selectable enemy on the x-1 skill range, air units included (PRTS 备注 "※可对空"). */
|
||||
const SLASH_PROFILE = Object.freeze({ canHitFly: true });
|
||||
|
||||
export default {
|
||||
// 郁金香 (尖兵, blocks 2) — numbers from data/backups.json (normal E2 Lv1 rank 4, elite E2 Lv60 rank 7; skill_table and
|
||||
// PRTS 郁金香(卫戍协议) agree). DP (部署费用) is the player's redeploy currency here (DESIGN §5.5 "DP").
|
||||
// 无垠之心: from each deployment until her 2nd skill activation of it, SP recovery +sp_recovery_per_sec (module SOL-X Lv3:
|
||||
// +1/s) — `cnt` 2.
|
||||
// 浪潮之心: ATK +atk; each enemy she kills +cost DP.
|
||||
// S1 钻心 (instant): +cost DP, then ATK × atk_scale physical damage to up to max_target enemies of her attack range
|
||||
// (the enemies she blocks included, ground only like her attack) [ASSUMED: at the cast — her S1 clip `Skill_2`
|
||||
// carries no hit event].
|
||||
// S2 迅瞬 (10 s): ASPD +attack_speed, ignores def_penetrate of the target's DEF, and "逐渐获得" trig_cnt × cost DP: one
|
||||
// every `interval` s from the cast (6 × 1.6 s / 7 × 1.4 s, all inside the 10 s) [ASSUMED: none at the cast, and the
|
||||
// DP not yet given is lost if the skill ends early].
|
||||
// S3 只余芬芳 (instant): +cost DP, then `times` slashes on the enemies around her (the x-1 skill range), each ATK ×
|
||||
// atk_scale physical ignoring def_penetrate (50 %) of DEF, timed by her Spine clip (SLASH_*). PRTS 郁金香 (the 集成战略
|
||||
// character char_513_apionr, whose S1 只余芬芳 has the same text, blackboard and x-1 range) 备注: "※可对空" and
|
||||
// "※技能生效期间,持有效果:无敌,无法阻挡,眩晕免疫,冻结免疫" [ASSUMED: the 卫戍 copy skchr_acpion_3 behaves the same].
|
||||
// Module SOL-X (elite, trait override): "阻挡敌人时攻击力和防御力各+8%" — while she blocks (none during S3: 无法阻挡).
|
||||
// Triggers (data, nothing set here): S1 / S2 DEFAULT; S3 SKILL_RANGE on its x-1 (any enemy in it, air units too).
|
||||
char_608_acpion: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1), tb = traitBb(chess);
|
||||
const r1 = skillRec(chess, S1), r2 = skillRec(chess, S2), r3 = skillRec(chess, S3);
|
||||
const dp = (battle, unit, n) => {
|
||||
if (!(n > 0)) return;
|
||||
battle.addDp(unit.ownerId, n);
|
||||
battle.fx('dp', { x: unit.x, y: unit.y, id: unit.id, n });
|
||||
};
|
||||
const skills = {};
|
||||
if (r1) {
|
||||
const b1 = r1.bb ?? {};
|
||||
skills[S1] = {
|
||||
kind: 'instant',
|
||||
onStart({ battle, unit }) {
|
||||
dp(battle, unit, num(b1.cost));
|
||||
const prof = unit.profile;
|
||||
const foes = battle.enemiesInKeys(unit.rangeKeys || [], unit, prof);
|
||||
for (const e of battle.blockedTargets(unit, prof)) if (!foes.includes(e)) foes.push(e);
|
||||
sortEnemyTargets(battle, unit, foes, prof?.priority ?? null);
|
||||
const v = foes.slice(0, Math.max(1, Math.floor(num(b1.max_target, 2))));
|
||||
if (v.length) battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length });
|
||||
for (const e of v) battle.dealDamage(unit, e, { amount: unit.s.atk * num(b1.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
|
||||
},
|
||||
};
|
||||
}
|
||||
if (r2) {
|
||||
const b2 = r2.bb ?? {};
|
||||
const total = Math.max(0, Math.floor(num(b2.trig_cnt, 0))), iv = Math.max(0.1, num(b2.interval, 1)), per = num(b2.cost, 1);
|
||||
skills[S2] = {
|
||||
kind: 'duration',
|
||||
mods: { aspd: num(b2.attack_speed), defIgnorePct: num(b2.def_penetrate) },
|
||||
onStart({ unit }) { unit.mem.acpionDp = { acc: 0, n: 0 }; },
|
||||
onTick({ battle, unit, dt }) {
|
||||
const m = unit.mem.acpionDp;
|
||||
if (!m) return;
|
||||
m.acc += dt;
|
||||
while (m.acc + 1e-9 >= iv && m.n < total) { m.acc -= iv; m.n++; dp(battle, unit, per); }
|
||||
},
|
||||
onEnd({ unit }) { unit.mem.acpionDp = null; },
|
||||
};
|
||||
}
|
||||
if (r3) {
|
||||
const b3 = r3.bb ?? {};
|
||||
const times = Math.max(1, Math.floor(num(b3.times, 8))), scale = num(b3.atk_scale, 1), pen = num(b3.def_penetrate, 0);
|
||||
const grid = r3.rangeGrid?.length ? r3.rangeGrid : null;
|
||||
const lastAt = SLASH_FIRST + (times - 1) * SLASH_GAP;
|
||||
const slash = (battle, unit) => {
|
||||
const keys = grid ? absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0) : unit.rangeKeys || [];
|
||||
const v = battle.enemiesInKeys(keys, unit, SLASH_PROFILE);
|
||||
if (v.length) battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length });
|
||||
for (const e of v) battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'phys', defIgnorePct: pen, isSkill: true, tags: ['skill', 'slash'] });
|
||||
};
|
||||
skills[S3] = {
|
||||
kind: 'instant',
|
||||
onStart({ battle, unit }) {
|
||||
dp(battle, unit, num(b3.cost));
|
||||
const seq = unit.deploySeq;
|
||||
battle.addBuff(unit, {
|
||||
key: FRAGRANCE_KEY, duration: lastAt + SLASH_END_CLIP,
|
||||
flags: { invulnerable: true, noBlock: true, disarm: true }, visible: true, source: unit, tags: ['skill'],
|
||||
});
|
||||
battle.releaseBlocked(unit);
|
||||
for (let i = 0; i < times; i++) {
|
||||
battle.after(SLASH_FIRST + i * SLASH_GAP, () => {
|
||||
if (up(unit) && unit.deploySeq === seq) slash(battle, unit);
|
||||
}, { owner: unit });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
skills,
|
||||
talents: [
|
||||
{ install(battle, unit) { // 无垠之心
|
||||
const per = num(t0.sp_recovery_per_sec), cnt = Math.max(1, Math.floor(num(t0.cnt, 2)));
|
||||
if (!(per > 0)) return;
|
||||
battle.on('deploy', ({ unit: u }) => {
|
||||
if (u !== unit) return;
|
||||
unit.mem.acpionCasts = 0;
|
||||
battle.addBuff(unit, { key: 'acpion:boundless', mods: { spRecoveryFlat: per }, tags: ['talent'] });
|
||||
}, { owner: unit });
|
||||
battle.on('skillStart', ({ unit: u }) => {
|
||||
if (u !== unit) return;
|
||||
unit.mem.acpionCasts = (unit.mem.acpionCasts ?? 0) + 1;
|
||||
if (unit.mem.acpionCasts >= cnt) battle.removeBuff(unit, 'acpion:boundless');
|
||||
}, { owner: unit });
|
||||
} },
|
||||
{ install(battle, unit) { // 浪潮之心
|
||||
statBuff(battle, unit, 'acpion:tide', { atkPct: num(t1.atk) });
|
||||
const n = num(t1.cost);
|
||||
if (n > 0) battle.on('kill', ({ killer, victim }) => { if (killer === unit && victim?.side === 'enemy') dp(battle, unit, n); }, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
const ma = num(tb.atk, 0), md = num(tb.def, 0); // module SOL-X: 阻挡敌人时攻击力和防御力各+8%
|
||||
if (ma || md) toggleBuff(battle, unit, 'acpion:sol', () => unit.blocking.length > 0, { atkPct: ma, defPct: md });
|
||||
// S3: 眩晕免疫, 冻结免疫 while its effects last
|
||||
if (r3) battle.on('beforeStatus', (c) => {
|
||||
if (c.target === unit && (c.status === 'stun' || c.status === 'freeze') && unit.findBuff(FRAGRANCE_KEY)) c.cancel = true;
|
||||
}, { owner: unit });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -74,7 +74,8 @@
|
||||
// makes the token (Battle.spawnToken also refuses summons the owner's loadout does not produce).
|
||||
//
|
||||
// Exports for other content: spawnYanyou, spawnMapChar, findSummonTile, summonToken, tacticalPoint, wolfShadows,
|
||||
// releaseSkillSummon, SKILL_SUMMON_START_DEPLOY, CAT_SHIELD_KEY, TOKEN_IDS.
|
||||
// releaseSkillSummon, SKILL_SUMMON_START_DEPLOY, CAT_SHIELD_KEY, TOKEN_IDS; mapCharTalents and touchGospel (Touch's 攫升 /
|
||||
// 超脱 and 恳切福音, shared with the Touch 补位 stand-in kit, kits/ops/standin-acmedc.js).
|
||||
|
||||
import { COLS, ROWS, MOVE_SCALE } from '../constants.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets, canTargetEnemy } from '../targeting.js';
|
||||
@@ -1228,12 +1229,13 @@ export function spawnYanyou(battle, playerId, { atk, hp, atkMul = 1, dmgTakenMul
|
||||
// band map characters (预备干员-医疗 / Touch)
|
||||
|
||||
/**
|
||||
* Talents of the band map characters (data talents of the character record):
|
||||
* Talents of the band map characters (data talents of the character record) — and of the Touch 补位 stand-in, whose kit
|
||||
* (kits/ops/standin-acmedc.js) uses this one implementation, the module's 超脱 upgrade (8 SP) coming with the record:
|
||||
* plain stat talent "攻击力+4%" (预备干员-医疗 攻击提升) → persistent ATK/DEF/HP/ASPD buff
|
||||
* 攫升 "治疗目标时使其获得3点技力" (Touch) → every heal by the character gives the healed unit `sp` SP
|
||||
* 超脱 "攻击范围内的友方干员被击倒时获得5点技力" (Touch) → an allied operator knocked out on a tile of its range: +`sp` SP
|
||||
*/
|
||||
function mapCharTalents(def) {
|
||||
export function mapCharTalents(def) {
|
||||
const out = [];
|
||||
for (const t of def?.talents ?? []) {
|
||||
const bb = t?.bb ?? {};
|
||||
@@ -1278,18 +1280,21 @@ function reserveMedicKit(bb, raw, def) {
|
||||
return { ...k, talents: [...(k.talents ?? []), ...mapCharTalents(def)] };
|
||||
}
|
||||
|
||||
/** Touch 恳切福音: +ATK, 2 heal targets in the skill range, ×heal_scale on allies ≤ hp_ratio, +addition heal. */
|
||||
function touchKit(bb, raw, def) {
|
||||
const sk = def?.skill;
|
||||
if (!sk) return { skill: null, talents: mapCharTalents(def) };
|
||||
/**
|
||||
* Touch 恳切福音 (skchr_acmedc_3): +ATK, 2 heal targets in the skill range, ×heal_scale on allies ≤ hp_ratio, +addition
|
||||
* heal — one implementation for the 外勤医疗 map character (touchKit) and the Touch 补位 stand-in (kits/ops/
|
||||
* standin-acmedc.js). `skill` = the skill record (`rangeGrid`, `duration`) of blackboard `bb`. Returns `{ skill:
|
||||
* SkillSpec, install(battle, unit) }`: `install` adds the heal boost, which acts while the unit's skill runs — install it
|
||||
* on a unit whose skill is this one only.
|
||||
*/
|
||||
export function touchGospel(bb, skill) {
|
||||
const hpRatio = num(bb.hp_ratio, 0), boost = num(bb.heal_scale, 1), extra = num(bb['attack@addition_heal_scale'], 0);
|
||||
const targeting = {};
|
||||
if (num(bb['attack@max_target'], 0) > 1) targeting.maxTargets = Math.floor(num(bb['attack@max_target'], 1));
|
||||
if (sk.rangeGrid && sk.rangeGrid.length) targeting.rangeGrid = sk.rangeGrid;
|
||||
if (skill.rangeGrid && skill.rangeGrid.length) targeting.rangeGrid = skill.rangeGrid;
|
||||
return {
|
||||
talents: mapCharTalents(def),
|
||||
skill: {
|
||||
kind: sk.duration > 0 ? 'duration' : 'instant', heal: true,
|
||||
kind: skill.duration > 0 ? 'duration' : 'instant', heal: true,
|
||||
mods: num(bb.atk, 0) ? { atkPct: num(bb.atk, 0) } : undefined,
|
||||
targeting,
|
||||
onHit({ battle, unit, target, heal }) {
|
||||
@@ -1321,6 +1326,14 @@ function touchKit(bb, raw, def) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Touch (外勤医疗 map character): 恳切福音 (touchGospel) + 攫升 / 超脱 (mapCharTalents). */
|
||||
function touchKit(bb, raw, def) {
|
||||
const sk = def?.skill;
|
||||
if (!sk) return { skill: null, talents: mapCharTalents(def) };
|
||||
const g = touchGospel(bb, sk);
|
||||
return { talents: mapCharTalents(def), skill: g.skill, install: g.install };
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a band map character (char_605_cmedic 预备干员-医疗 / char_613_acmedc Touch) at its stage position on the
|
||||
* player's half (data/stages.json `mapChars`; `_multi_only` positions only with a partner). Returns the unit or null.
|
||||
|
||||
Reference in new issue
Block a user