poi-plugin-compass 0.1.0

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.
Files changed (452) hide show
  1. package/README.md +79 -0
  2. package/air-status.js +28 -0
  3. package/assets/compass.css +647 -0
  4. package/assets/kcnav/icons/map_common/136.png +0 -0
  5. package/assets/kcnav/icons/map_common/137.png +0 -0
  6. package/assets/kcnav/icons/map_common/19.png +0 -0
  7. package/assets/kcnav/icons/map_main/17.png +0 -0
  8. package/assets/kcnav/icons/map_main/18.png +0 -0
  9. package/assets/kcnav/icons/map_main/40.png +0 -0
  10. package/assets/kcnav/icons/map_main/42.png +0 -0
  11. package/assets/kcnav/icons/map_main/46.png +0 -0
  12. package/assets/kcnav/icons/map_main/47.png +0 -0
  13. package/assets/kcnav/icons/map_main/48.png +0 -0
  14. package/assets/kcnav/icons/map_main/53.png +0 -0
  15. package/assets/kcnav/icons/map_main/54.png +0 -0
  16. package/assets/kcnav/icons/map_main/62.png +0 -0
  17. package/assets/kcnav/maps/1-1.webp +0 -0
  18. package/assets/kcnav/maps/1-2.webp +0 -0
  19. package/assets/kcnav/maps/1-3.webp +0 -0
  20. package/assets/kcnav/maps/1-4.webp +0 -0
  21. package/assets/kcnav/maps/1-5.webp +0 -0
  22. package/assets/kcnav/maps/1-6.webp +0 -0
  23. package/assets/kcnav/maps/2-1.webp +0 -0
  24. package/assets/kcnav/maps/2-2.webp +0 -0
  25. package/assets/kcnav/maps/2-3.webp +0 -0
  26. package/assets/kcnav/maps/2-4.webp +0 -0
  27. package/assets/kcnav/maps/2-5.webp +0 -0
  28. package/assets/kcnav/maps/3-1.webp +0 -0
  29. package/assets/kcnav/maps/3-2.webp +0 -0
  30. package/assets/kcnav/maps/3-3.webp +0 -0
  31. package/assets/kcnav/maps/3-4.webp +0 -0
  32. package/assets/kcnav/maps/3-5.webp +0 -0
  33. package/assets/kcnav/maps/4-1.webp +0 -0
  34. package/assets/kcnav/maps/4-2.webp +0 -0
  35. package/assets/kcnav/maps/4-3.webp +0 -0
  36. package/assets/kcnav/maps/4-4.webp +0 -0
  37. package/assets/kcnav/maps/4-5.webp +0 -0
  38. package/assets/kcnav/maps/5-1.webp +0 -0
  39. package/assets/kcnav/maps/5-2.webp +0 -0
  40. package/assets/kcnav/maps/5-3.webp +0 -0
  41. package/assets/kcnav/maps/5-4.webp +0 -0
  42. package/assets/kcnav/maps/5-5.webp +0 -0
  43. package/assets/kcnav/maps/5-6.webp +0 -0
  44. package/assets/kcnav/maps/6-1.webp +0 -0
  45. package/assets/kcnav/maps/6-2.webp +0 -0
  46. package/assets/kcnav/maps/6-3.webp +0 -0
  47. package/assets/kcnav/maps/6-4.webp +0 -0
  48. package/assets/kcnav/maps/6-5.webp +0 -0
  49. package/assets/kcnav/maps/7-1.webp +0 -0
  50. package/assets/kcnav/maps/7-2.webp +0 -0
  51. package/assets/kcnav/maps/7-3.webp +0 -0
  52. package/assets/kcnav/maps/7-4.webp +0 -0
  53. package/assets/kcnav/maps/7-5.webp +0 -0
  54. package/data/kc3kai-master-ids.json +1 -0
  55. package/data/kc3kai-ship-stats.json +21545 -0
  56. package/data/kcnav-node-types-by-map.json +606 -0
  57. package/data/kcnav-node-types.json +176 -0
  58. package/data/los-config.json +42 -0
  59. package/data/maps.json +590 -0
  60. package/data/normal-maps.json +1590 -0
  61. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-1/A.json +1 -0
  62. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-1/B.json +1 -0
  63. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-1/C.json +1 -0
  64. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-2/A.json +1 -0
  65. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-2/C.json +1 -0
  66. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-2/D.json +1 -0
  67. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-2/E.json +1 -0
  68. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-3/C.json +1 -0
  69. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-3/E.json +1 -0
  70. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-3/F.json +1 -0
  71. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-3/J.json +1 -0
  72. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-4/B.json +1 -0
  73. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-4/D.json +1 -0
  74. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-4/H.json +1 -0
  75. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-4/I.json +1 -0
  76. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-4/J.json +1 -0
  77. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-4/L.json +1 -0
  78. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/A.json +1 -0
  79. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/C.json +1 -0
  80. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/D.json +1 -0
  81. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/E.json +1 -0
  82. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/F.json +1 -0
  83. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/H.json +1 -0
  84. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/I.json +1 -0
  85. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-5/J.json +1 -0
  86. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/B.json +1 -0
  87. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/C.json +1 -0
  88. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/D.json +1 -0
  89. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/E.json +1 -0
  90. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/F.json +1 -0
  91. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/I.json +1 -0
  92. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/J.json +1 -0
  93. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/K.json +1 -0
  94. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/1-6/L.json +1 -0
  95. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-1/A.json +1 -0
  96. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-1/C.json +1 -0
  97. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-1/D.json +1 -0
  98. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-1/F.json +1 -0
  99. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-1/H.json +1 -0
  100. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-2/B.json +1 -0
  101. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-2/D.json +1 -0
  102. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-2/E.json +1 -0
  103. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-2/G.json +1 -0
  104. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-2/K.json +1 -0
  105. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-3/A.json +1 -0
  106. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-3/E.json +1 -0
  107. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-3/F.json +1 -0
  108. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-3/J.json +1 -0
  109. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-3/K.json +1 -0
  110. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-3/M.json +1 -0
  111. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-3/N.json +1 -0
  112. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-4/B.json +1 -0
  113. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-4/E.json +1 -0
  114. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-4/F.json +1 -0
  115. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-4/I.json +1 -0
  116. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-4/L.json +1 -0
  117. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-4/M.json +1 -0
  118. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-4/P.json +1 -0
  119. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-5/B.json +1 -0
  120. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-5/C.json +1 -0
  121. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-5/E.json +1 -0
  122. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-5/F.json +1 -0
  123. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-5/J.json +1 -0
  124. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-5/L.json +1 -0
  125. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/2-5/O.json +1 -0
  126. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-1/B.json +1 -0
  127. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-1/C.json +1 -0
  128. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-1/D.json +1 -0
  129. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-1/F.json +1 -0
  130. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-1/G.json +1 -0
  131. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-2/A.json +1 -0
  132. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-2/C.json +1 -0
  133. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-2/H.json +1 -0
  134. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-2/J.json +1 -0
  135. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-2/K.json +1 -0
  136. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-2/L.json +1 -0
  137. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-3/A.json +1 -0
  138. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-3/B.json +1 -0
  139. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-3/E.json +1 -0
  140. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-3/G.json +1 -0
  141. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-3/K.json +1 -0
  142. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-3/M.json +1 -0
  143. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/A.json +1 -0
  144. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/B.json +1 -0
  145. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/C.json +1 -0
  146. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/G.json +1 -0
  147. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/H.json +1 -0
  148. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/I.json +1 -0
  149. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/M.json +1 -0
  150. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/N.json +1 -0
  151. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-4/P.json +1 -0
  152. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-5/B.json +1 -0
  153. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-5/D.json +1 -0
  154. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-5/E.json +1 -0
  155. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-5/F.json +1 -0
  156. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-5/G.json +1 -0
  157. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-5/H.json +1 -0
  158. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/3-5/K.json +1 -0
  159. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-1/A.json +1 -0
  160. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-1/C.json +1 -0
  161. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-1/D.json +1 -0
  162. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-1/E.json +1 -0
  163. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-1/G.json +1 -0
  164. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-1/H.json +1 -0
  165. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-1/J.json +1 -0
  166. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/A.json +1 -0
  167. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/B.json +1 -0
  168. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/C.json +1 -0
  169. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/D.json +1 -0
  170. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/E.json +1 -0
  171. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/G.json +1 -0
  172. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/H.json +1 -0
  173. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-2/L.json +1 -0
  174. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/A.json +1 -0
  175. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/C.json +1 -0
  176. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/D.json +1 -0
  177. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/F.json +1 -0
  178. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/G.json +1 -0
  179. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/H.json +1 -0
  180. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/I.json +1 -0
  181. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/L.json +1 -0
  182. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-3/N.json +1 -0
  183. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/A.json +1 -0
  184. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/B.json +1 -0
  185. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/E.json +1 -0
  186. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/F.json +1 -0
  187. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/G.json +1 -0
  188. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/H.json +1 -0
  189. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/I.json +1 -0
  190. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-4/K.json +1 -0
  191. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/B.json +1 -0
  192. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/D.json +1 -0
  193. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/E.json +1 -0
  194. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/F.json +1 -0
  195. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/G.json +1 -0
  196. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/H.json +1 -0
  197. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/J.json +1 -0
  198. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/K.json +1 -0
  199. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/N.json +1 -0
  200. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/O.json +1 -0
  201. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/S.json +1 -0
  202. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/4-5/T.json +1 -0
  203. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-1/B.json +1 -0
  204. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-1/D.json +1 -0
  205. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-1/E.json +1 -0
  206. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-1/F.json +1 -0
  207. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-1/G.json +1 -0
  208. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-1/J.json +1 -0
  209. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/C.json +1 -0
  210. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/D.json +1 -0
  211. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/E.json +1 -0
  212. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/F.json +1 -0
  213. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/I.json +1 -0
  214. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/K.json +1 -0
  215. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/L.json +1 -0
  216. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-2/O.json +1 -0
  217. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/C.json +1 -0
  218. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/I.json +1 -0
  219. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/J.json +1 -0
  220. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/K.json +1 -0
  221. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/M.json +1 -0
  222. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/N.json +1 -0
  223. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/P.json +1 -0
  224. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-3/Q.json +1 -0
  225. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/C.json +1 -0
  226. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/E.json +1 -0
  227. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/F.json +1 -0
  228. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/G.json +1 -0
  229. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/H.json +1 -0
  230. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/J.json +1 -0
  231. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/L.json +1 -0
  232. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-4/P.json +1 -0
  233. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/B.json +1 -0
  234. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/C.json +1 -0
  235. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/G.json +1 -0
  236. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/H.json +1 -0
  237. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/J.json +1 -0
  238. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/K.json +1 -0
  239. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/M.json +1 -0
  240. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/N.json +1 -0
  241. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/P.json +1 -0
  242. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-5/S.json +1 -0
  243. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/A.json +1 -0
  244. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/A1.json +1 -0
  245. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/A2.json +1 -0
  246. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/B.json +1 -0
  247. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/C.json +1 -0
  248. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/C1.json +1 -0
  249. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/C2.json +1 -0
  250. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/D.json +1 -0
  251. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/G.json +1 -0
  252. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/H.json +1 -0
  253. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/J.json +1 -0
  254. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/K.json +1 -0
  255. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/K1.json +1 -0
  256. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/K2.json +1 -0
  257. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/L.json +1 -0
  258. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/N.json +1 -0
  259. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/P.json +1 -0
  260. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/Q.json +1 -0
  261. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/Q1.json +1 -0
  262. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/Q2.json +1 -0
  263. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/T.json +1 -0
  264. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/U.json +1 -0
  265. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/V.json +1 -0
  266. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/W.json +1 -0
  267. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/X.json +1 -0
  268. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/5-6/Z.json +1 -0
  269. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-1/C.json +1 -0
  270. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-1/D.json +1 -0
  271. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-1/F.json +1 -0
  272. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-1/H.json +1 -0
  273. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-1/I.json +1 -0
  274. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-1/J.json +1 -0
  275. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-1/K.json +1 -0
  276. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-2/B.json +1 -0
  277. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-2/C.json +1 -0
  278. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-2/F.json +1 -0
  279. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-2/H.json +1 -0
  280. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-2/I.json +1 -0
  281. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-2/J.json +1 -0
  282. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-2/K.json +1 -0
  283. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-3/B.json +1 -0
  284. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-3/C.json +1 -0
  285. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-3/D.json +1 -0
  286. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-3/E.json +1 -0
  287. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-3/F.json +1 -0
  288. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-3/J.json +1 -0
  289. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/A.json +1 -0
  290. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/B.json +1 -0
  291. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/C.json +1 -0
  292. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/D.json +1 -0
  293. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/E.json +1 -0
  294. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/F.json +1 -0
  295. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/G.json +1 -0
  296. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/H.json +1 -0
  297. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/I.json +1 -0
  298. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/J.json +1 -0
  299. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/K.json +1 -0
  300. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/L.json +1 -0
  301. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/M.json +1 -0
  302. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-4/N.json +1 -0
  303. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/A.json +1 -0
  304. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/B.json +1 -0
  305. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/C.json +1 -0
  306. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/D.json +1 -0
  307. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/E.json +1 -0
  308. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/F.json +1 -0
  309. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/G.json +1 -0
  310. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/H.json +1 -0
  311. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/I.json +1 -0
  312. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/J.json +1 -0
  313. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/6-5/M.json +1 -0
  314. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-1/B.json +1 -0
  315. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-1/C.json +1 -0
  316. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-1/D.json +1 -0
  317. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-1/F.json +1 -0
  318. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-1/G.json +1 -0
  319. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-1/H.json +1 -0
  320. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-1/K.json +1 -0
  321. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/B.json +1 -0
  322. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/C.json +1 -0
  323. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/E.json +1 -0
  324. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/G.json +1 -0
  325. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/H.json +1 -0
  326. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/I.json +1 -0
  327. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/J.json +1 -0
  328. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-2/M.json +1 -0
  329. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/B.json +1 -0
  330. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/C.json +1 -0
  331. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/D.json +1 -0
  332. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/E.json +1 -0
  333. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/J.json +1 -0
  334. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/M.json +1 -0
  335. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/N.json +1 -0
  336. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-3/P.json +1 -0
  337. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/B.json +1 -0
  338. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/C.json +1 -0
  339. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/D.json +1 -0
  340. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/E.json +1 -0
  341. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/G.json +1 -0
  342. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/H.json +1 -0
  343. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/J.json +1 -0
  344. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/K.json +1 -0
  345. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/L.json +1 -0
  346. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/M.json +1 -0
  347. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-4/P.json +1 -0
  348. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/A.json +1 -0
  349. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/B.json +1 -0
  350. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/C.json +1 -0
  351. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/D.json +1 -0
  352. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/E.json +1 -0
  353. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/G.json +1 -0
  354. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/J.json +1 -0
  355. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/K.json +1 -0
  356. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/L.json +1 -0
  357. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/M.json +1 -0
  358. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/N.json +1 -0
  359. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/Q.json +1 -0
  360. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/R.json +1 -0
  361. package/data/sources/kcnav/enemies/2026-08-11_2026-09-11/7-5/T.json +1 -0
  362. package/data/sources/kcnav/enemies/manifest.json +2120 -0
  363. package/data/sources/kcnav/maps/1-1.json +175 -0
  364. package/data/sources/kcnav/maps/1-2.json +277 -0
  365. package/data/sources/kcnav/maps/1-3.json +517 -0
  366. package/data/sources/kcnav/maps/1-4.json +652 -0
  367. package/data/sources/kcnav/maps/1-5.json +553 -0
  368. package/data/sources/kcnav/maps/1-6.json +668 -0
  369. package/data/sources/kcnav/maps/2-1.json +451 -0
  370. package/data/sources/kcnav/maps/2-2.json +557 -0
  371. package/data/sources/kcnav/maps/2-3.json +757 -0
  372. package/data/sources/kcnav/maps/2-4.json +817 -0
  373. package/data/sources/kcnav/maps/2-5.json +779 -0
  374. package/data/sources/kcnav/maps/3-1.json +379 -0
  375. package/data/sources/kcnav/maps/3-2.json +588 -0
  376. package/data/sources/kcnav/maps/3-3.json +654 -0
  377. package/data/sources/kcnav/maps/3-4.json +910 -0
  378. package/data/sources/kcnav/maps/3-5.json +643 -0
  379. package/data/sources/kcnav/maps/4-1.json +561 -0
  380. package/data/sources/kcnav/maps/4-2.json +664 -0
  381. package/data/sources/kcnav/maps/4-3.json +867 -0
  382. package/data/sources/kcnav/maps/4-4.json +690 -0
  383. package/data/sources/kcnav/maps/4-5.json +1232 -0
  384. package/data/sources/kcnav/maps/5-1.json +517 -0
  385. package/data/sources/kcnav/maps/5-2.json +791 -0
  386. package/data/sources/kcnav/maps/5-3.json +889 -0
  387. package/data/sources/kcnav/maps/5-4.json +851 -0
  388. package/data/sources/kcnav/maps/5-5.json +1070 -0
  389. package/data/sources/kcnav/maps/5-6.json +3200 -0
  390. package/data/sources/kcnav/maps/6-1.json +527 -0
  391. package/data/sources/kcnav/maps/6-2.json +717 -0
  392. package/data/sources/kcnav/maps/6-3.json +501 -0
  393. package/data/sources/kcnav/maps/6-4.json +870 -0
  394. package/data/sources/kcnav/maps/6-5.json +814 -0
  395. package/data/sources/kcnav/maps/7-1.json +640 -0
  396. package/data/sources/kcnav/maps/7-2.json +976 -0
  397. package/data/sources/kcnav/maps/7-3.json +1529 -0
  398. package/data/sources/kcnav/maps/7-4.json +1000 -0
  399. package/data/sources/kcnav/maps/7-5.json +1570 -0
  400. package/data/sources/kcnav/maps/manifest.json +340 -0
  401. package/data/sources/kcwiki/routes/1-1.wiki +2 -0
  402. package/data/sources/kcwiki/routes/1-2.wiki +1 -0
  403. package/data/sources/kcwiki/routes/1-3.wiki +1 -0
  404. package/data/sources/kcwiki/routes/1-4.wiki +1 -0
  405. package/data/sources/kcwiki/routes/1-5.wiki +1 -0
  406. package/data/sources/kcwiki/routes/1-6.wiki +1 -0
  407. package/data/sources/kcwiki/routes/2-1.wiki +1 -0
  408. package/data/sources/kcwiki/routes/2-2.wiki +1 -0
  409. package/data/sources/kcwiki/routes/2-3.wiki +1 -0
  410. package/data/sources/kcwiki/routes/2-4.wiki +81 -0
  411. package/data/sources/kcwiki/routes/2-5.wiki +1 -0
  412. package/data/sources/kcwiki/routes/3-1.wiki +1 -0
  413. package/data/sources/kcwiki/routes/3-2.wiki +1 -0
  414. package/data/sources/kcwiki/routes/3-3.wiki +1 -0
  415. package/data/sources/kcwiki/routes/3-4.wiki +1 -0
  416. package/data/sources/kcwiki/routes/3-5.wiki +1 -0
  417. package/data/sources/kcwiki/routes/4-1.wiki +1 -0
  418. package/data/sources/kcwiki/routes/4-2.wiki +1 -0
  419. package/data/sources/kcwiki/routes/4-3.wiki +1 -0
  420. package/data/sources/kcwiki/routes/4-4.wiki +1 -0
  421. package/data/sources/kcwiki/routes/4-5.wiki +1 -0
  422. package/data/sources/kcwiki/routes/5-1.wiki +57 -0
  423. package/data/sources/kcwiki/routes/5-2.wiki +1 -0
  424. package/data/sources/kcwiki/routes/5-3.wiki +1 -0
  425. package/data/sources/kcwiki/routes/5-4.wiki +1 -0
  426. package/data/sources/kcwiki/routes/5-5.wiki +80 -0
  427. package/data/sources/kcwiki/routes/5-6.wiki +753 -0
  428. package/data/sources/kcwiki/routes/6-1.wiki +1 -0
  429. package/data/sources/kcwiki/routes/6-2.wiki +1 -0
  430. package/data/sources/kcwiki/routes/6-3.wiki +1 -0
  431. package/data/sources/kcwiki/routes/6-4.wiki +1 -0
  432. package/data/sources/kcwiki/routes/6-5.wiki +1 -0
  433. package/data/sources/kcwiki/routes/7-1.wiki +1 -0
  434. package/data/sources/kcwiki/routes/7-2.wiki +1 -0
  435. package/data/sources/kcwiki/routes/7-3.wiki +1 -0
  436. package/data/sources/kcwiki/routes/7-4.wiki +1 -0
  437. package/data/sources/kcwiki/routes/7-5.wiki +1 -0
  438. package/data/sources/kcwiki/routes/README.md +11 -0
  439. package/data/sources/kcwiki/routes/manifest.json +450 -0
  440. package/docs/research.md +445 -0
  441. package/docs/sources.md +85 -0
  442. package/enemies.js +56 -0
  443. package/exporters.js +182 -0
  444. package/index.js +1081 -0
  445. package/logic.js +603 -0
  446. package/overview.js +54 -0
  447. package/package.json +66 -0
  448. package/scripts/cache-kcnav-enemies.js +97 -0
  449. package/scripts/cache-kcnav-maps.js +109 -0
  450. package/scripts/cache-kcwiki-routes.js +96 -0
  451. package/scripts/compact-kcnav-enemies.js +53 -0
  452. package/scripts/compress-kcnav-maps.js +90 -0
