@codexo/exojs-tilemap 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +33 -26
  2. package/dist/esm/ChunkSource.d.ts +33 -0
  3. package/dist/esm/ChunkSource.d.ts.map +1 -0
  4. package/dist/esm/ChunkStreamer.d.ts +98 -0
  5. package/dist/esm/ChunkStreamer.d.ts.map +1 -0
  6. package/dist/esm/ChunkStreamer.js +201 -0
  7. package/dist/esm/ChunkStreamer.js.map +1 -0
  8. package/dist/esm/ImageLayer.d.ts +11 -3
  9. package/dist/esm/ImageLayer.d.ts.map +1 -0
  10. package/dist/esm/ImageLayer.js +71 -64
  11. package/dist/esm/ImageLayer.js.map +1 -1
  12. package/dist/esm/ImageLayerNode.d.ts +89 -0
  13. package/dist/esm/ImageLayerNode.d.ts.map +1 -0
  14. package/dist/esm/ImageLayerNode.js +205 -0
  15. package/dist/esm/ImageLayerNode.js.map +1 -0
  16. package/dist/esm/MapObject.d.ts +70 -0
  17. package/dist/esm/MapObject.d.ts.map +1 -0
  18. package/dist/esm/MapObject.js +34 -0
  19. package/dist/esm/MapObject.js.map +1 -0
  20. package/dist/esm/MapObjectSpawner.d.ts +133 -0
  21. package/dist/esm/MapObjectSpawner.d.ts.map +1 -0
  22. package/dist/esm/MapObjectSpawner.js +147 -0
  23. package/dist/esm/MapObjectSpawner.js.map +1 -0
  24. package/dist/esm/MapSpawnSession.d.ts +53 -0
  25. package/dist/esm/MapSpawnSession.d.ts.map +1 -0
  26. package/dist/esm/MapSpawnSession.js +88 -0
  27. package/dist/esm/MapSpawnSession.js.map +1 -0
  28. package/dist/esm/MapWorld.d.ts +134 -0
  29. package/dist/esm/MapWorld.d.ts.map +1 -0
  30. package/dist/esm/MapWorld.js +150 -0
  31. package/dist/esm/MapWorld.js.map +1 -0
  32. package/dist/esm/MapWorldRuntime.d.ts +182 -0
  33. package/dist/esm/MapWorldRuntime.d.ts.map +1 -0
  34. package/dist/esm/MapWorldRuntime.js +261 -0
  35. package/dist/esm/MapWorldRuntime.js.map +1 -0
  36. package/dist/esm/ObjectLayer.d.ts +93 -7
  37. package/dist/esm/ObjectLayer.d.ts.map +1 -0
  38. package/dist/esm/ObjectLayer.js +184 -167
  39. package/dist/esm/ObjectLayer.js.map +1 -1
  40. package/dist/esm/SampledChunkSource.d.ts +43 -0
  41. package/dist/esm/SampledChunkSource.d.ts.map +1 -0
  42. package/dist/esm/SampledChunkSource.js +48 -0
  43. package/dist/esm/SampledChunkSource.js.map +1 -0
  44. package/dist/esm/TileAnimator.d.ts +12 -2
  45. package/dist/esm/TileAnimator.d.ts.map +1 -0
  46. package/dist/esm/TileAnimator.js +154 -165
  47. package/dist/esm/TileAnimator.js.map +1 -1
  48. package/dist/esm/TileChunk.d.ts +30 -5
  49. package/dist/esm/TileChunk.d.ts.map +1 -0
  50. package/dist/esm/TileChunk.js +190 -188
  51. package/dist/esm/TileChunk.js.map +1 -1
  52. package/dist/esm/TileChunkNode.d.ts +18 -3
  53. package/dist/esm/TileChunkNode.d.ts.map +1 -0
  54. package/dist/esm/TileChunkNode.js +104 -85
  55. package/dist/esm/TileChunkNode.js.map +1 -1
  56. package/dist/esm/TileLayer.d.ts +104 -24
  57. package/dist/esm/TileLayer.d.ts.map +1 -0
  58. package/dist/esm/TileLayer.js +511 -469
  59. package/dist/esm/TileLayer.js.map +1 -1
  60. package/dist/esm/TileLayerNode.d.ts +52 -11
  61. package/dist/esm/TileLayerNode.d.ts.map +1 -0
  62. package/dist/esm/TileLayerNode.js +224 -166
  63. package/dist/esm/TileLayerNode.js.map +1 -1
  64. package/dist/esm/TileMap.d.ts +96 -31
  65. package/dist/esm/TileMap.d.ts.map +1 -0
  66. package/dist/esm/TileMap.js +401 -357
  67. package/dist/esm/TileMap.js.map +1 -1
  68. package/dist/esm/TileMapBand.d.ts +42 -29
  69. package/dist/esm/TileMapBand.d.ts.map +1 -0
  70. package/dist/esm/TileMapBand.js +151 -158
  71. package/dist/esm/TileMapBand.js.map +1 -1
  72. package/dist/esm/TileMapNode.d.ts +34 -18
  73. package/dist/esm/TileMapNode.d.ts.map +1 -0
  74. package/dist/esm/TileMapNode.js +122 -109
  75. package/dist/esm/TileMapNode.js.map +1 -1
  76. package/dist/esm/TileMapView.d.ts +108 -47
  77. package/dist/esm/TileMapView.d.ts.map +1 -0
  78. package/dist/esm/TileMapView.js +385 -338
  79. package/dist/esm/TileMapView.js.map +1 -1
  80. package/dist/esm/TileSet.d.ts +15 -6
  81. package/dist/esm/TileSet.d.ts.map +1 -0
  82. package/dist/esm/TileSet.js +161 -181
  83. package/dist/esm/TileSet.js.map +1 -1
  84. package/dist/esm/WangSet.d.ts +5 -4
  85. package/dist/esm/WangSet.d.ts.map +1 -0
  86. package/dist/esm/WangSet.js +70 -78
  87. package/dist/esm/WangSet.js.map +1 -1
  88. package/dist/esm/WorkerSampledChunkSource.d.ts +95 -0
  89. package/dist/esm/WorkerSampledChunkSource.d.ts.map +1 -0
  90. package/dist/esm/WorkerSampledChunkSource.js +117 -0
  91. package/dist/esm/WorkerSampledChunkSource.js.map +1 -0
  92. package/dist/esm/autoTile.d.ts +10 -5
  93. package/dist/esm/autoTile.d.ts.map +1 -0
  94. package/dist/esm/autoTile.js +164 -205
  95. package/dist/esm/autoTile.js.map +1 -1
  96. package/dist/esm/chunkGeometry.d.ts +8 -7
  97. package/dist/esm/chunkGeometry.d.ts.map +1 -0
  98. package/dist/esm/chunkGeometry.js +83 -94
  99. package/dist/esm/chunkGeometry.js.map +1 -1
  100. package/dist/esm/index.d.ts +1 -0
  101. package/dist/esm/index.d.ts.map +1 -0
  102. package/dist/esm/index.js +26 -15
  103. package/dist/esm/nodeBounds.d.ts +20 -0
  104. package/dist/esm/nodeBounds.d.ts.map +1 -0
  105. package/dist/esm/nodeBounds.js +40 -0
  106. package/dist/esm/nodeBounds.js.map +1 -0
  107. package/dist/esm/pixelSnap.d.ts +4 -3
  108. package/dist/esm/pixelSnap.d.ts.map +1 -0
  109. package/dist/esm/pixelSnap.js +18 -16
  110. package/dist/esm/pixelSnap.js.map +1 -1
  111. package/dist/esm/public.d.ts +23 -3
  112. package/dist/esm/public.d.ts.map +1 -0
  113. package/dist/esm/tileCollision.d.ts +190 -0
  114. package/dist/esm/tileCollision.d.ts.map +1 -0
  115. package/dist/esm/tileCollision.js +367 -0
  116. package/dist/esm/tileCollision.js.map +1 -0
  117. package/dist/esm/tileWord.d.ts +12 -0
  118. package/dist/esm/tileWord.d.ts.map +1 -0
  119. package/dist/esm/tileWord.js +16 -0
  120. package/dist/esm/tileWord.js.map +1 -0
  121. package/dist/esm/tilemapExtension.d.ts +4 -3
  122. package/dist/esm/tilemapExtension.d.ts.map +1 -0
  123. package/dist/esm/tilemapExtension.js +47 -47
  124. package/dist/esm/tilemapExtension.js.map +1 -1
  125. package/dist/esm/tilemapSerializers.d.ts +5 -4
  126. package/dist/esm/tilemapSerializers.d.ts.map +1 -0
  127. package/dist/esm/tilemapSerializers.js +48 -41
  128. package/dist/esm/tilemapSerializers.js.map +1 -1
  129. package/dist/esm/types.d.ts +34 -18
  130. package/dist/esm/types.d.ts.map +1 -0
  131. package/dist/esm/types.js +111 -120
  132. package/dist/esm/types.js.map +1 -1
  133. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +50 -2
  134. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts.map +1 -0
  135. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +366 -332
  136. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -1
  137. package/dist/esm/webgl2/shaders/tile-chunk.frag.js +6 -0
  138. package/dist/esm/webgl2/shaders/tile-chunk.frag.js.map +1 -0
  139. package/dist/esm/webgl2/shaders/tile-chunk.vert.js +6 -0
  140. package/dist/esm/webgl2/shaders/tile-chunk.vert.js.map +1 -0
  141. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +70 -4
  142. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts.map +1 -0
  143. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +496 -383
  144. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -1
  145. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js +6 -0
  146. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js.map +1 -0
  147. package/package.json +8 -15
  148. package/dist/esm/index.js.map +0 -1
  149. package/dist/esm/register.d.ts +0 -1
  150. package/dist/esm/register.js +0 -25
  151. package/dist/esm/register.js.map +0 -1
