map.ts 82 KB

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