package/logic.js ADDED
@@ -0,0 +1,603 @@
1
+ 'use strict'
2
+
3
+ const SHIP_CATEGORIES = {
4
+ DE: new Set([1]),
5
+ DD: new Set([2]),
6
+ CL: new Set([3]),
7
+ CLT: new Set([4]),
8
+ CT: new Set([21]),
9
+ CL_ALL: new Set([3, 4, 21]),
10
+ CA: new Set([5]),
11
+ CAV: new Set([6]),
12
+ CVL: new Set([7]),
13
+ BB: new Set([8, 9]),
14
+ BBV: new Set([10]),
15
+ BB_ALL: new Set([8, 9, 10]),
16
+ CV: new Set([11, 12, 18]),
17
+ CV_ALL: new Set([7, 11, 12, 18]),
18
+ SS: new Set([13, 14]),
19
+ AO: new Set([15]),
20
+ AV: new Set([16]),
21
+ LHA: new Set([17]),
22
+ AR: new Set([19]),
23
+ AS: new Set([20]),
24
+ CV_MAIN: new Set([11, 12, 18]),
25
+ }
26
+
27
+ const CATEGORY_LABELS = {
28
+ DE: '海防',
29
+ DD: '驱逐',
30
+ CL: '轻巡',
31
+ CLT: '雷巡',
32
+ CT: '练巡',
33
+ CL_ALL: 'CL系',
34
+ CA: '重巡',
35
+ CAV: '航巡',
36
+ CVL: '轻空母',
37
+ BB: '战舰(BB/FBB)',
38
+ BBV: '航战',
39
+ BB_ALL: 'BB系',
40
+ CV: '空母(CV/CVB)',
41
+ CV_ALL: 'CV系',
42
+ SS: '潜水系',
43
+ AO: '补给',
44
+ AV: '水母',
45
+ LHA: '扬陆',
46
+ AR: '工作舰',
47
+ AS: '潜母',
48
+ CV_MAIN: '正规/装甲空母',
49
+ }
50
+
51
+ const EQUIPMENT_CATEGORY_LABELS = {
52
+ drum: '运输桶',
53
+ radar: '电探',
54
+ daihatsu: '大发系',
55
+ }
56
+
57
+ function number(value) {
58
+ const parsed = Number(value)
59
+ return Number.isFinite(parsed) ? parsed : 0
60
+ }
61
+
62
+ function integer(value) {
63
+ const parsed = Number(value)
64
+ return Number.isInteger(parsed) ? parsed : null
65
+ }
66
+
67
+ function normalizeMapId(value) {
68
+ if (typeof value === 'string') {
69
+ const match = value.trim().match(/^(\d+)[-](\d+)$/)
70
+ if (match) return `${number(match[1])}-${number(match[2])}`
71
+ const compact = value.trim().match(/^(\d+)(\d)$/)
72
+ if (compact) return `${number(compact[1])}-${number(compact[2])}`
73
+ }
74
+ const parsed = integer(value)
75
+ if (parsed != null && parsed >= 11) return `${Math.floor(parsed / 10)}-${parsed % 10}`
76
+ return null
77
+ }
78
+
79
+ function compare(left, operator, right) {
80
+ switch (operator) {
81
+ case '==':
82
+ return left === right
83
+ case '!=':
84
+ return left !== right
85
+ case '>':
86
+ return left > right
87
+ case '>=':
88
+ return left >= right
89
+ case '<':
90
+ return left < right
91
+ case '<=':
92
+ return left <= right
93
+ default:
94
+ return false
95
+ }
96
+ }
97
+
98
+ function statusResult(status, predicate, label) {
99
+ return {
100
+ status,
101
+ predicate,
102
+ label: label || predicate?.label || predicateLabel(predicate),
103
+ }
104
+ }
105
+
106
+ function categoryLabel(category) {
107
+ const value = String(category || '舰队')
108
+ if (value.endsWith('_ALL')) return `${value.slice(0, -4)}系`
109
+ return CATEGORY_LABELS[value] || value
110
+ }
111
+
112
+ function predicateLabel(predicate) {
113
+ if (!predicate || typeof predicate !== 'object') return '未知条件'
114
+ if (predicate.label) return predicate.label
115
+ if (predicate.kind === 'always') return '始终适用'
116
+ if (predicate.kind === 'count') {
117
+ const category = Array.isArray(predicate.category)
118
+ ? predicate.category.map(categoryLabel).join('+')
119
+ : categoryLabel(predicate.category)
120
+ return `${category} ${predicate.op || '=='} ${predicate.value}`
121
+ }
122
+ if (predicate.kind === 'equipmentShips') {
123
+ return `${EQUIPMENT_CATEGORY_LABELS[predicate.equipment] || predicate.equipment}舰数 ${predicate.op || '=='} ${predicate.value}`
124
+ }
125
+ if (predicate.kind === 'los') return `索敌 ${predicate.op || '=='} ${predicate.value}`
126
+ if (predicate.kind === 'losRange') return `索敌 ${predicate.min}~${predicate.max}`
127
+ if (predicate.kind === 'speed') {
128
+ if (predicate.mode === 'hasLow') return '含低速舰'
129
+ if (predicate.mode === 'highPlus') return '高速+舰队'
130
+ if (predicate.mode === 'fastest') return '最速舰队'
131
+ return '全高速'
132
+ }
133
+ if (predicate.kind === 'onlyCategories') {
134
+ return `仅含 ${(predicate.categories || []).map(categoryLabel).join('、')}`
135
+ }
136
+ if (predicate.kind === 'visited') return `已经过 ${predicate.node} 点`
137
+ if (predicate.kind === 'containsMaster') return `包含指定舰娘 (${(predicate.masterIds || []).join('/')})`
138
+ if (predicate.kind === 'containsName') return `包含 ${(predicate.names || []).join('/')}`
139
+ if (predicate.kind === 'nameCount') return `${(predicate.names || []).join('/')} ${predicate.op || '=='} ${predicate.value}`
140
+ if (predicate.kind === 'flag') return `状态:${predicate.name || predicate.flag}`
141
+ if (predicate.kind === 'phase') return `阶段:${predicate.phase || predicate.value}`
142
+ if (predicate.kind === 'equipmentShipsTotal') {
143
+ return `${(predicate.equipment || []).map((equipment) => EQUIPMENT_CATEGORY_LABELS[equipment] || equipment).join('+')} 舰数 ${predicate.op || '=='} ${predicate.value}`
144
+ }
145
+ if (predicate.kind === 'all') return (predicate.predicates || []).map(predicateLabel).join(' 且 ')
146
+ if (predicate.kind === 'any') return (predicate.predicates || []).map(predicateLabel).join(' 或 ')
147
+ return predicate.kind || '未知条件'
148
+ }
149
+
150
+ function categoryCount(context, category) {
151
+ const categories = Array.isArray(category) ? category : [category]
152
+ if (categories.includes('__all__')) return number(context.shipCount)
153
+ return categories.reduce((total, value) => total + number(context.counts?.[value]), 0)
154
+ }
155
+
156
+ function evaluatePredicate(predicate, context = {}) {
157
+ if (!predicate || typeof predicate !== 'object') return statusResult('unknown', predicate, '未知条件')
158
+
159
+ switch (predicate.kind) {
160
+ case 'always':
161
+ return statusResult('true', predicate)
162
+ case 'count': {
163
+ const actual = predicate.field === 'ships'
164
+ ? number(context.shipCount)
165
+ : categoryCount(context, predicate.category)
166
+ if (context.complete === false) return statusResult('unknown', predicate)
167
+ return statusResult(compare(actual, predicate.op || '==', number(predicate.value)) ? 'true' : 'false', predicate)
168
+ }
169
+ case 'equipmentShips': {
170
+ const actual = number(context.equipmentShips?.[predicate.equipment])
171
+ if (context.complete === false) return statusResult('unknown', predicate)
172
+ return statusResult(compare(actual, predicate.op || '==', number(predicate.value)) ? 'true' : 'false', predicate)
173
+ }
174
+ case 'speed': {
175
+ if (context.complete === false) return statusResult('unknown', predicate)
176
+ if (predicate.mode === 'highPlus' || predicate.mode === 'fastest') {
177
+ const enhancedSpeedClass = context.enhancedSpeedClass || 'unknown'
178
+ if (enhancedSpeedClass === 'unknown') return statusResult('unknown', predicate)
179
+ const matches = predicate.mode === 'fastest'
180
+ ? enhancedSpeedClass === 'fastest'
181
+ : ['highPlus', 'fastest'].includes(enhancedSpeedClass)
182
+ return statusResult(matches ? 'true' : 'false', predicate)
183
+ }
184
+ if (!context.speedClass || context.speedClass === 'unknown') return statusResult('unknown', predicate)
185
+ const matches = predicate.mode === 'hasLow'
186
+ ? context.speedClass === 'low'
187
+ : context.speedClass === 'high'
188
+ return statusResult(matches ? 'true' : 'false', predicate)
189
+ }
190
+ case 'los': {
191
+ if (context.losScore == null || !Number.isFinite(Number(context.losScore))) return statusResult('unknown', predicate)
192
+ return statusResult(compare(Number(context.losScore), predicate.op || '==', number(predicate.value)) ? 'true' : 'false', predicate)
193
+ }
194
+ case 'losRange': {
195
+ if (context.losScore == null || !Number.isFinite(Number(context.losScore))) return statusResult('unknown', predicate)
196
+ const score = Number(context.losScore)
197
+ const min = number(predicate.min)
198
+ const max = number(predicate.max)
199
+ return statusResult(score >= min && score < max ? 'true' : 'false', predicate)
200
+ }
201
+ case 'flagshipCategory': {
202
+ if (!context.flagship || context.flagship.categoryIds == null) return statusResult('unknown', predicate)
203
+ const matches = (Array.isArray(predicate.categories) ? predicate.categories : [predicate.category])
204
+ .some((category) => context.flagship.categoryIds.includes(category))
205
+ return statusResult(matches ? 'true' : 'false', predicate)
206
+ }
207
+ case 'onlyCategories': {
208
+ const categories = Array.isArray(predicate.categories) ? predicate.categories : []
209
+ if (context.complete === false || !context.ships?.length || !categories.length) {
210
+ return statusResult('unknown', predicate)
211
+ }
212
+ const matches = context.ships.every((ship) =>
213
+ categories.some((category) => ship.categoryIds?.includes(category)),
214
+ )
215
+ return statusResult(matches ? 'true' : 'false', predicate)
216
+ }
217
+ case 'visited': {
218
+ if (!Array.isArray(context.passedNodes)) return statusResult('unknown', predicate)
219
+ return statusResult(context.passedNodes.includes(predicate.node) ? 'true' : 'false', predicate)
220
+ }
221
+ case 'containsMaster': {
222
+ if (context.complete === false || !Array.isArray(context.ships)) return statusResult('unknown', predicate)
223
+ const masterIds = (predicate.masterIds || []).map((masterId) => number(masterId))
224
+ const matches = context.ships.some((ship) => masterIds.includes(number(ship.masterId)))
225
+ return statusResult(matches ? 'true' : 'false', predicate)
226
+ }
227
+ case 'containsName': {
228
+ if (context.complete === false || !Array.isArray(context.ships)) return statusResult('unknown', predicate)
229
+ const names = (predicate.names || []).map((name) => String(name))
230
+ const matches = context.ships.some((ship) => names.some((name) => String(ship.name || '').includes(name)))
231
+ return statusResult(matches ? 'true' : 'false', predicate)
232
+ }
233
+ case 'nameCount': {
234
+ if (context.complete === false || !Array.isArray(context.ships)) return statusResult('unknown', predicate)
235
+ const names = (predicate.names || []).map((name) => String(name))
236
+ const actual = context.ships.filter((ship) => names.some((name) => String(ship.name || '').includes(name))).length
237
+ return statusResult(compare(actual, predicate.op || '==', number(predicate.value)) ? 'true' : 'false', predicate)
238
+ }
239
+ case 'flag': {
240
+ if (!context.flags || typeof context.flags[predicate.flag] !== 'boolean') return statusResult('unknown', predicate)
241
+ return statusResult(context.flags[predicate.flag] === Boolean(predicate.value) ? 'true' : 'false', predicate)
242
+ }
243
+ case 'phase': {
244
+ if (typeof context.phase !== 'string' || !context.phase) return statusResult('unknown', predicate)
245
+ return statusResult(context.phase === String(predicate.phase || predicate.value) ? 'true' : 'false', predicate)
246
+ }
247
+ case 'equipmentShipsTotal': {
248
+ if (context.complete === false) return statusResult('unknown', predicate)
249
+ const actual = (predicate.equipment || []).reduce((total, equipment) => total + number(context.equipmentShips?.[equipment]), 0)
250
+ return statusResult(compare(actual, predicate.op || '==', number(predicate.value)) ? 'true' : 'false', predicate)
251
+ }
252
+ case 'all': {
253
+ const results = (predicate.predicates || []).map((child) => evaluatePredicate(child, context))
254
+ if (results.some((result) => result.status === 'false')) return statusResult('false', predicate)
255
+ if (results.some((result) => result.status === 'unknown')) return statusResult('unknown', predicate)
256
+ return statusResult('true', predicate)
257
+ }
258
+ case 'any': {
259
+ const results = (predicate.predicates || []).map((child) => evaluatePredicate(child, context))
260
+ if (results.some((result) => result.status === 'true')) return statusResult('true', predicate)
261
+ if (results.some((result) => result.status === 'unknown')) return statusResult('unknown', predicate)
262
+ return statusResult('false', predicate)
263
+ }
264
+ case 'not': {
265
+ const result = evaluatePredicate(predicate.predicate, context)
266
+ if (result.status === 'unknown') return statusResult('unknown', predicate)
267
+ return statusResult(result.status === 'true' ? 'false' : 'true', predicate)
268
+ }
269
+ default:
270
+ return statusResult('unknown', predicate)
271
+ }
272
+ }
273
+
274
+ function rulesForNode(mapDefinition, node) {
275
+ return (mapDefinition?.rules || [])
276
+ .filter((rule) => rule.node === node)
277
+ .sort((left, right) => number(left.priority) - number(right.priority))
278
+ }
279
+
280
+ function containsPhasePredicate(predicate) {
281
+ if (!predicate || typeof predicate !== 'object') return false
282
+ if (predicate.kind === 'phase') return true
283
+ if (Array.isArray(predicate.predicates) && predicate.predicates.some(containsPhasePredicate)) return true
284
+ return containsPhasePredicate(predicate.predicate)
285
+ }
286
+
287
+ function hasPhaseRules(mapDefinition, node) {
288
+ return rulesForNode(mapDefinition, node).some((rule) => containsPhasePredicate(rule.predicate))
289
+ }
290
+
291
+ function selectRouteRule(mapDefinition, node, context) {
292
+ const rules = rulesForNode(mapDefinition, node)
293
+ for (const rule of rules) {
294
+ const predicate = evaluatePredicate(rule.predicate, context)
295
+ if (predicate.status === 'true') {
296
+ const status = rule.terminal === true
297
+ ? 'terminal'
298
+ : rule.outcomes?.length ? 'matched' : 'unknown'
299
+ return { status, rule, predicate }
300
+ }
301
+ if (predicate.status === 'unknown') return { status: 'unknown', rule, predicate }
302
+ }
303
+ return { status: rules.length ? 'unknown' : 'terminal', rule: null, predicate: null }
304
+ }
305
+
306
+ function normalizeOutcomes(outcomes, rule = null, options = {}) {
307
+ const normalized = (Array.isArray(outcomes) ? outcomes : [])
308
+ .filter((outcome) => outcome && typeof outcome.to === 'string')
309
+ .map((outcome) => ({
310
+ to: outcome.to,
311
+ probability: outcome.probability == null
312
+ ? null
313
+ : Number.isFinite(Number(outcome.probability)) ? Number(outcome.probability) : null,
314
+ }))
315
+
316
+ const hasMissingProbability = normalized.some((outcome) => outcome.probability == null)
317
+ const equalize = options.equalizeUnknown !== false && (hasMissingProbability || rule?.confidence === 'unknown')
318
+ const estimated = equalize || rule?.confidence === 'approximate' && normalized.length > 1
319
+ return normalized.map((outcome) => {
320
+ const normalizedOutcome = {
321
+ to: outcome.to,
322
+ probability: equalize && normalized.length ? 1 / normalized.length : outcome.probability,
323
+ }
324
+ if (estimated) normalizedOutcome.estimated = true
325
+ return normalizedOutcome
326
+ })
327
+ }
328
+
329
+ function routeDecision(mapDefinition, node, context, overrides = {}) {
330
+ const selected = selectRouteRule(mapDefinition, node, context)
331
+ const configuredManualOutcomes = mapDefinition?.manualOutcomes?.[node]
332
+ const manual = selected.rule?.manual === true
333
+ || Array.isArray(mapDefinition?.manualNodes) && mapDefinition.manualNodes.includes(node)
334
+ const manualOutcomes = Array.isArray(configuredManualOutcomes)
335
+ ? normalizeOutcomes(configuredManualOutcomes, { confidence: 'unknown' }, { equalizeUnknown: false })
336
+ : null
337
+ const autoOutcomes = selected.status === 'matched'
338
+ ? normalizeOutcomes(selected.rule?.outcomes, selected.rule)
339
+ : []
340
+ const phaseRuleMatched = manual && hasPhaseRules(mapDefinition, node) && selected.status === 'matched'
341
+ const baseOutcomes = phaseRuleMatched
342
+ ? autoOutcomes
343
+ : manualOutcomes || autoOutcomes
344
+ const override = overrides[node]
345
+ const selectableOutcomes = manualOutcomes || baseOutcomes
346
+ const validOverride = manual && selectableOutcomes.some((outcome) => outcome.to === override)
347
+ const outcomes = validOverride
348
+ ? [{ to: override, probability: 1 }]
349
+ : baseOutcomes
350
+ return {
351
+ node,
352
+ status: selected.status,
353
+ rule: selected.rule,
354
+ predicate: selected.predicate,
355
+ baseOutcomes,
356
+ manualOutcomes,
357
+ outcomes,
358
+ manual,
359
+ phaseRuleMatched,
360
+ manualChoiceRequired: manual && !phaseRuleMatched && !validOverride,
361
+ manualOverride: validOverride ? override : null,
362
+ }
363
+ }
364
+
365
+ function edgeKey(from, to) {
366
+ return `${from}->${to}`
367
+ }
368
+
369
+ function propagateProbability(start, decisions) {
370
+ const reach = {}
371
+ const edgeMass = {}
372
+ const unknownNodes = new Set()
373
+ const unknownEdges = new Set()
374
+ const estimatedNodes = new Set()
375
+ const estimatedEdges = new Set()
376
+
377
+ function walk(node, mass, path, estimatedPath = false) {
378
+ if (!Number.isFinite(mass) || mass <= 0) return
379
+ reach[node] = number(reach[node]) + mass
380
+ if (estimatedPath) estimatedNodes.add(node)
381
+ if (path.has(node)) {
382
+ unknownNodes.add(node)
383
+ return
384
+ }
385
+ const decision = decisions[node]
386
+ if (!decision || decision.status === 'unknown' || decision.status === 'terminal' && !decision.outcomes.length) {
387
+ if (decision?.status === 'unknown') unknownNodes.add(node)
388
+ return
389
+ }
390
+ const nextPath = new Set(path)
391
+ nextPath.add(node)
392
+ decision.outcomes.forEach((outcome) => {
393
+ const key = edgeKey(node, outcome.to)
394
+ if (outcome.probability == null) {
395
+ unknownEdges.add(key)
396
+ return
397
+ }
398
+ const edgeEstimated = estimatedPath || outcome.estimated === true
399
+ if (edgeEstimated) estimatedEdges.add(key)
400
+ edgeMass[key] = number(edgeMass[key]) + mass * outcome.probability
401
+ walk(outcome.to, mass * outcome.probability, nextPath, edgeEstimated)
402
+ })
403
+ }
404
+
405
+ walk(start, 1, new Set())
406
+ return {
407
+ reach,
408
+ edgeMass,
409
+ unknownNodes: Array.from(unknownNodes),
410
+ unknownEdges: Array.from(unknownEdges),
411
+ estimatedNodes: Array.from(estimatedNodes),
412
+ estimatedEdges: Array.from(estimatedEdges),
413
+ }
414
+ }
415
+
416
+ function evaluateMap(mapDefinition, context = {}, overrides = {}) {
417
+ const nodes = new Set([mapDefinition?.start || '1'])
418
+ ;(mapDefinition?.rules || []).forEach((rule) => nodes.add(rule.node))
419
+ const decisions = {}
420
+ nodes.forEach((node) => {
421
+ decisions[node] = routeDecision(mapDefinition, node, context, overrides)
422
+ decisions[node].outcomes.forEach((outcome) => nodes.add(outcome.to))
423
+ })
424
+ // A rule outcome can introduce another node; resolve its decision once as well.
425
+ Array.from(nodes).forEach((node) => {
426
+ if (!decisions[node]) decisions[node] = routeDecision(mapDefinition, node, context, overrides)
427
+ })
428
+ const probability = propagateProbability(mapDefinition?.start || '1', decisions)
429
+ return { decisions, probability }
430
+ }
431
+
432
+ function mapNodeLabel(mapId, cell, maps) {
433
+ const normalized = normalizeMapId(mapId)
434
+ const route = maps?.[normalized]?.route
435
+ const entry = route?.[number(cell)]
436
+ return Array.isArray(entry) && typeof entry[1] === 'string' ? entry[1] : ''
437
+ }
438
+
439
+ function shipCategoryIds(typeId) {
440
+ return Object.entries(SHIP_CATEGORIES)
441
+ .filter(([, ids]) => ids.has(number(typeId)))
442
+ .map(([category]) => category)
443
+ }
444
+
445
+ function equipmentKind(master) {
446
+ const name = String(master?.api_name || '')
447
+ if (name.includes('ドラム缶') || name.includes('运输桶')) return 'drum'
448
+ if ([12, 13].includes(number(master?.api_type?.[2])) || name.includes('電探') || name.includes('电探')) return 'radar'
449
+ const excludedDaihatsu = ['特大発動艇+戦車第11連隊', 'M4A1 DD', '装甲艇(AB艇)', '特大動艇+战车第11连队']
450
+ if (!excludedDaihatsu.some((item) => name.includes(item)) && (name.includes('大発') || name.includes('大发') || name.includes('内火艇'))) return 'daihatsu'
451
+ return null
452
+ }
453
+
454
+ function getFleet(state, deckId = 1) {
455
+ const fleets = state?.info?.fleets
456
+ if (Array.isArray(fleets)) return fleets[deckId - 1] || null
457
+ return fleets?.[deckId - 1] || fleets?.[deckId] || null
458
+ }
459
+
460
+ function fleetContextFromState(state, deckId = 1, options = {}) {
461
+ const fleet = getFleet(state, deckId)
462
+ const instanceShips = state?.info?.ships || {}
463
+ const masterShips = state?.const?.$ships || {}
464
+ const instanceEquips = state?.info?.equips || {}
465
+ const masterEquips = state?.const?.$equips || {}
466
+ const shipIds = Array.isArray(fleet?.api_ship) ? fleet.api_ship : []
467
+ const activeShipIds = shipIds.filter((id) => number(id) > 0)
468
+ const dataIssues = []
469
+ if (!fleet) dataIssues.push('舰队')
470
+ if (!Array.isArray(fleet?.api_ship)) dataIssues.push('舰队槽位')
471
+ if (!activeShipIds.length) dataIssues.push('舰娘')
472
+ const ships = shipIds
473
+ .filter((id) => number(id) > 0)
474
+ .map((instanceId, index) => {
475
+ const instance = instanceShips[instanceId]
476
+ const master = masterShips[instance?.api_ship_id]
477
+ if (!instance) {
478
+ dataIssues.push(`舰娘实例 #${instanceId}`)
479
+ return null
480
+ }
481
+ if (!master) {
482
+ dataIssues.push(`舰娘 master #${instance.api_ship_id}`)
483
+ return null
484
+ }
485
+ if (!Number.isFinite(Number(master.api_stype))) dataIssues.push(`舰种 #${instance.api_ship_id}`)
486
+ if (!Array.isArray(instance.api_slot)) dataIssues.push(`装备槽位 #${instanceId}`)
487
+ const equipmentIds = Array.isArray(instance.api_slot) ? instance.api_slot : []
488
+ const equipments = []
489
+ for (const id of equipmentIds.filter((value) => number(value) > 0)) {
490
+ const equipment = instanceEquips[id]
491
+ const masterEquipment = masterEquips[equipment?.api_slotitem_id]
492
+ if (!equipment || !masterEquipment) {
493
+ dataIssues.push(`装备 #${id}`)
494
+ continue
495
+ }
496
+ equipments.push(masterEquipment)
497
+ }
498
+ if (!Number.isFinite(Number(instance.api_soku))) dataIssues.push(`速力 #${instanceId}`)
499
+ const categoryIds = shipCategoryIds(master.api_stype)
500
+ return {
501
+ instanceId: number(instanceId),
502
+ masterId: number(instance.api_ship_id),
503
+ name: master.api_name || `#${instance.api_ship_id}`,
504
+ categoryIds,
505
+ typeId: number(master.api_stype),
506
+ speed: Number.isFinite(Number(instance.api_soku)) ? Number(instance.api_soku) : null,
507
+ baseLos: Number.isFinite(Number(master.api_sakuteki)) ? Number(master.api_sakuteki) : null,
508
+ equipments,
509
+ equipmentKinds: equipments.map(equipmentKind).filter(Boolean),
510
+ flagship: index === 0,
511
+ }
512
+ })
513
+ .filter(Boolean)
514
+
515
+ const fleetComplete = dataIssues.length === 0 && ships.length === activeShipIds.length
516
+ const counts = {}
517
+ Object.keys(SHIP_CATEGORIES).forEach((category) => {
518
+ counts[category] = ships.filter((ship) => ship.categoryIds.includes(category)).length
519
+ })
520
+ const speedKnown = fleetComplete && ships.length > 0 && ships.every((ship) => Number.isFinite(ship.speed))
521
+ const fleetSpeed = speedKnown ? Math.min(...ships.map((ship) => ship.speed)) : null
522
+ const speedClass = fleetSpeed == null ? 'unknown' : fleetSpeed < 10 ? 'low' : 'high'
523
+ const enhancedSpeedClass = fleetSpeed == null
524
+ ? 'unknown'
525
+ : fleetSpeed >= 20 ? 'fastest'
526
+ : fleetSpeed >= 15 ? 'highPlus'
527
+ : 'high'
528
+ const equipmentShips = {
529
+ drum: ships.filter((ship) => ship.equipmentKinds.includes('drum')).length,
530
+ radar: ships.filter((ship) => ship.equipmentKinds.includes('radar')).length,
531
+ daihatsu: ships.filter((ship) => ship.equipmentKinds.includes('daihatsu')).length,
532
+ }
533
+ const losCoefficients = [1, 2, 3, 4]
534
+ const poiLosResults = losCoefficients.map((coefficient) => {
535
+ if (typeof options.losCalculator !== 'function') return null
536
+ try {
537
+ return options.losCalculator(state, deckId, coefficient)
538
+ } catch (_) {
539
+ return null
540
+ }
541
+ })
542
+ const poiLosScores = poiLosResults.map((result) => {
543
+ const score = result && typeof result === 'object' ? result.total : result
544
+ return score != null && Number.isFinite(Number(score)) ? Number(score) : null
545
+ })
546
+ const requestedCoefficient = Number(options.losCoefficient)
547
+ const selectedCoefficient = losCoefficients.includes(requestedCoefficient) ? requestedCoefficient : 1
548
+ const selectedIndex = selectedCoefficient - 1
549
+ const poiLos = poiLosResults[selectedIndex]
550
+ const poiLosScore = poiLosScores[selectedIndex]
551
+ const hasPoiLos = fleetComplete && poiLosScores.every((score) => score != null)
552
+ if (!hasPoiLos) dataIssues.push('poi 33式索敌')
553
+ const dataAvailable = fleetComplete && hasPoiLos
554
+ return {
555
+ complete: dataAvailable,
556
+ fleetComplete,
557
+ dataAvailable,
558
+ dataIssues: Array.from(new Set(dataIssues)),
559
+ shipCount: ships.length,
560
+ ships,
561
+ flagship: ships[0] || null,
562
+ counts,
563
+ equipmentShips,
564
+ fleetSpeed,
565
+ speedClass,
566
+ enhancedSpeedClass,
567
+ phase: typeof options.phase === 'string' ? options.phase : null,
568
+ passedNodes: Array.isArray(options.passedNodes) ? options.passedNodes : null,
569
+ flags: options.flags || null,
570
+ losScore: hasPoiLos ? poiLosScore : null,
571
+ losScores: hasPoiLos ? poiLosScores : [null, null, null, null],
572
+ losCoefficient: selectedCoefficient,
573
+ admiralCoefficient: options.admiralCoefficient ?? null,
574
+ losApproximate: false,
575
+ losSource: hasPoiLos ? 'poi-33' : 'unavailable',
576
+ losDetails: hasPoiLos && typeof poiLos === 'object' ? poiLos : null,
577
+ }
578
+ }
579
+
580
+ function formatPercent(value, estimated = false) {
581
+ if (!Number.isFinite(Number(value))) return '未知'
582
+ const suffix = estimated ? '?' : ''
583
+ return `${(Number(value) * 100).toFixed(1).replace(/\.0$/, '')}%${suffix}`
584
+ }
585
+
586
+ module.exports = {
587
+ CATEGORY_LABELS,
588
+ SHIP_CATEGORIES,
589
+ categoryCount,
590
+ edgeKey,
591
+ evaluateMap,
592
+ evaluatePredicate,
593
+ fleetContextFromState,
594
+ formatPercent,
595
+ mapNodeLabel,
596
+ normalizeMapId,
597
+ normalizeOutcomes,
598
+ predicateLabel,
599
+ propagateProbability,
600
+ routeDecision,
601
+ selectRouteRule,
602
+ shipCategoryIds,
603
+ }
package/overview.js ADDED
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+
3
+ const { routeDecision } = require('./logic')
4
+ const { loadEnemies } = require('./enemies')
5
+ const types = require('./data/kcnav-node-types-by-map.json')
6
+ const catalog = require('./data/kcnav-node-types.json')
7
+
8
+ function losBounds(predicate) {
9
+ if (!predicate) return []
10
+ if (predicate.kind === 'los') return [predicate.value]
11
+ if (predicate.kind === 'losRange') return [predicate.min, predicate.max]
12
+ return [...(predicate.predicates || []).flatMap(losBounds), ...losBounds(predicate.predicate)].filter(Number.isFinite)
13
+ }
14
+
15
+ function mapOverview(mapId, definition, context, overrides = {}, loader = loadEnemies) {
16
+ const visited = new Set(), missing = new Set(), los = []
17
+ let partial = false, steps = 0, superiority = null, supremacy = null
18
+ function walk(node, path) {
19
+ if (++steps > 10000 || path.includes(node)) { partial = true; return }
20
+ visited.add(node)
21
+ const current = { ...context, passedNodes: path }
22
+ const decide = score => routeDecision(definition, node, { ...current, losScore: score }, overrides)
23
+ const signature = decision => JSON.stringify([decision.status, decision.manualChoiceRequired, decision.outcomes])
24
+ // Only count LOS boundaries that actually change this fleet's decision.
25
+ // Earlier fixed rules and inapplicable fleet/phase branches must not count.
26
+ const bounds = new Set((definition.rules || []).filter(rule => rule.node === node).flatMap(rule => losBounds(rule.predicate)))
27
+ for (const value of bounds) {
28
+ const decisions = [decide(value - 0.000001), decide(value), decide(value + 0.000001)]
29
+ if (new Set(decisions.map(signature)).size > 1) los.push({ node, value })
30
+ }
31
+ const decision = routeDecision(definition, node, current, overrides)
32
+ if (decision.manualChoiceRequired || decision.status === 'unknown') { partial = true; return }
33
+ for (const outcome of decision.outcomes) if (outcome.probability > 0) walk(outcome.to, [...path, node])
34
+ }
35
+ walk(definition.start, [])
36
+ for (const node of visited) {
37
+ if (!catalog[types[mapId]?.[node]]?.hasEnemies) continue
38
+ const data = loader(mapId, node)
39
+ const rows = data.status === 'ready' ? data.rows.filter(row => row.count > 0) : []
40
+ if (!rows.length) missing.add(node)
41
+ for (const row of rows) {
42
+ if (!row.thresholds || row.uncertain) missing.add(node)
43
+ if (row.thresholds) {
44
+ superiority = Math.max(superiority ?? 0, row.thresholds.superiority)
45
+ supremacy = Math.max(supremacy ?? 0, row.thresholds.supremacy)
46
+ }
47
+ }
48
+ }
49
+ const maxLos = los.length ? Math.max(...los.map(item => item.value)) : null
50
+ return { nodes: [...visited], partial, missing: [...missing], superiority, supremacy, maxLos,
51
+ losNodes: [...new Set(los.filter(item => item.value === maxLos).map(item => item.node))] }
52
+ }
53
+
54
+ module.exports = { mapOverview }