dsh-eval-guard 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -13
- package/assets/commit0/instance.json +30 -0
- package/assets/commit0/workspace.zip +0 -0
- package/assets/featurebench/hidden/test_regression.py +690 -0
- package/assets/featurebench/instance.json +16 -0
- package/assets/featurebench/workspace.zip +0 -0
- package/assets/swe-bench-pro/hidden/test.patch +36 -0
- package/assets/swe-bench-pro/instance.json +10 -0
- package/assets/swe-bench-pro/workspace.zip +0 -0
- package/eval/README.md +18 -0
- package/eval/arena/arena/README.md +21 -0
- package/eval/arena/arena/bt.py +104 -0
- package/eval/arena/arena/clock.py +15 -0
- package/eval/arena/arena/core.py +265 -0
- package/eval/arena/arena/run.cmd +24 -0
- package/eval/arena/arena/run.sh +11 -0
- package/eval/arena/arena/schedule.py +79 -0
- package/eval/arena/arena/tournament.py +183 -0
- package/eval/arena/bomberman/bots/README.md +16 -0
- package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
- package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
- package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
- package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
- package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
- package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
- package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
- package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
- package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
- package/eval/arena/bomberman/tournament/README.md +11 -0
- package/eval/arena/bomberman/tournament/bt.py +56 -0
- package/eval/arena/bomberman/tournament/evaluate.py +199 -0
- package/eval/arena/bomberman/tournament/run.py +79 -0
- package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
- package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
- package/eval/arena/bomberman/workspace/README.md +29 -0
- package/eval/arena/bomberman/workspace/RULES.md +100 -0
- package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
- package/eval/arena/bomberman/workspace/clock.py +25 -0
- package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
- package/eval/arena/bomberman/workspace/engine/build.py +35 -0
- package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
- package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
- package/eval/arena/bomberman/workspace/run.cmd +28 -0
- package/eval/arena/bomberman/workspace/run.sh +16 -0
- package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
- package/eval/arena/ctf/bots/README.md +18 -0
- package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
- package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
- package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
- package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
- package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
- package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
- package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
- package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
- package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
- package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
- package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
- package/eval/arena/ctf/tournament/README.md +15 -0
- package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
- package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
- package/eval/arena/ctf/workspace/README.md +41 -0
- package/eval/arena/ctf/workspace/RULES.md +119 -0
- package/eval/arena/ctf/workspace/arena.py +186 -0
- package/eval/arena/ctf/workspace/baseline.cpp +257 -0
- package/eval/arena/ctf/workspace/clock.py +15 -0
- package/eval/arena/ctf/workspace/core.py +265 -0
- package/eval/arena/ctf/workspace/engine.py +389 -0
- package/eval/arena/ctf/workspace/run.cmd +24 -0
- package/eval/arena/ctf/workspace/run.sh +11 -0
- package/eval/arena/ctf/workspace/viewer.html +71 -0
- package/eval/arena/land/bots/README.md +17 -0
- package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
- package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
- package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
- package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
- package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
- package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
- package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
- package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
- package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
- package/eval/arena/land/tournament/make_replay_page.py +17 -0
- package/eval/arena/land/tournament/rate.py +290 -0
- package/eval/arena/land/tournament/round_robin.py +172 -0
- package/eval/arena/land/tournament/sparring.cpp +356 -0
- package/eval/arena/land/workspace/RULES.md +114 -0
- package/eval/arena/land/workspace/TASK.md +46 -0
- package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
- package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
- package/eval/arena/land/workspace/anchors/random.cpp +80 -0
- package/eval/arena/land/workspace/clock.py +64 -0
- package/eval/arena/land/workspace/engine/build.py +33 -0
- package/eval/arena/land/workspace/engine/game.py +273 -0
- package/eval/arena/land/workspace/engine/referee.py +308 -0
- package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
- package/eval/arena/land/workspace/starter/bot.cpp +82 -0
- package/eval/arena/land/workspace/viewer/viewer.html +210 -0
- package/eval/arena/tank/bots/README.md +24 -0
- package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
- package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
- package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
- package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
- package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
- package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
- package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
- package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
- package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
- package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
- package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
- package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
- package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
- package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
- package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
- package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
- package/eval/arena/tank/tournament/README.md +24 -0
- package/eval/arena/tank/tournament/bt.py +218 -0
- package/eval/arena/tank/tournament/final.json +202 -0
- package/eval/arena/tank/tournament/halftime.json +146 -0
- package/eval/arena/tank/tournament/progress.json +911 -0
- package/eval/arena/tank/tournament/schedule.py +100 -0
- package/eval/arena/tank/tournament/sparring.cpp +928 -0
- package/eval/arena/tank/tournament/tournament.py +1426 -0
- package/eval/arena/tank/workspace/README.md +169 -0
- package/eval/arena/tank/workspace/RULES.md +443 -0
- package/eval/arena/tank/workspace/arena.py +862 -0
- package/eval/arena/tank/workspace/baseline.cpp +259 -0
- package/eval/arena/tank/workspace/clock.py +103 -0
- package/eval/arena/tank/workspace/core.py +712 -0
- package/eval/arena/tank/workspace/engine.py +573 -0
- package/eval/arena/tank/workspace/history/.lock +0 -0
- package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
- package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
- package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
- package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
- package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
- package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
- package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
- package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
- package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
- package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
- package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
- package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
- package/eval/arena/tank/workspace/history/log.jsonl +103 -0
- package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
- package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
- package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
- package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
- package/eval/arena/tank/workspace/run.cmd +31 -0
- package/eval/arena/tank/workspace/run.sh +23 -0
- package/eval/arena/tank/workspace/tank.hpp +1762 -0
- package/eval/arena/tank/workspace/tank_main.cpp +215 -0
- package/eval/arena/tank/workspace/viewer.html +1150 -0
- package/eval/arena/warehouse/workspace/SPEC.md +130 -0
- package/eval/arena/warehouse/workspace/TASK.md +71 -0
- package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
- package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/clock.py +42 -0
- package/eval/arena/warehouse/workspace/src/common.h +88 -0
- package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
- package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
- package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
- package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
- package/eval/bomberman.py +20 -0
- package/eval/commit0.py +21 -0
- package/eval/common.py +61 -0
- package/eval/ctf.py +20 -0
- package/eval/featurebench.py +25 -0
- package/eval/land.py +20 -0
- package/eval/legacy/bomberman-prices.json +4 -0
- package/eval/legacy/bomberman.py +161 -0
- package/eval/legacy/commit0.py +91 -0
- package/eval/legacy/ctf-prices.json +4 -0
- package/eval/legacy/ctf.py +178 -0
- package/eval/legacy/featurebench.py +103 -0
- package/eval/legacy/land-prices.json +4 -0
- package/eval/legacy/land.py +177 -0
- package/eval/legacy/swe_bench_pro.py +116 -0
- package/eval/legacy/tank-prices.json +3 -0
- package/eval/legacy/tank.py +408 -0
- package/eval/legacy/warehouse-prices.json +4 -0
- package/eval/legacy/warehouse.py +365 -0
- package/eval/swe-bench-pro.py +25 -0
- package/eval/tank.py +20 -0
- package/eval/warehouse.py +20 -0
- package/lib/client/index.js +376 -63
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +684 -27
- package/package.json +2 -1
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>坦克大战回放</title>
|
|
7
|
+
<!--
|
|
8
|
+
坦克大战回放查看器。单个文件,不依赖任何外部资源,双击用浏览器打开。
|
|
9
|
+
数据:arena.py expand 展开出的逐帧 JSON(tank-frames-1)。三种来法:
|
|
10
|
+
1. arena.py play / arena.py html 生成的 HTML 里数据已经嵌好,打开就能看;
|
|
11
|
+
2. 把 *.frames.json 拖进页面;
|
|
12
|
+
3. 点右上角"打开文件"。
|
|
13
|
+
地址栏参数(?turn=12&p=0.5 这种写法,# 后面也行)见页面里的"帮助"。
|
|
14
|
+
-->
|
|
15
|
+
<style>
|
|
16
|
+
:root{
|
|
17
|
+
color-scheme:dark;
|
|
18
|
+
--bg:#0c0e11;--panel:#151920;--panel2:#1b2029;--line:#2a313c;--ink:#e9edf3;--ink2:#b3bcc8;--muted:#7c8794;
|
|
19
|
+
--c0:#d47a1a;--c0l:#f6b25c;--c1:#3d8fe8;--c1l:#9ccbff;--gold:#ffd24a;--bad:#ff5a4d;--warn:#f5c542;--good:#62d08a;
|
|
20
|
+
--board:600px;
|
|
21
|
+
}
|
|
22
|
+
*{box-sizing:border-box}
|
|
23
|
+
html,body{height:100%;margin:0}
|
|
24
|
+
body{background:var(--bg);color:var(--ink);font-family:"Segoe UI","Microsoft YaHei UI","Microsoft YaHei","PingFang SC","Noto Sans CJK SC",sans-serif;
|
|
25
|
+
font-size:clamp(12px,0.84vw,16px);overflow:hidden;-webkit-font-smoothing:antialiased}
|
|
26
|
+
body.narrow{overflow:auto}
|
|
27
|
+
#app{display:grid;grid-template-rows:auto minmax(0,1fr) auto;height:100vh}
|
|
28
|
+
#app>header{grid-row:1}#app>main{grid-row:2}#app>footer{grid-row:3}
|
|
29
|
+
body.narrow #app{height:auto;min-height:100vh}
|
|
30
|
+
b,strong{font-weight:600}
|
|
31
|
+
.mono{font-family:Consolas,"Cascadia Mono","Courier New",monospace}
|
|
32
|
+
|
|
33
|
+
header{display:flex;align-items:center;gap:1em;padding:.5em 1em;background:#101318;border-bottom:1px solid var(--line);min-height:3em}
|
|
34
|
+
header h1{margin:0;font-size:1.25em;letter-spacing:.08em;white-space:nowrap}
|
|
35
|
+
header h1 small{font-weight:400;color:var(--muted);font-size:.72em;letter-spacing:0;margin-left:.6em}
|
|
36
|
+
#meta{color:var(--ink2);font-size:.9em;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
37
|
+
#meta .warnchip{color:var(--bg);background:var(--warn);border-radius:.3em;padding:.05em .45em;margin-left:.6em;font-weight:600}
|
|
38
|
+
button,select{background:#232a35;color:var(--ink);border:1px solid #3a4452;border-radius:.4em;padding:.4em .75em;font:inherit;font-size:.92em;cursor:pointer;line-height:1.2}
|
|
39
|
+
button:hover,select:hover{background:#2e3745}
|
|
40
|
+
button:focus-visible,select:focus-visible{outline:2px solid var(--gold);outline-offset:1px}
|
|
41
|
+
|
|
42
|
+
main{display:grid;grid-template-columns:minmax(0,1fr) var(--board) minmax(0,1fr);gap:.75em;padding:.75em;min-height:0}
|
|
43
|
+
body.narrow main{display:flex;flex-direction:column;align-items:stretch}
|
|
44
|
+
aside{display:flex;flex-direction:column;gap:.75em;min-height:0;min-width:0;overflow:auto;scrollbar-width:thin}
|
|
45
|
+
aside>.team{flex:none}
|
|
46
|
+
body.narrow aside{overflow:visible}
|
|
47
|
+
body.narrow #stage{order:-1}
|
|
48
|
+
#stage{position:relative;width:var(--board);height:var(--board);align-self:center;justify-self:center;flex:none}
|
|
49
|
+
body.narrow #stage{margin:0 auto}
|
|
50
|
+
#board{width:100%;height:100%;display:block;border-radius:.4em}
|
|
51
|
+
.panel{background:var(--panel);border:1px solid var(--line);border-radius:.6em;padding:.7em .85em;min-height:0;min-width:0}
|
|
52
|
+
.panel h2{margin:0 0 .5em;font-size:.82em;font-weight:600;color:var(--muted);letter-spacing:.12em;display:flex;justify-content:space-between;gap:.6em;align-items:baseline}
|
|
53
|
+
.panel h2 span{letter-spacing:0;font-weight:400}
|
|
54
|
+
|
|
55
|
+
/* ---- 队伍卡片 ---- */
|
|
56
|
+
.team{border-left:.4em solid var(--tc);position:relative}
|
|
57
|
+
.team.s0{--tc:var(--c0);--tcl:var(--c0l)}
|
|
58
|
+
.team.s1{--tc:var(--c1);--tcl:var(--c1l)}
|
|
59
|
+
.thead{display:flex;align-items:center;gap:.6em;margin-bottom:.5em}
|
|
60
|
+
.mark{width:1.15em;height:1.15em;flex:none;border-radius:.2em;background:var(--tc);border:2px solid var(--tcl)}
|
|
61
|
+
.s1 .mark,.mark.s1{background:repeating-linear-gradient(135deg,var(--c1) 0 3px,#0d1626 3px 6px);border-color:var(--c1l)}
|
|
62
|
+
.mark.s0{background:var(--c0);border-color:var(--c0l)}
|
|
63
|
+
.tname{font-size:1.5em;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
64
|
+
.tseat{color:var(--muted);font-size:.85em;white-space:nowrap;margin-left:auto}
|
|
65
|
+
.tres{font-size:.85em;font-weight:700;padding:.1em .55em;border-radius:.3em;white-space:nowrap}
|
|
66
|
+
.tflt{font-size:.8em;font-weight:700;padding:.1em .5em;border-radius:.3em;white-space:nowrap;background:var(--bad);color:#fff}.tflt:empty{display:none}
|
|
67
|
+
.tres.win{background:var(--gold);color:#1a1300}
|
|
68
|
+
.tres.lose{background:#333b47;color:var(--ink2)}
|
|
69
|
+
.tres.draw{background:#333b47;color:var(--ink)}
|
|
70
|
+
.baserow{display:flex;align-items:center;gap:.6em;margin-bottom:.5em}
|
|
71
|
+
.baserow .lbl{color:var(--ink2);font-size:.88em;white-space:nowrap}
|
|
72
|
+
.hpbar{flex:1;display:flex;gap:2px;height:1.05em}
|
|
73
|
+
.hpbar i{flex:1;background:#262d38;border-radius:2px}
|
|
74
|
+
.hpbar i.on{background:var(--tc)}
|
|
75
|
+
.hpbar i.hit{background:#fff}
|
|
76
|
+
.hpnum{font-size:1.5em;font-weight:700;min-width:2.6em;text-align:right;font-family:Consolas,monospace}
|
|
77
|
+
.hpnum small{font-size:.6em;color:var(--muted);font-weight:400}
|
|
78
|
+
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.4em;margin-bottom:.5em}
|
|
79
|
+
.stats div{background:var(--panel2);border-radius:.35em;padding:.25em .5em;min-width:0}
|
|
80
|
+
.stats b{display:block;font-size:1.2em;font-family:Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
81
|
+
.stats span{display:block;font-size:.72em;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
82
|
+
.stats .near b{color:var(--warn)}.stats .over b{color:var(--bad)}
|
|
83
|
+
.trow{display:grid;grid-template-columns:3.1em minmax(0,1fr) auto;gap:.6em;align-items:center;padding:.3em 0;border-top:1px solid #222935}
|
|
84
|
+
.trow canvas{width:3.1em;height:3.1em;display:block;background:#0e1014;border-radius:.3em}
|
|
85
|
+
.trow.dead canvas{opacity:.35}
|
|
86
|
+
.tl1{display:flex;align-items:baseline;gap:.5em;white-space:nowrap}
|
|
87
|
+
.tl1 b{font-size:1.05em}
|
|
88
|
+
.tl1 .pos{color:var(--muted);font-size:.85em;font-family:Consolas,monospace}
|
|
89
|
+
.pips{display:inline-flex;gap:2px;vertical-align:middle}
|
|
90
|
+
.pips i{width:.7em;height:.55em;border:1px solid var(--good);border-radius:1px}
|
|
91
|
+
.pips i.on{background:var(--good)}
|
|
92
|
+
.lv{color:var(--gold);font-size:.85em;letter-spacing:.05em}
|
|
93
|
+
.lv.z{color:var(--muted)}
|
|
94
|
+
.tl2{color:var(--ink2);font-size:.82em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
95
|
+
.tl2 .rel{color:var(--warn)}.tl2 .ded{color:var(--bad)}
|
|
96
|
+
.ord{font-size:.95em;background:var(--panel2);border:1px solid #333c49;border-radius:.35em;padding:.25em .6em;white-space:nowrap;min-width:5.6em;text-align:center}
|
|
97
|
+
.ord .ar{font-family:"Segoe UI Symbol",Consolas,monospace;font-weight:700;color:var(--tcl);font-size:1.1em}
|
|
98
|
+
.ord.none{color:var(--muted);border-style:dashed;background:none}
|
|
99
|
+
.ord .x{color:var(--bad);font-weight:700}
|
|
100
|
+
.ord small{color:var(--muted)}
|
|
101
|
+
|
|
102
|
+
/* ---- 中间:回合信息 ---- */
|
|
103
|
+
#turnbox{flex:1 0 auto;display:flex;flex-direction:column;justify-content:center;gap:.55em;text-align:center}
|
|
104
|
+
#turnbig{font-size:2.6em;font-weight:700;line-height:1;font-family:Consolas,"Microsoft YaHei UI","Microsoft YaHei",sans-serif}
|
|
105
|
+
#turnbig small{font-size:.42em;color:var(--muted);font-weight:400;font-family:inherit}
|
|
106
|
+
#phases{display:flex;gap:.35em;justify-content:center}
|
|
107
|
+
#phases i{font-style:normal;font-size:.8em;padding:.15em .7em;border-radius:1em;background:var(--panel2);color:var(--muted);border:1px solid transparent}
|
|
108
|
+
#phases i.on{color:#111;background:var(--gold)}
|
|
109
|
+
#supplyline{color:var(--ink2);font-size:.92em}
|
|
110
|
+
#supplyline b{color:var(--gold);font-family:Consolas,monospace;font-size:1.15em}
|
|
111
|
+
#legend{width:100%;display:block}
|
|
112
|
+
@media (max-height:880px){body:not(.narrow) #legend{display:none}}
|
|
113
|
+
|
|
114
|
+
/* ---- 右栏 ---- */
|
|
115
|
+
#evpanel{flex:2.3;display:flex;flex-direction:column}
|
|
116
|
+
#events{flex:1;overflow:auto;font-size:.95em;line-height:1.62;min-height:3em}
|
|
117
|
+
#events .grp{color:var(--muted);font-size:.78em;letter-spacing:.1em;margin-top:.35em;border-bottom:1px solid #222935}
|
|
118
|
+
#events .ev{display:flex;gap:.5em;align-items:baseline}
|
|
119
|
+
#events .ev .ic{width:1.2em;text-align:center;flex:none;font-weight:700}
|
|
120
|
+
#events .ev.big{font-weight:600}
|
|
121
|
+
#events .none{color:var(--muted)}
|
|
122
|
+
.tk{white-space:nowrap;font-weight:600}
|
|
123
|
+
.tk i{display:inline-block;width:.62em;height:.62em;margin-right:.25em;border-radius:1px;vertical-align:baseline}
|
|
124
|
+
.tk.s0{color:var(--c0l)}.tk.s0 i{background:var(--c0l)}
|
|
125
|
+
.tk.s1{color:var(--c1l)}.tk.s1 i{border:2px solid var(--c1l);border-radius:50%}
|
|
126
|
+
.c-bad{color:var(--bad)}.c-gold{color:var(--gold)}.c-mut{color:var(--muted)}.c-ink2{color:var(--ink2)}
|
|
127
|
+
#thinkpanel{flex:none;position:relative}
|
|
128
|
+
#think{width:100%;height:11.5em;display:block;cursor:pointer}
|
|
129
|
+
#thinknote{font-size:.76em;color:var(--muted);margin-top:.3em;line-height:1.5}
|
|
130
|
+
#dbgpanel{flex:1;display:flex;flex-direction:column}
|
|
131
|
+
#debug{flex:1;overflow:auto;font-size:.85em;line-height:1.6;min-height:3em}
|
|
132
|
+
#debug .fault{color:var(--bad);cursor:pointer}
|
|
133
|
+
#debug .fault:hover{text-decoration:underline}
|
|
134
|
+
#debug .fault.now{font-weight:700}
|
|
135
|
+
#debug .ln{font-family:Consolas,"Microsoft YaHei",monospace;white-space:pre-wrap;word-break:break-all}
|
|
136
|
+
#debug .sub{color:var(--muted);font-size:.9em;margin-top:.3em}
|
|
137
|
+
|
|
138
|
+
/* ---- 底部控制条 ---- */
|
|
139
|
+
footer{display:flex;align-items:center;gap:.8em;padding:.55em 1em .65em;background:#101318;border-top:1px solid var(--line)}
|
|
140
|
+
footer .btns{display:flex;gap:.3em;flex:none}
|
|
141
|
+
footer button{min-width:2.6em;font-size:1em;padding:.5em .6em}
|
|
142
|
+
#bplay{min-width:5.2em;background:#35507a;border-color:#4a6da3;font-weight:600}
|
|
143
|
+
#bplay:hover{background:#40609180}
|
|
144
|
+
#tlwrap{flex:1;min-width:8em;position:relative}
|
|
145
|
+
#timeline{width:100%;height:4.6em;display:block;cursor:pointer;touch-action:none}
|
|
146
|
+
#tllegend{flex:none;font-size:.76em;color:var(--ink2);line-height:1.5;white-space:nowrap}
|
|
147
|
+
#tllegend b{display:inline-block;width:1.2em;text-align:center}
|
|
148
|
+
#turnlabel{flex:none;font-family:Consolas,monospace;font-size:1.25em;min-width:5.5em;text-align:right}
|
|
149
|
+
#turnlabel small{color:var(--muted);font-size:.7em}
|
|
150
|
+
.tip{position:fixed;z-index:20;background:#000d;border:1px solid #445;border-radius:.4em;padding:.35em .6em;font-size:.82em;line-height:1.5;pointer-events:none;display:none;max-width:26em;white-space:pre-line}
|
|
151
|
+
body.narrow footer{flex-wrap:wrap;position:sticky;bottom:0}
|
|
152
|
+
body.narrow #tllegend{display:none}
|
|
153
|
+
body.narrow #tlwrap{flex-basis:100%;order:9}
|
|
154
|
+
body.clean header,body.clean #tllegend{display:none}
|
|
155
|
+
|
|
156
|
+
/* ---- 覆盖层 ---- */
|
|
157
|
+
#endcard{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:#05070acc;border-radius:.4em}
|
|
158
|
+
#endcard.on{display:flex}
|
|
159
|
+
#endin{background:#12161ccc;border:2px solid var(--gold);border-radius:.8em;padding:1.6em 2.4em;text-align:center;max-width:86%;box-shadow:0 0 0 6px #0008}
|
|
160
|
+
#endin .ttl{font-size:.95em;letter-spacing:.4em;color:var(--muted);margin-bottom:.5em}
|
|
161
|
+
#endin .who{font-size:2.8em;font-weight:800;line-height:1.15;word-break:break-all}
|
|
162
|
+
#endin .why{font-size:1.25em;color:var(--ink);margin:.5em 0 .9em}
|
|
163
|
+
#endin table{margin:0 auto .9em;border-collapse:collapse;font-size:1.05em}
|
|
164
|
+
#endin td{padding:.15em .9em;font-family:Consolas,"Microsoft YaHei",monospace}
|
|
165
|
+
#endin td.k{color:var(--muted);font-family:inherit;font-size:.85em}
|
|
166
|
+
#endin td.l{text-align:right}#endin td.r{text-align:left}
|
|
167
|
+
#endin .foot{color:var(--muted);font-size:.8em;margin-top:.7em}
|
|
168
|
+
#drop{position:absolute;inset:0;display:none;align-items:center;justify-content:center;text-align:center;border:3px dashed #3a4452;border-radius:.6em;background:var(--panel);padding:2em;line-height:1.9}
|
|
169
|
+
#drop.on{display:flex}
|
|
170
|
+
#drop b{font-size:1.5em}
|
|
171
|
+
#drop code{background:#0008;padding:.1em .4em;border-radius:.3em;font-family:Consolas,monospace}
|
|
172
|
+
body.dragging #stage{outline:3px dashed var(--gold);outline-offset:4px}
|
|
173
|
+
#help{position:fixed;inset:0;background:#000b;display:none;align-items:center;justify-content:center;z-index:30}
|
|
174
|
+
#help.on{display:flex}
|
|
175
|
+
#helpin{background:var(--panel);border:1px solid var(--line);border-radius:.7em;padding:1.2em 1.6em;max-width:56em;max-height:90vh;overflow:auto;line-height:1.7;font-size:.95em}
|
|
176
|
+
#helpin h3{margin:.6em 0 .2em;font-size:1em;color:var(--gold)}
|
|
177
|
+
#helpin table{border-collapse:collapse}
|
|
178
|
+
#helpin td{padding:.05em 1.2em .05em 0;vertical-align:top}
|
|
179
|
+
#helpin kbd,#helpin code{background:#0009;border:1px solid #3a4452;border-radius:.3em;padding:0 .4em;font-family:Consolas,monospace;font-size:.92em;white-space:nowrap}
|
|
180
|
+
#err{position:fixed;left:0;right:0;top:0;background:#7a1512;color:#fff;padding:.4em 1em;font-size:.9em;z-index:40;display:none;white-space:pre-wrap}
|
|
181
|
+
</style>
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
<div id="err"></div>
|
|
185
|
+
<div id="app">
|
|
186
|
+
<header>
|
|
187
|
+
<h1>坦克大战<small>回放</small></h1>
|
|
188
|
+
<div id="meta">把 <span class="mono">*.frames.json</span> 拖进来,或点"打开文件"</div>
|
|
189
|
+
<button id="bopen" title="打开展开后的逐帧 JSON(O)">打开文件</button>
|
|
190
|
+
<button id="bhelp" title="快捷键和地址栏参数(H)">帮助</button>
|
|
191
|
+
</header>
|
|
192
|
+
<main>
|
|
193
|
+
<aside id="left">
|
|
194
|
+
<div class="panel team s1" id="team1"></div>
|
|
195
|
+
<div class="panel" id="turnbox">
|
|
196
|
+
<div id="turnbig">–</div>
|
|
197
|
+
<div id="phases"><i>开炮</i><i>小步 1</i><i>小步 2</i><i>回合末</i></div>
|
|
198
|
+
<div id="supplyline"></div>
|
|
199
|
+
<canvas id="legend"></canvas>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="panel team s0" id="team0"></div>
|
|
202
|
+
</aside>
|
|
203
|
+
<section id="stage">
|
|
204
|
+
<canvas id="board"></canvas>
|
|
205
|
+
<div id="endcard"><div id="endin"></div></div>
|
|
206
|
+
<div id="drop"><div><b>把展开后的回放拖到这里</b><br>
|
|
207
|
+
文件由 <code>arena.py expand 回放.json</code> 生成(<code>*.frames.json</code>)。<br>
|
|
208
|
+
也可以直接用 <code>arena.py html 回放.json</code> 生成一个打开就能看的网页。<br>
|
|
209
|
+
<button id="bopen2" style="margin-top:.8em;font-size:1.05em">打开文件</button></div></div>
|
|
210
|
+
</section>
|
|
211
|
+
<aside id="right">
|
|
212
|
+
<div class="panel" id="evpanel"><h2>本回合事件<span id="evturn"></span></h2><div id="events"></div></div>
|
|
213
|
+
<div class="panel" id="thinkpanel"><h2>每回合思考用量<span id="thinkunit"></span></h2><canvas id="think"></canvas><div id="thinknote"></div></div>
|
|
214
|
+
<div class="panel" id="dbgpanel"><h2>故障与调试输出<span id="dbgturn"></span></h2><div id="debug"></div></div>
|
|
215
|
+
</aside>
|
|
216
|
+
</main>
|
|
217
|
+
<footer>
|
|
218
|
+
<div class="btns">
|
|
219
|
+
<button id="bfirst" title="回到开局(Home)">⏮</button>
|
|
220
|
+
<button id="bpk" title="上一个关键事件([)">◀◆</button>
|
|
221
|
+
<button id="bprev" title="上一回合(←)">◀</button>
|
|
222
|
+
<button id="bplay" title="播放 / 暂停(空格)">▶ 播放</button>
|
|
223
|
+
<button id="bnext" title="下一回合(→)">▶</button>
|
|
224
|
+
<button id="bnk" title="下一个关键事件(])">◆▶</button>
|
|
225
|
+
<button id="blast" title="跳到终局(End)">⏭</button>
|
|
226
|
+
</div>
|
|
227
|
+
<select id="speed" title="播放速度(+ / -)"></select>
|
|
228
|
+
<div id="tllegend"></div>
|
|
229
|
+
<div id="tlwrap"><canvas id="timeline"></canvas></div>
|
|
230
|
+
<div id="turnlabel"></div>
|
|
231
|
+
</footer>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="tip" id="tip"></div>
|
|
234
|
+
<div id="help"><div id="helpin"></div></div>
|
|
235
|
+
<input type="file" id="file" accept=".json,.gz,application/json" hidden>
|
|
236
|
+
|
|
237
|
+
<script>
|
|
238
|
+
"use strict";
|
|
239
|
+
// 数据内嵌的位置:arena.py 把下一行的占位换成展开后的 JSON。
|
|
240
|
+
const EMBED=/*__TANK_DATA__*/null;
|
|
241
|
+
|
|
242
|
+
// ====================================================================
|
|
243
|
+
// 常量与小工具
|
|
244
|
+
// ====================================================================
|
|
245
|
+
const $=id=>document.getElementById(id);
|
|
246
|
+
const DV={U:[0,-1],D:[0,1],L:[-1,0],R:[1,0]},OPP={U:'D',D:'U',L:'R',R:'L'};
|
|
247
|
+
const ANG={U:0,R:Math.PI/2,D:Math.PI,L:-Math.PI/2};
|
|
248
|
+
const ARROW={U:'↑',D:'↓',L:'←',R:'→'};
|
|
249
|
+
const TYPE_CN={fast:'快速',fire:'火力',armor:'装甲'};
|
|
250
|
+
const STYLE_CN={'-1':'按种子轮换',0:'开阔',1:'多砖',2:'多水',3:'多钢'};
|
|
251
|
+
const PAL=[{m:'#d47a1a',l:'#f6b25c',d:'#7a430b',k:'#2a1704'},{m:'#3d8fe8',l:'#9ccbff',d:'#1c4a82',k:'#0d1626'}];
|
|
252
|
+
const GOLD='#ffd24a',BAD='#ff5a4d',WARN='#f5c542';
|
|
253
|
+
const FONT='"Segoe UI","Microsoft YaHei UI","Microsoft YaHei",sans-serif',NUMFONT='Consolas,"Courier New",monospace';
|
|
254
|
+
const SPEEDS=[0.5,1,2,4,8,16];
|
|
255
|
+
const TAIL_MAX=1.3; // 停在某一回合后,回合末的特效再播多久(单位:回合)
|
|
256
|
+
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
|
|
257
|
+
const esc=t=>String(t).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
258
|
+
const fmtMs=v=>v==null?'–':v>=100?v.toFixed(0):v>=10?v.toFixed(1):v.toFixed(2);
|
|
259
|
+
|
|
260
|
+
// 地址栏参数:?a=1&b=2 和 #a=1&b=2 都认
|
|
261
|
+
const ARGS=(()=>{const o={};for(const part of[location.search.slice(1),location.hash.slice(1)])
|
|
262
|
+
for(const kv of part.split('&')){if(!kv)continue;const i=kv.indexOf('=');o[decodeURIComponent(i<0?kv:kv.slice(0,i))]=i<0?'1':decodeURIComponent(kv.slice(i+1))}return o})();
|
|
263
|
+
const argNum=(k,d)=>k in ARGS&&ARGS[k]!==''&&isFinite(+ARGS[k])?+ARGS[k]:d;
|
|
264
|
+
|
|
265
|
+
window.addEventListener('error',e=>{const el=$('err');el.style.display='block';el.textContent='查看器出错:'+e.message+'('+(e.filename||'').split('/').pop()+':'+e.lineno+')'});
|
|
266
|
+
|
|
267
|
+
// ====================================================================
|
|
268
|
+
// 状态
|
|
269
|
+
// ====================================================================
|
|
270
|
+
let D=null,F=[],N=0; // 数据、帧、最后一帧的下标
|
|
271
|
+
let simCache=[],facing=[],keys=[],keyFrames=[],stderrByTurn=[],faultList=[];
|
|
272
|
+
let cur=0,prog=0,tail=TAIL_MAX; // 当前帧;正在播放第 cur+1 回合的进度(0 = 停在 cur);停下后经过的时间
|
|
273
|
+
let playing=false,stepping=false,speed=2,showEnd=true,showCoords=true,lastT=0,dirty=true;
|
|
274
|
+
let shownTurn=-1,hoverThink=-1,hoverTl=-1;
|
|
275
|
+
let holdTail=false; // 地址栏给了 tail= 时画面定在那一刻,直到有任何操作
|
|
276
|
+
|
|
277
|
+
// ====================================================================
|
|
278
|
+
// 载入
|
|
279
|
+
// ====================================================================
|
|
280
|
+
function load(data){
|
|
281
|
+
if(!data||data.format!=='tank-frames-1'){
|
|
282
|
+
const why=data&&data.format==='tank-replay-1'
|
|
283
|
+
?'这是原始回放(tank-replay-1),查看器读的是展开后的逐帧 JSON。\n先运行 arena.py expand 回放.json 得到 *.frames.json,或者直接 arena.py html 回放.json 生成网页。'
|
|
284
|
+
:'这个文件不是展开后的回放(需要 format 为 tank-frames-1)。';
|
|
285
|
+
alert(why);return false;
|
|
286
|
+
}
|
|
287
|
+
D=data;F=D.frames;N=F.length-1;simCache=[];cur=0;prog=0;tail=TAIL_MAX;playing=false;stepping=false;shownTurn=-1;showEnd=true;
|
|
288
|
+
// 朝向:规则里坦克没有朝向,这里按"最近一次移动或开炮的方向"画,只为好看
|
|
289
|
+
facing=[F[0].tanks.map(t=>t.seat?'D':'U')];
|
|
290
|
+
for(let k=1;k<=N;k++){const f=facing[k-1].slice(),fr=F[k];
|
|
291
|
+
fr.tanks.forEach((t,i)=>{const was=F[k-1].tanks[i].alive,o=fr.orders?fr.orders[t.seat][i%3]:'S';
|
|
292
|
+
if(was&&o&&o!=='S')f[i]=o[0]==='F'?o[1]:o[o.length-1];
|
|
293
|
+
if(!t.alive||!was)f[i]=t.seat?'D':'U'});
|
|
294
|
+
facing.push(f)}
|
|
295
|
+
// 关键事件:击毁、基地掉血、吃星、故障。lane = 得利的一方(故障记在出故障的一方)
|
|
296
|
+
keys=F.map(()=>[]);
|
|
297
|
+
for(let k=1;k<=N;k++)for(const e of F[k].events){
|
|
298
|
+
if(e.type==='death')keys[k].push({kind:'kill',lane:1-seatOf(e.tank),text:`${tname(e.tank)}被毁`+(e.by>=0?'(压出生点)':'')});
|
|
299
|
+
else if(e.type==='base_hit')keys[k].push({kind:'base',lane:1-e.seat,text:`${D.names[e.seat]} 的基地被打中`});
|
|
300
|
+
else if(e.type==='star')keys[k].push({kind:'star',lane:seatOf(e.tank),text:`${tname(e.tank)}吃星升到 ${e.level} 级`});
|
|
301
|
+
}
|
|
302
|
+
faultList=(D.faults||[]).slice().sort((a,b)=>a.turn-b.turn);
|
|
303
|
+
for(const f of faultList)keys[clamp(f.turn,0,N)].push({kind:'fault',lane:f.seat,text:`${D.names[f.seat]} 故障:${faultKind(f.kind)}`});
|
|
304
|
+
keyFrames=[];keys.forEach((v,k)=>{if(v.length)keyFrames.push(k)});
|
|
305
|
+
stderrByTurn={};
|
|
306
|
+
(D.stderr||[]).forEach((list,seat)=>{for(const[turn,line]of list||[])(stderrByTurn[turn]=stderrByTurn[turn]||[]).push([seat,line])});
|
|
307
|
+
$('drop').classList.remove('on');
|
|
308
|
+
buildStatic();layout();
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
const seatOf=i=>i<3?0:1;
|
|
312
|
+
const typeOf=i=>['fast','fire','armor'][i%3];
|
|
313
|
+
const tname=i=>`${D.names[seatOf(i)]} 的${TYPE_CN[typeOf(i)]}`;
|
|
314
|
+
const faultKind=k=>({timeout:'超出思考预算',illegal:'输出不合法',exit:'程序崩溃或退出'}[k]||k);
|
|
315
|
+
function reasonText(r){
|
|
316
|
+
const M={base:'对方基地被毁',hp:'到回合上限,比基地血量',kills:'到回合上限,比击毁数',stars:'到回合上限,比吃星数',damage:'到回合上限,比基地伤害',draw:'到回合上限,各项都相同',fault:'对手故障判负'};
|
|
317
|
+
if(r&&r.startsWith('both_')){const x=r.slice(5);return '两个基地同回合被毁,'+(x==='draw'?'各项都相同':'比'+({kills:'击毁数',stars:'吃星数',damage:'基地伤害',hp:'基地血量'}[x]||x))}
|
|
318
|
+
return M[r]||r||'';
|
|
319
|
+
}
|
|
320
|
+
function nextSupply(turn){ // 下一批补给在第几回合末出现(严格大于 turn)
|
|
321
|
+
const P=D.params,a=P.supply_first,per=P.supply_period;
|
|
322
|
+
if(a==null||!per)return null;
|
|
323
|
+
const t=turn<a?a:a+(Math.floor((turn-a)/per)+1)*per;
|
|
324
|
+
return t>(P.max_turns||1e9)?null:t;
|
|
325
|
+
}
|
|
326
|
+
const maxHp=t=>(D.params[t.type+'_hp']||1)+(t.level||0)*(D.params.level_hp||0);
|
|
327
|
+
|
|
328
|
+
// ====================================================================
|
|
329
|
+
// 重建一回合的两个小步
|
|
330
|
+
// 帧里只有回合前后的局面和事件。这里按引擎 step() 的顺序重算一遍,得到每辆车、每发子弹
|
|
331
|
+
// 在每个小步的位置和下场,动画和静态标记都从它出。?check=1 会逐回合核对重算结果和下一帧是否一致。
|
|
332
|
+
// ====================================================================
|
|
333
|
+
function simulate(k){
|
|
334
|
+
if(simCache[k])return simCache[k];
|
|
335
|
+
const A=F[k-1],B=F[k],n=D.size,P=D.params;
|
|
336
|
+
const grid=A.map.map(r=>r.split(''));
|
|
337
|
+
const cell=(x,y)=>x<0||y<0||x>=n||y>=n?'X':grid[y][x];
|
|
338
|
+
const T=A.tanks.map(t=>({id:t.id,seat:t.seat,on:t.alive,x:t.x,y:t.y,pos:[[t.x,t.y]],bump:[null,null],mv:[null,null],fire:null,hitT:[]}));
|
|
339
|
+
const fx=[],walls=[],baseT=[[],[]];
|
|
340
|
+
// 基地限伤(base_hit_cap):每个基地每回合最多扣这么多血,两个小步合计、敌我的子弹合在一起按先后数;
|
|
341
|
+
// 超出的子弹照样消失但不扣血。baseT 只记真正扣了血的那几次命中的时刻。
|
|
342
|
+
const baseHits=[0,0],baseLeft=[A.base_hp[0],A.base_hp[1]],cap=P.base_hit_cap||0;
|
|
343
|
+
const bullets=A.bullets.map(b=>({x:b.x,y:b.y,dir:b.dir,owner:b.owner,steel:b.steel,fresh:false,x0:b.x,y0:b.y,end:0,endS:9,kind:null}));
|
|
344
|
+
for(const e of B.events)if(e.type==='fire'){
|
|
345
|
+
const t=T[e.tank],at=A.tanks[e.tank];t.fire=e.dir;
|
|
346
|
+
bullets.push({x:t.x,y:t.y,dir:e.dir,owner:e.tank,steel:at.level>=2&&P.level2_steel!==0,fresh:true,x0:t.x,y0:t.y,end:0,endS:9,kind:null});
|
|
347
|
+
fx.push({t:0,type:'muzzle',x:t.x,y:t.y,dir:e.dir,seat:at.seat});
|
|
348
|
+
}
|
|
349
|
+
A.tanks.forEach((t,i)=>{
|
|
350
|
+
if(!t.alive||!B.orders)return;
|
|
351
|
+
const o=B.orders[t.seat][i%3]||'S';
|
|
352
|
+
if(o==='S'||o[0]==='F')return;
|
|
353
|
+
T[i].mv[0]=o[0];
|
|
354
|
+
if(o.length>1&&(P[t.type+'_speed']||1)>=2)T[i].mv[1]=o[1];
|
|
355
|
+
});
|
|
356
|
+
const maps=[];
|
|
357
|
+
for(let s=0;s<2;s++){
|
|
358
|
+
const at=(x,y)=>T.find(t=>t.on&&t.x===x&&t.y===y);
|
|
359
|
+
// (a) 坦克移动
|
|
360
|
+
const want=[];
|
|
361
|
+
for(const t of T){const d=t.mv[s];if(!t.on||!d)continue;
|
|
362
|
+
const tx=t.x+DV[d][0],ty=t.y+DV[d][1];
|
|
363
|
+
if(cell(tx,ty)==='.'&&!at(tx,ty))want.push({t,tx,ty});else t.bump[s]=d}
|
|
364
|
+
const moved=[];
|
|
365
|
+
for(const w of want){if(want.some(o=>o!==w&&o.tx===w.tx&&o.ty===w.ty))w.t.bump[s]=w.t.mv[s];else moved.push(w)}
|
|
366
|
+
for(const w of moved){w.fx=w.t.x;w.fy=w.t.y;w.t.x=w.tx;w.t.y=w.ty}
|
|
367
|
+
for(const t of T)t.pos.push([t.x,t.y]);
|
|
368
|
+
// (b) 子弹前进
|
|
369
|
+
const live=bullets.filter(b=>!b.end);
|
|
370
|
+
for(const b of live){b.px=b.x;b.py=b.y;b.x+=DV[b.dir][0];b.y+=DV[b.dir][1]}
|
|
371
|
+
// (c) 判定。back = 子弹在这一小步里飞到一半就没了的那部分(画面上撞在墙面、车身上,而不是格子中心)
|
|
372
|
+
const kill=(b,kind,back)=>{if(b.end)return (b.endS)/2;b.end=s+1;b.kind=kind;b.endS=s+1-back;return b.endS/2};
|
|
373
|
+
if(P.bullet_cancel!==0)for(let a=0;a<live.length;a++)for(let c=a+1;c<live.length;c++){
|
|
374
|
+
const ba=live[a],bb=live[c];
|
|
375
|
+
if(bb.dir===OPP[ba.dir]&&ba.px===bb.x&&ba.py===bb.y){
|
|
376
|
+
kill(ba,'cross',.5);const t=kill(bb,'cross',.5);
|
|
377
|
+
fx.push({t,type:'cancel',x:(ba.x+ba.px)/2,y:(ba.y+ba.py)/2})}
|
|
378
|
+
}
|
|
379
|
+
for(const w of moved)for(const b of live){
|
|
380
|
+
if(!b.end&&b.x===w.fx&&b.y===w.fy&&b.px===w.t.x&&b.py===w.t.y){
|
|
381
|
+
const t=kill(b,'swap',.5);w.t.hitT.push(t);fx.push({t,type:'hit',x:(b.x+b.px)/2,y:(b.y+b.py)/2,tank:w.t.id})}
|
|
382
|
+
}
|
|
383
|
+
const brk=[],stl=[];
|
|
384
|
+
for(const b of live){
|
|
385
|
+
if(b.end)continue;
|
|
386
|
+
const c=cell(b.x,b.y);
|
|
387
|
+
if(c==='.'){const v=at(b.x,b.y);if(v){const t=kill(b,'hit',.3);v.hitT.push(t);fx.push({t,type:'hit',x:b.x,y:b.y,tank:v.id})}}
|
|
388
|
+
else if(c==='~'){}
|
|
389
|
+
else if(c==='#'){const t=kill(b,'brick',.45);brk.push([b.x,b.y,t])}
|
|
390
|
+
else if(c==='@'){const t=kill(b,'steel',.45);if(b.steel)stl.push([b.x,b.y,t]);else fx.push({t,type:'ding',x:b.x,y:b.y,dir:b.dir})}
|
|
391
|
+
else if(c==='X'){const t=kill(b,'out',.5);fx.push({t,type:'out',x:b.x,y:b.y,dir:b.dir})}
|
|
392
|
+
else{const seat=+c,t=kill(b,'base',.4);baseHits[seat]++;
|
|
393
|
+
const dmg=baseLeft[seat]>0&&!(cap&&baseHits[seat]>cap);
|
|
394
|
+
if(dmg){baseLeft[seat]--;baseT[seat].push(t)}
|
|
395
|
+
fx.push({t,type:'basehit',x:b.x,y:b.y,seat,dmg})}
|
|
396
|
+
}
|
|
397
|
+
for(const[list,ch,kind]of[[brk,'#','brick'],[stl,'@','steel']])for(const[x,y,t]of list)if(grid[y][x]===ch){
|
|
398
|
+
grid[y][x]='.';walls.push({x,y,kind,sub:s+1});fx.push({t,type:'debris',x,y,kind})}
|
|
399
|
+
if(P.bullet_cancel!==0){
|
|
400
|
+
const left=live.filter(b=>!b.end);
|
|
401
|
+
for(let a=1;a<left.length;a++)for(let j=0;j<a;j++)if(left[j].x===left[a].x&&left[j].y===left[a].y){
|
|
402
|
+
kill(left[a],'same',0);kill(left[j],'same',0);fx.push({t:(s+1)/2,type:'cancel',x:left[a].x,y:left[a].y});break}
|
|
403
|
+
}
|
|
404
|
+
maps.push(grid.map(r=>r.join('')));
|
|
405
|
+
}
|
|
406
|
+
// 回合末的事:都记在 t = 1(两个小步走完的那一刻)
|
|
407
|
+
for(const e of B.events){
|
|
408
|
+
if(e.type==='death')fx.push({t:1,type:'death',x:e.x,y:e.y,tank:e.tank,by:e.by});
|
|
409
|
+
else if(e.type==='respawn')fx.push({t:1,type:'respawn',x:e.x,y:e.y,tank:e.tank});
|
|
410
|
+
else if(e.type==='star')fx.push({t:1,type:'star',x:e.x,y:e.y,tank:e.tank,level:e.level});
|
|
411
|
+
else if(e.type==='supply')fx.push({t:1,type:'supply',x:e.x,y:e.y});
|
|
412
|
+
else if(e.type==='bullet_cleared')fx.push({t:1,type:'cleared',x:e.x,y:e.y});
|
|
413
|
+
}
|
|
414
|
+
return simCache[k]={T,bullets,walls,maps,fx,baseT};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 自查:重算结果和下一帧对不上的地方。返回问题列表(空 = 全部一致)。
|
|
418
|
+
function selfCheck(){
|
|
419
|
+
const bad=[];
|
|
420
|
+
for(let k=1;k<=N;k++){
|
|
421
|
+
const A=F[k-1],B=F[k],S=simulate(k),ev=B.events,note=m=>{if(bad.length<40)bad.push(`回合 ${k}:${m}`)};
|
|
422
|
+
const touched=new Set(ev.filter(e=>['death','respawn','star'].includes(e.type)).map(e=>e.tank));
|
|
423
|
+
S.T.forEach((t,i)=>{
|
|
424
|
+
if(!t.on||touched.has(i)||!B.tanks[i].alive)return;
|
|
425
|
+
if(t.x!==B.tanks[i].x||t.y!==B.tanks[i].y)note(`${i} 号车位置 重算 (${t.x},${t.y}) 帧里 (${B.tanks[i].x},${B.tanks[i].y})`);
|
|
426
|
+
if(A.tanks[i].hp-t.hitT.length!==B.tanks[i].hp)note(`${i} 号车血量 重算 ${A.tanks[i].hp-t.hitT.length} 帧里 ${B.tanks[i].hp}`);
|
|
427
|
+
});
|
|
428
|
+
const blk=ev.filter(e=>e.type==='blocked').map(e=>e.tank+':'+e.sub).sort().join(),mine=[];
|
|
429
|
+
S.T.forEach((t,i)=>t.bump.forEach((d,s)=>{if(d)mine.push(i+':'+(s+1))}));
|
|
430
|
+
if(blk!==mine.sort().join())note(`没走成 重算 [${mine}] 事件 [${blk}]`);
|
|
431
|
+
const cleared=ev.filter(e=>e.type==='bullet_cleared').map(e=>e.x+','+e.y);
|
|
432
|
+
const sig=b=>[b.x,b.y,b.dir,b.owner,b.steel?1:0].join();
|
|
433
|
+
const a=S.bullets.filter(b=>!b.end&&!cleared.includes(b.x+','+b.y)).map(sig).sort().join(';'),b=B.bullets.map(sig).sort().join(';');
|
|
434
|
+
if(a!==b)note(`子弹 重算 [${a}] 帧里 [${b}]`);
|
|
435
|
+
if(S.maps[1].join()!==B.map.join())note('地形对不上');
|
|
436
|
+
const w1=S.walls.map(w=>[w.x,w.y,w.kind,w.sub].join()).sort().join(';'),w2=ev.filter(e=>e.type==='wall').map(w=>[w.x,w.y,w.kind,w.sub].join()).sort().join(';');
|
|
437
|
+
if(w1!==w2)note(`墙体事件 重算 [${w1}] 事件 [${w2}]`);
|
|
438
|
+
for(const[type,kinds]of[['hit',['hit','swap']],['base_hit',['base']]]){
|
|
439
|
+
const n1=ev.filter(e=>e.type===type).length,n2=S.bullets.filter(b=>kinds.includes(b.kind)).length;
|
|
440
|
+
if(n1!==n2)note(`${type} 次数 重算 ${n2} 事件 ${n1}`)}
|
|
441
|
+
const c1=ev.filter(e=>e.type==='cancel').length,c2=S.fx.filter(e=>e.type==='cancel').length;
|
|
442
|
+
if(c1!==c2)note(`对消次数 重算 ${c2} 事件 ${c1}`);
|
|
443
|
+
if(!D.params.base_regen&&!D.params.base_armor)for(const s of[0,1])
|
|
444
|
+
if(A.base_hp[s]-S.baseT[s].length!==B.base_hp[s])note(`${s} 号方基地血量 重算 ${A.base_hp[s]-S.baseT[s].length} 帧里 ${B.base_hp[s]}`);
|
|
445
|
+
}
|
|
446
|
+
return bad;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ====================================================================
|
|
450
|
+
// 画图:地形、坦克、子弹
|
|
451
|
+
// ====================================================================
|
|
452
|
+
const cv=$('board'),g=cv.getContext('2d');
|
|
453
|
+
let CS=40,OX=0,OY=0; // 格子边长、棋盘左上角(画布像素)
|
|
454
|
+
let terrainKey='',terrainImg=null;
|
|
455
|
+
const px=x=>OX+x*CS,py=y=>OY+y*CS,cxOf=x=>OX+(x+.5)*CS,cyOf=y=>OY+(y+.5)*CS;
|
|
456
|
+
|
|
457
|
+
function drawBrick(c,x,y,s){
|
|
458
|
+
c.fillStyle='#2a1509';c.fillRect(x,y,s,s);
|
|
459
|
+
const rows=4,h=s/rows,m=Math.max(1,s*.035);
|
|
460
|
+
for(let r=0;r<rows;r++){const off=r%2?-s/4:0;
|
|
461
|
+
for(let i=0;i<3;i++){const bx=x+off+i*s/2,l=Math.max(x,bx)+m/2,rr=Math.min(x+s,bx+s/2)-m/2;if(rr<=l)continue;
|
|
462
|
+
c.fillStyle='#a4502a';c.fillRect(l,y+r*h+m/2,rr-l,h-m);
|
|
463
|
+
c.fillStyle='#c9713f';c.fillRect(l,y+r*h+m/2,rr-l,Math.max(1,h*.22))}}
|
|
464
|
+
}
|
|
465
|
+
function drawSteel(c,x,y,s){
|
|
466
|
+
c.fillStyle='#3d444d';c.fillRect(x,y,s,s);
|
|
467
|
+
const h=s/2,m=Math.max(1,s*.04);
|
|
468
|
+
for(let i=0;i<2;i++)for(let j=0;j<2;j++){const bx=x+i*h+m/2,by=y+j*h+m/2,w=h-m;
|
|
469
|
+
c.fillStyle='#e3e8ee';c.fillRect(bx,by,w,w);
|
|
470
|
+
c.fillStyle='#727c88';c.beginPath();c.moveTo(bx+w,by);c.lineTo(bx+w,by+w);c.lineTo(bx,by+w);c.closePath();c.fill();
|
|
471
|
+
c.fillStyle='#aab3be';c.fillRect(bx+w*.22,by+w*.22,w*.56,w*.56)}
|
|
472
|
+
}
|
|
473
|
+
function drawWater(c,x,y,s){
|
|
474
|
+
c.fillStyle='#143a6b';c.fillRect(x,y,s,s);
|
|
475
|
+
c.strokeStyle='#4b8fd6';c.lineWidth=Math.max(1,s*.05);c.lineCap='round';
|
|
476
|
+
for(let r=0;r<3;r++){const yy=y+s*(.22+r*.28);c.beginPath();
|
|
477
|
+
for(let i=0;i<=8;i++){const xx=x+s*(.1+i*.1),dy=Math.sin(i*1.57+r*1.3)*s*.045;i?c.lineTo(xx,yy+dy):c.moveTo(xx,yy+dy)}c.stroke()}
|
|
478
|
+
}
|
|
479
|
+
function drawEmpty(c,x,y,s){c.fillStyle='#0f1216';c.fillRect(x,y,s,s);c.strokeStyle='#1c2129';c.lineWidth=1;c.strokeRect(x+.5,y+.5,s-1,s-1)}
|
|
480
|
+
function terrain(map){
|
|
481
|
+
const key=CS+'|'+cv.width+'|'+map.join('');
|
|
482
|
+
if(key===terrainKey)return terrainImg;
|
|
483
|
+
const n=D.size,im=terrainImg||document.createElement('canvas');im.width=n*CS;im.height=n*CS;
|
|
484
|
+
const c=im.getContext('2d');
|
|
485
|
+
for(let y=0;y<n;y++)for(let x=0;x<n;x++){const ch=map[y][x],X=x*CS,Y=y*CS;
|
|
486
|
+
drawEmpty(c,X,Y,CS);
|
|
487
|
+
if(ch==='#')drawBrick(c,X,Y,CS);else if(ch==='@')drawSteel(c,X,Y,CS);else if(ch==='~')drawWater(c,X,Y,CS)}
|
|
488
|
+
terrainKey=key;terrainImg=im;return im;
|
|
489
|
+
}
|
|
490
|
+
function starPath(c,x,y,r,rot=0){c.beginPath();for(let k=0;k<10;k++){const a=Math.PI/5*k-Math.PI/2+rot,rr=k%2?r*.45:r;c.lineTo(x+rr*Math.cos(a),y+rr*Math.sin(a))}c.closePath()}
|
|
491
|
+
function drawStar(c,x,y,r,alpha=1){c.save();c.globalAlpha*=alpha;starPath(c,x,y,r);c.fillStyle=GOLD;c.fill();c.lineWidth=Math.max(1,r*.14);c.strokeStyle='#7a5a00';c.stroke();
|
|
492
|
+
starPath(c,x,y,r*.45);c.fillStyle='#fff6c8';c.fill();c.restore()}
|
|
493
|
+
function drawSupply(c,x,y,s){ // 补给点:地上的虚线圈加四个刻度
|
|
494
|
+
c.save();c.strokeStyle='#b89a3a';c.lineWidth=Math.max(1,s*.04);c.setLineDash([s*.09,s*.07]);c.beginPath();c.arc(x,y,s*.36,0,7);c.stroke();c.setLineDash([]);
|
|
495
|
+
for(let k=0;k<4;k++){const a=k*Math.PI/2+Math.PI/4;c.beginPath();c.moveTo(x+Math.cos(a)*s*.4,y+Math.sin(a)*s*.4);c.lineTo(x+Math.cos(a)*s*.48,y+Math.sin(a)*s*.48);c.stroke()}
|
|
496
|
+
c.restore()}
|
|
497
|
+
function drawSpawn(c,x,y,s,seat){ // 出生点:四个角标
|
|
498
|
+
c.save();c.strokeStyle=PAL[seat].m;c.globalAlpha*=.85;c.lineWidth=Math.max(1.5,s*.055);const a=s*.42,b=s*.2;
|
|
499
|
+
for(const[sx,sy]of[[-1,-1],[1,-1],[1,1],[-1,1]]){c.beginPath();c.moveTo(x+sx*a,y+sy*(a-b));c.lineTo(x+sx*a,y+sy*a);c.lineTo(x+sx*(a-b),y+sy*a);c.stroke()}
|
|
500
|
+
c.restore()}
|
|
501
|
+
function drawBase(c,x,y,s,seat,hp,maxhp,flash){
|
|
502
|
+
const p=PAL[seat],X=x-s/2,Y=y-s/2;
|
|
503
|
+
c.save();
|
|
504
|
+
c.fillStyle=hp>0?'#0b0d10':'#1a1414';c.fillRect(X,Y,s,s);
|
|
505
|
+
if(hp>0){
|
|
506
|
+
// 盾形徽记:0 号方实心,1 号方斜纹(不只靠颜色区分)
|
|
507
|
+
const sh=new Path2D();sh.moveTo(x-s*.34,y-s*.40);sh.lineTo(x+s*.34,y-s*.40);sh.lineTo(x+s*.34,y-s*.02);sh.lineTo(x,y+s*.26);sh.lineTo(x-s*.34,y-s*.02);sh.closePath();
|
|
508
|
+
c.fillStyle=flash?'#fff':p.m;c.fill(sh);
|
|
509
|
+
if(seat===1&&!flash){c.save();c.clip(sh);c.strokeStyle=p.k;c.lineWidth=s*.07;for(let i=-4;i<5;i++){c.beginPath();c.moveTo(x+i*s*.17-s*.4,y+s*.4);c.lineTo(x+i*s*.17+s*.4,y-s*.4);c.stroke()}c.restore()}
|
|
510
|
+
c.lineWidth=Math.max(1.5,s*.05);c.strokeStyle=p.l;c.stroke(sh);
|
|
511
|
+
starPath(c,x,y-s*.12,s*.17);c.fillStyle=flash?p.m:'#fff';c.fill();
|
|
512
|
+
}else{
|
|
513
|
+
c.strokeStyle='#8a8f96';c.lineWidth=s*.09;c.lineCap='round';
|
|
514
|
+
c.beginPath();c.moveTo(x-s*.28,y-s*.32);c.lineTo(x+s*.28,y+s*.14);c.moveTo(x+s*.28,y-s*.32);c.lineTo(x-s*.28,y+s*.14);c.stroke();
|
|
515
|
+
}
|
|
516
|
+
// 血条和数字
|
|
517
|
+
const bw=s*.84,bh=s*.13,bx=x-bw/2,by=y+s*.33;
|
|
518
|
+
c.fillStyle='#000';c.fillRect(bx-1,by-1,bw+2,bh+2);c.fillStyle='#39414d';c.fillRect(bx,by,bw,bh);
|
|
519
|
+
c.fillStyle=hp>0?p.l:BAD;c.fillRect(bx,by,bw*clamp(hp/maxhp,0,1),bh);
|
|
520
|
+
c.restore();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 坦克。三种车型外形不同:快速型尖头窄车身加三角炮塔,火力型圆炮塔双炮管,装甲型宽车身方炮塔粗炮管。
|
|
524
|
+
// 两方除颜色外:0 号方炮塔是浅色实心的,1 号方炮塔是深色空心的、车身有斜纹。
|
|
525
|
+
function drawTank(c,x,y,s,type,seat,dir,o){
|
|
526
|
+
o=o||{};const p=PAL[seat];
|
|
527
|
+
c.save();c.translate(x,y);c.rotate(ANG[dir]);c.scale(s,s);c.globalAlpha*=o.alpha==null?1:o.alpha;
|
|
528
|
+
const fl=o.flash,dead=o.dead,ghost=o.ghost;
|
|
529
|
+
const col=v=>fl?'#fff':dead?'#4a4d52':v;
|
|
530
|
+
const W=type==='armor'?.46:type==='fast'?.35:.41,tw=type==='armor'?.15:.12,L=type==='fast'?.37:type==='armor'?.42:.39;
|
|
531
|
+
c.lineJoin='round';
|
|
532
|
+
if(ghost){c.strokeStyle=p.l;c.lineWidth=.035;c.setLineDash([.07,.05])}
|
|
533
|
+
const R=(a,b,w,h,fill)=>{if(ghost){c.strokeRect(a,b,w,h)}else{c.fillStyle=fill;c.fillRect(a,b,w,h)}};
|
|
534
|
+
// 履带
|
|
535
|
+
for(const sx of[-1,1]){const tx=sx>0?W-tw:-W;R(tx,-L,tw,2*L,col(p.d));
|
|
536
|
+
if(!ghost){c.fillStyle=fl?'#ddd':'#00000088';const ph=((o.tread||0)%1+1)%1*.1;for(let yy=-L+ph;yy<L-.02;yy+=.1)c.fillRect(tx,yy,tw,.035)}}
|
|
537
|
+
// 车身
|
|
538
|
+
const hull=new Path2D();
|
|
539
|
+
if(type==='fast'){hull.moveTo(-.21,.33);hull.lineTo(-.21,-.12);hull.lineTo(0,-.37);hull.lineTo(.21,-.12);hull.lineTo(.21,.33)}
|
|
540
|
+
else if(type==='fire'){hull.rect(-.27,-.33,.54,.67)}
|
|
541
|
+
else{hull.rect(-.33,-.40,.66,.80)}
|
|
542
|
+
hull.closePath();
|
|
543
|
+
if(ghost)c.stroke(hull);else{c.fillStyle=col(p.m);c.fill(hull);
|
|
544
|
+
if(seat===1&&!fl&&!dead){c.save();c.clip(hull);c.strokeStyle=p.d;c.lineWidth=.06;for(let i=-3;i<4;i++){c.beginPath();c.moveTo(i*.2-.4,.4);c.lineTo(i*.2+.4,-.4);c.stroke()}c.restore()}
|
|
545
|
+
c.lineWidth=.03;c.strokeStyle=fl?'#fff':dead?'#2a2c30':p.k;c.stroke(hull);
|
|
546
|
+
if(type==='armor'){c.fillStyle=col(p.l);c.fillRect(-.33,-.40,.66,.07);c.fillRect(-.33,.33,.66,.07)}}
|
|
547
|
+
// 炮管
|
|
548
|
+
const bc=col(seat?p.l:'#fff3dc');
|
|
549
|
+
if(type==='fast')R(-.03,-.49,.06,.42,bc);
|
|
550
|
+
else if(type==='fire'){R(-.125,-.49,.07,.5,bc);R(.055,-.49,.07,.5,bc)}
|
|
551
|
+
else R(-.06,-.49,.12,.42,bc);
|
|
552
|
+
// 炮塔
|
|
553
|
+
c.beginPath();
|
|
554
|
+
if(type==='fast'){c.moveTo(0,-.14);c.lineTo(.13,.13);c.lineTo(-.13,.13);c.closePath()}
|
|
555
|
+
else if(type==='fire')c.arc(0,.04,.17,0,7);
|
|
556
|
+
else c.rect(-.2,-.15,.4,.4);
|
|
557
|
+
if(ghost)c.stroke();else{c.fillStyle=fl?'#fff':dead?'#333':seat?p.k:p.l;c.fill();c.lineWidth=seat?.05:.03;c.strokeStyle=fl?'#fff':dead?'#222':seat?p.l:p.k;c.stroke()}
|
|
558
|
+
c.restore();
|
|
559
|
+
}
|
|
560
|
+
// 坦克头顶和脚下的小标记:血量格、等级星、子弹格(实心 = 还能打,空心 = 在飞;红色 = 装填中)
|
|
561
|
+
function drawHud(c,x,y,s,h){
|
|
562
|
+
c.save();
|
|
563
|
+
const n=Math.max(h.maxhp,1),pw=Math.min(s*.15,s*.8/n),ph=s*.085,gap=s*.025,tw=n*pw+(n-1)*gap,top=y-s*.5+s*.015;
|
|
564
|
+
c.fillStyle='#000c';c.fillRect(x-tw/2-2,top-1,tw+4,ph+3);
|
|
565
|
+
for(let i=0;i<n;i++){const bx=x-tw/2+i*(pw+gap);
|
|
566
|
+
if(i<h.hp){c.fillStyle='#62d08a';c.fillRect(bx,top,pw,ph)}else{c.strokeStyle='#62d08a';c.lineWidth=1;c.strokeRect(bx+.5,top+.5,pw-1,ph-1)}}
|
|
567
|
+
if(h.level>0){const r=s*.1;c.fillStyle='#000c';c.fillRect(x-s*.5,y+s*.5-r*2.2,h.level*r*2.1+2,r*2.2);
|
|
568
|
+
for(let i=0;i<h.level;i++){starPath(c,x-s*.5+r*1.05+i*r*2.1,y+s*.5-r*1.1,r);c.fillStyle=GOLD;c.fill()}}
|
|
569
|
+
if(h.cap>0){const r=s*.05,w=h.cap*r*2.6;c.fillStyle='#000c';c.fillRect(x+s*.5-w-2,y+s*.5-r*2.8,w+2,r*2.8);
|
|
570
|
+
for(let i=0;i<h.cap;i++){const bx=x+s*.5-r*1.4-i*r*2.6,by=y+s*.5-r*1.4;c.beginPath();c.arc(bx,by,r,0,7);
|
|
571
|
+
const ready=i<h.cap-h.out;
|
|
572
|
+
if(h.reload){c.strokeStyle=BAD;c.lineWidth=Math.max(1,s*.025);c.stroke();c.beginPath();c.moveTo(bx-r,by+r);c.lineTo(bx+r,by-r);c.stroke()}
|
|
573
|
+
else if(ready){c.fillStyle='#fff';c.fill()}else{c.strokeStyle='#fff';c.lineWidth=Math.max(1,s*.022);c.stroke()}}}
|
|
574
|
+
c.restore();
|
|
575
|
+
}
|
|
576
|
+
// 子弹:0 号方是圆头弹,1 号方是菱形弹;能破钢的子弹外面多一圈白环
|
|
577
|
+
function drawBullet(c,x,y,s,dir,owner,steel,trail,alpha){
|
|
578
|
+
const seat=seatOf(owner),p=PAL[seat],d=DV[dir];
|
|
579
|
+
c.save();c.globalAlpha*=alpha==null?1:alpha;
|
|
580
|
+
if(trail>0){const gr=c.createLinearGradient(x,y,x-d[0]*trail*s,y-d[1]*trail*s);gr.addColorStop(0,p.l+'cc');gr.addColorStop(1,p.l+'00');
|
|
581
|
+
c.strokeStyle=gr;c.lineWidth=s*.13;c.lineCap='butt';c.beginPath();c.moveTo(x,y);c.lineTo(x-d[0]*trail*s,y-d[1]*trail*s);c.stroke()}
|
|
582
|
+
c.translate(x,y);c.rotate(ANG[dir]);c.scale(s,s);
|
|
583
|
+
if(steel){c.strokeStyle='#fff';c.lineWidth=.04;c.beginPath();c.arc(0,0,.26,0,7);c.stroke()}
|
|
584
|
+
c.beginPath();
|
|
585
|
+
if(seat===0){c.moveTo(-.095,.17);c.lineTo(-.095,-.08);c.arc(0,-.08,.095,Math.PI,0);c.lineTo(.095,.17)}
|
|
586
|
+
else{c.moveTo(0,-.23);c.lineTo(.12,0);c.lineTo(0,.2);c.lineTo(-.12,0)}
|
|
587
|
+
c.closePath();c.fillStyle='#fff';c.fill();c.lineWidth=.055;c.strokeStyle=p.m;c.stroke();
|
|
588
|
+
c.restore();
|
|
589
|
+
}
|
|
590
|
+
function label(c,text,x,y,size,color,alpha,stroke){
|
|
591
|
+
c.save();c.globalAlpha*=alpha==null?1:alpha;c.font=`700 ${size}px ${FONT}`;c.textAlign='center';c.textBaseline='middle';
|
|
592
|
+
c.lineWidth=Math.max(2,size*.22);c.strokeStyle=stroke||'#000d';c.lineJoin='round';c.strokeText(text,x,y);c.fillStyle=color;c.fillText(text,x,y);c.restore();
|
|
593
|
+
}
|
|
594
|
+
const rnd=(i,j)=>{const v=Math.sin(i*127.1+j*311.7)*43758.5453;return v-Math.floor(v)}; // 固定的"随机",同一处每次画出来一样
|
|
595
|
+
|
|
596
|
+
// ====================================================================
|
|
597
|
+
// 特效:每个特效是 (类型, 位置, 开始时刻),画成它的"年龄"的函数,所以拖到哪里都能直接画出来
|
|
598
|
+
// ====================================================================
|
|
599
|
+
function drawFx(e,age,S){
|
|
600
|
+
if(age<0)return;
|
|
601
|
+
const s=CS,x=cxOf(e.x),y=cyOf(e.y);
|
|
602
|
+
g.save();
|
|
603
|
+
switch(e.type){
|
|
604
|
+
case 'muzzle':{if(age>.22)break;const u=age/.22,d=DV[e.dir],mx=x+d[0]*s*.55,my=y+d[1]*s*.55;
|
|
605
|
+
g.translate(mx,my);g.rotate(ANG[e.dir]);g.globalAlpha=1-u;
|
|
606
|
+
g.fillStyle='#fff7c0';g.beginPath();const r=s*(.18+.2*u);
|
|
607
|
+
for(let k=0;k<8;k++){const a=k*Math.PI/4,rr=k%2?r*.45:r*(k===0?1.7:1);g.lineTo(Math.sin(a)*rr,-Math.cos(a)*rr)}g.closePath();g.fill();
|
|
608
|
+
g.strokeStyle='#ff9d2e';g.lineWidth=s*.04;g.stroke();break}
|
|
609
|
+
case 'hit':{if(age<.4){const u=age/.4;g.globalAlpha=1-u;g.strokeStyle='#fff';g.lineWidth=s*.07;g.lineCap='round';
|
|
610
|
+
for(let k=0;k<8;k++){const a=k*Math.PI/4+.3;g.beginPath();g.moveTo(x+Math.cos(a)*s*(.12+.3*u),y+Math.sin(a)*s*(.12+.3*u));g.lineTo(x+Math.cos(a)*s*(.26+.42*u),y+Math.sin(a)*s*(.26+.42*u));g.stroke()}
|
|
611
|
+
g.strokeStyle=BAD;g.lineWidth=s*.06;g.beginPath();g.arc(x,y,s*(.2+.4*u),0,7);g.stroke()}
|
|
612
|
+
if(age<.75){const u=age/.75;label(g,'-1',x+s*.32,y-s*(.3+.5*u),s*.42,BAD,1-u*u)}break}
|
|
613
|
+
case 'ding':{if(age>.28)break;const u=age/.28,d=DV[e.dir],ix=x-d[0]*s*.5,iy=y-d[1]*s*.5;g.globalAlpha=1-u;g.strokeStyle='#e8eef5';g.lineWidth=s*.045;g.lineCap='round';
|
|
614
|
+
for(let k=-2;k<=2;k++){const a=Math.atan2(-d[1],-d[0])+k*.5;g.beginPath();g.moveTo(ix+Math.cos(a)*s*.08,iy+Math.sin(a)*s*.08);g.lineTo(ix+Math.cos(a)*s*(.14+.3*u),iy+Math.sin(a)*s*(.14+.3*u));g.stroke()}break}
|
|
615
|
+
case 'out':{if(age>.2)break;const d=DV[e.dir];g.globalAlpha=1-age/.2;g.fillStyle='#fff';g.beginPath();g.arc(x-d[0]*s*.5,y-d[1]*s*.5,s*.08,0,7);g.fill();break}
|
|
616
|
+
case 'basehit':{if(age<.5){const u=age/.5;g.globalAlpha=1-u;g.strokeStyle=BAD;g.lineWidth=s*.1;g.strokeRect(x-s*(.5+.25*u),y-s*(.5+.25*u),s*(1+.5*u),s*(1+.5*u));
|
|
617
|
+
g.strokeStyle='#fff';g.lineWidth=s*.05;g.strokeRect(x-s*(.5+.1*u),y-s*(.5+.1*u),s*(1+.2*u),s*(1+.2*u))}
|
|
618
|
+
if(e.dmg!==false&&age<.9){const u=age/.9,up=e.y<D.size/2?1:-1;label(g,'基地 -1',x,y+up*s*(.95+.35*u),s*.4,BAD,1-u*u)}break}
|
|
619
|
+
case 'debris':{if(age>.5)break;const u=age/.5,cols=e.kind==='brick'?['#c9713f','#a4502a','#7d3a1c']:['#e3e8ee','#aab3be','#727c88'];
|
|
620
|
+
if(u<.3){g.globalAlpha=1-u/.3;g.fillStyle='#fff';g.fillRect(x-s/2,y-s/2,s,s)}
|
|
621
|
+
g.globalAlpha=1-u*u;
|
|
622
|
+
for(let k=0;k<10;k++){const a=rnd(e.x*17+k,e.y*31)*6.283,r=s*(.12+(.35+.5*rnd(k,e.x+e.y))*u),sz=s*(.1+.09*rnd(k,7));
|
|
623
|
+
g.fillStyle=cols[k%3];g.fillRect(x+Math.cos(a)*r-sz/2,y+Math.sin(a)*r-sz/2+s*.25*u*u,sz,sz)}break}
|
|
624
|
+
case 'cancel':{if(age>.4)break;const u=age/.4;g.globalAlpha=1-u;g.strokeStyle='#fff';g.lineWidth=s*.07;g.lineCap='round';const r=s*(.1+.18*u);
|
|
625
|
+
g.beginPath();g.moveTo(x-r,y-r);g.lineTo(x+r,y+r);g.moveTo(x+r,y-r);g.lineTo(x-r,y+r);g.stroke();
|
|
626
|
+
g.strokeStyle='#bfe9ff';g.lineWidth=s*.04;g.beginPath();g.arc(x,y,s*(.12+.38*u),0,7);g.stroke();break}
|
|
627
|
+
case 'death':{const dur=.95;if(age<dur){const u=age/dur;
|
|
628
|
+
for(const[r0,c0,d0]of[[.62,'#ff7a1a',0],[.46,'#ffd24a',.06],[.28,'#fff',.12]]){const v=clamp((u-d0)/.55,0,1);if(v<=0||v>=1)continue;
|
|
629
|
+
g.globalAlpha=1-v*v;g.fillStyle=c0;g.beginPath();g.arc(x,y,s*r0*(.35+.9*v),0,7);g.fill()}
|
|
630
|
+
g.globalAlpha=clamp(1.2-u*1.3,0,1);g.strokeStyle='#ffd24a';g.lineWidth=s*.07;g.lineCap='round';
|
|
631
|
+
for(let k=0;k<10;k++){const a=k*.628+.2,r1=s*(.3+.75*u),r2=s*(.45+1.0*u);g.beginPath();g.moveTo(x+Math.cos(a)*r1,y+Math.sin(a)*r1);g.lineTo(x+Math.cos(a)*r2,y+Math.sin(a)*r2);g.stroke()}
|
|
632
|
+
g.globalAlpha=clamp((u-.25)/.3,0,1)*(1-u)*.9;g.fillStyle='#555';for(let k=0;k<5;k++){const a=k*1.257;g.beginPath();g.arc(x+Math.cos(a)*s*.3*u,y+Math.sin(a)*s*.3*u-s*.3*u,s*.2,0,7);g.fill()}
|
|
633
|
+
g.globalAlpha=1;label(g,e.by>=0?'压出生点 · 炸毁':'击毁',x,y-s*(.62+.3*u),s*.38,'#fff',clamp(2.4-u*2.4,0,1),PAL[1-seatOf(e.tank)].d)}
|
|
634
|
+
break}
|
|
635
|
+
case 'respawn':{if(age>.7)break;const u=age/.7,p=PAL[seatOf(e.tank)];g.globalAlpha=1-u;g.strokeStyle=p.l;g.lineWidth=s*.06;
|
|
636
|
+
for(const k of[0,.33,.66]){const v=(u+k)%1;g.beginPath();g.arc(x,y,s*(.95-.6*v),0,7);g.stroke()}
|
|
637
|
+
label(g,'复活',x,y-s*.7,s*.36,p.l,1-u);break}
|
|
638
|
+
case 'star':{if(age>.9)break;const u=age/.9;g.globalAlpha=1-u;g.strokeStyle=GOLD;g.lineWidth=s*.08;g.beginPath();g.arc(x,y,s*(.3+.6*u),0,7);g.stroke();
|
|
639
|
+
for(let k=0;k<5;k++){const a=k*1.257-1.57+u;drawStar(g,x+Math.cos(a)*s*(.35+.6*u),y+Math.sin(a)*s*(.35+.6*u),s*.11,1-u)}
|
|
640
|
+
g.globalAlpha=1;label(g,'升级 Lv'+e.level+(e.level>=2&&D.params.level2_steel!==0?' · 破钢':''),x,y-s*(.7+.35*u),s*.4,GOLD,clamp(2.2-u*2.2,0,1));break}
|
|
641
|
+
case 'supply':{if(age>.6)break;const u=age/.6;g.globalAlpha=1-u;g.strokeStyle=GOLD;g.lineWidth=s*.06;g.beginPath();g.arc(x,y,s*(.9-.55*u),0,7);g.stroke();break}
|
|
642
|
+
case 'cleared':{if(age>.3)break;g.globalAlpha=1-age/.3;g.fillStyle='#ccc';g.beginPath();g.arc(x,y,s*.2,0,7);g.fill();break}
|
|
643
|
+
}
|
|
644
|
+
g.restore();
|
|
645
|
+
}
|
|
646
|
+
// 停在某一回合时留在画面上的标记:这一回合子弹飞过的路线、打掉的墙、被毁的位置、没走成的方向……
|
|
647
|
+
function drawMarks(k,alpha){
|
|
648
|
+
if(k<1||alpha<=0)return;
|
|
649
|
+
const S=simulate(k),s=CS;
|
|
650
|
+
g.save();g.globalAlpha=alpha;
|
|
651
|
+
for(const b of S.bullets){const d=DV[b.dir],len=Math.min(b.endS,2),p=PAL[seatOf(b.owner)];
|
|
652
|
+
const x0=cxOf(b.x0),y0=cyOf(b.y0),x1=x0+d[0]*len*s,y1=y0+d[1]*len*s;
|
|
653
|
+
g.strokeStyle=p.l;g.globalAlpha=alpha*.38;g.lineWidth=s*.07;g.setLineDash([s*.12,s*.1]);g.beginPath();g.moveTo(x0,y0);g.lineTo(x1,y1);g.stroke();g.setLineDash([]);
|
|
654
|
+
if(b.end){g.globalAlpha=alpha*.8;g.fillStyle=p.l;g.beginPath();g.arc(x1,y1,s*.06,0,7);g.fill()}}
|
|
655
|
+
g.globalAlpha=alpha;
|
|
656
|
+
for(const w of S.walls){const x=px(w.x),y=py(w.y);g.strokeStyle=w.kind==='brick'?'#c9713f':'#e3e8ee';g.lineWidth=Math.max(1.5,s*.045);g.setLineDash([s*.14,s*.1]);
|
|
657
|
+
g.strokeRect(x+s*.08,y+s*.08,s*.84,s*.84);g.setLineDash([]);
|
|
658
|
+
g.fillStyle=w.kind==='brick'?'#a4502a':'#aab3be';for(let i=0;i<5;i++)g.fillRect(x+s*(.2+.6*rnd(i,w.x)),y+s*(.2+.6*rnd(w.y,i)),s*.08,s*.08)}
|
|
659
|
+
for(const e of S.fx){const x=cxOf(e.x),y=cyOf(e.y);
|
|
660
|
+
if(e.type==='death'){const p=PAL[seatOf(e.tank)];
|
|
661
|
+
g.fillStyle='#000a';g.beginPath();g.arc(x,y,s*.4,0,7);g.fill();
|
|
662
|
+
g.strokeStyle=p.m;g.lineWidth=s*.11;g.lineCap='round';g.beginPath();g.moveTo(x-s*.24,y-s*.24);g.lineTo(x+s*.24,y+s*.24);g.moveTo(x+s*.24,y-s*.24);g.lineTo(x-s*.24,y+s*.24);g.stroke();
|
|
663
|
+
g.strokeStyle='#fff';g.lineWidth=s*.035;g.beginPath();g.moveTo(x-s*.24,y-s*.24);g.lineTo(x+s*.24,y+s*.24);g.moveTo(x+s*.24,y-s*.24);g.lineTo(x-s*.24,y+s*.24);g.stroke()}
|
|
664
|
+
else if(e.type==='cancel'){g.strokeStyle='#fff';g.lineWidth=s*.05;g.lineCap='round';const r=s*.11;g.beginPath();g.moveTo(x-r,y-r);g.lineTo(x+r,y+r);g.moveTo(x+r,y-r);g.lineTo(x-r,y+r);g.stroke();
|
|
665
|
+
g.beginPath();g.arc(x,y,s*.2,0,7);g.lineWidth=s*.03;g.stroke()}
|
|
666
|
+
else if(e.type==='ding'){const d=DV[e.dir];g.fillStyle='#e8eef5';g.beginPath();g.arc(x-d[0]*s*.5,y-d[1]*s*.5,s*.07,0,7);g.fill()}
|
|
667
|
+
}
|
|
668
|
+
// 没走成:朝想去的方向画一道红杠
|
|
669
|
+
S.T.forEach(t=>t.bump.forEach((d,si)=>{if(!d)return;const p=t.pos[si+1],v=DV[d],x=cxOf(p[0])+v[0]*s*.5,y=cyOf(p[1])+v[1]*s*.5;
|
|
670
|
+
g.strokeStyle=BAD;g.lineWidth=s*.08;g.lineCap='round';g.beginPath();g.moveTo(x-v[1]*s*.22,y-v[0]*s*.22);g.lineTo(x+v[1]*s*.22,y+v[0]*s*.22);g.stroke()}));
|
|
671
|
+
g.restore();
|
|
672
|
+
}
|
|
673
|
+
// 画在坦克上面的静态标记:被打中的车外面一圈红色短线,刚复活、刚升级的车外面一圈环
|
|
674
|
+
function drawMarksTop(k,alpha){
|
|
675
|
+
if(k<1||alpha<=0)return;
|
|
676
|
+
const S=simulate(k),s=CS,fr=F[k];
|
|
677
|
+
g.save();g.globalAlpha=alpha;
|
|
678
|
+
for(const e of S.fx){
|
|
679
|
+
if(e.type==='hit'){const t=fr.tanks[e.tank];if(!t.alive)continue;const x=cxOf(t.x),y=cyOf(t.y);g.strokeStyle=BAD;g.lineWidth=s*.06;g.lineCap='round';
|
|
680
|
+
for(let k2=0;k2<8;k2++){const a=k2*Math.PI/4+.39;g.beginPath();g.moveTo(x+Math.cos(a)*s*.5,y+Math.sin(a)*s*.5);g.lineTo(x+Math.cos(a)*s*.62,y+Math.sin(a)*s*.62);g.stroke()}}
|
|
681
|
+
else if(e.type==='basehit'){const x=px(e.x),y=py(e.y);g.strokeStyle=BAD;g.lineWidth=s*.07;g.strokeRect(x-s*.03,y-s*.03,s*1.06,s*1.06)}
|
|
682
|
+
else if(e.type==='respawn'){const x=cxOf(e.x),y=cyOf(e.y);g.strokeStyle=PAL[seatOf(e.tank)].l;g.lineWidth=s*.04;g.setLineDash([s*.1,s*.08]);g.beginPath();g.arc(x,y,s*.6,0,7);g.stroke();g.setLineDash([])}
|
|
683
|
+
else if(e.type==='star'){const t=fr.tanks[e.tank];if(!t.alive)continue;g.strokeStyle=GOLD;g.lineWidth=s*.05;g.beginPath();g.arc(cxOf(t.x),cyOf(t.y),s*.6,0,7);g.stroke()}
|
|
684
|
+
}
|
|
685
|
+
g.restore();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ====================================================================
|
|
689
|
+
// 画一帧。k = 帧号;p = 正在播第 k+1 回合的进度(0 表示停在第 k 帧);tl = 停下后过了多久
|
|
690
|
+
// ====================================================================
|
|
691
|
+
function fitCanvas(c){const r=c.getBoundingClientRect(),dpr=window.devicePixelRatio||1,w=Math.max(1,Math.round(r.width*dpr)),h=Math.max(1,Math.round(r.height*dpr));
|
|
692
|
+
if(c.width!==w||c.height!==h){c.width=w;c.height=h}return dpr}
|
|
693
|
+
function drawBoard(k,p,tl){
|
|
694
|
+
fitCanvas(cv);
|
|
695
|
+
const W=cv.width,n=D.size,P=D.params;
|
|
696
|
+
const margin=showCoords?Math.round(W*.032):Math.round(W*.012);
|
|
697
|
+
CS=Math.floor((W-2*margin)/n);OX=Math.round((W-CS*n)/2);OY=OX;
|
|
698
|
+
const anim=p>0&&k<N,A=F[k],S=anim?simulate(k+1):null,s=CS;
|
|
699
|
+
g.setTransform(1,0,0,1,0,0);g.fillStyle='#262b33';g.fillRect(0,0,W,W);
|
|
700
|
+
g.fillStyle='#4a515c';g.fillRect(OX-4,OY-4,CS*n+8,CS*n+8);
|
|
701
|
+
const map=anim?(p<.275?A.map:p<.775?S.maps[0]:S.maps[1]):A.map;
|
|
702
|
+
g.drawImage(terrain(map),OX,OY);
|
|
703
|
+
// 坐标
|
|
704
|
+
if(showCoords){g.font=`${Math.round(margin*.5)}px ${NUMFONT}`;g.fillStyle='#8a94a1';g.textAlign='center';g.textBaseline='middle';
|
|
705
|
+
for(let i=0;i<n;i++){g.fillText(i,cxOf(i),OY/2-1);g.fillText(i,cxOf(i),OY+CS*n+OY/2+1);g.fillText(i,OX/2-1,cyOf(i));g.fillText(i,OX+CS*n+OX/2+1,cyOf(i))}}
|
|
706
|
+
// 地上的记号:出生点、补给点
|
|
707
|
+
D.spawns.forEach((sp,i)=>drawSpawn(g,cxOf(sp[0]),cyOf(sp[1]),s,seatOf(i)));
|
|
708
|
+
const turnNow=A.turn,ns=nextSupply(turnNow);
|
|
709
|
+
const hasStar=(x,y)=>A.stars.some(q=>q[0]===x&&q[1]===y);
|
|
710
|
+
const occupied=(x,y)=>A.tanks.some(t=>t.alive&&t.x===x&&t.y===y);
|
|
711
|
+
for(const[x,y]of D.supply){drawSupply(g,cxOf(x),cyOf(y),s);
|
|
712
|
+
if(!hasStar(x,y)&&ns!=null&&!(anim?false:occupied(x,y)))label(g,String(ns-turnNow),cxOf(x),cyOf(y),s*.34,'#b89a3a',anim&&occupied(x,y)?0:.95,'#0009')}
|
|
713
|
+
// 停着的时候:这一回合留下的痕迹
|
|
714
|
+
const markA=anim?0:clamp((tl-.35)/.4,0,1);
|
|
715
|
+
if(!anim)drawMarks(k,markA);
|
|
716
|
+
// 星星
|
|
717
|
+
const fresh=!anim&&k>=1?simulate(k).fx.filter(e=>e.type==='supply'):[];
|
|
718
|
+
for(const[x,y]of A.stars){const born=fresh.some(e=>e.x===x&&e.y===y)?clamp(tl/.35,0,1):1;
|
|
719
|
+
drawStar(g,cxOf(x),cyOf(y),s*.3*(born<1?.4+.6*born*(2-born):1),born)}
|
|
720
|
+
// 基地
|
|
721
|
+
const baseHp=[0,1].map(q=>anim?Math.max(0,A.base_hp[q]-S.baseT[q].filter(t=>t<=p).length):A.base_hp[q]);
|
|
722
|
+
D.bases.forEach((b,q)=>{const flash=anim&&S.baseT[q].some(t=>p>=t&&p-t<.09);
|
|
723
|
+
drawBase(g,cxOf(b[0]),cyOf(b[1]),s,q,baseHp[q],P.base_hp||12,flash);
|
|
724
|
+
label(g,String(baseHp[q]),cxOf(b[0])+s*.0,cyOf(b[1])+s*.4,s*.26,'#fff',1)});
|
|
725
|
+
// 不在场的车:出生点上画虚影和复活倒计时
|
|
726
|
+
A.tanks.forEach((t,i)=>{if(t.alive)return;const sp=D.spawns[i],x=cxOf(sp[0]),y=cyOf(sp[1]);
|
|
727
|
+
if(occupied(sp[0],sp[1])){label(g,t.respawn_in>0?String(t.respawn_in):'…',x+s*.34,y-s*.34,s*.3,PAL[t.seat].l,1);return}
|
|
728
|
+
drawTank(g,x,y,s,t.type,t.seat,t.seat?'D':'U',{ghost:true,alpha:.55});
|
|
729
|
+
label(g,t.respawn_in>0?String(t.respawn_in):'等',x,y,s*.5,'#fff',1,PAL[t.seat].d)});
|
|
730
|
+
// 坦克
|
|
731
|
+
const sub=p*2,si=Math.min(1,Math.floor(sub)),sf=sub-si;
|
|
732
|
+
const prevFx=k>=1?simulate(k).fx:[];
|
|
733
|
+
A.tanks.forEach((t,i)=>{
|
|
734
|
+
if(!t.alive)return;
|
|
735
|
+
let x=t.x,y=t.y,dir=facing[k][i],hp=t.hp,flash=false,alpha=1,tread=0;
|
|
736
|
+
if(anim){const st=S.T[i],a=st.pos[si],b=st.pos[si+1];x=a[0]+(b[0]-a[0])*sf;y=a[1]+(b[1]-a[1])*sf;
|
|
737
|
+
const bd=st.bump[si];if(bd){const off=.13*Math.sin(Math.PI*Math.min(1,sf*1.7));x+=DV[bd][0]*off;y+=DV[bd][1]*off}
|
|
738
|
+
if(st.fire)dir=st.fire;else if(st.mv[si])dir=st.mv[si];else if(si===1&&st.mv[0])dir=st.mv[0];
|
|
739
|
+
hp=t.hp-st.hitT.filter(q=>q<=p).length;flash=st.hitT.some(q=>p>=q&&p-q<.07);tread=(x+y)*4}
|
|
740
|
+
else if(prevFx.some(e=>e.type==='respawn'&&e.tank===i))alpha=clamp(tl/.3,0,1);
|
|
741
|
+
const X=cxOf(x),Y=cyOf(y);
|
|
742
|
+
drawTank(g,X,Y,s,t.type,t.seat,dir,{flash,dead:hp<=0,alpha,tread});
|
|
743
|
+
if(alpha>.5)drawHud(g,X,Y,s,{hp:Math.max(0,hp),maxhp:maxHp(t),level:t.level,cap:t.bullet_cap,out:Math.min(t.bullet_cap,t.bullets_out+(anim&&S.T[i].fire?1:0)),reload:!anim&&t.reload_left>0});
|
|
744
|
+
});
|
|
745
|
+
if(!anim)drawMarksTop(k,markA);
|
|
746
|
+
// 子弹
|
|
747
|
+
if(anim){for(const b of S.bullets){if(sub>=b.endS)continue;const d=DV[b.dir];
|
|
748
|
+
if(b.fresh&&sub<.3)continue; // 刚出膛的头一小段藏在车身下面
|
|
749
|
+
drawBullet(g,cxOf(b.x0+d[0]*sub),cyOf(b.y0+d[1]*sub),s,b.dir,b.owner,b.steel,b.fresh?Math.min(sub-.3,1.1):1.1)}}
|
|
750
|
+
else for(const b of A.bullets)drawBullet(g,cxOf(b.x),cyOf(b.y),s,b.dir,b.owner,b.steel,1.1);
|
|
751
|
+
// 特效
|
|
752
|
+
if(anim){for(const e of prevFx)drawFx(e,1+p-e.t);for(const e of S.fx)drawFx(e,p-e.t)}
|
|
753
|
+
else if(tl<TAIL_MAX)for(const e of prevFx)drawFx(e,1+tl-e.t);
|
|
754
|
+
return baseHp;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ====================================================================
|
|
758
|
+
// 侧栏
|
|
759
|
+
// ====================================================================
|
|
760
|
+
const side={};
|
|
761
|
+
function miniTank(canvas,type,seat){const dpr=window.devicePixelRatio||1,w=Math.round(52*dpr);canvas.width=canvas.height=w;const c=canvas.getContext('2d');drawTank(c,w/2,w/2,w*.86,type,seat,'U')}
|
|
762
|
+
function buildStatic(){
|
|
763
|
+
const P=D.params,unit=D.budget&&D.budget.kind==='cpu'?'CPU 毫秒':'毫秒';
|
|
764
|
+
for(const seat of[0,1]){
|
|
765
|
+
const el=$('team'+seat),hpn=P.base_hp||12;
|
|
766
|
+
el.innerHTML=`<div class="thead"><span class="mark"></span><span class="tname">${esc(D.names[seat])}</span><span class="tres" data-k="res"></span><span class="tflt" data-k="flt"></span><span class="tseat">${seat} 号位 · ${seat?'上方':'下方'}</span></div>
|
|
767
|
+
<div class="baserow"><span class="lbl">基地</span><div class="hpbar" data-k="hpbar">${'<i></i>'.repeat(hpn)}</div><span class="hpnum"><span data-k="hp"></span><small>/${hpn}</small></span></div>
|
|
768
|
+
<div class="stats"><div><b data-k="kills"></b><span>击毁</span></div><div><b data-k="stars"></b><span>吃星</span></div><div><b data-k="dmg"></b><span>打中对方基地</span></div><div data-k="msbox"><b data-k="ms"></b><span>本回合思考</span></div></div>
|
|
769
|
+
${[0,1,2].map(j=>{const i=seat*3+j;return `<div class="trow" data-k="row${i}"><canvas data-k="cv${i}"></canvas>
|
|
770
|
+
<div><div class="tl1"><b>${TYPE_CN[typeOf(i)]}</b><span class="pips" data-k="pips${i}"></span><span class="lv" data-k="lv${i}"></span><span class="pos" data-k="pos${i}"></span></div>
|
|
771
|
+
<div class="tl2" data-k="st${i}"></div></div><div class="ord" data-k="ord${i}"></div></div>`}).join('')}`;
|
|
772
|
+
el.querySelectorAll('[data-k]').forEach(n=>{side[n.dataset.k+(/\d$/.test(n.dataset.k)?'':'_'+seat)]={el:n,v:null}});
|
|
773
|
+
for(let j=0;j<3;j++)miniTank(side['cv'+(seat*3+j)].el,typeOf(seat*3+j),seat);
|
|
774
|
+
}
|
|
775
|
+
const res=D.result||{};
|
|
776
|
+
const style=D.style==null?'':` · 风格 ${STYLE_CN[D.style]||D.style}`;
|
|
777
|
+
$('meta').innerHTML=`<b>${esc(D.names[0])}</b> 对 <b>${esc(D.names[1])}</b> · 种子 ${esc(D.seed)}${style} · ${D.size}×${D.size} · 共 ${N} 回合 · ${esc(D.engine||'')}`
|
|
778
|
+
+(D.consistent===false?'<span class="warnchip" title="重新结算出的终局和回放里记的不一样,这份回放可能不是用当前的引擎打的">回放与引擎不一致</span>':'');
|
|
779
|
+
document.title=`${D.names[0]} 对 ${D.names[1]} · 种子 ${D.seed} · 坦克大战回放`;
|
|
780
|
+
$('thinkunit').textContent=unit;
|
|
781
|
+
$('tllegend').innerHTML=`<span class="tk s1"><i></i>${esc(D.names[1])}</span>(上排)<br><b>✕</b>击毁 <b>▮</b>打中基地 <b class="c-gold">★</b>吃星 <b class="c-bad">!</b>故障<br><span class="tk s0"><i></i>${esc(D.names[0])}</span>(下排)`;
|
|
782
|
+
drawLegend();
|
|
783
|
+
}
|
|
784
|
+
function put(key,html){const o=side[key];if(o&&o.v!==html){o.v=html;o.el.innerHTML=html}}
|
|
785
|
+
function cls(key,name,on){const o=side[key];if(o)o.el.classList.toggle(name,!!on)}
|
|
786
|
+
function orderHtml(i,turnFrame,prevFrame){ // 第 i 辆车在这一回合的命令
|
|
787
|
+
if(!turnFrame||!turnFrame.orders)return['—',true];
|
|
788
|
+
const seat=seatOf(i),o=turnFrame.orders[seat][i%3]||'S',was=prevFrame.tanks[i].alive,ev=turnFrame.events;
|
|
789
|
+
if(!was)return[`<small>不在场</small>`,true];
|
|
790
|
+
if(o==='S')return['不动',true];
|
|
791
|
+
if(o[0]==='F'){const ok=ev.some(e=>e.type==='fire'&&e.tank===i);return[`开炮 <span class="ar">${ARROW[o[1]]}</span>`+(ok?'':' <span class="x" title="装填中或子弹已达上限,这一炮没有打出去">未打出</span>'),false]}
|
|
792
|
+
let h='移动 ';
|
|
793
|
+
for(let s=0;s<o.length;s++){const blocked=ev.some(e=>e.type==='blocked'&&e.tank===i&&e.sub===s+1);h+=`<span class="ar">${ARROW[o[s]]}</span>`+(blocked?'<span class="x" title="这一步没走成">✕</span>':'')}
|
|
794
|
+
return[h,false];
|
|
795
|
+
}
|
|
796
|
+
function updateSide(baseHp){
|
|
797
|
+
const anim=prog>0&&cur<N,A=F[cur],show=anim?cur+1:cur,B=F[show],S=anim?simulate(cur+1):null,P=D.params,res=D.result||{};
|
|
798
|
+
const total=N,ended=cur===N&&!anim;
|
|
799
|
+
for(const seat of[0,1]){
|
|
800
|
+
const hp=baseHp[seat],hpn=P.base_hp||12;
|
|
801
|
+
put('hp_'+seat,String(hp));
|
|
802
|
+
const key='hpbar_'+seat,o=side[key],sig=hp+'/'+(anim&&S.baseT[seat].some(t=>prog>=t&&prog-t<.12)?1:0);
|
|
803
|
+
if(o.v!==sig){o.v=sig;[...o.el.children].forEach((n,i)=>{n.className=i<hp?'on':(i<A.base_hp[seat]&&sig.endsWith('1')?'hit':'')})}
|
|
804
|
+
put('kills_'+seat,String(A.kills[seat]));put('stars_'+seat,String(A.stars_eaten[seat]));put('dmg_'+seat,String(A.base_dmg?A.base_dmg[seat]:'–'));
|
|
805
|
+
const ms=B.used_ms?B.used_ms[seat]:null,bud=budgetOf(show);
|
|
806
|
+
put('ms_'+seat,fmtMs(ms));cls('msbox_'+seat,'near',ms!=null&&ms>=bud*.8&&ms<=bud);cls('msbox_'+seat,'over',ms!=null&&ms>bud);
|
|
807
|
+
put('flt_'+seat,(B.faults||[]).filter(f=>f.seat===seat).map(f=>'本回合'+esc(faultKind(f.kind))+',三辆都不动')[0]||'');
|
|
808
|
+
let r='',rc='';
|
|
809
|
+
if(ended){if(res.winner===seat){r='胜';rc='win'}else if(res.winner<0||res.winner==null){r='平';rc='draw'}else{r='负';rc='lose'}}
|
|
810
|
+
put('res_'+seat,r);const ro=side['res_'+seat].el;if(ro.dataset.c!==rc){ro.dataset.c=rc;ro.className='tres '+rc;ro.style.display=r?'':'none'}
|
|
811
|
+
for(let j=0;j<3;j++){
|
|
812
|
+
const i=seat*3+j,t=A.tanks[i];
|
|
813
|
+
const hpNow=anim?Math.max(0,t.hp-S.T[i].hitT.filter(q=>q<=prog).length):t.hp,mh=maxHp(t);
|
|
814
|
+
cls('row'+i,'dead',!t.alive);
|
|
815
|
+
put('pips'+i,t.alive?Array.from({length:mh},(_,q)=>`<i class="${q<hpNow?'on':''}"></i>`).join(''):'');
|
|
816
|
+
put('lv'+i,t.alive?(t.level>0?'★'.repeat(t.level)+(t.level>=2&&P.level2_steel!==0?' 破钢':''):''):'');
|
|
817
|
+
put('pos'+i,t.alive?`(${t.x},${t.y})`:'');
|
|
818
|
+
let st;
|
|
819
|
+
if(!t.alive)st=`<span class="ded">已被毁</span> · ${t.respawn_in>0?t.respawn_in+' 回合后复活':'等出生点空出来'}`;
|
|
820
|
+
else{const ready=t.bullet_cap-t.bullets_out;st=`血 ${hpNow}/${mh} · 子弹 ${ready}/${t.bullet_cap}`+(t.reload_left>0?` · <span class="rel">装填中</span>`:t.can_fire?' · 可开炮':' · 子弹都在飞')}
|
|
821
|
+
put('st'+i,st);
|
|
822
|
+
const[oh,none]=show>=1?orderHtml(i,B,F[show-1]):['—',true];
|
|
823
|
+
put('ord'+i,oh);cls('ord'+i,'none',none);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// 回合信息
|
|
827
|
+
const tb=anim?`第 ${show} 回合<small> / ${total}</small>`:cur===0?`开局<small> / 共 ${total} 回合</small>`:`第 ${cur} 回合<small> / ${total}</small>`;
|
|
828
|
+
if(side._tb!==tb){side._tb=tb;$('turnbig').innerHTML=tb}
|
|
829
|
+
const ph=anim?(prog<.06?0:prog<.5?1:2):(cur>0?3:-1);
|
|
830
|
+
if(side._ph!==ph){side._ph=ph;[...$('phases').children].forEach((n,i)=>n.classList.toggle('on',i===ph))}
|
|
831
|
+
const ns=nextSupply(A.turn),sl=ns==null?'不会再出补给了':`下一批补给:<b>${ns-A.turn}</b> 回合后(第 ${ns} 回合末) · 场上星星 <b>${A.stars.length}</b>`;
|
|
832
|
+
if(side._sl!==sl){side._sl=sl;$('supplyline').innerHTML=sl}
|
|
833
|
+
const lbl=`${anim?show:cur}<small> / ${total}</small>`;
|
|
834
|
+
if(side._lbl!==lbl){side._lbl=lbl;$('turnlabel').innerHTML=lbl}
|
|
835
|
+
if(show!==shownTurn){shownTurn=show;updateEvents(show);updateDebug(show);drawThink()}
|
|
836
|
+
}
|
|
837
|
+
const budgetOf=turn=>D.budget?(turn<=1?D.budget.first_ms:D.budget.turn_ms):Infinity;
|
|
838
|
+
const tk=i=>`<span class="tk s${seatOf(i)}"><i></i>${TYPE_CN[typeOf(i)]}</span>`;
|
|
839
|
+
const sideName=s=>`<span class="tk s${s}"><i></i>${esc(D.names[s])}</span>`;
|
|
840
|
+
function eventHtml(e){
|
|
841
|
+
switch(e.type){
|
|
842
|
+
case 'fire':return['➤','',`${tk(e.tank)} 向 ${ARROW[e.dir]} 开炮`];
|
|
843
|
+
case 'blocked':return['⊘','c-mut',`${tk(e.tank)} 没走成`];
|
|
844
|
+
case 'cancel':return['✕','c-ink2',`子弹在 (${e.x},${e.y}) ${e.how==='cross'?'对穿':'同格'}对消`];
|
|
845
|
+
case 'hit':return['✸','c-bad',`${tk(e.tank)} 在 (${e.x},${e.y}) 被 ${tk(e.by)} 打中`+(e.swap?'(迎着子弹走)':'')+(seatOf(e.tank)===seatOf(e.by)?' <span class="c-bad">友军误伤</span>':'')];
|
|
846
|
+
case 'base_hit':return['▮','c-bad big',`${sideName(e.seat)} 的基地被 ${tk(e.by)} 打中`+(seatOf(e.by)===e.seat?' <span class="c-bad">打了自己的基地</span>':'')];
|
|
847
|
+
case 'wall':return['▪','c-ink2',`(${e.x},${e.y}) 的${e.kind==='brick'?'砖墙':'钢墙'}被打掉`];
|
|
848
|
+
case 'death':return['☠','c-bad big',`${tk(e.tank)} 在 (${e.x},${e.y}) 被毁`+(e.by>=0?`(压在出生点上,被 ${tk(e.by)} 复活炸毁)`:'')];
|
|
849
|
+
case 'respawn':return['↺','',`${tk(e.tank)} 在 (${e.x},${e.y}) 复活`];
|
|
850
|
+
case 'bullet_cleared':return['·','c-mut',`(${e.x},${e.y}) 上的子弹因 ${tk(e.tank)} 复活而消失`];
|
|
851
|
+
case 'star':return['★','c-gold big',`${tk(e.tank)} 吃到星星,升到 ${e.level} 级`+(e.level>=2&&D.params.level2_steel!==0?'(能破钢)':'')];
|
|
852
|
+
case 'supply':return['☆','c-gold',`(${e.x},${e.y}) 出了一颗星星`];
|
|
853
|
+
case 'base_regen':return['+','',`${sideName(e.seat)} 的基地回 1 血`];
|
|
854
|
+
default:return['·','c-mut',esc(e.text||e.type)];
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function updateEvents(turn){
|
|
858
|
+
const f=F[turn];$('evturn').textContent=turn?`第 ${turn} 回合`:'开局';
|
|
859
|
+
if(!turn){$('events').innerHTML='<div class="none">开局。0 号位在下方,1 号位在上方。</div>';return}
|
|
860
|
+
const groups=[['开炮',[]],['小步 1',[]],['小步 2',[]],['回合末',[]]];
|
|
861
|
+
for(const e of f.events){const gi=e.type==='fire'?0:e.sub===1?1:e.sub===2?2:3;groups[gi][1].push(e)}
|
|
862
|
+
let h='';
|
|
863
|
+
for(const[name,list]of groups){if(!list.length)continue;h+=`<div class="grp">${name}</div>`;
|
|
864
|
+
for(const e of list){const[ic,c,t]=eventHtml(e);h+=`<div class="ev ${c.includes('big')?'big':''}"><span class="ic ${c.replace('big','')}">${ic}</span><span>${t}</span></div>`}}
|
|
865
|
+
$('events').innerHTML=h||'<div class="none">这一回合没有发生什么。</div>';
|
|
866
|
+
$('events').scrollTop=0;
|
|
867
|
+
}
|
|
868
|
+
function updateDebug(turn){
|
|
869
|
+
$('dbgturn').textContent=turn?`第 ${turn} 回合`:'';
|
|
870
|
+
let h='';
|
|
871
|
+
const ftext=f=>`回合 ${f.turn} · ${esc(D.names[f.seat])}:${esc(f.detail||faultKind(f.kind))}`;
|
|
872
|
+
const res=D.result||{},now=faultList.filter(f=>f.turn===turn);
|
|
873
|
+
for(const f of now)h+=`<div class="fault now" data-turn="${f.turn}">${ftext(f)}</div>`;
|
|
874
|
+
const lines=stderrByTurn[turn]||[];
|
|
875
|
+
h+=`<div class="sub">这一回合的调试输出${lines.length?'':':无'}</div>`;
|
|
876
|
+
for(const[seat,line]of lines)h+=`<div class="ln"><span class="tk s${seat}"><i></i></span>${esc(line)}</div>`;
|
|
877
|
+
if(faultList.length||(res.bots||[]).some(b=>b&&b.dead)){h+=`<div class="sub">整局的故障(共 ${faultList.length} 条,点一下跳过去)</div>`;
|
|
878
|
+
(res.bots||[]).forEach((b,s)=>{if(b&&b.dead)h+=`<div class="fault now" data-turn="${N}">${esc(D.names[s])} 被判负:${esc(b.dead)}</div>`});
|
|
879
|
+
for(const f of faultList)h+=`<div class="fault ${f.turn===turn?'now':''}" data-turn="${f.turn}">${ftext(f)}</div>`}
|
|
880
|
+
const total=(D.stderr||[]).reduce((a,l)=>a+(l?l.length:0),0);
|
|
881
|
+
if(!faultList.length&&!total)h=`<div class="none c-mut">这局没有故障,两边也都没有调试输出。<br>bot 写到标准错误的内容会按回合显示在这里。</div>`;
|
|
882
|
+
$('debug').innerHTML=h;
|
|
883
|
+
}
|
|
884
|
+
$('debug').addEventListener('click',e=>{const n=e.target.closest('.fault');if(n)seek(clamp(+n.dataset.turn,0,N))});
|
|
885
|
+
|
|
886
|
+
function drawLegend(){
|
|
887
|
+
const c=$('legend');if(!c.clientWidth)return;const items=[['砖墙','一炮打掉'],['钢墙','2 级才能打掉'],['水','挡车不挡子弹'],['补给点','定时出星星'],['出生点','复活的位置'],['星星','吃了升一级']];
|
|
888
|
+
const cssW=c.parentElement.clientWidth-4,fs=parseFloat(getComputedStyle(document.body).fontSize),cols=3,rowH=fs*2.6,dpr=window.devicePixelRatio||1;
|
|
889
|
+
c.style.height=rowH*2+'px';c.width=Math.round(cssW*dpr);c.height=Math.round(rowH*2*dpr);
|
|
890
|
+
const x=c.getContext('2d');x.scale(dpr,dpr);
|
|
891
|
+
items.forEach(([name,note],i)=>{const cx=(i%cols)*cssW/cols+4,cy=Math.floor(i/cols)*rowH+3,s=rowH-8;
|
|
892
|
+
if(i===0)drawBrick(x,cx,cy,s);else if(i===1)drawSteel(x,cx,cy,s);else if(i===2)drawWater(x,cx,cy,s);
|
|
893
|
+
else{drawEmpty(x,cx,cy,s);if(i===3)drawSupply(x,cx+s/2,cy+s/2,s);else if(i===4)drawSpawn(x,cx+s/2,cy+s/2,s,1);else drawStar(x,cx+s/2,cy+s/2,s*.34)}
|
|
894
|
+
x.textAlign='left';x.textBaseline='middle';x.fillStyle='#e9edf3';x.font=`600 ${fs*.88}px ${FONT}`;x.fillText(name,cx+s+7,cy+s*.28);
|
|
895
|
+
x.fillStyle='#7c8794';x.font=`${fs*.72}px ${FONT}`;x.fillText(note,cx+s+7,cy+s*.74)});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ====================================================================
|
|
899
|
+
// 思考用量小图:两方各一行,同一把纵轴
|
|
900
|
+
// ====================================================================
|
|
901
|
+
let thinkGeo=null;
|
|
902
|
+
function drawThink(){
|
|
903
|
+
const c=$('think');if(!D||!c.clientWidth)return;
|
|
904
|
+
const dpr=fitCanvas(c),x=c.getContext('2d'),W=c.width/dpr,H=c.height/dpr,fs=parseFloat(getComputedStyle(document.body).fontSize);
|
|
905
|
+
x.setTransform(dpr,0,0,dpr,0,0);x.clearRect(0,0,W,H);
|
|
906
|
+
const bud=D.budget||{},turnMs=bud.turn_ms||50;
|
|
907
|
+
const val=(k,s)=>F[k].used_ms&&F[k].used_ms[s]!=null?F[k].used_ms[s]:null;
|
|
908
|
+
let mx=0;for(let k=2;k<=N;k++)for(const s of[0,1]){const v=val(k,s);if(v!=null&&v>mx)mx=v}
|
|
909
|
+
const showBud=mx>=turnMs*.4;
|
|
910
|
+
let ymax;if(showBud)ymax=Math.max(turnMs*1.18,mx*1.06);else{const raw=Math.max(mx*1.25,1e-3),e=Math.pow(10,Math.floor(Math.log10(raw)));ymax=Math.ceil(raw/e)*e}
|
|
911
|
+
const padL=fs*2.9,padR=6,gap=fs*.9,top=fs*1.25,ph=(H-top*2-gap-fs*1.0)/2,pw=W-padL-padR,step=pw/Math.max(N,1),bw=Math.max(1,Math.min(step-1,step*.72));
|
|
912
|
+
const shown=prog>0&&cur<N?cur+1:cur;
|
|
913
|
+
thinkGeo={padL,step,W};
|
|
914
|
+
[1,0].forEach((seat,row)=>{
|
|
915
|
+
const y0=top+row*(ph+top+gap),base=y0+ph,p=PAL[seat];
|
|
916
|
+
// 标题:色块 + 名字(文字用正文色,不用系列色)
|
|
917
|
+
x.fillStyle=p.m;x.fillRect(padL,y0-fs*.95,fs*.7,fs*.7);if(seat){x.fillStyle='#0d1626';x.fillRect(padL+fs*.18,y0-fs*.77,fs*.34,fs*.34)}
|
|
918
|
+
x.fillStyle='#b3bcc8';x.font=`${fs*.78}px ${FONT}`;x.textAlign='left';x.textBaseline='middle';
|
|
919
|
+
const b=(D.result&&D.result.bots||[])[seat]||{};
|
|
920
|
+
const v1=val(1,seat);
|
|
921
|
+
x.fillText(`${D.names[seat]} 首回合 ${fmtMs(v1)} / ${bud.first_ms||'?'},之后最多 ${fmtMs(b.max_ms!=null?b.max_ms:mx)} / ${turnMs}`,padL+fs*1.0,y0-fs*.58);
|
|
922
|
+
// 轴
|
|
923
|
+
x.strokeStyle='#2a313c';x.lineWidth=1;x.beginPath();x.moveTo(padL,base+.5);x.lineTo(W-padR,base+.5);x.stroke();
|
|
924
|
+
x.fillStyle='#7c8794';x.font=`${fs*.68}px ${NUMFONT}`;x.textAlign='right';
|
|
925
|
+
x.fillText('0',padL-4,base-2);if(!showBud)x.fillText(fmtMs(ymax),padL-4,y0+fs*.3);
|
|
926
|
+
if(showBud){const yb=base-ph*turnMs/ymax;x.setLineDash([4,3]);x.strokeStyle='#8a94a1';x.beginPath();x.moveTo(padL,yb+.5);x.lineTo(W-padR,yb+.5);x.stroke();x.setLineDash([]);
|
|
927
|
+
x.fillText(String(turnMs),padL-4,yb)}
|
|
928
|
+
for(let k=1;k<=N;k++){const v=val(k,seat);if(v==null)continue;const bg=budgetOf(k),ratio=v/bg,near=ratio>=.8&&ratio<=1,over=ratio>1;
|
|
929
|
+
const h=Math.max(1,Math.min(1,v/ymax)*ph),bx=padL+(k-.5)*step-bw/2;
|
|
930
|
+
x.fillStyle=over?BAD:near?WARN:p.m;x.fillRect(bx,base-h,bw,h);
|
|
931
|
+
if(k===1&&v>ymax){x.fillStyle='#0c0e11';x.fillRect(bx-1,base-ph*.55,bw+2,2)} // 首回合单独的预算,柱子截断
|
|
932
|
+
if(near||over){const mxx=bx+bw/2,my=y0+2;x.fillStyle=over?BAD:WARN;x.beginPath();x.moveTo(mxx-4,my);x.lineTo(mxx+4,my);x.lineTo(mxx,my+6);x.closePath();
|
|
933
|
+
if(over)x.fill();else{x.strokeStyle=WARN;x.lineWidth=1.3;x.stroke()}}}
|
|
934
|
+
});
|
|
935
|
+
// 当前回合、悬停的回合
|
|
936
|
+
const line=(k,col)=>{if(k<1)return;const xx=padL+(k-.5)*step;x.strokeStyle=col;x.lineWidth=1;x.beginPath();x.moveTo(xx+.5,top-2);x.lineTo(xx+.5,H-fs*1.0);x.stroke()};
|
|
937
|
+
if(hoverThink>0)line(hoverThink,'#7c8794');line(shown,'#fff');
|
|
938
|
+
x.fillStyle='#7c8794';x.font=`${fs*.68}px ${NUMFONT}`;x.textAlign='center';x.textBaseline='alphabetic';
|
|
939
|
+
const every=N>150?50:N>60?20:N>20?10:5;for(let k=every;k<=N;k+=every)x.fillText(k,padL+(k-.5)*step,H-2);
|
|
940
|
+
x.textAlign='left';x.fillText('回合',2,H-2);
|
|
941
|
+
const over=[],near=[];for(let k=1;k<=N;k++)for(const s of[0,1]){const v=val(k,s);if(v==null)continue;const r=v/budgetOf(k);if(r>1)over.push(k);else if(r>=.8)near.push(k)}
|
|
942
|
+
$('thinknote').innerHTML=(showBud?`虚线 = 每回合预算 ${turnMs}。`:`纵轴顶端 ${fmtMs(ymax)},离每回合预算 ${turnMs} 还很远。`)
|
|
943
|
+
+(over.length?` <span class="c-bad">▼ 超出预算 ${over.length} 回合</span>`:'')+(near.length?` <span style="color:var(--warn)">▽ 接近预算(≥80%)${near.length} 回合</span>`:'')
|
|
944
|
+
+(!over.length&&!near.length?' 没有接近或超出预算的回合。':'')+' 首回合的预算单独算。';
|
|
945
|
+
}
|
|
946
|
+
function thinkTurnAt(ev){const r=$('think').getBoundingClientRect();if(!thinkGeo)return-1;const k=Math.floor((ev.clientX-r.left-thinkGeo.padL)/thinkGeo.step)+1;return k>=1&&k<=N?k:-1}
|
|
947
|
+
$('think').addEventListener('mousemove',ev=>{const k=thinkTurnAt(ev);if(k!==hoverThink){hoverThink=k;drawThink()}
|
|
948
|
+
if(k<1)return hideTip();const f=F[k],b=budgetOf(k);
|
|
949
|
+
showTip(ev,`第 ${k} 回合(预算 ${b})\n`+[1,0].map(s=>{const v=f.used_ms?f.used_ms[s]:null;return `${D.names[s]}:${fmtMs(v)}`+(v!=null&&v>b?' 超出预算':v!=null&&v>=b*.8?' 接近预算':'')}).join('\n'))});
|
|
950
|
+
$('think').addEventListener('mouseleave',()=>{hoverThink=-1;drawThink();hideTip()});
|
|
951
|
+
$('think').addEventListener('click',ev=>{const k=thinkTurnAt(ev);if(k>=0)seek(k)});
|
|
952
|
+
function showTip(ev,text){const t=$('tip');t.textContent=text;t.style.display='block';const w=t.offsetWidth,h=t.offsetHeight;
|
|
953
|
+
t.style.left=clamp(ev.clientX+12,4,innerWidth-w-4)+'px';t.style.top=clamp(ev.clientY-h-10,4,innerHeight-h-4)+'px'}
|
|
954
|
+
function hideTip(){$('tip').style.display='none'}
|
|
955
|
+
|
|
956
|
+
// ====================================================================
|
|
957
|
+
// 时间轴
|
|
958
|
+
// ====================================================================
|
|
959
|
+
let tlGeo=null;
|
|
960
|
+
function drawTimeline(){
|
|
961
|
+
const c=$('timeline');if(!D||!c.clientWidth)return;
|
|
962
|
+
const dpr=fitCanvas(c),x=c.getContext('2d'),W=c.width/dpr,H=c.height/dpr,fs=parseFloat(getComputedStyle(document.body).fontSize);
|
|
963
|
+
x.setTransform(dpr,0,0,dpr,0,0);x.clearRect(0,0,W,H);
|
|
964
|
+
const padL=10,padR=10,w=W-padL-padR,X=t=>padL+(N?t/N:0)*w,mid=H/2,lane=[mid+H*.25,mid-H*.25];
|
|
965
|
+
tlGeo={padL,w};
|
|
966
|
+
x.fillStyle='#1b2029';x.fillRect(padL-4,3,w+8,H-6);
|
|
967
|
+
x.fillStyle='#232a35';x.fillRect(padL,mid-3,w,6);
|
|
968
|
+
const pos=cur+prog;
|
|
969
|
+
x.fillStyle='#55647a';x.fillRect(padL,mid-3,X(pos)-padL,6);
|
|
970
|
+
const every=N>150?50:N>60?20:N>20?10:5;
|
|
971
|
+
x.font=`${fs*.74}px ${NUMFONT}`;x.textAlign='center';x.textBaseline='middle';
|
|
972
|
+
for(let k=0;k<=N;k+=every){x.fillStyle='#3a4452';x.fillRect(X(k)-.5,mid-6,1,12);if(k&&X(N)-X(k)>fs*1.2){x.fillStyle='#0c0e11';x.fillText(k,X(k),mid);x.fillStyle='#9aa5b3';x.fillText(k,X(k),mid+.5)}}
|
|
973
|
+
// 标记:形状分种类,颜色和上下排分两方
|
|
974
|
+
const r=Math.max(3,H*.085);
|
|
975
|
+
for(const k of keyFrames){const cnt=[{},{}];
|
|
976
|
+
for(const e of keys[k]){const y=lane[e.lane],xx=X(k),p=PAL[e.lane];cnt[e.lane][e.kind]=(cnt[e.lane][e.kind]||0)+1;if(cnt[e.lane][e.kind]>1)continue;
|
|
977
|
+
const yy=y+(e.kind==='base'?(e.lane?-1:1)*H*.13:0);
|
|
978
|
+
if(e.kind==='kill'){x.strokeStyle=p.l;x.lineWidth=2.2;x.lineCap='round';x.beginPath();x.moveTo(xx-r,yy-r);x.lineTo(xx+r,yy+r);x.moveTo(xx+r,yy-r);x.lineTo(xx-r,yy+r);x.stroke()}
|
|
979
|
+
else if(e.kind==='base'){x.fillStyle=p.m;x.fillRect(xx-1.5,yy-H*.07,3,H*.14)}
|
|
980
|
+
else if(e.kind==='star'){starPath(x,xx,yy,r*1.25);x.fillStyle=GOLD;x.fill();x.lineWidth=1.2;x.strokeStyle=p.m;x.stroke()}
|
|
981
|
+
else{x.fillStyle=BAD;x.font=`800 ${r*3}px ${FONT}`;x.fillText('!',xx,yy);x.font=`${fs*.74}px ${NUMFONT}`}}}
|
|
982
|
+
if(hoverTl>=0){x.fillStyle='#ffffff55';x.fillRect(X(hoverTl)-.5,3,1,H-6)}
|
|
983
|
+
const hx=X(pos);x.fillStyle='#fff';x.fillRect(hx-1,2,2,H-4);x.beginPath();x.moveTo(hx-6,1);x.lineTo(hx+6,1);x.lineTo(hx,9);x.closePath();x.fill();
|
|
984
|
+
x.beginPath();x.moveTo(hx-6,H-1);x.lineTo(hx+6,H-1);x.lineTo(hx,H-9);x.closePath();x.fill();
|
|
985
|
+
}
|
|
986
|
+
function tlTurnAt(ev){const r=$('timeline').getBoundingClientRect();if(!tlGeo)return 0;return clamp(Math.round((ev.clientX-r.left-tlGeo.padL)/tlGeo.w*N),0,N)}
|
|
987
|
+
{const c=$('timeline');let drag=false;
|
|
988
|
+
c.addEventListener('pointerdown',ev=>{if(!D)return;drag=true;c.setPointerCapture(ev.pointerId);seek(tlTurnAt(ev))});
|
|
989
|
+
c.addEventListener('pointermove',ev=>{if(!D)return;const k=tlTurnAt(ev);if(drag)seek(k);
|
|
990
|
+
if(k!==hoverTl){hoverTl=k;dirty=true}
|
|
991
|
+
const list=keys[k]||[];showTip(ev,`第 ${k} 回合`+(list.length?'\n'+[...new Set(list.map(e=>e.text))].join('\n'):''))});
|
|
992
|
+
c.addEventListener('pointerup',()=>{drag=false});c.addEventListener('pointercancel',()=>{drag=false});
|
|
993
|
+
c.addEventListener('pointerleave',()=>{hoverTl=-1;dirty=true;hideTip()})}
|
|
994
|
+
|
|
995
|
+
// ====================================================================
|
|
996
|
+
// 结束画面
|
|
997
|
+
// ====================================================================
|
|
998
|
+
function updateEnd(){
|
|
999
|
+
const on=!!D&&showEnd&&ARGS.end!=='0'&&cur===N&&prog===0&&!playing&&tail>=Math.min(TAIL_MAX,.9);
|
|
1000
|
+
const el=$('endcard');if(on===el.classList.contains('on'))return;
|
|
1001
|
+
el.classList.toggle('on',on);if(!on)return;
|
|
1002
|
+
const res=D.result||{},w=res.winner,last=F[N],bots=res.bots||[];
|
|
1003
|
+
const who=w>=0?`<span class="tk s${w}">${esc(D.names[w])}</span> 获胜`:'平局';
|
|
1004
|
+
let why=reasonText(res.reason||last.reason);
|
|
1005
|
+
if(res.reason==='fault'){const dead=bots.map((b,s)=>b&&b.dead?`${esc(D.names[s])}:${esc(b.dead)}`:'').filter(Boolean);why=(w>=0?'对手故障判负':'双方同一回合故障判负')+(dead.length?'<br><small class="c-bad">'+dead.join(';')+'</small>':'')}
|
|
1006
|
+
const row=(k,a,b)=>`<tr><td class="l">${a}</td><td class="k">${k}</td><td class="r">${b}</td></tr>`;
|
|
1007
|
+
const bh=res.base_hp||last.base_hp,ki=res.kills||last.kills,st=res.stars_eaten||last.stars_eaten;
|
|
1008
|
+
$('endin').innerHTML=`<div class="ttl">对局结束</div><div class="who">${who}</div><div class="why">${why}</div>
|
|
1009
|
+
<table>${row('','<span class="tk s0"><i></i>'+esc(D.names[0])+'</span>','<span class="tk s1"><i></i>'+esc(D.names[1])+'</span>')}${row('基地血量',bh[0],bh[1])}${row('击毁',ki[0],ki[1])}${row('吃星',st[0],st[1])}</table>
|
|
1010
|
+
<div>共 ${res.turns!=null?res.turns:N} 回合 · 种子 ${esc(D.seed)}</div>
|
|
1011
|
+
<div style="margin-top:1em"><button id="bendhide">看终局画面(E)</button> <button id="bendre">从头重播</button></div>`;
|
|
1012
|
+
$('bendhide').onclick=()=>{showEnd=false;dirty=true};
|
|
1013
|
+
$('bendre').onclick=()=>{seek(0);togglePlay(true)};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// ====================================================================
|
|
1017
|
+
// 播放控制
|
|
1018
|
+
// ====================================================================
|
|
1019
|
+
function seek(k){if(!D)return;holdTail=false;cur=clamp(Math.round(k),0,N);prog=0;tail=TAIL_MAX;playing=false;stepping=false;showEnd=true;dirty=true;syncPlay()}
|
|
1020
|
+
function togglePlay(on){if(!D)return;holdTail=false;const want=on==null?!(playing&&!stepping):on;
|
|
1021
|
+
if(want){if(cur>=N){cur=0;prog=0}playing=true;stepping=false;showEnd=true}else{playing=false;stepping=false;if(prog>0){prog=0;tail=TAIL_MAX}}
|
|
1022
|
+
dirty=true;syncPlay()}
|
|
1023
|
+
function stepFwd(){if(!D||cur>=N)return;holdTail=false;if(playing&&stepping){cur++;prog=0;if(cur>=N){seek(N);return}}playing=true;stepping=true;dirty=true;syncPlay()} // 播一回合的动画后停下
|
|
1024
|
+
function keyJump(dir){if(!D)return;const pos=prog>0?cur+(dir>0?0:1):cur;const list=dir>0?keyFrames.filter(k=>k>pos):keyFrames.filter(k=>k<pos);
|
|
1025
|
+
if(list.length)seek(dir>0?list[0]:list[list.length-1]);else seek(dir>0?N:0)}
|
|
1026
|
+
function syncPlay(){$('bplay').textContent=playing&&!stepping?'⏸ 暂停':'▶ 播放'}
|
|
1027
|
+
function setSpeed(v){speed=v;const s=$('speed');if(+s.value!==v){s.value=String(v)}}
|
|
1028
|
+
function frame(t){
|
|
1029
|
+
const dt=Math.min(.1,(t-lastT)/1000)||0;lastT=t;
|
|
1030
|
+
if(D){
|
|
1031
|
+
if(playing){prog+=dt*speed;
|
|
1032
|
+
while(prog>=1){cur++;prog-=1;if(cur>=N||stepping){cur=Math.min(cur,N);prog=0;tail=0;playing=false;stepping=false;syncPlay();break}}
|
|
1033
|
+
dirty=true}
|
|
1034
|
+
else if(tail<TAIL_MAX&&!holdTail){tail=Math.min(TAIL_MAX,tail+dt*Math.max(speed,1.2));dirty=true}
|
|
1035
|
+
if(dirty){dirty=false;paint()}
|
|
1036
|
+
}
|
|
1037
|
+
requestAnimationFrame(frame);
|
|
1038
|
+
}
|
|
1039
|
+
function paint(){const hp=drawBoard(cur,prog,tail);updateSide(hp);drawTimeline();updateEnd()}
|
|
1040
|
+
|
|
1041
|
+
// ====================================================================
|
|
1042
|
+
// 布局:棋盘尽量大,两侧栏各至少 21em;窗口窄了改成上下排
|
|
1043
|
+
// ====================================================================
|
|
1044
|
+
function layout(){
|
|
1045
|
+
const fs=parseFloat(getComputedStyle(document.body).fontSize),vw=innerWidth,vh=innerHeight;
|
|
1046
|
+
const narrow=vw<fs*21*2+420||vh<430;
|
|
1047
|
+
document.body.classList.toggle('narrow',narrow);
|
|
1048
|
+
let size;
|
|
1049
|
+
if(narrow)size=Math.max(260,Math.min(vw-fs*1.5,vh-fs*9));
|
|
1050
|
+
else{const m=document.querySelector('main'),avail=m.clientHeight-fs*1.5;size=Math.max(260,Math.min(avail,vw-fs*21*2-fs*3))}
|
|
1051
|
+
document.documentElement.style.setProperty('--board',Math.floor(size)+'px');
|
|
1052
|
+
terrainKey='';dirty=true;
|
|
1053
|
+
if(D){drawLegend();drawThink();for(const seat of[0,1])for(let j=0;j<3;j++)miniTank(side['cv'+(seat*3+j)].el,typeOf(seat*3+j),seat);paint()}
|
|
1054
|
+
}
|
|
1055
|
+
addEventListener('resize',layout);
|
|
1056
|
+
|
|
1057
|
+
// ====================================================================
|
|
1058
|
+
// 文件、按钮、键盘
|
|
1059
|
+
// ====================================================================
|
|
1060
|
+
async function openFile(file){
|
|
1061
|
+
try{
|
|
1062
|
+
let text;
|
|
1063
|
+
if(/\.gz$/i.test(file.name)&&window.DecompressionStream)text=await new Response(file.stream().pipeThrough(new DecompressionStream('gzip'))).text();
|
|
1064
|
+
else text=await file.text();
|
|
1065
|
+
if(load(JSON.parse(text)))seek(0);
|
|
1066
|
+
}catch(exc){alert('文件读不了:'+exc.message)}
|
|
1067
|
+
}
|
|
1068
|
+
$('file').addEventListener('change',e=>{if(e.target.files[0])openFile(e.target.files[0]);e.target.value=''});
|
|
1069
|
+
$('bopen').onclick=$('bopen2').onclick=()=>$('file').click();
|
|
1070
|
+
addEventListener('dragover',e=>{e.preventDefault();document.body.classList.add('dragging')});
|
|
1071
|
+
addEventListener('dragleave',()=>document.body.classList.remove('dragging'));
|
|
1072
|
+
addEventListener('drop',e=>{e.preventDefault();document.body.classList.remove('dragging');const f=e.dataTransfer.files[0];if(f)openFile(f)});
|
|
1073
|
+
$('bfirst').onclick=()=>seek(0);$('blast').onclick=()=>seek(N);
|
|
1074
|
+
$('bprev').onclick=()=>seek(prog>0?cur:cur-1);$('bnext').onclick=stepFwd;
|
|
1075
|
+
$('bpk').onclick=()=>keyJump(-1);$('bnk').onclick=()=>keyJump(1);
|
|
1076
|
+
$('bplay').onclick=()=>togglePlay();
|
|
1077
|
+
$('speed').innerHTML=SPEEDS.map(v=>`<option value="${v}">${v} 回合/秒</option>`).join('');
|
|
1078
|
+
$('speed').onchange=e=>{setSpeed(+e.target.value);e.target.blur()};
|
|
1079
|
+
$('endcard').addEventListener('click',e=>{if(e.target===$('endcard')){showEnd=false;dirty=true}});
|
|
1080
|
+
$('helpin').innerHTML=`<b style="font-size:1.2em">坦克大战回放查看器</b>
|
|
1081
|
+
<h3>键盘</h3><table>
|
|
1082
|
+
<tr><td><kbd>空格</kbd> / <kbd>K</kbd></td><td>播放 / 暂停</td><td><kbd>←</kbd> <kbd>→</kbd></td><td>上一回合 / 下一回合(下一回合会播动画)</td></tr>
|
|
1083
|
+
<tr><td><kbd>Shift</kbd>+<kbd>←</kbd> <kbd>→</kbd></td><td>后退 / 前进 10 回合</td><td><kbd>[</kbd> <kbd>]</kbd></td><td>上一个 / 下一个关键事件(击毁、基地掉血、吃星、故障)</td></tr>
|
|
1084
|
+
<tr><td><kbd>Home</kbd> <kbd>End</kbd></td><td>开局 / 终局</td><td><kbd>+</kbd> <kbd>-</kbd></td><td>加速 / 减速</td></tr>
|
|
1085
|
+
<tr><td><kbd>E</kbd></td><td>收起 / 显示结束画面</td><td><kbd>C</kbd></td><td>显示 / 隐藏坐标</td></tr>
|
|
1086
|
+
<tr><td><kbd>F</kbd></td><td>全屏</td><td><kbd>O</kbd> / <kbd>H</kbd></td><td>打开文件 / 本帮助</td></tr></table>
|
|
1087
|
+
<h3>画面</h3>
|
|
1088
|
+
两方:<span class="tk s0"><i></i>0 号位(下方)</span>橙色,炮塔浅色实心,子弹圆头;<span class="tk s1"><i></i>1 号位(上方)</span>蓝色,炮塔深色空心、车身带斜纹,子弹菱形。<br>
|
|
1089
|
+
车型:快速型尖头窄车身、三角炮塔;火力型圆炮塔、双炮管;装甲型宽车身、方炮塔、粗炮管。<br>
|
|
1090
|
+
坦克头顶的绿格是血量;左下角的金星是等级(2 级的子弹带白环,能打掉钢墙);右下角的圆点是子弹:实心还能打,空心在飞,红色带斜杠是装填中。<br>
|
|
1091
|
+
出生点上的虚影和数字是复活倒计时;补给点上的数字是离下一批星星还有几回合。<br>
|
|
1092
|
+
停在某一回合时,虚线是这一回合子弹飞过的路线,虚线框是刚被打掉的墙,叉是被毁的位置,红杠是没走成的方向,车外一圈红色短线表示这一回合被打中。<br>
|
|
1093
|
+
一回合分两个小步:动画的前一半是第 1 小步,后一半是第 2 小步;被毁、复活、吃星、出星星在回合末。
|
|
1094
|
+
<h3>地址栏参数</h3>
|
|
1095
|
+
写在文件名后面,例如 <code>名字.html?turn=37&p=0.5</code>(<code>#turn=37</code> 也行)。<table>
|
|
1096
|
+
<tr><td><code>turn=N</code></td><td>打开时停在第 N 回合结算后的画面</td></tr>
|
|
1097
|
+
<tr><td><code>p=0.5</code></td><td>和 turn 一起用:停在第 N 回合动画进行到一半的地方(0–1;0.5 是第 1 小步刚走完)</td></tr>
|
|
1098
|
+
<tr><td><code>tail=0.3</code></td><td>和 turn 一起用:停在第 N 回合结算后 0.3 回合的地方(回合末的爆炸、复活、升级特效)</td></tr>
|
|
1099
|
+
<tr><td><code>play=1</code></td><td>打开后自动播放(从 turn 指定的回合起)</td></tr>
|
|
1100
|
+
<tr><td><code>speed=4</code></td><td>播放速度,每秒几回合(0.5、1、2、4、8、16)</td></tr>
|
|
1101
|
+
<tr><td><code>end=0</code></td><td>到终局时不弹结束画面</td></tr>
|
|
1102
|
+
<tr><td><code>coords=0</code></td><td>不显示棋盘四周的坐标</td></tr>
|
|
1103
|
+
<tr><td><code>clean=1</code></td><td>录屏用:隐藏顶栏和时间轴图例</td></tr>
|
|
1104
|
+
<tr><td><code>check=1</code></td><td>自查:逐回合核对动画用的小步重算和回放数据是否一致,结果显示在顶栏</td></tr></table>
|
|
1105
|
+
<div style="margin-top:.8em;text-align:right"><button id="bhelpx">关闭</button></div>`;
|
|
1106
|
+
const toggleHelp=on=>$('help').classList.toggle('on',on);
|
|
1107
|
+
$('bhelp').onclick=()=>toggleHelp(true);$('bhelpx').onclick=()=>toggleHelp(false);
|
|
1108
|
+
$('help').addEventListener('click',e=>{if(e.target===$('help'))toggleHelp(false)});
|
|
1109
|
+
addEventListener('keydown',e=>{
|
|
1110
|
+
if(e.ctrlKey||e.metaKey||e.altKey)return;
|
|
1111
|
+
const tag=e.target.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;
|
|
1112
|
+
if($('help').classList.contains('on')){if(e.key==='Escape'||e.key==='h'||e.key==='H'||e.key==='?')toggleHelp(false);return}
|
|
1113
|
+
const k=e.key;let used=true;
|
|
1114
|
+
if(k===' '||k==='k'||k==='K')togglePlay();
|
|
1115
|
+
else if(k==='ArrowRight')e.shiftKey?seek(cur+10):stepFwd();
|
|
1116
|
+
else if(k==='ArrowLeft')e.shiftKey?seek(cur-10):seek(prog>0?cur:cur-1);
|
|
1117
|
+
else if(k==='Home')seek(0);else if(k==='End')seek(N);
|
|
1118
|
+
else if(k==='['||k===','||k==='PageUp')keyJump(-1);else if(k===']'||k==='.'||k==='PageDown')keyJump(1);
|
|
1119
|
+
else if(k==='+'||k==='=')setSpeed(SPEEDS[Math.min(SPEEDS.length-1,SPEEDS.indexOf(speed)+1)]);
|
|
1120
|
+
else if(k==='-'||k==='_')setSpeed(SPEEDS[Math.max(0,SPEEDS.indexOf(speed)-1)]);
|
|
1121
|
+
else if(k==='e'||k==='E'){showEnd=!showEnd;dirty=true}
|
|
1122
|
+
else if(k==='c'||k==='C'){showCoords=!showCoords;terrainKey='';dirty=true}
|
|
1123
|
+
else if(k==='f'||k==='F'){document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen&&document.documentElement.requestFullscreen()}
|
|
1124
|
+
else if(k==='o'||k==='O')$('file').click();
|
|
1125
|
+
else if(k==='h'||k==='H'||k==='?')toggleHelp(true);
|
|
1126
|
+
else used=false;
|
|
1127
|
+
if(used)e.preventDefault();
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
// ====================================================================
|
|
1131
|
+
// 启动
|
|
1132
|
+
// ====================================================================
|
|
1133
|
+
(function start(){
|
|
1134
|
+
setSpeed(SPEEDS.includes(argNum('speed',2))?argNum('speed',2):2);
|
|
1135
|
+
if(ARGS.coords==='0')showCoords=false;
|
|
1136
|
+
if(ARGS.clean==='1')document.body.classList.add('clean');
|
|
1137
|
+
if(!EMBED||!load(EMBED)){$('drop').classList.add('on');layout();requestAnimationFrame(frame);return}
|
|
1138
|
+
const turn=clamp(Math.round(argNum('turn',0)),0,N),p=argNum('p',0),tl=argNum('tail',-1);
|
|
1139
|
+
if(p>0&&p<1&&turn>=1){cur=turn-1;prog=p}else{cur=turn;prog=0;tail=tl>=0?Math.min(tl,TAIL_MAX):TAIL_MAX;holdTail=tl>=0}
|
|
1140
|
+
if(ARGS.end==='0')showEnd=false;
|
|
1141
|
+
if(ARGS.check==='1'){const bad=selfCheck(),el=document.createElement('span');el.id='selfcheck';el.className='warnchip';
|
|
1142
|
+
el.style.background=bad.length?'var(--bad)':'var(--good)';el.textContent=bad.length?`自查:${bad.length} 处不一致`:`自查通过(${N} 回合)`;el.title=bad.join('\n');el.dataset.detail=bad.join(' | ');$('meta').append(el)}
|
|
1143
|
+
layout();paint();
|
|
1144
|
+
const keepEnd=showEnd;
|
|
1145
|
+
if(ARGS.play==='1'){togglePlay(true);showEnd=keepEnd}
|
|
1146
|
+
requestAnimationFrame(frame);
|
|
1147
|
+
})();
|
|
1148
|
+
</script>
|
|
1149
|
+
</body>
|
|
1150
|
+
</html>
|