@simpludify/web 0.1.6 → 0.1.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 CHANGED
@@ -1,11 +1,11 @@
1
1
  # @simpludify/web
2
2
 
3
- Experimental v0.1.6 browser SDK for Simpludify, a code-first TypeScript 2D engine. APIs may change between 0.x releases. Includes Game, PixiJS rendering, browser input and Canvas/DOM UI adapters; re-exports the host-independent Core API. Core and PixiJS are installed as runtime dependencies, not bundled into this package.
3
+ Experimental v0.1.7 browser SDK for Simpludify, a code-first TypeScript 2D engine. APIs may change between 0.x releases. Includes Game, PixiJS rendering, browser input and Canvas/DOM UI adapters; re-exports the host-independent Core API. Core and PixiJS are installed as runtime dependencies, not bundled into this package.
4
4
 
5
- Registry installation (check availability with `npm view @simpludify/web@0.1.6 version`):
5
+ Registry installation (check availability with `npm view @simpludify/web@0.1.7 version`):
6
6
 
7
7
  ```sh
8
- npm install @simpludify/web@0.1.6
8
+ npm install @simpludify/web@0.1.7
9
9
  npm install --save-dev vite typescript
10
10
  ```
11
11
 
package/dist/game.d.ts CHANGED
@@ -89,7 +89,7 @@ export declare class Game<SceneId extends string = string> {
89
89
  private height;
90
90
  private ratio;
91
91
  private readonly scenes;
92
- private readonly pendingScenes;
92
+ private pendingScene;
93
93
  private readonly defaultUI;
94
94
  private ui;
95
95
  private prepared;
@@ -127,7 +127,7 @@ export declare class Game<SceneId extends string = string> {
127
127
  scene(id: SceneId, setup: SceneSetup): this;
128
128
  private wait;
129
129
  run(initialScene?: SceneId): Promise<void>;
130
- goTo(id: SceneId): Promise<void>;
130
+ changeScene(id: SceneId): Promise<void>;
131
131
  private navigate;
132
132
  private present;
133
133
  get viewport(): Viewport;
@@ -24,7 +24,7 @@ export declare class GeometryTilemapVisual {
24
24
  constructor(map: GeometryTilemap, released: () => void);
25
25
  private remove;
26
26
  private cell;
27
- render(view: ViewTransform): void;
27
+ render(view: ViewTransform, enabled?: boolean): void;
28
28
  private bulk;
29
29
  dispose(): void;
30
30
  }
package/dist/index.js CHANGED
@@ -703,6 +703,7 @@ var BrowserPointer = class {
703
703
  }
704
704
  this.flush();
705
705
  const data = this.data(e);
706
+ if (!data) return this.release();
706
707
  try {
707
708
  host.setPointerCapture(e.pointerId);
708
709
  } catch {}
@@ -720,7 +721,9 @@ var BrowserPointer = class {
720
721
  host.addEventListener("wheel", (e) => this.guard(() => {
721
722
  if (!this.surface(e.target)) return;
722
723
  this.flush();
723
- const { screen } = this.project(e);
724
+ const projected = this.project(e);
725
+ if (!projected) return;
726
+ const { screen } = projected;
724
727
  const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? host.clientHeight : 1;
725
728
  if (this.routing.scroll(screen.x, screen.y, e.deltaX * unit, e.deltaY * unit)) e.preventDefault();
726
729
  }), {
@@ -730,19 +733,27 @@ var BrowserPointer = class {
730
733
  window.addEventListener("pointermove", (e) => this.guard(() => {
731
734
  if (!this.routing.owns(e.pointerId) && !this.surface(e.target)) {
732
735
  if (e.pointerType === "mouse") this.releaseMouse(e.pointerId);
733
- if (this.routing.hasHover(e.pointerId)) this.routing.leave(this.data(e));
736
+ if (this.routing.hasHover(e.pointerId)) {
737
+ const data = this.data(e);
738
+ if (data) this.routing.leave(data);
739
+ else this.release();
740
+ }
734
741
  return;
735
742
  }
736
743
  this.flush();
744
+ const data = this.data(e);
745
+ if (!data) return this.release();
737
746
  this.updateMouse(e);
738
- this.routing.move(this.data(e));
747
+ this.routing.move(data);
739
748
  }), options);
740
749
  window.addEventListener("pointerup", (e) => this.guard(() => {
741
750
  if (e.pointerType === "mouse") this.releaseMouse(e.pointerId);
742
751
  if (this.routing.owns(e.pointerId)) {
743
752
  e.preventDefault();
744
753
  this.flush();
745
- this.routing.up(this.data(e));
754
+ const data = this.data(e);
755
+ if (data) this.routing.up(data);
756
+ else this.release();
746
757
  }
747
758
  }), options);
748
759
  window.addEventListener("pointercancel", (e) => this.guard(() => {
@@ -756,7 +767,11 @@ var BrowserPointer = class {
756
767
  else this.blockedMouse.delete(e.pointerId);
757
768
  this.routing.cancel(e.pointerId);
758
769
  }), options);
759
- host.addEventListener("pointerleave", (e) => this.guard(() => this.routing.leave(this.data(e))), options);
770
+ host.addEventListener("pointerleave", (e) => this.guard(() => {
771
+ const data = this.data(e);
772
+ if (data) this.routing.leave(data);
773
+ else this.release();
774
+ }), options);
760
775
  window.addEventListener("blur", (event) => {
761
776
  if (event.target === window) this.release();
762
777
  }, options);
@@ -806,7 +821,9 @@ var BrowserPointer = class {
806
821
  return target === this.host || target instanceof HTMLCanvasElement && target.parentElement === this.host;
807
822
  }
808
823
  data(e) {
809
- const { screen, world, view } = this.project(e);
824
+ const projected = this.project(e);
825
+ if (!projected) return void 0;
826
+ const { screen, world, view } = projected;
810
827
  return Object.freeze({
811
828
  pointerId: e.pointerId,
812
829
  pointerType: e.pointerType,
@@ -1274,6 +1291,7 @@ var CanvasVisual = class {
1274
1291
  },
1275
1292
  wheel: (event) => {
1276
1293
  const rect = this.host.getBoundingClientRect();
1294
+ if (rect.width <= 0 || rect.height <= 0) return;
1277
1295
  const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? this.host.clientHeight : 1;
1278
1296
  if (this.routing.scroll((event.clientX - rect.x) * this.host.clientWidth / rect.width, (event.clientY - rect.y) * this.host.clientHeight / rect.height, event.deltaX * unit, event.deltaY * unit)) event.preventDefault();
1279
1297
  }
@@ -1747,11 +1765,11 @@ var GeometryTilemapVisual = class {
1747
1765
  g.clear().poly(this.map.geometry.vertices(x, y).flatMap((p) => [p.x, p.y]), true).fill(color);
1748
1766
  this.stats.updatedTiles++;
1749
1767
  }
1750
- render(view) {
1768
+ render(view, enabled = true) {
1751
1769
  if (this.disposed) return;
1752
1770
  const m = this.map;
1753
1771
  this.display.position.set(m.x, m.y);
1754
- this.display.visible = m.visible && m.enabled;
1772
+ this.display.visible = enabled && m.visible && m.enabled;
1755
1773
  if (!this.display.visible) {
1756
1774
  this.invalid = true;
1757
1775
  this.bulk = true;
@@ -1986,7 +2004,7 @@ var TilemapVisual = class {
1986
2004
  invalid = true;
1987
2005
  off;
1988
2006
  offDispose;
1989
- constructor(map, scene, acquire, changed, onError, released) {
2007
+ constructor(map, isCurrent, acquire, changed, onError, released) {
1990
2008
  this.map = map;
1991
2009
  this.changed = changed;
1992
2010
  this.onError = onError;
@@ -2004,7 +2022,7 @@ var TilemapVisual = class {
2004
2022
  const controller = this.controller;
2005
2023
  if (!controller) throw Error("Missing tilemap request controller");
2006
2024
  acquire(map.sheet.url, controller.signal).then((lease) => {
2007
- if (this.disposed || map.disposed || map.scene !== scene) {
2025
+ if (this.disposed || map.disposed || !isCurrent()) {
2008
2026
  lease.release();
2009
2027
  return;
2010
2028
  }
@@ -2066,12 +2084,12 @@ var TilemapVisual = class {
2066
2084
  sprite.height = this.map.tileHeight;
2067
2085
  this.stats.updatedTiles++;
2068
2086
  }
2069
- render(view) {
2087
+ render(view, enabled = true) {
2070
2088
  if (this.disposed) return;
2071
2089
  this.sync();
2072
2090
  const m = this.map;
2073
2091
  this.display.position.set(m.x, m.y);
2074
- this.display.visible = m.visible && m.enabled;
2092
+ this.display.visible = enabled && m.visible && m.enabled;
2075
2093
  if (!this.display.visible) {
2076
2094
  this.invalid = true;
2077
2095
  return;
@@ -2471,6 +2489,7 @@ var PixiRenderer = class {
2471
2489
  root.mask = mask;
2472
2490
  const visuals = /* @__PURE__ */ new Map();
2473
2491
  const tilemaps = /* @__PURE__ */ new Map();
2492
+ const geometryTilemaps = /* @__PURE__ */ new Map();
2474
2493
  const drawOrder = [];
2475
2494
  let depthDirty = true;
2476
2495
  let revision = -1, disposed = false, clipX = NaN, clipY = NaN, clipWidth = NaN, clipHeight = NaN, removeSceneCleanup;
@@ -2670,20 +2689,30 @@ var PixiRenderer = class {
2670
2689
  entry.dispose();
2671
2690
  visuals.delete(actor);
2672
2691
  }
2673
- for (const [actor, entry] of tilemaps) if (actor.disposed || actor.scene !== scene || !(entry instanceof TilemapVisual ? actor.get(TilemapLayer.type) : actor.get(GeometryTilemapLayer.type)) || entry.disposed) {
2674
- entry.dispose();
2675
- if (entry instanceof TilemapVisual) this.tilemaps.delete(entry);
2676
- else this.geometryTilemaps.delete(entry);
2677
- tilemaps.delete(actor);
2692
+ for (const [actor, entry] of tilemaps) {
2693
+ const layer = actor.get(TilemapLayer.type);
2694
+ if (actor.disposed || actor.scene !== scene || !layer || layer.disposed || layer.map !== entry.map || entry.disposed) {
2695
+ entry.dispose();
2696
+ this.tilemaps.delete(entry);
2697
+ tilemaps.delete(actor);
2698
+ }
2699
+ }
2700
+ for (const [actor, entry] of geometryTilemaps) {
2701
+ const layer = actor.get(GeometryTilemapLayer.type);
2702
+ if (actor.disposed || actor.scene !== scene || !layer || layer.disposed || layer.map !== entry.map || entry.disposed) {
2703
+ entry.dispose();
2704
+ this.geometryTilemaps.delete(entry);
2705
+ geometryTilemaps.delete(actor);
2706
+ }
2678
2707
  }
2679
2708
  let order = 0;
2680
2709
  for (const actor of scene.actors) {
2681
2710
  const layer = actor.get(TilemapLayer.type);
2682
- if (layer) {
2711
+ if (layer && !layer.disposed && !layer.map.disposed) {
2683
2712
  let tile = tilemaps.get(actor);
2684
2713
  if (!tile) {
2685
- tile = new TilemapVisual(layer.map, scene, (url, signal) => this.acquireImage(url, signal), changed, this.onError, () => {
2686
- if (tile instanceof TilemapVisual) this.tilemaps.delete(tile);
2714
+ tile = new TilemapVisual(layer.map, () => actor.scene === scene && actor.get(TilemapLayer.type)?.map === layer.map, (url, signal) => this.acquireImage(url, signal), changed, this.onError, () => {
2715
+ if (tile) this.tilemaps.delete(tile);
2687
2716
  depthDirty = true;
2688
2717
  });
2689
2718
  world.addChild(tile.display);
@@ -2693,8 +2722,8 @@ var PixiRenderer = class {
2693
2722
  tile.order = order;
2694
2723
  }
2695
2724
  const geometryLayer = actor.get(GeometryTilemapLayer.type);
2696
- if (geometryLayer) {
2697
- let tile = tilemaps.get(actor);
2725
+ if (geometryLayer && !geometryLayer.disposed && !geometryLayer.map.disposed) {
2726
+ let tile = geometryTilemaps.get(actor);
2698
2727
  if (!tile) {
2699
2728
  const created = new GeometryTilemapVisual(geometryLayer.map, () => {
2700
2729
  this.geometryTilemaps.delete(created);
@@ -2702,7 +2731,7 @@ var PixiRenderer = class {
2702
2731
  });
2703
2732
  tile = created;
2704
2733
  world.addChild(tile.display);
2705
- tilemaps.set(actor, tile);
2734
+ geometryTilemaps.set(actor, tile);
2706
2735
  this.geometryTilemaps.add(created);
2707
2736
  }
2708
2737
  tile.order = order;
@@ -2722,7 +2751,7 @@ var PixiRenderer = class {
2722
2751
  const sync = () => {
2723
2752
  reconcile();
2724
2753
  for (const entry of visuals.values()) update(entry);
2725
- for (const entry of tilemaps.values()) if (entry instanceof TilemapVisual) entry.sync();
2754
+ for (const entry of tilemaps.values()) entry.sync();
2726
2755
  };
2727
2756
  const prepared = {
2728
2757
  ready: async () => {
@@ -2735,7 +2764,6 @@ var PixiRenderer = class {
2735
2764
  if (entry.controller) pending = true;
2736
2765
  }
2737
2766
  for (const entry of tilemaps.values()) {
2738
- if (!(entry instanceof TilemapVisual)) continue;
2739
2767
  if (entry.error) throw entry.error;
2740
2768
  if (entry.controller) pending = true;
2741
2769
  }
@@ -2777,8 +2805,15 @@ var PixiRenderer = class {
2777
2805
  entry.display.scale.set(scaleX, scaleY);
2778
2806
  }
2779
2807
  }
2780
- for (const entry of tilemaps.values()) {
2781
- entry.render(view);
2808
+ for (const [actor, entry] of tilemaps) {
2809
+ entry.render(view, actor.enabled && actor.get(TilemapLayer.type)?.enabled === true);
2810
+ if (entry.depth !== entry.map.zIndex) {
2811
+ entry.depth = entry.map.zIndex;
2812
+ depthDirty = true;
2813
+ }
2814
+ }
2815
+ for (const [actor, entry] of geometryTilemaps) {
2816
+ entry.render(view, actor.enabled && actor.get(GeometryTilemapLayer.type)?.enabled === true);
2782
2817
  if (entry.depth !== entry.map.zIndex) {
2783
2818
  entry.depth = entry.map.zIndex;
2784
2819
  depthDirty = true;
@@ -2796,6 +2831,11 @@ var PixiRenderer = class {
2796
2831
  order: entry.order,
2797
2832
  zIndex: entry.map.zIndex
2798
2833
  });
2834
+ for (const entry of geometryTilemaps.values()) drawOrder.push({
2835
+ display: entry.display,
2836
+ order: entry.order,
2837
+ zIndex: entry.map.zIndex
2838
+ });
2799
2839
  drawOrder.sort((a, b) => a.zIndex - b.zIndex || a.order - b.order);
2800
2840
  for (let i = 0; i < drawOrder.length; i++) {
2801
2841
  const entry = drawOrder[i];
@@ -2820,10 +2860,14 @@ var PixiRenderer = class {
2820
2860
  () => {
2821
2861
  for (const entry of tilemaps.values()) {
2822
2862
  entry.dispose();
2823
- if (entry instanceof TilemapVisual) this.tilemaps.delete(entry);
2824
- else this.geometryTilemaps.delete(entry);
2863
+ this.tilemaps.delete(entry);
2825
2864
  }
2826
2865
  tilemaps.clear();
2866
+ for (const entry of geometryTilemaps.values()) {
2867
+ entry.dispose();
2868
+ this.geometryTilemaps.delete(entry);
2869
+ }
2870
+ geometryTilemaps.clear();
2827
2871
  },
2828
2872
  () => root.destroy({
2829
2873
  children: true,
@@ -3201,7 +3245,7 @@ var Game = class {
3201
3245
  height = 450;
3202
3246
  ratio = 1;
3203
3247
  scenes = /* @__PURE__ */ new Map();
3204
- pendingScenes = /* @__PURE__ */ new Map();
3248
+ pendingScene;
3205
3249
  defaultUI = [];
3206
3250
  ui;
3207
3251
  prepared;
@@ -3278,7 +3322,7 @@ var Game = class {
3278
3322
  });
3279
3323
  this.pointers = new BrowserPointer(this.element, (event) => {
3280
3324
  const rect = this.element.getBoundingClientRect();
3281
- if (rect.width <= 0 || rect.height <= 0) throw new EngineError("VIEWPORT_UNAVAILABLE", "Game has no visible bounds");
3325
+ if (rect.width <= 0 || rect.height <= 0) return;
3282
3326
  const screen = screenPoint((event.clientX - rect.left) * this.width / rect.width, (event.clientY - rect.top) * this.height / rect.height);
3283
3327
  return {
3284
3328
  screen,
@@ -3384,12 +3428,17 @@ var Game = class {
3384
3428
  }
3385
3429
  scene(id, setup) {
3386
3430
  this.assertAlive();
3431
+ if (typeof id !== "string" || !id.length) throw new EngineError("INVALID_VALUE", "Scene id must not be empty");
3432
+ if (typeof setup !== "function") throw new EngineError("INVALID_VALUE", "Scene setup must be a function");
3387
3433
  if (this.scenes.has(id)) throw new EngineError("DUPLICATE_SCENE", "Scene id is already registered", { sceneId: id });
3388
3434
  this.scenes.set(id, setup);
3389
3435
  return this;
3390
3436
  }
3391
3437
  wait(promise) {
3392
- if (this.dead) return Promise.reject(new EngineError("GAME_DISPOSED", "Game has been disposed"));
3438
+ if (this.dead) {
3439
+ promise.catch(() => {});
3440
+ return Promise.reject(new EngineError("GAME_DISPOSED", "Game has been disposed"));
3441
+ }
3393
3442
  const signal = this.shutdown.signal;
3394
3443
  return new Promise((resolve, reject) => {
3395
3444
  const abort = () => {
@@ -3425,7 +3474,7 @@ var Game = class {
3425
3474
  })();
3426
3475
  return this.startPromise;
3427
3476
  }
3428
- goTo(id) {
3477
+ changeScene(id) {
3429
3478
  if (this.dead) return Promise.reject(new EngineError("GAME_DISPOSED", "Game has been disposed"));
3430
3479
  if (!this.ready) return Promise.reject(new EngineError("GAME_NOT_READY", "Await run before navigating"));
3431
3480
  return this.navigate(id);
@@ -3433,8 +3482,7 @@ var Game = class {
3433
3482
  navigate(id) {
3434
3483
  const setup = this.scenes.get(id);
3435
3484
  if (!setup) return Promise.reject(new EngineError("SCENE_NOT_FOUND", "Unknown scene", { sceneId: id }));
3436
- const existing = this.pendingScenes.get(id);
3437
- if (existing) return existing;
3485
+ if (this.pendingScene?.id === id) return this.pendingScene.task;
3438
3486
  const task = this.queue.then(async () => {
3439
3487
  this.assertAlive();
3440
3488
  const scene = new Scene(id), nodes = [], controller = new AbortController();
@@ -3476,7 +3524,9 @@ var Game = class {
3476
3524
  onDispose: (callback) => scene.onDispose(callback)
3477
3525
  })));
3478
3526
  this.assertAlive();
3479
- await this.present(scene, nodes, canvas);
3527
+ await this.present(scene, nodes, canvas, () => {
3528
+ if (this.pendingScene?.task === task) this.pendingScene = void 0;
3529
+ });
3480
3530
  } catch (error) {
3481
3531
  try {
3482
3532
  scene.dispose();
@@ -3488,13 +3538,16 @@ var Game = class {
3488
3538
  if (this.staging === scene) this.staging = void 0;
3489
3539
  }
3490
3540
  }).finally(() => {
3491
- if (this.pendingScenes.get(id) === task) this.pendingScenes.delete(id);
3541
+ if (this.pendingScene?.task === task) this.pendingScene = void 0;
3492
3542
  });
3493
- this.pendingScenes.set(id, task);
3543
+ this.pendingScene = {
3544
+ id,
3545
+ task
3546
+ };
3494
3547
  this.queue = task.catch(() => {});
3495
3548
  return task;
3496
3549
  }
3497
- async present(scene, nodes, canvas) {
3550
+ async present(scene, nodes, canvas, beforeActivate) {
3498
3551
  let prepared, ui, committed = false;
3499
3552
  let canvasPresentation;
3500
3553
  let virtualPresentation;
@@ -3520,6 +3573,7 @@ var Game = class {
3520
3573
  }
3521
3574
  virtualPresentation = this.prepareVirtual(scene);
3522
3575
  scene.markReady();
3576
+ beforeActivate?.();
3523
3577
  this.errorBox.hidden = true;
3524
3578
  try {
3525
3579
  if (this.playback && this.playbackScene !== scene) this.stopReplay();
@@ -3535,18 +3589,21 @@ var Game = class {
3535
3589
  this.canvas = canvas;
3536
3590
  this.canvasPresentation = canvasPresentation;
3537
3591
  this.virtualPresentation = virtualPresentation;
3538
- this.pointers.routing.setScene(this.channelFor(scene), canvas);
3539
3592
  committed = true;
3540
- this.element.append(ui.element);
3541
- for (const cleanup of [
3542
- () => oldPrepared?.dispose(),
3543
- () => oldUI?.dispose(),
3544
- () => oldCanvas?.dispose(),
3545
- () => oldVirtual?.dispose()
3546
- ]) try {
3547
- cleanup();
3548
- } catch (error) {
3549
- this.report(error);
3593
+ try {
3594
+ this.pointers.routing.setScene(this.channelFor(scene), canvas);
3595
+ this.element.append(ui.element);
3596
+ } finally {
3597
+ for (const cleanup of [
3598
+ () => oldPrepared?.dispose(),
3599
+ () => oldUI?.dispose(),
3600
+ () => oldCanvas?.dispose(),
3601
+ () => oldVirtual?.dispose()
3602
+ ]) try {
3603
+ cleanup();
3604
+ } catch (error) {
3605
+ this.report(error);
3606
+ }
3550
3607
  }
3551
3608
  this.assertAlive();
3552
3609
  this.previousTime = void 0;
@@ -3791,7 +3848,7 @@ var Game = class {
3791
3848
  this.defaultCanvas = void 0;
3792
3849
  this.defaultUI.length = 0;
3793
3850
  this.scenes.clear();
3794
- this.pendingScenes.clear();
3851
+ this.pendingScene = void 0;
3795
3852
  }
3796
3853
  };
3797
3854
  function validFontFamily(value) {