map.ts 88 KB

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