@utsp/runtime-client 0.11.0 → 0.12.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/dist/index.d.ts CHANGED
@@ -1,9 +1,704 @@
1
+ import { Core, User, AnyPostProcessOrder } from '@utsp/core';
2
+ import { IApplication, IRenderer } from '@utsp/types';
3
+ export { IApplication } from '@utsp/types';
1
4
  import { AutoplayOverlayOptions, PostProcessOverlay } from '@utsp/render';
2
5
  export { ScalingMode } from '@utsp/render';
3
6
  import { AudioManager } from '@utsp/audio';
4
7
  export { AudioManager, AudioManagerOptions, ToneOptions } from '@utsp/audio';
5
- import { IApplication, ScalingMode } from '@utsp/types';
6
- export { IApplication } from '@utsp/types';
8
+ import * as _utsp_input from '@utsp/input';
9
+ import { UnifiedInputRouter, MobileVibration } from '@utsp/input';
10
+
11
+ /**
12
+ * Base Runtime Types
13
+ *
14
+ * Types shared by all client runtime variants (Micro, Mini, Standard)
15
+ */
16
+
17
+ /**
18
+ * Renderer type to use
19
+ */
20
+ declare enum RendererType {
21
+ /** WebGL2 renderer - Optimized WebGL 1.0 with GPU palette, instanced rendering (default, recommended) */
22
+ TerminalGL = "webgl",
23
+ /** Canvas 2D renderer - CPU fallback with ImageData optimization for benchmarks */
24
+ Terminal2D = "terminal2d"
25
+ }
26
+ /**
27
+ * Render mode for runtimes
28
+ */
29
+ type RenderMode =
30
+ /** Continuous rendering at max FPS (default) - requestAnimationFrame loop */
31
+ 'continuous'
32
+ /** On-demand rendering - only renders when explicitly requested via requestRender() */
33
+ | 'on-demand';
34
+ /**
35
+ * Base options shared by all client runtime variants
36
+ */
37
+ interface BaseRuntimeOptions {
38
+ /** Application instance */
39
+ application: IApplication;
40
+ /** Container HTML element for rendering */
41
+ container: HTMLElement;
42
+ /** Enable debug logging */
43
+ debug?: boolean;
44
+ /** Initial display width in columns (default: 80). Can be overridden by IApplication via Display */
45
+ width?: number;
46
+ /** Initial display height in rows (default: 25). Can be overridden by IApplication via Display */
47
+ height?: number;
48
+ /** Renderer type (default: TerminalGL) */
49
+ renderer?: RendererType;
50
+ /** Enable ImageData rendering for Canvas 2D (default: true). */
51
+ useImageDataRendering?: boolean;
52
+ /** Render mode: 'continuous' (default) or 'on-demand' */
53
+ renderMode?: RenderMode;
54
+ }
55
+ /**
56
+ * Performance timing for a single frame
57
+ */
58
+ interface FrameTiming$1 {
59
+ /** Time spent in Core tick (ms) */
60
+ coreTime: number;
61
+ /** Time spent in Renderer (ms) */
62
+ renderTime: number;
63
+ /** Total frame time (ms) */
64
+ totalTime: number;
65
+ }
66
+ /**
67
+ * Base runtime statistics
68
+ */
69
+ interface BaseRuntimeStats {
70
+ /** Is runtime currently running */
71
+ running: boolean;
72
+ /** Number of users */
73
+ userCount: number;
74
+ /** Current FPS */
75
+ fps: number;
76
+ /** Uptime in milliseconds */
77
+ uptime: number;
78
+ /** Total frames processed */
79
+ totalFrames: number;
80
+ /** Last frame timing breakdown */
81
+ lastFrameTiming?: FrameTiming$1;
82
+ /** Average frame timing over last 60 frames */
83
+ avgFrameTiming?: FrameTiming$1;
84
+ }
85
+
86
+ /**
87
+ * Performance Monitor
88
+ *
89
+ * Tracks and monitors runtime performance metrics including FPS,
90
+ * frame timing, and rolling averages.
91
+ *
92
+ * This class is responsible for:
93
+ * - FPS calculation and tracking
94
+ * - Frame timing measurements (core, render, total)
95
+ * - Rolling averages over configurable sample size
96
+ * - Performance statistics reporting
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const monitor = new PerformanceMonitor(60); // 60 sample rolling average
101
+ *
102
+ * // In initialization
103
+ * monitor.startTracking(performance.now());
104
+ *
105
+ * // Every frame
106
+ * monitor.updateFPS(timestamp);
107
+ *
108
+ * // After frame work
109
+ * monitor.recordFrameTiming(coreTime, renderTime);
110
+ *
111
+ * // Get metrics
112
+ * const stats = monitor.getStats();
113
+ * console.log(`FPS: ${stats.fps}, Avg Frame Time: ${stats.avgTotalTime}ms`);
114
+ * ```
115
+ */
116
+ /**
117
+ * Frame timing data for a single frame
118
+ */
119
+ interface FrameTiming {
120
+ /** Time spent in core logic (ms) */
121
+ coreTime: number;
122
+ /** Time spent in rendering (ms) */
123
+ renderTime: number;
124
+ /** Total frame time (ms) */
125
+ totalTime: number;
126
+ }
127
+ /**
128
+ * Performance statistics summary
129
+ */
130
+ interface PerformanceStats {
131
+ /** Current frames per second */
132
+ fps: number;
133
+ /** Last recorded frame timing */
134
+ lastFrame?: FrameTiming;
135
+ /** Average frame timing over sample window */
136
+ avgFrame?: FrameTiming;
137
+ /** Number of samples in rolling average */
138
+ sampleCount: number;
139
+ /** Maximum sample size */
140
+ maxSamples: number;
141
+ }
142
+ /**
143
+ * Monitors and tracks runtime performance metrics
144
+ *
145
+ * Provides FPS tracking, frame timing measurements, and rolling averages
146
+ * for performance analysis and optimization.
147
+ */
148
+ declare class PerformanceMonitor {
149
+ private frameCount;
150
+ private fps;
151
+ private fpsUpdateTime;
152
+ private lastCoreTime;
153
+ private lastRenderTime;
154
+ private lastTotalTime;
155
+ private coreTimeSamples;
156
+ private renderTimeSamples;
157
+ private totalTimeSamples;
158
+ private readonly maxSamples;
159
+ /**
160
+ * Creates a new PerformanceMonitor
161
+ *
162
+ * @param maxSamples - Maximum number of samples to keep for rolling averages (default: 60)
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * const monitor = new PerformanceMonitor(60); // Track last 60 frames
167
+ * ```
168
+ */
169
+ constructor(maxSamples?: number);
170
+ /**
171
+ * Start FPS tracking
172
+ *
173
+ * Call this once when starting performance monitoring.
174
+ * Initializes the FPS calculation baseline.
175
+ *
176
+ * @param timestamp - Initial timestamp from performance.now()
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * monitor.startTracking(performance.now());
181
+ * ```
182
+ */
183
+ startTracking(timestamp: number): void;
184
+ /**
185
+ * Update frame counter and calculate FPS
186
+ *
187
+ * Call this every frame in your main loop. Automatically calculates
188
+ * FPS every second based on frame count.
189
+ *
190
+ * @param timestamp - Current timestamp from performance.now()
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * function mainLoop(timestamp: number) {
195
+ * monitor.updateFPS(timestamp);
196
+ * // ... rest of frame logic
197
+ * }
198
+ * ```
199
+ */
200
+ updateFPS(timestamp: number): void;
201
+ /**
202
+ * Record frame timing for the current frame
203
+ *
204
+ * Call this after each frame completes to record timing data.
205
+ * Maintains a rolling window of samples for average calculation.
206
+ *
207
+ * @param coreTime - Time spent in core logic (ms)
208
+ * @param renderTime - Time spent in rendering (ms)
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const coreStart = performance.now();
213
+ * // ... core logic
214
+ * const coreTime = performance.now() - coreStart;
215
+ *
216
+ * const renderStart = performance.now();
217
+ * // ... rendering
218
+ * const renderTime = performance.now() - renderStart;
219
+ *
220
+ * monitor.recordFrameTiming(coreTime, renderTime);
221
+ * ```
222
+ */
223
+ recordFrameTiming(coreTime: number, renderTime: number): void;
224
+ /**
225
+ * Get current FPS
226
+ *
227
+ * @returns Current frames per second
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * console.log(`FPS: ${monitor.getFPS()}`);
232
+ * ```
233
+ */
234
+ getFPS(): number;
235
+ /**
236
+ * Get last recorded frame timing
237
+ *
238
+ * Returns undefined if no frames have been recorded yet.
239
+ *
240
+ * @returns Last frame timing or undefined
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * const last = monitor.getLastFrameTiming();
245
+ * if (last) {
246
+ * console.log(`Last frame: ${last.totalTime.toFixed(2)}ms`);
247
+ * }
248
+ * ```
249
+ */
250
+ getLastFrameTiming(): FrameTiming | undefined;
251
+ /**
252
+ * Get average frame timing over sample window
253
+ *
254
+ * Returns undefined if no samples have been recorded yet.
255
+ *
256
+ * @returns Average frame timing or undefined
257
+ *
258
+ * @example
259
+ * ```typescript
260
+ * const avg = monitor.getAverageFrameTiming();
261
+ * if (avg) {
262
+ * console.log(`Avg frame: ${avg.totalTime.toFixed(2)}ms`);
263
+ * console.log(`Avg core: ${avg.coreTime.toFixed(2)}ms`);
264
+ * console.log(`Avg render: ${avg.renderTime.toFixed(2)}ms`);
265
+ * }
266
+ * ```
267
+ */
268
+ getAverageFrameTiming(): FrameTiming | undefined;
269
+ /**
270
+ * Get comprehensive performance statistics
271
+ *
272
+ * Returns all available performance metrics including FPS,
273
+ * last frame timing, average timing, and sample counts.
274
+ *
275
+ * @returns Performance statistics
276
+ *
277
+ * @example
278
+ * ```typescript
279
+ * const stats = monitor.getStats();
280
+ * console.log(`FPS: ${stats.fps}`);
281
+ * console.log(`Samples: ${stats.sampleCount}/${stats.maxSamples}`);
282
+ * if (stats.avgFrame) {
283
+ * console.log(`Avg Frame Time: ${stats.avgFrame.totalTime.toFixed(2)}ms`);
284
+ * }
285
+ * ```
286
+ */
287
+ getStats(): PerformanceStats;
288
+ /**
289
+ * Reset all performance data
290
+ *
291
+ * Clears all tracked metrics and samples. Useful for restarting
292
+ * performance monitoring without creating a new instance.
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * monitor.reset();
297
+ * monitor.startTracking(performance.now());
298
+ * ```
299
+ */
300
+ reset(): void;
301
+ /**
302
+ * Calculate average of an array of numbers
303
+ *
304
+ * @param arr - Array of numbers
305
+ * @returns Average value
306
+ * @private
307
+ */
308
+ private average;
309
+ }
310
+
311
+ /**
312
+ * Renderer Manager
313
+ *
314
+ * Manages renderer lifecycle including initialization, font loading,
315
+ * readiness checks, and rendering operations.
316
+ *
317
+ * This class is responsible for:
318
+ * - Renderer readiness waiting (async initialization)
319
+ * - Default font loading and registration
320
+ * - Rendering operation delegation
321
+ * - Renderer cleanup and destruction
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * const manager = new RendererManager(renderer, {
326
+ * debug: true,
327
+ * useImageDataRendering: false
328
+ * });
329
+ *
330
+ * // Initialize
331
+ * await manager.initialize(core);
332
+ *
333
+ * // Render
334
+ * manager.render(userId);
335
+ *
336
+ * // Cleanup
337
+ * manager.destroy();
338
+ * ```
339
+ */
340
+
341
+ /**
342
+ * Configuration options for RendererManager
343
+ */
344
+ interface RendererManagerOptions {
345
+ /** Enable debug logging */
346
+ debug?: boolean;
347
+ /** Enable ImageData rendering optimization (Terminal2D only) */
348
+ useImageDataRendering?: boolean;
349
+ }
350
+ /**
351
+ * Browser-specific renderer interface
352
+ * Refines IRenderer's getCanvas() return type from unknown to HTMLCanvasElement
353
+ * and adds optional Terminal2D-specific methods
354
+ */
355
+ type ConcreteRenderer = IRenderer & {
356
+ /**
357
+ * Get canvas element (browser-specific)
358
+ * Refines the return type from unknown to HTMLCanvasElement | null
359
+ */
360
+ getCanvas(): HTMLCanvasElement | null;
361
+ /**
362
+ * Enable ImageData rendering optimization (Terminal2D specific, optional)
363
+ */
364
+ setImageDataRendering?: (enabled: boolean) => void;
365
+ };
366
+ /**
367
+ * Manages renderer lifecycle and operations
368
+ *
369
+ * Provides abstraction layer for renderer initialization, font loading,
370
+ * and rendering operations. Handles differences between renderer types
371
+ * (e.g., async WebGL initialization vs instant Canvas2D).
372
+ */
373
+ declare class RendererManager {
374
+ private renderer;
375
+ private options;
376
+ /**
377
+ * Creates a new RendererManager
378
+ *
379
+ * @param renderer - Renderer instance to manage
380
+ * @param options - Configuration options
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * const renderer = new TerminalGL(container, width, height);
385
+ * const manager = new RendererManager(renderer, { debug: true });
386
+ * ```
387
+ */
388
+ constructor(renderer: ConcreteRenderer, options?: RendererManagerOptions);
389
+ /**
390
+ * Initialize renderer (wait for ready + load default font)
391
+ *
392
+ * Call this before using the renderer. Handles async initialization
393
+ * for WebGL renderers and loads the default ASCII 8x8 font atlas.
394
+ *
395
+ * @param core - Core instance for font registry access
396
+ * @throws Error if renderer fails to be ready after timeout
397
+ *
398
+ * @example
399
+ * ```typescript
400
+ * await manager.initialize(core);
401
+ * // Renderer is now ready and font is loaded
402
+ * ```
403
+ */
404
+ initialize(_core: Core): Promise<void>;
405
+ /**
406
+ * Wait for renderer to be ready
407
+ *
408
+ * Some renderers (WebGL) require async initialization. This method
409
+ * polls isReady() until the renderer is ready or timeout occurs.
410
+ *
411
+ * @param maxAttempts - Maximum polling attempts (default: 100)
412
+ * @param intervalMs - Polling interval in milliseconds (default: 50ms)
413
+ * @throws Error if renderer fails to be ready after maxAttempts
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * await manager.waitForReady();
418
+ * console.log('Renderer is ready!');
419
+ * ```
420
+ */
421
+ waitForReady(maxAttempts?: number, intervalMs?: number): Promise<void>;
422
+ /**
423
+ * @deprecated Default 8x8 font is no longer used. Use ImageFont instead.
424
+ * This method is kept for backwards compatibility but does nothing.
425
+ */
426
+ loadDefaultFont(_core: Core): Promise<void>;
427
+ /**
428
+ * Render display data for a specific user
429
+ *
430
+ * Retrieves render state from core and renders the first display.
431
+ * Checks renderer readiness before rendering.
432
+ *
433
+ * @param core - Core instance for render state access
434
+ * @param userId - User ID to render for
435
+ * @returns true if rendering succeeded, false otherwise
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const rendered = manager.render(core, userId);
440
+ * if (!rendered) {
441
+ * console.warn('Rendering failed or skipped');
442
+ * }
443
+ * ```
444
+ */
445
+ render(core: Core, userId: string): boolean;
446
+ /**
447
+ * Get the underlying renderer instance
448
+ *
449
+ * @returns Renderer instance
450
+ *
451
+ * @example
452
+ * ```typescript
453
+ * const renderer = manager.getRenderer();
454
+ * const canvas = renderer.getCanvas();
455
+ * ```
456
+ */
457
+ getRenderer(): ConcreteRenderer;
458
+ /**
459
+ * Get renderer canvas element
460
+ *
461
+ * Convenience method to access the renderer's canvas.
462
+ *
463
+ * @returns Canvas element or null if not available
464
+ *
465
+ * @example
466
+ * ```typescript
467
+ * const canvas = manager.getCanvas();
468
+ * if (canvas) {
469
+ * canvas.addEventListener('click', handleClick);
470
+ * }
471
+ * ```
472
+ */
473
+ getCanvas(): HTMLCanvasElement | null;
474
+ /**
475
+ * Check if renderer is ready
476
+ *
477
+ * @returns true if renderer is ready to render
478
+ *
479
+ * @example
480
+ * ```typescript
481
+ * if (manager.isReady()) {
482
+ * manager.render(core, userId);
483
+ * }
484
+ * ```
485
+ */
486
+ isReady(): boolean;
487
+ /**
488
+ * Destroy renderer and cleanup resources
489
+ *
490
+ * Delegates to the underlying renderer's destroy method.
491
+ * Should be called when the renderer is no longer needed.
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * manager.destroy();
496
+ * console.log('Renderer destroyed');
497
+ * ```
498
+ */
499
+ destroy(): void;
500
+ /**
501
+ * Log debug message if debug mode is enabled
502
+ *
503
+ * @param message - Message to log
504
+ * @private
505
+ */
506
+ private log;
507
+ }
508
+
509
+ /**
510
+ * Base Client Runtime
511
+ *
512
+ * Abstract base class for all client runtime variants.
513
+ * Provides core functionality: rendering, core management, and lifecycle.
514
+ *
515
+ * Features NOT included (handled by subclasses):
516
+ * - Input handling
517
+ * - Audio
518
+ * - Network
519
+ * - Post-processing effects
520
+ * - Autoplay overlay
521
+ */
522
+
523
+ /**
524
+ * Abstract base class for all client runtimes
525
+ */
526
+ declare abstract class BaseClientRuntime {
527
+ /** Core instance managing application state */
528
+ protected core: Core;
529
+ /** Renderer manager wrapping the actual renderer */
530
+ protected rendererManager: RendererManager;
531
+ /** Current renderer type */
532
+ protected rendererType: RendererType;
533
+ /** Normalized options with defaults applied */
534
+ protected options: Required<BaseRuntimeOptions>;
535
+ /** Is the runtime currently running */
536
+ protected running: boolean;
537
+ /** Timestamp when runtime started */
538
+ protected startTime: number;
539
+ /** Last frame timestamp */
540
+ protected lastTimestamp: number;
541
+ /** Current user ID */
542
+ protected userId: string;
543
+ /** Visibility change handler for tab switching */
544
+ protected visibilityChangeHandler?: () => void;
545
+ /** Tick rate in TPS (0 = no updates) */
546
+ protected tickRate: number;
547
+ /** Accumulated time for fixed timestep */
548
+ protected accumulatedTime: number;
549
+ /** Minimum frame time (1000/maxFPS) */
550
+ protected readonly FRAME_TIME_MIN: number;
551
+ /** Last render timestamp for frame limiting */
552
+ protected lastRenderTimestamp: number;
553
+ /** requestAnimationFrame ID */
554
+ protected rafId: number;
555
+ /** Flag to prevent catch-up ticks on first frame */
556
+ protected firstTickDone: boolean;
557
+ /** Performance monitoring */
558
+ protected performanceMonitor: PerformanceMonitor;
559
+ /** Render mode: continuous or on-demand */
560
+ protected renderMode: 'continuous' | 'on-demand';
561
+ /** Flag for on-demand render requests */
562
+ protected renderRequested: boolean;
563
+ constructor(options: BaseRuntimeOptions);
564
+ /**
565
+ * Create the appropriate renderer based on options
566
+ */
567
+ protected createRenderer(): ConcreteRenderer;
568
+ /**
569
+ * Get the renderer type
570
+ */
571
+ getRendererType(): RendererType;
572
+ /**
573
+ * Check if runtime is running
574
+ */
575
+ isRunning(): boolean;
576
+ /**
577
+ * Set the tick rate (TPS)
578
+ */
579
+ setTickRate(tickRate: number): void;
580
+ /**
581
+ * Set render mode
582
+ */
583
+ setRenderMode(mode: 'continuous' | 'on-demand'): void;
584
+ /**
585
+ * Set maximum FPS
586
+ */
587
+ setMaxFPS(fps: number): void;
588
+ /**
589
+ * Get current tick rate
590
+ */
591
+ getTickRate(): number;
592
+ /**
593
+ * Request a render in on-demand mode
594
+ */
595
+ requestRender(): void;
596
+ /**
597
+ * Start the runtime
598
+ */
599
+ start(): Promise<void>;
600
+ /**
601
+ * Hook called before start() initializes renderer
602
+ * Override in subclasses to handle autoplay, audio initialization, etc.
603
+ */
604
+ protected onBeforeStart(): Promise<void>;
605
+ /**
606
+ * Hook called after application.init() but before createLocalUser()
607
+ * Override in subclasses to configure input targets, etc.
608
+ */
609
+ protected onAfterInit(): Promise<void>;
610
+ /**
611
+ * Stop the runtime
612
+ */
613
+ stop(): Promise<void>;
614
+ /**
615
+ * Hook called during stop() for subclass cleanup
616
+ */
617
+ protected onStop(): Promise<void>;
618
+ /**
619
+ * Get runtime statistics
620
+ */
621
+ getStats(): BaseRuntimeStats;
622
+ /**
623
+ * Destroy the runtime and clean up all resources
624
+ */
625
+ destroy(): Promise<void>;
626
+ /**
627
+ * Hook called during destroy() for subclass cleanup
628
+ */
629
+ protected onDestroy(): Promise<void>;
630
+ /**
631
+ * Create a local user
632
+ */
633
+ protected createLocalUser(): Promise<void>;
634
+ /**
635
+ * Hook called when user is created, before initUser()
636
+ * Override to inject audio processor, vibration, etc.
637
+ */
638
+ protected onUserCreated(_user: User): void;
639
+ /**
640
+ * Process initial orders after initUser()
641
+ * Override to handle macro orders, post-process, etc.
642
+ */
643
+ protected processInitialOrders(user: User): void;
644
+ /**
645
+ * Main game loop
646
+ */
647
+ protected mainLoop(timestamp: number): void;
648
+ /**
649
+ * Collect and apply input from devices
650
+ * Override in subclasses with input support
651
+ */
652
+ protected collectAndApplyInput(_user: User): void;
653
+ /**
654
+ * Process orders generated during tick (sounds, vibration, etc.)
655
+ * Override in subclasses with audio/postprocess support
656
+ */
657
+ protected processTickOrders(user: User): void;
658
+ /**
659
+ * Render the current state
660
+ */
661
+ protected render(): void;
662
+ /**
663
+ * Handle palette change from Core
664
+ */
665
+ protected onCorePaletteChanged(palette: Map<number, {
666
+ r: number;
667
+ g: number;
668
+ b: number;
669
+ a: number;
670
+ e: number;
671
+ }>): void;
672
+ /**
673
+ * Handle bitmap font change from Core
674
+ */
675
+ protected onCoreBitmapFontChanged(fontId: number): void;
676
+ /**
677
+ * Handle image font change from Core
678
+ */
679
+ protected onCoreImageFontChanged(fontId: number): void;
680
+ /**
681
+ * Hook called after font change for post-process sync
682
+ * Override in subclasses with post-process support
683
+ */
684
+ protected onFontChanged(_renderer: ConcreteRenderer): void;
685
+ /**
686
+ * Debug logging
687
+ */
688
+ protected log(message: string): void;
689
+ /**
690
+ * Get the Core instance
691
+ */
692
+ getCore(): Core;
693
+ /**
694
+ * Get the renderer manager
695
+ */
696
+ getRendererManager(): RendererManager;
697
+ /**
698
+ * Get current user ID
699
+ */
700
+ getUserId(): string;
701
+ }
7
702
 
