|
|
@@ -6,12 +6,17 @@ 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 { GameRuntime } from '../../game/core/gameRuntime'
|
|
|
+import { type GameEffect } from '../../game/core/gameResult'
|
|
|
+import { buildGameDefinitionFromCourse } from '../../game/content/courseToGameDefinition'
|
|
|
+import { SoundDirector } from '../../game/audio/soundDirector'
|
|
|
+import { EMPTY_GAME_PRESENTATION_STATE, type GamePresentationState } from '../../game/presentation/presentationState'
|
|
|
|
|
|
const RENDER_MODE = 'Single WebGL Pipeline'
|
|
|
const PROJECTION_MODE = 'WGS84 -> WorldTile -> Camera -> Screen'
|
|
|
const MAP_NORTH_OFFSET_DEG = 0
|
|
|
let MAGNETIC_DECLINATION_DEG = -6.91
|
|
|
-let MAGNETIC_DECLINATION_TEXT = '6.91° W'
|
|
|
+let MAGNETIC_DECLINATION_TEXT = '6.91掳 W'
|
|
|
const MIN_ZOOM = 15
|
|
|
const MAX_ZOOM = 20
|
|
|
const DEFAULT_ZOOM = 17
|
|
|
@@ -112,6 +117,17 @@ export interface MapEngineViewState {
|
|
|
gpsTracking: boolean
|
|
|
gpsTrackingText: string
|
|
|
gpsCoordText: string
|
|
|
+ gameSessionStatus: 'idle' | 'running' | 'finished' | 'failed'
|
|
|
+ panelProgressText: string
|
|
|
+ punchButtonText: string
|
|
|
+ punchButtonEnabled: boolean
|
|
|
+ punchHintText: string
|
|
|
+ punchFeedbackVisible: boolean
|
|
|
+ punchFeedbackText: string
|
|
|
+ punchFeedbackTone: 'neutral' | 'success' | 'warning'
|
|
|
+ contentCardVisible: boolean
|
|
|
+ contentCardTitle: string
|
|
|
+ contentCardBody: string
|
|
|
osmReferenceEnabled: boolean
|
|
|
osmReferenceText: string
|
|
|
}
|
|
|
@@ -158,6 +174,17 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|
|
'gpsTracking',
|
|
|
'gpsTrackingText',
|
|
|
'gpsCoordText',
|
|
|
+ 'gameSessionStatus',
|
|
|
+ 'panelProgressText',
|
|
|
+ 'punchButtonText',
|
|
|
+ 'punchButtonEnabled',
|
|
|
+ 'punchHintText',
|
|
|
+ 'punchFeedbackVisible',
|
|
|
+ 'punchFeedbackText',
|
|
|
+ 'punchFeedbackTone',
|
|
|
+ 'contentCardVisible',
|
|
|
+ 'contentCardTitle',
|
|
|
+ 'contentCardBody',
|
|
|
'osmReferenceEnabled',
|
|
|
'osmReferenceText',
|
|
|
]
|
|
|
@@ -216,7 +243,7 @@ function formatHeadingText(headingDeg: number | null): string {
|
|
|
return '--'
|
|
|
}
|
|
|
|
|
|
- return `${Math.round(normalizeRotationDeg(headingDeg))}°`
|
|
|
+ return `${Math.round(normalizeRotationDeg(headingDeg))}掳`
|
|
|
}
|
|
|
|
|
|
function formatOrientationModeText(mode: OrientationMode): string {
|
|
|
@@ -244,7 +271,7 @@ function formatRotationToggleText(mode: OrientationMode): string {
|
|
|
return '切到朝向朝上'
|
|
|
}
|
|
|
|
|
|
- return '切到手动旋转'
|
|
|
+ return '鍒囧埌鎵嬪姩鏃嬭浆'
|
|
|
}
|
|
|
|
|
|
function formatAutoRotateSourceText(mode: AutoRotateSourceMode, hasCourseHeading: boolean): string {
|
|
|
@@ -324,7 +351,7 @@ function formatCompassDeclinationText(mode: NorthReferenceMode): string {
|
|
|
}
|
|
|
|
|
|
function formatNorthReferenceButtonText(mode: NorthReferenceMode): string {
|
|
|
- return mode === 'magnetic' ? '北参考:磁北' : '北参考:真北'
|
|
|
+ return mode === 'magnetic' ? '鍖楀弬鑰冿細纾佸寳' : '鍖楀弬鑰冿細鐪熷寳'
|
|
|
}
|
|
|
|
|
|
function formatNorthReferenceStatusText(mode: NorthReferenceMode): string {
|
|
|
@@ -371,7 +398,7 @@ function formatGpsCoordText(point: LonLatPoint | null, accuracyMeters: number |
|
|
|
return base
|
|
|
}
|
|
|
|
|
|
- return `${base} / ±${Math.round(accuracyMeters)}m`
|
|
|
+ return `${base} / 卤${Math.round(accuracyMeters)}m`
|
|
|
}
|
|
|
|
|
|
function getApproxDistanceMeters(a: LonLatPoint, b: LonLatPoint): number {
|
|
|
@@ -381,11 +408,22 @@ function getApproxDistanceMeters(a: LonLatPoint, b: LonLatPoint): number {
|
|
|
return Math.sqrt(dx * dx + dy * dy)
|
|
|
}
|
|
|
|
|
|
+function getInitialBearingDeg(from: LonLatPoint, to: LonLatPoint): number {
|
|
|
+ const fromLatRad = from.lat * Math.PI / 180
|
|
|
+ const toLatRad = to.lat * Math.PI / 180
|
|
|
+ const deltaLonRad = (to.lon - from.lon) * Math.PI / 180
|
|
|
+ const y = Math.sin(deltaLonRad) * Math.cos(toLatRad)
|
|
|
+ const x = Math.cos(fromLatRad) * Math.sin(toLatRad) - Math.sin(fromLatRad) * Math.cos(toLatRad) * Math.cos(deltaLonRad)
|
|
|
+ const bearingDeg = Math.atan2(y, x) * 180 / Math.PI
|
|
|
+ return normalizeRotationDeg(bearingDeg)
|
|
|
+}
|
|
|
+
|
|
|
export class MapEngine {
|
|
|
buildVersion: string
|
|
|
renderer: WebGLMapRenderer
|
|
|
compassController: CompassHeadingController
|
|
|
locationController: LocationController
|
|
|
+ soundDirector: SoundDirector
|
|
|
onData: (patch: Partial<MapEngineViewState>) => void
|
|
|
state: MapEngineViewState
|
|
|
previewScale: number
|
|
|
@@ -430,6 +468,14 @@ export class MapEngine {
|
|
|
currentGpsAccuracyMeters: number | null
|
|
|
courseData: OrienteeringCourseData | null
|
|
|
cpRadiusMeters: number
|
|
|
+ gameRuntime: GameRuntime
|
|
|
+ gamePresentation: GamePresentationState
|
|
|
+ gameMode: 'classic-sequential'
|
|
|
+ punchPolicy: 'enter' | 'enter-confirm'
|
|
|
+ punchRadiusMeters: number
|
|
|
+ autoFinishOnLastControl: boolean
|
|
|
+ punchFeedbackTimer: number
|
|
|
+ contentCardTimer: number
|
|
|
hasGpsCenteredOnce: boolean
|
|
|
|
|
|
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
|
|
|
@@ -471,6 +517,7 @@ export class MapEngine {
|
|
|
}, true)
|
|
|
},
|
|
|
})
|
|
|
+ this.soundDirector = new SoundDirector()
|
|
|
this.minZoom = MIN_ZOOM
|
|
|
this.maxZoom = MAX_ZOOM
|
|
|
this.defaultZoom = DEFAULT_ZOOM
|
|
|
@@ -482,6 +529,14 @@ export class MapEngine {
|
|
|
this.currentGpsAccuracyMeters = null
|
|
|
this.courseData = null
|
|
|
this.cpRadiusMeters = 5
|
|
|
+ this.gameRuntime = new GameRuntime()
|
|
|
+ this.gamePresentation = EMPTY_GAME_PRESENTATION_STATE
|
|
|
+ this.gameMode = 'classic-sequential'
|
|
|
+ this.punchPolicy = 'enter-confirm'
|
|
|
+ this.punchRadiusMeters = 5
|
|
|
+ this.autoFinishOnLastControl = true
|
|
|
+ this.punchFeedbackTimer = 0
|
|
|
+ this.contentCardTimer = 0
|
|
|
this.hasGpsCenteredOnce = false
|
|
|
this.state = {
|
|
|
buildVersion: this.buildVersion,
|
|
|
@@ -489,7 +544,7 @@ export class MapEngine {
|
|
|
projectionMode: PROJECTION_MODE,
|
|
|
mapReady: false,
|
|
|
mapReadyText: 'BOOTING',
|
|
|
- mapName: 'LCX 测试地图',
|
|
|
+ mapName: 'LCX 娴嬭瘯鍦板浘',
|
|
|
configStatusText: '远程配置待加载',
|
|
|
zoom: DEFAULT_ZOOM,
|
|
|
rotationDeg: 0,
|
|
|
@@ -502,7 +557,7 @@ export class MapEngine {
|
|
|
sensorHeadingText: '--',
|
|
|
compassDeclinationText: formatCompassDeclinationText(DEFAULT_NORTH_REFERENCE_MODE),
|
|
|
northReferenceButtonText: formatNorthReferenceButtonText(DEFAULT_NORTH_REFERENCE_MODE),
|
|
|
- autoRotateSourceText: formatAutoRotateSourceText('fusion', false),
|
|
|
+ autoRotateSourceText: formatAutoRotateSourceText('sensor', false),
|
|
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)),
|
|
|
northReferenceText: formatNorthReferenceText(DEFAULT_NORTH_REFERENCE_MODE),
|
|
|
compassNeedleDeg: 0,
|
|
|
@@ -526,10 +581,21 @@ export class MapEngine {
|
|
|
stageHeight: 0,
|
|
|
stageLeft: 0,
|
|
|
stageTop: 0,
|
|
|
- statusText: `单 WebGL 管线已准备接入方向传感器 (${this.buildVersion})`,
|
|
|
+ statusText: `鍗?WebGL 绠$嚎宸插噯澶囨帴鍏ユ柟鍚戜紶鎰熷櫒 (${this.buildVersion})`,
|
|
|
gpsTracking: false,
|
|
|
gpsTrackingText: '持续定位待启动',
|
|
|
gpsCoordText: '--',
|
|
|
+ panelProgressText: '0/0',
|
|
|
+ punchButtonText: '鎵撶偣',
|
|
|
+ gameSessionStatus: 'idle',
|
|
|
+ punchButtonEnabled: false,
|
|
|
+ punchHintText: '绛夊緟杩涘叆妫€鏌ョ偣鑼冨洿',
|
|
|
+ punchFeedbackVisible: false,
|
|
|
+ punchFeedbackText: '',
|
|
|
+ punchFeedbackTone: 'neutral',
|
|
|
+ contentCardVisible: false,
|
|
|
+ contentCardTitle: '',
|
|
|
+ contentCardBody: '',
|
|
|
osmReferenceEnabled: false,
|
|
|
osmReferenceText: 'OSM参考:关',
|
|
|
}
|
|
|
@@ -561,7 +627,7 @@ export class MapEngine {
|
|
|
this.autoRotateHeadingDeg = null
|
|
|
this.courseHeadingDeg = null
|
|
|
this.targetAutoRotationDeg = null
|
|
|
- this.autoRotateSourceMode = 'fusion'
|
|
|
+ this.autoRotateSourceMode = 'sensor'
|
|
|
this.autoRotateCalibrationOffsetDeg = getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)
|
|
|
this.autoRotateCalibrationPending = false
|
|
|
}
|
|
|
@@ -575,13 +641,222 @@ export class MapEngine {
|
|
|
this.clearPreviewResetTimer()
|
|
|
this.clearViewSyncTimer()
|
|
|
this.clearAutoRotateTimer()
|
|
|
+ this.clearPunchFeedbackTimer()
|
|
|
+ this.clearContentCardTimer()
|
|
|
this.compassController.destroy()
|
|
|
this.locationController.destroy()
|
|
|
+ this.soundDirector.destroy()
|
|
|
this.renderer.destroy()
|
|
|
this.mounted = false
|
|
|
}
|
|
|
|
|
|
|
|
|
+ clearGameRuntime(): void {
|
|
|
+ this.gameRuntime.clear()
|
|
|
+ this.gamePresentation = EMPTY_GAME_PRESENTATION_STATE
|
|
|
+ this.setCourseHeading(null)
|
|
|
+ }
|
|
|
+
|
|
|
+ loadGameDefinitionFromCourse(): GameEffect[] {
|
|
|
+ if (!this.courseData) {
|
|
|
+ this.clearGameRuntime()
|
|
|
+ return []
|
|
|
+ }
|
|
|
+
|
|
|
+ const definition = buildGameDefinitionFromCourse(
|
|
|
+ this.courseData,
|
|
|
+ this.cpRadiusMeters,
|
|
|
+ this.gameMode,
|
|
|
+ this.autoFinishOnLastControl,
|
|
|
+ this.punchPolicy,
|
|
|
+ this.punchRadiusMeters,
|
|
|
+ )
|
|
|
+ const result = this.gameRuntime.loadDefinition(definition)
|
|
|
+ this.gamePresentation = result.presentation
|
|
|
+ this.refreshCourseHeadingFromPresentation()
|
|
|
+ return result.effects
|
|
|
+ }
|
|
|
+
|
|
|
+ refreshCourseHeadingFromPresentation(): void {
|
|
|
+ if (!this.courseData || !this.gamePresentation.activeLegIndices.length) {
|
|
|
+ this.setCourseHeading(null)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const activeLegIndex = this.gamePresentation.activeLegIndices[0]
|
|
|
+ const activeLeg = this.courseData.layers.legs[activeLegIndex]
|
|
|
+ if (!activeLeg) {
|
|
|
+ this.setCourseHeading(null)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.setCourseHeading(getInitialBearingDeg(activeLeg.fromPoint, activeLeg.toPoint))
|
|
|
+ }
|
|
|
+
|
|
|
+ resolveGameStatusText(effects: GameEffect[]): string | null {
|
|
|
+ const lastEffect = effects.length ? effects[effects.length - 1] : null
|
|
|
+ if (!lastEffect) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lastEffect.type === 'control_completed') {
|
|
|
+ const sequenceText = typeof lastEffect.sequence === 'number' ? String(lastEffect.sequence) : lastEffect.controlId
|
|
|
+ return `宸插畬鎴愭鏌ョ偣 ${sequenceText} (${this.buildVersion})`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lastEffect.type === 'session_finished') {
|
|
|
+ return `璺嚎宸插畬鎴?(${this.buildVersion})`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lastEffect.type === 'session_started') {
|
|
|
+ return `椤哄簭鎵撶偣宸插紑濮?(${this.buildVersion})`
|
|
|
+ }
|
|
|
+
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ getGameViewPatch(statusText?: string | null): Partial<MapEngineViewState> {
|
|
|
+ const patch: Partial<MapEngineViewState> = {
|
|
|
+ gameSessionStatus: this.gameRuntime.state ? this.gameRuntime.state.status : 'idle',
|
|
|
+ panelProgressText: this.gamePresentation.progressText,
|
|
|
+ punchButtonText: this.gamePresentation.punchButtonText,
|
|
|
+ punchButtonEnabled: this.gamePresentation.punchButtonEnabled,
|
|
|
+ punchHintText: this.gamePresentation.punchHintText,
|
|
|
+ }
|
|
|
+
|
|
|
+ if (statusText) {
|
|
|
+ patch.statusText = statusText
|
|
|
+ }
|
|
|
+
|
|
|
+ return patch
|
|
|
+ }
|
|
|
+
|
|
|
+ clearPunchFeedbackTimer(): void {
|
|
|
+ if (this.punchFeedbackTimer) {
|
|
|
+ clearTimeout(this.punchFeedbackTimer)
|
|
|
+ this.punchFeedbackTimer = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ clearContentCardTimer(): void {
|
|
|
+ if (this.contentCardTimer) {
|
|
|
+ clearTimeout(this.contentCardTimer)
|
|
|
+ this.contentCardTimer = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showPunchFeedback(text: string, tone: 'neutral' | 'success' | 'warning'): void {
|
|
|
+ this.clearPunchFeedbackTimer()
|
|
|
+ this.setState({
|
|
|
+ punchFeedbackVisible: true,
|
|
|
+ punchFeedbackText: text,
|
|
|
+ punchFeedbackTone: tone,
|
|
|
+ }, true)
|
|
|
+ this.punchFeedbackTimer = setTimeout(() => {
|
|
|
+ this.punchFeedbackTimer = 0
|
|
|
+ this.setState({
|
|
|
+ punchFeedbackVisible: false,
|
|
|
+ }, true)
|
|
|
+ }, 1400) as unknown as number
|
|
|
+ }
|
|
|
+
|
|
|
+ showContentCard(title: string, body: string): void {
|
|
|
+ this.clearContentCardTimer()
|
|
|
+ this.setState({
|
|
|
+ contentCardVisible: true,
|
|
|
+ contentCardTitle: title,
|
|
|
+ contentCardBody: body,
|
|
|
+ }, true)
|
|
|
+ this.contentCardTimer = setTimeout(() => {
|
|
|
+ this.contentCardTimer = 0
|
|
|
+ this.setState({
|
|
|
+ contentCardVisible: false,
|
|
|
+ }, true)
|
|
|
+ }, 2600) as unknown as number
|
|
|
+ }
|
|
|
+
|
|
|
+ closeContentCard(): void {
|
|
|
+ this.clearContentCardTimer()
|
|
|
+ this.setState({
|
|
|
+ contentCardVisible: false,
|
|
|
+ }, true)
|
|
|
+ }
|
|
|
+
|
|
|
+ applyGameEffects(effects: GameEffect[]): string | null {
|
|
|
+ this.soundDirector.handleEffects(effects)
|
|
|
+ const statusText = this.resolveGameStatusText(effects)
|
|
|
+ for (const effect of effects) {
|
|
|
+ if (effect.type === 'punch_feedback') {
|
|
|
+ this.showPunchFeedback(effect.text, effect.tone)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (effect.type === 'control_completed') {
|
|
|
+ this.showPunchFeedback(`完成 ${typeof effect.sequence === 'number' ? effect.sequence : effect.label}`, 'success')
|
|
|
+ this.showContentCard(effect.displayTitle, effect.displayBody)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (effect.type === 'session_finished' && this.locationController.listening) {
|
|
|
+ this.locationController.stop()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return statusText
|
|
|
+ }
|
|
|
+
|
|
|
+ handleStartGame(): void {
|
|
|
+ if (!this.gameRuntime.definition || !this.gameRuntime.state) {
|
|
|
+ this.setState({
|
|
|
+ statusText: `当前还没有可开始的路线 (${this.buildVersion})`,
|
|
|
+ }, true)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.gameRuntime.state.status !== 'idle') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.locationController.listening) {
|
|
|
+ this.locationController.start()
|
|
|
+ }
|
|
|
+
|
|
|
+ const startedAt = Date.now()
|
|
|
+ let gameResult = this.gameRuntime.startSession(startedAt)
|
|
|
+ if (this.currentGpsPoint) {
|
|
|
+ gameResult = this.gameRuntime.dispatch({
|
|
|
+ type: 'gps_updated',
|
|
|
+ at: Date.now(),
|
|
|
+ lon: this.currentGpsPoint.lon,
|
|
|
+ lat: this.currentGpsPoint.lat,
|
|
|
+ accuracyMeters: this.currentGpsAccuracyMeters,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ this.gamePresentation = this.gameRuntime.getPresentation()
|
|
|
+ this.refreshCourseHeadingFromPresentation()
|
|
|
+ const defaultStatusText = this.currentGpsPoint
|
|
|
+ ? `顺序打点已开始 (${this.buildVersion})`
|
|
|
+ : `顺序打点已开始,GPS定位启动中 (${this.buildVersion})`
|
|
|
+ const gameStatusText = this.applyGameEffects(gameResult.effects) || defaultStatusText
|
|
|
+ this.setState({
|
|
|
+ ...this.getGameViewPatch(gameStatusText),
|
|
|
+ }, true)
|
|
|
+ this.syncRenderer()
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ handlePunchAction(): void {
|
|
|
+ const gameResult = this.gameRuntime.dispatch({
|
|
|
+ type: 'punch_requested',
|
|
|
+ at: Date.now(),
|
|
|
+ })
|
|
|
+ this.gamePresentation = gameResult.presentation
|
|
|
+ this.refreshCourseHeadingFromPresentation()
|
|
|
+ const gameStatusText = this.applyGameEffects(gameResult.effects)
|
|
|
+ this.setState({
|
|
|
+ ...this.getGameViewPatch(gameStatusText),
|
|
|
+ }, true)
|
|
|
+ this.syncRenderer()
|
|
|
+ }
|
|
|
+
|
|
|
handleLocationUpdate(longitude: number, latitude: number, accuracyMeters: number | null): void {
|
|
|
const nextPoint: LonLatPoint = { lon: longitude, lat: latitude }
|
|
|
const lastTrackPoint = this.currentGpsTrack.length ? this.currentGpsTrack[this.currentGpsTrack.length - 1] : null
|
|
|
@@ -596,6 +871,20 @@ 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)
|
|
|
+ let gameStatusText: string | null = null
|
|
|
+
|
|
|
+ if (this.courseData) {
|
|
|
+ const gameResult = this.gameRuntime.dispatch({
|
|
|
+ type: 'gps_updated',
|
|
|
+ at: Date.now(),
|
|
|
+ lon: longitude,
|
|
|
+ lat: latitude,
|
|
|
+ accuracyMeters,
|
|
|
+ })
|
|
|
+ this.gamePresentation = gameResult.presentation
|
|
|
+ this.refreshCourseHeadingFromPresentation()
|
|
|
+ gameStatusText = this.applyGameEffects(gameResult.effects)
|
|
|
+ }
|
|
|
|
|
|
if (gpsInsideMap && !this.hasGpsCenteredOnce) {
|
|
|
this.hasGpsCenteredOnce = true
|
|
|
@@ -607,7 +896,8 @@ export class MapEngine {
|
|
|
gpsTracking: true,
|
|
|
gpsTrackingText: '持续定位进行中',
|
|
|
gpsCoordText: formatGpsCoordText(nextPoint, accuracyMeters),
|
|
|
- }, `GPS定位成功,已定位到当前位置 (${this.buildVersion})`, true)
|
|
|
+ ...this.getGameViewPatch(),
|
|
|
+ }, gameStatusText || `GPS定位成功,已定位到当前位置 (${this.buildVersion})`, true)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -615,7 +905,7 @@ export class MapEngine {
|
|
|
gpsTracking: true,
|
|
|
gpsTrackingText: gpsInsideMap ? '持续定位进行中' : 'GPS不在当前地图范围内',
|
|
|
gpsCoordText: formatGpsCoordText(nextPoint, accuracyMeters),
|
|
|
- statusText: gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`,
|
|
|
+ ...this.getGameViewPatch(gameStatusText || (gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`)),
|
|
|
}, true)
|
|
|
this.syncRenderer()
|
|
|
}
|
|
|
@@ -649,7 +939,7 @@ export class MapEngine {
|
|
|
stageLeft: rect.left,
|
|
|
stageTop: rect.top,
|
|
|
},
|
|
|
- `地图视口与 WebGL 引擎已对齐 (${this.buildVersion})`,
|
|
|
+ `鍦板浘瑙嗗彛涓?WebGL 寮曟搸宸插榻?(${this.buildVersion})`,
|
|
|
true,
|
|
|
)
|
|
|
}
|
|
|
@@ -662,7 +952,7 @@ export class MapEngine {
|
|
|
this.onData({
|
|
|
mapReady: true,
|
|
|
mapReadyText: 'READY',
|
|
|
- statusText: `单 WebGL 管线已就绪,可切换手动或自动朝向 (${this.buildVersion})`,
|
|
|
+ statusText: `鍗?WebGL 绠$嚎宸插氨缁紝鍙垏鎹㈡墜鍔ㄦ垨鑷姩鏈濆悜 (${this.buildVersion})`,
|
|
|
})
|
|
|
this.syncRenderer()
|
|
|
this.compassController.start()
|
|
|
@@ -679,9 +969,15 @@ export class MapEngine {
|
|
|
this.tileBoundsByZoom = config.tileBoundsByZoom
|
|
|
this.courseData = config.course
|
|
|
this.cpRadiusMeters = config.cpRadiusMeters
|
|
|
+ this.gameMode = config.gameMode
|
|
|
+ this.punchPolicy = config.punchPolicy
|
|
|
+ this.punchRadiusMeters = config.punchRadiusMeters
|
|
|
+ this.autoFinishOnLastControl = config.autoFinishOnLastControl
|
|
|
|
|
|
+ const gameEffects = this.loadGameDefinitionFromCourse()
|
|
|
+ const gameStatusText = this.applyGameEffects(gameEffects)
|
|
|
const statePatch: Partial<MapEngineViewState> = {
|
|
|
- configStatusText: `远程配置已载入 / ${config.courseStatusText}`,
|
|
|
+ configStatusText: `杩滅▼閰嶇疆宸茶浇鍏?/ ${config.courseStatusText}`,
|
|
|
projectionMode: config.projectionModeText,
|
|
|
tileSource: config.tileSource,
|
|
|
sensorHeadingText: formatHeadingText(this.smoothedSensorHeadingDeg === null ? null : getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)),
|
|
|
@@ -689,6 +985,7 @@ export class MapEngine {
|
|
|
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
|
|
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
|
|
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg),
|
|
|
+ ...this.getGameViewPatch(),
|
|
|
}
|
|
|
|
|
|
if (!this.state.stageWidth || !this.state.stageHeight) {
|
|
|
@@ -698,7 +995,7 @@ export class MapEngine {
|
|
|
centerTileX: this.defaultCenterTileX,
|
|
|
centerTileY: this.defaultCenterTileY,
|
|
|
centerText: buildCenterText(this.defaultZoom, this.defaultCenterTileX, this.defaultCenterTileY),
|
|
|
- statusText: `远程地图配置已载入 (${this.buildVersion})`,
|
|
|
+ statusText: gameStatusText || `路线已载入,点击开始进入游戏 (${this.buildVersion})`,
|
|
|
}, true)
|
|
|
return
|
|
|
}
|
|
|
@@ -710,7 +1007,7 @@ export class MapEngine {
|
|
|
centerTileY: this.defaultCenterTileY,
|
|
|
tileTranslateX: 0,
|
|
|
tileTranslateY: 0,
|
|
|
- }, `远程地图配置已载入 (${this.buildVersion})`, true, () => {
|
|
|
+ }, gameStatusText || `路线已载入,点击开始进入游戏 (${this.buildVersion})`, true, () => {
|
|
|
this.resetPreviewState()
|
|
|
this.syncRenderer()
|
|
|
if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) {
|
|
|
@@ -722,7 +1019,6 @@ export class MapEngine {
|
|
|
handleTouchStart(event: WechatMiniprogram.TouchEvent): void {
|
|
|
this.clearInertiaTimer()
|
|
|
this.clearPreviewResetTimer()
|
|
|
- this.renderer.setAnimationPaused(true)
|
|
|
this.panVelocityX = 0
|
|
|
this.panVelocityY = 0
|
|
|
|
|
|
@@ -787,8 +1083,8 @@ export class MapEngine {
|
|
|
rotationText: formatRotationText(nextRotationDeg),
|
|
|
},
|
|
|
this.state.orientationMode === 'heading-up'
|
|
|
- ? `双指缩放中,自动朝向保持开启 (${this.buildVersion})`
|
|
|
- : `双指缩放与旋转中 (${this.buildVersion})`,
|
|
|
+ ? `鍙屾寚缂╂斁涓紝鑷姩鏈濆悜淇濇寔寮€鍚?(${this.buildVersion})`
|
|
|
+ : `鍙屾寚缂╂斁涓庢棆杞腑 (${this.buildVersion})`,
|
|
|
)
|
|
|
return
|
|
|
}
|
|
|
@@ -813,7 +1109,7 @@ export class MapEngine {
|
|
|
this.normalizeTranslate(
|
|
|
this.state.tileTranslateX + deltaX,
|
|
|
this.state.tileTranslateY + deltaY,
|
|
|
- `已拖拽单 WebGL 地图引擎 (${this.buildVersion})`,
|
|
|
+ `宸叉嫋鎷藉崟 WebGL 鍦板浘寮曟搸 (${this.buildVersion})`,
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -895,7 +1191,7 @@ export class MapEngine {
|
|
|
tileTranslateX: 0,
|
|
|
tileTranslateY: 0,
|
|
|
},
|
|
|
- `已回到单 WebGL 引擎默认首屏 (${this.buildVersion})`,
|
|
|
+ `宸插洖鍒板崟 WebGL 寮曟搸榛樿棣栧睆 (${this.buildVersion})`,
|
|
|
true,
|
|
|
() => {
|
|
|
this.resetPreviewState()
|
|
|
@@ -909,7 +1205,7 @@ export class MapEngine {
|
|
|
handleRotateStep(stepDeg = ROTATE_STEP_DEG): void {
|
|
|
if (this.state.rotationMode === 'auto') {
|
|
|
this.setState({
|
|
|
- statusText: `当前不是手动旋转模式,请先切回手动 (${this.buildVersion})`,
|
|
|
+ statusText: `褰撳墠涓嶆槸鎵嬪姩鏃嬭浆妯″紡锛岃鍏堝垏鍥炴墜鍔?(${this.buildVersion})`,
|
|
|
}, true)
|
|
|
return
|
|
|
}
|
|
|
@@ -929,7 +1225,7 @@ export class MapEngine {
|
|
|
rotationDeg: nextRotationDeg,
|
|
|
rotationText: formatRotationText(nextRotationDeg),
|
|
|
},
|
|
|
- `旋转角度调整到 ${formatRotationText(nextRotationDeg)} (${this.buildVersion})`,
|
|
|
+ `鏃嬭浆瑙掑害璋冩暣鍒?${formatRotationText(nextRotationDeg)} (${this.buildVersion})`,
|
|
|
true,
|
|
|
() => {
|
|
|
this.resetPreviewState()
|
|
|
@@ -942,7 +1238,7 @@ export class MapEngine {
|
|
|
handleRotationReset(): void {
|
|
|
if (this.state.rotationMode === 'auto') {
|
|
|
this.setState({
|
|
|
- statusText: `当前不是手动旋转模式,请先切回手动 (${this.buildVersion})`,
|
|
|
+ statusText: `褰撳墠涓嶆槸鎵嬪姩鏃嬭浆妯″紡锛岃鍏堝垏鍥炴墜鍔?(${this.buildVersion})`,
|
|
|
}, true)
|
|
|
return
|
|
|
}
|
|
|
@@ -966,7 +1262,7 @@ export class MapEngine {
|
|
|
rotationDeg: targetRotationDeg,
|
|
|
rotationText: formatRotationText(targetRotationDeg),
|
|
|
},
|
|
|
- `旋转角度已回到真北参考 (${this.buildVersion})`,
|
|
|
+ `鏃嬭浆瑙掑害宸插洖鍒扮湡鍖楀弬鑰?(${this.buildVersion})`,
|
|
|
true,
|
|
|
() => {
|
|
|
this.resetPreviewState()
|
|
|
@@ -1009,20 +1305,20 @@ export class MapEngine {
|
|
|
handleAutoRotateCalibrate(): void {
|
|
|
if (this.state.orientationMode !== 'heading-up') {
|
|
|
this.setState({
|
|
|
- statusText: `请先切到朝向朝上模式再校准 (${this.buildVersion})`,
|
|
|
+ statusText: `璇峰厛鍒囧埌鏈濆悜鏈濅笂妯″紡鍐嶆牎鍑?(${this.buildVersion})`,
|
|
|
}, true)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if (!this.calibrateAutoRotateToCurrentOrientation()) {
|
|
|
this.setState({
|
|
|
- statusText: `当前还没有传感器方向数据,暂时无法校准 (${this.buildVersion})`,
|
|
|
+ statusText: `褰撳墠杩樻病鏈変紶鎰熷櫒鏂瑰悜鏁版嵁锛屾殏鏃舵棤娉曟牎鍑?(${this.buildVersion})`,
|
|
|
}, true)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
this.setState({
|
|
|
- statusText: `已按当前持机方向完成朝向校准 (${this.buildVersion})`,
|
|
|
+ statusText: `宸叉寜褰撳墠鎸佹満鏂瑰悜瀹屾垚鏈濆悜鏍″噯 (${this.buildVersion})`,
|
|
|
}, true)
|
|
|
this.scheduleAutoRotate()
|
|
|
}
|
|
|
@@ -1038,7 +1334,7 @@ export class MapEngine {
|
|
|
orientationMode: 'manual',
|
|
|
orientationModeText: formatOrientationModeText('manual'),
|
|
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
|
|
|
- statusText: `已切回手动地图旋转 (${this.buildVersion})`,
|
|
|
+ statusText: `宸插垏鍥炴墜鍔ㄥ湴鍥炬棆杞?(${this.buildVersion})`,
|
|
|
}, true)
|
|
|
}
|
|
|
|
|
|
@@ -1065,7 +1361,7 @@ export class MapEngine {
|
|
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, mapNorthOffsetDeg),
|
|
|
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
|
|
},
|
|
|
- `地图已固定为真北朝上 (${this.buildVersion})`,
|
|
|
+ `鍦板浘宸插浐瀹氫负鐪熷寳鏈濅笂 (${this.buildVersion})`,
|
|
|
true,
|
|
|
() => {
|
|
|
this.resetPreviewState()
|
|
|
@@ -1086,7 +1382,7 @@ export class MapEngine {
|
|
|
orientationModeText: formatOrientationModeText('heading-up'),
|
|
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
|
|
|
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
|
|
- statusText: `正在启用朝向朝上模式 (${this.buildVersion})`,
|
|
|
+ statusText: `姝e湪鍚敤鏈濆悜鏈濅笂妯″紡 (${this.buildVersion})`,
|
|
|
}, true)
|
|
|
if (this.refreshAutoRotateTarget()) {
|
|
|
this.scheduleAutoRotate()
|
|
|
@@ -1409,6 +1705,15 @@ export class MapEngine {
|
|
|
gpsCalibrationOrigin: worldTileToLonLat({ x: this.defaultCenterTileX, y: this.defaultCenterTileY }, this.defaultZoom),
|
|
|
course: this.courseData,
|
|
|
cpRadiusMeters: this.cpRadiusMeters,
|
|
|
+ activeControlSequences: this.gamePresentation.activeControlSequences,
|
|
|
+ activeStart: this.gamePresentation.activeStart,
|
|
|
+ completedStart: this.gamePresentation.completedStart,
|
|
|
+ activeFinish: this.gamePresentation.activeFinish,
|
|
|
+ completedFinish: this.gamePresentation.completedFinish,
|
|
|
+ revealFullCourse: this.gamePresentation.revealFullCourse,
|
|
|
+ activeLegIndices: this.gamePresentation.activeLegIndices,
|
|
|
+ completedLegIndices: this.gamePresentation.completedLegIndices,
|
|
|
+ completedControlSequences: this.gamePresentation.completedControlSequences,
|
|
|
osmReferenceEnabled: this.state.osmReferenceEnabled,
|
|
|
overlayOpacity: MAP_OVERLAY_OPACITY,
|
|
|
}
|
|
|
@@ -1701,7 +2006,7 @@ export class MapEngine {
|
|
|
tileTranslateX: 0,
|
|
|
tileTranslateY: 0,
|
|
|
},
|
|
|
- `缩放级别调整到 ${nextZoom}`,
|
|
|
+ `缂╂斁绾у埆璋冩暣鍒?${nextZoom}`,
|
|
|
true,
|
|
|
() => {
|
|
|
this.setPreviewState(residualScale, stageX, stageY)
|
|
|
@@ -1728,7 +2033,7 @@ export class MapEngine {
|
|
|
zoom: nextZoom,
|
|
|
...resolvedViewport,
|
|
|
},
|
|
|
- `缩放级别调整到 ${nextZoom}`,
|
|
|
+ `缂╂斁绾у埆璋冩暣鍒?${nextZoom}`,
|
|
|
true,
|
|
|
() => {
|
|
|
this.setPreviewState(residualScale, stageX, stageY)
|
|
|
@@ -1748,7 +2053,7 @@ export class MapEngine {
|
|
|
|
|
|
if (Math.abs(this.panVelocityX) < INERTIA_MIN_SPEED && Math.abs(this.panVelocityY) < INERTIA_MIN_SPEED) {
|
|
|
this.setState({
|
|
|
- statusText: `惯性滑动结束 (${this.buildVersion})`,
|
|
|
+ statusText: `鎯€ф粦鍔ㄧ粨鏉?(${this.buildVersion})`,
|
|
|
})
|
|
|
this.renderer.setAnimationPaused(false)
|
|
|
this.inertiaTimer = 0
|
|
|
@@ -1759,7 +2064,7 @@ export class MapEngine {
|
|
|
this.normalizeTranslate(
|
|
|
this.state.tileTranslateX + this.panVelocityX * INERTIA_FRAME_MS,
|
|
|
this.state.tileTranslateY + this.panVelocityY * INERTIA_FRAME_MS,
|
|
|
- `惯性滑动中 (${this.buildVersion})`,
|
|
|
+ `鎯€ф粦鍔ㄤ腑 (${this.buildVersion})`,
|
|
|
)
|
|
|
|
|
|
this.inertiaTimer = setTimeout(step, INERTIA_FRAME_MS) as unknown as number
|
|
|
@@ -1805,6 +2110,18 @@ export class MapEngine {
|
|
|
|
|
|
|
|
|
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
|
|
|
|