@umituz/react-native-mascot 1.0.1
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/LICENSE +21 -0
- package/README.md +370 -0
- package/package.json +89 -0
- package/skills/SKILL.md +188 -0
- package/src/assets/index.ts +104 -0
- package/src/domain/entities/Mascot.ts +216 -0
- package/src/domain/interfaces/IAnimationController.ts +78 -0
- package/src/domain/interfaces/IAssetManager.ts +51 -0
- package/src/domain/interfaces/IMascotRepository.ts +39 -0
- package/src/domain/types/MascotTypes.ts +75 -0
- package/src/index.ts +99 -0
- package/src/infrastructure/assets/lottie/dance.json +61 -0
- package/src/infrastructure/assets/lottie/error.json +48 -0
- package/src/infrastructure/assets/lottie/idle.json +49 -0
- package/src/infrastructure/assets/lottie/jump.json +48 -0
- package/src/infrastructure/assets/lottie/success.json +46 -0
- package/src/infrastructure/assets/lottie/wave.json +46 -0
- package/src/infrastructure/assets/svg/cartoon-bot.svg +20 -0
- package/src/infrastructure/assets/svg/minimal-cat.svg +19 -0
- package/src/infrastructure/controllers/AnimationController.ts +163 -0
- package/src/infrastructure/managers/AssetManager.ts +159 -0
- package/src/infrastructure/managers/MascotFactory.ts +239 -0
- package/src/infrastructure/repositories/MascotRepository.ts +93 -0
- package/src/presentation/components/MascotView.tsx +296 -0
- package/src/presentation/contexts/MascotContext.tsx +141 -0
- package/src/presentation/hooks/useMascot.ts +224 -0
- package/src/presentation/hooks/useMascotAnimation.ts +168 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mascot Factory
|
|
3
|
+
* Creates pre-configured mascot instances
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Mascot } from '../../domain/entities/Mascot';
|
|
7
|
+
import type { MascotConfig, MascotType } from '../../domain/types/MascotTypes';
|
|
8
|
+
|
|
9
|
+
export class MascotFactory {
|
|
10
|
+
/**
|
|
11
|
+
* Create a mascot from predefined template
|
|
12
|
+
*/
|
|
13
|
+
static createFromTemplate(
|
|
14
|
+
template: MascotTemplate,
|
|
15
|
+
customizations?: Partial<MascotConfig>
|
|
16
|
+
): Mascot {
|
|
17
|
+
const config = this._getTemplateConfig(template);
|
|
18
|
+
const finalConfig = this._mergeConfigs(config, customizations);
|
|
19
|
+
return new Mascot(finalConfig);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a custom mascot
|
|
24
|
+
*/
|
|
25
|
+
static createCustom(config: MascotConfig): Mascot {
|
|
26
|
+
return new Mascot(config);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a simple mascot with minimal configuration
|
|
31
|
+
*/
|
|
32
|
+
static createSimple(options: {
|
|
33
|
+
id?: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
type?: MascotType;
|
|
36
|
+
baseColor?: string;
|
|
37
|
+
}): Mascot {
|
|
38
|
+
const config: MascotConfig = {
|
|
39
|
+
id: options.id || 'simple-mascot',
|
|
40
|
+
name: options.name || 'Simple Mascot',
|
|
41
|
+
type: options.type || 'svg',
|
|
42
|
+
personality: {
|
|
43
|
+
mood: 'happy',
|
|
44
|
+
energy: 0.7,
|
|
45
|
+
friendliness: 0.8,
|
|
46
|
+
playfulness: 0.6,
|
|
47
|
+
},
|
|
48
|
+
appearance: {
|
|
49
|
+
baseColor: options.baseColor || '#FF6B6B',
|
|
50
|
+
accentColor: '#4ECDC4',
|
|
51
|
+
accessories: [],
|
|
52
|
+
style: 'minimal',
|
|
53
|
+
scale: 1,
|
|
54
|
+
},
|
|
55
|
+
animations: this._getDefaultAnimations(),
|
|
56
|
+
interactive: true,
|
|
57
|
+
touchEnabled: true,
|
|
58
|
+
soundEnabled: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return new Mascot(config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Private Methods
|
|
65
|
+
private static _getTemplateConfig(template: MascotTemplate): MascotConfig {
|
|
66
|
+
const templates: Record<MascotTemplate, MascotConfig> = {
|
|
67
|
+
'friendly-bot': {
|
|
68
|
+
id: 'friendly-bot',
|
|
69
|
+
name: 'Friendly Bot',
|
|
70
|
+
type: 'lottie',
|
|
71
|
+
personality: {
|
|
72
|
+
mood: 'happy',
|
|
73
|
+
energy: 0.8,
|
|
74
|
+
friendliness: 0.9,
|
|
75
|
+
playfulness: 0.7,
|
|
76
|
+
},
|
|
77
|
+
appearance: {
|
|
78
|
+
baseColor: '#4A90E2',
|
|
79
|
+
accentColor: '#50E3C2',
|
|
80
|
+
accessories: [],
|
|
81
|
+
style: 'cartoon',
|
|
82
|
+
scale: 1,
|
|
83
|
+
},
|
|
84
|
+
animations: this._getDefaultAnimations(),
|
|
85
|
+
interactive: true,
|
|
86
|
+
touchEnabled: true,
|
|
87
|
+
soundEnabled: false,
|
|
88
|
+
},
|
|
89
|
+
'cute-pet': {
|
|
90
|
+
id: 'cute-pet',
|
|
91
|
+
name: 'Cute Pet',
|
|
92
|
+
type: 'svg',
|
|
93
|
+
personality: {
|
|
94
|
+
mood: 'excited',
|
|
95
|
+
energy: 0.9,
|
|
96
|
+
friendliness: 1.0,
|
|
97
|
+
playfulness: 0.95,
|
|
98
|
+
},
|
|
99
|
+
appearance: {
|
|
100
|
+
baseColor: '#FFB6C1',
|
|
101
|
+
accentColor: '#FF69B4',
|
|
102
|
+
secondaryColor: '#FFF',
|
|
103
|
+
accessories: [
|
|
104
|
+
{ id: 'bow', type: 'bow', color: '#FF69B4', visible: true },
|
|
105
|
+
],
|
|
106
|
+
style: 'cartoon',
|
|
107
|
+
scale: 1,
|
|
108
|
+
},
|
|
109
|
+
animations: this._getDefaultAnimations(),
|
|
110
|
+
interactive: true,
|
|
111
|
+
touchEnabled: true,
|
|
112
|
+
soundEnabled: false,
|
|
113
|
+
},
|
|
114
|
+
'wise-owl': {
|
|
115
|
+
id: 'wise-owl',
|
|
116
|
+
name: 'Wise Owl',
|
|
117
|
+
type: 'lottie',
|
|
118
|
+
personality: {
|
|
119
|
+
mood: 'thinking',
|
|
120
|
+
energy: 0.4,
|
|
121
|
+
friendliness: 0.7,
|
|
122
|
+
playfulness: 0.3,
|
|
123
|
+
},
|
|
124
|
+
appearance: {
|
|
125
|
+
baseColor: '#8B4513',
|
|
126
|
+
accentColor: '#DAA520',
|
|
127
|
+
accessories: [
|
|
128
|
+
{ id: 'glasses', type: 'glasses', visible: true },
|
|
129
|
+
],
|
|
130
|
+
style: 'realistic',
|
|
131
|
+
scale: 1,
|
|
132
|
+
},
|
|
133
|
+
animations: this._getDefaultAnimations(),
|
|
134
|
+
interactive: true,
|
|
135
|
+
touchEnabled: true,
|
|
136
|
+
soundEnabled: false,
|
|
137
|
+
},
|
|
138
|
+
'pixel-hero': {
|
|
139
|
+
id: 'pixel-hero',
|
|
140
|
+
name: 'Pixel Hero',
|
|
141
|
+
type: 'svg',
|
|
142
|
+
personality: {
|
|
143
|
+
mood: 'happy',
|
|
144
|
+
energy: 0.85,
|
|
145
|
+
friendliness: 0.75,
|
|
146
|
+
playfulness: 0.8,
|
|
147
|
+
},
|
|
148
|
+
appearance: {
|
|
149
|
+
baseColor: '#3498DB',
|
|
150
|
+
accentColor: '#E74C3C',
|
|
151
|
+
secondaryColor: '#F1C40F',
|
|
152
|
+
accessories: [
|
|
153
|
+
{ id: 'crown', type: 'crown', color: '#F1C40F', visible: true },
|
|
154
|
+
],
|
|
155
|
+
style: 'pixel',
|
|
156
|
+
scale: 1,
|
|
157
|
+
},
|
|
158
|
+
animations: this._getDefaultAnimations(),
|
|
159
|
+
interactive: true,
|
|
160
|
+
touchEnabled: true,
|
|
161
|
+
soundEnabled: false,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return templates[template];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private static _mergeConfigs(
|
|
169
|
+
base: MascotConfig,
|
|
170
|
+
custom?: Partial<MascotConfig>
|
|
171
|
+
): MascotConfig {
|
|
172
|
+
if (!custom) {
|
|
173
|
+
return base;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...base,
|
|
178
|
+
...custom,
|
|
179
|
+
id: custom.id || base.id,
|
|
180
|
+
personality: {
|
|
181
|
+
...base.personality,
|
|
182
|
+
...custom.personality,
|
|
183
|
+
},
|
|
184
|
+
appearance: {
|
|
185
|
+
...base.appearance,
|
|
186
|
+
...custom.appearance,
|
|
187
|
+
accessories: custom.appearance?.accessories || base.appearance.accessories,
|
|
188
|
+
},
|
|
189
|
+
animations: custom.animations || base.animations,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private static _getDefaultAnimations() {
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
id: 'idle',
|
|
197
|
+
name: 'Idle',
|
|
198
|
+
type: 'idle' as const,
|
|
199
|
+
source: 'idle.json',
|
|
200
|
+
loop: true,
|
|
201
|
+
autoplay: true,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'wave',
|
|
205
|
+
name: 'Wave',
|
|
206
|
+
type: 'action' as const,
|
|
207
|
+
source: 'wave.json',
|
|
208
|
+
loop: false,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'jump',
|
|
212
|
+
name: 'Jump',
|
|
213
|
+
type: 'action' as const,
|
|
214
|
+
source: 'jump.json',
|
|
215
|
+
loop: false,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'success',
|
|
219
|
+
name: 'Success',
|
|
220
|
+
type: 'reaction' as const,
|
|
221
|
+
source: 'success.json',
|
|
222
|
+
loop: false,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'error',
|
|
226
|
+
name: 'Error',
|
|
227
|
+
type: 'reaction' as const,
|
|
228
|
+
source: 'error.json',
|
|
229
|
+
loop: false,
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export type MascotTemplate =
|
|
236
|
+
| 'friendly-bot'
|
|
237
|
+
| 'cute-pet'
|
|
238
|
+
| 'wise-owl'
|
|
239
|
+
| 'pixel-hero';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mascot Repository Implementation
|
|
3
|
+
* In-memory repository with support for persistence (can be extended)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IMascotRepository } from '../../domain/interfaces/IMascotRepository';
|
|
7
|
+
import type { MascotConfig } from '../../domain/types/MascotTypes';
|
|
8
|
+
import { Mascot } from '../../domain/entities/Mascot';
|
|
9
|
+
|
|
10
|
+
export class MascotRepository implements IMascotRepository {
|
|
11
|
+
private readonly _storage: Map<string, MascotConfig>;
|
|
12
|
+
private readonly _mascotCache: Map<string, Mascot>;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this._storage = new Map();
|
|
16
|
+
this._mascotCache = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
save(config: MascotConfig): Promise<void> {
|
|
20
|
+
this._storage.set(config.id, config);
|
|
21
|
+
this._mascotCache.delete(config.id); // Invalidate cache
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
load(id: string): Promise<Mascot | null> {
|
|
26
|
+
// Check cache first
|
|
27
|
+
if (this._mascotCache.has(id)) {
|
|
28
|
+
return Promise.resolve(this._mascotCache.get(id)!);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const config = this._storage.get(id);
|
|
32
|
+
if (!config) {
|
|
33
|
+
return Promise.resolve(null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const mascot = new Mascot(config);
|
|
37
|
+
this._mascotCache.set(id, mascot);
|
|
38
|
+
return Promise.resolve(mascot);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async loadAll(): Promise<Mascot[]> {
|
|
42
|
+
const mascots: Mascot[] = [];
|
|
43
|
+
for (const config of this._storage.values()) {
|
|
44
|
+
const mascot = await this.load(config.id);
|
|
45
|
+
if (mascot !== null) {
|
|
46
|
+
mascots.push(mascot);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return mascots;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
delete(id: string): Promise<void> {
|
|
53
|
+
this._storage.delete(id);
|
|
54
|
+
this._mascotCache.delete(id);
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
update(id: string, config: Partial<MascotConfig>): Promise<void> {
|
|
59
|
+
const existing = this._storage.get(id);
|
|
60
|
+
if (!existing) {
|
|
61
|
+
throw new Error(`Mascot ${id} not found`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const updated: MascotConfig = {
|
|
65
|
+
...existing,
|
|
66
|
+
...config,
|
|
67
|
+
id, // Ensure ID cannot be changed
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this._storage.set(id, updated);
|
|
71
|
+
this._mascotCache.delete(id);
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
exists(id: string): Promise<boolean> {
|
|
76
|
+
return Promise.resolve(this._storage.has(id));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clear all stored mascots
|
|
81
|
+
*/
|
|
82
|
+
clear(): void {
|
|
83
|
+
this._storage.clear();
|
|
84
|
+
this._mascotCache.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get storage size
|
|
89
|
+
*/
|
|
90
|
+
get size(): number {
|
|
91
|
+
return this._storage.size;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MascotView Component
|
|
3
|
+
* Main component for rendering mascots
|
|
4
|
+
*/
|
|
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';
|
|
11
|
+
import type { Mascot } from '../../domain/entities/Mascot';
|
|
12
|
+
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
13
|
+
|
|
14
|
+
type LottieAnimationSource = string | AnimationObject | { uri: string };
|
|
15
|
+
|
|
16
|
+
export interface MascotViewProps {
|
|
17
|
+
mascot: Mascot | null;
|
|
18
|
+
animation?: MascotAnimation | null;
|
|
19
|
+
size?: number;
|
|
20
|
+
style?: ViewStyle;
|
|
21
|
+
testID?: string;
|
|
22
|
+
onPress?: () => void;
|
|
23
|
+
onLongPress?: () => void;
|
|
24
|
+
onAnimationFinish?: () => void;
|
|
25
|
+
resizeMode?: 'cover' | 'contain' | 'center';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const MascotView: React.FC<MascotViewProps> = ({
|
|
29
|
+
mascot,
|
|
30
|
+
animation,
|
|
31
|
+
size = 200,
|
|
32
|
+
style,
|
|
33
|
+
testID = 'mascot-view',
|
|
34
|
+
onPress,
|
|
35
|
+
onLongPress,
|
|
36
|
+
onAnimationFinish,
|
|
37
|
+
resizeMode = 'contain',
|
|
38
|
+
}) => {
|
|
39
|
+
const [opacity] = useState(new Animated.Value(0));
|
|
40
|
+
const [scale] = useState(new Animated.Value(0));
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (mascot?.state.isVisible) {
|
|
44
|
+
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
|
+
}),
|
|
56
|
+
]).start();
|
|
57
|
+
} else {
|
|
58
|
+
Animated.timing(opacity, {
|
|
59
|
+
toValue: 0,
|
|
60
|
+
duration: 200,
|
|
61
|
+
useNativeDriver: true,
|
|
62
|
+
}).start();
|
|
63
|
+
}
|
|
64
|
+
}, [mascot?.state.isVisible, opacity, scale]);
|
|
65
|
+
|
|
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
|
+
}
|
|
81
|
+
|
|
82
|
+
const animatedStyle = {
|
|
83
|
+
opacity,
|
|
84
|
+
transform: [{ scale }],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const containerStyle = [
|
|
88
|
+
styles.container,
|
|
89
|
+
{ width: size, height: size },
|
|
90
|
+
style,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Animated.View
|
|
95
|
+
style={animatedStyle}
|
|
96
|
+
testID={testID}
|
|
97
|
+
>
|
|
98
|
+
<TouchableOpacity
|
|
99
|
+
style={containerStyle}
|
|
100
|
+
onPress={handlePress}
|
|
101
|
+
onLongPress={handleLongPress}
|
|
102
|
+
disabled={!mascot.touchEnabled}
|
|
103
|
+
activeOpacity={0.8}
|
|
104
|
+
>
|
|
105
|
+
{mascot.type === 'lottie' ? (
|
|
106
|
+
<LottieMascot
|
|
107
|
+
mascot={mascot}
|
|
108
|
+
animation={animation}
|
|
109
|
+
resizeMode={resizeMode}
|
|
110
|
+
onAnimationFinish={onAnimationFinish}
|
|
111
|
+
/>
|
|
112
|
+
) : (
|
|
113
|
+
<SVGMascot
|
|
114
|
+
mascot={mascot}
|
|
115
|
+
size={size}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</TouchableOpacity>
|
|
119
|
+
</Animated.View>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Lottie Mascot Component
|
|
124
|
+
interface LottieMascotProps {
|
|
125
|
+
mascot: Mascot;
|
|
126
|
+
animation?: MascotAnimation | null;
|
|
127
|
+
resizeMode?: 'cover' | 'contain' | 'center';
|
|
128
|
+
onAnimationFinish?: () => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
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
|
+
};
|
|
271
|
+
|
|
272
|
+
const styles = StyleSheet.create({
|
|
273
|
+
container: {
|
|
274
|
+
justifyContent: 'center',
|
|
275
|
+
alignItems: 'center',
|
|
276
|
+
},
|
|
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
|
+
});
|