8
703
  /**
9
704
  * Runtime Client Types and Options
@@ -13,23 +708,6 @@ export { IApplication } from '@utsp/types';
13
708
  * Client runtime mode
14
709
  */
15
710
  type ClientRuntimeMode = 'local' | 'connected';
16
- /**
17
- * Renderer type to use
18
- */
19
- declare enum RendererType {
20
- /** WebGL2 renderer - Optimized WebGL 1.0 with GPU palette, instanced rendering (default, recommended) */
21
- TerminalGL = "webgl",
22
- /** Canvas 2D renderer - CPU fallback with ImageData optimization for benchmarks */
23
- Terminal2D = "terminal2d"
24
- }
25
- /**
26
- * Render mode for ClientRuntime
27
- */
28
- type RenderMode =
29
- /** Continuous rendering at max FPS (default) - requestAnimationFrame loop */
30
- 'continuous'
31
- /** On-demand rendering - only renders when explicitly requested via requestRender() */
32
- | 'on-demand';
33
711
  /**
34
712
  * Base client runtime options
35
713
  */
@@ -57,23 +735,8 @@ interface BaseClientRuntimeOptions {
57
735
  };
58
736
  /** Enable ImageData rendering for Canvas 2D (default: true). Provides pixel-perfect rendering with no gaps between cells. 10-20× faster than fillRect. Only for Terminal2D renderer with bitmap fonts. */
