Files
ark-stronghold-protocol/test/fixtures/stats-results.json
T
sganggs 51e0e60b6e feedback7 stats: a match left right away stays in the history but does not count; bounded storage, corrupt-data handling, the page reworked as 干员调配-style tabs, the settlement re-view without the app store (PR #323 by @2321Robin, owner's decision of 2026-10-08); golden: unchanged
Counting rule (ui/stats.js countsTowardStats, the owner's decision of 2026-10-08: do not count the matches entered and left
right away, but keep them in the history): a settled match (m.result arrived) always counts; a match given up (放弃模拟)
counts once at least one of the player's battles had finished — a round cleared (the server's own rule, platform.js
_quit: leaving while round k runs has passed k - 1, in its settlement k) or already eliminated. Anything earlier is
history only: marked 中途退出 and 不计入统计, dimmed, listed in 对局记录, left out of every aggregate (games, wins, win
rate, strategy table, titles, rounds, time, combat totals); 总览 says how many were left out.
- A leaver gets no m.result, so the page records the match itself: quitMatch (放弃模拟) → recordQuit builds an
  `end: 'quit'` record from the app state and a tracker (observeMatch, installed in main.js) that remembers when the page
  first saw the match and the last round the player was seen alive in. No record for a spectator seat, a match with its
  settlement already there, a match the page never saw; matches the server ended and closed tabs are not recorded.

Defects of PR #323 as submitted, reproduced (scratchpad/w7-stats-notes/repro-pr-bugs.mjs, loads-check*.mjs) and fixed:
- the title screen loaded all 16 game data files (5.3 MB) at boot: <StatsHost/> called useGameData() at mount; now the
  host renders nothing until opened, the overview reads config / bands / assets and the settlement re-view loads the rest
  when opened (the title screen loads the same 3 files as before);
- importing a file put the list oldest first (the page shows the first 20 as the newest and the cap would drop the
  newest); the list is newest first everywhere, also when loaded from storage;
