feedback5: merge WE1 — fidelity (sim rules): 秘术师 store at the attack check (深靛; 维伊 / 黑键 plug into it), enemy attacks land at their damage frame so a short stun / freeze / sleep / 浮空 / 缴械 / 恐惧 before it cancels the swing, 武者 heal per damage instance through 禁疗, 庇护 the strongest only, PHY-X 不高于 50 %, 链愈师 jumps in the 3×3, a summon readied at its owner's redeploy, Misery's duration skills, 伺夜 S2 in 战术点形态, centurions hit up to block-count targets; merge fixes: 黑键's kit keeps the up() import WE2's S2 cast needs (WE1 dropped it with its tick store — the first regeneration had stored 9288 swallowed errors in diy-114), the 自选 refusal names 集成战略 / 生息演算, tools/golden.mjs prints a missing value, test/golden.test.js fails on any stored engine error; golden: vs WE1 only the W4B / WE2 scenarios differ (roster-042, bond-egirShip-high, coop2-ABYSS-10-boosted, coop2-NORMAL-70-diy, diy-045 / 078 / 114 / 115 / 116)
This commit is contained in:
commit
e2bb090f44
60 files changed
+6888
-6103
No files matched your search
@@ -11,6 +11,7 @@ import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.
|
||||
import * as enemiesMod from '../../server/sim/content/enemies.js';
|
||||
import * as bossesMod from '../../server/sim/content/bosses.js';
|
||||
import { spawnYanyou } from '../../server/sim/content/tokens.js';
|
||||
import { attackWindup } from '../../server/sim/ai.js';
|
||||
|
||||
const E = JSON.parse(fs.readFileSync(new URL('../../data/enemies.json', import.meta.url), 'utf8'));
|
||||
const W = JSON.parse(fs.readFileSync(new URL('../../data/waves.json', import.meta.url), 'utf8'));
|
||||
@@ -291,8 +292,9 @@ for (const [key, n] of Object.entries(DEATH_SPAWN)) {
|
||||
|
||||
for (const key of ['enemy_1207_sfji', 'enemy_1207_sfji_2']) {
|
||||
// PRTS 天赋 "攻击力+X%,持有4个【断刃】 / 每次成功攻击后消耗1个【断刃】,攻击结束时若已耗尽【断刃】,则立刻切换为无断刃模式并失去攻击力
|
||||
// 加成" (popup: "清空当次攻击间隔" — it attacks again at once); GitHub #107: until 0.2.0 each attack stacked another layer
|
||||
test(`${nm(key)}: one ATK layer while it holds a blade, gone with the 4th attack (which is followed at once by the next); unspent blades become 矛头 on death (at least 1)`, () => {
|
||||
// 加成" (popup: "清空当次攻击间隔" — it attacks again at once: since 0.2.0 its next swing starts at once and strikes at the
|
||||
// clip's damage frame, ai.js attackWindup); GitHub #107: until 0.2.0 each attack stacked another layer
|
||||
test(`${nm(key)}: one ATK layer while it holds a blade, gone with the 4th attack (which is followed at once by the next swing); unspent blades become 矛头 on death (at least 1)`, () => {
|
||||
const per = tb(key, 'Atkup.atk') ?? tb(key, 'AtkUp.atk'), cnt = tb(key, 'DeadSpawn.cnt');
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 9, col: 5 }] });
|
||||
h.step();
|
||||
@@ -304,8 +306,10 @@ for (const key of ['enemy_1207_sfji', 'enemy_1207_sfji_2']) {
|
||||
h.runUntil(() => e.stats.attacks >= cnt, 20);
|
||||
approx(e.s.atk, base, 1e-6, 'the last blade spent: 无断刃模式, no bonus');
|
||||
const tLast = h.b.time;
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= cnt + 1, 0.2), 'the mode switch clears the attack interval: the next attack at once');
|
||||
assert.ok(h.b.time - tLast < e.s.interval / 2, `${h.b.time - tLast} s after the 4th attack`);
|
||||
const wind = attackWindup(e);
|
||||
assert.ok(wind > 0 && h.runUntil(() => e.stats.attacks >= cnt + 1, wind + 0.2), 'the mode switch clears the attack interval: the next swing at once');
|
||||
assert.ok(Math.abs(h.b.time - tLast - wind) <= 3 * h.TICK, `${h.b.time - tLast} s after the 4th attack: its wind-up (${wind} s)`);
|
||||
assert.ok(h.b.time - tLast < e.s.interval / 2);
|
||||
const child = E[key].talents.bbStr['DeadSpawn.enemy_key'];
|
||||
killed(h, e, null);
|
||||
assert.equal(alive(h, child).length, 1, 'no blade left: still one 矛头');
|
||||
@@ -1289,6 +1293,7 @@ test(`${nm('enemy_10027_vtsk')}: entrance barrage (${skb('enemy_10027_vtsk', 'Ap
|
||||
assert.ok(barrage.every((c) => c.target === h.unit('t_wall')), 'the highest-HP unit');
|
||||
approx(barrage[0].amount, e.s.atk);
|
||||
h.runUntil(() => e.stats.attacks >= 1, 10);
|
||||
h.run(1); // (its shot lands: since 0.2.0 the first strike comes at the clip's damage frame, after the barrage window)
|
||||
const d = h.hooksOf('damaged').find((c) => c.source === e && c.dmg.isAttack);
|
||||
approx(d.amount, e.s.atk * tb('enemy_10027_vtsk', 'range.attack@atk_scale_range'));
|
||||
h.run(skb('enemy_10027_vtsk', 'MultiCombat').initCooldown + 6);
|
||||
@@ -2412,7 +2417,8 @@ test('“斩胄之剑” / “破胄之锤” 初始模式 attack: every ally in
|
||||
const h = bossArena({ units: [{ chessId: 't_wall', row: 10, col: 9 }, { chessId: 't_wall2', row: 10, col: 7 }] });
|
||||
h.step();
|
||||
put(h, key, [3, 8], { tag: 'part' });
|
||||
h.run(1);
|
||||
// (the first attack: its 0.8 / 0.867 s wind-up, then the shots — since 0.2.0 an attack strikes at its damage frame)
|
||||
h.runUntil(() => h.unit('t_wall').stats.taken > 0 && h.unit('t_wall2').stats.taken > 0, 3);
|
||||
approx(h.unit('t_wall').stats.taken, E[key].stats.atk * scale, 1e-6, key);
|
||||
approx(h.unit('t_wall2').stats.taken, E[key].stats.atk * scale, 1e-6, key);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// test/content/feedback5-lowhp-heal.test.js — the module trait "治疗生命值低于50%的友方单位时治疗量提升15%" (PHY-X / GUA-X) compares the
|
||||
// target's HP the way the client's module buff does (follow-up 19): FilterByTargetHpRatio LT — strictly below — for 瑕光 /
|
||||
// 古米 / 塞雷娅's GUA-X (set_heal_scale_by_hpratio) and 录武官's PHY-X (reckpr_e_002_tr); LE — at or below — for the template
|
||||
// heal_scale_up[hpratio][LE] of 华法琳's and 闪灵's PHY-X (and 黍's GUA-X, 凯尔希's PHY-X, already so). The text's 「低于」 is the
|
||||
// display; the buff templates decide (the local client's [uc]equips.ab prefabs and buff_template_data, read 2026-10-06).
|
||||
// Run: node --test test/content/feedback5-lowhp-heal.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const WALL = chessRec({ id: 't_wall', stats: { maxHp: 1e6, atk: 0, def: 0 }, skill: null });
|
||||
|
||||
/** The bonus factor `healer` gives a heal on an ally at `ratio` of its max HP (1 = none). */
|
||||
function factorAt(unitSpec, ratio) {
|
||||
const h = makeBattle({ defs: { chess: { t_wall: WALL } }, units: [unitSpec, { chessId: 't_wall', row: 11, col: 5 }], autoFinish: false, timeLimit: 30, seed: 3 });
|
||||
h.step();
|
||||
const healer = h.b.allyUnits.find((u) => u.uid === 1), t = h.unit('t_wall');
|
||||
t.hp = t.s.maxHp * ratio;
|
||||
const before = t.hp;
|
||||
const base = 100 * healer.s.healingDealtMul * t.s.healingTakenMul;
|
||||
h.b.heal(healer, t, 100);
|
||||
checkInvariants(h.b);
|
||||
return (t.hp - before) / base;
|
||||
}
|
||||
const near = (a, b, msg) => assert.ok(Math.abs(a - b) < 1e-9, `${msg}: ${a} vs ${b}`);
|
||||
|
||||
test('LE (heal_scale_up[hpratio][LE]): 华法琳 (tier 4 and 5) and 闪灵 PHY-X heal an ally at exactly 50 % HP ×1.15', () => {
|
||||
for (const id of ['chess_char_4_26_b', 'chess_char_5_04_b']) {
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.5), 1.15, `${id} at 50 %`);
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.6), 1, `${id} at 60 %`);
|
||||
}
|
||||
const shining = { uid: 1, diy: { slot: 6, charId: 'char_147_shining', skillIndex: 0, uniEquipId: 'uniequip_003_shining' }, elite: true, row: 10, col: 5 };
|
||||
near(factorAt(shining, 0.5), 1.15, '闪灵 PHY-X at 50 %');
|
||||
near(factorAt(shining, 0.6), 1, '闪灵 PHY-X at 60 %');
|
||||
});
|
||||
|
||||
test('LT (set_heal_scale_by_hpratio, reckpr_e_002_tr): 塞雷娅 GUA-X and 录武官 PHY-X (tier 4 and 5) give nothing at exactly 50 %, ×1.15 below', () => {
|
||||
for (const id of ['chess_char_5_11_b', 'chess_char_4_15_b', 'chess_char_5_23_b']) {
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.5), 1, `${id} at 50 %`);
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.49), 1.15, `${id} at 49 %`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
// test/content/feedback5-protect.test.js — 庇护 is one effect per unit whoever grants it (gamedata_const ba.protect / PRTS 术语
|
||||
// 庇护 "受到的物理和法术伤害降低相应比例(同名效果取最高)"): the older kits — 宴, 余, 缪尔赛思 (chess), 赫拉格, 左乐, 赫德雷 (自选) —
|
||||
// hold it through the shared key the other 自选 kits use (kits/shared/tier1.js PROTECT / holdProtect → battle.applyStrongest),
|
||||
// so the strongest value applies instead of a product of every source. 遥's bubbles keep their own key (the client's
|
||||
// damage_resistance[bonus], which multiplies with the common damage_resistance[inf]).
|
||||
// Run: node --test test/content/feedback5-protect.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 { PROTECT, holdProtect } from '../../server/sim/content/kits/shared/tier1.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
|
||||
const ds = getDefaultSource();
|
||||
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 done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
|
||||
|
||||
/** 宴 (elite: 庇护 below hp_ratio) at 20 % HP with nothing to hit her. */
|
||||
function utage() {
|
||||
const h = makeBattle({ units: [{ chessId: 'chess_char_1_18_b', row: 9, col: 5 }], autoFinish: false, timeLimit: 60, seed: 3 });
|
||||
h.step();
|
||||
const u = h.unit('chess_char_1_18_b');
|
||||
u.hp = u.s.maxHp * 0.2;
|
||||
h.step();
|
||||
return { h, u, dr: ds.rawChess('chess_char_1_18_b').talents.filter((t) => t.index !== -1)[0].bb.damage_resistance };
|
||||
}
|
||||
/** Physical damage she takes from a 1000-point hit (her DEF taken off). */
|
||||
const physTaken = (h, u) => { const hp = u.hp; h.b.dealDamage(null, u, { amount: 1000, type: 'phys', canDodge: false }); const d = hp - u.hp; u.hp = hp; return d; };
|
||||
|
||||
test('宴 below her threshold holds the shared 庇护; a stronger one from another source replaces it (no product), a weaker one waits under it', () => {
|
||||
const { h, u, dr } = utage();
|
||||
assert.ok(dr > 0 && dr < 0.5);
|
||||
const b = u.findBuff(PROTECT);
|
||||
assert.ok(b && b.source === u && b.data.value === dr, 'her own 庇护, on the shared key');
|
||||
const base = 1000 - u.s.def;
|
||||
approx(physTaken(h, u), base * (1 - dr), 'hers alone');
|
||||
// a stronger 庇护 from another source: the highest counts
|
||||
holdProtect(h.b, u, 0.5, 10, { id: 'other' });
|
||||
h.step();
|
||||
approx(physTaken(h, u), base * (1 - 0.5), 'the stronger one only — not ×(1 − dr) as well');
|
||||
assert.equal(u.buffs.filter((x) => x.key === PROTECT).length, 1, 'one 庇护 effect');
|
||||
done(h);
|
||||
// a weaker one: hers holds
|
||||
const w = utage();
|
||||
holdProtect(w.h.b, w.u, 0.1, 10, { id: 'other' });
|
||||
w.h.step();
|
||||
approx(physTaken(w.h, w.u), (1000 - w.u.s.def) * (1 - w.dr), 'hers — the weaker 庇护 waits');
|
||||
// back above the threshold her own lapses within a tick; the weaker one resumes for its remaining time
|
||||
w.u.hp = w.u.s.maxHp;
|
||||
w.h.step(3);
|
||||
const after = w.u.findBuff(PROTECT);
|
||||
assert.ok(after && after.data.value === 0.1, 'the weaker 庇护 resumed');
|
||||
done(w.h);
|
||||
});
|
||||
|
||||
test('余 (blocking), 缪尔赛思\'s melee copies, 赫拉格 / 左乐 (SBL-X, below 50 %) and 赫德雷 (余火之氅) all hold 庇护 on the shared key', () => {
|
||||
const src = (f) => readFileSync(new URL(`../../server/sim/content/kits/ops/${f}`, import.meta.url), 'utf8');
|
||||
for (const f of ['chess_char_1_18-utage.js', 'chess_char_6_03-yu.js', 'chess_char_6_11-mlyss.js', 'op-helage.js', 'op-zuole.js', 'op-hodrer.js']) {
|
||||
assert.match(src(f), /holdProtect\(/, `${f} uses the shared 庇护`);
|
||||
assert.doesNotMatch(src(f), /physTakenMul: 1 - dr|dmg\.mul \*= 1 - (dr|protectCut|num\(t\.damage_resistance\))/, `${f}: no private 庇护 multiplier left`);
|
||||
}
|
||||
// 遥's bubble stays its own effect (damage_resistance[bonus])
|
||||
assert.doesNotMatch(src('op-haruka.js'), /holdProtect\(/);
|
||||
// 赫拉格 (SBL-X, tier 5) at 40 % HP: his 庇护 on the shared key
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { e: enemyRec({ key: 'e', hp: 1e9, speed: 0 }) } },
|
||||
units: [{ diy: { slot: 5, charId: 'char_188_helage', skillIndex: 0, uniEquipId: 'uniequip_002_helage' }, elite: true, row: 10, col: 5 }],
|
||||
autoFinish: false, timeLimit: 30, seed: 3,
|
||||
});
|
||||
h.step();
|
||||
const u = h.b.allyUnits.find((x) => x.def?.charId === 'char_188_helage');
|
||||
u.hp = u.s.maxHp * 0.4;
|
||||
h.step();
|
||||
const b = u.findBuff(PROTECT);
|
||||
assert.ok(b && b.source === u && b.data.value === 0.25, 'his SBL-X 庇护');
|
||||
u.hp = u.s.maxHp;
|
||||
h.step(3);
|
||||
assert.equal(u.findBuff(PROTECT), null, 'gone above 50 %');
|
||||
done(h);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
// test/content/feedback5-summon-recharge.test.js — one reading of the summoners' charge_token[born] (follow-up 20): the client's
|
||||
// RechargeToken on the owner's birth runs with timing NORMAL — the summon is ready at once —, while the summon's own
|
||||
// charge_token[finish] (ON_FINISH) is the timer that runs from its deployment (PRTS 傀影 备注 "部署召唤物后立刻开始计算再部署时间").
|
||||
// 傀影's 镜中虚影 (op-phatom.js) now reads it as 凯尔希's Mon3tr (op-kalts.js) and the other summoner kits do: when the owner
|
||||
// is deployed again, a summon still waiting for its own timer comes back at once (paying its cost).
|
||||
// Run: node --test test/content/feedback5-summon-recharge.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
/** The owner (uid 1, a tier-5 自选 piece) and its placed summon (uid 2). */
|
||||
function field(charId, tokenId) {
|
||||
const h = makeBattle({
|
||||
timeLimit: 900, autoFinish: false, seed: 5, flags: { dpInit: 99, dpPerSec: 0, dpMax: 999 },
|
||||
units: [{ uid: 1, diy: { slot: 'chess_char_5_diy1_a', charId, skillIndex: 0 }, row: 10, col: 2 }, { uid: 2, kind: 'token', tokenId, ownerUid: 1, row: 10, col: 4 }],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1), t: h.unit(2) };
|
||||
}
|
||||
|
||||
for (const [name, charId, tokenId] of [['傀影 / 镜中虚影', 'char_250_phatom', 'token_10007_phatom_twin'], ['凯尔希 / Mon3tr', 'char_003_kalts', 'token_10002_kalts_mon3tr']]) {
|
||||
test(`${name}: the owner's redeployment readies a waiting summon at once (charge_token[born], timing NORMAL); its own timer still holds it while the owner stands`, () => {
|
||||
const { h, u, t } = field(charId, tokenId);
|
||||
assert.ok(u.alive && t.alive, 'both deployed');
|
||||
h.run(2);
|
||||
// knocked out with the owner standing: its own timer (from its deployment) holds it
|
||||
h.b.kill(t, null);
|
||||
h.run(3);
|
||||
assert.ok(!t.alive, 'waits for its own timer');
|
||||
// the owner leaves and comes back long before that timer has run
|
||||
h.b.retreat(u);
|
||||
assert.ok(h.b.redeploy(u, { free: true }), 'the owner back');
|
||||
const dp = h.b.players[0].dp;
|
||||
assert.ok(h.runUntil(() => t.alive, 1), 'the summon comes back at once with the owner');
|
||||
assert.ok(h.b.time < 10, `at ${h.b.time.toFixed(2)} s — its own timer (≥ 25 s) has not run`);
|
||||
assert.equal(h.b.players[0].dp, dp - t.base.cost, 'paying its cost');
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
});
|
||||
}
|
||||
@@ -1036,7 +1036,7 @@ test('3_19 伺夜 S2 领袖的馈赠: +DP, the pack recovers HP, its next attack
|
||||
}
|
||||
});
|
||||
|
||||
test('3_19 伺夜 S2 领袖的馈赠 (自动触发): no enemy needed — cast at full SP while the pack stands and holds no unused gift (PRTS 备注), never while it is in its 战术点形态 [ASSUMED] — cast once it is back (kit pack and prep piece)', () => {
|
||||
test('3_19 伺夜 S2 领袖的馈赠 (自动触发): no enemy needed — cast at full SP while the pack is on the field and holds no unused gift (PRTS 备注), in its 战术点形态 too [ASSUMED: S1\'s words] — the DP at once, the gift kept for the returning pack (kit pack and prep piece)', () => {
|
||||
for (const id of BOTH('chess_char_3_19_a')) for (const piece of [false, true]) {
|
||||
const b = SB(id, 'skchr_vigil_2'), tag = `${id} ${piece ? 'piece' : 'kit pack'}`;
|
||||
const h = makeBattle({ defs: { chess: noGarrison(id) }, timeLimit: 60, flags: { dpPerSec: 0 }, units: vigilUnits(id, 'skchr_vigil_2', piece) });
|
||||
@@ -1052,17 +1052,20 @@ test('3_19 伺夜 S2 领袖的馈赠 (自动触发): no enemy needed — cast at
|
||||
fill(u);
|
||||
h.run(3);
|
||||
assert.equal(u.skill.activations, 1, '"狼群未获得此技能的充能时可触发": no second cast while the gift is unused');
|
||||
// the gift spent (as by a bite), the pack in its 战术点形态: no cast (the 备注 does not count that form for S2 —
|
||||
// [ASSUMED]); once it is back, the waiting cast fires
|
||||
// the gift spent (as by a bite), the pack in its 战术点形态: the cast fires there too (its 备注 uses S1's words, which count
|
||||
// that form — [ASSUMED]; until 0.2.0 it waited for the pack): +cost DP at once, the gift waits on the pack
|
||||
w.mem.vigilGift = null;
|
||||
for (let i = 0; i < 10 && w.alive; i++) h.b.dealDamage(null, w, { amount: 1e9, type: 'true' }); // a wolf is lost per KO
|
||||
assert.ok(wolfTacticalPoint(w), `${tag}: 战术点形态`);
|
||||
h.run(5);
|
||||
assert.equal(u.skill.activations, 1, `${tag}: no cast while the pack is in its 战术点形态`);
|
||||
assert.ok(u.skill.ready, 'the SP waits full');
|
||||
const dp1 = p.dp;
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 2, 0.5), `${tag}: cast while the pack is in its 战术点形态`);
|
||||
approx(p.dp, dp1 + b.cost, 1e-6, `${tag}: +cost DP at once`);
|
||||
assert.ok(w.mem.vigilGift && !w.alive, `${tag}: the gift waits on the pack`);
|
||||
fill(u);
|
||||
h.run(3);
|
||||
assert.equal(u.skill.activations, 2, `${tag}: no second cast while that gift is unused`);
|
||||
assert.ok(h.runUntil(() => w.alive, WOLF_IV(id)), `${tag}: the pack is back`);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 2, 0.5), `${tag}: cast once it stands again`);
|
||||
assert.ok(w.mem.vigilGift, 'the returned pack holds the gift');
|
||||
assert.ok(w.mem.vigilGift, `${tag}: the returned pack holds the gift for its next attack`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -708,7 +708,11 @@ test('6_11 缪尔赛思 S2 生态耦合: +cost DP; melee copies regenerate and t
|
||||
const eco = tok().findBuff('mlyss:eco');
|
||||
assert.ok(eco, 'melee copy buffed');
|
||||
approx(eco.mods.hpRegenRatio, bb.hp_recovery_per_sec_by_max_hp_ratio);
|
||||
approx(eco.mods.physTakenMul, 1 - bb.damage_resistance);
|
||||
// 庇护: the shared effect of every source (同名效果取最高 — kits/shared/tier1.js holdProtect)
|
||||
const protect = tok().findBuff('protect');
|
||||
assert.ok(protect && protect.source === u, 'the 庇护 she grants');
|
||||
approx(protect.mods.physTakenMul, 1 - bb.damage_resistance);
|
||||
approx(protect.mods.artsTakenMul, 1 - bb.damage_resistance);
|
||||
done(h);
|
||||
|
||||
// ranged copy: a sniper to copy
|
||||
|
||||
@@ -281,8 +281,8 @@ test('余: blocked enemies take 40 % ATK arts + 12 % burn per second, 庇护 whi
|
||||
assert.ok(dot.length >= 2);
|
||||
const burn = h.dmg.filter((d) => d.src === u.id && d.element === 'burn');
|
||||
assert.ok(burn.length >= 2);
|
||||
// 庇护 (ba.protect): physical and arts damage only
|
||||
assert.ok(u.findBuff('yu:shelter') && near(u.s.physTakenMul, 1 - t0.damage_resistance) && near(u.s.artsTakenMul, 1 - t0.damage_resistance));
|
||||
// 庇护 (ba.protect): physical and arts damage only — the shared effect of every source (kits/shared/tier1.js holdProtect)
|
||||
assert.ok(u.findBuff('protect')?.source === u && near(u.s.physTakenMul, 1 - t0.damage_resistance) && near(u.s.artsTakenMul, 1 - t0.damage_resistance));
|
||||
assert.equal(u.s.trueTakenMul, 1, '庇护 does not reduce true damage');
|
||||
assert.equal(u.s.dmgTakenMul, 1);
|
||||
const base = u.base.maxHp;
|
||||
|
||||
@@ -20,7 +20,8 @@ const FORMS = BACKUPS.units[HOD].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const CRUX = 'uniequip_002_hodrer', CRUY = 'uniequip_003_hodrer';
|
||||
const S1 = 'skchr_hodrer_1', S2 = 'skchr_hodrer_2', S3 = 'skchr_hodrer_3';
|
||||
const PROTECT = 'talent:hodrer:protect';
|
||||
const PROTECT = 'protect'; // the shared 庇护 of every source (kits/shared/tier1.js holdProtect)
|
||||
const DMG_UP = 'talent:hodrer:dmgUp'; // CRU-X stage 3's 物理伤害 + (its own one instance per ally)
|
||||
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);
|
||||
@@ -173,7 +174,7 @@ test('T2 余火之氅: he and the ally on the tile behind him get 18 % 庇护 (C
|
||||
assert.ok(m, `${label(f)}: ${a.def.charId} protected`);
|
||||
approx(m.physTakenMul, 1 - dr, `${label(f)}: phys`);
|
||||
approx(m.artsTakenMul, 1 - dr, `${label(f)}: arts`);
|
||||
assert.equal(m.physDealtMul ?? 1, x3 ? 1.1 : 1, `${label(f)}: 物理伤害`);
|
||||
assert.equal(a.findBuff(DMG_UP)?.mods.physDealtMul ?? 1, x3 ? 1.1 : 1, `${label(f)}: 物理伤害`);
|
||||
}
|
||||
assert.equal(beside.findBuff(PROTECT), null, `${label(f)}: not beside him`);
|
||||
const hp = behind.hp;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// server/sim/content/kits/ops/standin-acspec.js) on every chess she replaces: 缄默德克萨斯 (tier 4, S2), 山 (tier 5, S3 +
|
||||
// EXE-X), 新约能天使 / 锏 (tier 6, S3 + EXE-X), normal (E2 Lv1, skill 4) and elite (E2 Lv60, skill 7). Her three skills
|
||||
// are ON_DEPLOY passives: they start at every deployment — at the battle start no enemy is on the field yet, so S3 hits
|
||||
// nothing there and matters on a redeploy (再部署时间大幅度减少: 18 s). Numbers are read back from data/backups.json.
|
||||
// nothing there and matters on a redeploy (再部署时间大幅度减少: 18 s). S1 / S2 last 10 s: since 0.2.0 they are duration skills
|
||||
// started by every deployment (activateOnDeploy — PR #109's deploy-timed contract, their own skillStart / skillEnd), no
|
||||
// longer a passive holding a 10 s buff. Numbers are read back from data/backups.json.
|
||||
// Run: node --test test/content/standin_acspec.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
@@ -26,7 +28,7 @@ const ALLY = 'chess_char_1_02_a'; // 角峰, a melee blocker
|
||||
function battle(units, o = {}) {
|
||||
return makeBattle({
|
||||
defs: { enemies: ENEMIES }, units, timeLimit: o.timeLimit ?? 400, autoFinish: false, seed: o.seed ?? 5,
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'deploy', 'death'], captureNoisy: true,
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'deploy', 'death', 'skillStart', 'skillEnd'], captureNoisy: true,
|
||||
});
|
||||
}
|
||||
const MISERY = (id, standIn = true) => ({ chessId: id, row: 10, col: 4, standIn });
|
||||
@@ -38,7 +40,7 @@ function done(h) {
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
|
||||
test('Misery on every chess she replaces (normal + elite): her body (再部署 18 s), the backup skill as a passive, this kit; EXE-X on the tier-5 / 6 elites only', () => {
|
||||
test('Misery on every chess she replaces (normal + elite): her body (再部署 18 s), the backup skill (S2 a deploy-timed duration skill, S3 a passive), this kit; EXE-X on the tier-5 / 6 elites only', () => {
|
||||
const ids = UNIT.standsIn.flatMap((a) => [a, a.replace(/_a$/, '_b')]);
|
||||
assert.equal(ids.length, 8);
|
||||
const withModule = [];
|
||||
@@ -48,7 +50,8 @@ test('Misery on every chess she replaces (normal + elite): her body (再部署 1
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
const sk = form.skills.find((s) => s.index === c.backup.skillIndex);
|
||||
assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [CHAR, c.charId, sk.skillId, 'passive', false, 'skills'], id);
|
||||
const kind = sk.skillId === 'skchr_acspec_3' ? 'passive' : 'duration';
|
||||
assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [CHAR, c.charId, sk.skillId, kind, false, 'skills'], id);
|
||||
assert.deepEqual([sk.skillType, sk.spType], ['PASSIVE', 'ON_DEPLOY'], id);
|
||||
const mod = c.backup.uniEquipId && c.status.equipLevel > 0 ? form.modules.find((m) => m.uniEquipId === c.backup.uniEquipId) : null;
|
||||
assert.equal(!!u.def.raw.module?.active, !!mod, `${id}: module`);
|
||||
@@ -137,7 +140,7 @@ test('Misery S1 物理的服从 (no chess names it — a 自选 slot may): for 1
|
||||
const h = battle([MISERY(id, { skillIndex: 0 })]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
assert.deepEqual([u.skill.id, u.skill.kind, sk.duration], ['skchr_acspec_1', 'passive', 10]);
|
||||
assert.deepEqual([u.skill.id, u.skill.kind, u.skill.duration, sk.duration], ['skchr_acspec_1', 'duration', 10, 10]);
|
||||
const prob = () => { let p = null; u.profile.hitsFn({ rng: { chance: (x) => { p = x; return false; } } }, u); return p; };
|
||||
const on = () => { approx(u.s.dodgePhys, bb.prob, `${id}: dodge ${bb.prob}`); assert.equal(prob(), bb['attack@prob'], `${id}: chance ${bb['attack@prob']}`); };
|
||||
const off = () => { assert.equal(u.s.dodgePhys, 0, `${id}: no dodge`); assert.equal(prob(), talentBb(u, 0)['attack@prob'], `${id}: the talent's chance`); };
|
||||
@@ -162,9 +165,13 @@ test('Misery S2 战争的恭顺 (缄默德克萨斯): for 10 s from every deploy
|
||||
const h = battle([MISERY(id)]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
assert.deepEqual([u.skill.id, sk.duration], ['skchr_acspec_2', 10]);
|
||||
const on = () => { approx(u.s.atk, u.base.atk * (1 + bb.atk), `${id}: ATK +${bb.atk}`); assert.equal(u.s.aspd, 100 + bb.attack_speed, `${id}: ASPD`); };
|
||||
const off = () => { approx(u.s.atk, u.base.atk, `${id}: ATK back`); assert.equal(u.s.aspd, 100, `${id}: ASPD back`); };
|
||||
assert.deepEqual([u.skill.id, u.skill.kind, u.skill.duration, sk.duration], ['skchr_acspec_2', 'duration', 10, 10]);
|
||||
// the deploy-timed contract (PR #109): the skill runs — skillStart now, skillEnd 10 s later — and then is off
|
||||
const on = () => { approx(u.s.atk, u.base.atk * (1 + bb.atk), `${id}: ATK +${bb.atk}`); assert.equal(u.s.aspd, 100 + bb.attack_speed, `${id}: ASPD`); assert.ok(u.skill.active, `${id}: running`); };
|
||||
const off = () => { approx(u.s.atk, u.base.atk, `${id}: ATK back`); assert.equal(u.s.aspd, 100, `${id}: ASPD back`); assert.ok(!u.skill.active, `${id}: over`); };
|
||||
const starts = () => h.hooksOf('skillStart').filter((c) => c.unit === u).length;
|
||||
const ends = () => h.hooksOf('skillEnd').filter((c) => c.unit === u).length;
|
||||
assert.deepEqual([starts(), ends()], [1, 0], `${id}: started by the deployment`);
|
||||
on();
|
||||
h.run(9.8);
|
||||
on();
|
||||
@@ -176,6 +183,7 @@ test('Misery S2 战争的恭顺 (缄默德克萨斯): for 10 s from every deploy
|
||||
on();
|
||||
h.run(10.1);
|
||||
off();
|
||||
assert.deepEqual([starts(), ends()], [2, 2], `${id}: one window per deployment (the retreat ends none — it had ended)`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,6 +44,20 @@ test('golden files list exactly the corpus scenarios (regenerate with npm run go
|
||||
}
|
||||
});
|
||||
|
||||
// a stored digest that records engine errors means the corpus was regenerated over a crash (0.2.0: a merge dropped an
|
||||
// import a kit's tick handler used, and `npm run golden:update` stored 9288 swallowed errors as "the expected result")
|
||||
test('no stored scenario records engine errors (errors.count is 0 everywhere)', () => {
|
||||
for (const f of FAMILY_NAMES) {
|
||||
for (const [id, dg] of Object.entries(storedDigests(loadGolden(f)))) {
|
||||
if (!dg || typeof dg !== 'object' || !dg.errors) continue;
|
||||
// battles: { count, kinds }; matches: { engine, logged, sim, dispatcher }
|
||||
const e = dg.errors;
|
||||
const n = typeof e.count === 'number' ? e.count : Object.values(e).reduce((a, v) => a + (typeof v === 'number' ? v : 0), 0);
|
||||
assert.equal(n, 0, `${f} ${id}: ${n} engine error(s) ${JSON.stringify(e)} — fix the crash, then regenerate`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const f of FAMILY_NAMES) {
|
||||
test(`golden ${f}: ${FULL ? 'every scenario' : 'the fast subset (GOLDEN_FULL=1 for all)'} reproduces its stored digest`, () => {
|
||||
const computed = digests[f];
|
||||
|
||||
+487
-487
File diff suppressed because it is too large.
Load diff
+2634
-2637
File diff suppressed because it is too large.
Load diff
+629
-672
File diff suppressed because it is too large.
Load diff
+328
-323
File diff suppressed because it is too large.
Load diff
+1443
-1428
File diff suppressed because it is too large.
Load diff
+271
-271
@@ -20,58 +20,58 @@
|
||||
"time": 170,
|
||||
"ticks": 5100,
|
||||
"reason": "timeout",
|
||||
"killed": 17,
|
||||
"killed": 22,
|
||||
"total": 50,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 620,
|
||||
"rngDraws": 595,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {
|
||||
"killed": 17,
|
||||
"killed": 22,
|
||||
"total": 50,
|
||||
"perfect": false,
|
||||
"leaks": 33,
|
||||
"leaks": 28,
|
||||
"leaksUncounted": 0,
|
||||
"coins": 4,
|
||||
"dmg": 240621,
|
||||
"dmg": 280742,
|
||||
"bossDmg": 0,
|
||||
"heal": 17519,
|
||||
"deaths": 24,
|
||||
"heal": 26765,
|
||||
"deaths": 19,
|
||||
"layerGains": {"raidShip":13,"visiShip":2}
|
||||
}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_3_12_a",2,null,0,0,72,3,40439,3657,0,19,2],
|
||||
["p1",2,"op","chess_char_4_09_a",0,null,0,0,1,2,32698,2984,0,17,1],
|
||||
["p1",3,"op","chess_char_3_05_a",2,null,2505,1,74,2,29899,11886,0,45,2],
|
||||
["p1",4,"op","chess_char_3_18_a",2,null,0,0,7,1,14915,3668,0,36,1],
|
||||
["p1",5,"op","chess_char_4_04_a",1,null,497.45,1,91,2,12814,6576,0,82,10],
|
||||
["p1",6,"op","chess_char_4_07_a",1,null,0,0,75,1,18472,13600,0,43,1],
|
||||
["p1",7,"op","chess_char_4_16_a",1,null,0,0,88,0,3482,9543,0,103,0],
|
||||
["p1",8,"op","chess_char_3_01_a",2,null,1271,1,5,0,19777,0,0,141,2],
|
||||
["p1",9,"op","chess_char_3_04_a",2,null,1218,1,84,0,0,1599,17519,35,2],
|
||||
["p1",10,"op","chess_char_3_21_a",2,null,716.85,1,6,4,45212,555,0,138,2],
|
||||
["p1",11,"op","chess_char_4_02_a",1,null,1272,1,90,2,12787,1653,0,68,1],
|
||||
["p1",12,"op","chess_char_4_13_a",1,null,0,0,86,0,10126,1624,0,26,1]
|
||||
["p1",1,"op","chess_char_3_12_a",2,null,0,0,73,3,40405,3648,0,20,2],
|
||||
["p1",2,"op","chess_char_4_09_a",0,null,458.8,1,83,5,55322,5509,0,34,2],
|
||||
["p1",3,"op","chess_char_3_05_a",2,null,1865.8,1,77,3,29828,11858,0,46,2],
|
||||
["p1",4,"op","chess_char_3_18_a",2,null,2638,1,7,2,32526,8770,0,75,2],
|
||||
["p1",5,"op","chess_char_4_04_a",1,null,1264.1,1,91,2,17806,8706,0,100,9],
|
||||
["p1",6,"op","chess_char_4_07_a",1,null,0,0,74,1,16008,10455,0,37,1],
|
||||
["p1",7,"op","chess_char_4_16_a",1,null,807.8,1,92,1,9136,11490,0,98,14],
|
||||
["p1",8,"op","chess_char_3_01_a",2,null,1271,1,5,0,15937,0,0,131,2],
|
||||
["p1",9,"op","chess_char_3_04_a",2,null,1218,1,8,0,0,0,26765,59,3],
|
||||
["p1",10,"op","chess_char_3_21_a",2,null,161.7,1,6,1,42388,1110,0,121,2],
|
||||
["p1",11,"op","chess_char_4_02_a",1,null,1272,1,93,3,11537,1724,0,61,1],
|
||||
["p1",12,"op","chess_char_4_13_a",1,null,812,1,84,1,9849,812,0,27,1]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_10001_trslim": [1,1,0,17,4860],
|
||||
"enemy_10001_trslim": [1,1,0,8,4860],
|
||||
"enemy_1000_gopro_2": [1,1,0,0,1836],
|
||||
"enemy_10018_sgrobh": [1,1,0,2810,8100],
|
||||
"enemy_1001_bigbo": [1,0,1,1235,2581],
|
||||
"enemy_10027_vtsk": [1,0,1,15097,2542],
|
||||
"enemy_10018_sgrobh": [1,1,0,2811,8100],
|
||||
"enemy_1001_bigbo": [1,0,1,1301,3217],
|
||||
"enemy_10027_vtsk": [1,0,1,16122,2471],
|
||||
"enemy_10031_cnvsld": [1,0,1,0,0],
|
||||
"enemy_10034_cnvsax": [1,0,1,364,2496],
|
||||
"enemy_10034_cnvsax": [1,0,1,0,0],
|
||||
"enemy_10042_prtrop": [1,0,1,0,0],
|
||||
"enemy_1116_liprr_2": [21,11,10,5655,116700],
|
||||
"enemy_1119_vofsd": [12,2,8,13284,62929],
|
||||
"enemy_1121_lifbos_2": [9,1,7,11244,38577]
|
||||
"enemy_1116_liprr_2": [21,12,9,5347,131366],
|
||||
"enemy_1119_vofsd": [12,6,6,19166,93491],
|
||||
"enemy_1121_lifbos_2": [9,1,7,9882,35401]
|
||||
},
|
||||
"hooks": [1,91,5100,870,870,1441,1438,3866,41,82,25,33,0,18850,26,37,50,30,1,42,5,0,6],
|
||||
"events": {"atk":1013,"bounty":2,"deploy":41,"die":52,"dmg":1438,"engage":11,"fx":62,"heal":35,"layer":5,"leak":30,"skill":47,"spawn":62,"status":126},
|
||||
"fx": {"beam":4,"blink":11,"dodge":6,"dp":5,"garrison":5,"liberate":14,"slash":16,"telegraph":1},
|
||||
"snaps": "dec536bc cbfb0e19 608df77d 4bc1be00 31528b18 50c15974 172a64b1 80ed52af 452e6c6e 2cbbaf73 d2893b65 71598545 0ff0a232 8497b8ca 56857b5a 512860f1 25c11ae9"
|
||||
"hooks": [1,93,5100,939,939,1515,1509,3842,41,82,41,39,0,22264,26,41,50,27,1,41,5,0,6],
|
||||
"events": {"atk":1088,"bounty":2,"deploy":43,"die":55,"dmg":1509,"engage":11,"fx":61,"heal":59,"layer":5,"leak":27,"skill":80,"spawn":62,"status":123},
|
||||
"fx": {"blink":14,"dodge":7,"dp":5,"garrison":5,"liberate":13,"slash":16,"telegraph":1},
|
||||
"snaps": "8b8be66d 3d41101a 548df15a 1009f8b0 08d13c52 9fe4b099 64799759 70e168de 0b87fdee 880a5dbc 4750bf5e 71d1cbac a6d3a940 eda8aa0d 5dec048f 8b7b727d 0b96370a"
|
||||
},
|
||||
"standin-02": {
|
||||
"about": "normal band 5 as stand-ins: chess_char_4_17_a→char_610_acfend chess_char_4_18_a→char_602_cdfend chess_char_5_05_a→char_602_cdfend chess_char_4_19_a→char_608_acpion chess_char_5_01_a→char_600_cpione chess_char_4_22_a→char_609_acguad chess_char_4_23_a→char_609_acguad chess_char_5_06_a→char_609_acguad chess_char_4_20_a→char_603_csnipe chess_char_4_24_a→char_604_ccast chess_char_5_02_a→char_604_ccast chess_char_5_03_a→char_612_accast",
|
||||
@@ -89,42 +89,42 @@
|
||||
"total": 68,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 113,
|
||||
"rngDraws": 108,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {"killed":9,"total":68,"perfect":false,"leaks":59,"leaksUncounted":0,"coins":0,"dmg":98109,"bossDmg":0,"heal":0,"deaths":23,"layerGains":{"preciShip":12}}
|
||||
"p1": {"killed":9,"total":68,"perfect":false,"leaks":59,"leaksUncounted":0,"coins":0,"dmg":102545,"bossDmg":0,"heal":0,"deaths":22,"layerGains":{"preciShip":12}}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_4_17_a",1,null,0,0,3,0,57,2604,0,4,1],
|
||||
["p1",1,"op","chess_char_4_17_a",1,null,0,0,3,1,294,2604,0,4,1],
|
||||
["p1",2,"op","chess_char_4_18_a",1,null,0,0,78,0,2350,6810,0,6,1],
|
||||
["p1",3,"op","chess_char_5_05_a",2,null,0,0,70,0,14,4256,0,1,0],
|
||||
["p1",4,"op","chess_char_4_19_a",1,null,0,0,66,0,316,3058,0,13,1],
|
||||
["p1",5,"op","chess_char_5_01_a",2,null,0,0,91,0,35,3675,0,2,0],
|
||||
["p1",6,"op","chess_char_4_22_a",1,null,0,0,71,0,5165,5276,0,9,0],
|
||||
["p1",7,"op","chess_char_4_23_a",1,null,0,0,64,0,5497,5276,0,9,0],
|
||||
["p1",3,"op","chess_char_5_05_a",2,null,0,0,72,0,41,4256,0,3,1],
|
||||
["p1",4,"op","chess_char_4_19_a",1,null,0,0,66,0,632,3058,0,12,1],
|
||||
["p1",5,"op","chess_char_5_01_a",2,null,154.94,1,91,0,52,3520,0,3,0],
|
||||
["p1",6,"op","chess_char_4_22_a",1,null,0,0,74,0,6886,5276,0,11,0],
|
||||
["p1",7,"op","chess_char_4_23_a",1,null,0,0,65,0,5497,5276,0,9,0],
|
||||
["p1",8,"op","chess_char_5_06_a",2,null,0,0,1,0,0,2638,0,0,0],
|
||||
["p1",9,"op","chess_char_4_20_a",1,null,1271,1,92,1,11656,2542,0,25,0],
|
||||
["p1",10,"op","chess_char_4_24_a",1,null,0,0,90,0,3736,3399,0,8,0],
|
||||
["p1",11,"op","chess_char_5_02_a",2,null,0,0,84,3,13253,2266,0,25,1],
|
||||
["p1",12,"op","chess_char_5_03_a",2,null,0,0,4,5,56032,1372,0,10,1]
|
||||
["p1",10,"op","chess_char_4_24_a",1,null,0,0,90,0,4203,3399,0,9,0],
|
||||
["p1",11,"op","chess_char_5_02_a",2,null,0,0,84,2,14070,2266,0,26,1],
|
||||
["p1",12,"op","chess_char_5_03_a",2,null,0,0,4,5,56863,1372,0,10,1]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_10042_prtrop_2": [1,0,1,4567,14393],
|
||||
"enemy_10043_sailor": [1,1,0,1137,23649],
|
||||
"enemy_10044_wintun": [1,0,1,4374,12094],
|
||||
"enemy_10042_prtrop_2": [1,0,1,4429,14122],
|
||||
"enemy_10043_sailor": [1,1,0,1024,23649],
|
||||
"enemy_10044_wintun": [1,0,1,3812,11990],
|
||||
"enemy_10045_parrot": [1,0,1,0,4099],
|
||||
"enemy_10054_cjhot": [1,0,1,1676,1665],
|
||||
"enemy_1005_yokai": [27,8,19,0,29182],
|
||||
"enemy_1005_yokai_2": [27,0,24,11401,2816],
|
||||
"enemy_1005_yokai_3": [6,0,6,20017,14371],
|
||||
"enemy_10065_ftzlc": [1,0,1,0,560],
|
||||
"enemy_10054_cjhot": [1,0,1,2686,1997],
|
||||
"enemy_1005_yokai": [27,8,19,0,29752],
|
||||
"enemy_1005_yokai_2": [27,0,24,10786,3283],
|
||||
"enemy_1005_yokai_3": [6,0,6,20260,18054],
|
||||
"enemy_10065_ftzlc": [1,0,1,19,640],
|
||||
"enemy_10067_ftsjc": [1,0,1,0,680],
|
||||
"enemy_1006_shield": [1,0,1,0,760]
|
||||
},
|
||||
"hooks": [1,92,5700,337,337,558,523,0,32,88,5,5,0,12532,1,13,68,56,1,32,3,0,41],
|
||||
"events": {"atk":345,"deploy":24,"die":32,"dmg":523,"engage":11,"fx":81,"layer":3,"leak":56,"skill":10,"spawn":80,"status":2},
|
||||
"fx": {"aoe":4,"dodge":41,"dp":4,"explode":27,"garrison":3,"garrisonGrant":1,"zone":1},
|
||||
"snaps": "9d2468c6 9a32f006 99b2aa33 a01ecc39 97bb00b0 2fcede4b fe594f15 377f7691 e87b0fd3 af44759d 7b13ea89 61cd9b40 4f9d1182 5905fb57 d697ca06 c37e9513 3102b854 296cdd49 4d5efa28"
|
||||
"hooks": [1,92,5700,335,335,575,545,0,31,87,6,6,0,13069,1,13,68,56,1,31,3,0,41],
|
||||
"events": {"atk":343,"deploy":24,"die":31,"dmg":545,"engage":11,"fx":83,"layer":3,"leak":56,"skill":12,"spawn":80,"status":2},
|
||||
"fx": {"aoe":4,"dodge":41,"dp":4,"explode":29,"garrison":3,"garrisonGrant":1,"zone":1},
|
||||
"snaps": "bb11ef91 3dacfd27 a1a9e1ba 0c1cbf13 1a25c591 303d857a 3dc0bf41 8f47df11 f254e91e 21b1a437 0bef3640 47e52a7a d371b6b0 8a488ba7 9a40aca9 786c46b4 ca780f2a f8b06cfe 10e274ca"
|
||||
},
|
||||
"standin-03": {
|
||||
"about": "normal band 5 as stand-ins: chess_char_5_08_a→char_610_acfend chess_char_5_14_a→char_608_acpion chess_char_5_19_a→char_617_sharp2 chess_char_5_07_a→char_601_cguard chess_char_5_13_a→char_609_acguad chess_char_5_17_a→char_615_acspec chess_char_5_10_a→char_606_csuppo chess_char_5_11_a→char_613_acmedc chess_char_5_12_a→char_614_acsupo chess_char_5_15_a→char_603_csnipe chess_char_5_18_a→char_611_acnipe chess_char_5_20_a→char_614_acsupo",
|
||||
@@ -138,44 +138,44 @@
|
||||
"time": 190,
|
||||
"ticks": 5700,
|
||||
"reason": "timeout",
|
||||
"killed": 46,
|
||||
"killed": 44,
|
||||
"total": 74,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 247,
|
||||
"rngDraws": 317,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":46,"total":74,"perfect":false,"leaks":28,"leaksUncounted":0,"coins":8,"dmg":342938,"bossDmg":0,"heal":45325,"deaths":12,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":44,"total":74,"perfect":false,"leaks":30,"leaksUncounted":0,"coins":8,"dmg":357249,"bossDmg":0,"heal":48257,"deaths":10,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_08_a",2,null,515.8,1,6,0,11779,9335,0,131,2],
|
||||
["p1",2,"op","chess_char_5_14_a",2,null,0,0,89,24,159150,7595,0,61,6],
|
||||
["p1",3,"op","chess_char_5_19_a",0,null,0,0,90,6,38255,15228,0,94,2],
|
||||
["p1",4,"op","chess_char_5_07_a",2,null,2564,1,91,2,10656,2984,0,18,1],
|
||||
["p1",5,"op","chess_char_5_13_a",2,null,0,0,11,7,58468,14481,0,126,4],
|
||||
["p1",6,"op","chess_char_5_17_a",2,null,1154,1,93,3,11205,3361,0,59,0],
|
||||
["p1",7,"op","chess_char_5_10_a",2,null,932,1,2,0,8982,0,0,77,2],
|
||||
["p1",8,"op","chess_char_5_11_a",2,null,1224,1,3,0,0,0,45325,72,2],
|
||||
["p1",9,"op","chess_char_5_12_a",2,null,1160,1,4,1,22227,0,0,86,2],
|
||||
["p1",10,"op","chess_char_5_15_a",2,null,0,0,84,2,8190,12762,0,91,2],
|
||||
["p1",11,"op","chess_char_5_18_a",2,null,0,0,1,0,5656,1272,0,20,1],
|
||||
["p1",12,"op","chess_char_5_20_a",2,null,1972,1,92,1,8370,1972,0,35,1]
|
||||
["p1",1,"op","chess_char_5_08_a",2,null,2206.8,1,91,0,5923,3841,0,59,1],
|
||||
["p1",2,"op","chess_char_5_14_a",2,null,0,0,90,23,142481,7595,0,63,6],
|
||||
["p1",3,"op","chess_char_5_19_a",0,null,349.6,1,89,6,47622,15311,0,100,3],
|
||||
["p1",4,"op","chess_char_5_07_a",2,null,843.2,1,7,1,29523,11092,0,77,3],
|
||||
["p1",5,"op","chess_char_5_13_a",2,null,822,1,11,7,56384,11384,0,123,4],
|
||||
["p1",6,"op","chess_char_5_17_a",2,null,1154,1,92,0,4254,4200,0,59,0],
|
||||
["p1",7,"op","chess_char_5_10_a",2,null,932,1,2,1,8953,0,0,74,2],
|
||||
["p1",8,"op","chess_char_5_11_a",2,null,1224,1,3,0,0,0,48257,71,2],
|
||||
["p1",9,"op","chess_char_5_12_a",2,null,1160,1,4,0,19483,0,0,77,2],
|
||||
["p1",10,"op","chess_char_5_15_a",2,null,0,0,84,2,8336,14152,0,96,2],
|
||||
["p1",11,"op","chess_char_5_18_a",2,null,0,0,1,4,27171,1272,0,32,1],
|
||||
["p1",12,"op","chess_char_5_20_a",2,null,0,0,5,0,7118,1972,0,39,2]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1006_shield_2": [1,1,0,679,8000],
|
||||
"enemy_1006_shield_3": [1,0,1,12959,19581],
|
||||
"enemy_1006_shield_2": [1,1,0,582,8000],
|
||||
"enemy_1006_shield_3": [1,0,1,10399,13075],
|
||||
"enemy_10073_mpcar": [1,1,0,12,1600],
|
||||
"enemy_1007_slime": [1,1,0,0,440],
|
||||
"enemy_10081_mpplai": [1,1,0,0,2000],
|
||||
"enemy_10087_hlchgr": [1,1,0,58,4800],
|
||||
"enemy_10087_hlchgr": [1,1,0,38,4800],
|
||||
"enemy_10094_crstf": [1,1,0,28,3200],
|
||||
"enemy_10097_crshd": [1,1,0,0,8000],
|
||||
"enemy_1166_dusbr_2": [42,31,11,3354,115737],
|
||||
"enemy_1170_dushld_2": [12,6,4,28840,54328],
|
||||
"enemy_9011_acrefr": [12,2,3,23060,125251]
|
||||
"enemy_1166_dusbr_2": [42,30,12,2728,121684],
|
||||
"enemy_1170_dushld_2": [12,5,6,28117,47066],
|
||||
"enemy_9011_acrefr": [12,2,6,28914,147383]
|
||||
},
|
||||
"hooks": [1,93,5700,1154,1154,1775,1770,135,58,77,25,27,0,28761,3090,58,74,19,1,58,0,0,8],
|
||||
"events": {"atk":1247,"bounty":4,"deploy":19,"die":58,"dmg":1767,"engage":11,"fx":170,"heal":124,"leak":19,"skill":56,"spawn":86,"status":614},
|
||||
"fx": {"aoe":60,"displace":1,"dodge":18,"dp":30,"phase":1,"slash":48,"sp":2,"spGain":10},
|
||||
"snaps": "dbcb5cec 56a8a8f6 e8d97a5d d2594a66 b6f753a4 78a2f35e 80349f15 2cd292dd d7240cae 4f120b33 69d90058 45d5da9a ae0b244b 17b17251 4df0cf30 f2c7c569 baa94919 07ba89e9 e4a68e4d"
|
||||
"hooks": [1,92,5700,1098,1098,1657,1660,130,54,79,28,28,0,28379,3587,52,74,25,1,56,0,0,8],
|
||||
"events": {"atk":1229,"bounty":4,"deploy":18,"die":54,"dmg":1659,"engage":11,"fx":131,"heal":126,"leak":25,"skill":60,"spawn":86,"status":621},
|
||||
"fx": {"aoe":30,"displace":1,"dodge":13,"dp":29,"phase":1,"slash":48,"sp":1,"spGain":8},
|
||||
"snaps": "530ce9ab 3259d8c0 918a3cff a85e2d90 3b23751a 8f259c98 ac7cc207 e97f970a 310fc8e2 3ccf05ce 133339f2 3ce55224 a14db521 246e0ee0 c7100c7e 91ae8513 4b6a214c 8c1f8a7b 41afe990"
|
||||
},
|
||||
"standin-04": {
|
||||
"about": "normal band 6 as stand-ins: chess_char_6_03_a→char_610_acfend chess_char_6_02_a→char_608_acpion chess_char_6_07_a→char_617_sharp2 chess_char_6_10_a→char_617_sharp2 chess_char_6_11_a→char_608_acpion chess_char_6_06_a→char_609_acguad chess_char_5_22_a→char_611_acnipe chess_char_6_01_a→char_611_acnipe chess_char_6_04_a→char_614_acsupo chess_char_6_05_a→char_611_acnipe chess_char_6_08_a→char_612_accast chess_char_6_09_a→char_613_acmedc",
|
||||
@@ -186,62 +186,62 @@
|
||||
"round": 12,
|
||||
"seed": 428893583,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 201.7,
|
||||
"ticks": 6051,
|
||||
"time": 196.37,
|
||||
"ticks": 5891,
|
||||
"reason": "cleared",
|
||||
"killed": 136,
|
||||
"killed": 127,
|
||||
"total": 158,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 644,
|
||||
"rngDraws": 656,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {
|
||||
"killed": 136,
|
||||
"killed": 127,
|
||||
"total": 158,
|
||||
"perfect": false,
|
||||
"leaks": 22,
|
||||
"leaks": 31,
|
||||
"leaksUncounted": 0,
|
||||
"coins": 8,
|
||||
"dmg": 1309822,
|
||||
"dmg": 1279084,
|
||||
"bossDmg": 0,
|
||||
"heal": 23527,
|
||||
"deaths": 12,
|
||||
"heal": 23617,
|
||||
"deaths": 13,
|
||||
"layerGains": {"sargonShip":5}
|
||||
}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_03_a",2,null,0,0,174,0,698,7812,0,6,0],
|
||||
["p1",2,"op","chess_char_6_02_a",2,null,1529,1,179,1,2975,3058,0,7,2],
|
||||
["p1",3,"op","chess_char_6_07_a",0,null,0,0,172,9,135322,3730,0,74,1],
|
||||
["p1",4,"op","chess_char_6_10_a",0,null,0,0,166,5,94409,3730,0,75,2],
|
||||
["p1",5,"op","chess_char_6_11_a",2,null,1026.4,1,178,11,100320,8547,0,40,4],
|
||||
["p1",6,"op","chess_char_6_06_a",2,null,2080.4,1,156,9,111400,19283,0,111,4],
|
||||
["p1",7,"op","chess_char_5_22_a",2,null,1272,1,3,2,25528,0,0,19,2],
|
||||
["p1",8,"op","chess_char_6_01_a",2,null,1272,1,4,34,325813,0,0,164,2],
|
||||
["p1",9,"op","chess_char_6_04_a",2,null,1160,1,13,7,65845,0,0,121,2],
|
||||
["p1",10,"op","chess_char_6_05_a",2,null,1272,1,14,18,234561,2453,0,122,2],
|
||||
["p1",11,"op","chess_char_6_08_a",2,null,1372,1,5,39,212952,0,0,57,2],
|
||||
["p1",12,"op","chess_char_6_09_a",2,null,1224,1,11,0,0,0,23527,37,2],
|
||||
["p1",1,"op","chess_char_6_03_a",2,null,0,0,173,0,379,7812,0,3,0],
|
||||
["p1",2,"op","chess_char_6_02_a",2,null,0,0,175,2,4099,4587,0,9,1],
|
||||
["p1",3,"op","chess_char_6_07_a",0,null,0,0,169,9,146057,3730,0,77,1],
|
||||
["p1",4,"op","chess_char_6_10_a",0,null,1865,1,182,0,26824,3730,0,28,1],
|
||||
["p1",5,"op","chess_char_6_11_a",2,null,1529,1,179,11,105846,7699,0,33,4],
|
||||
["p1",6,"op","chess_char_6_06_a",2,null,2080.4,1,157,13,102010,19207,0,105,3],
|
||||
["p1",7,"op","chess_char_5_22_a",2,null,1272,1,3,2,36737,0,0,27,2],
|
||||
["p1",8,"op","chess_char_6_01_a",2,null,1272,1,4,41,375365,0,0,160,3],
|
||||
["p1",9,"op","chess_char_6_04_a",2,null,1160,1,13,5,66082,0,0,118,2],
|
||||
["p1",10,"op","chess_char_6_05_a",2,null,1272,1,14,16,199814,2964,0,123,2],
|
||||
["p1",11,"op","chess_char_6_08_a",2,null,1372,1,5,28,215871,0,0,59,2],
|
||||
["p1",12,"op","chess_char_6_09_a",2,null,1224,1,11,0,0,0,23617,34,2],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_10098_crhro": [1,1,0,3450,41472],
|
||||
"enemy_10099_crvln": [1,1,0,2453,20736],
|
||||
"enemy_10098_crhro": [1,1,0,1577,41472],
|
||||
"enemy_10099_crvln": [1,1,0,2964,20736],
|
||||
"enemy_1009_lurker": [1,1,0,16,3041],
|
||||
"enemy_1010_demon": [1,1,0,0,10368],
|
||||
"enemy_1010_demon_2": [28,22,6,15447,543909],
|
||||
"enemy_1010_demon_2": [28,20,8,11373,543506],
|
||||
"enemy_10112_ymgds": [1,1,0,0,6912],
|
||||
"enemy_10116_ymgtop": [1,1,0,2924,13824],
|
||||
"enemy_10118_ymgprc": [1,0,1,3965,18459],
|
||||
"enemy_1071_dftman": [63,63,0,447,243855],
|
||||
"enemy_1072_dlancer": [60,45,15,19911,429319]
|
||||
"enemy_10116_ymgtop": [1,1,0,3450,13824],
|
||||
"enemy_10118_ymgprc": [1,0,1,5235,10561],
|
||||
"enemy_1071_dftman": [63,60,3,290,241610],
|
||||
"enemy_1072_dlancer": [60,41,19,24824,407969]
|
||||
},
|
||||
"hooks": [1,181,6051,918,918,1945,1944,62,148,170,25,24,0,31170,4160,44,158,22,1,149,2,0,13],
|
||||
"events": {"atk":1080,"bounty":4,"deploy":21,"die":148,"dmg":1944,"engage":11,"fx":87,"heal":45,"layer":2,"leak":22,"skill":49,"spawn":172,"status":440},
|
||||
"fx": {"aoe":7,"dodge":13,"dp":18,"garrison":2,"revive":1,"slash":29,"sp":12,"spGain":3,"telegraph":2},
|
||||
"snaps": "29a29463 6fc96b2d 184b448a fce3c126 45d67357 4c47175a 1292297b 3e42d44e b7bf91a6 dd4e2386 0b4615ac 81792172 a056e50b 7e87af96 dd9818ad 8c9001ef b2db6556 9bf17d05 66cf3ead 709fbe1f c88b2f46"
|
||||
"hooks": [1,182,5891,853,853,1865,1862,60,140,171,23,22,0,28489,4121,43,158,31,1,141,2,0,12],
|
||||
"events": {"atk":998,"bounty":4,"deploy":22,"die":140,"dmg":1862,"engage":11,"fx":77,"heal":44,"layer":2,"leak":31,"skill":45,"spawn":172,"status":464},
|
||||
"fx": {"aoe":4,"dodge":15,"dp":18,"garrison":2,"revive":1,"slash":24,"sp":8,"spGain":3,"telegraph":2},
|
||||
"snaps": "1184691a ac872d2c 04c21bcf 302d411f 5ea6c6b7 1d6c2c6a d1adbc33 d86e6315 835cf160 a3576945 32ffef8a f9d81c1e 035b4b09 12f6f260 41a84026 a6a6ba61 9dd38c5e dcbccdf8 93092800 8b97b584"
|
||||
},
|
||||
"standin-05": {
|
||||
"about": "normal band 6 as stand-ins: chess_char_6_17_a→char_608_acpion chess_char_6_13_a→char_615_acspec chess_char_6_19_a→char_615_acspec chess_char_6_12_a→char_612_accast chess_char_6_15_a→char_614_acsupo chess_char_6_18_a→char_612_accast chess_char_6_20_a→char_613_acmedc",
|
||||
@@ -252,47 +252,47 @@
|
||||
"round": 12,
|
||||
"seed": 4096833445,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 214.33,
|
||||
"ticks": 6430,
|
||||
"time": 215.57,
|
||||
"ticks": 6467,
|
||||
"reason": "cleared",
|
||||
"killed": 7,
|
||||
"killed": 5,
|
||||
"total": 104,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 246,
|
||||
"rngDraws": 215,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {"killed":7,"total":104,"perfect":false,"leaks":97,"leaksUncounted":0,"coins":0,"dmg":426178,"bossDmg":0,"heal":1353,"deaths":22,"layerGains":{"raidShip":3}}
|
||||
"p1": {"killed":5,"total":104,"perfect":false,"leaks":99,"leaksUncounted":0,"coins":0,"dmg":414926,"bossDmg":0,"heal":451,"deaths":22,"layerGains":{"raidShip":3}}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_17_a",2,null,0,0,128,1,103267,9556,0,43,1],
|
||||
["p1",2,"op","chess_char_6_13_a",2,null,0,0,132,1,25092,9232,0,87,0],
|
||||
["p1",3,"op","chess_char_6_19_a",2,null,0,0,134,2,29265,9232,0,141,0],
|
||||
["p1",4,"op","chess_char_6_12_a",2,null,671.78,1,7,1,143681,975,0,85,2],
|
||||
["p1",5,"op","chess_char_6_15_a",2,null,0,0,133,2,54479,4965,0,113,2],
|
||||
["p1",6,"op","chess_char_6_18_a",2,null,1372,1,85,0,70393,1372,0,42,2],
|
||||
["p1",7,"op","chess_char_6_20_a",2,null,1224,1,4,0,0,1353,1353,3,1],
|
||||
["p1",1,"op","chess_char_6_17_a",2,null,0,0,130,1,102521,9556,0,46,1],
|
||||
["p1",2,"op","chess_char_6_13_a",2,null,1154,1,135,0,23355,9232,0,75,0],
|
||||
["p1",3,"op","chess_char_6_19_a",2,null,0,0,133,3,24753,9232,0,127,0],
|
||||
["p1",4,"op","chess_char_6_12_a",2,null,671.78,1,7,0,141278,975,0,83,2],
|
||||
["p1",5,"op","chess_char_6_15_a",2,null,0,0,134,1,52715,4965,0,109,2],
|
||||
["p1",6,"op","chess_char_6_18_a",2,null,1372,1,85,0,70303,1372,0,42,2],
|
||||
["p1",7,"op","chess_char_6_20_a",2,null,1224,1,4,0,0,451,451,1,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_10122_uacann_2": [1,0,1,6380,12182],
|
||||
"enemy_10122_uacann_2": [1,0,1,7356,12074],
|
||||
"enemy_10124_uashld_2": [1,0,1,0,5764],
|
||||
"enemy_10127_rkmbst_2": [1,0,1,834,0],
|
||||
"enemy_10127_rkmbst_2": [1,0,1,1154,0],
|
||||
"enemy_10138_xdsnow": [1,0,1,0,1276],
|
||||
"enemy_10141_xdpeng_2": [1,0,1,4211,14094],
|
||||
"enemy_10144_xdelk_2": [1,0,1,0,42],
|
||||
"enemy_10156_mncrer": [1,0,1,0,738],
|
||||
"enemy_10141_xdpeng_2": [1,0,1,771,638],
|
||||
"enemy_10144_xdelk_2": [1,0,1,0,0],
|
||||
"enemy_10156_mncrer": [1,0,1,0,1476],
|
||||
"enemy_10159_mntrjn": [1,0,1,0,0],
|
||||
"enemy_1381_winman": [27,5,22,3499,98764],
|
||||
"enemy_1381_winman_2": [39,1,38,5799,153206],
|
||||
"enemy_1387_winshd": [30,1,29,15960,140112]
|
||||
"enemy_1381_winman": [27,4,23,2007,92343],
|
||||
"enemy_1381_winman_2": [39,0,39,7769,165158],
|
||||
"enemy_1387_winshd": [30,1,29,16725,136197]
|
||||
},
|
||||
"hooks": [1,134,6430,620,620,988,988,6,29,128,8,24,0,14868,2437,28,104,97,1,29,1,0,0],
|
||||
"events": {"atk":651,"deploy":27,"die":31,"dmg":901,"engage":6,"fx":58,"heal":3,"layer":1,"leak":97,"skill":47,"spawn":114,"status":514},
|
||||
"fx": {"aoe":5,"blink":2,"camouflage":1,"displace":22,"dodge":3,"dp":2,"explode":6,"garrison":1,"garrisonGrant":2,"slash":8,"zone":6},
|
||||
"snaps": "8d7b3223 b6dafa64 3940bc8e fd79b9e2 e5e85618 6fcfc479 90958489 71bfbb91 d12c224c cd2c9c7f 4a9fff33 68e48a86 4b97e027 5f8e8dad da62239a 8601c0dc 73717b6d 9e4f784f 818e7ed1 c1f2a855 33118eab 9bafc0bc"
|
||||
"hooks": [1,135,6467,577,577,964,964,2,27,128,8,24,0,14795,2484,27,104,99,1,27,1,0,0],
|
||||
"events": {"atk":608,"deploy":28,"die":29,"dmg":872,"engage":6,"fx":65,"heal":1,"layer":1,"leak":99,"skill":50,"spawn":114,"status":505},
|
||||
"fx": {"aoe":5,"blink":2,"camouflage":1,"displace":29,"dodge":1,"dp":2,"explode":7,"garrison":1,"garrisonGrant":2,"slash":8,"zone":7},
|
||||
"snaps": "7d1098cf eed27d14 68262b97 c6f234c7 8b38f4e3 21342a44 02b1ea83 cb4a0d82 ab536b9b 983e88e0 79e6d79f 29c12df3 10ec6f87 f8880188 2817d38e c8292cb4 c36fbc55 11c36bec 13e7ad67 fa20b3bf b4378733 2c35030e"
|
||||
},
|
||||
"standin-06": {
|
||||
"about": "elite band 5 as stand-ins: chess_char_3_12_b→char_608_acpion chess_char_4_09_b→char_617_sharp2 chess_char_3_05_b→char_601_cguard chess_char_3_18_b→char_609_acguad chess_char_4_04_b→char_607_cspec chess_char_4_07_b→char_601_cguard chess_char_4_16_b→char_615_acspec chess_char_3_01_b→char_603_csnipe chess_char_3_04_b→char_605_cmedic chess_char_3_21_b→char_611_acnipe chess_char_4_02_b→char_611_acnipe chess_char_4_13_b→char_614_acsupo",
|
||||
@@ -303,61 +303,61 @@
|
||||
"round": 10,
|
||||
"seed": 1442657168,
|
||||
"waveId": "act1autochess_h03",
|
||||
"time": 182.73,
|
||||
"ticks": 5482,
|
||||
"time": 183.7,
|
||||
"ticks": 5511,
|
||||
"reason": "cleared",
|
||||
"killed": 46,
|
||||
"killed": 50,
|
||||
"total": 98,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 295,
|
||||
"rngDraws": 345,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {
|
||||
"killed": 46,
|
||||
"killed": 50,
|
||||
"total": 98,
|
||||
"perfect": false,
|
||||
"leaks": 52,
|
||||
"leaks": 48,
|
||||
"leaksUncounted": 0,
|
||||
"coins": 8,
|
||||
"dmg": 621924,
|
||||
"dmg": 676478,
|
||||
"bossDmg": 0,
|
||||
"heal": 27685,
|
||||
"deaths": 32,
|
||||
"layerGains": {"raidShip":26,"visiShip":4}
|
||||
"heal": 30426,
|
||||
"deaths": 31,
|
||||
"layerGains": {"raidShip":26,"visiShip":12}
|
||||
}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_3_12_b",2,"none",1904,1,138,10,143498,5544,956,21,3],
|
||||
["p1",2,"op","chess_char_4_09_b",0,"none",0,0,6,9,109862,3510,0,21,1],
|
||||
["p1",3,"op","chess_char_3_05_b",2,"none",0,0,93,2,26416,9128,0,18,2],
|
||||
["p1",4,"op","chess_char_3_18_b",2,"none",0,0,92,3,33920,9190,2808,27,2],
|
||||
["p1",5,"op","chess_char_4_04_b",1,"none",0,0,137,0,13321,10188,0,31,8],
|
||||
["p1",6,"op","chess_char_4_07_b",1,"none",0,0,96,1,23318,10676,0,23,1],
|
||||
["p1",7,"op","chess_char_4_16_b",1,"none",0,0,136,3,27487,13878,0,36,0],
|
||||
["p1",8,"op","chess_char_3_01_b",2,"none",0,0,132,2,23235,3020,0,28,1],
|
||||
["p1",9,"op","chess_char_3_04_b",2,"none",0,0,4,0,0,13615,23921,23,2],
|
||||
["p1",10,"op","chess_char_3_21_b",2,"none",0,0,117,11,155638,7911,0,47,1],
|
||||
["p1",11,"op","chess_char_4_02_b",1,"none",0,0,2,5,48494,1483,0,31,1],
|
||||
["p1",12,"op","chess_char_4_13_b",1,"none",0,0,99,0,16734,3262,0,23,2]
|
||||
["p1",1,"op","chess_char_3_12_b",2,"none",1904,1,140,10,155395,5504,915,21,3],
|
||||
["p1",2,"op","chess_char_4_09_b",0,"none",0,0,6,10,119787,3510,0,39,1],
|
||||
["p1",3,"op","chess_char_3_05_b",2,"none",0,0,90,2,23950,8566,0,18,2],
|
||||
["p1",4,"op","chess_char_3_18_b",2,"none",0,0,93,3,28745,9190,2808,27,2],
|
||||
["p1",5,"op","chess_char_4_04_b",1,"none",0,0,137,0,25476,10224,0,40,9],
|
||||
["p1",6,"op","chess_char_4_07_b",1,"none",0,0,101,3,28031,10981,0,27,1],
|
||||
["p1",7,"op","chess_char_4_16_b",1,"none",0,0,139,7,42175,16104,0,70,12],
|
||||
["p1",8,"op","chess_char_3_01_b",2,"none",0,0,122,1,16716,3020,0,24,1],
|
||||
["p1",9,"op","chess_char_3_04_b",2,"none",0,0,4,0,0,15932,26703,25,2],
|
||||
["p1",10,"op","chess_char_3_21_b",2,"none",0,0,118,12,175716,7911,0,55,1],
|
||||
["p1",11,"op","chess_char_4_02_b",1,"none",0,0,124,2,41362,2966,0,27,1],
|
||||
["p1",12,"op","chess_char_4_13_b",1,"none",0,0,103,0,19124,2704,0,26,2]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_10162_mnctpt": [1,1,0,7990,39813],
|
||||
"enemy_1019_jshoot": [1,1,0,423,4479],
|
||||
"enemy_1019_jshoot_2": [1,1,0,1526,6718],
|
||||
"enemy_1021_bslime": [1,1,0,3434,6121],
|
||||
"enemy_1023_jmage": [1,1,0,2086,5474],
|
||||
"enemy_10162_mnctpt": [1,1,0,7378,39813],
|
||||
"enemy_1019_jshoot": [1,1,0,295,4479],
|
||||
"enemy_1019_jshoot_2": [1,1,0,837,6718],
|
||||
"enemy_1021_bslime": [1,1,0,1648,6121],
|
||||
"enemy_1023_jmage": [1,1,0,2497,5474],
|
||||
"enemy_1025_reveng": [1,1,0,0,12442],
|
||||
"enemy_1026_aghost": [1,1,0,0,9456],
|
||||
"enemy_1043_zomsbr": [1,1,0,0,6287],
|
||||
"enemy_1251_lysyta_2": [57,29,28,15958,256380],
|
||||
"enemy_1252_lysytb_2": [21,8,13,24284,223998],
|
||||
"enemy_1329_cbshld_2": [12,1,11,35708,50755]
|
||||
"enemy_1043_zomsbr": [1,1,0,0,6285],
|
||||
"enemy_1251_lysyta_2": [57,33,24,13447,289829],
|
||||
"enemy_1252_lysytb_2": [21,8,13,29953,226804],
|
||||
"enemy_1329_cbshld_2": [12,1,11,40556,69057]
|
||||
},
|
||||
"hooks": [1,138,5482,523,523,776,802,93,78,137,24,36,0,6341,31,56,98,52,1,86,5,0,4],
|
||||
"events": {"atk":611,"bounty":4,"deploy":40,"die":85,"dmg":802,"engage":11,"fx":59,"heal":66,"layer":5,"leak":52,"skill":48,"spawn":110,"status":107},
|
||||
"fx": {"blink":7,"camouflage":1,"dodge":8,"dp":13,"explode":1,"garrison":5,"slash":24},
|
||||
"snaps": "32c77df2 e0a686fc 511e3625 2a2c9595 4d454cd6 ba68fe2a 509a2a40 0f18d74e 26c72bef 4d6d5cce 834f8ce9 462b0854 0fec1e43 fd1bd922 ab1c6896 53173cd5 dbdfb42d 6f942256 4207d16e"
|
||||
"hooks": [1,140,5511,596,596,875,906,94,81,139,37,37,0,7322,36,60,98,48,1,89,7,0,4],
|
||||
"events": {"atk":684,"bounty":4,"deploy":42,"die":91,"dmg":906,"engage":11,"fx":62,"heal":67,"layer":7,"leak":48,"skill":74,"spawn":110,"status":117},
|
||||
"fx": {"blink":10,"camouflage":1,"dodge":6,"dp":13,"explode":1,"garrison":7,"slash":24},
|
||||
"snaps": "be6d1821 fa43a30d 52099f77 a860ccb6 ea311a6e 44affa90 9dabf4d8 83adbc77 77dfae35 f02fd736 470fba6a a9871a60 b7e054aa ae8c4b73 55df9303 1c374baa 0d780d78 e03a2b60 b7879d42"
|
||||
},
|
||||
"standin-07": {
|
||||
"about": "elite band 6 as stand-ins: chess_char_4_17_b→char_610_acfend chess_char_4_18_b→char_602_cdfend chess_char_5_05_b→char_602_cdfend chess_char_4_19_b→char_608_acpion chess_char_5_01_b→char_600_cpione chess_char_4_22_b→char_609_acguad chess_char_4_23_b→char_609_acguad chess_char_5_06_b→char_609_acguad chess_char_4_20_b→char_603_csnipe chess_char_4_24_b→char_604_ccast chess_char_5_02_b→char_604_ccast chess_char_5_03_b→char_612_accast",
|
||||
@@ -368,24 +368,24 @@
|
||||
"round": 12,
|
||||
"seed": 816286799,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 222.97,
|
||||
"ticks": 6689,
|
||||
"time": 223.57,
|
||||
"ticks": 6707,
|
||||
"reason": "cleared",
|
||||
"killed": 8,
|
||||
"killed": 9,
|
||||
"total": 80,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 80,
|
||||
"rngDraws": 86,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {
|
||||
"killed": 8,
|
||||
"killed": 9,
|
||||
"total": 80,
|
||||
"perfect": false,
|
||||
"leaks": 72,
|
||||
"leaks": 71,
|
||||
"leaksUncounted": 0,
|
||||
"coins": 4,
|
||||
"dmg": 126420,
|
||||
"dmg": 124008,
|
||||
"bossDmg": 0,
|
||||
"heal": 0,
|
||||
"deaths": 22,
|
||||
@@ -393,38 +393,38 @@
|
||||
}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_4_17_b",1,"none",0,0,3,0,42,3211,0,2,1],
|
||||
["p1",1,"op","chess_char_4_17_b",1,"none",0,0,3,0,210,3211,0,2,1],
|
||||
["p1",2,"op","chess_char_4_18_b",1,"none",0,0,83,0,30,9306,0,1,1],
|
||||
["p1",3,"op","chess_char_5_05_b",2,"none",0,0,97,0,572,5816,0,1,1],
|
||||
["p1",4,"op","chess_char_4_19_b",1,"none",1847.63,1,107,1,367,3864,0,5,1],
|
||||
["p1",3,"op","chess_char_5_05_b",2,"none",0,0,97,0,1145,5816,0,2,1],
|
||||
["p1",4,"op","chess_char_4_19_b",1,"none",1847.63,1,107,0,230,3864,0,7,1],
|
||||
["p1",5,"op","chess_char_5_01_b",2,"none",1036.53,1,105,0,2197,3985,0,5,3],
|
||||
["p1",6,"op","chess_char_4_22_b",1,"none",3191,1,109,0,1178,14884,0,5,1],
|
||||
["p1",7,"op","chess_char_4_23_b",1,"none",0,0,67,0,3484,6610,0,8,1],
|
||||
["p1",6,"op","chess_char_4_22_b",1,"none",3191,1,109,0,1178,15724,0,5,1],
|
||||
["p1",7,"op","chess_char_4_23_b",1,"none",0,0,68,0,3484,6865,0,8,1],
|
||||
["p1",8,"op","chess_char_5_06_b",2,"uniequip_002_acguad",0,0,10,0,0,3820,0,0,0],
|
||||
["p1",9,"op","chess_char_4_20_b",1,"none",1255.06,1,106,2,16976,4326,0,31,1],
|
||||
["p1",10,"op","chess_char_4_24_b",1,"none",1007.59,1,108,0,5460,3072,0,18,1],
|
||||
["p1",11,"op","chess_char_5_02_b",2,"none",0,0,102,1,8032,4080,0,19,1],
|
||||
["p1",12,"op","chess_char_5_03_b",2,"uniequip_002_accast",0,0,4,4,88082,1749,0,10,1],
|
||||
["p1",9,"op","chess_char_4_20_b",1,"none",1255.06,1,106,2,14281,4581,0,27,1],
|
||||
["p1",10,"op","chess_char_4_24_b",1,"none",1007.59,1,108,0,5710,3072,0,19,1],
|
||||
["p1",11,"op","chess_char_5_02_b",2,"none",0,0,102,1,8684,4080,0,19,1],
|
||||
["p1",12,"op","chess_char_5_03_b",2,"uniequip_002_accast",0,0,4,6,86859,1749,0,10,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1005_yokai_2": [18,2,16,3894,7075],
|
||||
"enemy_1041_lazerd_2": [24,3,21,41810,33530],
|
||||
"enemy_1042_frostd": [30,1,29,0,24206],
|
||||
"enemy_1045_hammer": [1,1,0,1914,15552],
|
||||
"enemy_1045_hammer_2": [1,0,1,6132,25057],
|
||||
"enemy_1046_agent": [1,1,0,57,3888],
|
||||
"enemy_1050_lslime": [1,0,1,9425,1320],
|
||||
"enemy_1061_zomshd": [1,0,1,31,9241],
|
||||
"enemy_1062_rager_2": [1,0,1,1459,14551],
|
||||
"enemy_1005_yokai_2": [18,3,15,3871,7232],
|
||||
"enemy_1041_lazerd_2": [24,3,21,43406,32062],
|
||||
"enemy_1042_frostd": [30,1,29,0,25469],
|
||||
"enemy_1045_hammer": [1,1,0,2117,15552],
|
||||
"enemy_1045_hammer_2": [1,0,1,5327,20514],
|
||||
"enemy_1046_agent": [1,1,0,72,3888],
|
||||
"enemy_1050_lslime": [1,0,1,9701,0],
|
||||
"enemy_1061_zomshd": [1,0,1,61,13862],
|
||||
"enemy_1062_rager_2": [1,0,1,1519,13929],
|
||||
"enemy_1067_snslime": [1,0,1,0,0],
|
||||
"enemy_1071_dftman": [1,0,1,0,0]
|
||||
},
|
||||
"hooks": [1,109,6689,304,304,581,573,378,30,102,13,13,0,8920,9,11,80,72,1,33,2,0,8],
|
||||
"events": {"atk":330,"bounty":2,"deploy":27,"die":30,"dmg":573,"engage":11,"fx":22,"layer":2,"leak":72,"skill":26,"spawn":94,"status":103},
|
||||
"fx": {"aoe":1,"dodge":8,"dp":6,"garrison":2,"garrisonGrant":1,"palsy":1,"revive":3},
|
||||
"snaps": "ef9ff66a 4f4ac6fc 133f7f1c a187a8c8 46d89701 4bb496a8 0dfccf57 81173658 8c42876d 4718c77c d3b80c8e df3c692d 0576a74b 7684de92 e59b0c43 849795d5 e99812de 3f883cd6 6bb9cdf6 b246f597 289cd790 2f34e6ff 997f51f3"
|
||||
"hooks": [1,109,6707,301,301,585,576,545,31,102,13,13,0,8990,10,14,80,71,1,34,2,0,9],
|
||||
"events": {"atk":327,"bounty":2,"deploy":27,"die":31,"dmg":576,"engage":11,"fx":23,"layer":2,"leak":71,"skill":26,"spawn":94,"status":111},
|
||||
"fx": {"aoe":1,"dodge":9,"dp":5,"garrison":2,"garrisonGrant":1,"palsy":2,"revive":3},
|
||||
"snaps": "801e90ec 5f30137a 56120166 3365328a cd10076b ddf2ae60 00e0a9a1 f0fbc83c 9a8e0ded c4876430 15bb6d00 e61302d2 75a24a51 0b7275b4 f728ca3f 437d1568 ed5e8b79 5a14602a ac144838 2dcf417a 7b12357a 77ef33ec ff8819ba"
|
||||
},
|
||||
"standin-08": {
|
||||
"about": "elite band 6 as stand-ins: chess_char_5_08_b→char_610_acfend chess_char_5_14_b→char_608_acpion chess_char_5_19_b→char_617_sharp2 chess_char_5_07_b→char_601_cguard chess_char_5_13_b→char_609_acguad chess_char_5_17_b→char_615_acspec chess_char_5_10_b→char_606_csuppo chess_char_5_11_b→char_613_acmedc chess_char_5_12_b→char_614_acsupo chess_char_5_15_b→char_603_csnipe chess_char_5_18_b→char_611_acnipe chess_char_5_20_b→char_614_acsupo",
|
||||
@@ -438,47 +438,47 @@
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 43,
|
||||
"killed": 36,
|
||||
"total": 179,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 1025,
|
||||
"rngDraws": 1087,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":43,"total":179,"perfect":false,"leaks":136,"leaksUncounted":0,"coins":2,"dmg":2172511,"bossDmg":0,"heal":95038,"deaths":13,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":36,"total":179,"perfect":false,"leaks":143,"leaksUncounted":0,"coins":2,"dmg":1859123,"bossDmg":0,"heal":138461,"deaths":10,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_acfend",3371,1,203,4,94861,14912,0,116,3],
|
||||
["p1",2,"op","chess_char_5_14_b",2,"uniequip_002_acpion",0,0,124,14,988772,21777,0,61,10],
|
||||
["p1",3,"op","chess_char_5_19_b",0,"uniequip_002_sharp2",4154.5,1,205,8,235963,23398,0,64,2],
|
||||
["p1",4,"op","chess_char_5_07_b",2,"none",3334,1,204,2,34977,19978,0,29,2],
|
||||
["p1",5,"op","chess_char_5_13_b",2,"uniequip_002_acguad",0,0,161,5,103856,25240,0,51,3],
|
||||
["p1",6,"op","chess_char_5_17_b",2,"uniequip_002_acspec",1448,1,200,4,83107,24504,0,112,0],
|
||||
["p1",7,"op","chess_char_5_10_b",2,"none",1131,1,4,2,79514,0,0,153,4],
|
||||
["p1",8,"op","chess_char_5_11_b",2,"uniequip_002_acmedc",2262.4,1,5,0,0,0,95038,74,3],
|
||||
["p1",9,"op","chess_char_5_12_b",2,"uniequip_002_acsupo",1430,1,11,0,112393,0,0,146,3],
|
||||
["p1",10,"op","chess_char_5_15_b",2,"none",1510,1,13,0,77843,0,0,215,4],
|
||||
["p1",11,"op","chess_char_5_18_b",2,"uniequip_002_acnipe",2380.5,1,6,4,262886,0,0,169,3],
|
||||
["p1",12,"op","chess_char_5_20_b",2,"uniequip_002_acsupo",1430,1,14,0,98338,0,0,125,3],
|
||||
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_acfend",3167.17,1,165,4,83889,36030,0,116,3],
|
||||
["p1",2,"op","chess_char_5_14_b",2,"uniequip_002_acpion",1904,1,194,7,473061,9225,0,48,8],
|
||||
["p1",3,"op","chess_char_5_19_b",0,"uniequip_002_sharp2",0,0,156,7,350021,33747,0,99,3],
|
||||
["p1",4,"op","chess_char_5_07_b",2,"none",3334,1,203,1,30240,30649,0,29,2],
|
||||
["p1",5,"op","chess_char_5_13_b",2,"uniequip_002_acguad",2176.17,1,129,7,177239,32096,0,89,4],
|
||||
["p1",6,"op","chess_char_5_17_b",2,"uniequip_002_acspec",1448,1,188,5,87979,26616,0,142,0],
|
||||
["p1",7,"op","chess_char_5_10_b",2,"none",1131,1,4,0,74712,0,0,155,4],
|
||||
["p1",8,"op","chess_char_5_11_b",2,"uniequip_002_acmedc",2262.4,1,5,0,0,0,138461,78,3],
|
||||
["p1",9,"op","chess_char_5_12_b",2,"uniequip_002_acsupo",1430,1,11,2,99017,0,0,136,3],
|
||||
["p1",10,"op","chess_char_5_15_b",2,"none",1510,1,13,0,76181,0,0,234,4],
|
||||
["p1",11,"op","chess_char_5_18_b",2,"uniequip_002_acnipe",2380.5,1,6,3,299976,0,0,164,3],
|
||||
["p1",12,"op","chess_char_5_20_b",2,"uniequip_002_acsupo",1430,1,14,0,106807,0,0,136,3],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1072_dlancer": [1,1,0,3008,37150],
|
||||
"enemy_1081_sotisd": [1,0,1,0,27122],
|
||||
"enemy_1092_mdgint": [1,0,1,1448,67],
|
||||
"enemy_1116_liprr": [1,0,1,0,7702],
|
||||
"enemy_1116_liprr_2": [1,0,1,0,1829],
|
||||
"enemy_1081_sotisd": [1,0,1,0,27095],
|
||||
"enemy_1092_mdgint": [1,0,1,0,33],
|
||||
"enemy_1116_liprr": [1,0,1,0,6095],
|
||||
"enemy_1116_liprr_2": [1,0,1,0,2364],
|
||||
"enemy_1118_lidbox_2": [1,0,1,0,1344],
|
||||
"enemy_1119_vofsd": [1,0,1,0,889],
|
||||
"enemy_1121_lifbos": [1,0,1,0,1525],
|
||||
"enemy_1249_lysdb_2": [60,13,44,41971,986578],
|
||||
"enemy_1251_lysyta": [63,28,35,8822,665956],
|
||||
"enemy_1254_lypa_2": [48,1,47,74560,442348]
|
||||
"enemy_1121_lifbos": [1,0,1,0,1997],
|
||||
"enemy_1249_lysdb_2": [60,16,44,44297,925272],
|
||||
"enemy_1251_lysyta": [63,18,45,8302,477784],
|
||||
"enemy_1254_lypa_2": [48,1,46,112756,379100]
|
||||
},
|
||||
"hooks": [1,205,6900,1764,1764,3493,3533,147,56,189,40,44,0,26478,16569,72,179,133,1,59,0,0,10],
|
||||
"events": {"atk":2013,"bounty":1,"deploy":23,"die":56,"dmg":3508,"engage":11,"fx":308,"heal":130,"leak":133,"skill":90,"spawn":194,"status":1297},
|
||||
"fx": {"aoe":90,"displace":12,"dodge":31,"dp":24,"shieldBreak":59,"slash":80,"sp":1,"spGain":11},
|
||||
"snaps": "4d4dc5ab 00efc642 4dc3aa32 13e9533b a3617bdf 587927d0 9fe3b7b8 0b39dceb 2981edf9 2ba1ce8c ef825d5b 85fd9608 a83f0a12 9e6b1271 7c5bf7ee f6ad8d8e c76286f6 037365de bf979497 771a38f7 b74aaf11 518326a3 19e5e338"
|
||||
"hooks": [1,203,6900,1914,1914,3154,3238,154,46,188,40,41,0,27753,17018,59,179,142,1,51,0,0,24],
|
||||
"events": {"atk":2198,"bounty":1,"deploy":21,"die":46,"dmg":3227,"engage":11,"fx":296,"heal":145,"leak":142,"skill":85,"spawn":194,"status":1301},
|
||||
"fx": {"aoe":89,"displace":4,"dodge":50,"dp":15,"shieldBreak":60,"slash":64,"sp":4,"spGain":10},
|
||||
"snaps": "c4ba1bfb 2dfd8eae 9edfcbdb 398871eb 8ec5c018 bdeb7f73 3a80c711 b3542ba8 7c2bcdeb 332b7d77 cfba3c82 e42b9504 e04cec26 299bbeab 55c539ae fb163352 d1b5dcef e0628f10 16c64f3c f935f6ce dfa3bfea 0a6ae300 8f573ae7"
|
||||
},
|
||||
"standin-09": {
|
||||
"about": "elite band 7 as stand-ins: chess_char_6_03_b→char_610_acfend chess_char_6_02_b→char_608_acpion chess_char_6_07_b→char_617_sharp2 chess_char_6_10_b→char_617_sharp2 chess_char_6_11_b→char_608_acpion chess_char_6_06_b→char_609_acguad chess_char_5_22_b→char_611_acnipe chess_char_6_01_b→char_611_acnipe chess_char_6_04_b→char_614_acsupo chess_char_6_05_b→char_611_acnipe chess_char_6_08_b→char_612_accast chess_char_6_09_b→char_613_acmedc",
|
||||
@@ -489,31 +489,31 @@
|
||||
"round": 13,
|
||||
"seed": 2308517178,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 198.9,
|
||||
"ticks": 5967,
|
||||
"time": 198.43,
|
||||
"ticks": 5953,
|
||||
"reason": "cleared",
|
||||
"killed": 158,
|
||||
"total": 158,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 257,
|
||||
"rngDraws": 232,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {"killed":158,"total":158,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":8,"dmg":924432,"bossDmg":0,"heal":30129,"deaths":1,"layerGains":{"sargonShip":20}}
|
||||
"p1": {"killed":158,"total":158,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":8,"dmg":924222,"bossDmg":0,"heal":29638,"deaths":1,"layerGains":{"sargonShip":20}}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_acfend",4974.95,1,8,0,0,0,0,0,0],
|
||||
["p1",2,"op","chess_char_6_02_b",2,"uniequip_002_acpion",1904,1,6,0,0,0,0,0,0],
|
||||
["p1",3,"op","chess_char_6_07_b",0,"uniequip_002_sharp2",2494,1,9,0,0,0,0,0,0],
|
||||
["p1",4,"op","chess_char_6_10_b",0,"uniequip_002_sharp2",2494,1,11,20,146204,0,0,174,4],
|
||||
["p1",5,"op","chess_char_6_11_b",2,"uniequip_002_acpion",1834,1,13,45,168227,12564,0,58,23],
|
||||
["p1",6,"op","chess_char_6_06_b",2,"uniequip_002_acguad",3531,1,14,10,62057,943,0,157,4],
|
||||
["p1",4,"op","chess_char_6_10_b",0,"uniequip_002_sharp2",2494,1,11,27,129672,0,0,173,3],
|
||||
["p1",5,"op","chess_char_6_11_b",2,"uniequip_002_acpion",1904,1,13,35,186014,11957,0,60,20],
|
||||
["p1",6,"op","chess_char_6_06_b",2,"uniequip_002_acguad",3531,1,14,18,61248,763,0,155,4],
|
||||
["p1",7,"op","chess_char_5_22_b",2,"uniequip_002_acnipe",1587,1,4,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_6_01_b",2,"uniequip_002_acnipe",1643,1,5,0,0,0,0,0,0],
|
||||
["p1",9,"op","chess_char_6_04_b",2,"uniequip_002_acsupo",2465,1,137,8,65378,2465,0,91,2],
|
||||
["p1",10,"op","chess_char_6_05_b",2,"uniequip_002_acnipe",1643,1,7,22,90616,0,0,96,2],
|
||||
["p1",11,"op","chess_char_6_08_b",2,"uniequip_002_accast",1819,1,10,53,391951,0,0,83,2],
|
||||
["p1",12,"op","chess_char_6_09_b",2,"uniequip_002_acmedc",1394,1,12,0,0,16762,30129,83,3],
|
||||
["p1",8,"op","chess_char_6_01_b",2,"uniequip_002_acnipe",1643,1,5,0,948,0,0,4,1],
|
||||
["p1",9,"op","chess_char_6_04_b",2,"uniequip_002_acsupo",2465,1,141,7,73191,2465,0,92,2],
|
||||
["p1",10,"op","chess_char_6_05_b",2,"uniequip_002_acnipe",1643,1,7,20,89868,0,0,93,2],
|
||||
["p1",11,"op","chess_char_6_08_b",2,"uniequip_002_accast",1819,1,10,51,383282,0,0,78,2],
|
||||
["p1",12,"op","chess_char_6_09_b",2,"uniequip_002_acmedc",1394,1,12,0,0,16988,29638,83,3],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
@@ -522,19 +522,19 @@
|
||||
"enemy_1121_lifbos_2": [1,1,0,0,24000],
|
||||
"enemy_1148_dssbr": [1,1,0,0,2688],
|
||||
"enemy_1148_dssbr_2": [1,1,0,0,3456],
|
||||
"enemy_1158_divman": [1,1,0,195,3168],
|
||||
"enemy_1158_divman": [1,1,0,153,3168],
|
||||
"enemy_1160_hvyslr": [1,1,0,0,9600],
|
||||
"enemy_1160_hvyslr_2": [1,1,0,133,13440],
|
||||
"enemy_1161_tidmag": [1,1,0,1336,5760],
|
||||
"enemy_1161_tidmag_2": [1,1,0,1391,7680],
|
||||
"enemy_1249_lysdb_2": [60,60,0,1459,345600],
|
||||
"enemy_1161_tidmag": [1,1,0,554,5760],
|
||||
"enemy_1161_tidmag_2": [1,1,0,1758,7680],
|
||||
"enemy_1249_lysdb_2": [60,60,0,2085,345600],
|
||||
"enemy_1251_lysyta": [51,51,0,0,134640],
|
||||
"enemy_1329_cbshld": [39,39,0,3019,374400]
|
||||
"enemy_1329_cbshld": [39,39,0,2290,374400]
|
||||
},
|
||||
"hooks": [1,174,5967,836,836,1660,1803,179,159,159,40,38,0,28209,2688,60,158,0,1,159,3,1,23],
|
||||
"events": {"atk":974,"bounty":4,"deploy":13,"die":159,"dmg":1782,"engage":6,"fx":330,"heal":135,"layer":3,"skill":78,"spawn":173,"status":334},
|
||||
"fx": {"aoe":2,"burst":1,"dodge":28,"dp":68,"garrison":3,"shieldBreak":60,"slash":152,"sp":16},
|
||||
"snaps": "526d4c63 f04a2d68 53d511ef 5f8b4334 d796abdd 122e9876 81bad943 4c6a838a eb463850 1e1d2025 875b599b 2d763f55 51548a07 73b07326 88a8ea6f ec211cd8 e329b226 03d1aa7b 63fd2bb2 afee939a"
|
||||
"hooks": [1,174,5953,812,812,1599,1718,168,159,159,37,36,0,29335,2593,62,158,0,1,159,3,1,17],
|
||||
"events": {"atk":917,"bounty":4,"deploy":13,"die":159,"dmg":1707,"engage":7,"fx":298,"heal":127,"layer":3,"skill":73,"spawn":173,"status":324},
|
||||
"fx": {"aoe":2,"burst":1,"dodge":19,"dp":55,"garrison":3,"shieldBreak":60,"slash":144,"sp":12,"strike":2},
|
||||
"snaps": "526d4c63 f61576c6 e1be6631 53288d14 7de80a4f 26575d3b 14b2dc7b 09bff769 03f7af9b 81a2c2a0 80d45921 19ddd297 25520dc8 270c7d09 288a246a 05a0a7ec ed5b2582 a7da7717 af272d65 435b22b3"
|
||||
},
|
||||
"standin-10": {
|
||||
"about": "elite band 7 as stand-ins: chess_char_6_17_b→char_608_acpion chess_char_6_13_b→char_615_acspec chess_char_6_19_b→char_615_acspec chess_char_6_12_b→char_612_accast chess_char_6_15_b→char_614_acsupo chess_char_6_18_b→char_612_accast chess_char_6_20_b→char_613_acmedc",
|
||||
@@ -545,46 +545,46 @@
|
||||
"round": 13,
|
||||
"seed": 3949114127,
|
||||
"waveId": "act1autochess_h06",
|
||||
"time": 216.2,
|
||||
"ticks": 6486,
|
||||
"reason": "cleared",
|
||||
"killed": 25,
|
||||
"time": 230,
|
||||
"ticks": 6900,
|
||||
"reason": "timeout",
|
||||
"killed": 28,
|
||||
"total": 131,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 245,
|
||||
"rngDraws": 262,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {"killed":25,"total":131,"perfect":false,"leaks":106,"leaksUncounted":0,"coins":0,"dmg":508719,"bossDmg":0,"heal":10260,"deaths":25,"layerGains":{"raidShip":6}}
|
||||
"p1": {"killed":28,"total":131,"perfect":false,"leaks":103,"leaksUncounted":0,"coins":2,"dmg":537630,"bossDmg":0,"heal":10974,"deaths":25,"layerGains":{"raidShip":6}}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_17_b",2,"uniequip_002_acpion",0,0,164,8,199340,14855,0,64,9],
|
||||
["p1",2,"op","chess_char_6_13_b",2,"uniequip_002_acspec",0,0,163,6,43893,11872,0,99,0],
|
||||
["p1",3,"op","chess_char_6_19_b",2,"uniequip_002_acspec",1484,1,165,3,65411,16324,0,126,0],
|
||||
["p1",1,"op","chess_char_6_17_b",2,"uniequip_002_acpion",480.36,1,164,9,211459,13918,0,77,10],
|
||||
["p1",2,"op","chess_char_6_13_b",2,"uniequip_002_acspec",0,0,165,5,46951,13356,0,105,0],
|
||||
["p1",3,"op","chess_char_6_19_b",2,"uniequip_002_acspec",1484,1,166,4,74447,16324,0,138,0],
|
||||
["p1",4,"op","chess_char_6_12_b",2,"uniequip_002_accast",2265.49,1,4,4,78264,281,0,32,2],
|
||||
["p1",5,"op","chess_char_6_15_b",2,"uniequip_002_acsupo",0,0,147,2,52466,6493,0,38,3],
|
||||
["p1",6,"op","chess_char_6_18_b",2,"uniequip_002_accast",1819,1,8,2,69344,618,0,44,2],
|
||||
["p1",7,"op","chess_char_6_20_b",2,"uniequip_002_acmedc",1464,1,9,0,0,3373,10260,17,1],
|
||||
["p1",5,"op","chess_char_6_15_b",2,"uniequip_002_acsupo",0,0,147,2,54506,6494,0,39,3],
|
||||
["p1",6,"op","chess_char_6_18_b",2,"uniequip_002_accast",1819,1,8,4,72003,900,0,43,2],
|
||||
["p1",7,"op","chess_char_6_20_b",2,"uniequip_002_acmedc",1464,1,9,0,0,4263,10974,19,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_1006_shield_3": [21,1,20,14948,163592],
|
||||
"enemy_1043_zomsbr": [66,20,46,789,124139],
|
||||
"enemy_1162_magmot": [1,0,1,4063,11534],
|
||||
"enemy_1165_duhond": [1,1,0,139,4645],
|
||||
"enemy_1006_shield_3": [21,1,20,16907,164045],
|
||||
"enemy_1043_zomsbr": [66,22,44,926,136167],
|
||||
"enemy_1162_magmot": [1,0,1,4704,11483],
|
||||
"enemy_1165_duhond": [1,1,0,93,4645],
|
||||
"enemy_1165_duhond_2": [1,0,1,0,0],
|
||||
"enemy_1166_dusbr": [1,0,1,0,0],
|
||||
"enemy_1166_dusbr_2": [1,0,1,0,0],
|
||||
"enemy_1168_dumage": [1,1,0,1476,7465],
|
||||
"enemy_1168_dumage_2": [1,0,1,3178,0],
|
||||
"enemy_1169_duphlx": [1,0,1,0,0],
|
||||
"enemy_1402_tgshd_2": [36,2,34,29224,197345]
|
||||
"enemy_1168_dumage": [1,1,0,1897,7465],
|
||||
"enemy_1168_dumage_2": [1,1,0,4932,10783],
|
||||
"enemy_1169_duphlx": [1,0,1,0,1489],
|
||||
"enemy_1402_tgshd_2": [36,2,33,26077,201554]
|
||||
},
|
||||
"hooks": [1,165,6486,549,549,865,877,6809,50,159,17,36,0,8308,1125,49,131,106,1,50,1,1,0],
|
||||
"events": {"atk":554,"deploy":32,"die":53,"dmg":802,"engage":6,"fx":530,"heal":21,"layer":1,"leak":106,"skill":74,"spawn":140,"status":378},
|
||||
"fx": {"aoe":4,"blink":3,"burst":1,"displace":17,"dodge":3,"dp":17,"expose":414,"garrison":1,"garrisonGrant":3,"slash":67},
|
||||
"snaps": "80709a69 1b180600 2040e410 16970933 7ec28fab 061d6583 09f2da3e 98333fee 741e4051 c5f8165a 55db9474 b9b2bf46 d72aea5d 20d591d8 3077f062 ab32900d eb4540a2 37c492a6 3e3cd8f7 20a4cb8e 22ada53e b9ce62f2"
|
||||
"hooks": [1,166,6900,579,579,900,914,7045,53,158,18,38,0,8737,1167,49,131,102,1,53,1,1,0],
|
||||
"events": {"atk":586,"bounty":1,"deploy":33,"die":56,"dmg":848,"engage":6,"fx":538,"heal":25,"layer":1,"leak":102,"skill":78,"spawn":140,"status":384},
|
||||
"fx": {"aoe":5,"blink":3,"burst":1,"displace":18,"dodge":1,"dp":19,"expose":413,"garrison":1,"garrisonGrant":3,"slash":74},
|
||||
"snaps": "fba27448 de0d1730 e90ea321 52a21ef0 85a0833c 2a8e4ffa 3b4a24e3 f3d67a0b 9f0134f0 9cb0d19a e7da4c3d 843017d8 4557a167 130155ab 0afb1b21 d4aa9c6c c0ffca41 9e9e17c9 975fc2dd 20398d66 00b5e499 52760797 5a270974"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,9 @@ describe('#8 深池逐火: a knock-out is a walking 隐匿 ember that a blocker
|
||||
for (const u of dusk) assert.equal(deaths.get(u.id), 1, `${u.defId} #${u.id} dies exactly once`);
|
||||
assert.equal(ember.size, dusk.length, 'every 逐火 went through its ember');
|
||||
assert.deepEqual([...bad], [], 'ember rules');
|
||||
assert.ok(walked > 1, `an ember whose blocker fell walks on (${walked.toFixed(2)} tiles)`);
|
||||
// (until 0.2.0 one of 百炼嘉维尔's embers walked on once she fell; since 0.2.0 she — a 强攻手 — strikes up to her block count
|
||||
// of targets (ai.js targetCount) and holds: an ember whose blocker falls is checked on its own in the test below)
|
||||
assert.ok(walked >= 0);
|
||||
});
|
||||
|
||||
for (const key of EMBERS) {
|
||||
@@ -198,6 +200,20 @@ describe('#8 深池逐火: a knock-out is a walking 隐匿 ember that a blocker
|
||||
assert.equal(h2.b.killed, 1);
|
||||
assert.equal(h2.eventsOf('die').filter((ev) => ev[1] === f.id).length, 1, 'one die event');
|
||||
checkInvariants(h2.b);
|
||||
|
||||
// its blocker falls: the ember walks on (隐匿 again — nobody else can target it)
|
||||
const h3 = arena({ units: [{ chessId: 't_wall', row: 9, col: 7 }], kits: QUIET_GUNS });
|
||||
h3.step();
|
||||
const g = put(h3, key, { mods: { speedMul: 1 } });
|
||||
assert.ok(h3.runUntil(() => !!g.blockedBy, 20), 'blocked');
|
||||
h3.b.kill(g, null);
|
||||
h3.run(HUSK_REBIRTH + 0.1);
|
||||
assert.ok(g.blockedBy, 'an ember held by its blocker');
|
||||
const gx = g.x, gy = g.y;
|
||||
h3.b.kill(h3.unit('t_wall'), null);
|
||||
h3.run(3);
|
||||
assert.ok(g.alive && Math.hypot(g.x - gx, g.y - gy) > 0.5, `an ember whose blocker fell walks on (${Math.hypot(g.x - gx, g.y - gy).toFixed(2)} tiles)`);
|
||||
checkInvariants(h3.b);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// `attackAnim`, tools/build-data.mjs from the asset manifest; server/sim/ai.js attackStand). It used to stop only
|
||||
// ATTACK_PAUSE (0.35 s) after each strike while the client played the attack clip over the whole walk. An enemy with no
|
||||
// clip known keeps the old 0.35 s (test/sim/combat.test.js); a 「不停止移动」 attacker never stops; a stun cuts the stand.
|
||||
// Since 0.2.0 (GitHub #187 / #170) every attack has its wind-up — the strike comes at the clip's damage frame after the
|
||||
// swing starts, also the first one of an enemy whose cooldown ran out before it had a target (it used to strike at once)
|
||||
// — and a stun before the frame cuts the swing (test/sim/feedback5-enemy-swing.test.js).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -101,9 +104,10 @@ test('a 「不停止移动」 attacker (attackMoves) keeps walking through its a
|
||||
};
|
||||
const moving = run(true), standing = run(false);
|
||||
assert.ok(moving.atks.length >= 3 && standing.atks.length >= 3);
|
||||
for (const t of moving.atks.slice(1, 3)) assert.ok(walked(moving.log, t - 0.5, t + 1) > 0.6, `walks through the attack at ${t.toFixed(2)}`);
|
||||
// (the first attack lands as the target comes into range, its cooldown long over: no wind-up to stand through)
|
||||
for (const t of standing.atks.slice(1, 3)) assert.equal(walked(standing.log, t - 0.5 + 2 * TICK, t + 1 - TICK), 0, 'the same enemy without it stands');
|
||||
for (const t of moving.atks.slice(0, 3)) assert.ok(walked(moving.log, t - 0.5, t + 1) > 0.6, `walks through the attack at ${t.toFixed(2)}`);
|
||||
// (since 0.2.0 the first attack too: its cooldown long over as the target comes into range, it swings from the start —
|
||||
// the whole 0.5 s wind-up stood before the strike)
|
||||
for (const t of standing.atks.slice(0, 3)) assert.equal(walked(standing.log, t - 0.5 + 2 * TICK, t + 1 - TICK), 0, 'the same enemy without it stands');
|
||||
});
|
||||
|
||||
test('no attack clip known: ATTACK_PAUSE after the strike and no wind-up stand; a stun cuts a stand short', () => {
|
||||
@@ -116,8 +120,13 @@ test('no attack clip known: ATTACK_PAUSE after the strike and no wind-up stand;
|
||||
});
|
||||
h.step();
|
||||
const e = h.enemy('enemy_slow');
|
||||
assert.ok(e.lastAttackAt >= 0, 'attacked at once');
|
||||
assert.ok(Math.abs(e.atkStandUntil - (h.b.time - TICK + 2.5)) < 1e-6, 'stands the 2.5 s after the strike');
|
||||
// (0.2.0: a target in range at once, its cooldown over — it swings at once and strikes at the 0.5 s damage frame; it
|
||||
// used to strike in the first tick)
|
||||
assert.equal(e.lastAttackAt, -Infinity, 'swinging, not struck yet');
|
||||
h.runUntil(() => e.lastAttackAt >= 0, 2);
|
||||
assert.ok(Math.abs(e.lastAttackAt - (TICK + 0.5)) < TICK + 1e-6, `struck at the damage frame (${e.lastAttackAt.toFixed(3)})`);
|
||||
h.step();
|
||||
assert.ok(Math.abs(e.atkStandUntil - (e.lastAttackAt + 2.5)) < 1e-6, 'stands the 2.5 s after the strike');
|
||||
h.run(0.5);
|
||||
const x0 = e.x;
|
||||
h.b.applyStatus(e, 'stun', { duration: 0.5 });
|
||||
@@ -156,8 +165,9 @@ test('a ranged enemy with a target in range on a WAIT checkpoint leaves when the
|
||||
assert.equal(free.attacks, 0);
|
||||
assert.ok(sniper.attacks >= 2 && quick.attacks >= 5);
|
||||
assert.ok(Math.abs(sniper.waited - free.waited) < 1e-9 && Math.abs(quick.waited - free.waited) < 1e-9, `the wait ends at ${free.waited.toFixed(2)} s for all three`);
|
||||
// it walks on once the attack clip running at the end of the wait is over (the attack at 2.7 s: until 3.2 s)
|
||||
assert.ok(sniper.moved > free.moved && sniper.moved <= 3.2 + 2 * TICK, `leaves at ${sniper.moved?.toFixed(2)} s (no target: ${free.moved.toFixed(2)} s)`);
|
||||
// it walks on once the attack clip running at the end of the wait is over (since 0.2.0 the first strike at its 0.5 s
|
||||
// damage frame, the next one 2.7 s later at 3.2 s: its clip until 3.7 s; it used to strike at once — until 3.2 s)
|
||||
assert.ok(sniper.moved > free.moved && sniper.moved <= 3.7 + 2 * TICK, `leaves at ${sniper.moved?.toFixed(2)} s (no target: ${free.moved.toFixed(2)} s)`);
|
||||
assert.equal(quick.moved, null, 'an enemy attacking quicker than its clip stands while the target stays in range');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// test/sim/feedback5-block-targets.test.js — "同时攻击阻挡的所有敌人" takes up to the block count of targets (the queued 0.2.0
|
||||
// question; PRTS 分支特性信息 强攻手 / 重剑手 / 推击手 "普通攻击最大目标数等于阻挡数(不会低于1)", PRTS 作战机制 §AOE伤害判定 "锁定人数的无
|
||||
// 弹道AOE攻击(例如近卫分支“强攻手”)…在抬手时选取范围内的全体目标(不超过其攻击目标上限)"; the client's selector flag
|
||||
// _limitedMaxTargetNumToBlockedCnt) — server/sim/ai.js acquireTargets / targetCount (`hitAllBlocked`). Until 0.2.0 it struck
|
||||
// the enemies it blocked only (one in range when it blocked none).
|
||||
// Run: node --test test/sim/feedback5-block-targets.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
// a 强攻手 (block 3) whose range is its tile and the three ahead
|
||||
const CENT = chessRec({ id: 't_cent', subProfessionId: 'centurion', stats: { maxHp: 1e6, atk: 100, def: 1e4, blockCnt: 3, bat: 1 }, skill: null, rangeGrid: [[0, 0], [0, 1], [0, 2], [0, 3]] });
|
||||
const dummy = (key) => enemyRec({ key, hp: 1e9, speed: 0, atk: 0 });
|
||||
|
||||
/** The targets of its first attack, with enemies at `at` ([row, col] each). */
|
||||
function firstAttack(at, setup = null) {
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_cent: CENT }, enemies: { e: dummy('e') } }, units: [{ chessId: 't_cent', row: 10, col: 5 }],
|
||||
enemies: at.map((pos) => ({ key: 'e', pos })), content: 'none', autoFinish: false, timeLimit: 30, hooks: ['attack'], captureNoisy: true,
|
||||
setup,
|
||||
});
|
||||
const u = h.unit('t_cent');
|
||||
assert.ok(h.runUntil(() => h.hooksOf('attack').some((c) => c.attacker === u), 3));
|
||||
const atk = h.hooksOf('attack').find((c) => c.attacker === u);
|
||||
checkInvariants(h.b);
|
||||
return { h, u, targets: atk.targets };
|
||||
}
|
||||
|
||||
test('blocking one enemy with two more in its range: one attack strikes all three (up to its block count, the blocked one first)', () => {
|
||||
const { u, targets } = firstAttack([[10, 5], [10, 6], [10, 7]]);
|
||||
assert.equal(u.blocking.length, 1, 'it blocks the one on its tile');
|
||||
assert.equal(targets.length, 3);
|
||||
assert.equal(targets[0], u.blocking[0], 'the blocked enemy first');
|
||||
});
|
||||
|
||||
test('blocking none: up to its block count from the range; never more than the block count; never fewer than 1', () => {
|
||||
const free = firstAttack([[10, 6], [10, 7], [10, 8]]);
|
||||
assert.equal(free.u.blocking.length, 0);
|
||||
assert.equal(free.targets.length, 3, 'three in range, none blocked: three targets');
|
||||
const crowd = firstAttack([[10, 5], [10, 6], [10, 6], [10, 7], [10, 7], [10, 8]]);
|
||||
assert.equal(crowd.targets.length, 3, 'six in reach: its block count of them');
|
||||
assert.ok(crowd.u.blocking.every((e) => crowd.targets.includes(e)), 'every enemy it blocks among them');
|
||||
const zero = firstAttack([[10, 6], [10, 7]], (b) => {
|
||||
b.on('deploy', (c) => { if (c.unit.defId === 't_cent') b.addBuff(c.unit, { key: 'test:noBlock', mods: { blockCnt: -99 } }); });
|
||||
});
|
||||
assert.equal(zero.targets.length, 1, 'block count 0: still one target (不会低于1)');
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
// test/sim/feedback5-chain-heal.test.js — the 链愈师 heal chain (follow-up 27; PRTS 分支特性信息 链愈师 "跳跃范围为x-4,无特殊说明
|
||||
// 的场合一次治疗链不会对已跳跃过的单位重复跳跃", "优先跳跃至范围内生命比例最低>部署时间点最晚的我方单位。可选择满生命我方单位为跳跃目标,
|
||||
// 但仍受禁疗制约") — server/sim/ai.js chainHealNext / doHeal (莎草's profession chain; Mon3tr's kit uses the same links). It used
|
||||
// to jump to the most injured ally within 2.5 tiles (never a full-HP one, ties by list order).
|
||||
// Run: node --test test/sim/feedback5-chain-heal.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const GRID = []; for (let r = -2; r <= 2; r++) for (let c = 0; c <= 6; c++) GRID.push([r, c]);
|
||||
const HEALER = chessRec({ id: 't_chain', profession: 'MEDIC', subProfessionId: 'chainhealer', stats: { maxHp: 1e4, atk: 100, bat: 1 }, skill: null, rangeGrid: GRID });
|
||||
const wall = (id) => chessRec({ id, stats: { maxHp: 1e4, atk: 0, def: 0 }, skill: null });
|
||||
|
||||
/** The healer on (10, 2) and walls at `at` = { id: [row, col, hpRatio] }; returns the chain of its first heal (ids → wall ids). */
|
||||
function firstChain(at, setup = null) {
|
||||
const ids = Object.keys(at);
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_chain: HEALER, ...Object.fromEntries(ids.map((id) => [id, wall(id)])) } },
|
||||
units: [{ chessId: 't_chain', row: 10, col: 2 }, ...ids.map((id) => ({ chessId: id, row: at[id][0], col: at[id][1] }))],
|
||||
content: 'none', autoFinish: false, timeLimit: 30, seed: 3,
|
||||
});
|
||||
h.step(); // deployed (the healer heals nobody yet: everyone is at full HP)
|
||||
for (const id of ids) { const u = h.unit(id); u.hp = u.s.maxHp * at[id][2]; }
|
||||
if (setup) setup(h);
|
||||
const n0 = h.events.length;
|
||||
for (let i = 0; i < 90 && !h.events.slice(n0).some((e) => e[0] === 'atk'); i++) h.step(); // (step drains the events)
|
||||
const name = new Map(h.b.allyUnits.map((u) => [u.id, u.defId]));
|
||||
const ev = h.events.slice(n0).filter((e) => e[0] === 'atk');
|
||||
checkInvariants(h.b);
|
||||
return { h, main: name.get(ev.find((e) => e[3] !== 'chainHeal')[2]), jumps: ev.filter((e) => e[3] === 'chainHeal').map((e) => [name.get(e[1]), name.get(e[2])]) };
|
||||
}
|
||||
|
||||
test('the jump reaches the 3×3 around the last one healed (x-4), not an ally two tiles away; each jump goes on from where it landed', () => {
|
||||
// A (most injured) on (10, 5); B two tiles right of it (inside 2.5 tiles, outside its 3×3), C on its diagonal
|
||||
const r = firstChain({ t_a: [10, 5, 0.5], t_b: [10, 7, 0.6], t_c: [11, 6, 0.7] });
|
||||
assert.equal(r.main, 't_a');
|
||||
assert.deepEqual(r.jumps, [['t_a', 't_c'], ['t_c', 't_b']], 'A → C (its 3×3) → B (C\'s 3×3); it used to jump A → B → C');
|
||||
});
|
||||
|
||||
test('a full-HP ally is a jump target when no injured one is in reach — healed for nothing, the chain jumps on from it', () => {
|
||||
const r = firstChain({ t_a: [10, 5, 0.5], t_d: [10, 6, 1], t_e: [10, 7, 0.6] });
|
||||
assert.equal(r.main, 't_a');
|
||||
assert.deepEqual(r.jumps, [['t_a', 't_d'], ['t_d', 't_e']], 'A → D (full HP) → E');
|
||||
assert.equal(r.h.unit('t_d').hp, r.h.unit('t_d').s.maxHp);
|
||||
});
|
||||
|
||||
test('ties of the HP ratio: the latest deployed first; a 禁疗 unit is never a jump target', () => {
|
||||
const tie = firstChain({ t_a: [10, 5, 0.5], t_f: [9, 5, 0.8], t_g: [11, 5, 0.8] });
|
||||
const [f, g] = [tie.h.unit('t_f'), tie.h.unit('t_g')];
|
||||
const later = f.aggroSeq > g.aggroSeq ? 't_f' : 't_g';
|
||||
assert.deepEqual(tie.jumps[0], ['t_a', later], 'the later deployed of the two');
|
||||
const banned = firstChain({ t_a: [10, 5, 0.5], t_n: [10, 6, 0.2], t_m: [11, 5, 0.9] }, (h) => {
|
||||
h.b.addBuff(h.unit('t_n'), { key: 'test:healFree', status: 'healFree', flags: { noHeal: true, healFree: true } });
|
||||
});
|
||||
assert.equal(banned.main, 't_a', 'the 禁疗 unit is no main target either');
|
||||
assert.deepEqual(banned.jumps[0], ['t_a', 't_m'], 'the 禁疗 unit is skipped');
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
// test/sim/feedback5-enemy-swing.test.js — an enemy's normal attack strikes at its clip's damage frame, and a stun before the
|
||||
// frame cuts the swing (GitHub #187 point 1 「短轴眩晕无法打断敌人攻击,比如:六卡西米尔、异德三、忍冬三」, #170; PRTS 状态机
|
||||
// ATTACK / COMBAT "每帧检查异常状态,若有则切换到异常状态的状态", PRTS 异常效果 STUNNED / DISARMED "正在进行的普通攻击将被中断") —
|
||||
// server/sim/ai.js updateEnemy / enemyAttack / attackWindup. The damage frame is data/enemies.json `attackAnim.hit` (the
|
||||
// clip's strike frame), shortened with the clip when the attacks come quicker than it; no clip known ⇒ no wind-up.
|
||||
// Run: node --test test/sim/feedback5-enemy-swing.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { TICK } from '../../server/sim/constants.js';
|
||||
import { attackWindup } from '../../server/sim/ai.js';
|
||||
|
||||
const HIT = 0.4; // the clip's strike frame
|
||||
const BAT = 2; // attack interval
|
||||
const near = (a, b, msg, ticks = 2) => assert.ok(Math.abs(a - b) <= ticks * TICK + 1e-9, `${msg}: ${a.toFixed(3)} vs ${b.toFixed(3)}`);
|
||||
|
||||
/** A wall operator and one enemy (melee on the wall's tile — blocked at once —, or ranged 3 tiles away). */
|
||||
function field({ ranged = false, clip = true } = {}) {
|
||||
const rec = {
|
||||
...enemyRec({ key: 'enemy_sw', hp: 1e9, atk: 100, bat: BAT, speed: 0, ...(ranged ? { range: 3 } : null) }),
|
||||
...(clip ? { attackAnim: { clip: 'Attack', dur: 1, hit: HIT } } : null),
|
||||
};
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_wall: chessRec({ id: 't_wall', stats: { maxHp: 1e9, atk: 0, def: 0 }, skill: null }) }, enemies: { enemy_sw: rec } },
|
||||
units: [{ chessId: 't_wall', row: 10, col: 6 }], enemies: [{ key: 'enemy_sw', pos: ranged ? [10, 9] : [10, 6] }],
|
||||
content: 'none', autoFinish: false, timeLimit: 60, hooks: ['attack', 'damaged'], captureNoisy: true,
|
||||
});
|
||||
h.step();
|
||||
return { h, e: h.enemy('enemy_sw'), wall: h.unit('t_wall') };
|
||||
}
|
||||
const strikes = (h, e) => h.hooksOf('attack').filter((c) => c.attacker === e).map((c) => c.t);
|
||||
const hits = (h, e) => h.hooksOf('damaged').filter((c) => c.source === e && c.dmg.isAttack).map((c) => c.t);
|
||||
const done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
|
||||
|
||||
test('attackWindup: the clip\'s strike frame, shortened with the clip when the attacks come quicker than it; 0 with no clip', () => {
|
||||
const w = (anim, interval) => attackWindup({ def: { attackAnim: anim }, s: { interval } });
|
||||
assert.equal(w({ dur: 1, hit: 0.4 }, 2), 0.4);
|
||||
assert.equal(w({ dur: 2, hit: 1 }, 1), 0.5);
|
||||
assert.equal(w({ dur: 1 }, 2), 0.5, 'no strike frame named: half the clip');
|
||||
assert.equal(w(undefined, 2), 0);
|
||||
});
|
||||
|
||||
test('a blocked enemy swings when its cooldown is over and strikes at the damage frame: the first strike one wind-up after the block (its cooldown long over), then one per attack interval', () => {
|
||||
const { h, e } = field();
|
||||
const t0 = h.b.time; // blocked in the first tick
|
||||
h.run(4.6);
|
||||
const s = strikes(h, e);
|
||||
assert.ok(s.length >= 3, `${s.length} strikes`);
|
||||
near(s[0], t0 + HIT, 'the first strike: the whole wind-up from the block (it used to land at once)');
|
||||
near(s[1] - s[0], BAT, 'then one per interval', 1);
|
||||
near(s[2] - s[1], BAT, 'then one per interval', 1);
|
||||
assert.deepEqual(hits(h, e), s, 'melee: the damage at the strike');
|
||||
done(h);
|
||||
});
|
||||
|
||||
for (const dur of [0.1, 0.2]) {
|
||||
test(`a ${dur} s stun before the damage frame cuts the swing: no strike at that frame, the attack starts again from its wind-up after the stun (卡西米尔 6's pulse, 忍冬 S3)`, () => {
|
||||
const { h, e } = field();
|
||||
assert.ok(h.runUntil(() => strikes(h, e).length === 1, 2));
|
||||
const t1 = strikes(h, e)[0];
|
||||
// its next swing starts at t1 + BAT − HIT; the stun lands 0.2 s into it
|
||||
h.runUntil(() => h.b.time >= t1 + BAT - HIT + 0.2 - 1e-9, 3);
|
||||
assert.ok(e.swing, 'swinging');
|
||||
const tStun = h.b.time;
|
||||
h.b.applyStatus(e, 'stun', { duration: dur });
|
||||
h.run(dur + HIT + 0.3);
|
||||
const s = strikes(h, e);
|
||||
assert.equal(s.filter((t) => t > t1 && t < tStun + dur).length, 0, 'the cut swing never struck');
|
||||
near(s[1], tStun + dur + HIT, 'the next strike: the stun, then the whole wind-up again', 3);
|
||||
assert.ok(s[1] > t1 + BAT + dur + 0.1, `later than a mere pause of the countdown (${(t1 + BAT + dur).toFixed(2)})`);
|
||||
done(h);
|
||||
});
|
||||
}
|
||||
|
||||
test('a stun during the cooldown (before the swing) only pauses it; 缴械 cuts a swing like a stun; a strike already made stays made (a ranged shot in flight lands)', () => {
|
||||
// the cooldown: a 0.5 s stun 0.5 s after a strike delays the next one by 0.5 s
|
||||
const a = field();
|
||||
assert.ok(a.h.runUntil(() => strikes(a.h, a.e).length === 1, 2));
|
||||
const t1 = strikes(a.h, a.e)[0];
|
||||
a.h.run(0.5);
|
||||
a.h.b.applyStatus(a.e, 'stun', { duration: 0.5 });
|
||||
a.h.run(BAT + 0.3);
|
||||
near(strikes(a.h, a.e)[1], t1 + BAT + 0.5, 'paused by the stun, no new wind-up', 3);
|
||||
done(a.h);
|
||||
// 缴械 mid-swing: the swing is cut, a new one starts once it ends
|
||||
const b = field();
|
||||
assert.ok(b.h.runUntil(() => strikes(b.h, b.e).length === 1, 2));
|
||||
const u1 = strikes(b.h, b.e)[0];
|
||||
b.h.runUntil(() => b.h.b.time >= u1 + BAT - HIT + 0.2 - 1e-9, 3);
|
||||
const tD = b.h.b.time;
|
||||
b.h.b.addBuff(b.e, { key: 'test:disarm', duration: 0.1, flags: { disarm: true } });
|
||||
b.h.run(HIT + 0.5);
|
||||
near(strikes(b.h, b.e)[1], tD + 0.1 + HIT, '缴械 "正在进行的普通攻击将被中断": the whole wind-up after it', 3);
|
||||
done(b.h);
|
||||
// ranged: stunned just after the strike — the shot flies on and lands
|
||||
const c = field({ ranged: true });
|
||||
assert.ok(c.h.runUntil(() => strikes(c.h, c.e).length === 1, 3));
|
||||
assert.equal(hits(c.h, c.e).length, 0, 'the shot is in flight');
|
||||
c.h.b.applyStatus(c.e, 'stun', { duration: 1 });
|
||||
c.h.run(0.6);
|
||||
assert.equal(hits(c.h, c.e).length, 1, 'a strike already made is not undone');
|
||||
done(c.h);
|
||||
});
|
||||
|
||||
test('no attack clip known: no wind-up — it strikes as its swing starts, so a stun only pauses its cooldown (the rule before 0.2.0)', () => {
|
||||
const { h, e } = field({ clip: false });
|
||||
assert.equal(attackWindup(e), 0);
|
||||
h.step();
|
||||
assert.equal(strikes(h, e).length, 1, 'struck in its first tick blocked');
|
||||
const t1 = strikes(h, e)[0];
|
||||
h.runUntil(() => h.b.time >= t1 + BAT - 0.1 - 1e-9, 3);
|
||||
h.b.applyStatus(e, 'stun', { duration: 0.1 });
|
||||
h.run(0.5);
|
||||
near(strikes(h, e)[1], t1 + BAT + 0.1, 'paused by the stun', 3);
|
||||
done(h);
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
// test/sim/feedback5-musha-heal.test.js — the 武者 trait heal on every damage instance (PRTS 分支特性信息 武者 "特性治疗于干员每次
|
||||
// 输出伤害时触发(不局限于攻击)", "常态持有禁疗;通过自身特性/天赋/技能产生的作用于自身的治疗效果会无视自身的禁疗") —
|
||||
// server/sim/professions.js installMushaHeal; 赫拉格's kit no longer patches his double strikes (op-helage.js), 左乐's S1 reads
|
||||
// his HP when it fires (op-zuole.js).
|
||||
// Run: node --test test/sim/feedback5-musha-heal.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const VALUE = 50;
|
||||
/** A 武者 (trait bb value 50) hitting `hits` times per attack; an enemy that dodges `dodge` of physical hits. */
|
||||
function field({ hits = 1, dodge = 0, kit = null } = {}) {
|
||||
const musha = chessRec({ id: 't_musha', subProfessionId: 'musha', stats: { maxHp: 1e6, atk: 100, bat: 1, def: 0 }, skill: null });
|
||||
musha.trait = { desc: '', bb: { value: VALUE } };
|
||||
const medic = chessRec({ id: 't_medic', profession: 'MEDIC', stats: { maxHp: 1000, atk: 300, bat: 1 }, skill: null, rangeGrid: [[0, 0], [0, -1], [0, -2]] });
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_musha: musha, t_medic: medic }, enemies: { e: { ...enemyRec({ key: 'e', hp: 1e9, speed: 0 }) } } },
|
||||
kits: { t_musha: () => ({ trait: { hits, ...(kit?.trait ?? {}) }, ...(kit?.rest ?? {}) }) },
|
||||
units: [{ chessId: 't_musha', row: 10, col: 5 }, { chessId: 't_medic', row: 10, col: 7 }], enemies: [{ key: 'e', pos: [10, 6] }],
|
||||
content: 'generic', autoFinish: false, timeLimit: 60, hooks: ['heal', 'damaged', 'attack', 'dodge'], captureNoisy: true,
|
||||
setup: (b) => { if (dodge) b.on('enemySpawn', ({ enemy }) => { b.addBuff(enemy, { key: 'test:dodge', mods: { dodgePhys: dodge } }); }); },
|
||||
});
|
||||
h.step();
|
||||
const u = h.unit('t_musha');
|
||||
u.hp = u.s.maxHp / 2;
|
||||
return { h, u, e: h.enemies()[0] };
|
||||
}
|
||||
const traitHeals = (h, u) => h.hooksOf('heal').filter((c) => c.source === u && c.target === u && c.opts?.self && !c.opts?.tags);
|
||||
const done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
|
||||
|
||||
test('a double strike heals twice: one heal of the trait value per damage instance of the attack (it used to heal once per enemy struck)', () => {
|
||||
const { h, u } = field({ hits: 2 });
|
||||
h.run(3.05);
|
||||
const atks = h.hooksOf('attack').filter((c) => c.attacker === u).length;
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg.isAttack).length;
|
||||
assert.ok(atks >= 3 && hits === 2 * atks, `${atks} attacks, ${hits} hits`);
|
||||
assert.equal(traitHeals(h, u).length, hits, 'a heal per hit');
|
||||
assert.ok(traitHeals(h, u).every((c) => c.amount === VALUE));
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('every damage instance it outputs heals, not only attacks; a dodged hit, a 流失, an element 损伤 and a talent / DoT damage do not', () => {
|
||||
const { h, u, e } = field({ dodge: 1 });
|
||||
h.run(2.05);
|
||||
assert.ok(h.hooksOf('dodge').some((c) => c.source === u), 'its hits were dodged');
|
||||
assert.equal(traitHeals(h, u).length, 0, 'a dodged hit deals no damage: no heal');
|
||||
const n0 = traitHeals(h, u).length;
|
||||
h.b.dealDamage(u, e, { amount: 100, type: 'arts', isSkill: true, canDodge: false, tags: ['skill'] });
|
||||
h.b.dealDamage(u, e, { amount: 100, type: 'true', canDodge: false, tags: ['item', 'item:test'] });
|
||||
assert.equal(traitHeals(h, u).length - n0, 2, 'skill and item damage heal');
|
||||
h.b.loseHp(e, 100, { source: u });
|
||||
h.b.dealDamage(u, e, { type: 'element', element: 'burn', amount: 100 });
|
||||
h.b.dealDamage(u, e, { amount: 100, type: 'arts', canDodge: false, tags: ['talent', 'dot'] });
|
||||
assert.equal(traitHeals(h, u).length - n0, 2, 'no heal for a 流失, an element 损伤 or a DoT');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('禁疗 does not stop the trait heal ("会无视自身的禁疗"); another unit\'s heal still does not reach it', () => {
|
||||
const { h, u } = field();
|
||||
h.b.addBuff(u, { key: 'test:healFree', status: 'healFree', flags: { noHeal: true, healFree: true } });
|
||||
const hp0 = u.hp;
|
||||
h.run(1.05);
|
||||
assert.ok(traitHeals(h, u).length >= 1 && u.hp > hp0, 'healed through 禁疗');
|
||||
assert.equal(h.hooksOf('heal').filter((c) => c.target === u && c.source !== u).length, 0, 'the medic never heals it');
|
||||
assert.equal(h.b.heal(h.unit('t_medic'), u, 500), 0);
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('宴 (the chess): one heal per hit of her attacks', () => {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { e: enemyRec({ key: 'e', hp: 1e9, speed: 0 }) } }, units: [{ chessId: 'chess_char_1_18_a', row: 10, col: 5 }],
|
||||
enemies: [{ key: 'e', pos: [10, 6] }], autoFinish: false, timeLimit: 60, hooks: ['heal', 'damaged'], captureNoisy: true, seed: 5,
|
||||
});
|
||||
h.run(8);
|
||||
const u = h.unit('chess_char_1_18_a');
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.target.side === 'enemy' && c.type !== 'element' && !(c.dmg.tags || []).includes('dot')).length;
|
||||
assert.ok(hits >= 3);
|
||||
assert.equal(traitHeals(h, u).length, hits);
|
||||
done(h);
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
// test/sim/feedback5-mystic-store.test.js — the 秘术师 energy store at the attack check (GitHub #181; PRTS 分支特性信息 秘术师
|
||||
// "能量储存与攻击占用相同的攻击间隔:在进行攻击判定时,若范围内存在有效目标,则进行普通攻击;若无有效目标且能量储存数未满,则改为储存
|
||||
// 一份攻击能量(属于攻击行为);仅没有有效目标,且能量储存数已满的情况下才进入待机状态", "攻击被打断且弹道未能成功生成的情况下,储存的
|
||||
// 能量不会被消耗"; PRTS 异常效果 缴械 "秘术师储存能量同样无法进行") — server/sim/professions.js installMystic (storeEnergy /
|
||||
// releaseEnergy / hitsFn), server/sim/ai.js updateAlly / performAttack, 深靛's kit (a bound enemy is no valid target).
|
||||
// Run: node --test test/sim/feedback5-mystic-store.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
|
||||
const ds = getDefaultSource();
|
||||
const ID = 'chess_char_1_17_a'; // 深靛 (normal: trait times 3)
|
||||
const ELITE = 'chess_char_1_17_b'; // 深靛 elite (MSC-X: times 4)
|
||||
const TICK = 1 / 30;
|
||||
const approx = (a, b, msg, eps = TICK + 1e-6) => assert.ok(Math.abs(a - b) <= eps, `${msg}: ${a} vs ${b}`);
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, ...o });
|
||||
/** Her record without the talent's random bind (柔光缚目 prob 0): the binds of these tests are placed by hand. */
|
||||
const noBind = (id) => {
|
||||
const r = ds.rawChess(id);
|
||||
return { [id]: { ...r, talents: r.talents.map((t) => ({ ...t, bb: { ...t.bb, prob: 0 } })) } };
|
||||
};
|
||||
|
||||
function field({ id = ID, enemies = [], setup } = {}) {
|
||||
return makeBattle({
|
||||
seed: 3, autoFinish: false, timeLimit: 300, hooks: ['attack', 'damaged'], captureNoisy: true, setup,
|
||||
defs: { chess: noBind(id), enemies: { e: dummy('e') } }, units: [{ chessId: id, row: 10, col: 4 }], enemies,
|
||||
});
|
||||
}
|
||||
const attacksOf = (h, u) => h.hooksOf('attack').filter((c) => c.attacker === u);
|
||||
const done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
|
||||
|
||||
test('no target: her first attack check stores at once (an attack action: the interval restarts), then one energy per attack interval up to the trait times; full ⇒ idle with the attack ready', () => {
|
||||
for (const [id, max] of [[ID, 3], [ELITE, 4]]) {
|
||||
const h = field({ id });
|
||||
const u = h.unit(id);
|
||||
h.step();
|
||||
assert.equal(u.trait.stored, 1, `${id}: the attack is ready at deployment — stored at the first check`);
|
||||
const iv = u.s.interval;
|
||||
approx(u.atkCd, iv, `${id}: storing took the attack interval`, 1e-9);
|
||||
let last = h.b.time;
|
||||
for (let n = 2; n <= max; n++) {
|
||||
assert.ok(h.runUntil(() => u.trait.stored === n, iv + 1), `${id}: energy ${n}`);
|
||||
approx(h.b.time - last, iv, `${id}: energy ${n} one interval after the last`);
|
||||
last = h.b.time;
|
||||
}
|
||||
h.run(3 * iv);
|
||||
assert.equal(u.trait.stored, max, `${id}: at most ${max}`);
|
||||
assert.equal(u.atkCd, 0, `${id}: full — idle, the attack ready`);
|
||||
assert.equal(attacksOf(h, u).length, 0);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('a target in range while she holds energies: attacked at once, the energies leave with that attack and land with its main hit, one arts attack hit each', () => {
|
||||
const h = field({ enemies: [{ key: 'e', pos: [10, 6], time: 12 }] });
|
||||
const u = h.unit(ID);
|
||||
assert.ok(h.runUntil(() => attacksOf(h, u).length === 1, 15));
|
||||
approx(h.b.time, 12, 'attacked in the tick the enemy appeared (the full store idled with the attack ready)', 2 * TICK);
|
||||
assert.equal(u.trait.stored, 0, 'released with the attack (before the bolt lands)');
|
||||
h.run(1);
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg.isAttack);
|
||||
assert.equal(hits.length, 1 + 3, 'the main hit + 3 energies');
|
||||
assert.ok(hits.every((c) => c.type === 'arts' && c.dmg.attackId === hits[0].dmg.attackId), 'arts, one attack');
|
||||
assert.ok(hits.every((c) => Math.abs(c.amount - hits[0].amount) < 1e-6), 'each energy hits as the main hit');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('a bind on her only target is no valid target: her next attack check stores an energy, and the freed enemy waits for the interval the store took (GitHub #181)', () => {
|
||||
const h = field({ enemies: [{ key: 'e', pos: [10, 6] }] });
|
||||
const u = h.unit(ID);
|
||||
assert.ok(h.runUntil(() => attacksOf(h, u).length === 1, 2));
|
||||
const t0 = h.b.time, iv = u.s.interval;
|
||||
assert.equal(u.trait.stored, 0);
|
||||
const [e] = h.enemies();
|
||||
h.b.applyStatus(e, 'bind', { duration: 4 });
|
||||
assert.ok(h.runUntil(() => u.trait.stored === 1, iv + 1));
|
||||
approx(h.b.time - t0, iv, 'stored at her next attack check, during the bind');
|
||||
assert.equal(attacksOf(h, u).length, 1, 'no attack at the bound enemy');
|
||||
assert.ok(h.runUntil(() => attacksOf(h, u).length === 2, iv + 1));
|
||||
approx(h.b.time - t0, 2 * iv, 'the bind ended at +4 s; her next check comes an interval after the store', 2 * TICK + 1e-6);
|
||||
assert.equal(u.trait.stored, 0, 'the energy left with that attack');
|
||||
h.run(1);
|
||||
const n = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg.isAttack && c.t > t0 + 2 * iv - 0.1).length;
|
||||
assert.equal(n, 2, 'the main hit + the stored energy');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('disarmed (缴械) or stunned: no storing; an attack cancelled before its shot keeps the energies; a redeployment holds none [ASSUMED]', () => {
|
||||
const h = field();
|
||||
const u = h.unit(ID);
|
||||
h.step();
|
||||
assert.equal(u.trait.stored, 1);
|
||||
h.b.addBuff(u, { key: 'test:disarm', duration: 10, flags: { disarm: true } });
|
||||
h.run(9.5);
|
||||
assert.equal(u.trait.stored, 1, 'disarmed: "秘术师储存能量同样无法进行"');
|
||||
h.run(1);
|
||||
assert.equal(u.trait.stored, 2, 'stored at the first check after the disarm');
|
||||
h.b.applyStatus(u, 'stun', { duration: 10 });
|
||||
h.run(9.5);
|
||||
assert.equal(u.trait.stored, 2, 'stunned: no attack check');
|
||||
h.run(4);
|
||||
assert.equal(u.trait.stored, 3);
|
||||
// an attack whose shot never forms (beforeAttack empties it): the energies stay
|
||||
let cancel = true;
|
||||
h.b.on('beforeAttack', (ctx) => { if (ctx.attacker === u && cancel) ctx.targets = []; });
|
||||
h.spawn('e', { pos: [10, 6] });
|
||||
h.run(0.5);
|
||||
assert.equal(attacksOf(h, u).length, 0);
|
||||
assert.equal(u.trait.stored, 3, '"攻击被打断且弹道未能成功生成的情况下,储存的能量不会被消耗"');
|
||||
cancel = false;
|
||||
// knocked out holding them: the redeployment starts empty
|
||||
h.b.retreat(u);
|
||||
h.b.redeploy(u, { free: true });
|
||||
assert.equal(u.trait.stored, 0, 'a redeployment holds no energy');
|
||||
done(h);
|
||||
});
|
||||
Reference in new issue
Block a user