map.ts 65 KB

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