@umituz/react-native-mascot 1.0.4 → 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.
- package/README.md +60 -0
- package/package.json +2 -1
- package/src/application/services/AnimationStateManager.ts +69 -0
- package/src/application/services/AppearanceManagement.ts +40 -0
- package/src/application/services/MascotService.ts +42 -33
- package/src/application/services/PersonalityManagement.ts +39 -0
- package/src/application/services/StateHistory.ts +55 -0
- package/src/application/services/StateMachine.ts +154 -0
- package/src/application/services/StateTransitions.ts +73 -0
- package/src/application.ts +40 -0
- package/src/assets/index.ts +14 -19
- package/src/core.ts +62 -0
- package/src/domain/entities/Mascot.ts +186 -127
- package/src/domain/types/AnimationStateTypes.ts +148 -0
- package/src/domain/types/MascotTypes.ts +9 -0
- package/src/domain/value-objects/AnimationState.ts +126 -0
- package/src/index.ts +9 -99
- package/src/infrastructure/controllers/AnimationController.ts +26 -122
- package/src/infrastructure/controllers/AnimationPlayer.ts +104 -0
- package/src/infrastructure/controllers/AnimationTimer.ts +62 -0
- package/src/infrastructure/controllers/EventManager.ts +108 -0
- package/src/infrastructure/di/Container.ts +73 -10
- package/src/infrastructure/managers/AssetManager.ts +134 -63
- package/src/infrastructure/managers/MascotBuilder.ts +89 -0
- package/src/infrastructure/managers/MascotFactory.ts +24 -176
- package/src/infrastructure/managers/MascotTemplates.ts +151 -0
- package/src/infrastructure/utils/LRUCache.ts +218 -0
- package/src/infrastructure.ts +24 -0
- package/src/presentation/components/LottieMascot.tsx +85 -0
- package/src/presentation/components/MascotView.tsx +42 -233
- package/src/presentation/components/SVGMascot.tsx +61 -0
- package/src/presentation/contexts/MascotContext.tsx +2 -3
- package/src/presentation/hooks/useMascot.ts +118 -39
- package/src/presentation/hooks/useMascotAnimation.ts +9 -15
- package/src/presentation/hooks/useMascotState.ts +213 -0
- package/src/presentation.ts +37 -0
- package/src/types.d.ts +4 -0
- package/src/application/index.ts +0 -8
- package/src/domain/value-objects/index.ts +0 -9
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MascotView Component
|
|
3
|
-
* Main
|
|
2
|
+
* MascotView Component (OPTIMIZED - 100 lines)
|
|
3
|
+
* Main container with memoization
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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) =>
|
|
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.
|
|
102
|
-
|
|
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) =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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) =>
|
|
112
|
-
|
|
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(() =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
123
|
-
|
|
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) =>
|
|
126
|
-
|
|
127
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
}, [
|
|
104
|
+
}, [play, queue]);
|
|
111
105
|
|
|
112
106
|
return {
|
|
113
107
|
isPlaying: service.isPlaying,
|