feedback5: merge O17 — 自选 kits for 琴柳, 可露希尔 (with her 指挥中心) and 澄闪; golden: only diy moved
This commit is contained in:
commit
000ef7052d
8 files changed
+2195
-99
No files matched your search
@@ -127,6 +127,9 @@ export const OPERATOR_KIT_FILES = Object.freeze([
|
||||
'op-jesca2.js',
|
||||
'op-mcnist.js',
|
||||
'op-kalts.js',
|
||||
'op-sleach.js',
|
||||
'op-closur.js',
|
||||
'op-gdglow.js',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
// server/sim/content/kits/ops/op-closur.js — 可露希尔 (char_4228_closur) 自选 operator kit: 6★ 战术家 (先锋), an owned-6★ pick of
|
||||
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, the module TAC-X 给自己的小奖杯 at every form, and the
|
||||
// kit of her 援军 指挥中心 (token_10066_closur_ourbase). Kit contract and the 自选 rules: ../README.md ("How to add an operator
|
||||
// (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_4228_closur): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
|
||||
// module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no account]. Sources:
|
||||
// character_table / skill_table / battle_equip_table / the token's character_table row (zh_CN, as built into backups.json),
|
||||
// PRTS 可露希尔 (精准投放 备注 "战术点对位于战术点攻击范围内的所有我方干员/召唤物付与可露希尔的援军标记…在攻击这些单位阻挡的敌人时同样
|
||||
// 可以触发特性效果"; S1 备注 "回费间隔 = 持续时间 / 回费量,首次回费间隔为计算所得的回费间隔的一半"; S2 备注 "2秒固定间隔回复费用",
|
||||
// the 【返费】 "回复干员当前部署费用属性的40%(向上取整)", "上一次撤退/离场的原因为移动…不会生效"; S3 备注 "1.66秒的固定间隔", "攻击
|
||||
// 范围变为自身攻击范围+战术点的攻击范围+所有“援军”视野范围的复合范围", "能攻击到自身援军阻挡的敌人", "仅能对敌人类敌方单位施加迟钝"),
|
||||
// PRTS 指挥中心 (备注: 持有禁疗; 对攻击范围内的所有我方干员/召唤物施加援军标记; x-5 / x-4 / x-6 by her skill; 撤退或被击倒 ⇒ 战术点
|
||||
// 形态; 持有者离场后强制撤退), PRTS 分支特性信息 战术家, PRTS 术语释义 护盾 / 迟钝 / 部署费用下限, and the client's battle data read
|
||||
// from the local install — charpack char_4228_closur (trait closur_tr; T2 deck buff closur_t_2[atk] ATK MULTIPLIER on the
|
||||
// filterTag rhodes cards with `_minCostExtremeSetting`; S2 / S3 selectors: her range + the enemies blocked by a closur_friend
|
||||
// holder; S3's attack buff closur[slow_down]: overrideType, refreshRemainingTimeWhenStackMax, clearAllStackCntWhenTimeUp),
|
||||
// the token prefab token_10066_closur_ourbase (modes S1 / S2 / S3: closur_ourbase_aura on ground allies, operators and
|
||||
// summons (professionMask 767), itself too; closur_passive with 禁疗 (abnormal flag 7); the 战术点形态 "Recharge" mode:
|
||||
// 不死 / 无敌, block ×0, back at full HP — closur_ourbase_t_1[listener] / [trigger_switch] / [switch]), the skill prefabs
|
||||
// skchr_closur_1 (ApplyShield: closur_friend holders, damage_block_once, one layer, `overrideType`) / _2 (ActiveAura:
|
||||
// closur_s_2[friend] DEF MULTIPLIER + BLOCK_CNT, the born-state refund) / _3, buff_template_data closur_tr (AtkScaleUp vs a
|
||||
// target blocked by her summon or by a closur_friend holder), closur_e_002_tr (TAC-X DamageScale vs the damage of a unit the
|
||||
// holder blocks), closur_ourbase_aura (closur_friend from the host), closur_s_1[cost] / [trigger_modify_cost],
|
||||
// closur_s_2[friend], closur_s_3[data], slow_down.
|
||||
// - Trait (战术家) "可以在攻击范围内选择一次战术点来召唤援军,自身攻击援军阻挡的敌人时攻击力提升至150%" (trait bb atk_scale): her
|
||||
// 援军 is the 指挥中心 (T1); her damage on an enemy blocked by one of her 援军 — the 指挥中心, or any ally holding her mark — ×
|
||||
// atk_scale (closur_tr; every attack of hers, skill attacks included). Ranged physical, 3-3, hits air (PRTS 分支特性信息 战术家
|
||||
// "普通攻击可对空"), blocks 1, ground enemies target her. TAC-X adds "援军受到来自自身阻挡单位的伤害降低15%" (hidden module talent
|
||||
// damage_scale 0.85): damage on one of her 援军 from an enemy that 援军 blocks ×damage_scale.
|
||||
// - 援军 (her mark, closur_friend): while her 指挥中心 stands, it and every ally of its range — operators and summons (the aura's
|
||||
// ground-motion targets: every unit of our side), any player's, never a 孤立 one — hold her mark (re-read every tick). Her
|
||||
// own tile counts too when the player puts the 指挥中心 next to her [ASSUMED: nothing in the text leaves her out].
|
||||
// - T1 精准投放 "可以在战术点召唤指挥中心协助作战,其被击败后会在15秒后自动刷新;战术点效果范围随携带技能变化": the 指挥中心 is a hand
|
||||
// piece the player places in her range (data ownerRange); her deployment brings it on its tile (deployed early when she comes
|
||||
// first in the battle-start deployment, as 伺夜's pack); without a placed piece it is summoned on the tactical point
|
||||
// (Battle.findTacticalPoint — tier3 tacticalPoint, the 伺夜 rule). It never attacks (ATK 0), blocks 2, holds 禁疗, and its
|
||||
// range is the one of her skill (the variant's bySkill: x-5 / x-4 / x-6 — its passive skill's range here, so the card shows
|
||||
// it). Knocked out (or withdrawn) it enters its 战术点形态 — off the fight, its tile kept (伺夜's 狼群 model) — for the token's
|
||||
// hidden talent `interval` (15 s), then it is back on its tile at full HP; her leaving the field withdraws it for good (no
|
||||
// 战术点形态: "持有者离场后强制撤退") and her next deployment brings a new one.
|
||||
// - T2 极限调度 "携带可露希尔时,部署费用下限降低3,【罗德岛】干员攻击力+4%" (bb cost / atk; TAC-X stage 3: −5 / +8 %): from the battle
|
||||
// start every operator of her player whose nation is 罗德岛 (character_table nationId rhodes — the client's filterTag) ATK
|
||||
// +atk for the whole battle (a deck buff: on the field or not, herself included). The 部署费用下限 has no counterpart here: the
|
||||
// sim's deploy costs never go below 0 (the kits' cost cuts stop there) and only the auto-redeploy spends DP [ASSUMED: no
|
||||
// effect].
|
||||
// - S1 递归策略 (AUTO, 8 s): the 援军 of the moment get one 护盾 layer (shieldHits 1, 不叠加: back to one; until used) and she
|
||||
// gains n = min(cost + uses × cost_per_add, cost_add_max) DP over the duration — uses = her earlier casts of this deployment —
|
||||
// one at a time, every 8 / n s from half that interval. An AUTO skill acting on her side only: it fires as soon as its SP is
|
||||
// full (`trigger: 'SP_FULL'`, the owner's AUTO rule, kits/README.md checklist 5).
|
||||
// - S2 模型扩展 (MANUAL, data SP_FULL — the 战术家 row's ALWAYS; 30 s): +cost DP at once, 15 more one per 2 s; ATK +atk, 2 targets,
|
||||
// and the enemies her 援军 block are her targets too (Battle.setExtraRange: their tiles); every 援军 DEF +def (MULTIPLIER =
|
||||
// Σpct) and block +block_cnt while it runs; an operator of her player deployed on a tile of the 指挥中心's range meanwhile
|
||||
// gives back ceil(cost_return × its deploy cost) DP — not after a 【移动】.
|
||||
// - S3 Q.E.D. (MANUAL, SP_FULL, 30 s): 18 DP one per 1.66 s; attack interval base_attack_time (a flat −0.5 s on 1.0 s); her range
|
||||
// is her own + the 指挥中心's + every 援军's own range (their running grid, not their extra keys — PRTS "仅累加其复合范围中的基础
|
||||
// 范围") + the tiles of the enemies her 援军 block; every hit attack@atk_scale × ATK physical and one 迟钝 stack of
|
||||
// attack@slow_down on an enemy (≤ attack@max_stack_cnt, cap attack@slow_down_max), the stacks sharing one
|
||||
// attack@slow_down_time timer; every attack_trigger_cnt attacks one more target (max_trigger_cnt times).
|
||||
// - 迟钝 (ba.slowdown "移动速度、攻击速度…降低相应比例(同名效果取最高)"; PRTS "移动速度、攻击速度、迟钝乘数均降低相应数值(直接乘算)",
|
||||
// "同名同源效果叠加并刷新时间(不超过上限);不同名/不同源迟钝效果间取当前数值最高"): one 'slowdown' effect per enemy
|
||||
// (Battle.applyStrongest) of value v: move speed ×(1 − v) and attack interval ×1 / (1 − v) (ASPD ×(1 − v); the engine has
|
||||
// no ASPD Σpct bucket — exact while no other attack-interval change is on). Its effect on enemy ability cooldowns and on the
|
||||
// time of resistible statuses is not modelled [ASSUMED].
|
||||
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, batMod, up } from '../shared/tier1.js';
|
||||
import { tacticalPoint } from '../shared/tier3.js';
|
||||
import { COLS, FORCED_EXIT } from '../../../constants.js';
|
||||
|
||||
const S1 = 'skchr_closur_1';
|
||||
const S2 = 'skchr_closur_2';
|
||||
const S3 = 'skchr_closur_3';
|
||||
export const BASE = 'token_10066_closur_ourbase';
|
||||
/** 精准投放 "其被击败后会在15秒后自动刷新" when the token def carries no interval. */
|
||||
const REFRESH_FALLBACK = 15;
|
||||
/** Seconds between two tries of a 指挥中心 whose 战术点形态 is over but whose tile is taken. */
|
||||
const RETRY = 0.25;
|
||||
/** Removal reasons that put the 指挥中心 in its 战术点形态 ("撤退或被击倒"); 'expired' = her leaving / the deploy limit. */
|
||||
const TAC_EXITS = new Set(['killed', 'retreat', FORCED_EXIT]);
|
||||
export const SHIELD_KEY = 'closur:s1';
|
||||
export const S2_KEY = 'closur:s2';
|
||||
export const S3_KEY = 'closur:s3';
|
||||
export const RHODES_KEY = 'talent:closur:rhodes';
|
||||
export const SLOWDOWN = 'slowdown';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
const tileKey = (r, c) => r * COLS + c;
|
||||
/** First talent of a normalised token def holding `key` (token talents keep no index). */
|
||||
const tokTalent = (def, key) => (def?.talents ?? []).find((t) => t && t.bb && t.bb[key] != null) ?? null;
|
||||
|
||||
/** 迟钝 v: move speed ×(1 − v), attack interval ×1 / (1 − v). */
|
||||
const slowMods = (v) => ({ moveMul: Math.max(0, 1 - v), batPct: v < 0.99 ? v / (1 - v) : 99 });
|
||||
|
||||
/**
|
||||
* One 迟钝 stack of `unit` on enemy `e` (S3): the stacks of one source add up (≤ maxStacks, ≤ cap) and share one timer
|
||||
* refreshed by each stack (cleared together when it runs out); the strongest source wins (applyStrongest).
|
||||
*/
|
||||
export function slowdownStack(battle, unit, e, { step, maxStacks, cap, duration }) {
|
||||
if (!e || !e.alive || e.side !== 'enemy' || e.kind !== 'enemy' || !(step > 0) || !(duration > 0)) return;
|
||||
const key = `closurSlow:${unit.id}`;
|
||||
const cur = e.mem[key];
|
||||
const n = Math.min(Math.max(1, Math.floor(maxStacks) || 1), (cur && cur.until > battle.time + 1e-9 ? cur.n : 0) + 1);
|
||||
e.mem[key] = { n, until: battle.time + duration };
|
||||
battle.applyStrongest(e, SLOWDOWN, { duration, value: Math.min(cap > 0 ? cap : Infinity, step * n), source: unit, mods: slowMods });
|
||||
}
|
||||
|
||||
/**
|
||||
* A skill's DP over time (the client's periodic_cost / closur_s_1[trigger_modify_cost]): `step` DP every `iv` s from
|
||||
* `first` s on, `total` in all; a skill that runs its full duration pays the rest at its end.
|
||||
*/
|
||||
function dpTicker(unit, { total, step = 1, iv, first = iv }) {
|
||||
unit.mem.closurDp = { t: 0, next: first, given: 0, total, step, iv };
|
||||
}
|
||||
function dpTick(battle, unit, dt) {
|
||||
const d = unit.mem.closurDp;
|
||||
if (!d || !(d.iv > 0) || !(d.step > 0)) return;
|
||||
d.t += dt;
|
||||
while (d.t + 1e-9 >= d.next && d.given + d.step <= d.total + 1e-9) {
|
||||
d.given += d.step;
|
||||
d.next += d.iv;
|
||||
battle.addDp(unit.ownerId, d.step);
|
||||
}
|
||||
}
|
||||
function dpEnd(battle, unit, reason) {
|
||||
const d = unit.mem.closurDp;
|
||||
unit.mem.closurDp = null;
|
||||
if (!d) return;
|
||||
const rest = d.total - d.given;
|
||||
if (reason === 'duration' && rest > 1e-9) { battle.addDp(unit.ownerId, rest); d.given += rest; }
|
||||
if (d.given > 0) battle.fx('dp', { x: unit.x, y: unit.y, n: d.given, id: unit.id });
|
||||
}
|
||||
|
||||
export default {
|
||||
char_4228_closur: (bb, chess) => {
|
||||
const t1 = talentBb(chess, 1);
|
||||
const hidden = moduleBb(chess); // TAC-X: damage_scale (the 援军's guard)
|
||||
const scale = num(traitBb(chess).atk_scale, 1.5);
|
||||
const guard = num(hidden.damage_scale, 1);
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s1 = skillRec(chess, S1);
|
||||
const tokenId = (chess?.talents ?? []).find((t) => t && t.tokenKey)?.tokenKey ?? BASE;
|
||||
|
||||
/** Her 指挥中心 units (board pieces and summons). */
|
||||
const basesOf = (battle, unit) => battle.allyUnits.filter((t) => t.kind === 'token' && t.defId === tokenId && t.ownerUnit === unit);
|
||||
/** Her 援军 now (cached per tick): her standing 指挥中心 and every operator / summon of its range (no 孤立 one). */
|
||||
const reinfOf = (battle, unit) => {
|
||||
const m = unit.mem;
|
||||
if (m.closurReinf && m.closurReinfT === battle.time) return m.closurReinf;
|
||||
const set = new Set();
|
||||
for (const base of basesOf(battle, unit)) {
|
||||
if (!up(base)) continue;
|
||||
set.add(base);
|
||||
const keys = base.rangeKeySet ?? new Set(base.rangeKeys ?? []);
|
||||
for (const a of battle.allyUnits) {
|
||||
if (!up(a) || a.hidden || (a.kind !== 'op' && a.kind !== 'token')) continue;
|
||||
if (!battle.allySelectable(a, unit) || !keys.has(tileKey(a.tileR, a.tileC))) continue;
|
||||
set.add(a);
|
||||
}
|
||||
}
|
||||
m.closurReinf = set;
|
||||
m.closurReinfT = battle.time;
|
||||
return set;
|
||||
};
|
||||
/** Tiles of the enemies her 援军 block (S2 / S3: "能攻击到自身援军阻挡的敌人"). */
|
||||
const blockedTiles = (battle, unit, reinf) => {
|
||||
const keys = [];
|
||||
for (const r of reinf) for (const e of r.blocking) if (e.alive && e.blockedBy === r) keys.push(tileKey(Math.round(e.y), Math.round(e.x)));
|
||||
return keys;
|
||||
};
|
||||
const setExtra = (battle, unit, keys) => {
|
||||
const cur = unit.extraRangeKeys ?? [];
|
||||
const uniq = [...new Set(keys)].sort((a, b) => a - b);
|
||||
if (uniq.length === cur.length && uniq.every((k, i) => k === cur[i])) return;
|
||||
battle.setExtraRange(unit, uniq.length ? uniq : null);
|
||||
};
|
||||
|
||||
/** The 指挥中心's kit: no attack, 禁疗, the range of her skill, its 战术点形态. `def` = its def for her loadout. */
|
||||
const baseKit = (owner, def) => {
|
||||
const grid = def?.skill?.rangeGrid ?? def?.rangeGrid ?? [[0, 0]];
|
||||
const refresh = num(tokTalent(def, 'interval')?.bb?.interval, REFRESH_FALLBACK);
|
||||
return {
|
||||
skill: { kind: 'passive', targeting: { rangeGrid: grid.map((p) => [p[0], p[1]]) } },
|
||||
trait: { noAttack: true },
|
||||
talents: [],
|
||||
install(battle, t) {
|
||||
battle.addBuff(t, { key: 'closur:base', flags: { noHeal: true }, persist: true, allowDead: true });
|
||||
// 撤退 / 被击倒 ⇒ 战术点形态: off the fight for `refresh` s, its tile kept, then back at full HP (while she stands)
|
||||
battle.on('death', (ctx) => {
|
||||
if (ctx.unit !== t || battle.finished || !TAC_EXITS.has(ctx.reason) || !up(owner)) return;
|
||||
t.removed = false;
|
||||
const seq = (t.mem.closurTacSeq ?? 0) + 1;
|
||||
t.mem.closurTacSeq = seq;
|
||||
t.mem.closurTac = { seq, until: battle.time + refresh };
|
||||
battle.fx('disappear', { x: t.x, y: t.y, id: t.id });
|
||||
const back = () => {
|
||||
const tp = t.mem.closurTac;
|
||||
if (!tp || tp.seq !== seq || battle.finished || t.alive || t.removed) return;
|
||||
if (!up(owner)) { t.mem.closurTac = null; return; }
|
||||
if (battle.redeploy(t, { free: true })) { t.mem.closurTac = null; battle.fx('summon', { x: t.x, y: t.y, id: t.id, src: owner.id }); return; }
|
||||
battle.after(RETRY, back, { owner: t });
|
||||
};
|
||||
battle.after(refresh, back, { owner: t });
|
||||
}, { owner: t, priority: -10 });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// the 援军 is the 指挥中心 (T1): no engine generic 援军; ×atk_scale vs an enemy one of her 援军 blocks
|
||||
trait: {
|
||||
install() {},
|
||||
dmgMul(battle, unit, target) {
|
||||
const b = target && target.blockedBy;
|
||||
return b && reinfOf(battle, unit).has(b) ? scale : 1;
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'duration',
|
||||
trigger: 'SP_FULL',
|
||||
onStart({ battle, unit }) {
|
||||
const uses = unit.mem.closurS1Uses ?? 0;
|
||||
unit.mem.closurS1Uses = uses + 1;
|
||||
const n = Math.max(0, Math.min(num(b1.cost_add_max, Infinity), num(b1.cost) + uses * num(b1.cost_per_add)));
|
||||
const dur = num(s1?.duration, 8);
|
||||
if (n > 0) dpTicker(unit, { total: n, step: 1, iv: dur / n, first: dur / n / 2 });
|
||||
const layers = Math.max(1, Math.floor(num(b1.shield_cnt, 1)));
|
||||
for (const a of reinfOf(battle, unit)) {
|
||||
battle.addBuff(a, { key: `${SHIELD_KEY}:${unit.id}`, shieldHits: layers, source: unit, tags: ['skill'] });
|
||||
battle.fx('shield', { x: a.x, y: a.y, id: a.id, src: unit.id });
|
||||
}
|
||||
},
|
||||
onTick({ battle, unit, dt }) { dpTick(battle, unit, dt); },
|
||||
onEnd({ battle, unit, reason }) { dpEnd(battle, unit, reason); },
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b2.atk) },
|
||||
targeting: { maxTargets: Math.max(1, Math.floor(num(b2['attack@max_target'], 2))) },
|
||||
onStart({ battle, unit }) {
|
||||
const cost = num(b2.cost);
|
||||
if (cost > 0) { battle.addDp(unit.ownerId, cost); battle.fx('dp', { x: unit.x, y: unit.y, n: cost, id: unit.id }); }
|
||||
dpTicker(unit, { total: num(b2.cost_period), step: num(b2['closur_s_2[add_cost_period].cost'], 1), iv: num(b2['closur_s_2[add_cost_period].interval'], 2) });
|
||||
unit.mem.closurS2Set = new Set();
|
||||
},
|
||||
onTick({ battle, unit, dt }) {
|
||||
dpTick(battle, unit, dt);
|
||||
// every 援军 DEF +def, block +block_cnt; the enemies they block are her targets
|
||||
const reinf = reinfOf(battle, unit);
|
||||
const key = `${S2_KEY}:${unit.id}`;
|
||||
const had = unit.mem.closurS2Set ?? new Set();
|
||||
const mods = { defPct: num(b2.def), blockCnt: num(b2.block_cnt) };
|
||||
for (const a of reinf) if (!a.findBuff(key)) battle.addBuff(a, { key, mods, source: unit, tags: ['skill'] });
|
||||
for (const a of had) if (!reinf.has(a)) battle.removeBuff(a, key);
|
||||
unit.mem.closurS2Set = new Set(reinf);
|
||||
setExtra(battle, unit, blockedTiles(battle, unit, reinf));
|
||||
},
|
||||
onEnd({ battle, unit, reason }) {
|
||||
dpEnd(battle, unit, reason);
|
||||
for (const a of unit.mem.closurS2Set ?? []) battle.removeBuff(a, `${S2_KEY}:${unit.id}`);
|
||||
unit.mem.closurS2Set = null;
|
||||
battle.setExtraRange(unit, null);
|
||||
},
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { batPct: batMod(b3.base_attack_time, chess) },
|
||||
attack: {
|
||||
atkScale: num(b3['attack@atk_scale'], 1),
|
||||
onEachHit({ battle, unit, target }) {
|
||||
slowdownStack(battle, unit, target, {
|
||||
step: num(b3['attack@slow_down']), maxStacks: num(b3['attack@max_stack_cnt'], 10),
|
||||
cap: num(b3['attack@slow_down_max']), duration: num(b3['attack@slow_down_time'], 3),
|
||||
});
|
||||
},
|
||||
},
|
||||
onStart({ battle, unit }) {
|
||||
dpTicker(unit, { total: num(b3.cost_period), step: num(b3['closur_s_3[add_cost_period].cost'], 1), iv: num(b3['closur_s_3[add_cost_period].interval'], 1.66) });
|
||||
unit.mem.closurS3 = { attacks: 0, extra: 0 };
|
||||
battle.removeBuff(unit, S3_KEY);
|
||||
},
|
||||
onAttack({ battle, unit }) {
|
||||
const q = unit.mem.closurS3;
|
||||
if (!q) return;
|
||||
q.attacks++;
|
||||
if (q.extra < num(b3.max_trigger_cnt, 6) && q.attacks >= num(b3.attack_trigger_cnt, 9)) {
|
||||
q.attacks = 0;
|
||||
q.extra++;
|
||||
battle.addBuff(unit, { key: S3_KEY, mods: { maxTargets: q.extra }, tags: ['skill'] });
|
||||
}
|
||||
},
|
||||
onTick({ battle, unit, dt }) {
|
||||
dpTick(battle, unit, dt);
|
||||
// her own range + the 指挥中心's + every 援军's own range + the tiles of the enemies they block
|
||||
const reinf = reinfOf(battle, unit);
|
||||
const keys = [];
|
||||
for (const r of reinf) {
|
||||
const extra = r.extraRangeKeys ? new Set(r.extraRangeKeys) : null;
|
||||
for (const k of r.rangeKeys ?? []) if (!extra || !extra.has(k)) keys.push(k);
|
||||
}
|
||||
keys.push(...blockedTiles(battle, unit, reinf));
|
||||
setExtra(battle, unit, keys);
|
||||
},
|
||||
onEnd({ battle, unit, reason }) {
|
||||
dpEnd(battle, unit, reason);
|
||||
unit.mem.closurS3 = null;
|
||||
battle.removeBuff(unit, S3_KEY);
|
||||
battle.setExtraRange(unit, null);
|
||||
},
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 精准投放: the 指挥中心
|
||||
// her placed pieces run its kit (set up here, before the battle starts — as 鸿雪's 打字机)
|
||||
for (const t of basesOf(battle, unit)) {
|
||||
if (t.alive || t.deployed) continue;
|
||||
if (t.kit) battle.offOwner(t);
|
||||
battle._setupUnit(t, baseKit(unit, t.def));
|
||||
}
|
||||
const summon = () => {
|
||||
if (!up(unit)) return;
|
||||
const ps = basesOf(battle, unit);
|
||||
const live = ps.find((t) => up(t));
|
||||
if (live) { unit.trait.reinforcement = live; return; }
|
||||
if (ps.some((t) => !t.alive && !t.removed && t.mem.closurTac)) return; // in its 战术点形态: back by itself
|
||||
const waiting = ps.find((t) => !t.alive && !t.removed);
|
||||
if (waiting && battle.redeploy(waiting, { free: true })) {
|
||||
unit.trait.reinforcement = waiting;
|
||||
battle.fx('summon', { x: waiting.x, y: waiting.y, id: waiting.id, src: unit.id, token: tokenId });
|
||||
return;
|
||||
}
|
||||
const board = ps.find((t) => t.uid != null);
|
||||
const tile = tacticalPoint(battle, unit, board ? [board.homeR, board.homeC] : null);
|
||||
if (!tile) return;
|
||||
const def = battle.tokenDef(tokenId, unit);
|
||||
const t = battle.spawnToken(unit, tokenId, tile[0], tile[1], { kit: baseKit(unit, def) });
|
||||
if (!t) return;
|
||||
unit.trait.reinforcement = t;
|
||||
battle.fx('summon', { x: t.x, y: t.y, id: t.id, src: unit.id, token: tokenId });
|
||||
};
|
||||
battle.on('deploy', (ctx) => {
|
||||
if (ctx.unit === unit) { unit.mem.closurS1Uses = 0; summon(); return; }
|
||||
if (ctx.unit.kind === 'token' && ctx.unit.defId === tokenId && ctx.unit.ownerUnit === unit) unit.trait.reinforcement = ctx.unit;
|
||||
}, { owner: unit });
|
||||
// "持有者离场后强制撤退场上所有的指挥中心" (no 战术点形态); one in its 战术点形态 ends it there and waits for her
|
||||
battle.on('death', (ctx) => {
|
||||
if (ctx.unit !== unit) return;
|
||||
for (const t of basesOf(battle, unit)) {
|
||||
if (t.alive) battle.retreat(t, { reason: 'expired', permanent: true });
|
||||
else if (t.mem.closurTac) t.mem.closurTac = null;
|
||||
}
|
||||
unit.trait.reinforcement = null;
|
||||
}, { owner: unit });
|
||||
// TAC-X: damage on one of her 援军 from an enemy that 援军 blocks ×damage_scale
|
||||
if (guard !== 1) {
|
||||
battle.on('hit', (ctx) => {
|
||||
const tg = ctx.target, src = ctx.source;
|
||||
if (!tg || !src || src.side !== 'enemy' || src.blockedBy !== tg || !up(unit) || !reinfOf(battle, unit).has(tg)) return;
|
||||
ctx.dmg.mul *= guard;
|
||||
}, { owner: unit });
|
||||
}
|
||||
// S2: 在战术点效果范围内部署干员时,立即返还部署费用的40%(向上取整) — not after a 【移动】
|
||||
if (unit.skill?.id === S2) {
|
||||
const ret = num(b2.cost_return);
|
||||
battle.on('deploy', (ctx) => {
|
||||
const a = ctx.unit;
|
||||
if (!(ret > 0) || ctx.initial || ctx.move || !a || a.kind !== 'op' || a.ownerId !== unit.ownerId || a === unit) return;
|
||||
if (!unit.skill?.active || unit.skill.id !== S2 || !up(unit)) return;
|
||||
const inRange = basesOf(battle, unit).some((t) => up(t) && (t.rangeKeySet ?? new Set(t.rangeKeys)).has(tileKey(a.tileR, a.tileC)));
|
||||
if (!inRange) return;
|
||||
const back = Math.ceil(num(a.base.cost) * ret - 1e-9);
|
||||
if (back > 0) { battle.addDp(unit.ownerId, back); battle.fx('dp', { x: a.x, y: a.y, n: back, id: unit.id }); }
|
||||
}, { owner: unit });
|
||||
}
|
||||
} },
|
||||
{ install(battle, unit) { // 极限调度: the 【罗德岛】 operators of her player ATK +atk for the battle (携带可露希尔时)
|
||||
const atk = num(t1.atk);
|
||||
if (!(atk > 0)) return;
|
||||
const give = () => {
|
||||
for (const a of battle.allyUnits) {
|
||||
if (a.kind !== 'op' || a.ownerId !== unit.ownerId || (a.def?.raw?.nationId ?? a.def?.nationId) !== 'rhodes') continue;
|
||||
const cur = a.findBuff(RHODES_KEY);
|
||||
if (cur && num(cur.data?.v) >= atk) continue;
|
||||
battle.addBuff(a, { key: RHODES_KEY, mods: { atkPct: atk }, persist: true, allowDead: true, data: { v: atk }, source: unit, tags: ['talent'] });
|
||||
}
|
||||
};
|
||||
if (battle.started) give();
|
||||
else battle.on('battleStart', give, { owner: unit });
|
||||
} },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
// server/sim/content/kits/ops/op-gdglow.js — 澄闪 (char_377_gdglow) 自选 operator kit: 6★ 驭械术师 (术师), an owned-6★ pick of the
|
||||
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (FUN-X “辛劳之翼”, FUN-Y 梦想终将实现) at every
|
||||
// form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_377_gdglow): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
|
||||
// picked module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no account].
|
||||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json), PRTS 澄闪 (信标的愤怒 备注:
|
||||
// "每层1.5%概率,初始1层;未成功自爆时对应浮游单元层数增加1层,叠加至40层后下次攻击必定自爆。对应浮游单元成功自爆后/澄闪部署时层数
|
||||
// 重置。每个浮游单元自爆概率独立计算", "自爆伤害半径1.1;自爆不计入特性叠加;自爆伤害不受特性影响"; 精准导流 备注 "增加自身法术穿透
|
||||
// 属性(直接加算)"; S3 备注 "停顿效果可应用于自爆"), PRTS 分支特性信息 驭械术师 ("浮游单元攻击不同目标/干员再部署时,上述的伤害立刻
|
||||
// 恢复至初始值", "通过技能释放的浮游单元造成技能直接伤害,不受缴械类效果制约"), Arknights Terra Wiki Goldenglow, and the client's
|
||||
// battle data read from the local install — charpack char_377_gdglow (the skill modes S1 / S2 / S3: an attack whose
|
||||
// `_funnelActions` S1Funnel1–2 / S2Funnel1–2 / S3Funnel1–3 each throw a persistent funnel projectile at the attack's target
|
||||
// (`_alwaysIncludeTarget`) and fire again only once it is gone (`_waitForProjectileInvalid`); S3's attack has no projectile
|
||||
// of its own — "停止攻击" — and its funnels carry `sluggish`; OverrideTrait swaps the trait's Funnel trigger off in every
|
||||
// skill mode; the talent config copies attack@prob / attack@atk_scale_2 into the skills), the funnel projectiles
|
||||
// projectile_chr_gdglow_funnel_s1_1 … s3_3 (`_lifeTimeType` 2: until stopped, `_stopWhenSourceInvalid`, its crash
|
||||
// AdvancedApplyDamage MAGICAL at atk_scale_2) and their s12 / s3 blast projectiles.
|
||||
// - Trait (驭械术师) "操作浮游单元造成法术伤害;单元攻击同一敌人伤害提升(最高造成干员110%攻击力的伤害)" (trait bb init / delta /
|
||||
// max_atk_scale): the engine's funnel profile (professions.js: her normal attack = one drone hit at init → +delta per hit on
|
||||
// the same target → max). FUN-X: init 0.35 ("单元初始伤害提高"); FUN-Y: max 1.2. Ranged arts, 3-1, hits air (PRTS "可对空"),
|
||||
// blocks 1, ground enemies target her.
|
||||
// - Skill drones (the three skills "浮游单元+N,释放浮游单元锁定敌人攻击 … 锁敌后直至敌人被击杀、自爆或技能结束后返回干员身边"): while a
|
||||
// skill runs she makes no attack of her own — every drone is out, 1 + attack@cnt of them — and each drone locks the target
|
||||
// her targeting ranks first in her current range (the one her attack would take: the client's `_alwaysIncludeTarget`; so the
|
||||
// drones mostly share one enemy), keeps it — in range or not — until it falls or turns unselectable, then takes her current
|
||||
// first target again at once [ASSUMED: the client re-sends it with her next attack]. A drone attacks its enemy every attack
|
||||
// interval of hers (her live ASPD), the first as it locks [ASSUMED: no flight time], for ATK × its own ramp (the trait's
|
||||
// init, +delta per hit, ≤ max; every new lock starts at init [ASSUMED: a new funnel projectile]) — arts 技能直接伤害
|
||||
// (isSkill, no normal attack: on-attack items skip it), and no 缴械 stops it (PRTS); [ASSUMED] nor do her stun / freeze /
|
||||
// silence: the skill ticks on and so do the drones.
|
||||
// - T1 信标的愤怒 "技能开启后浮游单元攻击时有10%概率自爆(回到干员身边)对小范围敌人造成澄闪300%攻击力的法术伤害" (bb attack@prob /
|
||||
// attack@atk_scale_2 / attack@max_stack_cnt; FUN-X stage 3: 360 %): each skill drone (by its slot, S3's third one too) holds
|
||||
// a stack count from 1: on each of its attacks it self-destructs with prob × stacks (sure past attack@max_stack_cnt), else
|
||||
// +1 stack; the blast — after that attack's hit [ASSUMED: in addition to it] — deals atk_scale_2 × her ATK arts (isSkill,
|
||||
// no ramp) to every enemy within 1.1 of its target (中点判定, air units too), the drone goes back to her (its lock is free)
|
||||
// and its count restarts at 1; every deployment of hers resets the counts.
|
||||
// - T2 精准导流 "自身与浮游单元无视敌人15点法术抗性" (bb magic_resist_penetrate_fixed; FUN-Y stage 3: 20): resIgnoreFlat on her (her
|
||||
// drones' damage is hers).
|
||||
// - S1 火花四溅 (MANUAL, data DEFAULT, 25 s): ATK +atk, ASPD +attack_speed, 2 drones.
|
||||
// - S2 电流翻涌 (AUTO, 持续时间无限 — a toggle): range 3-18 (the skill's grid), ATK +atk, 2 drones. A self buff (no target to
|
||||
// wait for): the owner's AUTO rule fires it as soon as its SP is full (`trigger: 'SP_FULL'`, kits/README.md checklist 5; as
|
||||
// 黄's 链锯延伸模块 and 谬因's 连续映射).
|
||||
// - S3 澄净闪耀 (MANUAL, data GDGLOW_SKILL_2 — "全场存在可选目标时释放技能", the rule named after it; 30 s): "停止攻击", range the
|
||||
// whole field, ATK +atk, 3 drones; every drone hit — and every blast (PRTS 备注) — 停顿 attack@sluggish s.
|
||||
|
||||
import { num, talentBb, skillRec, statBuff } from '../shared/tier1.js';
|
||||
import { WHOLE_FIELD } from '../shared/tier6.js';
|
||||
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||||
|
||||
const S1 = 'skchr_gdglow_1';
|
||||
const S2 = 'skchr_gdglow_2';
|
||||
const S3 = 'skchr_gdglow_3';
|
||||
/** 信标的愤怒's blast radius (PRTS 备注 "自爆伤害半径1.1"; no blackboard key). */
|
||||
export const BLAST_RADIUS = 1.1;
|
||||
export const DRONE_TAG = 'gdglow:drone';
|
||||
export const BLAST_TAG = 'gdglow:blast';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
|
||||
/** Her first-ranked target of her current range (and the enemies she blocks), or null. */
|
||||
function firstTarget(battle, unit) {
|
||||
const prof = 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);
|
||||
if (!cands.length) return null;
|
||||
sortEnemyTargets(battle, unit, cands, prof?.priority ?? null);
|
||||
return cands[0];
|
||||
}
|
||||
|
||||
export default {
|
||||
char_377_gdglow: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const prob = num(t0['attack@prob']), blastScale = num(t0['attack@atk_scale_2']);
|
||||
const maxStack = Math.max(1, Math.floor(num(t0['attack@max_stack_cnt'], 40)));
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s2 = skillRec(chess, S2);
|
||||
const drones = (b) => 1 + Math.max(0, Math.floor(num(b['attack@cnt'])));
|
||||
|
||||
/** One drone of `unit` hits its enemy (and may self-destruct). */
|
||||
const droneHit = (battle, unit, d, t, sluggish) => {
|
||||
const f = unit.profile?.funnel || { init: 0.2, delta: 0.15, max: 1.1 };
|
||||
d.ramp = d.rampId === t.id ? Math.min(f.max, d.ramp + f.delta) : f.init;
|
||||
d.rampId = t.id;
|
||||
battle.fx('drone', { x: t.x, y: t.y, id: unit.id });
|
||||
battle.dealDamage(unit, t, { amount: unit.s.atk * unit.s.atkScaleMul * d.ramp, type: 'arts', isSkill: true, tags: ['skill', 'drone', DRONE_TAG] });
|
||||
if (sluggish > 0 && t.alive) battle.applyStatus(t, 'sluggish', { duration: sluggish, source: unit });
|
||||
if (!(prob > 0) || !(blastScale > 0) || !unit.alive) return;
|
||||
// 信标的愤怒: prob × stacks, sure past the cap; a miss adds a stack
|
||||
const st = unit.mem.gdStacks || (unit.mem.gdStacks = []);
|
||||
const k = st[d.i] ?? 1;
|
||||
if (!(k > maxStack || battle.rng.chance(Math.min(1, prob * k)))) { st[d.i] = Math.min(maxStack + 1, k + 1); return; }
|
||||
st[d.i] = 1;
|
||||
const x = t.x, y = t.y;
|
||||
d.lock = null; // back to her
|
||||
battle.fx('explode', { x, y, radius: BLAST_RADIUS, id: unit.id, dmgType: 'arts', skill: 'gdglow:blast' });
|
||||
for (const e of battle.foesInRadius(x, y, BLAST_RADIUS, true)) {
|
||||
if (!e.alive) continue;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * blastScale, type: 'arts', isSkill: true, tags: ['skill', BLAST_TAG] });
|
||||
if (sluggish > 0 && e.alive) battle.applyStatus(e, 'sluggish', { duration: sluggish, source: unit });
|
||||
}
|
||||
};
|
||||
|
||||
/** A skill's drones: `n` of them; S3's carry 停顿 `sluggish` s. */
|
||||
const droneSpec = (n, sluggish) => ({
|
||||
onStart({ battle, unit }) {
|
||||
unit.mem.gdDrones = Array.from({ length: n }, (_, i) => ({ i, lock: null, cd: 0, rampId: null, ramp: 0 }));
|
||||
battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n });
|
||||
},
|
||||
onTick({ battle, unit, dt }) {
|
||||
const D = unit.mem.gdDrones;
|
||||
if (!D) return;
|
||||
const ok = (e) => !!e && e.alive && !e.hidden && canTargetEnemy(unit, e, unit.profile);
|
||||
for (const d of D) {
|
||||
if (unit.mem.gdDrones !== D || !unit.alive) return;
|
||||
d.cd = Math.max(0, d.cd - dt);
|
||||
if (d.lock && !ok(d.lock)) d.lock = null;
|
||||
if (!d.lock) {
|
||||
const t = firstTarget(battle, unit);
|
||||
if (!t) continue;
|
||||
d.lock = t;
|
||||
d.rampId = null;
|
||||
battle.fx('droneLock', { x: t.x, y: t.y, id: t.id, src: unit.id });
|
||||
}
|
||||
if (d.cd > 1e-9) continue;
|
||||
d.cd = unit.s.interval;
|
||||
droneHit(battle, unit, d, d.lock, sluggish);
|
||||
}
|
||||
},
|
||||
onEnd({ unit }) { unit.mem.gdDrones = null; },
|
||||
});
|
||||
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b1.atk), aspd: num(b1.attack_speed) },
|
||||
attack: { noAttack: true },
|
||||
...droneSpec(drones(b1), 0),
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'toggle',
|
||||
trigger: 'SP_FULL',
|
||||
mods: { atkPct: num(b2.atk) },
|
||||
...(s2?.rangeGrid ? { targeting: { rangeGrid: s2.rangeGrid.map((p) => [p[0], p[1]]) } } : {}),
|
||||
attack: { noAttack: true },
|
||||
...droneSpec(drones(b2), 0),
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b3.atk) },
|
||||
targeting: { rangeGrid: WHOLE_FIELD },
|
||||
attack: { noAttack: true },
|
||||
...droneSpec(drones(b3), num(b3['attack@sluggish'])),
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 信标的愤怒: the skill drones' self-destruct (droneHit); counts reset at every deployment
|
||||
battle.on('deploy', (ctx) => { if (ctx.unit === unit) unit.mem.gdStacks = []; }, { owner: unit });
|
||||
} },
|
||||
{ install(battle, unit) { // 精准导流: 无视敌人 N 点法术抗性 (her drones' damage is hers)
|
||||
statBuff(battle, unit, 'talent:gdglow:pen', { resIgnoreFlat: num(t1.magic_resist_penetrate_fixed) });
|
||||
} },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
// server/sim/content/kits/ops/op-sleach.js — 琴柳 (char_479_sleach) 自选 operator kit: 6★ 执旗手 (先锋), an owned-6★ pick of the
|
||||
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (BEA-X 牧人的歌, BEA-Y “友谊万岁”) at every
|
||||
// form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
|
||||
//
|
||||
// Forms (data/backups.json units.char_479_sleach): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7, the
|
||||
// picked module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no account].
|
||||
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json), PRTS 琴柳 (不退之旗 备注
|
||||
// "与军旗相关的效果可视为地块效果,效果影响范围跟随军旗所在地块移动;军旗对干员的效果无视孤立"; 精神感召 备注 and its Y-module 修正
|
||||
// "在场期间一次,当有干员部署时,若干员为地面位干员,则立刻回复…部署费用"; S2 备注 "生命回复速度…不受治疗加成和禁疗影响" / "不会
|
||||
// 选择召唤物/装置所在位置…技能效果不会应用于召唤物/装置"; S3 备注 "停顿与脆弱效果会在军旗落地前就开始生效,眩晕与伤害效果会在军旗
|
||||
// 落地(约1s延迟)后再生效", "技能无法选择空中目标释放,但效果可对空", the 军旗索敌逻辑), PRTS 分支特性信息 执旗手, Arknights Terra
|
||||
// Wiki Saileach (the talent's 3×3 range), and the client's battle data read from the local install — charpack char_479_sleach
|
||||
// (talent 1: four x-4 auras, operators only — professionMask 639 — ignoring 孤立, enemies ground and air), the skill prefabs
|
||||
// skchr_sleach_1/2/3 (S2's Flag selector: operators, lowest HP share, one target; FlagBuff atk_to_hp_recovery every 1 s and DEF
|
||||
// MULTIPLIER on the 0-1 of the flag; S3's DamageFlag: TalentToEnemy / TalentToAlly / Damage / Debuff on one ground tile),
|
||||
// the projectiles projectile_chr_sleach_s2 / _s3 / _s3_2 / _t1 / _t1_2 (`_immediatelyReach` flags; the S3 strike's
|
||||
// `_lifeTime` 1 s, its x-4 hitting ground and air), buff_template_data sleach_s_2 (Flag `_excludeTargetType` SOURCE),
|
||||
// sleach_s_3, sleach_t_2, sleach_trait_e / [trigger] (BEA-X: every 0.2 s the operator of the 1-1 but her own tile, not
|
||||
// stunned / asleep, BLOCK_CNT +block_cnt until her skill ends), sleach_e_003[tr] (BEA-Y: CAMOUFLAGE for the skill),
|
||||
// sleach_e_003[t] / [t2] / [t][effect] (BEA-Y Lv2+: the next MELEE deployment +cost DP, once), periodic_cost / charge_cost /
|
||||
// atk_to_hp_recovery.
|
||||
// - Trait (执旗手) "技能发动期间阻挡数变为0": the profession's bearer install (block ×0 while a skill runs, the blocked enemies
|
||||
// released). Melee physical, 1-1, ground only (data), ground enemies target her. Every skill "停止攻击": no attack meanwhile.
|
||||
// BEA-X “牧人的歌” "但使身前一名干员阻挡数+1" (trait bb block_cnt): while her skill runs, every 0.2 s the operator on the tile
|
||||
// in front of her (not stunned or asleep) gets block +block_cnt, kept until the skill ends. BEA-Y “友谊万岁” "但获得迷彩":
|
||||
// 迷彩 (flag camou) while her skill runs.
|
||||
// - T1 不退之旗 "部署时自身持有军旗;军旗周围8格的干员攻击速度+10,敌人的攻击速度-10" (bb sleach_t_1[ally/enemy].attack_speed;
|
||||
// BEA-X stage 3: ±13): the flag is a tile effect — its tile and the 8 around it (x-4) — on her own tile while she holds it,
|
||||
// on the tile S2 / S3 throw it to while they run. Operators there (summons not; 孤立 ones too) ASPD +ally, enemies there
|
||||
// (ground and air, the ones her side can select) ASPD +enemy; one buff per 琴柳 (the client's independentCharacterSource),
|
||||
// refreshed every AURA_IV s and taken off a unit that left the area.
|
||||
// - T2 精神感召 "部署后,下一名部署的干员费用-2" (bb value): from each deployment of hers until the next deployment of one of
|
||||
// her player's operators (PRTS 备注 "于任一受影响干员首次部署后失效"), every operator of hers off the field costs −value DP
|
||||
// (野鬃's convention: base.cost, at least 0, given back when the effect ends); an operator that leaves the field meanwhile
|
||||
// gets it too [ASSUMED: the client's card filter covers the cards entering the hand]; her leaving ends it (Terra Wiki). In
|
||||
// this mode the battle-start deployment is free, so it only saves DP when the next deployment is a redeploy. BEA-Y stage 2+
|
||||
// adds "若该干员为地面干员,则部署后获得2点部署费用" (hidden module talent cost; PRTS 修正 "在场期间一次"): that next operator
|
||||
// deployment gives +cost DP when the operator is a 近战位 one (def.position MELEE), else it is wasted — once per deployment
|
||||
// of hers. A 【移动】 is no deployment; summons neither consume nor trigger either effect [ASSUMED].
|
||||
// - S1 支援号令·γ型 (MANUAL, data SP_FULL — the official ALWAYS of the 执旗手 row; 8 s): +cost DP every `interval` s (the first
|
||||
// after one interval), `value` in all (18 at 0.44 s: the last at 7.92 s).
|
||||
// - S2 信仰传承 (MANUAL, SP_FULL, 15 s): 20 DP (sleach_s_2[cost].cost every …interval 0.75 s, the 20th as the skill ends);
|
||||
// the flag is thrown (`_immediatelyReach`: at once) to the tile of the operator with the lowest HP share on the x-1, her
|
||||
// aside (Flag `_excludeTargetType` SOURCE; summons / devices never; a 孤立 one neither — the selector does not ignore it),
|
||||
// on her own tile when there is none (Terra Wiki), ties broken by the earliest deployed [ASSUMED]. The operator standing on that tile (a tile effect: whoever stands there,
|
||||
// herself included) has DEF +def (MULTIPLIER = Σpct) and 生命回复速度 +atk_to_hp_recovery_ratio × her ATK (an hpRegen buff,
|
||||
// never a heal: 禁疗 / 无法被友方治疗 do not stop it — kits/README.md checklist 11), the value read from her ATK at the throw
|
||||
// and every 1 s after (the template's 1 s trigger). T1's area follows the flag. Back to her when the skill ends.
|
||||
// - S3 光辉旗帜 (MANUAL, SP_FULL, 10 s): +cost DP at once; the flag goes to one ground tile of the 2-1 (PRTS 军旗索敌逻辑): the
|
||||
// tile of the selectable ground enemy her targeting ranks first (blocked → taunt → nearest the goal …: the highest 仇恨),
|
||||
// else the free ground tile nearest to it, else her own tile when no selectable ground enemy is there (flyers are never
|
||||
// chosen). At once every enemy of the flag's 3×3 (ground and air) is 停顿 and 脆弱 debuff.damage_scale (refreshed while it
|
||||
// stays there; the client's [inf] statuses — 抵抗 does not shorten a status that lasts while in the area); FLAG_FLIGHT s
|
||||
// after the throw the strike lands: every enemy of the 3×3 (ground and air) takes atk_scale × her ATK of the throw physical
|
||||
// and a `stun` s 晕眩. T1's area follows the flag; back to her when the skill ends.
|
||||
|
||||
import { num, talentBb, moduleBb, traitBb, moduleOn, skillRec, up } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { offsetTile } from '../../../dir.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
|
||||
const S1 = 'skcom_assist_cost[3]';
|
||||
const S2 = 'skchr_sleach_2';
|
||||
const S3 = 'skchr_sleach_3';
|
||||
/** The flag's area: its tile and the 8 around it (the client's x-4 auras of 不退之旗 and of S3's flag / strike). */
|
||||
const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
|
||||
/** S3's strike lands this long after the throw (PRTS 备注 "约1s延迟"; projectile_chr_sleach_s3_2 `_lifeTime` 1). */
|
||||
export const FLAG_FLIGHT = 1;
|
||||
/** Refresh period / lifetime of the flag's tile effects while a unit stands in their area. */
|
||||
const AURA_IV = 0.2;
|
||||
const AURA_DUR = 0.35;
|
||||
/** BEA-X: the client's sleach_trait_e[trigger] interval. */
|
||||
const FRONT_IV = 0.2;
|
||||
/** Selection profile of the flag's enemy effects: ground and air (targetMotion ALL), the enemies her side can select. */
|
||||
const ANY = Object.freeze({ canHitFly: true });
|
||||
const GROUND = Object.freeze({ canHitFly: false, groundOnly: true });
|
||||
|
||||
export const FLAG_ALLY = 'sleach:flag:ally';
|
||||
export const FLAG_ENEMY = 'sleach:flag:enemy';
|
||||
export const S2_KEY = 'sleach:s2';
|
||||
export const FRONT_KEY = 'sleach:front';
|
||||
export const CAMOU_KEY = 'sleach:camou';
|
||||
|
||||
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
|
||||
const tileKey = (r, c) => r * COLS + c;
|
||||
/** A living deployed operator (summons / devices are no 干员). */
|
||||
const isOp = (a) => !!a && a.kind === 'op' && a.alive && a.deployed && !a.hidden;
|
||||
|
||||
/** The tile of her flag: the one S2 / S3 threw it to while they run, else her own (she holds it). */
|
||||
function flagTile(unit) {
|
||||
const f = unit.mem.sleachFlag;
|
||||
return f ? [f.r, f.c] : [unit.tileR, unit.tileC];
|
||||
}
|
||||
const areaKeys = (unit, r, c) => new Set(absoluteRangeKeys(X4, r, c, unit.dir, 0));
|
||||
|
||||
/**
|
||||
* A skill's DP over time: `step` DP every `iv` s from `first` s on, `total` in all; a skill that runs its full duration pays
|
||||
* the rest at its end (the last tick may fall on the end itself — S2's 20th DP at 15 s). One 'dp' fx with the sum at the end.
|
||||
*/
|
||||
function dpOverTime({ total, step = 1, iv, first = iv }) {
|
||||
return {
|
||||
start(unit) { unit.mem.sleachDp = { t: 0, next: first, given: 0 }; },
|
||||
tick(battle, unit, dt) {
|
||||
const d = unit.mem.sleachDp;
|
||||
if (!d || !(iv > 0) || !(step > 0)) return;
|
||||
d.t += dt;
|
||||
while (d.t + 1e-9 >= d.next && d.given + step <= total + 1e-9) {
|
||||
d.given += step;
|
||||
d.next += iv;
|
||||
battle.addDp(unit.ownerId, step);
|
||||
}
|
||||
},
|
||||
end(battle, unit, reason) {
|
||||
const d = unit.mem.sleachDp;
|
||||
unit.mem.sleachDp = null;
|
||||
if (!d) return;
|
||||
const rest = total - d.given;
|
||||
if (reason === 'duration' && rest > 1e-9) { battle.addDp(unit.ownerId, rest); d.given += rest; }
|
||||
if (d.given > 0) battle.fx('dp', { x: unit.x, y: unit.y, n: d.given, id: unit.id });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* S2's target tile: the operator with the lowest HP share on the x-1 (not her; not a 孤立 one — the selector, unlike the flag's
|
||||
* own effects, does not ignore 孤立), else her own tile.
|
||||
*/
|
||||
function s2Tile(battle, unit, grid) {
|
||||
const keys = new Set(absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0));
|
||||
const cands = battle.allyUnits.filter((a) => isOp(a) && a !== unit && battle.allySelectable(a, unit) && keys.has(tileKey(a.tileR, a.tileC)));
|
||||
cands.sort((a, b) => a.hpRatio - b.hpRatio || a.deploySeq - b.deploySeq || a.id - b.id);
|
||||
const t = cands[0];
|
||||
return t ? [t.tileR, t.tileC] : [unit.tileR, unit.tileC];
|
||||
}
|
||||
|
||||
/**
|
||||
* S3's target tile (PRTS 军旗索敌逻辑): ground tiles of the 2-1 only — the tile of the selectable ground enemy ranked first
|
||||
* (her targeting order: the highest 仇恨), else the free ground tile of the range nearest to it, else her own tile.
|
||||
*/
|
||||
function s3Tile(battle, unit, grid) {
|
||||
const keys = absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0);
|
||||
const ground = (r, c) => battle.grid.inRect(r, c) && battle.grid.isLow(r, c);
|
||||
const foes = battle.enemiesInKeys(keys, unit, GROUND).filter((e) => !e.isFlying);
|
||||
if (!foes.length) return [unit.tileR, unit.tileC];
|
||||
sortEnemyTargets(battle, unit, foes, null);
|
||||
const top = foes[0];
|
||||
const er = Math.round(top.y), ec = Math.round(top.x);
|
||||
if (keys.includes(tileKey(er, ec)) && ground(er, ec)) return [er, ec];
|
||||
let best = null, bd = Infinity;
|
||||
for (const k of keys) {
|
||||
const r = (k / COLS) | 0, c = k % COLS;
|
||||
if (!ground(r, c) || battle.unitAt(r, c)) continue;
|
||||
const d = Math.hypot(c - top.x, r - top.y);
|
||||
if (d < bd - 1e-9) { bd = d; best = [r, c]; }
|
||||
}
|
||||
return best ?? [unit.tileR, unit.tileC];
|
||||
}
|
||||
|
||||
export default {
|
||||
char_479_sleach: (bb, chess) => {
|
||||
const t0 = talentBb(chess, 0), t1 = talentBb(chess, 1);
|
||||
const allyAspd = num(t0['sleach_t_1[ally].attack_speed']), enemyAspd = num(t0['sleach_t_1[enemy].attack_speed']);
|
||||
const hidden = moduleBb(chess); // BEA-Y stage 2+: cost (the next ground operator's DP)
|
||||
const frontBlock = num(traitBb(chess).block_cnt);
|
||||
const camou = moduleOn(chess) && /迷彩/.test(String(chess?.trait?.desc ?? ''));
|
||||
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
|
||||
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
|
||||
const X1 = s2?.rangeGrid ?? [[2, 0], [1, -1], [1, 0], [1, 1], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, -1], [-1, 0], [-1, 1], [-2, 0]];
|
||||
const R21 = s3?.rangeGrid ?? [[2, 0], [1, 0], [1, 1], [0, 0], [0, 1], [0, 2], [-1, 0], [-1, 1], [-2, 0]];
|
||||
|
||||
const dp1 = dpOverTime({ total: num(b1.value), step: num(b1.cost, 1), iv: num(b1.interval, 0.44) });
|
||||
const dp2 = dpOverTime({ total: num(b2.value), step: num(b2['sleach_s_2[cost].cost'], 1), iv: num(b2['sleach_s_2[cost].interval'], 0.75) });
|
||||
|
||||
/** S2's flag effect on the operator of its tile: DEF +def, 生命回复速度 from her ATK (re-read every second). */
|
||||
const s2Effect = (battle, unit) => {
|
||||
const m = unit.mem, f = m.sleachFlag;
|
||||
if (!f || !up(unit)) return;
|
||||
if (m.sleachS2Regen == null || m.sleachS2T + 1e-9 >= m.sleachS2Next) {
|
||||
m.sleachS2Regen = unit.s.atk * num(b2.atk_to_hp_recovery_ratio);
|
||||
while (m.sleachS2T + 1e-9 >= m.sleachS2Next) m.sleachS2Next += 1;
|
||||
}
|
||||
const a = battle.unitAt(f.r, f.c);
|
||||
const key = `${S2_KEY}:${unit.id}`;
|
||||
const cur = m.sleachS2On;
|
||||
if (cur && cur !== a) battle.removeBuff(cur, key);
|
||||
m.sleachS2On = null;
|
||||
if (!isOp(a)) return;
|
||||
const b = a.findBuff(key);
|
||||
if (!b || b.mods?.hpRegen !== m.sleachS2Regen) battle.addBuff(a, { key, mods: { defPct: num(b2.def), hpRegen: m.sleachS2Regen }, source: unit, tags: ['skill'] });
|
||||
m.sleachS2On = a;
|
||||
};
|
||||
const s2Clear = (battle, unit) => {
|
||||
const a = unit.mem.sleachS2On;
|
||||
if (a) battle.removeBuff(a, `${S2_KEY}:${unit.id}`);
|
||||
unit.mem.sleachS2On = null;
|
||||
};
|
||||
|
||||
return {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: 'duration',
|
||||
attack: { noAttack: true },
|
||||
onStart({ unit }) { dp1.start(unit); },
|
||||
onTick({ battle, unit, dt }) { dp1.tick(battle, unit, dt); },
|
||||
onEnd({ battle, unit, reason }) { dp1.end(battle, unit, reason); },
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
attack: { noAttack: true },
|
||||
onStart({ battle, unit }) {
|
||||
dp2.start(unit);
|
||||
const [r, c] = s2Tile(battle, unit, X1);
|
||||
unit.mem.sleachFlag = { r, c };
|
||||
Object.assign(unit.mem, { sleachS2T: 0, sleachS2Next: 0, sleachS2Regen: null, sleachS2On: null });
|
||||
battle.fx('zone', { x: c, y: r, radius: 0.5, dur: num(s2?.duration, 15), id: unit.id, skill: 'sleach:flag' });
|
||||
s2Effect(battle, unit);
|
||||
},
|
||||
onTick({ battle, unit, dt }) {
|
||||
dp2.tick(battle, unit, dt);
|
||||
unit.mem.sleachS2T += dt;
|
||||
s2Effect(battle, unit);
|
||||
},
|
||||
onEnd({ battle, unit, reason }) {
|
||||
dp2.end(battle, unit, reason);
|
||||
s2Clear(battle, unit);
|
||||
unit.mem.sleachFlag = null;
|
||||
},
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
attack: { noAttack: true },
|
||||
onStart({ battle, unit }) {
|
||||
const cost = num(b3.cost);
|
||||
if (cost > 0) { battle.addDp(unit.ownerId, cost); battle.fx('dp', { x: unit.x, y: unit.y, n: cost, id: unit.id }); }
|
||||
const [r, c] = s3Tile(battle, unit, R21);
|
||||
const seq = (unit.mem.sleachThrow ?? 0) + 1;
|
||||
unit.mem.sleachThrow = seq;
|
||||
unit.mem.sleachFlag = { r, c, s3: true };
|
||||
battle.fx('zone', { x: c, y: r, radius: 1.5, dur: num(s3?.duration, 10), id: unit.id, skill: 'sleach:flag' });
|
||||
unit.mem.sleachS3Acc = AURA_IV; // the 停顿 / 脆弱 start at once (before the flag lands)
|
||||
// the strike: FLAG_FLIGHT s later on the flag's 3×3 (ground and air), with her ATK of the throw
|
||||
const atk = unit.s.atk, scale = num(b3.atk_scale, 1), stun = num(b3.stun);
|
||||
battle.after(FLAG_FLIGHT, () => {
|
||||
if (battle.finished) return;
|
||||
const keys = [...areaKeys(unit, r, c)];
|
||||
battle.fx('aoe', { x: c, y: r, radius: 1.5, id: unit.id, skill: 'sleach:strike' });
|
||||
for (const e of battle.enemiesInKeys(keys, unit, ANY)) {
|
||||
if (!e.alive) continue;
|
||||
battle.dealDamage(unit, e, { amount: atk * scale, type: 'phys', isSkill: true, tags: ['skill', 'sleach:strike'] });
|
||||
if (e.alive && stun > 0) battle.applyStatus(e, 'stun', { duration: stun, source: unit });
|
||||
}
|
||||
}, { owner: unit });
|
||||
},
|
||||
onTick({ battle, unit, dt }) {
|
||||
const f = unit.mem.sleachFlag;
|
||||
if (!f) return;
|
||||
unit.mem.sleachS3Acc += dt;
|
||||
if (unit.mem.sleachS3Acc + 1e-9 < AURA_IV) return;
|
||||
unit.mem.sleachS3Acc = 0;
|
||||
const frag = num(b3['debuff.damage_scale'], num(b3.damage_scale, 1) - 1);
|
||||
for (const e of battle.enemiesInKeys([...areaKeys(unit, f.r, f.c)], unit, ANY)) {
|
||||
battle.applyStatus(e, 'sluggish', { duration: AURA_DUR, source: unit, resistApplied: true });
|
||||
if (frag > 0) battle.applyStatus(e, 'fragile', { duration: AURA_DUR, value: frag, source: unit, resistApplied: true });
|
||||
}
|
||||
},
|
||||
onEnd({ unit }) { unit.mem.sleachFlag = null; },
|
||||
},
|
||||
},
|
||||
talents: [
|
||||
{ install(battle, unit) { // 不退之旗: the flag's 3×3 — operators ASPD +ally, enemies ASPD +enemy (follows the flag)
|
||||
if (!allyAspd && !enemyAspd) return;
|
||||
const aKey = `${FLAG_ALLY}:${unit.id}`, eKey = `${FLAG_ENEMY}:${unit.id}`;
|
||||
let cur = new Set();
|
||||
battle.every(AURA_IV, () => {
|
||||
const next = new Set();
|
||||
if (up(unit)) {
|
||||
const [r, c] = flagTile(unit);
|
||||
const keys = areaKeys(unit, r, c);
|
||||
if (allyAspd) {
|
||||
for (const a of battle.allyUnits) {
|
||||
if (!isOp(a) || !keys.has(tileKey(a.tileR, a.tileC))) continue;
|
||||
battle.addBuff(a, { key: aKey, duration: AURA_DUR, refresh: 'extend', mods: { aspd: allyAspd }, source: unit, tags: ['aura'] });
|
||||
next.add(a);
|
||||
}
|
||||
}
|
||||
if (enemyAspd) {
|
||||
for (const e of battle.enemiesInKeys([...keys], unit, ANY)) {
|
||||
battle.addBuff(e, { key: eKey, duration: AURA_DUR, refresh: 'extend', mods: { aspd: enemyAspd }, source: unit, tags: ['aura'] });
|
||||
next.add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const x of cur) if (!next.has(x)) { battle.removeBuff(x, aKey); battle.removeBuff(x, eKey); }
|
||||
cur = next;
|
||||
}, { owner: unit, immediate: true });
|
||||
} },
|
||||
{ install(battle, unit) { // 精神感召 (+ BEA-Y Lv2+: the next ground operator +cost DP, once per deployment of hers)
|
||||
const cut = Math.abs(num(t1.value)), bonus = num(hidden.cost);
|
||||
if (!(cut > 0) && !(bonus > 0)) return;
|
||||
const cuts = new Map();
|
||||
let pending = false, bonusReady = false;
|
||||
const mine = (a) => !!a && a.kind === 'op' && a !== unit && a.ownerId === unit.ownerId;
|
||||
const waiting = (a) => mine(a) && !a.alive && !a.removed;
|
||||
const applyCut = (a) => {
|
||||
if (!(cut > 0) || cuts.has(a)) return;
|
||||
const d = Math.min(cut, Math.max(0, num(a.base.cost)));
|
||||
if (d > 0) a.base.cost -= d;
|
||||
cuts.set(a, d);
|
||||
};
|
||||
const restore = () => {
|
||||
for (const [a, d] of cuts) if (d > 0) a.base.cost += d;
|
||||
cuts.clear();
|
||||
};
|
||||
battle.on('deploy', (ctx) => {
|
||||
const a = ctx.unit;
|
||||
if (a === unit) {
|
||||
restore();
|
||||
pending = true;
|
||||
bonusReady = bonus > 0;
|
||||
for (const x of battle.allyUnits) if (waiting(x)) applyCut(x);
|
||||
return;
|
||||
}
|
||||
if (!pending || !mine(a) || ctx.move) return;
|
||||
// the next deployment of one of her player's operators uses both up
|
||||
restore();
|
||||
pending = false;
|
||||
if (bonusReady) {
|
||||
bonusReady = false;
|
||||
if (String(a.def?.position ?? '').toUpperCase() === 'MELEE') {
|
||||
battle.addDp(unit.ownerId, bonus);
|
||||
battle.fx('dp', { x: a.x, y: a.y, n: bonus, id: unit.id });
|
||||
}
|
||||
}
|
||||
}, { owner: unit });
|
||||
battle.on('death', (ctx) => {
|
||||
if (ctx.unit === unit) { restore(); pending = false; bonusReady = false; return; }
|
||||
if (pending && waiting(ctx.unit)) applyCut(ctx.unit);
|
||||
}, { owner: unit });
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// BEA-X 牧人的歌: while a skill of hers runs, the operator in front of her (not stunned / asleep) block +block_cnt
|
||||
if (frontBlock > 0) {
|
||||
const key = `${FRONT_KEY}:${unit.id}`;
|
||||
let given = new Set();
|
||||
let timer = null;
|
||||
const give = () => {
|
||||
if (!up(unit) || !unit.skill?.active) return;
|
||||
const [r, c] = offsetTile(unit.tileR, unit.tileC, 0, 1, unit.dir);
|
||||
const a = battle.grid.inRect(r, c) ? battle.unitAt(r, c) : null;
|
||||
if (!isOp(a) || a === unit || a.s.flags.stun || a.s.flags.freeze || a.s.flags.sleep) return;
|
||||
if (!a.findBuff(key)) battle.addBuff(a, { key, mods: { blockCnt: frontBlock }, source: unit, tags: ['trait'] });
|
||||
given.add(a);
|
||||
};
|
||||
battle.on('skillStart', (ctx) => {
|
||||
if (ctx.unit !== unit) return;
|
||||
give();
|
||||
if (timer) timer.cancel();
|
||||
timer = battle.every(FRONT_IV, () => give(), { owner: unit });
|
||||
}, { owner: unit });
|
||||
battle.on('skillEnd', (ctx) => {
|
||||
if (ctx.unit !== unit) return;
|
||||
if (timer) { timer.cancel(); timer = null; }
|
||||
for (const a of given) battle.removeBuff(a, key);
|
||||
given = new Set();
|
||||
}, { owner: unit });
|
||||
}
|
||||
// BEA-Y “友谊万岁”: 迷彩 while a skill of hers runs
|
||||
if (camou) {
|
||||
battle.on('skillStart', (ctx) => { if (ctx.unit === unit) battle.addBuff(unit, { key: CAMOU_KEY, flags: { camou: true }, tags: ['trait'] }); }, { owner: unit });
|
||||
battle.on('skillEnd', (ctx) => { if (ctx.unit === unit) battle.removeBuff(unit, CAMOU_KEY); }, { owner: unit });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user