poi-plugin-compass 0.1.1 → 0.1.3
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/assets/compass.css +33 -2
- 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 +117 -26
- package/logic.js +164 -15
- package/package.json +2 -2
package/docs/research.md
CHANGED
|
@@ -131,7 +131,7 @@ const label = maps?.[mapKey]?.route?.[Number(cell)]?.[1]
|
|
|
131
131
|
|
|
132
132
|
无法确定“约 80%”是否为长期规则、样本估计还是攻略近似时,应使用 `approximate` 或 `unknown`,并把原文保留到审阅队列。
|
|
133
133
|
|
|
134
|
-
本轮已将通常海域 36 张 `/{带路条件}` 子页以及 5-6 主页面的原始 wikitext 缓存到 `data/sources/kcwiki/routes/`,并在 `manifest.json` 中记录 revision ID、修订时间和 SHA-256。规则校正以这批缓存为审阅入口:`CV系`/`CV*` 归入 `CV_ALL`,明确的 `CV+CVB` 归入 `CV_MAIN`;`CL系`、`CL+CT`、`BB系` 和单独 `BB`
|
|
134
|
+
本轮已将通常海域 36 张 `/{带路条件}` 子页以及 5-6 主页面的原始 wikitext 缓存到 `data/sources/kcwiki/routes/`,并在 `manifest.json` 中记录 revision ID、修订时间和 SHA-256。规则校正以这批缓存为审阅入口:`CA系`/`CA+CAV` 归入 `CA_ALL`,`CV系`/`CV*` 归入 `CV_ALL`,明确的 `CV+CVB` 归入 `CV_MAIN`;`CL系`、`CL+CT`、`BB系` 和单独 `BB` 也分别使用不同令牌。`CV_ALL` 是 CV+CVB+CVL,`CV_MAIN` 只含 CV+CVB;`CL_ALL` 是 CL+CLT+CT;`BB_ALL` 是 BB+FBB+BBV,而单独 `BB` 只含 BB+FBB,避免把 CVL、CT、BBV 或 CAV 静默漏掉或误计入。
|
|
135
135
|
|
|
136
136
|
### 4.2 日文 Wiki
|
|
137
137
|
|
|
@@ -247,6 +247,8 @@ interface RouteRule {
|
|
|
247
247
|
to: NodeLabel
|
|
248
248
|
probability: number | 'unknown'
|
|
249
249
|
}>
|
|
250
|
+
// 原文明确表示本次概率判定失败后继续检查后续规则
|
|
251
|
+
continueOnFailure?: boolean
|
|
250
252
|
appliesWhen?: { difficulty?: string; gauge?: number; phase?: number }
|
|
251
253
|
sourceRefs: SourceRef[]
|
|
252
254
|
confidence: 'exact' | 'verified' | 'parsed' | 'approximate' | 'unknown'
|
|
@@ -287,11 +289,11 @@ interface EnemyComposition {
|
|
|
287
289
|
|
|
288
290
|
1. 按 `priority` 排序;
|
|
289
291
|
2. 依次求值规则的 predicate;
|
|
290
|
-
3.
|
|
291
|
-
4.
|
|
292
|
-
5.
|
|
292
|
+
3. 对命中的规则按其出口概率消费当前剩余概率质量;
|
|
293
|
+
4. 若规则标记 `continueOnFailure`,或已知出口概率之和小于 1,则只消费已知命中概率,把失败的剩余质量交给同节点后续规则;
|
|
294
|
+
5. 没有后续规则时,才将明确的未知随机出口均分并标记估算;predicate 本身无法判断时返回 `unknown`,不越过该条件猜测后续规则。
|
|
293
295
|
|
|
294
|
-
|
|
296
|
+
因此页面中的“第一条规则”只是在没有续判语义时消耗全部质量;`4-3 H`、`2-3 J`、`4-5 Q` 等节点会把多个命中规则组合成复合概率,而不是把前一条的失败质量强行并入另一个出口。
|
|
295
297
|
|
|
296
298
|
### 6.2 概率质量传播
|
|
297
299
|
|
package/docs/sources.md
CHANGED
|
@@ -82,4 +82,4 @@ data/sources/kcwiki/routes/manifest.json
|
|
|
82
82
|
|
|
83
83
|
`1-1`~`1-6`、`2-1`~`2-5`、`3-1`~`3-5`、`4-1`~`4-5`、`5-1`~`5-6`、`6-1`~`6-5`、`7-1`~`7-5`。5-6 的路线分支直接记录在海域主页面 `南方海域/5-6`,因此缓存脚本对该图使用主页面标题,而其他通常图继续使用 `/{带路条件}` 子页。
|
|
84
84
|
|
|
85
|
-
其中带有“约”“随机但未给比例”“原文带问号”或依赖分歧点系数/司令部系数的条件,使用 `approximate` 或 `unknown
|
|
85
|
+
其中带有“约”“随机但未给比例”“原文带问号”或依赖分歧点系数/司令部系数的条件,使用 `approximate` 或 `unknown`;这类记录仍提供可能出口,但不产生伪造的全局百分比。原文明确写出“判定失败后继续”“继续下面判定”或“逐条依次判定”时,规则会保留已知命中概率并设置 `continueOnFailure`,将剩余概率交给同节点后续规则。当前临时估计约定为:未给出比例的两个出口按 50%/50% 并标记 `?`;索敌区间内“概率随索敌变化”的两个出口也按 50%/50% 并标记 `?`;只有一个出口且明确表示失败后继续判定时,该出口按 50% 并把剩余 50% 交给后续规则。固定单出口仍按 100% 处理,但保留未知/近似标记。规则面板对 KCWiki 原文中的多条同向条件逐行显示,评估器仍按来源顺序处理。7-3 A 点曾将去 B 和去 C 的条件错误合并,已按缓存原文顺序拆开并修正出口。后续继续补录时,先以同一 API 子页取得规则原文,再用日文 Wiki 或浏览器中的 NGA 页面复核有冲突的条件。
|
package/index.js
CHANGED
|
@@ -404,6 +404,58 @@ function outcomeText(outcome) {
|
|
|
404
404
|
return `${outcome.to} ${formatPercent(outcome.probability, outcome.estimated)}`
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// These predicates came from separate list items in the cached KCWiki route
|
|
408
|
+
// tables. Keep the evaluator's compact OR semantics for compatibility, but
|
|
409
|
+
// render each source line separately so the UI does not invent one combined
|
|
410
|
+
// rule where the source did not have one.
|
|
411
|
+
const SEPARATE_SOURCE_LINE_RULE_IDS = new Set([
|
|
412
|
+
'2-5-C-heavy',
|
|
413
|
+
'1-5-F-heavy',
|
|
414
|
+
'1-6-start-heavy', '1-6-M-heavy',
|
|
415
|
+
'2-3-D-special',
|
|
416
|
+
'4-2-C-light-l',
|
|
417
|
+
'6-1-start-heavy3',
|
|
418
|
+
'6-2-C-a', '6-2-D-f', '6-2-E-f',
|
|
419
|
+
'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',
|
|
420
|
+
'6-5-start-left', '6-5-C-e', '6-5-E-i', '6-5-I-h',
|
|
421
|
+
'7-1-B-a', '7-1-H-k',
|
|
422
|
+
'7-2-C-d', '7-2-E-g', '7-2-I-j',
|
|
423
|
+
'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',
|
|
424
|
+
'7-4-start-c', '7-4-start-a', '7-4-C-d', '7-4-J-k', '7-4-J-l', '7-4-M-heavy',
|
|
425
|
+
'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',
|
|
426
|
+
])
|
|
427
|
+
|
|
428
|
+
function visibleRuleLabel(rule) {
|
|
429
|
+
const label = predicateLabel(rule?.predicate)
|
|
430
|
+
return label
|
|
431
|
+
.replace(/([^)]*(?:近似|约|原文|比例未知|分歧系数)[^)]*)/g, '')
|
|
432
|
+
.replace(/\([^)]*(?:approx|about|original|unknown|coefficient)[^)]*\)/gi, '')
|
|
433
|
+
.replace(/,?\s*失败后继续判定/g, '')
|
|
434
|
+
.replace(/:\s*[^,;:]+概率(?:未知|随索敌变化)/g, '')
|
|
435
|
+
.replace(/;\s*(?:近似|约)/g, '')
|
|
436
|
+
.replace(/(原文(?:标记\s*\?|比例未知)[^)]*)/g, '')
|
|
437
|
+
.replace(/(约)/g, '')
|
|
438
|
+
.replace(/\s{2,}/g, ' ')
|
|
439
|
+
.replace(/:\s*$/g, '')
|
|
440
|
+
.trim()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function rulePopoverNotes(rule, rawLabel, visibleLabel) {
|
|
444
|
+
const notes = []
|
|
445
|
+
if (rawLabel !== visibleLabel) notes.push(`原文补充:${rawLabel}`)
|
|
446
|
+
if (rule?.continueOnFailure === true) notes.push('本条判定失败后继续检查后续规则')
|
|
447
|
+
if (rule?.confidence === 'approximate') notes.push('该条件或概率来自近似资料')
|
|
448
|
+
if (rule?.confidence === 'unknown' || rule?.outcomes?.some((outcome) => outcome?.probability == null)) {
|
|
449
|
+
notes.push('来源没有给出明确概率;当前按均等概率暂估')
|
|
450
|
+
}
|
|
451
|
+
return notes
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function displayPredicatesForRule(rule) {
|
|
455
|
+
if (!SEPARATE_SOURCE_LINE_RULE_IDS.has(rule?.id) || rule?.predicate?.kind !== 'any') return [rule?.predicate]
|
|
456
|
+
return rule.predicate.predicates?.length ? rule.predicate.predicates : [rule.predicate]
|
|
457
|
+
}
|
|
458
|
+
|
|
407
459
|
function conditionIcon(status, active) {
|
|
408
460
|
if (active) return '✓'
|
|
409
461
|
if (status === 'true') return '•'
|
|
@@ -423,11 +475,16 @@ function ruleRows(mapDefinition, node, context) {
|
|
|
423
475
|
return (mapDefinition.rules || [])
|
|
424
476
|
.filter((rule) => rule.node === node)
|
|
425
477
|
.sort((left, right) => number(left.priority) - number(right.priority))
|
|
426
|
-
.
|
|
427
|
-
rule,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
478
|
+
.reduce((rows, rule) => {
|
|
479
|
+
displayPredicatesForRule(rule).forEach((predicate, index) => {
|
|
480
|
+
rows.push({
|
|
481
|
+
rule: { ...rule, id: `${rule.id}::${index}`, predicate, sourceRuleId: rule.id },
|
|
482
|
+
result: evaluatePredicate(predicate, context),
|
|
483
|
+
outcomes: normalizeOutcomes(rule.outcomes, rule),
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
return rows
|
|
487
|
+
}, [])
|
|
431
488
|
}
|
|
432
489
|
|
|
433
490
|
function fleetSummary(context) {
|
|
@@ -496,6 +553,7 @@ class Compass extends React.Component {
|
|
|
496
553
|
: mapIds[0],
|
|
497
554
|
phaseId: phaseOptionsForMap(mapIds[0])[0]?.id || null,
|
|
498
555
|
selectedNode: pluginState.currentNode || null,
|
|
556
|
+
selectedEdge: null,
|
|
499
557
|
overrides: {},
|
|
500
558
|
exportOpen: false,
|
|
501
559
|
exportRoute: null,
|
|
@@ -535,6 +593,7 @@ class Compass extends React.Component {
|
|
|
535
593
|
mapId,
|
|
536
594
|
phaseId: phaseOptionsForMap(mapId)[0]?.id || null,
|
|
537
595
|
selectedNode: null,
|
|
596
|
+
selectedEdge: null,
|
|
538
597
|
overrides: {},
|
|
539
598
|
exportRoute: null,
|
|
540
599
|
exportEnemies: {},
|
|
@@ -582,15 +641,15 @@ class Compass extends React.Component {
|
|
|
582
641
|
}
|
|
583
642
|
|
|
584
643
|
selectPhase(phaseId) {
|
|
585
|
-
this.setState({ phaseId, selectedNode: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
644
|
+
this.setState({ phaseId, selectedNode: null, selectedEdge: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
586
645
|
}
|
|
587
646
|
|
|
588
647
|
selectNode(node) {
|
|
589
|
-
this.setState({ selectedNode: node })
|
|
648
|
+
this.setState({ selectedNode: node, selectedEdge: null })
|
|
590
649
|
}
|
|
591
650
|
|
|
592
651
|
selectFleet(deckId) {
|
|
593
|
-
this.setState({ deckId, selectedNode: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
652
|
+
this.setState({ deckId, selectedNode: null, selectedEdge: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
594
653
|
}
|
|
595
654
|
|
|
596
655
|
renderLosIcons(scores) {
|
|
@@ -611,15 +670,20 @@ class Compass extends React.Component {
|
|
|
611
670
|
`制空 ${air.min === air.max ? air.min : `${air.min}~${air.max}`}`)
|
|
612
671
|
}
|
|
613
672
|
|
|
614
|
-
setManualOverride(node, to) {
|
|
673
|
+
setManualOverride(node, to, edgeId = null, selectedNode = node) {
|
|
615
674
|
this.setState((state) => ({
|
|
616
675
|
overrides: { ...state.overrides, [node]: to },
|
|
617
|
-
selectedNode
|
|
676
|
+
selectedNode,
|
|
677
|
+
selectedEdge: edgeId,
|
|
618
678
|
exportRoute: null,
|
|
619
679
|
exportError: null,
|
|
620
680
|
}))
|
|
621
681
|
}
|
|
622
682
|
|
|
683
|
+
selectEdge(edgeId) {
|
|
684
|
+
this.setState({ selectedEdge: edgeId })
|
|
685
|
+
}
|
|
686
|
+
|
|
623
687
|
renderMap(mapDefinition, geometry, evaluation, selectedNode, currentNode, passedNodes = [], mapId = null) {
|
|
624
688
|
if (!mapGeometryAvailable(mapDefinition, geometry)) {
|
|
625
689
|
return h('div', { className: 'compass-empty-panel' }, 'poi 地图数据不可用')
|
|
@@ -658,6 +722,7 @@ class Compass extends React.Component {
|
|
|
658
722
|
|| []
|
|
659
723
|
const manualChoice = evaluation.decisions[edge.from]?.manual
|
|
660
724
|
&& manualOutcomes.some((outcome) => outcome.to === edge.to)
|
|
725
|
+
const selected = this.state.selectedEdge === edge.cell
|
|
661
726
|
const edgeClassNames = [
|
|
662
727
|
'compass-edge',
|
|
663
728
|
probability.global != null ? 'is-reachable' : '',
|
|
@@ -665,21 +730,33 @@ class Compass extends React.Component {
|
|
|
665
730
|
probabilityTier(probability.global),
|
|
666
731
|
passedEdges.has(`${edge.from}->${edge.to}`) ? 'is-passed' : '',
|
|
667
732
|
manualChoice ? 'is-manual-choice' : '',
|
|
733
|
+
selected ? 'is-selected' : '',
|
|
668
734
|
].filter(Boolean).join(' ')
|
|
669
735
|
const activateEdge = (event) => {
|
|
670
736
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
671
737
|
event.preventDefault()
|
|
672
|
-
this.setManualOverride(edge.from, edge.to)
|
|
738
|
+
if (manualChoice) this.setManualOverride(edge.from, edge.to, edge.cell)
|
|
739
|
+
else this.selectEdge(edge.cell)
|
|
673
740
|
}
|
|
674
741
|
}
|
|
675
742
|
return h('g', {
|
|
676
743
|
key: `edge-${edge.cell}`,
|
|
677
744
|
className: edgeClassNames,
|
|
678
|
-
role:
|
|
679
|
-
tabIndex:
|
|
680
|
-
|
|
681
|
-
|
|
745
|
+
role: 'button',
|
|
746
|
+
tabIndex: 0,
|
|
747
|
+
'aria-label': `边 ID ${edge.cell}:${edge.from}→${edge.to}`,
|
|
748
|
+
onClick: () => manualChoice
|
|
749
|
+
? this.setManualOverride(edge.from, edge.to, edge.cell)
|
|
750
|
+
: this.selectEdge(edge.cell),
|
|
751
|
+
onKeyDown: activateEdge,
|
|
682
752
|
},
|
|
753
|
+
h('title', null, [
|
|
754
|
+
`边 ID ${edge.cell}:${edge.from} → ${edge.to}`,
|
|
755
|
+
manualChoice ? '点击选择方向' : null,
|
|
756
|
+
probability.global != null && Number(probability.global) > 0
|
|
757
|
+
? `局部 ${formatPercent(probability.local, probability.estimated)}`
|
|
758
|
+
: null,
|
|
759
|
+
].filter(Boolean).join(' · ')),
|
|
683
760
|
h('line', {
|
|
684
761
|
x1: from.x,
|
|
685
762
|
y1: from.y,
|
|
@@ -694,10 +771,12 @@ class Compass extends React.Component {
|
|
|
694
771
|
className: 'compass-edge-label',
|
|
695
772
|
}, formatPercent(probability.global, probability.estimated))
|
|
696
773
|
: null,
|
|
697
|
-
|
|
698
|
-
? h('
|
|
699
|
-
|
|
700
|
-
:
|
|
774
|
+
selected
|
|
775
|
+
? h('text', {
|
|
776
|
+
x: (from.x + to.x) / 2,
|
|
777
|
+
y: (from.y + to.y) / 2 - 18,
|
|
778
|
+
className: 'compass-edge-id-label',
|
|
779
|
+
}, `ID ${edge.cell}`)
|
|
701
780
|
: null,
|
|
702
781
|
)
|
|
703
782
|
}),
|
|
@@ -725,7 +804,7 @@ class Compass extends React.Component {
|
|
|
725
804
|
const activateNode = (event) => {
|
|
726
805
|
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
727
806
|
event.preventDefault()
|
|
728
|
-
if (manualTarget) this.setManualOverride(manualSource, node)
|
|
807
|
+
if (manualTarget) this.setManualOverride(manualSource, node, null, node)
|
|
729
808
|
else this.selectNode(node)
|
|
730
809
|
}
|
|
731
810
|
return h('g', {
|
|
@@ -733,7 +812,7 @@ class Compass extends React.Component {
|
|
|
733
812
|
className: classNames,
|
|
734
813
|
role: 'button',
|
|
735
814
|
tabIndex: 0,
|
|
736
|
-
onClick: () => manualTarget ? this.setManualOverride(manualSource, node) : this.selectNode(node),
|
|
815
|
+
onClick: () => manualTarget ? this.setManualOverride(manualSource, node, null, node) : this.selectNode(node),
|
|
737
816
|
onKeyDown: activateNode,
|
|
738
817
|
},
|
|
739
818
|
h('title', null, nodeInfo?.label ? `${node} · ${nodeInfo.label}` : node),
|
|
@@ -938,22 +1017,31 @@ class Compass extends React.Component {
|
|
|
938
1017
|
h('h4', null, `${node} 点`),
|
|
939
1018
|
rows.length
|
|
940
1019
|
? h('div', { className: 'compass-conditions' }, ...rows.map(({ rule, result, outcomes: ruleOutcomes }) => {
|
|
941
|
-
const
|
|
942
|
-
const
|
|
1020
|
+
const sourceRuleId = rule.sourceRuleId || rule.id
|
|
1021
|
+
const sourceWasMatched = decision?.matchedRuleIds?.includes(sourceRuleId) || decision?.rule?.id === sourceRuleId
|
|
1022
|
+
const splitSourceRule = SEPARATE_SOURCE_LINE_RULE_IDS.has(sourceRuleId)
|
|
1023
|
+
const active = splitSourceRule ? sourceWasMatched && result.status === 'true' : sourceWasMatched
|
|
1024
|
+
const displayedOutcomes = active && (decision?.matchedRuleIds?.length || 0) <= 1
|
|
1025
|
+
? decision.outcomes
|
|
1026
|
+
: ruleOutcomes
|
|
943
1027
|
const outcomes = displayedOutcomes.length ? displayedOutcomes.map(outcomeText).join(' / ') : '—'
|
|
1028
|
+
const rawLabel = predicateLabel(rule.predicate)
|
|
1029
|
+
const label = visibleRuleLabel(rule)
|
|
1030
|
+
const notes = rulePopoverNotes(rule, rawLabel, label)
|
|
944
1031
|
return h('div', {
|
|
945
1032
|
key: rule.id,
|
|
946
1033
|
className: `compass-condition is-${result.status}${active ? ' is-active' : ''}`,
|
|
947
1034
|
tabIndex: 0,
|
|
948
|
-
'aria-label': `${conditionStatus(result.status)}:${
|
|
1035
|
+
'aria-label': `${conditionStatus(result.status)}:${label}`,
|
|
949
1036
|
},
|
|
950
1037
|
h('span', { className: 'compass-condition-icon', 'aria-hidden': 'true' }, conditionIcon(result.status, active)),
|
|
951
|
-
h('span', { className: 'compass-condition-label' },
|
|
1038
|
+
h('span', { className: 'compass-condition-label' }, label),
|
|
952
1039
|
h('span', { className: 'compass-condition-outcomes' }, outcomes),
|
|
953
1040
|
h('div', { className: 'compass-condition-popover', role: 'tooltip' },
|
|
954
1041
|
h('div', null, `${conditionStatus(result.status)}${active ? ' · 当前出口规则' : ''}`),
|
|
1042
|
+
h('div', null, `条件:${rawLabel}`),
|
|
955
1043
|
h('div', null, `出口:${outcomes}`),
|
|
956
|
-
|
|
1044
|
+
...notes.map((note, index) => h('div', { key: `${rule.id}-note-${index}`, className: 'compass-condition-note' }, note)),
|
|
957
1045
|
),
|
|
958
1046
|
)
|
|
959
1047
|
}))
|
|
@@ -1078,4 +1166,7 @@ exports.__test = {
|
|
|
1078
1166
|
phaseOptionsForMap,
|
|
1079
1167
|
pluginState,
|
|
1080
1168
|
routeEdges,
|
|
1169
|
+
ruleRows,
|
|
1170
|
+
visibleRuleLabel,
|
|
1171
|
+
separateSourceLineRuleIds: SEPARATE_SOURCE_LINE_RULE_IDS,
|
|
1081
1172
|
}
|
package/logic.js
CHANGED
|
@@ -9,19 +9,20 @@ const SHIP_CATEGORIES = {
|
|
|
9
9
|
CL_ALL: new Set([3, 4, 21]),
|
|
10
10
|
CA: new Set([5]),
|
|
11
11
|
CAV: new Set([6]),
|
|
12
|
+
CA_ALL: new Set([5, 6]),
|
|
12
13
|
CVL: new Set([7]),
|
|
13
14
|
BB: new Set([8, 9]),
|
|
14
15
|
BBV: new Set([10]),
|
|
15
16
|
BB_ALL: new Set([8, 9, 10]),
|
|
16
|
-
CV: new Set([11, 12
|
|
17
|
-
CV_ALL: new Set([7, 11, 12
|
|
17
|
+
CV: new Set([11, 12]),
|
|
18
|
+
CV_ALL: new Set([7, 11, 12]),
|
|
18
19
|
SS: new Set([13, 14]),
|
|
19
20
|
AO: new Set([15]),
|
|
20
21
|
AV: new Set([16]),
|
|
21
22
|
LHA: new Set([17]),
|
|
22
23
|
AR: new Set([19]),
|
|
23
24
|
AS: new Set([20]),
|
|
24
|
-
CV_MAIN: new Set([11, 12
|
|
25
|
+
CV_MAIN: new Set([11, 12]),
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const CATEGORY_LABELS = {
|
|
@@ -33,6 +34,7 @@ const CATEGORY_LABELS = {
|
|
|
33
34
|
CL_ALL: 'CL系',
|
|
34
35
|
CA: '重巡',
|
|
35
36
|
CAV: '航巡',
|
|
37
|
+
CA_ALL: 'CA系',
|
|
36
38
|
CVL: '轻空母',
|
|
37
39
|
BB: '战舰(BB/FBB)',
|
|
38
40
|
BBV: '航战',
|
|
@@ -45,7 +47,7 @@ const CATEGORY_LABELS = {
|
|
|
45
47
|
LHA: '扬陆',
|
|
46
48
|
AR: '工作舰',
|
|
47
49
|
AS: '潜母',
|
|
48
|
-
CV_MAIN: '
|
|
50
|
+
CV_MAIN: 'CV/CVB',
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
const EQUIPMENT_CATEGORY_LABELS = {
|
|
@@ -313,17 +315,152 @@ function normalizeOutcomes(outcomes, rule = null, options = {}) {
|
|
|
313
315
|
: Number.isFinite(Number(outcome.probability)) ? Number(outcome.probability) : null,
|
|
314
316
|
}))
|
|
315
317
|
|
|
318
|
+
if (!normalized.length) return []
|
|
319
|
+
const hasKnownProbability = normalized.some((outcome) => outcome.probability != null)
|
|
316
320
|
const hasMissingProbability = normalized.some((outcome) => outcome.probability == null)
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
const allUnknown = !hasKnownProbability
|
|
322
|
+
if (rule?.continueOnFailure === true) {
|
|
323
|
+
if (!hasKnownProbability) {
|
|
324
|
+
// A rule such as “within this LOS range, go to N; if the check fails,
|
|
325
|
+
// continue below” has two effective branches. The source does not give
|
|
326
|
+
// a ratio, so expose the same temporary 50/50 estimate used by the
|
|
327
|
+
// route evaluator instead of showing an unhelpful “unknown” row.
|
|
328
|
+
const probability = 1 / (normalized.length + 1)
|
|
329
|
+
return normalized.map((outcome) => ({ to: outcome.to, probability, estimated: true }))
|
|
330
|
+
}
|
|
331
|
+
const estimated = rule.confidence === 'unknown' || rule.confidence === 'approximate'
|
|
332
|
+
return normalized
|
|
333
|
+
.filter((outcome) => outcome.probability != null)
|
|
334
|
+
.map((outcome) => ({
|
|
335
|
+
to: outcome.to,
|
|
336
|
+
probability: outcome.probability,
|
|
337
|
+
...(estimated ? { estimated: true } : {}),
|
|
338
|
+
}))
|
|
339
|
+
}
|
|
340
|
+
if (allUnknown && options.equalizeUnknown === false) {
|
|
341
|
+
return normalized.map((outcome) => ({ to: outcome.to, probability: null }))
|
|
342
|
+
}
|
|
343
|
+
const equalize = options.equalizeUnknown !== false && (
|
|
344
|
+
allUnknown || !hasMissingProbability && rule?.confidence === 'unknown'
|
|
345
|
+
)
|
|
346
|
+
const estimated = equalize || hasMissingProbability || rule?.confidence === 'unknown'
|
|
347
|
+
|| rule?.confidence === 'approximate' && normalized.length > 1
|
|
348
|
+
return normalized
|
|
349
|
+
.filter((outcome) => equalize || outcome.probability != null)
|
|
350
|
+
.map((outcome) => {
|
|
320
351
|
const normalizedOutcome = {
|
|
321
352
|
to: outcome.to,
|
|
322
353
|
probability: equalize && normalized.length ? 1 / normalized.length : outcome.probability,
|
|
323
354
|
}
|
|
324
355
|
if (estimated) normalizedOutcome.estimated = true
|
|
325
356
|
return normalizedOutcome
|
|
326
|
-
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function ruleDistribution(rule) {
|
|
361
|
+
const raw = (Array.isArray(rule?.outcomes) ? rule.outcomes : [])
|
|
362
|
+
.filter((outcome) => outcome && typeof outcome.to === 'string')
|
|
363
|
+
.map((outcome) => ({
|
|
364
|
+
to: outcome.to,
|
|
365
|
+
probability: outcome.probability == null
|
|
366
|
+
? null
|
|
367
|
+
: Number.isFinite(Number(outcome.probability)) ? Number(outcome.probability) : null,
|
|
368
|
+
}))
|
|
369
|
+
if (!raw.length) return { outcomes: [], residual: rule?.continueOnFailure === true ? 1 : 0 }
|
|
370
|
+
|
|
371
|
+
const known = raw.filter((outcome) => outcome.probability != null)
|
|
372
|
+
const knownTotal = known.reduce((total, outcome) => total + outcome.probability, 0)
|
|
373
|
+
const partial = rule?.continueOnFailure === true
|
|
374
|
+
|| (known.length > 0 && knownTotal < 1 - 1e-9)
|
|
375
|
+
|| (known.length > 0 && known.length < raw.length)
|
|
376
|
+
|
|
377
|
+
if (partial) {
|
|
378
|
+
if (known.length) {
|
|
379
|
+
return {
|
|
380
|
+
outcomes: normalizeOutcomes(raw, rule),
|
|
381
|
+
residual: Math.max(0, 1 - knownTotal),
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const probability = 1 / (raw.length + 1)
|
|
385
|
+
return {
|
|
386
|
+
outcomes: raw.map((outcome) => ({ to: outcome.to, probability, estimated: true })),
|
|
387
|
+
residual: probability,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { outcomes: normalizeOutcomes(raw, rule), residual: 0 }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function evaluateRouteRules(mapDefinition, node, context) {
|
|
395
|
+
const rules = rulesForNode(mapDefinition, node)
|
|
396
|
+
if (!rules.length) {
|
|
397
|
+
return { status: 'terminal', rule: null, predicate: null, matchedRuleIds: [], outcomes: [], unknownProbability: 0 }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const totals = new Map()
|
|
401
|
+
const matchedRuleIds = []
|
|
402
|
+
let residual = 1
|
|
403
|
+
let firstRule = null
|
|
404
|
+
let firstPredicate = null
|
|
405
|
+
let unresolvedPredicate = null
|
|
406
|
+
let terminal = false
|
|
407
|
+
|
|
408
|
+
function addOutcome(outcome, mass) {
|
|
409
|
+
if (!Number.isFinite(mass) || mass <= 0) return
|
|
410
|
+
const current = totals.get(outcome.to)
|
|
411
|
+
if (!current) {
|
|
412
|
+
totals.set(outcome.to, {
|
|
413
|
+
to: outcome.to,
|
|
414
|
+
probability: mass,
|
|
415
|
+
...(outcome.estimated === true ? { estimated: true } : {}),
|
|
416
|
+
})
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
current.probability += mass
|
|
420
|
+
if (outcome.estimated === true) current.estimated = true
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const rule of rules) {
|
|
424
|
+
if (residual <= 1e-9) break
|
|
425
|
+
const predicate = evaluatePredicate(rule.predicate, context)
|
|
426
|
+
if (predicate.status === 'false') continue
|
|
427
|
+
if (predicate.status === 'unknown') {
|
|
428
|
+
unresolvedPredicate = { rule, predicate }
|
|
429
|
+
break
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!firstRule) {
|
|
433
|
+
firstRule = rule
|
|
434
|
+
firstPredicate = predicate
|
|
435
|
+
}
|
|
436
|
+
if (rule.id) matchedRuleIds.push(rule.id)
|
|
437
|
+
if (rule.terminal === true) {
|
|
438
|
+
residual = 0
|
|
439
|
+
terminal = true
|
|
440
|
+
break
|
|
441
|
+
}
|
|
442
|
+
const distribution = ruleDistribution(rule)
|
|
443
|
+
if (!distribution.outcomes.length) {
|
|
444
|
+
unresolvedPredicate = { rule, predicate }
|
|
445
|
+
break
|
|
446
|
+
}
|
|
447
|
+
distribution.outcomes.forEach((outcome) => addOutcome(outcome, residual * outcome.probability))
|
|
448
|
+
residual *= distribution.residual
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const outcomes = [...totals.values()]
|
|
452
|
+
const unknownProbability = (unresolvedPredicate || residual > 1e-9) ? residual : 0
|
|
453
|
+
const status = unknownProbability > 1e-9
|
|
454
|
+
? 'unknown'
|
|
455
|
+
: outcomes.length ? 'matched' : terminal ? 'terminal' : 'unknown'
|
|
456
|
+
return {
|
|
457
|
+
status,
|
|
458
|
+
rule: firstRule || unresolvedPredicate?.rule || null,
|
|
459
|
+
predicate: firstPredicate || unresolvedPredicate?.predicate || null,
|
|
460
|
+
matchedRuleIds,
|
|
461
|
+
outcomes,
|
|
462
|
+
unknownProbability,
|
|
463
|
+
}
|
|
327
464
|
}
|
|
328
465
|
|
|
329
466
|
function routeDecision(mapDefinition, node, context, overrides = {}) {
|
|
@@ -334,9 +471,17 @@ function routeDecision(mapDefinition, node, context, overrides = {}) {
|
|
|
334
471
|
const manualOutcomes = Array.isArray(configuredManualOutcomes)
|
|
335
472
|
? normalizeOutcomes(configuredManualOutcomes, { confidence: 'unknown' }, { equalizeUnknown: false })
|
|
336
473
|
: null
|
|
337
|
-
const
|
|
338
|
-
?
|
|
339
|
-
|
|
474
|
+
const automatic = manual
|
|
475
|
+
? {
|
|
476
|
+
status: selected.status,
|
|
477
|
+
rule: selected.rule,
|
|
478
|
+
predicate: selected.predicate,
|
|
479
|
+
matchedRuleIds: selected.rule?.id ? [selected.rule.id] : [],
|
|
480
|
+
outcomes: selected.status === 'matched' ? normalizeOutcomes(selected.rule?.outcomes, selected.rule) : [],
|
|
481
|
+
unknownProbability: 0,
|
|
482
|
+
}
|
|
483
|
+
: evaluateRouteRules(mapDefinition, node, context)
|
|
484
|
+
const autoOutcomes = automatic.outcomes
|
|
340
485
|
const phaseRuleMatched = manual && hasPhaseRules(mapDefinition, node) && selected.status === 'matched'
|
|
341
486
|
const baseOutcomes = phaseRuleMatched
|
|
342
487
|
? autoOutcomes
|
|
@@ -349,9 +494,11 @@ function routeDecision(mapDefinition, node, context, overrides = {}) {
|
|
|
349
494
|
: baseOutcomes
|
|
350
495
|
return {
|
|
351
496
|
node,
|
|
352
|
-
status:
|
|
353
|
-
rule:
|
|
354
|
-
predicate:
|
|
497
|
+
status: automatic.status,
|
|
498
|
+
rule: automatic.rule,
|
|
499
|
+
predicate: automatic.predicate,
|
|
500
|
+
matchedRuleIds: automatic.matchedRuleIds,
|
|
501
|
+
unknownProbability: automatic.unknownProbability,
|
|
355
502
|
baseOutcomes,
|
|
356
503
|
manualOutcomes,
|
|
357
504
|
outcomes,
|
|
@@ -383,10 +530,12 @@ function propagateProbability(start, decisions) {
|
|
|
383
530
|
return
|
|
384
531
|
}
|
|
385
532
|
const decision = decisions[node]
|
|
386
|
-
if (!decision || decision.status === '
|
|
533
|
+
if (!decision || decision.status === 'terminal' && !decision.outcomes.length
|
|
534
|
+
|| decision.status === 'unknown' && !decision.outcomes.length) {
|
|
387
535
|
if (decision?.status === 'unknown') unknownNodes.add(node)
|
|
388
536
|
return
|
|
389
537
|
}
|
|
538
|
+
if (decision.status === 'unknown' || Number(decision.unknownProbability) > 1e-9) unknownNodes.add(node)
|
|
390
539
|
const nextPath = new Set(path)
|
|
391
540
|
nextPath.add(node)
|
|
392
541
|
decision.outcomes.forEach((outcome) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poi-plugin-compass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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.
|
|
55
|
+
"11.99.99": "0.1.3"
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|