碧蓝 wiki 大型作战成就记录地图增强

增加侵蚀度、成就奖励、海域id、海域名称、成就种类、档案筛选器,添加存/读档功能,避免弹出框偏移出地图,去除平移与缩放

  1. // ==UserScript==
  2. // @name 碧蓝 wiki 大型作战成就记录地图增强
  3. // @namespace http://github.com/8qwe24657913
  4. // @match https://wiki.biligame.com/blhx/%E5%A4%A7%E5%9E%8B%E4%BD%9C%E6%88%98%E6%88%90%E5%B0%B1%E8%AE%B0%E5%BD%95%E5%9C%B0%E5%9B%BE
  5. // @grant none
  6. // @run-at document-start
  7. // @version 1.1.2
  8. // @author 8q
  9. // @description 增加侵蚀度、成就奖励、海域id、海域名称、成就种类、档案筛选器,添加存/读档功能,避免弹出框偏移出地图,去除平移与缩放
  10. // ==/UserScript==
  11.  
  12. /* globals L mapData mapModel filterMouseover filterMouseout mapPoints filterClick updateSection mapSize updateMap saveAchievements normalAchievementKeywords safeAchievementKeywords */
  13. ;(function () {
  14. 'use strict'
  15. function patch() {
  16. const LEVEL_COUNT = 6
  17. const ALL_REWARDS = [
  18. '深渊6',
  19. '深渊5',
  20. '深渊4',
  21. '宝箱6',
  22. '金菜',
  23. '紫菜',
  24. '金猫箱',
  25. '魔方',
  26. '紫币',
  27. '物资',
  28. '指令书',
  29. ]
  30. const MAP_DATA_SUPPLEMENT = {
  31. 11: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  32. 12: [5, '指令书', '物资', '魔方', '紫币', '金猫箱'],
  33. 13: [5, '指令书', '金菜', '魔方', '紫币', '金猫箱'],
  34. 14: [4, '紫币', '物资', '金菜', '紫币', '魔方'],
  35. 21: [2, '紫币', '物资', '紫菜', '紫币', '金菜'],
  36. 22: [1, '紫币', '指令书', '紫菜', '紫币', '物资'],
  37. 23: [2, '指令书', '紫菜', '紫菜', '紫币', '金菜'],
  38. 24: [2, '指令书', '紫菜', '物资', '紫币', '物资'],
  39. 25: [3, '指令书', '紫菜', '金菜', '紫币', '金菜'],
  40. 31: [2, '紫币', '紫菜', '紫菜', '紫币', '物资'],
  41. 32: [3, '指令书', '紫菜', '金菜', '宝箱6', '金菜'],
  42. 33: [3, '指令书', '紫菜', '物资', '宝箱6', '金菜'],
  43. 34: [3, '指令书', '紫菜', '紫菜', '宝箱6', '魔方'],
  44. 41: [3, '指令书', '物资', '金菜', '紫币', '魔方'],
  45. 42: [4, '指令书', '物资', '金菜', '紫币', '魔方'],
  46. 43: [2, '紫币', '紫菜', '物资', '紫币', '物资'],
  47. 44: [1, '紫币', '指令书', '紫菜', '紫币', '物资'],
  48. 51: [4, '指令书', '物资', '魔方', '深渊4', '魔方'],
  49. 52: [4, '紫币', '金菜', '魔方', '紫币', '魔方'],
  50. 53: [4, '指令书', '金菜', '魔方', '紫币', '魔方'],
  51. 54: [4, '指令书', '金菜', '金菜', '深渊4', '魔方'],
  52. 61: [4, '指令书', '物资', '魔方', '深渊4', '魔方'],
  53. 62: [3, '指令书', '物资', '金菜', '宝箱6', '紫菜'],
  54. 63: [4, '指令书', '物资', '金菜', '紫币', '魔方'],
  55. 64: [4, '指令书', '金菜', '金菜', '深渊4', '魔方'],
  56. 65: [3, '指令书', '指令书', '金菜', '紫币', '魔方'],
  57. 66: [3, '指令书', '指令书', '物资', '紫币', '魔方'],
  58. 71: [5, '指令书', '金菜', '魔方', '深渊5', '金猫箱'],
  59. 72: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  60. 73: [5, '指令书', '金菜', '魔方', '深渊5', '金猫箱'],
  61. 81: [2, '指令书', '指令书', '物资', '紫币', '金菜'],
  62. 82: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
  63. 83: [2, '指令书', '物资', '物资', '紫币', '金菜'],
  64. 84: [2, '紫币', '紫菜', '物资', '紫币', '金菜'],
  65. 85: [4, '指令书', '金菜', '魔方', '深渊4', '魔方'],
  66. 91: [4, '指令书', '金菜', '魔方', '深渊4', '魔方'],
  67. 92: [2, '紫币', '指令书', '紫菜', '紫币', '物资'],
  68. 93: [2, '指令书', '指令书', '紫菜', '紫币', '物资'],
  69. 94: [3, '指令书', '指令书', '物资', '宝箱6', '紫菜'],
  70. 95: [3, '指令书', '金菜', '紫菜', '宝箱6', '紫菜'],
  71. 101: [5, '指令书', '物资', '魔方', '深渊5', '金猫箱'],
  72. 102: [5, '指令书', '物资', '魔方', '深渊5', '金猫箱'],
  73. 103: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
  74. 104: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
  75. 105: [3, '紫币', '指令书', '紫菜', '宝箱6', '魔方'],
  76. 106: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  77. 111: [3, '紫币', '指令书', '金菜', '紫币', '魔方'],
  78. 112: [2, '指令书', '指令书', '紫菜', '紫币', '物资'],
  79. 113: [3, '紫币', '指令书', '金菜', '紫币', '魔方'],
  80. 114: [3, '紫币', '指令书', '物资', '宝箱6', '物资'],
  81. 121: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  82. 122: [2, '指令书', '物资', '紫菜', '紫币', '物资'],
  83. 123: [3, '紫币', '金菜', '紫菜', '紫币', '物资'],
  84. 124: [5, '指令书', '物资', '魔方', '紫币', '金猫箱'],
  85. 125: [3, '紫币', '指令书', '紫菜', '紫币', '物资'],
  86. 131: [2, '指令书', '指令书', '紫菜', '紫币', '物资'],
  87. 132: [2, '指令书', '物资', '紫菜', '紫币', '物资'],
  88. 133: [3, '紫币', '金菜', '金菜', '紫币', '物资'],
  89. 134: [2, '指令书', '物资', '紫菜', '紫币', '物资'],
  90. 135: [3, '紫币', '金菜', '物资', '宝箱6', '魔方'],
  91. 141: [3, '紫币', '指令书', '金菜', '紫币', '魔方'],
  92. 142: [4, '紫币', '金菜', '魔方', '深渊4', '魔方'],
  93. 143: [3, '紫币', '指令书', '金菜', '宝箱6', '魔方'],
  94. 144: [5, '指令书', '金菜', '魔方', '紫币', '金猫箱'],
  95. 151: [5, '指令书', '金菜', '魔方', '深渊5', '金猫箱'],
  96. 152: [5, '指令书', '金菜', '魔方', '紫币', '金猫箱'],
  97. 153: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  98. 155: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  99. 156: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  100. 157: [6, '指令书', '金菜', '魔方', '深渊6', '金猫箱'],
  101. 158: [5, '指令书', '物资', '魔方', '紫币', '金猫箱'],
  102. 159: [5, '指令书', '物资', '魔方', '深渊5', '金猫箱'],
  103. }
  104. const FILES = {
  105. 陨石事件: [44, 84, 125, 95, 104, 52],
  106. 能源革命: [22, 134, 32, 94, 142, 53],
  107. 科技与生活: [83, 122, 135, 143, 63, 54],
  108. 生活的变革: [23, 62, 114, 51, 41, 14],
  109. 魔方军用化: [21, 31, 66, 113, 65, 85],
  110. 魔方军用化II: [43, 112, 34, 133, 61, 71],
  111. '「微光」计划': [81, 132, 123, 105, 91, 64],
  112. 军备竞赛: [24, 92, 111, 25, 82, 42],
  113. 冷战升级: [93, 131, 141, 33, 103, 13],
  114. }
  115. const NORMAL_FILE = '档案(1/3/5)'
  116. const SAFE_FILE = '档案(2/4/6)'
  117. const SELECTOR_CLASS = 'leaflet-control-layers-selector'
  118. const name2file = {}
  119. for (const [file, ids] of Object.entries(FILES)) {
  120. for (const [i, id] of ids.entries()) {
  121. if (!mapData[id]) continue
  122. const name = mapData[id][0]
  123. name2file[name] = [file, i + 1]
  124. }
  125. }
  126. const name2id = {}
  127. const name2level = {}
  128. const name2rewards = {}
  129. const achievementTypes = {
  130. 普通海域: [...normalAchievementKeywords, NORMAL_FILE],
  131. 安全海域: [...safeAchievementKeywords, SAFE_FILE],
  132. }
  133. const isSafe = {}
  134. for (const [safety, achievementType] of Object.entries(achievementTypes)) {
  135. for (const achievement of achievementType) {
  136. isSafe[achievement] = safety === '安全海域'
  137. }
  138. }
  139. const name2AchievementTypes = {}
  140. const achievementKeywordsWithoutFile = [...normalAchievementKeywords, ...safeAchievementKeywords]
  141. for (const [id, [level, ...rewards]] of Object.entries(MAP_DATA_SUPPLEMENT)) {
  142. if (!mapData[id]) continue
  143. const name = mapData[id][0]
  144. name2id[name] = Number(id)
  145. name2level[name] = level
  146. name2rewards[name] = rewards
  147. name2AchievementTypes[name] = mapData[id].slice(3).map((achievement) => {
  148. if (achievement.includes('档案')) {
  149. return name2file[name][1] % 2 === 0 ? SAFE_FILE : NORMAL_FILE
  150. } else {
  151. return achievementKeywordsWithoutFile.find((keyword) => achievement.includes(keyword))
  152. }
  153. })
  154. }
  155. // 原来就有的 bug,更新了成就情况后筛选结果不跟着变
  156. const oldUpdateMap = updateMap
  157. window.updateMap = function updateMap() {
  158. oldUpdateMap()
  159. updateSection()
  160. }
  161. // 增加各种筛选器
  162. const safetyFilterList = L.DomUtil.get('filter-safe-todo-box').parentElement
  163. const safetyFilterChildren = [...safetyFilterList.children]
  164. function createFilterList(type, text) {
  165. L.DomUtil.create(
  166. 'div',
  167. `filter-title filter-${type}-title`,
  168. safetyFilterList.parentElement,
  169. ).innerText = text
  170. return L.DomUtil.create('div', `filter-list filter-${type}-list`, safetyFilterList.parentElement)
  171. }
  172. function createFilterBox(parent, type, text = '') {
  173. const filterBox = L.DomUtil.create('label', null, parent)
  174. filterBox.id = `filter-${type}-box`
  175. filterBox.for = `filter-${type}`
  176. if (text) filterBox.innerText = text
  177. return filterBox
  178. }
  179. function createFilterCheckbox(parent, type, text) {
  180. const filterBox = createFilterBox(parent, type)
  181. filterBox.innerHTML = `<div><input type="checkbox" id="filter-${type}" class="${SELECTOR_CLASS}"><span>${text}</span></div>`
  182. return L.DomUtil.get(`filter-${type}`)
  183. }
  184. function createOption(parent, value, text = value) {
  185. const option = L.DomUtil.create('option', null, parent)
  186. option.value = value
  187. option.innerText = text
  188. return option
  189. }
  190. function createSelect(parent) {
  191. const filterSelect = L.DomUtil.create('select', SELECTOR_CLASS, parent)
  192. createOption(filterSelect, '', '(未选择)')
  193. return filterSelect
  194. }
  195. // 侵蚀度
  196. const levelFilterList = createFilterList('level', '侵蚀度:')
  197. const levelFilterBoxes = new Array(LEVEL_COUNT)
  198. .fill()
  199. .map((_, i) => createFilterCheckbox(levelFilterList, `level-${i + 1}`, `侵蚀${i + 1}`))
  200. // 未获取的奖励
  201. const rewardFilterList = createFilterList('reward', '未获取的奖励:')
  202. const rewardFilterBoxes = ALL_REWARDS.map((reward, i) =>
  203. createFilterCheckbox(rewardFilterList, `reward-${i + 1}`, reward),
  204. )
  205. // 海域搜索
  206. const searchFilterList = createFilterList('name-id', '海域搜索:')
  207. const searchFilterBoxes = [
  208. ['name', '海域名称:', null],
  209. ['id', '海域编号:', '^\\d+$'],
  210. ].map(([type, text, pattern]) => {
  211. const filterBox = createFilterBox(searchFilterList, type, text)
  212. const filterInput = L.DomUtil.create('input', SELECTOR_CLASS, filterBox)
  213. filterInput.type = 'text'
  214. filterInput.id = `filter-${type}`
  215. if (pattern) filterInput.pattern = pattern
  216. return filterInput
  217. })
  218. // 档案
  219. const fileFilterBoxes = [
  220. ['file-name', '档案名称:', Object.keys(FILES)],
  221. ['file-index', '档案序号:', [1, 2, 3, 4, 5, 6]],
  222. ].map(([type, text, options]) => {
  223. const filterBox = createFilterBox(searchFilterList, type, text)
  224. const filterSelect = createSelect(filterBox)
  225. for (const option of options) {
  226. createOption(filterSelect, option)
  227. }
  228. return filterSelect
  229. })
  230. // 未完成成就具体类型
  231. const achievementTypeSelect = (() => {
  232. const filterBox = createFilterBox(safetyFilterList, 'achievement', '具体类型:')
  233. const filterSelect = createSelect(filterBox)
  234. for (const [group, types] of Object.entries(achievementTypes)) {
  235. const optgroup = L.DomUtil.create('optgroup', null, filterSelect)
  236. optgroup.label = group
  237. for (const type of types) {
  238. createOption(optgroup, type)
  239. }
  240. }
  241. return filterSelect
  242. })()
  243. const filters = {
  244. safety: {
  245. checked: false,
  246. // 原来的筛选函数里没有考虑档案,这里修个 bug
  247. filter(section) {
  248. const achievementTypes = name2AchievementTypes[section.name]
  249. for (const [i, isCompleted] of section.completed.entries()) {
  250. if (isCompleted) continue
  251. if (isSafe[achievementTypes[i]] ? mapModel.filters.safeTodo : mapModel.filters.normalTodo)
  252. return true
  253. }
  254. return false
  255. },
  256. update() {
  257. mapModel.filters.safeTodo = L.DomUtil.get('filter-safe-todo').checked
  258. mapModel.filters.normalTodo = L.DomUtil.get('filter-normal-todo').checked
  259. this.checked = mapModel.filters.safeTodo || mapModel.filters.normalTodo
  260. },
  261. },
  262. level: {
  263. checked: false,
  264. filter(section) {
  265. return mapModel.filters.levels[name2level[section.name] - 1]
  266. },
  267. update() {
  268. mapModel.filters.levels = levelFilterBoxes.map((checkbox) => checkbox.checked)
  269. this.checked = mapModel.filters.levels.includes(true)
  270. },
  271. },
  272. reward: {
  273. checked: false,
  274. filter(section) {
  275. const rewards = name2rewards[section.name].slice(section.completed.filter((x) => x).length)
  276. return rewards.some((reward) => mapModel.filters.rewards.has(reward))
  277. },
  278. update() {
  279. mapModel.filters.rewards = new Set(
  280. rewardFilterBoxes.map((checkbox, i) => checkbox.checked && ALL_REWARDS[i]).filter((x) => x),
  281. )
  282. this.checked = mapModel.filters.rewards.size > 0
  283. },
  284. },
  285. name: {
  286. checked: false,
  287. filter(section) {
  288. return section.name.includes(mapModel.filters.name)
  289. },
  290. update() {
  291. mapModel.filters.name = searchFilterBoxes[0].value
  292. this.checked = !!mapModel.filters.name
  293. },
  294. },
  295. id: {
  296. checked: false,
  297. filter(section) {
  298. return name2id[section.name] === mapModel.filters.id
  299. },
  300. update() {
  301. const value = searchFilterBoxes[1].value
  302. mapModel.filters.id = Number(value)
  303. this.checked = !!value && !Number.isNaN(mapModel.filters.id)
  304. },
  305. },
  306. achievement: {
  307. checked: false,
  308. filter(section) {
  309. const achievementTypes = name2AchievementTypes[section.name]
  310. for (const [i, isCompleted] of section.completed.entries()) {
  311. if (isCompleted) continue
  312. if (achievementTypes[i] === mapModel.filters.achievementType) return true
  313. }
  314. return false
  315. },
  316. update() {
  317. mapModel.filters.achievementType = achievementTypeSelect.value
  318. this.checked = !!mapModel.filters.achievementType
  319. },
  320. },
  321. fileType: {
  322. checked: false,
  323. filter(section) {
  324. return name2file[section.name] && name2file[section.name][0] === mapModel.filters.fileType
  325. },
  326. update() {
  327. mapModel.filters.fileType = fileFilterBoxes[0].value
  328. this.checked = !!mapModel.filters.fileType
  329. },
  330. },
  331. fileIndex: {
  332. checked: false,
  333. filter(section) {
  334. return name2file[section.name] && name2file[section.name][1] === mapModel.filters.fileIndex
  335. },
  336. update() {
  337. mapModel.filters.fileIndex = Number(fileFilterBoxes[1].value)
  338. this.checked = !!mapModel.filters.fileIndex
  339. },
  340. },
  341. }
  342. /**
  343. * 筛选规则:
  344. * 1. 同一筛选器的不同选项间为逻辑或关系,不同筛选器间为逻辑与关系
  345. * 2. 若用户未填写某个筛选器,则视为该筛选器不存在
  346. * 3. 不存在任何筛选器时,不选中任何区块
  347. */
  348. let hasFilter = false
  349. window.filterSection = function filterSection(section) {
  350. if (!hasFilter) return false
  351. for (const { filter, checked } of Object.values(filters)) {
  352. if (checked && !filter(section)) return false
  353. }
  354. return true
  355. }
  356. function filterChange() {
  357. hasFilter = false
  358. for (const filter of Object.values(filters)) {
  359. filter.update()
  360. hasFilter = hasFilter || filter.checked
  361. }
  362. updateSection()
  363. }
  364. filterChange()
  365. function newFilterMouseover(checkbox) {
  366. if (!checkbox.checked) {
  367. checkbox.checked = true
  368. filterChange()
  369. checkbox.checked = false
  370. }
  371. }
  372. L.DomEvent.off(safetyFilterList, 'click', filterClick)
  373. for (const box of safetyFilterList.children) {
  374. L.DomEvent.off(box, {
  375. mouseover: filterMouseover,
  376. mouseout: filterMouseout,
  377. })
  378. }
  379. for (const box of [...safetyFilterChildren, ...levelFilterList.children, ...rewardFilterList.children]) {
  380. L.DomEvent.on(box, {
  381. mouseover: newFilterMouseover.bind(null, L.DomUtil.get(box.id.slice(0, -4))),
  382. mouseout: filterChange,
  383. change: filterChange,
  384. })
  385. }
  386. for (const box of searchFilterBoxes) {
  387. L.DomEvent.on(box, 'input paste change', filterChange)
  388. }
  389. for (const box of [achievementTypeSelect, ...fileFilterBoxes]) {
  390. L.DomEvent.on(box, 'change', filterChange)
  391. }
  392. // 备份 / 读取存档
  393. const collapseList = document.getElementsByClassName('leaflet-control-layers-overlays')[0].parentElement
  394. L.DomUtil.create('div', 'leaflet-control-layers-separator', collapseList)
  395. const saveLoadList = L.DomUtil.create('div', 'achievement-backup-load', collapseList)
  396. const saveLoad = [
  397. {
  398. type: 'backup',
  399. text: '备份',
  400. onclick() {
  401. const achievementList = []
  402. for (const i in mapModel.mapSection) {
  403. const section = mapModel.mapSection[i]
  404. for (const j in section.completed) {
  405. if (section.completed[j]) {
  406. achievementList.push(i + '-' + j)
  407. }
  408. }
  409. }
  410. const userAchievements = achievementList.join(',')
  411. const blob = new Blob([userAchievements], {
  412. type: 'text/plain',
  413. })
  414. const a = document.createElement('a')
  415. a.href = URL.createObjectURL(blob)
  416. a.download = `achievements-${new Date().toISOString()}.txt`
  417. a.click()
  418. },
  419. },
  420. {
  421. type: 'load',
  422. text: '读档',
  423. onclick() {
  424. const input = L.DomUtil.create('input')
  425. input.type = 'file'
  426. input.accept = '.txt,text/plain'
  427. input.onchange = () => {
  428. if (input.files.length === 0) return
  429. const [file] = input.files
  430. const fr = new FileReader()
  431. fr.onload = () => {
  432. updateAchievements(fr.result)
  433. saveAchievements()
  434. }
  435. fr.readAsText(file)
  436. }
  437. input.click()
  438. },
  439. },
  440. ]
  441. for (const { type, text, onclick } of saveLoad) {
  442. const button = L.DomUtil.create('button', `achievement-${type}`, saveLoadList)
  443. button.innerText = text
  444. L.DomEvent.on(button, 'click', onclick)
  445. }
  446. // 修改 popup 位置,使其不会超出窗口外
  447. initRrose()
  448. for (const mapPoint of Object.values(mapPoints)) {
  449. const { marker, popup } = mapPoint
  450. marker.bindPopup(
  451. (mapPoint.popup = new L.Rrose({
  452. ...popup.options,
  453. autoPan: false,
  454. }).setContent(popup.getContent())),
  455. )
  456. popup.remove()
  457. }
  458. // 防止用户手快事先平移或缩放了,复位一下
  459. const map = mapPoints[11].marker._map
  460. map.fitBounds([[0, 0], mapSize])
  461. // 禁止平移和缩放
  462. map.boxZoom.disable()
  463. map.doubleClickZoom.disable()
  464. map.dragging.disable()
  465. map.keyboard.disable()
  466. map.scrollWheelZoom.disable()
  467. map.touchZoom.disable()
  468. map.zoomControl.remove()
  469. // 自定义样式
  470. document.head.appendChild(document.createElement('style')).appendChild(
  471. document.createTextNode(`
  472. #filter-name {
  473. max-width: 90px;
  474. }
  475. #filter-id {
  476. max-width: 30px;
  477. }
  478. #alworldmap input:invalid {
  479. border-color: rgba(255, 0, 0, 0.5);
  480. }
  481. /* 存/读档按钮 */
  482. .achievement-backup-load {
  483. display: flex;
  484. justify-content: space-around;
  485. }
  486. /* 防止用户选中奇怪的东西 */
  487. #alworldmap {
  488. user-select: none;
  489. }
  490. .leaflet-rrose-content, .achievement-count-text {
  491. user-select: text;
  492. }
  493. /* 能点的东西给个提示啊 */
  494. #alworldmap .achievement,
  495. #alworldmap label,
  496. #alworldmap input[type="checkbox"],
  497. #alworldmap input[type="radio"],
  498. .leaflet-rrose-close-button,
  499. .achievement-backup-load button {
  500. cursor: pointer;
  501. }
  502. `),
  503. )
  504. }
  505. // 实测 bwiki 上 media wiki api 存的东西有时会乱套(读到了别人的存档?),先把网络同步禁用掉
  506. function loadAchievements() {
  507. return localStorage.getItem('userjs-worldmap-achievements') || ''
  508. }
  509. function updateAchievements(achievements) {
  510. const updated = new Set()
  511. for (const achievement of achievements.split(',')) {
  512. const [i, j] = achievement.split('-')
  513. const { completed } = mapModel.mapSection[i]
  514. if (!updated.has(i)) {
  515. completed.fill(false)
  516. updated.add(i)
  517. }
  518. completed[j] = true
  519. }
  520. updateMap()
  521. }
  522. // eslint-disable-next-line no-unused-vars
  523. function replaceLoadAchievements() {
  524. window.loadAchievements = loadAchievements
  525. }
  526. if (typeof mapModel === 'undefined') {
  527. // 早于地图初始化
  528. Object.defineProperty(window, 'mapModel', {
  529. get() {
  530. return undefined
  531. },
  532. set(value) {
  533. Object.defineProperty(window, 'mapModel', {
  534. value,
  535. writable: true,
  536. enumerable: true,
  537. configurable: true,
  538. })
  539. // replaceLoadAchievements()
  540. // 等初始化完了再执行 patch
  541. queueMicrotask(patch)
  542. },
  543. enumerable: false,
  544. configurable: true,
  545. })
  546. } else {
  547. // 晚于地图初始化
  548. // replaceLoadAchievements()
  549. // 执行时机晚则需要手动读取本地存档以更新地图
  550. // updateAchievements(loadAchievements())
  551. patch()
  552. }
  553. // 下面是引用的库,用来调整弹出框位置
  554. function initRrose() {
  555. /*
  556. Copyright (c) 2012 Eric S. Theise
  557. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  558. documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
  559. rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
  560. persons to whom the Software is furnished to do so, subject to the following conditions:
  561. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
  562. Software.
  563. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  564. WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  565. COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  566. OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  567. */
  568.  
  569. L.Rrose = L.Popup.extend({
  570. _initLayout: function () {
  571. const prefix = 'leaflet-rrose'
  572. const container = (this._container = L.DomUtil.create(
  573. 'div',
  574. prefix + ' ' + this.options.className + ' leaflet-zoom-animated',
  575. ))
  576. let closeButton
  577. let wrapper
  578.  
  579. if (this.options.closeButton) {
  580. closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container)
  581. closeButton.href = '#close'
  582. closeButton.innerHTML = '&#215;'
  583.  
  584. L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this)
  585. }
  586.  
  587. // Set the pixel distances from the map edges at which popups are too close and need to be re-oriented.
  588. const xBound = 200
  589. const yBound = 200
  590. // Determine the alternate direction to pop up; north mimics Leaflet's default behavior, so we initialize to that.
  591. this.options.position = 'n'
  592. // Then see if the point is too far north...
  593. const yDiff = yBound - this._map.latLngToContainerPoint(this._latlng).y
  594. if (yDiff > 0) {
  595. this.options.position = 's'
  596. }
  597. // or too far east...
  598. let xDiff = this._map.latLngToContainerPoint(this._latlng).x - (this._map.getSize().x - xBound)
  599. if (xDiff > 0) {
  600. this.options.position += 'w'
  601. } else {
  602. // or too far west.
  603. xDiff = xBound - this._map.latLngToContainerPoint(this._latlng).x
  604. if (xDiff > 0) {
  605. this.options.position += 'e'
  606. }
  607. }
  608.  
  609. // Create the necessary DOM elements in the correct order. Pure 'n' and 's' conditions need only one class for styling, others need two.
  610. if (/s/.test(this.options.position)) {
  611. if (this.options.position === 's') {
  612. this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container)
  613. wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container)
  614. } else {
  615. this._tipContainer = L.DomUtil.create(
  616. 'div',
  617. prefix + '-tip-container' + ' ' + prefix + '-tip-container-' + this.options.position,
  618. container,
  619. )
  620. wrapper = this._wrapper = L.DomUtil.create(
  621. 'div',
  622. prefix + '-content-wrapper' + ' ' + prefix + '-content-wrapper-' + this.options.position,
  623. container,
  624. )
  625. }
  626. this._tip = L.DomUtil.create(
  627. 'div',
  628. prefix + '-tip' + ' ' + prefix + '-tip-' + this.options.position,
  629. this._tipContainer,
  630. )
  631. L.DomEvent.disableClickPropagation(wrapper)
  632. this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper)
  633. L.DomEvent.on(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation)
  634. if (closeButton) closeButton.style.top = '20px'
  635. } else {
  636. if (this.options.position === 'n') {
  637. wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container)
  638. this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container)
  639. } else {
  640. wrapper = this._wrapper = L.DomUtil.create(
  641. 'div',
  642. prefix + '-content-wrapper' + ' ' + prefix + '-content-wrapper-' + this.options.position,
  643. container,
  644. )
  645. this._tipContainer = L.DomUtil.create(
  646. 'div',
  647. prefix + '-tip-container' + ' ' + prefix + '-tip-container-' + this.options.position,
  648. container,
  649. )
  650. }
  651. L.DomEvent.disableClickPropagation(wrapper)
  652. this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper)
  653. L.DomEvent.on(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation)
  654. this._tip = L.DomUtil.create(
  655. 'div',
  656. prefix + '-tip' + ' ' + prefix + '-tip-' + this.options.position,
  657. this._tipContainer,
  658. )
  659. }
  660. },
  661.  
  662. _updatePosition: function () {
  663. const pos = this._map.latLngToLayerPoint(this._latlng)
  664. const is3d = L.Browser.any3d
  665. const offset = new L.Point(...this.options.offset)
  666.  
  667. L.DomUtil.setPosition(this._container, pos)
  668.  
  669. if (/s/.test(this.options.position)) {
  670. this._containerBottom = -this._container.offsetHeight + offset.y - (is3d ? 0 : pos.y)
  671. } else {
  672. this._containerBottom = -offset.y - (is3d ? 0 : pos.y)
  673. }
  674.  
  675. if (/e/.test(this.options.position)) {
  676. this._containerLeft = offset.x + (is3d ? 0 : pos.x)
  677. } else if (/w/.test(this.options.position)) {
  678. this._containerLeft = -Math.round(this._containerWidth) + offset.x + (is3d ? 0 : pos.x)
  679. } else {
  680. this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x + (is3d ? 0 : pos.x)
  681. }
  682.  
  683. this._container.style.bottom = this._containerBottom + 'px'
  684. this._container.style.left = this._containerLeft + 'px'
  685. },
  686. })
  687. document.head.appendChild(document.createElement('style')).appendChild(
  688. document.createTextNode(`/* Rrose layout */
  689.  
  690. .leaflet-rrose {
  691. position: absolute;
  692. text-align: center;
  693. }
  694.  
  695. .leaflet-rrose-content-wrapper {
  696. padding: 1px;
  697. text-align: left;
  698. }
  699.  
  700. .leaflet-rrose-content {
  701. margin: 14px 20px;
  702. }
  703.  
  704. .leaflet-rrose-tip-container {
  705. margin: 0 auto;
  706. width: 40px;
  707. height: 20px;
  708. position: relative;
  709. overflow: hidden;
  710. }
  711.  
  712. .leaflet-rrose-tip-container-se, .leaflet-rrose-tip-container-ne {
  713. margin-left: 0;
  714. }
  715.  
  716. .leaflet-rrose-tip-container-sw, .leaflet-rrose-tip-container-nw {
  717. margin-right: 0;
  718. }
  719.  
  720. .leaflet-rrose-tip {
  721. width: 15px;
  722. height: 15px;
  723. padding: 1px;
  724.  
  725. -moz-transform: rotate(45deg);
  726. -webkit-transform: rotate(45deg);
  727. -ms-transform: rotate(45deg);
  728. -o-transform: rotate(45deg);
  729. transform: rotate(45deg);
  730. }
  731.  
  732. .leaflet-rrose-tip-n {
  733. margin: -8px auto 0;
  734. }
  735.  
  736. .leaflet-rrose-tip-s {
  737. margin: 11px auto 0;
  738. }
  739.  
  740. .leaflet-rrose-tip-se {
  741. margin: 11px 11px 11px -8px; overflow: hidden;
  742. }
  743.  
  744. .leaflet-rrose-tip-sw {
  745. margin: 11px 11px 11px 32px; overflow: hidden;
  746. }
  747.  
  748. .leaflet-rrose-tip-ne {
  749. margin: -8px 11px 11px -8px; overflow: hidden;
  750. }
  751.  
  752. .leaflet-rrose-tip-nw {
  753. margin: -8px 11px 11px 32px; overflow: hidden;
  754. }
  755.  
  756. a.leaflet-rrose-close-button {
  757. position: absolute;
  758. top: 0;
  759. right: 0;
  760. padding: 4px 5px 0 0;
  761. text-align: center;
  762. width: 18px;
  763. height: 14px;
  764. font: 16px/14px Tahoma, Verdana, sans-serif;
  765. color: #c3c3c3;
  766. text-decoration: none;
  767. font-weight: bold;
  768. }
  769.  
  770. a.leaflet-rrose-close-button:hover {
  771. color: #999;
  772. }
  773.  
  774. .leaflet-rrose-content p {
  775. margin: 18px 0;
  776. }
  777.  
  778. .leaflet-rrose-scrolled {
  779. overflow: auto;
  780. border-bottom: 1px solid #ddd;
  781. border-top: 1px solid #ddd;
  782. }
  783.  
  784. /* Visual appearance */
  785.  
  786. .leaflet-rrose-content-wrapper, .leaflet-rrose-tip {
  787. background: white;
  788.  
  789. box-shadow: 0 3px 10px #888;
  790. -moz-box-shadow: 0 3px 10px #888;
  791. -webkit-box-shadow: 0 3px 14px #999;
  792. }
  793.  
  794. .leaflet-rrose-content-wrapper {
  795. -moz-border-radius: 20px;
  796. -webkit-border-radius: 20px;
  797. border-radius: 20px;
  798. }
  799.  
  800. .leaflet-rrose-content-wrapper-se {
  801. -moz-border-radius: 0 20px 20px 20px;
  802. -webkit-border-radius: 0 20px 20px 20px;
  803. border-radius: 0 20px 20px 20px;
  804. }
  805.  
  806. .leaflet-rrose-content-wrapper-sw {
  807. -moz-border-radius: 20px 0 20px 20px;
  808. -webkit-border-radius: 20px 0 20px 20px;
  809. border-radius: 20px 0 20px 20px;
  810. }
  811.  
  812. .leaflet-rrose-content-wrapper-nw, .leaflet-rrose-content-wrapper-w {
  813. -moz-border-radius: 20px 20px 0 20px;
  814. -webkit-border-radius: 20px 20px 0 20px;
  815. border-radius: 20px 20px 0 20px;
  816. }
  817.  
  818. .leaflet-rrose-content-wrapper-ne, .leaflet-rrose-content-wrapper-e {
  819. -moz-border-radius: 20px 20px 20px 0;
  820. -webkit-border-radius: 20px 20px 20px 0;
  821. border-radius: 20px 20px 20px 0;
  822. }
  823.  
  824. .leaflet-rrose-content {
  825. font: 12px/1.4 "Helvetica Neue", Arial, Helvetica, sans-serif;
  826. }`),
  827. )
  828. }
  829. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址