| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- 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) => 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) : '',
- )
- this.host.showContentCard(
- effect.displayTitle,
- effect.displayBody,
- cue ? this.getContentCardMotionClass(cue.contentCardMotion) : '',
- )
- 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()
- }
- }
- }
- }
|