poi-plugin-compass 0.1.6 → 0.1.8

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 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
+ 从前曾在主窗口和独立窗口保存不同设置时,以首次成功迁移的窗口为准。
@@ -1,12 +1,25 @@
1
1
  .poi-compass {
2
- min-height: 100%;
3
- padding: 10px;
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 minmax(238px, 1fr) 190px;
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: 590px; }
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, 54px) minmax(160px, 1fr);
517
+ grid-template-columns: 40px repeat(5, 42px) minmax(150px, 1fr);
501
518
  align-items: center;
502
- gap: 8px;
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: right; white-space: nowrap; }
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>
@@ -507,7 +507,7 @@
507
507
  "G": 10,
508
508
  "H": 10,
509
509
  "I": 4,
510
- "J": 4,
510
+ "J": 11,
511
511
  "K": 90,
512
512
  "L": 90,
513
513
  "M": 5
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 PREFERENCES_STORAGE_KEY = 'poi-plugin-compass.preferences.v1'
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': [
@@ -209,12 +213,10 @@ function normalizeOverrides(value) {
209
213
  }
210
214
 
211
215
  function readCompassPreferences() {
212
- const storage = compassStorage()
213
- if (!storage) return {}
214
216
  try {
215
- const parsed = JSON.parse(storage.getItem(PREFERENCES_STORAGE_KEY) || '{}')
216
- return isRecord(parsed) ? parsed : {}
217
- } catch (_) {
217
+ return preferencesStorage.load(preferencesBase(), compassStorage())
218
+ } catch (error) {
219
+ console.warn('[compass] 无法读取设置;原数据已保留', error)
218
220
  return {}
219
221
  }
220
222
  }
@@ -375,6 +377,10 @@ function prophetSpotIconUrl(icon) {
375
377
  return pathToFileURL(path.join(LOCAL_PROPHET_SPOT_ICON_ROOT, `${icon}.svg`)).toString()
376
378
  }
377
379
 
