完善地图交互、动画与罗盘调试
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { getTileSizePx, screenToWorld, worldToScreen, type CameraState } from '../camera/camera'
|
||||
import { AccelerometerController } from '../sensor/accelerometerController'
|
||||
import { CompassHeadingController } from '../sensor/compassHeadingController'
|
||||
import { CompassHeadingController, type CompassTuningProfile } from '../sensor/compassHeadingController'
|
||||
import { DeviceMotionController } from '../sensor/deviceMotionController'
|
||||
import { GyroscopeController } from '../sensor/gyroscopeController'
|
||||
import { type HeartRateDiscoveredDevice } from '../sensor/heartRateController'
|
||||
@@ -11,6 +11,7 @@ import { type MapRendererStats } from '../renderer/mapRenderer'
|
||||
import { lonLatToWorldTile, worldTileToLonLat, type LonLatPoint, type MapCalibration } from '../../utils/projection'
|
||||
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
||||
import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
||||
import { formatAnimationLevelText, resolveAnimationLevel, type AnimationLevel } from '../../utils/animationLevel'
|
||||
import { GameRuntime } from '../../game/core/gameRuntime'
|
||||
import { type GameEffect, type GameResult } from '../../game/core/gameResult'
|
||||
import { buildGameDefinitionFromCourse } from '../../game/content/courseToGameDefinition'
|
||||
@@ -56,8 +57,27 @@ const AUTO_ROTATE_SNAP_DEG = 0.1
|
||||
const AUTO_ROTATE_DEADZONE_DEG = 4
|
||||
const AUTO_ROTATE_MAX_STEP_DEG = 0.75
|
||||
const AUTO_ROTATE_HEADING_SMOOTHING = 0.46
|
||||
const COMPASS_NEEDLE_MIN_SMOOTHING = 0.24
|
||||
const COMPASS_NEEDLE_MAX_SMOOTHING = 0.56
|
||||
const COMPASS_TUNING_PRESETS: Record<CompassTuningProfile, {
|
||||
needleMinSmoothing: number
|
||||
needleMaxSmoothing: number
|
||||
displayDeadzoneDeg: number
|
||||
}> = {
|
||||
smooth: {
|
||||
needleMinSmoothing: 0.16,
|
||||
needleMaxSmoothing: 0.4,
|
||||
displayDeadzoneDeg: 0.75,
|
||||
},
|
||||
balanced: {
|
||||
needleMinSmoothing: 0.22,
|
||||
needleMaxSmoothing: 0.52,
|
||||
displayDeadzoneDeg: 0.45,
|
||||
},
|
||||
responsive: {
|
||||
needleMinSmoothing: 0.3,
|
||||
needleMaxSmoothing: 0.68,
|
||||
displayDeadzoneDeg: 0.2,
|
||||
},
|
||||
}
|
||||
const SMART_HEADING_BLEND_START_SPEED_KMH = 1.2
|
||||
const SMART_HEADING_MOVEMENT_SPEED_KMH = 3.0
|
||||
const SMART_HEADING_MIN_DISTANCE_METERS = 12
|
||||
@@ -88,6 +108,7 @@ export interface MapEngineStageRect {
|
||||
}
|
||||
|
||||
export interface MapEngineViewState {
|
||||
animationLevel: AnimationLevel
|
||||
buildVersion: string
|
||||
renderMode: string
|
||||
projectionMode: string
|
||||
@@ -110,7 +131,11 @@ export interface MapEngineViewState {
|
||||
accelerometerText: string
|
||||
gyroscopeText: string
|
||||
deviceMotionText: string
|
||||
compassSourceText: string
|
||||
compassTuningProfile: CompassTuningProfile
|
||||
compassTuningProfileText: string
|
||||
compassDeclinationText: string
|
||||
northReferenceMode: NorthReferenceMode
|
||||
northReferenceButtonText: string
|
||||
autoRotateSourceText: string
|
||||
autoRotateCalibrationText: string
|
||||
@@ -199,6 +224,8 @@ export interface MapEngineViewState {
|
||||
contentCardTitle: string
|
||||
contentCardBody: string
|
||||
punchButtonFxClass: string
|
||||
panelProgressFxClass: string
|
||||
panelDistanceFxClass: string
|
||||
punchFeedbackFxClass: string
|
||||
contentCardFxClass: string
|
||||
mapPulseVisible: boolean
|
||||
@@ -228,6 +255,7 @@ export interface MapEngineGameInfoSnapshot {
|
||||
}
|
||||
|
||||
const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
||||
'animationLevel',
|
||||
'buildVersion',
|
||||
'renderMode',
|
||||
'projectionMode',
|
||||
@@ -252,7 +280,11 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
||||
'accelerometerText',
|
||||
'gyroscopeText',
|
||||
'deviceMotionText',
|
||||
'compassSourceText',
|
||||
'compassTuningProfile',
|
||||
'compassTuningProfileText',
|
||||
'compassDeclinationText',
|
||||
'northReferenceMode',
|
||||
'northReferenceButtonText',
|
||||
'autoRotateSourceText',
|
||||
'autoRotateCalibrationText',
|
||||
@@ -330,6 +362,8 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
||||
'contentCardTitle',
|
||||
'contentCardBody',
|
||||
'punchButtonFxClass',
|
||||
'panelProgressFxClass',
|
||||
'panelDistanceFxClass',
|
||||
'punchFeedbackFxClass',
|
||||
'contentCardFxClass',
|
||||
'mapPulseVisible',
|
||||
@@ -342,6 +376,38 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
||||
'osmReferenceText',
|
||||
]
|
||||
|
||||
const INTERACTION_DEFERRED_VIEW_KEYS = new Set<keyof MapEngineViewState>([
|
||||
'rotationText',
|
||||
'sensorHeadingText',
|
||||
'deviceHeadingText',
|
||||
'devicePoseText',
|
||||
'headingConfidenceText',
|
||||
'accelerometerText',
|
||||
'gyroscopeText',
|
||||
'deviceMotionText',
|
||||
'compassSourceText',
|
||||
'compassTuningProfile',
|
||||
'compassTuningProfileText',
|
||||
'compassDeclinationText',
|
||||
'autoRotateSourceText',
|
||||
'autoRotateCalibrationText',
|
||||
'northReferenceText',
|
||||
'centerText',
|
||||
'gpsCoordText',
|
||||
'visibleTileCount',
|
||||
'readyTileCount',
|
||||
'memoryTileCount',
|
||||
'diskTileCount',
|
||||
'memoryHitCount',
|
||||
'diskHitCount',
|
||||
'networkFetchCount',
|
||||
'cacheHitRateText',
|
||||
'heartRateDiscoveredDevices',
|
||||
'mockCoordText',
|
||||
'mockSpeedText',
|
||||
'mockHeartRateText',
|
||||
])
|
||||
|
||||
function buildCenterText(zoom: number, x: number, y: number): string {
|
||||
return `z${zoom} / x${x} / y${y}`
|
||||
}
|
||||
@@ -387,18 +453,23 @@ function interpolateAngleDeg(currentDeg: number, targetDeg: number, factor: numb
|
||||
return normalizeRotationDeg(currentDeg + normalizeAngleDeltaDeg(targetDeg - currentDeg) * factor)
|
||||
}
|
||||
|
||||
function getCompassNeedleSmoothingFactor(currentDeg: number, targetDeg: number): number {
|
||||
function getCompassNeedleSmoothingFactor(
|
||||
currentDeg: number,
|
||||
targetDeg: number,
|
||||
profile: CompassTuningProfile,
|
||||
): number {
|
||||
const preset = COMPASS_TUNING_PRESETS[profile]
|
||||
const deltaDeg = Math.abs(normalizeAngleDeltaDeg(targetDeg - currentDeg))
|
||||
if (deltaDeg <= 4) {
|
||||
return COMPASS_NEEDLE_MIN_SMOOTHING
|
||||
return preset.needleMinSmoothing
|
||||
}
|
||||
if (deltaDeg >= 36) {
|
||||
return COMPASS_NEEDLE_MAX_SMOOTHING
|
||||
return preset.needleMaxSmoothing
|
||||
}
|
||||
|
||||
const progress = (deltaDeg - 4) / (36 - 4)
|
||||
return COMPASS_NEEDLE_MIN_SMOOTHING
|
||||
+ (COMPASS_NEEDLE_MAX_SMOOTHING - COMPASS_NEEDLE_MIN_SMOOTHING) * progress
|
||||
return preset.needleMinSmoothing
|
||||
+ (preset.needleMaxSmoothing - preset.needleMinSmoothing) * progress
|
||||
}
|
||||
|
||||
function getMovementHeadingSmoothingFactor(speedKmh: number | null): number {
|
||||
@@ -434,7 +505,7 @@ function formatRotationText(rotationDeg: number): string {
|
||||
}
|
||||
|
||||
function normalizeDegreeDisplayText(text: string): string {
|
||||
return text.replace(/[°掳•]/g, '˚')
|
||||
return text.replace(/[掳•˚]/g, '°')
|
||||
}
|
||||
|
||||
function formatHeadingText(headingDeg: number | null): string {
|
||||
@@ -442,7 +513,7 @@ function formatHeadingText(headingDeg: number | null): string {
|
||||
return '--'
|
||||
}
|
||||
|
||||
return `${Math.round(normalizeRotationDeg(headingDeg))}˚`
|
||||
return `${Math.round(normalizeRotationDeg(headingDeg))}°`
|
||||
}
|
||||
|
||||
function formatDevicePoseText(pose: 'upright' | 'tilted' | 'flat'): string {
|
||||
@@ -494,9 +565,9 @@ function formatDeviceMotionText(motion: { alpha: number | null; beta: number | n
|
||||
return '--'
|
||||
}
|
||||
|
||||
const alphaDeg = motion.alpha === null ? '--' : Math.round(normalizeRotationDeg(360 - motion.alpha * 180 / Math.PI))
|
||||
const betaDeg = motion.beta === null ? '--' : Math.round(motion.beta * 180 / Math.PI)
|
||||
const gammaDeg = motion.gamma === null ? '--' : Math.round(motion.gamma * 180 / Math.PI)
|
||||
const alphaDeg = motion.alpha === null ? '--' : Math.round(normalizeRotationDeg(360 - motion.alpha))
|
||||
const betaDeg = motion.beta === null ? '--' : Math.round(motion.beta)
|
||||
const gammaDeg = motion.gamma === null ? '--' : Math.round(motion.gamma)
|
||||
return `a:${alphaDeg} b:${betaDeg} g:${gammaDeg}`
|
||||
}
|
||||
|
||||
@@ -620,6 +691,26 @@ function formatCompassDeclinationText(mode: NorthReferenceMode): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function formatCompassSourceText(source: 'compass' | 'motion' | null): string {
|
||||
if (source === 'compass') {
|
||||
return '罗盘'
|
||||
}
|
||||
if (source === 'motion') {
|
||||
return '设备方向兜底'
|
||||
}
|
||||
return '无数据'
|
||||
}
|
||||
|
||||
function formatCompassTuningProfileText(profile: CompassTuningProfile): string {
|
||||
if (profile === 'smooth') {
|
||||
return '顺滑'
|
||||
}
|
||||
if (profile === 'responsive') {
|
||||
return '跟手'
|
||||
}
|
||||
return '平衡'
|
||||
}
|
||||
|
||||
function formatNorthReferenceButtonText(mode: NorthReferenceMode): string {
|
||||
return mode === 'magnetic' ? '北参照:磁北' : '北参照:真北'
|
||||
}
|
||||
@@ -702,6 +793,7 @@ function getInitialBearingDeg(from: LonLatPoint, to: LonLatPoint): number {
|
||||
|
||||
export class MapEngine {
|
||||
buildVersion: string
|
||||
animationLevel: AnimationLevel
|
||||
renderer: WebGLMapRenderer
|
||||
accelerometerController: AccelerometerController
|
||||
compassController: CompassHeadingController
|
||||
@@ -742,6 +834,8 @@ export class MapEngine {
|
||||
sensorHeadingDeg: number | null
|
||||
smoothedSensorHeadingDeg: number | null
|
||||
compassDisplayHeadingDeg: number | null
|
||||
compassSource: 'compass' | 'motion' | null
|
||||
compassTuningProfile: CompassTuningProfile
|
||||
smoothedMovementHeadingDeg: number | null
|
||||
autoRotateHeadingDeg: number | null
|
||||
courseHeadingDeg: number | null
|
||||
@@ -789,6 +883,8 @@ export class MapEngine {
|
||||
|
||||
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
|
||||
this.buildVersion = buildVersion
|
||||
this.animationLevel = resolveAnimationLevel(wx.getSystemInfoSync())
|
||||
this.compassTuningProfile = 'balanced'
|
||||
this.onData = callbacks.onData
|
||||
this.accelerometerErrorText = null
|
||||
this.renderer = new WebGLMapRenderer(
|
||||
@@ -812,7 +908,7 @@ export class MapEngine {
|
||||
z,
|
||||
})
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
this.setState(this.getTelemetrySensorViewPatch())
|
||||
}
|
||||
},
|
||||
onError: (message) => {
|
||||
@@ -821,7 +917,7 @@ export class MapEngine {
|
||||
this.setState({
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
statusText: `加速度计启动失败 (${this.buildVersion})`,
|
||||
}, true)
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -833,6 +929,7 @@ export class MapEngine {
|
||||
this.handleCompassError(message)
|
||||
},
|
||||
})
|
||||
this.compassController.setTuningProfile(this.compassTuningProfile)
|
||||
this.gyroscopeController = new GyroscopeController({
|
||||
onSample: (x, y, z) => {
|
||||
this.telemetryRuntime.dispatch({
|
||||
@@ -843,12 +940,12 @@ export class MapEngine {
|
||||
z,
|
||||
})
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
this.setState(this.getTelemetrySensorViewPatch())
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
this.setState(this.getTelemetrySensorViewPatch())
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -865,16 +962,12 @@ export class MapEngine {
|
||||
this.setState({
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) {
|
||||
this.scheduleAutoRotate()
|
||||
})
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
this.setState(this.getTelemetrySensorViewPatch())
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -899,7 +992,7 @@ export class MapEngine {
|
||||
},
|
||||
onDebugStateChange: () => {
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getLocationControllerViewPatch(), true)
|
||||
this.setState(this.getLocationControllerViewPatch())
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -963,12 +1056,12 @@ export class MapEngine {
|
||||
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
}, true)
|
||||
})
|
||||
}
|
||||
},
|
||||
onDebugStateChange: () => {
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getHeartRateControllerViewPatch(), true)
|
||||
this.setState(this.getHeartRateControllerViewPatch())
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -982,6 +1075,12 @@ export class MapEngine {
|
||||
setPunchButtonFxClass: (className) => {
|
||||
this.setPunchButtonFxClass(className)
|
||||
},
|
||||
setHudProgressFxClass: (className) => {
|
||||
this.setHudProgressFxClass(className)
|
||||
},
|
||||
setHudDistanceFxClass: (className) => {
|
||||
this.setHudDistanceFxClass(className)
|
||||
},
|
||||
showMapPulse: (controlId, motionClass) => {
|
||||
this.showMapPulse(controlId, motionClass)
|
||||
},
|
||||
@@ -994,6 +1093,7 @@ export class MapEngine {
|
||||
}
|
||||
},
|
||||
})
|
||||
this.feedbackDirector.setAnimationLevel(this.animationLevel)
|
||||
this.minZoom = MIN_ZOOM
|
||||
this.maxZoom = MAX_ZOOM
|
||||
this.defaultZoom = DEFAULT_ZOOM
|
||||
@@ -1032,6 +1132,7 @@ export class MapEngine {
|
||||
this.sessionTimerInterval = 0
|
||||
this.hasGpsCenteredOnce = false
|
||||
this.state = {
|
||||
animationLevel: this.animationLevel,
|
||||
buildVersion: this.buildVersion,
|
||||
renderMode: RENDER_MODE,
|
||||
projectionMode: PROJECTION_MODE,
|
||||
@@ -1051,10 +1152,14 @@ export class MapEngine {
|
||||
deviceHeadingText: '--',
|
||||
devicePoseText: '竖持',
|
||||
headingConfidenceText: '低',
|
||||
accelerometerText: '未启用',
|
||||
accelerometerText: '未启用',
|
||||
gyroscopeText: '--',
|
||||
deviceMotionText: '--',
|
||||
compassSourceText: '无数据',
|
||||
compassTuningProfile: this.compassTuningProfile,
|
||||
compassTuningProfileText: formatCompassTuningProfileText(this.compassTuningProfile),
|
||||
compassDeclinationText: formatCompassDeclinationText(DEFAULT_NORTH_REFERENCE_MODE),
|
||||
northReferenceMode: DEFAULT_NORTH_REFERENCE_MODE,
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(DEFAULT_NORTH_REFERENCE_MODE),
|
||||
autoRotateSourceText: formatAutoRotateSourceText('smart', false),
|
||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)),
|
||||
@@ -1137,6 +1242,8 @@ export class MapEngine {
|
||||
contentCardTitle: '',
|
||||
contentCardBody: '',
|
||||
punchButtonFxClass: '',
|
||||
panelProgressFxClass: '',
|
||||
panelDistanceFxClass: '',
|
||||
punchFeedbackFxClass: '',
|
||||
contentCardFxClass: '',
|
||||
mapPulseVisible: false,
|
||||
@@ -1177,6 +1284,8 @@ export class MapEngine {
|
||||
this.sensorHeadingDeg = null
|
||||
this.smoothedSensorHeadingDeg = null
|
||||
this.compassDisplayHeadingDeg = null
|
||||
this.compassSource = null
|
||||
this.compassTuningProfile = 'balanced'
|
||||
this.smoothedMovementHeadingDeg = null
|
||||
this.autoRotateHeadingDeg = null
|
||||
this.courseHeadingDeg = null
|
||||
@@ -1241,6 +1350,7 @@ export class MapEngine {
|
||||
{ label: '配置版本', value: this.configVersion || '--' },
|
||||
{ label: 'Schema版本', value: this.configSchemaVersion || '--' },
|
||||
{ label: '活动ID', value: this.configAppId || '--' },
|
||||
{ label: '动画等级', value: formatAnimationLevelText(this.state.animationLevel) },
|
||||
{ label: '地图', value: this.state.mapName || '--' },
|
||||
{ label: '模式', value: this.getGameModeText() },
|
||||
{ label: '状态', value: formatGameSessionStatusText(this.state.gameSessionStatus) },
|
||||
@@ -1417,20 +1527,23 @@ export class MapEngine {
|
||||
|
||||
getTelemetrySensorViewPatch(): Partial<MapEngineViewState> {
|
||||
const telemetryState = this.telemetryRuntime.state
|
||||
return {
|
||||
deviceHeadingText: formatHeadingText(
|
||||
telemetryState.deviceHeadingDeg === null
|
||||
? null
|
||||
: getCompassReferenceHeadingDeg(this.northReferenceMode, telemetryState.deviceHeadingDeg),
|
||||
),
|
||||
devicePoseText: formatDevicePoseText(telemetryState.devicePose),
|
||||
headingConfidenceText: formatHeadingConfidenceText(telemetryState.headingConfidence),
|
||||
accelerometerText: telemetryState.accelerometer
|
||||
? `#${telemetryState.accelerometerSampleCount} ${formatClockTime(telemetryState.accelerometerUpdatedAt)} x:${telemetryState.accelerometer.x.toFixed(3)} y:${telemetryState.accelerometer.y.toFixed(3)} z:${telemetryState.accelerometer.z.toFixed(3)}`
|
||||
: '未启用',
|
||||
gyroscopeText: formatGyroscopeText(telemetryState.gyroscope),
|
||||
deviceMotionText: formatDeviceMotionText(telemetryState.deviceMotion),
|
||||
}
|
||||
return {
|
||||
deviceHeadingText: formatHeadingText(
|
||||
telemetryState.deviceHeadingDeg === null
|
||||
? null
|
||||
: getCompassReferenceHeadingDeg(this.northReferenceMode, telemetryState.deviceHeadingDeg),
|
||||
),
|
||||
devicePoseText: formatDevicePoseText(telemetryState.devicePose),
|
||||
headingConfidenceText: formatHeadingConfidenceText(telemetryState.headingConfidence),
|
||||
accelerometerText: telemetryState.accelerometer
|
||||
? `#${telemetryState.accelerometerSampleCount} ${formatClockTime(telemetryState.accelerometerUpdatedAt)} x:${telemetryState.accelerometer.x.toFixed(3)} y:${telemetryState.accelerometer.y.toFixed(3)} z:${telemetryState.accelerometer.z.toFixed(3)}`
|
||||
: '未启用',
|
||||
gyroscopeText: formatGyroscopeText(telemetryState.gyroscope),
|
||||
deviceMotionText: formatDeviceMotionText(telemetryState.deviceMotion),
|
||||
compassSourceText: formatCompassSourceText(this.compassSource),
|
||||
compassTuningProfile: this.compassTuningProfile,
|
||||
compassTuningProfileText: formatCompassTuningProfileText(this.compassTuningProfile),
|
||||
}
|
||||
}
|
||||
|
||||
getGameModeText(): string {
|
||||
@@ -1589,6 +1702,8 @@ export class MapEngine {
|
||||
stageFxVisible: false,
|
||||
stageFxClass: '',
|
||||
punchButtonFxClass: '',
|
||||
panelProgressFxClass: '',
|
||||
panelDistanceFxClass: '',
|
||||
}, true)
|
||||
}
|
||||
|
||||
@@ -1675,6 +1790,18 @@ export class MapEngine {
|
||||
}, true)
|
||||
}
|
||||
|
||||
setHudProgressFxClass(className: string): void {
|
||||
this.setState({
|
||||
panelProgressFxClass: className,
|
||||
}, true)
|
||||
}
|
||||
|
||||
setHudDistanceFxClass(className: string): void {
|
||||
this.setState({
|
||||
panelDistanceFxClass: className,
|
||||
}, true)
|
||||
}
|
||||
|
||||
showMapPulse(controlId: string, motionClass = ''): void {
|
||||
const screenPoint = this.getControlScreenPoint(controlId)
|
||||
if (!screenPoint) {
|
||||
@@ -1761,6 +1888,9 @@ export class MapEngine {
|
||||
applyGameEffects(effects: GameEffect[]): string | null {
|
||||
this.feedbackDirector.handleEffects(effects)
|
||||
if (effects.some((effect) => effect.type === 'session_finished')) {
|
||||
if (this.locationController.listening) {
|
||||
this.locationController.stop()
|
||||
}
|
||||
this.setState({
|
||||
gpsTracking: false,
|
||||
gpsTrackingText: '测试结束,定位已停止',
|
||||
@@ -1845,12 +1975,17 @@ export class MapEngine {
|
||||
|
||||
handleForceExitGame(): void {
|
||||
this.feedbackDirector.reset()
|
||||
if (this.locationController.listening) {
|
||||
this.locationController.stop()
|
||||
}
|
||||
|
||||
if (!this.courseData) {
|
||||
this.clearGameRuntime()
|
||||
this.resetTransientGameUiState()
|
||||
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
|
||||
this.setState({
|
||||
gpsTracking: false,
|
||||
gpsTrackingText: '已退出对局,定位已停止',
|
||||
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
||||
}, true)
|
||||
this.syncRenderer()
|
||||
@@ -1861,6 +1996,8 @@ export class MapEngine {
|
||||
this.resetTransientGameUiState()
|
||||
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
|
||||
this.setState({
|
||||
gpsTracking: false,
|
||||
gpsTrackingText: '已退出对局,定位已停止',
|
||||
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
||||
}, true)
|
||||
this.syncRenderer()
|
||||
@@ -1946,7 +2083,7 @@ export class MapEngine {
|
||||
gpsLockEnabled: this.gpsLockEnabled,
|
||||
gpsLockAvailable: gpsInsideMap,
|
||||
...this.getGameViewPatch(gameStatusText || (gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`)),
|
||||
}, true)
|
||||
})
|
||||
this.syncRenderer()
|
||||
}
|
||||
|
||||
@@ -2100,7 +2237,7 @@ export class MapEngine {
|
||||
this.setState({
|
||||
heartRateDeviceText: this.heartRateController.currentDeviceName || '--',
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
}, true)
|
||||
})
|
||||
}
|
||||
|
||||
handleDebugHeartRateTone(tone: HeartRateTone): void {
|
||||
@@ -2112,7 +2249,7 @@ export class MapEngine {
|
||||
})
|
||||
this.setState({
|
||||
heartRateStatusText: `调试心率: ${sampleBpm} bpm / ${tone.toUpperCase()}`,
|
||||
}, true)
|
||||
})
|
||||
this.syncSessionTimerText()
|
||||
}
|
||||
|
||||
@@ -2128,7 +2265,7 @@ export class MapEngine {
|
||||
: (this.heartRateController.sourceMode === 'mock' ? '模拟心率源未连接' : '心率带未连接'),
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
}, true)
|
||||
})
|
||||
this.syncSessionTimerText()
|
||||
}
|
||||
|
||||
@@ -2250,7 +2387,7 @@ export class MapEngine {
|
||||
configStatusText: `配置已载入 / ${config.configTitle} / ${config.courseStatusText}`,
|
||||
projectionMode: config.projectionModeText,
|
||||
tileSource: config.tileSource,
|
||||
sensorHeadingText: formatHeadingText(this.smoothedSensorHeadingDeg === null ? null : getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)),
|
||||
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
||||
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
||||
@@ -2308,7 +2445,7 @@ export class MapEngine {
|
||||
this.pinchAnchorWorldY = anchorWorld.y
|
||||
this.setPreviewState(this.pinchStartScale, origin.x, origin.y)
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2567,7 +2704,7 @@ export class MapEngine {
|
||||
() => {
|
||||
this.resetPreviewState()
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
this.scheduleAutoRotate()
|
||||
},
|
||||
)
|
||||
@@ -2601,7 +2738,7 @@ export class MapEngine {
|
||||
() => {
|
||||
this.resetPreviewState()
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2638,7 +2775,7 @@ export class MapEngine {
|
||||
() => {
|
||||
this.resetPreviewState()
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2673,6 +2810,38 @@ export class MapEngine {
|
||||
this.cycleNorthReferenceMode()
|
||||
}
|
||||
|
||||
handleSetNorthReferenceMode(mode: NorthReferenceMode): void {
|
||||
this.setNorthReferenceMode(mode)
|
||||
}
|
||||
|
||||
handleSetAnimationLevel(level: AnimationLevel): void {
|
||||
if (this.animationLevel === level) {
|
||||
return
|
||||
}
|
||||
|
||||
this.animationLevel = level
|
||||
this.feedbackDirector.setAnimationLevel(level)
|
||||
this.setState({
|
||||
animationLevel: level,
|
||||
statusText: `动画性能已切换为${formatAnimationLevelText(level)} (${this.buildVersion})`,
|
||||
})
|
||||
this.syncRenderer()
|
||||
}
|
||||
|
||||
handleSetCompassTuningProfile(profile: CompassTuningProfile): void {
|
||||
if (this.compassTuningProfile === profile) {
|
||||
return
|
||||
}
|
||||
|
||||
this.compassTuningProfile = profile
|
||||
this.compassController.setTuningProfile(profile)
|
||||
this.setState({
|
||||
compassTuningProfile: profile,
|
||||
compassTuningProfileText: formatCompassTuningProfileText(profile),
|
||||
statusText: `指北针响应已切换为${formatCompassTuningProfileText(profile)} (${this.buildVersion})`,
|
||||
}, true)
|
||||
}
|
||||
|
||||
handleAutoRotateCalibrate(): void {
|
||||
if (this.state.orientationMode !== 'heading-up') {
|
||||
this.setState({
|
||||
@@ -2761,30 +2930,40 @@ export class MapEngine {
|
||||
}
|
||||
}
|
||||
|
||||
handleCompassHeading(headingDeg: number): void {
|
||||
applyHeadingSample(headingDeg: number, source: 'compass' | 'motion'): void {
|
||||
this.compassSource = source
|
||||
this.sensorHeadingDeg = normalizeRotationDeg(headingDeg)
|
||||
this.smoothedSensorHeadingDeg = this.smoothedSensorHeadingDeg === null
|
||||
? this.sensorHeadingDeg
|
||||
: interpolateAngleDeg(this.smoothedSensorHeadingDeg, this.sensorHeadingDeg, AUTO_ROTATE_HEADING_SMOOTHING)
|
||||
|
||||
const compassHeadingDeg = getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)
|
||||
this.compassDisplayHeadingDeg = this.compassDisplayHeadingDeg === null
|
||||
? compassHeadingDeg
|
||||
: interpolateAngleDeg(
|
||||
this.compassDisplayHeadingDeg,
|
||||
compassHeadingDeg,
|
||||
getCompassNeedleSmoothingFactor(this.compassDisplayHeadingDeg, compassHeadingDeg),
|
||||
)
|
||||
if (this.compassDisplayHeadingDeg === null) {
|
||||
this.compassDisplayHeadingDeg = compassHeadingDeg
|
||||
} else {
|
||||
const displayDeltaDeg = Math.abs(normalizeAngleDeltaDeg(compassHeadingDeg - this.compassDisplayHeadingDeg))
|
||||
if (displayDeltaDeg >= COMPASS_TUNING_PRESETS[this.compassTuningProfile].displayDeadzoneDeg) {
|
||||
this.compassDisplayHeadingDeg = interpolateAngleDeg(
|
||||
this.compassDisplayHeadingDeg,
|
||||
compassHeadingDeg,
|
||||
getCompassNeedleSmoothingFactor(
|
||||
this.compassDisplayHeadingDeg,
|
||||
compassHeadingDeg,
|
||||
this.compassTuningProfile,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.autoRotateHeadingDeg = this.resolveAutoRotateInputHeadingDeg()
|
||||
|
||||
this.setState({
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
|
||||
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||
...(this.diagnosticUiEnabled
|
||||
? {
|
||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
||||
@@ -2801,18 +2980,31 @@ export class MapEngine {
|
||||
}
|
||||
}
|
||||
|
||||
handleCompassHeading(headingDeg: number): void {
|
||||
this.applyHeadingSample(headingDeg, 'compass')
|
||||
}
|
||||
|
||||
handleCompassError(message: string): void {
|
||||
this.clearAutoRotateTimer()
|
||||
this.targetAutoRotationDeg = null
|
||||
this.autoRotateCalibrationPending = false
|
||||
this.compassSource = null
|
||||
this.setState({
|
||||
compassSourceText: formatCompassSourceText(null),
|
||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
|
||||
statusText: `${message} (${this.buildVersion})`,
|
||||
}, true)
|
||||
}
|
||||
|
||||
cycleNorthReferenceMode(): void {
|
||||
const nextMode = getNextNorthReferenceMode(this.northReferenceMode)
|
||||
this.setNorthReferenceMode(getNextNorthReferenceMode(this.northReferenceMode))
|
||||
}
|
||||
|
||||
setNorthReferenceMode(nextMode: NorthReferenceMode): void {
|
||||
if (nextMode === this.northReferenceMode) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextMapNorthOffsetDeg = getMapNorthOffsetDeg(nextMode)
|
||||
const compassHeadingDeg = this.smoothedSensorHeadingDeg === null
|
||||
? null
|
||||
@@ -2831,9 +3023,10 @@ export class MapEngine {
|
||||
rotationDeg: MAP_NORTH_OFFSET_DEG,
|
||||
rotationText: formatRotationText(MAP_NORTH_OFFSET_DEG),
|
||||
northReferenceText: formatNorthReferenceText(nextMode),
|
||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
||||
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
||||
northReferenceMode: nextMode,
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
||||
@@ -2850,9 +3043,10 @@ export class MapEngine {
|
||||
|
||||
this.setState({
|
||||
northReferenceText: formatNorthReferenceText(nextMode),
|
||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
||||
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
||||
northReferenceMode: nextMode,
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
||||
@@ -3167,6 +3361,7 @@ export class MapEngine {
|
||||
|
||||
buildScene() {
|
||||
const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
|
||||
const readyControlSequences = this.resolveReadyControlSequences()
|
||||
return {
|
||||
tileSource: this.state.tileSource,
|
||||
osmTileSource: OSM_TILE_SOURCE,
|
||||
@@ -3183,6 +3378,7 @@ export class MapEngine {
|
||||
translateX: this.state.tileTranslateX,
|
||||
translateY: this.state.tileTranslateY,
|
||||
rotationRad: this.getRotationRad(this.state.rotationDeg),
|
||||
animationLevel: this.state.animationLevel,
|
||||
previewScale: this.previewScale || 1,
|
||||
previewOriginX: this.previewOriginX || this.state.stageWidth / 2,
|
||||
previewOriginY: this.previewOriginY || this.state.stageHeight / 2,
|
||||
@@ -3199,6 +3395,7 @@ export class MapEngine {
|
||||
focusedControlId: this.gamePresentation.map.focusedControlId,
|
||||
focusedControlSequences: this.gamePresentation.map.focusedControlSequences,
|
||||
activeControlSequences: this.gamePresentation.map.activeControlSequences,
|
||||
readyControlSequences,
|
||||
activeStart: this.gamePresentation.map.activeStart,
|
||||
completedStart: this.gamePresentation.map.completedStart,
|
||||
activeFinish: this.gamePresentation.map.activeFinish,
|
||||
@@ -3215,6 +3412,21 @@ export class MapEngine {
|
||||
}
|
||||
}
|
||||
|
||||
resolveReadyControlSequences(): number[] {
|
||||
const punchableControlId = this.gamePresentation.hud.punchableControlId
|
||||
const definition = this.gameRuntime.definition
|
||||
if (!punchableControlId || !definition) {
|
||||
return []
|
||||
}
|
||||
|
||||
const control = definition.controls.find((item) => item.id === punchableControlId)
|
||||
if (!control || control.sequence === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [control.sequence]
|
||||
}
|
||||
|
||||
syncRenderer(): void {
|
||||
if (!this.mounted || !this.state.stageWidth || !this.state.stageHeight) {
|
||||
return
|
||||
@@ -3374,8 +3586,32 @@ export class MapEngine {
|
||||
}
|
||||
|
||||
const patch = this.pendingViewPatch
|
||||
this.pendingViewPatch = {}
|
||||
this.onData(patch)
|
||||
const shouldDeferForInteraction = this.gestureMode !== 'idle' || !!this.inertiaTimer || !!this.previewResetTimer
|
||||
const nextPendingPatch = {} as Partial<MapEngineViewState>
|
||||
const outputPatch = {} as Partial<MapEngineViewState>
|
||||
|
||||
for (const [key, value] of Object.entries(patch) as Array<[keyof MapEngineViewState, MapEngineViewState[keyof MapEngineViewState]]>) {
|
||||
if (shouldDeferForInteraction && INTERACTION_DEFERRED_VIEW_KEYS.has(key)) {
|
||||
;(nextPendingPatch as Record<string, unknown>)[key] = value
|
||||
continue
|
||||
}
|
||||
;(outputPatch as Record<string, unknown>)[key] = value
|
||||
}
|
||||
|
||||
this.pendingViewPatch = nextPendingPatch
|
||||
|
||||
if (Object.keys(this.pendingViewPatch).length && !this.viewSyncTimer) {
|
||||
this.viewSyncTimer = setTimeout(() => {
|
||||
this.viewSyncTimer = 0
|
||||
this.flushViewPatch()
|
||||
}, UI_SYNC_INTERVAL_MS) as unknown as number
|
||||
}
|
||||
|
||||
if (!Object.keys(outputPatch).length) {
|
||||
return
|
||||
}
|
||||
|
||||
this.onData(outputPatch)
|
||||
}
|
||||
|
||||
getTouchDistance(touches: TouchPoint[]): number {
|
||||
@@ -3431,7 +3667,7 @@ export class MapEngine {
|
||||
if (Math.abs(startScale - 1) < 0.01) {
|
||||
this.resetPreviewState()
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
this.scheduleAutoRotate()
|
||||
return
|
||||
}
|
||||
@@ -3443,12 +3679,12 @@ export class MapEngine {
|
||||
const nextScale = startScale + (1 - startScale) * eased
|
||||
this.setPreviewState(nextScale, originX, originY)
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
|
||||
if (progress >= 1) {
|
||||
this.resetPreviewState()
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
this.previewResetTimer = 0
|
||||
this.scheduleAutoRotate()
|
||||
return
|
||||
@@ -3467,7 +3703,7 @@ export class MapEngine {
|
||||
tileTranslateY: translateY,
|
||||
})
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3530,7 +3766,7 @@ export class MapEngine {
|
||||
() => {
|
||||
this.setPreviewState(residualScale, stageX, stageY)
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
this.animatePreviewToRest()
|
||||
},
|
||||
)
|
||||
@@ -3557,7 +3793,7 @@ export class MapEngine {
|
||||
() => {
|
||||
this.setPreviewState(residualScale, stageX, stageY)
|
||||
this.syncRenderer()
|
||||
this.compassController.start()
|
||||
this.compassController.start()
|
||||
this.animatePreviewToRest()
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user