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.
Files changed (45) hide show
  1. package/dist/dopamine.js +1947 -0
  2. package/dist/dopamine.umd.cjs +45 -0
  3. package/package.json +24 -32
  4. package/src/core/Animator.js +62 -0
  5. package/src/core/Collider.js +50 -0
  6. package/src/core/Component.js +38 -0
  7. package/src/core/DopamineKernel.js +153 -0
  8. package/src/core/EventBus.js +183 -0
  9. package/src/core/Game.js +89 -0
  10. package/src/core/GameObject.js +83 -0
  11. package/src/core/PluginRegistry.js +197 -0
  12. package/src/core/Scene.js +65 -0
  13. package/src/core/Sprite.js +51 -0
  14. package/src/core/SystemRegistry.js +242 -0
  15. package/src/core/Vector2.js +50 -0
  16. package/src/{audio → dopamine/audio}/SoundManager.js +1 -3
  17. package/src/dopamine/components/ParticleEmitter.js +50 -0
  18. package/src/dopamine/components/ScreenShake.js +49 -0
  19. package/src/dopamine/components/index.js +4 -0
  20. package/src/dopamine/effects/WebGLParticleSystem.js +348 -0
  21. package/src/dopamine/index.js +62 -0
  22. package/src/dopamine/utils/TextureGenerator.js +90 -0
  23. package/src/index.js +34 -62
  24. package/src/interfaces/IAudioSystem.js +58 -0
  25. package/src/interfaces/IParticleSystem.js +51 -0
  26. package/src/interfaces/IPhysicsSystem.js +52 -0
  27. package/src/interfaces/ISystem.js +48 -0
  28. package/src/interfaces/index.js +7 -0
  29. package/src/renderer/Renderer.js +37 -0
  30. package/src/systems/Director.js +27 -0
  31. package/src/systems/Input.js +77 -0
  32. package/src/systems/Loader.js +44 -0
  33. package/src/systems/Physics.js +117 -0
  34. package/src/systems/Ticker.js +61 -0
  35. package/ARCHITECTURE.md +0 -71
  36. package/CHANGELOG.md +0 -36
  37. package/README.md +0 -264
  38. package/dist/dopamine.mjs +0 -737
  39. package/dist/dopamine.umd.js +0 -45
  40. /package/src/{core → dopamine/core}/DataService.js +0 -0
  41. /package/src/{core → dopamine/core}/EventEmitter.js +0 -0
  42. /package/src/{core → dopamine/core}/RewardSystem.js +0 -0
  43. /package/src/{effects → dopamine/effects}/ParticleSystem.js +0 -0
  44. /package/src/{ui → dopamine/ui}/GameUI.js +0 -0
  45. /package/src/{ui → dopamine/ui}/dopamine.css +0 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * EventBus - Central event system for loose coupling between systems
