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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Reusable UI elements for XP bars, achievement popups, level-up screens, etc.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const OVERLAY_ID = 'game-ui-overlay';
|
|
7
|
+
|
|
6
8
|
export class GameUI {
|
|
7
9
|
constructor(particleSystem) {
|
|
8
10
|
this.particleSystem = particleSystem;
|
|
@@ -10,6 +12,12 @@ export class GameUI {
|
|
|
10
12
|
this.xpBar = null;
|
|
11
13
|
this.levelDisplay = null;
|
|
12
14
|
this.streakDisplay = null;
|
|
15
|
+
|
|
16
|
+
// Everything this instance put in the DOM or scheduled, so destroy()
|
|
17
|
+
// can take it all back out again.
|
|
18
|
+
this._timers = new Set();
|
|
19
|
+
this._detached = [];
|
|
20
|
+
|
|
13
21
|
this.init();
|
|
14
22
|
}
|
|
15
23
|
|
|
@@ -19,50 +27,92 @@ export class GameUI {
|
|
|
19
27
|
init() {
|
|
20
28
|
// Create main container
|
|
21
29
|
this.container = document.createElement('div');
|
|
22
|
-
this.container.
|
|
30
|
+
this.container.className = 'game-ui-overlay';
|
|
31
|
+
|
|
32
|
+
// The stylesheet targets #game-ui-overlay. Claim the id only if it is
|
|
33
|
+
// free, so a second instance cannot produce a duplicate id.
|
|
34
|
+
if (!document.getElementById(OVERLAY_ID)) {
|
|
35
|
+
this.container.id = OVERLAY_ID;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
this.container.innerHTML = `
|
|
24
39
|
<div class="game-ui-top-bar">
|
|
25
|
-
<div class="level-badge"
|
|
40
|
+
<div class="level-badge">
|
|
26
41
|
<span class="level-label">LVL</span>
|
|
27
|
-
<span class="level-number"
|
|
42
|
+
<span class="level-number">1</span>
|
|
28
43
|
</div>
|
|
29
|
-
|
|
44
|
+
|
|
30
45
|
<div class="xp-container">
|
|
31
46
|
<div class="xp-bar-bg">
|
|
32
|
-
<div class="xp-bar-fill"
|
|
47
|
+
<div class="xp-bar-fill" style="width: 0%"></div>
|
|
33
48
|
</div>
|
|
34
|
-
<div class="xp-text"
|
|
49
|
+
<div class="xp-text">0 / 100 XP</div>
|
|
35
50
|
</div>
|
|
36
|
-
|
|
37
|
-
<div class="streak-badge"
|
|
51
|
+
|
|
52
|
+
<div class="streak-badge">
|
|
38
53
|
<span class="streak-icon">🔥</span>
|
|
39
|
-
<span class="streak-number"
|
|
54
|
+
<span class="streak-number">1</span>
|
|
40
55
|
</div>
|
|
41
56
|
</div>
|
|
42
57
|
`;
|
|
43
58
|
|
|
44
59
|
document.body.appendChild(this.container);
|
|
45
60
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
61
|
+
// Scoped to this instance. Document-wide getElementById would make a
|
|
62
|
+
// second GameUI silently rebind every element to the first one's DOM.
|
|
63
|
+
this.levelBadge = this.container.querySelector('.level-badge');
|
|
64
|
+
this.streakBadge = this.container.querySelector('.streak-badge');
|
|
65
|
+
this.xpBar = this.container.querySelector('.xp-bar-fill');
|
|
66
|
+
this.xpText = this.container.querySelector('.xp-text');
|
|
67
|
+
this.levelDisplay = this.container.querySelector('.level-number');
|
|
68
|
+
this.streakDisplay = this.container.querySelector('.streak-number');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* setTimeout that destroy() can cancel.
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
_defer(fn, delay) {
|
|
76
|
+
const id = setTimeout(() => {
|
|
77
|
+
this._timers.delete(id);
|
|
78
|
+
fn();
|
|
79
|
+
}, delay);
|
|
80
|
+
this._timers.add(id);
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Append a transient element and remember it for cleanup.
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
_mount(el) {
|
|
89
|
+
document.body.appendChild(el);
|
|
90
|
+
this._detached.push(el);
|
|
91
|
+
return el;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Replay a CSS animation by clearing it for one tick.
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
_replayAnimation(el, animation) {
|
|
99
|
+
if (!el) return;
|
|
100
|
+
el.style.animation = 'none';
|
|
101
|
+
this._defer(() => { el.style.animation = animation; }, 10);
|
|
51
102
|
}
|
|
52
103
|
|
|
53
104
|
/**
|
|
54
105
|
* Update XP bar
|
|
106
|
+
* @param {number} current - XP held right now
|
|
107
|
+
* @param {number} needed - XP still required for the next level
|
|
108
|
+
* @param {number} total - XP total that marks the next level
|
|
55
109
|
*/
|
|
56
110
|
updateXP(current, needed, total) {
|
|
57
|
-
const progress = (current / total) * 100;
|
|
58
|
-
this.xpBar.style.width = `${Math.min(100, progress)}%`;
|
|
111
|
+
const progress = total > 0 ? (current / total) * 100 : 0;
|
|
112
|
+
this.xpBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
|
59
113
|
this.xpText.textContent = `${current} / ${total} XP`;
|
|
60
114
|
|
|
61
|
-
|
|
62
|
-
this.xpBar.style.animation = 'none';
|
|
63
|
-
setTimeout(() => {
|
|
64
|
-
this.xpBar.style.animation = 'xp-pulse 0.3s ease-out';
|
|
65
|
-
}, 10);
|
|
115
|
+
this._replayAnimation(this.xpBar, 'xp-pulse 0.3s ease-out');
|
|
66
116
|
}
|
|
67
117
|
|
|
68
118
|
/**
|
|
@@ -70,13 +120,7 @@ export class GameUI {
|
|
|
70
120
|
*/
|
|
71
121
|
updateLevel(level) {
|
|
72
122
|
this.levelDisplay.textContent = level;
|
|
73
|
-
|
|
74
|
-
// Pulse animation
|
|
75
|
-
const badge = document.getElementById('level-badge');
|
|
76
|
-
badge.style.animation = 'none';
|
|
77
|
-
setTimeout(() => {
|
|
78
|
-
badge.style.animation = 'level-pulse 0.5s ease-out';
|
|
79
|
-
}, 10);
|
|
123
|
+
this._replayAnimation(this.levelBadge, 'level-pulse 0.5s ease-out');
|
|
80
124
|
}
|
|
81
125
|
|
|
82
126
|
/**
|
|
@@ -86,16 +130,20 @@ export class GameUI {
|
|
|
86
130
|
this.streakDisplay.textContent = days;
|
|
87
131
|
|
|
88
132
|
// Change color based on streak
|
|
89
|
-
const badge = document.getElementById('streak-badge');
|
|
90
133
|
if (days >= 7) {
|
|
91
|
-
|
|
134
|
+
this.streakBadge.style.background = 'linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)';
|
|
92
135
|
} else if (days >= 3) {
|
|
93
|
-
|
|
136
|
+
this.streakBadge.style.background = 'linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)';
|
|
94
137
|
}
|
|
95
138
|
}
|
|
96
139
|
|
|
97
140
|
/**
|
|
98
141
|
* Show achievement unlocked popup
|
|
142
|
+
*
|
|
143
|
+
* `icon` is rendered as HTML by design (documented in ARCHITECTURE.md, so
|
|
144
|
+
* games can pass an <img> or inline SVG). Every other field is game state
|
|
145
|
+
* and could carry a player-supplied or server-supplied string, so it goes
|
|
146
|
+
* in as text.
|
|
99
147
|
*/
|
|
100
148
|
showAchievement(achievement) {
|
|
101
149
|
const popup = document.createElement('div');
|
|
@@ -103,28 +151,33 @@ export class GameUI {
|
|
|
103
151
|
popup.innerHTML = `
|
|
104
152
|
<div class="achievement-shine"></div>
|
|
105
153
|
<div class="achievement-content">
|
|
106
|
-
<div class="achievement-icon"
|
|
154
|
+
<div class="achievement-icon"></div>
|
|
107
155
|
<div class="achievement-info">
|
|
108
156
|
<div class="achievement-title">Achievement Unlocked!</div>
|
|
109
|
-
<div class="achievement-name"
|
|
110
|
-
<div class="achievement-desc"
|
|
111
|
-
<div class="achievement-xp"
|
|
157
|
+
<div class="achievement-name"></div>
|
|
158
|
+
<div class="achievement-desc"></div>
|
|
159
|
+
<div class="achievement-xp"></div>
|
|
112
160
|
</div>
|
|
113
161
|
</div>
|
|
114
162
|
`;
|
|
115
163
|
|
|
116
|
-
|
|
164
|
+
popup.querySelector('.achievement-icon').innerHTML = achievement.icon ?? '';
|
|
165
|
+
popup.querySelector('.achievement-name').textContent = achievement.name ?? '';
|
|
166
|
+
popup.querySelector('.achievement-desc').textContent = achievement.description ?? '';
|
|
167
|
+
popup.querySelector('.achievement-xp').textContent = `+${achievement.xp ?? 0} XP`;
|
|
168
|
+
|
|
169
|
+
this._mount(popup);
|
|
117
170
|
|
|
118
171
|
// Trigger confetti at popup location
|
|
119
|
-
|
|
172
|
+
this._defer(() => {
|
|
120
173
|
const rect = popup.getBoundingClientRect();
|
|
121
174
|
this.particleSystem.confetti(rect.left + rect.width / 2, rect.top + rect.height / 2, 40);
|
|
122
175
|
}, 300);
|
|
123
176
|
|
|
124
177
|
// Remove after animation
|
|
125
|
-
|
|
178
|
+
this._defer(() => {
|
|
126
179
|
popup.style.animation = 'slideOut 0.3s ease-in forwards';
|
|
127
|
-
|
|
180
|
+
this._defer(() => this._remove(popup), 300);
|
|
128
181
|
}, 4000);
|
|
129
182
|
}
|
|
130
183
|
|
|
@@ -137,17 +190,18 @@ export class GameUI {
|
|
|
137
190
|
overlay.innerHTML = `
|
|
138
191
|
<div class="level-up-content">
|
|
139
192
|
<div class="level-up-title">LEVEL UP!</div>
|
|
140
|
-
<div class="level-up-number"
|
|
193
|
+
<div class="level-up-number"></div>
|
|
141
194
|
<div class="level-up-subtitle">Amazing progress!</div>
|
|
142
195
|
</div>
|
|
143
196
|
`;
|
|
197
|
+
overlay.querySelector('.level-up-number').textContent = newLevel;
|
|
144
198
|
|
|
145
|
-
|
|
199
|
+
this._mount(overlay);
|
|
146
200
|
|
|
147
201
|
// Fireworks effect
|
|
148
|
-
|
|
202
|
+
this._defer(() => {
|
|
149
203
|
for (let i = 0; i < 5; i++) {
|
|
150
|
-
|
|
204
|
+
this._defer(() => {
|
|
151
205
|
const x = Math.random() * window.innerWidth;
|
|
152
206
|
const y = Math.random() * window.innerHeight * 0.6;
|
|
153
207
|
this.particleSystem.confetti(x, y, 30);
|
|
@@ -156,9 +210,9 @@ export class GameUI {
|
|
|
156
210
|
}, 300);
|
|
157
211
|
|
|
158
212
|
// Remove after 3 seconds
|
|
159
|
-
|
|
213
|
+
this._defer(() => {
|
|
160
214
|
overlay.style.opacity = '0';
|
|
161
|
-
|
|
215
|
+
this._defer(() => this._remove(overlay), 500);
|
|
162
216
|
}, 2500);
|
|
163
217
|
}
|
|
164
218
|
|
|
@@ -174,9 +228,9 @@ export class GameUI {
|
|
|
174
228
|
floater.style.color = color;
|
|
175
229
|
floater.style.fontSize = size;
|
|
176
230
|
|
|
177
|
-
|
|
231
|
+
this._mount(floater);
|
|
178
232
|
|
|
179
|
-
|
|
233
|
+
this._defer(() => this._remove(floater), 2000);
|
|
180
234
|
}
|
|
181
235
|
|
|
182
236
|
/**
|
|
@@ -188,7 +242,7 @@ export class GameUI {
|
|
|
188
242
|
}
|
|
189
243
|
|
|
190
244
|
/**
|
|
191
|
-
* Show "near miss"indicator
|
|
245
|
+
* Show "near miss" indicator
|
|
192
246
|
*/
|
|
193
247
|
showNearMiss(x, y) {
|
|
194
248
|
this.showFloatingText('CLOSE CALL!', x, y, '#45b7d1', '20px');
|
|
@@ -222,6 +276,12 @@ export class GameUI {
|
|
|
222
276
|
|
|
223
277
|
/**
|
|
224
278
|
* Show Game Over Summary
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} data
|
|
281
|
+
* @param {number|string} data.score - Final score
|
|
282
|
+
* @param {Object} [data.metrics] - Label/value pairs listed under the score
|
|
283
|
+
* @param {Function} [data.onReplay] - Replay button handler (default: reload)
|
|
284
|
+
* @param {Function} [data.onExit] - Exit button handler (button hidden if omitted)
|
|
225
285
|
*/
|
|
226
286
|
showSummary(data) {
|
|
227
287
|
const overlay = document.createElement('div');
|
|
@@ -233,23 +293,85 @@ export class GameUI {
|
|
|
233
293
|
overlay.style.transform = 'translate(-50%, -50%)';
|
|
234
294
|
overlay.style.zIndex = '1000';
|
|
235
295
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
296
|
+
const heading = document.createElement('h1');
|
|
297
|
+
heading.textContent = 'Game Over';
|
|
298
|
+
|
|
299
|
+
const score = document.createElement('div');
|
|
300
|
+
score.className = 'summary-score';
|
|
301
|
+
score.style.cssText = 'font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;';
|
|
302
|
+
score.textContent = data.score;
|
|
303
|
+
|
|
304
|
+
const metrics = document.createElement('div');
|
|
305
|
+
metrics.className = 'summary-metrics';
|
|
306
|
+
metrics.style.cssText = 'width: 100%; margin-bottom: 20px;';
|
|
307
|
+
|
|
308
|
+
for (const [key, value] of Object.entries(data.metrics || {})) {
|
|
309
|
+
const row = document.createElement('div');
|
|
310
|
+
row.style.cssText = 'display:flex;justify-content:space-between;width:100%';
|
|
311
|
+
|
|
312
|
+
const label = document.createElement('span');
|
|
313
|
+
label.textContent = `${key}:`;
|
|
314
|
+
|
|
315
|
+
const amount = document.createElement('strong');
|
|
316
|
+
amount.textContent = value;
|
|
317
|
+
|
|
318
|
+
row.append(label, amount);
|
|
319
|
+
metrics.appendChild(row);
|
|
241
320
|
}
|
|
242
321
|
|
|
243
|
-
overlay.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
322
|
+
overlay.append(heading, score, metrics);
|
|
323
|
+
|
|
324
|
+
// addEventListener rather than an onclick attribute: inline handlers
|
|
325
|
+
// are blocked by any script-src CSP without 'unsafe-inline'.
|
|
326
|
+
const replay = document.createElement('button');
|
|
327
|
+
replay.className = 'btn';
|
|
328
|
+
replay.textContent = 'Play Again';
|
|
329
|
+
replay.addEventListener('click', data.onReplay || (() => window.location.reload()));
|
|
330
|
+
overlay.appendChild(replay);
|
|
252
331
|
|
|
253
|
-
|
|
332
|
+
if (data.onExit) {
|
|
333
|
+
const exit = document.createElement('button');
|
|
334
|
+
exit.className = 'btn';
|
|
335
|
+
exit.textContent = 'Exit';
|
|
336
|
+
exit.style.cssText = 'margin-top: 10px; background: #543847;';
|
|
337
|
+
exit.addEventListener('click', data.onExit);
|
|
338
|
+
overlay.appendChild(exit);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this._mount(overlay);
|
|
342
|
+
return overlay;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
_remove(el) {
|
|
349
|
+
el.remove();
|
|
350
|
+
const i = this._detached.indexOf(el);
|
|
351
|
+
if (i > -1) this._detached.splice(i, 1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Tear down the overlay, any transient popups, and all pending timers.
|
|
356
|
+
*/
|
|
357
|
+
destroy() {
|
|
358
|
+
for (const id of this._timers) {
|
|
359
|
+
clearTimeout(id);
|
|
360
|
+
}
|
|
361
|
+
this._timers.clear();
|
|
362
|
+
|
|
363
|
+
for (const el of this._detached) {
|
|
364
|
+
el.remove();
|
|
365
|
+
}
|
|
366
|
+
this._detached = [];
|
|
367
|
+
|
|
368
|
+
this.container?.remove();
|
|
369
|
+
this.container = null;
|
|
370
|
+
this.xpBar = null;
|
|
371
|
+
this.xpText = null;
|
|
372
|
+
this.levelBadge = null;
|
|
373
|
+
this.streakBadge = null;
|
|
374
|
+
this.levelDisplay = null;
|
|
375
|
+
this.streakDisplay = null;
|
|
254
376
|
}
|
|
255
377
|
}
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
/* ==================== TOP BAR (XP, Level, Streak) ==================== */
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/* The id is kept so existing `#game-ui-overlay` overrides still win on
|
|
10
|
+
specificity; the class is what every instance actually carries. */
|
|
11
|
+
#game-ui-overlay,
|
|
12
|
+
.game-ui-overlay {
|
|
10
13
|
position: fixed;
|
|
11
14
|
top: 0;
|
|
12
15
|
left: 0;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage resolution for environments that may not have localStorage.
|
|
3
|
+
*
|
|
4
|
+
* Three cases need to work:
|
|
5
|
+
* - Browser: localStorage.
|
|
6
|
+
* - SSR / worker / test runner: no `window` at all, so importing the package
|
|
7
|
+
* must not throw at module or constructor time.
|
|
8
|
+
* - Safari private browsing and storage-blocked iframes: localStorage exists
|
|
9
|
+
* but every write throws QuotaExceededError.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A Storage-shaped object backed by a Map. Values do not survive a reload,
|
|
14
|
+
* which is the correct degradation: the game runs, progress just is not kept.
|
|
15
|
+
*
|
|
16
|
+
* @returns {{getItem: Function, setItem: Function, removeItem: Function, clear: Function}}
|
|
17
|
+
*/
|
|
18
|
+
export function createMemoryStorage() {
|
|
19
|
+
const map = new Map();
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
getItem: (key) => (map.has(key) ? map.get(key) : null),
|
|
23
|
+
setItem: (key, value) => { map.set(key, String(value)); },
|
|
24
|
+
removeItem: (key) => { map.delete(key); },
|
|
25
|
+
clear: () => { map.clear(); }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return localStorage if it is present and writable, otherwise a memory store.
|
|
31
|
+
*
|
|
32
|
+
* @returns {Object} Storage-shaped object
|
|
33
|
+
*/
|
|
34
|
+
export function resolveStorage() {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof localStorage === 'undefined') {
|
|
37
|
+
return createMemoryStorage();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Presence is not enough: blocked contexts throw only on write.
|
|
41
|
+
const probe = '__dopamine_probe__';
|
|
42
|
+
localStorage.setItem(probe, '1');
|
|
43
|
+
localStorage.removeItem(probe);
|
|
44
|
+
|
|
45
|
+
return localStorage;
|
|
46
|
+
} catch {
|
|
47
|
+
return createMemoryStorage();
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,12 @@ export { PluginRegistry } from './core/PluginRegistry.js';
|
|
|
12
12
|
// Export System Interfaces
|
|
13
13
|
export * from './interfaces/index.js';
|
|
14
14
|
|
|
15
|
+
// The emitter RewardSystem extends, so consumers can type their listeners.
|
|
16
|
+
export { EventEmitter } from './dopamine/core/EventEmitter.js';
|
|
17
|
+
|
|
18
|
+
// Storage resolution, useful for tests and for SSR where localStorage is absent.
|
|
19
|
+
export { createMemoryStorage, resolveStorage } from './dopamine/utils/storage.js';
|
|
20
|
+
|
|
15
21
|
// Export New Engine Core
|
|
16
22
|
export { Game } from './core/Game.js';
|
|
17
23
|
export { Scene } from './core/Scene.js';
|
package/src/systems/Input.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { deprecatedGlobal } from './deprecate.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Input System
|
|
3
5
|
* Tracks keyboard and mouse input.
|
|
@@ -71,7 +73,9 @@ export class Input {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
// DEPRECATED:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// DEPRECATED: kept for v1 compatibility. Warns on first use, not on import.
|
|
77
|
+
// Note: listeners are not attached unless init() is called.
|
|
78
|
+
export const GlobalInput = deprecatedGlobal(
|
|
79
|
+
new Input(),
|
|
80
|
+
'[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.'
|
|
81
|
+
);
|
package/src/systems/Loader.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { deprecatedGlobal } from './deprecate.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Asset Loader
|
|
3
5
|
* Loads and caches images, audio, etc.
|
|
@@ -39,6 +41,8 @@ export class Loader {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
// DEPRECATED:
|
|
43
|
-
export const GlobalLoader =
|
|
44
|
-
|
|
44
|
+
// DEPRECATED: kept for v1 compatibility. Warns on first use, not on import.
|
|
45
|
+
export const GlobalLoader = deprecatedGlobal(
|
|
46
|
+
new Loader(),
|
|
47
|
+
'[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.'
|
|
48
|
+
);
|
package/src/systems/Physics.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { deprecatedGlobal } from './deprecate.js';
|
|
2
|
+
|
|
1
3
|
import { Collider } from '../core/Collider.js';
|
|
2
4
|
|
|
3
5
|
export class Physics {
|
|
@@ -112,6 +114,8 @@ export class Physics {
|
|
|
112
114
|
setGravity(x, y) { /* TODO: Add gravity support */ }
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
// DEPRECATED:
|
|
116
|
-
export const GlobalPhysics =
|
|
117
|
-
|
|
117
|
+
// DEPRECATED: kept for v1 compatibility. Warns on first use, not on import.
|
|
118
|
+
export const GlobalPhysics = deprecatedGlobal(
|
|
119
|
+
new Physics(),
|
|
120
|
+
'[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.'
|
|
121
|
+
);
|
package/src/systems/Ticker.js
CHANGED
|
@@ -7,6 +7,7 @@ export class Ticker {
|
|
|
7
7
|
this.running = false;
|
|
8
8
|
this.lastTime = 0;
|
|
9
9
|
this.callbacks = new Set();
|
|
10
|
+
this._frameHandle = null;
|
|
10
11
|
this._tick = this._tick.bind(this);
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -33,7 +34,14 @@ export class Ticker {
|
|
|
33
34
|
if (this.running) return;
|
|
34
35
|
this.running = true;
|
|
35
36
|
this.lastTime = performance.now();
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
// Cancel first: a frame queued before the last stop() may still be
|
|
39
|
+
// pending, and letting it through would leave two live loops running
|
|
40
|
+
// the game at double speed.
|
|
41
|
+
if (this._frameHandle !== null) {
|
|
42
|
+
cancelAnimationFrame(this._frameHandle);
|
|
43
|
+
}
|
|
44
|
+
this._frameHandle = requestAnimationFrame(this._tick);
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/**
|
|
@@ -41,9 +49,16 @@ export class Ticker {
|
|
|
41
49
|
*/
|
|
42
50
|
stop() {
|
|
43
51
|
this.running = false;
|
|
52
|
+
|
|
53
|
+
if (this._frameHandle !== null) {
|
|
54
|
+
cancelAnimationFrame(this._frameHandle);
|
|
55
|
+
this._frameHandle = null;
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
_tick(time) {
|
|
60
|
+
this._frameHandle = null;
|
|
61
|
+
|
|
47
62
|
if (!this.running) return;
|
|
48
63
|
|
|
49
64
|
const dt = (time - this.lastTime) / 1000; // Delta time in seconds
|
|
@@ -52,10 +67,14 @@ export class Ticker {
|
|
|
52
67
|
// Cap dt to prevent huge jumps if tab was inactive
|
|
53
68
|
const safeDt = Math.min(dt, 0.1);
|
|
54
69
|
|
|
55
|
-
|
|
70
|
+
// Snapshot: a callback may add or remove callbacks mid-frame.
|
|
71
|
+
for (const callback of [...this.callbacks]) {
|
|
56
72
|
callback(safeDt);
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
|
|
75
|
+
// A callback may have called stop(); don't queue another frame if so.
|
|
76
|
+
if (this.running) {
|
|
77
|
+
this._frameHandle = requestAnimationFrame(this._tick);
|
|
78
|
+
}
|
|
60
79
|
}
|
|
61
80
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deprecation helper for the v1 global singletons.
|
|
3
|
+
*
|
|
4
|
+
* These used to warn at module scope, which meant importing the package
|
|
5
|
+
* printed a warning for every legacy global whether or not the consumer
|
|
6
|
+
* touched one. A deprecation notice should cost you something only when you
|
|
7
|
+
* use the deprecated thing, otherwise it is just noise that trains people to
|
|
8
|
+
* ignore warnings.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} instance - The real system to delegate to
|
|
11
|
+
* @param {string} message - Warning text, emitted once on first use
|
|
12
|
+
* @returns {Object} A proxy that warns on first property access
|
|
13
|
+
*/
|
|
14
|
+
export function deprecatedGlobal(instance, message) {
|
|
15
|
+
let warned = false;
|
|
16
|
+
|
|
17
|
+
return new Proxy(instance, {
|
|
18
|
+
get(target, prop, receiver) {
|
|
19
|
+
if (!warned) {
|
|
20
|
+
warned = true;
|
|
21
|
+
console.warn(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const value = Reflect.get(target, prop, receiver);
|
|
25
|
+
// Methods must stay bound to the real instance, not the proxy,
|
|
26
|
+
// or internal `this` access re-enters the trap on every field.
|
|
27
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
set(target, prop, value) {
|
|
31
|
+
if (!warned) {
|
|
32
|
+
warned = true;
|
|
33
|
+
console.warn(message);
|
|
34
|
+
}
|
|
35
|
+
return Reflect.set(target, prop, value);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|