312 lines
9.4 KiB
TypeScript
312 lines
9.4 KiB
TypeScript
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'
|
|
|
|
export interface UiEffectHost {
|
|
showPunchFeedback: (text: string, tone: 'neutral' | 'success' | 'warning', motionClass?: string) => void
|
|
showContentCard: (title: string, body: string, motionClass?: string, options?: { contentKey?: string; autoPopup?: boolean; once?: boolean; priority?: number }) => void
|
|
setPunchButtonFxClass: (className: string) => void
|
|
setHudProgressFxClass: (className: string) => void
|
|
setHudDistanceFxClass: (className: string) => void
|
|
showMapPulse: (controlId: string, motionClass?: string) => void
|
|
showStageFx: (className: string) => void
|
|
}
|
|
|
|
export class UiEffectDirector {
|
|
enabled: boolean
|
|
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 {
|
|
if (this.punchButtonMotionTimer) {
|
|
clearTimeout(this.punchButtonMotionTimer)
|
|
this.punchButtonMotionTimer = 0
|
|
}
|
|
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'
|
|
}
|
|
if (motion === 'success') {
|
|
return 'game-punch-feedback--fx-success'
|
|
}
|
|
if (motion === 'pop') {
|
|
return 'game-punch-feedback--fx-pop'
|
|
}
|
|
return ''
|
|
}
|
|
|
|
getContentCardMotionClass(motion: UiContentCardMotion): string {
|
|
if (motion === 'finish') {
|
|
return 'game-content-card--fx-finish'
|
|
}
|
|
if (motion === 'pop') {
|
|
return 'game-content-card--fx-pop'
|
|
}
|
|
return ''
|
|
}
|
|
|
|
getMapPulseMotionClass(motion: UiMapPulseMotion): string {
|
|
if (motion === 'ready') {
|
|
return 'map-stage__map-pulse--ready'
|
|
}
|
|
if (motion === 'finish') {
|
|
return 'map-stage__map-pulse--finish'
|
|
}
|
|
if (motion === 'control') {
|
|
return 'map-stage__map-pulse--control'
|
|
}
|
|
return ''
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
this.punchButtonMotionToggle = !this.punchButtonMotionToggle
|
|
const variant = this.punchButtonMotionToggle ? 'a' : 'b'
|
|
const className = motion === 'warning'
|
|
? `map-punch-button--fx-warning-${variant}`
|
|
: `map-punch-button--fx-ready-${variant}`
|
|
|
|
this.host.setPunchButtonFxClass(className)
|
|
if (this.punchButtonMotionTimer) {
|
|
clearTimeout(this.punchButtonMotionTimer)
|
|
}
|
|
this.punchButtonMotionTimer = setTimeout(() => {
|
|
this.punchButtonMotionTimer = 0
|
|
this.host.setPunchButtonFxClass('')
|
|
}, durationMs) as unknown as number
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const cue = this.config.cues[key]
|
|
if (!cue || !cue.enabled) {
|
|
return null
|
|
}
|
|
|
|
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 {
|
|
for (const effect of effects) {
|
|
if (effect.type === 'punch_feedback' && effect.tone === 'warning') {
|
|
const cue = this.getCue('punch_feedback:warning')
|
|
this.host.showPunchFeedback(
|
|
effect.text,
|
|
effect.tone,
|
|
cue ? this.getPunchFeedbackMotionClass(cue.punchFeedbackMotion) : '',
|
|
)
|
|
if (cue) {
|
|
this.triggerPunchButtonMotion(cue.punchButtonMotion, cue.durationMs)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (effect.type === 'control_completed') {
|
|
const key: FeedbackCueKey = effect.controlKind === 'start'
|
|
? 'control_completed:start'
|
|
: effect.controlKind === 'finish'
|
|
? 'control_completed:finish'
|
|
: 'control_completed:control'
|
|
const cue = this.getCue(key)
|
|
this.host.showPunchFeedback(
|
|
`完成 ${typeof effect.sequence === 'number' ? effect.sequence : effect.label}`,
|
|
'success',
|
|
cue ? this.getPunchFeedbackMotionClass(cue.punchFeedbackMotion) : '',
|
|
)
|
|
if (effect.controlKind !== 'finish' && effect.displayAutoPopup) {
|
|
this.host.showContentCard(
|
|
effect.displayTitle,
|
|
effect.displayBody,
|
|
cue ? this.getContentCardMotionClass(cue.contentCardMotion) : '',
|
|
{
|
|
contentKey: effect.controlId,
|
|
autoPopup: effect.displayAutoPopup,
|
|
once: effect.displayOnce,
|
|
priority: effect.displayPriority,
|
|
},
|
|
)
|
|
}
|
|
if (cue && cue.mapPulseMotion !== 'none') {
|
|
this.host.showMapPulse(effect.controlId, this.getMapPulseMotionClass(cue.mapPulseMotion))
|
|
}
|
|
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
|
|
}
|
|
|
|
if (effect.type === 'guidance_state_changed' && effect.guidanceState === 'ready') {
|
|
const cue = this.getCue('guidance:ready')
|
|
if (cue) {
|
|
this.triggerPunchButtonMotion(cue.punchButtonMotion, cue.durationMs)
|
|
if (cue.mapPulseMotion !== 'none' && effect.controlId) {
|
|
this.host.showMapPulse(effect.controlId, this.getMapPulseMotionClass(cue.mapPulseMotion))
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (effect.type === 'session_finished') {
|
|
this.clearPunchButtonMotion()
|
|
this.clearHudProgressMotion()
|
|
this.clearHudDistanceMotion()
|
|
}
|
|
|
|
if (effect.type === 'session_cancelled') {
|
|
this.clearPunchButtonMotion()
|
|
this.clearHudProgressMotion()
|
|
this.clearHudDistanceMotion()
|
|
}
|
|
}
|
|
}
|
|
}
|