@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.
@@ -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
+ });