@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.
- package/README.md +33 -26
- package/dist/esm/ChunkSource.d.ts +33 -0
- package/dist/esm/ChunkSource.d.ts.map +1 -0
- package/dist/esm/ChunkStreamer.d.ts +98 -0
- package/dist/esm/ChunkStreamer.d.ts.map +1 -0
- package/dist/esm/ChunkStreamer.js +201 -0
- package/dist/esm/ChunkStreamer.js.map +1 -0
- package/dist/esm/ImageLayer.d.ts +11 -3
- package/dist/esm/ImageLayer.d.ts.map +1 -0
- package/dist/esm/ImageLayer.js +71 -64
- package/dist/esm/ImageLayer.js.map +1 -1
- package/dist/esm/ImageLayerNode.d.ts +89 -0
- package/dist/esm/ImageLayerNode.d.ts.map +1 -0
- package/dist/esm/ImageLayerNode.js +205 -0
- package/dist/esm/ImageLayerNode.js.map +1 -0
- package/dist/esm/MapObject.d.ts +70 -0
- package/dist/esm/MapObject.d.ts.map +1 -0
- package/dist/esm/MapObject.js +34 -0
- package/dist/esm/MapObject.js.map +1 -0
- package/dist/esm/MapObjectSpawner.d.ts +133 -0
- package/dist/esm/MapObjectSpawner.d.ts.map +1 -0
- package/dist/esm/MapObjectSpawner.js +147 -0
- package/dist/esm/MapObjectSpawner.js.map +1 -0
- package/dist/esm/MapSpawnSession.d.ts +53 -0
- package/dist/esm/MapSpawnSession.d.ts.map +1 -0
- package/dist/esm/MapSpawnSession.js +88 -0
- package/dist/esm/MapSpawnSession.js.map +1 -0
- package/dist/esm/MapWorld.d.ts +134 -0
- package/dist/esm/MapWorld.d.ts.map +1 -0
- package/dist/esm/MapWorld.js +150 -0
- package/dist/esm/MapWorld.js.map +1 -0
- package/dist/esm/MapWorldRuntime.d.ts +182 -0
- package/dist/esm/MapWorldRuntime.d.ts.map +1 -0
- package/dist/esm/MapWorldRuntime.js +261 -0
- package/dist/esm/MapWorldRuntime.js.map +1 -0
- package/dist/esm/ObjectLayer.d.ts +93 -7
- package/dist/esm/ObjectLayer.d.ts.map +1 -0
- package/dist/esm/ObjectLayer.js +184 -167
- package/dist/esm/ObjectLayer.js.map +1 -1
- package/dist/esm/SampledChunkSource.d.ts +43 -0
- package/dist/esm/SampledChunkSource.d.ts.map +1 -0
- package/dist/esm/SampledChunkSource.js +48 -0
- package/dist/esm/SampledChunkSource.js.map +1 -0
- package/dist/esm/TileAnimator.d.ts +12 -2
- package/dist/esm/TileAnimator.d.ts.map +1 -0
- package/dist/esm/TileAnimator.js +154 -165
- package/dist/esm/TileAnimator.js.map +1 -1
- package/dist/esm/TileChunk.d.ts +30 -5
- package/dist/esm/TileChunk.d.ts.map +1 -0
- package/dist/esm/TileChunk.js +190 -188
- package/dist/esm/TileChunk.js.map +1 -1
- package/dist/esm/TileChunkNode.d.ts +18 -3
- package/dist/esm/TileChunkNode.d.ts.map +1 -0
- package/dist/esm/TileChunkNode.js +104 -85
- package/dist/esm/TileChunkNode.js.map +1 -1
- package/dist/esm/TileLayer.d.ts +104 -24
- package/dist/esm/TileLayer.d.ts.map +1 -0
- package/dist/esm/TileLayer.js +511 -469
- package/dist/esm/TileLayer.js.map +1 -1
- package/dist/esm/TileLayerNode.d.ts +52 -11
- package/dist/esm/TileLayerNode.d.ts.map +1 -0
- package/dist/esm/TileLayerNode.js +224 -166
- package/dist/esm/TileLayerNode.js.map +1 -1
- package/dist/esm/TileMap.d.ts +96 -31
- package/dist/esm/TileMap.d.ts.map +1 -0
- package/dist/esm/TileMap.js +401 -357
- package/dist/esm/TileMap.js.map +1 -1
- package/dist/esm/TileMapBand.d.ts +42 -29
- package/dist/esm/TileMapBand.d.ts.map +1 -0
- package/dist/esm/TileMapBand.js +151 -158
- package/dist/esm/TileMapBand.js.map +1 -1
- package/dist/esm/TileMapNode.d.ts +34 -18
- package/dist/esm/TileMapNode.d.ts.map +1 -0
- package/dist/esm/TileMapNode.js +122 -109
- package/dist/esm/TileMapNode.js.map +1 -1
- package/dist/esm/TileMapView.d.ts +108 -47
- package/dist/esm/TileMapView.d.ts.map +1 -0
- package/dist/esm/TileMapView.js +385 -338
- package/dist/esm/TileMapView.js.map +1 -1
- package/dist/esm/TileSet.d.ts +15 -6
- package/dist/esm/TileSet.d.ts.map +1 -0
- package/dist/esm/TileSet.js +161 -181
- package/dist/esm/TileSet.js.map +1 -1
- package/dist/esm/WangSet.d.ts +5 -4
- package/dist/esm/WangSet.d.ts.map +1 -0
- package/dist/esm/WangSet.js +70 -78
- package/dist/esm/WangSet.js.map +1 -1
- package/dist/esm/WorkerSampledChunkSource.d.ts +95 -0
- package/dist/esm/WorkerSampledChunkSource.d.ts.map +1 -0
- package/dist/esm/WorkerSampledChunkSource.js +117 -0
- package/dist/esm/WorkerSampledChunkSource.js.map +1 -0
- package/dist/esm/autoTile.d.ts +10 -5
- package/dist/esm/autoTile.d.ts.map +1 -0
- package/dist/esm/autoTile.js +164 -205
- package/dist/esm/autoTile.js.map +1 -1
- package/dist/esm/chunkGeometry.d.ts +8 -7
- package/dist/esm/chunkGeometry.d.ts.map +1 -0
- package/dist/esm/chunkGeometry.js +83 -94
- package/dist/esm/chunkGeometry.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -15
- package/dist/esm/nodeBounds.d.ts +20 -0
- package/dist/esm/nodeBounds.d.ts.map +1 -0
- package/dist/esm/nodeBounds.js +40 -0
- package/dist/esm/nodeBounds.js.map +1 -0
- package/dist/esm/pixelSnap.d.ts +4 -3
- package/dist/esm/pixelSnap.d.ts.map +1 -0
- package/dist/esm/pixelSnap.js +18 -16
- package/dist/esm/pixelSnap.js.map +1 -1
- package/dist/esm/public.d.ts +23 -3
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/tileCollision.d.ts +190 -0
- package/dist/esm/tileCollision.d.ts.map +1 -0
- package/dist/esm/tileCollision.js +367 -0
- package/dist/esm/tileCollision.js.map +1 -0
- package/dist/esm/tileWord.d.ts +12 -0
- package/dist/esm/tileWord.d.ts.map +1 -0
- package/dist/esm/tileWord.js +16 -0
- package/dist/esm/tileWord.js.map +1 -0
- package/dist/esm/tilemapExtension.d.ts +4 -3
- package/dist/esm/tilemapExtension.d.ts.map +1 -0
- package/dist/esm/tilemapExtension.js +47 -47
- package/dist/esm/tilemapExtension.js.map +1 -1
- package/dist/esm/tilemapSerializers.d.ts +5 -4
- package/dist/esm/tilemapSerializers.d.ts.map +1 -0
- package/dist/esm/tilemapSerializers.js +48 -41
- package/dist/esm/tilemapSerializers.js.map +1 -1
- package/dist/esm/types.d.ts +34 -18
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +111 -120
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +50 -2
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts.map +1 -0
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +366 -332
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -1
- package/dist/esm/webgl2/shaders/tile-chunk.frag.js +6 -0
- package/dist/esm/webgl2/shaders/tile-chunk.frag.js.map +1 -0
- package/dist/esm/webgl2/shaders/tile-chunk.vert.js +6 -0
- package/dist/esm/webgl2/shaders/tile-chunk.vert.js.map +1 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +70 -4
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts.map +1 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +496 -383
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -1
- package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js +6 -0
- package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js.map +1 -0
- package/package.json +8 -15
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/register.d.ts +0 -1
- package/dist/esm/register.js +0 -25
- 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"}
|