3
+ *
4
+ * Performance optimizations:
5
+ * - Priority queues for critical events (tick, render)
6
+ * - Object pooling for event data (future enhancement)
7
+ * - Pre-allocated arrays to minimize GC
8
+ */
9
+ export class EventBus {
10
+ constructor() {
11
+ // Map<eventName, Set<{callback, priority}>>
12
+ this._listeners = new Map();
13
+ this._onceListeners = new Map();
14
+
15
+ // Cache for sorted listeners by priority
16
+ this._sortedCache = new Map();
17
+ }
18
+
19
+ /**
20
+ * Register an event listener
21
+ * @param {string} event - Event name
22
+ * @param {Function} callback - Callback function
23
+ * @param {number} priority - Higher priority = called first (default: 0)
24
+ * @returns {EventBus} - For chaining
25
+ */
26
+ on(event, callback, priority = 0) {
27
+ if (!this._listeners.has(event)) {
28
+ this._listeners.set(event, new Set());
29
+ }
30
+
31
+ this._listeners.get(event).add({ callback, priority });
32
+
33
+ // Invalidate cache for this event
34
+ this._sortedCache.delete(event);
35
+
36
+ return this;
37
+ }
38
+
39
+ /**
40
+ * Register a one-time event listener
41
+ * @param {string} event - Event name
42
+ * @param {Function} callback - Callback function
43
+ * @returns {EventBus} - For chaining
44
+ */
45
+ once(event, callback) {
46
+ if (!this._onceListeners.has(event)) {
47
+ this._onceListeners.set(event, new Set());
48
+ }
49
+
50
+ this._onceListeners.get(event).add(callback);
51
+ return this;
52
+ }
53
+
54
+ /**
55
+ * Remove an event listener
56
+ * @param {string} event - Event name
57
+ * @param {Function} callback - Callback function to remove
58
+ * @returns {EventBus} - For chaining
59
+ */
60
+ off(event, callback) {
61
+ const listeners = this._listeners.get(event);
62
+ if (listeners) {
63
+ // Find and remove the listener with matching callback
64
+ for (const listener of listeners) {
65
+ if (listener.callback === callback) {
66
+ listeners.delete(listener);
67
+ this._sortedCache.delete(event);
68
+ break;
69
+ }
70
+ }
71
+ }
72
+
73
+ const onceListeners = this._onceListeners.get(event);
74
+ if (onceListeners) {
75
+ onceListeners.delete(callback);
76
+ }
77
+
78
+ return this;
79
+ }
80
+
81
+ /**
82
+ * Emit an event to all registered listeners
83
+ * @param {string} event - Event name
84
+ * @param {*} data - Event data
85
+ */
86
+ emit(event, data) {
87
+ // Get sorted listeners (cached for performance)
88
+ const listeners = this._getSortedListeners(event);
89
+
90
+ // Call regular listeners
91
+ for (let i = 0; i < listeners.length; i++) {
92
+ listeners[i].callback(data);
93
+ }
94
+
95
+ // Call and remove once listeners
96
+ const onceListeners = this._onceListeners.get(event);
97
+ if (onceListeners && onceListeners.size > 0) {
98
+ for (const callback of onceListeners) {
99
+ callback(data);
100
+ }
101
+ onceListeners.clear();
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get sorted listeners for an event (with caching)
107
+ * @private
108
+ */
109
+ _getSortedListeners(event) {
110
+ // Check cache first
111
+ if (this._sortedCache.has(event)) {
112
+ return this._sortedCache.get(event);
113
+ }
114
+
115
+ const listeners = this._listeners.get(event);
116
+ if (!listeners || listeners.size === 0) {
117
+ return [];
118
+ }
119
+
120
+ // Sort by priority (higher first)
121
+ const sorted = Array.from(listeners).sort((a, b) => b.priority - a.priority);
122
+
123
+ // Cache the result
124
+ this._sortedCache.set(event, sorted);
125
+
126
+ return sorted;
127
+ }
128
+
129
+ /**
130
+ * Remove all listeners for an event, or all events if no event specified
131
+ * @param {string} [event] - Event name (optional)
132
+ */
133
+ clear(event) {
134
+ if (event) {
135
+ this._listeners.delete(event);
136
+ this._onceListeners.delete(event);
137
+ this._sortedCache.delete(event);
138
+ } else {
139
+ this._listeners.clear();
140
+ this._onceListeners.clear();
141
+ this._sortedCache.clear();
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check if an event has listeners
147
+ * @param {string} event - Event name
148
+ * @returns {boolean}
149
+ */
150
+ hasListeners(event) {
151
+ const listeners = this._listeners.get(event);
152
+ const onceListeners = this._onceListeners.get(event);
153
+ return (listeners && listeners.size > 0) || (onceListeners && onceListeners.size > 0);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Standard event names for performance-critical events
159
+ * Using constants avoids string allocation
160
+ */
161
+ EventBus.Events = {
162
+ // Core game loop events
163
+ TICK: 'tick',
164
+ FIXED_UPDATE: 'fixed_update',
165
+ RENDER: 'render',
166
+
167
+ // Physics events
168
+ COLLISION_ENTER: 'collision_enter',
169
+ COLLISION_EXIT: 'collision_exit',
170
+
171
+ // Dopamine events (from RewardSystem)
172
+ XP_GAINED: 'xp_gained',
173
+ LEVEL_UP: 'level_up',
174
+ ACHIEVEMENT_UNLOCKED: 'achievement_unlocked',
175
+ NEW_HIGH_SCORE: 'new_high_score',
176
+ STREAK_UPDATED: 'streak_updated',
177
+
178
+ // System lifecycle events
179
+ SYSTEM_REGISTERED: 'system_registered',
180
+ SYSTEM_UNREGISTERED: 'system_unregistered',
181
+ PLUGIN_LOADED: 'plugin_loaded',
182
+ PLUGIN_UNLOADED: 'plugin_unloaded',
183
+ };
@@ -0,0 +1,89 @@
1
+ import { DopamineKernel } from './DopamineKernel.js';
2
+ import { Scene } from './Scene.js';
3
+ import { Director } from '../systems/Director.js';
4
+ import { EventBus } from './EventBus.js';
5
+
6
+ /**
7
+ * The main Game entry point.
8
+ * Now uses DopamineKernel for system management.
9
+ */
10
+ export class Game {
11
+ constructor(config = {}) {
12
+ this.config = config;
13
+
14
+ // NEW: Use kernel for system management
15
+ this.kernel = new DopamineKernel(config);
16
+
17
+ // Initialize Director
18
+ this.director = new Director(this);
19
+
20
+ // Bind to kernel tick event for scene updates
21
+ this.kernel.events.on(EventBus.Events.TICK, this._update.bind(this), 10);
22
+ this.kernel.events.on(EventBus.Events.RENDER, this._render.bind(this), 10);
23
+ }
24
+
25
+ /**
26
+ * Backward compatibility accessors
27
+ */
28
+ get renderer() {
29
+ return this.kernel.renderer;
30
+ }
31
+
32
+ get ticker() {
33
+ return this.kernel.ticker;
34
+ }
35
+
36
+ get physics() {
37
+ return this.kernel.physics;
38
+ }
39
+
40
+ get input() {
41
+ return this.kernel.input;
42
+ }
43
+
44
+ /**
45
+ * Start the game loop.
46
+ */
47
+ start() {
48
+ this.kernel.start();
49
+ }
50
+
51
+ /**
52
+ * Stop the game loop.
53
+ */
54
+ stop() {
55
+ this.kernel.stop();
56
+ }
57
+
58
+ /**
59
+ * Switch to a new scene.
60
+ * @param {Scene} newScene
61
+ */
62
+ setScene(newScene) {
63
+ // Inject kernel into scene
64
+ newScene.kernel = this.kernel;
65
+ this.director.run(newScene);
66
+ }
67
+
68
+ _update({ dt }) {
69
+ // Update scene logic
70
+ if (this.director.currentScene) {
71
+ this.director.currentScene.update(dt);
72
+ }
73
+ }
74
+
75
+ _render({ dt }) {
76
+ // Clear and render
77
+ this.renderer.clear();
78
+ if (this.director.currentScene) {
79
+ this.director.currentScene.render(this.renderer.ctx);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Destroy the game and cleanup
85
+ */
86
+ destroy() {
87
+ this.kernel.destroy();
88
+ }
89
+ }
@@ -0,0 +1,83 @@
1
+ import { Component } from './Component.js';
2
+
3
+ /**
4
+ * Base Entity class.
5
+ * Has position, scale, rotation, and a list of Components.
6
+ */
7
+ export class GameObject {
8
+ constructor(x = 0, y = 0) {
9
+ this.x = x;
10
+ this.y = y;
11
+ this.rotation = 0;
12
+ this.scale = { x: 1, y: 1 };
13
+
14
+ this.components = [];
15
+ this.children = [];
16
+ this.parent = null;
17
+ this.kernel = null; // Injected by Scene or Game
18
+ }
19
+
20
+ /**
21
+ * Add a child object.
22
+ * @param {GameObject} child
23
+ */
24
+ addChild(child) {
25
+ child.parent = this;
26
+ child.kernel = this.kernel; // Propagate kernel
27
+ this.children.push(child);
28
+ return child;
29
+ }
30
+
31
+ /**
32
+ * Add a component.
33
+ * @param {Component} component
34
+ */
35
+ addComponent(component) {
36
+ component.gameObject = this;
37
+ component.kernel = this.kernel; // Inject kernel reference
38
+ this.components.push(component);
39
+ component.onAttach();
40
+ return component;
41
+ }
42
+
43
+ /**
44
+ * Get a component by class type.
45
+ * @param {class} type
46
+ */
47
+ getComponent(type) {
48
+ return this.components.find(c => c instanceof type);
49
+ }
50
+
51
+ update(dt) {
52
+ // Update components
53
+ for (const component of this.components) {
54
+ if (component.update) component.update(dt);
55
+ }
56
+
57
+ // Update children
58
+ for (const child of this.children) {
59
+ child.update(dt);
60
+ }
61
+ }
62
+
63
+ render(ctx) {
64
+ ctx.save();
65
+
66
+ // Apply Transforms
67
+ ctx.translate(this.x, this.y);
68
+ ctx.rotate(this.rotation);
69
+ ctx.scale(this.scale.x, this.scale.y);
70
+
71
+ // Render components
72
+ for (const component of this.components) {
73
+ if (component.render) component.render(ctx);
74
+ }
75
+
76
+ // Render children
77
+ for (const child of this.children) {
78
+ child.render(ctx);
79
+ }
80
+
81
+ ctx.restore();
82
+ }
83
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * PluginRegistry - Manages plugin lifecycle
3
+ *
4
+ * Plugins can extend the engine by:
5
+ * - Registering new systems
6
+ * - Replacing existing systems
7
+ * - Adding event listeners
8
+ * - Modifying kernel configuration
9
+ */
10
+ export class PluginRegistry {
11
+ constructor(kernel) {
12
+ this.kernel = kernel;
13
+
14
+ // Map<pluginName, plugin>
15
+ this._plugins = new Map();
16
+
17
+ // Track plugin load order for proper cleanup
18
+ this._loadOrder = [];
19
+ }
20
+
21
+ /**
22
+ * Register and initialize a plugin
23
+ *
24
+ * Plugin interface:
25
+ * {
26
+ * name: string,
27
+ * version?: string,
28
+ * init(kernel): void,
29
+ * destroy?(): void
30
+ * }
31
+ *
32
+ * @param {Object} plugin - Plugin instance
33
+ * @returns {PluginRegistry} - For chaining
34
+ */
35
+ use(plugin) {
36
+ if (!plugin.name) {
37
+ throw new Error('[PluginRegistry] Plugin must have a name');
38
+ }
39
+
40
+ if (this._plugins.has(plugin.name)) {
41
+ console.warn(`[PluginRegistry] Plugin "${plugin.name}" already registered. Skipping.`);
42
+ return this;
43
+ }
44
+
45
+ if (!plugin.init || typeof plugin.init !== 'function') {
46
+ throw new Error(`[PluginRegistry] Plugin "${plugin.name}" must have an init() method`);
47
+ }
48
+
49
+ // Store plugin
50
+ this._plugins.set(plugin.name, plugin);
51
+ this._loadOrder.push(plugin.name);
52
+
53
+ // Initialize plugin
54
+ try {
55
+ plugin.init(this.kernel);
56
+
57
+ // Emit event
58
+ this.kernel.events.emit('plugin_loaded', {
59
+ name: plugin.name,
60
+ version: plugin.version
61
+ });
62
+
63
+ console.log(`[PluginRegistry] Loaded plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`);
64
+ } catch (error) {
65
+ console.error(`[PluginRegistry] Failed to initialize plugin "${plugin.name}":`, error);
66
+ this._plugins.delete(plugin.name);
67
+ this._loadOrder.pop();
68
+ throw error;
69
+ }
70
+
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * Register and initialize a plugin asynchronously
76
+ * Useful for plugins that need to load external resources
77
+ *
78
+ * @param {Object} plugin - Plugin instance with async init
79
+ * @returns {Promise<PluginRegistry>} - For chaining
80
+ */
81
+ async useAsync(plugin) {
82
+ if (!plugin.name) {
83
+ throw new Error('[PluginRegistry] Plugin must have a name');
84
+ }
85
+
86
+ if (this._plugins.has(plugin.name)) {
87
+ console.warn(`[PluginRegistry] Plugin "${plugin.name}" already registered. Skipping.`);
88
+ return this;
89
+ }
90
+
91
+ if (!plugin.init || typeof plugin.init !== 'function') {
92
+ throw new Error(`[PluginRegistry] Plugin "${plugin.name}" must have an init() method`);
93
+ }
94
+
95
+ // Store plugin
96
+ this._plugins.set(plugin.name, plugin);
97
+ this._loadOrder.push(plugin.name);
98
+
99
+ // Initialize plugin (await if it returns a promise)
100
+ try {
101
+ await plugin.init(this.kernel);
102
+
103
+ // Emit event
104
+ this.kernel.events.emit('plugin_loaded', {
105
+ name: plugin.name,
106
+ version: plugin.version
107
+ });
108
+
109
+ console.log(`[PluginRegistry] Loaded plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`);
110
+ } catch (error) {
111
+ console.error(`[PluginRegistry] Failed to initialize plugin "${plugin.name}":`, error);
112
+ this._plugins.delete(plugin.name);
113
+ this._loadOrder.pop();
114
+ throw error;
115
+ }
116
+
117
+ return this;
118
+ }
119
+
120
+ /**
121
+ * Remove a plugin
122
+ * @param {string} name - Plugin name
123
+ * @returns {boolean} - True if plugin was found and removed
124
+ */
125
+ remove(name) {
126
+ const plugin = this._plugins.get(name);
127
+ if (!plugin) {
128
+ return false;
129
+ }
130
+
131
+ // Call destroy if available
132
+ if (plugin.destroy) {
133
+ try {
134
+ plugin.destroy();
135
+ } catch (error) {
136
+ console.error(`[PluginRegistry] Error destroying plugin "${name}":`, error);
137
+ }
138
+ }
139
+
140
+ this._plugins.delete(name);
141
+
142
+ // Remove from load order
143
+ const index = this._loadOrder.indexOf(name);
144
+ if (index > -1) {
145
+ this._loadOrder.splice(index, 1);
146
+ }
147
+
148
+ // Emit event
149
+ this.kernel.events.emit('plugin_unloaded', { name });
150
+
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * Get a plugin by name
156
+ * @param {string} name - Plugin name
157
+ * @returns {Object|undefined} - Plugin instance
158
+ */
159
+ get(name) {
160
+ return this._plugins.get(name);
161
+ }
162
+
163
+ /**
164
+ * Check if a plugin is registered
165
+ * @param {string} name - Plugin name
166
+ * @returns {boolean}
167
+ */
168
+ has(name) {
169
+ return this._plugins.has(name);
170
+ }
171
+
172
+ /**
173
+ * Get all registered plugin names
174
+ * @returns {string[]}
175
+ */
176
+ getPluginNames() {
177
+ return Array.from(this._plugins.keys());
178
+ }
179
+
180
+ /**
181
+ * Get plugin load order
182
+ * @returns {string[]}
183
+ */
184
+ getLoadOrder() {
185
+ return [...this._loadOrder];
186
+ }
187
+
188
+ /**
189
+ * Clear all plugins (in reverse load order)
190
+ */
191
+ clear() {
192
+ // Unload in reverse order
193
+ for (let i = this._loadOrder.length - 1; i >= 0; i--) {
194
+ this.remove(this._loadOrder[i]);
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Represents a specific state or screen in the game (e.g., Menu, Level1).
3
+ */
4
+ export class Scene {
5
+ constructor() {
6
+ this.children = [];
7
+ this.game = null;
8
+ }
9
+
10
+ /**
11
+ * Called when the scene is added to the Game.
12
+ */
13
+ onEnter() { }
14
+
15
+ onExit() { }
16
+
17
+ /**
18
+ * Update loop for the scene.
19
+ * @param {number} dt
20
+ */
21
+ update(dt) {
22
+ // Update all children (GameObjects)
23
+ for (const child of this.children) {
24
+ if (child.update) child.update(dt);
25
+ }
26
+ }
27
+
28
+ /**
29
+ this.gameObjects = [];
30
+ this.kernel = null; // Injected by Game
31
+ }
32
+
33
+ /**
34
+ * Add a GameObject to the scene.
35
+ * @param {GameObject} gameObject
36
+ */
37
+ add(gameObject) {
38
+ gameObject.kernel = this.kernel; // Inject kernel
39
+ this.gameObjects.push(gameObject);
40
+ return gameObject;
41
+ }
42
+
43
+ /**
44
+ * Remove a GameObject from the scene.
45
+ * @param {GameObject} gameObject
46
+ */
47
+ remove(gameObject) {
48
+ const index = this.gameObjects.indexOf(gameObject);
49
+ if (index > -1) {
50
+ this.gameObjects.splice(index, 1);
51
+ }
52
+ }
53
+
54
+ update(dt) {
55
+ for (const obj of this.gameObjects) {
56
+ obj.update(dt);
57
+ }
58
+ }
59
+
60
+ render(ctx) {
61
+ for (const obj of this.gameObjects) {
62
+ obj.render(ctx);
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,51 @@
1
+ import { Component } from '../core/Component.js';
2
+
3
+ export class Sprite extends Component {
4
+ constructor(image) {
5
+ super();
6
+ this.image = image; // Image or Canvas element
7
+ this.anchor = { x: 0.5, y: 0.5 };
8
+ this.width = image ? image.width : 0;
9
+ this.height = image ? image.height : 0;
10
+
11
+ // Frame support (for spritesheets)
12
+ this.frame = { x: 0, y: 0, w: this.width, h: this.height };
13
+ }
14
+
15
+ setFrame(x, y, w, h) {
16
+ this.frame = { x, y, w, h };
17
+ this.width = w;
18
+ this.height = h;
19
+ }
20
+
21
+ setTexture(image) {
22
+ this.image = image;
23
+ // Reset frame if full texture
24
+ if (image) {
25
+ this.setFrame(0, 0, image.width, image.height);
26
+ }
27
+ }
28
+
29
+ render(ctx) {
30
+ if (!this.image) return;
31
+
32
+ // The transform (pos, rot, scale) is already handled by the Scene/GO before calling render?
33
+ // Wait, our GameObject implementation currently propagates render but doesn't apply context transform automatically in the base class loop?
34
+ // Let's check GameObject.js.
35
+
36
+ // Checking GameObject.js...
37
+ // "ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation); ctx.scale(this.scale.x, this.scale.y); ... ctx.restore();"
38
+ // YES, it handles logic. So we render at 0,0 local space.
39
+
40
+ const w = this.frame.w;
41
+ const h = this.frame.h;
42
+ const offsetX = -w * this.anchor.x;
43
+ const offsetY = -h * this.anchor.y;
44
+
45
+ ctx.drawImage(
46
+ this.image,
47
+ this.frame.x, this.frame.y, w, h, // Source
48
+ offsetX, offsetY, w, h // Destination
49
+ );
50
+ }
51
+ }