diff --git a/docs/SIM.md b/docs/SIM.md index c4658234..6b8470b0 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -205,7 +205,11 @@ Leader parts (`tag:'part'`) pass damage to their leader with `loseHp(leader, sha (无来源, credited to the attacker's `bossDamage`): `PART_TRANSFER` 1 for 斩胄之剑 / 破胄之锤 (`BLADE_TRANSFER`, the same constant) and 碎铳之簧 (PRTS "受到伤害时令假想敌:胄/铳受到等量的无来源生命流失"; DESIGN §20.10, §20.13). Content may replace a part mid-battle: every 剑/锤 sortie (content/bosses.js `kitBlade`) ends by spawning a new 初始模式 copy on its level branch route (`left_hand_origin` / `right_hand_origin`) with the old -HP, then `kill(old, null)` — uncounted, no bounty; the client sees a `die` and a `spawn`. +HP, then `kill(old, null)` — uncounted, no bounty; the client sees a `die` and a `spawn`. The Hidden Core 铳's 【末日布道】 +puts every 碎铳之簧 into 追逐模式 for its whole 5 s gain (`dog_duration`; invulnerable, no ordinary attacks): the spring +keeps following the casting gun after reaching it (PR #347; until 0.2.1 arrival or 1 tile ended the chase), and each +gun casts on its own data timer (20 s after it spawns, then every 45 s), so on a pair field the later call retargets +every spring to that gun for a new 5 s (PRTS: 「持续召唤场上所有“碎铳之簧”向自身移动」). WALK legs pathfind on the stage grid inside the rect with the official flow field (grid.js: 4-direction SPFA from the destination, crates cost 1000, then Bresenham line-of-sight smoothing — research 08 §3.4). The official route stays diff --git a/server/sim/content/bosses.js b/server/sim/content/bosses.js index eb3cd4be..61b1eb3b 100644 --- a/server/sim/content/bosses.js +++ b/server/sim/content/bosses.js @@ -639,8 +639,11 @@ function kitGun(ab, e, b) { }); if (s2) list.push({ cd: s2.cd, icd: s2.icd, cond: (b2) => b2.enemies.some((o) => o.alive && isSpring(o)), - fire(b2, e2) { // 【末日布道】 springs dash to 铳, invulnerable, trampling operators - // PRTS “碎铳之簧” 追逐模式 "不进行普通攻击": `disarm` for the dash (until 0.1.3 it kept shooting while it ran) + fire(b2, e2) { // 【末日布道】 springs chase 铳 for the whole 5 s gain, invulnerable, trampling operators (kitSpring) + // PRTS “碎铳之簧” 追逐模式 "不进行普通攻击": `disarm` for the dash (until 0.1.3 it kept shooting while it ran). + // Each gun casts on its own data timer, and a cast takes every spring — on a pair field the later of the two + // calls retargets them all to its gun (handbook 「持续召唤场上所有“碎铳之簧”向自身移动」; no stagger, no lock while + // a chase runs: PR #347's pair coordination is not taken, the data has neither) for (const sp of b2.enemies) { if (!sp.alive || !isSpring(sp) || !sp.mem.ab) continue; sp.mem.ab.dash = { until: b2.time + (s2.bb.dog_duration ?? 0), mul: 1 + (s2.bb.move_speed ?? 0), gun: e2, hit: new Set() }; @@ -708,11 +711,16 @@ function kitSpring(ab, e) { if (!P.up && P.downAt != null && b.time - P.downAt >= regen) raise(b); const d = e2.mem.ab.dash; if (!d) return; + // 追逐模式 lasts the whole gain (PRTS 末日布道 "令全场的“碎铳之簧”获得5秒增益…增益期间切换为追逐模式"; “碎铳之簧” + // 追逐模式 "持续追踪令其进入该形态的场上的假想敌:铳移动" — no arrival clause, unlike the 铳's own 冲锋模式): reaching + // the gun does not end it, the spring keeps following the gun until the 5 s are over (PR #347 by @CXUtk; until + // 0.2.1 it ended on arrival or within 1 tile). The casting gun dead: the nearest other gun; none left on the + // field: it holds still, the gain running on. const g = d.gun && d.gun.alive ? d.gun : gun(b); - const arrived = !g || stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt) || hypot(g.x - e2.x, g.y - e2.y) < 1; + if (g) stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt); // 追逐模式 "对进入自身0.35半径范围内的我方单位(包括飞行单位)造成一次攻击力100%的物理普通伤害" (until 0.1.3: radius 0.5) for (const u of areaAllies(b, e2, e2.x, e2.y, CHARGE_RADIUS)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); } - if (arrived || b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; } + if (b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; } }, }, // while shielded, every (spCost+1)-th attack (enemy SP +1 per attack, as 粉碎攻坚手's official text) adds the shield's diff --git a/test/content/hidden-gun-pursuit.test.js b/test/content/hidden-gun-pursuit.test.js new file mode 100644 index 00000000..09c8b06a --- /dev/null +++ b/test/content/hidden-gun-pursuit.test.js @@ -0,0 +1,156 @@ +// 隐秘核心铳's 【末日布道】 (PR #347 by @CXUtk): every 碎铳之簧 chases the casting gun for the gain's whole 5 s — PRTS 末日布道 +// "令全场的“碎铳之簧”获得5秒增益:移动速度+800%,无敌,增益期间切换为追逐模式", “碎铳之簧” 追逐模式 "持续追踪令其进入该形态的场上的 +// 假想敌:铳移动" (no arrival clause). Until 0.2.1 reaching the gun (or 1 tile from it) ended the chase. Each gun casts on its +// own data timer (initCooldown 20, cooldown 45 — no stagger between the two guns of a pair field, no lock while a chase +// runs: the data has neither, and the handbook's 「向自身」 means the later call takes the springs). +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js'; +import * as enemies from '../../server/sim/content/enemies.js'; +import * as bosses from '../../server/sim/content/bosses.js'; + +const W = JSON.parse(fs.readFileSync(new URL('../../data/waves.json', import.meta.url), 'utf8')); +const E = JSON.parse(fs.readFileSync(new URL('../../data/enemies.json', import.meta.url), 'utf8')); +const GUN = 'enemy_9017_achunt_2'; +const SPRING = 'enemy_9020_actrpc'; +const wall = chessRec({ id: 'pursuit_wall', profession: 'TANK', stats: { maxHp: 1e9, atk: 0, def: 1000, blockCnt: 3 }, rangeGrid: [[0, 0]], skill: null }); + +/** The real h08_02(_s) template (leader + parts only, or no spawns with `manual`), one non-attacking wall per side. */ +function arena({ solo = false, manual = false, cooldown = null } = {}) { + const tpl = structuredClone(W[solo ? 'act1autochess_h08_02_s' : 'act1autochess_h08_02']); + tpl.spawns = manual ? [] : tpl.spawns.filter((s) => s.tag === 'boss' || s.tag === 'part'); + if (cooldown != null) { + tpl.overrides[GUN] ??= { skills: structuredClone(E[GUN].skills) }; + const skill = tpl.overrides[GUN].skills.find((s) => s.prefabKey === '2'); + skill.initCooldown = 0; + skill.cooldown = cooldown; + } + return makeBattle({ + kind: 'hidden', waveTemplate: tpl, content: 'generic', extraContent: [enemies, bosses], seed: 7, + autoFinish: false, timeLimit: 300, recordEvents: false, hooks: [], + defs: { chess: { pursuit_wall: wall } }, kits: { pursuit_wall: () => ({ trait: { noAttack: true } }) }, + sharedBoss: { hp: 1e12, maxHp: 1e12, damage(pid, n) { this.hp -= n; } }, + players: [ + { playerId: 'p1', seat: 0, side: 'L', colOffset: 0, units: [{ uid: 1, chessId: 'pursuit_wall', row: 3, col: 5, abs: true }] }, + ...solo ? [] : [{ playerId: 'p2', seat: 1, side: 'R', colOffset: 8, units: [{ uid: 2, chessId: 'pursuit_wall', row: 3, col: 15, abs: true }] }], + ], + setup(b) { b.enemyOverrides = tpl.overrides; b.opts.waveTemplate = tpl; b.opts.bossMirror = !manual; }, + }); +} + +/** Every 【末日布道】 call (a new chase object on the spring): { time, gun }. */ +function recordCalls(h) { + const calls = []; + let last = null; + h.b.on('tick', () => { + const d = h.enemies().find((e) => e.defId === SPRING)?.mem.ab.dash; + if (!d || d === last) return; + last = d; + calls.push({ time: h.b.time, gun: d.gun }); + }); + return calls; +} + +test('隐秘核心铳 pair: each gun calls on its own data timer (20 s after it spawns, then every 45 s); the later call takes the springs', () => { + const h = arena(); + const spawned = new Map(); + h.b.on('enemySpawn', (c) => { if (c.enemy.defId === GUN) spawned.set(c.enemy, h.b.time); }); + const calls = recordCalls(h); + h.run(150); + assert.equal(spawned.size, 2, 'the pair field has two guns (the mirrored one)'); + const skill = W.act1autochess_h08_02.overrides[GUN].skills.find((s) => s.prefabKey === '2'); + for (const [gun, t0] of spawned) { + const own = calls.filter((c) => c.gun === gun).map((c) => c.time); + assert.equal(own.length, 3, 'three calls each in 150 s'); + assert.ok(Math.abs(own[0] - t0 - skill.initCooldown) < h.TICK * 2, `first call ${skill.initCooldown} s after its spawn`); + for (let i = 1; i < own.length; i++) assert.ok(Math.abs(own[i] - own[i - 1] - skill.cooldown) < h.TICK * 2, `then every ${skill.cooldown} s`); + } + // the two mirrored guns call within a tick or two of each other (no half-cooldown stagger), the later one wins + for (let i = 1; i < calls.length; i += 2) assert.ok(calls[i].time - calls[i - 1].time < h.TICK * 3, 'the two calls come together'); + assert.equal(h.b.errorCount, 0); + checkInvariants(h.b); +}); + +test('隐秘核心铳: reaching a moving gun keeps pursuit, invulnerability and disarm for the full 5 s', () => { + const h = arena({ solo: true, manual: true, cooldown: 45 }); + h.step(); + const gun = h.spawn(GUN, { pos: [3, 5], tag: 'boss', mods: { speedMul: 0 } }); + const spring = h.spawn(SPRING, { pos: [3, 4.8], tag: 'part' }); + h.step(2); + const d = spring.mem.ab.dash; + assert.ok(d, 'arriving next to the gun does not end the chase'); + h.run(1); + gun.x = 6; + const oldX = spring.x; + h.run(1); + assert.ok(spring.x > oldX, 'it keeps following the gun when the gun moves'); + assert.equal(spring.mem.ab.dash, d); + assert.ok(spring.s.flags.invulnerable && spring.s.flags.disarm); + h.run(d.until - h.b.time - h.TICK * 2); + assert.equal(spring.mem.ab.dash, d, 'still chasing just before the 5 s are over'); + assert.ok(spring.s.flags.invulnerable && spring.s.flags.disarm); + h.run(h.TICK * 4); + assert.equal(spring.mem.ab.dash, null, 'over once the gain ends'); + assert.ok(!spring.s.flags.invulnerable && !spring.s.flags.disarm); + assert.equal(h.b.errorCount, 0); +}); + +test('隐秘核心铳: the other gun\'s call during a chase retargets every spring to that gun for a new 5 s', () => { + const h = arena({ manual: true, cooldown: 45 }); + h.step(); + const left = h.spawn(GUN, { pos: [3, 5], tag: 'boss', mods: { speedMul: 0 } }); + const right = h.spawn(GUN, { pos: [3, 15], tag: 'boss', mods: { speedMul: 0 } }); + const call = right.mem.ab.list.find((a) => a.cd === 45 && a.fire); + call.left = 999; // the right gun's call is not ready yet + const spring = h.spawn(SPRING, { pos: [3, 10], tag: 'part' }); + h.step(2); + const first = spring.mem.ab.dash; + assert.ok(first && first.gun === left, 'the first call takes the spring to the left gun'); + h.run(1); + assert.equal(spring.mem.ab.dash, first, 'chasing the left gun'); + call.left = 0; // the right gun's call comes ready mid-chase + h.step(2); + const second = spring.mem.ab.dash; + assert.ok(second && second !== first && second.gun === right, 'the later call takes the spring at once'); + assert.ok(Math.abs(second.until - h.b.time - 5) < h.TICK * 3, 'for a new 5 s'); + const oldX = spring.x; + h.run(1); + assert.ok(spring.x > oldX, 'it now runs to the right gun'); + assert.ok(spring.s.flags.invulnerable && spring.s.flags.disarm); + assert.equal(h.b.errorCount, 0); +}); + +test('隐秘核心铳: with no gun left on the field the spring holds still, invulnerable and disarmed, until the gain ends', () => { + const h = arena({ solo: true, manual: true, cooldown: 45 }); + h.step(); + const gun = h.spawn(GUN, { pos: [3, 5], tag: 'boss', mods: { speedMul: 0 } }); + const spring = h.spawn(SPRING, { pos: [3, 1], tag: 'part' }); + h.step(2); + const d = spring.mem.ab.dash; + assert.ok(d, 'chasing'); + h.b.kill(gun, null); + h.step(); + const at = [spring.x, spring.y]; + h.run(1); + assert.deepEqual([spring.x, spring.y], at, 'no gun to follow: it stands'); + assert.equal(spring.mem.ab.dash, d, 'the chase is not over'); + assert.ok(spring.s.flags.invulnerable && spring.s.flags.disarm); + h.run(d.until - h.b.time + h.TICK * 2); + assert.equal(spring.mem.ab.dash, null); + assert.ok(!spring.s.flags.invulnerable); + assert.equal(h.b.errorCount, 0); +}); + +test('隐秘核心铳: solo keeps the template initial cooldown and repeated call timing', () => { + const h = arena({ solo: true }); + const calls = recordCalls(h); + h.run(100); + assert.equal(calls.length, 2); + const tpl = W.act1autochess_h08_02_s; + const spawnTime = tpl.spawns.find((s) => s.tag === 'boss').time; + const skill = (tpl.overrides[GUN]?.skills ?? E[GUN].skills).find((s) => s.prefabKey === '2'); + assert.ok(Math.abs(calls[0].time - spawnTime - skill.initCooldown) < h.TICK * 2); + assert.ok(Math.abs(calls[1].time - calls[0].time - skill.cooldown) < h.TICK * 2); + assert.equal(h.b.errorCount, 0); +}); diff --git a/test/golden/fields.json b/test/golden/fields.json index fa0f436c..b7a41ef2 100644 --- a/test/golden/fields.json +++ b/test/golden/fields.json @@ -1111,107 +1111,106 @@ "killed": 8, "total": 8, "unspawned": 0, - "bossHpLeft": 3511007, - "rngDraws": 1705, + "bossHpLeft": 3747749, + "rngDraws": 1619, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1689940,"bossDmg":698535,"heal":15297,"deaths":5,"layerGains":{}}, - "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":3810677,"bossDmg":1348791,"heal":109383,"deaths":7,"layerGains":{}} + "p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1664353,"bossDmg":722232,"heal":20932,"deaths":5,"layerGains":{}}, + "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":3117516,"bossDmg":1090251,"heal":121649,"deaths":12,"layerGains":{}} }, "units": [ - ["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_yu",6348.28,1,67,0,9,7512,6537,1,3], - ["p1",2,"op","chess_char_6_06_b",2,"uniequip_002_pepe",2695.89,1,11,0,8762,1179,0,9,1], - ["p1",3,"op","chess_char_6_07_b",2,"uniequip_002_siege2",1406.57,1,17,0,35012,3869,0,15,2], - ["p1",4,"op","chess_char_6_01_b",2,"uniequip_002_lemuen",1165.15,1,7,2,1312507,1285,0,58,8], - ["p1",5,"op","chess_char_6_02_b",2,"uniequip_002_sbell2",0,0,9,0,19135,12418,0,23,1], - ["p1",6,"op","chess_char_6_04_b",2,"uniequip_002_skadi2",1104.58,1,55,0,35830,11463,444,0,4], - ["p1",7,"op","chess_char_6_05_b",2,"uniequip_002_pasngr",2451.4,1,13,0,67894,0,0,0,10], - ["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1668,1,19,0,30095,0,7111,86,3], - ["p1",9,"op","chess_char_6_09_b",0,"uniequip_002_cello",0,0,21,1,150260,13231,0,28,28], - ["p1",10,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",1893,1,60,0,22306,8566,349,73,3], + ["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_yu",5902.25,1,15,0,2317,10287,12324,32,3], + ["p1",2,"op","chess_char_6_06_b",2,"uniequip_002_pepe",2893.32,1,11,0,20047,281,0,22,1], + ["p1",3,"op","chess_char_6_07_b",2,"uniequip_002_siege2",1834.75,1,17,0,42326,1612,0,24,2], + ["p1",4,"op","chess_char_6_01_b",2,"uniequip_002_lemuen",430.19,1,7,1,1229734,2143,0,58,8], + ["p1",5,"op","chess_char_6_02_b",2,"uniequip_002_sbell2",1329.31,1,9,0,34472,14154,0,46,2], + ["p1",6,"op","chess_char_6_04_b",2,"uniequip_002_skadi2",0,0,66,0,35830,9923,233,0,3], + ["p1",7,"op","chess_char_6_05_b",2,"uniequip_002_pasngr",2451.4,1,13,0,69036,0,0,0,11], + ["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1668,1,19,0,26444,0,7415,77,2], + ["p1",9,"op","chess_char_6_09_b",0,"uniequip_002_cello",0,0,21,1,174620,13925,0,30,30], + ["p1",10,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",1893,1,60,0,22306,9359,349,73,2], ["p1",11,"token","token_10017_skadi2_dedant",null,null,138,0,28,0,0,0,0,0,null], ["p1",12,"token","token_10030_mlyss_wtrman",null,null,0,0,24,0,0,1640,318,4,1], - ["p2",13,"op","chess_char_6_15_b",2,"uniequip_002_qiubai",0,0,66,4,730725,36055,0,338,3], - ["p2",14,"op","chess_char_6_19_b",2,"uniequip_002_blkkgt",203.08,1,58,0,450324,11985,0,58,5], - ["p2",15,"op","chess_char_5_01_b",1,"uniequip_002_excu2",0,0,65,0,41071,8230,3308,52,2], - ["p2",16,"op","chess_char_6_17_b",2,"uniequip_002_nearl2",3430.93,1,59,1,771816,24776,0,77,3], - ["p2",17,"op","chess_char_6_12_b",1,"uniequip_002_rosmon",2886.8,1,8,0,378435,1696,0,78,3], - ["p2",18,"op","chess_char_6_13_b",1,"uniequip_002_angel2",2215,1,14,0,1014033,6744,2745,288,6], - ["p2",19,"op","chess_char_6_14_b",2,"uniequip_002_lumen",3038.4,1,20,0,0,0,7939,78,1], - ["p2",20,"op","chess_char_6_16_b",2,"uniequip_002_halo2",1439,1,10,0,105424,0,0,132,4], - ["p2",21,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",1690.1,1,41,0,234825,7508,0,60,4], - ["p2",22,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",1592.1,1,25,0,0,14000,95391,82,2], + ["p2",13,"op","chess_char_6_15_b",2,"uniequip_002_qiubai",2052.07,1,68,4,561677,30058,0,292,3], + ["p2",14,"op","chess_char_6_19_b",2,"uniequip_002_blkkgt",0,0,59,0,341242,18065,0,56,4], + ["p2",15,"op","chess_char_5_01_b",1,"uniequip_002_excu2",0,0,65,0,3198,6795,1515,24,1], + ["p2",16,"op","chess_char_6_17_b",2,"uniequip_002_nearl2",0,0,55,1,514814,17168,0,62,3], + ["p2",17,"op","chess_char_6_12_b",1,"uniequip_002_rosmon",942.17,1,8,0,674333,8720,0,73,3], + ["p2",18,"op","chess_char_6_13_b",1,"uniequip_002_angel2",0,0,14,0,769354,8634,2644,263,5], + ["p2",19,"op","chess_char_6_14_b",2,"uniequip_002_lumen",3038.4,1,20,0,0,0,15762,80,1], + ["p2",20,"op","chess_char_6_16_b",2,"uniequip_002_halo2",0,0,10,0,97174,1439,0,119,3], + ["p2",21,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",0,0,62,0,132239,19665,0,24,3], + ["p2",22,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",1183.78,1,25,0,0,15377,101728,82,2], ["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null], ["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null], ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null], ["-","~4","device","trap_1105_accrate",null,null,100,1,4,0,0,0,0,0,null], ["-","~5","device","trap_1105_accrate",null,null,100,1,5,0,0,0,0,0,null], ["-","~6","device","trap_1105_accrate",null,null,100,1,6,0,0,0,0,0,null], - ["p2","~1","token","token_10019_nearl2_sword",null,null,4710.39,0,47,0,6744,3570,0,5,null], - ["p1","~1","token","token_10030_mlyss_wtrman",null,null,0,0,48,0,1670,1538,0,22,1], - ["p1","~2","token","token_10030_mlyss_wtrman",null,null,0,0,49,0,220,3142,0,9,null], - ["p1","~3","token","token_10030_mlyss_wtrman",null,null,0,0,51,0,463,1322,0,7,null], - ["p1","~1","token","token_10040_siege2_vlion",null,null,3730,0,53,0,4750,270,0,10,null], - ["p1","~1","token","token_10017_skadi2_dedant",null,null,138,0,56,0,0,0,0,0,null], - ["p2","~2","token","token_10019_nearl2_sword",null,null,0,0,57,0,68925,6922,0,11,null], - ["p1","~4","token","token_10030_mlyss_wtrman",null,null,1172,1,61,0,1025,710,538,7,1], - ["p1","~2","token","token_10017_skadi2_dedant",null,null,100,0,62,0,0,0,0,0,null], - ["p1","~2","token","token_10040_siege2_vlion",null,null,3942.24,0,63,0,0,76,0,0,null], - ["p2","~3","token","token_10019_nearl2_sword",null,null,3290.4,0,64,0,8355,3008,0,21,null], - ["p1","~3","token","token_10017_skadi2_dedant",null,null,100,1,68,0,0,0,0,0,null] + ["p2","~1","token","token_10019_nearl2_sword",null,null,2916.93,0,47,0,6778,5363,0,21,null], + ["p1","~1","token","token_10030_mlyss_wtrman",null,null,0,0,48,1,1801,1538,0,22,1], + ["p1","~2","token","token_10030_mlyss_wtrman",null,null,0,0,49,0,207,3142,0,9,null], + ["p1","~3","token","token_10030_mlyss_wtrman",null,null,0,0,50,0,463,1322,0,7,null], + ["p1","~1","token","token_10040_siege2_vlion",null,null,3730,0,52,0,4750,270,0,10,null], + ["p1","~1","token","token_10017_skadi2_dedant",null,null,138,0,57,0,0,0,0,0,null], + ["p2","~2","token","token_10019_nearl2_sword",null,null,6950.53,0,58,0,0,963,0,0,null], + ["p1","~4","token","token_10030_mlyss_wtrman",null,null,1322,1,61,0,0,385,293,0,1], + ["p1","~2","token","token_10040_siege2_vlion",null,null,3897.9,0,63,0,0,137,0,0,null], + ["p2","~3","token","token_10019_nearl2_sword",null,null,133.02,0,64,0,16707,6165,0,9,null], + ["p1","~2","token","token_10017_skadi2_dedant",null,null,100,0,67,0,0,0,0,0,null] ], "enemies": { - "enemy_1267_nhpbr_2": [6,6,0,1800,141894], - "enemy_9007_acelem": [2,2,0,15725,309587], - "enemy_9017_achunt_2": [2,0,0,54791,2088993], - "enemy_9018_actrpa": [1,0,0,30160,855522], - "enemy_9019_actrpb": [1,0,0,29590,798033], - "enemy_9020_actrpc": [1,0,0,20449,1384683] + "enemy_1267_nhpbr_2": [6,6,0,1400,141894], + "enemy_9007_acelem": [2,2,0,17143,309587], + "enemy_9017_achunt_2": [2,0,0,58691,1852251], + "enemy_9018_actrpa": [1,0,0,44364,722451], + "enemy_9019_actrpb": [1,0,0,28255,660498], + "enemy_9020_actrpc": [1,0,0,23012,1168947] }, - "hooks": [1,66,6000,1799,1799,6944,13382,2834,25,37,99,90,326,47765,2836,14,13,0,1,26,0,13,0], - "events": {"atk":2246,"deploy":47,"die":37,"dmg":12926,"engage":25,"fx":2188,"heal":930,"skill":189,"spawn":53,"status":849}, + "hooks": [1,66,6000,1682,1682,6586,12669,2825,29,42,95,91,286,44763,2551,19,13,0,1,30,0,16,0], + "events": {"atk":2097,"deploy":47,"die":42,"dmg":12154,"engage":23,"fx":2024,"heal":885,"skill":186,"spawn":52,"status":769}, "fx": { - "airstrike": 123, - "beam": 31, + "airstrike": 88, + "beam": 27, "blessing": 1, "blink": 5, "bombard": 72, "bombardShell": 72, - "burst": 13, + "burst": 16, "camouflage": 1, "copy": 3, "dash": 24, - "displace": 1, - "dp": 3, - "drone": 320, - "droneLock": 14, - "drones": 4, - "extraAttack": 16, - "finale": 5, + "displace": 20, + "dp": 2, + "drone": 190, + "droneLock": 9, + "drones": 3, + "extraAttack": 7, + "finale": 4, "firewall": 3, "garrisonGrant": 1, - "lightning": 116, + "lightning": 117, "link": 1141, "lock": 72, - "lure": 1, - "phase": 12, + "lure": 2, + "phase": 11, "radiantSword": 3, "revive": 3, "scorchBurst": 1, - "shieldBreak": 10, - "slash": 55, - "snow": 15, + "shieldBreak": 9, + "slash": 44, + "snow": 26, "split": 2, - "steal": 6, - "storm": 10, + "steal": 5, + "storm": 11, "summon": 5, "sunBurst": 3, - "talent": 5, - "tide": 4, + "talent": 6, + "tide": 3, "wanted": 6, "zone": 6 }, - "snaps": "2a921421 57801ce9 151fd5ae 264faece f8920de3 cef81b6f 47f4a450 cc0a8289 6b7ac4fb 81b93e22 d10e889f 97a33a13 a796916b 202ae19b 16aa629d c7afee38 08c75624 11b64823 c1f874f3 b92e528f" + "snaps": "2a921421 57801ce9 151fd5ae 991b7440 79722413 1faab651 85db54ec 88d6f82e 6d938e5f fc881a30 9c294f55 4373e468 af475c5f df3e9b70 b0474d4c 538602b8 4e66de79 e70dcab9 481e5d84 e8e351b1" }, "hidden-boss_9-solo": { "about": "假想敌:铳 solo pool 2800000: chess_char_5_08_b chess_char_5_11_b chess_char_5_05_b chess_char_5_06_b chess_char_5_13_b chess_char_5_07_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b chess_char_5_12_b", @@ -1228,37 +1227,35 @@ "killed": 3, "total": 3, "unspawned": 0, - "bossHpLeft": 1932856, - "rngDraws": 128, + "bossHpLeft": 2011732, + "rngDraws": 207, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":3,"total":3,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2753803,"bossDmg":867144,"heal":73158,"deaths":5,"layerGains":{}}}, + "players": {"p1":{"killed":3,"total":3,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2574046,"bossDmg":788268,"heal":101441,"deaths":5,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_horn",2775,1,1,1,368359,8434,0,127,4], - ["p1",2,"op","chess_char_5_11_b",1,"uniequip_002_demkni",4804.8,1,3,0,1265,3442,23540,46,35], - ["p1",3,"op","chess_char_5_05_b",2,"uniequip_002_ulpia",11263.65,1,33,0,413261,29216,17173,39,5], - ["p1",4,"op","chess_char_5_06_b",2,"uniequip_002_etlchi",3747,1,7,1,1380629,16819,16926,201,5], - ["p1",5,"op","chess_char_5_13_b",1,"uniequip_002_ghost2",3448.92,1,9,0,128392,14229,3714,76,3], - ["p1",6,"op","chess_char_5_07_b",2,"uniequip_002_surtr",7680,1,35,0,42625,19991,4759,13,3], - ["p1",7,"op","chess_char_5_02_b",2,"uniequip_002_titi",1439,1,5,0,0,0,0,1,1], - ["p1",8,"op","chess_char_5_03_b",2,"uniequip_002_blaze2",1148.18,1,6,0,207074,1471,1147,186,6], - ["p1",9,"op","chess_char_5_10_b",2,"uniequip_002_lisa",1480,1,32,0,14018,13299,5899,28,6], - ["p1",10,"op","chess_char_5_12_b",0,"uniequip_002_dusk",1787,1,2,1,198179,0,0,53,41], - ["p1","~1","token","token_10039_ulpia_block",null,null,3290,0,20,0,0,210,0,0,null], - ["p1","~2","token","token_10039_ulpia_block",null,null,3335,0,28,0,0,165,0,0,null] + ["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_horn",2757.97,1,1,1,328544,10883,0,121,4], + ["p1",2,"op","chess_char_5_11_b",1,"uniequip_002_demkni",4784.8,1,3,0,2559,11172,34206,59,38], + ["p1",3,"op","chess_char_5_05_b",2,"uniequip_002_ulpia",10680.88,1,4,0,391098,51057,37953,64,5], + ["p1",4,"op","chess_char_5_06_b",2,"uniequip_002_etlchi",1801.92,1,7,0,1462149,23079,21225,197,5], + ["p1",5,"op","chess_char_5_13_b",1,"uniequip_002_ghost2",3448.92,1,19,1,146526,21512,5080,102,4], + ["p1",6,"op","chess_char_5_07_b",2,"uniequip_002_surtr",2680,1,23,0,4444,7780,113,7,1], + ["p1",7,"op","chess_char_5_02_b",2,"uniequip_002_titi",1439,1,5,0,0,0,0,0,0], + ["p1",8,"op","chess_char_5_03_b",2,"uniequip_002_blaze2",1207.04,1,6,0,32056,265,0,25,1], + ["p1",9,"op","chess_char_5_10_b",2,"uniequip_002_lisa",1480,1,28,0,0,8783,2863,1,5], + ["p1",10,"op","chess_char_5_12_b",0,"uniequip_002_dusk",1787,1,2,1,206669,0,0,57,43] ], "enemies": { "enemy_10067_ftsjc": [1,1,0,1161,21565], - "enemy_5601_entlec": [6,5,0,0,160038], - "enemy_9007_acelem": [2,2,0,632,119439], - "enemy_9017_achunt_2": [1,0,0,32010,867144], - "enemy_9018_actrpa": [1,0,0,14160,440951], - "enemy_9019_actrpb": [1,0,0,19148,445768], - "enemy_9020_actrpc": [1,0,0,13881,698896] + "enemy_5601_entlec": [8,7,0,0,221752], + "enemy_9007_acelem": [2,2,0,1897,119439], + "enemy_9017_achunt_2": [1,0,0,61863,788268], + "enemy_9018_actrpa": [1,0,0,22333,431138], + "enemy_9019_actrpb": [1,0,0,16840,403557], + "enemy_9020_actrpc": [1,0,0,15204,588326] }, - "hooks": [1,36,6000,876,876,3337,7978,7359,13,17,109,104,118,25510,1863,10,13,0,1,120,0,2,0], - "events": {"atk":1327,"deploy":23,"die":17,"dmg":7399,"engage":10,"fx":851,"heal":385,"skill":213,"spawn":25,"status":317}, - "fx": {"anchor":5,"aoe":3,"beam":22,"burst":2,"candle":6,"charge":7,"dash":12,"downed":1,"ember":2,"explode":1,"healAoe":35,"link":597,"overload":4,"palsy":3,"phase":12,"reborn":1,"shieldBreak":10,"splash":118,"substitute":3,"swap":3,"teleport":4}, - "snaps": "9f379cb4 794e9645 d2f1f9f3 6f045130 3ba3cbda c2dffcd7 002617f9 8eb229f1 44fa10f5 02a4f054 024d05f4 4f2b850e 264e7810 e232b25c fb23ac72 ffe30a9a 966d17a8 f21eca66 c335c5c3 dadd437f" + "hooks": [1,31,6000,758,758,3651,7974,3912,15,16,106,103,21,22194,1289,14,15,0,1,70,0,8,0], + "events": {"atk":1320,"deploy":16,"die":16,"dmg":7575,"engage":9,"fx":765,"heal":611,"skill":209,"spawn":25,"status":228}, + "fx": {"anchor":5,"aoe":1,"beam":30,"burst":8,"candle":8,"charge":7,"dash":12,"dollEnd":1,"ember":1,"explode":1,"healAoe":38,"link":597,"overload":4,"palsy":3,"phase":11,"reborn":1,"revive":1,"shieldBreak":9,"splash":21,"substitute":4,"swap":2}, + "snaps": "9f379cb4 794e9645 1eab82b0 9bd21a64 94bcd685 ea068a42 11f6e172 f1d86469 8dae333a a7b0bf9e 854de036 d7f516e9 946585b3 06953c95 f04d0312 832df1bd 4f3fdc1c eec59b6e 4c10a52c 9c930ebb" }, "hidden-boss_10-pair": { "about": "假想敌:管 pair pool 15200000: chess_char_5_19_b chess_char_4_01_b chess_char_5_14_b chess_char_5_17_b chess_char_5_15_b chess_char_5_20_b chess_char_5_21_b chess_char_5_22_b chess_char_5_23_b chess_char_4_02_b | chess_char_4_11_b chess_char_4_04_b chess_char_4_07_b chess_char_4_16_b chess_char_4_09_b chess_char_4_06_b chess_char_4_10_b chess_char_4_12_b chess_char_4_13_b chess_char_4_14_b token_10041_cathy_catsld token_10041_cathy_catsld", diff --git a/test/golden/matches.json b/test/golden/matches.json index befd37aa..634754e0 100644 --- a/test/golden/matches.json +++ b/test/golden/matches.json @@ -1485,7 +1485,7 @@ "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 8, "reward": 425, - "stats": {"activatedLayers":2267,"bossDamage":7523075,"dmgDealt":26462402,"gold":149,"itemsEquipped":23,"kills":290,"leaks":0,"lpLost":0,"merges":6,"perfectRounds":13,"refreshes":16}, + "stats": {"activatedLayers":2267,"bossDamage":6992346,"dmgDealt":24545248,"gold":149,"itemsEquipped":23,"kills":290,"leaks":0,"lpLost":0,"merges":6,"perfectRounds":13,"refreshes":16}, "lineup": ["3_16_b@12,8[3_01_e_a+1_01_e_b]","4_09_b@11,5","3_11_b@11,8[3_02_e_a+6_06_e_a]","1_02_b@10,4[1_02_e_a+4_02_e_a]","5_14_a@10,8[3_03_e_a+1_05_e_a]","6_20_a@9,6","3_20_a@9,8[3_07_e_a+3_08_e_a]","6_05_a@9,9[2_04_e_a+5_02_e_b]"], "bonds": ["arcaneShip 0/316","deputShip 1/312","egirShip 0/300","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 1/343","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/343","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 0/319","soloShip 1/310","steadShip 1/341","suntShip 1/310","swiftShip 1/308","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"] }, @@ -1499,7 +1499,7 @@ "title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"}, "trophies": 8, "reward": 425, - "stats": {"activatedLayers":2301,"bossDamage":6369365,"dmgDealt":22481097,"gold":165,"itemsEquipped":31,"kills":276,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":38}, + "stats": {"activatedLayers":2301,"bossDamage":6900614,"dmgDealt":23861530,"gold":165,"itemsEquipped":31,"kills":276,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":38}, "lineup": ["2_10_a@11,5[2_03_e_b+3_09_e_a]","5_03_a@11,8[1_02_e_a+5_02_e_a]","1_06_a@10,3[1_05_e_a+3_02_e_a]","3_16_a@10,4[3_03_e_b+4_04_e_b]","5_06_a@10,8[3_09_e_b+3_09_e_b]","4_21_b@9,6[4_06_e_a+3_10_e_b]","5_02_a@9,8[4_09_e_a+3_06_e_b]","1_17_b@9,9[3_08_e_a+1_01_e_a]"], "bonds": ["arcaneShip 1/327","deputShip 1/318","egirShip 0/300","emptyShip 0/300","indomShip 1/308","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/404","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/308","soloShip 0/310","steadShip 0/323","suntShip 1/304","swiftShip 0/300","victoriaShip 1/332","visiShip 0/300","yanShip 0/305"] } diff --git a/test/sim/feedback1e-healers.test.js b/test/sim/feedback1e-healers.test.js index d4d5eddc..4733801c 100644 --- a/test/sim/feedback1e-healers.test.js +++ b/test/sim/feedback1e-healers.test.js @@ -247,7 +247,9 @@ test('E2 audit: healers on the lane tiles of every boss / hidden field deal no d const lead = (e) => e.isBoss || e.tag === 'boss' || e.tag === 'part' || LUCIEN.test(e.defId); let healsOnLead = 0; h.b.on('heal', (c) => { if (healers.includes(c.source) && c.amount > 0 && c.source.blocking.some(lead)) healsOnLead++; }); - for (let i = 0; i < 120 * 30; i++) { + // 200 s on the fields whose leaders / parts a healer must block (the solo 铳 field's springs first meet a healer at + // 145 s since their 末日布道 chase lasts its whole 5 s — PR #347), 120 s elsewhere + for (let i = 0; i < (EXPECT[key] ? 200 : 120) * 30; i++) { h.step(); // operators break the 碎铳之簧 shields (unblockable while shielded): a hit of the kind each shield yields to for (const e of h.b.enemies) {