@codexo/exojs-tilemap 0.15.3 → 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,133 @@
1
+ import type { Destroyable, SceneNode } from '@codexo/exojs';
2
+ import type { MapObjectDescriptor } from './MapObject';
3
+ import { MapSpawnSession } from './MapSpawnSession';
4
+ import type { TileMap } from './TileMap';
5
+ /** Why a {@link MapSpawnError} was raised. */
6
+ export type MapSpawnErrorReason =
7
+ /** No factory is registered for the object's dispatch key and `unknown` is `'error'`. */
8
+ 'unknown-kind'
9
+ /** Two objects of the same map resolved to the same stable id. */
10
+ | 'duplicate-id'
11
+ /** A factory threw or rejected. The original error is the `cause`. */
12
+ | 'factory-failed';
13
+ /**
14
+ * A spawn failed. The whole spawn is rolled back before this is thrown - no
15
+ * partial session exists.
16
+ *
17
+ * For `'factory-failed'` the error a factory raised is preserved as `cause`.
18
+ */
19
+ export declare class MapSpawnError extends Error {
20
+ readonly name = "MapSpawnError";
21
+ readonly reason: MapSpawnErrorReason;
22
+ /** {@link MapObjectDescriptor.id} of the object that failed. */
23
+ readonly objectId: string;
24
+ /** {@link MapObjectDescriptor.kind} of the object that failed. */
25
+ readonly kind: string | null;
26
+ constructor(reason: MapSpawnErrorReason, objectId: string, kind: string | null, message: string, options?: ErrorOptions);
27
+ }
28
+ /**
29
+ * Creates one runtime object from one map object, or `null` to deliberately
30
+ * create nothing for it.
31
+ *
32
+ * `signal` aborts with the spawn it belongs to; a factory that loads assets or
33
+ * awaits anything else should pass it on, so a level unloaded mid-load stops
34
+ * its own work too.
35
+ */
36
+ export type MapObjectFactory<Context, Result extends Destroyable> = (object: MapObjectDescriptor, context: Context, signal: AbortSignal) => Result | null | Promise<Result | null>;
37
+ /** Dispatch key to factory. Keys are matched against the spawner's `identify` result. */
38
+ export type MapObjectFactories<Context, Result extends Destroyable> = Readonly<Record<string, MapObjectFactory<Context, Result>>>;
39
+ /** What to do with an object no factory is registered for. */
40
+ export type UnknownMapObjectPolicy = 'ignore' | 'error';
41
+ /** Options for {@link MapObjectSpawner}. */
42
+ export interface MapObjectSpawnerOptions<Context> {
43
+ /**
44
+ * Picks the factory key for an object, or returns `null` to treat it as
45
+ * unknown. Defaults to {@link MapObjectDescriptor.kind}.
46
+ *
47
+ * This is where project-specific dispatch policy lives - keying on a
48
+ * property, a name prefix, or a compound key:
49
+ *
50
+ * ```ts
51
+ * identify: object => `${object.kind}:${object.properties.variant}`
52
+ * ```
53
+ */
54
+ readonly identify?: (object: MapObjectDescriptor, context: Context) => string | null;
55
+ /**
56
+ * What to do with an object no factory matched. Defaults to `'ignore'`,
57
+ * because maps legitimately carry editor markers, decoration and metadata
58
+ * objects that no game entity corresponds to. Use `'error'` for a map format
59
+ * the project fully controls and wants validated.
60
+ */
61
+ readonly unknown?: UnknownMapObjectPolicy;
62
+ }
63
+ /** Options for {@link MapObjectSpawner.spawn}. */
64
+ export interface MapSpawnOptions {
65
+ /**
66
+ * Cancels the spawn. An in-flight factory is still awaited - abandoning it
67
+ * would leak whatever it eventually produced - but its result is destroyed
68
+ * along with the rest of the rollback.
69
+ */
70
+ readonly signal?: AbortSignal;
71
+ }
72
+ /**
73
+ * Turns the objects authored in a map into game objects.
74
+ *
75
+ * A spawner owns its own dispatch table, so several games, tests, editor
76
+ * previews or mods can run in one process without seeing each other's
77
+ * factories. There is no global registry to register into and nothing to reset
78
+ * between tests.
79
+ *
80
+ * ExoJS owns the mechanism: identity, ordering, cancellation, rollback and
81
+ * lifetime. The game owns the policy: which object becomes what, and what those
82
+ * objects need - dependencies travel through `Context`, not through any
83
+ * service locator in the engine.
84
+ *
85
+ * A spawn is **atomic**: if any factory fails, everything already created is
86
+ * destroyed in reverse order and no session is produced.
87
+ *
88
+ * @typeParam Context - the per-spawn value handed to every factory.
89
+ * @typeParam Result - what the factories produce. Defaults to `SceneNode`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * interface GameContext {
94
+ * services: GameServices;
95
+ * save: SaveGame;
96
+ * }
97
+ *
98
+ * const spawner = new MapObjectSpawner<GameContext>({
99
+ * Enemy: (object, ctx) => new Enemy({ x: object.x, y: object.y, services: ctx.services }),
100
+ * Chest: (object, ctx) => new Chest({ id: object.id, save: ctx.save }),
101
+ * });
102
+ *
103
+ * const session = await spawner.spawn(map, { services, save });
104
+ * session.get('chest-in-the-cellar')?.open();
105
+ * session.destroy();
106
+ * ```
107
+ */
108
+ export declare class MapObjectSpawner<Context = void, Result extends Destroyable = SceneNode> {
109
+ /** What to do with an object no factory matched. */
110
+ readonly unknown: UnknownMapObjectPolicy;
111
+ private readonly _factories;
112
+ private readonly _identify;
113
+ constructor(factories: MapObjectFactories<Context, Result>, options?: MapObjectSpawnerOptions<Context>);
114
+ /** Whether a factory is registered under `key`. */
115
+ handles(key: string): boolean;
116
+ /**
117
+ * Create a game object for every object of `map` a factory matches, and
118
+ * return the {@link MapSpawnSession} that owns them.
119
+ *
120
+ * Objects are visited in object-layer order, then object order within each
121
+ * layer, and asynchronous factories are awaited in that same order - a
122
+ * factory that resolves early can never overtake one before it.
123
+ *
124
+ * @throws {MapSpawnError} when a factory fails, two objects share a stable id,
125
+ * or an unmatched object is found while `unknown` is `'error'`. In every case
126
+ * the objects already created are destroyed first. A factory that fails while
127
+ * the spawn is also being aborted reports the factory failure, not the abort -
128
+ * the more specific of the two.
129
+ * @throws {DOMException} named `AbortError` when `options.signal` aborts.
130
+ */
131
+ spawn(map: TileMap, context: Context, options?: MapSpawnOptions): Promise<MapSpawnSession<Result>>;
132
+ }
133
+ //# sourceMappingURL=MapObjectSpawner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapObjectSpawner.d.ts","sourceRoot":"","sources":["../../src/MapObjectSpawner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,8CAA8C;AAC9C,MAAM,MAAM,mBAAmB;AAC7B,yFAAyF;AACvF,cAAc;AAChB,kEAAkE;GAChE,cAAc;AAChB,sEAAsE;GACpE,gBAAgB,CAAC;AAErB;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAyB,IAAI,mBAAmB;IAChD,SAAgB,MAAM,EAAE,mBAAmB,CAAC;IAC5C,gEAAgE;IAChE,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,kEAAkE;IAClE,SAAgB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEjB,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAM/H;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,CAAC,OAAO,EAAE,MAAM,SAAS,WAAW,IAAI,CAClE,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,WAAW,KAChB,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAE5C,yFAAyF;AACzF,MAAM,MAAM,kBAAkB,CAAC,OAAO,EAAE,MAAM,SAAS,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAElI,8DAA8D;AAC9D,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAExD,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB,CAAC,OAAO;IAC9C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IACrF;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,sBAAsB,CAAC;CAC3C;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,gBAAgB,CAAC,OAAO,GAAG,IAAI,EAAE,MAAM,SAAS,WAAW,GAAG,SAAS;IAClF,oDAAoD;IACpD,SAAgB,OAAO,EAAE,sBAAsB,CAAC;IAEhD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsC;IACjE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmE;gBAE1E,SAAS,EAAE,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC;IAM7G,mDAAmD;IAC5C,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIpC;;;;;;;;;;;;;;OAcG;IACU,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;CAwEhH"}
@@ -0,0 +1,147 @@
1
+ import { mapObjectDescriptors } from "./MapObject.js";
2
+ import { MapSpawnSession } from "./MapSpawnSession.js";
3
+ import { logger } from "@codexo/exojs";
4
+
5
+ //#region src/MapObjectSpawner.ts
6
+ /**
7
+ * A spawn failed. The whole spawn is rolled back before this is thrown - no
8
+ * partial session exists.
9
+ *
10
+ * For `'factory-failed'` the error a factory raised is preserved as `cause`.
11
+ */
12
+ var MapSpawnError = class extends Error {
13
+ name = "MapSpawnError";
14
+ reason;
15
+ /** {@link MapObjectDescriptor.id} of the object that failed. */
16
+ objectId;
17
+ /** {@link MapObjectDescriptor.kind} of the object that failed. */
18
+ kind;
19
+ constructor(reason, objectId, kind, message, options) {
20
+ super(message, options);
21
+ this.reason = reason;
22
+ this.objectId = objectId;
23
+ this.kind = kind;
24
+ }
25
+ };
26
+ /**
27
+ * Turns the objects authored in a map into game objects.
28
+ *
29
+ * A spawner owns its own dispatch table, so several games, tests, editor
30
+ * previews or mods can run in one process without seeing each other's
31
+ * factories. There is no global registry to register into and nothing to reset
32
+ * between tests.
33
+ *
34
+ * ExoJS owns the mechanism: identity, ordering, cancellation, rollback and
35
+ * lifetime. The game owns the policy: which object becomes what, and what those
36
+ * objects need - dependencies travel through `Context`, not through any
37
+ * service locator in the engine.
38
+ *
39
+ * A spawn is **atomic**: if any factory fails, everything already created is
40
+ * destroyed in reverse order and no session is produced.
41
+ *
42
+ * @typeParam Context - the per-spawn value handed to every factory.
43
+ * @typeParam Result - what the factories produce. Defaults to `SceneNode`.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * interface GameContext {
48
+ * services: GameServices;
49
+ * save: SaveGame;
50
+ * }
51
+ *
52
+ * const spawner = new MapObjectSpawner<GameContext>({
53
+ * Enemy: (object, ctx) => new Enemy({ x: object.x, y: object.y, services: ctx.services }),
54
+ * Chest: (object, ctx) => new Chest({ id: object.id, save: ctx.save }),
55
+ * });
56
+ *
57
+ * const session = await spawner.spawn(map, { services, save });
58
+ * session.get('chest-in-the-cellar')?.open();
59
+ * session.destroy();
60
+ * ```
61
+ */
62
+ var MapObjectSpawner = class {
63
+ /** What to do with an object no factory matched. */
64
+ unknown;
65
+ _factories;
66
+ _identify;
67
+ constructor(factories, options) {
68
+ this._factories = factories;
69
+ this._identify = options?.identify ?? defaultIdentify;
70
+ this.unknown = options?.unknown ?? "ignore";
71
+ }
72
+ /** Whether a factory is registered under `key`. */
73
+ handles(key) {
74
+ return Object.prototype.hasOwnProperty.call(this._factories, key);
75
+ }
76
+ /**
77
+ * Create a game object for every object of `map` a factory matches, and
78
+ * return the {@link MapSpawnSession} that owns them.
79
+ *
80
+ * Objects are visited in object-layer order, then object order within each
81
+ * layer, and asynchronous factories are awaited in that same order - a
82
+ * factory that resolves early can never overtake one before it.
83
+ *
84
+ * @throws {MapSpawnError} when a factory fails, two objects share a stable id,
85
+ * or an unmatched object is found while `unknown` is `'error'`. In every case
86
+ * the objects already created are destroyed first. A factory that fails while
87
+ * the spawn is also being aborted reports the factory failure, not the abort -
88
+ * the more specific of the two.
89
+ * @throws {DOMException} named `AbortError` when `options.signal` aborts.
90
+ */
91
+ async spawn(map, context, options) {
92
+ const signal = options?.signal ?? neverAborts();
93
+ const entries = [];
94
+ const seen = /* @__PURE__ */ new Set();
95
+ try {
96
+ throwIfAborted(signal);
97
+ for (const object of mapObjectDescriptors(map)) {
98
+ if (seen.has(object.id)) throw new MapSpawnError("duplicate-id", object.id, object.kind, `MapObjectSpawner: two objects of this map resolve to the stable id "${object.id}"; ids must be unique within a map for session lookup and savegame restoration to work.`);
99
+ seen.add(object.id);
100
+ const key = this._identify(object, context);
101
+ const factory = key !== null && this.handles(key) ? this._factories[key] : void 0;
102
+ if (factory === void 0) {
103
+ if (this.unknown === "error") throw new MapSpawnError("unknown-kind", object.id, object.kind, `MapObjectSpawner: no factory registered for "${key ?? "<no kind>"}" (object "${object.id}" on layer "${object.layer.name}"). Register one, or set unknown: 'ignore'.`);
104
+ continue;
105
+ }
106
+ const result = await runFactory(factory, object, context, signal);
107
+ if (result !== null) entries.push([object.id, result]);
108
+ throwIfAborted(signal);
109
+ }
110
+ } catch (error) {
111
+ for (let i = entries.length - 1; i >= 0; i--) {
112
+ const entry = entries[i];
113
+ if (entry === void 0) continue;
114
+ try {
115
+ entry[1].destroy();
116
+ } catch (rollbackError) {
117
+ logger.error(`MapObjectSpawner: rolling back object "${entry[0]}" failed; continuing rollback.`, {
118
+ source: "tilemap",
119
+ ...rollbackError instanceof Error && { error: rollbackError }
120
+ });
121
+ }
122
+ }
123
+ throw error;
124
+ }
125
+ return new MapSpawnSession(entries);
126
+ }
127
+ };
128
+ const defaultIdentify = (object) => object.kind;
129
+ const runFactory = async (factory, object, context, signal) => {
130
+ try {
131
+ return await factory(object, context, signal);
132
+ } catch (error) {
133
+ throw new MapSpawnError("factory-failed", object.id, object.kind, `MapObjectSpawner: the factory for "${object.kind ?? "<no kind>"}" failed on object "${object.id}" of layer "${object.layer.name}".`, { cause: error });
134
+ }
135
+ };
136
+ const throwIfAborted = (signal) => {
137
+ if (signal.aborted) throw new DOMException("Map object spawn was cancelled.", "AbortError");
138
+ };
139
+ let sharedNeverAborts;
140
+ const neverAborts = () => {
141
+ sharedNeverAborts ??= new AbortController().signal;
142
+ return sharedNeverAborts;
143
+ };
144
+
145
+ //#endregion
146
+ export { MapObjectSpawner, MapSpawnError };
147
+ //# sourceMappingURL=MapObjectSpawner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapObjectSpawner.js","names":[],"sources":["../../src/MapObjectSpawner.ts"],"sourcesContent":["import type { Destroyable, SceneNode } from '@codexo/exojs';\nimport { logger } from '@codexo/exojs';\n\nimport type { MapObjectDescriptor } from './MapObject';\nimport { mapObjectDescriptors } from './MapObject';\nimport { MapSpawnSession } from './MapSpawnSession';\nimport type { TileMap } from './TileMap';\n\n/** Why a {@link MapSpawnError} was raised. */\nexport type MapSpawnErrorReason =\n /** No factory is registered for the object's dispatch key and `unknown` is `'error'`. */\n | 'unknown-kind'\n /** Two objects of the same map resolved to the same stable id. */\n | 'duplicate-id'\n /** A factory threw or rejected. The original error is the `cause`. */\n | 'factory-failed';\n\n/**\n * A spawn failed. The whole spawn is rolled back before this is thrown - no\n * partial session exists.\n *\n * For `'factory-failed'` the error a factory raised is preserved as `cause`.\n */\nexport class MapSpawnError extends Error {\n public override readonly name = 'MapSpawnError';\n public readonly reason: MapSpawnErrorReason;\n /** {@link MapObjectDescriptor.id} of the object that failed. */\n public readonly objectId: string;\n /** {@link MapObjectDescriptor.kind} of the object that failed. */\n public readonly kind: string | null;\n\n public constructor(reason: MapSpawnErrorReason, objectId: string, kind: string | null, message: string, options?: ErrorOptions) {\n super(message, options);\n this.reason = reason;\n this.objectId = objectId;\n this.kind = kind;\n }\n}\n\n/**\n * Creates one runtime object from one map object, or `null` to deliberately\n * create nothing for it.\n *\n * `signal` aborts with the spawn it belongs to; a factory that loads assets or\n * awaits anything else should pass it on, so a level unloaded mid-load stops\n * its own work too.\n */\nexport type MapObjectFactory<Context, Result extends Destroyable> = (\n object: MapObjectDescriptor,\n context: Context,\n signal: AbortSignal,\n) => Result | null | Promise<Result | null>;\n\n/** Dispatch key to factory. Keys are matched against the spawner's `identify` result. */\nexport type MapObjectFactories<Context, Result extends Destroyable> = Readonly<Record<string, MapObjectFactory<Context, Result>>>;\n\n/** What to do with an object no factory is registered for. */\nexport type UnknownMapObjectPolicy = 'ignore' | 'error';\n\n/** Options for {@link MapObjectSpawner}. */\nexport interface MapObjectSpawnerOptions<Context> {\n /**\n * Picks the factory key for an object, or returns `null` to treat it as\n * unknown. Defaults to {@link MapObjectDescriptor.kind}.\n *\n * This is where project-specific dispatch policy lives - keying on a\n * property, a name prefix, or a compound key:\n *\n * ```ts\n * identify: object => `${object.kind}:${object.properties.variant}`\n * ```\n */\n readonly identify?: (object: MapObjectDescriptor, context: Context) => string | null;\n /**\n * What to do with an object no factory matched. Defaults to `'ignore'`,\n * because maps legitimately carry editor markers, decoration and metadata\n * objects that no game entity corresponds to. Use `'error'` for a map format\n * the project fully controls and wants validated.\n */\n readonly unknown?: UnknownMapObjectPolicy;\n}\n\n/** Options for {@link MapObjectSpawner.spawn}. */\nexport interface MapSpawnOptions {\n /**\n * Cancels the spawn. An in-flight factory is still awaited - abandoning it\n * would leak whatever it eventually produced - but its result is destroyed\n * along with the rest of the rollback.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * Turns the objects authored in a map into game objects.\n *\n * A spawner owns its own dispatch table, so several games, tests, editor\n * previews or mods can run in one process without seeing each other's\n * factories. There is no global registry to register into and nothing to reset\n * between tests.\n *\n * ExoJS owns the mechanism: identity, ordering, cancellation, rollback and\n * lifetime. The game owns the policy: which object becomes what, and what those\n * objects need - dependencies travel through `Context`, not through any\n * service locator in the engine.\n *\n * A spawn is **atomic**: if any factory fails, everything already created is\n * destroyed in reverse order and no session is produced.\n *\n * @typeParam Context - the per-spawn value handed to every factory.\n * @typeParam Result - what the factories produce. Defaults to `SceneNode`.\n *\n * @example\n * ```ts\n * interface GameContext {\n * services: GameServices;\n * save: SaveGame;\n * }\n *\n * const spawner = new MapObjectSpawner<GameContext>({\n * Enemy: (object, ctx) => new Enemy({ x: object.x, y: object.y, services: ctx.services }),\n * Chest: (object, ctx) => new Chest({ id: object.id, save: ctx.save }),\n * });\n *\n * const session = await spawner.spawn(map, { services, save });\n * session.get('chest-in-the-cellar')?.open();\n * session.destroy();\n * ```\n */\nexport class MapObjectSpawner<Context = void, Result extends Destroyable = SceneNode> {\n /** What to do with an object no factory matched. */\n public readonly unknown: UnknownMapObjectPolicy;\n\n private readonly _factories: MapObjectFactories<Context, Result>;\n private readonly _identify: (object: MapObjectDescriptor, context: Context) => string | null;\n\n public constructor(factories: MapObjectFactories<Context, Result>, options?: MapObjectSpawnerOptions<Context>) {\n this._factories = factories;\n this._identify = options?.identify ?? defaultIdentify;\n this.unknown = options?.unknown ?? 'ignore';\n }\n\n /** Whether a factory is registered under `key`. */\n public handles(key: string): boolean {\n return Object.prototype.hasOwnProperty.call(this._factories, key);\n }\n\n /**\n * Create a game object for every object of `map` a factory matches, and\n * return the {@link MapSpawnSession} that owns them.\n *\n * Objects are visited in object-layer order, then object order within each\n * layer, and asynchronous factories are awaited in that same order - a\n * factory that resolves early can never overtake one before it.\n *\n * @throws {MapSpawnError} when a factory fails, two objects share a stable id,\n * or an unmatched object is found while `unknown` is `'error'`. In every case\n * the objects already created are destroyed first. A factory that fails while\n * the spawn is also being aborted reports the factory failure, not the abort -\n * the more specific of the two.\n * @throws {DOMException} named `AbortError` when `options.signal` aborts.\n */\n public async spawn(map: TileMap, context: Context, options?: MapSpawnOptions): Promise<MapSpawnSession<Result>> {\n const signal = options?.signal ?? neverAborts();\n const entries: Array<readonly [id: string, result: Result]> = [];\n const seen = new Set<string>();\n\n try {\n throwIfAborted(signal);\n\n for (const object of mapObjectDescriptors(map)) {\n if (seen.has(object.id)) {\n throw new MapSpawnError(\n 'duplicate-id',\n object.id,\n object.kind,\n `MapObjectSpawner: two objects of this map resolve to the stable id \"${object.id}\"; ` +\n 'ids must be unique within a map for session lookup and savegame restoration to work.',\n );\n }\n\n seen.add(object.id);\n\n const key = this._identify(object, context);\n // Own-property check before indexing: a map is free to name an object\n // class \"toString\" or \"constructor\", and a bare index would resolve\n // those against Object.prototype and call a function that is not a\n // factory at all.\n const factory = key !== null && this.handles(key) ? this._factories[key] : undefined;\n\n if (factory === undefined) {\n if (this.unknown === 'error') {\n throw new MapSpawnError(\n 'unknown-kind',\n object.id,\n object.kind,\n `MapObjectSpawner: no factory registered for \"${key ?? '<no kind>'}\" (object \"${object.id}\" ` +\n `on layer \"${object.layer.name}\"). Register one, or set unknown: 'ignore'.`,\n );\n }\n\n continue;\n }\n\n const result = await runFactory(factory, object, context, signal);\n\n // Push before the abort check so a result that arrived after the abort\n // is rolled back with the rest instead of being left unowned.\n if (result !== null) entries.push([object.id, result]);\n\n throwIfAborted(signal);\n }\n } catch (error) {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n if (entry === undefined) continue;\n\n // A failing rollback step must neither replace the error that caused\n // the rollback nor strand the objects before it.\n try {\n entry[1].destroy();\n } catch (rollbackError) {\n logger.error(`MapObjectSpawner: rolling back object \"${entry[0]}\" failed; continuing rollback.`, {\n source: 'tilemap',\n ...(rollbackError instanceof Error && { error: rollbackError }),\n });\n }\n }\n\n throw error;\n }\n\n return new MapSpawnSession(entries);\n }\n}\n\nconst defaultIdentify = (object: MapObjectDescriptor): string | null => object.kind;\n\nconst runFactory = async <Context, Result extends Destroyable>(\n factory: MapObjectFactory<Context, Result>,\n object: MapObjectDescriptor,\n context: Context,\n signal: AbortSignal,\n): Promise<Result | null> => {\n try {\n return await factory(object, context, signal);\n } catch (error) {\n throw new MapSpawnError(\n 'factory-failed',\n object.id,\n object.kind,\n `MapObjectSpawner: the factory for \"${object.kind ?? '<no kind>'}\" failed on object \"${object.id}\" ` + `of layer \"${object.layer.name}\".`,\n { cause: error },\n );\n }\n};\n\nconst throwIfAborted = (signal: AbortSignal): void => {\n if (signal.aborted) {\n throw new DOMException('Map object spawn was cancelled.', 'AbortError');\n }\n};\n\n// One shared never-aborting signal: `spawn` without a signal is the common\n// case, and a fresh AbortController per spawn would allocate for nothing.\nlet sharedNeverAborts: AbortSignal | undefined;\n\nconst neverAborts = (): AbortSignal => {\n sharedNeverAborts ??= new AbortController().signal;\n return sharedNeverAborts;\n};\n"],"mappings":";;;;;;;;;;;AAuBA,IAAa,gBAAb,cAAmC,MAAM;CACvC,AAAyB,OAAO;CAChC,AAAgB;;CAEhB,AAAgB;;CAEhB,AAAgB;CAEhB,AAAO,YAAY,QAA6B,UAAkB,MAAqB,SAAiB,SAAwB;EAC9H,MAAM,SAAS,OAAO;EACtB,KAAK,SAAS;EACd,KAAK,WAAW;EAChB,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2FA,IAAa,mBAAb,MAAsF;;CAEpF,AAAgB;CAEhB,AAAiB;CACjB,AAAiB;CAEjB,AAAO,YAAY,WAAgD,SAA4C;EAC7G,KAAK,aAAa;EAClB,KAAK,YAAY,SAAS,YAAY;EACtC,KAAK,UAAU,SAAS,WAAW;CACrC;;CAGA,AAAO,QAAQ,KAAsB;EACnC,OAAO,OAAO,UAAU,eAAe,KAAK,KAAK,YAAY,GAAG;CAClE;;;;;;;;;;;;;;;;CAiBA,MAAa,MAAM,KAAc,SAAkB,SAA6D;EAC9G,MAAM,SAAS,SAAS,UAAU,YAAY;EAC9C,MAAM,UAAwD,CAAC;EAC/D,MAAM,uBAAO,IAAI,IAAY;EAE7B,IAAI;GACF,eAAe,MAAM;GAErB,KAAK,MAAM,UAAU,qBAAqB,GAAG,GAAG;IAC9C,IAAI,KAAK,IAAI,OAAO,EAAE,GACpB,MAAM,IAAI,cACR,gBACA,OAAO,IACP,OAAO,MACP,uEAAuE,OAAO,GAAG,wFAEnF;IAGF,KAAK,IAAI,OAAO,EAAE;IAElB,MAAM,MAAM,KAAK,UAAU,QAAQ,OAAO;IAK1C,MAAM,UAAU,QAAQ,QAAQ,KAAK,QAAQ,GAAG,IAAI,KAAK,WAAW,OAAO;IAE3E,IAAI,YAAY,QAAW;KACzB,IAAI,KAAK,YAAY,SACnB,MAAM,IAAI,cACR,gBACA,OAAO,IACP,OAAO,MACP,gDAAgD,OAAO,YAAY,aAAa,OAAO,GAAG,cAC3E,OAAO,MAAM,KAAK,4CACnC;KAGF;IACF;IAEA,MAAM,SAAS,MAAM,WAAW,SAAS,QAAQ,SAAS,MAAM;IAIhE,IAAI,WAAW,MAAM,QAAQ,KAAK,CAAC,OAAO,IAAI,MAAM,CAAC;IAErD,eAAe,MAAM;GACvB;EACF,SAAS,OAAO;GACd,KAAK,IAAI,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;IAC5C,MAAM,QAAQ,QAAQ;IACtB,IAAI,UAAU,QAAW;IAIzB,IAAI;KACF,MAAM,EAAE,CAAC,QAAQ;IACnB,SAAS,eAAe;KACtB,OAAO,MAAM,0CAA0C,MAAM,GAAG,iCAAiC;MAC/F,QAAQ;MACR,GAAI,yBAAyB,SAAS,EAAE,OAAO,cAAc;KAC/D,CAAC;IACH;GACF;GAEA,MAAM;EACR;EAEA,OAAO,IAAI,gBAAgB,OAAO;CACpC;AACF;AAEA,MAAM,mBAAmB,WAA+C,OAAO;AAE/E,MAAM,aAAa,OACjB,SACA,QACA,SACA,WAC2B;CAC3B,IAAI;EACF,OAAO,MAAM,QAAQ,QAAQ,SAAS,MAAM;CAC9C,SAAS,OAAO;EACd,MAAM,IAAI,cACR,kBACA,OAAO,IACP,OAAO,MACP,sCAAsC,OAAO,QAAQ,YAAY,sBAAsB,OAAO,GAAG,cAAmB,OAAO,MAAM,KAAK,KACtI,EAAE,OAAO,MAAM,CACjB;CACF;AACF;AAEA,MAAM,kBAAkB,WAA8B;CACpD,IAAI,OAAO,SACT,MAAM,IAAI,aAAa,mCAAmC,YAAY;AAE1E;AAIA,IAAI;AAEJ,MAAM,oBAAiC;CACrC,sBAAsB,IAAI,gBAAgB,CAAC,CAAC;CAC5C,OAAO;AACT"}
@@ -0,0 +1,53 @@
1
+ import type { Destroyable } from '@codexo/exojs';
2
+ /**
3
+ * The objects one spawn produced, and their shared lifetime.
4
+ *
5
+ * A session is handed out by
6
+ * {@link import('./MapObjectSpawner').MapObjectSpawner.spawn} and owns every
7
+ * result in it. Destroying the session destroys them all; nothing else has to
8
+ * be tracked, and nothing scans the scene to find them again.
9
+ *
10
+ * A session is not reusable - destroy it and spawn again.
11
+ *
12
+ * @typeParam Result - what the factories produced.
13
+ */
14
+ export declare class MapSpawnSession<Result extends Destroyable> implements Destroyable {
15
+ /**
16
+ * The spawned objects in spawn order: object-layer order, then object order
17
+ * within each layer. Objects whose factory returned `null` are not in here.
18
+ */
19
+ readonly objects: readonly Result[];
20
+ private readonly _byId;
21
+ private _destroyed;
22
+ /**
23
+ * Sessions are produced by
24
+ * {@link import('./MapObjectSpawner').MapObjectSpawner.spawn}, not
25
+ * constructed directly.
26
+ * @internal
27
+ */
28
+ constructor(entries: ReadonlyArray<readonly [id: string, result: Result]>);
29
+ /** Whether {@link destroy} has run. */
30
+ get destroyed(): boolean;
31
+ /**
32
+ * The object spawned from the source object with this
33
+ * {@link import('./MapObject').MapObjectDescriptor.id}, or `undefined` when
34
+ * no object with that id spawned anything. Constant time.
35
+ *
36
+ * The ids are the source-stable ones, which is what makes this the hook for
37
+ * savegame restoration: persist the id, look the runtime object back up.
38
+ */
39
+ get(sourceId: string): Result | undefined;
40
+ /** Whether {@link get} would find an object for `sourceId`. */
41
+ has(sourceId: string): boolean;
42
+ /**
43
+ * Destroy every spawned object, in reverse spawn order. Idempotent;
44
+ * {@link objects} still lists what the session held.
45
+ *
46
+ * Reverse order is the contract, not an implementation detail: an object
47
+ * spawned later may have attached itself to an earlier one (a turret to its
48
+ * platform, a light to its lamp), and tearing down the dependent first is
49
+ * the order that never observes a half-destroyed owner.
50
+ */
51
+ destroy(): void;
52
+ }
53
+ //# sourceMappingURL=MapSpawnSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapSpawnSession.d.ts","sourceRoot":"","sources":["../../src/MapSpawnSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD;;;;;;;;;;;GAWG;AACH,qBAAa,eAAe,CAAC,MAAM,SAAS,WAAW,CAAE,YAAW,WAAW;IAC7E;;;OAGG;IACH,SAAgB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAE3C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;OAKG;gBACgB,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAahF,uCAAuC;IACvC,IAAW,SAAS,IAAI,OAAO,CAE9B;IAED;;;;;;;OAOG;IACI,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD,+DAA+D;IACxD,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIrC;;;;;;;;OAQG;IACI,OAAO,IAAI,IAAI;CAqBvB"}
@@ -0,0 +1,88 @@
1
+ import { logger } from "@codexo/exojs";
2
+
3
+ //#region src/MapSpawnSession.ts
4
+ /**
5
+ * The objects one spawn produced, and their shared lifetime.
6
+ *
7
+ * A session is handed out by
8
+ * {@link import('./MapObjectSpawner').MapObjectSpawner.spawn} and owns every
9
+ * result in it. Destroying the session destroys them all; nothing else has to
10
+ * be tracked, and nothing scans the scene to find them again.
11
+ *
12
+ * A session is not reusable - destroy it and spawn again.
13
+ *
14
+ * @typeParam Result - what the factories produced.
15
+ */
16
+ var MapSpawnSession = class {
17
+ /**
18
+ * The spawned objects in spawn order: object-layer order, then object order
19
+ * within each layer. Objects whose factory returned `null` are not in here.
20
+ */
21
+ objects;
22
+ _byId;
23
+ _destroyed = false;
24
+ /**
25
+ * Sessions are produced by
26
+ * {@link import('./MapObjectSpawner').MapObjectSpawner.spawn}, not
27
+ * constructed directly.
28
+ * @internal
29
+ */
30
+ constructor(entries) {
31
+ const objects = [];
32
+ const byId = /* @__PURE__ */ new Map();
33
+ for (const [id, result] of entries) {
34
+ objects.push(result);
35
+ byId.set(id, result);
36
+ }
37
+ this.objects = Object.freeze(objects);
38
+ this._byId = byId;
39
+ }
40
+ /** Whether {@link destroy} has run. */
41
+ get destroyed() {
42
+ return this._destroyed;
43
+ }
44
+ /**
45
+ * The object spawned from the source object with this
46
+ * {@link import('./MapObject').MapObjectDescriptor.id}, or `undefined` when
47
+ * no object with that id spawned anything. Constant time.
48
+ *
49
+ * The ids are the source-stable ones, which is what makes this the hook for
50
+ * savegame restoration: persist the id, look the runtime object back up.
51
+ */
52
+ get(sourceId) {
53
+ return this._byId.get(sourceId);
54
+ }
55
+ /** Whether {@link get} would find an object for `sourceId`. */
56
+ has(sourceId) {
57
+ return this._byId.has(sourceId);
58
+ }
59
+ /**
60
+ * Destroy every spawned object, in reverse spawn order. Idempotent;
61
+ * {@link objects} still lists what the session held.
62
+ *
63
+ * Reverse order is the contract, not an implementation detail: an object
64
+ * spawned later may have attached itself to an earlier one (a turret to its
65
+ * platform, a light to its lamp), and tearing down the dependent first is
66
+ * the order that never observes a half-destroyed owner.
67
+ */
68
+ destroy() {
69
+ if (this._destroyed) return;
70
+ this._destroyed = true;
71
+ for (let i = this.objects.length - 1; i >= 0; i--) {
72
+ const object = this.objects[i];
73
+ if (object === void 0) continue;
74
+ try {
75
+ object.destroy();
76
+ } catch (error) {
77
+ logger.error("MapSpawnSession: destroying a spawned object failed; continuing teardown.", {
78
+ source: "tilemap",
79
+ ...error instanceof Error && { error }
80
+ });
81
+ }
82
+ }
83
+ }
84
+ };
85
+
86
+ //#endregion
87
+ export { MapSpawnSession };
88
+ //# sourceMappingURL=MapSpawnSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapSpawnSession.js","names":[],"sources":["../../src/MapSpawnSession.ts"],"sourcesContent":["import type { Destroyable } from '@codexo/exojs';\nimport { logger } from '@codexo/exojs';\n\n/**\n * The objects one spawn produced, and their shared lifetime.\n *\n * A session is handed out by\n * {@link import('./MapObjectSpawner').MapObjectSpawner.spawn} and owns every\n * result in it. Destroying the session destroys them all; nothing else has to\n * be tracked, and nothing scans the scene to find them again.\n *\n * A session is not reusable - destroy it and spawn again.\n *\n * @typeParam Result - what the factories produced.\n */\nexport class MapSpawnSession<Result extends Destroyable> implements Destroyable {\n /**\n * The spawned objects in spawn order: object-layer order, then object order\n * within each layer. Objects whose factory returned `null` are not in here.\n */\n public readonly objects: readonly Result[];\n\n private readonly _byId: ReadonlyMap<string, Result>;\n private _destroyed = false;\n\n /**\n * Sessions are produced by\n * {@link import('./MapObjectSpawner').MapObjectSpawner.spawn}, not\n * constructed directly.\n * @internal\n */\n public constructor(entries: ReadonlyArray<readonly [id: string, result: Result]>) {\n const objects: Result[] = [];\n const byId = new Map<string, Result>();\n\n for (const [id, result] of entries) {\n objects.push(result);\n byId.set(id, result);\n }\n\n this.objects = Object.freeze(objects);\n this._byId = byId;\n }\n\n /** Whether {@link destroy} has run. */\n public get destroyed(): boolean {\n return this._destroyed;\n }\n\n /**\n * The object spawned from the source object with this\n * {@link import('./MapObject').MapObjectDescriptor.id}, or `undefined` when\n * no object with that id spawned anything. Constant time.\n *\n * The ids are the source-stable ones, which is what makes this the hook for\n * savegame restoration: persist the id, look the runtime object back up.\n */\n public get(sourceId: string): Result | undefined {\n return this._byId.get(sourceId);\n }\n\n /** Whether {@link get} would find an object for `sourceId`. */\n public has(sourceId: string): boolean {\n return this._byId.has(sourceId);\n }\n\n /**\n * Destroy every spawned object, in reverse spawn order. Idempotent;\n * {@link objects} still lists what the session held.\n *\n * Reverse order is the contract, not an implementation detail: an object\n * spawned later may have attached itself to an earlier one (a turret to its\n * platform, a light to its lamp), and tearing down the dependent first is\n * the order that never observes a half-destroyed owner.\n */\n public destroy(): void {\n if (this._destroyed) return;\n\n this._destroyed = true;\n\n for (let i = this.objects.length - 1; i >= 0; i--) {\n const object = this.objects[i];\n if (object === undefined) continue;\n\n // One object's failure must not strand the rest: the session is single\n // use, so anything skipped here is never destroyed at all.\n try {\n object.destroy();\n } catch (error) {\n logger.error('MapSpawnSession: destroying a spawned object failed; continuing teardown.', {\n source: 'tilemap',\n ...(error instanceof Error && { error }),\n });\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,IAAa,kBAAb,MAAgF;;;;;CAK9E,AAAgB;CAEhB,AAAiB;CACjB,AAAQ,aAAa;;;;;;;CAQrB,AAAO,YAAY,SAA+D;EAChF,MAAM,UAAoB,CAAC;EAC3B,MAAM,uBAAO,IAAI,IAAoB;EAErC,KAAK,MAAM,CAAC,IAAI,WAAW,SAAS;GAClC,QAAQ,KAAK,MAAM;GACnB,KAAK,IAAI,IAAI,MAAM;EACrB;EAEA,KAAK,UAAU,OAAO,OAAO,OAAO;EACpC,KAAK,QAAQ;CACf;;CAGA,IAAW,YAAqB;EAC9B,OAAO,KAAK;CACd;;;;;;;;;CAUA,AAAO,IAAI,UAAsC;EAC/C,OAAO,KAAK,MAAM,IAAI,QAAQ;CAChC;;CAGA,AAAO,IAAI,UAA2B;EACpC,OAAO,KAAK,MAAM,IAAI,QAAQ;CAChC;;;;;;;;;;CAWA,AAAO,UAAgB;EACrB,IAAI,KAAK,YAAY;EAErB,KAAK,aAAa;EAElB,KAAK,IAAI,IAAI,KAAK,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;GACjD,MAAM,SAAS,KAAK,QAAQ;GAC5B,IAAI,WAAW,QAAW;GAI1B,IAAI;IACF,OAAO,QAAQ;GACjB,SAAS,OAAO;IACd,OAAO,MAAM,6EAA6E;KACxF,QAAQ;KACR,GAAI,iBAAiB,SAAS,EAAE,MAAM;IACxC,CAAC;GACH;EACF;CACF;AACF"}
@@ -0,0 +1,134 @@
1
+ import type { TileProperties } from './types';
2
+ /** An axis-aligned rectangle in world pixel space (+Y down). */
3
+ export interface MapBounds {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ readonly width: number;
7
+ readonly height: number;
8
+ }
9
+ /**
10
+ * Where a neighbouring level sits relative to the level that names it.
11
+ *
12
+ * `Overlap`, `Below` and `Above` describe depth relationships rather than
13
+ * cardinal directions - a source format that stacks levels (LDtk's depth
14
+ * layers) reports those. `Unknown` preserves an adjacency whose direction code
15
+ * this version does not recognise: the levels are neighbours, but the side is
16
+ * not interpretable.
17
+ */
18
+ export declare const MapLevelSide: {
19
+ readonly North: "north";
20
+ readonly South: "south";
21
+ readonly West: "west";
22
+ readonly East: "east";
23
+ readonly Overlap: "overlap";
24
+ readonly Below: "below";
25
+ readonly Above: "above";
26
+ readonly Unknown: "unknown";
27
+ };
28
+ /** Side discriminant for a {@link MapLevelNeighbour}. */
29
+ export type MapLevelSide = (typeof MapLevelSide)[keyof typeof MapLevelSide];
30
+ /** One adjacency edge from a level to another level in the same {@link MapWorld}. */
31
+ export interface MapLevelNeighbour {
32
+ /** {@link MapLevel.id} of the neighbouring level. */
33
+ readonly id: string;
34
+ readonly side: MapLevelSide;
35
+ }
36
+ /**
37
+ * Immutable metadata for one level of a {@link MapWorld} - everything needed to
38
+ * decide whether to load it, without loading it.
39
+ */
40
+ export interface MapLevel {
41
+ /**
42
+ * Stable identity, unique within the world. Carried from the source format
43
+ * (LDtk uses the level `iid`), so it survives re-ordering and re-export and
44
+ * is safe to persist in a savegame.
45
+ */
46
+ readonly id: string;
47
+ /** Human-readable level name. Not guaranteed unique. */
48
+ readonly name: string;
49
+ /** Position in source document order. */
50
+ readonly index: number;
51
+ /** Placement and size in world pixel space. */
52
+ readonly bounds: MapBounds;
53
+ /**
54
+ * Whether the level's layer payload lives outside the root document and has
55
+ * to be fetched separately when the level is loaded.
56
+ */
57
+ readonly external: boolean;
58
+ /** Adjacency edges declared by the source format; empty when it declares none. */
59
+ readonly neighbours: readonly MapLevelNeighbour[];
60
+ /** Level-level custom properties. */
61
+ readonly properties: TileProperties;
62
+ }
63
+ /** Construction options for a {@link MapWorld}. */
64
+ export interface MapWorldOptions {
65
+ /** World name. Defaults to an empty string. */
66
+ readonly name?: string;
67
+ /** The world's levels, in source document order. */
68
+ readonly levels: readonly MapLevel[];
69
+ }
70
+ /**
71
+ * A format-neutral description of how levels are laid out in a world.
72
+ *
73
+ * A `MapWorld` holds metadata only - no tiles, no textures, no runtime maps.
74
+ * It is what a game reads to decide which levels it needs; loading them is
75
+ * {@link import('./MapWorldRuntime').MapWorldRuntime}'s job.
76
+ *
77
+ * Adapters build one from their source document
78
+ * (`@codexo/exojs-ldtk` does so from a `.ldtk` project). A format with no world
79
+ * concept of its own - Tiled, today - can be described by constructing one
80
+ * directly from whatever placement data the game keeps.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const world = new MapWorld({
85
+ * name: 'overworld',
86
+ * levels: [
87
+ * { id: 'forest', name: 'Forest', index: 0, external: false,
88
+ * bounds: { x: 0, y: 0, width: 512, height: 512 },
89
+ * neighbours: [{ id: 'cave', side: MapLevelSide.East }], properties: {} },
90
+ * ],
91
+ * });
92
+ * ```
93
+ */
94
+ export declare class MapWorld {
95
+ /** World name; empty when the source format does not name it. */
96
+ readonly name: string;
97
+ /** The world's levels, in source document order. */
98
+ readonly levels: readonly MapLevel[];
99
+ private readonly _byId;
100
+ private readonly _byName;
101
+ private _bounds?;
102
+ constructor(options: MapWorldOptions);
103
+ /**
104
+ * Union of every level's bounds, or a zero rectangle for an empty world.
105
+ * Computed once and cached - a `MapWorld` is immutable.
106
+ */
107
+ get bounds(): MapBounds;
108
+ /** The level with this {@link MapLevel.id}, or `undefined`. Constant time. */
109
+ getLevel(id: string): MapLevel | undefined;
110
+ /**
111
+ * The first level in document order whose {@link MapLevel.name} matches, or
112
+ * `undefined`. Constant time. Names are not unique by contract.
113
+ */
114
+ getLevelByName(name: string): MapLevel | undefined;
115
+ /**
116
+ * The levels `id` declares as adjacent, resolved to their metadata and in the
117
+ * order the source format declared them.
118
+ *
119
+ * A neighbour naming a level this world does not contain is skipped - LDtk
120
+ * can reference across worlds of a multi-world project, and each of those
121
+ * worlds is its own `MapWorld`. Returns an empty array for an unknown `id`.
122
+ */
123
+ getNeighbours(id: string): readonly MapLevel[];
124
+ /**
125
+ * Every level whose bounds intersect `bounds`, in document order. Edge
126
+ * contact alone does not count as an intersection, so two levels laid out
127
+ * side by side are not reported for a query that only touches the seam.
128
+ *
129
+ * Linear in the number of levels - intended for a per-camera-move query over
130
+ * a world of hundreds of levels, not for a per-frame query over thousands.
131
+ */
132
+ getLevelsInBounds(bounds: MapBounds): MapLevel[];
133
+ }
134
+ //# sourceMappingURL=MapWorld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapWorld.d.ts","sourceRoot":"","sources":["../../src/MapWorld.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,gEAAgE;AAChE,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY;;;;;;;;;CASf,CAAC;AAEX,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAE5E,qFAAqF;AACrF,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAClD,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC;CACrC;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC;AAID;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,QAAQ;IACnB,iEAAiE;IACjE,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,oDAAoD;IACpD,SAAgB,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;IAE5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,OAAO,CAAC,CAAY;gBAET,OAAO,EAAE,eAAe;IAsB3C;;;OAGG;IACH,IAAW,MAAM,IAAI,SAAS,CAuB7B;IAED,8EAA8E;IACvE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIjD;;;OAGG;IACI,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIzD;;;;;;;OAOG;IACI,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,QAAQ,EAAE;IAcrD;;;;;;;OAOG;IACI,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,EAAE;CAYxD"}