@ouidesigner/toubani 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Toubani (tourterelle)
2
+
3
+ A small TypeScript 2D game helper built on top of the [ouider](https://www.npmjs.com/package/ouider) bridge. It wraps ODOM canvas APIs with a lightweight game loop, asset pipeline, sprite/scene graph utilities, tile rendering, simple physics/collision helpers, camera controls, and Web Audio playback.
4
+
5
+ ## Features
6
+ - Simple game loop with lifecycle hooks (`preload`, `setup`, `update`, `draw`)
7
+ - Asset manager for images, sprite sheets, and decoded audio buffers
8
+ - Sprite sheets and animations, plus a scene graph for transforms/parallax
9
+ - Tile map rendering with camera-aware culling
10
+ - Basic input handling (keyboard and mouse pressed/held state)
11
+ - Minimal physics for AABB bodies on a tilemap and tag-based collision rules
12
+ - Camera helper for pan/zoom/anchor transforms
13
+ - Web Audio playback via ouider (no direct DOM audio)
14
+
15
+ ## Installation
16
+ ```bash
17
+ npm install
18
+ npm run build # bundles to dist/ via tsup
19
+ ```
20
+
21
+ Toubani expects to run inside an ouider environment. Your app should already initialize ouider (e.g. `OUID.config()` in your bootstrap).
22
+
23
+ ## Quick start
24
+ Create a canvas (provided by ouider) and hand it to `tulon`:
25
+ ```ts
26
+ import { tulon, GameContext } from "toubani";
27
+ import { ODOM } from "ouider";
28
+
29
+ const canvas = new ODOM.CanvasElement(/* ouider element */);
30
+
31
+ const game = tulon({
32
+ canvas,
33
+ preload(assets) {
34
+ assets.image("player", "/player.png");
35
+ assets.sound("step", "/step.mp3");
36
+ },
37
+ setup({ world, assets }) {
38
+ // stash reusable objects on world for later frames
39
+ world.playerImage = assets.getImage("player");
40
+ },
41
+ update({ dt, input, audio }) {
42
+ if (input.isPressed("Space")) {
43
+ audio.play("step", { volume: 0.6 });
44
+ }
45
+ },
46
+ async draw({ ctx2d, canvas }) {
47
+ const w = await canvas.width();
48
+ const h = await canvas.height();
49
+ ctx2d.clearRect(0, 0, w, h);
50
+ ctx2d.fillStyle = "#20262e";
51
+ ctx2d.fillRect(0, 0, w, h);
52
+ // draw stuff...
53
+ },
54
+ });
55
+
56
+ await game.init();
57
+ game.start();
58
+ ```
59
+
60
+ ## Library modules
61
+ - **Game/core**: `Game`, `tulon`, and the `GameConfig/GameContext` contracts in `src/game.ts` and `src/core.ts`.
62
+ - **Assets/audio**: `AssetManager` loads images/sounds and builds sprite sheets; `AudioManager` decodes and plays sounds through Web Audio via ouider.
63
+ - **Sprites/scene**: `SpriteSheet`, `SpriteAnimation`, `Sprite`, `SpriteLayer`, and a small scene graph (`Scene`, `SceneNode`, `SpriteNode`) for nested transforms.
64
+ - **Tiles/camera**: `TileRenderer` and helpers for tile maps/tilesets, plus `Camera2D` for pan/zoom/rotation with configurable focus anchors.
65
+ - **Physics/collision**: `PhysicsBody` for AABB tile collisions with gravity/friction and `CollisionSystem` for tag-based collider handlers.
66
+ - **Input**: `InputManager` tracks key and mouse pressed/held state per frame.
67
+
68
+ ## Examples
69
+ See `examples/simple` for runnable demos:
70
+ - `src/Game.ts`: side-scroller style scene showing tile rendering, physics, camera follow, and sprite animations.
71
+ - `src/checkers/checkers.ts`: simple checkers board rendered on the canvas using camera transforms and click handling.
72
+
73
+ Each example has its own `package.json`/webpack config; install and run from `examples/simple` to try them in an ouider host.
74
+
75
+ ## Developing
76
+ - Build: `npm run build` (tsup → `dist/`)
77
+ - TypeScript config: `tsconfig.json` targets CommonJS output for the bundled library.
78
+
79
+ ## Notes
80
+ - Audio uses Web Audio via ouider’s `AudioContext` bridge; sounds are fetched through `OUID.fetch`, decoded once, and played from buffers (no DOM audio elements).
81
+ - Assets cache image sizes on load to support sprite and tile calculations.
@@ -0,0 +1,419 @@
1
+ import { ODOM } from 'ouider';
2
+
3
+ interface SpriteSheetConfig {
4
+ image: string;
5
+ frameWidth: number;
6
+ frameHeight: number;
7
+ frameCount: number;
8
+ from?: number;
9
+ fps?: number;
10
+ loop?: boolean;
11
+ }
12
+ declare class SpriteSheet {
13
+ image: ODOM.Image;
14
+ frameWidth: number;
15
+ frameHeight: number;
16
+ frameCount: number;
17
+ from: number;
18
+ columns: number;
19
+ constructor(image: ODOM.Image, frameWidth: number, frameHeight: number, frameCount: number, from?: number);
20
+ }
21
+ declare class SpriteAnimation {
22
+ readonly sheet: SpriteSheet;
23
+ private fps;
24
+ private loop;
25
+ private time;
26
+ constructor(sheet: SpriteSheet, fps?: number, loop?: boolean);
27
+ update(dt: number): void;
28
+ draw(ctx: ODOM.Canvas2DContext, x: number, y: number, sxScale?: number, syScale?: number): void;
29
+ reset(): void;
30
+ }
31
+ interface SpriteOptions {
32
+ animation: SpriteAnimation;
33
+ width?: number;
34
+ height?: number;
35
+ visible?: boolean;
36
+ tag?: string;
37
+ data?: Record<string, any>;
38
+ }
39
+ /**
40
+ * Pure visual sprite: no position/velocity.
41
+ * Draws at (0,0) in the current transform.
42
+ */
43
+ declare class Sprite {
44
+ animation: SpriteAnimation;
45
+ width: number;
46
+ height: number;
47
+ visible: boolean;
48
+ tag?: string;
49
+ data: Record<string, any>;
50
+ constructor(opts: SpriteOptions);
51
+ update(dt: number): void;
52
+ draw(ctx: ODOM.Canvas2DContext, scaleX?: number, scaleY?: number): void;
53
+ }
54
+ declare class SpriteLayer {
55
+ sprites: Sprite[];
56
+ add(sprite: Sprite): Sprite;
57
+ remove(sprite: Sprite): void;
58
+ clear(): void;
59
+ update(dt: number): void;
60
+ draw(ctx: ODOM.Canvas2DContext): void;
61
+ /**
62
+ * Find all sprites with a given tag.
63
+ * Example: layer.withTag("enemy")
64
+ */
65
+ withTag(tag: string): Sprite[];
66
+ }
67
+
68
+ declare class AssetManager implements AssetRegistry {
69
+ private imageDefs;
70
+ private soundDefs;
71
+ private sheetDefs;
72
+ private images;
73
+ private sounds;
74
+ private sheets;
75
+ image(key: string, url: string): void;
76
+ sound(key: string, url: string): void;
77
+ spriteSheet(key: string, config: SpriteSheetConfig): void;
78
+ loadAll(): Promise<void>;
79
+ private loadImage;
80
+ private loadSound;
81
+ getImage(key: string): ODOM.Image | undefined;
82
+ getSpriteSheet(key: string): SpriteSheet | undefined;
83
+ createAnimation(sheetKey: string, fps?: number, loop?: boolean): SpriteAnimation;
84
+ getSound(key: string): ODOM.OObject | undefined;
85
+ clean(): void;
86
+ }
87
+
88
+ type VolumeOptions = {
89
+ loop?: boolean;
90
+ volume?: number;
91
+ };
92
+ declare class AudioManager {
93
+ private assets;
94
+ private audioContext;
95
+ private audioDestination;
96
+ private decodedBuffers;
97
+ constructor(assets: AssetManager);
98
+ private getContext;
99
+ private getDestination;
100
+ private getBuffer;
101
+ clean(): void;
102
+ play(key: string, options?: VolumeOptions): Promise<void>;
103
+ }
104
+
105
+ declare class InputManager {
106
+ private keysDown;
107
+ private keysPressed;
108
+ private buttonsPressed;
109
+ private buttonsDown;
110
+ mouseX: number;
111
+ mouseY: number;
112
+ offsetX: number;
113
+ offsetY: number;
114
+ private ids;
115
+ private disposed;
116
+ constructor(targetElement: ODOM.CanvasElement);
117
+ update(): void;
118
+ isDown(code: string): boolean;
119
+ isPressed(code: string): boolean;
120
+ clean(): void;
121
+ private track;
122
+ private updateMousePosition;
123
+ }
124
+
125
+ interface AssetRegistry {
126
+ image(key: string, url: string): void;
127
+ sound(key: string, url: string): void;
128
+ spriteSheet(key: string, config: SpriteSheetConfig): void;
129
+ }
130
+ interface GameContext {
131
+ dt: number;
132
+ canvas: ODOM.CanvasElement;
133
+ ctx2d: ODOM.Canvas2DContext;
134
+ world: Record<string, any>;
135
+ assets: AssetManager;
136
+ audio: AudioManager;
137
+ input: InputManager;
138
+ }
139
+ interface GameConfig {
140
+ canvas: ODOM.CanvasElement;
141
+ background?: string;
142
+ preload?(assets: AssetRegistry): void;
143
+ setup?(ctx: GameContext): void;
144
+ update?(ctx: GameContext): void;
145
+ draw?(ctx: GameContext): Promise<void>;
146
+ }
147
+
148
+ declare class Game {
149
+ private config;
150
+ private canvas;
151
+ private ctx2d;
152
+ private assets;
153
+ private audio;
154
+ private input;
155
+ private world;
156
+ private running;
157
+ private initialized;
158
+ private lastTime;
159
+ private frameId;
160
+ private resizeListenerId;
161
+ constructor(config: GameConfig);
162
+ private buildContext;
163
+ private updateLocalSize;
164
+ private syncSize;
165
+ init(): Promise<void>;
166
+ start(): void;
167
+ stop(): void;
168
+ private scheduleFrame;
169
+ private tick;
170
+ }
171
+ declare function tulon(config: GameConfig): Game;
172
+ declare function objectSize(obj: any): {
173
+ width: number;
174
+ height: number;
175
+ };
176
+
177
+ declare class Camera2D {
178
+ x: number;
179
+ y: number;
180
+ zoom: number;
181
+ rotation: number;
182
+ followTarget: {
183
+ x: number;
184
+ y: number;
185
+ } | null;
186
+ followLerp: number;
187
+ focusX: number;
188
+ focusY: number;
189
+ follow(target: {
190
+ x: number;
191
+ y: number;
192
+ }, lerp?: number): void;
193
+ unfollow(): void;
194
+ update(dt: number): void;
195
+ begin(ctx: ODOM.Canvas2DContext, canvasWidth: number, canvasHeight: number): void;
196
+ end(ctx: ODOM.Canvas2DContext): void;
197
+ }
198
+
199
+ interface TileMap {
200
+ tileWidth: number;
201
+ tileHeight: number;
202
+ width: number;
203
+ height: number;
204
+ getTile(tx: number, ty: number): number;
205
+ isSolid(tx: number, ty: number): boolean;
206
+ }
207
+ interface TileSet {
208
+ image: ODOM.Image;
209
+ tileWidth: number;
210
+ tileHeight: number;
211
+ margin?: number;
212
+ spacing?: number;
213
+ }
214
+ declare function worldToTileX(x: number, map: TileMap): number;
215
+ declare function worldToTileY(y: number, map: TileMap): number;
216
+ declare function getTileSourceRect(index: number, tileset: TileSet): {
217
+ sx: number;
218
+ sy: number;
219
+ sw: number;
220
+ sh: number;
221
+ };
222
+ interface TileRendererOptions {
223
+ skipEmpty?: boolean;
224
+ }
225
+ declare class TileRenderer {
226
+ private tileset;
227
+ private sourceRectCache;
228
+ constructor(tileset: TileSet);
229
+ /**
230
+ * Optimized renderer with camera culling
231
+ *
232
+ * cameraX, cameraY = camera world position (center)
233
+ * viewWidth/viewHeight = viewport size in pixels
234
+ */
235
+ draw(ctx: ODOM.Canvas2DContext, map: TileMap, camera: Camera2D, canvasWidth: number, canvasHeight: number): void;
236
+ private getSourceRect;
237
+ }
238
+
239
+ declare class SceneNode {
240
+ x: number;
241
+ y: number;
242
+ rotation: number;
243
+ scaleX: number;
244
+ scaleY: number;
245
+ visible: boolean;
246
+ parent: SceneNode | null;
247
+ children: SceneNode[];
248
+ add(child: SceneNode): SceneNode;
249
+ remove(child: SceneNode): void;
250
+ clear(): void;
251
+ update(dt: number): void;
252
+ protected drawSelf(ctx: ODOM.Canvas2DContext): void;
253
+ draw(ctx: ODOM.Canvas2DContext): void;
254
+ updateTree(dt: number): void;
255
+ getWorldPosition(): {
256
+ x: number;
257
+ y: number;
258
+ };
259
+ }
260
+ declare class SpriteNode extends SceneNode {
261
+ sprite: Sprite;
262
+ constructor(sprite: Sprite);
263
+ update(dt: number): void;
264
+ protected drawSelf(ctx: ODOM.Canvas2DContext): void;
265
+ getWorldBounds(): {
266
+ x: number;
267
+ y: number;
268
+ width: number;
269
+ height: number;
270
+ };
271
+ }
272
+ declare class SceneLayer extends SceneNode {
273
+ parallaxX: number;
274
+ parallaxY: number;
275
+ constructor(parallaxX?: number, parallaxY?: number);
276
+ }
277
+ declare class Scene {
278
+ static id: number;
279
+ id: number;
280
+ root: SceneNode;
281
+ layers: SceneLayer[];
282
+ constructor();
283
+ addLayer(parallaxX?: number, parallaxY?: number): SceneLayer;
284
+ update(dt: number): void;
285
+ draw(ctx: ODOM.Canvas2DContext): void;
286
+ }
287
+
288
+ interface PhysicsBodySettings {
289
+ gravityX?: number;
290
+ gravityY?: number;
291
+ maxFallSpeed?: number;
292
+ friction?: number;
293
+ airFriction?: number;
294
+ width: number;
295
+ height: number;
296
+ tileMap: TileMap;
297
+ }
298
+ declare class PhysicsBody {
299
+ node: SceneNode;
300
+ settings: PhysicsBodySettings;
301
+ vx: number;
302
+ vy: number;
303
+ onGround: boolean;
304
+ constructor(node: SceneNode, settings: PhysicsBodySettings);
305
+ update(dt: number): void;
306
+ private moveAndCollideX;
307
+ private moveAndCollideY;
308
+ private isSolid;
309
+ }
310
+
311
+ interface AABB {
312
+ x: number;
313
+ y: number;
314
+ width: number;
315
+ height: number;
316
+ }
317
+ interface Collider {
318
+ id: string;
319
+ tag: string;
320
+ isActive: boolean;
321
+ getBounds(): AABB;
322
+ data?: any;
323
+ }
324
+ declare function aabbOverlap(a: AABB, b: AABB): boolean;
325
+ type CollisionHandler = (a: Collider, b: Collider) => void;
326
+ declare class CollisionSystem {
327
+ private colliders;
328
+ private rules;
329
+ private rulesByPair;
330
+ add(collider: Collider): void;
331
+ remove(collider: Collider): void;
332
+ clear(): void;
333
+ /**
334
+ * Register a collision handler between two tags.
335
+ * Order does not matter: ("player","enemy") == ("enemy","player")
336
+ */
337
+ onCollision(tagA: string, tagB: string, handler: CollisionHandler): void;
338
+ /**
339
+ * Call this once per frame AFTER physics updates.
340
+ */
341
+ update(): void;
342
+ private ruleKey;
343
+ private triggerRules;
344
+ }
345
+ declare function createSpritePhysicsCollider(tag: string, node: SceneNode, body: PhysicsBody, sprite?: Sprite): Collider;
346
+
347
+ interface NetMessage<TType extends string = string, TPayload = any> {
348
+ type: TType;
349
+ payload: TPayload;
350
+ }
351
+ interface NetworkTransport {
352
+ connect(url: string): Promise<void>;
353
+ send(data: string): Promise<void>;
354
+ close(code?: number, reason?: string): Promise<void>;
355
+ isConnected(): boolean;
356
+ onMessage(cb: (data: string) => void): void;
357
+ onOpen(cb: () => void): void;
358
+ onClose(cb: (ev?: any) => void): void;
359
+ onError(cb: (err: any) => void): void;
360
+ }
361
+ declare class WebSocketTransport implements NetworkTransport {
362
+ private static readonly OPEN;
363
+ private static readonly CONNECTING;
364
+ private opened;
365
+ private socket;
366
+ private msgHandlers;
367
+ private openHandlers;
368
+ private closeHandlers;
369
+ private errorHandlers;
370
+ private ids;
371
+ connect(url: string): Promise<void>;
372
+ send(data: string): Promise<void>;
373
+ close(code?: number, reason?: string): Promise<void>;
374
+ isConnected(): boolean;
375
+ onMessage(cb: (data: string) => void): void;
376
+ onOpen(cb: () => void): void;
377
+ offOpen(cb: () => void): void;
378
+ onClose(cb: (ev?: any) => void): void;
379
+ offClose(cb: (ev?: any) => void): void;
380
+ onError(cb: (err: any) => void): void;
381
+ offError(cb: (err: any) => void): void;
382
+ private cleanupSocket;
383
+ }
384
+ type Handler<T = any> = (msg: T) => void;
385
+ declare class NetworkClient {
386
+ private transport;
387
+ private handlers;
388
+ private rpcResolvers;
389
+ private rpcCounter;
390
+ constructor(transport: NetworkTransport);
391
+ connect(url: string): Promise<void>;
392
+ isConnected(): boolean;
393
+ send<TType extends string, TPayload>(type: TType, payload: TPayload): void;
394
+ on<TPayload = any>(type: string, handler: Handler<NetMessage<string, TPayload>>): void;
395
+ off(type: string, handler: Handler): void;
396
+ call<TReq = any, TRes = any>(type: string, payload: TReq): Promise<TRes>;
397
+ private handleRawMessage;
398
+ close(): Promise<void>;
399
+ }
400
+ interface StatePatch<TState> {
401
+ full?: TState;
402
+ patch?: Partial<TState>;
403
+ }
404
+ declare class StateSyncClient<TState extends object> {
405
+ private net;
406
+ private channels?;
407
+ private state;
408
+ private onChangeHandlers;
409
+ constructor(net: NetworkClient, initialState: TState, channels?: {
410
+ update: string;
411
+ patch: string;
412
+ });
413
+ getState(): TState;
414
+ onChange(handler: (state: TState) => void): void;
415
+ private emitChange;
416
+ sendPatch(patch: Partial<TState>): void;
417
+ }
418
+
419
+ export { type AABB, AssetManager, type AssetRegistry, AudioManager, Camera2D, type Collider, type CollisionHandler, CollisionSystem, Game, type GameConfig, type GameContext, InputManager, type NetMessage, NetworkClient, type NetworkTransport, PhysicsBody, type PhysicsBodySettings, Scene, SceneLayer, SceneNode, Sprite, SpriteAnimation, SpriteLayer, SpriteNode, type SpriteOptions, SpriteSheet, type SpriteSheetConfig, type StatePatch, StateSyncClient, type TileMap, TileRenderer, type TileRendererOptions, type TileSet, WebSocketTransport, aabbOverlap, createSpritePhysicsCollider, getTileSourceRect, objectSize, tulon, worldToTileX, worldToTileY };
@@ -0,0 +1,419 @@
1
+ import { ODOM } from 'ouider';
2
+
3
+ interface SpriteSheetConfig {
4
+ image: string;
5
+ frameWidth: number;
6
+ frameHeight: number;
7
+ frameCount: number;
8
+ from?: number;
9
+ fps?: number;
10
+ loop?: boolean;
11
+ }
12
+ declare class SpriteSheet {
13
+ image: ODOM.Image;
14
+ frameWidth: number;
15
+ frameHeight: number;
16
+ frameCount: number;
17
+ from: number;
18
+ columns: number;
19
+ constructor(image: ODOM.Image, frameWidth: number, frameHeight: number, frameCount: number, from?: number);
20
+ }
21
+ declare class SpriteAnimation {
22
+ readonly sheet: SpriteSheet;
23
+ private fps;
24
+ private loop;
25
+ private time;
26
+ constructor(sheet: SpriteSheet, fps?: number, loop?: boolean);
27
+ update(dt: number): void;
28
+ draw(ctx: ODOM.Canvas2DContext, x: number, y: number, sxScale?: number, syScale?: number): void;
29
+ reset(): void;
30
+ }
31
+ interface SpriteOptions {
32
+ animation: SpriteAnimation;
33
+ width?: number;
34
+ height?: number;
35
+ visible?: boolean;
36
+ tag?: string;
37
+ data?: Record<string, any>;
38
+ }
39
+ /**
40
+ * Pure visual sprite: no position/velocity.
41
+ * Draws at (0,0) in the current transform.
42
+ */
43
+ declare class Sprite {
44
+ animation: SpriteAnimation;
45
+ width: number;
46
+ height: number;
47
+ visible: boolean;
48
+ tag?: string;
49
+ data: Record<string, any>;
50
+ constructor(opts: SpriteOptions);
51
+ update(dt: number): void;
52
+ draw(ctx: ODOM.Canvas2DContext, scaleX?: number, scaleY?: number): void;
53
+ }
54
+ declare class SpriteLayer {
55
+ sprites: Sprite[];
56
+ add(sprite: Sprite): Sprite;
57
+ remove(sprite: Sprite): void;
58
+ clear(): void;
59
+ update(dt: number): void;
60
+ draw(ctx: ODOM.Canvas2DContext): void;
61
+ /**
62
+ * Find all sprites with a given tag.
63
+ * Example: layer.withTag("enemy")
64
+ */
65
+ withTag(tag: string): Sprite[];
66
+ }
67
+
68
+ declare class AssetManager implements AssetRegistry {
69
+ private imageDefs;
70
+ private soundDefs;
71
+ private sheetDefs;
72
+ private images;
73
+ private sounds;
74
+ private sheets;
75
+ image(key: string, url: string): void;
76
+ sound(key: string, url: string): void;
77
+ spriteSheet(key: string, config: SpriteSheetConfig): void;
78
+ loadAll(): Promise<void>;
79
+ private loadImage;
80
+ private loadSound;
81
+ getImage(key: string): ODOM.Image | undefined;
82
+ getSpriteSheet(key: string): SpriteSheet | undefined;
83
+ createAnimation(sheetKey: string, fps?: number, loop?: boolean): SpriteAnimation;
84
+ getSound(key: string): ODOM.OObject | undefined;
85
+ clean(): void;
86
+ }
87
+
88
+ type VolumeOptions = {
89
+ loop?: boolean;
90
+ volume?: number;
91
+ };
92
+ declare class AudioManager {
93
+ private assets;
94
+ private audioContext;
95
+ private audioDestination;
96
+ private decodedBuffers;
97
+ constructor(assets: AssetManager);
98
+ private getContext;
99
+ private getDestination;
100
+ private getBuffer;
101
+ clean(): void;
102
+ play(key: string, options?: VolumeOptions): Promise<void>;
103
+ }
104
+
105
+ declare class InputManager {
106
+ private keysDown;
107
+ private keysPressed;
108
+ private buttonsPressed;
109
+ private buttonsDown;
110
+ mouseX: number;
111
+ mouseY: number;
112
+ offsetX: number;
113
+ offsetY: number;
114
+ private ids;
115
+ private disposed;
116
+ constructor(targetElement: ODOM.CanvasElement);
117
+ update(): void;
118
+ isDown(code: string): boolean;
119
+ isPressed(code: string): boolean;
120
+ clean(): void;
121
+ private track;
122
+ private updateMousePosition;
123
+ }
124
+
125
+ interface AssetRegistry {
126
+ image(key: string, url: string): void;
127
+ sound(key: string, url: string): void;
128
+ spriteSheet(key: string, config: SpriteSheetConfig): void;
129
+ }
130
+ interface GameContext {
131
+ dt: number;
132
+ canvas: ODOM.CanvasElement;
133
+ ctx2d: ODOM.Canvas2DContext;
134
+ world: Record<string, any>;
135
+ assets: AssetManager;
136
+ audio: AudioManager;
137
+ input: InputManager;
138
+ }
139
+ interface GameConfig {
140
+ canvas: ODOM.CanvasElement;
141
+ background?: string;
142
+ preload?(assets: AssetRegistry): void;
143
+ setup?(ctx: GameContext): void;
144
+ update?(ctx: GameContext): void;
145
+ draw?(ctx: GameContext): Promise<void>;
146
+ }
147
+
148
+ declare class Game {
149
+ private config;
150
+ private canvas;
151
+ private ctx2d;
152
+ private assets;
153
+ private audio;
154
+ private input;
155
+ private world;
156
+ private running;
157
+ private initialized;
158
+ private lastTime;
159
+ private frameId;
160
+ private resizeListenerId;
161
+ constructor(config: GameConfig);
162
+ private buildContext;
163
+ private updateLocalSize;
164
+ private syncSize;
165
+ init(): Promise<void>;
166
+ start(): void;
167
+ stop(): void;
168
+ private scheduleFrame;
169
+ private tick;
170
+ }
171
+ declare function tulon(config: GameConfig): Game;
172
+ declare function objectSize(obj: any): {
173
+ width: number;
174
+ height: number;
175
+ };
176
+
177
+ declare class Camera2D {
178
+ x: number;
179
+ y: number;
180
+ zoom: number;
181
+ rotation: number;
182
+ followTarget: {
183
+ x: number;
184
+ y: number;
185
+ } | null;
186
+ followLerp: number;
187
+ focusX: number;
188
+ focusY: number;
189
+ follow(target: {
190
+ x: number;
191
+ y: number;
192
+ }, lerp?: number): void;
193
+ unfollow(): void;
194
+ update(dt: number): void;
195
+ begin(ctx: ODOM.Canvas2DContext, canvasWidth: number, canvasHeight: number): void;
196
+ end(ctx: ODOM.Canvas2DContext): void;
197
+ }
198
+
199
+ interface TileMap {
200
+ tileWidth: number;
201
+ tileHeight: number;
202
+ width: number;
203
+ height: number;
204
+ getTile(tx: number, ty: number): number;
205
+ isSolid(tx: number, ty: number): boolean;
206
+ }
207
+ interface TileSet {
208
+ image: ODOM.Image;
209
+ tileWidth: number;
210
+ tileHeight: number;
211
+ margin?: number;
212
+ spacing?: number;
213
+ }
214
+ declare function worldToTileX(x: number, map: TileMap): number;
215
+ declare function worldToTileY(y: number, map: TileMap): number;
216
+ declare function getTileSourceRect(index: number, tileset: TileSet): {
217
+ sx: number;
218
+ sy: number;
219
+ sw: number;
220
+ sh: number;
221
+ };
222
+ interface TileRendererOptions {
223
+ skipEmpty?: boolean;
224
+ }
225
+ declare class TileRenderer {
226
+ private tileset;
227
+ private sourceRectCache;
228
+ constructor(tileset: TileSet);
229
+ /**
230
+ * Optimized renderer with camera culling
231
+ *
232
+ * cameraX, cameraY = camera world position (center)
233
+ * viewWidth/viewHeight = viewport size in pixels
234
+ */
235
+ draw(ctx: ODOM.Canvas2DContext, map: TileMap, camera: Camera2D, canvasWidth: number, canvasHeight: number): void;
236
+ private getSourceRect;
237
+ }
238
+
239
+ declare class SceneNode {
240
+ x: number;
241
+ y: number;
242
+ rotation: number;
243
+ scaleX: number;
244
+ scaleY: number;
245
+ visible: boolean;
246
+ parent: SceneNode | null;
247
+ children: SceneNode[];
248
+ add(child: SceneNode): SceneNode;
249
+ remove(child: SceneNode): void;
250
+ clear(): void;
251
+ update(dt: number): void;
252
+ protected drawSelf(ctx: ODOM.Canvas2DContext): void;
253
+ draw(ctx: ODOM.Canvas2DContext): void;
254
+ updateTree(dt: number): void;
255
+ getWorldPosition(): {
256
+ x: number;
257
+ y: number;
258
+ };
259
+ }
260
+ declare class SpriteNode extends SceneNode {
261
+ sprite: Sprite;
262
+ constructor(sprite: Sprite);
263
+ update(dt: number): void;
264
+ protected drawSelf(ctx: ODOM.Canvas2DContext): void;
265
+ getWorldBounds(): {
266
+ x: number;
267
+ y: number;
268
+ width: number;
269
+ height: number;
270
+ };
271
+ }
272
+ declare class SceneLayer extends SceneNode {
273
+ parallaxX: number;
274
+ parallaxY: number;
275
+ constructor(parallaxX?: number, parallaxY?: number);
276
+ }
277
+ declare class Scene {
278
+ static id: number;
279
+ id: number;
280
+ root: SceneNode;
281
+ layers: SceneLayer[];
282
+ constructor();
283
+ addLayer(parallaxX?: number, parallaxY?: number): SceneLayer;
284
+ update(dt: number): void;
285
+ draw(ctx: ODOM.Canvas2DContext): void;
286
+ }
287
+
288
+ interface PhysicsBodySettings {
289
+ gravityX?: number;
290
+ gravityY?: number;
291
+ maxFallSpeed?: number;
292
+ friction?: number;
293
+ airFriction?: number;
294
+ width: number;
295
+ height: number;
296
+ tileMap: TileMap;
297
+ }
298
+ declare class PhysicsBody {
299
+ node: SceneNode;
300
+ settings: PhysicsBodySettings;
301
+ vx: number;
302
+ vy: number;
303
+ onGround: boolean;
304
+ constructor(node: SceneNode, settings: PhysicsBodySettings);
305
+ update(dt: number): void;
306
+ private moveAndCollideX;
307
+ private moveAndCollideY;
308
+ private isSolid;
309
+ }
310
+
311
+ interface AABB {
312
+ x: number;
313
+ y: number;
314
+ width: number;
315
+ height: number;
316
+ }
317
+ interface Collider {
318
+ id: string;
319
+ tag: string;
320
+ isActive: boolean;
321
+ getBounds(): AABB;
322
+ data?: any;
323
+ }
324
+ declare function aabbOverlap(a: AABB, b: AABB): boolean;
325
+ type CollisionHandler = (a: Collider, b: Collider) => void;
326
+ declare class CollisionSystem {
327
+ private colliders;
328
+ private rules;
329
+ private rulesByPair;
330
+ add(collider: Collider): void;
331
+ remove(collider: Collider): void;
332
+ clear(): void;
333
+ /**
334
+ * Register a collision handler between two tags.
335
+ * Order does not matter: ("player","enemy") == ("enemy","player")
336
+ */
337
+ onCollision(tagA: string, tagB: string, handler: CollisionHandler): void;
338
+ /**
339
+ * Call this once per frame AFTER physics updates.
340
+ */
341
+ update(): void;
342
+ private ruleKey;
343
+ private triggerRules;
344
+ }
345
+ declare function createSpritePhysicsCollider(tag: string, node: SceneNode, body: PhysicsBody, sprite?: Sprite): Collider;
346
+
347
+ interface NetMessage<TType extends string = string, TPayload = any> {
348
+ type: TType;
349
+ payload: TPayload;
350
+ }
351
+ interface NetworkTransport {
352
+ connect(url: string): Promise<void>;
353
+ send(data: string): Promise<void>;
354
+ close(code?: number, reason?: string): Promise<void>;
355
+ isConnected(): boolean;
356
+ onMessage(cb: (data: string) => void): void;
357
+ onOpen(cb: () => void): void;
358
+ onClose(cb: (ev?: any) => void): void;
359
+ onError(cb: (err: any) => void): void;
360
+ }
361
+ declare class WebSocketTransport implements NetworkTransport {
362
+ private static readonly OPEN;
363
+ private static readonly CONNECTING;
364
+ private opened;
365
+ private socket;
366
+ private msgHandlers;
367
+ private openHandlers;
368
+ private closeHandlers;
369
+ private errorHandlers;
370
+ private ids;
371
+ connect(url: string): Promise<void>;
372
+ send(data: string): Promise<void>;
373
+ close(code?: number, reason?: string): Promise<void>;
374
+ isConnected(): boolean;
375
+ onMessage(cb: (data: string) => void): void;
376
+ onOpen(cb: () => void): void;
377
+ offOpen(cb: () => void): void;
378
+ onClose(cb: (ev?: any) => void): void;
379
+ offClose(cb: (ev?: any) => void): void;
380
+ onError(cb: (err: any) => void): void;
381
+ offError(cb: (err: any) => void): void;
382
+ private cleanupSocket;
383
+ }
384
+ type Handler<T = any> = (msg: T) => void;
385
+ declare class NetworkClient {
386
+ private transport;
387
+ private handlers;
388
+ private rpcResolvers;
389
+ private rpcCounter;
390
+ constructor(transport: NetworkTransport);
391
+ connect(url: string): Promise<void>;
392
+ isConnected(): boolean;
393
+ send<TType extends string, TPayload>(type: TType, payload: TPayload): void;
394
+ on<TPayload = any>(type: string, handler: Handler<NetMessage<string, TPayload>>): void;
395
+ off(type: string, handler: Handler): void;
396
+ call<TReq = any, TRes = any>(type: string, payload: TReq): Promise<TRes>;
397
+ private handleRawMessage;
398
+ close(): Promise<void>;
399
+ }
400
+ interface StatePatch<TState> {
401
+ full?: TState;
402
+ patch?: Partial<TState>;
403
+ }
404
+ declare class StateSyncClient<TState extends object> {
405
+ private net;
406
+ private channels?;
407
+ private state;
408
+ private onChangeHandlers;
409
+ constructor(net: NetworkClient, initialState: TState, channels?: {
410
+ update: string;
411
+ patch: string;
412
+ });
413
+ getState(): TState;
414
+ onChange(handler: (state: TState) => void): void;
415
+ private emitChange;
416
+ sendPatch(patch: Partial<TState>): void;
417
+ }
418
+
419
+ export { type AABB, AssetManager, type AssetRegistry, AudioManager, Camera2D, type Collider, type CollisionHandler, CollisionSystem, Game, type GameConfig, type GameContext, InputManager, type NetMessage, NetworkClient, type NetworkTransport, PhysicsBody, type PhysicsBodySettings, Scene, SceneLayer, SceneNode, Sprite, SpriteAnimation, SpriteLayer, SpriteNode, type SpriteOptions, SpriteSheet, type SpriteSheetConfig, type StatePatch, StateSyncClient, type TileMap, TileRenderer, type TileRendererOptions, type TileSet, WebSocketTransport, aabbOverlap, createSpritePhysicsCollider, getTileSourceRect, objectSize, tulon, worldToTileX, worldToTileY };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ 'use strict';var ouider=require('ouider');var et=Object.defineProperty,bt=Object.defineProperties;var Dt=Object.getOwnPropertyDescriptors;var tt=Object.getOwnPropertySymbols;var Ct=Object.prototype.hasOwnProperty,Et=Object.prototype.propertyIsEnumerable;var Y=(d,t,e)=>t in d?et(d,t,{enumerable:true,configurable:true,writable:true,value:e}):d[t]=e,st=(d,t)=>{for(var e in t||(t={}))Ct.call(t,e)&&Y(d,e,t[e]);if(tt)for(var e of tt(t))Et.call(t,e)&&Y(d,e,t[e]);return d},it=(d,t)=>bt(d,Dt(t)),c=(d,t)=>et(d,"name",{value:t,configurable:true});var i=(d,t,e)=>Y(d,typeof t!="symbol"?t+"":t,e);var z=class z{constructor(t,e,s,o,n=0){i(this,"image");i(this,"frameWidth");i(this,"frameHeight");i(this,"frameCount");i(this,"from");i(this,"columns");this.image=t,this.frameWidth=e,this.frameHeight=s,this.frameCount=o,this.from=n;let r=I(t);this.columns=Math.max(1,Math.floor(r.width/e));}};c(z,"SpriteSheet");var E=z,S=class S{constructor(t,e=10,s=true){i(this,"sheet");i(this,"fps");i(this,"loop");i(this,"time",0);this.sheet=t,this.fps=e,this.loop=s;}update(t){this.time+=t;let e=this.sheet.frameCount/this.fps;this.loop?this.time=this.time%e:this.time>e&&(this.time=e);}draw(t,e,s,o=1,n=1){let r=Math.floor(this.time*this.fps)+this.sheet.from,a=Math.min(r,this.sheet.frameCount+this.sheet.from-1),h=this.sheet.columns,l=a%h*this.sheet.frameWidth,p=Math.floor(a/h)*this.sheet.frameHeight,u=this.sheet.frameWidth*o,f=this.sheet.frameHeight*n;t.drawImage(this.sheet.image,l,p,this.sheet.frameWidth,this.sheet.frameHeight,e,s,u,f);}reset(){this.time=0;}};c(S,"SpriteAnimation");var O=S,L=class L{constructor(t){i(this,"animation");i(this,"width");i(this,"height");i(this,"visible");i(this,"tag");i(this,"data");var e,s,o,n;this.animation=t.animation,this.visible=(e=t.visible)!=null?e:true,this.tag=t.tag,this.data=(s=t.data)!=null?s:{},this.width=(o=t.width)!=null?o:this.animation.sheet.frameWidth,this.height=(n=t.height)!=null?n:this.animation.sheet.frameHeight;}update(t){this.animation.update(t);}draw(t,e=1,s=1){this.visible&&this.animation.draw(t,0,0,e,s);}};c(L,"Sprite");var ot=L,W=class W{constructor(){i(this,"sprites",[]);}add(t){return this.sprites.push(t),t}remove(t){let e=this.sprites.indexOf(t);e>=0&&this.sprites.splice(e,1);}clear(){this.sprites.length=0;}update(t){for(let e of this.sprites)e.update(t);}draw(t){for(let e of this.sprites)e.draw(t);}withTag(t){return this.sprites.filter(e=>e.tag===t)}};c(W,"SpriteLayer");var nt=W;var B=class B{constructor(){i(this,"imageDefs",new Map);i(this,"soundDefs",new Map);i(this,"sheetDefs",new Map);i(this,"images",new Map);i(this,"sounds",new Map);i(this,"sheets",new Map);}image(t,e){this.imageDefs.set(t,e);}sound(t,e){this.soundDefs.set(t,e);}spriteSheet(t,e){this.sheetDefs.set(t,e);}async loadAll(){var s,o;let t=[],e=[];for(let[n,r]of this.imageDefs.entries())t.push(this.loadImage(r).then(a=>{this.images.set(n,a);}));for(let[n,r]of this.soundDefs.entries())e.push(this.loadSound(r).then(a=>{this.sounds.set(n,a);}));await Promise.all([...t,...e]);for(let[n,r]of this.sheetDefs.entries()){let a=(s=this.getImage(r.image))!=null?s:this.images.get(r.image);if(!a)throw new Error(`SpriteSheet "${n}" missing image "${r.image}"`);let h=new E(a,r.frameWidth,r.frameHeight,r.frameCount,(o=r.from)!=null?o:0);this.sheets.set(n,h);}}async loadImage(t){let e=await ouider.ODOM.Image.new();if(!e)throw new Error(`Unable to create image for "${t}"`);let s=null,o=null,n=false,r=c(async()=>{let h=[s,o].filter(Boolean);s=null,o=null,await Promise.all(h.map(l=>e.dettachEventListener(l).catch(()=>{})));},"detach"),a=c(async()=>{let[h,l,p,u]=await Promise.all([e.naturalWidth(),e.naturalHeight(),e.width(),e.height()]);e.__local_size__={width:h||p||0,height:l||u||0};},"cacheSize");return new Promise(async(h,l)=>{let p=c(async()=>{if(!n)try{await a(),n=!0,await r(),h(e);}catch(f){await u(f);}},"done"),u=c(async f=>{n||(n=true,await r(),await e.release().catch(()=>{}),l(new Error(`Unable to load image "${t}"`)));},"fail");try{s=await e.addEventListener("load",()=>{p();},{options:{once:!0}}),o=await e.addEventListener("error",()=>{u();},{options:{once:!0}}),await e.setSrc(t);let[f,m]=await Promise.all([e.getProperty("complete").catch(()=>!1),e.naturalWidth().catch(()=>0)]);f&&m>0&&await p();}catch(f){await u(f);}})}async loadSound(t){return await OUID.fetch(t,{method:"GET",headers:{},body:null,credentials:"omit"},"buffer")}getImage(t){return this.images.get(t)}getSpriteSheet(t){return this.sheets.get(t)}createAnimation(t,e,s){let o=this.getSpriteSheet(t);if(!o)throw new Error(`SpriteSheet "${t}" not found`);return new O(o,e!=null?e:10,s!=null?s:true)}getSound(t){return this.sounds.get(t)}clean(){this.images.forEach(t=>t.release()),this.sounds.forEach(t=>t.release()),this.images.clear(),this.sounds.clear(),this.sheets.clear();}};c(B,"AssetManager");var H=B;var A=class A{constructor(t){i(this,"assets");i(this,"audioContext",null);i(this,"audioDestination",null);i(this,"decodedBuffers",new Map);this.assets=t;}async getContext(){if(this.audioContext)return this.audioContext;let t=await OUID.newObject("AudioContext");return t?(this.audioContext=ouider.ODOM.OObject.toInstance(t),this.audioContext):null}async getDestination(t){if(this.audioDestination)return this.audioDestination;let e=await t.getPropertyAndHold("destination");return e?(this.audioDestination=e,this.audioDestination):null}async getBuffer(t){if(this.decodedBuffers.has(t))return this.decodedBuffers.get(t);let e=this.assets.getSound(t);if(!e)return null;let s=await this.getContext();if(!s)return null;let o=await s.invokeAndHold("decodeAudioData",e.ref);return o&&this.decodedBuffers.set(t,o),o}clean(){this.decodedBuffers.forEach(t=>t.release()),this.decodedBuffers.clear(),this.audioDestination&&(this.audioDestination.release(),this.audioDestination=null),this.audioContext&&(this.audioContext.invoke("close"),this.audioContext.release(),this.audioContext=null);}async play(t,e){let s=await this.getContext();if(!s)return;let o=await this.getBuffer(t);if(!o)return;await s.invoke("resume");let n=await s.invokeAndHold("createBufferSource");if(!n)return;await n.setProperty("buffer",o.ref),(e==null?void 0:e.loop)!=null&&await n.setProperty("loop",e.loop);let r=await this.getDestination(s);if(!r){n.release();return}if((e==null?void 0:e.volume)!=null){let a=await s.invokeAndHold("createGain");if(a){let h=await a.getPropertyAndHold("gain");h&&(await h.setProperty("value",e.volume),h.release()),await n.invoke("connect",a.ref),await a.invoke("connect",r.ref),await n.invoke("start",0),a.release(),n.release();return}}await n.invoke("connect",r.ref),await n.invoke("start",0),n.release();}};c(A,"AudioManager");var X=A;var R=class R{constructor(t){i(this,"keysDown",new Set);i(this,"keysPressed",new Set);i(this,"buttonsPressed",new Set);i(this,"buttonsDown",new Set);i(this,"mouseX",-1);i(this,"mouseY",-1);i(this,"offsetX",-1);i(this,"offsetY",-1);i(this,"ids",[]);i(this,"disposed",false);this.track(OUID.addEventListener("window","keydown",e=>{this.keysDown.has(e.code)||this.keysPressed.add(e.code),this.keysDown.add(e.code);})),this.track(OUID.addEventListener("window","keyup",e=>{this.keysDown.delete(e.code);})),this.track(t.addEventListener("mousedown",e=>{let s=e.button===0?"MouseLeft":"MouseRight";this.buttonsDown.has(s)||this.buttonsPressed.add(s),this.buttonsDown.add(s),this.updateMousePosition(e);})),this.track(t.addEventListener("mouseup",e=>{let s=e.button===0?"MouseLeft":"MouseRight";this.buttonsDown.delete(s),this.updateMousePosition(e);})),this.track(t.addEventListener("mousemove",e=>{this.updateMousePosition(e);})),this.track(t.addEventListener("mouseleave",()=>{this.buttonsDown.clear();}));}update(){this.keysPressed.clear(),this.buttonsPressed.clear();}isDown(t){return this.keysDown.has(t)||this.buttonsDown.has(t)}isPressed(t){return this.keysPressed.has(t)||this.buttonsPressed.has(t)}clean(){this.disposed=true,this.ids.forEach(t=>{OUID.dettachEventListener(t);}),this.ids=[],this.keysDown.clear(),this.keysPressed.clear(),this.buttonsDown.clear(),this.buttonsPressed.clear();}track(t){t.then(e=>{this.disposed?OUID.dettachEventListener(e):this.ids.push(e);}).catch(()=>{});}updateMousePosition(t){var e,s,o,n;this.mouseX=(e=t.x)!=null?e:this.mouseX,this.mouseY=(s=t.y)!=null?s:this.mouseY,this.offsetX=(o=t.offsetX)!=null?o:this.offsetX,this.offsetY=(n=t.offsetY)!=null?n:this.offsetY;}};c(R,"InputManager");var T=R;var $=class ${constructor(t){i(this,"config");i(this,"canvas");i(this,"ctx2d");i(this,"assets");i(this,"audio");i(this,"input");i(this,"world",{});i(this,"running",false);i(this,"initialized",false);i(this,"lastTime",0);i(this,"frameId",null);i(this,"resizeListenerId",null);this.config=t,this.canvas=t.canvas,this.assets=new H,this.audio=new X(this.assets),this.input=new T(this.canvas);}buildContext(t){return {dt:t,canvas:this.canvas,ctx2d:this.ctx2d,world:this.world,assets:this.assets,audio:this.audio,input:this.input}}async updateLocalSize(){var s,o;let t=(s=await this.canvas.width())!=null?s:0,e=(o=await this.canvas.height())!=null?o:0;this.canvas.__local_size__=this.ctx2d.__local_size__={width:t,height:e};}async syncSize(){var a,h;let[t,e,s,o]=await Promise.all([this.canvas.getProperty("clientWidth"),this.canvas.getProperty("clientHeight"),this.canvas.width(),this.canvas.height()]),n=(a=t!=null?t:s)!=null?a:0,r=(h=e!=null?e:o)!=null?h:0;await this.canvas.setProperty("width",n),await this.canvas.setProperty("height",r);}async init(){if(this.initialized)return;let t=await this.canvas.getContext("2d");if(!t)throw new Error("Unable to create a context");this.ctx2d=t,this.resizeListenerId=await this.canvas.addEventListener("resize",()=>{this.syncSize().then(()=>this.updateLocalSize());}),this.config.preload&&(this.config.preload(this.assets),await this.assets.loadAll()),await this.syncSize(),await this.updateLocalSize(),this.config.setup&&this.config.setup(this.buildContext(0)),this.initialized=true;}start(){this.running||(this.running=true,this.lastTime=performance.now(),this.scheduleFrame());}stop(){var t;!this.running&&!this.initialized||(this.running=false,this.frameId!=null&&(OUID.cancelAnimationFrame(this.frameId),this.frameId=null),this.resizeListenerId&&(OUID.dettachEventListener(this.resizeListenerId),this.resizeListenerId=null),this.audio.clean(),this.assets.clean(),this.input.clean(),(t=this.ctx2d)==null||t.release(),this.initialized=false);}scheduleFrame(){OUID.requestAnimationFrame(t=>{this.frameId=null,this.tick(t);}).then(t=>{this.running?this.frameId=t:OUID.cancelAnimationFrame(t);});}async tick(t){var o,n,r,a;if(!this.running)return;let s=Math.min((t-this.lastTime)/1e3,.1);this.lastTime=t;try{let h=this.buildContext(s);(n=(o=this.config).update)==null||n.call(o,h),this.input.update(),await((a=(r=this.config).draw)==null?void 0:a.call(r,h)),await this.ctx2d.commit();}catch(h){console.error("[Toubani] game loop error",h);}finally{this.running&&this.scheduleFrame();}}};c($,"Game");var U=$;function Kt(d){return new U(d)}c(Kt,"tulon");function I(d){var t;return (t=d.__local_size__)!=null?t:{width:0,height:0}}c(I,"objectSize");function v(d,t){return Math.floor(d/t.tileWidth)}c(v,"worldToTileX");function M(d,t){return Math.floor(d/t.tileHeight)}c(M,"worldToTileY");function Ht(d,t){let{image:e,tileWidth:s,tileHeight:o,margin:n=0,spacing:r=0}=t,a=I(e),h=Math.max(1,Math.floor((a.width-2*n+r)/(s+r))),l=d-1,p=l%h,u=Math.floor(l/h),f=n+p*(s+r),m=n+u*(o+r);return {sx:f,sy:m,sw:s,sh:o}}c(Ht,"getTileSourceRect");var N=class N{constructor(t){i(this,"tileset");i(this,"sourceRectCache",new Map);this.tileset=t;}draw(t,e,s,o,n){let{tileWidth:r,tileHeight:a}=e,h=o*s.focusX,l=n*s.focusY,p=s.zoom,u=s.x-h/p,f=s.y-l/p,m=s.x+(o-h)/p,w=s.y+(n-l)/p,g=Math.max(0,Math.floor(u/r)),y=Math.min(e.width-1,Math.floor(m/r)),b=Math.max(0,Math.floor(f/a)),mt=Math.min(e.height-1,Math.floor(w/a));for(let D=b;D<=mt;D++)for(let C=g;C<=y;C++){let _=e.getTile(C,D);if(_<=0)continue;let{sx:xt,sy:yt,sw:vt,sh:Mt}=this.getSourceRect(_),kt=C*r,Pt=D*a;t.drawImage(this.tileset.image,xt,yt,vt,Mt,kt,Pt,r,a);}}getSourceRect(t){let e=this.sourceRectCache.get(t);return e||(e=Ht(t,this.tileset),this.sourceRectCache.set(t,e)),e}};c(N,"TileRenderer");var rt=N;var j=class j{constructor(t,e){i(this,"node");i(this,"settings");i(this,"vx",0);i(this,"vy",0);i(this,"onGround",false);this.node=t,this.settings=e;}update(t){var f,m,w,g,y;let e=this.settings,s=(f=e.gravityX)!=null?f:0,o=(m=e.gravityY)!=null?m:1e3,n=(w=e.maxFallSpeed)!=null?w:2e3,r=(g=e.friction)!=null?g:.8,a=(y=e.airFriction)!=null?y:.98;this.vx+=s*t,this.vy+=o*t,this.vy>n&&(this.vy=n);let h=Math.max(1,Math.min(e.tileMap.tileWidth,e.tileMap.tileHeight)*.5),l=Math.max(Math.abs(this.vx*t),Math.abs(this.vy*t)),p=Math.max(1,Math.ceil(l/h)),u=t/p;for(let b=0;b<p;b++)this.moveAndCollideX(u),this.moveAndCollideY(u);this.onGround?this.vx*=r:this.vx*=a;}moveAndCollideX(t){let e=this.settings.tileMap,s=this.settings.width,o=this.settings.height,n=this.node.x+this.vx*t,r=this.node.y,a=Math.sign(this.vx);if(a===0){this.node.x=n;return}let h=n,l=n+s,p=r,u=r+o,f=Math.max(0,M(p,e)),m=Math.min(e.height-1,M(u-1,e));if(a>0){let w=v(l,e);for(let g=f;g<=m;g++)if(this.isSolid(w,g)){l=w*e.tileWidth,h=l-s,this.vx=0;break}}else {let w=v(h,e);for(let g=f;g<=m;g++)if(this.isSolid(w,g)){h=(w+1)*e.tileWidth,l=h+s,this.vx=0;break}}this.node.x=h;}moveAndCollideY(t){let e=this.settings.tileMap,s=this.settings.width,o=this.settings.height,n=this.node.x,r=this.node.y+this.vy*t,a=Math.sign(this.vy);if(this.onGround=false,a===0){this.node.y=r;return}let h=r,l=r+o,p=n,u=n+s,f=Math.max(0,v(p,e)),m=Math.min(e.width-1,v(u-1,e));if(a>0){let w=M(l,e);for(let g=f;g<=m;g++)if(this.isSolid(g,w)){l=w*e.tileHeight,h=l-o,this.vy=0,this.onGround=true;break}}else {let w=M(h,e);for(let g=f;g<=m;g++)if(this.isSolid(g,w)){h=(w+1)*e.tileHeight,l=h+o,this.vy=0;break}}this.node.y=h;}isSolid(t,e){let s=this.settings.tileMap;return t<0||e<0||t>=s.width||e>=s.height?false:s.isSolid(t,e)}};c(j,"PhysicsBody");var at=j;var F=class F{constructor(){i(this,"x",0);i(this,"y",0);i(this,"zoom",1);i(this,"rotation",0);i(this,"followTarget",null);i(this,"followLerp",.1);i(this,"focusX",.5);i(this,"focusY",.5);}follow(t,e=.1){this.followTarget=t,this.followLerp=e;}unfollow(){this.followTarget=null;}update(t){if(!this.followTarget)return;let e=this.followTarget.x,s=this.followTarget.y;this.x+=(e-this.x)*this.followLerp,this.y+=(s-this.y)*this.followLerp;}begin(t,e,s){t.save();let o=e*this.focusX,n=s*this.focusY;t.translate(o,n),this.rotation!==0&&t.rotate(-this.rotation),this.zoom!==1&&t.scale(this.zoom,this.zoom),t.translate(-this.x,-this.y);}end(t){t.restore();}};c(F,"Camera2D");var ht=F;var J=class J{constructor(){i(this,"x",0);i(this,"y",0);i(this,"rotation",0);i(this,"scaleX",1);i(this,"scaleY",1);i(this,"visible",true);i(this,"parent",null);i(this,"children",[]);}add(t){return t.parent=this,this.children.push(t),t}remove(t){let e=this.children.indexOf(t);e>=0&&(this.children.splice(e,1),t.parent=null);}clear(){for(let t of this.children)t.parent=null;this.children.length=0;}update(t){}drawSelf(t){}draw(t){if(this.visible){t.save(),t.translate(this.x,this.y),this.rotation!==0&&t.rotate(this.rotation),(this.scaleX!==1||this.scaleY!==1)&&t.scale(this.scaleX,this.scaleY),this.drawSelf(t);for(let e of this.children)e.draw(t);t.restore();}}updateTree(t){this.update(t);for(let e of this.children)e.updateTree(t);}getWorldPosition(){let t=this.x,e=this.y,s=this.parent;for(;s;)t+=s.x,e+=s.y,s=s.parent;return {x:t,y:e}}};c(J,"SceneNode");var P=J,q=class q extends P{constructor(e){super();i(this,"sprite");this.sprite=e;}update(e){this.sprite.update(e);}drawSelf(e){this.sprite.draw(e);}getWorldBounds(){let e=this.getWorldPosition();return {x:e.x,y:e.y,width:this.sprite.width*this.scaleX,height:this.sprite.height*this.scaleY}}};c(q,"SpriteNode");var ct=q,K=class K extends P{constructor(e=1,s=1){super();i(this,"parallaxX",1);i(this,"parallaxY",1);this.parallaxX=e,this.parallaxY=s;}};c(K,"SceneLayer");var G=K,k=class k{constructor(){i(this,"id");i(this,"root",new P);i(this,"layers",[]);this.id=k.id++;}addLayer(t=1,e=1){let s=new G(t,e);return this.layers.push(s),this.root.add(s),s}update(t){this.root.updateTree(t);}draw(t){this.root.draw(t);}};c(k,"Scene"),i(k,"id",0);var lt=k;function dt(d,t){return !(d.x+d.width<=t.x||d.x>=t.x+t.width||d.y+d.height<=t.y||d.y>=t.y+t.height)}c(dt,"aabbOverlap");var Q=class Q{constructor(){i(this,"colliders",[]);i(this,"rules",[]);i(this,"rulesByPair",new Map);}add(t){this.colliders.push(t);}remove(t){let e=this.colliders.indexOf(t);e>=0&&this.colliders.splice(e,1);}clear(){this.colliders.length=0;}onCollision(t,e,s){var a;let o={tagA:t,tagB:e,handler:s};this.rules.push(o);let n=this.ruleKey(t,e),r=(a=this.rulesByPair.get(n))!=null?a:[];r.push(o),this.rulesByPair.set(n,r);}update(){var o;let t=new Map,e=new Map;for(let n of this.colliders){if(!n.isActive)continue;let r=(o=t.get(n.tag))!=null?o:[];r.push(n),t.set(n.tag,r);}let s=c(n=>{let r=e.get(n);return r||(r=n.getBounds(),e.set(n,r)),r},"getBounds");for(let[n,r]of this.rulesByPair.entries()){let[a,h]=n.split("\0"),l=t.get(a),p=t.get(h);if(!(!l||!p))if(a===h)for(let u=0;u<l.length;u++){let f=l[u],m=s(f);for(let w=u+1;w<l.length;w++){let g=l[w];dt(m,s(g))&&this.triggerRules(f,g,r);}}else for(let u of l){let f=s(u);for(let m of p)dt(f,s(m))&&this.triggerRules(u,m,r);}}}ruleKey(t,e){return t<=e?`${t}\0${e}`:`${e}\0${t}`}triggerRules(t,e,s){for(let o of s){let n=o.tagA===t.tag&&o.tagB===e.tag,r=o.tagA===e.tag&&o.tagB===t.tag;n?o.handler(t,e):r&&o.handler(e,t);}}};c(Q,"CollisionSystem");var ut=Q,Xt=0;function he(d,t,e,s){return {id:`col-${Xt++}`,tag:d,isActive:true,data:{node:t,body:e,sprite:s},getBounds(){let n=t.getWorldPosition(),r=e.settings.width,a=e.settings.height;return {x:n.x,y:n.y,width:r,height:a}}}}c(he,"createSpritePhysicsCollider");var x=class x{constructor(){i(this,"opened",false);i(this,"socket",null);i(this,"msgHandlers",[]);i(this,"openHandlers",[]);i(this,"closeHandlers",[]);i(this,"errorHandlers",[]);i(this,"ids",[]);}async connect(t){if(this.socket){let s=await this.socket.getProperty("readyState");if(s===x.OPEN||s===x.CONNECTING)return;await this.cleanupSocket(false);}let e=await ouider.OUID.newObject("WebSocket",t);if(!e)throw new Error(`Unable to create WebSocket for ${t}`);this.socket=ouider.ODOM.OObject.toInstance(e),this.ids.push(await this.socket.addEventListener("open",()=>{this.opened=true,this.openHandlers.forEach(s=>s());})),this.ids.push(await this.socket.addEventListener("message",s=>{let o=typeof s.data=="string"?s.data:"";this.msgHandlers.forEach(n=>n(o));})),this.ids.push(await this.socket.addEventListener("close",s=>{this.opened=false,this.closeHandlers.forEach(o=>o(s));})),this.ids.push(await this.socket.addEventListener("error",s=>{this.errorHandlers.forEach(o=>o(s));}));try{await new Promise((s,o)=>{let n=c(()=>{h(),s();},"onOpen"),r=c(l=>{h(),o(l);},"onError"),a=c(l=>{h(),o(l);},"onClose"),h=c(()=>{this.offOpen(n),this.offError(r),this.offClose(a);},"cleanup");this.onOpen(n),this.onError(r),this.onClose(a);});}catch(s){throw await this.cleanupSocket(false),s}}async send(t){this.socket&&await this.socket.getProperty("readyState")===x.OPEN&&await this.socket.invoke("send",t);}async close(t,e){await this.cleanupSocket(true,t,e);}isConnected(){return this.opened}onMessage(t){this.msgHandlers.push(t);}onOpen(t){this.openHandlers.push(t);}offOpen(t){this.openHandlers=this.openHandlers.filter(e=>e!==t);}onClose(t){this.closeHandlers.push(t);}offClose(t){this.closeHandlers=this.closeHandlers.filter(e=>e!==t);}onError(t){this.errorHandlers.push(t);}offError(t){this.errorHandlers=this.errorHandlers.filter(e=>e!==t);}async cleanupSocket(t,e,s){let o=this.socket;this.socket=null,this.opened=false;let n=this.ids.splice(0);if(await Promise.all(n.map(r=>ouider.OUID.dettachEventListener(r).catch(()=>{}))),!!o)try{if(t){let r=await o.getProperty("readyState").catch(()=>null);(r===x.OPEN||r===x.CONNECTING)&&await o.invoke("close",e,s);}}finally{await o.release().catch(()=>{});}}};c(x,"WebSocketTransport"),i(x,"OPEN",1),i(x,"CONNECTING",0);var pt=x,V=class V{constructor(t){i(this,"transport");i(this,"handlers",new Map);i(this,"rpcResolvers",new Map);i(this,"rpcCounter",0);i(this,"handleRawMessage",c(t=>{if(!t)return;let e;try{e=JSON.parse(t);}catch(o){return}if(e.payload&&typeof e.payload=="object"&&"rpcId"in e.payload){let o=e.payload.rpcId,n=this.rpcResolvers.get(o);n&&(n.resolve(e.payload),this.rpcResolvers.delete(o));}let s=this.handlers.get(e.type);if(s)for(let o of s)o(e);},"handleRawMessage"));this.transport=t,this.transport.onMessage(this.handleRawMessage);}async connect(t){await this.transport.connect(t);}isConnected(){return this.transport.isConnected()}send(t,e){let s={type:t,payload:e};this.transport.send(JSON.stringify(s));}on(t,e){var o;let s=(o=this.handlers.get(t))!=null?o:[];s.push(e),this.handlers.set(t,s);}off(t,e){let s=this.handlers.get(t);s&&this.handlers.set(t,s.filter(o=>o!==e));}call(t,e){let s=`rpc_${++this.rpcCounter}_${Date.now()}`,o=e&&typeof e=="object"?it(st({},e),{rpcId:s}):{value:e,rpcId:s},n={type:t,payload:o};return new Promise((r,a)=>{this.rpcResolvers.set(s,{resolve:r,reject:a}),this.transport.send(JSON.stringify(n)).catch(h=>{this.rpcResolvers.delete(s),a(h);});})}close(){return this.rpcResolvers.forEach(({reject:t})=>t(new Error("Network client closed"))),this.rpcResolvers.clear(),this.transport.close()}};c(V,"NetworkClient");var gt=V,Z=class Z{constructor(t,e,s){i(this,"net");i(this,"channels");i(this,"state");i(this,"onChangeHandlers",[]);var o;this.net=t,this.channels=s,this.state=e,this.net.on((o=s==null?void 0:s.update)!=null?o:"state_update",n=>{let{payload:r}=n;r.full?this.state=r.full:r.patch&&(this.state=Object.assign({},this.state,r.patch)),this.emitChange();});}getState(){return this.state}onChange(t){this.onChangeHandlers.push(t);}emitChange(){for(let t of this.onChangeHandlers)t(this.state);}sendPatch(t){var e,s;this.net.send((s=(e=this.channels)==null?void 0:e.patch)!=null?s:"state_patch",t);}};c(Z,"StateSyncClient");var wt=Z;exports.AssetManager=H;exports.AudioManager=X;exports.Camera2D=ht;exports.CollisionSystem=ut;exports.Game=U;exports.InputManager=T;exports.NetworkClient=gt;exports.PhysicsBody=at;exports.Scene=lt;exports.SceneLayer=G;exports.SceneNode=P;exports.Sprite=ot;exports.SpriteAnimation=O;exports.SpriteLayer=nt;exports.SpriteNode=ct;exports.SpriteSheet=E;exports.StateSyncClient=wt;exports.TileRenderer=rt;exports.WebSocketTransport=pt;exports.aabbOverlap=dt;exports.createSpritePhysicsCollider=he;exports.getTileSourceRect=Ht;exports.objectSize=I;exports.tulon=Kt;exports.worldToTileX=v;exports.worldToTileY=M;
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import {ODOM,OUID as OUID$1}from'ouider';var et=Object.defineProperty,bt=Object.defineProperties;var Dt=Object.getOwnPropertyDescriptors;var tt=Object.getOwnPropertySymbols;var Ct=Object.prototype.hasOwnProperty,Et=Object.prototype.propertyIsEnumerable;var Y=(d,t,e)=>t in d?et(d,t,{enumerable:true,configurable:true,writable:true,value:e}):d[t]=e,st=(d,t)=>{for(var e in t||(t={}))Ct.call(t,e)&&Y(d,e,t[e]);if(tt)for(var e of tt(t))Et.call(t,e)&&Y(d,e,t[e]);return d},it=(d,t)=>bt(d,Dt(t)),c=(d,t)=>et(d,"name",{value:t,configurable:true});var i=(d,t,e)=>Y(d,typeof t!="symbol"?t+"":t,e);var z=class z{constructor(t,e,s,o,n=0){i(this,"image");i(this,"frameWidth");i(this,"frameHeight");i(this,"frameCount");i(this,"from");i(this,"columns");this.image=t,this.frameWidth=e,this.frameHeight=s,this.frameCount=o,this.from=n;let r=I(t);this.columns=Math.max(1,Math.floor(r.width/e));}};c(z,"SpriteSheet");var E=z,S=class S{constructor(t,e=10,s=true){i(this,"sheet");i(this,"fps");i(this,"loop");i(this,"time",0);this.sheet=t,this.fps=e,this.loop=s;}update(t){this.time+=t;let e=this.sheet.frameCount/this.fps;this.loop?this.time=this.time%e:this.time>e&&(this.time=e);}draw(t,e,s,o=1,n=1){let r=Math.floor(this.time*this.fps)+this.sheet.from,a=Math.min(r,this.sheet.frameCount+this.sheet.from-1),h=this.sheet.columns,l=a%h*this.sheet.frameWidth,p=Math.floor(a/h)*this.sheet.frameHeight,u=this.sheet.frameWidth*o,f=this.sheet.frameHeight*n;t.drawImage(this.sheet.image,l,p,this.sheet.frameWidth,this.sheet.frameHeight,e,s,u,f);}reset(){this.time=0;}};c(S,"SpriteAnimation");var O=S,L=class L{constructor(t){i(this,"animation");i(this,"width");i(this,"height");i(this,"visible");i(this,"tag");i(this,"data");var e,s,o,n;this.animation=t.animation,this.visible=(e=t.visible)!=null?e:true,this.tag=t.tag,this.data=(s=t.data)!=null?s:{},this.width=(o=t.width)!=null?o:this.animation.sheet.frameWidth,this.height=(n=t.height)!=null?n:this.animation.sheet.frameHeight;}update(t){this.animation.update(t);}draw(t,e=1,s=1){this.visible&&this.animation.draw(t,0,0,e,s);}};c(L,"Sprite");var ot=L,W=class W{constructor(){i(this,"sprites",[]);}add(t){return this.sprites.push(t),t}remove(t){let e=this.sprites.indexOf(t);e>=0&&this.sprites.splice(e,1);}clear(){this.sprites.length=0;}update(t){for(let e of this.sprites)e.update(t);}draw(t){for(let e of this.sprites)e.draw(t);}withTag(t){return this.sprites.filter(e=>e.tag===t)}};c(W,"SpriteLayer");var nt=W;var B=class B{constructor(){i(this,"imageDefs",new Map);i(this,"soundDefs",new Map);i(this,"sheetDefs",new Map);i(this,"images",new Map);i(this,"sounds",new Map);i(this,"sheets",new Map);}image(t,e){this.imageDefs.set(t,e);}sound(t,e){this.soundDefs.set(t,e);}spriteSheet(t,e){this.sheetDefs.set(t,e);}async loadAll(){var s,o;let t=[],e=[];for(let[n,r]of this.imageDefs.entries())t.push(this.loadImage(r).then(a=>{this.images.set(n,a);}));for(let[n,r]of this.soundDefs.entries())e.push(this.loadSound(r).then(a=>{this.sounds.set(n,a);}));await Promise.all([...t,...e]);for(let[n,r]of this.sheetDefs.entries()){let a=(s=this.getImage(r.image))!=null?s:this.images.get(r.image);if(!a)throw new Error(`SpriteSheet "${n}" missing image "${r.image}"`);let h=new E(a,r.frameWidth,r.frameHeight,r.frameCount,(o=r.from)!=null?o:0);this.sheets.set(n,h);}}async loadImage(t){let e=await ODOM.Image.new();if(!e)throw new Error(`Unable to create image for "${t}"`);let s=null,o=null,n=false,r=c(async()=>{let h=[s,o].filter(Boolean);s=null,o=null,await Promise.all(h.map(l=>e.dettachEventListener(l).catch(()=>{})));},"detach"),a=c(async()=>{let[h,l,p,u]=await Promise.all([e.naturalWidth(),e.naturalHeight(),e.width(),e.height()]);e.__local_size__={width:h||p||0,height:l||u||0};},"cacheSize");return new Promise(async(h,l)=>{let p=c(async()=>{if(!n)try{await a(),n=!0,await r(),h(e);}catch(f){await u(f);}},"done"),u=c(async f=>{n||(n=true,await r(),await e.release().catch(()=>{}),l(new Error(`Unable to load image "${t}"`)));},"fail");try{s=await e.addEventListener("load",()=>{p();},{options:{once:!0}}),o=await e.addEventListener("error",()=>{u();},{options:{once:!0}}),await e.setSrc(t);let[f,m]=await Promise.all([e.getProperty("complete").catch(()=>!1),e.naturalWidth().catch(()=>0)]);f&&m>0&&await p();}catch(f){await u(f);}})}async loadSound(t){return await OUID.fetch(t,{method:"GET",headers:{},body:null,credentials:"omit"},"buffer")}getImage(t){return this.images.get(t)}getSpriteSheet(t){return this.sheets.get(t)}createAnimation(t,e,s){let o=this.getSpriteSheet(t);if(!o)throw new Error(`SpriteSheet "${t}" not found`);return new O(o,e!=null?e:10,s!=null?s:true)}getSound(t){return this.sounds.get(t)}clean(){this.images.forEach(t=>t.release()),this.sounds.forEach(t=>t.release()),this.images.clear(),this.sounds.clear(),this.sheets.clear();}};c(B,"AssetManager");var H=B;var A=class A{constructor(t){i(this,"assets");i(this,"audioContext",null);i(this,"audioDestination",null);i(this,"decodedBuffers",new Map);this.assets=t;}async getContext(){if(this.audioContext)return this.audioContext;let t=await OUID.newObject("AudioContext");return t?(this.audioContext=ODOM.OObject.toInstance(t),this.audioContext):null}async getDestination(t){if(this.audioDestination)return this.audioDestination;let e=await t.getPropertyAndHold("destination");return e?(this.audioDestination=e,this.audioDestination):null}async getBuffer(t){if(this.decodedBuffers.has(t))return this.decodedBuffers.get(t);let e=this.assets.getSound(t);if(!e)return null;let s=await this.getContext();if(!s)return null;let o=await s.invokeAndHold("decodeAudioData",e.ref);return o&&this.decodedBuffers.set(t,o),o}clean(){this.decodedBuffers.forEach(t=>t.release()),this.decodedBuffers.clear(),this.audioDestination&&(this.audioDestination.release(),this.audioDestination=null),this.audioContext&&(this.audioContext.invoke("close"),this.audioContext.release(),this.audioContext=null);}async play(t,e){let s=await this.getContext();if(!s)return;let o=await this.getBuffer(t);if(!o)return;await s.invoke("resume");let n=await s.invokeAndHold("createBufferSource");if(!n)return;await n.setProperty("buffer",o.ref),(e==null?void 0:e.loop)!=null&&await n.setProperty("loop",e.loop);let r=await this.getDestination(s);if(!r){n.release();return}if((e==null?void 0:e.volume)!=null){let a=await s.invokeAndHold("createGain");if(a){let h=await a.getPropertyAndHold("gain");h&&(await h.setProperty("value",e.volume),h.release()),await n.invoke("connect",a.ref),await a.invoke("connect",r.ref),await n.invoke("start",0),a.release(),n.release();return}}await n.invoke("connect",r.ref),await n.invoke("start",0),n.release();}};c(A,"AudioManager");var X=A;var R=class R{constructor(t){i(this,"keysDown",new Set);i(this,"keysPressed",new Set);i(this,"buttonsPressed",new Set);i(this,"buttonsDown",new Set);i(this,"mouseX",-1);i(this,"mouseY",-1);i(this,"offsetX",-1);i(this,"offsetY",-1);i(this,"ids",[]);i(this,"disposed",false);this.track(OUID.addEventListener("window","keydown",e=>{this.keysDown.has(e.code)||this.keysPressed.add(e.code),this.keysDown.add(e.code);})),this.track(OUID.addEventListener("window","keyup",e=>{this.keysDown.delete(e.code);})),this.track(t.addEventListener("mousedown",e=>{let s=e.button===0?"MouseLeft":"MouseRight";this.buttonsDown.has(s)||this.buttonsPressed.add(s),this.buttonsDown.add(s),this.updateMousePosition(e);})),this.track(t.addEventListener("mouseup",e=>{let s=e.button===0?"MouseLeft":"MouseRight";this.buttonsDown.delete(s),this.updateMousePosition(e);})),this.track(t.addEventListener("mousemove",e=>{this.updateMousePosition(e);})),this.track(t.addEventListener("mouseleave",()=>{this.buttonsDown.clear();}));}update(){this.keysPressed.clear(),this.buttonsPressed.clear();}isDown(t){return this.keysDown.has(t)||this.buttonsDown.has(t)}isPressed(t){return this.keysPressed.has(t)||this.buttonsPressed.has(t)}clean(){this.disposed=true,this.ids.forEach(t=>{OUID.dettachEventListener(t);}),this.ids=[],this.keysDown.clear(),this.keysPressed.clear(),this.buttonsDown.clear(),this.buttonsPressed.clear();}track(t){t.then(e=>{this.disposed?OUID.dettachEventListener(e):this.ids.push(e);}).catch(()=>{});}updateMousePosition(t){var e,s,o,n;this.mouseX=(e=t.x)!=null?e:this.mouseX,this.mouseY=(s=t.y)!=null?s:this.mouseY,this.offsetX=(o=t.offsetX)!=null?o:this.offsetX,this.offsetY=(n=t.offsetY)!=null?n:this.offsetY;}};c(R,"InputManager");var T=R;var $=class ${constructor(t){i(this,"config");i(this,"canvas");i(this,"ctx2d");i(this,"assets");i(this,"audio");i(this,"input");i(this,"world",{});i(this,"running",false);i(this,"initialized",false);i(this,"lastTime",0);i(this,"frameId",null);i(this,"resizeListenerId",null);this.config=t,this.canvas=t.canvas,this.assets=new H,this.audio=new X(this.assets),this.input=new T(this.canvas);}buildContext(t){return {dt:t,canvas:this.canvas,ctx2d:this.ctx2d,world:this.world,assets:this.assets,audio:this.audio,input:this.input}}async updateLocalSize(){var s,o;let t=(s=await this.canvas.width())!=null?s:0,e=(o=await this.canvas.height())!=null?o:0;this.canvas.__local_size__=this.ctx2d.__local_size__={width:t,height:e};}async syncSize(){var a,h;let[t,e,s,o]=await Promise.all([this.canvas.getProperty("clientWidth"),this.canvas.getProperty("clientHeight"),this.canvas.width(),this.canvas.height()]),n=(a=t!=null?t:s)!=null?a:0,r=(h=e!=null?e:o)!=null?h:0;await this.canvas.setProperty("width",n),await this.canvas.setProperty("height",r);}async init(){if(this.initialized)return;let t=await this.canvas.getContext("2d");if(!t)throw new Error("Unable to create a context");this.ctx2d=t,this.resizeListenerId=await this.canvas.addEventListener("resize",()=>{this.syncSize().then(()=>this.updateLocalSize());}),this.config.preload&&(this.config.preload(this.assets),await this.assets.loadAll()),await this.syncSize(),await this.updateLocalSize(),this.config.setup&&this.config.setup(this.buildContext(0)),this.initialized=true;}start(){this.running||(this.running=true,this.lastTime=performance.now(),this.scheduleFrame());}stop(){var t;!this.running&&!this.initialized||(this.running=false,this.frameId!=null&&(OUID.cancelAnimationFrame(this.frameId),this.frameId=null),this.resizeListenerId&&(OUID.dettachEventListener(this.resizeListenerId),this.resizeListenerId=null),this.audio.clean(),this.assets.clean(),this.input.clean(),(t=this.ctx2d)==null||t.release(),this.initialized=false);}scheduleFrame(){OUID.requestAnimationFrame(t=>{this.frameId=null,this.tick(t);}).then(t=>{this.running?this.frameId=t:OUID.cancelAnimationFrame(t);});}async tick(t){var o,n,r,a;if(!this.running)return;let s=Math.min((t-this.lastTime)/1e3,.1);this.lastTime=t;try{let h=this.buildContext(s);(n=(o=this.config).update)==null||n.call(o,h),this.input.update(),await((a=(r=this.config).draw)==null?void 0:a.call(r,h)),await this.ctx2d.commit();}catch(h){console.error("[Toubani] game loop error",h);}finally{this.running&&this.scheduleFrame();}}};c($,"Game");var U=$;function Kt(d){return new U(d)}c(Kt,"tulon");function I(d){var t;return (t=d.__local_size__)!=null?t:{width:0,height:0}}c(I,"objectSize");function v(d,t){return Math.floor(d/t.tileWidth)}c(v,"worldToTileX");function M(d,t){return Math.floor(d/t.tileHeight)}c(M,"worldToTileY");function Ht(d,t){let{image:e,tileWidth:s,tileHeight:o,margin:n=0,spacing:r=0}=t,a=I(e),h=Math.max(1,Math.floor((a.width-2*n+r)/(s+r))),l=d-1,p=l%h,u=Math.floor(l/h),f=n+p*(s+r),m=n+u*(o+r);return {sx:f,sy:m,sw:s,sh:o}}c(Ht,"getTileSourceRect");var N=class N{constructor(t){i(this,"tileset");i(this,"sourceRectCache",new Map);this.tileset=t;}draw(t,e,s,o,n){let{tileWidth:r,tileHeight:a}=e,h=o*s.focusX,l=n*s.focusY,p=s.zoom,u=s.x-h/p,f=s.y-l/p,m=s.x+(o-h)/p,w=s.y+(n-l)/p,g=Math.max(0,Math.floor(u/r)),y=Math.min(e.width-1,Math.floor(m/r)),b=Math.max(0,Math.floor(f/a)),mt=Math.min(e.height-1,Math.floor(w/a));for(let D=b;D<=mt;D++)for(let C=g;C<=y;C++){let _=e.getTile(C,D);if(_<=0)continue;let{sx:xt,sy:yt,sw:vt,sh:Mt}=this.getSourceRect(_),kt=C*r,Pt=D*a;t.drawImage(this.tileset.image,xt,yt,vt,Mt,kt,Pt,r,a);}}getSourceRect(t){let e=this.sourceRectCache.get(t);return e||(e=Ht(t,this.tileset),this.sourceRectCache.set(t,e)),e}};c(N,"TileRenderer");var rt=N;var j=class j{constructor(t,e){i(this,"node");i(this,"settings");i(this,"vx",0);i(this,"vy",0);i(this,"onGround",false);this.node=t,this.settings=e;}update(t){var f,m,w,g,y;let e=this.settings,s=(f=e.gravityX)!=null?f:0,o=(m=e.gravityY)!=null?m:1e3,n=(w=e.maxFallSpeed)!=null?w:2e3,r=(g=e.friction)!=null?g:.8,a=(y=e.airFriction)!=null?y:.98;this.vx+=s*t,this.vy+=o*t,this.vy>n&&(this.vy=n);let h=Math.max(1,Math.min(e.tileMap.tileWidth,e.tileMap.tileHeight)*.5),l=Math.max(Math.abs(this.vx*t),Math.abs(this.vy*t)),p=Math.max(1,Math.ceil(l/h)),u=t/p;for(let b=0;b<p;b++)this.moveAndCollideX(u),this.moveAndCollideY(u);this.onGround?this.vx*=r:this.vx*=a;}moveAndCollideX(t){let e=this.settings.tileMap,s=this.settings.width,o=this.settings.height,n=this.node.x+this.vx*t,r=this.node.y,a=Math.sign(this.vx);if(a===0){this.node.x=n;return}let h=n,l=n+s,p=r,u=r+o,f=Math.max(0,M(p,e)),m=Math.min(e.height-1,M(u-1,e));if(a>0){let w=v(l,e);for(let g=f;g<=m;g++)if(this.isSolid(w,g)){l=w*e.tileWidth,h=l-s,this.vx=0;break}}else {let w=v(h,e);for(let g=f;g<=m;g++)if(this.isSolid(w,g)){h=(w+1)*e.tileWidth,l=h+s,this.vx=0;break}}this.node.x=h;}moveAndCollideY(t){let e=this.settings.tileMap,s=this.settings.width,o=this.settings.height,n=this.node.x,r=this.node.y+this.vy*t,a=Math.sign(this.vy);if(this.onGround=false,a===0){this.node.y=r;return}let h=r,l=r+o,p=n,u=n+s,f=Math.max(0,v(p,e)),m=Math.min(e.width-1,v(u-1,e));if(a>0){let w=M(l,e);for(let g=f;g<=m;g++)if(this.isSolid(g,w)){l=w*e.tileHeight,h=l-o,this.vy=0,this.onGround=true;break}}else {let w=M(h,e);for(let g=f;g<=m;g++)if(this.isSolid(g,w)){h=(w+1)*e.tileHeight,l=h+o,this.vy=0;break}}this.node.y=h;}isSolid(t,e){let s=this.settings.tileMap;return t<0||e<0||t>=s.width||e>=s.height?false:s.isSolid(t,e)}};c(j,"PhysicsBody");var at=j;var F=class F{constructor(){i(this,"x",0);i(this,"y",0);i(this,"zoom",1);i(this,"rotation",0);i(this,"followTarget",null);i(this,"followLerp",.1);i(this,"focusX",.5);i(this,"focusY",.5);}follow(t,e=.1){this.followTarget=t,this.followLerp=e;}unfollow(){this.followTarget=null;}update(t){if(!this.followTarget)return;let e=this.followTarget.x,s=this.followTarget.y;this.x+=(e-this.x)*this.followLerp,this.y+=(s-this.y)*this.followLerp;}begin(t,e,s){t.save();let o=e*this.focusX,n=s*this.focusY;t.translate(o,n),this.rotation!==0&&t.rotate(-this.rotation),this.zoom!==1&&t.scale(this.zoom,this.zoom),t.translate(-this.x,-this.y);}end(t){t.restore();}};c(F,"Camera2D");var ht=F;var J=class J{constructor(){i(this,"x",0);i(this,"y",0);i(this,"rotation",0);i(this,"scaleX",1);i(this,"scaleY",1);i(this,"visible",true);i(this,"parent",null);i(this,"children",[]);}add(t){return t.parent=this,this.children.push(t),t}remove(t){let e=this.children.indexOf(t);e>=0&&(this.children.splice(e,1),t.parent=null);}clear(){for(let t of this.children)t.parent=null;this.children.length=0;}update(t){}drawSelf(t){}draw(t){if(this.visible){t.save(),t.translate(this.x,this.y),this.rotation!==0&&t.rotate(this.rotation),(this.scaleX!==1||this.scaleY!==1)&&t.scale(this.scaleX,this.scaleY),this.drawSelf(t);for(let e of this.children)e.draw(t);t.restore();}}updateTree(t){this.update(t);for(let e of this.children)e.updateTree(t);}getWorldPosition(){let t=this.x,e=this.y,s=this.parent;for(;s;)t+=s.x,e+=s.y,s=s.parent;return {x:t,y:e}}};c(J,"SceneNode");var P=J,q=class q extends P{constructor(e){super();i(this,"sprite");this.sprite=e;}update(e){this.sprite.update(e);}drawSelf(e){this.sprite.draw(e);}getWorldBounds(){let e=this.getWorldPosition();return {x:e.x,y:e.y,width:this.sprite.width*this.scaleX,height:this.sprite.height*this.scaleY}}};c(q,"SpriteNode");var ct=q,K=class K extends P{constructor(e=1,s=1){super();i(this,"parallaxX",1);i(this,"parallaxY",1);this.parallaxX=e,this.parallaxY=s;}};c(K,"SceneLayer");var G=K,k=class k{constructor(){i(this,"id");i(this,"root",new P);i(this,"layers",[]);this.id=k.id++;}addLayer(t=1,e=1){let s=new G(t,e);return this.layers.push(s),this.root.add(s),s}update(t){this.root.updateTree(t);}draw(t){this.root.draw(t);}};c(k,"Scene"),i(k,"id",0);var lt=k;function dt(d,t){return !(d.x+d.width<=t.x||d.x>=t.x+t.width||d.y+d.height<=t.y||d.y>=t.y+t.height)}c(dt,"aabbOverlap");var Q=class Q{constructor(){i(this,"colliders",[]);i(this,"rules",[]);i(this,"rulesByPair",new Map);}add(t){this.colliders.push(t);}remove(t){let e=this.colliders.indexOf(t);e>=0&&this.colliders.splice(e,1);}clear(){this.colliders.length=0;}onCollision(t,e,s){var a;let o={tagA:t,tagB:e,handler:s};this.rules.push(o);let n=this.ruleKey(t,e),r=(a=this.rulesByPair.get(n))!=null?a:[];r.push(o),this.rulesByPair.set(n,r);}update(){var o;let t=new Map,e=new Map;for(let n of this.colliders){if(!n.isActive)continue;let r=(o=t.get(n.tag))!=null?o:[];r.push(n),t.set(n.tag,r);}let s=c(n=>{let r=e.get(n);return r||(r=n.getBounds(),e.set(n,r)),r},"getBounds");for(let[n,r]of this.rulesByPair.entries()){let[a,h]=n.split("\0"),l=t.get(a),p=t.get(h);if(!(!l||!p))if(a===h)for(let u=0;u<l.length;u++){let f=l[u],m=s(f);for(let w=u+1;w<l.length;w++){let g=l[w];dt(m,s(g))&&this.triggerRules(f,g,r);}}else for(let u of l){let f=s(u);for(let m of p)dt(f,s(m))&&this.triggerRules(u,m,r);}}}ruleKey(t,e){return t<=e?`${t}\0${e}`:`${e}\0${t}`}triggerRules(t,e,s){for(let o of s){let n=o.tagA===t.tag&&o.tagB===e.tag,r=o.tagA===e.tag&&o.tagB===t.tag;n?o.handler(t,e):r&&o.handler(e,t);}}};c(Q,"CollisionSystem");var ut=Q,Xt=0;function he(d,t,e,s){return {id:`col-${Xt++}`,tag:d,isActive:true,data:{node:t,body:e,sprite:s},getBounds(){let n=t.getWorldPosition(),r=e.settings.width,a=e.settings.height;return {x:n.x,y:n.y,width:r,height:a}}}}c(he,"createSpritePhysicsCollider");var x=class x{constructor(){i(this,"opened",false);i(this,"socket",null);i(this,"msgHandlers",[]);i(this,"openHandlers",[]);i(this,"closeHandlers",[]);i(this,"errorHandlers",[]);i(this,"ids",[]);}async connect(t){if(this.socket){let s=await this.socket.getProperty("readyState");if(s===x.OPEN||s===x.CONNECTING)return;await this.cleanupSocket(false);}let e=await OUID$1.newObject("WebSocket",t);if(!e)throw new Error(`Unable to create WebSocket for ${t}`);this.socket=ODOM.OObject.toInstance(e),this.ids.push(await this.socket.addEventListener("open",()=>{this.opened=true,this.openHandlers.forEach(s=>s());})),this.ids.push(await this.socket.addEventListener("message",s=>{let o=typeof s.data=="string"?s.data:"";this.msgHandlers.forEach(n=>n(o));})),this.ids.push(await this.socket.addEventListener("close",s=>{this.opened=false,this.closeHandlers.forEach(o=>o(s));})),this.ids.push(await this.socket.addEventListener("error",s=>{this.errorHandlers.forEach(o=>o(s));}));try{await new Promise((s,o)=>{let n=c(()=>{h(),s();},"onOpen"),r=c(l=>{h(),o(l);},"onError"),a=c(l=>{h(),o(l);},"onClose"),h=c(()=>{this.offOpen(n),this.offError(r),this.offClose(a);},"cleanup");this.onOpen(n),this.onError(r),this.onClose(a);});}catch(s){throw await this.cleanupSocket(false),s}}async send(t){this.socket&&await this.socket.getProperty("readyState")===x.OPEN&&await this.socket.invoke("send",t);}async close(t,e){await this.cleanupSocket(true,t,e);}isConnected(){return this.opened}onMessage(t){this.msgHandlers.push(t);}onOpen(t){this.openHandlers.push(t);}offOpen(t){this.openHandlers=this.openHandlers.filter(e=>e!==t);}onClose(t){this.closeHandlers.push(t);}offClose(t){this.closeHandlers=this.closeHandlers.filter(e=>e!==t);}onError(t){this.errorHandlers.push(t);}offError(t){this.errorHandlers=this.errorHandlers.filter(e=>e!==t);}async cleanupSocket(t,e,s){let o=this.socket;this.socket=null,this.opened=false;let n=this.ids.splice(0);if(await Promise.all(n.map(r=>OUID$1.dettachEventListener(r).catch(()=>{}))),!!o)try{if(t){let r=await o.getProperty("readyState").catch(()=>null);(r===x.OPEN||r===x.CONNECTING)&&await o.invoke("close",e,s);}}finally{await o.release().catch(()=>{});}}};c(x,"WebSocketTransport"),i(x,"OPEN",1),i(x,"CONNECTING",0);var pt=x,V=class V{constructor(t){i(this,"transport");i(this,"handlers",new Map);i(this,"rpcResolvers",new Map);i(this,"rpcCounter",0);i(this,"handleRawMessage",c(t=>{if(!t)return;let e;try{e=JSON.parse(t);}catch(o){return}if(e.payload&&typeof e.payload=="object"&&"rpcId"in e.payload){let o=e.payload.rpcId,n=this.rpcResolvers.get(o);n&&(n.resolve(e.payload),this.rpcResolvers.delete(o));}let s=this.handlers.get(e.type);if(s)for(let o of s)o(e);},"handleRawMessage"));this.transport=t,this.transport.onMessage(this.handleRawMessage);}async connect(t){await this.transport.connect(t);}isConnected(){return this.transport.isConnected()}send(t,e){let s={type:t,payload:e};this.transport.send(JSON.stringify(s));}on(t,e){var o;let s=(o=this.handlers.get(t))!=null?o:[];s.push(e),this.handlers.set(t,s);}off(t,e){let s=this.handlers.get(t);s&&this.handlers.set(t,s.filter(o=>o!==e));}call(t,e){let s=`rpc_${++this.rpcCounter}_${Date.now()}`,o=e&&typeof e=="object"?it(st({},e),{rpcId:s}):{value:e,rpcId:s},n={type:t,payload:o};return new Promise((r,a)=>{this.rpcResolvers.set(s,{resolve:r,reject:a}),this.transport.send(JSON.stringify(n)).catch(h=>{this.rpcResolvers.delete(s),a(h);});})}close(){return this.rpcResolvers.forEach(({reject:t})=>t(new Error("Network client closed"))),this.rpcResolvers.clear(),this.transport.close()}};c(V,"NetworkClient");var gt=V,Z=class Z{constructor(t,e,s){i(this,"net");i(this,"channels");i(this,"state");i(this,"onChangeHandlers",[]);var o;this.net=t,this.channels=s,this.state=e,this.net.on((o=s==null?void 0:s.update)!=null?o:"state_update",n=>{let{payload:r}=n;r.full?this.state=r.full:r.patch&&(this.state=Object.assign({},this.state,r.patch)),this.emitChange();});}getState(){return this.state}onChange(t){this.onChangeHandlers.push(t);}emitChange(){for(let t of this.onChangeHandlers)t(this.state);}sendPatch(t){var e,s;this.net.send((s=(e=this.channels)==null?void 0:e.patch)!=null?s:"state_patch",t);}};c(Z,"StateSyncClient");var wt=Z;export{H as AssetManager,X as AudioManager,ht as Camera2D,ut as CollisionSystem,U as Game,T as InputManager,gt as NetworkClient,at as PhysicsBody,lt as Scene,G as SceneLayer,P as SceneNode,ot as Sprite,O as SpriteAnimation,nt as SpriteLayer,ct as SpriteNode,E as SpriteSheet,wt as StateSyncClient,rt as TileRenderer,pt as WebSocketTransport,dt as aabbOverlap,he as createSpritePhysicsCollider,Ht as getTileSourceRect,I as objectSize,Kt as tulon,v as worldToTileX,M as worldToTileY};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ouidesigner/toubani",
3
+ "version": "0.0.7",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "clean": "rm -rf dist",
8
+ "build": "npm run clean;tsup",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "type": "commonjs",
15
+ "peerDependencies": {
16
+ "ouider": "^0.1.9"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^24.10.1",
20
+ "tsup": "^8.5.1"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.mjs",
25
+ "require": "./dist/index.js"
26
+ }
27
+ },
28
+ "types": "dist/index.d.ts",
29
+ "files": [
30
+ "/dist"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }