feedbackConfig.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { type AnimationLevel } from '../../utils/animationLevel'
  2. export type FeedbackCueKey =
  3. | 'session_started'
  4. | 'session_finished'
  5. | 'hint:changed'
  6. | 'control_completed:start'
  7. | 'control_completed:control'
  8. | 'control_completed:finish'
  9. | 'punch_feedback:warning'
  10. | 'guidance:searching'
  11. | 'guidance:distant'
  12. | 'guidance:approaching'
  13. | 'guidance:ready'
  14. export type HapticPattern = 'short' | 'long'
  15. export type UiPunchFeedbackMotion = 'none' | 'pop' | 'success' | 'warning'
  16. export type UiContentCardMotion = 'none' | 'pop' | 'finish'
  17. export type UiPunchButtonMotion = 'none' | 'ready' | 'warning'
  18. export type UiMapPulseMotion = 'none' | 'ready' | 'control' | 'finish'
  19. export type UiStageMotion = 'none' | 'control' | 'finish'
  20. export type UiHudProgressMotion = 'none' | 'success' | 'finish'
  21. export type UiHudDistanceMotion = 'none' | 'success'
  22. export interface HapticCueConfig {
  23. enabled: boolean
  24. pattern: HapticPattern
  25. }
  26. export interface UiCueConfig {
  27. enabled: boolean
  28. punchFeedbackMotion: UiPunchFeedbackMotion
  29. contentCardMotion: UiContentCardMotion
  30. punchButtonMotion: UiPunchButtonMotion
  31. mapPulseMotion: UiMapPulseMotion
  32. stageMotion: UiStageMotion
  33. hudProgressMotion: UiHudProgressMotion
  34. hudDistanceMotion: UiHudDistanceMotion
  35. durationMs: number
  36. }
  37. export interface GameHapticsConfig {
  38. enabled: boolean
  39. cues: Record<FeedbackCueKey, HapticCueConfig>
  40. }
  41. export interface GameUiEffectsConfig {
  42. enabled: boolean
  43. cues: Record<FeedbackCueKey, UiCueConfig>
  44. }
  45. export interface ResolvedGameUiEffectsConfig extends GameUiEffectsConfig {
  46. animationLevel: AnimationLevel
  47. }
  48. export interface PartialHapticCueConfig {
  49. enabled?: boolean
  50. pattern?: HapticPattern
  51. }
  52. export interface PartialUiCueConfig {
  53. enabled?: boolean
  54. punchFeedbackMotion?: UiPunchFeedbackMotion
  55. contentCardMotion?: UiContentCardMotion
  56. punchButtonMotion?: UiPunchButtonMotion
  57. mapPulseMotion?: UiMapPulseMotion
  58. stageMotion?: UiStageMotion
  59. hudProgressMotion?: UiHudProgressMotion
  60. hudDistanceMotion?: UiHudDistanceMotion
  61. durationMs?: number
  62. }
  63. export interface GameHapticsConfigOverrides {
  64. enabled?: boolean
  65. cues?: Partial<Record<FeedbackCueKey, PartialHapticCueConfig>>
  66. }
  67. export interface GameUiEffectsConfigOverrides {
  68. enabled?: boolean
  69. cues?: Partial<Record<FeedbackCueKey, PartialUiCueConfig>>
  70. }
  71. export const DEFAULT_GAME_HAPTICS_CONFIG: GameHapticsConfig = {
  72. enabled: true,
  73. cues: {
  74. session_started: { enabled: false, pattern: 'short' },
  75. session_finished: { enabled: true, pattern: 'long' },
  76. 'hint:changed': { enabled: true, pattern: 'short' },
  77. 'control_completed:start': { enabled: true, pattern: 'short' },
  78. 'control_completed:control': { enabled: true, pattern: 'short' },
  79. 'control_completed:finish': { enabled: true, pattern: 'long' },
  80. 'punch_feedback:warning': { enabled: true, pattern: 'short' },
  81. 'guidance:searching': { enabled: false, pattern: 'short' },
  82. 'guidance:distant': { enabled: true, pattern: 'short' },
  83. 'guidance:approaching': { enabled: true, pattern: 'short' },
  84. 'guidance:ready': { enabled: true, pattern: 'short' },
  85. },
  86. }
  87. export const DEFAULT_GAME_UI_EFFECTS_CONFIG: GameUiEffectsConfig = {
  88. enabled: true,
  89. cues: {
  90. session_started: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
  91. session_finished: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
  92. 'hint:changed': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
  93. 'control_completed:start': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'control', hudProgressMotion: 'success', hudDistanceMotion: 'success', durationMs: 560 },
  94. 'control_completed:control': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'control', hudProgressMotion: 'success', hudDistanceMotion: 'success', durationMs: 560 },
  95. 'control_completed:finish': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'finish', punchButtonMotion: 'none', mapPulseMotion: 'finish', stageMotion: 'finish', hudProgressMotion: 'finish', hudDistanceMotion: 'success', durationMs: 680 },
  96. 'punch_feedback:warning': { enabled: true, punchFeedbackMotion: 'warning', contentCardMotion: 'none', punchButtonMotion: 'warning', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 560 },
  97. 'guidance:searching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
  98. 'guidance:distant': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
  99. 'guidance:approaching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
  100. 'guidance:ready': { enabled: true, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'ready', mapPulseMotion: 'ready', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 900 },
  101. },
  102. }
  103. function clampDuration(value: number, fallback: number): number {
  104. return Number.isFinite(value) && value >= 0 ? value : fallback
  105. }
  106. function mergeHapticCue(baseCue: HapticCueConfig, override?: PartialHapticCueConfig): HapticCueConfig {
  107. return {
  108. enabled: override && override.enabled !== undefined ? !!override.enabled : baseCue.enabled,
  109. pattern: override && override.pattern ? override.pattern : baseCue.pattern,
  110. }
  111. }
  112. function mergeUiCue(baseCue: UiCueConfig, override?: PartialUiCueConfig): UiCueConfig {
  113. return {
  114. enabled: override && override.enabled !== undefined ? !!override.enabled : baseCue.enabled,
  115. punchFeedbackMotion: override && override.punchFeedbackMotion ? override.punchFeedbackMotion : baseCue.punchFeedbackMotion,
  116. contentCardMotion: override && override.contentCardMotion ? override.contentCardMotion : baseCue.contentCardMotion,
  117. punchButtonMotion: override && override.punchButtonMotion ? override.punchButtonMotion : baseCue.punchButtonMotion,
  118. mapPulseMotion: override && override.mapPulseMotion ? override.mapPulseMotion : baseCue.mapPulseMotion,
  119. stageMotion: override && override.stageMotion ? override.stageMotion : baseCue.stageMotion,
  120. hudProgressMotion: override && override.hudProgressMotion ? override.hudProgressMotion : baseCue.hudProgressMotion,
  121. hudDistanceMotion: override && override.hudDistanceMotion ? override.hudDistanceMotion : baseCue.hudDistanceMotion,
  122. durationMs: clampDuration(Number(override && override.durationMs), baseCue.durationMs),
  123. }
  124. }
  125. export function mergeGameHapticsConfig(overrides?: GameHapticsConfigOverrides | null): GameHapticsConfig {
  126. const cues: GameHapticsConfig['cues'] = {
  127. session_started: mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues.session_started, overrides && overrides.cues ? overrides.cues.session_started : undefined),
  128. session_finished: mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues.session_finished, overrides && overrides.cues ? overrides.cues.session_finished : undefined),
  129. 'hint:changed': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['hint:changed'], overrides && overrides.cues ? overrides.cues['hint:changed'] : undefined),
  130. 'control_completed:start': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['control_completed:start'], overrides && overrides.cues ? overrides.cues['control_completed:start'] : undefined),
  131. 'control_completed:control': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['control_completed:control'], overrides && overrides.cues ? overrides.cues['control_completed:control'] : undefined),
  132. 'control_completed:finish': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['control_completed:finish'], overrides && overrides.cues ? overrides.cues['control_completed:finish'] : undefined),
  133. 'punch_feedback:warning': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['punch_feedback:warning'], overrides && overrides.cues ? overrides.cues['punch_feedback:warning'] : undefined),
  134. 'guidance:searching': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['guidance:searching'], overrides && overrides.cues ? overrides.cues['guidance:searching'] : undefined),
  135. 'guidance:distant': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['guidance:distant'], overrides && overrides.cues ? overrides.cues['guidance:distant'] : undefined),
  136. 'guidance:approaching': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['guidance:approaching'], overrides && overrides.cues ? overrides.cues['guidance:approaching'] : undefined),
  137. 'guidance:ready': mergeHapticCue(DEFAULT_GAME_HAPTICS_CONFIG.cues['guidance:ready'], overrides && overrides.cues ? overrides.cues['guidance:ready'] : undefined),
  138. }
  139. return {
  140. enabled: overrides && overrides.enabled !== undefined ? !!overrides.enabled : DEFAULT_GAME_HAPTICS_CONFIG.enabled,
  141. cues,
  142. }
  143. }
  144. export function mergeGameUiEffectsConfig(overrides?: GameUiEffectsConfigOverrides | null): GameUiEffectsConfig {
  145. const cues: GameUiEffectsConfig['cues'] = {
  146. session_started: mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues.session_started, overrides && overrides.cues ? overrides.cues.session_started : undefined),
  147. session_finished: mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues.session_finished, overrides && overrides.cues ? overrides.cues.session_finished : undefined),
  148. 'hint:changed': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['hint:changed'], overrides && overrides.cues ? overrides.cues['hint:changed'] : undefined),
  149. 'control_completed:start': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['control_completed:start'], overrides && overrides.cues ? overrides.cues['control_completed:start'] : undefined),
  150. 'control_completed:control': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['control_completed:control'], overrides && overrides.cues ? overrides.cues['control_completed:control'] : undefined),
  151. 'control_completed:finish': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['control_completed:finish'], overrides && overrides.cues ? overrides.cues['control_completed:finish'] : undefined),
  152. 'punch_feedback:warning': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['punch_feedback:warning'], overrides && overrides.cues ? overrides.cues['punch_feedback:warning'] : undefined),
  153. 'guidance:searching': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['guidance:searching'], overrides && overrides.cues ? overrides.cues['guidance:searching'] : undefined),
  154. 'guidance:distant': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['guidance:distant'], overrides && overrides.cues ? overrides.cues['guidance:distant'] : undefined),
  155. 'guidance:approaching': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['guidance:approaching'], overrides && overrides.cues ? overrides.cues['guidance:approaching'] : undefined),
  156. 'guidance:ready': mergeUiCue(DEFAULT_GAME_UI_EFFECTS_CONFIG.cues['guidance:ready'], overrides && overrides.cues ? overrides.cues['guidance:ready'] : undefined),
  157. }
  158. return {
  159. enabled: overrides && overrides.enabled !== undefined ? !!overrides.enabled : DEFAULT_GAME_UI_EFFECTS_CONFIG.enabled,
  160. cues,
  161. }
  162. }