map.ts 90 KB

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