poi-plugin-compass 0.1.0 → 0.1.2
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 +33 -61
- package/assets/compass.css +20 -2
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/1-4/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/1-5/H.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/1-5/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/1-6/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/1-6/L.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/2-1/F.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/2-3/M.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/2-4/E.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-1/B.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-1/D.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-2/J.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-2/K.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-3/K.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-4/B.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-4/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-4/M.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/3-4/N.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-1/E.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-3/A.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-3/G.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-3/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-3/L.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-4/B.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-4/F.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-4/G.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-4/H.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-5/E.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-5/F.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-5/G.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-5/J.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-5/N.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-5/O.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/4-5/S.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-2/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-2/K.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-2/L.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-3/C.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-3/M.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-3/N.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-4/F.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-4/G.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-4/L.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-5/J.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-6/A1.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-6/C1.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-6/P.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-6/Q1.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-6/Q2.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-6/T.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/5-6/V.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-1/D.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-1/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-1/J.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-2/B.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-2/F.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-2/H.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-2/I.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-4/E.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-4/G.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-4/H.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/6-4/L.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-1/B.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-1/C.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-2/H.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-2/J.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-3/B.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-3/J.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-3/M.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-3/N.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-4/G.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-4/H.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-5/C.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-5/E.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-5/L.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-5/N.json +1 -0
- package/data/sources/kcnav/enemies/2025-09-11_2026-09-11/7-5/R.json +1 -0
- package/data/sources/kcnav/enemies/manifest.json +771 -381
- package/docs/sources.md +2 -2
- package/enemies.js +1 -1
- package/index.js +35 -13
- package/package.json +4 -3
- package/scripts/cache-kcnav-enemies-long-low.js +115 -0
- package/scripts/cache-kcnav-enemies.js +9 -1
package/docs/sources.md
CHANGED
|
@@ -31,9 +31,9 @@ data/kcnav-node-types-by-map.json
|
|
|
31
31
|
|
|
32
32
|
### 敌编成日期筛选(2026-09-11)
|
|
33
33
|
|
|
34
|
-
核对 `https://tsunkit.net/nav/scripts/routing.js`:`startDate`/`endDate` 分别使用参数 `start`/`end`,格式为 `YYYY-MM-DD`;`enemies` 面板将同一组筛选参数传给节点的 `enemycomps` 接口。本插件固定本轮起止日期,仅发送这两个参数,不发送司令部等级、血条、phase
|
|
34
|
+
核对 `https://tsunkit.net/nav/scripts/routing.js`:`startDate`/`endDate` 分别使用参数 `start`/`end`,格式为 `YYYY-MM-DD`;`enemies` 面板将同一组筛选参数传给节点的 `enemycomps` 接口。本插件固定本轮起止日期,仅发送这两个参数,不发送司令部等级、血条、phase、舰队或路线限制。通常节点采用当天 UTC 日期向前一个日历月(月末截到上月末日);月度样本总数小于 100 的战斗节点另使用前一年同日到当天的窗口,并按节点保存实际使用的日期窗口。
|
|
35
35
|
|
|
36
|
-
`https://tsunkit.net/lang/en/kcnav.json` 的 `panelEnemiesAirPowerHeader` 顺序为 `termAirAD / termAirAP / termAirAS / termAirASPlus`;与前端 `airpower[0..3]` 对应,分别为劣势、均势、优势、确保。`lbasAirpower` 使用相同顺序但单独显示。原始响应按日期窗口/海图/节点保存,manifest 记录请求 URL、SHA-256
|
|
36
|
+
`https://tsunkit.net/lang/en/kcnav.json` 的 `panelEnemiesAirPowerHeader` 顺序为 `termAirAD / termAirAP / termAirAS / termAirASPlus`;与前端 `airpower[0..3]` 对应,分别为劣势、均势、优势、确保。`lbasAirpower` 使用相同顺序但单独显示。原始响应按日期窗口/海图/节点保存,manifest 记录请求 URL、SHA-256、每节点抓取时间和完成状态。遇到请求失败即停止,保留进度供重跑;运行时只读取本地缓存。低样本节点的一年窗口补采由 `npm run cache:kcnav-enemies:long-low` 执行,默认间隔 15 秒。
|
|
37
37
|
|
|
38
38
|
每个 API entry 原样保留舰船顺序、装备、阵型及编成 masterId;舰船实体用 `mainFleet[].id` / `escortFleet[].id`。界面按 `count` 降序显示,以本节点返回记录的 count 总和计算样本占比。零样本不生成百分比;四档制空缺失时显示未知;任何编成缺制空时,节点整体范围也显示未知。混合阶段和司令部等级的占比仅描述当前抓取样本。
|
|
39
39
|
|
package/enemies.js
CHANGED
|
@@ -44,7 +44,7 @@ function loadEnemies(mapId, node) {
|
|
|
44
44
|
const source = manifest.nodes[`${mapId}/${node}`]
|
|
45
45
|
if (!source) return { status: 'missing', window: manifest.window }
|
|
46
46
|
const payload = readLocalJson(path.join(cacheRoot, source.file))
|
|
47
|
-
return { status: 'ready', window: manifest.window, source, ...summarizeEntries(payload.result.entries) }
|
|
47
|
+
return { status: 'ready', window: source.window || manifest.window, source, ...summarizeEntries(payload.result.entries) }
|
|
48
48
|
} catch (error) {
|
|
49
49
|
return { status: error.code === 'ENOENT' ? 'missing' : 'invalid' }
|
|
50
50
|
}
|
package/index.js
CHANGED
|
@@ -496,6 +496,7 @@ class Compass extends React.Component {
|
|
|
496
496
|
: mapIds[0],
|
|
497
497
|
phaseId: phaseOptionsForMap(mapIds[0])[0]?.id || null,
|
|
498
498
|
selectedNode: pluginState.currentNode || null,
|
|
499
|
+
selectedEdge: null,
|
|
499
500
|
overrides: {},
|
|
500
501
|
exportOpen: false,
|
|
501
502
|
exportRoute: null,
|
|
@@ -535,6 +536,7 @@ class Compass extends React.Component {
|
|
|
535
536
|
mapId,
|
|
536
537
|
phaseId: phaseOptionsForMap(mapId)[0]?.id || null,
|
|
537
538
|
selectedNode: null,
|
|
539
|
+
selectedEdge: null,
|
|
538
540
|
overrides: {},
|
|
539
541
|
exportRoute: null,
|
|
540
542
|
exportEnemies: {},
|
|
@@ -582,15 +584,15 @@ class Compass extends React.Component {
|
|
|
582
584
|
}
|
|
583
585
|
|
|
584
586
|
selectPhase(phaseId) {
|
|
585
|
-
this.setState({ phaseId, selectedNode: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
587
|
+
this.setState({ phaseId, selectedNode: null, selectedEdge: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
586
588
|
}
|
|
587
589
|
|
|
588
590
|
selectNode(node) {
|
|
589
|
-
this.setState({ selectedNode: node })
|
|
591
|
+
this.setState({ selectedNode: node, selectedEdge: null })
|
|
590
592
|
}
|
|
591
593
|
|
|
592
594
|
selectFleet(deckId) {
|
|
593
|
-
this.setState({ deckId, selectedNode: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
595
|
+
this.setState({ deckId, selectedNode: null, selectedEdge: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
|
|
594
596
|
}
|
|
595
597
|
|
|
596
598
|
renderLosIcons(scores) {
|
|
@@ -611,15 +613,20 @@ class Compass extends React.Component {
|
|
|
611
613
|
`制空 ${air.min === air.max ? air.min : `${air.min}~${air.max}`}`)
|
|
612
614
|
}
|
|
613
615
|
|
|
614
|
-
setManualOverride(node, to) {
|
|
616
|
+
setManualOverride(node, to, edgeId = null) {
|
|
615
617
|
this.setState((state) => ({
|
|
616
618
|
overrides: { ...state.overrides, [node]: to },
|
|
617
619
|
selectedNode: node,
|
|
620
|
+
selectedEdge: edgeId,
|
|
618
621
|
exportRoute: null,
|
|
619
622
|
exportError: null,
|
|
620
623
|
}))
|
|
621
624
|
}
|
|
622
625
|
|
|
626
|
+
selectEdge(edgeId) {
|
|
627
|
+
this.setState({ selectedEdge: edgeId })
|
|
628
|
+
}
|
|
629
|
+
|
|
623
630
|
renderMap(mapDefinition, geometry, evaluation, selectedNode, currentNode, passedNodes = [], mapId = null) {
|
|
624
631
|
if (!mapGeometryAvailable(mapDefinition, geometry)) {
|
|
625
632
|
return h('div', { className: 'compass-empty-panel' }, 'poi 地图数据不可用')
|
|
@@ -658,6 +665,7 @@ class Compass extends React.Component {
|
|
|
658
665
|
|| []
|
|
659
666
|
const manualChoice = evaluation.decisions[edge.from]?.manual
|
|
660
667
|
&& manualOutcomes.some((outcome) => outcome.to === edge.to)
|
|
668
|
+
const selected = this.state.selectedEdge === edge.cell
|
|
661
669
|
const edgeClassNames = [
|
|
662
670
|
'compass-edge',
|
|
663
671
|
probability.global != null ? 'is-reachable' : '',
|
|
@@ -665,21 +673,33 @@ class Compass extends React.Component {
|
|
|
665
673
|
probabilityTier(probability.global),
|
|
666
674
|
passedEdges.has(`${edge.from}->${edge.to}`) ? 'is-passed' : '',
|
|
667
675
|
manualChoice ? 'is-manual-choice' : '',
|
|
676
|
+
selected ? 'is-selected' : '',
|
|
668
677
|
].filter(Boolean).join(' ')
|
|
669
678
|
const activateEdge = (event) => {
|
|
670
679
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
671
680
|
event.preventDefault()
|
|
672
|
-
this.setManualOverride(edge.from, edge.to)
|
|
681
|
+
if (manualChoice) this.setManualOverride(edge.from, edge.to, edge.cell)
|
|
682
|
+
else this.selectEdge(edge.cell)
|
|
673
683
|
}
|
|
674
684
|
}
|
|
675
685
|
return h('g', {
|
|
676
686
|
key: `edge-${edge.cell}`,
|
|
677
687
|
className: edgeClassNames,
|
|
678
|
-
role:
|
|
679
|
-
tabIndex:
|
|
680
|
-
|
|
681
|
-
|
|
688
|
+
role: 'button',
|
|
689
|
+
tabIndex: 0,
|
|
690
|
+
'aria-label': `边 ID ${edge.cell}:${edge.from}→${edge.to}`,
|
|
691
|
+
onClick: () => manualChoice
|
|
692
|
+
? this.setManualOverride(edge.from, edge.to, edge.cell)
|
|
693
|
+
: this.selectEdge(edge.cell),
|
|
694
|
+
onKeyDown: activateEdge,
|
|
682
695
|
},
|
|
696
|
+
h('title', null, [
|
|
697
|
+
`边 ID ${edge.cell}:${edge.from} → ${edge.to}`,
|
|
698
|
+
manualChoice ? '点击选择方向' : null,
|
|
699
|
+
probability.global != null && Number(probability.global) > 0
|
|
700
|
+
? `局部 ${formatPercent(probability.local, probability.estimated)}`
|
|
701
|
+
: null,
|
|
702
|
+
].filter(Boolean).join(' · ')),
|
|
683
703
|
h('line', {
|
|
684
704
|
x1: from.x,
|
|
685
705
|
y1: from.y,
|
|
@@ -694,10 +714,12 @@ class Compass extends React.Component {
|
|
|
694
714
|
className: 'compass-edge-label',
|
|
695
715
|
}, formatPercent(probability.global, probability.estimated))
|
|
696
716
|
: null,
|
|
697
|
-
|
|
698
|
-
? h('
|
|
699
|
-
|
|
700
|
-
:
|
|
717
|
+
selected
|
|
718
|
+
? h('text', {
|
|
719
|
+
x: (from.x + to.x) / 2,
|
|
720
|
+
y: (from.y + to.y) / 2 - 18,
|
|
721
|
+
className: 'compass-edge-id-label',
|
|
722
|
+
}, `ID ${edge.cell}`)
|
|
701
723
|
: null,
|
|
702
724
|
)
|
|
703
725
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poi-plugin-compass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "通常海域舰队罗盘",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
"cache:kcwiki": "node scripts/cache-kcwiki-routes.js",
|
|
29
29
|
"cache:kcnav-maps": "node scripts/cache-kcnav-maps.js",
|
|
30
30
|
"cache:kcnav-enemies": "node scripts/cache-kcnav-enemies.js",
|
|
31
|
+
"cache:kcnav-enemies:long-low": "node scripts/cache-kcnav-enemies-long-low.js",
|
|
31
32
|
"compress:kcnav-maps": "node scripts/compress-kcnav-maps.js --remove-png",
|
|
32
33
|
"compress:kcnav-enemies": "node scripts/compact-kcnav-enemies.js",
|
|
33
|
-
"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/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",
|
|
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",
|
|
34
35
|
"test": "node test.js && node test-enemies.js && node test-exporters.js && node test-ui.js && node test-overview.js"
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"icon": "fa/compass",
|
|
52
53
|
"priority": 20,
|
|
53
54
|
"apiVer": {
|
|
54
|
-
"11.99.99": "0.1.
|
|
55
|
+
"11.99.99": "0.1.2"
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const nodeTypes = require('../data/kcnav-node-types-by-map.json')
|
|
7
|
+
const styles = require('../data/kcnav-node-types.json')
|
|
8
|
+
const { enemyUrl, validateResponse } = require('./cache-kcnav-enemies')
|
|
9
|
+
|
|
10
|
+
const root = path.join(__dirname, '../data/sources/kcnav/enemies')
|
|
11
|
+
const manifestPath = path.join(root, 'manifest.json')
|
|
12
|
+
const LOW_SAMPLE_THRESHOLD = 100
|
|
13
|
+
const INTERVAL_MS = 15000
|
|
14
|
+
|
|
15
|
+
function oneYearWindow(now = new Date()) {
|
|
16
|
+
const end = now.toISOString().slice(0, 10)
|
|
17
|
+
const startDate = new Date(`${end}T00:00:00Z`)
|
|
18
|
+
startDate.setUTCFullYear(startDate.getUTCFullYear() - 1)
|
|
19
|
+
return { start: startDate.toISOString().slice(0, 10), end }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readJson(file) {
|
|
23
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sampleCount(payload) {
|
|
27
|
+
return (payload?.result?.entries || []).reduce((sum, entry) => sum + entry.count, 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeJson(file, value, compact = false) {
|
|
31
|
+
const content = `${JSON.stringify(value, null, compact ? 0 : 2)}\n`
|
|
32
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
33
|
+
fs.writeFileSync(`${file}.tmp`, content)
|
|
34
|
+
fs.renameSync(`${file}.tmp`, file)
|
|
35
|
+
return crypto.createHash('sha256').update(content).digest('hex')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function combatTargets() {
|
|
39
|
+
return Object.entries(nodeTypes).sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
|
|
40
|
+
.flatMap(([mapId, nodes]) => Object.entries(nodes)
|
|
41
|
+
.filter(([, type]) => styles[type]?.hasEnemies)
|
|
42
|
+
.map(([node]) => ({ mapId, node })))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function currentSamples(manifest, target) {
|
|
46
|
+
const source = manifest.nodes[`${target.mapId}/${target.node}`]
|
|
47
|
+
if (!source) return 0
|
|
48
|
+
try {
|
|
49
|
+
return sampleCount(readJson(path.join(root, source.file)))
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const manifest = readJson(manifestPath)
|
|
57
|
+
const window = oneYearWindow()
|
|
58
|
+
const targets = combatTargets().filter((target) => currentSamples(manifest, target) < LOW_SAMPLE_THRESHOLD)
|
|
59
|
+
const previousRun = manifest.longWindow
|
|
60
|
+
manifest.longWindow = {
|
|
61
|
+
start: window.start,
|
|
62
|
+
end: window.end,
|
|
63
|
+
threshold: LOW_SAMPLE_THRESHOLD,
|
|
64
|
+
intervalMs: INTERVAL_MS,
|
|
65
|
+
targetCount: targets.length,
|
|
66
|
+
complete: false,
|
|
67
|
+
lastRequestAt: previousRun?.start === window.start && previousRun?.end === window.end
|
|
68
|
+
? previousRun.lastRequestAt : null,
|
|
69
|
+
}
|
|
70
|
+
writeJson(manifestPath, manifest)
|
|
71
|
+
|
|
72
|
+
for (const [index, target] of targets.entries()) {
|
|
73
|
+
const key = `${target.mapId}/${target.node}`
|
|
74
|
+
const existing = manifest.nodes[key]
|
|
75
|
+
const expectedFile = `${window.start}_${window.end}/${target.mapId}/${target.node}.json`
|
|
76
|
+
const existingFile = existing?.file === expectedFile ? path.join(root, existing.file) : null
|
|
77
|
+
if (existingFile && fs.existsSync(existingFile)) {
|
|
78
|
+
const hash = crypto.createHash('sha256').update(fs.readFileSync(existingFile)).digest('hex')
|
|
79
|
+
if (hash === existing.sha256) continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const elapsed = Date.now() - Date.parse(manifest.longWindow.lastRequestAt || '1970-01-01')
|
|
83
|
+
const wait = Math.max(0, INTERVAL_MS - elapsed)
|
|
84
|
+
if (wait) await new Promise((resolve) => setTimeout(resolve, wait))
|
|
85
|
+
manifest.longWindow.lastRequestAt = new Date().toISOString()
|
|
86
|
+
writeJson(manifestPath, manifest)
|
|
87
|
+
|
|
88
|
+
const url = enemyUrl(target.mapId, target.node, window)
|
|
89
|
+
console.log(`${index + 1}/${targets.length} ${key} ${window.start}..${window.end}`)
|
|
90
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(60000) })
|
|
91
|
+
if (!response.ok) throw new Error(`${key}: HTTP ${response.status}; cache retained, rerun to resume`)
|
|
92
|
+
const payload = await response.json()
|
|
93
|
+
const result = validateResponse(payload)
|
|
94
|
+
const file = expectedFile
|
|
95
|
+
const sha256 = writeJson(path.join(root, file), payload, true)
|
|
96
|
+
manifest.nodes[key] = {
|
|
97
|
+
file,
|
|
98
|
+
url,
|
|
99
|
+
sha256,
|
|
100
|
+
fetchedAt: new Date().toISOString(),
|
|
101
|
+
window,
|
|
102
|
+
entries: result.entries.length,
|
|
103
|
+
samples: sampleCount(payload),
|
|
104
|
+
}
|
|
105
|
+
writeJson(manifestPath, manifest)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
manifest.longWindow.complete = true
|
|
109
|
+
manifest.longWindow.finishedAt = new Date().toISOString()
|
|
110
|
+
writeJson(manifestPath, manifest)
|
|
111
|
+
console.log(`Cached ${targets.length} low-sample enemy nodes for one year`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (require.main === module) main().catch((error) => { console.error(error.message); process.exitCode = 1 })
|
|
115
|
+
module.exports = { oneYearWindow, sampleCount, combatTargets }
|
|
@@ -85,7 +85,15 @@ async function main() {
|
|
|
85
85
|
const result = validateResponse(payload)
|
|
86
86
|
const file = `${window.start}_${window.end}/${mapId}/${node}.json`
|
|
87
87
|
const sha256 = writeJson(path.join(root, file), payload, true)
|
|
88
|
-
manifest.nodes[key] = {
|
|
88
|
+
manifest.nodes[key] = {
|
|
89
|
+
file,
|
|
90
|
+
url,
|
|
91
|
+
sha256,
|
|
92
|
+
fetchedAt: new Date().toISOString(),
|
|
93
|
+
window,
|
|
94
|
+
entries: result.entries.length,
|
|
95
|
+
samples: result.entries.reduce((sum, entry) => sum + entry.count, 0),
|
|
96
|
+
}
|
|
89
97
|
writeJson(manifestPath, manifest)
|
|
90
98
|
}
|
|
91
99
|
manifest.complete = true
|