import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine' import { loadRemoteMapConfig } from '../../utils/remoteMapConfig' type CompassTickData = { angle: number long: boolean major: boolean } type CompassLabelData = { text: string angle: number rotateBack: number radius: number className: string } type SideButtonMode = 'all' | 'left' | 'right' | 'hidden' type MapPageData = MapEngineViewState & { showDebugPanel: boolean statusBarHeight: number topInsetHeight: number hudPanelIndex: number mockBridgeUrlDraft: string mockHeartRateBridgeUrlDraft: string panelTimerText: string panelMileageText: string panelDistanceValueText: string panelProgressText: string panelSpeedValueText: string compassTicks: CompassTickData[] compassLabels: CompassLabelData[] sideButtonMode: SideButtonMode showLeftButtonGroup: boolean showRightButtonGroups: boolean showBottomDebugButton: boolean } const INTERNAL_BUILD_VERSION = 'map-build-196' const REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/wxmini/test/game.json' let mapEngine: MapEngine | null = null let stageCanvasAttached = false function buildSideButtonVisibility(mode: SideButtonMode) { return { sideButtonMode: mode, showLeftButtonGroup: mode === 'all' || mode === 'left' || mode === 'right', showRightButtonGroups: mode === 'all' || mode === 'right', showBottomDebugButton: mode !== 'hidden', } } function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode { if (currentMode === 'all') { return 'left' } if (currentMode === 'left') { return 'right' } if (currentMode === 'right') { return 'hidden' } return 'left' } function buildCompassTicks(): CompassTickData[] { const ticks: CompassTickData[] = [] for (let angle = 0; angle < 360; angle += 5) { ticks.push({ angle, long: angle % 15 === 0, major: angle % 45 === 0, }) } return ticks } function buildCompassLabels(): CompassLabelData[] { return [ { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' }, { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' }, { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' }, { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' }, { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' }, { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' }, { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' }, { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' }, ] } function getFallbackStageRect(): MapEngineStageRect { const systemInfo = wx.getSystemInfoSync() const width = Math.max(320, systemInfo.windowWidth) const height = Math.max(280, systemInfo.windowHeight) return { width, height, left: 0, top: 0, } } Page({ data: { showDebugPanel: false, statusBarHeight: 0, topInsetHeight: 12, hudPanelIndex: 0, panelTimerText: '00:00:00', panelMileageText: '0m', panelActionTagText: '目标', panelDistanceTagText: '点距', panelDistanceValueText: '--', panelDistanceUnitText: '', panelProgressText: '0/0', gameSessionStatus: 'idle', gameModeText: '顺序赛', locationSourceMode: 'real', locationSourceText: '真实定位', mockBridgeConnected: false, mockBridgeStatusText: '未连接', mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps', mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps', mockCoordText: '--', mockSpeedText: '--', heartRateSourceMode: 'real', heartRateSourceText: '真实心率', mockHeartRateBridgeConnected: false, mockHeartRateBridgeStatusText: '未连接', mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps', mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps', mockHeartRateText: '--', heartRateScanText: '未扫描', heartRateDiscoveredDevices: [], panelSpeedValueText: '0', panelTelemetryTone: 'blue', panelHeartRateZoneNameText: '--', panelHeartRateZoneRangeText: '', heartRateConnected: false, heartRateStatusText: '心率带未连接', heartRateDeviceText: '--', panelHeartRateValueText: '--', panelHeartRateUnitText: '', panelCaloriesValueText: '0', panelCaloriesUnitText: 'kcal', panelAverageSpeedValueText: '0', panelAverageSpeedUnitText: 'km/h', panelAccuracyValueText: '--', panelAccuracyUnitText: '', punchButtonText: '打点', punchButtonEnabled: false, punchHintText: '等待进入检查点范围', punchFeedbackVisible: false, punchFeedbackText: '', punchFeedbackTone: 'neutral', contentCardVisible: false, contentCardTitle: '', contentCardBody: '', punchButtonFxClass: '', punchFeedbackFxClass: '', contentCardFxClass: '', mapPulseVisible: false, mapPulseLeftPx: 0, mapPulseTopPx: 0, mapPulseFxClass: '', stageFxVisible: false, stageFxClass: '', compassTicks: buildCompassTicks(), compassLabels: buildCompassLabels(), ...buildSideButtonVisibility('left'), } as unknown as MapPageData, onLoad() { const systemInfo = wx.getSystemInfoSync() const statusBarHeight = systemInfo.statusBarHeight || 0 const menuButtonRect = wx.getMenuButtonBoundingClientRect() const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, { onData: (patch) => { const nextPatch = patch as Partial const nextData: Partial = { ...nextPatch, } if ( typeof nextPatch.mockBridgeUrlText === 'string' && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText ) { nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText } if ( typeof nextPatch.mockHeartRateBridgeUrlText === 'string' && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText ) { nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText } this.setData(nextData) }, }) this.setData({ ...mapEngine.getInitialData(), showDebugPanel: false, statusBarHeight, topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20), hudPanelIndex: 0, panelTimerText: '00:00:00', panelMileageText: '0m', panelActionTagText: '目标', panelDistanceTagText: '点距', panelDistanceValueText: '--', panelDistanceUnitText: '', panelProgressText: '0/0', gameSessionStatus: 'idle', gameModeText: '顺序赛', locationSourceMode: 'real', locationSourceText: '真实定位', mockBridgeConnected: false, mockBridgeStatusText: '未连接', mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps', mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps', mockCoordText: '--', mockSpeedText: '--', heartRateSourceMode: 'real', heartRateSourceText: '真实心率', mockHeartRateBridgeConnected: false, mockHeartRateBridgeStatusText: '未连接', mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps', mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps', mockHeartRateText: '--', panelSpeedValueText: '0', panelTelemetryTone: 'blue', panelHeartRateZoneNameText: '--', panelHeartRateZoneRangeText: '', heartRateConnected: false, heartRateStatusText: '心率带未连接', heartRateDeviceText: '--', panelHeartRateValueText: '--', panelHeartRateUnitText: '', panelCaloriesValueText: '0', panelCaloriesUnitText: 'kcal', panelAverageSpeedValueText: '0', panelAverageSpeedUnitText: 'km/h', panelAccuracyValueText: '--', panelAccuracyUnitText: '', punchButtonText: '打点', punchButtonEnabled: false, punchHintText: '等待进入检查点范围', punchFeedbackVisible: false, punchFeedbackText: '', punchFeedbackTone: 'neutral', contentCardVisible: false, contentCardTitle: '', contentCardBody: '', punchButtonFxClass: '', punchFeedbackFxClass: '', contentCardFxClass: '', mapPulseVisible: false, mapPulseLeftPx: 0, mapPulseTopPx: 0, mapPulseFxClass: '', stageFxVisible: false, stageFxClass: '', compassTicks: buildCompassTicks(), compassLabels: buildCompassLabels(), ...buildSideButtonVisibility('left'), }) }, onReady() { stageCanvasAttached = false this.measureStageAndCanvas() this.loadMapConfigFromRemote() }, onShow() { if (mapEngine) { mapEngine.handleAppShow() } }, onHide() { if (mapEngine) { mapEngine.handleAppHide() } }, onUnload() { if (mapEngine) { mapEngine.destroy() mapEngine = null } stageCanvasAttached = false }, loadMapConfigFromRemote() { const currentEngine = mapEngine if (!currentEngine) { return } loadRemoteMapConfig(REMOTE_GAME_CONFIG_URL) .then((config) => { if (mapEngine !== currentEngine) { return } currentEngine.applyRemoteMapConfig(config) }) .catch((error) => { if (mapEngine !== currentEngine) { return } const errorMessage = error && error.message ? error.message : '未知错误' this.setData({ configStatusText: `载入失败: ${errorMessage}`, statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`, }) }) }, measureStageAndCanvas() { const page = this const applyStage = (rawRect?: Partial) => { const fallbackRect = getFallbackStageRect() const rect: MapEngineStageRect = { width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width, height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height, left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left, top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top, } const currentEngine = mapEngine if (!currentEngine) { return } currentEngine.setStage(rect) if (stageCanvasAttached) { return } const canvasQuery = wx.createSelectorQuery().in(page) canvasQuery.select('#mapCanvas').fields({ node: true, size: true }) canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true }) canvasQuery.exec((canvasRes) => { const canvasRef = canvasRes[0] as any const labelCanvasRef = canvasRes[1] as any if (!canvasRef || !canvasRef.node) { page.setData({ statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`, }) return } const dpr = wx.getSystemInfoSync().pixelRatio || 1 try { currentEngine.attachCanvas( canvasRef.node, rect.width, rect.height, dpr, labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined, ) stageCanvasAttached = true } catch (error) { page.setData({ statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`, }) } }) } const query = wx.createSelectorQuery().in(page) query.select('.map-stage').boundingClientRect() query.exec((res) => { const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined applyStage(rect) }) }, handleTouchStart(event: WechatMiniprogram.TouchEvent) { if (mapEngine) { mapEngine.handleTouchStart(event) } }, handleTouchMove(event: WechatMiniprogram.TouchEvent) { if (mapEngine) { mapEngine.handleTouchMove(event) } }, handleTouchEnd(event: WechatMiniprogram.TouchEvent) { if (mapEngine) { mapEngine.handleTouchEnd(event) } }, handleTouchCancel() { if (mapEngine) { mapEngine.handleTouchCancel() } }, handleRecenter() { if (mapEngine) { mapEngine.handleRecenter() } }, handleRotateStep() { if (mapEngine) { mapEngine.handleRotateStep() } }, handleRotationReset() { if (mapEngine) { mapEngine.handleRotationReset() } }, handleSetManualMode() { if (mapEngine) { mapEngine.handleSetManualMode() } }, handleSetNorthUpMode() { if (mapEngine) { mapEngine.handleSetNorthUpMode() } }, handleSetHeadingUpMode() { if (mapEngine) { mapEngine.handleSetHeadingUpMode() } }, handleCycleNorthReferenceMode() { if (mapEngine) { mapEngine.handleCycleNorthReferenceMode() } }, handleAutoRotateCalibrate() { if (mapEngine) { mapEngine.handleAutoRotateCalibrate() } }, handleToggleGpsTracking() { if (mapEngine) { mapEngine.handleToggleGpsTracking() } }, handleSetRealLocationMode() { if (mapEngine) { mapEngine.handleSetRealLocationMode() } }, handleSetMockLocationMode() { if (mapEngine) { mapEngine.handleSetMockLocationMode() } }, handleConnectMockLocationBridge() { if (mapEngine) { mapEngine.handleConnectMockLocationBridge() } }, handleMockBridgeUrlInput(event: WechatMiniprogram.Input) { this.setData({ mockBridgeUrlDraft: event.detail.value, }) }, handleSaveMockBridgeUrl() { if (mapEngine) { mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft) } }, handleDisconnectMockLocationBridge() { if (mapEngine) { mapEngine.handleDisconnectMockLocationBridge() } }, handleSetRealHeartRateMode() { if (mapEngine) { mapEngine.handleSetRealHeartRateMode() } }, handleSetMockHeartRateMode() { if (mapEngine) { mapEngine.handleSetMockHeartRateMode() } }, handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) { this.setData({ mockHeartRateBridgeUrlDraft: event.detail.value, }) }, handleSaveMockHeartRateBridgeUrl() { if (mapEngine) { mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft) } }, handleConnectMockHeartRateBridge() { if (mapEngine) { mapEngine.handleConnectMockHeartRateBridge() } }, handleDisconnectMockHeartRateBridge() { if (mapEngine) { mapEngine.handleDisconnectMockHeartRateBridge() } }, handleConnectHeartRate() { if (mapEngine) { mapEngine.handleConnectHeartRate() } }, handleDisconnectHeartRate() { if (mapEngine) { mapEngine.handleDisconnectHeartRate() } }, handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) { if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) { mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId) } }, handleClearPreferredHeartRateDevice() { if (mapEngine) { mapEngine.handleClearPreferredHeartRateDevice() } }, handleDebugHeartRateBlue() { if (mapEngine) { mapEngine.handleDebugHeartRateTone('blue') } }, handleDebugHeartRatePurple() { if (mapEngine) { mapEngine.handleDebugHeartRateTone('purple') } }, handleDebugHeartRateGreen() { if (mapEngine) { mapEngine.handleDebugHeartRateTone('green') } }, handleDebugHeartRateYellow() { if (mapEngine) { mapEngine.handleDebugHeartRateTone('yellow') } }, handleDebugHeartRateOrange() { if (mapEngine) { mapEngine.handleDebugHeartRateTone('orange') } }, handleDebugHeartRateRed() { if (mapEngine) { mapEngine.handleDebugHeartRateTone('red') } }, handleClearDebugHeartRate() { if (mapEngine) { mapEngine.handleClearDebugHeartRate() } }, handleToggleOsmReference() { if (mapEngine) { mapEngine.handleToggleOsmReference() } }, handleStartGame() { if (mapEngine) { mapEngine.handleStartGame() } }, handleSetClassicMode() { if (mapEngine) { mapEngine.handleSetGameMode('classic-sequential') } }, handleSetScoreOMode() { if (mapEngine) { mapEngine.handleSetGameMode('score-o') } }, handleClearMapTestArtifacts() { if (mapEngine) { mapEngine.handleClearMapTestArtifacts() } }, handleOverlayTouch() {}, handlePunchAction() { if (!this.data.punchButtonEnabled) { return } if (mapEngine) { mapEngine.handlePunchAction() } }, handleCloseContentCard() { if (mapEngine) { mapEngine.closeContentCard() } }, handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) { this.setData({ hudPanelIndex: event.detail.current || 0, }) }, handleCycleSideButtons() { this.setData(buildSideButtonVisibility(getNextSideButtonMode(this.data.sideButtonMode))) }, handleToggleMapRotateMode() { if (!mapEngine) { return } if (this.data.orientationMode === 'heading-up') { mapEngine.handleSetManualMode() return } mapEngine.handleSetHeadingUpMode() }, handleToggleDebugPanel() { this.setData({ showDebugPanel: !this.data.showDebugPanel, }) }, handleCloseDebugPanel() { this.setData({ showDebugPanel: false, }) }, handleDebugPanelTap() {}, })