380
+ function prophetParameterIconUrl(icon) {
381
+ return pathToFileURL(path.join(LOCAL_PROPHET_PARAMETER_ICON_ROOT, `${icon}.svg`)).toString()
382
+ }
383
+
378
384
  function nodeMarker(nodeType, nodeInfo) {
379
385
  const prophetIcon = PROPHET_NODE_ICONS[nodeType]
380
386
  if (prophetIcon) return { source: 'prophet', url: prophetSpotIconUrl(prophetIcon) }
@@ -728,10 +734,8 @@ class Compass extends React.Component {
728
734
  }
729
735
 
730
736
  persistPreferences() {
731
- const storage = compassStorage()
732
- if (!storage) return
733
737
  try {
734
- const previous = readCompassPreferences()
738
+ const previous = preferencesStorage.load(preferencesBase(), compassStorage())
735
739
  const overridesByView = isRecord(previous.overridesByView) ? { ...previous.overridesByView } : {}
736
740
  overridesByView[viewPreferenceKey(this.state.mapId, this.state.phaseId, this.state.deckId)] = normalizeOverrides(this.state.overrides)
737
741
  const exportByView = isRecord(previous.exportByView) ? { ...previous.exportByView } : {}
@@ -744,7 +748,7 @@ class Compass extends React.Component {
744
748
  phaseByMap[this.state.mapId] = this.state.phaseId
745
749
  const routeOptionByMap = isRecord(previous.routeOptionByMap) ? { ...previous.routeOptionByMap } : {}
746
750
  if (this.state.routeOptionId) routeOptionByMap[this.state.mapId] = this.state.routeOptionId
747
- storage.setItem(PREFERENCES_STORAGE_KEY, JSON.stringify({
751
+ preferencesStorage.save(preferencesBase(), {
748
752
  ...previous,
749
753
  mapId: this.state.mapId,
750
754
  phaseId: this.state.phaseId,
@@ -756,9 +760,9 @@ class Compass extends React.Component {
756
760
  showEnemyAvatars: this.state.showEnemyAvatars,
757
761
  overridesByView,
758
762
  exportByView,
759
- }))
760
- } catch (_) {
761
- // Preferences are optional; a restricted localStorage must not break the plugin.
763
+ })
764
+ } catch (error) {
765
+ console.warn('[compass] 无法保存设置;原数据已保留', error)
762
766
  }
763
767
  }
764
768
 
@@ -861,8 +865,15 @@ class Compass extends React.Component {
861
865
 
862
866
  renderFleetAir(air) {
863
867
  if (!air || !Number.isFinite(air.min) || !Number.isFinite(air.max) || air.max <= 0) return null
864
- return h('span', { className: 'compass-fleet-air', title: 'poi 当前舰队制空(熟练度范围)' },
865
- `制空 ${air.min === air.max ? air.min : `${air.min}~${air.max}`}`)
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))
866
877
  }
867
878
 
868
879
  setManualOverride(node, to, edgeId = null, selectedNode = node) {
@@ -1217,6 +1228,45 @@ class Compass extends React.Component {
1217
1228
  return `${master.api_name}${rank}`
1218
1229
  }
1219
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
+ }
1220
1270
  return h('section', { className: 'compass-panel compass-enemies' }, heading,
1221
1271
  data.rows.length ? h('div', { className: 'compass-enemy-air-range' }, `空优 ${rangeText(data.superiority)} / 空确 ${rangeText(data.supremacy)}${data.uncertain ? '?' : ''}`) : null,
1222
1272
  !data.rows.length ? h('div', { className: 'compass-muted' }, '此期间无记录') : null,
@@ -1239,11 +1289,13 @@ class Compass extends React.Component {
1239
1289
  h('div', null, `陆航:${airText(entry.lbasThresholds)}`),
1240
1290
  entry.uncertain ? h('div', null, `装备不确定:${entry.uncertainAirpowerItems.join('、')}`) : null,
1241
1291
  h('div', { className: 'compass-enemy-stats' },
1242
- h('div', { className: 'compass-enemy-stat-row compass-muted' }, ...['', 'HP', '火力', '雷装', '对空', '装甲', '装备'].map((label, i) => h('span', { key: i }, label))),
1243
1292
  ...fleet.map((ship, shipIndex) => h('div', { key: shipIndex, className: `compass-enemy-stat-row${shipIndex === entry.mainFleet.length ? ' is-escort-start' : ''}` },
1244
1293
  this.renderShipPortrait(ship.id, shipName(ship), getRootState()),
1245
- ...['hp', 'fp', 'torp', 'aa', 'armor'].map(field => h('span', { key: field }, ship[field] ?? '—')),
1246
- h('span', { className: 'compass-enemy-equips' }, (ship.equips || []).filter(id => id > 0).map(equipName).join(' / ') || '—'),
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' }, '—')),
1247
1299
  )),
1248
1300
  ),
1249
1301
  ),
@@ -1361,6 +1413,7 @@ class Compass extends React.Component {
1361
1413
  ].filter(Boolean)
1362
1414
 
1363
1415
  return h('div', { className: 'poi-compass' },
1416
+ h('div', { className: 'compass-scroll-viewport' },
1364
1417
  h('link', { rel: 'stylesheet', href: cssPath }),
1365
1418
  h('header', { className: 'compass-titlebar' }, h('h3', null, '舰队罗盘')),
1366
1419
  h('div', { className: 'compass-toolbar' },
@@ -1446,6 +1499,7 @@ class Compass extends React.Component {
1446
1499
  ),
1447
1500
  ),
1448
1501
  h('div', { className: 'compass-enemy-region' }, this.renderEnemyPanel(this.state.mapId, selectedNode)),
1502
+ ),
1449
1503
  )
1450
1504
  }
1451
1505
  }
package/logic.js CHANGED
@@ -11,18 +11,18 @@ const SHIP_CATEGORIES = {
11
11
  CAV: new Set([6]),
12
12
  CA_ALL: new Set([5, 6]),
13
13
  CVL: new Set([7]),
14
- BB: new Set([8, 9]),
14
+ BB: new Set([8, 9, 12]),
15
15
  BBV: new Set([10]),
16
- BB_ALL: new Set([8, 9, 10]),
17
- CV: new Set([11, 12]),
18
- CV_ALL: new Set([7, 11, 12]),
16
+ BB_ALL: new Set([8, 9, 10, 12]),
17
+ CV: new Set([11, 18]),
18
+ CV_ALL: new Set([7, 11, 18]),
19
19
  SS: new Set([13, 14]),
20
20
  AO: new Set([15, 22]),
21
21
  AV: new Set([16]),
22
22
  LHA: new Set([17]),
23
23
  AR: new Set([19]),
24
24
  AS: new Set([20]),
25
- CV_MAIN: new Set([11, 12]),
25
+ CV_MAIN: new Set([11, 18]),
26
26
  }
27
27
 
28
28
  const CATEGORY_LABELS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-compass",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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.6"
56
+ "11.99.99": "0.1.8"
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 }