diff --git a/docs/DATA.md b/docs/DATA.md index 04c1621d..1e9faea3 100644 --- a/docs/DATA.md +++ b/docs/DATA.md @@ -445,7 +445,7 @@ as official level data, and sim tests use them as a plain two-halves road. | `special` | `{"mire":{"intervalSec":1,"aspdPerStack":-0.05,"moveMulPerStack":-0.05,"maxStacks":10,"heavyWeight":3,…}}`, `{"infection":{"bb":{"damage":70,"atk":0.2,"attack_speed":20,"duration":300}}}`, `{"deepsea":{"skillId":"sktok_tidectrl_3","bb":{…}}}`, `{"smog":{"rule":"operatorsNotTargetableByEnemyRanged"}}`, `{"blower":{…}}` | special-terrain parameters | | `runes[]`, `globalBuffs[]` | `{"key":"map_tile_blackb_assign","bb":{…},"bbStr":{…}}` | level runes (act2 m03 reed rune has no reed tiles — leftover) | | `deployTiles` | `{"normal":{"melee":[[9,3],…],"rangedOnly":[[10,4],…],"changedByDevices":[…]},"bossLeft":{…},"bossRight":{…}}` | deployable tiles at match start: `melee` = LOW (effective) buildable ALL/MELEE (ranged may use them too) — and a 深水区 under an active 特制水上平台 (`waterPlatform`, "在水上建立可以部署任意单位的平台"; act1 m05 only — the prep's deploy map only: the sim does not model the canoes, so its Grid keeps those tiles NONE, automatic placements avoid them and the path tie-break counts them as non-blockable [ASSUMED, weight 0 this season]) —, `rangedOnly` = high ground / active platforms; tiles under active crates/mounds and the 深水区 excluded. `bossLeft` / `bossRight` (field coordinates, boss rows 1–5) are what a player may deploy on in a boss round's prep: board (r, c) = boss (r − 7, c) / the right half mirrored (r − 7, 20 − c) (server/match/board.js `field`, user playtest #5 item 7) | -| `groundPaths` / `groundPathsWithDevices` | `{"12,10->9,2":[[12,10],[12,9],…,[12,3],[11,3],[10,3],[9,3],[9,2]]}` | the ground route of the sim's own `server/sim/grid.js` flow field (research 08 §3.4: 4-direction SPFA from the goal, crates cost 1000, Bresenham line-of-sight smoothing — official route lengths — kept unless the remake's (0.1.0's) road-over-floor preference route crosses strictly fewer non-blockable floor / gate / 深水区 tiles — the fewest among equal-length chains, smoothing that never cuts across floor its grid route does not walk; a corner touch does not count as a crossing — e.g. 战场#01's lower gate takes the col-8 road, not the col-9 floor lane, while 战场#04's keeps the official diagonal from row 9 into row 10; DESIGN §14 corrections, test/sim/pathing-official.test.js) as the tiles an enemy crosses, over the whole map, without / with the match-start devices (crates cost 1000, platforms / mounds blocking [ASSUMED]); the battle re-reads its flow fields live | +| `groundPaths` / `groundPathsWithDevices` | `{"12,10->9,2":[[12,10],[12,9],…,[12,3],[11,3],[10,3],[9,3],[9,2]]}` | the ground route of the sim's own `server/sim/grid.js` flow field (research 08 §3.4: 4-direction SPFA from the goal, crates cost 1000, Bresenham line-of-sight smoothing — official route lengths — kept unless the remake's (0.1.0's) road-over-floor preference route crosses strictly fewer non-blockable floor / gate / 深水区 tiles and no more 深水区 (GitHub #375) — the fewest among equal-length chains, smoothing that never cuts across floor its grid route does not walk; a corner touch does not count as a crossing — e.g. 战场#01's lower gate takes the col-8 road, not the col-9 floor lane, while 战场#04's keeps the official diagonal from row 9 into row 10; DESIGN §14 corrections, test/sim/pathing-official.test.js) as the tiles an enemy crosses, over the whole map, without / with the match-start devices (crates cost 1000, platforms / mounds blocking [ASSUMED]); the battle re-reads its flow fields live | | `options`, `config` | `{"characterLimit":8,"moveMultiplier":0.5}`, `{…configBlackBoard strings…}` | | Glyph legend (`rows`): diff --git a/docs/SIM.md b/docs/SIM.md index f06eff19..fdd6ce73 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -190,7 +190,14 @@ a part) is not, even when it counts for LP (`counted`). `RouteSpec` accepts data/waves.json routes (`{motion, start, end, checkpoints:[[r,c]…], steps:[{t:'move',p},{t:'wait',s},{t:'disappear'},{t:'appear',p}]}`) and research routes (`{m, s, e, cp:[['MOVE',r,c]…]}`). `spawnsFromTemplate(waveEntry, {mods})` (simdata.js) converts a template into `{ routes, spawns, maxPlayTime, overrides, extraRoutes }` (non-spawn `action` entries are skipped; `unharmful` -and `tag:'part'` spawns don't count in `total`). +and `tag:'part'` spawns don't count in `total`). The boss templates' 传送门 reads as "reappear on the far side" (GitHub +#336, PR #337): four official routes write the crossing as `disappear → wait → move` to the exit with no `appear` (their +twins spell the `appear` out), so normalizeRoute re-inserts the exit `appear` ahead of that move — otherwise the enemy +walks the rest of its route hidden and leaks unseen (test/sim/portal-appear.test.js). A route that ENDS on an entrance +(`tile_telin`; the boss / Hidden Core circuits end on [1,3] / [1,17]) does not leak there: the enemy vanishes, comes +out of the far exit (`tile_telout`; both entrances feed [5,10], the pairing of every explicit `disappear`/`appear` pair +of the data) after 3 s hidden inside — the wait of the explicit crossings (85 of 95) — and walks on to the blue door +on the entrance's side, where it leaks (ai.js portalPickup; [ASSUMED] the pairing and the 3 s). A `bounty` pays `coins` once, when the enemy really dies (not a knock-out it survives; a leak pays nothing), to `Battle._bountyPayee`: the player of the operator or summon that dealt the blow, if that player is in the battle; any @@ -201,13 +208,19 @@ 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 unless 0.1.0's road-over-floor preference route (the fewest non-blockable tiles among equal-length chains, a line of sight that never covers floor — diagonal-step corners included — its grid route does not walk; user playtest #2 item 2) -crosses strictly fewer non-blockable tiles (floor / gate lanes); "crosses" = passes through the tile's interior, a +crosses strictly fewer non-blockable tiles (floor / gate lanes) and no more 深水区 (GitHub #375: on 战场#08(下半) the +patrolling 铳 / 卢西恩 waded through two water tiles to dodge one floor tile; the official upper road crosses one); +"crosses" = passes through the tile's interior, a corner touch does not count (`grid.js` `segmentTiles`; community report D5 after 0.1.0: 战场#04's lower-gate enemies cut diagonally from row 9 into row 10 as officially), and on equal counts the official route stays. test/sim/pathing-official.test.js lists the 21 of 154 stage routes that still differ from the official ones (战场#01's diff --git a/docs/design/network.md b/docs/design/network.md index 10510a7f..5f2e70e6 100644 --- a/docs/design/network.md +++ b/docs/design/network.md @@ -110,7 +110,7 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ { **Corrections from research 08/09 (normative):** - Waves: official generator (research 08 §2): 3 of 6 types, each owns exactly 3 of the 15 round slots, SPECIAL the rest (shuffled per match); per round one type + one weighted special entry (half by round); each template SPAWN **action** becomes `clamp(roundHalfEven(n·P(t)/P(new)), 1, 5)` units over the same window; placeholder actions of the other movement class are skipped. Stage per match from the mode's list (标准 = 战场#01 only, §6.5). No custom balance multipliers (`data/tuning.json` keeps only titles); co-op boss pool = `bloodPoint[difficulty]` unscaled. -- Ground pathing: official SPFA-style 4-direction search from the goal with crates as cost-1000 tiles, then straight-line smoothing (research 08 §3.4). act1 m02 crates #001–#004 inactive in 下半. Remake refinement (user playtest #2, narrowed by §21.15): route lengths and routes stay the official ones unless 0.1.0's road-over-floor preference route crosses strictly fewer non-blockable (floor / gate / 深水区) tiles — a corner touch is not a crossing; ties keep the official route. E.g. 战场#01's lower-gate enemies take the col-8 road, while 战场#04's keep the official diagonal from row 9 into row 10 (server/sim/grid.js header; test/sim/pathing-official.test.js, pathing-blockable.test.js). +- Ground pathing: official SPFA-style 4-direction search from the goal with crates as cost-1000 tiles, then straight-line smoothing (research 08 §3.4). act1 m02 crates #001–#004 inactive in 下半. Remake refinement (user playtest #2, narrowed by §21.15): route lengths and routes stay the official ones unless 0.1.0's road-over-floor preference route crosses strictly fewer non-blockable (floor / gate / 深水区) tiles and no more 深水区 (GitHub #375) — a corner touch is not a crossing; ties keep the official route. E.g. 战场#01's lower-gate enemies take the col-8 road, while 战场#04's keep the official diagonal from row 9 into row 10 (server/sim/grid.js header; test/sim/pathing-official.test.js, pathing-blockable.test.js). - Observing: no watching while your own normal battle runs; after it ends (and in prep) tap a teammate → 前往查看 (local replica, fast-forwarded). ‹ › only inside 联防/FA fields; FA: own pair only; eliminated: free. - Prep interactions: tap a unit → underframe with 出售 +1 (and 撤退 on board); items/Arts → 销毁; shop and upgrade are two-tap (select → confirm; the remake's 机变 cards too, §18.2); strategy draft forbids a strategy already taken by a teammate (队友已选). - Emotes: 6 themes × 6 battle emotes (research 09 §4.2), image-only bubbles beside the sender avatar, 1 s cooldown, 3 s bubble; `g.emote {id}` with the official emoji id. diff --git a/docs/research/08-waves-official.md b/docs/research/08-waves-official.md index 3b87b93a..b84dcad7 100644 --- a/docs/research/08-waves-official.md +++ b/docs/research/08-waves-official.md @@ -352,8 +352,9 @@ reported the 战场#01 lower-gate enemies walking up the col-9 floor lane, where `server/sim/grid.js` keeps every official route LENGTH and the official route itself unless the 2026-09-29 road-over-floor preference route (equal-length ties to the chain with the fewest non-blockable floor / gate tiles, smoothing that never cuts across floor its grid route does not walk, the corner tiles of a diagonal step included) -crosses strictly fewer non-blockable tiles; in that comparison a segment that only touches a floor tile's corner does -not cross it. Used alone (0.1.0) the preference also bent official diagonals into L shapes — the D5 report after +crosses strictly fewer non-blockable tiles and no more 深水区 (GitHub #375, 0.2.2: the 深水区 is non-blockable too, and +on 战场#08(下半) the patrolling leaders had waded through two water tiles to dodge one floor tile); in that comparison a +segment that only touches a floor tile's corner does not cross it. Used alone (0.1.0) the preference also bent official diagonals into L shapes — the D5 report after 0.1.0: on 战场#04 the lower-gate enemies walked (9,10) → (9,8) → (10,8) instead of the official diagonal (9,10) → (10,7), which only brushes the corner of the floor (10,9). Lanes that still differ from the table above (21 of the 154 stage × gate × field routes, test/sim/pathing-official.test.js): diff --git a/server/sim/ai.js b/server/sim/ai.js index 00f08b9e..887fcc20 100644 --- a/server/sim/ai.js +++ b/server/sim/ai.js @@ -28,10 +28,11 @@ // (PRTS 状态机: an enemy's COMBAT state ends with its attack, not with its blocker); the candidates pass the // enemy's own rule (`e.profile.canTarget`) and are ordered // blocker → taunt → latest deployed (targeting.js sortAllyTargets). An enemy's damage type is its data's unless content -// arms it (`e.profile.dmgType`: 转译基底's forms, whose data never attacks). Reaching the final leg's end = leak. A `fear` (恐惧) status suspends the route: the -// enemy runs between random checkpoints away from the fear's source (fear.js moveFeared; a self-inflicted fear -// flutters inside its own tile); an `attract` (诱导) status walks it to the status point instead (moveAttracted); -// both re-plan the route when released (恐惧 outranks 诱导). +// arms it (`e.profile.dmgType`: 转译基底's forms, whose data never attacks). Reaching the final leg's end = leak — on a +// portal entrance it is a teleport to the far exit and a walk to that side's blue door instead (portalPickup). A `fear` +// (恐惧) status suspends the route: the enemy runs between random checkpoints away from the fear's source (fear.js +// moveFeared; a self-inflicted fear flutters inside its own tile); an `attract` (诱导) status walks it to the status +// point instead (moveAttracted); both re-plan the route when released (恐惧 outranks 诱导). import { ATTACK_PAUSE, ALLY_COLLIDER_RADIUS, MOVE_SCALE, PROJECTILE_SPEEDS, PROJECTILE_SPEED, BOOMERANG_RETURN_SPEED, COLS, CHAIN_RADIUS } from './constants.js'; import { sortEnemyTargets, sortAllyTargets, canTargetEnemy, canTargetAlly, tileKeyOf } from './targeting.js'; @@ -634,7 +635,10 @@ function advanceRoute(b, e, dt, R, standing = false) { let guard = 16; while (budget > 1e-9 && guard-- > 0 && e.alive) { const leg = R.legs[R.legIdx]; - if (!leg) { b.leak(e); return; } + if (!leg) { + if (!portalPickup(b, e)) { b.leak(e); return; } + continue; + } if (leg.t === 'wait') { if (R.waitLeft == null) R.waitLeft = leg.time; const use = Math.min(budget, R.waitLeft); @@ -684,13 +688,48 @@ function advanceRoute(b, e, dt, R, standing = false) { } budget = dist / speed; if (R.pts && R.ptIdx >= R.pts.length) { - if (leg.final) { b.leak(e); return; } + if (leg.final) { + if (!portalPickup(b, e)) { b.leak(e); return; } + } R.legIdx++; R.pts = null; } } } +/** + * Boss-field portal pickup (tile_telin / tile_telout; GitHub #336, PR #337 by @2321Robin, both with a recording of the + * official game): a route that ENDS on a portal entrance does not leak there — the enemy is teleported out of the far + * exit and walks on to the blue door on the entrance's side, where it leaks. The boss / Hidden Core circuits end on + * an entrance ([1,3] / [1,17]; 137 routes of data/waves.json); their mid-route crossings are the routes' own DISAPPEAR / + * WAIT / APPEAR steps, every MOVE onto an entrance being followed by a DISAPPEAR, so only the route's end needs this. + * Appends vanish → wait → reappear → walk-to-door legs to the live route and returns true (false: not on an entrance, + * or no exit in the field). The exit is the telout farthest from the entrance, the door the end tile nearest to it: + * every explicit pair of the data (95, routes and extra routes) reads [1,3] / [1,17] → [5,10] — the farther of the two + * telouts [5,10] / [2,10] of the battle stages — and the doors [2,2] / [2,18] stand beside the entrances. Inside the + * portal the enemy stays hidden PORTAL_WAIT s, the wait of the explicit crossings of the same tiles (WAIT 3 s in 85 of + * the 95; 5 s in 9, 1 s in 1). [ASSUMED] the pairing (no tile carries a link) and the 3 s (no source times this one). + */ +const PORTAL_WAIT = 3; +function portalPickup(b, e) { + const r = Math.round(e.y), c = Math.round(e.x); + if (!b.grid.inBounds(r, c) || b.grid.tile(r, c).special !== 'telin') return false; + const outs = b.grid.specialTiles('telout'); + if (!outs.length) return false; + const dist = (p) => hypot(p[0] - r, p[1] - c); + const out = outs.reduce((a, x) => (dist(x) > dist(a) ? x : a)); + const R = e.route; + R.legs.splice(R.legIdx + 1, 0, { t: 'disappear' }, { t: 'wait', time: PORTAL_WAIT }, { t: 'appear', r: out[0], c: out[1] }); + const ends = b.grid.specialTiles('end'); + if (ends.length) { + const door = ends.reduce((a, x) => (dist(x) < dist(a) ? x : a)); + R.legs.push({ t: 'move', r: door[0], c: door[1], final: true }); + } + R.pts = null; + R.tailVersion = -1; // the appended tail invalidates the cached remaining-distance suffixes + return true; +} + /** * How long an enemy stands for one attack: an unblocked ranged enemy for its whole clip (GitHub #58 — the owner's * decision of 2026-10-04 from first-hand memory of the official game: a ranged enemy stops for each attack's animation diff --git a/server/sim/content/bosses.js b/server/sim/content/bosses.js index 8c7f663c..e47b7bf7 100644 --- a/server/sim/content/bosses.js +++ b/server/sim/content/bosses.js @@ -640,8 +640,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() }; @@ -709,11 +712,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/server/sim/content/garrisons/battle.js b/server/sim/content/garrisons/battle.js index 122205b8..214f7029 100644 --- a/server/sim/content/garrisons/battle.js +++ b/server/sim/content/garrisons/battle.js @@ -400,7 +400,10 @@ function applyReader(battle, it) { else battle.removeBuff(u, key); break; case 'act1autochess_gar_eff_respawnTimeByBond': - if (k > 0) S.passiveBuff(battle, u, key, { redeployMul: Math.max(0.05, 1 + S.num(it.bb.respawn_time, 0) * k) }); + // 耀骑士临光's 144: −1.5 % / −3 % per 3 卡西米尔 layers with no minimum in the blackboard (divide_num, respawn_time + // only) — clamped at 0 like every other redeploy multiplier, so a knock-out timer can reach 0 s (GitHub #370; + // until 0.2.1 a 0.05 floor kept it at 5 % of the base time from 192 layers, 96 for the elite) + if (k > 0) S.passiveBuff(battle, u, key, { redeployMul: Math.max(0, 1 + S.num(it.bb.respawn_time, 0) * k) }); else battle.removeBuff(u, key); break; case 'act2autochess_gar_eff_attrByBond_add_onstart': { diff --git a/server/sim/grid.js b/server/sim/grid.js index 8eb6404d..41bbe5dd 100644 --- a/server/sim/grid.js +++ b/server/sim/grid.js @@ -26,8 +26,7 @@ // 0.1.0: on 战场#04 活性源石 the lower-gate enemies must cross diagonally from row 9 into row 10 as officially, not // walk two tiles left of the gate and then straight up). A tile is non-blockable when it is walkable but not LOW // ground buildable for melee (floor / gate / goal / teleport / 深水区 tiles — `blockable()` false; no gate route of 战场#08 -// changed when its 深水区 became non-buildable, a few off-route smoothed steps of its boss field, rows 4–5, go around the -// water instead of cutting across it). Each field holds two candidate +// changed when its 深水区 became non-buildable). Each field holds two candidate // pointers per tile, both with the official `dist` (so the crate cost 1000 and every grid route length stay official): // * official: the plain algorithm above; // * preference (0.1.0's): the SPFA relaxes on (dist, pen) lexicographically, `pen` = non-blockable tiles on the chain @@ -36,7 +35,10 @@ // only on the tile's own raw chain between its two ends, so it never slips past a road tile beside floor its grid // route does not walk. // `next[tile]` is the official pointer unless the preference pointer leads to a route CROSSING strictly fewer -// non-blockable tiles (`cost`, counted to the destination in increasing `dist` order). A segment crosses the tiles +// non-blockable tiles (`cost`, counted to the destination in increasing `dist` order) and no more 深水区 (`deep`): 深水区 +// is non-blockable itself, so without that test the preference waded into the pool to dodge floor (GitHub #375: on +// 战场#08(下半) the patrolling 铳 / 卢西恩 took the lower road (3,9) → (2,3) through two water tiles, one floor tile fewer +// than the official upper road with one). A segment crosses the tiles // whose interior it passes through (`crossTiles`, exact geometry); one it only touches at a corner point is not crossed // — the first version counted the corner tiles there too and so bent official diagonals into L shapes (D5). On equal // counts the official pointer stays, except where the preference one merely skips it (the official waypoint lies on @@ -166,9 +168,12 @@ export class Grid { } /** 1 = walkable terrain an operator cannot block on (floor, gate, goal, teleport…): the path tie-break penalty */ this.unblockable = new Uint8Array(ROWS * COLS); + /** 1 = walkable 深水区 (terrain 'deepsea': tile_deepsea / tile_deepwater): the tie-break never walks into more of it */ + this.deepWater = new Uint8Array(ROWS * COLS); for (let k = 0; k < ROWS * COLS; k++) { const t = this.tiles[k]; this.unblockable[k] = t.pass === 'ALL' && !isBlockableTile(t) ? 1 : 0; + this.deepWater[k] = t.pass === 'ALL' && t.terrain === 'deepsea' ? 1 : 0; } /** obstacle bits per tile (OB_BLOCK | OB_CRATE) */ this.obstacle = new Uint8Array(ROWS * COLS); @@ -272,10 +277,10 @@ export class Grid { _buildField(er, ec, allowDiagonal, ignore) { const N = ROWS * COLS; - const f = { dest: -1, dist: null, parent: null, pen: null, next: null, official: null, cost: null, len: null, version: this.version, allowDiagonal, ignore }; + const f = { dest: -1, dist: null, parent: null, pen: null, next: null, official: null, cost: null, deep: null, len: null, version: this.version, allowDiagonal, ignore }; if (!this.inBounds(er, ec)) { const none = new Int32Array(N).fill(-1); - return Object.assign(f, { dist: none, parent: none, next: none, official: none, pen: new Int32Array(N), cost: new Int32Array(N) }); + return Object.assign(f, { dist: none, parent: none, next: none, official: none, pen: new Int32Array(N), cost: new Int32Array(N), deep: new Int32Array(N) }); } const dest = er * COLS + ec; f.dest = dest; @@ -300,25 +305,30 @@ export class Grid { (n) => { for (let x = n, g = N; x >= 0 && g-- > 0; x = pref.parent[x]) onChain[x] = n; }); // per tile, in increasing distance: the official pointer unless the preference one crosses strictly fewer // non-blockable tiles on the way to the destination — or as few while only skipping the official waypoint o, which - // then lies on the straight line to it and leads there (the same walk, one waypoint fewer) + // then lies on the straight line to it and leads there (the same walk, one waypoint fewer) — and no more 深水区 + // than the official one (GitHub #375) const order = []; for (let k = 0; k < N; k++) if (dist[k] >= 0) order.push(k); order.sort((a, b) => dist[a] - dist[b] || a - b); const through = (a, b) => { let n = -unb[a]; crossTiles(a, b, (r, c) => { n += unb[r * COLS + c]; }); return n; }; + const deepW = this.deepWater; + const wet = (a, b) => { let n = -deepW[a]; crossTiles(a, b, (r, c) => { n += deepW[r * COLS + c]; }); return n; }; const next = new Int32Array(N).fill(-1); const cost = new Int32Array(N); + const deep = new Int32Array(N); for (const k of order) { const o = nextO[k], p = nextP[k]; if (o < 0) continue; // the destination - let use = o, best = through(k, o) + cost[o]; + let use = o, best = through(k, o) + cost[o], bestWet = wet(k, o) + deep[o]; if (p >= 0 && p !== o) { - const cp = through(k, p) + cost[p]; - if (cp < best || (cp === best && next[o] === p && onSegment(k, o, p))) { use = p; best = cp; } + const cp = through(k, p) + cost[p], wp = wet(k, p) + deep[p]; + if (wp <= bestWet && (cp < best || (cp === best && next[o] === p && onSegment(k, o, p)))) { use = p; best = cp; bestWet = wp; } } next[k] = use; cost[k] = best; + deep[k] = bestWet; } - return Object.assign(f, { dist, parent: pref.parent, pen: pref.pen, next, official: nextO, cost }); + return Object.assign(f, { dist, parent: pref.parent, pen: pref.pen, next, official: nextO, cost, deep }); } /** @@ -434,8 +444,9 @@ export class Grid { * @property {Int32Array} pen non-blockable tiles on that raw chain (tile included, destination excluded) * @property {Int32Array} official smoothed parent of the pure official algorithm * @property {Int32Array} next the tile to walk straight toward (official, or the preference pointer when it crosses fewer - * non-blockable tiles — see the module header) + * non-blockable tiles and no more 深水区 — see the module header) * @property {Int32Array} cost non-blockable tiles the route of `next` crosses from the tile (excluded) to the destination + * @property {Int32Array} deep 深水区 tiles the route of `next` crosses from the tile (excluded) to the destination * @property {Float64Array|null} len memoised smoothed length (fieldLength) */ diff --git a/server/sim/simdata.js b/server/sim/simdata.js index 70921635..60eed174 100644 --- a/server/sim/simdata.js +++ b/server/sim/simdata.js @@ -394,6 +394,7 @@ export function normalizeRoute(r) { const t = String(st.t ?? st.type ?? 'move').toUpperCase(); cps.push({ type: normCpType(t), pos: pairOf(st.p ?? st.pos) ?? [0, 0], time: num(st.s ?? st.time, 0) }); } + portalAppear(cps); return { motion, start, end, checkpoints: cps }; } for (const cp of r.checkpoints ?? r.cp ?? []) { @@ -406,9 +407,29 @@ export function normalizeRoute(r) { cps.push({ type: normCpType(cp.type), pos: pairOf(cp.pos ?? cp.position ?? [cp.row, cp.col]) ?? [0, 0], time: num(cp.time ?? cp.t, 0) }); } } + portalAppear(cps); return { motion, start, end, checkpoints: cps }; } +/** + * The boss / Hidden Core templates' 传送门 (GitHub #336, PR #337 by @2321Robin): the twin routes spell a crossing as + * DISAPPEAR → WAIT → APPEAR[exit], but four official routes leave the APPEAR out and go straight to MOVE-to-the-exit + * (act1autochess_h07_02 #4, act1autochess_h08_02 #5 / #9, act2autochess_h07_05 #6; the level files themselves have no + * APPEAR_AT_POS there). Taken literally, such a route walks the enemy's whole remaining run hidden (untargetable, + * unblockable) and it leaks unseen — the crossing must read "reappear on the far side". Re-insert the exit APPEAR + * ahead of that MOVE, at the MOVE's own target, where the twins appear ([5,10]; the leftover MOVE becomes a + * zero-length hop, and the hop no longer counts as walking distance for the targeting order). A DISAPPEAR followed by + * an APPEAR, or by no MOVE, stays as it is; idempotent. + */ +function portalAppear(cps) { + for (let i = 0; i < cps.length; i++) { + if (cps[i].type !== 'DISAPPEAR') continue; + let j = i + 1; + while (j < cps.length && cps[j].type === 'WAIT') j++; + if (cps[j] && cps[j].type === 'MOVE') cps.splice(j, 0, { type: 'APPEAR', pos: cps[j].pos.slice(), time: 0 }); + } +} + function normCpType(t) { const s = String(t ?? 'MOVE').toUpperCase(); if (s.includes('DISAPPEAR')) return 'DISAPPEAR'; diff --git a/test/content/garrisons_battle.test.js b/test/content/garrisons_battle.test.js index a4f5f83c..fbb8ec49 100644 --- a/test/content/garrisons_battle.test.js +++ b/test/content/garrisons_battle.test.js @@ -465,6 +465,33 @@ test('ADD_BOND 耀骑士临光 145 / 160 (real chess): self + front get 144 (red } }); +test('respawnTimeByBond 144 has no 5% floor (#370): redeployMul is max(0, 1 + respawn_time * steps)', () => { + for (const [gid, rt] of [['garrison_144_a', -0.015], ['garrison_144_b', -0.03]]) { + assert.equal(GR(gid).bb.respawn_time, rt); + assert.deepEqual(Object.keys(GR(gid).bb).sort(), ['divide_num', 'respawn_time']); + // 96 / 192 layers: the old 0.05 floor bound the elite / the normal form (raw 0.04); 300: below zero + for (const layers of [9, 96, 192, 300]) { + const k = Math.floor(layers / 3); + const want = Math.max(0, 1 + rt * k); + const h = battle({ + units: [{ id: 'op', g: [gid], row: 10, col: 4, o: { stats: { respawnTime: 100 } } }], + bonds: { kazimierzShip: B(layers) }, + }); + const u = h.unit('op'); + approx(u.findBuff(`gar:${gid}`).mods.redeployMul, want, `${gid} @ ${layers}`); + h.b.dealDamage(null, u, { amount: 1e9, type: 'true' }); + assert.equal(u.alive, false); + approx(u.respawnAt - u.deathAt, 100 * want, `${gid} timer @ ${layers}`); + if (want === 0) { + h.step(2); + assert.ok(u.alive && u.deployed, `${gid} @ ${layers}: a 0 s timer brings it straight back`); + assert.equal(h.b.errorCount, 0); + } + } + cover(gid); + } +}); + // --------------------------------------------------------------------------------------------------------------------- // 魔王 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 7ce84d78..eab3a881 100644 --- a/test/golden/fields.json +++ b/test/golden/fields.json @@ -142,75 +142,75 @@ "killed": 8, "total": 8, "unspawned": 0, - "bossHpLeft": 5311560, - "rngDraws": 839, + "bossHpLeft": 5301010, + "rngDraws": 783, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":612448,"bossDmg":313008,"heal":16689,"deaths":0,"layerGains":{}}, - "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":937710,"bossDmg":372608,"heal":6945,"deaths":2,"layerGains":{}} + "p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":741043,"bossDmg":366117,"heal":19827,"deaths":1,"layerGains":{}}, + "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":819707,"bossDmg":330056,"heal":4781,"deaths":3,"layerGains":{}} }, "units": [ - ["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",3907,1,9,0,183438,1132,168,53,3], - ["p1",2,"op","chess_char_4_01_b",2,"uniequip_002_rmixer",5482.35,1,11,0,3903,11319,311,35,1], - ["p1",3,"op","chess_char_5_14_b",1,"none",2174,1,7,0,30509,953,294,58,17], - ["p1",4,"op","chess_char_5_17_b",1,"uniequip_002_f12yin",2738,1,13,0,10026,0,0,66,1], - ["p1",5,"op","chess_char_5_15_b",1,"uniequip_002_thorn2",1170,1,1,0,79315,0,0,32,8], - ["p1",6,"op","chess_char_5_20_b",2,"uniequip_002_aglina",2418.5,1,3,0,78933,0,0,44,4], - ["p1",7,"op","chess_char_5_21_b",1,"uniequip_002_sntlla",2769.3,1,17,1,26033,0,0,43,2], - ["p1",8,"op","chess_char_5_22_b",1,"uniequip_002_nymph",1611,1,19,2,168011,1748,1748,55,18], - ["p1",9,"op","chess_char_5_23_b",1,"uniequip_002_reckpr",1610,1,5,0,0,0,14114,21,3], - ["p1",10,"op","chess_char_4_02_b",2,"uniequip_002_mostma",1695,1,15,0,32280,1383,55,35,2], - ["p2",11,"op","chess_char_4_11_b",1,"uniequip_002_cathy",836.62,1,12,0,0,3919,1500,0,2], - ["p2",12,"op","chess_char_4_04_b",1,"uniequip_002_ines",3545.85,1,51,2,253580,496,458,287,8], - ["p2",13,"op","chess_char_4_07_b",1,"uniequip_002_bpipe",2099.31,1,8,0,4500,1149,865,24,8], - ["p2",14,"op","chess_char_4_16_b",2,"uniequip_002_texas2",2671.35,1,52,1,112288,6746,1602,147,13], - ["p2",15,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",2777.6,1,10,0,62072,0,0,54,3], - ["p2",16,"op","chess_char_4_06_b",1,"uniequip_002_kroos2",1561,1,2,1,303642,0,0,181,3], + ["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",3907,1,9,0,235262,2487,437,74,3], + ["p1",2,"op","chess_char_4_01_b",2,"uniequip_002_rmixer",5482.35,1,11,0,12166,13761,3921,33,1], + ["p1",3,"op","chess_char_5_14_b",1,"none",2174,1,7,0,24813,953,294,16,12], + ["p1",4,"op","chess_char_5_17_b",1,"uniequip_002_f12yin",2738,1,13,0,5871,0,0,20,1], + ["p1",5,"op","chess_char_5_15_b",1,"uniequip_002_thorn2",1170,1,1,0,123408,0,0,77,10], + ["p1",6,"op","chess_char_5_20_b",2,"uniequip_002_aglina",2418.5,1,3,0,81805,0,0,44,4], + ["p1",7,"op","chess_char_5_21_b",1,"uniequip_002_sntlla",2769.3,1,17,2,56882,1768,1768,100,3], + ["p1",8,"op","chess_char_5_22_b",1,"uniequip_002_nymph",748.12,1,43,1,153989,3270,796,46,16], + ["p1",9,"op","chess_char_5_23_b",1,"uniequip_002_reckpr",1610,1,5,0,0,0,12611,20,3], + ["p1",10,"op","chess_char_4_02_b",2,"uniequip_002_mostma",1695,1,15,1,46847,0,0,34,2], + ["p2",11,"op","chess_char_4_11_b",1,"uniequip_002_cathy",2172.64,1,50,0,0,5191,1078,0,1], + ["p2",12,"op","chess_char_4_04_b",1,"uniequip_002_ines",3545.85,1,51,2,259014,0,0,279,8], + ["p2",13,"op","chess_char_4_07_b",1,"uniequip_002_bpipe",2383,1,52,0,3678,2726,343,18,7], + ["p2",14,"op","chess_char_4_16_b",2,"uniequip_002_texas2",2671.35,1,49,1,97836,2233,1258,153,10], + ["p2",15,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",2777.6,1,10,0,82406,0,0,73,4], + ["p2",16,"op","chess_char_4_06_b",1,"uniequip_002_kroos2",1561,1,2,0,179138,0,0,231,3], ["p2",17,"op","chess_char_4_10_b",1,"uniequip_002_aroma",1664,1,4,0,0,0,0,0,0], - ["p2",18,"op","chess_char_4_12_b",2,"uniequip_002_glady",1418.27,1,18,1,69028,3331,2517,58,3], - ["p2",19,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,40,0,44456,1991,2,63,18], - ["p2",20,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",1626,1,6,0,88144,0,0,57,14], + ["p2",18,"op","chess_char_4_12_b",2,"uniequip_002_glady",1003.26,1,18,0,72377,3328,2099,61,4], + ["p2",19,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,40,0,62819,1991,2,81,22], + ["p2",20,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",1626,1,6,1,62440,0,0,47,13], ["p2",21,"token","token_10041_cathy_catsld",null,null,100,1,21,0,0,0,0,0,null], ["p2",22,"token","token_10041_cathy_catsld",null,null,100,1,22,0,0,0,0,0,null] ], - "enemies": {"enemy_1005_yokai_2":[6,6,0,3179,62190],"enemy_1430_lrrook":[2,2,0,0,802449],"enemy_9017_achunt":[2,0,0,30987,688440]}, - "hooks": [1,50,6000,1312,1325,2789,2834,4861,11,26,131,125,21,35636,978,0,10,0,1,11,0,13,1], - "events": {"atk":1342,"deploy":40,"die":26,"dmg":2826,"engage":17,"fx":602,"heal":41,"skill":256,"spawn":32,"status":489}, + "enemies": {"enemy_1005_yokai_2":[6,6,0,3956,62190],"enemy_1430_lrrook":[2,2,0,0,802449],"enemy_9017_achunt":[2,0,0,33752,698990]}, + "hooks": [1,50,6000,1372,1410,2907,2944,3787,13,26,127,123,21,38164,1047,0,10,0,1,13,0,12,1], + "events": {"atk":1411,"deploy":40,"die":26,"dmg":2934,"engage":17,"fx":696,"heal":41,"skill":250,"spawn":32,"status":549}, "fx": { - "aoe": 38, + "aoe": 33, "appear": 2, - "blink": 16, - "burst": 13, + "blink": 14, + "burst": 12, "catShield": 1, - "charge": 8, + "charge": 7, "counter": 21, - "crit": 88, - "dilemma": 3, + "crit": 89, + "dilemma": 4, "disappear": 2, "dodge": 1, - "explosion": 14, - "frostNova": 18, + "explosion": 13, + "frostNova": 22, "garrisonGrant": 1, - "iceSpike": 39, - "overclock": 2, - "pull": 3, + "iceSpike": 89, + "overclock": 1, + "pull": 4, "quadShot": 2, - "ripple": 13, + "ripple": 20, "sentry": 6, "sermon": 1, - "shadowWeave": 11, - "shield": 6, - "soul": 5, - "sp": 11, + "shadowWeave": 10, + "shield": 5, + "soul": 4, + "sp": 7, "stealth": 8, - "swordRain": 96, - "swordStorm": 13, - "tornado": 3, - "tornadoPulse": 15, + "swordRain": 82, + "swordStorm": 10, + "tornado": 4, + "tornadoPulse": 20, "weightless": 4, - "zone": 138 + "zone": 197 }, - "snaps": "7917cfe4 ec53e8ac 09d8b201 823345e1 5d4f2a95 9efdd7ee 56e0addd 78c7726c 4ee3695b 20aee5b5 ba3311c0 d4d127c9 0c500cac d842272b 4cbf4a87 2525db60 3291ce13 ecb45e94 c2e5c2d0 a4ac7943" + "snaps": "7917cfe4 719c3d1e 5fce9836 dcc6d1db c6372862 d28f9510 24fc36dd 451a8b51 a84bc802 17264489 87aca75c 241abfe9 0bdd4e23 94a753a7 f66cf2d6 27f37155 8c831d53 112e1ac2 a0065d70 7e66122d" }, "boss-boss_2-solo": { "about": "假想敌:铳 solo pool 3000000: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_4_20_b chess_char_4_21_b chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b", @@ -227,27 +227,27 @@ "killed": 5, "total": 7, "unspawned": 0, - "bossHpLeft": 2761255, - "rngDraws": 127, + "bossHpLeft": 2757148, + "rngDraws": 130, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":5,"total":7,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":401784,"bossDmg":232315,"heal":40581,"deaths":1,"layerGains":{}}}, + "players": {"p1":{"killed":5,"total":7,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":451604,"bossDmg":236418,"heal":39582,"deaths":1,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",2383.55,1,18,0,4821,19443,15985,82,0], - ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",6104,1,4,0,86351,0,0,79,4], - ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",2811,1,6,0,77745,6115,3711,91,4], - ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",2118,1,7,1,23391,7108,1936,80,7], - ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2569,1,8,4,131322,8759,0,69,2], - ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",1419,1,9,0,16118,0,0,11,0], + ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",2383.55,1,18,0,5154,18917,15557,82,0], + ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",6104,1,4,0,89239,0,0,82,4], + ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",2811,1,6,0,89910,6115,3639,104,4], + ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",2118,1,7,1,23474,7108,1915,82,7], + ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2569,1,8,4,162219,8759,0,75,3], + ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",1419,1,9,0,23155,0,0,12,2], ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",1584,1,10,0,0,1263,566,2,1], - ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2178,1,1,0,62036,0,1129,85,6], - ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",695.86,1,3,0,0,1448,1057,0,3], - ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,5,0,0,0,16195,34,3] + ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2178,1,1,0,58453,0,1107,75,5], + ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1671,1,3,0,0,0,574,0,3], + ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,5,0,0,0,16224,34,3] ], - "enemies": {"enemy_1249_lysdb_2":[5,5,0,8138,107495],"enemy_1430_lrrook":[2,0,2,0,66943],"enemy_9017_achunt":[1,0,0,23716,238745]}, - "hooks": [1,19,6000,580,580,1299,1898,5497,6,8,30,29,0,13846,99,5,8,2,1,8,0,6,2], - "events": {"atk":615,"deploy":11,"die":6,"dmg":1888,"engage":7,"fx":277,"heal":37,"leak":2,"skill":62,"spawn":18,"status":481}, - "fx": {"bandage":3,"block":9,"burst":6,"charge":7,"dodge":2,"flame":7,"garrisonGrant":1,"healField":1,"jungleSoul":4,"mote":102,"redistribute":45,"reweave":3,"riposte":2,"rockslide":4,"sandChains":5,"sandChainsCharged":1,"shieldBreak":15,"slash":32,"spGift":5,"thorns":21,"truesilver":2}, - "snaps": "a42f06d2 4a40e022 e95a4901 abb693b9 1c238b4b afb89449 d6c6b3df c846d6b3 fa04e7ce c4d85f2e 4a6790c9 effed23a 72e8e203 8146bf98 e8dfb61c 69d1b960 6d949949 0310d38f a1429900 b8610bee" + "enemies": {"enemy_1249_lysdb_2":[5,5,0,8138,107495],"enemy_1430_lrrook":[2,0,2,0,112690],"enemy_9017_achunt":[1,0,0,21742,242852]}, + "hooks": [1,19,6000,595,595,1368,1966,5211,6,8,32,32,0,15401,91,5,8,2,1,8,0,6,2], + "events": {"atk":638,"deploy":11,"die":6,"dmg":1956,"engage":7,"fx":289,"heal":37,"leak":2,"skill":67,"spawn":18,"status":465}, + "fx": {"appear":2,"bandage":3,"block":10,"burst":6,"charge":7,"disappear":2,"dodge":2,"featherArrow":2,"flame":7,"garrisonGrant":1,"healField":1,"jungleSoul":4,"mote":102,"redistribute":45,"reweave":3,"riposte":2,"rockslide":4,"sandChains":5,"shieldBreak":15,"slash":38,"spGift":5,"thorns":20,"truesilver":3}, + "snaps": "a42f06d2 4a40e022 e95a4901 abb693b9 1c238b4b afb89449 d6c6b3df c846d6b3 7c93c566 120f2baa 5eba2341 475bd0e1 a82180cf 2ec0f97d b5acfe2a 942750af b2587ba1 f8bb84fe 0dee9c0d c305f7f7" }, "boss-boss_3-pair": { "about": "假想敌:管 pair pool 4000000: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b chess_char_6_09_b chess_char_6_11_b token_10017_skadi2_dedant | chess_char_6_15_b chess_char_6_19_b chess_char_5_01_b chess_char_6_17_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b chess_char_6_18_b chess_char_6_20_b", @@ -516,29 +516,29 @@ "killed": 5, "total": 7, "unspawned": 0, - "bossHpLeft": 3759559, - "rngDraws": 84, + "bossHpLeft": 3757752, + "rngDraws": 87, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":5,"total":7,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":713408,"bossDmg":440441,"heal":95170,"deaths":6,"layerGains":{}}}, + "players": {"p1":{"killed":5,"total":7,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":731117,"bossDmg":442248,"heal":91813,"deaths":7,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",2497.84,1,23,2,19548,11293,12704,21,0], + ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",2762.76,1,23,2,19548,11760,12983,21,0], ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",5981.62,1,22,0,42568,20827,14636,11,5], - ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",2811,1,21,3,202794,41516,12031,43,1], - ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",3350.14,1,9,0,0,7920,0,0,0], - ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2541.58,1,10,0,162116,8875,0,135,3], - ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,24,0,234028,2838,0,104,5], - ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",1584,1,11,0,0,4444,24160,29,2], - ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2178,1,25,0,52353,7314,487,76,5], - ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1394.71,1,3,0,0,5589,5880,0,3], - ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,5,0,0,4444,25271,28,4], + ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",2811,1,21,3,202794,41516,12121,43,1], + ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",2350.14,1,9,0,785,9979,0,1,1], + ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2061.88,1,24,0,177744,6029,0,120,3], + ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,25,0,234028,2838,0,104,5], + ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",1545.34,1,11,0,0,4444,18376,28,2], + ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",1748.63,1,26,0,53649,7314,487,76,5], + ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1394.71,1,3,0,0,6735,7186,0,3], + ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,5,0,0,4444,26024,27,5], ["-","~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_1010_demon_2":[2,2,0,5940,107495],"enemy_1438_dspred":[5,3,2,31950,165472],"enemy_1521_dslily":[1,0,0,71001,440441]}, - "hooks": [1,25,6000,565,565,958,1133,7303,11,13,28,28,0,13157,125,16,8,2,1,16,0,29,1], - "events": {"atk":770,"deploy":15,"die":11,"dmg":1115,"engage":6,"fx":335,"heal":105,"leak":2,"skill":59,"spawn":20,"status":479}, - "fx": {"bandage":4,"block":15,"burst":29,"camouflage":1,"dodge":1,"featherArrow":5,"garrisonGrant":1,"grow":1,"healField":2,"jungleSoul":1,"mote":61,"redistribute":45,"reweave":3,"rockfall":18,"rockslide":5,"sandChains":5,"shieldBreak":18,"slash":53,"spGift":4,"tentacle":6,"thorns":48,"tide":5,"truesilver":3,"undying":1}, - "snaps": "b3cecd10 d3d68301 c10d8554 c91066bb 2ac0bbdd cb65c8b1 e357c87a 1cfd4db1 1a8bd8d8 e639cb94 69f98f4a 92de5c02 51bb18c2 a8b02c45 54b09843 f60538cb 94407072 8c567dae 4c15135e a957f119" + "enemies": {"enemy_1010_demon_2":[2,2,0,5940,107495],"enemy_1438_dspred":[5,3,2,34519,181373],"enemy_1521_dslily":[1,0,0,69256,442248]}, + "hooks": [1,26,6000,551,551,949,1120,8051,12,14,30,30,0,13414,126,18,8,2,1,17,0,28,2], + "events": {"atk":754,"deploy":16,"die":12,"dmg":1102,"engage":7,"fx":339,"heal":91,"leak":2,"skill":63,"spawn":20,"status":479}, + "fx": {"appear":2,"bandage":5,"block":15,"burst":28,"camouflage":1,"disappear":2,"dodge":2,"featherArrow":5,"flame":1,"garrisonGrant":1,"grow":1,"healField":2,"jungleSoul":1,"mote":61,"redistribute":45,"reweave":3,"riposte":1,"rockfall":18,"rockslide":5,"sandChains":5,"shieldBreak":18,"slash":50,"spGift":4,"tentacle":6,"thorns":48,"tide":5,"truesilver":3,"undying":1}, + "snaps": "b3cecd10 d3d68301 c10d8554 c91066bb 2ac0bbdd cb65c8b1 e357c87a 1cfd4db1 842efc1f 73ccb5e1 7cbe283e 83895aff b84dfc8e 65315bfe 89187e2c 11947687 dc8072f3 31f16379 3c0d78cb cf38d801" }, "boss-boss_5-pair": { "about": "卢西恩,“猩红血钻” pair pool 1560000: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b chess_char_6_09_b chess_char_6_11_b token_10017_skadi2_dedant token_10030_mlyss_wtrman | chess_char_6_15_b chess_char_6_19_b chess_char_5_01_b chess_char_6_17_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b chess_char_6_18_b chess_char_6_20_b", @@ -703,35 +703,35 @@ "time": 200, "ticks": 6000, "reason": "forced", - "killed": 4, + "killed": 5, "total": 6, "unspawned": 0, - "bossHpLeft": 7017881, - "rngDraws": 570, + "bossHpLeft": 6932482, + "rngDraws": 708, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":4,"total":6,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":869760,"bossDmg":436648,"heal":2146,"deaths":5,"layerGains":{}}, - "p2": {"killed":0,"total":0,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":1033990,"bossDmg":505471,"heal":1246,"deaths":19,"layerGains":{}} + "p1": {"killed":5,"total":6,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1096623,"bossDmg":528737,"heal":2146,"deaths":5,"layerGains":{}}, + "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1199975,"bossDmg":498782,"heal":1246,"deaths":19,"layerGains":{}} }, "units": [ - ["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",3907,1,3,1,147578,0,0,30,2], + ["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",3907,1,3,1,211527,0,0,46,3], ["p1",2,"op","chess_char_4_01_b",2,"uniequip_002_rmixer",3537,1,1,0,0,0,0,0,0], - ["p1",3,"op","chess_char_5_14_b",1,"none",2174,1,5,1,25423,0,0,0,10], + ["p1",3,"op","chess_char_5_14_b",1,"none",2174,1,5,1,39624,0,0,0,16], ["p1",4,"op","chess_char_5_17_b",1,"uniequip_002_f12yin",4654.6,1,11,1,4568,0,0,6,1], - ["p1",5,"op","chess_char_5_15_b",1,"uniequip_002_thorn2",1170,1,7,1,303339,0,0,64,13], - ["p1",6,"op","chess_char_5_20_b",2,"uniequip_002_aglina",1382,1,13,0,179149,0,0,76,6], - ["p1",7,"op","chess_char_5_21_b",1,"uniequip_002_sntlla",1629,1,66,0,19506,1629,0,43,2], - ["p1",8,"op","chess_char_5_22_b",1,"uniequip_002_nymph",1611,1,67,0,135206,3222,0,46,14], - ["p1",9,"op","chess_char_5_23_b",1,"uniequip_002_reckpr",1610,1,65,0,2158,3220,2146,3,1], - ["p1",10,"op","chess_char_4_02_b",2,"uniequip_002_mostma",1695,1,9,0,52833,0,0,38,2], + ["p1",5,"op","chess_char_5_15_b",1,"uniequip_002_thorn2",1170,1,7,1,407448,0,0,83,18], + ["p1",6,"op","chess_char_5_20_b",2,"uniequip_002_aglina",1382,1,13,0,198029,0,0,73,6], + ["p1",7,"op","chess_char_5_21_b",1,"uniequip_002_sntlla",1629,1,66,0,24193,1629,0,43,2], + ["p1",8,"op","chess_char_5_22_b",1,"uniequip_002_nymph",1611,1,67,0,140802,3222,0,46,15], + ["p1",9,"op","chess_char_5_23_b",1,"uniequip_002_reckpr",1610,1,65,0,4316,3220,2146,3,1], + ["p1",10,"op","chess_char_4_02_b",2,"uniequip_002_mostma",1695,1,9,0,66114,0,0,47,3], ["p2",11,"op","chess_char_4_11_b",1,"uniequip_002_cathy",2644,1,49,0,4798,9646,260,10,1], - ["p2",12,"op","chess_char_4_04_b",1,"uniequip_002_ines",190.91,1,63,0,118842,15240,280,209,6], + ["p2",12,"op","chess_char_4_04_b",1,"uniequip_002_ines",190.91,1,63,0,134122,15240,280,209,6], ["p2",13,"op","chess_char_4_07_b",1,"uniequip_002_bpipe",2383,1,6,0,0,0,0,0,0], - ["p2",14,"op","chess_char_4_16_b",2,"uniequip_002_texas2",0,0,61,0,58463,13618,706,128,7], - ["p2",15,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",2777.6,1,14,0,205534,0,0,117,6], - ["p2",16,"op","chess_char_4_06_b",1,"uniequip_002_kroos2",1561,1,8,0,493990,0,0,140,2], + ["p2",14,"op","chess_char_4_16_b",2,"uniequip_002_texas2",0,0,61,0,60730,13618,706,128,7], + ["p2",15,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",2777.6,1,14,0,201953,0,0,114,6], + ["p2",16,"op","chess_char_4_06_b",1,"uniequip_002_kroos2",1561,1,8,1,634150,0,0,182,3], ["p2",17,"op","chess_char_4_10_b",1,"uniequip_002_aroma",1664,1,12,0,0,0,0,0,0], - ["p2",18,"op","chess_char_4_12_b",2,"uniequip_002_glady",2232,1,47,0,28737,2232,0,110,5], + ["p2",18,"op","chess_char_4_12_b",2,"uniequip_002_glady",2232,1,47,0,40597,2232,0,106,5], ["p2",19,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",0,0,58,0,47100,5967,0,39,9], ["p2",20,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",0,0,59,0,76526,4878,0,22,4], ["p2",21,"token","token_10041_cathy_catsld",null,null,100,1,21,0,0,0,0,0,null], @@ -741,20 +741,20 @@ "enemy_10028_vtswd": [3,3,0,0,0], "enemy_10029_vtshld": [5,5,0,0,0], "enemy_10030_vtwand": [5,5,0,0,0], - "enemy_10085_hllevi_2": [2,0,2,0,761019], + "enemy_10085_hllevi_2": [2,1,0,0,1068467], "enemy_1010_demon": [4,4,0,12402,200612], - "enemy_9032_aclionk": [1,0,0,47250,942119] + "enemy_9032_aclionk": [1,0,0,47250,1027518] }, - "hooks": [1,65,6000,1074,1088,2031,2044,431,41,45,91,88,0,30474,660,8,20,2,1,29,0,2,0], - "events": {"atk":1139,"deploy":45,"die":43,"dmg":1988,"engage":15,"fx":618,"heal":7,"leak":2,"skill":179,"spawn":42,"status":942}, + "hooks": [1,65,6000,1150,1164,2296,2310,431,42,44,106,101,0,32028,744,8,20,0,1,30,0,2,0], + "events": {"atk":1232,"deploy":45,"die":44,"dmg":2254,"engage":15,"fx":761,"heal":7,"skill":207,"spawn":42,"status":1102}, "fx": { - "aoe": 30, - "appear": 4, + "aoe": 44, + "appear": 6, "blink": 2, "burst": 2, - "crit": 81, + "crit": 109, "dilemma": 6, - "disappear": 4, + "disappear": 6, "displace": 19, "equip": 18, "explosion": 4, @@ -763,26 +763,26 @@ "iceSpike": 33, "overclock": 1, "pull": 4, - "quadShot": 2, + "quadShot": 3, "revive": 2, - "ripple": 18, + "ripple": 25, "sentry": 4, - "shadowWeave": 4, + "shadowWeave": 5, "shield": 1, "soul": 2, - "sp": 13, + "sp": 19, "stealth": 6, "sword": 1, - "swordRain": 25, + "swordRain": 26, "swordStorm": 7, - "telegraph": 19, + "telegraph": 21, "tornado": 5, "tornadoPulse": 20, "vest": 5, "weightless": 6, - "zone": 260 + "zone": 339 }, - "snaps": "8ef2441b ee06d28e 0f318fc5 8e4156fd 466da928 e81f42d7 7687b463 1723e66d 7f419938 8e9ddd0f 9f00d62e 2aa9f2dd bca0bec4 7ba9a9bc a548f671 7a235ae3 7fc35fab 2bf5becc eb0dbe2a ee2661d9" + "snaps": "8ef2441b ee06d28e 0f318fc5 8e4156fd 466da928 e81f42d7 7687b463 1723e66d 7f419938 8e9ddd0f 9f00d62e 2aa9f2dd bca0bec4 90e786de 4f33ccda b4b8971e b9cab80b b4845fa4 1fdd7096 42eef665" }, "boss-boss_6-solo": { "about": "阿利斯泰尔,帝国余晖 solo pool 3980000: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_4_20_b chess_char_4_21_b chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b", @@ -796,23 +796,23 @@ "time": 200, "ticks": 6000, "reason": "forced", - "killed": 5, + "killed": 8, "total": 4, "unspawned": 0, - "bossHpLeft": 3874806, - "rngDraws": 45, + "bossHpLeft": 3907393, + "rngDraws": 54, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":5,"total":4,"perfect":false,"leaks":5,"leaksUncounted":0,"coins":0,"dmg":152625,"bossDmg":105194,"heal":1783,"deaths":16,"layerGains":{}}}, + "players": {"p1":{"killed":8,"total":4,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":164010,"bossDmg":72607,"heal":1088,"deaths":18,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3513,1,10,2,8,0,0,8,0], - ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",6104,1,11,0,6,0,0,6,1], - ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",2811,1,12,1,8984,35,35,15,2], - ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",2118,1,48,0,1033,8472,0,40,3], - ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",0,0,46,0,52228,18906,1436,38,4], - ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",1419,1,7,2,88481,0,0,50,7], + ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3513,1,10,2,10,0,0,10,0], + ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",6104,1,11,1,8,0,0,8,1], + ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",2811,1,12,2,8987,35,35,17,2], + ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",0,0,48,1,790,10590,0,30,2], + ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",0,0,46,1,87664,18412,943,31,4], + ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",1419,1,7,1,59955,0,0,53,7], ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",2772,1,8,0,0,0,0,0,0], - ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2178,1,49,0,1884,8712,0,10,3], - ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",0,0,44,0,0,6995,311,0,0], + ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",0,0,43,0,6596,8712,0,7,2], + ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",0,0,49,0,0,8465,110,0,0], ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,9,0,0,0,0,0,0], ["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null], ["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null], @@ -822,18 +822,18 @@ ["-","~6","device","trap_1105_accrate",null,null,100,1,6,0,0,0,0,0,null] ], "enemies": { - "enemy_10028_vtswd": [4,4,0,0,0], - "enemy_10029_vtshld": [3,3,0,0,0], - "enemy_10030_vtwand": [1,1,0,0,0], - "enemy_10085_hllevi_2": [2,0,2,0,29468], - "enemy_1197_sfshu": [2,2,0,17,17916], - "enemy_1198_msfshu": [6,3,3,0,47], - "enemy_9032_aclionk": [1,0,0,43085,105194] + "enemy_10028_vtswd": [3,3,0,0,0], + "enemy_10029_vtshld": [4,4,0,0,0], + "enemy_10030_vtwand": [2,2,0,0,0], + "enemy_10085_hllevi_2": [2,0,2,0,73421], + "enemy_1197_sfshu": [2,2,0,383,17916], + "enemy_1198_msfshu": [6,6,0,0,66], + "enemy_9032_aclionk": [1,0,0,45814,72607] }, - "hooks": [1,49,6000,179,179,275,472,1334,29,34,20,20,0,6463,12,1,19,5,1,21,0,0,3], - "events": {"atk":205,"deploy":24,"die":29,"dmg":201,"engage":7,"fx":91,"leak":5,"skill":42,"spawn":35,"status":73}, - "fx": {"dodge":3,"equip":9,"featherArrow":7,"flame":3,"garrisonGrant":1,"jungleSoul":2,"mote":8,"riposte":3,"rockslide":1,"sandChains":3,"slash":29,"summon":2,"sword":3,"telegraph":10,"truesilver":4,"vest":3}, - "snaps": "3e08ed3d b0f5c6bf 12616b1d 9cce9eed afcfc076 fb56bae7 617f9500 b046bb87 398d9618 80f1089f 99802228 823c06f9 a478439e 71b3d152 452c5be2 683a7bbf 1f49e639 2d0e0575 72ea261c 8fc27088" + "hooks": [1,50,6000,169,169,290,488,922,35,37,18,18,0,5924,10,2,20,2,1,27,0,0,2], + "events": {"atk":212,"deploy":24,"die":35,"dmg":238,"engage":7,"fx":94,"leak":2,"skill":38,"spawn":36,"status":77}, + "fx": {"appear":5,"disappear":5,"dodge":2,"equip":11,"featherArrow":7,"flame":2,"garrisonGrant":1,"jungleSoul":2,"mote":6,"riposte":1,"rockslide":1,"sandChains":2,"slash":22,"spGift":1,"summon":2,"sword":3,"telegraph":13,"truesilver":4,"vest":4}, + "snaps": "3e08ed3d b0f5c6bf 12616b1d 26cd4bb1 3ac9a7a9 dd6d6849 99b9989f 8d641dd4 9437b015 26f59cb1 cd9f4a7b d484bda0 a0fbf26e bb4ffa9d 8bd4554c d61db91c 9810e989 2fda8eab d003c4fc bad20758" }, "boss-boss_7-pair": { "about": "“萨米的意志” pair pool 4000000: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b chess_char_6_09_b chess_char_6_11_b token_10017_skadi2_dedant token_10030_mlyss_wtrman | chess_char_6_15_b chess_char_6_19_b chess_char_5_01_b chess_char_6_17_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b chess_char_6_18_b chess_char_6_20_b", @@ -964,7 +964,7 @@ "errors": {"count":0,"kinds":[]}, "players": { "p1": {"killed":16,"total":14,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2677042,"bossDmg":1550396,"heal":58431,"deaths":17,"layerGains":{}}, - "p2": {"killed":0,"total":0,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":560253,"bossDmg":262771,"heal":20725,"deaths":25,"layerGains":{}} + "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":560253,"bossDmg":262771,"heal":20725,"deaths":25,"layerGains":{}} }, "units": [ ["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",7814,1,90,1,335665,24097,655,20,3], @@ -991,7 +991,7 @@ ["p2",22,"token","token_10041_cathy_catsld",null,null,100,1,22,0,0,0,0,0,null] ], "enemies": { - "enemy_10027_vtsk": [6,4,1,137764,1019681], + "enemy_10027_vtsk": [6,4,0,137764,1019681], "enemy_1005_yokai": [4,4,0,0,10699], "enemy_1229_darmy_2": [8,8,0,6104,294231], "enemy_9013_acstmk_2": [1,0,0,63941,1814230], @@ -999,8 +999,8 @@ "enemy_9015_acstmb": [3,2,0,9835,83569], "enemy_9016_acstmr": [8,7,0,0,150] }, - "hooks": [1,92,6000,904,910,2960,3146,8374,72,75,107,101,6,25061,1027,17,36,1,1,61,0,5,9], - "events": {"atk":1083,"deploy":56,"die":74,"dmg":3143,"engage":18,"fx":651,"heal":62,"leak":1,"skill":208,"spawn":58,"status":990}, + "hooks": [1,92,6000,904,910,2960,3146,8374,72,74,107,101,6,25061,1027,17,36,0,1,61,0,5,9], + "events": {"atk":1083,"deploy":56,"die":74,"dmg":3143,"engage":18,"fx":652,"heal":62,"skill":208,"spawn":58,"status":990}, "fx": { "aoe": 42, "appear": 7, @@ -1011,7 +1011,7 @@ "counter": 6, "crit": 59, "dilemma": 3, - "disappear": 7, + "disappear": 8, "displace": 1, "dodge": 9, "explode": 10, @@ -1044,7 +1044,7 @@ "weightless": 3, "zone": 180 }, - "snaps": "0499777c 2a8a8958 27548bdc 4d50b14c 2d952e29 e8bac176 f62bddd6 c54603c8 eec4acbb 0e35cf5c f32cd036 b1a89b99 601ad31e 90860daf 260abb25 982d94cf 1e000364 bf1ae288 42ae578f 90a8c025" + "snaps": "0499777c 2a8a8958 27548bdc 4d50b14c 2d952e29 e8bac176 f62bddd6 c54603c8 eec4acbb 0e35cf5c f32cd036 b1a89b99 601ad31e 90860daf 260abb25 982d94cf 1e000364 bf1ae288 42ae578f 9af424ea" }, "hidden-boss_8-solo": { "about": "假想敌:胄 solo pool 7200000: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_4_20_b chess_char_4_21_b chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b", @@ -1058,43 +1058,43 @@ "time": 200, "ticks": 6000, "reason": "forced", - "killed": 3, + "killed": 6, "total": 6, "unspawned": 0, - "bossHpLeft": 6797394, - "rngDraws": 227, + "bossHpLeft": 7007993, + "rngDraws": 122, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":3,"total":6,"perfect":false,"leaks":10,"leaksUncounted":0,"coins":0,"dmg":450453,"bossDmg":402606,"heal":28293,"deaths":23,"layerGains":{}}}, + "players": {"p1":{"killed":6,"total":6,"perfect":false,"leaks":12,"leaksUncounted":0,"coins":0,"dmg":236889,"bossDmg":192007,"heal":29302,"deaths":24,"layerGains":{}}}, "units": [ ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",0,0,6,0,264,7875,0,4,0], - ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",0,0,32,1,13954,18678,6470,10,5], - ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",0,0,7,0,0,2811,0,0,0], - ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",0,0,53,0,0,10739,0,0,0], - ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2569,1,11,2,397515,0,140,154,3], - ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,29,0,5558,2838,0,6,0], - ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",0,0,55,0,0,8993,10992,37,6], + ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",0,0,35,1,12200,18678,6470,12,5], + ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",0,0,7,0,10,2811,0,8,1], + ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",0,0,59,2,97,11660,0,13,1], + ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",0,0,11,3,187439,3191,140,63,1], + ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,29,0,3718,2838,0,6,0], + ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",0,0,60,0,0,8933,12476,37,6], ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",0,0,13,0,33161,6074,939,21,1], - ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",0,0,52,0,0,12130,3641,0,1], - ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",0,0,50,0,0,5960,6111,15,1], + ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",0,0,57,0,0,11657,3168,0,1], + ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",0,0,55,0,0,5960,6111,15,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], ["-","~4","device","trap_1105_accrate",null,null,100,1,4,0,0,0,0,0,null] ], "enemies": { - "enemy_10027_vtsk": [3,0,3,29903,18989], - "enemy_1005_yokai": [4,2,2,0,2867], - "enemy_1197_sfshu": [3,1,2,27,17625], - "enemy_1198_msfshu": [3,0,3,0,7], - "enemy_9013_acstmk_2": [1,0,0,37240,402606], - "enemy_9014_acstma": [3,2,0,0,7972], - "enemy_9015_acstmb": [4,3,0,6587,340], - "enemy_9016_acstmr": [7,6,0,0,47] + "enemy_10027_vtsk": [3,0,3,34423,16521], + "enemy_1005_yokai": [4,1,2,0,1433], + "enemy_1197_sfshu": [3,3,0,649,26874], + "enemy_1198_msfshu": [9,2,7,0,43], + "enemy_9013_acstmk_2": [1,0,0,36456,192007], + "enemy_9014_acstma": [4,3,0,0,0], + "enemy_9015_acstmb": [4,3,0,5808,0], + "enemy_9016_acstmr": [7,6,0,0,11] }, - "hooks": [1,56,6000,303,303,564,576,1204,37,47,17,17,0,5895,50,3,28,10,1,29,0,0,0], - "events": {"atk":360,"deploy":24,"die":37,"dmg":569,"engage":5,"fx":188,"heal":76,"leak":10,"skill":35,"spawn":42,"status":149}, - "fx": {"bandage":1,"beam":34,"blink":11,"block":6,"charge":6,"explode":11,"garrisonGrant":1,"healField":6,"mote":4,"redistribute":5,"reweave":1,"rockslide":5,"sandChains":1,"shell":7,"shieldBreak":7,"slash":58,"spGift":2,"summon":5,"telegraph":3,"thorns":11,"truesilver":3}, - "snaps": "9b5e58cf c4e06fea a40166a3 0e30f917 42214d72 ebc20f66 7258ad62 49f2a70f 0b93ecfb 8a8ebc4e 4bf3c623 15ee0c07 41d58e70 7b48444f 6d3425f8 2d1f93c0 a755c2ad 76e23872 1016f3ce e5423b09" + "hooks": [1,63,6000,241,241,391,386,909,42,54,17,18,0,3782,53,7,35,12,1,36,0,0,0], + "events": {"atk":287,"deploy":24,"die":42,"dmg":379,"engage":7,"fx":174,"heal":78,"leak":12,"skill":36,"spawn":49,"status":159}, + "fx": {"appear":11,"bandage":1,"beam":32,"blink":12,"block":6,"charge":6,"disappear":11,"explode":12,"flame":1,"garrisonGrant":1,"healField":6,"jungleSoul":1,"mote":4,"redistribute":5,"reweave":1,"rockslide":5,"sandChains":1,"shell":7,"shieldBreak":7,"slash":20,"spGift":2,"summon":7,"telegraph":3,"thorns":11,"truesilver":1}, + "snaps": "9b5e58cf c4e06fea a40166a3 0e30f917 34fdd11e 47c00c11 fcd7b05e 1cc31311 8cab7ffd b87cca35 51d24d5e 732c5c43 e37369d0 c87fb776 672f8ced c7d655a3 119d0939 7599310d 196c7089 f76ddd4f" }, "hidden-boss_9-pair": { "about": "假想敌:铳 pair pool 5600000: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b chess_char_6_09_b chess_char_6_11_b token_10017_skadi2_dedant token_10030_mlyss_wtrman | chess_char_6_15_b chess_char_6_19_b chess_char_5_01_b chess_char_6_17_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b chess_char_6_18_b chess_char_6_20_b", @@ -1111,106 +1111,106 @@ "killed": 8, "total": 8, "unspawned": 0, - "bossHpLeft": 3570058, - "rngDraws": 1579, + "bossHpLeft": 3747749, + "rngDraws": 1619, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1989162,"bossDmg":803960,"heal":23383,"deaths":4,"layerGains":{}}, - "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":3240431,"bossDmg":1176195,"heal":126751,"deaths":10,"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",5343.08,1,15,0,1906,10482,12401,28,3], - ["p1",2,"op","chess_char_6_06_b",2,"uniequip_002_pepe",2910.14,1,11,0,19692,205,0,21,1], - ["p1",3,"op","chess_char_6_07_b",2,"uniequip_002_siege2",1761.93,1,17,0,50071,1601,0,31,2], - ["p1",4,"op","chess_char_6_01_b",2,"uniequip_002_lemuen",0,0,7,2,1548110,3561,0,55,8], - ["p1",5,"op","chess_char_6_02_b",2,"uniequip_002_sbell2",1074.65,1,9,0,33675,10123,0,46,2], - ["p1",6,"op","chess_char_6_04_b",2,"uniequip_002_skadi2",463.12,1,55,0,25244,10860,954,0,4], - ["p1",7,"op","chess_char_6_05_b",2,"uniequip_002_pasngr",2451.4,1,13,1,90440,0,0,0,11], - ["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1668,1,19,1,42621,0,7847,77,2], - ["p1",9,"op","chess_char_6_09_b",0,"uniequip_002_cello",0,0,21,1,144190,13392,0,29,29], - ["p1",10,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",1893,1,63,1,24647,9390,349,66,2], + ["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,62,1,459553,38345,0,197,3], - ["p2",14,"op","chess_char_6_19_b",2,"uniequip_002_blkkgt",0,0,60,0,497307,16632,0,66,5], - ["p2",15,"op","chess_char_5_01_b",1,"uniequip_002_excu2",0,0,66,0,13781,7737,2820,42,2], - ["p2",16,"op","chess_char_6_17_b",2,"uniequip_002_nearl2",1302.59,1,61,0,512511,25088,0,68,3], - ["p2",17,"op","chess_char_6_12_b",1,"uniequip_002_rosmon",765.29,1,8,0,477111,13038,0,78,3], - ["p2",18,"op","chess_char_6_13_b",1,"uniequip_002_angel2",2215,1,14,0,969378,5435,3408,291,6], - ["p2",19,"op","chess_char_6_14_b",2,"uniequip_002_lumen",3038.4,1,20,0,0,0,24080,78,1], - ["p2",20,"op","chess_char_6_16_b",2,"uniequip_002_halo2",1439,1,10,0,98857,0,0,131,4], - ["p2",21,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",1695.1,1,59,1,183585,13999,0,2,5], - ["p2",22,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",1592.1,1,25,0,0,14000,96443,80,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,2280.4,0,47,0,6744,6000,0,5,null], - ["p1","~1","token","token_10030_mlyss_wtrman",null,null,1322,1,48,0,1766,2370,1514,30,1], - ["p1","~2","token","token_10030_mlyss_wtrman",null,null,1102.65,0,49,0,595,675,0,11,null], - ["p1","~3","token","token_10030_mlyss_wtrman",null,null,0,0,51,0,661,1322,0,10,null], - ["p1","~4","token","token_10030_mlyss_wtrman",null,null,0,0,53,0,794,1322,0,12,null], - ["p1","~1","token","token_10040_siege2_vlion",null,null,3730,0,54,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,21605,6600,0,12,null], - ["p1","~2","token","token_10017_skadi2_dedant",null,null,100,0,64,0,0,0,0,0,null], - ["p1","~2","token","token_10040_siege2_vlion",null,null,3926.59,0,65,0,0,97,0,0,null], - ["p1","~3","token","token_10017_skadi2_dedant",null,null,100,1,67,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,5992,141894], - "enemy_9007_acelem": [2,2,0,22960,309587], - "enemy_9017_achunt_2": [2,0,0,57558,2029942], - "enemy_9018_actrpa": [1,0,0,42972,765851], - "enemy_9019_actrpb": [1,0,0,26761,756394], - "enemy_9020_actrpc": [1,0,0,18223,1328263] + "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,65,6000,1653,1653,7414,13693,3721,26,37,100,94,326,46845,2814,18,13,0,1,27,0,18,0], - "events": {"atk":2008,"deploy":46,"die":37,"dmg":13203,"engage":24,"fx":2168,"heal":883,"skill":194,"spawn":52,"status":833}, + "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": 117, - "beam": 31, + "airstrike": 88, + "beam": 27, "blessing": 1, - "blink": 4, - "bombard": 68, - "bombardShell": 68, - "burst": 18, + "blink": 5, + "bombard": 72, + "bombardShell": 72, + "burst": 16, "camouflage": 1, - "copy": 2, + "copy": 3, "dash": 24, - "displace": 24, + "displace": 20, "dp": 2, - "drone": 275, - "droneLock": 15, - "drones": 5, - "extraAttack": 12, - "finale": 5, + "drone": 190, + "droneLock": 9, + "drones": 3, + "extraAttack": 7, + "finale": 4, "firewall": 3, "garrisonGrant": 1, - "lightning": 119, + "lightning": 117, "link": 1141, "lock": 72, "lure": 2, "phase": 11, - "radiantSword": 2, + "radiantSword": 3, "revive": 3, - "scorchBurst": 3, + "scorchBurst": 1, "shieldBreak": 9, - "slash": 55, - "snow": 25, - "split": 3, - "steal": 6, + "slash": 44, + "snow": 26, + "split": 2, + "steal": 5, "storm": 11, - "summon": 4, + "summon": 5, "sunBurst": 3, - "talent": 7, - "tide": 4, + "talent": 6, + "tide": 3, "wanted": 6, "zone": 6 }, - "snaps": "2a921421 d160972c 4e5b9396 7b13edd2 689bae2b f9a15be0 67150ecd 3a4286df 13119f08 b1b42c22 b5c0b449 f4bddab7 22e104fd fd2f838a b7964c3e 83e4b794 62994daa 16b9850d 3f3a5054 498cda94" + "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", @@ -1227,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", @@ -1372,36 +1370,36 @@ "time": 200, "ticks": 6000, "reason": "forced", - "killed": 11, + "killed": 10, "total": 5, "unspawned": 0, - "bossHpLeft": 7323878, - "rngDraws": 102, + "bossHpLeft": 7333760, + "rngDraws": 139, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":11,"total":5,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":430741,"bossDmg":276122,"heal":148595,"deaths":10,"layerGains":{}}}, + "players": {"p1":{"killed":10,"total":5,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":448196,"bossDmg":266240,"heal":121244,"deaths":16,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3123.26,1,29,5,45,3903,0,19,0], - ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",1092.31,1,24,0,36,24428,13312,31,6], - ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",0,0,28,0,140999,24118,1514,143,3], - ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",3600.6,1,31,0,13,10454,0,12,0], - ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",0,0,22,3,176153,57886,6009,118,2], - ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",1419,1,30,1,87204,1419,0,56,6], - ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",1584,1,25,0,0,20837,92999,101,2], - ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2178,1,2,2,26292,3463,0,38,4], - ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1671,1,23,0,0,33782,4919,0,1], - ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,3,0,0,2358,29841,56,9] + ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",0,0,29,6,69,7026,0,24,0], + ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",0,0,25,0,30,21486,9278,25,5], + ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",0,0,26,1,135024,23127,1337,137,3], + ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",0,0,34,1,24,20162,0,21,1], + ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",3905.8,1,35,1,190029,35166,2983,92,3], + ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,30,0,76234,2838,0,53,6], + ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",2920.8,1,24,0,0,20473,74171,95,2], + ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",343.79,1,31,1,46787,6392,468,62,4], + ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1671,1,23,0,0,26222,3713,0,2], + ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",0,0,3,0,0,7174,29294,42,11] ], "enemies": { - "enemy_1005_yokai_3": [1,0,1,11629,68252], + "enemy_1005_yokai_3": [1,0,1,18962,95601], "enemy_9010_acpupp": [2,2,0,19404,86026], - "enemy_9021_acduml_2": [1,0,0,6962,276122], - "enemy_9022_acdumm": [1,0,0,3379,0], - "enemy_9023_acdums": [10,9,0,138776,342] + "enemy_9021_acduml_2": [1,0,0,4772,266240], + "enemy_9022_acdumm": [1,0,0,2812,0], + "enemy_9023_acdums": [10,8,0,122384,328] }, - "hooks": [1,33,6000,650,650,2402,2852,7246,21,22,33,33,0,9568,105,16,15,1,1,65,0,27,2], - "events": {"atk":973,"deploy":18,"die":21,"dmg":2838,"engage":7,"fx":844,"heal":298,"leak":1,"skill":69,"spawn":25,"status":303}, - "fx": {"bandage":9,"beam":21,"block":14,"burst":27,"dodge":2,"ember":2,"explode":500,"featherArrow":6,"garrisonGrant":1,"healField":2,"jungleSoul":3,"mote":52,"phase":55,"redistribute":15,"reweave":1,"riposte":1,"rockslide":6,"sandChains":1,"sandChainsCharged":3,"shieldBreak":21,"slash":40,"spGift":2,"summon":8,"telegraph":2,"thorns":47,"truesilver":2,"undying":1}, - "snaps": "65ab7720 4d0f9bc6 b175a7ad decfc6d9 d1ad8895 b6add8db 8b187de4 1339864b 84136f5a 963ae966 244b6531 f0d249a1 55dc7a49 f44509fa 5ba1fdbd c65a6ccf 00d4edb3 c6669080 45cab92a 414911cc" + "hooks": [1,35,6000,629,629,2222,2669,4675,26,27,37,36,0,8979,144,19,15,1,1,63,0,28,5], + "events": {"atk":938,"deploy":20,"die":26,"dmg":2659,"engage":7,"fx":895,"heal":257,"leak":1,"skill":75,"spawn":25,"status":361}, + "fx": {"appear":2,"bandage":11,"beam":21,"block":26,"burst":28,"disappear":2,"dodge":5,"ember":2,"explode":497,"featherArrow":6,"flame":1,"garrisonGrant":1,"healField":2,"jungleSoul":3,"mote":48,"phase":56,"redistribute":29,"reweave":2,"riposte":3,"rockslide":5,"sandChains":1,"sandChainsCharged":3,"shieldBreak":17,"slash":51,"spGift":1,"summon":8,"telegraph":2,"thorns":58,"truesilver":3,"undying":1}, + "snaps": "65ab7720 4d0f9bc6 b175a7ad decfc6d9 d1ad8895 b6add8db d001bf54 dc66077c e9917391 7dcdab2e 165041d1 65a94aa3 35d72c2e 6ee3e9c7 a7174d70 93c52780 189aaca7 d0965cc7 aedfc5b2 1b3f4a01" }, "unite-1": { "about": "联防 1 helper(s), 11 leaked: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b token_10017_skadi2_dedant", diff --git a/test/golden/matches.json b/test/golden/matches.json index 8b714153..f070298a 100644 --- a/test/golden/matches.json +++ b/test/golden/matches.json @@ -1314,7 +1314,7 @@ "combat": "client", "boost": {"lp":400,"layers":300}, "setup": {"stageId":"act1autochess_m02","bossId":"boss_5","hiddenBossId":"boss_10","factions":["TIMES","ELEMENT","DOT"]}, - "end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":395,"bossPoolLeft":0,"virtualMs":113700}, + "end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":397,"bossPoolLeft":0,"virtualMs":113700}, "rng": {"setup":25,"shop":346,"waves":0,"draft":42,"bots":10350,"meta":3}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ @@ -1377,12 +1377,12 @@ "victory": true, "roundsPassed": 15, "eliminatedRound": null, - "lp": 395, + "lp": 397, "bandId": "band_paganini", "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 0, "reward": 425, - "stats": {"activatedLayers":2281,"bossDamage":4580000,"dmgDealt":6374743,"gold":175,"itemsEquipped":25,"kills":376,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":34}, + "stats": {"activatedLayers":2281,"bossDamage":4580000,"dmgDealt":6374751,"gold":175,"itemsEquipped":25,"kills":376,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":34}, "lineup": ["4_02_b@12,7[3_08_e_a+1_02_e_b]","6_01_a@12,8[3_03_e_a+3_02_e_a]","6_16_a@11,5","3_18_a@11,8[4_11_e_a+4_05_e_b]","4_10_a@10,4[1_05_e_b]","6_05_a@10,5","4_01_b@10,6[4_06_e_a+4_05_e_a]","1_08_b@9,9[4_10_e_a]"], "bonds": ["arcaneShip 2/389","deputShip 0/300","egirShip 0/300","emptyShip 0/300","indomShip 0/308","investShip 0/300","kazimierzShip 0/308","kjeragShip 0/300","lateranoShip 1/322","maniShip 0/300","miraShip 0/300","preciShip 1/302","raidShip 0/300","sargonShip 0/300","siracusaShip 1/345","skillfulShip 0/335","soloShip 1/307","steadShip 0/300","suntShip 1/300","swiftShip 1/316","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"] } @@ -1398,7 +1398,7 @@ "combat": "client", "boost": {"lp":400,"layers":300}, "setup": {"stageId":"act2autochess_m04","bossId":"boss_5","hiddenBossId":"boss_9","factions":["FLY","ELEMENT","INVISIBLE"]}, - "end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":800,"bossPoolLeft":0,"virtualMs":124650}, + "end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":698,"bossPoolLeft":0,"virtualMs":124650}, "rng": {"setup":25,"shop":750,"waves":0,"draft":61,"bots":22165,"meta":198}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ @@ -1474,12 +1474,12 @@ "victory": true, "roundsPassed": 15, "eliminatedRound": null, - "lp": 400, + "lp": 349, "bandId": "band_sarkazb", "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 8, "reward": 425, - "stats": {"activatedLayers":3346,"bossDamage":7735817,"dmgDealt":25492475,"gold":154,"itemsEquipped":21,"kills":285,"leaks":8,"lpLost":0,"merges":7,"perfectRounds":12,"refreshes":21}, + "stats": {"activatedLayers":3346,"bossDamage":8584552,"dmgDealt":21757593,"gold":154,"itemsEquipped":21,"kills":287,"leaks":8,"lpLost":0,"merges":7,"perfectRounds":12,"refreshes":21}, "lineup": ["4_09_b@11,5[3_11_e_a+3_11_e_b]","6_11_a@11,8","3_13_b@10,3","3_05_b@10,8[1_01_e_a+6_09_e_a]","6_16_a@9,6","6_05_a@9,7[3_02_e_a]","4_14_b@9,8[2_01_e_a+1_02_e_b]","5_07_a@9,9[4_10_e_b]"], "bonds": ["arcaneShip 1/365","deputShip 0/304","egirShip 1/364","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 0/308","lateranoShip 0/300","maniShip 1/300","miraShip 0/300","preciShip 1/345","raidShip 1/405","sargonShip 1/300","siracusaShip 0/300","skillfulShip 1/338","soloShip 1/307","steadShip 0/338","suntShip 1/300","swiftShip 1/322","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"] }, @@ -1488,12 +1488,12 @@ "victory": true, "roundsPassed": 15, "eliminatedRound": null, - "lp": 400, + "lp": 349, "bandId": "band_doberm", "title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"}, "trophies": 8, "reward": 425, - "stats": {"activatedLayers":2801,"bossDamage":6160343,"dmgDealt":21332968,"gold":191,"itemsEquipped":32,"kills":287,"leaks":1,"lpLost":0,"merges":5,"perfectRounds":12,"refreshes":55}, + "stats": {"activatedLayers":2801,"bossDamage":5304736,"dmgDealt":21479720,"gold":191,"itemsEquipped":32,"kills":287,"leaks":1,"lpLost":0,"merges":5,"perfectRounds":12,"refreshes":55}, "lineup": ["4_10_a@11,5[3_05_e_a+4_03_e_a]","6_05_a@11,8[4_08_e_a+6_09_e_a]","6_16_a@10,3[3_11_e_a+1_05_e_b]","2_13_a@10,7[4_04_e_a+4_05_e_a]","1_08_b@10,8[4_07_e_b+5_07_e_a]","6_08_a@9,6[2_01_e_b+3_01_e_b]","5_20_a@9,7[1_05_e_a+3_04_e_a]","6_08_a@9,8[3_03_e_a+1_01_e_a]","3_19_b@9,9[2_04_e_a+3_07_e_b]"], "bonds": ["arcaneShip 1/371","deputShip 0/332","egirShip 0/300","emptyShip 0/308","indomShip 0/300","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 1/334","preciShip 1/311","raidShip 0/300","sargonShip 0/300","siracusaShip 1/465","skillfulShip 1/356","soloShip 1/322","steadShip 0/312","suntShip 1/308","swiftShip 1/334","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"] } 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) { diff --git a/test/sim/pathing-blockable.test.js b/test/sim/pathing-blockable.test.js index 90cb6f52..3b814e2c 100644 --- a/test/sim/pathing-blockable.test.js +++ b/test/sim/pathing-blockable.test.js @@ -1,7 +1,8 @@ // Blockable-ground preference of the ground pathing (grid.js header; user playtest #2: on 战场#01 the lower-gate enemies // walked up the col-9 floor lane, where no operator can stand, instead of the col-8 road). The flow field keeps the // official route unless the preference route — the fewest non-blockable tiles among equal-length chains, smoothing -// that never cuts across floor its grid route does not walk — crosses fewer non-blockable tiles. "Crosses" = the +// that never cuts across floor its grid route does not walk — crosses fewer non-blockable tiles and no more 深水区 +// (GitHub #375). "Crosses" = the // segment passes through the tile's interior (grid.js segmentTiles; brushing a corner is not crossing — the D5 report // after 0.1.0, test/sim/pathing-official.test.js). Audit: every active stage × gate × field (normal, 联防 both halves, // boss both halves incl. the solo `_s` templates) — the non-blockable tiles an enemy still crosses are listed and @@ -69,27 +70,36 @@ function nbCrossed(g, s, e) { /** * Fewest non-blockable tiles (start and goal excluded) over all 4-connected crate-free routes s → e of length ≤ the - * shortest + `slack`; { best, min } (min = the minimum over those lengths). + * shortest + `slack` that cross at most `maxWet` 深水区 tiles (grid.js: the tie-break never walks into more 深水区 than + * the official route — GitHub #375); { best, min } (min = the minimum over those lengths). */ -function fewestNb(g, s, e, slack = 2) { +function fewestNb(g, s, e, slack = 2, maxWet = Infinity) { const f = g.flowField(e[0], e[1]); const best = f.dist[s[0] * COLS + s[1]]; if (best < 0 || best >= 1000) return null; const ek = e[0] * COLS + e[1]; const cost = (k) => (k === ek ? 0 : g.unblockable[k]); - let cur = new Map([[s[0] * COLS + s[1], 0]]); + const wetOf = Number.isFinite(maxWet) ? (k) => (k === ek ? 0 : g.deepWater[k]) : () => 0; + // tile → (深水区 so far → fewest non-blockable so far) + let cur = new Map([[s[0] * COLS + s[1], new Map([[0, 0]])]]); let min = Infinity; for (let L = 0; L <= best + slack; L++) { - if (cur.has(ek)) min = Math.min(min, cur.get(ek)); + if (cur.has(ek)) for (const p of cur.get(ek).values()) min = Math.min(min, p); const nx = new Map(); - for (const [k, p] of cur) { + for (const [k, byWet] of cur) { if (k === ek) continue; const r = (k / COLS) | 0, c = k % COLS; for (const [dr, dc] of [[1, 0], [0, 1], [-1, 0], [0, -1]]) { const nr = r + dr, nc = c + dc; if (!g.walkable(nr, nc) || g.isCrate(nr, nc)) continue; - const nk = nr * COLS + nc, np = p + cost(nk); - if (!nx.has(nk) || nx.get(nk) > np) nx.set(nk, np); + const nk = nr * COLS + nc; + let m = nx.get(nk); + for (const [w, p] of byWet) { + const nw = w + wetOf(nk), np = p + cost(nk); + if (nw > maxWet) continue; + if (!m) nx.set(nk, m = new Map()); + if (!m.has(nw) || m.get(nw) > np) m.set(nw, np); + } } } cur = nx; @@ -97,6 +107,14 @@ function fewestNb(g, s, e, slack = 2) { return { best, min }; } +/** 深水区 tiles crossed from s to e (start and goal excluded). */ +function wetCrossed(g, s, e) { + const wp = g.waypoints(s[0], s[1], e[0], e[1]); + const seen = new Set(); + for (const [r, c] of crossed(wp)) if (!(r === s[0] && c === s[1]) && !(r === e[0] && c === e[1]) && g.deepWater[r * COLS + c]) seen.add(`${r},${c}`); + return seen.size; +} + /** Tiles an enemy of `route` visits (rounded position per tick) and whether `u` blocked it. */ function walk(h) { const seen = []; @@ -293,12 +311,15 @@ test('audit: every WALK leg of every wave template (normal / 联防 / boss / hid if (!grids.has(key)) grids.set(key, stageGrid(sid, rect)); const g = grids.get(key); if (!g.walkable(from[0], from[1]) || !g.walkable(to[0], to[1])) continue; - const eq = fewestNb(g, from, to, 0); - if (!eq) continue; + if (!fewestNb(g, from, to, 0)) continue; const got = nbCrossed(g, from, to); + // compared with the routes that wade through no more 深水区 than ours (#375: 战场#08(下半)'s patrol legs to (2,3) / + // (2,17) keep the official upper road, not the lower one through two water tiles that crosses one floor tile less) + const wet = wetCrossed(g, from, to); + const eq = fewestNb(g, from, to, 0, wet); const what = `${sid} ${id} (${from}) → (${to}) crosses ${got.join(' ')}`; assert.ok(got.length <= eq.min, `${what}, an equal-length route only ${eq.min}`); - const near = fewestNb(g, from, to, 2); + const near = fewestNb(g, from, to, 2, wet); if (near.min < got.length) { assert.ok(rect === GEO.BOSS_RECT && got.every(arena), `${what}: a route of length ≤ ${near.best}+2 crosses only ${near.min}`); accepted.push(what); diff --git a/test/sim/pathing-crosscheck.test.js b/test/sim/pathing-crosscheck.test.js index 7325ca59..520f1c44 100644 --- a/test/sim/pathing-crosscheck.test.js +++ b/test/sim/pathing-crosscheck.test.js @@ -5,8 +5,9 @@ // preference field = 0.1.0's — equal-length ties to the chain with the fewest non-blockable tiles, a line of sight that // covers non-blockable tiles, diagonal-step corners included, only on the own raw chain — whose pointer replaces the // official one where its route crosses fewer non-blockable tiles, or as few while only skipping the official waypoint -// (on the straight line to it, leading there); "crosses" = positive-length intersection with the tile, found here by -// clipping the segment against each tile, independently of grid.js crossTiles): +// (on the straight line to it, leading there), and no more 深水区 than the official route (GitHub #375); "crosses" = +// positive-length intersection with the tile, found here by clipping the segment against each tile, independently of +// grid.js crossTiles): // * the 8 active stages with their match-start devices: the smoothed chain from EVERY walkable tile of the normal, // 联防 and boss rects to every goal equals the full-map reference one (the sim's rect limit changes nothing); // * 400 random crate / block layouts: identical chains to the same algorithm limited to the rect; @@ -56,6 +57,8 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec if (t.tileKey === 'tile_deepsea') return true; return !(t.heightType === 'LOWLAND' && (t.buildableType === 'ALL' || t.buildableType === 'MELEE')); }; + /** 深水区 — the preference may not cross more of it than the official route (#375) */ + const deep = (r, c) => ['tile_deepsea', 'tile_deepwater'].includes(tile(r, c).tileKey); const bres = ([r0, c0], [r1, c1], clear) => { const dr = Math.abs(r1 - r0), dc = Math.abs(c1 - c0), sr = r1 > r0 ? 1 : -1, sc = c1 > c0 ? 1 : -1; let err = dc - dr, r = r0, c = c0; @@ -140,12 +143,15 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec const off = spfa(false); const dist = off.dist; const segNb = (a, b) => through(a, b).filter(([y, x]) => !(y === a[0] && x === a[1]) && nb(y, x)).length; + const segWet = (a, b) => through(a, b).filter(([y, x]) => !(y === a[0] && x === a[1]) && deep(y, x)).length; let nxt = smooth(off, false); if (prefer) { // per tile in increasing distance: the preference pointer when its route crosses fewer non-blockable tiles (or as // few while it only skips the official waypoint: o strictly inside the segment a → p, and o's chosen pointer is p) + // and no more 深水区 than the official pointer's route const nxtP = smooth(spfa(true), true); const cost = new Map([[D, 0]]); + const wet = new Map([[D, 0]]); const chosen = new Map(); const skips = (a, o, p) => { const t = through(a, p); @@ -155,9 +161,11 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec if (k === D) continue; const a = unkey(k), o = nxt.get(k), p = nxtP.get(k); const co = segNb(a, o) + cost.get(key(o)), cp = segNb(a, p) + cost.get(key(p)); - const useP = cp < co || (cp === co && key(o) !== key(p) && key(chosen.get(key(o)) ?? []) === key(p) && skips(a, o, p)); + const wo = segWet(a, o) + wet.get(key(o)), wp = segWet(a, p) + wet.get(key(p)); + const useP = wp <= wo && (cp < co || (cp === co && key(o) !== key(p) && key(chosen.get(key(o)) ?? []) === key(p) && skips(a, o, p))); chosen.set(k, useP ? p : o); cost.set(k, useP ? cp : co); + wet.set(k, useP ? wp : wo); } nxt = chosen; } diff --git a/test/sim/pathing-official.test.js b/test/sim/pathing-official.test.js index 9bf0c881..aefc032d 100644 --- a/test/sim/pathing-official.test.js +++ b/test/sim/pathing-official.test.js @@ -1,7 +1,8 @@ // Official route shapes (community report after 0.1.0, D5: on 战场#04 活性源石 the lower-gate enemies walked two tiles // straight left of the red gate and then straight up, instead of crossing diagonally from the 4th row to the 3rd as in // the official game). grid.js keeps the official flow field (research 08 §3.1) unless the blockable-ground preference -// route of user playtest #2 (0.1.0's) crosses strictly fewer non-blockable tiles (floor, gates) — a segment that only +// route of user playtest #2 (0.1.0's) crosses strictly fewer non-blockable tiles (floor, gates) and no more 深水区 +// (GitHub #375) — a segment that only // brushes a floor tile's corner does not cross it. Audit: every stage (active or not) × gate × field against the pure // official algorithm; the routes that still differ are listed, each avoiding floor the official route walks over. import { test } from 'node:test'; @@ -127,6 +128,43 @@ test('audit: every route of every stage (11, active or not) × gate × field is assert.equal(official, stages.length * legs.length - seen.size); }); +// GitHub #375 (FrogThai: 「水图boss会从下面这条路往蓝门走,实际应该从上路走」): on 战场#08(下半) 涨潮控制 the patrolling leaders +// (铳 — level_act1autochess_h07_02 route 6 —, 卢西恩 — act2autochess_h07_05 route 8: PATROL_MOVE (3,9) → (2,3) → (5,9)) +// took the lower road (3,9) → (2,3) through the two 深水区 tiles (3,6) / (4,6) — one floor tile fewer than the official +// upper road along row 5, which crosses one water tile. The tie-break may not wade into more 深水区 than the official +// route (grid.js), so these legs are official again. +test('#375 战场#08(下半): the (3,9) → (2,3) patrol leg stays on the official upper road', REAL, () => { + const sid = 'act2autochess_m04'; + const g = stageGrid(sid, GEO.BOSS_RECT), pure = stageGrid(sid, GEO.BOSS_RECT, true); + for (const [s, e, wp] of [ + [[3, 9], [2, 3], '3,9 5,9 5,4 2,4 2,3'], + [[3, 11], [2, 17], '3,11 5,11 5,16 2,16 2,17'], + [[3, 10], [2, 3], '3,10 5,9 5,4 2,4 2,3'], + [[4, 10], [3, 9], '4,10 3,9'], + ]) { + assert.deepEqual(g.waypoints(s[0], s[1], e[0], e[1]), pure.waypoints(s[0], s[1], e[0], e[1]), `(${s}) → (${e})`); + assert.equal(str(g.waypoints(s[0], s[1], e[0], e[1])), wp); + } +}); + +test('#375 real battle (act2 m04, 铳 on act1autochess_h07_02): the patrol walks row 5 to (2,3) and steps on one 深水区 only', REAL, () => { + const h = makeBattle({ kind: 'boss', stageId: 'act2autochess_m04', waveTemplate: 'act1autochess_h07_02', units: [], autoFinish: false, timeLimit: 120, + sharedBoss: { hp: 1e12, maxHp: 1e12, damage(pid, n) { this.hp -= n; } } }); + const rows = getDefaultSource().getStage('act2autochess_m04').raw.rows; + const tiles = []; + h.b.on('tick', () => { + const e = h.b.enemies.find((u) => u.alive && u.defId === 'enemy_9017_achunt'); + if (!e) return; + const k = `${Math.round(e.y)},${Math.round(e.x)}`; + if (tiles[tiles.length - 1] !== k) tiles.push(k); + }); + assert.ok(h.runUntil(() => tiles.includes('2,4'), 60), `reaches (2,4): ${tiles.join(' ')}`); + const upTo = tiles.slice(0, tiles.indexOf('2,4') + 1); + assert.deepEqual(upTo.slice(0, 4), ['4,10', '3,9', '4,9', '5,9'], `up to row 5 first: ${upTo.join(' ')}`); + const water = upTo.filter((k) => { const [r, c] = k.split(',').map(Number); return rows[r][c] === 'd'; }); + assert.deepEqual(water, ['5,6'], `深水区 stepped on: ${water.join(' ')} (route ${upTo.join(' ')})`); +}); + test('D5: an operator on (9,9), (9,8) or (10,8) blocks the lower-gate enemy on the official diagonal', REAL, () => { const guard = chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 0, blockCnt: 3, maxHp: 1e6 }, skill: null }); const walker = enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1 }); diff --git a/test/sim/portal-appear.test.js b/test/sim/portal-appear.test.js new file mode 100644 index 00000000..ed62b8ee --- /dev/null +++ b/test/sim/portal-appear.test.js @@ -0,0 +1,137 @@ +// The 传送门 (portal) of the boss / hidden-core templates (GitHub #336, PR #337 by @2321Robin): an enemy flies or walks +// into the portal and reappears on the far side. Four official routes spell the crossing as DISAPPEAR → WAIT → MOVE-to-the-exit with no APPEAR step +// (act1autochess_h07_02 route #4, act1autochess_h08_02 routes #5/#9, act2autochess_h07_05 route #6), while their twin +// routes in the same templates write it out as DISAPPEAR → WAIT → APPEAR[exit]. Taken literally, an appear-less route +// made the enemy walk its whole remaining route invisible and then leak unseen — the blue door takes the leak and the +// team never sees who or how (user report: 打 boss 时小怪进传送门后被判定进蓝门,全程隐身). normalizeRoute re-inserts +// the exit APPEAR ahead of that MOVE — at the MOVE's own target, which is exactly where the twins appear. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { normalizeRoute, hasGeneratedData } from '../../server/sim/simdata.js'; +import { makeBattle, enemyRec } from '../helpers/battleHarness.js'; + +const REAL = { skip: !hasGeneratedData() && 'no generated data' }; + +/** Leg signature of a normalized route: `MOVE[r,c]` / `APPEAR[r,c]` / `DISAPPEAR` / `WAIT`. */ +const sig = (r) => r.checkpoints.map((c) => c.type + (c.type === 'MOVE' || c.type === 'APPEAR' ? `[${c.pos[0]},${c.pos[1]}]` : '')); +/** The test enemy's leaks (the templates' own spawns leak too). */ +const leaksOf = (h) => h.hooksOf('enemyLeak').filter((l) => l.enemy.defId === 'enemy_test_portal'); +/** The test enemy's hidden spans: [{ from: [r,c], to: [r,c], dt }] (vanish tile, reappear tile, seconds hidden). */ +function hiddenSpans(h) { + const spans = []; + let open = null; + const set = h.b._setHidden.bind(h.b); + h.b._setHidden = (e, on) => { + if (e.defId === 'enemy_test_portal' && on !== !!e.hidden) { + const at = [Math.round(e.y), Math.round(e.x)]; + if (on) open = { at, t: h.b.time }; + else if (open) { spans.push({ from: open.at, to: at, dt: h.b.time - open.t }); open = null; } + } + return set(e, on); + }; + return spans; +} + +test('normalizeRoute: DISAPPEAR → WAIT → MOVE gains the twin APPEAR at the move target', () => { + const r = normalizeRoute({ + motion: 'FLY', start: [2, 10], end: [1, 3], + steps: [ + { t: 'move', p: [2, 11] }, { t: 'move', p: [1, 17] }, { t: 'disappear' }, { t: 'wait', s: 3 }, + { t: 'move', p: [5, 10] }, { t: 'move', p: [5, 9] }, + ], + }); + assert.deepEqual(sig(r), ['MOVE[2,11]', 'MOVE[1,17]', 'DISAPPEAR', 'WAIT', 'APPEAR[5,10]', 'MOVE[5,10]', 'MOVE[5,9]']); +}); + +test('normalizeRoute: a DISAPPEAR the data does spell out with an APPEAR is kept as the only one', () => { + const r = normalizeRoute({ + motion: 'FLY', start: [2, 10], end: [1, 17], + steps: [ + { t: 'move', p: [1, 3] }, { t: 'disappear' }, { t: 'wait', s: 3 }, { t: 'appear', p: [5, 10] }, { t: 'move', p: [5, 13] }, + ], + }); + assert.equal(r.checkpoints.filter((c) => c.type === 'APPEAR').length, 1); + assert.deepEqual(sig(r), ['MOVE[1,3]', 'DISAPPEAR', 'WAIT', 'APPEAR[5,10]', 'MOVE[5,13]']); +}); + +test('normalizeRoute: idempotent — a second pass over a fixed route changes nothing', () => { + const once = normalizeRoute({ + motion: 'FLY', start: [2, 10], end: [1, 3], + steps: [{ t: 'move', p: [1, 17] }, { t: 'disappear' }, { t: 'wait', s: 3 }, { t: 'move', p: [5, 10] }], + }); + assert.deepEqual(normalizeRoute(once), once); +}); + +// The four official appear-less routes on their real templates: after the portal the enemy must be back in the open +// at the exit [5,10] (where the twins appear) and the route's leak at the blue door must happen VISIBLE. +const PORTAL_EXIT = [5, 10]; +const CASES = [ + ['act1autochess_h07_02', 4, 'FLY'], + ['act1autochess_h08_02', 5, 'WALK'], + ['act1autochess_h08_02', 9, 'WALK'], + ['act2autochess_h07_05', 6, 'FLY'], +]; + +for (const [tplId, routeIdx, motion] of CASES) { + test(`${tplId} route #${routeIdx}: portal crossing reappears at [${PORTAL_EXIT}] and leaks visible`, REAL, () => { + const h = makeBattle({ + kind: 'boss', waveTemplate: tplId, content: 'none', + defs: { enemies: { enemy_test_portal: enemyRec({ key: 'enemy_test_portal', hp: 1e6, speed: 3, motion }) } }, + enemies: [{ key: 'enemy_test_portal', route: routeIdx }], + autoFinish: true, timeLimit: 400, hooks: ['enemyLeak'], + }); + const e = () => h.enemy('enemy_test_portal'); + assert.ok(h.runUntil(() => e() && e().hidden, 400), 'reaches the portal (disappears)'); + assert.ok(h.runUntil(() => e() && !e().hidden, 400), 'reappears after the portal'); + assert.deepEqual([Math.round(e().y), Math.round(e().x)], PORTAL_EXIT, 'reappears at the portal exit'); + h.runToEnd(400); + const [leak, ...more] = leaksOf(h); + assert.ok(leak, 'the route still ends in its leak'); + assert.equal(more.length, 0, 'one leak'); + assert.equal(leak.enemy.hidden, false, 'the leak is visible'); + }); +} + +// A route that ENDS on a portal entrance does not leak there: the entrance teleports the enemy out of the far exit +// ([5,10] — the same pairing every explicit DISAPPEAR / APPEAR pair in the data reads) and it walks on to the blue +// door on that side of the field, where the leak happens (GitHub #336: 打 boss 小怪第一次进传送门就被判定进蓝门, +// 官方是传送后走向蓝门). The solo boss routes are exactly this shape: straight from the centre into the corner portal. +// Inside the portal it stays hidden for 3 s, as the explicit crossings of the same tiles (WAIT 3 in 85 of the 95). +test('solo boss: a route ending on the left portal entrance leaks at the left blue door, not the portal', REAL, () => { + const h = makeBattle({ + kind: 'boss', stageId: 'act1autochess_m01', rect: { r0: 0, r1: 5, c0: 0, c1: 20 }, + waveTemplate: 'act1autochess_h07_02_s', content: 'none', + defs: { enemies: { enemy_test_portal: enemyRec({ key: 'enemy_test_portal', hp: 1e6, speed: 3 }) } }, + enemies: [{ key: 'enemy_test_portal', route: 0 }], + autoFinish: true, timeLimit: 300, hooks: ['enemyLeak'], + }); + const spans = hiddenSpans(h); + const e = () => h.enemy('enemy_test_portal'); + assert.ok(h.runUntil(() => e() && Math.round(e().y) === 1 && Math.round(e().x) === 3, 300), 'reaches the portal entrance [1,3]'); + assert.ok(h.runUntil(() => e() && Math.round(e().y) === 5 && Math.round(e().x) === 10, 300), 'teleports out of the exit [5,10]'); + h.runToEnd(400); + const [leak, ...more] = leaksOf(h); + assert.ok(leak, 'leaks eventually'); + assert.deepEqual([Math.round(leak.enemy.y), Math.round(leak.enemy.x)], [2, 2], 'the leak is at the left blue door [2,2]'); + assert.equal(more.length, 0, 'stepping into the portal entrance is no leak of its own'); + assert.equal(spans.length, 1, 'one portal crossing'); + assert.deepEqual([spans[0].from, spans[0].to], [[1, 3], [5, 10]], 'hidden from the entrance to the exit'); + assert.ok(Math.abs(spans[0].dt - 3) < h.TICK * 1.5, `hidden 3 s inside the portal (${spans[0].dt.toFixed(3)} s)`); +}); + +test('a route ending on the right portal entrance mirrors: leak at the right blue door', REAL, () => { + const h = makeBattle({ + kind: 'boss', stageId: 'act1autochess_m01', rect: { r0: 0, r1: 5, c0: 0, c1: 20 }, content: 'none', + defs: { enemies: { enemy_test_portal: enemyRec({ key: 'enemy_test_portal', hp: 1e6, speed: 3 }) } }, + enemies: [{ key: 'enemy_test_portal', route: { motion: 'WALK', start: [2, 10], end: [1, 17], checkpoints: [] } }], + autoFinish: true, timeLimit: 300, hooks: ['enemyLeak'], + }); + const spans = hiddenSpans(h); + h.runToEnd(400); + const [leak, ...more] = leaksOf(h); + assert.ok(leak, 'leaks eventually'); + assert.deepEqual([Math.round(leak.enemy.y), Math.round(leak.enemy.x)], [2, 18], 'the leak is at the right blue door [2,18]'); + assert.equal(more.length, 0, 'stepping into the portal entrance is no leak of its own'); + assert.deepEqual(spans.map((x) => [x.from, x.to]), [[[1, 17], [5, 10]]], 'one crossing, entrance to exit'); + assert.ok(Math.abs(spans[0].dt - 3) < h.TICK * 1.5, `hidden 3 s inside the portal (${spans[0].dt.toFixed(3)} s)`); +});