map.ts 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093
  1. import {
  2. MapEngine,
  3. type MapEngineGameInfoRow,
  4. type MapEngineGameInfoSnapshot,
  5. type MapEngineStageRect,
  6. type MapEngineViewState,
  7. } from '../../engine/map/mapEngine'
  8. import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
  9. import { type AnimationLevel } from '../../utils/animationLevel'
  10. type CompassTickData = {
  11. angle: number
  12. long: boolean
  13. major: boolean
  14. }
  15. type CompassLabelData = {
  16. text: string
  17. angle: number
  18. rotateBack: number
  19. radius: number
  20. className: string
  21. }
  22. type ScaleRulerMinorTickData = {
  23. key: string
  24. topPx: number
  25. long: boolean
  26. }
  27. type ScaleRulerMajorMarkData = {
  28. key: string
  29. topPx: number
  30. label: string
  31. }
  32. type SideButtonMode = 'shown' | 'hidden'
  33. type SideActionButtonState = 'muted' | 'default' | 'active'
  34. type SideButtonPlacement = 'left' | 'right'
  35. type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
  36. type UserNorthReferenceMode = 'magnetic' | 'true'
  37. type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive'
  38. type SettingLockKey =
  39. | 'lockAnimationLevel'
  40. | 'lockSideButtonPlacement'
  41. | 'lockAutoRotate'
  42. | 'lockCompassTuning'
  43. | 'lockScaleRulerVisible'
  44. | 'lockScaleRulerAnchor'
  45. | 'lockNorthReference'
  46. | 'lockHeartRateDevice'
  47. type StoredUserSettings = {
  48. animationLevel?: AnimationLevel
  49. autoRotateEnabled?: boolean
  50. compassTuningProfile?: CompassTuningProfile
  51. northReferenceMode?: UserNorthReferenceMode
  52. sideButtonPlacement?: SideButtonPlacement
  53. showCenterScaleRuler?: boolean
  54. centerScaleRulerAnchorMode?: CenterScaleRulerAnchorMode
  55. lockAnimationLevel?: boolean
  56. lockSideButtonPlacement?: boolean
  57. lockAutoRotate?: boolean
  58. lockCompassTuning?: boolean
  59. lockScaleRulerVisible?: boolean
  60. lockScaleRulerAnchor?: boolean
  61. lockNorthReference?: boolean
  62. lockHeartRateDevice?: boolean
  63. }
  64. type MapPageData = MapEngineViewState & {
  65. showDebugPanel: boolean
  66. showGameInfoPanel: boolean
  67. showSystemSettingsPanel: boolean
  68. showCenterScaleRuler: boolean
  69. showPunchHintBanner: boolean
  70. centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
  71. statusBarHeight: number
  72. topInsetHeight: number
  73. hudPanelIndex: number
  74. configSourceText: string
  75. mockBridgeUrlDraft: string
  76. mockHeartRateBridgeUrlDraft: string
  77. gameInfoTitle: string
  78. gameInfoSubtitle: string
  79. gameInfoLocalRows: MapEngineGameInfoRow[]
  80. gameInfoGlobalRows: MapEngineGameInfoRow[]
  81. panelTimerText: string
  82. panelMileageText: string
  83. panelDistanceValueText: string
  84. panelProgressText: string
  85. panelSpeedValueText: string
  86. panelTimerFxClass: string
  87. panelMileageFxClass: string
  88. panelSpeedFxClass: string
  89. panelHeartRateFxClass: string
  90. compassTicks: CompassTickData[]
  91. compassLabels: CompassLabelData[]
  92. sideButtonMode: SideButtonMode
  93. sideButtonPlacement: SideButtonPlacement
  94. autoRotateEnabled: boolean
  95. lockAnimationLevel: boolean
  96. lockSideButtonPlacement: boolean
  97. lockAutoRotate: boolean
  98. lockCompassTuning: boolean
  99. lockScaleRulerVisible: boolean
  100. lockScaleRulerAnchor: boolean
  101. lockNorthReference: boolean
  102. lockHeartRateDevice: boolean
  103. sideToggleIconSrc: string
  104. sideButton2Class: string
  105. sideButton4Class: string
  106. sideButton11Class: string
  107. sideButton12Class: string
  108. sideButton13Class: string
  109. sideButton14Class: string
  110. sideButton16Class: string
  111. centerScaleRulerVisible: boolean
  112. centerScaleRulerCenterXPx: number
  113. centerScaleRulerZeroYPx: number
  114. centerScaleRulerHeightPx: number
  115. centerScaleRulerAxisBottomPx: number
  116. centerScaleRulerZeroVisible: boolean
  117. centerScaleRulerZeroLabel: string
  118. centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
  119. centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
  120. showLeftButtonGroup: boolean
  121. showRightButtonGroups: boolean
  122. showBottomDebugButton: boolean
  123. }
  124. const INTERNAL_BUILD_VERSION = 'map-build-283'
  125. const USER_SETTINGS_STORAGE_KEY = 'cmr_user_settings_v1'
  126. const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
  127. const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
  128. const PUNCH_HINT_AUTO_HIDE_MS = 30000
  129. let mapEngine: MapEngine | null = null
  130. let stageCanvasAttached = false
  131. let gameInfoPanelSyncTimer = 0
  132. let centerScaleRulerSyncTimer = 0
  133. let centerScaleRulerUpdateTimer = 0
  134. let punchHintDismissTimer = 0
  135. let panelTimerFxTimer = 0
  136. let panelMileageFxTimer = 0
  137. let panelSpeedFxTimer = 0
  138. let panelHeartRateFxTimer = 0
  139. let lastCenterScaleRulerStablePatch: Pick<
  140. MapPageData,
  141. | 'centerScaleRulerVisible'
  142. | 'centerScaleRulerCenterXPx'
  143. | 'centerScaleRulerZeroYPx'
  144. | 'centerScaleRulerHeightPx'
  145. | 'centerScaleRulerAxisBottomPx'
  146. | 'centerScaleRulerZeroVisible'
  147. | 'centerScaleRulerZeroLabel'
  148. | 'centerScaleRulerMinorTicks'
  149. | 'centerScaleRulerMajorMarks'
  150. > = {
  151. centerScaleRulerVisible: false,
  152. centerScaleRulerCenterXPx: 0,
  153. centerScaleRulerZeroYPx: 0,
  154. centerScaleRulerHeightPx: 0,
  155. centerScaleRulerAxisBottomPx: 0,
  156. centerScaleRulerZeroVisible: false,
  157. centerScaleRulerZeroLabel: '0 m',
  158. centerScaleRulerMinorTicks: [],
  159. centerScaleRulerMajorMarks: [],
  160. }
  161. let centerScaleRulerInputCache: Partial<Pick<
  162. MapPageData,
  163. 'stageWidth'
  164. | 'stageHeight'
  165. | 'zoom'
  166. | 'centerTileY'
  167. | 'tileSizePx'
  168. | 'previewScale'
  169. >> = {}
  170. const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
  171. 'buildVersion',
  172. 'renderMode',
  173. 'projectionMode',
  174. 'mapReady',
  175. 'mapReadyText',
  176. 'mapName',
  177. 'configStatusText',
  178. 'deviceHeadingText',
  179. 'devicePoseText',
  180. 'headingConfidenceText',
  181. 'accelerometerText',
  182. 'gyroscopeText',
  183. 'deviceMotionText',
  184. 'compassSourceText',
  185. 'compassTuningProfile',
  186. 'compassTuningProfileText',
  187. 'northReferenceButtonText',
  188. 'autoRotateSourceText',
  189. 'autoRotateCalibrationText',
  190. 'northReferenceText',
  191. 'centerText',
  192. 'tileSource',
  193. 'visibleTileCount',
  194. 'readyTileCount',
  195. 'memoryTileCount',
  196. 'diskTileCount',
  197. 'memoryHitCount',
  198. 'diskHitCount',
  199. 'networkFetchCount',
  200. 'cacheHitRateText',
  201. 'locationSourceMode',
  202. 'locationSourceText',
  203. 'mockBridgeConnected',
  204. 'mockBridgeStatusText',
  205. 'mockBridgeUrlText',
  206. 'mockCoordText',
  207. 'mockSpeedText',
  208. 'gpsCoordText',
  209. 'heartRateSourceMode',
  210. 'heartRateSourceText',
  211. 'heartRateConnected',
  212. 'heartRateStatusText',
  213. 'heartRateDeviceText',
  214. 'heartRateScanText',
  215. 'heartRateDiscoveredDevices',
  216. 'mockHeartRateBridgeConnected',
  217. 'mockHeartRateBridgeStatusText',
  218. 'mockHeartRateBridgeUrlText',
  219. 'mockHeartRateText',
  220. ])
  221. const CENTER_SCALE_RULER_DEP_KEYS = new Set<string>([
  222. 'showCenterScaleRuler',
  223. 'centerScaleRulerAnchorMode',
  224. 'stageWidth',
  225. 'stageHeight',
  226. 'topInsetHeight',
  227. 'zoom',
  228. 'centerTileY',
  229. 'tileSizePx',
  230. 'previewScale',
  231. ])
  232. const CENTER_SCALE_RULER_CACHE_KEYS: Array<keyof typeof centerScaleRulerInputCache> = [
  233. 'stageWidth',
  234. 'stageHeight',
  235. 'zoom',
  236. 'centerTileY',
  237. 'tileSizePx',
  238. 'previewScale',
  239. ]
  240. const RULER_ONLY_VIEW_KEYS = new Set<string>([
  241. 'zoom',
  242. 'centerTileX',
  243. 'centerTileY',
  244. 'tileSizePx',
  245. 'previewScale',
  246. 'stageWidth',
  247. 'stageHeight',
  248. 'stageLeft',
  249. 'stageTop',
  250. ])
  251. const SIDE_BUTTON_DEP_KEYS = new Set<string>([
  252. 'sideButtonMode',
  253. 'showGameInfoPanel',
  254. 'showCenterScaleRuler',
  255. 'centerScaleRulerAnchorMode',
  256. 'skipButtonEnabled',
  257. 'gameSessionStatus',
  258. 'gpsLockEnabled',
  259. 'gpsLockAvailable',
  260. ])
  261. function hasAnyPatchKey(patch: Record<string, unknown>, keys: Set<string>): boolean {
  262. return Object.keys(patch).some((key) => keys.has(key))
  263. }
  264. function filterDebugOnlyPatch(
  265. patch: Partial<MapPageData>,
  266. includeDebugFields: boolean,
  267. includeRulerFields: boolean,
  268. ): Partial<MapPageData> {
  269. if (includeDebugFields && includeRulerFields) {
  270. return patch
  271. }
  272. const filteredPatch: Partial<MapPageData> = {}
  273. for (const [key, value] of Object.entries(patch)) {
  274. if (!includeDebugFields && DEBUG_ONLY_VIEW_KEYS.has(key)) {
  275. continue
  276. }
  277. if (!includeRulerFields && RULER_ONLY_VIEW_KEYS.has(key)) {
  278. continue
  279. }
  280. {
  281. ;(filteredPatch as Record<string, unknown>)[key] = value
  282. }
  283. }
  284. return filteredPatch
  285. }
  286. function clearGameInfoPanelSyncTimer() {
  287. if (gameInfoPanelSyncTimer) {
  288. clearTimeout(gameInfoPanelSyncTimer)
  289. gameInfoPanelSyncTimer = 0
  290. }
  291. }
  292. function clearCenterScaleRulerSyncTimer() {
  293. if (centerScaleRulerSyncTimer) {
  294. clearTimeout(centerScaleRulerSyncTimer)
  295. centerScaleRulerSyncTimer = 0
  296. }
  297. }
  298. function clearCenterScaleRulerUpdateTimer() {
  299. if (centerScaleRulerUpdateTimer) {
  300. clearTimeout(centerScaleRulerUpdateTimer)
  301. centerScaleRulerUpdateTimer = 0
  302. }
  303. }
  304. function clearPunchHintDismissTimer() {
  305. if (punchHintDismissTimer) {
  306. clearTimeout(punchHintDismissTimer)
  307. punchHintDismissTimer = 0
  308. }
  309. }
  310. function clearHudFxTimer(key: 'timer' | 'mileage' | 'speed' | 'heartRate') {
  311. const timerMap = {
  312. timer: panelTimerFxTimer,
  313. mileage: panelMileageFxTimer,
  314. speed: panelSpeedFxTimer,
  315. heartRate: panelHeartRateFxTimer,
  316. }
  317. const timer = timerMap[key]
  318. if (timer) {
  319. clearTimeout(timer)
  320. }
  321. if (key === 'timer') {
  322. panelTimerFxTimer = 0
  323. } else if (key === 'mileage') {
  324. panelMileageFxTimer = 0
  325. } else if (key === 'speed') {
  326. panelSpeedFxTimer = 0
  327. } else {
  328. panelHeartRateFxTimer = 0
  329. }
  330. }
  331. function updateCenterScaleRulerInputCache(patch: Partial<MapPageData>) {
  332. for (const key of CENTER_SCALE_RULER_CACHE_KEYS) {
  333. if (Object.prototype.hasOwnProperty.call(patch, key)) {
  334. ;(centerScaleRulerInputCache as Record<string, unknown>)[key] =
  335. (patch as Record<string, unknown>)[key]
  336. }
  337. }
  338. }
  339. function loadStoredUserSettings(): StoredUserSettings {
  340. try {
  341. const stored = wx.getStorageSync(USER_SETTINGS_STORAGE_KEY)
  342. if (!stored || typeof stored !== 'object') {
  343. return {}
  344. }
  345. const normalized = stored as Record<string, unknown>
  346. const settings: StoredUserSettings = {}
  347. if (normalized.animationLevel === 'standard' || normalized.animationLevel === 'lite') {
  348. settings.animationLevel = normalized.animationLevel
  349. }
  350. if (normalized.northReferenceMode === 'magnetic' || normalized.northReferenceMode === 'true') {
  351. settings.northReferenceMode = normalized.northReferenceMode
  352. }
  353. if (typeof normalized.autoRotateEnabled === 'boolean') {
  354. settings.autoRotateEnabled = normalized.autoRotateEnabled
  355. }
  356. if (normalized.compassTuningProfile === 'smooth' || normalized.compassTuningProfile === 'balanced' || normalized.compassTuningProfile === 'responsive') {
  357. settings.compassTuningProfile = normalized.compassTuningProfile
  358. }
  359. if (normalized.sideButtonPlacement === 'left' || normalized.sideButtonPlacement === 'right') {
  360. settings.sideButtonPlacement = normalized.sideButtonPlacement
  361. }
  362. if (typeof normalized.showCenterScaleRuler === 'boolean') {
  363. settings.showCenterScaleRuler = normalized.showCenterScaleRuler
  364. }
  365. if (normalized.centerScaleRulerAnchorMode === 'screen-center' || normalized.centerScaleRulerAnchorMode === 'compass-center') {
  366. settings.centerScaleRulerAnchorMode = normalized.centerScaleRulerAnchorMode
  367. }
  368. if (typeof normalized.lockAnimationLevel === 'boolean') {
  369. settings.lockAnimationLevel = normalized.lockAnimationLevel
  370. }
  371. if (typeof normalized.lockSideButtonPlacement === 'boolean') {
  372. settings.lockSideButtonPlacement = normalized.lockSideButtonPlacement
  373. }
  374. if (typeof normalized.lockAutoRotate === 'boolean') {
  375. settings.lockAutoRotate = normalized.lockAutoRotate
  376. }
  377. if (typeof normalized.lockCompassTuning === 'boolean') {
  378. settings.lockCompassTuning = normalized.lockCompassTuning
  379. }
  380. if (typeof normalized.lockScaleRulerVisible === 'boolean') {
  381. settings.lockScaleRulerVisible = normalized.lockScaleRulerVisible
  382. }
  383. if (typeof normalized.lockScaleRulerAnchor === 'boolean') {
  384. settings.lockScaleRulerAnchor = normalized.lockScaleRulerAnchor
  385. }
  386. if (typeof normalized.lockNorthReference === 'boolean') {
  387. settings.lockNorthReference = normalized.lockNorthReference
  388. }
  389. if (typeof normalized.lockHeartRateDevice === 'boolean') {
  390. settings.lockHeartRateDevice = normalized.lockHeartRateDevice
  391. }
  392. return settings
  393. } catch {
  394. return {}
  395. }
  396. }
  397. function persistStoredUserSettings(settings: StoredUserSettings) {
  398. try {
  399. wx.setStorageSync(USER_SETTINGS_STORAGE_KEY, settings)
  400. } catch {}
  401. }
  402. function toggleStoredSettingLock(settings: StoredUserSettings, key: SettingLockKey): StoredUserSettings {
  403. return {
  404. ...settings,
  405. [key]: !settings[key],
  406. }
  407. }
  408. function buildSideButtonVisibility(mode: SideButtonMode) {
  409. return {
  410. sideButtonMode: mode,
  411. showLeftButtonGroup: mode === 'shown',
  412. showRightButtonGroups: false,
  413. showBottomDebugButton: true,
  414. }
  415. }
  416. function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
  417. return currentMode === 'shown' ? 'hidden' : 'shown'
  418. }
  419. function buildCompassTicks(): CompassTickData[] {
  420. const ticks: CompassTickData[] = []
  421. for (let angle = 0; angle < 360; angle += 5) {
  422. ticks.push({
  423. angle,
  424. long: angle % 15 === 0,
  425. major: angle % 45 === 0,
  426. })
  427. }
  428. return ticks
  429. }
  430. function buildCompassLabels(): CompassLabelData[] {
  431. return [
  432. { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
  433. { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
  434. { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  435. { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  436. { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  437. { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  438. { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  439. { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
  440. ]
  441. }
  442. function getFallbackStageRect(): MapEngineStageRect {
  443. const systemInfo = wx.getSystemInfoSync()
  444. const width = Math.max(320, systemInfo.windowWidth)
  445. const height = Math.max(280, systemInfo.windowHeight)
  446. return {
  447. width,
  448. height,
  449. left: 0,
  450. top: 0,
  451. }
  452. }
  453. function getSideToggleIconSrc(mode: SideButtonMode): string {
  454. if (mode === 'hidden') {
  455. return '../../assets/btn_more1.png'
  456. }
  457. return '../../assets/btn_more3.png'
  458. }
  459. function getSideActionButtonClass(state: SideActionButtonState): string {
  460. if (state === 'muted') {
  461. return 'map-side-button map-side-button--muted'
  462. }
  463. if (state === 'active') {
  464. return 'map-side-button map-side-button--active'
  465. }
  466. return 'map-side-button map-side-button--default'
  467. }
  468. function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showSystemSettingsPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
  469. const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
  470. ? 'muted'
  471. : data.gpsLockEnabled
  472. ? 'active'
  473. : 'default'
  474. const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'idle' ? 'default' : 'active'
  475. const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
  476. const sideButton12State: SideActionButtonState = data.showSystemSettingsPanel ? 'active' : 'default'
  477. const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
  478. const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
  479. ? 'muted'
  480. : data.centerScaleRulerAnchorMode === 'compass-center'
  481. ? 'active'
  482. : 'default'
  483. const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'
  484. return {
  485. sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
  486. sideButton2Class: getSideActionButtonClass(sideButton2State),
  487. sideButton4Class: getSideActionButtonClass(sideButton4State),
  488. sideButton11Class: getSideActionButtonClass(sideButton11State),
  489. sideButton12Class: getSideActionButtonClass(sideButton12State),
  490. sideButton13Class: getSideActionButtonClass(sideButton13State),
  491. sideButton14Class: getSideActionButtonClass(sideButton14State),
  492. sideButton16Class: getSideActionButtonClass(sideButton16State),
  493. }
  494. }
  495. function getRpxUnitInPx(): number {
  496. const systemInfo = wx.getSystemInfoSync()
  497. return systemInfo.windowWidth / 750
  498. }
  499. function worldTileYToLat(worldTileY: number, zoom: number): number {
  500. const scale = Math.pow(2, zoom)
  501. const n = Math.PI - (2 * Math.PI * worldTileY) / scale
  502. return (180 / Math.PI) * Math.atan(Math.sinh(n))
  503. }
  504. function getNiceDistanceMeters(rawDistanceMeters: number): number {
  505. if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
  506. return 50
  507. }
  508. const exponent = Math.floor(Math.log10(rawDistanceMeters))
  509. const base = Math.pow(10, exponent)
  510. const normalized = rawDistanceMeters / base
  511. if (normalized <= 1) {
  512. return base
  513. }
  514. if (normalized <= 2) {
  515. return 2 * base
  516. }
  517. if (normalized <= 5) {
  518. return 5 * base
  519. }
  520. return 10 * base
  521. }
  522. function formatScaleDistanceLabel(distanceMeters: number): string {
  523. if (distanceMeters >= 1000) {
  524. const distanceKm = distanceMeters / 1000
  525. const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
  526. return `${formatted.replace(/\.0$/, '')} km`
  527. }
  528. return `${Math.round(distanceMeters)} m`
  529. }
  530. function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
  531. if (!data.showCenterScaleRuler) {
  532. lastCenterScaleRulerStablePatch = {
  533. centerScaleRulerVisible: false,
  534. centerScaleRulerCenterXPx: 0,
  535. centerScaleRulerZeroYPx: 0,
  536. centerScaleRulerHeightPx: 0,
  537. centerScaleRulerAxisBottomPx: 0,
  538. centerScaleRulerZeroVisible: false,
  539. centerScaleRulerZeroLabel: '0 m',
  540. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  541. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  542. }
  543. return { ...lastCenterScaleRulerStablePatch }
  544. }
  545. if (!data.stageWidth || !data.stageHeight) {
  546. return { ...lastCenterScaleRulerStablePatch }
  547. }
  548. const topPadding = 12
  549. const rpxUnitPx = getRpxUnitInPx()
  550. const compassBottomPaddingPx = 248 * rpxUnitPx
  551. const compassDialRadiusPx = (196 * rpxUnitPx) / 2
  552. const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
  553. const compassOcclusionPaddingPx = 10 * rpxUnitPx
  554. const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
  555. ? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
  556. : Math.round(data.stageHeight / 2)
  557. const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
  558. const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
  559. ? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
  560. : 0
  561. if (
  562. !data.tileSizePx
  563. || !Number.isFinite(data.zoom)
  564. || !Number.isFinite(data.centerTileY)
  565. ) {
  566. return {
  567. ...lastCenterScaleRulerStablePatch,
  568. centerScaleRulerVisible: true,
  569. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  570. centerScaleRulerZeroYPx: zeroYPx,
  571. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  572. centerScaleRulerAxisBottomPx: coveredBottomPx,
  573. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  574. }
  575. }
  576. const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
  577. const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
  578. const metersPerPixel = metersPerTile / data.tileSizePx
  579. const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
  580. const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
  581. const rulerHeight = Math.floor(zeroYPx - topPadding)
  582. if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
  583. return {
  584. ...lastCenterScaleRulerStablePatch,
  585. centerScaleRulerVisible: true,
  586. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  587. centerScaleRulerZeroYPx: zeroYPx,
  588. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  589. centerScaleRulerAxisBottomPx: coveredBottomPx,
  590. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  591. }
  592. }
  593. const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
  594. const minorDistanceMeters = labelDistanceMeters / 8
  595. const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
  596. const visibleTopLimitPx = rulerHeight - coveredBottomPx
  597. const minorTicks: ScaleRulerMinorTickData[] = []
  598. const majorMarks: ScaleRulerMajorMarkData[] = []
  599. for (let index = 1; index <= 200; index += 1) {
  600. const topPx = Math.round(rulerHeight - index * minorStepPx)
  601. if (topPx < 0) {
  602. break
  603. }
  604. if (topPx >= visibleTopLimitPx) {
  605. continue
  606. }
  607. const isHalfMajor = index % 4 === 0
  608. const isLabelMajor = index % 8 === 0
  609. minorTicks.push({
  610. key: `minor-${index}`,
  611. topPx,
  612. long: isHalfMajor,
  613. })
  614. if (isLabelMajor) {
  615. majorMarks.push({
  616. key: `major-${index}`,
  617. topPx,
  618. label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
  619. })
  620. }
  621. }
  622. lastCenterScaleRulerStablePatch = {
  623. centerScaleRulerVisible: true,
  624. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  625. centerScaleRulerZeroYPx: zeroYPx,
  626. centerScaleRulerHeightPx: rulerHeight,
  627. centerScaleRulerAxisBottomPx: coveredBottomPx,
  628. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  629. centerScaleRulerZeroLabel: '0 m',
  630. centerScaleRulerMinorTicks: minorTicks,
  631. centerScaleRulerMajorMarks: majorMarks,
  632. }
  633. return { ...lastCenterScaleRulerStablePatch }
  634. }
  635. function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
  636. return {
  637. title: '当前游戏',
  638. subtitle: '未开始',
  639. localRows: [],
  640. globalRows: [
  641. { label: '全球积分', value: '未接入' },
  642. { label: '全球排名', value: '未接入' },
  643. { label: '在线人数', value: '未接入' },
  644. { label: '队伍状态', value: '未接入' },
  645. { label: '实时广播', value: '未接入' },
  646. ],
  647. }
  648. }
  649. Page({
  650. data: {
  651. showDebugPanel: false,
  652. showGameInfoPanel: false,
  653. showSystemSettingsPanel: false,
  654. showCenterScaleRuler: false,
  655. statusBarHeight: 0,
  656. topInsetHeight: 12,
  657. hudPanelIndex: 0,
  658. configSourceText: '顺序赛配置',
  659. centerScaleRulerAnchorMode: 'screen-center',
  660. autoRotateEnabled: false,
  661. lockAnimationLevel: false,
  662. lockSideButtonPlacement: false,
  663. lockAutoRotate: false,
  664. lockCompassTuning: false,
  665. lockScaleRulerVisible: false,
  666. lockScaleRulerAnchor: false,
  667. lockNorthReference: false,
  668. lockHeartRateDevice: false,
  669. gameInfoTitle: '当前游戏',
  670. gameInfoSubtitle: '未开始',
  671. gameInfoLocalRows: [],
  672. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  673. panelTimerText: '00:00:00',
  674. panelMileageText: '0m',
  675. panelActionTagText: '目标',
  676. panelDistanceTagText: '点距',
  677. panelDistanceValueText: '--',
  678. panelDistanceUnitText: '',
  679. panelProgressText: '0/0',
  680. showPunchHintBanner: true,
  681. sideButtonPlacement: 'left',
  682. gameSessionStatus: 'idle',
  683. gameModeText: '顺序赛',
  684. gpsLockEnabled: false,
  685. gpsLockAvailable: false,
  686. locationSourceMode: 'real',
  687. locationSourceText: '真实定位',
  688. mockBridgeConnected: false,
  689. mockBridgeStatusText: '未连接',
  690. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  691. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  692. mockCoordText: '--',
  693. mockSpeedText: '--',
  694. heartRateSourceMode: 'real',
  695. heartRateSourceText: '真实心率',
  696. mockHeartRateBridgeConnected: false,
  697. mockHeartRateBridgeStatusText: '未连接',
  698. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  699. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  700. mockHeartRateText: '--',
  701. heartRateScanText: '未扫描',
  702. heartRateDiscoveredDevices: [],
  703. panelSpeedValueText: '0',
  704. panelTelemetryTone: 'blue',
  705. panelHeartRateZoneNameText: '--',
  706. panelHeartRateZoneRangeText: '',
  707. heartRateConnected: false,
  708. heartRateStatusText: '心率带未连接',
  709. heartRateDeviceText: '--',
  710. panelHeartRateValueText: '--',
  711. panelHeartRateUnitText: '',
  712. panelCaloriesValueText: '0',
  713. panelCaloriesUnitText: 'kcal',
  714. panelAverageSpeedValueText: '0',
  715. panelAverageSpeedUnitText: 'km/h',
  716. panelAccuracyValueText: '--',
  717. panelAccuracyUnitText: '',
  718. deviceHeadingText: '--',
  719. devicePoseText: '竖持',
  720. headingConfidenceText: '低',
  721. accelerometerText: '--',
  722. gyroscopeText: '--',
  723. deviceMotionText: '--',
  724. compassSourceText: '无数据',
  725. compassTuningProfile: 'balanced',
  726. compassTuningProfileText: '平衡',
  727. punchButtonText: '打点',
  728. punchButtonEnabled: false,
  729. skipButtonEnabled: false,
  730. punchHintText: '等待进入检查点范围',
  731. punchFeedbackVisible: false,
  732. punchFeedbackText: '',
  733. punchFeedbackTone: 'neutral',
  734. contentCardVisible: false,
  735. contentCardTitle: '',
  736. contentCardBody: '',
  737. punchButtonFxClass: '',
  738. panelProgressFxClass: '',
  739. panelDistanceFxClass: '',
  740. punchFeedbackFxClass: '',
  741. contentCardFxClass: '',
  742. mapPulseVisible: false,
  743. mapPulseLeftPx: 0,
  744. mapPulseTopPx: 0,
  745. mapPulseFxClass: '',
  746. stageFxVisible: false,
  747. stageFxClass: '',
  748. centerScaleRulerVisible: false,
  749. centerScaleRulerCenterXPx: 0,
  750. centerScaleRulerZeroYPx: 0,
  751. centerScaleRulerHeightPx: 0,
  752. centerScaleRulerAxisBottomPx: 0,
  753. centerScaleRulerZeroVisible: false,
  754. centerScaleRulerZeroLabel: '0 m',
  755. centerScaleRulerMinorTicks: [],
  756. centerScaleRulerMajorMarks: [],
  757. compassTicks: buildCompassTicks(),
  758. compassLabels: buildCompassLabels(),
  759. ...buildSideButtonVisibility('shown'),
  760. ...buildSideButtonState({
  761. sideButtonMode: 'shown',
  762. showGameInfoPanel: false,
  763. showSystemSettingsPanel: false,
  764. showCenterScaleRuler: false,
  765. centerScaleRulerAnchorMode: 'screen-center',
  766. skipButtonEnabled: false,
  767. gameSessionStatus: 'idle',
  768. gpsLockEnabled: false,
  769. gpsLockAvailable: false,
  770. }),
  771. } as unknown as MapPageData,
  772. onLoad() {
  773. const systemInfo = wx.getSystemInfoSync()
  774. const statusBarHeight = systemInfo.statusBarHeight || 0
  775. const menuButtonRect = wx.getMenuButtonBoundingClientRect()
  776. const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
  777. if (mapEngine) {
  778. mapEngine.destroy()
  779. mapEngine = null
  780. }
  781. mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
  782. onData: (patch) => {
  783. const nextPatch = patch as Partial<MapPageData>
  784. const includeDebugFields = this.data.showDebugPanel
  785. const includeRulerFields = this.data.showCenterScaleRuler
  786. const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
  787. ...nextPatch,
  788. }, includeDebugFields, includeRulerFields)
  789. if (
  790. typeof nextPatch.mockBridgeUrlText === 'string'
  791. && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
  792. ) {
  793. nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
  794. }
  795. if (
  796. typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
  797. && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
  798. ) {
  799. nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
  800. }
  801. updateCenterScaleRulerInputCache(nextPatch)
  802. const mergedData = {
  803. ...centerScaleRulerInputCache,
  804. ...this.data,
  805. ...nextData,
  806. } as MapPageData
  807. const derivedPatch: Partial<MapPageData> = {}
  808. if (typeof nextPatch.orientationMode === 'string') {
  809. nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up'
  810. }
  811. if (
  812. this.data.showCenterScaleRuler
  813. && hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
  814. ) {
  815. clearCenterScaleRulerUpdateTimer()
  816. Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
  817. }
  818. if (hasAnyPatchKey(nextPatch as Record<string, unknown>, SIDE_BUTTON_DEP_KEYS)) {
  819. Object.assign(derivedPatch, buildSideButtonState(mergedData))
  820. }
  821. if (typeof nextPatch.punchHintText === 'string') {
  822. const nextHintText = nextPatch.punchHintText.trim()
  823. if (nextHintText !== this.data.punchHintText) {
  824. clearPunchHintDismissTimer()
  825. nextData.showPunchHintBanner = nextHintText.length > 0
  826. if (nextHintText.length > 0) {
  827. punchHintDismissTimer = setTimeout(() => {
  828. punchHintDismissTimer = 0
  829. this.setData({
  830. showPunchHintBanner: false,
  831. })
  832. }, PUNCH_HINT_AUTO_HIDE_MS) as unknown as number
  833. }
  834. } else if (!nextHintText) {
  835. clearPunchHintDismissTimer()
  836. nextData.showPunchHintBanner = false
  837. }
  838. }
  839. const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
  840. ? nextPatch.animationLevel
  841. : this.data.animationLevel
  842. if (nextAnimationLevel === 'lite') {
  843. clearHudFxTimer('timer')
  844. clearHudFxTimer('mileage')
  845. clearHudFxTimer('speed')
  846. clearHudFxTimer('heartRate')
  847. nextData.panelTimerFxClass = ''
  848. nextData.panelMileageFxClass = ''
  849. nextData.panelSpeedFxClass = ''
  850. nextData.panelHeartRateFxClass = ''
  851. } else {
  852. if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
  853. clearHudFxTimer('timer')
  854. nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
  855. panelTimerFxTimer = setTimeout(() => {
  856. panelTimerFxTimer = 0
  857. this.setData({ panelTimerFxClass: '' })
  858. }, 320) as unknown as number
  859. }
  860. if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
  861. clearHudFxTimer('mileage')
  862. nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
  863. panelMileageFxTimer = setTimeout(() => {
  864. panelMileageFxTimer = 0
  865. this.setData({ panelMileageFxClass: '' })
  866. }, 360) as unknown as number
  867. }
  868. if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
  869. clearHudFxTimer('speed')
  870. nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
  871. panelSpeedFxTimer = setTimeout(() => {
  872. panelSpeedFxTimer = 0
  873. this.setData({ panelSpeedFxClass: '' })
  874. }, 360) as unknown as number
  875. }
  876. if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
  877. clearHudFxTimer('heartRate')
  878. nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
  879. panelHeartRateFxTimer = setTimeout(() => {
  880. panelHeartRateFxTimer = 0
  881. this.setData({ panelHeartRateFxClass: '' })
  882. }, 400) as unknown as number
  883. }
  884. }
  885. if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
  886. this.setData({
  887. ...nextData,
  888. ...derivedPatch,
  889. })
  890. }
  891. if (this.data.showGameInfoPanel) {
  892. this.scheduleGameInfoPanelSnapshotSync()
  893. }
  894. },
  895. })
  896. const storedUserSettings = loadStoredUserSettings()
  897. if (storedUserSettings.animationLevel) {
  898. mapEngine.handleSetAnimationLevel(storedUserSettings.animationLevel)
  899. }
  900. const initialAutoRotateEnabled = storedUserSettings.autoRotateEnabled !== false
  901. if (initialAutoRotateEnabled) {
  902. mapEngine.handleSetHeadingUpMode()
  903. } else {
  904. mapEngine.handleSetManualMode()
  905. }
  906. if (storedUserSettings.compassTuningProfile) {
  907. mapEngine.handleSetCompassTuningProfile(storedUserSettings.compassTuningProfile)
  908. }
  909. if (storedUserSettings.northReferenceMode) {
  910. mapEngine.handleSetNorthReferenceMode(storedUserSettings.northReferenceMode)
  911. }
  912. const initialSideButtonPlacement = storedUserSettings.sideButtonPlacement || 'left'
  913. mapEngine.setDiagnosticUiEnabled(false)
  914. centerScaleRulerInputCache = {
  915. stageWidth: 0,
  916. stageHeight: 0,
  917. zoom: 0,
  918. centerTileY: 0,
  919. tileSizePx: 0,
  920. previewScale: 1,
  921. }
  922. const initialShowCenterScaleRuler = !!storedUserSettings.showCenterScaleRuler
  923. const initialCenterScaleRulerAnchorMode = storedUserSettings.centerScaleRulerAnchorMode || 'screen-center'
  924. this.setData({
  925. ...mapEngine.getInitialData(),
  926. showDebugPanel: false,
  927. showGameInfoPanel: false,
  928. showSystemSettingsPanel: false,
  929. showCenterScaleRuler: initialShowCenterScaleRuler,
  930. statusBarHeight,
  931. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  932. hudPanelIndex: 0,
  933. configSourceText: '顺序赛配置',
  934. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  935. autoRotateEnabled: initialAutoRotateEnabled,
  936. lockAnimationLevel: !!storedUserSettings.lockAnimationLevel,
  937. lockSideButtonPlacement: !!storedUserSettings.lockSideButtonPlacement,
  938. lockAutoRotate: !!storedUserSettings.lockAutoRotate,
  939. lockCompassTuning: !!storedUserSettings.lockCompassTuning,
  940. lockScaleRulerVisible: !!storedUserSettings.lockScaleRulerVisible,
  941. lockScaleRulerAnchor: !!storedUserSettings.lockScaleRulerAnchor,
  942. lockNorthReference: !!storedUserSettings.lockNorthReference,
  943. lockHeartRateDevice: !!storedUserSettings.lockHeartRateDevice,
  944. sideButtonPlacement: initialSideButtonPlacement,
  945. gameInfoTitle: '当前游戏',
  946. gameInfoSubtitle: '未开始',
  947. gameInfoLocalRows: [],
  948. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  949. panelTimerText: '00:00:00',
  950. panelTimerFxClass: '',
  951. panelMileageText: '0m',
  952. panelMileageFxClass: '',
  953. panelActionTagText: '目标',
  954. panelDistanceTagText: '点距',
  955. panelDistanceValueText: '--',
  956. panelDistanceUnitText: '',
  957. panelProgressText: '0/0',
  958. showPunchHintBanner: true,
  959. gameSessionStatus: 'idle',
  960. gameModeText: '顺序赛',
  961. gpsLockEnabled: false,
  962. gpsLockAvailable: false,
  963. locationSourceMode: 'real',
  964. locationSourceText: '真实定位',
  965. mockBridgeConnected: false,
  966. mockBridgeStatusText: '未连接',
  967. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  968. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  969. mockCoordText: '--',
  970. mockSpeedText: '--',
  971. heartRateSourceMode: 'real',
  972. heartRateSourceText: '真实心率',
  973. mockHeartRateBridgeConnected: false,
  974. mockHeartRateBridgeStatusText: '未连接',
  975. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  976. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  977. mockHeartRateText: '--',
  978. panelSpeedValueText: '0',
  979. panelSpeedFxClass: '',
  980. panelTelemetryTone: 'blue',
  981. panelHeartRateZoneNameText: '--',
  982. panelHeartRateZoneRangeText: '',
  983. heartRateConnected: false,
  984. heartRateStatusText: '心率带未连接',
  985. heartRateDeviceText: '--',
  986. panelHeartRateValueText: '--',
  987. panelHeartRateFxClass: '',
  988. panelHeartRateUnitText: '',
  989. panelCaloriesValueText: '0',
  990. panelCaloriesUnitText: 'kcal',
  991. panelAverageSpeedValueText: '0',
  992. panelAverageSpeedUnitText: 'km/h',
  993. panelAccuracyValueText: '--',
  994. panelAccuracyUnitText: '',
  995. deviceHeadingText: '--',
  996. devicePoseText: '竖持',
  997. headingConfidenceText: '低',
  998. accelerometerText: '--',
  999. gyroscopeText: '--',
  1000. deviceMotionText: '--',
  1001. compassSourceText: '无数据',
  1002. compassTuningProfile: 'balanced',
  1003. compassTuningProfileText: '平衡',
  1004. punchButtonText: '打点',
  1005. punchButtonEnabled: false,
  1006. skipButtonEnabled: false,
  1007. punchHintText: '等待进入检查点范围',
  1008. punchFeedbackVisible: false,
  1009. punchFeedbackText: '',
  1010. punchFeedbackTone: 'neutral',
  1011. contentCardVisible: false,
  1012. contentCardTitle: '',
  1013. contentCardBody: '',
  1014. punchButtonFxClass: '',
  1015. panelProgressFxClass: '',
  1016. panelDistanceFxClass: '',
  1017. punchFeedbackFxClass: '',
  1018. contentCardFxClass: '',
  1019. mapPulseVisible: false,
  1020. mapPulseLeftPx: 0,
  1021. mapPulseTopPx: 0,
  1022. mapPulseFxClass: '',
  1023. stageFxVisible: false,
  1024. stageFxClass: '',
  1025. compassTicks: buildCompassTicks(),
  1026. compassLabels: buildCompassLabels(),
  1027. ...buildSideButtonVisibility('shown'),
  1028. ...buildSideButtonState({
  1029. sideButtonMode: 'shown',
  1030. showGameInfoPanel: false,
  1031. showSystemSettingsPanel: false,
  1032. showCenterScaleRuler: initialShowCenterScaleRuler,
  1033. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  1034. skipButtonEnabled: false,
  1035. gameSessionStatus: 'idle',
  1036. gpsLockEnabled: false,
  1037. gpsLockAvailable: false,
  1038. }),
  1039. ...buildCenterScaleRulerPatch({
  1040. ...(mapEngine.getInitialData() as MapPageData),
  1041. showCenterScaleRuler: initialShowCenterScaleRuler,
  1042. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  1043. stageWidth: 0,
  1044. stageHeight: 0,
  1045. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  1046. zoom: 0,
  1047. centerTileY: 0,
  1048. tileSizePx: 0,
  1049. }),
  1050. })
  1051. },
  1052. onReady() {
  1053. stageCanvasAttached = false
  1054. this.measureStageAndCanvas()
  1055. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  1056. },
  1057. onShow() {
  1058. if (mapEngine) {
  1059. mapEngine.handleAppShow()
  1060. }
  1061. },
  1062. onHide() {
  1063. if (mapEngine) {
  1064. mapEngine.handleAppHide()
  1065. }
  1066. },
  1067. onUnload() {
  1068. clearGameInfoPanelSyncTimer()
  1069. clearCenterScaleRulerSyncTimer()
  1070. clearCenterScaleRulerUpdateTimer()
  1071. clearPunchHintDismissTimer()
  1072. clearHudFxTimer('timer')
  1073. clearHudFxTimer('mileage')
  1074. clearHudFxTimer('speed')
  1075. clearHudFxTimer('heartRate')
  1076. if (mapEngine) {
  1077. mapEngine.destroy()
  1078. mapEngine = null
  1079. }
  1080. stageCanvasAttached = false
  1081. },
  1082. loadMapConfigFromRemote(configUrl: string, configLabel: string) {
  1083. const currentEngine = mapEngine
  1084. if (!currentEngine) {
  1085. return
  1086. }
  1087. this.setData({
  1088. configSourceText: configLabel,
  1089. configStatusText: `加载中: ${configLabel}`,
  1090. })
  1091. loadRemoteMapConfig(configUrl)
  1092. .then((config) => {
  1093. if (mapEngine !== currentEngine) {
  1094. return
  1095. }
  1096. currentEngine.applyRemoteMapConfig(config)
  1097. })
  1098. .catch((error) => {
  1099. if (mapEngine !== currentEngine) {
  1100. return
  1101. }
  1102. const errorMessage = error && error.message ? error.message : '未知错误'
  1103. this.setData({
  1104. configStatusText: `载入失败: ${errorMessage}`,
  1105. statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
  1106. })
  1107. })
  1108. },
  1109. measureStageAndCanvas(onApplied?: () => void) {
  1110. const page = this
  1111. const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
  1112. const fallbackRect = getFallbackStageRect()
  1113. const rect: MapEngineStageRect = {
  1114. width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
  1115. height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
  1116. left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
  1117. top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
  1118. }
  1119. const currentEngine = mapEngine
  1120. if (!currentEngine) {
  1121. return
  1122. }
  1123. currentEngine.setStage(rect)
  1124. if (onApplied) {
  1125. onApplied()
  1126. }
  1127. if (stageCanvasAttached) {
  1128. return
  1129. }
  1130. const canvasQuery = wx.createSelectorQuery().in(page)
  1131. canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
  1132. canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
  1133. canvasQuery.exec((canvasRes) => {
  1134. const canvasRef = canvasRes[0] as any
  1135. const labelCanvasRef = canvasRes[1] as any
  1136. if (!canvasRef || !canvasRef.node) {
  1137. page.setData({
  1138. statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
  1139. })
  1140. return
  1141. }
  1142. const dpr = wx.getSystemInfoSync().pixelRatio || 1
  1143. try {
  1144. currentEngine.attachCanvas(
  1145. canvasRef.node,
  1146. rect.width,
  1147. rect.height,
  1148. dpr,
  1149. labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
  1150. )
  1151. stageCanvasAttached = true
  1152. } catch (error) {
  1153. page.setData({
  1154. statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
  1155. })
  1156. }
  1157. })
  1158. }
  1159. const query = wx.createSelectorQuery().in(page)
  1160. query.select('.map-stage').boundingClientRect()
  1161. query.exec((res) => {
  1162. const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
  1163. applyStage(rect)
  1164. })
  1165. },
  1166. handleTouchStart(event: WechatMiniprogram.TouchEvent) {
  1167. if (mapEngine) {
  1168. mapEngine.handleTouchStart(event)
  1169. }
  1170. },
  1171. handleTouchMove(event: WechatMiniprogram.TouchEvent) {
  1172. if (mapEngine) {
  1173. mapEngine.handleTouchMove(event)
  1174. }
  1175. },
  1176. handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
  1177. if (mapEngine) {
  1178. mapEngine.handleTouchEnd(event)
  1179. }
  1180. },
  1181. handleTouchCancel() {
  1182. if (mapEngine) {
  1183. mapEngine.handleTouchCancel()
  1184. }
  1185. },
  1186. handleRecenter() {
  1187. if (mapEngine) {
  1188. mapEngine.handleRecenter()
  1189. }
  1190. },
  1191. handleRotateStep() {
  1192. if (mapEngine) {
  1193. mapEngine.handleRotateStep()
  1194. }
  1195. },
  1196. handleRotationReset() {
  1197. if (mapEngine) {
  1198. mapEngine.handleRotationReset()
  1199. }
  1200. },
  1201. handleSetManualMode() {
  1202. if (mapEngine) {
  1203. mapEngine.handleSetManualMode()
  1204. }
  1205. },
  1206. handleSetNorthUpMode() {
  1207. if (mapEngine) {
  1208. mapEngine.handleSetNorthUpMode()
  1209. }
  1210. },
  1211. handleSetHeadingUpMode() {
  1212. if (mapEngine) {
  1213. mapEngine.handleSetHeadingUpMode()
  1214. }
  1215. },
  1216. handleCycleNorthReferenceMode() {
  1217. if (mapEngine) {
  1218. mapEngine.handleCycleNorthReferenceMode()
  1219. }
  1220. },
  1221. handleAutoRotateCalibrate() {
  1222. if (mapEngine) {
  1223. mapEngine.handleAutoRotateCalibrate()
  1224. }
  1225. },
  1226. handleToggleGpsTracking() {
  1227. if (mapEngine) {
  1228. mapEngine.handleToggleGpsTracking()
  1229. }
  1230. },
  1231. handleSetRealLocationMode() {
  1232. if (mapEngine) {
  1233. mapEngine.handleSetRealLocationMode()
  1234. }
  1235. },
  1236. handleSetMockLocationMode() {
  1237. if (mapEngine) {
  1238. mapEngine.handleSetMockLocationMode()
  1239. }
  1240. },
  1241. handleConnectMockLocationBridge() {
  1242. if (mapEngine) {
  1243. mapEngine.handleConnectMockLocationBridge()
  1244. }
  1245. },
  1246. handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
  1247. this.setData({
  1248. mockBridgeUrlDraft: event.detail.value,
  1249. })
  1250. },
  1251. handleSaveMockBridgeUrl() {
  1252. if (mapEngine) {
  1253. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  1254. }
  1255. },
  1256. handleDisconnectMockLocationBridge() {
  1257. if (mapEngine) {
  1258. mapEngine.handleDisconnectMockLocationBridge()
  1259. }
  1260. },
  1261. handleSetRealHeartRateMode() {
  1262. if (mapEngine) {
  1263. mapEngine.handleSetRealHeartRateMode()
  1264. }
  1265. },
  1266. handleSetMockHeartRateMode() {
  1267. if (mapEngine) {
  1268. mapEngine.handleSetMockHeartRateMode()
  1269. }
  1270. },
  1271. handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
  1272. this.setData({
  1273. mockHeartRateBridgeUrlDraft: event.detail.value,
  1274. })
  1275. },
  1276. handleSaveMockHeartRateBridgeUrl() {
  1277. if (mapEngine) {
  1278. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  1279. }
  1280. },
  1281. handleConnectMockHeartRateBridge() {
  1282. if (mapEngine) {
  1283. mapEngine.handleConnectMockHeartRateBridge()
  1284. }
  1285. },
  1286. handleDisconnectMockHeartRateBridge() {
  1287. if (mapEngine) {
  1288. mapEngine.handleDisconnectMockHeartRateBridge()
  1289. }
  1290. },
  1291. handleConnectHeartRate() {
  1292. if (mapEngine) {
  1293. mapEngine.handleConnectHeartRate()
  1294. }
  1295. },
  1296. handleDisconnectHeartRate() {
  1297. if (mapEngine) {
  1298. mapEngine.handleDisconnectHeartRate()
  1299. }
  1300. },
  1301. handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  1302. if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
  1303. mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
  1304. }
  1305. },
  1306. handleClearPreferredHeartRateDevice() {
  1307. if (this.data.lockHeartRateDevice) {
  1308. return
  1309. }
  1310. if (mapEngine) {
  1311. mapEngine.handleClearPreferredHeartRateDevice()
  1312. }
  1313. },
  1314. handleDebugHeartRateBlue() {
  1315. if (mapEngine) {
  1316. mapEngine.handleDebugHeartRateTone('blue')
  1317. }
  1318. },
  1319. handleDebugHeartRatePurple() {
  1320. if (mapEngine) {
  1321. mapEngine.handleDebugHeartRateTone('purple')
  1322. }
  1323. },
  1324. handleDebugHeartRateGreen() {
  1325. if (mapEngine) {
  1326. mapEngine.handleDebugHeartRateTone('green')
  1327. }
  1328. },
  1329. handleDebugHeartRateYellow() {
  1330. if (mapEngine) {
  1331. mapEngine.handleDebugHeartRateTone('yellow')
  1332. }
  1333. },
  1334. handleDebugHeartRateOrange() {
  1335. if (mapEngine) {
  1336. mapEngine.handleDebugHeartRateTone('orange')
  1337. }
  1338. },
  1339. handleDebugHeartRateRed() {
  1340. if (mapEngine) {
  1341. mapEngine.handleDebugHeartRateTone('red')
  1342. }
  1343. },
  1344. handleClearDebugHeartRate() {
  1345. if (mapEngine) {
  1346. mapEngine.handleClearDebugHeartRate()
  1347. }
  1348. },
  1349. handleToggleOsmReference() {
  1350. if (mapEngine) {
  1351. mapEngine.handleToggleOsmReference()
  1352. }
  1353. },
  1354. handleStartGame() {
  1355. if (mapEngine) {
  1356. mapEngine.handleStartGame()
  1357. }
  1358. },
  1359. handleLoadClassicConfig() {
  1360. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  1361. },
  1362. handleLoadScoreOConfig() {
  1363. this.loadMapConfigFromRemote(SCORE_O_REMOTE_GAME_CONFIG_URL, '积分赛配置')
  1364. },
  1365. handleForceExitGame() {
  1366. if (!mapEngine || this.data.gameSessionStatus === 'idle') {
  1367. return
  1368. }
  1369. wx.showModal({
  1370. title: '确认退出',
  1371. content: '确认强制结束当前对局并返回开始前状态?',
  1372. confirmText: '确认退出',
  1373. cancelText: '取消',
  1374. success: (result) => {
  1375. if (result.confirm && mapEngine) {
  1376. mapEngine.handleForceExitGame()
  1377. }
  1378. },
  1379. })
  1380. },
  1381. handleSkipAction() {
  1382. if (!mapEngine || !this.data.skipButtonEnabled) {
  1383. return
  1384. }
  1385. if (!mapEngine.shouldConfirmSkipAction()) {
  1386. mapEngine.handleSkipAction()
  1387. return
  1388. }
  1389. wx.showModal({
  1390. title: '确认跳点',
  1391. content: '确认跳过当前检查点并切换到下一个目标点?',
  1392. confirmText: '确认跳过',
  1393. cancelText: '取消',
  1394. success: (result) => {
  1395. if (result.confirm && mapEngine) {
  1396. mapEngine.handleSkipAction()
  1397. }
  1398. },
  1399. })
  1400. },
  1401. handleClearMapTestArtifacts() {
  1402. if (mapEngine) {
  1403. mapEngine.handleClearMapTestArtifacts()
  1404. }
  1405. },
  1406. syncGameInfoPanelSnapshot() {
  1407. if (!mapEngine) {
  1408. return
  1409. }
  1410. const snapshot = mapEngine.getGameInfoSnapshot()
  1411. const localRows = snapshot.localRows.concat([
  1412. { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
  1413. { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
  1414. { label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
  1415. { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
  1416. { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
  1417. { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
  1418. { label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
  1419. { label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
  1420. ])
  1421. this.setData({
  1422. gameInfoTitle: snapshot.title,
  1423. gameInfoSubtitle: snapshot.subtitle,
  1424. gameInfoLocalRows: localRows,
  1425. gameInfoGlobalRows: snapshot.globalRows,
  1426. })
  1427. },
  1428. scheduleGameInfoPanelSnapshotSync() {
  1429. if (!this.data.showGameInfoPanel) {
  1430. clearGameInfoPanelSyncTimer()
  1431. return
  1432. }
  1433. if (gameInfoPanelSyncTimer) {
  1434. return
  1435. }
  1436. gameInfoPanelSyncTimer = setTimeout(() => {
  1437. gameInfoPanelSyncTimer = 0
  1438. if (this.data.showGameInfoPanel) {
  1439. this.syncGameInfoPanelSnapshot()
  1440. }
  1441. }, 400) as unknown as number
  1442. },
  1443. handleOpenGameInfoPanel() {
  1444. clearGameInfoPanelSyncTimer()
  1445. this.syncGameInfoPanelSnapshot()
  1446. this.setData({
  1447. showDebugPanel: false,
  1448. showSystemSettingsPanel: false,
  1449. showGameInfoPanel: true,
  1450. ...buildSideButtonState({
  1451. sideButtonMode: this.data.sideButtonMode,
  1452. showGameInfoPanel: true,
  1453. showSystemSettingsPanel: false,
  1454. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1455. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1456. skipButtonEnabled: this.data.skipButtonEnabled,
  1457. gameSessionStatus: this.data.gameSessionStatus,
  1458. gpsLockEnabled: this.data.gpsLockEnabled,
  1459. gpsLockAvailable: this.data.gpsLockAvailable,
  1460. }),
  1461. })
  1462. },
  1463. handleCloseGameInfoPanel() {
  1464. clearGameInfoPanelSyncTimer()
  1465. this.setData({
  1466. showGameInfoPanel: false,
  1467. ...buildSideButtonState({
  1468. sideButtonMode: this.data.sideButtonMode,
  1469. showGameInfoPanel: false,
  1470. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  1471. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1472. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1473. skipButtonEnabled: this.data.skipButtonEnabled,
  1474. gameSessionStatus: this.data.gameSessionStatus,
  1475. gpsLockEnabled: this.data.gpsLockEnabled,
  1476. gpsLockAvailable: this.data.gpsLockAvailable,
  1477. }),
  1478. })
  1479. },
  1480. handleGameInfoPanelTap() {},
  1481. handleOpenSystemSettingsPanel() {
  1482. clearGameInfoPanelSyncTimer()
  1483. this.setData({
  1484. showDebugPanel: false,
  1485. showGameInfoPanel: false,
  1486. showSystemSettingsPanel: true,
  1487. ...buildSideButtonState({
  1488. sideButtonMode: this.data.sideButtonMode,
  1489. showGameInfoPanel: false,
  1490. showSystemSettingsPanel: true,
  1491. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1492. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1493. skipButtonEnabled: this.data.skipButtonEnabled,
  1494. gameSessionStatus: this.data.gameSessionStatus,
  1495. gpsLockEnabled: this.data.gpsLockEnabled,
  1496. gpsLockAvailable: this.data.gpsLockAvailable,
  1497. }),
  1498. })
  1499. },
  1500. handleCloseSystemSettingsPanel() {
  1501. this.setData({
  1502. showSystemSettingsPanel: false,
  1503. ...buildSideButtonState({
  1504. sideButtonMode: this.data.sideButtonMode,
  1505. showGameInfoPanel: this.data.showGameInfoPanel,
  1506. showSystemSettingsPanel: false,
  1507. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1508. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1509. skipButtonEnabled: this.data.skipButtonEnabled,
  1510. gameSessionStatus: this.data.gameSessionStatus,
  1511. gpsLockEnabled: this.data.gpsLockEnabled,
  1512. gpsLockAvailable: this.data.gpsLockAvailable,
  1513. }),
  1514. })
  1515. },
  1516. handleSystemSettingsPanelTap() {},
  1517. handleSetAnimationLevelStandard() {
  1518. if (this.data.lockAnimationLevel || !mapEngine) {
  1519. return
  1520. }
  1521. mapEngine.handleSetAnimationLevel('standard')
  1522. persistStoredUserSettings({
  1523. ...loadStoredUserSettings(),
  1524. animationLevel: 'standard',
  1525. })
  1526. },
  1527. handleSetAnimationLevelLite() {
  1528. if (this.data.lockAnimationLevel || !mapEngine) {
  1529. return
  1530. }
  1531. mapEngine.handleSetAnimationLevel('lite')
  1532. persistStoredUserSettings({
  1533. ...loadStoredUserSettings(),
  1534. animationLevel: 'lite',
  1535. })
  1536. },
  1537. handleSetSideButtonPlacementLeft() {
  1538. if (this.data.lockSideButtonPlacement) {
  1539. return
  1540. }
  1541. this.setData({
  1542. sideButtonPlacement: 'left',
  1543. })
  1544. persistStoredUserSettings({
  1545. ...loadStoredUserSettings(),
  1546. sideButtonPlacement: 'left',
  1547. })
  1548. },
  1549. handleSetSideButtonPlacementRight() {
  1550. if (this.data.lockSideButtonPlacement) {
  1551. return
  1552. }
  1553. this.setData({
  1554. sideButtonPlacement: 'right',
  1555. })
  1556. persistStoredUserSettings({
  1557. ...loadStoredUserSettings(),
  1558. sideButtonPlacement: 'right',
  1559. })
  1560. },
  1561. handleSetAutoRotateEnabledOn() {
  1562. if (this.data.lockAutoRotate || !mapEngine) {
  1563. return
  1564. }
  1565. mapEngine.handleSetHeadingUpMode()
  1566. persistStoredUserSettings({
  1567. ...loadStoredUserSettings(),
  1568. autoRotateEnabled: true,
  1569. })
  1570. },
  1571. handleSetAutoRotateEnabledOff() {
  1572. if (this.data.lockAutoRotate || !mapEngine) {
  1573. return
  1574. }
  1575. mapEngine.handleSetManualMode()
  1576. persistStoredUserSettings({
  1577. ...loadStoredUserSettings(),
  1578. autoRotateEnabled: false,
  1579. })
  1580. },
  1581. handleSetCompassTuningSmooth() {
  1582. if (this.data.lockCompassTuning || !mapEngine) {
  1583. return
  1584. }
  1585. mapEngine.handleSetCompassTuningProfile('smooth')
  1586. persistStoredUserSettings({
  1587. ...loadStoredUserSettings(),
  1588. compassTuningProfile: 'smooth',
  1589. })
  1590. },
  1591. handleSetCompassTuningBalanced() {
  1592. if (this.data.lockCompassTuning || !mapEngine) {
  1593. return
  1594. }
  1595. mapEngine.handleSetCompassTuningProfile('balanced')
  1596. persistStoredUserSettings({
  1597. ...loadStoredUserSettings(),
  1598. compassTuningProfile: 'balanced',
  1599. })
  1600. },
  1601. handleSetCompassTuningResponsive() {
  1602. if (this.data.lockCompassTuning || !mapEngine) {
  1603. return
  1604. }
  1605. mapEngine.handleSetCompassTuningProfile('responsive')
  1606. persistStoredUserSettings({
  1607. ...loadStoredUserSettings(),
  1608. compassTuningProfile: 'responsive',
  1609. })
  1610. },
  1611. handleSetNorthReferenceMagnetic() {
  1612. if (this.data.lockNorthReference || !mapEngine) {
  1613. return
  1614. }
  1615. mapEngine.handleSetNorthReferenceMode('magnetic')
  1616. persistStoredUserSettings({
  1617. ...loadStoredUserSettings(),
  1618. northReferenceMode: 'magnetic',
  1619. })
  1620. },
  1621. handleSetNorthReferenceTrue() {
  1622. if (this.data.lockNorthReference || !mapEngine) {
  1623. return
  1624. }
  1625. mapEngine.handleSetNorthReferenceMode('true')
  1626. persistStoredUserSettings({
  1627. ...loadStoredUserSettings(),
  1628. northReferenceMode: 'true',
  1629. })
  1630. },
  1631. handleToggleSettingLock(event: WechatMiniprogram.TouchEvent) {
  1632. const key = event.currentTarget.dataset.key as SettingLockKey | undefined
  1633. if (!key) {
  1634. return
  1635. }
  1636. const nextValue = !this.data[key]
  1637. this.setData({
  1638. [key]: nextValue,
  1639. } as Record<string, boolean>)
  1640. persistStoredUserSettings(toggleStoredSettingLock(loadStoredUserSettings(), key))
  1641. },
  1642. handleOverlayTouch() {},
  1643. handlePunchAction() {
  1644. if (!this.data.punchButtonEnabled) {
  1645. return
  1646. }
  1647. if (mapEngine) {
  1648. mapEngine.handlePunchAction()
  1649. }
  1650. },
  1651. handleCloseContentCard() {
  1652. if (mapEngine) {
  1653. mapEngine.closeContentCard()
  1654. }
  1655. },
  1656. handleClosePunchHint() {
  1657. clearPunchHintDismissTimer()
  1658. this.setData({
  1659. showPunchHintBanner: false,
  1660. })
  1661. },
  1662. handlePunchHintTap() {},
  1663. handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
  1664. this.setData({
  1665. hudPanelIndex: event.detail.current || 0,
  1666. })
  1667. },
  1668. handleCycleSideButtons() {
  1669. const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
  1670. this.setData({
  1671. ...buildSideButtonVisibility(nextMode),
  1672. ...buildSideButtonState({
  1673. sideButtonMode: nextMode,
  1674. showGameInfoPanel: this.data.showGameInfoPanel,
  1675. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  1676. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1677. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1678. skipButtonEnabled: this.data.skipButtonEnabled,
  1679. gameSessionStatus: this.data.gameSessionStatus,
  1680. gpsLockEnabled: this.data.gpsLockEnabled,
  1681. gpsLockAvailable: this.data.gpsLockAvailable,
  1682. }),
  1683. })
  1684. },
  1685. handleToggleGpsLock() {
  1686. if (mapEngine) {
  1687. mapEngine.handleToggleGpsLock()
  1688. }
  1689. },
  1690. handleToggleMapRotateMode() {
  1691. if (!mapEngine || this.data.lockAutoRotate) {
  1692. return
  1693. }
  1694. if (this.data.orientationMode === 'heading-up') {
  1695. mapEngine.handleSetManualMode()
  1696. persistStoredUserSettings({
  1697. ...loadStoredUserSettings(),
  1698. autoRotateEnabled: false,
  1699. })
  1700. return
  1701. }
  1702. mapEngine.handleSetHeadingUpMode()
  1703. persistStoredUserSettings({
  1704. ...loadStoredUserSettings(),
  1705. autoRotateEnabled: true,
  1706. })
  1707. },
  1708. handleToggleDebugPanel() {
  1709. const nextShowDebugPanel = !this.data.showDebugPanel
  1710. if (!nextShowDebugPanel) {
  1711. clearGameInfoPanelSyncTimer()
  1712. }
  1713. if (mapEngine) {
  1714. mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
  1715. }
  1716. this.setData({
  1717. showDebugPanel: nextShowDebugPanel,
  1718. showGameInfoPanel: false,
  1719. showSystemSettingsPanel: false,
  1720. ...buildSideButtonState({
  1721. sideButtonMode: this.data.sideButtonMode,
  1722. showGameInfoPanel: false,
  1723. showSystemSettingsPanel: false,
  1724. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1725. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1726. skipButtonEnabled: this.data.skipButtonEnabled,
  1727. gameSessionStatus: this.data.gameSessionStatus,
  1728. gpsLockEnabled: this.data.gpsLockEnabled,
  1729. gpsLockAvailable: this.data.gpsLockAvailable,
  1730. }),
  1731. })
  1732. },
  1733. handleCloseDebugPanel() {
  1734. if (mapEngine) {
  1735. mapEngine.setDiagnosticUiEnabled(false)
  1736. }
  1737. this.setData({
  1738. showDebugPanel: false,
  1739. ...buildSideButtonState({
  1740. sideButtonMode: this.data.sideButtonMode,
  1741. showGameInfoPanel: this.data.showGameInfoPanel,
  1742. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  1743. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1744. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1745. skipButtonEnabled: this.data.skipButtonEnabled,
  1746. gameSessionStatus: this.data.gameSessionStatus,
  1747. gpsLockEnabled: this.data.gpsLockEnabled,
  1748. gpsLockAvailable: this.data.gpsLockAvailable,
  1749. }),
  1750. })
  1751. },
  1752. applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
  1753. this.data.showCenterScaleRuler = nextEnabled
  1754. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  1755. clearCenterScaleRulerSyncTimer()
  1756. clearCenterScaleRulerUpdateTimer()
  1757. const syncRulerFromEngine = () => {
  1758. if (!mapEngine) {
  1759. return
  1760. }
  1761. const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
  1762. updateCenterScaleRulerInputCache(engineSnapshot)
  1763. const mergedData = {
  1764. ...centerScaleRulerInputCache,
  1765. ...this.data,
  1766. showCenterScaleRuler: nextEnabled,
  1767. centerScaleRulerAnchorMode: nextAnchorMode,
  1768. } as MapPageData
  1769. this.setData({
  1770. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
  1771. showCenterScaleRuler: nextEnabled,
  1772. centerScaleRulerAnchorMode: nextAnchorMode,
  1773. ...buildCenterScaleRulerPatch(mergedData),
  1774. ...buildSideButtonState(mergedData),
  1775. })
  1776. }
  1777. if (!nextEnabled) {
  1778. syncRulerFromEngine()
  1779. return
  1780. }
  1781. this.setData({
  1782. showCenterScaleRuler: true,
  1783. centerScaleRulerAnchorMode: nextAnchorMode,
  1784. ...buildSideButtonState({
  1785. ...this.data,
  1786. showCenterScaleRuler: true,
  1787. centerScaleRulerAnchorMode: nextAnchorMode,
  1788. } as MapPageData),
  1789. })
  1790. this.measureStageAndCanvas(() => {
  1791. syncRulerFromEngine()
  1792. })
  1793. centerScaleRulerSyncTimer = setTimeout(() => {
  1794. centerScaleRulerSyncTimer = 0
  1795. if (!this.data.showCenterScaleRuler) {
  1796. return
  1797. }
  1798. syncRulerFromEngine()
  1799. }, 96) as unknown as number
  1800. },
  1801. handleSetCenterScaleRulerVisibleOn() {
  1802. if (this.data.lockScaleRulerVisible) {
  1803. return
  1804. }
  1805. this.applyCenterScaleRulerSettings(true, this.data.centerScaleRulerAnchorMode)
  1806. persistStoredUserSettings({
  1807. ...loadStoredUserSettings(),
  1808. showCenterScaleRuler: true,
  1809. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1810. })
  1811. },
  1812. handleSetCenterScaleRulerVisibleOff() {
  1813. if (this.data.lockScaleRulerVisible) {
  1814. return
  1815. }
  1816. this.applyCenterScaleRulerSettings(false, this.data.centerScaleRulerAnchorMode)
  1817. persistStoredUserSettings({
  1818. ...loadStoredUserSettings(),
  1819. showCenterScaleRuler: false,
  1820. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1821. })
  1822. },
  1823. handleSetCenterScaleRulerAnchorScreenCenter() {
  1824. if (this.data.lockScaleRulerAnchor) {
  1825. return
  1826. }
  1827. this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'screen-center')
  1828. persistStoredUserSettings({
  1829. ...loadStoredUserSettings(),
  1830. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1831. centerScaleRulerAnchorMode: 'screen-center',
  1832. })
  1833. },
  1834. handleSetCenterScaleRulerAnchorCompassCenter() {
  1835. if (this.data.lockScaleRulerAnchor) {
  1836. return
  1837. }
  1838. this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'compass-center')
  1839. persistStoredUserSettings({
  1840. ...loadStoredUserSettings(),
  1841. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1842. centerScaleRulerAnchorMode: 'compass-center',
  1843. })
  1844. },
  1845. handleToggleCenterScaleRulerAnchor() {
  1846. if (!this.data.showCenterScaleRuler) {
  1847. return
  1848. }
  1849. const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
  1850. ? 'compass-center'
  1851. : 'screen-center'
  1852. const engineSnapshot = mapEngine ? (mapEngine.getInitialData() as Partial<MapPageData>) : {}
  1853. updateCenterScaleRulerInputCache(engineSnapshot)
  1854. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  1855. const mergedData = {
  1856. ...centerScaleRulerInputCache,
  1857. ...this.data,
  1858. centerScaleRulerAnchorMode: nextAnchorMode,
  1859. } as MapPageData
  1860. this.setData({
  1861. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, true),
  1862. centerScaleRulerAnchorMode: nextAnchorMode,
  1863. ...buildCenterScaleRulerPatch(mergedData),
  1864. ...buildSideButtonState(mergedData),
  1865. })
  1866. if (this.data.showGameInfoPanel) {
  1867. this.syncGameInfoPanelSnapshot()
  1868. }
  1869. },
  1870. handleDebugPanelTap() {},
  1871. })