poi-plugin-compass 0.1.4 → 0.1.6

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.
@@ -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-fixed", "node": "O", "priority": 1, "predicate": { "kind": "always", "label": "固定前往 Q" }, "outcomes": [{ "to": "Q", "probability": 1 }], "confidence": "parsed" },
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
@@ -50,6 +50,13 @@ const MAP_PHASES = {
50
50
  ],
51
51
  }
52
52
 
53
+ const MAP_ROUTE_OPTIONS = {
54
+ '5-6': [
55
+ { id: 'locked', label: '未解锁', flags: { qRoutes: false } },
56
+ { id: 'unlocked', label: '已解锁', flags: { qRoutes: true } },
57
+ ],
58
+ }
59
+
53
60
  function phaseOptionsForMap(mapId) {
54
61
  return MAP_PHASES[mapId] || []
55
62
  }
@@ -58,10 +65,25 @@ function phaseOptionForMap(mapId, phaseId) {
58
65
  return phaseOptionsForMap(mapId).find((phase) => phase.id === phaseId) || phaseOptionsForMap(mapId)[0] || null
59
66
  }
60
67
 
61
- function phaseFlags(mapId, phaseId) {
68
+ function routeOptionsForMap(mapId) {
69
+ return MAP_ROUTE_OPTIONS[mapId] || []
70
+ }
71
+
72
+ function defaultRouteOptionId(mapId) {
73
+ return routeOptionsForMap(mapId)[0]?.id || null
74
+ }
75
+
76
+ function routeOptionForMap(mapId, routeOptionId) {
77
+ return routeOptionsForMap(mapId).find((option) => option.id === routeOptionId)
78
+ || routeOptionsForMap(mapId)[0]
79
+ || null
80
+ }
81
+
82
+ function phaseFlags(mapId, phaseId, routeOptionId) {
62
83
  const flags = {}
63
84
  if (mapId === '7-3') flags.phase1 = phaseId === 'P1'
64
- if (mapId === '5-6') flags.qRoutes = phaseId === 'P3'
85
+ const routeOption = routeOptionForMap(mapId, routeOptionId)
86
+ if (routeOption?.flags) Object.assign(flags, routeOption.flags)
65
87
  return flags
66
88
  }
67
89
 
@@ -662,11 +684,14 @@ class Compass extends React.Component {
662
684
  const initialDeckId = [1, 2, 3, 4].includes(Number(preferences.deckId)) ? Number(preferences.deckId) : 1
663
685
  const preferredPhaseId = preferences.phaseByMap?.[initialMapId] ?? preferences.phaseId
664
686
  const initialPhaseId = phaseOptionForMap(initialMapId, preferredPhaseId)?.id || null
687
+ const initialRouteOptionId = routeOptionForMap(initialMapId, preferences.routeOptionByMap?.[initialMapId])?.id
688
+ || defaultRouteOptionId(initialMapId)
665
689
  const initialExportPreferences = storedExportPreferencesForView(preferences, initialMapId, initialPhaseId, initialDeckId)
666
690
  this.state = {
667
691
  deckId: initialDeckId,
668
692
  mapId: initialMapId,
669
693
  phaseId: initialPhaseId,
694
+ routeOptionId: initialRouteOptionId,
670
695
  selectedNode: pluginState.currentNode || null,
671
696
  selectedEdge: null,
672
697
  overrides: storedOverridesForView(preferences, initialMapId, initialPhaseId, initialDeckId),
@@ -717,11 +742,14 @@ class Compass extends React.Component {
717
742
  }
718
743
  const phaseByMap = isRecord(previous.phaseByMap) ? { ...previous.phaseByMap } : {}
719
744
  phaseByMap[this.state.mapId] = this.state.phaseId
745
+ const routeOptionByMap = isRecord(previous.routeOptionByMap) ? { ...previous.routeOptionByMap } : {}
746
+ if (this.state.routeOptionId) routeOptionByMap[this.state.mapId] = this.state.routeOptionId
720
747
  storage.setItem(PREFERENCES_STORAGE_KEY, JSON.stringify({
721
748
  ...previous,
722
749
  mapId: this.state.mapId,
723
750
  phaseId: this.state.phaseId,
724
751
  phaseByMap,
752
+ routeOptionByMap,
725
753
  deckId: this.state.deckId,
726
754
  probabilityFormat: this.state.probabilityFormat,
727
755
  showEstimatedAir: this.state.showEstimatedAir,
@@ -743,10 +771,13 @@ class Compass extends React.Component {
743
771
  selectMap(mapId) {
744
772
  const preferences = readCompassPreferences()
745
773
  const phaseId = phaseOptionForMap(mapId, preferences.phaseByMap?.[mapId])?.id || null
774
+ const routeOptionId = routeOptionForMap(mapId, preferences.routeOptionByMap?.[mapId])?.id
775
+ || defaultRouteOptionId(mapId)
746
776
  const exportPreferences = storedExportPreferencesForView(preferences, mapId, phaseId, this.state.deckId)
747
777
  this.setState({
748
778
  mapId,
749
779
  phaseId,
780
+ routeOptionId,
750
781
  selectedNode: null,
751
782
  selectedEdge: null,
752
783
  overrides: storedOverridesForView(preferences, mapId, phaseId, this.state.deckId),
@@ -800,6 +831,11 @@ class Compass extends React.Component {
800
831
  this.setState({ phaseId, selectedNode: null, selectedEdge: null, overrides, ...exportPreferences, exportError: null })
801
832
  }
802
833
 
834
+ selectRouteOption(routeOptionId) {
835
+ if (!routeOptionForMap(this.state.mapId, routeOptionId)) return
836
+ this.setState({ routeOptionId, selectedNode: null, selectedEdge: null, exportRoute: null, exportError: null })
837
+ }
838
+
803
839
  selectNode(node) {
804
840
  this.setState({ selectedNode: node, selectedEdge: null })
805
841
  }
@@ -1299,7 +1335,7 @@ class Compass extends React.Component {
1299
1335
  admiralCoefficient: mapLosConfig.admiralCoefficient,
1300
1336
  passedNodes: pluginState.currentMapId === this.state.mapId ? pluginState.passedNodes : [],
1301
1337
  phase: phase?.id || null,
1302
- flags: phaseFlags(this.state.mapId, phase?.id),
1338
+ flags: phaseFlags(this.state.mapId, phase?.id, this.state.routeOptionId),
1303
1339
  })
1304
1340
  const geometry = mapGeometry(this.state.mapId)
1305
1341
  const displayMapDefinition = phase?.boss
@@ -1342,6 +1378,18 @@ class Compass extends React.Component {
1342
1378
  }, option.label)),
1343
1379
  )
1344
1380
  : null,
1381
+ routeOptionsForMap(this.state.mapId).length > 1
1382
+ ? h('div', { className: 'compass-phase-group', role: 'group', 'aria-label': '路线解锁状态' },
1383
+ h('span', { className: 'compass-option-label' }, 'Q路线'),
1384
+ routeOptionsForMap(this.state.mapId).map((option) => h('button', {
1385
+ key: option.id,
1386
+ type: 'button',
1387
+ className: routeOptionForMap(this.state.mapId, this.state.routeOptionId)?.id === option.id ? 'is-active' : '',
1388
+ 'aria-pressed': routeOptionForMap(this.state.mapId, this.state.routeOptionId)?.id === option.id,
1389
+ onClick: () => this.selectRouteOption(option.id),
1390
+ }, option.label)),
1391
+ )
1392
+ : null,
1345
1393
  h('div', { className: 'compass-option-group', role: 'group', 'aria-label': '概率标签格式' },
1346
1394
  h('span', { className: 'compass-option-label' }, '概率'),
1347
1395
  ...[
@@ -1423,6 +1471,8 @@ exports.__test = {
1423
1471
  nodeIconUrl,
1424
1472
  losConfig: losCatalog.maps,
1425
1473
  phaseOptionsForMap,
1474
+ routeOptionsForMap,
1475
+ routeOptionForMap,
1426
1476
  pluginState,
1427
1477
  routeEdges,
1428
1478
  ruleRows,
package/logic.js CHANGED
@@ -17,7 +17,7 @@ const SHIP_CATEGORIES = {
17
17
  CV: new Set([11, 12]),
18
18
  CV_ALL: new Set([7, 11, 12]),
19
19
  SS: new Set([13, 14]),
20
- AO: new Set([15]),
20
+ AO: new Set([15, 22]),
21
21
  AV: new Set([16]),
22
22
  LHA: new Set([17]),
23
23
  AR: new Set([19]),
@@ -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 = propagateProbability(mapDefinition?.start || '1', decisions)
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.4",
3
+ "version": "0.1.6",
4
4
  "description": "通常海域舰队罗盘",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -52,7 +52,7 @@
52
52
  "icon": "fa/compass",
53
53
  "priority": 20,
54
54
  "apiVer": {
55
- "11.99.99": "0.1.4"
55
+ "11.99.99": "0.1.6"
56
56
  }
57
57
  },
58
58
  "peerDependencies": {