@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.
- package/README.md +60 -0
- package/package.json +2 -1
- package/src/application/dto/MascotDTO.ts +64 -0
- package/src/application/errors/MascotErrors.ts +76 -0
- package/src/application/services/AnimationStateManager.ts +69 -0
- package/src/application/services/AppearanceManagement.ts +40 -0
- package/src/application/services/MascotService.ts +203 -0
- 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 +197 -99
- 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/domain/value-objects/EnergyLevel.ts +80 -0
- package/src/domain/value-objects/FriendlinessLevel.ts +66 -0
- package/src/domain/value-objects/Mood.ts +59 -0
- package/src/domain/value-objects/PlayfulnessLevel.ts +66 -0
- package/src/index.ts +16 -68
- 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 +153 -0
- 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 +28 -111
- package/src/presentation/hooks/useMascot.ts +153 -169
- package/src/presentation/hooks/useMascotAnimation.ts +48 -94
- package/src/presentation/hooks/useMascotState.ts +213 -0
- package/src/presentation.ts +37 -0
- package/src/types.d.ts +4 -0
|
@@ -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);
|
|
@@ -1,135 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* MascotContext
|
|
3
|
+
* Thin wrapper that provides MascotService to components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { createContext, useContext,
|
|
7
|
-
import { Mascot } from '../../domain/entities/Mascot';
|
|
8
|
-
import type { MascotConfig
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
-
|
|
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
|
|
21
|
+
export interface MascotProviderProps {
|
|
22
|
+
children: ReactNode;
|
|
29
23
|
initialConfig?: MascotConfig;
|
|
30
|
-
template?:
|
|
24
|
+
template?: MascotTemplate;
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
export const MascotProvider: React.FC<MascotProviderProps> = ({
|
|
34
28
|
children,
|
|
35
|
-
initialConfig
|
|
36
|
-
template
|
|
37
|
-
}) => {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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 => {
|