Improve map lock and smart heading behavior
This commit is contained in:
@@ -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<keyof MapEngineViewState> = [
|
||||
'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(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -189,6 +189,10 @@ export class UiEffectDirector {
|
||||
if (effect.type === 'session_finished') {
|
||||
this.clearPunchButtonMotion()
|
||||
}
|
||||
|
||||
if (effect.type === 'session_cancelled') {
|
||||
this.clearPunchButtonMotion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'skipButtonEnabled' | 'gameSessionStatus'>) {
|
||||
function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
|
||||
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,
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
></canvas>
|
||||
</view>
|
||||
|
||||
<view class="map-stage__crosshair"></view>
|
||||
<view class="map-stage__map-pulse {{mapPulseFxClass}}" wx:if="{{mapPulseVisible}}" style="left: {{mapPulseLeftPx}}px; top: {{mapPulseTopPx}}px;"></view>
|
||||
<view class="map-stage__stage-fx {{stageFxClass}}" wx:if="{{stageFxVisible}}"></view>
|
||||
|
||||
@@ -77,7 +76,7 @@
|
||||
<cover-view class="map-side-column map-side-column--left map-side-column--left-group" wx:if="{{!showDebugPanel && !showGameInfoPanel && showLeftButtonGroup}}" style="top: {{topInsetHeight}}px;">
|
||||
<cover-view class="map-side-button map-side-button--icon" bindtap="handleToggleMapRotateMode"><cover-image class="map-side-button__rotate-image {{orientationMode === 'heading-up' ? 'map-side-button__rotate-image--active' : ''}}" src="../../assets/btn_map_rotate_cropped.png"></cover-image></cover-view>
|
||||
<cover-view class="map-side-button map-side-button--muted"><cover-view class="map-side-button__text">1</cover-view></cover-view>
|
||||
<cover-view class="map-side-button"><cover-view class="map-side-button__text">2</cover-view></cover-view>
|
||||
<cover-view class="{{sideButton2Class}}" bindtap="handleToggleGpsLock"><cover-view class="map-side-button__text">2</cover-view></cover-view>
|
||||
<cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">3</cover-view></cover-view>
|
||||
<cover-view class="{{sideButton4Class}}" bindtap="handleForceExitGame"><cover-image class="map-side-button__action-image" src="../../assets/btn_exit.png"></cover-image></cover-view>
|
||||
</cover-view>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user