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.
@@ -0,0 +1,617 @@
1
+ /**
2
+ * Type declarations for dopaminejs.
3
+ *
4
+ * Hand-written to match src/index.js. The runtime is plain JavaScript with
5
+ * JSDoc; these exist so TypeScript consumers get completion and checking
6
+ * without the project adopting TypeScript.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Reward system
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface PlayerStreak {
14
+ current: number;
15
+ longest: number;
16
+ /** Local calendar day, YYYY-MM-DD. Not UTC. */
17
+ lastPlayDate: string;
18
+ }
19
+
20
+ export interface UnlockedAchievement {
21
+ unlockedAt: number;
22
+ seen: boolean;
23
+ }
24
+
25
+ export interface Player {
26
+ name: string;
27
+ xp: number;
28
+ level: number;
29
+ totalGamesPlayed: number;
30
+ createdAt: number;
31
+ lastPlayedAt: number;
32
+ streak: PlayerStreak;
33
+ achievements: Record<string, UnlockedAchievement>;
34
+ /** Per-game stats. Games add their own keys. */
35
+ stats: Record<string, Record<string, number>>;
36
+ }
37
+
38
+ export interface AchievementDefinition {
39
+ name: string;
40
+ description?: string;
41
+ /** Rendered as HTML, so an <img> or inline SVG is allowed here. */
42
+ icon?: string;
43
+ /** Omitted means 0. Never undefined at runtime. */
44
+ xp?: number;
45
+ check(player: Player, gameName?: string, result?: GameResult): boolean;
46
+ }
47
+
48
+ export interface GameResult {
49
+ score?: number;
50
+ [metric: string]: number | undefined;
51
+ }
52
+
53
+ export interface XPProgress {
54
+ /** Total XP marking the next level. */
55
+ total: number;
56
+ /** XP still required, floored at 0. */
57
+ needed: number;
58
+ /** Position within the current level band, 0 to 1 inclusive. */
59
+ progress: number;
60
+ }
61
+
62
+ export interface AddXPResult {
63
+ leveledUp: boolean;
64
+ newLevel: number;
65
+ xpGained: number;
66
+ }
67
+
68
+ export interface RewardSystemConfig {
69
+ achievements?: Record<string, AchievementDefinition>;
70
+ /** Mirror reward events onto a kernel EventBus. */
71
+ events?: EventBus;
72
+ /** Alternative to `events`; the kernel's bus is used. */
73
+ kernel?: DopamineKernel;
74
+ }
75
+
76
+ export type RewardEvent =
77
+ | 'xp_gained'
78
+ | 'level_up'
79
+ | 'achievement_unlocked'
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';
85
+
86
+ export class EventEmitter {
87
+ on(event: string, callback: (data?: unknown) => void): () => void;
88
+ off(event: string, callback: (data?: unknown) => void): void;
89
+ emit(event: string, data?: unknown): void;
90
+ clear(): void;
91
+ }
92
+
93
+ export class RewardSystem extends EventEmitter {
94
+ constructor(dataService: DataService, config?: RewardSystemConfig);
95
+
96
+ player: Player | null;
97
+ achievements: Record<string, AchievementDefinition>;
98
+ /** Kernel bus events are mirrored onto, if one was supplied. */
99
+ events: EventBus | null;
100
+
101
+ init(): Promise<Player>;
102
+ save(): Promise<void>;
103
+
104
+ /** @throws TypeError if `amount` is not finite. */
105
+ addXP(amount: number, reason?: string): Promise<AddXPResult>;
106
+ getXPForNextLevel(): XPProgress;
107
+
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>;
114
+
115
+ checkAchievements(gameName: string, result: GameResult): Promise<AchievementDefinition[]>;
116
+ unlockAchievement(achievementId: string): Promise<boolean>;
117
+ getUnlockedAchievements(): Array<AchievementDefinition & UnlockedAchievement & { id: string }>;
118
+ getUnseenAchievements(): Array<AchievementDefinition & UnlockedAchievement & { id: string }>;
119
+ markAchievementsSeen(achievementIds: string[]): Promise<void>;
120
+ getStreakMultiplier(): number;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Persistence
125
+ // ---------------------------------------------------------------------------
126
+
127
+ export interface StorageLike {
128
+ getItem(key: string): string | null;
129
+ setItem(key: string, value: string): void;
130
+ removeItem(key: string): void;
131
+ }
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
+
144
+ export interface DataServiceConfig {
145
+ /** Defaults to localStorage, falling back to an in-memory store. */
146
+ storage?: StorageLike | AsyncStorageLike;
147
+ /** Key prefix, default 'dopamine_'. */
148
+ prefix?: string;
149
+ }
150
+
151
+ export class DataService {
152
+ constructor(config?: DataServiceConfig);
153
+ storage: StorageLike | AsyncStorageLike;
154
+ prefix: string;
155
+ save(key: string, data: unknown): Promise<boolean>;
156
+ load<T = unknown>(key: string, defaultValue?: T | null): Promise<T | null>;
157
+ clear(key: string): Promise<void>;
158
+ }
159
+
160
+ /** Storage backed by a Map. Used when localStorage is absent or blocked. */
161
+ export function createMemoryStorage(): StorageLike;
162
+ /** localStorage if present and writable, otherwise an in-memory store. */
163
+ export function resolveStorage(): StorageLike;
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // UI
167
+ // ---------------------------------------------------------------------------
168
+
169
+ export interface SummaryData {
170
+ score: number | string;
171
+ /** Label to value pairs. Rendered as text. */
172
+ metrics?: Record<string, number | string>;
173
+ /** Defaults to reloading the page. */
174
+ onReplay?: () => void;
175
+ /** Exit button is omitted when this is not supplied. */
176
+ onExit?: () => void;
177
+ }
178
+
179
+ export class GameUI {
180
+ constructor(particleSystem: ParticleSystem);
181
+
182
+ container: HTMLElement | null;
183
+
184
+ /** `progress` is the position within the current level, 0 to 1. */
185
+ updateXP(current: number, needed: number, total: number, progress?: number): void;
186
+ updateLevel(level: number): void;
187
+ updateStreak(days: number): void;
188
+
189
+ showAchievement(achievement: AchievementDefinition): void;
190
+ showLevelUp(oldLevel: number, newLevel: number): void;
191
+ showFloatingText(text: string, x: number, y: number, color?: string, size?: string): void;
192
+ showCombo(multiplier: number, x: number, y: number): void;
193
+ showNearMiss(x: number, y: number): void;
194
+ showLuckyMoment(multiplier: number, x: number, y: number): void;
195
+ showNotification(message: string, type?: 'normal' | 'rare' | 'legendary'): void;
196
+ showSummary(data: SummaryData): HTMLElement;
197
+
198
+ /** Removes the overlay, any transient popups, and all pending timers. */
199
+ destroy(): void;
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Particles
204
+ // ---------------------------------------------------------------------------
205
+
206
+ export interface ParticleSystemConfig {
207
+ /** Element or selector. Defaults to document.body. */
208
+ container?: HTMLElement | string;
209
+ canvasId?: string;
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;
215
+ }
216
+
217
+ export interface ParticleConfig {
218
+ x: number;
219
+ y: number;
220
+ count?: number;
221
+ color?: string;
222
+ size?: number;
223
+ life?: number;
224
+ decay?: number;
225
+ gravity?: number;
226
+ spread?: number;
227
+ speed?: number;
228
+ type?: string;
229
+ sprite?: string;
230
+ }
231
+
232
+ export class ParticleSystem {
233
+ constructor(config?: ParticleSystemConfig);
234
+
235
+ canvas: HTMLCanvasElement;
236
+ /** Width in CSS pixels. The backing store is scaled by devicePixelRatio. */
237
+ width: number;
238
+ height: number;
239
+ isAnimating: boolean;
240
+
241
+ registerSprite(key: string, url: string): void;
242
+ registerEffect(name: string, callback: (x: number, y: number, ...args: unknown[]) => void): void;
243
+ play(name: string, x: number, y: number, ...args: unknown[]): void;
244
+ emit(config: ParticleConfig): void;
245
+
246
+ confetti(x: number, y: number, count?: number): void;
247
+ coinShower(x: number, y: number, count?: number): void;
248
+ sparkle(x: number, y: number, count?: number, color?: string): void;
249
+ fire(x: number, y: number, count?: number): void;
250
+ starBurst(x: number, y: number, count?: number): void;
251
+
252
+ clear(): void;
253
+ /** Stops animation, detaches resize handling, removes the canvas. */
254
+ destroy(): void;
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Audio
259
+ // ---------------------------------------------------------------------------
260
+
261
+ export interface SoundManagerConfig {
262
+ storageKey?: string;
263
+ storage?: StorageLike;
264
+ /** Master volume 0 to 1, default 1. */
265
+ volume?: number;
266
+ /** Key to URL map, loaded when the audio context is first created. */
267
+ customSounds?: Record<string, string>;
268
+ }
269
+
270
+ export class SoundManager {
271
+ constructor(config?: SoundManagerConfig);
272
+
273
+ muted: boolean;
274
+ assets: Map<string, AudioBuffer>;
275
+ customSounds: Record<string, string>;
276
+
277
+ /** Returns false when no Web Audio API is available, rather than throwing. */
278
+ initAudio(): boolean;
279
+ setVolume(value: number): number;
280
+ getVolume(): number;
281
+ toggleMute(): boolean;
282
+
283
+ registerSound(key: string, url: string): void;
284
+ preloadSounds(soundMap: Record<string, string>): Promise<void>;
285
+ loadSound(key: string, url: string): Promise<void>;
286
+ play(key: string): Promise<void>;
287
+ playTone(frequency: number, duration: number, type?: OscillatorType, volume?: number): void;
288
+
289
+ playJump(force?: boolean): void;
290
+ playScore(force?: boolean): void;
291
+ playGameOver(force?: boolean): void;
292
+ playClick(force?: boolean): void;
293
+ playSuccess(force?: boolean): void;
294
+ playError(force?: boolean): void;
295
+
296
+ /** Cancels queued tones, closes the audio context, drops decoded buffers. */
297
+ destroy(): void;
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Kernel
302
+ // ---------------------------------------------------------------------------
303
+
304
+ export interface EventBusEvents {
305
+ TICK: 'tick';
306
+ FIXED_UPDATE: 'fixed_update';
307
+ RENDER: 'render';
308
+ COLLISION_ENTER: 'collision_enter';
309
+ COLLISION_EXIT: 'collision_exit';
310
+ XP_GAINED: 'xp_gained';
311
+ LEVEL_UP: 'level_up';
312
+ ACHIEVEMENT_UNLOCKED: 'achievement_unlocked';
313
+ NEW_HIGH_SCORE: 'new_high_score';
314
+ STREAK_UPDATED: 'streak_updated';
315
+ SYSTEM_REGISTERED: 'system_registered';
316
+ SYSTEM_UNREGISTERED: 'system_unregistered';
317
+ PLUGIN_LOADED: 'plugin_loaded';
318
+ PLUGIN_UNLOADED: 'plugin_unloaded';
319
+ }
320
+
321
+ export class EventBus {
322
+ static readonly Events: EventBusEvents;
323
+
324
+ /** Higher priority runs first. */
325
+ on(event: string, callback: (data?: any) => void, priority?: number): this;
326
+ once(event: string, callback: (data?: any) => void): this;
327
+ /** Removes every registration of `callback` for `event`. */
328
+ off(event: string, callback: (data?: any) => void): this;
329
+ /** A throwing listener is logged and does not abort the remaining ones. */
330
+ emit(event: string, data?: any): void;
331
+ clear(event?: string): void;
332
+ hasListeners(event: string): boolean;
333
+ }
334
+
335
+ export interface ISystemLike {
336
+ init?(kernel: DopamineKernel): void;
337
+ update?(dt: number): void;
338
+ fixedUpdate?(dt: number): void;
339
+ destroy?(): void;
340
+ }
341
+
342
+ export interface SystemRegisterOptions {
343
+ /** Higher runs earlier. Default 0. */
344
+ priority?: number;
345
+ /** Names of systems that must update first. */
346
+ dependencies?: string[];
347
+ }
348
+
349
+ export class SystemRegistry {
350
+ constructor(kernel: DopamineKernel);
351
+ register(name: string, system: ISystemLike, options?: SystemRegisterOptions): this;
352
+ get<T = ISystemLike>(name: string): T | undefined;
353
+ has(name: string): boolean;
354
+ unregister(name: string): boolean;
355
+ update(dt: number): void;
356
+ fixedUpdate(dt: number): void;
357
+ getSystemNames(): string[];
358
+ clear(): void;
359
+ }
360
+
361
+ export interface Plugin {
362
+ name: string;
363
+ version?: string;
364
+ init(kernel: DopamineKernel): void | Promise<void>;
365
+ destroy?(): void;
366
+ }
367
+
368
+ export class PluginRegistry {
369
+ constructor(kernel: DopamineKernel);
370
+ use(plugin: Plugin): this;
371
+ useAsync(plugin: Plugin): Promise<this>;
372
+ remove(name: string): boolean;
373
+ get(name: string): Plugin | undefined;
374
+ has(name: string): boolean;
375
+ getPluginNames(): string[];
376
+ getLoadOrder(): string[];
377
+ clear(): void;
378
+ }
379
+
380
+ export interface KernelConfig {
381
+ width?: number;
382
+ height?: number;
383
+ backgroundColor?: string;
384
+ canvas?: HTMLCanvasElement;
385
+ renderer?: Record<string, unknown>;
386
+ /** Seconds per fixed update. Default 1/60. */
387
+ fixedTimestep?: number;
388
+ }
389
+
390
+ export class DopamineKernel {
391
+ constructor(config?: KernelConfig);
392
+
393
+ config: KernelConfig;
394
+ events: EventBus;
395
+ systems: SystemRegistry;
396
+ plugins: PluginRegistry;
397
+ fixedTimestep: number;
398
+
399
+ readonly physics: Physics;
400
+ readonly input: Input;
401
+ readonly loader: Loader;
402
+ readonly renderer: Renderer;
403
+ readonly ticker: Ticker;
404
+
405
+ start(): void;
406
+ stop(): void;
407
+ destroy(): void;
408
+ }
409
+
410
+ // ---------------------------------------------------------------------------
411
+ // Engine core
412
+ // ---------------------------------------------------------------------------
413
+
414
+ export class Vector2 {
415
+ constructor(x?: number, y?: number);
416
+ x: number;
417
+ y: number;
418
+ add(v: Vector2): Vector2;
419
+ sub(v: Vector2): Vector2;
420
+ scale(s: number): Vector2;
421
+ distance(v: Vector2): number;
422
+ normalize(): Vector2;
423
+ clone(): Vector2;
424
+ static distance(a: Vector2, b: Vector2): number;
425
+ }
426
+
427
+ export class Component {
428
+ gameObject: GameObject | null;
429
+ kernel: DopamineKernel | null;
430
+ onAttach(): void;
431
+ onDetach(): void;
432
+ update(dt: number): void;
433
+ render(ctx: CanvasRenderingContext2D): void;
434
+ }
435
+
436
+ export interface Bounds {
437
+ x: number;
438
+ y: number;
439
+ width: number;
440
+ height: number;
441
+ }
442
+
443
+ export class Collider extends Component {
444
+ constructor(type?: 'box' | 'circle', width?: number, height?: number, radius?: number);
445
+ type: 'box' | 'circle';
446
+ width: number;
447
+ height: number;
448
+ radius: number;
449
+ getBounds(): Bounds;
450
+ }
451
+
452
+ export class Sprite extends Component {
453
+ constructor(image: HTMLImageElement | HTMLCanvasElement);
454
+ setFrame(x: number, y: number, w: number, h: number): void;
455
+ setTexture(image: HTMLImageElement | HTMLCanvasElement): void;
456
+ }
457
+
458
+ export class Animator extends Component {
459
+ addAnimation(name: string, frames: unknown[], fps?: number, loop?: boolean): void;
460
+ play(name: string): void;
461
+ }
462
+
463
+ export class GameObject {
464
+ constructor(x?: number, y?: number);
465
+ position: Vector2;
466
+ rotation: number;
467
+ scale: Vector2;
468
+ components: Component[];
469
+ children: GameObject[];
470
+ kernel: DopamineKernel | null;
471
+ tag?: string;
472
+ addChild(child: GameObject): GameObject;
473
+ addComponent<T extends Component>(component: T): T;
474
+ getComponent<T extends Component>(type: new (...args: any[]) => T): T | undefined;
475
+ update(dt: number): void;
476
+ render(ctx: CanvasRenderingContext2D): void;
477
+ }
478
+
479
+ export class Scene {
480
+ gameObjects: GameObject[];
481
+ kernel: DopamineKernel | null;
482
+ onEnter(): void;
483
+ onExit(): void;
484
+ add(gameObject: GameObject): GameObject;
485
+ remove(gameObject: GameObject): void;
486
+ update(dt: number): void;
487
+ render(ctx: CanvasRenderingContext2D): void;
488
+ }
489
+
490
+ export class Renderer {
491
+ constructor(options?: Record<string, unknown>);
492
+ canvas: HTMLCanvasElement;
493
+ ctx: CanvasRenderingContext2D;
494
+ clear(): void;
495
+ resize(width: number, height: number): void;
496
+ }
497
+
498
+ export class Ticker {
499
+ add(callback: (dt: number) => void): void;
500
+ remove(callback: (dt: number) => void): void;
501
+ /** Cancels any pending frame first, so restarting cannot double-step. */
502
+ start(): void;
503
+ stop(): void;
504
+ running: boolean;
505
+ callbacks: Set<(dt: number) => void>;
506
+ }
507
+
508
+ export class Loader {
509
+ init(kernel: DopamineKernel): void;
510
+ destroy(): void;
511
+ loadImage(key: string, url: string): Promise<HTMLImageElement>;
512
+ get(key: string): HTMLImageElement | undefined;
513
+ }
514
+
515
+ export class Input {
516
+ init(kernel: DopamineKernel): void;
517
+ destroy(): void;
518
+ isKeyDown(key: string): boolean;
519
+ isMouseButtonDown(button?: number): boolean;
520
+ mouse: { x: number; y: number };
521
+ }
522
+
523
+ export class Physics {
524
+ init(kernel: DopamineKernel): void;
525
+ fixedUpdate(dt: number): void;
526
+ destroy(): void;
527
+ add(collider: Collider): void;
528
+ remove(collider: Collider): void;
529
+ step(): void;
530
+ checkOverlap(source: Collider, targetTag: string): GameObject | null;
531
+ addBody(body: Collider): void;
532
+ removeBody(body: Collider): void;
533
+ checkCollision(a: Collider, b: Collider): boolean;
534
+ /** Not implemented. Always null. */
535
+ raycast(origin: Vector2, direction: Vector2, distance: number): null;
536
+ /** Not implemented. */
537
+ setGravity(x: number, y: number): void;
538
+ }
539
+
540
+ export class Director {
541
+ constructor(game: Game);
542
+ run(scene: Scene): void;
543
+ }
544
+
545
+ export class Game {
546
+ constructor(config?: KernelConfig);
547
+ kernel: DopamineKernel;
548
+ scene: Scene | null;
549
+ start(): void;
550
+ stop(): void;
551
+ setScene(newScene: Scene): void;
552
+ destroy(): void;
553
+ }
554
+
555
+ export class TextureGenerator {
556
+ static createGround(width: number, height: number, color?: string): HTMLCanvasElement;
557
+ static createCharacterSheet(colorBody: string, colorDetail: string): HTMLCanvasElement;
558
+ }
559
+
560
+ export class ParticleEmitter extends Component {
561
+ constructor(particleSystem: ParticleSystem);
562
+ play(effectName: string, options?: Record<string, unknown>): void;
563
+ }
564
+
565
+ export class ScreenShake extends Component {
566
+ shake(intensity?: number, duration?: number): void;
567
+ }
568
+
569
+ /** Deprecated singletons kept for v1 compatibility. Prefer kernel.<system>. */
570
+ export const GlobalLoader: Loader;
571
+ export const GlobalInput: Input;
572
+ export const GlobalPhysics: Physics;
573
+
574
+ // ---------------------------------------------------------------------------
575
+ // Interfaces
576
+ // ---------------------------------------------------------------------------
577
+
578
+ export const ISystem: Record<string, unknown>;
579
+ export const System: Record<string, unknown>;
580
+ export const IPhysicsSystem: Record<string, unknown>;
581
+ export const IAudioSystem: Record<string, unknown>;
582
+ export const IParticleSystem: Record<string, unknown>;
583
+
584
+ // ---------------------------------------------------------------------------
585
+ // Facade
586
+ // ---------------------------------------------------------------------------
587
+
588
+ export interface DopamineConfig {
589
+ data?: DataServiceConfig;
590
+ rewards?: RewardSystemConfig;
591
+ sound?: SoundManagerConfig;
592
+ particles?: ParticleSystemConfig;
593
+ }
594
+
595
+ export interface DopamineSubsystems {
596
+ rewardSystem: RewardSystem;
597
+ gameUI: GameUI;
598
+ particleSystem: ParticleSystem;
599
+ soundManager: SoundManager;
600
+ }
601
+
602
+ /**
603
+ * Wires DataService, RewardSystem, SoundManager, ParticleSystem and GameUI
604
+ * together and binds the UI to reward events.
605
+ */
606
+ export default class Dopamine {
607
+ constructor(config?: DopamineConfig);
608
+ config: DopamineConfig;
609
+ dataService: DataService;
610
+ rewardSystem: RewardSystem;
611
+ soundManager: SoundManager;
612
+ particleSystem: ParticleSystem;
613
+ gameUI: GameUI;
614
+ init(): Promise<DopamineSubsystems>;
615
+ /** Removes the overlay and canvas, closes audio, detaches from reward events. */
616
+ destroy(): void;
617
+ }