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/index.js ADDED
@@ -0,0 +1,1081 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+ const { pathToFileURL } = require('url')
5
+ const React = require('react')
6
+ const { store } = require('views/create-store')
7
+ const primaryMapCatalog = require('./data/maps.json')
8
+ const normalMapCatalog = require('./data/normal-maps.json')
9
+ const losCatalog = require('./data/los-config.json')
10
+ const kcnavNodeCatalog = require('./data/kcnav-node-types.json')
11
+ const localKcnavNodeTypes = require('./data/kcnav-node-types-by-map.json')
12
+ const { loadEnemies, formationLabel } = require('./enemies')
13
+ const { mapOverview } = require('./overview')
14
+ const { airStateRange } = require('./air-status')
15
+ const { enumerateRoutes, battleNodes, compositionKey, kc3Save, noroDeck, kc3Url, noroUrl } = require('./exporters')
16
+ const mapCatalog = {
17
+ ...normalMapCatalog,
18
+ ...primaryMapCatalog,
19
+ maps: { ...normalMapCatalog.maps, ...primaryMapCatalog.maps },
20
+ }
21
+ const mapIds = Object.keys(mapCatalog.maps).sort((left, right) =>
22
+ left.localeCompare(right, undefined, { numeric: true }),
23
+ )
24
+ const mapAreas = [
25
+ ['1', '镇守府'], ['2', '南西群岛'], ['3', '北方'], ['7', '南西'],
26
+ ['4', '西方'], ['5', '南方'], ['6', '中部'],
27
+ ]
28
+ const LOCAL_KCNAV_ASSET_ROOT = path.join(__dirname, 'assets', 'kcnav')
29
+
30
+ const MAP_PHASES = {
31
+ '5-6': [
32
+ { id: 'P1', label: 'P1', start: '1', boss: 'G' },
33
+ { id: 'P2', label: 'P2', start: '2', boss: 'N' },
34
+ { id: 'P3', label: 'P3', start: '2', boss: 'Z' },
35
+ ],
36
+ '7-2': [
37
+ { id: 'P1', label: 'P1', boss: 'G' },
38
+ { id: 'P2', label: 'P2', boss: 'M' },
39
+ ],
40
+ '7-3': [
41
+ { id: 'P1', label: 'P1', boss: 'E' },
42
+ { id: 'P2', label: 'P2', boss: 'P' },
43
+ ],
44
+ '7-5': [
45
+ { id: 'P1', label: 'P1', boss: 'K' },
46
+ { id: 'P2', label: 'P2', boss: 'Q' },
47
+ { id: 'P3', label: 'P3', boss: 'T' },
48
+ ],
49
+ }
50
+
51
+ function phaseOptionsForMap(mapId) {
52
+ return MAP_PHASES[mapId] || []
53
+ }
54
+
55
+ function phaseOptionForMap(mapId, phaseId) {
56
+ return phaseOptionsForMap(mapId).find((phase) => phase.id === phaseId) || phaseOptionsForMap(mapId)[0] || null
57
+ }
58
+
59
+ function phaseFlags(mapId, phaseId) {
60
+ const flags = {}
61
+ if (mapId === '7-3') flags.phase1 = phaseId === 'P1'
62
+ if (mapId === '5-6') flags.qRoutes = phaseId === 'P3'
63
+ return flags
64
+ }
65
+
66
+ const {
67
+ evaluateMap,
68
+ evaluatePredicate,
69
+ fleetContextFromState,
70
+ formatPercent,
71
+ mapNodeLabel,
72
+ normalizeMapId,
73
+ normalizeOutcomes,
74
+ predicateLabel,
75
+ } = require('./logic')
76
+
77
+ let poiGetSaku33 = null
78
+ let poiGetTyku = null
79
+ try {
80
+ ;({ getSaku33: poiGetSaku33, getTyku: poiGetTyku } = require('views/utils/game-utils'))
81
+ } catch (_) {
82
+ // The standalone test harness does not load poi's path aliases.
83
+ }
84
+
85
+ const h = React.createElement
86
+ const listeners = new Set()
87
+ const pluginState = {
88
+ currentMapId: null,
89
+ currentNode: null,
90
+ passedNodes: [],
91
+ }
92
+ function number(value) {
93
+ const parsed = Number(value)
94
+ return Number.isFinite(parsed) ? parsed : 0
95
+ }
96
+
97
+ const NODE_DEFAULTS = {
98
+ color: 'black',
99
+ strokeColor: '#eee9',
100
+ fontSize: 16,
101
+ stroke: 2,
102
+ textOffsetX: 0,
103
+ textOffsetY: 1.8,
104
+ offsetX: 0,
105
+ offsetY: 0,
106
+ offsetX2: 0,
107
+ offsetY2: 0,
108
+ simpleSize: 1.5,
109
+ width: 1.5,
110
+ height: 1.5,
111
+ opacity: 1,
112
+ opacity2: 1,
113
+ background: '#dddddd',
114
+ border: '#999999',
115
+ }
116
+
117
+ function nodeVisualStyle(nodeInfo = {}) {
118
+ const style = { ...NODE_DEFAULTS, ...nodeInfo }
119
+ const size = Number.isFinite(Number(style.size)) ? Number(style.size) : Number(style.simpleSize)
120
+ const width = Number.isFinite(Number(nodeInfo.width))
121
+ ? Number(nodeInfo.width)
122
+ : Number.isFinite(Number(nodeInfo.size)) ? size : Number(style.width)
123
+ const height = Number.isFinite(Number(nodeInfo.height))
124
+ ? Number(nodeInfo.height)
125
+ : Number.isFinite(Number(nodeInfo.size)) ? size : Number(style.height)
126
+ const diameter = ((width + height) / 2) * 10
127
+ return {
128
+ ...style,
129
+ rx: diameter,
130
+ ry: diameter,
131
+ // KCNav offsets position sprites, not the circles rendered here.
132
+ offsetX: 0,
133
+ offsetY: 0,
134
+ offsetX2: Number(style.offsetX2) || 0,
135
+ offsetY2: Number(style.offsetY2) || 0,
136
+ textOffsetX: Number(style.textOffsetX) || 0,
137
+ textOffsetY: Number(style.textOffsetY) || 0,
138
+ }
139
+ }
140
+
141
+ function getStore(pathName) {
142
+ if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
143
+ return window.getStore(pathName)
144
+ }
145
+ const root = store && typeof store.getState === 'function' ? store.getState() : null
146
+ if (!pathName) return root
147
+ return String(pathName)
148
+ .split('.')
149
+ .reduce((value, key) => (value == null ? undefined : value[key]), root)
150
+ }
151
+
152
+ function getRootState() {
153
+ if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
154
+ return window.getStore() || {}
155
+ }
156
+ return store && typeof store.getState === 'function' ? store.getState() : {}
157
+ }
158
+
159
+ function getFleet(state, deckId = 1) {
160
+ const fleets = state?.info?.fleets
161
+ if (Array.isArray(fleets)) return fleets[deckId - 1] || null
162
+ return fleets?.[deckId - 1] || fleets?.[deckId] || null
163
+ }
164
+
165
+ function escapedShipIds(state) {
166
+ const sortieStatus = state?.sortie?.sortieStatus
167
+ const escapedPos = state?.sortie?.escapedPos
168
+ const fleets = state?.info?.fleets
169
+ if (!Array.isArray(sortieStatus) || !Array.isArray(escapedPos) || !Array.isArray(fleets)) return new Set()
170
+ const sortieShipIds = []
171
+ sortieStatus.forEach((active, index) => {
172
+ if (!active || !Array.isArray(fleets[index]?.api_ship)) return
173
+ sortieShipIds.push(...fleets[index].api_ship)
174
+ })
175
+ return new Set(
176
+ escapedPos
177
+ .map((position) => sortieShipIds[number(position)])
178
+ .filter((shipId) => number(shipId) > 0)
179
+ .map((shipId) => number(shipId)),
180
+ )
181
+ }
182
+
183
+ function calculatePoiFleetStat(state, deckId = 1, mapModifier = 1, air = false) {
184
+ if (typeof (air ? poiGetTyku : poiGetSaku33) !== 'function') return null
185
+ const fleet = getFleet(state, deckId)
186
+ const escaped = escapedShipIds(state)
187
+ const shipIds = Array.isArray(fleet?.api_ship)
188
+ ? fleet.api_ship.filter((id) => number(id) > 0 && !escaped.has(number(id)))
189
+ : []
190
+ if (!fleet || !shipIds.length) return null
191
+
192
+ const ships = state?.info?.ships || {}
193
+ const masterShips = state?.const?.$ships || {}
194
+ const equips = state?.info?.equips || {}
195
+ const masterEquips = state?.const?.$equips || {}
196
+ const shipsData = []
197
+ const equipsData = []
198
+
199
+ for (const shipId of shipIds) {
200
+ const ship = ships[shipId]
201
+ const masterShip = masterShips[ship?.api_ship_id]
202
+ if (!ship || !masterShip) return null
203
+ shipsData.push([ship, masterShip])
204
+
205
+ if (!Array.isArray(ship.api_slot) || !Array.isArray(ship.api_onslot)) return null
206
+ if (!Number.isFinite(Number(ship.api_slotnum))) return null
207
+ const slotIds = [...ship.api_slot, ship.api_slot_ex ?? -1].map((id) => number(id))
208
+ const onslots = [...ship.api_onslot, 0]
209
+ const shipEquips = []
210
+ for (let index = 0; index < slotIds.length; index += 1) {
211
+ const equipId = slotIds[index]
212
+ if (equipId <= 0) {
213
+ shipEquips.push(undefined)
214
+ continue
215
+ }
216
+ const equip = equips[equipId]
217
+ const masterEquip = masterEquips[equip?.api_slotitem_id]
218
+ if (!equip || !masterEquip) return null
219
+ shipEquips.push([equip, masterEquip, onslots[index]])
220
+ }
221
+ const slotCount = Number(ship.api_slotnum)
222
+ equipsData.push(shipEquips.length > slotCount
223
+ ? [...shipEquips.slice(0, slotCount), shipEquips[shipEquips.length - 1]]
224
+ : shipEquips)
225
+ }
226
+
227
+ if (air) return poiGetTyku(equipsData)
228
+ const commanderLevel = state?.info?.basic?.api_level
229
+ if (!Number.isFinite(Number(commanderLevel))) return null
230
+ const slotCount = Array.isArray(fleet.api_ship) ? fleet.api_ship.length : 6
231
+ return poiGetSaku33(shipsData, equipsData, Number(commanderLevel), Number(mapModifier), slotCount)
232
+ }
233
+
234
+ function calculatePoiLos33(state, deckId = 1, mapModifier = 1) {
235
+ return calculatePoiFleetStat(state, deckId, mapModifier)
236
+ }
237
+
238
+ function notify() {
239
+ listeners.forEach((listener) => listener())
240
+ }
241
+
242
+ function kcnavNodeTypesFromMap(map) {
243
+ const nodeValues = {}
244
+ Object.keys(map?.spots || {}).forEach((node) => { nodeValues[node] = -1 })
245
+ const route = map?.route || {}
246
+ Object.keys(route).forEach((key) => {
247
+ const entry = route[key] || []
248
+ const startNode = entry[0]
249
+ const endNode = entry[1]
250
+ const nodeType = Number(entry[2])
251
+ const hasNodeType = Number.isFinite(nodeType)
252
+ const leading = Object.keys(route).filter((routeKey) => route[routeKey]?.[1] === endNode)
253
+ if (
254
+ (leading.length === 1 && startNode === null && map?.spots?.[endNode]?.[2] === 'Start')
255
+ || (hasNodeType && nodeType !== -1 && nodeType !== 0)
256
+ ) {
257
+ nodeValues[endNode] = Number.isFinite(nodeType) ? nodeType : -1
258
+ }
259
+ })
260
+ return nodeValues
261
+ }
262
+
263
+ function localKcnavAssetUrl(relativePath) {
264
+ return pathToFileURL(path.join(LOCAL_KCNAV_ASSET_ROOT, relativePath)).toString()
265
+ }
266
+
267
+ function nodeIconUrl(icon) {
268
+ if (icon == null) return null
269
+ const value = String(icon)
270
+ if (/^\d+$/.test(value)) {
271
+ const directory = value === '19' ? 'map_common' : 'map_main'
272
+ return localKcnavAssetUrl(`icons/${directory}/${value}.png`)
273
+ }
274
+ if (/^\w+\/\d+$/.test(value)) return localKcnavAssetUrl(`icons/${value}.png`)
275
+ return null
276
+ }
277
+
278
+ function localNodeTypesForMap(mapId, mapDefinition) {
279
+ const nodeTypes = { ...(localKcnavNodeTypes[mapId] || {}) }
280
+ if (mapDefinition?.start) nodeTypes[mapDefinition.start] = 0
281
+ if (mapDefinition?.boss && nodeTypes[mapDefinition.boss] == null) nodeTypes[mapDefinition.boss] = 5
282
+ ;(mapDefinition?.manualNodes || []).forEach((node) => {
283
+ if (nodeTypes[node] == null || nodeTypes[node] === -1) nodeTypes[node] = 91
284
+ })
285
+ return nodeTypes
286
+ }
287
+
288
+ function mapIdFromPair(area, mapNo) {
289
+ const normalizedArea = number(area)
290
+ const normalizedMapNo = number(mapNo)
291
+ return normalizedArea > 0 && normalizedMapNo > 0 ? `${normalizedArea}-${normalizedMapNo}` : null
292
+ }
293
+
294
+ function mapIdFromDetail(detail) {
295
+ const postBody = detail?.postBody || {}
296
+ const body = detail?.body || {}
297
+ return normalizeMapId(
298
+ postBody.api_map_id || body.api_map_id || mapIdFromPair(postBody.api_maparea_id, postBody.api_mapinfo_no) || mapIdFromPair(body.api_maparea_id, body.api_mapinfo_no),
299
+ )
300
+ }
301
+
302
+ function cellFromDetail(detail) {
303
+ const body = detail?.body || {}
304
+ const postBody = detail?.postBody || {}
305
+ const candidates = [body.api_no, body.api_cell_id, body.api_map_next, body.api_next, postBody.api_no]
306
+ const cell = candidates.find((value) => value != null && Number.isFinite(Number(value)) && Number(value) >= 0)
307
+ return cell == null ? null : number(cell)
308
+ }
309
+
310
+ function updateSortieState(detail) {
311
+ const mapId = mapIdFromDetail(detail) || pluginState.currentMapId
312
+ if (!mapId) return
313
+ if (pluginState.currentMapId !== mapId) pluginState.passedNodes = []
314
+ const maps = getStore('fcd.map') || {}
315
+ const cell = cellFromDetail(detail)
316
+ const node = cell == null ? null : mapNodeLabel(mapId, cell, maps)
317
+ pluginState.currentMapId = mapId
318
+ if (node) {
319
+ pluginState.currentNode = node
320
+ if (pluginState.passedNodes.at(-1) !== node) pluginState.passedNodes.push(node)
321
+ }
322
+ notify()
323
+ }
324
+
325
+ function handleGameResponse(event) {
326
+ const detail = event?.detail || {}
327
+ const pathName = detail.path || ''
328
+ if (pathName === '/kcsapi/api_req_map/start') {
329
+ pluginState.currentNode = null
330
+ pluginState.passedNodes = []
331
+ updateSortieState(detail)
332
+ } else if (pathName === '/kcsapi/api_req_map/next') {
333
+ updateSortieState(detail)
334
+ } else if (pathName === '/kcsapi/api_port/port') {
335
+ pluginState.currentNode = null
336
+ pluginState.passedNodes = []
337
+ // Redux subscription also catches the committed port payload. Defer this
338
+ // explicit refresh so a synchronous game.response handler cannot read old state.
339
+ queueMicrotask(notify)
340
+ }
341
+ }
342
+
343
+ function startPlugin() {
344
+ pluginState.currentMapId = normalizeMapId(
345
+ getStore('sortie.sortieMapId') || getStore('sortie.mapId') || getStore('info.sortieMapId'),
346
+ )
347
+ pluginState.passedNodes = []
348
+ if (typeof window !== 'undefined') window.addEventListener('game.response', handleGameResponse)
349
+ }
350
+
351
+ function stopPlugin() {
352
+ if (typeof window !== 'undefined') window.removeEventListener('game.response', handleGameResponse)
353
+ listeners.clear()
354
+ }
355
+
356
+ function mapGeometry(mapId) {
357
+ const maps = getStore('fcd.map') || {}
358
+ return maps[mapId] || null
359
+ }
360
+
361
+ function mapNodes(mapDefinition, geometry) {
362
+ const nodes = new Set([mapDefinition.start])
363
+ Object.keys(geometry?.spots || {}).forEach((node) => nodes.add(node))
364
+ ;(mapDefinition.rules || []).forEach((rule) => {
365
+ nodes.add(rule.node)
366
+ ;(rule.outcomes || []).forEach((outcome) => nodes.add(outcome.to))
367
+ })
368
+ ;(mapDefinition.manualNodes || []).forEach((node) => nodes.add(node))
369
+ Object.values(mapDefinition.manualOutcomes || {}).forEach((outcomes) => {
370
+ ;(outcomes || []).forEach((outcome) => nodes.add(outcome.to))
371
+ })
372
+ return Array.from(nodes)
373
+ }
374
+
375
+ function mapGeometryAvailable(mapDefinition, geometry) {
376
+ const spots = geometry?.spots || {}
377
+ const route = geometry?.route || {}
378
+ if (!Object.keys(spots).length || !Object.keys(route).length) return false
379
+ return mapNodes(mapDefinition, geometry).every((node) => {
380
+ const spot = spots[node]
381
+ return Array.isArray(spot) && Number.isFinite(Number(spot[0])) && Number.isFinite(Number(spot[1]))
382
+ })
383
+ }
384
+
385
+ function positionsForNodes(nodes, geometry) {
386
+ const spots = geometry?.spots || {}
387
+ const position = {}
388
+ nodes.forEach((node, index) => {
389
+ const spot = spots[node]
390
+ position[node] = Array.isArray(spot) && Number.isFinite(Number(spot[0]))
391
+ ? { x: number(spot[0]), y: number(spot[1]) }
392
+ : { x: 80 + (index % 5) * 170, y: 90 + Math.floor(index / 5) * 100 }
393
+ })
394
+ return position
395
+ }
396
+
397
+ function routeEdges(geometry) {
398
+ return Object.entries(geometry?.route || {})
399
+ .map(([cell, value]) => ({ cell: number(cell), from: value?.[0], to: value?.[1] }))
400
+ .filter((edge) => edge.from && edge.to)
401
+ }
402
+
403
+ function outcomeText(outcome) {
404
+ return `${outcome.to} ${formatPercent(outcome.probability, outcome.estimated)}`
405
+ }
406
+
407
+ function conditionIcon(status, active) {
408
+ if (active) return '✓'
409
+ if (status === 'true') return '•'
410
+ if (status === 'false') return '×'
411
+ return '?'
412
+ }
413
+
414
+ function conditionStatus(status) {
415
+ return {
416
+ true: '满足',
417
+ false: '不满足',
418
+ unknown: '无法判断',
419
+ }[status] || status
420
+ }
421
+
422
+ function ruleRows(mapDefinition, node, context) {
423
+ return (mapDefinition.rules || [])
424
+ .filter((rule) => rule.node === node)
425
+ .sort((left, right) => number(left.priority) - number(right.priority))
426
+ .map((rule) => ({
427
+ rule,
428
+ result: evaluatePredicate(rule.predicate, context),
429
+ outcomes: normalizeOutcomes(rule.outcomes, rule),
430
+ }))
431
+ }
432
+
433
+ function fleetSummary(context) {
434
+ const summaryCategories = ['BB', 'BBV', 'CV', 'CVL', 'CA', 'CAV', 'CL', 'CLT', 'CT', 'DD', 'DE', 'SS', 'AO', 'AV', 'LHA', 'AR', 'AS']
435
+ const entries = summaryCategories
436
+ .map((category) => [category, context.counts?.[category]])
437
+ .filter(([, count]) => count > 0)
438
+ .map(([category, count]) => `${count}${category}`)
439
+ return entries.length ? entries.join(' ') : '—'
440
+ }
441
+
442
+ function formatLosScore(value) {
443
+ if (!Number.isFinite(Number(value))) return '—'
444
+ return Number(value).toFixed(2).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
445
+ }
446
+
447
+ function edgeProbability(evaluation, from, to) {
448
+ const decision = evaluation.decisions[from]
449
+ const outcome = decision?.outcomes?.find((entry) => entry.to === to)
450
+ const baseOutcome = decision?.baseOutcomes?.find((entry) => entry.to === to)
451
+ || decision?.manualOutcomes?.find((entry) => entry.to === to)
452
+ const known = outcome || baseOutcome
453
+ const edgeMass = evaluation.probability.edgeMass[`${from}->${to}`]
454
+ return {
455
+ local: outcome?.probability ?? (known ? 0 : undefined),
456
+ global: Number.isFinite(Number(edgeMass)) ? Number(edgeMass) : known ? 0 : undefined,
457
+ estimated: known?.estimated === true || evaluation.probability.estimatedEdges.includes(`${from}->${to}`),
458
+ }
459
+ }
460
+
461
+ function probabilityTier(value) {
462
+ if (!Number.isFinite(Number(value))) return ''
463
+ if (Number(value) === 0) return 'is-probability-zero'
464
+ if (Number(value) >= 0.75) return 'is-probability-high'
465
+ if (Number(value) >= 0.4) return 'is-probability-medium'
466
+ return 'is-probability-low'
467
+ }
468
+
469
+ function mapBackgroundUrl(mapId) {
470
+ return localKcnavAssetUrl(`maps/${mapId}.webp`)
471
+ }
472
+
473
+ function passedEdgeKeys(passedNodes) {
474
+ const keys = new Set()
475
+ for (let index = 1; index < passedNodes.length; index += 1) {
476
+ keys.add(`${passedNodes[index - 1]}->${passedNodes[index]}`)
477
+ }
478
+ return keys
479
+ }
480
+
481
+ function manualSourceForTarget(evaluation, target) {
482
+ return Object.keys(evaluation.decisions).find((source) => {
483
+ const decision = evaluation.decisions[source]
484
+ const manualOutcomes = decision?.manualOutcomes || decision?.baseOutcomes || []
485
+ return decision?.manual && manualOutcomes.some((outcome) => outcome.to === target)
486
+ }) || null
487
+ }
488
+
489
+ class Compass extends React.Component {
490
+ constructor(props) {
491
+ super(props)
492
+ this.state = {
493
+ deckId: 1,
494
+ mapId: pluginState.currentMapId && mapCatalog.maps[pluginState.currentMapId]
495
+ ? pluginState.currentMapId
496
+ : mapIds[0],
497
+ phaseId: phaseOptionsForMap(mapIds[0])[0]?.id || null,
498
+ selectedNode: pluginState.currentNode || null,
499
+ overrides: {},
500
+ exportOpen: false,
501
+ exportRoute: null,
502
+ exportEnemies: {},
503
+ exportFormations: {},
504
+ exportError: null,
505
+ exportBusy: false,
506
+ }
507
+ this.refresh = () => this.forceUpdate()
508
+ }
509
+
510
+ componentDidMount() {
511
+ listeners.add(this.refresh)
512
+ const snapshot = () => {
513
+ const root = getRootState()
514
+ return [root.info?.fleets, root.info?.ships, root.info?.equips, root.info?.basic, root.info?.server, root.const, root.sortie, root.fcd]
515
+ }
516
+ let previous = snapshot()
517
+ this.unsubscribeStore = store?.subscribe?.(() => {
518
+ const next = snapshot()
519
+ if (next.some((value, index) => value !== previous[index])) {
520
+ previous = next
521
+ this.refresh()
522
+ }
523
+ })
524
+ this.refresh()
525
+ }
526
+
527
+ componentWillUnmount() {
528
+ listeners.delete(this.refresh)
529
+ this.unsubscribeStore?.()
530
+ this.unsubscribeStore = null
531
+ }
532
+
533
+ selectMap(mapId) {
534
+ this.setState({
535
+ mapId,
536
+ phaseId: phaseOptionsForMap(mapId)[0]?.id || null,
537
+ selectedNode: null,
538
+ overrides: {},
539
+ exportRoute: null,
540
+ exportEnemies: {},
541
+ exportFormations: {},
542
+ exportError: null,
543
+ })
544
+ }
545
+
546
+ renderMapSelector() {
547
+ return h('details', {
548
+ className: 'compass-map-select',
549
+ onBlur: (event) => {
550
+ if (!event.currentTarget.contains(event.relatedTarget)) event.currentTarget.open = false
551
+ },
552
+ onKeyDown: (event) => {
553
+ if (event.key === 'Escape') {
554
+ event.currentTarget.open = false
555
+ event.currentTarget.querySelector('summary').focus()
556
+ }
557
+ },
558
+ },
559
+ h('summary', { 'aria-label': '选择海域', title: mapCatalog.maps[this.state.mapId].name }, this.state.mapId),
560
+ h('div', { className: 'compass-map-matrix', 'aria-label': '通常海域' },
561
+ ...mapAreas.map(([area, name]) => h('div', {
562
+ key: area, className: 'compass-map-matrix-row', role: 'group', 'aria-label': `${name}海域`,
563
+ },
564
+ h('span', { className: 'compass-map-area' }, name),
565
+ ...mapIds.filter((id) => id.startsWith(`${area}-`)).map((id) => h('button', {
566
+ key: id,
567
+ type: 'button',
568
+ style: { gridColumn: Number(id.split('-')[1]) + 1 },
569
+ className: this.state.mapId === id ? 'is-active' : '',
570
+ 'aria-pressed': this.state.mapId === id,
571
+ 'aria-label': `${id} ${mapCatalog.maps[id].name}`,
572
+ title: mapCatalog.maps[id].name,
573
+ onClick: (event) => {
574
+ this.selectMap(id)
575
+ const picker = event.currentTarget.closest('details')
576
+ picker.open = false
577
+ picker.querySelector('summary').focus()
578
+ },
579
+ }, id)),
580
+ )),
581
+ ))
582
+ }
583
+
584
+ selectPhase(phaseId) {
585
+ this.setState({ phaseId, selectedNode: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
586
+ }
587
+
588
+ selectNode(node) {
589
+ this.setState({ selectedNode: node })
590
+ }
591
+
592
+ selectFleet(deckId) {
593
+ this.setState({ deckId, selectedNode: null, overrides: {}, exportRoute: null, exportEnemies: {}, exportFormations: {}, exportError: null })
594
+ }
595
+
596
+ renderLosIcons(scores) {
597
+ const { SlotitemIcon } = require('views/components/etc/icon')
598
+ return h('span', { className: 'compass-los-icons', 'aria-label': '33式索敌' },
599
+ ...scores.map((score, index) => {
600
+ const label = `33式索敌 · 系数 ${index + 1}:${formatLosScore(score)}`
601
+ return h('span', { key: index, className: 'compass-los-item', title: label, 'aria-label': label },
602
+ h('span', { className: 'compass-los-icon' }, h(SlotitemIcon, { slotitemId: 11 }),
603
+ h('span', { className: 'compass-los-coefficient', 'aria-hidden': true }, index + 1)),
604
+ h('span', { className: 'compass-los-value' }, formatLosScore(score)))
605
+ }))
606
+ }
607
+
608
+ renderFleetAir(air) {
609
+ if (!air || !Number.isFinite(air.min) || !Number.isFinite(air.max) || air.max <= 0) return null
610
+ return h('span', { className: 'compass-fleet-air', title: 'poi 当前舰队制空(熟练度范围)' },
611
+ `制空 ${air.min === air.max ? air.min : `${air.min}~${air.max}`}`)
612
+ }
613
+
614
+ setManualOverride(node, to) {
615
+ this.setState((state) => ({
616
+ overrides: { ...state.overrides, [node]: to },
617
+ selectedNode: node,
618
+ exportRoute: null,
619
+ exportError: null,
620
+ }))
621
+ }
622
+
623
+ renderMap(mapDefinition, geometry, evaluation, selectedNode, currentNode, passedNodes = [], mapId = null) {
624
+ if (!mapGeometryAvailable(mapDefinition, geometry)) {
625
+ return h('div', { className: 'compass-empty-panel' }, 'poi 地图数据不可用')
626
+ }
627
+ const nodes = mapNodes(mapDefinition, geometry)
628
+ const positions = positionsForNodes(nodes, geometry)
629
+ const edges = routeEdges(geometry)
630
+ const passedEdges = passedEdgeKeys(passedNodes)
631
+ const nodeTypes = localNodeTypesForMap(mapId, mapDefinition)
632
+ const airPower = calculatePoiFleetStat(getRootState(), this.state.deckId, 1, true)
633
+ const width = Math.max(1200, ...Object.values(positions).map((position) => position.x + 100))
634
+ const height = Math.max(720, ...Object.values(positions).map((position) => position.y + 100))
635
+ return h(
636
+ 'svg',
637
+ { className: 'compass-map', viewBox: `0 0 ${width} ${height}`, role: 'img', 'aria-label': `${mapDefinition.name} 路线图`,
638
+ onClick: event => { if (!event.target.closest('.compass-node, .compass-edge')) this.selectNode(null) } },
639
+ mapId
640
+ ? h('image', {
641
+ className: 'compass-map-background',
642
+ href: mapBackgroundUrl(mapId),
643
+ x: 0,
644
+ y: 0,
645
+ width: 1200,
646
+ height: 720,
647
+ preserveAspectRatio: 'none',
648
+ 'aria-hidden': 'true',
649
+ })
650
+ : null,
651
+ ...edges.map((edge) => {
652
+ const from = positions[edge.from]
653
+ const to = positions[edge.to]
654
+ if (!from || !to) return null
655
+ const probability = edgeProbability(evaluation, edge.from, edge.to)
656
+ const manualOutcomes = evaluation.decisions[edge.from]?.manualOutcomes
657
+ || evaluation.decisions[edge.from]?.baseOutcomes
658
+ || []
659
+ const manualChoice = evaluation.decisions[edge.from]?.manual
660
+ && manualOutcomes.some((outcome) => outcome.to === edge.to)
661
+ const edgeClassNames = [
662
+ 'compass-edge',
663
+ probability.global != null ? 'is-reachable' : '',
664
+ probability.estimated ? 'is-estimated' : '',
665
+ probabilityTier(probability.global),
666
+ passedEdges.has(`${edge.from}->${edge.to}`) ? 'is-passed' : '',
667
+ manualChoice ? 'is-manual-choice' : '',
668
+ ].filter(Boolean).join(' ')
669
+ const activateEdge = (event) => {
670
+ if (event.key === 'Enter' || event.key === ' ') {
671
+ event.preventDefault()
672
+ this.setManualOverride(edge.from, edge.to)
673
+ }
674
+ }
675
+ return h('g', {
676
+ key: `edge-${edge.cell}`,
677
+ className: edgeClassNames,
678
+ role: manualChoice ? 'button' : undefined,
679
+ tabIndex: manualChoice ? 0 : undefined,
680
+ onClick: manualChoice ? () => this.setManualOverride(edge.from, edge.to) : undefined,
681
+ onKeyDown: manualChoice ? activateEdge : undefined,
682
+ },
683
+ h('line', {
684
+ x1: from.x,
685
+ y1: from.y,
686
+ x2: to.x,
687
+ y2: to.y,
688
+ }),
689
+ probability.global != null && Number(probability.global) > 0
690
+ ? h('text', {
691
+ x: (from.x + to.x) / 2,
692
+ y: (from.y + to.y) / 2,
693
+ dominantBaseline: 'middle',
694
+ className: 'compass-edge-label',
695
+ }, formatPercent(probability.global, probability.estimated))
696
+ : null,
697
+ probability.global != null && Number(probability.global) > 0
698
+ ? h('title', null, manualChoice
699
+ ? `能动分歧:选择 ${edge.to}`
700
+ : `局部 ${formatPercent(probability.local, probability.estimated)}`)
701
+ : null,
702
+ )
703
+ }),
704
+ ...nodes.map((node) => {
705
+ const position = positions[node]
706
+ const nodeType = nodeTypes[node]
707
+ const nodeInfo = nodeType == null
708
+ ? kcnavNodeCatalog['-1']
709
+ : kcnavNodeCatalog[String(nodeType)] || kcnavNodeCatalog['-1']
710
+ const visual = nodeVisualStyle(nodeInfo)
711
+ const airState = evaluation.probability.reach[node] > 0 && [4, 5, 7, 10, 15].includes(nodeType)
712
+ ? airStateRange(airPower, loadEnemies(mapId, node)) : null
713
+ const centerX = position.x + visual.offsetX
714
+ const centerY = position.y + visual.offsetY
715
+ const manualSource = manualSourceForTarget(evaluation, node)
716
+ const manualTarget = manualSource && manualSource !== node
717
+ const classNames = [
718
+ 'compass-node',
719
+ nodeInfo?.className || '',
720
+ node === selectedNode ? 'is-selected' : '',
721
+ node === currentNode ? 'is-current' : '',
722
+ nodeType === 5 ? 'is-boss' : '',
723
+ manualTarget ? 'is-manual-target' : '',
724
+ ].filter(Boolean).join(' ')
725
+ const activateNode = (event) => {
726
+ if (event.key !== 'Enter' && event.key !== ' ') return
727
+ event.preventDefault()
728
+ if (manualTarget) this.setManualOverride(manualSource, node)
729
+ else this.selectNode(node)
730
+ }
731
+ return h('g', {
732
+ key: `node-${node}`,
733
+ className: classNames,
734
+ role: 'button',
735
+ tabIndex: 0,
736
+ onClick: () => manualTarget ? this.setManualOverride(manualSource, node) : this.selectNode(node),
737
+ onKeyDown: activateNode,
738
+ },
739
+ h('title', null, nodeInfo?.label ? `${node} · ${nodeInfo.label}` : node),
740
+ h('ellipse', {
741
+ className: 'compass-node-shape',
742
+ cx: centerX,
743
+ cy: centerY,
744
+ rx: visual.rx,
745
+ ry: visual.ry,
746
+ fill: visual.background,
747
+ stroke: visual.border,
748
+ strokeWidth: visual.stroke,
749
+ opacity: visual.opacity,
750
+ }),
751
+ visual.simpleBorder
752
+ ? h('ellipse', {
753
+ className: 'compass-node-simple-border',
754
+ cx: centerX,
755
+ cy: centerY,
756
+ rx: visual.rx + 2,
757
+ ry: visual.ry + 2,
758
+ stroke: visual.simpleBorder,
759
+ strokeWidth: 2,
760
+ opacity: visual.opacity,
761
+ })
762
+ : null,
763
+ h('text', {
764
+ x: centerX,
765
+ y: centerY + visual.textOffsetY,
766
+ dominantBaseline: 'middle',
767
+ className: 'compass-node-label',
768
+ style: {
769
+ fill: '#ffffff',
770
+ stroke: '#000000',
771
+ strokeWidth: Math.min(4, Math.max(2, Number(visual.fontSize) / 5)),
772
+ fontSize: visual.fontSize,
773
+ },
774
+ }, node),
775
+ airState ? h('text', { x: centerX, y: centerY + visual.ry + 21, className: 'compass-node-air-state', textAnchor: 'middle' },
776
+ h('title', null, `当前舰队制空 ${airPower.min}~${airPower.max};未计沿途损耗、陆航和支援${airState.uncertain ? ';敌编成数据不完整或不确定' : ''}`),
777
+ ...airState.states.flatMap((state, index) => [
778
+ index ? h('tspan', { key: 'separator', fill: '#ffffff' }, '/') : null,
779
+ h('tspan', { key: state.label, fill: state.color }, state.label),
780
+ ]), airState.uncertain ? '?' : null) : null,
781
+ )
782
+ }),
783
+ )
784
+ }
785
+
786
+ async openSimulator(target, rootState, battles) {
787
+ this.setState({ exportBusy: true, exportError: null })
788
+ try {
789
+ const url = target === 'kc3'
790
+ ? await kc3Url(kc3Save(rootState, this.state.mapId, battles, this.state.exportFormations, this.state.deckId))
791
+ : noroUrl(noroDeck(rootState, this.state.mapId, battles, this.state.exportEnemies, this.state.exportFormations, this.state.deckId))
792
+ await require('electron').shell.openExternal(url)
793
+ } catch (error) {
794
+ this.setState({ exportError: error.message || String(error) })
795
+ } finally {
796
+ this.setState({ exportBusy: false })
797
+ }
798
+ }
799
+
800
+ renderShipPortrait(masterId, name, rootState, size = 34) {
801
+ const { getShipImgPath } = require('views/utils/ship-img')
802
+ const enemy = masterId >= 1500
803
+ // poi prefers cached files, then uses the player's game server.
804
+ const server = rootState?.info?.server?.ip
805
+ const version = rootState?.const?.$shipgraph?.find(graph => graph.api_id === masterId)?.api_version?.[0]
806
+ const url = getShipImgPath(masterId, enemy ? 'banner' : 'remodel', false, server, version)
807
+ const available = /^(file:|https?:\/\/)/.test(url)
808
+ const offset = enemy ? 1.5 : rootState?.fcd?.shipavatar?.marginMagics?.[masterId]?.normal ?? 0.555
809
+ return h('span', { className: 'compass-portrait', title: `${name} #${masterId}`, role: 'img', 'aria-label': `${name} #${masterId}`,
810
+ style: { width: size, height: size } },
811
+ available ? h('img', { src: url, alt: '', style: { height: Math.round(size * 182 / 176), marginLeft: -Math.round((offset + 0.25) * size) } })
812
+ : h('span', { className: 'compass-portrait-missing' }, `#${masterId}`))
813
+ }
814
+
815
+ renderExportPanel(definition, geometry, context, rootState) {
816
+ if (!this.state.exportOpen) return null
817
+ let choices, route, battles, error
818
+ try {
819
+ if (!context.dataAvailable || !mapGeometryAvailable(definition, geometry)) throw new Error('poi 数据不可用')
820
+ choices = enumerateRoutes(definition, geometry, context, this.state.overrides)
821
+ route = choices.routes.find((candidate) => candidate.key === this.state.exportRoute) || choices.routes[0]
822
+ if (!route) throw new Error('暂无可导出的完整路线,请先确认带路条件')
823
+ battles = battleNodes(this.state.mapId, route, geometry)
824
+ } catch (caught) { error = caught.message }
825
+ const ready = battles?.length && battles.every((battle) => battle.rows.length)
826
+ return h('section', { className: 'compass-panel compass-export-panel' },
827
+ h('div', { className: 'compass-panel-heading' }, h('h4', null, '导出模拟器'),
828
+ h('button', { type: 'button', className: 'compass-reset', 'aria-label': '关闭导出', onClick: () => this.setState({ exportOpen: false }) }, '关闭')),
829
+ error ? h('div', { className: 'compass-warnings' }, error) : null,
830
+ route ? h('label', { className: 'compass-export-route' }, '路线',
831
+ h('select', { value: route.key, 'aria-label': '导出路线', onChange: (event) => this.setState({ exportRoute: event.target.value, exportError: null }) },
832
+ ...choices.routes.map((candidate) => h('option', { key: candidate.key, value: candidate.key },
833
+ `${candidate.nodes.join('→')}${candidate.boss ? ' · Boss' : ''} · ${candidate.probability == null ? '含能动选路' : formatPercent(candidate.probability, candidate.estimated)}`))),
834
+ ) : null,
835
+ choices?.blocked.length ? h('div', { className: 'compass-warnings' }, `未知分歧:${choices.blocked.join('、')}`) : null,
836
+ battles?.length ? h('div', { className: 'compass-export-battles' },
837
+ h('div', { className: 'compass-export-battle compass-muted' }, h('span', null, '节点'), h('span', null, '我方阵型'), h('span', null, 'noro6 敌编成')),
838
+ ...battles.map((battle) => h('div', { key: battle.node, className: 'compass-export-battle' },
839
+ h('strong', null, battle.node),
840
+ h('select', { 'aria-label': `${battle.node} 我方阵型`, value: this.state.exportFormations[battle.node] || battle.formation,
841
+ onChange: (event) => this.setState({ exportFormations: { ...this.state.exportFormations, [battle.node]: Number(event.target.value) }, exportError: null }) },
842
+ ...[1, 2, 3, 4, 5, 6].map((id) => h('option', { key: id, value: id }, formationLabel(id)))),
843
+ battle.rows.length ? h('select', { 'aria-label': `${battle.node} noro6 敌编成`,
844
+ value: battle.rows.some((entry) => compositionKey(entry) === this.state.exportEnemies[battle.node]) ? this.state.exportEnemies[battle.node] : compositionKey(battle.rows[0]),
845
+ onChange: (event) => this.setState({ exportEnemies: { ...this.state.exportEnemies, [battle.node]: event.target.value }, exportError: null }) },
846
+ ...battle.rows.map((entry) => h('option', { key: compositionKey(entry), value: compositionKey(entry) },
847
+ `${(entry.share * 100).toFixed(1)}% · ${formationLabel(entry.formation)} · ${entry.mainFleet.map((ship) => `${ship.name} #${ship.id}`).join(' / ')}${entry.escortFleet.length ? ' + 护卫舰队' : ''}`)))
848
+ : h('span', { className: 'compass-muted' }, '尚未缓存/本月无样本'),
849
+ )),
850
+ ) : null,
851
+ h('div', { className: 'compass-export-actions' },
852
+ h('button', { type: 'button', disabled: !ready || this.state.exportBusy, onClick: () => this.openSimulator('kc3', rootState, battles) }, 'KC3Kai'),
853
+ h('button', { type: 'button', disabled: !ready || this.state.exportBusy, onClick: () => this.openSimulator('noro', rootState, battles) }, 'noro6'),
854
+ ),
855
+ route?.nodes.some((node) => localKcnavNodeTypes[this.state.mapId]?.[node] === 3)
856
+ ? h('div', { className: 'compass-warnings' }, '漩涡损耗未设置') : null,
857
+ this.state.exportError ? h('div', { className: 'compass-warnings', role: 'alert' }, this.state.exportError) : null,
858
+ )
859
+ }
860
+
861
+ renderEnemyPanel(mapId, node) {
862
+ if (!node) return null
863
+ const type = localNodeTypesForMap(mapId, mapCatalog.maps[mapId])[node]
864
+ if (!kcnavNodeCatalog[type]?.hasEnemies) return null
865
+ const data = loadEnemies(mapId, node)
866
+ const heading = h('h4', null, `${node} 点 · 敌舰编成`)
867
+ if (data.status !== 'ready') return h('section', { className: 'compass-panel' }, heading,
868
+ h('div', { className: 'compass-muted' }, data.status === 'invalid' ? '敌编成缓存不可用' : '敌编成尚未缓存'))
869
+ const rangeText = (range) => !range ? '—' : range[0] === range[1] ? String(range[0]) : range.join('~')
870
+ const airText = (air) => air ? `劣势 ${air.disadvantage} / 均势 ${air.parity} / 空优 ${air.superiority} / 空确 ${air.supremacy}` : '制空未知'
871
+ const masters = getRootState()?.const || {}
872
+ const shipName = (ship) => {
873
+ const master = masters.$ships?.[ship.id]
874
+ if (!master) return `${ship.name || '敌舰'} #${ship.id}`
875
+ const rank = /^(elite|flagship)$/i.test(master.api_yomi) ? ` ${master.api_yomi}` : ''
876
+ return `${master.api_name}${rank}`
877
+ }
878
+ const equipName = (id) => masters.$equips?.[id]?.api_name || `#${id}`
879
+ return h('section', { className: 'compass-panel compass-enemies' }, heading,
880
+ data.rows.length ? h('div', { className: 'compass-enemy-air-range' }, `空优 ${rangeText(data.superiority)} / 空确 ${rangeText(data.supremacy)}${data.uncertain ? '?' : ''}`) : null,
881
+ !data.rows.length ? h('div', { className: 'compass-muted' }, '此期间无记录') : null,
882
+ h('div', { className: 'compass-enemy-list' }, ...data.rows.map((entry, index) => {
883
+ const fleet = [...entry.mainFleet, ...entry.escortFleet]
884
+ const share = entry.share == null ? '—' : `${(entry.share * 100).toFixed(1)}%`
885
+ return h('details', { className: 'compass-enemy-entry', key: `${entry.masterId}-${entry.formation}-${index}` },
886
+ h('summary', { title: `${airText(entry.thresholds)}${entry.uncertain ? ' · 装备不确定' : ''}` },
887
+ h('span', { className: 'compass-enemy-share' }, share),
888
+ h('span', null, formationLabel(entry.formation)),
889
+ h('span', { className: 'compass-enemy-fleet' }, ...fleet.map((ship, shipIndex) => h('span', {
890
+ key: shipIndex, className: 'compass-enemy-ship',
891
+ style: { gridColumn: shipIndex < entry.mainFleet.length ? shipIndex + 1 : shipIndex - entry.mainFleet.length + 1, gridRow: shipIndex < entry.mainFleet.length ? 1 : 2 },
892
+ title: shipIndex >= entry.mainFleet.length ? '护卫舰队' : '主力舰队',
893
+ }, this.renderShipPortrait(ship.id, shipName(ship), getRootState())))),
894
+ h('span', { className: 'compass-enemy-air' }, `空优 ${entry.thresholds?.superiority ?? '—'} / 空确 ${entry.thresholds?.supremacy ?? '—'}${entry.uncertain ? '?' : ''}`),
895
+ ),
896
+ h('div', { className: 'compass-enemy-detail' },
897
+ h('div', null, airText(entry.thresholds)),
898
+ h('div', null, `陆航:${airText(entry.lbasThresholds)}`),
899
+ entry.uncertain ? h('div', null, `装备不确定:${entry.uncertainAirpowerItems.join('、')}`) : null,
900
+ h('div', { className: 'compass-enemy-stats' },
901
+ h('div', { className: 'compass-enemy-stat-row compass-muted' }, ...['', 'HP', '火力', '雷装', '对空', '装甲', '装备'].map((label, i) => h('span', { key: i }, label))),
902
+ ...fleet.map((ship, shipIndex) => h('div', { key: shipIndex, className: `compass-enemy-stat-row${shipIndex === entry.mainFleet.length ? ' is-escort-start' : ''}` },
903
+ this.renderShipPortrait(ship.id, shipName(ship), getRootState()),
904
+ ...['hp', 'fp', 'torp', 'aa', 'armor'].map(field => h('span', { key: field }, ship[field] ?? '—')),
905
+ h('span', { className: 'compass-enemy-equips' }, (ship.equips || []).filter(id => id > 0).map(equipName).join(' / ') || '—'),
906
+ )),
907
+ ),
908
+ ),
909
+ )
910
+ })),
911
+ )
912
+ }
913
+
914
+ renderOverview(definition, context, geometryAvailable) {
915
+ if (!context.dataAvailable || !geometryAvailable) return h('section', { className: 'compass-panel' }, 'poi 数据不可用')
916
+ const summary = mapOverview(this.state.mapId, definition, context, this.state.overrides)
917
+ const config = losCatalog.maps[this.state.mapId] || {}
918
+ return h('section', { className: 'compass-panel compass-overview' },
919
+ h('h4', null, `${this.state.mapId} · ${definition.name}`),
920
+ h('dl', null,
921
+ h('dt', null, '最高空优'), h('dd', { className: 'compass-overview-air' }, `${summary.superiority ?? '—'}${summary.missing.length ? '?' : ''}`),
922
+ h('dt', null, '最高空确'), h('dd', { className: 'compass-overview-air' }, `${summary.supremacy ?? '—'}${summary.missing.length ? '?' : ''}`),
923
+ h('dt', null, '最高索敌线'), h('dd', { className: 'compass-overview-los' }, summary.maxLos == null ? '无' : h(React.Fragment, null, summary.maxLos, h('small', null, ` · ${summary.losNodes.join(' / ')}`))),
924
+ summary.maxLos != null ? h(React.Fragment, null,
925
+ h('dt', null, '分歧点系数'), h('dd', null, config.branchingCoefficient ?? '—'),
926
+ h('dt', null, '司令部系数'), h('dd', null, `${config.admiralCoefficient ?? '—'}${config.confidence === 'approximate' ? '?' : ''}`)) : null,
927
+ ),
928
+ summary.partial ? h('div', { className: 'compass-warnings' }, '路线未完整确定') : null,
929
+ summary.missing.length ? h('div', { className: 'compass-warnings' }, `制空数据缺失/不确定:${summary.missing.join('、')}`) : null,
930
+ )
931
+ }
932
+
933
+ renderRulePanel(mapDefinition, node, context, evaluation) {
934
+ if (!node) return h('div', { className: 'compass-empty-panel' }, '选择节点')
935
+ const rows = ruleRows(mapDefinition, node, context)
936
+ const decision = evaluation.decisions[node]
937
+ return h('section', { className: 'compass-panel' },
938
+ h('h4', null, `${node} 点`),
939
+ rows.length
940
+ ? h('div', { className: 'compass-conditions' }, ...rows.map(({ rule, result, outcomes: ruleOutcomes }) => {
941
+ const active = decision?.rule?.id === rule.id
942
+ const displayedOutcomes = active ? decision.outcomes : ruleOutcomes
943
+ const outcomes = displayedOutcomes.length ? displayedOutcomes.map(outcomeText).join(' / ') : '—'
944
+ return h('div', {
945
+ key: rule.id,
946
+ className: `compass-condition is-${result.status}${active ? ' is-active' : ''}`,
947
+ tabIndex: 0,
948
+ 'aria-label': `${conditionStatus(result.status)}:${predicateLabel(rule.predicate)}`,
949
+ },
950
+ h('span', { className: 'compass-condition-icon', 'aria-hidden': 'true' }, conditionIcon(result.status, active)),
951
+ h('span', { className: 'compass-condition-label' }, predicateLabel(rule.predicate)),
952
+ h('span', { className: 'compass-condition-outcomes' }, outcomes),
953
+ h('div', { className: 'compass-condition-popover', role: 'tooltip' },
954
+ h('div', null, `${conditionStatus(result.status)}${active ? ' · 当前出口规则' : ''}`),
955
+ h('div', null, `出口:${outcomes}`),
956
+ rule.confidence === 'approximate' ? h('div', null, '概率为资料中的近似值') : null,
957
+ ),
958
+ )
959
+ }))
960
+ : h('div', { className: 'compass-muted' }, '暂无规则'),
961
+ decision?.manual
962
+ ? h('div', { className: 'compass-manual' },
963
+ h('strong', null, decision.manualChoiceRequired ? '请指定方向' : '手动覆盖出口'),
964
+ ...((decision.manualOutcomes || decision.baseOutcomes || [])).map((outcome) => h('button', {
965
+ key: outcome.to,
966
+ type: 'button',
967
+ className: decision.manualOverride === outcome.to ? 'is-active' : '',
968
+ onClick: () => this.setManualOverride(node, outcome.to),
969
+ }, outcome.to)),
970
+ )
971
+ : null,
972
+ )
973
+ }
974
+
975
+ render() {
976
+ const mapDefinition = mapCatalog.maps[this.state.mapId] || mapCatalog.maps[mapIds[0]]
977
+ const mapLosConfig = losCatalog.maps?.[this.state.mapId] || {}
978
+ const phase = phaseOptionForMap(this.state.mapId, this.state.phaseId)
979
+ const rootState = getRootState()
980
+ const context = fleetContextFromState(rootState, this.state.deckId, {
981
+ losCalculator: calculatePoiLos33,
982
+ losCoefficient: mapLosConfig.branchingCoefficient,
983
+ admiralCoefficient: mapLosConfig.admiralCoefficient,
984
+ passedNodes: pluginState.currentMapId === this.state.mapId ? pluginState.passedNodes : [],
985
+ phase: phase?.id || null,
986
+ flags: phaseFlags(this.state.mapId, phase?.id),
987
+ })
988
+ const geometry = mapGeometry(this.state.mapId)
989
+ const displayMapDefinition = phase?.boss
990
+ ? { ...mapDefinition, start: phase.start || mapDefinition.start, boss: phase.boss }
991
+ : mapDefinition
992
+ const geometryAvailable = mapGeometryAvailable(displayMapDefinition, geometry)
993
+ const evaluation = evaluateMap(displayMapDefinition, context, this.state.overrides)
994
+ const currentNode = pluginState.currentMapId === this.state.mapId ? pluginState.currentNode : null
995
+ const passedNodes = pluginState.currentMapId === this.state.mapId ? pluginState.passedNodes : []
996
+ const mapLosText = mapLosConfig.branchingCoefficient == null
997
+ ? '本图无索敌分歧'
998
+ : `本图分歧点系数 ${mapLosConfig.branchingCoefficient};司令部系数 ${mapLosConfig.admiralCoefficient}${mapLosConfig.confidence === 'approximate' ? '?' : ''}`
999
+ const nodes = mapNodes(displayMapDefinition, geometry)
1000
+ const selectedNode = nodes.includes(this.state.selectedNode)
1001
+ ? this.state.selectedNode
1002
+ : null
1003
+ const cssPath = path.join(__dirname, 'assets', 'compass.css')
1004
+ const warnings = [
1005
+ context.dataAvailable && geometryAvailable ? null : 'poi 数据不可用',
1006
+ !context.dataAvailable && context.dataIssues.length ? context.dataIssues.join('、') : null,
1007
+ !geometryAvailable ? '地图几何' : null,
1008
+ evaluation.probability.unknownNodes.length ? `未知路线 ${evaluation.probability.unknownNodes.join('、')}` : null,
1009
+ ].filter(Boolean)
1010
+
1011
+ return h('div', { className: 'poi-compass' },
1012
+ h('link', { rel: 'stylesheet', href: cssPath }),
1013
+ h('header', { className: 'compass-titlebar' }, h('h3', null, '舰队罗盘')),
1014
+ h('div', { className: 'compass-toolbar' },
1015
+ this.renderMapSelector(),
1016
+ h('select', { className: 'compass-fleet-select', 'aria-label': '使用舰队', value: this.state.deckId,
1017
+ onChange: event => this.selectFleet(Number(event.target.value)) },
1018
+ ...[1, 2, 3, 4].map(id => h('option', { key: id, value: id }, `舰队 ${id}`))),
1019
+ phaseOptionsForMap(this.state.mapId).length > 1
1020
+ ? h('div', { className: 'compass-phase-group', role: 'group', 'aria-label': '地图阶段' },
1021
+ phaseOptionsForMap(this.state.mapId).map((option) => h('button', {
1022
+ key: option.id,
1023
+ type: 'button',
1024
+ className: phase?.id === option.id ? 'is-active' : '',
1025
+ onClick: () => this.selectPhase(option.id),
1026
+ }, option.label)),
1027
+ )
1028
+ : null,
1029
+ h('span', {
1030
+ className: 'compass-toolbar-fleet',
1031
+ title: `舰队 ${this.state.deckId}:${fleetSummary(context)};${mapLosText}`,
1032
+ }, h('span', { className: 'compass-fleet-portraits' }, ...(getFleet(rootState, this.state.deckId)?.api_ship || []).filter(id => id > 0).map(id => {
1033
+ const ship = rootState.info.ships?.[id]
1034
+ if (!ship) return null
1035
+ const name = rootState.const?.$ships?.[ship.api_ship_id]?.api_name || '舰娘'
1036
+ return h('span', { key: id }, this.renderShipPortrait(ship.api_ship_id, name, rootState, 30))
1037
+ })), h('span', null, fleetSummary(context))),
1038
+ this.renderLosIcons(context.losScores),
1039
+ this.renderFleetAir(calculatePoiFleetStat(rootState, this.state.deckId, 1, true)),
1040
+ currentNode ? h('span', { className: 'compass-toolbar-node' }, `当前 ${currentNode}`) : null,
1041
+ h('button', { type: 'button', className: 'compass-reset', 'aria-expanded': this.state.exportOpen, onClick: () => this.setState({ exportOpen: !this.state.exportOpen }) }, '导出'),
1042
+ h('button', { type: 'button', className: 'compass-reset', onClick: () => this.setState({ overrides: {} }) }, '重置路线'),
1043
+ ),
1044
+ warnings.length ? h('div', { className: 'compass-warnings' }, ...warnings.map((warning) => h('div', { key: warning }, warning))) : null,
1045
+ h('main', { className: 'compass-layout' },
1046
+ h('section', { className: 'compass-map-panel' }, this.renderMap(displayMapDefinition, geometry, evaluation, selectedNode, currentNode, passedNodes, this.state.mapId)),
1047
+ h('aside', { className: 'compass-sidebar' },
1048
+ this.state.exportOpen
1049
+ ? this.renderExportPanel(displayMapDefinition, geometry, context, rootState)
1050
+ : selectedNode
1051
+ ? this.renderRulePanel(mapDefinition, selectedNode, context, evaluation)
1052
+ : this.renderOverview(displayMapDefinition, context, geometryAvailable),
1053
+ ),
1054
+ ),
1055
+ h('div', { className: 'compass-enemy-region' }, this.renderEnemyPanel(this.state.mapId, selectedNode)),
1056
+ )
1057
+ }
1058
+ }
1059
+
1060
+ exports.reactClass = Compass
1061
+ exports.windowMode = true
1062
+ exports.pluginDidLoad = startPlugin
1063
+ exports.pluginWillUnload = stopPlugin
1064
+ exports.__test = {
1065
+ calculatePoiLos33,
1066
+ cellFromDetail,
1067
+ handleGameResponse,
1068
+ kcnavNodeTypesFromMap,
1069
+ localNodeTypesForMap,
1070
+ mapIdFromDetail,
1071
+ mapGeometryAvailable,
1072
+ mapBackgroundUrl,
1073
+ mapIds,
1074
+ mapNodes,
1075
+ nodeVisualStyle,
1076
+ nodeIconUrl,
1077
+ losConfig: losCatalog.maps,
1078
+ phaseOptionsForMap,
1079
+ pluginState,
1080
+ routeEdges,
1081
+ }