map.ts 111 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564
  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. Page({
  803. data: {
  804. showDebugPanel: false,
  805. showGameInfoPanel: false,
  806. showResultScene: false,
  807. showSystemSettingsPanel: false,
  808. showHeartRateDevicePicker: false,
  809. showCenterScaleRuler: false,
  810. statusBarHeight: 0,
  811. topInsetHeight: 12,
  812. hudPanelIndex: 0,
  813. configSourceText: '顺序赛配置',
  814. centerScaleRulerAnchorMode: DEFAULT_STORED_USER_SETTINGS.centerScaleRulerAnchorMode,
  815. punchHintFxClass: '',
  816. autoRotateEnabled: DEFAULT_STORED_USER_SETTINGS.autoRotateEnabled,
  817. ...DEFAULT_SETTING_LOCKS,
  818. gameInfoTitle: '当前游戏',
  819. gameInfoSubtitle: '未开始',
  820. gameInfoLocalRows: [],
  821. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  822. resultSceneTitle: '本局结果',
  823. resultSceneSubtitle: '未开始',
  824. resultSceneHeroLabel: '本局用时',
  825. resultSceneHeroValue: '--',
  826. resultSceneRows: buildEmptyResultSceneSnapshot().rows,
  827. resultSceneCountdownText: '',
  828. panelTimerText: '00:00:00',
  829. panelTimerMode: 'elapsed',
  830. panelMileageText: '0m',
  831. panelActionTagText: '目标',
  832. panelDistanceTagText: '点距',
  833. panelTargetSummaryText: '等待选择目标',
  834. panelDistanceValueText: '--',
  835. panelDistanceUnitText: '',
  836. panelProgressText: '0/0',
  837. showPunchHintBanner: true,
  838. sideButtonPlacement: 'left',
  839. gameSessionStatus: 'idle',
  840. gameModeText: '顺序赛',
  841. gpsLockEnabled: false,
  842. gpsLockAvailable: false,
  843. locationSourceMode: 'real',
  844. locationSourceText: '真实定位',
  845. mockBridgeConnected: false,
  846. mockBridgeStatusText: '未连接',
  847. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  848. mockChannelIdText: 'default',
  849. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  850. mockChannelIdDraft: 'default',
  851. mockCoordText: '--',
  852. mockSpeedText: '--',
  853. heartRateSourceMode: 'real',
  854. heartRateSourceText: '真实心率',
  855. mockHeartRateBridgeConnected: false,
  856. mockHeartRateBridgeStatusText: '未连接',
  857. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
  858. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
  859. mockHeartRateText: '--',
  860. mockDebugLogBridgeConnected: false,
  861. mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
  862. mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
  863. mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
  864. heartRateScanText: '未扫描',
  865. heartRateDiscoveredDevices: [],
  866. panelSpeedValueText: '0',
  867. panelTelemetryTone: 'blue',
  868. trackDisplayMode: DEFAULT_STORED_USER_SETTINGS.trackDisplayMode,
  869. trackTailLength: DEFAULT_STORED_USER_SETTINGS.trackTailLength,
  870. trackColorPreset: DEFAULT_STORED_USER_SETTINGS.trackColorPreset,
  871. trackStyleProfile: DEFAULT_STORED_USER_SETTINGS.trackStyleProfile,
  872. gpsMarkerVisible: DEFAULT_STORED_USER_SETTINGS.gpsMarkerVisible,
  873. gpsMarkerStyle: DEFAULT_STORED_USER_SETTINGS.gpsMarkerStyle,
  874. gpsMarkerSize: DEFAULT_STORED_USER_SETTINGS.gpsMarkerSize,
  875. gpsMarkerColorPreset: DEFAULT_STORED_USER_SETTINGS.gpsMarkerColorPreset,
  876. gpsLogoStatusText: '未配置',
  877. gpsLogoSourceText: '--',
  878. panelHeartRateZoneNameText: '--',
  879. panelHeartRateZoneRangeText: '',
  880. heartRateConnected: false,
  881. heartRateStatusText: '心率带未连接',
  882. heartRateDeviceText: '--',
  883. panelHeartRateValueText: '--',
  884. panelHeartRateUnitText: '',
  885. panelCaloriesValueText: '0',
  886. panelCaloriesUnitText: 'kcal',
  887. panelAverageSpeedValueText: '0',
  888. panelAverageSpeedUnitText: 'km/h',
  889. panelAccuracyValueText: '--',
  890. panelAccuracyUnitText: '',
  891. deviceHeadingText: '--',
  892. devicePoseText: '竖持',
  893. headingConfidenceText: '低',
  894. accelerometerText: '--',
  895. gyroscopeText: '--',
  896. deviceMotionText: '--',
  897. compassSourceText: '无数据',
  898. compassTuningProfile: DEFAULT_STORED_USER_SETTINGS.compassTuningProfile,
  899. compassTuningProfileText: '平衡',
  900. punchButtonText: '打点',
  901. punchButtonEnabled: false,
  902. skipButtonEnabled: false,
  903. punchHintText: '等待进入检查点范围',
  904. punchFeedbackVisible: false,
  905. punchFeedbackText: '',
  906. punchFeedbackTone: 'neutral',
  907. contentCardVisible: false,
  908. contentCardTemplate: 'story',
  909. contentCardTitle: '',
  910. contentCardBody: '',
  911. contentCardActions: [],
  912. contentQuizVisible: false,
  913. contentQuizQuestionText: '',
  914. contentQuizCountdownText: '',
  915. contentQuizOptions: [],
  916. contentQuizFeedbackVisible: false,
  917. contentQuizFeedbackText: '',
  918. contentQuizFeedbackTone: 'neutral',
  919. punchButtonFxClass: '',
  920. panelProgressFxClass: '',
  921. panelDistanceFxClass: '',
  922. punchFeedbackFxClass: '',
  923. contentCardFxClass: '',
  924. mapPulseVisible: false,
  925. mapPulseLeftPx: 0,
  926. mapPulseTopPx: 0,
  927. mapPulseFxClass: '',
  928. stageFxVisible: false,
  929. stageFxClass: '',
  930. centerScaleRulerVisible: false,
  931. centerScaleRulerCenterXPx: 0,
  932. centerScaleRulerZeroYPx: 0,
  933. centerScaleRulerHeightPx: 0,
  934. centerScaleRulerAxisBottomPx: 0,
  935. centerScaleRulerZeroVisible: false,
  936. centerScaleRulerZeroLabel: '0 m',
  937. centerScaleRulerMinorTicks: [],
  938. centerScaleRulerMajorMarks: [],
  939. compassTicks: buildCompassTicks(),
  940. compassLabels: buildCompassLabels(),
  941. showStartEntryButton: true,
  942. ...buildSideButtonVisibility('shown'),
  943. ...buildSideButtonState({
  944. sideButtonMode: 'shown',
  945. showGameInfoPanel: false,
  946. showSystemSettingsPanel: false,
  947. showCenterScaleRuler: false,
  948. centerScaleRulerAnchorMode: 'screen-center',
  949. skipButtonEnabled: false,
  950. gameSessionStatus: 'idle',
  951. gpsLockEnabled: false,
  952. gpsLockAvailable: false,
  953. }),
  954. } as unknown as MapPageData,
  955. onLoad(options: MapPageLaunchOptions) {
  956. clearSessionRecoveryPersistTimer()
  957. clearResultExitRedirectTimer()
  958. clearResultExitCountdownTimer()
  959. syncedBackendSessionStartId = ''
  960. syncedBackendSessionFinishId = ''
  961. redirectedToResultPage = false
  962. shouldAutoRestoreRecoverySnapshot = options && options.recoverSession === '1'
  963. shouldAutoStartSessionOnEnter = !!(options && options.autoStartOnEnter === '1')
  964. const recoverySnapshot = loadSessionRecoverySnapshot()
  965. if (shouldAutoRestoreRecoverySnapshot && recoverySnapshot) {
  966. // Recovery should trust the persisted session envelope first so it can
  967. // survive launchId stash misses and still reconstruct the original round.
  968. currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
  969. } else {
  970. currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
  971. if (!hasExplicitLaunchOptions(options) && recoverySnapshot) {
  972. currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
  973. }
  974. }
  975. currentSystemSettingsConfig = undefined
  976. currentRemoteMapConfig = undefined
  977. systemSettingsLockLifetimeActive = false
  978. const storedMockChannelId = loadStoredMockChannelId()
  979. const shouldAutoConnectMockSources = loadMockAutoConnectEnabled()
  980. const systemInfo = wx.getSystemInfoSync()
  981. const statusBarHeight = systemInfo.statusBarHeight || 0
  982. const menuButtonRect = wx.getMenuButtonBoundingClientRect()
  983. const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
  984. this.setData({
  985. showStartEntryButton: !shouldAutoStartSessionOnEnter,
  986. })
  987. if (mapEngine) {
  988. mapEngine.destroy()
  989. mapEngine = null
  990. }
  991. mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
  992. onData: (patch) => {
  993. const nextPatch = patch as Partial<MapPageData>
  994. const includeDebugFields = this.data.showDebugPanel
  995. const includeRulerFields = this.data.showCenterScaleRuler
  996. let shouldSyncRuntimeSystemSettings = false
  997. let nextLockLifetimeActive = isSystemSettingsLockLifetimeActive()
  998. let heartRateSwitchToastText = ''
  999. const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
  1000. ...nextPatch,
  1001. }, includeDebugFields, includeRulerFields)
  1002. if (
  1003. typeof nextPatch.mockBridgeUrlText === 'string'
  1004. && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
  1005. ) {
  1006. nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
  1007. }
  1008. if (
  1009. typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
  1010. && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
  1011. ) {
  1012. nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
  1013. }
  1014. if (
  1015. typeof nextPatch.mockDebugLogBridgeUrlText === 'string'
  1016. && this.data.mockDebugLogBridgeUrlDraft === this.data.mockDebugLogBridgeUrlText
  1017. ) {
  1018. nextData.mockDebugLogBridgeUrlDraft = nextPatch.mockDebugLogBridgeUrlText
  1019. }
  1020. if (
  1021. typeof nextPatch.mockChannelIdText === 'string'
  1022. && this.data.mockChannelIdDraft === this.data.mockChannelIdText
  1023. ) {
  1024. nextData.mockChannelIdDraft = nextPatch.mockChannelIdText
  1025. }
  1026. updateCenterScaleRulerInputCache(nextPatch)
  1027. const mergedData = {
  1028. ...centerScaleRulerInputCache,
  1029. ...this.data,
  1030. ...nextData,
  1031. } as MapPageData
  1032. const derivedPatch: Partial<MapPageData> = {}
  1033. if (typeof nextPatch.orientationMode === 'string') {
  1034. nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up'
  1035. }
  1036. if (
  1037. this.data.showCenterScaleRuler
  1038. && hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
  1039. ) {
  1040. clearCenterScaleRulerUpdateTimer()
  1041. Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
  1042. }
  1043. if (hasAnyPatchKey(nextPatch as Record<string, unknown>, SIDE_BUTTON_DEP_KEYS)) {
  1044. Object.assign(derivedPatch, buildSideButtonState(mergedData))
  1045. }
  1046. if (typeof nextPatch.punchHintText === 'string') {
  1047. const nextHintText = nextPatch.punchHintText.trim()
  1048. if (nextHintText !== this.data.punchHintText) {
  1049. clearPunchHintDismissTimer()
  1050. clearPunchHintFxTimer()
  1051. nextData.showPunchHintBanner = nextHintText.length > 0
  1052. if (nextHintText.length > 0) {
  1053. nextData.punchHintFxClass = 'game-punch-hint--fx-enter'
  1054. punchHintFxTimer = setTimeout(() => {
  1055. punchHintFxTimer = 0
  1056. this.setData({
  1057. punchHintFxClass: '',
  1058. })
  1059. }, PUNCH_HINT_FX_DURATION_MS) as unknown as number
  1060. const now = Date.now()
  1061. if (mapEngine && now - lastPunchHintHapticAt >= PUNCH_HINT_HAPTIC_GAP_MS) {
  1062. mapEngine.playPunchHintHaptic()
  1063. lastPunchHintHapticAt = now
  1064. }
  1065. punchHintDismissTimer = setTimeout(() => {
  1066. punchHintDismissTimer = 0
  1067. this.setData({
  1068. showPunchHintBanner: false,
  1069. })
  1070. }, PUNCH_HINT_AUTO_HIDE_MS) as unknown as number
  1071. }
  1072. } else if (!nextHintText) {
  1073. clearPunchHintDismissTimer()
  1074. clearPunchHintFxTimer()
  1075. nextData.showPunchHintBanner = false
  1076. nextData.punchHintFxClass = ''
  1077. }
  1078. }
  1079. const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
  1080. ? nextPatch.animationLevel
  1081. : this.data.animationLevel
  1082. let shouldSyncBackendSessionStart = false
  1083. let backendSessionFinishStatus: 'finished' | 'failed' | null = null
  1084. let shouldOpenResultExitPrompt = false
  1085. let resultPageSnapshot: MapEngineResultSnapshot | null = null
  1086. if (nextAnimationLevel === 'lite') {
  1087. clearHudFxTimer('timer')
  1088. clearHudFxTimer('mileage')
  1089. clearHudFxTimer('speed')
  1090. clearHudFxTimer('heartRate')
  1091. nextData.panelTimerFxClass = ''
  1092. nextData.panelMileageFxClass = ''
  1093. nextData.panelSpeedFxClass = ''
  1094. nextData.panelHeartRateFxClass = ''
  1095. } else {
  1096. if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
  1097. clearHudFxTimer('timer')
  1098. nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
  1099. panelTimerFxTimer = setTimeout(() => {
  1100. panelTimerFxTimer = 0
  1101. this.setData({ panelTimerFxClass: '' })
  1102. }, 320) as unknown as number
  1103. }
  1104. if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
  1105. clearHudFxTimer('mileage')
  1106. nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
  1107. panelMileageFxTimer = setTimeout(() => {
  1108. panelMileageFxTimer = 0
  1109. this.setData({ panelMileageFxClass: '' })
  1110. }, 360) as unknown as number
  1111. }
  1112. if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
  1113. clearHudFxTimer('speed')
  1114. nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
  1115. panelSpeedFxTimer = setTimeout(() => {
  1116. panelSpeedFxTimer = 0
  1117. this.setData({ panelSpeedFxClass: '' })
  1118. }, 360) as unknown as number
  1119. }
  1120. if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
  1121. clearHudFxTimer('heartRate')
  1122. nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
  1123. panelHeartRateFxTimer = setTimeout(() => {
  1124. panelHeartRateFxTimer = 0
  1125. this.setData({ panelHeartRateFxClass: '' })
  1126. }, 400) as unknown as number
  1127. }
  1128. }
  1129. if (typeof nextPatch.gameSessionStatus === 'string') {
  1130. if (
  1131. nextPatch.gameSessionStatus !== this.data.gameSessionStatus
  1132. && (nextPatch.gameSessionStatus === 'finished' || nextPatch.gameSessionStatus === 'failed')
  1133. ) {
  1134. systemSettingsLockLifetimeActive = false
  1135. nextLockLifetimeActive = false
  1136. shouldSyncRuntimeSystemSettings = true
  1137. clearSessionRecoverySnapshot()
  1138. clearSessionRecoveryPersistTimer()
  1139. clearResultExitRedirectTimer()
  1140. clearResultExitCountdownTimer()
  1141. resultPageSnapshot = mapEngine ? mapEngine.getResultSceneSnapshot() : null
  1142. nextData.showResultScene = true
  1143. nextData.showDebugPanel = false
  1144. nextData.showGameInfoPanel = false
  1145. nextData.showSystemSettingsPanel = false
  1146. clearGameInfoPanelSyncTimer()
  1147. backendSessionFinishStatus = nextPatch.gameSessionStatus === 'finished' ? 'finished' : 'failed'
  1148. shouldOpenResultExitPrompt = true
  1149. if (resultPageSnapshot) {
  1150. nextData.resultSceneTitle = resultPageSnapshot.title
  1151. nextData.resultSceneSubtitle = resultPageSnapshot.subtitle
  1152. nextData.resultSceneHeroLabel = resultPageSnapshot.heroLabel
  1153. nextData.resultSceneHeroValue = resultPageSnapshot.heroValue
  1154. nextData.resultSceneRows = resultPageSnapshot.rows
  1155. }
  1156. nextData.resultSceneCountdownText = '3 秒后自动进入成绩页'
  1157. } else if (
  1158. nextPatch.gameSessionStatus !== this.data.gameSessionStatus
  1159. && nextPatch.gameSessionStatus === 'idle'
  1160. && !isSystemSettingsLockLifetimeActive()
  1161. ) {
  1162. nextLockLifetimeActive = false
  1163. shouldSyncRuntimeSystemSettings = true
  1164. clearSessionRecoverySnapshot()
  1165. clearSessionRecoveryPersistTimer()
  1166. clearResultExitRedirectTimer()
  1167. clearResultExitCountdownTimer()
  1168. } else if (
  1169. nextPatch.gameSessionStatus !== this.data.gameSessionStatus
  1170. && nextPatch.gameSessionStatus === 'running'
  1171. ) {
  1172. shouldSyncBackendSessionStart = true
  1173. } else if (nextPatch.gameSessionStatus === 'running' || nextPatch.gameSessionStatus === 'idle') {
  1174. nextData.showResultScene = false
  1175. }
  1176. }
  1177. if (
  1178. pendingHeartRateSwitchDeviceName
  1179. && nextPatch.heartRateConnected === true
  1180. && typeof nextPatch.heartRateDeviceText === 'string'
  1181. ) {
  1182. const connectedDeviceName = nextPatch.heartRateDeviceText.trim()
  1183. if (connectedDeviceName && connectedDeviceName === pendingHeartRateSwitchDeviceName) {
  1184. heartRateSwitchToastText = `已切换到 ${connectedDeviceName}`
  1185. nextData.statusText = `已切换心率带:${connectedDeviceName}`
  1186. pendingHeartRateSwitchDeviceName = null
  1187. }
  1188. }
  1189. if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
  1190. this.setData({
  1191. ...nextData,
  1192. ...derivedPatch,
  1193. }, () => {
  1194. if (typeof nextPatch.gameSessionStatus === 'string') {
  1195. this.syncSessionRecoveryLifecycle(nextPatch.gameSessionStatus)
  1196. }
  1197. if (shouldSyncBackendSessionStart) {
  1198. this.syncBackendSessionStart()
  1199. }
  1200. if (backendSessionFinishStatus) {
  1201. this.syncBackendSessionFinish(backendSessionFinishStatus)
  1202. }
  1203. if (shouldOpenResultExitPrompt && resultPageSnapshot) {
  1204. this.stashPendingResultSnapshot(resultPageSnapshot)
  1205. this.presentResultExitPrompt()
  1206. }
  1207. if (heartRateSwitchToastText) {
  1208. wx.showToast({
  1209. title: `${heartRateSwitchToastText},并设为首选设备`,
  1210. icon: 'none',
  1211. duration: 1800,
  1212. })
  1213. }
  1214. if (shouldSyncRuntimeSystemSettings) {
  1215. this.applyRuntimeSystemSettings(nextLockLifetimeActive)
  1216. }
  1217. if (this.data.showGameInfoPanel) {
  1218. this.scheduleGameInfoPanelSnapshotSync()
  1219. }
  1220. })
  1221. } else {
  1222. if (typeof nextPatch.gameSessionStatus === 'string') {
  1223. this.syncSessionRecoveryLifecycle(nextPatch.gameSessionStatus)
  1224. }
  1225. if (shouldSyncBackendSessionStart) {
  1226. this.syncBackendSessionStart()
  1227. }
  1228. if (backendSessionFinishStatus) {
  1229. this.syncBackendSessionFinish(backendSessionFinishStatus)
  1230. }
  1231. if (shouldOpenResultExitPrompt && resultPageSnapshot) {
  1232. this.stashPendingResultSnapshot(resultPageSnapshot)
  1233. this.presentResultExitPrompt()
  1234. }
  1235. if (shouldSyncRuntimeSystemSettings) {
  1236. this.applyRuntimeSystemSettings(nextLockLifetimeActive)
  1237. }
  1238. if (this.data.showGameInfoPanel) {
  1239. this.scheduleGameInfoPanelSnapshotSync()
  1240. }
  1241. }
  1242. },
  1243. onOpenH5Experience: (request) => {
  1244. this.openH5Experience(request)
  1245. },
  1246. })
  1247. mapEngine.applyTelemetryPlayerProfile(getGlobalTelemetryProfile())
  1248. const systemSettingsState = resolveSystemSettingsState(undefined, undefined, false)
  1249. const initialSystemSettings = systemSettingsState.values
  1250. mapEngine.applyCompiledSettingsProfile({
  1251. values: initialSystemSettings,
  1252. locks: systemSettingsState.locks,
  1253. lockLifetimeActive: false,
  1254. })
  1255. mapEngine.setDiagnosticUiEnabled(false)
  1256. centerScaleRulerInputCache = {
  1257. stageWidth: 0,
  1258. stageHeight: 0,
  1259. zoom: 0,
  1260. centerTileY: 0,
  1261. tileSizePx: 0,
  1262. previewScale: 1,
  1263. }
  1264. const initialEngineData = mapEngine.getInitialData()
  1265. this.setData({
  1266. ...initialEngineData,
  1267. ...buildResolvedSystemSettingsPatch(systemSettingsState),
  1268. showDebugPanel: false,
  1269. showGameInfoPanel: false,
  1270. showResultScene: false,
  1271. showSystemSettingsPanel: false,
  1272. statusBarHeight,
  1273. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  1274. hudPanelIndex: 0,
  1275. configSourceText: currentGameLaunchEnvelope.config.configLabel,
  1276. gameInfoTitle: '当前游戏',
  1277. gameInfoSubtitle: '未开始',
  1278. gameInfoLocalRows: [],
  1279. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  1280. resultSceneTitle: '本局结果',
  1281. resultSceneSubtitle: '未开始',
  1282. resultSceneHeroLabel: '本局用时',
  1283. resultSceneHeroValue: '--',
  1284. resultSceneRows: buildEmptyResultSceneSnapshot().rows,
  1285. resultSceneCountdownText: '',
  1286. panelTimerText: '00:00:00',
  1287. panelTimerMode: 'elapsed',
  1288. panelTimerFxClass: '',
  1289. panelMileageText: '0m',
  1290. panelMileageFxClass: '',
  1291. panelActionTagText: '目标',
  1292. panelDistanceTagText: '点距',
  1293. panelTargetSummaryText: '等待选择目标',
  1294. panelDistanceValueText: '--',
  1295. panelDistanceUnitText: '',
  1296. panelProgressText: '0/0',
  1297. showPunchHintBanner: true,
  1298. gameSessionStatus: 'idle',
  1299. gameModeText: '顺序赛',
  1300. gpsLockEnabled: false,
  1301. gpsLockAvailable: false,
  1302. locationSourceMode: 'real',
  1303. locationSourceText: '真实定位',
  1304. mockBridgeConnected: false,
  1305. mockBridgeStatusText: '未连接',
  1306. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  1307. mockChannelIdText: storedMockChannelId,
  1308. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  1309. mockChannelIdDraft: storedMockChannelId,
  1310. mockCoordText: '--',
  1311. mockSpeedText: '--',
  1312. heartRateSourceMode: 'real',
  1313. heartRateSourceText: '真实心率',
  1314. mockHeartRateBridgeConnected: false,
  1315. mockHeartRateBridgeStatusText: '未连接',
  1316. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
  1317. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
  1318. mockHeartRateText: '--',
  1319. mockDebugLogBridgeConnected: false,
  1320. mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
  1321. mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
  1322. mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
  1323. panelSpeedValueText: '0',
  1324. panelSpeedFxClass: '',
  1325. panelTelemetryTone: 'blue',
  1326. gpsLogoStatusText: '未配置',
  1327. gpsLogoSourceText: '--',
  1328. panelHeartRateZoneNameText: '--',
  1329. panelHeartRateZoneRangeText: '',
  1330. heartRateConnected: false,
  1331. heartRateStatusText: '心率带未连接',
  1332. heartRateDeviceText: '--',
  1333. panelHeartRateValueText: '--',
  1334. panelHeartRateFxClass: '',
  1335. panelHeartRateUnitText: '',
  1336. panelCaloriesValueText: '0',
  1337. panelCaloriesUnitText: 'kcal',
  1338. panelAverageSpeedValueText: '0',
  1339. panelAverageSpeedUnitText: 'km/h',
  1340. panelAccuracyValueText: '--',
  1341. panelAccuracyUnitText: '',
  1342. deviceHeadingText: '--',
  1343. devicePoseText: '竖持',
  1344. headingConfidenceText: '低',
  1345. accelerometerText: '--',
  1346. gyroscopeText: '--',
  1347. deviceMotionText: '--',
  1348. compassSourceText: '无数据',
  1349. compassTuningProfileText: initialEngineData.compassTuningProfileText || '平衡',
  1350. punchButtonText: '打点',
  1351. punchButtonEnabled: false,
  1352. skipButtonEnabled: false,
  1353. punchHintText: '等待进入检查点范围',
  1354. punchHintFxClass: '',
  1355. punchFeedbackVisible: false,
  1356. punchFeedbackText: '',
  1357. punchFeedbackTone: 'neutral',
  1358. contentCardVisible: false,
  1359. contentCardTemplate: 'story',
  1360. contentCardTitle: '',
  1361. contentCardBody: '',
  1362. contentCardActions: [],
  1363. contentQuizVisible: false,
  1364. contentQuizQuestionText: '',
  1365. contentQuizCountdownText: '',
  1366. contentQuizOptions: [],
  1367. contentQuizFeedbackVisible: false,
  1368. contentQuizFeedbackText: '',
  1369. contentQuizFeedbackTone: 'neutral',
  1370. punchButtonFxClass: '',
  1371. panelProgressFxClass: '',
  1372. panelDistanceFxClass: '',
  1373. punchFeedbackFxClass: '',
  1374. contentCardFxClass: '',
  1375. mapPulseVisible: false,
  1376. mapPulseLeftPx: 0,
  1377. mapPulseTopPx: 0,
  1378. mapPulseFxClass: '',
  1379. stageFxVisible: false,
  1380. stageFxClass: '',
  1381. compassTicks: buildCompassTicks(),
  1382. compassLabels: buildCompassLabels(),
  1383. ...buildSideButtonVisibility('shown'),
  1384. ...buildSideButtonState({
  1385. sideButtonMode: 'shown',
  1386. showGameInfoPanel: false,
  1387. showSystemSettingsPanel: false,
  1388. showCenterScaleRuler: initialSystemSettings.showCenterScaleRuler,
  1389. centerScaleRulerAnchorMode: initialSystemSettings.centerScaleRulerAnchorMode,
  1390. skipButtonEnabled: false,
  1391. gameSessionStatus: 'idle',
  1392. gpsLockEnabled: false,
  1393. gpsLockAvailable: false,
  1394. }),
  1395. ...buildCenterScaleRulerPatch({
  1396. ...(mapEngine.getInitialData() as MapPageData),
  1397. showCenterScaleRuler: initialSystemSettings.showCenterScaleRuler,
  1398. centerScaleRulerAnchorMode: initialSystemSettings.centerScaleRulerAnchorMode,
  1399. stageWidth: 0,
  1400. stageHeight: 0,
  1401. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  1402. zoom: 0,
  1403. centerTileY: 0,
  1404. tileSizePx: 0,
  1405. }),
  1406. }, () => {
  1407. if (shouldAutoConnectMockSources) {
  1408. this.handleConnectAllMockSources()
  1409. }
  1410. })
  1411. },
  1412. onReady() {
  1413. stageCanvasAttached = false
  1414. this.measureStageAndCanvas()
  1415. this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  1416. const app = getApp<IAppOption>()
  1417. const pendingHeartRateAutoConnect = app.globalData ? app.globalData.pendingHeartRateAutoConnect : null
  1418. if (pendingHeartRateAutoConnect && pendingHeartRateAutoConnect.enabled && mapEngine) {
  1419. const pendingDeviceName = pendingHeartRateAutoConnect.deviceName || '心率带'
  1420. app.globalData.pendingHeartRateAutoConnect = null
  1421. mapEngine.handleConnectHeartRate()
  1422. this.setData({
  1423. statusText: `正在自动连接局前设备:${pendingDeviceName}`,
  1424. heartRateStatusText: `正在自动连接 ${pendingDeviceName}`,
  1425. heartRateDeviceText: pendingDeviceName,
  1426. })
  1427. }
  1428. },
  1429. onShow() {
  1430. if (mapEngine) {
  1431. this.applyCompiledRuntimeProfiles()
  1432. mapEngine.handleAppShow()
  1433. }
  1434. },
  1435. onHide() {
  1436. this.persistSessionRecoverySnapshot()
  1437. clearResultExitRedirectTimer()
  1438. clearResultExitCountdownTimer()
  1439. if (mapEngine) {
  1440. mapEngine.handleAppHide()
  1441. }
  1442. },
  1443. onUnload() {
  1444. this.persistSessionRecoverySnapshot()
  1445. clearSessionRecoveryPersistTimer()
  1446. clearResultExitRedirectTimer()
  1447. clearResultExitCountdownTimer()
  1448. syncedBackendSessionStartId = ''
  1449. syncedBackendSessionFinishId = ''
  1450. clearGameInfoPanelSyncTimer()
  1451. clearCenterScaleRulerSyncTimer()
  1452. clearCenterScaleRulerUpdateTimer()
  1453. clearPunchHintDismissTimer()
  1454. clearPunchHintFxTimer()
  1455. clearHudFxTimer('timer')
  1456. clearHudFxTimer('mileage')
  1457. clearHudFxTimer('speed')
  1458. clearHudFxTimer('heartRate')
  1459. if (mapEngine) {
  1460. mapEngine.destroy()
  1461. mapEngine = null
  1462. }
  1463. currentSystemSettingsConfig = undefined
  1464. currentRemoteMapConfig = undefined
  1465. systemSettingsLockLifetimeActive = false
  1466. currentGameLaunchEnvelope = getDemoGameLaunchEnvelope()
  1467. shouldAutoRestoreRecoverySnapshot = false
  1468. shouldAutoStartSessionOnEnter = false
  1469. redirectedToResultPage = false
  1470. stageCanvasAttached = false
  1471. },
  1472. loadGameLaunchEnvelope(envelope: GameLaunchEnvelope) {
  1473. this.loadMapConfigFromRemote(
  1474. envelope.config.configUrl,
  1475. envelope.config.configLabel,
  1476. )
  1477. },
  1478. persistSessionRecoverySnapshot() {
  1479. if (!mapEngine || !currentRemoteMapConfig) {
  1480. return false
  1481. }
  1482. const runtimeSnapshot = mapEngine.buildSessionRecoveryRuntimeSnapshot()
  1483. if (!runtimeSnapshot) {
  1484. return false
  1485. }
  1486. const snapshot: SessionRecoverySnapshot = {
  1487. schemaVersion: 1,
  1488. savedAt: Date.now(),
  1489. launchEnvelope: currentGameLaunchEnvelope,
  1490. configAppId: currentRemoteMapConfig.configAppId,
  1491. configVersion: currentRemoteMapConfig.configVersion,
  1492. runtime: runtimeSnapshot,
  1493. }
  1494. saveSessionRecoverySnapshot(snapshot)
  1495. return true
  1496. },
  1497. syncBackendSessionStart() {
  1498. const sessionContext = getCurrentBackendSessionContext()
  1499. if (!sessionContext || syncedBackendSessionStartId === sessionContext.sessionId) {
  1500. return
  1501. }
  1502. startSession({
  1503. baseUrl: getCurrentBackendBaseUrl(),
  1504. sessionId: sessionContext.sessionId,
  1505. sessionToken: sessionContext.sessionToken,
  1506. })
  1507. .then(() => {
  1508. syncedBackendSessionStartId = sessionContext.sessionId
  1509. })
  1510. .catch((error) => {
  1511. const message = error && error.message ? error.message : '未知错误'
  1512. this.setData({
  1513. statusText: `session start 上报失败: ${message}`,
  1514. })
  1515. })
  1516. },
  1517. syncBackendSessionFinish(statusOverride?: 'finished' | 'failed' | 'cancelled') {
  1518. const sessionContext = getCurrentBackendSessionContext()
  1519. if (!sessionContext || syncedBackendSessionFinishId === sessionContext.sessionId || !mapEngine) {
  1520. return
  1521. }
  1522. const finishSummary = mapEngine.getSessionFinishSummary(statusOverride)
  1523. if (!finishSummary) {
  1524. return
  1525. }
  1526. const summaryPayload: BackendSessionFinishSummaryPayload = {}
  1527. if (typeof finishSummary.finalDurationSec === 'number') {
  1528. summaryPayload.finalDurationSec = finishSummary.finalDurationSec
  1529. }
  1530. if (typeof finishSummary.finalScore === 'number') {
  1531. summaryPayload.finalScore = finishSummary.finalScore
  1532. }
  1533. if (typeof finishSummary.completedControls === 'number') {
  1534. summaryPayload.completedControls = finishSummary.completedControls
  1535. }
  1536. if (typeof finishSummary.totalControls === 'number') {
  1537. summaryPayload.totalControls = finishSummary.totalControls
  1538. }
  1539. if (typeof finishSummary.distanceMeters === 'number') {
  1540. summaryPayload.distanceMeters = finishSummary.distanceMeters
  1541. }
  1542. if (typeof finishSummary.averageSpeedKmh === 'number') {
  1543. summaryPayload.averageSpeedKmh = finishSummary.averageSpeedKmh
  1544. }
  1545. finishSession({
  1546. baseUrl: getCurrentBackendBaseUrl(),
  1547. sessionId: sessionContext.sessionId,
  1548. sessionToken: sessionContext.sessionToken,
  1549. status: finishSummary.status,
  1550. summary: summaryPayload,
  1551. })
  1552. .then(() => {
  1553. syncedBackendSessionFinishId = sessionContext.sessionId
  1554. })
  1555. .catch((error) => {
  1556. const message = error && error.message ? error.message : '未知错误'
  1557. this.setData({
  1558. statusText: `session finish 上报失败: ${message}`,
  1559. })
  1560. })
  1561. },
  1562. reportAbandonedRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
  1563. const sessionContext = getBackendSessionContextFromLaunchEnvelope(snapshot.launchEnvelope)
  1564. if (!sessionContext) {
  1565. reportBackendClientLog({
  1566. level: 'warn',
  1567. category: 'session-recovery',
  1568. message: 'abandon recovery without valid session context',
  1569. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1570. ? snapshot.launchEnvelope.business.eventId
  1571. : '',
  1572. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1573. ? snapshot.launchEnvelope.config.releaseId
  1574. : '',
  1575. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1576. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1577. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1578. ? snapshot.launchEnvelope.config.configUrl
  1579. : '',
  1580. details: {
  1581. phase: 'abandon-no-session',
  1582. },
  1583. })
  1584. clearSessionRecoverySnapshot()
  1585. return
  1586. }
  1587. reportBackendClientLog({
  1588. level: 'info',
  1589. category: 'session-recovery',
  1590. message: 'abandon recovery requested',
  1591. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1592. ? snapshot.launchEnvelope.business.eventId
  1593. : '',
  1594. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1595. ? snapshot.launchEnvelope.config.releaseId
  1596. : '',
  1597. sessionId: sessionContext.sessionId,
  1598. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1599. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1600. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1601. ? snapshot.launchEnvelope.config.configUrl
  1602. : '',
  1603. details: {
  1604. phase: 'abandon-requested',
  1605. },
  1606. })
  1607. finishSession({
  1608. baseUrl: getCurrentBackendBaseUrl(),
  1609. sessionId: sessionContext.sessionId,
  1610. sessionToken: sessionContext.sessionToken,
  1611. status: 'cancelled',
  1612. summary: {},
  1613. })
  1614. .then(() => {
  1615. syncedBackendSessionFinishId = sessionContext.sessionId
  1616. reportBackendClientLog({
  1617. level: 'info',
  1618. category: 'session-recovery',
  1619. message: 'abandon recovery synced as cancelled',
  1620. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1621. ? snapshot.launchEnvelope.business.eventId
  1622. : '',
  1623. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1624. ? snapshot.launchEnvelope.config.releaseId
  1625. : '',
  1626. sessionId: sessionContext.sessionId,
  1627. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1628. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1629. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1630. ? snapshot.launchEnvelope.config.configUrl
  1631. : '',
  1632. details: {
  1633. phase: 'abandon-finished',
  1634. },
  1635. })
  1636. clearSessionRecoverySnapshot()
  1637. wx.showToast({
  1638. title: '已放弃上次对局',
  1639. icon: 'none',
  1640. duration: 1400,
  1641. })
  1642. })
  1643. .catch((error) => {
  1644. reportBackendClientLog({
  1645. level: 'warn',
  1646. category: 'session-recovery',
  1647. message: 'abandon recovery finish(cancelled) failed',
  1648. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1649. ? snapshot.launchEnvelope.business.eventId
  1650. : '',
  1651. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1652. ? snapshot.launchEnvelope.config.releaseId
  1653. : '',
  1654. sessionId: sessionContext.sessionId,
  1655. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1656. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1657. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1658. ? snapshot.launchEnvelope.config.configUrl
  1659. : '',
  1660. details: {
  1661. phase: 'abandon-failed',
  1662. message: error && error.message ? error.message : '未知错误',
  1663. },
  1664. })
  1665. clearSessionRecoverySnapshot()
  1666. const message = error && error.message ? error.message : '未知错误'
  1667. this.setData({
  1668. statusText: `放弃恢复已生效,后端取消上报失败: ${message}`,
  1669. })
  1670. wx.showToast({
  1671. title: '已放弃上次对局',
  1672. icon: 'none',
  1673. duration: 1400,
  1674. })
  1675. })
  1676. },
  1677. stashPendingResultSnapshot(snapshot: MapEngineResultSnapshot) {
  1678. const app = getApp<IAppOption>()
  1679. if (app.globalData) {
  1680. app.globalData.pendingResultSnapshot = snapshot
  1681. app.globalData.pendingResultLaunchEnvelope = currentGameLaunchEnvelope
  1682. }
  1683. },
  1684. redirectToResultPage() {
  1685. if (redirectedToResultPage) {
  1686. return
  1687. }
  1688. clearResultExitRedirectTimer()
  1689. clearResultExitCountdownTimer()
  1690. redirectedToResultPage = true
  1691. const sessionContext = getCurrentBackendSessionContext()
  1692. const resultUrl = sessionContext
  1693. ? `/pages/result/result?sessionId=${encodeURIComponent(sessionContext.sessionId)}`
  1694. : '/pages/result/result'
  1695. wx.redirectTo({
  1696. url: resultUrl,
  1697. })
  1698. },
  1699. presentResultExitPrompt() {
  1700. clearResultExitRedirectTimer()
  1701. clearResultExitCountdownTimer()
  1702. let remainingSeconds = Math.ceil(RESULT_EXIT_REDIRECT_DELAY_MS / 1000)
  1703. this.setData({
  1704. showResultScene: true,
  1705. resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
  1706. })
  1707. resultExitCountdownTimer = setInterval(() => {
  1708. remainingSeconds -= 1
  1709. if (remainingSeconds <= 0) {
  1710. clearResultExitCountdownTimer()
  1711. return
  1712. }
  1713. this.setData({
  1714. resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
  1715. })
  1716. }, 1000) as unknown as number
  1717. resultExitRedirectTimer = setTimeout(() => {
  1718. resultExitRedirectTimer = 0
  1719. this.redirectToResultPage()
  1720. }, RESULT_EXIT_REDIRECT_DELAY_MS) as unknown as number
  1721. },
  1722. restoreRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
  1723. systemSettingsLockLifetimeActive = true
  1724. this.applyRuntimeSystemSettings(true)
  1725. const restored = mapEngine ? mapEngine.restoreSessionRecoveryRuntimeSnapshot(snapshot.runtime) : false
  1726. if (!restored) {
  1727. reportBackendClientLog({
  1728. level: 'warn',
  1729. category: 'session-recovery',
  1730. message: 'recovery restore failed',
  1731. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1732. ? snapshot.launchEnvelope.business.eventId
  1733. : '',
  1734. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1735. ? snapshot.launchEnvelope.config.releaseId
  1736. : '',
  1737. sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
  1738. ? snapshot.launchEnvelope.business.sessionId
  1739. : '',
  1740. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1741. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1742. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1743. ? snapshot.launchEnvelope.config.configUrl
  1744. : '',
  1745. details: {
  1746. phase: 'restore-failed',
  1747. },
  1748. })
  1749. clearSessionRecoverySnapshot()
  1750. wx.showToast({
  1751. title: '恢复失败,已回到初始状态',
  1752. icon: 'none',
  1753. duration: 1600,
  1754. })
  1755. return false
  1756. }
  1757. this.setData({
  1758. showResultScene: false,
  1759. showDebugPanel: false,
  1760. showGameInfoPanel: false,
  1761. showSystemSettingsPanel: false,
  1762. showStartEntryButton: false,
  1763. })
  1764. const sessionContext = getCurrentBackendSessionContext()
  1765. if (sessionContext) {
  1766. syncedBackendSessionStartId = sessionContext.sessionId
  1767. }
  1768. reportBackendClientLog({
  1769. level: 'info',
  1770. category: 'session-recovery',
  1771. message: 'recovery restored',
  1772. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1773. ? snapshot.launchEnvelope.business.eventId
  1774. : '',
  1775. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1776. ? snapshot.launchEnvelope.config.releaseId
  1777. : '',
  1778. sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
  1779. ? snapshot.launchEnvelope.business.sessionId
  1780. : '',
  1781. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1782. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1783. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1784. ? snapshot.launchEnvelope.config.configUrl
  1785. : '',
  1786. details: {
  1787. phase: 'restored',
  1788. },
  1789. })
  1790. this.syncSessionRecoveryLifecycle('running')
  1791. return true
  1792. },
  1793. syncSessionRecoveryLifecycle(status: MapPageData['gameSessionStatus']) {
  1794. if (status === 'running') {
  1795. this.persistSessionRecoverySnapshot()
  1796. if (!sessionRecoveryPersistTimer) {
  1797. sessionRecoveryPersistTimer = setInterval(() => {
  1798. this.persistSessionRecoverySnapshot()
  1799. }, SESSION_RECOVERY_PERSIST_INTERVAL_MS) as unknown as number
  1800. }
  1801. return
  1802. }
  1803. clearSessionRecoveryPersistTimer()
  1804. },
  1805. maybePromptSessionRecoveryRestore(config: RemoteMapConfig) {
  1806. const snapshot = loadSessionRecoverySnapshot()
  1807. if (!snapshot || !mapEngine) {
  1808. return false
  1809. }
  1810. if (
  1811. snapshot.launchEnvelope.config.configUrl !== currentGameLaunchEnvelope.config.configUrl
  1812. || snapshot.configAppId !== config.configAppId
  1813. ) {
  1814. reportBackendClientLog({
  1815. level: 'warn',
  1816. category: 'session-recovery',
  1817. message: 'recovery snapshot dropped due to config mismatch',
  1818. eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
  1819. ? snapshot.launchEnvelope.business.eventId
  1820. : '',
  1821. releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
  1822. ? snapshot.launchEnvelope.config.releaseId
  1823. : '',
  1824. sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
  1825. ? snapshot.launchEnvelope.business.sessionId
  1826. : '',
  1827. manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1828. ? snapshot.launchEnvelope.resolvedRelease.manifestUrl
  1829. : snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
  1830. ? snapshot.launchEnvelope.config.configUrl
  1831. : '',
  1832. details: {
  1833. phase: 'config-mismatch',
  1834. currentConfigUrl: currentGameLaunchEnvelope.config.configUrl,
  1835. snapshotConfigUrl: snapshot.launchEnvelope.config.configUrl,
  1836. currentConfigAppId: config.configAppId,
  1837. snapshotConfigAppId: snapshot.configAppId,
  1838. },
  1839. })
  1840. clearSessionRecoverySnapshot()
  1841. this.setData({
  1842. statusText: '检测到旧局恢复记录,但当前配置源已变化,已回到初始状态',
  1843. })
  1844. return false
  1845. }
  1846. if (shouldAutoRestoreRecoverySnapshot) {
  1847. shouldAutoRestoreRecoverySnapshot = false
  1848. reportBackendClientLog({
  1849. level: 'info',
  1850. category: 'session-recovery',
  1851. message: 'auto recovery requested',
  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: 'auto-restore',
  1868. },
  1869. })
  1870. this.restoreRecoverySnapshot(snapshot)
  1871. return true
  1872. }
  1873. this.setData({
  1874. showStartEntryButton: true,
  1875. })
  1876. wx.showModal({
  1877. title: '恢复对局',
  1878. content: '检测到上次有未正常结束的对局,是否继续恢复?',
  1879. confirmText: '继续恢复',
  1880. cancelText: '放弃',
  1881. success: (result) => {
  1882. if (!result.confirm) {
  1883. this.reportAbandonedRecoverySnapshot(snapshot)
  1884. return
  1885. }
  1886. this.restoreRecoverySnapshot(snapshot)
  1887. },
  1888. })
  1889. return true
  1890. },
  1891. maybeAutoStartSessionOnEnter() {
  1892. if (!shouldAutoStartSessionOnEnter || !mapEngine) {
  1893. return
  1894. }
  1895. shouldAutoStartSessionOnEnter = false
  1896. systemSettingsLockLifetimeActive = true
  1897. this.applyRuntimeSystemSettings(true)
  1898. this.setData({
  1899. showStartEntryButton: false,
  1900. })
  1901. mapEngine.handleStartGame()
  1902. },
  1903. compileCurrentRuntimeProfile(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
  1904. if (!currentRemoteMapConfig) {
  1905. return null
  1906. }
  1907. return compileRuntimeProfile(currentRemoteMapConfig, {
  1908. playerTelemetryProfile: getGlobalTelemetryProfile(),
  1909. settingsLockLifetimeActive: lockLifetimeActive,
  1910. })
  1911. },
  1912. applyCompiledRuntimeProfiles(
  1913. lockLifetimeActive = isSystemSettingsLockLifetimeActive(),
  1914. options?: {
  1915. includeSettings?: boolean
  1916. includeMap?: boolean
  1917. includeGame?: boolean
  1918. includePresentation?: boolean
  1919. includeTelemetry?: boolean
  1920. includeFeedback?: boolean
  1921. },
  1922. ) {
  1923. const currentEngine = mapEngine
  1924. if (!currentEngine) {
  1925. return null
  1926. }
  1927. const compiledProfile = this.compileCurrentRuntimeProfile(lockLifetimeActive)
  1928. if (!compiledProfile) {
  1929. return null
  1930. }
  1931. if (options && options.includeMap) {
  1932. currentEngine.applyCompiledMapProfile(compiledProfile.map)
  1933. }
  1934. if (options && options.includeSettings) {
  1935. currentEngine.applyCompiledSettingsProfile(compiledProfile.settings)
  1936. }
  1937. if (options && options.includeGame) {
  1938. currentEngine.applyCompiledGameProfile(compiledProfile.game)
  1939. }
  1940. if (options && options.includePresentation) {
  1941. currentEngine.applyCompiledPresentationProfile(compiledProfile.presentation)
  1942. }
  1943. if (!options || options.includeTelemetry !== false) {
  1944. currentEngine.applyCompiledTelemetryProfile(compiledProfile.telemetry)
  1945. }
  1946. if (!options || options.includeFeedback !== false) {
  1947. currentEngine.applyCompiledFeedbackProfile(compiledProfile.feedback)
  1948. }
  1949. return compiledProfile
  1950. },
  1951. applyRuntimeSystemSettings(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
  1952. const currentEngine = mapEngine
  1953. if (!currentEngine) {
  1954. return null
  1955. }
  1956. const compiledProfile = this.applyCompiledRuntimeProfiles(lockLifetimeActive, {
  1957. includeSettings: true,
  1958. })
  1959. || {
  1960. settings: resolveSystemSettingsState(
  1961. currentSystemSettingsConfig,
  1962. undefined,
  1963. lockLifetimeActive,
  1964. ),
  1965. }
  1966. const resolvedSettings = compiledProfile.settings
  1967. const engineSnapshot = currentEngine.getInitialData() as Partial<MapPageData>
  1968. updateCenterScaleRulerInputCache(engineSnapshot)
  1969. const resolvedPatch = buildResolvedSystemSettingsPatch(resolvedSettings)
  1970. const mergedData = {
  1971. ...centerScaleRulerInputCache,
  1972. ...this.data,
  1973. ...engineSnapshot,
  1974. ...resolvedPatch,
  1975. } as MapPageData
  1976. this.setData({
  1977. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, resolvedSettings.values.showCenterScaleRuler),
  1978. ...resolvedPatch,
  1979. ...buildCenterScaleRulerPatch(mergedData),
  1980. ...buildSideButtonState(mergedData),
  1981. })
  1982. return resolvedSettings
  1983. },
  1984. persistAndApplySystemSettings(
  1985. patch: Partial<StoredUserSettings>,
  1986. options?: {
  1987. applyCenterScaleRuler?: boolean
  1988. },
  1989. ) {
  1990. updateStoredUserSettings(patch)
  1991. const lockLifetimeActive = isSystemSettingsLockLifetimeActive()
  1992. const resolvedSettings = this.applyRuntimeSystemSettings(lockLifetimeActive)
  1993. if (!resolvedSettings || !(options && options.applyCenterScaleRuler)) {
  1994. return resolvedSettings
  1995. }
  1996. this.applyCenterScaleRulerSettings(
  1997. resolvedSettings.values.showCenterScaleRuler,
  1998. resolvedSettings.values.centerScaleRulerAnchorMode,
  1999. )
  2000. return resolvedSettings
  2001. },
  2002. loadMapConfigFromRemote(configUrl: string, configLabel: string) {
  2003. const currentEngine = mapEngine
  2004. if (!currentEngine) {
  2005. return
  2006. }
  2007. this.setData({
  2008. configSourceText: configLabel,
  2009. configStatusText: `加载中: ${configLabel}`,
  2010. })
  2011. loadRemoteMapConfig(configUrl)
  2012. .then((config) => {
  2013. if (mapEngine !== currentEngine) {
  2014. return
  2015. }
  2016. currentEngine.applyRemoteMapConfig(config)
  2017. this.applyConfiguredSystemSettings(config)
  2018. const compiledProfile = this.applyCompiledRuntimeProfiles(true, {
  2019. includeMap: true,
  2020. includeGame: true,
  2021. includePresentation: true,
  2022. })
  2023. if (compiledProfile) {
  2024. reportBackendClientLog({
  2025. level: 'info',
  2026. category: 'runtime-compiler',
  2027. message: 'compiled runtime profile applied',
  2028. eventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
  2029. ? currentGameLaunchEnvelope.business.eventId
  2030. : '',
  2031. releaseId: currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.releaseId
  2032. ? currentGameLaunchEnvelope.config.releaseId
  2033. : '',
  2034. sessionId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.sessionId
  2035. ? currentGameLaunchEnvelope.business.sessionId
  2036. : '',
  2037. manifestUrl: currentGameLaunchEnvelope.resolvedRelease && currentGameLaunchEnvelope.resolvedRelease.manifestUrl
  2038. ? currentGameLaunchEnvelope.resolvedRelease.manifestUrl
  2039. : currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.configUrl
  2040. ? currentGameLaunchEnvelope.config.configUrl
  2041. : '',
  2042. details: {
  2043. phase: 'compiled-runtime-applied',
  2044. schemaVersion: config.configSchemaVersion || '',
  2045. playfield: {
  2046. kind: config.playfieldKind || '',
  2047. },
  2048. game: {
  2049. mode: config.gameMode || '',
  2050. },
  2051. },
  2052. })
  2053. }
  2054. const recoveryHandled = this.maybePromptSessionRecoveryRestore(config)
  2055. if (!recoveryHandled) {
  2056. this.maybeAutoStartSessionOnEnter()
  2057. } else {
  2058. shouldAutoStartSessionOnEnter = false
  2059. }
  2060. })
  2061. .catch((error) => {
  2062. if (mapEngine !== currentEngine) {
  2063. return
  2064. }
  2065. const rawErrorMessage = error && error.message ? error.message : '未知错误'
  2066. const errorMessage = rawErrorMessage.indexOf('404') >= 0
  2067. ? `release manifest 不存在或未发布 (${configLabel})`
  2068. : rawErrorMessage
  2069. this.setData({
  2070. configStatusText: `载入失败: ${errorMessage}`,
  2071. statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
  2072. })
  2073. })
  2074. },
  2075. applyConfiguredSystemSettings(config: RemoteMapConfig) {
  2076. currentRemoteMapConfig = config
  2077. currentSystemSettingsConfig = config.systemSettingsConfig
  2078. systemSettingsLockLifetimeActive = true
  2079. this.applyRuntimeSystemSettings(true)
  2080. },
  2081. measureStageAndCanvas(onApplied?: () => void) {
  2082. const page = this
  2083. const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
  2084. const fallbackRect = getFallbackStageRect()
  2085. const rect: MapEngineStageRect = {
  2086. width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
  2087. height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
  2088. left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
  2089. top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
  2090. }
  2091. const currentEngine = mapEngine
  2092. if (!currentEngine) {
  2093. return
  2094. }
  2095. currentEngine.setStage(rect)
  2096. if (onApplied) {
  2097. onApplied()
  2098. }
  2099. if (stageCanvasAttached) {
  2100. return
  2101. }
  2102. const canvasQuery = wx.createSelectorQuery().in(page)
  2103. canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
  2104. canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
  2105. canvasQuery.exec((canvasRes) => {
  2106. const canvasRef = canvasRes[0] as any
  2107. const labelCanvasRef = canvasRes[1] as any
  2108. if (!canvasRef || !canvasRef.node) {
  2109. page.setData({
  2110. statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
  2111. })
  2112. return
  2113. }
  2114. const dpr = wx.getSystemInfoSync().pixelRatio || 1
  2115. try {
  2116. currentEngine.attachCanvas(
  2117. canvasRef.node,
  2118. rect.width,
  2119. rect.height,
  2120. dpr,
  2121. labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
  2122. )
  2123. stageCanvasAttached = true
  2124. } catch (error) {
  2125. page.setData({
  2126. statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
  2127. })
  2128. }
  2129. })
  2130. }
  2131. const query = wx.createSelectorQuery().in(page)
  2132. query.select('.map-stage').boundingClientRect()
  2133. query.exec((res) => {
  2134. const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
  2135. applyStage(rect)
  2136. })
  2137. },
  2138. handleTouchStart(event: WechatMiniprogram.TouchEvent) {
  2139. if (mapEngine) {
  2140. mapEngine.handleTouchStart(event)
  2141. }
  2142. },
  2143. handleTouchMove(event: WechatMiniprogram.TouchEvent) {
  2144. if (mapEngine) {
  2145. mapEngine.handleTouchMove(event)
  2146. }
  2147. },
  2148. handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
  2149. if (mapEngine) {
  2150. mapEngine.handleTouchEnd(event)
  2151. }
  2152. },
  2153. handleTouchCancel() {
  2154. if (mapEngine) {
  2155. mapEngine.handleTouchCancel()
  2156. }
  2157. },
  2158. handleRecenter() {
  2159. if (mapEngine) {
  2160. mapEngine.handleRecenter()
  2161. }
  2162. },
  2163. handleRotateStep() {
  2164. if (mapEngine) {
  2165. mapEngine.handleRotateStep()
  2166. }
  2167. },
  2168. handleRotationReset() {
  2169. if (mapEngine) {
  2170. mapEngine.handleRotationReset()
  2171. }
  2172. },
  2173. handleSetManualMode() {
  2174. if (mapEngine) {
  2175. mapEngine.handleSetManualMode()
  2176. }
  2177. },
  2178. handleSetNorthUpMode() {
  2179. if (mapEngine) {
  2180. mapEngine.handleSetNorthUpMode()
  2181. }
  2182. },
  2183. handleSetHeadingUpMode() {
  2184. if (mapEngine) {
  2185. mapEngine.handleSetHeadingUpMode()
  2186. }
  2187. },
  2188. handleCycleNorthReferenceMode() {
  2189. if (mapEngine) {
  2190. mapEngine.handleCycleNorthReferenceMode()
  2191. }
  2192. },
  2193. handleAutoRotateCalibrate() {
  2194. if (mapEngine) {
  2195. mapEngine.handleAutoRotateCalibrate()
  2196. }
  2197. },
  2198. handleToggleGpsTracking() {
  2199. if (mapEngine) {
  2200. mapEngine.handleToggleGpsTracking()
  2201. }
  2202. },
  2203. handleSetRealLocationMode() {
  2204. if (mapEngine) {
  2205. mapEngine.handleSetRealLocationMode()
  2206. }
  2207. },
  2208. handleSetMockLocationMode() {
  2209. if (mapEngine) {
  2210. mapEngine.handleSetMockLocationMode()
  2211. }
  2212. },
  2213. handleConnectMockLocationBridge() {
  2214. if (mapEngine) {
  2215. mapEngine.handleConnectMockLocationBridge()
  2216. }
  2217. },
  2218. handleConnectAllMockSources() {
  2219. if (!mapEngine) {
  2220. return
  2221. }
  2222. const channelId = (this.data.mockChannelIdDraft || '').trim() || 'default'
  2223. this.setData({
  2224. mockChannelIdDraft: channelId,
  2225. })
  2226. persistMockChannelId(channelId)
  2227. persistMockAutoConnectEnabled(true)
  2228. setGlobalMockDebugBridgeChannelId(channelId)
  2229. setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2230. persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2231. setGlobalMockDebugBridgeEnabled(true)
  2232. mapEngine.handleSetMockChannelId(channelId)
  2233. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  2234. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  2235. mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2236. mapEngine.handleConnectMockLocationBridge()
  2237. mapEngine.handleSetMockLocationMode()
  2238. mapEngine.handleSetMockHeartRateMode()
  2239. mapEngine.handleConnectMockHeartRateBridge()
  2240. mapEngine.handleConnectMockDebugLogBridge()
  2241. },
  2242. handleOpenWebViewTest() {
  2243. wx.navigateTo({
  2244. url: '/pages/webview-test/webview-test',
  2245. })
  2246. },
  2247. handleMockChannelIdInput(event: WechatMiniprogram.Input) {
  2248. this.setData({
  2249. mockChannelIdDraft: event.detail.value,
  2250. })
  2251. },
  2252. handleSaveMockChannelId() {
  2253. const channelId = (this.data.mockChannelIdDraft || '').trim() || 'default'
  2254. this.setData({
  2255. mockChannelIdDraft: channelId,
  2256. })
  2257. persistMockChannelId(channelId)
  2258. setGlobalMockDebugBridgeChannelId(channelId)
  2259. if (mapEngine) {
  2260. mapEngine.handleSetMockChannelId(channelId)
  2261. }
  2262. },
  2263. handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
  2264. this.setData({
  2265. mockBridgeUrlDraft: event.detail.value,
  2266. })
  2267. },
  2268. handleSaveMockBridgeUrl() {
  2269. if (mapEngine) {
  2270. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  2271. }
  2272. },
  2273. handleDisconnectMockLocationBridge() {
  2274. persistMockAutoConnectEnabled(false)
  2275. if (mapEngine) {
  2276. mapEngine.handleDisconnectMockLocationBridge()
  2277. }
  2278. },
  2279. handleSetRealHeartRateMode() {
  2280. if (mapEngine) {
  2281. mapEngine.handleSetRealHeartRateMode()
  2282. }
  2283. },
  2284. handleSetMockHeartRateMode() {
  2285. if (mapEngine) {
  2286. mapEngine.handleSetMockHeartRateMode()
  2287. }
  2288. },
  2289. handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
  2290. this.setData({
  2291. mockHeartRateBridgeUrlDraft: event.detail.value,
  2292. })
  2293. },
  2294. handleSaveMockHeartRateBridgeUrl() {
  2295. if (mapEngine) {
  2296. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  2297. }
  2298. },
  2299. handleMockDebugLogBridgeUrlInput(event: WechatMiniprogram.Input) {
  2300. this.setData({
  2301. mockDebugLogBridgeUrlDraft: event.detail.value,
  2302. })
  2303. },
  2304. handleSaveMockDebugLogBridgeUrl() {
  2305. persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2306. setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2307. if (mapEngine) {
  2308. mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2309. }
  2310. },
  2311. handleConnectMockDebugLogBridge() {
  2312. setGlobalMockDebugBridgeChannelId((this.data.mockChannelIdDraft || '').trim() || 'default')
  2313. setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  2314. setGlobalMockDebugBridgeEnabled(true)
  2315. if (mapEngine) {
  2316. mapEngine.handleConnectMockDebugLogBridge()
  2317. }
  2318. },
  2319. handleDisconnectMockDebugLogBridge() {
  2320. persistMockAutoConnectEnabled(false)
  2321. setGlobalMockDebugBridgeEnabled(false)
  2322. if (mapEngine) {
  2323. mapEngine.handleDisconnectMockDebugLogBridge()
  2324. }
  2325. },
  2326. handleConnectMockHeartRateBridge() {
  2327. if (mapEngine) {
  2328. mapEngine.handleConnectMockHeartRateBridge()
  2329. }
  2330. },
  2331. handleDisconnectMockHeartRateBridge() {
  2332. persistMockAutoConnectEnabled(false)
  2333. if (mapEngine) {
  2334. mapEngine.handleDisconnectMockHeartRateBridge()
  2335. }
  2336. },
  2337. handleConnectHeartRate() {
  2338. if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
  2339. return
  2340. }
  2341. if (mapEngine) {
  2342. mapEngine.handleConnectHeartRate()
  2343. }
  2344. },
  2345. handleOpenHeartRateDevicePicker() {
  2346. if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
  2347. return
  2348. }
  2349. this.setData({
  2350. showHeartRateDevicePicker: true,
  2351. })
  2352. if (mapEngine) {
  2353. mapEngine.handleConnectHeartRate()
  2354. }
  2355. },
  2356. handleCloseHeartRateDevicePicker() {
  2357. this.setData({
  2358. showHeartRateDevicePicker: false,
  2359. })
  2360. },
  2361. handleDisconnectHeartRate() {
  2362. if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
  2363. return
  2364. }
  2365. if (mapEngine) {
  2366. mapEngine.handleDisconnectHeartRate()
  2367. }
  2368. },
  2369. handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  2370. if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
  2371. const targetDeviceId = event.currentTarget.dataset.deviceId
  2372. const targetDevice = this.data.heartRateDiscoveredDevices.find((item) => item.deviceId === targetDeviceId)
  2373. pendingHeartRateSwitchDeviceName = targetDevice ? targetDevice.name : null
  2374. mapEngine.handleConnectHeartRateDevice(targetDeviceId)
  2375. this.setData({
  2376. showHeartRateDevicePicker: false,
  2377. statusText: targetDevice
  2378. ? `正在切换到 ${targetDevice.name}`
  2379. : '正在切换心率带设备',
  2380. })
  2381. }
  2382. },
  2383. handleClearPreferredHeartRateDevice() {
  2384. if (this.data.lockHeartRateDevice) {
  2385. return
  2386. }
  2387. if (mapEngine) {
  2388. mapEngine.handleClearPreferredHeartRateDevice()
  2389. }
  2390. },
  2391. handleDebugHeartRateBlue() {
  2392. if (mapEngine) {
  2393. mapEngine.handleDebugHeartRateTone('blue')
  2394. }
  2395. },
  2396. handleDebugHeartRatePurple() {
  2397. if (mapEngine) {
  2398. mapEngine.handleDebugHeartRateTone('purple')
  2399. }
  2400. },
  2401. handleDebugHeartRateGreen() {
  2402. if (mapEngine) {
  2403. mapEngine.handleDebugHeartRateTone('green')
  2404. }
  2405. },
  2406. handleDebugHeartRateYellow() {
  2407. if (mapEngine) {
  2408. mapEngine.handleDebugHeartRateTone('yellow')
  2409. }
  2410. },
  2411. handleDebugHeartRateOrange() {
  2412. if (mapEngine) {
  2413. mapEngine.handleDebugHeartRateTone('orange')
  2414. }
  2415. },
  2416. handleDebugHeartRateRed() {
  2417. if (mapEngine) {
  2418. mapEngine.handleDebugHeartRateTone('red')
  2419. }
  2420. },
  2421. handleDebugSetSessionRemainingWarning() {
  2422. if (mapEngine) {
  2423. mapEngine.handleDebugSetSessionRemainingWarning()
  2424. }
  2425. },
  2426. handleDebugSetSessionRemainingOneMinute() {
  2427. if (mapEngine) {
  2428. mapEngine.handleDebugSetSessionRemainingOneMinute()
  2429. }
  2430. },
  2431. handleDebugTimeoutSession() {
  2432. if (mapEngine) {
  2433. mapEngine.handleDebugTimeoutSession()
  2434. }
  2435. },
  2436. handleClearDebugHeartRate() {
  2437. if (mapEngine) {
  2438. mapEngine.handleClearDebugHeartRate()
  2439. }
  2440. },
  2441. handleToggleOsmReference() {
  2442. if (mapEngine) {
  2443. mapEngine.handleToggleOsmReference()
  2444. }
  2445. },
  2446. handleStartGame() {
  2447. if (mapEngine) {
  2448. shouldAutoStartSessionOnEnter = false
  2449. systemSettingsLockLifetimeActive = true
  2450. this.applyRuntimeSystemSettings(true)
  2451. this.setData({
  2452. showStartEntryButton: false,
  2453. })
  2454. mapEngine.handleStartGame()
  2455. }
  2456. },
  2457. handleLoadClassicConfig() {
  2458. currentGameLaunchEnvelope = getDemoGameLaunchEnvelope('classic')
  2459. this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  2460. },
  2461. handleLoadScoreOConfig() {
  2462. currentGameLaunchEnvelope = getDemoGameLaunchEnvelope('score-o')
  2463. this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
  2464. },
  2465. handleForceExitGame() {
  2466. if (!mapEngine || this.data.gameSessionStatus !== 'running') {
  2467. return
  2468. }
  2469. wx.showModal({
  2470. title: '确认退出',
  2471. content: '确认强制结束当前对局并返回开始前状态?',
  2472. confirmText: '确认退出',
  2473. cancelText: '取消',
  2474. success: (result) => {
  2475. if (result.confirm && mapEngine) {
  2476. clearResultExitRedirectTimer()
  2477. clearResultExitCountdownTimer()
  2478. this.syncBackendSessionFinish('cancelled')
  2479. clearSessionRecoverySnapshot()
  2480. clearSessionRecoveryPersistTimer()
  2481. systemSettingsLockLifetimeActive = false
  2482. mapEngine.handleForceExitGame()
  2483. wx.showToast({
  2484. title: '已退出当前对局',
  2485. icon: 'none',
  2486. duration: 1000,
  2487. })
  2488. setTimeout(() => {
  2489. navigateAwayFromMapAfterCancel()
  2490. }, 180)
  2491. }
  2492. },
  2493. })
  2494. },
  2495. handleSkipAction() {
  2496. if (!mapEngine || !this.data.skipButtonEnabled) {
  2497. return
  2498. }
  2499. if (!mapEngine.shouldConfirmSkipAction()) {
  2500. mapEngine.handleSkipAction()
  2501. return
  2502. }
  2503. wx.showModal({
  2504. title: '确认跳点',
  2505. content: '确认跳过当前检查点并切换到下一个目标点?',
  2506. confirmText: '确认跳过',
  2507. cancelText: '取消',
  2508. success: (result) => {
  2509. if (result.confirm && mapEngine) {
  2510. mapEngine.handleSkipAction()
  2511. }
  2512. },
  2513. })
  2514. },
  2515. handleClearMapTestArtifacts() {
  2516. if (mapEngine) {
  2517. mapEngine.handleClearMapTestArtifacts()
  2518. }
  2519. },
  2520. syncGameInfoPanelSnapshot() {
  2521. if (!mapEngine) {
  2522. return
  2523. }
  2524. const snapshot = mapEngine.getGameInfoSnapshot()
  2525. const localRows = snapshot.localRows.concat([
  2526. ...buildRuntimeSummaryRows(currentGameLaunchEnvelope),
  2527. ...buildLaunchConfigSummaryRows(currentGameLaunchEnvelope),
  2528. { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
  2529. { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
  2530. { label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
  2531. { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
  2532. { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
  2533. { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
  2534. { label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
  2535. { label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
  2536. ])
  2537. this.setData({
  2538. gameInfoTitle: snapshot.title,
  2539. gameInfoSubtitle: snapshot.subtitle,
  2540. gameInfoLocalRows: localRows,
  2541. gameInfoGlobalRows: snapshot.globalRows,
  2542. })
  2543. },
  2544. syncResultSceneSnapshot() {
  2545. if (!mapEngine) {
  2546. return
  2547. }
  2548. const snapshot = mapEngine.getResultSceneSnapshot()
  2549. this.setData({
  2550. resultSceneTitle: snapshot.title,
  2551. resultSceneSubtitle: snapshot.subtitle,
  2552. resultSceneHeroLabel: snapshot.heroLabel,
  2553. resultSceneHeroValue: snapshot.heroValue,
  2554. resultSceneRows: snapshot.rows
  2555. .concat(buildRuntimeSummaryRows(currentGameLaunchEnvelope))
  2556. .concat(buildLaunchConfigSummaryRows(currentGameLaunchEnvelope)),
  2557. })
  2558. },
  2559. scheduleGameInfoPanelSnapshotSync() {
  2560. if (!this.data.showGameInfoPanel) {
  2561. clearGameInfoPanelSyncTimer()
  2562. return
  2563. }
  2564. if (gameInfoPanelSyncTimer) {
  2565. return
  2566. }
  2567. gameInfoPanelSyncTimer = setTimeout(() => {
  2568. gameInfoPanelSyncTimer = 0
  2569. if (this.data.showGameInfoPanel) {
  2570. this.syncGameInfoPanelSnapshot()
  2571. }
  2572. }, 400) as unknown as number
  2573. },
  2574. handleOpenGameInfoPanel() {
  2575. clearGameInfoPanelSyncTimer()
  2576. this.syncGameInfoPanelSnapshot()
  2577. this.setData({
  2578. showDebugPanel: false,
  2579. showSystemSettingsPanel: false,
  2580. showGameInfoPanel: true,
  2581. ...buildSideButtonState({
  2582. sideButtonMode: this.data.sideButtonMode,
  2583. showGameInfoPanel: true,
  2584. showSystemSettingsPanel: false,
  2585. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2586. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2587. skipButtonEnabled: this.data.skipButtonEnabled,
  2588. gameSessionStatus: this.data.gameSessionStatus,
  2589. gpsLockEnabled: this.data.gpsLockEnabled,
  2590. gpsLockAvailable: this.data.gpsLockAvailable,
  2591. }),
  2592. })
  2593. },
  2594. handleCloseGameInfoPanel() {
  2595. clearGameInfoPanelSyncTimer()
  2596. this.setData({
  2597. showGameInfoPanel: false,
  2598. ...buildSideButtonState({
  2599. sideButtonMode: this.data.sideButtonMode,
  2600. showGameInfoPanel: false,
  2601. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  2602. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2603. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2604. skipButtonEnabled: this.data.skipButtonEnabled,
  2605. gameSessionStatus: this.data.gameSessionStatus,
  2606. gpsLockEnabled: this.data.gpsLockEnabled,
  2607. gpsLockAvailable: this.data.gpsLockAvailable,
  2608. }),
  2609. })
  2610. },
  2611. handleGameInfoPanelTap() {},
  2612. handleResultSceneTap() {},
  2613. handleCloseResultScene() {
  2614. this.redirectToResultPage()
  2615. },
  2616. handleRestartFromResult() {
  2617. this.redirectToResultPage()
  2618. },
  2619. handleOpenSystemSettingsPanel() {
  2620. clearGameInfoPanelSyncTimer()
  2621. this.setData({
  2622. showDebugPanel: false,
  2623. showGameInfoPanel: false,
  2624. showSystemSettingsPanel: true,
  2625. ...buildSideButtonState({
  2626. sideButtonMode: this.data.sideButtonMode,
  2627. showGameInfoPanel: false,
  2628. showSystemSettingsPanel: true,
  2629. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2630. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2631. skipButtonEnabled: this.data.skipButtonEnabled,
  2632. gameSessionStatus: this.data.gameSessionStatus,
  2633. gpsLockEnabled: this.data.gpsLockEnabled,
  2634. gpsLockAvailable: this.data.gpsLockAvailable,
  2635. }),
  2636. })
  2637. },
  2638. handleCloseSystemSettingsPanel() {
  2639. this.setData({
  2640. showSystemSettingsPanel: false,
  2641. ...buildSideButtonState({
  2642. sideButtonMode: this.data.sideButtonMode,
  2643. showGameInfoPanel: this.data.showGameInfoPanel,
  2644. showSystemSettingsPanel: false,
  2645. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2646. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2647. skipButtonEnabled: this.data.skipButtonEnabled,
  2648. gameSessionStatus: this.data.gameSessionStatus,
  2649. gpsLockEnabled: this.data.gpsLockEnabled,
  2650. gpsLockAvailable: this.data.gpsLockAvailable,
  2651. }),
  2652. })
  2653. },
  2654. handleSystemSettingsPanelTap() {},
  2655. handleSetAnimationLevelStandard() {
  2656. if (this.data.lockAnimationLevel || !mapEngine) {
  2657. return
  2658. }
  2659. this.persistAndApplySystemSettings({
  2660. animationLevel: 'standard',
  2661. })
  2662. },
  2663. handleSetAnimationLevelLite() {
  2664. if (this.data.lockAnimationLevel || !mapEngine) {
  2665. return
  2666. }
  2667. this.persistAndApplySystemSettings({
  2668. animationLevel: 'lite',
  2669. })
  2670. },
  2671. handleSetTrackModeNone() {
  2672. if (this.data.lockTrackMode || !mapEngine) {
  2673. return
  2674. }
  2675. this.persistAndApplySystemSettings({
  2676. trackDisplayMode: 'none',
  2677. })
  2678. },
  2679. handleSetTrackModeTail() {
  2680. if (this.data.lockTrackMode || !mapEngine) {
  2681. return
  2682. }
  2683. this.persistAndApplySystemSettings({
  2684. trackDisplayMode: 'tail',
  2685. })
  2686. },
  2687. handleSetTrackModeFull() {
  2688. if (this.data.lockTrackMode || !mapEngine) {
  2689. return
  2690. }
  2691. this.persistAndApplySystemSettings({
  2692. trackDisplayMode: 'full',
  2693. })
  2694. },
  2695. handleSetTrackTailLengthShort() {
  2696. if (this.data.lockTrackTailLength || !mapEngine) {
  2697. return
  2698. }
  2699. this.persistAndApplySystemSettings({
  2700. trackTailLength: 'short',
  2701. })
  2702. },
  2703. handleSetTrackTailLengthMedium() {
  2704. if (this.data.lockTrackTailLength || !mapEngine) {
  2705. return
  2706. }
  2707. this.persistAndApplySystemSettings({
  2708. trackTailLength: 'medium',
  2709. })
  2710. },
  2711. handleSetTrackTailLengthLong() {
  2712. if (this.data.lockTrackTailLength || !mapEngine) {
  2713. return
  2714. }
  2715. this.persistAndApplySystemSettings({
  2716. trackTailLength: 'long',
  2717. })
  2718. },
  2719. handleSetTrackColorPreset(event: WechatMiniprogram.TouchEvent) {
  2720. if (this.data.lockTrackColor || !mapEngine) {
  2721. return
  2722. }
  2723. const color = event.currentTarget.dataset.color as TrackColorPreset | undefined
  2724. if (!color) {
  2725. return
  2726. }
  2727. this.persistAndApplySystemSettings({
  2728. trackColorPreset: color,
  2729. })
  2730. },
  2731. handleSetTrackStyleClassic() {
  2732. if (this.data.lockTrackStyle || !mapEngine) {
  2733. return
  2734. }
  2735. this.persistAndApplySystemSettings({
  2736. trackStyleProfile: 'classic',
  2737. })
  2738. },
  2739. handleSetTrackStyleNeon() {
  2740. if (this.data.lockTrackStyle || !mapEngine) {
  2741. return
  2742. }
  2743. this.persistAndApplySystemSettings({
  2744. trackStyleProfile: 'neon',
  2745. })
  2746. },
  2747. handleSetGpsMarkerVisibleOn() {
  2748. if (this.data.lockGpsMarkerVisible || !mapEngine) {
  2749. return
  2750. }
  2751. this.persistAndApplySystemSettings({
  2752. gpsMarkerVisible: true,
  2753. })
  2754. },
  2755. handleSetGpsMarkerVisibleOff() {
  2756. if (this.data.lockGpsMarkerVisible || !mapEngine) {
  2757. return
  2758. }
  2759. this.persistAndApplySystemSettings({
  2760. gpsMarkerVisible: false,
  2761. })
  2762. },
  2763. handleSetGpsMarkerStyleDot() {
  2764. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2765. return
  2766. }
  2767. this.persistAndApplySystemSettings({
  2768. gpsMarkerStyle: 'dot',
  2769. })
  2770. },
  2771. handleSetGpsMarkerStyleBeacon() {
  2772. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2773. return
  2774. }
  2775. this.persistAndApplySystemSettings({
  2776. gpsMarkerStyle: 'beacon',
  2777. })
  2778. },
  2779. handleSetGpsMarkerStyleDisc() {
  2780. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2781. return
  2782. }
  2783. this.persistAndApplySystemSettings({
  2784. gpsMarkerStyle: 'disc',
  2785. })
  2786. },
  2787. handleSetGpsMarkerStyleBadge() {
  2788. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  2789. return
  2790. }
  2791. this.persistAndApplySystemSettings({
  2792. gpsMarkerStyle: 'badge',
  2793. })
  2794. },
  2795. handleSetGpsMarkerSizeSmall() {
  2796. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2797. return
  2798. }
  2799. this.persistAndApplySystemSettings({
  2800. gpsMarkerSize: 'small',
  2801. })
  2802. },
  2803. handleSetGpsMarkerSizeMedium() {
  2804. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2805. return
  2806. }
  2807. this.persistAndApplySystemSettings({
  2808. gpsMarkerSize: 'medium',
  2809. })
  2810. },
  2811. handleSetGpsMarkerSizeLarge() {
  2812. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2813. return
  2814. }
  2815. this.persistAndApplySystemSettings({
  2816. gpsMarkerSize: 'large',
  2817. })
  2818. },
  2819. handleSetGpsMarkerColorPreset(event: WechatMiniprogram.TouchEvent) {
  2820. if (this.data.lockGpsMarkerColor || !mapEngine) {
  2821. return
  2822. }
  2823. const color = event.currentTarget.dataset.color as GpsMarkerColorPreset | undefined
  2824. if (!color) {
  2825. return
  2826. }
  2827. this.persistAndApplySystemSettings({
  2828. gpsMarkerColorPreset: color,
  2829. })
  2830. },
  2831. handleSetSideButtonPlacementLeft() {
  2832. if (this.data.lockSideButtonPlacement) {
  2833. return
  2834. }
  2835. this.persistAndApplySystemSettings({
  2836. sideButtonPlacement: 'left',
  2837. })
  2838. },
  2839. handleSetSideButtonPlacementRight() {
  2840. if (this.data.lockSideButtonPlacement) {
  2841. return
  2842. }
  2843. this.persistAndApplySystemSettings({
  2844. sideButtonPlacement: 'right',
  2845. })
  2846. },
  2847. handleSetAutoRotateEnabledOn() {
  2848. if (this.data.lockAutoRotate || !mapEngine) {
  2849. return
  2850. }
  2851. this.persistAndApplySystemSettings({
  2852. autoRotateEnabled: true,
  2853. })
  2854. },
  2855. handleSetAutoRotateEnabledOff() {
  2856. if (this.data.lockAutoRotate || !mapEngine) {
  2857. return
  2858. }
  2859. this.persistAndApplySystemSettings({
  2860. autoRotateEnabled: false,
  2861. })
  2862. },
  2863. handleSetCompassTuningSmooth() {
  2864. if (this.data.lockCompassTuning || !mapEngine) {
  2865. return
  2866. }
  2867. this.persistAndApplySystemSettings({
  2868. compassTuningProfile: 'smooth',
  2869. })
  2870. },
  2871. handleSetCompassTuningBalanced() {
  2872. if (this.data.lockCompassTuning || !mapEngine) {
  2873. return
  2874. }
  2875. this.persistAndApplySystemSettings({
  2876. compassTuningProfile: 'balanced',
  2877. })
  2878. },
  2879. handleSetCompassTuningResponsive() {
  2880. if (this.data.lockCompassTuning || !mapEngine) {
  2881. return
  2882. }
  2883. this.persistAndApplySystemSettings({
  2884. compassTuningProfile: 'responsive',
  2885. })
  2886. },
  2887. handleSetNorthReferenceMagnetic() {
  2888. if (this.data.lockNorthReference || !mapEngine) {
  2889. return
  2890. }
  2891. this.persistAndApplySystemSettings({
  2892. northReferenceMode: 'magnetic',
  2893. })
  2894. },
  2895. handleSetNorthReferenceTrue() {
  2896. if (this.data.lockNorthReference || !mapEngine) {
  2897. return
  2898. }
  2899. this.persistAndApplySystemSettings({
  2900. northReferenceMode: 'true',
  2901. })
  2902. },
  2903. handleOverlayTouch() {},
  2904. handlePunchAction() {
  2905. if (!this.data.punchButtonEnabled) {
  2906. return
  2907. }
  2908. if (mapEngine) {
  2909. mapEngine.handlePunchAction()
  2910. }
  2911. },
  2912. handleOpenPendingContentCard() {
  2913. if (mapEngine) {
  2914. mapEngine.openPendingContentCard()
  2915. }
  2916. },
  2917. handleOpenContentCardAction(event: WechatMiniprogram.BaseEvent) {
  2918. if (!mapEngine) {
  2919. return
  2920. }
  2921. wx.showToast({
  2922. title: '点击CTA',
  2923. icon: 'none',
  2924. duration: 900,
  2925. })
  2926. const actionType = event.currentTarget.dataset.type
  2927. const action = typeof actionType === 'string' ? mapEngine.openCurrentContentCardAction(actionType) : null
  2928. if (action === 'detail') {
  2929. wx.showToast({
  2930. title: '打开详情',
  2931. icon: 'none',
  2932. duration: 900,
  2933. })
  2934. return
  2935. }
  2936. if (action === 'quiz') {
  2937. return
  2938. }
  2939. if (action === 'photo') {
  2940. wx.chooseMedia({
  2941. count: 1,
  2942. mediaType: ['image'],
  2943. sourceType: ['camera'],
  2944. success: () => {
  2945. if (mapEngine) {
  2946. mapEngine.handleContentCardPhotoCaptured()
  2947. }
  2948. },
  2949. })
  2950. return
  2951. }
  2952. if (action === 'audio') {
  2953. if (!contentAudioRecorder) {
  2954. contentAudioRecorder = wx.getRecorderManager()
  2955. contentAudioRecorder.onStop(() => {
  2956. contentAudioRecording = false
  2957. if (mapEngine) {
  2958. mapEngine.handleContentCardAudioRecorded()
  2959. }
  2960. })
  2961. }
  2962. const recorder = contentAudioRecorder
  2963. if (!contentAudioRecording) {
  2964. contentAudioRecording = true
  2965. recorder.start({
  2966. duration: 8000,
  2967. format: 'mp3',
  2968. } as any)
  2969. wx.showToast({
  2970. title: '开始录音',
  2971. icon: 'none',
  2972. duration: 800,
  2973. })
  2974. } else {
  2975. recorder.stop()
  2976. }
  2977. }
  2978. },
  2979. handleContentQuizAnswer(event: WechatMiniprogram.BaseEvent) {
  2980. if (!mapEngine) {
  2981. return
  2982. }
  2983. const optionKey = event.currentTarget.dataset.key
  2984. if (typeof optionKey === 'string') {
  2985. mapEngine.handleContentCardQuizAnswer(optionKey)
  2986. }
  2987. },
  2988. handleDismissTransientContentCard() {
  2989. if (mapEngine) {
  2990. mapEngine.closeContentCard()
  2991. }
  2992. },
  2993. handleContentCardTap() {
  2994. if (!mapEngine) {
  2995. return
  2996. }
  2997. if (!this.data.contentCardActions.length) {
  2998. mapEngine.closeContentCard()
  2999. }
  3000. },
  3001. openH5Experience(request: H5ExperienceRequest) {
  3002. wx.navigateTo({
  3003. url: '/pages/experience-webview/experience-webview',
  3004. success: (result) => {
  3005. const eventChannel = result.eventChannel
  3006. eventChannel.on('fallback', (payload: H5ExperienceFallbackPayload) => {
  3007. if (mapEngine) {
  3008. mapEngine.handleH5ExperienceFallback(payload)
  3009. }
  3010. })
  3011. eventChannel.on('close', () => {
  3012. if (mapEngine) {
  3013. mapEngine.handleH5ExperienceClosed()
  3014. }
  3015. })
  3016. eventChannel.on('submitResult', () => {
  3017. if (mapEngine) {
  3018. mapEngine.handleH5ExperienceClosed()
  3019. }
  3020. })
  3021. eventChannel.emit('init', request)
  3022. },
  3023. fail: () => {
  3024. if (mapEngine) {
  3025. mapEngine.handleH5ExperienceFallback(request.fallback)
  3026. }
  3027. },
  3028. })
  3029. },
  3030. handleCloseContentCard() {
  3031. if (mapEngine) {
  3032. mapEngine.closeContentCard()
  3033. }
  3034. },
  3035. handleClosePunchHint() {
  3036. clearPunchHintDismissTimer()
  3037. this.setData({
  3038. showPunchHintBanner: false,
  3039. })
  3040. },
  3041. handlePunchHintTap() {},
  3042. handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
  3043. this.setData({
  3044. hudPanelIndex: event.detail.current || 0,
  3045. })
  3046. },
  3047. handleCycleSideButtons() {
  3048. const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
  3049. this.setData({
  3050. ...buildSideButtonVisibility(nextMode),
  3051. ...buildSideButtonState({
  3052. sideButtonMode: nextMode,
  3053. showGameInfoPanel: this.data.showGameInfoPanel,
  3054. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  3055. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3056. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3057. skipButtonEnabled: this.data.skipButtonEnabled,
  3058. gameSessionStatus: this.data.gameSessionStatus,
  3059. gpsLockEnabled: this.data.gpsLockEnabled,
  3060. gpsLockAvailable: this.data.gpsLockAvailable,
  3061. }),
  3062. })
  3063. },
  3064. handleToggleGpsLock() {
  3065. if (mapEngine) {
  3066. mapEngine.handleToggleGpsLock()
  3067. }
  3068. },
  3069. handleToggleMapRotateMode() {
  3070. if (!mapEngine || this.data.lockAutoRotate) {
  3071. return
  3072. }
  3073. if (this.data.orientationMode === 'heading-up') {
  3074. this.persistAndApplySystemSettings({
  3075. autoRotateEnabled: false,
  3076. })
  3077. return
  3078. }
  3079. this.persistAndApplySystemSettings({
  3080. autoRotateEnabled: true,
  3081. })
  3082. },
  3083. handleToggleDebugPanel() {
  3084. const nextShowDebugPanel = !this.data.showDebugPanel
  3085. if (!nextShowDebugPanel) {
  3086. clearGameInfoPanelSyncTimer()
  3087. }
  3088. if (mapEngine) {
  3089. mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
  3090. }
  3091. this.setData({
  3092. showDebugPanel: nextShowDebugPanel,
  3093. showGameInfoPanel: false,
  3094. showSystemSettingsPanel: false,
  3095. ...buildSideButtonState({
  3096. sideButtonMode: this.data.sideButtonMode,
  3097. showGameInfoPanel: false,
  3098. showSystemSettingsPanel: false,
  3099. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3100. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3101. skipButtonEnabled: this.data.skipButtonEnabled,
  3102. gameSessionStatus: this.data.gameSessionStatus,
  3103. gpsLockEnabled: this.data.gpsLockEnabled,
  3104. gpsLockAvailable: this.data.gpsLockAvailable,
  3105. }),
  3106. })
  3107. },
  3108. handleCloseDebugPanel() {
  3109. if (mapEngine) {
  3110. mapEngine.setDiagnosticUiEnabled(false)
  3111. }
  3112. this.setData({
  3113. showDebugPanel: false,
  3114. ...buildSideButtonState({
  3115. sideButtonMode: this.data.sideButtonMode,
  3116. showGameInfoPanel: this.data.showGameInfoPanel,
  3117. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  3118. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3119. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3120. skipButtonEnabled: this.data.skipButtonEnabled,
  3121. gameSessionStatus: this.data.gameSessionStatus,
  3122. gpsLockEnabled: this.data.gpsLockEnabled,
  3123. gpsLockAvailable: this.data.gpsLockAvailable,
  3124. }),
  3125. })
  3126. },
  3127. applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
  3128. this.data.showCenterScaleRuler = nextEnabled
  3129. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  3130. clearCenterScaleRulerSyncTimer()
  3131. clearCenterScaleRulerUpdateTimer()
  3132. const syncRulerFromEngine = () => {
  3133. if (!mapEngine) {
  3134. return
  3135. }
  3136. const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
  3137. updateCenterScaleRulerInputCache(engineSnapshot)
  3138. const mergedData = {
  3139. ...centerScaleRulerInputCache,
  3140. ...this.data,
  3141. showCenterScaleRuler: nextEnabled,
  3142. centerScaleRulerAnchorMode: nextAnchorMode,
  3143. } as MapPageData
  3144. this.setData({
  3145. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
  3146. showCenterScaleRuler: nextEnabled,
  3147. centerScaleRulerAnchorMode: nextAnchorMode,
  3148. ...buildCenterScaleRulerPatch(mergedData),
  3149. ...buildSideButtonState(mergedData),
  3150. })
  3151. }
  3152. if (!nextEnabled) {
  3153. syncRulerFromEngine()
  3154. return
  3155. }
  3156. this.setData({
  3157. showCenterScaleRuler: true,
  3158. centerScaleRulerAnchorMode: nextAnchorMode,
  3159. ...buildSideButtonState({
  3160. ...this.data,
  3161. showCenterScaleRuler: true,
  3162. centerScaleRulerAnchorMode: nextAnchorMode,
  3163. } as MapPageData),
  3164. })
  3165. this.measureStageAndCanvas(() => {
  3166. syncRulerFromEngine()
  3167. })
  3168. centerScaleRulerSyncTimer = setTimeout(() => {
  3169. centerScaleRulerSyncTimer = 0
  3170. if (!this.data.showCenterScaleRuler) {
  3171. return
  3172. }
  3173. syncRulerFromEngine()
  3174. }, 96) as unknown as number
  3175. },
  3176. handleSetCenterScaleRulerVisibleOn() {
  3177. if (this.data.lockScaleRulerVisible) {
  3178. return
  3179. }
  3180. this.persistAndApplySystemSettings({
  3181. showCenterScaleRuler: true,
  3182. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3183. }, {
  3184. applyCenterScaleRuler: true,
  3185. })
  3186. },
  3187. handleSetCenterScaleRulerVisibleOff() {
  3188. if (this.data.lockScaleRulerVisible) {
  3189. return
  3190. }
  3191. this.persistAndApplySystemSettings({
  3192. showCenterScaleRuler: false,
  3193. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  3194. }, {
  3195. applyCenterScaleRuler: true,
  3196. })
  3197. },
  3198. handleSetCenterScaleRulerAnchorScreenCenter() {
  3199. if (this.data.lockScaleRulerAnchor) {
  3200. return
  3201. }
  3202. this.persistAndApplySystemSettings({
  3203. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3204. centerScaleRulerAnchorMode: 'screen-center',
  3205. }, {
  3206. applyCenterScaleRuler: true,
  3207. })
  3208. },
  3209. handleSetCenterScaleRulerAnchorCompassCenter() {
  3210. if (this.data.lockScaleRulerAnchor) {
  3211. return
  3212. }
  3213. this.persistAndApplySystemSettings({
  3214. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3215. centerScaleRulerAnchorMode: 'compass-center',
  3216. }, {
  3217. applyCenterScaleRuler: true,
  3218. })
  3219. },
  3220. handleToggleCenterScaleRulerAnchor() {
  3221. if (!this.data.showCenterScaleRuler || this.data.lockScaleRulerAnchor) {
  3222. return
  3223. }
  3224. const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
  3225. ? 'compass-center'
  3226. : 'screen-center'
  3227. this.persistAndApplySystemSettings({
  3228. centerScaleRulerAnchorMode: nextAnchorMode,
  3229. showCenterScaleRuler: this.data.showCenterScaleRuler,
  3230. }, {
  3231. applyCenterScaleRuler: true,
  3232. })
  3233. },
  3234. handleDebugPanelTap() {},
  3235. })