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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebGL Particle System
|
|
3
|
+
*
|
|
4
|
+
* High-performance particle renderer using WebGL for 10,000+ particles
|
|
5
|
+
* Implements IParticleSystem interface
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class WebGLParticleSystem {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.maxParticles = config.maxParticles || 10000;
|
|
11
|
+
this.kernel = null;
|
|
12
|
+
|
|
13
|
+
// WebGL context and resources
|
|
14
|
+
this.canvas = null;
|
|
15
|
+
this.gl = null;
|
|
16
|
+
this.program = null;
|
|
17
|
+
this.particleBuffer = null;
|
|
18
|
+
|
|
19
|
+
// Particle data (flat arrays for GPU upload)
|
|
20
|
+
this.positions = new Float32Array(this.maxParticles * 2); // x, y
|
|
21
|
+
this.velocities = new Float32Array(this.maxParticles * 2); // vx, vy
|
|
22
|
+
this.colors = new Float32Array(this.maxParticles * 4); // r, g, b, a
|
|
23
|
+
this.sizes = new Float32Array(this.maxParticles);
|
|
24
|
+
this.lifetimes = new Float32Array(this.maxParticles);
|
|
25
|
+
this.ages = new Float32Array(this.maxParticles);
|
|
26
|
+
|
|
27
|
+
this.particleCount = 0;
|
|
28
|
+
this.registeredEffects = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ISystem interface
|
|
32
|
+
init(kernel) {
|
|
33
|
+
this.kernel = kernel;
|
|
34
|
+
this._initWebGL();
|
|
35
|
+
this._createShaders();
|
|
36
|
+
this._createBuffers();
|
|
37
|
+
|
|
38
|
+
console.log('[WebGLParticles] Initialized with max particles:', this.maxParticles);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
update(dt) {
|
|
42
|
+
// Update particle physics
|
|
43
|
+
for (let i = 0; i < this.particleCount; i++) {
|
|
44
|
+
// Update age
|
|
45
|
+
this.ages[i] += dt;
|
|
46
|
+
|
|
47
|
+
// Remove dead particles
|
|
48
|
+
if (this.ages[i] >= this.lifetimes[i]) {
|
|
49
|
+
this._removeParticle(i);
|
|
50
|
+
i--; // Recheck this index
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Update position
|
|
55
|
+
this.positions[i * 2] += this.velocities[i * 2] * dt;
|
|
56
|
+
this.positions[i * 2 + 1] += this.velocities[i * 2 + 1] * dt;
|
|
57
|
+
|
|
58
|
+
// Apply gravity
|
|
59
|
+
this.velocities[i * 2 + 1] += 500 * dt; // Gravity
|
|
60
|
+
|
|
61
|
+
// Fade out
|
|
62
|
+
const lifeRatio = this.ages[i] / this.lifetimes[i];
|
|
63
|
+
this.colors[i * 4 + 3] = 1 - lifeRatio; // Alpha fade
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Render
|
|
67
|
+
this._render();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
destroy() {
|
|
71
|
+
if (this.gl) {
|
|
72
|
+
this.gl.deleteProgram(this.program);
|
|
73
|
+
this.gl.deleteBuffer(this.particleBuffer);
|
|
74
|
+
}
|
|
75
|
+
if (this.canvas && this.canvas.parentNode) {
|
|
76
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// IParticleSystem interface
|
|
81
|
+
registerSprite(key, url) {
|
|
82
|
+
// WebGL particles use point sprites, not textures (for simplicity)
|
|
83
|
+
console.warn('[WebGLParticles] Sprite registration not yet implemented');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
registerEffect(name, callback) {
|
|
87
|
+
this.registeredEffects.set(name, callback);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
play(name, x, y, ...args) {
|
|
91
|
+
const effect = this.registeredEffects.get(name);
|
|
92
|
+
if (effect) {
|
|
93
|
+
effect.call(this, x, y, ...args);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
emit(config) {
|
|
98
|
+
const {
|
|
99
|
+
x = 0,
|
|
100
|
+
y = 0,
|
|
101
|
+
count = 10,
|
|
102
|
+
color = [1, 1, 1, 1],
|
|
103
|
+
size = 5,
|
|
104
|
+
lifetime = 1,
|
|
105
|
+
velocity = { x: 0, y: -100 },
|
|
106
|
+
spread = 50
|
|
107
|
+
} = config;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < count; i++) {
|
|
110
|
+
if (this.particleCount >= this.maxParticles) break;
|
|
111
|
+
|
|
112
|
+
const idx = this.particleCount;
|
|
113
|
+
|
|
114
|
+
// Position
|
|
115
|
+
this.positions[idx * 2] = x + (Math.random() - 0.5) * spread;
|
|
116
|
+
this.positions[idx * 2 + 1] = y + (Math.random() - 0.5) * spread;
|
|
117
|
+
|
|
118
|
+
// Velocity
|
|
119
|
+
const angle = Math.random() * Math.PI * 2;
|
|
120
|
+
const speed = Math.random() * 200 + 100;
|
|
121
|
+
this.velocities[idx * 2] = Math.cos(angle) * speed + velocity.x;
|
|
122
|
+
this.velocities[idx * 2 + 1] = Math.sin(angle) * speed + velocity.y;
|
|
123
|
+
|
|
124
|
+
// Color
|
|
125
|
+
this.colors[idx * 4] = color[0];
|
|
126
|
+
this.colors[idx * 4 + 1] = color[1];
|
|
127
|
+
this.colors[idx * 4 + 2] = color[2];
|
|
128
|
+
this.colors[idx * 4 + 3] = color[3];
|
|
129
|
+
|
|
130
|
+
// Size and lifetime
|
|
131
|
+
this.sizes[idx] = size;
|
|
132
|
+
this.lifetimes[idx] = lifetime;
|
|
133
|
+
this.ages[idx] = 0;
|
|
134
|
+
|
|
135
|
+
this.particleCount++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
clear() {
|
|
140
|
+
this.particleCount = 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Built-in effects
|
|
144
|
+
confetti(x, y, count = 50) {
|
|
145
|
+
const colors = [
|
|
146
|
+
[1, 0, 0, 1], // Red
|
|
147
|
+
[0, 1, 0, 1], // Green
|
|
148
|
+
[0, 0, 1, 1], // Blue
|
|
149
|
+
[1, 1, 0, 1], // Yellow
|
|
150
|
+
[1, 0, 1, 1], // Magenta
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < count; i++) {
|
|
154
|
+
this.emit({
|
|
155
|
+
x, y,
|
|
156
|
+
count: 1,
|
|
157
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
158
|
+
size: Math.random() * 8 + 4,
|
|
159
|
+
lifetime: Math.random() * 2 + 1,
|
|
160
|
+
velocity: { x: 0, y: -300 },
|
|
161
|
+
spread: 100
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
sparkle(x, y, count = 20, color = [1, 1, 1, 1]) {
|
|
167
|
+
this.emit({
|
|
168
|
+
x, y,
|
|
169
|
+
count,
|
|
170
|
+
color,
|
|
171
|
+
size: 3,
|
|
172
|
+
lifetime: 0.5,
|
|
173
|
+
velocity: { x: 0, y: -50 },
|
|
174
|
+
spread: 30
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Private methods
|
|
179
|
+
_initWebGL() {
|
|
180
|
+
// Create overlay canvas
|
|
181
|
+
this.canvas = document.createElement('canvas');
|
|
182
|
+
this.canvas.style.cssText = `
|
|
183
|
+
position: fixed;
|
|
184
|
+
top: 0;
|
|
185
|
+
left: 0;
|
|
186
|
+
width: 100%;
|
|
187
|
+
height: 100%;
|
|
188
|
+
pointer-events: none;
|
|
189
|
+
z-index: 9999;
|
|
190
|
+
`;
|
|
191
|
+
this.canvas.width = window.innerWidth;
|
|
192
|
+
this.canvas.height = window.innerHeight;
|
|
193
|
+
document.body.appendChild(this.canvas);
|
|
194
|
+
|
|
195
|
+
// Get WebGL context
|
|
196
|
+
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
|
|
197
|
+
if (!this.gl) {
|
|
198
|
+
console.error('[WebGLParticles] WebGL not supported');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Setup WebGL state
|
|
203
|
+
this.gl.enable(this.gl.BLEND);
|
|
204
|
+
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
205
|
+
|
|
206
|
+
// Handle resize
|
|
207
|
+
window.addEventListener('resize', () => {
|
|
208
|
+
this.canvas.width = window.innerWidth;
|
|
209
|
+
this.canvas.height = window.innerHeight;
|
|
210
|
+
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_createShaders() {
|
|
215
|
+
const vertexShaderSource = `
|
|
216
|
+
attribute vec2 a_position;
|
|
217
|
+
attribute vec4 a_color;
|
|
218
|
+
attribute float a_size;
|
|
219
|
+
|
|
220
|
+
uniform vec2 u_resolution;
|
|
221
|
+
|
|
222
|
+
varying vec4 v_color;
|
|
223
|
+
|
|
224
|
+
void main() {
|
|
225
|
+
// Convert from pixels to clip space
|
|
226
|
+
vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
|
|
227
|
+
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
|
|
228
|
+
gl_PointSize = a_size;
|
|
229
|
+
v_color = a_color;
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const fragmentShaderSource = `
|
|
234
|
+
precision mediump float;
|
|
235
|
+
varying vec4 v_color;
|
|
236
|
+
|
|
237
|
+
void main() {
|
|
238
|
+
// Circular point sprite
|
|
239
|
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
240
|
+
if (length(coord) > 0.5) {
|
|
241
|
+
discard;
|
|
242
|
+
}
|
|
243
|
+
gl_FragColor = v_color;
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
const vertexShader = this._compileShader(this.gl.VERTEX_SHADER, vertexShaderSource);
|
|
248
|
+
const fragmentShader = this._compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSource);
|
|
249
|
+
|
|
250
|
+
this.program = this.gl.createProgram();
|
|
251
|
+
this.gl.attachShader(this.program, vertexShader);
|
|
252
|
+
this.gl.attachShader(this.program, fragmentShader);
|
|
253
|
+
this.gl.linkProgram(this.program);
|
|
254
|
+
|
|
255
|
+
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
|
|
256
|
+
console.error('[WebGLParticles] Shader program failed to link');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_compileShader(type, source) {
|
|
261
|
+
const shader = this.gl.createShader(type);
|
|
262
|
+
this.gl.shaderSource(shader, source);
|
|
263
|
+
this.gl.compileShader(shader);
|
|
264
|
+
|
|
265
|
+
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
266
|
+
console.error('[WebGLParticles] Shader compilation error:', this.gl.getShaderInfoLog(shader));
|
|
267
|
+
this.gl.deleteShader(shader);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return shader;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_createBuffers() {
|
|
275
|
+
this.particleBuffer = this.gl.createBuffer();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_render() {
|
|
279
|
+
const gl = this.gl;
|
|
280
|
+
|
|
281
|
+
// Clear canvas
|
|
282
|
+
gl.clearColor(0, 0, 0, 0);
|
|
283
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
284
|
+
|
|
285
|
+
if (this.particleCount === 0) return;
|
|
286
|
+
|
|
287
|
+
// Use shader program
|
|
288
|
+
gl.useProgram(this.program);
|
|
289
|
+
|
|
290
|
+
// Set resolution uniform
|
|
291
|
+
const resolutionLocation = gl.getUniformLocation(this.program, 'u_resolution');
|
|
292
|
+
gl.uniform2f(resolutionLocation, this.canvas.width, this.canvas.height);
|
|
293
|
+
|
|
294
|
+
// Interleave particle data for GPU
|
|
295
|
+
const vertexData = new Float32Array(this.particleCount * 7); // x, y, r, g, b, a, size
|
|
296
|
+
for (let i = 0; i < this.particleCount; i++) {
|
|
297
|
+
vertexData[i * 7] = this.positions[i * 2];
|
|
298
|
+
vertexData[i * 7 + 1] = this.positions[i * 2 + 1];
|
|
299
|
+
vertexData[i * 7 + 2] = this.colors[i * 4];
|
|
300
|
+
vertexData[i * 7 + 3] = this.colors[i * 4 + 1];
|
|
301
|
+
vertexData[i * 7 + 4] = this.colors[i * 4 + 2];
|
|
302
|
+
vertexData[i * 7 + 5] = this.colors[i * 4 + 3];
|
|
303
|
+
vertexData[i * 7 + 6] = this.sizes[i];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Upload to GPU
|
|
307
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.particleBuffer);
|
|
308
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.DYNAMIC_DRAW);
|
|
309
|
+
|
|
310
|
+
// Setup attributes
|
|
311
|
+
const stride = 7 * 4; // 7 floats * 4 bytes
|
|
312
|
+
|
|
313
|
+
const positionLocation = gl.getAttribLocation(this.program, 'a_position');
|
|
314
|
+
gl.enableVertexAttribArray(positionLocation);
|
|
315
|
+
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, stride, 0);
|
|
316
|
+
|
|
317
|
+
const colorLocation = gl.getAttribLocation(this.program, 'a_color');
|
|
318
|
+
gl.enableVertexAttribArray(colorLocation);
|
|
319
|
+
gl.vertexAttribPointer(colorLocation, 4, gl.FLOAT, false, stride, 2 * 4);
|
|
320
|
+
|
|
321
|
+
const sizeLocation = gl.getAttribLocation(this.program, 'a_size');
|
|
322
|
+
gl.enableVertexAttribArray(sizeLocation);
|
|
323
|
+
gl.vertexAttribPointer(sizeLocation, 1, gl.FLOAT, false, stride, 6 * 4);
|
|
324
|
+
|
|
325
|
+
// Draw particles
|
|
326
|
+
gl.drawArrays(gl.POINTS, 0, this.particleCount);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_removeParticle(index) {
|
|
330
|
+
// Swap with last particle and decrease count
|
|
331
|
+
const last = this.particleCount - 1;
|
|
332
|
+
if (index !== last) {
|
|
333
|
+
// Copy last particle to this index
|
|
334
|
+
this.positions[index * 2] = this.positions[last * 2];
|
|
335
|
+
this.positions[index * 2 + 1] = this.positions[last * 2 + 1];
|
|
336
|
+
this.velocities[index * 2] = this.velocities[last * 2];
|
|
337
|
+
this.velocities[index * 2 + 1] = this.velocities[last * 2 + 1];
|
|
338
|
+
this.colors[index * 4] = this.colors[last * 4];
|
|
339
|
+
this.colors[index * 4 + 1] = this.colors[last * 4 + 1];
|
|
340
|
+
this.colors[index * 4 + 2] = this.colors[last * 4 + 2];
|
|
341
|
+
this.colors[index * 4 + 3] = this.colors[last * 4 + 3];
|
|
342
|
+
this.sizes[index] = this.sizes[last];
|
|
343
|
+
this.lifetimes[index] = this.lifetimes[last];
|
|
344
|
+
this.ages[index] = this.ages[last];
|
|
345
|
+
}
|
|
346
|
+
this.particleCount--;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { RewardSystem } from './core/RewardSystem.js';
|
|
2
|
+
import { DataService } from './core/DataService.js';
|
|
3
|
+
import { GameUI } from './ui/GameUI.js';
|
|
4
|
+
import { ParticleSystem } from './effects/ParticleSystem.js';
|
|
5
|
+
import { SoundManager } from './audio/SoundManager.js';
|
|
6
|
+
import './ui/dopamine.css';
|
|
7
|
+
|
|
8
|
+
export { RewardSystem, DataService, GameUI, ParticleSystem, SoundManager };
|
|
9
|
+
|
|
10
|
+
export default class Dopamine {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
|
|
14
|
+
// Initialize subsystems with config
|
|
15
|
+
this.dataService = new DataService(config.data || {});
|
|
16
|
+
this.rewardSystem = new RewardSystem(this.dataService, config.rewards || {});
|
|
17
|
+
this.soundManager = new SoundManager(config.sound || {});
|
|
18
|
+
this.particleSystem = new ParticleSystem(config.particles || {});
|
|
19
|
+
this.gameUI = new GameUI(this.particleSystem);
|
|
20
|
+
|
|
21
|
+
// Bind UI to RewardSystem events
|
|
22
|
+
this._bindEvents();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async init() {
|
|
26
|
+
await this.rewardSystem.init();
|
|
27
|
+
console.log('DopamineJS Initialized 🚀');
|
|
28
|
+
return {
|
|
29
|
+
rewardSystem: this.rewardSystem,
|
|
30
|
+
gameUI: this.gameUI,
|
|
31
|
+
particleSystem: this.particleSystem,
|
|
32
|
+
soundManager: this.soundManager
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_bindEvents() {
|
|
37
|
+
this.rewardSystem.on('xp_gained', (data) => {
|
|
38
|
+
const { total, needed } = this.rewardSystem.getXPForNextLevel();
|
|
39
|
+
this.gameUI.updateXP(this.rewardSystem.player.xp, needed, total);
|
|
40
|
+
|
|
41
|
+
if (data.xpGained > 0) {
|
|
42
|
+
// Show floating text for XP gain
|
|
43
|
+
// We might need a way to know WHERE to show this, or just show it in a standard place
|
|
44
|
+
// For now, let's just update the bar
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.rewardSystem.on('level_up', (data) => {
|
|
49
|
+
this.gameUI.updateLevel(data.newLevel);
|
|
50
|
+
this.gameUI.showLevelUp(data.oldLevel, data.newLevel);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.rewardSystem.on('achievement_unlocked', (achievement) => {
|
|
54
|
+
this.gameUI.showAchievement(achievement);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.rewardSystem.on('new_high_score', (data) => {
|
|
58
|
+
this.gameUI.showNotification(`New High Score: ${data.score}!`, 'legendary');
|
|
59
|
+
this.soundManager.playSuccess();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates procedural textures for prototyping.
|
|
3
|
+
*/
|
|
4
|
+
export class TextureGenerator {
|
|
5
|
+
static createGround(width, height, color = '#333') {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.width = width;
|
|
8
|
+
canvas.height = height;
|
|
9
|
+
const ctx = canvas.getContext('2d');
|
|
10
|
+
|
|
11
|
+
ctx.fillStyle = color;
|
|
12
|
+
ctx.fillRect(0, 0, width, height);
|
|
13
|
+
|
|
14
|
+
// Noise / Grit
|
|
15
|
+
for (let i = 0; i < 1000; i++) {
|
|
16
|
+
ctx.fillStyle = Math.random() > 0.5 ? '#444' : '#222';
|
|
17
|
+
const size = Math.random() * 4 + 2;
|
|
18
|
+
ctx.fillRect(Math.random() * width, Math.random() * height, size, size);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Grid lines overlay (faint)
|
|
22
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
|
|
23
|
+
ctx.lineWidth = 2;
|
|
24
|
+
ctx.beginPath();
|
|
25
|
+
for (let i = 0; i <= width; i += 64) {
|
|
26
|
+
ctx.moveTo(i, 0); ctx.lineTo(i, height);
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i <= height; i += 64) {
|
|
29
|
+
ctx.moveTo(0, i); ctx.lineTo(width, i);
|
|
30
|
+
}
|
|
31
|
+
ctx.stroke();
|
|
32
|
+
|
|
33
|
+
return canvas;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static createCharacterSheet(colorBody, colorDetail) {
|
|
37
|
+
// Create a 4-frame walk cycle strip (Vertical or Horizontal)
|
|
38
|
+
// Let's do Horizontal: 64x64 frames, 4 frames -> 256x64 image
|
|
39
|
+
const cellW = 64;
|
|
40
|
+
const cellH = 64;
|
|
41
|
+
const canvas = document.createElement('canvas');
|
|
42
|
+
canvas.width = cellW * 4;
|
|
43
|
+
canvas.height = cellH;
|
|
44
|
+
const ctx = canvas.getContext('2d');
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < 4; i++) {
|
|
47
|
+
const x = i * cellW;
|
|
48
|
+
const y = 0;
|
|
49
|
+
const cx = x + cellW / 2;
|
|
50
|
+
const cy = y + cellH / 2;
|
|
51
|
+
|
|
52
|
+
// -- Frame Animation Variances --
|
|
53
|
+
// Bobbing effect
|
|
54
|
+
const bob = (i % 2 === 0) ? 0 : -2;
|
|
55
|
+
// Leg swing (simulated by shape change or just simple offset)
|
|
56
|
+
const legOffset = (i === 1) ? 5 : (i === 3 ? -5 : 0);
|
|
57
|
+
|
|
58
|
+
// Body
|
|
59
|
+
ctx.fillStyle = colorBody;
|
|
60
|
+
ctx.beginPath();
|
|
61
|
+
ctx.arc(cx, cy + bob, 20, 0, Math.PI * 2);
|
|
62
|
+
ctx.fill();
|
|
63
|
+
|
|
64
|
+
// Shoulders/Hands (Top Down)
|
|
65
|
+
ctx.fillStyle = colorDetail;
|
|
66
|
+
ctx.beginPath();
|
|
67
|
+
ctx.arc(cx - 15, cy + bob + legOffset, 8, 0, Math.PI * 2); // Left hand
|
|
68
|
+
ctx.arc(cx + 15, cy + bob - legOffset, 8, 0, Math.PI * 2); // Right hand
|
|
69
|
+
ctx.fill();
|
|
70
|
+
|
|
71
|
+
// Head/Helmet
|
|
72
|
+
ctx.fillStyle = adjustColor(colorBody, -20);
|
|
73
|
+
ctx.beginPath();
|
|
74
|
+
ctx.arc(cx, cy + bob, 15, 0, Math.PI * 2);
|
|
75
|
+
ctx.fill();
|
|
76
|
+
|
|
77
|
+
// Gun (if soldier)
|
|
78
|
+
if (colorDetail === '#333') { // Hacky check
|
|
79
|
+
ctx.fillStyle = '#000';
|
|
80
|
+
ctx.fillRect(cx + 10, cy + bob - 5, 25, 6);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return canvas;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function adjustColor(color, amount) {
|
|
89
|
+
return color; // Placeholder for color darkening logic
|
|
90
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,62 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
_bindEvents() {
|
|
37
|
-
this.rewardSystem.on('xp_gained', (data) => {
|
|
38
|
-
const { total, needed } = this.rewardSystem.getXPForNextLevel();
|
|
39
|
-
this.gameUI.updateXP(this.rewardSystem.player.xp, needed, total);
|
|
40
|
-
|
|
41
|
-
if (data.xpGained > 0) {
|
|
42
|
-
// Show floating text for XP gain
|
|
43
|
-
// We might need a way to know WHERE to show this, or just show it in a standard place
|
|
44
|
-
// For now, let's just update the bar
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
this.rewardSystem.on('level_up', (data) => {
|
|
49
|
-
this.gameUI.updateLevel(data.newLevel);
|
|
50
|
-
this.gameUI.showLevelUp(data.oldLevel, data.newLevel);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
this.rewardSystem.on('achievement_unlocked', (achievement) => {
|
|
54
|
-
this.gameUI.showAchievement(achievement);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
this.rewardSystem.on('new_high_score', (data) => {
|
|
58
|
-
this.gameUI.showNotification(`New High Score: ${data.score}!`, 'legendary');
|
|
59
|
-
this.soundManager.playSuccess();
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
1
|
+
import Dopamine from './dopamine/index.js';
|
|
2
|
+
|
|
3
|
+
// Re-export legacy systems
|
|
4
|
+
export * from './dopamine/index.js';
|
|
5
|
+
|
|
6
|
+
// Export New Kernel Architecture (Phase 0)
|
|
7
|
+
export { DopamineKernel } from './core/DopamineKernel.js';
|
|
8
|
+
export { EventBus } from './core/EventBus.js';
|
|
9
|
+
export { SystemRegistry } from './core/SystemRegistry.js';
|
|
10
|
+
export { PluginRegistry } from './core/PluginRegistry.js';
|
|
11
|
+
|
|
12
|
+
// Export System Interfaces
|
|
13
|
+
export * from './interfaces/index.js';
|
|
14
|
+
|
|
15
|
+
// Export New Engine Core
|
|
16
|
+
export { Game } from './core/Game.js';
|
|
17
|
+
export { Scene } from './core/Scene.js';
|
|
18
|
+
export { GameObject } from './core/GameObject.js';
|
|
19
|
+
export { Component } from './core/Component.js';
|
|
20
|
+
export { Renderer } from './renderer/Renderer.js';
|
|
21
|
+
export { Ticker } from './systems/Ticker.js';
|
|
22
|
+
export { Loader, GlobalLoader } from './systems/Loader.js';
|
|
23
|
+
export { Input, GlobalInput } from './systems/Input.js';
|
|
24
|
+
export { Director } from './systems/Director.js';
|
|
25
|
+
export { Physics, GlobalPhysics } from './systems/Physics.js';
|
|
26
|
+
export { Vector2 } from './core/Vector2.js';
|
|
27
|
+
export { Collider } from './core/Collider.js';
|
|
28
|
+
export { Sprite } from './core/Sprite.js';
|
|
29
|
+
export { Animator } from './core/Animator.js';
|
|
30
|
+
export { TextureGenerator } from './dopamine/utils/TextureGenerator.js';
|
|
31
|
+
export { ParticleEmitter, ScreenShake } from './dopamine/components/index.js';
|
|
32
|
+
|
|
33
|
+
// Export default for backward compatibility
|
|
34
|
+
export default Dopamine;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ISystem } from './ISystem.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IAudioSystem - Interface for audio implementations
|
|
5
|
+
*
|
|
6
|
+
* Allows different audio engines (Web Audio, Howler.js, etc.) to be swapped
|
|
7
|
+
*/
|
|
8
|
+
export const IAudioSystem = {
|
|
9
|
+
...ISystem,
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a sound with a key
|
|
13
|
+
* @param {string} key - Sound identifier
|
|
14
|
+
* @param {string} url - Path to audio file
|
|
15
|
+
*/
|
|
16
|
+
registerSound(key, url) { },
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Preload multiple sounds
|
|
20
|
+
* @param {Object} soundMap - Map of key -> url
|
|
21
|
+
* @returns {Promise<void>}
|
|
22
|
+
*/
|
|
23
|
+
preloadSounds(soundMap) { },
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Play a sound by key
|
|
27
|
+
* @param {string} key - Sound identifier
|
|
28
|
+
* @param {Object} options - Playback options (volume, loop, etc.)
|
|
29
|
+
*/
|
|
30
|
+
play(key, options) { },
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Play a synthesized tone
|
|
34
|
+
* @param {number} frequency - Frequency in Hz
|
|
35
|
+
* @param {number} duration - Duration in seconds
|
|
36
|
+
* @param {string} type - Waveform type ('sine', 'square', etc.)
|
|
37
|
+
* @param {number} volume - Volume (0-1)
|
|
38
|
+
*/
|
|
39
|
+
playTone(frequency, duration, type, volume) { },
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stop a playing sound
|
|
43
|
+
* @param {string} key - Sound identifier
|
|
44
|
+
*/
|
|
45
|
+
stop(key) { },
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Toggle mute state
|
|
49
|
+
* @returns {boolean} - New mute state
|
|
50
|
+
*/
|
|
51
|
+
toggleMute() { },
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set master volume
|
|
55
|
+
* @param {number} volume - Volume (0-1)
|
|
56
|
+
*/
|
|
57
|
+
setVolume(volume) { },
|
|
58
|
+
};
|