2179 lines
65 KiB
TypeScript
2179 lines
65 KiB
TypeScript
import {
|
|
MapEngine,
|
|
type MapEngineGameInfoRow,
|
|
type MapEngineGameInfoSnapshot,
|
|
type MapEngineResultSnapshot,
|
|
type MapEngineStageRect,
|
|
type MapEngineViewState,
|
|
} from '../../engine/map/mapEngine'
|
|
import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
|
|
import { type AnimationLevel } from '../../utils/animationLevel'
|
|
type CompassTickData = {
|
|
angle: number
|
|
long: boolean
|
|
major: boolean
|
|
}
|
|
type CompassLabelData = {
|
|
text: string
|
|
angle: number
|
|
rotateBack: number
|
|
radius: number
|
|
className: string
|
|
}
|
|
type ScaleRulerMinorTickData = {
|
|
key: string
|
|
topPx: number
|
|
long: boolean
|
|
}
|
|
type ScaleRulerMajorMarkData = {
|
|
key: string
|
|
topPx: number
|
|
label: string
|
|
}
|
|
type SideButtonMode = 'shown' | 'hidden'
|
|
type SideActionButtonState = 'muted' | 'default' | 'active'
|
|
type SideButtonPlacement = 'left' | 'right'
|
|
type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
|
|
type UserNorthReferenceMode = 'magnetic' | 'true'
|
|
type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive'
|
|
type SettingLockKey =
|
|
| 'lockAnimationLevel'
|
|
| 'lockSideButtonPlacement'
|
|
| 'lockAutoRotate'
|
|
| 'lockCompassTuning'
|
|
| 'lockScaleRulerVisible'
|
|
| 'lockScaleRulerAnchor'
|
|
| 'lockNorthReference'
|
|
| 'lockHeartRateDevice'
|
|
type StoredUserSettings = {
|
|
animationLevel?: AnimationLevel
|
|
autoRotateEnabled?: boolean
|
|
compassTuningProfile?: CompassTuningProfile
|
|
northReferenceMode?: UserNorthReferenceMode
|
|
sideButtonPlacement?: SideButtonPlacement
|
|
showCenterScaleRuler?: boolean
|
|
centerScaleRulerAnchorMode?: CenterScaleRulerAnchorMode
|
|
lockAnimationLevel?: boolean
|
|
lockSideButtonPlacement?: boolean
|
|
lockAutoRotate?: boolean
|
|
lockCompassTuning?: boolean
|
|
lockScaleRulerVisible?: boolean
|
|
lockScaleRulerAnchor?: boolean
|
|
lockNorthReference?: boolean
|
|
lockHeartRateDevice?: boolean
|
|
}
|
|
type MapPageData = MapEngineViewState & {
|
|
showDebugPanel: boolean
|
|
showGameInfoPanel: boolean
|
|
showResultScene: boolean
|
|
showSystemSettingsPanel: boolean
|
|
showCenterScaleRuler: boolean
|
|
showPunchHintBanner: boolean
|
|
centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
|
|
statusBarHeight: number
|
|
topInsetHeight: number
|
|
hudPanelIndex: number
|
|
configSourceText: string
|
|
mockBridgeUrlDraft: string
|
|
mockHeartRateBridgeUrlDraft: string
|
|
gameInfoTitle: string
|
|
gameInfoSubtitle: string
|
|
gameInfoLocalRows: MapEngineGameInfoRow[]
|
|
gameInfoGlobalRows: MapEngineGameInfoRow[]
|
|
resultSceneTitle: string
|
|
resultSceneSubtitle: string
|
|
resultSceneHeroLabel: string
|
|
resultSceneHeroValue: string
|
|
resultSceneRows: MapEngineGameInfoRow[]
|
|
panelTimerText: string
|
|
panelMileageText: string
|
|
panelDistanceValueText: string
|
|
panelProgressText: string
|
|
panelSpeedValueText: string
|
|
panelTimerFxClass: string
|
|
panelMileageFxClass: string
|
|
panelSpeedFxClass: string
|
|
panelHeartRateFxClass: string
|
|
compassTicks: CompassTickData[]
|
|
compassLabels: CompassLabelData[]
|
|
sideButtonMode: SideButtonMode
|
|
sideButtonPlacement: SideButtonPlacement
|
|
autoRotateEnabled: boolean
|
|
lockAnimationLevel: boolean
|
|
lockSideButtonPlacement: boolean
|
|
lockAutoRotate: boolean
|
|
lockCompassTuning: boolean
|
|
lockScaleRulerVisible: boolean
|
|
lockScaleRulerAnchor: boolean
|
|
lockNorthReference: boolean
|
|
lockHeartRateDevice: boolean
|
|
sideToggleIconSrc: string
|
|
sideButton2Class: string
|
|
sideButton4Class: string
|
|
sideButton11Class: string
|
|
sideButton12Class: string
|
|
sideButton13Class: string
|
|
sideButton14Class: string
|
|
sideButton16Class: string
|
|
centerScaleRulerVisible: boolean
|
|
centerScaleRulerCenterXPx: number
|
|
centerScaleRulerZeroYPx: number
|
|
centerScaleRulerHeightPx: number
|
|
centerScaleRulerAxisBottomPx: number
|
|
centerScaleRulerZeroVisible: boolean
|
|
centerScaleRulerZeroLabel: string
|
|
centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
|
|
centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
|
|
showLeftButtonGroup: boolean
|
|
showRightButtonGroups: boolean
|
|
showBottomDebugButton: boolean
|
|
}
|
|
const INTERNAL_BUILD_VERSION = 'map-build-291'
|
|
const USER_SETTINGS_STORAGE_KEY = 'cmr_user_settings_v1'
|
|
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 centerScaleRulerUpdateTimer = 0
|
|
let punchHintDismissTimer = 0
|
|
let panelTimerFxTimer = 0
|
|
let panelMileageFxTimer = 0
|
|
let panelSpeedFxTimer = 0
|
|
let panelHeartRateFxTimer = 0
|
|
let lastCenterScaleRulerStablePatch: Pick<
|
|
MapPageData,
|
|
| 'centerScaleRulerVisible'
|
|
| 'centerScaleRulerCenterXPx'
|
|
| 'centerScaleRulerZeroYPx'
|
|
| 'centerScaleRulerHeightPx'
|
|
| 'centerScaleRulerAxisBottomPx'
|
|
| 'centerScaleRulerZeroVisible'
|
|
| 'centerScaleRulerZeroLabel'
|
|
| 'centerScaleRulerMinorTicks'
|
|
| 'centerScaleRulerMajorMarks'
|
|
> = {
|
|
centerScaleRulerVisible: false,
|
|
centerScaleRulerCenterXPx: 0,
|
|
centerScaleRulerZeroYPx: 0,
|
|
centerScaleRulerHeightPx: 0,
|
|
centerScaleRulerAxisBottomPx: 0,
|
|
centerScaleRulerZeroVisible: false,
|
|
centerScaleRulerZeroLabel: '0 m',
|
|
centerScaleRulerMinorTicks: [],
|
|
centerScaleRulerMajorMarks: [],
|
|
}
|
|
let centerScaleRulerInputCache: Partial<Pick<
|
|
MapPageData,
|
|
'stageWidth'
|
|
| 'stageHeight'
|
|
| 'zoom'
|
|
| 'centerTileY'
|
|
| 'tileSizePx'
|
|
| 'previewScale'
|
|
>> = {}
|
|
|
|
const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
|
|
'buildVersion',
|
|
'renderMode',
|
|
'projectionMode',
|
|
'mapReady',
|
|
'mapReadyText',
|
|
'mapName',
|
|
'configStatusText',
|
|
'deviceHeadingText',
|
|
'devicePoseText',
|
|
'headingConfidenceText',
|
|
'accelerometerText',
|
|
'gyroscopeText',
|
|
'deviceMotionText',
|
|
'compassSourceText',
|
|
'compassTuningProfile',
|
|
'compassTuningProfileText',
|
|
'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 CENTER_SCALE_RULER_CACHE_KEYS: Array<keyof typeof centerScaleRulerInputCache> = [
|
|
'stageWidth',
|
|
'stageHeight',
|
|
'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 clearCenterScaleRulerUpdateTimer() {
|
|
if (centerScaleRulerUpdateTimer) {
|
|
clearTimeout(centerScaleRulerUpdateTimer)
|
|
centerScaleRulerUpdateTimer = 0
|
|
}
|
|
}
|
|
|
|
function clearPunchHintDismissTimer() {
|
|
if (punchHintDismissTimer) {
|
|
clearTimeout(punchHintDismissTimer)
|
|
punchHintDismissTimer = 0
|
|
}
|
|
}
|
|
|
|
function clearHudFxTimer(key: 'timer' | 'mileage' | 'speed' | 'heartRate') {
|
|
const timerMap = {
|
|
timer: panelTimerFxTimer,
|
|
mileage: panelMileageFxTimer,
|
|
speed: panelSpeedFxTimer,
|
|
heartRate: panelHeartRateFxTimer,
|
|
}
|
|
const timer = timerMap[key]
|
|
if (timer) {
|
|
clearTimeout(timer)
|
|
}
|
|
if (key === 'timer') {
|
|
panelTimerFxTimer = 0
|
|
} else if (key === 'mileage') {
|
|
panelMileageFxTimer = 0
|
|
} else if (key === 'speed') {
|
|
panelSpeedFxTimer = 0
|
|
} else {
|
|
panelHeartRateFxTimer = 0
|
|
}
|
|
}
|
|
|
|
function updateCenterScaleRulerInputCache(patch: Partial<MapPageData>) {
|
|
for (const key of CENTER_SCALE_RULER_CACHE_KEYS) {
|
|
if (Object.prototype.hasOwnProperty.call(patch, key)) {
|
|
;(centerScaleRulerInputCache as Record<string, unknown>)[key] =
|
|
(patch as Record<string, unknown>)[key]
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadStoredUserSettings(): StoredUserSettings {
|
|
try {
|
|
const stored = wx.getStorageSync(USER_SETTINGS_STORAGE_KEY)
|
|
if (!stored || typeof stored !== 'object') {
|
|
return {}
|
|
}
|
|
|
|
const normalized = stored as Record<string, unknown>
|
|
const settings: StoredUserSettings = {}
|
|
if (normalized.animationLevel === 'standard' || normalized.animationLevel === 'lite') {
|
|
settings.animationLevel = normalized.animationLevel
|
|
}
|
|
if (normalized.northReferenceMode === 'magnetic' || normalized.northReferenceMode === 'true') {
|
|
settings.northReferenceMode = normalized.northReferenceMode
|
|
}
|
|
if (typeof normalized.autoRotateEnabled === 'boolean') {
|
|
settings.autoRotateEnabled = normalized.autoRotateEnabled
|
|
}
|
|
if (normalized.compassTuningProfile === 'smooth' || normalized.compassTuningProfile === 'balanced' || normalized.compassTuningProfile === 'responsive') {
|
|
settings.compassTuningProfile = normalized.compassTuningProfile
|
|
}
|
|
if (normalized.sideButtonPlacement === 'left' || normalized.sideButtonPlacement === 'right') {
|
|
settings.sideButtonPlacement = normalized.sideButtonPlacement
|
|
}
|
|
if (typeof normalized.showCenterScaleRuler === 'boolean') {
|
|
settings.showCenterScaleRuler = normalized.showCenterScaleRuler
|
|
}
|
|
if (normalized.centerScaleRulerAnchorMode === 'screen-center' || normalized.centerScaleRulerAnchorMode === 'compass-center') {
|
|
settings.centerScaleRulerAnchorMode = normalized.centerScaleRulerAnchorMode
|
|
}
|
|
if (typeof normalized.lockAnimationLevel === 'boolean') {
|
|
settings.lockAnimationLevel = normalized.lockAnimationLevel
|
|
}
|
|
if (typeof normalized.lockSideButtonPlacement === 'boolean') {
|
|
settings.lockSideButtonPlacement = normalized.lockSideButtonPlacement
|
|
}
|
|
if (typeof normalized.lockAutoRotate === 'boolean') {
|
|
settings.lockAutoRotate = normalized.lockAutoRotate
|
|
}
|
|
if (typeof normalized.lockCompassTuning === 'boolean') {
|
|
settings.lockCompassTuning = normalized.lockCompassTuning
|
|
}
|
|
if (typeof normalized.lockScaleRulerVisible === 'boolean') {
|
|
settings.lockScaleRulerVisible = normalized.lockScaleRulerVisible
|
|
}
|
|
if (typeof normalized.lockScaleRulerAnchor === 'boolean') {
|
|
settings.lockScaleRulerAnchor = normalized.lockScaleRulerAnchor
|
|
}
|
|
if (typeof normalized.lockNorthReference === 'boolean') {
|
|
settings.lockNorthReference = normalized.lockNorthReference
|
|
}
|
|
if (typeof normalized.lockHeartRateDevice === 'boolean') {
|
|
settings.lockHeartRateDevice = normalized.lockHeartRateDevice
|
|
}
|
|
return settings
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function persistStoredUserSettings(settings: StoredUserSettings) {
|
|
try {
|
|
wx.setStorageSync(USER_SETTINGS_STORAGE_KEY, settings)
|
|
} catch {}
|
|
}
|
|
|
|
function toggleStoredSettingLock(settings: StoredUserSettings, key: SettingLockKey): StoredUserSettings {
|
|
return {
|
|
...settings,
|
|
[key]: !settings[key],
|
|
}
|
|
}
|
|
function buildSideButtonVisibility(mode: SideButtonMode) {
|
|
return {
|
|
sideButtonMode: mode,
|
|
showLeftButtonGroup: mode === 'shown',
|
|
showRightButtonGroups: false,
|
|
showBottomDebugButton: true,
|
|
}
|
|
}
|
|
|
|
function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
|
|
return currentMode === 'shown' ? 'hidden' : 'shown'
|
|
}
|
|
function buildCompassTicks(): CompassTickData[] {
|
|
const ticks: CompassTickData[] = []
|
|
for (let angle = 0; angle < 360; angle += 5) {
|
|
ticks.push({
|
|
angle,
|
|
long: angle % 15 === 0,
|
|
major: angle % 45 === 0,
|
|
})
|
|
}
|
|
return ticks
|
|
}
|
|
function buildCompassLabels(): CompassLabelData[] {
|
|
return [
|
|
{ text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
|
|
{ text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
|
|
{ text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
|
|
{ text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
|
|
{ text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
|
|
{ text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
|
|
{ text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
|
|
{ text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
|
|
]
|
|
}
|
|
function getFallbackStageRect(): MapEngineStageRect {
|
|
const systemInfo = wx.getSystemInfoSync()
|
|
const width = Math.max(320, systemInfo.windowWidth)
|
|
const height = Math.max(280, systemInfo.windowHeight)
|
|
|
|
return {
|
|
width,
|
|
height,
|
|
left: 0,
|
|
top: 0,
|
|
}
|
|
}
|
|
|
|
function getSideToggleIconSrc(mode: SideButtonMode): string {
|
|
if (mode === 'hidden') {
|
|
return '../../assets/btn_more1.png'
|
|
}
|
|
return '../../assets/btn_more3.png'
|
|
}
|
|
|
|
function getSideActionButtonClass(state: SideActionButtonState): string {
|
|
if (state === 'muted') {
|
|
return 'map-side-button map-side-button--muted'
|
|
}
|
|
if (state === 'active') {
|
|
return 'map-side-button map-side-button--active'
|
|
}
|
|
return 'map-side-button map-side-button--default'
|
|
}
|
|
|
|
function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showSystemSettingsPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
|
|
const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
|
|
? 'muted'
|
|
: data.gpsLockEnabled
|
|
? 'active'
|
|
: 'default'
|
|
const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'running' ? 'active' : 'muted'
|
|
const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
|
|
const sideButton12State: SideActionButtonState = data.showSystemSettingsPanel ? 'active' : 'default'
|
|
const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
|
|
const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
|
|
? 'muted'
|
|
: data.centerScaleRulerAnchorMode === 'compass-center'
|
|
? 'active'
|
|
: 'default'
|
|
const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'
|
|
|
|
return {
|
|
sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
|
|
sideButton2Class: getSideActionButtonClass(sideButton2State),
|
|
sideButton4Class: getSideActionButtonClass(sideButton4State),
|
|
sideButton11Class: getSideActionButtonClass(sideButton11State),
|
|
sideButton12Class: getSideActionButtonClass(sideButton12State),
|
|
sideButton13Class: getSideActionButtonClass(sideButton13State),
|
|
sideButton14Class: getSideActionButtonClass(sideButton14State),
|
|
sideButton16Class: getSideActionButtonClass(sideButton16State),
|
|
}
|
|
}
|
|
|
|
function getRpxUnitInPx(): number {
|
|
const systemInfo = wx.getSystemInfoSync()
|
|
return systemInfo.windowWidth / 750
|
|
}
|
|
|
|
function worldTileYToLat(worldTileY: number, zoom: number): number {
|
|
const scale = Math.pow(2, zoom)
|
|
const n = Math.PI - (2 * Math.PI * worldTileY) / scale
|
|
return (180 / Math.PI) * Math.atan(Math.sinh(n))
|
|
}
|
|
|
|
function getNiceDistanceMeters(rawDistanceMeters: number): number {
|
|
if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
|
|
return 50
|
|
}
|
|
|
|
const exponent = Math.floor(Math.log10(rawDistanceMeters))
|
|
const base = Math.pow(10, exponent)
|
|
const normalized = rawDistanceMeters / base
|
|
|
|
if (normalized <= 1) {
|
|
return base
|
|
}
|
|
if (normalized <= 2) {
|
|
return 2 * base
|
|
}
|
|
if (normalized <= 5) {
|
|
return 5 * base
|
|
}
|
|
return 10 * base
|
|
}
|
|
|
|
function formatScaleDistanceLabel(distanceMeters: number): string {
|
|
if (distanceMeters >= 1000) {
|
|
const distanceKm = distanceMeters / 1000
|
|
const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
|
|
return `${formatted.replace(/\.0$/, '')} km`
|
|
}
|
|
|
|
return `${Math.round(distanceMeters)} m`
|
|
}
|
|
|
|
function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
|
|
if (!data.showCenterScaleRuler) {
|
|
lastCenterScaleRulerStablePatch = {
|
|
centerScaleRulerVisible: false,
|
|
centerScaleRulerCenterXPx: 0,
|
|
centerScaleRulerZeroYPx: 0,
|
|
centerScaleRulerHeightPx: 0,
|
|
centerScaleRulerAxisBottomPx: 0,
|
|
centerScaleRulerZeroVisible: false,
|
|
centerScaleRulerZeroLabel: '0 m',
|
|
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
|
|
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
|
|
}
|
|
return { ...lastCenterScaleRulerStablePatch }
|
|
}
|
|
|
|
if (!data.stageWidth || !data.stageHeight) {
|
|
return { ...lastCenterScaleRulerStablePatch }
|
|
}
|
|
|
|
const topPadding = 12
|
|
const rpxUnitPx = getRpxUnitInPx()
|
|
const compassBottomPaddingPx = 248 * rpxUnitPx
|
|
const compassDialRadiusPx = (196 * rpxUnitPx) / 2
|
|
const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
|
|
const compassOcclusionPaddingPx = 10 * rpxUnitPx
|
|
const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
|
|
? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
|
|
: Math.round(data.stageHeight / 2)
|
|
const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
|
|
const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
|
|
? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
|
|
: 0
|
|
|
|
if (
|
|
!data.tileSizePx
|
|
|| !Number.isFinite(data.zoom)
|
|
|| !Number.isFinite(data.centerTileY)
|
|
) {
|
|
return {
|
|
...lastCenterScaleRulerStablePatch,
|
|
centerScaleRulerVisible: true,
|
|
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
|
centerScaleRulerZeroYPx: zeroYPx,
|
|
centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
|
|
centerScaleRulerAxisBottomPx: coveredBottomPx,
|
|
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
|
|
}
|
|
}
|
|
|
|
const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
|
|
const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
|
|
const metersPerPixel = metersPerTile / data.tileSizePx
|
|
const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
|
|
const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
|
|
const rulerHeight = Math.floor(zeroYPx - topPadding)
|
|
|
|
if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
|
|
return {
|
|
...lastCenterScaleRulerStablePatch,
|
|
centerScaleRulerVisible: true,
|
|
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
|
centerScaleRulerZeroYPx: zeroYPx,
|
|
centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
|
|
centerScaleRulerAxisBottomPx: coveredBottomPx,
|
|
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
|
|
}
|
|
}
|
|
|
|
const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
|
|
const minorDistanceMeters = labelDistanceMeters / 8
|
|
const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
|
|
const visibleTopLimitPx = rulerHeight - coveredBottomPx
|
|
const minorTicks: ScaleRulerMinorTickData[] = []
|
|
const majorMarks: ScaleRulerMajorMarkData[] = []
|
|
|
|
for (let index = 1; index <= 200; index += 1) {
|
|
const topPx = Math.round(rulerHeight - index * minorStepPx)
|
|
if (topPx < 0) {
|
|
break
|
|
}
|
|
if (topPx >= visibleTopLimitPx) {
|
|
continue
|
|
}
|
|
|
|
const isHalfMajor = index % 4 === 0
|
|
const isLabelMajor = index % 8 === 0
|
|
minorTicks.push({
|
|
key: `minor-${index}`,
|
|
topPx,
|
|
long: isHalfMajor,
|
|
})
|
|
|
|
if (isLabelMajor) {
|
|
majorMarks.push({
|
|
key: `major-${index}`,
|
|
topPx,
|
|
label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
|
|
})
|
|
}
|
|
}
|
|
|
|
lastCenterScaleRulerStablePatch = {
|
|
centerScaleRulerVisible: true,
|
|
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
|
centerScaleRulerZeroYPx: zeroYPx,
|
|
centerScaleRulerHeightPx: rulerHeight,
|
|
centerScaleRulerAxisBottomPx: coveredBottomPx,
|
|
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
|
|
centerScaleRulerZeroLabel: '0 m',
|
|
centerScaleRulerMinorTicks: minorTicks,
|
|
centerScaleRulerMajorMarks: majorMarks,
|
|
}
|
|
return { ...lastCenterScaleRulerStablePatch }
|
|
}
|
|
|
|
function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
|
|
return {
|
|
title: '当前游戏',
|
|
subtitle: '未开始',
|
|
localRows: [],
|
|
globalRows: [
|
|
{ label: '全球积分', value: '未接入' },
|
|
{ label: '全球排名', value: '未接入' },
|
|
{ label: '在线人数', value: '未接入' },
|
|
{ label: '队伍状态', value: '未接入' },
|
|
{ label: '实时广播', value: '未接入' },
|
|
],
|
|
}
|
|
}
|
|
|
|
function buildEmptyResultSceneSnapshot(): MapEngineResultSnapshot {
|
|
return {
|
|
title: '本局结果',
|
|
subtitle: '未开始',
|
|
heroLabel: '本局用时',
|
|
heroValue: '--',
|
|
rows: [],
|
|
}
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
showDebugPanel: false,
|
|
showGameInfoPanel: false,
|
|
showResultScene: false,
|
|
showSystemSettingsPanel: false,
|
|
showCenterScaleRuler: false,
|
|
statusBarHeight: 0,
|
|
topInsetHeight: 12,
|
|
hudPanelIndex: 0,
|
|
configSourceText: '顺序赛配置',
|
|
centerScaleRulerAnchorMode: 'screen-center',
|
|
autoRotateEnabled: false,
|
|
lockAnimationLevel: false,
|
|
lockSideButtonPlacement: false,
|
|
lockAutoRotate: false,
|
|
lockCompassTuning: false,
|
|
lockScaleRulerVisible: false,
|
|
lockScaleRulerAnchor: false,
|
|
lockNorthReference: false,
|
|
lockHeartRateDevice: false,
|
|
gameInfoTitle: '当前游戏',
|
|
gameInfoSubtitle: '未开始',
|
|
gameInfoLocalRows: [],
|
|
gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
|
|
resultSceneTitle: '本局结果',
|
|
resultSceneSubtitle: '未开始',
|
|
resultSceneHeroLabel: '本局用时',
|
|
resultSceneHeroValue: '--',
|
|
resultSceneRows: buildEmptyResultSceneSnapshot().rows,
|
|
panelTimerText: '00:00:00',
|
|
panelMileageText: '0m',
|
|
panelActionTagText: '目标',
|
|
panelDistanceTagText: '点距',
|
|
panelDistanceValueText: '--',
|
|
panelDistanceUnitText: '',
|
|
panelProgressText: '0/0',
|
|
showPunchHintBanner: true,
|
|
sideButtonPlacement: 'left',
|
|
gameSessionStatus: 'idle',
|
|
gameModeText: '顺序赛',
|
|
gpsLockEnabled: false,
|
|
gpsLockAvailable: false,
|
|
locationSourceMode: 'real',
|
|
locationSourceText: '真实定位',
|
|
mockBridgeConnected: false,
|
|
mockBridgeStatusText: '未连接',
|
|
mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockCoordText: '--',
|
|
mockSpeedText: '--',
|
|
heartRateSourceMode: 'real',
|
|
heartRateSourceText: '真实心率',
|
|
mockHeartRateBridgeConnected: false,
|
|
mockHeartRateBridgeStatusText: '未连接',
|
|
mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockHeartRateText: '--',
|
|
heartRateScanText: '未扫描',
|
|
heartRateDiscoveredDevices: [],
|
|
panelSpeedValueText: '0',
|
|
panelTelemetryTone: 'blue',
|
|
panelHeartRateZoneNameText: '--',
|
|
panelHeartRateZoneRangeText: '',
|
|
heartRateConnected: false,
|
|
heartRateStatusText: '心率带未连接',
|
|
heartRateDeviceText: '--',
|
|
panelHeartRateValueText: '--',
|
|
panelHeartRateUnitText: '',
|
|
panelCaloriesValueText: '0',
|
|
panelCaloriesUnitText: 'kcal',
|
|
panelAverageSpeedValueText: '0',
|
|
panelAverageSpeedUnitText: 'km/h',
|
|
panelAccuracyValueText: '--',
|
|
panelAccuracyUnitText: '',
|
|
deviceHeadingText: '--',
|
|
devicePoseText: '竖持',
|
|
headingConfidenceText: '低',
|
|
accelerometerText: '--',
|
|
gyroscopeText: '--',
|
|
deviceMotionText: '--',
|
|
compassSourceText: '无数据',
|
|
compassTuningProfile: 'balanced',
|
|
compassTuningProfileText: '平衡',
|
|
punchButtonText: '打点',
|
|
punchButtonEnabled: false,
|
|
skipButtonEnabled: false,
|
|
punchHintText: '等待进入检查点范围',
|
|
punchFeedbackVisible: false,
|
|
punchFeedbackText: '',
|
|
punchFeedbackTone: 'neutral',
|
|
contentCardVisible: false,
|
|
contentCardTitle: '',
|
|
contentCardBody: '',
|
|
punchButtonFxClass: '',
|
|
panelProgressFxClass: '',
|
|
panelDistanceFxClass: '',
|
|
punchFeedbackFxClass: '',
|
|
contentCardFxClass: '',
|
|
mapPulseVisible: false,
|
|
mapPulseLeftPx: 0,
|
|
mapPulseTopPx: 0,
|
|
mapPulseFxClass: '',
|
|
stageFxVisible: false,
|
|
stageFxClass: '',
|
|
centerScaleRulerVisible: false,
|
|
centerScaleRulerCenterXPx: 0,
|
|
centerScaleRulerZeroYPx: 0,
|
|
centerScaleRulerHeightPx: 0,
|
|
centerScaleRulerAxisBottomPx: 0,
|
|
centerScaleRulerZeroVisible: false,
|
|
centerScaleRulerZeroLabel: '0 m',
|
|
centerScaleRulerMinorTicks: [],
|
|
centerScaleRulerMajorMarks: [],
|
|
compassTicks: buildCompassTicks(),
|
|
compassLabels: buildCompassLabels(),
|
|
...buildSideButtonVisibility('shown'),
|
|
...buildSideButtonState({
|
|
sideButtonMode: 'shown',
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: false,
|
|
showCenterScaleRuler: false,
|
|
centerScaleRulerAnchorMode: 'screen-center',
|
|
skipButtonEnabled: false,
|
|
gameSessionStatus: 'idle',
|
|
gpsLockEnabled: false,
|
|
gpsLockAvailable: false,
|
|
}),
|
|
} as unknown as MapPageData,
|
|
|
|
onLoad() {
|
|
const systemInfo = wx.getSystemInfoSync()
|
|
const statusBarHeight = systemInfo.statusBarHeight || 0
|
|
const menuButtonRect = wx.getMenuButtonBoundingClientRect()
|
|
const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
|
|
|
|
if (mapEngine) {
|
|
mapEngine.destroy()
|
|
mapEngine = null
|
|
}
|
|
|
|
mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
|
|
onData: (patch) => {
|
|
const nextPatch = patch as 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'
|
|
&& this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
|
|
) {
|
|
nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
|
|
}
|
|
|
|
if (
|
|
typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
|
|
&& this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
|
|
) {
|
|
nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
|
|
}
|
|
|
|
updateCenterScaleRulerInputCache(nextPatch)
|
|
|
|
const mergedData = {
|
|
...centerScaleRulerInputCache,
|
|
...this.data,
|
|
...nextData,
|
|
} as MapPageData
|
|
|
|
const derivedPatch: Partial<MapPageData> = {}
|
|
if (typeof nextPatch.orientationMode === 'string') {
|
|
nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up'
|
|
}
|
|
if (
|
|
this.data.showCenterScaleRuler
|
|
&& hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
|
|
) {
|
|
clearCenterScaleRulerUpdateTimer()
|
|
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
|
|
}
|
|
}
|
|
|
|
const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
|
|
? nextPatch.animationLevel
|
|
: this.data.animationLevel
|
|
|
|
if (nextAnimationLevel === 'lite') {
|
|
clearHudFxTimer('timer')
|
|
clearHudFxTimer('mileage')
|
|
clearHudFxTimer('speed')
|
|
clearHudFxTimer('heartRate')
|
|
nextData.panelTimerFxClass = ''
|
|
nextData.panelMileageFxClass = ''
|
|
nextData.panelSpeedFxClass = ''
|
|
nextData.panelHeartRateFxClass = ''
|
|
} else {
|
|
if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
|
|
clearHudFxTimer('timer')
|
|
nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
|
|
panelTimerFxTimer = setTimeout(() => {
|
|
panelTimerFxTimer = 0
|
|
this.setData({ panelTimerFxClass: '' })
|
|
}, 320) as unknown as number
|
|
}
|
|
|
|
if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
|
|
clearHudFxTimer('mileage')
|
|
nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
|
|
panelMileageFxTimer = setTimeout(() => {
|
|
panelMileageFxTimer = 0
|
|
this.setData({ panelMileageFxClass: '' })
|
|
}, 360) as unknown as number
|
|
}
|
|
|
|
if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
|
|
clearHudFxTimer('speed')
|
|
nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
|
|
panelSpeedFxTimer = setTimeout(() => {
|
|
panelSpeedFxTimer = 0
|
|
this.setData({ panelSpeedFxClass: '' })
|
|
}, 360) as unknown as number
|
|
}
|
|
|
|
if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
|
|
clearHudFxTimer('heartRate')
|
|
nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
|
|
panelHeartRateFxTimer = setTimeout(() => {
|
|
panelHeartRateFxTimer = 0
|
|
this.setData({ panelHeartRateFxClass: '' })
|
|
}, 400) as unknown as number
|
|
}
|
|
}
|
|
|
|
if (typeof nextPatch.gameSessionStatus === 'string') {
|
|
if (
|
|
nextPatch.gameSessionStatus !== this.data.gameSessionStatus
|
|
&& (nextPatch.gameSessionStatus === 'finished' || nextPatch.gameSessionStatus === 'failed')
|
|
) {
|
|
this.syncResultSceneSnapshot()
|
|
nextData.showResultScene = true
|
|
nextData.showDebugPanel = false
|
|
nextData.showGameInfoPanel = false
|
|
nextData.showSystemSettingsPanel = false
|
|
clearGameInfoPanelSyncTimer()
|
|
} else if (nextPatch.gameSessionStatus === 'running' || nextPatch.gameSessionStatus === 'idle') {
|
|
nextData.showResultScene = false
|
|
}
|
|
}
|
|
|
|
if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
|
|
this.setData({
|
|
...nextData,
|
|
...derivedPatch,
|
|
})
|
|
}
|
|
|
|
if (this.data.showGameInfoPanel) {
|
|
this.scheduleGameInfoPanelSnapshotSync()
|
|
}
|
|
},
|
|
})
|
|
|
|
const storedUserSettings = loadStoredUserSettings()
|
|
if (storedUserSettings.animationLevel) {
|
|
mapEngine.handleSetAnimationLevel(storedUserSettings.animationLevel)
|
|
}
|
|
const initialAutoRotateEnabled = storedUserSettings.autoRotateEnabled !== false
|
|
if (initialAutoRotateEnabled) {
|
|
mapEngine.handleSetHeadingUpMode()
|
|
} else {
|
|
mapEngine.handleSetManualMode()
|
|
}
|
|
if (storedUserSettings.compassTuningProfile) {
|
|
mapEngine.handleSetCompassTuningProfile(storedUserSettings.compassTuningProfile)
|
|
}
|
|
if (storedUserSettings.northReferenceMode) {
|
|
mapEngine.handleSetNorthReferenceMode(storedUserSettings.northReferenceMode)
|
|
}
|
|
const initialSideButtonPlacement = storedUserSettings.sideButtonPlacement || 'left'
|
|
|
|
mapEngine.setDiagnosticUiEnabled(false)
|
|
centerScaleRulerInputCache = {
|
|
stageWidth: 0,
|
|
stageHeight: 0,
|
|
zoom: 0,
|
|
centerTileY: 0,
|
|
tileSizePx: 0,
|
|
previewScale: 1,
|
|
}
|
|
|
|
const initialShowCenterScaleRuler = !!storedUserSettings.showCenterScaleRuler
|
|
const initialCenterScaleRulerAnchorMode = storedUserSettings.centerScaleRulerAnchorMode || 'screen-center'
|
|
|
|
this.setData({
|
|
...mapEngine.getInitialData(),
|
|
showDebugPanel: false,
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: false,
|
|
showCenterScaleRuler: initialShowCenterScaleRuler,
|
|
statusBarHeight,
|
|
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
|
|
hudPanelIndex: 0,
|
|
configSourceText: '顺序赛配置',
|
|
centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
|
|
autoRotateEnabled: initialAutoRotateEnabled,
|
|
lockAnimationLevel: !!storedUserSettings.lockAnimationLevel,
|
|
lockSideButtonPlacement: !!storedUserSettings.lockSideButtonPlacement,
|
|
lockAutoRotate: !!storedUserSettings.lockAutoRotate,
|
|
lockCompassTuning: !!storedUserSettings.lockCompassTuning,
|
|
lockScaleRulerVisible: !!storedUserSettings.lockScaleRulerVisible,
|
|
lockScaleRulerAnchor: !!storedUserSettings.lockScaleRulerAnchor,
|
|
lockNorthReference: !!storedUserSettings.lockNorthReference,
|
|
lockHeartRateDevice: !!storedUserSettings.lockHeartRateDevice,
|
|
sideButtonPlacement: initialSideButtonPlacement,
|
|
gameInfoTitle: '当前游戏',
|
|
gameInfoSubtitle: '未开始',
|
|
gameInfoLocalRows: [],
|
|
gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
|
|
panelTimerText: '00:00:00',
|
|
panelTimerFxClass: '',
|
|
panelMileageText: '0m',
|
|
panelMileageFxClass: '',
|
|
panelActionTagText: '目标',
|
|
panelDistanceTagText: '点距',
|
|
panelDistanceValueText: '--',
|
|
panelDistanceUnitText: '',
|
|
panelProgressText: '0/0',
|
|
showPunchHintBanner: true,
|
|
gameSessionStatus: 'idle',
|
|
gameModeText: '顺序赛',
|
|
gpsLockEnabled: false,
|
|
gpsLockAvailable: false,
|
|
locationSourceMode: 'real',
|
|
locationSourceText: '真实定位',
|
|
mockBridgeConnected: false,
|
|
mockBridgeStatusText: '未连接',
|
|
mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockCoordText: '--',
|
|
mockSpeedText: '--',
|
|
heartRateSourceMode: 'real',
|
|
heartRateSourceText: '真实心率',
|
|
mockHeartRateBridgeConnected: false,
|
|
mockHeartRateBridgeStatusText: '未连接',
|
|
mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
|
|
mockHeartRateText: '--',
|
|
panelSpeedValueText: '0',
|
|
panelSpeedFxClass: '',
|
|
panelTelemetryTone: 'blue',
|
|
panelHeartRateZoneNameText: '--',
|
|
panelHeartRateZoneRangeText: '',
|
|
heartRateConnected: false,
|
|
heartRateStatusText: '心率带未连接',
|
|
heartRateDeviceText: '--',
|
|
panelHeartRateValueText: '--',
|
|
panelHeartRateFxClass: '',
|
|
panelHeartRateUnitText: '',
|
|
panelCaloriesValueText: '0',
|
|
panelCaloriesUnitText: 'kcal',
|
|
panelAverageSpeedValueText: '0',
|
|
panelAverageSpeedUnitText: 'km/h',
|
|
panelAccuracyValueText: '--',
|
|
panelAccuracyUnitText: '',
|
|
deviceHeadingText: '--',
|
|
devicePoseText: '竖持',
|
|
headingConfidenceText: '低',
|
|
accelerometerText: '--',
|
|
gyroscopeText: '--',
|
|
deviceMotionText: '--',
|
|
compassSourceText: '无数据',
|
|
compassTuningProfile: 'balanced',
|
|
compassTuningProfileText: '平衡',
|
|
punchButtonText: '打点',
|
|
punchButtonEnabled: false,
|
|
skipButtonEnabled: false,
|
|
punchHintText: '等待进入检查点范围',
|
|
punchFeedbackVisible: false,
|
|
punchFeedbackText: '',
|
|
punchFeedbackTone: 'neutral',
|
|
contentCardVisible: false,
|
|
contentCardTitle: '',
|
|
contentCardBody: '',
|
|
punchButtonFxClass: '',
|
|
panelProgressFxClass: '',
|
|
panelDistanceFxClass: '',
|
|
punchFeedbackFxClass: '',
|
|
contentCardFxClass: '',
|
|
mapPulseVisible: false,
|
|
mapPulseLeftPx: 0,
|
|
mapPulseTopPx: 0,
|
|
mapPulseFxClass: '',
|
|
stageFxVisible: false,
|
|
stageFxClass: '',
|
|
compassTicks: buildCompassTicks(),
|
|
compassLabels: buildCompassLabels(),
|
|
...buildSideButtonVisibility('shown'),
|
|
...buildSideButtonState({
|
|
sideButtonMode: 'shown',
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: false,
|
|
showCenterScaleRuler: initialShowCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
|
|
skipButtonEnabled: false,
|
|
gameSessionStatus: 'idle',
|
|
gpsLockEnabled: false,
|
|
gpsLockAvailable: false,
|
|
}),
|
|
...buildCenterScaleRulerPatch({
|
|
...(mapEngine.getInitialData() as MapPageData),
|
|
showCenterScaleRuler: initialShowCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
|
|
stageWidth: 0,
|
|
stageHeight: 0,
|
|
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
|
|
zoom: 0,
|
|
centerTileY: 0,
|
|
tileSizePx: 0,
|
|
}),
|
|
})
|
|
},
|
|
|
|
onReady() {
|
|
stageCanvasAttached = false
|
|
this.measureStageAndCanvas()
|
|
this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
|
|
},
|
|
|
|
onShow() {
|
|
if (mapEngine) {
|
|
mapEngine.handleAppShow()
|
|
}
|
|
},
|
|
|
|
onHide() {
|
|
if (mapEngine) {
|
|
mapEngine.handleAppHide()
|
|
}
|
|
},
|
|
|
|
onUnload() {
|
|
clearGameInfoPanelSyncTimer()
|
|
clearCenterScaleRulerSyncTimer()
|
|
clearCenterScaleRulerUpdateTimer()
|
|
clearPunchHintDismissTimer()
|
|
clearHudFxTimer('timer')
|
|
clearHudFxTimer('mileage')
|
|
clearHudFxTimer('speed')
|
|
clearHudFxTimer('heartRate')
|
|
if (mapEngine) {
|
|
mapEngine.destroy()
|
|
mapEngine = null
|
|
}
|
|
stageCanvasAttached = false
|
|
},
|
|
|
|
loadMapConfigFromRemote(configUrl: string, configLabel: string) {
|
|
const currentEngine = mapEngine
|
|
if (!currentEngine) {
|
|
return
|
|
}
|
|
|
|
this.setData({
|
|
configSourceText: configLabel,
|
|
configStatusText: `加载中: ${configLabel}`,
|
|
})
|
|
|
|
loadRemoteMapConfig(configUrl)
|
|
.then((config) => {
|
|
if (mapEngine !== currentEngine) {
|
|
return
|
|
}
|
|
|
|
currentEngine.applyRemoteMapConfig(config)
|
|
})
|
|
.catch((error) => {
|
|
if (mapEngine !== currentEngine) {
|
|
return
|
|
}
|
|
|
|
const errorMessage = error && error.message ? error.message : '未知错误'
|
|
this.setData({
|
|
configStatusText: `载入失败: ${errorMessage}`,
|
|
statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
|
|
})
|
|
})
|
|
},
|
|
|
|
measureStageAndCanvas(onApplied?: () => void) {
|
|
const page = this
|
|
const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
|
|
const fallbackRect = getFallbackStageRect()
|
|
const rect: MapEngineStageRect = {
|
|
width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
|
|
height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
|
|
left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
|
|
top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
|
|
}
|
|
|
|
const currentEngine = mapEngine
|
|
if (!currentEngine) {
|
|
return
|
|
}
|
|
|
|
currentEngine.setStage(rect)
|
|
if (onApplied) {
|
|
onApplied()
|
|
}
|
|
|
|
if (stageCanvasAttached) {
|
|
return
|
|
}
|
|
|
|
const canvasQuery = wx.createSelectorQuery().in(page)
|
|
canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
|
|
canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
|
|
canvasQuery.exec((canvasRes) => {
|
|
const canvasRef = canvasRes[0] as any
|
|
const labelCanvasRef = canvasRes[1] as any
|
|
if (!canvasRef || !canvasRef.node) {
|
|
page.setData({
|
|
statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
|
|
})
|
|
return
|
|
}
|
|
|
|
const dpr = wx.getSystemInfoSync().pixelRatio || 1
|
|
try {
|
|
currentEngine.attachCanvas(
|
|
canvasRef.node,
|
|
rect.width,
|
|
rect.height,
|
|
dpr,
|
|
labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
|
|
)
|
|
stageCanvasAttached = true
|
|
} catch (error) {
|
|
page.setData({
|
|
statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const query = wx.createSelectorQuery().in(page)
|
|
query.select('.map-stage').boundingClientRect()
|
|
query.exec((res) => {
|
|
const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
|
|
applyStage(rect)
|
|
})
|
|
},
|
|
|
|
handleTouchStart(event: WechatMiniprogram.TouchEvent) {
|
|
if (mapEngine) {
|
|
mapEngine.handleTouchStart(event)
|
|
}
|
|
},
|
|
|
|
handleTouchMove(event: WechatMiniprogram.TouchEvent) {
|
|
if (mapEngine) {
|
|
mapEngine.handleTouchMove(event)
|
|
}
|
|
},
|
|
|
|
handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
|
|
if (mapEngine) {
|
|
mapEngine.handleTouchEnd(event)
|
|
}
|
|
},
|
|
|
|
handleTouchCancel() {
|
|
if (mapEngine) {
|
|
mapEngine.handleTouchCancel()
|
|
}
|
|
},
|
|
|
|
handleRecenter() {
|
|
if (mapEngine) {
|
|
mapEngine.handleRecenter()
|
|
}
|
|
},
|
|
|
|
handleRotateStep() {
|
|
if (mapEngine) {
|
|
mapEngine.handleRotateStep()
|
|
}
|
|
},
|
|
|
|
handleRotationReset() {
|
|
if (mapEngine) {
|
|
mapEngine.handleRotationReset()
|
|
}
|
|
},
|
|
|
|
handleSetManualMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetManualMode()
|
|
}
|
|
},
|
|
|
|
handleSetNorthUpMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetNorthUpMode()
|
|
}
|
|
},
|
|
|
|
handleSetHeadingUpMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetHeadingUpMode()
|
|
}
|
|
},
|
|
|
|
handleCycleNorthReferenceMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleCycleNorthReferenceMode()
|
|
}
|
|
},
|
|
|
|
handleAutoRotateCalibrate() {
|
|
if (mapEngine) {
|
|
mapEngine.handleAutoRotateCalibrate()
|
|
}
|
|
},
|
|
|
|
handleToggleGpsTracking() {
|
|
if (mapEngine) {
|
|
mapEngine.handleToggleGpsTracking()
|
|
}
|
|
},
|
|
|
|
handleSetRealLocationMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetRealLocationMode()
|
|
}
|
|
},
|
|
|
|
handleSetMockLocationMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetMockLocationMode()
|
|
}
|
|
},
|
|
|
|
handleConnectMockLocationBridge() {
|
|
if (mapEngine) {
|
|
mapEngine.handleConnectMockLocationBridge()
|
|
}
|
|
},
|
|
|
|
handleConnectAllMockSources() {
|
|
if (!mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleConnectMockLocationBridge()
|
|
mapEngine.handleSetMockLocationMode()
|
|
mapEngine.handleSetMockHeartRateMode()
|
|
mapEngine.handleConnectMockHeartRateBridge()
|
|
},
|
|
|
|
handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
|
|
this.setData({
|
|
mockBridgeUrlDraft: event.detail.value,
|
|
})
|
|
},
|
|
|
|
handleSaveMockBridgeUrl() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
|
|
}
|
|
},
|
|
|
|
handleDisconnectMockLocationBridge() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDisconnectMockLocationBridge()
|
|
}
|
|
},
|
|
|
|
handleSetRealHeartRateMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetRealHeartRateMode()
|
|
}
|
|
},
|
|
|
|
handleSetMockHeartRateMode() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetMockHeartRateMode()
|
|
}
|
|
},
|
|
|
|
handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
|
|
this.setData({
|
|
mockHeartRateBridgeUrlDraft: event.detail.value,
|
|
})
|
|
},
|
|
|
|
handleSaveMockHeartRateBridgeUrl() {
|
|
if (mapEngine) {
|
|
mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
|
|
}
|
|
},
|
|
|
|
handleConnectMockHeartRateBridge() {
|
|
if (mapEngine) {
|
|
mapEngine.handleConnectMockHeartRateBridge()
|
|
}
|
|
},
|
|
|
|
handleDisconnectMockHeartRateBridge() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDisconnectMockHeartRateBridge()
|
|
}
|
|
},
|
|
|
|
handleConnectHeartRate() {
|
|
if (mapEngine) {
|
|
mapEngine.handleConnectHeartRate()
|
|
}
|
|
},
|
|
|
|
handleDisconnectHeartRate() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDisconnectHeartRate()
|
|
}
|
|
},
|
|
|
|
handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
|
|
if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
|
|
mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
|
|
}
|
|
},
|
|
|
|
handleClearPreferredHeartRateDevice() {
|
|
if (this.data.lockHeartRateDevice) {
|
|
return
|
|
}
|
|
if (mapEngine) {
|
|
mapEngine.handleClearPreferredHeartRateDevice()
|
|
}
|
|
},
|
|
|
|
handleDebugHeartRateBlue() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDebugHeartRateTone('blue')
|
|
}
|
|
},
|
|
|
|
handleDebugHeartRatePurple() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDebugHeartRateTone('purple')
|
|
}
|
|
},
|
|
|
|
handleDebugHeartRateGreen() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDebugHeartRateTone('green')
|
|
}
|
|
},
|
|
|
|
handleDebugHeartRateYellow() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDebugHeartRateTone('yellow')
|
|
}
|
|
},
|
|
|
|
handleDebugHeartRateOrange() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDebugHeartRateTone('orange')
|
|
}
|
|
},
|
|
|
|
handleDebugHeartRateRed() {
|
|
if (mapEngine) {
|
|
mapEngine.handleDebugHeartRateTone('red')
|
|
}
|
|
},
|
|
|
|
handleClearDebugHeartRate() {
|
|
if (mapEngine) {
|
|
mapEngine.handleClearDebugHeartRate()
|
|
}
|
|
},
|
|
|
|
handleToggleOsmReference() {
|
|
if (mapEngine) {
|
|
mapEngine.handleToggleOsmReference()
|
|
}
|
|
},
|
|
|
|
handleStartGame() {
|
|
if (mapEngine) {
|
|
mapEngine.handleStartGame()
|
|
}
|
|
},
|
|
|
|
handleLoadClassicConfig() {
|
|
this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
|
|
},
|
|
|
|
handleLoadScoreOConfig() {
|
|
this.loadMapConfigFromRemote(SCORE_O_REMOTE_GAME_CONFIG_URL, '积分赛配置')
|
|
},
|
|
|
|
handleForceExitGame() {
|
|
if (!mapEngine || this.data.gameSessionStatus !== 'running') {
|
|
return
|
|
}
|
|
|
|
wx.showModal({
|
|
title: '确认退出',
|
|
content: '确认强制结束当前对局并返回开始前状态?',
|
|
confirmText: '确认退出',
|
|
cancelText: '取消',
|
|
success: (result) => {
|
|
if (result.confirm && mapEngine) {
|
|
mapEngine.handleForceExitGame()
|
|
}
|
|
},
|
|
})
|
|
},
|
|
|
|
handleSkipAction() {
|
|
if (!mapEngine || !this.data.skipButtonEnabled) {
|
|
return
|
|
}
|
|
|
|
if (!mapEngine.shouldConfirmSkipAction()) {
|
|
mapEngine.handleSkipAction()
|
|
return
|
|
}
|
|
|
|
wx.showModal({
|
|
title: '确认跳点',
|
|
content: '确认跳过当前检查点并切换到下一个目标点?',
|
|
confirmText: '确认跳过',
|
|
cancelText: '取消',
|
|
success: (result) => {
|
|
if (result.confirm && mapEngine) {
|
|
mapEngine.handleSkipAction()
|
|
}
|
|
},
|
|
})
|
|
},
|
|
|
|
handleClearMapTestArtifacts() {
|
|
if (mapEngine) {
|
|
mapEngine.handleClearMapTestArtifacts()
|
|
}
|
|
},
|
|
|
|
syncGameInfoPanelSnapshot() {
|
|
if (!mapEngine) {
|
|
return
|
|
}
|
|
|
|
const snapshot = mapEngine.getGameInfoSnapshot()
|
|
const localRows = snapshot.localRows.concat([
|
|
{ label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
|
|
{ label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
|
|
{ label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
|
|
{ label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
|
|
{ label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
|
|
{ label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
|
|
{ label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
|
|
{ label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
|
|
])
|
|
this.setData({
|
|
gameInfoTitle: snapshot.title,
|
|
gameInfoSubtitle: snapshot.subtitle,
|
|
gameInfoLocalRows: localRows,
|
|
gameInfoGlobalRows: snapshot.globalRows,
|
|
})
|
|
},
|
|
|
|
syncResultSceneSnapshot() {
|
|
if (!mapEngine) {
|
|
return
|
|
}
|
|
|
|
const snapshot = mapEngine.getResultSceneSnapshot()
|
|
this.setData({
|
|
resultSceneTitle: snapshot.title,
|
|
resultSceneSubtitle: snapshot.subtitle,
|
|
resultSceneHeroLabel: snapshot.heroLabel,
|
|
resultSceneHeroValue: snapshot.heroValue,
|
|
resultSceneRows: snapshot.rows,
|
|
})
|
|
},
|
|
|
|
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,
|
|
showSystemSettingsPanel: false,
|
|
showGameInfoPanel: true,
|
|
...buildSideButtonState({
|
|
sideButtonMode: this.data.sideButtonMode,
|
|
showGameInfoPanel: true,
|
|
showSystemSettingsPanel: false,
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
|
gameSessionStatus: this.data.gameSessionStatus,
|
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
|
}),
|
|
})
|
|
},
|
|
|
|
handleCloseGameInfoPanel() {
|
|
clearGameInfoPanelSyncTimer()
|
|
this.setData({
|
|
showGameInfoPanel: false,
|
|
...buildSideButtonState({
|
|
sideButtonMode: this.data.sideButtonMode,
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: this.data.showSystemSettingsPanel,
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
|
gameSessionStatus: this.data.gameSessionStatus,
|
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
|
}),
|
|
})
|
|
},
|
|
|
|
handleGameInfoPanelTap() {},
|
|
|
|
handleResultSceneTap() {},
|
|
|
|
handleCloseResultScene() {
|
|
this.setData({
|
|
showResultScene: false,
|
|
})
|
|
},
|
|
|
|
handleRestartFromResult() {
|
|
if (!mapEngine) {
|
|
return
|
|
}
|
|
this.setData({
|
|
showResultScene: false,
|
|
}, () => {
|
|
if (mapEngine) {
|
|
mapEngine.handleStartGame()
|
|
}
|
|
})
|
|
},
|
|
|
|
handleOpenSystemSettingsPanel() {
|
|
clearGameInfoPanelSyncTimer()
|
|
this.setData({
|
|
showDebugPanel: false,
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: true,
|
|
...buildSideButtonState({
|
|
sideButtonMode: this.data.sideButtonMode,
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: true,
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
|
gameSessionStatus: this.data.gameSessionStatus,
|
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
|
}),
|
|
})
|
|
},
|
|
|
|
handleCloseSystemSettingsPanel() {
|
|
this.setData({
|
|
showSystemSettingsPanel: false,
|
|
...buildSideButtonState({
|
|
sideButtonMode: this.data.sideButtonMode,
|
|
showGameInfoPanel: this.data.showGameInfoPanel,
|
|
showSystemSettingsPanel: false,
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
|
gameSessionStatus: this.data.gameSessionStatus,
|
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
|
}),
|
|
})
|
|
},
|
|
|
|
handleSystemSettingsPanelTap() {},
|
|
|
|
handleSetAnimationLevelStandard() {
|
|
if (this.data.lockAnimationLevel || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetAnimationLevel('standard')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
animationLevel: 'standard',
|
|
})
|
|
},
|
|
|
|
handleSetAnimationLevelLite() {
|
|
if (this.data.lockAnimationLevel || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetAnimationLevel('lite')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
animationLevel: 'lite',
|
|
})
|
|
},
|
|
|
|
handleSetSideButtonPlacementLeft() {
|
|
if (this.data.lockSideButtonPlacement) {
|
|
return
|
|
}
|
|
this.setData({
|
|
sideButtonPlacement: 'left',
|
|
})
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
sideButtonPlacement: 'left',
|
|
})
|
|
},
|
|
|
|
handleSetSideButtonPlacementRight() {
|
|
if (this.data.lockSideButtonPlacement) {
|
|
return
|
|
}
|
|
this.setData({
|
|
sideButtonPlacement: 'right',
|
|
})
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
sideButtonPlacement: 'right',
|
|
})
|
|
},
|
|
|
|
handleSetAutoRotateEnabledOn() {
|
|
if (this.data.lockAutoRotate || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetHeadingUpMode()
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
autoRotateEnabled: true,
|
|
})
|
|
},
|
|
|
|
handleSetAutoRotateEnabledOff() {
|
|
if (this.data.lockAutoRotate || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetManualMode()
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
autoRotateEnabled: false,
|
|
})
|
|
},
|
|
|
|
handleSetCompassTuningSmooth() {
|
|
if (this.data.lockCompassTuning || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetCompassTuningProfile('smooth')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
compassTuningProfile: 'smooth',
|
|
})
|
|
},
|
|
|
|
handleSetCompassTuningBalanced() {
|
|
if (this.data.lockCompassTuning || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetCompassTuningProfile('balanced')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
compassTuningProfile: 'balanced',
|
|
})
|
|
},
|
|
|
|
handleSetCompassTuningResponsive() {
|
|
if (this.data.lockCompassTuning || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetCompassTuningProfile('responsive')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
compassTuningProfile: 'responsive',
|
|
})
|
|
},
|
|
|
|
handleSetNorthReferenceMagnetic() {
|
|
if (this.data.lockNorthReference || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetNorthReferenceMode('magnetic')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
northReferenceMode: 'magnetic',
|
|
})
|
|
},
|
|
|
|
handleSetNorthReferenceTrue() {
|
|
if (this.data.lockNorthReference || !mapEngine) {
|
|
return
|
|
}
|
|
mapEngine.handleSetNorthReferenceMode('true')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
northReferenceMode: 'true',
|
|
})
|
|
},
|
|
|
|
handleToggleSettingLock(event: WechatMiniprogram.TouchEvent) {
|
|
const key = event.currentTarget.dataset.key as SettingLockKey | undefined
|
|
if (!key) {
|
|
return
|
|
}
|
|
const nextValue = !this.data[key]
|
|
this.setData({
|
|
[key]: nextValue,
|
|
} as Record<string, boolean>)
|
|
persistStoredUserSettings(toggleStoredSettingLock(loadStoredUserSettings(), key))
|
|
},
|
|
|
|
handleOverlayTouch() {},
|
|
|
|
handlePunchAction() {
|
|
if (!this.data.punchButtonEnabled) {
|
|
return
|
|
}
|
|
|
|
if (mapEngine) {
|
|
mapEngine.handlePunchAction()
|
|
}
|
|
},
|
|
|
|
handleCloseContentCard() {
|
|
if (mapEngine) {
|
|
mapEngine.closeContentCard()
|
|
}
|
|
},
|
|
|
|
handleClosePunchHint() {
|
|
clearPunchHintDismissTimer()
|
|
this.setData({
|
|
showPunchHintBanner: false,
|
|
})
|
|
},
|
|
|
|
handlePunchHintTap() {},
|
|
|
|
handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
|
|
this.setData({
|
|
hudPanelIndex: event.detail.current || 0,
|
|
})
|
|
},
|
|
|
|
handleCycleSideButtons() {
|
|
const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
|
|
this.setData({
|
|
...buildSideButtonVisibility(nextMode),
|
|
...buildSideButtonState({
|
|
sideButtonMode: nextMode,
|
|
showGameInfoPanel: this.data.showGameInfoPanel,
|
|
showSystemSettingsPanel: this.data.showSystemSettingsPanel,
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
|
gameSessionStatus: this.data.gameSessionStatus,
|
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
|
}),
|
|
})
|
|
},
|
|
handleToggleGpsLock() {
|
|
if (mapEngine) {
|
|
mapEngine.handleToggleGpsLock()
|
|
}
|
|
},
|
|
handleToggleMapRotateMode() {
|
|
if (!mapEngine || this.data.lockAutoRotate) {
|
|
return
|
|
}
|
|
|
|
if (this.data.orientationMode === 'heading-up') {
|
|
mapEngine.handleSetManualMode()
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
autoRotateEnabled: false,
|
|
})
|
|
return
|
|
}
|
|
|
|
mapEngine.handleSetHeadingUpMode()
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
autoRotateEnabled: true,
|
|
})
|
|
},
|
|
handleToggleDebugPanel() {
|
|
const nextShowDebugPanel = !this.data.showDebugPanel
|
|
if (!nextShowDebugPanel) {
|
|
clearGameInfoPanelSyncTimer()
|
|
}
|
|
if (mapEngine) {
|
|
mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
|
|
}
|
|
this.setData({
|
|
showDebugPanel: nextShowDebugPanel,
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: false,
|
|
...buildSideButtonState({
|
|
sideButtonMode: this.data.sideButtonMode,
|
|
showGameInfoPanel: false,
|
|
showSystemSettingsPanel: false,
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
|
gameSessionStatus: this.data.gameSessionStatus,
|
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
|
}),
|
|
})
|
|
},
|
|
|
|
handleCloseDebugPanel() {
|
|
if (mapEngine) {
|
|
mapEngine.setDiagnosticUiEnabled(false)
|
|
}
|
|
this.setData({
|
|
showDebugPanel: false,
|
|
...buildSideButtonState({
|
|
sideButtonMode: this.data.sideButtonMode,
|
|
showGameInfoPanel: this.data.showGameInfoPanel,
|
|
showSystemSettingsPanel: this.data.showSystemSettingsPanel,
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
|
gameSessionStatus: this.data.gameSessionStatus,
|
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
|
}),
|
|
})
|
|
},
|
|
|
|
applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
|
|
this.data.showCenterScaleRuler = nextEnabled
|
|
this.data.centerScaleRulerAnchorMode = nextAnchorMode
|
|
clearCenterScaleRulerSyncTimer()
|
|
clearCenterScaleRulerUpdateTimer()
|
|
|
|
const syncRulerFromEngine = () => {
|
|
if (!mapEngine) {
|
|
return
|
|
}
|
|
const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
|
|
updateCenterScaleRulerInputCache(engineSnapshot)
|
|
const mergedData = {
|
|
...centerScaleRulerInputCache,
|
|
...this.data,
|
|
showCenterScaleRuler: nextEnabled,
|
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
|
} as MapPageData
|
|
|
|
this.setData({
|
|
...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
|
|
showCenterScaleRuler: nextEnabled,
|
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
|
...buildCenterScaleRulerPatch(mergedData),
|
|
...buildSideButtonState(mergedData),
|
|
})
|
|
}
|
|
|
|
if (!nextEnabled) {
|
|
syncRulerFromEngine()
|
|
return
|
|
}
|
|
|
|
this.setData({
|
|
showCenterScaleRuler: true,
|
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
|
...buildSideButtonState({
|
|
...this.data,
|
|
showCenterScaleRuler: true,
|
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
|
} as MapPageData),
|
|
})
|
|
|
|
this.measureStageAndCanvas(() => {
|
|
syncRulerFromEngine()
|
|
})
|
|
|
|
centerScaleRulerSyncTimer = setTimeout(() => {
|
|
centerScaleRulerSyncTimer = 0
|
|
if (!this.data.showCenterScaleRuler) {
|
|
return
|
|
}
|
|
syncRulerFromEngine()
|
|
}, 96) as unknown as number
|
|
},
|
|
|
|
handleSetCenterScaleRulerVisibleOn() {
|
|
if (this.data.lockScaleRulerVisible) {
|
|
return
|
|
}
|
|
this.applyCenterScaleRulerSettings(true, this.data.centerScaleRulerAnchorMode)
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
showCenterScaleRuler: true,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
})
|
|
},
|
|
|
|
handleSetCenterScaleRulerVisibleOff() {
|
|
if (this.data.lockScaleRulerVisible) {
|
|
return
|
|
}
|
|
this.applyCenterScaleRulerSettings(false, this.data.centerScaleRulerAnchorMode)
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
showCenterScaleRuler: false,
|
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
|
})
|
|
},
|
|
|
|
handleSetCenterScaleRulerAnchorScreenCenter() {
|
|
if (this.data.lockScaleRulerAnchor) {
|
|
return
|
|
}
|
|
this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'screen-center')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: 'screen-center',
|
|
})
|
|
},
|
|
|
|
handleSetCenterScaleRulerAnchorCompassCenter() {
|
|
if (this.data.lockScaleRulerAnchor) {
|
|
return
|
|
}
|
|
this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'compass-center')
|
|
persistStoredUserSettings({
|
|
...loadStoredUserSettings(),
|
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
|
centerScaleRulerAnchorMode: 'compass-center',
|
|
})
|
|
},
|
|
|
|
handleToggleCenterScaleRulerAnchor() {
|
|
if (!this.data.showCenterScaleRuler) {
|
|
return
|
|
}
|
|
|
|
const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
|
|
? 'compass-center'
|
|
: 'screen-center'
|
|
const engineSnapshot = mapEngine ? (mapEngine.getInitialData() as Partial<MapPageData>) : {}
|
|
updateCenterScaleRulerInputCache(engineSnapshot)
|
|
this.data.centerScaleRulerAnchorMode = nextAnchorMode
|
|
const mergedData = {
|
|
...centerScaleRulerInputCache,
|
|
...this.data,
|
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
|
} as MapPageData
|
|
|
|
this.setData({
|
|
...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, true),
|
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
|
...buildCenterScaleRulerPatch(mergedData),
|
|
...buildSideButtonState(mergedData),
|
|
})
|
|
|
|
if (this.data.showGameInfoPanel) {
|
|
this.syncGameInfoPanelSnapshot()
|
|
}
|
|
},
|
|
|
|
handleDebugPanelTap() {},
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|