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,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
|
+
};
|
package/src/core/Game.js
ADDED
|
@@ -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
|
+
}
|