@@ -0,0 +1,43 @@
1
+ import type { ChunkSource } from './ChunkSource';
2
+ import type { TileLayer } from './TileLayer';
3
+ import type { ResolvedTile } from './types';
4
+ /**
5
+ * Options for {@link createSampledChunkSource}.
6
+ * @advanced
7
+ */
8
+ export interface SampledChunkSourceOptions {
9
+ /**
10
+ * Sample a scalar value at a tile coordinate. Called once per tile within
11
+ * each requested chunk. Must be a pure, deterministic function of
12
+ * `(tx, ty)` - {@link import('./ChunkStreamer').ChunkStreamer} regenerates
13
+ * a chunk from scratch via this source whenever it's revisited after
14
+ * eviction, so a non-deterministic sampler would make previously-visited
15
+ * terrain silently change underneath the player.
16
+ */
17
+ sample(tx: number, ty: number): number;
18
+ /**
19
+ * Convert a sampled value (and its tile coordinate, for position-dependent
20
+ * logic such as river carving or map edges) into a resolved tile, or
21
+ * `null` for an empty cell. Must be pure and deterministic, same
22
+ * requirement as {@link sample}.
23
+ */
24
+ mapValueToTile(value: number, tx: number, ty: number): ResolvedTile | null;
25
+ }
26
+ /**
27
+ * Build a {@link import('./ChunkStreamer').ChunkStreamer}-ready
28
+ * {@link ChunkSource} from a caller-supplied sampling function - the
29
+ * generic mechanism behind procedurally generated tile content (noise,
30
+ * a heightmap, or any other per-tile-coordinate scalar function). This
31
+ * module intentionally has no opinion on the sampling algorithm itself
32
+ * ({@link SampledChunkSourceOptions.sample}/`mapValueToTile` are both
33
+ * required, not defaulted) - pick or write whatever generation function
34
+ * fits your game.
35
+ *
36
+ * `layer` supplies the target chunk grid ({@link TileLayer.chunkWidth}/
37
+ * `chunkHeight`) and the tileset list tiles are packed against
38
+ * ({@link TileLayer.tilesets}) - the same `TileLayer` you'll construct the
39
+ * `ChunkStreamer` with.
40
+ * @advanced
41
+ */
42
+ export declare const createSampledChunkSource: (layer: TileLayer, options: SampledChunkSourceOptions) => ChunkSource;
43
+ //# sourceMappingURL=SampledChunkSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SampledChunkSource.d.ts","sourceRoot":"","sources":["../../src/SampledChunkSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACvC;;;;;OAKG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;CAC5E;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,EAAE,SAAS,yBAAyB,KAAG,WAiC/F,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { packTile } from "./types.js";
2
+
3
+ //#region src/SampledChunkSource.ts
4
+ /**
5
+ * Build a {@link import('./ChunkStreamer').ChunkStreamer}-ready
6
+ * {@link ChunkSource} from a caller-supplied sampling function - the
7
+ * generic mechanism behind procedurally generated tile content (noise,
8
+ * a heightmap, or any other per-tile-coordinate scalar function). This
9
+ * module intentionally has no opinion on the sampling algorithm itself
10
+ * ({@link SampledChunkSourceOptions.sample}/`mapValueToTile` are both
11
+ * required, not defaulted) - pick or write whatever generation function
12
+ * fits your game.
13
+ *
14
+ * `layer` supplies the target chunk grid ({@link TileLayer.chunkWidth}/
15
+ * `chunkHeight`) and the tileset list tiles are packed against
16
+ * ({@link TileLayer.tilesets}) - the same `TileLayer` you'll construct the
17
+ * `ChunkStreamer` with.
18
+ * @advanced
19
+ */
20
+ const createSampledChunkSource = (layer, options) => {
21
+ const sample = options.sample.bind(options);
22
+ const mapValueToTile = options.mapValueToTile.bind(options);
23
+ return { getChunk(cx, cy) {
24
+ const chunkWidth = layer.chunkWidth;
25
+ const chunkHeight = layer.chunkHeight;
26
+ const startTx = cx * chunkWidth;
27
+ const startTy = cy * chunkHeight;
28
+ let out = null;
29
+ for (let ty = startTy; ty < startTy + chunkHeight; ty++) for (let tx = startTx; tx < startTx + chunkWidth; tx++) {
30
+ if (layer.width !== void 0 && layer.height !== void 0 && (tx >= layer.width || ty >= layer.height)) continue;
31
+ const value = sample(tx, ty);
32
+ const resolved = mapValueToTile(value, tx, ty);
33
+ if (!resolved) continue;
34
+ out ??= new Uint32Array(chunkWidth * chunkHeight);
35
+ const tilesetIndex = layer.tilesets.indexOf(resolved.tileset);
36
+ out[(ty - startTy) * chunkWidth + (tx - startTx)] = packTile(tilesetIndex, resolved.localTileId, resolved.transform);
37
+ }
38
+ return out === null ? null : {
39
+ width: chunkWidth,
40
+ height: chunkHeight,
41
+ tiles: out
42
+ };
43
+ } };
44
+ };
45
+
46
+ //#endregion
47
+ export { createSampledChunkSource };
48
+ //# sourceMappingURL=SampledChunkSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SampledChunkSource.js","names":[],"sources":["../../src/SampledChunkSource.ts"],"sourcesContent":["import type { ChunkPayload, ChunkSource } from './ChunkSource';\nimport type { TileLayer } from './TileLayer';\nimport type { ResolvedTile } from './types';\nimport { packTile } from './types';\n\n/**\n * Options for {@link createSampledChunkSource}.\n * @advanced\n */\nexport interface SampledChunkSourceOptions {\n /**\n * Sample a scalar value at a tile coordinate. Called once per tile within\n * each requested chunk. Must be a pure, deterministic function of\n * `(tx, ty)` - {@link import('./ChunkStreamer').ChunkStreamer} regenerates\n * a chunk from scratch via this source whenever it's revisited after\n * eviction, so a non-deterministic sampler would make previously-visited\n * terrain silently change underneath the player.\n */\n sample(tx: number, ty: number): number;\n /**\n * Convert a sampled value (and its tile coordinate, for position-dependent\n * logic such as river carving or map edges) into a resolved tile, or\n * `null` for an empty cell. Must be pure and deterministic, same\n * requirement as {@link sample}.\n */\n mapValueToTile(value: number, tx: number, ty: number): ResolvedTile | null;\n}\n\n/**\n * Build a {@link import('./ChunkStreamer').ChunkStreamer}-ready\n * {@link ChunkSource} from a caller-supplied sampling function - the\n * generic mechanism behind procedurally generated tile content (noise,\n * a heightmap, or any other per-tile-coordinate scalar function). This\n * module intentionally has no opinion on the sampling algorithm itself\n * ({@link SampledChunkSourceOptions.sample}/`mapValueToTile` are both\n * required, not defaulted) - pick or write whatever generation function\n * fits your game.\n *\n * `layer` supplies the target chunk grid ({@link TileLayer.chunkWidth}/\n * `chunkHeight`) and the tileset list tiles are packed against\n * ({@link TileLayer.tilesets}) - the same `TileLayer` you'll construct the\n * `ChunkStreamer` with.\n * @advanced\n */\nexport const createSampledChunkSource = (layer: TileLayer, options: SampledChunkSourceOptions): ChunkSource => {\n const sample = options.sample.bind(options);\n const mapValueToTile = options.mapValueToTile.bind(options);\n\n return {\n getChunk(cx: number, cy: number): ChunkPayload | null {\n const chunkWidth = layer.chunkWidth;\n const chunkHeight = layer.chunkHeight;\n const startTx = cx * chunkWidth;\n const startTy = cy * chunkHeight;\n\n let out: Uint32Array | null = null;\n for (let ty = startTy; ty < startTy + chunkHeight; ty++) {\n for (let tx = startTx; tx < startTx + chunkWidth; tx++) {\n // Bounded-layer edge chunks may be smaller than a full chunk\n // (TileLayer._ensureChunk clamps them); this provider always\n // samples a full chunk rect, so clamp here instead of letting\n // _adoptChunk over-report tiles past the layer's declared bounds.\n if (layer.width !== undefined && layer.height !== undefined && (tx >= layer.width || ty >= layer.height)) continue;\n\n const value = sample(tx, ty);\n const resolved = mapValueToTile(value, tx, ty);\n if (!resolved) continue;\n\n out ??= new Uint32Array(chunkWidth * chunkHeight);\n const tilesetIndex = layer.tilesets.indexOf(resolved.tileset);\n out[(ty - startTy) * chunkWidth + (tx - startTx)] = packTile(tilesetIndex, resolved.localTileId, resolved.transform);\n }\n }\n\n return out === null ? null : { width: chunkWidth, height: chunkHeight, tiles: out };\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA4CA,MAAa,4BAA4B,OAAkB,YAAoD;CAC7G,MAAM,SAAS,QAAQ,OAAO,KAAK,OAAO;CAC1C,MAAM,iBAAiB,QAAQ,eAAe,KAAK,OAAO;CAE1D,OAAO,EACL,SAAS,IAAY,IAAiC;EACpD,MAAM,aAAa,MAAM;EACzB,MAAM,cAAc,MAAM;EAC1B,MAAM,UAAU,KAAK;EACrB,MAAM,UAAU,KAAK;EAErB,IAAI,MAA0B;EAC9B,KAAK,IAAI,KAAK,SAAS,KAAK,UAAU,aAAa,MACjD,KAAK,IAAI,KAAK,SAAS,KAAK,UAAU,YAAY,MAAM;GAKtD,IAAI,MAAM,UAAU,UAAa,MAAM,WAAW,WAAc,MAAM,MAAM,SAAS,MAAM,MAAM,SAAS;GAE1G,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,MAAM,WAAW,eAAe,OAAO,IAAI,EAAE;GAC7C,IAAI,CAAC,UAAU;GAEf,QAAQ,IAAI,YAAY,aAAa,WAAW;GAChD,MAAM,eAAe,MAAM,SAAS,QAAQ,SAAS,OAAO;GAC5D,KAAK,KAAK,WAAW,cAAc,KAAK,YAAY,SAAS,cAAc,SAAS,aAAa,SAAS,SAAS;EACrH;EAGF,OAAO,QAAQ,OAAO,OAAO;GAAE,OAAO;GAAY,QAAQ;GAAa,OAAO;EAAI;CACpF,EACF;AACF"}
@@ -5,12 +5,21 @@ import type { TileLayer } from './TileLayer';
5
5
  * On construction it scans the given layer(s) once and registers every cell
