完善地图交互、动画与罗盘调试
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||
|
||||
export type FeedbackCueKey =
|
||||
| 'session_started'
|
||||
| 'session_finished'
|
||||
@@ -14,7 +16,9 @@ export type UiPunchFeedbackMotion = 'none' | 'pop' | 'success' | 'warning'
|
||||
export type UiContentCardMotion = 'none' | 'pop' | 'finish'
|
||||
export type UiPunchButtonMotion = 'none' | 'ready' | 'warning'
|
||||
export type UiMapPulseMotion = 'none' | 'ready' | 'control' | 'finish'
|
||||
export type UiStageMotion = 'none' | 'finish'
|
||||
export type UiStageMotion = 'none' | 'control' | 'finish'
|
||||
export type UiHudProgressMotion = 'none' | 'success' | 'finish'
|
||||
export type UiHudDistanceMotion = 'none' | 'success'
|
||||
|
||||
export interface HapticCueConfig {
|
||||
enabled: boolean
|
||||
@@ -28,6 +32,8 @@ export interface UiCueConfig {
|
||||
punchButtonMotion: UiPunchButtonMotion
|
||||
mapPulseMotion: UiMapPulseMotion
|
||||
stageMotion: UiStageMotion
|
||||
hudProgressMotion: UiHudProgressMotion
|
||||
hudDistanceMotion: UiHudDistanceMotion
|
||||
durationMs: number
|
||||
}
|
||||
|
||||
@@ -41,6 +47,10 @@ export interface GameUiEffectsConfig {
|
||||
cues: Record<FeedbackCueKey, UiCueConfig>
|
||||
}
|
||||
|
||||
export interface ResolvedGameUiEffectsConfig extends GameUiEffectsConfig {
|
||||
animationLevel: AnimationLevel
|
||||
}
|
||||
|
||||
export interface PartialHapticCueConfig {
|
||||
enabled?: boolean
|
||||
pattern?: HapticPattern
|
||||
@@ -53,6 +63,8 @@ export interface PartialUiCueConfig {
|
||||
punchButtonMotion?: UiPunchButtonMotion
|
||||
mapPulseMotion?: UiMapPulseMotion
|
||||
stageMotion?: UiStageMotion
|
||||
hudProgressMotion?: UiHudProgressMotion
|
||||
hudDistanceMotion?: UiHudDistanceMotion
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
@@ -84,15 +96,15 @@ export const DEFAULT_GAME_HAPTICS_CONFIG: GameHapticsConfig = {
|
||||
export const DEFAULT_GAME_UI_EFFECTS_CONFIG: GameUiEffectsConfig = {
|
||||
enabled: true,
|
||||
cues: {
|
||||
session_started: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
||||
session_finished: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
||||
'control_completed:start': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'none', durationMs: 0 },
|
||||
'control_completed:control': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'none', durationMs: 0 },
|
||||
'control_completed:finish': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'finish', punchButtonMotion: 'none', mapPulseMotion: 'finish', stageMotion: 'finish', durationMs: 0 },
|
||||
'punch_feedback:warning': { enabled: true, punchFeedbackMotion: 'warning', contentCardMotion: 'none', punchButtonMotion: 'warning', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 560 },
|
||||
'guidance:searching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
||||
'guidance:approaching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
||||
'guidance:ready': { enabled: true, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'ready', mapPulseMotion: 'ready', stageMotion: 'none', durationMs: 900 },
|
||||
session_started: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||
session_finished: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||
'control_completed:start': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'control', hudProgressMotion: 'success', hudDistanceMotion: 'success', durationMs: 560 },
|
||||
'control_completed:control': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'control', hudProgressMotion: 'success', hudDistanceMotion: 'success', durationMs: 560 },
|
||||
'control_completed:finish': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'finish', punchButtonMotion: 'none', mapPulseMotion: 'finish', stageMotion: 'finish', hudProgressMotion: 'finish', hudDistanceMotion: 'success', durationMs: 680 },
|
||||
'punch_feedback:warning': { enabled: true, punchFeedbackMotion: 'warning', contentCardMotion: 'none', punchButtonMotion: 'warning', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 560 },
|
||||
'guidance:searching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||
'guidance:approaching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||
'guidance:ready': { enabled: true, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'ready', mapPulseMotion: 'ready', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 900 },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -115,6 +127,8 @@ function mergeUiCue(baseCue: UiCueConfig, override?: PartialUiCueConfig): UiCueC
|
||||
punchButtonMotion: override && override.punchButtonMotion ? override.punchButtonMotion : baseCue.punchButtonMotion,
|
||||
mapPulseMotion: override && override.mapPulseMotion ? override.mapPulseMotion : baseCue.mapPulseMotion,
|
||||
stageMotion: override && override.stageMotion ? override.stageMotion : baseCue.stageMotion,
|
||||
hudProgressMotion: override && override.hudProgressMotion ? override.hudProgressMotion : baseCue.hudProgressMotion,
|
||||
hudDistanceMotion: override && override.hudDistanceMotion ? override.hudDistanceMotion : baseCue.hudDistanceMotion,
|
||||
durationMs: clampDuration(Number(override && override.durationMs), baseCue.durationMs),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DEFAULT_GAME_AUDIO_CONFIG, type GameAudioConfig } from '../audio/audioConfig'
|
||||
import { SoundDirector } from '../audio/soundDirector'
|
||||
import { type GameEffect } from '../core/gameResult'
|
||||
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||
import {
|
||||
DEFAULT_GAME_HAPTICS_CONFIG,
|
||||
DEFAULT_GAME_UI_EFFECTS_CONFIG,
|
||||
@@ -41,6 +42,9 @@ export class FeedbackDirector {
|
||||
|
||||
reset(): void {
|
||||
this.soundDirector.resetContexts()
|
||||
this.uiEffectDirector.clearPunchButtonMotion()
|
||||
this.uiEffectDirector.clearHudProgressMotion()
|
||||
this.uiEffectDirector.clearHudDistanceMotion()
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
@@ -49,6 +53,10 @@ export class FeedbackDirector {
|
||||
this.uiEffectDirector.destroy()
|
||||
}
|
||||
|
||||
setAnimationLevel(level: AnimationLevel): void {
|
||||
this.uiEffectDirector.setAnimationLevel(level)
|
||||
}
|
||||
|
||||
setAppAudioMode(mode: 'foreground' | 'background'): void {
|
||||
this.soundDirector.setAppAudioMode(mode)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { type GameEffect } from '../core/gameResult'
|
||||
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||
import {
|
||||
DEFAULT_GAME_UI_EFFECTS_CONFIG,
|
||||
type FeedbackCueKey,
|
||||
type GameUiEffectsConfig,
|
||||
type UiContentCardMotion,
|
||||
type UiHudDistanceMotion,
|
||||
type UiHudProgressMotion,
|
||||
type UiMapPulseMotion,
|
||||
type UiPunchButtonMotion,
|
||||
type UiPunchFeedbackMotion,
|
||||
type UiCueConfig,
|
||||
type UiStageMotion,
|
||||
} from './feedbackConfig'
|
||||
|
||||
@@ -14,6 +18,8 @@ export interface UiEffectHost {
|
||||
showPunchFeedback: (text: string, tone: 'neutral' | 'success' | 'warning', motionClass?: string) => void
|
||||
showContentCard: (title: string, body: string, motionClass?: string) => void
|
||||
setPunchButtonFxClass: (className: string) => void
|
||||
setHudProgressFxClass: (className: string) => void
|
||||
setHudDistanceFxClass: (className: string) => void
|
||||
showMapPulse: (controlId: string, motionClass?: string) => void
|
||||
showStageFx: (className: string) => void
|
||||
}
|
||||
@@ -23,30 +29,46 @@ export class UiEffectDirector {
|
||||
config: GameUiEffectsConfig
|
||||
host: UiEffectHost
|
||||
punchButtonMotionTimer: number
|
||||
hudProgressMotionTimer: number
|
||||
hudDistanceMotionTimer: number
|
||||
punchButtonMotionToggle: boolean
|
||||
animationLevel: AnimationLevel
|
||||
|
||||
constructor(host: UiEffectHost, config: GameUiEffectsConfig = DEFAULT_GAME_UI_EFFECTS_CONFIG) {
|
||||
this.enabled = true
|
||||
this.host = host
|
||||
this.config = config
|
||||
this.punchButtonMotionTimer = 0
|
||||
this.hudProgressMotionTimer = 0
|
||||
this.hudDistanceMotionTimer = 0
|
||||
this.punchButtonMotionToggle = false
|
||||
this.animationLevel = 'standard'
|
||||
}
|
||||
|
||||
configure(config: GameUiEffectsConfig): void {
|
||||
this.config = config
|
||||
this.clearPunchButtonMotion()
|
||||
this.clearHudProgressMotion()
|
||||
this.clearHudDistanceMotion()
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled
|
||||
if (!enabled) {
|
||||
this.clearPunchButtonMotion()
|
||||
this.clearHudProgressMotion()
|
||||
this.clearHudDistanceMotion()
|
||||
}
|
||||
}
|
||||
|
||||
setAnimationLevel(level: AnimationLevel): void {
|
||||
this.animationLevel = level
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.clearPunchButtonMotion()
|
||||
this.clearHudProgressMotion()
|
||||
this.clearHudDistanceMotion()
|
||||
}
|
||||
|
||||
clearPunchButtonMotion(): void {
|
||||
@@ -57,6 +79,22 @@ export class UiEffectDirector {
|
||||
this.host.setPunchButtonFxClass('')
|
||||
}
|
||||
|
||||
clearHudProgressMotion(): void {
|
||||
if (this.hudProgressMotionTimer) {
|
||||
clearTimeout(this.hudProgressMotionTimer)
|
||||
this.hudProgressMotionTimer = 0
|
||||
}
|
||||
this.host.setHudProgressFxClass('')
|
||||
}
|
||||
|
||||
clearHudDistanceMotion(): void {
|
||||
if (this.hudDistanceMotionTimer) {
|
||||
clearTimeout(this.hudDistanceMotionTimer)
|
||||
this.hudDistanceMotionTimer = 0
|
||||
}
|
||||
this.host.setHudDistanceFxClass('')
|
||||
}
|
||||
|
||||
getPunchFeedbackMotionClass(motion: UiPunchFeedbackMotion): string {
|
||||
if (motion === 'warning') {
|
||||
return 'game-punch-feedback--fx-warning'
|
||||
@@ -94,12 +132,32 @@ export class UiEffectDirector {
|
||||
}
|
||||
|
||||
getStageMotionClass(motion: UiStageMotion): string {
|
||||
if (motion === 'control') {
|
||||
return 'map-stage__stage-fx--control'
|
||||
}
|
||||
if (motion === 'finish') {
|
||||
return 'map-stage__stage-fx--finish'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
getHudProgressMotionClass(motion: UiHudProgressMotion): string {
|
||||
if (motion === 'finish') {
|
||||
return 'race-panel__progress--fx-finish'
|
||||
}
|
||||
if (motion === 'success') {
|
||||
return 'race-panel__progress--fx-success'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
getHudDistanceMotionClass(motion: UiHudDistanceMotion): string {
|
||||
if (motion === 'success') {
|
||||
return 'race-panel__metric-group--fx-distance-success'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
triggerPunchButtonMotion(motion: UiPunchButtonMotion, durationMs: number): void {
|
||||
if (motion === 'none') {
|
||||
return
|
||||
@@ -121,7 +179,37 @@ export class UiEffectDirector {
|
||||
}, durationMs) as unknown as number
|
||||
}
|
||||
|
||||
getCue(key: FeedbackCueKey) {
|
||||
triggerHudProgressMotion(motion: UiHudProgressMotion, durationMs: number): void {
|
||||
const className = this.getHudProgressMotionClass(motion)
|
||||
if (!className) {
|
||||
return
|
||||
}
|
||||
this.host.setHudProgressFxClass(className)
|
||||
if (this.hudProgressMotionTimer) {
|
||||
clearTimeout(this.hudProgressMotionTimer)
|
||||
}
|
||||
this.hudProgressMotionTimer = setTimeout(() => {
|
||||
this.hudProgressMotionTimer = 0
|
||||
this.host.setHudProgressFxClass('')
|
||||
}, durationMs) as unknown as number
|
||||
}
|
||||
|
||||
triggerHudDistanceMotion(motion: UiHudDistanceMotion, durationMs: number): void {
|
||||
const className = this.getHudDistanceMotionClass(motion)
|
||||
if (!className) {
|
||||
return
|
||||
}
|
||||
this.host.setHudDistanceFxClass(className)
|
||||
if (this.hudDistanceMotionTimer) {
|
||||
clearTimeout(this.hudDistanceMotionTimer)
|
||||
}
|
||||
this.hudDistanceMotionTimer = setTimeout(() => {
|
||||
this.hudDistanceMotionTimer = 0
|
||||
this.host.setHudDistanceFxClass('')
|
||||
}, durationMs) as unknown as number
|
||||
}
|
||||
|
||||
getCue(key: FeedbackCueKey): UiCueConfig | null {
|
||||
if (!this.enabled || !this.config.enabled) {
|
||||
return null
|
||||
}
|
||||
@@ -131,7 +219,16 @@ export class UiEffectDirector {
|
||||
return null
|
||||
}
|
||||
|
||||
return cue
|
||||
if (this.animationLevel === 'standard') {
|
||||
return cue
|
||||
}
|
||||
|
||||
return {
|
||||
...cue,
|
||||
stageMotion: 'none' as const,
|
||||
hudDistanceMotion: 'none' as const,
|
||||
durationMs: cue.durationMs > 0 ? Math.max(260, Math.round(cue.durationMs * 0.6)) : 0,
|
||||
}
|
||||
}
|
||||
|
||||
handleEffects(effects: GameEffect[]): void {
|
||||
@@ -172,6 +269,10 @@ export class UiEffectDirector {
|
||||
if (cue && cue.stageMotion !== 'none') {
|
||||
this.host.showStageFx(this.getStageMotionClass(cue.stageMotion))
|
||||
}
|
||||
if (cue) {
|
||||
this.triggerHudProgressMotion(cue.hudProgressMotion, cue.durationMs)
|
||||
this.triggerHudDistanceMotion(cue.hudDistanceMotion, cue.durationMs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -188,10 +289,14 @@ export class UiEffectDirector {
|
||||
|
||||
if (effect.type === 'session_finished') {
|
||||
this.clearPunchButtonMotion()
|
||||
this.clearHudProgressMotion()
|
||||
this.clearHudDistanceMotion()
|
||||
}
|
||||
|
||||
if (effect.type === 'session_cancelled') {
|
||||
this.clearPunchButtonMotion()
|
||||
this.clearHudProgressMotion()
|
||||
this.clearHudDistanceMotion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,44 @@ function interpolateHeadingDeg(currentDeg: number, targetDeg: number, factor: nu
|
||||
return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor)
|
||||
}
|
||||
|
||||
function resolveMotionCompassHeadingDeg(
|
||||
alpha: number | null,
|
||||
beta: number | null,
|
||||
gamma: number | null,
|
||||
): number | null {
|
||||
if (alpha === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (beta === null || gamma === null) {
|
||||
return normalizeHeadingDeg(360 - alpha)
|
||||
}
|
||||
|
||||
const alphaRad = alpha * Math.PI / 180
|
||||
const betaRad = beta * Math.PI / 180
|
||||
const gammaRad = gamma * Math.PI / 180
|
||||
|
||||
const cA = Math.cos(alphaRad)
|
||||
const sA = Math.sin(alphaRad)
|
||||
const sB = Math.sin(betaRad)
|
||||
const cG = Math.cos(gammaRad)
|
||||
const sG = Math.sin(gammaRad)
|
||||
|
||||
const headingX = -cA * sG - sA * sB * cG
|
||||
const headingY = -sA * sG + cA * sB * cG
|
||||
|
||||
if (Math.abs(headingX) < 1e-6 && Math.abs(headingY) < 1e-6) {
|
||||
return normalizeHeadingDeg(360 - alpha)
|
||||
}
|
||||
|
||||
let headingRad = Math.atan2(headingX, headingY)
|
||||
if (headingRad < 0) {
|
||||
headingRad += Math.PI * 2
|
||||
}
|
||||
|
||||
return normalizeHeadingDeg(headingRad * 180 / Math.PI)
|
||||
}
|
||||
|
||||
function getApproxDistanceMeters(
|
||||
a: { lon: number; lat: number },
|
||||
b: { lon: number; lat: number },
|
||||
@@ -530,13 +568,13 @@ export class TelemetryRuntime {
|
||||
}
|
||||
|
||||
if (event.type === 'device_motion_updated') {
|
||||
const nextDeviceHeadingDeg = event.alpha === null
|
||||
const motionHeadingDeg = resolveMotionCompassHeadingDeg(event.alpha, event.beta, event.gamma)
|
||||
const nextDeviceHeadingDeg = motionHeadingDeg === null
|
||||
? this.state.deviceHeadingDeg
|
||||
: (() => {
|
||||
const nextHeadingDeg = normalizeHeadingDeg(360 - event.alpha * 180 / Math.PI)
|
||||
return this.state.deviceHeadingDeg === null
|
||||
? nextHeadingDeg
|
||||
: interpolateHeadingDeg(this.state.deviceHeadingDeg, nextHeadingDeg, DEVICE_HEADING_SMOOTHING_ALPHA)
|
||||
? motionHeadingDeg
|
||||
: interpolateHeadingDeg(this.state.deviceHeadingDeg, motionHeadingDeg, DEVICE_HEADING_SMOOTHING_ALPHA)
|
||||
})()
|
||||
|
||||
this.state = {
|
||||
|
||||
Reference in New Issue
Block a user