uiEffectDirector.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { type GameEffect } from '../core/gameResult'
  2. import {
  3. DEFAULT_GAME_UI_EFFECTS_CONFIG,
  4. type FeedbackCueKey,
  5. type GameUiEffectsConfig,
  6. type UiContentCardMotion,
  7. type UiMapPulseMotion,
  8. type UiPunchButtonMotion,
  9. type UiPunchFeedbackMotion,
  10. type UiStageMotion,
  11. } from './feedbackConfig'
  12. export interface UiEffectHost {
  13. showPunchFeedback: (text: string, tone: 'neutral' | 'success' | 'warning', motionClass?: string) => void
  14. showContentCard: (title: string, body: string, motionClass?: string) => void
  15. setPunchButtonFxClass: (className: string) => void
  16. showMapPulse: (controlId: string, motionClass?: string) => void
  17. showStageFx: (className: string) => void
  18. }
  19. export class UiEffectDirector {
  20. enabled: boolean
  21. config: GameUiEffectsConfig
  22. host: UiEffectHost
  23. punchButtonMotionTimer: number
  24. punchButtonMotionToggle: boolean
  25. constructor(host: UiEffectHost, config: GameUiEffectsConfig = DEFAULT_GAME_UI_EFFECTS_CONFIG) {
  26. this.enabled = true
  27. this.host = host
  28. this.config = config
  29. this.punchButtonMotionTimer = 0
  30. this.punchButtonMotionToggle = false
  31. }
  32. configure(config: GameUiEffectsConfig): void {
  33. this.config = config
  34. this.clearPunchButtonMotion()
  35. }
  36. setEnabled(enabled: boolean): void {
  37. this.enabled = enabled
  38. if (!enabled) {
  39. this.clearPunchButtonMotion()
  40. }
  41. }
  42. destroy(): void {
  43. this.clearPunchButtonMotion()
  44. }
  45. clearPunchButtonMotion(): void {
  46. if (this.punchButtonMotionTimer) {
  47. clearTimeout(this.punchButtonMotionTimer)
  48. this.punchButtonMotionTimer = 0
  49. }
  50. this.host.setPunchButtonFxClass('')
  51. }
  52. getPunchFeedbackMotionClass(motion: UiPunchFeedbackMotion): string {
  53. if (motion === 'warning') {
  54. return 'game-punch-feedback--fx-warning'
  55. }
  56. if (motion === 'success') {
  57. return 'game-punch-feedback--fx-success'
  58. }
  59. if (motion === 'pop') {
  60. return 'game-punch-feedback--fx-pop'
  61. }
  62. return ''
  63. }
  64. getContentCardMotionClass(motion: UiContentCardMotion): string {
  65. if (motion === 'finish') {
  66. return 'game-content-card--fx-finish'
  67. }
  68. if (motion === 'pop') {
  69. return 'game-content-card--fx-pop'
  70. }
  71. return ''
  72. }
  73. getMapPulseMotionClass(motion: UiMapPulseMotion): string {
  74. if (motion === 'ready') {
  75. return 'map-stage__map-pulse--ready'
  76. }
  77. if (motion === 'finish') {
  78. return 'map-stage__map-pulse--finish'
  79. }
  80. if (motion === 'control') {
  81. return 'map-stage__map-pulse--control'
  82. }
  83. return ''
  84. }
  85. getStageMotionClass(motion: UiStageMotion): string {
  86. if (motion === 'finish') {
  87. return 'map-stage__stage-fx--finish'
  88. }
  89. return ''
  90. }
  91. triggerPunchButtonMotion(motion: UiPunchButtonMotion, durationMs: number): void {
  92. if (motion === 'none') {
  93. return
  94. }
  95. this.punchButtonMotionToggle = !this.punchButtonMotionToggle
  96. const variant = this.punchButtonMotionToggle ? 'a' : 'b'
  97. const className = motion === 'warning'
  98. ? `map-punch-button--fx-warning-${variant}`
  99. : `map-punch-button--fx-ready-${variant}`
  100. this.host.setPunchButtonFxClass(className)
  101. if (this.punchButtonMotionTimer) {
  102. clearTimeout(this.punchButtonMotionTimer)
  103. }
  104. this.punchButtonMotionTimer = setTimeout(() => {
  105. this.punchButtonMotionTimer = 0
  106. this.host.setPunchButtonFxClass('')
  107. }, durationMs) as unknown as number
  108. }
  109. getCue(key: FeedbackCueKey) {
  110. if (!this.enabled || !this.config.enabled) {
  111. return null
  112. }
  113. const cue = this.config.cues[key]
  114. if (!cue || !cue.enabled) {
  115. return null
  116. }
  117. return cue
  118. }
  119. handleEffects(effects: GameEffect[]): void {
  120. for (const effect of effects) {
  121. if (effect.type === 'punch_feedback' && effect.tone === 'warning') {
  122. const cue = this.getCue('punch_feedback:warning')
  123. this.host.showPunchFeedback(
  124. effect.text,
  125. effect.tone,
  126. cue ? this.getPunchFeedbackMotionClass(cue.punchFeedbackMotion) : '',
  127. )
  128. if (cue) {
  129. this.triggerPunchButtonMotion(cue.punchButtonMotion, cue.durationMs)
  130. }
  131. continue
  132. }
  133. if (effect.type === 'control_completed') {
  134. const key: FeedbackCueKey = effect.controlKind === 'start'
  135. ? 'control_completed:start'
  136. : effect.controlKind === 'finish'
  137. ? 'control_completed:finish'
  138. : 'control_completed:control'
  139. const cue = this.getCue(key)
  140. this.host.showPunchFeedback(
  141. `完成 ${typeof effect.sequence === 'number' ? effect.sequence : effect.label}`,
  142. 'success',
  143. cue ? this.getPunchFeedbackMotionClass(cue.punchFeedbackMotion) : '',
  144. )
  145. this.host.showContentCard(
  146. effect.displayTitle,
  147. effect.displayBody,
  148. cue ? this.getContentCardMotionClass(cue.contentCardMotion) : '',
  149. )
  150. if (cue && cue.mapPulseMotion !== 'none') {
  151. this.host.showMapPulse(effect.controlId, this.getMapPulseMotionClass(cue.mapPulseMotion))
  152. }
  153. if (cue && cue.stageMotion !== 'none') {
  154. this.host.showStageFx(this.getStageMotionClass(cue.stageMotion))
  155. }
  156. continue
  157. }
  158. if (effect.type === 'guidance_state_changed' && effect.guidanceState === 'ready') {
  159. const cue = this.getCue('guidance:ready')
  160. if (cue) {
  161. this.triggerPunchButtonMotion(cue.punchButtonMotion, cue.durationMs)
  162. if (cue.mapPulseMotion !== 'none' && effect.controlId) {
  163. this.host.showMapPulse(effect.controlId, this.getMapPulseMotionClass(cue.mapPulseMotion))
  164. }
  165. }
  166. continue
  167. }
  168. if (effect.type === 'session_finished') {
  169. this.clearPunchButtonMotion()
  170. }
  171. if (effect.type === 'session_cancelled') {
  172. this.clearPunchButtonMotion()
  173. }
  174. }
  175. }
  176. }