@umituz/react-native-mascot 1.0.4 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +60 -0
  2. package/package.json +2 -1
  3. package/src/application/services/AnimationStateManager.ts +69 -0
  4. package/src/application/services/AppearanceManagement.ts +40 -0
  5. package/src/application/services/MascotService.ts +42 -33
  6. package/src/application/services/PersonalityManagement.ts +39 -0
  7. package/src/application/services/StateHistory.ts +55 -0
  8. package/src/application/services/StateMachine.ts +154 -0
  9. package/src/application/services/StateTransitions.ts +73 -0
  10. package/src/application.ts +40 -0
  11. package/src/assets/index.ts +14 -19
  12. package/src/core.ts +62 -0
  13. package/src/domain/entities/Mascot.ts +186 -127
  14. package/src/domain/types/AnimationStateTypes.ts +148 -0
  15. package/src/domain/types/MascotTypes.ts +9 -0
  16. package/src/domain/value-objects/AnimationState.ts +126 -0
  17. package/src/index.ts +9 -99
  18. package/src/infrastructure/controllers/AnimationController.ts +26 -122
  19. package/src/infrastructure/controllers/AnimationPlayer.ts +104 -0
  20. package/src/infrastructure/controllers/AnimationTimer.ts +62 -0
  21. package/src/infrastructure/controllers/EventManager.ts +108 -0
  22. package/src/infrastructure/di/Container.ts +73 -10
  23. package/src/infrastructure/managers/AssetManager.ts +134 -63
  24. package/src/infrastructure/managers/MascotBuilder.ts +89 -0
  25. package/src/infrastructure/managers/MascotFactory.ts +24 -176
  26. package/src/infrastructure/managers/MascotTemplates.ts +151 -0
  27. package/src/infrastructure/utils/LRUCache.ts +218 -0
  28. package/src/infrastructure.ts +24 -0
  29. package/src/presentation/components/LottieMascot.tsx +85 -0
  30. package/src/presentation/components/MascotView.tsx +42 -233
  31. package/src/presentation/components/SVGMascot.tsx +61 -0
  32. package/src/presentation/contexts/MascotContext.tsx +2 -3
  33. package/src/presentation/hooks/useMascot.ts +118 -39
  34. package/src/presentation/hooks/useMascotAnimation.ts +9 -15
  35. package/src/presentation/hooks/useMascotState.ts +213 -0
  36. package/src/presentation.ts +37 -0
  37. package/src/types.d.ts +4 -0
  38. package/src/application/index.ts +0 -8
  39. package/src/domain/value-objects/index.ts +0 -9
@@ -1,17 +1,14 @@
1
1
  /**
2
- * MascotView Component
3
- * Main component for rendering mascots
2
+ * MascotView Component (OPTIMIZED - 100 lines)
3
+ * Main container with memoization
4
4
  */
5
5
 
6
- import React, { useRef, useEffect, useState } from 'react';
7
- import { View, StyleSheet, Animated, TouchableOpacity, ViewStyle } from 'react-native';
8
- import LottieView from 'lottie-react-native';
9
- import type { AnimationObject } from 'lottie-react-native';
10
- import { Svg } from 'react-native-svg';
6
+ import React, { memo, useMemo, useEffect, useCallback } from 'react';
7
+ import { StyleSheet, Animated, TouchableOpacity, ViewStyle } from 'react-native';
11
8
  import type { Mascot } from '../../domain/entities/Mascot';
12
9
  import type { MascotAnimation } from '../../domain/types/MascotTypes';
13
-
14
- type LottieAnimationSource = string | AnimationObject | { uri: string };
10
+ import { LottieMascot } from './LottieMascot';
11
+ import { SVGMascot } from './SVGMascot';
15
12
 