6
6
  * whose tile carries an `animation` (see {@link import('./types').TileDefinition}).
7
7
  * Each {@link update} advances a shared clock and rewrites **only** the
8
- * registered animated cells, and **only** when a cell crosses a frame boundary
8
+ * registered animated cells, and **only** when a cell crosses a frame boundary -
9
9
  * static tiles are never touched. Because a tile rewrite goes through
10
10
  * `layer.setTileAt`, only the chunks that actually contain animated cells rebuild
11
11
  * their geometry, and only on the (infrequent) frames where a boundary is
12
12
  * crossed. The large static body of the map never rebuilds.
13
13
  *
14
+ * An unbounded {@link import('./TileLayer').TileLayer} (see
15
+ * {@link import('./TileLayer').TileLayer.bounded}) is skipped entirely by
16
+ * the initial scan and reports zero animated cells for that layer, rather
17
+ * than sweeping every ever-loaded chunk. Streaming/procedural chunk
18
+ * providers that need animated tiles must drive per-chunk `TileAnimator`
19
+ * instances themselves (scanning only the newly-adopted chunk's tile
20
+ * range) - a whole-layer `TileAnimator` is not currently wired to react to
21
+ * chunk load/unload.
22
+ *
14
23
  * Tick it from your update loop, like `TweenSequencer`:
