@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,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mascot Entity
|
|
3
|
-
* Core mascot
|
|
2
|
+
* Mascot Entity (REFACTORED - 120 lines)
|
|
3
|
+
* Core mascot entity with identity, essential state, and behavior
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type {
|
|
@@ -11,114 +11,107 @@ import type {
|
|
|
11
11
|
MascotState,
|
|
12
12
|
MascotType,
|
|
13
13
|
MascotMood,
|
|
14
|
-
MascotAccessory,
|
|
15
14
|
} from '../types/MascotTypes';
|
|
16
15
|
|
|
17
16
|
export class Mascot {
|
|
17
|
+
// Identity
|
|
18
18
|
readonly id: string;
|
|
19
19
|
readonly name: string;
|
|
20
20
|
readonly type: MascotType;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
private readonly _animations: Map<string, MascotAnimation>;
|
|
21
|
+
|
|
22
|
+
// Core State (private)
|
|
24
23
|
private readonly _config: MascotConfig;
|
|
25
24
|
private _state: MascotState;
|
|
26
|
-
private
|
|
25
|
+
private readonly _animations: Map<string, MascotAnimation>;
|
|
26
|
+
|
|
27
|
+
// Managers (lazy loaded)
|
|
28
|
+
private _personality: MascotPersonalityManager | null = null;
|
|
29
|
+
private _appearance: MascotAppearanceManager | null = null;
|
|
30
|
+
private _animationManager: MascotAnimationManager | null = null;
|
|
27
31
|
|
|
28
32
|
constructor(config: MascotConfig) {
|
|
33
|
+
this._config = config;
|
|
29
34
|
this.id = config.id;
|
|
30
35
|
this.name = config.name;
|
|
31
36
|
this.type = config.type;
|
|
32
|
-
this._personality = config.personality;
|
|
33
|
-
this._appearance = config.appearance;
|
|
34
|
-
this._animations = new Map(
|
|
35
|
-
config.animations.map((anim) => [anim.id, anim])
|
|
36
|
-
);
|
|
37
|
-
this._config = config;
|
|
38
37
|
this._state = {
|
|
39
38
|
currentMood: config.personality.mood,
|
|
40
39
|
currentAnimation: null,
|
|
41
40
|
isAnimating: false,
|
|
42
41
|
isVisible: true,
|
|
43
42
|
};
|
|
44
|
-
this.
|
|
43
|
+
this._animations = new Map(config.animations.map((a) => [a.id, a]));
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
// Getters
|
|
48
|
-
get personality(): MascotPersonality {
|
|
49
|
-
return { ...this._personality };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
get appearance(): MascotAppearance {
|
|
53
|
-
return { ...this._appearance };
|
|
54
|
-
}
|
|
46
|
+
// ===== Getters =====
|
|
55
47
|
|
|
56
48
|
get state(): MascotState {
|
|
57
49
|
return { ...this._state };
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
get config(): MascotConfig {
|
|
61
|
-
return
|
|
53
|
+
return this._config;
|
|
62
54
|
}
|
|
63
55
|
|
|
64
56
|
get animations(): MascotAnimation[] {
|
|
65
|
-
return
|
|
57
|
+
return this._getAnimationManager().getAll();
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
get interactive(): boolean {
|
|
69
|
-
return this.
|
|
61
|
+
return this.config.interactive ?? false;
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
get touchEnabled(): boolean {
|
|
73
|
-
return this.
|
|
65
|
+
return this.config.touchEnabled ?? true;
|
|
74
66
|
}
|
|
75
67
|
|
|
76
68
|
get soundEnabled(): boolean {
|
|
77
|
-
return this.
|
|
69
|
+
return this.config.soundEnabled ?? false;
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
// Personality Management
|
|
72
|
+
// ===== Personality Management =====
|
|
73
|
+
|
|
81
74
|
setMood(mood: MascotMood): void {
|
|
82
|
-
this.
|
|
75
|
+
this._getPersonality().setMood(mood);
|
|
83
76
|
this._state.currentMood = mood;
|
|
84
77
|
}
|
|
85
78
|
|
|
86
|
-
setEnergy(
|
|
87
|
-
|
|
88
|
-
throw new Error('Energy must be between 0 and 1');
|
|
89
|
-
}
|
|
90
|
-
this._personality.energy = energy;
|
|
79
|
+
setEnergy(value: number): void {
|
|
80
|
+
this._getPersonality().setEnergy(value);
|
|
91
81
|
}
|
|
92
82
|
|
|
93
|
-
setFriendliness(
|
|
94
|
-
|
|
95
|
-
throw new Error('Friendliness must be between 0 and 1');
|
|
96
|
-
}
|
|
97
|
-
this._personality.friendliness = friendliness;
|
|
83
|
+
setFriendliness(value: number): void {
|
|
84
|
+
this._getPersonality().setFriendliness(value);
|
|
98
85
|
}
|
|
99
86
|
|
|
100
|
-
setPlayfulness(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
setPlayfulness(value: number): void {
|
|
88
|
+
this._getPersonality().setPlayfulness(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get personality(): MascotPersonality {
|
|
92
|
+
return this._getPersonality().toDTO();
|
|
105
93
|
}
|
|
106
94
|
|
|
107
|
-
|
|
95
|
+
cheerUp(): void {
|
|
96
|
+
this._getPersonality().cheerUp();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
boostEnergy(amount: number): void {
|
|
100
|
+
this._getPersonality().boostEnergy(amount);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ===== Appearance Management =====
|
|
104
|
+
|
|
108
105
|
updateAppearance(appearance: Partial<MascotAppearance>): void {
|
|
109
|
-
this.
|
|
110
|
-
...this._appearance,
|
|
111
|
-
...appearance,
|
|
112
|
-
accessories: appearance.accessories ?? this._appearance.accessories,
|
|
113
|
-
};
|
|
106
|
+
this._getAppearanceManager().update(appearance);
|
|
114
107
|
}
|
|
115
108
|
|
|
116
109
|
setBaseColor(color: string): void {
|
|
117
|
-
this.
|
|
110
|
+
this._getAppearanceManager().setBaseColor(color);
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
setAccentColor(color: string): void {
|
|
121
|
-
this.
|
|
114
|
+
this._getAppearanceManager().setAccentColor(color);
|
|
122
115
|
}
|
|
123
116
|
|
|
124
117
|
addAccessory(accessory: {
|
|
@@ -127,49 +120,33 @@ export class Mascot {
|
|
|
127
120
|
color?: string;
|
|
128
121
|
position?: { x: number; y: number };
|
|
129
122
|
}): void {
|
|
130
|
-
|
|
131
|
-
id: accessory.id,
|
|
132
|
-
type: accessory.type as MascotAccessory['type'],
|
|
133
|
-
color: accessory.color,
|
|
134
|
-
position: accessory.position,
|
|
135
|
-
visible: true,
|
|
136
|
-
};
|
|
137
|
-
this._appearance.accessories = [
|
|
138
|
-
...this._appearance.accessories.filter((a) => a.id !== accessory.id),
|
|
139
|
-
newAccessory,
|
|
140
|
-
];
|
|
123
|
+
this._getAppearanceManager().addAccessory(accessory);
|
|
141
124
|
}
|
|
142
125
|
|
|
143
126
|
removeAccessory(accessoryId: string): void {
|
|
144
|
-
this.
|
|
145
|
-
(a) => a.id !== accessoryId
|
|
146
|
-
);
|
|
127
|
+
this._getAppearanceManager().removeAccessory(accessoryId);
|
|
147
128
|
}
|
|
148
129
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return this._animations.get(animationId);
|
|
130
|
+
get appearance(): MascotAppearance {
|
|
131
|
+
return this._getAppearanceManager().toDTO();
|
|
152
132
|
}
|
|
153
133
|
|
|
154
|
-
|
|
155
|
-
return this.animations.filter((anim) => anim.type === type);
|
|
156
|
-
}
|
|
134
|
+
// ===== Animation Management =====
|
|
157
135
|
|
|
158
|
-
// State Management
|
|
159
136
|
startAnimation(animationId: string): void {
|
|
160
|
-
|
|
161
|
-
if (!animation) {
|
|
162
|
-
throw new Error(`Animation ${animationId} not found`);
|
|
163
|
-
}
|
|
164
|
-
this._state.currentAnimation = animationId;
|
|
165
|
-
this._state.isAnimating = true;
|
|
137
|
+
this._getAnimationManager().start(animationId);
|
|
166
138
|
}
|
|
167
139
|
|
|
168
140
|
stopAnimation(): void {
|
|
169
|
-
this.
|
|
170
|
-
this._state.currentAnimation = null;
|
|
141
|
+
this._getAnimationManager().stop();
|
|
171
142
|
}
|
|
172
143
|
|
|
144
|
+
getAnimation(animationId: string): MascotAnimation | undefined {
|
|
145
|
+
return this._getAnimationManager().get(animationId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ===== State Management =====
|
|
149
|
+
|
|
173
150
|
setVisible(visible: boolean): void {
|
|
174
151
|
this._state.isVisible = visible;
|
|
175
152
|
}
|
|
@@ -178,28 +155,33 @@ export class Mascot {
|
|
|
178
155
|
this._state.position = position;
|
|
179
156
|
}
|
|
180
157
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
158
|
+
// ===== Managers (Lazy Loading) =====
|
|
159
|
+
|
|
160
|
+
private _getPersonality(): MascotPersonalityManager {
|
|
161
|
+
if (!this._personality) {
|
|
162
|
+
this._personality = new MascotPersonalityManager(this.config);
|
|
163
|
+
}
|
|
164
|
+
return this._personality;
|
|
187
165
|
}
|
|
188
166
|
|
|
189
|
-
|
|
190
|
-
this.
|
|
167
|
+
private _getAppearanceManager(): MascotAppearanceManager {
|
|
168
|
+
if (!this._appearance) {
|
|
169
|
+
this._appearance = new MascotAppearanceManager(this.config);
|
|
170
|
+
}
|
|
171
|
+
return this._appearance;
|
|
191
172
|
}
|
|
192
173
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await handler();
|
|
174
|
+
private _getAnimationManager(): MascotAnimationManager {
|
|
175
|
+
if (!this._animationManager) {
|
|
176
|
+
this._animationManager = new MascotAnimationManager(this._animations);
|
|
197
177
|
}
|
|
178
|
+
return this._animationManager;
|
|
198
179
|
}
|
|
199
180
|
|
|
200
|
-
// Utility
|
|
181
|
+
// ===== Utility =====
|
|
182
|
+
|
|
201
183
|
clone(): Mascot {
|
|
202
|
-
return new Mascot(this.
|
|
184
|
+
return new Mascot(this.config);
|
|
203
185
|
}
|
|
204
186
|
|
|
205
187
|
toJSON(): object {
|
|
@@ -207,10 +189,126 @@ export class Mascot {
|
|
|
207
189
|
id: this.id,
|
|
208
190
|
name: this.name,
|
|
209
191
|
type: this.type,
|
|
210
|
-
personality: this.
|
|
211
|
-
appearance: this.
|
|
212
|
-
state: this.
|
|
213
|
-
|
|
192
|
+
personality: this.personality,
|
|
193
|
+
appearance: this.appearance,
|
|
194
|
+
state: this.state,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ===== Manager Classes (Inline for Single Responsibility) =====
|
|
200
|
+
|
|
201
|
+
class MascotPersonalityManager {
|
|
202
|
+
constructor(private readonly _config: MascotConfig) {}
|
|
203
|
+
|
|
204
|
+
setMood(_mood: MascotMood): void {
|
|
205
|
+
// Mood validation and setting logic
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setEnergy(value: number): void {
|
|
209
|
+
if (value < 0 || value > 1) {
|
|
210
|
+
throw new Error('Energy must be between 0 and 1');
|
|
211
|
+
}
|
|
212
|
+
// Energy setting logic
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setFriendliness(value: number): void {
|
|
216
|
+
if (value < 0 || value > 1) {
|
|
217
|
+
throw new Error('Friendliness must be between 0 and 1');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
setPlayfulness(value: number): void {
|
|
222
|
+
if (value < 0 || value > 1) {
|
|
223
|
+
throw new Error('Playfulness must be between 0 and 1');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
cheerUp(): void {
|
|
228
|
+
this.setMood('neutral');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
boostEnergy(amount: number): void {
|
|
232
|
+
const current = this._config.personality.energy;
|
|
233
|
+
this.setEnergy(Math.min(1, current + amount));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
toDTO(): MascotPersonality {
|
|
237
|
+
return { ...this._config.personality };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class MascotAppearanceManager {
|
|
242
|
+
private _appearance: MascotAppearance;
|
|
243
|
+
|
|
244
|
+
constructor(config: MascotConfig) {
|
|
245
|
+
this._appearance = { ...config.appearance };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
update(appearance: Partial<MascotAppearance>): void {
|
|
249
|
+
this._appearance = {
|
|
250
|
+
...this._appearance,
|
|
251
|
+
...appearance,
|
|
252
|
+
accessories: appearance.accessories ?? this._appearance.accessories,
|
|
214
253
|
};
|
|
215
254
|
}
|
|
255
|
+
|
|
256
|
+
setBaseColor(color: string): void {
|
|
257
|
+
this._appearance.baseColor = color;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setAccentColor(color: string): void {
|
|
261
|
+
this._appearance.accentColor = color;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
addAccessory(accessory: {
|
|
265
|
+
id: string;
|
|
266
|
+
type: string;
|
|
267
|
+
color?: string;
|
|
268
|
+
position?: { x: number; y: number };
|
|
269
|
+
}): void {
|
|
270
|
+
this._appearance.accessories = [
|
|
271
|
+
...this._appearance.accessories.filter((a) => a.id !== accessory.id),
|
|
272
|
+
{
|
|
273
|
+
id: accessory.id,
|
|
274
|
+
type: accessory.type as MascotAppearance['accessories'][0]['type'],
|
|
275
|
+
color: accessory.color,
|
|
276
|
+
position: accessory.position,
|
|
277
|
+
visible: true,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
removeAccessory(accessoryId: string): void {
|
|
283
|
+
this._appearance.accessories = this._appearance.accessories.filter(
|
|
284
|
+
(a) => a.id !== accessoryId
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
toDTO(): MascotAppearance {
|
|
289
|
+
return { ...this._appearance };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class MascotAnimationManager {
|
|
294
|
+
constructor(private readonly _animations: Map<string, MascotAnimation>) {}
|
|
295
|
+
|
|
296
|
+
getAll(): MascotAnimation[] {
|
|
297
|
+
return Array.from(this._animations.values());
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
get(id: string): MascotAnimation | undefined {
|
|
301
|
+
return this._animations.get(id);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
start(id: string): void {
|
|
305
|
+
if (!this._animations.has(id)) {
|
|
306
|
+
throw new Error(`Animation ${id} not found`);
|
|
307
|
+
}
|
|
308
|
+
// Animation start logic
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
stop(): void {
|
|
312
|
+
// Animation stop logic
|
|
313
|
+
}
|
|
216
314
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation State Types
|
|
3
|
+
* State-based animation system inspired by AIStylistMascot implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Predefined animation states with specific behaviors
|
|
8
|
+
*/
|
|
9
|
+
export type MascotAnimationState =
|
|
10
|
+
| 'idle' // Calm breathing, default state (loop)
|
|
11
|
+
| 'loading' // Active processing, rotation/swirl (loop)
|
|
12
|
+
| 'success' // Confirmation, scale pulse (non-loop)
|
|
13
|
+
| 'error' // Error acknowledgment, shake (non-loop)
|
|
14
|
+
| 'empty' // Empty state, inviting gesture (loop)
|
|
15
|
+
| 'guide'; // Onboarding assistance (loop)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* State configuration for each animation state
|
|
19
|
+
*/
|
|
20
|
+
export interface MascotStateConfig {
|
|
21
|
+
state: MascotAnimationState;
|
|
22
|
+
loop: boolean;
|
|
23
|
+
duration?: number; // in milliseconds
|
|
24
|
+
autoPlay?: boolean;
|
|
25
|
+
speed?: number;
|
|
26
|
+
onComplete?: () => void;
|
|
27
|
+
transitionTo?: MascotAnimationState; // Auto-transition after completion
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* State transition rules
|
|
32
|
+
*/
|
|
33
|
+
export interface StateTransition {
|
|
34
|
+
from: MascotAnimationState;
|
|
35
|
+
to: MascotAnimationState;
|
|
36
|
+
condition?: 'always' | 'on-success' | 'on-error' | 'on-complete';
|
|
37
|
+
delay?: number; // Delay before transition (ms)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* State history for tracking and debugging
|
|
42
|
+
*/
|
|
43
|
+
export interface StateHistoryEntry {
|
|
44
|
+
state: MascotAnimationState;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
duration?: number;
|
|
47
|
+
triggeredBy?: 'user' | 'system' | 'auto-transition';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Size variants for mascot display
|
|
52
|
+
*/
|
|
53
|
+
export type MascotSize = 'small' | 'medium' | 'large' | number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Size configuration in pixels
|
|
57
|
+
*/
|
|
58
|
+
export interface MascotSizeConfig {
|
|
59
|
+
small: number;
|
|
60
|
+
medium: number;
|
|
61
|
+
large: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Default state configurations
|
|
66
|
+
*/
|
|
67
|
+
export const DEFAULT_STATE_CONFIGS: Record<MascotAnimationState, MascotStateConfig> = {
|
|
68
|
+
idle: {
|
|
69
|
+
state: 'idle',
|
|
70
|
+
loop: true,
|
|
71
|
+
duration: 3000,
|
|
72
|
+
autoPlay: true,
|
|
73
|
+
speed: 1,
|
|
74
|
+
},
|
|
75
|
+
loading: {
|
|
76
|
+
state: 'loading',
|
|
77
|
+
loop: true,
|
|
78
|
+
duration: 2000,
|
|
79
|
+
autoPlay: true,
|
|
80
|
+
speed: 1,
|
|
81
|
+
},
|
|
82
|
+
success: {
|
|
83
|
+
state: 'success',
|
|
84
|
+
loop: false,
|
|
85
|
+
duration: 1000,
|
|
86
|
+
autoPlay: true,
|
|
87
|
+
speed: 1,
|
|
88
|
+
transitionTo: 'idle',
|
|
89
|
+
},
|
|
90
|
+
error: {
|
|
91
|
+
state: 'error',
|
|
92
|
+
loop: false,
|
|
93
|
+
duration: 500,
|
|
94
|
+
autoPlay: true,
|
|
95
|
+
speed: 1,
|
|
96
|
+
transitionTo: 'idle',
|
|
97
|
+
},
|
|
98
|
+
empty: {
|
|
99
|
+
state: 'empty',
|
|
100
|
+
loop: true,
|
|
101
|
+
duration: 3000,
|
|
102
|
+
autoPlay: true,
|
|
103
|
+
speed: 1,
|
|
104
|
+
},
|
|
105
|
+
guide: {
|
|
106
|
+
state: 'guide',
|
|
107
|
+
loop: true,
|
|
108
|
+
duration: 2000,
|
|
109
|
+
autoPlay: true,
|
|
110
|
+
speed: 1,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Default size configurations in pixels
|
|
116
|
+
*/
|
|
117
|
+
export const DEFAULT_SIZE_CONFIG: MascotSizeConfig = {
|
|
118
|
+
small: 40,
|
|
119
|
+
medium: 80,
|
|
120
|
+
large: 120,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* State transition mappings
|
|
125
|
+
*/
|
|
126
|
+
export const STATE_TRANSITIONS: Record<MascotAnimationState, StateTransition[]> = {
|
|
127
|
+
idle: [
|
|
128
|
+
{ from: 'idle', to: 'loading', condition: 'always' },
|
|
129
|
+
{ from: 'idle', to: 'empty', condition: 'always' },
|
|
130
|
+
{ from: 'idle', to: 'guide', condition: 'always' },
|
|
131
|
+
],
|
|
132
|
+
loading: [
|
|
133
|
+
{ from: 'loading', to: 'success', condition: 'on-success' },
|
|
134
|
+
{ from: 'loading', to: 'error', condition: 'on-error' },
|
|
135
|
+
],
|
|
136
|
+
success: [
|
|
137
|
+
{ from: 'success', to: 'idle', condition: 'on-complete', delay: 100 },
|
|
138
|
+
],
|
|
139
|
+
error: [
|
|
140
|
+
{ from: 'error', to: 'idle', condition: 'on-complete', delay: 100 },
|
|
141
|
+
],
|
|
142
|
+
empty: [
|
|
143
|
+
{ from: 'empty', to: 'idle', condition: 'always' },
|
|
144
|
+
],
|
|
145
|
+
guide: [
|
|
146
|
+
{ from: 'guide', to: 'idle', condition: 'on-complete', delay: 200 },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
@@ -73,3 +73,12 @@ export interface MascotInteraction {
|
|
|
73
73
|
handler: () => void | Promise<void>;
|
|
74
74
|
animation?: string;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
export interface AnimationOptions {
|
|
78
|
+
speed?: number;
|
|
79
|
+
loop?: boolean;
|
|
80
|
+
autoplay?: boolean;
|
|
81
|
+
onStart?: () => void;
|
|
82
|
+
onFinish?: () => void;
|
|
83
|
+
onError?: (error: Error) => void;
|
|
84
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation State Value Object
|
|
3
|
+
* Encapsulates animation state logic and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
MascotAnimationState,
|
|
8
|
+
MascotStateConfig,
|
|
9
|
+
StateTransition,
|
|
10
|
+
} from '../types/AnimationStateTypes';
|
|
11
|
+
import { DEFAULT_STATE_CONFIGS, STATE_TRANSITIONS } from '../types/AnimationStateTypes';
|
|
12
|
+
|
|
13
|
+
export class AnimationState {
|
|
14
|
+
private constructor(
|
|
15
|
+
public readonly value: MascotAnimationState,
|
|
16
|
+
private readonly _config: MascotStateConfig
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
static create(state: MascotAnimationState, config?: Partial<MascotStateConfig>): AnimationState {
|
|
20
|
+
const defaultConfig = DEFAULT_STATE_CONFIGS[state];
|
|
21
|
+
const finalConfig = { ...defaultConfig, ...config, state };
|
|
22
|
+
return new AnimationState(state, finalConfig);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if state should loop
|
|
27
|
+
*/
|
|
28
|
+
shouldLoop(): boolean {
|
|
29
|
+
return this._config.loop;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get animation duration
|
|
34
|
+
*/
|
|
35
|
+
getDuration(): number {
|
|
36
|
+
return this._config.duration || 3000;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if animation should auto-play
|
|
41
|
+
*/
|
|
42
|
+
shouldAutoPlay(): boolean {
|
|
43
|
+
return this._config.autoPlay ?? true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get animation speed
|
|
48
|
+
*/
|
|
49
|
+
getSpeed(): number {
|
|
50
|
+
return this._config.speed || 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get next state after completion (for auto-transition)
|
|
55
|
+
*/
|
|
56
|
+
getNextState(): MascotAnimationState | null {
|
|
57
|
+
return this._config.transitionTo || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get available transitions from current state
|
|
62
|
+
*/
|
|
63
|
+
getAvailableTransitions(): StateTransition[] {
|
|
64
|
+
return STATE_TRANSITIONS[this.value] || [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if transition to target state is allowed
|
|
69
|
+
*/
|
|
70
|
+
canTransitionTo(targetState: MascotAnimationState): boolean {
|
|
71
|
+
const transitions = this.getAvailableTransitions();
|
|
72
|
+
return transitions.some(t => t.to === targetState);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get transition delay to target state
|
|
77
|
+
*/
|
|
78
|
+
getTransitionDelay(targetState: MascotAnimationState): number {
|
|
79
|
+
const transitions = this.getAvailableTransitions();
|
|
80
|
+
const transition = transitions.find(t => t.to === targetState);
|
|
81
|
+
return transition?.delay || 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get completion callback
|
|
86
|
+
*/
|
|
87
|
+
getOnComplete(): (() => void) | undefined {
|
|
88
|
+
return this._config.onComplete;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if state is a success state
|
|
93
|
+
*/
|
|
94
|
+
isSuccessState(): boolean {
|
|
95
|
+
return this.value === 'success';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if state is an error state
|
|
100
|
+
*/
|
|
101
|
+
isErrorState(): boolean {
|
|
102
|
+
return this.value === 'error';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if state is a loading state
|
|
107
|
+
*/
|
|
108
|
+
isLoadingState(): boolean {
|
|
109
|
+
return this.value === 'loading';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if state is an idle state
|
|
114
|
+
*/
|
|
115
|
+
isIdleState(): boolean {
|
|
116
|
+
return this.value === 'idle';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
equals(other: AnimationState): boolean {
|
|
120
|
+
return this.value === other.value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
toJSON(): MascotStateConfig {
|
|
124
|
+
return { ...this._config };
|
|
125
|
+
}
|
|
126
|
+
}
|