feedback7: merge fb7-dobermann — 教鞭 is the official three-choice (PR #353 by @Sukvii, the owner's decision), the Final Assault / Hidden Core pay a 战术特训 card's perfect coins (guarded by b.progress leaksBy), the bots put zero-range heavy guards on the road first (PR #339 by @IceCodeNew, placement only)
- Conflicts (the branch started atae22335c): spec.js / protocol.js b.progress (resolved from fb7-ui-backlog + leaksBy), gameLogic.js / game.js exports (device tips + normalizePersonalChoice), docs/PLAYING.md / design/network.md lines merged against the base, META.md (画卷 note + 教鞭), test/docs-consistency.test.js pin (both fields). - Grok review: MERGE AFTER FIXES — the HIGH (a forged boss result paid the cards) fixed on the branch (66a228c6), the META wording (1ab12763). - Golden: matches 10 (the 杜宾 bot picks and the guard placement); union: coop3-ABYSS-5 differs from HEAD only in ai_2's dmgDealt (1238011 → 1186269), no outcome change — HEAD's side had moved it, the new placement applies on that path. - Broad node --test 5623 (5621 pass, 0 fail, 2 skipped); lint 0 errors; typecheck; i18n strict clean.
This commit is contained in:
commit
2de2feee3c
57 files changed
+1339
-276
No files matched your search
+3
-2
@@ -393,12 +393,13 @@ heuristics CPU 36 / 88 → 35 / 86 ms and 16 / 40 → 16 / 40 ms.
|
|||||||
|
|
||||||
* Bounties with battles left (multi-round cards — "之后的每场作战" — last two battles since playtest #6, META §1.2)
|
* Bounties with battles left (multi-round cards — "之后的每场作战" — last two battles since playtest #6, META §1.2)
|
||||||
also spawn in the Final Assault / Hidden Core, on the owner's half (route to its goal), are shown in the boss-round
|
also spawn in the Final Assault / Hidden Core, on the owner's half (route to its goal), are shown in the boss-round
|
||||||
preview, and the boss battle uses up one of their battles (kill coins → pending funds for the Hidden Core prep).
|
preview, and the boss battle uses up one of their battles (kill coins, and a perfect-payout 战术特训 card's coins when the player's own field is perfect and the team wins → pending funds for the Hidden Core prep).
|
||||||
* A 驰援 card is never offered when its bond has no chess left in the match's pool (every member banned).
|
* A 驰援 card is never offered when its bond has no chess left in the match's pool (every member banned).
|
||||||
* docs/META.md §2.6 example guards `ctx.source.kind === 'choice'` (its EffectRef reuses the handler's key).
|
* docs/META.md §2.6 example guards `ctx.source.kind === 'choice'` (its EffectRef reuses the handler's key).
|
||||||
* 坚若磐石 = least LP lost (`titles.comment_3 { stat: 'lpLost', rule: 'min' }` in tuning.json; results.js supports
|
* 坚若磐石 = least LP lost (`titles.comment_3 { stat: 'lpLost', rule: 'min' }` in tuning.json; results.js supports
|
||||||
`rule: 'min'` among the players still alive).
|
`rule: 'min'` among the players still alive).
|
||||||
* 教鞭 / “神秘顾客” stay a random bounty, documented in docs/META.md §2.5.
|
* 教鞭 offers a personal choice of three 战术特训 cards (a bot picks by the bounty scorer, the prep's deadline at random);
|
||||||
|
“神秘顾客” stays a random bounty — docs/META.md §2.5.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+59
-19
@@ -175,18 +175,18 @@ A `choice:<effectId>` registry handler overrides the default application (§2.4)
|
|||||||
|
|
||||||
### 1.3 Disconnects, AI takeover
|
### 1.3 Disconnects, AI takeover
|
||||||
* Disconnected human: the seat keeps playing its last lineup; drafts auto-resolve at their deadlines, prep auto-readies at
|
* Disconnected human: the seat keeps playing its last lineup; drafts auto-resolve at their deadlines, prep auto-readies at
|
||||||
the deadline (the temp pieces due at that prep sold/destroyed). Nothing is bought for them. A battle the human was authority of goes to the server
|
the deadline (an open 教鞭 choice is picked at random first, §2.5; then the temp pieces due at that prep are sold/destroyed). Nothing is bought for them. A battle the human was authority of goes to the server
|
||||||
(normal / 联防: re-simulated from t = 0) or, on a boss field, to the partner's replica (DESIGN §14). The session stays
|
(normal / 联防: re-simulated from t = 0) or, on a boss field, to the partner's replica (DESIGN §14). The session stays
|
||||||
resumable for 10 min (net.js `reconnectWindowMs`); a **solo** run's for the official `constants.singleReconnectTime`
|
resumable for 10 min (net.js `reconnectWindowMs`); a **solo** run's for the official `constants.singleReconnectTime`
|
||||||
(86 400 s = 24 h, lobby.js `soloResumeWindowMs`) — nothing in a solo run is timed, so it simply waits. `onReconnect`
|
(86 400 s = 24 h, lobby.js `soloResumeWindowMs`) — nothing in a solo run is timed, so it simply waits. `onReconnect`
|
||||||
resends `m.public`, `m.private` and the `b.start` of the field the player is on / watching.
|
resends `m.public`, `m.private` and the `b.start` of the field the player is on / watching.
|
||||||
* `g.autoplay { on }` ("AI 托管"): the bot plays the seat (drafts, buying, placement, ready) until turned off.
|
* `g.autoplay { on }` ("AI 托管"): the bot plays the seat (drafts, buying, placement, an open 教鞭 choice, ready) until turned off.
|
||||||
* `onLeave` (quit / reconnect window expired): 中途退出 counts as elimination (research 00-INDEX §3, 01 §9, 06 §7 /
|
* `onLeave` (quit / reconnect window expired): 中途退出 counts as elimination (research 00-INDEX §3, 01 §9, 06 §7 /
|
||||||
§10.3): every copy the seat holds returns to the shared pool at once; the seat leaves the round loop and the Final
|
§10.3): every copy the seat holds returns to the shared pool at once; the seat leaves the round loop and the Final
|
||||||
Assault pairing (re-planned when it quits before the boss fight) and the boss pool (bloodPoint per player alive when
|
Assault pairing (re-planned when it quits before the boss fight) and the boss pool (bloodPoint per player alive when
|
||||||
the fight starts — a departed seat no longer counts, DESIGN §25.13.4); its
|
the fight starts — a departed seat no longer counts, DESIGN §25.13.4); its
|
||||||
running normal battle is force-ended; a pending band pick
|
running normal battle is force-ended; a pending band pick
|
||||||
becomes the default band and a 机变 turn passes on. Status `left`, LP 0, rounds passed = the rounds it had survived.
|
becomes the default band, a 机变 turn passes on and an open 教鞭 choice goes with the seat (`eliminate`). Status `left`, LP 0, rounds passed = the rounds it had survived.
|
||||||
When no human is left at all the match ends immediately (`reason: 'abandoned'`); when only eliminated spectators are
|
When no human is left at all the match ends immediately (`reason: 'abandoned'`); when only eliminated spectators are
|
||||||
left it ends as `'eliminated'`.
|
left it ends as `'eliminated'`.
|
||||||
|
|
||||||
@@ -263,7 +263,8 @@ like any owned unit — AI 托管 too; the buy loop's bench shed, which sells by
|
|||||||
never sells a piece gained since the bot's last prep ended, `rememberOwned`), 身份牌 / 通讯机 / 寻呼模块 on a focus
|
never sells a piece gained since the bot's last prep ended, `rememberOwned`), 身份牌 / 通讯机 / 寻呼模块 on a focus
|
||||||
member; 画卷 copies the most
|
member; 画卷 copies the most
|
||||||
valuable deployed operator; 教鞭 / “神秘顾客” are used after a perfect battle and otherwise kept in the hand — a bot drops
|
valuable deployed operator; 教鞭 / “神秘顾客” are used after a perfect battle and otherwise kept in the hand — a bot drops
|
||||||
a kept one only when it needs the hand slot (“神秘顾客” then pays its fund), a human's seat under AI 托管 never. 机变: a
|
a kept one only when it needs the hand slot (“神秘顾客” then pays its fund), a human's seat under AI 托管 never; 教鞭's three
|
||||||
|
cards are scored like a 机变 bounty draft (§2.5). 机变: a
|
||||||
bounty card is scored by its expected payout minus its expected leaks × the value of an LP (2 + 20 / LP), the kill
|
bounty card is scored by its expected payout minus its expected leaks × the value of an LP (2 + 20 / LP), the kill
|
||||||
chance from the exposure model below for that one enemy against the own board as it stands (its HP × the round's
|
chance from the exposure model below for that one enemy against the own board as it stands (its HP × the round's
|
||||||
multiplier, DEF / RES, speed, route kind) — an expected-value comparison: it prefers the card with the best payout
|
multiplier, DEF / RES, speed, route kind) — an expected-value comparison: it prefers the card with the best payout
|
||||||
@@ -275,7 +276,9 @@ its enemies; an exposure model (time on each tile × DPS against the round's arm
|
|||||||
unit's cover is the range it is deployed with under the player's loadout, `rangeRec` = `attackRangeGrid`, the grid the
|
unit's cover is the range it is deployed with under the player's loadout, `rangeRec` = `attackRangeGrid`, the grid the
|
||||||
server's `summonRange` and the card use —, blocker hold time, anti-air only on flying routes) is maximized greedily
|
server's `summonRange` and the card use —, blocker hold time, anti-air only on flying routes) is maximized greedily
|
||||||
(blockers, then damage dealers, then healers) over the server's deploy map (no 深水区, PlayerState.deployMap); a
|
(blockers, then damage dealers, then healers) over the server's deploy map (no 深水区, PlayerState.deployMap); a
|
||||||
tactician's 援军 goes on a tile of the tactician's attack range (`PlayerState.summonRange`, the server's own rule; player
|
blocker whose range is its own tile only (range 0-1, the one grid (0,0): 角峰, 古米, 泡泡, 折桠, 菲莱, 蛇屠箱, 塞雷娅, 余) takes a free
|
||||||
|
tile of the enemy road first — it blocks and hits nothing off it — and only when the road has none free any legal tile (0.2.2, PR #339 by
|
||||||
|
@IceCodeNew); a tactician's 援军 goes on a tile of the tactician's attack range (`PlayerState.summonRange`, the server's own rule; player
|
||||||
report #9 after 0.1.0), and a tile the server's `g.move` refuses is skipped for the next best one (the planner reads
|
report #9 after 0.1.0), and a tile the server's `g.move` refuses is skipped for the next best one (the planner reads
|
||||||
`board.js legalTiles` on the deploy map; `g.move` is the judge). Boss
|
`board.js legalTiles` on the deploy map; `g.move` is the judge). Boss
|
||||||
rounds: the player's boss-field template (`Match.bossWaves`) is mapped onto the own board (rows −7, the right player
|
rounds: the player's boss-field template (`Match.bossWaves`) is mapped onto the own board (rows −7, the right player
|
||||||
@@ -425,6 +428,7 @@ Writes (all validated, never throw on bad input, never make funds / pools negati
|
|||||||
| `setDeviceActive(alias, on)` / `setTileOverride(r, c, 'melee'\|'ranged'\|'none')` | terrain changes of this player's board (legality + battle input `deviceOverrides`) |
|
| `setDeviceActive(alias, on)` / `setTileOverride(r, c, 'melee'\|'ranged'\|'none')` | terrain changes of this player's board (legality + battle input `deviceOverrides`) |
|
||||||
| `addEffect(ref)` / `removeEffect(id)` / `setEffectCounter(id, v)` | EffectRefs `{ id, key?, name, desc, iconKind, iconId, counter?, battle=true, params?, data?, hidden? }` — shown in `m.private.effects`, passed to battles as `playerEffects` when `battle` |
|
| `addEffect(ref)` / `removeEffect(id)` / `setEffectCounter(id, v)` | EffectRefs `{ id, key?, name, desc, iconKind, iconId, counter?, battle=true, params?, data?, hidden? }` — shown in `m.private.effects`, passed to battles as `playerEffects` when `battle` |
|
||||||
| `addBounty(card)` | a bounty (choices.json cards.bounty shape) on the next battles |
|
| `addBounty(card)` | a bounty (choices.json cards.bounty shape) on the next battles |
|
||||||
|
| `offerBountyChoice(cards, sourceItemId)` | 教鞭 only: open a PERSONAL choice of up to three of `cards` (cards.bounty entries) for this player — shuffled once with the meta rng, kept in `m.private.personalChoice` until the owner confirms one (`g.choice { idx, choiceId }`) or the prep's deadline / an AI seat picks (§2.5); returns the intent result (`{ ok }`, or `{ error, detail }` for a phase that is not PREP, a Ready player, a choice already open or no card — before any rng draw or id, so the Art stays in the hand) |
|
||||||
| `toast(text, kind)` / `ticker(text)` / `giftTicker(fromName, chessId)` | messages |
|
| `toast(text, kind)` / `ticker(text)` / `giftTicker(fromName, chessId)` | messages |
|
||||||
| `teammates()` / `player(playerId)` | ctx objects of other alive players (team effects) |
|
| `teammates()` / `player(playerId)` | ctx objects of other alive players (team effects) |
|
||||||
|
|
||||||
@@ -458,20 +462,47 @@ bilibili BV1vzyVBuEN9 ≈ 8:24 / BV1Qkw1zMEoR ≈ 7:25, pointed out in PR #2 —
|
|||||||
is not consumed: PRTS 下半 记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)", PRTS 卫戍协议/帮助 "佩戴的
|
is not consumed: PRTS 下半 记录 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)", PRTS 卫戍协议/帮助 "佩戴的
|
||||||
装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)…时自动卸除", players re-inject it every round; player feedback after
|
装备无法手动卸除,在失去该干员(干员出售、销毁、合并等)…时自动卸除", players re-inject it every round; player feedback after
|
||||||
0.1.0), 画卷 (copy the operator in range with its
|
0.1.0), 画卷 (copy the operator in range with its
|
||||||
items — the copied items arrive unequipped in the hand, PRTS 画卷 备注 "获得的装备为未装备状态"), 教鞭 / “神秘顾客” (a random bounty is added).
|
items — the copied items arrive unequipped in the hand, PRTS 画卷 备注 "获得的装备为未装备状态"), 教鞭 (a personal choice of three 战术特训 cards), “神秘顾客” (a random bounty is added).
|
||||||
|
|
||||||
**教鞭 / “神秘顾客” stay a random bounty (deliberate).** The official Arts open a personal 悬赏 choice
|
**教鞭 offers a personal choice of three 战术特训 cards (0.2.2); “神秘顾客” stays a random bounty.** The official Arts open a
|
||||||
("选择一项(特殊)悬赏任务进行挑战", `choice_event hunter_band_1`). The server applies a random bounty instead: content
|
personal 悬赏 choice ("选择一项(特殊)悬赏任务进行挑战", `choice_event hunter_band_1`, choiceType `PERSONAL_CHOOSE`). 教鞭
|
||||||
(server/sim/content/items/meta.js) draws 3 cards and takes one at random — 教鞭 from the 20 战术特训 cards (payout
|
(PRTS 下半 记录 §法术 教鞭 "使用后销毁,于3个战术特训的悬赏任务中选择一项"; §机变阶段 "※以下悬赏任务仅由法术教鞭生成"; 杜宾
|
||||||
`perfect`: extra enemies, the card's coins when the own phase is perfect; PRTS 下半 记录 §法术 教鞭 "于3个战术特训的悬赏
|
加练! "<教鞭>:使用后为下场战斗添加额外敌人,若自身战斗完美作战可获得资金"; user playtest #6 item 4 review) offers three of
|
||||||
任务中选择一项", §机变阶段 "※以下悬赏任务仅由法术教鞭生成", 杜宾 加练! "若自身战斗完美作战可获得资金"; user playtest #6
|
the 20 战术特训 cards (payout `perfect`: extra enemies, the card's coins when the own phase is perfect), each limited to
|
||||||
item 4 review), “神秘顾客” (granted by nothing in act2) from the band-bounty family `enemyeffect_b_*` [ASSUMED] — each
|
enemies the mode can field. Until 0.2.1 the server took one of the three at random — a deliberate simplification: a
|
||||||
limited to enemies the mode can field (the built-in fallback: a random tier ≤ II bounty of `cards.bounty`), and adds it
|
personal choice looked like it needed a phase of its own, a protocol message outside SP_DRAFT, a timer and an AI
|
||||||
with `ctx.addBounty`. A personal choice overlay would need its own phase / protocol message outside SP_DRAFT (a
|
takeover for a rarely used Art. The owner decided on 2026-10-08 to follow the official choice (PR #353 by @Sukvii), and
|
||||||
second, simultaneous draft in co-op, with timers and AI takeover) for a rarely used Art, while the random pick keeps the
|
it fits in the existing parts:
|
||||||
risk / reward the item is about. The
|
|
||||||
bounty then behaves like any other (next battles, 联防 payouts, Final Assault spawns). 神秘顾客's destroy clause (+1
|
* **Offer.** Content (server/sim/content/items/meta.js) calls `ctx.offerBountyChoice(cards, itemId)` (`Match.offerBountyChoice`,
|
||||||
fund, the Art passes to the next alive player) is content too (`onDestroy`).
|
match/spDraft.js): the eligible cards are shuffled once with the meta rng and up to three different ones are kept in
|
||||||
|
`ps.personalChoice { id, round, sourceItemId, cards }`. The Art is used up (and counts for the two per round) when the
|
||||||
|
offer is made; nothing is applied yet. Refused — the Art stays in the hand, no rng draw, no id — outside the player's
|
||||||
|
PREP, for a Ready player, while an offer is already open and when no card is left to offer.
|
||||||
|
* **Private.** Only the owner sees it: `m.private.personalChoice` (re-sent on a reconnect or a refresh, so the same cards
|
||||||
|
and id come back); never `m.public`, a scout's view or a spectator. Players have offers of their own, at the same time.
|
||||||
|
* **Confirm.** `g.choice { idx, choiceId }` (`Match.pickPersonalChoice`) validates the owner, the id, the round and the
|
||||||
|
index, then adds the card with `addBounty` and closes the offer; a request without a `choiceId` is the public 机变 draft
|
||||||
|
as ever, and a stale, foreign or repeated id never falls through to it (`BAD_TARGET`). The client shows the overlay of
|
||||||
|
the 机变 draft (`ChoiceOverlay`, `personal`: the same two taps, no order, no turns) over a locked prep.
|
||||||
|
* **No phase, no timer.** The offer lives inside PREP. Ready is refused while it is open (`setReady`: `BAD_TARGET`;
|
||||||
|
`m.private.canReady` false), and the prep's own deadline resolves it: in a timed co-op prep `prepDeadline` takes one of
|
||||||
|
the cards at random (the meta rng, like a 机变 turn that runs out) before the temp pieces and Ready; a solo or
|
||||||
|
single-human prep is untimed and has no timer for it, and a disconnected player waits for the deadline like for any
|
||||||
|
other prep action. A seat the engine plays — an AI teammate or a human under AI 托管 — picks like the bots do; a seat that
|
||||||
|
left is eliminated (`onLeave` → `eliminate`), which drops its open offer: nobody picks for it.
|
||||||
|
* **The bots.** `bot.js` scores the three cards like a 机变 bounty draft (`botPickCard` → `bountyScore`: the expected
|
||||||
|
payout minus the expected leaks × the value of an LP, against the own board; the bots' seeded rng only breaks ties), so
|
||||||
|
the choice is deterministic for a seed: at the start of the bot's prep (an offer left by a human who turned 托管 on),
|
||||||
|
right after it cast the Art, and before Ready. When a step of the bot's prep fails, the scheduler's fallback readies the
|
||||||
|
seat with the random pick instead — no seat is ever left un-ready by an offer.
|
||||||
|
* The chosen bounty is a bounty like any other: next battles (a multi-round card lasts two, §1.2), 联防 payouts, the Final
|
||||||
|
Assault / Hidden Core spawns — and in the boss rounds its perfect-battle coins (§3 Waves).
|
||||||
|
|
||||||
|
“神秘顾客” (granted by nothing in act2, no PRTS list of its cards) remains random: three cards are drawn from the band-bounty
|
||||||
|
family `enemyeffect_b_*` [ASSUMED], limited to enemies the mode can field, and one is added with `ctx.addBounty` (the
|
||||||
|
built-in fallback: a random tier ≤ II bounty of `cards.bounty`). Its destroy clause (+1 fund, the Art passes to the next
|
||||||
|
alive player) is content too (`onDestroy`).
|
||||||
EffectRefs: `effect:builtin_round_coin`, `effect:builtin_gift`, `effect:builtin_next_buy_golden_item` (整备),
|
EffectRefs: `effect:builtin_round_coin`, `effect:builtin_gift`, `effect:builtin_next_buy_golden_item` (整备),
|
||||||
`effect:builtin_next_buy_elite` (升华). An eliminated player gets no dispatch, except an EffectRef whose handler sets
|
`effect:builtin_next_buy_elite` (升华). An eliminated player gets no dispatch, except an EffectRef whose handler sets
|
||||||
`afterElimination: true` (its `onRoundStart` still runs: `EffectDispatcher.dispatchEliminated`, called by
|
`afterElimination: true` (its `onRoundStart` still runs: `EffectDispatcher.dispatchEliminated`, called by
|
||||||
@@ -619,7 +650,16 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
|||||||
`enemyScale[r]` × the tuning layer §3.1, bounties, boss templates, 联防 routing). Bounties with battles left (every one
|
`enemyScale[r]` × the tuning layer §3.1, bounties, boss templates, 联防 routing). Bounties with battles left (every one
|
||||||
that lasts more than one battle; an official multi-round card lasts two, §1.2) also spawn in the Final Assault /
|
that lasts more than one battle; an official multi-round card lasts two, §1.2) also spawn in the Final Assault /
|
||||||
Hidden Core, on the owner's half of the boss field (a route ending at its goal); the boss battle then uses up one of
|
Hidden Core, on the owner's half of the boss field (a route ending at its goal); the boss battle then uses up one of
|
||||||
the bounty's battles and its kill coins go to pending funds.
|
the bounty's battles and its kill coins go to pending funds — with, for every perfect-payout card (战术特训), its coins
|
||||||
|
as after a normal battle ("若各自行动阶段就达成完美作战,获得N资金"): once per player, when the team won (the first end the
|
||||||
|
server registered: the leader's pool empty), the result is a real one (not the stand-in of a field that never
|
||||||
|
reported) and the player's own field let no counted enemy through [ASSUMED: the text has no boss-battle exception, and
|
||||||
|
the card's enemies do come in the boss battle] — and, for a field a client reported (the boss path never re-simulates
|
||||||
|
it), the result agrees with what its authority reported while it fought: LP it asked the team to pay must have been
|
||||||
|
split in `b.progress.leaksBy` (per player, the enemy leaks; the rest is the leader's own "扣除目标生命" effects), and a
|
||||||
|
player's leaked list must show at least its reported leak LP (`Match._bossLeaksAgree`), so a result cannot hide a charged
|
||||||
|
leak or hand it to the other seat; a server-run field is the truth itself. They are spent in the Hidden Core's prep like
|
||||||
|
the kill coins.
|
||||||
* **Combat time limit**: data `combatTimeLimit` (the level's `maxPlayTime`) counts REAL seconds of the forced 2×
|
* **Combat time limit**: data `combatTimeLimit` (the level's `maxPlayTime`) counts REAL seconds of the forced 2×
|
||||||
battle; the Battle / 联防 limit is `gd.combatTimeLimit(r)` = 2 × that in game seconds (`config.combatTimeScale`,
|
battle; the Battle / 联防 limit is `gd.combatTimeLimit(r)` = 2 × that in game seconds (`config.combatTimeScale`,
|
||||||
default 2). Read as game seconds the rounds' own spawn schedules would not fit (R2's last flyer spawns at 43 s of
|
default 2). Read as game seconds the rounds' own spawn schedules would not fit (R2's last flyer spawns at 43 s of
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@
|
|||||||
| 招募价格(按阶级 I–VI) | 2 / 3 / 3 / 3 / 4 / 4 |
|
| 招募价格(按阶级 I–VI) | 2 / 3 / 3 / 3 / 4 / 4 |
|
||||||
| 刷新 | 1 资金 |
|
| 刷新 | 1 资金 |
|
||||||
| 出售干员 | 任何干员(含精锐)都只返还 **1** 资金 |
|
| 出售干员 | 任何干员(含精锐)都只返还 **1** 资金 |
|
||||||
| 装备 / 法术 | 调度中心出售除特殊装备外的所有装备:4 把特殊维式重锤(战栗 / 坚固 / 加速 / 灼燃)只来自维多利亚盟约(每 25 层)和洛洛的定制品,突变细胞只来自昆图斯的策略。装备不能出售,整备区里的只能「销毁」(不返还)。已配发的装备锁定在干员身上:只能被第三件装备替换(弹出替换窗口,选择要替换的一件,它会被销毁;盟约之币、博士投影等装备即销毁的道具也一样,先销毁选中的那件再生效,干员留下一个空栏位,精锐干员不能使用博士投影,此时什么都不销毁),或在干员出售 / 合成精锐时退回整备区。**突变细胞**不会用完:战斗结束后装备者被销毁(部署在场上的话,原来的格子空出来,剩余可放置角色加 1),突变细胞和它的其他装备先退回整备区,可以再次配发给任意干员;随后获得一名高一阶(最高 VI)的随机普通干员(装备者是精锐也一样),和其他新获得的干员一样进入整备区(满了进入临时整备区),需要自己重新部署;如果它凑齐了合成所需的同名干员,照常合成精锐。**坚固维式重锤**首次受到致命伤害时锁血 8 秒,**每次部署一次**:被击倒再部署(或突袭跳跃再部署、被 M3茧甲 / 埃芒加德复活)后又能锁一次,娜仁图亚策略借来的锤子也一样;借用到期时正在锁血的会锁满 8 秒;站在阿戈尔干员身前被开场吞噬、自带源石溶剂掉血时也会用掉这一次 |
|
| 装备 / 法术 | 调度中心出售除特殊装备外的所有装备:4 把特殊维式重锤(战栗 / 坚固 / 加速 / 灼燃)只来自维多利亚盟约(每 25 层)和洛洛的定制品,突变细胞只来自昆图斯的策略。装备不能出售,整备区里的只能「销毁」(不返还)。已配发的装备锁定在干员身上:只能被第三件装备替换(弹出替换窗口,选择要替换的一件,它会被销毁;盟约之币、博士投影等装备即销毁的道具也一样,先销毁选中的那件再生效,干员留下一个空栏位,精锐干员不能使用博士投影,此时什么都不销毁),或在干员出售 / 合成精锐时退回整备区。**突变细胞**不会用完:战斗结束后装备者被销毁(部署在场上的话,原来的格子空出来,剩余可放置角色加 1),突变细胞和它的其他装备先退回整备区,可以再次配发给任意干员;随后获得一名高一阶(最高 VI)的随机普通干员(装备者是精锐也一样),和其他新获得的干员一样进入整备区(满了进入临时整备区),需要自己重新部署;如果它凑齐了合成所需的同名干员,照常合成精锐。**坚固维式重锤**首次受到致命伤害时锁血 8 秒,**每次部署一次**:被击倒再部署(或突袭跳跃再部署、被 M3茧甲 / 埃芒加德复活)后又能锁一次,娜仁图亚策略借来的锤子也一样;借用到期时正在锁血的会锁满 8 秒;站在阿戈尔干员身前被开场吞噬、自带源石溶剂掉血时也会用掉这一次。**教鞭**(杜宾的策略每 2 个回合给一枚)拖到场上使用后即销毁,休整期里弹出 3 张战术特训悬赏(只有你自己看得到,刷新页面或断线重连后还是这 3 张),点一张再点一次确认,选中的那张成为接下来的悬赏;选定之前不能点「准备就绪」。同盟模式的休整期倒计时结束时还没选,会随机选一张;独立模拟和只有一名玩家时休整期不限时,所以不会自动选。AI 队友和 AI 托管的座位按悬赏评分自己选。战术特训的赏金要「各自行动阶段就达成完美作战」才给(最终攻势、隐秘核心里是你这一侧没有漏怪且全队获胜);“神秘顾客”仍然随机给一个悬赏 |
|
||||||
| 冻结 | 免费,保留当前所有未买的格子到下一回合;冻结状态下仍可主动刷新。策略「梓兰」的每次主动刷新会单独冻结送来的那张同名干员:只有这一张卡显示冰霜(上方的冻结按钮不亮),没有再刷新就留到下一回合,再手动刷新会连它一起重抽 |
|
| 冻结 | 免费,保留当前所有未买的格子到下一回合;冻结状态下仍可主动刷新。策略「梓兰」的每次主动刷新会单独冻结送来的那张同名干员:只有这一张卡显示冰霜(上方的冻结按钮不亮),没有再刷新就留到下一回合,再手动刷新会连它一起重抽 |
|
||||||
| 升级调度中心(1→2 … 5→6) | 险境及以上 5 / 8 / 11 / 12 / 13;同盟标准 5 / 8 / 10 / 11 / 11;独立标准 1 / 1 / 5 / 8 / 10。**每过一回合价格 −1**(最低 0),升级后按下一级的价格重新计算。升级到干员栏位更多的等级(险境及以上 2 级 4 个、4 级 5 个;标准 2 级 4 个、6 级 5 个)时,**新栏位立刻出现,但空着不补干员**(官方如此:升级只开位,新位在下一次刷新——手动刷新或回合开始的自动刷新——时才会出现干员;空栏位只画一个空框,不写「已招募」),已有的卡片不变 |
|
| 升级调度中心(1→2 … 5→6) | 险境及以上 5 / 8 / 11 / 12 / 13;同盟标准 5 / 8 / 10 / 11 / 11;独立标准 1 / 1 / 5 / 8 / 10。**每过一回合价格 −1**(最低 0),升级后按下一级的价格重新计算。升级到干员栏位更多的等级(险境及以上 2 级 4 个、4 级 5 个;标准 2 级 4 个、6 级 5 个)时,**新栏位立刻出现,但空着不补干员**(官方如此:升级只开位,新位在下一次刷新——手动刷新或回合开始的自动刷新——时才会出现干员;空栏位只画一个空框,不写「已招募」),已有的卡片不变 |
|
||||||
| 其他收入 | 悬赏敌人、盟约「远见」「奇迹」、装备「骑士储蓄罐」、部分策略和特质 |
|
| 其他收入 | 悬赏敌人、盟约「远见」「奇迹」、装备「骑士储蓄罐」、部分策略和特质 |
|
||||||
|
|||||||
@@ -177,6 +177,6 @@ Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies
|
|||||||
- items: 56 equipment (+golden) + 3 Arts; 变形同构体 bond grant; VI-tier set/combos.
|
- items: 56 equipment (+golden) + 3 Arts; 变形同构体 bond grant; VI-tier set/combos.
|
||||||
- bands: 40 strategies (meta + battle). 鸭爵 “神秘顾客” swaps 0–2 ground enemies (not the leader, its parts or bounties; on a pair boss field only the player's half — the boss / hidden `onBattleStart` event carries `routes` and `side`) in every teammate's normal / Final Assault / Hidden Core battle from R5 into the act2 鸭爵 / 高普尼克 / 流泪小子 / 圆仔 (lpr 1, 1 fund to the killer, 联防 included) (§21.5); the swaps carry the spawn tag `'duck'`, and the swapped 流泪小子 hides again 1 s after a block ends, not the official 0 s (the owner's decision of 2026-10-07, §25.22.6).
|
- bands: 40 strategies (meta + battle). 鸭爵 “神秘顾客” swaps 0–2 ground enemies (not the leader, its parts or bounties; on a pair boss field only the player's half — the boss / hidden `onBattleStart` event carries `routes` and `side`) in every teammate's normal / Final Assault / Hidden Core battle from R5 into the act2 鸭爵 / 高普尼克 / 流泪小子 / 圆仔 (lpr 1, 1 fund to the killer, 联防 included) (§21.5); the swaps carry the spawn tag `'duck'`, and the swapped 流泪小子 hides again 1 s after a block ends, not the official 0 s (the owner's decision of 2026-10-07, §25.22.6).
|
||||||
- enemies: special-type behaviours (FLY, TIMES, ELEMENT, DOT, INVISIBLE, REFLECTION, SPECIAL) and notable individual abilities from `data/enemies.json`; bosses 1–10 scripted — the huge leaders (假想敌:胄 ×2, 假想敌:管 ×2, 盐风主教昆图斯, 阿利斯泰尔, “萨米的意志”) are 自缚 + 无法被阻挡 (PRTS 天赋; `content/bosses.js SELF_BOUND`: a persistent noMove + unblockable buff from spawn) with their hit area (§19.4); 圆仔 and 鸭爵 are 无法被阻挡 (PRTS 天赋, §21.5); the knock-out forms follow PRTS — 深池逐火 余烬 (5 / 10 hits, 隐匿, blockable, back after 10 s), 转译基底·α (damage cancelled until its 2 s change into 寻仇者 / 特战术师 / 幽灵), the leaders' 重生, 守墓石像's statue and flight — and every 重生 clears what operators put on the enemy and restarts its skills from their initial cooldown (§21.4, §21.19); the mode's air units are 静态刚体, never moved (§21.9); leader parts (斩胄之剑 / 破胄之锤 / 碎铳之簧) pass every damage they take to their leader 1:1 as 无来源 HP loss (`PART_TRANSFER` 1, PRTS "等量"), a 假想敌:胄 drone that dies costs it 2 % of the pool (`DRONE_LINK_BASE 'pool'`) (§20.10, §20.13); devices/terrain.
|
- enemies: special-type behaviours (FLY, TIMES, ELEMENT, DOT, INVISIBLE, REFLECTION, SPECIAL) and notable individual abilities from `data/enemies.json`; bosses 1–10 scripted — the huge leaders (假想敌:胄 ×2, 假想敌:管 ×2, 盐风主教昆图斯, 阿利斯泰尔, “萨米的意志”) are 自缚 + 无法被阻挡 (PRTS 天赋; `content/bosses.js SELF_BOUND`: a persistent noMove + unblockable buff from spawn) with their hit area (§19.4); 圆仔 and 鸭爵 are 无法被阻挡 (PRTS 天赋, §21.5); the knock-out forms follow PRTS — 深池逐火 余烬 (5 / 10 hits, 隐匿, blockable, back after 10 s), 转译基底·α (damage cancelled until its 2 s change into 寻仇者 / 特战术师 / 幽灵), the leaders' 重生, 守墓石像's statue and flight — and every 重生 clears what operators put on the enemy and restarts its skills from their initial cooldown (§21.4, §21.19); the mode's air units are 静态刚体, never moved (§21.9); leader parts (斩胄之剑 / 破胄之锤 / 碎铳之簧) pass every damage they take to their leader 1:1 as 无来源 HP loss (`PART_TRANSFER` 1, PRTS "等量"), a 假想敌:胄 drone that dies costs it 2 % of the pool (`DRONE_LINK_BASE 'pool'`) (§20.10, §20.13); devices/terrain.
|
||||||
- choices: 机变 families (悬赏决策 bounties, 道具补给, 机密商店, 战术决策 team/personal buffs). 悬赏决策 = the official structure of its round (choices.json `bountyDrafts`, 86 drafted cards: R3 a fixed set of six 两场 cards, R9 6 of a boss group's list, R11 6 of a seen 7-card list; §21.2); 机密商店 (R11 the official composition) and 战术决策 can offer the same card twice (`shopDraft` / `tacticDraft`); bounty cards carry `descRaw`; no draft offers a multi-round card, and 教鞭's lasts two battles (`choices.js MULTI_ROUND_BOUNTY_BATTLES`, §20.6). 教鞭 = a random 战术特训 card (3 drawn, 1 taken).
|
- choices: 机变 families (悬赏决策 bounties, 道具补给, 机密商店, 战术决策 team/personal buffs). 悬赏决策 = the official structure of its round (choices.json `bountyDrafts`, 86 drafted cards: R3 a fixed set of six 两场 cards, R9 6 of a boss group's list, R11 6 of a seen 7-card list; §21.2); 机密商店 (R11 the official composition) and 战术决策 can offer the same card twice (`shopDraft` / `tacticDraft`); bounty cards carry `descRaw`; no draft offers a multi-round card, and 教鞭's lasts two battles (`choices.js MULTI_ROUND_BOUNTY_BATTLES`, §20.6). Pointing Stick (教鞭) offers up to three private Tactical Training cards during PREP; the owner confirms one through `g.choice` with its `choiceId`, with Ready blocked until confirmation or the existing prep deadline's auto-pick. “神秘顾客” remains random (META §2.5).
|
||||||
|
|
||||||
---
|
---
|
||||||
+13
-2
@@ -14,7 +14,8 @@ LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed
|
|||||||
[SP_DRAFT] (if r ∈ spRounds: 机变 draft; co-op: random order (「AI 队友最后选择」, off by default: humans first, the same partition as the strategy draft), 30 s first picker / 16 s others; solo 3 cards; solo / single human untimed;
|
[SP_DRAFT] (if r ∈ spRounds: 机变 draft; co-op: random order (「AI 队友最后选择」, off by default: humans first, the same partition as the strategy draft), 30 s first picker / 16 s others; solo 3 cards; solo / single human untimed;
|
||||||
a card is picked with two taps, §18.2)
|
a card is picked with two taps, §18.2)
|
||||||
PREP (co-op: timer from config, ends when all alive humans+bots ready; solo / single human: untimed, ends on ready)
|
PREP (co-op: timer from config, ends when all alive humans+bots ready; solo / single human: untimed, ends on ready)
|
||||||
PREP_END (<休整期结束时> effects; unfrozen shop cleared; temp overflow: Ready is blocked while temp is non-empty; at the deadline
|
PREP_END (<休整期结束时> effects; unfrozen shop cleared; Ready is blocked while a personal choice or temp overflow is pending;
|
||||||
|
at the deadline personal choices are auto-picked first, then
|
||||||
the temp pieces due at this prep are resolved (chess sold back, items destroyed, summon stacks removed — they come back at the next round start, §20.1) — pieces that arrived
|
the temp pieces due at this prep are resolved (chess sold back, items destroyed, summon stacks removed — they come back at the next round start, §20.1) — pieces that arrived
|
||||||
after Ready or during the <休整期结束时> effects wait for the next prep (§6.2 tempDue))
|
after Ready or during the <休整期结束时> effects wait for the next prep (§6.2 tempDue))
|
||||||
COMBAT (one Battle per alive player, all in parallel, 2×; watchers get snapshots; solo: pausable, §14 Solo pause)
|
COMBAT (one Battle per alive player, all in parallel, 2×; watchers get snapshots; solo: pausable, §14 Solo pause)
|
||||||
@@ -44,6 +45,7 @@ Solo FUNNY has 9 rounds (boss at R9); everything is read from `data/config.json
|
|||||||
board: Map<'r,c', Piece>, // chess & token pieces on the normal field
|
board: Map<'r,c', Piece>, // chess & token pieces on the normal field
|
||||||
bonds: { [bondId]: { count, active, tier, layers } }, // layers persist all match
|
bonds: { [bondId]: { count, active, tier, layers } }, // layers persist all match
|
||||||
effects: [ EffectRef ], // band/choice/team/garrison persistent effects & counters
|
effects: [ EffectRef ], // band/choice/team/garrison persistent effects & counters
|
||||||
|
personalChoice: null | { id, round, sourceItemId, cards: [ BountyCard ] }, // private PREP offer; raw cards on the server
|
||||||
counters: { refreshes, buys, sells, … },
|
counters: { refreshes, buys, sells, … },
|
||||||
stats: { … for results } }
|
stats: { … for results } }
|
||||||
Slot = { kind:'chess'|'item', id, price, sold:false, frozen }
|
Slot = { kind:'chess'|'item', id, price, sold:false, frozen }
|
||||||
@@ -62,6 +64,12 @@ Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (inco
|
|||||||
|
|
||||||
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants — the operator a 突变细胞 transformation gains included, §21.1 —, battle-result rewards, equipment returned by a merged / sold / transformed carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
|
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants — the operator a 突变细胞 transformation gains included, §21.1 —, battle-result rewards, equipment returned by a merged / sold / transformed carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
|
||||||
|
|
||||||
|
Pointing Stick creates at most one pending offer per alive, non-Ready player in PREP. The owner confirms it with
|
||||||
|
`g.choice { idx, choiceId }`; confirmation validates its ID, round and index, adds one bounty and clears the offer.
|
||||||
|
Ready requires no pending offer as well as an empty temp area. The co-op prep deadline resolves the offer randomly
|
||||||
|
before resolving temp and setting Ready; untimed solo / single-human prep does not schedule a separate choice timer.
|
||||||
|
The offer is recipient-private, including after reconnect, and never enters `m.public.sp` or a teammate's scout view.
|
||||||
|
|
||||||
### 6.3 Bonds & layers (bondsMeta.js)
|
### 6.3 Bonds & layers (bondsMeta.js)
|
||||||
|
|
||||||
- `count` = number of **distinct base chess** on the board carrying the bond (`BOARD`); `BOARD_AND_DECK` bonds also count the hand; 绝技 counts elites on board; 调和 adds +1 to each core bond count that has a real member on the board (the views mark such an entry `harmony: 1`, §21.26), etc. (research 02).
|
- `count` = number of **distinct base chess** on the board carrying the bond (`BOARD`); `BOARD_AND_DECK` bonds also count the hand; 绝技 counts elites on board; 调和 adds +1 to each core bond count that has a real member on the board (the views mark such an entry `harmony: 1`, §21.26), etc. (research 02).
|
||||||
@@ -81,7 +89,10 @@ Per match: pick stage (weighted among the mode's `config.modes[m].stages`, weigh
|
|||||||
|
|
||||||
### 6.6 AI players (bot.js)
|
### 6.6 AI players (bot.js)
|
||||||
|
|
||||||
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP; never one built around a bond the mode switches off — `gd.bandBondIds`, §21.26); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a tactician's 援军 within `PlayerState.summonRange`; a melee chess whose trait reads 「可以放置于远程位」 (钩索师 / 推击手) on a 高台 whose range covers the enemy road, else on the ground, counted as a blocker in the lineup either way — §3; `g.move` judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it, the operator its transformation gains is deployed by the next prep's lineup step like any owned unit — AI 托管 too; the buy loop's bench shed never sells a piece gained since the bot's last prep, §21.1); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (`botPrepBeginSteps`, 25–70 ms late in a 4-bot match), the layout rehearsal and `botPrepEndSteps` — runs in wall-clock slices of `botSliceMs` (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time). Under AI 托管 a human's own 自选 pieces score `DIY_PIECE_BONUS` (12) more in the shop / reward choice and the 调度中心 is levelled one round earlier for the step that opens a slotted tier (`levelUp`, `diyOpensAt`; 0.2.0 — bots field no 自选 piece).
|
Pending Pointing Stick offers use the existing bounty scorer at bot prep entry, immediately after a successful cast,
|
||||||
|
and before Ready. The prep's deadline — and the scheduler's fallback when a step of a bot's prep fails — randomly resolves any remaining offer before temp and Ready; a seat that left is eliminated and its offer dropped.
|
||||||
|
|
||||||
|
Deterministic-ish heuristic player used for (a) AI teammates, (b) auto-play for disconnected humans (only auto-ready + auto-picks, not shopping, unless "AI 托管" is toggled), (c) tests. Strategy (§21.6): pick a band (weighted by starting LP; never one built around a bond the mode switches off — `gd.bandBondIds`, §21.26); each prep: take offers, sell dead bench weight, fill the board, complete affordable merges, level on the curve, then buy only merge progress / lineup upgrades worth more than the refreshes their price would buy (shop odds of completing held pairs), freeze for an unaffordable third copy (坎诺特 keeps 5 funds back); build around a committed focus core bond (a teammate's main bond on its strip counts against it; bonds the mode switches off never) + a second bond; value dealers by DPS vs the round's DEF / RES (an attack on every enemy in range counts double — 阵法术师 / 轰击术师), with the range the unit is deployed with; place by the exposure model + rehearsal on the server's deploy map (no 深水区; a blocker whose range is its own tile only — range 0-1 — takes a free tile of the enemy road first; a tactician's 援军 within `PlayerState.summonRange`; a melee chess whose trait reads 「可以放置于远程位」 (钩索师 / 推击手) on a 高台 whose range covers the enemy road, else on the ground, counted as a blocker in the lineup either way — §3; `g.move` judges legality); items by effect (META §1.5: 突变细胞 on the least valuable normal single below 6阶, a cell left in temp gets a hand slot made for it, the operator its transformation gains is deployed by the next prep's lineup step like any owned unit — AI 托管 too; the buy loop's bench shed never sells a piece gained since the bot's last prep, §21.1); 机变 bounties by expected payout − expected LP loss over the offered cards; resolve temp — on a human seat under AI 托管 never by destroying an item while a bench operator can be sold for the slot —; ready. A bot's whole prep — shop decisions and layout planning (`botPrepBeginSteps`, 25–70 ms late in a 4-bot match), the layout rehearsal and `botPrepEndSteps` — runs in wall-clock slices of `botSliceMs` (8 ms) per scheduler callback on a real host (step generators that yield between whole actions, never with a transient board; the same actions in the same order, so decisions are unchanged; at once in virtual time). Under AI 托管 a human's own 自选 pieces score `DIY_PIECE_BONUS` (12) more in the shop / reward choice and the 调度中心 is levelled one round earlier for the step that opens a slotted tier (`levelUp`, `diyOpensAt`; 0.2.0 — bots field no 自选 piece).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Platform additions (implemented): `welcome.{resumed, version}`, `welcome.diyKitt
|
|||||||
**Spectator seats** (a remake feature — the official room has none; community report #26, owner's decision 2026-10-04; the spectator-seat subsection of §23): `room.spectate {code}` takes one of a co-op room's `MAX_SPECTATORS` (2) spectator seats, in its lobby or during its match; `room.removeSpectator {playerId}` (host, at any time — the room screen's strip and, once the match runs, the game screen's 观战席 capsule in the top bar, GitHub #120) frees one (`room.closed {reason:'kicked'}` to it, no match frame after it); `room.state.spectators: [{playerId, name, connected}]` (still pushed while the match runs). A spectator is never a player (not in `seats`, not counted for the start, never host, keeps no room alive — `room.closed {empty}` when the last human leaves), receives the room's broadcasts and watches like an eliminated player (`Match.addSpectator`: `b.start` watch / `m.field`, never `m.private`); it may only `g.watch`, leave, and store a `room.loadout` — anything else answers `error SPECTATOR`. Its seat is kept and given back on reconnect like a player's.
|
**Spectator seats** (a remake feature — the official room has none; community report #26, owner's decision 2026-10-04; the spectator-seat subsection of §23): `room.spectate {code}` takes one of a co-op room's `MAX_SPECTATORS` (2) spectator seats, in its lobby or during its match; `room.removeSpectator {playerId}` (host, at any time — the room screen's strip and, once the match runs, the game screen's 观战席 capsule in the top bar, GitHub #120) frees one (`room.closed {reason:'kicked'}` to it, no match frame after it); `room.state.spectators: [{playerId, name, connected}]` (still pushed while the match runs). A spectator is never a player (not in `seats`, not counted for the start, never host, keeps no room alive — `room.closed {empty}` when the last human leaves), receives the room's broadcasts and watches like an eliminated player (`Match.addSpectator`: `b.start` watch / `m.field`, never `m.private`); it may only `g.watch`, leave, and store a `room.loadout` — anything else answers `error SPECTATOR`. Its seat is kept and given back on reconnect like a player's.
|
||||||
|
|
||||||
### 8.2 Match
|
### 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`
|
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, choiceId?}` (with an ID: the recipient's PREP personal choice; without one: the public SP_DRAFT; stale or wrong IDs never fall through to the public draft) · `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:
|
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...], 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.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.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).
|
||||||
@@ -41,7 +41,8 @@ S→C:
|
|||||||
|
|
||||||
`m.private`:
|
`m.private`:
|
||||||
```js
|
```js
|
||||||
{ playerId, seat, alive, lp, funds, bandId, ready, canReady /* false while temp non-empty */,
|
{ playerId, seat, alive, lp, funds, bandId, ready, canReady /* false while a personal choice or temp overflow is pending */,
|
||||||
|
personalChoice: null | { id, round, sourceItemId, cards: [ { kind: 'bounty', id, name, desc, descRaw, ... } ] }, // recipient only, including reconnect; absent from public / scout views
|
||||||
shop: { level, maxLevel: 6, upgradePrice, refreshPrice /* effective (0 when a free refresh is available) */, freeRefreshes,
|
shop: { level, maxLevel: 6, upgradePrice, refreshPrice /* effective (0 when a free refresh is available) */, freeRefreshes,
|
||||||
frozen, slots: [ { kind: 'chess'|'item', id, price, basePrice, sold, frozen } | null ], // slot.frozen: the 冻结 toggle copies onto every unsold slot, and 梓兰's 猎头顾问 freezes ONE card of an active refresh with the toggle off — every card draws its own flag (GitHub #354)
|
frozen, slots: [ { kind: 'chess'|'item', id, price, basePrice, sold, frozen } | null ], // slot.frozen: the 冻结 toggle copies onto every unsold slot, and 梓兰's 猎头顾问 freezes ONE card of an active refresh with the toggle off — every card draws its own flag (GitHub #354)
|
||||||
rewardOffer: null | { tier, source: 'merge'|'special', label, queued, slots: [ { kind: 'chess'|'item', id, price: 0, sold } ] } }, // §21.2
|
rewardOffer: null | { tier, source: 'merge'|'special', label, queued, slots: [ { kind: 'chess'|'item', id, price: 0, sold } ] } }, // §21.2
|
||||||
@@ -83,7 +84,7 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ {
|
|||||||
**Protocol (additions; `shared/protocol.js` is normative).**
|
**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.
|
- `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).
|
- 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, 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.progress { battleId, gt, killed, total, resolved?, leaks?, left?, bossDmg?, by?, leaksBy?, 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`), `leaksBy` = its split per player (the LP the enemies that reached that player's goal cost: every leak entry the result will show × its `lpr`; `leaks` minus its sum is the leader's effects; `spec.js battleProgress`) — the server keeps the highest value per player and, before a perfect-payout bounty pays in a client-reported field, requires the result's leaked list of that player to show at least that much and the LP asked for to have been split at all (`Match._bossLeaksAgree`, the boss path does not re-simulate). 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`).
|
- 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.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.
|
- 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.
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ Eight workstreams were merged into `playtest6` (WH → WG → WD → WC → WB
|
|||||||
- **#19**: PRTS 帮助 "临时刷新3名高于调度中心等级1阶(最多6阶)的干员"; the official 玩法介绍 image shows three different operators at price 0. v2.4.1 rolled three times independently (14–21 % of offers had a duplicate). **Now** three different chess, copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED]; the offer reserves no copies. The integration QA found the same repeat in the other free pick-one chess offers — 信标 (same tier), 寻呼模块 (same bond ≤ shop level, content/items/meta.js) and the engine default `builtinMeta use_equip_reward_special_goods_char_chess` — they now draw different operators too (信标 tops up from the tier below, 寻呼模块 shows fewer cards when the pool has no other one), and `pushRewardOffer` drops a repeated id from any list it is given (`test/content/items.test.js`).
|
- **#19**: PRTS 帮助 "临时刷新3名高于调度中心等级1阶(最多6阶)的干员"; the official 玩法介绍 image shows three different operators at price 0. v2.4.1 rolled three times independently (14–21 % of offers had a duplicate). **Now** three different chess, copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED]; the offer reserves no copies. The integration QA found the same repeat in the other free pick-one chess offers — 信标 (same tier), 寻呼模块 (same bond ≤ shop level, content/items/meta.js) and the engine default `builtinMeta use_equip_reward_special_goods_char_chess` — they now draw different operators too (信标 tops up from the tier below, 寻呼模块 shows fewer cards when the pool has no other one), and `pushRewardOffer` drops a repeated id from any list it is given (`test/content/items.test.js`).
|
||||||
- Tests: `test/match/playtest6-matchflow.test.js` (the draft, two battles for a multi-round card end to end, 联防 counts, rewards), `test/match/followups.test.js`, `test/ui/playtest6-unite.test.js`, `test/ui/playtest6-bounty.test.js`, `test/content/items.test.js` (教鞭).
|
- Tests: `test/match/playtest6-matchflow.test.js` (the draft, two battles for a multi-round card end to end, 联防 counts, rewards), `test/match/followups.test.js`, `test/ui/playtest6-unite.test.js`, `test/ui/playtest6-bounty.test.js`, `test/content/items.test.js` (教鞭).
|
||||||
|
|
||||||
|
Superseded in 0.2.2: 教鞭 no longer takes one of its three 战术特训 cards at random — its owner chooses (docs/META.md §2.5).
|
||||||
|
|
||||||
### 20.7 机变 card text; the shop's confirming tap on phones (#6, #10) — `ui/choiceOverlay.js`, `css/screens/game-panels.css`, `ui/shopBar.js`, `css/screens/game-shop.css`, `css/devices.css`
|
### 20.7 机变 card text; the shop's confirming tap on phones (#6, #10) — `ui/choiceOverlay.js`, `css/screens/game-panels.css`, `ui/shopBar.js`, `css/screens/game-shop.css`, `css/devices.css`
|
||||||
|
|
||||||
- **#6 seen** (screenshot 2, Android 756×366 CSS px, 终极 co-op): the 道具补给 cards showed only icon, name and tier. **Cause**: the card was a centred column sized for 16:9; with the root font clamped at 40 px a 3×2 card was 105 px tall and showed 0.2 lines of text (217 of 223 texts cut), in all four families; at 1920×1080 nine long texts were cut at 4 lines. **Official** (research 06 §11.5; screenshots Mb2mtd9 / Wrkhtxv / SEbySVl): a framed icon at the top-left with the bold name beside it, the description below, numbers in blue, the tier chip on the icon's corner, the taker's round avatar alone at the top-right. **Now**: that layout; the text from `cardText` (the card's own `descRaw`, else the data `descRaw` while its plain form equals the server's `desc`, else the server text), clamped to 7 lines (8 at line height 1.3 on phones under 700 px wide: with Noto Sans SC — the page's web font, metrics of Android's CJK font — 海沟实验体 wraps to 8 lines at 640×360, residual QA); short screens get a tighter overlay header (no CONTINGENCY micro line) and .19rem text; the taker badge is the avatar alone (name in its tooltip and the card's aria-label) and the taken card's head reserves its width. All 223 texts (51 items, 129 bounty, 43 tactic) fit in full, taken or not, from 640×360 to 2560×1080, measured with Noto Sans SC.
|
- **#6 seen** (screenshot 2, Android 756×366 CSS px, 终极 co-op): the 道具补给 cards showed only icon, name and tier. **Cause**: the card was a centred column sized for 16:9; with the root font clamped at 40 px a 3×2 card was 105 px tall and showed 0.2 lines of text (217 of 223 texts cut), in all four families; at 1920×1080 nine long texts were cut at 4 lines. **Official** (research 06 §11.5; screenshots Mb2mtd9 / Wrkhtxv / SEbySVl): a framed icon at the top-left with the bold name beside it, the description below, numbers in blue, the tier chip on the icon's corner, the taker's round avatar alone at the top-right. **Now**: that layout; the text from `cardText` (the card's own `descRaw`, else the data `descRaw` while its plain form equals the server's `desc`, else the server text), clamped to 7 lines (8 at line height 1.3 on phones under 700 px wide: with Noto Sans SC — the page's web font, metrics of Android's CJK font — 海沟实验体 wraps to 8 lines at 640×360, residual QA); short screens get a tighter overlay header (no CONTINGENCY micro line) and .19rem text; the taker badge is the avatar alone (name in its tooltip and the card's aria-label) and the taken card's head reserves its width. All 223 texts (51 items, 129 bounty, 43 tactic) fit in full, taken or not, from 640×360 to 2560×1080, measured with Noto Sans SC.
|
||||||
|
|||||||
+22
-3
@@ -104,7 +104,7 @@ function refreshPrivate() {
|
|||||||
const p = S.priv;
|
const p = S.priv;
|
||||||
p.deployCount = p.board.filter((x) => x.kind === 'chess').length;
|
p.deployCount = p.board.filter((x) => x.kind === 'chess').length;
|
||||||
p.bonds = computeBonds(p);
|
p.bonds = computeBonds(p);
|
||||||
p.canReady = !p.temp.some(Boolean);
|
p.canReady = !p.personalChoice && !p.temp.some(Boolean);
|
||||||
const upg = [5, 8, 11, 12, 13];
|
const upg = [5, 8, 11, 12, 13];
|
||||||
p.shop.maxLevel = 6;
|
p.shop.maxLevel = 6;
|
||||||
if (p.shop.upgradePrice == null) p.shop.upgradePrice = upg[p.shop.level - 1] ?? 0;
|
if (p.shop.upgradePrice == null) p.shop.upgradePrice = upg[p.shop.level - 1] ?? 0;
|
||||||
@@ -181,7 +181,7 @@ function buildState() {
|
|||||||
].sort((a, b) => a.t - b.t);
|
].sort((a, b) => a.t - b.t);
|
||||||
|
|
||||||
const priv = {
|
const priv = {
|
||||||
playerId: ME, seat: 0, alive: !VARIANTS.has('dead'), lp: 24, funds: 13, bandId: 'band_bldsk', ready: false, canReady: true,
|
playerId: ME, seat: 0, alive: !VARIANTS.has('dead'), lp: 24, funds: 13, bandId: 'band_bldsk', ready: false, canReady: true, personalChoice: null,
|
||||||
shop: { level, maxLevel: 6, upgradePrice: 9, refreshPrice: 1, freeRefreshes: 0, frozen: VARIANTS.has('frozen'), slots, rewardOffer: null },
|
shop: { level, maxLevel: 6, upgradePrice: 9, refreshPrice: 1, freeRefreshes: 0, frozen: VARIANTS.has('frozen'), slots, rewardOffer: null },
|
||||||
hand, temp, board, deployCap: 8, deployCount: 0, bonds: [],
|
hand, temp, board, deployCap: 8, deployCount: 0, bonds: [],
|
||||||
effects: [
|
effects: [
|
||||||
@@ -680,7 +680,16 @@ async function mockRequest(t, f = {}) {
|
|||||||
}
|
}
|
||||||
pushPublic(); return {};
|
pushPublic(); return {};
|
||||||
}
|
}
|
||||||
case 'g.art': { prepOnly(); const it = findPiece(f.itemUid); if (!it) fail('BAD_TARGET'); removeAt(it); toast('奇术已生效', 'success'); refreshPrivate(); return {}; }
|
case 'g.art': {
|
||||||
|
prepOnly(); const it = findPiece(f.itemUid); if (!it) fail('BAD_TARGET');
|
||||||
|
if (it.piece.id === 'chess_item_6_03_m') {
|
||||||
|
if (p.personalChoice) fail('BAD_TARGET');
|
||||||
|
const cards = data.get('choices').cards.bounty.filter((c) => c.payout === 'perfect' && c.rounds < 90).slice(0, 3)
|
||||||
|
.map((c) => ({ ...c, kind: 'bounty', id: c.effectId, descRaw: data.lookup('effects', c.effectId)?.descRaw }));
|
||||||
|
p.personalChoice = { id: `mock.choice.${nextUid()}`, round: pub.round, sourceItemId: it.piece.id, cards };
|
||||||
|
}
|
||||||
|
removeAt(it); toast('奇术已生效', 'success'); refreshPrivate(); return {};
|
||||||
|
}
|
||||||
case 'g.reward': {
|
case 'g.reward': {
|
||||||
if (!p.shop.rewardOffer) fail('WRONG_PHASE');
|
if (!p.shop.rewardOffer) fail('WRONG_PHASE');
|
||||||
const s = p.shop.rewardOffer.slots[f.idx]; if (!s) fail('BAD_TARGET');
|
const s = p.shop.rewardOffer.slots[f.idx]; if (!s) fail('BAD_TARGET');
|
||||||
@@ -688,6 +697,15 @@ async function mockRequest(t, f = {}) {
|
|||||||
p.hand[idx] = chessPiece(data.lookup('chess', s.id)); p.shop.rewardOffer = null; refreshPrivate(); return {};
|
p.hand[idx] = chessPiece(data.lookup('chess', s.id)); p.shop.rewardOffer = null; refreshPrivate(); return {};
|
||||||
}
|
}
|
||||||
case 'g.choice': {
|
case 'g.choice': {
|
||||||
|
if (f.choiceId !== undefined) {
|
||||||
|
prepOnly();
|
||||||
|
const choice = p.personalChoice;
|
||||||
|
if (!choice || choice.id !== f.choiceId || choice.round !== pub.round || !choice.cards[f.idx]) fail('BAD_TARGET');
|
||||||
|
const card = choice.cards[f.idx];
|
||||||
|
p.effects.push({ id: choice.id, name: card.name, desc: card.descRaw || card.desc, iconKind: 'choice', iconId: card.id });
|
||||||
|
p.personalChoice = null;
|
||||||
|
refreshPrivate(); return {};
|
||||||
|
}
|
||||||
if (!pub.sp) fail('WRONG_PHASE');
|
if (!pub.sp) fail('WRONG_PHASE');
|
||||||
const taken = Object.values(pub.sp.picks).includes(f.idx);
|
const taken = Object.values(pub.sp.picks).includes(f.idx);
|
||||||
if (taken) fail('BAD_TARGET');
|
if (taken) fail('BAD_TARGET');
|
||||||
@@ -695,6 +713,7 @@ async function mockRequest(t, f = {}) {
|
|||||||
}
|
}
|
||||||
case 'g.ready': {
|
case 'g.ready': {
|
||||||
if (pub.phase !== PHASE.PREP) fail('WRONG_PHASE');
|
if (pub.phase !== PHASE.PREP) fail('WRONG_PHASE');
|
||||||
|
if (f.ready && p.personalChoice) fail('BAD_TARGET');
|
||||||
if (f.ready && !p.canReady) fail('TEMP_NOT_EMPTY');
|
if (f.ready && !p.canReady) fail('TEMP_NOT_EMPTY');
|
||||||
p.ready = !!f.ready; refreshPrivate(); return {};
|
p.ready = !!f.ready; refreshPrivate(); return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,6 +386,12 @@
|
|||||||
"CONTINGENCY // 机变阶段": "CONTINGENCY // Improv Phase",
|
"CONTINGENCY // 机变阶段": "CONTINGENCY // Improv Phase",
|
||||||
"选择一项": "Choose one",
|
"选择一项": "Choose one",
|
||||||
"倒计时结束后仍未选定将自动分配": "If no choice is made before the countdown ends, one will be assigned automatically",
|
"倒计时结束后仍未选定将自动分配": "If no choice is made before the countdown ends, one will be assigned automatically",
|
||||||
|
"教鞭选择": "Pointing Stick Choice",
|
||||||
|
"教鞭 · 战术特训": "Pointing Stick · Tactical Training",
|
||||||
|
"请选择一项战术特训": "Choose a Tactical Training bounty",
|
||||||
|
"请先完成教鞭选择": "Complete the Pointing Stick choice first",
|
||||||
|
"休整期结束时未选择将自动选定": "An unselected bounty will be picked automatically when the Rest Phase ends",
|
||||||
|
"从候选中选择一项战术特训": "Choose one Tactical Training bounty from the offered cards",
|
||||||
"选择一项(无时间限制)": "Choose one (no time limit)",
|
"选择一项(无时间限制)": "Choose one (no time limit)",
|
||||||
" · 点击卡牌选中,再次点击确认": " · Tap a card to select, tap again to confirm",
|
" · 点击卡牌选中,再次点击确认": " · Tap a card to select, tap again to confirm",
|
||||||
"已完成选择": "Choice made",
|
"已完成选择": "Choice made",
|
||||||
|
|||||||
@@ -389,6 +389,12 @@
|
|||||||
"CONTINGENCY // 机变阶段": "CONTINGENCY // 調整段階",
|
"CONTINGENCY // 机变阶段": "CONTINGENCY // 調整段階",
|
||||||
"选择一项": "1つ選択",
|
"选择一项": "1つ選択",
|
||||||
"倒计时结束后仍未选定将自动分配": "カウントダウン終了までに選ばない場合、自動で割り当てられます",
|
"倒计时结束后仍未选定将自动分配": "カウントダウン終了までに選ばない場合、自動で割り当てられます",
|
||||||
|
"教鞭选择": "教鞭の選択",
|
||||||
|
"教鞭 · 战术特训": "教鞭 · 戦術訓練",
|
||||||
|
"请选择一项战术特训": "戦術訓練を1つ選択してください",
|
||||||
|
"请先完成教鞭选择": "先に教鞭の選択を完了してください",
|
||||||
|
"休整期结束时未选择将自动选定": "未選択の場合、整備フェーズ終了時に自動で選択されます",
|
||||||
|
"从候选中选择一项战术特训": "候補から戦術訓練を1つ選択",
|
||||||
"选择一项(无时间限制)": "1つ選択(時間制限なし)",
|
"选择一项(无时间限制)": "1つ選択(時間制限なし)",
|
||||||
" · 点击卡牌选中,再次点击确认": " · カードをタップで選択、もう一度タップで確定",
|
" · 点击卡牌选中,再次点击确认": " · カードをタップで選択、もう一度タップで確定",
|
||||||
"已完成选择": "選択完了",
|
"已完成选择": "選択完了",
|
||||||
|
|||||||
@@ -385,6 +385,12 @@
|
|||||||
"CONTINGENCY // 机变阶段": "CONTINGENCY // 기회 단계",
|
"CONTINGENCY // 机变阶段": "CONTINGENCY // 기회 단계",
|
||||||
"选择一项": "하나를 선택하세요",
|
"选择一项": "하나를 선택하세요",
|
||||||
"倒计时结束后仍未选定将自动分配": "카운트다운이 끝날 때까지 선택하지 않으면 자동으로 지정됩니다",
|
"倒计时结束后仍未选定将自动分配": "카운트다운이 끝날 때까지 선택하지 않으면 자동으로 지정됩니다",
|
||||||
|
"教鞭选择": "교편 선택",
|
||||||
|
"教鞭 · 战术特训": "교편 · 전술 특훈",
|
||||||
|
"请选择一项战术特训": "전술 특훈 1개를 선택해 주세요",
|
||||||
|
"请先完成教鞭选择": "먼저 교편 선택을 완료해 주세요",
|
||||||
|
"休整期结束时未选择将自动选定": "선택하지 않으면 휴식 기간 종료 시 자동으로 선택됩니다",
|
||||||
|
"从候选中选择一项战术特训": "후보 중 전술 특훈 1개 선택",
|
||||||
"选择一项(无时间限制)": "하나를 선택하세요 (시간 제한 없음)",
|
"选择一项(无时间限制)": "하나를 선택하세요 (시간 제한 없음)",
|
||||||
" · 点击卡牌选中,再次点击确认": " · 카드를 탭해 선택하고 한 번 더 탭해 확인",
|
" · 点击卡牌选中,再次点击确认": " · 카드를 탭해 선택하고 한 번 더 탭해 확인",
|
||||||
"已完成选择": "선택 완료",
|
"已完成选择": "선택 완료",
|
||||||
|
|||||||
@@ -387,6 +387,12 @@
|
|||||||
"CONTINGENCY // 机变阶段": "CONTINGENCY // 機變階段",
|
"CONTINGENCY // 机变阶段": "CONTINGENCY // 機變階段",
|
||||||
"选择一项": "選擇一項",
|
"选择一项": "選擇一項",
|
||||||
"倒计时结束后仍未选定将自动分配": "倒計時結束後仍未選定將自動分配",
|
"倒计时结束后仍未选定将自动分配": "倒計時結束後仍未選定將自動分配",
|
||||||
|
"教鞭选择": "教鞭選擇",
|
||||||
|
"教鞭 · 战术特训": "教鞭 · 戰術特訓",
|
||||||
|
"请选择一项战术特训": "請選擇一項戰術特訓",
|
||||||
|
"请先完成教鞭选择": "請先完成教鞭選擇",
|
||||||
|
"休整期结束时未选择将自动选定": "休整期結束時未選擇將自動選定",
|
||||||
|
"从候选中选择一项战术特训": "從候選中選擇一項戰術特訓",
|
||||||
"选择一项(无时间限制)": "選擇一項(無時間限制)",
|
"选择一项(无时间限制)": "選擇一項(無時間限制)",
|
||||||
" · 点击卡牌选中,再次点击确认": " · 點擊卡牌選中,再次點擊確認",
|
" · 点击卡牌选中,再次点击确认": " · 點擊卡牌選中,再次點擊確認",
|
||||||
"已完成选择": "已完成選擇",
|
"已完成选择": "已完成選擇",
|
||||||
|
|||||||
@@ -448,6 +448,9 @@ export function createBattleRunner(deps) {
|
|||||||
if (bossLike(e)) {
|
if (bossLike(e)) {
|
||||||
const pool = e.battle.sharedBoss;
|
const pool = e.battle.sharedBoss;
|
||||||
msg.leaks = Math.min(1e6, e.meter.lp);
|
msg.leaks = Math.min(1e6, e.meter.lp);
|
||||||
|
// the split of that LP between the players' own leaks (the rest is the leader's effects): the server holds the result's
|
||||||
|
// leaked lists to it before a perfect-payout bounty pays (Match._bossLeaksAgree)
|
||||||
|
if (p.leaksBy) msg.leaksBy = p.leaksBy;
|
||||||
msg.bossDmg = pool && Number.isFinite(pool.cum) ? pool.cum : 0;
|
msg.bossDmg = pool && Number.isFinite(pool.cum) ? pool.cum : 0;
|
||||||
if (pool && pool.byPlayer) {
|
if (pool && pool.byPlayer) {
|
||||||
const by = {};
|
const by = {};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ import { pieceTile } from '../render/drag.js';
|
|||||||
import {
|
import {
|
||||||
phaseMode, phaseBanner, isCombatPhase, showDeadPill, isBossPhase, placementContext, canPlace, boardTargets, dropIntent,
|
phaseMode, phaseBanner, isCombatPhase, showDeadPill, isBossPhase, placementContext, canPlace, boardTargets, dropIntent,
|
||||||
battleOverSfx, uniteResultBox, battleResultBox,
|
battleOverSfx, uniteResultBox, battleResultBox,
|
||||||
snapHud, activeBubbles, shortcutFor, shortcutBlocked, closesOnFieldPress, phaseTotalSeconds, homeFieldId, ownFieldId, normalizeSp, sortedPlayers,
|
snapHud, activeBubbles, shortcutFor, shortcutBlocked, closesOnFieldPress, phaseTotalSeconds, homeFieldId, ownFieldId, normalizeSp, normalizePersonalChoice, sortedPlayers,
|
||||||
terrainInfo, deviceInfo, deviceTipAt, noteDeviceUnits,
|
terrainInfo, deviceInfo, deviceTipAt, noteDeviceUnits,
|
||||||
countdownState, shopBlockReason, stageOverrides, effectiveStage, watchTarget, dropFailureReason,
|
countdownState, shopBlockReason, stageOverrides, effectiveStage, watchTarget, dropFailureReason,
|
||||||
previewEnemyKey, prepCamera, prepCameraFor, foldCamera, deployFieldOf, fieldTile, panelSide, panelSlots, bondPopupPlace, unitLoadout, deployedRecord,
|
previewEnemyKey, prepCamera, prepCameraFor, foldCamera, deployFieldOf, fieldTile, panelSide, panelSlots, bondPopupPlace, unitLoadout, deployedRecord,
|
||||||
@@ -197,6 +197,7 @@ function MatchScreen() {
|
|||||||
const [resultBox, setResultBox] = useState(null); // the round's result box (ResultDialog), shown at SETTLE
|
const [resultBox, setResultBox] = useState(null); // the round's result box (ResultDialog), shown at SETTLE
|
||||||
const [readyBusy, setReadyBusy] = useState(false);
|
const [readyBusy, setReadyBusy] = useState(false);
|
||||||
const [spBusy, setSpBusy] = useState(null);
|
const [spBusy, setSpBusy] = useState(null);
|
||||||
|
const [personalBusy, setPersonalBusy] = useState(null); // { choiceId, idx }: an older request cannot clear a newer pick
|
||||||
const [layer, setLayer] = useState('ALL'); // 联防 / 最终攻势 camera: 'L' | 'ALL' | 'R'
|
const [layer, setLayer] = useState('ALL'); // 联防 / 最终攻势 camera: 'L' | 'ALL' | 'R'
|
||||||
const [pen, setPen] = useState(false); // the camera shows the enemy preview pen (research 09 §2)
|
const [pen, setPen] = useState(false); // the camera shows the enemy preview pen (research 09 §2)
|
||||||
const [camKind, setCamKind] = useState('prep'); // kind of the last camera request (data-camera)
|
const [camKind, setCamKind] = useState('prep'); // kind of the last camera request (data-camera)
|
||||||
@@ -220,7 +221,9 @@ function MatchScreen() {
|
|||||||
const scoutPid = watchingOther && field?.prep && typeof field.fieldId === 'string' && field.fieldId.startsWith('n:') ? field.fieldId.slice(2) : null;
|
const scoutPid = watchingOther && field?.prep && typeof field.fieldId === 'string' && field.fieldId.startsWith('n:') ? field.fieldId.slice(2) : null;
|
||||||
const scoutBandId = scoutPid ? ownerBandId(pub, scoutPid) : null;
|
const scoutBandId = scoutPid ? ownerBandId(pub, scoutPid) : null;
|
||||||
const scoutBandOwner = scoutPid ? (players.find((p) => p.playerId === scoutPid)?.name || null) : null;
|
const scoutBandOwner = scoutPid ? (players.find((p) => p.playerId === scoutPid)?.name || null) : null;
|
||||||
const editable = phase === PHASE.PREP && !!priv && alive && !priv.ready && !watchingOther;
|
const personalChoice = !spectator && alive ? normalizePersonalChoice(pub, priv, myId) : null;
|
||||||
|
const hasPersonalChoice = !!personalChoice;
|
||||||
|
const editable = phase === PHASE.PREP && !!priv && alive && !priv.ready && !watchingOther && !hasPersonalChoice;
|
||||||
const showShop = !!priv && alive && (phase === PHASE.PREP || phase === PHASE.SP_DRAFT || phase === PHASE.ROUND_START) && !watchingOther;
|
const showShop = !!priv && alive && (phase === PHASE.PREP || phase === PHASE.SP_DRAFT || phase === PHASE.ROUND_START) && !watchingOther;
|
||||||
const layersDisabled = phase === PHASE.UNITE || isBossPhase(phase);
|
const layersDisabled = phase === PHASE.UNITE || isBossPhase(phase);
|
||||||
const sp = phase === PHASE.SP_DRAFT ? normalizeSp(pub?.sp, players) : null;
|
const sp = phase === PHASE.SP_DRAFT ? normalizeSp(pub?.sp, players) : null;
|
||||||
@@ -284,7 +287,7 @@ function MatchScreen() {
|
|||||||
getChess: (id) => { const c = gd.chess(id); return ownDiyRecord(c, priv, { chess: data.get('chess'), backups: data.get('backups') }) || c; },
|
getChess: (id) => { const c = gd.chess(id); return ownDiyRecord(c, priv, { chess: data.get('chess'), backups: data.get('backups') }) || c; },
|
||||||
getToken: gd.token, getItem: gd.item, getEffect: gd.effect, backups: gd.backups,
|
getToken: gd.token, getItem: gd.item, getEffect: gd.effect, backups: gd.backups,
|
||||||
}), [priv, pub?.stageId, editable, gd.ready, deployField]);
|
}), [priv, pub?.stageId, editable, gd.ready, deployField]);
|
||||||
live.current = { pub, priv, field, editable, placeCtx, watching, watchWho, home, myId, alive, spectator, detail, drawer, bondOpen, emoteOpen, settingsOpen, exitOpen, drag, facing, sel, selBusy, pen, collapsedNow: collapsed, localDone: false, canPause: false, paused };
|
live.current = { pub, priv, field, editable, hasPersonalChoice, placeCtx, watching, watchWho, home, myId, alive, spectator, detail, drawer, bondOpen, emoteOpen, settingsOpen, exitOpen, drag, facing, sel, selBusy, pen, collapsedNow: collapsed, localDone: false, canPause: false, paused };
|
||||||
|
|
||||||
// ---- camera: every request goes through setCam, which remembers it for the pen's way back -----------------------
|
// ---- camera: every request goes through setCam, which remembers it for the pen's way back -----------------------
|
||||||
// the own prep board: the normal board, or — in the prep of a boss round — the player's half of the boss field
|
// the own prep board: the normal board, or — in the prep of a boss round — the player's half of the boss field
|
||||||
@@ -673,6 +676,17 @@ function MatchScreen() {
|
|||||||
setSpBusy(null);
|
setSpBusy(null);
|
||||||
}, [phaseKey]);
|
}, [phaseKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPersonalBusy((b) => b?.choiceId === personalChoice?.id ? b : null);
|
||||||
|
if (!personalChoice) return;
|
||||||
|
setSel(null);
|
||||||
|
setDetail(null);
|
||||||
|
setArmedCard(null);
|
||||||
|
setDrag(null);
|
||||||
|
view?.highlightTiles(null, null);
|
||||||
|
cancelFacingRef.current();
|
||||||
|
}, [personalChoice?.id, view]);
|
||||||
|
|
||||||
// a reload / reconnect while watching a teammate's battle after the own one (client-side combat): the server resends
|
// a reload / reconnect while watching a teammate's battle after the own one (client-side combat): the server resends
|
||||||
// the watched field, the fresh screen adopts it as watched once per battle — the observing pill, 返回战场 and the own
|
// the watched field, the fresh screen adopts it as watched once per battle — the observing pill, 返回战场 and the own
|
||||||
// row work again, and the bond strip's "👁 name" matches the HUD (battle/observe.js resumedWatch)
|
// row work again, and the bond strip's "👁 name" matches the HUD (battle/observe.js resumedWatch)
|
||||||
@@ -1199,6 +1213,14 @@ function MatchScreen() {
|
|||||||
const L = live.current;
|
const L = live.current;
|
||||||
// dialogs / the guide own the keyboard; behind the 本局信息 / 敌方情报 drawer only Esc (closing it) acts
|
// dialogs / the guide own the keyboard; behind the 本局信息 / 敌方情报 drawer only Esc (closing it) acts
|
||||||
if (shortcutBlocked(act, { modal: !!document.querySelector('.modal, .guide'), drawer: !!L.drawer })) return;
|
if (shortcutBlocked(act, { modal: !!document.querySelector('.modal, .guide'), drawer: !!L.drawer })) return;
|
||||||
|
if (L.hasPersonalChoice) {
|
||||||
|
if (act !== 'escape') e.preventDefault();
|
||||||
|
if (act === 'ready') {
|
||||||
|
audio.sfx('error', { volume: 0.5 });
|
||||||
|
toast(shopBlockReason('ready', { priv: L.priv, editable: true }), 'warn');
|
||||||
|
}
|
||||||
|
return; // ChoiceOverlay handles Escape without cancelling the server's pending choice.
|
||||||
|
}
|
||||||
if (act === 'escape') {
|
if (act === 'escape') {
|
||||||
if (L.emoteOpen) setEmoteOpen(false);
|
if (L.emoteOpen) setEmoteOpen(false);
|
||||||
else if (L.pen && !L.detail) togglePenRef.current(false);
|
else if (L.pen && !L.detail) togglePenRef.current(false);
|
||||||
@@ -1301,7 +1323,7 @@ function MatchScreen() {
|
|||||||
const temp = tempInfo(priv);
|
const temp = tempInfo(priv);
|
||||||
const tempNotice = temp.count > 0 && !!view && viewKind !== 'loading' && showPrep && alive && !pen && !sp;
|
const tempNotice = temp.count > 0 && !!view && viewKind !== 'loading' && showPrep && alive && !pen && !sp;
|
||||||
// the ready button shows why it is refused under it (ui/hud.js ReadyToggle): the effects column moves down a line
|
// the ready button shows why it is refused under it (ui/hud.js ReadyToggle): the effects column moves down a line
|
||||||
const readyWhy = phase === PHASE.PREP && alive && !priv?.ready && temp.count > 0;
|
const readyWhy = phase === PHASE.PREP && alive && !priv?.ready && (hasPersonalChoice || temp.count > 0);
|
||||||
// the frame the panels are laid out in: the HUD layer (client px; inside the safe-area insets of a notched phone),
|
// the frame the panels are laid out in: the HUD layer (client px; inside the safe-area insets of a notched phone),
|
||||||
// and the root font size (1rem)
|
// and the root font size (1rem)
|
||||||
const panelFrame = () => {
|
const panelFrame = () => {
|
||||||
@@ -1369,7 +1391,7 @@ function MatchScreen() {
|
|||||||
// bonds this mode never activates (标准: 10 of 23, 奥术 among them) — shown 本局禁用 on cards, chips and the popup
|
// bonds this mode never activates (标准: 10 of 23, 奥术 among them) — shown 本局禁用 on cards, chips and the popup
|
||||||
const offBonds = modeOffBonds(getMode(pub?.modeId));
|
const offBonds = modeOffBonds(getMode(pub?.modeId));
|
||||||
|
|
||||||
return html`<div class=${cx('screen', 'gm', `gm--${mode}`, drag && 'is-dragging', collapsed && 'is-collapsed', sp && 'has-sp', pen && 'is-pen', readyWhy && 'has-readywhy')}
|
return html`<div class=${cx('screen', 'gm', `gm--${mode}`, drag && 'is-dragging', collapsed && 'is-collapsed', (sp || hasPersonalChoice) && 'has-sp', pen && 'is-pen', readyWhy && 'has-readywhy')}
|
||||||
data-camera=${pen ? 'pen' : camKind}>
|
data-camera=${pen ? 'pen' : camKind}>
|
||||||
<div class="gm__field" ref=${hostRef} onContextMenu=${(e) => e.preventDefault()}></div>
|
<div class="gm__field" ref=${hostRef} onContextMenu=${(e) => e.preventDefault()}></div>
|
||||||
${viewKind === 'loading' ? html`<div class="gm__loading"><${Spinner} label="LOADING FIELD" /></div>` : null}
|
${viewKind === 'loading' ? html`<div class="gm__loading"><${Spinner} label="LOADING FIELD" /></div>` : null}
|
||||||
@@ -1465,6 +1487,14 @@ function MatchScreen() {
|
|||||||
|
|
||||||
${sp ? html`<${ChoiceOverlay} pub=${pub} sp=${sp} myId=${myId} solo=${solo} busyIdx=${spBusy} total=${total}
|
${sp ? html`<${ChoiceOverlay} pub=${pub} sp=${sp} myId=${myId} solo=${solo} busyIdx=${spBusy} total=${total}
|
||||||
onPick=${async (i) => { setSpBusy(i); await actions.choice(i); setSpBusy(null); }} />` : null}
|
onPick=${async (i) => { setSpBusy(i); await actions.choice(i); setSpBusy(null); }} />` : null}
|
||||||
|
${personalChoice ? html`<${ChoiceOverlay} key=${personalChoice.id} pub=${pub} sp=${personalChoice} myId=${myId} solo=${solo} personal=${true}
|
||||||
|
busyIdx=${personalBusy?.choiceId === personalChoice.id ? personalBusy.idx : null} total=${total}
|
||||||
|
onPick=${async (i) => {
|
||||||
|
const choiceId = personalChoice.id;
|
||||||
|
setPersonalBusy({ choiceId, idx: i });
|
||||||
|
await actions.choice(i, choiceId);
|
||||||
|
setPersonalBusy((b) => b?.choiceId === choiceId ? null : b);
|
||||||
|
}} />` : null}
|
||||||
|
|
||||||
${banner ? html`<${PhaseBanner} key=${banner.key} mode="overlay" title=${banner.title} sub=${banner.sub} micro=${banner.micro}
|
${banner ? html`<${PhaseBanner} key=${banner.key} mode="overlay" title=${banner.title} sub=${banner.sub} micro=${banner.micro}
|
||||||
tone=${banner.tone} duration=${banner.duration || 1500} onDone=${() => setBanner(null)} />` : null}
|
tone=${banner.tone} duration=${banner.duration || 1500} onDone=${() => setBanner(null)} />` : null}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function armedCard(armed, sp, o) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The overlay: keeps the two-tap selection and renders ChoiceView. `onPick(idx)` sends the confirmed card (g.choice).
|
* The overlay: keeps the two-tap selection and renders ChoiceView. `onPick(idx)` sends the confirmed card (g.choice).
|
||||||
* @param {{ pub:any, sp:any, myId:string, solo:boolean, onPick:(idx:number)=>void, busyIdx?:number|null, total?:number|null }} props
|
* @param {{ pub:any, sp:any, myId:string, solo:boolean, personal?:boolean, onPick:(idx:number)=>void, busyIdx?:number|null, total?:number|null }} props
|
||||||
*/
|
*/
|
||||||
export function ChoiceOverlay(props) {
|
export function ChoiceOverlay(props) {
|
||||||
const { sp, myId, solo, busyIdx = null, onPick } = props;
|
const { sp, myId, solo, busyIdx = null, onPick } = props;
|
||||||
@@ -154,10 +154,10 @@ export function ChoiceOverlay(props) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The overlay's view (pure: no hooks — test/ui renders it as a function).
|
* The overlay's view (pure: no hooks — test/ui renders it as a function).
|
||||||
* @param {{ pub:any, sp:any, myId:string, solo:boolean, busyIdx?:number|null, total?:number|null, armed?:number|null,
|
* @param {{ pub:any, sp:any, myId:string, solo:boolean, personal?:boolean, busyIdx?:number|null, total?:number|null, armed?:number|null,
|
||||||
* onTap?:(idx:number)=>void, onConfirm?:()=>void, onDisarm?:()=>void }} props
|
* onTap?:(idx:number)=>void, onConfirm?:()=>void, onDisarm?:()=>void }} props
|
||||||
*/
|
*/
|
||||||
export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null, armed = null, onTap = () => {}, onConfirm = () => {}, onDisarm = () => {} }) {
|
export function ChoiceView({ pub, sp, myId, solo, personal = false, busyIdx = null, total = null, armed = null, onTap = () => {}, onConfirm = () => {}, onDisarm = () => {} }) {
|
||||||
if (!sp) return null;
|
if (!sp) return null;
|
||||||
const fam = data.get('choices')?.families?.[sp.family] || null;
|
const fam = data.get('choices')?.families?.[sp.family] || null;
|
||||||
const rawFam = data.getRaw('choices')?.families?.[sp.family] || null;
|
const rawFam = data.getRaw('choices')?.families?.[sp.family] || null;
|
||||||
@@ -166,8 +166,8 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
|||||||
const mine = sp.pickOf.get(myId);
|
const mine = sp.pickOf.get(myId);
|
||||||
const turnName = players.get(sp.turnPid)?.name || t('队友');
|
const turnName = players.get(sp.turnPid)?.name || t('队友');
|
||||||
const special = /_s$/.test(String(sp.family || ''));
|
const special = /_s$/.test(String(sp.family || ''));
|
||||||
const order = solo ? [] : sp.order;
|
const order = personal || solo ? [] : sp.order;
|
||||||
const timed = !solo && !sp.untimed;
|
const timed = personal ? pub?.deadline > 0 : !solo && !sp.untimed;
|
||||||
const armedCardRec = armed != null ? sp.cards.find((c) => c && c.idx === armed) : null;
|
const armedCardRec = armed != null ? sp.cards.find((c) => c && c.idx === armed) : null;
|
||||||
const armedName = armedCardRec ? resolveSpCard(armedCardRec, sp.family).name : null;
|
const armedName = armedCardRec ? resolveSpCard(armedCardRec, sp.family).name : null;
|
||||||
// a press anywhere but a card or the confirm button drops the selection
|
// a press anywhere but a card or the confirm button drops the selection
|
||||||
@@ -177,17 +177,18 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
|||||||
if (t && typeof t.closest === 'function' && t.closest('.spcard, .spov__confirm')) return;
|
if (t && typeof t.closest === 'function' && t.closest('.spcard, .spov__confirm')) return;
|
||||||
onDisarm();
|
onDisarm();
|
||||||
};
|
};
|
||||||
return html`<div class=${cx('spov', armed != null && 'has-armed')} role="dialog" aria-label=${t('机变阶段')} onPointerDown=${onDown}>
|
return html`<div class=${cx('spov', armed != null && 'has-armed')} role="dialog" aria-label=${personal ? t('教鞭选择') : t('机变阶段')} onPointerDown=${onDown}>
|
||||||
<div class="spov__veil" aria-hidden="true"></div>
|
<div class="spov__veil" aria-hidden="true"></div>
|
||||||
<div class="spov__inner">
|
<div class="spov__inner">
|
||||||
<header class="spov__head">
|
<header class="spov__head">
|
||||||
<div class="spov__titles">
|
<div class="spov__titles">
|
||||||
<${MicroLabel} tone="mint">${t('CONTINGENCY // 机变阶段')}</${MicroLabel}>
|
<${MicroLabel} tone="mint">${personal ? t('教鞭选择') : t('CONTINGENCY // 机变阶段')}</${MicroLabel}>
|
||||||
<h2 class=${cx('spov__title', special && 'is-special')}>${sentText(sp.name, rawFam?.name, fam?.name) || fam?.name || t('机变')}<span class="spov__bar">|</span><span class="spov__desc"><${RichText} text=${sentText(sp.desc, rawFam?.desc, fam?.desc) || fam?.desc || t('选择一项')} /></span></h2>
|
<h2 class=${cx('spov__title', special && 'is-special')}>${sentText(sp.name, rawFam?.name, fam?.name) || fam?.name || t('机变')}<span class="spov__bar">|</span><span class="spov__desc"><${RichText} text=${sentText(sp.desc, rawFam?.desc, fam?.desc) || fam?.desc || t('选择一项')} /></span></h2>
|
||||||
<p class="spov__sub">${timed ? t('倒计时结束后仍未选定将自动分配') : t('选择一项(无时间限制)')}${mine == null && myTurn ? t(' · 点击卡牌选中,再次点击确认') : ''}</p>
|
<p class="spov__sub">${timed ? (personal ? t('休整期结束时未选择将自动选定') : t('倒计时结束后仍未选定将自动分配')) : t('选择一项(无时间限制)')}${mine == null && myTurn ? t(' · 点击卡牌选中,再次点击确认') : ''}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="spov__turn">
|
<div class="spov__turn">
|
||||||
${mine != null ? html`<span class="spov__turntxt is-done"><${Icon} name="check" />${t('已完成选择')}</span>`
|
${personal ? html`<span class="spov__turntxt is-mine">${t('从候选中选择一项战术特训')}</span>`
|
||||||
|
: mine != null ? html`<span class="spov__turntxt is-done"><${Icon} name="check" />${t('已完成选择')}</span>`
|
||||||
: myTurn ? html`<span class="spov__turntxt is-mine">${t('当前轮到你决策')}</span>`
|
: myTurn ? html`<span class="spov__turntxt is-mine">${t('当前轮到你决策')}</span>`
|
||||||
: html`<span class="spov__turntxt">${t('{turnName} 正在决策…', { turnName })}<${Icon} name="hourglass" /></span>`}
|
: html`<span class="spov__turntxt">${t('{turnName} 正在决策…', { turnName })}<${Icon} name="hourglass" /></span>`}
|
||||||
${armed != null ? html`<${Button} variant="primary" size="lg" icon="check" class="spov__confirm" data-testid="sp-confirm"
|
${armed != null ? html`<${Button} variant="primary" size="lg" icon="check" class="spov__confirm" data-testid="sp-confirm"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const actions = {
|
|||||||
art: (itemUid, row, col, dir) => act('g.art', dir ? { itemUid, row, col, dir } : { itemUid, row, col }),
|
art: (itemUid, row, col, dir) => act('g.art', dir ? { itemUid, row, col, dir } : { itemUid, row, col }),
|
||||||
destroy: (uid) => act('g.destroy', { uid }),
|
destroy: (uid) => act('g.destroy', { uid }),
|
||||||
reward: (idx) => act('g.reward', { idx }),
|
reward: (idx) => act('g.reward', { idx }),
|
||||||
choice: (idx) => act('g.choice', { idx }),
|
choice: (idx, choiceId) => act('g.choice', choiceId === undefined ? { idx } : { idx, choiceId }),
|
||||||
ready: (ready) => act('g.ready', { ready }, { sfx: ready ? 'ready' : 'back' }),
|
ready: (ready) => act('g.ready', { ready }, { sfx: ready ? 'ready' : 'back' }),
|
||||||
emote: (id) => act('g.emote', { id }, { quiet: true }),
|
emote: (id) => act('g.emote', { id }, { quiet: true }),
|
||||||
// `playerId`: the player tapped in the team panel (a shared field shows two) — what an eliminated viewer follows
|
// `playerId`: the player tapped in the team panel (a shared field shows two) — what an eliminated viewer follows
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export { sortBonds, bondTier, nextThreshold, grantedBonds, pieceBondIds, morphPa
|
|||||||
export { priceTone, mergeProgress, mergeTarget, handFull, completesMerge, offerHeader, shopBlockReason, readyFundsPrompt, readyShopFold } from './gameLogic/shop.js';
|
export { priceTone, mergeProgress, mergeTarget, handFull, completesMerge, offerHeader, shopBlockReason, readyFundsPrompt, readyShopFold } from './gameLogic/shop.js';
|
||||||
export { deploySets, stageOverrides, deployMap, effectiveStage, indexPieces, placementContext, piecePosition, tileAllows, summonRange, summonExcluded, canPlace, equipReplaces, equipMerges, boardTargets, dropIntent, dropFailureReason } from './gameLogic/placement.js';
|
export { deploySets, stageOverrides, deployMap, effectiveStage, indexPieces, placementContext, piecePosition, tileAllows, summonRange, summonExcluded, canPlace, equipReplaces, equipMerges, boardTargets, dropIntent, dropFailureReason } from './gameLogic/placement.js';
|
||||||
export { terrainInfo, deviceInfo, deviceTipAt, noteDeviceUnits } from './gameLogic/terrain.js';
|
export { terrainInfo, deviceInfo, deviceTipAt, noteDeviceUnits } from './gameLogic/terrain.js';
|
||||||
export { normalizeDraft, normalizeSp } from './gameLogic/draft.js';
|
export { normalizeDraft, normalizeSp, normalizePersonalChoice } from './gameLogic/draft.js';
|
||||||
export { groupEnemies, PEN, penZoneTiles, penPlacement, previewEnemyKey, factionTypes } from './gameLogic/enemies.js';
|
export { groupEnemies, PEN, penZoneTiles, penPlacement, previewEnemyKey, factionTypes } from './gameLogic/enemies.js';
|
||||||
export { snapHud, bossFrac, bossPctText, hasFlag, UF, attackInterval, fmtNum, rangeGridBox } from './gameLogic/format.js';
|
export { snapHud, bossFrac, bossPctText, hasFlag, UF, attackInterval, fmtNum, rangeGridBox } from './gameLogic/format.js';
|
||||||
export { shortcutFor, closesOnFieldPress, shortcutBlocked, HOTKEY_ACTIONS, DEFAULT_HOTKEYS, isBindableCode, hotkeyLabel, sanitizeHotkeys, rebindHotkey, isDefaultHotkeys, hotkeyOf, actionForKey, captureHotkey, facingSwallows, facingEnter } from './gameLogic/shortcuts.js';
|
export { shortcutFor, closesOnFieldPress, shortcutBlocked, HOTKEY_ACTIONS, DEFAULT_HOTKEYS, isBindableCode, hotkeyLabel, sanitizeHotkeys, rebindHotkey, isDefaultHotkeys, hotkeyOf, actionForKey, captureHotkey, facingSwallows, facingEnter } from './gameLogic/shortcuts.js';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// ui/gameLogic/draft.js — 机变 and band-draft normalisation. Re-exported from ../gameLogic.js.
|
// ui/gameLogic/draft.js — 机变 and band-draft normalisation. Re-exported from ../gameLogic.js.
|
||||||
|
|
||||||
import { isObj } from './shared.js';
|
import { isObj } from './shared.js';
|
||||||
|
import { PHASE } from '../../../../shared/constants.js';
|
||||||
|
import { t } from '../../../../shared/i18n.js';
|
||||||
|
|
||||||
|
|
||||||
// ---- 机变 / band draft normalisation ----------------------------------------------------------------------
|
// ---- 机变 / band draft normalisation ----------------------------------------------------------------------
|
||||||
@@ -73,3 +75,15 @@ export function normalizeSp(sp, players = []) {
|
|||||||
cards, order, turnPid, pickOf, takenBy, pickedCount: pickOf.size,
|
cards, order, turnPid, pickOf, takenBy, pickedCount: pickOf.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adapt only the recipient's current PREP choice; never write it into the public draft. */
|
||||||
|
export function normalizePersonalChoice(pub, priv, myId) {
|
||||||
|
const choice = priv?.personalChoice;
|
||||||
|
if (pub?.phase !== PHASE.PREP || !priv || priv.playerId !== myId || priv.alive === false
|
||||||
|
|| !choice || choice.round !== pub.round) return null;
|
||||||
|
return {
|
||||||
|
...normalizeSp({ family: 'bounty', name: t('教鞭 · 战术特训'), desc: t('请选择一项战术特训'),
|
||||||
|
cards: choice.cards, turn: myId, order: [myId], picks: {} }),
|
||||||
|
id: choice.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -140,7 +140,10 @@ export function offerHeader(offer) {
|
|||||||
export function shopBlockReason(kind, { priv, editable, slot } = {}) {
|
export function shopBlockReason(kind, { priv, editable, slot } = {}) {
|
||||||
if (!priv) return t('尚未就绪');
|
if (!priv) return t('尚未就绪');
|
||||||
if (priv.alive === false) return t('你已被淘汰');
|
if (priv.alive === false) return t('你已被淘汰');
|
||||||
if (kind === 'ready') return priv.canReady === false ? t('临时整备区不为空,请先处理溢出的资源') : null;
|
if (kind === 'ready') {
|
||||||
|
if (priv.personalChoice) return t('请先完成教鞭选择');
|
||||||
|
return priv.canReady === false ? t('临时整备区不为空,请先处理溢出的资源') : null;
|
||||||
|
}
|
||||||
if (!editable) {
|
if (!editable) {
|
||||||
if (kind === 'reward') return t('当前无法选择');
|
if (kind === 'reward') return t('当前无法选择');
|
||||||
return priv.ready ? t('已准备就绪,取消准备后才能操作') : t('当前阶段无法进行该操作');
|
return priv.ready ? t('已准备就绪,取消准备后才能操作') : t('当前阶段无法进行该操作');
|
||||||
|
|||||||
+9
-6
@@ -246,24 +246,27 @@ export function tempReadyReason(priv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ready toggle (PREP only). Disabled while the temp row holds pieces — the reason shows under it (not only on hover):
|
* Ready toggle (PREP only). Disabled while the temp row holds pieces or a personal choice (教鞭) is open — the reason shows
|
||||||
* "临时整备区 N 个单位待处理" (user playtest #3 item 3).
|
* under it (not only on hover): "临时整备区 N 个单位待处理" (user playtest #3 item 3), "请先完成教鞭选择".
|
||||||
* @param {{ priv:any, onToggle:(ready:boolean)=>void, busy?:boolean, readyCount?:number, total?:number }} props
|
* @param {{ priv:any, onToggle:(ready:boolean)=>void, busy?:boolean, readyCount?:number, total?:number }} props
|
||||||
*/
|
*/
|
||||||
export function ReadyToggle({ priv, onToggle, busy, readyCount, total }) {
|
export function ReadyToggle({ priv, onToggle, busy, readyCount, total }) {
|
||||||
const ready = !!priv?.ready;
|
const ready = !!priv?.ready;
|
||||||
const temp = tempInfo(priv);
|
const temp = tempInfo(priv);
|
||||||
const reason = !ready ? tempReadyReason(priv) || shopBlockReason('ready', { priv, editable: true }) : null;
|
const choice = !!priv?.personalChoice;
|
||||||
|
const reason = !ready ? (choice ? shopBlockReason('ready', { priv, editable: true }) : tempReadyReason(priv) || shopBlockReason('ready', { priv, editable: true })) : null;
|
||||||
|
const why = !ready && (choice || temp.count > 0);
|
||||||
const btn = html`<button type="button" class=${cx('readybtn', 'tapx', ready && 'is-on', busy && 'is-busy')} disabled=${!!reason || busy}
|
const btn = html`<button type="button" class=${cx('readybtn', 'tapx', ready && 'is-on', busy && 'is-busy')} disabled=${!!reason || busy}
|
||||||
aria-pressed=${ready ? 'true' : 'false'} aria-describedby=${!ready && temp.count ? 'readywrap-why' : undefined} onClick=${() => onToggle(!ready)}>
|
aria-pressed=${ready ? 'true' : 'false'} aria-describedby=${why ? 'readywrap-why' : undefined} onClick=${() => onToggle(!ready)}>
|
||||||
<span class="readybtn__box">${ready ? html`<${Icon} name="check" />` : null}</span>
|
<span class="readybtn__box">${ready ? html`<${Icon} name="check" />` : null}</span>
|
||||||
<span class="readybtn__label">${ready ? t('取消准备') : t('准备就绪')}</span>
|
<span class="readybtn__label">${ready ? t('取消准备') : t('准备就绪')}</span>
|
||||||
<kbd class="readybtn__key">${hotkeyLabelOf('ready')}</kbd>
|
<kbd class="readybtn__key">${hotkeyLabelOf('ready')}</kbd>
|
||||||
</button>`;
|
</button>`;
|
||||||
return html`<div class="readywrap">
|
return html`<div class="readywrap">
|
||||||
${reason ? html`<${Tooltip} text=${reason} placement="bottom">${btn}<//>` : btn}
|
${reason ? html`<${Tooltip} text=${reason} placement="bottom">${btn}<//>` : btn}
|
||||||
${!ready && temp.count ? html`<span class="readywrap__why" id="readywrap-why" role="status" data-testid="ready-why">
|
${why ? html`<span class="readywrap__why" id="readywrap-why" role="status" data-testid="ready-why">
|
||||||
<${Icon} name="warn" /><span>${tParts('临时整备区 {n} 个单位待处理', { n: html`<b class="num">${temp.count}</b>`, count: temp.count })}</span></span>` : null}
|
<${Icon} name="warn" />${choice ? html`<span>${reason}</span>`
|
||||||
|
: html`<span>${tParts('临时整备区 {n} 个单位待处理', { n: html`<b class="num">${temp.count}</b>`, count: temp.count })}</span>`}</span>` : null}
|
||||||
${Number.isFinite(total) && total > 1 ? html`<span class="readywrap__count">${t('已就绪')} <b class="num">${readyCount}</b>/<span class="num">${total}</span></span>` : null}
|
${Number.isFinite(total) && total > 1 ? html`<span class="readywrap__count">${t('已就绪')} <b class="num">${readyCount}</b>/<span class="num">${total}</span></span>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,13 @@ export class PlayerState {
|
|||||||
this.effects = [];
|
this.effects = [];
|
||||||
/** active bounties: { id, card, roundsLeft, chooser } */
|
/** active bounties: { id, card, roundsLeft, chooser } */
|
||||||
this.bounties = [];
|
this.bounties = [];
|
||||||
|
/**
|
||||||
|
* The open personal choice of the Art 教鞭 (Match.offerBountyChoice): up to three cards.bounty entries the owner picks one
|
||||||
|
* of with `g.choice { idx, choiceId }` — m.private only; Ready is refused while it is open, the prep's deadline (or the
|
||||||
|
* bot) resolves it
|
||||||
|
* @type {null | { id: string, round: number, sourceItemId: string, cards: any[] }}
|
||||||
|
*/
|
||||||
|
this.personalChoice = null;
|
||||||
/** free-form counters for content (ctx.counter / setCounter) */
|
/** free-form counters for content (ctx.counter / setCounter) */
|
||||||
this.counters = {};
|
this.counters = {};
|
||||||
/** per-round counters (reset at round start) */
|
/** per-round counters (reset at round start) */
|
||||||
|
|||||||
+19
-5
@@ -37,7 +37,8 @@
|
|||||||
// tiles, where it blocks — except one whose trait reads 「可以放置于远程位」 (歌蕾蒂娅, 崖心, 见行者, any module:
|
// tiles, where it blocks — except one whose trait reads 「可以放置于远程位」 (歌蕾蒂娅, 崖心, 见行者, any module:
|
||||||
// placeClass 'all', shared/highGround.js), which takes a 高台 whose range covers the enemy road, else the ground
|
// placeClass 'all', shared/highGround.js), which takes a 高台 whose range covers the enemy road, else the ground
|
||||||
// (owner 2026-10-04 for the 高台 preference, 2026-10-05 for who may use one). It stays a blocker in the lineup
|
// (owner 2026-10-04 for the 高台 preference, 2026-10-05 for who may use one). It stays a blocker in the lineup
|
||||||
// (basePositionClass). Each unit's range grid — the one it is
|
// (basePositionClass). A blocker whose range grid is its own tile only (range 0-1: 角峰, 古米, 泡泡, 折桠, 菲莱, 蛇屠箱,
|
||||||
|
// 塞雷娅, 余) is planned on a free tile of the enemy road first: off it nothing is blocked or hit. Each unit's range grid — the one it is
|
||||||
// deployed with, rangeRec (loadoutRecord attackRangeGrid) — is rotated per direction (DESIGN §3; RIGHT is tried first
|
// deployed with, rangeRec (loadoutRecord attackRangeGrid) — is rotated per direction (DESIGN §3; RIGHT is tried first
|
||||||
// and kept on ties, so symmetric ranges and melee units whose front adds nothing stay facing the gates), so
|
// and kept on ties, so symmetric ranges and melee units whose front adds nothing stay facing the gates), so
|
||||||
// ranged units turn toward the enemy path tiles they cover best and blockers toward the road; on 气流 tiles
|
// ranged units turn toward the enemy path tiles they cover best and blockers toward the road; on 气流 tiles
|
||||||
@@ -978,6 +979,12 @@ export function* planLayoutSteps(m, ps, pieces, params = LAYOUT_PARAMS, { occupi
|
|||||||
const preferHigh = cls === 'all' && basePositionClass(r0) === 'melee';
|
const preferHigh = cls === 'all' && basePositionClass(r0) === 'melee';
|
||||||
let bestHigh = null;
|
let bestHigh = null;
|
||||||
let bestHighV = -Infinity;
|
let bestHighV = -Infinity;
|
||||||
|
// a blocker whose range is its own tile only (range 0-1: 角峰, 古米, 泡泡, 折桠, 菲莱, 蛇屠箱, 塞雷娅, 余) neither blocks nor
|
||||||
|
// hits anything off the enemy road — the official range "0-1" (excel range_table) is the one grid (0,0) — so a free road tile
|
||||||
|
// comes first (the kill estimate saturates in a full layout and then cannot tell the tiles apart)
|
||||||
|
const preferRoad = isBlocker(r0) && r0.rangeGrid?.length === 1 && r0.rangeGrid[0].every((v) => v === 0);
|
||||||
|
let bestRoad = null;
|
||||||
|
let bestRoadV = -Infinity;
|
||||||
for (const [r, c] of legalTiles(map, cls)) {
|
for (const [r, c] of legalTiles(map, cls)) {
|
||||||
const k = tileKey(r, c);
|
const k = tileKey(r, c);
|
||||||
if (taken.has(k) || (within && !within.has(k)) || (without && without.has(k))) continue;
|
if (taken.has(k) || (within && !within.has(k)) || (without && without.has(k))) continue;
|
||||||
@@ -993,13 +1000,15 @@ export function* planLayoutSteps(m, ps, pieces, params = LAYOUT_PARAMS, { occupi
|
|||||||
const v = layout.value() + noise;
|
const v = layout.value() + noise;
|
||||||
layout.units.pop();
|
layout.units.pop();
|
||||||
if (v > bestV) { bestV = v; best = [k, r, c, dir]; }
|
if (v > bestV) { bestV = v; best = [k, r, c, dir]; }
|
||||||
|
// (the best road tile by the same value)
|
||||||
|
if (preferRoad && model.ground.has(k) && v > bestRoadV) { bestRoadV = v; bestRoad = [k, r, c, dir]; }
|
||||||
if (preferHigh && map.get(k) === 'ranged' && [...u.cover].some((ck) => model.ground.has(ck)) && v > bestHighV) {
|
if (preferHigh && map.get(k) === 'ranged' && [...u.cover].some((ck) => model.ground.has(ck)) && v > bestHighV) {
|
||||||
bestHighV = v;
|
bestHighV = v;
|
||||||
bestHigh = [k, r, c, dir];
|
bestHigh = [k, r, c, dir];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const pick = bestHigh || best;
|
const pick = bestHigh || bestRoad || best;
|
||||||
if (!pick) continue;
|
if (!pick) continue;
|
||||||
taken.add(pick[0]);
|
taken.add(pick[0]);
|
||||||
layout.units.push(unitOf(r0, pick[0], pick[1], pick[2], pick[3], model));
|
layout.units.push(unitOf(r0, pick[0], pick[1], pick[2], pick[3], model));
|
||||||
@@ -1329,6 +1338,7 @@ export function botPrepBegin(m, ps) {
|
|||||||
*/
|
*/
|
||||||
export function* botPrepBeginSteps(m, ps) {
|
export function* botPrepBeginSteps(m, ps) {
|
||||||
if (!ps.alive || ps.ready) return null;
|
if (!ps.alive || ps.ready) return null;
|
||||||
|
m.autoPickPersonalChoice(ps, 'bot');
|
||||||
// 1. reward offers (free)
|
// 1. reward offers (free)
|
||||||
takeOffers(m, ps);
|
takeOffers(m, ps);
|
||||||
yield;
|
yield;
|
||||||
@@ -1366,6 +1376,7 @@ export function* botPrepEndSteps(m, ps, job = null) {
|
|||||||
if (freeSlot(ps.hand) < 0) freeHandSlot(m, ps);
|
if (freeSlot(ps.hand) < 0) freeHandSlot(m, ps);
|
||||||
// what the next prep's bench shed may judge by value: what is owned now (later gains wait for a placement step)
|
// what the next prep's bench shed may judge by value: what is owned now (later gains wait for a placement step)
|
||||||
rememberOwned(ps);
|
rememberOwned(ps);
|
||||||
|
m.autoPickPersonalChoice(ps, 'bot');
|
||||||
tryDo(() => ps.setReady(true));
|
tryDo(() => ps.setReady(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1807,8 +1818,8 @@ const isBountyArt = (gd, itemId) => { const rec = gd.item(itemId); return !!rec
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Use an Art: 画卷 copies the operator on its tile (its range is the tile + the one in front) — the most valuable
|
* Use an Art: 画卷 copies the operator on its tile (its range is the tile + the one in front) — the most valuable
|
||||||
* deployed operator, with a free hand slot for the copy; 教鞭 / “神秘顾客” add a bounty to the next battle (one the engine
|
* deployed operator, with a free hand slot for the copy; 教鞭 offers a scored personal bounty choice, “神秘顾客” adds a
|
||||||
* draws, not chosen) — only after a perfect battle with LP to spare, else the Art stays in the hand for a later round
|
* random bounty — only after a perfect battle with LP to spare, else the Art stays in the hand for a later round
|
||||||
* (destroying 教鞭 gives nothing; a full hand at the prep end: freeHandSlot).
|
* (destroying 教鞭 gives nothing; a full hand at the prep end: freeHandSlot).
|
||||||
*/
|
*/
|
||||||
function useArt(m, ps, item, ctx) {
|
function useArt(m, ps, item, ctx) {
|
||||||
@@ -1825,7 +1836,10 @@ function useArt(m, ps, item, ctx) {
|
|||||||
if (keys.includes('trap_create_self_choice')) {
|
if (keys.includes('trap_create_self_choice')) {
|
||||||
if (!lastPerfect(m, ps) || ps.lp < 10) return; // kept
|
if (!lastPerfect(m, ps) || ps.lp < 10) return; // kept
|
||||||
const [key] = [...ps.board.keys()];
|
const [key] = [...ps.board.keys()];
|
||||||
if (key) { const [r, c] = parseKey(key); tryDo(() => ps.useArt(item.uid, r, c)); }
|
if (key) {
|
||||||
|
const [r, c] = parseKey(key);
|
||||||
|
if (tryDo(() => ps.useArt(item.uid, r, c))) m.autoPickPersonalChoice(ps, 'bot');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [key] = [...ps.board.keys()];
|
const [key] = [...ps.board.keys()];
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
// operator gained into the hand
|
// operator gained into the hand
|
||||||
// trap_copy_front_char 画卷 (Art) copy the chess on the tile / in front
|
// trap_copy_front_char 画卷 (Art) copy the chess on the tile / in front
|
||||||
// trap_create_self_choice {choice_event} 教鞭 / 神秘顾客 (Art) add a random bounty to your next battle
|
// trap_create_self_choice {choice_event} 教鞭 / 神秘顾客 (Art) add a random bounty to your next battle
|
||||||
// [ASSUMED simplification, documented in docs/META.md: 教鞭/神秘顾客 pick the bounty for the player instead of opening
|
// This builtin picks a random bounty by default; content/items/meta.js overrides Pointing Stick with a private choice.
|
||||||
// a personal choice overlay.]
|
|
||||||
|
|
||||||
import { getData } from '../data.js';
|
import { getData } from '../data.js';
|
||||||
import { itemKey } from './gamedata.js';
|
import { itemKey } from './gamedata.js';
|
||||||
|
|||||||
@@ -674,6 +674,7 @@ export function makeCtx(m, ps, source, hook, ev = null) {
|
|||||||
|
|
||||||
// ---- bounties / choices
|
// ---- bounties / choices
|
||||||
addBounty: (card) => m.addBounty(ps, card),
|
addBounty: (card) => m.addBounty(ps, card),
|
||||||
|
offerBountyChoice: (cards, sourceItemId) => m.offerBountyChoice(ps, cards, sourceItemId),
|
||||||
|
|
||||||
// ---- messaging
|
// ---- messaging
|
||||||
// a string or a shared/i18n.js msg(msgid, params)
|
// a string or a shared/i18n.js msg(msgid, params)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
// bonds ps.bonds equals a fresh computeBonds() (every mutation recomputed them); every bond's layers 0 … BOND_LAYER_CAP
|
// bonds ps.bonds equals a fresh computeBonds() (every mutation recomputed them); every bond's layers 0 … BOND_LAYER_CAP
|
||||||
// shop slot count follows the rolled layout; ids known; banned chess never offered by the shop / rewards
|
// shop slot count follows the rolled layout; ids known; banned chess never offered by the shop / rewards
|
||||||
// elim. an eliminated player owns nothing (board, hand, temp, shop, offers, bounties, funds)
|
// elim. an eliminated player owns nothing (board, hand, temp, shop, offers, bounties, funds)
|
||||||
|
// choice a player's open 教鞭 choice (`personalChoice`) is one of an alive, not-Ready player in the PREP of its own round,
|
||||||
|
// with one to three different cards
|
||||||
// match phase known; teamLp / boss pool within range; combat fields match the alive players
|
// match phase known; teamLp / boss pool within range; combat fields match the alive players
|
||||||
|
|
||||||
import { PHASE, BOND_LAYER_CAP } from '../../shared/constants.js';
|
import { PHASE, BOND_LAYER_CAP } from '../../shared/constants.js';
|
||||||
@@ -75,6 +77,12 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
|||||||
if (ps.shop.slots.length || ps.offers.length || ps.bounties.length) fail(`${id}: eliminated but keeps shop/offers/bounties`);
|
if (ps.shop.slots.length || ps.offers.length || ps.bounties.length) fail(`${id}: eliminated but keeps shop/offers/bounties`);
|
||||||
if (ps.funds || ps.pendingFunds) fail(`${id}: eliminated with funds ${ps.funds}+${ps.pendingFunds}`);
|
if (ps.funds || ps.pendingFunds) fail(`${id}: eliminated with funds ${ps.funds}+${ps.pendingFunds}`);
|
||||||
}
|
}
|
||||||
|
// 教鞭's personal choice (Match.offerBountyChoice): resolved before the prep ends — by its owner, the deadline or the bot
|
||||||
|
const pc = ps.personalChoice;
|
||||||
|
if (pc) {
|
||||||
|
if (!ps.alive || ps.ready || m.phase !== PHASE.PREP || pc.round !== m.round) fail(`${id}: a personal choice outside its own prep (${m.phase} R${m.round}, offered in R${pc.round})`);
|
||||||
|
if (!Array.isArray(pc.cards) || !pc.cards.length || pc.cards.length > 3 || new Set(pc.cards.map((c) => c && c.effectId)).size !== pc.cards.length) fail(`${id}: personal choice of ${pc.cards && pc.cards.length} cards`);
|
||||||
|
}
|
||||||
|
|
||||||
// pieces
|
// pieces
|
||||||
const all = [...ps.board.values(), ...ps.hand.filter(Boolean), ...ps.temp.filter(Boolean)];
|
const all = [...ps.board.values(), ...ps.hand.filter(Boolean), ...ps.temp.filter(Boolean)];
|
||||||
|
|||||||
@@ -231,6 +231,35 @@ export class MatchBoss {
|
|||||||
if (credit > 0) this._teamLpLoss(credit);
|
if (credit > 0) this._teamLpLoss(credit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authority's per-player split of the LP its field's enemy leaks cost (b.progress `leaksBy`: cumulative, per player,
|
||||||
|
* the leader's own LP effects not in it) — the highest value seen per player. The field's perfect-payout bounties are
|
||||||
|
* paid only where the final result agrees with it (`_bossLeaksAgree`).
|
||||||
|
*/
|
||||||
|
_noteLeaksBy(f, by) {
|
||||||
|
if (!by || typeof by !== 'object') return;
|
||||||
|
if (!f.leaksBy) f.leaksBy = {};
|
||||||
|
for (const pid of f.players) {
|
||||||
|
const v = Number(by[pid]);
|
||||||
|
if (Number.isFinite(v) && v > (f.leaksBy[pid] || 0)) f.leaksBy[pid] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a boss field's b.result may be believed about one player's leaks before a perfect-payout bounty pays: the
|
||||||
|
* result comes from a client (a server run is the truth itself) and does not contradict what that client reported while
|
||||||
|
* it fought — LP it asked the team to pay must have been split into the players' own leaks and the leader's effects
|
||||||
|
* (b.progress `leaksBy`; reports without the split explain nothing), and the player's leaked list must show at least the
|
||||||
|
* leak LP reported for it (a leak is never moved to the other seat or dropped). A modified authority can still lie in
|
||||||
|
* both; what the server cannot see it cannot check (the boss path never re-simulates).
|
||||||
|
*/
|
||||||
|
_bossLeaksAgree(f, res, pid) {
|
||||||
|
if (!f.cc || f.resultSource !== 'client') return true;
|
||||||
|
if (f.lpReported > 1e-9 && !f.leaksBy) return false;
|
||||||
|
const shown = ((res.perPlayer[pid] && res.perPlayer[pid].leaked) || []).reduce((n, l) => n + (Number.isFinite(l.lpr) && l.lpr >= 0 ? l.lpr : 1), 0);
|
||||||
|
return shown + 1e-6 >= ((f.leaksBy && f.leaksBy[pid]) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
/** The server runs a boss field in real time (no client left): credits only what exceeds the client's reports. */
|
/** The server runs a boss field in real time (no client left): credits only what exceeds the client's reports. */
|
||||||
_bossServerRun(f) {
|
_bossServerRun(f) {
|
||||||
const credit = new CreditPool(this.bossPool, { acked: f.bossAcked, ackedBy: f.bossBy });
|
const credit = new CreditPool(this.bossPool, { acked: f.bossAcked, ackedBy: f.bossBy });
|
||||||
@@ -372,11 +401,15 @@ export class MatchBoss {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bounties after a boss field: kill-bounty coins go to pending funds (spent in the Hidden Core's prep) and every
|
* Bounties after a boss field: kill-bounty coins and — when `perfect` — the coins of every perfect-payout bounty (a
|
||||||
* bounty used one of its battles, exactly like SETTLE does for normal rounds.
|
* 战术特训 card: PRTS "若各自行动阶段就达成完美作战,获得N资金", the same sum SETTLE pays after a normal battle) go to
|
||||||
|
* pending funds (spent in the Hidden Core's prep), and every bounty used one of its battles, exactly like SETTLE does
|
||||||
|
* for normal rounds.
|
||||||
|
* @param {boolean} perfect the player's own field was perfect and the team won (`_finishFinal`)
|
||||||
*/
|
*/
|
||||||
_settleBossBounties(ps, pp) {
|
_settleBossBounties(ps, pp, perfect) {
|
||||||
const coins = Math.max(0, Math.trunc(Number(pp.coins) || 0));
|
let coins = Math.max(0, Math.trunc(Number(pp.coins) || 0));
|
||||||
|
if (perfect) for (const b of ps.bounties) if (b.card.payout === 'perfect') coins += b.card.coin;
|
||||||
if (coins > 0) { ps.pendingFunds += coins; ps.stats.fundsGained += coins; }
|
if (coins > 0) { ps.pendingFunds += coins; ps.stats.fundsGained += coins; }
|
||||||
if (!ps.bounties.length) return;
|
if (!ps.bounties.length) return;
|
||||||
for (const b of ps.bounties) b.roundsLeft--;
|
for (const b of ps.bounties) b.roundsLeft--;
|
||||||
@@ -471,6 +504,9 @@ export class MatchBoss {
|
|||||||
_finishFinal(hidden, resultOf) {
|
_finishFinal(hidden, resultOf) {
|
||||||
if (this.phase !== (hidden ? PHASE.HIDDEN_CORE : PHASE.FINAL_ASSAULT)) return;
|
if (this.phase !== (hidden ? PHASE.HIDDEN_CORE : PHASE.FINAL_ASSAULT)) return;
|
||||||
this._stopClientCombat();
|
this._stopClientCombat();
|
||||||
|
// the end condition the server registered first decides (client-side combat: _endFinal — pool 0 → victory, team LP 0
|
||||||
|
// → defeat); a boss field's final result may never turn a defeat into a victory (user playtest #6 item 5)
|
||||||
|
const victory = this._finalEnding ? this._finalEnding === 'cleared' : this.bossPool.hp <= 0;
|
||||||
for (const f of this.fields) {
|
for (const f of this.fields) {
|
||||||
const res = resultOf(f);
|
const res = resultOf(f);
|
||||||
this._collectSimErrors(f, res);
|
this._collectSimErrors(f, res);
|
||||||
@@ -485,13 +521,17 @@ export class MatchBoss {
|
|||||||
ps.dirty(); // m.private.stats
|
ps.dirty(); // m.private.stats
|
||||||
this._charDamageTickers(ps, pp);
|
this._charDamageTickers(ps, pp);
|
||||||
}
|
}
|
||||||
if (pp && ps) this._settleBossBounties(ps, pp);
|
if (pp && ps) {
|
||||||
|
// the own battle counts as perfect when the team won, the result is a real one (not the stand-in of a field that
|
||||||
|
// never reported) and the player's own field let no counted enemy through [ASSUMED: the boss battle is a battle
|
||||||
|
// of the player's own — "下场作战" — so a perfect-payout card pays as after a normal one]
|
||||||
|
const perfect = victory && !res.synthetic && pp.perfect === true && !(pp.leaked || []).some((l) => l && l.counted !== false)
|
||||||
|
&& this._bossLeaksAgree(f, res, pid);
|
||||||
|
this._settleBossBounties(ps, pp, perfect);
|
||||||
|
}
|
||||||
if (pp && ps) this.dispatch(ps, 'onBattleResult', { result: pp, lpLoss: 0, perfect: !!pp.perfect, boss: true });
|
if (pp && ps) this.dispatch(ps, 'onBattleResult', { result: pp, lpLoss: 0, perfect: !!pp.perfect, boss: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the end condition the server registered first decides (client-side combat: _endFinal — pool 0 → victory, team LP 0
|
|
||||||
// → defeat); a boss field's final result may never turn a defeat into a victory (user playtest #6 item 5)
|
|
||||||
const victory = this._finalEnding ? this._finalEnding === 'cleared' : this.bossPool.hp <= 0;
|
|
||||||
this._syncTeamLp();
|
this._syncTeamLp();
|
||||||
this.deadline = 0;
|
this.deadline = 0;
|
||||||
this.overtimeAt = 0;
|
this.overtimeAt = 0;
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ export class MatchClientCombat {
|
|||||||
bossAcked: 0, bossBy: {}, lpAcked: 0, lpCum: 0, deadlineTimer: null, doneTimer: null, waitTimer: null,
|
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
|
// 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'
|
// CreditPool, humans demoted for an implausible result (never the authority of this field again), a 'cleared'
|
||||||
// b.result waiting for the budget to credit the pool it emptied (`heldResult`, _onResult)
|
// b.result waiting for the budget to credit the pool it emptied (`heldResult`, _onResult); `leaksBy`: the authority's
|
||||||
bossReported: null, lpReported: 0, credit: null, demoted: new Set(), heldResult: null,
|
// per-player split of the LP its enemy leaks cost (b.progress, the highest value seen; null until one carried it),
|
||||||
|
// which a result must not contradict before a perfect-payout bounty pays (_bossLeaksAgree)
|
||||||
|
bossReported: null, lpReported: 0, credit: null, demoted: new Set(), heldResult: null, leaksBy: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ export class MatchIntents {
|
|||||||
case 'g.art': return ps.useArt(msg.itemUid, msg.row, msg.col, msg.dir);
|
case 'g.art': return ps.useArt(msg.itemUid, msg.row, msg.col, msg.dir);
|
||||||
case 'g.destroy': return ps.destroy(msg.uid);
|
case 'g.destroy': return ps.destroy(msg.uid);
|
||||||
case 'g.reward': return ps.pickReward(msg.idx);
|
case 'g.reward': return ps.pickReward(msg.idx);
|
||||||
case 'g.choice': return this.pickCard(ps, msg.idx);
|
case 'g.choice': return msg.choiceId !== undefined
|
||||||
|
? this.pickPersonalChoice(ps, msg.idx, msg.choiceId)
|
||||||
|
: this.pickCard(ps, msg.idx);
|
||||||
case 'g.ready': return ps.setReady(!!msg.ready);
|
case 'g.ready': return ps.setReady(!!msg.ready);
|
||||||
case 'g.emote': return this.emote(ps, msg.id);
|
case 'g.emote': return this.emote(ps, msg.id);
|
||||||
// playerId: the player tapped (a shared field names two) — the watch preference (item 56)
|
// playerId: the player tapped (a shared field names two) — the watch preference (item 56)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export class MatchPrep {
|
|||||||
};
|
};
|
||||||
const ready = () => {
|
const ready = () => {
|
||||||
if (!ps.ready) {
|
if (!ps.ready) {
|
||||||
|
this.autoPickPersonalChoice(ps, 'random');
|
||||||
ps.resolveTemp();
|
ps.resolveTemp();
|
||||||
ps.setReady(true);
|
ps.setReady(true);
|
||||||
}
|
}
|
||||||
@@ -113,6 +114,7 @@ export class MatchPrep {
|
|||||||
if (this.phase !== PHASE.PREP) return;
|
if (this.phase !== PHASE.PREP) return;
|
||||||
for (const ps of this.alivePlayers()) {
|
for (const ps of this.alivePlayers()) {
|
||||||
if (ps.ready) continue;
|
if (ps.ready) continue;
|
||||||
|
this.autoPickPersonalChoice(ps, 'random');
|
||||||
ps.resolveTemp();
|
ps.resolveTemp();
|
||||||
ps.ready = true;
|
ps.ready = true;
|
||||||
ps.dirty();
|
ps.dirty();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class MatchReports {
|
|||||||
if (f.kind === 'boss' || f.kind === 'hidden') {
|
if (f.kind === 'boss' || f.kind === 'hidden') {
|
||||||
this._creditBoss(f, msg.bossDmg, msg.by);
|
this._creditBoss(f, msg.bossDmg, msg.by);
|
||||||
this._creditLp(f, msg.leaks, true);
|
this._creditLp(f, msg.leaks, true);
|
||||||
|
this._noteLeaksBy(f, msg.leaksBy);
|
||||||
this._checkFinalEnd();
|
this._checkFinalEnd();
|
||||||
this._broadcastPool(false);
|
this._broadcastPool(false);
|
||||||
// 4 Hz per field: b.pool carries the exact pool / team LP; m.public (boss HP, LP, progress) follows at ~1 Hz
|
// 4 Hz per field: b.pool carries the exact pool / team LP; m.public (boss HP, LP, progress) follows at ~1 Hz
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
// server/match/match/spDraft.js — Match methods: the 机变 draft (SP_DRAFT: turn order, timers, AI picks, the card applied
|
// server/match/match/spDraft.js — Match methods: the 机变 draft (SP_DRAFT: turn order, timers, AI picks, the card applied
|
||||||
// by choices.js applyCard) and the rolls the meta effects ask for — bounties (addBounty), item ids (rollItemId) and
|
// by choices.js applyCard), the personal bounty choice of the Art 教鞭 (offerBountyChoice / pickPersonalChoice /
|
||||||
// choices.json pools (rollPool).
|
// autoPickPersonalChoice: held by the player inside PREP — no phase, no timer of its own) and the rolls the meta effects
|
||||||
|
// ask for — bounties (addBounty), item ids (rollItemId) and choices.json pools (rollPool).
|
||||||
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
|
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
|
||||||
|
|
||||||
import { PHASE, ERR } from '../../../shared/constants.js';
|
import { PHASE, ERR } from '../../../shared/constants.js';
|
||||||
import { generateDraft, applyCard, bountyBattles, isMultiRoundBounty } from '../choices.js';
|
import { generateDraft, applyCard, bountyBattles, isMultiRoundBounty, bountyCard } from '../choices.js';
|
||||||
import { weightedPick } from '../waves.js';
|
import { weightedPick } from '../waves.js';
|
||||||
import { botPickCard } from '../bot.js';
|
import { botPickCard } from '../bot.js';
|
||||||
import { OK, fail, DELAYS } from './common.js';
|
import { OK, fail, DELAYS } from './common.js';
|
||||||
|
|
||||||
|
/** Cards of the personal choice (PRTS 卫戍协议:盟约 下半 §法术 教鞭: "于3个战术特训的悬赏任务中选择一项"). */
|
||||||
|
const PERSONAL_OFFER_SIZE = 3;
|
||||||
|
|
||||||
export class MatchSpDraft {
|
export class MatchSpDraft {
|
||||||
enterSpDraft() {
|
enterSpDraft() {
|
||||||
const draft = generateDraft(this.gd, this.rngDraft, this.round, { stageId: this.stageId, bondAvailable: (bondId) => this.bondLive(bondId) });
|
const draft = generateDraft(this.gd, this.rngDraft, this.round, { stageId: this.stageId, bondAvailable: (bondId) => this.bondLive(bondId) });
|
||||||
@@ -108,6 +112,58 @@ export class MatchSpDraft {
|
|||||||
this.enterPrep();
|
this.enterPrep();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教鞭 (content/items/meta.js, ctx.offerBountyChoice): the Art opens a PERSONAL choice (act2autochess choice event
|
||||||
|
* `hunter_band_1`, choiceType PERSONAL_CHOOSE) between up to three of `candidates` — drawn once from the meta rng, kept in
|
||||||
|
* `ps.personalChoice` (m.private only) until the owner confirms one with `g.choice { idx, choiceId }`. It lives inside
|
||||||
|
* PREP: no phase of its own and no timer — the prep's deadline resolves it (prepDeadline), Ready is refused while it is
|
||||||
|
* pending (PlayerPrep.setReady). Refused — before an rng draw or an id is used, so the Art stays in the hand — when it
|
||||||
|
* is not the player's PREP, a choice is already open or no card is left to offer.
|
||||||
|
* @param {import('../PlayerState.js').PlayerState} ps @param {any[]} candidates cards.bounty entries
|
||||||
|
* @param {string} sourceItemId the Art
|
||||||
|
*/
|
||||||
|
offerBountyChoice(ps, candidates, sourceItemId) {
|
||||||
|
if (this.phase !== PHASE.PREP) return fail(ERR.WRONG_PHASE);
|
||||||
|
if (!ps.alive) return fail(ERR.ELIMINATED);
|
||||||
|
if (ps.ready) return fail(ERR.WRONG_PHASE, 'ready');
|
||||||
|
if (ps.personalChoice) return fail(ERR.BAD_TARGET, '请先完成当前教鞭选择'); // i18n-ignore: developer error detail
|
||||||
|
if (!candidates.length) return fail(ERR.BAD_TARGET, '当前没有可用的战术特训'); // i18n-ignore: developer error detail
|
||||||
|
const cards = this.rngMeta.shuffle(candidates.slice()).slice(0, PERSONAL_OFFER_SIZE);
|
||||||
|
ps.personalChoice = { id: `${this.battlePrefix}.choice.${this.nextUid()}`, round: this.round, sourceItemId, cards };
|
||||||
|
ps.dirty();
|
||||||
|
return OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** g.choice { idx, choiceId }: the owner's pick of its open personal choice — the card becomes a bounty on its next battles. */
|
||||||
|
pickPersonalChoice(ps, idx, choiceId) {
|
||||||
|
if (this.phase !== PHASE.PREP) return fail(ERR.WRONG_PHASE);
|
||||||
|
if (!ps.alive) return fail(ERR.ELIMINATED);
|
||||||
|
if (ps.ready) return fail(ERR.WRONG_PHASE, 'ready');
|
||||||
|
const pending = ps.personalChoice;
|
||||||
|
if (!pending || pending.id !== choiceId || pending.round !== this.round) return fail(ERR.BAD_TARGET);
|
||||||
|
if (!Number.isInteger(idx) || idx < 0 || idx >= pending.cards.length) return fail(ERR.BAD_TARGET);
|
||||||
|
if (!this.addBounty(ps, pending.cards[idx])) return fail(ERR.BAD_TARGET);
|
||||||
|
ps.personalChoice = null;
|
||||||
|
ps.dirty();
|
||||||
|
return OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pick made for a player who did not make it. 'bot': an AI seat or a human under AI 托管 — the bounty scorer of the
|
||||||
|
* 机变 draft (bot.js botPickCard: the expected pay minus the expected LP lost against the own board, the bots' seeded rng
|
||||||
|
* only breaks ties); 'random': the prep's deadline and a seat the engine finishes by other means — one of the cards at
|
||||||
|
* random from the meta rng, like a 机变 turn that runs out. Nothing pending: nothing happens, no rng draw.
|
||||||
|
*/
|
||||||
|
autoPickPersonalChoice(ps, mode) {
|
||||||
|
const pending = ps.personalChoice;
|
||||||
|
if (!pending) return undefined;
|
||||||
|
const indices = pending.cards.map((c, i) => i);
|
||||||
|
const idx = mode === 'bot'
|
||||||
|
? botPickCard(this, ps, pending.cards.map((c) => bountyCard(this.gd, c)), indices)
|
||||||
|
: this.rngMeta.pick(indices);
|
||||||
|
return this.pickPersonalChoice(ps, idx, pending.id);
|
||||||
|
}
|
||||||
|
|
||||||
addBounty(ps, card) {
|
addBounty(ps, card) {
|
||||||
if (!ps || !card || !this.gd.enemy(card.enemyKey)) return null;
|
if (!ps || !card || !this.gd.enemy(card.enemyKey)) return null;
|
||||||
// a multi-round card lasts MULTI_ROUND_BOUNTY_BATTLES battles (choices.js; the user's call after playtest #6)
|
// a multi-round card lasts MULTI_ROUND_BOUNTY_BATTLES battles (choices.js; the user's call after playtest #6)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export class PlayerPrep {
|
|||||||
setReady(on) {
|
setReady(on) {
|
||||||
if (!this.alive) return fail(ERR.ELIMINATED);
|
if (!this.alive) return fail(ERR.ELIMINATED);
|
||||||
if (this.m.phase !== PHASE.PREP) return fail(ERR.WRONG_PHASE);
|
if (this.m.phase !== PHASE.PREP) return fail(ERR.WRONG_PHASE);
|
||||||
|
if (on && this.personalChoice) return fail(ERR.BAD_TARGET, '请先完成教鞭选择'); // i18n-ignore: developer error detail
|
||||||
if (on && !this.tempEmpty) return fail(ERR.TEMP_NOT_EMPTY);
|
if (on && !this.tempEmpty) return fail(ERR.TEMP_NOT_EMPTY);
|
||||||
if (this.ready === !!on) return OK;
|
if (this.ready === !!on) return OK;
|
||||||
this.ready = !!on;
|
this.ready = !!on;
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export class PlayerRound {
|
|||||||
this._tempDue.clear();
|
this._tempDue.clear();
|
||||||
this.offers = [];
|
this.offers = [];
|
||||||
this.bounties = [];
|
this.bounties = [];
|
||||||
|
this.personalChoice = null;
|
||||||
this.shop.slots = [];
|
this.shop.slots = [];
|
||||||
this.funds = 0;
|
this.funds = 0;
|
||||||
this.pendingFunds = 0;
|
this.pendingFunds = 0;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { PHASE } from '../../../shared/constants.js';
|
import { PHASE } from '../../../shared/constants.js';
|
||||||
import { boardOrder, pieceDir } from '../board.js';
|
import { boardOrder, pieceDir } from '../board.js';
|
||||||
import { bondList, offBondCounts } from '../bondsMeta.js';
|
import { bondList, offBondCounts } from '../bondsMeta.js';
|
||||||
import { bountyText } from '../choices.js';
|
import { bountyText, bountyCard } from '../choices.js';
|
||||||
|
|
||||||
export class PlayerViews {
|
export class PlayerViews {
|
||||||
pieceView(p, rc = null) {
|
pieceView(p, rc = null) {
|
||||||
@@ -66,7 +66,13 @@ export class PlayerViews {
|
|||||||
funds: this.funds,
|
funds: this.funds,
|
||||||
bandId: this.bandId,
|
bandId: this.bandId,
|
||||||
ready: this.ready,
|
ready: this.ready,
|
||||||
canReady: this.alive && this.tempEmpty && this.m.phase === PHASE.PREP,
|
canReady: this.alive && this.tempEmpty && !this.personalChoice && this.m.phase === PHASE.PREP,
|
||||||
|
personalChoice: this.personalChoice ? {
|
||||||
|
id: this.personalChoice.id,
|
||||||
|
round: this.personalChoice.round,
|
||||||
|
sourceItemId: this.personalChoice.sourceItemId,
|
||||||
|
cards: this.personalChoice.cards.map((c) => bountyCard(this.gd, c)),
|
||||||
|
} : null,
|
||||||
shop: {
|
shop: {
|
||||||
level: this.shop.level,
|
level: this.shop.level,
|
||||||
maxLevel: this.gd.maxShopLevel,
|
maxLevel: this.gd.maxShopLevel,
|
||||||
|
|||||||
@@ -13,17 +13,19 @@
|
|||||||
// the target's items skipped the item after a merge)
|
// the target's items skipped the item after a merge)
|
||||||
// 紧急调度券 a shop operator leaves its slot only when it was actually granted (built-in cleared the slot first)
|
// 紧急调度券 a shop operator leaves its slot only when it was actually granted (built-in cleared the slot first)
|
||||||
// 寻呼模块 the special refresh shows `refresh_cnt` DIFFERENT operators (fewer when the pool has no more)
|
// 寻呼模块 the special refresh shows `refresh_cnt` DIFFERENT operators (fewer when the pool has no more)
|
||||||
// 教鞭 trap_create_self_choice {choice_event: hunter_band_1}: the bounty is a 战术特训 card (choices.json
|
// 教鞭 trap_create_self_choice {choice_event: hunter_band_1, choiceType PERSONAL_CHOOSE}: a personal choice of
|
||||||
// cards.bounty payout `perfect`, e.g. 战术特训·飞行I "若各自行动阶段就达成完美作战,获得1资金") — PRTS
|
// one of THREE 战术特训 cards (choices.json cards.bounty payout `perfect`, e.g. 战术特训·飞行I "若各自行动阶段就
|
||||||
// 卫戍协议:盟约 下半/PRTS盟约记录 §法术 教鞭 "于3个战术特训的悬赏任务中选择一项", §机变阶段 "※以下悬赏任务仅由
|
// 达成完美作战,获得1资金") whose enemy the mode can field — PRTS 卫戍协议:盟约 下半/PRTS盟约记录 §法术 教鞭
|
||||||
// 法术教鞭生成", and 杜宾 加练! "<教鞭>:使用后为下场战斗添加额外敌人,若自身战斗完美作战可获得资金" (user
|
// "使用后销毁,于3个战术特训的悬赏任务中选择一项", §机变阶段 "※以下悬赏任务仅由法术教鞭生成", and 杜宾 加练!
|
||||||
// playtest #6 item 4 review; the built-in drew any tier ≤ II kill bounty, and research 04 §7 [ASSUMED]
|
// "<教鞭>:使用后为下场战斗添加额外敌人,若自身战斗完美作战可获得资金". onArt → ctx.offerBountyChoice
|
||||||
// the band family `enemyeffect_b_*`).
|
// (Match.offerBountyChoice): the three cards go to the owner alone, who confirms one inside the PREP
|
||||||
// “神秘顾客” the same trap_create_self_choice ("选择一项特殊悬赏任务"; no band grants it in act2, research 04): a band
|
// (g.choice with the offer's id); the prep's deadline or an AI seat resolves it (docs/META.md §2.5). Until
|
||||||
// bounty `enemyeffect_b_*` (research 04 §7 [ASSUMED]: "adds 1 enemy to your next battle, killer gets `coin`
|
// 0.2.2 the engine took one of the three at random (user playtest #6 item 4 review; the built-in drew any
|
||||||
// funds").
|
// tier ≤ II kill bounty, and research 04 §7 [ASSUMED] the band family `enemyeffect_b_*`).
|
||||||
// Both: [ASSUMED simplification, engine: no PERSONAL_CHOOSE overlay] 3 cards whose enemy can appear in the
|
// “神秘顾客” the same trap_create_self_choice ("选择一项特殊悬赏任务"; no band grants it in act2, research 04, and PRTS
|
||||||
// mode are drawn and one of them is taken at random; the built-in runs when the family is empty.
|
// lists no pool): still [ASSUMED simplification] 3 band bounties `enemyeffect_b_*` (research 04 §7 [ASSUMED]:
|
||||||
|
// "adds 1 enemy to your next battle, killer gets `coin` funds") whose enemy can appear in the mode, one of them
|
||||||
|
// taken at random; the built-in runs when the family is empty.
|
||||||
// “神秘顾客” trap_disney_special: when actively destroyed, +count funds and the Art passes to the next alive player
|
// “神秘顾客” trap_disney_special: when actively destroyed, +count funds and the Art passes to the next alive player
|
||||||
// (seat order, cyclic)
|
// (seat order, cyclic)
|
||||||
// 天师古鼎 equip_with_another_gain_coin_when_gain_char: a 【炎】 carrier also holding 炎国短刀 (either quality)
|
// 天师古鼎 equip_with_another_gain_coin_when_gain_char: a 【炎】 carrier also holding 炎国短刀 (either quality)
|
||||||
@@ -48,7 +50,7 @@ function pieceIsMember(ctx, piece, bondId) {
|
|||||||
return bonds.includes('maniShip') && isCoreBond(bondId) && ctx.bondActive('maniShip') && ctx.bondActive(bondId);
|
return bonds.includes('maniShip') && isCoreBond(bondId) && ctx.bondActive('maniShip') && ctx.bondActive(bondId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Offer size of the personal bounty choice (PRTS 法术 教鞭 "于3个战术特训的悬赏任务中选择一项"). */
|
/** Cards drawn before “神秘顾客” takes one at random. */
|
||||||
const OFFER_SIZE = 3;
|
const OFFER_SIZE = 3;
|
||||||
/** cards.bounty entries matching `test` whose enemy can appear in this mode. */
|
/** cards.bounty entries matching `test` whose enemy can appear in this mode. */
|
||||||
function bountyCards(ctx, test) {
|
function bountyCards(ctx, test) {
|
||||||
@@ -103,18 +105,24 @@ export function registerMeta(registry) {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 教鞭 / “神秘顾客” — trap_create_self_choice {choice_event}: 教鞭 a 战术特训 card, “神秘顾客” a band bounty (enemyeffect_b_*)
|
// 教鞭 — trap_create_self_choice {choice_event}: the server keeps the offered 战术特训 cards until confirmed.
|
||||||
for (const [key, family] of [['chess_item_6_03_m', isTraining], ['chess_item_6_01_m', isBandBounty]]) {
|
wrap(registry, 'chess_item_6_03_m', () => ({
|
||||||
wrap(registry, key, (base) => ({
|
onArt(ctx, ev) {
|
||||||
onArt(ctx, ev) {
|
const result = ctx.offerBountyChoice(bountyCards(ctx, isTraining), ev.item.id);
|
||||||
const cards = bountyCards(ctx, family);
|
if (!result.ok) { ev.error = result.error; ev.detail = result.detail; }
|
||||||
if (!cards.length) { if (typeof base.onArt === 'function') base.onArt.call(base, ctx, ev); return; }
|
},
|
||||||
const offer = ctx.rng.shuffle(cards.slice()).slice(0, OFFER_SIZE);
|
}));
|
||||||
const card = ctx.rng.pick(offer);
|
|
||||||
if (!card || !ctx.addBounty(card)) { ev.error = 'BAD_TARGET'; ev.detail = 'no bounty available'; }
|
// “神秘顾客” — a random band bounty (enemyeffect_b_*).
|
||||||
},
|
wrap(registry, 'chess_item_6_01_m', (base) => ({
|
||||||
}));
|
onArt(ctx, ev) {
|
||||||
}
|
const cards = bountyCards(ctx, isBandBounty);
|
||||||
|
if (!cards.length) { if (typeof base.onArt === 'function') base.onArt.call(base, ctx, ev); return; }
|
||||||
|
const offer = ctx.rng.shuffle(cards.slice()).slice(0, OFFER_SIZE);
|
||||||
|
const card = ctx.rng.pick(offer);
|
||||||
|
if (!card || !ctx.addBounty(card)) { ev.error = 'BAD_TARGET'; ev.detail = 'no bounty available'; }
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// 画卷 — trap_copy_front_char: copy the operator in range (elite status included) and its equipment. The copied items
|
// 画卷 — trap_copy_front_char: copy the operator in range (elite status included) and its equipment. The copied items
|
||||||
// are gained unequipped, like any gained item — the hand, overflow temp, destroyed with the usual toast when both are
|
// are gained unequipped, like any gained item — the hand, overflow temp, destroyed with the usual toast when both are
|
||||||
|
|||||||
+14
-3
@@ -264,7 +264,9 @@ export function uniteLeft(battle) {
|
|||||||
/**
|
/**
|
||||||
* Progress numbers of a battle for b.progress / the teammates' waiting UI: game time, kills, total, the capsule's
|
* 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
|
* `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
|
* (normal / unite), the boss pool damage of this field, `leaksBy` of a boss / hidden field — per player, the LP the
|
||||||
|
* enemies that reached its goal cost (every leak entry the result will show, × its `lpr`; the field's LP meter minus
|
||||||
|
* their sum is the leader's own "扣除目标生命" effects) — and, unite fields, `left` (uniteLeft: each leaker's enemies
|
||||||
* still standing).
|
* still standing).
|
||||||
*/
|
*/
|
||||||
export function battleProgress(battle) {
|
export function battleProgress(battle) {
|
||||||
@@ -290,6 +292,11 @@ export function battleProgress(battle) {
|
|||||||
const left = uniteLeft(battle);
|
const left = uniteLeft(battle);
|
||||||
if (left) out.left = left;
|
if (left) out.left = left;
|
||||||
}
|
}
|
||||||
|
if (battle && (battle.kind === 'boss' || battle.kind === 'hidden')) {
|
||||||
|
const leaksBy = {};
|
||||||
|
for (const pid of Object.keys(pp).slice(0, 4)) leaksBy[pid] = Math.min(1e6, leakEntries(pp[pid]).reduce((n, l) => n + leakLpr(l), 0));
|
||||||
|
out.leaksBy = leaksBy;
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +342,10 @@ const cap = (list, n) => (Array.isArray(list) ? list.slice(0, n) : []);
|
|||||||
const isKey = (v) => typeof v === 'string' && v.length > 0 && v.length <= 64 && /^[A-Za-z0-9_\-.:]+$/.test(v);
|
const isKey = (v) => typeof v === 'string' && v.length > 0 && v.length <= 64 && /^[A-Za-z0-9_\-.:]+$/.test(v);
|
||||||
const uidOr = (v) => (Number.isInteger(v) && v >= 1 && v <= 0x80000000 ? v : null);
|
const uidOr = (v) => (Number.isInteger(v) && v >= 1 && v <= 0x80000000 ? v : null);
|
||||||
const keyOr = (v) => (isKey(v) ? v : null);
|
const keyOr = (v) => (isKey(v) ? v : null);
|
||||||
|
/** A player's leak entries as the b.result carries them (compactResult: a valid enemy key, at most 400). */
|
||||||
|
const leakEntries = (p) => cap(p && p.leaked, 400).filter((l) => l && isKey(l.enemyKey));
|
||||||
|
/** What one leak entry costs the team on the wire: its `lpr` (0 … 1000), 1 when absent. */
|
||||||
|
const leakLpr = (l) => Math.max(0, Math.min(1000, fnum(l && l.lpr, 1)));
|
||||||
|
|
||||||
function compactMods(m) {
|
function compactMods(m) {
|
||||||
if (!m || typeof m !== 'object') return null;
|
if (!m || typeof m !== 'object') return null;
|
||||||
@@ -365,8 +376,8 @@ export function compactResult(res) {
|
|||||||
// NOT clamped to `total`: the denominator counts only the round's own scheduled enemies (Battle.resolved)
|
// 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))),
|
killed: Math.max(0, Math.trunc(fnum(p.killed))),
|
||||||
total,
|
total,
|
||||||
leaked: cap(p.leaked, 400).filter((l) => l && isKey(l.enemyKey)).map((l) => {
|
leaked: leakEntries(p).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 };
|
const o = { enemyKey: l.enemyKey, mods: compactMods(l.mods), lpr: leakLpr(l), sourcePlayerId: keyOr(l.sourcePlayerId), tag: typeof l.tag === 'string' && l.tag.length <= 16 ? l.tag : null, counted: l.counted !== false };
|
||||||
if (l.boss) o.boss = true;
|
if (l.boss) o.boss = true;
|
||||||
if (l.spawned) o.spawned = true;
|
if (l.spawned) o.spawned = true;
|
||||||
return o;
|
return o;
|
||||||
|
|||||||
+6
-3
@@ -376,7 +376,7 @@ export const C2S = {
|
|||||||
'g.art': { itemUid: isUid, row: (v) => isInt(v, 0, GEO.ROWS - 1), col: (v) => isInt(v, 0, GEO.COLS - 1), dir: isDir, $optional: ['dir'] },
|
'g.art': { itemUid: isUid, row: (v) => isInt(v, 0, GEO.ROWS - 1), col: (v) => isInt(v, 0, GEO.COLS - 1), dir: isDir, $optional: ['dir'] },
|
||||||
'g.destroy': { uid: isUid },
|
'g.destroy': { uid: isUid },
|
||||||
'g.reward': { idx: (v) => isInt(v, 0, 5) },
|
'g.reward': { idx: (v) => isInt(v, 0, 5) },
|
||||||
'g.choice': { idx: (v) => isInt(v, 0, 5) },
|
'g.choice': { idx: (v) => isInt(v, 0, 5), choiceId: isId, $optional: ['choiceId'] },
|
||||||
'g.ready': { ready: isBool },
|
'g.ready': { ready: isBool },
|
||||||
'g.emote': { id: (v) => EMOTES.includes(v) },
|
'g.emote': { id: (v) => EMOTES.includes(v) },
|
||||||
// playerId: the player tapped in the team panel (a 联防 / boss pair field shows two) — what an eliminated viewer or a
|
// playerId: the player tapped in the team panel (a 联防 / boss pair field shows two) — what an eliminated viewer or a
|
||||||
@@ -395,14 +395,17 @@ export const C2S = {
|
|||||||
// `left` = { [leakerId]: its enemies still standing (unspawned, alive, or through again) } (server/sim/spec.js
|
// `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). `resolved` = the HUD capsule's numerator: the
|
// 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,
|
// 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).
|
// 打死会分裂的 3/3 — Battle.leakedInTotal; runtime splits / summons stay out of both parts of the capsule). A boss / hidden field adds
|
||||||
|
// `leaksBy` = { [playerId]: the cumulative LP the enemies that reached that player's goal cost } — `leaks` minus its
|
||||||
|
// sum is the leader's own "扣除目标生命" effects; the server holds the b.result's leaked lists to it (Match._bossLeaksAgree)
|
||||||
'b.progress': {
|
'b.progress': {
|
||||||
battleId: isId, gt: (v) => isNum(v, 0, 1e5), killed: (v) => isInt(v, 0, 1e5), total: (v) => isInt(v, 0, 1e5),
|
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),
|
resolved: (v) => isInt(v, 0, 1e5),
|
||||||
leaks: (v) => isNum(v, 0, 1e6), bossDmg: (v) => isNum(v, 0, BIG),
|
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,
|
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)),
|
left: (v) => isMap(v, RESULT_LIMITS.players, isId, (x) => isInt(x, 0, 1e5)),
|
||||||
$optional: ['resolved', 'leaks', 'bossDmg', 'by', 'done', 'left'],
|
leaksBy: (v) => isMap(v, RESULT_LIMITS.players, isId, (x) => isNum(x, 0, 1e6)),
|
||||||
|
$optional: ['resolved', 'leaks', 'bossDmg', 'by', 'done', 'left', 'leaksBy'],
|
||||||
},
|
},
|
||||||
'b.result': { battleId: isId, result: isBattleResult },
|
'b.result': { battleId: isId, result: isBattleResult },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1140,7 +1140,23 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
|||||||
const { m, ps } = setup({ seed: 60 + s });
|
const { m, ps } = setup({ seed: 60 + s });
|
||||||
for (let k = 0; k < 2; k++) {
|
for (let k = 0; k < 2; k++) {
|
||||||
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
const deadline = m.deadline;
|
||||||
assert.deepEqual(m.handle('p_0', { t: 'g.art', itemUid: art.uid, row: 10, col: 5 }), OK);
|
assert.deepEqual(m.handle('p_0', { t: 'g.art', itemUid: art.uid, row: 10, col: 5 }), OK);
|
||||||
|
const pending = ps.personalChoice;
|
||||||
|
assert.equal(ps.bounties.length, k, 'offering cards does not add a bounty');
|
||||||
|
assert.equal(pending.cards.length, 3);
|
||||||
|
assert.equal(new Set(pending.cards.map((c) => c.effectId)).size, 3);
|
||||||
|
assert.equal(pending.round, m.round);
|
||||||
|
assert.equal(pending.sourceItemId, art.id);
|
||||||
|
assert.equal(m.deadline, deadline, 'uses the existing PREP clock');
|
||||||
|
assert.equal(ps.find(art.uid), null);
|
||||||
|
assert.equal(ps.round.arts, k + 1, 'consumed when offered');
|
||||||
|
for (const c of pending.cards) assert.ok(training.has(c.effectId) && !m.gd.inactiveEnemies.has(c.enemyKey));
|
||||||
|
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: pending.id }), OK);
|
||||||
|
assert.equal(ps.personalChoice, null);
|
||||||
|
assert.equal(ps.bounties[k].card.effectId, pending.cards[1].effectId, 'the submitted card is applied');
|
||||||
|
assert.equal(ps.round.arts, k + 1, 'picking does not consume another Art');
|
||||||
|
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: pending.id }).error, 'BAD_TARGET');
|
||||||
}
|
}
|
||||||
assert.equal(ps.bounties.length, 2);
|
assert.equal(ps.bounties.length, 2);
|
||||||
for (const b of ps.bounties) {
|
for (const b of ps.bounties) {
|
||||||
@@ -1151,7 +1167,7 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
|||||||
seen.add(b.card.effectId);
|
seen.add(b.card.effectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.ok(seen.size >= 5, 'random pick');
|
assert.ok(seen.size >= 5, 'different offered cards across seeds');
|
||||||
const { h, m } = setup({ mode: 'coop', humans: 2, seed: 7 });
|
const { h, m } = setup({ mode: 'coop', humans: 2, seed: 7 });
|
||||||
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
const p0 = h.ps('p_0'), p1 = h.ps('p_1');
|
||||||
const art = giveItem(m, p0, 'chess_item_6_01_m');
|
const art = giveItem(m, p0, 'chess_item_6_01_m');
|
||||||
@@ -1162,9 +1178,60 @@ test('教鞭 (Art): a 战术特训 bounty (PRTS "于3个战术特训的悬赏任
|
|||||||
const passed = p1.hand.find((p) => p && p.id === 'chess_item_6_01_m');
|
const passed = p1.hand.find((p) => p && p.id === 'chess_item_6_01_m');
|
||||||
assert.deepEqual(m.handle('p_1', { t: 'g.art', itemUid: passed.uid, row: 10, col: 5 }), OK);
|
assert.deepEqual(m.handle('p_1', { t: 'g.art', itemUid: passed.uid, row: 10, col: 5 }), OK);
|
||||||
assert.match(p1.bounties[0].card.effectId, /^enemyeffect_b_/);
|
assert.match(p1.bounties[0].card.effectId, /^enemyeffect_b_/);
|
||||||
|
assert.equal(p1.personalChoice, null, '神秘顾客 still applies its random bounty immediately');
|
||||||
cover('chess_item_6_03_m', 'chess_item_6_01_m');
|
cover('chess_item_6_03_m', 'chess_item_6_01_m');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('教鞭: pending and empty-pool failures keep the Art, Arts count and RNG; short offers use actual cards', () => {
|
||||||
|
const { m, ps } = setup();
|
||||||
|
const first = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
const second = giveItem(m, ps, 'chess_item_6_03_m', 'temp');
|
||||||
|
assert.deepEqual(ps.useArt(first.uid, 10, 5), OK);
|
||||||
|
const pending = ps.personalChoice;
|
||||||
|
const rng = m.rngMeta.state(), uid = m.uidSeq;
|
||||||
|
assert.deepEqual(ps.useArt(second.uid, 10, 5), { error: 'BAD_TARGET', detail: '请先完成当前教鞭选择' });
|
||||||
|
assert.equal(ps.personalChoice, pending);
|
||||||
|
assert.ok(ps.find(second.uid));
|
||||||
|
assert.equal(ps.round.arts, 1);
|
||||||
|
assert.equal(m.rngMeta.state(), rng);
|
||||||
|
assert.equal(m.uidSeq, uid);
|
||||||
|
assert.equal(m.pickPersonalChoice(ps, -1, pending.id).error, 'BAD_TARGET');
|
||||||
|
assert.equal(m.pickPersonalChoice(ps, 0.5, pending.id).error, 'BAD_TARGET');
|
||||||
|
assert.equal(m.pickPersonalChoice(ps, 3, pending.id).error, 'BAD_TARGET');
|
||||||
|
assert.equal(ps.personalChoice, pending);
|
||||||
|
assert.deepEqual(m.pickPersonalChoice(ps, 0, pending.id), OK);
|
||||||
|
const training = DATA.choices.cards.bounty.filter((c) => c.payout === 'perfect');
|
||||||
|
m.gd.inactiveEnemies = new Set(training.map((c) => c.enemyKey));
|
||||||
|
assert.deepEqual(ps.useArt(second.uid, 10, 5), { error: 'BAD_TARGET', detail: '当前没有可用的战术特训' });
|
||||||
|
assert.ok(ps.find(second.uid));
|
||||||
|
assert.equal(ps.round.arts, 1);
|
||||||
|
assert.equal(ps.personalChoice, null);
|
||||||
|
assert.equal(m.rngMeta.state(), rng);
|
||||||
|
const allowed = training.find((c) => c.multiRound && m.gd.enemy(c.enemyKey));
|
||||||
|
m.gd.inactiveEnemies.delete(allowed.enemyKey);
|
||||||
|
assert.deepEqual(ps.useArt(second.uid, 10, 5), OK);
|
||||||
|
assert.ok(ps.personalChoice.cards.length > 0 && ps.personalChoice.cards.length < 3);
|
||||||
|
assert.equal(new Set(ps.personalChoice.cards.map((c) => c.effectId)).size, ps.personalChoice.cards.length);
|
||||||
|
assert.equal(ps.find(second.uid), null, 'Arts in temp are consumed too');
|
||||||
|
const view = ps.privateView().personalChoice;
|
||||||
|
assert.ok(view.cards.every((c) => c.kind === 'bounty' && c.rounds === 2));
|
||||||
|
assert.ok(view.cards.every((c) => !/每场/.test(c.descRaw || c.desc)), 'multi-round candidates use the same two-battle text as applied bounties');
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('教鞭: an open choice goes with a player who is eliminated, and nothing of it is left in its views', () => {
|
||||||
|
const { h, m } = setup({ mode: 'coop', humans: 2, seed: 8 });
|
||||||
|
const a = h.ps('p_0');
|
||||||
|
const art = giveItem(m, a, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(a.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
assert.equal(a.privateView().personalChoice.cards.length, 3);
|
||||||
|
a.eliminate(m.round);
|
||||||
|
assert.equal(a.personalChoice, null);
|
||||||
|
assert.equal(a.privateView().personalChoice, null);
|
||||||
|
assert.equal(a.privateView().canReady, false);
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('merging: two identical normal items become the golden one (upgradeNum 2); upgradeNum-100 items never merge', () => {
|
test('merging: two identical normal items become the golden one (upgradeNum 2); upgradeNum-100 items never merge', () => {
|
||||||
const never = Object.values(DATA.items).filter((r) => r.itemType === 'EQUIP' && !r.isGolden && r.upgradeNum === 100).map((r) => r.name).sort();
|
const never = Object.values(DATA.items).filter((r) => r.itemType === 'EQUIP' && !r.isGolden && r.upgradeNum === 100).map((r) => r.name).sort();
|
||||||
assert.deepEqual(never, ['人事部文档', '信标', '变形同构体', '寻呼模块', '拟态物质', '突变细胞'].sort());
|
assert.deepEqual(never, ['人事部文档', '信标', '变形同构体', '寻呼模块', '拟态物质', '突变细胞'].sort());
|
||||||
|
|||||||
@@ -490,7 +490,8 @@ test('user playtest #6 (DESIGN §20): summons, skill triggers, blocking, push fo
|
|||||||
assert.match(sec(17), /\*\*revised by §20\.6\*\*/);
|
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.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(8), /uniteLeft\? \/\* 联防: the leaker's enemies still standing/);
|
||||||
assert.match(sec(14), /`b\.progress \{ battleId, gt, killed, total, resolved\?, leaks\?, left\?, bossDmg\?, by\?, done\? \}`/);
|
assert.match(sec(14), /`b\.progress \{ battleId, gt, killed, total, resolved\?, leaks\?, left\?, bossDmg\?, by\?, leaksBy\?, done\? \}`/);
|
||||||
|
assert.match(doc('shared/protocol.js'), /leaksBy: \(v\) => isMap\(v, RESULT_LIMITS\.players, isId, \(x\) => isNum\(x, 0, 1e6\)\)/);
|
||||||
assert.match(sec(6), /3 \*\*different\*\* free chess of tier `min\(level\+1, 6\)`/);
|
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/);
|
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)
|
// #11 / #9 / #13: the gauge look (§18.3 / §19.5 superseded, §8.2 fill), model scale and fear (§9, §5.3, §2)
|
||||||
|
|||||||
+153
-157
@@ -23,7 +23,7 @@
|
|||||||
{"round":1,"ai_0":[1,27,4,0,0,1,2,"20c00c02",0,"preciShip 0/2, sargonShip 0/2",4,4,0,4]},
|
{"round":1,"ai_0":[1,27,4,0,0,1,2,"20c00c02",0,"preciShip 0/2, sargonShip 0/2",4,4,0,4]},
|
||||||
{"round":2,"ai_0":[1,27,5,0,0,2,4,"8cdbb4d3",0,"preciShip 0/2, sargonShip 0/4",8,8,0,9]},
|
{"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,"24c9b010",1,"indomShip 1/1, preciShip 0/2, sargonShip 0/4",7,7,0,15]},
|
{"round":3,"ai_0":[1,27,6,0,0,3,6,"24c9b010",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,"2f6d1395",16,"deputShip 1/2, indomShip 1/4, preciShip 0/2, sargonShip 1/10",10,10,0,22]},
|
{"round":4,"ai_0":[1,27,7,0,0,3,8,"e0ec421c",16,"deputShip 1/2, indomShip 1/4, preciShip 0/2, sargonShip 1/10",10,10,0,22]},
|
||||||
{"round":5,"ai_0":[1,27,8,0,0,3,8,"beffa9be",29,"deputShip 1/4, indomShip 1/7, preciShip 0/2, sargonShip 1/12, steadShip 1/6",53,16,0,29]},
|
{"round":5,"ai_0":[1,27,8,0,0,3,8,"beffa9be",29,"deputShip 1/4, indomShip 1/7, preciShip 0/2, sargonShip 1/12, steadShip 1/6",53,16,0,29]},
|
||||||
{"round":6,"ai_0":[1,27,9,0,0,4,8,"522b51a4",40,"deputShip 2/8, indomShip 0/7, preciShip 0/2, sargonShip 1/18, steadShip 2/14",28,28,0,40]},
|
{"round":6,"ai_0":[1,27,9,0,0,4,8,"522b51a4",40,"deputShip 2/8, indomShip 0/7, preciShip 0/2, sargonShip 1/18, steadShip 2/14",28,28,0,40]},
|
||||||
{"round":7,"ai_0":[1,27,10,0,0,4,8,"84e59f1d",58,"deputShip 2/12, indomShip 0/7, preciShip 0/2, sargonShip 1/24, steadShip 2/22",27,27,0,51]},
|
{"round":7,"ai_0":[1,27,10,0,0,4,8,"84e59f1d",58,"deputShip 2/12, indomShip 0/7, preciShip 0/2, sargonShip 1/24, steadShip 2/22",27,27,0,51]},
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||||
"trophies": 0,
|
"trophies": 0,
|
||||||
"reward": 113,
|
"reward": 113,
|
||||||
"stats": {"activatedLayers":78,"bossDamage":200000,"dmgDealt":776675,"gold":72,"itemsEquipped":10,"kills":173,"leaks":0,"lpLost":0,"merges":2,"perfectRounds":8,"refreshes":14},
|
"stats": {"activatedLayers":78,"bossDamage":200000,"dmgDealt":775946,"gold":72,"itemsEquipped":10,"kills":173,"leaks":0,"lpLost":0,"merges":2,"perfectRounds":8,"refreshes":14},
|
||||||
"lineup": ["4_14_a@12,7","2_08_a@12,8[4_05_e_a+2_04_e_b]","2_14_a@11,8","3_10_a@10,3","2_17_a@10,4[1_01_e_b+4_02_e_a]","4_01_b@10,7","1_12_b@10,8[3_05_e_a+5_02_e_a]","5_02_a@9,8[4_08_e_a]"],
|
"lineup": ["4_14_a@12,7","2_08_a@12,8[4_05_e_a+2_04_e_b]","2_14_a@11,8","3_10_a@10,3","2_17_a@10,4[1_01_e_b+4_02_e_a]","4_01_b@10,7","1_12_b@10,8[3_05_e_a+5_02_e_a]","5_02_a@9,8[4_08_e_a]"],
|
||||||
"bonds": ["deputShip 0/16","emptyShip 1/0","indomShip 0/7","preciShip 1/7","sargonShip 1/28","steadShip 2/38","swiftShip 1/5"]
|
"bonds": ["deputShip 0/16","emptyShip 1/0","indomShip 0/7","preciShip 1/7","sargonShip 1/28","steadShip 2/38","swiftShip 1/5"]
|
||||||
}
|
}
|
||||||
@@ -106,8 +106,8 @@
|
|||||||
{"round":1,"ai_0":[1,27,4,0,0,1,2,"cc1bac8f",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
|
{"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":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,0,1,6,"a5dafe29",1,"kazimierzShip 0/2, sargonShip 0/8, soloShip 1/1, swiftShip 0/2",8,8,0,15]},
|
{"round":3,"ai_0":[1,27,6,0,0,1,6,"a5dafe29",1,"kazimierzShip 0/2, sargonShip 0/8, soloShip 1/1, swiftShip 0/2",8,8,0,15]},
|
||||||
{"round":4,"ai_0":[1,27,7,0,0,2,8,"3fbb6d23",2,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/2, steadShip 0/2, swiftShip 0/4",8,8,0,22]},
|
{"round":4,"ai_0":[1,27,7,0,0,2,8,"71c18729",2,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/2, steadShip 0/2, swiftShip 0/4",8,8,0,22]},
|
||||||
{"round":5,"ai_0":[1,27,8,0,0,3,8,"e6160244",3,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/3, steadShip 0/2, swiftShip 0/4",21,21,0,30]},
|
{"round":5,"ai_0":[1,27,8,0,0,3,8,"b2a814f4",3,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/3, steadShip 0/2, swiftShip 0/4",21,21,0,30]},
|
||||||
{
|
{
|
||||||
"round": 6,
|
"round": 6,
|
||||||
"ai_0": [1,27,9,0,0,3,8,"83353941",23,"deputShip 1/2, kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/8, soloShip 1/9, steadShip 1/4, swiftShip 0/4",64,19,0,39]
|
"ai_0": [1,27,9,0,0,3,8,"83353941",23,"deputShip 1/2, kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/8, soloShip 1/9, steadShip 1/4, swiftShip 0/4",64,19,0,39]
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
{"round":1,"ai_0":[1,27,4,0,0,1,2,"cc1bac8f",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
|
{"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":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,9,0,15]},
|
{"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":4,"ai_0":[1,27,8,0,1,2,8,"9e01b865",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,"8f1605f3",5,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/4, swiftShip 0/4",21,21,0,31]},
|
{"round":5,"ai_0":[1,27,9,0,0,3,8,"8f1605f3",5,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/4, swiftShip 0/4",21,21,0,31]},
|
||||||
{
|
{
|
||||||
"round": 6,
|
"round": 6,
|
||||||
@@ -387,58 +387,54 @@
|
|||||||
"combat": "client",
|
"combat": "client",
|
||||||
"boost": null,
|
"boost": null,
|
||||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_5","hiddenBossId":"boss_8","factions":["TIMES","DOT","INVISIBLE"]},
|
"setup": {"stageId":"act1autochess_m03","bossId":"boss_5","hiddenBossId":"boss_8","factions":["TIMES","DOT","INVISIBLE"]},
|
||||||
"end": {"victory":false,"roundsPassed":11,"hiddenReached":false,"hiddenCleared":false,"reason":"eliminated","teamLp":null,"bossPoolLeft":null,"virtualMs":94500},
|
"end": {"victory":false,"roundsPassed":10,"hiddenReached":false,"hiddenCleared":false,"reason":"eliminated","teamLp":null,"bossPoolLeft":null,"virtualMs":87100},
|
||||||
"rng": {"setup":25,"shop":121,"waves":0,"draft":42,"bots":7368,"meta":2},
|
"rng": {"setup":25,"shop":114,"waves":0,"draft":42,"bots":6638,"meta":1},
|
||||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||||
"rounds": [
|
"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":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":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,9,0,15]},
|
{"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":4,"ai_0":[1,24,8,0,1,2,8,"dd7803fd",4,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/3, swiftShip 0/4",8,9,3,23]},
|
||||||
{"round":5,"ai_0":[1,27,9,0,0,3,8,"5fdaa5e6",5,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/4, swiftShip 0/4",21,21,0,31]},
|
{"round":5,"ai_0":[1,24,9,0,0,3,8,"5fdaa5e6",5,"kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, soloShip 1/1, steadShip 1/4, swiftShip 0/4",21,21,0,31]},
|
||||||
{
|
{
|
||||||
"round": 6,
|
"round": 6,
|
||||||
"ai_0": [1,27,9,0,0,3,8,"3bdfd652",23,"deputShip 1/2, kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/8, soloShip 1/3, steadShip 1/10, swiftShip 0/4",56,17,0,40]
|
"ai_0": [1,24,9,0,0,3,8,"3bdfd652",23,"deputShip 1/2, kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/8, soloShip 1/3, steadShip 1/10, swiftShip 0/4",56,17,0,40]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 7,
|
"round": 7,
|
||||||
"ai_0": [1,27,10,0,0,4,8,"1e743cfe",38,"deputShip 1/4, kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/16, soloShip 1/5, steadShip 1/13, swiftShip 0/4",28,28,0,50]
|
"ai_0": [1,24,10,0,0,4,8,"1e743cfe",38,"deputShip 1/4, kazimierzShip 0/4, kjeragShip 0/2, sargonShip 0/8, siracusaShip 1/16, soloShip 1/5, steadShip 1/13, swiftShip 0/4",28,28,0,50]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 8,
|
"round": 8,
|
||||||
"ai_0": [1,22,11,0,0,4,8,"0ccee9fb",70,"deputShip 1/6, emptyShip 1/2, kazimierzShip 0/8, kjeragShip 1/9, preciShip 0/5, sargonShip 0/8, siracusaShip 1/31, soloShip 1/7, steadShip 1/15, swiftShip 0/8",14,19,5,61]
|
"ai_0": [1,19,11,0,0,4,8,"0ccee9fb",70,"deputShip 1/6, emptyShip 1/2, kazimierzShip 0/8, kjeragShip 1/9, preciShip 0/5, sargonShip 0/8, siracusaShip 1/31, soloShip 1/7, steadShip 1/15, swiftShip 0/8",14,19,5,61]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 9,
|
"round": 9,
|
||||||
"ai_0": [1,22,12,0,0,4,8,"8f39e18f",106,"arcaneShip 1/10, deputShip 1/8, emptyShip 1/4, kazimierzShip 0/8, kjeragShip 1/16, preciShip 1/14, sargonShip 0/8, siracusaShip 1/45, soloShip 1/9, steadShip 0/15, swiftShip 0/8",34,34,0,75]
|
"ai_0": [1,19,12,0,0,4,8,"8f39e18f",106,"arcaneShip 1/10, deputShip 1/8, emptyShip 1/4, kazimierzShip 0/8, kjeragShip 1/16, preciShip 1/14, sargonShip 0/8, siracusaShip 1/45, soloShip 1/9, steadShip 0/15, swiftShip 0/8",34,34,0,75]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 10,
|
"round": 10,
|
||||||
"ai_0": [1,12,12,0,0,5,8,"2c008a00",122,"arcaneShip 1/12, deputShip 1/10, emptyShip 1/6, kazimierzShip 0/8, kjeragShip 1/18, preciShip 1/16, sargonShip 0/8, siracusaShip 1/49, soloShip 1/11, steadShip 0/15, swiftShip 0/8",14,24,10,86]
|
"ai_0": [1,9,12,0,0,5,8,"2c008a00",122,"arcaneShip 1/12, deputShip 1/10, emptyShip 1/6, kazimierzShip 0/8, kjeragShip 1/18, preciShip 1/16, sargonShip 0/8, siracusaShip 1/49, soloShip 1/11, steadShip 0/15, swiftShip 0/8",14,24,10,86]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 11,
|
"round": 11,
|
||||||
"ai_0": [1,2,12,0,1,5,8,"662804e8",152,"arcaneShip 1/18, deputShip 1/12, emptyShip 1/8, kazimierzShip 0/8, kjeragShip 1/22, preciShip 1/18, sargonShip 0/8, siracusaShip 1/61, soloShip 1/13, steadShip 0/17, swiftShip 0/8",22,34,12,98]
|
"ai_0": [0,0,12,0,0,5,0,"811c9dc5",0,"arcaneShip 0/18, deputShip 0/12, emptyShip 0/8, kazimierzShip 0/8, kjeragShip 0/22, preciShip 0/18, sargonShip 0/8, siracusaShip 0/61, soloShip 0/13, steadShip 0/17, swiftShip 0/8",22,34,12,98]
|
||||||
},
|
|
||||||
{
|
|
||||||
"round": 12,
|
|
||||||
"ai_0": [0,0,13,0,0,6,0,"811c9dc5",0,"arcaneShip 0/18, deputShip 0/14, emptyShip 0/10, kazimierzShip 0/8, kjeragShip 0/27, preciShip 0/23, sargonShip 0/8, siracusaShip 0/69, soloShip 0/15, steadShip 0/17, swiftShip 0/8",9,31,22,111]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"players": {
|
"players": {
|
||||||
"ai_0": {
|
"ai_0": {
|
||||||
"alive": false,
|
"alive": false,
|
||||||
"victory": false,
|
"victory": false,
|
||||||
"roundsPassed": 11,
|
"roundsPassed": 10,
|
||||||
"eliminatedRound": 12,
|
"eliminatedRound": 11,
|
||||||
"lp": 0,
|
"lp": 0,
|
||||||
"bandId": "band_ermengard",
|
"bandId": "band_ermengard",
|
||||||
"title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},
|
"title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},
|
||||||
"trophies": 0,
|
"trophies": 0,
|
||||||
"reward": 255,
|
"reward": 213,
|
||||||
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1683862,"gold":111,"itemsEquipped":13,"kills":235,"leaks":49,"lpLost":35,"merges":2,"perfectRounds":8,"refreshes":6},
|
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1352800,"gold":98,"itemsEquipped":12,"kills":223,"leaks":30,"lpLost":28,"merges":2,"perfectRounds":7,"refreshes":6},
|
||||||
"lineup": [],
|
"lineup": [],
|
||||||
"bonds": ["arcaneShip 0/18","deputShip 0/14","emptyShip 0/10","kazimierzShip 0/8","kjeragShip 0/27","preciShip 0/23","sargonShip 0/8","siracusaShip 0/69","soloShip 0/15","steadShip 0/17","swiftShip 0/8"]
|
"bonds": ["arcaneShip 0/18","deputShip 0/12","emptyShip 0/8","kazimierzShip 0/8","kjeragShip 0/22","preciShip 0/18","sargonShip 0/8","siracusaShip 0/61","soloShip 0/13","steadShip 0/17","swiftShip 0/8"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -651,48 +647,48 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 4,
|
"round": 4,
|
||||||
"ai_0": [1,25,8,0,1,2,8,"0c0d931c",5,"kazimierzShip 0/2, kjeragShip 0/2, soloShip 1/3, steadShip 2/2, swiftShip 0/2",12,12,0,23],
|
"ai_0": [1,25,8,0,1,2,8,"30629777",5,"kazimierzShip 0/2, kjeragShip 0/2, soloShip 1/3, steadShip 2/2, swiftShip 0/2",12,12,0,23],
|
||||||
"ai_1": [1,26,8,0,1,2,8,"81a34b39",0,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, soloShip 1/0, swiftShip 0/2",11,12,1,23]
|
"ai_1": [1,25,8,0,1,2,8,"81a34b39",0,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, soloShip 1/0, swiftShip 0/2",11,12,1,23]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 5,
|
"round": 5,
|
||||||
"ai_0": [1,25,9,0,0,3,8,"9d0d6b64",7,"kazimierzShip 0/2, kjeragShip 0/2, soloShip 1/3, steadShip 2/2, swiftShip 1/2",18,18,0,32],
|
"ai_0": [1,25,9,0,0,3,8,"9d0d6b64",7,"kazimierzShip 0/2, kjeragShip 0/2, soloShip 1/3, steadShip 2/2, swiftShip 1/2",18,18,0,32],
|
||||||
"ai_1": [1,26,9,0,0,3,8,"7a9c7584",0,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, swiftShip 0/2",18,18,0,32]
|
"ai_1": [1,25,9,0,0,3,8,"7a9c7584",0,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, swiftShip 0/2",18,18,0,32]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 6,
|
"round": 6,
|
||||||
"ai_0": [1,25,9,0,0,3,8,"a0e6e026",9,"kazimierzShip 1/2, kjeragShip 0/2, skillfulShip 1/0, soloShip 1/3, steadShip 2/2, swiftShip 1/2",24,24,0,41],
|
"ai_0": [1,25,9,0,0,3,8,"a0e6e026",9,"kazimierzShip 1/2, kjeragShip 0/2, skillfulShip 1/0, soloShip 1/3, steadShip 2/2, swiftShip 1/2",24,24,0,41],
|
||||||
"ai_1": [1,26,9,0,0,3,8,"e1a93e5c",2,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, swiftShip 1/2",24,24,0,43]
|
"ai_1": [1,25,9,0,0,3,8,"e1a93e5c",2,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, swiftShip 1/2",24,24,0,43]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 7,
|
"round": 7,
|
||||||
"ai_0": [1,25,10,0,0,4,8,"7dbd16ef",20,"kazimierzShip 1/2, kjeragShip 0/2, raidShip 1/4, skillfulShip 1/3, soloShip 1/3, steadShip 1/6, swiftShip 1/2",28,28,0,51],
|
"ai_0": [1,25,10,0,0,4,8,"7dbd16ef",20,"kazimierzShip 1/2, kjeragShip 0/2, raidShip 1/4, skillfulShip 1/3, soloShip 1/3, steadShip 1/6, swiftShip 1/2",28,28,0,51],
|
||||||
"ai_1": [1,26,10,0,2,4,8,"6a9b4c34",2,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, swiftShip 1/2",28,28,0,53]
|
"ai_1": [1,25,10,0,2,4,8,"6a9b4c34",2,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 0/2, sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, swiftShip 1/2",28,28,0,53]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 8,
|
"round": 8,
|
||||||
"ai_0": [1,25,11,0,0,4,8,"50b7d162",57,"kazimierzShip 1/16, kjeragShip 0/2, raidShip 1/21, skillfulShip 1/6, soloShip 1/3, steadShip 2/11, swiftShip 0/4",9,9,0,61],
|
"ai_0": [1,25,11,0,0,4,8,"50b7d162",57,"kazimierzShip 1/16, kjeragShip 0/2, raidShip 1/21, skillfulShip 1/6, soloShip 1/3, steadShip 2/11, swiftShip 0/4",9,9,0,61],
|
||||||
"ai_1": [1,26,13,0,0,4,8,"bdd566ef",15,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 2/15, sargonShip 0/4, siracusaShip 1/0, skillfulShip 1/0, soloShip 1/0, swiftShip 0/2",6,9,3,66]
|
"ai_1": [1,25,13,0,0,4,8,"bdd566ef",15,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 2/15, sargonShip 0/4, siracusaShip 1/0, skillfulShip 1/0, soloShip 1/0, swiftShip 0/2",6,9,3,66]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 9,
|
"round": 9,
|
||||||
"ai_0": [1,25,12,0,2,4,8,"d4f6e277",74,"kazimierzShip 1/19, kjeragShip 0/2, raidShip 1/32, skillfulShip 0/9, soloShip 1/3, steadShip 2/12, suntShip 1/0, swiftShip 1/8",23,23,0,73],
|
"ai_0": [1,25,12,0,2,4,8,"d4f6e277",74,"kazimierzShip 1/19, kjeragShip 0/2, raidShip 1/32, skillfulShip 0/9, soloShip 1/3, steadShip 2/12, suntShip 1/0, swiftShip 1/8",23,23,0,73],
|
||||||
"ai_1": [1,25,12,0,1,4,8,"f01fb873",39,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/28, sargonShip 0/4, siracusaShip 1/4, skillfulShip 1/0, soloShip 1/0, swiftShip 1/7",17,23,6,78]
|
"ai_1": [1,24,12,0,1,4,8,"f01fb873",39,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/28, sargonShip 0/4, siracusaShip 1/4, skillfulShip 1/0, soloShip 1/0, swiftShip 1/7",17,23,6,78]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 10,
|
"round": 10,
|
||||||
"ai_0": [1,23,14,0,1,5,8,"eb530a8d",86,"kazimierzShip 1/25, kjeragShip 0/2, raidShip 1/32, skillfulShip 0/9, soloShip 1/3, steadShip 2/12, suntShip 1/0, swiftShip 1/14",6,8,2,88],
|
"ai_0": [1,23,14,0,1,5,8,"eb530a8d",86,"kazimierzShip 1/25, kjeragShip 0/2, raidShip 1/32, skillfulShip 0/9, soloShip 1/3, steadShip 2/12, suntShip 1/0, swiftShip 1/14",6,8,2,88],
|
||||||
"ai_1": [1,22,13,0,0,5,8,"b403dc5e",61,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/41, sargonShip 0/4, siracusaShip 1/4, skillfulShip 1/2, soloShip 1/0, suntShip 1/0, swiftShip 1/14",5,8,3,91]
|
"ai_1": [1,21,13,0,0,5,8,"b403dc5e",61,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/41, sargonShip 0/4, siracusaShip 1/4, skillfulShip 1/2, soloShip 1/0, suntShip 1/0, swiftShip 1/14",5,8,3,91]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 11,
|
"round": 11,
|
||||||
"ai_0": [1,13,13,0,1,5,8,"5c79cc63",105,"kazimierzShip 1/29, kjeragShip 0/2, raidShip 1/38, skillfulShip 0/9, soloShip 1/3, steadShip 2/13, suntShip 1/0, swiftShip 1/22",13,27,14,100],
|
"ai_0": [1,13,13,0,1,5,8,"5c79cc63",105,"kazimierzShip 1/29, kjeragShip 0/2, raidShip 1/38, skillfulShip 0/9, soloShip 1/3, steadShip 2/13, suntShip 1/0, swiftShip 1/22",13,27,14,100],
|
||||||
"ai_1": [1,12,12,0,1,5,8,"33d685b9",87,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/54, sargonShip 0/4, siracusaShip 1/8, skillfulShip 1/4, soloShip 1/0, suntShip 1/0, swiftShip 1/21",8,27,19,102]
|
"ai_1": [1,11,12,0,1,5,8,"33d685b9",87,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/54, sargonShip 0/4, siracusaShip 1/8, skillfulShip 1/4, soloShip 1/0, suntShip 1/0, swiftShip 1/21",8,27,19,102]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 12,
|
"round": 12,
|
||||||
"ai_0": [1,3,13,0,0,6,8,"a701cd86",125,"kazimierzShip 1/29, kjeragShip 0/2, raidShip 1/49, skillfulShip 0/9, soloShip 1/3, steadShip 2/18, suntShip 1/0, swiftShip 1/26, visiShip 1/0",31,50,19,114],
|
"ai_0": [1,3,13,0,0,6,8,"a701cd86",125,"kazimierzShip 1/29, kjeragShip 0/2, raidShip 1/49, skillfulShip 0/9, soloShip 1/3, steadShip 2/18, suntShip 1/0, swiftShip 1/26, visiShip 1/0",31,50,19,114],
|
||||||
"ai_1": [1,2,13,0,0,6,8,"62b855c3",71,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/63, sargonShip 0/4, siracusaShip 2/8, skillfulShip 0/6, soloShip 1/0, suntShip 1/0, swiftShip 0/23",23,50,27,114]
|
"ai_1": [1,1,13,0,0,6,8,"62b855c3",71,"arcaneShip 0/2, kazimierzShip 0/2, preciShip 1/63, sargonShip 0/4, siracusaShip 2/8, skillfulShip 0/6, soloShip 1/0, suntShip 1/0, swiftShip 0/23",23,50,27,114]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 13,
|
"round": 13,
|
||||||
@@ -711,7 +707,7 @@
|
|||||||
"title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},
|
"title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},
|
||||||
"trophies": 3,
|
"trophies": 3,
|
||||||
"reward": 298,
|
"reward": 298,
|
||||||
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":2920409,"gold":127,"itemsEquipped":9,"kills":226,"leaks":65,"lpLost":32,"merges":4,"perfectRounds":9,"refreshes":12},
|
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":2915613,"gold":127,"itemsEquipped":9,"kills":225,"leaks":65,"lpLost":32,"merges":4,"perfectRounds":9,"refreshes":12},
|
||||||
"lineup": [],
|
"lineup": [],
|
||||||
"bonds": ["kazimierzShip 0/45","kjeragShip 0/10","miraShip 0/7","raidShip 0/49","skillfulShip 0/9","soloShip 0/3","steadShip 0/18","swiftShip 0/46","yanShip 0/6"]
|
"bonds": ["kazimierzShip 0/45","kjeragShip 0/10","miraShip 0/7","raidShip 0/49","skillfulShip 0/9","soloShip 0/3","steadShip 0/18","swiftShip 0/46","yanShip 0/6"]
|
||||||
},
|
},
|
||||||
@@ -725,7 +721,7 @@
|
|||||||
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
|
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
|
||||||
"trophies": 3,
|
"trophies": 3,
|
||||||
"reward": 298,
|
"reward": 298,
|
||||||
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":2271756,"gold":126,"itemsEquipped":11,"kills":194,"leaks":88,"lpLost":34,"merges":3,"perfectRounds":6,"refreshes":23},
|
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":2271756,"gold":126,"itemsEquipped":11,"kills":194,"leaks":88,"lpLost":35,"merges":3,"perfectRounds":6,"refreshes":23},
|
||||||
"lineup": [],
|
"lineup": [],
|
||||||
"bonds": ["arcaneShip 0/2","kazimierzShip 0/2","preciShip 0/63","sargonShip 0/4","siracusaShip 0/24","skillfulShip 0/14","swiftShip 0/28"]
|
"bonds": ["arcaneShip 0/2","kazimierzShip 0/2","preciShip 0/63","sargonShip 0/4","siracusaShip 0/24","skillfulShip 0/14","swiftShip 0/28"]
|
||||||
}
|
}
|
||||||
@@ -857,7 +853,7 @@
|
|||||||
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
|
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
|
||||||
"trophies": 2,
|
"trophies": 2,
|
||||||
"reward": 191,
|
"reward": 191,
|
||||||
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1238011,"gold":83,"itemsEquipped":7,"kills":160,"leaks":67,"lpLost":30,"merges":2,"perfectRounds":2,"refreshes":14},
|
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1186269,"gold":83,"itemsEquipped":7,"kills":160,"leaks":67,"lpLost":30,"merges":2,"perfectRounds":2,"refreshes":14},
|
||||||
"lineup": [],
|
"lineup": [],
|
||||||
"bonds": ["indomShip 0/2","siracusaShip 0/80","soloShip 0/4","swiftShip 0/6","yanShip 0/1"]
|
"bonds": ["indomShip 0/2","siracusaShip 0/80","soloShip 0/4","swiftShip 0/6","yanShip 0/1"]
|
||||||
}
|
}
|
||||||
@@ -874,7 +870,7 @@
|
|||||||
"boost": null,
|
"boost": null,
|
||||||
"setup": {"stageId":"act1autochess_m01","bossId":"boss_3","hiddenBossId":null,"factions":["TIMES","ELEMENT","REFLECTION"]},
|
"setup": {"stageId":"act1autochess_m01","bossId":"boss_3","hiddenBossId":null,"factions":["TIMES","ELEMENT","REFLECTION"]},
|
||||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":130,"bossPoolLeft":0,"virtualMs":130000},
|
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":130,"bossPoolLeft":0,"virtualMs":130000},
|
||||||
"rng": {"setup":16,"shop":910,"waves":0,"draft":25,"bots":35892,"meta":30},
|
"rng": {"setup":16,"shop":832,"waves":0,"draft":25,"bots":35892,"meta":31},
|
||||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||||
"rounds": [
|
"rounds": [
|
||||||
{
|
{
|
||||||
@@ -893,7 +889,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 3,
|
"round": 3,
|
||||||
"ai_0": [1,26,6,0,0,1,6,"762d2060",4,"deputShip 1/4, kjeragShip 0/2, sargonShip 0/13, steadShip 0/2",6,6,0,15],
|
"ai_0": [1,26,6,0,0,1,6,"21f68a64",4,"deputShip 1/4, kjeragShip 0/2, sargonShip 0/13, steadShip 0/2",6,6,0,15],
|
||||||
"ai_1": [1,45,6,0,0,1,7,"24a9876d",0,"preciShip 0/2",6,6,0,15],
|
"ai_1": [1,45,6,0,0,1,7,"24a9876d",0,"preciShip 0/2",6,6,0,15],
|
||||||
"ai_2": [1,28,6,0,0,1,6,"683d4946",2,"sargonShip 0/8, siracusaShip 1/2",6,6,0,15],
|
"ai_2": [1,28,6,0,0,1,6,"683d4946",2,"sargonShip 0/8, siracusaShip 1/2",6,6,0,15],
|
||||||
"ai_3": [1,31,6,0,0,1,6,"3694ec95",4,"preciShip 1/4, siracusaShip 0/3, yanShip 0/1",6,6,0,15]
|
"ai_3": [1,31,6,0,0,1,6,"3694ec95",4,"preciShip 1/4, siracusaShip 0/3, yanShip 0/1",6,6,0,15]
|
||||||
@@ -921,52 +917,52 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 7,
|
"round": 7,
|
||||||
"ai_0": [1,26,10,0,0,4,8,"caacae15",53,"deputShip 2/34, kjeragShip 0/2, sargonShip 0/15, steadShip 2/19",29,29,0,52],
|
"ai_0": [1,26,10,0,0,4,8,"58c5f5d4",52,"deputShip 2/34, kjeragShip 0/2, sargonShip 0/15, steadShip 2/18",29,29,0,52],
|
||||||
"ai_1": [1,45,10,0,0,4,8,"ec27f6eb",45,"deputShip 2/30, preciShip 0/4, sargonShip 0/2, siracusaShip 1/15",29,29,0,49],
|
"ai_1": [1,45,10,0,0,4,8,"ec27f6eb",45,"deputShip 2/30, preciShip 0/4, sargonShip 0/2, siracusaShip 1/15",29,29,0,49],
|
||||||
"ai_2": [1,28,10,0,0,4,8,"15280eab",16,"sargonShip 0/8, siracusaShip 1/16, swiftShip 0/6, visiShip 1/0, yanShip 0/1",29,29,0,50],
|
"ai_2": [1,28,10,0,0,4,8,"15280eab",16,"sargonShip 0/8, siracusaShip 1/16, swiftShip 0/6, visiShip 1/0, yanShip 0/1",29,29,0,50],
|
||||||
"ai_3": [1,31,10,0,1,4,8,"53fd121b",82,"deputShip 2/12, emptyShip 1/8, kjeragShip 0/20, preciShip 1/53, siracusaShip 0/3, swiftShip 1/9, yanShip 0/1",29,29,0,50]
|
"ai_3": [1,31,10,0,1,4,8,"53fd121b",82,"deputShip 2/12, emptyShip 1/8, kjeragShip 0/20, preciShip 1/53, siracusaShip 0/3, swiftShip 1/9, yanShip 0/1",29,29,0,50]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 8,
|
"round": 8,
|
||||||
"ai_0": [1,26,11,0,0,4,8,"eba0dfca",119,"deputShip 1/41, kjeragShip 0/2, preciShip 2/20, sargonShip 1/29, steadShip 1/29",15,15,0,61],
|
"ai_0": [1,26,11,0,0,4,8,"eba0dfca",118,"deputShip 1/41, kjeragShip 0/2, preciShip 2/20, sargonShip 1/29, steadShip 1/28",15,15,0,61],
|
||||||
"ai_1": [1,45,11,0,0,4,8,"528da1bc",65,"deputShip 2/38, preciShip 0/4, sargonShip 0/2, siracusaShip 1/23, steadShip 1/4",15,15,0,60],
|
"ai_1": [1,45,11,0,0,4,8,"528da1bc",65,"deputShip 2/38, preciShip 0/4, sargonShip 0/2, siracusaShip 1/23, steadShip 1/4",15,15,0,60],
|
||||||
"ai_2": [1,28,11,0,0,4,8,"dfed3d0a",48,"sargonShip 0/8, siracusaShip 1/36, swiftShip 1/12, visiShip 1/0, yanShip 0/1",15,15,0,59],
|
"ai_2": [1,28,11,0,0,4,8,"dfed3d0a",48,"sargonShip 0/8, siracusaShip 1/36, swiftShip 1/12, visiShip 1/0, yanShip 0/1",15,15,0,59],
|
||||||
"ai_3": [1,31,12,0,1,4,7,"363e51cd",130,"deputShip 1/14, emptyShip 1/10, kjeragShip 1/27, preciShip 1/63, siracusaShip 0/3, swiftShip 1/16, yanShip 0/1",15,15,0,63]
|
"ai_3": [1,31,12,0,1,4,7,"363e51cd",130,"deputShip 1/14, emptyShip 1/10, kjeragShip 1/27, preciShip 1/63, siracusaShip 0/3, swiftShip 1/16, yanShip 0/1",15,15,0,63]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 9,
|
"round": 9,
|
||||||
"ai_0": [1,26,12,0,0,4,8,"fc454676",165,"deputShip 1/47, kjeragShip 0/2, preciShip 2/38, sargonShip 1/41, steadShip 1/39",30,30,0,74],
|
"ai_0": [1,26,12,0,0,4,8,"fc454676",164,"deputShip 1/47, kjeragShip 0/2, preciShip 2/38, sargonShip 1/41, steadShip 1/38",30,30,0,74],
|
||||||
"ai_1": [1,45,12,0,0,4,8,"33bd98dc",83,"deputShip 2/48, emptyShip 1/4, preciShip 0/4, sargonShip 0/2, siracusaShip 1/31, steadShip 0/4",30,30,0,70],
|
"ai_1": [1,45,12,0,0,4,8,"33bd98dc",83,"deputShip 2/48, emptyShip 1/4, preciShip 0/4, sargonShip 0/2, siracusaShip 1/31, steadShip 0/4",30,30,0,70],
|
||||||
"ai_2": [1,28,12,0,0,4,8,"6965081a",70,"sargonShip 0/8, siracusaShip 1/58, swiftShip 1/12, visiShip 1/0, yanShip 0/1",30,30,0,72],
|
"ai_2": [1,28,12,0,0,4,8,"6965081a",70,"sargonShip 0/8, siracusaShip 1/58, swiftShip 1/12, visiShip 1/0, yanShip 0/1",30,30,0,72],
|
||||||
"ai_3": [1,31,13,0,0,4,8,"9a60eaff",155,"deputShip 1/16, emptyShip 1/12, kjeragShip 1/32, preciShip 1/72, siracusaShip 0/3, swiftShip 1/23, yanShip 0/1",30,30,0,76]
|
"ai_3": [1,31,13,0,0,4,8,"9a60eaff",155,"deputShip 1/16, emptyShip 1/12, kjeragShip 1/32, preciShip 1/72, siracusaShip 0/3, swiftShip 1/23, yanShip 0/1",30,30,0,76]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 10,
|
"round": 10,
|
||||||
"ai_0": [1,26,12,0,0,5,8,"2b6a123a",239,"deputShip 1/49, kjeragShip 0/2, preciShip 2/70, sargonShip 1/67, steadShip 1/53",29,29,0,86],
|
"ai_0": [1,26,12,0,0,5,8,"1f68e81b",230,"deputShip 1/53, kjeragShip 0/2, preciShip 2/66, sargonShip 1/63, steadShip 1/48",29,29,0,86],
|
||||||
"ai_1": [1,45,12,0,0,5,8,"d8c7d895",101,"deputShip 2/58, emptyShip 1/8, preciShip 0/4, sargonShip 0/2, siracusaShip 1/35, steadShip 0/4",29,29,0,82],
|
"ai_1": [1,45,12,0,0,5,8,"d8c7d895",101,"deputShip 2/58, emptyShip 1/8, preciShip 0/4, sargonShip 0/2, siracusaShip 1/35, steadShip 0/4",29,29,0,82],
|
||||||
"ai_2": [1,28,12,0,0,5,8,"fe139ef2",72,"sargonShip 0/8, siracusaShip 2/60, swiftShip 1/12, visiShip 1/0, yanShip 0/1",29,29,0,83],
|
"ai_2": [1,28,12,0,0,5,8,"fe139ef2",72,"sargonShip 0/8, siracusaShip 2/60, swiftShip 1/12, visiShip 1/0, yanShip 0/1",29,29,0,83],
|
||||||
"ai_3": [1,31,12,0,0,5,7,"957d85d7",169,"deputShip 1/18, emptyShip 0/14, kjeragShip 1/40, preciShip 1/81, siracusaShip 0/3, swiftShip 1/30, yanShip 0/1",29,29,0,88]
|
"ai_3": [1,31,12,0,0,5,7,"957d85d7",169,"deputShip 1/18, emptyShip 0/14, kjeragShip 1/40, preciShip 1/81, siracusaShip 0/3, swiftShip 1/30, yanShip 0/1",29,29,0,88]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 11,
|
"round": 11,
|
||||||
"ai_0": [1,26,12,0,0,5,8,"a6455ca9",219,"deputShip 0/49, kjeragShip 0/2, preciShip 2/85, sargonShip 1/73, steadShip 2/61",28,28,0,98],
|
"ai_0": [1,26,12,0,0,5,8,"a6455ca9",206,"deputShip 0/53, kjeragShip 0/2, preciShip 2/81, sargonShip 1/69, steadShip 2/56",28,28,0,98],
|
||||||
"ai_1": [1,45,12,0,0,5,8,"8d2d3b28",139,"deputShip 2/68, emptyShip 1/15, indomShip 1/6, preciShip 0/4, sargonShip 0/2, siracusaShip 1/50, steadShip 0/4",13,28,15,95],
|
"ai_1": [1,45,12,0,0,5,8,"8d2d3b28",139,"deputShip 2/68, emptyShip 1/15, indomShip 1/6, preciShip 0/4, sargonShip 0/2, siracusaShip 1/50, steadShip 0/4",13,28,15,95],
|
||||||
"ai_2": [1,28,12,0,0,5,8,"11fc8c7b",106,"emptyShip 1/0, sargonShip 0/8, siracusaShip 2/94, swiftShip 1/12, yanShip 0/1",27,28,1,97],
|
"ai_2": [1,28,12,0,0,5,8,"98a908c4",138,"emptyShip 1/0, sargonShip 0/8, siracusaShip 2/114, swiftShip 1/24, yanShip 0/1",28,28,0,95],
|
||||||
"ai_3": [1,31,12,0,0,5,7,"b2ef4516",206,"deputShip 1/20, emptyShip 0/14, kjeragShip 1/55, preciShip 1/94, siracusaShip 0/3, swiftShip 1/37, yanShip 0/1",28,28,0,100]
|
"ai_3": [1,31,12,0,0,5,7,"e35238bd",203,"deputShip 1/20, emptyShip 0/14, kjeragShip 1/52, preciShip 1/94, siracusaShip 0/3, swiftShip 1/37, yanShip 0/1",28,28,0,100]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 12,
|
"round": 12,
|
||||||
"ai_0": [1,26,12,0,0,6,8,"3fc98b94",258,"deputShip 0/49, kjeragShip 0/2, preciShip 2/106, sargonShip 1/83, steadShip 2/69",46,46,0,112],
|
"ai_0": [1,26,12,0,0,6,8,"6982f097",241,"deputShip 0/53, kjeragShip 0/2, preciShip 2/100, sargonShip 1/77, steadShip 2/64",46,46,0,113],
|
||||||
"ai_1": [1,45,12,0,0,6,8,"c13985aa",151,"deputShip 2/78, emptyShip 1/19, indomShip 0/6, preciShip 0/4, sargonShip 0/2, siracusaShip 1/54, steadShip 0/4",24,46,22,107],
|
"ai_1": [1,45,12,0,0,6,8,"9409d711",157,"deputShip 2/78, emptyShip 1/19, indomShip 0/6, preciShip 0/4, sargonShip 0/2, siracusaShip 1/60, steadShip 0/4",26,46,20,107],
|
||||||
"ai_2": [1,28,12,0,0,6,8,"cbd1cd4c",124,"emptyShip 1/0, sargonShip 0/8, siracusaShip 2/112, swiftShip 1/12, yanShip 0/1",46,46,0,107],
|
"ai_2": [1,28,12,0,0,6,8,"a00d83bb",204,"emptyShip 1/0, sargonShip 0/8, siracusaShip 2/180, swiftShip 1/24, yanShip 0/1",46,46,0,107],
|
||||||
"ai_3": [1,31,12,0,0,6,7,"960125ca",269,"deputShip 1/24, emptyShip 0/14, kjeragShip 1/90, preciShip 1/111, siracusaShip 0/3, swiftShip 1/44, yanShip 0/1",46,46,0,112]
|
"ai_3": [1,31,12,0,0,6,7,"0adf3859",250,"deputShip 1/22, emptyShip 0/14, kjeragShip 1/77, preciShip 1/107, siracusaShip 0/3, swiftShip 1/44, yanShip 0/1",46,46,0,111]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 13,
|
"round": 13,
|
||||||
"ai_0": [1,26,12,0,0,6,8,"c7cc20e6",270,"deputShip 0/49, kjeragShip 0/2, preciShip 2/110, sargonShip 1/83, steadShip 2/77, victoriaShip 0/5, yanShip 0/5",32,32,0,124],
|
"ai_0": [1,26,12,0,0,6,8,"7e6a7c23",277,"deputShip 0/53, kjeragShip 0/2, preciShip 2/120, sargonShip 1/85, steadShip 2/72, victoriaShip 0/5, yanShip 0/5",32,32,0,125],
|
||||||
"ai_1": [1,45,12,0,0,6,8,"b8ae2ad4",181,"deputShip 1/86, emptyShip 1/21, indomShip 2/10, preciShip 0/4, sargonShip 0/2, siracusaShip 1/64, steadShip 0/4",32,32,0,118],
|
"ai_1": [1,45,12,0,0,6,8,"8ffb520b",201,"deputShip 2/90, emptyShip 1/23, indomShip 0/6, preciShip 1/10, sargonShip 0/2, siracusaShip 1/78, steadShip 0/4",32,32,0,122],
|
||||||
"ai_2": [1,28,12,0,0,6,8,"690ad961",210,"emptyShip 1/0, sargonShip 0/8, siracusaShip 2/186, swiftShip 1/24, yanShip 0/1",32,32,0,119],
|
"ai_2": [1,28,12,0,0,6,8,"417dbf48",256,"emptyShip 1/0, sargonShip 0/8, siracusaShip 2/232, swiftShip 1/24, yanShip 0/1",32,32,0,119],
|
||||||
"ai_3": [1,31,12,0,0,6,8,"7229fc68",300,"deputShip 1/26, emptyShip 0/14, kjeragShip 1/99, preciShip 2/124, siracusaShip 0/3, swiftShip 1/51, yanShip 0/1",32,32,0,125]
|
"ai_3": [1,31,12,0,2,6,7,"c443f903",203,"deputShip 0/22, emptyShip 0/14, kjeragShip 1/91, preciShip 2/112, siracusaShip 0/3, swiftShip 0/49, yanShip 0/1",32,32,0,122]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"players": {
|
"players": {
|
||||||
@@ -980,9 +976,9 @@
|
|||||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||||
"trophies": 3,
|
"trophies": 3,
|
||||||
"reward": 250,
|
"reward": 250,
|
||||||
"stats": {"activatedLayers":354,"bossDamage":141377,"dmgDealt":1741970,"gold":138,"itemsEquipped":19,"kills":302,"leaks":0,"lpLost":0,"merges":6,"perfectRounds":13,"refreshes":20},
|
"stats": {"activatedLayers":281,"bossDamage":147152,"dmgDealt":1866747,"gold":139,"itemsEquipped":20,"kills":323,"leaks":0,"lpLost":0,"merges":6,"perfectRounds":13,"refreshes":18},
|
||||||
"lineup": ["2_08_a@12,8[4_04_e_a+4_02_e_b]","2_14_b@11,6","6_12_a@11,7[4_07_e_a+4_03_e_b]","6_05_a@11,8[1_05_e_a+5_02_e_b]","6_14_a@10,3","6_03_a@9,4","3_20_b@9,8[3_02_e_a]","5_02_b@9,9[3_06_e_a+5_02_e_b]"],
|
"lineup": ["6_03_a@12,4","5_02_b@11,6[3_06_e_a+5_02_e_b]","4_20_b@11,7","2_08_b@10,4[4_04_e_a+4_02_e_a]","1_12_b@10,8[3_02_e_a+4_05_e_a]","4_22_b@9,4","6_08_a@9,8[5_01_e_a+4_07_e_a]","6_05_a@9,9[5_09_e_a+3_04_e_a]"],
|
||||||
"bonds": ["deputShip 2/53","emptyShip 1/4","kjeragShip 0/7","preciShip 2/125","sargonShip 1/91","steadShip 1/81","victoriaShip 0/5","yanShip 0/5"]
|
"bonds": ["deputShip 0/53","kjeragShip 0/2","preciShip 2/124","sargonShip 1/85","steadShip 1/72","victoriaShip 0/5","yanShip 0/5"]
|
||||||
},
|
},
|
||||||
"ai_1": {
|
"ai_1": {
|
||||||
"alive": true,
|
"alive": true,
|
||||||
@@ -994,9 +990,9 @@
|
|||||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||||
"trophies": 3,
|
"trophies": 3,
|
||||||
"reward": 250,
|
"reward": 250,
|
||||||
"stats": {"activatedLayers":213,"bossDamage":49702,"dmgDealt":1353781,"gold":130,"itemsEquipped":17,"kills":246,"leaks":37,"lpLost":0,"merges":5,"perfectRounds":11,"refreshes":20},
|
"stats": {"activatedLayers":237,"bossDamage":75279,"dmgDealt":1384510,"gold":135,"itemsEquipped":22,"kills":248,"leaks":35,"lpLost":0,"merges":5,"perfectRounds":11,"refreshes":17},
|
||||||
"lineup": ["2_14_b@12,7","5_10_a@11,6[4_08_e_a+3_01_e_b]","5_20_a@11,7[3_07_e_a+5_02_e_b]","4_16_a@11,8[4_04_e_a+4_11_e_a]","1_20_b@10,7","6_06_a@10,8[4_05_e_b+2_04_e_a]","1_13_b@9,8","6_06_a@9,9"],
|
"lineup": ["2_14_b@12,3","5_10_a@11,6[4_08_e_a+3_01_e_a]","6_12_a@11,7[1_01_e_a+4_05_e_b]","5_20_a@11,8[2_01_e_a+1_05_e_a]","6_14_a@10,3","2_16_b@10,8[4_03_e_a+4_06_e_a]","1_13_b@9,8[5_02_e_b+5_02_e_b]","4_16_a@9,9[4_04_e_a+4_11_e_a]"],
|
||||||
"bonds": ["deputShip 1/94","emptyShip 1/23","indomShip 1/14","preciShip 0/4","sargonShip 0/2","siracusaShip 1/82","steadShip 0/4"]
|
"bonds": ["deputShip 2/102","emptyShip 1/27","indomShip 0/6","preciShip 0/12","sargonShip 0/2","siracusaShip 1/108","steadShip 0/4"]
|
||||||
},
|
},
|
||||||
"ai_2": {
|
"ai_2": {
|
||||||
"alive": true,
|
"alive": true,
|
||||||
@@ -1005,12 +1001,12 @@
|
|||||||
"eliminatedRound": null,
|
"eliminatedRound": null,
|
||||||
"lp": 28,
|
"lp": 28,
|
||||||
"bandId": "band_mlyss",
|
"bandId": "band_mlyss",
|
||||||
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
|
"title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},
|
||||||
"trophies": 3,
|
"trophies": 3,
|
||||||
"reward": 250,
|
"reward": 250,
|
||||||
"stats": {"activatedLayers":240,"bossDamage":315493,"dmgDealt":1782088,"gold":131,"itemsEquipped":20,"kills":287,"leaks":1,"lpLost":0,"merges":4,"perfectRounds":12,"refreshes":21},
|
"stats": {"activatedLayers":280,"bossDamage":320516,"dmgDealt":1866796,"gold":129,"itemsEquipped":20,"kills":288,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":16},
|
||||||
"lineup": ["6_14_a@12,3","6_18_a@11,3[3_01_e_a+1_02_e_a]","1_07_b@11,7[1_01_e_a+1_05_e_a]","5_10_a@11,8[4_03_e_b+5_01_e_b]","3_10_a@10,3","3_18_b@9,4[2_01_e_b+4_02_e_a]","5_20_a@9,8[3_02_e_a+2_01_e_b]","2_16_b@9,9[3_07_e_a]"],
|
"lineup": ["6_14_a@11,6","3_10_b@11,7","5_10_a@11,8[4_03_e_b+4_12_e_a]","3_18_b@10,4[3_07_e_a+4_02_e_a]","2_16_b@10,8[5_01_e_a+2_01_e_a]","4_16_b@9,4","6_18_a@9,8[4_03_e_a+5_02_e_a]","5_20_a@9,9[3_02_e_a+2_01_e_b]"],
|
||||||
"bonds": ["emptyShip 1/0","sargonShip 0/8","siracusaShip 2/216","swiftShip 1/24","yanShip 0/1"]
|
"bonds": ["emptyShip 1/2","sargonShip 0/8","siracusaShip 2/254","swiftShip 1/24","yanShip 0/1"]
|
||||||
},
|
},
|
||||||
"ai_3": {
|
"ai_3": {
|
||||||
"alive": true,
|
"alive": true,
|
||||||
@@ -1022,9 +1018,9 @@
|
|||||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||||
"trophies": 3,
|
"trophies": 3,
|
||||||
"reward": 250,
|
"reward": 250,
|
||||||
"stats": {"activatedLayers":320,"bossDamage":633429,"dmgDealt":2257252,"gold":136,"itemsEquipped":37,"kills":305,"leaks":0,"lpLost":0,"merges":4,"perfectRounds":13,"refreshes":23},
|
"stats": {"activatedLayers":245,"bossDamage":597053,"dmgDealt":2088676,"gold":134,"itemsEquipped":38,"kills":283,"leaks":0,"lpLost":0,"merges":3,"perfectRounds":13,"refreshes":20},
|
||||||
"lineup": ["3_20_b@12,8[5_02_e_b+6_03_e_a]","6_14_a@11,3[4_03_e_a+4_05_e_a]","4_13_b@11,6[3_11_e_a+5_02_e_b]","6_01_a@11,7[4_04_e_b+1_01_e_b]","4_14_a@11,8[2_01_e_b+5_01_e_a]","4_22_a@10,8[4_02_e_a+3_08_e_a]","3_11_b@9,8[4_09_e_a+3_07_e_a]","5_14_a@9,9[4_06_e_b+4_10_e_a]"],
|
"lineup": ["4_21_a@12,3","6_08_a@11,6[4_11_e_a]","6_01_a@11,7","4_13_b@11,8[2_01_e_b+5_02_e_b]","3_20_b@10,3[5_02_e_b+6_03_e_a]","4_22_a@10,8[4_04_e_a+4_02_e_a]","3_11_b@9,8[3_07_e_b+3_06_e_a]","5_14_a@9,9[4_06_e_b+4_04_e_b]"],
|
||||||
"bonds": ["deputShip 1/28","emptyShip 0/14","kjeragShip 1/101","preciShip 2/133","siracusaShip 0/3","swiftShip 1/58","yanShip 0/1"]
|
"bonds": ["deputShip 1/24","emptyShip 0/14","kjeragShip 1/104","preciShip 2/117","siracusaShip 0/3","swiftShip 0/57","yanShip 0/1"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1203,8 +1199,8 @@
|
|||||||
"combat": "server",
|
"combat": "server",
|
||||||
"boost": null,
|
"boost": null,
|
||||||
"setup": {"stageId":"act1autochess_m01","bossId":"boss_1","hiddenBossId":"boss_9","factions":["FLY","DOT","REFLECTION"]},
|
"setup": {"stageId":"act1autochess_m01","bossId":"boss_1","hiddenBossId":"boss_9","factions":["FLY","DOT","REFLECTION"]},
|
||||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":42,"bossPoolLeft":0,"virtualMs":122900},
|
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":46,"bossPoolLeft":0,"virtualMs":116900},
|
||||||
"rng": {"setup":25,"shop":550,"waves":0,"draft":34,"bots":18090,"meta":100},
|
"rng": {"setup":25,"shop":485,"waves":0,"draft":34,"bots":18112,"meta":106},
|
||||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||||
"rounds": [
|
"rounds": [
|
||||||
{
|
{
|
||||||
@@ -1214,63 +1210,63 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 2,
|
"round": 2,
|
||||||
"ai_0": [1,29,5,0,0,1,4,"76d92108",2,"arcaneShip 0/2, preciShip 1/2, soloShip 1/0",10,11,1,9],
|
"ai_0": [1,30,5,0,2,1,4,"4678f135",2,"arcaneShip 0/2, preciShip 1/2, soloShip 1/0",11,11,0,9],
|
||||||
"ai_1": [1,28,5,0,0,1,4,"fca300a0",2,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, soloShip 1/0, steadShip 0/2",10,10,0,9]
|
"ai_1": [1,28,5,0,0,1,4,"a2f52619",2,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, soloShip 1/0, steadShip 0/2",10,10,0,9]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 3,
|
"round": 3,
|
||||||
"ai_0": [1,29,6,0,0,1,7,"b7a24935",5,"arcaneShip 0/2, preciShip 1/2, siracusaShip 0/3, soloShip 1/3",9,9,0,15],
|
"ai_0": [1,30,8,0,0,1,7,"388a275b",5,"arcaneShip 0/2, preciShip 1/2, siracusaShip 0/3, soloShip 1/3",9,9,0,16],
|
||||||
"ai_1": [1,28,6,0,0,1,7,"bf48e672",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",9,9,0,15]
|
"ai_1": [1,28,6,0,0,1,7,"d5a926f3",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",9,9,0,15]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 4,
|
"round": 4,
|
||||||
"ai_0": [1,29,7,0,1,1,8,"e64a4d06",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",13,13,0,22],
|
"ai_0": [1,30,7,0,2,1,8,"3158c50f",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",13,13,0,23],
|
||||||
"ai_1": [1,28,7,0,0,2,8,"ef6d85b4",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",12,12,0,22]
|
"ai_1": [1,28,7,0,0,2,8,"1db5c96f",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",12,12,0,22]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 5,
|
"round": 5,
|
||||||
"ai_0": [1,29,9,0,0,3,8,"c1f02bc4",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",15,15,0,31],
|
"ai_0": [1,30,10,0,0,3,8,"e8ba0f59",13,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 0/6, soloShip 1/9, steadShip 1/2",15,15,0,33],
|
||||||
"ai_1": [1,28,8,0,2,3,8,"a3c40192",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",15,15,0,30]
|
"ai_1": [1,28,8,0,2,3,8,"a3c40192",6,"arcaneShip 0/2, kjeragShip 0/4, preciShip 1/2, soloShip 1/0, steadShip 1/4",15,15,0,30]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 6,
|
"round": 6,
|
||||||
"ai_0": [1,29,9,0,3,3,8,"5840167c",27,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 1/12, soloShip 1/11, steadShip 2/2",25,23,0,39],
|
"ai_0": [1,30,9,0,3,3,8,"64e268da",27,"arcaneShip 0/2, kjeragShip 0/2, preciShip 1/2, siracusaShip 1/12, soloShip 1/11, steadShip 2/2",23,23,0,41],
|
||||||
"ai_1": [1,28,11,0,1,3,8,"c883cfec",11,"arcaneShip 0/2, kjeragShip 0/9, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/4",22,22,0,41]
|
"ai_1": [1,28,11,0,1,3,8,"9fbb73ca",11,"arcaneShip 0/2, kjeragShip 0/9, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/4",22,22,0,41]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 7,
|
"round": 7,
|
||||||
"ai_0": [1,19,13,0,2,4,8,"2261a840",41,"arcaneShip 0/2, deputShip 1/2, kjeragShip 0/2, preciShip 1/4, siracusaShip 1/22, soloShip 1/13, steadShip 0/2",17,27,10,51],
|
"ai_0": [1,20,13,0,2,4,8,"e485391c",41,"arcaneShip 0/2, deputShip 1/2, kjeragShip 0/2, preciShip 1/4, siracusaShip 1/22, soloShip 1/13, steadShip 0/2",16,27,11,53],
|
||||||
"ai_1": [1,23,11,0,1,4,8,"a0f53595",17,"arcaneShip 0/2, kjeragShip 0/9, preciShip 2/7, siracusaShip 1/6, soloShip 1/0, steadShip 1/4, swiftShip 0/6",22,27,5,53]
|
"ai_1": [1,26,11,0,1,4,8,"ba8ea888",17,"arcaneShip 0/2, kjeragShip 0/9, preciShip 2/7, siracusaShip 1/6, soloShip 1/0, steadShip 1/4, swiftShip 0/6",25,27,2,53]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 8,
|
"round": 8,
|
||||||
"ai_0": [1,19,13,0,0,4,8,"876b6843",81,"arcaneShip 1/12, deputShip 2/6, kjeragShip 0/2, preciShip 1/12, siracusaShip 1/34, soloShip 1/17, steadShip 0/2",23,23,0,66],
|
"ai_0": [1,20,13,0,0,4,8,"07dcbf6d",81,"arcaneShip 1/12, deputShip 2/6, kjeragShip 0/2, preciShip 1/12, siracusaShip 1/34, soloShip 1/17, steadShip 0/2",23,23,0,68],
|
||||||
"ai_1": [1,23,12,0,0,4,8,"170e7347",43,"arcaneShip 1/8, kjeragShip 0/13, preciShip 1/7, siracusaShip 2/28, soloShip 1/0, steadShip 0/8, swiftShip 0/6",21,23,2,63]
|
"ai_1": [1,26,12,0,0,4,8,"c6ebef9d",43,"arcaneShip 1/8, kjeragShip 0/13, preciShip 1/7, siracusaShip 2/28, soloShip 1/0, steadShip 0/8, swiftShip 0/6",23,23,0,63]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 9,
|
"round": 9,
|
||||||
"ai_0": [1,19,12,0,1,4,8,"1adfa3ff",119,"arcaneShip 1/23, deputShip 1/8, emptyShip 1/2, indomShip 0/8, kjeragShip 0/2, preciShip 1/18, siracusaShip 1/47, soloShip 1/19, steadShip 0/2, suntShip 1/2, victoriaShip 0/8, visiShip 0/8",16,16,0,76],
|
"ai_0": [1,20,12,0,2,4,8,"4bdb7882",113,"arcaneShip 1/20, deputShip 1/8, emptyShip 1/2, indomShip 0/8, kjeragShip 0/2, preciShip 1/18, siracusaShip 1/44, soloShip 1/19, steadShip 0/2, suntShip 1/2, victoriaShip 0/8, visiShip 0/8",16,16,0,80],
|
||||||
"ai_1": [1,23,12,0,0,4,8,"2376d356",71,"arcaneShip 1/14, indomShip 0/8, kjeragShip 0/13, miraShip 1/0, preciShip 1/7, siracusaShip 2/50, soloShip 1/0, steadShip 0/8, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",15,15,0,74]
|
"ai_1": [1,26,12,0,0,4,8,"f7fe62df",84,"arcaneShip 1/16, indomShip 0/8, kjeragShip 0/13, miraShip 1/3, preciShip 1/9, siracusaShip 2/56, soloShip 1/0, steadShip 0/8, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",15,15,0,74]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 10,
|
"round": 10,
|
||||||
"ai_0": [1,19,13,0,4,5,8,"d7f57e17",144,"arcaneShip 1/27, deputShip 1/10, emptyShip 1/4, indomShip 0/8, kjeragShip 0/2, preciShip 1/25, siracusaShip 1/53, soloShip 1/21, steadShip 0/2, suntShip 1/4, victoriaShip 0/8, visiShip 0/8",24,24,0,89],
|
"ai_0": [1,20,14,0,4,5,8,"1feec931",138,"arcaneShip 1/24, deputShip 1/10, emptyShip 1/4, indomShip 0/8, kjeragShip 0/2, preciShip 1/25, siracusaShip 1/50, soloShip 1/21, steadShip 0/2, suntShip 1/4, victoriaShip 0/8, visiShip 0/8",24,24,0,93],
|
||||||
"ai_1": [1,23,12,0,0,5,8,"c9414faf",92,"arcaneShip 1/16, indomShip 0/8, kjeragShip 0/13, miraShip 1/3, preciShip 1/7, siracusaShip 2/66, soloShip 1/0, steadShip 0/8, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",23,23,0,86]
|
"ai_1": [1,26,12,0,0,5,8,"7b71e64a",105,"arcaneShip 1/18, indomShip 0/8, kjeragShip 0/13, miraShip 1/6, preciShip 1/9, siracusaShip 2/72, soloShip 1/0, steadShip 0/8, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",23,23,0,86]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 11,
|
"round": 11,
|
||||||
"ai_0": [1,19,16,0,0,5,8,"d9d7baf5",196,"arcaneShip 1/35, deputShip 1/12, emptyShip 1/6, indomShip 0/8, kjeragShip 0/2, preciShip 1/32, siracusaShip 1/69, soloShip 1/23, steadShip 0/2, suntShip 1/6, swiftShip 1/13, victoriaShip 0/8, visiShip 0/8",21,23,2,106],
|
"ai_0": [1,20,16,0,0,5,8,"65d57ecf",151,"arcaneShip 1/32, deputShip 0/10, emptyShip 1/4, indomShip 0/8, kjeragShip 0/2, preciShip 2/32, raidShip 1/0, siracusaShip 1/58, soloShip 1/21, steadShip 0/2, suntShip 1/4, victoriaShip 0/8, visiShip 0/8",23,23,0,110],
|
||||||
"ai_1": [1,23,12,0,0,5,8,"b94df622",144,"arcaneShip 1/22, indomShip 0/8, kjeragShip 0/13, miraShip 1/5, preciShip 1/7, siracusaShip 2/110, soloShip 1/0, steadShip 0/8, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",23,23,0,98]
|
"ai_1": [1,26,12,0,0,5,8,"6412e274",145,"arcaneShip 2/30, indomShip 0/8, kjeragShip 0/13, miraShip 1/9, preciShip 0/9, siracusaShip 2/106, soloShip 1/0, steadShip 0/8, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",23,23,0,98]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 12,
|
"round": 12,
|
||||||
"ai_0": [1,19,12,0,0,6,8,"9673c983",228,"arcaneShip 1/39, deputShip 1/14, emptyShip 1/8, indomShip 0/8, kjeragShip 0/2, preciShip 1/41, siracusaShip 1/73, soloShip 1/25, steadShip 0/2, suntShip 1/8, swiftShip 1/20, victoriaShip 0/8, visiShip 0/8",36,46,10,118],
|
"ai_0": [1,20,12,0,1,6,8,"d8b6db14",156,"arcaneShip 1/32, deputShip 0/10, emptyShip 1/4, indomShip 0/8, kjeragShip 0/2, preciShip 2/37, raidShip 1/0, siracusaShip 1/58, soloShip 1/21, steadShip 0/2, suntShip 1/4, victoriaShip 0/8, visiShip 0/8",47,47,0,123],
|
||||||
"ai_1": [1,23,12,0,2,5,8,"7af40c48",221,"arcaneShip 1/30, indomShip 0/8, kjeragShip 0/13, miraShip 1/8, preciShip 1/9, siracusaShip 2/168, soloShip 1/0, steadShip 0/8, swiftShip 1/6, victoriaShip 0/8, visiShip 0/8",46,46,0,112]
|
"ai_1": [1,26,12,0,0,6,8,"6e6d725d",194,"arcaneShip 2/30, indomShip 0/8, kjeragShip 0/13, miraShip 1/12, preciShip 0/9, siracusaShip 2/152, soloShip 1/0, steadShip 0/8, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",46,46,0,111]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 13,
|
"round": 13,
|
||||||
"ai_0": [1,19,12,0,0,6,8,"6b48239d",268,"arcaneShip 1/47, deputShip 1/16, emptyShip 1/10, indomShip 0/8, kjeragShip 0/2, preciShip 1/48, siracusaShip 1/83, soloShip 1/27, steadShip 0/2, suntShip 1/10, swiftShip 1/27, victoriaShip 0/8, visiShip 0/8",28,39,11,128],
|
"ai_0": [1,20,13,0,0,6,8,"dfdf661e",164,"arcaneShip 1/36, deputShip 0/10, emptyShip 1/4, indomShip 0/8, kjeragShip 0/2, preciShip 2/37, raidShip 1/0, siracusaShip 1/62, soloShip 1/21, steadShip 0/2, suntShip 1/4, victoriaShip 0/8, visiShip 0/8",38,39,1,135],
|
||||||
"ai_1": [1,23,14,0,0,6,8,"03a221b4",270,"arcaneShip 1/32, indomShip 0/8, kjeragShip 0/13, miraShip 1/11, preciShip 1/9, siracusaShip 2/212, soloShip 1/0, steadShip 0/8, swiftShip 1/6, victoriaShip 0/8, visiShip 0/8",39,39,0,126]
|
"ai_1": [1,26,12,0,0,6,8,"61848044",279,"arcaneShip 2/40, indomShip 0/8, kjeragShip 0/13, miraShip 1/15, preciShip 0/9, siracusaShip 2/224, soloShip 1/0, steadShip 0/8, suntShip 1/0, swiftShip 0/6, victoriaShip 0/8, visiShip 0/8",39,39,0,121]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"players": {
|
"players": {
|
||||||
@@ -1279,28 +1275,28 @@
|
|||||||
"victory": true,
|
"victory": true,
|
||||||
"roundsPassed": 14,
|
"roundsPassed": 14,
|
||||||
"eliminatedRound": null,
|
"eliminatedRound": null,
|
||||||
"lp": 19,
|
"lp": 20,
|
||||||
"bandId": "band_doberm",
|
"bandId": "band_doberm",
|
||||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
"title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},
|
||||||
"trophies": 4,
|
"trophies": 4,
|
||||||
"reward": 400,
|
"reward": 400,
|
||||||
"stats": {"activatedLayers":312,"bossDamage":190349,"dmgDealt":3265362,"gold":143,"itemsEquipped":17,"kills":253,"leaks":34,"lpLost":11,"merges":3,"perfectRounds":8,"refreshes":26},
|
"stats": {"activatedLayers":176,"bossDamage":279994,"dmgDealt":3560951,"gold":149,"itemsEquipped":22,"kills":273,"leaks":12,"lpLost":10,"merges":4,"perfectRounds":11,"refreshes":20},
|
||||||
"lineup": ["2_14_a@12,3","5_10_a@11,3[1_01_e_a+4_08_e_a]","1_17_a@11,6[1_02_e_a+1_05_e_a]","1_06_b@11,7","4_14_a@10,3[4_06_e_a+4_02_e_a]","4_10_a@10,7[3_11_e_a+6_01_e_a]","1_08_b@10,8[5_02_e_a+4_03_e_b]","3_18_a@9,9[3_04_e_a+4_05_e_a]"],
|
"lineup": ["5_10_a@11,3[6_10_e_a+3_03_e_b]","6_12_a@11,6[2_01_e_a+6_09_e_a]","6_11_a@11,7","1_17_a@10,3[1_05_e_a+1_02_e_b]","4_10_a@10,7[3_11_e_a+4_03_e_a]","4_16_b@10,8[3_05_e_b+5_01_e_a]","6_08_a@9,8[6_01_e_a+4_02_e_a]","1_08_b@9,9[5_02_e_a+1_01_e_b]"],
|
||||||
"bonds": ["arcaneShip 1/57","deputShip 1/18","emptyShip 1/12","indomShip 0/8","kjeragShip 0/2","preciShip 1/57","siracusaShip 1/93","soloShip 1/29","steadShip 0/2","suntShip 1/12","swiftShip 1/34","victoriaShip 0/8","visiShip 0/8"]
|
"bonds": ["arcaneShip 1/42","deputShip 0/10","emptyShip 1/4","indomShip 0/8","kjeragShip 0/2","maniShip 1/0","preciShip 2/37","siracusaShip 1/68","soloShip 1/21","steadShip 0/2","suntShip 1/4","victoriaShip 0/8","visiShip 0/8"]
|
||||||
},
|
},
|
||||||
"ai_1": {
|
"ai_1": {
|
||||||
"alive": true,
|
"alive": true,
|
||||||
"victory": true,
|
"victory": true,
|
||||||
"roundsPassed": 14,
|
"roundsPassed": 14,
|
||||||
"eliminatedRound": null,
|
"eliminatedRound": null,
|
||||||
"lp": 23,
|
"lp": 26,
|
||||||
"bandId": "band_malkie",
|
"bandId": "band_malkie",
|
||||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||||
"trophies": 4,
|
"trophies": 4,
|
||||||
"reward": 400,
|
"reward": 400,
|
||||||
"stats": {"activatedLayers":294,"bossDamage":1159651,"dmgDealt":5116628,"gold":136,"itemsEquipped":19,"kills":284,"leaks":7,"lpLost":5,"merges":4,"perfectRounds":11,"refreshes":26},
|
"stats": {"activatedLayers":321,"bossDamage":1070006,"dmgDealt":4448912,"gold":137,"itemsEquipped":21,"kills":267,"leaks":2,"lpLost":2,"merges":5,"perfectRounds":12,"refreshes":21},
|
||||||
"lineup": ["5_20_a@11,3[3_11_e_a+5_02_e_a]","1_17_a@11,6[5_07_e_a+1_02_e_b]","3_19_a@11,7","6_18_a@10,3[3_06_e_b]","4_10_a@10,7[2_01_e_b+3_04_e_a]","1_08_b@10,8","6_05_a@9,8[1_01_e_b+4_07_e_a]","3_18_a@9,9[4_05_e_a+4_06_e_b]"],
|
"lineup": ["6_18_a@11,3[3_11_e_a+5_07_e_a]","3_19_a@11,6[4_04_e_a+3_04_e_a]","1_17_b@11,7[1_02_e_a]","5_20_a@10,3[4_07_e_a+4_03_e_a]","4_10_a@10,7[2_01_e_b+1_01_e_a]","2_16_b@10,8[4_06_e_a+1_02_e_b]","5_12_a@9,8","1_08_b@9,9[4_10_e_a+4_05_e_a]"],
|
||||||
"bonds": ["arcaneShip 1/38","indomShip 0/8","kjeragShip 0/13","miraShip 1/11","preciShip 1/9","siracusaShip 2/230","soloShip 1/0","steadShip 0/8","swiftShip 1/6","victoriaShip 0/8","visiShip 0/8"]
|
"bonds": ["arcaneShip 2/56","indomShip 0/8","kjeragShip 0/13","miraShip 1/15","preciShip 0/13","siracusaShip 2/250","soloShip 1/0","steadShip 0/8","suntShip 1/0","swiftShip 0/6","victoriaShip 0/8","visiShip 0/8"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1398,8 +1394,8 @@
|
|||||||
"combat": "client",
|
"combat": "client",
|
||||||
"boost": {"lp":400,"layers":300},
|
"boost": {"lp":400,"layers":300},
|
||||||
"setup": {"stageId":"act2autochess_m04","bossId":"boss_5","hiddenBossId":"boss_9","factions":["FLY","ELEMENT","INVISIBLE"]},
|
"setup": {"stageId":"act2autochess_m04","bossId":"boss_5","hiddenBossId":"boss_9","factions":["FLY","ELEMENT","INVISIBLE"]},
|
||||||
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":795,"bossPoolLeft":0,"virtualMs":124650},
|
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":543,"bossPoolLeft":0,"virtualMs":123150},
|
||||||
"rng": {"setup":25,"shop":750,"waves":0,"draft":61,"bots":22165,"meta":198},
|
"rng": {"setup":25,"shop":591,"waves":0,"draft":61,"bots":22093,"meta":138},
|
||||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||||
"rounds": [
|
"rounds": [
|
||||||
{
|
{
|
||||||
@@ -1410,62 +1406,62 @@
|
|||||||
{
|
{
|
||||||
"round": 2,
|
"round": 2,
|
||||||
"ai_0": [1,400,5,0,0,1,4,"f5818d14",302,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/302, 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 0/300, steadShip 1/302, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",9,9,0,9],
|
"ai_0": [1,400,5,0,0,1,4,"f5818d14",302,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/302, 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 0/300, steadShip 1/302, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",9,9,0,9],
|
||||||
"ai_1": [1,400,5,0,1,1,4,"e14f7c28",0,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, 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 0/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,10,0,9]
|
"ai_1": [1,400,5,0,1,1,4,"1bb10c7b",0,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, 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 0/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",10,10,0,9]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 3,
|
"round": 3,
|
||||||
"ai_0": [1,400,6,0,1,1,7,"6ff64423",603,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, 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/300, steadShip 1/303, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,15],
|
"ai_0": [1,400,6,0,1,1,7,"0d06d571",603,"arcaneShip 0/300, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, 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/300, steadShip 1/303, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,15],
|
||||||
"ai_1": [1,400,7,0,2,1,7,"97624e1b",602,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,16]
|
"ai_1": [1,400,7,0,2,1,7,"f4115666",602,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/300, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",11,11,0,16]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 4,
|
"round": 4,
|
||||||
"ai_0": [1,400,8,0,1,2,8,"f2591697",1206,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/300, soloShip 1/300, steadShip 1/304, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",21,21,0,23],
|
"ai_0": [1,400,8,0,1,2,8,"f2591697",1206,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/300, soloShip 1/300, steadShip 1/304, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",21,21,0,23],
|
||||||
"ai_1": [1,400,9,0,4,2,8,"a5d7e73a",909,"arcaneShip 0/302, deputShip 1/302, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/305, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/302, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,24]
|
"ai_1": [1,400,9,0,4,2,8,"bb20c536",909,"arcaneShip 0/302, deputShip 1/302, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/305, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/302, steadShip 0/300, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",22,22,0,24]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 5,
|
"round": 5,
|
||||||
"ai_0": [1,400,9,0,0,3,8,"e81795ce",1207,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/300, soloShip 1/300, steadShip 1/305, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,31],
|
"ai_0": [1,400,9,0,0,3,8,"1ba198e4",1207,"arcaneShip 0/302, deputShip 0/300, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/302, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/300, soloShip 1/300, steadShip 1/305, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,31],
|
||||||
"ai_1": [1,400,12,0,0,3,8,"9203fcff",1219,"arcaneShip 0/302, deputShip 1/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/302, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,36]
|
"ai_1": [1,400,12,0,0,3,8,"db4f0eb6",1219,"arcaneShip 0/302, deputShip 1/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/302, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,36]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 6,
|
"round": 6,
|
||||||
"ai_0": [1,400,9,0,2,3,8,"765371f3",1528,"arcaneShip 1/307, deputShip 1/302, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/304, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/302, steadShip 1/313, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",27,27,0,41],
|
"ai_0": [1,400,9,0,2,3,8,"765371f3",1528,"arcaneShip 1/307, deputShip 1/302, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/304, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/302, steadShip 1/313, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",27,27,0,41],
|
||||||
"ai_1": [1,400,9,0,3,3,8,"2eaa7d4c",919,"arcaneShip 0/302, deputShip 1/308, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/306, steadShip 2/305, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",28,28,0,44]
|
"ai_1": [1,400,9,0,2,3,8,"12bd0f90",921,"arcaneShip 0/302, deputShip 1/307, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/306, steadShip 2/308, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",28,28,0,44]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 7,
|
"round": 7,
|
||||||
"ai_0": [1,400,12,0,1,4,8,"d26ea2d9",1544,"arcaneShip 1/312, deputShip 1/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/306, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/318, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",25,25,0,54],
|
"ai_0": [1,400,12,0,1,4,8,"22644261",1545,"arcaneShip 1/312, deputShip 1/304, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/306, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/319, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",25,25,0,54],
|
||||||
"ai_1": [1,400,13,0,0,4,8,"b18267fd",1242,"arcaneShip 0/302, deputShip 2/316, egirShip 0/300, emptyShip 1/304, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/310, steadShip 2/312, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,25,1,59]
|
"ai_1": [1,400,12,0,0,4,8,"2dd93a93",938,"arcaneShip 0/302, deputShip 2/311, egirShip 0/300, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/310, steadShip 2/317, suntShip 0/300, swiftShip 0/300, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",25,25,0,57]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 8,
|
"round": 8,
|
||||||
"ai_0": [1,400,12,0,0,4,8,"d692537c",1558,"arcaneShip 1/315, deputShip 0/304, egirShip 1/307, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/310, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 2/322, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,18,0,64],
|
"ai_0": [1,400,12,0,0,4,8,"61230d1b",1552,"arcaneShip 1/312, deputShip 0/304, egirShip 1/303, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 2/310, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 2/323, suntShip 0/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,18,0,64],
|
||||||
"ai_1": [1,400,11,0,0,4,8,"b2496d66",1574,"arcaneShip 0/302, deputShip 2/324, egirShip 0/300, emptyShip 1/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 1/318, skillfulShip 0/300, soloShip 1/314, steadShip 0/312, suntShip 0/300, swiftShip 1/310, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",18,18,0,70]
|
"ai_1": [1,400,11,0,1,4,8,"770dde40",1261,"arcaneShip 0/302, deputShip 2/315, egirShip 0/300, emptyShip 0/300, indomShip 1/304, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/314, steadShip 1/328, suntShip 0/300, swiftShip 0/300, victoriaShip 0/305, visiShip 0/300, yanShip 0/305",19,19,0,68]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 9,
|
"round": 9,
|
||||||
"ai_0": [1,400,12,0,0,4,8,"cdec5dfa",2231,"arcaneShip 2/318, deputShip 0/304, egirShip 1/319, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/310, raidShip 1/353, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/327, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,77],
|
"ai_0": [1,400,12,0,0,4,8,"b75b1b8c",2237,"arcaneShip 2/312, deputShip 0/304, egirShip 1/314, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/310, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/314, raidShip 1/353, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/330, suntShip 0/300, swiftShip 0/306, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,77],
|
||||||
"ai_1": [1,400,12,0,3,6,8,"36a202d9",2210,"arcaneShip 0/302, deputShip 1/326, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/306, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 1/342, skillfulShip 1/302, soloShip 1/316, steadShip 0/312, suntShip 1/302, swiftShip 1/316, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",29,25,0,114]
|
"ai_1": [1,400,13,0,3,4,8,"d94eb18f",1569,"arcaneShip 0/302, deputShip 1/317, egirShip 0/300, emptyShip 0/300, indomShip 1/306, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/302, soloShip 0/314, steadShip 1/336, suntShip 0/300, swiftShip 0/300, victoriaShip 1/308, visiShip 0/300, yanShip 0/305",24,24,0,80]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 10,
|
"round": 10,
|
||||||
"ai_0": [1,400,12,0,0,5,8,"582856d0",2251,"arcaneShip 2/318, deputShip 0/304, egirShip 1/327, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/304, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/310, raidShip 1/363, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/329, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,89],
|
"ai_0": [1,400,12,0,0,5,8,"6e225676",2268,"arcaneShip 2/312, deputShip 0/304, egirShip 1/319, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/318, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/322, raidShip 1/363, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/330, suntShip 0/300, swiftShip 0/306, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,92],
|
||||||
"ai_1": [1,400,15,0,2,6,8,"3a43d0ba",2260,"arcaneShip 0/302, deputShip 1/328, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/314, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 1/370, skillfulShip 1/306, soloShip 1/318, steadShip 0/312, suntShip 1/304, swiftShip 1/320, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",13,13,0,130]
|
"ai_1": [1,400,15,0,0,5,8,"a3501bfb",1588,"arcaneShip 0/302, deputShip 1/319, egirShip 0/300, emptyShip 0/300, indomShip 0/306, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/314, soloShip 0/314, steadShip 1/342, suntShip 1/302, swiftShip 0/300, victoriaShip 1/311, visiShip 0/300, yanShip 0/305",12,12,0,97]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 11,
|
"round": 11,
|
||||||
"ai_0": [1,400,12,0,1,5,8,"9ac6e043",2281,"arcaneShip 2/332, deputShip 0/304, egirShip 1/340, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/315, raidShip 1/382, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/308, soloShip 1/304, steadShip 0/333, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",38,34,0,102],
|
"ai_0": [1,400,12,0,2,5,8,"3fe9e60e",2646,"arcaneShip 1/315, deputShip 0/304, egirShip 1/338, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/326, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/335, raidShip 1/384, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/333, suntShip 0/300, swiftShip 1/311, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",34,34,0,102],
|
||||||
"ai_1": [1,400,14,0,1,6,8,"120b46d6",2628,"arcaneShip 1/318, deputShip 1/330, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/322, preciShip 0/311, raidShip 0/300, sargonShip 0/300, siracusaShip 1/390, skillfulShip 1/318, soloShip 1/320, steadShip 0/312, suntShip 1/306, swiftShip 1/324, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",34,34,0,144]
|
"ai_1": [1,400,12,0,1,5,8,"44ef6174",1936,"arcaneShip 0/302, deputShip 1/321, egirShip 0/300, emptyShip 0/300, indomShip 0/306, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/327, soloShip 1/316, steadShip 1/351, suntShip 1/304, swiftShip 0/300, victoriaShip 1/317, visiShip 0/300, yanShip 0/305",34,34,0,110]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 12,
|
"round": 12,
|
||||||
"ai_0": [1,400,13,0,0,6,8,"afebef50",2325,"arcaneShip 2/340, deputShip 0/304, egirShip 1/345, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/320, raidShip 1/400, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/316, soloShip 1/304, steadShip 0/333, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",6,14,8,115],
|
"ai_0": [1,400,14,0,0,6,8,"2a0b76e3",2717,"arcaneShip 1/318, deputShip 0/304, egirShip 1/351, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/342, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/356, raidShip 1/397, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/333, suntShip 0/300, swiftShip 1/316, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",6,14,8,116],
|
||||||
"ai_1": [1,400,13,0,5,6,8,"9e462d87",2688,"arcaneShip 1/333, deputShip 1/332, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/328, preciShip 0/311, raidShip 0/300, sargonShip 0/300, siracusaShip 1/411, skillfulShip 1/326, soloShip 1/322, steadShip 0/312, suntShip 1/308, swiftShip 1/328, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",15,15,0,152]
|
"ai_1": [1,400,13,0,3,6,8,"224928d1",1961,"arcaneShip 0/302, deputShip 1/323, egirShip 0/300, emptyShip 0/300, indomShip 0/306, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/335, soloShip 1/318, steadShip 1/359, suntShip 1/306, swiftShip 0/300, victoriaShip 1/320, visiShip 0/300, yanShip 0/305",15,15,0,121]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 13,
|
"round": 13,
|
||||||
"ai_0": [1,400,12,0,2,6,8,"97689296",2380,"arcaneShip 2/349, deputShip 0/304, egirShip 1/359, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/325, raidShip 1/421, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/322, soloShip 1/304, steadShip 0/333, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,126],
|
"ai_0": [1,400,12,0,2,6,8,"06928963",3079,"arcaneShip 1/321, deputShip 0/304, egirShip 1/358, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 1/354, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/369, raidShip 1/415, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/333, suntShip 1/300, swiftShip 1/325, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,129],
|
||||||
"ai_1": [1,400,17,0,0,6,8,"36a5d772",2717,"arcaneShip 1/345, deputShip 0/332, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/334, preciShip 1/311, raidShip 0/300, sargonShip 0/300, siracusaShip 1/429, skillfulShip 1/338, soloShip 1/322, steadShip 0/312, suntShip 1/308, swiftShip 1/330, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,168]
|
"ai_1": [1,400,15,0,0,6,8,"b067967e",1988,"arcaneShip 0/302, deputShip 1/325, egirShip 0/300, emptyShip 0/300, indomShip 0/306, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/345, soloShip 1/320, steadShip 1/367, suntShip 1/308, swiftShip 0/300, victoriaShip 1/323, visiShip 0/300, yanShip 0/305",41,41,0,134]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"players": {
|
"players": {
|
||||||
@@ -1474,28 +1470,28 @@
|
|||||||
"victory": true,
|
"victory": true,
|
||||||
"roundsPassed": 15,
|
"roundsPassed": 15,
|
||||||
"eliminatedRound": null,
|
"eliminatedRound": null,
|
||||||
"lp": 398,
|
"lp": 272,
|
||||||
"bandId": "band_sarkazb",
|
"bandId": "band_sarkazb",
|
||||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||||
"trophies": 8,
|
"trophies": 8,
|
||||||
"reward": 425,
|
"reward": 425,
|
||||||
"stats": {"activatedLayers":3362,"bossDamage":7735924,"dmgDealt":20519044,"gold":154,"itemsEquipped":21,"kills":287,"leaks":8,"lpLost":0,"merges":7,"perfectRounds":12,"refreshes":21},
|
"stats": {"activatedLayers":2790,"bossDamage":7592235,"dmgDealt":21017016,"gold":156,"itemsEquipped":19,"kills":331,"leaks":8,"lpLost":0,"merges":8,"perfectRounds":12,"refreshes":31},
|
||||||
"lineup": ["4_09_b@11,5[3_11_e_a+3_11_e_b]","6_11_a@11,8","3_13_b@10,3","3_05_b@10,8[1_01_e_a+6_09_e_a]","6_16_a@9,6","6_05_a@9,7[3_02_e_a]","4_14_b@9,8[2_01_e_a+1_02_e_b]","5_07_a@9,9[4_10_e_b]"],
|
"lineup": ["2_18_b@11,5[1_05_e_b+1_01_e_a]","4_14_a@11,8[4_03_e_a]","4_09_a@10,3","1_19_b@10,4","6_17_b@10,8[2_01_e_a+1_02_e_a]","3_09_a@9,7","4_20_b@9,8[1_01_e_b+3_11_e_a]","3_05_b@9,9"],
|
||||||
"bonds": ["arcaneShip 1/365","deputShip 0/304","egirShip 1/362","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 0/308","lateranoShip 0/300","maniShip 1/300","miraShip 0/300","preciShip 1/345","raidShip 1/423","sargonShip 1/300","siracusaShip 0/300","skillfulShip 1/338","soloShip 1/307","steadShip 0/333","suntShip 1/300","swiftShip 1/322","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
"bonds": ["arcaneShip 0/321","deputShip 0/304","egirShip 1/358","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 1/357","kjeragShip 0/302","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/379","raidShip 1/424","sargonShip 0/300","siracusaShip 0/300","skillfulShip 0/300","soloShip 1/304","steadShip 1/333","suntShip 2/300","swiftShip 1/335","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
||||||
},
|
},
|
||||||
"ai_1": {
|
"ai_1": {
|
||||||
"alive": true,
|
"alive": true,
|
||||||
"victory": true,
|
"victory": true,
|
||||||
"roundsPassed": 15,
|
"roundsPassed": 15,
|
||||||
"eliminatedRound": null,
|
"eliminatedRound": null,
|
||||||
"lp": 397,
|
"lp": 271,
|
||||||
"bandId": "band_doberm",
|
"bandId": "band_doberm",
|
||||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||||
"trophies": 8,
|
"trophies": 8,
|
||||||
"reward": 425,
|
"reward": 425,
|
||||||
"stats": {"activatedLayers":2801,"bossDamage":6156699,"dmgDealt":23996575,"gold":191,"itemsEquipped":32,"kills":287,"leaks":1,"lpLost":0,"merges":5,"perfectRounds":12,"refreshes":55},
|
"stats": {"activatedLayers":2377,"bossDamage":6286125,"dmgDealt":26191649,"gold":159,"itemsEquipped":34,"kills":281,"leaks":0,"lpLost":0,"merges":3,"perfectRounds":13,"refreshes":23},
|
||||||
"lineup": ["4_10_a@11,5[3_05_e_a+4_03_e_a]","6_05_a@11,8[4_08_e_a+6_09_e_a]","6_16_a@10,3[3_11_e_a+1_05_e_b]","2_13_a@10,7[4_04_e_a+4_05_e_a]","1_08_b@10,8[4_07_e_b+5_07_e_a]","6_08_a@9,6[2_01_e_b+3_01_e_b]","5_20_a@9,7[1_05_e_a+3_04_e_a]","6_08_a@9,8[3_03_e_a+1_01_e_a]","3_19_b@9,9[2_04_e_a+3_07_e_b]"],
|
"lineup": ["2_17_a@12,8[4_02_e_a+3_03_e_a]","1_13_b@11,5[4_07_e_a+3_11_e_a]","5_03_a@11,8[3_02_e_a+2_03_e_b]","3_16_a@10,4[1_05_e_b+2_01_e_b]","4_11_a@10,8[4_11_e_a+4_09_e_a]","6_16_a@9,7[2_03_e_b+4_09_e_b]","1_06_b@9,8[3_06_e_a+3_08_e_b]","5_12_a@9,9[5_03_e_a+1_01_e_b]"],
|
||||||
"bonds": ["arcaneShip 1/371","deputShip 0/332","egirShip 0/300","emptyShip 0/308","indomShip 0/300","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 1/334","preciShip 1/311","raidShip 0/300","sargonShip 0/300","siracusaShip 1/465","skillfulShip 1/356","soloShip 1/322","steadShip 0/312","suntShip 1/308","swiftShip 1/334","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
"bonds": ["arcaneShip 1/332","deputShip 1/329","egirShip 0/300","emptyShip 0/300","indomShip 0/306","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 0/309","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/370","soloShip 1/324","steadShip 1/377","suntShip 1/312","swiftShip 0/300","victoriaShip 1/333","visiShip 0/300","yanShip 0/305"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1638,8 +1634,8 @@
|
|||||||
"combat": "client",
|
"combat": "client",
|
||||||
"boost": null,
|
"boost": null,
|
||||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_7","hiddenBossId":"boss_10","factions":["ELEMENT","INVISIBLE","REFLECTION"]},
|
"setup": {"stageId":"act1autochess_m03","bossId":"boss_7","hiddenBossId":"boss_10","factions":["ELEMENT","INVISIBLE","REFLECTION"]},
|
||||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":55,"bossPoolLeft":0,"virtualMs":119900},
|
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":55,"bossPoolLeft":0,"virtualMs":122900},
|
||||||
"rng": {"setup":25,"shop":477,"waves":0,"draft":34,"bots":16274,"meta":23},
|
"rng": {"setup":25,"shop":512,"waves":0,"draft":34,"bots":16483,"meta":18},
|
||||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||||
"rounds": [
|
"rounds": [
|
||||||
{"round":1,"h_0":[1,27,4,0,0,5,1,"3e4697a0",0,"",5,5,0,3],"ai_0":[1,28,4,0,0,1,2,"a1734bc6",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
|
{"round":1,"h_0":[1,27,4,0,0,5,1,"3e4697a0",0,"",5,5,0,3],"ai_0":[1,28,4,0,0,1,2,"a1734bc6",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
|
||||||
@@ -1652,48 +1648,48 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 5,
|
"round": 5,
|
||||||
"h_0": [1,27,8,0,0,5,8,"1a62176b",30,"indomShip 1/0, preciShip 0/2, sargonShip 1/28, steadShip 1/1, victoriaShip 1/1",27,27,0,28],
|
"h_0": [1,27,8,0,0,5,8,"89fff9d8",29,"indomShip 1/0, preciShip 0/2, sargonShip 1/26, steadShip 1/2, victoriaShip 1/1",27,27,0,28],
|
||||||
"ai_0": [1,28,8,0,1,2,8,"e0bfc937",0,"sargonShip 0/2, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",27,27,0,29]
|
"ai_0": [1,28,8,0,1,2,8,"e0bfc937",0,"sargonShip 0/2, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",27,27,0,29]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 6,
|
"round": 6,
|
||||||
"h_0": [1,27,9,0,1,5,8,"7f0036b7",93,"arcaneShip 0/2, indomShip 1/6, preciShip 1/17, sargonShip 1/50, steadShip 1/6, victoriaShip 1/8, visiShip 1/6",24,22,0,38],
|
"h_0": [1,27,9,0,1,5,8,"7f0036b7",92,"arcaneShip 0/2, indomShip 1/6, preciShip 1/17, sargonShip 1/48, steadShip 1/7, victoriaShip 1/8, visiShip 1/6",24,22,0,38],
|
||||||
"ai_0": [1,28,10,0,2,3,8,"ea720e1b",0,"sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",22,22,0,39]
|
"ai_0": [1,28,10,0,2,3,8,"ea720e1b",0,"sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",22,22,0,39]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 7,
|
"round": 7,
|
||||||
"h_0": [1,27,11,0,1,5,8,"f502b39b",129,"arcaneShip 0/2, indomShip 1/10, preciShip 1/24, sargonShip 1/71, steadShip 1/11, victoriaShip 1/13, visiShip 0/6",28,26,0,50],
|
"h_0": [1,27,11,0,1,5,8,"5702ef65",134,"arcaneShip 0/2, indomShip 1/6, preciShip 1/40, sargonShip 1/67, steadShip 1/12, victoriaShip 1/9, visiShip 0/6",27,26,1,50],
|
||||||
"ai_0": [1,28,12,0,2,4,8,"6aec478c",0,"sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",26,26,0,51]
|
"ai_0": [1,28,12,0,2,4,8,"6aec478c",0,"sargonShip 0/4, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, yanShip 0/6",26,26,0,51]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 8,
|
"round": 8,
|
||||||
"h_0": [1,27,12,0,0,5,8,"97941a60",215,"arcaneShip 0/2, indomShip 1/22, preciShip 2/57, sargonShip 1/104, soloShip 1/0, steadShip 2/32, victoriaShip 0/25, visiShip 0/6",18,18,0,65],
|
"h_0": [1,27,12,0,0,5,8,"b6d1cc82",224,"arcaneShip 0/2, indomShip 1/18, preciShip 2/73, sargonShip 1/100, soloShip 1/0, steadShip 2/33, victoriaShip 0/21, visiShip 0/6",18,18,0,63],
|
||||||
"ai_0": [1,28,13,0,1,4,8,"8d499ef9",16,"miraShip 1/0, sargonShip 0/4, skillfulShip 1/3, soloShip 1/0, steadShip 1/0, suntShip 1/0, yanShip 1/13",17,18,1,63]
|
"ai_0": [1,28,13,0,1,4,8,"8d499ef9",16,"miraShip 1/0, sargonShip 0/4, skillfulShip 1/3, soloShip 1/0, steadShip 1/0, suntShip 1/0, yanShip 1/13",17,18,1,63]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 9,
|
"round": 9,
|
||||||
"h_0": [1,27,12,0,4,5,8,"96baa6b8",260,"arcaneShip 0/2, indomShip 1/22, preciShip 1/66, sargonShip 1/117, soloShip 1/0, steadShip 2/40, swiftShip 1/15, victoriaShip 0/25, visiShip 0/6",23,23,0,76],
|
"h_0": [1,27,12,0,4,5,8,"e8c67194",264,"arcaneShip 0/2, indomShip 1/18, preciShip 2/94, sargonShip 1/108, steadShip 1/34, victoriaShip 0/21, visiShip 1/10",22,23,1,77],
|
||||||
"ai_0": [1,28,13,0,0,4,8,"a5033a5f",77,"miraShip 1/0, preciShip 1/29, sargonShip 1/28, skillfulShip 1/3, soloShip 1/0, suntShip 1/0, yanShip 1/17",20,23,3,76]
|
"ai_0": [1,28,13,0,2,4,8,"f213e56e",30,"sargonShip 0/4, siracusaShip 1/8, skillfulShip 1/3, soloShip 1/0, suntShip 1/0, yanShip 1/19",23,23,0,76]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 10,
|
"round": 10,
|
||||||
"h_0": [1,27,16,0,2,5,8,"831f33bf",270,"arcaneShip 0/2, indomShip 1/22, kazimierzShip 0/8, kjeragShip 0/8, preciShip 1/80, sargonShip 1/123, soloShip 1/0, steadShip 2/45, suntShip 1/0, swiftShip 0/23, victoriaShip 0/25, visiShip 0/6",31,31,0,90],
|
"h_0": [1,27,16,0,2,5,8,"9e1684a3",309,"arcaneShip 0/2, indomShip 1/18, preciShip 2/110, sargonShip 1/116, soloShip 1/0, steadShip 2/43, suntShip 1/0, victoriaShip 0/21, visiShip 1/22",31,31,0,91],
|
||||||
"ai_0": [1,28,12,0,0,5,8,"276b0c45",128,"miraShip 1/0, preciShip 1/56, sargonShip 1/50, skillfulShip 1/3, soloShip 1/0, suntShip 1/0, visiShip 1/0, yanShip 1/19",31,31,0,88]
|
"ai_0": [1,28,14,0,0,5,8,"dcc3d5bd",35,"sargonShip 0/4, siracusaShip 1/8, skillfulShip 1/6, soloShip 1/0, suntShip 2/0, yanShip 1/21",31,31,0,89]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 11,
|
"round": 11,
|
||||||
"h_0": [1,27,14,0,0,6,8,"e9f0f2fe",363,"arcaneShip 0/2, indomShip 1/22, kazimierzShip 0/8, kjeragShip 0/8, preciShip 2/99, sargonShip 1/147, steadShip 1/49, suntShip 1/0, swiftShip 1/40, victoriaShip 0/25, visiShip 1/6",19,19,0,102],
|
"h_0": [1,27,16,0,0,6,8,"92c1a6c0",354,"arcaneShip 0/2, indomShip 1/18, preciShip 2/127, sargonShip 1/120, steadShip 1/46, suntShip 1/0, swiftShip 1/5, victoriaShip 0/21, visiShip 1/38",19,19,0,106],
|
||||||
"ai_0": [1,28,12,0,0,5,8,"bd0a32a3",183,"preciShip 1/76, sargonShip 1/74, skillfulShip 1/3, soloShip 1/0, suntShip 1/0, victoriaShip 0/5, visiShip 1/0, yanShip 1/30",19,19,0,102]
|
"ai_0": [1,28,12,0,0,5,8,"57021233",53,"sargonShip 0/4, siracusaShip 1/12, skillfulShip 1/12, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/29",19,19,0,101]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 12,
|
"round": 12,
|
||||||
"h_0": [1,27,12,0,0,6,8,"efd0ac13",413,"arcaneShip 0/2, indomShip 1/26, kazimierzShip 0/8, kjeragShip 0/8, maniShip 1/0, preciShip 1/129, sargonShip 1/169, steadShip 2/54, suntShip 1/0, swiftShip 0/40, victoriaShip 1/35, visiShip 0/6",33,33,0,117],
|
"h_0": [1,27,14,0,2,6,8,"7d4a4b17",428,"arcaneShip 0/2, indomShip 1/18, preciShip 2/159, sargonShip 1/139, steadShip 1/48, suntShip 1/0, swiftShip 1/10, victoriaShip 0/21, visiShip 1/54",33,33,0,124],
|
||||||
"ai_0": [1,28,12,0,0,6,8,"931d261f",245,"preciShip 1/105, sargonShip 1/107, skillfulShip 1/3, soloShip 1/0, suntShip 1/0, victoriaShip 0/5, visiShip 1/0, yanShip 1/30",27,33,6,113]
|
"ai_0": [1,28,12,0,0,6,8,"672ecfc2",63,"sargonShip 0/8, siracusaShip 1/12, skillfulShip 1/18, soloShip 1/0, suntShip 2/0, yanShip 1/33",22,33,11,114]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"round": 13,
|
"round": 13,
|
||||||
"h_0": [1,27,12,0,0,6,8,"e1220022",484,"arcaneShip 0/2, indomShip 1/28, kazimierzShip 0/8, kjeragShip 0/8, maniShip 1/0, preciShip 1/161, sargonShip 1/193, steadShip 2/59, suntShip 1/0, swiftShip 0/40, victoriaShip 1/43, visiShip 0/6",50,50,0,130],
|
"h_0": [1,27,16,0,2,6,8,"dc6467d8",466,"arcaneShip 0/2, indomShip 0/18, preciShip 2/186, sargonShip 1/189, siracusaShip 0/6, steadShip 0/48, suntShip 1/0, swiftShip 1/21, victoriaShip 0/21, visiShip 1/70",50,50,0,140],
|
||||||
"ai_0": [1,28,12,0,0,6,8,"a2f4a969",302,"preciShip 1/134, sargonShip 1/131, skillfulShip 1/3, soloShip 1/0, suntShip 1/0, victoriaShip 0/5, visiShip 1/0, yanShip 1/34",50,50,0,127]
|
"ai_0": [1,28,12,0,0,6,8,"a47a6f63",87,"kazimierzShip 0/2, preciShip 2/24, sargonShip 0/8, siracusaShip 0/12, skillfulShip 1/24, soloShip 1/0, suntShip 2/0, swiftShip 0/2, yanShip 1/39",44,50,6,126]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"players": {
|
"players": {
|
||||||
@@ -1704,12 +1700,12 @@
|
|||||||
"eliminatedRound": null,
|
"eliminatedRound": null,
|
||||||
"lp": 27,
|
"lp": 27,
|
||||||
"bandId": "band_yu",
|
"bandId": "band_yu",
|
||||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||||
"trophies": 4,
|
"trophies": 4,
|
||||||
"reward": 400,
|
"reward": 400,
|
||||||
"stats": {"activatedLayers":496,"bossDamage":678256,"dmgDealt":4102418,"gold":141,"itemsEquipped":24,"kills":319,"leaks":0,"lpLost":0,"merges":2,"perfectRounds":13,"refreshes":19},
|
"stats": {"activatedLayers":492,"bossDamage":1089764,"dmgDealt":4817588,"gold":156,"itemsEquipped":24,"kills":325,"leaks":2,"lpLost":0,"merges":4,"perfectRounds":11,"refreshes":32},
|
||||||
"lineup": ["4_20_b@11,8[3_05_e_a+5_03_e_a]","6_11_a@10,4[6_05_e_a+4_06_e_a]","5_02_a@10,6[5_01_e_a+6_01_e_a]","6_07_a@10,8[2_04_e_a+6_09_e_a]","3_16_a@9,4[4_05_e_a+3_09_e_a]","2_08_b@9,5[3_11_e_a+3_07_e_a]","6_03_a@9,6[4_04_e_b+2_01_e_a]","4_07_a@9,9[4_11_e_a+4_02_e_a]"],
|
"lineup": ["6_05_a@12,8[3_04_e_a+6_03_e_a]","4_14_a@11,6","5_21_b@11,7[4_03_e_a+5_01_e_b]","4_20_b@11,8[1_01_e_b]","6_05_a@10,6[4_08_e_a+2_01_e_b]","5_02_a@9,7[6_01_e_a+3_11_e_a]","4_07_b@9,8[4_04_e_b+4_11_e_b]","4_23_b@9,9[4_06_e_a+4_04_e_a]"],
|
||||||
"bonds": ["arcaneShip 0/2","indomShip 1/28","kazimierzShip 0/8","kjeragShip 0/8","maniShip 1/0","preciShip 1/161","sargonShip 1/195","steadShip 2/63","suntShip 1/0","swiftShip 0/40","victoriaShip 1/49","visiShip 0/6"]
|
"bonds": ["arcaneShip 0/2","indomShip 0/18","preciShip 2/191","sargonShip 1/189","siracusaShip 0/6","steadShip 0/48","suntShip 1/0","swiftShip 1/26","victoriaShip 0/21","visiShip 1/86"]
|
||||||
},
|
},
|
||||||
"ai_0": {
|
"ai_0": {
|
||||||
"alive": true,
|
"alive": true,
|
||||||
@@ -1718,12 +1714,12 @@
|
|||||||
"eliminatedRound": null,
|
"eliminatedRound": null,
|
||||||
"lp": 28,
|
"lp": 28,
|
||||||
"bandId": "band_mlyss",
|
"bandId": "band_mlyss",
|
||||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||||
"trophies": 4,
|
"trophies": 4,
|
||||||
"reward": 400,
|
"reward": 400,
|
||||||
"stats": {"activatedLayers":316,"bossDamage":896744,"dmgDealt":3970823,"gold":140,"itemsEquipped":19,"kills":279,"leaks":10,"lpLost":0,"merges":4,"perfectRounds":10,"refreshes":21},
|
"stats": {"activatedLayers":115,"bossDamage":485236,"dmgDealt":3388651,"gold":140,"itemsEquipped":20,"kills":273,"leaks":18,"lpLost":0,"merges":6,"perfectRounds":10,"refreshes":11},
|
||||||
"lineup": ["5_02_a@12,8[3_01_e_a+5_01_e_a]","2_04_b@11,7[2_04_e_a+3_08_e_a]","5_03_a@11,8","3_13_a@10,5","6_08_a@10,6","4_04_b@10,8[4_02_e_a+4_08_e_a]","3_03_a@9,8[4_06_e_b]","6_06_a@9,9[4_10_e_a+4_07_e_a]"],
|
"lineup": ["4_14_a@12,8","2_04_b@11,7[5_03_e_a]","1_03_b@11,8","5_02_a@10,5","6_05_a@10,6[3_03_e_a]","3_03_b@10,8[3_11_e_b+2_04_e_b]","1_08_b@9,8[1_02_e_a+4_06_e_a]","3_02_b@9,9"],
|
||||||
"bonds": ["preciShip 1/134","sargonShip 1/131","skillfulShip 1/3","suntShip 1/0","victoriaShip 0/5","visiShip 1/10","yanShip 1/38"]
|
"bonds": ["kazimierzShip 0/2","preciShip 2/29","sargonShip 0/8","siracusaShip 0/12","skillfulShip 1/33","soloShip 1/0","suntShip 2/0","swiftShip 1/7","yanShip 1/46"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"diy": {
|
"diy": {
|
||||||
@@ -1749,7 +1745,7 @@
|
|||||||
[12,"",""],
|
[12,"",""],
|
||||||
[13,"",""]
|
[13,"",""]
|
||||||
],
|
],
|
||||||
"stock": {"chess_char_5_diy1_a":7,"chess_char_5_diy2_a":8,"chess_char_6_diy1_a":5,"chess_char_6_diy2_a":5}
|
"stock": {"chess_char_5_diy1_a":8,"chess_char_5_diy2_a":8,"chess_char_6_diy1_a":5,"chess_char_6_diy2_a":5}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,45 @@ test('bounty pick: the card the own board can beat, never one it cannot (even wh
|
|||||||
assert.equal(botPickCard(m, ps, [hard, easy, huge].map((c, idx) => ({ ...c, idx })), [0, 1, 2]), 1);
|
assert.equal(botPickCard(m, ps, [hard, easy, huge].map((c, idx) => ({ ...c, idx })), [0, 1, 2]), 1);
|
||||||
ps.lp = 2;
|
ps.lp = 2;
|
||||||
assert.equal(botPickCard(m, ps, [hard, huge, easy].map((c, idx) => ({ ...c, idx })), [0, 1, 2]), 2, 'low LP: still the beatable one');
|
assert.equal(botPickCard(m, ps, [hard, huge, easy].map((c, idx) => ({ ...c, idx })), [0, 1, 2]), 2, 'low LP: still the beatable one');
|
||||||
|
const raw = [hard, easy].map((c) => DATA.choices.cards.bounty.find((x) => x.effectId === c.id));
|
||||||
|
assert.deepEqual(m.offerBountyChoice(ps, raw, 'chess_item_6_03_m'), { ok: true });
|
||||||
|
assert.deepEqual(m.autoPickPersonalChoice(ps, 'bot'), { ok: true });
|
||||||
|
assert.equal(ps.personalChoice, null);
|
||||||
|
assert.equal(ps.bounties.at(-1).card.enemyKey, easy.enemyKey, 'personal cards use bounty scoring too');
|
||||||
|
const metaState = m.rngMeta.state(), botState = m.rngBots.state();
|
||||||
|
m.autoPickPersonalChoice(ps, 'bot');
|
||||||
|
m.autoPickPersonalChoice(ps, 'random');
|
||||||
|
assert.equal(m.rngMeta.state(), metaState);
|
||||||
|
assert.equal(m.rngBots.state(), botState, 'nothing pending: no RNG draws');
|
||||||
m.dispose();
|
m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a bot\'s 教鞭 pick is deterministic: one seed offers the same three cards and picks the same one, drawing only on the bots\' rng', () => {
|
||||||
|
const runs = [];
|
||||||
|
for (let k = 0; k < 2; k++) {
|
||||||
|
const h = soloBot({ seed: 5, difficulty: 'HARD' }).start();
|
||||||
|
const m = h.m;
|
||||||
|
const ps = m.order[0];
|
||||||
|
h.run(() => m.phase === PHASE.PREP && m.round === 1);
|
||||||
|
ps.lp = 999;
|
||||||
|
h.run(() => m.phase === PHASE.PREP && m.round === 5);
|
||||||
|
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
const offered = ps.personalChoice.cards.map((c) => c.effectId);
|
||||||
|
assert.equal(offered.length, 3);
|
||||||
|
assert.equal(new Set(offered).size, 3, 'three different cards');
|
||||||
|
const meta = m.rngMeta.state(), bots = m.rngBots.state();
|
||||||
|
assert.deepEqual(m.autoPickPersonalChoice(ps, 'bot'), { ok: true });
|
||||||
|
assert.equal(m.rngMeta.state(), meta, 'the pick leaves the meta rng alone');
|
||||||
|
assert.notEqual(m.rngBots.state(), bots, 'one scoring draw per card from the bots\' rng');
|
||||||
|
assert.equal(ps.personalChoice, null);
|
||||||
|
runs.push({ offered, picked: ps.bounties.at(-1).card.effectId, bots: m.rngBots.state() });
|
||||||
|
assert.ok(offered.includes(runs[k].picked));
|
||||||
|
m.dispose();
|
||||||
|
}
|
||||||
|
assert.deepEqual(runs[0], runs[1]);
|
||||||
|
});
|
||||||
|
|
||||||
test('bounty pick in real drafts (绝境 R3 悬赏决策): a card the board likely beats (p ≥ 0.5) whenever a near-sure one (p ≥ 0.9) is offered', () => {
|
test('bounty pick in real drafts (绝境 R3 悬赏决策): a card the board likely beats (p ≥ 0.5) whenever a near-sure one (p ≥ 0.9) is offered', () => {
|
||||||
let drafts = 0;
|
let drafts = 0;
|
||||||
for (const seed of [1, 2, 3, 4]) {
|
for (const seed of [1, 2, 3, 4]) {
|
||||||
@@ -138,9 +174,12 @@ test('bounty Arts (教鞭): kept after a battle with leaks, used after a perfect
|
|||||||
assert.ok(owns(ps, WHIP), 'kept after a battle with leaks (not destroyed)');
|
assert.ok(owns(ps, WHIP), 'kept after a battle with leaks (not destroyed)');
|
||||||
h.run(() => m.phase === PHASE.PREP && m.round === 4);
|
h.run(() => m.phase === PHASE.PREP && m.round === 4);
|
||||||
m.lastResults.set(ps.playerId, clean);
|
m.lastResults.set(ps.playerId, clean);
|
||||||
|
giveItem(m, ps, WHIP);
|
||||||
const bounties = ps.bounties.length;
|
const bounties = ps.bounties.length;
|
||||||
h.run(() => m.phase === PHASE.COMBAT && m.round === 4);
|
h.run(() => m.phase === PHASE.COMBAT && m.round === 4);
|
||||||
assert.ok(!owns(ps, WHIP) && ps.bounties.length > bounties, 'used after a perfect battle');
|
assert.ok(!owns(ps, WHIP) && ps.bounties.length === bounties + 2, 'both Arts used and picked after a perfect battle');
|
||||||
|
assert.equal(ps.personalChoice, null);
|
||||||
|
assert.equal(ps.round.arts, 2);
|
||||||
assert.equal(m.errorCount, 0);
|
assert.equal(m.errorCount, 0);
|
||||||
m.dispose();
|
m.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-2
@@ -3,11 +3,62 @@ import { test } from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { PHASE } from '../../shared/constants.js';
|
import { PHASE } from '../../shared/constants.js';
|
||||||
import { fieldModel, planLayout, rehearse, rangeTiles, REHEARSAL_VARIANTS, LAYOUT_PARAMS, botPickCard, botPickBand } from '../../server/match/bot.js';
|
import { fieldModel, planLayout, rehearse, rangeTiles, REHEARSAL_VARIANTS, LAYOUT_PARAMS, botPickCard, botPickBand } from '../../server/match/bot.js';
|
||||||
import { FIELD, canPlace, placeClass, parseKey } from '../../server/match/board.js';
|
import { FIELD, canPlace, placeClass, parseKey, legalTiles, tileKey } from '../../server/match/board.js';
|
||||||
import { makeMatch, checkInvariants, give, DATA } from './harness.js';
|
import { makeMatch, checkInvariants, give, giveItem, DATA } from './harness.js';
|
||||||
|
import { makeBattle } from '../helpers/battleHarness.js';
|
||||||
|
|
||||||
const soloBot = (o = {}) => makeMatch({ mode: 'solo', difficulty: 'NORMAL', seats: [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }], ...o });
|
const soloBot = (o = {}) => makeMatch({ mode: 'solo', difficulty: 'NORMAL', seats: [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }], ...o });
|
||||||
|
|
||||||
|
test('user: Given a saturated layout, When placing a self-range defender, Then prefer a free enemy path tile', () => {
|
||||||
|
const h = makeMatch({ mode: 'solo', seed: 11 }).start().toPrep();
|
||||||
|
const m = h.m;
|
||||||
|
const ps = m.order[0];
|
||||||
|
try {
|
||||||
|
const pieces = ['chess_char_6_03_a', 'chess_char_5_11_a', 'chess_char_1_02_a',
|
||||||
|
'chess_char_1_10_a', 'chess_char_2_08_a', 'chess_char_3_16_a'].map((id) => give(m, ps, id, 'hand'));
|
||||||
|
const plan = planLayout(m, ps, pieces);
|
||||||
|
// act2autochess_m04, first wave: the ground route crosses these deployable tiles.
|
||||||
|
const road = new Set(['9,9', '9,8', '9,7', '9,6', '11,5', '11,4', '10,4', '9,4', '9,3']);
|
||||||
|
assert.equal(m.stageId, 'act2autochess_m04');
|
||||||
|
assert.ok(road.has(plan.get(pieces.at(-1).uid)), `Cuora must block the route, got ${plan.get(pieces.at(-1).uid)}`);
|
||||||
|
for (const [key, expectedToBlock] of [[plan.get(pieces.at(-1).uid), true], ['12,5', false]]) {
|
||||||
|
const [row, col] = parseKey(key);
|
||||||
|
const battle = makeBattle({ stageId: m.stageId, waveTemplate: 'act1autochess_01',
|
||||||
|
units: [{ chessId: 'chess_char_3_16_a', row, col }] });
|
||||||
|
battle.runToEnd();
|
||||||
|
assert.equal(battle.hooksOf('blocked').length > 0, expectedToBlock, `blocking at ${key}`);
|
||||||
|
assert.equal(battle.result().perPlayer.p1.damageDealt > 0, expectedToBlock, `damage at ${key}`);
|
||||||
|
}
|
||||||
|
// When every path position is occupied, a legal fallback remains available.
|
||||||
|
const fallback = planLayout(m, ps, [pieces.at(-1)], undefined, { occupied: road });
|
||||||
|
const key = fallback.get(pieces.at(-1).uid);
|
||||||
|
assert.ok(key && !road.has(key));
|
||||||
|
assert.equal(ps.deployMap().get(key), 'melee');
|
||||||
|
} finally { m.dispose(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('self-range blockers (range 0-1, only their own tile) fill the free enemy-road tiles before any other tile, on every stage and seed', () => {
|
||||||
|
// official range_table "0-1": the one grid (0,0) — 角峰 古米 泡泡 折桠 菲莱 蛇屠箱 塞雷娅 余
|
||||||
|
const guards = Object.values(DATA.chess).filter((c) => c && !c.isGolden && c.rangeGrid && c.rangeGrid.length === 1 && c.rangeGrid[0].every((v) => v === 0));
|
||||||
|
assert.equal(guards.length, 8);
|
||||||
|
const stages = new Set();
|
||||||
|
for (let seed = 1; seed <= 40; seed++) {
|
||||||
|
const h = makeMatch({ mode: 'solo', seed }).start().toPrep();
|
||||||
|
const m = h.m;
|
||||||
|
const ps = m.order[0];
|
||||||
|
try {
|
||||||
|
stages.add(m.stageId);
|
||||||
|
const pieces = guards.map((c) => give(m, ps, c.chessId, 'hand'));
|
||||||
|
const plan = planLayout(m, ps, pieces);
|
||||||
|
const model = fieldModel(m, ps);
|
||||||
|
const road = legalTiles(ps.deployMap(), 'melee').filter(([r, c]) => model.ground.has(tileKey(r, c)));
|
||||||
|
const onRoad = pieces.filter((p) => model.ground.has(plan.get(p.uid))).length;
|
||||||
|
assert.equal(onRoad, Math.min(pieces.length, road.length), `seed ${seed}, ${m.stageId}: ${onRoad} of ${pieces.length} guards on the ${road.length} road tiles`);
|
||||||
|
} finally { m.dispose(); }
|
||||||
|
}
|
||||||
|
assert.equal(stages.size, 8, 'every stage was tried');
|
||||||
|
});
|
||||||
|
|
||||||
test('field model: the round\'s routes traced over the own board, weighted by the enemies that use them', () => {
|
test('field model: the round\'s routes traced over the own board, weighted by the enemies that use them', () => {
|
||||||
const h = soloBot({ seed: 2 }).start();
|
const h = soloBot({ seed: 2 }).start();
|
||||||
const m = h.m;
|
const m = h.m;
|
||||||
@@ -222,6 +273,32 @@ test('a sliced rehearsal is dropped when the prep ends first; the default layout
|
|||||||
m.dispose();
|
m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('AI takeover resolves a personal choice in untimed PREP, including generator-error Ready cleanup', () => {
|
||||||
|
for (const broken of [false, true]) {
|
||||||
|
const h = makeMatch({ mode: 'coop', humans: 1, seed: 6, fake: true, botSliceMs: 0 }).start();
|
||||||
|
h.toPrep(1);
|
||||||
|
const m = h.m, ps = h.ps('p_0');
|
||||||
|
assert.equal(m.deadline, 0, 'single-human co-op is untimed');
|
||||||
|
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
const pick = m.autoPickPersonalChoice.bind(m), modes = [];
|
||||||
|
m.autoPickPersonalChoice = (p, mode) => {
|
||||||
|
if (p.personalChoice) modes.push(mode);
|
||||||
|
if (broken && mode === 'bot') throw new Error('scripted choice scoring failure');
|
||||||
|
return pick(p, mode);
|
||||||
|
};
|
||||||
|
assert.deepEqual(m.handle(ps.playerId, { t: 'g.autoplay', on: true }), { ok: true });
|
||||||
|
assert.ok(h.run(() => m.phase !== PHASE.PREP));
|
||||||
|
assert.equal(ps.personalChoice, null);
|
||||||
|
assert.equal(ps.bounties.length, 1);
|
||||||
|
assert.equal(ps.round.arts, 1);
|
||||||
|
assert.ok(ps.ready);
|
||||||
|
assert.ok(modes.includes(broken ? 'random' : 'bot'));
|
||||||
|
assert.equal(m.errorCount > 0, broken);
|
||||||
|
m.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('economy: the bot fills the board first (8 units by round 4), levels on its curve and keeps a hand slot free', () => {
|
test('economy: the bot fills the board first (8 units by round 4), levels on its curve and keeps a hand slot free', () => {
|
||||||
for (const seed of [5, 8]) {
|
for (const seed of [5, 8]) {
|
||||||
const h = soloBot({ seed }).start();
|
const h = soloBot({ seed }).start();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { PHASE } from '../../shared/constants.js';
|
|||||||
import { validateC2S, isBattleResult } from '../../shared/protocol.js';
|
import { validateC2S, isBattleResult } from '../../shared/protocol.js';
|
||||||
import { buildBattleSpec, createBattleFromSpec, resultDigest, compactResult, LocalBossPool, jsonClone } from '../../server/sim/spec.js';
|
import { buildBattleSpec, createBattleFromSpec, resultDigest, compactResult, LocalBossPool, jsonClone } from '../../server/sim/spec.js';
|
||||||
import { DataSource } from '../../server/sim/simdata.js';
|
import { DataSource } from '../../server/sim/simdata.js';
|
||||||
import { validateClientResult, specBounds } from '../../server/match/fields.js';
|
import { validateClientResult, specBounds, syntheticResult } from '../../server/match/fields.js';
|
||||||
import { CreditPool, SharedBossPool } from '../../server/match/finalAssault.js';
|
import { CreditPool, SharedBossPool } from '../../server/match/finalAssault.js';
|
||||||
import { FakeBattle } from './fakeBattle.js';
|
import { FakeBattle } from './fakeBattle.js';
|
||||||
import { DATA, makeMatch, checkInvariants, give, chessOfTier } from './harness.js';
|
import { DATA, makeMatch, checkInvariants, give, chessOfTier } from './harness.js';
|
||||||
@@ -516,6 +516,10 @@ test('Final Assault: an early boss b.result (forced at t≈0) does not end the p
|
|||||||
|
|
||||||
test('Final Assault: a 999-layer kill faster than the budget — the \'cleared\' b.result whose report covers the pool waits for the budget, no takeover (QA 6b)', () => {
|
test('Final Assault: a 999-layer kill faster than the budget — the \'cleared\' b.result whose report covers the pool waits for the budget, no takeover (QA 6b)', () => {
|
||||||
const { h, m, f } = faWithForger(9122);
|
const { h, m, f } = faWithForger(9122);
|
||||||
|
const ps = h.ps('p_0');
|
||||||
|
const training = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey));
|
||||||
|
m.addBounty(ps, { ...training, rounds: 2, multiRound: false });
|
||||||
|
const pending = ps.pendingFunds;
|
||||||
const max = m.bossPool.maxHp;
|
const max = m.bossPool.maxHp;
|
||||||
h.sched.advance(500); // 1 game s on the field clock: the budget credits ≤ 20 % of the pool
|
h.sched.advance(500); // 1 game s on the field clock: the budget credits ≤ 20 % of the pool
|
||||||
const gt = m._fieldElapsed(f);
|
const gt = m._fieldElapsed(f);
|
||||||
@@ -544,10 +548,141 @@ test('Final Assault: a 999-layer kill faster than the budget — the \'cleared\'
|
|||||||
assert.equal(f.resultSource, 'client', 'the held client result completed the field');
|
assert.equal(f.resultSource, 'client', 'the held client result completed the field');
|
||||||
assert.equal(m.verifyStats.takeovers, takeovers0);
|
assert.equal(m.verifyStats.takeovers, takeovers0);
|
||||||
assert.equal(m.verifyStats.rejected, rejected0);
|
assert.equal(m.verifyStats.rejected, rejected0);
|
||||||
|
assert.equal(ps.pendingFunds, pending + training.coin, 'validated held client result earns the server card amount');
|
||||||
|
assert.equal(ps.bounties[0].roundsLeft, 1);
|
||||||
|
assert.deepEqual(m.handle('p_0', { t: 'b.result', battleId: f.battleId, result: res }), { ok: true });
|
||||||
|
assert.equal(ps.pendingFunds, pending + training.coin, 'duplicate accepted result does not pay twice');
|
||||||
|
assert.equal(ps.bounties[0].roundsLeft, 1);
|
||||||
for (const pid of f.players) assert.ok(Math.abs((f.bossBy[pid] || 0) - Math.min(by[pid], max)) <= max * 1e-9 + 5, `${pid} credited as reported (${f.bossBy[pid]} vs ${by[pid]})`);
|
for (const pid of f.players) assert.ok(Math.abs((f.bossBy[pid] || 0) - Math.min(by[pid], max)) <= max * 1e-9 + 5, `${pid} credited as reported (${f.bossBy[pid]} vs ${by[pid]})`);
|
||||||
m.dispose();
|
m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('boss bounty eligibility: forced real results on a team victory qualify; synthetic, leaked and team defeat do not', () => {
|
||||||
|
for (const [ending, synthetic, perfect, leaked, eligible] of [
|
||||||
|
['cleared', false, true, [], true],
|
||||||
|
['cleared', true, true, [], false],
|
||||||
|
['cleared', false, false, [], false],
|
||||||
|
['cleared', false, true, [{ counted: true }], false],
|
||||||
|
['cleared', false, true, [{ counted: false }], true],
|
||||||
|
['forced', false, true, [], false],
|
||||||
|
]) {
|
||||||
|
const { m, f } = faWithForger(9122);
|
||||||
|
const ps = m.players.get(f.players[0]);
|
||||||
|
const card = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey));
|
||||||
|
m.addBounty(ps, { ...card, rounds: 2, multiRound: false });
|
||||||
|
m.addBounty(ps, { ...card, rounds: 2, multiRound: false });
|
||||||
|
const res = syntheticResult(f.players);
|
||||||
|
if (!synthetic) delete res.synthetic;
|
||||||
|
res.reason = 'forced';
|
||||||
|
Object.assign(res.perPlayer[ps.playerId], { perfect, leaked, coins: 3 });
|
||||||
|
const pending = ps.pendingFunds, gained = ps.stats.fundsGained;
|
||||||
|
m._finalEnding = ending;
|
||||||
|
m.bossPool.hp = 0; // even a late clearing report cannot override an already-registered team defeat
|
||||||
|
m._finishFinal(false, () => res);
|
||||||
|
assert.equal(ps.pendingFunds, pending + 3 + (eligible ? 2 * card.coin : 0));
|
||||||
|
assert.equal(ps.stats.fundsGained, gained + 3 + (eligible ? 2 * card.coin : 0));
|
||||||
|
assert.deepEqual(ps.bounties.map((b) => b.roundsLeft), [1, 1]);
|
||||||
|
m.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- a perfect-payout bounty and what the authority reported (a modified client could tell the two halves apart) -------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Final Assault pair field played by hand (the SimClient of p_0, the authority, is muted): both players hold the same
|
||||||
|
* perfect-payout card; the authority sends one b.progress — `leaks` (the field's LP cost) and `leaksBy` (its per-player
|
||||||
|
* split, when given) — then a 'cleared' b.result whose leaked lists hold `leaked[pid]` counted leaks (lpr 1); the fight
|
||||||
|
* is driven to its end. Returns the funds each player was paid (kill coins: none) and what the server charged.
|
||||||
|
*/
|
||||||
|
function bossPaid(seed, { leaks = 0, leaksBy = null, leaked = {} }) {
|
||||||
|
const { h, m, f } = faWithForger(seed);
|
||||||
|
const card = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey));
|
||||||
|
const ps = f.players.map((pid) => h.ps(pid));
|
||||||
|
for (const p of ps) m.addBounty(p, { ...card, rounds: 2, multiRound: false });
|
||||||
|
const before = ps.map((p) => p.pendingFunds);
|
||||||
|
const lp0 = m.teamLp;
|
||||||
|
const max = m.bossPool.maxHp;
|
||||||
|
h.sched.advance(500);
|
||||||
|
const gt = m._fieldElapsed(f);
|
||||||
|
const by = { [f.players[0]]: max * 0.6, [f.players[1]]: max * 0.4 + 5 };
|
||||||
|
const progress = { t: 'b.progress', battleId: f.battleId, gt, killed: 0, total: 0, bossDmg: max + 5, by, leaks, ...(leaksBy ? { leaksBy } : {}) };
|
||||||
|
assert.equal(validateC2S(progress), null);
|
||||||
|
assert.deepEqual(m.handle('p_0', progress), { ok: true });
|
||||||
|
const entry = (pid) => ({ enemyKey: card.enemyKey, mods: null, lpr: 1, sourcePlayerId: pid, tag: null, counted: true, spawned: true });
|
||||||
|
const res = { reason: 'cleared', time: gt, killed: 0, total: 0, errors: 0,
|
||||||
|
perPlayer: Object.fromEntries(f.players.map((pid) => {
|
||||||
|
const list = Array.from({ length: leaked[pid] || 0 }, () => entry(pid));
|
||||||
|
return [pid, { killed: 0, total: 0, leaked: list, perfect: list.length === 0, layerGains: {}, coins: 0, damageDealt: by[pid], bossDamage: by[pid], healingDone: 0, deaths: 0, unitsEnd: [], unitStats: [] }];
|
||||||
|
})) };
|
||||||
|
assert.equal(isBattleResult(res), true);
|
||||||
|
assert.deepEqual(m.handle('p_0', { t: 'b.result', battleId: f.battleId, result: res }), { ok: true });
|
||||||
|
h.drive(() => m._finalEnding != null || m.phase !== PHASE.FINAL_ASSAULT, { maxSteps: 2e5 });
|
||||||
|
const out = { ending: m._finalEnding, source: f.resultSource, coin: card.coin, charged: lp0 - m.teamLp, paid: ps.map((p, i) => p.pendingFunds - before[i]), left: ps.map((p) => p.bounties.map((b) => b.roundsLeft)) };
|
||||||
|
m.dispose();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Final Assault, a modified authority: LP charged by b.progress while both leaked lists are empty pays no perfect-payout card', () => {
|
||||||
|
const r = bossPaid(9124, { leaks: 1, leaked: {} });
|
||||||
|
assert.equal(r.ending, 'cleared');
|
||||||
|
assert.equal(r.source, 'client');
|
||||||
|
assert.equal(r.charged, 1, 'the server charged the team leak');
|
||||||
|
assert.deepEqual(r.paid, [0, 0], 'a result that shows no leak after a charged one is not believed');
|
||||||
|
assert.deepEqual(r.left, [[1], [1]], 'the boss battle still used one of the cards\' battles');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Final Assault, a modified authority: one seat\'s reported leak cannot be moved to the other — nobody is paid', () => {
|
||||||
|
// progress: the leak is p_0's; the result pins it on p_1 so that p_0 reads perfect (and p_1 cannot be paid anyway)
|
||||||
|
const r = bossPaid(9125, { leaks: 1, leaksBy: { p_0: 1, p_1: 0 }, leaked: { p_1: 1 } });
|
||||||
|
assert.equal(r.ending, 'cleared');
|
||||||
|
assert.equal(r.charged, 1);
|
||||||
|
assert.deepEqual(r.paid, [0, 0]);
|
||||||
|
// …and with no split at all the result decides nothing either: the same leak pinned on p_1
|
||||||
|
assert.deepEqual(bossPaid(9126, { leaks: 1, leaked: { p_1: 1 } }).paid, [0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Final Assault, honest authority: the seat whose half let nothing through is paid, the leaker is not; a leader\'s own LP effect costs nobody the card', () => {
|
||||||
|
const leak = bossPaid(9127, { leaks: 1, leaksBy: { p_0: 0, p_1: 1 }, leaked: { p_1: 1 } });
|
||||||
|
assert.equal(leak.charged, 1);
|
||||||
|
assert.deepEqual(leak.paid, [leak.coin, 0]);
|
||||||
|
// 2 LP of leader "扣除目标生命" effects (no leaked enemy): the report says so, the result agrees — both halves are perfect
|
||||||
|
const fx = bossPaid(9128, { leaks: 2, leaksBy: { p_0: 0, p_1: 0 }, leaked: {} });
|
||||||
|
assert.equal(fx.charged, 2);
|
||||||
|
assert.deepEqual(fx.paid, [fx.coin, fx.coin]);
|
||||||
|
// nothing charged, nothing leaked: as before
|
||||||
|
assert.deepEqual(bossPaid(9129, { leaks: 0, leaked: {} }).paid.map((n, i, a) => n === a[0]), [true, true]);
|
||||||
|
assert.ok(bossPaid(9130, { leaks: 0, leaked: {} }).paid.every((n) => n > 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Final Assault, honest clients (client-side combat): a leak on one half costs that seat its perfect-payout card, not its partner; the leader\'s own LP effect costs nobody', () => {
|
||||||
|
const h = makeMatch({ mode: 'coop', difficulty: 'FUNNY', humans: 2, seed: 9132, fake: true, clientCombat: true, instant: false,
|
||||||
|
script: (b) => (b.kind === 'boss' ? { bossDps: 20000, leaks: { p_1: 1 }, leakEvents: [{ at: 1, lpr: 1 }], lpLossEvents: [{ at: 1.5, amount: 2 }] } : { duration: 2 }) }).start();
|
||||||
|
const m = h.m;
|
||||||
|
h.autoHumans();
|
||||||
|
h.drive(() => m.phase === PHASE.FINAL_ASSAULT);
|
||||||
|
const card = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey));
|
||||||
|
const [p0, p1] = [h.ps('p_0'), h.ps('p_1')];
|
||||||
|
for (const p of [p0, p1]) m.addBounty(p, { ...card, rounds: 2, multiRound: false });
|
||||||
|
const [a, b] = [p0.pendingFunds, p1.pendingFunds];
|
||||||
|
const end = h.runToEnd();
|
||||||
|
assert.equal(end.victory, true);
|
||||||
|
const f = fields(h)[0];
|
||||||
|
assert.equal(f.resultSource, 'client');
|
||||||
|
assert.equal(f.lpReported, 3, 'leak lpr 1 + leader effect 2');
|
||||||
|
assert.deepEqual(f.leaksBy, { p_1: 1 }, 'the authority split it: p_1\'s one leak (the other 2 LP are the leader\'s effect), nothing on p_0');
|
||||||
|
assert.equal(p0.pendingFunds - a, card.coin, 'the seat that let nothing through is paid');
|
||||||
|
assert.equal(p1.pendingFunds - b, 0, 'the seat that leaked is not');
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('b.progress carries `leaksBy`, the per-player split of a boss field\'s leak LP (shared/protocol.js)', () => {
|
||||||
|
const base = { t: 'b.progress', battleId: 'a.1.1.b1', gt: 3, killed: 0, total: 0, leaks: 2, bossDmg: 10, by: { p_0: 10 } };
|
||||||
|
assert.equal(validateC2S({ ...base, leaksBy: { p_0: 1, p_1: 0.5 } }), null);
|
||||||
|
assert.equal(validateC2S({ ...base, leaksBy: {} }), null);
|
||||||
|
assert.equal(validateC2S(base), null, 'optional: a client that never sends it still validates');
|
||||||
|
for (const bad of [{ p_0: -1 }, { p_0: 'x' }, { p_0: 1e7 }, [1, 2], 'p_0', null, { 'a b': 1 }]) assert.notEqual(validateC2S({ ...base, leaksBy: bad }), null, JSON.stringify(bad));
|
||||||
|
});
|
||||||
|
|
||||||
test('Final Assault: a \'cleared\' b.result whose report does NOT cover the pool is still handed over (the partner takes the field)', () => {
|
test('Final Assault: a \'cleared\' b.result whose report does NOT cover the pool is still handed over (the partner takes the field)', () => {
|
||||||
const { h, m, f } = faWithForger(9123);
|
const { h, m, f } = faWithForger(9123);
|
||||||
const max = m.bossPool.maxHp;
|
const max = m.bossPool.maxHp;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { PHASE, ERR } from '../../shared/constants.js';
|
import { PHASE, ERR } from '../../shared/constants.js';
|
||||||
import { Match } from '../../server/match/Match.js';
|
import { Match } from '../../server/match/Match.js';
|
||||||
import { StubMatch } from '../../server/match/StubMatch.js';
|
import { StubMatch } from '../../server/match/StubMatch.js';
|
||||||
import { DATA, makeMatch, checkInvariants, give, chessOfTier } from './harness.js';
|
import { DATA, makeMatch, checkInvariants, give, giveItem, chessOfTier } from './harness.js';
|
||||||
import { bossPoolHp } from '../../server/match/finalAssault.js';
|
import { bossPoolHp } from '../../server/match/finalAssault.js';
|
||||||
|
|
||||||
const STATUSES = ['acting', 'ready', 'deciding', 'combat', 'done', 'helping', 'left', 'dead'];
|
const STATUSES = ['acting', 'ready', 'deciding', 'combat', 'done', 'helping', 'left', 'dead'];
|
||||||
@@ -51,6 +51,7 @@ test('m.private shape in PREP matches DESIGN §8.3 (pieces, slots, bonds, effect
|
|||||||
assert.ok(s.kind === 'chess' || s.kind === 'item');
|
assert.ok(s.kind === 'chess' || s.kind === 'item');
|
||||||
}
|
}
|
||||||
assert.equal(priv.shop.rewardOffer, null);
|
assert.equal(priv.shop.rewardOffer, null);
|
||||||
|
assert.equal(priv.personalChoice, null);
|
||||||
assert.equal(priv.deployCap, 8);
|
assert.equal(priv.deployCap, 8);
|
||||||
assert.ok(priv.effects.some((e) => e.iconKind === 'band' && e.name));
|
assert.ok(priv.effects.some((e) => e.iconKind === 'band' && e.name));
|
||||||
assert.ok(priv.nextEnemies.length > 0);
|
assert.ok(priv.nextEnemies.length > 0);
|
||||||
@@ -109,6 +110,45 @@ test('disconnect: the seat keeps playing; draft turns / prep auto-resolve at dea
|
|||||||
m.dispose();
|
m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('教鞭: simultaneous private choices survive reconnect; foreign, duplicate and old IDs cannot add bounties', () => {
|
||||||
|
const h = makeMatch({ mode: 'coop', humans: 2, spectators: ['watcher'], seed: 63, fake: true }).start();
|
||||||
|
h.toPrep(1);
|
||||||
|
const m = h.m, a = h.ps('p_0'), b = h.ps('p_1');
|
||||||
|
for (const ps of [a, b]) {
|
||||||
|
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
}
|
||||||
|
const aChoice = a.privateView().personalChoice, bChoice = b.privateView().personalChoice;
|
||||||
|
assert.notEqual(aChoice.id, bChoice.id);
|
||||||
|
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 0, choiceId: bChoice.id }).error, ERR.BAD_TARGET);
|
||||||
|
assert.equal(a.bounties.length + b.bounties.length, 0);
|
||||||
|
m.onDisconnect('p_0');
|
||||||
|
m.onReconnect('p_0');
|
||||||
|
assert.deepEqual(h.lastTo('p_0', 'm.private').personalChoice, aChoice);
|
||||||
|
m.flush(true);
|
||||||
|
assert.deepEqual(h.lastTo('p_1', 'm.private').personalChoice, bChoice);
|
||||||
|
assert.ok(!h.allTo('watcher', 'm.private').length);
|
||||||
|
assert.ok(!JSON.stringify(m.publicView()).includes(aChoice.id));
|
||||||
|
assert.ok(!JSON.stringify(m.prepFieldMeta(a)).includes(aChoice.id));
|
||||||
|
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: aChoice.id }), { ok: true });
|
||||||
|
m.flush(true);
|
||||||
|
assert.equal(h.lastTo('p_0', 'm.private').personalChoice, null);
|
||||||
|
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 1, choiceId: aChoice.id }).error, ERR.BAD_TARGET);
|
||||||
|
const next = giveItem(m, a, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(a.useArt(next.uid, 10, 5), { ok: true });
|
||||||
|
const nextId = a.personalChoice.id;
|
||||||
|
assert.notEqual(nextId, aChoice.id);
|
||||||
|
assert.equal(m.handle('p_0', { t: 'g.choice', idx: 0, choiceId: aChoice.id }).error, ERR.BAD_TARGET);
|
||||||
|
assert.equal(a.personalChoice.id, nextId);
|
||||||
|
assert.equal(a.bounties.length, 1);
|
||||||
|
m.onDisconnect('p_1');
|
||||||
|
h.sched.advance(m.deadline - h.sched.now() + 1);
|
||||||
|
m.onReconnect('p_1');
|
||||||
|
assert.equal(h.lastTo('p_1', 'm.private').personalChoice, null);
|
||||||
|
assert.equal(b.bounties.length, 1, 'offline choice picked at the existing PREP deadline');
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('g.autoplay lets the bot play a human seat (buys, places, readies); turning it off returns control', () => {
|
test('g.autoplay lets the bot play a human seat (buys, places, readies); turning it off returns control', () => {
|
||||||
const h = makeMatch({ mode: 'solo', seed: 64, fake: true }).start();
|
const h = makeMatch({ mode: 'solo', seed: 64, fake: true }).start();
|
||||||
const m = h.m;
|
const m = h.m;
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ test('机变 (co-op): 6 shared cards, random order, 30 s first / 16 s others, ti
|
|||||||
assert.deepEqual(sp.order.slice().sort(), ['p_0', 'p_1', 'p_2']);
|
assert.deepEqual(sp.order.slice().sort(), ['p_0', 'p_1', 'p_2']);
|
||||||
assert.equal(Math.round((m2.deadline - h2.sched.now()) / 1000), 30, 'first picker 30 s');
|
assert.equal(Math.round((m2.deadline - h2.sched.now()) / 1000), 30, 'first picker 30 s');
|
||||||
const [a, b, c] = sp.order;
|
const [a, b, c] = sp.order;
|
||||||
|
assert.equal(m2.handle(a, { t: 'g.choice', idx: 2, choiceId: 'old.choice.1' }).error, ERR.WRONG_PHASE);
|
||||||
|
assert.deepEqual(m2.sp.picks, {}, 'personal IDs never fall through to the global draft');
|
||||||
assert.equal(m2.handle(b, { t: 'g.choice', idx: 0 }).error, ERR.NOT_YOUR_TURN);
|
assert.equal(m2.handle(b, { t: 'g.choice', idx: 0 }).error, ERR.NOT_YOUR_TURN);
|
||||||
assert.deepEqual(m2.handle(a, { t: 'g.choice', idx: 2 }), { ok: true });
|
assert.deepEqual(m2.handle(a, { t: 'g.choice', idx: 2 }), { ok: true });
|
||||||
assert.equal(m2.handle(a, { t: 'g.choice', idx: 3 }).error, ERR.ALREADY);
|
assert.equal(m2.handle(a, { t: 'g.choice', idx: 3 }).error, ERR.ALREADY);
|
||||||
|
|||||||
@@ -214,6 +214,15 @@ test('ready gating: temp must be empty (a freed hand slot pulls the temp piece i
|
|||||||
assert.equal(a.hand[3], t);
|
assert.equal(a.hand[3], t);
|
||||||
assert.ok(a.tempEmpty);
|
assert.ok(a.tempEmpty);
|
||||||
assert.equal(a.privateView().canReady, true);
|
assert.equal(a.privateView().canReady, true);
|
||||||
|
const art = giveItem(m, a, 'chess_item_6_03_m', 'temp');
|
||||||
|
assert.deepEqual(a.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
assert.ok(a.tempEmpty);
|
||||||
|
assert.equal(a.privateView().canReady, false);
|
||||||
|
assert.deepEqual(a.setReady(true), { error: ERR.BAD_TARGET, detail: '请先完成教鞭选择' });
|
||||||
|
assert.deepEqual(a.setReady(false), { ok: true });
|
||||||
|
assert.deepEqual(m.handle('p_0', { t: 'g.choice', idx: 0, choiceId: a.personalChoice.id }), { ok: true });
|
||||||
|
assert.equal(a.privateView().canReady, true);
|
||||||
|
assert.equal(a.round.arts, 1);
|
||||||
assert.deepEqual(m.handle('p_0', { t: 'g.ready', ready: true }), { ok: true });
|
assert.deepEqual(m.handle('p_0', { t: 'g.ready', ready: true }), { ok: true });
|
||||||
assert.deepEqual(m.handle('p_0', { t: 'g.refresh' }), { error: ERR.WRONG_PHASE, detail: 'ready' });
|
assert.deepEqual(m.handle('p_0', { t: 'g.refresh' }), { error: ERR.WRONG_PHASE, detail: 'ready' });
|
||||||
assert.deepEqual(m.handle('p_0', { t: 'g.ready', ready: false }), { ok: true });
|
assert.deepEqual(m.handle('p_0', { t: 'g.ready', ready: false }), { ok: true });
|
||||||
@@ -235,13 +244,26 @@ test('prep deadline (co-op): unready players are auto-readied and their temp is
|
|||||||
const a = h.ps('p_0');
|
const a = h.ps('p_0');
|
||||||
const id = chessOfTier(1).find((x) => m.pool.has(x));
|
const id = chessOfTier(1).find((x) => m.pool.has(x));
|
||||||
const left = m.pool.left(id);
|
const left = m.pool.left(id);
|
||||||
|
const art = giveItem(m, a, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(a.useArt(art.uid, 10, 5), { ok: true });
|
||||||
fillHand(a);
|
fillHand(a);
|
||||||
give(m, a, id, 'temp');
|
give(m, a, id, 'temp');
|
||||||
assert.ok(!a.tempEmpty, 'a full hand: it waits in temp');
|
assert.ok(!a.tempEmpty, 'a full hand: it waits in temp');
|
||||||
|
const endPrep = m.endPrep.bind(m);
|
||||||
|
let checked = false;
|
||||||
|
m.endPrep = () => {
|
||||||
|
assert.equal(a.personalChoice, null, 'deadline picks before endPrep builds the battle');
|
||||||
|
assert.equal(a.bounties.length, 1);
|
||||||
|
assert.equal(a.round.arts, 1);
|
||||||
|
assert.ok(a.ready && a.tempEmpty);
|
||||||
|
checked = true;
|
||||||
|
endPrep();
|
||||||
|
};
|
||||||
assert.ok(m.deadline > h.sched.now(), 'co-op prep is timed');
|
assert.ok(m.deadline > h.sched.now(), 'co-op prep is timed');
|
||||||
assert.equal(Math.round((m.deadline - h.sched.now()) / 1000), m.gd.prepTime(1));
|
assert.equal(Math.round((m.deadline - h.sched.now()) / 1000), m.gd.prepTime(1));
|
||||||
h.sched.advance(m.deadline - h.sched.now() + 1);
|
h.sched.advance(m.deadline - h.sched.now() + 1);
|
||||||
assert.notEqual(m.phase, PHASE.PREP);
|
assert.notEqual(m.phase, PHASE.PREP);
|
||||||
|
assert.ok(checked);
|
||||||
assert.ok(a.tempEmpty);
|
assert.ok(a.tempEmpty);
|
||||||
assert.equal(m.pool.left(id), left, 'temp chess sold back to the pool');
|
assert.equal(m.pool.left(id), left, 'temp chess sold back to the pool');
|
||||||
checkInvariants(m);
|
checkInvariants(m);
|
||||||
@@ -252,7 +274,12 @@ test('solo prep and 机变 are untimed', () => {
|
|||||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 13 }).start();
|
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 13 }).start();
|
||||||
h.toPrep(1);
|
h.toPrep(1);
|
||||||
assert.equal(h.m.deadline, 0);
|
assert.equal(h.m.deadline, 0);
|
||||||
|
const ps = h.ps('p_0');
|
||||||
|
const art = giveItem(h.m, ps, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
const id = ps.personalChoice.id;
|
||||||
h.sched.advance(10 * 60 * 1000);
|
h.sched.advance(10 * 60 * 1000);
|
||||||
assert.equal(h.m.phase, PHASE.PREP);
|
assert.equal(h.m.phase, PHASE.PREP);
|
||||||
|
assert.equal(ps.personalChoice.id, id, 'no separate personal-choice timer');
|
||||||
h.m.dispose();
|
h.m.dispose();
|
||||||
});
|
});
|
||||||
@@ -11,7 +11,7 @@ import { fieldModel } from '../../server/match/bot.js';
|
|||||||
import { FIELD, parseKey } from '../../server/match/board.js';
|
import { FIELD, parseKey } from '../../server/match/board.js';
|
||||||
import { createRng } from '../../server/sim/rng.js';
|
import { createRng } from '../../server/sim/rng.js';
|
||||||
import { FakeBattle } from './fakeBattle.js';
|
import { FakeBattle } from './fakeBattle.js';
|
||||||
import { DATA, makeMatch, checkInvariants } from './harness.js';
|
import { DATA, makeMatch, checkInvariants, giveItem } from './harness.js';
|
||||||
|
|
||||||
const bossFields = () => FakeBattle.instances.filter((b) => b.kind === 'boss' || b.kind === 'hidden');
|
const bossFields = () => FakeBattle.instances.filter((b) => b.kind === 'boss' || b.kind === 'hidden');
|
||||||
|
|
||||||
@@ -56,6 +56,90 @@ test('multi-round bounties spawn in the Final Assault on the owner\'s half, show
|
|||||||
m.dispose();
|
m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A 战术特训 card pays its coins when the player's own battle is perfect (PRTS "若各自行动阶段就达成完美作战,获得N资金");
|
||||||
|
// the Final Assault and the Hidden Core are such battles — they pay it with the kill coins, at the next income.
|
||||||
|
test('perfect-payout bounties in the boss rounds: a 战术特训 card\'s coins join the kill coins at the next income (R14 → R15, then the Hidden Core)', () => {
|
||||||
|
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 2, seed: 61, fake: true,
|
||||||
|
script: (b) => (b.kind === 'boss' || b.kind === 'hidden' ? { bossDps: 1e9, coins: { p_0: 3 } } : {}) }).start();
|
||||||
|
const m = h.m;
|
||||||
|
h.toPrep(14);
|
||||||
|
const ps = h.ps('p_0');
|
||||||
|
const [card, second] = DATA.choices.cards.bounty.filter((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey) && !m.gd.inactiveEnemies.has(c.enemyKey));
|
||||||
|
m.addBounty(ps, card);
|
||||||
|
const pending = ps.pendingFunds, gained = ps.stats.fundsGained;
|
||||||
|
h.drive(() => m.phase === PHASE.FINAL_ASSAULT);
|
||||||
|
m.hiddenLayerSum = 1e6; // the Hidden Core opens
|
||||||
|
assert.ok(bossFields()[0].opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === card.enemyKey && s.ownerPlayerId === ps.playerId), 'the card\'s enemies are in the boss battle');
|
||||||
|
h.run(() => m.runner === null);
|
||||||
|
assert.equal(ps.pendingFunds, pending + 3 + card.coin, 'kill coins + one card amount, regardless of its enemy count');
|
||||||
|
assert.equal(ps.stats.fundsGained, gained + 3 + card.coin);
|
||||||
|
const left = bountyBattles(card);
|
||||||
|
assert.equal(ps.bounties.length, left > 1 ? 1 : 0, 'the boss battle used one of its battles');
|
||||||
|
if (left > 1) assert.equal(ps.bounties[0].roundsLeft, left - 1);
|
||||||
|
h.toPrep(15);
|
||||||
|
assert.equal(ps.pendingFunds, 0);
|
||||||
|
assert.equal(ps.funds, m.gd.income(15) + pending + 3 + card.coin, 'the base income is separate from the bounty funds');
|
||||||
|
m.addBounty(ps, second);
|
||||||
|
const owed = ps.bounties.filter((b) => b.card.payout === 'perfect').reduce((n, b) => n + b.card.coin, 0);
|
||||||
|
h.drive(() => m.phase === PHASE.HIDDEN_CORE);
|
||||||
|
assert.ok(bossFields().at(-1).opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === second.enemyKey), 'and in the Hidden Core');
|
||||||
|
h.runToEnd();
|
||||||
|
assert.equal(ps.pendingFunds, 3 + owed, 'the Hidden Core pays by the same rule');
|
||||||
|
assert.equal(ps.bounties.length, 0, 'every bounty used its battles');
|
||||||
|
checkInvariants(m);
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('教鞭 in the boss rounds (R14, R15): the card the owner chose spawns in the boss battle and pays at the next income, the Hidden Core takes another choice', () => {
|
||||||
|
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 2, seed: 61, fake: true,
|
||||||
|
script: (b) => (b.kind === 'boss' || b.kind === 'hidden' ? { bossDps: 1e9, coins: { p_0: 3 } } : {}) }).start();
|
||||||
|
const m = h.m;
|
||||||
|
h.toPrep(14);
|
||||||
|
const ps = h.ps('p_0');
|
||||||
|
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
assert.equal(ps.bounties.length, 0, 'nothing is applied before the owner confirms');
|
||||||
|
assert.equal(ps.setReady(true).error, 'BAD_TARGET', 'Ready waits for the choice');
|
||||||
|
const card = ps.personalChoice.cards[2];
|
||||||
|
assert.deepEqual(m.handle(ps.playerId, { t: 'g.choice', idx: 2, choiceId: ps.personalChoice.id }), { ok: true });
|
||||||
|
assert.equal(ps.bounties[0].card.effectId, card.effectId, 'the card of the tapped slot');
|
||||||
|
const pending = ps.pendingFunds;
|
||||||
|
h.drive(() => m.phase === PHASE.FINAL_ASSAULT);
|
||||||
|
m.hiddenLayerSum = 1e6;
|
||||||
|
assert.ok(bossFields()[0].opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === card.enemyKey && s.ownerPlayerId === ps.playerId), 'the chosen card\'s enemies come in the boss battle');
|
||||||
|
h.run(() => m.runner === null);
|
||||||
|
assert.equal(ps.pendingFunds, pending + 3 + card.coin, 'kill coins + the chosen card\'s coins');
|
||||||
|
h.toPrep(15);
|
||||||
|
assert.equal(ps.funds, m.gd.income(15) + pending + 3 + card.coin, 'paid at the R15 income, apart from the base income');
|
||||||
|
const hiddenArt = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(ps.useArt(hiddenArt.uid, 10, 5), { ok: true });
|
||||||
|
const hiddenCard = ps.personalChoice.cards[0];
|
||||||
|
assert.deepEqual(m.handle(ps.playerId, { t: 'g.choice', idx: 0, choiceId: ps.personalChoice.id }), { ok: true });
|
||||||
|
const owed = ps.bounties.filter((b) => b.card.payout === 'perfect').reduce((n, b) => n + b.card.coin, 0);
|
||||||
|
h.drive(() => m.phase === PHASE.HIDDEN_CORE);
|
||||||
|
assert.ok(bossFields().at(-1).opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === hiddenCard.enemyKey));
|
||||||
|
h.runToEnd();
|
||||||
|
assert.equal(ps.pendingFunds, 3 + owed, 'the Hidden Core pays by the same rule');
|
||||||
|
checkInvariants(m);
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('boss victory without a perfect own result keeps the kill coins but gives no perfect-card funds', () => {
|
||||||
|
const h = makeMatch({ mode: 'coop', humans: 2, seed: 61, fake: true,
|
||||||
|
script: (b) => (b.kind === 'boss' ? { bossDps: 1e9, leaks: { p_0: 1 }, coins: { p_0: 3 } } : {}) }).start();
|
||||||
|
h.toPrep(14);
|
||||||
|
const m = h.m, ps = h.ps('p_0');
|
||||||
|
const card = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && !m.gd.inactiveEnemies.has(c.enemyKey));
|
||||||
|
m.addBounty(ps, { ...card, rounds: 2, multiRound: false });
|
||||||
|
const gained = ps.stats.fundsGained;
|
||||||
|
const result = h.drive(() => h.ended != null);
|
||||||
|
assert.ok(result && h.ended.victory);
|
||||||
|
assert.equal(ps.pendingFunds, 3);
|
||||||
|
assert.equal(ps.stats.fundsGained, gained + 3);
|
||||||
|
assert.equal(ps.bounties[0].roundsLeft, 1);
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('solo Final Assault: the bounty spawns on the `_s` template; bountySpawns route choice by side', () => {
|
test('solo Final Assault: the bounty spawns on the `_s` template; bountySpawns route choice by side', () => {
|
||||||
const h = makeMatch({ mode: 'solo', difficulty: 'HARD', seed: 62, fake: true, script: (b) => (b.kind === 'boss' ? { bossDps: 1e9 } : {}) }).start();
|
const h = makeMatch({ mode: 'solo', difficulty: 'HARD', seed: 62, fake: true, script: (b) => (b.kind === 'boss' ? { bossDps: 1e9 } : {}) }).start();
|
||||||
const m = h.m;
|
const m = h.m;
|
||||||
|
|||||||
@@ -397,6 +397,22 @@ test('collectViolations reports corrupted state (funds, pool accounting, duplica
|
|||||||
m.dispose();
|
m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('collectViolations: a 教鞭 choice belongs to its owner\'s open prep only', () => {
|
||||||
|
const { m, ps } = prepSolo({ seed: 922 });
|
||||||
|
const art = giveItem(m, ps, 'chess_item_6_03_m');
|
||||||
|
assert.deepEqual(ps.useArt(art.uid, 10, 5), { ok: true });
|
||||||
|
assert.deepEqual(collectViolations(m), [], 'an open choice in its own prep is fine');
|
||||||
|
const pc = ps.personalChoice;
|
||||||
|
pc.round = m.round - 1;
|
||||||
|
assert.match(collectViolations(m).join('\n'), /personal choice outside its own prep/, 'a choice left from an earlier round');
|
||||||
|
pc.round = m.round;
|
||||||
|
pc.cards = [pc.cards[0], pc.cards[0]];
|
||||||
|
assert.match(collectViolations(m).join('\n'), /personal choice of 2 cards/, 'the same card twice');
|
||||||
|
ps.personalChoice = null;
|
||||||
|
assert.deepEqual(collectViolations(m), []);
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('the rule auditor flags an LP loss that does not match the leaks (engine regression guard)', () => {
|
test('the rule auditor flags an LP loss that does not match the leaks (engine regression guard)', () => {
|
||||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 921, fake: true, script: (b) => (b.kind === 'normal' ? { leaks: Object.fromEntries(b.players.map((p) => [p, 2])) } : {}) }).start();
|
const h = makeMatch({ mode: 'coop', humans: 2, seed: 921, fake: true, script: (b) => (b.kind === 'normal' ? { leaks: Object.fromEntries(b.players.map((p) => [p, 2])) } : {}) }).start();
|
||||||
const m = h.m;
|
const m = h.m;
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export function makeMatch(o = {}) {
|
|||||||
if (idx != null) m.handle(ps.playerId, { t: 'g.choice', idx });
|
if (idx != null) m.handle(ps.playerId, { t: 'g.choice', idx });
|
||||||
}
|
}
|
||||||
if (ready && m.phase === 'PREP' && ps.alive && !ps.ready) {
|
if (ready && m.phase === 'PREP' && ps.alive && !ps.ready) {
|
||||||
|
if (ps.personalChoice) m.handle(ps.playerId, { t: 'g.choice', idx: 0, choiceId: ps.personalChoice.id });
|
||||||
if (!ps.tempEmpty) ps.resolveTemp();
|
if (!ps.tempEmpty) ps.resolveTemp();
|
||||||
m.handle(ps.playerId, { t: 'g.ready', ready: true });
|
m.handle(ps.playerId, { t: 'g.ready', ready: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,12 @@ test('boss field: the local pool follows b.pool (server hp − unacknowledged lo
|
|||||||
assert.ok(last.bossDmg <= pool.cum);
|
assert.ok(last.bossDmg <= pool.cum);
|
||||||
assert.ok(last.by && typeof last.by === 'object');
|
assert.ok(last.by && typeof last.by === 'object');
|
||||||
assert.equal(typeof last.leaks, 'number');
|
assert.equal(typeof last.leaks, 'number');
|
||||||
|
// the per-player split of the leak LP (the server holds the result's leaked lists to it, Match._bossLeaksAgree): the
|
||||||
|
// leaks of every player of the field, never more than the field's LP cost (leader effects make up the rest)
|
||||||
|
assert.ok(last.leaksBy && typeof last.leaksBy === 'object', 'boss progress carries leaksBy');
|
||||||
|
assert.deepEqual(Object.keys(last.leaksBy).sort(), start.spec.players.map((p) => p.playerId).sort());
|
||||||
|
assert.ok(Object.values(last.leaksBy).every((n) => Number.isFinite(n) && n >= 0));
|
||||||
|
assert.ok(Object.values(last.leaksBy).reduce((a, b) => a + b, 0) <= last.leaks + 1e-9);
|
||||||
r.net.emit('b.pool', { hp: pool.maxHp * 0.5, max: pool.maxHp, teamLp: 20, acked: { [start.fieldId]: pool.cum } });
|
r.net.emit('b.pool', { hp: pool.maxHp * 0.5, max: pool.maxHp, teamLp: 20, acked: { [start.fieldId]: pool.cum } });
|
||||||
assert.ok(Math.abs(pool.hp - pool.maxHp * 0.5) < 1e-6, 'server hp when everything is acknowledged');
|
assert.ok(Math.abs(pool.hp - pool.maxHp * 0.5) < 1e-6, 'server hp when everything is acknowledged');
|
||||||
r.runner.dispose();
|
r.runner.dispose();
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export class SimClient {
|
|||||||
if (bossLike) {
|
if (bossLike) {
|
||||||
const pool = e.battle.sharedBoss;
|
const pool = e.battle.sharedBoss;
|
||||||
msg.leaks = e.meter.lp;
|
msg.leaks = e.meter.lp;
|
||||||
|
if (p.leaksBy) msg.leaksBy = p.leaksBy; // like public/js/battle/runner.js
|
||||||
msg.bossDmg = pool && Number.isFinite(pool.cum) ? pool.cum : 0;
|
msg.bossDmg = pool && Number.isFinite(pool.cum) ? pool.cum : 0;
|
||||||
if (pool && pool.byPlayer) msg.by = { ...pool.byPlayer };
|
if (pool && pool.byPlayer) msg.by = { ...pool.byPlayer };
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ test('protocol: g.unitStats { seq? } and g.bandFocus { bandId? } validate; m.uni
|
|||||||
assert.equal(validateC2S({ t: 'g.bandFocus', bandId: 'band_bldsk' }), null);
|
assert.equal(validateC2S({ t: 'g.bandFocus', bandId: 'band_bldsk' }), null);
|
||||||
assert.notEqual(validateC2S({ t: 'g.bandFocus', bandId: 'a b' }), null);
|
assert.notEqual(validateC2S({ t: 'g.bandFocus', bandId: 'a b' }), null);
|
||||||
assert.ok(S2C.includes('m.unitStats'));
|
assert.ok(S2C.includes('m.unitStats'));
|
||||||
|
assert.equal(validateC2S({ t: 'g.choice', idx: 0 }), null);
|
||||||
|
assert.equal(validateC2S({ t: 'g.choice', idx: 2, choiceId: 'seed-2.choice.12' }), null);
|
||||||
|
for (const choiceId of [null, '', 'a b', 12, 'a'.repeat(65)]) {
|
||||||
|
assert.notEqual(validateC2S({ t: 'g.choice', idx: 0, choiceId }), null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unitStatsEntry: effective stats next to the base, rounded for display; the interval from bat / aspd when absent', () => {
|
test('unitStatsEntry: effective stats next to the base, rounded for display; the interval from bat / aspd when absent', () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
phaseMode, phaseBanner, isCombatPhase, isBossPhase, countdownState, phaseTotalSeconds, sortBonds, bondTier, nextThreshold,
|
phaseMode, phaseBanner, isCombatPhase, isBossPhase, countdownState, phaseTotalSeconds, sortBonds, bondTier, nextThreshold,
|
||||||
battleOverSfx, ownRoundLoss, uniteResultBox, battleResultBox, roundResultBox, RESULT_BOX_MS,
|
battleOverSfx, ownRoundLoss, uniteResultBox, battleResultBox, roundResultBox, RESULT_BOX_MS,
|
||||||
bondMembers, memberHeadCount, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
|
bondMembers, memberHeadCount, bannedPerBond, priceTone, mergeProgress, shopBlockReason, deploySets, indexPieces, placementContext, canPlace,
|
||||||
boardTargets, dropIntent, normalizeDraft, normalizeSp, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
|
boardTargets, dropIntent, normalizeDraft, normalizeSp, normalizePersonalChoice, groupEnemies, factionTypes, snapHud, bossFrac, attackInterval, fmtNum,
|
||||||
rangeGridBox, shortcutFor, sanitizeSettings, DEFAULT_SETTINGS, normalizeResult, cycleField, fieldLabel, homeFieldId,
|
rangeGridBox, shortcutFor, sanitizeSettings, DEFAULT_SETTINGS, normalizeResult, cycleField, fieldLabel, homeFieldId,
|
||||||
activeBubbles, sortedPlayers, tileKey, prepCapsuleLabel, prepCamera, dropFailureReason, terrainInfo,
|
activeBubbles, sortedPlayers, tileKey, prepCapsuleLabel, prepCamera, dropFailureReason, terrainInfo,
|
||||||
} from '../../public/js/ui/gameLogic.js';
|
} from '../../public/js/ui/gameLogic.js';
|
||||||
@@ -584,6 +584,32 @@ describe('drafts', () => {
|
|||||||
assert.equal(normalizeSp(null), null);
|
assert.equal(normalizeSp(null), null);
|
||||||
assert.equal(normalizeSp({ cards: new Array(9).fill({}) }).cards.length, 6, 'at most 6 cards');
|
assert.equal(normalizeSp({ cards: new Array(9).fill({}) }).cards.length, 6, 'at most 6 cards');
|
||||||
});
|
});
|
||||||
|
test('personal choice: only the living recipient in the current PREP, independent of the public draft', () => {
|
||||||
|
const pub = { phase: PHASE.PREP, round: 14, sp: { family: 'supply', cards: ['global'] } };
|
||||||
|
const priv = { playerId: 'a', alive: true, canReady: false, temp: [], personalChoice: {
|
||||||
|
id: 'seed.choice.3', round: 14, sourceItemId: 'chess_item_6_03_m',
|
||||||
|
cards: [{ id: 'e1', kind: 'bounty', name: '战术特训', coin: 2 }, { id: 'e2', kind: 'bounty' }],
|
||||||
|
} };
|
||||||
|
const before = JSON.stringify({ pub, priv });
|
||||||
|
const sp = normalizePersonalChoice(pub, priv, 'a');
|
||||||
|
assert.equal(sp.id, priv.personalChoice.id);
|
||||||
|
assert.equal(sp.family, 'bounty');
|
||||||
|
assert.equal(sp.name, '教鞭 · 战术特训');
|
||||||
|
assert.equal(sp.turnPid, 'a');
|
||||||
|
assert.equal(sp.pickOf.size, 0);
|
||||||
|
assert.deepEqual(sp.cards.map((c) => c.idx), [0, 1]);
|
||||||
|
assert.ok(sp.cards.every((c) => c.takenBy === null));
|
||||||
|
assert.equal(JSON.stringify({ pub, priv }), before);
|
||||||
|
assert.equal(shopBlockReason('ready', { priv, editable: true }), '请先完成教鞭选择');
|
||||||
|
assert.equal(shopBlockReason('ready', { priv: { ...priv, temp: [{}] }, editable: true }), '请先完成教鞭选择');
|
||||||
|
assert.match(shopBlockReason('ready', { priv: { ...priv, personalChoice: null }, editable: true }), /临时整备区/);
|
||||||
|
for (const phase of [PHASE.SP_DRAFT, PHASE.COMBAT, PHASE.ROUND_START]) assert.equal(normalizePersonalChoice({ ...pub, phase }, priv, 'a'), null);
|
||||||
|
assert.equal(normalizePersonalChoice(pub, null, 'a'), null, 'spectator has no private state');
|
||||||
|
assert.equal(normalizePersonalChoice(pub, priv, 'b'), null);
|
||||||
|
assert.equal(normalizePersonalChoice(pub, { ...priv, alive: false }, 'a'), null);
|
||||||
|
assert.equal(normalizePersonalChoice({ ...pub, round: 15 }, priv, 'a'), null);
|
||||||
|
assert.equal(normalizePersonalChoice(pub, { ...priv, personalChoice: null }, 'a'), null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fields, players, emotes', () => {
|
describe('fields, players, emotes', () => {
|
||||||
|
|||||||
+140
-1
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import { test, describe, before, after } from 'node:test';
|
import { test, describe, before, after } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { existsSync, mkdirSync } from 'node:fs';
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
@@ -71,6 +71,16 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set
|
|||||||
}
|
}
|
||||||
const mockState = (page) => page.evaluate(() => JSON.parse(JSON.stringify(globalThis.__MOCK__.S().priv)));
|
const mockState = (page) => page.evaluate(() => JSON.parse(JSON.stringify(globalThis.__MOCK__.S().priv)));
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
const useWhip = (page) => page.evaluate(async () => {
|
||||||
|
const { net } = await import('/js/net.js');
|
||||||
|
const uid = 900000 + globalThis.__MOCK__.S().requests.length;
|
||||||
|
globalThis.__MOCK__.mutate((S) => {
|
||||||
|
const idx = S.priv.hand.findIndex((p) => !p);
|
||||||
|
S.priv.hand[idx] = { uid, kind: 'item', id: 'chess_item_6_03_m', tier: 6 };
|
||||||
|
});
|
||||||
|
await net.request('g.art', { itemUid: uid, row: 10, col: 5 });
|
||||||
|
return globalThis.__MOCK__.S().priv.personalChoice;
|
||||||
|
});
|
||||||
/** Swipe the open direction wheel from its centre towards `dir` and release (research 09 §1.2). */
|
/** Swipe the open direction wheel from its centre towards `dir` and release (research 09 §1.2). */
|
||||||
const swipeWheel = async (page, dir = 'RIGHT') => {
|
const swipeWheel = async (page, dir = 'RIGHT') => {
|
||||||
await page.waitForSelector('.fwheel__dia', { timeout: 4000 });
|
await page.waitForSelector('.fwheel__dia', { timeout: 4000 });
|
||||||
@@ -480,6 +490,135 @@ describe('in-match UI (mock harness, headless Chrome)', { skip: !ENABLED && 'set
|
|||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('教鞭: PREP cards confirm twice, use its timer, reset on a new ID; old replies cannot clear a new busy pick', async () => {
|
||||||
|
const { page, problems } = await open('phase=PREP', { render: 'fallback' });
|
||||||
|
await page.click('.gm__gear');
|
||||||
|
await page.waitForSelector('.modal .set-keys');
|
||||||
|
await page.click('.set-key[data-action="ready"]');
|
||||||
|
await page.waitForSelector('.set-key.is-waiting[data-action="ready"]');
|
||||||
|
await page.keyboard.press('KeyW');
|
||||||
|
await page.waitForFunction(() => document.querySelector('.set-key[data-action="ready"]')?.textContent.trim() === 'W');
|
||||||
|
await page.click('.modal__actions .btn--primary');
|
||||||
|
await page.waitForSelector('.modal', { hidden: true });
|
||||||
|
const deadline = await page.evaluate(() => globalThis.__MOCK__.S().pub.deadline);
|
||||||
|
const first = await useWhip(page);
|
||||||
|
await page.waitForSelector('.spov[aria-label="教鞭选择"]');
|
||||||
|
assert.equal(await page.evaluate(() => globalThis.__MOCK__.S().pub.deadline), deadline);
|
||||||
|
assert.equal(await page.$('.spov__order'), null);
|
||||||
|
assert.ok(await page.$('.spov .countdown'));
|
||||||
|
assert.doesNotMatch(await page.$eval('.spov', (el) => el.textContent), /机变阶段|当前轮到|正在决策/);
|
||||||
|
assert.equal(await page.$eval('.readybtn', (el) => el.disabled), true);
|
||||||
|
assert.match(await page.$eval('[data-testid="ready-why"]', (el) => el.textContent), /请先完成教鞭选择/);
|
||||||
|
await page.evaluate(async () => { await (await import('/js/ui/lang.js')).loadLangIndex(); });
|
||||||
|
for (const lang of ['en', 'ja', 'ko', 'zh-TW']) {
|
||||||
|
const messages = JSON.parse(readFileSync(path.join(ROOT, `public/i18n/${lang}.json`), 'utf8'));
|
||||||
|
await page.evaluate(async (code) => {
|
||||||
|
await (await import('/js/ui/lang.js')).switchLang(code);
|
||||||
|
globalThis.__MOCK__.pushPublic(); // this harness has no App/useLang root; render again with the active language
|
||||||
|
}, lang);
|
||||||
|
await page.waitForFunction((label) => document.querySelector('.spov')?.getAttribute('aria-label') === label, {}, messages['教鞭选择']);
|
||||||
|
assert.equal(await page.$eval('.spov__title', (el) => el.textContent), `${messages['教鞭 · 战术特训']}|${messages['请选择一项战术特训']}`);
|
||||||
|
assert.match(await page.$eval('.spov__sub', (el) => el.textContent), new RegExp(messages['休整期结束时未选择将自动选定']));
|
||||||
|
assert.equal(await page.$eval('[data-testid="ready-why"]', (el) => el.textContent), messages['请先完成教鞭选择']);
|
||||||
|
assert.equal((await mockState(page)).personalChoice.id, first.id, 'language switch keeps the same pending offer');
|
||||||
|
}
|
||||||
|
await page.evaluate(async () => { await (await import('/js/ui/lang.js')).switchLang('zh'); globalThis.__MOCK__.pushPublic(); });
|
||||||
|
await page.waitForSelector('.spov[aria-label="教鞭选择"]');
|
||||||
|
const requests = await page.evaluate(() => globalThis.__MOCK__.S().requests.length);
|
||||||
|
for (const key of ['KeyW', 'KeyR', 'KeyF', 'KeyD', 'KeyQ', 'KeyX']) await page.keyboard.press(key);
|
||||||
|
await sleep(150);
|
||||||
|
assert.equal(await page.evaluate(() => globalThis.__MOCK__.S().requests.length), requests, 'background shortcuts send no intents');
|
||||||
|
assert.match(await page.$$eval('.toast', (els) => els.map((el) => el.textContent).join('\n')), /请先完成教鞭选择/);
|
||||||
|
assert.equal(await page.$eval('.readybtn__key', (el) => el.textContent), 'W');
|
||||||
|
await page.click('.spcard.is-pickable');
|
||||||
|
await page.waitForSelector('.spcard.is-armed');
|
||||||
|
await sleep(200); // the overlay attaches its Escape listener in an effect, a frame after the highlight shows
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('.spcard.is-armed'));
|
||||||
|
assert.equal((await mockState(page)).personalChoice.id, first.id, 'Escape only removes the highlight');
|
||||||
|
await page.screenshot({ path: path.join(OUT, 'ui-personal-choice-1920.png') });
|
||||||
|
await page.evaluate(async (id) => {
|
||||||
|
const { net } = await import('/js/net.js');
|
||||||
|
const request = net.request;
|
||||||
|
globalThis.__choiceReplies = {};
|
||||||
|
net.request = async (t, fields) => {
|
||||||
|
if (t !== 'g.choice' || fields.choiceId === undefined) return request(t, fields);
|
||||||
|
if (fields.choiceId === id) {
|
||||||
|
const result = await request(t, fields);
|
||||||
|
await new Promise((resolve) => { globalThis.__choiceReplies[fields.choiceId] = resolve; });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => { globalThis.__choiceReplies[fields.choiceId] = resolve; });
|
||||||
|
return request(t, fields);
|
||||||
|
};
|
||||||
|
}, first.id);
|
||||||
|
await page.click('.spcard.is-pickable');
|
||||||
|
assert.equal((await mockState(page)).personalChoice.id, first.id, 'one tap does not apply it');
|
||||||
|
await page.click('.spcard.is-armed');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('.spov'));
|
||||||
|
const firstRequest = await page.evaluate(() => globalThis.__MOCK__.S().requests.filter(([t]) => t === 'g.choice').at(-1));
|
||||||
|
assert.deepEqual(firstRequest, ['g.choice', { idx: 0, choiceId: first.id }]);
|
||||||
|
const second = await useWhip(page);
|
||||||
|
await page.waitForSelector('.spov');
|
||||||
|
await page.click('.spcard.is-pickable');
|
||||||
|
await page.waitForSelector('.spcard.is-armed');
|
||||||
|
const thirdId = second.id + '.next';
|
||||||
|
await page.evaluate((id) => globalThis.__MOCK__.mutate((S) => { S.pub.deadline = 0; S.priv.personalChoice.id = id; }), thirdId);
|
||||||
|
await page.waitForFunction(() => !document.querySelector('.spcard.is-armed'));
|
||||||
|
assert.equal(await page.$('.spov .countdown'), null, 'co-op PREP without a deadline is untimed');
|
||||||
|
await page.click('.spcard.is-pickable');
|
||||||
|
await page.click('.spov__confirm');
|
||||||
|
await page.waitForSelector('.spcard.is-busy');
|
||||||
|
await page.waitForFunction((id) => !!globalThis.__choiceReplies[id], {}, first.id);
|
||||||
|
await page.evaluate((id) => globalThis.__choiceReplies[id](), first.id);
|
||||||
|
await sleep(150);
|
||||||
|
assert.ok(await page.$('.spcard.is-busy'), 'old reply leaves the newer choice busy');
|
||||||
|
await page.evaluate((id) => globalThis.__choiceReplies[id](), thirdId);
|
||||||
|
await page.waitForFunction(() => !document.querySelector('.spov'));
|
||||||
|
assert.equal((await mockState(page)).personalChoice, null);
|
||||||
|
assert.deepEqual(problems, []);
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('教鞭 appearing cancels the uncommitted direction wheel and active drag, releasing its held piece', async () => {
|
||||||
|
const { page, problems } = await open('phase=PREP', { render: 'fallback' });
|
||||||
|
const s = await mockState(page);
|
||||||
|
const hand = s.hand.find((p) => p && p.kind === 'chess');
|
||||||
|
const target = s.board.find((p) => p.kind === 'chess' && p.row === 9);
|
||||||
|
const selector = `.ff-piece[data-uid="${hand.uid}"]`;
|
||||||
|
const center = (sel) => page.$eval(sel, (el) => { const r = el.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; });
|
||||||
|
const homeStyle = await page.$eval(selector, (el) => el.style.transform);
|
||||||
|
const from = await center(selector), to = await center(`.ff-piece[data-uid="${target.uid}"]`);
|
||||||
|
await page.mouse.move(from.x, from.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(to.x, to.y, { steps: 12 });
|
||||||
|
await sleep(80);
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.waitForSelector('.fwheel');
|
||||||
|
await useWhip(page);
|
||||||
|
await page.waitForSelector('.spov');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('.fwheel'));
|
||||||
|
await page.waitForFunction((sel, transform) => document.querySelector(sel)?.style.transform === transform, {}, selector, homeStyle);
|
||||||
|
assert.equal(await page.$eval(selector, (el) => el.style.transform), homeStyle, 'cancelFacing released the held bench piece');
|
||||||
|
assert.equal(await page.$('.ff-piece.is-draggable'), null);
|
||||||
|
await page.click('.spcard.is-pickable');
|
||||||
|
await page.click('.spov__confirm');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('.spov'));
|
||||||
|
const current = await center(selector);
|
||||||
|
await page.mouse.move(current.x, current.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(current.x + 90, current.y - 40, { steps: 8 });
|
||||||
|
await page.waitForSelector('.gm.is-dragging');
|
||||||
|
await useWhip(page);
|
||||||
|
await page.waitForSelector('.spov');
|
||||||
|
await page.waitForFunction(() => !document.querySelector('.gm.is-dragging, .ff-piece.is-lifted'));
|
||||||
|
await page.mouse.up();
|
||||||
|
assert.deepEqual((await mockState(page)).board, s.board);
|
||||||
|
assert.ok(!(await page.evaluate(() => globalThis.__MOCK__.S().requests.some(([t]) => t === 'g.move'))));
|
||||||
|
assert.deepEqual(problems, []);
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
// ---- round-1 UI fixes ----------------------------------------------------------------------------------------
|
// ---- round-1 UI fixes ----------------------------------------------------------------------------------------
|
||||||
const rectOf = (page, sel) => page.$eval(sel, (el) => { const r = el.getBoundingClientRect(); return { left: r.left, top: r.top, right: r.right, bottom: r.bottom }; });
|
const rectOf = (page, sel) => page.$eval(sel, (el) => { const r = el.getBoundingClientRect(); return { left: r.left, top: r.top, right: r.right, bottom: r.bottom }; });
|
||||||
const overlaps = (a, b) => !!a && !!b && a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
const overlaps = (a, b) => !!a && !!b && a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ describe('user playtest #2 item 10 — boss-round prep on the boss field (real s
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('co-op 险境 (R14): host on the left half, guest on the mirrored right half; scouting and 返回 keep the pen hidden', { skip: only('coop'), timeout: 10 * 60 * 1000 }, async () => {
|
test('co-op 险境 (R14): host on the left half, guest on the mirrored right half; scouting and 返回 keep the pen hidden', { skip: only('coop'), timeout: 10 * 60 * 1000 }, async () => {
|
||||||
const srv = await startRealServer({ fast: { timerScale: 0.5, combatSpeed: 8, startRound: 'boss' } });
|
const srv = await startRealServer({ fast: { timerScale: 1, combatSpeed: 8, startRound: 'boss', items: ['chess_item_6_03_m'] } });
|
||||||
const P = await pptr();
|
const P = await pptr();
|
||||||
const host = new Client(P, srv.base, 'host', { prefix: 'fix' });
|
const host = new Client(P, srv.base, 'host', { prefix: 'fix' });
|
||||||
const guest = new Client(P, srv.base, 'guest', { prefix: 'fix', w: 1280, h: 720 });
|
const guest = new Client(P, srv.base, 'guest', { prefix: 'fix', w: 1280, h: 720 });
|
||||||
@@ -412,6 +412,47 @@ describe('user playtest #2 item 10 — boss-round prep on the boss field (real s
|
|||||||
assert.deepEqual(back.prepField, { kind: 'bossPrep', side: 'L', mirror: false }, '返回: the own boss half again');
|
assert.deepEqual(back.prepField, { kind: 'bossPrep', side: 'L', mirror: false }, '返回: the own boss half again');
|
||||||
}
|
}
|
||||||
await penRoundTrip(guest, 'guest');
|
await penRoundTrip(guest, 'guest');
|
||||||
|
// Real clients open their own 教鞭 candidates together; refreshing one tab restores that same private choice.
|
||||||
|
const choiceOf = (c) => c.page.evaluate(() => JSON.parse(JSON.stringify(globalThis.__SP__.store.get().match.private.personalChoice)));
|
||||||
|
const deadline = await host.page.evaluate(() => globalThis.__SP__.store.get().match.public.deadline);
|
||||||
|
await Promise.all([[host, hp], [guest, gp]].map(async ([c, placed]) => {
|
||||||
|
const art = (await c.handPieces('item')).find((p) => p.id === 'chess_item_6_03_m');
|
||||||
|
assert.ok(art, `${c.label}: starter 教鞭`);
|
||||||
|
await c.drag(await c.piecePoint(art.uid), await c.tilePoint(placed.row, placed.col));
|
||||||
|
await c.page.waitForSelector('.spov[aria-label="教鞭选择"]', { timeout: 6000 });
|
||||||
|
}));
|
||||||
|
const hostChoice = await choiceOf(host), guestChoice = await choiceOf(guest);
|
||||||
|
assert.notEqual(hostChoice.id, guestChoice.id);
|
||||||
|
for (const c of [host, guest]) {
|
||||||
|
assert.equal((await choiceOf(c)).cards.length, 3);
|
||||||
|
assert.equal((await c.st()).canReady, false);
|
||||||
|
assert.equal(await c.page.$('.spov__order'), null);
|
||||||
|
assert.equal(await c.page.evaluate(() => globalThis.__SP__.store.get().match.public.deadline), deadline);
|
||||||
|
}
|
||||||
|
await host.page.reload({ waitUntil: 'networkidle0' });
|
||||||
|
await host.page.waitForSelector('.spov[aria-label="教鞭选择"]', { timeout: 20000 });
|
||||||
|
assert.deepEqual(await choiceOf(host), hostChoice, 'refresh reuses the ID and cards');
|
||||||
|
assert.deepEqual(await choiceOf(guest), guestChoice, 'the teammate keeps its own candidates');
|
||||||
|
await host.page.evaluate(async () => { await (await import('/js/ui/lang.js')).switchLang('en'); });
|
||||||
|
await host.page.waitForSelector('.spov[aria-label="Pointing Stick Choice"]');
|
||||||
|
assert.match(await host.page.$eval('.spov__title', (el) => el.textContent), /Pointing Stick · Tactical Training/);
|
||||||
|
assert.equal(await host.page.$eval('[data-testid="ready-why"]', (el) => el.textContent), 'Complete the Pointing Stick choice first');
|
||||||
|
assert.equal(await guest.page.$eval('.spov', (el) => el.getAttribute('aria-label')), '教鞭选择', 'language stays per client');
|
||||||
|
assert.deepEqual(await choiceOf(host), hostChoice, 'language switch does not replace the offer');
|
||||||
|
await host.page.evaluate(async () => { await (await import('/js/ui/lang.js')).switchLang('zh'); });
|
||||||
|
await host.page.waitForSelector('.spov[aria-label="教鞭选择"]');
|
||||||
|
const refused = await host.page.evaluate(async (id) => {
|
||||||
|
try { await globalThis.__SP__.net.request('g.choice', { idx: 0, choiceId: id }); return null; }
|
||||||
|
catch (e) { return e.code; }
|
||||||
|
}, guestChoice.id);
|
||||||
|
assert.equal(refused, 'BAD_TARGET', 'a live client cannot submit the teammate\'s ID');
|
||||||
|
await host.shot('dobermann-private-reconnect');
|
||||||
|
for (const c of [host, guest]) {
|
||||||
|
await c.click('.spcard.is-pickable');
|
||||||
|
assert.ok(await choiceOf(c), 'first tap only highlights');
|
||||||
|
await c.click('.spov__confirm');
|
||||||
|
await c.page.waitForFunction(() => globalThis.__SP__.store.get().match.private.personalChoice === null, { timeout: 6000 });
|
||||||
|
}
|
||||||
for (const c of [host, guest]) await c.click('.readybtn');
|
for (const c of [host, guest]) await c.click('.readybtn');
|
||||||
await checkBossBattle(host, hp, 'L', 'host');
|
await checkBossBattle(host, hp, 'L', 'host');
|
||||||
await checkBossBattle(guest, gp, 'R', 'guest');
|
await checkBossBattle(guest, gp, 'R', 'guest');
|
||||||
|
|||||||
@@ -222,6 +222,18 @@ describe('3: the temp overflow row (临时整备区)', () => {
|
|||||||
assert.deepEqual(tempInfo(null), { count: 0, items: 0 });
|
assert.deepEqual(tempInfo(null), { count: 0, items: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Ready shows the personal-choice reason and describes it even with no temp pieces', () => {
|
||||||
|
for (const temp of [[], [{ uid: 1, kind: 'item' }]]) {
|
||||||
|
const v = ReadyToggle({ priv: { ...priv(temp), canReady: false, personalChoice: { id: 'choice.1' } }, onToggle() {} });
|
||||||
|
const nodes = [...walk(v)];
|
||||||
|
const btn = nodes.find((n) => n.type === 'button');
|
||||||
|
assert.equal(btn.props.disabled, true);
|
||||||
|
assert.equal(btn.props['aria-describedby'], 'readywrap-why');
|
||||||
|
assert.equal(textOf(nodes.find((n) => hasClass(n, 'readywrap__why'))), '请先完成教鞭选择');
|
||||||
|
assert.equal(nodes.find((n) => n.type?.name === 'Tooltip').props.text, '请先完成教鞭选择');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('the row frame: the outer corners of both end tiles, the label on the side with room (mirrored Final Assault prep too)', () => {
|
test('the row frame: the outer corners of both end tiles, the label on the side with room (mirrored Final Assault prep too)', () => {
|
||||||
// tileScreen polys: [back-left, back-right, front-right, front-left] (render/app.js tileScreen corner order)
|
// tileScreen polys: [back-left, back-right, front-right, front-left] (render/app.js tileScreen corner order)
|
||||||
const tile = (cx, y, w = 100, d = 60) => ({ x: cx, y, s: w, poly: [[cx - w / 2 + 6, y - d / 2], [cx + w / 2 - 6, y - d / 2], [cx + w / 2, y + d / 2], [cx - w / 2, y + d / 2]] });
|
const tile = (cx, y, w = 100, d = 60) => ({ x: cx, y, s: w, poly: [[cx - w / 2 + 6, y - d / 2], [cx + w / 2 - 6, y - d / 2], [cx + w / 2, y + d / 2], [cx - w / 2, y + d / 2]] });
|
||||||
@@ -258,7 +270,7 @@ describe('3: the temp overflow row (临时整备区)', () => {
|
|||||||
test('while the ready button shows its reason the effects column moves down a line (on phones the ready count ran into it)', () => {
|
test('while the ready button shows its reason the effects column moves down a line (on phones the ready count ran into it)', () => {
|
||||||
const game = read('public/js/screens/game.js');
|
const game = read('public/js/screens/game.js');
|
||||||
// the same condition as ReadyToggle's reason: PREP, alive, not ready, pieces in the row
|
// the same condition as ReadyToggle's reason: PREP, alive, not ready, pieces in the row
|
||||||
assert.match(game, /const readyWhy = phase === PHASE\.PREP && alive && !priv\?\.ready && temp\.count > 0;/);
|
assert.match(game, /const readyWhy = phase === PHASE\.PREP && alive && !priv\?\.ready && \(hasPersonalChoice \|\| temp\.count > 0\);/);
|
||||||
assert.match(game, /readyWhy && 'has-readywhy'/);
|
assert.match(game, /readyWhy && 'has-readywhy'/);
|
||||||
const css = read('public/css/screens/game.css');
|
const css = read('public/css/screens/game.css');
|
||||||
assert.match(css, /\.gm__effects \{ position: absolute; right: \.3rem; top: 2\.4rem; \}/);
|
assert.match(css, /\.gm__effects \{ position: absolute; right: \.3rem; top: 2\.4rem; \}/);
|
||||||
@@ -389,6 +401,18 @@ describe('9: busy indicators and data loading', () => {
|
|||||||
assert.ok(![...walk(landed)].some((n) => hasClass(n, 'spcard__busy')));
|
assert.ok(![...walk(landed)].some((n) => hasClass(n, 'spcard__busy')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('choice requests carry the personal ID while global requests keep their existing shape', async () => {
|
||||||
|
const { net } = await import('../../public/js/net.js');
|
||||||
|
const { actions } = await import('../../public/js/ui/gameActions.js');
|
||||||
|
const request = net.request, sent = [];
|
||||||
|
net.request = async (t, fields) => { sent.push([t, fields]); };
|
||||||
|
try {
|
||||||
|
assert.equal(await actions.choice(0), true);
|
||||||
|
assert.equal(await actions.choice(2, 'seed.choice.3'), true);
|
||||||
|
assert.deepEqual(sent, [['g.choice', { idx: 0 }], ['g.choice', { idx: 2, choiceId: 'seed.choice.3' }]]);
|
||||||
|
} finally { net.request = request; }
|
||||||
|
});
|
||||||
|
|
||||||
test('the "作战结束,等待队友完成作战" hourglass stands still', () => {
|
test('the "作战结束,等待队友完成作战" hourglass stands still', () => {
|
||||||
const css = read('public/css/screens/game.css');
|
const css = read('public/css/screens/game.css');
|
||||||
assert.doesNotMatch(css, /hg-flip/);
|
assert.doesNotMatch(css, /hg-flip/);
|
||||||
|
|||||||
@@ -130,6 +130,28 @@ describe('2: 机变 cards take two taps (select → confirm), like buying in the
|
|||||||
assert.match(textOf(lone.find((n) => hasClass(n, 'spov__sub'))), /无时间限制/);
|
assert.match(textOf(lone.find((n) => hasClass(n, 'spov__sub'))), /无时间限制/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('教鞭 ChoiceView: personal heading, no global order or turn; timing follows PREP deadline', () => {
|
||||||
|
const s = sp({ name: '教鞭 · 战术特训', desc: '请选择一项战术特训', cards: sp().cards.slice(0, 2) });
|
||||||
|
for (const [solo, deadline] of [[false, 0], [true, 123456]]) {
|
||||||
|
let confirmed = false;
|
||||||
|
const v = ChoiceView({ pub: { players: [{ playerId: 'me', name: 'Me' }, { playerId: 'p2', name: 'P2' }], deadline }, sp: s,
|
||||||
|
...me, solo, personal: true, armed: 0, total: 74, onConfirm: () => { confirmed = true; } });
|
||||||
|
const nodes = [...walk(v)];
|
||||||
|
assert.equal(v.props['aria-label'], '教鞭选择');
|
||||||
|
assert.ok(!nodes.some((n) => hasClass(n, 'spov__order')));
|
||||||
|
assert.doesNotMatch(textOf(v), /机变阶段|当前轮到|正在决策/);
|
||||||
|
const clock = nodes.find((n) => n.type?.name === 'Countdown');
|
||||||
|
assert.equal(!!clock, deadline > 0);
|
||||||
|
if (clock) { assert.equal(clock.props.deadline, deadline); assert.equal(clock.props.total, 74); }
|
||||||
|
assert.match(textOf(nodes.find((n) => hasClass(n, 'spov__sub'))), deadline ? /休整期结束时未选择将自动选定/ : /无时间限制/);
|
||||||
|
const btn = nodes.find((n) => n.type === Button);
|
||||||
|
btn.props.onClick();
|
||||||
|
assert.ok(confirmed);
|
||||||
|
assert.match(btn.props.title, /确认选择「A」/);
|
||||||
|
assert.match(nodes.find((n) => hasClass(n, 'spcard')).props.title, /^A\na$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('the overlay keeps the selection itself and drops it on Esc / a tap elsewhere (source contract)', () => {
|
test('the overlay keeps the selection itself and drops it on Esc / a tap elsewhere (source contract)', () => {
|
||||||
const src = read('public/js/ui/choiceOverlay.js');
|
const src = read('public/js/ui/choiceOverlay.js');
|
||||||
assert.match(src, /export function ChoiceOverlay\(props\) \{[\s\S]*useState\(null\)[\s\S]*armedCard\(sel, sp/);
|
assert.match(src, /export function ChoiceOverlay\(props\) \{[\s\S]*useState\(null\)[\s\S]*armedCard\(sel, sp/);
|
||||||
|
|||||||
Reference in new issue
Block a user