map.ts 67 KB

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