@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
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* DI Container (OPTIMIZED)
|
|
3
|
+
* Lazy imports with caching and better singleton management
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { IMascotRepository } from '../../domain/interfaces/IMascotRepository';
|
|
@@ -9,12 +9,21 @@ import { AnimationController } from '../controllers/AnimationController';
|
|
|
9
9
|
import { AssetManager } from '../managers/AssetManager';
|
|
10
10
|
import { MascotRepository } from '../repositories/MascotRepository';
|
|
11
11
|
|
|
12
|
+
// Cache for lazy-loaded modules
|
|
13
|
+
interface ModuleCache<T> {
|
|
14
|
+
module: T | null;
|
|
15
|
+
initialized: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export class DIContainer {
|
|
13
19
|
private static _instance: DIContainer;
|
|
14
20
|
private _animationController: AnimationController | null = null;
|
|
15
21
|
private _assetManager: AssetManager | null = null;
|
|
16
22
|
private _repository: IMascotRepository | null = null;
|
|
17
|
-
private
|
|
23
|
+
private _mascotServiceCache: ModuleCache<MascotService> = {
|
|
24
|
+
module: null,
|
|
25
|
+
initialized: false,
|
|
26
|
+
};
|
|
18
27
|
|
|
19
28
|
private constructor() {}
|
|
20
29
|
|
|
@@ -56,35 +65,89 @@ export class DIContainer {
|
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
|
-
* Get or create MascotService singleton
|
|
68
|
+
* Get or create MascotService singleton (with caching)
|
|
60
69
|
*/
|
|
61
70
|
getMascotService(): MascotService {
|
|
62
|
-
if (!this.
|
|
71
|
+
if (!this._mascotServiceCache.initialized) {
|
|
63
72
|
// Lazy import to avoid circular dependencies
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
64
74
|
const { MascotService } = require('../../application/services/MascotService');
|
|
65
|
-
|
|
75
|
+
|
|
76
|
+
this._mascotServiceCache.module = new MascotService(
|
|
66
77
|
this.getRepository(),
|
|
67
78
|
this.getAnimationController(),
|
|
68
79
|
this.getAssetManager()
|
|
69
80
|
);
|
|
81
|
+
this._mascotServiceCache.initialized = true;
|
|
70
82
|
}
|
|
71
|
-
|
|
83
|
+
|
|
84
|
+
return this._mascotServiceCache.module!;
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
/**
|
|
75
88
|
* Reset all instances (useful for testing)
|
|
76
89
|
*/
|
|
77
90
|
reset(): void {
|
|
78
|
-
|
|
91
|
+
// Cleanup animation controller
|
|
92
|
+
if (this._animationController) {
|
|
93
|
+
this._animationController.destroy();
|
|
94
|
+
this._animationController = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
this._assetManager = null;
|
|
80
98
|
this._repository = null;
|
|
81
|
-
this.
|
|
99
|
+
this._mascotServiceCache = {
|
|
100
|
+
module: null,
|
|
101
|
+
initialized: false,
|
|
102
|
+
};
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
/**
|
|
85
106
|
* Check if container has been initialized
|
|
86
107
|
*/
|
|
87
108
|
isInitialized(): boolean {
|
|
88
|
-
return this.
|
|
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;
|
|
89
152
|
}
|
|
90
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
|
|
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.
|
|
20
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
120
|
+
return this.lruCache.has(assetId);
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
getLoadedAssets(): string[] {
|
|
84
|
-
return
|
|
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 =
|
|
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
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
return this._loadedAssets.has(assetId) && !!this._cache[assetId];
|
|
185
|
+
this._currentCacheSize += spaceNeeded;
|
|
115
186
|
}
|
|
116
187
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
196
|
+
const keysToRemove: string[] = [];
|
|
123
197
|
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|