@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,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mascot Factory
|
|
3
|
-
*
|
|
2
|
+
* Mascot Factory (100 lines)
|
|
3
|
+
* Factory methods for mascot creation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Mascot } from '../../domain/entities/Mascot';
|
|
7
7
|
import type { MascotConfig, MascotType } from '../../domain/types/MascotTypes';
|
|
8
|
+
import { getTemplateConfig } from './MascotTemplates';
|
|
9
|
+
import { MascotBuilder } from './MascotBuilder';
|
|
10
|
+
|
|
11
|
+
export type MascotTemplate =
|
|
12
|
+
| 'friendly-bot'
|
|
13
|
+
| 'cute-pet'
|
|
14
|
+
| 'wise-owl'
|
|
15
|
+
| 'pixel-hero';
|
|
8
16
|
|
|
9
17
|
export class MascotFactory {
|
|
10
18
|
/**
|
|
@@ -14,7 +22,7 @@ export class MascotFactory {
|
|
|
14
22
|
template: MascotTemplate,
|
|
15
23
|
customizations?: Partial<MascotConfig>
|
|
16
24
|
): Mascot {
|
|
17
|
-
const config =
|
|
25
|
+
const config = getTemplateConfig(template);
|
|
18
26
|
const finalConfig = this._mergeConfigs(config, customizations);
|
|
19
27
|
return new Mascot(finalConfig);
|
|
20
28
|
}
|
|
@@ -35,136 +43,23 @@ export class MascotFactory {
|
|
|
35
43
|
type?: MascotType;
|
|
36
44
|
baseColor?: string;
|
|
37
45
|
}): Mascot {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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);
|
|
46
|
+
const builder = new MascotBuilder(options);
|
|
47
|
+
if (options.baseColor) {
|
|
48
|
+
builder.withBaseColor(options.baseColor);
|
|
49
|
+
}
|
|
50
|
+
return new Mascot(builder.build());
|
|
62
51
|
}
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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];
|
|
53
|
+
/**
|
|
54
|
+
* Get a builder for fluent mascot creation
|
|
55
|
+
*/
|
|
56
|
+
static builder(): MascotBuilder {
|
|
57
|
+
return new MascotBuilder();
|
|
166
58
|
}
|
|
167
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Merge base config with customizations
|
|
62
|
+
*/
|
|
168
63
|
private static _mergeConfigs(
|
|
169
64
|
base: MascotConfig,
|
|
170
65
|
custom?: Partial<MascotConfig>
|
|
@@ -189,51 +84,4 @@ export class MascotFactory {
|
|
|
189
84
|
animations: custom.animations || base.animations,
|
|
190
85
|
};
|
|
191
86
|
}
|
|
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
87
|
}
|
|
234
|
-
|
|
235
|
-
export type MascotTemplate =
|
|
236
|
-
| 'friendly-bot'
|
|
237
|
-
| 'cute-pet'
|
|
238
|
-
| 'wise-owl'
|
|
239
|
-
| 'pixel-hero';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mascot Templates (100 lines)
|
|
3
|
+
* Predefined mascot template configurations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotConfig } from '../../domain/types/MascotTypes';
|
|
7
|
+
import type { MascotTemplate } from './MascotFactory';
|
|
8
|
+
|
|
9
|
+
export function getTemplateConfig(template: MascotTemplate): MascotConfig {
|
|
10
|
+
const templates: Record<MascotTemplate, MascotConfig> = {
|
|
11
|
+
'friendly-bot': {
|
|
12
|
+
id: 'friendly-bot',
|
|
13
|
+
name: 'Friendly Bot',
|
|
14
|
+
type: 'lottie',
|
|
15
|
+
personality: {
|
|
16
|
+
mood: 'happy',
|
|
17
|
+
energy: 0.8,
|
|
18
|
+
friendliness: 0.9,
|
|
19
|
+
playfulness: 0.7,
|
|
20
|
+
},
|
|
21
|
+
appearance: {
|
|
22
|
+
baseColor: '#4A90E2',
|
|
23
|
+
accentColor: '#50E3C2',
|
|
24
|
+
accessories: [],
|
|
25
|
+
style: 'cartoon',
|
|
26
|
+
scale: 1,
|
|
27
|
+
},
|
|
28
|
+
animations: getDefaultAnimations(),
|
|
29
|
+
interactive: true,
|
|
30
|
+
touchEnabled: true,
|
|
31
|
+
soundEnabled: false,
|
|
32
|
+
},
|
|
33
|
+
'cute-pet': {
|
|
34
|
+
id: 'cute-pet',
|
|
35
|
+
name: 'Cute Pet',
|
|
36
|
+
type: 'svg',
|
|
37
|
+
personality: {
|
|
38
|
+
mood: 'excited',
|
|
39
|
+
energy: 0.9,
|
|
40
|
+
friendliness: 1.0,
|
|
41
|
+
playfulness: 0.95,
|
|
42
|
+
},
|
|
43
|
+
appearance: {
|
|
44
|
+
baseColor: '#FFB6C1',
|
|
45
|
+
accentColor: '#FF69B4',
|
|
46
|
+
secondaryColor: '#FFF',
|
|
47
|
+
accessories: [
|
|
48
|
+
{ id: 'bow', type: 'bow', color: '#FF69B4', visible: true },
|
|
49
|
+
],
|
|
50
|
+
style: 'cartoon',
|
|
51
|
+
scale: 1,
|
|
52
|
+
},
|
|
53
|
+
animations: getDefaultAnimations(),
|
|
54
|
+
interactive: true,
|
|
55
|
+
touchEnabled: true,
|
|
56
|
+
soundEnabled: false,
|
|
57
|
+
},
|
|
58
|
+
'wise-owl': {
|
|
59
|
+
id: 'wise-owl',
|
|
60
|
+
name: 'Wise Owl',
|
|
61
|
+
type: 'lottie',
|
|
62
|
+
personality: {
|
|
63
|
+
mood: 'thinking',
|
|
64
|
+
energy: 0.4,
|
|
65
|
+
friendliness: 0.7,
|
|
66
|
+
playfulness: 0.3,
|
|
67
|
+
},
|
|
68
|
+
appearance: {
|
|
69
|
+
baseColor: '#8B4513',
|
|
70
|
+
accentColor: '#DAA520',
|
|
71
|
+
accessories: [
|
|
72
|
+
{ id: 'glasses', type: 'glasses', visible: true },
|
|
73
|
+
],
|
|
74
|
+
style: 'realistic',
|
|
75
|
+
scale: 1,
|
|
76
|
+
},
|
|
77
|
+
animations: getDefaultAnimations(),
|
|
78
|
+
interactive: true,
|
|
79
|
+
touchEnabled: true,
|
|
80
|
+
soundEnabled: false,
|
|
81
|
+
},
|
|
82
|
+
'pixel-hero': {
|
|
83
|
+
id: 'pixel-hero',
|
|
84
|
+
name: 'Pixel Hero',
|
|
85
|
+
type: 'svg',
|
|
86
|
+
personality: {
|
|
87
|
+
mood: 'happy',
|
|
88
|
+
energy: 0.85,
|
|
89
|
+
friendliness: 0.75,
|
|
90
|
+
playfulness: 0.8,
|
|
91
|
+
},
|
|
92
|
+
appearance: {
|
|
93
|
+
baseColor: '#3498DB',
|
|
94
|
+
accentColor: '#E74C3C',
|
|
95
|
+
secondaryColor: '#F1C40F',
|
|
96
|
+
accessories: [
|
|
97
|
+
{ id: 'crown', type: 'crown', color: '#F1C40F', visible: true },
|
|
98
|
+
],
|
|
99
|
+
style: 'pixel',
|
|
100
|
+
scale: 1,
|
|
101
|
+
},
|
|
102
|
+
animations: getDefaultAnimations(),
|
|
103
|
+
interactive: true,
|
|
104
|
+
touchEnabled: true,
|
|
105
|
+
soundEnabled: false,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return templates[template];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getDefaultAnimations() {
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
id: 'idle',
|
|
116
|
+
name: 'Idle',
|
|
117
|
+
type: 'idle' as const,
|
|
118
|
+
source: 'idle.json',
|
|
119
|
+
loop: true,
|
|
120
|
+
autoplay: true,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'wave',
|
|
124
|
+
name: 'Wave',
|
|
125
|
+
type: 'action' as const,
|
|
126
|
+
source: 'wave.json',
|
|
127
|
+
loop: false,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'jump',
|
|
131
|
+
name: 'Jump',
|
|
132
|
+
type: 'action' as const,
|
|
133
|
+
source: 'jump.json',
|
|
134
|
+
loop: false,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'success',
|
|
138
|
+
name: 'Success',
|
|
139
|
+
type: 'reaction' as const,
|
|
140
|
+
source: 'success.json',
|
|
141
|
+
loop: false,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'error',
|
|
145
|
+
name: 'Error',
|
|
146
|
+
type: 'reaction' as const,
|
|
147
|
+
source: 'error.json',
|
|
148
|
+
loop: false,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU (Least Recently Used) Cache Entry
|
|
3
|
+
* Efficient cache implementation with O(1) operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface LRUNode<K, V> {
|
|
7
|
+
key: K;
|
|
8
|
+
value: V;
|
|
9
|
+
prev: LRUNode<K, V> | null;
|
|
10
|
+
next: LRUNode<K, V> | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* LRU Cache implementation
|
|
15
|
+
* Provides O(1) get/set operations with automatic eviction
|
|
16
|
+
*/
|
|
17
|
+
export class LRUCache<K, V> {
|
|
18
|
+
private capacity: number;
|
|
19
|
+
private cache: Map<K, LRUNode<K, V>>;
|
|
20
|
+
private head: LRUNode<K, V> | null;
|
|
21
|
+
private tail: LRUNode<K, V> | null;
|
|
22
|
+
private _size: number;
|
|
23
|
+
|
|
24
|
+
constructor(capacity: number) {
|
|
25
|
+
this.capacity = capacity;
|
|
26
|
+
this.cache = new Map();
|
|
27
|
+
this.head = null;
|
|
28
|
+
this.tail = null;
|
|
29
|
+
this._size = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get value by key and move to front (most recently used)
|
|
34
|
+
*/
|
|
35
|
+
get(key: K): V | undefined {
|
|
36
|
+
const node = this.cache.get(key);
|
|
37
|
+
if (!node) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Move to front (most recently used)
|
|
42
|
+
this.moveToFront(node);
|
|
43
|
+
return node.value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Set value and evict least recently used if at capacity
|
|
48
|
+
*/
|
|
49
|
+
set(key: K, value: V): void {
|
|
50
|
+
const existingNode = this.cache.get(key);
|
|
51
|
+
|
|
52
|
+
if (existingNode) {
|
|
53
|
+
// Update existing node
|
|
54
|
+
existingNode.value = value;
|
|
55
|
+
this.moveToFront(existingNode);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create new node
|
|
60
|
+
const newNode: LRUNode<K, V> = {
|
|
61
|
+
key,
|
|
62
|
+
value,
|
|
63
|
+
prev: null,
|
|
64
|
+
next: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Add to cache
|
|
68
|
+
this.cache.set(key, newNode);
|
|
69
|
+
this.addToFront(newNode);
|
|
70
|
+
this._size++;
|
|
71
|
+
|
|
72
|
+
// Evict if at capacity
|
|
73
|
+
if (this._size > this.capacity) {
|
|
74
|
+
this.removeLeastRecentlyUsed();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if key exists
|
|
80
|
+
*/
|
|
81
|
+
has(key: K): boolean {
|
|
82
|
+
return this.cache.has(key);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delete specific key
|
|
87
|
+
*/
|
|
88
|
+
delete(key: K): boolean {
|
|
89
|
+
const node = this.cache.get(key);
|
|
90
|
+
if (!node) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.removeNode(node);
|
|
95
|
+
this.cache.delete(key);
|
|
96
|
+
this._size--;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clear all entries
|
|
102
|
+
*/
|
|
103
|
+
clear(): void {
|
|
104
|
+
this.cache.clear();
|
|
105
|
+
this.head = null;
|
|
106
|
+
this.tail = null;
|
|
107
|
+
this._size = 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get current size
|
|
112
|
+
*/
|
|
113
|
+
size(): number {
|
|
114
|
+
return this._size;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all keys in order (most recent to least recent)
|
|
119
|
+
*/
|
|
120
|
+
keys(): K[] {
|
|
121
|
+
const keys: K[] = [];
|
|
122
|
+
let current = this.head;
|
|
123
|
+
while (current) {
|
|
124
|
+
keys.push(current.key);
|
|
125
|
+
current = current.next;
|
|
126
|
+
}
|
|
127
|
+
return keys;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get cache statistics
|
|
132
|
+
*/
|
|
133
|
+
getStats(): { size: number; capacity: number; usage: number } {
|
|
134
|
+
return {
|
|
135
|
+
size: this._size,
|
|
136
|
+
capacity: this.capacity,
|
|
137
|
+
usage: this._size / this.capacity,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Add node to front of list (most recently used)
|
|
143
|
+
*/
|
|
144
|
+
private addToFront(node: LRUNode<K, V>): void {
|
|
145
|
+
node.prev = null;
|
|
146
|
+
node.next = this.head;
|
|
147
|
+
|
|
148
|
+
if (this.head) {
|
|
149
|
+
this.head.prev = node;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.head = node;
|
|
153
|
+
|
|
154
|
+
if (!this.tail) {
|
|
155
|
+
this.tail = node;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Move existing node to front
|
|
161
|
+
*/
|
|
162
|
+
private moveToFront(node: LRUNode<K, V>): void {
|
|
163
|
+
if (node === this.head) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Remove node from current position
|
|
168
|
+
this.removeNode(node);
|
|
169
|
+
|
|
170
|
+
// Add to front
|
|
171
|
+
node.prev = null;
|
|
172
|
+
node.next = this.head;
|
|
173
|
+
|
|
174
|
+
if (this.head) {
|
|
175
|
+
this.head.prev = node;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.head = node;
|
|
179
|
+
|
|
180
|
+
// If node was tail, update tail
|
|
181
|
+
if (this.tail === null) {
|
|
182
|
+
this.tail = node;
|
|
183
|
+
} else if (this.tail.prev === node) {
|
|
184
|
+
this.tail = node;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Remove node from list (without deleting from cache)
|
|
190
|
+
*/
|
|
191
|
+
private removeNode(node: LRUNode<K, V>): void {
|
|
192
|
+
if (node.prev) {
|
|
193
|
+
node.prev.next = node.next;
|
|
194
|
+
} else {
|
|
195
|
+
this.head = node.next;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (node.next) {
|
|
199
|
+
node.next.prev = node.prev;
|
|
200
|
+
} else {
|
|
201
|
+
this.tail = node.prev;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove least recently used (tail) node
|
|
207
|
+
*/
|
|
208
|
+
private removeLeastRecentlyUsed(): void {
|
|
209
|
+
if (!this.tail) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const lruKey = this.tail.key;
|
|
214
|
+
this.removeNode(this.tail);
|
|
215
|
+
this.cache.delete(lruKey);
|
|
216
|
+
this._size--;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Layer Exports (60 lines)
|
|
3
|
+
* Controllers, repositories, managers, and utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Infrastructure - DI
|
|
7
|
+
export { DIContainer } from './infrastructure/di/Container';
|
|
8
|
+
|
|
9
|
+
// Infrastructure - Repositories
|
|
10
|
+
export { MascotRepository } from './infrastructure/repositories/MascotRepository';
|
|
11
|
+
|
|
12
|
+
// Infrastructure - Controllers
|
|
13
|
+
export { AnimationController } from './infrastructure/controllers/AnimationController';
|
|
14
|
+
export { AnimationPlayer } from './infrastructure/controllers/AnimationPlayer';
|
|
15
|
+
export { EventManager } from './infrastructure/controllers/EventManager';
|
|
16
|
+
export { AnimationTimer } from './infrastructure/controllers/AnimationTimer';
|
|
17
|
+
|
|
18
|
+
// Infrastructure - Managers
|
|
19
|
+
export { AssetManager } from './infrastructure/managers/AssetManager';
|
|
20
|
+
export { MascotFactory, type MascotTemplate as FactoryMascotTemplate } from './infrastructure/managers/MascotFactory';
|
|
21
|
+
export { MascotBuilder } from './infrastructure/managers/MascotBuilder';
|
|
22
|
+
|
|
23
|
+
// Infrastructure - Utils
|
|
24
|
+
export { LRUCache } from './infrastructure/utils/LRUCache';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lottie Mascot Component (80 lines)
|
|
3
|
+
* Lottie-specific rendering with auto-play and caching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { memo, useMemo, useRef, useEffect } from 'react';
|
|
7
|
+
import { View, ViewStyle } from 'react-native';
|
|
8
|
+
import LottieView from 'lottie-react-native';
|
|
9
|
+
import type { AnimationObject } from 'lottie-react-native';
|
|
10
|
+
import type { Mascot } from '../../domain/entities/Mascot';
|
|
11
|
+
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
12
|
+
|
|
13
|
+
type LottieAnimationSource = string | AnimationObject | { uri: string };
|
|
14
|
+
|
|
15
|
+
export interface LottieMascotProps {
|
|
16
|
+
mascot: Mascot;
|
|
17
|
+
animation?: MascotAnimation | null;
|
|
18
|
+
resizeMode?: 'cover' | 'contain' | 'center';
|
|
19
|
+
onAnimationFinish?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LottieMascotComponent: React.FC<LottieMascotProps> = memo(({
|
|
23
|
+
mascot,
|
|
24
|
+
animation,
|
|
25
|
+
resizeMode = 'contain',
|
|
26
|
+
onAnimationFinish,
|
|
27
|
+
}) => {
|
|
28
|
+
const lottieRef = useRef<LottieView>(null);
|
|
29
|
+
|
|
30
|
+
// Memoize source to prevent reloads
|
|
31
|
+
const source = useMemo(() => {
|
|
32
|
+
return animation?.source || mascot.animations.find((a: MascotAnimation) => a.type === 'idle')?.source;
|
|
33
|
+
}, [animation?.source, mascot.animations]);
|
|
34
|
+
|
|
35
|
+
// Play animation when source changes
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (source && lottieRef.current) {
|
|
38
|
+
lottieRef.current.play();
|
|
39
|
+
}
|
|
40
|
+
}, [source]);
|
|
41
|
+
|
|
42
|
+
if (!source) {
|
|
43
|
+
return (
|
|
44
|
+
<FallbackMascot
|
|
45
|
+
mascot={mascot}
|
|
46
|
+
size={0}
|
|
47
|
+
style={{ width: '100%', height: '100%', backgroundColor: mascot.appearance.baseColor }}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<LottieView
|
|
54
|
+
ref={lottieRef}
|
|
55
|
+
source={source as LottieAnimationSource}
|
|
56
|
+
style={{ width: '100%', height: '100%' }}
|
|
57
|
+
resizeMode={resizeMode}
|
|
58
|
+
autoPlay={animation?.autoplay}
|
|
59
|
+
loop={animation?.loop}
|
|
60
|
+
onAnimationFinish={onAnimationFinish}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
LottieMascotComponent.displayName = 'LottieMascot';
|
|
66
|
+
|
|
67
|
+
export const LottieMascot = LottieMascotComponent;
|
|
68
|
+
|
|
69
|
+
// Fallback component for when no source is available
|
|
70
|
+
interface FallbackMascotProps {
|
|
71
|
+
mascot: Mascot;
|
|
72
|
+
size?: number;
|
|
73
|
+
style?: ViewStyle;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const FallbackMascotComponent: React.FC<FallbackMascotProps> = ({ mascot, style }) => (
|
|
77
|
+
<View style={[{ backgroundColor: mascot.appearance.baseColor, borderRadius: 100, justifyContent: 'center', alignItems: 'center' }, style]}>
|
|
78
|
+
<View style={{ width: 10, height: 10, backgroundColor: '#000', borderRadius: 5, top: '40%', position: 'absolute' }} />
|
|
79
|
+
<View style={{ width: 10, height: 10, backgroundColor: '#000', borderRadius: 5, top: '40%', position: 'absolute' }} />
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
FallbackMascotComponent.displayName = 'FallbackMascot';
|
|
84
|
+
|
|
85
|
+
export const FallbackMascot = memo(FallbackMascotComponent);
|