poi-plugin-compass 0.1.2 → 0.1.4
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/air-status.js +5 -5
- package/assets/compass.css +79 -48
- package/assets/icons/COPYRIGHT.md +7 -0
- package/assets/icons/spot/2.svg +37 -0
- package/assets/icons/spot/3.svg +37 -0
- package/assets/icons/spot/4-1.svg +37 -0
- package/assets/icons/spot/4-2.svg +37 -0
- package/assets/icons/spot/4-3.svg +8 -0
- package/assets/icons/spot/4-4.svg +30 -0
- package/assets/icons/spot/5.svg +50 -0
- package/assets/icons/spot/8.svg +56 -0
- package/assets/icons/spot/9.svg +37 -0
- package/data/maps.json +7 -13
- package/data/normal-maps.json +168 -154
- package/docs/research.md +7 -5
- package/docs/sources.md +1 -1
- package/index.js +382 -54
- package/logic.js +164 -15
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -26,6 +26,8 @@ const mapAreas = [
|
|
|
26
26
|
['4', '西方'], ['5', '南方'], ['6', '中部'],
|
|
27
27
|
]
|
|
28
28
|
const LOCAL_KCNAV_ASSET_ROOT = path.join(__dirname, 'assets', 'kcnav')
|
|
29
|
+
const LOCAL_PROPHET_SPOT_ICON_ROOT = path.join(__dirname, 'assets', 'icons', 'spot')
|
|
30
|
+
const PREFERENCES_STORAGE_KEY = 'poi-plugin-compass.preferences.v1'
|
|
29
31
|
|
|
30
32
|
const MAP_PHASES = {
|
|
31
33
|
'5-6': [
|
|
@@ -67,7 +69,6 @@ const {
|
|
|
67
69
|
evaluateMap,
|
|
68
70
|
evaluatePredicate,
|
|
69
71
|
fleetContextFromState,
|
|
70
|
-
formatPercent,
|
|
71
72
|
mapNodeLabel,
|
|
72
73
|
normalizeMapId,
|
|
73
74
|
normalizeOutcomes,
|
|
@@ -114,6 +115,18 @@ const NODE_DEFAULTS = {
|
|
|
114
115
|
border: '#999999',
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
const COMBAT_NODE_TYPES = new Set([4, 5, 11, 13, 15])
|
|
119
|
+
const AIR_NODE_TYPES = new Set([7, 10])
|
|
120
|
+
const AIR_STATUS_NODE_TYPES = new Set([4, 5, 7, 10, 11, 13, 15])
|
|
121
|
+
const ICON_ONLY_NODE_TYPES = new Set([0, 5, 7, 8, 10])
|
|
122
|
+
const PROPHET_NODE_ICONS = {
|
|
123
|
+
5: '5',
|
|
124
|
+
8: '8',
|
|
125
|
+
}
|
|
126
|
+
const NODE_ICON_SIZE = 64
|
|
127
|
+
const NODE_ICON_SIZE_BY_TYPE = { 7: 56, 8: 48, 10: 56 }
|
|
128
|
+
const NODE_ICON_OFFSET_Y_BY_TYPE = { 0: -2, 5: -6 }
|
|
129
|
+
|
|
117
130
|
function nodeVisualStyle(nodeInfo = {}) {
|
|
118
131
|
const style = { ...NODE_DEFAULTS, ...nodeInfo }
|
|
119
132
|
const size = Number.isFinite(Number(style.size)) ? Number(style.size) : Number(style.simpleSize)
|
|
@@ -156,6 +169,66 @@ function getRootState() {
|
|
|
156
169
|
return store && typeof store.getState === 'function' ? store.getState() : {}
|
|
157
170
|
}
|
|
158
171
|
|
|
172
|
+
function compassStorage() {
|
|
173
|
+
try {
|
|
174
|
+
return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null
|
|
175
|
+
} catch (_) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isRecord(value) {
|
|
181
|
+
return value != null && typeof value === 'object' && !Array.isArray(value)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeOverrides(value) {
|
|
185
|
+
if (!isRecord(value)) return {}
|
|
186
|
+
return Object.fromEntries(Object.entries(value).filter(([node, target]) => node && typeof target === 'string' && target))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readCompassPreferences() {
|
|
190
|
+
const storage = compassStorage()
|
|
191
|
+
if (!storage) return {}
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(storage.getItem(PREFERENCES_STORAGE_KEY) || '{}')
|
|
194
|
+
return isRecord(parsed) ? parsed : {}
|
|
195
|
+
} catch (_) {
|
|
196
|
+
return {}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function viewPreferenceKey(mapId, phaseId, deckId) {
|
|
201
|
+
return `${mapId}|${phaseId || ''}|${deckId}`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function storedOverridesForView(preferences, mapId, phaseId, deckId) {
|
|
205
|
+
const overridesByView = isRecord(preferences?.overridesByView) ? preferences.overridesByView : {}
|
|
206
|
+
return normalizeOverrides(overridesByView[viewPreferenceKey(mapId, phaseId, deckId)])
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeStringMap(value) {
|
|
210
|
+
if (!isRecord(value)) return {}
|
|
211
|
+
return Object.fromEntries(Object.entries(value).filter(([key, entry]) => key && typeof entry === 'string' && entry))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeNumberMap(value) {
|
|
215
|
+
if (!isRecord(value)) return {}
|
|
216
|
+
return Object.fromEntries(Object.entries(value)
|
|
217
|
+
.map(([key, entry]) => [key, Number(entry)])
|
|
218
|
+
.filter(([key, entry]) => key && Number.isFinite(entry)))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function storedExportPreferencesForView(preferences, mapId, phaseId, deckId) {
|
|
222
|
+
const exportByView = isRecord(preferences?.exportByView) ? preferences.exportByView : {}
|
|
223
|
+
const stored = exportByView[viewPreferenceKey(mapId, phaseId, deckId)]
|
|
224
|
+
if (!isRecord(stored)) return { exportRoute: null, exportEnemies: {}, exportFormations: {} }
|
|
225
|
+
return {
|
|
226
|
+
exportRoute: typeof stored.route === 'string' && stored.route ? stored.route : null,
|
|
227
|
+
exportEnemies: normalizeStringMap(stored.enemies),
|
|
228
|
+
exportFormations: normalizeNumberMap(stored.formations),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
159
232
|
function getFleet(state, deckId = 1) {
|
|
160
233
|
const fleets = state?.info?.fleets
|
|
161
234
|
if (Array.isArray(fleets)) return fleets[deckId - 1] || null
|
|
@@ -275,6 +348,21 @@ function nodeIconUrl(icon) {
|
|
|
275
348
|
return null
|
|
276
349
|
}
|
|
277
350
|
|
|
351
|
+
function prophetSpotIconUrl(icon) {
|
|
352
|
+
if (icon == null) return null
|
|
353
|
+
return pathToFileURL(path.join(LOCAL_PROPHET_SPOT_ICON_ROOT, `${icon}.svg`)).toString()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function nodeMarker(nodeType, nodeInfo) {
|
|
357
|
+
const prophetIcon = PROPHET_NODE_ICONS[nodeType]
|
|
358
|
+
if (prophetIcon) return { source: 'prophet', url: prophetSpotIconUrl(prophetIcon) }
|
|
359
|
+
if ([0, 7, 10].includes(nodeType)) {
|
|
360
|
+
const kcnavIcon = nodeIconUrl(nodeInfo?.icon)
|
|
361
|
+
return kcnavIcon ? { source: 'kcnav', url: kcnavIcon } : null
|
|
362
|
+
}
|
|
363
|
+
return null
|
|
364
|
+
}
|
|
365
|
+
|
|
278
366
|
function localNodeTypesForMap(mapId, mapDefinition) {
|
|
279
367
|
const nodeTypes = { ...(localKcnavNodeTypes[mapId] || {}) }
|
|
280
368
|
if (mapDefinition?.start) nodeTypes[mapDefinition.start] = 0
|
|
@@ -400,8 +488,71 @@ function routeEdges(geometry) {
|
|
|
400
488
|
.filter((edge) => edge.from && edge.to)
|
|
401
489
|
}
|
|
402
490
|
|
|
403
|
-
function
|
|
404
|
-
|
|
491
|
+
function formatProbability(value, estimated = false, mode = 'percent') {
|
|
492
|
+
if (!Number.isFinite(Number(value))) return '未知'
|
|
493
|
+
const probability = Math.max(0, Math.min(1, Number(value)))
|
|
494
|
+
const suffix = estimated ? '?' : ''
|
|
495
|
+
if (mode === 'decimal') {
|
|
496
|
+
const rounded = Math.round(probability * 100) / 100
|
|
497
|
+
return `${rounded === 1 ? '1' : rounded.toFixed(2).replace(/^0/, '')}${suffix}`
|
|
498
|
+
}
|
|
499
|
+
return `${Math.round(probability * 100)}%${suffix}`
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function outcomeText(outcome, mode = 'percent') {
|
|
503
|
+
return `${outcome.to} ${formatProbability(outcome.probability, outcome.estimated, mode)}`
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// These predicates came from separate list items in the cached KCWiki route
|
|
507
|
+
// tables. Keep the evaluator's compact OR semantics for compatibility, but
|
|
508
|
+
// render each source line separately so the UI does not invent one combined
|
|
509
|
+
// rule where the source did not have one.
|
|
510
|
+
const SEPARATE_SOURCE_LINE_RULE_IDS = new Set([
|
|
511
|
+
'2-5-C-heavy',
|
|
512
|
+
'1-5-F-heavy',
|
|
513
|
+
'1-6-start-heavy', '1-6-M-heavy',
|
|
514
|
+
'2-3-D-special',
|
|
515
|
+
'4-2-C-light-l',
|
|
516
|
+
'6-1-start-heavy3',
|
|
517
|
+
'6-2-C-a', '6-2-D-f', '6-2-E-f',
|
|
518
|
+
'6-4-start-right-lha', '6-4-A-noakitsu-heavy', '6-4-E-special', '6-4-E-g', '6-4-J-l', '6-4-J-i', '6-4-K-h',
|
|
519
|
+
'6-5-start-left', '6-5-C-e', '6-5-E-i', '6-5-I-h',
|
|
520
|
+
'7-1-B-a', '7-1-H-k',
|
|
521
|
+
'7-2-C-d', '7-2-E-g', '7-2-I-j',
|
|
522
|
+
'7-3-C-heavy', '7-3-G-special', '7-3-I-heavy', '7-3-J-m', '7-3-J-p', '7-3-M-n', '7-3-M-o',
|
|
523
|
+
'7-4-start-c', '7-4-start-a', '7-4-C-d', '7-4-J-k', '7-4-J-l', '7-4-M-heavy',
|
|
524
|
+
'7-5-B-c', '7-5-D-e', '7-5-D-fast', '7-5-J-N', '7-5-J-O-heavy', '7-5-P-r',
|
|
525
|
+
])
|
|
526
|
+
|
|
527
|
+
function visibleRuleLabel(rule) {
|
|
528
|
+
const label = predicateLabel(rule?.predicate)
|
|
529
|
+
return label
|
|
530
|
+
.replace(/([^)]*(?:近似|约|原文|比例未知|分歧系数)[^)]*)/g, '')
|
|
531
|
+
.replace(/\([^)]*(?:approx|about|original|unknown|coefficient)[^)]*\)/gi, '')
|
|
532
|
+
.replace(/,?\s*失败后继续判定/g, '')
|
|
533
|
+
.replace(/:\s*[^,;:]+概率(?:未知|随索敌变化)/g, '')
|
|
534
|
+
.replace(/;\s*(?:近似|约)/g, '')
|
|
535
|
+
.replace(/(原文(?:标记\s*\?|比例未知)[^)]*)/g, '')
|
|
536
|
+
.replace(/(约)/g, '')
|
|
537
|
+
.replace(/\s{2,}/g, ' ')
|
|
538
|
+
.replace(/:\s*$/g, '')
|
|
539
|
+
.trim()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function rulePopoverNotes(rule, rawLabel, visibleLabel) {
|
|
543
|
+
const notes = []
|
|
544
|
+
if (rawLabel !== visibleLabel) notes.push(`原文补充:${rawLabel}`)
|
|
545
|
+
if (rule?.continueOnFailure === true) notes.push('本条判定失败后继续检查后续规则')
|
|
546
|
+
if (rule?.confidence === 'approximate') notes.push('该条件或概率来自近似资料')
|
|
547
|
+
if (rule?.confidence === 'unknown' || rule?.outcomes?.some((outcome) => outcome?.probability == null)) {
|
|
548
|
+
notes.push('来源没有给出明确概率;当前按均等概率暂估')
|
|
549
|
+
}
|
|
550
|
+
return notes
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function displayPredicatesForRule(rule) {
|
|
554
|
+
if (!SEPARATE_SOURCE_LINE_RULE_IDS.has(rule?.id) || rule?.predicate?.kind !== 'any') return [rule?.predicate]
|
|
555
|
+
return rule.predicate.predicates?.length ? rule.predicate.predicates : [rule.predicate]
|
|
405
556
|
}
|
|
406
557
|
|
|
407
558
|
function conditionIcon(status, active) {
|
|
@@ -423,11 +574,16 @@ function ruleRows(mapDefinition, node, context) {
|
|
|
423
574
|
return (mapDefinition.rules || [])
|
|
424
575
|
.filter((rule) => rule.node === node)
|
|
425
576
|
.sort((left, right) => number(left.priority) - number(right.priority))
|
|
426
|
-
.
|
|
427
|
-
rule,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
577
|
+
.reduce((rows, rule) => {
|
|
578
|
+
displayPredicatesForRule(rule).forEach((predicate, index) => {
|
|
579
|
+
rows.push({
|
|
580
|
+
rule: { ...rule, id: `${rule.id}::${index}`, predicate, sourceRuleId: rule.id },
|
|
581
|
+
result: evaluatePredicate(predicate, context),
|
|
582
|
+
outcomes: normalizeOutcomes(rule.outcomes, rule),
|
|
583
|
+
})
|
|
584
|
+
})
|
|
585
|
+
return rows
|
|
586
|
+
}, [])
|
|
431
587
|
}
|
|
432
588
|
|
|
433
589
|
function fleetSummary(context) {
|
|
@@ -458,12 +614,21 @@ function edgeProbability(evaluation, from, to) {
|
|
|
458
614
|
}
|
|
459
615
|
}
|
|
460
616
|
|
|
461
|
-
function
|
|
462
|
-
|
|
463
|
-
if (Number(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
617
|
+
function edgeVisualStyle(value) {
|
|
618
|
+
const probability = Number(value)
|
|
619
|
+
if (!Number.isFinite(probability) || probability <= 0) {
|
|
620
|
+
return { color: '#87919a', width: 2.25, opacity: 0.3 }
|
|
621
|
+
}
|
|
622
|
+
const ratio = Math.max(0, Math.min(1, probability))
|
|
623
|
+
if (ratio >= 0.999999) return { color: '#78d99a', width: 7, opacity: 1 }
|
|
624
|
+
const start = [125, 135, 145]
|
|
625
|
+
const end = [120, 217, 154]
|
|
626
|
+
const color = `rgb(${start.map((component, index) => Math.round(component + (end[index] - component) * ratio)).join(', ')})`
|
|
627
|
+
return {
|
|
628
|
+
color,
|
|
629
|
+
width: 2.25 + ratio * 4.75,
|
|
630
|
+
opacity: 0.3 + ratio * 0.7,
|
|
631
|
+
}
|
|
467
632
|
}
|
|
468
633
|
|
|
469
634
|
function mapBackgroundUrl(mapId) {
|
|
@@ -489,19 +654,27 @@ function manualSourceForTarget(evaluation, target) {
|
|
|
489
654
|
class Compass extends React.Component {
|
|
490
655
|
constructor(props) {
|
|
491
656
|
super(props)
|
|
657
|
+
const preferences = readCompassPreferences()
|
|
658
|
+
const storedMapId = typeof preferences.mapId === 'string' && mapCatalog.maps[preferences.mapId]
|
|
659
|
+
? preferences.mapId : null
|
|
660
|
+
const initialMapId = storedMapId
|
|
661
|
+
|| (pluginState.currentMapId && mapCatalog.maps[pluginState.currentMapId] ? pluginState.currentMapId : mapIds[0])
|
|
662
|
+
const initialDeckId = [1, 2, 3, 4].includes(Number(preferences.deckId)) ? Number(preferences.deckId) : 1
|
|
663
|
+
const preferredPhaseId = preferences.phaseByMap?.[initialMapId] ?? preferences.phaseId
|
|
664
|
+
const initialPhaseId = phaseOptionForMap(initialMapId, preferredPhaseId)?.id || null
|
|
665
|
+
const initialExportPreferences = storedExportPreferencesForView(preferences, initialMapId, initialPhaseId, initialDeckId)
|
|
492
666
|
this.state = {
|
|
493
|
-
deckId:
|
|
494
|
-
mapId:
|
|
495
|
-
|
|
496
|
-
: mapIds[0],
|
|
497
|
-
phaseId: phaseOptionsForMap(mapIds[0])[0]?.id || null,
|
|
667
|
+
deckId: initialDeckId,
|
|
668
|
+
mapId: initialMapId,
|
|
669
|
+
phaseId: initialPhaseId,
|
|
498
670
|
selectedNode: pluginState.currentNode || null,
|
|
499
671
|
selectedEdge: null,
|
|
500
|
-
overrides:
|
|
672
|
+
overrides: storedOverridesForView(preferences, initialMapId, initialPhaseId, initialDeckId),
|
|
673
|
+
probabilityFormat: preferences.probabilityFormat === 'decimal' ? 'decimal' : 'percent',
|
|
674
|
+
showEstimatedAir: typeof preferences.showEstimatedAir === 'boolean' ? preferences.showEstimatedAir : true,
|
|
675
|
+
showEnemyAvatars: typeof preferences.showEnemyAvatars === 'boolean' ? preferences.showEnemyAvatars : true,
|
|
501
676
|
exportOpen: false,
|
|
502
|
-
|
|
503
|
-
exportEnemies: {},
|
|
504
|
-
exportFormations: {},
|
|
677
|
+
...initialExportPreferences,
|
|
505
678
|
exportError: null,
|
|
506
679
|
exportBusy: false,
|
|
507
680
|
}
|
|
@@ -525,6 +698,42 @@ class Compass extends React.Component {
|
|
|
525
698
|
this.refresh()
|
|
526
699
|
}
|
|
527
700
|
|
|
701
|
+
componentDidUpdate() {
|
|
702
|
+
this.persistPreferences()
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
persistPreferences() {
|
|
706
|
+
const storage = compassStorage()
|
|
707
|
+
if (!storage) return
|
|
708
|
+
try {
|
|
709
|
+
const previous = readCompassPreferences()
|
|
710
|
+
const overridesByView = isRecord(previous.overridesByView) ? { ...previous.overridesByView } : {}
|
|
711
|
+
overridesByView[viewPreferenceKey(this.state.mapId, this.state.phaseId, this.state.deckId)] = normalizeOverrides(this.state.overrides)
|
|
712
|
+
const exportByView = isRecord(previous.exportByView) ? { ...previous.exportByView } : {}
|
|
713
|
+
exportByView[viewPreferenceKey(this.state.mapId, this.state.phaseId, this.state.deckId)] = {
|
|
714
|
+
route: typeof this.state.exportRoute === 'string' ? this.state.exportRoute : null,
|
|
715
|
+
enemies: normalizeStringMap(this.state.exportEnemies),
|
|
716
|
+
formations: normalizeNumberMap(this.state.exportFormations),
|
|
717
|
+
}
|
|
718
|
+
const phaseByMap = isRecord(previous.phaseByMap) ? { ...previous.phaseByMap } : {}
|
|
719
|
+
phaseByMap[this.state.mapId] = this.state.phaseId
|
|
720
|
+
storage.setItem(PREFERENCES_STORAGE_KEY, JSON.stringify({
|
|
721
|
+
...previous,
|
|
722
|
+
mapId: this.state.mapId,
|
|
723
|
+
phaseId: this.state.phaseId,
|
|
724
|
+
phaseByMap,
|
|
725
|
+
deckId: this.state.deckId,
|
|
726
|
+
probabilityFormat: this.state.probabilityFormat,
|
|
727
|
+
showEstimatedAir: this.state.showEstimatedAir,
|
|
728
|
+
showEnemyAvatars: this.state.showEnemyAvatars,
|
|
729
|
+
overridesByView,
|
|
730
|
+
exportByView,
|
|
731
|
+
}))
|
|
732
|
+
} catch (_) {
|
|
733
|
+
// Preferences are optional; a restricted localStorage must not break the plugin.
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
528
737
|
componentWillUnmount() {
|
|
529
738
|
listeners.delete(this.refresh)
|
|
530
739
|
this.unsubscribeStore?.()
|
|
@@ -532,15 +741,16 @@ class Compass extends React.Component {
|
|
|
532
741
|
}
|
|
533
742
|
|
|
534
743
|
selectMap(mapId) {
|
|
744
|
+
const preferences = readCompassPreferences()
|
|
745
|
+
const phaseId = phaseOptionForMap(mapId, preferences.phaseByMap?.[mapId])?.id || null
|
|
746
|
+
const exportPreferences = storedExportPreferencesForView(preferences, mapId, phaseId, this.state.deckId)
|
|
535
747
|
this.setState({
|
|
536
748
|
mapId,
|
|
537
|
-
phaseId
|
|
749
|
+
phaseId,
|
|
538
750
|
selectedNode: null,
|
|
539
751
|
selectedEdge: null,
|
|
540
|
-
overrides:
|
|
541
|
-
|
|
542
|
-
exportEnemies: {},
|
|
543
|
-
exportFormations: {},
|
|
752
|
+
overrides: storedOverridesForView(preferences, mapId, phaseId, this.state.deckId),
|
|
753
|
+
...exportPreferences,
|
|
544
754
|
exportError: null,
|
|
545
755
|
})
|
|
546
756
|
}
|
|
@@ -584,7 +794,10 @@ class Compass extends React.Component {
|
|
|
584
794
|
}
|
|
585
795
|
|
|
586
796
|
selectPhase(phaseId) {
|
|
587
|
-
|
|
797
|
+
const preferences = readCompassPreferences()
|
|
798
|
+
const overrides = storedOverridesForView(preferences, this.state.mapId, phaseId, this.state.deckId)
|
|
799
|
+
const exportPreferences = storedExportPreferencesForView(preferences, this.state.mapId, phaseId, this.state.deckId)
|
|
800
|
+
this.setState({ phaseId, selectedNode: null, selectedEdge: null, overrides, ...exportPreferences, exportError: null })
|
|
588
801
|
}
|
|
589
802
|
|
|
590
803
|
selectNode(node) {
|
|
@@ -592,7 +805,10 @@ class Compass extends React.Component {
|
|
|
592
805
|
}
|
|
593
806
|
|
|
594
807
|
selectFleet(deckId) {
|
|
595
|
-
|
|
808
|
+
const preferences = readCompassPreferences()
|
|
809
|
+
const overrides = storedOverridesForView(preferences, this.state.mapId, this.state.phaseId, deckId)
|
|
810
|
+
const exportPreferences = storedExportPreferencesForView(preferences, this.state.mapId, this.state.phaseId, deckId)
|
|
811
|
+
this.setState({ deckId, selectedNode: null, selectedEdge: null, overrides, ...exportPreferences, exportError: null })
|
|
596
812
|
}
|
|
597
813
|
|
|
598
814
|
renderLosIcons(scores) {
|
|
@@ -613,10 +829,10 @@ class Compass extends React.Component {
|
|
|
613
829
|
`制空 ${air.min === air.max ? air.min : `${air.min}~${air.max}`}`)
|
|
614
830
|
}
|
|
615
831
|
|
|
616
|
-
setManualOverride(node, to, edgeId = null) {
|
|
832
|
+
setManualOverride(node, to, edgeId = null, selectedNode = node) {
|
|
617
833
|
this.setState((state) => ({
|
|
618
834
|
overrides: { ...state.overrides, [node]: to },
|
|
619
|
-
selectedNode
|
|
835
|
+
selectedNode,
|
|
620
836
|
selectedEdge: edgeId,
|
|
621
837
|
exportRoute: null,
|
|
622
838
|
exportError: null,
|
|
@@ -627,6 +843,35 @@ class Compass extends React.Component {
|
|
|
627
843
|
this.setState({ selectedEdge: edgeId })
|
|
628
844
|
}
|
|
629
845
|
|
|
846
|
+
renderNodeEnemyAvatar(ship, rootState, x, y, size) {
|
|
847
|
+
if (!ship?.id) return null
|
|
848
|
+
const name = ship.name || '敌舰'
|
|
849
|
+
return h('foreignObject', {
|
|
850
|
+
className: 'compass-node-overlay compass-node-enemy-avatar',
|
|
851
|
+
x,
|
|
852
|
+
y,
|
|
853
|
+
width: size,
|
|
854
|
+
height: size,
|
|
855
|
+
'aria-label': `最常见敌方旗舰:${name}`,
|
|
856
|
+
}, h('div', { className: 'compass-node-overlay-content' }, this.renderShipPortrait(ship.id, name, rootState, size)))
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
renderNodeIcon(marker, centerX, centerY, nodeType) {
|
|
860
|
+
if (!marker?.url) return null
|
|
861
|
+
const size = NODE_ICON_SIZE_BY_TYPE[nodeType] || NODE_ICON_SIZE
|
|
862
|
+
const offsetY = NODE_ICON_OFFSET_Y_BY_TYPE[nodeType] || 0
|
|
863
|
+
return h('image', {
|
|
864
|
+
className: `compass-node-icon is-${marker.source}`,
|
|
865
|
+
href: marker.url,
|
|
866
|
+
x: centerX - size / 2,
|
|
867
|
+
y: centerY + offsetY - size / 2,
|
|
868
|
+
width: size,
|
|
869
|
+
height: size,
|
|
870
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
871
|
+
'aria-hidden': 'true',
|
|
872
|
+
})
|
|
873
|
+
}
|
|
874
|
+
|
|
630
875
|
renderMap(mapDefinition, geometry, evaluation, selectedNode, currentNode, passedNodes = [], mapId = null) {
|
|
631
876
|
if (!mapGeometryAvailable(mapDefinition, geometry)) {
|
|
632
877
|
return h('div', { className: 'compass-empty-panel' }, 'poi 地图数据不可用')
|
|
@@ -636,7 +881,8 @@ class Compass extends React.Component {
|
|
|
636
881
|
const edges = routeEdges(geometry)
|
|
637
882
|
const passedEdges = passedEdgeKeys(passedNodes)
|
|
638
883
|
const nodeTypes = localNodeTypesForMap(mapId, mapDefinition)
|
|
639
|
-
const
|
|
884
|
+
const rootState = getRootState()
|
|
885
|
+
const airPower = calculatePoiFleetStat(rootState, this.state.deckId, 1, true)
|
|
640
886
|
const width = Math.max(1200, ...Object.values(positions).map((position) => position.x + 100))
|
|
641
887
|
const height = Math.max(720, ...Object.values(positions).map((position) => position.y + 100))
|
|
642
888
|
return h(
|
|
@@ -660,6 +906,7 @@ class Compass extends React.Component {
|
|
|
660
906
|
const to = positions[edge.to]
|
|
661
907
|
if (!from || !to) return null
|
|
662
908
|
const probability = edgeProbability(evaluation, edge.from, edge.to)
|
|
909
|
+
const edgeVisual = edgeVisualStyle(probability.global)
|
|
663
910
|
const manualOutcomes = evaluation.decisions[edge.from]?.manualOutcomes
|
|
664
911
|
|| evaluation.decisions[edge.from]?.baseOutcomes
|
|
665
912
|
|| []
|
|
@@ -670,7 +917,6 @@ class Compass extends React.Component {
|
|
|
670
917
|
'compass-edge',
|
|
671
918
|
probability.global != null ? 'is-reachable' : '',
|
|
672
919
|
probability.estimated ? 'is-estimated' : '',
|
|
673
|
-
probabilityTier(probability.global),
|
|
674
920
|
passedEdges.has(`${edge.from}->${edge.to}`) ? 'is-passed' : '',
|
|
675
921
|
manualChoice ? 'is-manual-choice' : '',
|
|
676
922
|
selected ? 'is-selected' : '',
|
|
@@ -688,6 +934,11 @@ class Compass extends React.Component {
|
|
|
688
934
|
role: 'button',
|
|
689
935
|
tabIndex: 0,
|
|
690
936
|
'aria-label': `边 ID ${edge.cell}:${edge.from}→${edge.to}`,
|
|
937
|
+
style: {
|
|
938
|
+
'--compass-edge-color': edgeVisual.color,
|
|
939
|
+
'--compass-edge-width': edgeVisual.width,
|
|
940
|
+
'--compass-edge-opacity': edgeVisual.opacity,
|
|
941
|
+
},
|
|
691
942
|
onClick: () => manualChoice
|
|
692
943
|
? this.setManualOverride(edge.from, edge.to, edge.cell)
|
|
693
944
|
: this.selectEdge(edge.cell),
|
|
@@ -697,7 +948,7 @@ class Compass extends React.Component {
|
|
|
697
948
|
`边 ID ${edge.cell}:${edge.from} → ${edge.to}`,
|
|
698
949
|
manualChoice ? '点击选择方向' : null,
|
|
699
950
|
probability.global != null && Number(probability.global) > 0
|
|
700
|
-
? `局部 ${
|
|
951
|
+
? `局部 ${formatProbability(probability.local, probability.estimated, this.state.probabilityFormat)}`
|
|
701
952
|
: null,
|
|
702
953
|
].filter(Boolean).join(' · ')),
|
|
703
954
|
h('line', {
|
|
@@ -706,13 +957,13 @@ class Compass extends React.Component {
|
|
|
706
957
|
x2: to.x,
|
|
707
958
|
y2: to.y,
|
|
708
959
|
}),
|
|
709
|
-
probability.global != null && Number(probability.global) > 0
|
|
960
|
+
probability.global != null && Number(probability.global) > 0 && Math.round(Number(probability.global) * 100) > 0
|
|
710
961
|
? h('text', {
|
|
711
962
|
x: (from.x + to.x) / 2,
|
|
712
963
|
y: (from.y + to.y) / 2,
|
|
713
964
|
dominantBaseline: 'middle',
|
|
714
965
|
className: 'compass-edge-label',
|
|
715
|
-
},
|
|
966
|
+
}, formatProbability(probability.global, probability.estimated, this.state.probabilityFormat))
|
|
716
967
|
: null,
|
|
717
968
|
selected
|
|
718
969
|
? h('text', {
|
|
@@ -730,10 +981,20 @@ class Compass extends React.Component {
|
|
|
730
981
|
? kcnavNodeCatalog['-1']
|
|
731
982
|
: kcnavNodeCatalog[String(nodeType)] || kcnavNodeCatalog['-1']
|
|
732
983
|
const visual = nodeVisualStyle(nodeInfo)
|
|
733
|
-
const
|
|
734
|
-
|
|
984
|
+
const marker = nodeMarker(nodeType, nodeInfo)
|
|
985
|
+
const iconOnly = ICON_ONLY_NODE_TYPES.has(nodeType)
|
|
986
|
+
const enemyData = (COMBAT_NODE_TYPES.has(nodeType) || AIR_NODE_TYPES.has(nodeType) || AIR_STATUS_NODE_TYPES.has(nodeType))
|
|
987
|
+
? loadEnemies(mapId, node) : null
|
|
988
|
+
const commonFlagship = this.state.showEnemyAvatars
|
|
989
|
+
&& (COMBAT_NODE_TYPES.has(nodeType) || AIR_NODE_TYPES.has(nodeType))
|
|
990
|
+
&& enemyData?.status === 'ready'
|
|
991
|
+
? enemyData.rows?.[0]?.mainFleet?.[0] : null
|
|
992
|
+
const airState = evaluation.probability.reach[node] > 0 && AIR_STATUS_NODE_TYPES.has(nodeType)
|
|
993
|
+
? airStateRange(airPower, enemyData) : null
|
|
994
|
+
const displayAirState = this.state.showEstimatedAir ? airState : null
|
|
735
995
|
const centerX = position.x + visual.offsetX
|
|
736
996
|
const centerY = position.y + visual.offsetY
|
|
997
|
+
const overlaySize = Math.min(20, Math.max(12, Math.round(Math.min(visual.rx, visual.ry))))
|
|
737
998
|
const manualSource = manualSourceForTarget(evaluation, node)
|
|
738
999
|
const manualTarget = manualSource && manualSource !== node
|
|
739
1000
|
const classNames = [
|
|
@@ -747,7 +1008,7 @@ class Compass extends React.Component {
|
|
|
747
1008
|
const activateNode = (event) => {
|
|
748
1009
|
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
749
1010
|
event.preventDefault()
|
|
750
|
-
if (manualTarget) this.setManualOverride(manualSource, node)
|
|
1011
|
+
if (manualTarget) this.setManualOverride(manualSource, node, null, node)
|
|
751
1012
|
else this.selectNode(node)
|
|
752
1013
|
}
|
|
753
1014
|
return h('g', {
|
|
@@ -755,11 +1016,11 @@ class Compass extends React.Component {
|
|
|
755
1016
|
className: classNames,
|
|
756
1017
|
role: 'button',
|
|
757
1018
|
tabIndex: 0,
|
|
758
|
-
onClick: () => manualTarget ? this.setManualOverride(manualSource, node) : this.selectNode(node),
|
|
1019
|
+
onClick: () => manualTarget ? this.setManualOverride(manualSource, node, null, node) : this.selectNode(node),
|
|
759
1020
|
onKeyDown: activateNode,
|
|
760
1021
|
},
|
|
761
1022
|
h('title', null, nodeInfo?.label ? `${node} · ${nodeInfo.label}` : node),
|
|
762
|
-
h('ellipse', {
|
|
1023
|
+
iconOnly ? null : h('ellipse', {
|
|
763
1024
|
className: 'compass-node-shape',
|
|
764
1025
|
cx: centerX,
|
|
765
1026
|
cy: centerY,
|
|
@@ -770,7 +1031,7 @@ class Compass extends React.Component {
|
|
|
770
1031
|
strokeWidth: visual.stroke,
|
|
771
1032
|
opacity: visual.opacity,
|
|
772
1033
|
}),
|
|
773
|
-
visual.simpleBorder
|
|
1034
|
+
!iconOnly && visual.simpleBorder
|
|
774
1035
|
? h('ellipse', {
|
|
775
1036
|
className: 'compass-node-simple-border',
|
|
776
1037
|
cx: centerX,
|
|
@@ -782,9 +1043,10 @@ class Compass extends React.Component {
|
|
|
782
1043
|
opacity: visual.opacity,
|
|
783
1044
|
})
|
|
784
1045
|
: null,
|
|
1046
|
+
this.renderNodeIcon(marker, centerX, centerY, nodeType),
|
|
785
1047
|
h('text', {
|
|
786
1048
|
x: centerX,
|
|
787
|
-
y: centerY + visual.textOffsetY,
|
|
1049
|
+
y: centerY + (iconOnly ? 0 : visual.textOffsetY),
|
|
788
1050
|
dominantBaseline: 'middle',
|
|
789
1051
|
className: 'compass-node-label',
|
|
790
1052
|
style: {
|
|
@@ -794,11 +1056,32 @@ class Compass extends React.Component {
|
|
|
794
1056
|
fontSize: visual.fontSize,
|
|
795
1057
|
},
|
|
796
1058
|
}, node),
|
|
797
|
-
|
|
1059
|
+
commonFlagship ? this.renderNodeEnemyAvatar(
|
|
1060
|
+
commonFlagship,
|
|
1061
|
+
rootState,
|
|
1062
|
+
centerX + visual.rx * 0.7 - overlaySize / 2,
|
|
1063
|
+
centerY + visual.ry * 0.7 - overlaySize / 2,
|
|
1064
|
+
overlaySize,
|
|
1065
|
+
) : null,
|
|
1066
|
+
displayAirState ? h('text', {
|
|
1067
|
+
x: centerX + visual.rx * 0.85,
|
|
1068
|
+
y: centerY - visual.ry * 0.85,
|
|
1069
|
+
className: 'compass-node-air-state',
|
|
1070
|
+
textAnchor: 'middle',
|
|
1071
|
+
dominantBaseline: 'middle',
|
|
1072
|
+
},
|
|
798
1073
|
h('title', null, `当前舰队制空 ${airPower.min}~${airPower.max};未计沿途损耗、陆航和支援${airState.uncertain ? ';敌编成数据不完整或不确定' : ''}`),
|
|
799
|
-
...
|
|
1074
|
+
...displayAirState.states.flatMap((state, index) => [
|
|
800
1075
|
index ? h('tspan', { key: 'separator', fill: '#ffffff' }, '/') : null,
|
|
801
|
-
h('tspan', {
|
|
1076
|
+
h('tspan', {
|
|
1077
|
+
key: `${state.label}-${index}`,
|
|
1078
|
+
fill: '#ffffff',
|
|
1079
|
+
style: {
|
|
1080
|
+
fill: '#ffffff',
|
|
1081
|
+
textShadow: `1px 1px 0 ${state.shadow}`,
|
|
1082
|
+
filter: `drop-shadow(1px 1px 0 ${state.shadow})`,
|
|
1083
|
+
},
|
|
1084
|
+
}, state.label),
|
|
802
1085
|
]), airState.uncertain ? '?' : null) : null,
|
|
803
1086
|
)
|
|
804
1087
|
}),
|
|
@@ -852,7 +1135,7 @@ class Compass extends React.Component {
|
|
|
852
1135
|
route ? h('label', { className: 'compass-export-route' }, '路线',
|
|
853
1136
|
h('select', { value: route.key, 'aria-label': '导出路线', onChange: (event) => this.setState({ exportRoute: event.target.value, exportError: null }) },
|
|
854
1137
|
...choices.routes.map((candidate) => h('option', { key: candidate.key, value: candidate.key },
|
|
855
|
-
`${candidate.nodes.join('→')}${candidate.boss ? ' · Boss' : ''} · ${candidate.probability == null ? '含能动选路' :
|
|
1138
|
+
`${candidate.nodes.join('→')}${candidate.boss ? ' · Boss' : ''} · ${candidate.probability == null ? '含能动选路' : formatProbability(candidate.probability, candidate.estimated, this.state.probabilityFormat)}`))),
|
|
856
1139
|
) : null,
|
|
857
1140
|
choices?.blocked.length ? h('div', { className: 'compass-warnings' }, `未知分歧:${choices.blocked.join('、')}`) : null,
|
|
858
1141
|
battles?.length ? h('div', { className: 'compass-export-battles' },
|
|
@@ -960,22 +1243,33 @@ class Compass extends React.Component {
|
|
|
960
1243
|
h('h4', null, `${node} 点`),
|
|
961
1244
|
rows.length
|
|
962
1245
|
? h('div', { className: 'compass-conditions' }, ...rows.map(({ rule, result, outcomes: ruleOutcomes }) => {
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
const
|
|
1246
|
+
const sourceRuleId = rule.sourceRuleId || rule.id
|
|
1247
|
+
const sourceWasMatched = decision?.matchedRuleIds?.includes(sourceRuleId) || decision?.rule?.id === sourceRuleId
|
|
1248
|
+
const splitSourceRule = SEPARATE_SOURCE_LINE_RULE_IDS.has(sourceRuleId)
|
|
1249
|
+
const active = splitSourceRule ? sourceWasMatched && result.status === 'true' : sourceWasMatched
|
|
1250
|
+
const displayedOutcomes = active && (decision?.matchedRuleIds?.length || 0) <= 1
|
|
1251
|
+
? decision.outcomes
|
|
1252
|
+
: ruleOutcomes
|
|
1253
|
+
const outcomes = displayedOutcomes.length
|
|
1254
|
+
? displayedOutcomes.map((outcome) => outcomeText(outcome, this.state.probabilityFormat)).join(' / ')
|
|
1255
|
+
: '—'
|
|
1256
|
+
const rawLabel = predicateLabel(rule.predicate)
|
|
1257
|
+
const label = visibleRuleLabel(rule)
|
|
1258
|
+
const notes = rulePopoverNotes(rule, rawLabel, label)
|
|
966
1259
|
return h('div', {
|
|
967
1260
|
key: rule.id,
|
|
968
1261
|
className: `compass-condition is-${result.status}${active ? ' is-active' : ''}`,
|
|
969
1262
|
tabIndex: 0,
|
|
970
|
-
'aria-label': `${conditionStatus(result.status)}:${
|
|
1263
|
+
'aria-label': `${conditionStatus(result.status)}:${label}`,
|
|
971
1264
|
},
|
|
972
1265
|
h('span', { className: 'compass-condition-icon', 'aria-hidden': 'true' }, conditionIcon(result.status, active)),
|
|
973
|
-
h('span', { className: 'compass-condition-label' },
|
|
1266
|
+
h('span', { className: 'compass-condition-label' }, label),
|
|
974
1267
|
h('span', { className: 'compass-condition-outcomes' }, outcomes),
|
|
975
1268
|
h('div', { className: 'compass-condition-popover', role: 'tooltip' },
|
|
976
1269
|
h('div', null, `${conditionStatus(result.status)}${active ? ' · 当前出口规则' : ''}`),
|
|
1270
|
+
h('div', null, `条件:${rawLabel}`),
|
|
977
1271
|
h('div', null, `出口:${outcomes}`),
|
|
978
|
-
|
|
1272
|
+
...notes.map((note, index) => h('div', { key: `${rule.id}-note-${index}`, className: 'compass-condition-note' }, note)),
|
|
979
1273
|
),
|
|
980
1274
|
)
|
|
981
1275
|
}))
|
|
@@ -1048,6 +1342,35 @@ class Compass extends React.Component {
|
|
|
1048
1342
|
}, option.label)),
|
|
1049
1343
|
)
|
|
1050
1344
|
: null,
|
|
1345
|
+
h('div', { className: 'compass-option-group', role: 'group', 'aria-label': '概率标签格式' },
|
|
1346
|
+
h('span', { className: 'compass-option-label' }, '概率'),
|
|
1347
|
+
...[
|
|
1348
|
+
['percent', '%'],
|
|
1349
|
+
['decimal', '小数'],
|
|
1350
|
+
].map(([value, label]) => h('button', {
|
|
1351
|
+
key: value,
|
|
1352
|
+
type: 'button',
|
|
1353
|
+
className: this.state.probabilityFormat === value ? 'is-active' : '',
|
|
1354
|
+
'aria-pressed': this.state.probabilityFormat === value,
|
|
1355
|
+
onClick: () => this.setState({ probabilityFormat: value }),
|
|
1356
|
+
}, label)),
|
|
1357
|
+
),
|
|
1358
|
+
h('label', { className: 'compass-option-toggle' },
|
|
1359
|
+
h('input', {
|
|
1360
|
+
type: 'checkbox',
|
|
1361
|
+
checked: this.state.showEstimatedAir,
|
|
1362
|
+
onChange: (event) => this.setState({ showEstimatedAir: event.target.checked }),
|
|
1363
|
+
}),
|
|
1364
|
+
h('span', null, '推测制空'),
|
|
1365
|
+
),
|
|
1366
|
+
h('label', { className: 'compass-option-toggle' },
|
|
1367
|
+
h('input', {
|
|
1368
|
+
type: 'checkbox',
|
|
1369
|
+
checked: this.state.showEnemyAvatars,
|
|
1370
|
+
onChange: (event) => this.setState({ showEnemyAvatars: event.target.checked }),
|
|
1371
|
+
}),
|
|
1372
|
+
h('span', null, '敌舰头像'),
|
|
1373
|
+
),
|
|
1051
1374
|
h('span', {
|
|
1052
1375
|
className: 'compass-toolbar-fleet',
|
|
1053
1376
|
title: `舰队 ${this.state.deckId}:${fleetSummary(context)};${mapLosText}`,
|
|
@@ -1086,6 +1409,8 @@ exports.pluginWillUnload = stopPlugin
|
|
|
1086
1409
|
exports.__test = {
|
|
1087
1410
|
calculatePoiLos33,
|
|
1088
1411
|
cellFromDetail,
|
|
1412
|
+
edgeVisualStyle,
|
|
1413
|
+
formatProbability,
|
|
1089
1414
|
handleGameResponse,
|
|
1090
1415
|
kcnavNodeTypesFromMap,
|
|
1091
1416
|
localNodeTypesForMap,
|
|
@@ -1100,4 +1425,7 @@ exports.__test = {
|
|
|
1100
1425
|
phaseOptionsForMap,
|
|
1101
1426
|
pluginState,
|
|
1102
1427
|
routeEdges,
|
|
1428
|
+
ruleRows,
|
|
1429
|
+
visibleRuleLabel,
|
|
1430
|
+
separateSourceLineRuleIds: SEPARATE_SOURCE_LINE_RULE_IDS,
|
|
1103
1431
|
}
|