@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
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@ Interactive mascot system for React Native apps - Customizable animated characte
|
|
|
8
8
|
- ✅ **Lottie Animations** - Professional JSON animations
|
|
9
9
|
- ✅ **SVG Rendering** - Custom vector graphics
|
|
10
10
|
- ✅ **Mood System** - Dynamic personality and emotions
|
|
11
|
+
- ✅ **State-Based Animations** - 6 predefined states (idle, loading, success, error, empty, guide)
|
|
12
|
+
- ✅ **Auto-Transitions** - Automatic state transitions (e.g., loading → success)
|
|
13
|
+
- ✅ **Size Variants** - Built-in small, medium, large sizes
|
|
14
|
+
- ✅ **Optimized Performance** - LRU cache, React.memo, memory leak prevention
|
|
11
15
|
- ✅ **Interactive** - Touch-enabled mascots
|
|
12
16
|
- ✅ **Animation Queue** - Sequence animations
|
|
13
17
|
- ✅ **Custom Accessories** - Add glasses, hats, etc.
|
|
@@ -99,6 +103,62 @@ function CustomMascotScreen() {
|
|
|
99
103
|
touchEnabled: true
|
|
100
104
|
});
|
|
101
105
|
|
|
106
|
+
return <MascotView mascot={mascot} size={150} />;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### State-Based Animations (NEW!)
|
|
111
|
+
|
|
112
|
+
Inspired by production apps like Vivoim Style, the new state-based system provides predefined animation states with auto-transitions:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { useMascotState } from '@umituz/react-native-mascot';
|
|
116
|
+
|
|
117
|
+
function StatefulMascot() {
|
|
118
|
+
const mascot = useMascotState({
|
|
119
|
+
initialState: 'idle',
|
|
120
|
+
size: 'medium',
|
|
121
|
+
enableAutoTransition: true,
|
|
122
|
+
onStateChange: (from, to) => console.log(`${from} → ${to}`),
|
|
123
|
+
onAnimationComplete: (state) => console.log(`Completed: ${state}`)
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<View>
|
|
128
|
+
{/* Use mascot.state to drive your Lottie animations */}
|
|
129
|
+
{/* mascot.size: 40 (small), 80 (medium), 120 (large), or custom number */}
|
|
130
|
+
{/* mascot.isLooping: true for idle/loading/empty/guide, false for success/error */}
|
|
131
|
+
{/* mascot.duration: animation duration in milliseconds */}
|
|
132
|
+
{/* mascot.speed: animation speed multiplier */}
|
|
133
|
+
|
|
134
|
+
<MascotView
|
|
135
|
+
mascot={mascot.mascot}
|
|
136
|
+
size={mascot.size}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
<Button title="Start Loading" onPress={mascot.startLoading} />
|
|
140
|
+
<Button title="Success" onPress={mascot.triggerSuccess} />
|
|
141
|
+
<Button title="Error" onPress={mascot.triggerError} />
|
|
142
|
+
<Button title="Reset" onPress={mascot.reset} />
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Available States:**
|
|
149
|
+
- `idle` - Calm breathing animation (default, loops)
|
|
150
|
+
- `loading` - Active processing animation (loops)
|
|
151
|
+
- `success` - Confirmation animation (non-looping, auto-transitions to idle)
|
|
152
|
+
- `error` - Error acknowledgment (non-looping, auto-transitions to idle)
|
|
153
|
+
- `empty` - Empty state invitation (loops)
|
|
154
|
+
- `guide` - Onboarding assistance (loops)
|
|
155
|
+
|
|
156
|
+
**Auto-Transitions:**
|
|
157
|
+
- `success` → `idle` (after 100ms delay)
|
|
158
|
+
- `error` → `idle` (after 100ms delay)
|
|
159
|
+
- `guide` → `idle` (after 200ms delay)
|
|
160
|
+
- `loading` → `success` or `error` (when you call `stopLoading(success)`)
|
|
161
|
+
|
|
102
162
|
return <MascotView mascot={mascot} />;
|
|
103
163
|
}
|
|
104
164
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-mascot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Interactive mascot system for React Native apps - Customizable animated characters with Lottie and SVG support, mood system, and easy integration",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
+
"@types/node": "^22.10.2",
|
|
68
69
|
"@types/react": "~19.1.0",
|
|
69
70
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
70
71
|
"@typescript-eslint/parser": "^7.0.0",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation State Manager (Main - 60 lines)
|
|
3
|
+
* Public API for state management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StateMachine } from './StateMachine';
|
|
7
|
+
import type { MascotAnimationState } from '../../domain/types/AnimationStateTypes';
|
|
8
|
+
|
|
9
|
+
export interface AnimationStateManagerConfig {
|
|
10
|
+
enableAutoTransition?: boolean;
|
|
11
|
+
enableHistoryTracking?: boolean;
|
|
12
|
+
maxHistorySize?: number;
|
|
13
|
+
onStateChange?: (from: MascotAnimationState, to: MascotAnimationState) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class AnimationStateManager {
|
|
17
|
+
private readonly _stateMachine: StateMachine;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
initialState: MascotAnimationState = 'idle',
|
|
21
|
+
config: AnimationStateManagerConfig = {}
|
|
22
|
+
) {
|
|
23
|
+
this._stateMachine = new StateMachine(initialState, config);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get currentState(): MascotAnimationState {
|
|
27
|
+
return this._stateMachine.currentStateValue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get previousState(): MascotAnimationState | null {
|
|
31
|
+
return this._stateMachine.previousState?.value || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get stateHistory(): MascotAnimationState[] {
|
|
35
|
+
return this._stateMachine.history;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
transitionTo(newState: MascotAnimationState): void {
|
|
39
|
+
this._stateMachine.transitionTo(newState);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
triggerSuccess(): void {
|
|
43
|
+
this._stateMachine.triggerSuccess();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
triggerError(): void {
|
|
47
|
+
this._stateMachine.triggerError();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
startLoading(): void {
|
|
51
|
+
this._stateMachine.startLoading();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
stopLoading(success: boolean): void {
|
|
55
|
+
this._stateMachine.stopLoading(success);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
reset(): void {
|
|
59
|
+
this._stateMachine.reset();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
isLooping(): boolean {
|
|
63
|
+
return this._stateMachine.isLooping();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
destroy(): void {
|
|
67
|
+
this._stateMachine.destroy();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appearance Management (60 lines)
|
|
3
|
+
* Mascot appearance and accessory operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Mascot } from '../../domain/entities/Mascot';
|
|
7
|
+
import type { MascotAppearance } from '../../domain/types/MascotTypes';
|
|
8
|
+
|
|
9
|
+
export class AppearanceManagement {
|
|
10
|
+
constructor(private readonly mascot: Mascot) {}
|
|
11
|
+
|
|
12
|
+
updateAppearance(appearance: Partial<MascotAppearance>): void {
|
|
13
|
+
this.mascot.updateAppearance(appearance);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setBaseColor(color: string): void {
|
|
17
|
+
this.mascot.setBaseColor(color);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setAccentColor(color: string): void {
|
|
21
|
+
this.mascot.setAccentColor(color);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
addAccessory(accessory: {
|
|
25
|
+
id: string;
|
|
26
|
+
type: string;
|
|
27
|
+
color?: string;
|
|
28
|
+
position?: { x: number; y: number };
|
|
29
|
+
}): void {
|
|
30
|
+
this.mascot.addAccessory(accessory);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
removeAccessory(accessoryId: string): void {
|
|
34
|
+
this.mascot.removeAccessory(accessoryId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get appearance() {
|
|
38
|
+
return this.mascot.appearance;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This is the main entry point for all mascot operations
|
|
2
|
+
* Mascot Service (120 lines)
|
|
3
|
+
* Core application service for mascot operations
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { Mascot } from '../../domain/entities/Mascot';
|
|
8
|
-
import type { MascotConfig, MascotMood, MascotAppearance
|
|
7
|
+
import type { MascotConfig, MascotMood, MascotAppearance } from '../../domain/types/MascotTypes';
|
|
9
8
|
import type { IMascotRepository } from '../../domain/interfaces/IMascotRepository';
|
|
10
9
|
import type { IAnimationController, AnimationOptions } from '../../domain/interfaces/IAnimationController';
|
|
11
10
|
import type { IAssetManager } from '../../domain/interfaces/IAssetManager';
|
|
12
11
|
import { MascotFactory } from '../../infrastructure/managers/MascotFactory';
|
|
13
12
|
import { MascotNotInitializedError, AnimationNotFoundError } from '../errors/MascotErrors';
|
|
13
|
+
import { PersonalityManagement } from './PersonalityManagement';
|
|
14
|
+
import { AppearanceManagement } from './AppearanceManagement';
|
|
14
15
|
|
|
15
16
|
export type MascotTemplate = 'friendly-bot' | 'cute-pet' | 'wise-owl' | 'pixel-hero';
|
|
16
17
|
|
|
17
18
|
export class MascotService {
|
|
18
19
|
private _mascot: Mascot | null = null;
|
|
19
20
|
private _changeListeners: Set<() => void> = new Set();
|
|
21
|
+
private _personality: PersonalityManagement | null = null;
|
|
22
|
+
private _appearance: AppearanceManagement | null = null;
|
|
20
23
|
|
|
21
24
|
constructor(
|
|
22
25
|
private readonly _repository: IMascotRepository,
|
|
23
26
|
private readonly _animationController: IAnimationController,
|
|
24
|
-
|
|
25
|
-
) {
|
|
27
|
+
_assetManager: IAssetManager
|
|
28
|
+
) {
|
|
29
|
+
void _assetManager;
|
|
30
|
+
}
|
|
26
31
|
|
|
27
32
|
// ✅ Initialization
|
|
28
33
|
async initialize(config: MascotConfig): Promise<void> {
|
|
@@ -58,60 +63,67 @@ export class MascotService {
|
|
|
58
63
|
return this._mascot !== null;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
// ✅
|
|
66
|
+
// ✅ Manager Access (lazy loaded)
|
|
67
|
+
private _getPersonality(): PersonalityManagement {
|
|
68
|
+
if (!this._personality) {
|
|
69
|
+
this._ensureMascot();
|
|
70
|
+
this._personality = new PersonalityManagement(this._mascot!);
|
|
71
|
+
}
|
|
72
|
+
return this._personality;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private _getAppearance(): AppearanceManagement {
|
|
76
|
+
if (!this._appearance) {
|
|
77
|
+
this._ensureMascot();
|
|
78
|
+
this._appearance = new AppearanceManagement(this._mascot!);
|
|
79
|
+
}
|
|
80
|
+
return this._appearance;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ✅ Personality Management (delegated)
|
|
62
84
|
setMood(mood: MascotMood): void {
|
|
63
|
-
this.
|
|
64
|
-
this._mascot!.setMood(mood);
|
|
85
|
+
this._getPersonality().setMood(mood);
|
|
65
86
|
this._notifyChange();
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
setEnergy(value: number): void {
|
|
69
|
-
this.
|
|
70
|
-
this._mascot!.setEnergy(value);
|
|
90
|
+
this._getPersonality().setEnergy(value);
|
|
71
91
|
this._notifyChange();
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
setFriendliness(value: number): void {
|
|
75
|
-
this.
|
|
76
|
-
this._mascot!.setFriendliness(value);
|
|
95
|
+
this._getPersonality().setFriendliness(value);
|
|
77
96
|
this._notifyChange();
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
setPlayfulness(value: number): void {
|
|
81
|
-
this.
|
|
82
|
-
this._mascot!.setPlayfulness(value);
|
|
100
|
+
this._getPersonality().setPlayfulness(value);
|
|
83
101
|
this._notifyChange();
|
|
84
102
|
}
|
|
85
103
|
|
|
86
|
-
// ✅ Rich Behaviors
|
|
87
104
|
cheerUp(): void {
|
|
88
|
-
this.
|
|
89
|
-
this._mascot!.cheerUp();
|
|
105
|
+
this._getPersonality().cheerUp();
|
|
90
106
|
this._notifyChange();
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
boostEnergy(amount: number): void {
|
|
94
|
-
this.
|
|
95
|
-
this._mascot!.boostEnergy(amount);
|
|
110
|
+
this._getPersonality().boostEnergy(amount);
|
|
96
111
|
this._notifyChange();
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
// ✅ Appearance Management
|
|
114
|
+
// ✅ Appearance Management (delegated)
|
|
100
115
|
updateAppearance(appearance: Partial<MascotAppearance>): void {
|
|
101
|
-
this.
|
|
102
|
-
this._mascot!.updateAppearance(appearance);
|
|
116
|
+
this._getAppearance().updateAppearance(appearance);
|
|
103
117
|
this._notifyChange();
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
setBaseColor(color: string): void {
|
|
107
|
-
this.
|
|
108
|
-
this._mascot!.setBaseColor(color);
|
|
121
|
+
this._getAppearance().setBaseColor(color);
|
|
109
122
|
this._notifyChange();
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
setAccentColor(color: string): void {
|
|
113
|
-
this.
|
|
114
|
-
this._mascot!.setAccentColor(color);
|
|
126
|
+
this._getAppearance().setAccentColor(color);
|
|
115
127
|
this._notifyChange();
|
|
116
128
|
}
|
|
117
129
|
|
|
@@ -121,14 +133,12 @@ export class MascotService {
|
|
|
121
133
|
color?: string;
|
|
122
134
|
position?: { x: number; y: number };
|
|
123
135
|
}): void {
|
|
124
|
-
this.
|
|
125
|
-
this._mascot!.addAccessory(accessory);
|
|
136
|
+
this._getAppearance().addAccessory(accessory);
|
|
126
137
|
this._notifyChange();
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
removeAccessory(accessoryId: string): void {
|
|
130
|
-
this.
|
|
131
|
-
this._mascot!.removeAccessory(accessoryId);
|
|
141
|
+
this._getAppearance().removeAccessory(accessoryId);
|
|
132
142
|
this._notifyChange();
|
|
133
143
|
}
|
|
134
144
|
|
|
@@ -175,7 +185,7 @@ export class MascotService {
|
|
|
175
185
|
this._notifyChange();
|
|
176
186
|
}
|
|
177
187
|
|
|
178
|
-
// ✅ Observable Pattern
|
|
188
|
+
// ✅ Observable Pattern
|
|
179
189
|
subscribe(listener: () => void): () => void {
|
|
180
190
|
this._changeListeners.add(listener);
|
|
181
191
|
return () => this._changeListeners.delete(listener);
|
|
@@ -185,7 +195,6 @@ export class MascotService {
|
|
|
185
195
|
this._changeListeners.forEach((listener) => listener());
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
// ✅ Validation
|
|
189
198
|
private _ensureMascot(): void {
|
|
190
199
|
if (!this._mascot) {
|
|
191
200
|
throw new MascotNotInitializedError();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personality Management (60 lines)
|
|
3
|
+
* Mascot personality operations and behaviors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Mascot } from '../../domain/entities/Mascot';
|
|
7
|
+
import type { MascotMood } from '../../domain/types/MascotTypes';
|
|
8
|
+
|
|
9
|
+
export class PersonalityManagement {
|
|
10
|
+
constructor(private readonly mascot: Mascot) {}
|
|
11
|
+
|
|
12
|
+
setMood(mood: MascotMood): void {
|
|
13
|
+
this.mascot.setMood(mood);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setEnergy(value: number): void {
|
|
17
|
+
this.mascot.setEnergy(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setFriendliness(value: number): void {
|
|
21
|
+
this.mascot.setFriendliness(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setPlayfulness(value: number): void {
|
|
25
|
+
this.mascot.setPlayfulness(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cheerUp(): void {
|
|
29
|
+
this.mascot.cheerUp();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
boostEnergy(amount: number): void {
|
|
33
|
+
this.mascot.boostEnergy(amount);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get personality() {
|
|
37
|
+
return this.mascot.personality;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State History (60 lines)
|
|
3
|
+
* Efficient history tracking with circular buffer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotAnimationState } from '../../domain/types/AnimationStateTypes';
|
|
7
|
+
|
|
8
|
+
interface HistoryEntry {
|
|
9
|
+
state: MascotAnimationState;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
triggeredBy: 'user' | 'system' | 'auto-transition';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class StateHistory {
|
|
15
|
+
private readonly _buffer: HistoryEntry[];
|
|
16
|
+
private readonly _capacity: number;
|
|
17
|
+
private _size: number = 0;
|
|
18
|
+
private _head: number = 0;
|
|
19
|
+
|
|
20
|
+
constructor(capacity: number = 50) {
|
|
21
|
+
this._capacity = capacity;
|
|
22
|
+
this._buffer = new Array(capacity);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
add(state: MascotAnimationState, triggeredBy: HistoryEntry['triggeredBy']): void {
|
|
26
|
+
this._buffer[this._head] = { state, timestamp: Date.now(), triggeredBy };
|
|
27
|
+
this._head = (this._head + 1) % this._capacity;
|
|
28
|
+
|
|
29
|
+
if (this._size < this._capacity) {
|
|
30
|
+
this._size++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getStates(): MascotAnimationState[] {
|
|
35
|
+
const result: MascotAnimationState[] = [];
|
|
36
|
+
let index = this._head - this._size;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < this._size; i++) {
|
|
39
|
+
if (index < 0) index += this._capacity;
|
|
40
|
+
result.push(this._buffer[index].state);
|
|
41
|
+
index++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clear(): void {
|
|
48
|
+
this._size = 0;
|
|
49
|
+
this._head = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get size(): number {
|
|
53
|
+
return this._size;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Machine (120 lines)
|
|
3
|
+
* Core state transition logic with validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotAnimationState } from '../../domain/types/AnimationStateTypes';
|
|
7
|
+
import { AnimationState } from '../../domain/value-objects/AnimationState';
|
|
8
|
+
import { StateTransitions } from './StateTransitions';
|
|
9
|
+
import { StateHistory } from './StateHistory';
|
|
10
|
+
|
|
11
|
+
export interface StateMachineConfig {
|
|
12
|
+
enableAutoTransition?: boolean;
|
|
13
|
+
enableHistoryTracking?: boolean;
|
|
14
|
+
maxHistorySize?: number;
|
|
15
|
+
onStateChange?: (from: MascotAnimationState, to: MascotAnimationState) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class StateMachine {
|
|
19
|
+
private _currentState: AnimationState;
|
|
20
|
+
private _previousState: AnimationState | null = null;
|
|
21
|
+
private readonly _transitions: StateTransitions;
|
|
22
|
+
private readonly _history: StateHistory;
|
|
23
|
+
private readonly _config: Required<StateMachineConfig>;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
initialState: MascotAnimationState = 'idle',
|
|
27
|
+
config: StateMachineConfig = {}
|
|
28
|
+
) {
|
|
29
|
+
this._currentState = AnimationState.create(initialState);
|
|
30
|
+
this._transitions = new StateTransitions();
|
|
31
|
+
this._history = new StateHistory(config.maxHistorySize || 50);
|
|
32
|
+
this._config = {
|
|
33
|
+
enableAutoTransition: config.enableAutoTransition ?? true,
|
|
34
|
+
enableHistoryTracking: config.enableHistoryTracking ?? true,
|
|
35
|
+
maxHistorySize: config.maxHistorySize ?? 50,
|
|
36
|
+
onStateChange: config.onStateChange ?? (() => {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get currentState(): AnimationState {
|
|
41
|
+
return this._currentState;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get currentStateValue(): MascotAnimationState {
|
|
45
|
+
return this._currentState.value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get previousState(): AnimationState | null {
|
|
49
|
+
return this._previousState;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get history(): MascotAnimationState[] {
|
|
53
|
+
return this._history.getStates();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
transitionTo(
|
|
57
|
+
newState: MascotAnimationState,
|
|
58
|
+
triggeredBy: 'user' | 'system' | 'auto-transition' = 'user'
|
|
59
|
+
): void {
|
|
60
|
+
// Validate transition
|
|
61
|
+
if (!this._transitions.canTransitionTo(this.currentStateValue, newState)) {
|
|
62
|
+
const validTransitions = this._transitions
|
|
63
|
+
.getAvailableTransitions(this.currentStateValue)
|
|
64
|
+
.map((t) => t.to);
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Cannot transition from '${this.currentStateValue}' to '${newState}'. ` +
|
|
67
|
+
`Valid transitions: ${validTransitions.join(', ')}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Store previous state
|
|
72
|
+
this._previousState = this._currentState;
|
|
73
|
+
|
|
74
|
+
// Track history
|
|
75
|
+
if (this._config.enableHistoryTracking) {
|
|
76
|
+
this._history.add(this.currentStateValue, triggeredBy);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Update state
|
|
80
|
+
const oldState = this.currentStateValue;
|
|
81
|
+
this._currentState = AnimationState.create(newState);
|
|
82
|
+
|
|
83
|
+
// Notify
|
|
84
|
+
this._config.onStateChange(oldState, newState);
|
|
85
|
+
|
|
86
|
+
// Schedule auto-transition
|
|
87
|
+
if (this._config.enableAutoTransition && !this._currentState.shouldLoop()) {
|
|
88
|
+
const next = this._currentState.getNextState();
|
|
89
|
+
if (next) {
|
|
90
|
+
this._scheduleAutoTransition(next);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Shortcut methods
|
|
96
|
+
triggerSuccess(): void {
|
|
97
|
+
this.transitionTo('success', 'system');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
triggerError(): void {
|
|
101
|
+
this.transitionTo('error', 'system');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
startLoading(): void {
|
|
105
|
+
this.transitionTo('loading', 'system');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
stopLoading(success: boolean): void {
|
|
109
|
+
if (this.currentStateValue === 'loading') {
|
|
110
|
+
this.transitionTo(success ? 'success' : 'error', 'system');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
reset(): void {
|
|
115
|
+
this.transitionTo('idle', 'system');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// State queries
|
|
119
|
+
isLooping(): boolean {
|
|
120
|
+
return this._currentState.shouldLoop();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isIdle(): boolean {
|
|
124
|
+
return this.currentStateValue === 'idle';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
isLoading(): boolean {
|
|
128
|
+
return this.currentStateValue === 'loading';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Auto-transition scheduling
|
|
132
|
+
private _autoTransitionTimer: NodeJS.Timeout | null = null;
|
|
133
|
+
|
|
134
|
+
private _scheduleAutoTransition(targetState: MascotAnimationState): void {
|
|
135
|
+
const duration = this._currentState.getDuration();
|
|
136
|
+
const delay = this._currentState.getTransitionDelay(targetState);
|
|
137
|
+
|
|
138
|
+
this._autoTransitionTimer = setTimeout(() => {
|
|
139
|
+
try {
|
|
140
|
+
this.transitionTo(targetState, 'auto-transition');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn('Auto-transition failed:', error);
|
|
143
|
+
}
|
|
144
|
+
}, duration + delay);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
destroy(): void {
|
|
148
|
+
if (this._autoTransitionTimer) {
|
|
149
|
+
clearTimeout(this._autoTransitionTimer);
|
|
150
|
+
}
|
|
151
|
+
this._history.clear();
|
|
152
|
+
this._config.onStateChange = () => {};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Transitions (80 lines)
|
|
3
|
+
* Transition rules and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotAnimationState } from '../../domain/types/AnimationStateTypes';
|
|
7
|
+
|
|
8
|
+
export interface StateTransition {
|
|
9
|
+
from: MascotAnimationState;
|
|
10
|
+
to: MascotAnimationState;
|
|
11
|
+
condition?: 'always' | 'on-success' | 'on-error' | 'on-complete';
|
|
12
|
+
delay?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class StateTransitions {
|
|
16
|
+
private readonly _transitions: Map<MascotAnimationState, StateTransition[]>;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this._transitions = new Map();
|
|
20
|
+
this._initializeTransitions();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
canTransitionTo(from: MascotAnimationState, to: MascotAnimationState): boolean {
|
|
24
|
+
const transitions = this._transitions.get(from);
|
|
25
|
+
if (!transitions) return false;
|
|
26
|
+
return transitions.some((t) => t.to === to);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getAvailableTransitions(state: MascotAnimationState): StateTransition[] {
|
|
30
|
+
return this._transitions.get(state) || [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getTransitionDelay(from: MascotAnimationState, to: MascotAnimationState): number {
|
|
34
|
+
const transitions = this._transitions.get(from);
|
|
35
|
+
const transition = transitions?.find((t) => t.to === to);
|
|
36
|
+
return transition?.delay || 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private _initializeTransitions(): void {
|
|
40
|
+
// Idle transitions
|
|
41
|
+
this._addTransition('idle', 'loading', 'always');
|
|
42
|
+
this._addTransition('idle', 'empty', 'always');
|
|
43
|
+
this._addTransition('idle', 'guide', 'always');
|
|
44
|
+
|
|
45
|
+
// Loading transitions
|
|
46
|
+
this._addTransition('loading', 'success', 'on-success');
|
|
47
|
+
this._addTransition('loading', 'error', 'on-error');
|
|
48
|
+
|
|
49
|
+
// Success transitions
|
|
50
|
+
this._addTransition('success', 'idle', 'on-complete', 100);
|
|
51
|
+
|
|
52
|
+
// Error transitions
|
|
53
|
+
this._addTransition('error', 'idle', 'on-complete', 100);
|
|
54
|
+
|
|
55
|
+
// Empty transitions
|
|
56
|
+
this._addTransition('empty', 'idle', 'always');
|
|
57
|
+
|
|
58
|
+
// Guide transitions
|
|
59
|
+
this._addTransition('guide', 'idle', 'on-complete', 200);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _addTransition(
|
|
63
|
+
from: MascotAnimationState,
|
|
64
|
+
to: MascotAnimationState,
|
|
65
|
+
condition: StateTransition['condition'] = 'always',
|
|
66
|
+
delay: number = 0
|
|
67
|
+
): void {
|
|
68
|
+
if (!this._transitions.has(from)) {
|
|
69
|
+
this._transitions.set(from, []);
|
|
70
|
+
}
|
|
71
|
+
this._transitions.get(from)!.push({ from, to, condition, delay });
|
|
72
|
+
}
|
|
73
|
+
}
|