@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.
Files changed (43) hide show
  1. package/README.md +60 -0
  2. package/package.json +2 -1
  3. package/src/application/dto/MascotDTO.ts +64 -0
  4. package/src/application/errors/MascotErrors.ts +76 -0
  5. package/src/application/services/AnimationStateManager.ts +69 -0
  6. package/src/application/services/AppearanceManagement.ts +40 -0
  7. package/src/application/services/MascotService.ts +203 -0
  8. package/src/application/services/PersonalityManagement.ts +39 -0
  9. package/src/application/services/StateHistory.ts +55 -0
  10. package/src/application/services/StateMachine.ts +154 -0
  11. package/src/application/services/StateTransitions.ts +73 -0
  12. package/src/application.ts +40 -0
  13. package/src/assets/index.ts +14 -19
  14. package/src/core.ts +62 -0
  15. package/src/domain/entities/Mascot.ts +197 -99
  16. package/src/domain/types/AnimationStateTypes.ts +148 -0
  17. package/src/domain/types/MascotTypes.ts +9 -0
  18. package/src/domain/value-objects/AnimationState.ts +126 -0
  19. package/src/domain/value-objects/EnergyLevel.ts +80 -0
  20. package/src/domain/value-objects/FriendlinessLevel.ts +66 -0
  21. package/src/domain/value-objects/Mood.ts +59 -0
  22. package/src/domain/value-objects/PlayfulnessLevel.ts +66 -0
  23. package/src/index.ts +16 -68
  24. package/src/infrastructure/controllers/AnimationController.ts +26 -122
  25. package/src/infrastructure/controllers/AnimationPlayer.ts +104 -0
  26. package/src/infrastructure/controllers/AnimationTimer.ts +62 -0
  27. package/src/infrastructure/controllers/EventManager.ts +108 -0
  28. package/src/infrastructure/di/Container.ts +153 -0
  29. package/src/infrastructure/managers/AssetManager.ts +134 -63
  30. package/src/infrastructure/managers/MascotBuilder.ts +89 -0
  31. package/src/infrastructure/managers/MascotFactory.ts +24 -176
  32. package/src/infrastructure/managers/MascotTemplates.ts +151 -0
  33. package/src/infrastructure/utils/LRUCache.ts +218 -0
  34. package/src/infrastructure.ts +24 -0
  35. package/src/presentation/components/LottieMascot.tsx +85 -0
  36. package/src/presentation/components/MascotView.tsx +42 -233
  37. package/src/presentation/components/SVGMascot.tsx +61 -0
  38. package/src/presentation/contexts/MascotContext.tsx +28 -111
  39. package/src/presentation/hooks/useMascot.ts +153 -169
  40. package/src/presentation/hooks/useMascotAnimation.ts +48 -94
  41. package/src/presentation/hooks/useMascotState.ts +213 -0
  42. package/src/presentation.ts +37 -0
  43. 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",
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
+ }