@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
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Animation Timer (60 lines)
3
+ * Timer management with proper cleanup
4
+ */
5
+
6
+ export class AnimationTimer {
7
+ private _timer: NodeJS.Timeout | null = null;
8
+ private _paused: boolean = false;
9
+ private _startTime: number = 0;
10
+ private _duration: number = 0;
11
+ private _remaining: number = 0;
12
+
13
+ start(duration: number, onComplete: () => void): void {
14
+ this._duration = duration;
15
+ this._startTime = Date.now();
16
+ this._remaining = duration;
17
+ this._paused = false;
18
+
19
+ this._timer = setTimeout(() => {
20
+ onComplete();
21
+ this._timer = null;
22
+ }, duration);
23
+ }
24
+
25
+ pause(): void {
26
+ if (!this._timer || this._paused) return;
27
+
28
+ const elapsed = Date.now() - this._startTime;
29
+ this._remaining = this._duration - elapsed;
30
+ clearTimeout(this._timer);
31
+ this._timer = null;
32
+ this._paused = true;
33
+ }
34
+
35
+ resume(): void {
36
+ if (this._timer || !this._paused || this._remaining <= 0) return;
37
+
38
+ this._startTime = Date.now();
39
+ this._paused = false;
40
+
41
+ this._timer = setTimeout(() => {
42
+ this._timer = null;
43
+ }, this._remaining);
44
+ }
45
+
46
+ stop(): void {
47
+ if (this._timer) {
48
+ clearTimeout(this._timer);
49
+ this._timer = null;
50
+ }
51
+ this._paused = false;
52
+ this._remaining = 0;
53
+ }
54
+
55
+ destroy(): void {
56
+ this.stop();
57
+ }
58
+
59
+ get isActive(): boolean {
60
+ return this._timer !== null;
61
+ }
62
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Event Manager (80 lines)
3
+ * Event listener management with memory leak prevention
4
+ */
5
+
6
+ import type { AnimationEvent } from '../../domain/interfaces/IAnimationController';
7
+
8
+ // Maximum listeners per event
9
+ const MAX_LISTENERS = 10;
10
+
11
+ interface ListenerWrapper {
12
+ callback: (data?: unknown) => void;
13
+ isActive: boolean;
14
+ }
15
+
16
+ export class EventManager {
17
+ private _listeners: Map<AnimationEvent, Set<ListenerWrapper>>;
18
+
19
+ constructor() {
20
+ this._listeners = new Map();
21
+ this._initializeListeners();
22
+ }
23
+
24
+ on(event: AnimationEvent, callback: (data?: unknown) => void): () => void {
25
+ const listeners = this._getListeners(event);
26
+
27
+ // Check limit
28
+ if (listeners.size >= MAX_LISTENERS) {
29
+ console.warn(`Max listeners (${MAX_LISTENERS}) for event "${event}"`);
30
+ }
31
+
32
+ const wrapper: ListenerWrapper = { callback, isActive: true };
33
+ listeners.add(wrapper);
34
+
35
+ return () => {
36
+ wrapper.isActive = false;
37
+ listeners.delete(wrapper);
38
+ };
39
+ }
40
+
41
+ off(event: AnimationEvent, callback: (data?: unknown) => void): void {
42
+ const listeners = this._getListeners(event);
43
+ for (const wrapper of listeners) {
44
+ if (wrapper.callback === callback) {
45
+ wrapper.isActive = false;
46
+ listeners.delete(wrapper);
47
+ break;
48
+ }
49
+ }
50
+ }
51
+
52
+ emit(event: AnimationEvent, data?: unknown): void {
53
+ const listeners = this._listeners.get(event);
54
+ if (!listeners || listeners.size === 0) return;
55
+
56
+ for (const wrapper of listeners) {
57
+ if (wrapper.isActive) {
58
+ try {
59
+ wrapper.callback(data);
60
+ } catch {
61
+ // Silent fail to prevent breaking animation
62
+ }
63
+ }
64
+ }
65
+
66
+ // Periodic cleanup
67
+ if (listeners.size > MAX_LISTENERS / 2) {
68
+ this._cleanupInactive(event);
69
+ }
70
+ }
71
+
72
+ clear(): void {
73
+ this._listeners.clear();
74
+ this._initializeListeners();
75
+ }
76
+
77
+ private _getListeners(event: AnimationEvent): Set<ListenerWrapper> {
78
+ if (!this._listeners.has(event)) {
79
+ this._listeners.set(event, new Set());
80
+ }
81
+ return this._listeners.get(event)!;
82
+ }
83
+
84
+ private _initializeListeners(): void {
85
+ const events: AnimationEvent[] = ['start', 'finish', 'pause', 'resume', 'progress', 'error'];
86
+ events.forEach((event) => {
87
+ if (!this._listeners.has(event)) {
88
+ this._listeners.set(event, new Set());
89
+ }
90
+ });
91
+ }
92
+
93
+ private _cleanupInactive(event: AnimationEvent): void {
94
+ const listeners = this._listeners.get(event);
95
+ if (!listeners) return;
96
+
97
+ const toRemove: ListenerWrapper[] = [];
98
+ for (const wrapper of listeners) {
99
+ if (!wrapper.isActive) {
100
+ toRemove.push(wrapper);
101
+ }
102
+ }
103
+
104
+ for (const wrapper of toRemove) {
105
+ listeners.delete(wrapper);
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * DI Container (OPTIMIZED)
3
+ * Lazy imports with caching and better singleton management
4
+ */
5
+
6
+ import type { IMascotRepository } from '../../domain/interfaces/IMascotRepository';
7
+ import type { MascotService } from '../../application/services/MascotService';
8
+ import { AnimationController } from '../controllers/AnimationController';
9
+ import { AssetManager } from '../managers/AssetManager';
10
+ import { MascotRepository } from '../repositories/MascotRepository';
11
+
12
+ // Cache for lazy-loaded modules
13
+ interface ModuleCache<T> {
14
+ module: T | null;
15
+ initialized: boolean;
16
+ }
17
+
18
+ export class DIContainer {
19
+ private static _instance: DIContainer;
20
+ private _animationController: AnimationController | null = null;
21
+ private _assetManager: AssetManager | null = null;
22
+ private _repository: IMascotRepository | null = null;
23
+ private _mascotServiceCache: ModuleCache<MascotService> = {
24
+ module: null,
25
+ initialized: false,
26
+ };
27
+
28
+ private constructor() {}
29
+
30
+ static getInstance(): DIContainer {
31
+ if (!this._instance) {
32
+ this._instance = new DIContainer();
33
+ }
34
+ return this._instance;
35
+ }
36
+
37
+ /**
38
+ * Get or create AnimationController singleton
39
+ */
40
+ getAnimationController(): AnimationController {
41
+ if (!this._animationController) {
42
+ this._animationController = new AnimationController();
43
+ }
44
+ return this._animationController;
45
+ }
46
+
47
+ /**
48
+ * Get or create AssetManager singleton
49
+ */
50
+ getAssetManager(): AssetManager {
51
+ if (!this._assetManager) {
52
+ this._assetManager = new AssetManager();
53
+ }
54
+ return this._assetManager;
55
+ }
56
+
57
+ /**
58
+ * Get or create MascotRepository singleton
59
+ */
60
+ getRepository(): IMascotRepository {
61
+ if (!this._repository) {
62
+ this._repository = new MascotRepository();
63
+ }
64
+ return this._repository;
65
+ }
66
+
67
+ /**
68
+ * Get or create MascotService singleton (with caching)
69
+ */
70
+ getMascotService(): MascotService {
71
+ if (!this._mascotServiceCache.initialized) {
72
+ // Lazy import to avoid circular dependencies
73
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
74
+ const { MascotService } = require('../../application/services/MascotService');
75
+
76
+ this._mascotServiceCache.module = new MascotService(
77
+ this.getRepository(),
78
+ this.getAnimationController(),
79
+ this.getAssetManager()
80
+ );
81
+ this._mascotServiceCache.initialized = true;
82
+ }
83
+
84
+ return this._mascotServiceCache.module!;
85
+ }
86
+
87
+ /**
88
+ * Reset all instances (useful for testing)
89
+ */
90
+ reset(): void {
91
+ // Cleanup animation controller
92
+ if (this._animationController) {
93
+ this._animationController.destroy();
94
+ this._animationController = null;
95
+ }
96
+
97
+ this._assetManager = null;
98
+ this._repository = null;
99
+ this._mascotServiceCache = {
100
+ module: null,
101
+ initialized: false,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Check if container has been initialized
107
+ */
108
+ isInitialized(): boolean {
109
+ return this._mascotServiceCache.initialized;
110
+ }
111
+
112
+ /**
113
+ * Get container statistics (for debugging)
114
+ */
115
+ getStats(): {
116
+ singletons: {
117
+ animationController: boolean;
118
+ assetManager: boolean;
119
+ repository: boolean;
120
+ mascotService: boolean;
121
+ };
122
+ } {
123
+ return {
124
+ singletons: {
125
+ animationController: this._animationController !== null,
126
+ assetManager: this._assetManager !== null,
127
+ repository: this._repository !== null,
128
+ mascotService: this._mascotServiceCache.initialized,
129
+ },
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Cleanup method (call when app unmounts)
135
+ */
136
+ destroy(): void {
137
+ // Destroy animation controller
138
+ if (this._animationController) {
139
+ this._animationController.destroy();
140
+ this._animationController = null;
141
+ }
142
+
143
+ // Clear cache
144
+ this._mascotServiceCache = {
145
+ module: null,
146
+ initialized: false,
147
+ };
148
+
149
+ // Reset other singletons
150
+ this._assetManager = null;
151
+ this._repository = null;
152
+ }
153
+ }
@@ -1,23 +1,59 @@
1
1
  /**
2
- * Asset Manager Implementation
3
- * Loads and caches Lottie and SVG assets
2
+ * Asset Manager Implementation (OPTIMIZED)
3
+ * Loads and caches Lottie and SVG assets with LRU cache
4
4
  */
5
5
 
6
- import type {
7
- IAssetManager,
8
- AssetCache,
9
- } from '../../domain/interfaces/IAssetManager';
6
+ import type { IAssetManager } from '../../domain/interfaces/IAssetManager';
10
7
  import type { MascotAnimation } from '../../domain/types/MascotTypes';
8
+ import { LRUCache } from '../utils/LRUCache';
9
+
10
+ // Cache size constants
11
+ const DEFAULT_MAX_CACHE_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
12
+ const CACHE_EVICTION_RATIO = 0.3; // Evict 30% of cache when full
13
+ const BYTES_PER_CHAR = 2; // UTF-16 encoding
14
+
15
+ // Size cache to avoid repeated JSON.stringify calls
16
+ const SIZE_CACHE_MAX_SIZE = 100;
17
+ const sizeCache = new Map<unknown, number>();
18
+
19
+ /**
20
+ * Get estimated size with caching
21
+ */
22
+ function getEstimatedSize(data: unknown): number {
23
+ // Check cache first
24
+ if (sizeCache.has(data)) {
25
+ return sizeCache.get(data)!;
26
+ }
27
+
28
+ // Calculate and cache
29
+ const size = JSON.stringify(data).length * BYTES_PER_CHAR;
30
+
31
+ // Add to cache if space available
32
+ if (sizeCache.size < SIZE_CACHE_MAX_SIZE) {
33
+ sizeCache.set(data, size);
34
+ }
35
+
36
+ return size;
37
+ }
38
+
39
+ /**
40
+ * Cache entry with metadata
41
+ */
42
+ interface CacheEntry {
43
+ data: unknown;
44
+ size: number;
45
+ timestamp: number;
46
+ }
11
47
 
12
48
  export class AssetManager implements IAssetManager {
13
- private readonly _cache: AssetCache;
14
- private readonly _loadedAssets: Set<string>;
15
- private readonly _maxCacheSize: number = 50 * 1024 * 1024; // 50MB
49
+ private readonly lruCache: LRUCache<string, CacheEntry>;
16
50
  private _currentCacheSize: number = 0;
51
+ private readonly _maxCacheSize: number;
17
52
 
18
- constructor() {
19
- this._cache = {};
20
- this._loadedAssets = new Set();
53
+ constructor(maxCacheSize: number = DEFAULT_MAX_CACHE_SIZE_BYTES) {
54
+ this._maxCacheSize = maxCacheSize;
55
+ // Cache up to 1000 items (will be limited by size anyway)
56
+ this.lruCache = new LRUCache<string, CacheEntry>(1000);
21
57
  }
22
58
 
23
59
  loadLottieAnimation(
@@ -25,8 +61,10 @@ export class AssetManager implements IAssetManager {
25
61
  ): Promise<MascotAnimation> {
26
62
  const assetId = this._getAssetId(source);
27
63
 
28
- if (this._isAssetLoaded(assetId)) {
29
- return Promise.resolve(this._cache[assetId].data as MascotAnimation);
64
+ // Check LRU cache first (O(1) operation)
65
+ const cached = this.lruCache.get(assetId);
66
+ if (cached) {
67
+ return Promise.resolve(cached.data as MascotAnimation);
30
68
  }
31
69
 
32
70
  // Simulate loading - in real implementation, this would actually load the file
@@ -39,13 +77,16 @@ export class AssetManager implements IAssetManager {
39
77
  autoplay: false,
40
78
  };
41
79
 
80
+ // Cache the animation
42
81
  this._cacheAsset(assetId, animation);
43
82
  return Promise.resolve(animation);
44
83
  }
45
84
 
46
85
  loadSVGAsset(source: string): Promise<string> {
47
- if (this._isAssetLoaded(source)) {
48
- return Promise.resolve(this._cache[source].data as string);
86
+ // Check LRU cache first
87
+ const cached = this.lruCache.get(source);
88
+ if (cached) {
89
+ return Promise.resolve(cached.data as string);
49
90
  }
50
91
 
51
92
  // Simulate loading - in real implementation, this would load the SVG file
@@ -55,6 +96,7 @@ export class AssetManager implements IAssetManager {
55
96
  }
56
97
 
57
98
  async preloadAnimations(sources: Array<string | object>): Promise<void> {
99
+ // Load in parallel for better performance
58
100
  const promises = sources.map((source) =>
59
101
  this.loadLottieAnimation(source)
60
102
  );
@@ -62,26 +104,43 @@ export class AssetManager implements IAssetManager {
62
104
  }
63
105
 
64
106
  clearCache(): void {
65
- Object.keys(this._cache).forEach((key) => {
66
- delete this._cache[key];
67
- });
68
- this._loadedAssets.clear();
107
+ this.lruCache.clear();
69
108
  this._currentCacheSize = 0;
109
+ sizeCache.clear(); // Clear size cache too
70
110
  }
71
111
 
72
112
  getAssetUrl(assetId: string): string | null {
73
- if (this._isAssetLoaded(assetId)) {
113
+ if (this.lruCache.has(assetId)) {
74
114
  return assetId;
75
115
  }
76
116
  return null;
77
117
  }
78
118
 
79
119
  isAssetLoaded(assetId: string): boolean {
80
- return this._loadedAssets.has(assetId);
120
+ return this.lruCache.has(assetId);
81
121
  }
82
122
 
83
123
  getLoadedAssets(): string[] {
84
- return Array.from(this._loadedAssets);
124
+ return this.lruCache.keys();
125
+ }
126
+
127
+ /**
128
+ * Get cache statistics
129
+ */
130
+ getCacheStats(): {
131
+ size: number;
132
+ count: number;
133
+ maxSize: number;
134
+ usage: number;
135
+ lruStats: { size: number; capacity: number; usage: number };
136
+ } {
137
+ return {
138
+ size: this._currentCacheSize,
139
+ count: this.lruCache.size(),
140
+ maxSize: this._maxCacheSize,
141
+ usage: this._currentCacheSize / this._maxCacheSize,
142
+ lruStats: this.lruCache.getStats(),
143
+ };
85
144
  }
86
145
 
87
146
  // Private Methods
@@ -92,68 +151,80 @@ export class AssetManager implements IAssetManager {
92
151
  return JSON.stringify(source);
93
152
  }
94
153
 
154
+ /**
155
+ * Cache asset with LRU eviction
156
+ */
95
157
  private _cacheAsset(assetId: string, data: unknown): void {
96
- const size = this._estimateSize(data);
158
+ const size = getEstimatedSize(data);
159
+
160
+ // Check if single asset exceeds cache size
161
+ if (size > this._maxCacheSize) {
162
+ console.warn(`Asset ${assetId} (${size} bytes) exceeds cache size (${this._maxCacheSize} bytes)`);
163
+ return;
164
+ }
165
+
166
+ // Check if updating existing asset
167
+ const existing = this.lruCache.get(assetId);
168
+ if (existing) {
169
+ this._currentCacheSize -= existing.size;
170
+ }
97
171
 
98
- // Check if cache is full
99
- if (this._currentCacheSize + size > this._maxCacheSize) {
100
- this._evictOldestAssets();
172
+ // Evict assets if needed (LRU handles this automatically)
173
+ const spaceNeeded = size - (existing?.size || 0);
174
+ if (this._currentCacheSize + spaceNeeded > this._maxCacheSize) {
175
+ this._evictLRUAssets(spaceNeeded);
101
176
  }
102
177
 
103
- this._cache[assetId] = {
178
+ // Add to cache
179
+ this.lruCache.set(assetId, {
104
180
  data,
105
- timestamp: Date.now(),
106
181
  size,
107
- };
108
-
109
- this._loadedAssets.add(assetId);
110
- this._currentCacheSize += size;
111
- }
182
+ timestamp: Date.now(),
183
+ });
112
184
 
113
- private _isAssetLoaded(assetId: string): boolean {
114
- return this._loadedAssets.has(assetId) && !!this._cache[assetId];
185
+ this._currentCacheSize += spaceNeeded;
115
186
  }
116
187
 
117
- private _evictOldestAssets(): void {
118
- const sortedAssets = Object.entries(this._cache)
119
- .sort(([, a], [, b]) => a.timestamp - b.timestamp);
188
+ /**
189
+ * Evict LRU assets to make space (much faster than sorting)
190
+ */
191
+ private _evictLRUAssets(requiredSpace: number): void {
192
+ const targetSpace = this._maxCacheSize * CACHE_EVICTION_RATIO;
193
+ const spaceToFree = Math.max(requiredSpace, targetSpace);
120
194
 
121
195
  let freedSpace = 0;
122
- const targetSpace = this._maxCacheSize * 0.3; // Evict 30% of cache
196
+ const keysToRemove: string[] = [];
123
197
 
124
- for (const [assetId, asset] of sortedAssets) {
125
- if (freedSpace >= targetSpace) {
198
+ // Iterate from least recently used (tail) to most recently used (head)
199
+ const keys = this.lruCache.keys();
200
+ for (const key of keys) {
201
+ if (freedSpace >= spaceToFree) {
126
202
  break;
127
203
  }
128
204
 
129
- delete this._cache[assetId];
130
- this._loadedAssets.delete(assetId);
131
- freedSpace += asset.size;
132
- this._currentCacheSize -= asset.size;
205
+ const entry = this.lruCache.get(key);
206
+ if (entry) {
207
+ freedSpace += entry.size;
208
+ keysToRemove.push(key);
209
+ }
133
210
  }
134
- }
135
211
 
136
- private _estimateSize(data: unknown): number {
137
- // Rough estimation in bytes
138
- return JSON.stringify(data).length * 2; // 2 bytes per char (UTF-16)
212
+ // Remove evicted entries
213
+ for (const key of keysToRemove) {
214
+ const entry = this.lruCache.get(key);
215
+ if (entry) {
216
+ this._currentCacheSize -= entry.size;
217
+ }
218
+ this.lruCache.delete(key);
219
+ }
139
220
  }
140
221
 
222
+ /**
223
+ * Load SVG from file (placeholder)
224
+ */
141
225
  private _loadSVGFromFile(_source: string): string {
142
226
  // In real implementation, this would use FileSystem or require()
143
227
  // For now, return a placeholder
144
228
  return `<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="currentColor"/></svg>`;
145
229
  }
146
-
147
- // Getters
148
- get cacheSize(): number {
149
- return this._currentCacheSize;
150
- }
151
-
152
- get cacheStats(): { size: number; count: number; maxSize: number } {
153
- return {
154
- size: this._currentCacheSize,
155
- count: this._loadedAssets.size,
156
- maxSize: this._maxCacheSize,
157
- };
158
- }
159
230
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Mascot Builder (80 lines)
3
+ * Builder pattern for custom mascot creation
4
+ */
5
+
6
+ import type { MascotConfig, MascotType } from '../../domain/types/MascotTypes';
7
+ import { getDefaultAnimations } from './MascotTemplates';
8
+
9
+ export class MascotBuilder {
10
+ private config: MascotConfig;
11
+
12
+ constructor(options: {
13
+ id?: string;
14
+ name?: string;
15
+ type?: MascotType;
16
+ } = {}) {
17
+ this.config = {
18
+ id: options.id || 'custom-mascot',
19
+ name: options.name || 'Custom Mascot',
20
+ type: options.type || 'svg',
21
+ personality: {
22
+ mood: 'happy',
23
+ energy: 0.7,
24
+ friendliness: 0.8,
25
+ playfulness: 0.6,
26
+ },
27
+ appearance: {
28
+ baseColor: '#FF6B6B',
29
+ accentColor: '#4ECDC4',
30
+ accessories: [],
31
+ style: 'minimal',
32
+ scale: 1,
33
+ },
34
+ animations: getDefaultAnimations(),
35
+ interactive: true,
36
+ touchEnabled: true,
37
+ soundEnabled: false,
38
+ };
39
+ }
40
+
41
+ withId(id: string): MascotBuilder {
42
+ this.config.id = id;
43
+ return this;
44
+ }
45
+
46
+ withName(name: string): MascotBuilder {
47
+ this.config.name = name;
48
+ return this;
49
+ }
50
+
51
+ withType(type: MascotType): MascotBuilder {
52
+ this.config.type = type;
53
+ return this;
54
+ }
55
+
56
+ withPersonality(personality: Partial<MascotConfig['personality']>): MascotBuilder {
57
+ this.config.personality = { ...this.config.personality, ...personality };
58
+ return this;
59
+ }
60
+
61
+ withAppearance(appearance: Partial<MascotConfig['appearance']>): MascotBuilder {
62
+ this.config.appearance = { ...this.config.appearance, ...appearance };
63
+ return this;
64
+ }
65
+
66
+ withBaseColor(color: string): MascotBuilder {
67
+ this.config.appearance.baseColor = color;
68
+ return this;
69
+ }
70
+
71
+ withAccentColor(color: string): MascotBuilder {
72
+ this.config.appearance.accentColor = color;
73
+ return this;
74
+ }
75
+
76
+ interactive(enabled: boolean = true): MascotBuilder {
77
+ this.config.interactive = enabled;
78
+ return this;
79
+ }
80
+
81
+ touchEnabled(enabled: boolean = true): MascotBuilder {
82
+ this.config.touchEnabled = enabled;
83
+ return this;
84
+ }
85
+
86
+ build(): MascotConfig {
87
+ return this.config;
88
+ }
89
+ }