dopaminejs 1.2.0 → 2.0.0
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/dist/dopamine.js +1947 -0
- package/dist/dopamine.umd.cjs +45 -0
- package/package.json +24 -32
- package/src/core/Animator.js +62 -0
- package/src/core/Collider.js +50 -0
- package/src/core/Component.js +38 -0
- package/src/core/DopamineKernel.js +153 -0
- package/src/core/EventBus.js +183 -0
- package/src/core/Game.js +89 -0
- package/src/core/GameObject.js +83 -0
- package/src/core/PluginRegistry.js +197 -0
- package/src/core/Scene.js +65 -0
- package/src/core/Sprite.js +51 -0
- package/src/core/SystemRegistry.js +242 -0
- package/src/core/Vector2.js +50 -0
- package/src/{audio → dopamine/audio}/SoundManager.js +1 -3
- package/src/dopamine/components/ParticleEmitter.js +50 -0
- package/src/dopamine/components/ScreenShake.js +49 -0
- package/src/dopamine/components/index.js +4 -0
- package/src/dopamine/effects/WebGLParticleSystem.js +348 -0
- package/src/dopamine/index.js +62 -0
- package/src/dopamine/utils/TextureGenerator.js +90 -0
- package/src/index.js +34 -62
- package/src/interfaces/IAudioSystem.js +58 -0
- package/src/interfaces/IParticleSystem.js +51 -0
- package/src/interfaces/IPhysicsSystem.js +52 -0
- package/src/interfaces/ISystem.js +48 -0
- package/src/interfaces/index.js +7 -0
- package/src/renderer/Renderer.js +37 -0
- package/src/systems/Director.js +27 -0
- package/src/systems/Input.js +77 -0
- package/src/systems/Loader.js +44 -0
- package/src/systems/Physics.js +117 -0
- package/src/systems/Ticker.js +61 -0
- package/ARCHITECTURE.md +0 -71
- package/CHANGELOG.md +0 -36
- package/README.md +0 -264
- package/dist/dopamine.mjs +0 -737
- package/dist/dopamine.umd.js +0 -45
- /package/src/{core → dopamine/core}/DataService.js +0 -0
- /package/src/{core → dopamine/core}/EventEmitter.js +0 -0
- /package/src/{core → dopamine/core}/RewardSystem.js +0 -0
- /package/src/{effects → dopamine/effects}/ParticleSystem.js +0 -0
- /package/src/{ui → dopamine/ui}/GameUI.js +0 -0
- /package/src/{ui → dopamine/ui}/dopamine.css +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SystemRegistry - Manages system lifecycle and dependency resolution
|
|
3
|
+
*
|
|
4
|
+
* Systems are registered with names and can have dependencies on other systems.
|
|
5
|
+
* The registry ensures systems are updated in the correct order based on dependencies.
|
|
6
|
+
*/
|
|
7
|
+
export class SystemRegistry {
|
|
8
|
+
constructor(kernel) {
|
|
9
|
+
this.kernel = kernel;
|
|
10
|
+
|
|
11
|
+
// Map<systemName, { system, priority, dependencies }>
|
|
12
|
+
this._systems = new Map();
|
|
13
|
+
|
|
14
|
+
// Pre-computed update order for performance
|
|
15
|
+
this._updateOrder = [];
|
|
16
|
+
this._fixedUpdateOrder = [];
|
|
17
|
+
|
|
18
|
+
// Flag to track if order needs recomputation
|
|
19
|
+
this._needsRecompute = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a system
|
|
24
|
+
* @param {string} name - Unique system name
|
|
25
|
+
* @param {Object} system - System instance (must implement ISystem interface)
|
|
26
|
+
* @param {Object} options - Configuration options
|
|
27
|
+
* @param {number} options.priority - Update priority (higher = earlier, default: 0)
|
|
28
|
+
* @param {string[]} options.dependencies - System names this depends on
|
|
29
|
+
* @returns {SystemRegistry} - For chaining
|
|
30
|
+
*/
|
|
31
|
+
register(name, system, { priority = 0, dependencies = [] } = {}) {
|
|
32
|
+
if (this._systems.has(name)) {
|
|
33
|
+
console.warn(`[SystemRegistry] System "${name}" already registered. Replacing.`);
|
|
34
|
+
this.unregister(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate dependencies exist
|
|
38
|
+
for (const dep of dependencies) {
|
|
39
|
+
if (!this._systems.has(dep)) {
|
|
40
|
+
console.warn(`[SystemRegistry] Dependency "${dep}" not found for system "${name}"`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Store system metadata
|
|
45
|
+
this._systems.set(name, {
|
|
46
|
+
system,
|
|
47
|
+
priority,
|
|
48
|
+
dependencies,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Initialize the system
|
|
52
|
+
if (system.init) {
|
|
53
|
+
system.init(this.kernel);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Mark for recomputation
|
|
57
|
+
this._needsRecompute = true;
|
|
58
|
+
|
|
59
|
+
// Emit event
|
|
60
|
+
this.kernel.events.emit('system_registered', { name, system });
|
|
61
|
+
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a system by name
|
|
67
|
+
* @param {string} name - System name
|
|
68
|
+
* @returns {Object|undefined} - System instance
|
|
69
|
+
*/
|
|
70
|
+
get(name) {
|
|
71
|
+
const entry = this._systems.get(name);
|
|
72
|
+
return entry ? entry.system : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a system is registered
|
|
77
|
+
* @param {string} name - System name
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
has(name) {
|
|
81
|
+
return this._systems.has(name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unregister a system
|
|
86
|
+
* @param {string} name - System name
|
|
87
|
+
* @returns {boolean} - True if system was found and removed
|
|
88
|
+
*/
|
|
89
|
+
unregister(name) {
|
|
90
|
+
const entry = this._systems.get(name);
|
|
91
|
+
if (!entry) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Call destroy if available
|
|
96
|
+
if (entry.system.destroy) {
|
|
97
|
+
entry.system.destroy();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._systems.delete(name);
|
|
101
|
+
this._needsRecompute = true;
|
|
102
|
+
|
|
103
|
+
// Emit event
|
|
104
|
+
this.kernel.events.emit('system_unregistered', { name });
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Update all systems (called each frame)
|
|
111
|
+
* @param {number} dt - Delta time in seconds
|
|
112
|
+
*/
|
|
113
|
+
update(dt) {
|
|
114
|
+
if (this._needsRecompute) {
|
|
115
|
+
this._recomputeUpdateOrder();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fast iteration - no lookups, just array access
|
|
119
|
+
for (let i = 0; i < this._updateOrder.length; i++) {
|
|
120
|
+
const system = this._updateOrder[i];
|
|
121
|
+
if (system.update) {
|
|
122
|
+
system.update(dt);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fixed update for physics (called at fixed timestep)
|
|
129
|
+
* @param {number} dt - Fixed delta time
|
|
130
|
+
*/
|
|
131
|
+
fixedUpdate(dt) {
|
|
132
|
+
if (this._needsRecompute) {
|
|
133
|
+
this._recomputeUpdateOrder();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < this._fixedUpdateOrder.length; i++) {
|
|
137
|
+
const system = this._fixedUpdateOrder[i];
|
|
138
|
+
if (system.fixedUpdate) {
|
|
139
|
+
system.fixedUpdate(dt);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Recompute the update order based on dependencies and priorities
|
|
146
|
+
* Uses topological sort for dependencies
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
_recomputeUpdateOrder() {
|
|
150
|
+
const systems = Array.from(this._systems.entries());
|
|
151
|
+
|
|
152
|
+
// Build dependency graph
|
|
153
|
+
const graph = new Map();
|
|
154
|
+
const inDegree = new Map();
|
|
155
|
+
|
|
156
|
+
for (const [name, entry] of systems) {
|
|
157
|
+
graph.set(name, entry.dependencies);
|
|
158
|
+
inDegree.set(name, 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Calculate in-degrees
|
|
162
|
+
for (const [name, deps] of graph) {
|
|
163
|
+
for (const dep of deps) {
|
|
164
|
+
if (inDegree.has(dep)) {
|
|
165
|
+
inDegree.set(name, inDegree.get(name) + 1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Topological sort (Kahn's algorithm)
|
|
171
|
+
const queue = [];
|
|
172
|
+
const sorted = [];
|
|
173
|
+
|
|
174
|
+
// Start with systems that have no dependencies
|
|
175
|
+
for (const [name, degree] of inDegree) {
|
|
176
|
+
if (degree === 0) {
|
|
177
|
+
queue.push(name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
while (queue.length > 0) {
|
|
182
|
+
// Sort queue by priority before processing
|
|
183
|
+
queue.sort((a, b) => {
|
|
184
|
+
const priorityA = this._systems.get(a).priority;
|
|
185
|
+
const priorityB = this._systems.get(b).priority;
|
|
186
|
+
return priorityB - priorityA; // Higher priority first
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const name = queue.shift();
|
|
190
|
+
sorted.push(name);
|
|
191
|
+
|
|
192
|
+
// Reduce in-degree for dependent systems
|
|
193
|
+
for (const [otherName, deps] of graph) {
|
|
194
|
+
if (deps.includes(name)) {
|
|
195
|
+
const newDegree = inDegree.get(otherName) - 1;
|
|
196
|
+
inDegree.set(otherName, newDegree);
|
|
197
|
+
if (newDegree === 0) {
|
|
198
|
+
queue.push(otherName);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check for circular dependencies
|
|
205
|
+
if (sorted.length !== systems.length) {
|
|
206
|
+
console.error('[SystemRegistry] Circular dependency detected in systems!');
|
|
207
|
+
// Fallback: just use priority order
|
|
208
|
+
sorted.length = 0;
|
|
209
|
+
sorted.push(...systems.map(([name]) => name).sort((a, b) => {
|
|
210
|
+
return this._systems.get(b).priority - this._systems.get(a).priority;
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Convert to system instances
|
|
215
|
+
this._updateOrder = sorted
|
|
216
|
+
.map(name => this._systems.get(name).system)
|
|
217
|
+
.filter(system => system.update);
|
|
218
|
+
|
|
219
|
+
this._fixedUpdateOrder = sorted
|
|
220
|
+
.map(name => this._systems.get(name).system)
|
|
221
|
+
.filter(system => system.fixedUpdate);
|
|
222
|
+
|
|
223
|
+
this._needsRecompute = false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get all registered system names
|
|
228
|
+
* @returns {string[]}
|
|
229
|
+
*/
|
|
230
|
+
getSystemNames() {
|
|
231
|
+
return Array.from(this._systems.keys());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Clear all systems
|
|
236
|
+
*/
|
|
237
|
+
clear() {
|
|
238
|
+
for (const name of this._systems.keys()) {
|
|
239
|
+
this.unregister(name);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2D Vector class for position, velocity, and physics calculations.
|
|
3
|
+
*/
|
|
4
|
+
export class Vector2 {
|
|
5
|
+
constructor(x = 0, y = 0) {
|
|
6
|
+
this.x = x;
|
|
7
|
+
this.y = y;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
add(v) {
|
|
11
|
+
this.x += v.x;
|
|
12
|
+
this.y += v.y;
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
sub(v) {
|
|
17
|
+
this.x -= v.x;
|
|
18
|
+
this.y -= v.y;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
scale(s) {
|
|
23
|
+
this.x *= s;
|
|
24
|
+
this.y *= s;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
distance(v) {
|
|
29
|
+
const dx = this.x - v.x;
|
|
30
|
+
const dy = this.y - v.y;
|
|
31
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
normalize() {
|
|
35
|
+
const len = Math.sqrt(this.x * this.x + this.y * this.y);
|
|
36
|
+
if (len > 0) {
|
|
37
|
+
this.x /= len;
|
|
38
|
+
this.y /= len;
|
|
39
|
+
}
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clone() {
|
|
44
|
+
return new Vector2(this.x, this.y);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static distance(a, b) {
|
|
48
|
+
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -11,7 +11,6 @@ export class SoundManager {
|
|
|
11
11
|
|
|
12
12
|
// Asset management
|
|
13
13
|
this.assets = new Map(); // key -> AudioBuffer
|
|
14
|
-
this.soundPack = config.soundPack || 'synth'; // 'synth' or custom
|
|
15
14
|
this.customSounds = config.customSounds || {}; // key -> url
|
|
16
15
|
|
|
17
16
|
// Preload custom sounds if provided
|
|
@@ -111,9 +110,8 @@ export class SoundManager {
|
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
// 3. Fallback to synth based on key name
|
|
114
|
-
// Map common keys to synth methods
|
|
115
113
|
const synthMap = {
|
|
116
|
-
'jump': () => this.playJump(true),
|
|
114
|
+
'jump': () => this.playJump(true),
|
|
117
115
|
'score': () => this.playScore(true),
|
|
118
116
|
'success': () => this.playSuccess(true),
|
|
119
117
|
'click': () => this.playClick(true),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Component } from '../../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps the Dopamine ParticleSystem for a GameObject.
|
|
5
|
+
* Allows easy "Emit at my position" logic.
|
|
6
|
+
*/
|
|
7
|
+
export class ParticleEmitter extends Component {
|
|
8
|
+
constructor(particleSystem) {
|
|
9
|
+
super();
|
|
10
|
+
this.system = particleSystem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Trigger an effect at the GameObject's current position.
|
|
15
|
+
* @param {string} effectName - 'confetti', 'fire', 'sparkle', etc.
|
|
16
|
+
* @param {object} options - override options
|
|
17
|
+
*/
|
|
18
|
+
play(effectName, options = {}) {
|
|
19
|
+
const x = this.gameObject.x;
|
|
20
|
+
const y = this.gameObject.y;
|
|
21
|
+
|
|
22
|
+
switch (effectName) {
|
|
23
|
+
case 'confetti':
|
|
24
|
+
this.system.confetti(x, y, options.count);
|
|
25
|
+
break;
|
|
26
|
+
case 'fire':
|
|
27
|
+
this.system.fire(x, y, options.count);
|
|
28
|
+
break;
|
|
29
|
+
case 'sparkle':
|
|
30
|
+
this.system.sparkle(x, y, options.count, options.color);
|
|
31
|
+
break;
|
|
32
|
+
case 'blood': // Custom shortcut for our game
|
|
33
|
+
this.system.emit({
|
|
34
|
+
x, y,
|
|
35
|
+
count: 10,
|
|
36
|
+
color: ['#8a0303', '#ff0000'],
|
|
37
|
+
speed: 4,
|
|
38
|
+
gravity: 0.1,
|
|
39
|
+
decay: 0.05
|
|
40
|
+
});
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
// Direct emit fallback
|
|
44
|
+
this.system.emit({
|
|
45
|
+
x, y,
|
|
46
|
+
...options
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Component } from '../../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adds screen shake capability.
|
|
5
|
+
* Usually attached to the Camera or the Root Scene object.
|
|
6
|
+
*/
|
|
7
|
+
export class ScreenShake extends Component {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.intensity = 0;
|
|
11
|
+
this.duration = 0;
|
|
12
|
+
this.timer = 0;
|
|
13
|
+
this.originalPos = { x: 0, y: 0 };
|
|
14
|
+
this.isShaking = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
shake(intensity = 5, duration = 0.2) {
|
|
18
|
+
this.intensity = intensity;
|
|
19
|
+
this.duration = duration;
|
|
20
|
+
this.timer = 0;
|
|
21
|
+
this.isShaking = true;
|
|
22
|
+
|
|
23
|
+
// Save original if not already shaking
|
|
24
|
+
if (this.timer === 0) {
|
|
25
|
+
this.originalPos.x = this.gameObject.x;
|
|
26
|
+
this.originalPos.y = this.gameObject.y;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
update(dt) {
|
|
31
|
+
if (!this.isShaking) return;
|
|
32
|
+
|
|
33
|
+
this.timer += dt;
|
|
34
|
+
|
|
35
|
+
if (this.timer >= this.duration) {
|
|
36
|
+
this.isShaking = false;
|
|
37
|
+
this.gameObject.x = this.originalPos.x;
|
|
38
|
+
this.gameObject.y = this.originalPos.y;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Random offset
|
|
43
|
+
const offsetX = (Math.random() - 0.5) * 2 * this.intensity;
|
|
44
|
+
const offsetY = (Math.random() - 0.5) * 2 * this.intensity;
|
|
45
|
+
|
|
46
|
+
this.gameObject.x = this.originalPos.x + offsetX;
|
|
47
|
+
this.gameObject.y = this.originalPos.y + offsetY;
|
|
48
|
+
}
|
|
49
|
+
}
|