feedbackConfig.ts 10 KB

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