优化地图交互与文档方案
This commit is contained in:
@@ -55,12 +55,15 @@ const AUTO_ROTATE_EASE = 0.34
|
||||
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.32
|
||||
const COMPASS_NEEDLE_SMOOTHING = 0.12
|
||||
const AUTO_ROTATE_HEADING_SMOOTHING = 0.46
|
||||
const COMPASS_NEEDLE_MIN_SMOOTHING = 0.24
|
||||
const COMPASS_NEEDLE_MAX_SMOOTHING = 0.56
|
||||
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_MIN_DISTANCE_METERS = 12
|
||||
const SMART_HEADING_MAX_ACCURACY_METERS = 25
|
||||
const SMART_HEADING_MOVEMENT_MIN_SMOOTHING = 0.12
|
||||
const SMART_HEADING_MOVEMENT_MAX_SMOOTHING = 0.24
|
||||
const GPS_TRACK_MAX_POINTS = 200
|
||||
const GPS_TRACK_MIN_STEP_METERS = 3
|
||||
const MAP_TAP_MOVE_THRESHOLD_PX = 14
|
||||
@@ -384,6 +387,35 @@ function interpolateAngleDeg(currentDeg: number, targetDeg: number, factor: numb
|
||||
return normalizeRotationDeg(currentDeg + normalizeAngleDeltaDeg(targetDeg - currentDeg) * factor)
|
||||
}
|
||||
|
||||
function getCompassNeedleSmoothingFactor(currentDeg: number, targetDeg: number): number {
|
||||
const deltaDeg = Math.abs(normalizeAngleDeltaDeg(targetDeg - currentDeg))
|
||||
if (deltaDeg <= 4) {
|
||||
return COMPASS_NEEDLE_MIN_SMOOTHING
|
||||
}
|
||||
if (deltaDeg >= 36) {
|
||||
return COMPASS_NEEDLE_MAX_SMOOTHING
|
||||
}
|
||||
|
||||
const progress = (deltaDeg - 4) / (36 - 4)
|
||||
return COMPASS_NEEDLE_MIN_SMOOTHING
|
||||
+ (COMPASS_NEEDLE_MAX_SMOOTHING - COMPASS_NEEDLE_MIN_SMOOTHING) * progress
|
||||
}
|
||||
|
||||
function getMovementHeadingSmoothingFactor(speedKmh: number | null): number {
|
||||
if (speedKmh === null || !Number.isFinite(speedKmh) || speedKmh <= SMART_HEADING_BLEND_START_SPEED_KMH) {
|
||||
return SMART_HEADING_MOVEMENT_MIN_SMOOTHING
|
||||
}
|
||||
|
||||
if (speedKmh >= SMART_HEADING_MOVEMENT_SPEED_KMH) {
|
||||
return SMART_HEADING_MOVEMENT_MAX_SMOOTHING
|
||||
}
|
||||
|
||||
const progress = (speedKmh - SMART_HEADING_BLEND_START_SPEED_KMH)
|
||||
/ (SMART_HEADING_MOVEMENT_SPEED_KMH - SMART_HEADING_BLEND_START_SPEED_KMH)
|
||||
return SMART_HEADING_MOVEMENT_MIN_SMOOTHING
|
||||
+ (SMART_HEADING_MOVEMENT_MAX_SMOOTHING - SMART_HEADING_MOVEMENT_MIN_SMOOTHING) * progress
|
||||
}
|
||||
|
||||
function formatGameSessionStatusText(status: 'idle' | 'running' | 'finished' | 'failed'): string {
|
||||
if (status === 'running') {
|
||||
return '进行中'
|
||||
@@ -705,16 +737,19 @@ export class MapEngine {
|
||||
autoRotateTimer: number
|
||||
pendingViewPatch: Partial<MapEngineViewState>
|
||||
mounted: boolean
|
||||
diagnosticUiEnabled: boolean
|
||||
northReferenceMode: NorthReferenceMode
|
||||
sensorHeadingDeg: number | null
|
||||
smoothedSensorHeadingDeg: number | null
|
||||
compassDisplayHeadingDeg: number | null
|
||||
smoothedMovementHeadingDeg: number | null
|
||||
autoRotateHeadingDeg: number | null
|
||||
courseHeadingDeg: number | null
|
||||
targetAutoRotationDeg: number | null
|
||||
autoRotateSourceMode: AutoRotateSourceMode
|
||||
autoRotateCalibrationOffsetDeg: number | null
|
||||
autoRotateCalibrationPending: boolean
|
||||
lastStatsUiSyncAt: number
|
||||
minZoom: number
|
||||
maxZoom: number
|
||||
defaultZoom: number
|
||||
@@ -776,14 +811,18 @@ export class MapEngine {
|
||||
y,
|
||||
z,
|
||||
})
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
}
|
||||
},
|
||||
onError: (message) => {
|
||||
this.accelerometerErrorText = `不可用: ${message}`
|
||||
this.setState({
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
statusText: `加速度计启动失败 (${this.buildVersion})`,
|
||||
}, true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState({
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
statusText: `加速度计启动失败 (${this.buildVersion})`,
|
||||
}, true)
|
||||
}
|
||||
},
|
||||
})
|
||||
this.compassController = new CompassHeadingController({
|
||||
@@ -803,10 +842,14 @@ export class MapEngine {
|
||||
y,
|
||||
z,
|
||||
})
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
}
|
||||
},
|
||||
})
|
||||
this.deviceMotionController = new DeviceMotionController({
|
||||
@@ -818,17 +861,21 @@ export class MapEngine {
|
||||
beta,
|
||||
gamma,
|
||||
})
|
||||
this.setState({
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||
}, true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState({
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) {
|
||||
this.scheduleAutoRotate()
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
||||
}
|
||||
},
|
||||
})
|
||||
this.locationController = new LocationController({
|
||||
@@ -840,7 +887,7 @@ export class MapEngine {
|
||||
gpsTracking: this.locationController.listening,
|
||||
gpsTrackingText: message,
|
||||
...this.getLocationControllerViewPatch(),
|
||||
}, true)
|
||||
})
|
||||
},
|
||||
onError: (message) => {
|
||||
this.setState({
|
||||
@@ -848,10 +895,12 @@ export class MapEngine {
|
||||
gpsTrackingText: message,
|
||||
...this.getLocationControllerViewPatch(),
|
||||
statusText: `${message} (${this.buildVersion})`,
|
||||
}, true)
|
||||
})
|
||||
},
|
||||
onDebugStateChange: () => {
|
||||
this.setState(this.getLocationControllerViewPatch(), true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getLocationControllerViewPatch(), true)
|
||||
}
|
||||
},
|
||||
})
|
||||
this.heartRateController = new HeartRateInputController({
|
||||
@@ -872,7 +921,7 @@ export class MapEngine {
|
||||
heartRateDeviceText: deviceName,
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
}, true)
|
||||
})
|
||||
},
|
||||
onError: (message) => {
|
||||
this.clearHeartRateSignal()
|
||||
@@ -886,7 +935,7 @@ export class MapEngine {
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
statusText: `${message} (${this.buildVersion})`,
|
||||
}, true)
|
||||
})
|
||||
},
|
||||
onConnectionChange: (connected, deviceName) => {
|
||||
if (!connected) {
|
||||
@@ -906,17 +955,21 @@ export class MapEngine {
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
heartRateDiscoveredDevices: this.formatHeartRateDevices(this.heartRateController.discoveredDevices),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
}, true)
|
||||
})
|
||||
},
|
||||
onDeviceListChange: (devices) => {
|
||||
this.setState({
|
||||
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
}, true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState({
|
||||
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
|
||||
heartRateScanText: this.getHeartRateScanText(),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
}, true)
|
||||
}
|
||||
},
|
||||
onDebugStateChange: () => {
|
||||
this.setState(this.getHeartRateControllerViewPatch(), true)
|
||||
if (this.diagnosticUiEnabled) {
|
||||
this.setState(this.getHeartRateControllerViewPatch(), true)
|
||||
}
|
||||
},
|
||||
})
|
||||
this.feedbackDirector = new FeedbackDirector({
|
||||
@@ -1119,22 +1172,53 @@ export class MapEngine {
|
||||
this.autoRotateTimer = 0
|
||||
this.pendingViewPatch = {}
|
||||
this.mounted = false
|
||||
this.diagnosticUiEnabled = false
|
||||
this.northReferenceMode = DEFAULT_NORTH_REFERENCE_MODE
|
||||
this.sensorHeadingDeg = null
|
||||
this.smoothedSensorHeadingDeg = null
|
||||
this.compassDisplayHeadingDeg = null
|
||||
this.smoothedMovementHeadingDeg = null
|
||||
this.autoRotateHeadingDeg = null
|
||||
this.courseHeadingDeg = null
|
||||
this.targetAutoRotationDeg = null
|
||||
this.autoRotateSourceMode = 'smart'
|
||||
this.autoRotateCalibrationOffsetDeg = getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)
|
||||
this.autoRotateCalibrationPending = false
|
||||
this.lastStatsUiSyncAt = 0
|
||||
}
|
||||
|
||||
getInitialData(): MapEngineViewState {
|
||||
return { ...this.state }
|
||||
}
|
||||
|
||||
setDiagnosticUiEnabled(enabled: boolean): void {
|
||||
if (this.diagnosticUiEnabled === enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
this.diagnosticUiEnabled = enabled
|
||||
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
...this.getLocationControllerViewPatch(),
|
||||
...this.getHeartRateControllerViewPatch(),
|
||||
heartRateDiscoveredDevices: this.formatHeartRateDevices(this.heartRateController.discoveredDevices),
|
||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||
visibleTileCount: this.state.visibleTileCount,
|
||||
readyTileCount: this.state.readyTileCount,
|
||||
memoryTileCount: this.state.memoryTileCount,
|
||||
diskTileCount: this.state.diskTileCount,
|
||||
memoryHitCount: this.state.memoryHitCount,
|
||||
diskHitCount: this.state.diskHitCount,
|
||||
networkFetchCount: this.state.networkFetchCount,
|
||||
cacheHitRateText: this.state.cacheHitRateText,
|
||||
}, true)
|
||||
}
|
||||
|
||||
getGameInfoSnapshot(): MapEngineGameInfoSnapshot {
|
||||
const definition = this.gameRuntime.definition
|
||||
const sessionState = this.gameRuntime.state
|
||||
@@ -1253,12 +1337,14 @@ export class MapEngine {
|
||||
this.currentGpsTrack = []
|
||||
this.currentGpsAccuracyMeters = null
|
||||
this.currentGpsInsideMap = false
|
||||
this.smoothedMovementHeadingDeg = null
|
||||
this.courseOverlayVisible = false
|
||||
this.setCourseHeading(null)
|
||||
}
|
||||
|
||||
clearStartSessionResidue(): void {
|
||||
this.currentGpsTrack = []
|
||||
this.smoothedMovementHeadingDeg = null
|
||||
this.courseOverlayVisible = false
|
||||
this.setCourseHeading(null)
|
||||
}
|
||||
@@ -1534,7 +1620,7 @@ export class MapEngine {
|
||||
panelAverageSpeedUnitText: telemetryPresentation.averageSpeedUnitText,
|
||||
panelAccuracyValueText: telemetryPresentation.accuracyValueText,
|
||||
panelAccuracyUnitText: telemetryPresentation.accuracyUnitText,
|
||||
}, true)
|
||||
})
|
||||
}
|
||||
|
||||
updateSessionTimerLoop(): void {
|
||||
@@ -1798,6 +1884,7 @@ export class MapEngine {
|
||||
|
||||
this.currentGpsPoint = nextPoint
|
||||
this.currentGpsAccuracyMeters = accuracyMeters
|
||||
this.updateMovementHeadingDeg()
|
||||
|
||||
const gpsWorldPoint = lonLatToWorldTile(nextPoint, this.state.zoom)
|
||||
const gpsTileX = Math.floor(gpsWorldPoint.x)
|
||||
@@ -2167,7 +2254,7 @@ export class MapEngine {
|
||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
||||
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
|
||||
...this.getGameViewPatch(gameStatusText),
|
||||
}
|
||||
|
||||
@@ -2683,18 +2770,26 @@ export class MapEngine {
|
||||
const compassHeadingDeg = getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)
|
||||
this.compassDisplayHeadingDeg = this.compassDisplayHeadingDeg === null
|
||||
? compassHeadingDeg
|
||||
: interpolateAngleDeg(this.compassDisplayHeadingDeg, compassHeadingDeg, COMPASS_NEEDLE_SMOOTHING)
|
||||
: interpolateAngleDeg(
|
||||
this.compassDisplayHeadingDeg,
|
||||
compassHeadingDeg,
|
||||
getCompassNeedleSmoothingFactor(this.compassDisplayHeadingDeg, compassHeadingDeg),
|
||||
)
|
||||
|
||||
this.autoRotateHeadingDeg = this.resolveAutoRotateInputHeadingDeg()
|
||||
|
||||
this.setState({
|
||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg),
|
||||
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
|
||||
...(this.diagnosticUiEnabled
|
||||
? {
|
||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
if (!this.refreshAutoRotateTarget()) {
|
||||
@@ -2740,7 +2835,7 @@ export class MapEngine {
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.smoothedSensorHeadingDeg),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
||||
},
|
||||
`${formatNorthReferenceStatusText(nextMode)} (${this.buildVersion})`,
|
||||
@@ -2759,7 +2854,7 @@ export class MapEngine {
|
||||
...this.getTelemetrySensorViewPatch(),
|
||||
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
||||
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.smoothedSensorHeadingDeg),
|
||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
||||
statusText: `${formatNorthReferenceStatusText(nextMode)} (${this.buildVersion})`,
|
||||
}, true)
|
||||
@@ -2780,7 +2875,7 @@ export class MapEngine {
|
||||
}
|
||||
}
|
||||
|
||||
getMovementHeadingDeg(): number | null {
|
||||
getRawMovementHeadingDeg(): number | null {
|
||||
if (!this.currentGpsInsideMap) {
|
||||
return null
|
||||
}
|
||||
@@ -2809,6 +2904,23 @@ export class MapEngine {
|
||||
return null
|
||||
}
|
||||
|
||||
updateMovementHeadingDeg(): void {
|
||||
const rawMovementHeadingDeg = this.getRawMovementHeadingDeg()
|
||||
if (rawMovementHeadingDeg === null) {
|
||||
this.smoothedMovementHeadingDeg = null
|
||||
return
|
||||
}
|
||||
|
||||
const smoothingFactor = getMovementHeadingSmoothingFactor(this.telemetryRuntime.state.currentSpeedKmh)
|
||||
this.smoothedMovementHeadingDeg = this.smoothedMovementHeadingDeg === null
|
||||
? rawMovementHeadingDeg
|
||||
: interpolateAngleDeg(this.smoothedMovementHeadingDeg, rawMovementHeadingDeg, smoothingFactor)
|
||||
}
|
||||
|
||||
getMovementHeadingDeg(): number | null {
|
||||
return this.smoothedMovementHeadingDeg
|
||||
}
|
||||
|
||||
getPreferredSensorHeadingDeg(): number | null {
|
||||
return this.smoothedSensorHeadingDeg === null
|
||||
? null
|
||||
@@ -2959,7 +3071,7 @@ export class MapEngine {
|
||||
}
|
||||
|
||||
applyStats(stats: MapRendererStats): void {
|
||||
this.setState({
|
||||
const statsPatch = {
|
||||
visibleTileCount: stats.visibleTileCount,
|
||||
readyTileCount: stats.readyTileCount,
|
||||
memoryTileCount: stats.memoryTileCount,
|
||||
@@ -2968,7 +3080,27 @@ export class MapEngine {
|
||||
diskHitCount: stats.diskHitCount,
|
||||
networkFetchCount: stats.networkFetchCount,
|
||||
cacheHitRateText: formatCacheHitRate(stats.memoryHitCount, stats.diskHitCount, stats.networkFetchCount),
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.diagnosticUiEnabled) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...statsPatch,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
if (now - this.lastStatsUiSyncAt < 500) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...statsPatch,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.lastStatsUiSyncAt = now
|
||||
this.setState(statsPatch)
|
||||
}
|
||||
|
||||
setState(patch: Partial<MapEngineViewState>, immediateUi = false): void {
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface CompassHeadingControllerCallbacks {
|
||||
|
||||
type SensorSource = 'compass' | 'motion' | null
|
||||
|
||||
const ABSOLUTE_HEADING_CORRECTION = 0.24
|
||||
const ABSOLUTE_HEADING_CORRECTION = 0.44
|
||||
|
||||
function normalizeHeadingDeg(headingDeg: number): number {
|
||||
const normalized = headingDeg % 360
|
||||
@@ -202,5 +202,3 @@ export class CompassHeadingController {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ type MapPageData = MapEngineViewState & {
|
||||
showDebugPanel: boolean
|
||||
showGameInfoPanel: boolean
|
||||
showCenterScaleRuler: boolean
|
||||
showPunchHintBanner: boolean
|
||||
centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
|
||||
statusBarHeight: number
|
||||
topInsetHeight: number
|
||||
@@ -74,11 +75,150 @@ type MapPageData = MapEngineViewState & {
|
||||
showRightButtonGroups: boolean
|
||||
showBottomDebugButton: boolean
|
||||
}
|
||||
const INTERNAL_BUILD_VERSION = 'map-build-252'
|
||||
const INTERNAL_BUILD_VERSION = 'map-build-261'
|
||||
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'
|
||||
const PUNCH_HINT_AUTO_HIDE_MS = 30000
|
||||
let mapEngine: MapEngine | null = null
|
||||
let stageCanvasAttached = false
|
||||
let gameInfoPanelSyncTimer = 0
|
||||
let centerScaleRulerSyncTimer = 0
|
||||
let punchHintDismissTimer = 0
|
||||
|
||||
const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
|
||||
'buildVersion',
|
||||
'renderMode',
|
||||
'projectionMode',
|
||||
'mapReady',
|
||||
'mapReadyText',
|
||||
'mapName',
|
||||
'configStatusText',
|
||||
'sensorHeadingText',
|
||||
'deviceHeadingText',
|
||||
'devicePoseText',
|
||||
'headingConfidenceText',
|
||||
'accelerometerText',
|
||||
'gyroscopeText',
|
||||
'deviceMotionText',
|
||||
'compassDeclinationText',
|
||||
'northReferenceButtonText',
|
||||
'autoRotateSourceText',
|
||||
'autoRotateCalibrationText',
|
||||
'northReferenceText',
|
||||
'centerText',
|
||||
'tileSource',
|
||||
'visibleTileCount',
|
||||
'readyTileCount',
|
||||
'memoryTileCount',
|
||||
'diskTileCount',
|
||||
'memoryHitCount',
|
||||
'diskHitCount',
|
||||
'networkFetchCount',
|
||||
'cacheHitRateText',
|
||||
'locationSourceMode',
|
||||
'locationSourceText',
|
||||
'mockBridgeConnected',
|
||||
'mockBridgeStatusText',
|
||||
'mockBridgeUrlText',
|
||||
'mockCoordText',
|
||||
'mockSpeedText',
|
||||
'gpsCoordText',
|
||||
'heartRateSourceMode',
|
||||
'heartRateSourceText',
|
||||
'heartRateConnected',
|
||||
'heartRateStatusText',
|
||||
'heartRateDeviceText',
|
||||
'heartRateScanText',
|
||||
'heartRateDiscoveredDevices',
|
||||
'mockHeartRateBridgeConnected',
|
||||
'mockHeartRateBridgeStatusText',
|
||||
'mockHeartRateBridgeUrlText',
|
||||
'mockHeartRateText',
|
||||
])
|
||||
|
||||
const CENTER_SCALE_RULER_DEP_KEYS = new Set<string>([
|
||||
'showCenterScaleRuler',
|
||||
'centerScaleRulerAnchorMode',
|
||||
'stageWidth',
|
||||
'stageHeight',
|
||||
'topInsetHeight',
|
||||
'zoom',
|
||||
'centerTileY',
|
||||
'tileSizePx',
|
||||
'previewScale',
|
||||
])
|
||||
|
||||
const RULER_ONLY_VIEW_KEYS = new Set<string>([
|
||||
'zoom',
|
||||
'centerTileX',
|
||||
'centerTileY',
|
||||
'tileSizePx',
|
||||
'previewScale',
|
||||
'stageWidth',
|
||||
'stageHeight',
|
||||
'stageLeft',
|
||||
'stageTop',
|
||||
])
|
||||
|
||||
const SIDE_BUTTON_DEP_KEYS = new Set<string>([
|
||||
'sideButtonMode',
|
||||
'showGameInfoPanel',
|
||||
'showCenterScaleRuler',
|
||||
'centerScaleRulerAnchorMode',
|
||||
'skipButtonEnabled',
|
||||
'gameSessionStatus',
|
||||
'gpsLockEnabled',
|
||||
'gpsLockAvailable',
|
||||
])
|
||||
|
||||
function hasAnyPatchKey(patch: Record<string, unknown>, keys: Set<string>): boolean {
|
||||
return Object.keys(patch).some((key) => keys.has(key))
|
||||
}
|
||||
|
||||
function filterDebugOnlyPatch(
|
||||
patch: Partial<MapPageData>,
|
||||
includeDebugFields: boolean,
|
||||
includeRulerFields: boolean,
|
||||
): Partial<MapPageData> {
|
||||
if (includeDebugFields && includeRulerFields) {
|
||||
return patch
|
||||
}
|
||||
|
||||
const filteredPatch: Partial<MapPageData> = {}
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (!includeDebugFields && DEBUG_ONLY_VIEW_KEYS.has(key)) {
|
||||
continue
|
||||
}
|
||||
if (!includeRulerFields && RULER_ONLY_VIEW_KEYS.has(key)) {
|
||||
continue
|
||||
}
|
||||
{
|
||||
;(filteredPatch as Record<string, unknown>)[key] = value
|
||||
}
|
||||
}
|
||||
return filteredPatch
|
||||
}
|
||||
|
||||
function clearGameInfoPanelSyncTimer() {
|
||||
if (gameInfoPanelSyncTimer) {
|
||||
clearTimeout(gameInfoPanelSyncTimer)
|
||||
gameInfoPanelSyncTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
function clearCenterScaleRulerSyncTimer() {
|
||||
if (centerScaleRulerSyncTimer) {
|
||||
clearTimeout(centerScaleRulerSyncTimer)
|
||||
centerScaleRulerSyncTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
function clearPunchHintDismissTimer() {
|
||||
if (punchHintDismissTimer) {
|
||||
clearTimeout(punchHintDismissTimer)
|
||||
punchHintDismissTimer = 0
|
||||
}
|
||||
}
|
||||
function buildSideButtonVisibility(mode: SideButtonMode) {
|
||||
return {
|
||||
sideButtonMode: mode,
|
||||
@@ -389,6 +529,7 @@ Page({
|
||||
panelDistanceValueText: '--',
|
||||
panelDistanceUnitText: '',
|
||||
panelProgressText: '0/0',
|
||||
showPunchHintBanner: true,
|
||||
gameSessionStatus: 'idle',
|
||||
gameModeText: '顺序赛',
|
||||
gpsLockEnabled: false,
|
||||
@@ -488,9 +629,11 @@ Page({
|
||||
mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
|
||||
onData: (patch) => {
|
||||
const nextPatch = patch as Partial<MapPageData>
|
||||
const nextData: Partial<MapPageData> = {
|
||||
const includeDebugFields = this.data.showDebugPanel
|
||||
const includeRulerFields = this.data.showCenterScaleRuler
|
||||
const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
|
||||
...nextPatch,
|
||||
}
|
||||
}, includeDebugFields, includeRulerFields)
|
||||
|
||||
if (
|
||||
typeof nextPatch.mockBridgeUrlText === 'string'
|
||||
@@ -511,18 +654,52 @@ Page({
|
||||
...nextData,
|
||||
} as MapPageData
|
||||
|
||||
this.setData({
|
||||
...nextData,
|
||||
...buildCenterScaleRulerPatch(mergedData),
|
||||
...buildSideButtonState(mergedData),
|
||||
})
|
||||
const derivedPatch: Partial<MapPageData> = {}
|
||||
if (
|
||||
this.data.showCenterScaleRuler
|
||||
&& hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
|
||||
) {
|
||||
Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
|
||||
}
|
||||
|
||||
if (hasAnyPatchKey(nextPatch as Record<string, unknown>, SIDE_BUTTON_DEP_KEYS)) {
|
||||
Object.assign(derivedPatch, buildSideButtonState(mergedData))
|
||||
}
|
||||
|
||||
if (typeof nextPatch.punchHintText === 'string') {
|
||||
const nextHintText = nextPatch.punchHintText.trim()
|
||||
if (nextHintText !== this.data.punchHintText) {
|
||||
clearPunchHintDismissTimer()
|
||||
nextData.showPunchHintBanner = nextHintText.length > 0
|
||||
if (nextHintText.length > 0) {
|
||||
punchHintDismissTimer = setTimeout(() => {
|
||||
punchHintDismissTimer = 0
|
||||
this.setData({
|
||||
showPunchHintBanner: false,
|
||||
})
|
||||
}, PUNCH_HINT_AUTO_HIDE_MS) as unknown as number
|
||||
}
|
||||
} else if (!nextHintText) {
|
||||
clearPunchHintDismissTimer()
|
||||
nextData.showPunchHintBanner = false
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
|
||||
this.setData({
|
||||
...nextData,
|
||||
...derivedPatch,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.data.showGameInfoPanel) {
|
||||
this.syncGameInfoPanelSnapshot()
|
||||
this.scheduleGameInfoPanelSnapshotSync()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
mapEngine.setDiagnosticUiEnabled(false)
|
||||
|
||||
this.setData({
|
||||
...mapEngine.getInitialData(),
|
||||
showDebugPanel: false,
|
||||
@@ -542,6 +719,7 @@ Page({
|
||||
panelDistanceValueText: '--',
|
||||
panelDistanceUnitText: '',
|
||||
panelProgressText: '0/0',
|
||||
showPunchHintBanner: true,
|
||||
gameSessionStatus: 'idle',
|
||||
gameModeText: '顺序赛',
|
||||
gpsLockEnabled: false,
|
||||
@@ -647,6 +825,9 @@ Page({
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
clearGameInfoPanelSyncTimer()
|
||||
clearCenterScaleRulerSyncTimer()
|
||||
clearPunchHintDismissTimer()
|
||||
if (mapEngine) {
|
||||
mapEngine.destroy()
|
||||
mapEngine = null
|
||||
@@ -686,7 +867,7 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
measureStageAndCanvas() {
|
||||
measureStageAndCanvas(onApplied?: () => void) {
|
||||
const page = this
|
||||
const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
|
||||
const fallbackRect = getFallbackStageRect()
|
||||
@@ -703,6 +884,9 @@ Page({
|
||||
}
|
||||
|
||||
currentEngine.setStage(rect)
|
||||
if (onApplied) {
|
||||
onApplied()
|
||||
}
|
||||
|
||||
if (stageCanvasAttached) {
|
||||
return
|
||||
@@ -1053,7 +1237,26 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
scheduleGameInfoPanelSnapshotSync() {
|
||||
if (!this.data.showGameInfoPanel) {
|
||||
clearGameInfoPanelSyncTimer()
|
||||
return
|
||||
}
|
||||
|
||||
if (gameInfoPanelSyncTimer) {
|
||||
return
|
||||
}
|
||||
|
||||
gameInfoPanelSyncTimer = setTimeout(() => {
|
||||
gameInfoPanelSyncTimer = 0
|
||||
if (this.data.showGameInfoPanel) {
|
||||
this.syncGameInfoPanelSnapshot()
|
||||
}
|
||||
}, 400) as unknown as number
|
||||
},
|
||||
|
||||
handleOpenGameInfoPanel() {
|
||||
clearGameInfoPanelSyncTimer()
|
||||
this.syncGameInfoPanelSnapshot()
|
||||
this.setData({
|
||||
showDebugPanel: false,
|
||||
@@ -1072,6 +1275,7 @@ Page({
|
||||
},
|
||||
|
||||
handleCloseGameInfoPanel() {
|
||||
clearGameInfoPanelSyncTimer()
|
||||
this.setData({
|
||||
showGameInfoPanel: false,
|
||||
...buildSideButtonState({
|
||||
@@ -1107,6 +1311,13 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
handleClosePunchHint() {
|
||||
clearPunchHintDismissTimer()
|
||||
this.setData({
|
||||
showPunchHintBanner: false,
|
||||
})
|
||||
},
|
||||
|
||||
handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
|
||||
this.setData({
|
||||
hudPanelIndex: event.detail.current || 0,
|
||||
@@ -1147,8 +1358,15 @@ Page({
|
||||
mapEngine.handleSetHeadingUpMode()
|
||||
},
|
||||
handleToggleDebugPanel() {
|
||||
const nextShowDebugPanel = !this.data.showDebugPanel
|
||||
if (!nextShowDebugPanel) {
|
||||
clearGameInfoPanelSyncTimer()
|
||||
}
|
||||
if (mapEngine) {
|
||||
mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
|
||||
}
|
||||
this.setData({
|
||||
showDebugPanel: !this.data.showDebugPanel,
|
||||
showDebugPanel: nextShowDebugPanel,
|
||||
showGameInfoPanel: false,
|
||||
...buildSideButtonState({
|
||||
sideButtonMode: this.data.sideButtonMode,
|
||||
@@ -1164,6 +1382,9 @@ Page({
|
||||
},
|
||||
|
||||
handleCloseDebugPanel() {
|
||||
if (mapEngine) {
|
||||
mapEngine.setDiagnosticUiEnabled(false)
|
||||
}
|
||||
this.setData({
|
||||
showDebugPanel: false,
|
||||
...buildSideButtonState({
|
||||
@@ -1182,16 +1403,51 @@ Page({
|
||||
handleToggleCenterScaleRuler() {
|
||||
const nextEnabled = !this.data.showCenterScaleRuler
|
||||
this.data.showCenterScaleRuler = nextEnabled
|
||||
const mergedData = {
|
||||
...this.data,
|
||||
showCenterScaleRuler: nextEnabled,
|
||||
} as MapPageData
|
||||
clearCenterScaleRulerSyncTimer()
|
||||
|
||||
const syncRulerFromEngine = () => {
|
||||
if (!mapEngine) {
|
||||
return
|
||||
}
|
||||
const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
|
||||
const mergedData = {
|
||||
...engineSnapshot,
|
||||
...this.data,
|
||||
showCenterScaleRuler: nextEnabled,
|
||||
} as MapPageData
|
||||
|
||||
this.setData({
|
||||
...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
|
||||
showCenterScaleRuler: nextEnabled,
|
||||
...buildCenterScaleRulerPatch(mergedData),
|
||||
...buildSideButtonState(mergedData),
|
||||
})
|
||||
}
|
||||
|
||||
if (!nextEnabled) {
|
||||
syncRulerFromEngine()
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
showCenterScaleRuler: nextEnabled,
|
||||
...buildCenterScaleRulerPatch(mergedData),
|
||||
...buildSideButtonState(mergedData),
|
||||
showCenterScaleRuler: true,
|
||||
...buildSideButtonState({
|
||||
...this.data,
|
||||
showCenterScaleRuler: true,
|
||||
} as MapPageData),
|
||||
})
|
||||
|
||||
this.measureStageAndCanvas(() => {
|
||||
syncRulerFromEngine()
|
||||
})
|
||||
|
||||
centerScaleRulerSyncTimer = setTimeout(() => {
|
||||
centerScaleRulerSyncTimer = 0
|
||||
if (!this.data.showCenterScaleRuler) {
|
||||
return
|
||||
}
|
||||
syncRulerFromEngine()
|
||||
}, 96) as unknown as number
|
||||
},
|
||||
|
||||
handleToggleCenterScaleRulerAnchor() {
|
||||
@@ -1202,13 +1458,16 @@ Page({
|
||||
const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
|
||||
? 'compass-center'
|
||||
: 'screen-center'
|
||||
const engineSnapshot = mapEngine ? (mapEngine.getInitialData() as Partial<MapPageData>) : {}
|
||||
this.data.centerScaleRulerAnchorMode = nextAnchorMode
|
||||
const mergedData = {
|
||||
...engineSnapshot,
|
||||
...this.data,
|
||||
centerScaleRulerAnchorMode: nextAnchorMode,
|
||||
} as MapPageData
|
||||
|
||||
this.setData({
|
||||
...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, true),
|
||||
centerScaleRulerAnchorMode: nextAnchorMode,
|
||||
...buildCenterScaleRulerPatch(mergedData),
|
||||
...buildSideButtonState(mergedData),
|
||||
|
||||
@@ -28,7 +28,10 @@
|
||||
<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>
|
||||
|
||||
<view class="game-punch-hint" wx:if="{{punchHintText}}">{{punchHintText}}</view>
|
||||
<view class="game-punch-hint" wx:if="{{showPunchHintBanner && punchHintText}}" style="top: {{topInsetHeight}}px;">
|
||||
<view class="game-punch-hint__text">{{punchHintText}}</view>
|
||||
<view class="game-punch-hint__close" bindtap="handleClosePunchHint">×</view>
|
||||
</view>
|
||||
<view class="game-punch-feedback game-punch-feedback--{{punchFeedbackTone}} {{punchFeedbackFxClass}}" wx:if="{{punchFeedbackVisible}}">{{punchFeedbackText}}</view>
|
||||
<view class="game-content-card {{contentCardFxClass}}" wx:if="{{contentCardVisible}}" bindtap="handleCloseContentCard">
|
||||
<view class="game-content-card__title">{{contentCardTitle}}</view>
|
||||
|
||||
@@ -1580,18 +1580,40 @@
|
||||
.game-punch-hint {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 280rpx;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
max-width: 72vw;
|
||||
padding: 14rpx 24rpx;
|
||||
max-width: calc(100vw - 112rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 14rpx 18rpx 14rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(18, 33, 24, 0.78);
|
||||
color: #f7fbf2;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
z-index: 16;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.game-punch-hint__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.game-punch-hint__close {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
flex: 0 0 40rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(247, 251, 242, 0.9);
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.game-punch-feedback {
|
||||
|
||||
Reference in New Issue
Block a user