From a19342d61f4f49ed2596933b2857e799fde610f7 Mon Sep 17 00:00:00 2001 From: zhangyan Date: Wed, 25 Mar 2026 14:56:28 +0800 Subject: [PATCH] Improve map lock and smart heading behavior --- miniprogram/engine/map/mapEngine.ts | 256 ++++++++++++++++-- miniprogram/game/audio/soundDirector.ts | 6 + miniprogram/game/core/gameResult.ts | 1 + miniprogram/game/feedback/hapticsDirector.ts | 5 + miniprogram/game/feedback/uiEffectDirector.ts | 4 + miniprogram/pages/map/map.ts | 50 +++- miniprogram/pages/map/map.wxml | 3 +- miniprogram/pages/map/map.wxss | 37 --- 8 files changed, 300 insertions(+), 62 deletions(-) diff --git a/miniprogram/engine/map/mapEngine.ts b/miniprogram/engine/map/mapEngine.ts index 6ac0af3..8c27064 100644 --- a/miniprogram/engine/map/mapEngine.ts +++ b/miniprogram/engine/map/mapEngine.ts @@ -54,6 +54,10 @@ const AUTO_ROTATE_DEADZONE_DEG = 4 const AUTO_ROTATE_MAX_STEP_DEG = 0.75 const AUTO_ROTATE_HEADING_SMOOTHING = 0.32 const COMPASS_NEEDLE_SMOOTHING = 0.12 +const SMART_HEADING_BLEND_START_SPEED_KMH = 1.2 +const SMART_HEADING_MOVEMENT_SPEED_KMH = 3.0 +const SMART_HEADING_MIN_DISTANCE_METERS = 8 +const SMART_HEADING_MAX_ACCURACY_METERS = 25 const GPS_TRACK_MAX_POINTS = 200 const GPS_TRACK_MIN_STEP_METERS = 3 const MAP_TAP_MOVE_THRESHOLD_PX = 14 @@ -64,7 +68,8 @@ type TouchPoint = WechatMiniprogram.TouchDetail type GestureMode = 'idle' | 'pan' | 'pinch' type RotationMode = 'manual' | 'auto' type OrientationMode = 'manual' | 'north-up' | 'heading-up' -type AutoRotateSourceMode = 'sensor' | 'course' | 'fusion' +type AutoRotateSourceMode = 'sensor' | 'course' | 'fusion' | 'smart' +type SmartHeadingSource = 'sensor' | 'blended' | 'movement' type NorthReferenceMode = 'magnetic' | 'true' const DEFAULT_NORTH_REFERENCE_MODE: NorthReferenceMode = 'magnetic' @@ -122,6 +127,8 @@ export interface MapEngineViewState { statusText: string gpsTracking: boolean gpsTrackingText: string + gpsLockEnabled: boolean + gpsLockAvailable: boolean locationSourceMode: 'real' | 'mock' locationSourceText: string mockBridgeConnected: boolean @@ -244,6 +251,8 @@ const VIEW_SYNC_KEYS: Array = [ 'statusText', 'gpsTracking', 'gpsTrackingText', + 'gpsLockEnabled', + 'gpsLockAvailable', 'locationSourceMode', 'locationSourceText', 'mockBridgeConnected', @@ -406,6 +415,10 @@ function formatRotationToggleText(mode: OrientationMode): string { } function formatAutoRotateSourceText(mode: AutoRotateSourceMode, hasCourseHeading: boolean): string { + if (mode === 'smart') { + return 'Smart / 手机朝向' + } + if (mode === 'sensor') { return 'Sensor Only' } @@ -417,6 +430,18 @@ function formatAutoRotateSourceText(mode: AutoRotateSourceMode, hasCourseHeading return hasCourseHeading ? 'Sensor + Course' : 'Sensor Only' } +function formatSmartHeadingSourceText(source: SmartHeadingSource): string { + if (source === 'movement') { + return 'Smart / 前进方向' + } + + if (source === 'blended') { + return 'Smart / 融合' + } + + return 'Smart / 手机朝向' +} + function formatAutoRotateCalibrationText(pending: boolean, offsetDeg: number | null): string { if (pending) { return 'Pending' @@ -539,6 +564,18 @@ function getApproxDistanceMeters(a: LonLatPoint, b: LonLatPoint): number { return Math.sqrt(dx * dx + dy * dy) } +function resolveSmartHeadingSource(speedKmh: number | null, movementReliable: boolean): SmartHeadingSource { + if (!movementReliable || speedKmh === null || !Number.isFinite(speedKmh) || speedKmh <= SMART_HEADING_BLEND_START_SPEED_KMH) { + return 'sensor' + } + + if (speedKmh >= SMART_HEADING_MOVEMENT_SPEED_KMH) { + return 'movement' + } + + return 'blended' +} + function getInitialBearingDeg(from: LonLatPoint, to: LonLatPoint): number { const fromLatRad = from.lat * Math.PI / 180 const toLatRad = to.lat * Math.PI / 180 @@ -601,6 +638,7 @@ export class MapEngine { currentGpsPoint: LonLatPoint | null currentGpsTrack: LonLatPoint[] currentGpsAccuracyMeters: number | null + currentGpsInsideMap: boolean courseData: OrienteeringCourseData | null courseOverlayVisible: boolean cpRadiusMeters: number @@ -626,6 +664,7 @@ export class MapEngine { stageFxTimer: number sessionTimerInterval: number hasGpsCenteredOnce: boolean + gpsLockEnabled: boolean constructor(buildVersion: string, callbacks: MapEngineCallbacks) { this.buildVersion = buildVersion @@ -767,6 +806,7 @@ export class MapEngine { this.currentGpsPoint = null this.currentGpsTrack = [] this.currentGpsAccuracyMeters = null + this.currentGpsInsideMap = false this.courseData = null this.courseOverlayVisible = false this.cpRadiusMeters = 5 @@ -787,6 +827,7 @@ export class MapEngine { this.skipRadiusMeters = 30 this.skipRequiresConfirm = true this.autoFinishOnLastControl = true + this.gpsLockEnabled = false this.punchFeedbackTimer = 0 this.contentCardTimer = 0 this.mapPulseTimer = 0 @@ -812,7 +853,7 @@ export class MapEngine { sensorHeadingText: '--', compassDeclinationText: formatCompassDeclinationText(DEFAULT_NORTH_REFERENCE_MODE), northReferenceButtonText: formatNorthReferenceButtonText(DEFAULT_NORTH_REFERENCE_MODE), - autoRotateSourceText: formatAutoRotateSourceText('sensor', false), + autoRotateSourceText: formatAutoRotateSourceText('smart', false), autoRotateCalibrationText: formatAutoRotateCalibrationText(false, getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)), northReferenceText: formatNorthReferenceText(DEFAULT_NORTH_REFERENCE_MODE), compassNeedleDeg: 0, @@ -839,6 +880,8 @@ export class MapEngine { statusText: `单 WebGL 管线已就绪,等待传感器接入 (${this.buildVersion})`, gpsTracking: false, gpsTrackingText: '持续定位待启动', + gpsLockEnabled: false, + gpsLockAvailable: false, locationSourceMode: 'real', locationSourceText: '真实定位', mockBridgeConnected: false, @@ -932,7 +975,7 @@ export class MapEngine { this.autoRotateHeadingDeg = null this.courseHeadingDeg = null this.targetAutoRotationDeg = null - this.autoRotateSourceMode = 'sensor' + this.autoRotateSourceMode = 'smart' this.autoRotateCalibrationOffsetDeg = getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE) this.autoRotateCalibrationPending = false } @@ -1052,6 +1095,7 @@ export class MapEngine { this.currentGpsPoint = null this.currentGpsTrack = [] this.currentGpsAccuracyMeters = null + this.currentGpsInsideMap = false this.courseOverlayVisible = false this.setCourseHeading(null) } @@ -1104,6 +1148,8 @@ export class MapEngine { const debugState = this.locationController.getDebugState() return { gpsTracking: debugState.listening, + gpsLockEnabled: this.gpsLockEnabled, + gpsLockAvailable: !!this.currentGpsPoint && this.currentGpsInsideMap, locationSourceMode: debugState.sourceMode, locationSourceText: debugState.sourceModeText, mockBridgeConnected: debugState.mockBridgeConnected, @@ -1224,6 +1270,8 @@ export class MapEngine { punchButtonEnabled: this.gamePresentation.hud.punchButtonEnabled, skipButtonEnabled: this.isSkipAvailable(), punchHintText: this.gamePresentation.hud.punchHintText, + gpsLockEnabled: this.gpsLockEnabled, + gpsLockAvailable: !!this.currentGpsPoint && this.currentGpsInsideMap, } if (statusText) { @@ -1510,15 +1558,21 @@ export class MapEngine { } const startedAt = Date.now() - let gameResult = this.gameRuntime.startSession(startedAt) + const startResult = this.gameRuntime.startSession(startedAt) + let gameResult = startResult if (this.currentGpsPoint) { - gameResult = this.gameRuntime.dispatch({ + const gpsResult = this.gameRuntime.dispatch({ type: 'gps_updated', at: Date.now(), lon: this.currentGpsPoint.lon, lat: this.currentGpsPoint.lat, accuracyMeters: this.currentGpsAccuracyMeters, }) + gameResult = { + nextState: gpsResult.nextState, + presentation: gpsResult.presentation, + effects: [...startResult.effects, ...gpsResult.effects], + } } this.courseOverlayVisible = true @@ -1534,6 +1588,7 @@ export class MapEngine { if (!this.courseData) { this.clearGameRuntime() this.resetTransientGameUiState() + this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }]) this.setState({ ...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`), }, true) @@ -1543,6 +1598,7 @@ export class MapEngine { this.loadGameDefinitionFromCourse() this.resetTransientGameUiState() + this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }]) this.setState({ ...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`), }, true) @@ -1572,8 +1628,14 @@ export class MapEngine { const gpsTileX = Math.floor(gpsWorldPoint.x) const gpsTileY = Math.floor(gpsWorldPoint.y) const gpsInsideMap = isTileWithinBounds(this.tileBoundsByZoom, this.state.zoom, gpsTileX, gpsTileY) + this.currentGpsInsideMap = gpsInsideMap let gameStatusText: string | null = null + if (!gpsInsideMap && this.gpsLockEnabled) { + this.gpsLockEnabled = false + gameStatusText = `GPS已超出地图范围,锁定已关闭 (${this.buildVersion})` + } + if (this.courseData) { const eventAt = Date.now() const gameResult = this.gameRuntime.dispatch({ @@ -1594,18 +1656,23 @@ export class MapEngine { gameStatusText = this.resolveAppliedGameStatusText(gameResult) } - if (gpsInsideMap && !this.hasGpsCenteredOnce) { + if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) { + this.scheduleAutoRotate() + } + + if (gpsInsideMap && (this.gpsLockEnabled || !this.hasGpsCenteredOnce)) { this.hasGpsCenteredOnce = true + const lockedViewport = this.resolveViewportForExactCenter(gpsWorldPoint.x, gpsWorldPoint.y) this.commitViewport({ - centerTileX: gpsWorldPoint.x, - centerTileY: gpsWorldPoint.y, - tileTranslateX: 0, - tileTranslateY: 0, + ...lockedViewport, gpsTracking: true, gpsTrackingText: '持续定位进行中', gpsCoordText: formatGpsCoordText(nextPoint, accuracyMeters), + autoRotateSourceText: this.getAutoRotateSourceText(), + gpsLockEnabled: this.gpsLockEnabled, + gpsLockAvailable: true, ...this.getGameViewPatch(), - }, gameStatusText || `GPS定位成功,已定位到当前位置 (${this.buildVersion})`, true) + }, gameStatusText || (this.gpsLockEnabled ? `GPS锁定跟随中 (${this.buildVersion})` : `GPS定位成功,已定位到当前位置 (${this.buildVersion})`), true) return } @@ -1613,11 +1680,62 @@ export class MapEngine { gpsTracking: true, gpsTrackingText: gpsInsideMap ? '持续定位进行中' : 'GPS不在当前地图范围内', gpsCoordText: formatGpsCoordText(nextPoint, accuracyMeters), + autoRotateSourceText: this.getAutoRotateSourceText(), + gpsLockEnabled: this.gpsLockEnabled, + gpsLockAvailable: gpsInsideMap, ...this.getGameViewPatch(gameStatusText || (gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`)), }, true) this.syncRenderer() } + handleToggleGpsLock(): void { + if (!this.currentGpsPoint || !this.currentGpsInsideMap) { + this.setState({ + gpsLockEnabled: false, + gpsLockAvailable: false, + statusText: this.currentGpsPoint + ? `当前位置不在地图范围内,无法锁定 (${this.buildVersion})` + : `当前还没有可锁定的GPS位置 (${this.buildVersion})`, + }, true) + return + } + + const nextEnabled = !this.gpsLockEnabled + this.gpsLockEnabled = nextEnabled + + if (nextEnabled) { + const gpsWorldPoint = lonLatToWorldTile(this.currentGpsPoint, this.state.zoom) + const gpsTileX = Math.floor(gpsWorldPoint.x) + const gpsTileY = Math.floor(gpsWorldPoint.y) + const gpsInsideMap = isTileWithinBounds(this.tileBoundsByZoom, this.state.zoom, gpsTileX, gpsTileY) + if (gpsInsideMap) { + this.hasGpsCenteredOnce = true + const lockedViewport = this.resolveViewportForExactCenter(gpsWorldPoint.x, gpsWorldPoint.y) + this.commitViewport({ + ...lockedViewport, + gpsLockEnabled: true, + gpsLockAvailable: true, + }, `GPS已锁定在屏幕中央 (${this.buildVersion})`, true) + return + } + + this.setState({ + gpsLockEnabled: true, + gpsLockAvailable: true, + statusText: `GPS锁定已开启,等待进入地图范围 (${this.buildVersion})`, + }, true) + this.syncRenderer() + return + } + + this.setState({ + gpsLockEnabled: false, + gpsLockAvailable: true, + statusText: `GPS锁定已关闭 (${this.buildVersion})`, + }, true) + this.syncRenderer() + } + handleToggleOsmReference(): void { const nextEnabled = !this.state.osmReferenceEnabled this.setState({ @@ -1906,13 +2024,17 @@ export class MapEngine { this.panVelocityY = 0 if (event.touches.length >= 2) { - const origin = this.getStagePoint(event.touches) + const origin = this.gpsLockEnabled + ? { x: this.state.stageWidth / 2, y: this.state.stageHeight / 2 } + : this.getStagePoint(event.touches) this.gestureMode = 'pinch' this.pinchStartDistance = this.getTouchDistance(event.touches) this.pinchStartScale = this.previewScale || 1 this.pinchStartAngle = this.getTouchAngle(event.touches) this.pinchStartRotationDeg = this.state.rotationDeg - const anchorWorld = screenToWorld(this.getCameraState(), origin, true) + const anchorWorld = this.gpsLockEnabled && this.currentGpsPoint + ? lonLatToWorldTile(this.currentGpsPoint, this.state.zoom) + : screenToWorld(this.getCameraState(), origin, true) this.pinchAnchorWorldX = anchorWorld.x this.pinchAnchorWorldY = anchorWorld.y this.setPreviewState(this.pinchStartScale, origin.x, origin.y) @@ -1936,13 +2058,17 @@ export class MapEngine { if (event.touches.length >= 2) { const distance = this.getTouchDistance(event.touches) const angle = this.getTouchAngle(event.touches) - const origin = this.getStagePoint(event.touches) + const origin = this.gpsLockEnabled + ? { x: this.state.stageWidth / 2, y: this.state.stageHeight / 2 } + : this.getStagePoint(event.touches) if (!this.pinchStartDistance) { this.pinchStartDistance = distance this.pinchStartScale = this.previewScale || 1 this.pinchStartAngle = angle this.pinchStartRotationDeg = this.state.rotationDeg - const anchorWorld = screenToWorld(this.getCameraState(), origin, true) + const anchorWorld = this.gpsLockEnabled && this.currentGpsPoint + ? lonLatToWorldTile(this.currentGpsPoint, this.state.zoom) + : screenToWorld(this.getCameraState(), origin, true) this.pinchAnchorWorldX = anchorWorld.x this.pinchAnchorWorldY = anchorWorld.y } @@ -1992,6 +2118,12 @@ export class MapEngine { this.panLastY = touch.pageY this.panLastTimestamp = nextTimestamp + if (this.gpsLockEnabled) { + this.panVelocityX = 0 + this.panVelocityY = 0 + return + } + this.normalizeTranslate( this.state.tileTranslateX + deltaX, this.state.tileTranslateY + deltaY, @@ -2011,8 +2143,8 @@ export class MapEngine { if (this.gestureMode === 'pinch' && event.touches.length < 2) { const gestureScale = this.previewScale || 1 const zoomDelta = Math.round(Math.log2(gestureScale)) - const originX = this.previewOriginX || this.state.stageWidth / 2 - const originY = this.previewOriginY || this.state.stageHeight / 2 + const originX = this.gpsLockEnabled ? this.state.stageWidth / 2 : (this.previewOriginX || this.state.stageWidth / 2) + const originY = this.gpsLockEnabled ? this.state.stageHeight / 2 : (this.previewOriginY || this.state.stageHeight / 2) if (zoomDelta) { const residualScale = gestureScale / Math.pow(2, zoomDelta) @@ -2350,6 +2482,7 @@ export class MapEngine { rotationToggleText: formatRotationToggleText('heading-up'), orientationMode: 'heading-up', orientationModeText: formatOrientationModeText('heading-up'), + autoRotateSourceText: this.getAutoRotateSourceText(), autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg), northReferenceText: formatNorthReferenceText(this.northReferenceMode), statusText: `正在启用朝向朝上模式 (${this.buildVersion})`, @@ -2376,7 +2509,7 @@ export class MapEngine { sensorHeadingText: formatHeadingText(compassHeadingDeg), compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode), northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode), - autoRotateSourceText: formatAutoRotateSourceText(this.autoRotateSourceMode, this.courseHeadingDeg !== null), + autoRotateSourceText: this.getAutoRotateSourceText(), compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg), northReferenceText: formatNorthReferenceText(this.northReferenceMode), }) @@ -2454,7 +2587,7 @@ export class MapEngine { setCourseHeading(headingDeg: number | null): void { this.courseHeadingDeg = headingDeg === null ? null : normalizeRotationDeg(headingDeg) this.setState({ - autoRotateSourceText: formatAutoRotateSourceText(this.autoRotateSourceMode, this.courseHeadingDeg !== null), + autoRotateSourceText: this.getAutoRotateSourceText(), }) if (this.refreshAutoRotateTarget()) { @@ -2462,7 +2595,72 @@ export class MapEngine { } } + getMovementHeadingDeg(): number | null { + if (!this.currentGpsInsideMap) { + return null + } + + if (this.currentGpsAccuracyMeters !== null && this.currentGpsAccuracyMeters > SMART_HEADING_MAX_ACCURACY_METERS) { + return null + } + + if (this.currentGpsTrack.length < 2) { + return null + } + + const lastPoint = this.currentGpsTrack[this.currentGpsTrack.length - 1] + let accumulatedDistanceMeters = 0 + + for (let index = this.currentGpsTrack.length - 2; index >= 0; index -= 1) { + const nextPoint = this.currentGpsTrack[index + 1] + const point = this.currentGpsTrack[index] + accumulatedDistanceMeters += getApproxDistanceMeters(point, nextPoint) + + if (accumulatedDistanceMeters >= SMART_HEADING_MIN_DISTANCE_METERS) { + return getInitialBearingDeg(point, lastPoint) + } + } + + return null + } + + getSmartAutoRotateHeadingDeg(): number | null { + const sensorHeadingDeg = this.smoothedSensorHeadingDeg === null + ? null + : getMapReferenceHeadingDegFromSensor(this.northReferenceMode, this.smoothedSensorHeadingDeg) + const movementHeadingDeg = this.getMovementHeadingDeg() + const speedKmh = this.telemetryRuntime.state.currentSpeedKmh + const smartSource = resolveSmartHeadingSource(speedKmh, movementHeadingDeg !== null) + + if (smartSource === 'movement') { + return movementHeadingDeg === null ? sensorHeadingDeg : movementHeadingDeg + } + + if (smartSource === 'blended' && sensorHeadingDeg !== null && movementHeadingDeg !== null && speedKmh !== null) { + const blend = Math.max(0, Math.min(1, (speedKmh - SMART_HEADING_BLEND_START_SPEED_KMH) / (SMART_HEADING_MOVEMENT_SPEED_KMH - SMART_HEADING_BLEND_START_SPEED_KMH))) + return interpolateAngleDeg(sensorHeadingDeg, movementHeadingDeg, blend) + } + + return sensorHeadingDeg === null ? movementHeadingDeg : sensorHeadingDeg + } + + getAutoRotateSourceText(): string { + if (this.autoRotateSourceMode !== 'smart') { + return formatAutoRotateSourceText(this.autoRotateSourceMode, this.courseHeadingDeg !== null) + } + + const smartSource = resolveSmartHeadingSource( + this.telemetryRuntime.state.currentSpeedKmh, + this.getMovementHeadingDeg() !== null, + ) + return formatSmartHeadingSourceText(smartSource) + } + resolveAutoRotateInputHeadingDeg(): number | null { + if (this.autoRotateSourceMode === 'smart') { + return this.getSmartAutoRotateHeadingDeg() + } + const sensorHeadingDeg = this.smoothedSensorHeadingDeg === null ? null : getMapReferenceHeadingDegFromSensor(this.northReferenceMode, this.smoothedSensorHeadingDeg) @@ -2976,6 +3174,26 @@ export class MapEngine { return } + if (this.gpsLockEnabled && this.currentGpsPoint) { + const nextGpsWorldPoint = lonLatToWorldTile(this.currentGpsPoint, nextZoom) + const resolvedViewport = this.resolveViewportForExactCenter(nextGpsWorldPoint.x, nextGpsWorldPoint.y) + this.commitViewport( + { + zoom: nextZoom, + ...resolvedViewport, + }, + `缩放级别调整到 ${nextZoom}`, + true, + () => { + this.setPreviewState(residualScale, this.state.stageWidth / 2, this.state.stageHeight / 2) + this.syncRenderer() + this.compassController.start() + this.animatePreviewToRest() + }, + ) + return + } + if (!this.state.stageWidth || !this.state.stageHeight) { this.commitViewport( { diff --git a/miniprogram/game/audio/soundDirector.ts b/miniprogram/game/audio/soundDirector.ts index 6af7a6c..d98c6de 100644 --- a/miniprogram/game/audio/soundDirector.ts +++ b/miniprogram/game/audio/soundDirector.ts @@ -73,6 +73,12 @@ export class SoundDirector { continue } + if (effect.type === 'session_cancelled') { + this.stopGuidanceLoop() + this.play('control_completed:finish') + continue + } + if (effect.type === 'punch_feedback' && effect.tone === 'warning') { this.play('punch_feedback:warning') continue diff --git a/miniprogram/game/core/gameResult.ts b/miniprogram/game/core/gameResult.ts index a2e1ab9..970ed21 100644 --- a/miniprogram/game/core/gameResult.ts +++ b/miniprogram/game/core/gameResult.ts @@ -3,6 +3,7 @@ import { type GamePresentationState } from '../presentation/presentationState' export type GameEffect = | { type: 'session_started' } + | { type: 'session_cancelled' } | { type: 'punch_feedback'; text: string; tone: 'neutral' | 'success' | 'warning' } | { type: 'control_completed'; controlId: string; controlKind: 'start' | 'control' | 'finish'; sequence: number | null; label: string; displayTitle: string; displayBody: string } | { type: 'guidance_state_changed'; guidanceState: GuidanceState; controlId: string | null } diff --git a/miniprogram/game/feedback/hapticsDirector.ts b/miniprogram/game/feedback/hapticsDirector.ts index 75e2f40..16f777a 100644 --- a/miniprogram/game/feedback/hapticsDirector.ts +++ b/miniprogram/game/feedback/hapticsDirector.ts @@ -51,6 +51,11 @@ export class HapticsDirector { continue } + if (effect.type === 'session_cancelled') { + this.trigger('session_finished') + continue + } + if (effect.type === 'punch_feedback' && effect.tone === 'warning') { this.trigger('punch_feedback:warning') continue diff --git a/miniprogram/game/feedback/uiEffectDirector.ts b/miniprogram/game/feedback/uiEffectDirector.ts index 73b5e30..00f5e19 100644 --- a/miniprogram/game/feedback/uiEffectDirector.ts +++ b/miniprogram/game/feedback/uiEffectDirector.ts @@ -189,6 +189,10 @@ export class UiEffectDirector { if (effect.type === 'session_finished') { this.clearPunchButtonMotion() } + + if (effect.type === 'session_cancelled') { + this.clearPunchButtonMotion() + } } } } diff --git a/miniprogram/pages/map/map.ts b/miniprogram/pages/map/map.ts index fb06cf8..708c37b 100644 --- a/miniprogram/pages/map/map.ts +++ b/miniprogram/pages/map/map.ts @@ -42,6 +42,7 @@ type MapPageData = MapEngineViewState & { compassLabels: CompassLabelData[] sideButtonMode: SideButtonMode sideToggleIconSrc: string + sideButton2Class: string sideButton4Class: string sideButton11Class: string sideButton16Class: string @@ -49,7 +50,7 @@ type MapPageData = MapEngineViewState & { showRightButtonGroups: boolean showBottomDebugButton: boolean } -const INTERNAL_BUILD_VERSION = 'map-build-207' +const INTERNAL_BUILD_VERSION = 'map-build-213' const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json' const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json' let mapEngine: MapEngine | null = null @@ -131,13 +132,19 @@ function getSideActionButtonClass(state: SideActionButtonState): string { return 'map-side-button map-side-button--default' } -function buildSideButtonState(data: Pick) { +function buildSideButtonState(data: Pick) { + const sideButton2State: SideActionButtonState = !data.gpsLockAvailable + ? 'muted' + : data.gpsLockEnabled + ? 'active' + : 'default' const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'idle' ? 'default' : 'active' const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default' const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted' return { sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode), + sideButton2Class: getSideActionButtonClass(sideButton2State), sideButton4Class: getSideActionButtonClass(sideButton4State), sideButton11Class: getSideActionButtonClass(sideButton11State), sideButton16Class: getSideActionButtonClass(sideButton16State), @@ -180,6 +187,8 @@ Page({ panelProgressText: '0/0', gameSessionStatus: 'idle', gameModeText: '顺序赛', + gpsLockEnabled: false, + gpsLockAvailable: false, locationSourceMode: 'real', locationSourceText: '真实定位', mockBridgeConnected: false, @@ -239,6 +248,8 @@ Page({ showGameInfoPanel: false, skipButtonEnabled: false, gameSessionStatus: 'idle', + gpsLockEnabled: false, + gpsLockAvailable: false, }), } as unknown as MapPageData, @@ -306,6 +317,8 @@ Page({ panelProgressText: '0/0', gameSessionStatus: 'idle', gameModeText: '顺序赛', + gpsLockEnabled: false, + gpsLockAvailable: false, locationSourceMode: 'real', locationSourceText: '真实定位', mockBridgeConnected: false, @@ -363,6 +376,8 @@ Page({ showGameInfoPanel: false, skipButtonEnabled: false, gameSessionStatus: 'idle', + gpsLockEnabled: false, + gpsLockAvailable: false, }), }) }, @@ -723,9 +738,21 @@ Page({ }, handleForceExitGame() { - if (mapEngine) { - mapEngine.handleForceExitGame() + if (!mapEngine || this.data.gameSessionStatus === 'idle') { + return } + + wx.showModal({ + title: '确认退出', + content: '确认强制结束当前对局并返回开始前状态?', + confirmText: '确认退出', + cancelText: '取消', + success: (result) => { + if (result.confirm && mapEngine) { + mapEngine.handleForceExitGame() + } + }, + }) }, handleSkipAction() { @@ -781,6 +808,8 @@ Page({ showGameInfoPanel: true, skipButtonEnabled: this.data.skipButtonEnabled, gameSessionStatus: this.data.gameSessionStatus, + gpsLockEnabled: this.data.gpsLockEnabled, + gpsLockAvailable: this.data.gpsLockAvailable, }), }) }, @@ -793,6 +822,8 @@ Page({ showGameInfoPanel: false, skipButtonEnabled: this.data.skipButtonEnabled, gameSessionStatus: this.data.gameSessionStatus, + gpsLockEnabled: this.data.gpsLockEnabled, + gpsLockAvailable: this.data.gpsLockAvailable, }), }) }, @@ -832,9 +863,16 @@ Page({ showGameInfoPanel: this.data.showGameInfoPanel, skipButtonEnabled: this.data.skipButtonEnabled, gameSessionStatus: this.data.gameSessionStatus, + gpsLockEnabled: this.data.gpsLockEnabled, + gpsLockAvailable: this.data.gpsLockAvailable, }), }) }, + handleToggleGpsLock() { + if (mapEngine) { + mapEngine.handleToggleGpsLock() + } + }, handleToggleMapRotateMode() { if (!mapEngine) { return @@ -856,6 +894,8 @@ Page({ showGameInfoPanel: false, skipButtonEnabled: this.data.skipButtonEnabled, gameSessionStatus: this.data.gameSessionStatus, + gpsLockEnabled: this.data.gpsLockEnabled, + gpsLockAvailable: this.data.gpsLockAvailable, }), }) }, @@ -868,6 +908,8 @@ Page({ showGameInfoPanel: this.data.showGameInfoPanel, skipButtonEnabled: this.data.skipButtonEnabled, gameSessionStatus: this.data.gameSessionStatus, + gpsLockEnabled: this.data.gpsLockEnabled, + gpsLockAvailable: this.data.gpsLockAvailable, }), }) }, diff --git a/miniprogram/pages/map/map.wxml b/miniprogram/pages/map/map.wxml index 50c01b5..5cad661 100644 --- a/miniprogram/pages/map/map.wxml +++ b/miniprogram/pages/map/map.wxml @@ -25,7 +25,6 @@ > - @@ -77,7 +76,7 @@ 1 - 2 + 2 3 diff --git a/miniprogram/pages/map/map.wxss b/miniprogram/pages/map/map.wxss index 6d0df4a..225a87f 100644 --- a/miniprogram/pages/map/map.wxss +++ b/miniprogram/pages/map/map.wxss @@ -51,43 +51,6 @@ pointer-events: none; } -.map-stage__crosshair { - position: absolute; - left: 50%; - top: 50%; - width: 44rpx; - height: 44rpx; - transform: translate(-50%, -50%); - border: 3rpx solid rgba(255, 255, 255, 0.95); - border-radius: 50%; - box-shadow: 0 0 0 4rpx rgba(22, 48, 32, 0.2); - pointer-events: none; - z-index: 3; -} - -.map-stage__crosshair::before, -.map-stage__crosshair::after { - content: ''; - position: absolute; - background: rgba(255, 255, 255, 0.95); -} - -.map-stage__crosshair::before { - left: 50%; - top: -18rpx; - width: 2rpx; - height: 76rpx; - transform: translateX(-50%); -} - -.map-stage__crosshair::after { - left: -18rpx; - top: 50%; - width: 76rpx; - height: 2rpx; - transform: translateY(-50%); -} - .map-stage__map-pulse { position: absolute; width: 44rpx;