- recordResult wrote over the envelope of a NEWER build (a downgrade, against the PR's own rule); it refuses now, as
  saveStats does;
- 1000 four-player records are ~7.2 M characters (the PR's ~1.5 kB/record is a solo match) against a ~2.5 M-character
  quota: MAX_RECORDS 1000 stays, plus a 800 000-character budget (the oldest go first), and only the newest 30 records
  keep their rows (lineup, bonds, teammates' names; the replay needs them) — older ones are compacted to the player's own
  summary row (~620 characters), a refused write retries with half the history; unknown fields and oversized values are
  dropped (a strict schema), import files are bounded (8 MB);
- the re-view clobbered the app store (match.public / match.result, the route, the music); ResultScreen is split into
  ResultScreen (the store) and ResultView (props: result, myId, back label), so the stats page renders a stored match
  itself; a 自选 pick and a 补位 stand-in were dropped from the stored lineup (the replayed card drew the plain
  operator); DIFFICULTY_NAMES were not translated; a band cell was a display:flex <td>; the overlay stayed open when a
  co-op match started; the 清空 two-step blink was replaced by the shared confirm dialog.
- Corrupt storage: unreadable data is moved to sp.pref.stats.damaged and the page says so; damaged rows are dropped and
  counted; duplicates, an oversized or unsorted list are repaired on load; a pre-`end` (PR-era) envelope reads as settled.

UI: a full-screen overlay in the grammar of 干员调配 (top bar with 返回 / four tabs 总览 · 策略 · 战斗累计 · 对局记录 /
导出 导入 清空, a note line saying the data stays in this browser and that teammates' names are stored too), text sizes
with max(.xxrem, N px) for phones in landscape, a 50-row history page with 显示更多, a pass-rate bar, a per-difficulty
table in order; the entries use a new `chart` icon (not the ping bars), the title screen's sits beside the language menu
(the connection row has no room: it squeezed the gear and wrapped the status text), the lobby / room buttons show the
icon alone below 700 px (the room's title ran under the label at 640 px). docs/PLAYING.md §12, README row; 41 + 25 UI
strings in en / ja / ko / zh-TW (the PR's four unused ones removed).

Tests: test/ui/stats.test.js 44 / 44 (counting rule per phase, quit records, bounds, corrupt / old / newer storage,
import order, aggregation, the replay payload); test/ui/stats.e2e.test.js (opt-in SP_E2E=1, real server, headless Chrome:
seeded history on 1920×1080 and 844×390, the re-view, export / import / clear, corrupt and newer storage, and the real
flows: a solo match given up in the briefing is history only, one given up after round 1 counts, a settled match is
recorded once and not again after a reload) 6 / 6; lint 0 errors, i18n extract 0 / check --all --strict clean,
check:imports, typecheck, docs and version tests, golden unchanged.
2026-10-08 20:36:41 +08:00

2 lines
14 KiB
JSON

{"solo-normal":{"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"modeId":"mode_single_normal","difficulty":"NORMAL","stageId":"act2autochess_m04","bossId":"boss_1","hiddenBossId":"boss_8","seed":101,"durationMs":291900,"players":[{"playerId":"p_0","seat":0,"name":"P0","isBot":false,"left":false,"alive":true,"victory":false,"roundsPassed":13,"eliminatedRound":null,"lp":0,"bandId":"band_sarkazb","lineup":[{"id":"chess_char_1_14_b","golden":true,"tier":1,"row":11,"col":7,"items":[]},{"id":"chess_char_3_13_b","golden":true,"tier":3,"row":11,"col":8,"items":["chess_item_3_06_e_a","chess_item_4_08_e_a"]},{"id":"chess_char_3_14_a","golden":false,"tier":3,"row":10,"col":5,"items":["chess_item_3_03_e_a","chess_item_5_02_e_a"]},{"id":"chess_char_5_14_a","golden":false,"tier":5,"row":10,"col":8,"items":["chess_item_3_01_e_a","chess_item_1_01_e_b"]},{"id":"chess_char_1_02_b","golden":true,"tier":1,"row":9,"col":6,"items":["chess_item_3_11_e_a","chess_item_2_01_e_b"]},{"id":"chess_char_1_20_b","golden":true,"tier":1,"row":9,"col":7,"items":[]},{"id":"chess_char_5_06_a","golden":false,"tier":5,"row":9,"col":8,"items":["chess_item_3_08_e_a"]},{"id":"chess_char_4_07_b","golden":true,"tier":4,"row":9,"col":9,"items":["chess_item_6_05_e_a","chess_item_3_09_e_a"]}],"bonds":[{"bondId":"indomShip","count":3,"active":true,"tier":2,"layers":30},{"bondId":"visiShip","count":2,"active":true,"tier":1,"layers":19},{"bondId":"kjeragShip","count":3,"active":true,"tier":1,"layers":12},{"bondId":"skillfulShip","count":3,"active":true,"tier":1,"layers":8},{"bondId":"suntShip","count":5,"active":true,"tier":2,"layers":0},{"bondId":"steadShip","count":1,"active":false,"tier":0,"layers":8},{"bondId":"swiftShip","count":1,"active":false,"tier":0,"layers":6},{"bondId":"preciShip","count":0,"active":false,"tier":0,"layers":2},{"bondId":"arcaneShip","count":0,"active":false,"tier":0,"layers":2}],"stats":{"dmgDealt":2724948,"kills":327,"leaks":19,"gold":133,"refreshes":14,"merges":5,"itemsEquipped":21,"bossDamage":509756,"activatedLayers":69,"lpLost":10,"perfectRounds":12},"title":{"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},"trophies":0,"reward":320}]},"coop4-hard":{"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":37,"modeId":"mode_multi_hard","difficulty":"HARD","stageId":"act2autochess_m04","bossId":"boss_2","hiddenBossId":"boss_10","seed":102,"durationMs":308166.2697753906,"players":[{"playerId":"p_0","seat":0,"name":"P0","isBot":false,"left":false,"alive":true,"victory":true,"roundsPassed":14,"eliminatedRound":null,"lp":16,"bandId":"band_quintus","lineup":[{"id":"chess_char_3_09_b","golden":true,"tier":3,"row":11,"col":8,"items":["chess_item_4_08_e_a","chess_item_4_07_e_a"]},{"id":"chess_char_3_17_a","golden":false,"tier":3,"row":10,"col":3,"items":["chess_item_4_03_e_a","chess_item_3_05_e_b"]},{"id":"chess_char_6_19_a","golden":false,"tier":6,"row":10,"col":4,"items":["chess_item_3_01_e_a","chess_item_2_04_e_a"]},{"id":"chess_char_1_10_b","golden":true,"tier":1,"row":10,"col":5,"items":[]},{"id":"chess_char_4_04_a","golden":false,"tier":4,"row":10,"col":7,"items":[]},{"id":"chess_char_6_07_a","golden":false,"tier":6,"row":10,"col":8,"items":["chess_item_4_04_e_a"]},{"id":"chess_char_2_18_a","golden":false,"tier":2,"row":9,"col":8,"items":["chess_item_1_01_e_a","chess_item_1_05_e_a"]},{"id":"chess_char_5_07_a","golden":false,"tier":5,"row":9,"col":9,"items":["chess_item_4_06_e_a","chess_item_3_03_e_a"]}],"bonds":[{"bondId":"raidShip","count":2,"active":true,"tier":1,"layers":147},{"bondId":"kazimierzShip","count":3,"active":true,"tier":1,"layers":78},{"bondId":"arcaneShip","count":2,"active":true,"tier":1,"layers":18},{"bondId":"steadShip","count":2,"active":true,"tier":1,"layers":3},{"bondId":"soloShip","count":1,"active":true,"tier":1,"layers":0},{"bondId":"suntShip","count":2,"active":true,"tier":1,"layers":0},{"bondId":"visiShip","count":1,"active":false,"tier":0,"layers":21},{"bondId":"swiftShip","count":1,"active":false,"tier":0,"layers":18},{"bondId":"kjeragShip","count":1,"active":false,"tier":0,"layers":16}],"stats":{"dmgDealt":6871190,"kills":393,"leaks":21,"gold":148,"refreshes":23,"merges":3,"itemsEquipped":33,"bossDamage":945575,"activatedLayers":246,"lpLost":10,"perfectRounds":7},"title":{"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},"trophies":5,"reward":425},{"playerId":"p_1","seat":1,"name":"P1","isBot":false,"left":false,"alive":true,"victory":true,"roundsPassed":14,"eliminatedRound":null,"lp":6,"bandId":"band_ducklord","lineup":[{"id":"chess_char_4_13_a","golden":false,"tier":4,"row":12,"col":5,"items":[]},{"id":"chess_char_1_02_b","golden":true,"tier":1,"row":12,"col":8,"items":["chess_item_4_06_e_b","chess_item_4_08_e_a"]},{"id":"chess_char_3_14_b","golden":true,"tier":3,"row":11,"col":8,"items":[]},{"id":"chess_char_3_13_b","golden":true,"tier":3,"row":10,"col":3,"items":["chess_item_5_07_e_a"]},{"id":"chess_char_6_03_b","golden":true,"tier":6,"row":10,"col":4,"items":["chess_item_2_01_e_a","chess_item_2_04_e_b"]},{"id":"chess_char_3_06_b","golden":true,"tier":3,"row":10,"col":5,"items":["chess_item_4_04_e_a","chess_item_3_08_e_b"]},{"id":"chess_char_4_04_b","golden":true,"tier":4,"row":10,"col":7,"items":["chess_item_3_02_e_a","chess_item_1_02_e_a"]},{"id":"chess_char_6_06_a","golden":false,"tier":6,"row":10,"col":8,"items":["chess_item_4_02_e_a"]}],"bonds":[{"bondId":"sargonShip","count":3,"active":true,"tier":1,"layers":52},{"bondId":"kjeragShip","count":3,"active":true,"tier":1,"layers":43},{"bondId":"visiShip","count":2,"active":true,"tier":1,"layers":40},{"bondId":"steadShip","count":2,"active":true,"tier":1,"layers":24},{"bondId":"skillfulShip","count":2,"active":true,"tier":1,"layers":13},{"bondId":"suntShip","count":6,"active":true,"tier":2,"layers":0},{"bondId":"preciShip","count":0,"active":false,"tier":0,"layers":2},{"bondId":"arcaneShip","count":0,"active":false,"tier":0,"layers":2}],"stats":{"dmgDealt":3293490,"kills":314,"leaks":51,"gold":154,"refreshes":26,"merges":7,"itemsEquipped":19,"bossDamage":220004,"activatedLayers":172,"lpLost":21,"perfectRounds":8},"title":{"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},"trophies":5,"reward":425},{"playerId":"ai_0","seat":2,"name":"AI0","isBot":true,"left":false,"alive":true,"victory":true,"roundsPassed":14,"eliminatedRound":null,"lp":7,"bandId":"band_yu","lineup":[{"id":"chess_char_3_20_a","golden":false,"tier":3,"row":12,"col":4,"items":["chess_item_2_01_e_a"]},{"id":"chess_char_6_05_a","golden":false,"tier":6,"row":12,"col":5,"items":[]},{"id":"chess_char_6_12_a","golden":false,"tier":6,"row":12,"col":7,"items":["chess_item_4_07_e_a","chess_item_3_11_e_a"]},{"id":"chess_char_5_10_a","golden":false,"tier":5,"row":11,"col":3,"items":[]},{"id":"chess_char_3_11_a","golden":false,"tier":3,"row":11,"col":8,"items":["chess_item_1_05_e_b","chess_item_1_01_e_a"]},{"id":"chess_char_4_10_b","golden":true,"tier":4,"row":10,"col":3,"items":["chess_item_3_06_e_a","chess_item_3_07_e_a"]},{"id":"chess_char_1_08_b","golden":true,"tier":1,"row":10,"col":8,"items":["chess_item_4_11_e_a"]},{"id":"chess_char_5_14_a","golden":false,"tier":5,"row":9,"col":9,"items":["chess_item_5_02_e_b","chess_item_3_03_e_a"]}],"bonds":[{"bondId":"preciShip","count":3,"active":true,"tier":2,"layers":89},{"bondId":"soloShip","count":1,"active":true,"tier":1,"layers":20},{"bondId":"kjeragShip","count":3,"active":true,"tier":1,"layers":19},{"bondId":"siracusaShip","count":3,"active":true,"tier":1,"layers":16},{"bondId":"swiftShip","count":2,"active":true,"tier":1,"layers":5},{"bondId":"visiShip","count":2,"active":true,"tier":1,"layers":0},{"bondId":"emptyShip","count":2,"active":true,"tier":1,"layers":0},{"bondId":"suntShip","count":2,"active":true,"tier":1,"layers":0},{"bondId":"arcaneShip","count":1,"active":false,"tier":0,"layers":42},{"bondId":"skillfulShip","count":0,"active":false,"tier":0,"layers":22},{"bondId":"deputShip","count":1,"active":false,"tier":0,"layers":20},{"bondId":"steadShip","count":0,"active":false,"tier":0,"layers":4},{"bondId":"sargonShip","count":1,"active":false,"tier":0,"layers":2}],"stats":{"dmgDealt":4192977,"kills":324,"leaks":48,"gold":151,"refreshes":16,"merges":3,"itemsEquipped":15,"bossDamage":610492,"activatedLayers":149,"lpLost":17,"perfectRounds":5},"title":{"id":"comment_6","name":"挥金如土","picId":"comment_icon_6","text":"消耗资金最多"},"trophies":5,"reward":425},{"playerId":"ai_1","seat":3,"name":"AI1","isBot":true,"left":false,"alive":true,"victory":true,"roundsPassed":14,"eliminatedRound":null,"lp":8,"bandId":"band_vodfox","lineup":[{"id":"chess_char_5_02_a","golden":false,"tier":5,"row":12,"col":4,"items":[]},{"id":"chess_char_4_14_a","golden":false,"tier":4,"row":12,"col":5,"items":[]},{"id":"chess_char_4_20_a","golden":false,"tier":4,"row":11,"col":8,"items":["chess_item_1_01_e_b","chess_item_5_02_e_a"]},{"id":"chess_char_3_17_b","golden":true,"tier":3,"row":10,"col":3,"items":["chess_item_3_04_e_a","chess_item_3_11_e_b"]},{"id":"chess_char_4_19_a","golden":false,"tier":4,"row":10,"col":4,"items":["chess_item_3_05_e_a"]},{"id":"chess_char_3_12_a","golden":false,"tier":3,"row":10,"col":5,"items":[]},{"id":"chess_char_6_17_a","golden":false,"tier":6,"row":10,"col":8,"items":["chess_item_5_01_e_a","chess_item_4_04_e_a"]},{"id":"chess_char_1_19_b","golden":true,"tier":1,"row":9,"col":9,"items":["chess_item_4_10_e_a","chess_item_3_11_e_a"]}],"bonds":[{"bondId":"preciShip","count":3,"active":true,"tier":2,"layers":177},{"bondId":"kazimierzShip","count":6,"active":true,"tier":2,"layers":52},{"bondId":"swiftShip","count":2,"active":true,"tier":1,"layers":33},{"bondId":"raidShip","count":2,"active":true,"tier":1,"layers":0},{"bondId":"soloShip","count":1,"active":true,"tier":1,"layers":0},{"bondId":"suntShip","count":2,"active":true,"tier":1,"layers":0},{"bondId":"indomShip","count":0,"active":false,"tier":0,"layers":18},{"bondId":"kjeragShip","count":0,"active":false,"tier":0,"layers":11},{"bondId":"visiShip","count":1,"active":false,"tier":0,"layers":8},{"bondId":"steadShip","count":0,"active":false,"tier":0,"layers":6},{"bondId":"sargonShip","count":1,"active":false,"tier":0,"layers":2},{"bondId":"arcaneShip","count":0,"active":false,"tier":0,"layers":2}],"stats":{"dmgDealt":5326588,"kills":308,"leaks":34,"gold":149,"refreshes":21,"merges":2,"itemsEquipped":16,"bossDamage":1413858,"activatedLayers":262,"lpLost":11,"perfectRounds":4},"title":{"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},"trophies":5,"reward":425}]},"coop4-abyss":{"victory":false,"roundsPassed":9,"hiddenReached":false,"hiddenCleared":false,"reason":"eliminated","teamLp":null,"modeId":"mode_multi_abyss","difficulty":"ABYSS","stageId":"act1autochess_m04","bossId":"boss_2","hiddenBossId":"boss_10","seed":103,"durationMs":94400,"players":[{"playerId":"p_0","seat":0,"name":"P0","isBot":false,"left":false,"alive":false,"victory":false,"roundsPassed":7,"eliminatedRound":8,"lp":0,"bandId":"band_mlyss","lineup":[],"bonds":[{"bondId":"sargonShip","count":0,"active":false,"tier":0,"layers":23},{"bondId":"steadShip","count":0,"active":false,"tier":0,"layers":12},{"bondId":"skillfulShip","count":0,"active":false,"tier":0,"layers":3}],"stats":{"dmgDealt":849856,"kills":77,"leaks":48,"gold":63,"refreshes":1,"merges":1,"itemsEquipped":6,"bossDamage":0,"activatedLayers":0,"lpLost":34,"perfectRounds":3},"title":{"id":"comment_6","name":"挥金如土","picId":"comment_icon_6","text":"消耗资金最多"},"trophies":1,"reward":149},{"playerId":"ai_0","seat":1,"name":"AI0","isBot":true,"left":false,"alive":false,"victory":false,"roundsPassed":7,"eliminatedRound":8,"lp":0,"bandId":"band_malkie","lineup":[],"bonds":[{"bondId":"preciShip","count":0,"active":false,"tier":0,"layers":19},{"bondId":"arcaneShip","count":0,"active":false,"tier":0,"layers":8},{"bondId":"kjeragShip","count":0,"active":false,"tier":0,"layers":7},{"bondId":"steadShip","count":0,"active":false,"tier":0,"layers":5},{"bondId":"sargonShip","count":0,"active":false,"tier":0,"layers":2},{"bondId":"indomShip","count":0,"active":false,"tier":0,"layers":2}],"stats":{"dmgDealt":644546,"kills":66,"leaks":59,"gold":62,"refreshes":2,"merges":2,"itemsEquipped":2,"bossDamage":0,"activatedLayers":0,"lpLost":37,"perfectRounds":3},"title":null,"trophies":1,"reward":149},{"playerId":"ai_1","seat":2,"name":"AI1","isBot":true,"left":false,"alive":false,"victory":false,"roundsPassed":9,"eliminatedRound":10,"lp":0,"bandId":"band_sarkazb","lineup":[],"bonds":[{"bondId":"steadShip","count":0,"active":false,"tier":0,"layers":59},{"bondId":"sargonShip","count":0,"active":false,"tier":0,"layers":56},{"bondId":"indomShip","count":0,"active":false,"tier":0,"layers":24},{"bondId":"deputShip","count":0,"active":false,"tier":0,"layers":10},{"bondId":"suntShip","count":0,"active":false,"tier":0,"layers":10},{"bondId":"kjeragShip","count":0,"active":false,"tier":0,"layers":8},{"bondId":"soloShip","count":0,"active":false,"tier":0,"layers":8}],"stats":{"dmgDealt":1583411,"kills":124,"leaks":60,"gold":85,"refreshes":6,"merges":4,"itemsEquipped":5,"bossDamage":0,"activatedLayers":0,"lpLost":47,"perfectRounds":5},"title":{"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},"trophies":2,"reward":191},{"playerId":"ai_2","seat":3,"name":"AI2","isBot":true,"left":false,"alive":false,"victory":false,"roundsPassed":7,"eliminatedRound":8,"lp":0,"bandId":"band_justin","lineup":[],"bonds":[{"bondId":"steadShip","count":0,"active":false,"tier":0,"layers":12},{"bondId":"swiftShip","count":0,"active":false,"tier":0,"layers":7},{"bondId":"sargonShip","count":0,"active":false,"tier":0,"layers":6},{"bondId":"kazimierzShip","count":0,"active":false,"tier":0,"layers":4},{"bondId":"preciShip","count":0,"active":false,"tier":0,"layers":2},{"bondId":"arcaneShip","count":0,"active":false,"tier":0,"layers":2}],"stats":{"dmgDealt":757780,"kills":82,"leaks":43,"gold":63,"refreshes":2,"merges":1,"itemsEquipped":8,"bossDamage":0,"activatedLayers":0,"lpLost":32,"perfectRounds":3},"title":{"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"},"trophies":1,"reward":149}]}}