dopaminejs 2.1.0 → 2.3.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/README.md +10 -10
- package/dist/dopamine.cjs +37 -0
- package/dist/dopamine.js +632 -1378
- package/dist/engine.cjs +1 -0
- package/dist/engine.js +1158 -0
- package/dist/style.css +1 -1
- package/package.json +31 -14
- package/src/core/Collider.js +43 -7
- package/src/core/PluginRegistry.js +84 -7
- package/src/core/Scene.js +50 -5
- package/src/core/SystemRegistry.js +32 -11
- package/src/dopamine/audio/SoundManager.js +91 -20
- package/src/dopamine/components/ScreenShake.js +7 -6
- package/src/dopamine/components/index.js +0 -2
- package/src/dopamine/core/DataService.js +31 -3
- package/src/dopamine/core/RewardSystem.js +255 -25
- package/src/dopamine/effects/ParticleSystem.js +72 -15
- package/src/dopamine/index.js +56 -24
- package/src/dopamine/ui/GameUI.js +48 -5
- package/src/dopamine/ui/dopamine.css +18 -0
- package/src/engine.js +38 -0
- package/src/index.js +6 -0
- package/src/systems/Director.js +7 -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 +14 -3
- package/src/systems/deprecate.js +38 -0
- package/types/index.d.ts +617 -0
- package/dist/dopamine.umd.cjs +0 -37
package/src/dopamine/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export default class Dopamine {
|
|
|
24
24
|
|
|
25
25
|
async init() {
|
|
26
26
|
await this.rewardSystem.init();
|
|
27
|
+
this._syncUI();
|
|
27
28
|
return {
|
|
28
29
|
rewardSystem: this.rewardSystem,
|
|
29
30
|
gameUI: this.gameUI,
|
|
@@ -32,30 +33,61 @@ export default class Dopamine {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Draw the loaded player. Without this the overlay keeps its placeholder
|
|
38
|
+
* (level 1, 0 XP, streak 1) until the first event arrives.
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
_syncUI() {
|
|
42
|
+
const { player } = this.rewardSystem;
|
|
43
|
+
const { total, needed, progress } = this.rewardSystem.getXPForNextLevel();
|
|
44
|
+
|
|
45
|
+
this.gameUI.updateXP(player.xp, needed, total, progress);
|
|
46
|
+
this.gameUI.updateLevel(player.level);
|
|
47
|
+
this.gameUI.updateStreak(player.streak.current);
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
_bindEvents() {
|
|
36
|
-
this.rewardSystem
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
const rewards = this.rewardSystem;
|
|
52
|
+
|
|
53
|
+
this._unsubscribe = [
|
|
54
|
+
rewards.on('xp_gained', () => {
|
|
55
|
+
const { total, needed, progress } = rewards.getXPForNextLevel();
|
|
56
|
+
this.gameUI.updateXP(rewards.player.xp, needed, total, progress);
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
rewards.on('level_up', (data) => {
|
|
60
|
+
this.gameUI.updateLevel(data.newLevel);
|
|
61
|
+
this.gameUI.showLevelUp(data.oldLevel, data.newLevel);
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
rewards.on('achievement_unlocked', (achievement) => {
|
|
65
|
+
this.gameUI.showAchievement(achievement);
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
rewards.on('streak_updated', (data) => {
|
|
69
|
+
this.gameUI.updateStreak(data.current);
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
rewards.on('new_high_score', (data) => {
|
|
73
|
+
this.gameUI.showNotification(`New High Score: ${data.score}!`, 'legendary');
|
|
74
|
+
this.soundManager.playSuccess();
|
|
75
|
+
})
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Remove the overlay and canvas, stop audio, and detach from the reward
|
|
81
|
+
* system. The reward system itself stays usable.
|
|
82
|
+
*/
|
|
83
|
+
destroy() {
|
|
84
|
+
for (const unsubscribe of this._unsubscribe) {
|
|
85
|
+
unsubscribe();
|
|
86
|
+
}
|
|
87
|
+
this._unsubscribe = [];
|
|
88
|
+
|
|
89
|
+
this.gameUI.destroy();
|
|
90
|
+
this.particleSystem.destroy();
|
|
91
|
+
this.soundManager.destroy();
|
|
60
92
|
}
|
|
61
93
|
}
|
|
@@ -56,6 +56,22 @@ export class GameUI {
|
|
|
56
56
|
</div>
|
|
57
57
|
`;
|
|
58
58
|
|
|
59
|
+
// Popups are decorative and short-lived, so a screen reader would
|
|
60
|
+
// miss them. This region repeats them as plain text.
|
|
61
|
+
this.liveRegion = document.createElement('div');
|
|
62
|
+
this.liveRegion.className = 'dopamine-live-region';
|
|
63
|
+
this.liveRegion.setAttribute('aria-live', 'polite');
|
|
64
|
+
this.liveRegion.setAttribute('role', 'status');
|
|
65
|
+
Object.assign(this.liveRegion.style, {
|
|
66
|
+
position: 'absolute',
|
|
67
|
+
width: '1px',
|
|
68
|
+
height: '1px',
|
|
69
|
+
overflow: 'hidden',
|
|
70
|
+
clipPath: 'inset(50%)',
|
|
71
|
+
whiteSpace: 'nowrap'
|
|
72
|
+
});
|
|
73
|
+
this.container.appendChild(this.liveRegion);
|
|
74
|
+
|
|
59
75
|
document.body.appendChild(this.container);
|
|
60
76
|
|
|
61
77
|
// Scoped to this instance. Document-wide getElementById would make a
|
|
@@ -91,6 +107,16 @@ export class GameUI {
|
|
|
91
107
|
return el;
|
|
92
108
|
}
|
|
93
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Repeat a popup as text for assistive technology.
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
_announce(message) {
|
|
115
|
+
if (this.liveRegion) {
|
|
116
|
+
this.liveRegion.textContent = message;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
94
120
|
/**
|
|
95
121
|
* Replay a CSS animation by clearing it for one tick.
|
|
96
122
|
* @private
|
|
@@ -106,10 +132,15 @@ export class GameUI {
|
|
|
106
132
|
* @param {number} current - XP held right now
|
|
107
133
|
* @param {number} needed - XP still required for the next level
|
|
108
134
|
* @param {number} total - XP total that marks the next level
|
|
135
|
+
* @param {number} [progress] - Position within the current level, 0 to 1.
|
|
136
|
+
* Without it the bar falls back to current / total, which is lifetime
|
|
137
|
+
* XP and starts every level after the first partly full.
|
|
109
138
|
*/
|
|
110
|
-
updateXP(current, needed, total) {
|
|
111
|
-
const
|
|
112
|
-
|
|
139
|
+
updateXP(current, needed, total, progress) {
|
|
140
|
+
const fraction = Number.isFinite(progress)
|
|
141
|
+
? progress
|
|
142
|
+
: (total > 0 ? current / total : 0);
|
|
143
|
+
this.xpBar.style.width = `${Math.max(0, Math.min(100, fraction * 100))}%`;
|
|
113
144
|
this.xpText.textContent = `${current} / ${total} XP`;
|
|
114
145
|
|
|
115
146
|
this._replayAnimation(this.xpBar, 'xp-pulse 0.3s ease-out');
|
|
@@ -134,6 +165,8 @@ export class GameUI {
|
|
|
134
165
|
this.streakBadge.style.background = 'linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)';
|
|
135
166
|
} else if (days >= 3) {
|
|
136
167
|
this.streakBadge.style.background = 'linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)';
|
|
168
|
+
} else {
|
|
169
|
+
this.streakBadge.style.background = '';
|
|
137
170
|
}
|
|
138
171
|
}
|
|
139
172
|
|
|
@@ -167,6 +200,7 @@ export class GameUI {
|
|
|
167
200
|
popup.querySelector('.achievement-xp').textContent = `+${achievement.xp ?? 0} XP`;
|
|
168
201
|
|
|
169
202
|
this._mount(popup);
|
|
203
|
+
this._announce(`Achievement unlocked: ${achievement.name ?? ''}`);
|
|
170
204
|
|
|
171
205
|
// Trigger confetti at popup location
|
|
172
206
|
this._defer(() => {
|
|
@@ -197,6 +231,7 @@ export class GameUI {
|
|
|
197
231
|
overlay.querySelector('.level-up-number').textContent = newLevel;
|
|
198
232
|
|
|
199
233
|
this._mount(overlay);
|
|
234
|
+
this._announce(`Level up: ${newLevel}`);
|
|
200
235
|
|
|
201
236
|
// Fireworks effect
|
|
202
237
|
this._defer(() => {
|
|
@@ -265,13 +300,20 @@ export class GameUI {
|
|
|
265
300
|
const x = window.innerWidth / 2;
|
|
266
301
|
const y = window.innerHeight / 3;
|
|
267
302
|
|
|
303
|
+
// The message is shown as written. showLuckyMoment and showCombo
|
|
304
|
+
// expect a multiplier and would wrap it in "LUCKY ...×!".
|
|
268
305
|
if (type === 'legendary') {
|
|
269
|
-
this.
|
|
306
|
+
this.showFloatingText(message, x, y, '#ffd700', '36px');
|
|
307
|
+
this.particleSystem.starBurst(x, y, 12);
|
|
308
|
+
this.particleSystem.confetti(x, y, 25);
|
|
270
309
|
} else if (type === 'rare') {
|
|
271
|
-
this.
|
|
310
|
+
this.showFloatingText(message, x, y, '#ff6b6b', '32px');
|
|
311
|
+
this.particleSystem.fire(x, y, 20);
|
|
272
312
|
} else {
|
|
273
313
|
this.showFloatingText(message, x, y, '#fff', '24px');
|
|
274
314
|
}
|
|
315
|
+
|
|
316
|
+
this._announce(message);
|
|
275
317
|
}
|
|
276
318
|
|
|
277
319
|
/**
|
|
@@ -367,6 +409,7 @@ export class GameUI {
|
|
|
367
409
|
|
|
368
410
|
this.container?.remove();
|
|
369
411
|
this.container = null;
|
|
412
|
+
this.liveRegion = null;
|
|
370
413
|
this.xpBar = null;
|
|
371
414
|
this.xpText = null;
|
|
372
415
|
this.levelBadge = null;
|
|
@@ -375,3 +375,21 @@
|
|
|
375
375
|
font-size: 80px;
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
|
+
|
|
379
|
+
/* Users who asked the OS for less motion get the same information without
|
|
380
|
+
the movement. Floating text keeps its place until the script removes it:
|
|
381
|
+
its animation ends at opacity 0, so cutting the duration would hide it. */
|
|
382
|
+
@media (prefers-reduced-motion: reduce) {
|
|
383
|
+
.game-ui-overlay *,
|
|
384
|
+
.achievement-popup,
|
|
385
|
+
.achievement-popup *,
|
|
386
|
+
.level-up-overlay,
|
|
387
|
+
.level-up-overlay * {
|
|
388
|
+
animation: none !important;
|
|
389
|
+
transition: none !important;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.floating-text {
|
|
393
|
+
animation: none !important;
|
|
394
|
+
}
|
|
395
|
+
}
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine subpath export — `import { Game, Scene } from 'dopaminejs/engine'`
|
|
3
|
+
*
|
|
4
|
+
* The game engine that DopamineJS is built on. Use this subpath if your project
|
|
5
|
+
* only needs the entity/component system, renderer, or physics, and not the
|
|
6
|
+
* progression mechanics (RewardSystem, GameUI, DataService).
|
|
7
|
+
*
|
|
8
|
+
* The root `dopaminejs` export continues to re-export everything, so nothing breaks.
|
|
9
|
+
* This subpath makes the README's distinction between the engine and the progression
|
|
10
|
+
* layer real in code and gives bundlers a clear surface to tree-shake against.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Entity / component system
|
|
14
|
+
export { Game } from './core/Game.js';
|
|
15
|
+
export { Scene } from './core/Scene.js';
|
|
16
|
+
export { GameObject } from './core/GameObject.js';
|
|
17
|
+
export { Component } from './core/Component.js';
|
|
18
|
+
export { Vector2 } from './core/Vector2.js';
|
|
19
|
+
export { Collider } from './core/Collider.js';
|
|
20
|
+
export { Sprite } from './core/Sprite.js';
|
|
21
|
+
export { Animator } from './core/Animator.js';
|
|
22
|
+
|
|
23
|
+
// Kernel and plugin infrastructure
|
|
24
|
+
export { DopamineKernel } from './core/DopamineKernel.js';
|
|
25
|
+
export { EventBus } from './core/EventBus.js';
|
|
26
|
+
export { SystemRegistry } from './core/SystemRegistry.js';
|
|
27
|
+
export { PluginRegistry } from './core/PluginRegistry.js';
|
|
28
|
+
|
|
29
|
+
// Built-in systems
|
|
30
|
+
export { Renderer } from './renderer/Renderer.js';
|
|
31
|
+
export { Ticker } from './systems/Ticker.js';
|
|
32
|
+
export { Loader, GlobalLoader } from './systems/Loader.js';
|
|
33
|
+
export { Input, GlobalInput } from './systems/Input.js';
|
|
34
|
+
export { Director } from './systems/Director.js';
|
|
35
|
+
export { Physics, GlobalPhysics } from './systems/Physics.js';
|
|
36
|
+
|
|
37
|
+
// Engine-level components
|
|
38
|
+
export { ParticleEmitter, ScreenShake } from './dopamine/components/index.js';
|
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/Director.js
CHANGED
|
@@ -14,11 +14,18 @@ export class Director {
|
|
|
14
14
|
run(scene) {
|
|
15
15
|
if (this.currentScene) {
|
|
16
16
|
this.currentScene.onExit();
|
|
17
|
+
|
|
18
|
+
// Its colliders would keep colliding with the next scene.
|
|
19
|
+
this.currentScene.kernel = null;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
this.currentScene = scene;
|
|
20
23
|
this.currentScene.game = this.game;
|
|
21
24
|
|
|
25
|
+
if (this.game?.kernel) {
|
|
26
|
+
scene.kernel = this.game.kernel;
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
// Let the game know (if game engine needs to reference it directly)
|
|
23
30
|
this.game.scene = scene;
|
|
24
31
|
|
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
|
@@ -9,6 +9,7 @@ export class Ticker {
|
|
|
9
9
|
this.callbacks = new Set();
|
|
10
10
|
this._frameHandle = null;
|
|
11
11
|
this._tick = this._tick.bind(this);
|
|
12
|
+
this._failed = new WeakSet();
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -64,12 +65,22 @@ export class Ticker {
|
|
|
64
65
|
const dt = (time - this.lastTime) / 1000; // Delta time in seconds
|
|
65
66
|
this.lastTime = time;
|
|
66
67
|
|
|
67
|
-
// Cap dt to prevent huge jumps if tab was inactive
|
|
68
|
-
|
|
68
|
+
// Cap dt to prevent huge jumps if tab was inactive. Floored at 0: the
|
|
69
|
+
// first frame's timestamp can predate the performance.now() in start().
|
|
70
|
+
const safeDt = Math.max(0, Math.min(dt, 0.1));
|
|
69
71
|
|
|
70
72
|
// Snapshot: a callback may add or remove callbacks mid-frame.
|
|
71
73
|
for (const callback of [...this.callbacks]) {
|
|
72
|
-
|
|
74
|
+
try {
|
|
75
|
+
callback(safeDt);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// A throw here used to skip the re-queue below with `running`
|
|
78
|
+
// still true, so the loop died and start() refused to revive it.
|
|
79
|
+
if (!this._failed.has(callback)) {
|
|
80
|
+
this._failed.add(callback);
|
|
81
|
+
console.error('[Ticker] Callback threw:', error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
// A callback may have called stop(); don't queue another frame if so.
|
|
@@ -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
|
+
}
|