59
737
  useImageDataRendering?: boolean;
60
- /** Render mode: 'continuous' (default) renders at max FPS, 'on-demand' only renders when explicitly requested. When tickRate is 0, automatically switches to 'on-demand'. */
738
+ /** Render mode: 'continuous' (default) renders at max FPS, 'on-demand' only renders when explicitly requested. */
61
739
  renderMode?: RenderMode;
62
- /** Tick rate in ticks per second (default: 30). Set to 0 to disable update loop (only init/initUser, no update/updateUser). When set to 0, automatically enables 'on-demand' render mode. */
63
- tickRate?: number;
64
- /**
65
- * Scaling mode for pixel-perfect rendering (default: ScalingMode.None)
66
- *
67
- * Controls how the canvas is scaled to fit the container:
68
- * - ScalingMode.None: Fills available space, may have sub-pixel artifacts (default)
69
- * - ScalingMode.Eighth: Snaps to 0.125 increments (1.0, 1.125, 1.25...)
70
- * - ScalingMode.Quarter: Snaps to 0.25 increments (1.0, 1.25, 1.5...)
71
- * - ScalingMode.Half: Snaps to 0.5 increments (1.0, 1.5, 2.0...)
72
- * - ScalingMode.Integer: Integer scaling only (1x, 2x, 3x...), crispest pixels
73
- *
74
- * Use ScalingMode.Integer for pixel-art style games that need crisp, artifact-free rendering.
75
- */
76
- scalingMode?: ScalingMode;
77
740
  /** Capture input events to prevent default browser behavior (Tab, arrows, etc.). Default: false. When true, preventDefault() and stopPropagation() are called on keyboard and mouse events to keep focus in the terminal. All F keys (F1-F12) and Ctrl/Cmd shortcuts are automatically excluded. */