15
24
  *
16
25
  * ```ts
@@ -18,7 +27,7 @@ import type { TileLayer } from './TileLayer';
18
27
  * scene.systems.add({ update: (t) => animator.update(t.deltaSeconds) });
19
28
  * ```
20
29
  *
21
- * The animator references but never owns the layers and their tilesets;
30
+ * The animator references - but never owns - the layers and their tilesets;
22
31
  * {@link destroy} only drops its own cell registry.
23
32
  *
24
33
  * @advanced
@@ -60,3 +69,4 @@ export declare class TileAnimator {
60
69
  /** Scan all layers for cells whose tile carries a (multi-frame) animation. */
61
70
  private _scan;
62
71
  }
72
+ //# sourceMappingURL=TileAnimator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TileAnimator.d.ts","sourceRoot":"","sources":["../../src/TileAnimator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAmB7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,UAAU,CAAK;IAEvB;;;OAGG;gBACgB,MAAM,EAAE,SAAS,GAAG,SAAS,SAAS,EAAE;IAK3D,uEAAuE;IACvE,IAAW,iBAAiB,IAAI,MAAM,CAErC;IAED,6EAA6E;IAC7E,IAAW,SAAS,IAAI,MAAM,CAE7B;IAED;;;;;;OAMG;IACI,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IA0BzC;;;OAGG;IACI,KAAK,IAAI,IAAI;IAgBpB;;;;OAIG;IACI,MAAM,IAAI,IAAI;IAKrB,sEAAsE;IAC/D,OAAO,IAAI,IAAI;IAKtB,8EAA8E;IAC9E,OAAO,CAAC,KAAK;CAqDd"}
@@ -1,169 +1,158 @@
1
+ //#region src/TileAnimator.ts
1
2
  /**
2
- * Drives per-tile animations on one or more {@link TileLayer}s, RPG-Maker style.
3
- *
4
- * On construction it scans the given layer(s) once and registers every cell
5
- * whose tile carries an `animation` (see {@link import('./types').TileDefinition}).
6
- * Each {@link update} advances a shared clock and rewrites **only** the
7
- * registered animated cells, and **only** when a cell crosses a frame boundary
8
- * static tiles are never touched. Because a tile rewrite goes through
9
- * `layer.setTileAt`, only the chunks that actually contain animated cells rebuild
10
- * their geometry, and only on the (infrequent) frames where a boundary is
11
- * crossed. The large static body of the map never rebuilds.
12
- *
13
- * Tick it from your update loop, like `TweenSequencer`:
14
- *
15
- * ```ts
16
- * const animator = new TileAnimator(map.layers);
17
- * scene.systems.add({ update: (t) => animator.update(t.deltaSeconds) });
18
- * ```
19
- *
20
- * The animator references — but never owns — the layers and their tilesets;
21
- * {@link destroy} only drops its own cell registry.
22
- *
23
- * @advanced
24
- */
25
- class TileAnimator {
26
- _layers;
27
- _cells = [];
28
- _elapsedMs = 0;
29
- /**
30
- * @param layers One layer, or an array of layers (e.g. `map.layers`), to scan
31
- * for animated tiles.
32
- */
33
- constructor(layers) {
34
- this._layers = Array.isArray(layers) ? layers : [layers];
35
- this._scan();
36
- }
37
- /** Number of animated cells currently registered across all layers. */
38
- get animatedCellCount() {
39
- return this._cells.length;
40
- }
41
- /** Total elapsed animation time in milliseconds since construction/reset. */
42
- get elapsedMs() {
43
- return this._elapsedMs;
44
- }
45
- /**
46
- * Advance all registered animations by `deltaSeconds` and write the current
47
- * frame into any cell that crossed a frame boundary. Cells that did not change
48
- * frame are not touched, so their chunks do not rebuild.
49
- *
50
- * @param deltaSeconds Elapsed wall-clock time since the last call, in seconds.
51
- */
52
- update(deltaSeconds) {
53
- if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0 || this._cells.length === 0) {
54
- return;
55
- }
56
- this._elapsedMs += deltaSeconds * 1000;
57
- for (const cell of this._cells) {
58
- const t = this._elapsedMs % cell.totalMs;
59
- const frameIndex = frameIndexAt(cell.cumulative, t);
60
- if (frameIndex === cell.currentFrame) {
61
- continue;
62
- }
63
- cell.currentFrame = frameIndex;
64
- const frame = cell.frames[frameIndex];
65
- if (frame === undefined) {
66
- continue;
67
- }
68
- cell.layer.setTileAt(cell.tx, cell.ty, {
69
- tileset: cell.tileset,
70
- localTileId: frame.localTileId,
71
- transform: cell.transform,
72
- });
73
- }
74
- }
75
- /**
76
- * Restore every animated cell to its first frame and reset the clock.
77
- * Useful before serialising or pausing.
78
- */
79
- reset() {
80
- this._elapsedMs = 0;
81
- for (const cell of this._cells) {
82
- const frame = cell.frames[0];
83
- if (frame === undefined) {
84
- continue;
85
- }
86
- cell.currentFrame = 0;
87
- cell.layer.setTileAt(cell.tx, cell.ty, {
88
- tileset: cell.tileset,
89
- localTileId: frame.localTileId,
90
- transform: cell.transform,
91
- });
92
- }
93
- }
94
- /**
95
- * Re-scan the layers for animated cells. Call after structural edits that add
96
- * or remove animated tiles. Resets all currently-tracked cells to frame 0
97
- * first so the rescan starts from a clean, deterministic state.
98
- */
99
- rescan() {
100
- this.reset();
101
- this._scan();
102
- }
103
- /** Drop the cell registry. Does not modify the layers or tilesets. */
104
- destroy() {
105
- this._cells = [];
106
- this._elapsedMs = 0;
107
- }
108
- /** Scan all layers for cells whose tile carries a (multi-frame) animation. */
109
- _scan() {
110
- const cells = [];
111
- for (const layer of this._layers) {
112
- for (let ty = 0; ty < layer.height; ty++) {
113
- for (let tx = 0; tx < layer.width; tx++) {
114
- const tile = layer.getTileAt(tx, ty);
115
- if (!tile) {
116
- continue;
117
- }
118
- const def = tile.tileset.getTileDefinition(tile.localTileId);
119
- const frames = def?.animation;
120
- if (!frames || frames.length < 2) {
121
- continue;
122
- }
123
- // Skip animations referencing out-of-range frames so update() never
124
- // throws from setTileAt; such data is malformed and silently ignored.
125
- if (frames.some(f => f.localTileId < 0 || f.localTileId >= tile.tileset.tileCount)) {
126
- continue;
127
- }
128
- const cumulative = [];
129
- let total = 0;
130
- for (const frame of frames) {
131
- total += Math.max(0, frame.duration);
132
- cumulative.push(total);
133
- }
134
- if (total <= 0) {
135
- continue;
136
- }
137
- cells.push({
138
- layer,
139
- tx,
140
- ty,
141
- tileset: tile.tileset,
142
- transform: tile.transform,
143
- frames,
144
- cumulative,
145
- totalMs: total,
146
- currentFrame: -1,
147
- });
148
- }
149
- }
150
- }
151
- this._cells = cells;
152
- }
153
- }
3
+ * Drives per-tile animations on one or more {@link TileLayer}s, RPG-Maker style.
4
+ *
5
+ * On construction it scans the given layer(s) once and registers every cell
6
+ * whose tile carries an `animation` (see {@link import('./types').TileDefinition}).
7
+ * Each {@link update} advances a shared clock and rewrites **only** the
8
+ * registered animated cells, and **only** when a cell crosses a frame boundary -
9
+ * static tiles are never touched. Because a tile rewrite goes through
10
+ * `layer.setTileAt`, only the chunks that actually contain animated cells rebuild
11
+ * their geometry, and only on the (infrequent) frames where a boundary is
12
+ * crossed. The large static body of the map never rebuilds.
13
+ *
14
+ * An unbounded {@link import('./TileLayer').TileLayer} (see
15
+ * {@link import('./TileLayer').TileLayer.bounded}) is skipped entirely by
16
+ * the initial scan and reports zero animated cells for that layer, rather
17
+ * than sweeping every ever-loaded chunk. Streaming/procedural chunk
18
+ * providers that need animated tiles must drive per-chunk `TileAnimator`
19
+ * instances themselves (scanning only the newly-adopted chunk's tile
20
+ * range) - a whole-layer `TileAnimator` is not currently wired to react to
21
+ * chunk load/unload.
22
+ *
23
+ * Tick it from your update loop, like `TweenSequencer`:
24
+ *
25
+ * ```ts
26
+ * const animator = new TileAnimator(map.layers);
27
+ * scene.systems.add({ update: (t) => animator.update(t.deltaSeconds) });
28
+ * ```
29
+ *
30
+ * The animator references - but never owns - the layers and their tilesets;
31
+ * {@link destroy} only drops its own cell registry.
32
+ *
33
+ * @advanced
34
+ */
35
+ var TileAnimator = class {
36
+ _layers;
37
+ _cells = [];
38
+ _elapsedMs = 0;
39
+ /**
40
+ * @param layers One layer, or an array of layers (e.g. `map.layers`), to scan
41
+ * for animated tiles.
42
+ */
43
+ constructor(layers) {
44
+ this._layers = Array.isArray(layers) ? layers : [layers];
45
+ this._scan();
46
+ }
47
+ /** Number of animated cells currently registered across all layers. */
48
+ get animatedCellCount() {
49
+ return this._cells.length;
50
+ }
51
+ /** Total elapsed animation time in milliseconds since construction/reset. */
52
+ get elapsedMs() {
53
+ return this._elapsedMs;
54
+ }
55
+ /**
56
+ * Advance all registered animations by `deltaSeconds` and write the current
57
+ * frame into any cell that crossed a frame boundary. Cells that did not change
58
+ * frame are not touched, so their chunks do not rebuild.
59
+ *
60
+ * @param deltaSeconds Elapsed wall-clock time since the last call, in seconds.
61
+ */
62
+ update(deltaSeconds) {
63
+ if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0 || this._cells.length === 0) return;
64
+ this._elapsedMs += deltaSeconds * 1e3;
65
+ for (const cell of this._cells) {
66
+ const t = this._elapsedMs % cell.totalMs;
67
+ const frameIndex = frameIndexAt(cell.cumulative, t);
68
+ if (frameIndex === cell.currentFrame) continue;
69
+ cell.currentFrame = frameIndex;
70
+ const frame = cell.frames[frameIndex];
71
+ if (frame === void 0) continue;
72
+ cell.layer.setTileAt(cell.tx, cell.ty, {
73
+ tileset: cell.tileset,
74
+ localTileId: frame.localTileId,
75
+ transform: cell.transform
76
+ });
77
+ }
78
+ }
79
+ /**
80
+ * Restore every animated cell to its first frame and reset the clock.
81
+ * Useful before serialising or pausing.
82
+ */
83
+ reset() {
84
+ this._elapsedMs = 0;
85
+ for (const cell of this._cells) {
86
+ const frame = cell.frames[0];
87
+ if (frame === void 0) continue;
88
+ cell.currentFrame = 0;
89
+ cell.layer.setTileAt(cell.tx, cell.ty, {
90
+ tileset: cell.tileset,
91
+ localTileId: frame.localTileId,
92
+ transform: cell.transform
93
+ });
94
+ }
95
+ }
96
+ /**
97
+ * Re-scan the layers for animated cells. Call after structural edits that add
98
+ * or remove animated tiles. Resets all currently-tracked cells to frame 0
99
+ * first so the rescan starts from a clean, deterministic state.
100
+ */
101
+ rescan() {
102
+ this.reset();
103
+ this._scan();
104
+ }
105
+ /** Drop the cell registry. Does not modify the layers or tilesets. */
106
+ destroy() {
107
+ this._cells = [];
108
+ this._elapsedMs = 0;
109
+ }
110
+ /** Scan all layers for cells whose tile carries a (multi-frame) animation. */
111
+ _scan() {
112
+ const cells = [];
113
+ for (const layer of this._layers) {
114
+ if (layer.width === void 0 || layer.height === void 0) continue;
115
+ for (let ty = 0; ty < layer.height; ty++) for (let tx = 0; tx < layer.width; tx++) {
116
+ const tile = layer.getTileAt(tx, ty);
117
+ if (!tile) continue;
118
+ const frames = tile.tileset.getTileDefinition(tile.localTileId)?.animation;
119
+ if (!frames || frames.length < 2) continue;
120
+ if (frames.some((f) => f.localTileId < 0 || f.localTileId >= tile.tileset.tileCount)) continue;
121
+ const cumulative = [];
122
+ let total = 0;
123
+ for (const frame of frames) {
124
+ total += Math.max(0, frame.duration);
125
+ cumulative.push(total);
126
+ }
127
+ if (total <= 0) continue;
128
+ cells.push({
129
+ layer,
130
+ tx,
131
+ ty,
132
+ tileset: tile.tileset,
133
+ transform: tile.transform,
134
+ frames,
135
+ cumulative,
136
+ totalMs: total,
137
+ currentFrame: -1
138
+ });
139
+ }
140
+ }
141
+ this._cells = cells;
142
+ }
143
+ };
154
144
  /**
155
- * Find the frame index whose cumulative window contains time `t` (0 ≤ t < total).
156
- * `cumulative[i]` is the end time of frame `i`; the last entry equals the total.
157
- */
158
- function frameIndexAt(cumulative, t) {
159
- for (let i = 0; i < cumulative.length; i++) {
160
- const end = cumulative[i];
161
- if (end !== undefined && t < end) {
162
- return i;
163
- }
164
- }
165
- return cumulative.length - 1;
166
- }
145
+ * Find the frame index whose cumulative window contains time `t` (0 ≤ t < total).
146
+ * `cumulative[i]` is the end time of frame `i`; the last entry equals the total.
147
+ */
148
+ const frameIndexAt = (cumulative, t) => {
149
+ for (let i = 0; i < cumulative.length; i++) {
150
+ const end = cumulative[i];
151
+ if (end !== void 0 && t < end) return i;
152
+ }
153
+ return cumulative.length - 1;
154
+ };
167
155
 
