feedback5 O12: 自选 operator kits W (with her 此面向敌), 菲亚梅塔 and 维什戴尔 (with her 魂灵之影) — every skill, talent, trait and module (stage 1 / 3) at every form
- kits/ops/op-cqbw.js (W, 炮手; ART-X / ART-Y), op-phenxi.js (菲亚梅塔, 炮手; ART-Y / ART-X / ISW-A attributes only), op-wisdel.js (维什戴尔, 投掷手; BOM-X), appended to OPERATOR_KIT_FILES; numbers from data/backups.json, mechanics from PRTS (备注) and the client's skill prefabs / buff templates. The 炮手 kits splash 1.0 (PRTS 溅射半径一览; the aoesniper profile keeps 1.1): docs/SIM.md §8 says so. - W: 红桃K / D12 cast like an attack (that attack throws the grenade / bombs), 惊吓盒子 plants 此面向敌 by PRTS's tile priority and is refused with no free deployable tile (activate guard); the mine arms within 1.35, blows 1.5 s later with W's ATK read at arming, ground only; D12 is 无来源 with the cast's cached ATK, early on a fallen target; 设伏 / ART-Y ramp, 落井下石 (any physical damage on a stunned enemy of her range, strongest of two W), ART-X SP on her projectiles' / mines' knock-outs. - 菲亚梅塔: 陈述苦难 流失 every 0.1 s and 精力充沛 (ART-X stage 3 floor 50.3 %), 宣告终局 outside / inside a skill, S1 per the 2026-08-01 client (PRTS 备注: ground targets only, the splash still hits air, no range growth — one constant switches to the skill text), S2 灼痕弹 and its 灼痕, S3 持续攻击 shells; ISW-A: base talents. - 维什戴尔: the aftershocks are the kit's (好礼 ×main, 残影 and its 15 % / 100 % explosion), S1 / S2 过载 (the second half, 4 random rounds) / S3 (air targets and splash, 2.5), 魂灵之影 (talent and S3, cap 3, 迷彩 on its x-5, 禁疗, its skill on her range, unmarked first, 0–2 SP back). - Tests: test/content/op_cqbw.test.js (10), op_phenxi.test.js (8), op_wisdel.test.js (8). - Golden: only diy moved — diy-049 … diy-054 now field the three new kits in every form × module × skill (W: 049 / 050, 菲亚梅塔: 051 – 053, 维什戴尔: 053 / 054), the prototypes moved to diy-055 … diy-058 (same packing, new ids); roster, bonds, fields, matches and standins unchanged.
This commit is contained in:
1 parent
fef4ea3d4b
commit
fc18242be1
9 files changed
+2407
-104
No files matched your search
@@ -0,0 +1,440 @@
|
||||
// test/content/op_cqbw.test.js — the 自选 operator kit of W (char_113_cqbw, 6★ 炮手; kit
|
||||
// server/sim/content/kits/ops/op-cqbw.js) and of her summon “此面向敌” (token_10008_cqbw_box), 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, ART-X “佣兵的行囊” or ART-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.
|
||||
// Run: node --test test/content/op_cqbw.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 W = 'char_113_cqbw';
|
||||
const MINE = 'token_10008_cqbw_box';
|
||||
const FORMS = BACKUPS.units[W].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const X = 'uniequip_002_cqbw', Y = 'uniequip_003_cqbw';
|
||||
const S1 = 'skchr_cqbw_1', S2 = 'skchr_cqbw_2', S3 = 'skchr_cqbw_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 mine's variant of a form with the pick's skill / module (getDiyToken). */
|
||||
const mineOf = (tier, elite, mod) => {
|
||||
let v = BACKUPS.tokens[MINE].variants[`${W}@${statusOf(tier, elite)}`];
|
||||
v = { ...v, ...v.bySkill['1'] };
|
||||
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' }), enemy_def: dummy('enemy_def', { def: 300 }),
|
||||
enemy_weak: dummy('enemy_weak', { hp: 100 }),
|
||||
};
|
||||
for (const hp of [2e8, 3e8, 4e8, 5e8]) ENEMIES[`enemy_hp${hp / 1e8}`] = dummy(`enemy_hp${hp / 1e8}`, { hp });
|
||||
/** 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]))];
|
||||
/** Flat-stage rows with W's 3-10 at (10, 4) left without a free deployable tile, or with only (9, 5) and (11, 8). */
|
||||
const NO_TILE = { 9: '##ErfffffrSrrrrrrrS##', 10: '##hrrffffrfrrrrrrrf##', 11: '##hrfffffrfrrrrrrrf##' };
|
||||
const TWO_TILES = { 9: '##ErfrfffrSrrrrrrrS##', 10: '##hrrffffrfrrrrrrrf##', 11: '##hrffffrrfrrrrrrrf##' };
|
||||
|
||||
/** A battle with W as uid 1 at (row, col) facing RIGHT (her 3-10: the 3 rows around her, her column and the 4 ahead). */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, others = [], seed = 5, rows = null } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed, flat: rows ? { rows } : {},
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'spGain', 'dodge'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: W, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src);
|
||||
const on = (h, e) => h.hooksOf('damaged').filter((c) => c.target === e);
|
||||
const minesOf = (h, u) => h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === MINE && t.ownerUnit === u);
|
||||
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('W in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-10, ranged physical 炮手 (splash 1.0), hits air, blocks 1, ground-targetable, the data triggers; offered as a pick', () => {
|
||||
assert.equal(OPERATOR_KITS[W], KITS[W]);
|
||||
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], [W, 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`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat, u.profile.splashRadius, u.profile.sub],
|
||||
[1, 'ranged', true, 'phys', 2.8, 1, 'aoesniper'], `${label(f)}: 炮手`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-10`);
|
||||
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.skillType, s.trigger.rule]), [['MANUAL', 'DEFAULT'], ['AUTO', 'DEFAULT'], ['MANUAL', 'DEFAULT']]);
|
||||
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], [1284, 711, 1497, 844]);
|
||||
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr], [{ maxHp: 130, atk: 54 }, { maxHp: 210, atk: 78 }, { maxHp: 160, atk: 45 }, { maxHp: 200, atk: 85 }]);
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(W) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(W)));
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: W, skillIndex: 1, uniEquipId: X } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: W, skillIndex: 1, uniEquipId: X } } });
|
||||
});
|
||||
|
||||
test('trait 炮手: every enemy within 1.0 of the struck one takes the hit in full (PRTS 溅射半径一览 1.0, not the profile\'s 1.1), air units too', () => {
|
||||
const { h, u } = field({ skill: 2 });
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const near = h.spawn('enemy_dummy', { pos: [11, 8.9] }); // 1.35 away, outside her range (col 9)
|
||||
const one = h.spawn('enemy_fly', { pos: [9, 8.6] }); // √(1 + 0.36) ≈ 1.166: outside 1.0 as well
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 9] }); // 1.0 away
|
||||
const edge = h.spawn('enemy_dummy', { pos: [10, 9.08] }); // 1.08: inside 1.1, outside 1.0
|
||||
assert.ok(h.runUntil(() => from(h, u).some((c) => c.target === main), 6), 'she attacks the one enemy of her range');
|
||||
h.run(0.5);
|
||||
const id = from(h, u).find((c) => c.target === main).dmg.attackId;
|
||||
const hit = from(h, u).filter((c) => c.dmg.attackId === id);
|
||||
assert.deepEqual(new Set(hit.map((c) => c.target)), new Set([main, fly]), 'the struck one and the flyer 1.0 away');
|
||||
for (const c of hit) approx(c.amount, u.s.atk, 'full damage');
|
||||
assert.deepEqual([near, one, edge].map((e) => on(h, e).length), [0, 0, 0], 'beyond 1.0 untouched (1.08 too)');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S1 红桃K (MANUAL, data DEFAULT, 22 / 19 SP): cast like an attack — that attack is the grenade: 270 % / 310 % ATK physical to every enemy within 1.2 (air too), each stunned 1.8 / 2.1 s; no extra hit; plain attacks after', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.spCost, sk.initSp, sk.spType, sk.bb.atk_scale, sk.bb.stun], elite ? [19, 0, 'INCREASE_WITH_TIME', 3.1, 2.1] : [22, 0, 'INCREASE_WITH_TIME', 2.7, 1.8], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 0 });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule], ['instant', 'DEFAULT'], `T${tier}`);
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 9.15] }); // 1.15: inside 1.2, outside her 1.0
|
||||
const b = h.spawn('enemy_dummy', { pos: [10, 9.3] }); // 1.3: outside 1.2
|
||||
const f = h.spawn('enemy_fly', { pos: [9, 8.55] }); // √(1 + 0.3025) ≈ 1.141, outside her range (col 9)
|
||||
assert.ok(h.runUntil(() => from(h, u).length > 0, 6), `T${tier}: a plain attack first`);
|
||||
assert.deepEqual(from(h, u).map((c) => c.target), [main], `T${tier}: her 1.0 splash reaches none of them`);
|
||||
u.skill.gainSp(999);
|
||||
const n0 = from(h, u).length;
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `T${tier}: cast on her next attack`);
|
||||
h.run(0.6);
|
||||
const shot = from(h, u).slice(n0);
|
||||
const id = shot[0].dmg.attackId;
|
||||
assert.ok(shot.every((c) => c.dmg.attackId === id), `T${tier}: one attack — the grenade, nothing else`);
|
||||
assert.deepEqual(new Set(shot.map((c) => c.target)), new Set([main, a, f]), `T${tier}: everything within 1.2`);
|
||||
for (const c of shot) {
|
||||
approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} %`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true], `T${tier}: physical skill damage`);
|
||||
}
|
||||
assert.equal(on(h, b).length, 0, `T${tier}: 1.3 away untouched`);
|
||||
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun');
|
||||
assert.deepEqual(new Set(stuns.map((c) => c.target)), new Set([main, a, f]), `T${tier}: each victim stunned`);
|
||||
for (const c of stuns) approx(c.duration, sk.bb.stun, `T${tier}: ${sk.bb.stun} s`);
|
||||
const n1 = from(h, u).length;
|
||||
h.run(6);
|
||||
const later = from(h, u).slice(n1);
|
||||
assert.ok(later.length >= 2 && later.every((c) => !c.dmg.isSkill && Math.abs(c.amount - u.s.atk) < 1e-6), `T${tier}: plain attacks again`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 惊吓盒子 (AUTO, data DEFAULT, 11 / 10 SP): the next attack plants a 此面向敌 under the targetable ground enemy instead of hitting; 1.5 s after an enemy comes within 1.35, W\'s ATK × 220 % / 250 % to every ground enemy within 1.35 + stun 1.6 / 1.8 s; then it is gone', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2), mv = mineOf(tier, elite, null);
|
||||
assert.deepEqual([sk.skillType, sk.spCost, sk.initSp, sk.bb.atk_scale, sk.bb.stun, sk.overrideTokenKey], elite ? ['AUTO', 10, 0, 2.5, 1.8, MINE] : ['AUTO', 11, 0, 2.2, 1.6, MINE], `T${tier}`);
|
||||
assert.deepEqual([mv.skill.bb.atk_scale, mv.skill.bb.stun, mv.talents[0].bb.duration], [sk.bb.atk_scale, sk.bb.stun, 120], `T${tier}: the token carries the form's S2 blackboard`);
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule], ['instant', 'DEFAULT'], `T${tier}`);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
const g = h.spawn('enemy_dummy', { pos: [11, 7] }); // 1.0 from the mine
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 8] }); // 1.0 from the mine, an air unit
|
||||
const far = h.spawn('enemy_dummy', { pos: [9, 8.3] }); // √(1 + 1.69) ≈ 1.64
|
||||
u.skill.gainSp(999);
|
||||
const n0 = from(h, u).length;
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `T${tier}: cast`);
|
||||
assert.equal(from(h, u).length, n0, `T${tier}: that attack made no hit`);
|
||||
const [m] = minesOf(h, u);
|
||||
assert.ok(m && m.alive, `T${tier}: a mine`);
|
||||
assert.deepEqual([m.tileR, m.tileC], [10, 7], `T${tier}: under the targetable ground enemy (of the four, by her order)`);
|
||||
assert.deepEqual([!!m.s.flags.untargetable, m.profile.noAttack, m.s.blockCnt, m.base.maxHp], [true, true, 0, mv.stats.maxHp], `T${tier}: untargetable trap, no attack, blocks nothing`);
|
||||
const atk = u.s.atk;
|
||||
const t0 = h.b.time;
|
||||
assert.ok(h.runUntil(() => !m.alive, 3), `T${tier}: goes off`);
|
||||
assert.ok(Math.abs(h.b.time - t0 - 1.5) < 0.07, `T${tier}: 1.5 s after it was armed (at once: an enemy on it) — ${h.b.time - t0}`);
|
||||
const blast = from(h, m);
|
||||
assert.deepEqual(new Set(blast.map((c) => c.target)), new Set([e, g]), `T${tier}: the ground enemies within 1.35`);
|
||||
for (const c of blast) {
|
||||
approx(c.amount, atk * sk.bb.atk_scale, `T${tier}: W's ATK × ${sk.bb.atk_scale}`);
|
||||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true], `T${tier}: physical`);
|
||||
}
|
||||
assert.deepEqual([on(h, fly).filter((c) => c.source === m).length, on(h, far).filter((c) => c.source === m).length], [0, 0], `T${tier}: not the flyer, not 1.64 away`);
|
||||
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === m && c.status === 'stun');
|
||||
assert.deepEqual(new Set(stuns.map((c) => c.target)), new Set([e, g]), `T${tier}: stunned`);
|
||||
for (const c of stuns) approx(c.duration, sk.bb.stun, `T${tier}: ${sk.bb.stun} s`);
|
||||
assert.equal(h.hooksOf('death').find((c) => c.unit === m)?.reason, 'expired', `T${tier}: used up, no knock-out`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 惊吓盒子: no free deployable tile in her range ⇒ the skill does not fire (SP kept, she attacks); else the tile nearest to the enemy; flyers neither pick the tile nor arm it; 120 s life; her retreat withdraws every mine', () => {
|
||||
const blocked = field({ skill: 1, rows: NO_TILE });
|
||||
blocked.h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
blocked.u.skill.gainSp(999);
|
||||
blocked.h.run(10);
|
||||
assert.deepEqual([blocked.u.skill.activations, blocked.u.skill.charges, minesOf(blocked.h, blocked.u).length], [0, 1, 0], 'refused, SP kept');
|
||||
assert.ok(from(blocked.h, blocked.u).length >= 3, 'her attacks go on');
|
||||
done(blocked.h);
|
||||
|
||||
const two = field({ skill: 1, rows: TWO_TILES });
|
||||
two.h.spawn('enemy_dummy', { pos: [10, 8] }); // on floor: 1.0 from (11, 8), √(1 + 9) from (9, 5)
|
||||
two.u.skill.gainSp(999);
|
||||
assert.ok(two.h.runUntil(() => two.u.skill.activations === 1, 4));
|
||||
assert.deepEqual(minesOf(two.h, two.u).map((m) => [m.tileR, m.tileC]), [[11, 8]], 'the free tile nearest to the enemy');
|
||||
done(two.h);
|
||||
|
||||
const { h, u } = field({ skill: 1 });
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 7] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), 'a flyer in range: cast');
|
||||
const [m] = minesOf(h, u);
|
||||
assert.ok(m && m.alive, 'a mine on a free tile of her range (drawn: no ground enemy to place it by)');
|
||||
h.run(30);
|
||||
assert.ok(m.alive && !m.mem.armed && on(h, fly).every((c) => c.source !== m), 'the flyer never arms it');
|
||||
assert.ok(h.runUntil(() => !m.alive, 100), 'gone at last');
|
||||
approx(m.deathAt - m.deployedAt, 120, 'after 120 s', 0.01);
|
||||
assert.equal(h.hooksOf('death').find((c) => c.unit === m)?.reason, 'expired');
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => minesOf(h, u).filter((t) => t.alive).length >= 1, 8), 'another one');
|
||||
h.b.retreat(u);
|
||||
h.step();
|
||||
assert.equal(minesOf(h, u).filter((t) => t.alive).length, 0, 'W撤退时撤退全部此面向敌');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('S3 D12 (MANUAL, data DEFAULT, 42 / 39 SP from 14 / 17): bombs on the 3 enemies of her range with the most HP (air too), cached ATK × 250 % / 280 % 无来源 physical within 1.2 of each 3 s after it lands + stun 3.5 / 4 s; early when its target falls; no hit of its own', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.spCost, sk.initSp, sk.bb.max_target, sk.bb.atk_scale, sk.bb.stun], elite ? [39, 17, 3, 2.8, 4] : [42, 14, 3, 2.5, 3.5], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 2 });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule, Math.floor(u.skill.sp)], ['instant', 'DEFAULT', sk.initSp], `T${tier}: from ${sk.initSp} SP`);
|
||||
const lo = h.spawn('enemy_hp2', { pos: [9, 5] });
|
||||
const e3 = h.spawn('enemy_hp3', { pos: [11, 5] });
|
||||
const e4 = h.spawn('enemy_hp4', { pos: [10, 8] });
|
||||
const f5 = h.spawn('enemy_hp5', { pos: [9, 8] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [11, 8] }); // 1e9 HP: the most of all
|
||||
const side = h.spawn('enemy_dummy', { pos: [10, 9.15] }); // outside her range, 1.15 from e4
|
||||
u.skill.gainSp(999);
|
||||
const n0 = from(h, u).length;
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `T${tier}: cast`);
|
||||
const atk = u.s.atk;
|
||||
h.b.addBuff(u, { key: 'test:atk', mods: { atkPct: 1 } });
|
||||
h.run(0.1);
|
||||
assert.equal(from(h, u).length, n0, `T${tier}: that attack made no hit`);
|
||||
h.run(1);
|
||||
assert.ok(h.hooksOf('damaged').every((c) => !c.dmg.tags?.includes('cqbw:d12')), `T${tier}: nothing before 3 s`);
|
||||
h.run(2.6);
|
||||
const blasts = h.hooksOf('damaged').filter((c) => c.dmg.tags?.includes('cqbw:d12'));
|
||||
const victims = new Set(blasts.map((c) => c.target));
|
||||
assert.ok([fly, f5, e4].every((e) => victims.has(e)), `T${tier}: the three with the most HP (the flyer too)`);
|
||||
assert.ok(victims.has(side), `T${tier}: and what stands within 1.2 of a bomb`);
|
||||
assert.ok(!victims.has(lo) && !victims.has(e3), `T${tier}: the others untouched`);
|
||||
// the ATK cached at the cast (her +100 % since then counts for nothing); a victim of her range that an earlier bomb
|
||||
// stunned takes 落井下石's ×1.18 on top, the one outside her range never
|
||||
const base = atk * sk.bb.atk_scale;
|
||||
for (const c of blasts) {
|
||||
const stunnedBefore = c.target !== side && Math.abs(c.amount - base * 1.18) < 1e-6 * base;
|
||||
assert.ok(Math.abs(c.amount - base) < 1e-6 * base || stunnedBefore, `T${tier}: the cached ATK × ${sk.bb.atk_scale} (${c.amount} vs ${base})`);
|
||||
assert.deepEqual([c.type, c.source, c.credit, !!c.dmg.sourceless], ['phys', null, u, true], `T${tier}: 无来源, credited to her`);
|
||||
}
|
||||
assert.ok(on(h, side).filter((c) => c.dmg.tags?.includes('cqbw:d12')).every((c) => Math.abs(c.amount - base) < 1e-6 * base), `T${tier}: outside her range: no 落井下石`);
|
||||
for (const e of [fly, f5, e4]) approx(blasts.find((c) => c.target === e).amount, base, `T${tier}: its first blast, not stunned yet`);
|
||||
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun');
|
||||
assert.ok(stuns.length >= 4 && stuns.every((c) => Math.abs(c.duration - sk.bb.stun) < 1e-6), `T${tier}: stun ${sk.bb.stun} s`);
|
||||
done(h);
|
||||
}
|
||||
// early burst: the bombed enemy falls before its 3 s; a bomb still goes off after she leaves the field
|
||||
const { h, u } = field({ skill: 2 });
|
||||
const target = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const by = h.spawn('enemy_dummy', { pos: [10, 9.1] }); // outside her range and her 1.0 splash, 1.1 from the target
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), 'cast');
|
||||
h.run(0.8);
|
||||
assert.ok(on(h, by).length === 0, 'the bomb landed, nothing yet');
|
||||
h.b.kill(target, null);
|
||||
h.step();
|
||||
assert.ok(on(h, by).some((c) => c.dmg.tags?.includes('cqbw:d12')), 'it went off at once');
|
||||
const two = field({ skill: 2 });
|
||||
const t2 = two.h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
two.u.skill.gainSp(999);
|
||||
assert.ok(two.h.runUntil(() => two.u.skill.activations === 1, 4));
|
||||
two.h.run(0.8);
|
||||
two.h.b.retreat(two.u);
|
||||
two.h.run(3);
|
||||
assert.ok(on(two.h, t2).some((c) => c.dmg.tags?.includes('cqbw:d12')), 'her retreat keeps the bomb');
|
||||
done(h);
|
||||
done(two.h);
|
||||
});
|
||||
|
||||
test('T1 设伏: 10 s after each deployment 60 % physical and arts dodge, taunt −1, until she leaves; ART-Y stage 3: after 8 s, and ATK +1.25 % a second up to +20 %, reset by damage taken (not by a dodge or a 流失)', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
const y3 = elite && mod === Y && tier === 6;
|
||||
const iv = y3 ? 8 : 10;
|
||||
assert.deepEqual(u.def.raw.talents.find((t) => t.index === 0).bb, { interval: iv, prob: 0.6, taunt_level: -1 }, label(f));
|
||||
h.run(iv - 0.3);
|
||||
assert.equal(u.findBuff('talent:cqbw:ambush'), null, `${label(f)}: not before ${iv} s`);
|
||||
h.run(0.5);
|
||||
assert.deepEqual(u.findBuff('talent:cqbw:ambush')?.mods, { dodgePhys: 0.6, dodgeArts: 0.6, taunt: -1 }, `${label(f)}: after ${iv} s`);
|
||||
assert.deepEqual([u.s.dodgePhys, u.s.dodgeArts, u.s.taunt], [0.6, 0.6, -1], label(f));
|
||||
const ramp = u.findBuff('talent:cqbw:ramp');
|
||||
if (!y3) assert.equal(ramp, null, `${label(f)}: no ramp`);
|
||||
else {
|
||||
assert.equal(ramp.stacks, Math.floor(iv + 0.2), `${label(f)}: a stack a second`);
|
||||
approx(u.s.atk, u.base.atk * (1 + 0.0125 * ramp.stacks), `${label(f)}: ATK +1.25 % each`);
|
||||
h.run(10);
|
||||
assert.equal(u.findBuff('talent:cqbw:ramp').stacks, 16, `${label(f)}: at most 16`);
|
||||
approx(u.s.atk, u.base.atk * 1.2, `${label(f)}: +20 %`);
|
||||
h.b.loseHp(u, 10, { source: u });
|
||||
assert.equal(u.findBuff('talent:cqbw:ramp')?.stacks, 16, `${label(f)}: a 流失 keeps it`);
|
||||
const src = h.spawn('enemy_dummy', { pos: [10, 12] });
|
||||
h.b.dealDamage(src, u, { amount: 10, type: 'phys', canDodge: false });
|
||||
assert.equal(u.findBuff('talent:cqbw:ramp'), null, `${label(f)}: damage clears it`);
|
||||
h.run(1.01);
|
||||
assert.equal(u.findBuff('talent:cqbw:ramp')?.stacks, 1, `${label(f)}: and it climbs again`);
|
||||
}
|
||||
h.b.retreat(u);
|
||||
assert.equal(u.findBuff('talent:cqbw:ambush'), null, `${label(f)}: gone when she leaves`);
|
||||
done(h);
|
||||
}
|
||||
// a dodged hit does not reset the ramp (dodges come from the 60 % of 设伏)
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: Y, skill: 2, seed: 9 });
|
||||
h.run(12);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 12] });
|
||||
let checked = 0;
|
||||
for (let i = 0; i < 60 && checked < 3; i++) {
|
||||
if (!u.findBuff('talent:cqbw:ramp')) h.run(1.05);
|
||||
const before = u.findBuff('talent:cqbw:ramp')?.stacks ?? 0;
|
||||
const n = h.hooksOf('dodge').length;
|
||||
h.b.dealDamage(e, u, { amount: 1, type: 'phys' });
|
||||
if (h.hooksOf('dodge').length > n) {
|
||||
assert.ok(before > 0 && u.findBuff('talent:cqbw:ramp')?.stacks === before, 'a dodge keeps the ramp');
|
||||
checked++;
|
||||
} else assert.equal(u.findBuff('talent:cqbw:ramp'), null, 'a hit that lands clears it');
|
||||
}
|
||||
assert.equal(checked, 3, 'three dodges seen');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('T2 落井下石: a stunned enemy in her range takes ×1.18 physical damage from anyone (×1.24 with ART-X stage 3); not arts, not unstunned, not frozen, not outside her range; two W keep the strongest', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const x3 = elite && mod === X && tier === 6;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 12, col: 9 }] });
|
||||
const ally = h.unit(2);
|
||||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||||
const inR = h.spawn('enemy_dummy', { pos: [11, 7] }), outR = h.spawn('enemy_dummy', { pos: [12, 7] });
|
||||
const scale = x3 ? 1.24 : 1.18;
|
||||
assert.equal(u.def.raw.talents.find((t) => t.index === 1).bb.damage_scale, scale, label(f));
|
||||
const deal = (e, type) => h.b.dealDamage(ally, e, { amount: 1000, type, canDodge: false });
|
||||
assert.equal(deal(inR, 'phys'), 1000, `${label(f)}: not stunned`);
|
||||
for (const e of [inR, outR]) h.b.applyStatus(e, 'stun', { duration: 5 });
|
||||
approx(deal(inR, 'phys'), 1000 * scale, `${label(f)}: stunned in her range`);
|
||||
assert.equal(deal(inR, 'arts'), 1000, `${label(f)}: physical only`);
|
||||
assert.equal(deal(outR, 'phys'), 1000, `${label(f)}: outside her range`);
|
||||
h.b.removeStatus(inR, 'stun');
|
||||
h.b.applyStatus(inR, 'freeze', { duration: 5 });
|
||||
assert.equal(deal(inR, 'phys'), 1000, `${label(f)}: 冻结 is no 晕眩`);
|
||||
done(h);
|
||||
}
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 60, autoFinish: false, seed: 1, flags: { dpPerSec: 0, dpMax: 999 },
|
||||
units: [{ uid: 1, diy: { slot: 5, charId: W, skillIndex: 2 }, row: 10, col: 4 }, { uid: 2, diy: { slot: 6, charId: W, skillIndex: 2, uniEquipId: X }, elite: true, row: 11, col: 4 }],
|
||||
});
|
||||
h.step();
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.b.applyStatus(e, 'stun', { duration: 5 });
|
||||
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'phys', canDodge: false }), 1240, 'two W: ×1.24, not ×1.18 × 1.24');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('ART-X stage 3 落井下石: +1 SP (forced, under 阻回 too) when her projectile — a normal attack, a D12 bomb — knocks out an enemy of her range, or her mine knocks out any enemy; not stage 1, not others\' kills', () => {
|
||||
for (const f of [[5, true, X], [6, true, X], [6, true, Y], [6, false, null]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const x3 = tier === 6 && mod === X;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 12, col: 9 }] });
|
||||
assert.equal(u.def.raw.talents.find((t) => t.index === 1).bb.sp ?? 0, x3 ? 1 : 0, label(f));
|
||||
const forced = () => h.hooksOf('spGain').filter((c) => c.unit === u && c.reason === 'init').length;
|
||||
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
|
||||
const n0 = forced();
|
||||
const weak = h.spawn('enemy_weak', { pos: [10, 7] });
|
||||
assert.ok(h.runUntil(() => !weak.alive, 6), `${label(f)}: her attack knocks it out`);
|
||||
assert.equal(forced() - n0, x3 ? 1 : 0, `${label(f)}: +1 SP under 阻回`);
|
||||
const other = h.spawn('enemy_weak', { pos: [12, 12] });
|
||||
h.b.dealDamage(h.unit(2), other, { amount: 1e6, type: 'true' });
|
||||
assert.equal(forced() - n0, x3 ? 1 : 0, `${label(f)}: not another unit's kill`);
|
||||
done(h);
|
||||
}
|
||||
// her mine's knock-out: any enemy (S2), and a D12 bomb's (S3)
|
||||
for (const skill of [1, 2]) {
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: X, skill });
|
||||
const forced = () => h.hooksOf('spGain').filter((c) => c.unit === u && c.reason === 'init').length;
|
||||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||||
const weak = h.spawn('enemy_weak', { pos: [10, 7] });
|
||||
h.b.removeBuff(u, 'test:disarm');
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `S${skill + 1}: cast`);
|
||||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||||
assert.ok(h.runUntil(() => !weak.alive, 5), `S${skill + 1}: the ${skill === 1 ? 'mine' : 'bomb'} knocks it out`);
|
||||
const killer = h.hooksOf('death').find((c) => c.unit === weak)?.killer;
|
||||
assert.equal(skill === 1 ? killer?.defId : killer, skill === 1 ? MINE : u, `S${skill + 1}: the killer`);
|
||||
assert.equal(forced(), 1, `S${skill + 1}: +1 SP`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('ART-X trait: ×1.1 on her damage to a blocked enemy (main and splash), stage 1 and 3; its token part: the mine ×1.1 on a blocked one; ART-Y trait: ignores 100 DEF', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const x = elite && mod === X, y = elite && mod === Y;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 8, dir: 'LEFT' }] });
|
||||
const blockedE = h.spawn('enemy_dummy', { pos: [10, 8] }); // on 角峰's tile: blocked by him
|
||||
const free = h.spawn('enemy_dummy', { pos: [11, 8] }); // 1.0 away: her splash
|
||||
h.step();
|
||||
assert.ok(blockedE.blockedBy, `${label(f)}: blocked`);
|
||||
assert.ok(h.runUntil(() => from(h, u).some((c) => c.target === free), 6), `${label(f)}: an attack`);
|
||||
const id = from(h, u).find((c) => c.target === free).dmg.attackId;
|
||||
const hits = from(h, u).filter((c) => c.dmg.attackId === id);
|
||||
const amt = (e) => hits.find((c) => c.target === e)?.amount;
|
||||
approx(amt(free), u.s.atk, `${label(f)}: not blocked`);
|
||||
approx(amt(blockedE), u.s.atk * (x ? 1.1 : 1), `${label(f)}: blocked`);
|
||||
if (x) assert.deepEqual(u.def.raw.trait.bb, { atk_scale: 1.1 }, label(f));
|
||||
if (y) assert.deepEqual(u.def.raw.trait.bb, { def_penetrate_fixed: 100 }, label(f));
|
||||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||||
const d = h.spawn('enemy_def', { pos: [9, 12] });
|
||||
approx(h.b.dealDamage(u, d, { amount: 1000, type: 'phys', canDodge: false }), y ? 800 : 700, `${label(f)}: DEF 300 ${y ? '− 100' : ''}`);
|
||||
done(h);
|
||||
}
|
||||
// the mine of an ART-X W: ×1.1 on the blocked enemy within its blast
|
||||
for (const [tier, mod] of [[5, X], [6, X], [6, Y]]) {
|
||||
const { h, u } = field({ tier, elite: true, mod, skill: 1, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 9, col: 7, dir: 'LEFT' }] });
|
||||
const blockedE = h.spawn('enemy_dummy', { pos: [9, 7] });
|
||||
const free = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.step();
|
||||
assert.ok(blockedE.blockedBy && !free.blockedBy);
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4));
|
||||
const [m] = minesOf(h, u);
|
||||
assert.deepEqual([m.tileR, m.tileC], [10, 7], `T${tier} ${mod}: the free tile under an enemy`);
|
||||
assert.equal(m.def.traitBb.atk_scale ?? 1, mod === X ? 1.1 : 1, `T${tier} ${mod}: the token's trait`);
|
||||
assert.ok(h.runUntil(() => !m.alive, 3));
|
||||
const amt = (e) => from(h, m).find((c) => c.target === e)?.amount;
|
||||
approx(amt(blockedE), amt(free) * (mod === X ? 1.1 : 1), `T${tier} ${mod}: ×1.1 on the blocked one`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
// test/content/op_phenxi.test.js — the 自选 operator kit of 菲亚梅塔 (char_300_phenxi, 6★ 炮手; kit
|
||||
// server/sim/content/kits/ops/op-phenxi.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, ART-Y
|
||||
// “‘律外’特种弹药配给组”, ART-X “漫长的旅途” or ISW-A “菲亚梅塔特限证章” 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.
|
||||
// Run: node --test test/content/op_phenxi.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 PX = 'char_300_phenxi';
|
||||
const FORMS = BACKUPS.units[PX].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const Y = 'uniequip_002_phenxi', X = 'uniequip_003_phenxi', ISW = 'uniequip_004_phenxi';
|
||||
const S1 = 'skchr_phenxi_1', S2 = 'skchr_phenxi_2', S3 = 'skchr_phenxi_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 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_def: dummy('enemy_def', { def: 300 }) };
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, Y, X, ISW].map((m) => [t, true, m]))];
|
||||
const P1 = 'phenxi_t_1[peak_1].peak_performance', P2 = 'phenxi_t_1[peak_2].peak_performance';
|
||||
|
||||
/** 菲亚梅塔 as uid 1 at (10, 4) facing RIGHT (3-10: rows 9–11, columns 4–8; her 4th tile ahead is (10, 8)). `pin` keeps
|
||||
* her HP full at the end of every tick (陈述苦难's 流失 would move 精力充沛 under exact damage checks). */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, others = [], seed = 5, pin = false } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'hit'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: PX, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
const u = h.unit(1);
|
||||
if (pin) h.b.on('tick', () => { if (u.alive) u.hp = u.s.maxHp; });
|
||||
h.step();
|
||||
return { h, u };
|
||||
}
|
||||
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src && !c.dmg.tags?.includes('hpLoss'));
|
||||
const on = (h, e) => h.hooksOf('damaged').filter((c) => c.target === e);
|
||||
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'}`;
|
||||
/** The talents a form fights with: the base ones under the 集成战略 module. */
|
||||
const talentsOf = (tier, elite, mod) => {
|
||||
const form = formOf(tier, elite);
|
||||
const m = elite ? modOf(tier, mod) : null;
|
||||
if (!m || mod === ISW) return form.talents;
|
||||
return form.talents.map((t) => {
|
||||
const ch = m.talentChanges.find((c) => c.talentIndex === t.index);
|
||||
return ch ? { ...t, bb: { ...t.bb, ...ch.bb } } : t;
|
||||
});
|
||||
};
|
||||
|
||||
test('菲亚梅塔 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-10, ranged physical 炮手 (splash 1.0), hits air, blocks 1, ground-targetable, the data triggers; offered as a pick', () => {
|
||||
assert.equal(OPERATOR_KITS[PX], KITS[PX]);
|
||||
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], [PX, 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`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat, u.profile.splashRadius, u.profile.sub],
|
||||
[1, 'ranged', true, 'phys', 2.8, 1, 'aoesniper'], `${label(f)}: 炮手`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-10`);
|
||||
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.skillType, s.trigger.rule]), [['MANUAL', 'ACTIVE_RANGE'], ['MANUAL', 'DEFAULT'], ['MANUAL', 'DEFAULT']]);
|
||||
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], [1540, 671, 1796, 797]);
|
||||
assert.deepEqual([Y, X, ISW].flatMap((id) => [modOf(5, id).attr, modOf(6, id).attr]),
|
||||
[{ atk: 48, def: 26 }, { atk: 70, def: 43 }, { atk: 60, def: 17 }, { atk: 85, def: 32 }, { maxHp: 85, atk: 65 }, { maxHp: 115, atk: 90 }]);
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(PX) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(PX)));
|
||||
assert.equal(validateDiyPicks({ [SLOT[5]]: { charId: PX, skillIndex: 2, uniEquipId: X } }, { data, kitted: KITTED_CHARS }).ok, true);
|
||||
});
|
||||
|
||||
test('T1 陈述苦难: every 0.1 s a 流失 of ⌈0.5 % of her current HP⌉ (no hit, no SP), never below 1; ART-X stage 3: none at or under 50.3 %; ISW-A: the base talent (its 集成战略 0.2 s / 0.2 % floor ignored)', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = talentsOf(tier, elite, mod)[0].bb;
|
||||
const x3 = elite && mod === X && tier === 6;
|
||||
assert.deepEqual([t0.interval, t0.hp_ratio, t0.min_hp_ratio ?? 0], [0.1, 0.005, x3 ? 0.503 : 0], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
if (elite && mod === ISW && tier === 6) assert.equal(u.def.raw.talents.find((t) => t.index === 0).bb.interval, 0.2, 'the composed record carries the 集成战略 blackboard');
|
||||
h.run(1.02);
|
||||
const loss = h.hooksOf('damaged').filter((c) => c.target === u && c.dmg.tags?.includes('phenxi:bleed'));
|
||||
assert.equal(loss.length, 10, `${label(f)}: ten in a second`);
|
||||
let hp = u.s.maxHp;
|
||||
for (const c of loss) {
|
||||
const want = Math.ceil(hp * 0.005 - 1e-9);
|
||||
assert.equal(c.amount, want, `${label(f)}: ⌈0.5 % of ${hp}⌉`);
|
||||
assert.ok(c.dmg.tags.includes('hpLoss') && c.dmg.noSp, `${label(f)}: a 流失`);
|
||||
hp -= want;
|
||||
}
|
||||
assert.equal(h.hooksOf('hit').filter((c) => c.target === u).length, 0, `${label(f)}: no hit`);
|
||||
approx(u.hp, hp, `${label(f)}: HP`);
|
||||
h.run(120);
|
||||
if (x3) {
|
||||
assert.ok(u.hpRatio > 0.5 && u.hpRatio <= 0.503, `${label(f)}: stops just above 50 % (${u.hpRatio})`);
|
||||
const n = h.hooksOf('damaged').filter((c) => c.target === u).length;
|
||||
h.run(2);
|
||||
assert.equal(h.hooksOf('damaged').filter((c) => c.target === u).length, n, `${label(f)}: no more`);
|
||||
} else assert.equal(u.hp, 1, `${label(f)}: down to 1, never 0`);
|
||||
assert.ok(u.alive, label(f));
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 精力充沛: ATK +25 % above 50 % HP, +50 % above 80 % (ART-X stage 3: +30 % / +60 %; ISW-A: +25 % / +50 %, not its 集成战略 double)', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = talentsOf(tier, elite, mod)[0].bb;
|
||||
const x3 = elite && mod === X && tier === 6;
|
||||
assert.deepEqual([t0[`${P1}.atk`], t0[`${P1}.hp_ratio`], t0[`${P2}.atk`], t0[`${P2}.hp_ratio`]], x3 ? [0.3, 0.5, 0.6, 0.8] : [0.25, 0.5, 0.5, 0.8], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2 });
|
||||
const at = (ratio) => {
|
||||
u.hp = u.s.maxHp * ratio;
|
||||
h.step();
|
||||
return u.findBuff('talent:phenxi:peak')?.mods.atkPct ?? 0;
|
||||
};
|
||||
assert.equal(at(1), t0[`${P2}.atk`], `${label(f)}: full HP`);
|
||||
approx(u.s.atk, u.base.atk * (1 + t0[`${P2}.atk`]), `${label(f)}: ATK`);
|
||||
assert.equal(at(0.79), t0[`${P1}.atk`], `${label(f)}: under 80 %`);
|
||||
assert.equal(at(0.49), 0, `${label(f)}: under 50 %`);
|
||||
approx(u.s.atk, u.base.atk, `${label(f)}: none`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 宣告终局: ASPD +27 outside a running skill, none while S1 / S3 run (S2 is instant); ART-Y stage 3: +30 outside, +10 inside', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t1 = talentsOf(tier, elite, mod)[1].bb;
|
||||
const y3 = elite && mod === Y && tier === 6;
|
||||
assert.deepEqual([t1.attack_speed, t1['phenxi_e_t_2[in_skill].attack_speed'] ?? 0], y3 ? [30, 10] : [27, 0], label(f));
|
||||
for (const skill of [0, 1, 2]) {
|
||||
const { h, u } = field({ tier, elite, mod, skill });
|
||||
const own = formOf(tier, elite).stats.aspd + (elite ? modOf(tier, mod)?.attr.aspd ?? 0 : 0);
|
||||
assert.equal(u.s.aspd, own + t1.attack_speed, `${label(f)} S${skill + 1}: outside`);
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 6), `${label(f)} S${skill + 1}: cast`);
|
||||
h.step();
|
||||
assert.equal(u.s.aspd, own + (skill === 1 ? t1.attack_speed : (t1['phenxi_e_t_2[in_skill].attack_speed'] ?? 0)), `${label(f)} S${skill + 1}: while it runs`);
|
||||
if (skill === 0) {
|
||||
u.skill.end('test');
|
||||
h.step();
|
||||
assert.equal(u.s.aspd, own + t1.attack_speed, `${label(f)}: back after S1`);
|
||||
}
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 “你须直面” (MANUAL, attack SP 17 / 14 from 5, 30 s, data ACTIVE_RANGE on 3-10 + 1): ATK +45 % / +60 %; since the 2026-08 client her attacks pick ground enemies only (the splash still hits air) and her range stays 3-10', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S1);
|
||||
assert.deepEqual([sk.spType, sk.spCost, sk.initSp, sk.duration, sk.bb.atk, sk.bb.ability_range_forward_extend], ['INCREASE_WHEN_ATTACK', elite ? 14 : 17, 5, 30, elite ? 0.6 : 0.45, 1], `T${tier}`);
|
||||
assert.equal(sk.trigger.customRangeGrid.length, formOf(tier, elite).rangeGrid.length + 3, `T${tier}: the data's trigger grid is the +1 range`);
|
||||
const { h, u } = field({ tier, elite, skill: 0, pin: true });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.spType], ['duration', 'ACTIVE_RANGE', 'attack'], `T${tier}`);
|
||||
const far = h.spawn('enemy_dummy', { pos: [10, 9] }); // 5 tiles ahead: on the trigger grid, outside 3-10
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 2), `T${tier}: an enemy on the +1 column casts it (the data rule)`);
|
||||
approx(u.skill.timeLeft, 30, `T${tier}: 30 s`, 0.01);
|
||||
assert.deepEqual(u.findBuff(`skill:${u.id}`)?.mods, { atkPct: sk.bb.atk }, `T${tier}: ATK +${sk.bb.atk * 100} %`);
|
||||
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: her range stays 3-10`);
|
||||
h.run(6);
|
||||
assert.equal(on(h, far).length, 0, `T${tier}: 5 tiles ahead stays out of reach`);
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 7] });
|
||||
h.run(6);
|
||||
assert.equal(on(h, fly).length, 0, `T${tier}: no air target while it runs`);
|
||||
const g = h.spawn('enemy_dummy', { pos: [11, 7] });
|
||||
assert.ok(h.runUntil(() => on(h, g).length > 0, 4), `T${tier}: a ground target`);
|
||||
h.run(0.3);
|
||||
const id = on(h, g)[0].dmg.attackId;
|
||||
assert.ok(on(h, fly).some((c) => c.dmg.attackId === id && c.dmg.isSplash), `T${tier}: her shell still splashes the flyer 1.0 away`);
|
||||
for (const c of from(h, u).filter((c) => c.dmg.attackId === id)) approx(c.amount, u.base.atk * (1 + sk.bb.atk + u.findBuff('talent:phenxi:peak').mods.atkPct), `T${tier}: ATK`);
|
||||
u.skill.end('test');
|
||||
h.b.kill(g, null);
|
||||
assert.ok(h.runUntil(() => on(h, fly).some((c) => !c.dmg.isSplash), 4), `T${tier}: flyers are targets again once it ends`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 “你须愧悔” (MANUAL, attack SP 11 / 10, data DEFAULT): the attack it is cast for fires the 灼痕弹 instead — 260 % / 320 % within 1.5 of her 4th tile ahead ≈ 0.5 s later, a 灼痕 every 0.66 tiles bursting 0.8 s after it was left for 130 % / 160 % within 1.1, air too; cast every 12th attack', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.spType, sk.spCost, sk.initSp, sk.bb.atk_scale, sk.bb.atk_scale_2, sk.bb.dist], ['INCREASE_WHEN_ATTACK', elite ? 10 : 11, 0, elite ? 3.2 : 2.6, elite ? 1.6 : 1.3, 0.66], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 1, pin: true });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule], ['instant', 'DEFAULT'], `T${tier}`);
|
||||
const A = h.spawn('enemy_dummy', { pos: [10, 8] }); // the end point; 灼痕 5 (7.30) and 6 (7.96) within 1.1
|
||||
const B = h.spawn('enemy_dummy', { pos: [11, 8] }); // 1.0 from the end; 灼痕 6 within 1.1
|
||||
const M = h.spawn('enemy_dummy', { pos: [11, 6] }); // only 灼痕 3 (5.98) within 1.1
|
||||
const F = h.spawn('enemy_fly', { pos: [9, 6] }); // the same, an air unit
|
||||
const C = h.spawn('enemy_dummy', { pos: [9, 9.4] }); // 1.72 from the end, 1.75 from 灼痕 6
|
||||
u.skill.gainSp(999);
|
||||
const a0 = u.stats.attacks;
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `T${tier}: cast`);
|
||||
const t0 = h.b.time;
|
||||
assert.equal(u.stats.attacks, a0, `T${tier}: that attack was the cast`);
|
||||
h.run(1.6);
|
||||
const sk2 = (c) => c.dmg.tags?.includes('phenxi:s2') ? 'end' : c.dmg.tags?.includes('phenxi:mark') ? 'mark' : 'attack';
|
||||
const scales = (e) => on(h, e).filter((c) => c.source === u && sk2(c) !== 'attack' && c.t <= t0 + 1.6).map((c) => [sk2(c), Math.round(c.amount / u.s.atk * 1000) / 1000]).sort();
|
||||
const end = Math.round(sk.bb.atk_scale * 1000) / 1000, mark = Math.round(sk.bb.atk_scale_2 * 1000) / 1000;
|
||||
assert.deepEqual(scales(A), [['end', end], ['mark', mark], ['mark', mark]], `T${tier}: A`);
|
||||
assert.deepEqual(scales(B), [['end', end], ['mark', mark]], `T${tier}: B`);
|
||||
assert.deepEqual(scales(M), [['mark', mark]], `T${tier}: M`);
|
||||
assert.deepEqual(scales(F), [['mark', mark]], `T${tier}: the flyer`);
|
||||
assert.deepEqual(scales(C), [], `T${tier}: C`);
|
||||
const endHit = on(h, A).find((c) => sk2(c) === 'end');
|
||||
assert.ok(Math.abs(endHit.t - t0 - 0.5) < 0.07, `T${tier}: the shell bursts ≈ 0.5 s later (${endHit.t - t0})`);
|
||||
const mHit = on(h, M).find((c) => sk2(c) === 'mark');
|
||||
assert.ok(Math.abs(mHit.t - t0 - (3 * 0.66 / 8 + 0.8)) < 0.07, `T${tier}: 灼痕 3 bursts 0.8 s after it was left (${mHit.t - t0})`);
|
||||
assert.ok(on(h, A).every((c) => c.dmg.type === 'phys'), `T${tier}: physical`);
|
||||
// attack SP: the next cast after spCost more attacks
|
||||
const n1 = u.stats.attacks;
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 2, 60), `T${tier}: cast again`);
|
||||
assert.equal(u.stats.attacks - n1, sk.spCost, `T${tier}: after ${sk.spCost} attacks`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 “你须偿还” (MANUAL, attack SP 22 / 19, data DEFAULT, a toggle): 持续攻击 — every attack interval a shell on her 4th tile ahead, enemy or not: 110 % / 115 % within 2.0, 170 % / 185 % within 1.1, air too; it never ends; a stun holds it', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.spCost, sk.duration, sk.bb['attack@atk_scale'], sk.bb['attack@atk_scale_2'], sk.bb['attack@dist'], sk.bb.base_attack_time],
|
||||
[elite ? 19 : 22, -1, elite ? 1.85 : 1.7, elite ? 1.15 : 1.1, 1.1, 0], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 2, pin: true });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule], ['toggle', 'DEFAULT'], `T${tier}`);
|
||||
const core = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const ring = h.spawn('enemy_dummy', { pos: [11, 8] }); // 1.0: the centre circle
|
||||
const outer = h.spawn('enemy_dummy', { pos: [10, 9.5] }); // 1.5
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 8.5] }); // 1.118: outside the centre circle
|
||||
const far = h.spawn('enemy_dummy', { pos: [11, 10] }); // 2.24
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 4), `T${tier}: cast`);
|
||||
const a0 = u.stats.attacks;
|
||||
h.run(20);
|
||||
assert.ok(u.skill.active, `T${tier}: 持续时间无限`);
|
||||
const shots = u.stats.attacks - a0;
|
||||
assert.equal(shots, Math.floor(20 / u.s.interval) + 1, `T${tier}: one shell at once, then one per attack interval (${u.s.interval} s)`);
|
||||
const s3 = (e) => on(h, e).filter((c) => c.dmg.tags?.includes('phenxi:s3'));
|
||||
for (const [e, sc] of [[core, sk.bb['attack@atk_scale']], [ring, sk.bb['attack@atk_scale']], [outer, sk.bb['attack@atk_scale_2']], [fly, sk.bb['attack@atk_scale_2']]]) {
|
||||
assert.ok(s3(e).length >= shots - 1, `T${tier}: ${e.defId} hit by every shell`);
|
||||
for (const c of s3(e)) {
|
||||
approx(c.amount, u.s.atk * sc, `T${tier}: ×${sc}`);
|
||||
assert.deepEqual([c.type, c.dmg.isAttack, c.dmg.isSkill], ['phys', true, true], `T${tier}: her attack`);
|
||||
}
|
||||
}
|
||||
assert.equal(s3(far).length, 0, `T${tier}: 2.24 away untouched`);
|
||||
assert.equal(from(h, u).filter((c) => !c.dmg.tags?.includes('phenxi:s3') && c.t > h.b.time - 19).length, 0, `T${tier}: no normal attack meanwhile`);
|
||||
for (const e of [core, ring, outer, fly, far]) h.b.kill(e, null);
|
||||
const a1 = u.stats.attacks;
|
||||
h.run(6);
|
||||
assert.ok(u.stats.attacks - a1 >= 2, `T${tier}: no enemy, still firing`);
|
||||
h.b.applyStatus(u, 'stun', { duration: 6, force: true });
|
||||
const a2 = u.stats.attacks;
|
||||
h.run(5.5);
|
||||
assert.equal(u.stats.attacks, a2, `T${tier}: a stun holds it`);
|
||||
h.run(3);
|
||||
assert.ok(u.stats.attacks > a2, `T${tier}: and it goes on`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('ART-X trait: ×1.1 on her damage to a blocked enemy (attack, splash, S2 blasts), stages 1 and 3; ART-Y trait: ignores 100 DEF; ISW-A: attributes only', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const x = elite && mod === X, y = elite && mod === Y;
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, pin: true, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 8, dir: 'LEFT' }] });
|
||||
const blockedE = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const free = h.spawn('enemy_dummy', { pos: [11, 8] });
|
||||
h.step();
|
||||
assert.ok(blockedE.blockedBy, `${label(f)}: blocked`);
|
||||
assert.ok(h.runUntil(() => from(h, u).some((c) => c.target === free), 6), `${label(f)}: an attack`);
|
||||
const id = from(h, u).find((c) => c.target === free).dmg.attackId;
|
||||
const amt = (e) => from(h, u).find((c) => c.dmg.attackId === id && c.target === e)?.amount;
|
||||
approx(amt(free), u.s.atk, `${label(f)}: not blocked`);
|
||||
approx(amt(blockedE), u.s.atk * (x ? 1.1 : 1), `${label(f)}: blocked`);
|
||||
assert.deepEqual(u.def.raw.trait.bb, x ? { atk_scale: 1.1 } : y ? { def_penetrate_fixed: 100 } : {}, `${label(f)}: trait bb`);
|
||||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||||
const d = h.spawn('enemy_def', { pos: [9, 12] });
|
||||
approx(h.b.dealDamage(u, d, { amount: 1000, type: 'phys', canDodge: false }), y ? 800 : 700, `${label(f)}: DEF 300`);
|
||||
done(h);
|
||||
}
|
||||
// S2's blasts on a blocked enemy (ART-X stage 3)
|
||||
const { h, u } = field({ tier: 6, elite: true, mod: X, skill: 1, pin: true, others: [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 8, dir: 'LEFT' }] });
|
||||
const blockedE = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const free = h.spawn('enemy_dummy', { pos: [11, 8] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4));
|
||||
h.run(1);
|
||||
const endOf = (e) => on(h, e).find((c) => c.dmg.tags?.includes('phenxi:s2'))?.amount;
|
||||
approx(endOf(blockedE), endOf(free) * 1.1, 'the shell ×1.1 on the blocked one');
|
||||
done(h);
|
||||
});
|
||||
@@ -0,0 +1,319 @@
|
||||
// test/content/op_wisdel.test.js — the 自选 operator kit of 维什戴尔 (char_1035_wisdel, 6★ 投掷手; kit
|
||||
// server/sim/content/kits/ops/op-wisdel.js) and of her summon “魂灵之影” (token_10035_wisdel_wward), 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 or BOM-X “‘祖宗发射器’” 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.
|
||||
// Run: node --test test/content/op_wisdel.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 WD = 'char_1035_wisdel';
|
||||
const SHADOW = 'token_10035_wisdel_wward';
|
||||
const FORMS = BACKUPS.units[WD].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const BOM = 'uniequip_002_wisdel';
|
||||
const S1 = 'skchr_wisdel_1', S2 = 'skchr_wisdel_2', S3 = 'skchr_wisdel_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 shadowOf = (tier, elite) => BACKUPS.tokens[SHADOW].variants[`${WD}@${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: 100, range: 3 }) };
|
||||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, BOM].map((m) => [t, true, m]))];
|
||||
|
||||
/** 维什戴尔 as uid 1 at (10, 4) facing RIGHT (3-9: rows 8–12; (10, 4..8), (9 / 11, 4..7), (8 / 12, 4..6)). */
|
||||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 4, others = [], seed = 5 } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 1200, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'heal'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: WD, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src);
|
||||
const on = (h, e) => h.hooksOf('damaged').filter((c) => c.target === e);
|
||||
const shadowsOf = (h, u) => h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === SHADOW && t.ownerUnit === u);
|
||||
const tag = (c, t) => !!c.dmg.tags?.includes(t);
|
||||
const markKey = (u) => `wisdel:mark:${u.id}`;
|
||||
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'}`;
|
||||
const t0Of = (tier, elite, mod) => (elite && mod === BOM && tier === 6 ? modOf(6, BOM).talentChanges[0].bb : formOf(tier, elite).talents[0].bb);
|
||||
|
||||
test('维什戴尔 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-9, 投掷手 (ground-only 0.9 splash, never air), blocks 1, the data triggers, a 魂灵之影 beside her at once; offered as a pick', () => {
|
||||
assert.equal(OPERATOR_KITS[WD], KITS[WD]);
|
||||
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], [WD, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.res, u.base.aspd], [form.stats.maxHp, form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, form.stats.res, 100 + (m?.attr.aspd ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.groundOnly, u.profile.dmgType, u.base.bat, u.profile.splashRadius, u.profile.sub],
|
||||
[1, 'ranged', false, true, 'phys', 2.1, 0.9, 'bombarder'], `${label(f)}: 投掷手`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-9`);
|
||||
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
|
||||
const sh = shadowsOf(h, u);
|
||||
assert.deepEqual(sh.map((t) => [t.tileR, t.tileC]), [[9, 4]], `${label(f)}: a shadow on the nearest free tile of her range (lower row first)`);
|
||||
assert.ok(u.s.flags.camou && !u.s.flags.liftoff && !u.s.flags.stealth, `${label(f)}: 迷彩 beside it, ground-targetable otherwise`);
|
||||
done(h);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(formOf(6, true).skills.map((s) => [s.skillType, s.trigger.rule, s.trigger.rawRule]), [['AUTO', 'DEFAULT', 'DEFAULT'], ['MANUAL', 'DEFAULT', 'DEFAULT'], ['MANUAL', 'SP_FULL', 'ALWAYS']]);
|
||||
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], [1434, 583, 1735, 652]);
|
||||
assert.deepEqual([modOf(5, BOM).attr, modOf(6, BOM).attr], [{ atk: 45, aspd: 5 }, { atk: 65, aspd: 7 }]);
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(WD) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(WD)));
|
||||
assert.equal(validateDiyPicks({ [SLOT[6]]: { charId: WD, skillIndex: 2, uniEquipId: BOM } }, { data, kitted: KITTED_CHARS }).ok, true);
|
||||
});
|
||||
|
||||
test('trait 投掷手 + T1 好礼: the main target ×1.15 (BOM-X stage 3 ×1.25), the 0.9 splash in full, then 0.3 s later an aftershock at 50 % ATK on the spot (×1.15 on the main one); BOM-X: two; flyers never hit; her 残影 on the main target', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = t0Of(tier, elite, mod);
|
||||
const shocks = elite && mod ? 2 : 1;
|
||||
assert.deepEqual([t0['attack@main_atk_scale'], t0['attack@bomb_atk_scale']], elite && mod === BOM && tier === 6 ? [1.25, 1.75] : [1.15, 1.5], label(f));
|
||||
assert.equal(formOf(tier, elite).trait.bb['attack@append_atk_scale'], 0.5, label(f));
|
||||
if (elite && mod) assert.equal(modOf(tier, mod).traitOverride.bb['attack@enable_third_attack'], 1, label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 21 });
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const sp = h.spawn('enemy_dummy', { pos: [10, 8.85] }); // 0.85: her splash, outside her range
|
||||
const out = h.spawn('enemy_dummy', { pos: [11, 8.75] }); // √(1 + 0.5625) = 1.25
|
||||
const fly = h.spawn('enemy_fly', { pos: [9, 8] }); // in her range, an air unit
|
||||
assert.ok(h.runUntil(() => on(h, main).length > 0, 4), `${label(f)}: an attack`);
|
||||
const t = h.b.time;
|
||||
h.run(0.8);
|
||||
const atk = u.s.atk;
|
||||
const ms = t0['attack@main_atk_scale'];
|
||||
const plain = (c) => !tag(c, 'wisdel:bomb');
|
||||
const first = from(h, u).filter((c) => plain(c) && c.t <= t + 1e-9);
|
||||
assert.deepEqual(new Set(first.map((c) => c.target)), new Set([main, sp]), `${label(f)}: the main target and the splash`);
|
||||
approx(first.find((c) => c.target === main).amount, atk * ms, `${label(f)}: main ×${ms}`);
|
||||
approx(first.find((c) => c.target === sp).amount, atk, `${label(f)}: splash in full`);
|
||||
const shockHits = from(h, u).filter((c) => plain(c) && tag(c, 'aftershock') && c.t <= t + 0.75);
|
||||
assert.equal(shockHits.filter((c) => c.target === main).length, shocks, `${label(f)}: ${shocks} aftershock(s)`);
|
||||
for (const c of shockHits) approx(c.amount, atk * 0.5 * (c.target === main ? ms : 1), `${label(f)}: 50 %`);
|
||||
assert.ok(Math.abs(shockHits[0].t - t - 0.3) < 0.04, `${label(f)}: 0.3 s later`);
|
||||
assert.deepEqual([on(h, out).length, on(h, fly).length], [0, 0], `${label(f)}: 1.25 away and the flyer untouched`);
|
||||
assert.ok(main.findBuff(markKey(u)) || on(h, main).some((c) => tag(c, 'wisdel:bomb')), `${label(f)}: marked (or the mark went off)`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T1 好礼: each aftershock on a marked enemy goes off 15 % of the time — 150 % (175 %) ATK physical to every enemy within 1.1, air too, stun 1 s, the mark spent; the 残影 leave with her', () => {
|
||||
for (const f of [[5, false, null], [6, true, BOM]]) {
|
||||
const [tier, elite, mod] = f;
|
||||
const t0 = t0Of(tier, elite, mod);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 33 });
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 9] }); // 1.0 from the main target: only the explosion reaches it
|
||||
h.run(400);
|
||||
const bombs = from(h, u).filter((c) => tag(c, 'wisdel:bomb') && c.target === main);
|
||||
const rolls = from(h, u).filter((c) => tag(c, 'aftershock') && c.target === main).length;
|
||||
assert.ok(rolls > 150, `${label(f)}: ${rolls} rolls`);
|
||||
const r = bombs.length / rolls;
|
||||
assert.ok(r > 0.09 && r < 0.21, `${label(f)}: ${bombs.length} / ${rolls} ≈ 15 %`);
|
||||
for (const c of bombs) assert.equal(c.type, 'phys', label(f));
|
||||
const flyBombs = on(h, fly).filter((c) => tag(c, 'wisdel:bomb'));
|
||||
assert.equal(flyBombs.length, bombs.length, `${label(f)}: the flyer 1.0 away takes every explosion`);
|
||||
assert.equal(on(h, fly).length, flyBombs.length, `${label(f)}: and nothing else`);
|
||||
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun' && c.target === fly);
|
||||
assert.equal(stuns.length, bombs.length, `${label(f)}: each explosion stuns`);
|
||||
for (const c of stuns) approx(c.duration, t0['attack@stun'], `${label(f)}: 1 s`);
|
||||
// the amounts: her ATK × bomb_atk_scale (the explosion is no attack: no ×main)
|
||||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||||
h.run(2);
|
||||
main.buffs = main.buffs.filter((b) => b.key !== markKey(u));
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
h.b.addBuff(e, { key: markKey(u), source: u });
|
||||
h.b.removeBuff(u, 'test:disarm');
|
||||
assert.ok(h.runUntil(() => on(h, e).some((c) => tag(c, 'wisdel:bomb')), 200), `${label(f)}: it goes off on a marked one`);
|
||||
const b = on(h, e).find((c) => tag(c, 'wisdel:bomb'));
|
||||
approx(b.amount, u.s.atk * t0['attack@bomb_atk_scale'], `${label(f)}: ×${t0['attack@bomb_atk_scale']}`);
|
||||
assert.ok(!e.findBuff(markKey(u)) || on(h, e).filter((c) => c.t > b.t && !tag(c, 'wisdel:bomb')).length > 0, `${label(f)}: spent (or re-marked by a later attack)`);
|
||||
h.b.addBuff(e, { key: markKey(u), source: u });
|
||||
h.b.retreat(u);
|
||||
assert.ok(!e.findBuff(markKey(u)) && !main.findBuff(markKey(u)), `${label(f)}: her 残影 leave with her`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('T2 死魂灵的余息: a 魂灵之影 (the form\'s stats, 禁疗, no attack, attackable) at each deployment on the nearest free tile of her range, at most 3; 迷彩 while one that came in on her x-5 stands; the shadows stay when she leaves', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const v = shadowOf(tier, elite);
|
||||
assert.deepEqual([v.stats.maxHp, v.stats.atk, v.stats.def, v.stats.res, v.stats.respawnTime, v.stats.deployLimit], [3500, elite ? 738 : 660, elite ? 628 : 585, 50, 5, 3], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
const [t] = shadowsOf(h, u);
|
||||
assert.deepEqual([t.base.maxHp, t.base.atk, t.base.def, t.base.res, t.s.blockCnt, !!t.s.flags.noHeal, !!t.s.flags.untargetable, t.profile.noAttack],
|
||||
[3500, v.stats.atk, v.stats.def, 50, 0, true, false, true], `T${tier}: stats, 禁疗, attackable, no attack`);
|
||||
assert.ok(u.findBuff(`wisdel:camou:${t.id}`) && u.s.flags.camou, `T${tier}: 迷彩`);
|
||||
// a ranged enemy in reach: she is not its target while camouflaged and not blocking
|
||||
const shooter = h.spawn('enemy_atk', { pos: [10, 6] });
|
||||
h.run(4);
|
||||
assert.ok(!h.hooksOf('damaged').some((c) => c.source === shooter && c.target === u), `T${tier}: 迷彩: the shooter does not pick her`);
|
||||
h.b.kill(shooter, null);
|
||||
h.b.kill(t, null);
|
||||
assert.ok(!u.s.flags.camou, `T${tier}: gone with its shadow`);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
h.b.retreat(u);
|
||||
h.run(0.2);
|
||||
assert.ok(h.b.redeploy(u, { free: true }), `T${tier}: redeploy ${i}`);
|
||||
h.run(0.2);
|
||||
}
|
||||
assert.equal(shadowsOf(h, u).filter((s) => s.alive).length, 3, `T${tier}: at most 3 (they stay when she leaves)`);
|
||||
done(h);
|
||||
}
|
||||
// no free tile next to her: the shadow lands on the next nearest (lower row first) and gives no 迷彩
|
||||
const others = [[9, 4], [11, 4], [10, 5]].map(([r, c], i) => ({ uid: 10 + i, chessId: 'chess_char_1_02_a', row: r, col: c }));
|
||||
const { h, u } = field({ skill: 1, others });
|
||||
assert.deepEqual(shadowsOf(h, u).map((t) => [t.tileR, t.tileC]), [[9, 5]], 'the nearest free one: √2 away, lower row first');
|
||||
assert.ok(!u.s.flags.camou, 'not on its x-5: no 迷彩');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('魂灵之影\'s skill: every 5 SP (time; +0–2 after each cast) a ground enemy of 维什戴尔\'s range — unmarked first — takes its ATK as arts damage, 停顿 1 s and her 残影; never an air unit', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const v = shadowOf(tier, elite);
|
||||
assert.deepEqual([v.skill.skillType, v.skill.spType, v.skill.spCost, v.skill.bb], ['AUTO', 'INCREASE_WITH_TIME', 5, { sluggish: 1, sp_min: 0, sp_max: 3 }], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 1 });
|
||||
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
|
||||
const [t] = shadowsOf(h, u);
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 6] });
|
||||
h.run(8);
|
||||
assert.deepEqual([t.skill.activations, on(h, fly).length], [0, 0], `T${tier}: an air unit only: nothing, SP kept`);
|
||||
assert.equal(t.skill.charges, 1, `T${tier}`);
|
||||
const a = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
const b = h.spawn('enemy_dummy', { pos: [11, 7] });
|
||||
const outside = h.spawn('enemy_dummy', { pos: [10, 9] }); // outside her 3-9
|
||||
h.b.addBuff(a, { key: markKey(u), source: u }); // already marked
|
||||
h.step();
|
||||
assert.equal(t.skill.activations, 1, `T${tier}: cast at once`);
|
||||
const hit = from(h, t);
|
||||
assert.deepEqual(hit.map((c) => c.target), [b], `T${tier}: the unmarked one`);
|
||||
approx(hit[0].amount, t.s.atk, `T${tier}: its ATK`);
|
||||
assert.deepEqual([hit[0].type, hit[0].dmg.isSkill], ['arts', true], `T${tier}: arts`);
|
||||
assert.ok(b.findBuff(markKey(u)), `T${tier}: her 残影`);
|
||||
const slow = h.hooksOf('statusApplied').find((c) => c.source === t && c.target === b);
|
||||
assert.deepEqual([slow?.status, slow?.duration], ['sluggish', 1], `T${tier}: 停顿 1 s`);
|
||||
h.run(60);
|
||||
const casts = t.skill.activations;
|
||||
assert.ok(casts >= 60 / 5 + 1 && casts <= 60 / 3 + 2, `T${tier}: 5 SP a cast, 0–2 back each time (${casts} in 60 s)`);
|
||||
assert.equal(on(h, outside).length, 0, `T${tier}: outside her range: never`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 定点清算 (AUTO, attack SP 4 / 3, data DEFAULT): the next attack splashes 1.1, makes 2 more aftershocks at 75 % / 90 % ATK and stuns every enemy it hits 0.5 / 1 s', () => {
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
const sk = skillOf(tier, elite, S1), t0 = t0Of(tier, elite, mod);
|
||||
assert.deepEqual([sk.spType, sk.spCost, sk.bb.append_atk_scale, sk.bb.stun_duration], ['INCREASE_WHEN_ATTACK', elite ? 3 : 4, elite ? 0.9 : 0.75, elite ? 1 : 0.5], label(f));
|
||||
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 2 });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule], ['instant', 'DEFAULT'], label(f));
|
||||
h.b.addBuff(shadowsOf(h, u)[0], { key: 'test:silence', flags: { silence: true } });
|
||||
const main = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
const sp = h.spawn('enemy_dummy', { pos: [10, 9.05] }); // 1.05: only the 1.1 splash
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 4), `${label(f)}: cast on her next attack`);
|
||||
h.run(2);
|
||||
const atk = u.s.atk;
|
||||
const hits = from(h, u).filter((c) => !tag(c, 'wisdel:bomb') && c.dmg.isSkill);
|
||||
const n = (elite && mod ? 2 : 1) + 2;
|
||||
assert.equal(hits.filter((c) => c.target === main && tag(c, 'aftershock')).length, n, `${label(f)}: ${n} aftershocks`);
|
||||
for (const c of hits.filter((x) => tag(x, 'aftershock'))) approx(c.amount, atk * sk.bb.append_atk_scale * (c.target === main ? t0['attack@main_atk_scale'] : 1), `${label(f)}: ${sk.bb.append_atk_scale * 100} %`);
|
||||
assert.ok(hits.some((c) => c.target === sp && !tag(c, 'aftershock')), `${label(f)}: the 1.1 splash`);
|
||||
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun' && c.target === sp);
|
||||
assert.ok(stuns.length >= 1 && stuns.every((c) => Math.abs(c.duration - sk.bb.stun_duration) < 1e-9), `${label(f)}: stunned ${sk.bb.stun_duration} s`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 饱和复仇 (MANUAL, 32 / 29 SP from 15, 25 s, data DEFAULT): ATK +16 % / +25 %, interval −0.5 / −0.7 s, 3 targets; 过载 the second half: every attack 4 rounds of 65 % / 70 % on random enemies of her range', () => {
|
||||
for (const [tier, elite] of [[5, false], [6, true]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.spCost, sk.initSp, sk.duration, sk.bb.atk, sk.bb.base_attack_time, sk.bb['attack@atk_scale_ol']], elite ? [29, 15, 25, 0.25, -0.7, 0.7] : [32, 15, 25, 0.16, -0.5, 0.65], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, skill: 1, seed: 8 });
|
||||
h.b.addBuff(shadowsOf(h, u)[0], { key: 'test:silence', flags: { silence: true } });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule], ['duration', 'DEFAULT'], `T${tier}`);
|
||||
for (const p of [[10, 6], [11, 6], [9, 6], [12, 5]]) h.spawn('enemy_dummy', { pos: p });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 4), `T${tier}: cast`);
|
||||
const ts = h.b.time;
|
||||
approx(u.s.interval, 2.1 + sk.bb.base_attack_time, `T${tier}: interval`);
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
|
||||
h.run(26);
|
||||
const mains = from(h, u).filter((c) => c.dmg.isAttack && !c.dmg.isSplash && c.dmg.isSkill);
|
||||
const early = mains.filter((c) => c.t < ts + 12.4), late = mains.filter((c) => c.t > ts + 12.6 && c.t < ts + 25);
|
||||
const byAttack = (list) => { const m = new Map(); for (const c of list) m.set(c.dmg.attackId, [...(m.get(c.dmg.attackId) ?? []), c]); return [...m.values()]; };
|
||||
for (const g of byAttack(early)) {
|
||||
assert.equal(new Set(g.map((c) => c.target)).size, 3, `T${tier}: 3 targets at once`);
|
||||
for (const c of g) approx(c.amount, u.base.atk * (1 + sk.bb.atk) * 1.15, `T${tier}: ×1.15 on each main target`);
|
||||
}
|
||||
const lg = byAttack(late);
|
||||
assert.ok(lg.length >= 5, `T${tier}: overload attacks`);
|
||||
for (const g of lg) {
|
||||
assert.equal(g.length, 4, `T${tier}: 4 rounds`);
|
||||
for (const c of g) approx(c.amount, u.base.atk * (1 + sk.bb.atk) * sk.bb['attack@atk_scale_ol'] * 1.15, `T${tier}: ${sk.bb['attack@atk_scale_ol'] * 100} %`);
|
||||
}
|
||||
assert.ok(lg.some((g) => new Set(g.map((c) => c.target)).size < 4), `T${tier}: random picks (repeats happen)`);
|
||||
assert.ok(new Set(lg.flatMap((g) => g.map((c) => c.target))).size === 4, `T${tier}: every enemy of her range drawn some time`);
|
||||
assert.ok(!u.skill.active, `T${tier}: ends after 25 s`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 爆裂黎明 (MANUAL, data SP_FULL: at once, no enemy needed): 1 / 2 shadows (the first with 3 SP), ATK +120 % / +150 %, interval +2.9 s, 6 attacks at 175 % / 190 % with a 2.5 splash on air units too, 好礼 at 100 %', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, null], [6, true, BOM]]) {
|
||||
const sk = skillOf(tier, elite, S3);
|
||||
assert.deepEqual([sk.spCost, sk.initSp, sk.bb.max_cnt, sk.bb.atk, sk.bb.base_attack_time, sk.bb['attack@atk_scale_3'], sk.bb['attack@trigger_time'], sk.bb['attack@prob'], sk.bb.sp],
|
||||
elite ? [58, 35, 2, 1.5, 2.9, 1.9, 6, 1, 3] : [64, 30, 1, 1.2, 2.9, 1.75, 6, 1, 3], `T${tier}`);
|
||||
const { h, u } = field({ tier, elite, mod, skill: 2, seed: 4 });
|
||||
assert.deepEqual([u.skill.kind, u.skill.rule], ['ammo', 'SP_FULL'], `T${tier}`);
|
||||
for (const t of shadowsOf(h, u)) h.b.addBuff(t, { key: 'test:silence', flags: { silence: true } });
|
||||
u.skill.gainSp(999);
|
||||
h.step();
|
||||
assert.ok(u.skill.active, `T${tier}: cast at once, no enemy on the field`);
|
||||
const sh = shadowsOf(h, u);
|
||||
assert.equal(sh.length, 1 + sk.bb.max_cnt, `T${tier}: the talent's + ${sk.bb.max_cnt}`);
|
||||
assert.ok(Math.floor(sh[1].skill.sp) === 3 && (sh.length < 3 || Math.floor(sh[2].skill.sp) === 0), `T${tier}: the first S3 shadow with 3 SP`);
|
||||
approx(u.s.interval, (2.1 + 2.9) * 100 / u.s.aspd, `T${tier}: interval +2.9 s`);
|
||||
const fly = h.spawn('enemy_fly', { pos: [10, 7] });
|
||||
assert.ok(h.runUntil(() => on(h, fly).length > 0, 2), `T${tier}: an air unit alone in her range is her target now`);
|
||||
const g = h.spawn('enemy_dummy', { pos: [11, 8] }); // √2 from the flyer: her 2.5 splash, her aftershocks
|
||||
const ring = h.spawn('enemy_fly', { pos: [11.5, 8.9] }); // 2.42 from the flyer, outside her range
|
||||
h.b.addBuff(g, { key: markKey(u), source: u });
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 60), `T${tier}: 6 attacks and done`);
|
||||
h.run(1);
|
||||
const mains = from(h, u).filter((c) => c.dmg.isAttack && !c.dmg.isSplash);
|
||||
assert.equal(new Set(mains.map((c) => c.dmg.attackId)).size, 6, `T${tier}: 6 attacks`);
|
||||
// ATK +atk while it runs (the last shell lands after its 6th bullet ended it: the engine's ammo rule, skills.js)
|
||||
const tEnd = h.hooksOf('skillEnd').find((c) => c.unit === u).t;
|
||||
const atkAt = (t) => u.base.atk * (t <= tEnd + 1e-9 ? 1 + sk.bb.atk : 1);
|
||||
const ms = t0Of(tier, elite, mod)['attack@main_atk_scale'];
|
||||
for (const c of mains) approx(c.amount, atkAt(c.t) * sk.bb['attack@atk_scale_3'] * ms, `T${tier}: ×${sk.bb['attack@atk_scale_3']} × main`);
|
||||
assert.ok(mains.filter((c) => c.t <= tEnd).length >= 5, `T${tier}: five land while it runs`);
|
||||
assert.ok(mains.every((c) => c.target === fly), `T${tier}: the flyer was her target throughout`);
|
||||
const splash = from(h, u).filter((c) => c.dmg.isSplash && !tag(c, 'aftershock') && !tag(c, 'wisdel:bomb'));
|
||||
assert.ok(splash.some((c) => c.target === ring) && splash.some((c) => c.target === g), `T${tier}: the 2.5 splash reaches the air unit 2.42 away and the ground one`);
|
||||
for (const c of splash) approx(c.amount, atkAt(c.t) * sk.bb['attack@atk_scale_3'], `T${tier}: splash in full`);
|
||||
assert.ok(on(h, fly).every((c) => !tag(c, 'aftershock')) && on(h, ring).every((c) => !tag(c, 'aftershock')), `T${tier}: aftershocks only on ground enemies`);
|
||||
const shocks = from(h, u).filter((c) => tag(c, 'aftershock') && c.target === g);
|
||||
assert.equal(shocks.length, 6 * (elite && mod ? 2 : 1), `T${tier}: every aftershock reaches the ground one (it came before the first one)`);
|
||||
for (const c of shocks) approx(c.amount, atkAt(c.t) * sk.bb['attack@atk_scale_3'] * 0.5, `T${tier}: 50 % of the boosted ATK`);
|
||||
assert.ok(on(h, g).some((c) => tag(c, 'wisdel:bomb') && Math.abs(c.t - shocks[0].t) < 1e-9), `T${tier}: 好礼 at 100 %: the marked one went off on the first aftershock`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
+425
-103
@@ -1974,8 +1974,8 @@
|
||||
"snaps": "8eac4d32 fa5c2376 bc1e9184 84fb1db6 b4b3b807 cc6f6399 c665e4ff 261094cd 5d5c61fd 1bf27396 b526ffa6 e433a3a2 7d8bd521 d7eb687d 4d18ecc9 f640c5fd 0912a7d6 cc22df71 1a833e56 d877d28b 8a887ef8 8014c7dd"
|
||||
},
|
||||
"diy-049": {
|
||||
"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=cqbw/S1 5_diy2_a=cqbw/S2 5_diy1_a=cqbw/S3 5_diy2_b=cqbw/S1 5_diy1_b=cqbw/S2 5_diy2_b=cqbw/S3 5_diy1_b=cqbw/S1/002_cqbw 5_diy2_b=cqbw/S2/002_cqbw 5_diy1_b=cqbw/S3/002_cqbw 5_diy2_b=cqbw/S1/003_cqbw 5_diy1_b=cqbw/S2/003_cqbw 5_diy2_b=cqbw/S3/003_cqbw",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m05",
|
||||
"modeId": "mode_multi_normal",
|
||||
@@ -1985,122 +1985,173 @@
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 2,
|
||||
"killed": 8,
|
||||
"total": 60,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"rngDraws": 41,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":2,"total":60,"perfect":false,"leaks":58,"leaksUncounted":0,"coins":0,"dmg":267738,"bossDmg":0,"heal":59968,"deaths":6,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":8,"total":60,"perfect":false,"leaks":52,"leaksUncounted":0,"coins":0,"dmg":915423,"bossDmg":0,"heal":0,"deaths":5,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,76,0,304,6384,0,22,1],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,75,0,652,8724,0,41,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,2564,1,6,0,21282,21790,0,137,3],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",3334,1,8,2,22607,38177,0,115,4],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,1271,1,10,0,2272,0,0,99,2],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1510,1,11,0,2916,0,0,108,3],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1133,1,1,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",1360,1,3,0,97722,0,0,178,4],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1218,1,5,0,0,0,29529,69,4],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",1374,1,7,0,0,0,30439,60,4],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,932,1,9,0,56880,0,0,144,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",1131,1,12,0,63104,0,0,136,3]
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,1284,1,10,0,6421,0,0,39,5],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1284,1,11,0,4189,0,0,43,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1284,1,1,2,77009,0,0,72,5],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",1497,1,3,1,85838,0,0,80,11],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",1497,1,5,0,13124,0,0,54,18],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1497,1,7,0,129574,0,0,65,5],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_cqbw",1627,1,9,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_cqbw",1627,1,12,0,21453,0,0,51,16],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_cqbw",178.3,1,119,1,95898,3076,0,48,4],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_cqbw",0,0,110,2,78924,3314,0,41,5],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_cqbw",1657,1,121,0,27773,1657,0,37,12],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_cqbw",1657,1,122,1,115875,1657,0,43,3],
|
||||
["p1","~1","token","token_10008_cqbw_box",null,null,1000,0,19,0,3380,0,0,0,0],
|
||||
["p1","~2","token","token_10008_cqbw_box",null,null,1000,0,21,0,5460,0,0,0,0],
|
||||
["p1","~3","token","token_10008_cqbw_box",null,null,1000,0,22,0,4713,0,0,0,0],
|
||||
["p1","~4","token","token_10008_cqbw_box",null,null,1000,0,25,0,6025,0,0,0,0],
|
||||
["p1","~5","token","token_10008_cqbw_box",null,null,1000,0,27,0,5350,0,0,0,0],
|
||||
["p1","~6","token","token_10008_cqbw_box",null,null,1000,0,29,0,6977,0,0,0,0],
|
||||
["p1","~7","token","token_10008_cqbw_box",null,null,1000,0,33,0,9969,0,0,0,0],
|
||||
["p1","~8","token","token_10008_cqbw_box",null,null,1000,0,34,0,9500,0,0,0,0],
|
||||
["p1","~9","token","token_10008_cqbw_box",null,null,1000,0,37,0,10625,0,0,0,0],
|
||||
["p1","~10","token","token_10008_cqbw_box",null,null,1000,0,38,0,6270,0,0,0,0],
|
||||
["p1","~11","token","token_10008_cqbw_box",null,null,1000,0,40,0,6770,0,0,0,0],
|
||||
["p1","~12","token","token_10008_cqbw_box",null,null,1000,0,41,0,3290,0,0,0,0],
|
||||
["p1","~13","token","token_10008_cqbw_box",null,null,1000,0,44,0,4580,0,0,0,0],
|
||||
["p1","~14","token","token_10008_cqbw_box",null,null,1000,0,45,0,4040,0,0,0,0],
|
||||
["p1","~15","token","token_10008_cqbw_box",null,null,1000,0,48,0,4490,0,0,0,0],
|
||||
["p1","~16","token","token_10008_cqbw_box",null,null,1000,0,51,0,5404,0,0,0,0],
|
||||
["p1","~17","token","token_10008_cqbw_box",null,null,1000,0,55,0,6187,0,0,0,0],
|
||||
["p1","~18","token","token_10008_cqbw_box",null,null,1000,0,57,0,10340,0,0,0,0],
|
||||
["p1","~19","token","token_10008_cqbw_box",null,null,1000,0,59,0,6270,0,0,0,0],
|
||||
["p1","~20","token","token_10008_cqbw_box",null,null,1000,0,61,0,6770,0,0,0,0],
|
||||
["p1","~21","token","token_10008_cqbw_box",null,null,1000,0,63,0,8980,0,0,0,0],
|
||||
["p1","~22","token","token_10008_cqbw_box",null,null,1000,0,66,0,4580,0,0,0,0],
|
||||
["p1","~23","token","token_10008_cqbw_box",null,null,1000,0,69,0,5460,0,0,0,0],
|
||||
["p1","~24","token","token_10008_cqbw_box",null,null,1000,0,71,0,7558,0,0,0,0],
|
||||
["p1","~25","token","token_10008_cqbw_box",null,null,1000,0,73,0,6270,0,0,0,0],
|
||||
["p1","~26","token","token_10008_cqbw_box",null,null,1000,0,74,0,5460,0,0,0,0],
|
||||
["p1","~27","token","token_10008_cqbw_box",null,null,1000,0,76,0,7239,0,0,0,0],
|
||||
["p1","~28","token","token_10008_cqbw_box",null,null,1000,0,79,0,8844,0,0,0,0],
|
||||
["p1","~29","token","token_10008_cqbw_box",null,null,1000,0,80,0,7753,0,0,0,0],
|
||||
["p1","~30","token","token_10008_cqbw_box",null,null,1000,0,81,0,6135,0,0,0,0],
|
||||
["p1","~31","token","token_10008_cqbw_box",null,null,1000,0,86,0,4580,0,0,0,0],
|
||||
["p1","~32","token","token_10008_cqbw_box",null,null,1000,0,87,0,2730,0,0,0,0],
|
||||
["p1","~33","token","token_10008_cqbw_box",null,null,1000,0,91,0,6135,0,0,0,0],
|
||||
["p1","~34","token","token_10008_cqbw_box",null,null,1000,0,94,0,6025,0,0,0,0],
|
||||
["p1","~35","token","token_10008_cqbw_box",null,null,1000,0,95,0,5350,0,0,0,0],
|
||||
["p1","~36","token","token_10008_cqbw_box",null,null,1000,0,98,0,5913,0,0,0,0],
|
||||
["p1","~37","token","token_10008_cqbw_box",null,null,1000,0,100,0,6560,0,0,0,0],
|
||||
["p1","~38","token","token_10008_cqbw_box",null,null,1000,0,102,0,6057,0,0,0,0],
|
||||
["p1","~39","token","token_10008_cqbw_box",null,null,1000,0,107,0,6270,0,0,0,0],
|
||||
["p1","~40","token","token_10008_cqbw_box",null,null,1000,0,108,0,5460,0,0,0,0],
|
||||
["p1","~41","token","token_10008_cqbw_box",null,null,1000,0,112,0,2890,0,0,0,0],
|
||||
["p1","~42","token","token_10008_cqbw_box",null,null,1000,0,114,0,3930,0,0,0,0],
|
||||
["p1","~43","token","token_10008_cqbw_box",null,null,1000,0,116,0,1445,0,0,0,0],
|
||||
["p1","~44","token","token_10008_cqbw_box",null,null,1000,0,117,0,1310,0,0,0,0],
|
||||
["p1","~45","token","token_10008_cqbw_box",null,null,1000,1,118,0,0,0,0,0,0],
|
||||
["p1","~46","token","token_10008_cqbw_box",null,null,1000,1,120,0,0,0,0,0,0]
|
||||
],
|
||||
"enemies": {"enemy_1045_hammer_2":[12,0,11,37471,77249],"enemy_1061_zomshd":[48,2,45,37605,269489]},
|
||||
"hooks": [1,76,6900,1205,1205,3051,3051,29902,8,64,33,29,0,37113,285,23,60,56,1,8,0,0,0],
|
||||
"events": {"atk":1205,"deploy":16,"die":8,"dmg":3051,"heal":129,"leak":56,"skill":62,"spawn":72,"status":555},
|
||||
"fx": {},
|
||||
"snaps": "c6bdc18a 6fb12fe1 c3c94d6f f42e9bba 6ea4c93a ff3506ac f551dbb2 248e7a29 e877982d 0b8ac60e d21ffc26 89eda7b5 7ef13ac0 2d2fce6f 32484e38 3407d0b5 1b59f452 55141007 fc87be97 6239dcab e11e96f9 250fe5b6 a75b766c"
|
||||
"enemies": {"enemy_1045_hammer_2":[12,0,12,1141,121989],"enemy_1061_zomshd":[48,8,39,8563,888751]},
|
||||
"hooks": [1,122,6900,680,617,5390,5362,63332,13,108,84,128,0,52616,599,14,60,51,1,13,0,0,28],
|
||||
"events": {"atk":617,"deploy":62,"die":57,"dmg":5362,"fx":274,"leak":51,"skill":168,"spawn":118,"status":740},
|
||||
"fx": {"aoe":92,"buff":16,"dodge":28,"lock":48,"summon":46,"telegraph":44},
|
||||
"snaps": "c64f3337 5d2cbbb6 150d0f56 b853826b 0dcc94a0 755987ba 6894eef1 f039fd39 016fbed0 b46c385d 1f1b9146 59d0088f 49eb9bd7 08d48f1d 16ba53f5 bbc18848 9a641c8e 5039a76a f02fcd70 d66c9817 34d21f22 7831aa13 9688a9d5"
|
||||
},
|
||||
"diy-050": {
|
||||
"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=cqbw/S1 6_diy2_a=cqbw/S2 6_diy1_a=cqbw/S3 6_diy2_b=cqbw/S1 6_diy1_b=cqbw/S2 6_diy2_b=cqbw/S3 6_diy1_b=cqbw/S1/002_cqbw 6_diy2_b=cqbw/S2/002_cqbw 6_diy1_b=cqbw/S3/002_cqbw 6_diy2_b=cqbw/S1/003_cqbw 6_diy1_b=cqbw/S2/003_cqbw 6_diy2_b=cqbw/S3/003_cqbw",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m06",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 12,
|
||||
"round": 13,
|
||||
"seed": 574294847,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 214.8,
|
||||
"ticks": 6444,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 209.53,
|
||||
"ticks": 6286,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 60,
|
||||
"killed": 9,
|
||||
"total": 105,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 115,
|
||||
"rngDraws": 66,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":60,"perfect":false,"leaks":60,"leaksUncounted":0,"coins":0,"dmg":157435,"bossDmg":0,"heal":12039,"deaths":23,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":9,"total":105,"perfect":false,"leaks":96,"leaksUncounted":0,"coins":0,"dmg":201296,"bossDmg":0,"heal":0,"deaths":18,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,82,0,28,5208,0,2,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",3371,1,6,0,0,0,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1529,1,9,0,20856,0,0,0,7],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,72,0,174,5712,0,6,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,67,0,13430,5276,0,29,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,78,0,61051,12366,0,48,2],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,85,0,698,3816,0,13,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,84,0,2755,4761,0,39,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,52,0,19051,2744,0,7,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,51,0,39393,3498,0,7,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,47,0,0,2448,1260,3,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",0,0,54,0,0,9324,10779,8,1]
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,126,1,17966,2568,0,21,2],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,112,2,4512,2568,0,15,4],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1284,1,136,1,16198,1284,0,20,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",0,0,2,0,19751,1497,0,9,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,8,1,10145,1497,0,8,3],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,81,1,10194,2994,0,6,0],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_002_cqbw",0,0,77,0,556,3414,0,3,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_002_cqbw",0,0,7,0,9115,1707,0,12,3],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_002_cqbw",0,0,9,1,21861,1707,0,11,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_003_cqbw",0,0,74,0,3309,3394,0,7,0],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_003_cqbw",0,0,89,0,10510,3394,0,8,2],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_003_cqbw",0,0,6,1,29494,1697,0,12,1],
|
||||
["p1","~1","token","token_10008_cqbw_box",null,null,1000,0,21,0,12659,0,0,0,0],
|
||||
["p1","~2","token","token_10008_cqbw_box",null,null,1000,0,22,0,0,0,0,0,0],
|
||||
["p1","~3","token","token_10008_cqbw_box",null,null,1000,0,24,0,564,0,0,0,0],
|
||||
["p1","~4","token","token_10008_cqbw_box",null,null,1000,0,25,0,13053,0,0,0,0],
|
||||
["p1","~5","token","token_10008_cqbw_box",null,null,1000,0,33,1,7836,0,0,0,0],
|
||||
["p1","~6","token","token_10008_cqbw_box",null,null,1000,0,34,0,1986,0,0,0,0],
|
||||
["p1","~7","token","token_10008_cqbw_box",null,null,1000,0,35,0,1014,0,0,0,0],
|
||||
["p1","~8","token","token_10008_cqbw_box",null,null,1000,0,36,0,5421,0,0,0,0],
|
||||
["p1","~9","token","token_10008_cqbw_box",null,null,1000,0,41,0,2462,0,0,0,0],
|
||||
["p1","~10","token","token_10008_cqbw_box",null,null,1000,0,43,0,1110,0,0,0,0],
|
||||
["p1","~11","token","token_10008_cqbw_box",null,null,1000,0,45,0,1014,0,0,0,0],
|
||||
["p1","~12","token","token_10008_cqbw_box",null,null,1000,0,50,0,564,0,0,0,0]
|
||||
],
|
||||
"enemies": {"enemy_1045_hammer_2":[12,0,12,15694,22503],"enemy_1061_zomshd":[48,0,48,39458,134932]},
|
||||
"hooks": [1,85,6444,245,245,412,396,10112,23,83,15,15,0,16629,0,28,60,60,1,26,0,0,16],
|
||||
"events": {"atk":263,"deploy":25,"die":23,"dmg":396,"fx":86,"heal":23,"leak":60,"skill":30,"spawn":72,"status":14},
|
||||
"fx": {"aoe":5,"dodge":16,"dp":7,"slash":56,"spGain":2},
|
||||
"snaps": "580b238a aa91295b 3af047b3 a833e612 f6a0b595 1eba1563 0c047aab 7838a66c 34fc5adc 627a4814 4d7c5d84 41a4b79a 721f8563 f6d5a347 76cda6c2 74e73fdb 220b1dd6 2f7b90e4 d3a2ce26 6a84c6a3 1aae6574 0e8ef572"
|
||||
"enemies": {"enemy_1165_duhond":[51,9,42,917,94127],"enemy_1174_duholy_2":[24,0,24,7469,32467],"enemy_1175_dushdo_2":[30,0,30,19335,74702]},
|
||||
"hooks": [1,136,6286,225,210,394,348,0,27,135,18,30,0,14205,56,22,105,96,1,27,0,0,46],
|
||||
"events": {"atk":210,"deploy":31,"die":39,"dmg":348,"fx":112,"leak":96,"skill":36,"spawn":129,"status":186},
|
||||
"fx": {"aoe":21,"buff":12,"dodge":46,"lock":9,"summon":12,"telegraph":12},
|
||||
"snaps": "46ba2f3b 12647621 9df60d24 442a3969 d3e61660 6e13abf0 964b1d23 455771d5 07681ff6 44d62b8f 13d8742c 286c093e 2d1ce0c4 a25bbf9a 6ba4ea41 9abf6fd7 3182e798 be68ecf4 98b07668 4fc93e64 cb297a56"
|
||||
},
|
||||
"diy-051": {
|
||||
"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=phenxi/S1 5_diy2_a=phenxi/S2 5_diy1_a=phenxi/S3 5_diy2_b=phenxi/S1 5_diy1_b=phenxi/S2 5_diy2_b=phenxi/S3 5_diy1_b=phenxi/S1/002_phenxi 5_diy2_b=phenxi/S2/002_phenxi 5_diy1_b=phenxi/S3/002_phenxi 5_diy2_b=phenxi/S1/003_phenxi 5_diy1_b=phenxi/S2/003_phenxi 5_diy2_b=phenxi/S3/003_phenxi",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m07",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"round": 12,
|
||||
"seed": 3864737019,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 204.8,
|
||||
"ticks": 6144,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 197.17,
|
||||
"ticks": 5915,
|
||||
"reason": "cleared",
|
||||
"killed": 197,
|
||||
"total": 290,
|
||||
"killed": 35,
|
||||
"total": 99,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 282,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":197,"total":290,"perfect":false,"leaks":93,"leaksUncounted":0,"coins":0,"dmg":444885,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":35,"total":99,"perfect":false,"leaks":64,"leaksUncounted":0,"coins":0,"dmg":730338,"bossDmg":0,"heal":0,"deaths":1,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,4,7,21881,2604,0,65,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,7,11,35637,3431,0,87,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,1560.61,1,279,11,28493,2169,0,91,2],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",1890.36,1,277,51,98505,2858,0,110,3],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,241,13,39114,3058,0,54,3],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",0,0,163,43,73993,3808,0,55,4],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,285,5,26351,8078,0,87,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,3,7,19,0,0,16,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,2638,1,5,10,60,0,0,60,3],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",2339.65,1,8,13,64107,1191,0,82,3],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,6,10,18397,0,0,78,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,9,16,38328,0,0,93,2],
|
||||
["p1",1,"op","chess_char_5_diy1_a",0,null,1,1,5,2,23997,1539,0,40,2],
|
||||
["p1",2,"op","chess_char_5_diy2_a",1,null,1,1,7,2,36036,1539,0,45,4],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,1,1,9,2,24072,1539,0,66,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"none",1,1,11,2,40238,1795,0,49,3],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"none",1,1,13,2,37371,1795,0,44,4],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",1,1,6,6,158436,1795,0,73,1],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_002_phenxi",1,1,8,3,13750,1795,0,22,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_002_phenxi",1,1,10,7,157527,1795,0,77,7],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_002_phenxi",1,1,12,0,51934,1795,0,74,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"uniequip_003_phenxi",1,1,14,1,126082,1795,0,74,3],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"uniequip_003_phenxi",1,1,3,6,33613,1795,0,31,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_003_phenxi",357,1,109,2,27282,3235,0,27,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_1195_sfyin": [39,30,9,4143,113683],
|
||||
"enemy_1196_msfyin": [60,32,28,0,65],
|
||||
"enemy_1197_sfshu": [48,38,10,10130,184299],
|
||||
"enemy_1198_msfshu": [114,78,36,0,243],
|
||||
"enemy_1199_sfjin": [18,11,7,12925,146349],
|
||||
"enemy_1200_msfjin": [11,8,3,0,246]
|
||||
},
|
||||
"hooks": [1,314,6144,1470,1470,1969,2082,0,212,305,25,30,0,33234,6218,91,290,93,1,212,0,0,11],
|
||||
"events": {"atk":1512,"deploy":22,"die":212,"dmg":2076,"fx":273,"leak":93,"skill":48,"spawn":304,"status":488},
|
||||
"fx": {"aoe":56,"displace":1,"dodge":22,"dp":63,"slash":52,"summon":79},
|
||||
"snaps": "cf051c06 587dcba3 94ed6adb bba4fd81 815661ad cbf17744 a8d0c19b 32b88916 1e8bde83 51a1d669 b023b185 df725cf4 bf4681aa 2c372e04 1a001766 3d81d3d6 7f8effe5 11826d80 b4fb05d0 0eed6d6c 65f6503d"
|
||||
"enemies": {"enemy_1010_demon":[42,15,27,1,361146],"enemy_1320_wdrrl_2":[12,0,12,0,35526],"enemy_1422_lrsldr_2":[45,20,25,0,333667]},
|
||||
"hooks": [1,114,5915,480,462,1052,8007,0,36,100,31,28,0,404,0,1,99,64,1,36,0,0,0],
|
||||
"events": {"atk":462,"deploy":13,"die":36,"dmg":1052,"fx":395,"leak":64,"skill":59,"spawn":113,"status":26},
|
||||
"fx": {"aoe":287,"zone":108},
|
||||
"snaps": "f6f29ba4 be0c7694 60d96ed2 6395d4a4 1c64bcd2 2e2036da bad037bb 05a37288 c53f8765 a60f21e1 8e7746ff b7a79c37 da35b759 16a2f9e0 99904cb2 b0a48c7d cbb35fb8 26a76139 cb736be1 5a4cb3be"
|
||||
},
|
||||
"diy-052": {
|
||||
"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": "5_diy1_b=phenxi/S1/004_phenxi 5_diy2_b=phenxi/S2/004_phenxi 5_diy1_b=phenxi/S3/004_phenxi 6_diy1_a=phenxi/S1 6_diy2_a=phenxi/S2 6_diy1_a=phenxi/S3 6_diy2_b=phenxi/S1 6_diy1_b=phenxi/S2 6_diy2_b=phenxi/S3 6_diy1_b=phenxi/S1/002_phenxi 6_diy2_b=phenxi/S2/002_phenxi 6_diy1_b=phenxi/S3/002_phenxi",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m01",
|
||||
@@ -2108,35 +2159,306 @@
|
||||
"round": 13,
|
||||
"seed": 1210502478,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 218.57,
|
||||
"ticks": 6557,
|
||||
"time": 218.07,
|
||||
"ticks": 6542,
|
||||
"reason": "cleared",
|
||||
"killed": 0,
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 81,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":76446,"bossDmg":0,"heal":2762,"deaths":33,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":75388,"bossDmg":0,"heal":0,"deaths":19,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,6,0,2101,1865,0,8,0],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,9,0,4660,2494,0,8,0],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,157,0,13058,11540,0,21,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,156,0,24836,14840,0,17,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,0,0,151,0,3559,2544,0,18,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,3,0,11692,1643,0,23,0],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,4,0,9700,1372,0,5,1],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,11,0,1022,1819,0,1,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,86,0,0,4120,2092,4,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,74,0,0,4018,670,2,0],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,1,0,1487,1160,0,3,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,5,0,4330,1450,0,7,1]
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_004_phenxi",0,0,142,0,9768,5643,0,11,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_004_phenxi",0,0,114,0,7282,3762,0,10,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_004_phenxi",0,0,129,0,9768,3762,0,12,0],
|
||||
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,6,0,2187,1540,0,4,0],
|
||||
["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,9,0,2016,1540,0,3,0],
|
||||
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,75,0,6046,3080,0,4,0],
|
||||
["p1",7,"op","chess_char_6_diy2_b",0,"none",0,0,7,0,774,1796,0,3,0],
|
||||
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,73,0,7070,3592,0,3,0],
|
||||
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,8,0,3483,1796,0,5,0],
|
||||
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_phenxi",0,0,69,0,6286,3592,0,2,0],
|
||||
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_phenxi",0,0,1,0,11988,1796,0,8,0],
|
||||
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_phenxi",0,0,5,0,8720,1796,0,7,0]
|
||||
],
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,0,57,29550,4113],"enemy_1299_ymkilr_2":[27,0,27,14576,26756],"enemy_1422_lrsldr_2":[39,0,39,4739,45577]},
|
||||
"hooks": [1,157,6557,277,277,283,291,7,33,156,4,24,0,6272,192,29,123,123,1,33,0,0,0],
|
||||
"events": {"atk":278,"deploy":34,"die":33,"dmg":269,"fx":19,"heal":7,"leak":123,"skill":8,"spawn":135,"status":92},
|
||||
"fx": {"aoe":2,"displace":15,"spGain":2},
|
||||
"snaps": "21be44ba 23e43266 29788d55 2f1037e9 363ad836 051606c5 26acdfeb 8dc54607 7a7c6a86 7ebe59b3 d1661fdb 203d434c 2e82423b f7e9ffc1 7a1afa0e 8998df31 0ca38cb6 f3ac5677 87bcc7cf 0f5f3879 0e082cac 62241cef"
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,0,57,15393,0],"enemy_1299_ymkilr_2":[27,0,27,3995,22169],"enemy_1422_lrsldr_2":[39,0,39,1041,53219]},
|
||||
"hooks": [1,142,6542,138,138,145,2460,0,19,142,0,0,0,72,0,11,123,123,1,19,0,0,0],
|
||||
"events": {"atk":138,"deploy":19,"die":19,"dmg":145,"leak":123,"spawn":135,"status":38},
|
||||
"fx": {},
|
||||
"snaps": "6e8ee454 8edfa242 b7e75010 a83e4be7 4ccef23a 33f34213 8e8eef3b 763d6024 1adb75bb 6576beb6 b6e218b7 9540c94d 261d033e 57278583 15dc49f9 b9d528a4 7f4a723d 72900b79 e3ab5a79 d43bd453 95ce8bec bd6f06ec"
|
||||
},
|
||||
"diy-053": {
|
||||
"about": "6_diy1_b=phenxi/S1/003_phenxi 6_diy2_b=phenxi/S2/003_phenxi 6_diy1_b=phenxi/S3/003_phenxi 6_diy2_b=phenxi/S1/004_phenxi 6_diy1_b=phenxi/S2/004_phenxi 6_diy2_b=phenxi/S3/004_phenxi 5_diy1_a=wisdel/S1 5_diy2_a=wisdel/S2 5_diy1_a=wisdel/S3 5_diy2_b=wisdel/S1 5_diy1_b=wisdel/S2 5_diy2_b=wisdel/S3",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m02",
|
||||
"modeId": "mode_multi_funny",
|
||||
"round": 13,
|
||||
"seed": 2850577873,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 201.77,
|
||||
"ticks": 6053,
|
||||
"reason": "cleared",
|
||||
"killed": 105,
|
||||
"total": 114,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 896,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":105,"total":114,"perfect":false,"leaks":9,"leaksUncounted":0,"coins":0,"dmg":902742,"bossDmg":0,"heal":0,"deaths":4,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_003_phenxi",902,1,7,6,45525,894,0,72,3],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_003_phenxi",902,1,8,1,37108,894,0,73,7],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_003_phenxi",902,1,16,0,6830,894,0,74,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_004_phenxi",1,1,5,9,33468,1910,0,75,3],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_004_phenxi",1,1,6,10,104891,1910,0,78,7],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_004_phenxi",1,1,9,2,87851,1910,0,76,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,1434,1,17,6,23951,0,0,84,16],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,1434,1,138,0,9333,1434,0,31,2],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,117,3,22092,2868,0,35,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",335,1,11,7,42199,1400,0,47,11],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",1735,1,13,24,168190,0,0,58,4],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,3,25,200732,1735,0,57,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],
|
||||
["p1","~1","token","token_10035_wisdel_wward",null,null,3500,1,4,4,21002,0,0,0,37],
|
||||
["p1","~2","token","token_10035_wisdel_wward",null,null,3500,1,12,1,30842,0,0,0,48],
|
||||
["p1","~3","token","token_10035_wisdel_wward",null,null,3500,1,14,3,30594,0,0,0,48],
|
||||
["p1","~4","token","token_10035_wisdel_wward",null,null,3500,1,37,3,19098,0,0,0,33],
|
||||
["p1","~5","token","token_10035_wisdel_wward",null,null,3500,1,38,1,19036,0,0,0,31]
|
||||
],
|
||||
"enemies": {"enemy_1381_winman":[27,27,0,0,150336],"enemy_1381_winman_2":[48,48,0,96,359424],"enemy_1387_winshd":[39,30,9,7341,392982]},
|
||||
"hooks": [1,135,6053,699,685,3204,5313,0,109,118,255,252,18,35466,695,13,114,9,1,109,0,0,0],
|
||||
"events": {"atk":786,"deploy":19,"die":109,"dmg":3204,"fx":623,"leak":9,"skill":507,"spawn":133,"status":797},
|
||||
"fx": {"aoe":330,"camouflage":1,"overload":6,"strike":197,"summon":5,"zone":84},
|
||||
"snaps": "fdcd038f 485cf328 d0601711 6d9af9b1 a9bb142f e3875ce2 b406d31a 6015188f e29a5361 30975489 87121454 df338ffd 8cd931c0 cfc62427 20498486 62e7e568 516e35e7 7ea984e7 5452357d a13dbfd1 b96962f5"
|
||||
},
|
||||
"diy-054": {
|
||||
"about": "5_diy1_b=wisdel/S1/002_wisdel 5_diy2_b=wisdel/S2/002_wisdel 5_diy1_b=wisdel/S3/002_wisdel 6_diy1_a=wisdel/S1 6_diy2_a=wisdel/S2 6_diy1_a=wisdel/S3 6_diy2_b=wisdel/S1 6_diy1_b=wisdel/S2 6_diy2_b=wisdel/S3 6_diy1_b=wisdel/S1/002_wisdel 6_diy2_b=wisdel/S2/002_wisdel 6_diy1_b=wisdel/S3/002_wisdel",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m03",
|
||||
"modeId": "mode_single_hard",
|
||||
"round": 13,
|
||||
"seed": 196461732,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 43,
|
||||
"total": 66,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":43,"total":66,"perfect":false,"leaks":23,"leaksUncounted":0,"coins":0,"dmg":422223,"bossDmg":0,"heal":0,"deaths":6,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_wisdel",540.29,1,11,0,0,1195,0,0,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_wisdel",1699.64,1,112,0,0,1770,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_wisdel",1735,1,107,7,51814,1735,0,6,2],
|
||||
["p1",4,"op","chess_char_6_diy1_a",0,null,1434,1,101,0,0,1434,0,0,0],
|
||||
["p1",5,"op","chess_char_6_diy2_a",1,null,1434,1,4,0,0,0,0,0,0],
|
||||
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,13,6,77771,1434,0,10,2],
|
||||
["p1",7,"op","chess_char_6_diy2_b",0,"none",1735,1,6,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_6_diy1_b",1,"none",1735,1,9,0,0,0,0,0,0],
|
||||
["p1",9,"op","chess_char_6_diy2_b",2,"none",1735,1,15,12,151201,0,0,12,3],
|
||||
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_wisdel",1717.32,1,111,0,0,1753,0,0,0],
|
||||
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_wisdel",1137.65,1,105,0,0,2332,0,0,0],
|
||||
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_wisdel",1735,1,7,18,141437,0,0,12,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],
|
||||
["p1","~1","token","token_10035_wisdel_wward",null,null,0,0,5,0,0,3500,0,0,0],
|
||||
["p1","~2","token","token_10035_wisdel_wward",null,null,0,0,8,0,0,3500,0,0,0],
|
||||
["p1","~3","token","token_10035_wisdel_wward",null,null,3500,1,10,0,0,0,0,0,0],
|
||||
["p1","~4","token","token_10035_wisdel_wward",null,null,0,0,12,0,0,3500,0,0,0],
|
||||
["p1","~5","token","token_10035_wisdel_wward",null,null,0,0,14,0,0,3500,0,0,0],
|
||||
["p1","~6","token","token_10035_wisdel_wward",null,null,0,0,16,0,0,3500,0,0,0],
|
||||
["p1","~7","token","token_10035_wisdel_wward",null,null,0,0,18,0,0,3500,0,0,0],
|
||||
["p1","~8","token","token_10035_wisdel_wward",null,null,0,0,21,0,0,3500,0,0,0],
|
||||
["p1","~9","token","token_10035_wisdel_wward",null,null,0,0,41,0,0,3500,0,0,0],
|
||||
["p1","~10","token","token_10035_wisdel_wward",null,null,0,0,42,0,0,3500,0,0,0],
|
||||
["p1","~11","token","token_10035_wisdel_wward",null,null,0,0,76,0,0,3500,0,0,0],
|
||||
["p1","~12","token","token_10035_wisdel_wward",null,null,0,0,77,0,0,3500,0,0,0],
|
||||
["p1","~13","token","token_10035_wisdel_wward",null,null,0,0,82,0,0,3500,0,0,0],
|
||||
["p1","~14","token","token_10035_wisdel_wward",null,null,2784.35,1,86,0,0,716,0,0,0],
|
||||
["p1","~15","token","token_10035_wisdel_wward",null,null,1040.31,1,87,0,0,2460,0,0,0],
|
||||
["p1","~16","token","token_10035_wisdel_wward",null,null,1340.16,1,102,0,0,2160,0,0,0],
|
||||
["p1","~17","token","token_10035_wisdel_wward",null,null,2981.12,1,108,0,0,519,0,0,0],
|
||||
["p1","~18","token","token_10035_wisdel_wward",null,null,3500,1,110,0,0,0,0,0,0],
|
||||
["p1","~19","token","token_10035_wisdel_wward",null,null,3500,1,113,0,0,0,0,0,0]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[12,11,0,1395,29473],"enemy_1041_lazerd_2":[24,16,8,58112,171588],"enemy_1042_frostd":[30,16,14,0,221161]},
|
||||
"hooks": [1,105,6900,272,272,409,409,0,61,83,9,7,40,18780,0,0,66,22,1,61,0,0,0],
|
||||
"events": {"atk":272,"deploy":36,"die":61,"dmg":409,"fx":25,"leak":22,"skill":16,"spawn":100,"status":191},
|
||||
"fx": {"camouflage":6,"summon":19},
|
||||
"snaps": "29d9a0dd ac133a97 1cf24938 f19a3df7 63924875 c479dade d5d847aa ff660b56 2cc41b2e 4630f701 6959661e ae8b045c 77ed22e9 4eb31003 b2497731 c4b93c15 6a01a5ad ec1df7d2 2e15fdc5 e7b7621a 710bf722 8cfd7316 8e336506"
|
||||
},
|
||||
"diy-055": {
|
||||
"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,
|
||||
"kind": "normal",
|
||||
"stageId": "act2autochess_m04",
|
||||
"modeId": "mode_multi_normal",
|
||||
"round": 12,
|
||||
"seed": 1597707536,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 226.43,
|
||||
"ticks": 6793,
|
||||
"reason": "cleared",
|
||||
"killed": 8,
|
||||
"total": 117,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":8,"total":117,"perfect":false,"leaks":109,"leaksUncounted":0,"coins":0,"dmg":52598,"bossDmg":0,"heal":8900,"deaths":25,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,117,0,0,6078,0,0,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"none",0,0,111,0,0,7823,0,0,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,144,0,0,5128,0,0,0],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"none",208.49,1,145,0,0,10902,0,0,0],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,132,0,18064,2542,0,48,1],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"none",868.62,1,146,5,21138,3661,0,44,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,86,1,660,2480,0,2,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"none",0,0,79,1,4810,2924,0,9,0],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,5,0,0,1218,5678,12,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"none",0,0,92,0,0,4841,3223,7,1],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,142,0,3950,2796,0,11,0],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,135,1,3976,4845,0,9,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]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai":[51,8,43,0,18454],"enemy_1005_yokai_2":[54,0,54,21039,2515],"enemy_1005_yokai_3":[12,0,12,34200,31629]},
|
||||
"hooks": [1,146,6793,492,492,425,425,19,33,142,4,4,0,11479,18,0,117,109,1,33,0,0,0],
|
||||
"events": {"atk":492,"deploy":27,"die":33,"dmg":425,"heal":19,"leak":109,"skill":8,"spawn":131,"status":28},
|
||||
"fx": {},
|
||||
"snaps": "e90d8ac5 3e7826d8 2ea79392 0d0a30f6 55810e9b a62c5a87 b1c5cfca 6959234f 84a973da 3386636e fa19929f 31ac9ec4 cc1b23bc 0ce6593e 1aa312f9 8de415cc 7670e151 f5f13c6f 12c4e5a4 75fb7a93 385d158a f1ce79bb 455d3b6d"
|
||||
},
|
||||
"diy-056": {
|
||||
"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",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m01",
|
||||
"modeId": "mode_multi_hard",
|
||||
"round": 12,
|
||||
"seed": 3238296381,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 208.03,
|
||||
"ticks": 6241,
|
||||
"reason": "cleared",
|
||||
"killed": 3,
|
||||
"total": 75,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 196,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":3,"total":75,"perfect":false,"leaks":72,"leaksUncounted":0,"coins":0,"dmg":162615,"bossDmg":0,"heal":9891,"deaths":23,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_a",2,null,0,0,7,0,249,2808,0,3,0],
|
||||
["p1",2,"op","chess_char_5_diy2_b",2,"uniequip_002_acfend",0,0,9,1,600,3371,0,5,0],
|
||||
["p1",3,"op","chess_char_5_diy1_a",2,null,0,0,97,1,12118,4587,0,5,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",2,"uniequip_002_acpion",0,0,101,0,24851,5712,0,11,1],
|
||||
["p1",5,"op","chess_char_5_diy1_a",2,null,0,0,68,0,6186,5276,0,8,0],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_002_acguad",0,0,8,0,35092,8965,0,13,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,0,0,75,0,17393,2748,0,33,1],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acnipe",0,0,84,1,48820,3924,0,40,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,61,0,8325,3344,0,4,0],
|
||||
["p1",10,"op","chess_char_5_diy2_b",2,"uniequip_002_accast",0,0,55,0,8980,3498,0,4,0],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,0,0,93,0,0,3198,8062,15,1],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acmedc",1414,1,102,0,0,4657,1829,3,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_1267_nhpbr_2":[36,2,34,5542,68908],"enemy_1270_nhstlk_2":[27,1,26,12814,85923],"enemy_1272_nhtank_2":[12,0,12,33732,7784]},
|
||||
"hooks": [1,102,6241,222,222,529,526,32,26,98,6,6,0,8985,0,21,75,72,1,56,0,0,3],
|
||||
"events": {"atk":261,"deploy":24,"die":26,"dmg":526,"fx":42,"heal":32,"leak":72,"skill":12,"spawn":90,"status":32},
|
||||
"fx": {"dodge":3,"dp":3,"slash":16,"spGain":2,"zone":18},
|
||||
"snaps": "4a66aa50 cd957f8d 29040588 85dbfdfb 24a06993 403a5cd0 af8c7db4 ece6f6e6 e208e92d b00a9483 cdbb6067 b86efe8c 4d65b9e6 9e9b8ccc 4dd884e0 de6f0bb5 1ee54cb6 187ba789 edc6ec98 27566532 7101f464"
|
||||
},
|
||||
"diy-057": {
|
||||
"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",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m02",
|
||||
"modeId": "mode_single_normal",
|
||||
"round": 13,
|
||||
"seed": 583790378,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 229.57,
|
||||
"ticks": 6887,
|
||||
"reason": "cleared",
|
||||
"killed": 25,
|
||||
"total": 147,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 349,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":25,"total":147,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":447188,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",2,null,0,0,144,0,5014,5208,0,37,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",2,"uniequip_002_acfend",0,0,1,0,7239,3431,0,80,2],
|
||||
["p1",3,"op","chess_char_5_diy1_a",0,null,0,0,143,0,12621,3730,0,59,1],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_002_sharp2",0,0,139,5,129318,4748,0,83,2],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1529,1,176,2,19391,3058,0,28,2],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acpion",1904,1,174,8,121508,3808,0,33,4],
|
||||
["p1",7,"op","chess_char_5_diy1_a",2,null,1154,1,165,0,5198,6924,0,54,0],
|
||||
["p1",8,"op","chess_char_5_diy2_b",2,"uniequip_002_acspec",1448,1,171,0,7386,4344,0,52,0],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,2638,1,177,2,17400,2638,0,21,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acguad",0,0,146,6,70277,7062,0,82,2],
|
||||
["p1",11,"op","chess_char_5_diy1_a",2,null,1160,1,6,0,21302,0,0,139,3],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"uniequip_002_acsupo",1430,1,8,2,30532,0,0,150,3]
|
||||
],
|
||||
"enemies": {"enemy_1166_dusbr_2":[39,10,29,2081,102398],"enemy_1169_duphlx_2":[57,9,48,10670,156040],"enemy_1170_dushld_2":[51,6,45,32200,188750]},
|
||||
"hooks": [1,177,6887,1087,1087,1627,1734,0,48,170,21,28,0,26130,14208,52,147,122,1,48,0,0,14],
|
||||
"events": {"atk":1145,"deploy":30,"die":48,"dmg":1705,"fx":155,"leak":122,"skill":40,"spawn":159,"status":1199},
|
||||
"fx": {"aoe":51,"displace":8,"dodge":32,"dp":16,"slash":48},
|
||||
"snaps": "3c92533d c853aee7 5b1063fd 5fc57b7e 4a05e8e6 38a71bec 3070ca8c d432fa88 49683316 30a3854a 9104d2b4 923ae15a 22aa5459 cdd051aa 9f802034 82bafa84 ccf4b89d e3e17beb 6eb62b58 fe7abec6 989ccd10 d648552b 98e9bf71"
|
||||
},
|
||||
"diy-058": {
|
||||
"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",
|
||||
"fast": false,
|
||||
"kind": "normal",
|
||||
"stageId": "act1autochess_m03",
|
||||
"modeId": "mode_multi_abyss",
|
||||
"round": 13,
|
||||
"seed": 2224511831,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 208.07,
|
||||
"ticks": 6242,
|
||||
"reason": "cleared",
|
||||
"killed": 2,
|
||||
"total": 105,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 148,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":2,"total":105,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":0,"dmg":240102,"bossDmg":0,"heal":14281,"deaths":30,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,5,0,14676,1865,0,12,1],
|
||||
["p1",2,"op","chess_char_6_diy2_b",0,"uniequip_002_sharp2",0,0,7,1,65680,3799,0,21,1],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1154,1,140,0,19129,12041,0,30,0],
|
||||
["p1",4,"op","chess_char_6_diy2_b",2,"uniequip_002_acspec",0,0,139,0,38020,15591,0,35,0],
|
||||
["p1",5,"op","chess_char_6_diy1_a",2,null,1272,1,131,0,5981,1272,0,26,0],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_acnipe",0,0,12,0,8584,1643,0,26,0],
|
||||
["p1",7,"op","chess_char_6_diy1_a",2,null,0,0,78,0,5748,4303,0,7,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",2,"uniequip_002_accast",0,0,4,1,75199,1819,0,10,1],
|
||||
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,6,0,0,2056,5589,7,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",2,"uniequip_002_acmedc",0,0,9,0,0,5567,8692,11,1],
|
||||
["p1",11,"op","chess_char_6_diy1_a",2,null,0,0,13,0,3038,1160,0,13,0],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_002_acsupo",0,0,73,0,4048,5494,0,11,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_1229_darmy":[27,1,26,5045,88057],"enemy_1229_darmy_2":[57,1,56,19387,116235],"enemy_1275_dwlock_2":[21,0,21,32176,35810]},
|
||||
"hooks": [1,140,6242,294,294,501,589,31,32,135,6,24,0,7426,388,24,105,103,1,32,0,4,3],
|
||||
"events": {"atk":328,"deploy":32,"die":32,"dmg":541,"fx":78,"heal":27,"leak":103,"skill":12,"spawn":120,"status":178},
|
||||
"fx": {"aoe":8,"beam":24,"burst":4,"displace":25,"dodge":5,"spGain":12},
|
||||
"snaps": "c9dff647 505536a6 046d3d99 29e1ce4f 95f692ed cb471e6e 3e929e64 72083a81 77cee0d9 d920f33c d26b1231 10d91924 2bc3222a 3b3d8eb1 a41709a6 a48f0d23 774d80e5 2256086c dbd65fc9 29f0c70c fa23ead3"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user