78
741
  captureInput?: boolean;
79
742
  /** Enable input handling (keyboard, mouse, touch, gamepad). Default: true. When false, no input listeners are created - useful for display-only clients in documentation pages where you want to scroll freely without the client intercepting events. */
@@ -140,17 +803,6 @@ interface ConnectedModeOptions extends BaseClientRuntimeOptions {
140
803
  * Union type of client runtime options
141
804
  */
142
805
  type ClientRuntimeOptions = LocalModeOptions | ConnectedModeOptions;
143
- /**
144
- * Performance timing for a single frame
145
- */
146
- interface FrameTiming {
147
- /** Time spent in Core tick (ms) */
148
- coreTime: number;
149
- /** Time spent in Renderer (ms) */
150
- renderTime: number;
151
- /** Total frame time (ms) */
152
- totalTime: number;
153
- }
154
806
  /**
155
807
  * Client runtime statistics
156
808
  */
@@ -170,39 +822,29 @@ interface ClientRuntimeStats {
170
822
  /** Network latency in ms (connected mode only) */
171
823
  latency?: number;
172
824
  /** Last frame timing breakdown */
173
- lastFrameTiming?: FrameTiming;
825
+ lastFrameTiming?: FrameTiming$1;
174
826
  /** Average frame timing over last 60 frames */
175
- avgFrameTiming?: FrameTiming;
827
+ avgFrameTiming?: FrameTiming$1;
176
828
  }
177
829
 
178
830
  /**
179
- * Client Runtime
831
+ * Client Runtime (Standard)
180
832
  *
181
- * Unified client runtime supporting both local and connected modes.
833
+ * Full-featured client runtime supporting both local and connected modes.
834
+ * Extends BaseClientRuntime with:
835
+ * - Input handling (keyboard, mouse, touch, gamepad)
836
+ * - Audio support
837
+ * - Network synchronization
838
+ * - Post-processing effects
839
+ * - Autoplay overlay
840
+ * - Bridge messaging
182
841
  */
183
842
 
184
- declare class ClientRuntime {
185
- private core;
186
- private rendererManager;
187
- private rendererType;
843
+ declare class ClientRuntime extends BaseClientRuntime {
188
844
  private input;
189
845
  private networkSync;
190
- private options;
191
- private running;
192
- private startTime;
193
- private lastTimestamp;
194
- private userId;
195
846
  private mode;
196
- private visibilityChangeHandler?;
197
- private tickRate;
198
- private accumulatedTime;
199
- private readonly FRAME_TIME_MIN;
200
- private lastRenderTimestamp;
201
- private rafId;
202
- private firstTickDone;
203
- private performanceMonitor;
204
- private renderMode;
205
- private renderRequested;
847
+ private localOptions;
206
848
  private autoplayOverlay;
207
849
  private autoplay;
208
850
  private audioManager;
@@ -213,221 +855,239 @@ declare class ClientRuntime {
213
855
  private localBridgeWildcardHandlers;
214
856
  constructor(options: ClientRuntimeOptions);
215
857
  getMode(): ClientRuntimeMode;
216
- getRendererType(): RendererType;
217
- isRunning(): boolean;
218
- private createRenderer;
219
858
  setTickRate(tickRate: number): void;
220
- setRenderMode(mode: 'continuous' | 'on-demand'): void;
221
- setMaxFPS(fps: number): void;
222
- getTickRate(): number;
223
859
  /**
224
- * Request a render in on-demand mode.
225
- * Has no effect in continuous mode (renders automatically).
226
- *
227
- * @example
228
- * ```typescript
229
- * // On-demand mode: render only when needed
230
- * const runtime = new ClientRuntime({
231
- * mode: 'local',
232
- * renderMode: 'on-demand',
233
- * tickRate: 0, // Disable update loop
234
- * application: myApp,
235
- * container: document.getElementById('app')
236
- * });
237
- *
238
- * await runtime.start();
239
- *
240
- * // Manually request render after changing something
241
- * core.getUser('local').setCell(0, 0, '@', 1, 0);
242
- * runtime.requestRender(); // Trigger render
243
- * ```
860
+ * Override onBeforeStart to handle autoplay and audio initialization
861
+ */
862
+ protected onBeforeStart(): Promise<void>;
863
+ /**
864
+ * Override onAfterInit to configure input and post-process
865
+ */
866
+ protected onAfterInit(): Promise<void>;
867
+ /**
868
+ * Override start() for connected mode
244
869
  */
245
- requestRender(): void;
246
870
  start(): Promise<void>;
247
- stop(): Promise<void>;
871
+ /**
872
+ * Override onStop for cleanup
873
+ */
874
+ protected onStop(): Promise<void>;
248
875
  getStats(): ClientRuntimeStats;
249
- destroy(): Promise<void>;
250
- private createLocalUser;
876
+ /**
877
+ * Override onDestroy for full cleanup
878
+ */
879
+ protected onDestroy(): Promise<void>;
880
+ /**
881
+ * Override createLocalUser for full feature support
882
+ */
883
+ protected createLocalUser(): Promise<void>;
251
884
  /**
252
885
  * Load sounds from Core's SoundRegistry into AudioManager's SoundBank
253
- * Used in standalone mode to transfer sounds registered via core.loadSound()
254
- * @private
255
886
  */
256
887
  private loadSoundsFromRegistry;
257
- private mainLoop;
258
- private collectAndApplyInput;
259
- private collectAndSendInput;
260
- private render;
261
888
  /**
262
- * Handle palette change from Core
263
- * Converts Core palette Map to RGBColor array and updates renderer
889
+ * Override mainLoop for connected mode support
264
890
  */
265
- private onCorePaletteChanged;
891
+ protected mainLoop(timestamp: number): void;
266
892
  /**
267
- * Handle bitmap font change from Core
268
- * Loads the font from registry and updates renderer
893
+ * Override collectAndApplyInput for full input support
269
894
  */
270
- private onCoreBitmapFontChanged;
895
+ protected collectAndApplyInput(user: User): void;
896
+ private collectAndSendInput;
271
897
  /**
272
- * Handle image font change from Core
273
- * Loads the PNG font atlas from registry and updates renderer
898
+ * Override onFontChanged to sync PostProcessOverlay
274
899
  */
275
- private onCoreImageFontChanged;
900
+ protected onFontChanged(renderer: ConcreteRenderer): void;
276
901
  /**
277
902
  * Get the AudioManager instance
278
- * Returns null before start() is called
279
903
  */
280
904
  getAudioManager(): InstanceType<typeof AudioManager> | null;
281
905
  /**
282
- * Get the AudioContext instance (convenience method)
283
- * Returns null if AudioManager not initialized
906
+ * Get the AudioContext instance
284
907
  */
285
908
  getAudioContext(): AudioContext | null;
286
909
  /**
287
- * Apply ambient effect configuration to the TerminalGL renderer
910
+ * Get the PostProcessOverlay instance
911
+ */
912
+ getPostProcessOverlay(): PostProcessOverlay | null;
913
+ /**
914
+ * Apply ambient effect configuration
288
915
  */
289
916
  private applyAmbientEffectConfig;
290
917
  /**
291
- * Apply post-process orders received from the network
292
- * These are binary orders decoded from UpdatePacket
918
+ * Apply post-process orders
293
919
  */
294
920
  private applyPostProcessOrders;
921
+ private applyScalingMode;
922
+ private applyGridConfig;
923
+ private patternFromType;
924
+ private dispatchLocalBridgeMessages;
925
+ sendBridge(channel: string, data: unknown): void;
926
+ onBridge<T = unknown>(channel: string, handler: (data: T) => void): void;
927
+ offBridge<T = unknown>(channel: string, handler: (data: T) => void): void;
928
+ removeAllBridgeHandlers(channel?: string): void;
295
929
  /**
296
- * Apply scaling mode from order
930
+ * Override log for mode-specific prefix
297
931
  */
298
- private applyScalingMode;
932
+ protected log(message: string): void;
933
+ }
934
+
935
+ interface InputFeatureOptions {
936
+ debug?: boolean;
937
+ captureInput?: boolean;
938
+ mobileInputConfig?: {
939
+ preventDefault?: boolean;
940
+ passive?: boolean;
941
+ maxTouches?: number;
942
+ };
943
+ }
944
+ declare class InputFeature {
945
+ private input;
946
+ private mobileVibration;
947
+ constructor(options?: InputFeatureOptions);
299
948
  /**
300
- * Apply grid configuration from order
949
+ * Set mobile input target (usually the canvas)
301
950
  */
302
- private applyGridConfig;
951
+ setMobileTarget(element: HTMLElement): void;
303
952
  /**
304
- * Convert ScanlinesPatternType enum to string pattern
953
+ * Start listening for input
305
954
  */
306
- private patternFromType;
955
+ start(): void;
307
956
  /**
308
- * Get the PostProcessOverlay instance
309
- * Returns null before start() is called
957
+ * Stop listening for input
310
958
  */
311
- getPostProcessOverlay(): PostProcessOverlay | null;
959
+ stop(): void;
312
960
  /**
313
- * Send a bridge message to the server
314
- *
315
- * Bridge messages bypass the UTSP protocol and allow direct communication
316
- * with the server application. Use this for external UI communication
317
- * (chat, settings, custom commands, etc.).
318
- *
319
- * **Note:** Only available in 'connected' mode. In 'local' mode, this is a no-op.
320
- *
321
- * @param channel - Channel name (e.g., 'chat', 'settings', 'ready')
322
- * @param data - Any JSON-serializable data
323
- *
324
- * @example
325
- * ```typescript
326
- * // Send a chat message
327
- * runtime.sendBridge('chat', { message: 'Hello everyone!' });
328
- *
961
+ * Destroy and cleanup
962
+ */
963
+ destroy(): void;
329
964
  /**
330
- * Dispatch bridge messages from application to local handlers
331
- *
332
- * In local mode, this simulates the server→client path by calling
333
- * registered handlers directly. Same timing as connected mode.
334
- *
335
- * @param messages - Array of bridge messages from user.getBridgeMessages()
336
- * @private
965
+ * Get the input router (for network sync)
337
966
  */
338
- private dispatchLocalBridgeMessages;
967
+ getRouter(): UnifiedInputRouter;
339
968
  /**
340
- * Send a message to the server via the bridge channel
341
- *
342
- * Bridge messages are sent immediately (not queued), bypassing the UTSP
343
- * protocol for real-time external app communication.
344
- *
345
- * **Note:** In 'connected' mode, sends to server. In 'local' mode, routes
346
- * directly to application.onBridgeMessage for isomorphic behavior.
347
- *
348
- * @param channel - Channel name (e.g., 'chat', 'ready', 'settings')
349
- * @param data - Data to send (will be JSON serialized)
350
- *
351
- * @example
352
- * ```typescript
353
- * // Send a chat message
354
- * runtime.sendBridge('chat', { message: 'Hello everyone!' });
355
- *
356
- * // Mark player as ready
357
- * runtime.sendBridge('ready', { ready: true });
358
- *
359
- * // Send custom settings
360
- * runtime.sendBridge('settings', { volume: 0.8, theme: 'dark' });
361
- * ```
969
+ * Get gamepad for vibration processor injection
362
970
  */
363
- sendBridge(channel: string, data: unknown): void;
971
+ getGamepad(): _utsp_input.GamepadInputs | null;
364
972
  /**
365
- * Register a handler for incoming bridge messages from the application
366
- *
367
- * Bridge messages from the application (via user.sendBridge) are synchronized
368
- * with tick updates, arriving at the same time as render updates for consistency.
369
- *
370
- * **Isomorphic:** Works in both 'connected' and 'local' modes.
371
- * - Connected: Messages come from server via network
372
- * - Local: Messages are dispatched directly from application
373
- *
374
- * @param channel - Channel name to listen for (use '*' for all channels)
375
- * @param handler - Callback function receiving the parsed data
376
- *
377
- * @example
378
- * ```typescript
379
- * // Listen for score updates
380
- * runtime.onBridge('score', (data) => {
381
- * updateScoreboard(data.value, data.combo);
382
- * });
383
- *
384
- * // Listen for notifications
385
- * runtime.onBridge('notification', (data) => {
386
- * showToast(data.title, data.message);
387
- * });
388
- *
389
- * // Wildcard handler for all channels
390
- * runtime.onBridge('*', (channel, data) => {
391
- * console.log(`Received ${channel}:`, data);
392
- * });
393
- * ```
973
+ * Get mobile vibration processor
394
974
  */
395
- onBridge<T = unknown>(channel: string, handler: (data: T) => void): void;
975
+ getMobileVibration(): MobileVibration;
396
976
  /**
397
- * Remove a specific bridge handler
398
- *
399
- * @param channel - Channel name
400
- * @param handler - The exact handler function to remove
401
- *
402
- * @example
403
- * ```typescript
404
- * const handler = (data) => console.log(data);
405
- * runtime.onBridge('score', handler);
406
- *
407
- * // Later, remove the handler
408
- * runtime.offBridge('score', handler);
409
- * ```
977
+ * Collect and apply input to user
410
978
  */
411
- offBridge<T = unknown>(channel: string, handler: (data: T) => void): void;
979
+ collectAndApply(user: User, canvas: HTMLCanvasElement | null, displayWidth: number, displayHeight: number, rendererOffsets: {
980
+ offsetX: number;
981
+ offsetY: number;
982
+ }): void;
412
983
  /**
413
- * Remove all bridge handlers for a channel (or all channels if not specified)
414
- *
415
- * Useful for cleanup in React useEffect or component unmount.
416
- *
417
- * @param channel - Optional channel name. If omitted, removes all handlers.
418
- *
419
- * @example
420
- * ```typescript
421
- * // Remove all handlers for 'score' channel
422
- * runtime.removeAllBridgeHandlers('score');
423
- *
424
- * // Remove ALL bridge handlers
425
- * runtime.removeAllBridgeHandlers();
426
- * ```
984
+ * Process vibration commands from user
427
985
  */
428
- removeAllBridgeHandlers(channel?: string): void;
986
+ processVibrationCommands(user: User): void;
987
+ }
988
+
989
+ /**
990
+ * Audio Feature
991
+ *
992
+ * Handles sound playback and spatial audio.
993
+ * Can be omitted for silent clients (spectators, embeds without sound).
994
+ */
995
+
996
+ interface AudioFeatureOptions {
997
+ debug?: boolean;
998
+ }
999
+ declare class AudioFeature {
1000
+ private audioManager;
1001
+ private debug;
1002
+ constructor(options?: AudioFeatureOptions);
1003
+ /**
1004
+ * Initialize audio context (should be called after user interaction)
1005
+ */
1006
+ initialize(): void;
1007
+ /**
1008
+ * Play the start sound (used with autoplay overlay)
1009
+ */
1010
+ playStartSound(): void;
1011
+ /**
1012
+ * Stop all sounds
1013
+ */
1014
+ stopAll(): void;
1015
+ /**
1016
+ * Destroy and cleanup
1017
+ */
1018
+ destroy(): void;
1019
+ /**
1020
+ * Get the AudioManager instance
1021
+ */
1022
+ getManager(): AudioManager;
1023
+ /**
1024
+ * Get the AudioContext
1025
+ */
1026
+ getContext(): AudioContext | null;
1027
+ /**
1028
+ * Inject audio processor into user
1029
+ */
1030
+ injectIntoUser(user: User): void;
1031
+ /**
1032
+ * Process audio commands from user
1033
+ */
1034
+ processAudioCommands(user: User): void;
1035
+ /**
1036
+ * Load sounds from Core's SoundRegistry into AudioManager's SoundBank
1037
+ */
1038
+ loadSoundsFromRegistry(core: Core): Promise<void>;
1039
+ private log;
1040
+ }
1041
+
1042
+ /**
1043
+ * PostProcess Feature
1044
+ *
1045
+ * Handles scanlines, ambient effects, grid overlay, and scaling modes.
1046
+ * Can be omitted for minimal clients without visual effects.
1047
+ */
1048
+
1049
+ interface PostProcessFeatureOptions {
1050
+ debug?: boolean;
1051
+ }
1052
+ declare class PostProcessFeature {
1053
+ private overlay;
1054
+ private orderCollector;
1055
+ private debug;
1056
+ constructor(container: HTMLElement, options?: PostProcessFeatureOptions);
1057
+ /**
1058
+ * Destroy and cleanup
1059
+ */
1060
+ destroy(): void;
1061
+ /**
1062
+ * Get the PostProcessOverlay instance
1063
+ */
1064
+ getOverlay(): PostProcessOverlay;
1065
+ /**
1066
+ * Sync overlay with renderer dimensions after font change
1067
+ */
1068
+ syncWithRenderer(renderer: {
1069
+ getCellWidth(): number;
1070
+ getCellHeight(): number;
1071
+ getCurrentScale(): number;
1072
+ getGridSize(): {
1073
+ cols: number;
1074
+ rows: number;
1075
+ };
1076
+ }): void;
1077
+ /**
1078
+ * Process post-process commands from user (converts to orders and applies)
1079
+ */
1080
+ processCommands(user: User, renderer: any): void;
1081
+ /**
1082
+ * Apply post-process orders (used by both local and network modes)
1083
+ */
1084
+ applyOrders(orders: AnyPostProcessOrder[], renderer: any): void;
1085
+ private applyAmbientEffect;
1086
+ private applyScalingMode;
1087
+ private applyGridConfig;
1088
+ private patternFromType;
429
1089
  private log;
430
1090
  }
431
1091
 
432
- export { ClientRuntime, RendererType };
433
- export type { BaseClientRuntimeOptions, ClientRuntimeMode, ClientRuntimeOptions, ClientRuntimeStats, ConnectedModeOptions, FrameTiming, LocalModeOptions, RenderMode };
1092
+ export { AudioFeature, BaseClientRuntime, ClientRuntime, InputFeature, PostProcessFeature, RendererType };
1093
+ export type { AudioFeatureOptions, BaseClientRuntimeOptions, BaseRuntimeOptions, BaseRuntimeStats, ClientRuntimeMode, ClientRuntimeOptions, ClientRuntimeStats, ConnectedModeOptions, FrameTiming$1 as FrameTiming, InputFeatureOptions, LocalModeOptions, PostProcessFeatureOptions, RenderMode };