@umituz/react-native-mascot 1.0.2 → 1.0.4
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/package.json +1 -1
- package/src/application/dto/MascotDTO.ts +64 -0
- package/src/application/errors/MascotErrors.ts +76 -0
- package/src/application/index.ts +8 -0
- package/src/application/services/MascotService.ts +194 -0
- package/src/domain/entities/Mascot.ts +59 -20
- 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/domain/value-objects/index.ts +9 -0
- package/src/index.ts +40 -2
- package/src/infrastructure/di/Container.ts +90 -0
- package/src/presentation/contexts/MascotContext.tsx +26 -108
- package/src/presentation/hooks/useMascot.ts +77 -172
- package/src/presentation/hooks/useMascotAnimation.ts +48 -88
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.4",
|
|
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",
|
|
@@ -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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MascotService
|
|
3
|
+
* Application service that orchestrates mascot use cases
|
|
4
|
+
* This is the main entry point for all mascot operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Mascot } from '../../domain/entities/Mascot';
|
|
8
|
+
import type { MascotConfig, MascotMood, MascotAppearance, MascotAccessory } from '../../domain/types/MascotTypes';
|
|
9
|
+
import type { IMascotRepository } from '../../domain/interfaces/IMascotRepository';
|
|
10
|
+
import type { IAnimationController, AnimationOptions } from '../../domain/interfaces/IAnimationController';
|
|
11
|
+
import type { IAssetManager } from '../../domain/interfaces/IAssetManager';
|
|
12
|
+
import { MascotFactory } from '../../infrastructure/managers/MascotFactory';
|
|
13
|
+
import { MascotNotInitializedError, AnimationNotFoundError } from '../errors/MascotErrors';
|
|
14
|
+
|
|
15
|
+
export type MascotTemplate = 'friendly-bot' | 'cute-pet' | 'wise-owl' | 'pixel-hero';
|
|
16
|
+
|
|
17
|
+
export class MascotService {
|
|
18
|
+
private _mascot: Mascot | null = null;
|
|
19
|
+
private _changeListeners: Set<() => void> = new Set();
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly _repository: IMascotRepository,
|
|
23
|
+
private readonly _animationController: IAnimationController,
|
|
24
|
+
private readonly _assetManager: IAssetManager
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
// ✅ Initialization
|
|
28
|
+
async initialize(config: MascotConfig): Promise<void> {
|
|
29
|
+
this._mascot = new Mascot(config);
|
|
30
|
+
await this._repository.save(config);
|
|
31
|
+
this._notifyChange();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async fromTemplate(
|
|
35
|
+
template: MascotTemplate,
|
|
36
|
+
customizations?: Partial<MascotConfig>
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const mascot = MascotFactory.createFromTemplate(template, customizations);
|
|
39
|
+
this._mascot = mascot;
|
|
40
|
+
await this._repository.save(mascot.config);
|
|
41
|
+
this._notifyChange();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ✅ State Getters
|
|
45
|
+
get mascot(): Mascot | null {
|
|
46
|
+
return this._mascot;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get isPlaying(): boolean {
|
|
50
|
+
return this._animationController.isPlaying();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get currentAnimation(): string | null {
|
|
54
|
+
return this._mascot?.state.currentAnimation ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get isReady(): boolean {
|
|
58
|
+
return this._mascot !== null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ✅ Personality Management
|
|
62
|
+
setMood(mood: MascotMood): void {
|
|
63
|
+
this._ensureMascot();
|
|
64
|
+
this._mascot!.setMood(mood);
|
|
65
|
+
this._notifyChange();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setEnergy(value: number): void {
|
|
69
|
+
this._ensureMascot();
|
|
70
|
+
this._mascot!.setEnergy(value);
|
|
71
|
+
this._notifyChange();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setFriendliness(value: number): void {
|
|
75
|
+
this._ensureMascot();
|
|
76
|
+
this._mascot!.setFriendliness(value);
|
|
77
|
+
this._notifyChange();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setPlayfulness(value: number): void {
|
|
81
|
+
this._ensureMascot();
|
|
82
|
+
this._mascot!.setPlayfulness(value);
|
|
83
|
+
this._notifyChange();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ✅ Rich Behaviors
|
|
87
|
+
cheerUp(): void {
|
|
88
|
+
this._ensureMascot();
|
|
89
|
+
this._mascot!.cheerUp();
|
|
90
|
+
this._notifyChange();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
boostEnergy(amount: number): void {
|
|
94
|
+
this._ensureMascot();
|
|
95
|
+
this._mascot!.boostEnergy(amount);
|
|
96
|
+
this._notifyChange();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ✅ Appearance Management
|
|
100
|
+
updateAppearance(appearance: Partial<MascotAppearance>): void {
|
|
101
|
+
this._ensureMascot();
|
|
102
|
+
this._mascot!.updateAppearance(appearance);
|
|
103
|
+
this._notifyChange();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setBaseColor(color: string): void {
|
|
107
|
+
this._ensureMascot();
|
|
108
|
+
this._mascot!.setBaseColor(color);
|
|
109
|
+
this._notifyChange();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setAccentColor(color: string): void {
|
|
113
|
+
this._ensureMascot();
|
|
114
|
+
this._mascot!.setAccentColor(color);
|
|
115
|
+
this._notifyChange();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
addAccessory(accessory: {
|
|
119
|
+
id: string;
|
|
120
|
+
type: string;
|
|
121
|
+
color?: string;
|
|
122
|
+
position?: { x: number; y: number };
|
|
123
|
+
}): void {
|
|
124
|
+
this._ensureMascot();
|
|
125
|
+
this._mascot!.addAccessory(accessory);
|
|
126
|
+
this._notifyChange();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
removeAccessory(accessoryId: string): void {
|
|
130
|
+
this._ensureMascot();
|
|
131
|
+
this._mascot!.removeAccessory(accessoryId);
|
|
132
|
+
this._notifyChange();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ✅ Animation Management
|
|
136
|
+
async playAnimation(animationId: string, options?: AnimationOptions): Promise<void> {
|
|
137
|
+
this._ensureMascot();
|
|
138
|
+
const animation = this._mascot!.getAnimation(animationId);
|
|
139
|
+
if (!animation) {
|
|
140
|
+
throw new AnimationNotFoundError(animationId);
|
|
141
|
+
}
|
|
142
|
+
this._mascot!.startAnimation(animationId);
|
|
143
|
+
await this._animationController.play(animation, options);
|
|
144
|
+
this._notifyChange();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stopAnimation(): void {
|
|
148
|
+
this._animationController.stop();
|
|
149
|
+
if (this._mascot) {
|
|
150
|
+
this._mascot.stopAnimation();
|
|
151
|
+
}
|
|
152
|
+
this._notifyChange();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pauseAnimation(): void {
|
|
156
|
+
this._animationController.pause();
|
|
157
|
+
this._notifyChange();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resumeAnimation(): void {
|
|
161
|
+
this._animationController.resume();
|
|
162
|
+
this._notifyChange();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ✅ Visibility & Position
|
|
166
|
+
setVisible(visible: boolean): void {
|
|
167
|
+
this._ensureMascot();
|
|
168
|
+
this._mascot!.setVisible(visible);
|
|
169
|
+
this._notifyChange();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setPosition(position: { x: number; y: number }): void {
|
|
173
|
+
this._ensureMascot();
|
|
174
|
+
this._mascot!.setPosition(position);
|
|
175
|
+
this._notifyChange();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ✅ Observable Pattern for React Integration
|
|
179
|
+
subscribe(listener: () => void): () => void {
|
|
180
|
+
this._changeListeners.add(listener);
|
|
181
|
+
return () => this._changeListeners.delete(listener);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private _notifyChange(): void {
|
|
185
|
+
this._changeListeners.forEach((listener) => listener());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ✅ Validation
|
|
189
|
+
private _ensureMascot(): void {
|
|
190
|
+
if (!this._mascot) {
|
|
191
|
+
throw new MascotNotInitializedError();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mascot Entity
|
|
3
|
-
* Core mascot representation following DDD principles
|
|
3
|
+
* Core mascot representation following DDD principles with Value Objects
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type {
|
|
@@ -13,12 +13,16 @@ import type {
|
|
|
13
13
|
MascotMood,
|
|
14
14
|
MascotAccessory,
|
|
15
15
|
} from '../types/MascotTypes';
|
|
16
|
+
import { Mood, EnergyLevel, FriendlinessLevel, PlayfulnessLevel } from '../value-objects';
|
|
16
17
|
|
|
17
18
|
export class Mascot {
|
|
18
19
|
readonly id: string;
|
|
19
20
|
readonly name: string;
|
|
20
21
|
readonly type: MascotType;
|
|
21
|
-
private
|
|
22
|
+
private _mood: Mood;
|
|
23
|
+
private _energy: EnergyLevel;
|
|
24
|
+
private _friendliness: FriendlinessLevel;
|
|
25
|
+
private _playfulness: PlayfulnessLevel;
|
|
22
26
|
private _appearance: MascotAppearance;
|
|
23
27
|
private readonly _animations: Map<string, MascotAnimation>;
|
|
24
28
|
private readonly _config: MascotConfig;
|
|
@@ -29,7 +33,10 @@ export class Mascot {
|
|
|
29
33
|
this.id = config.id;
|
|
30
34
|
this.name = config.name;
|
|
31
35
|
this.type = config.type;
|
|
32
|
-
this.
|
|
36
|
+
this._mood = Mood.create(config.personality.mood);
|
|
37
|
+
this._energy = EnergyLevel.create(config.personality.energy);
|
|
38
|
+
this._friendliness = FriendlinessLevel.create(config.personality.friendliness);
|
|
39
|
+
this._playfulness = PlayfulnessLevel.create(config.personality.playfulness);
|
|
33
40
|
this._appearance = config.appearance;
|
|
34
41
|
this._animations = new Map(
|
|
35
42
|
config.animations.map((anim) => [anim.id, anim])
|
|
@@ -46,7 +53,12 @@ export class Mascot {
|
|
|
46
53
|
|
|
47
54
|
// Getters
|
|
48
55
|
get personality(): MascotPersonality {
|
|
49
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
mood: this._mood.value,
|
|
58
|
+
energy: this._energy.value,
|
|
59
|
+
friendliness: this._friendliness.value,
|
|
60
|
+
playfulness: this._playfulness.value,
|
|
61
|
+
};
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
get appearance(): MascotAppearance {
|
|
@@ -77,31 +89,58 @@ export class Mascot {
|
|
|
77
89
|
return this._config.soundEnabled ?? false;
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
//
|
|
92
|
+
// Value Object Getters (for domain logic)
|
|
93
|
+
get mood(): Mood {
|
|
94
|
+
return this._mood;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get energy(): EnergyLevel {
|
|
98
|
+
return this._energy;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get friendliness(): FriendlinessLevel {
|
|
102
|
+
return this._friendliness;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get playfulness(): PlayfulnessLevel {
|
|
106
|
+
return this._playfulness;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Personality Management (using Value Objects)
|
|
81
110
|
setMood(mood: MascotMood): void {
|
|
82
|
-
this.
|
|
111
|
+
this._mood = Mood.create(mood);
|
|
83
112
|
this._state.currentMood = mood;
|
|
84
113
|
}
|
|
85
114
|
|
|
86
|
-
setEnergy(
|
|
87
|
-
|
|
88
|
-
throw new Error('Energy must be between 0 and 1');
|
|
89
|
-
}
|
|
90
|
-
this._personality.energy = energy;
|
|
115
|
+
setEnergy(value: number): void {
|
|
116
|
+
this._energy = EnergyLevel.create(value);
|
|
91
117
|
}
|
|
92
118
|
|
|
93
|
-
setFriendliness(
|
|
94
|
-
|
|
95
|
-
throw new Error('Friendliness must be between 0 and 1');
|
|
96
|
-
}
|
|
97
|
-
this._personality.friendliness = friendliness;
|
|
119
|
+
setFriendliness(value: number): void {
|
|
120
|
+
this._friendliness = FriendlinessLevel.create(value);
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
setPlayfulness(
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
setPlayfulness(value: number): void {
|
|
124
|
+
this._playfulness = PlayfulnessLevel.create(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Rich behavior with Value Objects
|
|
128
|
+
cheerUp(): void {
|
|
129
|
+
if (this._mood.isNegative()) {
|
|
130
|
+
this._mood = Mood.create('neutral');
|
|
103
131
|
}
|
|
104
|
-
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
boostEnergy(amount: number): void {
|
|
135
|
+
this._energy = this._energy.increase(amount);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
drainEnergy(amount: number): void {
|
|
139
|
+
this._energy = this._energy.decrease(amount);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
makeMoreFriendly(amount: number): void {
|
|
143
|
+
this._friendliness = this._friendliness.increase(amount);
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
// Appearance Management
|
|
@@ -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
|
+
}
|