dopaminejs 2.2.0 → 2.3.1

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.
@@ -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 progress = total > 0 ? (current / total) * 100 : 0;
112
- this.xpBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
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.showLuckyMoment(message, x, y);
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.showCombo(message, x, y);
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';
@@ -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
 
@@ -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
- const safeDt = Math.min(dt, 0.1);
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
- callback(safeDt);
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.
package/types/index.d.ts CHANGED
@@ -77,7 +77,11 @@ export type RewardEvent =
77
77
  | 'xp_gained'
78
78
  | 'level_up'
79
79
  | 'achievement_unlocked'
80
- | 'new_high_score';
80
+ | 'new_high_score'
81
+ /** Payload `{ current, longest }`. Fires when the day rolls over. */
82
+ | 'streak_updated'
83
+ /** Payload `{ key }`. The storage write failed; state lives in memory only. */
84
+ | 'save_failed';
81
85
 
82
86
  export class EventEmitter {
83
87
  on(event: string, callback: (data?: unknown) => void): () => void;
@@ -101,8 +105,12 @@ export class RewardSystem extends EventEmitter {
101
105
  addXP(amount: number, reason?: string): Promise<AddXPResult>;
102
106
  getXPForNextLevel(): XPProgress;
103
107
 
104
- /** Persists once for the whole call, not once per internal mutation. */
105
- recordGame(gameName: string, result: GameResult): Promise<void>;
108
+ /**
109
+ * Persists once for the whole call, not once per internal mutation.
110
+ * @throws TypeError for an empty or reserved game name, or a `score` that
111
+ * is not a finite number.
112
+ */
113
+ recordGame(gameName: string, result?: GameResult): Promise<void>;
106
114
 
107
115
  checkAchievements(gameName: string, result: GameResult): Promise<AchievementDefinition[]>;
108
116
  unlockAchievement(achievementId: string): Promise<boolean>;
@@ -122,16 +130,27 @@ export interface StorageLike {
122
130
  removeItem(key: string): void;
123
131
  }
124
132
 
133
+ /**
134
+ * Promise-returning store such as AsyncStorage or an IndexedDB wrapper.
135
+ * DataService accepts either kind. SoundManager reads its mute flag in the
136
+ * constructor and needs the synchronous one.
137
+ */
138
+ export interface AsyncStorageLike {
139
+ getItem(key: string): Promise<string | null>;
140
+ setItem(key: string, value: string): Promise<void>;
141
+ removeItem(key: string): Promise<void>;
142
+ }
143
+
125
144
  export interface DataServiceConfig {
126
145
  /** Defaults to localStorage, falling back to an in-memory store. */
127
- storage?: StorageLike;
146
+ storage?: StorageLike | AsyncStorageLike;
128
147
  /** Key prefix, default 'dopamine_'. */
129
148
  prefix?: string;
130
149
  }
131
150
 
132
151
  export class DataService {
133
152
  constructor(config?: DataServiceConfig);
134
- storage: StorageLike;
153
+ storage: StorageLike | AsyncStorageLike;
135
154
  prefix: string;
136
155
  save(key: string, data: unknown): Promise<boolean>;
137
156
  load<T = unknown>(key: string, defaultValue?: T | null): Promise<T | null>;
@@ -162,7 +181,8 @@ export class GameUI {
162
181
 
163
182
  container: HTMLElement | null;
164
183
 
165
- updateXP(current: number, needed: number, total: number): void;
184
+ /** `progress` is the position within the current level, 0 to 1. */
185
+ updateXP(current: number, needed: number, total: number, progress?: number): void;
166
186
  updateLevel(level: number): void;
167
187
  updateStreak(days: number): void;
168
188
 
@@ -188,18 +208,25 @@ export interface ParticleSystemConfig {
188
208
  container?: HTMLElement | string;
189
209
  canvasId?: string;
190
210
  zIndex?: string;
211
+ /** Cap on live particles, default 5000. Emits past it are dropped. */
212
+ maxParticles?: number;
213
+ /** Skip effects under prefers-reduced-motion. Default true. */
214
+ respectReducedMotion?: boolean;
191
215
  }
192
216
 
193
217
  export interface ParticleConfig {
194
218
  x: number;
195
219
  y: number;
196
220
  count?: number;
197
- color?: string;
221
+ /** One colour, or a list to pick from at random per particle. */
222
+ color?: string | string[];
198
223
  size?: number;
199
224
  life?: number;
200
225
  decay?: number;
201
226
  gravity?: number;
202
227
  spread?: number;
228
+ /** Direction in radians. */
229
+ angle?: number;
203
230
  speed?: number;
204
231
  type?: string;
205
232
  sprite?: string;
@@ -239,7 +266,7 @@ export interface SoundManagerConfig {
239
266
  storage?: StorageLike;
240
267
  /** Master volume 0 to 1, default 1. */
241
268
  volume?: number;
242
- /** Key to URL map, preloaded on construction. */
269
+ /** Key to URL map, loaded when the audio context is first created. */
243
270
  customSounds?: Record<string, string>;
244
271
  }
245
272
 
@@ -268,6 +295,9 @@ export class SoundManager {
268
295
  playClick(force?: boolean): void;
269
296
  playSuccess(force?: boolean): void;
270
297
  playError(force?: boolean): void;
298
+
299
+ /** Cancels queued tones, closes the audio context, drops decoded buffers. */
300
+ destroy(): void;
271
301
  }
272
302
 
273
303
  // ---------------------------------------------------------------------------
@@ -406,19 +436,29 @@ export class Component {
406
436
  render(ctx: CanvasRenderingContext2D): void;
407
437
  }
408
438
 
409
- export interface Bounds {
439
+ export interface BoxBounds {
440
+ left: number;
441
+ right: number;
442
+ top: number;
443
+ bottom: number;
444
+ }
445
+
446
+ export interface CircleBounds {
410
447
  x: number;
411
448
  y: number;
412
- width: number;
413
- height: number;
449
+ radius: number;
414
450
  }
415
451
 
452
+ /** A box collider returns edges, a circle collider returns centre and radius. */
453
+ export type Bounds = BoxBounds | CircleBounds;
454
+
416
455
  export class Collider extends Component {
417
456
  constructor(type?: 'box' | 'circle', width?: number, height?: number, radius?: number);
418
457
  type: 'box' | 'circle';
419
458
  width: number;
420
459
  height: number;
421
460
  radius: number;
461
+ tag: string | null;
422
462
  getBounds(): Bounds;
423
463
  }
424
464
 
@@ -435,11 +475,13 @@ export class Animator extends Component {
435
475
 
436
476
  export class GameObject {
437
477
  constructor(x?: number, y?: number);
438
- position: Vector2;
478
+ x: number;
479
+ y: number;
439
480
  rotation: number;
440
- scale: Vector2;
481
+ scale: { x: number; y: number };
441
482
  components: Component[];
442
483
  children: GameObject[];
484
+ parent: GameObject | null;
443
485
  kernel: DopamineKernel | null;
444
486
  tag?: string;
445
487
  addChild(child: GameObject): GameObject;
@@ -500,7 +542,8 @@ export class Physics {
500
542
  add(collider: Collider): void;
501
543
  remove(collider: Collider): void;
502
544
  step(): void;
503
- checkOverlap(source: Collider, targetTag: string): GameObject | null;
545
+ /** Every collider that overlaps `source`. With a tag, only colliders carrying it. */
546
+ checkOverlap(source: Collider, targetTag?: string): Collider[];
504
547
  addBody(body: Collider): void;
505
548
  removeBody(body: Collider): void;
506
549
  checkCollision(a: Collider, b: Collider): boolean;
@@ -585,4 +628,6 @@ export default class Dopamine {
585
628
  particleSystem: ParticleSystem;
586
629
  gameUI: GameUI;
587
630
  init(): Promise<DopamineSubsystems>;
631
+ /** Removes the overlay and canvas, closes audio, detaches from reward events. */
632
+ destroy(): void;
588
633
  }
@@ -1,37 +0,0 @@
1
- (function(h,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(h=typeof globalThis<"u"?globalThis:h||self,f(h.Dopamine={}))})(this,function(h){"use strict";class f{constructor(){this.listeners=new Map}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>this.off(e,t)}off(e,t){this.listeners.has(e)&&this.listeners.get(e).delete(t)}emit(e,t){this.listeners.has(e)&&this.listeners.get(e).forEach(s=>{try{s(t)}catch(i){console.error(`Error in listener for event '${e}':`,i)}})}clear(){this.listeners.clear()}}class P extends f{constructor(e,t={}){var s;super(),this.dataService=e,this.player=null,this.achievements={...this._initAchievements(),...t.achievements||{}},this.events=t.events||((s=t.kernel)==null?void 0:s.events)||null,this._batchDepth=0,this._pendingSave=!1}_publish(e,t){this.emit(e,t),this.events&&this.events.emit(e,t)}async _batch(e){this._batchDepth++;try{return await e()}finally{this._batchDepth--,this._batchDepth===0&&this._pendingSave&&(this._pendingSave=!1,await this._write())}}async init(){const e=await this.dataService.load("player",null);return this.player=this._migrate(e),this._updateDailyStreak(),await this.save(),this.player}_getDefaultPlayer(){return{name:"Player",xp:0,level:1,totalGamesPlayed:0,createdAt:Date.now(),lastPlayedAt:Date.now(),streak:{current:1,longest:1,lastPlayDate:this._getTodayDateString()},achievements:{},stats:{}}}_migrate(e){const t=this._getDefaultPlayer();return!e||typeof e!="object"?t:{...t,...e,streak:{...t.streak,...e.streak||{}},achievements:e.achievements||{},stats:e.stats||{}}}async save(){if(this.player){if(this._batchDepth>0){this._pendingSave=!0;return}await this._write()}}async _write(){this.player.lastPlayedAt=Date.now(),await this.dataService.save("player",this.player)}async addXP(e,t=""){if(!Number.isFinite(e))throw new TypeError(`[DopamineJS] addXP expects a finite number, received ${e}`);const s=this.player.level;this.player.xp+=e;const i=this._calculateLevel(this.player.xp),r=i>s;return r&&(this.player.level=i),await this.save(),this._publish("xp_gained",{amount:e,reason:t,leveledUp:r,newLevel:i}),r&&this._publish("level_up",{oldLevel:s,newLevel:i}),{leveledUp:r,newLevel:i,xpGained:e}}_calculateLevel(e){const t=Math.floor((1+Math.sqrt(1+4*e/50))/2);return Math.max(1,t)}getXPForNextLevel(){const e=this.player.level+1,t=50*e*(e-1),s=50*this.player.level*(this.player.level-1),i=t-s,r=i>0?Math.max(0,Math.min(1,(this.player.xp-s)/i)):0;return{total:t,needed:Math.max(0,t-this.player.xp),progress:r}}async recordGame(e,t){return this._batch(()=>this._recordGame(e,t))}async _recordGame(e,t){this.player.stats[e]||(this.player.stats[e]={totalPlays:0,highScore:0});const s=this.player.stats[e];s.totalPlays++,this.player.totalGamesPlayed++,t.score!==void 0&&t.score>s.highScore&&(s.highScore=t.score,this._publish("new_high_score",{gameName:e,score:t.score})),Object.keys(t).forEach(r=>{r!=="score"&&(s[r]=(s[r]||0)+(typeof t[r]=="number"?t[r]:0))});let i=10;t.score&&(i+=Math.floor(t.score/5)),i=Math.floor(i*this.getStreakMultiplier()),await this.addXP(i,`Played ${e}`),await this.checkAchievements(e,t),await this.save()}async checkAchievements(e,t){const s=[];for(const[i,r]of Object.entries(this.achievements)){if(this.player.achievements[i])continue;let n=!1;try{n=r.check(this.player,e,t)}catch(o){console.error(`[DopamineJS] Achievement "${i}" check threw:`,o);continue}n&&(await this.unlockAchievement(i),s.push(r))}return s}async unlockAchievement(e){const t=this.achievements[e];return t?(this.player.achievements[e]={unlockedAt:Date.now(),seen:!1},await this.addXP(Number.isFinite(t.xp)?t.xp:0,`Achievement: ${t.name}`),this._publish("achievement_unlocked",t),await this.save(),!0):!1}getUnlockedAchievements(){return Object.entries(this.player.achievements).map(([e,t])=>({...this.achievements[e],id:e,...t}))}getUnseenAchievements(){return this.getUnlockedAchievements().filter(e=>!e.seen)}async markAchievementsSeen(e){for(const t of e)this.player.achievements[t]&&(this.player.achievements[t].seen=!0);await this.save()}_updateDailyStreak(){const e=this._getTodayDateString(),t=this.player.streak.lastPlayDate;if(t===e)return;const s=this._getYesterdayDateString();t===s?(this.player.streak.current++,this.player.streak.current>this.player.streak.longest&&(this.player.streak.longest=this.player.streak.current)):this.player.streak.current=1,this.player.streak.lastPlayDate=e}getStreakMultiplier(){const e=this.player.streak.current;return e>=7?2:e>=5?1.5:e>=3?1.25:1}_toLocalDateString(e){const t=e.getFullYear(),s=String(e.getMonth()+1).padStart(2,"0"),i=String(e.getDate()).padStart(2,"0");return`${t}-${s}-${i}`}_getTodayDateString(){return this._toLocalDateString(new Date)}_getYesterdayDateString(){const e=new Date;return e.setDate(e.getDate()-1),this._toLocalDateString(e)}_initAchievements(){return{first_game:{name:"First Steps",description:"Play your first game",icon:"🎮",xp:50,check:e=>e.totalGamesPlayed>=1},level_5:{name:"Rising Star",description:"Reach level 5",icon:"⭐",xp:100,check:e=>e.level>=5},level_10:{name:"Expert Player",description:"Reach level 10",icon:"🏆",xp:250,check:e=>e.level>=10},streak_3:{name:"Dedicated",description:"Play 3 days in a row",icon:"🔥",xp:100,check:e=>e.streak.current>=3},streak_7:{name:"Unstoppable",description:"Play 7 days in a row",icon:"💥",xp:300,check:e=>e.streak.current>=7}}}}function w(){const a=new Map;return{getItem:e=>a.has(e)?a.get(e):null,setItem:(e,t)=>{a.set(e,String(t))},removeItem:e=>{a.delete(e)},clear:()=>{a.clear()}}}function x(){try{if(typeof localStorage>"u")return w();const a="__dopamine_probe__";return localStorage.setItem(a,"1"),localStorage.removeItem(a),localStorage}catch{return w()}}class C{constructor(e={}){this.storage=e.storage||x(),this.prefix=e.prefix||"dopamine_"}async save(e,t){try{const s=JSON.stringify(t);return this.storage.setItem(this.prefix+e,s),!0}catch(s){return console.error("Error saving data:",s),!1}}async load(e,t=null){try{const s=this.storage.getItem(this.prefix+e);return s?JSON.parse(s):t}catch(s){return console.error("Error loading data:",s),t}}async clear(e){this.storage.removeItem(this.prefix+e)}}const D="game-ui-overlay";class I{constructor(e){this.particleSystem=e,this.container=null,this.xpBar=null,this.levelDisplay=null,this.streakDisplay=null,this._timers=new Set,this._detached=[],this.init()}init(){this.container=document.createElement("div"),this.container.className="game-ui-overlay",document.getElementById(D)||(this.container.id=D),this.container.innerHTML=`
2
- <div class="game-ui-top-bar">
3
- <div class="level-badge">
4
- <span class="level-label">LVL</span>
5
- <span class="level-number">1</span>
6
- </div>
7
-
8
- <div class="xp-container">
9
- <div class="xp-bar-bg">
10
- <div class="xp-bar-fill" style="width: 0%"></div>
11
- </div>
12
- <div class="xp-text">0 / 100 XP</div>
13
- </div>
14
-
15
- <div class="streak-badge">
16
- <span class="streak-icon">🔥</span>
17
- <span class="streak-number">1</span>
18
- </div>
19
- </div>
20
- `,document.body.appendChild(this.container),this.levelBadge=this.container.querySelector(".level-badge"),this.streakBadge=this.container.querySelector(".streak-badge"),this.xpBar=this.container.querySelector(".xp-bar-fill"),this.xpText=this.container.querySelector(".xp-text"),this.levelDisplay=this.container.querySelector(".level-number"),this.streakDisplay=this.container.querySelector(".streak-number")}_defer(e,t){const s=setTimeout(()=>{this._timers.delete(s),e()},t);return this._timers.add(s),s}_mount(e){return document.body.appendChild(e),this._detached.push(e),e}_replayAnimation(e,t){e&&(e.style.animation="none",this._defer(()=>{e.style.animation=t},10))}updateXP(e,t,s){const i=s>0?e/s*100:0;this.xpBar.style.width=`${Math.max(0,Math.min(100,i))}%`,this.xpText.textContent=`${e} / ${s} XP`,this._replayAnimation(this.xpBar,"xp-pulse 0.3s ease-out")}updateLevel(e){this.levelDisplay.textContent=e,this._replayAnimation(this.levelBadge,"level-pulse 0.5s ease-out")}updateStreak(e){this.streakDisplay.textContent=e,e>=7?this.streakBadge.style.background="linear-gradient(135deg, #ff6b00 0%, #ff4400 100%)":e>=3&&(this.streakBadge.style.background="linear-gradient(135deg, #ff8800 0%, #ff6b00 100%)")}showAchievement(e){const t=document.createElement("div");t.className="achievement-popup",t.innerHTML=`
21
- <div class="achievement-shine"></div>
22
- <div class="achievement-content">
23
- <div class="achievement-icon"></div>
24
- <div class="achievement-info">
25
- <div class="achievement-title">Achievement Unlocked!</div>
26
- <div class="achievement-name"></div>
27
- <div class="achievement-desc"></div>
28
- <div class="achievement-xp"></div>
29
- </div>
30
- </div>
31
- `,t.querySelector(".achievement-icon").innerHTML=e.icon??"",t.querySelector(".achievement-name").textContent=e.name??"",t.querySelector(".achievement-desc").textContent=e.description??"",t.querySelector(".achievement-xp").textContent=`+${e.xp??0} XP`,this._mount(t),this._defer(()=>{const s=t.getBoundingClientRect();this.particleSystem.confetti(s.left+s.width/2,s.top+s.height/2,40)},300),this._defer(()=>{t.style.animation="slideOut 0.3s ease-in forwards",this._defer(()=>this._remove(t),300)},4e3)}showLevelUp(e,t){const s=document.createElement("div");s.className="level-up-overlay",s.innerHTML=`
32
- <div class="level-up-content">
33
- <div class="level-up-title">LEVEL UP!</div>
34
- <div class="level-up-number"></div>
35
- <div class="level-up-subtitle">Amazing progress!</div>
36
- </div>
37
- `,s.querySelector(".level-up-number").textContent=t,this._mount(s),this._defer(()=>{for(let i=0;i<5;i++)this._defer(()=>{const r=Math.random()*window.innerWidth,n=Math.random()*window.innerHeight*.6;this.particleSystem.confetti(r,n,30)},i*200)},300),this._defer(()=>{s.style.opacity="0",this._defer(()=>this._remove(s),500)},2500)}showFloatingText(e,t,s,i="#ffd700",r="24px"){const n=document.createElement("div");n.className="floating-text",n.textContent=e,n.style.left=t+"px",n.style.top=s+"px",n.style.color=i,n.style.fontSize=r,this._mount(n),this._defer(()=>this._remove(n),2e3)}showCombo(e,t,s){this.showFloatingText(`${e}× COMBO!`,t,s,"#ff6b6b","32px"),this.particleSystem.fire(t,s,20)}showNearMiss(e,t){this.showFloatingText("CLOSE CALL!",e,t,"#45b7d1","20px"),this.particleSystem.sparkle(e,t,15,"#45b7d1")}showLuckyMoment(e,t,s){this.showFloatingText(`LUCKY ${e}×!`,t,s,"#ffd700","36px"),this.particleSystem.starBurst(t,s,12),this.particleSystem.confetti(t,s,25)}showNotification(e,t="normal"){const s=window.innerWidth/2,i=window.innerHeight/3;t==="legendary"?this.showLuckyMoment(e,s,i):t==="rare"?this.showCombo(e,s,i):this.showFloatingText(e,s,i,"#fff","24px")}showSummary(e){const t=document.createElement("div");t.className="message-box",t.style.display="flex",t.style.position="absolute",t.style.top="50%",t.style.left="50%",t.style.transform="translate(-50%, -50%)",t.style.zIndex="1000";const s=document.createElement("h1");s.textContent="Game Over";const i=document.createElement("div");i.className="summary-score",i.style.cssText="font-size: 3rem; font-weight: bold; color: #e06020; margin: 10px 0;",i.textContent=e.score;const r=document.createElement("div");r.className="summary-metrics",r.style.cssText="width: 100%; margin-bottom: 20px;";for(const[o,l]of Object.entries(e.metrics||{})){const c=document.createElement("div");c.style.cssText="display:flex;justify-content:space-between;width:100%";const d=document.createElement("span");d.textContent=`${o}:`;const m=document.createElement("strong");m.textContent=l,c.append(d,m),r.appendChild(c)}t.append(s,i,r);const n=document.createElement("button");if(n.className="btn",n.textContent="Play Again",n.addEventListener("click",e.onReplay||(()=>window.location.reload())),t.appendChild(n),e.onExit){const o=document.createElement("button");o.className="btn",o.textContent="Exit",o.style.cssText="margin-top: 10px; background: #543847;",o.addEventListener("click",e.onExit),t.appendChild(o)}return this._mount(t),t}_remove(e){e.remove();const t=this._detached.indexOf(e);t>-1&&this._detached.splice(t,1)}destroy(){var e;for(const t of this._timers)clearTimeout(t);this._timers.clear();for(const t of this._detached)t.remove();this._detached=[],(e=this.container)==null||e.remove(),this.container=null,this.xpBar=null,this.xpText=null,this.levelBadge=null,this.streakBadge=null,this.levelDisplay=null,this.streakDisplay=null}}class O{constructor(e={}){this.container=e.container?typeof e.container=="string"?document.querySelector(e.container):e.container:document.body,this.canvasId=e.canvasId||"dopamine-particle-canvas",this.canvas=document.createElement("canvas"),this.canvas.classList.add("dopamine-particle-canvas"),document.getElementById(this.canvasId)||(this.canvas.id=this.canvasId),this.canvas.style.pointerEvents="none",this.canvas.style.zIndex=e.zIndex||"9999",this.container===document.body?(this.canvas.style.position="fixed",this.canvas.style.top="0",this.canvas.style.left="0"):(this.canvas.style.position="absolute",this.canvas.style.top="0",this.canvas.style.left="0",window.getComputedStyle(this.container).position==="static"&&(this.container.style.position="relative")),this.container.appendChild(this.canvas),this.ctx=this.canvas.getContext("2d"),this.particles=[],this.pool=[],this.sprites=new Map,this.customEffects=new Map,this.isAnimating=!1,this._onResize=()=>this._resize(),this._resizeObserver=null,this._frameHandle=null,this._resize(),window.ResizeObserver&&this.container!==document.body?(this._resizeObserver=new ResizeObserver(this._onResize),this._resizeObserver.observe(this.container)):window.addEventListener("resize",this._onResize)}_resize(){const e=this.container===document.body?window.innerWidth:this.container.clientWidth,t=this.container===document.body?window.innerHeight:this.container.clientHeight,s=window.devicePixelRatio||1;this.width=e,this.height=t,this.canvas.width=Math.round(e*s),this.canvas.height=Math.round(t*s),this.canvas.style.width=`${e}px`,this.canvas.style.height=`${t}px`,this.ctx.setTransform(s,0,0,s,0,0)}registerSprite(e,t){const s=new Image;s.src=t,this.sprites.set(e,s)}registerEffect(e,t){this.customEffects.set(e,t)}play(e,t,s,...i){const r=this.customEffects.get(e);r?r(t,s,...i):console.warn(`[DopamineJS] Effect '${e}' not found.`)}emit(e){const{x:t,y:s,count:i=10,speed:r=5,gravity:n=.1,decay:o=.02,life:l=1,color:c="#ffd700",sprite:d=null,spread:m=Math.PI*2,angle:y=0,size:v=5}=e;for(let B=0;B<i;B++){const u=this._getParticle(),F=y+(Math.random()-.5)*m,N=Math.random()*r;u.x=t,u.y=s,u.vx=Math.cos(F)*N,u.vy=Math.sin(F)*N,u.life=l,u.decay=o*(.8+Math.random()*.4),u.gravity=n,u.color=Array.isArray(c)?c[Math.floor(Math.random()*c.length)]:c,u.size=v*(.8+Math.random()*.4),u.sprite=d,u.rotation=Math.random()*Math.PI*2,u.rotationSpeed=(Math.random()-.5)*.2,this.particles.push(u)}this._startAnimation()}_getParticle(){return this.pool.length>0?this.pool.pop():{}}_recycleParticle(e){this.pool.push(e)}confetti(e,t,s=50){const i=["#ff6b6b","#4ecdc4","#45b7d1","#f7dc6f","#c06c84","#6c5ce7"];this.emit({x:e,y:t,count:s,color:i,speed:10,gravity:.3,decay:.015,size:8,spread:Math.PI*2})}coinShower(e,t,s=10){this.emit({x:e,y:t,count:s,color:"#ffd700",speed:4,gravity:.4,decay:.012,size:12,angle:-Math.PI/2,spread:Math.PI/2})}sparkle(e,t,s=20,i="#ffd700"){this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.05,decay:.03,size:3})}fire(e,t,s=15){const i=["#ff6b00","#ff8800","#ffaa00","#ff4400"];this.emit({x:e,y:t,count:s,color:i,speed:3,gravity:-.2,decay:.04,size:5,angle:-Math.PI/2,spread:Math.PI/4})}starBurst(e,t,s=8){for(let i=0;i<s;i++){const r=this._getParticle(),n=Math.PI*2/s*i,o=4;r.x=e,r.y=t,r.vx=Math.cos(n)*o,r.vy=Math.sin(n)*o,r.life=1,r.decay=.03,r.gravity=0,r.color="#ffffff",r.size=15,r.sprite=null,r.type="star",r.rotation=n,r.rotationSpeed=0,this.particles.push(r)}this._startAnimation()}_startAnimation(){this.isAnimating||(this.isAnimating=!0,this._animate())}_animate(){this._frameHandle=null,this.ctx.clearRect(0,0,this.width,this.height);for(let e=this.particles.length-1;e>=0;e--){const t=this.particles[e];if(t.vy+=t.gravity,t.x+=t.vx,t.y+=t.vy,t.life-=t.decay,t.rotationSpeed&&(t.rotation+=t.rotationSpeed),t.life<=0){this._recycleParticle(t),this.particles.splice(e,1);continue}if(this.ctx.save(),this.ctx.globalAlpha=t.life,this.ctx.translate(t.x,t.y),this.ctx.rotate(t.rotation),t.sprite&&this.sprites.has(t.sprite)){const s=this.sprites.get(t.sprite);s.complete&&this.ctx.drawImage(s,-t.size/2,-t.size/2,t.size,t.size)}else t.type==="star"?(this.ctx.fillStyle=t.color,this._drawStarPath(0,0,5,t.size,t.size/2),this.ctx.fill()):(this.ctx.fillStyle=t.color,this.ctx.beginPath(),this.ctx.arc(0,0,t.size,0,Math.PI*2),this.ctx.fill());this.ctx.restore()}this.particles.length>0?this._frameHandle=requestAnimationFrame(()=>this._animate()):this.isAnimating=!1}_drawStarPath(e,t,s,i,r){let n=Math.PI/2*3,o=e,l=t;const c=Math.PI/s;this.ctx.beginPath(),this.ctx.moveTo(e,t-i);for(let d=0;d<s;d++)o=e+Math.cos(n)*i,l=t+Math.sin(n)*i,this.ctx.lineTo(o,l),n+=c,o=e+Math.cos(n)*r,l=t+Math.sin(n)*r,this.ctx.lineTo(o,l),n+=c;this.ctx.lineTo(e,t-i),this.ctx.closePath()}clear(){this.particles.forEach(e=>this._recycleParticle(e)),this.particles=[],this.ctx.clearRect(0,0,this.width,this.height)}destroy(){this.clear(),this.isAnimating=!1,this._frameHandle!==null&&(cancelAnimationFrame(this._frameHandle),this._frameHandle=null),this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),window.removeEventListener("resize",this._onResize),this.canvas.remove(),this.sprites.clear(),this.customEffects.clear(),this.pool=[]}}class T{constructor(e={}){this.audioContext=null,this.storageKey=e.storageKey||"dopamineSoundsMuted",this.storage=e.storage||x(),this.muted=this.storage.getItem(this.storageKey)==="true",this.assets=new Map,this.customSounds=e.customSounds||{},this.masterGain=null,this._volume=e.volume??1,Object.keys(this.customSounds).length>0&&this.preloadSounds(this.customSounds)}initAudio(){if(!this.audioContext){const e=typeof window<"u"?window.AudioContext||window.webkitAudioContext:void 0;if(!e)return!1;this.audioContext=new e,this.masterGain=this.audioContext.createGain(),this.masterGain.gain.value=this._volume,this.masterGain.connect(this.audioContext.destination)}return this.audioContext.state==="suspended"&&this.audioContext.resume(),!0}setVolume(e){return this._volume=Math.max(0,Math.min(1,e)),this.masterGain&&(this.masterGain.gain.value=this._volume),this._volume}getVolume(){return this._volume}_output(){return this.masterGain||this.audioContext.destination}toggleMute(){return this.muted=!this.muted,this.storage.setItem(this.storageKey,this.muted),this.muted}registerSound(e,t){this.customSounds[e]=t,this.audioContext&&this.loadSound(e,t)}async preloadSounds(e){const t=Object.entries(e).map(([s,i])=>this.loadSound(s,i));await Promise.allSettled(t)}async loadSound(e,t){try{if(!this.audioContext)return;const i=await(await fetch(t)).arrayBuffer(),r=await this.audioContext.decodeAudioData(i);this.assets.set(e,r)}catch(s){console.warn(`[DopamineJS] Failed to load sound '${e}':`,s)}}async play(e){if(this.muted||!this.initAudio())return;if(this.assets.has(e)){this._playBuffer(this.assets.get(e));return}if(this.customSounds[e]&&!this.assets.has(e)&&(await this.loadSound(e,this.customSounds[e]),this.assets.has(e))){this._playBuffer(this.assets.get(e));return}const t={jump:()=>this.playJump(!0),score:()=>this.playScore(!0),success:()=>this.playSuccess(!0),click:()=>this.playClick(!0),error:()=>this.playError(!0),gameover:()=>this.playGameOver(!0)};t[e]&&t[e]()}_playBuffer(e){const t=this.audioContext.createBufferSource();t.buffer=e,t.connect(this._output()),t.start(0)}playTone(e,t,s="sine",i=.3){if(this.muted||!this.initAudio())return;const r=this.audioContext.createOscillator(),n=this.audioContext.createGain();r.connect(n),n.connect(this._output()),r.frequency.value=e,r.type=s;const o=Math.max(1e-4,i);n.gain.setValueAtTime(o,this.audioContext.currentTime),n.gain.exponentialRampToValueAtTime(.01,this.audioContext.currentTime+t),r.start(this.audioContext.currentTime),r.stop(this.audioContext.currentTime+t)}playJump(e=!1){if(!e&&this.assets.has("jump"))return this.play("jump");this.playTone(400,.1,"square",.2)}playScore(e=!1){if(!e&&this.assets.has("score"))return this.play("score");this.playTone(800,.15,"sine",.25),setTimeout(()=>this.playTone(1e3,.15,"sine",.25),50)}playGameOver(e=!1){if(!e&&this.assets.has("gameover"))return this.play("gameover");this.playTone(300,.2,"sawtooth",.3),setTimeout(()=>this.playTone(200,.2,"sawtooth",.3),100),setTimeout(()=>this.playTone(150,.3,"sawtooth",.3),200)}playClick(e=!1){if(!e&&this.assets.has("click"))return this.play("click");this.playTone(600,.05,"sine",.1)}playSuccess(e=!1){if(!e&&this.assets.has("success"))return this.play("success");this.playTone(600,.1,"sine",.2),setTimeout(()=>this.playTone(800,.2,"sine",.2),100)}playError(e=!1){if(!e&&this.assets.has("error"))return this.play("error");this.playTone(200,.2,"sawtooth",.3)}}class H{constructor(e={}){this.config=e,this.dataService=new C(e.data||{}),this.rewardSystem=new P(this.dataService,e.rewards||{}),this.soundManager=new T(e.sound||{}),this.particleSystem=new O(e.particles||{}),this.gameUI=new I(this.particleSystem),this._bindEvents()}async init(){return await this.rewardSystem.init(),{rewardSystem:this.rewardSystem,gameUI:this.gameUI,particleSystem:this.particleSystem,soundManager:this.soundManager}}_bindEvents(){this.rewardSystem.on("xp_gained",e=>{const{total:t,needed:s}=this.rewardSystem.getXPForNextLevel();this.gameUI.updateXP(this.rewardSystem.player.xp,s,t),e.xpGained>0}),this.rewardSystem.on("level_up",e=>{this.gameUI.updateLevel(e.newLevel),this.gameUI.showLevelUp(e.oldLevel,e.newLevel)}),this.rewardSystem.on("achievement_unlocked",e=>{this.gameUI.showAchievement(e)}),this.rewardSystem.on("new_high_score",e=>{this.gameUI.showNotification(`New High Score: ${e.score}!`,"legendary"),this.soundManager.playSuccess()})}}class p{constructor(){this._listeners=new Map,this._onceListeners=new Map,this._sortedCache=new Map}on(e,t,s=0){return this._listeners.has(e)||this._listeners.set(e,new Set),this._listeners.get(e).add({callback:t,priority:s}),this._sortedCache.delete(e),this}once(e,t){return this._onceListeners.has(e)||this._onceListeners.set(e,new Set),this._onceListeners.get(e).add(t),this}off(e,t){const s=this._listeners.get(e);if(s)for(const r of[...s])r.callback===t&&(s.delete(r),this._sortedCache.delete(e));const i=this._onceListeners.get(e);return i&&i.delete(t),this}emit(e,t){const s=this._getSortedListeners(e);for(let r=0;r<s.length;r++)this._invoke(s[r].callback,t,e);const i=this._onceListeners.get(e);if(i&&i.size>0){const r=[...i];i.clear();for(const n of r)this._invoke(n,t,e)}}_invoke(e,t,s){try{e(t)}catch(i){console.error(`[DopamineJS] Listener for "${s}" threw:`,i)}}_getSortedListeners(e){if(this._sortedCache.has(e))return this._sortedCache.get(e);const t=this._listeners.get(e);if(!t||t.size===0)return[];const s=Array.from(t).sort((i,r)=>r.priority-i.priority);return this._sortedCache.set(e,s),s}clear(e){e?(this._listeners.delete(e),this._onceListeners.delete(e),this._sortedCache.delete(e)):(this._listeners.clear(),this._onceListeners.clear(),this._sortedCache.clear())}hasListeners(e){const t=this._listeners.get(e),s=this._onceListeners.get(e);return!!(t!=null&&t.size)||!!(s!=null&&s.size)}}p.Events={TICK:"tick",FIXED_UPDATE:"fixed_update",RENDER:"render",COLLISION_ENTER:"collision_enter",COLLISION_EXIT:"collision_exit",XP_GAINED:"xp_gained",LEVEL_UP:"level_up",ACHIEVEMENT_UNLOCKED:"achievement_unlocked",NEW_HIGH_SCORE:"new_high_score",STREAK_UPDATED:"streak_updated",SYSTEM_REGISTERED:"system_registered",SYSTEM_UNREGISTERED:"system_unregistered",PLUGIN_LOADED:"plugin_loaded",PLUGIN_UNLOADED:"plugin_unloaded"};class A{constructor(e){this.kernel=e,this._systems=new Map,this._updateOrder=[],this._fixedUpdateOrder=[],this._needsRecompute=!1}register(e,t,{priority:s=0,dependencies:i=[]}={}){this._systems.has(e)&&(console.warn(`[SystemRegistry] System "${e}" already registered. Replacing.`),this.unregister(e));for(const r of i)this._systems.has(r)||console.warn(`[SystemRegistry] Dependency "${r}" not found for system "${e}"`);return this._systems.set(e,{system:t,priority:s,dependencies:i}),t.init&&t.init(this.kernel),this._needsRecompute=!0,this.kernel.events.emit("system_registered",{name:e,system:t}),this}get(e){const t=this._systems.get(e);return t?t.system:void 0}has(e){return this._systems.has(e)}unregister(e){const t=this._systems.get(e);return t?(t.system.destroy&&t.system.destroy(),this._systems.delete(e),this._needsRecompute=!0,this.kernel.events.emit("system_unregistered",{name:e}),!0):!1}update(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._updateOrder.length;t++){const s=this._updateOrder[t];s.update&&s.update(e)}}fixedUpdate(e){this._needsRecompute&&this._recomputeUpdateOrder();for(let t=0;t<this._fixedUpdateOrder.length;t++){const s=this._fixedUpdateOrder[t];s.fixedUpdate&&s.fixedUpdate(e)}}_recomputeUpdateOrder(){const e=Array.from(this._systems.entries()),t=new Map,s=new Map;for(const[n,o]of e)t.set(n,o.dependencies),s.set(n,0);for(const[n,o]of t)for(const l of o)s.has(l)&&s.set(n,s.get(n)+1);const i=[],r=[];for(const[n,o]of s)o===0&&i.push(n);for(;i.length>0;){i.sort((o,l)=>{const c=this._systems.get(o).priority;return this._systems.get(l).priority-c});const n=i.shift();r.push(n);for(const[o,l]of t)if(l.includes(n)){const c=s.get(o)-1;s.set(o,c),c===0&&i.push(o)}}r.length!==e.length&&(console.error("[SystemRegistry] Circular dependency detected in systems!"),r.length=0,r.push(...e.map(([n])=>n).sort((n,o)=>this._systems.get(o).priority-this._systems.get(n).priority))),this._updateOrder=r.map(n=>this._systems.get(n).system).filter(n=>n.update),this._fixedUpdateOrder=r.map(n=>this._systems.get(n).system).filter(n=>n.fixedUpdate),this._needsRecompute=!1}getSystemNames(){return Array.from(this._systems.keys())}clear(){for(const e of this._systems.keys())this.unregister(e)}}class L{constructor(e){this.kernel=e,this._plugins=new Map,this._loadOrder=[]}use(e){if(!e.name)throw new Error("[PluginRegistry] Plugin must have a name");if(this._plugins.has(e.name))return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`),this;if(!e.init||typeof e.init!="function")throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);this._plugins.set(e.name,e),this._loadOrder.push(e.name);try{e.init(this.kernel),this.kernel.events.emit("plugin_loaded",{name:e.name,version:e.version})}catch(t){throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`,t),this._plugins.delete(e.name),this._loadOrder.pop(),t}return this}async useAsync(e){if(!e.name)throw new Error("[PluginRegistry] Plugin must have a name");if(this._plugins.has(e.name))return console.warn(`[PluginRegistry] Plugin "${e.name}" already registered. Skipping.`),this;if(!e.init||typeof e.init!="function")throw new Error(`[PluginRegistry] Plugin "${e.name}" must have an init() method`);this._plugins.set(e.name,e),this._loadOrder.push(e.name);try{await e.init(this.kernel),this.kernel.events.emit("plugin_loaded",{name:e.name,version:e.version})}catch(t){throw console.error(`[PluginRegistry] Failed to initialize plugin "${e.name}":`,t),this._plugins.delete(e.name),this._loadOrder.pop(),t}return this}remove(e){const t=this._plugins.get(e);if(!t)return!1;if(t.destroy)try{t.destroy()}catch(i){console.error(`[PluginRegistry] Error destroying plugin "${e}":`,i)}this._plugins.delete(e);const s=this._loadOrder.indexOf(e);return s>-1&&this._loadOrder.splice(s,1),this.kernel.events.emit("plugin_unloaded",{name:e}),!0}get(e){return this._plugins.get(e)}has(e){return this._plugins.has(e)}getPluginNames(){return Array.from(this._plugins.keys())}getLoadOrder(){return[...this._loadOrder]}clear(){for(let e=this._loadOrder.length-1;e>=0;e--)this.remove(this._loadOrder[e])}}class U{constructor(e={}){this.width=e.width||800,this.height=e.height||600,this.backgroundColor=e.backgroundColor||"#000000",e.canvas?(this.canvas=e.canvas,this.canvas.width=this.width,this.canvas.height=this.height):(this.canvas=document.createElement("canvas"),this.canvas.width=this.width,this.canvas.height=this.height,(e.parent?typeof e.parent=="string"?document.querySelector(e.parent):e.parent:document.body).appendChild(this.canvas)),this.ctx=this.canvas.getContext("2d")}clear(){this.ctx.fillStyle=this.backgroundColor,this.ctx.fillRect(0,0,this.width,this.height)}resize(e,t){this.width=e,this.height=t,this.canvas.width=e,this.canvas.height=t}}class R{constructor(){this.running=!1,this.lastTime=0,this.callbacks=new Set,this._frameHandle=null,this._tick=this._tick.bind(this)}add(e){this.callbacks.add(e)}remove(e){this.callbacks.delete(e)}start(){this.running||(this.running=!0,this.lastTime=performance.now(),this._frameHandle!==null&&cancelAnimationFrame(this._frameHandle),this._frameHandle=requestAnimationFrame(this._tick))}stop(){this.running=!1,this._frameHandle!==null&&(cancelAnimationFrame(this._frameHandle),this._frameHandle=null)}_tick(e){if(this._frameHandle=null,!this.running)return;const t=(e-this.lastTime)/1e3;this.lastTime=e;const s=Math.min(t,.1);for(const i of[...this.callbacks])i(s);this.running&&(this._frameHandle=requestAnimationFrame(this._tick))}}function S(a,e){let t=!1;return new Proxy(a,{get(s,i,r){t||(t=!0,console.warn(e));const n=Reflect.get(s,i,r);return typeof n=="function"?n.bind(s):n},set(s,i,r){return t||(t=!0,console.warn(e)),Reflect.set(s,i,r)}})}class g{constructor(){this.gameObject=null,this.kernel=null}get physics(){var e;return(e=this.kernel)==null?void 0:e.systems.get("physics")}get input(){var e;return(e=this.kernel)==null?void 0:e.systems.get("input")}get loader(){var e;return(e=this.kernel)==null?void 0:e.systems.get("loader")}get events(){var e;return(e=this.kernel)==null?void 0:e.events}onAttach(){}onDetach(){}update(e){}render(e){}}class $ extends g{constructor(e="box",t=50,s=50,i=25){super(),this.type=e,this.width=t,this.height=s,this.radius=i,this.tag=null}onAttach(){super.onAttach(),this.physics&&this.physics.add(this)}onDetach(){super.onDetach(),this.physics&&this.physics.remove(this)}getBounds(){const e=this.gameObject;if(this.type==="box")return{left:e.x-this.width/2,right:e.x+this.width/2,top:e.y-this.height/2,bottom:e.y+this.height/2};if(this.type==="circle")return{x:e.x,y:e.y,radius:this.radius}}}class k{constructor(){this.colliders=[],this.kernel=null}init(e){this.kernel=e}fixedUpdate(e){this.step()}destroy(){this.colliders=[]}add(e){this.colliders.push(e)}remove(e){const t=this.colliders.indexOf(e);t>-1&&this.colliders.splice(t,1)}step(){for(let e=0;e<this.colliders.length;e++)for(let t=e+1;t<this.colliders.length;t++){const s=this.colliders[e],i=this.colliders[t];this._intersects(s,s.getBounds(),i,i.getBounds())&&(this._notifyCollision(s.gameObject,i.gameObject),this._notifyCollision(i.gameObject,s.gameObject))}}_notifyCollision(e,t){!e||!e.components||e.components.forEach(s=>{s.onCollisionEnter&&s.onCollisionEnter(t)})}checkOverlap(e,t){const s=[],i=e.getBounds();for(const r of this.colliders){if(r===e||t&&r.tag!==t)continue;const n=r.getBounds();this._intersects(e,i,r,n)&&s.push(r)}return s}_intersects(e,t,s,i){if(e.type==="box"&&s.type==="box")return t.left<i.right&&t.right>i.left&&t.top<i.bottom&&t.bottom>i.top;if(e.type==="circle"&&s.type==="circle"){const r=t.x-i.x,n=t.y-i.y;return Math.sqrt(r*r+n*n)<t.radius+i.radius}return!1}addBody(e){this.add(e)}removeBody(e){this.remove(e)}checkCollision(e,t){return this._intersects(e,e.getBounds(),t,t.getBounds())}raycast(e,t,s){return null}setGravity(e,t){}}const q=S(new k,"[DopamineJS] GlobalPhysics is deprecated. Use kernel.physics instead.");class b{constructor(){this.keys=new Map,this.mouse={x:0,y:0,buttons:new Map},this.kernel=null,this._onKeyDown=this._onKeyDown.bind(this),this._onKeyUp=this._onKeyUp.bind(this),this._onMouseMove=this._onMouseMove.bind(this),this._onMouseDown=this._onMouseDown.bind(this),this._onMouseUp=this._onMouseUp.bind(this)}init(e){this.kernel=e,window.addEventListener("keydown",this._onKeyDown),window.addEventListener("keyup",this._onKeyUp),window.addEventListener("mousemove",this._onMouseMove),window.addEventListener("mousedown",this._onMouseDown),window.addEventListener("mouseup",this._onMouseUp)}destroy(){window.removeEventListener("keydown",this._onKeyDown),window.removeEventListener("keyup",this._onKeyUp),window.removeEventListener("mousemove",this._onMouseMove),window.removeEventListener("mousedown",this._onMouseDown),window.removeEventListener("mouseup",this._onMouseUp)}_onKeyDown(e){this.keys.set(e.key,!0)}_onKeyUp(e){this.keys.set(e.key,!1)}_onMouseMove(e){this.mouse.x=e.clientX,this.mouse.y=e.clientY}_onMouseDown(e){this.mouse.buttons.set(e.button,!0)}_onMouseUp(e){this.mouse.buttons.set(e.button,!1)}isKeyDown(e){return this.keys.get(e)||!1}isMouseButtonDown(e=0){return this.mouse.buttons.get(e)||!1}}const K=S(new b,"[DopamineJS] GlobalInput is deprecated. Use kernel.input instead.");class M{constructor(){this.assets=new Map,this.kernel=null}init(e){this.kernel=e}destroy(){this.assets.clear()}async loadImage(e,t){return new Promise((s,i)=>{const r=new Image;r.onload=()=>{this.assets.set(e,r),s(r)},r.onerror=i,r.src=t})}get(e){return this.assets.get(e)}}const X=S(new M,"[DopamineJS] GlobalLoader is deprecated. Use kernel.loader instead.");class G{constructor(e={}){this.config=e,this._boundUpdate=this._update.bind(this),this.events=new p,this.systems=new A(this),this.plugins=new L(this),this._registerCoreSystems(e),this.fixedTimestep=e.fixedTimestep||1/60,this._accumulator=0,this._maxAccumulator=this.fixedTimestep*5}_registerCoreSystems(e){this.systems.register("ticker",new R,{priority:100});const t={width:e.width,height:e.height,backgroundColor:e.backgroundColor,canvas:e.canvas,...e.renderer};this.systems.register("renderer",new U(t),{priority:90}),this.systems.register("input",new b,{priority:80}),this.systems.register("loader",new M,{priority:70}),this.systems.register("physics",new k,{priority:60})}start(){const e=this.systems.get("ticker");e&&(e.add(this._boundUpdate),e.start()),this.events.emit("kernel_started")}stop(){const e=this.systems.get("ticker");e&&e.stop(),this.events.emit("kernel_stopped")}_update(e){for(this.events.emit(p.Events.TICK,{dt:e}),this._accumulator+=e,this._accumulator>this._maxAccumulator&&(this._accumulator=this._maxAccumulator);this._accumulator>=this.fixedTimestep;)this.events.emit(p.Events.FIXED_UPDATE,{dt:this.fixedTimestep}),this.systems.fixedUpdate(this.fixedTimestep),this._accumulator-=this.fixedTimestep;this.systems.update(e),this.events.emit(p.Events.RENDER,{dt:e})}get physics(){return this.systems.get("physics")}get input(){return this.systems.get("input")}get loader(){return this.systems.get("loader")}get renderer(){return this.systems.get("renderer")}get ticker(){return this.systems.get("ticker")}destroy(){this.stop();const e=this.systems.get("ticker");e&&e.remove(this._boundUpdate),this.plugins.clear(),this.systems.clear(),this.events.clear()}}const _={init(a){},update(a){},fixedUpdate(a){},destroy(){}};class J{constructor(){this.kernel=null}init(e){this.kernel=e}update(e){}fixedUpdate(e){}destroy(){}}const W={..._,addBody(a){},removeBody(a){},checkCollision(a,e){},raycast(a,e,t){},setGravity(a,e){},step(a){}},V={..._,registerSound(a,e){},preloadSounds(a){},play(a,e){},playTone(a,e,t,s){},stop(a){},toggleMute(){},setVolume(a){}},Y={..._,registerSprite(a,e){},registerEffect(a,e){},play(a,e,t,...s){},emit(a){},clear(){},confetti(a,e,t){},coinShower(a,e,t){},sparkle(a,e,t,s){},fire(a,e,t){},starBurst(a,e,t){}};class Q{constructor(){this.gameObjects=[],this.kernel=null}onEnter(){}onExit(){}add(e){return this.kernel&&(e.kernel=this.kernel),this.gameObjects.push(e),e}remove(e){const t=this.gameObjects.indexOf(e);t>-1&&this.gameObjects.splice(t,1)}update(e){for(const t of this.gameObjects)t.update(e)}render(e){for(const t of this.gameObjects)t.render(e)}}class z{constructor(e){this.game=e,this.currentScene=null}run(e){this.currentScene&&this.currentScene.onExit(),this.currentScene=e,this.currentScene.game=this.game,this.game.scene=e,e.onEnter()}}class Z{constructor(e={}){this.config=e,this.kernel=new G(e),this.director=new z(this),this.kernel.systems.register("director",this.director),this.kernel.events.on(p.Events.TICK,this._update.bind(this),10),this.kernel.events.on(p.Events.RENDER,this._render.bind(this),10)}get renderer(){return this.kernel.renderer}get ticker(){return this.kernel.ticker}get physics(){return this.kernel.physics}get input(){return this.kernel.input}start(){this.kernel.start()}stop(){this.kernel.stop()}setScene(e){e.kernel=this.kernel,this.director.run(e)}_update({dt:e}){this.director.currentScene&&this.director.currentScene.update(e)}_render({dt:e}){this.renderer.clear(),this.director.currentScene&&this.director.currentScene.render(this.renderer.ctx)}destroy(){this.kernel.destroy()}}class ee{constructor(e=0,t=0){this.x=e,this.y=t,this.rotation=0,this.scale={x:1,y:1},this.components=[],this.children=[],this.parent=null,this._kernel=null}get kernel(){return this._kernel}set kernel(e){this._kernel=e;for(const t of this.components)t.kernel=e;for(const t of this.children)t.kernel=e}addChild(e){return e.parent=this,e.kernel=this.kernel,this.children.push(e),e}addComponent(e){return e.gameObject=this,e.kernel=this.kernel,this.components.push(e),e.onAttach(),e}getComponent(e){return this.components.find(t=>t instanceof e)}update(e){for(const t of this.components)t.update&&t.update(e);for(const t of this.children)t.update(e)}render(e){e.save(),e.translate(this.x,this.y),e.rotate(this.rotation),e.scale(this.scale.x,this.scale.y);for(const t of this.components)t.render&&t.render(e);for(const t of this.children)t.render(e);e.restore()}}class E{constructor(e=0,t=0){this.x=e,this.y=t}add(e){return this.x+=e.x,this.y+=e.y,this}sub(e){return this.x-=e.x,this.y-=e.y,this}scale(e){return this.x*=e,this.y*=e,this}distance(e){const t=this.x-e.x,s=this.y-e.y;return Math.sqrt(t*t+s*s)}normalize(){const e=Math.sqrt(this.x*this.x+this.y*this.y);return e>0&&(this.x/=e,this.y/=e),this}clone(){return new E(this.x,this.y)}static distance(e,t){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}}class j extends g{constructor(e){super(),this.image=e,this.anchor={x:.5,y:.5},this.width=e?e.width:0,this.height=e?e.height:0,this.frame={x:0,y:0,w:this.width,h:this.height}}setFrame(e,t,s,i){this.frame={x:e,y:t,w:s,h:i},this.width=s,this.height=i}setTexture(e){this.image=e,e&&this.setFrame(0,0,e.width,e.height)}render(e){if(!this.image)return;const t=this.frame.w,s=this.frame.h,i=-t*this.anchor.x,r=-s*this.anchor.y;e.drawImage(this.image,this.frame.x,this.frame.y,t,s,i,r,t,s)}}class te extends g{constructor(){super(),this.animations={},this.currentAnim=null,this.timer=0,this.currentFrameIndex=0,this.sprite=null}onAttach(){this.sprite=this.gameObject.getComponent(j),this.sprite||console.warn("Animator attached to GameObject without a Sprite component.")}addAnimation(e,t,s=10,i=!0){this.animations[e]={frames:t,fps:s,loop:i}}play(e){this.currentAnim&&this.currentAnim.name===e||this.animations[e]&&(this.currentAnim={name:e,...this.animations[e]},this.currentFrameIndex=0,this.timer=0,this._updateFrame())}update(e){if(!this.currentAnim||!this.sprite)return;this.timer+=e;const t=1/this.currentAnim.fps;this.timer>=t&&(this.timer-=t,this.currentFrameIndex++,this.currentFrameIndex>=this.currentAnim.frames.length&&(this.currentAnim.loop?this.currentFrameIndex=0:this.currentFrameIndex=this.currentAnim.frames.length-1),this._updateFrame())}_updateFrame(){const e=this.currentAnim.frames[this.currentFrameIndex];this.sprite.setFrame(e.x,e.y,e.w,e.h)}}class se{static createGround(e,t,s="#333"){const i=document.createElement("canvas");i.width=e,i.height=t;const r=i.getContext("2d");r.fillStyle=s,r.fillRect(0,0,e,t);for(let n=0;n<1e3;n++){r.fillStyle=Math.random()>.5?"#444":"#222";const o=Math.random()*4+2;r.fillRect(Math.random()*e,Math.random()*t,o,o)}r.strokeStyle="rgba(255,255,255,0.05)",r.lineWidth=2,r.beginPath();for(let n=0;n<=e;n+=64)r.moveTo(n,0),r.lineTo(n,t);for(let n=0;n<=t;n+=64)r.moveTo(0,n),r.lineTo(e,n);return r.stroke(),i}static createCharacterSheet(e,t){const r=document.createElement("canvas");r.width=64*4,r.height=64;const n=r.getContext("2d");for(let o=0;o<4;o++){const l=o*64,c=0,d=l+64/2,m=c+64/2,y=o%2===0?0:-2,v=o===1?5:o===3?-5:0;n.fillStyle=e,n.beginPath(),n.arc(d,m+y,20,0,Math.PI*2),n.fill(),n.fillStyle=t,n.beginPath(),n.arc(d-15,m+y+v,8,0,Math.PI*2),n.arc(d+15,m+y-v,8,0,Math.PI*2),n.fill(),n.fillStyle=ie(e),n.beginPath(),n.arc(d,m+y,15,0,Math.PI*2),n.fill(),t==="#333"&&(n.fillStyle="#000",n.fillRect(d+10,m+y-5,25,6))}return r}}function ie(a,e){return a}class re extends g{constructor(e){super(),this.system=e}play(e,t={}){const s=this.gameObject.x,i=this.gameObject.y;switch(e){case"confetti":this.system.confetti(s,i,t.count);break;case"fire":this.system.fire(s,i,t.count);break;case"sparkle":this.system.sparkle(s,i,t.count,t.color);break;case"blood":this.system.emit({x:s,y:i,count:10,color:["#8a0303","#ff0000"],speed:4,gravity:.1,decay:.05});break;default:this.system.emit({x:s,y:i,...t})}}}class ne extends g{constructor(){super(),this.intensity=0,this.duration=0,this.timer=0,this.originalPos={x:0,y:0},this.isShaking=!1}shake(e=5,t=.2){this.intensity=e,this.duration=t,this.timer=0,this.isShaking=!0,this.timer===0&&(this.originalPos.x=this.gameObject.x,this.originalPos.y=this.gameObject.y)}update(e){if(!this.isShaking)return;if(this.timer+=e,this.timer>=this.duration){this.isShaking=!1,this.gameObject.x=this.originalPos.x,this.gameObject.y=this.originalPos.y;return}const t=(Math.random()-.5)*2*this.intensity,s=(Math.random()-.5)*2*this.intensity;this.gameObject.x=this.originalPos.x+t,this.gameObject.y=this.originalPos.y+s}}h.Animator=te,h.Collider=$,h.Component=g,h.DataService=C,h.Director=z,h.DopamineKernel=G,h.EventBus=p,h.EventEmitter=f,h.Game=Z,h.GameObject=ee,h.GameUI=I,h.GlobalInput=K,h.GlobalLoader=X,h.GlobalPhysics=q,h.IAudioSystem=V,h.IParticleSystem=Y,h.IPhysicsSystem=W,h.ISystem=_,h.Input=b,h.Loader=M,h.ParticleEmitter=re,h.ParticleSystem=O,h.Physics=k,h.PluginRegistry=L,h.Renderer=U,h.RewardSystem=P,h.Scene=Q,h.ScreenShake=ne,h.SoundManager=T,h.Sprite=j,h.System=J,h.SystemRegistry=A,h.TextureGenerator=se,h.Ticker=R,h.Vector2=E,h.createMemoryStorage=w,h.default=H,h.resolveStorage=x,Object.defineProperties(h,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});