@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
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,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Transfer Objects
|
|
3
|
+
* Used for data transfer between layers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotConfig, MascotState, MascotAppearance, MascotPersonality } from '../../domain/types/MascotTypes';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mascot DTO - simplified mascot data for presentation
|
|
10
|
+
*/
|
|
11
|
+
export interface MascotDTO {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
personality: MascotPersonality;
|
|
16
|
+
appearance: MascotAppearance;
|
|
17
|
+
state: MascotState;
|
|
18
|
+
interactive: boolean;
|
|
19
|
+
touchEnabled: boolean;
|
|
20
|
+
soundEnabled: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Animation State DTO
|
|
25
|
+
*/
|
|
26
|
+
export interface AnimationStateDTO {
|
|
27
|
+
isPlaying: boolean;
|
|
28
|
+
currentAnimation: string | null;
|
|
29
|
+
progress: number;
|
|
30
|
+
speed: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mascot Initialization Options DTO
|
|
35
|
+
*/
|
|
36
|
+
export interface MascotInitOptionsDTO {
|
|
37
|
+
config?: MascotConfig;
|
|
38
|
+
template?: string;
|
|
39
|
+
autoInitialize?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Animation Playback Options DTO
|
|
44
|
+
*/
|
|
45
|
+
export interface AnimationPlaybackOptionsDTO {
|
|
46
|
+
speed?: number;
|
|
47
|
+
loop?: boolean;
|
|
48
|
+
autoplay?: boolean;
|
|
49
|
+
onStart?: () => void;
|
|
50
|
+
onFinish?: () => void;
|
|
51
|
+
onError?: (error: Error) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mascot Update Options DTO
|
|
56
|
+
*/
|
|
57
|
+
export interface MascotUpdateOptionsDTO {
|
|
58
|
+
mood?: string;
|
|
59
|
+
energy?: number;
|
|
60
|
+
friendliness?: number;
|
|
61
|
+
playfulness?: number;
|
|
62
|
+
baseColor?: string;
|
|
63
|
+
accentColor?: string;
|
|
64
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Errors
|
|
3
|
+
* Custom error classes for mascot domain
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class MascotError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'MascotError';
|
|
10
|
+
Object.setPrototypeOf(this, MascotError.prototype);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class MascotNotInitializedError extends MascotError {
|
|
15
|
+
constructor() {
|
|
16
|
+
super('Mascot has not been initialized. Call initialize() or fromTemplate() first.');
|
|
17
|
+
this.name = 'MascotNotInitializedError';
|
|
18
|
+
Object.setPrototypeOf(this, MascotNotInitializedError.prototype);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class AnimationNotFoundError extends MascotError {
|
|
23
|
+
constructor(animationId: string) {
|
|
24
|
+
super(`Animation with id "${animationId}" not found.`);
|
|
25
|
+
this.name = 'AnimationNotFoundError';
|
|
26
|
+
Object.setPrototypeOf(this, AnimationNotFoundError.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class InvalidEnergyLevelError extends MascotError {
|
|
31
|
+
constructor(value: number) {
|
|
32
|
+
super(`Energy level must be between 0 and 1, got: ${value}`);
|
|
33
|
+
this.name = 'InvalidEnergyLevelError';
|
|
34
|
+
Object.setPrototypeOf(this, InvalidEnergyLevelError.prototype);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class InvalidFriendlinessLevelError extends MascotError {
|
|
39
|
+
constructor(value: number) {
|
|
40
|
+
super(`Friendliness level must be between 0 and 1, got: ${value}`);
|
|
41
|
+
this.name = 'InvalidFriendlinessLevelError';
|
|
42
|
+
Object.setPrototypeOf(this, InvalidFriendlinessLevelError.prototype);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class InvalidPlayfulnessLevelError extends MascotError {
|
|
47
|
+
constructor(value: number) {
|
|
48
|
+
super(`Playfulness level must be between 0 and 1, got: ${value}`);
|
|
49
|
+
this.name = 'InvalidPlayfulnessLevelError';
|
|
50
|
+
Object.setPrototypeOf(this, InvalidPlayfulnessLevelError.prototype);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class InvalidMoodTransitionError extends MascotError {
|
|
55
|
+
constructor(from: string, to: string) {
|
|
56
|
+
super(`Cannot transition mood from "${from}" to "${to}".`);
|
|
57
|
+
this.name = 'InvalidMoodTransitionError';
|
|
58
|
+
Object.setPrototypeOf(this, InvalidMoodTransitionError.prototype);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class MascotNotFoundError extends MascotError {
|
|
63
|
+
constructor(id: string) {
|
|
64
|
+
super(`Mascot with id "${id}" not found.`);
|
|
65
|
+
this.name = 'MascotNotFoundError';
|
|
66
|
+
Object.setPrototypeOf(this, MascotNotFoundError.prototype);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class TemplateNotFoundError extends MascotError {
|
|
71
|
+
constructor(template: string) {
|
|
72
|
+
super(`Mascot template "${template}" not found.`);
|
|
73
|
+
this.name = 'TemplateNotFoundError';
|
|
74
|
+
Object.setPrototypeOf(this, TemplateNotFoundError.prototype);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mascot Service (120 lines)
|
|
3
|
+
* Core application service for mascot operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Mascot } from '../../domain/entities/Mascot';
|
|
7
|
+
import type { MascotConfig, MascotMood, MascotAppearance } from '../../domain/types/MascotTypes';
|
|
8
|
+
import type { IMascotRepository } from '../../domain/interfaces/IMascotRepository';
|
|
9
|
+
import type { IAnimationController, AnimationOptions } from '../../domain/interfaces/IAnimationController';
|
|
10
|
+
import type { IAssetManager } from '../../domain/interfaces/IAssetManager';
|
|
11
|
+
import { MascotFactory } from '../../infrastructure/managers/MascotFactory';
|
|
12
|
+
import { MascotNotInitializedError, AnimationNotFoundError } from '../errors/MascotErrors';
|
|
13
|
+
import { PersonalityManagement } from './PersonalityManagement';
|
|
14
|
+
import { AppearanceManagement } from './AppearanceManagement';
|
|
15
|
+
|
|
16
|
+
export type MascotTemplate = 'friendly-bot' | 'cute-pet' | 'wise-owl' | 'pixel-hero';
|
|
17
|
+
|
|
18
|
+
export class MascotService {
|
|
19
|
+
private _mascot: Mascot | null = null;
|
|
20
|
+
private _changeListeners: Set<() => void> = new Set();
|
|
21
|
+
private _personality: PersonalityManagement | null = null;
|
|
22
|
+
private _appearance: AppearanceManagement | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly _repository: IMascotRepository,
|
|
26
|
+
private readonly _animationController: IAnimationController,
|
|
27
|
+
_assetManager: IAssetManager
|
|
28
|
+
) {
|
|
29
|
+
void _assetManager;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ✅ Initialization
|
|
33
|
+
async initialize(config: MascotConfig): Promise<void> {
|
|
34
|
+
this._mascot = new Mascot(config);
|
|
35
|
+
await this._repository.save(config);
|
|
36
|
+
this._notifyChange();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async fromTemplate(
|
|
40
|
+
template: MascotTemplate,
|
|
41
|
+
customizations?: Partial<MascotConfig>
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const mascot = MascotFactory.createFromTemplate(template, customizations);
|
|
44
|
+
this._mascot = mascot;
|
|
45
|
+
await this._repository.save(mascot.config);
|
|
46
|
+
this._notifyChange();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ✅ State Getters
|
|
50
|
+
get mascot(): Mascot | null {
|
|
51
|
+
return this._mascot;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get isPlaying(): boolean {
|
|
55
|
+
return this._animationController.isPlaying();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get currentAnimation(): string | null {
|
|
59
|
+
return this._mascot?.state.currentAnimation ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get isReady(): boolean {
|
|
63
|
+
return this._mascot !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
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)
|
|
84
|
+
setMood(mood: MascotMood): void {
|
|
85
|
+
this._getPersonality().setMood(mood);
|
|
86
|
+
this._notifyChange();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setEnergy(value: number): void {
|
|
90
|
+
this._getPersonality().setEnergy(value);
|
|
91
|
+
this._notifyChange();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setFriendliness(value: number): void {
|
|
95
|
+
this._getPersonality().setFriendliness(value);
|
|
96
|
+
this._notifyChange();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setPlayfulness(value: number): void {
|
|
100
|
+
this._getPersonality().setPlayfulness(value);
|
|
101
|
+
this._notifyChange();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
cheerUp(): void {
|
|
105
|
+
this._getPersonality().cheerUp();
|
|
106
|
+
this._notifyChange();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
boostEnergy(amount: number): void {
|
|
110
|
+
this._getPersonality().boostEnergy(amount);
|
|
111
|
+
this._notifyChange();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ✅ Appearance Management (delegated)
|
|
115
|
+
updateAppearance(appearance: Partial<MascotAppearance>): void {
|
|
116
|
+
this._getAppearance().updateAppearance(appearance);
|
|
117
|
+
this._notifyChange();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setBaseColor(color: string): void {
|
|
121
|
+
this._getAppearance().setBaseColor(color);
|
|
122
|
+
this._notifyChange();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setAccentColor(color: string): void {
|
|
126
|
+
this._getAppearance().setAccentColor(color);
|
|
127
|
+
this._notifyChange();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addAccessory(accessory: {
|
|
131
|
+
id: string;
|
|
132
|
+
type: string;
|
|
133
|
+
color?: string;
|
|
134
|
+
position?: { x: number; y: number };
|
|
135
|
+
}): void {
|
|
136
|
+
this._getAppearance().addAccessory(accessory);
|
|
137
|
+
this._notifyChange();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
removeAccessory(accessoryId: string): void {
|
|
141
|
+
this._getAppearance().removeAccessory(accessoryId);
|
|
142
|
+
this._notifyChange();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ✅ Animation Management
|
|
146
|
+
async playAnimation(animationId: string, options?: AnimationOptions): Promise<void> {
|
|
147
|
+
this._ensureMascot();
|
|
148
|
+
const animation = this._mascot!.getAnimation(animationId);
|
|
149
|
+
if (!animation) {
|
|
150
|
+
throw new AnimationNotFoundError(animationId);
|
|
151
|
+
}
|
|
152
|
+
this._mascot!.startAnimation(animationId);
|
|
153
|
+
await this._animationController.play(animation, options);
|
|
154
|
+
this._notifyChange();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
stopAnimation(): void {
|
|
158
|
+
this._animationController.stop();
|
|
159
|
+
if (this._mascot) {
|
|
160
|
+
this._mascot.stopAnimation();
|
|
161
|
+
}
|
|
162
|
+
this._notifyChange();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pauseAnimation(): void {
|
|
166
|
+
this._animationController.pause();
|
|
167
|
+
this._notifyChange();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resumeAnimation(): void {
|
|
171
|
+
this._animationController.resume();
|
|
172
|
+
this._notifyChange();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ✅ Visibility & Position
|
|
176
|
+
setVisible(visible: boolean): void {
|
|
177
|
+
this._ensureMascot();
|
|
178
|
+
this._mascot!.setVisible(visible);
|
|
179
|
+
this._notifyChange();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setPosition(position: { x: number; y: number }): void {
|
|
183
|
+
this._ensureMascot();
|
|
184
|
+
this._mascot!.setPosition(position);
|
|
185
|
+
this._notifyChange();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ✅ Observable Pattern
|
|
189
|
+
subscribe(listener: () => void): () => void {
|
|
190
|
+
this._changeListeners.add(listener);
|
|
191
|
+
return () => this._changeListeners.delete(listener);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private _notifyChange(): void {
|
|
195
|
+
this._changeListeners.forEach((listener) => listener());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private _ensureMascot(): void {
|
|
199
|
+
if (!this._mascot) {
|
|
200
|
+
throw new MascotNotInitializedError();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -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
|
+
}
|