feedback5: merge O6 — 自选 kits for 黑 and 鸿雪 (with her 打字机); 焰狐龙梓兰's kit and test left out (a collab operator, out of the pool since 8287422); 黑 S3's tests and kit header follow its DEFAULT trigger (「攻击范围改为」); golden: only diy moved

This commit is contained in:
sganggs committed 2026-10-06 00:21:15 +08:00
commit 379822a04e
6 files changed
+1252 -117

No files matched your search

+2
View File
@@ -98,6 +98,8 @@ export const OPERATOR_KIT_FILES = Object.freeze([
'op-zuole.js',
'op-chyue.js',
'op-huang.js',
'op-shwaz.js',
'op-bgsnow.js',
]);
/**
+226
View File
@@ -0,0 +1,226 @@
// server/sim/content/kits/ops/op-bgsnow.js — 鸿雪 (char_4055_bgsnow) 自选 operator kit: 6★ 重射手 (狙击), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, both modules (ARC-Y 打字机色带, ARC-X 仪式感) at
// every form, and the kit of her summon “打字机” (token_10026_bgsnow_subbow). Kit contract and the 自选 rules: ../README.md
// ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4055_bgsnow): 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 / token_table (zh_CN, as built into backups.json)
// and PRTS 鸿雪 + “打字机” (点题 备注; the {{**|82%|下降18%}} marks = a final ×0.82; PRTS 伤害分类: "鸿雪2技能三段伤害的后2段"
// are 持续伤害).
// - Trait (重射手) "高精度的近距离射击": ranged physical arrows, 3-6, can hit air units (PRTS 分支特性信息 重射手 "可对空"),
// blocks 1, ground enemies target her (no flag).
// - Module ARC-Y “打字机色带” trait "攻击正前方的敌人时攻击力提升至105%且无视其物理闪避" (trait bb atk_scale): her damage on
// an enemy whose body is on her straight line ahead ×atk_scale, not dodgeable — 点题's two 持续伤害 hits included
// [ASSUMED: they are two of its "3次攻击"]; 正前方 = local row 0, column ≥ 0 (her own tile included, as the 3-2 "前方3格"
// lines count it) [ASSUMED]. Stage 2+ "属性更强的打字机": the typewriter's stats of the data (token module attributes).
// - Module ARC-X “仪式感”: "再部署时间减少" (attribute respawn_time −25, in the stats); stage 2+ upgrades 弱点速记 (below).
// - T1 自动打字机 "可使用一个持续25秒的“打字机”,“打字机”拥有和自己一样的技能,拥有独立的再部署时间": the typewriter is a
// hand piece the player places (the mode's talent summons, content/tokens.js header): it deploys with the board, leaves
// after 25 s (the token's hidden talent `interval`), and comes back on its tile after its own redeploy time (data 40 s; ×
// respawn_time while she carries S2), paying its cost (5 DP), while she is on the field [ASSUMED: as the 海嗣]. Its skill
// is the copy of hers the data gives (bySkill: sktok_bgsnow_1 / 2 / 3 — the 卫戍 strategy force-opens a summon's manual
// skill, PRTS 卫戍协议/帮助): free (spCost 0), once per deployment, by the data's DEFAULT. It is untargetable ("不会受到
// 攻击"), blocks nothing, hits air units. content/tokens.js has no kit for it: her install gives her pieces this one.
// - T2 弱点速记 "“打字机”的攻击会使命中目标的防御力下降18%,持续4秒;若“打字机”放在鸿雪周围四格则效果提升至23%": every
// damage the typewriter's attacks land cuts the target's DEF by bgsnow_token[def_down]_1 (_2 while the typewriter stands
// on one of the 4 tiles next to her — while she is on the field [ASSUMED]) for `duration` s: the catalogue `defDown`
// (final ×, 同名 keeps the strongest). ARC-X stage 2+: 28 % / 33 % and "周围八格" (the talent text names the ring).
// - S1 抑扬格 (AUTO, attack SP, data DEFAULT — an AUTO attack buff waits for her next attack): ATK +atk until she leaves the
// field ("持续时间无限": a toggle), and every attack meanwhile `prob` to deal ×atk_scale (one roll per attack).
// - S2 点题 (MANUAL, 2 charges, data SKILL_RANGE on its 3-1): passive — the typewriter's redeploy time ×respawn_time;
// active — 3 attacks at once ("立即": the cast takes the next attack now) on one target of the 3-1, atk_scale × ATK each:
// the first a physical 普通伤害 attack, the other two physical 持续伤害 (PRTS 伤害分类: tags 'dot', not dodgeable).
// PRTS 备注 "仅在干员自身范围内有可攻击目标时可释放": the data's SKILL_RANGE fires for any enemy of the 3-1, untargetable
// ones too; the cast then waits (pending) for an attackable target — reported as an open question (the 备注 reading
// of the strategy would be the basic one on her own range).
// - S3 锐笔速写 (MANUAL, data ACTIVE_RANGE on its 3-1): range 3-1 while it runs, attack interval base_attack_time s (−0.6 on
// 1.6 s), every attack atk_scale × ATK, bgsnow_s_3[atk_up].atk_scale × ATK on the 正前方3格 (the 3-2 line, her tile
// included [ASSUMED]).
import { num, up, traitBb, skillRec, batMod } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { bodyKeys, bodyInKeys } from '../../../body.js';
import { toLocal } from '../../../dir.js';
import { hasHp } from '../../../damage.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_bgsnow_1';
const S2 = 'skchr_bgsnow_2';
const S3 = 'skchr_bgsnow_3';
export const TYPEWRITER = 'token_10026_bgsnow_subbow';
const TS1 = 'sktok_bgsnow_1';
const TS2 = 'sktok_bgsnow_2';
const TS3 = 'sktok_bgsnow_3';
/** 锐笔速写's 正前方3格: the 3-2 line (range_table) — her tile and the three ahead. */
const FRONT_3 = Object.freeze([[0, 0], [0, 1], [0, 2], [0, 3]]);
/** 点题's two 持续伤害 hits (PRTS 伤害分类 attackType BUFF). */
const TAG_PRECIS = 'bgsnow:precis';
/** 自动打字机: the typewriter's life when neither the token nor the talent carries it (PRTS: 持续25秒 at E2). */
const LIFE_FALLBACK = 25;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** 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;
/** ARC-Y 正前方: the enemy's body touches the unit's straight line ahead (local row 0, local column ≥ 0). */
function inFrontLine(unit, e) {
for (const k of bodyKeys(e)) {
const [lr, lc] = toLocal(Math.floor(k / COLS) - unit.tileR, (k % COLS) - unit.tileC, unit.dir);
if (lr === 0 && lc >= 0) return true;
}
return false;
}
/** 点题 / its copy: the two 持续伤害 hits after the attack's first (普通伤害) hit, on the same target. */
function precisFollowUps(battle, unit, target, scale) {
for (let i = 0; i < 2 && target && target.alive; i++) {
battle.dealDamage(unit, target, { amount: unit.s.atk * scale, type: 'phys', isSkill: true, canDodge: false, tags: ['skill', 'dot', TAG_PRECIS] });
}
}
/** 抑扬格 / its copy: while `on(unit)`, every attack `prob` to deal ×scale (one roll per attack). */
function installIamb(battle, unit, prob, scale, on) {
if (!(prob > 0) || !(scale > 0) || scale === 1) return;
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || d.isSplash || d.type === 'element' || !on(unit)) return;
const m = unit.mem;
if (!d.attackId || m.iambRollId !== d.attackId) {
m.iambRollId = d.attackId;
m.iambProc = battle.rng.chance(prob);
if (m.iambProc) battle.fx('crit', { x: t.x, y: t.y, id: unit.id });
}
if (m.iambProc) d.amount *= scale;
}, { owner: unit });
}
/**
* The typewriter's kit: its skill (the copy of her skill the data gives), 弱点速记, its life and its redeploys.
* `owner` = 鸿雪, `chess` = her composed record.
*/
function typewriterKit(t, owner, chess) {
const def = t.def;
const sk = def.skill ?? null;
const tb = sk?.bb ?? {};
const weak = tokTalent(def, 'duration');
const wb = weak?.bb ?? {};
const cut1 = -num(wb['bgsnow_token[def_down]_1.def']), cut2 = -num(wb['bgsnow_token[def_down]_2.def'], -cut1);
const ring8 = /八格/.test(String(weak?.description ?? ''));
const ownTalent = (chess?.talents ?? []).find((x) => x && x.index === 0)?.bb ?? {};
const life = num(tokTalent(def, 'interval')?.bb?.interval, num(ownTalent.duration, LIFE_FALLBACK));
const respawnMul = owner.skill?.id === S2 ? num(bbOf(chess, S2).respawn_time, 1) : 1;
const near = () => {
if (!up(owner)) return false;
const dr = Math.abs(t.tileR - owner.tileR), dc = Math.abs(t.tileC - owner.tileC);
return ring8 ? Math.max(dr, dc) === 1 : dr + dc === 1;
};
const skillOn = (u) => !!(u.skill && u.skill.active);
let skill = null;
if (sk?.id === TS1) skill = { kind: 'toggle', mods: { atkPct: num(tb.atk) } };
else if (sk?.id === TS2) {
const scale = num(tb.atk_scale, 1);
skill = { kind: 'instant', attack: { atkScale: scale, onHit({ battle, unit, target }) { precisFollowUps(battle, unit, target, scale); } } };
} else if (sk?.id === TS3) {
skill = {
kind: 'duration',
targeting: { rangeGrid: sk.rangeGrid ?? [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]] },
mods: { batPct: batMod(tb.base_attack_time, { stats: def.stats }) },
attack: { atkScale: num(tb['attack@atk_scale'], 1) },
};
}
return {
skill,
talents: [{ install(battle, unit) { // 弱点速记: every damage of its attacks cuts the target's DEF (near her: more)
const dur = num(wb.duration, 4);
if (!(cut1 > 0) || !(dur > 0)) return;
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.target || ctx.target.side !== 'enemy' || !hasHp(ctx.target) || ctx.dmg?.type === 'element') return;
battle.applyStatus(ctx.target, 'defDown', { duration: dur, value: near() ? cut2 : cut1, source: unit });
}, { owner: unit });
} }],
install(battle, unit) {
if (sk?.id === TS1) installIamb(battle, unit, num(tb.prob), num(tb.atk_scale, 1), skillOn);
// 持续25秒: leaves `life` s after each deployment
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit || !(life > 0)) return;
const seq = unit.deploySeq;
battle.after(life, () => { if (unit.alive && unit.deploySeq === seq) battle.retreat(unit, { reason: 'expired', permanent: true }); }, { owner: unit });
}, { owner: unit });
// 拥有独立的再部署时间: back on its tile after its redeploy time, paying its cost, while 鸿雪 stands (the piece is kept:
// `removed` false, as content/tokens.js enableRespawn)
battle.on('death', (ctx) => {
if (ctx.unit !== unit || battle.finished) return;
unit.removed = false;
const at = battle.time + Math.max(0, num(unit.base.respawnTime) * respawnMul);
unit.mem.respawnAt = at;
battle.every(0.25, (b, sched) => {
if (unit.alive || unit.removed) { sched.cancel(); return; }
if (b.time + 1e-9 < at || !up(owner)) return;
if (b.redeploy(unit, { free: false })) sched.cancel();
}, { owner: unit });
}, { owner: unit, priority: -10 });
},
};
}
export default {
char_4055_bgsnow: (bb, chess) => {
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
const frontScale = num(traitBb(chess).atk_scale, 1);
const s2Scale = num(b2.atk_scale, 1);
const s3Scale = num(b3.atk_scale, 1), s3Front = num(b3['bgsnow_s_3[atk_up].atk_scale'], s3Scale);
return {
skills: {
[S1]: { kind: 'toggle', mods: { atkPct: num(b1.atk) } },
[S2]: {
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
// the 3-1 selects the target; her attack range stays her own (a 技能范围, not a range change)
targeting: { rangeGrid: s2?.rangeGrid ?? null, showOwnRange: true },
attack: { atkScale: s2Scale, onHit({ battle, unit, target }) { precisFollowUps(battle, unit, target, s2Scale); } },
onStart({ unit }) { unit.atkCd = 0; }, // "立即": the pending attack goes off now when a target is there
},
[S3]: {
kind: 'duration',
targeting: { rangeGrid: s3?.rangeGrid ?? null },
mods: { batPct: batMod(b3.base_attack_time, chess) },
attack: { atkScale: s3Scale },
},
},
talents: [
// 自动打字机 (and 弱点速记, the typewriter's own talent): her typewriter pieces run the typewriter's kit — set up
// here, before the battle starts (Battle._setupUnit with a kit, as spawnToken's `kit` option)
{ install(battle, unit) {
for (const t of battle.allyUnits) {
if (t.kind !== 'token' || t.defId !== TYPEWRITER || t.ownerUnit !== unit || t.alive || t.deployed) continue;
if (t.kit) battle.offOwner(t); // a piece set up before her: drop its generic kit's hooks
battle._setupUnit(t, typewriterKit(t, unit, chess));
}
} },
],
install(battle, unit) {
const sid = unit.skill?.id;
if (sid === S1) installIamb(battle, unit, num(b1.prob), num(b1.atk_scale, 1), (u) => !!u.skill?.active);
// 锐笔速写: 正前方3格 ⇒ atk_up's scale instead of the skill's
if (sid === S3 && s3Front !== s3Scale && s3Scale > 0) {
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || !unit.skill?.active) return;
if (bodyInKeys(t, absoluteRangeKeys(FRONT_3, unit.tileR, unit.tileC, unit.dir, 0))) d.amount *= s3Front / s3Scale;
}, { owner: unit });
}
// ARC-Y 打字机色带: 攻击正前方的敌人时攻击力提升至105%且无视其物理闪避
if (frontScale > 0 && frontScale !== 1) {
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || d.type === 'element') return;
if (!(d.isAttack || d.tags?.includes(TAG_PRECIS)) || !inFrontLine(unit, t)) return;
d.amount *= frontScale;
d.canDodge = false;
}, { owner: unit });
}
},
};
},
};
+162
View File
@@ -0,0 +1,162 @@
// server/sim/content/kits/ops/op-shwaz.js — 黑 (char_340_shwaz) 自选 operator kit: 6★ 重射手 (狙击), an owned-6★ pick of the
// tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (ARC-X 定制弩配件套装, ARC-Y 老剃刀) at
// every form. Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_340_shwaz): 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) and PRTS 黑
// (破甲箭头 备注 "攻击力提升效果为提升攻击力倍率"; 战术的终结 "攻击间隔略微增大(+0.4)"; the {{**|80%|下降20%}} marks of the
// DEF cut = a final ×0.8).
// - Trait (重射手) "高精度的近距离射击": the plain closerange profile — ranged physical arrows, 3-6, can hit air units
// (PRTS 分支特性信息 重射手 "可对空"), blocks 1, ground enemies target her (no flag).
// - T1 破甲箭头 "攻击时,20%几率当次攻击的攻击力提升至160%,并使命中目标的防御力下降20%,持续5秒": one roll per attack
// (`prob`); on a proc the attack's damage ×atk_scale (an ATK-scale multiplier, PRTS 备注, so it multiplies the skills'
// own scales) and, once the hit landed (命中: a dodged hit does nothing), the target's DEF ×(1 + def) for
// defdown_duration s — the catalogue `defDown` (final ×0.8, PRTS; 同名 keeps the strongest). The skills raise the chance:
// S1's next attack to S1 talent@prob, S2 / S3's attacks while they run to their talent@prob (every attack a skill makes
// carries dmg.isSkill — S2's empty attack override marks its attacks for that). ARC-Y stage 2+ upgrades it (stage 3:
// 170 %, DEF −25 %) — read from the base talent and the module's NAMED change: ARC-Y's hidden talent part (talentIndex 0,
// isHideTalent, atk_scale 1.05, prefabKey "10") is the trait's 105 % as a talent of its own in the game, which the
// data's merge by talentIndex folds into 破甲箭头 at stage 1 (composed atk_scale 1.05) — reported, not used.
// - T2 交叉火力 "场上存在黑和另外至少一名【狙击】干员时,所有【狙击】干员的攻击力+8%": while she is on the field and another
// 【狙击】 operator is too, every 【狙击】 operator of the field (her included; a partner's in a shared field too [ASSUMED,
// as 推进之王's 万兽之王]) ATK +atk, refreshed every 0.5 s (lapses within 0.6 s when the condition ends); several sources
// keep the strongest. ARC-X stage 2+ "携带黑和另外至少一名【狙击】干员时…+13%": 携带 = in the squad — the player's own
// pieces of the battle, deployed or not — so with another 【狙击】 piece in her player's battle every 【狙击】 operator of
// that player ATK +13 % for the whole battle (persist; a partner's operators are not in her squad [ASSUMED]).
// - Module ARC-X “定制弩配件套装”: "再部署时间减少" (attribute respawn_time −25, in the stats: 70 → 45 s) + HP / ATK.
// - Module ARC-Y “老剃刀” trait "攻击正前方的敌人时攻击力提升至105%且无视其物理闪避" (trait bb atk_scale): her attack damage
// on an enemy whose body is on her straight line ahead ×atk_scale and not dodgeable (canDodge false — her damage is
// physical only). 正前方 = local row 0, column ≥ 0 in her facing — her own tile included, as S3's 3-2 "前方3格" counts
// it [ASSUMED: no source draws the line].
// - S1 强弩 (AUTO, attack SP 4, data DEFAULT): the next attack ×atk_scale (and the T1 chance above).
// - S2 暮眼锐瞳 (MANUAL, data DEFAULT): ATK +atk for 33 / 36 s, T1 chance talent@prob.
// - S3 战术的终结 (MANUAL): range → 3-2 (her tile + 3 ahead) while it runs, attack interval +base_attack_time s (a flat
// +0.4 on her 1.6 s, PRTS), ATK +atk, T1 chance 100 %. Trigger: the data's DEFAULT (the basic strategy) — "攻击范围改为前方
// 3格" is an attack-range change, not a 技能范围 (tools/build-data.mjs ATTACK_RANGE_CHANGE has "改为" since O6's report).
import { num, up, traitBb, moduleOn, skillRec, batMod } from '../shared/tier1.js';
import { bodyKeys } from '../../../body.js';
import { toLocal } from '../../../dir.js';
import { hasHp } from '../../../damage.js';
import { COLS } from '../../../constants.js';
const S1 = 'skchr_shwaz_1';
const S2 = 'skchr_shwaz_2';
const S3 = 'skchr_shwaz_3';
/** 交叉火力's buff (the field aura and the squad version share it: one effect per operator, the strongest). */
const CROSSFIRE = 'talent:shwaz:crossfire';
/** 交叉火力's refresh interval (s), as installAura. */
const AURA_EVERY = 0.5;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** A 【狙击】 operator (summons are never operators). */
const isSniperOp = (a) => !!a && a.kind === 'op' && a.def?.profession === 'SNIPER';
/** The module the record fights with (its active one), or null. */
function activeModule(chess) {
if (!moduleOn(chess)) return null;
return (chess.modules ?? []).find((m) => m && m.uniEquipId === chess.module.id) ?? null;
}
/**
* 破甲箭头's blackboard: the base talent (no module) with the module's NAMED changes of talent 0 only — never the hidden
* trait part (see the header).
*/
function pierceBb(chess) {
const base = (chess?.talentsBase ?? chess?.talents ?? []).find((t) => t && t.index === 0)?.bb ?? {};
const ups = (activeModule(chess)?.talentChanges ?? []).filter((t) => t && t.talentIndex === 0 && !t.hidden && t.name);
return Object.assign({}, base, ...ups.map((t) => t.bb || {}));
}
/** The record's 交叉火力 (talent 1, the module's change included): its blackboard and whether it reads 携带 (squad). */
function crossfireOf(chess) {
const t = (chess?.talents ?? []).find((x) => x && x.index === 1) ?? null;
return { atk: num(t?.bb?.atk), squad: /携带/.test(String(t?.desc ?? '')) };
}
/** ARC-Y 正前方: the enemy's body touches the unit's straight line ahead (local row 0, local column ≥ 0). */
function inFrontLine(unit, e) {
for (const k of bodyKeys(e)) {
const [lr, lc] = toLocal(Math.floor(k / COLS) - unit.tileR, (k % COLS) - unit.tileC, unit.dir);
if (lr === 0 && lc >= 0) return true;
}
return false;
}
export default {
char_340_shwaz: (bb, chess) => {
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s3 = skillRec(chess, S3);
const t0 = pierceBb(chess);
const cross = crossfireOf(chess);
const frontScale = num(traitBb(chess).atk_scale, 1);
return {
skills: {
[S1]: { kind: 'instant', attack: { atkScale: num(b1.atk_scale, 1) } },
// (an empty attack override: the attacks made while it runs carry dmg.isSkill — 破甲箭头's raised chance)
[S2]: { kind: 'duration', mods: { atkPct: num(b2.atk) }, attack: {} },
[S3]: {
kind: 'duration',
targeting: { rangeGrid: s3?.rangeGrid ?? [[0, 0], [0, 1], [0, 2], [0, 3]] },
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
},
},
talents: [
{ install(battle, unit) { // 破甲箭头: one roll per attack; ×atk_scale on a proc, then DEF cut on the target hit
const scale = num(t0.atk_scale, 1), cut = -num(t0.def), dur = num(t0.defdown_duration, 5), base = num(t0.prob);
const sid = unit.skill?.id;
const raised = num(bbOf(chess, sid)['talent@prob'], base);
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || d.isSplash || d.type === 'element') return;
const m = unit.mem;
if (!d.attackId || m.shwazRollId !== d.attackId) {
m.shwazRollId = d.attackId;
const p = d.isSkill ? raised : base;
m.shwazProc = p > 0 && battle.rng.chance(p);
if (m.shwazProc) battle.fx('crit', { x: t.x, y: t.y, id: unit.id });
}
if (!m.shwazProc) return;
d.amount *= scale;
d.shwazPierce = true;
}, { owner: unit });
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || !ctx.dmg?.shwazPierce || !(cut > 0) || !hasHp(ctx.target) || ctx.target.side !== 'enemy') return;
battle.applyStatus(ctx.target, 'defDown', { duration: dur, value: cut, source: unit });
}, { owner: unit });
} },
{ install(battle, unit) { // 交叉火力: field aura, or (ARC-X stage 2+) the squad's snipers for the whole battle
const v = cross.atk;
if (!(v > 0)) return;
const give = (a, extra) => {
const cur = a.findBuff(CROSSFIRE);
if (cur && cur.source !== unit && (cur.data?.v ?? 0) > v && cur.timeLeft > 0.05) return;
battle.addBuff(a, { key: CROSSFIRE, mods: { atkPct: v }, source: unit, data: { v }, ...extra });
};
if (cross.squad) {
const squad = battle.allyUnits.filter((a) => isSniperOp(a) && a.ownerId === unit.ownerId);
if (squad.some((a) => a !== unit)) for (const a of squad) give(a, { persist: true, allowDead: true, tags: ['talent'] });
return;
}
battle.every(AURA_EVERY, () => {
if (!up(unit)) return;
const snipers = battle.alliesFor(unit).filter(isSniperOp);
if (!snipers.some((a) => a !== unit)) return;
for (const a of snipers) give(a, { duration: AURA_EVERY + 0.1, tags: ['aura'] });
}, { owner: unit, immediate: true });
} },
],
install(battle, unit) {
// ARC-Y 老剃刀: 攻击正前方的敌人时攻击力提升至105%且无视其物理闪避
if (!(frontScale > 0) || frontScale === 1) return;
battle.on('hit', (ctx) => {
const d = ctx.dmg, t = ctx.target;
if (ctx.source !== unit || !t || t.side !== 'enemy' || !d.isAttack || d.type === 'element' || !inFrontLine(unit, t)) return;
d.amount *= frontScale;
d.canDodge = false;
}, { owner: unit });
},
};
},
};
+306
View File
@@ -0,0 +1,306 @@
// test/content/op_bgsnow.test.js — the 自选 operator kit of 鸿雪 (char_4055_bgsnow, 6★ 重射手; kit
// server/sim/content/kits/ops/op-bgsnow.js) and of her summon “打字机” (token_10026_bgsnow_subbow), fielded the production
// way (a DIY slot + its `diy` pick, simdata getDiy; the typewriter 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, ARC-Y “打字机色带” or
// ARC-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_bgsnow.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';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const SNOW = 'char_4055_bgsnow';
const TOK = 'token_10026_bgsnow_subbow';
const FORMS = BACKUPS.units[SNOW].forms;
const TOKREC = BACKUPS.tokens[TOK];
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const Y = 'uniequip_002_bgsnow', X = 'uniequip_003_bgsnow';
const S1 = 'skchr_bgsnow_1', S2 = 'skchr_bgsnow_2', S3 = 'skchr_bgsnow_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);
/** The typewriter's variant of a form, with the pick's skill / module (getDiyToken). */
const tokOf = (tier, elite, skill, mod) => {
let v = TOKREC.variants[`${SNOW}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
return v;
};
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' }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, Y, X].map((m) => [t, true, m]))];
/** 鸿雪 as uid 1 at (10, 5) facing RIGHT; `tok` = the typewriter piece's tile (uid 2) or null. */
function field({ tier = 5, elite = false, mod = null, skill = 0, tok = null, tokFirst = false, seed = 5, others = [] } = {}) {
const op = { uid: 1, diy: { slot: SLOT[tier], charId: SNOW, skillIndex: skill, uniEquipId: mod }, elite, row: 10, col: 5 };
const piece = tok ? [{ uid: 2, kind: 'token', tokenId: TOK, ownerUid: 1, row: tok[0], col: tok[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', 'dodge'], captureNoisy: true,
units: tokFirst ? [...piece, op, ...others] : [op, ...piece, ...others],
});
h.step();
return { h, u: h.unit(1), t: h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === TOK) ?? null };
}
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
test('鸿雪 in every 自选 form: her kit, the form\'s stats + module attributes, 3-6, ranged physical, hits air, blocks 1, ground-targetable, the data triggers; offered as a pick', () => {
assert.equal(OPERATOR_KITS[SNOW], KITS[SNOW]);
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;
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [SNOW, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.respawnTime],
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.respawnTime + (m?.attr.respawnTime ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [1, 'ranged', true, 'phys', 1.6], `${label(f)}: 重射手`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-6`);
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'SKILL_RANGE', 'ACTIVE_RANGE']);
assert.deepEqual([modOf(5, Y).attr, modOf(6, Y).attr, modOf(5, X).attr, modOf(6, X).attr],
[{ maxHp: 130, atk: 55 }, { maxHp: 170, atk: 75 }, { atk: 80, def: 10, respawnTime: -25 }, { atk: 120, def: 16, respawnTime: -25 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(SNOW) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(SNOW)));
assert.equal(validateDiyPicks({ [SLOT[5]]: { charId: SNOW, skillIndex: 1, uniEquipId: X } }, { data, kitted: KITTED_CHARS }).ok, true);
});
test('S1 抑扬格 (AUTO, attack SP 29 / 25, data DEFAULT): after that many attacks ATK +35 % / +50 % until she leaves the field, every attack meanwhile 30 % to deal ×145 % / ×160 %', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.spCost, sk.bb.atk, sk.bb.prob, sk.bb.atk_scale], elite ? [25, 0.5, 0.3, 1.6] : [29, 0.35, 0.3, 1.45], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 0, seed: 12 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.spType], ['toggle', 'DEFAULT', 'attack'], `T${tier}`);
h.spawn('enemy_dummy', { pos: [11, 6] });
assert.ok(h.runUntil(() => u.skill.active, 60), `T${tier}: cast`);
const before = from(h, u);
assert.equal(before.length, sk.spCost, `T${tier}: after ${sk.spCost} attacks`);
assert.ok(before.every((c) => Math.abs(c.amount - u.base.atk) < 1e-6), `T${tier}: plain before it`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
h.run(500);
assert.ok(u.skill.active, `T${tier}: 持续时间无限`);
const after = from(h, u).slice(before.length);
const atk = u.base.atk * (1 + sk.bb.atk);
for (const c of after) assert.ok([atk, atk * sk.bb.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: ${c.amount}`);
const r = after.filter((c) => c.amount > atk + 1e-6).length / after.length;
assert.ok(Math.abs(r - sk.bb.prob) < 0.06, `T${tier}: ${r}`);
h.b.retreat(u);
assert.ok(!u.skill.active, `T${tier}: ends when she leaves`);
done(h);
}
});
test('S2 点题 (MANUAL, 2 charges, data SKILL_RANGE on 3-1): an enemy 3 tiles ahead (3-1, not 3-6) casts it and is struck at once: atk_scale × ATK as a 普通伤害 attack, then twice as 持续伤害 (not dodgeable); her card range stays 3-6', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.bb.atk_scale, sk.bb.respawn_time, sk.maxChargeTime, sk.spCost, sk.initSp], elite ? [2, 0.6, 2, 10, 6] : [1.8, 0.65, 2, 11, 3], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 1, seed: 3 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.maxCharges], ['charges', 'SKILL_RANGE', 2], `T${tier}`);
u.skill.gainSp(999);
h.run(1);
assert.equal(u.skill.activations, 0, `T${tier}: nobody in range`);
const e = h.spawn('enemy_dummy', { pos: [10, 8] });
h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), `T${tier}: cast for the enemy on 3-1`);
const tc = h.b.time;
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: the card keeps 3-6`);
h.run(0.5);
const atk = h.hooksOf('attack').filter((c) => c.attacker === u);
approx(atk[0].t, tc, `T${tier}: 立即`, 0.04);
assert.equal(atk[0].isSkill, true);
const dodged = h.hooksOf('dodge').filter((c) => c.source === u);
const hits = from(h, u);
assert.equal(dodged.length, 1, `T${tier}: the 普通伤害 hit can be dodged (a dodge enemy)`);
assert.equal(hits.length, 2, `T${tier}: the two 持续伤害 hits cannot`);
for (const c of hits) {
approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ×${sk.bb.atk_scale}`);
assert.deepEqual([c.dmg.isAttack, c.dmg.canDodge, c.dmg.tags.includes('dot'), c.type], [false, false, true, 'phys']);
}
assert.equal(u.skill.charges, 1, `T${tier}: one of two charges spent`);
h.b.removeBuff(e, 'test:dodge');
const n0 = from(h, u).length;
h.run(3.5);
const second = from(h, u).slice(n0).filter((c) => c.dmg.isSkill);
assert.deepEqual(second.map((c) => [c.dmg.isAttack, c.dmg.tags.includes('dot')]), [[true, false], [false, true], [false, true]], `T${tier}: the second charge 3 s later: 普通 then 2 × 持续`);
done(h);
}
});
test('S3 锐笔速写 (MANUAL, data ACTIVE_RANGE on 3-1): an enemy only on [0,3] casts it; range 3-1 (and back), interval 1.6 − 0.6 s, ×155 % / 170 % off her line, ×185 % / 215 % on the 正前方3格', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.bb.base_attack_time, sk.bb.atk_scale, sk.bb['bgsnow_s_3[atk_up].atk_scale'], sk.rangeId], elite ? [27, -0.6, 1.7, 2.15, '3-1'] : [26, -0.6, 1.55, 1.85, '3-1'], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 2, seed: 7 });
assert.deepEqual([u.skill.kind, u.skill.rule], ['duration', 'ACTIVE_RANGE']);
u.skill.gainSp(999);
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast for [0,3] (outside 3-6)`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-1 while it runs`);
approx(u.s.interval, 1.0, `T${tier}: interval`);
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
h.b.kill(far, null);
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
h.run(8);
const onFront = from(h, u).filter((c) => c.dmg.isAttack && c.target === front);
h.b.kill(front, null);
h.run(4);
const onSide2 = from(h, u).filter((c) => c.dmg.isAttack && c.target === side);
assert.ok(onFront.length > 3 && onSide2.length > 1, `T${tier}: both struck (${onFront.length} / ${onSide2.length})`);
for (const c of onFront) approx(c.amount, u.base.atk * sk.bb['bgsnow_s_3[atk_up].atk_scale'], `T${tier}: 正前方3格`);
for (const c of onSide2) approx(c.amount, u.base.atk * sk.bb.atk_scale, `T${tier}: off the line`);
u.skill.extend(-999);
h.step(2);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-6`);
approx(u.s.interval, 1.6, `T${tier}: interval back`);
done(h);
}
});
test('ARC-Y 打字机色带 trait: ×105 % and no dodge on her straight line ahead (stages 1 and 3) — 点题\'s 持续伤害 hits too; off the line it dodges; ARC-X / no module nothing', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y = elite && mod === Y;
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 3 });
u.skill.charges = 0; u.skill.sp = 0;
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
for (const e of [front, side]) h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
h.run(12);
const onFront = from(h, u).filter((c) => c.target === front);
if (y) {
assert.ok(onFront.length > 4, `${label(f)}: the front enemy is hit`);
for (const c of onFront) approx(c.amount, u.s.atk * 1.05, `${label(f)}: ×1.05`);
// 点题 on the front enemy: all three hits ×1.05
h.b.removeBuff(u, 'test:noSp');
u.skill.addCharge(1);
const n0 = from(h, u).length;
h.run(0.5);
const s2 = from(h, u).slice(n0).filter((c) => c.dmg.isSkill);
assert.equal(s2.length, 3, `${label(f)}: 点题's three hits land`);
for (const c of s2) approx(c.amount, u.s.atk * skillOf(tier, true, S2).bb.atk_scale * 1.05, `${label(f)}: 点题 ×1.05`);
h.b.kill(front, null);
h.run(6);
assert.ok(h.hooksOf('dodge').filter((c) => c.source === u && c.target === side).length > 2, `${label(f)}: off the line it dodges`);
} else {
assert.equal(onFront.length, 0, `${label(f)}: every attack dodged`);
}
done(h);
}
});
test('“打字机”: her placed piece runs its kit (not the generic one) — the variant\'s stats (ARC-Y stage 3 stronger), untargetable, 3-2, blocks 0, hits air; deploys with the board, leaves after 25 s, back on its tile after 40 s (S2: ×65 % / ×60 %) for 5 DP, never while 鸿雪 is down', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const v = tokOf(tier, elite, skill, elite ? mod : null);
const { h, t } = field({ tier, elite, mod, skill, tok: [12, 3], tokFirst: skill === 1 });
assert.ok(t && t.alive && t.deployed, `${label(f)} S${skill + 1}: deployed with the board`);
assert.deepEqual([!!t.kit.generic, t.skill.id, t.base.atk, t.base.maxHp, t.base.respawnTime, t.base.cost], [false, v.skill.skillId, v.stats.atk, v.stats.maxHp, 40, 5], `${label(f)} S${skill + 1}`);
assert.deepEqual([!!t.s.flags.untargetable, t.s.blockCnt, t.profile.canHitFly, t.profile.attack, t.liveRangeGrid], [true, 0, true, 'ranged', [[0, 0], [0, 1], [0, 2], [0, 3]]], `${label(f)} S${skill + 1}: 不会受到攻击`);
done(h);
}
}
assert.deepEqual([tokOf(5, false, 0).stats.atk, tokOf(5, true, 0).stats.atk, tokOf(6, true, 0, Y).stats.atk, tokOf(5, true, 0, Y).stats.atk], [666, 799, 869, 799]);
// life and redeploys (S1: 40 s; S2: ×respawn_time)
for (const [tier, elite, skill, mul] of [[5, false, 0, 1], [5, false, 1, 0.65], [6, true, 1, 0.6]]) {
const { h, u, t } = field({ tier, elite, skill, tok: [12, 3] });
h.b.players[0].dp = 20;
assert.ok(h.runUntil(() => !t.alive, 26));
approx(h.b.time, 25, `T${tier} S${skill + 1}: 25 s`, 0.05);
assert.equal(h.hooksOf('death').find((c) => c.unit === t).reason, 'expired');
assert.ok(h.runUntil(() => t.alive, 60));
approx(h.b.time, 25 + 40 * mul, `T${tier} S${skill + 1}: back after ${40 * mul} s`, 0.3);
assert.deepEqual([t.tileR, t.tileC, h.b.players[0].dp], [12, 3, 15], `T${tier} S${skill + 1}: on its tile, 5 DP`);
assert.ok(u.alive);
done(h);
}
// 鸿雪 down: the typewriter waits for her
const { h, u, t } = field({ tier: 5, elite: false, skill: 0, tok: [12, 3] });
h.b.players[0].dp = 0;
h.run(10);
h.b.kill(u, null);
h.run(60);
assert.ok(!t.alive, 'not while 鸿雪 is down');
h.b.players[0].dp = 25;
assert.ok(h.runUntil(() => u.alive, 30), '鸿雪 back');
assert.ok(h.runUntil(() => t.alive, 1), 'then the typewriter');
done(h);
});
test('“打字机”\'s skill is the copy of hers, free once per deployment: S1 ATK +35 % / +50 % for good with the 30 % proc, S2 three hits of ×180 % / ×200 % (普通 + 2 × 持续), S3 4-1 + interval −0.6 s + ×185 % / ×215 % for 16 / 17 s', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
for (const skill of [0, 1, 2]) {
const v = tokOf(tier, elite, skill, null);
const tb = v.skill.bb;
const { h, t } = field({ tier, elite, skill, tok: [11, 5], seed: 4 });
h.spawn('enemy_dummy', { pos: [11, 7] });
assert.deepEqual([t.skill.rule, t.skill.spCost, t.skill.charges], ['DEFAULT', 0, 1], `T${tier} S${skill + 1}: free, ready`);
assert.ok(h.runUntil(() => t.skill.activations === 1, 1), `T${tier} S${skill + 1}: cast at its first attack`);
if (skill === 0) {
approx(t.s.atk, t.base.atk * (1 + tb.atk), `T${tier}: S1 copy ATK`);
h.run(20);
assert.ok(t.skill.active, `T${tier}: S1 copy stays`);
const hits = from(h, t);
const atk = t.base.atk * (1 + tb.atk);
assert.ok(hits.some((c) => Math.abs(c.amount - atk * tb.atk_scale) < 1e-6) && hits.some((c) => Math.abs(c.amount - atk) < 1e-6), `T${tier}: S1 copy procs ×${tb.atk_scale}`);
} else if (skill === 1) {
h.run(0.5);
const s2 = from(h, t).filter((c) => c.dmg.isSkill);
assert.deepEqual(s2.map((c) => [c.dmg.isAttack, c.dmg.tags.includes('dot')]), [[true, false], [false, true], [false, true]], `T${tier}: S2 copy`);
for (const c of s2) approx(c.amount, t.base.atk * tb.atk_scale, `T${tier}: S2 copy ×${tb.atk_scale}`);
} else {
assert.deepEqual(t.liveRangeGrid, [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]], `T${tier}: S3 copy 4-1`);
approx(t.s.interval, 1.0, `T${tier}: S3 copy interval`);
approx(t.skill.timeLeft, v.skill.duration, `T${tier}: ${v.skill.duration} s`, 0.05);
h.run(3);
for (const c of from(h, t).filter((x) => x.dmg.isSkill)) approx(c.amount, t.base.atk * tb['attack@atk_scale'], `T${tier}: S3 copy ×${tb['attack@atk_scale']}`);
}
h.run(24);
assert.equal(t.skill.activations, 1, `T${tier} S${skill + 1}: once per deployment`);
done(h);
}
}
});
test('弱点速记: every damage of the typewriter cuts the target\'s DEF 18 % for 4 s, 23 % on one of the 4 tiles next to 鸿雪 (not diagonal); ARC-X stage 3: 28 % / 33 % and the 8 tiles; 鸿雪 down ⇒ the base cut', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const x3 = tier === 6 && elite && mod === X;
const tal = tokOf(tier, elite, 2, elite ? mod : null).talents.find((x) => x && x.bb.duration);
const c1 = -tal.bb['bgsnow_token[def_down]_1.def'], c2 = -tal.bb['bgsnow_token[def_down]_2.def'];
assert.deepEqual([c1, c2, tal.bb.duration], x3 ? [0.28, 0.33, 4] : [0.18, 0.23, 4], label(f));
for (const [tile, near] of [[[11, 5], true], [[11, 4], x3], [[12, 3], false]]) {
const { h, u, t } = field({ tier, elite, mod, skill: 0, tok: tile, seed: 2 });
h.spawn('enemy_dummy', { pos: [tile[0], tile[1] + 2] });
h.run(4);
const cuts = h.hooksOf('statusApplied').filter((c) => c.source === t && c.status === 'defDown');
assert.ok(cuts.length >= 2, `${label(f)} ${tile}: cuts`);
for (const c of cuts) assert.deepEqual([c.value, c.duration], [near ? c2 : c1, 4], `${label(f)} ${tile}: ${near ? 'near' : 'apart'}`);
if (near) {
h.b.kill(u, null);
h.run(3);
const after = h.hooksOf('statusApplied').filter((c) => c.source === t && c.status === 'defDown').slice(cuts.length);
assert.ok(after.length && after.every((c) => c.value === c1), `${label(f)}: 鸿雪 down ⇒ ${c1}`);
}
done(h);
}
}
});
+269
View File
@@ -0,0 +1,269 @@
// test/content/op_shwaz.test.js — the 自选 operator kit of 黑 (char_340_shwaz, 6★ 重射手; kit
// server/sim/content/kits/ops/op-shwaz.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, ARC-X
// “定制弩配件套装” or ARC-Y “老剃刀” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json (the
// form of that slot status); the fidelity checklist of kits/README.md item by item.
// Run: node --test test/content/op_shwaz.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';
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
const CHESS = load('chess');
const BACKUPS = load('backups');
const SHWAZ = 'char_340_shwaz';
const FORMS = BACKUPS.units[SHWAZ].forms;
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const X = 'uniequip_002_shwaz', Y = 'uniequip_003_shwaz';
const S1 = 'skchr_shwaz_1', S2 = 'skchr_shwaz_2', S3 = 'skchr_shwaz_3';
const PROVE = 'chess_char_1_07_a', YAK = 'chess_char_1_02_a';
/** The unit form of a slot (tier, normal / elite). */
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 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' }) };
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, X, Y].map((m) => [t, true, m]))];
/** 破甲箭头 of a form: the base talent, upgraded by ARC-Y's NAMED change only (stage 3: 170 % / −25 %). */
const pierce = (tier, elite, mod) => ({ ...formOf(tier, elite).talents[0].bb, ...(modOf(tier, mod)?.talentChanges ?? []).filter((t) => t.talentIndex === 0 && t.name).reduce((o, t) => ({ ...o, ...t.bb }), {}) });
/** 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, players = null } = {}) {
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'dodge'], captureNoisy: true,
...(players ? { players } : { units: [{ uid: 1, diy: { slot: SLOT[tier], charId: SHWAZ, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others] }),
});
h.step();
return { h, u: h.unit(1) };
}
const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
test('黑 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 3-6, ranged physical, hits air, blocks 1, ground-targetable, data triggers', () => {
assert.equal(OPERATOR_KITS[SHWAZ], KITS[SHWAZ]);
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;
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [SHWAZ, SLOT[tier], form.skills[skill].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`);
// ARC-X “定制弩配件套装”: "再部署时间减少" — the attribute respawn_time −25 (70 → 45 s)
assert.equal(u.base.respawnTime, form.stats.respawnTime + (m?.attr.respawnTime ?? 0), `${label(f)}: redeploy time`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [1, 'ranged', true, 'phys', 1.6], `${label(f)}: 重射手`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-6`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
done(h);
}
}
// the data: all three DEFAULT (S3's 「攻击范围改为」 is an attack-range change, not a 技能范围 — tools/build-data.mjs
// ATTACK_RANGE_CHANGE); E2 Lv1 1393 / 646 / 164, Lv60 1685 / 775 / 204
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'DEFAULT']);
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [646, 775]);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr],
[{ maxHp: 130, atk: 55, respawnTime: -25 }, { maxHp: 170, atk: 75, respawnTime: -25 }, { atk: 80, def: 10 }, { atk: 120, def: 16 }]);
});
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(SHWAZ));
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(SHWAZ), `tier ${t}`);
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: SHWAZ, skillIndex: 2, uniEquipId: Y } }, { data, kitted: KITTED_CHARS }),
{ ok: true, picks: { [SLOT[6]]: { charId: SHWAZ, skillIndex: 2, uniEquipId: Y } } });
});
test('S1 强弩 (AUTO, attack SP 4, data DEFAULT): every 5th attack ×175 % / 190 % ATK, 破甲箭头 then at 60 % / 70 % (20 % otherwise); air units are hit', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1), t0 = pierce(tier, elite, null);
const { h, u } = field({ tier, elite, skill: 0, seed: 21 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.spCost, u.skill.spType, sk.bb.atk_scale, sk.bb['talent@prob']],
['instant', 'DEFAULT', 4, 'attack', elite ? 1.9 : 1.75, elite ? 0.7 : 0.6], `T${tier}`);
h.spawn('enemy_fly', { pos: [10, 6] }); // a flyer in front: she hits air units
h.run(800);
const hits = atkHits(h, u);
const sk1 = hits.filter((c) => c.dmg.isSkill), plain = hits.filter((c) => !c.dmg.isSkill);
assert.ok(Math.abs(sk1.length * 5 - hits.length) <= 5, `T${tier}: one S1 attack in 5 (${sk1.length} / ${hits.length})`);
const base = u.s.atk;
for (const c of sk1) assert.ok([base * sk.bb.atk_scale, base * sk.bb.atk_scale * t0.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: S1 hit ${c.amount}`);
for (const c of plain) assert.ok([base, base * t0.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: plain hit ${c.amount}`);
const rate = (list, scale) => list.filter((c) => c.amount > base * scale + 1e-6).length / list.length;
const r1 = rate(sk1, sk.bb.atk_scale), r0 = rate(plain, 1);
assert.ok(Math.abs(r1 - sk.bb['talent@prob']) < 0.12, `T${tier}: 破甲箭头 on S1 ${r1}`);
assert.ok(Math.abs(r0 - t0.prob) < 0.07, `T${tier}: 破甲箭头 otherwise ${r0}`);
assert.ok(hits.every((c) => c.type === 'phys' && c.target.motion === 'FLY'), `T${tier}: physical, on the flyer`);
done(h);
}
});
test('S2 暮眼锐瞳 (MANUAL, data DEFAULT): ATK +85 % / +100 % for 33 / 36 s, 破甲箭头 at 40 % / 45 % meanwhile; no cast without an enemy', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2), t0 = pierce(tier, elite, null);
const { h, u } = field({ tier, elite, skill: 1, seed: 8 });
assert.deepEqual([u.skill.kind, u.skill.rule, sk.duration, sk.bb.atk, sk.bb['talent@prob']], ['duration', 'DEFAULT', elite ? 36 : 33, elite ? 1 : 0.85, elite ? 0.45 : 0.4], `T${tier}`);
u.skill.gainSp(999);
h.run(2);
assert.equal(u.skill.activations, 0, `T${tier}: no enemy, no cast`);
h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in range`);
approx(u.skill.timeLeft, sk.duration, `T${tier}: ${sk.duration} s`, 0.01);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK +${sk.bb.atk * 100} %`);
const n0 = atkHits(h, u).length;
u.skill.extend(600);
h.runUntil(() => !u.skill.active, 700);
const during = atkHits(h, u).slice(n0);
const atk = u.base.atk * (1 + sk.bb.atk);
assert.ok(during.length > 300 && during.every((c) => c.dmg.isSkill), `T${tier}: ${during.length} attacks, marked`);
const r = during.filter((c) => c.amount > atk + 1e-6).length / during.length;
assert.ok(Math.abs(r - sk.bb['talent@prob']) < 0.07, `T${tier}: 破甲箭头 ${r}`);
for (const c of during.slice(0, 20)) assert.ok([atk, atk * t0.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: ${c.amount}`);
approx(u.s.atk, u.base.atk, `T${tier}: ATK back`);
done(h);
}
});
test('S3 战术的终结: range 3-2 while it runs (and back), interval 1.6 + 0.4 s, ATK +120 % / +140 %, 破甲箭头 on every attack; the basic strategy (DEFAULT): an enemy she is about to attack casts it, then only the 3-2 line is attacked', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3), t0 = pierce(tier, elite, null);
assert.deepEqual(sk.rangeGrid, [[0, 0], [0, 1], [0, 2], [0, 3]]);
const { h, u } = field({ tier, elite, skill: 2, seed: 4 });
assert.deepEqual([u.skill.kind, u.skill.rule, sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.bb['talent@prob']],
['duration', 'DEFAULT', elite ? 21 : 18, elite ? 1.4 : 1.2, 0.4, 1], `T${tier}`);
u.skill.gainSp(999);
const side = h.spawn('enemy_dummy', { pos: [11, 6] }); // in 3-6, not in 3-2
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast as she is about to attack the 3-6 enemy (DEFAULT)`);
const far = h.spawn('enemy_dummy', { pos: [10, 8] }); // 3 tiles ahead: 3-2, outside 3-6
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: range 3-2 while it runs`);
approx(u.s.interval, 2.0, `T${tier}: interval +0.4 s`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
const n0 = atkHits(h, u).length;
h.runUntil(() => !u.skill.active, 30);
const during = atkHits(h, u).slice(n0);
assert.ok(during.length >= 8 && during.every((c) => c.target === far), `T${tier}: only the 3-2 enemy, not the side one (${during.length})`);
for (const c of during) approx(c.amount, u.base.atk * (1 + sk.bb.atk) * t0.atk_scale, `T${tier}: every attack procs`);
const cuts = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'defDown' && c.target === far);
assert.ok(cuts.length >= during.length - 1, `T${tier}: DEF cut each time`);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-6`);
approx(u.s.interval, 1.6, `T${tier}: interval back`);
done(h);
}
});
test('T1 破甲箭头: a proc ×160 % (ARC-Y stage 3: 170 %) and, once it hit, DEF ×0.8 (×0.75) for 5 s (defDown); ARC-Y stage 1 keeps 160 % (its hidden trait part is not 破甲箭头); a dodged attack cuts nothing', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t0 = pierce(tier, elite, mod);
assert.deepEqual([t0.atk_scale, t0.def, t0.prob, t0.defdown_duration], tier === 6 && mod === Y ? [1.7, -0.25, 0.2, 5] : [1.6, -0.2, 0.2, 5], label(f));
const { h, u } = field({ tier, elite, mod, skill: 2, seed: 13 });
const e = h.spawn('enemy_dummy', { pos: [11, 6] }); // off her front line: no ARC-Y bonus
h.run(120);
const hits = atkHits(h, u).filter((c) => c.target === e);
const procs = hits.filter((c) => c.amount > u.s.atk + 1e-6);
assert.ok(procs.length > 5 && procs.length < hits.length * 0.4, `${label(f)}: ${procs.length} / ${hits.length}`);
for (const c of procs) approx(c.amount, u.s.atk * t0.atk_scale, `${label(f)}: ×${t0.atk_scale}`);
const cuts = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'defDown');
assert.equal(cuts.length, procs.length, `${label(f)}: one DEF cut per proc`);
for (const c of cuts) assert.deepEqual([c.value, c.duration], [-t0.def, t0.defdown_duration], label(f));
// a dodged attack: no hit, no cut
const d = h.spawn('enemy_dummy', { pos: [9, 6] });
h.b.addBuff(d, { key: 'test:dodge', mods: { dodgePhys: 1 } });
h.b.kill(e, null);
const c0 = cuts.length;
h.run(60);
assert.ok(h.hooksOf('dodge').filter((c) => c.source === u).length > 20, `${label(f)}: dodged`);
assert.equal(h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'defDown').length, c0, `${label(f)}: nothing cut`);
done(h);
}
});
test('T2 交叉火力: with another 【狙击】 operator on the field every sniper (her too, a partner\'s too) ATK +8 % — not 角峰, not alone, not once she leaves; ARC-X stage 3 "携带": +13 % on her player\'s snipers for the whole battle', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const squad = tier === 6 && mod === X;
const v = squad ? 0.13 : 0.08;
const t1 = { ...formOf(tier, elite).talents[1].bb, ...(modOf(tier, mod)?.talentChanges ?? []).filter((t) => t.talentIndex === 1).reduce((o, t) => ({ ...o, ...t.bb }), {}) };
assert.equal(t1.atk, v, label(f));
// alone: nothing
const solo = field({ tier, elite, mod, skill: 0 });
solo.h.run(1);
assert.equal(solo.u.findBuff('talent:shwaz:crossfire'), null, `${label(f)}: alone`);
done(solo.h);
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 2, chessId: PROVE, row: 12, col: 3 }, { uid: 3, chessId: YAK, row: 9, col: 3 }] });
h.run(1);
const prove = h.unit(2), yak = h.unit(3);
for (const a of [u, prove]) assert.deepEqual(a.findBuff('talent:shwaz:crossfire')?.mods, { atkPct: v }, `${label(f)}: ${a.def.name}`);
assert.equal(yak.findBuff('talent:shwaz:crossfire'), null, `${label(f)}: 角峰 (重装)`);
approx(u.s.atk, u.base.atk * (1 + v), `${label(f)}: her ATK`);
h.b.retreat(prove);
h.run(1);
assert.equal(!!u.findBuff('talent:shwaz:crossfire'), squad, `${label(f)}: 普罗旺斯 off the field (携带: still in the squad)`);
h.b.redeploy(prove);
h.run(1);
h.b.retreat(u);
h.run(1);
assert.equal(!!prove.findBuff('talent:shwaz:crossfire'), squad, `${label(f)}: 黑 off the field`);
done(h);
}
// a partner's sniper on a shared field: the field version counts it and buffs it; the squad version does not
const pair = (mod, tier) => field({ players: [
{ playerId: 'p1', seat: 0, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: tier === 6 ? 'chess_char_6_diy1_b' : 'chess_char_5_diy1_b', diy: { charId: SHWAZ, skillIndex: 0, uniEquipId: mod }, row: 10, col: 5 }], bonds: {}, bandId: null, playerEffects: [] },
{ playerId: 'p2', seat: 1, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: PROVE, row: 12, col: 3 }], bonds: {}, bandId: null, playerEffects: [] },
] });
for (const [mod, tier, want] of [[null, 6, 0.08], [X, 6, null]]) {
const { h } = pair(mod, tier);
h.run(1);
const mine = h.b.allyUnits.find((a) => a.ownerId === 'p1'), theirs = h.b.allyUnits.find((a) => a.ownerId === 'p2');
assert.equal(mine.findBuff('talent:shwaz:crossfire')?.mods.atkPct ?? null, want, `${mod ?? 'none'}: 黑`);
assert.equal(theirs.findBuff('talent:shwaz:crossfire')?.mods.atkPct ?? null, want, `${mod ?? 'none'}: the partner's 普罗旺斯`);
done(h);
}
});
test('ARC-Y 老剃刀 trait: an enemy on her straight line ahead takes ×105 % and cannot dodge (stages 1 and 3); off the line it dodges; ARC-X / no module change nothing; facing UP turns the line', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y = elite && mod === Y;
if (y) assert.deepEqual(formOf(tier, true).modules.find((m) => m.uniEquipId === Y).traitOverride.bb, { atk_scale: 1.05 }, label(f));
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 3 });
u.skill.sp = 0; u.skill.charges = 0;
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
for (const e of [front, side]) h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
h.run(40);
const onFront = atkHits(h, u).filter((c) => c.target === front);
const dodged = h.hooksOf('dodge').filter((c) => c.source === u);
if (y) {
assert.ok(onFront.length > 10, `${label(f)}: the front enemy is hit (${onFront.length})`);
for (const c of onFront.filter((x) => !x.dmg.isSkill)) assert.ok([u.s.atk * 1.05, u.s.atk * 1.05 * pierce(tier, elite, mod).atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `${label(f)}: ×1.05 (${c.amount})`);
h.b.kill(front, null);
h.run(10);
assert.ok(h.hooksOf('dodge').filter((c) => c.source === u && c.target === side).length > 3, `${label(f)}: off the line it dodges`);
} else {
assert.equal(onFront.length, 0, `${label(f)}: no bonus — every attack dodged`);
assert.ok(dodged.length > 10, label(f));
}
done(h);
}
// facing UP: the line is the column above her
const h = makeBattle({ defs: { enemies: ENEMIES }, timeLimit: 60, autoFinish: false, seed: 2, flags: { dpPerSec: 0 }, hooks: ['damaged'], captureNoisy: true,
units: [{ uid: 1, diy: { slot: 6, charId: SHWAZ, skillIndex: 0, uniEquipId: Y }, elite: true, row: 10, col: 5, dir: 'UP' }] });
h.step();
const u = h.unit(1);
const e = h.spawn('enemy_dummy', { pos: [12, 5] });
h.run(20);
const plain = h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && !c.dmg.isSkill);
assert.ok(plain.length > 5);
for (const c of plain) assert.ok([1.05, 1.05 * pierce(6, true, Y).atk_scale].some((k) => Math.abs(c.amount - u.base.atk * k) < 1e-6), `UP: two tiles above her is her line (${c.amount})`);
done(h);
});
+287 -117
View File
@@ -1560,126 +1560,132 @@
"snaps": "08ec0e9b d4419c45 56a2bcb4 9e752221 efa858fd 0b8e0f61 07fd1174 33d5625e 3bcffa82 6bdfc54a 70fb78b8 6b78ec10 bb3a00e7 79fe9836 bfc1fad7 93de838f 3b2411a2 2abcc04e 5ed202f1 f86b9678 44fac894 5dffdeda 38dc3675"
},
"diy-039": {
"about": "6_diy1_b=huang/S1/002_huang 6_diy2_b=huang/S2/002_huang 6_diy1_b=huang/S3/002_huang 6_diy2_b=huang/S1/003_huang 6_diy1_b=huang/S2/003_huang 6_diy2_b=huang/S3/003_huang 5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"about": "6_diy1_b=huang/S1/002_huang 6_diy2_b=huang/S2/002_huang 6_diy1_b=huang/S3/002_huang 6_diy2_b=huang/S1/003_huang 6_diy1_b=huang/S2/003_huang 6_diy2_b=huang/S3/003_huang 5_diy1_a=shwaz/S1 5_diy2_a=shwaz/S2 5_diy1_a=shwaz/S3 5_diy2_b=shwaz/S1 5_diy1_b=shwaz/S2 5_diy2_b=shwaz/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m06",
"modeId": "mode_single_normal",
"round": 13,
"seed": 1549362176,
"waveId": "act1autochess_h06",
"time": 221.33,
"ticks": 6640,
"time": 214.83,
"ticks": 6445,
"reason": "cleared",
"killed": 27,
"killed": 25,
"total": 132,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"rngDraws": 87,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":27,"total":132,"perfect":false,"leaks":105,"leaksUncounted":0,"coins":0,"dmg":272890,"bossDmg":0,"heal":23304,"deaths":22,"layerGains":{}}},
"players": {"p1":{"killed":25,"total":132,"perfect":false,"leaks":107,"leaksUncounted":0,"coins":0,"dmg":295753,"bossDmg":0,"heal":21810,"deaths":21,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_huang",0,0,2,5,52981,4302,1434,24,6],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_huang",0,0,7,0,0,4302,1434,0,1],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_huang",0,0,111,0,0,8604,2868,0,0],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_huang",0,0,86,5,74003,11712,5856,36,9],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_huang",0,0,128,3,25035,11712,5856,33,0],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_huang",0,0,98,6,52825,11712,5856,24,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,979,1,153,1,2665,5405,0,20,0],
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,4,0,4712,2908,0,38,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,155,1,15620,7692,0,22,1],
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,143,0,2653,6668,0,2,1],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,136,2,17302,2542,0,63,1],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,156,4,25093,3020,0,70,1]
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_huang",0,0,134,7,62379,8604,2868,30,6],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_huang",0,0,6,0,0,4302,1434,0,1],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_huang",0,0,107,0,0,8604,2868,0,0],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_huang",0,0,87,5,74810,11712,5856,37,9],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_huang",0,0,7,0,9260,5856,2928,16,0],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_huang",0,0,97,6,51087,11712,5856,24,2],
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,144,0,0,2786,0,0,0],
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,152,1,22019,2786,0,22,1],
["p1",9,"op","chess_char_5_diy1_a",2,null,1393,1,154,2,32606,1393,0,29,2],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,114,1,6128,3370,0,8,1],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,93,3,22259,3370,0,19,1],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,85,0,15205,3370,0,10,1]
],
"enemies": {"enemy_1019_jshoot_2":[57,15,42,6830,79336],"enemy_1381_winman":[27,9,18,2379,101941],"enemy_1389_winbab_2":[48,3,45,70638,91612]},
"hooks": [1,156,6640,1321,1321,1459,1460,16,49,154,23,23,0,15923,9,56,132,105,1,49,0,0,0],
"events": {"atk":1462,"deploy":24,"die":49,"dmg":954,"fx":30,"heal":16,"leak":105,"skill":46,"spawn":144,"status":22},
"fx": {"aoe":13,"buff":1,"heal":16},
"snaps": "2fa44821 69a68226 b2cef126 7d2e5fb3 62dbdb20 72209675 acef606d 006470ea b21fe529 3ab7999a da964e75 1209cd30 9e6da239 ba028ddc 84454051 a38b0cf9 8cea238e e7ce450d ad612988 504a7c00 caaecf97 9d741f38 89306431"
"enemies": {"enemy_1019_jshoot_2":[57,13,44,5282,72855],"enemy_1381_winman":[27,7,20,1770,107737],"enemy_1389_winbab_2":[48,5,43,60081,115162]},
"hooks": [1,154,6445,1018,1018,1137,1138,15,46,153,24,24,0,11532,35,51,132,107,1,46,0,0,0],
"events": {"atk":1148,"deploy":22,"die":46,"dmg":687,"fx":60,"heal":15,"leak":107,"skill":48,"spawn":144,"status":43},
"fx": {"aoe":13,"buff":1,"crit":31,"heal":15},
"snaps": "63799646 193a03bc e02a11dc 263432bc 9f6b3637 9bf602dd c0f31b96 bae8b88d 6ca39ae3 684fe9a9 36891e10 66784112 997d4983 72fdd9e0 4d3f0646 362e0e80 435d205e cc2c1c2f 4c433ad5 66329e55 2c1a2ed6 6204ef02"
},
"diy-040": {
"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",
"about": "5_diy1_b=shwaz/S1/002_shwaz 5_diy2_b=shwaz/S2/002_shwaz 5_diy1_b=shwaz/S3/002_shwaz 5_diy2_b=shwaz/S1/003_shwaz 5_diy1_b=shwaz/S2/003_shwaz 5_diy2_b=shwaz/S3/003_shwaz 6_diy1_a=shwaz/S1 6_diy2_a=shwaz/S2 6_diy1_a=shwaz/S3 6_diy2_b=shwaz/S1 6_diy1_b=shwaz/S2 6_diy2_b=shwaz/S3",
"fast": false,
"kind": "normal",
"stageId": "act1autochess_m07",
"modeId": "mode_multi_abyss",
"round": 12,
"round": 13,
"seed": 4204057621,
"waveId": "act1autochess_h05",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 0,
"total": 60,
"waveId": "act1autochess_h06",
"time": 201.23,
"ticks": 6037,
"reason": "cleared",
"killed": 29,
"total": 124,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 24,
"rngDraws": 614,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":60,"perfect":false,"leaks":60,"leaksUncounted":0,"coins":0,"dmg":228914,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"players": {"p1":{"killed":29,"total":124,"perfect":false,"leaks":95,"leaksUncounted":0,"coins":0,"dmg":596134,"bossDmg":0,"heal":0,"deaths":4,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,82,0,71,7812,0,5,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",675.54,1,84,0,2254,9437,0,52,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,80,0,2618,4587,0,10,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,78,0,4995,5712,0,17,1],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,85,0,1003,5276,0,12,0],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",3411,1,86,0,29363,6822,0,29,1],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,4,0,40162,0,0,86,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,7,0,47763,0,0,87,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,9,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,11,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,13,0,29230,0,0,74,2],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,5,0,71456,0,0,154,4],
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_shwaz",1815,1,5,5,33000,0,0,43,8],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_shwaz",1815,1,7,5,39152,0,0,41,2],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_shwaz",1815,1,9,0,17507,0,0,16,3],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_shwaz",1685,1,11,0,44820,0,0,39,7],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_shwaz",1685,1,13,0,43434,0,0,33,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_shwaz",1685,1,6,0,62610,0,0,64,4],
["p1",7,"op","chess_char_6_diy1_a",0,null,1393,1,8,5,18820,0,0,43,8],
["p1",8,"op","chess_char_6_diy2_a",1,null,1393,1,10,0,125278,0,0,114,3],
["p1",9,"op","chess_char_6_diy1_a",2,null,1393,1,12,0,52933,0,0,72,4],
["p1",10,"op","chess_char_6_diy2_b",0,"none",1685,1,14,0,122754,0,0,106,21],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,83,9,12604,3370,0,28,2],
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,82,5,23220,3370,0,23,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]
],
"enemies": {"enemy_1045_hammer_2":[12,0,12,11679,29920],"enemy_1061_zomshd":[48,0,47,27967,198994]},
"hooks": [1,86,6900,614,614,638,630,18665,15,74,17,15,0,23511,230,32,60,59,1,17,0,0,8],
"events": {"atk":614,"deploy":24,"die":15,"dmg":630,"fx":30,"leak":59,"skill":32,"spawn":74,"status":464},
"fx": {"aoe":4,"dodge":8,"dp":2,"slash":16},
"snaps": "55d29c01 137a0f46 4141bbfd 7089b49a ee191823 68945e1a 43ec812f d17c03a8 01486d26 5b26dde8 ce02b619 845c68e2 379c94bf f6905a41 8bec372a e1f60831 bf9ad198 ed155d25 e54254a4 a2e404f1 e9112874 40dca7a0 65049e51"
"enemies": {
"enemy_1195_sfyin": [39,2,37,400,200384],
"enemy_1196_msfyin": [4,4,0,0,8],
"enemy_1197_sfshu": [48,7,41,1259,333965],
"enemy_1198_msfshu": [21,16,5,0,50],
"enemy_1207_sfji": [12,0,12,5081,61727]
},
"hooks": [1,140,6037,630,630,622,622,0,33,128,66,66,0,21144,179,7,124,95,1,33,0,0,0],
"events": {"atk":630,"deploy":14,"die":33,"dmg":622,"fx":207,"leak":95,"skill":132,"spawn":138,"status":122},
"fx": {"crit":198,"summon":9},
"snaps": "0554c5a8 47183f70 b38ddae7 9aa32de8 397cfcee 0eb2fab3 92870fae a81c8f73 c593f05b 8508b52f 83d476ce bd8afb76 619f5934 576b4968 a66f530c 086d4e37 0e4d5e5e 9cbe3d31 4a93cc40 518ac5ad db0490b7"
},
"diy-041": {
"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",
"about": "6_diy1_b=shwaz/S1/002_shwaz 6_diy2_b=shwaz/S2/002_shwaz 6_diy1_b=shwaz/S3/002_shwaz 6_diy2_b=shwaz/S1/003_shwaz 6_diy1_b=shwaz/S2/003_shwaz 6_diy2_b=shwaz/S3/003_shwaz 5_diy1_a=bgsnow/S1 5_diy2_a=bgsnow/S2 5_diy1_a=bgsnow/S3 5_diy2_b=bgsnow/S1 5_diy1_b=bgsnow/S2 5_diy2_b=bgsnow/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m01",
"modeId": "mode_multi_funny",
"round": 12,
"round": 13,
"seed": 2453815680,
"waveId": "act1autochess_h05",
"time": 186.23,
"ticks": 5587,
"waveId": "act1autochess_h06",
"time": 219.63,
"ticks": 6589,
"reason": "cleared",
"killed": 117,
"total": 117,
"killed": 10,
"total": 78,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 201,
"rngDraws": 398,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":117,"total":117,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":465120,"bossDmg":0,"heal":41772,"deaths":2,"layerGains":{}}},
"players": {"p1":{"killed":10,"total":78,"perfect":false,"leaks":68,"leaksUncounted":0,"coins":0,"dmg":377319,"bossDmg":0,"heal":0,"deaths":19,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,1865,1,6,7,30628,0,0,34,2],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",2374,1,9,36,169772,0,0,133,3],
["p1",3,"op","chess_char_5_diy1_a",2,null,800.84,1,131,0,0,2661,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,7,0,0,0,0,0,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,2,2,31621,0,0,77,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,3,2,52971,0,0,79,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1372,1,4,0,0,0,0,0,0],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,11,64,174591,788,0,75,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,8,0,0,0,21828,57,2],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,12,0,0,40984,19945,54,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,1,0,0,0,0,0,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,5,6,5537,0,0,12,1]
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_shwaz",1855,1,2,1,63517,0,0,118,23],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_shwaz",1855,1,3,1,103035,0,0,118,3],
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_shwaz",1855,1,4,3,109146,0,0,97,4],
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_shwaz",453,1,101,0,16746,4602,0,26,4],
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_shwaz",0,0,99,3,25738,5055,0,24,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_shwaz",0,0,58,1,17991,3370,0,17,1],
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,62,0,336,2738,0,12,0],
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,56,0,5082,2738,0,20,2],
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,78,0,9441,2738,0,33,1],
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,92,0,2039,4968,0,24,0],
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,87,1,8010,3312,0,19,4],
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,5,0,16240,1656,0,28,1]
],
"enemies": {"enemy_1005_yokai":[51,51,0,0,39168],"enemy_1005_yokai_2":[54,54,0,7242,80352],"enemy_1005_yokai_3":[12,12,0,37192,345600]},
"hooks": [1,131,5587,747,747,826,943,181,119,119,18,17,0,25353,30,0,117,0,1,119,0,0,0],
"events": {"atk":791,"deploy":14,"die":119,"dmg":943,"fx":8,"heal":130,"skill":33,"spawn":129,"status":22},
"fx": {"aoe":8},
"snaps": "c9844d80 ed7a1b55 d98f60d1 b7552333 c9d7859c 85c0287b 82bbddbc a6c50613 056aff9b 924dfb3b aef7f0a5 559859eb 7d1f5e69 12531b10 3e40e3a2 42d4c2ca fbd7bf8e cae939db 97bd2db3"
"enemies": {"enemy_1045_hammer_2":[18,0,18,12967,54170],"enemy_1061_zomshd":[60,10,50,18210,323150]},
"hooks": [1,101,6589,610,610,620,620,22217,29,97,44,44,0,12461,157,26,78,68,1,29,0,0,0],
"events": {"atk":610,"deploy":23,"die":29,"dmg":620,"fx":163,"leak":68,"skill":88,"spawn":90,"status":161},
"fx": {"crit":163},
"snaps": "2c84fb54 99234134 07fd8ffd b1f4540d a6c32b3b 5ddfa840 fe8930b7 a622c62a f1623427 a85628cf aeeadcd6 a12ca12c 64a9cad1 bde230bb 5564c1fb a141bebf 15944e00 93033726 131e5452 ef7538c9 3b3a53d1 a577ce3d"
},
"diy-042": {
"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",
"about": "5_diy1_b=bgsnow/S1/002_bgsnow 5_diy2_b=bgsnow/S2/002_bgsnow 5_diy1_b=bgsnow/S3/002_bgsnow 5_diy2_b=bgsnow/S1/003_bgsnow 5_diy1_b=bgsnow/S2/003_bgsnow 5_diy2_b=bgsnow/S3/003_bgsnow 6_diy1_a=bgsnow/S1 6_diy2_a=bgsnow/S2 6_diy1_a=bgsnow/S3 6_diy2_b=bgsnow/S1 6_diy1_b=bgsnow/S2 6_diy2_b=bgsnow/S3",
"fast": false,
"kind": "normal",
"stageId": "act2autochess_m02",
@@ -1690,70 +1696,234 @@
"time": 204.5,
"ticks": 6135,
"reason": "cleared",
"killed": 21,
"killed": 15,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 203,
"rngDraws": 141,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":21,"total":105,"perfect":false,"leaks":84,"leaksUncounted":0,"coins":0,"dmg":387639,"bossDmg":0,"heal":74750,"deaths":14,"layerGains":{}}},
"players": {"p1":{"killed":15,"total":105,"perfect":false,"leaks":90,"leaksUncounted":0,"coins":0,"dmg":305955,"bossDmg":0,"heal":0,"deaths":12,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,123,0,42,7812,0,3,0],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,122,0,129,10293,0,8,0],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,120,4,12864,5267,0,13,1],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,119,6,61348,7713,0,16,3],
["p1",5,"op","chess_char_6_diy1_a",2,null,2638,1,110,5,46735,24288,0,63,4],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",3531,1,116,4,107368,53950,0,65,4],
["p1",7,"op","chess_char_6_diy1_a",2,null,1272,1,8,0,1305,0,0,12,1],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",1643,1,9,1,22245,0,0,112,2],
["p1",9,"op","chess_char_6_diy1_a",2,null,1372,1,13,0,5721,0,0,10,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",1819,1,4,1,129881,0,0,75,2],
["p1",11,"op","chess_char_6_diy1_a",2,null,1224,1,6,0,0,0,34513,43,2],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",1464,1,10,0,0,0,40237,43,2],
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_bgsnow",1786,1,6,1,27528,0,0,51,1],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_bgsnow",1786,1,7,2,52188,0,0,67,17],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_bgsnow",1786,1,13,0,16994,0,0,38,2],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_bgsnow",1656,1,4,3,49048,0,0,103,1],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_bgsnow",1656,1,5,5,62091,0,0,115,20],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_bgsnow",1656,1,8,0,51560,0,0,126,3],
["p1",7,"op","chess_char_6_diy1_a",0,null,1369,1,14,1,12790,0,0,66,1],
["p1",8,"op","chess_char_6_diy2_a",1,null,689.66,1,117,0,1428,3417,0,11,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,113,0,912,4107,0,11,0],
["p1",10,"op","chess_char_6_diy2_b",0,"none",1656,1,122,0,1138,3312,0,12,0],
["p1",11,"op","chess_char_6_diy1_b",1,"none",1656,1,129,3,25943,3312,0,22,7],
["p1",12,"op","chess_char_6_diy2_b",2,"none",0,0,119,0,4336,4968,0,10,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]
],
"enemies": {"enemy_1165_duhond":[51,14,37,1617,98010],"enemy_1174_duholy_2":[24,0,24,11049,80574],"enemy_1175_dushdo_2":[30,7,23,96657,209055]},
"hooks": [1,129,6135,667,667,924,921,170,35,119,22,21,0,26294,0,48,105,84,1,54,0,0,3],
"events": {"atk":735,"deploy":22,"die":35,"dmg":921,"fx":63,"heal":165,"leak":84,"skill":43,"spawn":119,"status":213},
"fx": {"aoe":6,"dodge":3,"dp":14,"slash":27,"spGain":13},
"snaps": "3cd53cb0 c89b2368 40b28879 59bff952 5a64be7e 30701bd3 7477ea7e 2456da9c 8bf2c8d0 952f99f8 203f6c6a 8dec7426 387a1424 cfbf0fa2 0fbe2619 a8d8c27b 465f68b8 38c87469 23c13bb1 0f2fc6c7 dc5e3ec1"
"enemies": {"enemy_1165_duhond":[51,12,39,176,67202],"enemy_1174_duholy_2":[24,0,24,6171,156902],"enemy_1175_dushdo_2":[30,3,27,12769,81850]},
"hooks": [1,129,6135,666,666,750,750,0,27,117,54,51,0,22377,0,16,105,90,1,27,0,0,0],
"events": {"atk":666,"deploy":22,"die":27,"dmg":750,"fx":36,"leak":90,"skill":105,"spawn":119,"status":195},
"fx": {"crit":36},
"snaps": "20b7fc37 08678156 135cc57c 89dbd854 b8ecd492 a19c7df4 24b1162b 55c5c742 28fd3c49 bfc80994 5c821609 77fc27bc 3f2ff411 6b8a048a 187c1e4b 9c39986d 0fa15a34 4ad95b9b 09abaf91 eb92a8c4 b64e626c"
},
"diy-043": {
"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,
"about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 6_diy1_b=bgsnow/S1/002_bgsnow 6_diy2_b=bgsnow/S2/002_bgsnow 6_diy1_b=bgsnow/S3/002_bgsnow 6_diy2_b=bgsnow/S1/003_bgsnow 6_diy1_b=bgsnow/S2/003_bgsnow 6_diy2_b=bgsnow/S3/003_bgsnow 5_diy1_a=csnipe 5_diy2_b=csnipe",
"fast": true,
"kind": "normal",
"stageId": "act2autochess_m03",
"modeId": "mode_multi_normal",
"round": 13,
"seed": 1440042731,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 5,
"time": 215.17,
"ticks": 6455,
"reason": "cleared",
"killed": 6,
"total": 117,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 269,
"rngDraws": 194,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":5,"total":117,"perfect":false,"leaks":112,"leaksUncounted":0,"coins":0,"dmg":238841,"bossDmg":0,"heal":0,"deaths":20,"layerGains":{}}},
"players": {"p1":{"killed":6,"total":117,"perfect":false,"leaks":111,"leaksUncounted":0,"coins":0,"dmg":700578,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,142,2,38305,5595,0,40,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",160.54,1,144,3,80636,7321,0,49,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,141,0,16517,9232,0,98,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",1484,1,143,0,42548,10388,0,80,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,6,0,23478,0,0,157,3],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,7,0,37358,0,0,171,3],
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,143,0,620,6384,0,33,1],
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,119,0,8344,5816,0,64,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,135,0,8794,7692,0,27,0],
["p1",4,"op","chess_char_5_diy2_b",2,"none",0,0,139,1,12488,10002,0,31,1],
["p1",5,"op","chess_char_6_diy1_b",0,"uniequip_002_bgsnow",1826,1,9,0,122692,0,0,122,1],
["p1",6,"op","chess_char_6_diy2_b",1,"uniequip_002_bgsnow",1826,1,11,1,130874,0,0,138,20],
["p1",7,"op","chess_char_6_diy1_b",2,"uniequip_002_bgsnow",1826,1,13,0,57438,0,0,74,3],
["p1",8,"op","chess_char_6_diy2_b",0,"uniequip_003_bgsnow",1656,1,15,0,129749,0,0,122,1],
["p1",9,"op","chess_char_6_diy1_b",1,"uniequip_003_bgsnow",1656,1,4,3,123659,0,0,136,21],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_003_bgsnow",1656,1,10,1,103257,0,0,148,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,1271,1,130,0,1630,2542,0,22,0],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1510,1,126,0,1035,3020,0,16,0],
["-","~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_1166_dusbr_2":[39,3,36,7541,82135],"enemy_1169_duphlx_2":[57,2,54,10159,100700],"enemy_1172_dugago":[21,0,20,14836,56006]},
"hooks": [1,144,6900,714,714,887,957,0,25,135,8,21,0,11777,11673,31,117,110,1,25,0,0,0],
"events": {"atk":755,"deploy":24,"die":25,"dmg":899,"fx":33,"leak":110,"skill":14,"spawn":126,"status":1124},
"fx": {"displace":24,"dodge":9},
"snaps": "9bbd23cd 2d4db0d2 0b708c5d e12c3e68 e2172814 260b1a9b 80c15326 e5bfb4ae ebfb431f aa6ad13b 090dd69e 08cd9a4a 90873d2d 538a19f4 91a7f5a8 94fe8900 215e97c9 c6bde6b7 d23df7d4 babbeb8e 2e8c6850 80a40788 51003e6c"
"enemies": {"enemy_1166_dusbr_2":[39,5,34,8549,238889],"enemy_1169_duphlx_2":[57,1,56,14552,262496],"enemy_1172_dugago":[21,0,21,12355,199192]},
"hooks": [1,143,6455,1126,1126,1207,1207,0,21,132,52,50,0,30589,0,27,117,111,1,21,0,0,0],
"events": {"atk":1126,"deploy":23,"die":21,"dmg":1207,"fx":56,"leak":111,"skill":102,"spawn":132,"status":117},
"fx": {"crit":56},
"snaps": "16b2f305 eafa3ff3 2a1c8165 ce308737 9eeb163c 87863b4e 5bb3a0af 894ca696 0f98dafb bc8b6e4c e73802d2 2a71c651 37a948e1 2e3a11c8 10d22fe5 1c634514 db620b5b 9bc32a6a 22c16473 34cae7d7 0f1a1234 6153da56"
},
"diy-044": {
"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": "act2autochess_m04",
"modeId": "mode_multi_hard",
"round": 12,
"seed": 4094229119,
"waveId": "act1autochess_h05",
"time": 221.97,
"ticks": 6659,
"reason": "cleared",
"killed": 3,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 31,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":3,"total":102,"perfect":false,"leaks":99,"leaksUncounted":0,"coins":0,"dmg":192904,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,123,0,1971,7812,0,20,0],
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",3371,1,127,0,7190,6742,0,35,1],
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,125,0,16252,4587,0,13,1],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,124,0,20294,5712,0,11,2],
["p1",5,"op","chess_char_5_diy1_a",2,null,2638,1,126,2,18375,5276,0,20,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,87,1,58519,6822,0,47,2],
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,3,0,7285,0,0,52,3],
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,4,0,27011,0,0,164,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,13,0,0,0,0,0,0],
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,14,0,0,0,0,0,0],
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,5,0,15405,0,0,132,3],
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,11,0,20602,0,0,148,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_1166_dusbr_2":[45,3,42,12104,88284],"enemy_1170_dushld_2":[27,0,27,14317,15722],"enemy_1174_duholy_2":[30,0,30,10530,116257]},
"hooks": [1,127,6659,779,779,1569,1558,0,18,117,20,19,0,23824,278,35,102,99,1,22,0,0,11],
"events": {"atk":779,"deploy":23,"die":18,"dmg":1558,"fx":52,"leak":99,"skill":39,"spawn":116,"status":532},
"fx": {"aoe":14,"dodge":11,"dp":3,"slash":24},
"snaps": "30cbe1b0 05bcd998 fbb42f1f 6da22ddd 354379d2 708b0c83 454a4a54 8e01e4f7 67033904 4f9606c5 e038a3b1 5e41e6cd 5cbebe3c 58f3a062 143d8571 d0f375cb b2ce9936 615804ca 181c3efa 947f6da4 ae4a580a 18f00b15 c2c220ef"
},
"diy-045": {
"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_m01",
"modeId": "mode_single_normal",
"round": 12,
"seed": 426256309,
"waveId": "act1autochess_h05",
"time": 211.8,
"ticks": 6354,
"reason": "cleared",
"killed": 79,
"total": 114,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 551,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":79,"total":114,"perfect":false,"leaks":35,"leaksUncounted":0,"coins":0,"dmg":667046,"bossDmg":0,"heal":56212,"deaths":22,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,95,6,46896,3730,0,55,2],
["p1",2,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,92,16,112690,4748,0,57,2],
["p1",3,"op","chess_char_5_diy1_a",2,null,1154,1,146,6,25757,8380,0,71,0],
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",876.42,1,141,11,49351,7812,0,116,0],
["p1",5,"op","chess_char_5_diy1_a",2,null,1272,1,4,5,50224,0,0,126,2],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",1587,1,5,10,72378,0,0,143,3],
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,11,13,93765,8448,0,61,2],
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",1749,1,13,11,193691,4022,0,80,3],
["p1",9,"op","chess_char_5_diy1_a",2,null,1224,1,6,0,0,8270,28921,60,3],
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,14,0,0,33436,27292,59,3],
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,129,0,2407,3480,0,8,0],
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",0,0,90,1,19885,5965,0,35,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_1019_jshoot_2":[51,39,12,64550,167399],"enemy_1299_ymkilr_2":[18,7,11,21876,152985],"enemy_1422_lrsldr_2":[45,33,12,1866,346662]},
"hooks": [1,146,6354,1427,1427,1690,1753,214,101,136,22,31,0,25801,317,77,114,35,1,101,0,0,0],
"events": {"atk":1533,"deploy":29,"die":101,"dmg":1744,"fx":27,"heal":167,"leak":35,"skill":41,"spawn":129,"status":132},
"fx": {"aoe":15,"displace":5,"dodge":4,"spGain":3},
"snaps": "13f5f790 ae100a1a ac170e75 549900c8 7eb0a835 6f629aae 12139fb3 6a9fdaa0 cb8b3c58 b1ccd449 1dead90f 6213ba54 943b6234 debe5fb6 2c8e086b 523ea4e6 24a3d7e7 9aaa0b94 f1c67fcf b71b3e8a a988a786 4d3be5fd"
},
"diy-046": {
"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_m02",
"modeId": "mode_multi_abyss",
"round": 13,
"seed": 3081100192,
"waveId": "act1autochess_h06",
"time": 216.3,
"ticks": 6489,
"reason": "cleared",
"killed": 4,
"total": 171,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 77,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":4,"total":171,"perfect":false,"leaks":167,"leaksUncounted":0,"coins":0,"dmg":120617,"bossDmg":0,"heal":20221,"deaths":23,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,3,0,1544,2604,0,8,0],
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,1,0,1214,3431,0,3,1],
["p1",3,"op","chess_char_6_diy1_a",2,null,133.31,1,195,1,22421,4454,0,16,2],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1904,1,196,2,37478,4938,0,19,2],
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,10,0,10366,6707,0,10,1],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,11,1,22693,11669,0,13,1],
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,190,0,2299,4595,0,6,0],
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,180,0,4791,5469,0,11,0],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,116,0,17234,2744,0,6,1],
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,111,0,578,6634,0,1,0],
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,172,0,0,5772,2100,5,0],
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,160,0,0,3397,18121,12,1]
],
"enemies": {"enemy_1009_lurker":[66,2,64,3709,43347],"enemy_1019_jshoot_2":[57,2,55,27981,61793],"enemy_1404_msnip":[48,0,48,30723,15477]},
"hooks": [1,196,6489,256,256,357,333,29,27,194,9,9,0,11531,30,30,171,167,1,34,0,0,24],
"events": {"atk":264,"deploy":25,"die":27,"dmg":333,"fx":62,"heal":29,"leak":167,"skill":18,"spawn":183,"status":48},
"fx": {"aoe":9,"dodge":24,"dp":7,"slash":18,"spGain":4},
"snaps": "f6bb9115 32785fce 7671d335 6ad496ca 51e64b0d b0287abb c3e8e27b 23bf00a1 17a804a0 22dde806 36b68342 0932e99f d42af37c ec3a5482 1815af63 40c22b65 f5cfdd7c 207a1f75 e0042953 a56964f2 27c42850 e92e1f19"
},
"diy-047": {
"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_m03",
"modeId": "mode_multi_funny",
"round": 13,
"seed": 3707706975,
"waveId": "act1autochess_h06",
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 27,
"total": 129,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 278,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":27,"total":129,"perfect":false,"leaks":102,"leaksUncounted":0,"coins":0,"dmg":245743,"bossDmg":0,"heal":0,"deaths":16,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,1865,1,152,3,10159,3730,0,51,1],
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,111,13,115470,4988,0,75,2],
["p1",3,"op","chess_char_6_diy1_a",2,null,30,1,151,6,25325,8048,0,120,0],
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,150,5,29008,8904,0,131,0],
["p1",5,"op","chess_char_6_diy1_a",2,null,1160,1,6,0,24610,0,0,77,2],
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",1450,1,9,0,41171,0,0,110,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_10124_uashld_2":[18,0,18,6585,52204],"enemy_1043_zomsbr":[66,19,47,384,72934],"enemy_1061_zomshd":[45,8,36,18701,120605]},
"hooks": [1,152,6900,733,733,951,1065,14528,43,144,7,19,0,11946,5594,47,129,101,1,43,0,0,0],
"events": {"atk":802,"deploy":20,"die":43,"dmg":1008,"fx":22,"leak":101,"skill":14,"spawn":138,"status":695},
"fx": {"displace":18,"dodge":4},
"snaps": "e0e91a81 0a9ea352 26f09f4f 99eb0a4b 8b68119f 5c2640c9 3c4cd73f 0225f4b1 58a50a40 b28249ff 4c4af077 845ea84d 52b7882c 796342c8 e75aa812 ae38af25 adcc2100 66fe32f4 5cf3bc1f d66728f6 5b750bf1 c9f87d9a 96e966b3"
}
}
}