dopaminejs 2.0.2 → 2.2.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/LICENSE +373 -0
- package/README.md +115 -0
- package/dist/dopamine.js +933 -673
- package/dist/dopamine.umd.cjs +17 -25
- package/dist/style.css +1 -1
- package/package.json +7 -4
- package/src/core/DopamineKernel.js +11 -1
- package/src/core/EventBus.js +32 -9
- package/src/dopamine/audio/SoundManager.js +59 -9
- package/src/dopamine/core/DataService.js +3 -1
- package/src/dopamine/core/RewardSystem.js +166 -21
- package/src/dopamine/effects/ParticleSystem.js +84 -32
- package/src/dopamine/ui/GameUI.js +185 -63
- package/src/dopamine/ui/dopamine.css +4 -1
- package/src/dopamine/utils/storage.js +49 -0
- package/src/index.js +6 -0
- package/src/systems/Input.js +8 -4
- package/src/systems/Loader.js +7 -3
- package/src/systems/Physics.js +7 -3
- package/src/systems/Ticker.js +22 -3
- package/src/systems/deprecate.js +38 -0
- package/types/index.d.ts +588 -0
- package/src/dopamine/effects/WebGLParticleSystem.js +0 -345
|
@@ -1,345 +0,0 @@
|
|
|
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
|
-
init(kernel) {
|
|
32
|
-
this.kernel = kernel;
|
|
33
|
-
this._initWebGL();
|
|
34
|
-
this._createShaders();
|
|
35
|
-
this._createBuffers();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
update(dt) {
|
|
39
|
-
// Update particle physics
|
|
40
|
-
for (let i = 0; i < this.particleCount; i++) {
|
|
41
|
-
// Update age
|
|
42
|
-
this.ages[i] += dt;
|
|
43
|
-
|
|
44
|
-
// Remove dead particles
|
|
45
|
-
if (this.ages[i] >= this.lifetimes[i]) {
|
|
46
|
-
this._removeParticle(i);
|
|
47
|
-
i--; // Recheck this index
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Update position
|
|
52
|
-
this.positions[i * 2] += this.velocities[i * 2] * dt;
|
|
53
|
-
this.positions[i * 2 + 1] += this.velocities[i * 2 + 1] * dt;
|
|
54
|
-
|
|
55
|
-
// Apply gravity
|
|
56
|
-
this.velocities[i * 2 + 1] += 500 * dt; // Gravity
|
|
57
|
-
|
|
58
|
-
// Fade out
|
|
59
|
-
const lifeRatio = this.ages[i] / this.lifetimes[i];
|
|
60
|
-
this.colors[i * 4 + 3] = 1 - lifeRatio; // Alpha fade
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Render
|
|
64
|
-
this._render();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
destroy() {
|
|
68
|
-
if (this.gl) {
|
|
69
|
-
this.gl.deleteProgram(this.program);
|
|
70
|
-
this.gl.deleteBuffer(this.particleBuffer);
|
|
71
|
-
}
|
|
72
|
-
if (this.canvas && this.canvas.parentNode) {
|
|
73
|
-
this.canvas.parentNode.removeChild(this.canvas);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// IParticleSystem interface
|
|
78
|
-
registerSprite(key, url) {
|
|
79
|
-
// WebGL particles use point sprites, not textures (for simplicity)
|
|
80
|
-
console.warn('[WebGLParticles] Sprite registration not yet implemented');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
registerEffect(name, callback) {
|
|
84
|
-
this.registeredEffects.set(name, callback);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
play(name, x, y, ...args) {
|
|
88
|
-
const effect = this.registeredEffects.get(name);
|
|
89
|
-
if (effect) {
|
|
90
|
-
effect.call(this, x, y, ...args);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
emit(config) {
|
|
95
|
-
const {
|
|
96
|
-
x = 0,
|
|
97
|
-
y = 0,
|
|
98
|
-
count = 10,
|
|
99
|
-
color = [1, 1, 1, 1],
|
|
100
|
-
size = 5,
|
|
101
|
-
lifetime = 1,
|
|
102
|
-
velocity = { x: 0, y: -100 },
|
|
103
|
-
spread = 50
|
|
104
|
-
} = config;
|
|
105
|
-
|
|
106
|
-
for (let i = 0; i < count; i++) {
|
|
107
|
-
if (this.particleCount >= this.maxParticles) break;
|
|
108
|
-
|
|
109
|
-
const idx = this.particleCount;
|
|
110
|
-
|
|
111
|
-
// Position
|
|
112
|
-
this.positions[idx * 2] = x + (Math.random() - 0.5) * spread;
|
|
113
|
-
this.positions[idx * 2 + 1] = y + (Math.random() - 0.5) * spread;
|
|
114
|
-
|
|
115
|
-
// Velocity
|
|
116
|
-
const angle = Math.random() * Math.PI * 2;
|
|
117
|
-
const speed = Math.random() * 200 + 100;
|
|
118
|
-
this.velocities[idx * 2] = Math.cos(angle) * speed + velocity.x;
|
|
119
|
-
this.velocities[idx * 2 + 1] = Math.sin(angle) * speed + velocity.y;
|
|
120
|
-
|
|
121
|
-
// Color
|
|
122
|
-
this.colors[idx * 4] = color[0];
|
|
123
|
-
this.colors[idx * 4 + 1] = color[1];
|
|
124
|
-
this.colors[idx * 4 + 2] = color[2];
|
|
125
|
-
this.colors[idx * 4 + 3] = color[3];
|
|
126
|
-
|
|
127
|
-
// Size and lifetime
|
|
128
|
-
this.sizes[idx] = size;
|
|
129
|
-
this.lifetimes[idx] = lifetime;
|
|
130
|
-
this.ages[idx] = 0;
|
|
131
|
-
|
|
132
|
-
this.particleCount++;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
clear() {
|
|
137
|
-
this.particleCount = 0;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Built-in effects
|
|
141
|
-
confetti(x, y, count = 50) {
|
|
142
|
-
const colors = [
|
|
143
|
-
[1, 0, 0, 1], // Red
|
|
144
|
-
[0, 1, 0, 1], // Green
|
|
145
|
-
[0, 0, 1, 1], // Blue
|
|
146
|
-
[1, 1, 0, 1], // Yellow
|
|
147
|
-
[1, 0, 1, 1], // Magenta
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
for (let i = 0; i < count; i++) {
|
|
151
|
-
this.emit({
|
|
152
|
-
x, y,
|
|
153
|
-
count: 1,
|
|
154
|
-
color: colors[Math.floor(Math.random() * colors.length)],
|
|
155
|
-
size: Math.random() * 8 + 4,
|
|
156
|
-
lifetime: Math.random() * 2 + 1,
|
|
157
|
-
velocity: { x: 0, y: -300 },
|
|
158
|
-
spread: 100
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
sparkle(x, y, count = 20, color = [1, 1, 1, 1]) {
|
|
164
|
-
this.emit({
|
|
165
|
-
x, y,
|
|
166
|
-
count,
|
|
167
|
-
color,
|
|
168
|
-
size: 3,
|
|
169
|
-
lifetime: 0.5,
|
|
170
|
-
velocity: { x: 0, y: -50 },
|
|
171
|
-
spread: 30
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Private methods
|
|
176
|
-
_initWebGL() {
|
|
177
|
-
// Create overlay canvas
|
|
178
|
-
this.canvas = document.createElement('canvas');
|
|
179
|
-
this.canvas.style.cssText = `
|
|
180
|
-
position: fixed;
|
|
181
|
-
top: 0;
|
|
182
|
-
left: 0;
|
|
183
|
-
width: 100%;
|
|
184
|
-
height: 100%;
|
|
185
|
-
pointer-events: none;
|
|
186
|
-
z-index: 9999;
|
|
187
|
-
`;
|
|
188
|
-
this.canvas.width = window.innerWidth;
|
|
189
|
-
this.canvas.height = window.innerHeight;
|
|
190
|
-
document.body.appendChild(this.canvas);
|
|
191
|
-
|
|
192
|
-
// Get WebGL context
|
|
193
|
-
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
|
|
194
|
-
if (!this.gl) {
|
|
195
|
-
console.error('[WebGLParticles] WebGL not supported');
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Setup WebGL state
|
|
200
|
-
this.gl.enable(this.gl.BLEND);
|
|
201
|
-
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
202
|
-
|
|
203
|
-
// Handle resize
|
|
204
|
-
window.addEventListener('resize', () => {
|
|
205
|
-
this.canvas.width = window.innerWidth;
|
|
206
|
-
this.canvas.height = window.innerHeight;
|
|
207
|
-
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_createShaders() {
|
|
212
|
-
const vertexShaderSource = `
|
|
213
|
-
attribute vec2 a_position;
|
|
214
|
-
attribute vec4 a_color;
|
|
215
|
-
attribute float a_size;
|
|
216
|
-
|
|
217
|
-
uniform vec2 u_resolution;
|
|
218
|
-
|
|
219
|
-
varying vec4 v_color;
|
|
220
|
-
|
|
221
|
-
void main() {
|
|
222
|
-
// Convert from pixels to clip space
|
|
223
|
-
vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
|
|
224
|
-
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
|
|
225
|
-
gl_PointSize = a_size;
|
|
226
|
-
v_color = a_color;
|
|
227
|
-
}
|
|
228
|
-
`;
|
|
229
|
-
|
|
230
|
-
const fragmentShaderSource = `
|
|
231
|
-
precision mediump float;
|
|
232
|
-
varying vec4 v_color;
|
|
233
|
-
|
|
234
|
-
void main() {
|
|
235
|
-
// Circular point sprite
|
|
236
|
-
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
237
|
-
if (length(coord) > 0.5) {
|
|
238
|
-
discard;
|
|
239
|
-
}
|
|
240
|
-
gl_FragColor = v_color;
|
|
241
|
-
}
|
|
242
|
-
`;
|
|
243
|
-
|
|
244
|
-
const vertexShader = this._compileShader(this.gl.VERTEX_SHADER, vertexShaderSource);
|
|
245
|
-
const fragmentShader = this._compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSource);
|
|
246
|
-
|
|
247
|
-
this.program = this.gl.createProgram();
|
|
248
|
-
this.gl.attachShader(this.program, vertexShader);
|
|
249
|
-
this.gl.attachShader(this.program, fragmentShader);
|
|
250
|
-
this.gl.linkProgram(this.program);
|
|
251
|
-
|
|
252
|
-
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
|
|
253
|
-
console.error('[WebGLParticles] Shader program failed to link');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
_compileShader(type, source) {
|
|
258
|
-
const shader = this.gl.createShader(type);
|
|
259
|
-
this.gl.shaderSource(shader, source);
|
|
260
|
-
this.gl.compileShader(shader);
|
|
261
|
-
|
|
262
|
-
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
263
|
-
console.error('[WebGLParticles] Shader compilation error:', this.gl.getShaderInfoLog(shader));
|
|
264
|
-
this.gl.deleteShader(shader);
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return shader;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
_createBuffers() {
|
|
272
|
-
this.particleBuffer = this.gl.createBuffer();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
_render() {
|
|
276
|
-
const gl = this.gl;
|
|
277
|
-
|
|
278
|
-
// Clear canvas
|
|
279
|
-
gl.clearColor(0, 0, 0, 0);
|
|
280
|
-
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
281
|
-
|
|
282
|
-
if (this.particleCount === 0) return;
|
|
283
|
-
|
|
284
|
-
// Use shader program
|
|
285
|
-
gl.useProgram(this.program);
|
|
286
|
-
|
|
287
|
-
// Set resolution uniform
|
|
288
|
-
const resolutionLocation = gl.getUniformLocation(this.program, 'u_resolution');
|
|
289
|
-
gl.uniform2f(resolutionLocation, this.canvas.width, this.canvas.height);
|
|
290
|
-
|
|
291
|
-
// Interleave particle data for GPU
|
|
292
|
-
const vertexData = new Float32Array(this.particleCount * 7); // x, y, r, g, b, a, size
|
|
293
|
-
for (let i = 0; i < this.particleCount; i++) {
|
|
294
|
-
vertexData[i * 7] = this.positions[i * 2];
|
|
295
|
-
vertexData[i * 7 + 1] = this.positions[i * 2 + 1];
|
|
296
|
-
vertexData[i * 7 + 2] = this.colors[i * 4];
|
|
297
|
-
vertexData[i * 7 + 3] = this.colors[i * 4 + 1];
|
|
298
|
-
vertexData[i * 7 + 4] = this.colors[i * 4 + 2];
|
|
299
|
-
vertexData[i * 7 + 5] = this.colors[i * 4 + 3];
|
|
300
|
-
vertexData[i * 7 + 6] = this.sizes[i];
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Upload to GPU
|
|
304
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.particleBuffer);
|
|
305
|
-
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.DYNAMIC_DRAW);
|
|
306
|
-
|
|
307
|
-
// Setup attributes
|
|
308
|
-
const stride = 7 * 4; // 7 floats * 4 bytes
|
|
309
|
-
|
|
310
|
-
const positionLocation = gl.getAttribLocation(this.program, 'a_position');
|
|
311
|
-
gl.enableVertexAttribArray(positionLocation);
|
|
312
|
-
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, stride, 0);
|
|
313
|
-
|
|
314
|
-
const colorLocation = gl.getAttribLocation(this.program, 'a_color');
|
|
315
|
-
gl.enableVertexAttribArray(colorLocation);
|
|
316
|
-
gl.vertexAttribPointer(colorLocation, 4, gl.FLOAT, false, stride, 2 * 4);
|
|
317
|
-
|
|
318
|
-
const sizeLocation = gl.getAttribLocation(this.program, 'a_size');
|
|
319
|
-
gl.enableVertexAttribArray(sizeLocation);
|
|
320
|
-
gl.vertexAttribPointer(sizeLocation, 1, gl.FLOAT, false, stride, 6 * 4);
|
|
321
|
-
|
|
322
|
-
// Draw particles
|
|
323
|
-
gl.drawArrays(gl.POINTS, 0, this.particleCount);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
_removeParticle(index) {
|
|
327
|
-
// Swap with last particle and decrease count
|
|
328
|
-
const last = this.particleCount - 1;
|
|
329
|
-
if (index !== last) {
|
|
330
|
-
// Copy last particle to this index
|
|
331
|
-
this.positions[index * 2] = this.positions[last * 2];
|
|
332
|
-
this.positions[index * 2 + 1] = this.positions[last * 2 + 1];
|
|
333
|
-
this.velocities[index * 2] = this.velocities[last * 2];
|
|
334
|
-
this.velocities[index * 2 + 1] = this.velocities[last * 2 + 1];
|
|
335
|
-
this.colors[index * 4] = this.colors[last * 4];
|
|
336
|
-
this.colors[index * 4 + 1] = this.colors[last * 4 + 1];
|
|
337
|
-
this.colors[index * 4 + 2] = this.colors[last * 4 + 2];
|
|
338
|
-
this.colors[index * 4 + 3] = this.colors[last * 4 + 3];
|
|
339
|
-
this.sizes[index] = this.sizes[last];
|
|
340
|
-
this.lifetimes[index] = this.lifetimes[last];
|
|
341
|
-
this.ages[index] = this.ages[last];
|
|
342
|
-
}
|
|
343
|
-
this.particleCount--;
|
|
344
|
-
}
|
|
345
|
-
}
|