poi-plugin-compass 0.1.5 → 0.1.7
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 +9 -0
- package/assets/compass.css +78 -8
- package/assets/icons/parameter/AA.svg +11 -0
- package/assets/icons/parameter/armor.svg +22 -0
- package/assets/icons/parameter/firepower.svg +14 -0
- package/assets/icons/parameter/torpedo.svg +22 -0
- package/data/kcnav-node-types-by-map.json +1 -1
- package/data/normal-maps.json +4 -3
- package/index.js +125 -21
- package/logic.js +94 -1
- package/package.json +5 -4
- package/scripts/cache-kcnav-maps.js +11 -2
- package/storage.js +47 -0
package/README.md
CHANGED
|
@@ -49,3 +49,12 @@ npm install poi-plugin-compass
|
|
|
49
49
|
- 地图、规则和敌舰资料随插件提供;舰船头像在 poi 图片缓存不可用时可能请求游戏图片服务。
|
|
50
50
|
|
|
51
51
|
问题反馈:<https://github.com/RikaKagurasaka/poi-plugin-compass/issues>
|
|
52
|
+
|
|
53
|
+
### 设置存储
|
|
54
|
+
|
|
55
|
+
地图、阶段、舰队、显示偏好、路线覆盖和导出选择保存在 poi 数据目录下的
|
|
56
|
+
`compass/preferences.json`。首次使用新版时,自动从当前插件窗口的
|
|
57
|
+
`poi-plugin-compass.preferences.v1` localStorage 项导入;旧项保留作备份,
|
|
58
|
+
已有文件优先,不会再次被旧设置覆盖。设置通过临时文件写入后替换正式文件;
|
|
59
|
+
读取或迁移失败时保留原数据并在控制台报告错误,不会覆盖损坏文件。
|
|
60
|
+
从前曾在主窗口和独立窗口保存不同设置时,以首次成功迁移的窗口为准。
|
package/assets/compass.css
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
.poi-compass {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
height: 100%;
|
|
3
|
+
min-height: 0;
|
|
4
|
+
overflow: hidden;
|
|
4
5
|
color: var(--color-text, #f1f5f9);
|
|
5
6
|
background: var(--color-background, #20242b);
|
|
6
7
|
box-sizing: border-box;
|
|
7
8
|
font-size: 13px;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
.compass-scroll-viewport {
|
|
12
|
+
height: 100%;
|
|
13
|
+
max-height: 100vh;
|
|
14
|
+
min-height: 0;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
overflow-x: hidden;
|
|
17
|
+
overflow-y: auto;
|
|
18
|
+
overscroll-behavior: contain;
|
|
19
|
+
padding: 10px;
|
|
20
|
+
scrollbar-gutter: stable;
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
.compass-titlebar {
|
|
11
24
|
display: flex;
|
|
12
25
|
align-items: center;
|
|
@@ -57,7 +70,10 @@
|
|
|
57
70
|
.compass-los-icons { display: flex; gap: 6px; flex-shrink: 0; }
|
|
58
71
|
.compass-los-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
59
72
|
.compass-los-value { color: #a9daf8; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
60
|
-
.compass-fleet-air { color: #91d9b2; font-weight: 650; font-variant-numeric: tabular-nums; white-space: nowrap; flex-shrink: 0; }
|
|
73
|
+
.compass-fleet-air { display: inline-flex; align-items: center; gap: 3px; color: #91d9b2; font-weight: 650; font-variant-numeric: tabular-nums; white-space: nowrap; flex-shrink: 0; }
|
|
74
|
+
.compass-fleet-air-icon,
|
|
75
|
+
.compass-fleet-air-icon img { display: inline-flex; width: 22px; height: 22px; align-items: center; justify-content: center; }
|
|
76
|
+
.compass-fleet-air-icon img { object-fit: contain; }
|
|
61
77
|
.compass-los-icon { position: relative; display: inline-flex; width: 28px; height: 28px; align-items: center; justify-content: center; }
|
|
62
78
|
.compass-los-icon img { width: 28px; height: 28px; }
|
|
63
79
|
.compass-los-coefficient { position: absolute; right: -1px; bottom: -3px; font-size: 14px; font-weight: 800; color: white; -webkit-text-stroke: 3px #151a22; paint-order: stroke fill; }
|
|
@@ -456,7 +472,8 @@
|
|
|
456
472
|
|
|
457
473
|
.compass-enemy-entry summary {
|
|
458
474
|
display: grid;
|
|
459
|
-
grid-template-columns: 58px 64px
|
|
475
|
+
grid-template-columns: 58px 64px max-content max-content;
|
|
476
|
+
justify-content: start;
|
|
460
477
|
align-items: center;
|
|
461
478
|
gap: 6px;
|
|
462
479
|
padding: 7px 2px;
|
|
@@ -493,19 +510,72 @@
|
|
|
493
510
|
overflow-wrap: anywhere;
|
|
494
511
|
}
|
|
495
512
|
|
|
496
|
-
.compass-enemy-entry { min-width:
|
|
513
|
+
.compass-enemy-entry { min-width: 540px; }
|
|
497
514
|
.compass-enemy-stats { margin-top: 8px; }
|
|
498
515
|
.compass-enemy-stat-row {
|
|
499
516
|
display: grid;
|
|
500
|
-
grid-template-columns: 40px repeat(5,
|
|
517
|
+
grid-template-columns: 40px repeat(5, 42px) minmax(150px, 1fr);
|
|
501
518
|
align-items: center;
|
|
502
|
-
gap:
|
|
519
|
+
gap: 6px;
|
|
503
520
|
padding: 4px 0;
|
|
504
521
|
font-variant-numeric: tabular-nums;
|
|
505
522
|
}
|
|
506
523
|
.compass-enemy-stat-row > span:not(:first-child):not(:last-child) { text-align: right; }
|
|
507
524
|
.compass-enemy-stat-row.is-escort-start { border-top: 1px solid #687587; margin-top: 5px; }
|
|
508
|
-
.compass-enemy-air { text-align:
|
|
525
|
+
.compass-enemy-air { text-align: left; white-space: nowrap; }
|
|
526
|
+
|
|
527
|
+
.compass-enemy-equips {
|
|
528
|
+
display: grid;
|
|
529
|
+
grid-template-columns: repeat(5, 28px);
|
|
530
|
+
align-items: center;
|
|
531
|
+
gap: 2px;
|
|
532
|
+
min-width: 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.compass-enemy-equip {
|
|
536
|
+
display: inline-flex;
|
|
537
|
+
width: 28px;
|
|
538
|
+
height: 28px;
|
|
539
|
+
align-items: center;
|
|
540
|
+
justify-content: center;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.compass-enemy-equip img {
|
|
544
|
+
display: block;
|
|
545
|
+
width: 28px;
|
|
546
|
+
height: 28px;
|
|
547
|
+
object-fit: contain;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.compass-enemy-equip.is-empty { background: transparent; }
|
|
551
|
+
|
|
552
|
+
.compass-enemy-stat {
|
|
553
|
+
display: inline-flex;
|
|
554
|
+
min-width: 0;
|
|
555
|
+
align-items: center;
|
|
556
|
+
gap: 2px;
|
|
557
|
+
white-space: nowrap;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.compass-enemy-stat-icon {
|
|
561
|
+
display: inline-block;
|
|
562
|
+
width: 18px;
|
|
563
|
+
height: 18px;
|
|
564
|
+
flex: 0 0 18px;
|
|
565
|
+
object-fit: contain;
|
|
566
|
+
vertical-align: middle;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.compass-enemy-stat-icon.fa {
|
|
570
|
+
color: #f1f5f9;
|
|
571
|
+
font-size: 16px;
|
|
572
|
+
line-height: 18px;
|
|
573
|
+
text-align: center;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.compass-enemy-stat-value {
|
|
577
|
+
font-variant-numeric: tabular-nums;
|
|
578
|
+
}
|
|
509
579
|
|
|
510
580
|
.compass-condition.is-unknown {
|
|
511
581
|
border-left-color: #e0af56;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 16.0.0, Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
5
|
+
width="800px" height="800px" viewBox="0 0 800 800" enable-background="new 0 0 800 800" xml:space="preserve">
|
|
6
|
+
<rect x="125" y="400" fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" width="601" height="200"/>
|
|
7
|
+
<line fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" x1="125" y1="441" x2="726" y2="441"/>
|
|
8
|
+
<path fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" d="M726,400H164c0-27.88,11.47-54.16,31.75-77.25
|
|
9
|
+
L73,200h30l107.71,107.71C261.01,262.66,347.12,233,445,233C600.32,233,726,307.69,726,400z"/>
|
|
10
|
+
<path fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" d="M303,400c0-88.605,98.869-160.304,221.054-160.304"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 16.0.0, Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
5
|
+
width="800px" height="800px" viewBox="0 0 800 800" enable-background="new 0 0 800 800" xml:space="preserve">
|
|
6
|
+
<polygon fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="615,643.5 586,656.5 215,643.5
|
|
7
|
+
186,156.75 215,144 615,144 "/>
|
|
8
|
+
<rect x="186" y="157" fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" width="400" height="500"/>
|
|
9
|
+
<line fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" x1="586" y1="156.5" x2="615" y2="143.5"/>
|
|
10
|
+
<polygon fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="240.975,226 235.95,217 240.975,208
|
|
11
|
+
251.025,208 256.05,217 251.025,226 "/>
|
|
12
|
+
<polygon fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="240.975,606 235.95,597 240.975,588
|
|
13
|
+
251.025,588 256.05,597 251.025,606 "/>
|
|
14
|
+
<polygon fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="240.975,416 235.95,407 240.975,398
|
|
15
|
+
251.025,398 256.05,407 251.025,416 "/>
|
|
16
|
+
<polygon fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="521.975,226 516.95,217 521.975,208
|
|
17
|
+
532.025,208 537.05,217 532.025,226 "/>
|
|
18
|
+
<polygon fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="521.975,606 516.95,597 521.975,588
|
|
19
|
+
532.025,588 537.05,597 532.025,606 "/>
|
|
20
|
+
<polygon fill="none" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="521.975,416 516.95,407 521.975,398
|
|
21
|
+
532.025,398 537.05,407 532.025,416 "/>
|
|
22
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 16.0.0, Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="800px"
|
|
5
|
+
height="800px" viewBox="0 0 800 800" enable-background="new 0 0 800 800" xml:space="preserve">
|
|
6
|
+
<g id="Layer_1">
|
|
7
|
+
</g>
|
|
8
|
+
<g id="Layer_2">
|
|
9
|
+
<polygon fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="748,424 748,547 708,547 708,661
|
|
10
|
+
92,661 92,547 52,547 52,424 92,424 92,335 368.566,335 552.831,139 626.871,139 442.606,335 490.197,335 674.461,139 748.51,139
|
|
11
|
+
564.246,335 708,335 708,424 ">
|
|
12
|
+
</polygon>
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 16.0.0, Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
5
|
+
width="800px" height="800px" viewBox="0 0 800 800" enable-background="new 0 0 800 800" xml:space="preserve">
|
|
6
|
+
<g>
|
|
7
|
+
<polyline fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="339.943,481.163 420.951,562.173
|
|
8
|
+
337.583,681.722 278.595,622.733 220.395,564.534 339.701,480.924 "/>
|
|
9
|
+
<path fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" d="M339.155,480.377
|
|
10
|
+
c-32.604,32.605-83.143,119.773-60.56,142.356c22.583,22.583,109.752-27.955,142.356-60.561L627.014,356.11
|
|
11
|
+
c47.386-47.383,67.413-104.042,44.829-126.625c-22.581-22.583-79.239-2.554-126.626,44.83L339.155,480.377z"/>
|
|
12
|
+
<line fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" x1="545.217" y1="274.314" x2="627.014" y2="356.11"/>
|
|
13
|
+
</g>
|
|
14
|
+
<g>
|
|
15
|
+
<polyline fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" points="238.324,379.087 319.333,460.097
|
|
16
|
+
235.964,579.646 176.977,520.656 118.777,462.457 238.083,378.847 "/>
|
|
17
|
+
<path fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" d="M237.537,378.301
|
|
18
|
+
c-32.604,32.604-83.143,119.773-60.561,142.355c22.583,22.584,109.752-27.955,142.357-60.56l206.062-206.063
|
|
19
|
+
c47.385-47.385,67.412-104.043,44.83-126.626c-22.582-22.582-79.241-2.555-126.627,44.83L237.537,378.301z"/>
|
|
20
|
+
<line fill="#F2F2F2" stroke="#4D4D4D" stroke-width="10" stroke-miterlimit="10" x1="443.599" y1="172.238" x2="525.396" y2="254.034"/>
|
|
21
|
+
</g>
|
|
22
|
+
</svg>
|
package/data/normal-maps.json
CHANGED
|
@@ -1138,9 +1138,10 @@
|
|
|
1138
1138
|
"name": "拉包尔方面海域",
|
|
1139
1139
|
"start": "1",
|
|
1140
1140
|
"boss": "G",
|
|
1141
|
-
"manualNodes": ["I"],
|
|
1141
|
+
"manualNodes": ["I", "O"],
|
|
1142
1142
|
"manualOutcomes": {
|
|
1143
|
-
"I": [{ "to": "J", "probability": null }, { "to": "O", "probability": null }]
|
|
1143
|
+
"I": [{ "to": "J", "probability": null }, { "to": "O", "probability": null }],
|
|
1144
|
+
"O": [{ "to": "P", "probability": null }, { "to": "Q", "probability": null }]
|
|
1144
1145
|
},
|
|
1145
1146
|
"sourceRefs": [
|
|
1146
1147
|
{ "kind": "kcwiki", "url": "https://zh.kcwiki.cn/wiki/南方海域/5-6", "checkedAt": "2026-09-11" },
|
|
@@ -1186,7 +1187,7 @@
|
|
|
1186
1187
|
{ "id": "5-6-L-fallback", "node": "L", "priority": 999, "predicate": { "kind": "always", "label": "索敌不足" }, "outcomes": [{ "to": "M", "probability": 1 }], "confidence": "approximate" },
|
|
1187
1188
|
{ "id": "5-6-I-p2", "node": "I", "priority": 10, "predicate": { "kind": "phase", "phase": "P2", "label": "P2:向下前往 J" }, "outcomes": [{ "to": "J", "probability": 1 }], "confidence": "verified" },
|
|
1188
1189
|
{ "id": "5-6-I-p3", "node": "I", "priority": 20, "predicate": { "kind": "phase", "phase": "P3", "label": "P3:向上前往 O" }, "outcomes": [{ "to": "O", "probability": 1 }], "confidence": "verified" },
|
|
1189
|
-
{ "id": "5-6-O-
|
|
1190
|
+
{ "id": "5-6-O-manual", "node": "O", "priority": 1, "predicate": { "kind": "always", "label": "能动分歧:选择 P / Q" }, "outcomes": [{ "to": "P", "probability": null }, { "to": "Q", "probability": null }], "confidence": "verified" },
|
|
1190
1191
|
{ "id": "5-6-Q-locked", "node": "Q", "priority": 10, "predicate": { "kind": "flag", "flag": "qRoutes", "value": false, "label": "Q-U/Q-W 路线未解锁" }, "outcomes": [{ "to": "Q2", "probability": 1 }], "confidence": "verified" },
|
|
1191
1192
|
{ "id": "5-6-Q-fast-plus", "node": "Q", "priority": 20, "predicate": { "kind": "speed", "mode": "highPlus" }, "outcomes": [{ "to": "W", "probability": 1 }], "confidence": "verified" },
|
|
1192
1193
|
{ "id": "5-6-Q-high-light", "node": "Q", "priority": 30, "predicate": { "kind": "all", "predicates": [{ "kind": "count", "category": "CL", "op": ">=", "value": 1 }, { "kind": "count", "category": "DD", "op": ">=", "value": 3 }, { "kind": "speed", "mode": "allHigh" }] }, "outcomes": [{ "to": "W", "probability": 1 }], "confidence": "verified" },
|
package/index.js
CHANGED
|
@@ -27,7 +27,11 @@ const mapAreas = [
|
|
|
27
27
|
]
|
|
28
28
|
const LOCAL_KCNAV_ASSET_ROOT = path.join(__dirname, 'assets', 'kcnav')
|
|
29
29
|
const LOCAL_PROPHET_SPOT_ICON_ROOT = path.join(__dirname, 'assets', 'icons', 'spot')
|
|
30
|
-
const
|
|
30
|
+
const LOCAL_PROPHET_PARAMETER_ICON_ROOT = path.join(__dirname, 'assets', 'icons', 'parameter')
|
|
31
|
+
const preferencesStorage = require('./storage')
|
|
32
|
+
const preferencesBase = () => typeof APPDATA_PATH !== 'undefined'
|
|
33
|
+
? APPDATA_PATH
|
|
34
|
+
: path.join(process.env.HOME || process.cwd(), '.poi-dark')
|
|
31
35
|
|
|
32
36
|
const MAP_PHASES = {
|
|
33
37
|
'5-6': [
|
|
@@ -50,6 +54,13 @@ const MAP_PHASES = {
|
|
|
50
54
|
],
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
const MAP_ROUTE_OPTIONS = {
|
|
58
|
+
'5-6': [
|
|
59
|
+
{ id: 'locked', label: '未解锁', flags: { qRoutes: false } },
|
|
60
|
+
{ id: 'unlocked', label: '已解锁', flags: { qRoutes: true } },
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
function phaseOptionsForMap(mapId) {
|
|
54
65
|
return MAP_PHASES[mapId] || []
|
|
55
66
|
}
|
|
@@ -58,10 +69,25 @@ function phaseOptionForMap(mapId, phaseId) {
|
|
|
58
69
|
return phaseOptionsForMap(mapId).find((phase) => phase.id === phaseId) || phaseOptionsForMap(mapId)[0] || null
|
|
59
70
|
}
|
|
60
71
|
|
|
61
|
-
function
|
|
72
|
+
function routeOptionsForMap(mapId) {
|
|
73
|
+
return MAP_ROUTE_OPTIONS[mapId] || []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function defaultRouteOptionId(mapId) {
|
|
77
|
+
return routeOptionsForMap(mapId)[0]?.id || null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function routeOptionForMap(mapId, routeOptionId) {
|
|
81
|
+
return routeOptionsForMap(mapId).find((option) => option.id === routeOptionId)
|
|
82
|
+
|| routeOptionsForMap(mapId)[0]
|
|
83
|
+
|| null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function phaseFlags(mapId, phaseId, routeOptionId) {
|
|
62
87
|
const flags = {}
|
|
63
88
|
if (mapId === '7-3') flags.phase1 = phaseId === 'P1'
|
|
64
|
-
|
|
89
|
+
const routeOption = routeOptionForMap(mapId, routeOptionId)
|
|
90
|
+
if (routeOption?.flags) Object.assign(flags, routeOption.flags)
|
|
65
91
|
return flags
|
|
66
92
|
}
|
|
67
93
|
|
|
@@ -187,12 +213,10 @@ function normalizeOverrides(value) {
|
|
|
187
213
|
}
|
|
188
214
|
|
|
189
215
|
function readCompassPreferences() {
|
|
190
|
-
const storage = compassStorage()
|
|
191
|
-
if (!storage) return {}
|
|
192
216
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
217
|
+
return preferencesStorage.load(preferencesBase(), compassStorage())
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.warn('[compass] 无法读取设置;原数据已保留', error)
|
|
196
220
|
return {}
|
|
197
221
|
}
|
|
198
222
|
}
|
|
@@ -353,6 +377,10 @@ function prophetSpotIconUrl(icon) {
|
|
|
353
377
|
return pathToFileURL(path.join(LOCAL_PROPHET_SPOT_ICON_ROOT, `${icon}.svg`)).toString()
|
|
354
378
|
}
|
|
355
379
|
|
|
380
|
+
function prophetParameterIconUrl(icon) {
|
|
381
|
+
return pathToFileURL(path.join(LOCAL_PROPHET_PARAMETER_ICON_ROOT, `${icon}.svg`)).toString()
|
|
382
|
+
}
|
|
383
|
+
|
|
356
384
|
function nodeMarker(nodeType, nodeInfo) {
|
|
357
385
|
const prophetIcon = PROPHET_NODE_ICONS[nodeType]
|
|
358
386
|
if (prophetIcon) return { source: 'prophet', url: prophetSpotIconUrl(prophetIcon) }
|
|
@@ -662,11 +690,14 @@ class Compass extends React.Component {
|
|
|
662
690
|
const initialDeckId = [1, 2, 3, 4].includes(Number(preferences.deckId)) ? Number(preferences.deckId) : 1
|
|
663
691
|
const preferredPhaseId = preferences.phaseByMap?.[initialMapId] ?? preferences.phaseId
|
|
664
692
|
const initialPhaseId = phaseOptionForMap(initialMapId, preferredPhaseId)?.id || null
|
|
693
|
+
const initialRouteOptionId = routeOptionForMap(initialMapId, preferences.routeOptionByMap?.[initialMapId])?.id
|
|
694
|
+
|| defaultRouteOptionId(initialMapId)
|
|
665
695
|
const initialExportPreferences = storedExportPreferencesForView(preferences, initialMapId, initialPhaseId, initialDeckId)
|
|
666
696
|
this.state = {
|
|
667
697
|
deckId: initialDeckId,
|
|
668
698
|
mapId: initialMapId,
|
|
669
699
|
phaseId: initialPhaseId,
|
|
700
|
+
routeOptionId: initialRouteOptionId,
|
|
670
701
|
selectedNode: pluginState.currentNode || null,
|
|
671
702
|
selectedEdge: null,
|
|
672
703
|
overrides: storedOverridesForView(preferences, initialMapId, initialPhaseId, initialDeckId),
|
|
@@ -703,10 +734,8 @@ class Compass extends React.Component {
|
|
|
703
734
|
}
|
|
704
735
|
|
|
705
736
|
persistPreferences() {
|
|
706
|
-
const storage = compassStorage()
|
|
707
|
-
if (!storage) return
|
|
708
737
|
try {
|
|
709
|
-
const previous =
|
|
738
|
+
const previous = preferencesStorage.load(preferencesBase(), compassStorage())
|
|
710
739
|
const overridesByView = isRecord(previous.overridesByView) ? { ...previous.overridesByView } : {}
|
|
711
740
|
overridesByView[viewPreferenceKey(this.state.mapId, this.state.phaseId, this.state.deckId)] = normalizeOverrides(this.state.overrides)
|
|
712
741
|
const exportByView = isRecord(previous.exportByView) ? { ...previous.exportByView } : {}
|
|
@@ -717,20 +746,23 @@ class Compass extends React.Component {
|
|
|
717
746
|
}
|
|
718
747
|
const phaseByMap = isRecord(previous.phaseByMap) ? { ...previous.phaseByMap } : {}
|
|
719
748
|
phaseByMap[this.state.mapId] = this.state.phaseId
|
|
720
|
-
|
|
749
|
+
const routeOptionByMap = isRecord(previous.routeOptionByMap) ? { ...previous.routeOptionByMap } : {}
|
|
750
|
+
if (this.state.routeOptionId) routeOptionByMap[this.state.mapId] = this.state.routeOptionId
|
|
751
|
+
preferencesStorage.save(preferencesBase(), {
|
|
721
752
|
...previous,
|
|
722
753
|
mapId: this.state.mapId,
|
|
723
754
|
phaseId: this.state.phaseId,
|
|
724
755
|
phaseByMap,
|
|
756
|
+
routeOptionByMap,
|
|
725
757
|
deckId: this.state.deckId,
|
|
726
758
|
probabilityFormat: this.state.probabilityFormat,
|
|
727
759
|
showEstimatedAir: this.state.showEstimatedAir,
|
|
728
760
|
showEnemyAvatars: this.state.showEnemyAvatars,
|
|
729
761
|
overridesByView,
|
|
730
762
|
exportByView,
|
|
731
|
-
})
|
|
732
|
-
} catch (
|
|
733
|
-
|
|
763
|
+
})
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.warn('[compass] 无法保存设置;原数据已保留', error)
|
|
734
766
|
}
|
|
735
767
|
}
|
|
736
768
|
|
|
@@ -743,10 +775,13 @@ class Compass extends React.Component {
|
|
|
743
775
|
selectMap(mapId) {
|
|
744
776
|
const preferences = readCompassPreferences()
|
|
745
777
|
const phaseId = phaseOptionForMap(mapId, preferences.phaseByMap?.[mapId])?.id || null
|
|
778
|
+
const routeOptionId = routeOptionForMap(mapId, preferences.routeOptionByMap?.[mapId])?.id
|
|
779
|
+
|| defaultRouteOptionId(mapId)
|
|
746
780
|
const exportPreferences = storedExportPreferencesForView(preferences, mapId, phaseId, this.state.deckId)
|
|
747
781
|
this.setState({
|
|
748
782
|
mapId,
|
|
749
783
|
phaseId,
|
|
784
|
+
routeOptionId,
|
|
750
785
|
selectedNode: null,
|
|
751
786
|
selectedEdge: null,
|
|
752
787
|
overrides: storedOverridesForView(preferences, mapId, phaseId, this.state.deckId),
|
|
@@ -800,6 +835,11 @@ class Compass extends React.Component {
|
|
|
800
835
|
this.setState({ phaseId, selectedNode: null, selectedEdge: null, overrides, ...exportPreferences, exportError: null })
|
|
801
836
|
}
|
|
802
837
|
|
|
838
|
+
selectRouteOption(routeOptionId) {
|
|
839
|
+
if (!routeOptionForMap(this.state.mapId, routeOptionId)) return
|
|
840
|
+
this.setState({ routeOptionId, selectedNode: null, selectedEdge: null, exportRoute: null, exportError: null })
|
|
841
|
+
}
|
|
842
|
+
|
|
803
843
|
selectNode(node) {
|
|
804
844
|
this.setState({ selectedNode: node, selectedEdge: null })
|
|
805
845
|
}
|
|
@@ -825,8 +865,15 @@ class Compass extends React.Component {
|
|
|
825
865
|
|
|
826
866
|
renderFleetAir(air) {
|
|
827
867
|
if (!air || !Number.isFinite(air.min) || !Number.isFinite(air.max) || air.max <= 0) return null
|
|
828
|
-
|
|
829
|
-
|
|
868
|
+
const { SlotitemIcon } = require('views/components/etc/icon')
|
|
869
|
+
const value = air.min === air.max ? air.min : `${air.min}~${air.max}`
|
|
870
|
+
return h('span', {
|
|
871
|
+
className: 'compass-fleet-air',
|
|
872
|
+
title: `poi 当前舰队制空(熟练度范围):${value}`,
|
|
873
|
+
'aria-label': `当前舰队制空 ${value}`,
|
|
874
|
+
},
|
|
875
|
+
h('span', { className: 'compass-fleet-air-icon', 'aria-hidden': 'true' }, h(SlotitemIcon, { slotitemId: 6, alt: '' })),
|
|
876
|
+
h('span', { className: 'compass-fleet-air-value' }, value))
|
|
830
877
|
}
|
|
831
878
|
|
|
832
879
|
setManualOverride(node, to, edgeId = null, selectedNode = node) {
|
|
@@ -1181,6 +1228,45 @@ class Compass extends React.Component {
|
|
|
1181
1228
|
return `${master.api_name}${rank}`
|
|
1182
1229
|
}
|
|
1183
1230
|
const equipName = (id) => masters.$equips?.[id]?.api_name || `#${id}`
|
|
1231
|
+
const equipIconId = (id) => {
|
|
1232
|
+
const iconId = Number(masters.$equips?.[id]?.api_type?.[3])
|
|
1233
|
+
return Number.isFinite(iconId) && iconId > 0 ? iconId : Number(id)
|
|
1234
|
+
}
|
|
1235
|
+
const { SlotitemIcon } = require('views/components/etc/icon')
|
|
1236
|
+
const FontAwesome = require('react-fontawesome')
|
|
1237
|
+
const statDefinitions = {
|
|
1238
|
+
hp: { label: 'HP', icon: 'heart' },
|
|
1239
|
+
fp: { label: '火力', icon: 'firepower' },
|
|
1240
|
+
torp: { label: '雷装', icon: 'torpedo' },
|
|
1241
|
+
aa: { label: '对空', icon: 'AA' },
|
|
1242
|
+
armor: { label: '装甲', icon: 'armor' },
|
|
1243
|
+
}
|
|
1244
|
+
const renderEnemyStat = (field, value) => {
|
|
1245
|
+
const definition = statDefinitions[field]
|
|
1246
|
+
const icon = definition.icon === 'heart'
|
|
1247
|
+
? h(FontAwesome, { name: 'heart', className: 'compass-enemy-stat-icon', 'aria-hidden': 'true' })
|
|
1248
|
+
: h('img', { className: 'compass-enemy-stat-icon', src: prophetParameterIconUrl(definition.icon), alt: '' })
|
|
1249
|
+
return h('span', {
|
|
1250
|
+
key: field,
|
|
1251
|
+
className: `compass-enemy-stat compass-enemy-stat-${field}`,
|
|
1252
|
+
title: definition.label,
|
|
1253
|
+
'aria-label': `${definition.label} ${value ?? '—'}`,
|
|
1254
|
+
}, icon, h('span', { className: 'compass-enemy-stat-value' }, value ?? '—'))
|
|
1255
|
+
}
|
|
1256
|
+
const renderEnemyEquip = (id, index) => {
|
|
1257
|
+
if (!(Number(id) > 0)) return h('span', {
|
|
1258
|
+
key: `empty-${index}`,
|
|
1259
|
+
className: 'compass-enemy-equip is-empty',
|
|
1260
|
+
'aria-hidden': 'true',
|
|
1261
|
+
})
|
|
1262
|
+
const name = equipName(id)
|
|
1263
|
+
return h('span', {
|
|
1264
|
+
key: `${id}-${index}`,
|
|
1265
|
+
className: 'compass-enemy-equip',
|
|
1266
|
+
title: name,
|
|
1267
|
+
'aria-label': name,
|
|
1268
|
+
}, h(SlotitemIcon, { slotitemId: equipIconId(id), alt: '' }))
|
|
1269
|
+
}
|
|
1184
1270
|
return h('section', { className: 'compass-panel compass-enemies' }, heading,
|
|
1185
1271
|
data.rows.length ? h('div', { className: 'compass-enemy-air-range' }, `空优 ${rangeText(data.superiority)} / 空确 ${rangeText(data.supremacy)}${data.uncertain ? '?' : ''}`) : null,
|
|
1186
1272
|
!data.rows.length ? h('div', { className: 'compass-muted' }, '此期间无记录') : null,
|
|
@@ -1203,11 +1289,13 @@ class Compass extends React.Component {
|
|
|
1203
1289
|
h('div', null, `陆航:${airText(entry.lbasThresholds)}`),
|
|
1204
1290
|
entry.uncertain ? h('div', null, `装备不确定:${entry.uncertainAirpowerItems.join('、')}`) : null,
|
|
1205
1291
|
h('div', { className: 'compass-enemy-stats' },
|
|
1206
|
-
h('div', { className: 'compass-enemy-stat-row compass-muted' }, ...['', 'HP', '火力', '雷装', '对空', '装甲', '装备'].map((label, i) => h('span', { key: i }, label))),
|
|
1207
1292
|
...fleet.map((ship, shipIndex) => h('div', { key: shipIndex, className: `compass-enemy-stat-row${shipIndex === entry.mainFleet.length ? ' is-escort-start' : ''}` },
|
|
1208
1293
|
this.renderShipPortrait(ship.id, shipName(ship), getRootState()),
|
|
1209
|
-
...['hp', 'fp', 'torp', 'aa', 'armor'].map(field =>
|
|
1210
|
-
h('span', { className: 'compass-enemy-equips' },
|
|
1294
|
+
...['hp', 'fp', 'torp', 'aa', 'armor'].map(field => renderEnemyStat(field, ship[field])),
|
|
1295
|
+
h('span', { className: 'compass-enemy-equips' },
|
|
1296
|
+
Array.isArray(ship.equips) && ship.equips.length
|
|
1297
|
+
? ship.equips.map(renderEnemyEquip)
|
|
1298
|
+
: h('span', { className: 'compass-enemy-equip is-empty' }, '—')),
|
|
1211
1299
|
)),
|
|
1212
1300
|
),
|
|
1213
1301
|
),
|
|
@@ -1299,7 +1387,7 @@ class Compass extends React.Component {
|
|
|
1299
1387
|
admiralCoefficient: mapLosConfig.admiralCoefficient,
|
|
1300
1388
|
passedNodes: pluginState.currentMapId === this.state.mapId ? pluginState.passedNodes : [],
|
|
1301
1389
|
phase: phase?.id || null,
|
|
1302
|
-
flags: phaseFlags(this.state.mapId, phase?.id),
|
|
1390
|
+
flags: phaseFlags(this.state.mapId, phase?.id, this.state.routeOptionId),
|
|
1303
1391
|
})
|
|
1304
1392
|
const geometry = mapGeometry(this.state.mapId)
|
|
1305
1393
|
const displayMapDefinition = phase?.boss
|
|
@@ -1325,6 +1413,7 @@ class Compass extends React.Component {
|
|
|
1325
1413
|
].filter(Boolean)
|
|
1326
1414
|
|
|
1327
1415
|
return h('div', { className: 'poi-compass' },
|
|
1416
|
+
h('div', { className: 'compass-scroll-viewport' },
|
|
1328
1417
|
h('link', { rel: 'stylesheet', href: cssPath }),
|
|
1329
1418
|
h('header', { className: 'compass-titlebar' }, h('h3', null, '舰队罗盘')),
|
|
1330
1419
|
h('div', { className: 'compass-toolbar' },
|
|
@@ -1342,6 +1431,18 @@ class Compass extends React.Component {
|
|
|
1342
1431
|
}, option.label)),
|
|
1343
1432
|
)
|
|
1344
1433
|
: null,
|
|
1434
|
+
routeOptionsForMap(this.state.mapId).length > 1
|
|
1435
|
+
? h('div', { className: 'compass-phase-group', role: 'group', 'aria-label': '路线解锁状态' },
|
|
1436
|
+
h('span', { className: 'compass-option-label' }, 'Q路线'),
|
|
1437
|
+
routeOptionsForMap(this.state.mapId).map((option) => h('button', {
|
|
1438
|
+
key: option.id,
|
|
1439
|
+
type: 'button',
|
|
1440
|
+
className: routeOptionForMap(this.state.mapId, this.state.routeOptionId)?.id === option.id ? 'is-active' : '',
|
|
1441
|
+
'aria-pressed': routeOptionForMap(this.state.mapId, this.state.routeOptionId)?.id === option.id,
|
|
1442
|
+
onClick: () => this.selectRouteOption(option.id),
|
|
1443
|
+
}, option.label)),
|
|
1444
|
+
)
|
|
1445
|
+
: null,
|
|
1345
1446
|
h('div', { className: 'compass-option-group', role: 'group', 'aria-label': '概率标签格式' },
|
|
1346
1447
|
h('span', { className: 'compass-option-label' }, '概率'),
|
|
1347
1448
|
...[
|
|
@@ -1398,6 +1499,7 @@ class Compass extends React.Component {
|
|
|
1398
1499
|
),
|
|
1399
1500
|
),
|
|
1400
1501
|
h('div', { className: 'compass-enemy-region' }, this.renderEnemyPanel(this.state.mapId, selectedNode)),
|
|
1502
|
+
),
|
|
1401
1503
|
)
|
|
1402
1504
|
}
|
|
1403
1505
|
}
|
|
@@ -1423,6 +1525,8 @@ exports.__test = {
|
|
|
1423
1525
|
nodeIconUrl,
|
|
1424
1526
|
losConfig: losCatalog.maps,
|
|
1425
1527
|
phaseOptionsForMap,
|
|
1528
|
+
routeOptionsForMap,
|
|
1529
|
+
routeOptionForMap,
|
|
1426
1530
|
pluginState,
|
|
1427
1531
|
routeEdges,
|
|
1428
1532
|
ruleRows,
|
package/logic.js
CHANGED
|
@@ -290,6 +290,17 @@ function hasPhaseRules(mapDefinition, node) {
|
|
|
290
290
|
return rulesForNode(mapDefinition, node).some((rule) => containsPhasePredicate(rule.predicate))
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
function containsVisitedPredicate(predicate) {
|
|
294
|
+
if (!predicate || typeof predicate !== 'object') return false
|
|
295
|
+
if (predicate.kind === 'visited') return true
|
|
296
|
+
if (Array.isArray(predicate.predicates) && predicate.predicates.some(containsVisitedPredicate)) return true
|
|
297
|
+
return containsVisitedPredicate(predicate.predicate)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function hasVisitedRules(mapDefinition) {
|
|
301
|
+
return (mapDefinition?.rules || []).some((rule) => containsVisitedPredicate(rule.predicate))
|
|
302
|
+
}
|
|
303
|
+
|
|
293
304
|
function selectRouteRule(mapDefinition, node, context) {
|
|
294
305
|
const rules = rulesForNode(mapDefinition, node)
|
|
295
306
|
for (const rule of rules) {
|
|
@@ -562,6 +573,74 @@ function propagateProbability(start, decisions) {
|
|
|
562
573
|
}
|
|
563
574
|
}
|
|
564
575
|
|
|
576
|
+
function propagatePathProbability(start, mapDefinition, context = {}, overrides = {}) {
|
|
577
|
+
const reach = {}
|
|
578
|
+
const edgeMass = {}
|
|
579
|
+
const unknownNodes = new Set()
|
|
580
|
+
const unknownEdges = new Set()
|
|
581
|
+
const estimatedNodes = new Set()
|
|
582
|
+
const estimatedEdges = new Set()
|
|
583
|
+
const decisionVariants = new Map()
|
|
584
|
+
let visits = 0
|
|
585
|
+
|
|
586
|
+
function rememberDecision(node, passedNodes, decision) {
|
|
587
|
+
const variants = decisionVariants.get(node) || []
|
|
588
|
+
const key = passedNodes.join('\u0000')
|
|
589
|
+
if (!variants.some((variant) => variant.key === key)) {
|
|
590
|
+
variants.push({ key, passedNodes, decision })
|
|
591
|
+
decisionVariants.set(node, variants)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function walk(node, passedNodes, mass, path, estimatedPath) {
|
|
596
|
+
if (!Number.isFinite(mass) || mass <= 0) return
|
|
597
|
+
if (++visits > 10000) {
|
|
598
|
+
unknownNodes.add(node)
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
reach[node] = number(reach[node]) + mass
|
|
602
|
+
if (estimatedPath) estimatedNodes.add(node)
|
|
603
|
+
if (path.has(node)) {
|
|
604
|
+
unknownNodes.add(node)
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const decision = routeDecision(mapDefinition, node, { ...context, passedNodes }, overrides)
|
|
609
|
+
rememberDecision(node, passedNodes, decision)
|
|
610
|
+
if (!decision || decision.status === 'terminal' && !decision.outcomes.length
|
|
611
|
+
|| decision.status === 'unknown' && !decision.outcomes.length) {
|
|
612
|
+
if (decision?.status === 'unknown') unknownNodes.add(node)
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
if (decision.status === 'unknown' || Number(decision.unknownProbability) > 1e-9) unknownNodes.add(node)
|
|
616
|
+
|
|
617
|
+
const nextPath = new Set(path)
|
|
618
|
+
nextPath.add(node)
|
|
619
|
+
decision.outcomes.forEach((outcome) => {
|
|
620
|
+
const key = edgeKey(node, outcome.to)
|
|
621
|
+
if (outcome.probability == null) {
|
|
622
|
+
unknownEdges.add(key)
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
const edgeEstimated = estimatedPath || outcome.estimated === true
|
|
626
|
+
if (edgeEstimated) estimatedEdges.add(key)
|
|
627
|
+
edgeMass[key] = number(edgeMass[key]) + mass * outcome.probability
|
|
628
|
+
walk(outcome.to, [...passedNodes, node], mass * outcome.probability, nextPath, edgeEstimated)
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
walk(start, [], 1, new Set(), false)
|
|
633
|
+
return {
|
|
634
|
+
reach,
|
|
635
|
+
edgeMass,
|
|
636
|
+
unknownNodes: Array.from(unknownNodes),
|
|
637
|
+
unknownEdges: Array.from(unknownEdges),
|
|
638
|
+
estimatedNodes: Array.from(estimatedNodes),
|
|
639
|
+
estimatedEdges: Array.from(estimatedEdges),
|
|
640
|
+
decisionVariants,
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
565
644
|
function evaluateMap(mapDefinition, context = {}, overrides = {}) {
|
|
566
645
|
const nodes = new Set([mapDefinition?.start || '1'])
|
|
567
646
|
;(mapDefinition?.rules || []).forEach((rule) => nodes.add(rule.node))
|
|
@@ -574,7 +653,21 @@ function evaluateMap(mapDefinition, context = {}, overrides = {}) {
|
|
|
574
653
|
Array.from(nodes).forEach((node) => {
|
|
575
654
|
if (!decisions[node]) decisions[node] = routeDecision(mapDefinition, node, context, overrides)
|
|
576
655
|
})
|
|
577
|
-
const probability =
|
|
656
|
+
const probability = hasVisitedRules(mapDefinition)
|
|
657
|
+
? propagatePathProbability(mapDefinition?.start || '1', mapDefinition, context, overrides)
|
|
658
|
+
: propagateProbability(mapDefinition?.start || '1', decisions)
|
|
659
|
+
if (probability.decisionVariants) {
|
|
660
|
+
const preferredPath = Array.isArray(context.passedNodes) ? context.passedNodes : []
|
|
661
|
+
probability.decisionVariants.forEach((variants, node) => {
|
|
662
|
+
const preferred = preferredPath.length
|
|
663
|
+
? variants
|
|
664
|
+
.filter((variant) => variant.passedNodes.every((passedNode, index) => preferredPath[index] === passedNode))
|
|
665
|
+
.sort((left, right) => right.passedNodes.length - left.passedNodes.length)[0]
|
|
666
|
+
: null
|
|
667
|
+
decisions[node] = (preferred || variants[0]).decision
|
|
668
|
+
})
|
|
669
|
+
delete probability.decisionVariants
|
|
670
|
+
}
|
|
578
671
|
return { decisions, probability }
|
|
579
672
|
}
|
|
580
673
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poi-plugin-compass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "通常海域舰队罗盘",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"homepage": "https://github.com/RikaKagurasaka/poi-plugin-compass#readme",
|
|
14
14
|
"files": [
|
|
15
15
|
"index.js",
|
|
16
|
+
"storage.js",
|
|
16
17
|
"logic.js",
|
|
17
18
|
"enemies.js",
|
|
18
19
|
"exporters.js",
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
"cache:kcnav-enemies:long-low": "node scripts/cache-kcnav-enemies-long-low.js",
|
|
32
33
|
"compress:kcnav-maps": "node scripts/compress-kcnav-maps.js --remove-png",
|
|
33
34
|
"compress:kcnav-enemies": "node scripts/compact-kcnav-enemies.js",
|
|
34
|
-
"check:syntax": "node --check index.js && node --check logic.js && node --check enemies.js && node --check exporters.js && node --check scripts/cache-kcnav-enemies.js && node --check scripts/cache-kcnav-enemies-long-low.js && node --check scripts/compress-kcnav-maps.js && node --check scripts/compact-kcnav-enemies.js && node --check test.js && node --check test-enemies.js && node --check test-exporters.js",
|
|
35
|
-
"test": "node test.js && node test-enemies.js && node test-exporters.js && node test-ui.js && node test-overview.js"
|
|
35
|
+
"check:syntax": "node --check index.js && node --check logic.js && node --check enemies.js && node --check exporters.js && node --check scripts/cache-kcnav-enemies.js && node --check scripts/cache-kcnav-enemies-long-low.js && node --check scripts/compress-kcnav-maps.js && node --check scripts/compact-kcnav-enemies.js && node --check test.js && node --check test-enemies.js && node --check test-exporters.js && node --check storage.js",
|
|
36
|
+
"test": "node test.js && node test-enemies.js && node test-exporters.js && node test-ui.js && node test-overview.js && node --test test-storage.js"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"poi-plugin",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"icon": "fa/compass",
|
|
53
54
|
"priority": 20,
|
|
54
55
|
"apiVer": {
|
|
55
|
-
"11.99.99": "0.1.
|
|
56
|
+
"11.99.99": "0.1.7"
|
|
56
57
|
}
|
|
57
58
|
},
|
|
58
59
|
"peerDependencies": {
|
|
@@ -17,6 +17,14 @@ const mapIds = Array.from(new Set(catalogs.flatMap((catalog) => Object.keys(cata
|
|
|
17
17
|
|
|
18
18
|
const DEFAULT_INTERVAL_MS = 15_000
|
|
19
19
|
|
|
20
|
+
// KCNav currently exposes 6-5 J as a normal battle node, but the WikiWiki
|
|
21
|
+
// route table identifies it as an opening-night battle. Keep the raw response
|
|
22
|
+
// unchanged and apply only this verified presentation correction to the
|
|
23
|
+
// derived node-type cache.
|
|
24
|
+
const VERIFIED_NODE_TYPE_OVERRIDES = {
|
|
25
|
+
'6-5': { J: 11 },
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
function apiUrl(mapId) {
|
|
21
29
|
return `https://tsunkit.net/api/routing/maps/${mapId}/`
|
|
22
30
|
}
|
|
@@ -32,7 +40,7 @@ function intervalFromArgs() {
|
|
|
32
40
|
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_INTERVAL_MS
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
function nodeTypesFromResult(result) {
|
|
43
|
+
function nodeTypesFromResult(result, mapId = null) {
|
|
36
44
|
const nodeValues = {}
|
|
37
45
|
const spots = result?.spots || {}
|
|
38
46
|
Object.keys(spots).forEach((node) => { nodeValues[node] = -1 })
|
|
@@ -48,6 +56,7 @@ function nodeTypesFromResult(result) {
|
|
|
48
56
|
const nodeType = Number(entry[2])
|
|
49
57
|
if (Number.isFinite(nodeType)) nodeValues[entry[1]] = nodeType
|
|
50
58
|
})
|
|
59
|
+
Object.assign(nodeValues, VERIFIED_NODE_TYPE_OVERRIDES[mapId] || {})
|
|
51
60
|
return nodeValues
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -82,7 +91,7 @@ async function main() {
|
|
|
82
91
|
const content = `${JSON.stringify(payload, null, 2)}\n`
|
|
83
92
|
const contentFile = `${mapId}.json`
|
|
84
93
|
fs.writeFileSync(path.join(outputDir, contentFile), content, 'utf8')
|
|
85
|
-
nodeTypesByMap[mapId] = nodeTypesFromResult(payload.result)
|
|
94
|
+
nodeTypesByMap[mapId] = nodeTypesFromResult(payload.result, mapId)
|
|
86
95
|
manifest.push({
|
|
87
96
|
mapId,
|
|
88
97
|
apiUrl: url,
|
package/storage.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { randomUUID } = require('crypto')
|
|
5
|
+
const legacyKey = 'poi-plugin-compass.preferences.v1'
|
|
6
|
+
const isRecord = value => value != null && typeof value === 'object' && !Array.isArray(value)
|
|
7
|
+
|
|
8
|
+
function parse(raw) {
|
|
9
|
+
const value = JSON.parse(raw)
|
|
10
|
+
if (!isRecord(value)) throw Error('罗盘设置文件格式错误;原数据已保留')
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
function file(base) {
|
|
14
|
+
if (!base) throw Error('poi 数据目录尚未加载')
|
|
15
|
+
return path.join(base, 'compass', 'preferences.json')
|
|
16
|
+
}
|
|
17
|
+
function save(base, value) {
|
|
18
|
+
const target = file(base)
|
|
19
|
+
// Never replace an unreadable or malformed existing file with default preferences.
|
|
20
|
+
const previous = fs.existsSync(target) ? parse(fs.readFileSync(target, 'utf8')) : null
|
|
21
|
+
const raw = JSON.stringify(value, null, 2) + '\n'
|
|
22
|
+
parse(raw)
|
|
23
|
+
if (previous && JSON.stringify(previous, null, 2) + '\n' === raw) return
|
|
24
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
25
|
+
const temporary = `${target}.${randomUUID()}.tmp`
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(temporary, raw, { flag: 'wx' })
|
|
28
|
+
fs.renameSync(temporary, target)
|
|
29
|
+
} finally {
|
|
30
|
+
if (fs.existsSync(temporary)) fs.unlinkSync(temporary)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function load(base, legacy) {
|
|
34
|
+
const target = file(base)
|
|
35
|
+
try {
|
|
36
|
+
return parse(fs.readFileSync(target, 'utf8'))
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.code !== 'ENOENT') throw error
|
|
39
|
+
}
|
|
40
|
+
const raw = legacy?.getItem(legacyKey)
|
|
41
|
+
if (raw == null) return {}
|
|
42
|
+
const value = parse(raw)
|
|
43
|
+
save(base, value)
|
|
44
|
+
// Keep the original localStorage entry intact as a migration backup.
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
module.exports = { load, save, legacyKey }
|