map.ts 113 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619
  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 {
  10. getBackendSessionContextFromLaunchEnvelope,
  11. getDemoGameLaunchEnvelope,
  12. resolveGameLaunchEnvelope,
  13. type GameLaunchEnvelope,
  14. type MapPageLaunchOptions,
  15. } from '../../utils/gameLaunch'
  16. import { finishSession, startSession, type BackendSessionFinishSummaryPayload } from '../../utils/backendApi'
  17. import { loadBackendBaseUrl } from '../../utils/backendAuth'
  18. import { loadRemoteMapConfig, type RemoteMapConfig } from '../../utils/remoteMapConfig'
  19. import {
  20. persistStoredMockDebugLogBridgeUrl,
  21. setGlobalMockDebugBridgeChannelId,
  22. setGlobalMockDebugBridgeEnabled,
  23. setGlobalMockDebugBridgeUrl,
  24. } from '../../utils/globalMockDebugBridge'
  25. import { reportBackendClientLog } from '../../utils/backendClientLogs'
  26. import { type H5ExperienceFallbackPayload, type H5ExperienceRequest } from '../../game/experience/h5Experience'
  27. import { type TrackColorPreset } from '../../game/presentation/trackStyleConfig'
  28. import { type GpsMarkerColorPreset } from '../../game/presentation/gpsMarkerStyleConfig'
  29. import { type PlayerTelemetryProfile } from '../../game/telemetry/playerTelemetryProfile'
  30. import {
  31. DEFAULT_SETTING_LOCKS,
  32. DEFAULT_STORED_USER_SETTINGS,
  33. loadStoredUserSettings,
  34. mergeStoredUserSettings,
  35. persistStoredUserSettings,
  36. resolveSystemSettingsState,
  37. type SystemSettingsConfig,
  38. type CenterScaleRulerAnchorMode,
  39. type ResolvedSystemSettingsState,
  40. type SideButtonPlacement,
  41. type StoredUserSettings,
  42. } from '../../game/core/systemSettingsState'
  43. import {
  44. compileRuntimeProfile,
  45. } from '../../game/core/runtimeProfileCompiler'
  46. import {
  47. clearSessionRecoverySnapshot,
  48. loadSessionRecoverySnapshot,
  49. saveSessionRecoverySnapshot,
  50. type SessionRecoverySnapshot,
  51. } from '../../game/core/sessionRecovery'
  52. type CompassTickData = {
  53. angle: number
  54. long: boolean
  55. major: boolean
  56. }
  57. type CompassLabelData = {
  58. text: string
  59. angle: number
  60. rotateBack: number
  61. radius: number
  62. className: string
  63. }
  64. type ScaleRulerMinorTickData = {
  65. key: string
  66. topPx: number
  67. long: boolean
  68. }
  69. type ScaleRulerMajorMarkData = {
  70. key: string
  71. topPx: number
  72. label: string
  73. }
  74. type SideButtonMode = 'shown' | 'hidden'
  75. type SideActionButtonState = 'muted' | 'default' | 'active'
  76. type MapPageData = MapEngineViewState & {
  77. showDebugPanel: boolean
  78. showGameInfoPanel: boolean
  79. showResultScene: boolean
  80. showSystemSettingsPanel: boolean
  81. showHeartRateDevicePicker: boolean
  82. showCenterScaleRuler: boolean
  83. showPunchHintBanner: boolean
  84. punchHintFxClass: string
  85. centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
  86. statusBarHeight: number
  87. topInsetHeight: number
  88. hudPanelIndex: number
  89. configSourceText: string
  90. mockBridgeUrlDraft: string
  91. mockHeartRateBridgeUrlDraft: string
  92. mockDebugLogBridgeUrlDraft: string
  93. mockChannelIdDraft: string
  94. gameInfoTitle: string
  95. gameInfoSubtitle: string
  96. gameInfoLocalRows: MapEngineGameInfoRow[]
  97. gameInfoGlobalRows: MapEngineGameInfoRow[]
  98. resultSceneTitle: string
  99. resultSceneSubtitle: string
  100. resultSceneHeroLabel: string
  101. resultSceneHeroValue: string
  102. resultSceneRows: MapEngineGameInfoRow[]
  103. resultSceneCountdownText: string
  104. panelTimerText: string
  105. panelTimerMode: 'elapsed' | 'countdown'
  106. panelMileageText: string
  107. panelTargetSummaryText: string
  108. panelDistanceValueText: string
  109. panelProgressText: string
  110. panelSpeedValueText: string
  111. panelTimerFxClass: string
  112. panelMileageFxClass: string
  113. panelSpeedFxClass: string
  114. panelHeartRateFxClass: string
  115. compassTicks: CompassTickData[]
  116. compassLabels: CompassLabelData[]
  117. sideButtonMode: SideButtonMode
  118. sideButtonPlacement: SideButtonPlacement
  119. autoRotateEnabled: boolean
  120. lockAnimationLevel: boolean
  121. lockTrackMode: boolean
  122. lockTrackTailLength: boolean
  123. lockTrackColor: boolean
  124. lockTrackStyle: boolean
  125. lockGpsMarkerVisible: boolean
  126. lockGpsMarkerStyle: boolean
  127. lockGpsMarkerSize: boolean
  128. lockGpsMarkerColor: boolean
  129. lockSideButtonPlacement: boolean
  130. lockAutoRotate: boolean
  131. lockCompassTuning: boolean
  132. lockScaleRulerVisible: boolean
  133. lockScaleRulerAnchor: boolean
  134. lockNorthReference: boolean
  135. lockHeartRateDevice: boolean
  136. sideToggleIconSrc: string
  137. sideButton2Class: string
  138. sideButton4Class: string
  139. sideButton11Class: string
  140. sideButton12Class: string
  141. sideButton13Class: string
  142. sideButton14Class: string
  143. sideButton16Class: string
  144. centerScaleRulerVisible: boolean
  145. centerScaleRulerCenterXPx: number
  146. centerScaleRulerZeroYPx: number
  147. centerScaleRulerHeightPx: number
  148. centerScaleRulerAxisBottomPx: number
  149. centerScaleRulerZeroVisible: boolean
  150. centerScaleRulerZeroLabel: string
  151. centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
  152. centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
  153. showLeftButtonGroup: boolean
  154. showRightButtonGroups: boolean
  155. showBottomDebugButton: boolean
  156. showStartEntryButton: boolean
  157. }
  158. function getGlobalTelemetryProfile(): PlayerTelemetryProfile | null {
  159. const app = getApp<IAppOption>()
  160. const profile = app.globalData && app.globalData.telemetryPlayerProfile
  161. return profile ? { ...profile } : null
  162. }
  163. const INTERNAL_BUILD_VERSION = 'map-build-293'
  164. const PUNCH_HINT_AUTO_HIDE_MS = 30000
  165. const PUNCH_HINT_FX_DURATION_MS = 420
  166. const PUNCH_HINT_HAPTIC_GAP_MS = 2400
  167. const SESSION_RECOVERY_PERSIST_INTERVAL_MS = 5000
  168. const RESULT_EXIT_REDIRECT_DELAY_MS = 3000
  169. let currentGameLaunchEnvelope: GameLaunchEnvelope = getDemoGameLaunchEnvelope()
  170. let mapEngine: MapEngine | null = null
  171. let stageCanvasAttached = false
  172. let gameInfoPanelSyncTimer = 0
  173. let centerScaleRulerSyncTimer = 0
  174. let contentAudioRecorder: WechatMiniprogram.RecorderManager | null = null
  175. let contentAudioRecording = false
  176. let centerScaleRulerUpdateTimer = 0
  177. let punchHintDismissTimer = 0
  178. let punchHintFxTimer = 0
  179. let panelTimerFxTimer = 0
  180. let panelMileageFxTimer = 0
  181. let panelSpeedFxTimer = 0
  182. let panelHeartRateFxTimer = 0
  183. let sessionRecoveryPersistTimer = 0
  184. let resultExitRedirectTimer = 0
  185. let resultExitCountdownTimer = 0
  186. let lastPunchHintHapticAt = 0
  187. let currentSystemSettingsConfig: SystemSettingsConfig | undefined
  188. let currentRemoteMapConfig: RemoteMapConfig | undefined
  189. let systemSettingsLockLifetimeActive = false
  190. let syncedBackendSessionStartId = ''
  191. let syncedBackendSessionFinishId = ''
  192. let shouldAutoRestoreRecoverySnapshot = false
  193. let shouldAutoStartSessionOnEnter = false
  194. let redirectedToResultPage = false
  195. let pendingHeartRateSwitchDeviceName: string | null = null
  196. const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
  197. const DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY = 'cmr.debug.autoConnectMockSources.v1'
  198. let lastCenterScaleRulerStablePatch: Pick<
  199. MapPageData,
  200. | 'centerScaleRulerVisible'
  201. | 'centerScaleRulerCenterXPx'
  202. | 'centerScaleRulerZeroYPx'
  203. | 'centerScaleRulerHeightPx'
  204. | 'centerScaleRulerAxisBottomPx'
  205. | 'centerScaleRulerZeroVisible'
  206. | 'centerScaleRulerZeroLabel'
  207. | 'centerScaleRulerMinorTicks'
  208. | 'centerScaleRulerMajorMarks'
  209. > = {
  210. centerScaleRulerVisible: false,
  211. centerScaleRulerCenterXPx: 0,
  212. centerScaleRulerZeroYPx: 0,
  213. centerScaleRulerHeightPx: 0,
  214. centerScaleRulerAxisBottomPx: 0,
  215. centerScaleRulerZeroVisible: false,
  216. centerScaleRulerZeroLabel: '0 m',
  217. centerScaleRulerMinorTicks: [],
  218. centerScaleRulerMajorMarks: [],
  219. }
  220. let centerScaleRulerInputCache: Partial<Pick<
  221. MapPageData,
  222. 'stageWidth'
  223. | 'stageHeight'
  224. | 'zoom'
  225. | 'centerTileY'
  226. | 'tileSizePx'
  227. | 'previewScale'
  228. >> = {}
  229. const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
  230. 'buildVersion',
  231. 'renderMode',
  232. 'projectionMode',
  233. 'mapReady',
  234. 'mapReadyText',
  235. 'mapName',
  236. 'configStatusText',
  237. 'deviceHeadingText',
  238. 'devicePoseText',
  239. 'headingConfidenceText',
  240. 'accelerometerText',
  241. 'gyroscopeText',
  242. 'deviceMotionText',
  243. 'compassSourceText',
  244. 'compassTuningProfile',
  245. 'compassTuningProfileText',
  246. 'northReferenceButtonText',
  247. 'autoRotateSourceText',
  248. 'autoRotateCalibrationText',
  249. 'northReferenceText',
  250. 'centerText',
  251. 'tileSource',
  252. 'visibleTileCount',
  253. 'readyTileCount',
  254. 'memoryTileCount',
  255. 'diskTileCount',
  256. 'memoryHitCount',
  257. 'diskHitCount',
  258. 'networkFetchCount',
  259. 'cacheHitRateText',
  260. 'locationSourceMode',
  261. 'locationSourceText',
  262. 'mockBridgeConnected',
  263. 'mockBridgeStatusText',
  264. 'mockBridgeUrlText',
  265. 'mockCoordText',
  266. 'mockSpeedText',
  267. 'gpsCoordText',
  268. 'heartRateSourceMode',
  269. 'heartRateSourceText',
  270. 'heartRateConnected',
  271. 'heartRateStatusText',
  272. 'heartRateDeviceText',
  273. 'heartRateScanText',
  274. 'heartRateDiscoveredDevices',
  275. 'mockHeartRateBridgeConnected',
  276. 'mockHeartRateBridgeStatusText',
  277. 'mockHeartRateBridgeUrlText',
  278. 'mockHeartRateText',
  279. ])
  280. const CENTER_SCALE_RULER_DEP_KEYS = new Set<string>([
  281. 'showCenterScaleRuler',
  282. 'centerScaleRulerAnchorMode',
  283. 'stageWidth',
  284. 'stageHeight',
  285. 'topInsetHeight',
  286. 'zoom',
  287. 'centerTileY',
  288. 'tileSizePx',
  289. 'previewScale',
  290. ])
  291. const CENTER_SCALE_RULER_CACHE_KEYS: Array<keyof typeof centerScaleRulerInputCache> = [
  292. 'stageWidth',
  293. 'stageHeight',
  294. 'zoom',
  295. 'centerTileY',
  296. 'tileSizePx',
  297. 'previewScale',
  298. ]
  299. const RULER_ONLY_VIEW_KEYS = new Set<string>([
  300. 'zoom',
  301. 'centerTileX',
  302. 'centerTileY',
  303. 'tileSizePx',
  304. 'previewScale',
  305. 'stageWidth',
  306. 'stageHeight',
  307. 'stageLeft',
  308. 'stageTop',
  309. ])
  310. const SIDE_BUTTON_DEP_KEYS = new Set<string>([
  311. 'sideButtonMode',
  312. 'showGameInfoPanel',
  313. 'showCenterScaleRuler',
  314. 'centerScaleRulerAnchorMode',
  315. 'skipButtonEnabled',
  316. 'gameSessionStatus',
  317. 'gpsLockEnabled',
  318. 'gpsLockAvailable',
  319. ])
  320. function hasAnyPatchKey(patch: Record<string, unknown>, keys: Set<string>): boolean {
  321. return Object.keys(patch).some((key) => keys.has(key))
  322. }
  323. function filterDebugOnlyPatch(
  324. patch: Partial<MapPageData>,
  325. includeDebugFields: boolean,
  326. includeRulerFields: boolean,
  327. ): Partial<MapPageData> {
  328. if (includeDebugFields && includeRulerFields) {
  329. return patch
  330. }
  331. const filteredPatch: Partial<MapPageData> = {}
  332. for (const [key, value] of Object.entries(patch)) {
  333. if (!includeDebugFields && DEBUG_ONLY_VIEW_KEYS.has(key)) {
  334. continue
  335. }
  336. if (!includeRulerFields && RULER_ONLY_VIEW_KEYS.has(key)) {
  337. continue
  338. }
  339. {
  340. ;(filteredPatch as Record<string, unknown>)[key] = value
  341. }
  342. }
  343. return filteredPatch
  344. }
  345. function clearGameInfoPanelSyncTimer() {
  346. if (gameInfoPanelSyncTimer) {
  347. clearTimeout(gameInfoPanelSyncTimer)
  348. gameInfoPanelSyncTimer = 0
  349. }
  350. }
  351. function clearCenterScaleRulerSyncTimer() {
  352. if (centerScaleRulerSyncTimer) {
  353. clearTimeout(centerScaleRulerSyncTimer)
  354. centerScaleRulerSyncTimer = 0
  355. }
  356. }
  357. function clearCenterScaleRulerUpdateTimer() {
  358. if (centerScaleRulerUpdateTimer) {
  359. clearTimeout(centerScaleRulerUpdateTimer)
  360. centerScaleRulerUpdateTimer = 0
  361. }
  362. }
  363. function clearPunchHintDismissTimer() {
  364. if (punchHintDismissTimer) {
  365. clearTimeout(punchHintDismissTimer)
  366. punchHintDismissTimer = 0
  367. }
  368. }
  369. function clearPunchHintFxTimer() {
  370. if (punchHintFxTimer) {
  371. clearTimeout(punchHintFxTimer)
  372. punchHintFxTimer = 0
  373. }
  374. }
  375. function clearHudFxTimer(key: 'timer' | 'mileage' | 'speed' | 'heartRate') {
  376. const timerMap = {
  377. timer: panelTimerFxTimer,
  378. mileage: panelMileageFxTimer,
  379. speed: panelSpeedFxTimer,
  380. heartRate: panelHeartRateFxTimer,
  381. }
  382. const timer = timerMap[key]
  383. if (timer) {
  384. clearTimeout(timer)
  385. }
  386. if (key === 'timer') {
  387. panelTimerFxTimer = 0
  388. } else if (key === 'mileage') {
  389. panelMileageFxTimer = 0
  390. } else if (key === 'speed') {
  391. panelSpeedFxTimer = 0
  392. } else {
  393. panelHeartRateFxTimer = 0
  394. }
  395. }
  396. function updateCenterScaleRulerInputCache(patch: Partial<MapPageData>) {
  397. for (const key of CENTER_SCALE_RULER_CACHE_KEYS) {
  398. if (Object.prototype.hasOwnProperty.call(patch, key)) {
  399. ;(centerScaleRulerInputCache as Record<string, unknown>)[key] =
  400. (patch as Record<string, unknown>)[key]
  401. }
  402. }
  403. }
  404. function updateStoredUserSettings(patch: Partial<StoredUserSettings>) {
  405. persistStoredUserSettings(
  406. mergeStoredUserSettings(loadStoredUserSettings(), patch),
  407. )
  408. }
  409. function loadStoredMockChannelId(): string {
  410. try {
  411. const value = wx.getStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY)
  412. if (typeof value === 'string' && value.trim().length > 0) {
  413. return value.trim()
  414. }
  415. } catch (_error) {
  416. // Ignore storage read failures and fall back to default.
  417. }
  418. return 'default'
  419. }
  420. function persistMockChannelId(channelId: string) {
  421. try {
  422. wx.setStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY, channelId)
  423. } catch (_error) {
  424. // Ignore storage write failures in debug preference persistence.
  425. }
  426. }
  427. function loadMockAutoConnectEnabled(): boolean {
  428. try {
  429. return wx.getStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY) === true
  430. } catch (_error) {
  431. return false
  432. }
  433. }
  434. function persistMockAutoConnectEnabled(enabled: boolean) {
  435. try {
  436. wx.setStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY, enabled)
  437. } catch (_error) {
  438. // Ignore storage write failures in debug preference persistence.
  439. }
  440. }
  441. function buildResolvedSystemSettingsPatch(
  442. resolvedSettings: ResolvedSystemSettingsState,
  443. ): Partial<MapPageData> {
  444. return {
  445. ...resolvedSettings.values,
  446. ...resolvedSettings.locks,
  447. autoRotateEnabled: resolvedSettings.values.autoRotateEnabled,
  448. sideButtonPlacement: resolvedSettings.values.sideButtonPlacement,
  449. showCenterScaleRuler: resolvedSettings.values.showCenterScaleRuler,
  450. centerScaleRulerAnchorMode: resolvedSettings.values.centerScaleRulerAnchorMode,
  451. }
  452. }
  453. function isSystemSettingsLockLifetimeActive(): boolean {
  454. return systemSettingsLockLifetimeActive
  455. }
  456. function clearSessionRecoveryPersistTimer() {
  457. if (sessionRecoveryPersistTimer) {
  458. clearInterval(sessionRecoveryPersistTimer)
  459. sessionRecoveryPersistTimer = 0
  460. }
  461. }
  462. function clearResultExitRedirectTimer() {
  463. if (resultExitRedirectTimer) {
  464. clearTimeout(resultExitRedirectTimer)
  465. resultExitRedirectTimer = 0
  466. }
  467. }
  468. function clearResultExitCountdownTimer() {
  469. if (resultExitCountdownTimer) {
  470. clearInterval(resultExitCountdownTimer)
  471. resultExitCountdownTimer = 0
  472. }
  473. }
  474. function navigateAwayFromMapAfterCancel() {
  475. const pages = getCurrentPages()
  476. if (pages.length > 1) {
  477. wx.navigateBack({
  478. delta: 1,
  479. })
  480. return
  481. }
  482. wx.redirectTo({
  483. url: '/pages/home/home',
  484. })
  485. }
  486. function hasExplicitLaunchOptions(options?: MapPageLaunchOptions | null): boolean {
  487. if (!options) {
  488. return false
  489. }
  490. return !!(
  491. options.launchId
  492. || options.preset
  493. || options.configUrl
  494. || options.competitionId
  495. || options.eventId
  496. || options.sessionId
  497. || options.launchRequestId
  498. )
  499. }
  500. function getCurrentBackendSessionContext(): { sessionId: string; sessionToken: string } | null {
  501. return getBackendSessionContextFromLaunchEnvelope(currentGameLaunchEnvelope)
  502. }
  503. function getCurrentBackendBaseUrl(): string {
  504. const app = getApp<IAppOption>()
  505. if (app.globalData && app.globalData.backendBaseUrl) {
  506. return app.globalData.backendBaseUrl
  507. }
  508. return loadBackendBaseUrl()
  509. }
  510. function buildSideButtonVisibility(mode: SideButtonMode) {
  511. return {
  512. sideButtonMode: mode,
  513. showLeftButtonGroup: mode === 'shown',
  514. showRightButtonGroups: false,
  515. showBottomDebugButton: true,
  516. }
  517. }
  518. function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
  519. return currentMode === 'shown' ? 'hidden' : 'shown'
  520. }
  521. function buildCompassTicks(): CompassTickData[] {
  522. const ticks: CompassTickData[] = []
  523. for (let angle = 0; angle < 360; angle += 5) {
  524. ticks.push({
  525. angle,
  526. long: angle % 15 === 0,
  527. major: angle % 45 === 0,
  528. })
  529. }
  530. return ticks
  531. }
  532. function buildCompassLabels(): CompassLabelData[] {
  533. return [
  534. { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
  535. { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
  536. { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  537. { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  538. { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  539. { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  540. { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  541. { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
  542. ]
  543. }
  544. function getFallbackStageRect(): MapEngineStageRect {
  545. const systemInfo = wx.getSystemInfoSync()
  546. const width = Math.max(320, systemInfo.windowWidth)
  547. const height = Math.max(280, systemInfo.windowHeight)
  548. return {
  549. width,
  550. height,
  551. left: 0,
  552. top: 0,
  553. }
  554. }
  555. function getSideToggleIconSrc(mode: SideButtonMode): string {
  556. if (mode === 'hidden') {
  557. return '../../assets/btn_more1.png'
  558. }
  559. return '../../assets/btn_more3.png'
  560. }
  561. function getSideActionButtonClass(state: SideActionButtonState): string {
  562. if (state === 'muted') {
  563. return 'map-side-button map-side-button--muted'
  564. }
  565. if (state === 'active') {
  566. return 'map-side-button map-side-button--active'
  567. }
  568. return 'map-side-button map-side-button--default'
  569. }
  570. function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showSystemSettingsPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
  571. const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
  572. ? 'muted'
  573. : data.gpsLockEnabled
  574. ? 'active'
  575. : 'default'
  576. const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'running' ? 'active' : 'muted'
  577. const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
  578. const sideButton12State: SideActionButtonState = data.showSystemSettingsPanel ? 'active' : 'default'
  579. const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
  580. const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
  581. ? 'muted'
  582. : data.centerScaleRulerAnchorMode === 'compass-center'
  583. ? 'active'
  584. : 'default'
  585. const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'
  586. return {
  587. sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
  588. sideButton2Class: getSideActionButtonClass(sideButton2State),
  589. sideButton4Class: getSideActionButtonClass(sideButton4State),
  590. sideButton11Class: getSideActionButtonClass(sideButton11State),
  591. sideButton12Class: getSideActionButtonClass(sideButton12State),
  592. sideButton13Class: getSideActionButtonClass(sideButton13State),
  593. sideButton14Class: getSideActionButtonClass(sideButton14State),
  594. sideButton16Class: getSideActionButtonClass(sideButton16State),
  595. }
  596. }
  597. function getRpxUnitInPx(): number {
  598. const systemInfo = wx.getSystemInfoSync()
  599. return systemInfo.windowWidth / 750
  600. }
  601. function worldTileYToLat(worldTileY: number, zoom: number): number {
  602. const scale = Math.pow(2, zoom)
  603. const n = Math.PI - (2 * Math.PI * worldTileY) / scale
  604. return (180 / Math.PI) * Math.atan(Math.sinh(n))
  605. }
  606. function getNiceDistanceMeters(rawDistanceMeters: number): number {
  607. if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
  608. return 50
  609. }
  610. const exponent = Math.floor(Math.log10(rawDistanceMeters))
  611. const base = Math.pow(10, exponent)
  612. const normalized = rawDistanceMeters / base
  613. if (normalized <= 1) {
  614. return base
  615. }
  616. if (normalized <= 2) {
  617. return 2 * base
  618. }
  619. if (normalized <= 5) {
  620. return 5 * base
  621. }
  622. return 10 * base
  623. }
  624. function formatScaleDistanceLabel(distanceMeters: number): string {
  625. if (distanceMeters >= 1000) {
  626. const distanceKm = distanceMeters / 1000
  627. const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
  628. return `${formatted.replace(/\.0$/, '')} km`
  629. }
  630. return `${Math.round(distanceMeters)} m`
  631. }
  632. function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
  633. if (!data.showCenterScaleRuler) {
  634. lastCenterScaleRulerStablePatch = {
  635. centerScaleRulerVisible: false,
  636. centerScaleRulerCenterXPx: 0,
  637. centerScaleRulerZeroYPx: 0,
  638. centerScaleRulerHeightPx: 0,
  639. centerScaleRulerAxisBottomPx: 0,
  640. centerScaleRulerZeroVisible: false,
  641. centerScaleRulerZeroLabel: '0 m',
  642. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  643. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  644. }
  645. return { ...lastCenterScaleRulerStablePatch }
  646. }
  647. if (!data.stageWidth || !data.stageHeight) {
  648. return { ...lastCenterScaleRulerStablePatch }
  649. }
  650. const topPadding = 12
  651. const rpxUnitPx = getRpxUnitInPx()
  652. const compassBottomPaddingPx = 248 * rpxUnitPx
  653. const compassDialRadiusPx = (196 * rpxUnitPx) / 2
  654. const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
  655. const compassOcclusionPaddingPx = 10 * rpxUnitPx
  656. const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
  657. ? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
  658. : Math.round(data.stageHeight / 2)
  659. const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
  660. const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
  661. ? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
  662. : 0
  663. if (
  664. !data.tileSizePx
  665. || !Number.isFinite(data.zoom)
  666. || !Number.isFinite(data.centerTileY)
  667. ) {
  668. return {
  669. ...lastCenterScaleRulerStablePatch,
  670. centerScaleRulerVisible: true,
  671. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  672. centerScaleRulerZeroYPx: zeroYPx,
  673. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  674. centerScaleRulerAxisBottomPx: coveredBottomPx,
  675. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  676. }
  677. }
  678. const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
  679. const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
  680. const metersPerPixel = metersPerTile / data.tileSizePx
  681. const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
  682. const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
  683. const rulerHeight = Math.floor(zeroYPx - topPadding)
  684. if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
  685. return {
  686. ...lastCenterScaleRulerStablePatch,
  687. centerScaleRulerVisible: true,
  688. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  689. centerScaleRulerZeroYPx: zeroYPx,
  690. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  691. centerScaleRulerAxisBottomPx: coveredBottomPx,
  692. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  693. }
  694. }
  695. const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
  696. const minorDistanceMeters = labelDistanceMeters / 8
  697. const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
  698. const visibleTopLimitPx = rulerHeight - coveredBottomPx
  699. const minorTicks: ScaleRulerMinorTickData[] = []
  700. const majorMarks: ScaleRulerMajorMarkData[] = []
  701. for (let index = 1; index <= 200; index += 1) {
  702. const topPx = Math.round(rulerHeight - index * minorStepPx)
  703. if (topPx < 0) {
  704. break
  705. }
  706. if (topPx >= visibleTopLimitPx) {
  707. continue
  708. }
  709. const isHalfMajor = index % 4 === 0
  710. const isLabelMajor = index % 8 === 0
  711. minorTicks.push({
  712. key: `minor-${index}`,
  713. topPx,
  714. long: isHalfMajor,
  715. })
  716. if (isLabelMajor) {
  717. majorMarks.push({
  718. key: `major-${index}`,
  719. topPx,
  720. label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
  721. })
  722. }
  723. }
  724. lastCenterScaleRulerStablePatch = {
  725. centerScaleRulerVisible: true,
  726. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  727. centerScaleRulerZeroYPx: zeroYPx,
  728. centerScaleRulerHeightPx: rulerHeight,
  729. centerScaleRulerAxisBottomPx: coveredBottomPx,
  730. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  731. centerScaleRulerZeroLabel: '0 m',
  732. centerScaleRulerMinorTicks: minorTicks,
  733. centerScaleRulerMajorMarks: majorMarks,
  734. }
  735. return { ...lastCenterScaleRulerStablePatch }
  736. }
  737. function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
  738. return {
  739. title: '当前游戏',
  740. subtitle: '未开始',
  741. localRows: [],
  742. globalRows: [
  743. { label: '全球积分', value: '未接入' },
  744. { label: '全球排名', value: '未接入' },
  745. { label: '在线人数', value: '未接入' },
  746. { label: '队伍状态', value: '未接入' },
  747. { label: '实时广播', value: '未接入' },
  748. ],
  749. }
  750. }
  751. function buildEmptyResultSceneSnapshot(): MapEngineResultSnapshot {
  752. return {
  753. title: '本局结果',
  754. subtitle: '未开始',
  755. heroLabel: '本局用时',
  756. heroValue: '--',
  757. rows: [],
  758. }
  759. }
  760. function buildRuntimeSummaryRows(envelope: GameLaunchEnvelope): MapEngineGameInfoRow[] {
  761. const runtime = envelope.runtime
  762. const variantName = envelope.variant ? (envelope.variant.variantName || envelope.variant.variantId || null) : null
  763. const variantRouteCode = envelope.variant ? (envelope.variant.routeCode || null) : null
  764. if (!runtime) {
  765. return []
  766. }
  767. const rows: MapEngineGameInfoRow[] = []
  768. rows.push({ label: '运行绑定', value: runtime.runtimeBindingId || '--' })
  769. rows.push({ label: '地点', value: runtime.placeName || runtime.placeId || '--' })
  770. rows.push({ label: '地图', value: runtime.mapName || runtime.mapId || '--' })
  771. rows.push({ label: '赛道集', value: runtime.courseSetId || '--' })
  772. rows.push({ label: '赛道版本', value: runtime.courseVariantId || variantName || '--' })
  773. rows.push({ label: 'RouteCode', value: runtime.routeCode || variantRouteCode || '--' })
  774. rows.push({ label: '瓦片版本', value: runtime.tileReleaseId || '--' })
  775. return rows
  776. }
  777. function buildLaunchConfigSummaryRows(envelope: GameLaunchEnvelope): MapEngineGameInfoRow[] {
  778. const rows: MapEngineGameInfoRow[] = []
  779. rows.push({ label: '配置标签', value: envelope.config.configLabel || '--' })
  780. rows.push({ label: '配置URL', value: envelope.config.configUrl || '--' })
  781. rows.push({ label: '配置Release', value: envelope.config.releaseId || '--' })
  782. rows.push({
  783. label: 'Launch Event',
  784. value: envelope.business && envelope.business.eventId
  785. ? envelope.business.eventId
  786. : '--',
  787. })
  788. rows.push({
  789. label: 'Resolved Manifest',
  790. value: envelope.resolvedRelease && envelope.resolvedRelease.manifestUrl
  791. ? envelope.resolvedRelease.manifestUrl
  792. : '--',
  793. })
  794. rows.push({
  795. label: 'Resolved Release',
  796. value: envelope.resolvedRelease && envelope.resolvedRelease.releaseId
  797. ? envelope.resolvedRelease.releaseId
  798. : '--',
  799. })
  800. return rows
  801. }
  802. function emitSimulatorLaunchDiagnostic(
  803. stage: string,
  804. payload: Record<string, unknown>,
  805. ) {
  806. reportBackendClientLog({
  807. level: 'info',
  808. category: 'launch-diagnostic',
  809. message: stage,
  810. eventId: typeof payload.launchEventId === 'string' ? payload.launchEventId : '',
  811. releaseId: typeof payload.configReleaseId === 'string'
  812. ? payload.configReleaseId
  813. : (typeof payload.resolvedReleaseId === 'string' ? payload.resolvedReleaseId : ''),
  814. sessionId: typeof payload.launchSessionId === 'string' ? payload.launchSessionId : '',
  815. manifestUrl: typeof payload.resolvedManifestUrl === 'string'
  816. ? payload.resolvedManifestUrl
  817. : (typeof payload.configUrl === 'string' ? payload.configUrl : ''),
  818. details: payload,
  819. })
  820. }
  821. Page({
  822. data: {
  823. showDebugPanel: false,
  824. showGameInfoPanel: false,
  825. showResultScene: false,
  826. showSystemSettingsPanel: false,
  827. showHeartRateDevicePicker: false,
  828. showCenterScaleRuler: false,
  829. statusBarHeight: 0,
  830. topInsetHeight: 12,
  831. hudPanelIndex: 0,
  832. configSourceText: '顺序赛配置',
  833. centerScaleRulerAnchorMode: DEFAULT_STORED_USER_SETTINGS.centerScaleRulerAnchorMode,
  834. punchHintFxClass: '',
  835. autoRotateEnabled: DEFAULT_STORED_USER_SETTINGS.autoRotateEnabled,
  836. ...DEFAULT_SETTING_LOCKS,
  837. gameInfoTitle: '当前游戏',
  838. gameInfoSubtitle: '未开始',
  839. gameInfoLocalRows: [],
  840. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  841. resultSceneTitle: '本局结果',
  842. resultSceneSubtitle: '未开始',
  843. resultSceneHeroLabel: '本局用时',
  844. resultSceneHeroValue: '--',
  845. resultSceneRows: buildEmptyResultSceneSnapshot().rows,
  846. resultSceneCountdownText: '',
  847. panelTimerText: '00:00:00',
  848. panelTimerMode: 'elapsed',
  849. panelMileageText: '0m',
  850. panelActionTagText: '目标',
  851. panelDistanceTagText: '点距',
  852. panelTargetSummaryText: '等待选择目标',
  853. panelDistanceValueText: '--',
  854. panelDistanceUnitText: '',
  855. panelProgressText: '0/0',
  856. showPunchHintBanner: true,
  857. sideButtonPlacement: 'left',
  858. gameSessionStatus: 'idle',
  859. gameModeText: '顺序赛',
  860. gpsLockEnabled: false,
  861. gpsLockAvailable: false,
  862. locationSourceMode: 'real',
  863. locationSourceText: '真实定位',
  864. mockBridgeConnected: false,
  865. mockBridgeStatusText: '未连接',
  866. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  867. mockChannelIdText: 'default',
  868. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  869. mockChannelIdDraft: 'default',
  870. mockCoordText: '--',
  871. mockSpeedText: '--',
  872. heartRateSourceMode: 'real',
  873. heartRateSourceText: '真实心率',
  874. mockHeartRateBridgeConnected: false,
  875. mockHeartRateBridgeStatusText: '未连接',
  876. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
  877. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
  878. mockHeartRateText: '--',
  879. mockDebugLogBridgeConnected: false,
  880. mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
  881. mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
  882. mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
  883. heartRateScanText: '未扫描',
  884. heartRateDiscoveredDevices: [],
  885. panelSpeedValueText: '0',
  886. panelTelemetryTone: 'blue',
  887. trackDisplayMode: DEFAULT_STORED_USER_SETTINGS.trackDisplayMode,
  888. trackTailLength: DEFAULT_STORED_USER_SETTINGS.trackTailLength,
  889. trackColorPreset: DEFAULT_STORED_USER_SETTINGS.trackColorPreset,
  890. trackStyleProfile: DEFAULT_STORED_USER_SETTINGS.trackStyleProfile,
  891. gpsMarkerVisible: DEFAULT_STORED_USER_SETTINGS.gpsMarkerVisible,
  892. gpsMarkerStyle: DEFAULT_STORED_USER_SETTINGS.gpsMarkerStyle,
  893. gpsMarkerSize: DEFAULT_STORED_USER_SETTINGS.gpsMarkerSize,
  894. gpsMarkerColorPreset: DEFAULT_STORED_USER_SETTINGS.gpsMarkerColorPreset,
  895. gpsLogoStatusText: '未配置',
  896. gpsLogoSourceText: '--',
  897. panelHeartRateZoneNameText: '--',
  898. panelHeartRateZoneRangeText: '',
  899. heartRateConnected: false,
  900. heartRateStatusText: '心率带未连接',
  901. heartRateDeviceText: '--',
  902. panelHeartRateValueText: '--',
  903. panelHeartRateUnitText: '',
  904. panelCaloriesValueText: '0',
  905. panelCaloriesUnitText: 'kcal',
  906. panelAverageSpeedValueText: '0',
  907. panelAverageSpeedUnitText: 'km/h',
  908. panelAccuracyValueText: '--',
  909. panelAccuracyUnitText: '',
  910. deviceHeadingText: '--',
  911. devicePoseText: '竖持',
  912. headingConfidenceText: '低',
  913. accelerometerText: '--',
  914. gyroscopeText: '--',
  915. deviceMotionText: '--',
  916. compassSourceText: '无数据',
  917. compassTuningProfile: DEFAULT_STORED_USER_SETTINGS.compassTuningProfile,
  918. compassTuningProfileText: '平衡',
  919. punchButtonText: '打点',
  920. punchButtonEnabled: false,
  921. skipButtonEnabled: false,
  922. punchHintText: '等待进入检查点范围',
  923. punchFeedbackVisible: false,
  924. punchFeedbackText: '',
  925. punchFeedbackTone: 'neutral',
  926. contentCardVisible: false,
  927. contentCardTemplate: 'story',
  928. contentCardTitle: '',
  929. contentCardBody: '',
  930. contentCardActions: [],
  931. contentQuizVisible: false,
  932. contentQuizQuestionText: '',
  933. contentQuizCountdownText: '',
  934. contentQuizOptions: [],
  935. contentQuizFeedbackVisible: false,
  936. contentQuizFeedbackText: '',
  937. contentQuizFeedbackTone: 'neutral',
  938. punchButtonFxClass: '',
  939. panelProgressFxClass: '',
  940. panelDistanceFxClass: '',
  941. punchFeedbackFxClass: '',
  942. contentCardFxClass: '',
  943. mapPulseVisible: false,
  944. mapPulseLeftPx: 0,
  945. mapPulseTopPx: 0,
  946. mapPulseFxClass: '',
  947. stageFxVisible: false,
  948. stageFxClass: '',
  949. centerScaleRulerVisible: false,
  950. centerScaleRulerCenterXPx: 0,
  951. centerScaleRulerZeroYPx: 0,
  952. centerScaleRulerHeightPx: 0,
  953. centerScaleRulerAxisBottomPx: 0,
  954. centerScaleRulerZeroVisible: false,
  955. centerScaleRulerZeroLabel: '0 m',
  956. centerScaleRulerMinorTicks: [],
  957. centerScaleRulerMajorMarks: [],
  958. compassTicks: buildCompassTicks(),
  959. compassLabels: buildCompassLabels(),
  960. showStartEntryButton: true,
  961. ...buildSideButtonVisibility('shown'),
  962. ...buildSideButtonState({
  963. sideButtonMode: 'shown',
  964. showGameInfoPanel: false,
  965. showSystemSettingsPanel: false,
  966. showCenterScaleRuler: false,
  967. centerScaleRulerAnchorMode: 'screen-center',
  968. skipButtonEnabled: false,
  969. gameSessionStatus: 'idle',
  970. gpsLockEnabled: false,
  971. gpsLockAvailable: false,
  972. }),
  973. } as unknown as MapPageData,
  974. onLoad(options: MapPageLaunchOptions) {
  975. clearSessionRecoveryPersistTimer()
  976. clearResultExitRedirectTimer()
  977. clearResultExitCountdownTimer()
  978. syncedBackendSessionStartId = ''
  979. syncedBackendSessionFinishId = ''
  980. redirectedToResultPage = false
  981. shouldAutoRestoreRecoverySnapshot = options && options.recoverSession === '1'
  982. shouldAutoStartSessionOnEnter = !!(options && options.autoStartOnEnter === '1')
  983. const recoverySnapshot = loadSessionRecoverySnapshot()
  984. if (shouldAutoRestoreRecoverySnapshot && recoverySnapshot) {
  985. // Recovery should trust the persisted session envelope first so it can
  986. // survive launchId stash misses and still reconstruct the original round.
  987. currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
  988. } else {
  989. currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
  990. if (!hasExplicitLaunchOptions(options) && recoverySnapshot) {
  991. currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
  992. }
  993. }
  994. currentSystemSettingsConfig = undefined
  995. currentRemoteMapConfig = undefined
  996. systemSettingsLockLifetimeActive = false
  997. const storedMockChannelId = loadStoredMockChannelId()
  998. const shouldAutoConnectMockSources = loadMockAutoConnectEnabled()
  999. const systemInfo = wx.getSystemInfoSync()
  1000. const statusBarHeight = systemInfo.statusBarHeight || 0
  1001. const menuButtonRect = wx.getMenuButtonBoundingClientRect()
  1002. const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
  1003. this.setData({
  1004. showStartEntryButton: !shouldAutoStartSessionOnEnter,
  1005. })
  1006. if (mapEngine) {
  1007. mapEngine.destroy()
  1008. mapEngine = null
  1009. }
  1010. mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
  1011. onData: (patch) => {
  1012. const nextPatch = patch as Partial<MapPageData>
  1013. const includeDebugFields = this.data.showDebugPanel
  1014. const includeRulerFields = this.data.showCenterScaleRuler
  1015. let shouldSyncRuntimeSystemSettings = false
  1016. let nextLockLifetimeActive = isSystemSettingsLockLifetimeActive()
  1017. let heartRateSwitchToastText = ''
  1018. const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
  1019. ...nextPatch,
  1020. }, includeDebugFields, includeRulerFields)
  1021. if (
  1022. typeof nextPatch.mockBridgeUrlText === 'string'
  1023. && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
  1024. ) {
  1025. nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
  1026. }
  1027. if (
  1028. typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
  1029. && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
  1030. ) {
  1031. nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
  1032. }
  1033. if (
  1034. typeof nextPatch.mockDebugLogBridgeUrlText === 'string'
  1035. && this.data.mockDebugLogBridgeUrlDraft === this.data.mockDebugLogBridgeUrlText
  1036. ) {
  1037. nextData.mockDebugLogBridgeUrlDraft = nextPatch.mockDebugLogBridgeUrlText
  1038. }
  1039. if (
  1040. typeof nextPatch.mockChannelIdText === 'string'
  1041. && this.data.mockChannelIdDraft === this.data.mockChannelIdText
  1042. ) {
  1043. nextData.mockChannelIdDraft = nextPatch.mockChannelIdText
  1044. }
  1045. updateCenterScaleRulerInputCache(nextPatch)
  1046. const mergedData = {
  1047. ...centerScaleRulerInputCache,
  1048. ...this.data,
  1049. ...nextData,
  1050. } as MapPageData
  1051. const derivedPatch: Partial<MapPageData> = {}
  1052. if (typeof nextPatch.orientationMode === 'string') {
  1053. nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up'
  1054. }
  1055. if (
  1056. this.data.showCenterScaleRuler
  1057. && hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
  1058. ) {
  1059. clearCenterScaleRulerUpdateTimer()
  1060. Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
  1061. }
  1062. if (hasAnyPatchKey(nextPatch as Record<string, unknown>, SIDE_BUTTON_DEP_KEYS)) {
  1063. Object.assign(derivedPatch, buildSideButtonState(mergedData))
  1064. }
  1065. if (typeof nextPatch.punchHintText === 'string') {
  1066. const nextHintText = nextPatch.punchHintText.trim()
  1067. if (nextHintText !== this.data.punchHintText) {
  1068. clearPunchHintDismissTimer()
  1069. clearPunchHintFxTimer()
  1070. nextData.showPunchHintBanner = nextHintText.length > 0
  1071. if (nextHintText.length > 0) {
  1072. nextData.punchHintFxClass = 'game-punch-hint--fx-enter'
  1073. punchHintFxTimer = setTimeout(() => {
  1074. punchHintFxTimer = 0
  1075. this.setData({
  1076. punchHintFxClass: '',
  1077. })
  1078. }, PUNCH_HINT_FX_DURATION_MS) as unknown as number
  1079. const now = Date.now()
  1080. if (mapEngine && now - lastPunchHintHapticAt >= PUNCH_HINT_HAPTIC_GAP_MS) {
  1081. mapEngine.playPunchHintHaptic()
  1082. lastPunchHintHapticAt = now
  1083. }
  1084. punchHintDismissTimer = setTimeout(() => {
  1085. punchHintDismissTimer = 0
  1086. this.setData({
  1087. showPunchHintBanner: false,
  1088. })
  1089. }, PUNCH_HINT_AUTO_HIDE_MS) as unknown as number
  1090. }
  1091. } else if (!nextHintText) {
  1092. clearPunchHintDismissTimer()
  1093. clearPunchHintFxTimer()
  1094. nextData.showPunchHintBanner = false
  1095. nextData.punchHintFxClass = ''
  1096. }
  1097. }
  1098. const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
  1099. ? nextPatch.animationLevel
  1100. : this.data.animationLevel
  1101. let shouldSyncBackendSessionStart = false
  1102. let backendSessionFinishStatus: 'finished' | 'failed' | null = null
  1103. let shouldOpenResultExitPrompt = false
  1104. let resultPageSnapshot: MapEngineResultSnapshot | null = null
  1105. if (nextAnimationLevel === 'lite') {
  1106. clearHudFxTimer('timer')
  1107. clearHudFxTimer('mileage')
  1108. clearHudFxTimer('speed')
  1109. clearHudFxTimer('heartRate')
  1110. nextData.panelTimerFxClass = ''
  1111. nextData.panelMileageFxClass = ''
  1112. nextData.panelSpeedFxClass = ''
  1113. nextData.panelHeartRateFxClass = ''
  1114. } else {
  1115. if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
  1116. clearHudFxTimer('timer')
  1117. nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
  1118. panelTimerFxTimer = setTimeout(() => {
  1119. panelTimerFxTimer = 0
  1120. this.setData({ panelTimerFxClass: '' })
  1121. }, 320) as unknown as number
  1122. }
  1123. if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
  1124. clearHudFxTimer('mileage')
  1125. nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
  1126. panelMileageFxTimer = setTimeout(() => {
  1127. panelMileageFxTimer = 0
  1128. this.setData({ panelMileageFxClass: '' })
  1129. }, 360) as unknown as number
  1130. }
  1131. if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
  1132. clearHudFxTimer('speed')
  1133. nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
  1134. panelSpeedFxTimer = setTimeout(() => {
  1135. panelSpeedFxTimer = 0
  1136. this.setData({ panelSpeedFxClass: '' })
  1137. }, 360) as unknown as number
  1138. }
  1139. if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
  1140. clearHudFxTimer('heartRate')
  1141. nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
  1142. panelHeartRateFxTimer = setTimeout(() => {
  1143. panelHeartRateFxTimer = 0
  1144. this.setData({ panelHeartRateFxClass: '' })
  1145. }, 400) as unknown as number
  1146. }
  1147. }
  1148. if (typeof nextPatch.gameSessionStatus === 'string') {
  1149. if (
  1150. nextPatch.gameSessionStatus !== this.data.gameSessionStatus
  1151. && (nextPatch.gameSessionStatus === 'finished' || nextPatch.gameSessionStatus === 'failed')
  1152. ) {
  1153. systemSettingsLockLifetimeActive = false
  1154. nextLockLifetimeActive = false
  1155. shouldSyncRuntimeSystemSettings = true
  1156. clearSessionRecoverySnapshot()
  1157. clearSessionRecoveryPersistTimer()
  1158. clearResultExitRedirectTimer()
  1159. clearResultExitCountdownTimer()
  1160. resultPageSnapshot = mapEngine ? mapEngine.getResultSceneSnapshot() : null
  1161. nextData.showResultScene = true
  1162. nextData.showDebugPanel = false
  1163. nextData.showGameInfoPanel = false
  1164. nextData.showSystemSettingsPanel = false
  1165. clearGameInfoPanelSyncTimer()
  1166. backendSessionFinishStatus = nextPatch.gameSessionStatus === 'finished' ? 'finished' : 'failed'
  1167. shouldOpenResultExitPrompt = true
  1168. if (resultPageSnapshot) {
  1169. nextData.resultSceneTitle = resultPageSnapshot.title
  1170. nextData.resultSceneSubtitle = resultPageSnapshot.subtitle
  1171. nextData.resultSceneHeroLabel = resultPageSnapshot.heroLabel
  1172. nextData.resultSceneHeroValue = resultPageSnapshot.heroValue
  1173. nextData.resultSceneRows = resultPageSnapshot.rows
  1174. }
  1175. nextData.resultSceneCountdownText = '3 秒后自动进入成绩页'
  1176. } else if (
  1177. nextPatch.gameSessionStatus !== this.data.gameSessionStatus
  1178. && nextPatch.gameSessionStatus === 'idle'
  1179. && !isSystemSettingsLockLifetimeActive()
  1180. ) {
  1181. nextLockLifetimeActive = false
  1182. shouldSyncRuntimeSystemSettings = true
  1183. clearSessionRecoverySnapshot()
  1184. clearSessionRecoveryPersistTimer()
  1185. clearResultExitRedirectTimer()
  1186. clearResultExitCountdownTimer()
  1187. } else if (
  1188. nextPatch.gameSessionStatus !== this.data.gameSessionStatus
  1189. && nextPatch.gameSessionStatus === 'running'
  1190. ) {
  1191. shouldSyncBackendSessionStart = true
  1192. } else if (nextPatch.gameSessionStatus === 'running' || nextPatch.gameSessionStatus === 'idle') {
  1193. nextData.showResultScene = false
  1194. }
  1195. }
  1196. if (
  1197. pendingHeartRateSwitchDeviceName
  1198. && nextPatch.heartRateConnected === true
  1199. && typeof nextPatch.heartRateDeviceText === 'string'
  1200. ) {
  1201. const connectedDeviceName = nextPatch.heartRateDeviceText.trim()
  1202. if (connectedDeviceName && connectedDeviceName === pendingHeartRateSwitchDeviceName) {
  1203. heartRateSwitchToastText = `已切换到 ${connectedDeviceName}`
  1204. nextData.statusText = `已切换心率带:${connectedDeviceName}`
  1205. pendingHeartRateSwitchDeviceName = null
  1206. }
  1207. }
  1208. if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
  1209. this.setData({
  1210. ...nextData,
  1211. ...derivedPatch,
  1212. }, () => {
  1213. if (typeof nextPatch.gameSessionStatus === 'string') {
  1214. this.syncSessionRecoveryLifecycle(nextPatch.gameSessionStatus)
  1215. }
  1216. if (shouldSyncBackendSessionStart) {
  1217. this.syncBackendSessionStart()
  1218. }
  1219. if (backendSessionFinishStatus) {
  1220. this.syncBackendSessionFinish(backendSessionFinishStatus)
  1221. }
  1222. if (shouldOpenResultExitPrompt && resultPageSnapshot) {
  1223. this.stashPendingResultSnapshot(resultPageSnapshot)
  1224. this.presentResultExitPrompt()
  1225. }
  1226. if (heartRateSwitchToastText) {
  1227. wx.showToast({
  1228. title: `${heartRateSwitchToastText},并设为首选设备`,
  1229. icon: 'none',
  1230. duration: 1800,
  1231. })
  1232. }
  1233. if (shouldSyncRuntimeSystemSettings) {
  1234. this.applyRuntimeSystemSettings(nextLockLifetimeActive)
  1235. }
  1236. if (this.data.showGameInfoPanel) {
  1237. this.scheduleGameInfoPanelSnapshotSync()
  1238. }
  1239. })
  1240. } else {
  1241. if (typeof nextPatch.gameSessionStatus === 'string') {
  1242. this.syncSessionRecoveryLifecycle(nextPatch.gameSessionStatus)
  1243. }
  1244. if (shouldSyncBackendSessionStart) {
  1245. this.syncBackendSessionStart()
  1246. }
  1247. if (backendSessionFinishStatus) {
  1248. this.syncBackendSessionFinish(backendSessionFinishStatus)
  1249. }
  1250. if (shouldOpenResultExitPrompt && resultPageSnapshot) {
  1251. this.stashPendingResultSnapshot(resultPageSnapshot)
  1252. this.presentResultExitPrompt()
  1253. }
  1254. if (shouldSyncRuntimeSystemSettings) {
  1255. this.applyRuntimeSystemSettings(nextLockLifetimeActive)
  1256. }
  1257. if (this.data.showGameInfoPanel) {
  1258. this.scheduleGameInfoPanelSnapshotSync()
  1259. }
  1260. }
  1261. },
  1262. onOpenH5Experience: (request) => {
  1263. this.openH5Experience(request)
  1264. },
  1265. })
  1266. mapEngine.applyTelemetryPlayerProfile(getGlobalTelemetryProfile())
  1267. const systemSettingsState = resolveSystemSettingsState(undefined, undefined, false)
  1268. const initialSystemSettings = systemSettingsState.values
  1269. mapEngine.applyCompiledSettingsProfile({
  1270. values: initialSystemSettings,
  1271. locks: systemSettingsState.locks,
  1272. lockLifetimeActive: false,
  1273. })
  1274. mapEngine.setDiagnosticUiEnabled(false)
  1275. centerScaleRulerInputCache = {
  1276. stageWidth: 0,
  1277. stageHeight: 0,
  1278. zoom: 0,
  1279. centerTileY: 0,
  1280. tileSizePx: 0,
  1281. previewScale: 1,
  1282. }
  1283. const initialEngineData = mapEngine.getInitialData()
  1284. this.setData({
  1285. ...initialEngineData,
  1286. ...buildResolvedSystemSettingsPatch(systemSettingsState),
  1287. showDebugPanel: false,
  1288. showGameInfoPanel: false,
  1289. showResultScene: false,
  1290. showSystemSettingsPanel: false,
  1291. statusBarHeight,
  1292. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  1293. hudPanelIndex: 0,
  1294. configSourceText: currentGameLaunchEnvelope.config.configLabel,
  1295. gameInfoTitle: '当前游戏',
  1296. gameInfoSubtitle: '未开始',
  1297. gameInfoLocalRows: [],
  1298. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  1299. resultSceneTitle: '本局结果',
  1300. resultSceneSubtitle: '未开始',
  1301. resultSceneHeroLabel: '本局用时',
  1302. resultSceneHeroValue: '--',
  1303. resultSceneRows: buildEmptyResultSceneSnapshot().rows,
  1304. resultSceneCountdownText: '',
  1305. panelTimerText: '00:00:00',
  1306. panelTimerMode: 'elapsed',
  1307. panelTimerFxClass: '',
  1308. panelMileageText: '0m',
  1309. panelMileageFxClass: '',
  1310. panelActionTagText: '目标',
  1311. panelDistanceTagText: '点距',
  1312. panelTargetSummaryText: '等待选择目标',
  1313. panelDistanceValueText: '--',
  1314. panelDistanceUnitText: '',
  1315. panelProgressText: '0/0',
  1316. showPunchHintBanner: true,
  1317. gameSessionStatus: 'idle',
  1318. gameModeText: '顺序赛',
  1319. gpsLockEnabled: false,
  1320. gpsLockAvailable: false,
  1321. locationSourceMode: 'real',
  1322. locationSourceText: '真实定位',
  1323. mockBridgeConnected: false,
  1324. mockBridgeStatusText: '未连接',
  1325. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  1326. mockChannelIdText: storedMockChannelId,
  1327. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  1328. mockChannelIdDraft: storedMockChannelId,
  1329. mockCoordText: '--',
  1330. mockSpeedText: '--',
  1331. heartRateSourceMode: 'real',
  1332. heartRateSourceText: '真实心率',
  1333. mockHeartRateBridgeConnected: false,
  1334. mockHeartRateBridgeStatusText: '未连接',
  1335. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
  1336. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
  1337. mockHeartRateText: '--',
  1338. mockDebugLogBridgeConnected: false,
  1339. mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
  1340. mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
  1341. mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
  1342. panelSpeedValueText: '0',
  1343. panelSpeedFxClass: '',
  1344. panelTelemetryTone: 'blue',
  1345. gpsLogoStatusText: '未配置',
  1346. gpsLogoSourceText: '--',
  1347. panelHeartRateZoneNameText: '--',
  1348. panelHeartRateZoneRangeText: '',
  1349. heartRateConnected: false,
  1350. heartRateStatusText: '心率带未连接',
  1351. heartRateDeviceText: '--',
  1352. panelHeartRateValueText: '--',
  1353. panelHeartRateFxClass: '',
  1354. panelHeartRateUnitText: '',
  1355. panelCaloriesValueText: '0',
  1356. panelCaloriesUnitText: 'kcal',
  1357. panelAverageSpeedValueText: '0',
  1358. panelAverageSpeedUnitText: 'km/h',
  1359. panelAccuracyValueText: '--',
  1360. panelAccuracyUnitText: '',
  1361. deviceHeadingText: '--',
  1362. devicePoseText: '竖持',
  1363. headingConfidenceText: '低',
  1364. accelerometerText: '--',
  1365. gyroscopeText: '--',
  1366. deviceMotionText: '--',
  1367. compassSourceText: '无数据',
  1368. compassTuningProfileText: initialEngineData.compassTuningProfileText || '平衡',
  1369. punchButtonText: '打点',
  1370. punchButtonEnabled: false,
  1371. skipButtonEnabled: false,
  1372. punchHintText: '等待进入检查点范围',
  1373. punchHintFxClass: '',
  1374. punchFeedbackVisible: false,
  1375. punchFeedbackText: '',
  1376. punchFeedbackTone: 'neutral',
  1377. contentCardVisible: false,
  1378. contentCardTemplate: 'story',
  1379. contentCardTitle: '',
  1380. contentCardBody: '',
  1381. contentCardActions: [],
  1382. contentQuizVisible: false,
  1383. contentQuizQuestionText: '',
  1384. contentQuizCountdownText: '',
  1385. contentQuizOptions: [],
  1386. contentQuizFeedbackVisible: false,
  1387. contentQuizFeedbackText: '',
  1388. contentQuizFeedbackTone: 'neutral',
  1389. punchButtonFxClass: '',
  1390. panelProgressFxClass: '',
  1391. panelDistanceFxClass: '',
  1392. punchFeedbackFxClass: '',
  1393. contentCardFxClass: '',
  1394. mapPulseVisible: false,
  1395. mapPulseLeftPx: 0,
  1396. mapPulseTopPx: 0,
  1397. mapPulseFxClass: '',
  1398. stageFxVisible: false,
  1399. stageFxClass: '',
  1400. compassTicks: buildCompassTicks(),
  1401. compassLabels: buildCompassLabels(),
  1402. ...buildSideButtonVisibility('shown'),
  1403. ...buildSideButtonState({
  1404. sideButtonMode: 'shown',
  1405. showGameInfoPanel: false,
  1406. showSystemSettingsPanel: false,
  1407. showCenterScaleRuler: initialSystemSettings.showCenterScaleRuler,
  1408. centerScaleRulerAnchorMode: initialSystemSettings.centerScaleRulerAnchorMode,
  1409. skipButtonEnabled: false,
  1410. gameSessionStatus: 'idle',
  1411. gpsLockEnabled: false,
  1412. gpsLockAvailable: false,
  1413. }),
  1414. ...buildCenterScaleRulerPatch({
  1415. ...(mapEngine.getInitialData() as MapPageData),
  1416. showCenterScaleRuler: initialSystemSettings.showCenterScaleRuler,
  1417. centerScaleRulerAnchorMode: initialSystemSettings.centerScaleRulerAnchorMode,
  1418. stageWidth: 0,
  1419. stageHeight: 0,
  1420. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  1421. zoom: 0,
  1422. centerTileY: 0,
  1423. tileSizePx: 0,
  1424. }),
  1425. }, () => {
  1426. if (shouldAutoConnectMockSources) {
  1427. this.handleConnectAllMockSources()
  1428. }
  1429. })
  1430. },
  1431. onReady() {
  1432. stageCanvasAttached = false
  1433. this.measureStageAndCanvas()
  1434. this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  1435. const app = getApp<IAppOption>()
  1436. const pendingHeartRateAutoConnect = app.globalData ? app.globalData.pendingHeartRateAutoConnect : null
  1437. if (pendingHeartRateAutoConnect && pendingHeartRateAutoConnect.enabled && mapEngine) {
  1438. const pendingDeviceName = pendingHeartRateAutoConnect.deviceName || '心率带'
  1439. app.globalData.pendingHeartRateAutoConnect = null
  1440. mapEngine.handleConnectHeartRate()
  1441. this.setData({
  1442. statusText: `正在自动连接局前设备:${pendingDeviceName}`,
  1443. heartRateStatusText: `正在自动连接 ${pendingDeviceName}`,
  1444. heartRateDeviceText: pendingDeviceName,
  1445. })
  1446. }
  1447. },
  1448. onShow() {
  1449. if (mapEngine) {
  1450. this.applyCompiledRuntimeProfiles()
  1451. mapEngine.handleAppShow()
  1452. }
  1453. },
  1454. onHide() {
  1455. this.persistSessionRecoverySnapshot()
  1456. clearResultExitRedirectTimer()
  1457. clearResultExitCountdownTimer()
  1458. if (mapEngine) {
  1459. mapEngine.handleAppHide()
  1460. }
  1461. },
  1462. onUnload() {
  1463. this.persistSessionRecoverySnapshot()
  1464. clearSessionRecoveryPersistTimer()
  1465. clearResultExitRedirectTimer()
  1466. clearResultExitCountdownTimer()
  1467. syncedBackendSessionStartId = ''
  1468. syncedBackendSessionFinishId = ''
  1469. clearGameInfoPanelSyncTimer()
  1470. clearCenterScaleRulerSyncTimer()
  1471. clearCenterScaleRulerUpdateTimer()
  1472. clearPunchHintDismissTimer()
  1473. clearPunchHintFxTimer()
  1474. clearHudFxTimer('timer')
  1475. clearHudFxTimer('mileage')
  1476. clearHudFxTimer('speed')
  1477. clearHudFxTimer('heartRate')
  1478. if (mapEngine) {
  1479. mapEngine.destroy()
  1480. mapEngine = null
  1481. }
  1482. currentSystemSettingsConfig = undefined
  1483. currentRemoteMapConfig = undefined
  1484. systemSettingsLockLifetimeActive = false
  1485. currentGameLaunchEnvelope = getDemoGameLaunchEnvelope()
  1486. shouldAutoRestoreRecoverySnapshot = false
  1487. shouldAutoStartSessionOnEnter = false
  1488. redirectedToResultPage = false
  1489. stageCanvasAttached = false
  1490. },
  1491. loadGameLaunchEnvelope(envelope: GameLaunchEnvelope) {
  1492. emitSimulatorLaunchDiagnostic('loadGameLaunchEnvelope', {
  1493. launchEventId: envelope.business && envelope.business.eventId ? envelope.business.eventId : '',
  1494. launchSessionId: envelope.business && envelope.business.sessionId ? envelope.business.sessionId : '',
  1495. configUrl: envelope.config.configUrl || '',
  1496. configReleaseId: envelope.config.releaseId || '',
  1497. resolvedManifestUrl: envelope.resolvedRelease && envelope.resolvedRelease.manifestUrl
  1498. ? envelope.resolvedRelease.manifestUrl
  1499. : '',
  1500. resolvedReleaseId: envelope.resolvedRelease && envelope.resolvedRelease.releaseId
  1501. ? envelope.resolvedRelease.releaseId
  1502. : '',
  1503. launchVariantId: envelope.variant && envelope.variant.variantId ? envelope.variant.variantId : null,
  1504. launchVariantRouteCode: envelope.variant && envelope.variant.routeCode ? envelope.variant.routeCode : null,
  1505. runtimeCourseVariantId: envelope.runtime && envelope.runtime.courseVariantId ? envelope.runtime.courseVariantId : null,
  1506. })
  1507. this.loadMapConfigFromRemote(
  1508. envelope.config.configUrl,
  1509. envelope.config.configLabel,
  1510. )
  1511. },
  1512. persistSessionRecoverySnapshot() {
  1513. if (!mapEngine || !currentRemoteMapConfig) {
  1514. return false
  1515. }
  1516. const runtimeSnapshot = mapEngine.buildSessionRecoveryRuntimeSnapshot()
  1517. if (!runtimeSnapshot) {
  1518. return false
  1519. }
  1520. const snapshot: SessionRecoverySnapshot = {
  1521. schemaVersion: 1,
  1522. savedAt: Date.now(),
  1523. launchEnvelope: currentGameLaunchEnvelope,
  1524. configAppId: currentRemoteMapConfig.configAppId,
  1525. configVersion: currentRemoteMapConfig.configVersion,
  1526. runtime: runtimeSnapshot,
  1527. }
  1528. saveSessionRecoverySnapshot(snapshot)
  1529. return true
  1530. },
  1531. syncBackendSessionStart() {
  1532. const sessionContext = getCurrentBackendSessionContext()
  1533. if (!sessionContext || syncedBackendSessionStartId === sessionContext.sessionId) {
  1534. return
  1535. }
  1536. startSession({
  1537. baseUrl: getCurrentBackendBaseUrl(),
  1538. sessionId: sessionContext.sessionId,
  1539. sessionToken: sessionContext.sessionToken,
  1540. })
  1541. .then(() => {
  1542. syncedBackendSessionStartId = sessionContext.sessionId
  1543. })
  1544. .catch((error) => {
  1545. const message = error && error.message ? error.message : '未知错误'
  1546. this.setData({
  1547. statusText: `session start 上报失败: ${message}`,
  1548. })
  1549. })
  1550. },
  1551. syncBackendSessionFinish(statusOverride?: 'finished' | 'failed' | 'cancelled') {
  1552. const sessionContext = getCurrentBackendSessionContext()
  1553. if (!sessionContext || syncedBackendSessionFinishId === sessionContext.sessionId || !mapEngine) {
  1554. return
  1555. }
  1556. const finishSummary = mapEngine.getSessionFinishSummary(statusOverride)
  1557. if (!finishSummary) {
  1558. return
  1559. }
  1560. const summaryPayload: BackendSessionFinishSummaryPayload = {}
  1561. if (typeof finishSummary.finalDurationSec === 'number') {
  1562. summaryPayload.finalDurationSec = finishSummary.finalDurationSec
  1563. }
  1564. if (typeof finishSummary.finalScore === 'number') {
  1565. summaryPayload.finalScore = finishSummary.finalScore
  1566. }
  1567. if (typeof finishSummary.completedControls === 'number') {
  1568. summaryPayload.completedControls = finishSummary.completedControls
  1569. }
  1570. if (typeof finishSummary.totalControls === 'number') {
  1571. summaryPayload.totalControls = finishSummary.totalControls
  1572. }
  1573. if (typeof finishSummary.distanceMeters === 'number') {
  1574. summaryPayload.distanceMeters = finishSummary.distanceMeters
  1575. }
  1576. if (typeof finishSummary.averageSpeedKmh === 'number') {
  1577. summaryPayload.averageSpeedKmh = finishSummary.averageSpeedKmh
  1578. }
  1579. finishSession({
  1580. baseUrl: getCurrentBackendBaseUrl(),
  1581. sessionId: sessionContext.sessionId,
  1582. sessionToken: sessionContext.sessionToken,
  1583. status: finishSummary.status,
  1584. summary: summaryPayload,
  1585. })
  1586. .then(() => {
  1587. syncedBackendSessionFinishId = sessionContext.sessionId
  1588. })
  1589. .catch((error) => {
  1590. const message = error && error.message ? error.message : '未知错误'
  1591. this.setData({
  1592. statusText: `session finish 上报失败: ${message}`,
  1593. })
  1594. })
  1595. },
  1596. reportAbandonedRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
  1597. const sessionContext = getBackendSessionContextFromLaunchEnvelope(snapshot.launchEnvelope)
  1598. if (!sessionContext) {
  1599. reportBackendClientLog({
  1600. level: 'warn',
  1601. category: 'session-recovery',
  1602. message: 'abandon recovery without valid session context',
  1603. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1604. ? snapshot.launchEnvelope.business.eventId
  1605. : '',
  1606. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1607. ? snapshot.launchEnvelope.config.releaseId
  1608. : '',
  1609. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1610. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1611. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1612. ? snapshot.launchEnvelope.config.configUrl
  1613. : '',
  1614. details: {
  1615. phase: 'abandon-no-session',
  1616. },
  1617. })
  1618. clearSessionRecoverySnapshot()
  1619. return
  1620. }
  1621. reportBackendClientLog({
  1622. level: 'info',
  1623. category: 'session-recovery',
  1624. message: 'abandon recovery requested',
  1625. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1626. ? snapshot.launchEnvelope.business.eventId
  1627. : '',
  1628. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1629. ? snapshot.launchEnvelope.config.releaseId
  1630. : '',
  1631. sessionId: sessionContext.sessionId,
  1632. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1633. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1634. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1635. ? snapshot.launchEnvelope.config.configUrl
  1636. : '',
  1637. details: {
  1638. phase: 'abandon-requested',
  1639. },
  1640. })
  1641. finishSession({
  1642. baseUrl: getCurrentBackendBaseUrl(),
  1643. sessionId: sessionContext.sessionId,
  1644. sessionToken: sessionContext.sessionToken,
  1645. status: 'cancelled',
  1646. summary: {},
  1647. })
  1648. .then(() => {
  1649. syncedBackendSessionFinishId = sessionContext.sessionId
  1650. reportBackendClientLog({
  1651. level: 'info',
  1652. category: 'session-recovery',
  1653. message: 'abandon recovery synced as cancelled',
  1654. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1655. ? snapshot.launchEnvelope.business.eventId
  1656. : '',
  1657. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1658. ? snapshot.launchEnvelope.config.releaseId
  1659. : '',
  1660. sessionId: sessionContext.sessionId,
  1661. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1662. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1663. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1664. ? snapshot.launchEnvelope.config.configUrl
  1665. : '',
  1666. details: {
  1667. phase: 'abandon-finished',
  1668. },
  1669. })
  1670. clearSessionRecoverySnapshot()
  1671. wx.showToast({
  1672. title: '已放弃上次对局',
  1673. icon: 'none',
  1674. duration: 1400,
  1675. })
  1676. })
  1677. .catch((error) => {
  1678. reportBackendClientLog({
  1679. level: 'warn',
  1680. category: 'session-recovery',
  1681. message: 'abandon recovery finish(cancelled) failed',
  1682. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1683. ? snapshot.launchEnvelope.business.eventId
  1684. : '',
  1685. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1686. ? snapshot.launchEnvelope.config.releaseId
  1687. : '',
  1688. sessionId: sessionContext.sessionId,
  1689. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1690. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1691. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1692. ? snapshot.launchEnvelope.config.configUrl
  1693. : '',
  1694. details: {
  1695. phase: 'abandon-failed',
  1696. message: error && error.message ? error.message : '未知错误',
  1697. },
  1698. })
  1699. clearSessionRecoverySnapshot()
  1700. const message = error && error.message ? error.message : '未知错误'
  1701. this.setData({
  1702. statusText: `放弃恢复已生效,后端取消上报失败: ${message}`,
  1703. })
  1704. wx.showToast({
  1705. title: '已放弃上次对局',
  1706. icon: 'none',
  1707. duration: 1400,
  1708. })
  1709. })
  1710. },
  1711. stashPendingResultSnapshot(snapshot: MapEngineResultSnapshot) {
  1712. const app = getApp<IAppOption>()
  1713. if (app.globalData) {
  1714. app.globalData.pendingResultSnapshot = snapshot
  1715. app.globalData.pendingResultLaunchEnvelope = currentGameLaunchEnvelope
  1716. }
  1717. },
  1718. redirectToResultPage() {
  1719. if (redirectedToResultPage) {
  1720. return
  1721. }
  1722. clearResultExitRedirectTimer()
  1723. clearResultExitCountdownTimer()
  1724. redirectedToResultPage = true
  1725. const sessionContext = getCurrentBackendSessionContext()
  1726. const resultUrl = sessionContext
  1727. ? `/pages/result/result?sessionId=${encodeURIComponent(sessionContext.sessionId)}`
  1728. : '/pages/result/result'
  1729. wx.redirectTo({
  1730. url: resultUrl,
  1731. })
  1732. },
  1733. presentResultExitPrompt() {
  1734. clearResultExitRedirectTimer()
  1735. clearResultExitCountdownTimer()
  1736. let remainingSeconds = Math.ceil(RESULT_EXIT_REDIRECT_DELAY_MS / 1000)
  1737. this.setData({
  1738. showResultScene: true,
  1739. resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
  1740. })
  1741. resultExitCountdownTimer = setInterval(() => {
  1742. remainingSeconds -= 1
  1743. if (remainingSeconds <= 0) {
  1744. clearResultExitCountdownTimer()
  1745. return
  1746. }
  1747. this.setData({
  1748. resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
  1749. })
  1750. }, 1000) as unknown as number
  1751. resultExitRedirectTimer = setTimeout(() => {
  1752. resultExitRedirectTimer = 0
  1753. this.redirectToResultPage()
  1754. }, RESULT_EXIT_REDIRECT_DELAY_MS) as unknown as number
  1755. },
  1756. restoreRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
  1757. systemSettingsLockLifetimeActive = true
  1758. this.applyRuntimeSystemSettings(true)
  1759. const restored = mapEngine ? mapEngine.restoreSessionRecoveryRuntimeSnapshot(snapshot.runtime) : false
  1760. if (!restored) {
  1761. reportBackendClientLog({
  1762. level: 'warn',
  1763. category: 'session-recovery',
  1764. message: 'recovery restore failed',
  1765. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1766. ? snapshot.launchEnvelope.business.eventId
  1767. : '',
  1768. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1769. ? snapshot.launchEnvelope.config.releaseId
  1770. : '',
  1771. sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
  1772. ? snapshot.launchEnvelope.business.sessionId
  1773. : '',
  1774. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1775. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1776. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1777. ? snapshot.launchEnvelope.config.configUrl
  1778. : '',
  1779. details: {
  1780. phase: 'restore-failed',
  1781. },
  1782. })
  1783. clearSessionRecoverySnapshot()
  1784. wx.showToast({
  1785. title: '恢复失败,已回到初始状态',
  1786. icon: 'none',
  1787. duration: 1600,
  1788. })
  1789. return false
  1790. }
  1791. this.setData({
  1792. showResultScene: false,
  1793. showDebugPanel: false,
  1794. showGameInfoPanel: false,
  1795. showSystemSettingsPanel: false,
  1796. showStartEntryButton: false,
  1797. })
  1798. const sessionContext = getCurrentBackendSessionContext()
  1799. if (sessionContext) {
  1800. syncedBackendSessionStartId = sessionContext.sessionId
  1801. }
  1802. reportBackendClientLog({
  1803. level: 'info',
  1804. category: 'session-recovery',
  1805. message: 'recovery restored',
  1806. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1807. ? snapshot.launchEnvelope.business.eventId
  1808. : '',
  1809. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1810. ? snapshot.launchEnvelope.config.releaseId
  1811. : '',
  1812. sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
  1813. ? snapshot.launchEnvelope.business.sessionId
  1814. : '',
  1815. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1816. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1817. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1818. ? snapshot.launchEnvelope.config.configUrl
  1819. : '',
  1820. details: {
  1821. phase: 'restored',
  1822. },
  1823. })
  1824. this.syncSessionRecoveryLifecycle('running')
  1825. return true
  1826. },
  1827. syncSessionRecoveryLifecycle(status: MapPageData['gameSessionStatus']) {
  1828. if (status === 'running') {
  1829. this.persistSessionRecoverySnapshot()
  1830. if (!sessionRecoveryPersistTimer) {
  1831. sessionRecoveryPersistTimer = setInterval(() => {
  1832. this.persistSessionRecoverySnapshot()
  1833. }, SESSION_RECOVERY_PERSIST_INTERVAL_MS) as unknown as number
  1834. }
  1835. return
  1836. }
  1837. clearSessionRecoveryPersistTimer()
  1838. },
  1839. maybePromptSessionRecoveryRestore(config: RemoteMapConfig) {
  1840. const snapshot = loadSessionRecoverySnapshot()
  1841. if (!snapshot || !mapEngine) {
  1842. return false
  1843. }
  1844. if (
  1845. snapshot.launchEnvelope.config.configUrl !== currentGameLaunchEnvelope.config.configUrl
  1846. || snapshot.configAppId !== config.configAppId
  1847. ) {
  1848. reportBackendClientLog({
  1849. level: 'warn',
  1850. category: 'session-recovery',
  1851. message: 'recovery snapshot dropped due to config mismatch',
  1852. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1853. ? snapshot.launchEnvelope.business.eventId
  1854. : '',
  1855. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1856. ? snapshot.launchEnvelope.config.releaseId
  1857. : '',
  1858. sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
  1859. ? snapshot.launchEnvelope.business.sessionId
  1860. : '',
  1861. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1862. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1863. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1864. ? snapshot.launchEnvelope.config.configUrl
  1865. : '',
  1866. details: {
  1867. phase: 'config-mismatch',
  1868. currentConfigUrl: currentGameLaunchEnvelope.config.configUrl,
  1869. snapshotConfigUrl: snapshot.launchEnvelope.config.configUrl,
  1870. currentConfigAppId: config.configAppId,
  1871. snapshotConfigAppId: snapshot.configAppId,
  1872. },
  1873. })
  1874. clearSessionRecoverySnapshot()
  1875. this.setData({
  1876. statusText: '检测到旧局恢复记录,但当前配置源已变化,已回到初始状态',
  1877. })
  1878. return false
  1879. }
  1880. if (shouldAutoRestoreRecoverySnapshot) {
  1881. shouldAutoRestoreRecoverySnapshot = false
  1882. reportBackendClientLog({
  1883. level: 'info',
  1884. category: 'session-recovery',
  1885. message: 'auto recovery requested',
  1886. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1887. ? snapshot.launchEnvelope.business.eventId
  1888. : '',
  1889. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1890. ? snapshot.launchEnvelope.config.releaseId
  1891. : '',
  1892. sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
  1893. ? snapshot.launchEnvelope.business.sessionId
  1894. : '',
  1895. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1896. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1897. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1898. ? snapshot.launchEnvelope.config.configUrl
  1899. : '',
  1900. details: {
  1901. phase: 'auto-restore',
  1902. },
  1903. })
  1904. this.restoreRecoverySnapshot(snapshot)
  1905. return true
  1906. }
  1907. this.setData({
  1908. showStartEntryButton: true,
  1909. })
  1910. wx.showModal({
  1911. title: '恢复对局',
  1912. content: '检测到上次有未正常结束的对局,是否继续恢复?',
  1913. confirmText: '继续恢复',
  1914. cancelText: '放弃',
  1915. success: (result) => {
  1916. if (!result.confirm) {
  1917. this.reportAbandonedRecoverySnapshot(snapshot)
  1918. return
  1919. }
  1920. this.restoreRecoverySnapshot(snapshot)
  1921. },
  1922. })
  1923. return true
  1924. },
  1925. maybeAutoStartSessionOnEnter() {
  1926. if (!shouldAutoStartSessionOnEnter || !mapEngine) {
  1927. return
  1928. }
  1929. shouldAutoStartSessionOnEnter = false
  1930. systemSettingsLockLifetimeActive = true
  1931. this.applyRuntimeSystemSettings(true)
  1932. this.setData({
  1933. showStartEntryButton: false,
  1934. })
  1935. mapEngine.handleStartGame()
  1936. },
  1937. compileCurrentRuntimeProfile(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
  1938. if (!currentRemoteMapConfig) {
  1939. return null
  1940. }
  1941. return compileRuntimeProfile(currentRemoteMapConfig, {
  1942. playerTelemetryProfile: getGlobalTelemetryProfile(),
  1943. settingsLockLifetimeActive: lockLifetimeActive,
  1944. })
  1945. },
  1946. applyCompiledRuntimeProfiles(
  1947. lockLifetimeActive = isSystemSettingsLockLifetimeActive(),
  1948. options?: {
  1949. includeSettings?: boolean
  1950. includeMap?: boolean
  1951. includeGame?: boolean
  1952. includePresentation?: boolean
  1953. includeTelemetry?: boolean
  1954. includeFeedback?: boolean
  1955. },
  1956. ) {
  1957. const currentEngine = mapEngine
  1958. if (!currentEngine) {
  1959. return null
  1960. }
  1961. const compiledProfile = this.compileCurrentRuntimeProfile(lockLifetimeActive)
  1962. if (!compiledProfile) {
  1963. return null
  1964. }
  1965. if (options && options.includeMap) {
  1966. currentEngine.applyCompiledMapProfile(compiledProfile.map)
  1967. }
  1968. if (options && options.includeSettings) {
  1969. currentEngine.applyCompiledSettingsProfile(compiledProfile.settings)
  1970. }
  1971. if (options && options.includeGame) {
  1972. currentEngine.applyCompiledGameProfile(compiledProfile.game)
  1973. }
  1974. if (options && options.includePresentation) {
  1975. currentEngine.applyCompiledPresentationProfile(compiledProfile.presentation)
  1976. }
  1977. if (!options || options.includeTelemetry !== false) {
  1978. currentEngine.applyCompiledTelemetryProfile(compiledProfile.telemetry)
  1979. }
  1980. if (!options || options.includeFeedback !== false) {
  1981. currentEngine.applyCompiledFeedbackProfile(compiledProfile.feedback)
  1982. }
  1983. return compiledProfile
  1984. },
  1985. applyRuntimeSystemSettings(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
  1986. const currentEngine = mapEngine
  1987. if (!currentEngine) {
  1988. return null
  1989. }
  1990. const compiledProfile = this.applyCompiledRuntimeProfiles(lockLifetimeActive, {
  1991. includeSettings: true,
  1992. })
  1993. || {
  1994. settings: resolveSystemSettingsState(
  1995. currentSystemSettingsConfig,
  1996. undefined,
  1997. lockLifetimeActive,
  1998. ),
  1999. }
  2000. const resolvedSettings = compiledProfile.settings
  2001. const engineSnapshot = currentEngine.getInitialData() as Partial<MapPageData>
  2002. updateCenterScaleRulerInputCache(engineSnapshot)
  2003. const resolvedPatch = buildResolvedSystemSettingsPatch(resolvedSettings)
  2004. const mergedData = {
  2005. ...centerScaleRulerInputCache,
  2006. ...this.data,
  2007. ...engineSnapshot,
  2008. ...resolvedPatch,
  2009. } as MapPageData
  2010. this.setData({
  2011. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, resolvedSettings.values.showCenterScaleRuler),
  2012. ...resolvedPatch,
  2013. ...buildCenterScaleRulerPatch(mergedData),
  2014. ...buildSideButtonState(mergedData),
  2015. })
  2016. return resolvedSettings
  2017. },
  2018. persistAndApplySystemSettings(
  2019. patch: Partial<StoredUserSettings>,
  2020. options?: {
  2021. applyCenterScaleRuler?: boolean
  2022. },
  2023. ) {
  2024. updateStoredUserSettings(patch)
  2025. const lockLifetimeActive = isSystemSettingsLockLifetimeActive()
  2026. const resolvedSettings = this.applyRuntimeSystemSettings(lockLifetimeActive)
  2027. if (!resolvedSettings || !(options && options.applyCenterScaleRuler)) {
  2028. return resolvedSettings
  2029. }
  2030. this.applyCenterScaleRulerSettings(
  2031. resolvedSettings.values.showCenterScaleRuler,
  2032. resolvedSettings.values.centerScaleRulerAnchorMode,
  2033. )
  2034. return resolvedSettings
  2035. },
  2036. loadMapConfigFromRemote(configUrl: string, configLabel: string) {
  2037. const currentEngine = mapEngine
  2038. if (!currentEngine) {
  2039. return
  2040. }
  2041. this.setData({
  2042. configSourceText: configLabel,
  2043. configStatusText: `加载中: ${configLabel}`,
  2044. })
  2045. loadRemoteMapConfig(configUrl)
  2046. .then((config) => {
  2047. if (mapEngine !== currentEngine) {
  2048. return
  2049. }
  2050. emitSimulatorLaunchDiagnostic('loadRemoteMapConfig:resolved', {
  2051. launchEventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
  2052. ? currentGameLaunchEnvelope.business.eventId
  2053. : '',
  2054. configUrl,
  2055. configVersion: config.configVersion || '',
  2056. schemaVersion: config.configSchemaVersion || '',
  2057. playfieldKind: config.playfieldKind || '',
  2058. gameMode: config.gameMode || '',
  2059. configTitle: config.configTitle || '',
  2060. })
  2061. currentEngine.applyRemoteMapConfig(config)
  2062. this.applyConfiguredSystemSettings(config)
  2063. const compiledProfile = this.applyCompiledRuntimeProfiles(true, {
  2064. includeMap: true,
  2065. includeGame: true,
  2066. includePresentation: true,
  2067. })
  2068. if (compiledProfile) {
  2069. reportBackendClientLog({
  2070. level: 'info',
  2071. category: 'runtime-compiler',
  2072. message: 'compiled runtime profile applied',
  2073. eventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
  2074. ? currentGameLaunchEnvelope.business.eventId
  2075. : '',
  2076. releaseId: currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.releaseId
  2077. ? currentGameLaunchEnvelope.config.releaseId
  2078. : '',
  2079. sessionId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.sessionId
  2080. ? currentGameLaunchEnvelope.business.sessionId
  2081. : '',
  2082. manifestUrl: currentGameLaunchEnvelope.resolvedRelease && currentGameLaunchEnvelope.resolvedRelease.manifestUrl
  2083. ? currentGameLaunchEnvelope.resolvedRelease.manifestUrl
  2084. : currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.configUrl
  2085. ? currentGameLaunchEnvelope.config.configUrl
  2086. : '',
  2087. details: {
  2088. phase: 'compiled-runtime-applied',
  2089. schemaVersion: config.configSchemaVersion || '',
  2090. playfield: {
  2091. kind: config.playfieldKind || '',
  2092. },
  2093. game: {
  2094. mode: config.gameMode || '',
  2095. },
  2096. },
  2097. })
  2098. }
  2099. const recoveryHandled = this.maybePromptSessionRecoveryRestore(config)
  2100. if (!recoveryHandled) {
  2101. this.maybeAutoStartSessionOnEnter()
  2102. } else {
  2103. shouldAutoStartSessionOnEnter = false
  2104. }
  2105. })
  2106. .catch((error) => {
  2107. if (mapEngine !== currentEngine) {
  2108. return
  2109. }
  2110. emitSimulatorLaunchDiagnostic('loadRemoteMapConfig:error', {
  2111. launchEventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
  2112. ? currentGameLaunchEnvelope.business.eventId
  2113. : '',
  2114. configUrl,
  2115. message: error && error.message ? error.message : '未知错误',
  2116. })
  2117. const rawErrorMessage = error && error.message ? error.message : '未知错误'
  2118. const errorMessage = rawErrorMessage.indexOf('404') >= 0
  2119. ? `release manifest 不存在或未发布 (${configLabel})`
  2120. : rawErrorMessage
  2121. this.setData({
  2122. configStatusText: `载入失败: ${errorMessage}`,
  2123. statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
  2124. })
  2125. })
  2126. },
  2127. applyConfiguredSystemSettings(config: RemoteMapConfig) {
  2128. currentRemoteMapConfig = config
  2129. currentSystemSettingsConfig = config.systemSettingsConfig
  2130. systemSettingsLockLifetimeActive = true
  2131. this.applyRuntimeSystemSettings(true)
  2132. },
  2133. measureStageAndCanvas(onApplied?: () => void) {
  2134. const page = this
  2135. const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
  2136. const fallbackRect = getFallbackStageRect()
  2137. const rect: MapEngineStageRect = {
  2138. width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
  2139. height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
  2140. left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
  2141. top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
  2142. }
  2143. const currentEngine = mapEngine
  2144. if (!currentEngine) {
  2145. return
  2146. }
  2147. currentEngine.setStage(rect)
  2148. if (onApplied) {
  2149. onApplied()
  2150. }
  2151. if (stageCanvasAttached) {
  2152. return
  2153. }
  2154. const canvasQuery = wx.createSelectorQuery().in(page)
  2155. canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
  2156. canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
  2157. canvasQuery.exec((canvasRes) => {
  2158. const canvasRef = canvasRes[0] as any
  2159. const labelCanvasRef = canvasRes[1] as any
  2160. if (!canvasRef || !canvasRef.node) {
  2161. page.setData({
  2162. statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
  2163. })
  2164. return
  2165. }
  2166. const dpr = wx.getSystemInfoSync().pixelRatio || 1
  2167. try {
  2168. currentEngine.attachCanvas(
  2169. canvasRef.node,
  2170. rect.width,
  2171. rect.height,
  2172. dpr,
  2173. labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
  2174. )
  2175. stageCanvasAttached = true
  2176. } catch (error) {
  2177. page.setData({
  2178. statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
  2179. })
  2180. }
  2181. })
  2182. }
  2183. const query = wx.createSelectorQuery().in(page)
  2184. query.select('.map-stage').boundingClientRect()
  2185. query.exec((res) => {
  2186. const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
  2187. applyStage(rect)
  2188. })
  2189. },
  2190. handleTouchStart(event: WechatMiniprogram.TouchEvent) {
  2191. if (mapEngine) {
  2192. mapEngine.handleTouchStart(event)
  2193. }
  2194. },
  2195. handleTouchMove(event: WechatMiniprogram.TouchEvent) {
  2196. if (mapEngine) {
  2197. mapEngine.handleTouchMove(event)
  2198. }
  2199. },
  2200. handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
  2201. if (mapEngine) {
  2202. mapEngine.handleTouchEnd(event)
  2203. }
  2204. },
  2205. handleTouchCancel() {
  2206. if (mapEngine) {
  2207. mapEngine.handleTouchCancel()
  2208. }
  2209. },
  2210. handleRecenter() {
  2211. if (mapEngine) {
  2212. mapEngine.handleRecenter()
  2213. }
  2214. },
  2215. handleRotateStep() {
  2216. if (mapEngine) {
  2217. mapEngine.handleRotateStep()
  2218. }
  2219. },
  2220. handleRotationReset() {
  2221. if (mapEngine) {
  2222. mapEngine.handleRotationReset()
  2223. }
  2224. },
  2225. handleSetManualMode() {
  2226. if (mapEngine) {
  2227. mapEngine.handleSetManualMode()
  2228. }
  2229. },
  2230. handleSetNorthUpMode() {
  2231. if (mapEngine) {
  2232. mapEngine.handleSetNorthUpMode()
  2233. }
  2234. },
  2235. handleSetHeadingUpMode() {
  2236. if (mapEngine) {
  2237. mapEngine.handleSetHeadingUpMode()
  2238. }
  2239. },
  2240. handleCycleNorthReferenceMode() {
  2241. if (mapEngine) {
  2242. mapEngine.handleCycleNorthReferenceMode()
  2243. }
  2244. },
  2245. handleAutoRotateCalibrate() {
  2246. if (mapEngine) {
  2247. mapEngine.handleAutoRotateCalibrate()
  2248. }
  2249. },
  2250. handleToggleGpsTracking() {
  2251. if (mapEngine) {
  2252. mapEngine.handleToggleGpsTracking()
  2253. }
  2254. },
  2255. handleSetRealLocationMode() {
  2256. if (mapEngine) {
  2257. mapEngine.handleSetRealLocationMode()
  2258. }
  2259. },
  2260. handleSetMockLocationMode() {
  2261. if (mapEngine) {
  2262. mapEngine.handleSetMockLocationMode()
  2263. }
  2264. },
  2265. handleConnectMockLocationBridge() {
  2266. if (mapEngine) {
  2267. mapEngine.handleConnectMockLocationBridge()
  2268. }
  2269. },
  2270. handleConnectAllMockSources() {
  2271. if (!mapEngine) {
  2272. return
  2273. }
  2274. const channelId = (this.data.mockChannelIdDraft || '').trim() || 'default'
  2275. this.setData({
  2276. mockChannelIdDraft: channelId,
  2277. })
  2278. persistMockChannelId(channelId)
  2279. persistMockAutoConnectEnabled(true)
  2280. setGlobalMockDebugBridgeChannelId(channelId)
  2281. setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2282. persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2283. setGlobalMockDebugBridgeEnabled(true)
  2284. mapEngine.handleSetMockChannelId(channelId)
  2285. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  2286. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  2287. mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2288. mapEngine.handleConnectMockLocationBridge()
  2289. mapEngine.handleSetMockLocationMode()
  2290. mapEngine.handleSetMockHeartRateMode()
  2291. mapEngine.handleConnectMockHeartRateBridge()
  2292. mapEngine.handleConnectMockDebugLogBridge()
  2293. },
  2294. handleOpenWebViewTest() {
  2295. wx.navigateTo({
  2296. url: '/pages/webview-test/webview-test',
  2297. })
  2298. },
  2299. handleMockChannelIdInput(event: WechatMiniprogram.Input) {
  2300. this.setData({
  2301. mockChannelIdDraft: event.detail.value,
  2302. })
  2303. },
  2304. handleSaveMockChannelId() {
  2305. const channelId = (this.data.mockChannelIdDraft || '').trim() || 'default'
  2306. this.setData({
  2307. mockChannelIdDraft: channelId,
  2308. })
  2309. persistMockChannelId(channelId)
  2310. setGlobalMockDebugBridgeChannelId(channelId)
  2311. if (mapEngine) {
  2312. mapEngine.handleSetMockChannelId(channelId)
  2313. }
  2314. },
  2315. handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
  2316. this.setData({
  2317. mockBridgeUrlDraft: event.detail.value,
  2318. })
  2319. },
  2320. handleSaveMockBridgeUrl() {
  2321. if (mapEngine) {
  2322. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  2323. }
  2324. },
  2325. handleDisconnectMockLocationBridge() {
  2326. persistMockAutoConnectEnabled(false)
  2327. if (mapEngine) {
  2328. mapEngine.handleDisconnectMockLocationBridge()
  2329. }
  2330. },
  2331. handleSetRealHeartRateMode() {
  2332. if (mapEngine) {
  2333. mapEngine.handleSetRealHeartRateMode()
  2334. }
  2335. },
  2336. handleSetMockHeartRateMode() {
  2337. if (mapEngine) {
  2338. mapEngine.handleSetMockHeartRateMode()
  2339. }
  2340. },
  2341. handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
  2342. this.setData({
  2343. mockHeartRateBridgeUrlDraft: event.detail.value,
  2344. })
  2345. },
  2346. handleSaveMockHeartRateBridgeUrl() {
  2347. if (mapEngine) {
  2348. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  2349. }
  2350. },
  2351. handleMockDebugLogBridgeUrlInput(event: WechatMiniprogram.Input) {
  2352. this.setData({
  2353. mockDebugLogBridgeUrlDraft: event.detail.value,
  2354. })
  2355. },
  2356. handleSaveMockDebugLogBridgeUrl() {
  2357. persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2358. setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2359. if (mapEngine) {
  2360. mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2361. }
  2362. },
  2363. handleConnectMockDebugLogBridge() {
  2364. setGlobalMockDebugBridgeChannelId((this.data.mockChannelIdDraft || '').trim() || 'default')
  2365. setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2366. setGlobalMockDebugBridgeEnabled(true)
  2367. if (mapEngine) {
  2368. mapEngine.handleConnectMockDebugLogBridge()
  2369. }
  2370. },
  2371. handleDisconnectMockDebugLogBridge() {
  2372. persistMockAutoConnectEnabled(false)
  2373. setGlobalMockDebugBridgeEnabled(false)
  2374. if (mapEngine) {
  2375. mapEngine.handleDisconnectMockDebugLogBridge()
  2376. }
  2377. },
  2378. handleConnectMockHeartRateBridge() {
  2379. if (mapEngine) {
  2380. mapEngine.handleConnectMockHeartRateBridge()
  2381. }
  2382. },
  2383. handleDisconnectMockHeartRateBridge() {
  2384. persistMockAutoConnectEnabled(false)
  2385. if (mapEngine) {
  2386. mapEngine.handleDisconnectMockHeartRateBridge()
  2387. }
  2388. },
  2389. handleConnectHeartRate() {
  2390. if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
  2391. return
  2392. }
  2393. if (mapEngine) {
  2394. mapEngine.handleConnectHeartRate()
  2395. }
  2396. },
  2397. handleOpenHeartRateDevicePicker() {
  2398. if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
  2399. return
  2400. }
  2401. this.setData({
  2402. showHeartRateDevicePicker: true,
  2403. })
  2404. if (mapEngine) {
  2405. mapEngine.handleConnectHeartRate()
  2406. }
  2407. },
  2408. handleCloseHeartRateDevicePicker() {
  2409. this.setData({
  2410. showHeartRateDevicePicker: false,
  2411. })
  2412. },
  2413. handleDisconnectHeartRate() {
  2414. if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
  2415. return
  2416. }
  2417. if (mapEngine) {
  2418. mapEngine.handleDisconnectHeartRate()
  2419. }
  2420. },
  2421. handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  2422. if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
  2423. const targetDeviceId = event.currentTarget.dataset.deviceId
  2424. const targetDevice = this.data.heartRateDiscoveredDevices.find((item) => item.deviceId === targetDeviceId)
  2425. pendingHeartRateSwitchDeviceName = targetDevice ? targetDevice.name : null
  2426. mapEngine.handleConnectHeartRateDevice(targetDeviceId)
  2427. this.setData({
  2428. showHeartRateDevicePicker: false,
  2429. statusText: targetDevice
  2430. ? `正在切换到 ${targetDevice.name}`
  2431. : '正在切换心率带设备',
  2432. })
  2433. }
  2434. },
  2435. handleClearPreferredHeartRateDevice() {
  2436. if (this.data.lockHeartRateDevice) {
  2437. return
  2438. }
  2439. if (mapEngine) {
  2440. mapEngine.handleClearPreferredHeartRateDevice()
  2441. }
  2442. },
  2443. handleDebugHeartRateBlue() {
  2444. if (mapEngine) {
  2445. mapEngine.handleDebugHeartRateTone('blue')
  2446. }
  2447. },
  2448. handleDebugHeartRatePurple() {
  2449. if (mapEngine) {
  2450. mapEngine.handleDebugHeartRateTone('purple')
  2451. }
  2452. },
  2453. handleDebugHeartRateGreen() {
  2454. if (mapEngine) {
  2455. mapEngine.handleDebugHeartRateTone('green')
  2456. }
  2457. },
  2458. handleDebugHeartRateYellow() {
  2459. if (mapEngine) {
  2460. mapEngine.handleDebugHeartRateTone('yellow')
  2461. }
  2462. },
  2463. handleDebugHeartRateOrange() {
  2464. if (mapEngine) {
  2465. mapEngine.handleDebugHeartRateTone('orange')
  2466. }
  2467. },
  2468. handleDebugHeartRateRed() {
  2469. if (mapEngine) {
  2470. mapEngine.handleDebugHeartRateTone('red')
  2471. }
  2472. },
  2473. handleDebugSetSessionRemainingWarning() {
  2474. if (mapEngine) {
  2475. mapEngine.handleDebugSetSessionRemainingWarning()
  2476. }
  2477. },
  2478. handleDebugSetSessionRemainingOneMinute() {
  2479. if (mapEngine) {
  2480. mapEngine.handleDebugSetSessionRemainingOneMinute()
  2481. }
  2482. },
  2483. handleDebugTimeoutSession() {
  2484. if (mapEngine) {
  2485. mapEngine.handleDebugTimeoutSession()
  2486. }
  2487. },
  2488. handleClearDebugHeartRate() {
  2489. if (mapEngine) {
  2490. mapEngine.handleClearDebugHeartRate()
  2491. }
  2492. },
  2493. handleToggleOsmReference() {
  2494. if (mapEngine) {
  2495. mapEngine.handleToggleOsmReference()
  2496. }
  2497. },
  2498. handleStartGame() {
  2499. if (mapEngine) {
  2500. shouldAutoStartSessionOnEnter = false
  2501. systemSettingsLockLifetimeActive = true
  2502. this.applyRuntimeSystemSettings(true)
  2503. this.setData({
  2504. showStartEntryButton: false,
  2505. })
  2506. mapEngine.handleStartGame()
  2507. }
  2508. },
  2509. handleLoadClassicConfig() {
  2510. currentGameLaunchEnvelope = getDemoGameLaunchEnvelope('classic')
  2511. this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  2512. },
  2513. handleLoadScoreOConfig() {
  2514. currentGameLaunchEnvelope = getDemoGameLaunchEnvelope('score-o')
  2515. this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  2516. },
  2517. handleForceExitGame() {
  2518. if (!mapEngine || this.data.gameSessionStatus !== 'running') {
  2519. return
  2520. }
  2521. wx.showModal({
  2522. title: '确认退出',
  2523. content: '确认强制结束当前对局并返回开始前状态?',
  2524. confirmText: '确认退出',
  2525. cancelText: '取消',
  2526. success: (result) => {
  2527. if (result.confirm && mapEngine) {
  2528. clearResultExitRedirectTimer()
  2529. clearResultExitCountdownTimer()
  2530. this.syncBackendSessionFinish('cancelled')
  2531. clearSessionRecoverySnapshot()
  2532. clearSessionRecoveryPersistTimer()
  2533. systemSettingsLockLifetimeActive = false
  2534. mapEngine.handleForceExitGame()
  2535. wx.showToast({
  2536. title: '已退出当前对局',
  2537. icon: 'none',
  2538. duration: 1000,
  2539. })
  2540. setTimeout(() => {
  2541. navigateAwayFromMapAfterCancel()
  2542. }, 180)
  2543. }
  2544. },
  2545. })
  2546. },
  2547. handleSkipAction() {
  2548. if (!mapEngine || !this.data.skipButtonEnabled) {
  2549. return
  2550. }
  2551. if (!mapEngine.shouldConfirmSkipAction()) {
  2552. mapEngine.handleSkipAction()
  2553. return
  2554. }
  2555. wx.showModal({
  2556. title: '确认跳点',
  2557. content: '确认跳过当前检查点并切换到下一个目标点?',
  2558. confirmText: '确认跳过',
  2559. cancelText: '取消',
  2560. success: (result) => {
  2561. if (result.confirm && mapEngine) {
  2562. mapEngine.handleSkipAction()
  2563. }
  2564. },
  2565. })
  2566. },
  2567. handleClearMapTestArtifacts() {
  2568. if (mapEngine) {
  2569. mapEngine.handleClearMapTestArtifacts()
  2570. }
  2571. },
  2572. syncGameInfoPanelSnapshot() {
  2573. if (!mapEngine) {
  2574. return
  2575. }
  2576. const snapshot = mapEngine.getGameInfoSnapshot()
  2577. const localRows = snapshot.localRows.concat([
  2578. ...buildRuntimeSummaryRows(currentGameLaunchEnvelope),
  2579. ...buildLaunchConfigSummaryRows(currentGameLaunchEnvelope),
  2580. { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
  2581. { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
  2582. { label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
  2583. { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
  2584. { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
  2585. { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
  2586. { label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
  2587. { label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
  2588. ])
  2589. this.setData({
  2590. gameInfoTitle: snapshot.title,
  2591. gameInfoSubtitle: snapshot.subtitle,
  2592. gameInfoLocalRows: localRows,
  2593. gameInfoGlobalRows: snapshot.globalRows,
  2594. })
  2595. },
  2596. syncResultSceneSnapshot() {
  2597. if (!mapEngine) {
  2598. return
  2599. }
  2600. const snapshot = mapEngine.getResultSceneSnapshot()
  2601. this.setData({
  2602. resultSceneTitle: snapshot.title,
  2603. resultSceneSubtitle: snapshot.subtitle,
  2604. resultSceneHeroLabel: snapshot.heroLabel,
  2605. resultSceneHeroValue: snapshot.heroValue,
  2606. resultSceneRows: snapshot.rows
  2607. .concat(buildRuntimeSummaryRows(currentGameLaunchEnvelope))
  2608. .concat(buildLaunchConfigSummaryRows(currentGameLaunchEnvelope)),
  2609. })
  2610. },
  2611. scheduleGameInfoPanelSnapshotSync() {
  2612. if (!this.data.showGameInfoPanel) {
  2613. clearGameInfoPanelSyncTimer()
  2614. return
  2615. }
  2616. if (gameInfoPanelSyncTimer) {
  2617. return
  2618. }
  2619. gameInfoPanelSyncTimer = setTimeout(() => {
  2620. gameInfoPanelSyncTimer = 0
  2621. if (this.data.showGameInfoPanel) {
  2622. this.syncGameInfoPanelSnapshot()
  2623. }
  2624. }, 400) as unknown as number
  2625. },
  2626. handleOpenGameInfoPanel() {
  2627. clearGameInfoPanelSyncTimer()
  2628. this.syncGameInfoPanelSnapshot()
  2629. this.setData({
  2630. showDebugPanel: false,
  2631. showSystemSettingsPanel: false,
  2632. showGameInfoPanel: true,
  2633. ...buildSideButtonState({
  2634. sideButtonMode: this.data.sideButtonMode,
  2635. showGameInfoPanel: true,
  2636. showSystemSettingsPanel: false,
  2637. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2638. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2639. skipButtonEnabled: this.data.skipButtonEnabled,
  2640. gameSessionStatus: this.data.gameSessionStatus,
  2641. gpsLockEnabled: this.data.gpsLockEnabled,
  2642. gpsLockAvailable: this.data.gpsLockAvailable,
  2643. }),
  2644. })
  2645. },
  2646. handleCloseGameInfoPanel() {
  2647. clearGameInfoPanelSyncTimer()
  2648. this.setData({
  2649. showGameInfoPanel: false,
  2650. ...buildSideButtonState({
  2651. sideButtonMode: this.data.sideButtonMode,
  2652. showGameInfoPanel: false,
  2653. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  2654. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2655. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2656. skipButtonEnabled: this.data.skipButtonEnabled,
  2657. gameSessionStatus: this.data.gameSessionStatus,
  2658. gpsLockEnabled: this.data.gpsLockEnabled,
  2659. gpsLockAvailable: this.data.gpsLockAvailable,
  2660. }),
  2661. })
  2662. },
  2663. handleGameInfoPanelTap() {},
  2664. handleResultSceneTap() {},
  2665. handleCloseResultScene() {
  2666. this.redirectToResultPage()
  2667. },
  2668. handleRestartFromResult() {
  2669. this.redirectToResultPage()
  2670. },
  2671. handleOpenSystemSettingsPanel() {
  2672. clearGameInfoPanelSyncTimer()
  2673. this.setData({
  2674. showDebugPanel: false,
  2675. showGameInfoPanel: false,
  2676. showSystemSettingsPanel: true,
  2677. ...buildSideButtonState({
  2678. sideButtonMode: this.data.sideButtonMode,
  2679. showGameInfoPanel: false,
  2680. showSystemSettingsPanel: true,
  2681. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2682. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2683. skipButtonEnabled: this.data.skipButtonEnabled,
  2684. gameSessionStatus: this.data.gameSessionStatus,
  2685. gpsLockEnabled: this.data.gpsLockEnabled,
  2686. gpsLockAvailable: this.data.gpsLockAvailable,
  2687. }),
  2688. })
  2689. },
  2690. handleCloseSystemSettingsPanel() {
  2691. this.setData({
  2692. showSystemSettingsPanel: false,
  2693. ...buildSideButtonState({
  2694. sideButtonMode: this.data.sideButtonMode,
  2695. showGameInfoPanel: this.data.showGameInfoPanel,
  2696. showSystemSettingsPanel: false,
  2697. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2698. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2699. skipButtonEnabled: this.data.skipButtonEnabled,
  2700. gameSessionStatus: this.data.gameSessionStatus,
  2701. gpsLockEnabled: this.data.gpsLockEnabled,
  2702. gpsLockAvailable: this.data.gpsLockAvailable,
  2703. }),
  2704. })
  2705. },
  2706. handleSystemSettingsPanelTap() {},
  2707. handleSetAnimationLevelStandard() {
  2708. if (this.data.lockAnimationLevel || !mapEngine) {
  2709. return
  2710. }
  2711. this.persistAndApplySystemSettings({
  2712. animationLevel: 'standard',
  2713. })
  2714. },
  2715. handleSetAnimationLevelLite() {
  2716. if (this.data.lockAnimationLevel || !mapEngine) {
  2717. return
  2718. }
  2719. this.persistAndApplySystemSettings({
  2720. animationLevel: 'lite',
  2721. })
  2722. },
  2723. handleSetTrackModeNone() {
  2724. if (this.data.lockTrackMode || !mapEngine) {
  2725. return
  2726. }
  2727. this.persistAndApplySystemSettings({
  2728. trackDisplayMode: 'none',
  2729. })
  2730. },
  2731. handleSetTrackModeTail() {
  2732. if (this.data.lockTrackMode || !mapEngine) {
  2733. return
  2734. }
  2735. this.persistAndApplySystemSettings({
  2736. trackDisplayMode: 'tail',
  2737. })
  2738. },
  2739. handleSetTrackModeFull() {
  2740. if (this.data.lockTrackMode || !mapEngine) {
  2741. return
  2742. }
  2743. this.persistAndApplySystemSettings({
  2744. trackDisplayMode: 'full',
  2745. })
  2746. },
  2747. handleSetTrackTailLengthShort() {
  2748. if (this.data.lockTrackTailLength || !mapEngine) {
  2749. return
  2750. }
  2751. this.persistAndApplySystemSettings({
  2752. trackTailLength: 'short',
  2753. })
  2754. },
  2755. handleSetTrackTailLengthMedium() {
  2756. if (this.data.lockTrackTailLength || !mapEngine) {
  2757. return
  2758. }
  2759. this.persistAndApplySystemSettings({
  2760. trackTailLength: 'medium',
  2761. })
  2762. },
  2763. handleSetTrackTailLengthLong() {
  2764. if (this.data.lockTrackTailLength || !mapEngine) {
  2765. return
  2766. }
  2767. this.persistAndApplySystemSettings({
  2768. trackTailLength: 'long',
  2769. })
  2770. },
  2771. handleSetTrackColorPreset(event: WechatMiniprogram.TouchEvent) {
  2772. if (this.data.lockTrackColor || !mapEngine) {
  2773. return
  2774. }
  2775. const color = event.currentTarget.dataset.color as TrackColorPreset | undefined
  2776. if (!color) {
  2777. return
  2778. }
  2779. this.persistAndApplySystemSettings({
  2780. trackColorPreset: color,
  2781. })
  2782. },
  2783. handleSetTrackStyleClassic() {
  2784. if (this.data.lockTrackStyle || !mapEngine) {
  2785. return
  2786. }
  2787. this.persistAndApplySystemSettings({
  2788. trackStyleProfile: 'classic',
  2789. })
  2790. },
  2791. handleSetTrackStyleNeon() {
  2792. if (this.data.lockTrackStyle || !mapEngine) {
  2793. return
  2794. }
  2795. this.persistAndApplySystemSettings({
  2796. trackStyleProfile: 'neon',
  2797. })
  2798. },
  2799. handleSetGpsMarkerVisibleOn() {
  2800. if (this.data.lockGpsMarkerVisible || !mapEngine) {
  2801. return
  2802. }
  2803. this.persistAndApplySystemSettings({
  2804. gpsMarkerVisible: true,
  2805. })
  2806. },
  2807. handleSetGpsMarkerVisibleOff() {
  2808. if (this.data.lockGpsMarkerVisible || !mapEngine) {
  2809. return
  2810. }
  2811. this.persistAndApplySystemSettings({
  2812. gpsMarkerVisible: false,
  2813. })
  2814. },
  2815. handleSetGpsMarkerStyleDot() {
  2816. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2817. return
  2818. }
  2819. this.persistAndApplySystemSettings({
  2820. gpsMarkerStyle: 'dot',
  2821. })
  2822. },
  2823. handleSetGpsMarkerStyleBeacon() {
  2824. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2825. return
  2826. }
  2827. this.persistAndApplySystemSettings({
  2828. gpsMarkerStyle: 'beacon',
  2829. })
  2830. },
  2831. handleSetGpsMarkerStyleDisc() {
  2832. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2833. return
  2834. }
  2835. this.persistAndApplySystemSettings({
  2836. gpsMarkerStyle: 'disc',
  2837. })
  2838. },
  2839. handleSetGpsMarkerStyleBadge() {
  2840. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2841. return
  2842. }
  2843. this.persistAndApplySystemSettings({
  2844. gpsMarkerStyle: 'badge',
  2845. })
  2846. },
  2847. handleSetGpsMarkerSizeSmall() {
  2848. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2849. return
  2850. }
  2851. this.persistAndApplySystemSettings({
  2852. gpsMarkerSize: 'small',
  2853. })
  2854. },
  2855. handleSetGpsMarkerSizeMedium() {
  2856. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2857. return
  2858. }
  2859. this.persistAndApplySystemSettings({
  2860. gpsMarkerSize: 'medium',
  2861. })
  2862. },
  2863. handleSetGpsMarkerSizeLarge() {
  2864. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2865. return
  2866. }
  2867. this.persistAndApplySystemSettings({
  2868. gpsMarkerSize: 'large',
  2869. })
  2870. },
  2871. handleSetGpsMarkerColorPreset(event: WechatMiniprogram.TouchEvent) {
  2872. if (this.data.lockGpsMarkerColor || !mapEngine) {
  2873. return
  2874. }
  2875. const color = event.currentTarget.dataset.color as GpsMarkerColorPreset | undefined
  2876. if (!color) {
  2877. return
  2878. }
  2879. this.persistAndApplySystemSettings({
  2880. gpsMarkerColorPreset: color,
  2881. })
  2882. },
  2883. handleSetSideButtonPlacementLeft() {
  2884. if (this.data.lockSideButtonPlacement) {
  2885. return
  2886. }
  2887. this.persistAndApplySystemSettings({
  2888. sideButtonPlacement: 'left',
  2889. })
  2890. },
  2891. handleSetSideButtonPlacementRight() {
  2892. if (this.data.lockSideButtonPlacement) {
  2893. return
  2894. }
  2895. this.persistAndApplySystemSettings({
  2896. sideButtonPlacement: 'right',
  2897. })
  2898. },
  2899. handleSetAutoRotateEnabledOn() {
  2900. if (this.data.lockAutoRotate || !mapEngine) {
  2901. return
  2902. }
  2903. this.persistAndApplySystemSettings({
  2904. autoRotateEnabled: true,
  2905. })
  2906. },
  2907. handleSetAutoRotateEnabledOff() {
  2908. if (this.data.lockAutoRotate || !mapEngine) {
  2909. return
  2910. }
  2911. this.persistAndApplySystemSettings({
  2912. autoRotateEnabled: false,
  2913. })
  2914. },
  2915. handleSetCompassTuningSmooth() {
  2916. if (this.data.lockCompassTuning || !mapEngine) {
  2917. return
  2918. }
  2919. this.persistAndApplySystemSettings({
  2920. compassTuningProfile: 'smooth',
  2921. })
  2922. },
  2923. handleSetCompassTuningBalanced() {
  2924. if (this.data.lockCompassTuning || !mapEngine) {
  2925. return
  2926. }
  2927. this.persistAndApplySystemSettings({
  2928. compassTuningProfile: 'balanced',
  2929. })
  2930. },
  2931. handleSetCompassTuningResponsive() {
  2932. if (this.data.lockCompassTuning || !mapEngine) {
  2933. return
  2934. }
  2935. this.persistAndApplySystemSettings({
  2936. compassTuningProfile: 'responsive',
  2937. })
  2938. },
  2939. handleSetNorthReferenceMagnetic() {
  2940. if (this.data.lockNorthReference || !mapEngine) {
  2941. return
  2942. }
  2943. this.persistAndApplySystemSettings({
  2944. northReferenceMode: 'magnetic',
  2945. })
  2946. },
  2947. handleSetNorthReferenceTrue() {
  2948. if (this.data.lockNorthReference || !mapEngine) {
  2949. return
  2950. }
  2951. this.persistAndApplySystemSettings({
  2952. northReferenceMode: 'true',
  2953. })
  2954. },
  2955. handleOverlayTouch() {},
  2956. handlePunchAction() {
  2957. if (!this.data.punchButtonEnabled) {
  2958. return
  2959. }
  2960. if (mapEngine) {
  2961. mapEngine.handlePunchAction()
  2962. }
  2963. },
  2964. handleOpenPendingContentCard() {
  2965. if (mapEngine) {
  2966. mapEngine.openPendingContentCard()
  2967. }
  2968. },
  2969. handleOpenContentCardAction(event: WechatMiniprogram.BaseEvent) {
  2970. if (!mapEngine) {
  2971. return
  2972. }
  2973. wx.showToast({
  2974. title: '点击CTA',
  2975. icon: 'none',
  2976. duration: 900,
  2977. })
  2978. const actionType = event.currentTarget.dataset.type
  2979. const action = typeof actionType === 'string' ? mapEngine.openCurrentContentCardAction(actionType) : null
  2980. if (action === 'detail') {
  2981. wx.showToast({
  2982. title: '打开详情',
  2983. icon: 'none',
  2984. duration: 900,
  2985. })
  2986. return
  2987. }
  2988. if (action === 'quiz') {
  2989. return
  2990. }
  2991. if (action === 'photo') {
  2992. wx.chooseMedia({
  2993. count: 1,
  2994. mediaType: ['image'],
  2995. sourceType: ['camera'],
  2996. success: () => {
  2997. if (mapEngine) {
  2998. mapEngine.handleContentCardPhotoCaptured()
  2999. }
  3000. },
  3001. })
  3002. return
  3003. }
  3004. if (action === 'audio') {
  3005. if (!contentAudioRecorder) {
  3006. contentAudioRecorder = wx.getRecorderManager()
  3007. contentAudioRecorder.onStop(() => {
  3008. contentAudioRecording = false
  3009. if (mapEngine) {
  3010. mapEngine.handleContentCardAudioRecorded()
  3011. }
  3012. })
  3013. }
  3014. const recorder = contentAudioRecorder
  3015. if (!contentAudioRecording) {
  3016. contentAudioRecording = true
  3017. recorder.start({
  3018. duration: 8000,
  3019. format: 'mp3',
  3020. } as any)
  3021. wx.showToast({
  3022. title: '开始录音',
  3023. icon: 'none',
  3024. duration: 800,
  3025. })
  3026. } else {
  3027. recorder.stop()
  3028. }
  3029. }
  3030. },
  3031. handleContentQuizAnswer(event: WechatMiniprogram.BaseEvent) {
  3032. if (!mapEngine) {
  3033. return
  3034. }
  3035. const optionKey = event.currentTarget.dataset.key
  3036. if (typeof optionKey === 'string') {
  3037. mapEngine.handleContentCardQuizAnswer(optionKey)
  3038. }
  3039. },
  3040. handleDismissTransientContentCard() {
  3041. if (mapEngine) {
  3042. mapEngine.closeContentCard()
  3043. }
  3044. },
  3045. handleContentCardTap() {
  3046. if (!mapEngine) {
  3047. return
  3048. }
  3049. if (!this.data.contentCardActions.length) {
  3050. mapEngine.closeContentCard()
  3051. }
  3052. },
  3053. openH5Experience(request: H5ExperienceRequest) {
  3054. wx.navigateTo({
  3055. url: '/pages/experience-webview/experience-webview',
  3056. success: (result) => {
  3057. const eventChannel = result.eventChannel
  3058. eventChannel.on('fallback', (payload: H5ExperienceFallbackPayload) => {
  3059. if (mapEngine) {
  3060. mapEngine.handleH5ExperienceFallback(payload)
  3061. }
  3062. })
  3063. eventChannel.on('close', () => {
  3064. if (mapEngine) {
  3065. mapEngine.handleH5ExperienceClosed()
  3066. }
  3067. })
  3068. eventChannel.on('submitResult', () => {
  3069. if (mapEngine) {
  3070. mapEngine.handleH5ExperienceClosed()
  3071. }
  3072. })
  3073. eventChannel.emit('init', request)
  3074. },
  3075. fail: () => {
  3076. if (mapEngine) {
  3077. mapEngine.handleH5ExperienceFallback(request.fallback)
  3078. }
  3079. },
  3080. })
  3081. },
  3082. handleCloseContentCard() {
  3083. if (mapEngine) {
  3084. mapEngine.closeContentCard()
  3085. }
  3086. },
  3087. handleClosePunchHint() {
  3088. clearPunchHintDismissTimer()
  3089. this.setData({
  3090. showPunchHintBanner: false,
  3091. })
  3092. },
  3093. handlePunchHintTap() {},
  3094. handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
  3095. this.setData({
  3096. hudPanelIndex: event.detail.current || 0,
  3097. })
  3098. },
  3099. handleCycleSideButtons() {
  3100. const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
  3101. this.setData({
  3102. ...buildSideButtonVisibility(nextMode),
  3103. ...buildSideButtonState({
  3104. sideButtonMode: nextMode,
  3105. showGameInfoPanel: this.data.showGameInfoPanel,
  3106. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  3107. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3108. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3109. skipButtonEnabled: this.data.skipButtonEnabled,
  3110. gameSessionStatus: this.data.gameSessionStatus,
  3111. gpsLockEnabled: this.data.gpsLockEnabled,
  3112. gpsLockAvailable: this.data.gpsLockAvailable,
  3113. }),
  3114. })
  3115. },
  3116. handleToggleGpsLock() {
  3117. if (mapEngine) {
  3118. mapEngine.handleToggleGpsLock()
  3119. }
  3120. },
  3121. handleToggleMapRotateMode() {
  3122. if (!mapEngine || this.data.lockAutoRotate) {
  3123. return
  3124. }
  3125. if (this.data.orientationMode === 'heading-up') {
  3126. this.persistAndApplySystemSettings({
  3127. autoRotateEnabled: false,
  3128. })
  3129. return
  3130. }
  3131. this.persistAndApplySystemSettings({
  3132. autoRotateEnabled: true,
  3133. })
  3134. },
  3135. handleToggleDebugPanel() {
  3136. const nextShowDebugPanel = !this.data.showDebugPanel
  3137. if (!nextShowDebugPanel) {
  3138. clearGameInfoPanelSyncTimer()
  3139. }
  3140. if (mapEngine) {
  3141. mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
  3142. }
  3143. this.setData({
  3144. showDebugPanel: nextShowDebugPanel,
  3145. showGameInfoPanel: false,
  3146. showSystemSettingsPanel: false,
  3147. ...buildSideButtonState({
  3148. sideButtonMode: this.data.sideButtonMode,
  3149. showGameInfoPanel: false,
  3150. showSystemSettingsPanel: false,
  3151. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3152. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3153. skipButtonEnabled: this.data.skipButtonEnabled,
  3154. gameSessionStatus: this.data.gameSessionStatus,
  3155. gpsLockEnabled: this.data.gpsLockEnabled,
  3156. gpsLockAvailable: this.data.gpsLockAvailable,
  3157. }),
  3158. })
  3159. },
  3160. handleCloseDebugPanel() {
  3161. if (mapEngine) {
  3162. mapEngine.setDiagnosticUiEnabled(false)
  3163. }
  3164. this.setData({
  3165. showDebugPanel: false,
  3166. ...buildSideButtonState({
  3167. sideButtonMode: this.data.sideButtonMode,
  3168. showGameInfoPanel: this.data.showGameInfoPanel,
  3169. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  3170. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3171. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3172. skipButtonEnabled: this.data.skipButtonEnabled,
  3173. gameSessionStatus: this.data.gameSessionStatus,
  3174. gpsLockEnabled: this.data.gpsLockEnabled,
  3175. gpsLockAvailable: this.data.gpsLockAvailable,
  3176. }),
  3177. })
  3178. },
  3179. applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
  3180. this.data.showCenterScaleRuler = nextEnabled
  3181. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  3182. clearCenterScaleRulerSyncTimer()
  3183. clearCenterScaleRulerUpdateTimer()
  3184. const syncRulerFromEngine = () => {
  3185. if (!mapEngine) {
  3186. return
  3187. }
  3188. const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
  3189. updateCenterScaleRulerInputCache(engineSnapshot)
  3190. const mergedData = {
  3191. ...centerScaleRulerInputCache,
  3192. ...this.data,
  3193. showCenterScaleRuler: nextEnabled,
  3194. centerScaleRulerAnchorMode: nextAnchorMode,
  3195. } as MapPageData
  3196. this.setData({
  3197. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
  3198. showCenterScaleRuler: nextEnabled,
  3199. centerScaleRulerAnchorMode: nextAnchorMode,
  3200. ...buildCenterScaleRulerPatch(mergedData),
  3201. ...buildSideButtonState(mergedData),
  3202. })
  3203. }
  3204. if (!nextEnabled) {
  3205. syncRulerFromEngine()
  3206. return
  3207. }
  3208. this.setData({
  3209. showCenterScaleRuler: true,
  3210. centerScaleRulerAnchorMode: nextAnchorMode,
  3211. ...buildSideButtonState({
  3212. ...this.data,
  3213. showCenterScaleRuler: true,
  3214. centerScaleRulerAnchorMode: nextAnchorMode,
  3215. } as MapPageData),
  3216. })
  3217. this.measureStageAndCanvas(() => {
  3218. syncRulerFromEngine()
  3219. })
  3220. centerScaleRulerSyncTimer = setTimeout(() => {
  3221. centerScaleRulerSyncTimer = 0
  3222. if (!this.data.showCenterScaleRuler) {
  3223. return
  3224. }
  3225. syncRulerFromEngine()
  3226. }, 96) as unknown as number
  3227. },
  3228. handleSetCenterScaleRulerVisibleOn() {
  3229. if (this.data.lockScaleRulerVisible) {
  3230. return
  3231. }
  3232. this.persistAndApplySystemSettings({
  3233. showCenterScaleRuler: true,
  3234. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3235. }, {
  3236. applyCenterScaleRuler: true,
  3237. })
  3238. },
  3239. handleSetCenterScaleRulerVisibleOff() {
  3240. if (this.data.lockScaleRulerVisible) {
  3241. return
  3242. }
  3243. this.persistAndApplySystemSettings({
  3244. showCenterScaleRuler: false,
  3245. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3246. }, {
  3247. applyCenterScaleRuler: true,
  3248. })
  3249. },
  3250. handleSetCenterScaleRulerAnchorScreenCenter() {
  3251. if (this.data.lockScaleRulerAnchor) {
  3252. return
  3253. }
  3254. this.persistAndApplySystemSettings({
  3255. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3256. centerScaleRulerAnchorMode: 'screen-center',
  3257. }, {
  3258. applyCenterScaleRuler: true,
  3259. })
  3260. },
  3261. handleSetCenterScaleRulerAnchorCompassCenter() {
  3262. if (this.data.lockScaleRulerAnchor) {
  3263. return
  3264. }
  3265. this.persistAndApplySystemSettings({
  3266. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3267. centerScaleRulerAnchorMode: 'compass-center',
  3268. }, {
  3269. applyCenterScaleRuler: true,
  3270. })
  3271. },
  3272. handleToggleCenterScaleRulerAnchor() {
  3273. if (!this.data.showCenterScaleRuler || this.data.lockScaleRulerAnchor) {
  3274. return
  3275. }
  3276. const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
  3277. ? 'compass-center'
  3278. : 'screen-center'
  3279. this.persistAndApplySystemSettings({
  3280. centerScaleRulerAnchorMode: nextAnchorMode,
  3281. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3282. }, {
  3283. applyCenterScaleRuler: true,
  3284. })
  3285. },
  3286. handleDebugPanelTap() {},
  3287. })