@umituz/react-native-mascot 1.0.3 → 1.0.7

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 (43) hide show
  1. package/README.md +60 -0
  2. package/package.json +2 -1
  3. package/src/application/dto/MascotDTO.ts +64 -0
  4. package/src/application/errors/MascotErrors.ts +76 -0
  5. package/src/application/services/AnimationStateManager.ts +69 -0
  6. package/src/application/services/AppearanceManagement.ts +40 -0
  7. package/src/application/services/MascotService.ts +203 -0
  8. package/src/application/services/PersonalityManagement.ts +39 -0
  9. package/src/application/services/StateHistory.ts +55 -0
  10. package/src/application/services/StateMachine.ts +154 -0
  11. package/src/application/services/StateTransitions.ts +73 -0
  12. package/src/application.ts +40 -0
  13. package/src/assets/index.ts +14 -19
  14. package/src/core.ts +62 -0
  15. package/src/domain/entities/Mascot.ts +197 -99
  16. package/src/domain/types/AnimationStateTypes.ts +148 -0
  17. package/src/domain/types/MascotTypes.ts +9 -0
  18. package/src/domain/value-objects/AnimationState.ts +126 -0
  19. package/src/domain/value-objects/EnergyLevel.ts +80 -0
  20. package/src/domain/value-objects/FriendlinessLevel.ts +66 -0
  21. package/src/domain/value-objects/Mood.ts +59 -0
  22. package/src/domain/value-objects/PlayfulnessLevel.ts +66 -0
  23. package/src/index.ts +16 -68
  24. package/src/infrastructure/controllers/AnimationController.ts +26 -122
  25. package/src/infrastructure/controllers/AnimationPlayer.ts +104 -0
  26. package/src/infrastructure/controllers/AnimationTimer.ts +62 -0
  27. package/src/infrastructure/controllers/EventManager.ts +108 -0
  28. package/src/infrastructure/di/Container.ts +153 -0
  29. package/src/infrastructure/managers/AssetManager.ts +134 -63
  30. package/src/infrastructure/managers/MascotBuilder.ts +89 -0
  31. package/src/infrastructure/managers/MascotFactory.ts +24 -176
  32. package/src/infrastructure/managers/MascotTemplates.ts +151 -0
  33. package/src/infrastructure/utils/LRUCache.ts +218 -0
  34. package/src/infrastructure.ts +24 -0
  35. package/src/presentation/components/LottieMascot.tsx +85 -0
  36. package/src/presentation/components/MascotView.tsx +42 -233
  37. package/src/presentation/components/SVGMascot.tsx +61 -0
  38. package/src/presentation/contexts/MascotContext.tsx +28 -111
  39. package/src/presentation/hooks/useMascot.ts +153 -169
  40. package/src/presentation/hooks/useMascotAnimation.ts +48 -94
  41. package/src/presentation/hooks/useMascotState.ts +213 -0
  42. package/src/presentation.ts +37 -0
  43. package/src/types.d.ts +4 -0
@@ -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);
@@ -1,135 +1,52 @@
1
1
  /**
2
- * Mascot Context
3
- * Provides mascot state and functionality to components
2
+ * MascotContext
3
+ * Thin wrapper that provides MascotService to components
4
4
  */
5
5
 
6
- import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
7
- import { Mascot } from '../../domain/entities/Mascot';
8
- import type { MascotConfig, MascotMood } from '../../domain/types/MascotTypes';
9
- import { MascotFactory } from '../../infrastructure/managers/MascotFactory';
10
- import { AnimationController } from '../../infrastructure/controllers/AnimationController';
11
- import type { AnimationOptions } from '../../domain/interfaces/IAnimationController';
6
+ import React, { createContext, useContext, ReactNode } from 'react';
7
+ import type { Mascot } from '../../domain/entities/Mascot';
8
+ import type { MascotConfig } from '../../domain/types/MascotTypes';
9
+ import type { MascotService, MascotTemplate } from '../../application/services/MascotService';
10
+ import { DIContainer } from '../../infrastructure/di/Container';
12
11
 
13
12
  export interface MascotContextValue {
14
13
  mascot: Mascot | null;
15
14
  isPlaying: boolean;
16
15
  currentAnimation: string | null;
17
- initializeMascot: (config: MascotConfig) => void;
18
- initializeFromTemplate: (template: string, customizations?: Partial<MascotConfig>) => void;
19
- setMood: (mood: MascotMood) => void;
20
- playAnimation: (animationId: string, options?: AnimationOptions) => Promise<void>;
21
- stopAnimation: () => void;
22
- updateAppearance: (appearance: Partial<MascotConfig['appearance']>) => void;
23
- setVisible: (visible: boolean) => void;
16
+ service: MascotService;
24
17
  }