156
+ //#endregion
168
157
  export { TileAnimator };
169
- //# sourceMappingURL=TileAnimator.js.map
158
+ //# sourceMappingURL=TileAnimator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TileAnimator.js","sources":["../../../src/TileAnimator.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TileAnimator.js","names":[],"sources":["../../src/TileAnimator.ts"],"sourcesContent":["import type { TileLayer } from './TileLayer';\nimport type { TileSet } from './TileSet';\nimport type { TileAnimationFrame, TileTransform } from './types';\n\n/** Internal per-cell animation record. */\ninterface AnimatedCell {\n readonly layer: TileLayer;\n readonly tx: number;\n readonly ty: number;\n readonly tileset: TileSet;\n readonly transform: TileTransform;\n readonly frames: readonly TileAnimationFrame[];\n /** Cumulative end-time (ms) of each frame, so `cumulative.at(-1) === totalMs`. */\n readonly cumulative: readonly number[];\n readonly totalMs: number;\n /** Index of the frame currently written into the layer (-1 = none yet). */\n currentFrame: number;\n}\n\n/**\n * Drives per-tile animations on one or more {@link TileLayer}s, RPG-Maker style.\n *\n * On construction it scans the given layer(s) once and registers every cell\n * whose tile carries an `animation` (see {@link import('./types').TileDefinition}).\n * Each {@link update} advances a shared clock and rewrites **only** the\n * registered animated cells, and **only** when a cell crosses a frame boundary -\n * static tiles are never touched. Because a tile rewrite goes through\n * `layer.setTileAt`, only the chunks that actually contain animated cells rebuild\n * their geometry, and only on the (infrequent) frames where a boundary is\n * crossed. The large static body of the map never rebuilds.\n *\n * An unbounded {@link import('./TileLayer').TileLayer} (see\n * {@link import('./TileLayer').TileLayer.bounded}) is skipped entirely by\n * the initial scan and reports zero animated cells for that layer, rather\n * than sweeping every ever-loaded chunk. Streaming/procedural chunk\n * providers that need animated tiles must drive per-chunk `TileAnimator`\n * instances themselves (scanning only the newly-adopted chunk's tile\n * range) - a whole-layer `TileAnimator` is not currently wired to react to\n * chunk load/unload.\n *\n * Tick it from your update loop, like `TweenSequencer`:\n *\n * ```ts\n * const animator = new TileAnimator(map.layers);\n * scene.systems.add({ update: (t) => animator.update(t.deltaSeconds) });\n * ```\n *\n * The animator references - but never owns - the layers and their tilesets;\n * {@link destroy} only drops its own cell registry.\n *\n * @advanced\n */\nexport class TileAnimator {\n private readonly _layers: readonly TileLayer[];\n private _cells: AnimatedCell[] = [];\n private _elapsedMs = 0;\n\n /**\n * @param layers One layer, or an array of layers (e.g. `map.layers`), to scan\n * for animated tiles.\n */\n public constructor(layers: TileLayer | readonly TileLayer[]) {\n this._layers = Array.isArray(layers) ? layers : [layers as TileLayer];\n this._scan();\n }\n\n /** Number of animated cells currently registered across all layers. */\n public get animatedCellCount(): number {\n return this._cells.length;\n }\n\n /** Total elapsed animation time in milliseconds since construction/reset. */\n public get elapsedMs(): number {\n return this._elapsedMs;\n }\n\n /**\n * Advance all registered animations by `deltaSeconds` and write the current\n * frame into any cell that crossed a frame boundary. Cells that did not change\n * frame are not touched, so their chunks do not rebuild.\n *\n * @param deltaSeconds Elapsed wall-clock time since the last call, in seconds.\n */\n public update(deltaSeconds: number): void {\n if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0 || this._cells.length === 0) {\n return;\n }\n\n this._elapsedMs += deltaSeconds * 1000;\n\n for (const cell of this._cells) {\n const t = this._elapsedMs % cell.totalMs;\n const frameIndex = frameIndexAt(cell.cumulative, t);\n if (frameIndex === cell.currentFrame) {\n continue;\n }\n cell.currentFrame = frameIndex;\n const frame = cell.frames[frameIndex];\n if (frame === undefined) {\n continue;\n }\n cell.layer.setTileAt(cell.tx, cell.ty, {\n tileset: cell.tileset,\n localTileId: frame.localTileId,\n transform: cell.transform,\n });\n }\n }\n\n /**\n * Restore every animated cell to its first frame and reset the clock.\n * Useful before serialising or pausing.\n */\n public reset(): void {\n this._elapsedMs = 0;\n for (const cell of this._cells) {\n const frame = cell.frames[0];\n if (frame === undefined) {\n continue;\n }\n cell.currentFrame = 0;\n cell.layer.setTileAt(cell.tx, cell.ty, {\n tileset: cell.tileset,\n localTileId: frame.localTileId,\n transform: cell.transform,\n });\n }\n }\n\n /**\n * Re-scan the layers for animated cells. Call after structural edits that add\n * or remove animated tiles. Resets all currently-tracked cells to frame 0\n * first so the rescan starts from a clean, deterministic state.\n */\n public rescan(): void {\n this.reset();\n this._scan();\n }\n\n /** Drop the cell registry. Does not modify the layers or tilesets. */\n public destroy(): void {\n this._cells = [];\n this._elapsedMs = 0;\n }\n\n /** Scan all layers for cells whose tile carries a (multi-frame) animation. */\n private _scan(): void {\n const cells: AnimatedCell[] = [];\n\n for (const layer of this._layers) {\n if (layer.width === undefined || layer.height === undefined) {\n continue; // unbounded layers are not scanned in v1 — see class doc comment.\n }\n for (let ty = 0; ty < layer.height; ty++) {\n for (let tx = 0; tx < layer.width; tx++) {\n const tile = layer.getTileAt(tx, ty);\n if (!tile) {\n continue;\n }\n\n const def = tile.tileset.getTileDefinition(tile.localTileId);\n const frames = def?.animation;\n if (!frames || frames.length < 2) {\n continue;\n }\n\n // Skip animations referencing out-of-range frames so update() never\n // throws from setTileAt; such data is malformed and silently ignored.\n if (frames.some(f => f.localTileId < 0 || f.localTileId >= tile.tileset.tileCount)) {\n continue;\n }\n\n const cumulative: number[] = [];\n let total = 0;\n for (const frame of frames) {\n total += Math.max(0, frame.duration);\n cumulative.push(total);\n }\n if (total <= 0) {\n continue;\n }\n\n cells.push({\n layer,\n tx,\n ty,\n tileset: tile.tileset,\n transform: tile.transform,\n frames,\n cumulative,\n totalMs: total,\n currentFrame: -1,\n });\n }\n }\n }\n\n this._cells = cells;\n }\n}\n\n/**\n * Find the frame index whose cumulative window contains time `t` (0 ≤ t < total).\n * `cumulative[i]` is the end time of frame `i`; the last entry equals the total.\n */\nconst frameIndexAt = (cumulative: readonly number[], t: number): number => {\n for (let i = 0; i < cumulative.length; i++) {\n const end = cumulative[i];\n if (end !== undefined && t < end) {\n return i;\n }\n }\n return cumulative.length - 1;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,IAAa,eAAb,MAA0B;CACxB,AAAiB;CACjB,AAAQ,SAAyB,CAAC;CAClC,AAAQ,aAAa;;;;;CAMrB,AAAO,YAAY,QAA0C;EAC3D,KAAK,UAAU,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAmB;EACpE,KAAK,MAAM;CACb;;CAGA,IAAW,oBAA4B;EACrC,OAAO,KAAK,OAAO;CACrB;;CAGA,IAAW,YAAoB;EAC7B,OAAO,KAAK;CACd;;;;;;;;CASA,AAAO,OAAO,cAA4B;EACxC,IAAI,CAAC,OAAO,SAAS,YAAY,KAAK,gBAAgB,KAAK,KAAK,OAAO,WAAW,GAChF;EAGF,KAAK,cAAc,eAAe;EAElC,KAAK,MAAM,QAAQ,KAAK,QAAQ;GAC9B,MAAM,IAAI,KAAK,aAAa,KAAK;GACjC,MAAM,aAAa,aAAa,KAAK,YAAY,CAAC;GAClD,IAAI,eAAe,KAAK,cACtB;GAEF,KAAK,eAAe;GACpB,MAAM,QAAQ,KAAK,OAAO;GAC1B,IAAI,UAAU,QACZ;GAEF,KAAK,MAAM,UAAU,KAAK,IAAI,KAAK,IAAI;IACrC,SAAS,KAAK;IACd,aAAa,MAAM;IACnB,WAAW,KAAK;GAClB,CAAC;EACH;CACF;;;;;CAMA,AAAO,QAAc;EACnB,KAAK,aAAa;EAClB,KAAK,MAAM,QAAQ,KAAK,QAAQ;GAC9B,MAAM,QAAQ,KAAK,OAAO;GAC1B,IAAI,UAAU,QACZ;GAEF,KAAK,eAAe;GACpB,KAAK,MAAM,UAAU,KAAK,IAAI,KAAK,IAAI;IACrC,SAAS,KAAK;IACd,aAAa,MAAM;IACnB,WAAW,KAAK;GAClB,CAAC;EACH;CACF;;;;;;CAOA,AAAO,SAAe;EACpB,KAAK,MAAM;EACX,KAAK,MAAM;CACb;;CAGA,AAAO,UAAgB;EACrB,KAAK,SAAS,CAAC;EACf,KAAK,aAAa;CACpB;;CAGA,AAAQ,QAAc;EACpB,MAAM,QAAwB,CAAC;EAE/B,KAAK,MAAM,SAAS,KAAK,SAAS;GAChC,IAAI,MAAM,UAAU,UAAa,MAAM,WAAW,QAChD;GAEF,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,QAAQ,MAClC,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,OAAO,MAAM;IACvC,MAAM,OAAO,MAAM,UAAU,IAAI,EAAE;IACnC,IAAI,CAAC,MACH;IAIF,MAAM,SADM,KAAK,QAAQ,kBAAkB,KAAK,WAC/B,CAAC,EAAE;IACpB,IAAI,CAAC,UAAU,OAAO,SAAS,GAC7B;IAKF,IAAI,OAAO,MAAK,MAAK,EAAE,cAAc,KAAK,EAAE,eAAe,KAAK,QAAQ,SAAS,GAC/E;IAGF,MAAM,aAAuB,CAAC;IAC9B,IAAI,QAAQ;IACZ,KAAK,MAAM,SAAS,QAAQ;KAC1B,SAAS,KAAK,IAAI,GAAG,MAAM,QAAQ;KACnC,WAAW,KAAK,KAAK;IACvB;IACA,IAAI,SAAS,GACX;IAGF,MAAM,KAAK;KACT;KACA;KACA;KACA,SAAS,KAAK;KACd,WAAW,KAAK;KAChB;KACA;KACA,SAAS;KACT,cAAc;IAChB,CAAC;GACH;EAEJ;EAEA,KAAK,SAAS;CAChB;AACF;;;;;AAMA,MAAM,gBAAgB,YAA+B,MAAsB;CACzE,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;EAC1C,MAAM,MAAM,WAAW;EACvB,IAAI,QAAQ,UAAa,IAAI,KAC3B,OAAO;CAEX;CACA,OAAO,WAAW,SAAS;AAC7B"}
@@ -42,7 +42,7 @@ export interface ReadonlyTileChunk {
42
42
  * laid out row-major. Each cell is a packed tile word (0 = empty).
43
43
  * Chunk coordinates are signed to support future infinite maps.
44
44
  *
45
- * The backing array is **private** external code cannot mutate storage
45
+ * The backing array is **private** - external code cannot mutate storage
46
46
  * directly. All mutation must flow through {@link TileLayer} public APIs,
47
47
  * which call the package-internal `_setRawAt` method.
48
48
  *
@@ -60,12 +60,21 @@ export declare class TileChunk implements ReadonlyTileChunk {
60
60
  readonly width: number;
61
61
  /** Height of this chunk in tiles. */
62
62
  readonly height: number;
63
- /** Packed tile storage (row-major). Private never exposed directly. */
63
+ /** Packed tile storage (row-major). Private - never exposed directly. */
64
64
  private readonly _tiles;
65
65
  /** Whether every cell in this chunk is empty (0). Lazy-computed. */
66
66
  private _empty;
67
- /** Monotonic revision counter incremented on every mutation. */
67
+ /** Monotonic revision counter - incremented on every mutation. */
68
68
  private _revision;
69
+ /**
70
+ * Render-node-owned callbacks notified whenever `_revision` advances.
71
+ * `TileChunkNode.pages` caches geometry against `revision`, but that cache
72
+ * is only consulted from `render()` - a call the engine skips entirely for
73
+ * a content-clean subtree under a `RetainedContainer`. Without
74
+ * this push, an in-place tile edit after capture would replay stale cached
75
+ * geometry forever. Package-internal; not part of {@link ReadonlyTileChunk}.
76
+ */
77
+ private _dirtyListeners;
69
78
  /**
70
79
  * @param cx Signed chunk X coordinate (must be finite safe integer).
71
80
  * @param cy Signed chunk Y coordinate (must be finite safe integer).
@@ -91,7 +100,7 @@ export declare class TileChunk implements ReadonlyTileChunk {
91
100
  cloneTiles(): Uint32Array;
92
101
  /**
93
102
  * Package-internal access to the underlying tile storage.
94
- * Returns the actual mutable `Uint32Array` DO NOT USE publicly.
103
+ * Returns the actual mutable `Uint32Array` - DO NOT USE publicly.
95
104
  *
96
105
  * Provided for the future tilemap renderer (same package) so it can
97
106
  * read chunk data efficiently without a full `cloneTiles()` copy per frame.
@@ -102,7 +111,7 @@ export declare class TileChunk implements ReadonlyTileChunk {
102
111
  _getRawStorage(): Uint32Array;
103
112
  /**
104
113
  * Validate a packed tile value before storing.
105
- * Accepts any finite integer `Uint32Array` stores via unsigned 32-bit
114
+ * Accepts any finite integer - `Uint32Array` stores via unsigned 32-bit
106
115
  * truncation (`ToUint32`). Negative values produced by JavaScript's
107
116
  * signed bitwise ops (e.g. when the transform bits set bit 31) are valid.
108
117
  * @throws If the value is NaN, Infinity, or non-integer.
@@ -133,4 +142,20 @@ export declare class TileChunk implements ReadonlyTileChunk {
133
142
  * @internal
134
143
  */
135
144
  _clear(): void;
145
+ /**
146
+ * Register a callback invoked synchronously whenever this chunk's revision
147
+ * advances. Multiple listeners are supported (a chunk may back more than one
148
+ * `TileChunkNode` - e.g. two `TileLayerNode`s over the same `TileLayer`).
149
+ * Package-internal: called by `TileChunkNode` to push mutations onto the
150
+ * scene-node content-revision chain (see the `_dirtyListeners` field).
151
+ * @internal
152
+ */
153
+ _addDirtyListener(listener: () => void): void;
154
+ /**
155
+ * Unregister a listener added via {@link _addDirtyListener} (node destroy).
156
+ * @internal
157
+ */
158
+ _removeDirtyListener(listener: () => void): void;
159
+ private _notifyDirty;
136
160
  }
161
+ //# sourceMappingURL=TileChunk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TileChunk.d.ts","sourceRoot":"","sources":["../../src/TileChunk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAG1C;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC;IAE7C;;;OAGG;IACH,UAAU,IAAI,WAAW,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,SAAU,YAAW,iBAAiB;IACjD,iCAAiC;IACjC,SAAgB,EAAE,EAAE,MAAM,CAAC;IAC3B,iCAAiC;IACjC,SAAgB,EAAE,EAAE,MAAM,CAAC;IAE3B,oCAAoC;IACpC,SAAgB,KAAK,EAAE,MAAM,CAAC;IAC9B,qCAAqC;IACrC,SAAgB,MAAM,EAAE,MAAM,CAAC;IAE/B,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC,oEAAoE;IACpE,OAAO,CAAC,MAAM,CAAwB;IAEtC,kEAAkE;IAClE,OAAO,CAAC,SAAS,CAAK;IAEtB;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe,CAAgC;IAEvD;;;;;;OAMG;gBACgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW;IA0C9F,2DAA2D;IAC3D,OAAO,CAAC,MAAM;IAId;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAa3B,kBAAkB;IACX,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU;IAKnD,kBAAkB;IAClB,IAAW,KAAK,IAAI,OAAO,CAK1B;IAED,kBAAkB;IAClB,IAAW,QAAQ,IAAI,MAAM,CAE5B;IAED,kBAAkB;IACX,UAAU,IAAI,WAAW;IAMhC;;;;;;;;;OASG;IACI,cAAc,IAAI,WAAW;IAIpC;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAMvB;;;;;;OAMG;IACI,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO;IAarE;;;;;;OAMG;IACI,UAAU,IAAI,IAAI;IAMzB;;;;;;OAMG;IACI,MAAM,IAAI,IAAI;IASrB;;;;;;;OAOG;IACI,iBAAiB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAIpD;;;OAGG;IACI,oBAAoB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAIvD,OAAO,CAAC,YAAY;CAMrB"}