feedback5 O17: 自选 kits for 琴柳, 可露希尔 (with her 指挥中心) and 澄闪

Three owned-6★ picks get operator kits (kits/ops/op-sleach.js, op-closur.js, op-gdglow.js, appended to OPERATOR_KIT_FILES),
every skill under `skills`, every talent with its module change, the trait and every module at stages 1 and 3, numbers from
the composed record; sources: backups.json, PRTS (skill / talent 备注, 指挥中心, 分支特性信息), Terra Wiki and the client's
charpacks, skill / projectile / token prefabs and buff templates.
- 琴柳: the flag as a tile effect (its 3×3: operators ASPD +10/+13, enemies −10/−13, following S2 / S3's throw); 精神感召's
  −2 on the operators off the field until the next deployment, BEA-Y Lv3 +2 DP on a 近战位 one; S1 / S2 DP over time, S2's
  DEF + 生命回复速度 (hpRegen) on the flag's operator, S3's 停顿 + 脆弱 area and the strike 1 s later; BEA-X block +1 in
  front, BEA-Y 迷彩 while a skill runs.
- 可露希尔: the 指挥中心 (placed piece or tactical point; 禁疗, range x-5 / x-4 / x-6 by her skill, 15 s 战术点形态, withdrawn
  with her) marks her 援军; trait ×1.5 vs enemies they block; S1 shields + growing DP (AUTO ⇒ SP_FULL), S2 援军 DEF / block,
  2 targets, the 40 % refund, S3 range union, 迟钝 stacks (a kit-local applyStrongest 'slowdown'), +1 target per 9 attacks;
  极限调度 ATK for 罗德岛 operators; TAC-X ×0.85.
- 澄闪: skill drones (1 + attack@cnt) locking her first target with their own ramps, 技能直接伤害 that 缴械 does not stop;
  信标的愤怒's per-drone stacks (1.5 % × stacks, sure past 40) and its 1.1 blast; S2 (AUTO, self) ⇒ SP_FULL, range 3-18;
  S3 whole field, 停顿 per hit; 精准导流 resIgnoreFlat.
No engine change. Tests: test/content/op_sleach.test.js (8), op_closur.test.js (9), op_gdglow.test.js (7).
Golden: only diy moved — diy-094 … diy-097 re-chunked (the configurations after the new kits shift), diy-098 … diy-103 new
(the three operators' forms × modules × skills); roster, bonds, fields, matches and standins unchanged.
This commit is contained in:
sganggs committed 2026-10-06 03:08:39 +08:00
1 parent 78251f499c
commit 5d5336e233
8 files changed
+2195 -99

No files matched your search

+3
View File
@@ -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',
]);
/**
+393
View File
@@ -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 });
} },
],
};
},
};
+168
View File
@@ -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) });
} },
],
};
},
};
+376
View File
@@ -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 });
}
},
};
},
};
+346
View File
@@ -0,0 +1,346 @@
// test/content/op_closur.test.js — the 自选 operator kit of 可露希尔 (char_4228_closur, 6★ 战术家; kit
// server/sim/content/kits/ops/op-closur.js) and of her 援军 指挥中心 (token_10066_closur_ourbase), fielded the production way (a
// DIY slot + its `diy` pick, simdata getDiy; the 指挥中心 as the placed hand piece of her player) in every form: tiers 5 / 6,
// normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or TAC-X 给自己的小奖杯 at stage 1
// (tier 5) / 3 (tier 6). Numbers from data/backups.json; the fidelity checklist of kits/README.md.
// Run: node --test test/content/op_closur.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
import { BASE, SHIELD_KEY, S2_KEY, RHODES_KEY, SLOWDOWN } from '../../server/sim/content/kits/ops/op-closur.js';
import { COLS } from '../../server/sim/constants.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const CL = 'char_4228_closur';
const FORMS = BACKUPS.units[CL].forms;
const TOKREC = BACKUPS.tokens[BASE];
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const TX = 'uniequip_002_closur';
const S1 = 'skchr_closur_1', S2 = 'skchr_closur_2', S3 = 'skchr_closur_3';
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
const talentOf = (tier, elite, mod, i) => modOf(tier, elite ? mod : null)?.talentChanges?.find((t) => t.talentIndex === i) ?? formOf(tier, elite).talents.find((t) => t.index === i);
const tokVariant = (tier, elite) => TOKREC.variants[`${CL}@${statusOf(tier, elite)}`];
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_atk: dummy('enemy_atk', { atk: 2000, bat: 1 }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, TX].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
const YAK = 'chess_char_1_02_a', TEXAS = 'chess_char_1_08_a', PODEGO = 'chess_char_1_13_a';
const tk = (r, c) => r * COLS + c;
/**
* 可露希尔 as uid 1 at (row, col) facing RIGHT; `base` = the 指挥中心 piece's tile (uid 2) or null; `others` as given.
*/
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, base = [10, 6], others = [], seed = 5 } = {}) {
const op = { uid: 1, diy: { slot: SLOT[tier], charId: CL, skillIndex: skill, uniEquipId: mod }, elite, row, col };
const piece = base ? [{ uid: 2, kind: 'token', tokenId: BASE, ownerUid: 1, row: base[0], col: base[1] }] : [];
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack'], captureNoisy: true,
units: [op, ...piece, ...others],
});
h.step();
return { h, u: h.unit(1), t: h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === BASE && a.alive) ?? null };
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const dpOf = (h) => h.b.players[0].dp;
const hitsOn = (h, u, e) => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e);
/** Record the DP gains' times. */
function dpLog(h) {
const log = [];
let last = dpOf(h);
h.b.on('tick', () => { const v = dpOf(h); if (v !== last) { log.push([h.b.time, v - last]); last = v; } });
return log;
}
test('可露希尔 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-3, 战术家 (ranged physical, hits air, blocks 1), ground-targetable, the triggers (S1 at full SP); the placed 指挥中心 deploys with its variant\'s stats, no attack, 禁疗, blocks 2, its range by her skill (x-5 / x-4 / x-6); no generic 援军', () => {
assert.equal(OPERATOR_KITS[CL], KITS[CL]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const { h, u, t } = field({ tier, elite, mod, skill });
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, sk = form.skills[skill];
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [CL, SLOT[tier], sk.skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [1, 'ranged', true, 'phys', 'tactician', 1], `${label(f)}: 战术家`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-3`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
assert.deepEqual([u.skill.rule, u.skill.spCost], [skill === 0 ? 'SP_FULL' : sk.trigger.rule, sk.spCost], `${label(f)}: trigger`);
if (skill > 0) assert.deepEqual([sk.trigger.rule, sk.trigger.rawRule], ['SP_FULL', 'ALWAYS'], `${label(f)}: the 战术家 row`);
else assert.equal(sk.trigger.rule, 'DEFAULT', `${label(f)}: the data's AUTO rule (the kit fires it at full SP)`);
const v = tokVariant(tier, elite);
assert.ok(t && t.uid === 2 && t.tileR === 10 && t.tileC === 6, `${label(f)}: the placed 指挥中心 on its tile`);
assert.deepEqual([t.base.maxHp, t.base.def, t.s.blockCnt, !!t.profile.noAttack, !!t.s.flags.noHeal], [v.stats.maxHp, v.stats.def, 2, true, true], `${label(f)}: 指挥中心`);
const grid = skill === 0 ? v.skill.rangeGrid : v.bySkill[String(skill)].skill.rangeGrid;
assert.deepEqual(t.liveRangeGrid, grid, `${label(f)}: its range by her S${skill + 1}`);
assert.equal(h.b.allyUnits.filter((a) => a.defId === 'token_tactician_reinforce').length, 0, `${label(f)}: no generic 援军`);
assert.equal(u.trait.reinforcement, t, `${label(f)}: her 援军`);
done(h);
}
}
assert.deepEqual([modOf(5, TX).attr, modOf(6, TX).attr], [{ maxHp: 150, atk: 20, def: 15 }, { maxHp: 180, atk: 30, def: 25 }]);
assert.deepEqual([tokVariant(5, false).stats.maxHp, tokVariant(5, true).stats.maxHp, tokVariant(5, true).skill.rangeId, tokVariant(5, true).bySkill['1'].skill.rangeId, tokVariant(5, true).bySkill['2'].skill.rangeId], [2620, 3054, 'x-5', 'x-4', 'x-6']);
});
test('a 自选 pick: 可露希尔 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(CL));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(CL), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: CL, skillIndex: 1, uniEquipId: TX } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[5]]: { charId: CL, skillIndex: 1, uniEquipId: TX } } });
});
test('精准投放 without a placed piece: the 指挥中心 comes on the tactical point (a walkable tile of her range, the enemy path first); her leaving withdraws it for good, her redeploy brings a new one; knocked out it sits in its 战术点形态 for 15 s (off the fight, its tile kept, no 援军) and comes back at full HP', () => {
const { h, u, t } = field({ tier: 6, elite: true, mod: TX, skill: 0, base: null });
assert.ok(t && t.uid == null, 'summoned');
assert.ok(u.baseRangeKeys.includes(tk(t.tileR, t.tileC)) && h.b.grid.groundPassable(t.tileR, t.tileC), 'a walkable tile of her range');
assert.ok(h.b.groundPathTiles().has(tk(t.tileR, t.tileC)), 'on an enemy ground path');
done(h);
// the placed piece: knock-out ⇒ 战术点形态
const r = field({ tier: 5, elite: true, mod: TX, skill: 1, others: [{ uid: 3, chessId: YAK, row: 11, col: 6 }] });
const refresh = tokVariant(5, true).talents.find((x) => x.bb.interval != null).bb.interval;
assert.equal(refresh, 15);
r.h.b.kill(r.t, null);
assert.ok(!r.t.alive && !r.t.removed && r.t.mem.closurTac, 'in its 战术点形态');
assert.ok(r.h.b.isReservedTile(10, 6), 'its tile kept');
r.u.skill.gainSp(999);
r.h.run(0.5);
assert.equal(r.h.unit(3).findBuff(`${S2_KEY}:${r.u.id}`), null, 'no 援军 while it is down (角峰 in its range unmarked)');
r.h.run(refresh - 0.6);
assert.ok(!r.t.alive, 'still down before 15 s');
r.h.run(0.3);
assert.ok(r.t.alive && r.t.tileR === 10 && r.t.tileC === 6, 'back on its tile after 15 s');
approx(r.t.hp, r.t.s.maxHp, 'at full HP');
r.h.run(0.2);
assert.ok(r.h.unit(3).findBuff(`${S2_KEY}:${r.u.id}`), '角峰 marked again (S2 runs)');
// her leaving: withdrawn for good; her redeploy: a new one on the board tile
r.h.b.kill(r.u, null);
assert.ok(!r.t.alive && r.t.removed, 'withdrawn with her (no 战术点形态)');
r.h.b.redeploy(r.u, { free: true });
const n = r.h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === BASE && a.alive);
assert.ok(n && n !== r.t && n.tileR === 10 && n.tileC === 6, 'a new 指挥中心 on the board tile');
done(r.h);
});
test('Trait: her damage on an enemy blocked by her 援军 — the 指挥中心 or an ally of its range — ×atk_scale (1.5); ×1 vs an enemy blocked by an ally outside its range', () => {
for (const f of [[5, false, null], [6, true, TX]]) {
const [tier, elite, mod] = f;
// S1 carried: the 指挥中心's x-5 around (10,6) holds (11,6) (角峰), not (11,7) (德克萨斯)
const { h, u, t } = field({ tier, elite, mod, skill: 0, others: [{ uid: 3, chessId: YAK, row: 11, col: 6 }, { uid: 4, chessId: TEXAS, row: 11, col: 7 }] });
const scale = formOf(tier, elite).trait.bb.atk_scale;
assert.equal(scale, 1.5);
for (const [pos, blocker, mul] of [[[10, 6], t, scale], [[11, 6], h.unit(3), scale], [[11, 7], h.unit(4), 1]]) {
const e = h.spawn('enemy_dummy', { pos });
assert.ok(h.runUntil(() => e.blockedBy === blocker && hitsOn(h, u, e).length > 0, 5), `${label(f)}: ${pos} blocked and hit`);
const c = hitsOn(h, u, e)[0];
approx(c.amount, u.s.atk * mul, `${label(f)}: ×${mul} on the enemy blocked at ${pos}`);
h.b.kill(e, null);
h.step();
}
done(h);
}
});
test('S1 递归策略 (AUTO, at full SP): every 援军 — the 指挥中心 and the allies of its x-5, not one outside, not a 孤立 one — gets one 护盾 layer (不叠加); 3 DP, then 4, 5 … up to 6 (normal) / 7 (elite), one at a time every 8 / n s from half that; the count restarts with a redeploy', () => {
for (const f of [[5, false, null], [6, true, TX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.duration, sk.bb.cost, sk.bb.cost_per_add, sk.bb.cost_add_max, sk.bb.shield_cnt, sk.initSp], [8, 3, 1, elite ? 7 : 6, 1, 0], label(f));
const others = [{ uid: 3, chessId: YAK, row: 11, col: 6 }, { uid: 4, chessId: TEXAS, row: 11, col: 7 }, { uid: 5, chessId: PODEGO, row: 9, col: 6 }];
const { h, u, t } = field({ tier, elite, mod, skill: 0, others });
h.b.addBuff(h.unit(5), { key: 'test:isolated', flags: { isolated: true } });
const log = dpLog(h);
h.b.players[0].dp = 0;
const casts = [];
h.b.on('skillStart', (c) => { if (c.unit === u) casts.push(h.b.time); });
assert.ok(h.runUntil(() => u.skill.activations === 1, sk.spCost + 1), `${label(f)}: cast at full SP, no enemy on the field`);
h.step();
for (const a of [t, h.unit(3)]) assert.equal(a.findBuff(`${SHIELD_KEY}:${u.id}`)?.shieldHits, 1, `${label(f)}: ${a.defId} one 护盾 layer`);
for (const a of [h.unit(4), h.unit(5), u]) assert.equal(a.findBuff(`${SHIELD_KEY}:${u.id}`), null, `${label(f)}: ${a.defId} no 护盾`);
// the 护盾 takes one hit whole
const e = h.spawn('enemy_atk', { pos: [10, 6] });
assert.ok(h.runUntil(() => !t.findBuff(`${SHIELD_KEY}:${u.id}`), 4), `${label(f)}: spent by a hit`);
approx(t.hp, t.s.maxHp, `${label(f)}: that hit blocked whole`);
h.b.kill(e, null);
// the DP of the casts: n = 3, 4, … ≤ max; each over 8 s every 8 / n from half that
const want = [];
for (let k = 0; k < 6; k++) want.push(Math.min(sk.bb.cost_add_max, sk.bb.cost + k));
h.runUntil(() => u.skill.activations >= 6 && !u.skill.active, 6 * (sk.spCost + 9));
for (let k = 0; k < 6; k++) {
const s0 = casts[k], n = want[k];
const gains = log.filter(([tt]) => tt > s0 - 1e-9 && tt < s0 + 8 + 0.05);
assert.equal(gains.reduce((a, [, d]) => a + d, 0), n, `${label(f)}: cast ${k + 1} gives ${n}`);
approx(gains[0][0] - s0, 8 / n / 2, `${label(f)}: cast ${k + 1} first DP after ${8 / n / 2} s`, 0.06);
}
// a redeploy restarts the count
h.b.retreat(u);
h.b.redeploy(u, { free: true });
const before = dpOf(h);
h.runUntil(() => u.skill.activations >= 7 && !u.skill.active, sk.spCost + 10);
assert.equal(dpOf(h) - before, sk.bb.cost, `${label(f)}: back to ${sk.bb.cost} after a redeploy`);
done(h);
}
});
test('S2 模型扩展: +cost DP at once and 15 one per 2 s; ATK +atk, 2 targets; every 援军 (x-4) DEF +def and block +1; the enemies they block are her targets beyond her range; an operator of hers deployed on the 指挥中心\'s range meanwhile gives back ceil(40 % × its cost) — not after a 【移动】, not outside the range', () => {
for (const f of [[5, false, null], [6, true, TX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.duration, sk.bb.cost, sk.bb.cost_period, sk.bb['closur_s_2[add_cost_period].interval'], sk.bb.atk, sk.bb.def, sk.bb.block_cnt, sk.bb['attack@max_target'], sk.bb.cost_return],
[30, elite ? 7 : 6, 15, 2, elite ? 0.55 : 0.4, elite ? 0.45 : 0.3, 1, 2, 0.4], label(f));
// 角峰 (11,7) is in the x-4 of (10,6); 德克萨斯 (12,8) is outside it and outside her 3-3; 波登可 (9,9) far
const others = [{ uid: 3, chessId: YAK, row: 11, col: 7 }, { uid: 4, chessId: TEXAS, row: 12, col: 8 }, { uid: 5, chessId: PODEGO, row: 12, col: 4 }];
const { h, u, t } = field({ tier, elite, mod, skill: 1, others });
const yak = h.unit(3), texas = h.unit(4), podego = h.unit(5);
const log = dpLog(h);
h.b.players[0].dp = 0;
const atk0 = u.s.atk, yakDef = yak.s.def, yakBlock = yak.s.blockCnt;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
const t0 = h.b.time;
approx(dpOf(h), sk.bb.cost, `${label(f)}: +${sk.bb.cost} at once`);
approx(u.s.atk, atk0 * (1 + sk.bb.atk + (u.findBuff(RHODES_KEY)?.mods.atkPct ?? 0)) / (1 + (u.findBuff(RHODES_KEY)?.mods.atkPct ?? 0)), `${label(f)}: ATK +${sk.bb.atk * 100} %`, 1e-3);
h.run(0.2);
assert.deepEqual([t.s.blockCnt, yak.s.blockCnt], [3, yakBlock + 1], `${label(f)}: 援军 block +1`);
approx(yak.s.def - yakDef, yak.base.def * sk.bb.def, `${label(f)}: 援军 DEF +${sk.bb.def * 100} %`, 1e-3);
assert.equal(texas.findBuff(`${S2_KEY}:${u.id}`), null, `${label(f)}: not outside the x-4`);
// an enemy blocked by 角峰 outside her 3-3? (11,7) is inside it; one blocked by the 指挥中心 and two more in range: 2 targets
const es = [h.spawn('enemy_dummy', { pos: [10, 6] }), h.spawn('enemy_dummy', { pos: [9, 5] }), h.spawn('enemy_dummy', { pos: [11, 5] })];
const n0 = h.hooksOf('attack').length;
h.run(3);
const atks = h.hooksOf('attack').slice(n0).filter((c) => c.attacker === u);
assert.ok(atks.length >= 2 && atks.every((c) => c.targets.length === 2), `${label(f)}: 2 targets per attack (${atks.map((c) => c.targets.length)})`);
for (const e of es) h.b.kill(e, null);
// the refund: 角峰 (cost 19) redeployed on the x-4 ⇒ +8; 波登可 outside ⇒ nothing; a 【移动】 ⇒ nothing
h.step();
const d0 = dpOf(h);
h.b.kill(yak, null);
h.b.redeploy(yak, { free: true });
approx(dpOf(h) - d0, Math.ceil(yak.base.cost * 0.4), `${label(f)}: ceil(40 % × ${yak.base.cost})`);
const d1 = dpOf(h);
h.b.kill(podego, null);
h.b.redeploy(podego, { free: true });
approx(dpOf(h) - d1, 0, `${label(f)}: outside the range — nothing`);
assert.ok(h.b.moveRedeploy(yak, 9, 6), '【移动】');
approx(dpOf(h) - d1, 0, `${label(f)}: a 【移动】 — nothing`);
h.runUntil(() => !u.skill.active, 31);
approx(h.b.time - t0, 30, `${label(f)}: 30 s`, 0.01);
const periodic = log.filter(([tt]) => tt > t0 + 0.5 && tt <= t0 + 30.05).filter(([, d]) => d === 1);
assert.equal(periodic.length, 15, `${label(f)}: 15 DP over the skill`);
approx(periodic[0][0] - t0, 2, `${label(f)}: one per 2 s`, 0.06);
assert.equal(t.findBuff(`${S2_KEY}:${u.id}`), null, `${label(f)}: the 援军 buff ends with it`);
assert.equal(u.extraRangeKeys, null, `${label(f)}: no extra range after`);
done(h);
}
// the enemies her 援军 block are her targets beyond her own range: 角峰 at (11,8) — out of her 3-3 ([1, 4]) — is in the
// x-4 of a 指挥中心 at (10,7)
const { h, u } = field({ tier: 6, elite: true, mod: TX, skill: 1, base: [10, 7], others: [{ uid: 3, chessId: YAK, row: 11, col: 8 }] });
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
h.run(0.5);
assert.equal(e.blockedBy, h.unit(3));
assert.equal(hitsOn(h, u, e).length, 0, 'out of her range before');
u.skill.gainSp(999);
assert.ok(h.runUntil(() => hitsOn(h, u, e).length > 0, 4), 'hit while S2 runs');
approx(hitsOn(h, u, e)[0].amount, u.s.atk * 1.5, 'with the trait ×1.5');
done(h);
});
test('S3 Q.E.D.: 18 DP one per 1.66 s; attack interval 1.0 − 0.5 s; hits of attack@atk_scale × ATK with one 迟钝 stack each (4 % / 5 %, ≤ 10 stacks, one 3 s timer); one more target per 9 attacks (≤ 6); her range adds the 指挥中心\'s x-6 and her 援军\'s ranges', () => {
for (const f of [[5, false, null], [6, true, TX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.bb.cost_period, sk.bb['closur_s_3[add_cost_period].interval'], sk.bb.base_attack_time, sk.bb['attack@atk_scale'], sk.bb['attack@slow_down'], sk.bb['attack@slow_down_time'], sk.bb['attack@max_stack_cnt'], sk.bb['attack@slow_down_max'], sk.bb.attack_trigger_cnt, sk.bb.max_trigger_cnt],
[30, 18, 1.66, -0.5, elite ? 1.9 : 1.5, elite ? 0.05 : 0.04, 3, 10, elite ? 0.5 : 0.4, 9, 6], label(f));
const { h, u, t } = field({ tier, elite, mod, skill: 2 });
const log = dpLog(h);
// eight enemies in her 3-3 (a flyer among them): enough for 1 + 6 targets
const near = [[9, 4], [9, 5], [9, 6], [9, 7], [11, 4], [11, 5], [11, 6], [11, 7]].map((p, i) => h.spawn(i === 2 ? 'enemy_fly' : 'enemy_dummy', { pos: p }));
h.run(1);
h.b.players[0].dp = 0;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
const t0 = h.b.time;
approx(u.s.interval, 0.5, `${label(f)}: interval 0.5 s`);
let attacks = 0;
const perAttack = [];
h.b.on('attack', (c) => { if (c.attacker === u && u.skill.active) { attacks++; perAttack.push(c.targets.length); } });
h.runUntil(() => attacks >= 9 + 1, 10);
assert.ok(perAttack.slice(0, 9).every((n) => n === 1) && perAttack[9] === 2, `${label(f)}: one more target after 9 attacks (${perAttack})`);
// the 迟钝 on one enemy: 1 stack per hit, capped
const e0 = [...near].sort((a, b) => hitsOn(h, u, b).filter((c) => c.dmg.isSkill).length - hitsOn(h, u, a).filter((c) => c.dmg.isSkill).length)[0];
const sl = e0.findBuff(SLOWDOWN);
assert.ok(sl, `${label(f)}: 迟钝 on a hit enemy`);
const skillHits = (e) => hitsOn(h, u, e).filter((c) => c.dmg.isSkill);
const stacks = Math.min(10, skillHits(e0).length);
approx(sl.data.value, Math.min(sk.bb['attack@slow_down_max'], sk.bb['attack@slow_down'] * stacks), `${label(f)}: ${stacks} stacks`);
approx(e0.s.moveSpeed, e0.base.moveSpeed * (1 - sl.data.value), `${label(f)}: move speed ×(1 − v)`);
for (const c of skillHits(e0).slice(0, 3)) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'], `${label(f)}: ${sk.bb['attack@atk_scale'] * 100} % ATK`);
h.runUntil(() => !u.skill.active, 31);
approx(h.b.time - t0, 30, `${label(f)}: 30 s`, 0.01);
assert.equal(Math.max(...perAttack), 7, `${label(f)}: at most 1 + 6 targets`);
approx(log.reduce((a, [tt, d]) => a + (tt > t0 - 1e-9 && tt < t0 + 30.05 ? d : 0), 0), 18, `${label(f)}: 18 DP`);
approx(log.find(([tt]) => tt > t0 - 1e-9)[0] - t0, 1.66, `${label(f)}: the first after 1.66 s`, 0.06);
// the cap after 10 hits, one timer: 3 s without a hit clears every stack
const capped = near.find((e) => skillHits(e).length >= 10);
assert.ok(capped);
h.run(0.1);
h.b.kill(u, null);
h.run(3.1);
assert.equal(capped.findBuff(SLOWDOWN), null, `${label(f)}: gone 3 s after the last hit`);
done(h);
void t;
}
// her range adds the 指挥中心's x-6: (10,8) is outside her 3-3 (cols 4–7), inside the x-6 of (10,6) ([0, 2])
const { h, u } = field({ tier: 6, elite: true, mod: TX, skill: 2 });
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
h.run(2);
assert.equal(hitsOn(h, u, far).length, 0, 'out of her range before');
u.skill.gainSp(999);
assert.ok(h.runUntil(() => hitsOn(h, u, far).length > 0, 3), 'hit while S3 runs');
assert.ok(u.extraRangeKeys.includes(tk(10, 8)) && u.extraRangeKeys.includes(tk(8, 6)), 'the x-6 in her range');
done(h);
});
test('T2 极限调度: from the battle start the 【罗德岛】 operators of her player (herself too) ATK +4 % (TAC-X stage 3 +8 %) for the whole battle; others not', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const tb = talentOf(tier, elite, mod, 1).bb;
assert.deepEqual(tb, tier === 6 && elite && mod === TX ? { cost: -5, atk: 0.08 } : { cost: -3, atk: 0.04 }, label(f));
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 3, chessId: PODEGO, row: 12, col: 3 }, { uid: 4, chessId: YAK, row: 11, col: 3 }] });
const podego = h.unit(3), yak = h.unit(4);
assert.equal(podego.def.raw.nationId, 'rhodes');
for (const a of [u, podego]) assert.equal(a.findBuff(RHODES_KEY)?.mods.atkPct, tb.atk, `${label(f)}: ${a.defId}`);
assert.equal(yak.findBuff(RHODES_KEY), null, `${label(f)}: 角峰 is no 罗德岛 operator`);
h.b.kill(u, null);
h.run(1);
assert.equal(podego.findBuff(RHODES_KEY)?.mods.atkPct, tb.atk, `${label(f)}: kept while she is down (携带)`);
done(h);
}
});
test('TAC-X “给自己的小奖杯”: damage on one of her 援军 from an enemy it blocks ×0.85 (stages 1 and 3); not without the module', () => {
for (const f of [[5, false, null], [5, true, null], [5, true, TX], [6, true, TX]]) {
const [tier, elite, mod] = f;
const { h, u, t } = field({ tier, elite, mod, skill: 0 });
const guard = elite && mod === TX ? modOf(tier, TX).talentChanges.find((x) => x.talentIndex === -1).bb.damage_scale : 1;
if (elite && mod === TX) assert.equal(guard, 0.85);
const e = h.spawn('enemy_atk', { pos: [10, 6] });
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === e && c.target === t), 5), `${label(f)}: hit by the enemy it blocks`);
const c = h.hooksOf('damaged').find((x) => x.source === e && x.target === t);
approx(c.amount, Math.max(2000 - t.s.def, 100) * guard, `${label(f)}: ×${guard}`);
done(h);
void u;
}
});
+226
View File
@@ -0,0 +1,226 @@
// test/content/op_gdglow.test.js — the 自选 operator kit of 澄闪 (char_377_gdglow, 6★ 驭械术师; kit
// server/sim/content/kits/ops/op-gdglow.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in every
// form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, FUN-X “辛劳之翼” or
// FUN-Y 梦想终将实现 at stage 1 (tier 5) / 3 (tier 6). Numbers from data/backups.json; the fidelity checklist of kits/README.md.
// Run: node --test test/content/op_gdglow.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
import { BLAST_RADIUS, DRONE_TAG, BLAST_TAG } from '../../server/sim/content/kits/ops/op-gdglow.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const GD = 'char_377_gdglow';
const FORMS = BACKUPS.units[GD].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const FX = 'uniequip_002_gdglow', FY = 'uniequip_003_gdglow';
const S1 = 'skchr_gdglow_1', S2 = 'skchr_gdglow_2', S3 = 'skchr_gdglow_3';
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
const talentOf = (tier, elite, mod, i) => modOf(tier, elite ? mod : null)?.talentChanges?.find((t) => t.talentIndex === i) ?? formOf(tier, elite).talents.find((t) => t.index === i);
const traitOf = (tier, elite, mod) => (elite && mod ? modOf(tier, mod).traitOverride : formOf(tier, elite).trait);
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_res: dummy('enemy_res', { res: 50 }), enemy_weak: dummy('enemy_weak', { hp: 10 }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, FX, FY].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
/** 澄闪 as uid 1 at (row, col) facing RIGHT. */
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, others = [], seed = 5 } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'attack'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: GD, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const mine = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u);
const droneHits = (h, u, e = null) => mine(h, u).filter((c) => c.dmg.tags?.includes(DRONE_TAG) && (!e || c.target === e));
test('澄闪 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, 驭械术师 (ranged arts, hits air, blocks 1; the trait ramp of the form — FUN-X init 0.35, FUN-Y cap 1.2), ground-targetable; S1 DEFAULT, S2 at full SP (AUTO on herself), S3 GDGLOW_SKILL_2; 精准导流 15 (FUN-Y stage 3 20)', () => {
assert.equal(OPERATOR_KITS[GD], KITS[GD]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const { h, u } = field({ tier, elite, mod, skill });
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, sk = form.skills[skill];
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [GD, SLOT[tier], sk.skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 100 + (m?.attr.aspd ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [1, 'ranged', true, 'arts', 'funnel', 1.3], `${label(f)}: 驭械术师`);
const tb = traitOf(tier, elite, mod).bb;
assert.deepEqual(u.profile.funnel, { init: tb.init_atk_scale, delta: tb.delta_atk_scale, max: tb.max_atk_scale }, `${label(f)}: the trait ramp`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
assert.deepEqual([u.skill.rule, u.skill.kind], [[sk.trigger.rule, 'SP_FULL', 'GDGLOW_SKILL_2'][skill], ['duration', 'toggle', 'duration'][skill]], `${label(f)}: S${skill + 1}`);
assert.equal(u.s.resIgnoreFlat, talentOf(tier, elite, mod, 1).bb.magic_resist_penetrate_fixed, `${label(f)}: 精准导流`);
done(h);
}
}
assert.deepEqual([skillOf(5, false, S1).trigger.rule, skillOf(5, false, S2).trigger.rule, skillOf(5, false, S2).skillType], ['DEFAULT', 'DEFAULT', 'AUTO']);
assert.deepEqual([modOf(5, FX).attr, modOf(6, FX).attr, modOf(5, FY).attr, modOf(6, FY).attr], [{ maxHp: 90, atk: 22 }, { maxHp: 125, atk: 38 }, { atk: 30, aspd: 5 }, { atk: 45, aspd: 7 }]);
assert.deepEqual([talentOf(6, true, FY, 1).bb.magic_resist_penetrate_fixed, talentOf(6, true, FX, 0).bb['attack@atk_scale_2'], talentOf(5, true, FX, 0).bb['attack@atk_scale_2']], [20, 3.6, 3]);
});
test('a 自选 pick: 澄闪 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(GD));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(GD), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: GD, skillIndex: 2, uniEquipId: FY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: GD, skillIndex: 2, uniEquipId: FY } } });
});
test('her normal attack (no skill): one drone hit per attack — init, +delta per hit on the same target, up to the cap (FUN-X from 0.35, FUN-Y to 1.2); 精准导流 lowers the RES it meets', () => {
for (const f of [[5, false, null], [5, true, FX], [6, true, FY]]) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2 });
const e = h.spawn('enemy_res', { pos: [10, 6] });
h.run(12);
const hits = mine(h, u).filter((c) => c.target === e && c.dmg.isAttack);
assert.ok(hits.length >= 8, `${label(f)}: ${hits.length} attacks`);
const tb = traitOf(tier, elite, mod).bb, pen = talentOf(tier, elite, mod, 1).bb.magic_resist_penetrate_fixed;
hits.slice(0, 8).forEach((c, i) => {
const ramp = Math.min(tb.max_atk_scale, tb.init_atk_scale + i * tb.delta_atk_scale);
approx(c.amount, u.s.atk * ramp * (1 - (50 - pen) / 100), `${label(f)}: attack ${i + 1} at ${ramp}`);
assert.equal(c.type, 'arts');
});
done(h);
}
});
test('S1 火花四溅 (DEFAULT: on her attack with an enemy in range): 25 s, ATK +atk, ASPD +aspd, no attack of her own; 2 drones lock her first target (in range or not afterwards) and hit it every attack interval — each its own ramp, arts 技能伤害 — and take her next first target once it falls; 缴械 stops nothing', () => {
for (const f of [[5, false, null], [6, true, FX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb.attack_speed, sk.bb['attack@cnt']], [25, elite ? 0.32 : 0.26, elite ? 38 : 29, 1], label(f));
const { h, u } = field({ tier, elite, mod, skill: 0 });
u.skill.gainSp(999);
h.run(1);
assert.equal(u.skill.activations, 0, `${label(f)}: no enemy, no cast`);
const a = h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3), `${label(f)}: cast as she is about to attack`);
const t0 = h.b.time;
approx(u.s.aspd, u.base.aspd + sk.bb.attack_speed, `${label(f)}: ASPD +${sk.bb.attack_speed}`);
h.b.applyStatus(u, 'disarm', { duration: 30, source: u, force: true });
h.run(3);
const hits = droneHits(h, u, a);
assert.ok(hits.length >= 4, `${label(f)}: drone hits while 缴械 (${hits.length})`);
assert.ok(hits.every((c) => c.dmg.isSkill && !c.dmg.isAttack && c.type === 'arts'), `${label(f)}: arts 技能伤害, no normal attack`);
const f0 = u.profile.funnel;
approx(hits[0].amount, u.s.atk * f0.init, `${label(f)}: the first at init`);
approx(hits[1].amount, u.s.atk * f0.init, `${label(f)}: the second drone too`);
approx(hits[2].amount, u.s.atk * (f0.init + f0.delta), `${label(f)}: +delta`);
approx(hits[2].t - hits[0].t, u.s.interval, `${label(f)}: one hit per attack interval each`, 0.05);
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u && c.t > t0).length, 0, `${label(f)}: no attack of her own`);
// locked: the enemy walks out of her range — still hit
a.x = 12; a.y = 12;
const n1 = droneHits(h, u, a).length;
h.run(2);
assert.ok(droneHits(h, u, a).length > n1, `${label(f)}: kept out of range`);
// it falls: the drones take her first target
const b = h.spawn('enemy_dummy', { pos: [11, 5] });
h.b.kill(a, null);
h.run(1.5);
assert.ok(droneHits(h, u, b).length >= 2, `${label(f)}: the next first target`);
h.runUntil(() => !u.skill.active, 25);
approx(h.b.time - t0, 25, `${label(f)}: 25 s`, 0.01);
assert.equal(u.mem.gdDrones, null, `${label(f)}: the drones back`);
done(h);
}
});
test('S2 电流翻涌 (AUTO, at full SP — no enemy needed): on for good, range 3-18, ATK +atk, 2 drones on an enemy of the 3-18 outside her 3-1', () => {
for (const f of [[5, false, null], [6, true, FY]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb['attack@cnt'], sk.rangeId, sk.initSp, sk.spCost], [-1, elite ? 0.45 : 0.32, 1, '3-18', 0, elite ? 70 : 75], label(f));
const { h, u } = field({ tier, elite, mod, skill: 1 });
h.run(sk.spCost - 0.5);
assert.equal(u.skill.activations, 0, `${label(f)}: not before its SP is full`);
assert.ok(h.runUntil(() => u.skill.active, 1), `${label(f)}: on at full SP with no enemy`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `${label(f)}: 3-18`);
// (12,4) = [2, 0]: in the 3-18, not in the 3-1
const e = h.spawn('enemy_fly', { pos: [12, 4] });
h.run(3);
const hits = droneHits(h, u, e);
assert.ok(hits.length >= 4, `${label(f)}: the drones on it (${hits.length})`);
approx(hits[0].amount, u.s.atk * u.profile.funnel.init, `${label(f)}: ATK +${sk.bb.atk * 100} % (live ATK)`);
h.run(200);
assert.ok(u.skill.active, `${label(f)}: 持续时间无限`);
done(h);
}
});
test('S3 澄净闪耀 (GDGLOW_SKILL_2: an enemy anywhere): 30 s, no attack of her own, the whole field, ATK +atk, 3 drones, each hit 0.5 s 停顿', () => {
for (const f of [[5, false, null], [6, true, FY]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.bb.atk, sk.bb['attack@cnt'], sk.bb['attack@sluggish']], [30, elite ? 0.55 : 0.4, 2, 0.5], label(f));
const { h, u } = field({ tier, elite, mod, skill: 2 });
u.skill.gainSp(999);
h.run(1);
assert.equal(u.skill.activations, 0, `${label(f)}: no enemy, no cast`);
const e = h.spawn('enemy_dummy', { pos: [12, 10] }); // far out of her 3-1
assert.ok(h.runUntil(() => u.skill.active, 1), `${label(f)}: cast with an enemy anywhere`);
const t0 = h.b.time;
h.run(0.2);
assert.equal(u.mem.gdDrones.length, 3, `${label(f)}: 3 drones`);
h.run(3);
const hits = droneHits(h, u, e);
assert.ok(hits.length >= 6, `${label(f)}: hit across the field (${hits.length})`);
const sl = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'sluggish' && c.target === e);
assert.ok(sl.length >= hits.length - 1, `${label(f)}: 停顿 per hit`);
approx(sl[0].duration, 0.5, `${label(f)}: 0.5 s`);
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u && c.t > t0).length, 0, `${label(f)}: 停止攻击`);
h.runUntil(() => !u.skill.active, 30);
approx(h.b.time - t0, 30, `${label(f)}: 30 s`, 0.01);
done(h);
}
});
test('T1 信标的愤怒: per drone 1.5 % × its stacks (from 1, +1 per miss, sure past 40): ~10 % of the drone attacks; the blast — atk_scale_2 × ATK arts within 1.1 of the target (air too), no ramp (FUN-X stage 3 360 %), 停顿 under S3 — sends the drone back and restarts its count; reset at a deployment', () => {
for (const f of [[5, false, null], [6, true, FX]]) {
const [tier, elite, mod] = f;
const tb = talentOf(tier, elite, mod, 0).bb;
assert.deepEqual([tb['attack@prob'], tb['attack@atk_scale_2'], tb['attack@max_stack_cnt']], [0.015, elite && mod === FX && tier === 6 ? 3.6 : 3, 40], label(f));
// a sure blast: both drones past the cap
const { h, u } = field({ tier, elite, mod, skill: 2 });
const e = h.spawn('enemy_dummy', { pos: [10, 6] }), near = h.spawn('enemy_fly', { pos: [11, 6] }), far = h.spawn('enemy_dummy', { pos: [10, 8] });
u.mem.gdStacks = [41, 41, 41];
u.skill.gainSp(999);
assert.ok(h.runUntil(() => mine(h, u).some((c) => c.dmg.tags?.includes(BLAST_TAG)), 3), `${label(f)}: blast`);
const blast = mine(h, u).filter((c) => c.dmg.tags?.includes(BLAST_TAG));
const hitT = new Set(blast.map((c) => c.target));
assert.ok(hitT.has(e) && hitT.has(near) && !hitT.has(far), `${label(f)}: within ${BLAST_RADIUS} of the target (air too), not 2 tiles away`);
for (const c of blast) {
approx(c.amount, u.s.atk * tb['attack@atk_scale_2'], `${label(f)}: ${tb['attack@atk_scale_2'] * 100} % ATK, no ramp`);
assert.deepEqual([c.type, c.dmg.isSkill], ['arts', true]);
}
assert.ok(h.hooksOf('statusApplied').some((c) => c.source === u && c.status === 'sluggish' && c.target === far) === false, 'no 停顿 outside');
assert.ok(h.hooksOf('statusApplied').some((c) => c.source === u && c.status === 'sluggish' && c.target === near), `${label(f)}: S3's 停顿 on the blast`);
assert.ok(u.mem.gdStacks.slice(0, 3).some((k) => k <= 2), `${label(f)}: a count restarted`);
h.b.retreat(u);
h.b.redeploy(u, { free: true });
assert.deepEqual(u.mem.gdStacks, [], `${label(f)}: reset at a deployment`);
done(h);
}
// the macro rate: ~10 % of the drone attacks (the pseudo-random distribution's 1 / 9.91)
const { h, u } = field({ tier: 6, elite: true, mod: FY, skill: 1 });
h.spawn('enemy_dummy', { pos: [10, 6] });
u.skill.gainSp(999);
h.run(600);
const n = droneHits(h, u).length, b = new Set(mine(h, u).filter((c) => c.dmg.tags?.includes(BLAST_TAG)).map((c) => c.t)).size;
assert.ok(n > 800, `${n} drone attacks`);
assert.ok(b / n > 0.08 && b / n < 0.125, `${b} / ${n} blasts ≈ 10 %`);
done(h);
});
+312
View File
@@ -0,0 +1,312 @@
// test/content/op_sleach.test.js — the 自选 operator kit of 琴柳 (char_479_sleach, 6★ 执旗手; kit
// server/sim/content/kits/ops/op-sleach.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in every
// form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, BEA-X 牧人的歌 or
// BEA-Y “友谊万岁” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json; the fidelity checklist
// of kits/README.md item by item.
// Run: node --test test/content/op_sleach.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js';
import { diyPool, validateDiyPicks } from '../../shared/diy.js';
import { FLAG_FLIGHT, FLAG_ALLY, FLAG_ENEMY, S2_KEY, FRONT_KEY, CAMOU_KEY } from '../../server/sim/content/kits/ops/op-sleach.js';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const SL = 'char_479_sleach';
const FORMS = BACKUPS.units[SL].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const BX = 'uniequip_002_sleach', BY = 'uniequip_003_sleach';
const S1 = 'skcom_assist_cost[3]', S2 = 'skchr_sleach_2', S3 = 'skchr_sleach_3';
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
/** The talents of a form with the module's changes (the composed record's). */
const talentOf = (tier, elite, mod, i) => {
const ch = modOf(tier, elite ? mod : null)?.talentChanges?.find((t) => t.talentIndex === i);
return ch ?? formOf(tier, elite).talents.find((t) => t.index === i);
};
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, BX, BY].map((m) => [t, true, m]))];
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
const TEXAS = 'chess_char_1_08_a', YAK = 'chess_char_1_02_a', PROVE = 'chess_char_1_07_a';
/** A battle with 琴柳 as uid 1 at (row, col) facing RIGHT, plus `others`. */
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5, flags = {} } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999, ...flags }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'deploy', 'death', 'attack'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: SL, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
});
h.step();
return { h, u: h.unit(1) };
}
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const dpOf = (h) => h.b.players[0].dp;
test('琴柳 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 1-1, 执旗手 (blocks 1, melee physical, ground-only), ground-targetable, 维多利亚; the three triggers SP_FULL (the 执旗手 row)', () => {
assert.equal(OPERATOR_KITS[SL], KITS[SL]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const { h, u } = field({ tier, elite, mod, skill });
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, sk = form.skills[skill];
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [SL, SLOT[tier], sk.skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [1, 'melee', false, 'phys', 'bearer', 1.3], `${label(f)}: 执旗手`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
assert.deepEqual([u.skill.rule, u.skill.spCost, u.skill.kind, sk.trigger.rawRule], ['SP_FULL', sk.spCost, 'duration', 'ALWAYS'], `${label(f)}: trigger / SP`);
done(h);
}
}
// the form numbers (zh_CN): E2 Lv1 1339 / 458 / 311, E2 Lv60 1668 / 516 / 348; BEA-X +150 / +48 → +200 / +68 HP / ATK,
// BEA-Y +150 / +45 → +200 / +70 HP / DEF
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1339, 458, 1668, 516]);
assert.deepEqual([modOf(5, BX).attr, modOf(6, BX).attr, modOf(5, BY).attr, modOf(6, BY).attr], [{ maxHp: 150, atk: 48 }, { maxHp: 200, atk: 68 }, { maxHp: 150, def: 45 }, { maxHp: 200, def: 70 }]);
});
test('a 自选 pick: 琴柳 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks', () => {
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(SL));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(SL), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: SL, skillIndex: 2, uniEquipId: BY } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: SL, skillIndex: 2, uniEquipId: BY } } });
});
test('S1 支援号令·γ型: cast as soon as its SP is full (no enemy needed), 8 s, no attack, blocks 0 meanwhile; +1 DP every 0.44 s from 0.44 s, 18 in all', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, BX]]) {
const sk = skillOf(tier, elite, S1);
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 2, chessId: YAK, row: 10, col: 7 }] });
assert.deepEqual([sk.duration, sk.bb.value, sk.bb.interval, sk.bb.cost, sk.initSp, sk.spCost], [8, 18, 0.44, 1, elite ? 12 : 11, elite ? 29 : 32], `T${tier}`);
h.run(sk.spCost - sk.initSp - 0.5);
assert.equal(u.skill.activations, 0, `T${tier}: not before its SP is full`);
h.b.players[0].dp = 0;
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast at full SP with no enemy on the field`);
const t0 = h.b.time;
let attacksOn = 0;
h.b.on('attack', (c) => { if (c.attacker === u && u.skill.active) attacksOn++; });
assert.equal(u.s.blockCnt, 0, `T${tier}: 执旗手 — block 0 while it runs`);
h.spawn('enemy_dummy', { pos: [10, 6] });
h.runUntil(() => dpOf(h) >= 1, 1);
approx(h.b.time - t0, 0.44, `T${tier}: the first DP after one interval`, 0.1);
h.runUntil(() => !u.skill.active, 9);
approx(h.b.time - t0, 8, `T${tier}: 8 s`, 0.01);
approx(dpOf(h), 18, `T${tier}: 18 DP`);
assert.equal(attacksOn, 0, `T${tier}: 停止攻击`);
h.run(1);
approx(dpOf(h), 18, `T${tier}: nothing after`);
assert.equal(u.s.blockCnt, 1, `T${tier}: block back`);
done(h);
}
});
test('S2 信仰传承: the flag goes to the operator with the lowest HP share on the x-1 (not her, not a 孤立 one, not out of range): DEF +def and 生命回复速度 ATK × ratio there (through 禁疗), T1\'s area follows the flag; 20 DP over 15 s; back to her after', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, BY]]) {
const sk = skillOf(tier, elite, S2);
const others = [
{ uid: 2, chessId: YAK, row: 10, col: 6 }, // [0, 1]: in range, 80 %
{ uid: 3, chessId: TEXAS, row: 11, col: 4 }, // [1, -1]: in range, 40 % — the target
{ uid: 4, chessId: PROVE, row: 12, col: 7 }, // [2, 2]: outside the x-1, 10 %
{ uid: 5, chessId: 'chess_char_1_09_a', row: 9, col: 5 }, // [-1, 0]: in range, 5 % but 孤立 — never chosen
];
const { h, u } = field({ tier, elite, mod, skill: 1, row: 10, col: 5, others });
const yak = h.unit(2), texas = h.unit(3), prove = h.unit(4);
assert.deepEqual([sk.duration, sk.bb.value, sk.bb['sleach_s_2[cost].interval'], sk.rangeId, sk.bb.def, sk.bb.atk_to_hp_recovery_ratio], [15, 20, 0.75, 'x-1', elite ? 0.35 : 0.22, elite ? 0.35 : 0.22], `T${tier}`);
yak.hp = yak.s.maxHp * 0.8; texas.hp = texas.s.maxHp * 0.4; prove.hp = prove.s.maxHp * 0.1;
u.hp = u.s.maxHp * 0.05; // her own share never makes her the target
const iso = h.unit(5);
iso.hp = iso.s.maxHp * 0.05;
h.b.addBuff(iso, { key: 'test:isolated', flags: { isolated: true } });
h.b.addBuff(texas, { key: 'test:noheal', flags: { noHeal: true } });
h.b.players[0].dp = 0;
const defBefore = texas.s.def;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
const t0 = h.b.time;
assert.deepEqual(u.mem.sleachFlag && [u.mem.sleachFlag.r, u.mem.sleachFlag.c], [11, 4], `T${tier}: the flag on 德克萨斯's tile`);
h.run(0.3);
const b = texas.findBuff(`${S2_KEY}:${u.id}`);
assert.ok(b, `T${tier}: the flag effect on 德克萨斯`);
approx(b.mods.defPct, sk.bb.def, `T${tier}: DEF +${sk.bb.def * 100} %`);
approx(b.mods.hpRegen, u.s.atk * sk.bb.atk_to_hp_recovery_ratio, `T${tier}: 生命回复速度 = ATK × ratio`);
approx(texas.s.def - defBefore, texas.base.def * sk.bb.def, `T${tier}: DEF applied (直接乘算)`, 1e-3);
const hp0 = texas.hp;
h.run(2);
assert.ok(texas.hp > hp0 + 1.5 * u.s.atk * sk.bb.atk_to_hp_recovery_ratio, `T${tier}: it regenerates through 禁疗 (${hp0} → ${texas.hp})`);
assert.equal(yak.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: only the operator on the flag's tile`);
assert.equal(prove.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: the 10 % operator outside the x-1 is never chosen`);
// T1's 3×3 follows the flag: 德克萨斯 (on it) and 琴柳 ([−1, +1] of it) are inside, 角峰 ([−1, +2]) is not
const asp = talentOf(tier, elite, mod, 0).bb['sleach_t_1[ally].attack_speed'];
assert.equal(texas.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 德克萨斯 in the flag's area`);
assert.equal(u.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 琴柳 next to the flag`);
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`), null, `T${tier}: 角峰 outside the flag's area`);
h.runUntil(() => !u.skill.active, 16);
approx(h.b.time - t0, 15, `T${tier}: 15 s`, 0.01);
approx(dpOf(h), 20, `T${tier}: 20 DP`);
assert.equal(u.mem.sleachFlag, null, `T${tier}: the flag back`);
assert.equal(texas.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: the effect ends with the skill`);
h.run(0.5);
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 角峰 next to her again`);
done(h);
}
// nobody else on the x-1: the flag stays on her tile — she has DEF / 生命回复速度 there
const { h, u } = field({ tier: 6, elite: true, mod: BX, skill: 1 });
const sk = skillOf(6, true, S2);
u.skill.gainSp(999);
h.run(0.3);
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 5], 'her own tile');
approx(u.findBuff(`${S2_KEY}:${u.id}`)?.mods.defPct ?? 0, sk.bb.def, 'on herself');
done(h);
});
test('S3 光辉旗帜: +10 DP at once; the flag on the ground tile of the enemy ranked first in the 2-1 (never a flyer): 停顿 + 脆弱 on its 3×3 at once, the strike 1 s later — atk_scale × ATK physical and 3 s 晕眩 to ground and air enemies of the 3×3; no ground enemy → her own tile', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, BX]]) {
const sk = skillOf(tier, elite, S3);
const { h, u } = field({ tier, elite, mod, skill: 2, row: 10, col: 4 });
assert.deepEqual([sk.duration, sk.bb.cost, sk.bb.atk_scale, sk.bb.stun, sk.bb['debuff.damage_scale'], sk.rangeId], [10, 10, elite ? 2.4 : 2, 3, elite ? 0.25 : 0.2, '2-1'], `T${tier}`);
// in the 2-1 (facing RIGHT from 10,4): (10,6) [0,2] and (11,5) [1,1]; a taunt makes (10,6) the one her targeting ranks
// first (the highest 仇恨); its 3×3 (rows 9–11, cols 5–7) holds (11,5) and the flyer at (9,6), not (10,8)
const first = h.spawn('enemy_dummy', { pos: [10, 6] }), second = h.spawn('enemy_dummy', { pos: [11, 5] });
h.b.addBuff(first, { key: 'test:taunt', mods: { taunt: 1 } });
const fly = h.spawn('enemy_fly', { pos: [9, 6] });
const out = h.spawn('enemy_dummy', { pos: [10, 8] });
h.b.players[0].dp = 0;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
const t0 = h.b.time;
approx(dpOf(h), 10, `T${tier}: +10 DP at once`);
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 6], `T${tier}: the first-ranked ground enemy's tile`);
h.run(0.3);
const inArea = [first, second, fly];
for (const e of inArea) {
assert.ok(e.findBuff('sluggish'), `T${tier}: ${e.defId} 停顿 before the flag lands`);
approx(e.findBuff('fragile')?.data?.value ?? 0, sk.bb['debuff.damage_scale'], `T${tier}: ${e.defId} 脆弱`);
assert.equal(e.findBuff('stun'), null, `T${tier}: no 晕眩 yet`);
}
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u).length, 0, `T${tier}: no damage before it lands`);
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === u), 2));
approx(h.b.time - t0, FLAG_FLIGHT, `T${tier}: it lands ${FLAG_FLIGHT} s after the throw`, 0.05);
const hits = h.hooksOf('damaged').filter((c) => c.source === u);
const struck = new Set(hits.map((c) => c.target));
for (const e of inArea) {
assert.ok(struck.has(e), `T${tier}: ${e.defId} struck (ground and air)`);
const c = hits.find((x) => x.target === e);
approx(c.amount, u.s.atk * sk.bb.atk_scale * (1 + sk.bb['debuff.damage_scale']), `T${tier}: ${sk.bb.atk_scale * 100} % ATK × the 脆弱`);
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true]);
const st = h.hooksOf('statusApplied').find((x) => x.source === u && x.status === 'stun' && x.target === e);
approx(st?.duration ?? 0, sk.bb.stun, `T${tier}: ${sk.bb.stun} s 晕眩`);
}
assert.ok(!struck.has(out) && !out.findBuff('sluggish'), `T${tier}: nothing outside the 3×3`);
h.runUntil(() => !u.skill.active, 11);
approx(h.b.time - t0, 10, `T${tier}: 10 s`, 0.01);
assert.equal(u.mem.sleachFlag, null, `T${tier}: the flag back`);
done(h);
}
// only a flyer in the 2-1, then nobody: her own tile
for (const pos of [[10, 6], null]) {
const { h, u } = field({ tier: 6, elite: true, mod: BY, skill: 2, row: 10, col: 4 });
if (pos) h.spawn('enemy_fly', { pos });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 4], pos ? 'a flyer is never the target' : 'no enemy: her tile');
done(h);
}
});
test('T1 不退之旗 in every form: operators of the 3×3 around her ASPD +10 (BEA-X stage 3 +13) — 孤立 ones too, summons not —, enemies there (ground and air) ASPD −10 (−13); nothing outside, nothing once she left', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const tb = talentOf(tier, elite, mod, 0).bb;
const others = [{ uid: 2, chessId: YAK, row: 11, col: 6 }, { uid: 3, chessId: TEXAS, row: 9, col: 4 }, { uid: 4, chessId: PROVE, row: 10, col: 7 }];
const { h, u } = field({ tier, elite, mod, skill: 2, others });
const yak = h.unit(2), texas = h.unit(3), prove = h.unit(4);
h.b.addBuff(texas, { key: 'test:isolated', flags: { isolated: true } });
const tok = h.b.spawnToken('p1', 'token_10000_silent_healrb', 11, 4);
const e1 = h.spawn('enemy_dummy', { pos: [10, 6] }), e2 = h.spawn('enemy_fly', { pos: [9, 5] }), e3 = h.spawn('enemy_dummy', { pos: [10, 7] });
h.run(0.5);
const a = tb['sleach_t_1[ally].attack_speed'], en = tb['sleach_t_1[enemy].attack_speed'];
assert.deepEqual([a, en], tier === 6 && elite && mod === BX ? [13, -13] : [10, -10], label(f));
for (const x of [u, yak, texas]) assert.equal(x.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, a, `${label(f)}: ${x.defId} +${a}`);
assert.equal(prove.findBuff(`${FLAG_ALLY}:${u.id}`), null, `${label(f)}: two tiles away`);
assert.ok(tok && !tok.findBuff(`${FLAG_ALLY}:${u.id}`), `${label(f)}: no summon`);
for (const e of [e1, e2]) assert.equal(e.findBuff(`${FLAG_ENEMY}:${u.id}`)?.mods.aspd, en, `${label(f)}: ${e.defId} ${en}`);
assert.equal(e3.findBuff(`${FLAG_ENEMY}:${u.id}`), null, `${label(f)}: the enemy two tiles away`);
approx(e1.s.aspd, 100 + en, `${label(f)}: enemy ASPD`);
h.b.retreat(u);
h.run(0.5);
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`), null, `${label(f)}: gone with her`);
assert.equal(e1.findBuff(`${FLAG_ENEMY}:${u.id}`), null, `${label(f)}: gone with her (enemy)`);
done(h);
}
});
test('T2 精神感召: from her deployment the operators of her player off the field cost −2 until the next operator deployment; BEA-Y stage 3 adds +2 DP when that operator is 近战位 (once) — nothing at stage 1 or for a 远程位 one', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const bonus = tier === 6 && elite && mod === BY ? 2 : 0;
if (bonus) assert.deepEqual(modOf(6, BY).talentChanges.find((t) => t.talentIndex === -1).bb, { cost: 2 });
// she deploys last (col 7 after cols 3 / 4): no operator waits, so the effect stays; a knocked-out one gets the cut
for (const [chessId, melee] of [[TEXAS, true], [PROVE, false]]) {
const { h } = field({ tier, elite, mod, skill: 0, row: 10, col: 7, others: [{ uid: 2, chessId, row: 10, col: 3 }, { uid: 3, chessId: YAK, row: 11, col: 4 }] });
const op = h.unit(2), cost = op.base.cost;
h.b.kill(op);
assert.equal(op.base.cost, cost - 2, `${label(f)} ${chessId}: −2 while it waits`);
h.b.players[0].dp = 20;
assert.ok(h.b.redeploy(op, { free: false }), 'redeploys');
approx(dpOf(h), 20 - (cost - 2) + (melee ? bonus : 0), `${label(f)} ${chessId}: paid ${cost - 2}${melee && bonus ? ', +2 DP (近战位)' : ''}`);
assert.equal(op.base.cost, cost, `${label(f)} ${chessId}: the cost back once used`);
h.b.kill(op);
assert.equal(op.base.cost, cost, `${label(f)} ${chessId}: used up — no cut on the next knock-out`);
h.b.players[0].dp = 20;
h.b.redeploy(op, { free: false });
approx(dpOf(h), 20 - cost, `${label(f)} ${chessId}: and no second +DP`);
done(h);
}
}
// an operator deploying after her in the battle-start deployment uses it up at once (free: nothing saved); her leaving
// ends a pending one
const { h, u } = field({ tier: 6, elite: true, mod: BY, skill: 0, row: 10, col: 3, others: [{ uid: 2, chessId: YAK, row: 10, col: 6 }] });
const yk = h.unit(2), cost = yk.base.cost;
approx(dpOf(h), 10 + 2, 'BEA-Y stage 3: 角峰 (近战位) deployed right after her: +2 DP at the battle start');
h.b.kill(yk);
assert.equal(yk.base.cost, cost, 'used up by the battle-start deployment');
h.b.retreat(u);
h.b.redeploy(u, { free: true });
assert.equal(yk.base.cost, cost - 2, 'her redeploy: the waiting 角峰 −2 again');
h.b.retreat(u);
assert.equal(yk.base.cost, cost, 'her leaving ends it');
done(h);
});
test('BEA-X “牧人的歌”: while her skill runs the operator in front of her blocks +1 (stages 1 and 3), not before / after, not without the module; BEA-Y “友谊万岁”: 迷彩 while her skill runs', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, row: 10, col: 5, others: [{ uid: 2, chessId: YAK, row: 10, col: 6 }, { uid: 3, chessId: TEXAS, row: 11, col: 5 }] });
const front = h.unit(2), side = h.unit(3);
const bx = elite && mod === BX, by = elite && mod === BY;
if (bx) assert.deepEqual(u.def.raw.trait.bb, { block_cnt: 1 }, label(f));
const base = front.s.blockCnt;
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
h.run(0.3);
assert.equal(front.s.blockCnt, base + (bx ? 1 : 0), `${label(f)}: the operator in front`);
assert.equal(!!side.findBuff(`${FRONT_KEY}:${u.id}`), false, `${label(f)}: not the one beside her`);
assert.equal(!!u.s.flags.camou, by, `${label(f)}: 迷彩 while it runs`);
assert.equal(!!u.findBuff(CAMOU_KEY), by);
h.runUntil(() => !u.skill.active, 10);
h.step();
assert.equal(front.s.blockCnt, base, `${label(f)}: back after`);
assert.equal(!!u.s.flags.camou, false, `${label(f)}: no 迷彩 after`);
done(h);
}
});
+371 -99
View File
@@ -3993,126 +3993,139 @@
"snaps": "48868d9e c0fba9dd f1ed5a7f 45492016 1d7b7193 c7926ddf 2d138777 c3b2ae7b 63b5a8a6 be0fb4e1 3d3b899f 6d87b249 6587575a a6d3b62f 38c282fd 25732b97 f11d26b4 dd995a3a f7c48f36 475f3497 0f0fa445 b5ff96e3 0660ba17"
},
"diy-094": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"fast": true,
"about": "5_diy1_a=sleach/S1 5_diy2_a=sleach/S2 5_diy1_a=sleach/S3 5_diy2_b=sleach/S1 5_diy1_b=sleach/S2 5_diy2_b=sleach/S3 5_diy1_b=sleach/S1/002_sleach 5_diy2_b=sleach/S2/002_sleach 5_diy1_b=sleach/S3/002_sleach 5_diy2_b=sleach/S1/003_sleach 5_diy1_b=sleach/S2/003_sleach 5_diy2_b=sleach/S3/003_sleach",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m06",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 3107422628,
"waveId": "act1autochess_h05",
"time": 201.93,
"ticks": 6058,
"reason": "cleared",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 0,
"total": 90,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":90,"perfect":false,"leaks":90,"leaksUncounted":0,"coins":0,"dmg":51407,"bossDmg":0,"heal":7484,"deaths":21,"layerGains":{}}},
"players": {"p1":{"killed":0,"total":90,"perfect":false,"leaks":90,"leaksUncounted":0,"coins":0,"dmg":166775,"bossDmg":0,"heal":9967,"deaths":17,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,97,0,28,4256,0,2,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",2908,1,6,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,9,0,0,0,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,66,0,13494,11270,0,28,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1271,1,115,0,2450,2542,0,27,0],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,116,0,4334,3020,0,34,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,67,0,4670,2266,0,10,0],
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,85,0,6588,2720,0,12,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,68,0,0,2436,3670,10,1],
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,63,0,0,3302,3814,7,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,113,0,5925,5124,0,15,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,107,0,13920,2262,0,30,1]
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,93,0,13930,2972,294,35,3],
["p1",2,"op","chess_char_5_diy2_a",1,null,1339,1,7,0,0,0,0,0,5],
["p1",3,"op","chess_char_5_diy1_a",2,null,1339,1,9,0,15085,0,0,0,6],
["p1",4,"op","chess_char_5_diy2_b",0,"none",238.13,1,105,0,903,4766,0,35,2],
["p1",5,"op","chess_char_5_diy1_b",1,"none",1668,1,109,0,1463,3098,1430,52,3],
["p1",6,"op","chess_char_5_diy2_b",2,"none",1668,1,114,0,8502,5183,1847,49,2],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_sleach",1818,1,115,0,14897,3636,0,45,2],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_sleach",0,0,108,0,24546,5147,1511,77,2],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_sleach",1620.3,1,88,0,56592,5561,3545,120,5],
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_sleach",0,0,94,0,4873,3636,0,29,3],
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_sleach",1818,1,113,0,1593,3158,1340,58,3],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_sleach",0,0,78,0,24391,3636,0,41,2]
],
"enemies": {"enemy_1116_liprr_2":[27,0,27,13892,31976],"enemy_1119_vofsd":[39,0,39,7161,10901],"enemy_1121_lifbos_2":[24,0,24,18145,8530]},
"hooks": [1,116,6058,223,223,206,206,349,21,111,5,4,0,11353,45,23,90,90,1,21,0,0,0],
"events": {"atk":223,"deploy":26,"die":21,"dmg":206,"fx":14,"heal":17,"leak":90,"skill":9,"spawn":102,"status":206},
"fx": {"liberate":14},
"snaps": "952a1d80 2f0f0d5f f2bdbb89 e8db3c70 f3853d8b 8f4446bc f4d82c02 fa5e04c8 18b7bcb9 0445bb38 8ece1312 65a9927c 2c377492 580b26ff 88379641 2eb28b76 c4a3698e 12d59c0b a3cd2fde 0d47f642 897ee040"
"enemies": {"enemy_1116_liprr_2":[27,0,27,11449,128156],"enemy_1119_vofsd":[39,0,38,19434,27023],"enemy_1121_lifbos_2":[24,0,24,9910,11597]},
"hooks": [1,115,6900,613,613,695,695,2647,17,106,38,36,0,36447,9762,37,90,89,1,17,0,0,0],
"events": {"atk":613,"deploy":25,"die":17,"dmg":695,"fx":85,"leak":89,"skill":74,"spawn":102,"status":630},
"fx": {"aoe":15,"dp":37,"liberate":5,"zone":28},
"snaps": "4f65c948 a7488529 1d9ddd3a a390cb77 0a947f76 1dc3db1c cbb131ce 994c4d9d e44f2b9f 52317ea2 773ca0b8 170984fb dc69a94f 18859be4 00bcc557 5b677f3d 89caf568 f25479ae 2080aec0 f76455c8 bea56260 7851d186 4ed027e9"
},
"diy-095": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc",
"about": "6_diy1_a=sleach/S1 6_diy2_a=sleach/S2 6_diy1_a=sleach/S3 6_diy2_b=sleach/S1 6_diy1_b=sleach/S2 6_diy2_b=sleach/S3 6_diy1_b=sleach/S1/002_sleach 6_diy2_b=sleach/S2/002_sleach 6_diy1_b=sleach/S3/002_sleach 6_diy2_b=sleach/S1/003_sleach 6_diy1_b=sleach/S2/003_sleach 6_diy2_b=sleach/S3/003_sleach",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_funny",
"round": 12,
"round": 13,
"seed": 2719610662,
"waveId": "act1autochess_h05",
"time": 186.43,
"ticks": 5593,
"waveId": "act1autochess_h06",
"time": 214.37,
"ticks": 6431,
"reason": "cleared",
"killed": 65,
"total": 90,
"killed": 45,
"total": 156,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 215,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":65,"total":90,"perfect":false,"leaks":25,"leaksUncounted":0,"coins":0,"dmg":588639,"bossDmg":0,"heal":50045,"deaths":6,"layerGains":{}}},
"players": {"p1":{"killed":45,"total":156,"perfect":false,"leaks":111,"leaksUncounted":0,"coins":0,"dmg":158103,"bossDmg":0,"heal":8026,"deaths":20,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,90,0,2431,5208,0,22,1],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",3371,1,106,1,2780,3981,0,14,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,1529,1,100,8,64368,3082,0,35,5],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1904,1,93,18,142873,10262,0,77,6],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,12,13,107750,26742,0,126,4],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",2976.6,1,101,7,108379,15127,0,73,3],
["p1",7,"op","chess_char_5_diy1_a",2,null,1272,1,4,3,23337,0,0,55,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,7,7,49714,0,0,92,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1372,1,9,3,31223,0,0,39,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,11,5,55784,0,0,47,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,1224,1,13,0,0,0,0,0,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,5,0,0,1500,50045,61,2],
["p1",1,"op","chess_char_6_diy1_a",0,null,1339,1,185,3,4934,2678,0,29,1],
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,132,3,14319,3276,598,54,2],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,119,9,16271,2678,0,45,2],
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,120,5,9892,3336,0,46,2],
["p1",5,"op","chess_char_6_diy1_b",1,"none",1668,1,184,0,8888,3336,0,36,1],
["p1",6,"op","chess_char_6_diy2_b",2,"none",1595.84,1,180,4,16697,3408,0,43,2],
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_sleach",0,0,138,0,16563,6445,2709,56,2],
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_sleach",0,0,174,1,11221,3736,0,46,2],
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_sleach",1868,1,178,10,16264,1868,0,59,4],
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_sleach",1868,1,177,1,7819,3225,1357,57,4],
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_sleach",1868,1,144,5,9142,1868,0,60,3],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_sleach",1868,1,117,4,26095,5230,3362,83,4],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1229_darmy":[27,19,8,6602,76927],"enemy_1229_darmy_2":[51,40,11,25602,212189],"enemy_9007_acelem":[12,6,6,33699,299524]},
"hooks": [1,109,5593,736,736,1336,1416,112,71,96,30,28,0,31536,0,53,90,25,1,71,0,7,0],
"events": {"atk":782,"deploy":17,"die":71,"dmg":1416,"fx":177,"heal":110,"leak":25,"skill":58,"spawn":104,"status":61},
"fx": {"aoe":37,"beam":13,"burst":7,"dp":37,"slash":80,"spGain":3},
"snaps": "7f6efb1f 0110b674 b3f9e162 8f5e5c60 2064df74 137a8184 d8ad578a fb70ee65 5de6cc0b a36f5030 4dcf550b d894bc10 fa706850 14776874 96fd0f22 8d647370 4ce5dc0d 2bf4e8bc 378b3869"
"enemies": {
"enemy_1197_sfshu": [15,7,8,366,16800],
"enemy_1197_sfshu_2": [39,14,25,1501,49156],
"enemy_1198_msfshu": [21,9,12,0,30],
"enemy_1198_msfshu_2": [42,14,28,0,65],
"enemy_9010_acpupp": [39,1,38,39217,92052]
},
"hooks": [1,185,6431,763,763,796,779,1390,65,176,29,25,0,29499,4162,55,156,111,1,69,0,0,0],
"events": {"atk":763,"deploy":27,"die":65,"dmg":779,"fx":101,"leak":111,"skill":54,"spawn":170,"status":265},
"fx": {"aoe":12,"dp":34,"ember":4,"revive":3,"shieldBreak":3,"summon":21,"telegraph":4,"zone":20},
"snaps": "9ab0f026 6e2404bb 82e309ea bd322e57 88d176da de473536 6db81985 19f56b81 1cf2cd96 7fd32e17 3c6c9ec6 212cebb4 c3a30087 8d564abf 593382de 45b19991 bc6c14c0 9e2aca3c 152124a7 fd5ecede 27b4af57 aad5f380"
},
"diy-096": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 5_diy1_a=sharp2 5_diy2_b=sharp2 6_diy1_a=acpion 6_diy2_b=acpion 5_diy1_a=acspec 5_diy2_b=acspec 6_diy1_a=acguad 6_diy2_b=acguad 5_diy1_a=acsupo 5_diy2_b=acsupo",
"about": "5_diy1_a=closur/S1 5_diy2_a=closur/S2 5_diy1_a=closur/S3 5_diy2_b=closur/S1 5_diy1_b=closur/S2 5_diy2_b=closur/S3 5_diy1_b=closur/S1/002_closur 5_diy2_b=closur/S2/002_closur 5_diy1_b=closur/S3/002_closur 6_diy1_a=closur/S1 6_diy2_a=closur/S2 6_diy1_a=closur/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_single_hard",
"round": 13,
"round": 12,
"seed": 65368379,
"waveId": "act1autochess_h06",
"time": 226.63,
"ticks": 6799,
"waveId": "act1autochess_h05",
"time": 215.77,
"ticks": 6473,
"reason": "cleared",
"killed": 47,
"total": 150,
"killed": 33,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 398,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":47,"total":150,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":0,"dmg":517440,"bossDmg":0,"heal":0,"deaths":28,"layerGains":{}}},
"players": {"p1":{"killed":33,"total":102,"perfect":false,"leaks":69,"leaksUncounted":0,"coins":0,"dmg":211351,"bossDmg":0,"heal":0,"deaths":27,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,135,0,5981,5208,0,58,1],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,9,2,21421,3431,0,83,1],
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,109,2,11923,3730,0,42,1],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,149,10,107184,4748,0,73,1],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,168,4,24242,4587,0,21,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,148,7,117115,3808,0,26,4],
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,182,1,15464,9232,0,73,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",0,0,181,3,25520,7240,0,80,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,162,3,19570,5276,0,40,2],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",153.42,1,176,7,51410,6909,0,58,2],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,2,4,42304,0,0,129,2],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,3,4,75304,0,0,146,3]
["p1",1,"op","chess_char_5_diy1_a",0,null,1444,1,3,2,17550,0,0,164,10],
["p1",2,"op","chess_char_5_diy2_a",1,null,1444,1,4,3,23336,0,0,175,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,1444,1,5,15,80835,0,0,238,3],
["p1",4,"op","chess_char_5_diy2_b",0,"none",0,0,144,0,1126,4965,0,29,2],
["p1",5,"op","chess_char_5_diy1_b",1,"none",0,0,143,1,6711,4965,0,35,1],
["p1",6,"op","chess_char_5_diy2_b",2,"none",0,0,139,0,7128,4965,0,31,1],
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_closur",0,0,147,1,2345,5415,0,24,2],
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_closur",0,0,141,4,15334,5415,0,39,1],
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_closur",0,0,145,5,43636,5415,0,62,1],
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,133,1,5361,4332,0,29,1],
["p1",11,"op","chess_char_6_diy2_a",1,null,0,0,149,0,1664,4332,0,31,1],
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,148,1,6326,4332,0,55,1],
["p1","~1","token","token_10066_closur_ourbase",null,null,0,0,150,0,0,7860,0,0,0],
["p1","~2","token","token_10066_closur_ourbase",null,null,2332.5,1,140,0,0,10767,0,0,0],
["p1","~3","token","token_10066_closur_ourbase",null,null,2429.49,0,11,0,0,625,0,0,0],
["p1","~4","token","token_10066_closur_ourbase",null,null,889.61,0,77,0,0,4350,0,0,0],
["p1","~5","token","token_10066_closur_ourbase",null,null,2362.78,0,96,0,0,691,0,0,0],
["p1","~6","token","token_10066_closur_ourbase",null,null,2588.06,0,134,0,0,32,0,0,0],
["p1","~7","token","token_10066_closur_ourbase",null,null,3054,0,146,0,0,0,0,0,0]
],
"enemies": {"enemy_1249_lysdb_2":[60,20,40,16145,279875],"enemy_1251_lysyta":[51,23,28,2827,117497],"enemy_1329_cbshld":[39,4,35,35197,120068]},
"hooks": [1,182,6799,1215,1215,1759,1784,0,75,178,18,30,0,23883,11457,79,150,103,1,76,0,0,18],
"events": {"atk":1266,"deploy":32,"die":75,"dmg":1739,"fx":205,"leak":103,"skill":63,"spawn":162,"status":1070},
"fx": {"aoe":42,"displace":16,"dodge":38,"dp":16,"shieldBreak":56,"slash":37},
"snaps": "eedb1978 c0335295 4783edee f3486b2e 5f85509c 6d3f0014 b7df554c be565b07 bf617b2a 914ca1d1 507a8687 fbc7a618 a6d2c90e 3e84d000 c7cafa9a 3e8b4d04 ccd4d028 1b5edcab 2eb8a107 318e94df 71cd27b8 3df7cd04 4e95efb5"
"enemies": {"enemy_1165_duhond":[63,33,30,8340,149464],"enemy_1174_duholy_2":[15,0,15,9879,23971],"enemy_1175_dushdo_2":[24,0,24,50243,37917]},
"hooks": [1,146,6473,1233,1233,1728,1728,0,68,142,27,39,0,21120,0,72,102,69,1,68,0,0,0],
"events": {"atk":1734,"deploy":44,"die":73,"dmg":1728,"fx":54,"leak":69,"skill":81,"spawn":121,"status":208},
"fx": {"disappear":8,"dp":32,"summon":14},
"snaps": "adeb9475 8cde7ee7 c44273a7 7366543a 71a25809 18290070 6376b468 e56825c8 41cf0c93 a6ea1e48 5283dae2 93e68357 e62b6a6e 6056011c 7b0f92f1 fba9d9a0 3f0c0819 a1c79203 ec090c31 ce937d08 625572ba 4e09675a"
},
"diy-097": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc 6_diy1_a=acsupo 6_diy2_b=acsupo",
"about": "6_diy1_b=closur/S1 6_diy2_b=closur/S2 6_diy1_b=closur/S3 6_diy2_b=closur/S1/002_closur 6_diy1_b=closur/S2/002_closur 6_diy2_b=closur/S3/002_closur 5_diy1_a=gdglow/S1 5_diy2_a=gdglow/S2 5_diy1_a=gdglow/S3 5_diy2_b=gdglow/S1 5_diy1_b=gdglow/S2 5_diy2_b=gdglow/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
@@ -4120,37 +4133,296 @@
"round": 13,
"seed": 3733794685,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 6,
"time": 222,
"ticks": 6660,
"reason": "cleared",
"killed": 4,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 913,
"rngDraws": 202,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":6,"total":105,"perfect":false,"leaks":99,"leaksUncounted":0,"coins":0,"dmg":937154,"bossDmg":0,"heal":61210,"deaths":15,"layerGains":{}}},
"players": {"p1":{"killed":4,"total":105,"perfect":false,"leaks":101,"leaksUncounted":0,"coins":0,"dmg":598609,"bossDmg":0,"heal":0,"deaths":14,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,131,1,17294,5595,0,32,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,130,2,52003,7482,0,39,2],
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,132,0,19347,18200,0,119,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1006.92,1,89,0,22744,51896,0,215,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,8,0,18480,0,0,136,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,9,1,47557,0,0,150,2],
["p1",7,"op","chess_char_6_diy1_a",2,null,1372,1,13,0,104033,0,0,63,2],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,1,594540,0,0,92,3],
["p1",9,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,28988,64,3],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,10,0,0,0,32222,49,2],
["p1",11,"op","chess_char_6_diy1_a",2,null,1160,1,14,1,54357,0,0,126,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,121,0,6799,2900,0,13,0],
["p1",1,"op","chess_char_6_diy1_b",0,"none",1655,1,8,0,8375,0,0,162,12],
["p1",2,"op","chess_char_6_diy2_b",1,"none",1655,1,9,0,17668,0,0,176,3],
["p1",3,"op","chess_char_6_diy1_b",2,"none",1655,1,16,0,20100,0,0,150,4],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_closur",1835,1,4,1,33060,0,0,204,12],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_closur",1835,1,6,0,78577,0,0,204,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_closur",1835,1,10,3,254255,0,0,278,4],
["p1",7,"op","chess_char_5_diy1_a",0,null,1124,1,17,0,124536,0,0,82,3],
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,132,0,1696,3372,0,14,0],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,129,0,1918,3372,0,12,0],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,144,0,14573,4080,0,10,1],
["p1",11,"op","chess_char_5_diy1_b",1,"none",1360,1,151,0,3842,2720,0,29,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,149,0,40007,4080,0,18,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["p1","~1","token","token_10066_closur_ourbase",null,null,3054,1,154,0,0,18324,0,0,0],
["p1","~2","token","token_10066_closur_ourbase",null,null,0,0,152,0,0,21378,0,0,0],
["p1","~3","token","token_10066_closur_ourbase",null,null,3054,1,153,0,0,21378,0,0,0]
],
"enemies": {"enemy_1116_liprr_2":[27,4,23,6683,217791],"enemy_1119_vofsd":[48,0,48,32538,175639],"enemy_1121_lifbos_2":[30,0,30,39482,205179]},
"hooks": [1,151,6660,1512,1512,2370,2370,6793,38,139,43,59,0,33344,50,66,105,101,1,38,0,0,0],
"events": {"atk":2064,"deploy":44,"die":38,"dmg":2358,"fx":393,"leak":101,"skill":126,"spawn":122,"status":209},
"fx": {"disappear":20,"dp":40,"drone":202,"droneLock":34,"drones":5,"explode":22,"liberate":29,"shield":19,"summon":22},
"snaps": "aefc11f0 65f0e3bc 55f21515 f77681c5 4da7991f 73a2a33f 93a7506d c5697857 fe16b7b2 881d16d1 b4deb0ba 88cfc955 cd4aea3e 55cc3ba2 65768d8a c6afb13f 461ce8a9 64138ccb f0ab5b31 91e8e0b0 da2eaee4 10935d20 e51cbfd2"
},
"diy-098": {
"about": "5_diy1_b=gdglow/S1/002_gdglow 5_diy2_b=gdglow/S2/002_gdglow 5_diy1_b=gdglow/S3/002_gdglow 5_diy2_b=gdglow/S1/003_gdglow 5_diy1_b=gdglow/S2/003_gdglow 5_diy2_b=gdglow/S3/003_gdglow 6_diy1_a=gdglow/S1 6_diy2_a=gdglow/S2 6_diy1_a=gdglow/S3 6_diy2_b=gdglow/S1 6_diy1_b=gdglow/S2 6_diy2_b=gdglow/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m03",
"modeId": "mode_multi_hard",
"round": 13,
"seed": 1079403473,
"waveId": "act1autochess_h06",
"time": 229.47,
"ticks": 6884,
"reason": "cleared",
"killed": 1,
"total": 66,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 159,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":1,"total":66,"perfect":false,"leaks":65,"leaksUncounted":0,"coins":0,"dmg":59983,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_gdglow",0,0,90,0,4888,4350,0,3,1],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_gdglow",828.56,1,91,0,1389,3521,0,11,0],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_gdglow",0,0,85,0,1507,4350,0,10,0],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_gdglow",0,0,47,0,892,2720,0,6,0],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_gdglow",0,0,76,0,384,2720,0,6,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_gdglow",0,0,9,0,662,1360,0,5,1],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,5,0,0,1124,0,0,0],
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,7,0,632,1124,0,7,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,1124,1,10,0,40185,0,0,51,3],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,55,1,1681,2720,0,11,1],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,50,0,1570,2720,0,9,0],
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,6,0,6193,1360,0,2,1],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1005_yokai_2":[12,1,11,4030,13179],"enemy_1041_lazerd_2":[24,0,24,24040,27900],"enemy_1042_frostd":[30,0,30,0,18904]},
"hooks": [1,91,6884,192,192,367,367,0,21,86,7,7,0,12606,175,0,66,65,1,21,0,0,0],
"events": {"atk":192,"deploy":22,"die":21,"dmg":367,"fx":300,"leak":65,"skill":14,"spawn":81,"status":167},
"fx": {"drone":159,"droneLock":120,"drones":7,"explode":14},
"snaps": "ab6ec9e5 0cd29374 535b8f29 3a00102a 4e2f0e45 89bec34e 3229b975 60a9b6e7 f72a6124 fdcf62df d8ebef5d 58a1a24d 40484f5d 030d318d 2d3105b6 b2e4e0b0 fa368d3b a4d01055 23684e50 b6c74b76 de80150b 2b817302 aae3bd11"
},
"diy-099": {
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=gdglow/S1/002_gdglow 6_diy2_b=gdglow/S2/002_gdglow 6_diy1_b=gdglow/S3/002_gdglow 6_diy2_b=gdglow/S1/003_gdglow 6_diy1_b=gdglow/S2/003_gdglow 6_diy2_b=gdglow/S3/003_gdglow 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m04",
"modeId": "mode_single_normal",
"round": 13,
"seed": 213318073,
"waveId": "act1autochess_h06",
"time": 228.97,
"ticks": 6869,
"reason": "cleared",
"killed": 44,
"total": 129,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1205,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":44,"total":129,"perfect":false,"leaks":85,"leaksUncounted":0,"coins":0,"dmg":1076426,"bossDmg":0,"heal":0,"deaths":11,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,141,0,442,6384,0,32,0],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,128,0,2841,5816,0,77,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,150,2,6181,7692,0,24,1],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,149,1,3684,10002,0,31,1],
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_gdglow",1485,1,3,14,219946,0,0,87,4],
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_gdglow",1485,1,4,13,204111,0,0,41,1],
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_gdglow",1485,1,11,3,168014,0,0,88,3],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_gdglow",1360,1,13,0,29119,0,0,48,3],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_gdglow",1360,1,5,5,225706,0,0,45,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_gdglow",1360,1,10,6,159660,0,0,96,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,12,0,24093,0,0,193,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,14,0,32630,0,0,190,3],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_1116_liprr_2":[27,6,21,9816,240987],"enemy_1119_vofsd":[48,0,47,68191,434386],"enemy_1121_lifbos_2":[30,0,30,8066,261781]},
"hooks": [1,132,6900,1215,1215,2060,2108,5872,21,119,19,24,0,34271,3565,27,105,98,1,21,0,0,0],
"events": {"atk":1373,"deploy":25,"die":21,"dmg":2081,"fx":42,"heal":131,"leak":98,"skill":54,"spawn":119,"status":633},
"fx": {"aoe":20,"displace":3,"dodge":4,"liberate":7,"spGain":8},
"snaps": "1f17f098 a46fd1ea 4a4da16f 00118c9e 301b8f94 43fb2ce4 30085383 18a41889 a4cf28a0 651906c1 c289a97d d4e7ba5b bfeaf6ae 216c97d4 6ca7b7e7 4f28458f 599bf30a 0e3da688 e641a055 d5fd320a 5891c36b faf831cf ddfb2668"
"enemies": {"enemy_10124_uashld_2":[18,1,17,7878,352011],"enemy_1043_zomsbr":[66,39,27,1291,322100],"enemy_1061_zomshd":[45,4,41,20725,439034]},
"hooks": [1,150,6869,1151,1151,3763,3763,53819,55,140,25,23,0,35062,581,35,129,85,1,55,0,0,0],
"events": {"atk":1151,"deploy":19,"die":55,"dmg":3763,"fx":1605,"leak":85,"skill":48,"spawn":143,"status":370},
"fx": {"drone":1205,"droneLock":269,"drones":15,"explode":116},
"snaps": "9996902b 61b66e98 732c6d67 48ae38a8 b8731a98 00463d97 bbba43ed 58597bf1 871b7fd8 4e975154 4ff72676 f097682e 29ef1afa 5fdb0354 b7df1987 d1307d71 a8af977d 090fb008 25361ebd ff7fb0e8 ca2e7947 559c1e70 cec6ba4d"
},
"diy-100": {
"about": "5_diy1_a=acfend 5_diy2_b=acfend 5_diy1_a=acpion 5_diy2_b=acpion 5_diy1_a=acguad 5_diy2_b=acguad 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m01",
"modeId": "mode_multi_abyss",
"round": 12,
"seed": 1853641125,
"waveId": "act1autochess_h05",
"time": 203.93,
"ticks": 6118,
"reason": "cleared",
"killed": 60,
"total": 153,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 131,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":60,"total":153,"perfect":false,"leaks":93,"leaksUncounted":0,"coins":0,"dmg":727055,"bossDmg":0,"heal":52388,"deaths":10,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,114,0,9773,5208,0,45,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,129,1,38916,6742,0,80,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,162,8,21954,8669,0,25,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",1904,1,169,29,281570,24533,0,100,6],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,15,7,102220,25677,0,137,3],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,136,4,66511,6822,0,38,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,4,40791,0,0,108,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,5,6,63600,0,0,148,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,11,0,0,0,25465,59,3],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,13,0,0,0,26923,57,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,6,1,39821,0,0,112,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,14,0,61898,0,0,137,3],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {
"enemy_1195_sfyin": [45,11,34,18165,212500],
"enemy_1196_msfyin": [22,20,2,0,41],
"enemy_1197_sfshu": [39,11,28,33792,288323],
"enemy_1198_msfshu": [33,16,17,0,58],
"enemy_1199_sfjin": [12,2,10,25694,226118],
"enemy_1200_msfjin": [2,0,2,0,15]
},
"hooks": [1,174,6118,1612,1612,2011,1965,116,70,163,32,32,0,34916,243,55,153,93,1,79,0,0,46],
"events": {"atk":1612,"deploy":18,"die":70,"dmg":1965,"fx":206,"heal":116,"leak":93,"skill":64,"spawn":168,"status":496},
"fx": {"aoe":36,"dodge":46,"dp":44,"slash":56,"summon":24},
"snaps": "e63c08bc 993deae4 dd02af70 2d6546d3 04bbd4f3 1830ce73 2a424989 7fa082c5 ce8dd734 cc64a140 25615b6b 12274f2a dd8c1029 a4f58d5c 46f041f6 104cd848 39c8bf6f abe3608e 30a3b7d5 ab980aa3 d803a54b"
},
"diy-101": {
"about": "5_diy1_a=sharp2 5_diy2_b=sharp2 5_diy1_a=acspec 5_diy2_b=acspec 5_diy1_a=acnipe 5_diy2_b=acnipe 5_diy1_a=accast 5_diy2_b=accast 5_diy1_a=acmedc 5_diy2_b=acmedc 5_diy1_a=acsupo 5_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m02",
"modeId": "mode_multi_funny",
"round": 12,
"seed": 739544218,
"waveId": "act1autochess_h05",
"time": 204.8,
"ticks": 6144,
"reason": "cleared",
"killed": 87,
"total": 99,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 1064,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":87,"total":99,"perfect":false,"leaks":12,"leaksUncounted":0,"coins":0,"dmg":704152,"bossDmg":0,"heal":47857,"deaths":1,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,841,1,3,20,91078,1024,0,124,3],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,1,6,68740,0,0,55,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,311,1,4,0,1569,843,0,26,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,75,1,7264,1902,0,29,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,5,16,84001,0,0,202,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,8,18,178620,0,0,188,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,6,0,18491,0,0,17,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,9,20,187306,0,0,77,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,10,0,0,8016,24220,63,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,11,0,0,14535,23637,67,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,12,5,36418,24853,0,129,4],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,2,1,30664,0,0,54,2]
],
"enemies": {"enemy_1010_demon":[42,41,1,41095,296286],"enemy_1320_wdrrl_2":[12,2,10,0,174829],"enemy_1422_lrsldr_2":[45,44,1,10077,233038]},
"hooks": [1,112,6144,1243,1243,1798,1861,200,88,100,24,22,0,36141,6922,29,99,12,1,88,0,0,0],
"events": {"atk":1498,"deploy":13,"die":88,"dmg":1860,"fx":40,"heal":155,"leak":12,"skill":51,"spawn":111,"status":674},
"fx": {"aoe":13,"displace":1,"dodge":26},
"snaps": "9b12b5a2 cc58812c b15c7a02 65275a10 32f7558d 695f18fe 4167d093 a33b1752 1bd38a64 97735c90 9cfcd534 393bf4da dc5c2c12 2b2038b4 dbf5688c 52665c6d 997dacea 92e59d36 d02c5b07 0d9e13e2 efc5468b"
},
"diy-102": {
"about": "6_diy1_a=acfend 6_diy2_b=acfend 6_diy1_a=acpion 6_diy2_b=acpion 6_diy1_a=acguad 6_diy2_b=acguad 6_diy1_a=acnipe 6_diy2_b=acnipe 6_diy1_a=accast 6_diy2_b=accast 6_diy1_a=acmedc 6_diy2_b=acmedc",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m03",
"modeId": "mode_single_hard",
"round": 13,
"seed": 2379881591,
"waveId": "act1autochess_h06",
"time": 201.57,
"ticks": 6047,
"reason": "cleared",
"killed": 261,
"total": 297,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 207,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":261,"total":297,"perfect":false,"leaks":36,"leaksUncounted":0,"coins":0,"dmg":479266,"bossDmg":0,"heal":53831,"deaths":10,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,2604,1,253,12,2143,2604,0,41,2],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",2890.55,1,7,4,24083,33048,0,133,3],
["p1",3,"op","chess_char_6_diy1_a",2,null,1207.76,1,268,37,35114,4792,0,39,3],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,129,84,102697,5299,0,57,6],
["p1",5,"op","chess_char_6_diy1_a",2,null,2343.99,1,288,5,10524,7451,0,22,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",2020.71,1,132,20,93856,20393,0,81,3],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,5,3227,0,0,28,1],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,12,17,31071,0,0,65,2],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,14,1,17185,0,0,23,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,76,159366,0,0,73,2],
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,40735,67,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,152,0,0,4115,13096,22,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {
"enemy_1195_sfyin": [39,31,8,2507,109166],
"enemy_1196_msfyin": [62,60,2,0,122],
"enemy_1197_sfshu": [48,42,6,5882,183185],
"enemy_1198_msfshu": [126,118,8,0,366],
"enemy_1207_sfji": [12,10,2,69314,186308],
"enemy_1208_msfji": [10,0,10,0,119]
},
"hooks": [1,320,6047,960,960,1820,1800,140,271,307,28,25,0,31391,0,80,297,36,1,272,0,0,20],
"events": {"atk":1011,"deploy":20,"die":271,"dmg":1800,"fx":414,"heal":133,"leak":36,"skill":53,"spawn":312,"status":40},
"fx": {"aoe":97,"dodge":20,"dp":130,"slash":72,"spGain":12,"summon":83},
"snaps": "6506a5c4 821dac7d 9216e8f9 291487fb 5d47614f 5dff6977 3eeeee66 9a21bbca 8e530e36 4e93e9d1 54f80175 b1404805 b528127f 7fbbfb92 0cf1a8e3 356ad797 4d9a6a79 edae73d6 39b44426 08667abf 724dcbc6"
},
"diy-103": {
"about": "6_diy1_a=sharp2 6_diy2_b=sharp2 6_diy1_a=acspec 6_diy2_b=acspec 6_diy1_a=acsupo 6_diy2_b=acsupo",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m04",
"modeId": "mode_multi_normal",
"round": 13,
"seed": 1753717681,
"waveId": "act1autochess_h06",
"time": 210.67,
"ticks": 6320,
"reason": "cleared",
"killed": 23,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 202,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":23,"total":123,"perfect":false,"leaks":100,"leaksUncounted":0,"coins":0,"dmg":407198,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,86,2,27050,3730,0,26,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,126,14,178020,4988,0,57,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,148,0,42484,9232,0,50,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",651.83,1,150,3,84913,11220,0,77,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,160,1,132,0,30869,2160,0,81,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",450,1,133,2,43862,2450,0,84,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1148_dssbr":[51,12,39,8112,169910],"enemy_1148_dssbr_2":[60,11,49,15328,229068],"enemy_1439_dslntf":[12,0,12,10340,113857]},
"hooks": [1,150,6320,499,499,2118,2622,0,44,144,6,19,0,7621,4142,38,123,100,1,44,0,10,0],
"events": {"atk":538,"deploy":24,"die":44,"dmg":2564,"fx":61,"leak":100,"skill":42,"spawn":132,"status":458},
"fx": {"burst":10,"displace":45,"dodge":6},
"snaps": "76a5789a 0d8fe1fc aca88ba3 93412a3b 57642d38 7d0e9d04 05edb1c1 12d5b5b4 b880f5a2 706856a1 61435e3d a65a20a2 35fd9300 9021e870 94a24b43 85a49207 c097c8a1 9acd4c1a a1420866 0769fc29 60f3e7a3"
}
}
}