@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnergyLevel Value Object
|
|
3
|
+
* Encapsulates energy level validation and business rules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class EnergyLevel {
|
|
7
|
+
private readonly MIN = 0;
|
|
8
|
+
private readonly MAX = 1;
|
|
9
|
+
private readonly HIGH_THRESHOLD = 0.7;
|
|
10
|
+
private readonly LOW_THRESHOLD = 0.3;
|
|
11
|
+
|
|
12
|
+
private constructor(public readonly value: number) {
|
|
13
|
+
if (value < this.MIN || value > this.MAX) {
|
|
14
|
+
throw new Error(`Energy level must be between ${this.MIN} and ${this.MAX}, got: ${value}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static create(value: number): EnergyLevel {
|
|
19
|
+
return new EnergyLevel(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if energy is high
|
|
24
|
+
*/
|
|
25
|
+
isHigh(): boolean {
|
|
26
|
+
return this.value > this.HIGH_THRESHOLD;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if energy is low
|
|
31
|
+
*/
|
|
32
|
+
isLow(): boolean {
|
|
33
|
+
return this.value < this.LOW_THRESHOLD;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if energy is moderate
|
|
38
|
+
*/
|
|
39
|
+
isModerate(): boolean {
|
|
40
|
+
return !this.isHigh() && !this.isLow();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Increase energy by amount (clamped to MAX)
|
|
45
|
+
*/
|
|
46
|
+
increase(amount: number): EnergyLevel {
|
|
47
|
+
const newValue = Math.min(this.MAX, this.value + amount);
|
|
48
|
+
return EnergyLevel.create(newValue);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decrease energy by amount (clamped to MIN)
|
|
53
|
+
*/
|
|
54
|
+
decrease(amount: number): EnergyLevel {
|
|
55
|
+
const newValue = Math.max(this.MIN, this.value - amount);
|
|
56
|
+
return EnergyLevel.create(newValue);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set energy to specific level
|
|
61
|
+
*/
|
|
62
|
+
setLevel(value: number): EnergyLevel {
|
|
63
|
+
return EnergyLevel.create(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get energy as percentage (0-100)
|
|
68
|
+
*/
|
|
69
|
+
toPercentage(): number {
|
|
70
|
+
return Math.round(this.value * 100);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
equals(other: EnergyLevel): boolean {
|
|
74
|
+
return this.value === other.value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
toJSON(): number {
|
|
78
|
+
return this.value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FriendlinessLevel Value Object
|
|
3
|
+
* Encapsulates friendliness validation and business rules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class FriendlinessLevel {
|
|
7
|
+
private readonly MIN = 0;
|
|
8
|
+
private readonly MAX = 1;
|
|
9
|
+
private readonly FRIENDLY_THRESHOLD = 0.6;
|
|
10
|
+
private readonly VERY_FRIENDLY_THRESHOLD = 0.8;
|
|
11
|
+
|
|
12
|
+
private constructor(public readonly value: number) {
|
|
13
|
+
if (value < this.MIN || value > this.MAX) {
|
|
14
|
+
throw new Error(`Friendliness level must be between ${this.MIN} and ${this.MAX}, got: ${value}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static create(value: number): FriendlinessLevel {
|
|
19
|
+
return new FriendlinessLevel(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if mascot is friendly
|
|
24
|
+
*/
|
|
25
|
+
isFriendly(): boolean {
|
|
26
|
+
return this.value >= this.FRIENDLY_THRESHOLD;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if mascot is very friendly
|
|
31
|
+
*/
|
|
32
|
+
isVeryFriendly(): boolean {
|
|
33
|
+
return this.value >= this.VERY_FRIENDLY_THRESHOLD;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if mascot is shy
|
|
38
|
+
*/
|
|
39
|
+
isShy(): boolean {
|
|
40
|
+
return this.value < 0.4;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Increase friendliness
|
|
45
|
+
*/
|
|
46
|
+
increase(amount: number): FriendlinessLevel {
|
|
47
|
+
const newValue = Math.min(this.MAX, this.value + amount);
|
|
48
|
+
return FriendlinessLevel.create(newValue);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decrease friendliness
|
|
53
|
+
*/
|
|
54
|
+
decrease(amount: number): FriendlinessLevel {
|
|
55
|
+
const newValue = Math.max(this.MIN, this.value - amount);
|
|
56
|
+
return FriendlinessLevel.create(newValue);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
equals(other: FriendlinessLevel): boolean {
|
|
60
|
+
return this.value === other.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toJSON(): number {
|
|
64
|
+
return this.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mood Value Object
|
|
3
|
+
* Encapsulates mood logic and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotMood } from '../types/MascotTypes';
|
|
7
|
+
|
|
8
|
+
export class Mood {
|
|
9
|
+
private constructor(public readonly value: MascotMood) {}
|
|
10
|
+
|
|
11
|
+
static create(value: MascotMood): Mood {
|
|
12
|
+
return new Mood(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if mood is positive
|
|
17
|
+
*/
|
|
18
|
+
isPositive(): boolean {
|
|
19
|
+
return ['happy', 'excited', 'surprised'].includes(this.value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if mood is negative
|
|
24
|
+
*/
|
|
25
|
+
isNegative(): boolean {
|
|
26
|
+
return ['sad', 'angry'].includes(this.value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if mood is neutral
|
|
31
|
+
*/
|
|
32
|
+
isNeutral(): boolean {
|
|
33
|
+
return ['neutral', 'thinking'].includes(this.value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get opposite mood
|
|
38
|
+
*/
|
|
39
|
+
getOpposite(): Mood {
|
|
40
|
+
const opposites: Record<MascotMood, MascotMood> = {
|
|
41
|
+
happy: 'sad',
|
|
42
|
+
sad: 'happy',
|
|
43
|
+
excited: 'angry',
|
|
44
|
+
angry: 'excited',
|
|
45
|
+
thinking: 'neutral',
|
|
46
|
+
neutral: 'thinking',
|
|
47
|
+
surprised: 'neutral',
|
|
48
|
+
};
|
|
49
|
+
return Mood.create(opposites[this.value]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
equals(other: Mood): boolean {
|
|
53
|
+
return this.value === other.value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
toJSON(): MascotMood {
|
|
57
|
+
return this.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlayfulnessLevel Value Object
|
|
3
|
+
* Encapsulates playfulness validation and business rules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class PlayfulnessLevel {
|
|
7
|
+
private readonly MIN = 0;
|
|
8
|
+
private readonly MAX = 1;
|
|
9
|
+
private readonly PLAYFUL_THRESHOLD = 0.5;
|
|
10
|
+
private readonly VERY_PLAYFUL_THRESHOLD = 0.8;
|
|
11
|
+
|
|
12
|
+
private constructor(public readonly value: number) {
|
|
13
|
+
if (value < this.MIN || value > this.MAX) {
|
|
14
|
+
throw new Error(`Playfulness level must be between ${this.MIN} and ${this.MAX}, got: ${value}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static create(value: number): PlayfulnessLevel {
|
|
19
|
+
return new PlayfulnessLevel(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if mascot is playful
|
|
24
|
+
*/
|
|
25
|
+
isPlayful(): boolean {
|
|
26
|
+
return this.value >= this.PLAYFUL_THRESHOLD;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if mascot is very playful
|
|
31
|
+
*/
|
|
32
|
+
isVeryPlayful(): boolean {
|
|
33
|
+
return this.value >= this.VERY_PLAYFUL_THRESHOLD;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if mascot is serious
|
|
38
|
+
*/
|
|
39
|
+
isSerious(): boolean {
|
|
40
|
+
return this.value < 0.3;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Increase playfulness
|
|
45
|
+
*/
|
|
46
|
+
increase(amount: number): PlayfulnessLevel {
|
|
47
|
+
const newValue = Math.min(this.MAX, this.value + amount);
|
|
48
|
+
return PlayfulnessLevel.create(newValue);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decrease playfulness
|
|
53
|
+
*/
|
|
54
|
+
decrease(amount: number): PlayfulnessLevel {
|
|
55
|
+
const newValue = Math.max(this.MIN, this.value - amount);
|
|
56
|
+
return PlayfulnessLevel.create(newValue);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
equals(other: PlayfulnessLevel): boolean {
|
|
60
|
+
return this.value === other.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toJSON(): number {
|
|
64
|
+
return this.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,78 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @umituz/react-native-mascot
|
|
3
3
|
*
|
|
4
|
-
* Interactive mascot system for React Native apps
|
|
4
|
+
* Interactive mascot system for React Native apps with DDD architecture
|
|
5
|
+
* Version: 1.0.6
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
// Domain - Types
|
|
13
|
-
export type {
|
|
14
|
-
MascotType,
|
|
15
|
-
MascotStyle,
|
|
16
|
-
MascotAnimationType,
|
|
17
|
-
MascotAppearance,
|
|
18
|
-
MascotAccessory,
|
|
19
|
-
MascotPersonality,
|
|
20
|
-
MascotAnimation,
|
|
21
|
-
MascotConfig,
|
|
22
|
-
MascotState,
|
|
23
|
-
MascotInteraction,
|
|
24
|
-
MascotMood,
|
|
25
|
-
AnimationSpeed,
|
|
26
|
-
} from './domain/types/MascotTypes';
|
|
27
|
-
|
|
28
|
-
// Domain - Interfaces
|
|
29
|
-
export type {
|
|
30
|
-
IAnimationController,
|
|
31
|
-
AnimationEvent,
|
|
32
|
-
AnimationOptions,
|
|
33
|
-
} from './domain/interfaces/IAnimationController';
|
|
34
|
-
|
|
35
|
-
export type {
|
|
36
|
-
IAssetManager,
|
|
37
|
-
AssetCache,
|
|
38
|
-
} from './domain/interfaces/IAssetManager';
|
|
39
|
-
|
|
40
|
-
export type {
|
|
41
|
-
IMascotRepository,
|
|
42
|
-
} from './domain/interfaces/IMascotRepository';
|
|
43
|
-
|
|
44
|
-
// Infrastructure - Repositories
|
|
45
|
-
export { MascotRepository } from './infrastructure/repositories/MascotRepository';
|
|
46
|
-
|
|
47
|
-
// Infrastructure - Controllers
|
|
48
|
-
export { AnimationController } from './infrastructure/controllers/AnimationController';
|
|
49
|
-
|
|
50
|
-
// Infrastructure - Managers
|
|
51
|
-
export { AssetManager } from './infrastructure/managers/AssetManager';
|
|
52
|
-
export { MascotFactory, type MascotTemplate } from './infrastructure/managers/MascotFactory';
|
|
53
|
-
|
|
54
|
-
// Presentation - Components
|
|
55
|
-
export { MascotView } from './presentation/components/MascotView';
|
|
56
|
-
export type { MascotViewProps } from './presentation/components/MascotView';
|
|
57
|
-
|
|
58
|
-
// Presentation - Hooks
|
|
59
|
-
export { useMascot } from './presentation/hooks/useMascot';
|
|
60
|
-
export type {
|
|
61
|
-
UseMascotOptions,
|
|
62
|
-
UseMascotReturn,
|
|
63
|
-
} from './presentation/hooks/useMascot';
|
|
64
|
-
|
|
65
|
-
export { useMascotAnimation } from './presentation/hooks/useMascotAnimation';
|
|
66
|
-
export type {
|
|
67
|
-
UseMascotAnimationOptions,
|
|
68
|
-
UseMascotAnimationReturn,
|
|
69
|
-
} from './presentation/hooks/useMascotAnimation';
|
|
70
|
-
|
|
71
|
-
// Presentation - Contexts
|
|
72
|
-
export { MascotProvider, useMascotContext } from './presentation/contexts/MascotContext';
|
|
73
|
-
export type { MascotProviderProps, MascotContextValue } from './presentation/contexts/MascotContext';
|
|
8
|
+
// Re-export all layers
|
|
9
|
+
export * from './core';
|
|
10
|
+
export * from './application';
|
|
11
|
+
export * from './infrastructure';
|
|
12
|
+
export * from './presentation';
|
|
74
13
|
|
|
75
14
|
// Constants
|
|
15
|
+
import type { MascotMood, AnimationSpeed } from './domain/types/MascotTypes';
|
|
16
|
+
|
|
76
17
|
export const MASCOT_TEMPLATES = [
|
|
77
18
|
'friendly-bot',
|
|
78
19
|
'cute-pet',
|
|
@@ -97,3 +38,10 @@ export const DEFAULT_ANIMATION_SPEEDS: AnimationSpeed[] = [
|
|
|
97
38
|
'fast',
|
|
98
39
|
'very-fast',
|
|
99
40
|
];
|
|
41
|
+
|
|
42
|
+
// Convenience export - get service instance
|
|
43
|
+
export const getMascotService = () => {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
45
|
+
const { DIContainer } = require('./infrastructure/di/Container');
|
|
46
|
+
return DIContainer.getInstance().getMascotService();
|
|
47
|
+
};
|
|
@@ -1,163 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Animation Controller
|
|
3
|
-
*
|
|
2
|
+
* Animation Controller (Main - 60 lines)
|
|
3
|
+
* Unified animation control interface
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
IAnimationController,
|
|
8
|
-
AnimationOptions,
|
|
9
|
-
AnimationEvent,
|
|
10
|
-
} from '../../domain/interfaces/IAnimationController';
|
|
6
|
+
import type { IAnimationController, AnimationOptions, AnimationEvent } from '../../domain/interfaces/IAnimationController';
|
|
11
7
|
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
8
|
+
import { AnimationPlayer } from './AnimationPlayer';
|
|
9
|
+
import { EventManager } from './EventManager';
|
|
12
10
|
|
|
13
11
|
export class AnimationController implements IAnimationController {
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private _isPaused: boolean = false;
|
|
17
|
-
private _progress: number = 0;
|
|
18
|
-
private _speed: number = 1;
|
|
19
|
-
private _eventListeners: Map<AnimationEvent, Set<(data?: unknown) => void>>;
|
|
12
|
+
private _player: AnimationPlayer;
|
|
13
|
+
private _events: EventManager;
|
|
20
14
|
|
|
21
15
|
constructor() {
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
16
|
+
this._player = new AnimationPlayer();
|
|
17
|
+
this._events = new EventManager();
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
play(
|
|
27
|
-
animation
|
|
28
|
-
options?: AnimationOptions
|
|
29
|
-
): Promise<void> {
|
|
30
|
-
this._currentAnimation = animation;
|
|
31
|
-
this._isPlaying = true;
|
|
32
|
-
this._isPaused = false;
|
|
33
|
-
|
|
34
|
-
if (options?.speed !== undefined) {
|
|
35
|
-
this._speed = options.speed;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
this._emit('start', { animation: animation.id });
|
|
39
|
-
|
|
40
|
-
// Simulate animation completion
|
|
41
|
-
// In real implementation, this would be controlled by LottieView
|
|
42
|
-
const duration = animation.duration || 2000;
|
|
43
|
-
const adjustedDuration = duration / this._speed;
|
|
44
|
-
|
|
45
|
-
return new Promise((resolve) => {
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
if (this._isPlaying && !this._isPaused) {
|
|
48
|
-
this._isPlaying = false;
|
|
49
|
-
this._progress = 1;
|
|
50
|
-
this._emit('finish', { animation: animation.id });
|
|
51
|
-
options?.onFinish?.();
|
|
52
|
-
}
|
|
53
|
-
resolve();
|
|
54
|
-
}, adjustedDuration);
|
|
55
|
-
|
|
56
|
-
options?.onStart?.();
|
|
57
|
-
});
|
|
20
|
+
play(animation: MascotAnimation, options?: AnimationOptions): Promise<void> {
|
|
21
|
+
return this._player.play(animation, options);
|
|
58
22
|
}
|
|
59
23
|
|
|
60
24
|
pause(): void {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this._isPaused = true;
|
|
66
|
-
this._emit('pause');
|
|
25
|
+
this._player.pause();
|
|
26
|
+
this._events.emit('pause');
|
|
67
27
|
}
|
|
68
28
|
|
|
69
29
|
resume(): void {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this._isPaused = false;
|
|
75
|
-
this._emit('resume');
|
|
30
|
+
this._player.resume();
|
|
31
|
+
this._events.emit('resume');
|
|
76
32
|
}
|
|
77
33
|
|
|
78
34
|
stop(): void {
|
|
79
|
-
this.
|
|
80
|
-
this._isPaused = false;
|
|
81
|
-
this._progress = 0;
|
|
82
|
-
this._currentAnimation = null;
|
|
35
|
+
this._player.stop();
|
|
83
36
|
}
|
|
84
37
|
|
|
85
38
|
getProgress(): number {
|
|
86
|
-
return this.
|
|
39
|
+
return this._player.progress;
|
|
87
40
|
}
|
|
88
41
|
|
|
89
42
|
setProgress(progress: number): void {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
this._progress = progress;
|
|
94
|
-
this._emit('progress', { progress });
|
|
43
|
+
this._player.setProgress(progress);
|
|
44
|
+
this._events.emit('progress', { progress });
|
|
95
45
|
}
|
|
96
46
|
|
|
97
47
|
setSpeed(speed: number): void {
|
|
98
|
-
|
|
99
|
-
throw new Error('Speed must be greater than 0');
|
|
100
|
-
}
|
|
101
|
-
this._speed = speed;
|
|
48
|
+
this._player.setSpeed(speed);
|
|
102
49
|
}
|
|
103
50
|
|
|
104
51
|
isPlaying(): boolean {
|
|
105
|
-
return this.
|
|
52
|
+
return this._player.isPlaying;
|
|
106
53
|
}
|
|
107
54
|
|
|
108
55
|
on(event: AnimationEvent, callback: (data?: unknown) => void): () => void {
|
|
109
|
-
|
|
110
|
-
this._eventListeners.set(event, new Set());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
this._eventListeners.get(event)!.add(callback);
|
|
114
|
-
|
|
115
|
-
// Return unsubscribe function
|
|
116
|
-
return () => {
|
|
117
|
-
this.off(event, callback);
|
|
118
|
-
};
|
|
56
|
+
return this._events.on(event, callback);
|
|
119
57
|
}
|
|
120
58
|
|
|
121
59
|
off(event: AnimationEvent, callback: (data?: unknown) => void): void {
|
|
122
|
-
|
|
123
|
-
if (listeners) {
|
|
124
|
-
listeners.delete(callback);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Private Methods
|
|
129
|
-
private _initializeEventListeners(): void {
|
|
130
|
-
const events: AnimationEvent[] = ['start', 'finish', 'pause', 'resume', 'progress', 'error'];
|
|
131
|
-
events.forEach((event) => {
|
|
132
|
-
if (!this._eventListeners.has(event)) {
|
|
133
|
-
this._eventListeners.set(event, new Set());
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private _emit(event: AnimationEvent, data?: unknown): void {
|
|
139
|
-
const listeners = this._eventListeners.get(event);
|
|
140
|
-
if (listeners) {
|
|
141
|
-
listeners.forEach((callback) => {
|
|
142
|
-
try {
|
|
143
|
-
callback(data);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.error(`Error in ${event} event listener:`, error);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Getters
|
|
152
|
-
get currentAnimation(): MascotAnimation | null {
|
|
153
|
-
return this._currentAnimation;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
get speed(): number {
|
|
157
|
-
return this._speed;
|
|
60
|
+
this._events.off(event, callback);
|
|
158
61
|
}
|
|
159
62
|
|
|
160
|
-
|
|
161
|
-
|
|
63
|
+
destroy(): void {
|
|
64
|
+
this._player.destroy();
|
|
65
|
+
this._events.clear();
|
|
162
66
|
}
|
|
163
67
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Player (100 lines)
|
|
3
|
+
* Core playback logic for mascot animations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MascotAnimation } from '../../domain/types/MascotTypes';
|
|
7
|
+
import type { AnimationOptions } from '../../domain/interfaces/IAnimationController';
|
|
8
|
+
import { AnimationTimer } from './AnimationTimer';
|
|
9
|
+
|
|
10
|
+
export class AnimationPlayer {
|
|
11
|
+
private _currentAnimation: MascotAnimation | null = null;
|
|
12
|
+
private _isPlaying: boolean = false;
|
|
13
|
+
private _isPaused: boolean = false;
|
|
14
|
+
private _progress: number = 0;
|
|
15
|
+
private _speed: number = 1;
|
|
16
|
+
private readonly _timer: AnimationTimer;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this._timer = new AnimationTimer();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get currentAnimation(): MascotAnimation | null {
|
|
23
|
+
return this._currentAnimation;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get isPlaying(): boolean {
|
|
27
|
+
return this._isPlaying && !this._isPaused;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get progress(): number {
|
|
31
|
+
return this._progress;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get speed(): number {
|
|
35
|
+
return this._speed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
play(animation: MascotAnimation, options?: AnimationOptions): Promise<void> {
|
|
39
|
+
// Stop existing animation
|
|
40
|
+
this.stop();
|
|
41
|
+
|
|
42
|
+
this._currentAnimation = animation;
|
|
43
|
+
this._isPlaying = true;
|
|
44
|
+
this._isPaused = false;
|
|
45
|
+
|
|
46
|
+
if (options?.speed !== undefined) {
|
|
47
|
+
this._speed = options.speed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const duration = animation.duration || 2000;
|
|
51
|
+
const adjustedDuration = duration / this._speed;
|
|
52
|
+
|
|
53
|
+
// Start timer
|
|
54
|
+
this._timer.start(adjustedDuration, () => {
|
|
55
|
+
this._isPlaying = false;
|
|
56
|
+
this._progress = 1;
|
|
57
|
+
options?.onFinish?.();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
options?.onStart?.();
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pause(): void {
|
|
65
|
+
if (!this._isPlaying || this._isPaused) return;
|
|
66
|
+
|
|
67
|
+
this._isPaused = true;
|
|
68
|
+
this._timer.pause();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resume(): void {
|
|
72
|
+
if (!this._isPaused) return;
|
|
73
|
+
|
|
74
|
+
this._isPaused = false;
|
|
75
|
+
this._timer.resume();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stop(): void {
|
|
79
|
+
this._isPlaying = false;
|
|
80
|
+
this._isPaused = false;
|
|
81
|
+
this._progress = 0;
|
|
82
|
+
this._currentAnimation = null;
|
|
83
|
+
this._timer.stop();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setProgress(progress: number): void {
|
|
87
|
+
if (progress < 0 || progress > 1) {
|
|
88
|
+
throw new Error('Progress must be between 0 and 1');
|
|
89
|
+
}
|
|
90
|
+
this._progress = progress;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setSpeed(speed: number): void {
|
|
94
|
+
if (speed <= 0) {
|
|
95
|
+
throw new Error('Speed must be greater than 0');
|
|
96
|
+
}
|
|
97
|
+
this._speed = speed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
destroy(): void {
|
|
101
|
+
this.stop();
|
|
102
|
+
this._timer.destroy();
|
|
103
|
+
}
|
|
104
|
+
}
|