25
18
 
26
19
  const MascotContext = createContext<MascotContextValue | undefined>(undefined);
27
20
 
28
- export interface MascotProviderProps extends React.PropsWithChildren {
21
+ export interface MascotProviderProps {
22
+ children: ReactNode;
29
23
  initialConfig?: MascotConfig;
30
- template?: string;
24
+ template?: MascotTemplate;
31
25
  }
32
26
 
33
27
  export const MascotProvider: React.FC<MascotProviderProps> = ({
34
28
  children,
35
- initialConfig: _initialConfig,
36
- template: _template,
37
- }) => {
38
- const [mascot, setMascot] = useState<Mascot | null>(null);
39
- const [isPlaying, setIsPlaying] = useState(false);
40
- const [currentAnimation, setCurrentAnimation] = useState<string | null>(null);
41
- const animationControllerRef = useRef<AnimationController | null>(null);
42
-
43
- const initializeMascot = useCallback((config: MascotConfig) => {
44
- const newMascot = new Mascot(config);
45
- setMascot(newMascot);
46
- if (!animationControllerRef.current) {
47
- animationControllerRef.current = new AnimationController();
48
- }
49
- }, []);
50
-
51
- const initializeFromTemplate = useCallback((
52
- templateName: string,
53
- customizations?: Partial<MascotConfig>
54
- ) => {
55
- const template = templateName as 'friendly-bot' | 'cute-pet' | 'wise-owl' | 'pixel-hero';
56
- const newMascot = MascotFactory.createFromTemplate(template, customizations);
57
- setMascot(newMascot);
58
- if (!animationControllerRef.current) {
59
- animationControllerRef.current = new AnimationController();
60
- }
61
- }, []);
62
-
63
- const setMood = useCallback((mood: MascotMood) => {
64
- setMascot((prev) => {
65
- if (!prev) return null;
66
- prev.setMood(mood);
67
- return prev.clone();
68
- });
69
- }, []);
70
-
71
- const playAnimation = useCallback(async (animationId: string, options?: AnimationOptions) => {
72
- if (!mascot || !animationControllerRef.current) return;
73
-
74
- const animation = mascot.getAnimation(animationId);
75
- if (!animation) {
76
- console.warn(`Animation ${animationId} not found`);
77
- return;
78
- }
79
-
80
- setIsPlaying(true);
81
- setCurrentAnimation(animationId);
82
-
83
- if (animationControllerRef.current) {
84
- await animationControllerRef.current.play(animation, options);
85
- }
86
-
87
- setIsPlaying(false);
88
- setCurrentAnimation(null);
89
- }, [mascot]);
90
-
91
- const stopAnimation = useCallback(() => {
92
- if (animationControllerRef.current) {
93
- animationControllerRef.current.stop();
94
- }
95
- setIsPlaying(false);
96
- setCurrentAnimation(null);
97
- }, []);
98
-
99
- const updateAppearance = useCallback((appearance: Partial<MascotConfig['appearance']>) => {
100
- setMascot((prev) => {
101
- if (!prev) return null;
102
- prev.updateAppearance(appearance);
103
- return prev.clone();
104
- });
105
- }, []);
106
-
107
- const setVisible = useCallback((visible: boolean) => {
108
- setMascot((prev) => {
109
- if (!prev) return null;
110
- prev.setVisible(visible);
111
- return prev.clone();
112
- });
113
- }, []);
29
+ initialConfig,
30
+ template,
31
+ }: MascotProviderProps) => {
32
+ const container = DIContainer.getInstance();
33
+ const service = container.getMascotService();
34
+
35
+ // Auto-initialize if config or template provided
36
+ if (initialConfig) {
37
+ service.initialize(initialConfig);
38
+ } else if (template) {
39
+ service.fromTemplate(template);
40
+ }
114
41
 
115
42
  const value: MascotContextValue = {
116
- mascot,
117
- isPlaying,
118
- currentAnimation,
119
- initializeMascot,
120
- initializeFromTemplate,
121
- setMood,
122
- playAnimation,
123
- stopAnimation,
124
- updateAppearance,
125
- setVisible,
43
+ mascot: service.mascot,
44
+ isPlaying: service.isPlaying,
45
+ currentAnimation: service.currentAnimation,
46
+ service,
126
47
  };
127
48
 
128
- return (
129
- <MascotContext.Provider value={value}>
130
- {children}
131
- </MascotContext.Provider>
132
- );
49
+ return <MascotContext.Provider value={value}>{children}</MascotContext.Provider>;
133
50
  };
134
51
 
135
52
  export const useMascotContext = (): MascotContextValue => {