feedback7 UIBACK: the top-bar capsule counts like the official — resolved = killed + leaked among the enemies the round scheduled (opening 0/3, a leak 1/3, a kill 2/3, the splitter 3/3); a split child, a summon and a boss summon are in neither number but still cost LP and break 完美作战 (PR #308 by @xcdoge, the capsule half of PR #157 — authored by them: the sim / protocol / HUD hunks applied as reviewed): inTotal / killedInTotal / leakedInTotal / resolved, b.snap / BattleResult / b.progress (optional field) / m.public progress carry resolved, killed may exceed total (validation bounds it by the content-spawn allowance); dropped the PR's stray design-doc line — it had replaced the S→C b.end bullet of docs/design/network.md, which is back; the 联防 live-counter change mark follows the enemy list now that a spawn no longer moves total (uniteLeftMark + test); golden: only total / players.*.total / snaps move, outcomes bit-identical — roster 23 (roster-002 003 007 008 009 010 011 012 013 015 017 018 020 023 024 025 026 027 037 045 047 048 049), bonds 2 (bond-maniShip-low, bond-soloShip-high), fields 11 (boss-boss_1 3 5 pair+solo, boss-boss_6-solo, hidden-boss_8 10 pair+solo), diy 11 (diy-036 040 056 062 068 073 083 091 095 114 115), matches 12 (solo-FUNNY-1 NORMAL-1 HARD-1 ABYSS-1 ABYSS-2, coop2-NORMAL-3 coop2-HARD-4 coop3-ABYSS-5 coop4-ABYSS-7 coop2-NORMAL-8-serverrun solo-HARD-9-boosted coop2-NORMAL-70-diy: the total column of 36 round rows), standins unchanged

This commit is contained in:
xcdoge authored and sganggs committed 2026-10-08 19:30:36 +08:00
1 parent d7c665dbf6
commit b4bc89ad4e
39 files changed
+836 -252

No files matched your search

+1 -1
View File
@@ -22,7 +22,7 @@
| 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消;点「准备就绪」时商店自动收起、棋盘放大到全场,取消准备时商店再展开(资金旁的「收起」和商店标签「展开商店」随时可以手动切换)。 |
- **特殊地形点一下就有说明**:战场上的**活性源石**、**沼泽**、**排气格栅**、**深水区**,以及**红门 / 蓝门 / 传送点**,点一下就会弹出它自己的说明卡——名称、机制,以及**这一关自己的数值**(比如战场#04 的活性源石:每秒 70 点真实伤害、攻击力 +20%、攻击速度 +20、持续 300 秒)。休整期和作战期都能点。普通地板 / 道路 / 高台点了不会弹东西,所以点一下仍然只是「取消选中、关掉卡片」。
- **场地装置点一下也有说明**:**阻隔工事**(挡路:地面敌人绕着走,无路可走才撞碎它;作战中说明卡上有它的实时血量,被摧毁后点不出来)、**“双眼皮”炮台**(自动法术攻击、命中附加脆弱,按当前层数最高的盟约强化;还没被机械援助或机变卡启用的炮台不显示、点不出来)、**射击台**(远程位可部署的高台位)和**源石流发生装置**(吹风方向,以及对干员攻击力、敌人移动速度的加减)点一下同样弹出说明卡,数值都取自所在关卡的装置数据,装置名称按当前语言显示官方译名;被机变卡移除的箱子同样不显示。休整期和作战期都能点(GitHub #228,PR #229,感谢 @rickylxw)。
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样;唯一的例外是乌尔比安:三技能把他移到船锚处期间被击倒时,他回到自己的部署位躺下、再部署——这是项目维护者 2026-10-07 应社区反馈有意做出的改动,与官方「原地倒下」不同);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。顶栏中间的胶囊是官方的「已解决 / 本回合敌人数」:打死一个、漏掉一个都算已解决(开局 0/3,漏一个 1/3,打死一个 2/3,打死会分裂的那只 3/3);分裂出来的小怪、召唤物和领袖召唤的敌人不进分子也不进分母,但它们漏掉时仍然各自扣目标生命值、也会破坏完美作战(4 只磨砻全被打死、分裂出的 8 只全漏进蓝门:胶囊 4/4,扣 8 点),所以会召唤的领袖关胶囊也能填满。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样;唯一的例外是乌尔比安:三技能把他移到船锚处期间被击倒时,他回到自己的部署位躺下、再部署——这是项目维护者 2026-10-07 应社区反馈有意做出的改动,与官方「原地倒下」不同);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
| 联防 | 见第 5 节。 |
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。结算开始时屏幕正中跳出官方的「**作战结束**」框(约 3 秒后自己消失,不挡操作),下面一行只写**你自己**这一回合被扣的目标生命值:没扣是「**全员无伤!**」,扣了是「**生命值减少 −N**」;同时播官方的战斗结束音效(扣了、联防里没扣、普通回合没扣各一种)。联防回合见第 5 节。最终攻势和隐秘核心没有这一框(之后直接是结算称号或隐秘核心);已淘汰的玩家和观战者不弹框。 |
+25 -4
View File
@@ -184,6 +184,9 @@ by a `battleStart` handler covers it like a later knock-out (机变 征召 −50
`SpawnSpec = { time, enemyKey, routeIndex, count=1, interval=0, mods:{hpMul,atkMul,defMul,resMul,speedMul}, sourcePlayerId?,
bounty?:{coins, ownerPlayerId}, tag?:'boss'|'part'|'escort'|'bounty', ownerPlayerId?, pos?:[r,c], route?:RouteSpec, countInTotal? }`.
A spec entry the battle queues (`_queueSpawn`) is `inTotal` when it counts (`countInTotal`, `tag`, data `notCountInTotal`):
that is the HUD capsule's own set (§1.3); everything content spawns at runtime (a split, a summon — a boss's included —,
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`
@@ -410,13 +413,29 @@ recorded for the player whose half contains the goal it reached, with `sourcePla
### 1.3 BattleResult
```js
{ time, reason: 'cleared'|'timeout'|'forced', killed, total, errors, unspawned?: [{enemyKey,time,tag}], bossHpLeft?,
perPlayer: { [playerId]: { killed, total, perfect,
{ time, reason: 'cleared'|'timeout'|'forced', killed, total, resolved, errors, unspawned?: [{enemyKey,time,tag}], bossHpLeft?,
perPlayer: { [playerId]: { killed, total, resolved, killedInTotal, leakedInTotal, perfect,
leaked: [{ enemyKey, mods, lpr, sourcePlayerId, tag, counted, boss?, spawned }],
layerGains: {bondId: n}, coins, damageDealt, bossDamage, healingDone, deaths,
unitsEnd: [{ uid, id, defId, hpPct, sp, skillActive, alive }], // operators + board summon pieces; sp = skill.spTotal
unitStats: [{ id, uid, defId, name, kind, dmg, kills, heal, taken, attacks }] } } }
```
- **Two sets of counters, and they never cross.** `counted` drives the official LP / 完美作战 / `killed` reading (master's,
untouched): every counted enemy — a runtime split child, a summon, a boss's summon included — is in it. `inTotal` (the
new flag, `spawns.js`) is the HUD capsule's own set (DESIGN §14 顶栏胶囊): only the enemies the field itself scheduled
(`_queueSpawn`, and only those that `counted` — `countInTotal: false`, `tag: 'boss'` / `'part'`, data `notCountInTotal`
stay out) are `inTotal`. Everything a runtime `spawnEnemy` makes defaults to `inTotal: false` (an explicit
`inTotal: true` is the content back door); a 变身 / 重生 (`setForm`, a revive) is the same enemy and keeps its flag.
- `total` counts the `inTotal` enemies (the capsule's denominator), `killedInTotal` / `leakedInTotal` (battle level and per
player) count those that were knocked down or leaked, and `resolved = min(total, killedInTotal + leakedInTotal)` is the
capsule's numerator (official: 开局 0/3 → 漏一个 1/3 → 打死一个 2/3 → 打死会分裂的 3/3). A leaked split child or summon
moves none of them, while `leakedCount` / `leaked[]` still charge its LP and break 完美作战. The per-player numbers are
attributed like their `counted` twins — a knock-out to the enemy's owner (as `total`), a leak to the player whose half it
reached (`_recordLeak`) — so in a 联防 field a per-player sum can pass that player's `total` and `resolved` clamps it
(the battle-level numbers stay exact).
- `killed` therefore may **exceed** `total` (it counts runtime children, they do not): 4 磨砻 killed plus one of their 8
木制瑞印 reads `killed` 5 with `total` 4 and a capsule of 4/4. `spec.js compactResult` and the match's validation bound
`killed` by the spec's content-spawn allowance (`4·spawns + 100`) instead of by `total`.
- `cleared`: no spawns pending and no enemy alive (or the shared boss pool reached 0 — a pool never holds less than
`BOSS_POOL_MIN_HP` (1): the hit that would leave less takes the rest and the browser's `LocalBossPool` reads less as
0, so float dust of the pool sync can never keep a leader alive; user playtest #6). A boss/hidden battle **with a
@@ -833,7 +852,7 @@ instance) and skip `'counter'` / `'reflect'` damage. When the guard trips, the l
| `addBuff(unit, buff)`, `removeBuff(unit, key)` | §3 |
| `spawnToken(ownerUnit | playerId, tokenId, row, col, { def, stats, hp, duration, untargetable, dir, kit, force, anySource })` | field tiles; def from data/tokens.json `variants[ownerChessId]` for the owner unit's selected skill / module (`tokenDef`); `dir` defaults to the owner unit's (else the player's: RIGHT, mirrored side LEFT; a legacy `facing` ±1 is still read); returns the token or null (tile busy; or the owner runs a **non-default** skill that does not produce the token — `producesToken` — unless `anySource`: kit install hooks written for the default skill run under every skill). `spawnDevice(key, row, col, { …, dir })` likewise |
| `tokenDef(tokenId, ownerUnit | chessId)`, `producesToken(ownerUnit, tokenId)` | the token def a summon of that owner gets — `getToken(id, owner.defId, owner.def.loadout)`, exact even when two players of one field give the same chess different loadouts (prefer it over an id-only `battle.data.getToken(id, unit.defId)` for summon stats / blackboards); whether the owner's loadout makes the token (DATA.md §14 `sources` has 'skill' or 'talent'; true when the data does not tell: no own variant, player-owned summons) |
| `spawnEnemy(enemyKey, { routeIndex, route, pos, mods, tag, sourcePlayerId, ownerPlayerId, bounty, countInTotal, def })` | returns the enemy, or null past `MAX_ALIVE_ENEMIES`; `def` = inline record (enemies.json shape or normalised) for keys missing from data |
| `spawnEnemy(enemyKey, { routeIndex, route, pos, mods, tag, sourcePlayerId, ownerPlayerId, bounty, countInTotal, inTotal, def })` | returns the enemy, or null past `MAX_ALIVE_ENEMIES`; `def` = inline record (enemies.json shape or normalised) for keys missing from data. A runtime spawn is never `inTotal` (§1.3 — the HUD capsule's own set) unless content passes `inTotal: true` explicitly |
| `spawnDevice(key, row, col, { hp, obstacle, blockCnt, name, def, res, atk, bat, aspd })`, `setObstacle(r, c, on)` | obstacles re-path enemies; spawnDevice returns null outside the rect, on a living unit or on a knocked-out operator's tile |
| `isReservedTile(r, c)` | true when a living unit stands there or it is the rest tile (`restTile`) of an ally piece that has not deployed yet / waits to redeploy — the tile a knocked-out operator lies on, else the home tile — every automatic picker (the 突袭 landing tile, tactical points, summon / device tiles) must skip these (`findTacticalPoint` does) |
| `downOn(r, c, except?)`, `restTile(u)` | the down operator (`isDown`: knocked out or forced out) lying on a tile (no ally deploys / moves there: `_deploy`, `spawnDevice`, `relocate`); the tile a withdrawn ally comes back on — its `body` tile when down, else its home (§1) |
@@ -1296,7 +1315,9 @@ Unknown subprofessions fall back to the profession default (test `professions.te
## 9. Wire format (snapshot.js, DESIGN §8.2)
- `snapshot()` → `{ fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total, dps?, boss?, down?, elem? }`.
- `snapshot()` → `{ fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total, resolved, dps?, boss?, down?, elem? }`.
`killed` counts every `counted` knock-out (§1.3) and `total` only the enemies this field scheduled (`inTotal`), so it may
exceed it; `resolved = min(total, killedInTotal + leakedInTotal)` is the HUD capsule's numerator (DESIGN §14 顶栏胶囊).
`sp/spMax` show remaining duration/ammo as a draining bar while a timed skill is active (ammo: `ammoLeft / ammoMax`, the
activation's real total — 拉特兰's and 逃犯引渡手续's extra bullets included, community report #35). Units in DIE state stay 0.8 s.
Active finite zero-SP duration skills display `timeLeft/duration` using `duration` as `spMax`; after end they show
+14 -6
View File
@@ -17,13 +17,13 @@ Platform additions (implemented): `welcome.{resumed, version}`, `welcome.diyKitt
### 8.2 Match
C→S (all carry `rid`): `g.infoReady` · `g.band {bandId}` · `g.bandSkip` · `g.bandFocus {bandId?}` (the strategy highlighted in the draft screen — what a timed-out turn takes; absent / null clears it; `WRONG_PHASE` outside BAND_DRAFT, `ALREADY` after the pick, `BAD_TARGET` for a band not allowed; §18.2) · `g.buy {slot}` · `g.refresh` · `g.freeze` · `g.levelUp` · `g.sell {uid}` · `g.move {uid, to:{area:'board', row, col}|{area:'hand', idx}, dir?}` · `g.equip {itemUid, targetUid, replaceUid?}` (`replaceUid`: which equipped item a third one replaces, §6.2) · `g.art {itemUid, row, col, dir?}` (`dir` ∈ UP|RIGHT|DOWN|LEFT from the deploy wheel, §3; absent ⇒ `to.dir`, then RIGHT; `g.move` onto the piece's own tile re-orients it; board pieces in `m.private` / `m.field {prep:true}` carry `dir`) · `g.destroy {uid}` (hand / temp items only; equipped items are locked, §6.2) · `g.reward {idx}` · `g.choice {idx}` · `g.ready {ready}` · `g.emote {id}` · `g.watch {fieldId, playerId?}` (`playerId`: the player tapped — a shared 联防 / boss pair field shows two; what an eliminated viewer follows, §25) · `g.autoplay {on}` · `g.pause {on}` (solo battles only, §14 Solo pause) · `g.unitStats {seq?}` (ROUND_START / SP_DRAFT / PREP, else `WRONG_PHASE`; eliminated: `ELIMINATED`) → push `m.unitStats` (§18.5) · `g.leave`
S→C:
- `m.public` — full public state on every change (throttled ≤ 10/s): `{ phase, round, lastRound, deadline, serverNow, modeId, difficulty, stageId, factions, disabledBonds, bannedChess, bossId, hiddenBossId?, teamLp?, bossHp?:{hp,max}, overtimeAt? /* FA / HC: ms epoch the overtime drain starts, §4 */, paused /* solo pause, §14 */, draft?:{order,turn,picks,skipsLeft,turnDeadline,turnSeconds /* one turn's length, 0 untimed; deadline = turnDeadline (§18.2) */,untimed}, sp?:{family,cards /* bounty cards carry descRaw?, §20.6 */,turn,picks}, players:[{playerId, seat, name, isBot, connected, alive, lp, bandId, shopLevel, boardCount, ready, bonds:[{bondId,count,active,tier,layers,harmony? /* 调和's +1 is in count, §21.26 */}], fieldId, status, pendingLp? /* COMBAT / 联防 of a normal round: the LP the player's own battle will cost at settlement so far, min(lpCapPerRound, counted leaks) — in 联防 min(lpCapPerRound, uniteLeft) — omitted when 0 and in every other phase (§17.5 live LP) */, uniteLeft? /* 联防: the leaker's enemies still standing — not spawned yet + alive + counted leaks, uncapped, live (§20.6) */}] , fields:[{fieldId, kind, players:[playerId...]}] }` · `m.public.uniteResult` — SETTLE after a 联防 only, its outcome as data: `{ through /* the leakers' enemies that still got through, uncapped */, helpers:[playerId...], leakers:[playerId...], losses:{playerId: the LP settlement charged this round — every alive player} }`; absent when no 联防 resolved (the client's result box then reads the round's own battle; GitHub #235, PR #112)
- `m.public` — full public state on every change (throttled ≤ 10/s): `{ phase, round, lastRound, deadline, serverNow, modeId, difficulty, stageId, factions, disabledBonds, bannedChess, bossId, hiddenBossId?, teamLp?, bossHp?:{hp,max}, overtimeAt? /* FA / HC: ms epoch the overtime drain starts, §4 */, paused /* solo pause, §14 */, draft?:{order,turn,picks,skipsLeft,turnDeadline,turnSeconds /* one turn's length, 0 untimed; deadline = turnDeadline (§18.2) */,untimed}, sp?:{family,cards /* bounty cards carry descRaw?, §20.6 */,turn,picks}, players:[{playerId, seat, name, isBot, connected, alive, lp, bandId, shopLevel, boardCount, ready, bonds:[{bondId,count,active,tier,layers,harmony? /* 调和's +1 is in count, §21.26 */}], fieldId, status, pendingLp? /* COMBAT / 联防 of a normal round: the LP the player's own battle will cost at settlement so far, min(lpCapPerRound, counted leaks) — in 联防 min(lpCapPerRound, uniteLeft) — omitted when 0 and in every other phase (§17.5 live LP) */, uniteLeft? /* 联防: the leaker's enemies still standing — not spawned yet + alive + counted leaks, uncapped, live (§20.6) */}] , fields:[{fieldId, kind, players:[playerId...], progress?:{killed, resolved, total, done} /* §14 顶栏胶囊: resolved = the field's own scheduled enemies knocked down or leaked, null while unknown */}] }` · `m.public.uniteResult` — SETTLE after a 联防 only, its outcome as data: `{ through /* the leakers' enemies that still got through, uncapped */, helpers:[playerId...], leakers:[playerId...], losses:{playerId: the LP settlement charged this round — every alive player} }`; absent when no 联防 resolved (the client's result box then reads the round's own battle; GitHub #235, PR #112)
- `m.private` — the recipient's full PlayerState view (§6.2 minus server-only data) on every change; in a boss round's prep also `bossMate { playerId, side, units }` — the pair partner's board on its half of the boss field (UnitInfo, mirrored on the right half), re-sent when the partner moves (0.2.0, community report item 51).
- `m.field` — field meta when watching starts: `{ fieldId, kind, rect, stageId, units: [UnitInfo], effects? }` (the units on the field, knocked-out operators waiting to redeploy included — `Battle.fieldMeta`, §18.3). The prep-scouting field (`Match.prepFieldMeta`) puts the scouted player's **hand (整备区) and 临时整备区 into `units` as well**: held pieces as units on the hand row (row 7, col = hand slot) and temp pieces on the temp row (row 8, cols 4..8 = the temp slots; PRTS 帮助 counts the temp area with the hand; a held item is a `kind: 'item'` unit the client draws as its floating plate) — the scout renders exactly like the own prep bench, for every watcher alike, the spectator seat's copy equal to a teammate's (§23.19's "never a private view" stays the no-`m.private` rule); the scout signature `_prepScoutSig` carries the hand / temp slots, so a piece moved to another slot is a change — and carries `effects`: the scouted player's effects view (策略 / 机变 / 悬赏 …), the watched player's effects column ready-made
- NOTE: game time in `b.snap`/`b.ev` is carried as **`gt`** (the frame's `t` is the message type).
- Active finite zero-SP timed skills display `sp=timeLeft`, `spMax=duration` and `UF.SKILL`; after end they display
`0/0`. Snapshot display capacity does not change runtime SP/cost.
- `b.snap` — `{ fieldId, gt, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]] , dp, killed, total, down?, elem? }` 20 Hz (a countdown summon's `hp` is maxHp × the share of its life left, §25.16.5). Optional, only when non-empty (§18.3): `down: [[id, respawnAt (game s), respawnTime (s), state, row, col]]` — knocked-out operators waiting to redeploy, `state` 0 counting | 1 timer done, waiting for DP | 2 timer done, its tile taken — a safeguard, no ally deploys there (sim/constants.js `DOWN_STATE`); `row, col` = the tile it lies on and comes back on (§21.24); `elem: [[id, element, fill, cooldownEnd (game s), cooldown (s)]]` — the one gauge a unit shows: the fullest (ties by the official element id 神经 < 侵蚀 < 灼燃 < 凋亡), fill with 2 decimals rounded DOWN and kept at 0.01–0.99 outside a burst (§20.8); during a 爆发冷却 the bursting element with fill 1 and the cooldown numbers (else 0, 0)
- `b.snap` — `{ fieldId, gt, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]] , dp, killed, total, resolved, down?, elem? }` 20 Hz (a countdown summon's `hp` is maxHp × the share of its life left, §25.16.5). Optional, only when non-empty (§18.3): `down: [[id, respawnAt (game s), respawnTime (s), state, row, col]]` — knocked-out operators waiting to redeploy, `state` 0 counting | 1 timer done, waiting for DP | 2 timer done, its tile taken — a safeguard, no ally deploys there (sim/constants.js `DOWN_STATE`); `row, col` = the tile it lies on and comes back on (§21.24); `elem: [[id, element, fill, cooldownEnd (game s), cooldown (s)]]` — the one gauge a unit shows: the fullest (ties by the official element id 神经 < 侵蚀 < 灼燃 < 凋亡), fill with 2 decimals rounded DOWN and kept at 0.01–0.99 outside a burst (§20.8); during a 爆发冷却 the bursting element with fill 1 and the cooldown numbers (else 0, 0)
- `b.ev` — `{ fieldId, ev: [ ... ] }` event tuples: `['spawn', UnitInfo]`, `['atk', srcId, tgtId, projKind]`, `['dmg', tgtId, amount, type]`, `['heal', tgtId, amount]`, `['skill', id, on:0|1]`, `['die', id, reason]` (reason `'killed'`, `'retreat'`, `'expired'` …; only `'killed'` plays an operator's knock-down sound), `['status', id, key, on]`, `['fx', kind, x, y, extra]` (`'hitCap' {id, n}` = a cancelled 限伤 hit, drawn as nothing, §20.12; `extra.form` = the unit's model form from then on (an enemy's mode, a 傀儡师's 替身 §22.11) — `shared/protocol.js fxForm`, state like a spawn: never dropped by the client, §21.4), `['layer', playerId, bondId, n]`, `['bounty', playerId, coins]`, `['deploy', id]`
- `m.toast {kind, text}` · `m.ticker {text, id, type, priority, playerId}` (the strip plays the highest priority first, §21.10) · `m.emote {playerId, id}` · `m.result {...}`
- `m.unitStats {seq, round, units: [unitStatsEntry]}` — the answer to `g.unitStats` (`seq` echoed): every own board operator / summon by uid with `{id, uid, defId, hp, alive, maxHp, atk, def, res, interval, blockCnt, moveSpeed, range?, base: {…same}}` (shared/protocol.js `unitStatsEntry`; `range` = an ally's live range grid, facing RIGHT, never a target-selection grid, §21.16) as its next battle starts (§18.5)
@@ -70,20 +70,28 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ {
**Authority per field.**
- Normal field: the owning human's client is **authoritative** (if connected).
- Unite field (联防) and boss/hidden pair fields: the lowest-seat **connected human** in that field is authoritative; every other human in (or watching) that field receives the same spec and simulates it locally **for display only**.
- Bots, departed/disconnected players, fields with no connected human: the **server** simulates. Normal and 联防 fields headlessly (`fields.js HeadlessJob`, with a `[gt, killed, total(, left)]` progress timeline — `left`: the 联防 leakers' enemies still standing, §20.6) in wall-clock slices of `headlessSliceMs` (8 ms) per callback on a real host — never one blocking run per field (3 bot fields ≈ 0.2–0.5 s of CPU at COMBAT start here, seconds on a mini PC); at once in virtual time; the result is released at the battle's natural end on the field clock (so round pacing and the teammates' progress look live). Boss fields in real time without snapshots (`HeadlessPacer`); when no field of a boss round has a connected human the whole round runs server-side as before (FieldRunner with streaming off).
- Bots, departed/disconnected players, fields with no connected human: the **server** simulates. Normal and 联防 fields headlessly (`fields.js HeadlessJob`, with a `[gt, killed, total, resolved(, left)]` progress timeline — `resolved`: the capsule's numerator on the field clock (the maintainer's review of PR #157: a bot field has no authority reporting `b.progress`), `left`: the 联防 leakers' enemies still standing, §20.6) in wall-clock slices of `headlessSliceMs` (8 ms) per callback on a real host — never one blocking run per field (3 bot fields ≈ 0.2–0.5 s of CPU at COMBAT start here, seconds on a mini PC); at once in virtual time; the result is released at the battle's natural end on the field clock (so round pacing and the teammates' progress look live). Boss fields in real time without snapshots (`HeadlessPacer`); when no field of a boss round has a connected human the whole round runs server-side as before (FieldRunner with streaming off).
- Takeover: an authority that disconnects or leaves, or misses its deadline (`timeLimit / speed` real s + 15 s grace, normal/联防) loses the field: normal/联防 → the server re-simulates the spec from t = 0 (deterministic: the same result); boss → the partner's replica is promoted (`b.start authoritative: true`), else the server runs it, fast-forwarded to the field clock through a `CreditPool` that only credits damage beyond what the client already reported (the fast-forward is spread over the pacing intervals, ≤ 240 extra ticks each). A boss authority silent for 12 s is handed over the same way. The former authority gets `b.end { reason: 'takeover' }` and keeps displaying.
**Protocol (additions; `shared/protocol.js` is normative).**
- `battleId` = `<prefix>.<round>.<seq>.<fieldId>` (`Match._ccField`; `<prefix>` = the match seed in base 36 plus `-<matchNo>`, the room's match number the lobby passes as `opts.matchNo`; `<seq>` counts every battle of the match; the informational `.<fieldId>` is dropped when the id would exceed the protocol's 64 chars): unique for the whole match although field ids repeat (`n:<pid>` every round, `u`, `b1`/`b2` in both boss rounds), and across the matches of one room. The client runner keys its local battles by it (the same id again = the same battle, no rebuild) and the server matches reports by it, so a late `b.progress` / `b.result` of a finished battle — or one that crossed into the room's next match on the same socket — is ignored instead of being credited to another battle.
- S→C `b.start { battleId, fieldId, kind, spec, authoritative, startAt, serverNow, elapsed, speed, watch, done }` — sent at COMBAT/UNITE/FINAL_ASSAULT/HIDDEN start to every human in the field (联防: to every human; COMBAT and boss rounds: eliminated humans and spectator seats get the field of the player they follow as a display replica — the one they last watched with `g.watch`, else the first player still in; research 09 "keep-watching auto-observes the first available field", 0.2.0 item 56), on `g.watch`, and on reconnect/resync. `elapsed` = game seconds on the field clock (a finished field: its end time); the client fast-forwards to it. `speed` = game seconds per real second (2; tests/tools may speed up).
- C→S `b.progress { battleId, gt, killed, total, leaks?, left?, bossDmg?, by?, done? }` — authoritative clients, ~1 Hz (boss fields 4 Hz). Normal/联防: `leaks` = counted leaks so far; 联防: `left` = per leaker its enemies still standing (`spec.js uniteLeft`, clamped by the server to `fields.js uniteBillBounds`; §20.6). Boss: `bossDmg` = cumulative damage of this field to the shared pool, `by` = the same per player, `leaks` = the field's cumulative LP cost (leaks × `lpr` + leader "扣除目标生命" effects; `spec.js attachLpMeter`). The server applies deltas (never twice). Stale reports (unknown battle, not the authority, after the match ended) are ignored, never answered with an error; the platform never answers a rid-less `b.progress` with an error frame (it would surface as a toast).
- C→S `b.progress { battleId, gt, killed, total, resolved?, leaks?, left?, bossDmg?, by?, done? }` — authoritative clients, ~1 Hz (boss fields 4 Hz). `resolved` = the HUD capsule's numerator of this field (below; omitted by a client that cannot count it — the teammate UI then falls back to `killed`); `killed` / `total` are the field's own counters: `killed` counts every `counted` knock-out (a runtime split / summon included) and may exceed `total`, which counts only the enemies the round scheduled. Normal/联防: `leaks` = counted leaks so far; 联防: `left` = per leaker its enemies still standing (`spec.js uniteLeft`, clamped by the server to `fields.js uniteBillBounds`; §20.6). Boss: `bossDmg` = cumulative damage of this field to the shared pool, `by` = the same per player, `leaks` = the field's cumulative LP cost (leaks × `lpr` + leader "扣除目标生命" effects; `spec.js attachLpMeter`). The server applies deltas (never twice). Stale reports (unknown battle, not the authority, after the match ended) are ignored, never answered with an error; the platform never answers a rid-less `b.progress` with an error frame (it would surface as a toast).
- C→S `b.result { battleId, result }` — authoritative client at battle end; `result` = `compactResult(BattleResult)` (the fields the match consumes; ≤ 4 players, ≤ 400 leaks, ≤ 64 unitsEnd, ≤ 160 unitStats, ≤ 40 layer gains; `fitResult` keeps the frame ≤ 60 KB: unit statistics, then leak mods, then — boss only — leak entries go first). Structural checks in `isBattleResult`; `server/net.js` routes `b.*` frames to the match (lobby `routeGame`).
- S→C `b.pool { hp, max, teamLp, acked: { [fieldId]: cumulative damage counted } }` (boss rounds, ≤ 4 Hz; exact numbers, never rounded) — clients reconcile their `LocalBossPool`: displayed pool = server hp − (local cumulative damage − acked). Pools never hold less than 1 HP (`BOSS_POOL_MIN_HP`: `SharedBossPool` and `LocalBossPool`; a browser pool reads any finite rest below 1 as 0 — §20.5); the 'cleared'-result dust rule (a boss field whose client result is `'cleared'` while < 1 HP is left on the server empties the pool) only catches noise at that boundary.
- S→C `b.end { battleId, fieldId, reason }` — `'cleared'` (pool 0) / `'forced'` (team LP 0): the client calls `battle.forceEnd` and reports; `'takeover'`: stop reporting, keep displaying. Both also reach a battle the client is still preparing (the sim loading, the silent catch-up): Client runner below.
- `m.public.fields[]` gains `progress: { killed, total, done }`; `m.public.combatMode`; `m.public.paused: boolean` (always present; solo pause below).
- `m.public.fields[]` gains `progress: { killed, resolved, total, done }`; `m.public.combatMode`; `m.public.paused: boolean` (always present; solo pause below).
- `b.snap` / `b.ev` are **not sent for combat** in client mode. `g.watch` rules: see Spectating.
**Result validation (server, `fields.js validateClientResult`).** A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; `killed ≤ total ≤ 4·spawns + 100`; every leaked `enemyKey` is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through `b.progress`); `counted: false` only for enemies that never count (data `notCountInTotal`, schedule `countInTotal: false`, boss/part entries, content spawns); 联防: leaks + `unspawned` per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond `layerGains ≤ min(60 + 4·round, 999 − the bond's starting layers)` (§20.12) — the flat part raised by what the player's layer 特质 can add to that bond (their per-battle caps; an uncapped one leaves only the 999 room, §21.26) —, none when the spec disables gains, bonds must exist and be named by the player's bond snapshot, band, effects, units or items; `coins ≤ Σ bounty coins of the spawns`; `perfect` consistent with the counted leaks; unit states only for the player's own board uids with hpPct ∈ [0, 1]. The accepted result is **rebuilt** from whitelisted fields (leak mods/tags/sources from the spec — a content-spawned enemy (split, summon) keeps the mods of the schedule entry they equal, i.e. its parent's round multipliers and bounty id, so its 联防 re-entry and bounty match the server run; unit names from server data). Otherwise (or on any exception) the server re-simulates (normal/联防); a boss field is **handed over** instead (below). `SP_VERIFY=all|sample|off` (default `off`): `all` re-simulates every accepted normal/联防 result before accepting it (the server result wins on a digest mismatch); `sample` checks ~1 battle in 8 in a later callback and logs mismatches. `Match.verifyStats = { checked, mismatches, rejected, takeovers }`. Desync between display-only clients and the authority is tolerated: the authoritative outcome is what counts and is broadcast at settlement.
**顶栏胶囊 (the top bar's 击倒 / 漏掉 counter; PR #157, official reading).** The capsule shows `resolved / total`:
- `total` = the enemies **this field itself scheduled** (`Battle` `total`: the `spawns` the match built, `inTotal`) that count — `countInTotal: false`, `tag: 'boss'` / `'part'` and data `notCountInTotal` entries stay out, like the 领袖 of a boss field.
- `resolved` = `min(total, killedInTotal + leakedInTotal)` (`Battle.resolved`): that same set's enemies that are 已解决 — knocked down (`killedInTotal`) **or** leaked (`leakedInTotal`). Official: 开局 0/3 → 漏一个 1/3 → 打死一个 2/3 → 打死会分裂的 3/3.
- Everything spawned while the battle runs is in **neither** part: a split child (磨砻's 木制瑞印), a summon (枯朽萃聚使徒's 枯朽之种, a boss's 余音 / 妖怪 / 不祥幻影 / 王权号令 equipment), a 变身 copy, a part. They still count for LP, 完美作战 and `killed` exactly as master did (`counted`): a leaked split child costs its `lpr` and breaks 完美作战, it just moves no capsule number.
- `killed` (`Battle.killed`, per player too) keeps the official `counted` reading — a runtime split / summon knocked down counts — so it may **exceed** `total` (4 磨砻 killed plus one of their 8 children reads `killed` 5 with `total` 4 while the capsule reads 4/4). Nothing clamps it: the match's stats and the bot's scoring read those knock-outs.
- Carried by `b.snap`, `BattleResult` (battle level and per player, `resolved` clamped to the player's `total`), `b.progress` and `m.public.fields[].progress`. A bot / server-run field has no authority reporting `b.progress`, so its capsule reads the battle's own counters through the `HeadlessJob` timeline sample (`[gt, killed, total, resolved(, left)]`), never the report-driven `progress.leaks` (which stays 0 there); `progress.resolved` starts at `null` and is only adopted from a real reported integer — a reported `0` is a value, "not reported" is not.
- The HUD reads it as `resolved ?? killed` (`ui/hud.js` two capsules, `ui/combatHud.js` teammates' rows, `ui/gameLogic/format.js snapHud`, `battle/observe.js teammateProgress`), so a server that does not send `resolved` displays master's numbers unchanged.
**Result validation (server, `fields.js validateClientResult`).** A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; `killed ≤ maxTotal` and `total ≤ maxTotal` (`4·spawns + 100`, the content-spawn allowance — `killed ≤ total` no longer holds, see the capsule above) and `resolved ≤ total`; every leaked `enemyKey` is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through `b.progress`); `counted: false` only for enemies that never count (data `notCountInTotal`, schedule `countInTotal: false`, boss/part entries, content spawns); 联防: leaks + `unspawned` per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond `layerGains ≤ min(60 + 4·round, 999 − the bond's starting layers)` (§20.12) — the flat part raised by what the player's layer 特质 can add to that bond (their per-battle caps; an uncapped one leaves only the 999 room, §21.26) —, none when the spec disables gains, bonds must exist and be named by the player's bond snapshot, band, effects, units or items; `coins ≤ Σ bounty coins of the spawns`; `perfect` consistent with the counted leaks; unit states only for the player's own board uids with hpPct ∈ [0, 1]. The accepted result is **rebuilt** from whitelisted fields (leak mods/tags/sources from the spec — a content-spawned enemy (split, summon) keeps the mods of the schedule entry they equal, i.e. its parent's round multipliers and bounty id, so its 联防 re-entry and bounty match the server run; unit names from server data). Otherwise (or on any exception) the server re-simulates (normal/联防); a boss field is **handed over** instead (below). `SP_VERIFY=all|sample|off` (default `off`): `all` re-simulates every accepted normal/联防 result before accepting it (the server result wins on a digest mismatch); `sample` checks ~1 battle in 8 in a later callback and logs mismatches. `Match.verifyStats = { checked, mismatches, rejected, takeovers }`. Desync between display-only clients and the authority is tolerated: the authoritative outcome is what counts and is broadcast at settlement.
**Boss rounds.** The shared pool (`SharedBossPool`, size = `bloodPoint[difficulty]` × the players alive when the fight starts, solo × 1 — the owner's decision of 2026-10-06 adopting PR #209, which replaces the fixed pool of 「保持固定血量」, §25.13.4) lives on the server and is fed by the authorities' `b.progress` damage deltas (attributed per player through `by`; BOSS_HIT tickers from each player's damage to that round's pool, §21.10) and by server-run fields; a server clock (250 ms) applies the overtime drain (−`bossOvertimeDrainPerSec` team LP per whole **real** second past `bossOvertimeAfter` real s, measured on the field clock — `gamedata.js bossOvertimeDue`, §4), the silence watchdog and the end checks: pool 0 → `b.end cleared` (victory), team LP 0 → `b.end forced` (defeat) — the first one the server registers decides the verdict (`Match._finalEnding`) and after a forced end no report (an in-flight `b.progress`, the final `b.result`) credits the pool (§20.5); results are awaited ≤ 6 s (× timerScale) after a forced end. `b.pool` is the live channel for the pool and the team LP: a boss `b.progress` (4 Hz per field) and the team-LP losses never re-mark the whole public state — `m.public` (`bossHp`, `teamLp`, `fields[].progress`) and the players' merged-LP shares (`_syncTeamLp`) refresh at ~1 Hz (`Match._bossPublic`, `BOSS_PUBLIC_MS`), and at once when the round ends. A boss field's `b.result` ends it only when the shared pool is empty or after that `b.end` — except a `'cleared'` result whose cumulative report covers what the pool still holds (within its 1 HP floor) while the plausibility budget (one field credits ≤ the pool per `BOSS_MIN_CLEAR_GS` 5 game s) has not caught up: it is **held** (`f.heldResult`; 999-layer boards kill a leader in 2–4 game s, §20.14) — the boss clock keeps crediting the report at the budget's rate, the silence watchdog and handovers skip the field, a re-sent copy is ignored, and the pool's end (`_endFinal`) completes the field with it; a forged report gains nothing a `b.progress` could not already get. Any other result — `'forced'` / `'timeout'` at t = 0, `'cleared'` while the pool still holds more than the report covers — and any result failing validation would stop the pair's fight (and, with every field done, end the Final Assault as a defeat), so the field is **handed over** (`Match._bossHandover`) to the partner's display replica (promoted with `b.start authoritative: true`) or, without one, to the server (fast-forwarded to the field clock); the sender gets `b.end takeover` and is never that field's authority again (`f.demoted`). There is no synthetic zero result for a running boss field: a stats-only stand-in (`fields.js syntheticResult`, carrying the damage already credited, never charging LP) only fills a result that cannot be had otherwise — the Final Assault already ending (no result, or an invalid one, within the grace) or a server-run battle whose `result()` threw.
+4 -2
View File
@@ -100,7 +100,7 @@ export function followedScout({ field = null, watching = null, alive = true, spe
return fid;
}
/** Teammates' progress for the waiting pill: [{ playerId, name, killed, total, done, isBot }]. */
/** Teammates' progress for the waiting pill: [{ playerId, name, killed, resolved, total, done, isBot }]. */
export function teammateProgress(pub, myId) {
const out = [];
for (const f of fields(pub)) {
@@ -111,7 +111,9 @@ export function teammateProgress(pub, myId) {
const pr = isObj(f.progress) ? f.progress : null;
out.push({
playerId: pid, name: p?.name || t('队友'), isBot: !!p?.isBot,
killed: Number.isFinite(pr?.killed) ? pr.killed : null, total: Number.isFinite(pr?.total) ? pr.total : null,
killed: Number.isFinite(pr?.killed) ? pr.killed : null,
resolved: Number.isFinite(pr?.resolved) ? pr.resolved : (Number.isFinite(pr?.killed) ? pr.killed : null),
total: Number.isFinite(pr?.total) ? pr.total : null,
done: f.live === false || !!pr?.done,
});
}
+13 -1
View File
@@ -119,6 +119,14 @@ export const LOST_RESULT_CODES = Object.freeze(['DISCONNECTED', 'OFFLINE', 'TIME
/** Ticks per frame at a speed (same cap as the server pacing: server/match/fields.js maxTicksPerInterval). */
export const ticksPerFrameCap = (speed) => Math.max(8, Math.ceil((Number(speed) || 2) * 4));
/**
* What the runner compares between two looks at a 联防 battle (noteUniteLeft) before it reads the leakers' enemies still
* standing again: a knock-out, a leak, a spawn, the end. `total` stands for the spawns no longer — a split child or a summon
* is outside the capsule's denominator (DESIGN §14 顶栏胶囊) — so the length of the enemy list does.
* @param {any} b the battle
*/
export const uniteLeftMark = (b) => `${Number(b.killed) || 0}:${Number(b.leakedCount) || 0}:${Number(b.total) || 0}:${Array.isArray(b.enemies) ? b.enemies.length : 0}:${b.finished ? 1 : 0}`;
function deepFreeze(root) {
const stack = [root];
while (stack.length) {
@@ -288,7 +296,7 @@ export function createBattleRunner(deps) {
function noteUniteLeft(e) {
const b = e.battle;
const mark = `${Number(b.killed) || 0}:${Number(b.leakedCount) || 0}:${Number(b.total) || 0}:${b.finished ? 1 : 0}`;
const mark = uniteLeftMark(b);
if (mark === e.leakMark) return;
e.leakMark = mark;
let left = e.left;
@@ -428,6 +436,10 @@ export function createBattleRunner(deps) {
e.lastProgressAt = t;
const p = e.sim.spec.battleProgress(e.battle);
const msg = { battleId: e.battleId, gt: Math.min(1e5, p.gt), killed: Math.min(p.killed, p.total), total: Math.min(1e5, p.total), done: !!p.done };
// the HUD capsule's numerator of this field (shared/protocol.js b.progress `resolved`): the field's own scheduled
// enemies knocked out or leaked. Sent only when the battle reports one (Battle.resolved) — an absent field leaves
// the teammate UI on its `resolved ?? killed` fallback
if (Number.isFinite(p.resolved)) msg.resolved = Math.max(0, Math.min(msg.total, p.resolved));
if (bossLike(e)) {
const pool = e.battle.sharedBoss;
msg.leaks = Math.min(1e6, e.meter.lp);
+1 -1
View File
@@ -25,7 +25,7 @@ function ProgressList({ list }) {
<b>${p.name}</b>
${p.done
? html`<span class="chud__prog__ok" aria-label=${t('作战结束')}>✓</span>`
: html`<span class="num">${p.killed != null && p.total != null ? `${p.killed}/${p.total}` : '•••'}</span>`}
: html`<span class="num">${(p.resolved ?? p.killed) != null && p.total != null ? `${p.resolved ?? p.killed}/${p.total}` : '•••'}</span>`}
</span>`)}
</div>`;
}
+5 -2
View File
@@ -22,7 +22,9 @@ export const STATUS_META = Object.freeze({
// ---- snapshots / battle HUD ---------------------------------------------------------------------------------
/**
* HUD numbers from a b.snap: { killed, total, dp, boss } (boss: { hp, max } when present).
* HUD numbers from a b.snap: { killed, resolved, total, dp, boss } (boss: { hp, max } when present).
* `resolved` is the capsule's numerator — knocked out + leaked among the round's own enemies (Battle.leakedInTotal);
* an older snapshot without it falls back to the kill count.
* @param {any} snap
*/
export function snapHud(snap) {
@@ -30,7 +32,8 @@ export function snapHud(snap) {
const n = (v) => (Number.isFinite(v) ? v : null);
let boss = null;
if (isObj(snap.boss) && Number.isFinite(snap.boss.hp)) boss = { hp: snap.boss.hp, max: n(snap.boss.max) ?? n(snap.boss.maxHp) };
return { killed: n(snap.killed), total: n(snap.total), dp: n(snap.dp), boss };
const killed = n(snap.killed);
return { killed, resolved: n(snap.resolved) ?? killed, total: n(snap.total), dp: n(snap.dp), boss };
}
/** Boss HP fraction 0..1 (null when unknown). */
+2 -2
View File
@@ -88,7 +88,7 @@ export function PhaseCapsule({ pub, hud, miss = null }) {
const frac = bossFrac(boss);
return html`<div class="capsule capsule--boss" role="status">
<${Sprite} k="hudPanel/icon_boss" class="capsule__icon" fallback=${html`<${GIcon} name="skull" class="capsule__icon" />`} />
${hud?.total != null ? html`<span class="capsule__kills num"><b>${hud.killed ?? 0}</b>/${hud.total}</span>` : null}
${hud?.total != null ? html`<span class="capsule__kills num"><b>${hud.resolved ?? hud.killed ?? 0}</b>/${hud.total}</span>` : null}
<div class="bossbar" title=${boss ? `${fmtNum(boss.hp)} / ${fmtNum(boss.max)}` : t('敌方领袖')}>
<div class="bossbar__fill" style=${`width:${frac == null ? 100 : frac * 100}%`}></div>
<span class="bossbar__txt num">${frac == null ? t('敌方领袖') : bossPctText(frac)}</span>
@@ -99,7 +99,7 @@ export function PhaseCapsule({ pub, hud, miss = null }) {
return html`<div class=${cx('capsule', 'capsule--combat', phase === PHASE.UNITE && 'capsule--unite')} role="status">
<${Sprite} k=${phase === PHASE.UNITE ? 'hudPanel/icon_coop' : 'hudPanel/icon_battle'} class="capsule__icon"
fallback=${html`<${Icon} name="sword" class="capsule__icon" />`} />
<span class="capsule__kills num"><b>${hud?.killed ?? 0}</b>/${hud?.total ?? '--'}</span>
<span class="capsule__kills num"><b>${hud?.resolved ?? hud?.killed ?? 0}</b>/${hud?.total ?? '--'}</span>
${phase === PHASE.UNITE ? html`<span class="capsule__tag">${t('联防')}</span>` : null}
${phase === PHASE.UNITE && Number.isFinite(miss) ? html`<${MissTag} n=${miss} />` : null}
</div>`;
+40 -18
View File
@@ -17,9 +17,11 @@
// Client-side combat (DESIGN §14) helpers:
// HeadlessJob / runHeadless(battle) step a battle to its end — in wall-clock-bounded slices (bots, takeovers
// under a real scheduler) or at once (verification, virtual time) — and keep a
// progress timeline [[gt, killed, total]] for the teammates' waiting UI
// (联防: [gt, killed, total, left] — left = spec.js uniteLeft, the leakers'
// enemies still standing, for their live counter; user playtest #6 item 7)
// progress timeline [[gt, killed, total, resolved]] for the teammates' waiting UI
// (联防: a 5th element — left = spec.js uniteLeft, the leakers'
// enemies still standing, for their live counter; user playtest #6 item 7).
// `resolved` = the HUD capsule's numerator (Battle.resolved), sampled on the
// field clock: a server-run / bot field has no authority reporting b.progress
// HeadlessPacer real-time pacing of a dynamic set of server-run battles without snapshots
// (boss fields that share the pool while other fields run on clients); a
// takeover's fast-forward is spread over the pacing intervals
@@ -56,7 +58,7 @@ export function snapFrame(fieldId, snap) {
/** Synthetic per-player result used when a field could not run at all (never punishes the player). */
export function emptyPerPlayer() {
return { killed: 0, total: 0, leaked: [], perfect: true, layerGains: {}, coins: 0, damageDealt: 0, bossDamage: 0, healingDone: 0, deaths: 0, unitsEnd: [], unitStats: [] };
return { killed: 0, total: 0, resolved: 0, leaked: [], perfect: true, layerGains: {}, coins: 0, damageDealt: 0, bossDamage: 0, healingDone: 0, deaths: 0, unitsEnd: [], unitStats: [] };
}
/** A finished stand-in for a battle that failed to construct. */
@@ -72,13 +74,13 @@ export class DeadBattle {
this.errorCount = 1;
const perPlayer = {};
for (const p of opts.players || []) perPlayer[p.playerId] = emptyPerPlayer();
this._result = { time: 0, reason, perPlayer, killed: 0, total: 0, errors: 1, synthetic: true };
this._result = { time: 0, reason, perPlayer, killed: 0, total: 0, resolved: 0, errors: 1, synthetic: true };
this.errors = [];
}
step() {}
forceEnd() {}
result() { return this._result; }
snapshot() { return { fieldId: this.fieldId, t: 0, units: [], dp: 0, killed: 0, total: 0 }; }
snapshot() { return { fieldId: this.fieldId, t: 0, units: [], dp: 0, killed: 0, total: 0, resolved: 0 }; }
drainEvents() { return []; }
fieldMeta() { return { fieldId: this.fieldId, kind: this.kind, rect: this.rect, stageId: this.stageId, units: [] }; }
on() { return null; }
@@ -238,7 +240,7 @@ export class FieldRunner {
}
const perPlayer = {};
for (const pid of f.players) perPlayer[pid] = emptyPerPlayer();
return { time: 0, reason: 'forced', perPlayer, killed: 0, total: 0, errors: 1, synthetic: true };
return { time: 0, reason: 'forced', perPlayer, killed: 0, total: 0, resolved: 0, errors: 1, synthetic: true };
}
}
@@ -259,11 +261,15 @@ export const CATCHUP_TICKS_PER_INTERVAL = 240;
const perfNow = () => (globalThis.performance ? globalThis.performance.now() : Date.now());
/**
* A timeline sample of a battle: [gt, killed, total], plus — 联防 — the leakers' enemies still standing
* (spec.js uniteLeft; omitted when unknown).
* A timeline sample of a battle: [gt, killed, total, resolved], plus — 联防 — the leakers' enemies still standing at
* index 4 (spec.js uniteLeft; omitted when unknown). `resolved` is the HUD capsule's numerator of that game second
* (Battle.resolved: the field's own scheduled enemies knocked out or leaked) — the server-run / bot fields have no
* authority that reports b.progress, so the teammates' capsule reads it from here, never from `progress.leaks`.
*/
export function timelineSample(b) {
const s = [Number(b && b.time) || 0, Number(b && b.killed) || 0, Number(b && b.total) || 0];
const total = Number(b && b.total) || 0;
const resolved = Number.isFinite(b && b.resolved) ? Math.max(0, Math.min(total, b.resolved)) : null;
const s = [Number(b && b.time) || 0, Number(b && b.killed) || 0, total, resolved];
if (b && b.kind === 'unite') {
let left = null;
try { left = uniteLeft(b); } catch { left = null; }
@@ -276,7 +282,7 @@ export function timelineSample(b) {
* A server-run battle stepped to its end, in one go or in wall-clock-bounded slices (a low-power host must not stall
* its event loop for the ~0.1–1 s a whole battle takes, several times at once when bots fight): `run(budgetMs)` steps
* until the battle ends (→ true) or the budget is used (→ false; checked every 16 ticks). `timeline` grows while it
* runs (timelineSample: [gt, killed, total(, left)] every TIMELINE_EVERY game seconds, then the final state);
* runs (timelineSample: [gt, killed, total, resolved(, left)] every TIMELINE_EVERY game seconds, then the final state);
* `output()` once done gives `{ battle, result, timeline, crashed }`. A throwing battle is force-ended, then replaced by
* a DeadBattle; the run is bounded by HARD_CAP_SECONDS.
*/
@@ -345,9 +351,9 @@ export function runHeadless(battle, opts = {}) {
return job.output();
}
/** Timeline sample at game time `gt`: [gt, killed, total(, left)] of the last sample ≤ gt. */
/** Timeline sample at game time `gt`: [gt, killed, total, resolved(, left)] of the last sample ≤ gt. */
export function timelineAt(timeline, gt) {
if (!Array.isArray(timeline) || !timeline.length) return [0, 0, 0];
if (!Array.isArray(timeline) || !timeline.length) return [0, 0, 0, null];
let lo = 0, hi = timeline.length - 1;
if (gt >= timeline[hi][0]) return timeline[hi];
while (lo < hi) {
@@ -734,7 +740,9 @@ const sameMods = (a, b) => {
/**
* Semantic validation of a client's BattleResult against the battle's spec (DESIGN §14 "Result validation"). The
* payload already passed shared/protocol.js isBattleResult (types, sizes). Checks: every spec player reported and
* nobody else; killed ≤ total ≤ bound; each leaked enemy key exists in the spawns (multiset bound; keys content may
* nobody else; killed ≤ bound and total ≤ bound (maxTotal: `killed` counts every counted knock-out — runtime splits
* and summons included — and may exceed `total`, which counts only the round's own scheduled enemies; PR #157 capsule
* semantics); each leaked enemy key exists in the spawns (multiset bound; keys content may
* spawn are bounded by the total) — boss fields excepted (their leaks cost team LP through b.progress); a leak is
* `counted: false` only for enemies that never count (data notCountInTotal, countInTotal false, boss / part entries,
* content spawns); 联防: leaks + never-spawned re-entries per (enemy, leaker) ≤ what that leaker sent in, and a split /
@@ -742,7 +750,8 @@ const sameMods = (a, b) => {
* gains ≤ 60 + 4·round + what the player's layer 特质 can add to that bond (layerAllowanceOf: their per-battle caps, no
* flat bound when one is uncapped) and ≤ the room left under BOND_LAYER_CAP (999) from the bond's starting layers, only
* on bonds the player's lineup / band / effects / items name, none when the spec disables gains; coins ≤ the spawns' bounty coins;
* perfect consistent with the counted leaks; unit states only for the player's own units, within range.
* perfect consistent with the counted leaks; unit states only for the player's own units, within range. `resolved` (the
* HUD capsule's numerator) is kept as reported, clamped to the player's `total`; an absent one stays absent.
* @returns {{ ok: true, result: object } | { ok: false, reason: string }}
*/
export function validateClientResult(spec, raw, { gd = null } = {}) {
@@ -781,7 +790,10 @@ export function validateClientResult(spec, raw, { gd = null } = {}) {
const p = raw.perPlayer[pid];
if (!p || typeof p !== 'object') return bad('player');
const own = B.players.get(pid);
if (!Number.isInteger(p.killed) || !Number.isInteger(p.total) || p.killed < 0 || p.killed > p.total || p.total > B.maxTotal) return bad('counts');
// `killed` counts every counted knock-out — a runtime split child / summon too — while `total` counts only the
// enemies the round scheduled, so `killed ≤ total` no longer holds (PR #157 capsule semantics): bound it by
// maxTotal (the spec's content-spawn allowance) instead, exactly like `total`
if (!Number.isInteger(p.killed) || !Number.isInteger(p.total) || p.killed < 0 || p.killed > B.maxTotal || p.total > B.maxTotal) return bad('counts');
const leaked = [];
for (const l of Array.isArray(p.leaked) ? p.leaked : []) {
if (!l || typeof l.enemyKey !== 'string') return bad('leak');
@@ -820,7 +832,9 @@ export function validateClientResult(spec, raw, { gd = null } = {}) {
leaked.push(e);
}
const countedLeaks = leaked.filter((l) => l.counted !== false).length;
if (!bossLike && countedLeaks > p.total + B.spawnCount) return bad('leaks > total');
// `total` is the round's own scheduled enemies; the leaks may include content-spawned children (a split / summon),
// which the spec bounds by maxTotal (the same allowance `killed` is checked against above)
if (!bossLike && countedLeaks > p.total + B.maxTotal) return bad('leaks > total');
if (typeof p.perfect !== 'boolean' || p.perfect !== (countedLeaks === 0)) return bad('perfect');
const layerGains = {};
for (const [bondId, n] of Object.entries(p.layerGains || {})) {
@@ -868,10 +882,18 @@ export function validateClientResult(spec, raw, { gd = null } = {}) {
damageDealt: stat(p.damageDealt), bossDamage: stat(p.bossDamage), healingDone: stat(p.healingDone), deaths: Math.trunc(stat(p.deaths)),
unitsEnd, unitStats,
};
// the HUD capsule's numerator: kept only when the client reported one (an absent value must stay absent — the
// teammate UI then falls back to `killed`, never to a fabricated 0); clamped to the denominator
if (Number.isInteger(p.resolved) && p.resolved >= 0) perPlayer[pid].resolved = Math.min(p.total, p.resolved);
}
if (coinsSum > B.bountyCoins + 1e-6) return bad('coins');
const result = { time: raw.time, reason: raw.reason, perPlayer, killed: 0, total: 0, errors: Math.max(0, Math.trunc(Number(raw.errors) || 0)) };
for (const pid of pids) { result.killed += perPlayer[pid].killed; result.total += perPlayer[pid].total; }
for (const pid of pids) {
result.killed += perPlayer[pid].killed;
result.total += perPlayer[pid].total;
result.resolved = (result.resolved ?? 0) + (Number.isInteger(perPlayer[pid].resolved) ? perPlayer[pid].resolved : 0);
}
if (pids.some((pid) => !Number.isInteger(perPlayer[pid].resolved))) delete result.resolved;
if (spec.kind === 'unite' && Array.isArray(raw.unspawned)) {
const unspawned = [];
for (const u of raw.unspawned) {
+6 -2
View File
@@ -35,11 +35,15 @@ export class MatchClientCombat {
const battleId = seq.length + 1 + String(fieldId).length <= 64 ? `${seq}.${fieldId}` : seq;
const spec = buildBattleSpec({ ...opts, battleId, fieldId, kind, content: this.battleContent, boss });
let total = 0;
for (const x of spec.spawns) if (x && x.tag !== 'boss' && x.tag !== 'part') total += Math.max(1, Math.floor(Number(x.count) || 1));
for (const x of spec.spawns) if (x && x.tag !== 'boss' && x.tag !== 'part' && x.countInTotal !== false) total += Math.max(1, Math.floor(Number(x.count) || 1));
return {
cc: true, fieldId, kind, players: players.slice(), battleId, spec, battle: null, live: true, done: false,
mode: null, authority: null, startAt: this.sched.now(), result: null, resultSource: null, timeline: null, endGt: null,
progress: { gt: 0, killed: 0, total, leaks: 0, done: false }, lastProgressAt: this.sched.now(),
// progress: the field's own numbers as far as they are known before its first b.progress / result (`total` = the
// spec's scheduled enemies — the capsule's denominator). `resolved` starts at **null**: it is only adopted from a
// real report, so `resolved ?? killed` fallbacks (an unreported field, a synthetic result) keep working — a 0 here
// would read as "nothing resolved yet" and could never be told apart from a reported 0.
progress: { gt: 0, killed: 0, total, leaks: 0, resolved: null, done: false }, lastProgressAt: this.sched.now(),
bossAcked: 0, bossBy: {}, lpAcked: 0, lpCum: 0, deadlineTimer: null, doneTimer: null, waitTimer: null,
// boss fields: the latest client reports (re-credited as the plausibility budget grows), the server run's
// CreditPool, humans demoted for an implausible result (never the authority of this field again), a 'cleared'
+5
View File
@@ -25,6 +25,11 @@ export class MatchReports {
// the latest total (spawns never reached before the limit leave it at the end)
p.total = Math.min(maxTotal, Math.max(0, msg.total | 0));
p.killed = Math.min(p.total, Math.max(p.killed, msg.killed | 0));
// the HUD capsule's numerator as the authority reported it (shared/protocol.js b.progress `resolved`). Only a real
// integer is adopted — an absent field leaves the null placeholder alone, so "not reported" and a reported 0 differ
// (a `Number(null) === 0` here would defeat every `resolved ?? killed` fallback); a lower value is kept (the final
// report drops never-spawned enemies from `total`, and a takeover may report a different number)
if (Number.isInteger(msg.resolved)) p.resolved = Math.max(0, Math.min(1e5, msg.resolved));
f.lastProgressAt = this.sched.now();
if (f.kind === 'boss' || f.kind === 'hidden') {
this._creditBoss(f, msg.bossDmg, msg.by);
+2 -1
View File
@@ -126,8 +126,9 @@ export class MatchUnite {
let live = null;
if (f && f.cc) {
if (f.mode === 'server' && f.timeline) {
// the sample is [gt, killed, total, resolved(, left)]: a 联防 sample's `left` is its 5th element
const sample = timelineAt(f.timeline, this._fieldElapsed(f));
live = sample && sample[3] && typeof sample[3] === 'object' ? sample[3] : null;
live = sample && sample[4] && typeof sample[4] === 'object' ? sample[4] : null;
} else live = f.progress && f.progress.left && typeof f.progress.left === 'object' ? f.progress.left : null;
} else if (f && f.battle) {
try { live = uniteLeft(f.battle); } catch { live = null; }
+32 -9
View File
@@ -13,6 +13,14 @@ import { timelineAt } from '../fields.js';
import { bossFieldPlacement } from '../finalAssault.js';
import { battleProgress } from '../../sim/spec.js';
/** A reported capsule numerator clamped to its denominator, else null (unknown — never a fabricated 0). */
const finiteOrNull = (v, cap = Infinity) => {
if (v == null) return null; // Number(null) === 0: an unreported value must not read as "0 resolved"
const n = Number(v);
if (!Number.isFinite(n)) return null;
return Math.max(0, Math.min(Number.isFinite(cap) ? cap : Infinity, Math.trunc(n)));
};
export class MatchViews {
statusOf(ps) {
if (ps.left) return 'left';
@@ -127,25 +135,40 @@ export class MatchViews {
return v;
}
/** m.public.fields[].progress: { killed, total, done } (teammates' waiting UI). */
/**
* m.public.fields[].progress: { killed, resolved, total, done } (teammates' waiting UI). `resolved` is the HUD
* capsule's numerator — the field's own scheduled enemies knocked out or leaked (Battle.resolved / the reported
* b.progress `resolved`); it is `null` (never 0) while unknown, so the client's `resolved ?? killed` fallback holds.
* `total` is the capsule's denominator: only the enemies the round scheduled (runtime splits / summons — boss summons
* included — are in neither part).
*/
_fieldProgress(f) {
if (!f) return null;
if (!f.cc) {
const b = f.battle;
if (!b) return null;
return { killed: Number(b.killed) || 0, total: Number(b.total) || 0, done: !f.live };
const total = Number(b.total) || 0;
return { killed: Number(b.killed) || 0, resolved: finiteOrNull(b.resolved, total), total, done: !f.live };
}
if (f.done && f.result) {
let killed = 0, total = 0;
for (const pp of Object.values(f.result.perPlayer || {})) { killed += Number(pp && pp.killed) || 0; total += Number(pp && pp.total) || 0; }
if (f.result.synthetic) { killed = f.progress.killed; total = f.progress.total; }
return { killed, total, done: true };
let killed = 0, total = 0, resolved = 0, known = true;
for (const pp of Object.values(f.result.perPlayer || {})) {
killed += Number(pp && pp.killed) || 0;
total += Number(pp && pp.total) || 0;
if (Number.isFinite(pp && pp.resolved)) resolved += Number(pp.resolved); else known = false;
}
if (f.result.synthetic) { killed = f.progress.killed; total = f.progress.total; resolved = f.progress.resolved; known = Number.isFinite(resolved); }
return { killed, resolved: known ? Math.min(total, resolved) : null, total, done: true };
}
if (f.mode === 'server' && f.timeline) {
const [, killed, total] = timelineAt(f.timeline, this._fieldElapsed(f));
return { killed, total, done: false };
// a server-run / bot field: no authority ever sends a b.progress, so the capsule reads the battle's own counters —
// the timeline sample carries `resolved` (Battle.resolved: knocked out + leaked among the field's own enemies),
// never the report-driven `progress.leaks`, which would leave such a field at 0 forever
const [, killed, total, resolved] = timelineAt(f.timeline, this._fieldElapsed(f));
return { killed, resolved: finiteOrNull(resolved, total), total, done: false };
}
return { killed: f.progress.killed, total: f.progress.total, done: false };
const total = f.progress.total;
return { killed: f.progress.killed, resolved: finiteOrNull(f.progress.resolved, total), total, done: false };
}
/**
+15
View File
@@ -148,6 +148,11 @@ export class Battle {
this.killed = 0;
this.total = 0;
this.leakedCount = 0;
// The HUD capsule's own counters (DESIGN §14, PR #157 semantics): only the enemies the field itself scheduled
// (`inTotal`, spawns.js `_queueSpawn` — `total` is their denominator) count here. `killed` / `leakedCount` above keep
// the official LP / 完美作战 reading: every `counted` enemy, runtime splits and summons included.
this.killedInTotal = 0;
this.leakedInTotal = 0;
this.errors = [];
this.errorCount = 0;
this._errKeys = new Set();
@@ -172,6 +177,16 @@ export class Battle {
if (typeof opts.setup === 'function') this._safe(() => opts.setup(this), 'opts.setup');
}
/**
* The HUD capsule's numerator (DESIGN §14): the field's own scheduled enemies (`inTotal`) that are 已解决 — knocked
* down (`killedInTotal`) or leaked (`leakedInTotal`) — never above the denominator `total`. The official reading:
* 开局 0/3 → 漏一个 1/3 → 打死一个 2/3 → 打死会分裂的 3/3; a leaked split child still costs LP (`leakedCount`) and breaks
* 完美作战, but it is no enemy of the stage's own list and moves no capsule number.
*/
get resolved() {
return Math.min(this.total, this.killedInTotal + this.leakedInTotal);
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return `Battle<${this.kind} seed=${this.seed} t=${this.time.toFixed(2)} units=${this.units.length}${this.finished ? ' ' + this.reason : ''}>`;
}
+14 -1
View File
@@ -135,11 +135,17 @@ export class BattleDeploy {
this._unblock(unit);
this._enemiesDirty = true;
if (reason === 'killed') {
const pp = this._pp(unit.ownerId);
if (unit.counted) {
this.killed++;
const pp = this._pp(unit.ownerId);
if (pp) pp.killed++;
}
// the HUD capsule's numerator (DESIGN §14): only the enemies the stage itself scheduled (`inTotal`) — a split
// child / summon / part knocked down here is no 已解决 of the stage's own list and moves the capsule not at all
if (unit.inTotal) {
this.killedInTotal++;
if (pp) pp.killedInTotal++;
}
if (killer) killer.stats.kills++;
if (unit.bounty && unit.bounty.coins > 0) this.addCoins(this._bountyPayee(unit, killer), unit.bounty.coins);
}
@@ -252,5 +258,12 @@ export class BattleDeploy {
if (e.counted || e.isBoss) pp.perfect = false;
}
if (e.counted) this.leakedCount++;
// the HUD capsule's leak counter (DESIGN §14): only the stage's own enemies (`inTotal`) — a leaked split child or
// summon still costs LP through `counted`/`leakedCount` and still breaks 完美作战, but it is no 漏掉 of the stage's
// own list, so the capsule's numerator does not move for it
if (e.inTotal) {
this.leakedInTotal++;
if (pp) pp.leakedInTotal++;
}
}
}
+3
View File
@@ -47,6 +47,9 @@ export class BattleEvents {
dp: this.players.length ? Math.floor(this.players[0].dp) : 0,
killed: this.killed,
total: this.total,
// the HUD capsule's numerator (DESIGN §14): the field's own scheduled enemies that are 已解决 (down or leaked).
// `killed` above counts every counted knock-out (runtime splits / summons too) and may exceed `total`.
resolved: this.resolved,
};
if (this.players.length > 1) {
snap.dps = {};
+5 -2
View File
@@ -121,7 +121,7 @@ export class BattleLifecycle {
this.finished = true;
this.reason = reason === 'timeout' ? 'timeout' : 'forced';
this._endReq = null;
try { this._result = this._buildResult(); } catch { this._result = { time: this.time, reason: this.reason, perPlayer: {}, killed: this.killed, total: this.total, errors: this.errorCount }; }
try { this._result = this._buildResult(); } catch { this._result = { time: this.time, reason: this.reason, perPlayer: {}, killed: this.killed, total: this.total, resolved: this.resolved, errors: this.errorCount }; }
this.projectiles.clear();
}
@@ -209,6 +209,9 @@ export class BattleLifecycle {
for (const ps of this.players) {
const pp = this._perPlayer[ps.playerId];
pp.perfect = !pp.leaked.some((l) => l.counted !== false);
// the HUD capsule's numerator of this field (DESIGN §14): this player's own scheduled enemies resolved — knocked
// down or leaked (`counted` keeps the LP / 完美作战 reading, runtime splits and summons included)
pp.resolved = Math.min(pp.total, pp.killedInTotal + pp.leakedInTotal);
// the operators and the board's summon pieces (a board uid): 联防 carries an operator's HP ratio and SP, a summon's
// SP only (match/unite.js). `sp` is the official 技力 — stored charges included (PRTS 技能 "可充能X次…当前技力上限等于该
// 技能技力需求的X倍"); a running skill spent its SP at activation, so it reports what was left (0 for one charge).
@@ -226,7 +229,7 @@ export class BattleLifecycle {
}));
perPlayer[ps.playerId] = pp;
}
const res = { time: Math.round(this.time * 1000) / 1000, reason: this.reason, perPlayer, killed: this.killed, total: this.total, errors: this.errorCount };
const res = { time: Math.round(this.time * 1000) / 1000, reason: this.reason, perPlayer, killed: this.killed, total: this.total, resolved: this.resolved, errors: this.errorCount };
if (this.unspawned && this.unspawned.length) res.unspawned = this.unspawned;
if (this.sharedBoss) res.bossHpLeft = Math.max(0, this.sharedBoss.hp);
return res;
+5 -2
View File
@@ -39,8 +39,11 @@ export class BattlePlayers {
};
this.players.push(ps);
this._perPlayer[ps.playerId] = {
killed: 0, total: 0, leaked: [], perfect: true, layerGains: {}, coins: 0,
damageDealt: 0, bossDamage: 0, healingDone: 0, deaths: 0, unitsEnd: [], unitStats: [],
// `killed` / `total` / `leaked` / `perfect` keep the official `counted` reading (DESIGN §5.1); `killedInTotal` /
// `leakedInTotal` are the HUD capsule's counters of this player's own field (only enemies the round scheduled:
// spawns.js `_queueSpawn` / `inTotal`, deploy.js), `resolved` is derived from them at result time.
killed: 0, total: 0, leaked: [], perfect: true, killedInTotal: 0, leakedInTotal: 0,
layerGains: {}, coins: 0, damageDealt: 0, bossDamage: 0, healingDone: 0, deaths: 0, unitsEnd: [], unitStats: [],
};
const late = [];
for (const u of p.units ?? []) {
+14 -4
View File
@@ -1,4 +1,5 @@
// server/sim/battle/spawns.js — Battle methods: enemies: the spawn queue (counted in `total` up front), routes and the
// server/sim/battle/spawns.js — Battle methods: enemies: the spawn queue (the stage's own enemies: `inTotal`, counted in
// `total` up front — the HUD capsule's numerator/denominator, deploy.js killedInTotal / leakedInTotal), routes and the
// owner of a tile, spawnEnemy, and the enemies on the field — hidden ones, the shared boss pool sync, the field clamp
// and the list compaction.
// Installed on Battle.prototype by server/sim/Battle.js (a method container: never instantiated; `this` is the battle).
@@ -31,7 +32,11 @@ export class BattleSpawns {
ownerPlayerId: s.ownerPlayerId ?? null, pos: s.pos ?? null, seq: ++this._spawnSeq, countInTotal: s.countInTotal,
};
p.counted = p.countInTotal ?? (!(def && def.notCountInTotal) && p.tag !== 'boss' && p.tag !== 'part');
if (precount && p.counted) {
// inTotal: the enemies THIS STAGE scheduled that count — the HUD capsule's own set (DESIGN §14 "顶栏胶囊"). A
// runtime spawn (a split child, a summon, a part, a form change) is never in it, so the capsule's denominator stays
// the stage's own enemies while `counted` (LP, 破坏完美作战 and the kill counters) keeps counting them all.
p.inTotal = !!p.counted;
if (precount && p.inTotal) {
this.total++;
const owner = p.ownerPlayerId ?? this._ownerForTile(p.pos ?? this._routeFor(p.routeIndex, p.route)?.start);
const pp = this._pp(owner);
@@ -73,7 +78,7 @@ export class BattleSpawns {
/**
* Spawn an enemy now. opts: { routeIndex, route, pos:[r,c], mods:{hpMul,atkMul,defMul,resMul,speedMul}, tag,
* sourcePlayerId, ownerPlayerId, bounty:{coins,ownerPlayerId}, countInTotal }
* sourcePlayerId, ownerPlayerId, bounty:{coins,ownerPlayerId}, countInTotal, inTotal }
*/
spawnEnemy(enemyKey, opts = {}) {
if (this.enemies.length >= MAX_ALIVE_ENEMIES && this.aliveEnemies().length >= MAX_ALIVE_ENEMIES) {
@@ -143,7 +148,12 @@ export class BattleSpawns {
if (end) e.route.legs.push({ t: 'move', r: end[0], c: end[1], final: true });
}
e.counted = opts.countInTotal ?? (!def.notCountInTotal && e.tag !== 'boss' && e.tag !== 'part');
if (e.counted && !opts._precounted) {
// inTotal (the HUD capsule's own counter, DESIGN §14): only an enemy the stage scheduled (`_queueSpawn`, which
// pre-counts it) — or, for future content, an explicit `opts.inTotal: true` — and that counts. Everything spawned
// while the battle runs (a split child, a summon — bosses included —, a part, a 变身 copy) stays out of the capsule's
// numerator and denominator, while `counted` (LP, 破坏完美作战, `killed`) keeps counting it as before.
e.inTotal = opts.inTotal === true && e.counted;
if (e.inTotal && !opts._precounted) {
this.total++;
const pp = this._pp(e.ownerId);
if (pp) pp.total++;
+1 -1
View File
@@ -1,6 +1,6 @@
// server/sim/snapshot.js — compact serialization for clients (DESIGN §8.2).
//
// b.snap = { fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total }
// b.snap = { fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total, resolved }
// (hp of a countdown summon — unit.countdown, content/tokens.js startCountdown — is maxHp × the share of its life left)
// UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?,
// form?, skillIndex?, moduleId?, items?, standInFor?, diy? } (standInFor = the replaced operator's charId of a 补位
+22 -4
View File
@@ -262,7 +262,8 @@ export function uniteLeft(battle) {
}
/**
* Progress numbers of a battle for b.progress / the teammates' waiting UI: game time, kills, total, counted leaks
* Progress numbers of a battle for b.progress / the teammates' waiting UI: game time, kills, total, the capsule's
* `resolved` (this field's own scheduled enemies knocked out or leaked — Battle.resolved), counted leaks
* (normal / unite), the boss pool damage of this field and — unite fields — `left` (uniteLeft: each leaker's enemies
* still standing).
*/
@@ -273,10 +274,14 @@ export function battleProgress(battle) {
for (const k of Object.keys(pp)) for (const l of pp[k].leaked || []) if (l && l.counted !== false) leaks++;
const pool = battle && battle.sharedBoss;
const gt = Number(battle && battle.time) || 0;
const total = Math.max(0, Math.trunc(Number(battle && battle.total) || 0));
const out = {
gt: Math.round(gt * 1000) / 1000,
killed: Math.max(0, Math.trunc(Number(battle && battle.killed) || 0)),
total: Math.max(0, Math.trunc(Number(battle && battle.total) || 0)),
total,
// `null` (never 0) when the battle cannot report it: a display replica / a stand-in must not look like "0 resolved"
// (a `0` from here would defeat every `resolved ?? killed` fallback of the HUD)
resolved: numberOrNull(battle && battle.resolved, total),
leaks,
bossDmg: pool && Number.isFinite(pool.cum) ? pool.cum : 0,
done: !!(battle && battle.finished),
@@ -288,6 +293,14 @@ export function battleProgress(battle) {
return out;
}
/** A finite, non-negative integer (never above `cap`), else null — a value the HUD may fall back from. */
function numberOrNull(v, cap = Infinity) {
if (v == null) return null; // Number(null) === 0: a battle without the counter (a stand-in) must not read as "0"
const n = Number(v);
if (!Number.isFinite(n)) return null;
return Math.max(0, Math.min(cap, Math.trunc(n)));
}
const r4 = (v) => Math.round((Number(v) || 0) * 1e4) / 1e4;
const modsKey = (m) => (m && typeof m === 'object' ? JSON.stringify(Object.keys(m).filter((k) => m[k] !== undefined).sort().map((k) => [k, m[k]])) : '');
@@ -348,7 +361,9 @@ export function compactResult(res) {
for (const [k, v] of Object.entries(p.layerGains || {}).slice(0, 40)) if (isKey(k) && fnum(v) > 0) layerGains[k] = Math.min(1e4, fnum(v));
const total = Math.max(0, Math.trunc(fnum(p.total)));
perPlayer[pid] = {
killed: Math.min(total, Math.max(0, Math.trunc(fnum(p.killed)))),
// `killed` keeps the sim's reading (every counted knock-out, runtime splits / summons included) and is deliberately
// NOT clamped to `total`: the denominator counts only the round's own scheduled enemies (Battle.resolved)
killed: Math.max(0, Math.trunc(fnum(p.killed))),
total,
leaked: cap(p.leaked, 400).filter((l) => l && isKey(l.enemyKey)).map((l) => {
const o = { enemyKey: l.enemyKey, mods: compactMods(l.mods), lpr: Math.max(0, Math.min(1000, fnum(l.lpr, 1))), sourcePlayerId: keyOr(l.sourcePlayerId), tag: typeof l.tag === 'string' && l.tag.length <= 16 ? l.tag : null, counted: l.counted !== false };
@@ -373,6 +388,9 @@ export function compactResult(res) {
taken: Math.max(0, Math.round(fnum(u.taken))), attacks: Math.max(0, Math.trunc(fnum(u.attacks))),
})),
};
// the HUD capsule's numerator (or absent for a result that has none: the teammate UI falls back to `killed`)
const resolved = Number.isFinite(p.resolved) ? Math.max(0, Math.min(total, Math.trunc(p.resolved))) : null;
if (resolved != null) perPlayer[pid].resolved = resolved;
}
const out = {
reason: ['cleared', 'timeout', 'forced'].includes(r.reason) ? r.reason : 'forced',
@@ -382,7 +400,7 @@ export function compactResult(res) {
perPlayer,
errors: Math.max(0, Math.min(1e9, Math.trunc(fnum(r.errors)))),
};
if (out.killed > out.total) out.killed = out.total;
if (Number.isFinite(r.resolved)) out.resolved = Math.max(0, Math.min(out.total, Math.trunc(r.resolved)));
if (Array.isArray(r.unspawned) && r.unspawned.length) {
out.unspawned = cap(r.unspawned, 400).filter((u) => u && isKey(u.enemyKey)).map((u) => ({
enemyKey: u.enemyKey, sourcePlayerId: keyOr(u.sourcePlayerId), tag: typeof u.tag === 'string' && u.tag.length <= 16 ? u.tag : null,
+3
View File
@@ -96,6 +96,9 @@ export class Unit {
this.anim = 0;
this.persist = { redeployMul: 1, freeRedeploys: 0 };
this.isBoss = false;
// the HUD capsule's own flag (battle/spawns.js: set on the enemies the field itself scheduled, DESIGN §14); declared
// here so every unit keeps the same object shape (the hot loops' property reads)
this.inTotal = false;
this.bossPool = null;
}
+12 -3
View File
@@ -40,7 +40,12 @@ const isUnitEnd = (u) => isPlain(u) && nullable(isUid)(u.uid) && isNum(u.hpPct,
&& optional(isBool)(u.skillActive) && nullable(isId)(u.defId);
const isUnitStat = (u) => isPlain(u) && nullable(isUid)(u.uid) && nullable(isId)(u.defId) && optional((v) => isStr(v, 16))(u.kind)
&& isStat(u.dmg) && isStat(u.kills) && isStat(u.heal) && isStat(u.taken) && isStat(u.attacks);
const isPerPlayer = (p) => isPlain(p) && isInt(p.killed, 0, 1e5) && isInt(p.total, 0, 1e5) && p.killed <= p.total
const isPerPlayer = (p) => isPlain(p) && isInt(p.killed, 0, 1e5) && isInt(p.total, 0, 1e5)
// `resolved` = the HUD capsule's numerator of this player's own field (the round's own scheduled enemies knocked out
// or leaked: server/sim/battle/deploy.js killedInTotal / leakedInTotal, Battle.resolved). `killed` counts every counted
// knock-out — a runtime split child / summon too — and may therefore exceed `total`, which counts only the round's own
// scheduled enemies (server/match/fields.js validateClientResult bounds it against maxTotal instead).
&& optional((x) => isInt(x, 0, 1e5))(p.resolved)
&& isList(p.leaked, RESULT_LIMITS.leaked, isLeak) && isBool(p.perfect)
&& isMap(p.layerGains, RESULT_LIMITS.layerGains, isId, (v) => isNum(v, 0, 1e4))
&& isStat(p.coins) && isStat(p.damageDealt) && isStat(p.bossDamage) && isStat(p.healingDone) && isStat(p.deaths)
@@ -56,6 +61,7 @@ const isUnspawned = (u) => isPlain(u) && isId(u.enemyKey) && nullable(isId)(u.so
export function isBattleResult(v) {
return isPlain(v) && ['cleared', 'timeout', 'forced'].includes(v.reason) && isNum(v.time, 0, 1e5)
&& optional((x) => isInt(x, 0, 1e5))(v.killed) && optional((x) => isInt(x, 0, 1e5))(v.total)
&& optional((x) => isInt(x, 0, 1e5))(v.resolved)
&& isMap(v.perPlayer, RESULT_LIMITS.players, isId, isPerPlayer) && Object.keys(v.perPlayer).length > 0
&& (v.unspawned === undefined || isList(v.unspawned, RESULT_LIMITS.unspawned, isUnspawned))
&& optional((x) => isInt(x, 0, 1e9))(v.errors) && optional((x) => isNum(x, 0, BIG))(v.bossHpLeft);
@@ -382,13 +388,16 @@ export const C2S = {
// client-side combat (DESIGN §14): the authoritative client of a field reports its battle; a 联防 field adds
// `left` = { [leakerId]: its enemies still standing (unspawned, alive, or through again) } (server/sim/spec.js
// uniteLeft; user playtest #6 item 7 — the leakers' live counter)
// uniteLeft; user playtest #6 item 7 — the leakers' live counter). `resolved` = the HUD capsule's numerator: the
// field's own scheduled enemies that were knocked out **or leaked** (official: 漏一个 1/3, 打死一个 2/3,
// 打死会分裂的 3/3 — Battle.leakedInTotal; runtime splits / summons stay out of both parts of the capsule).
'b.progress': {
battleId: isId, gt: (v) => isNum(v, 0, 1e5), killed: (v) => isInt(v, 0, 1e5), total: (v) => isInt(v, 0, 1e5),
resolved: (v) => isInt(v, 0, 1e5),
leaks: (v) => isNum(v, 0, 1e6), bossDmg: (v) => isNum(v, 0, BIG),
by: (v) => isMap(v, RESULT_LIMITS.players, isId, (x) => isNum(x, 0, BIG)), done: isBool,
left: (v) => isMap(v, RESULT_LIMITS.players, isId, (x) => isInt(x, 0, 1e5)),
$optional: ['leaks', 'bossDmg', 'by', 'done', 'left'],
$optional: ['resolved', 'leaks', 'bossDmg', 'by', 'done', 'left'],
},
'b.result': { battleId: isId, result: isBattleResult },
};
+1 -1
View File
@@ -490,7 +490,7 @@ test('user playtest #6 (DESIGN §20): summons, skill triggers, blocking, push fo
assert.match(sec(17), /\*\*revised by §20\.6\*\*/);
assert.ok(!/the own battle's count stays on show as an upper bound tagged 联防中/.test(DESIGN), '§17.5: the frozen 联防 count is gone');
assert.match(sec(8), /uniteLeft\? \/\* 联防: the leaker's enemies still standing/);
assert.match(sec(14), /`b\.progress \{ battleId, gt, killed, total, leaks\?, left\?, bossDmg\?, by\?, done\? \}`/);
assert.match(sec(14), /`b\.progress \{ battleId, gt, killed, total, resolved\?, leaks\?, left\?, bossDmg\?, by\?, done\? \}`/);
assert.match(sec(6), /3 \*\*different\*\* free chess of tier `min\(level\+1, 6\)`/);
assert.match(S20, /105 cards: 56 next-battle incl\. 源石虫·特训, 42 two-battle, 7 multi-round/);
// #11 / #9 / #13: the gauge look (§18.3 / §19.5 superseded, §8.2 fill), model scale and fear (§9, §5.3, §2)
+6 -6
View File
@@ -729,12 +729,12 @@
"ticks": 1371,
"reason": "cleared",
"killed": 53,
"total": 53,
"total": 16,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 18,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":53,"total":53,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":30262,"bossDmg":0,"heal":0,"deaths":0,"layerGains":{}}},
"players": {"p1":{"killed":53,"total":16,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":30262,"bossDmg":0,"heal":0,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_3_12_a",2,null,2334,1,7,0,0,0,0,0,0],
["p1",2,"op","chess_char_3_16_a",1,null,2173,1,8,0,0,0,0,0,0],
@@ -752,7 +752,7 @@
"hooks": [1,64,1371,110,110,180,180,0,53,53,13,10,0,7978,38,0,53,0,1,53,0,0,0],
"events": {"atk":148,"deploy":9,"die":53,"dmg":180,"engage":7,"fx":25,"skill":23,"spawn":64,"status":59},
"fx": {"copy":1,"dp":1,"sleep":3,"strike":2,"summon":16,"volley":2},
"snaps": "e32ad9c0 c651ddcb ddac8e0f ee7dddc6 74ae6f3c"
"snaps": "5ea42d46 9b9df7e7 90ea3365 87d00bc2 cdcf7237"
},
"bond-maniShip-high": {
"about": "调和 high: tier 1 layers 999; chess_char_5_19_b chess_char_5_17_b chess_char_6_11_b chess_char_1_03_a chess_char_2_04_a chess_char_5_15_b chess_char_5_20_b chess_char_5_21_b token_10030_mlyss_wtrman",
@@ -1314,12 +1314,12 @@
"ticks": 2730,
"reason": "cleared",
"killed": 66,
"total": 66,
"total": 21,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 48,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":66,"total":66,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":155889,"bossDmg":0,"heal":2860,"deaths":3,"layerGains":{}}},
"players": {"p1":{"killed":66,"total":21,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":155889,"bossDmg":0,"heal":2860,"deaths":3,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",3907,1,6,15,28905,0,0,35,2],
["p1",2,"op","chess_char_1_08_b",1,"uniequip_002_texas",0,0,7,4,6105,3310,62,24,3],
@@ -1344,7 +1344,7 @@
"hooks": [1,79,2730,216,216,542,541,519,69,69,33,31,0,9330,93,15,66,0,1,79,0,0,1],
"events": {"atk":313,"deploy":10,"die":69,"dmg":541,"engage":8,"fx":142,"heal":12,"skill":64,"spawn":77,"status":201},
"fx": {"aoe":11,"crit":6,"dodge":1,"dp":3,"sp":3,"substitute":2,"summon":21,"swap":1,"weightless":3,"zone":91},
"snaps": "0d5fce4b 008db012 69aeb33a 4ce7c8d2 b1642f13 224f1fa4 94c69830 28a9268a 8c7e5ae4 7bef3667"
"snaps": "afb0ceff f21761d4 d7ea90d0 794e64bc 49e2ff22 cedf8fb7 032b46b6 d8e9a02c bff04f67 e4b0ff92"
},
"bond-steadShip-low": {
"about": "坚守 low: tier 1 layers 1; chess_char_1_02_a chess_char_1_10_a chess_char_4_11_a chess_char_4_04_a chess_char_4_07_a chess_char_4_09_a chess_char_4_06_a chess_char_4_10_a token_10041_cathy_catsld token_10041_cathy_catsld",
+33 -33
View File
@@ -1443,12 +1443,12 @@
"ticks": 6115,
"reason": "cleared",
"killed": 106,
"total": 223,
"total": 99,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 526,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":106,"total":223,"perfect":false,"leaks":117,"leaksUncounted":0,"coins":0,"dmg":230330,"bossDmg":0,"heal":0,"deaths":26,"layerGains":{}}},
"players": {"p1":{"killed":106,"total":99,"perfect":false,"leaks":117,"leaksUncounted":0,"coins":0,"dmg":230330,"bossDmg":0,"heal":0,"deaths":26,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_chyue",2648.69,1,218,9,6652,5406,0,51,10],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_chyue",0,0,208,7,11931,8055,0,43,5],
@@ -1477,7 +1477,7 @@
"hooks": [1,257,6115,815,815,888,881,0,132,249,94,94,0,9703,3,76,223,117,1,132,0,0,7],
"events": {"atk":815,"deploy":31,"die":132,"dmg":881,"engage":12,"fx":89,"leak":117,"skill":188,"spawn":238,"status":24},
"fx": {"aoe":33,"buff":1,"dodge":7,"summon":48},
"snaps": "3bbe012e d31f4ea8 cb8f67bb 242b9a30 f3ce30d3 e9830293 cf0729b3 988d2c83 02f42254 4f557eea 8e95bd4c 83c92c27 cefc8d7b 213319ae 9f9a26fc f4d90af3 71c1e322 dc08f466 512b9fd0 d6074647 bfccfff2"
"snaps": "1cb8ac18 8da946b7 790e7a4a f39b06ca 2e66dea7 fc31cf4a 3bb31ee9 5259691f e5517687 117f3c7b f499ef53 72bad6b7 d2c5d36a 78b1823c a2ed3107 318dd5e1 39bf29d0 4cdd1f36 8280f0cc 423d96fb b1d5dadb"
},
"diy-037": {
"about": "5_diy1_a=huang/S1 5_diy2_a=huang/S2 5_diy1_a=huang/S3 5_diy2_b=huang/S1 5_diy1_b=huang/S2 5_diy2_b=huang/S3 6_diy1_b=chyue/S1/002_chyue 6_diy2_b=chyue/S2/002_chyue 6_diy1_b=chyue/S3/002_chyue 6_diy2_b=chyue/S1/003_chyue 6_diy1_b=chyue/S2/003_chyue 6_diy2_b=chyue/S3/003_chyue",
@@ -1612,12 +1612,12 @@
"ticks": 6037,
"reason": "cleared",
"killed": 15,
"total": 130,
"total": 99,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 615,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":15,"total":130,"perfect":false,"leaks":115,"leaksUncounted":0,"coins":0,"dmg":611305,"bossDmg":0,"heal":0,"deaths":4,"layerGains":{}}},
"players": {"p1":{"killed":15,"total":99,"perfect":false,"leaks":115,"leaksUncounted":0,"coins":0,"dmg":611305,"bossDmg":0,"heal":0,"deaths":4,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_shwaz",1815,1,5,6,30948,0,0,44,8],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_shwaz",1815,1,7,3,48356,0,0,42,2],
@@ -1644,7 +1644,7 @@
"hooks": [1,146,6037,633,633,624,624,0,19,134,66,66,0,21155,181,8,130,115,1,19,0,0,0],
"events": {"atk":633,"deploy":14,"die":19,"dmg":624,"engage":12,"fx":201,"leak":115,"skill":132,"spawn":144,"status":127},
"fx": {"crit":189,"summon":12},
"snaps": "38c74c5d e93e1fbc 17213ca8 9e5dbf4f aeb96f48 96b6b90b 4ba7be37 44efb9e4 36e8665c 4b6a1bb6 b75f695f 8f4aed34 d37bd760 f91ca2dd 2be01f90 6af5eeab addcfefb d9063e6f e5425fc1 63b87c62 54fc399d"
"snaps": "9065d270 13f6ff85 b2a5c04c b102dd18 6e22ab90 6a03313c 44e746bf 098c8b1a f9ccc1cb c41a9dc4 adad77f1 fd868c79 ca018d6f 5405ea3e 17fad043 58ce5efb f52ab9b6 cdb17a71 9e5eb1f7 9b80db2e e8b19910"
},
"diy-041": {
"about": "6_diy1_b=shwaz/S1/002_shwaz 6_diy2_b=shwaz/S2/002_shwaz 6_diy1_b=shwaz/S3/002_shwaz 6_diy2_b=shwaz/S1/003_shwaz 6_diy1_b=shwaz/S2/003_shwaz 6_diy2_b=shwaz/S3/003_shwaz 5_diy1_a=bgsnow/S1 5_diy2_a=bgsnow/S2 5_diy1_a=bgsnow/S3 5_diy2_b=bgsnow/S1 5_diy1_b=bgsnow/S2 5_diy2_b=bgsnow/S3",
@@ -2269,12 +2269,12 @@
"ticks": 6837,
"reason": "cleared",
"killed": 43,
"total": 205,
"total": 99,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":43,"total":205,"perfect":false,"leaks":162,"leaksUncounted":0,"coins":0,"dmg":375046,"bossDmg":0,"heal":2958,"deaths":16,"layerGains":{}}},
"players": {"p1":{"killed":43,"total":99,"perfect":false,"leaks":162,"leaksUncounted":0,"coins":0,"dmg":375046,"bossDmg":0,"heal":2958,"deaths":16,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,5,1,5895,2960,0,8,2],
["p1",2,"op","chess_char_5_diy2_a",1,null,0,0,9,0,5444,2960,0,4,1],
@@ -2302,7 +2302,7 @@
"hooks": [1,227,6837,648,648,742,742,677,59,221,13,15,0,10521,812,69,205,162,1,59,0,0,0],
"events": {"atk":648,"deploy":19,"die":59,"dmg":688,"engage":12,"fx":121,"leak":162,"skill":33,"spawn":220,"status":42},
"fx": {"aoe":2,"crit":2,"shield":14,"summon":41,"thorns":62},
"snaps": "e7738144 fa178bf1 4f3d484b b5d03ce8 afe1d843 99e0ea61 70f20deb b5ca27cc dc0aba99 af1b025f cfa5c424 f4e158c7 350c8cd1 69bf60f5 90608560 b84e08e9 2fd4f2c5 a130198a 00871290 afb866ce cc73d72e 8837eef9 5010bcac"
"snaps": "e7738144 151f13c6 b636a6a5 d0703ed4 852ec2b3 6586b7a9 ece72287 c18ee8a6 f5803b7b d36bafc4 087320ac ed30204b 60b9e874 e2667db9 b7531c0a 91b4ff9b cb45d547 3210802b 095c2645 37708498 6ec41b79 fa8c03c9 695daa3e"
},
"diy-057": {
"about": "5_diy1_b=judge/S1/002_judge 5_diy2_b=judge/S2/002_judge 5_diy1_b=judge/S3/002_judge 5_diy2_b=judge/S1/003_judge 5_diy1_b=judge/S2/003_judge 5_diy2_b=judge/S3/003_judge 6_diy1_a=judge/S1 6_diy2_a=judge/S2 6_diy1_a=judge/S3 6_diy2_b=judge/S1 6_diy1_b=judge/S2 6_diy2_b=judge/S3",
@@ -2518,12 +2518,12 @@
"ticks": 5825,
"reason": "cleared",
"killed": 17,
"total": 117,
"total": 75,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":17,"total":117,"perfect":false,"leaks":100,"leaksUncounted":0,"coins":0,"dmg":260434,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
"players": {"p1":{"killed":17,"total":75,"perfect":false,"leaks":100,"leaksUncounted":0,"coins":0,"dmg":260434,"bossDmg":0,"heal":0,"deaths":21,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,0,0,103,2,8464,5596,0,19,4],
["p1",2,"op","chess_char_5_diy2_a",1,null,2798,1,142,0,8787,5596,0,20,0],
@@ -2550,7 +2550,7 @@
"hooks": [1,142,5825,388,388,424,424,0,38,138,21,28,0,5293,0,35,117,100,1,42,0,0,0],
"events": {"atk":388,"deploy":23,"die":38,"dmg":424,"engage":12,"fx":18,"leak":100,"skill":42,"spawn":131,"status":74},
"fx": {"revive":4,"summon":14},
"snaps": "638e6eb2 31544512 a15b078a 17800483 1b7dabbb ce2f306c 19062081 3a041721 39afa7a1 006a4df8 eae507b7 1fba63a7 bf273699 69d9619c 665866c4 88e8e4db 455dea8e 6e7ea6b6 95f2c90c 9a23a08a"
"snaps": "638e6eb2 80afea17 3d1f4331 fb3d8acf 99046faa 8da15fd7 000f36ff b76690fc 2ea4822f 617fe41d ddad08cd 9389f818 e1357674 c10e8abd b4f4dd6b d6d77cf4 1a30174a 0af2ac29 d34b557b 112056b4"
},
"diy-063": {
"about": "6_diy1_a=lessng/S1 6_diy2_a=lessng/S2 6_diy1_a=lessng/S3 6_diy2_b=lessng/S1 6_diy1_b=lessng/S2 6_diy2_b=lessng/S3 6_diy1_b=lessng/S1/002_lessng 6_diy2_b=lessng/S2/002_lessng 6_diy1_b=lessng/S3/002_lessng 6_diy2_b=lessng/S1/003_lessng 6_diy1_b=lessng/S2/003_lessng 6_diy2_b=lessng/S3/003_lessng",
@@ -2911,12 +2911,12 @@
"ticks": 6151,
"reason": "cleared",
"killed": 32,
"total": 126,
"total": 75,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":32,"total":126,"perfect":false,"leaks":94,"leaksUncounted":0,"coins":0,"dmg":794494,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"players": {"p1":{"killed":32,"total":75,"perfect":false,"leaks":94,"leaksUncounted":0,"coins":0,"dmg":794494,"bossDmg":0,"heal":0,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_phenxi",1,1,4,2,114557,1795,0,73,3],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_phenxi",1,1,7,3,270924,1795,0,77,7],
@@ -2941,7 +2941,7 @@
"hooks": [1,145,6151,358,351,1134,5570,0,47,141,14,13,0,257,0,17,126,94,1,48,0,0,0],
"events": {"atk":351,"deploy":19,"die":47,"dmg":1120,"engage":12,"fx":167,"leak":94,"skill":27,"spawn":138,"status":40},
"fx": {"aoe":104,"ember":1,"revive":1,"shieldBreak":1,"summon":17,"telegraph":1,"zone":42},
"snaps": "5e309964 e2b294a4 336cbe3c 6268007a 4bb2b146 9232c146 140b6fe6 cdbce0d8 5c252b94 99c7cbee 5d2d6af5 73dc4682 51ea8af4 342978eb 9036bbd3 105a2926 5cfb9b97 74a9fa53 7bdb61aa 3ec7691c 4a72e70c"
"snaps": "cc2cf866 7add2346 2b333f0d 914dde3b 59a20841 00c1e926 2f835686 3d9bb179 c198b367 c151a38c aef71635 1771e992 1ff3d4e1 fdd4c4b8 55318fba d036a5b3 35fe084a ec50b0d0 a981982c aec453e7 db864bb0"
},
"diy-069": {
"about": "6_diy1_b=phenxi/S1 6_diy2_b=phenxi/S2 6_diy1_b=phenxi/S3 6_diy2_b=phenxi/S1/002_phenxi 6_diy1_b=phenxi/S2/002_phenxi 6_diy2_b=phenxi/S3/002_phenxi 6_diy1_b=phenxi/S1/003_phenxi 6_diy2_b=phenxi/S2/003_phenxi 6_diy1_b=phenxi/S3/003_phenxi 6_diy2_b=phenxi/S1/004_phenxi 6_diy1_b=phenxi/S2/004_phenxi 6_diy2_b=phenxi/S3/004_phenxi",
@@ -3128,12 +3128,12 @@
"ticks": 6098,
"reason": "cleared",
"killed": 67,
"total": 156,
"total": 93,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 138,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":67,"total":156,"perfect":false,"leaks":89,"leaksUncounted":0,"coins":0,"dmg":877531,"bossDmg":0,"heal":8248,"deaths":18,"layerGains":{}}},
"players": {"p1":{"killed":67,"total":93,"perfect":false,"leaks":89,"leaksUncounted":0,"coins":0,"dmg":877531,"bossDmg":0,"heal":8248,"deaths":18,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_thorns",0,0,131,7,17174,5374,0,31,1],
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_thorns",0,0,188,0,11587,8648,587,13,1],
@@ -3160,7 +3160,7 @@
"hooks": [1,188,6098,435,435,1218,1333,444,85,174,21,21,0,18383,8,44,156,89,1,91,0,0,4],
"events": {"atk":476,"deploy":30,"die":85,"dmg":1333,"engage":11,"fx":508,"heal":7,"leak":89,"skill":42,"spawn":170,"status":9},
"fx": {"blink":7,"dash":14,"dodge":4,"ember":6,"shield":138,"slash":308,"summon":21,"telegraph":6,"thorns":4},
"snaps": "f1faec58 cbf72c28 58e3f561 b8182f46 b1f5b0f5 1de38a22 bc7c2a2b f233c949 01d56108 560ee3fc 8ec7a4b9 b95b08ce 1943beb7 c593b60a e179c3a0 4a2ad117 171d3fbd 1e1c4411 efd84577 32339940 33fdecb2"
"snaps": "f1faec58 0d0a446f 117456bd e2110e41 645daba7 d3bdb7ad a53991f8 c5889bd7 5cb419cb 56b004a0 ec27a876 4404a392 f7f12c61 63afbbe1 a14b5e85 f3f42e12 efdf3148 2b91f091 b4e7c871 d82451f0 ba0e0a67"
},
"diy-074": {
"about": "5_diy1_b=chen3/S1/002_chen3 5_diy2_b=chen3/S2/002_chen3 5_diy1_b=chen3/S3/002_chen3 6_diy1_a=chen3/S1 6_diy2_a=chen3/S2 6_diy1_a=chen3/S3 6_diy2_b=chen3/S1 6_diy1_b=chen3/S2 6_diy2_b=chen3/S3 6_diy1_b=chen3/S1/002_chen3 6_diy2_b=chen3/S2/002_chen3 6_diy1_b=chen3/S3/002_chen3",
@@ -3542,12 +3542,12 @@
"ticks": 5842,
"reason": "cleared",
"killed": 255,
"total": 255,
"total": 93,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 244,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":255,"total":255,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":623421,"bossDmg":0,"heal":0,"deaths":2,"layerGains":{}}},
"players": {"p1":{"killed":255,"total":93,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":623421,"bossDmg":0,"heal":0,"deaths":2,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,1875,1,1,0,0,0,0,0,0],
["p1",2,"op","chess_char_6_diy2_a",1,null,1875,1,3,0,0,0,0,0,0],
@@ -3572,7 +3572,7 @@
"hooks": [1,269,5842,481,481,2496,1707,0,257,257,24,20,363,19695,0,18,255,0,1,297,0,0,0],
"events": {"atk":1996,"deploy":14,"die":257,"dmg":1707,"engage":8,"fx":357,"skill":44,"spawn":267,"status":41},
"fx": {"ember":40,"reload":52,"revive":1,"shieldBreak":153,"summon":54,"telegraph":40,"zone":17},
"snaps": "30d0dbf8 070d8377 ba316257 264e2c33 72bddae0 d2e7b88a d1fbfd87 0c7c2947 f981c30d 910370b7 10f3a8d7 d122bb52 77e8a4e0 03c43958 9b1b9bcb d0c54363 314530e3 144be113 ea6b1e0b f121eccd"
"snaps": "8a9af3b0 2299cb56 47ff12cb ceb52d71 7b0c947c 1dda5aa2 744bfdf3 17a9ef97 281f57a7 831dce1d e2331e7b 4dd31465 c27b30a6 be9f3593 fa841f00 7139d4df d42f76b9 c09066c0 f93531f5 db0cca6a"
},
"diy-084": {
"about": "5_diy1_a=ray/S1 5_diy2_a=ray/S2 5_diy1_a=ray/S3 5_diy2_b=ray/S1 5_diy1_b=ray/S2 5_diy2_b=ray/S3 5_diy1_b=ray/S1/002_ray 5_diy2_b=ray/S2/002_ray 5_diy1_b=ray/S3/002_ray 5_diy2_b=ray/S1/003_ray 5_diy1_b=ray/S2/003_ray 5_diy2_b=ray/S3/003_ray",
@@ -3872,12 +3872,12 @@
"ticks": 6561,
"reason": "cleared",
"killed": 280,
"total": 345,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":280,"total":345,"perfect":false,"leaks":65,"leaksUncounted":0,"coins":0,"dmg":905052,"bossDmg":0,"heal":61143,"deaths":0,"layerGains":{}}},
"players": {"p1":{"killed":280,"total":105,"perfect":false,"leaks":65,"leaksUncounted":0,"coins":0,"dmg":905052,"bossDmg":0,"heal":61143,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_b",0,"none",3288,1,5,80,5829,0,0,135,11],
["p1",2,"op","chess_char_6_diy2_b",1,"none",3288,1,7,19,1634,0,0,159,1],
@@ -3906,7 +3906,7 @@
"hooks": [1,360,6561,1458,1458,3548,3548,181,280,345,34,30,79,12661,83,102,345,65,1,280,0,0,0],
"events": {"atk":1458,"deploy":12,"die":280,"dmg":3429,"engage":6,"fx":201,"heal":181,"leak":65,"skill":64,"spawn":360,"status":185},
"fx": {"aoe":74,"shield":22,"summon":105},
"snaps": "c440567a f5cbc1c4 c3321a04 8118710d 1d76d702 8b67ece4 3f62819e 179d97ba cef3a956 cdab6acc c097c581 f7b3c60a 05792236 a8a63e3a 081c51c2 c08a2e26 f4b512af 6aab85bc 79cb0a6a 2e6effc0 f37d30cd e45ba017"
"snaps": "c440567a f5cbc1c4 db941e1c 98161e86 7c25733a 412e9f93 345064da 20ef84d4 ddced398 f23be251 68250ac2 38b6fa07 5b3e4205 2d12aba9 a0fd9884 2c69428a 72adafce 537f7ae5 e303df67 2f32732e 76f5e13a a1202906"
},
"diy-092": {
"about": "5_diy1_b=kalts/S1/002_kalts 5_diy2_b=kalts/S2/002_kalts 5_diy1_b=kalts/S3/002_kalts 5_diy2_b=kalts/S1/003_kalts 5_diy1_b=kalts/S2/003_kalts 5_diy2_b=kalts/S3/003_kalts 5_diy1_b=kalts/S1/004_kalts 5_diy2_b=kalts/S2/004_kalts 5_diy1_b=kalts/S3/004_kalts 6_diy1_a=kalts/S1 6_diy2_a=kalts/S2 6_diy1_a=kalts/S3",
@@ -4041,12 +4041,12 @@
"ticks": 6603,
"reason": "cleared",
"killed": 49,
"total": 171,
"total": 93,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":49,"total":171,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":206701,"bossDmg":0,"heal":12234,"deaths":19,"layerGains":{}}},
"players": {"p1":{"killed":49,"total":93,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":206701,"bossDmg":0,"heal":12234,"deaths":19,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,161,4,8407,4272,1594,50,3],
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,127,5,10691,2678,0,44,2],
@@ -4073,7 +4073,7 @@
"hooks": [1,198,6603,873,873,913,886,2062,68,190,36,34,0,33122,4712,70,171,122,1,74,0,0,0],
"events": {"atk":873,"deploy":25,"die":68,"dmg":886,"engage":12,"fx":125,"leak":122,"skill":70,"spawn":185,"status":327},
"fx": {"aoe":13,"dp":40,"ember":6,"revive":5,"shieldBreak":5,"summon":26,"telegraph":6,"zone":24},
"snaps": "74b9ab2e fcc89fa5 251ebfac fd5390f8 4210aa71 cfea27b1 61084509 0c2dd520 b4b6fe6b 3299f55e 612cb6a9 da52c793 abdcdde3 6f440b10 8bc0f32b 9dc74346 477b2d00 654418b2 ec216448 d0d7e05c ee88e2cf ec291971"
"snaps": "74b9ab2e d85db0c0 d1c3bdb8 be104e4a c91c1023 0e638fa7 bd6273fc 6c18e1be 29b67037 ca353f68 1cf8ad1b ae165281 73511a7a 35610d7c 128ab73f 59029157 cb4fe6d3 36e9cd0b 96b30c61 bca5c5f8 c103a041 c0f160cb"
},
"diy-096": {
"about": "5_diy1_a=closur/S1 5_diy2_a=closur/S2 5_diy1_a=closur/S3 5_diy2_b=closur/S1 5_diy1_b=closur/S2 5_diy2_b=closur/S3 5_diy1_b=closur/S1/002_closur 5_diy2_b=closur/S2/002_closur 5_diy1_b=closur/S3/002_closur 6_diy1_a=closur/S1 6_diy2_a=closur/S2 6_diy1_a=closur/S3",
@@ -4874,12 +4874,12 @@
"ticks": 6179,
"reason": "cleared",
"killed": 106,
"total": 189,
"total": 93,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 8,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":106,"total":189,"perfect":false,"leaks":83,"leaksUncounted":0,"coins":0,"dmg":604810,"bossDmg":0,"heal":0,"deaths":18,"layerGains":{}}},
"players": {"p1":{"killed":106,"total":93,"perfect":false,"leaks":83,"leaksUncounted":0,"coins":0,"dmg":604810,"bossDmg":0,"heal":0,"deaths":18,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_diy1_a",0,null,0,0,162,10,98104,3768,0,58,2],
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,202,16,43127,3768,0,51,2],
@@ -4911,7 +4911,7 @@
"hooks": [1,219,6179,587,587,2744,2520,0,124,211,20,18,61,13129,23,42,189,83,1,137,0,0,0],
"events": {"atk":973,"deploy":27,"die":128,"dmg":2520,"engage":11,"fx":145,"leak":83,"skill":38,"spawn":208,"status":442},
"fx": {"aoe":4,"dash":3,"displace":8,"ember":13,"pull":8,"shieldBreak":36,"summon":36,"takeoff":9,"telegraph":13,"volley":15},
"snaps": "39df8f19 dc0b29a2 9e238c17 43dd1e57 a6a34d5f a555ab38 25a0c85d 1ad33783 acbe4560 13b28161 da34a727 b95db8d6 23e509f7 faaca8f2 9f5d7dc2 a8cc015f 4ced24ff 3dece084 287324b5 6dbabf08 a99101bd"
"snaps": "216e6311 22bbd174 e2861a2f db9fa2cb 9ccc6a30 fe834987 143b56d4 3e1bf123 0a9fb02b eb5b35d6 033fff75 60f27492 bf703ce7 93e8e5aa eb86edae eeb8fb64 a361acb7 20b6a8cd f300097c f0f3ad12 0ab29755"
},
"diy-115": {
"about": "5_diy1_b=ebnhlz/S1/002_ebnhlz 5_diy2_b=ebnhlz/S2/002_ebnhlz 5_diy1_b=ebnhlz/S3/002_ebnhlz 5_diy2_b=ebnhlz/S1/003_ebnhlz 5_diy1_b=ebnhlz/S2/003_ebnhlz 5_diy2_b=ebnhlz/S3/003_ebnhlz 5_diy1_b=ebnhlz/S1/004_ebnhlz 5_diy2_b=ebnhlz/S2/004_ebnhlz 5_diy1_b=ebnhlz/S3/004_ebnhlz 6_diy1_a=ebnhlz/S1 6_diy2_a=ebnhlz/S2 6_diy1_a=ebnhlz/S3",
@@ -4926,12 +4926,12 @@
"ticks": 6168,
"reason": "cleared",
"killed": 154,
"total": 232,
"total": 96,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":154,"total":232,"perfect":false,"leaks":78,"leaksUncounted":0,"coins":0,"dmg":511834,"bossDmg":0,"heal":0,"deaths":10,"layerGains":{}}},
"players": {"p1":{"killed":154,"total":96,"perfect":false,"leaks":78,"leaksUncounted":0,"coins":0,"dmg":511834,"bossDmg":0,"heal":0,"deaths":10,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_ebnhlz",1570,1,10,4,57606,0,0,47,5],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_ebnhlz",1570,1,11,7,31893,0,0,29,0],
@@ -4978,7 +4978,7 @@
"hooks": [1,273,6168,461,461,2423,2548,0,164,261,49,47,0,30421,0,28,232,78,1,164,0,0,0],
"events": {"atk":461,"deploy":41,"die":183,"dmg":2548,"engage":11,"fx":193,"leak":78,"skill":96,"spawn":265},
"fx": {"aoe":13,"displace":17,"pull":17,"summon":82,"volley":64},
"snaps": "948c5168 c32d266d 43ba134b 4372fae1 6e4bf334 4c5fe4c1 6dfbc99b bc7d903c 631d9949 3a438571 2e0c2541 2a1dae92 a90b81e6 ca4ec2b1 454f0153 6e8de2d3 0dd5e1fa d3cd56d3 feeb4a26 da6eab43 8a27c708"
"snaps": "25481013 3b3236fa f6b7b1d9 80f81d61 34ef1943 fc11110a a08ee007 369e405c 41ff7208 5e487926 dafe54d3 1b0b63a8 f995283d 0cacb5c4 02b48bb8 4fb3f62d 355c0b0f e3d263b8 07e90d84 8b402749 0f3160ec"
},
"diy-116": {
"about": "6_diy1_b=ebnhlz/S1 6_diy2_b=ebnhlz/S2 6_diy1_b=ebnhlz/S3 6_diy2_b=ebnhlz/S1/002_ebnhlz 6_diy1_b=ebnhlz/S2/002_ebnhlz 6_diy2_b=ebnhlz/S3/002_ebnhlz 6_diy1_b=ebnhlz/S1/003_ebnhlz 6_diy2_b=ebnhlz/S2/003_ebnhlz 6_diy1_b=ebnhlz/S3/003_ebnhlz 6_diy2_b=ebnhlz/S1/004_ebnhlz 6_diy1_b=ebnhlz/S2/004_ebnhlz 6_diy2_b=ebnhlz/S3/004_ebnhlz",
+33 -33
View File
@@ -21,13 +21,13 @@
"ticks": 6000,
"reason": "forced",
"killed": 20,
"total": 20,
"total": 16,
"unspawned": 0,
"bossHpLeft": 860227,
"rngDraws": 465,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":20,"total":20,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1654181,"bossDmg":1231515,"heal":19498,"deaths":1,"layerGains":{}},
"p1": {"killed":20,"total":16,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1654181,"bossDmg":1231515,"heal":19498,"deaths":1,"layerGains":{}},
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1792443,"bossDmg":1508258,"heal":66028,"deaths":1,"layerGains":{}}
},
"units": [
@@ -82,7 +82,7 @@
"hooks": [1,72,6000,1206,1206,3099,3496,3972,31,41,95,89,9,39824,2903,15,29,0,1,32,0,23,0],
"events": {"atk":1542,"deploy":37,"die":41,"dmg":3212,"engage":18,"fx":658,"heal":385,"skill":184,"spawn":69,"status":710},
"fx": {"airstrike":5,"beam":54,"blessing":1,"blink":1,"bombard":9,"bombardShell":9,"burst":23,"copy":3,"dp":4,"drone":335,"droneLock":17,"drones":3,"finale":2,"firewall":1,"garrisonGrant":1,"lightning":124,"link":1,"lock":9,"radiantSword":4,"shell":8,"slash":2,"snow":5,"split":3,"storm":10,"summon":8,"talent":4,"tide":5,"wanted":7},
"snaps": "4b23de24 b826ead4 8478a42a 5f198411 d8f2bcc3 1887ff31 4c303f89 eb6f8150 4dfd4f69 9992c24d 2addc854 8d1008cc 5356698a b471ba51 316422fb c3bf9985 bfae7486 4e4d06f2 d87e4df8 2727e182"
"snaps": "44bc2b70 b3daaf96 baf6929d 143779f3 c1387369 80ca050e 4e208395 6728039d 417e6ec7 e83606e2 06a61aec 78738a35 a6c6ae9c c7844f12 05ba7c16 89e6563a eafb1f1c 5f815a0a f048b25a cef59d00"
},
"boss-boss_1-solo": {
"about": "假想敌:胄 solo pool 1800000: 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",
@@ -97,12 +97,12 @@
"ticks": 6000,
"reason": "forced",
"killed": 17,
"total": 17,
"total": 13,
"unspawned": 0,
"bossHpLeft": 1047606,
"rngDraws": 51,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":17,"total":17,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":966812,"bossDmg":752394,"heal":37356,"deaths":0,"layerGains":{}}},
"players": {"p1":{"killed":17,"total":13,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":966812,"bossDmg":752394,"heal":37356,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_horn",2775,1,3,1,7118,3554,0,5,1],
["p1",2,"op","chess_char_5_11_b",1,"uniequip_002_demkni",4804.8,1,5,0,0,4111,23085,0,35],
@@ -125,7 +125,7 @@
"hooks": [1,36,6000,693,693,1031,1120,3142,24,24,60,59,168,20939,2386,0,26,0,1,24,0,0,0],
"events": {"atk":709,"deploy":10,"die":24,"dmg":1060,"engage":6,"fx":278,"heal":148,"skill":119,"spawn":36,"status":286},
"fx": {"anchor":3,"beam":54,"camouflage":1,"healAoe":35,"overload":1,"shell":8,"splash":168,"substitute":2,"summon":4,"swap":2},
"snaps": "e67f9eea 2430c808 1d53d0cb 883a39b4 e2e83b6d 8d8ac019 679768b3 0e10a007 f2a7dd03 e508cc03 00b8aaac c75a3d01 837a2744 4e854516 4dc32e71 f923cb9e f9c21aa0 de1d717c 454f544e ab149f66"
"snaps": "5160aa85 7e265445 0be2d2e9 6c0188d5 103cf813 9aace512 f579cdb4 8ee9aa58 8005f42d ba1509c1 5738e08d a56aea09 c76253e8 4d964f19 99f8d5c9 4cc5c3f9 9ea422bb 63e8d010 53339522 4b7760db"
},
"boss-boss_2-pair": {
"about": "假想敌:铳 pair pool 6000000: 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",
@@ -262,13 +262,13 @@
"ticks": 5803,
"reason": "cleared",
"killed": 19,
"total": 21,
"total": 6,
"unspawned": 0,
"bossHpLeft": 0,
"rngDraws": 1423,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":19,"total":21,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1572377,"bossDmg":1451138,"heal":8263,"deaths":4,"layerGains":{}},
"p1": {"killed":19,"total":6,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1572377,"bossDmg":1451138,"heal":8263,"deaths":4,"layerGains":{}},
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2612061,"bossDmg":2548862,"heal":37169,"deaths":3,"layerGains":{}}
},
"units": [
@@ -360,7 +360,7 @@
"tide": 3,
"wanted": 1
},
"snaps": "8c6eb9d3 f5f9a5b4 160c191e 00bb0683 7dd5622f 4b0281e0 6b9388d5 d238f18e 0d5c7c32 c5a0818c 04311046 f68b2536 1eb1629f b8281779 5c9abae1 b74b5a58 4ca28388 6c1d2341 c8e29d0c 0892f5e8"
"snaps": "8c6eb9d3 796da093 9e3c16b7 fa7000d5 b0ba5df0 c13bfdd0 b5a04ea3 1a096951 99f7023b ee7eca69 27c012ea 189a5d94 675b6a80 c16a2d26 fb45381b dc80cb4d 62258e5c f5fd826c 841da98a c3777617"
},
"boss-boss_3-solo": {
"about": "假想敌:管 solo pool 2000000: 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",
@@ -375,12 +375,12 @@
"ticks": 6000,
"reason": "forced",
"killed": 15,
"total": 15,
"total": 9,
"unspawned": 0,
"bossHpLeft": 1281945,
"rngDraws": 11,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":15,"total":15,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":790763,"bossDmg":718055,"heal":12252,"deaths":2,"layerGains":{}}},
"players": {"p1":{"killed":15,"total":9,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":790763,"bossDmg":718055,"heal":12252,"deaths":2,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_horn",2775,1,11,0,187454,8125,0,133,4],
["p1",2,"op","chess_char_5_11_b",1,"uniequip_002_demkni",4804.8,1,9,0,0,0,8390,0,18],
@@ -411,7 +411,7 @@
"hooks": [1,42,6000,403,403,736,1597,1098,20,24,64,63,21,16163,1212,2,19,0,1,153,0,1,0],
"events": {"atk":422,"deploy":17,"die":24,"dmg":716,"engage":8,"fx":217,"heal":42,"skill":127,"spawn":36,"status":180},
"fx": {"anchor":1,"aoe":3,"beam":6,"burst":1,"candle":3,"ember":3,"explode":132,"healAoe":18,"overload":4,"phase":13,"revive":1,"splash":21,"substitute":1,"summon":6,"swap":1,"telegraph":1,"teleport":2},
"snaps": "edad47f7 eb7e962c 901e9a90 f6931b7e ebcc98aa 25e8d278 8613cfef ec8ed003 b9a58927 9405e611 adc77694 58659919 2f682a2d 7386dac2 87591558 f5f68337 ad5b46f9 a460afb6 0337e937 99d41b9c"
"snaps": "edad47f7 646819de f70d4209 623036bb 007bda8c c32ef17d 45a767ca ba1d4895 692a0b5b 00e8b931 0257c6ee b3448033 79f115e6 61c7911a e82a3934 95da73a6 72fc7d06 d6376b51 d8d5a6a7 2e2cb739"
},
"boss-boss_4-pair": {
"about": "盐风主教昆图斯 pair pool 8400000: 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",
@@ -553,13 +553,13 @@
"ticks": 3856,
"reason": "cleared",
"killed": 18,
"total": 19,
"total": 8,
"unspawned": 0,
"bossHpLeft": 0,
"rngDraws": 1055,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":18,"total":19,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1203964,"bossDmg":884209,"heal":18083,"deaths":2,"layerGains":{}},
"p1": {"killed":18,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1203964,"bossDmg":884209,"heal":18083,"deaths":2,"layerGains":{}},
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1184403,"bossDmg":675791,"heal":65228,"deaths":1,"layerGains":{}}
},
"units": [
@@ -642,7 +642,7 @@
"tide": 3,
"wanted": 7
},
"snaps": "7db84769 07f86d88 93f09fef c359f639 8c1c3866 2e4a5893 f241408f 1d2dafff 2e0c6f08 0716004e 656c6621 e7fc5f6b 5fdc9566"
"snaps": "7db84769 ccd2ecf5 49d646d8 1185f93a 64db5dca 1a9e67db 3a4e3045 5b3ba069 0dcc32cf 7d6016ed 4061bb14 3c99a82f a50f8c81"
},
"boss-boss_5-solo": {
"about": "卢西恩,“猩红血钻” solo pool 780000: 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",
@@ -657,12 +657,12 @@
"ticks": 6000,
"reason": "forced",
"killed": 10,
"total": 11,
"total": 5,
"unspawned": 0,
"bossHpLeft": 466087,
"rngDraws": 291,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":10,"total":11,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":476733,"bossDmg":313913,"heal":44154,"deaths":2,"layerGains":{}}},
"players": {"p1":{"killed":10,"total":5,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":476733,"bossDmg":313913,"heal":44154,"deaths":2,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_horn",0,0,2,1,103410,6835,4077,56,3],
["p1",2,"op","chess_char_5_11_b",1,"uniequip_002_demkni",4804.8,1,1,0,0,0,12704,0,25],
@@ -689,7 +689,7 @@
"hooks": [1,30,6000,357,357,687,1120,4237,14,18,58,57,92,17108,995,17,14,0,1,60,0,5,47],
"events": {"atk":367,"deploy":16,"die":18,"dmg":806,"engage":9,"fx":221,"heal":249,"skill":115,"spawn":27,"status":191},
"fx": {"anchor":1,"aoe":1,"blink":6,"bloodBattle":1,"burst":5,"candle":2,"dodge":47,"ember":1,"explode":24,"healAoe":25,"overload":3,"shieldBreak":2,"splash":92,"summon":8,"telegraph":1,"teleport":2},
"snaps": "20a08a18 81858159 9443afc2 37c4a489 c77051bb 940bfbc5 d27ac583 503cf292 a5771a92 598f6f84 ce5a0bfd bd96c315 8f1d4a4b 387b9dbd 9e4cb6fb e7146e90 3a787983 7725c0a5 b8f07b0b a90fc040"
"snaps": "20a08a18 e7c4e5c0 19953744 0b473fe3 5361c85f cb383aa0 6804cf6e 34e854d8 abc09b9d 4729ee83 d3e3e78f bf8d4ce2 824cc2db fcf4e1ba 663a28e6 358410c3 de4f40de 6c169100 7b105e25 70e4dc0f"
},
"boss-boss_6-pair": {
"about": "阿利斯泰尔,帝国余晖 pair pool 7960000: 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",
@@ -797,12 +797,12 @@
"ticks": 6000,
"reason": "forced",
"killed": 5,
"total": 10,
"total": 4,
"unspawned": 0,
"bossHpLeft": 3874806,
"rngDraws": 45,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":5,"total":10,"perfect":false,"leaks":5,"leaksUncounted":0,"coins":0,"dmg":152625,"bossDmg":105194,"heal":1783,"deaths":16,"layerGains":{}}},
"players": {"p1":{"killed":5,"total":4,"perfect":false,"leaks":5,"leaksUncounted":0,"coins":0,"dmg":152625,"bossDmg":105194,"heal":1783,"deaths":16,"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],
@@ -833,7 +833,7 @@
"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 2a259699 77c4f1c2 a547c8e8 80cdbe94 5387ac7c ac8acb26 e405b502 b8cb4679 62dee17d 6a983199 49974232 ee10619f d218a36a 29eef739 77b2f124 b45c9755 95f6faee a7fcd01a edbf901f"
"snaps": "3e08ed3d b0f5c6bf 12616b1d 9cce9eed afcfc076 fb56bae7 617f9500 b046bb87 398d9618 80f1089f 99802228 823c06f9 a478439e 71b3d152 452c5be2 683a7bbf 1f49e639 2d0e0575 72ea261c 8fc27088"
},
"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",
@@ -957,13 +957,13 @@
"ticks": 6000,
"reason": "forced",
"killed": 16,
"total": 18,
"total": 14,
"unspawned": 0,
"bossHpLeft": 12585770,
"rngDraws": 666,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":16,"total":18,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2677042,"bossDmg":1550396,"heal":69312,"deaths":17,"layerGains":{}},
"p1": {"killed":16,"total":14,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2677042,"bossDmg":1550396,"heal":69312,"deaths":17,"layerGains":{}},
"p2": {"killed":0,"total":0,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":560253,"bossDmg":262771,"heal":9942,"deaths":25,"layerGains":{}}
},
"units": [
@@ -1044,7 +1044,7 @@
"weightless": 3,
"zone": 180
},
"snaps": "cac31b2c d1e28667 87626189 11a56b43 640cf8cf 9a92be69 12784d9a 195ea951 99de9374 65451b7e 90846f89 9bcf64aa 0cf3dfb9 5737ca93 68f962d8 98c8303b e5af0a0e 3c947908 ce14aa80 61dc2e23"
"snaps": "0499777c 66895c47 00cd2e8d 02a8aa95 5d3f3829 3d4bf1c3 1ecb5a32 b9d715e5 54e94d88 7c9b3f95 de81172e 15cddd71 78890306 d4586c67 72920db0 61a8a2c6 4a931d91 d389bff6 1cdd3841 f76bc0a3"
},
"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",
@@ -1059,12 +1059,12 @@
"ticks": 6000,
"reason": "forced",
"killed": 3,
"total": 13,
"total": 6,
"unspawned": 0,
"bossHpLeft": 6797394,
"rngDraws": 227,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":3,"total":13,"perfect":false,"leaks":10,"leaksUncounted":0,"coins":0,"dmg":450453,"bossDmg":402606,"heal":28293,"deaths":23,"layerGains":{}}},
"players": {"p1":{"killed":3,"total":6,"perfect":false,"leaks":10,"leaksUncounted":0,"coins":0,"dmg":450453,"bossDmg":402606,"heal":28293,"deaths":23,"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],
@@ -1094,7 +1094,7 @@
"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": "cf292ec5 7036981d ce112fc3 25f0eb93 7e29d0f3 a4667973 30a2224e 8827f472 5ed6aeed 09c8a1e1 a4c3c74f 484ec38d cf5610d2 28371e20 bee6b488 d24d7a86 3e0c2516 47741f73 949b25ac 7b412345"
"snaps": "9b5e58cf c4e06fea a40166a3 0e30f917 42214d72 ebc20f66 7258ad62 49f2a70f 0b93ecfb 8a8ebc4e 4bf3c623 15ee0c07 41d58e70 7b48444f 6d3425f8 2d1f93c0 a755c2ad 76e23872 1016f3ce e5423b09"
},
"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",
@@ -1272,13 +1272,13 @@
"ticks": 6000,
"reason": "forced",
"killed": 10,
"total": 12,
"total": 4,
"unspawned": 0,
"bossHpLeft": 14635261,
"rngDraws": 918,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":10,"total":12,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":459267,"bossDmg":234580,"heal":65462,"deaths":5,"layerGains":{}},
"p1": {"killed":10,"total":4,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":459267,"bossDmg":234580,"heal":65462,"deaths":5,"layerGains":{}},
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":520827,"bossDmg":330160,"heal":13987,"deaths":24,"layerGains":{}}
},
"units": [
@@ -1360,7 +1360,7 @@
"weightless": 2,
"zone": 200
},
"snaps": "f891236c f6672d61 695d2e11 dd4444a7 42309327 9cec6e57 b8fe3b31 cfaa0d6b 4be820e6 693491bb cddbaa3a 1e1a7871 945cbd12 d9b820e0 034525ef 35395d78 56b4abc5 8f47eaee d52f39d7 520c8b02"
"snaps": "f891236c f6672d61 695d2e11 dd4444a7 2a7dde13 b3213880 431cd2c4 6abcdf88 03c938b6 2adbab93 89502717 3fc3d404 0c63a20c 8d49e8fc f8eb3fc4 640373e1 0243b53a 3b3694f0 569f5d6c 6ebfcb06"
},
"hidden-boss_10-solo": {
"about": "假想敌:管 solo pool 7600000: 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",
@@ -1375,12 +1375,12 @@
"ticks": 6000,
"reason": "forced",
"killed": 11,
"total": 13,
"total": 5,
"unspawned": 0,
"bossHpLeft": 7323878,
"rngDraws": 102,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":11,"total":13,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":430741,"bossDmg":276122,"heal":148595,"deaths":10,"layerGains":{}}},
"players": {"p1":{"killed":11,"total":5,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":430741,"bossDmg":276122,"heal":148595,"deaths":10,"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],
@@ -1403,7 +1403,7 @@
"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 b1fdbc1b 7b25f64c 67b47291 502d1b71 8861598b 7112c963 3170b708 8e72387d c384da93 0d0d33a6 19ea6be5 8daa9858 108f2f2f 471283be f8dc4d37 8c04e516"
"snaps": "65ab7720 4d0f9bc6 b175a7ad decfc6d9 d1ad8895 b6add8db 8b187de4 1339864b 84136f5a 963ae966 244b6531 f0d249a1 55dc7a49 f44509fa 5ba1fdbd c65a6ccf 00d4edb3 c6669080 45cab92a 414911cc"
},
"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",
+36 -36
View File
@@ -24,7 +24,7 @@
{"round":2,"ai_0":[1,27,5,0,0,2,4,"8cdbb4d3",0,"preciShip 0/2, sargonShip 0/4",8,8,0,9]},
{"round":3,"ai_0":[1,27,6,0,0,3,6,"88b2c40f",1,"indomShip 1/1, preciShip 0/2, sargonShip 0/4",7,7,0,15]},
{"round":4,"ai_0":[1,27,7,0,0,3,8,"12cbee9d",2,"indomShip 1/2, preciShip 0/2, sargonShip 0/4",10,10,0,21]},
{"round":5,"ai_0":[1,27,8,0,0,3,8,"bea79fc5",3,"indomShip 2/3, kjeragShip 1/0, preciShip 0/2, sargonShip 0/4",53,53,0,29]},
{"round":5,"ai_0":[1,27,8,0,0,3,8,"bea79fc5",3,"indomShip 2/3, kjeragShip 1/0, preciShip 0/2, sargonShip 0/4",53,16,0,29]},
{"round":6,"ai_0":[1,27,9,0,0,4,8,"b6051456",15,"indomShip 2/4, kjeragShip 1/3, preciShip 0/2, sargonShip 0/4, steadShip 1/8",28,28,0,39]},
{"round":7,"ai_0":[1,27,10,0,0,4,8,"bc01603b",31,"indomShip 2/5, kjeragShip 1/7, preciShip 0/2, sargonShip 0/4, steadShip 2/19",27,27,0,49]},
{
@@ -113,7 +113,7 @@
},
{
"round": 6,
"ai_0": [1,27,11,0,0,3,8,"bc2d0f8e",27,"kazimierzShip 0/8, kjeragShip 0/2, preciShip 1/3, sargonShip 0/8, siracusaShip 1/12, soloShip 1/3, steadShip 0/2, suntShip 1/0, swiftShip 1/9",64,64,0,39]
"ai_0": [1,27,11,0,0,3,8,"bc2d0f8e",27,"kazimierzShip 0/8, kjeragShip 0/2, preciShip 1/3, sargonShip 0/8, siracusaShip 1/12, soloShip 1/3, steadShip 0/2, suntShip 1/0, swiftShip 1/9",64,19,0,39]
},
{
"round": 7,
@@ -141,7 +141,7 @@
},
{
"round": 13,
"ai_0": [1,27,12,0,0,6,8,"20f861b4",335,"arcaneShip 2/18, kazimierzShip 0/10, kjeragShip 0/2, preciShip 2/65, sargonShip 0/8, siracusaShip 1/204, soloShip 1/3, steadShip 0/2, suntShip 1/0, swiftShip 1/45",115,115,0,121]
"ai_0": [1,27,12,0,0,6,8,"20f861b4",335,"arcaneShip 2/18, kazimierzShip 0/10, kjeragShip 0/2, preciShip 2/65, sargonShip 0/8, siracusaShip 1/204, soloShip 1/3, steadShip 0/2, suntShip 1/0, swiftShip 1/45",115,35,0,121]
}
],
"players": {
@@ -252,15 +252,15 @@
"rounds": [
{"round":1,"ai_0":[1,27,4,0,0,1,2,"cc1bac8f",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
{"round":2,"ai_0":[1,27,5,0,0,1,4,"eee78fe4",0,"kazimierzShip 0/2, sargonShip 0/4, soloShip 1/0, swiftShip 0/2",10,10,0,9]},
{"round":3,"ai_0":[1,27,6,0,1,1,6,"d5f9d956",1,"kazimierzShip 0/2, sargonShip 0/8, soloShip 1/1, swiftShip 0/2",11,11,0,15]},
{"round":4,"ai_0":[1,27,8,0,1,2,8,"247bff16",4,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/3, swiftShip 0/4",11,11,0,23]},
{"round":3,"ai_0":[1,27,6,0,1,1,6,"d5f9d956",1,"kazimierzShip 0/2, sargonShip 0/8, soloShip 1/1, swiftShip 0/2",11,9,0,15]},
{"round":4,"ai_0":[1,27,8,0,1,2,8,"247bff16",4,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/3, swiftShip 0/4",11,9,0,23]},
{
"round": 5,
"ai_0": [1,27,9,0,0,3,8,"98f10fe1",11,"kazimierzShip 0/8, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/6, soloShip 1/1, steadShip 1/4, suntShip 1/0, swiftShip 0/8",21,21,0,32]
},
{
"round": 6,
"ai_0": [1,27,9,0,0,3,8,"3b1cee08",42,"kazimierzShip 0/8, kjeragShip 0/2, raidShip 1/16, sargonShip 0/8, siracusaShip 1/17, soloShip 1/1, steadShip 1/8, suntShip 1/0, swiftShip 0/8",56,56,0,41]
"ai_0": [1,27,9,0,0,3,8,"3b1cee08",42,"kazimierzShip 0/8, kjeragShip 0/2, raidShip 1/16, sargonShip 0/8, siracusaShip 1/17, soloShip 1/1, steadShip 1/8, suntShip 1/0, swiftShip 0/8",56,17,0,41]
},
{
"round": 7,
@@ -288,7 +288,7 @@
},
{
"round": 13,
"ai_0": [1,27,12,0,0,6,8,"b6fe7f54",432,"arcaneShip 1/6, kazimierzShip 1/104, kjeragShip 0/2, miraShip 1/3, preciShip 1/60, raidShip 1/134, sargonShip 0/8, siracusaShip 1/91, soloShip 1/1, steadShip 0/10, suntShip 1/0, swiftShip 1/33",115,115,0,125]
"ai_0": [1,27,12,0,0,6,8,"b6fe7f54",432,"arcaneShip 1/6, kazimierzShip 1/104, kjeragShip 0/2, miraShip 1/3, preciShip 1/60, raidShip 1/134, sargonShip 0/8, siracusaShip 1/91, soloShip 1/1, steadShip 0/10, suntShip 1/0, swiftShip 1/33",115,35,0,125]
}
],
"players": {
@@ -399,15 +399,15 @@
"rounds": [
{"round":1,"ai_0":[1,27,4,0,0,1,2,"cc1bac8f",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
{"round":2,"ai_0":[1,27,5,0,0,1,4,"eee78fe4",0,"kazimierzShip 0/2, sargonShip 0/4, soloShip 1/0, swiftShip 0/2",10,10,0,9]},
{"round":3,"ai_0":[1,27,6,0,1,1,6,"d5f9d956",1,"kazimierzShip 0/2, sargonShip 0/8, soloShip 1/1, swiftShip 0/2",11,11,0,15]},
{"round":4,"ai_0":[1,27,8,0,1,2,8,"247bff16",4,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/3, swiftShip 0/4",11,11,0,23]},
{"round":3,"ai_0":[1,27,6,0,1,1,6,"d5f9d956",1,"kazimierzShip 0/2, sargonShip 0/8, soloShip 1/1, swiftShip 0/2",11,9,0,15]},
{"round":4,"ai_0":[1,27,8,0,1,2,8,"247bff16",4,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/3, swiftShip 0/4",11,9,0,23]},
{
"round": 5,
"ai_0": [1,27,9,0,0,3,8,"1ca911b7",11,"kazimierzShip 0/8, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/6, soloShip 1/1, steadShip 1/4, suntShip 1/0, swiftShip 0/8",21,21,0,32]
},
{
"round": 6,
"ai_0": [1,27,9,0,0,3,8,"3b1cee08",40,"kazimierzShip 0/8, kjeragShip 0/2, raidShip 1/15, sargonShip 0/8, siracusaShip 1/17, soloShip 1/1, steadShip 1/7, suntShip 1/0, swiftShip 0/8",56,56,0,41]
"ai_0": [1,27,9,0,0,3,8,"3b1cee08",40,"kazimierzShip 0/8, kjeragShip 0/2, raidShip 1/15, sargonShip 0/8, siracusaShip 1/17, soloShip 1/1, steadShip 1/7, suntShip 1/0, swiftShip 0/8",56,17,0,41]
},
{
"round": 7,
@@ -435,7 +435,7 @@
},
{
"round": 13,
"ai_0": [1,3,12,0,0,6,8,"c27bbe57",455,"arcaneShip 1/6, kazimierzShip 1/116, kjeragShip 0/2, miraShip 1/3, preciShip 1/68, raidShip 1/147, sargonShip 0/8, siracusaShip 1/81, soloShip 1/1, steadShip 0/12, suntShip 1/0, swiftShip 1/33",110,115,5,125]
"ai_0": [1,3,12,0,0,6,8,"c27bbe57",455,"arcaneShip 1/6, kazimierzShip 1/116, kjeragShip 0/2, miraShip 1/3, preciShip 1/68, raidShip 1/147, sargonShip 0/8, siracusaShip 1/81, soloShip 1/1, steadShip 0/12, suntShip 1/0, swiftShip 1/33",110,35,5,125]
}
],
"players": {
@@ -502,7 +502,7 @@
},
{
"round": 11,
"ai_0": [0,0,16,0,0,5,0,"811c9dc5",0,"arcaneShip 0/2, egirShip 0/60, kazimierzShip 0/2, kjeragShip 0/8, lateranoShip 0/45, preciShip 0/2, raidShip 0/18, sargonShip 0/8, steadShip 0/16, swiftShip 0/5, visiShip 0/38",18,28,10,107]
"ai_0": [0,0,16,0,0,5,0,"811c9dc5",0,"arcaneShip 0/2, egirShip 0/60, kazimierzShip 0/2, kjeragShip 0/8, lateranoShip 0/45, preciShip 0/2, raidShip 0/18, sargonShip 0/8, steadShip 0/16, swiftShip 0/5, visiShip 0/38",18,24,10,107]
}
],
"players": {
@@ -548,8 +548,8 @@
},
{
"round": 3,
"ai_0": [1,45,6,0,0,1,6,"cb2c7b2a",3,"preciShip 0/10, sargonShip 0/10, siracusaShip 0/3, soloShip 1/3, suntShip 1/0, swiftShip 1/0",19,19,0,15],
"ai_1": [1,28,6,0,0,1,7,"b7cd12ed",2,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/12, sargonShip 0/10, soloShip 1/0, swiftShip 1/2",19,19,0,15]
"ai_0": [1,45,6,0,0,1,6,"cb2c7b2a",3,"preciShip 0/10, sargonShip 0/10, siracusaShip 0/3, soloShip 1/3, suntShip 1/0, swiftShip 1/0",19,6,0,15],
"ai_1": [1,28,6,0,0,1,7,"b7cd12ed",2,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/12, sargonShip 0/10, soloShip 1/0, swiftShip 1/2",19,6,0,15]
},
{
"round": 4,
@@ -593,8 +593,8 @@
},
{
"round": 12,
"ai_0": [1,37,12,0,0,6,8,"e51a7992",183,"arcaneShip 1/26, egirShip 1/82, preciShip 0/10, raidShip 1/7, sargonShip 0/10, siracusaShip 1/32, skillfulShip 0/8, soloShip 1/6, suntShip 1/0, swiftShip 1/30",103,105,2,114],
"ai_1": [1,16,12,0,0,6,8,"c13a70f2",199,"arcaneShip 2/22, indomShip 0/2, kazimierzShip 1/62, kjeragShip 0/10, preciShip 2/88, sargonShip 0/10, soloShip 1/0, steadShip 0/9, suntShip 1/0, swiftShip 1/27",100,105,5,111]
"ai_0": [1,37,12,0,0,6,8,"e51a7992",183,"arcaneShip 1/26, egirShip 1/82, preciShip 0/10, raidShip 1/7, sargonShip 0/10, siracusaShip 1/32, skillfulShip 0/8, soloShip 1/6, suntShip 1/0, swiftShip 1/30",103,32,2,114],
"ai_1": [1,16,12,0,0,6,8,"c13a70f2",199,"arcaneShip 2/22, indomShip 0/2, kazimierzShip 1/62, kjeragShip 0/10, preciShip 2/88, sargonShip 0/10, soloShip 1/0, steadShip 0/9, suntShip 1/0, swiftShip 1/27",100,32,5,111]
},
{
"round": 13,
@@ -659,8 +659,8 @@
},
{
"round": 3,
"ai_0": [1,25,6,0,1,1,7,"24189309",0,"kazimierzShip 0/2, kjeragShip 0/2, soloShip 1/0, steadShip 0/2, swiftShip 0/2",20,20,0,15],
"ai_1": [1,26,6,0,1,1,7,"000cdad8",0,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/2, swiftShip 0/2",20,20,0,15]
"ai_0": [1,25,6,0,1,1,7,"24189309",0,"kazimierzShip 0/2, kjeragShip 0/2, soloShip 1/0, steadShip 0/2, swiftShip 0/2",20,7,0,15],
"ai_1": [1,26,6,0,1,1,7,"000cdad8",0,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/2, swiftShip 0/2",20,7,0,15]
},
{
"round": 4,
@@ -766,27 +766,27 @@
},
{
"round": 2,
"ai_0": [1,23,5,0,0,1,4,"a6e72808",0,"",18,18,0,9],
"ai_1": [1,45,5,0,0,1,4,"0fb6b61b",5,"kazimierzShip 0/5, swiftShip 1/5",18,18,0,9],
"ai_0": [1,23,5,0,0,1,4,"a6e72808",0,"",18,6,0,9],
"ai_1": [1,45,5,0,0,1,4,"0fb6b61b",5,"kazimierzShip 0/5, swiftShip 1/5",18,6,0,9],
"ai_2": [1,27,0,0,0,1,0,"811c9dc5",0,"",0,6,6,0]
},
{
"round": 3,
"ai_0": [1,23,6,0,2,1,7,"205b9f2b",4,"deputShip 1/4",10,10,0,15],
"ai_1": [1,45,6,0,1,1,6,"81325be0",7,"kazimierzShip 0/5, swiftShip 1/7",12,12,0,15],
"ai_1": [1,45,6,0,1,1,6,"81325be0",7,"kazimierzShip 0/5, swiftShip 1/7",12,10,0,15],
"ai_2": [1,27,15,0,1,1,8,"ee62b7b8",2,"soloShip 1/2, yanShip 0/1",10,10,0,15]
},
{
"round": 4,
"ai_0": [1,23,9,0,2,2,8,"1b31669c",14,"deputShip 1/8, kazimierzShip 0/2, skillfulShip 1/2, swiftShip 1/4",11,12,1,24],
"ai_1": [1,45,8,0,1,2,8,"370b1623",11,"kazimierzShip 0/7, swiftShip 1/11, yanShip 0/2",14,14,0,23],
"ai_1": [1,45,8,0,1,2,8,"370b1623",11,"kazimierzShip 0/7, swiftShip 1/11, yanShip 0/2",14,12,0,23],
"ai_2": [1,27,8,0,1,2,8,"80fcb6a2",4,"siracusaShip 1/0, soloShip 1/4, yanShip 0/1",12,12,0,23]
},
{
"round": 5,
"ai_0": [1,23,10,0,0,2,8,"be84400f",34,"deputShip 1/12, indomShip 1/3, kazimierzShip 1/7, skillfulShip 0/5, soloShip 1/2, suntShip 1/2, swiftShip 1/8",53,53,0,34],
"ai_1": [1,45,9,0,0,3,8,"1f27df00",13,"kazimierzShip 0/7, swiftShip 1/13, yanShip 0/2",53,53,0,32],
"ai_2": [1,27,9,0,0,3,8,"9bdbc0d9",12,"deputShip 1/2, siracusaShip 1/2, soloShip 1/8, yanShip 0/1",49,53,4,32]
"ai_0": [1,23,10,0,0,2,8,"be84400f",34,"deputShip 1/12, indomShip 1/3, kazimierzShip 1/7, skillfulShip 0/5, soloShip 1/2, suntShip 1/2, swiftShip 1/8",53,16,0,34],
"ai_1": [1,45,9,0,0,3,8,"1f27df00",13,"kazimierzShip 0/7, swiftShip 1/13, yanShip 0/2",53,16,0,32],
"ai_2": [1,27,9,0,0,3,8,"9bdbc0d9",12,"deputShip 1/2, siracusaShip 1/2, soloShip 1/8, yanShip 0/1",49,16,4,32]
},
{
"round": 6,
@@ -808,9 +808,9 @@
},
{
"round": 9,
"ai_0": [1,3,12,0,0,4,8,"e73e84c6",170,"deputShip 2/24, indomShip 1/29, kazimierzShip 1/29, siracusaShip 0/24, skillfulShip 0/19, soloShip 1/14, suntShip 1/14, swiftShip 1/60",32,48,16,76],
"ai_1": [1,25,12,0,0,4,8,"fec52ed0",54,"arcaneShip 1/17, indomShip 1/18, kazimierzShip 0/7, siracusaShip 1/12, soloShip 1/0, suntShip 1/0, swiftShip 0/17, victoriaShip 1/7, visiShip 0/4, yanShip 0/2",19,43,24,76],
"ai_2": [1,15,12,0,0,4,8,"549a87ca",142,"deputShip 1/12, indomShip 1/12, siracusaShip 1/88, skillfulShip 1/2, soloShip 1/18, suntShip 1/10, swiftShip 0/6, yanShip 0/1",52,59,7,80]
"ai_0": [1,3,12,0,0,4,8,"e73e84c6",170,"deputShip 2/24, indomShip 1/29, kazimierzShip 1/29, siracusaShip 0/24, skillfulShip 0/19, soloShip 1/14, suntShip 1/14, swiftShip 1/60",32,22,16,76],
"ai_1": [1,25,12,0,0,4,8,"fec52ed0",54,"arcaneShip 1/17, indomShip 1/18, kazimierzShip 0/7, siracusaShip 1/12, soloShip 1/0, suntShip 1/0, swiftShip 0/17, victoriaShip 1/7, visiShip 0/4, yanShip 0/2",19,22,24,76],
"ai_2": [1,15,12,0,0,4,8,"549a87ca",142,"deputShip 1/12, indomShip 1/12, siracusaShip 1/88, skillfulShip 1/2, soloShip 1/18, suntShip 1/10, swiftShip 0/6, yanShip 0/1",52,22,7,80]
},
{
"round": 10,
@@ -1072,14 +1072,14 @@
{
"round": 3,
"ai_0": [1,45,6,0,1,1,7,"a7d37d87",4,"arcaneShip 0/2, kazimierzShip 0/4, preciShip 0/2, swiftShip 1/4",9,9,0,15],
"ai_1": [1,26,6,0,1,1,7,"bf8932c2",5,"kazimierzShip 0/5, kjeragShip 0/2, sargonShip 0/4, soloShip 1/0, steadShip 0/2, swiftShip 1/5",11,11,0,15],
"ai_1": [1,26,6,0,1,1,7,"bf8932c2",5,"kazimierzShip 0/5, kjeragShip 0/2, sargonShip 0/4, soloShip 1/0, steadShip 0/2, swiftShip 1/5",11,9,0,15],
"ai_2": [1,28,6,0,1,1,6,"7363c3d8",0,"deputShip 0/4, kazimierzShip 0/2, kjeragShip 0/8, soloShip 1/0, steadShip 0/4, swiftShip 0/2, victoriaShip 0/8, yanShip 0/1",9,9,0,15],
"ai_3": [1,26,6,0,2,1,7,"5051466d",1,"arcaneShip 0/2, preciShip 0/2, sargonShip 0/2, soloShip 1/1",9,9,0,15]
},
{
"round": 4,
"ai_0": [1,45,8,0,1,2,8,"822a03c7",6,"arcaneShip 0/8, kazimierzShip 0/4, preciShip 0/8, swiftShip 1/6",10,10,0,23],
"ai_1": [1,26,8,0,1,2,8,"1d09144d",12,"arcaneShip 1/2, kazimierzShip 0/5, kjeragShip 0/2, preciShip 0/2, sargonShip 0/4, soloShip 1/0, steadShip 1/2, swiftShip 1/8",12,12,0,23],
"ai_1": [1,26,8,0,1,2,8,"1d09144d",12,"arcaneShip 1/2, kazimierzShip 0/5, kjeragShip 0/2, preciShip 0/2, sargonShip 0/4, soloShip 1/0, steadShip 1/2, swiftShip 1/8",12,10,0,23],
"ai_2": [1,28,8,0,1,2,8,"38880204",9,"deputShip 1/7, kazimierzShip 0/2, kjeragShip 0/10, soloShip 1/2, steadShip 0/4, swiftShip 0/2, victoriaShip 0/10, yanShip 0/4",10,10,0,22],
"ai_3": [1,26,9,0,2,2,8,"654ed0cf",10,"arcaneShip 1/5, deputShip 1/2, preciShip 0/2, sargonShip 0/2, soloShip 1/3",9,10,1,24]
},
@@ -1240,7 +1240,7 @@
},
{
"round": 6,
"ai_0": [1,29,9,0,3,3,8,"41bbfcb2",32,"arcaneShip 0/2, deputShip 1/2, kjeragShip 0/2, preciShip 1/4, siracusaShip 1/12, soloShip 1/14, steadShip 0/2",25,25,0,39],
"ai_0": [1,29,9,0,3,3,8,"41bbfcb2",32,"arcaneShip 0/2, deputShip 1/2, kjeragShip 0/2, preciShip 1/4, siracusaShip 1/12, soloShip 1/14, steadShip 0/2",25,23,0,39],
"ai_1": [1,28,9,0,1,3,8,"ee24046e",11,"arcaneShip 0/2, kjeragShip 0/9, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/4",20,22,2,37]
},
{
@@ -1334,11 +1334,11 @@
},
{
"round": 3,
"ai_0": [1,400,6,0,1,1,6,"193fde59",603,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/308, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/300, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/303, steadShip 1/300, suntShip 0/300, swiftShip 0/308, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,10,0,15]
"ai_0": [1,400,6,0,1,1,6,"193fde59",603,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/308, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/300, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/303, steadShip 1/300, suntShip 0/300, swiftShip 0/308, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,8,0,15]
},
{
"round": 4,
"ai_0": [1,400,8,0,1,2,8,"1fa2c5ec",603,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/308, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/303, steadShip 1/300, suntShip 0/300, swiftShip 0/308, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,10,0,22]
"ai_0": [1,400,8,0,1,2,8,"1fa2c5ec",603,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/308, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/303, steadShip 1/300, suntShip 0/300, swiftShip 0/308, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,8,0,22]
},
{
"round": 5,
@@ -1370,7 +1370,7 @@
},
{
"round": 12,
"ai_0": [1,399,14,0,0,6,8,"bc6cba50",2478,"arcaneShip 1/310, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 1/300, investShip 0/300, kazimierzShip 1/308, kjeragShip 0/310, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/314, raidShip 0/300, sargonShip 0/300, siracusaShip 0/306, skillfulShip 0/316, soloShip 1/307, steadShip 1/310, suntShip 1/300, swiftShip 1/329, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",115,115,0,112]
"ai_0": [1,399,14,0,0,6,8,"bc6cba50",2478,"arcaneShip 1/310, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 1/300, investShip 0/300, kazimierzShip 1/308, kjeragShip 0/310, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/314, raidShip 0/300, sargonShip 0/300, siracusaShip 0/306, skillfulShip 0/316, soloShip 1/307, steadShip 1/310, suntShip 1/300, swiftShip 1/329, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",115,32,0,112]
},
{
"round": 13,
@@ -1663,12 +1663,12 @@
},
{
"round": 6,
"h_0": [1,27,9,0,3,5,8,"8d6a5be3",70,"preciShip 0/2, raidShip 1/16, sargonShip 1/36, steadShip 1/2, victoriaShip 1/6, visiShip 1/10",24,24,0,33],
"h_0": [1,27,9,0,3,5,8,"8d6a5be3",70,"preciShip 0/2, raidShip 1/16, sargonShip 1/36, steadShip 1/2, victoriaShip 1/6, visiShip 1/10",24,22,0,33],
"ai_0": [1,28,9,0,1,3,8,"546cd677",6,"kjeragShip 0/2, sargonShip 0/2, siracusaShip 1/4, soloShip 1/0, steadShip 1/2, suntShip 1/0, yanShip 0/8",22,22,0,38]
},
{
"round": 7,
"h_0": [1,27,13,0,1,5,8,"ccd092d2",106,"preciShip 0/2, raidShip 1/22, sargonShip 1/50, steadShip 1/3, victoriaShip 1/14, visiShip 1/17",28,28,0,47],
"h_0": [1,27,13,0,1,5,8,"ccd092d2",106,"preciShip 0/2, raidShip 1/22, sargonShip 1/50, steadShip 1/3, victoriaShip 1/14, visiShip 1/17",28,26,0,47],
"ai_0": [1,28,11,0,1,4,8,"483ff3a4",4,"kjeragShip 0/2, sargonShip 0/2, siracusaShip 1/4, skillfulShip 1/0, soloShip 1/0, steadShip 0/2, suntShip 1/0, yanShip 0/8",26,26,0,50]
},
{
+69 -69
View File
@@ -79,12 +79,12 @@
"ticks": 3300,
"reason": "timeout",
"killed": 29,
"total": 44,
"total": 43,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 88,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":29,"total":44,"perfect":false,"leaks":15,"leaksUncounted":0,"coins":8,"dmg":264848,"bossDmg":0,"heal":6152,"deaths":14,"layerGains":{}}},
"players": {"p1":{"killed":29,"total":43,"perfect":false,"leaks":15,"leaksUncounted":0,"coins":8,"dmg":264848,"bossDmg":0,"heal":6152,"deaths":14,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_1_02_b",1,"uniequip_002_yak",3645.23,1,63,2,9693,5979,0,26,1],
["p1",2,"op","chess_char_1_20_a",1,null,2857.4,1,1,0,0,0,0,0,0],
@@ -121,7 +121,7 @@
"hooks": [1,64,3300,376,376,473,517,3286,43,54,16,18,48,7606,34,12,44,11,1,44,0,0,0],
"events": {"atk":398,"bounty":4,"deploy":20,"die":43,"dmg":517,"engage":10,"fx":83,"heal":36,"leak":11,"skill":37,"spawn":56,"status":68},
"fx": {"aoe":5,"counter":14,"crit":23,"displace":10,"dp":3,"ember":1,"pulse":22,"revive":1,"sp":1,"summon":1,"taunt":2},
"snaps": "e780a460 61ee47ae bdf76d96 06fbd6a7 1037bf32 a42ebfa2 12a813ce ef2d4564 747331aa a207b3fb d814a43b"
"snaps": "e780a460 61ee47ae bdf76d96 06fbd6a7 1037bf32 f06a06c4 cbd23de7 17d297e3 95dffa16 fe0088d3 7c8c9766"
},
"roster-003": {
"about": "pass 0 band 2: chess_char_1_10_b chess_char_1_12_b chess_char_1_20_b chess_char_1_18_b chess_char_1_19_b chess_char_1_13_b chess_char_1_14_b chess_char_1_17_b chess_char_2_01_a chess_char_2_02_a chess_char_2_04_a chess_char_2_05_a token_10000_silent_healrb",
@@ -136,13 +136,13 @@
"ticks": 3300,
"reason": "timeout",
"killed": 83,
"total": 85,
"total": 34,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 59,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":83,"total":85,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":14,"dmg":119766,"bossDmg":0,"heal":20323,"deaths":3,"layerGains":{"preciShip":3}}
"p1": {"killed":83,"total":34,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":14,"dmg":119766,"bossDmg":0,"heal":20323,"deaths":3,"layerGains":{"preciShip":3}}
},
"units": [
["p1",1,"op","chess_char_1_10_b",0,"uniequip_002_sunbr",2493,1,5,0,0,0,288,2,2],
@@ -184,7 +184,7 @@
"hooks": [1,105,3300,579,579,660,773,241,86,90,20,19,0,15985,207,29,85,0,1,92,1,0,0],
"events": {"atk":656,"bounty":7,"deploy":17,"die":90,"dmg":749,"engage":9,"fx":101,"heal":212,"layer":1,"skill":39,"spawn":101,"status":228},
"fx": {"aoe":1,"blink":1,"displace":8,"ember":1,"explode":9,"garrison":1,"heal":2,"phase":1,"pulse":41,"reveal":1,"sleep":7,"summon":25,"undying":1,"zone":2},
"snaps": "ffe12829 556d50e1 da7db459 81849f4b 4e232e69 eedbef98 c39ed31b ac57d325 1cfe2ef9 4afd989a 6671745c"
"snaps": "23086eb2 62198379 4013be03 54f09692 fac5503d 7def8563 16aff8bb 1c932c52 15f57f1a 500e8654 2f228080"
},
"roster-004": {
"about": "pass 0 band 2: chess_char_2_08_a chess_char_2_17_a chess_char_2_18_a chess_char_2_07_a chess_char_2_09_a chess_char_2_11_a chess_char_2_13_a chess_char_2_16_a chess_char_2_12_a chess_char_2_06_a chess_char_2_10_a chess_char_2_14_a",
@@ -386,12 +386,12 @@
"ticks": 4200,
"reason": "timeout",
"killed": 110,
"total": 163,
"total": 71,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 63,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":110,"total":163,"perfect":false,"leaks":53,"leaksUncounted":1,"coins":4,"dmg":209618,"bossDmg":0,"heal":1552,"deaths":11,"layerGains":{}}},
"players": {"p1":{"killed":110,"total":71,"perfect":false,"leaks":53,"leaksUncounted":1,"coins":4,"dmg":209618,"bossDmg":0,"heal":1552,"deaths":11,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_3_12_a",2,null,2334,1,185,2,2951,2334,989,14,1],
["p1",2,"op","chess_char_3_16_a",1,null,2173,1,189,0,455,3725,564,1,1],
@@ -434,7 +434,7 @@
"hooks": [1,189,4200,602,602,858,862,392,122,176,30,29,0,18633,16,36,164,53,1,129,0,0,0],
"events": {"atk":734,"bounty":2,"deploy":23,"die":123,"dmg":852,"engage":14,"fx":133,"heal":2,"leak":53,"skill":59,"spawn":180,"status":55},
"fx": {"aoe":2,"camouflage":1,"displace":22,"dp":2,"explode":21,"phase":1,"pull":22,"revive":2,"sp":1,"strike":3,"summon":44,"volley":7,"wolfShadow":2,"wolfShadowLost":3},
"snaps": "b9af8173 9dbbce46 0545bdc2 a248c945 96d564d2 dfdab949 5c15b625 9b9cfc78 8c803cc4 a8a11123 6ce4d37a 806b7f61 023220d2 bccf5abb"
"snaps": "8447aca6 0085b7c9 33307063 cd2bf975 e6c8eeb1 7331d165 340230f4 4e983e4a fd8f005c e400c6cd 0e8b1aa8 9330488b d2efc4c0 40e02a59"
},
"roster-008": {
"about": "pass 0 band 4: chess_char_3_06_b chess_char_3_12_b chess_char_3_02_b chess_char_3_03_b chess_char_3_04_b chess_char_3_05_b chess_char_3_21_a chess_char_3_01_b chess_char_3_08_b chess_char_3_09_b chess_char_3_10_b chess_char_3_11_b",
@@ -449,7 +449,7 @@
"ticks": 5100,
"reason": "timeout",
"killed": 45,
"total": 87,
"total": 83,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 17,
@@ -457,7 +457,7 @@
"players": {
"p1": {
"killed": 45,
"total": 87,
"total": 83,
"perfect": false,
"leaks": 42,
"leaksUncounted": 1,
@@ -530,7 +530,7 @@
"hooks": [1,137,5100,539,539,1062,1157,387,60,130,21,23,0,8443,80,45,88,39,1,66,12,12,0],
"events": {"atk":624,"bounty":4,"deploy":49,"die":91,"dmg":1084,"engage":12,"fx":231,"heal":386,"layer":12,"leak":39,"skill":47,"spawn":128,"status":130},
"fx": {"aoe":20,"blink":3,"buff":1,"burst":12,"coin":29,"displace":32,"explode":34,"garrison":8,"layer":4,"pull":32,"revive":6,"shield":1,"shieldBreak":1,"strike":9,"summon":29,"telegraph":1,"volley":9},
"snaps": "17903402 6c8167ba 07381013 82bb886b 608b7ec4 249d7653 0d98c4e2 486693a5 84927b5d 4ec21382 2bb263b6 816b9d4f 845b7cfc 1702154b d531c7dc 0cff75b4 79d1c073"
"snaps": "17903402 6c8167ba 07381013 82bb886b 608b7ec4 49a9f161 e85b61cf 22016225 ee0925e6 4ee5eeb2 8a500100 aa9abb07 2ce0b42a fcb5b9cb d8b6506e b8cd651a b26cce51"
},
"roster-009": {
"about": "pass 0 band 4: chess_char_3_16_b chess_char_4_01_a chess_char_3_18_b chess_char_4_04_a chess_char_3_13_b chess_char_3_14_b chess_char_3_17_b chess_char_3_19_b chess_char_3_20_b chess_char_3_21_b chess_char_4_02_a chess_char_4_06_a token_10028_vigil_wolf",
@@ -545,12 +545,12 @@
"ticks": 4674,
"reason": "cleared",
"killed": 171,
"total": 171,
"total": 63,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 148,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":171,"total":171,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":12,"dmg":279620,"bossDmg":0,"heal":0,"deaths":1,"layerGains":{}}},
"players": {"p1":{"killed":171,"total":63,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":12,"dmg":279620,"bossDmg":0,"heal":0,"deaths":1,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_3_16_b",1,"uniequip_002_snakek",5363,1,3,0,0,0,0,0,0],
["p1",2,"op","chess_char_4_01_a",2,null,4563.74,1,6,0,0,0,0,0,0],
@@ -591,7 +591,7 @@
"hooks": [1,187,4674,713,713,1306,1290,0,172,172,63,61,0,21782,106,10,171,0,1,179,0,0,0],
"events": {"atk":886,"bounty":6,"deploy":14,"die":172,"dmg":1290,"engage":11,"fx":176,"skill":124,"spawn":186,"status":238},
"fx": {"camouflage":1,"crit":24,"displace":30,"dp":5,"explode":4,"phase":1,"ripple":8,"shadowWeave":24,"shield":2,"stealth":4,"summon":51,"telegraph":6,"volley":8,"wolfShadow":4,"wolfShadowLost":3,"zone":1},
"snaps": "aea843d4 76d68647 dede7c8e 5e9b6d5c 2a73c288 94334408 2debb788 3158593d c2e8e21e 178efcca 33143e68 c91245db 8d2f8f1e 0134b665 955f8917 a00328a1"
"snaps": "69a7f9d4 a676cad6 57a90446 c208123b ae74e9b8 1b323369 8b5283e9 4c70fd14 717d3193 f69fe3aa 1e6c1532 ddb7d885 f774a464 57e74ab4 20bd8a39 ac5fa54d"
},
"roster-010": {
"about": "pass 0 band 4: chess_char_4_17_a chess_char_4_18_a chess_char_4_11_a chess_char_4_19_a chess_char_4_07_a chess_char_4_16_a chess_char_4_09_a chess_char_4_10_a chess_char_4_12_a chess_char_4_13_a chess_char_4_14_a chess_char_4_20_a token_10041_cathy_catsld token_10041_cathy_catsld",
@@ -606,13 +606,13 @@
"ticks": 5100,
"reason": "timeout",
"killed": 45,
"total": 102,
"total": 69,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 99,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":45,"total":102,"perfect":false,"leaks":57,"leaksUncounted":0,"coins":4,"dmg":442763,"bossDmg":0,"heal":3335,"deaths":23,"layerGains":{"preciShip":12}}
"p1": {"killed":45,"total":69,"perfect":false,"leaks":57,"leaksUncounted":0,"coins":4,"dmg":442763,"bossDmg":0,"heal":3335,"deaths":23,"layerGains":{"preciShip":12}}
},
"units": [
["p1",1,"op","chess_char_4_17_a",1,null,0,0,5,0,3672,2849,0,7,0],
@@ -654,7 +654,7 @@
"hooks": [1,132,5100,457,457,1127,1077,332,71,119,25,26,0,8109,376,12,102,51,1,200,3,0,41],
"events": {"atk":557,"bounty":2,"deploy":27,"die":68,"dmg":985,"engage":12,"fx":195,"heal":7,"layer":3,"leak":51,"skill":52,"spawn":119,"status":335},
"fx": {"block":9,"catShield":2,"charge":1,"dilemma":1,"dodge":41,"explode":18,"explosion":2,"featherArrow":1,"flame":1,"frostNova":4,"garrison":3,"garrisonGrant":1,"overclock":1,"phase":1,"pull":1,"riposte":2,"rockslide":4,"shieldBreak":5,"slippery":1,"summon":11,"swordRain":46,"swordStorm":8,"telegraph":2,"thorns":16,"tornado":1,"tornadoPulse":5,"undying":7},
"snaps": "06e15d5c 9c6e5b81 2ecc1e6a 71fee850 b79ab0d2 457ad226 ff2b9874 4cc2db7e f0f9ed59 2f89b40a aad31065 cbcace62 0b121c2c 860ca274 3c5123e2 f265b649 88aa7607"
"snaps": "06e15d5c 9c6e5b81 2ecc1e6a 71fee850 b79ab0d2 3601a029 af7e3130 8290988c ef603826 41e4badb 232f36b3 92680cda 18b57768 3253586c 584ddab4 65fdbd7c 43bb928d"
},
"roster-011": {
"about": "pass 0 band 5: chess_char_4_23_a chess_char_4_01_b chess_char_4_22_a chess_char_4_04_b chess_char_4_07_b chess_char_4_09_b chess_char_4_21_a chess_char_4_24_a chess_char_4_25_a chess_char_4_26_a chess_char_4_02_b chess_char_4_06_b",
@@ -669,13 +669,13 @@
"ticks": 5700,
"reason": "timeout",
"killed": 78,
"total": 82,
"total": 78,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 231,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":78,"total":82,"perfect":false,"leaks":4,"leaksUncounted":0,"coins":12,"dmg":698950,"bossDmg":0,"heal":15928,"deaths":5,"layerGains":{"visiShip":8}}
"p1": {"killed":78,"total":78,"perfect":false,"leaks":4,"leaksUncounted":0,"coins":12,"dmg":698950,"bossDmg":0,"heal":15928,"deaths":5,"layerGains":{"visiShip":8}}
},
"units": [
["p1",1,"op","chess_char_4_23_a",2,null,4358,1,99,11,140344,15336,0,111,3],
@@ -714,7 +714,7 @@
"hooks": [1,101,5700,972,972,2317,2716,1581,83,83,48,44,30,29691,498,34,82,0,1,84,2,0,1],
"events": {"atk":1788,"bounty":6,"deploy":17,"die":83,"dmg":2486,"engage":9,"fx":280,"heal":79,"layer":2,"skill":92,"spawn":96,"status":1157},
"fx": {"camouflage":2,"charge":17,"counter":30,"crit":38,"dilemma":6,"displace":7,"dodge":1,"explode":1,"garrison":2,"healField":2,"jungleSoul":3,"mote":26,"quadShot":1,"redistribute":30,"revive":1,"reweave":2,"ripple":15,"sandChains":5,"sermon":1,"shadowWeave":35,"shield":4,"slash":36,"stealth":6,"summon":1,"telegraph":6,"truesilver":2},
"snaps": "4ec6c58b 31b6e4f7 b3a1832d 1d3aef5f fadc09c0 7c30f46f 4b78bbe9 86d9c187 89912cdc 65c1ecc2 769a2481 b2b9d6bb 1247c905 a2a0f97e ad63e000 ffd73b97 41a6748a 0b473fca 661ca1b1"
"snaps": "4ec6c58b 31b6e4f7 b3a1832d 1d3aef5f a87a8d54 03884138 51cdef78 9ed2fd94 6dc7967a 5abbf904 1c8c11b7 1d22075e 459d6f35 c29e281e 36befd7b 59fdbfb6 ba6c43fa eafb4ef7 2ce67130"
},
"roster-012": {
"about": "pass 0 band 5: chess_char_4_17_b chess_char_4_18_b chess_char_4_11_b chess_char_4_19_b chess_char_4_22_b chess_char_4_16_b chess_char_4_10_b chess_char_4_12_b chess_char_4_13_b chess_char_4_14_b chess_char_4_20_b chess_char_4_21_b token_10041_cathy_catsld token_10041_cathy_catsld",
@@ -729,13 +729,13 @@
"ticks": 4847,
"reason": "cleared",
"killed": 99,
"total": 100,
"total": 96,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 175,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":99,"total":100,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":10,"dmg":998389,"bossDmg":0,"heal":23063,"deaths":10,"layerGains":{"preciShip":8}}
"p1": {"killed":99,"total":96,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":10,"dmg":998389,"bossDmg":0,"heal":23063,"deaths":10,"layerGains":{"preciShip":8}}
},
"units": [
["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3473.8,1,7,0,296,39,0,19,0],
@@ -777,7 +777,7 @@
"hooks": [1,125,4847,485,485,1427,1424,821,110,113,83,82,0,17042,1514,29,100,1,1,119,1,0,3],
"events": {"atk":636,"bounty":5,"deploy":22,"die":112,"dmg":1415,"engage":10,"fx":275,"heal":212,"layer":1,"leak":1,"skill":167,"spawn":117,"status":1018},
"fx": {"beam":6,"blink":2,"block":2,"displace":11,"dodge":3,"explode":6,"explosion":23,"featherArrow":4,"flame":3,"frostNova":35,"garrison":1,"garrisonGrant":1,"healField":2,"overclock":1,"palsy":1,"pull":2,"revive":4,"riposte":2,"rockslide":4,"shieldBreak":9,"slash":30,"summon":1,"swordRain":28,"swordStorm":6,"telegraph":3,"thorns":1,"tornado":2,"tornadoPulse":10,"truesilver":3,"zone":69},
"snaps": "ccfbde35 4d2aba8a 8dfe85b0 528967cd 3dbb0c86 c73d7931 d025c97a c9dab1f0 ecebc7c3 77ac09b9 fe0c30b6 3bcdfc52 7d4f4208 bdb5d1fa 39999c60 d73cc178 9d069a0a"
"snaps": "ccfbde35 4d2aba8a 8dfe85b0 528967cd 3dbb0c86 c06ed10c 2e96bd48 7b9b35a5 b7b36032 323ec922 4199cd1a 0940cade b87b745a ff75d11f 64cca2ce b9bb5bac fc4d294c"
},
"roster-013": {
"about": "pass 0 band 5: chess_char_4_23_b chess_char_5_08_a chess_char_5_01_a chess_char_5_05_a chess_char_5_06_a chess_char_5_07_a chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b chess_char_5_02_a chess_char_5_03_a chess_char_5_10_a",
@@ -792,7 +792,7 @@
"ticks": 5645,
"reason": "cleared",
"killed": 99,
"total": 104,
"total": 102,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 53,
@@ -800,7 +800,7 @@
"players": {
"p1": {
"killed": 99,
"total": 104,
"total": 102,
"perfect": false,
"leaks": 5,
"leaksUncounted": 0,
@@ -848,7 +848,7 @@
"hooks": [1,124,5645,628,628,1890,2641,19730,108,114,26,25,18,27889,1089,33,110,5,1,163,29,2,0],
"events": {"atk":1099,"bounty":5,"deploy":14,"die":109,"dmg":2218,"engage":8,"fx":297,"heal":65,"layer":29,"leak":5,"skill":51,"spawn":122,"status":801},
"fx": {"anchor":3,"aoe":1,"bandage":2,"beam":3,"burst":2,"candle":6,"downed":1,"ember":1,"explode":1,"garrison":29,"garrisonGrant":1,"jungleSoul":2,"mote":96,"overload":2,"redistribute":40,"revive":1,"reweave":3,"sandChains":3,"sandChainsCharged":1,"spGift":44,"splash":18,"summon":1,"telegraph":1,"wake":35},
"snaps": "14f005fc 39387feb a7349b84 20afb02c a4a1bc3c b2ee7185 6c44cc59 e221af55 b857607b 43c6569b 5c18ca91 7353ffa7 c2202075 ba12516c 0b0ebd92 57f918d9 fc6e30e4 df0b767f 1f363bf1"
"snaps": "14f005fc 39387feb a7349b84 20afb02c d0ced437 9221ce4b f3059442 d779613c b02261a1 aa86888c 027f54e2 c5539bb8 ca44f291 62ae4698 8893fcdd c4700cc1 6fe6886f 682dbfd2 35a8b2dc"
},
"roster-014": {
"about": "pass 0 band 6: chess_char_5_11_a chess_char_5_19_a chess_char_5_13_a chess_char_5_14_a chess_char_5_01_b chess_char_5_17_a chess_char_5_12_a chess_char_5_15_a chess_char_5_20_a chess_char_5_21_a chess_char_5_22_a chess_char_5_23_a",
@@ -936,7 +936,7 @@
"ticks": 5425,
"reason": "cleared",
"killed": 107,
"total": 107,
"total": 105,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 200,
@@ -944,7 +944,7 @@
"players": {
"p1": {
"killed": 107,
"total": 107,
"total": 105,
"perfect": true,
"leaks": 0,
"leaksUncounted": 0,
@@ -1002,7 +1002,7 @@
"hooks": [1,142,5425,343,343,2083,2705,4099,110,120,153,151,0,15568,4219,52,110,0,1,225,2,0,0],
"events": {"atk":402,"bounty":6,"deploy":29,"die":120,"dmg":2098,"engage":10,"fx":465,"heal":1553,"layer":2,"skill":304,"spawn":131,"status":720},
"fx": {"anchor":6,"aoe":45,"candle":3,"ember":2,"garrison":2,"healAoe":79,"overload":4,"revive":1,"sp":101,"substitute":1,"summon":3,"swap":1,"telegraph":1,"teleport":9,"zone":207},
"snaps": "22cf0582 8a0c0b85 516b60a1 84aa4163 e05892d0 18489b1b 648324df 80fac9f4 376b8bee 85dbe4cb e627378e d788a444 4b8ef6d8 e2cf34c7 e37bfa3f c7e3226a 4c5d887f 61f8d5f0"
"snaps": "22cf0582 8a0c0b85 516b60a1 c00ea058 dc8d82f9 9ea7f69d 76fe7830 9f220f9d 40bd8a20 94e08671 39d32dd9 963505f6 3e70ec19 de5a5755 6b3cf9e2 fe38d43e ecb66d06 6bd2841f"
},
"roster-016": {
"about": "pass 0 band 6: chess_char_5_19_b chess_char_6_03_a chess_char_6_06_a chess_char_5_17_b chess_char_5_20_b chess_char_5_21_b chess_char_5_22_b chess_char_5_23_b chess_char_6_01_a chess_char_6_02_a chess_char_6_04_a chess_char_6_05_a token_10017_skadi2_dedant",
@@ -1078,7 +1078,7 @@
"ticks": 5965,
"reason": "cleared",
"killed": 174,
"total": 174,
"total": 171,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 384,
@@ -1086,7 +1086,7 @@
"players": {
"p1": {
"killed": 174,
"total": 174,
"total": 171,
"perfect": true,
"leaks": 0,
"leaksUncounted": 0,
@@ -1135,7 +1135,7 @@
"hooks": [1,193,5965,895,895,1366,1742,349,184,186,46,43,123,35603,1303,179,174,0,1,193,3,3,0],
"events": {"atk":910,"bounty":6,"deploy":19,"die":186,"dmg":1518,"engage":11,"fx":201,"heal":138,"layer":3,"skill":89,"spawn":188,"status":918},
"fx": {"airstrike":33,"beam":6,"burst":3,"cleanse":3,"displace":1,"dp":3,"drone":50,"droneLock":21,"drones":1,"emergency":7,"explode":16,"finale":4,"garrison":3,"link":2,"scorchBurst":18,"slash":17,"steal":3,"summon":3,"sunBurst":2,"talent":3,"undying":2},
"snaps": "43b03df4 4fcc619d 2be2121d 28e92f35 ef487f7c 940fa13a 63b8128f 2dbfde08 4bbc92c9 5ff16546 090219f9 b0cab1a7 1d90c054 357d876a f60e8ae5 ae2c1c51 a715ad2a 4c2e4416 9b88218f 226d981f"
"snaps": "43b03df4 4fcc619d 2be2121d b478fa2a 1c29c601 b33d14b7 0584e2d0 82b225f7 09241fe7 4f2805dd 3462bfac 3ffdbd77 73f9a044 2a84c2d8 8d905cc5 3ebb29ff 02a7318c 9bab956c 7db8bfe9 5cc46ec7"
},
"roster-018": {
"about": "pass 0 band 7: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_20_a 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 chess_char_6_12_b token_10017_skadi2_dedant token_10030_mlyss_wtrman",
@@ -1150,7 +1150,7 @@
"ticks": 5849,
"reason": "cleared",
"killed": 120,
"total": 120,
"total": 117,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 483,
@@ -1158,7 +1158,7 @@
"players": {
"p1": {
"killed": 120,
"total": 120,
"total": 117,
"perfect": true,
"leaks": 0,
"leaksUncounted": 0,
@@ -1217,7 +1217,7 @@
"hooks": [1,145,5849,513,513,1336,1487,3519,124,130,34,30,37,24659,381,5,122,0,1,124,11,6,0],
"events": {"atk":606,"bounty":6,"deploy":21,"die":130,"dmg":1407,"engage":12,"fx":260,"heal":185,"layer":11,"skill":64,"spawn":145,"status":385},
"fx": {"beam":4,"bombard":31,"bombardShell":31,"burst":6,"camouflage":2,"copy":1,"dp":4,"explode":1,"garrison":8,"lightning":87,"link":1,"lock":37,"lure":2,"scorchBurst":17,"snow":7,"split":4,"storm":7,"summon":2,"talent":1,"tide":4,"wanted":3},
"snaps": "19eb7843 2593e6ad ced96303 5ffc34ca 8cbd7231 1e7d8197 948c256c bd58965c b2457d86 151be715 19fcaae7 098875f8 0e3baca8 3c739f96 4210ffa1 2d576f76 35e8b3e6 e853bd87 a170c428 32d5be0d"
"snaps": "19eb7843 2593e6ad ced96303 9384a4db 45274713 75c80112 cd1ef1a1 6ff0d830 14dbf244 b55f9101 060019d3 8b4d624b 170d8186 4e4fbe70 f9844c3a d8a1becb eec26a32 99466fc4 2cf9d317 4ac77e1f"
},
"roster-019": {
"about": "pass 0 band 7: chess_char_6_15_b chess_char_6_19_b chess_char_5_01_b chess_char_5_05_b chess_char_6_17_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 chess_char_5_02_b chess_char_5_03_b",
@@ -1303,12 +1303,12 @@
"ticks": 2700,
"reason": "timeout",
"killed": 30,
"total": 43,
"total": 38,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 93,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":30,"total":43,"perfect":false,"leaks":13,"leaksUncounted":0,"coins":8,"dmg":169863,"bossDmg":0,"heal":20420,"deaths":6,"layerGains":{}}},
"players": {"p1":{"killed":30,"total":38,"perfect":false,"leaks":13,"leaksUncounted":0,"coins":8,"dmg":169863,"bossDmg":0,"heal":20420,"deaths":6,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_1_02_a",0,null,6437.42,1,3,0,1704,1216,282,11,1],
["p1",2,"op","chess_char_1_04_a",0,null,4318.46,1,5,3,8847,1618,0,36,1],
@@ -1341,7 +1341,7 @@
"hooks": [1,57,2700,527,526,673,721,5609,36,39,26,24,6,12672,50,35,43,3,1,36,0,3,0],
"events": {"atk":597,"bounty":4,"deploy":12,"die":36,"dmg":703,"engage":12,"fx":48,"heal":53,"leak":3,"skill":50,"spawn":57,"status":149},
"fx": {"buff":1,"burst":3,"crit":28,"dp":2,"explode":10,"summon":2,"telegraph":2},
"snaps": "7569fa79 a7c3130a 6e861c2b f81be7ba 7f7d11f4 b000e94a a637c5f7 adaab972 f533cb58"
"snaps": "7569fa79 a7c3130a 6e861c2b f81be7ba 7f7d11f4 1838edf3 c81da2a1 0cddd171 e575a9d9"
},
"roster-021": {
"about": "pass 1 band 2: chess_char_1_02_b chess_char_1_20_a chess_char_1_04_b chess_char_1_08_b chess_char_1_18_a chess_char_1_19_a chess_char_1_17_a chess_char_1_01_b chess_char_1_03_b chess_char_1_06_b chess_char_1_07_b chess_char_1_09_b",
@@ -1462,12 +1462,12 @@
"ticks": 3300,
"reason": "timeout",
"killed": 33,
"total": 36,
"total": 35,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":33,"total":36,"perfect":false,"leaks":3,"leaksUncounted":0,"coins":6,"dmg":156300,"bossDmg":0,"heal":21166,"deaths":4,"layerGains":{}}},
"players": {"p1":{"killed":33,"total":35,"perfect":false,"leaks":3,"leaksUncounted":0,"coins":6,"dmg":156300,"bossDmg":0,"heal":21166,"deaths":4,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_2_08_a",0,null,1209.08,1,7,3,11496,6440,380,12,1],
["p1",2,"op","chess_char_2_17_a",0,null,0,0,11,5,14348,6625,2659,15,3],
@@ -1502,7 +1502,7 @@
"hooks": [1,53,3300,397,397,859,960,3173,37,37,21,19,0,10920,67,46,36,0,1,37,0,3,0],
"events": {"atk":577,"bounty":3,"deploy":14,"die":37,"dmg":879,"engage":10,"fx":55,"heal":97,"skill":40,"spawn":51,"status":183},
"fx": {"beam":42,"buff":3,"burst":3,"explode":1,"summon":1,"takeoff":1,"telegraph":4},
"snaps": "ac6bb3ba c0659ec1 ae5b2e72 9a4892cf dcee00e3 e6820472 6acace6e 585a9080 190d8eac c4627eb2 95fb3d5c"
"snaps": "ac6bb3ba c0659ec1 ae5b2e72 9a4892cf dcee00e3 e6820472 6acace6e 585a9080 190d8eac 52c080f3 2bc23cb2"
},
"roster-024": {
"about": "pass 1 band 3: chess_char_2_07_b chess_char_2_08_b chess_char_2_09_b chess_char_2_11_b chess_char_2_12_b chess_char_2_19_a chess_char_2_01_b chess_char_2_02_b chess_char_2_04_b chess_char_2_05_b chess_char_2_06_b chess_char_2_10_b",
@@ -1517,12 +1517,12 @@
"ticks": 4200,
"reason": "timeout",
"killed": 167,
"total": 193,
"total": 67,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 11,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":167,"total":193,"perfect":false,"leaks":26,"leaksUncounted":0,"coins":6,"dmg":198262,"bossDmg":0,"heal":21090,"deaths":2,"layerGains":{}}},
"players": {"p1":{"killed":167,"total":67,"perfect":false,"leaks":26,"leaksUncounted":0,"coins":6,"dmg":198262,"bossDmg":0,"heal":21090,"deaths":2,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_2_07_b",0,"none",0,0,3,54,81792,7383,4670,60,2],
["p1",2,"op","chess_char_2_08_b",0,"none",3193,1,1,0,0,0,0,0,0],
@@ -1556,7 +1556,7 @@
"hooks": [1,205,4200,764,764,1112,1178,2273,169,192,67,65,0,13967,1101,46,193,23,1,176,0,0,0],
"events": {"atk":978,"bounty":3,"deploy":12,"die":169,"dmg":1114,"engage":8,"fx":79,"heal":146,"leak":23,"skill":132,"spawn":205,"status":533},
"fx": {"beam":3,"buff":5,"phase":1,"summon":58,"undying":1,"zone":11},
"snaps": "1fdc139e b67d37a5 27cdbfd2 91118bdf 5bc23ae1 dae0fe50 cdd53ba4 ff5c2f87 6658a90d 16089f28 0066b1b1 8273a045 1b52d504 45f8843e"
"snaps": "1fdc139e 5e66695d 23864921 b3d77d2d 9375efa4 7e27f254 3d501adb ccd8238b 93471fba 03de623d 3b85f08c 6b325e0e bfacea34 155ec05b"
},
"roster-025": {
"about": "pass 1 band 3: chess_char_2_17_b chess_char_2_18_b chess_char_3_06_a chess_char_2_13_b chess_char_2_16_b chess_char_3_02_a chess_char_3_03_a chess_char_3_04_a chess_char_3_05_a chess_char_2_14_b chess_char_2_19_b chess_char_3_01_a",
@@ -1571,13 +1571,13 @@
"ticks": 4200,
"reason": "timeout",
"killed": 65,
"total": 65,
"total": 64,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 154,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":65,"total":65,"perfect":true,"leaks":0,"leaksUncounted":1,"coins":6,"dmg":412510,"bossDmg":0,"heal":30454,"deaths":0,"layerGains":{"steadShip":1}}
"p1": {"killed":65,"total":64,"perfect":true,"leaks":0,"leaksUncounted":1,"coins":6,"dmg":412510,"bossDmg":0,"heal":30454,"deaths":0,"layerGains":{"steadShip":1}}
},
"units": [
["p1",1,"op","chess_char_2_17_b",0,"none",12144.76,1,4,0,0,4149,4149,0,6],
@@ -1612,7 +1612,7 @@
"hooks": [1,81,4200,876,876,1291,1295,8512,65,65,136,135,0,8514,337,96,66,0,1,123,1,0,0],
"events": {"atk":929,"bounty":3,"deploy":12,"die":65,"dmg":1295,"engage":8,"fx":177,"heal":230,"layer":1,"skill":273,"spawn":81,"status":249},
"fx": {"buff":1,"coin":2,"ember":58,"garrison":1,"revive":1,"sp":95,"summon":1,"takeoff":1,"zone":17},
"snaps": "93455703 0f041837 3bae9724 5e0316f5 8ed5e397 724be440 817d612f 08b77e5e 84166039 862c8938 f534d19b d3c927ca de1e8a39 a7c4f443"
"snaps": "93455703 0f041837 3bae9724 5e0316f5 8ed5e397 724be440 6dbe253b d1c21d2a cf2cc6da feba90e7 fd2030a8 5e9f571d 0b4e33a8 06fbbefa"
},
"roster-026": {
"about": "pass 1 band 3: chess_char_3_12_a chess_char_3_16_a chess_char_3_18_a chess_char_3_08_a chess_char_3_09_a chess_char_3_10_a chess_char_3_11_a chess_char_3_13_a chess_char_3_14_a chess_char_3_17_a chess_char_3_19_a chess_char_3_20_a token_10028_vigil_wolf",
@@ -1627,12 +1627,12 @@
"ticks": 4200,
"reason": "timeout",
"killed": 18,
"total": 73,
"total": 71,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 164,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":18,"total":73,"perfect":false,"leaks":55,"leaksUncounted":0,"coins":6,"dmg":183879,"bossDmg":0,"heal":1883,"deaths":21,"layerGains":{}}},
"players": {"p1":{"killed":18,"total":71,"perfect":false,"leaks":55,"leaksUncounted":0,"coins":6,"dmg":183879,"bossDmg":0,"heal":1883,"deaths":21,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_3_12_a",0,null,0,0,96,1,7787,4668,1883,19,4],
["p1",2,"op","chess_char_3_16_a",0,null,0,0,6,1,3499,2173,0,12,1],
@@ -1669,7 +1669,7 @@
"hooks": [1,102,4200,383,383,1447,1501,12,40,94,30,30,0,9243,22,42,73,53,1,43,0,0,0],
"events": {"atk":436,"bounty":3,"deploy":26,"die":41,"dmg":1499,"engage":14,"fx":27,"heal":11,"leak":53,"skill":60,"spawn":90,"status":63},
"fx": {"dp":5,"explode":4,"liberate":1,"phase":1,"revive":4,"shield":2,"sp":1,"strike":1,"summon":2,"telegraph":3,"wolfShadow":1,"wolfShadowLost":2},
"snaps": "1a7d12d1 7955f440 d99919a7 ba142976 2092d384 8dfb7d8e b96cb02d f9c0f4f2 731ec520 3fc5716e da654f48 c224a8bc e3c0b86b 51db902d"
"snaps": "1a7d12d1 7955f440 d99919a7 09818fe9 5d85594c eb4b0484 1591b58e c034a33b 2c27bf89 7a9f0e18 14dd1382 37e5dbf7 b8884791 2ad0053c"
},
"roster-027": {
"about": "pass 1 band 4: chess_char_3_06_b chess_char_3_12_b chess_char_3_02_b chess_char_3_03_b chess_char_3_04_b chess_char_3_05_b chess_char_3_21_a chess_char_3_01_b chess_char_3_08_b chess_char_3_09_b chess_char_3_10_b chess_char_3_11_b",
@@ -1684,13 +1684,13 @@
"ticks": 5100,
"reason": "timeout",
"killed": 41,
"total": 57,
"total": 53,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 143,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":41,"total":57,"perfect":false,"leaks":16,"leaksUncounted":0,"coins":8,"dmg":451319,"bossDmg":0,"heal":38831,"deaths":7,"layerGains":{"raidShip":18}}
"p1": {"killed":41,"total":53,"perfect":false,"leaks":16,"leaksUncounted":0,"coins":8,"dmg":451319,"bossDmg":0,"heal":38831,"deaths":7,"layerGains":{"raidShip":18}}
},
"units": [
["p1",1,"op","chess_char_3_06_b",0,"none",0,0,2,0,1394,6130,0,11,1],
@@ -1723,7 +1723,7 @@
"hooks": [1,71,5100,765,765,2076,2076,69,48,64,151,151,0,10608,52,29,57,14,1,48,3,0,0],
"events": {"atk":950,"bounty":4,"deploy":14,"die":50,"dmg":1979,"engage":12,"fx":118,"heal":69,"layer":3,"leak":14,"skill":305,"spawn":69,"status":185},
"fx": {"blink":1,"buff":1,"coin":32,"explode":15,"garrison":3,"shield":18,"shieldBreak":1,"strike":13,"summon":1,"volley":7,"zone":26},
"snaps": "5cda3b22 5fca5f0c d70d8372 4fe579a4 2c9b6956 8d5ffb46 9aeb8795 69cfc5d4 e7047e1f 38960979 63e2a094 700bd4c4 0fc17381 24617d77 874750a0 4b37c245 395b3b9a"
"snaps": "5cda3b22 5fca5f0c d70d8372 4fe579a4 dac79dc9 4a2e282a 6a55c54b 86e02720 9144eaf2 1bf08368 f064ba8f 2fb3e530 9274187f a89b50de 49881a0a be34c52c 49de8118"
},
"roster-028": {
"about": "pass 1 band 4: chess_char_3_16_b chess_char_4_01_a chess_char_3_18_b chess_char_4_04_a chess_char_3_13_b chess_char_3_14_b chess_char_3_17_b chess_char_3_19_b chess_char_3_20_b chess_char_3_21_b chess_char_4_02_a chess_char_4_06_a token_10028_vigil_wolf",
@@ -2268,7 +2268,7 @@
"ticks": 6492,
"reason": "cleared",
"killed": 51,
"total": 131,
"total": 86,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 136,
@@ -2276,7 +2276,7 @@
"players": {
"p1": {
"killed": 51,
"total": 131,
"total": 86,
"perfect": false,
"leaks": 80,
"leaksUncounted": 0,
@@ -2331,7 +2331,7 @@
"hooks": [1,175,6492,504,504,2453,2631,28400,82,166,61,59,39,10872,1110,32,131,80,1,57,1,2,0],
"events": {"atk":605,"bounty":2,"deploy":41,"die":86,"dmg":2534,"engage":15,"fx":82,"heal":20,"layer":1,"leak":80,"skill":120,"spawn":156,"status":656},
"fx": {"airstrike":11,"aoe":8,"burst":2,"copy":5,"explode":29,"garrison":1,"heal":4,"healField":2,"link":2,"revive":1,"split":1,"stone":1,"summon":15},
"snaps": "e7f57cb9 5d2f7fdf a34e9406 8a58323a 69de9406 d21d1321 1a7dd414 1453d4cf 75734598 fe883da1 9cea1cbf d7499dcf 74a67785 3dae277f 03326b45 4d301f69 b4e3db34 2f746db2 a0ce697f 30caa953 92e4bdb1 c3ddaeb8"
"snaps": "e7f57cb9 5d2f7fdf a34e9406 8a58323a 7d07bc24 abc407ce 319e7ebf 95f85fd9 9a3a7827 7b2bbf31 12b76db4 084b6d1d ed65c31c dbb67422 b1a1c69a 25e7a551 0c4ac123 51e0bc76 65d11431 1570d59d 03fbdd0e 985fd819"
},
"roster-038": {
"about": "pass 1 band 7: chess_char_5_08_b chess_char_6_19_b chess_char_5_01_b chess_char_5_05_b chess_char_5_06_b chess_char_6_17_b chess_char_5_07_b chess_char_6_18_b chess_char_6_20_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b",
@@ -2763,7 +2763,7 @@
"ticks": 5511,
"reason": "cleared",
"killed": 68,
"total": 68,
"total": 67,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 38,
@@ -2771,7 +2771,7 @@
"players": {
"p1": {
"killed": 68,
"total": 68,
"total": 67,
"perfect": true,
"leaks": 0,
"leaksUncounted": 0,
@@ -2822,7 +2822,7 @@
"hooks": [1,91,5511,386,379,786,1232,8816,69,74,13,10,34,23943,622,17,68,0,1,69,4,52,0],
"events": {"atk":446,"bounty":4,"deploy":20,"die":74,"dmg":1049,"engage":8,"fx":162,"layer":4,"skill":23,"spawn":90,"status":1246},
"fx": {"airstrike":10,"anchor":1,"burst":52,"crit":14,"dp":3,"expose":6,"garrison":3,"link":2,"lock":14,"snow":5,"summon":2,"summonStun":2,"talent":1,"wanted":47},
"snaps": "120f2050 546489af 7dcae1cb ba57ea31 14949879 f5908ce9 602d5c12 63bba2cf a8e27e28 9ffc8315 03831118 1358bb3c 17c12939 59ead8d8 4c60897d 078878c9 827f7455 b7ae543f 912fccf8"
"snaps": "120f2050 546489af cf372130 f94ff0af c7481d33 d45316d4 4f6aa634 25182725 e35a21e0 eef0037b 837c3a5d a6636a5e 1e78c189 0b81163f 723d13fc 6d325c2f 4e7ece19 c7ca24b8 06924ce2"
},
"roster-046": {
"about": "pass 2 band 7: chess_char_6_03_b chess_char_6_15_a chess_char_6_19_a chess_char_6_17_a chess_char_6_14_a chess_char_6_16_a chess_char_6_18_a chess_char_6_20_a chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b token_10017_skadi2_dedant",
@@ -2892,12 +2892,12 @@
"ticks": 6312,
"reason": "cleared",
"killed": 66,
"total": 142,
"total": 139,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 257,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":66,"total":142,"perfect":false,"leaks":76,"leaksUncounted":0,"coins":6,"dmg":348541,"bossDmg":0,"heal":10968,"deaths":28,"layerGains":{}}},
"players": {"p1":{"killed":66,"total":139,"perfect":false,"leaks":76,"leaksUncounted":0,"coins":6,"dmg":348541,"bossDmg":0,"heal":10968,"deaths":28,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_6_06_b",1,"none",0,0,5,11,42583,2639,0,22,2],
["p1",2,"op","chess_char_6_15_b",1,"none",0,0,151,4,39276,6912,0,29,2],
@@ -2937,7 +2937,7 @@
"hooks": [1,182,6312,730,723,806,909,111,97,177,14,14,90,10310,718,53,142,76,1,97,0,0,0],
"events": {"atk":737,"bounty":3,"deploy":37,"die":101,"dmg":824,"engage":12,"fx":52,"heal":19,"leak":76,"skill":28,"spawn":164,"status":153},
"fx": {"airstrike":29,"anchor":2,"appear":1,"blink":1,"copy":1,"disappear":1,"explode":1,"garrisonGrant":2,"heal":2,"link":1,"revive":3,"summon":2,"summonStun":2,"sunBurst":1,"sword":2,"zone":1},
"snaps": "3c9f691f 34faf375 087176f6 1c545a7a 0aa65801 59083ac8 08f660b0 d5292070 05ddc682 c1288138 21433f07 92e5a16c 7bfac0b9 0e1d7724 c53060df a263901a 9433d9ba 6046dcd9 3521e34a 4ddff910 6f17f8e1"
"snaps": "3c9f691f 34faf375 b7085c9e ad0ffeeb a5b75ffe d6e8b1d3 035b35db 66381fb1 b9877510 753ba510 04252066 ed355da1 5c82f27b 1be63ee2 55031d43 90c8bd6f b369a095 593b947c be089a07 29e3c756 28b150ee"
},
"roster-048": {
"about": "pass 2 band 7: chess_char_6_19_b chess_char_5_13_b chess_char_6_20_b chess_char_5_12_b chess_char_5_20_b chess_char_6_05_b",
@@ -2952,7 +2952,7 @@
"ticks": 6164,
"reason": "cleared",
"killed": 34,
"total": 87,
"total": 86,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 145,
@@ -2960,7 +2960,7 @@
"players": {
"p1": {
"killed": 34,
"total": 87,
"total": 86,
"perfect": false,
"leaks": 53,
"leaksUncounted": 0,
@@ -2998,7 +2998,7 @@
"hooks": [1,103,6164,249,249,2817,2826,30376,41,94,66,66,0,9498,399,25,87,53,1,74,9,1,0],
"events": {"atk":463,"bounty":1,"deploy":13,"die":41,"dmg":2826,"engage":5,"fx":129,"heal":44,"layer":9,"leak":53,"skill":132,"spawn":96,"status":835},
"fx": {"burst":1,"dollEnd":1,"explode":1,"garrison":8,"layer":1,"lightning":58,"shield":5,"slash":11,"sp":28,"storm":3,"substitute":3,"summon":1,"swap":2,"telegraph":1,"weightless":5},
"snaps": "114138f7 91b6a2fa cc32ef2d c73b8b92 cfc9b573 b1f8d939 67a68be3 fbe5df15 f7521760 ef229a79 2c9fc754 fd4c140e 7704bbb0 20c916c8 38931f1b 403b9470 3735233a 40c9ddae c1216447 f0ba8a3b addc333e"
"snaps": "114138f7 91b6a2fa cc32ef2d c73b8b92 cfc9b573 7b3dd24c 0c53a125 af35b361 6fca06d6 2c88ae52 e6b5584c b5db6ac3 b1fe0c6c 1b308f78 394e7946 d6b4633c 08432cc7 25ebf66b 22366e40 448fd11e e921151e"
},
"roster-049": {
"about": "pass 3 band 5: chess_char_4_23_b chess_char_3_04_b chess_char_4_09_b chess_char_3_21_b chess_char_4_13_b",
@@ -3013,12 +3013,12 @@
"ticks": 5700,
"reason": "timeout",
"killed": 28,
"total": 64,
"total": 62,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":28,"total":64,"perfect":false,"leaks":36,"leaksUncounted":0,"coins":6,"dmg":430513,"bossDmg":0,"heal":0,"deaths":6,"layerGains":{}}},
"players": {"p1":{"killed":28,"total":62,"perfect":false,"leaks":36,"leaksUncounted":0,"coins":6,"dmg":430513,"bossDmg":0,"heal":0,"deaths":6,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_4_23_b",2,"none",0,0,1,0,0,2661,0,0,0],
["p1",2,"op","chess_char_3_04_b",1,"none",3289.5,1,74,1,17288,6579,0,88,0],
@@ -3043,7 +3043,7 @@
"hooks": [1,74,5700,336,336,1757,1757,0,34,69,31,32,0,3916,725,3,64,35,1,35,0,0,0],
"events": {"atk":389,"bounty":3,"deploy":10,"die":34,"dmg":1756,"engage":4,"fx":47,"leak":35,"skill":67,"spawn":69,"status":415},
"fx": {"coin":9,"dilemma":3,"explode":2,"frostNova":25,"revive":1,"shield":2,"shieldBreak":1,"summon":1,"telegraph":3},
"snaps": "22feba20 4bc82bff c2ef8225 6fc18bf4 0986acbf 61e7f956 eb664c7f db99546b 96c386fa 8701aeb3 c2cb18a0 f881beec 96991826 da3b1101 c2a76f22 f89bc3d9 a6d90c11 4ec4cbb4 0b2fb538"
"snaps": "22feba20 4bc82bff 64cc615e 21da6144 58240a76 f0531e80 a0205176 3d77feab 942ccd22 485f3fd3 56333d9f 02add93c cbc1098a dd516fb0 72fb6266 e093854d 6173f536 de8c5328 1f0a2be6"
}
}
}
+8 -1
View File
@@ -366,8 +366,15 @@ export function checkInvariants(b) {
for (const pr of b.projectiles.list) if (![pr.x, pr.y, pr.tx, pr.ty].every(Number.isFinite)) bad(`projectile at ${pr.x},${pr.y} → ${pr.tx},${pr.ty}`);
if (b._emitDepth) bad(`hook emit depth ${b._emitDepth} between steps`);
for (const pp of Object.values(b._perPlayer ?? {})) {
for (const k of ['killed', 'total', 'coins', 'damageDealt', 'bossDamage', 'healingDone', 'deaths']) if (!Number.isFinite(pp[k])) bad(`result ${k}=${pp[k]}`);
for (const k of ['killed', 'total', 'killedInTotal', 'leakedInTotal', 'coins', 'damageDealt', 'bossDamage', 'healingDone', 'deaths']) if (!Number.isFinite(pp[k])) bad(`result ${k}=${pp[k]}`);
// The capsule's counters are attributed differently on purpose: a knock-out goes to the enemy's owner (as its `total`
// did), while a leak goes to the player whose half the enemy reached (`_recordLeak`) — in a 联防 field an enemy
// spawned on one half and leaked on the other bills the leaker, so per player only `killedInTotal ≤ total` holds
// (BattleResult's per-player `resolved` clamps the sum to `total`, and the battle-level sum is exact)
if (pp.killedInTotal > pp.total) bad(`capsule ${pp.killedInTotal} > ${pp.total}`);
if (pp.total < 0 || pp.killedInTotal < 0 || pp.leakedInTotal < 0) bad('negative capsule counter');
}
if (b.killedInTotal + b.leakedInTotal > b.total) bad(`battle capsule ${b.killedInTotal}+${b.leakedInTotal} > ${b.total}`);
return true;
}
+8 -1
View File
@@ -95,7 +95,14 @@ test('COMBAT: humans get their own spec (authoritative), bots are simulated by t
assert.deepEqual(m.handle('p_0', { t: 'b.progress', battleId: human.battleId, gt: 4, killed: 3, total: 9, leaks: 0 }), { ok: true });
m.flush(true);
const pub = m.publicView();
assert.deepEqual(pub.fields.find((f) => f.fieldId === 'n:p_0').progress, { killed: 3, total: 9, done: false });
// `resolved` is null while the authority has not reported one — never 0, so the client's `resolved ?? killed` fallback
// holds (a report without `resolved` must read as "unknown", not as "nothing resolved")
assert.deepEqual(pub.fields.find((f) => f.fieldId === 'n:p_0').progress, { killed: 3, resolved: null, total: 9, done: false });
// a report that carries `resolved` is adopted, a reported 0 included (PR #157 capsule numerator)
assert.deepEqual(m.handle('p_0', { t: 'b.progress', battleId: human.battleId, gt: 5, killed: 3, total: 9, leaks: 0, resolved: 0 }), { ok: true });
m.flush(true);
const pub2 = m.publicView();
assert.deepEqual(pub2.fields.find((f) => f.fieldId === 'n:p_0').progress, { killed: 3, resolved: 0, total: 9, done: false });
// a stale / foreign report is ignored (never an error toast)
assert.deepEqual(m.handle('p_1', { t: 'b.progress', battleId: human.battleId, gt: 9, killed: 9, total: 9 }), { ok: true });
assert.equal(human.progress.killed, 3);
+131
View File
@@ -0,0 +1,131 @@
// test/match/hud-capsule-resolved.test.js — the HUD capsule's server half (PR #157): m.public.fields[].progress carries
// the field's own `resolved` (knocked out + leaked among the enemies the round scheduled), and
// * a bot / server-run field has no authority reporting b.progress — its capsule must read the battle's own counters
// (`timelineSample` → Battle.resolved), not the report-driven `progress.leaks`, which stays 0 forever (maintainer
// review point ①);
// * `progress.resolved` starts at **null**, never 0: only a real reported number is adopted, so the client's
// `resolved ?? killed` fallback keeps working (a `Number(null) === 0` would defeat it) — and a reported 0 is a real
// value, told apart from "not reported" (review point ②).
// Run: node --test test/match/hud-capsule-resolved.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { PHASE } from '../../shared/constants.js';
import { makeMatch } from './harness.js';
import { timelineSample, timelineAt } from '../../server/match/fields.js';
import { battleProgress } from '../../server/sim/spec.js';
import { makeBattle } from '../helpers/battleHarness.js';
import { teammateProgress } from '../../public/js/battle/observe.js';
import { uniteLeftMark } from '../../public/js/battle/runner.js';
const fields = (h) => h.m.fields;
const progressOf = (m, fieldId) => (m.publicView().fields.find((f) => f.fieldId === fieldId) || {}).progress;
test('① 机器人战场 (mode server, 无上报): 胶囊读该战斗自己的计数器,不是停在 0 的 progress.leaks', () => {
// no FakeBattle: the bot's field runs the real sim on the server (HeadlessJob + timeline), and nobody reports b.progress
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 1, bots: 1, seed: 7, instant: false, clientCombat: true, clients: false }).start();
const m = h.m;
h.toPrep(1);
m.handle('p_0', { t: 'g.ready', ready: true });
h.run(() => m.phase === PHASE.COMBAT);
const f = fields(h).find((x) => x.fieldId === 'n:ai_0');
assert.equal(f.mode, 'server', 'the bot field runs on the server');
assert.equal(f.authority, null, 'no authority ever reports it');
assert.ok(f.timeline && f.timeline.length > 1, 'it has a progress timeline');
assert.equal(f.progress.leaks, 0, 'nothing was reported: progress.leaks stays 0');
assert.equal(f.progress.resolved, null, 'and progress.resolved stays null (never a fabricated 0)');
let filled = 0;
for (let i = 0; i < 20 && !f.done; i++) {
h.sched.advance(2000);
const pr = progressOf(m, 'n:ai_0');
// the capsule reads the battle's own counters, sampled on the field clock
assert.equal(pr.resolved, timelineAt(f.timeline, m._fieldElapsed(f))[3], 'resolved = the timeline sample');
assert.ok(pr.resolved != null && pr.resolved <= pr.total, 'a real number, never above the denominator');
if (pr.resolved > 0) { filled = pr.resolved; break; }
}
assert.ok(filled > 0, 'the bot field\'s capsule fills up (a report-driven number would have stayed at 0)');
// the sample is composed of the battle's own two counters — the field's own scheduled enemies that were resolved
assert.equal(timelineSample(f.battle)[3], Math.min(f.battle.total, f.battle.killedInTotal + f.battle.leakedInTotal));
assert.equal(progressOf(m, 'n:ai_0').resolved, timelineAt(f.timeline, m._fieldElapsed(f))[3], 'clamped by the field clock');
});
test('① 一场只漏不杀的机器人战场: resolved 由 leakedInTotal 顶起来 (不是 0)', () => {
// three walkers, nobody to stop them: the field's own enemies leak — leakedInTotal is what the capsule must show
const h = makeBattle({ seed: 5, enemies: [{ key: 'enemy_1005_yokai', count: 3 }], content: 'full' });
h.runToEnd(120);
assert.equal(h.b.total, 3);
assert.equal(h.b.leakedInTotal, 3, 'the field leaked all three of its own enemies');
assert.equal(h.b.leakedCount, 3, 'counted leaks — the LP charge, unchanged from master');
const sample = timelineSample(h.b);
assert.equal(sample[3], 3, 'the timeline sample carries the capsule numerator');
assert.equal(battleProgress(h.b).resolved, 3, 'b.progress too (what a teammate HUD reads)');
assert.equal(battleProgress(h.b).total, 3);
assert.equal(battleProgress(h.b).killed, 0);
});
test('② 上报没有 resolved ⇒ progress.resolved = null,客户端回落到 killed;上报 resolved: 0 ⇒ 采用 0', () => {
const h = makeMatch({ mode: 'coop', humans: 2, bots: 1, seed: 9103, fake: true, clientCombat: true, clients: false, script: () => ({ duration: 6 }) }).start();
const m = h.m;
h.toPrep(1);
m.handle('p_0', { t: 'g.ready', ready: true });
m.handle('p_1', { t: 'g.ready', ready: true });
h.run(() => m.phase === PHASE.COMBAT);
const human = fields(h).find((x) => x.fieldId === 'n:p_0');
assert.equal(human.mode, 'client');
assert.equal(human.progress.resolved, null, 'a fresh field: resolved unknown, not 0');
// a report without `resolved` (an older client / a field that cannot count it) leaves it unknown
assert.deepEqual(m.handle('p_0', { t: 'b.progress', battleId: human.battleId, gt: 4, killed: 3, total: 9, leaks: 0 }), { ok: true });
m.flush(true);
assert.deepEqual(progressOf(m, 'n:p_0'), { killed: 3, resolved: null, total: 9, done: false });
// … and the teammate HUD falls back to `killed` (master's display: resolved ?? killed)
const mine = (pub) => teammateProgress(pub, 'p_1').find((x) => x.playerId === 'p_0');
assert.deepEqual(mine(m.publicView()), { playerId: 'p_0', name: 'P0', isBot: false, killed: 3, resolved: 3, total: 9, done: false });
// a reported 0 is a real value: adopted, and the HUD shows 0 (not the fallback)
assert.deepEqual(m.handle('p_0', { t: 'b.progress', battleId: human.battleId, gt: 5, killed: 3, total: 9, leaks: 0, resolved: 0 }), { ok: true });
m.flush(true);
assert.deepEqual(progressOf(m, 'n:p_0'), { killed: 3, resolved: 0, total: 9, done: false });
assert.deepEqual(mine(m.publicView()), { playerId: 'p_0', name: 'P0', isBot: false, killed: 3, resolved: 0, total: 9, done: false });
});
test('a done field\'s m.public progress sums the reported per-player resolved (never a fabricated one)', () => {
const h = makeMatch({ mode: 'coop', humans: 2, bots: 1, seed: 9104, fake: true, clientCombat: true, clients: false, script: () => ({ duration: 2 }) }).start();
const m = h.m;
h.toPrep(1);
m.handle('p_0', { t: 'g.ready', ready: true });
m.handle('p_1', { t: 'g.ready', ready: true });
h.run(() => m.phase === PHASE.COMBAT);
const human = fields(h).find((x) => x.fieldId === 'n:p_0');
// a result carrying the capsule numbers: 5 counted knock-outs (a split child included), 3 of the round's own resolved
const result = {
reason: 'cleared', time: 6, killed: 5, total: 3, resolved: 3,
perPlayer: { p_0: { killed: 5, total: 3, resolved: 3, leaked: [], perfect: true, layerGains: {}, coins: 0, damageDealt: 0, bossDamage: 0, healingDone: 0, deaths: 0, unitsEnd: [], unitStats: [] } },
errors: 0,
};
assert.deepEqual(m.handle('p_0', { t: 'b.result', battleId: human.battleId, result }), { ok: true });
m.flush(true);
const pr = progressOf(m, 'n:p_0');
assert.equal(pr.done, true);
assert.equal(pr.killed, 5, '`killed` keeps the counted reading (may exceed the denominator)');
assert.equal(pr.total, 3, 'the denominator is the round\'s own list');
assert.equal(pr.resolved, 3, 'the capsule numerator of the finished field');
});
test('a 联防 battle\'s live-counter mark still moves for a spawn — `total` no longer does (a split child / a summon is outside the capsule)', () => {
// the runner re-reads the leakers' enemies still standing (uniteLeft) only when this mark moves; before the capsule
// counted the round's own enemies only, every counted spawn raised `total` and so moved it
const h = makeBattle({ seed: 41, autoFinish: false, timeLimit: 60, enemies: [{ key: 'enemy_1195_sfyin', pos: [9, 6] }] });
h.step();
const m0 = uniteLeftMark(h.b);
assert.equal(h.b.total, 1);
assert.equal(uniteLeftMark(h.b), m0, 'nothing happened: the same mark');
const child = h.spawn('enemy_1196_msfyin', { pos: [9, 4] }); // a runtime spawn: counted, outside the denominator
assert.ok(child && child.counted && !child.inTotal);
assert.equal(h.b.total, 1, 'the denominator did not move');
assert.notEqual(uniteLeftMark(h.b), m0, 'the mark did: the spawn is a standing enemy the counter must read');
const m1 = uniteLeftMark(h.b);
h.b.kill(child, null);
assert.notEqual(uniteLeftMark(h.b), m1, 'a knock-out moves it');
});
+6 -1
View File
@@ -22,7 +22,12 @@ test('observeTarget: prep, own battle running / over, boss pairs, eliminated pla
assert.deepEqual(observeTarget(players[1], combat(true), 'a', { ownDone: true }), { fieldId: 'n:b' }, 'the local battle already ended');
assert.deepEqual(observeTarget(players[1], combat(false), 'a'), { fieldId: 'n:b' });
assert.deepEqual(observeTarget(players[0], combat(false), 'a', { observing: true }), { back: true });
assert.deepEqual(teammateProgress(combat(false), 'a'), [{ playerId: 'b', name: 'B', isBot: false, killed: 3, total: 8, done: false }]);
// the teammate's capsule reads `resolved` (the field's own enemies knocked out or leaked); a report without one falls
// back to `killed` (public/js/battle/observe.js teammateProgress)
assert.deepEqual(teammateProgress(combat(false), 'a'), [{ playerId: 'b', name: 'B', isBot: false, killed: 3, resolved: 3, total: 8, done: false }]);
const reported = pubOf('COMBAT', [{ fieldId: 'n:b', kind: 'normal', players: ['b'], live: true, progress: { killed: 5, resolved: 3, total: 8, done: false } }], players);
assert.deepEqual(teammateProgress(reported, 'a'), [{ playerId: 'b', name: 'B', isBot: false, killed: 5, resolved: 3, total: 8, done: false }],
'a reported `resolved` wins over `killed` (5 counted knock-outs — splits included —, 3 of the round\'s own enemies resolved)');
const fa = pubOf('FINAL_ASSAULT', [
{ fieldId: 'b1', kind: 'boss', players: ['a', 'b'], live: true },
{ fieldId: 'b2', kind: 'boss', players: ['d'], live: true },
+7 -1
View File
@@ -430,7 +430,13 @@ test('#7 a leaked enemy that splits (磨砻: DeadSpawn ×2) raises the counter
seen.push(p.uniteLeft);
h.sched.advance(500);
}
assert.ok(b.total > SENT, `the 磨砻 split (${SENT} → ${b.total} on the field)`);
// the split children are runtime spawns: they enter neither part of the HUD capsule (PR #157: the denominator counts
// only what the 联防 scheduled — `b.total` — and the numerator only that set's own knock-outs / leaks), while the
// 联防 live counter (m.public uniteLeft) still bills them to the leaker
const kids = b.units.filter((u) => u.side === 'enemy' && !u.inTotal).length;
assert.ok(kids > 0, `the 磨砻 split into ${kids} runtime children`);
assert.equal(b.total, SENT, `the children stay out of the capsule's denominator (total = the ${SENT} the 联防 scheduled)`);
assert.equal(b.resolved, Math.min(SENT, b.killedInTotal + b.leakedInTotal), 'the capsule numerator counts only the 联防\'s own enemies');
const rises = seen.filter((v, i) => i > 0 && v > seen[i - 1]).length;
assert.ok(rises > 0, `the counter rose after a split: ${seen.filter((v, i) => i === 0 || v !== seen[i - 1]).join(' → ')}`);
assert.ok(Math.max(...seen) > SENT, 'above the number sent in (the old clamp hid it)');
+2
View File
@@ -129,6 +129,8 @@ export class SimClient {
_progress(e) {
const p = battleProgress(e.battle);
const msg = { t: 'b.progress', battleId: e.battleId, gt: p.gt, killed: Math.min(p.killed, p.total), total: p.total, done: p.done };
// the HUD capsule's numerator, like public/js/battle/runner.js (kept only when the battle reports one)
if (Number.isFinite(p.resolved)) msg.resolved = Math.min(p.resolved, p.total);
const bossLike = e.spec.kind === 'boss' || e.spec.kind === 'hidden';
if (bossLike) {
const pool = e.battle.sharedBoss;
+6 -1
View File
@@ -298,7 +298,12 @@ test('real data: R5 template on act1 m04 (crates) with a real lineup runs to the
assert.ok(h.b.allyUnits.some((u) => u.kind === 'device'), 'm04 crates present');
const r = h.result();
const p = r.perPlayer.p1;
assert.equal(p.killed + p.leaked.filter((l) => l.counted).length, p.total, 'every counted enemy is killed or leaked');
// the capsule identity (PR #157): this round's own scheduled enemies are all 已解决 — knocked down or leaked. It
// replaces `killed + counted leaks = total`, which mixed the counted reading (runtime splits / summons included) with
// the scheduled denominator: `killed` may now exceed `total`, so only `≥` holds for it, while the capsule pair is exact
assert.ok(p.killed + p.leaked.filter((l) => l.counted).length >= p.total, 'every counted enemy is killed or leaked');
assert.equal(p.killedInTotal + p.leakedInTotal, p.total, 'the capsule is full: killedInTotal + leakedInTotal = total');
assert.equal(p.resolved, p.total);
});
test('generic kit never throws and yields finite numbers for every real chess (normal & elite)', () => {
+14 -2
View File
@@ -106,9 +106,21 @@ test('fuzz: 200 random battles on real data run to completion with invariants',
const r = b.result();
stats[r.reason]++;
for (const [pid, pp] of Object.entries(r.perPlayer)) {
assert.ok(pp.killed <= pp.total || kind === 'unite', `${pid} killed ≤ total`);
// the capsule's own pair: `total` counts only the enemies this field scheduled (`inTotal`), and its numerator
// never exceeds it. `killed` deliberately keeps the `counted` reading — a runtime split child / summon knocked
// down counts there — so `killed > total` is legal since PR #157's capsule semantics (a 4-磨砻 round killed 4
// plus a child reads 5/4 on nothing but `killed/total`; the capsule reads resolved/total = 4/4)
assert.ok(pp.killedInTotal <= pp.total, `${pid} killedInTotal ≤ total`);
assert.ok(pp.resolved <= pp.total, `${pid} resolved ≤ total`);
for (const k of ['damageDealt', 'healingDone', 'bossDamage']) assert.ok(Number.isFinite(pp[k]) && pp[k] >= 0, `${k} finite`);
if (r.reason === 'cleared' && kind === 'normal') assert.equal(pp.killed + pp.leaked.filter((l) => l.counted).length, pp.total, 'every counted enemy killed or leaked');
// the capsule identity: every one of the field's own enemies is 已解决 — knocked down (killedInTotal) or leaked
// (leakedInTotal). It replaces the old `killed + counted leaks = total`, which mixed the counted reading with the
// scheduled denominator and no longer holds (killed counts runtime children, total does not)
if (r.reason === 'cleared' && kind === 'normal') {
assert.ok(pp.killed + pp.leaked.filter((l) => l.counted).length >= pp.total, 'every counted enemy killed or leaked');
assert.equal(pp.killedInTotal + pp.leakedInTotal, pp.total, 'the capsule is full: killedInTotal + leakedInTotal = total');
assert.equal(pp.resolved, pp.total, 'a cleared normal field resolves its own list');
}
}
JSON.stringify(r);
}
+228
View File
@@ -0,0 +1,228 @@
// test/sim/hud-capsule-resolved.test.js — the HUD capsule's own counters (PR #157, maintainer-reviewed direction; the
// bounty / kill semantics stay master's):
// resolved = min(total, killedInTotal + leakedInTotal) — the field's OWN scheduled enemies (`inTotal`) that are
// 已解决: knocked down or leaked. The official example: 开局 0/3 → 漏一个 1/3 → 打死一个 2/3 → 打死会分裂的 3/3.
// A runtime spawn — a split child, a summon (a boss's included), a part — is in neither the numerator nor the
// denominator, while `counted` (the LP / 完美作战 / `killed` reading) keeps counting it exactly as master did.
// Run: node --test test/sim/hud-capsule-resolved.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { getData } from '../../server/data.js';
import { GameData } from '../../server/match/gamedata.js';
import { setupMatchWaves, buildBossWave } from '../../server/match/waves.js';
import { createRng, deriveSeed } from '../../server/sim/rng.js';
const QUIET = { warn() {}, error() {}, info() {}, log() {}, debug() {} };
/** The real mode config: the per-round LP cap settlement charges min(cap, counted leaks) with (match/settle.js). */
const gd = new GameData(getData({ log: QUIET }), 'mode_multi_normal');
const MOLANG = 'enemy_1195_sfyin'; // 磨砻 — 被击倒后生成2个<木制瑞印> (data talents DeadSpawn)
const MOLANG_CHILD = 'enemy_1196_msfyin'; // 木制瑞印
const APOSTLE = 'enemy_1321_wdarft'; // 枯朽萃聚使徒 — 召唤数个<枯朽之种> while it lives
const SEED = 'enemy_1269_nhfly'; // 枯朽之种
const done = (h) => {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
};
const capsule = (b) => ({ total: b.total, killedInTotal: b.killedInTotal, leakedInTotal: b.leakedInTotal, resolved: b.resolved });
test('官方例子: 开局 0/3 → 漏一个 1/3 → 打死一个 2/3 → 打死会分裂的 3/3 (分裂子体不进胶囊)', () => {
const h = makeBattle({
seed: 11, autoFinish: false, timeLimit: 300,
enemies: [
{ key: 'enemy_1195_sfyin', pos: [9, 6], count: 1 },
{ key: 'enemy_1005_yokai', pos: [9, 8], count: 2 },
],
});
h.step();
const molang = h.b.enemies.find((e) => e.defId === MOLANG);
const yokai = h.b.enemies.filter((e) => e.defId === 'enemy_1005_yokai');
assert.equal(h.b.total, 3, '分母 = 本关自己排定的 3 个敌人');
assert.ok(molang.inTotal && yokai.every((e) => e.inTotal), '排定的敌人 inTotal');
assert.equal(h.b.resolved, 0, '0/3');
const snap = h.snapshot();
assert.equal(snap.resolved, 0, 'b.snap carries the capsule numerator');
assert.equal(snap.total, 3);
h.b.leak(yokai[0]); // 漏一个
assert.equal(h.b.resolved, 1, '1/3');
h.b.kill(yokai[1], null); // 打死一个
assert.equal(h.b.resolved, 2, '2/3');
h.b.kill(molang, null); // 打死会分裂的
assert.equal(h.b.killedInTotal + h.b.leakedInTotal, 3, '三个排定的敌人都已解决 (2 击倒 + 1 漏掉)');
assert.equal(h.b.killedInTotal, 2);
assert.equal(h.b.leakedInTotal, 1);
assert.equal(h.b.resolved, 3, '3/3');
assert.equal(h.b.total, 3, '分裂出来的子体没有抬高分母');
const kids = h.b.enemies.filter((e) => e.alive && e.defId === MOLANG_CHILD);
assert.equal(kids.length, 2, '磨砻 split into 2 木制瑞印');
for (const k of kids) assert.ok(!k.inTotal && k.counted, 'a split child counts for LP, not for the capsule');
assert.equal(h.b.resolved, 3, 'the children move no capsule number');
done(h);
});
test('磨砻 4 个全被击倒 → 胶囊 4/4;它们 8 个子体全漏 → leakedInTotal 仍 0、counted 口径的 LP 照扣 min(10, 8) = 8', () => {
const h = makeBattle({ seed: 12, autoFinish: false, timeLimit: 300, enemies: [{ key: MOLANG, pos: [9, 6], count: 4 }] });
h.step();
const molang = h.b.enemies.filter((e) => e.defId === MOLANG && e.alive);
assert.equal(molang.length, 4);
assert.equal(h.b.total, 4, '分母 = 排定的 4 个磨砻');
for (const e of molang) h.b.kill(e, null);
assert.equal(h.b.killedInTotal, 4);
assert.equal(h.b.resolved, 4, '胶囊 4/4 — 击倒的是本关自己的敌人');
assert.equal(h.b.total, 4, '子体不进分母');
const kids = h.b.enemies.filter((e) => e.alive && e.defId === MOLANG_CHILD);
assert.equal(kids.length, 8, '四个磨砻各分裂出 2 个木制瑞印');
assert.equal(h.b.leakedInTotal, 0);
for (const k of kids) h.b.leak(k);
// the capsule: the leaked children are no enemies of the stage's own list — the numerator stays at 4/4
assert.deepEqual(capsule(h.b), { total: 4, killedInTotal: 4, leakedInTotal: 0, resolved: 4 });
// the LP reading is master's, unchanged: every leaked child is a counted leak, and settlement charges min(cap, 8)
// (server/match/settle.js `Math.min(cap, counted)`); leakedCount is the count the LP meter reads
assert.equal(h.b.leakedCount, 8);
const r = h.result();
const countedLeaks = r.perPlayer.p1.leaked.filter((l) => l.counted !== false).length;
assert.equal(countedLeaks, 8, '8 counted leaks — the children each cost LP as before');
assert.equal(Math.min(gd.lpCapPerRound, countedLeaks), 8, 'the settled charge is min(10, 8) = 8, exactly as master');
assert.equal(r.perPlayer.p1.perfect, false, 'a counted leak still breaks 完美作战');
assert.equal(r.perPlayer.p1.resolved, 4, 'BattleResult carries the capsule numerator');
assert.equal(r.resolved, 4);
done(h);
});
test('分裂子体被击倒: 胶囊数字不动,`killed` 仍按 counted 口径 +1 (master 的击杀语义不变)', () => {
const h = makeBattle({ seed: 13, autoFinish: false, timeLimit: 300, enemies: [{ key: MOLANG, pos: [9, 6] }] });
h.step();
const molang = h.b.enemies.find((e) => e.defId === MOLANG);
h.b.kill(molang, null);
const kids = h.b.enemies.filter((e) => e.alive && e.defId === MOLANG_CHILD);
assert.equal(kids.length, 2);
const before = capsule(h.b);
const killedBefore = h.b.killed;
const coinsBefore = h.result().perPlayer.p1.coins;
h.b.kill(kids[0], null);
assert.deepEqual(capsule(h.b), before, 'a knocked-down split child moves no capsule number');
assert.equal(h.b.killed, killedBefore + 1, '`killed` counts every counted knock-out (master semantics, unchanged)');
assert.equal(h.result().perPlayer.p1.coins, coinsBefore, 'no bounty for a split child');
h.b.leak(kids[1]);
assert.deepEqual(capsule(h.b), before, 'a leaked split child moves no capsule number either');
assert.equal(h.b.leakedCount, 1, '… but it is a counted leak (LP / 完美作战)');
assert.equal(h.b.resolved, 1, 'the capsule stands at 1/1 — the 磨砻 itself');
done(h);
});
test('枯朽萃聚使徒: 召唤出的枯朽之种出生 / 被击倒 / 漏掉都不动胶囊,本体照旧结算 (悬赏口径不变)', () => {
const card = { id: 'enemyeffect_10_8', card: { payout: 'kill', coin: 2, enemyKey: APOSTLE } };
const h = makeBattle({
seed: 14, autoFinish: false, timeLimit: 300,
enemies: [{ key: APOSTLE, route: 2, mods: { hpMul: 1, bountyId: card.id }, bounty: { coins: 2, ownerPlayerId: 'p1' }, tag: 'bounty', ownerPlayerId: 'p1' }],
});
assert.ok(h.runUntil(() => h.b.enemies.filter((e) => e.alive && e.defId === SEED).length >= 2, 30), 'BornBugs cast');
const seeds = h.b.enemies.filter((e) => e.alive && e.defId === SEED);
assert.equal(h.b.total, 1, '分母只有排定的本体 — 召唤物不进分母');
for (const s of seeds) {
assert.ok(!s.inTotal, 'a summoned 枯朽之种 is not in the capsule');
assert.ok(s.counted, '… but it counts for LP / 完美作战 (master)');
assert.equal(s.bounty, null, '悬赏只在召唤物本体上 (master rule, untouched)');
}
const before = capsule(h.b);
const killedBefore = h.b.killed;
h.b.kill(seeds[0], null);
assert.deepEqual(capsule(h.b), before, 'the summoned seed\'s knock-out moves no capsule number');
assert.equal(h.b.killed, killedBefore + 1, '`killed` still counts it (master)');
assert.deepEqual(h.eventsOf('bounty'), [], 'a summoned seed pays no bounty');
h.b.leak(seeds[1]);
assert.deepEqual(capsule(h.b), before, 'the summoned seed\'s leak moves no capsule number');
assert.equal(h.b.leakedCount, 1, 'counted leak: the LP charge is unchanged from master');
// the body itself is the field's own scheduled enemy: its knock-out resolves the capsule and pays once
const body = h.b.enemies.find((e) => e.alive && e.defId === APOSTLE);
assert.ok(body && body.inTotal);
h.b.kill(body, null);
assert.deepEqual(h.eventsOf('bounty'), [['bounty', 'p1', 2]], 'the body pays once (master rule)');
assert.equal(h.b.resolved, h.b.total, 'the field\'s own list is resolved');
done(h);
});
// =====================================================================================================================
// A real Boss round: a boss summon is a runtime spawn too. The 假想敌:管 (boss_3, act1autochess_h07_03) summons a
// 余音 (enemy_9023_acdums) every 40 game s (20 below half HP); its wave ALSO schedules 2 余音 of its own — those are
// the round's own enemies and do enter the capsule, the summoned ones do not.
test('Boss 召唤物 (假想敌:管的余音) 出生 / 被击倒 / 漏掉都不动胶囊;Boss 死后 resolved === total', () => {
const bossId = 'boss_3';
const ECHO = 'enemy_9023_acdums'; // 余音 — 管 summons it every 40 game s (20 s below half HP)
const seed = deriveSeed(20261005, `boss:${bossId}:p`);
const wave = buildBossWave(gd, createRng(deriveSeed(seed, 'waves')), setupMatchWaves(gd, createRng(deriveSeed(seed, 'setup'))), gd.bossRound, { bossId, solo: false });
const h = makeBattle({
kind: 'boss', seed: 21, autoFinish: false,
routes: wave.routes,
enemies: wave.spawns.map((s) => ({ key: s.enemyKey, time: s.time, route: s.routeIndex, count: s.count, interval: s.interval, mods: s.mods, tag: s.tag, countInTotal: s.countInTotal })),
});
// the round's own list: 2 疏术士 + 2 疏den + 2 余音 (the wave's own 余音, scheduled 40 s apart) — the 领袖 (tag 'boss')
// and everything 管 summons are outside it. 会召唤的 Boss 关胶囊 therefore fills up now (the old `total++` of a runtime
// spawn left it short forever).
const scheduled = wave.spawns.filter((s) => s.tag !== 'boss' && s.countInTotal !== false).reduce((n, s) => n + s.count, 0);
assert.equal(scheduled, 6);
assert.equal(h.b.total, scheduled, `分母 = 本关排定且该计入的 ${scheduled} 个敌人`);
h.run(3); // the 领袖 comes in at t = 1 s, the wave's first 余音 at t = 0
const leader = h.b.enemies.find((e) => e.defId === 'enemy_9021_acduml');
assert.ok(leader, 'the 领袖 is on the field');
assert.ok(!leader.inTotal && !leader.counted, 'the 领袖 (tag boss) is in no capsule part');
assert.equal(h.b.units.filter((u) => u.side === 'enemy' && u.inTotal && u.defId === ECHO).length, 1, 'the wave scheduled its own 余音');
assert.equal(h.b.total, scheduled, 'the 领袖 does not add to the denominator (tag boss ⇒ not counted)');
// the boss summons one of its own (pipeCore, bosses.js: `spawnEnemy` — never the spawn queue)
const runtime = () => h.b.units.find((u) => u.side === 'enemy' && u.alive && !u.inTotal && u.defId === ECHO);
assert.ok(h.runUntil(() => !!runtime(), 90), 'the boss summoned a 余音');
const first = runtime();
assert.ok(first.counted, 'the summon still counts for LP (master)');
assert.equal(h.b.total, scheduled, 'the summoned 余音 never raised the denominator (the old defect: an unfillable capsule)');
const before = capsule(h.b);
const killedBefore = h.b.killed;
h.b.kill(first, null);
assert.deepEqual(capsule(h.b), before, 'a knocked-down boss summon moves no capsule number');
assert.equal(h.b.killed, killedBefore + 1, '`killed` counts it (master semantics unchanged)');
assert.ok(h.runUntil(() => !!runtime(), 120), 'a second summon');
const beforeLeak = capsule(h.b);
const leakedBefore = h.b.leakedCount;
h.b.leak(runtime());
assert.deepEqual(capsule(h.b), beforeLeak, 'a leaked boss summon moves no capsule number');
assert.equal(h.b.leakedCount, leakedBefore + 1, 'a counted leak — the LP charge follows the real data (`counted`), as master');
// the leader itself: tag 'boss' ⇒ counted false ⇒ in no capsule number, and killing it is no `killed` either (master)
const killedBeforeLeader = h.b.killed;
h.b.kill(leader, null);
assert.equal(h.b.killed, killedBeforeLeader, 'the leader is not a counted knock-out (master reading)');
// every one of the round's own enemies resolved ⇒ the capsule is full
for (const e of h.b.enemies.filter((e) => e.alive && e.inTotal)) h.b.kill(e, null);
for (const e of h.b.enemies.filter((e) => e.alive && e.inTotal)) h.b.leak(e);
assert.equal(h.b.killedInTotal + h.b.leakedInTotal, h.b.total);
assert.equal(h.b.resolved, h.b.total, 'Boss 死后 resolved === total — the summons never pushed the denominator up');
assert.equal(h.b.total, scheduled, 'nor did they move it at the end');
done(h);
});
test('kept honest: a helper enemy record the tests build is a scheduled enemy (inTotal) while spawnEnemy is not', () => {
const h = makeBattle({
seed: 31, autoFinish: false, timeLimit: 60,
defs: { enemies: { enemy_helper: enemyRec({ key: 'enemy_helper', hp: 10, speed: 0 }) } },
enemies: [{ key: 'enemy_helper' }],
});
h.step();
assert.equal(h.b.total, 1);
assert.equal(h.b.enemies[0].inTotal, true);
const runtime = h.spawn('enemy_helper', { pos: [9, 4] });
assert.equal(h.b.total, 1, 'a runtime spawnEnemy is not in the denominator');
assert.ok(!runtime.inTotal && runtime.counted, '… and not in the capsule, while counted stays master\'s');
const explicit = h.b.spawnEnemy('enemy_helper', { pos: [9, 3], inTotal: true }); // the content back door
assert.equal(h.b.total, 2, 'an explicit `inTotal: true` is honoured');
assert.ok(explicit.inTotal);
assert.equal(h.b.enemies.filter((e) => e.alive).length, 3);
done(h);
});
+4 -1
View File
@@ -583,7 +583,10 @@ describe('enemies, HUD, stats', () => {
assert.deepEqual(factionTypes(['FLY', { type: 'TIMES' }, { id: 'SPECIAL' }, 'FLY', 3, null]), ['FLY', 'TIMES', 'SPECIAL']);
});
test('snapHud / bossFrac', () => {
assert.deepEqual(snapHud({ killed: 3, total: 9, dp: 20 }), { killed: 3, total: 9, dp: 20, boss: null });
assert.deepEqual(snapHud({ killed: 3, total: 9, dp: 20 }), { killed: 3, resolved: 3, total: 9, dp: 20, boss: null });
// the capsule's numerator: the field's own enemies knocked out or leaked (a runtime split / summon knocked down
// counts in `killed` only, so the two may differ — 5/9 down but 3/9 of the round's own list resolved)
assert.deepEqual(snapHud({ killed: 5, resolved: 3, total: 9, dp: 20 }), { killed: 5, resolved: 3, total: 9, dp: 20, boss: null });
assert.deepEqual(snapHud({ boss: { hp: 50, max: 200 } }).boss, { hp: 50, max: 200 });
assert.equal(snapHud(null), null);
assert.equal(bossFrac({ hp: 50, max: 200 }), 0.25);