16
13
  export interface MascotViewProps {
17
14
  mascot: Mascot | null;
@@ -25,7 +22,17 @@ export interface MascotViewProps {
25
22
  resizeMode?: 'cover' | 'contain' | 'center';
26
23
  }
27
24
 
28
- export const MascotView: React.FC<MascotViewProps> = ({
25
+ function arePropsEqual(prevProps: MascotViewProps, nextProps: MascotViewProps): boolean {
26
+ return (
27
+ prevProps.mascot === nextProps.mascot &&
28
+ prevProps.animation === nextProps.animation &&
29
+ prevProps.size === nextProps.size &&
30
+ prevProps.onPress === nextProps.onPress &&
31
+ prevProps.onLongPress === nextProps.onLongPress
32
+ );
33
+ }
34
+
35
+ const MascotViewComponent: React.FC<MascotViewProps> = ({
29
36
  mascot,
30
37
  animation,
31
38
  size = 200,
@@ -36,65 +43,39 @@ export const MascotView: React.FC<MascotViewProps> = ({
36
43
  onAnimationFinish,
37
44
  resizeMode = 'contain',
38
45
  }) => {
39
- const [opacity] = useState(new Animated.Value(0));
40
- const [scale] = useState(new Animated.Value(0));
46
+ // Create animated values once
47
+ const animatedValues = useMemo(
48
+ () => ({
49
+ opacity: new Animated.Value(0),
50
+ scale: new Animated.Value(0),
51
+ }),
52
+ []
53
+ );
54
+
55
+ const { opacity, scale } = animatedValues;
41
56
 
57
+ // Animation
42
58
  useEffect(() => {
43
59
  if (mascot?.state.isVisible) {
44
60
  Animated.parallel([
45
- Animated.timing(opacity, {
46
- toValue: 1,
47
- duration: 300,
48
- useNativeDriver: true,
49
- }),
50
- Animated.spring(scale, {
51
- toValue: 1,
52
- friction: 8,
53
- tension: 40,
54
- useNativeDriver: true,
55
- }),
61
+ Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true }),
62
+ Animated.spring(scale, { toValue: 1, friction: 8, tension: 40, useNativeDriver: true }),
56
63
  ]).start();
57
64
  } else {
58
- Animated.timing(opacity, {
59
- toValue: 0,
60
- duration: 200,
61
- useNativeDriver: true,
62
- }).start();
65
+ Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true }).start();
63
66
  }
64
67
  }, [mascot?.state.isVisible, opacity, scale]);
65
68
 
66
- const handlePress = () => {
67
- if (mascot?.touchEnabled && onPress) {
68
- onPress();
69
- }
70
- };
71
-
72
- const handleLongPress = () => {
73
- if (mascot?.touchEnabled && onLongPress) {
74
- onLongPress();
75
- }
76
- };
77
-
78
- if (!mascot || !mascot.state.isVisible) {
79
- return null;
80
- }
69
+ // Memoized styles and handlers
70
+ const animatedStyle = useMemo(() => ({ opacity, transform: [{ scale }] }), [opacity, scale]);
71
+ const containerStyle = useMemo(() => [styles.container, { width: size, height: size }, style], [size, style]);
72
+ const handlePress = useCallback(() => mascot?.touchEnabled && onPress?.(), [mascot?.touchEnabled, onPress]);
73
+ const handleLongPress = useCallback(() => mascot?.touchEnabled && onLongPress?.(), [mascot?.touchEnabled, onLongPress]);
81
74
 
82
- const animatedStyle = {
83
- opacity,
84
- transform: [{ scale }],
85
- };
86
-
87
- const containerStyle = [
88
- styles.container,
89
- { width: size, height: size },
90
- style,
91
- ];
75
+ if (!mascot || !mascot.state.isVisible) return null;
92
76
 
93
77
  return (
94
- <Animated.View
95
- style={animatedStyle}
96
- testID={testID}
97
- >
78
+ <Animated.View style={animatedStyle} testID={testID}>
98
79
  <TouchableOpacity
99
80
  style={containerStyle}
100
81
  onPress={handlePress}
@@ -103,194 +84,22 @@ export const MascotView: React.FC<MascotViewProps> = ({
103
84
  activeOpacity={0.8}
104
85
  >
105
86
  {mascot.type === 'lottie' ? (
106
- <LottieMascot
107
- mascot={mascot}
108
- animation={animation}
109
- resizeMode={resizeMode}
110
- onAnimationFinish={onAnimationFinish}
111
- />
87
+ <LottieMascot mascot={mascot} animation={animation} resizeMode={resizeMode} onAnimationFinish={onAnimationFinish} />
112
88
  ) : (
113
- <SVGMascot
114
- mascot={mascot}
115
- size={size}
116
- />
89
+ <SVGMascot mascot={mascot} size={size} />
117
90
  )}
118
91
  </TouchableOpacity>
119
92
  </Animated.View>
120
93
  );
121
94
  };
122
95
 
123
- // Lottie Mascot Component
124
- interface LottieMascotProps {
125
- mascot: Mascot;
126
- animation?: MascotAnimation | null;
127
- resizeMode?: 'cover' | 'contain' | 'center';
128
- onAnimationFinish?: () => void;
129
- }
96
+ MascotViewComponent.displayName = 'MascotView';
130
97
 
131
- const LottieMascot: React.FC<LottieMascotProps> = ({
132
- mascot,
133
- animation,
134
- resizeMode = 'contain',
135
- onAnimationFinish,
136
- }) => {
137
- const lottieRef = useRef<LottieView>(null);
138
-
139
- useEffect(() => {
140
- if (animation && lottieRef.current) {
141
- lottieRef.current.play();
142
- }
143
- }, [animation]);
144
-
145
- const source = animation?.source || mascot.animations.find((a) => a.type === 'idle')?.source;
146
-
147
- if (!source) {
148
- return <FallbackMascot mascot={mascot} />;
149
- }
150
-
151
- return (
152
- <LottieView
153
- ref={lottieRef}
154
- source={source as LottieAnimationSource}
155
- style={styles.lottie}
156
- resizeMode={resizeMode}
157
- autoPlay={animation?.autoplay}
158
- loop={animation?.loop}
159
- onAnimationFinish={onAnimationFinish}
160
- />
161
- );
162
- };
163
-
164
- // SVG Mascot Component
165
- interface SVGMascotProps {
166
- mascot: Mascot;
167
- size: number;
168
- }
169
-
170
- const SVGMascot: React.FC<SVGMascotProps> = ({ mascot, size }) => {
171
- const { appearance } = mascot;
172
-
173
- return (
174
- <Svg width={size} height={size} viewBox="0 0 100 100">
175
- {/* Base shape */}
176
- <circle
177
- cx="50"
178
- cy="50"
179
- r="40"
180
- fill={appearance.baseColor}
181
- />
182
- {/* Accent */}
183
- <circle
184
- cx="50"
185
- cy="50"
186
- r="35"
187
- fill={appearance.accentColor}
188
- opacity="0.3"
189
- />
190
- {/* Face */}
191
- <circle cx="35" cy="40" r="5" fill="#000" />
192
- <circle cx="65" cy="40" r="5" fill="#000" />
193
- {/* Mouth based on mood */}
194
- <MoodMood mood={mascot.personality.mood} />
195
- </Svg>
196
- );
197
- };
198
-
199
- // Mood-based mouth
200
- interface MoodMoodProps {
201
- mood: string;
202
- }
203
-
204
- const MoodMood: React.FC<MoodMoodProps> = ({ mood }) => {
205
- switch (mood) {
206
- case 'happy':
207
- case 'excited':
208
- return (
209
- <path
210
- d="M 35 60 Q 50 75 65 60"
211
- stroke="#000"
212
- strokeWidth="3"
213
- fill="none"
214
- />
215
- );
216
- case 'sad':
217
- case 'angry':
218
- return (
219
- <path
220
- d="M 35 70 Q 50 55 65 70"
221
- stroke="#000"
222
- strokeWidth="3"
223
- fill="none"
224
- />
225
- );
226
- case 'surprised':
227
- return (
228
- <circle
229
- cx="50"
230
- cy="65"
231
- r="8"
232
- fill="#000"
233
- />
234
- );
235
- case 'thinking':
236
- return (
237
- <path
238
- d="M 40 65 L 60 65"
239
- stroke="#000"
240
- strokeWidth="3"
241
- fill="none"
242
- />
243
- );
244
- default:
245
- return (
246
- <path
247
- d="M 40 65 L 60 65"
248
- stroke="#000"
249
- strokeWidth="3"
250
- fill="none"
251
- />
252
- );
253
- }
254
- };
255
-
256
- // Fallback Mascot
257
- interface FallbackMascotProps {
258
- mascot: Mascot;
259
- }
260
-
261
- const FallbackMascot: React.FC<FallbackMascotProps> = ({ mascot }) => {
262
- const { appearance } = mascot;
263
-
264
- return (
265
- <View style={[styles.fallback, { backgroundColor: appearance.baseColor }]}>
266
- <View style={[styles.eye, { left: '30%' }]} />
267
- <View style={[styles.eye, { right: '30%' }]} />
268
- </View>
269
- );
270
- };
98
+ export const MascotView = memo(MascotViewComponent, arePropsEqual);
271
99
 
272
100
  const styles = StyleSheet.create({
273
101
  container: {
274
102
  justifyContent: 'center',
275
103
  alignItems: 'center',
276
104
  },
277
- lottie: {
278
- width: '100%',
279
- height: '100%',
280
- },
281
- fallback: {
282
- width: '100%',
283
- height: '100%',
284
- borderRadius: 100,
285
- justifyContent: 'center',
286
- alignItems: 'center',
287
- },
288
- eye: {
289
- position: 'absolute',
290
- width: 10,
291
- height: 10,
292
- backgroundColor: '#000',
293
- borderRadius: 5,
294
- top: '40%',
295
- },
296
105
  });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * SVG Mascot Component (70 lines)
3
+ * SVG-specific rendering with mood-based expressions
4
+ */
5
+
6
+ import React, { memo } from 'react';
7
+ import { Svg } from 'react-native-svg';
8
+ import type { Mascot } from '../../domain/entities/Mascot';
9
+
10
+ export interface SVGMascotProps {
11
+ mascot: Mascot;
12
+ size: number;
13
+ }
14
+
15
+ const SVGMascotComponent = memo<SVGMascotProps>(({ mascot, size }) => {
16
+ const { appearance } = mascot;
17
+
18
+ return (
19
+ <Svg width={size} height={size} viewBox="0 0 100 100">
20
+ {/* Base */}
21
+ <circle cx="50" cy="50" r="40" fill={appearance.baseColor} />
22
+ {/* Accent */}
23
+ <circle cx="50" cy="50" r="35" fill={appearance.accentColor} opacity="0.3" />
24
+ {/* Eyes */}
25
+ <circle cx="35" cy="40" r="5" fill="#000" />
26
+ <circle cx="65" cy="40" r="5" fill="#000" />
27
+ {/* Mouth based on mood */}
28
+ <MoodMood mood={mascot.personality.mood} />
29
+ </Svg>
30
+ );
31
+ });
32
+
33
+ SVGMascotComponent.displayName = 'SVGMascot';
34
+
35
+ export const SVGMascot = SVGMascotComponent;
36
+
37
+ // Mood-based mouth component
38
+ interface MoodMoodProps {
39
+ mood: string;
40
+ }
41
+
42
+ const MoodMoodComponent = memo<MoodMoodProps>(({ mood }) => {
43
+ switch (mood) {
44
+ case 'happy':
45
+ case 'excited':
46
+ return <path d="M 35 60 Q 50 75 65 60" stroke="#000" strokeWidth="3" fill="none" />;
47
+ case 'sad':
48
+ case 'angry':
49
+ return <path d="M 35 70 Q 50 55 65 70" stroke="#000" strokeWidth="3" fill="none" />;
50
+ case 'surprised':
51
+ return <circle cx="50" cy="65" r="8" fill="#000" />;
52
+ case 'thinking':
53
+ return <path d="M 40 65 L 60 65" stroke="#000" strokeWidth="3" fill="none" />;
54
+ default:
55
+ return <path d="M 40 65 L 60 65" stroke="#000" strokeWidth="3" fill="none" />;
56
+ }
57
+ });
58
+
59
+ MoodMoodComponent.displayName = 'MoodMood';
60
+
61
+ const MoodMood = memo(MoodMoodComponent);
@@ -5,8 +5,7 @@
5
5
 
6
6
  import React, { createContext, useContext, ReactNode } from 'react';
7
7
  import type { Mascot } from '../../domain/entities/Mascot';
8
- import type { MascotConfig, MascotMood } from '../../domain/types/MascotTypes';
9
- import type { AnimationOptions } from '../../domain/interfaces/IAnimationController';
8
+ import type { MascotConfig } from '../../domain/types/MascotTypes';
10
9
  import type { MascotService, MascotTemplate } from '../../application/services/MascotService';
11
10
  import { DIContainer } from '../../infrastructure/di/Container';
12
11
 
@@ -29,7 +28,7 @@ export const MascotProvider: React.FC<MascotProviderProps> = ({
29
28
  children,
30
29
  initialConfig,
31
30
  template,
32
- }) => {
31
+ }: MascotProviderProps) => {
33
32
  const container = DIContainer.getInstance();
34
33
  const service = container.getMascotService();
35
34
 
@@ -64,27 +64,32 @@ export function useMascot(options: UseMascotOptions = {}): UseMascotReturn {
64
64
 
65
65
  const serviceRef = useRef<MascotService | null>(null);
66
66
 
67
- // ✅ Initialize service once
68
- if (!serviceRef.current) {
69
- const container = DIContainer.getInstance();
70
- serviceRef.current = container.getMascotService();
67
+ // ✅ Initialize service once and subscribe to changes
68
+ useEffect(() => {
69
+ if (serviceRef.current == null) {
70
+ const container = DIContainer.getInstance();
71
+ const service = container.getMascotService();
72
+ serviceRef.current = service;
71
73
 
72
- // Subscribe to service changes
73
- serviceRef.current.subscribe(() => {
74
- const service = serviceRef.current!;
75
- setState({
76
- mascot: service.mascot,
77
- isReady: service.isReady,
78
- isPlaying: service.isPlaying,
79
- currentAnimation: service.currentAnimation,
74
+ // Subscribe to service changes
75
+ const unsubscribe = service.subscribe(() => {
76
+ setState({
77
+ mascot: service.mascot,
78
+ isReady: service.isReady,
79
+ isPlaying: service.isPlaying,
80
+ currentAnimation: service.currentAnimation,
81
+ });
80
82
  });
81
- });
82
- }
83
83
 
84
- const service = serviceRef.current;
84
+ return unsubscribe;
85
+ }
86
+ return undefined;
87
+ }, []);
85
88
 
86
89
  // ✅ Auto-initialize
87
90
  useEffect(() => {
91
+ const service = serviceRef.current;
92
+ if (!service) return;
88
93
  if (autoInitialize && initialConfig) {
89
94
  service.initialize(initialConfig);
90
95
  } else if (autoInitialize && initialTemplate) {
@@ -95,35 +100,109 @@ export function useMascot(options: UseMascotOptions = {}): UseMascotReturn {
95
100
  // ✅ All methods delegate to service - NO business logic!
96
101
  return {
97
102
  ...state,
98
- initialize: useCallback((config: MascotConfig) => service.initialize(config), [service]),
103
+ initialize: useCallback((config: MascotConfig) => {
104
+ const service = serviceRef.current;
105
+ if (!service) throw new Error('Service not initialized');
106
+ return service.initialize(config);
107
+ }, []),
99
108
  fromTemplate: useCallback(
100
- (template: MascotTemplate, customizations?: Partial<MascotConfig>) =>
101
- service.fromTemplate(template, customizations),
102
- [service]
109
+ (template: MascotTemplate, customizations?: Partial<MascotConfig>) => {
110
+ const service = serviceRef.current;
111
+ if (!service) throw new Error('Service not initialized');
112
+ return service.fromTemplate(template, customizations);
113
+ },
114
+ []
103
115
  ),
104
- setMood: useCallback((mood: MascotMood) => service.setMood(mood), [service]),
105
- setEnergy: useCallback((energy: number) => service.setEnergy(energy), [service]),
106
- setFriendliness: useCallback((friendliness: number) => service.setFriendliness(friendliness), [service]),
107
- setPlayfulness: useCallback((playfulness: number) => service.setPlayfulness(playfulness), [service]),
108
- cheerUp: useCallback(() => service.cheerUp(), [service]),
109
- boostEnergy: useCallback((amount: number) => service.boostEnergy(amount), [service]),
116
+ setMood: useCallback((mood: MascotMood) => {
117
+ const service = serviceRef.current;
118
+ if (!service) throw new Error('Service not initialized');
119
+ service.setMood(mood);
120
+ }, []),
121
+ setEnergy: useCallback((energy: number) => {
122
+ const service = serviceRef.current;
123
+ if (!service) throw new Error('Service not initialized');
124
+ service.setEnergy(energy);
125
+ }, []),
126
+ setFriendliness: useCallback((friendliness: number) => {
127
+ const service = serviceRef.current;
128
+ if (!service) throw new Error('Service not initialized');
129
+ service.setFriendliness(friendliness);
130
+ }, []),
131
+ setPlayfulness: useCallback((playfulness: number) => {
132
+ const service = serviceRef.current;
133
+ if (!service) throw new Error('Service not initialized');
134
+ service.setPlayfulness(playfulness);
135
+ }, []),
136
+ cheerUp: useCallback(() => {
137
+ const service = serviceRef.current;
138
+ if (!service) throw new Error('Service not initialized');
139
+ service.cheerUp();
140
+ }, []),
141
+ boostEnergy: useCallback((amount: number) => {
142
+ const service = serviceRef.current;
143
+ if (!service) throw new Error('Service not initialized');
144
+ service.boostEnergy(amount);
145
+ }, []),
110
146
  playAnimation: useCallback(
111
- (animationId: string, opts?: AnimationOptions) => service.playAnimation(animationId, opts),
112
- [service]
147
+ (animationId: string, opts?: AnimationOptions) => {
148
+ const service = serviceRef.current;
149
+ if (!service) throw new Error('Service not initialized');
150
+ return service.playAnimation(animationId, opts);
151
+ },
152
+ []
113
153
  ),
114
- stopAnimation: useCallback(() => service.stopAnimation(), [service]),
115
- pauseAnimation: useCallback(() => service.pauseAnimation(), [service]),
116
- resumeAnimation: useCallback(() => service.resumeAnimation(), [service]),
117
- updateAppearance: useCallback((appearance: Partial<MascotAppearance>) => service.updateAppearance(appearance), [service]),
118
- setBaseColor: useCallback((color: string) => service.setBaseColor(color), [service]),
119
- setAccentColor: useCallback((color: string) => service.setAccentColor(color), [service]),
154
+ stopAnimation: useCallback(() => {
155
+ const service = serviceRef.current;
156
+ if (!service) throw new Error('Service not initialized');
157
+ service.stopAnimation();
158
+ }, []),
159
+ pauseAnimation: useCallback(() => {
160
+ const service = serviceRef.current;
161
+ if (!service) throw new Error('Service not initialized');
162
+ service.pauseAnimation();
163
+ }, []),
164
+ resumeAnimation: useCallback(() => {
165
+ const service = serviceRef.current;
166
+ if (!service) throw new Error('Service not initialized');
167
+ service.resumeAnimation();
168
+ }, []),
169
+ updateAppearance: useCallback((appearance: Partial<MascotAppearance>) => {
170
+ const service = serviceRef.current;
171
+ if (!service) throw new Error('Service not initialized');
172
+ service.updateAppearance(appearance);
173
+ }, []),
174
+ setBaseColor: useCallback((color: string) => {
175
+ const service = serviceRef.current;
176
+ if (!service) throw new Error('Service not initialized');
177
+ service.setBaseColor(color);
178
+ }, []),
179
+ setAccentColor: useCallback((color: string) => {
180
+ const service = serviceRef.current;
181
+ if (!service) throw new Error('Service not initialized');
182
+ service.setAccentColor(color);
183
+ }, []),
120
184
  addAccessory: useCallback(
121
- (accessory: { id: string; type: string; color?: string; position?: { x: number; y: number } }) =>
122
- service.addAccessory(accessory),
123
- [service]
185
+ (accessory: { id: string; type: string; color?: string; position?: { x: number; y: number } }) => {
186
+ const service = serviceRef.current;
187
+ if (!service) throw new Error('Service not initialized');
188
+ service.addAccessory(accessory);
189
+ },
190
+ []
124
191
  ),
125
- removeAccessory: useCallback((accessoryId: string) => service.removeAccessory(accessoryId), [service]),
126
- setVisible: useCallback((visible: boolean) => service.setVisible(visible), [service]),
127
- setPosition: useCallback((position: { x: number; y: number }) => service.setPosition(position), [service]),
192
+ removeAccessory: useCallback((accessoryId: string) => {
193
+ const service = serviceRef.current;
194
+ if (!service) throw new Error('Service not initialized');
195
+ service.removeAccessory(accessoryId);
196
+ }, []),
197
+ setVisible: useCallback((visible: boolean) => {
198
+ const service = serviceRef.current;
199
+ if (!service) throw new Error('Service not initialized');
200
+ service.setVisible(visible);
201
+ }, []),
202
+ setPosition: useCallback((position: { x: number; y: number }) => {
203
+ const service = serviceRef.current;
204
+ if (!service) throw new Error('Service not initialized');
205
+ service.setPosition(position);
206
+ }, []),
128
207
  };
129
208
  }
@@ -4,15 +4,10 @@
4
4
  */
5
5
 
6
6
  import { useCallback, useState } from 'react';
7
- import type { Mascot } from '../../domain/entities/Mascot';
8
7
  import type { AnimationSpeed, AnimationOptions } from '../../domain/types/MascotTypes';
9
- import type { MascotService } from '../../application/services/MascotService';
10
8
  import { DIContainer } from '../../infrastructure/di/Container';
11
9
 
12
10
  export interface UseMascotAnimationOptions {
13
- mascot?: Mascot | null;
14
- autoplay?: boolean;
15
- queue?: boolean;
16
11
  speed?: AnimationSpeed;
17
12
  }
18
13
 
@@ -44,7 +39,7 @@ const SPEED_MULTIPLIERS: Record<AnimationSpeed, number> = {
44
39
  export function useMascotAnimation(
45
40
  options: UseMascotAnimationOptions = {}
46
41
  ): UseMascotAnimationReturn {
47
- const { mascot, speed = 'normal' } = options;
42
+ const { speed = 'normal' } = options;
48
43
  const [queue, setQueue] = useState<string[]>([]);
49
44
 
50
45
  const container = DIContainer.getInstance();
@@ -74,18 +69,17 @@ export function useMascotAnimation(
74
69
  service.stopAnimation();
75
70
  }, [service]);
76
71
 
77
- const setSpeed = useCallback(() => {
78
- // Speed is handled via options in play()
79
- console.warn('setSpeed: Use play() with speed option instead');
72
+ const setSpeed = useCallback((_speed: number) => {
73
+ // Speed is handled via options in play() - this method is kept for API compatibility
74
+ // but does nothing as speed should be passed to play() directly
80
75
  }, []);
81
76
 
82
- const setProgress = useCallback(() => {
83
- // Progress tracking would need AnimationController reference
84
- console.warn('setProgress: Not implemented in service layer yet');
77
+ const setProgress = useCallback((_progress: number) => {
78
+ // Progress tracking not yet implemented - would need AnimationController reference
85
79
  }, []);
86
80
 
87
81
  const queueAnimation = useCallback((animationId: string) => {
88
- setQueue((prev) => [...prev, animationId]);
82
+ setQueue((prev: string[]) => [...prev, animationId]);
89
83
  }, []);
90
84
 
91
85
  const clearQueue = useCallback(() => {
@@ -104,10 +98,10 @@ export function useMascotAnimation(
104
98
  const processQueue = useCallback(async () => {
105
99
  while (queue.length > 0) {
106
100
  const nextAnimation = queue[0];
107
- setQueue((prev) => prev.slice(1));
101
+ setQueue((prev: string[]) => prev.slice(1));
108
102
  await play(nextAnimation);
109
103
  }
110
- }, [queue, play]);
104
+ }, [play, queue]);
111
105
 
112
106
  return {
113
107
  isPlaying: service.isPlaying,