@codexo/exojs-tilemap 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,150 @@
|
|
|
1
|
+
//#region src/MapWorld.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where a neighbouring level sits relative to the level that names it.
|
|
4
|
+
*
|
|
5
|
+
* `Overlap`, `Below` and `Above` describe depth relationships rather than
|
|
6
|
+
* cardinal directions - a source format that stacks levels (LDtk's depth
|
|
7
|
+
* layers) reports those. `Unknown` preserves an adjacency whose direction code
|
|
8
|
+
* this version does not recognise: the levels are neighbours, but the side is
|
|
9
|
+
* not interpretable.
|
|
10
|
+
*/
|
|
11
|
+
const MapLevelSide = {
|
|
12
|
+
North: "north",
|
|
13
|
+
South: "south",
|
|
14
|
+
West: "west",
|
|
15
|
+
East: "east",
|
|
16
|
+
Overlap: "overlap",
|
|
17
|
+
Below: "below",
|
|
18
|
+
Above: "above",
|
|
19
|
+
Unknown: "unknown"
|
|
20
|
+
};
|
|
21
|
+
const EMPTY_BOUNDS = Object.freeze({
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 0,
|
|
24
|
+
width: 0,
|
|
25
|
+
height: 0
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* A format-neutral description of how levels are laid out in a world.
|
|
29
|
+
*
|
|
30
|
+
* A `MapWorld` holds metadata only - no tiles, no textures, no runtime maps.
|
|
31
|
+
* It is what a game reads to decide which levels it needs; loading them is
|
|
32
|
+
* {@link import('./MapWorldRuntime').MapWorldRuntime}'s job.
|
|
33
|
+
*
|
|
34
|
+
* Adapters build one from their source document
|
|
35
|
+
* (`@codexo/exojs-ldtk` does so from a `.ldtk` project). A format with no world
|
|
36
|
+
* concept of its own - Tiled, today - can be described by constructing one
|
|
37
|
+
* directly from whatever placement data the game keeps.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const world = new MapWorld({
|
|
42
|
+
* name: 'overworld',
|
|
43
|
+
* levels: [
|
|
44
|
+
* { id: 'forest', name: 'Forest', index: 0, external: false,
|
|
45
|
+
* bounds: { x: 0, y: 0, width: 512, height: 512 },
|
|
46
|
+
* neighbours: [{ id: 'cave', side: MapLevelSide.East }], properties: {} },
|
|
47
|
+
* ],
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
var MapWorld = class {
|
|
52
|
+
/** World name; empty when the source format does not name it. */
|
|
53
|
+
name;
|
|
54
|
+
/** The world's levels, in source document order. */
|
|
55
|
+
levels;
|
|
56
|
+
_byId;
|
|
57
|
+
_byName;
|
|
58
|
+
_bounds;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.name = options.name ?? "";
|
|
61
|
+
this.levels = Object.freeze([...options.levels]);
|
|
62
|
+
const byId = /* @__PURE__ */ new Map();
|
|
63
|
+
const byName = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const level of this.levels) {
|
|
65
|
+
if (byId.has(level.id)) throw new Error(`MapWorld: duplicate level id "${level.id}" - level ids must be unique within a world.`);
|
|
66
|
+
byId.set(level.id, level);
|
|
67
|
+
if (!byName.has(level.name)) byName.set(level.name, level);
|
|
68
|
+
}
|
|
69
|
+
this._byId = byId;
|
|
70
|
+
this._byName = byName;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Union of every level's bounds, or a zero rectangle for an empty world.
|
|
74
|
+
* Computed once and cached - a `MapWorld` is immutable.
|
|
75
|
+
*/
|
|
76
|
+
get bounds() {
|
|
77
|
+
if (this._bounds !== void 0) return this._bounds;
|
|
78
|
+
if (this.levels.length === 0) {
|
|
79
|
+
this._bounds = EMPTY_BOUNDS;
|
|
80
|
+
return this._bounds;
|
|
81
|
+
}
|
|
82
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
83
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
84
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
85
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
86
|
+
for (const level of this.levels) {
|
|
87
|
+
const b = level.bounds;
|
|
88
|
+
if (b.x < minX) minX = b.x;
|
|
89
|
+
if (b.y < minY) minY = b.y;
|
|
90
|
+
if (b.x + b.width > maxX) maxX = b.x + b.width;
|
|
91
|
+
if (b.y + b.height > maxY) maxY = b.y + b.height;
|
|
92
|
+
}
|
|
93
|
+
this._bounds = Object.freeze({
|
|
94
|
+
x: minX,
|
|
95
|
+
y: minY,
|
|
96
|
+
width: maxX - minX,
|
|
97
|
+
height: maxY - minY
|
|
98
|
+
});
|
|
99
|
+
return this._bounds;
|
|
100
|
+
}
|
|
101
|
+
/** The level with this {@link MapLevel.id}, or `undefined`. Constant time. */
|
|
102
|
+
getLevel(id) {
|
|
103
|
+
return this._byId.get(id);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The first level in document order whose {@link MapLevel.name} matches, or
|
|
107
|
+
* `undefined`. Constant time. Names are not unique by contract.
|
|
108
|
+
*/
|
|
109
|
+
getLevelByName(name) {
|
|
110
|
+
return this._byName.get(name);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The levels `id` declares as adjacent, resolved to their metadata and in the
|
|
114
|
+
* order the source format declared them.
|
|
115
|
+
*
|
|
116
|
+
* A neighbour naming a level this world does not contain is skipped - LDtk
|
|
117
|
+
* can reference across worlds of a multi-world project, and each of those
|
|
118
|
+
* worlds is its own `MapWorld`. Returns an empty array for an unknown `id`.
|
|
119
|
+
*/
|
|
120
|
+
getNeighbours(id) {
|
|
121
|
+
const level = this._byId.get(id);
|
|
122
|
+
if (level === void 0) return [];
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const neighbour of level.neighbours) {
|
|
125
|
+
const resolved = this._byId.get(neighbour.id);
|
|
126
|
+
if (resolved !== void 0) out.push(resolved);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Every level whose bounds intersect `bounds`, in document order. Edge
|
|
132
|
+
* contact alone does not count as an intersection, so two levels laid out
|
|
133
|
+
* side by side are not reported for a query that only touches the seam.
|
|
134
|
+
*
|
|
135
|
+
* Linear in the number of levels - intended for a per-camera-move query over
|
|
136
|
+
* a world of hundreds of levels, not for a per-frame query over thousands.
|
|
137
|
+
*/
|
|
138
|
+
getLevelsInBounds(bounds) {
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const level of this.levels) {
|
|
141
|
+
const b = level.bounds;
|
|
142
|
+
if (b.x < bounds.x + bounds.width && bounds.x < b.x + b.width && b.y < bounds.y + bounds.height && bounds.y < b.y + b.height) out.push(level);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
export { MapLevelSide, MapWorld };
|
|
150
|
+
//# sourceMappingURL=MapWorld.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MapWorld.js","names":[],"sources":["../../src/MapWorld.ts"],"sourcesContent":["import type { TileProperties } from './types';\n\n/** An axis-aligned rectangle in world pixel space (+Y down). */\nexport interface MapBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Where a neighbouring level sits relative to the level that names it.\n *\n * `Overlap`, `Below` and `Above` describe depth relationships rather than\n * cardinal directions - a source format that stacks levels (LDtk's depth\n * layers) reports those. `Unknown` preserves an adjacency whose direction code\n * this version does not recognise: the levels are neighbours, but the side is\n * not interpretable.\n */\nexport const MapLevelSide = {\n North: 'north',\n South: 'south',\n West: 'west',\n East: 'east',\n Overlap: 'overlap',\n Below: 'below',\n Above: 'above',\n Unknown: 'unknown',\n} as const;\n\n/** Side discriminant for a {@link MapLevelNeighbour}. */\nexport type MapLevelSide = (typeof MapLevelSide)[keyof typeof MapLevelSide];\n\n/** One adjacency edge from a level to another level in the same {@link MapWorld}. */\nexport interface MapLevelNeighbour {\n /** {@link MapLevel.id} of the neighbouring level. */\n readonly id: string;\n readonly side: MapLevelSide;\n}\n\n/**\n * Immutable metadata for one level of a {@link MapWorld} - everything needed to\n * decide whether to load it, without loading it.\n */\nexport interface MapLevel {\n /**\n * Stable identity, unique within the world. Carried from the source format\n * (LDtk uses the level `iid`), so it survives re-ordering and re-export and\n * is safe to persist in a savegame.\n */\n readonly id: string;\n /** Human-readable level name. Not guaranteed unique. */\n readonly name: string;\n /** Position in source document order. */\n readonly index: number;\n /** Placement and size in world pixel space. */\n readonly bounds: MapBounds;\n /**\n * Whether the level's layer payload lives outside the root document and has\n * to be fetched separately when the level is loaded.\n */\n readonly external: boolean;\n /** Adjacency edges declared by the source format; empty when it declares none. */\n readonly neighbours: readonly MapLevelNeighbour[];\n /** Level-level custom properties. */\n readonly properties: TileProperties;\n}\n\n/** Construction options for a {@link MapWorld}. */\nexport interface MapWorldOptions {\n /** World name. Defaults to an empty string. */\n readonly name?: string;\n /** The world's levels, in source document order. */\n readonly levels: readonly MapLevel[];\n}\n\nconst EMPTY_BOUNDS: MapBounds = Object.freeze({ x: 0, y: 0, width: 0, height: 0 });\n\n/**\n * A format-neutral description of how levels are laid out in a world.\n *\n * A `MapWorld` holds metadata only - no tiles, no textures, no runtime maps.\n * It is what a game reads to decide which levels it needs; loading them is\n * {@link import('./MapWorldRuntime').MapWorldRuntime}'s job.\n *\n * Adapters build one from their source document\n * (`@codexo/exojs-ldtk` does so from a `.ldtk` project). A format with no world\n * concept of its own - Tiled, today - can be described by constructing one\n * directly from whatever placement data the game keeps.\n *\n * @example\n * ```ts\n * const world = new MapWorld({\n * name: 'overworld',\n * levels: [\n * { id: 'forest', name: 'Forest', index: 0, external: false,\n * bounds: { x: 0, y: 0, width: 512, height: 512 },\n * neighbours: [{ id: 'cave', side: MapLevelSide.East }], properties: {} },\n * ],\n * });\n * ```\n */\nexport class MapWorld {\n /** World name; empty when the source format does not name it. */\n public readonly name: string;\n /** The world's levels, in source document order. */\n public readonly levels: readonly MapLevel[];\n\n private readonly _byId: ReadonlyMap<string, MapLevel>;\n private readonly _byName: ReadonlyMap<string, MapLevel>;\n private _bounds?: MapBounds;\n\n public constructor(options: MapWorldOptions) {\n this.name = options.name ?? '';\n this.levels = Object.freeze([...options.levels]);\n\n const byId = new Map<string, MapLevel>();\n const byName = new Map<string, MapLevel>();\n\n for (const level of this.levels) {\n if (byId.has(level.id)) {\n throw new Error(`MapWorld: duplicate level id \"${level.id}\" - level ids must be unique within a world.`);\n }\n\n byId.set(level.id, level);\n // Names are not unique by contract; first in document order wins so the\n // lookup stays deterministic instead of depending on iteration order.\n if (!byName.has(level.name)) byName.set(level.name, level);\n }\n\n this._byId = byId;\n this._byName = byName;\n }\n\n /**\n * Union of every level's bounds, or a zero rectangle for an empty world.\n * Computed once and cached - a `MapWorld` is immutable.\n */\n public get bounds(): MapBounds {\n if (this._bounds !== undefined) return this._bounds;\n\n if (this.levels.length === 0) {\n this._bounds = EMPTY_BOUNDS;\n return this._bounds;\n }\n\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n\n for (const level of this.levels) {\n const b = level.bounds;\n if (b.x < minX) minX = b.x;\n if (b.y < minY) minY = b.y;\n if (b.x + b.width > maxX) maxX = b.x + b.width;\n if (b.y + b.height > maxY) maxY = b.y + b.height;\n }\n\n this._bounds = Object.freeze({ x: minX, y: minY, width: maxX - minX, height: maxY - minY });\n return this._bounds;\n }\n\n /** The level with this {@link MapLevel.id}, or `undefined`. Constant time. */\n public getLevel(id: string): MapLevel | undefined {\n return this._byId.get(id);\n }\n\n /**\n * The first level in document order whose {@link MapLevel.name} matches, or\n * `undefined`. Constant time. Names are not unique by contract.\n */\n public getLevelByName(name: string): MapLevel | undefined {\n return this._byName.get(name);\n }\n\n /**\n * The levels `id` declares as adjacent, resolved to their metadata and in the\n * order the source format declared them.\n *\n * A neighbour naming a level this world does not contain is skipped - LDtk\n * can reference across worlds of a multi-world project, and each of those\n * worlds is its own `MapWorld`. Returns an empty array for an unknown `id`.\n */\n public getNeighbours(id: string): readonly MapLevel[] {\n const level = this._byId.get(id);\n if (level === undefined) return [];\n\n const out: MapLevel[] = [];\n\n for (const neighbour of level.neighbours) {\n const resolved = this._byId.get(neighbour.id);\n if (resolved !== undefined) out.push(resolved);\n }\n\n return out;\n }\n\n /**\n * Every level whose bounds intersect `bounds`, in document order. Edge\n * contact alone does not count as an intersection, so two levels laid out\n * side by side are not reported for a query that only touches the seam.\n *\n * Linear in the number of levels - intended for a per-camera-move query over\n * a world of hundreds of levels, not for a per-frame query over thousands.\n */\n public getLevelsInBounds(bounds: MapBounds): MapLevel[] {\n const out: MapLevel[] = [];\n\n for (const level of this.levels) {\n const b = level.bounds;\n if (b.x < bounds.x + bounds.width && bounds.x < b.x + b.width && b.y < bounds.y + bounds.height && bounds.y < b.y + b.height) {\n out.push(level);\n }\n }\n\n return out;\n }\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAa,eAAe;CAC1B,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,SAAS;CACT,OAAO;CACP,OAAO;CACP,SAAS;AACX;AAgDA,MAAM,eAA0B,OAAO,OAAO;CAAE,GAAG;CAAG,GAAG;CAAG,OAAO;CAAG,QAAQ;AAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BjF,IAAa,WAAb,MAAsB;;CAEpB,AAAgB;;CAEhB,AAAgB;CAEhB,AAAiB;CACjB,AAAiB;CACjB,AAAQ;CAER,AAAO,YAAY,SAA0B;EAC3C,KAAK,OAAO,QAAQ,QAAQ;EAC5B,KAAK,SAAS,OAAO,OAAO,CAAC,GAAG,QAAQ,MAAM,CAAC;EAE/C,MAAM,uBAAO,IAAI,IAAsB;EACvC,MAAM,yBAAS,IAAI,IAAsB;EAEzC,KAAK,MAAM,SAAS,KAAK,QAAQ;GAC/B,IAAI,KAAK,IAAI,MAAM,EAAE,GACnB,MAAM,IAAI,MAAM,iCAAiC,MAAM,GAAG,6CAA6C;GAGzG,KAAK,IAAI,MAAM,IAAI,KAAK;GAGxB,IAAI,CAAC,OAAO,IAAI,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,MAAM,KAAK;EAC3D;EAEA,KAAK,QAAQ;EACb,KAAK,UAAU;CACjB;;;;;CAMA,IAAW,SAAoB;EAC7B,IAAI,KAAK,YAAY,QAAW,OAAO,KAAK;EAE5C,IAAI,KAAK,OAAO,WAAW,GAAG;GAC5B,KAAK,UAAU;GACf,OAAO,KAAK;EACd;EAEA,IAAI,OAAO,OAAO;EAClB,IAAI,OAAO,OAAO;EAClB,IAAI,OAAO,OAAO;EAClB,IAAI,OAAO,OAAO;EAElB,KAAK,MAAM,SAAS,KAAK,QAAQ;GAC/B,MAAM,IAAI,MAAM;GAChB,IAAI,EAAE,IAAI,MAAM,OAAO,EAAE;GACzB,IAAI,EAAE,IAAI,MAAM,OAAO,EAAE;GACzB,IAAI,EAAE,IAAI,EAAE,QAAQ,MAAM,OAAO,EAAE,IAAI,EAAE;GACzC,IAAI,EAAE,IAAI,EAAE,SAAS,MAAM,OAAO,EAAE,IAAI,EAAE;EAC5C;EAEA,KAAK,UAAU,OAAO,OAAO;GAAE,GAAG;GAAM,GAAG;GAAM,OAAO,OAAO;GAAM,QAAQ,OAAO;EAAK,CAAC;EAC1F,OAAO,KAAK;CACd;;CAGA,AAAO,SAAS,IAAkC;EAChD,OAAO,KAAK,MAAM,IAAI,EAAE;CAC1B;;;;;CAMA,AAAO,eAAe,MAAoC;EACxD,OAAO,KAAK,QAAQ,IAAI,IAAI;CAC9B;;;;;;;;;CAUA,AAAO,cAAc,IAAiC;EACpD,MAAM,QAAQ,KAAK,MAAM,IAAI,EAAE;EAC/B,IAAI,UAAU,QAAW,OAAO,CAAC;EAEjC,MAAM,MAAkB,CAAC;EAEzB,KAAK,MAAM,aAAa,MAAM,YAAY;GACxC,MAAM,WAAW,KAAK,MAAM,IAAI,UAAU,EAAE;GAC5C,IAAI,aAAa,QAAW,IAAI,KAAK,QAAQ;EAC/C;EAEA,OAAO;CACT;;;;;;;;;CAUA,AAAO,kBAAkB,QAA+B;EACtD,MAAM,MAAkB,CAAC;EAEzB,KAAK,MAAM,SAAS,KAAK,QAAQ;GAC/B,MAAM,IAAI,MAAM;GAChB,IAAI,EAAE,IAAI,OAAO,IAAI,OAAO,SAAS,OAAO,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,OAAO,IAAI,OAAO,UAAU,OAAO,IAAI,EAAE,IAAI,EAAE,QACpH,IAAI,KAAK,KAAK;EAElB;EAEA,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Destroyable, LoaderScope } from '@codexo/exojs';
|
|
2
|
+
import type { MapObjectSpawner } from './MapObjectSpawner';
|
|
3
|
+
import type { MapSpawnSession } from './MapSpawnSession';
|
|
4
|
+
import type { MapLevel, MapWorld } from './MapWorld';
|
|
5
|
+
import type { TileMap } from './TileMap';
|
|
6
|
+
/** What a {@link MapLevelProvider} is given for one level load. */
|
|
7
|
+
export interface MapLevelLoadContext {
|
|
8
|
+
/** Metadata of the level being loaded. */
|
|
9
|
+
readonly level: MapLevel;
|
|
10
|
+
/**
|
|
11
|
+
* The level's own {@link LoaderScope}. Assets claimed through it are released
|
|
12
|
+
* when the level unloads, and claiming an asset another level also holds
|
|
13
|
+
* never takes it away from that level.
|
|
14
|
+
*
|
|
15
|
+
* Its lifetime ends when {@link signal} aborts: an unload releases the scope
|
|
16
|
+
* without waiting for a provider still in flight, so a provider that resumes
|
|
17
|
+
* after that point must not claim through it any more. Check `signal.aborted`
|
|
18
|
+
* after every `await` before touching the scope again.
|
|
19
|
+
*/
|
|
20
|
+
readonly scope: LoaderScope;
|
|
21
|
+
/** Aborts when the level is unloaded or the load is cancelled. */
|
|
22
|
+
readonly signal: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Produces the runtime {@link TileMap} for one level - the single seam where a
|
|
26
|
+
* source format meets the format-neutral world runtime.
|
|
27
|
+
*
|
|
28
|
+
* Anything awaitable is accepted, so a provider can hand back a
|
|
29
|
+
* {@link LoaderScope.load} queue directly.
|
|
30
|
+
*
|
|
31
|
+
* The returned map becomes **owned by that level runtime**, which destroys it
|
|
32
|
+
* on unload. Return a map built for this load; handing back a shared map (an
|
|
33
|
+
* entry of an eagerly converted document, say) makes the runtime destroy an
|
|
34
|
+
* object other code still holds.
|
|
35
|
+
*/
|
|
36
|
+
export type MapLevelProvider = (context: MapLevelLoadContext) => TileMap | PromiseLike<TileMap>;
|
|
37
|
+
/** Options for {@link MapWorldRuntime}. */
|
|
38
|
+
export interface MapWorldRuntimeOptions {
|
|
39
|
+
/** The world metadata to stream levels out of. */
|
|
40
|
+
readonly world: MapWorld;
|
|
41
|
+
/** Builds a level's runtime map. See {@link MapLevelProvider} for its ownership rule. */
|
|
42
|
+
readonly load: MapLevelProvider;
|
|
43
|
+
/**
|
|
44
|
+
* Parent scope for everything the runtime loads - typically `scene.loader`.
|
|
45
|
+
*
|
|
46
|
+
* The runtime creates its own child scope under it and never destroys the
|
|
47
|
+
* scope it was given: the caller keeps owning that lifetime.
|
|
48
|
+
*/
|
|
49
|
+
readonly scope: LoaderScope;
|
|
50
|
+
/** Diagnostic name for the runtime's own scope. Defaults to the world's name. */
|
|
51
|
+
readonly name?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Options for a {@link MapWorldRuntime.loadLevel} that spawns nothing. */
|
|
54
|
+
export interface MapLevelCancelOptions {
|
|
55
|
+
/** Cancels this load. Unloading the level cancels it too. */
|
|
56
|
+
readonly signal?: AbortSignal;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Options for a {@link MapWorldRuntime.loadLevel} that also spawns the level's
|
|
60
|
+
* map objects. `spawner` and `context` travel together: the context exists to
|
|
61
|
+
* be handed to that spawner's factories.
|
|
62
|
+
*/
|
|
63
|
+
export interface MapLevelLoadOptions<Context, Result extends Destroyable> extends MapLevelCancelOptions {
|
|
64
|
+
/** Spawns the level's map objects as part of the load. */
|
|
65
|
+
readonly spawner: MapObjectSpawner<Context, Result>;
|
|
66
|
+
/** Value handed to every factory of `spawner`. */
|
|
67
|
+
readonly context: Context;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One loaded level and everything that load produced.
|
|
71
|
+
*
|
|
72
|
+
* Created by {@link MapWorldRuntime.loadLevel}; destroying it unloads the
|
|
73
|
+
* level. There is nothing else to release afterwards - the map, the spawned
|
|
74
|
+
* objects and the level's asset claims all end here.
|
|
75
|
+
*
|
|
76
|
+
* @typeParam Result - what the level's spawner produced, if it had one.
|
|
77
|
+
*/
|
|
78
|
+
export declare class MapLevelRuntime<Result extends Destroyable = Destroyable> implements Destroyable {
|
|
79
|
+
/** {@link MapLevel.id} of the loaded level. */
|
|
80
|
+
readonly id: string;
|
|
81
|
+
/** Metadata of the loaded level. */
|
|
82
|
+
readonly level: MapLevel;
|
|
83
|
+
/** The level's asset scope. Released last, after everything that could hold an asset is gone. */
|
|
84
|
+
readonly scope: LoaderScope;
|
|
85
|
+
/** The level's runtime map, owned by this runtime. */
|
|
86
|
+
readonly map: TileMap;
|
|
87
|
+
/** What the level's spawner produced, or `null` when the load ran without one. */
|
|
88
|
+
readonly spawns: MapSpawnSession<Result> | null;
|
|
89
|
+
private readonly _onDestroy;
|
|
90
|
+
private _destroyed;
|
|
91
|
+
/** Level runtimes are produced by {@link MapWorldRuntime.loadLevel}. @internal */
|
|
92
|
+
constructor(level: MapLevel, scope: LoaderScope, map: TileMap, spawns: MapSpawnSession<Result> | null, onDestroy: (runtime: MapLevelRuntime) => void);
|
|
93
|
+
/** Whether this level has been unloaded. */
|
|
94
|
+
get destroyed(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Unload the level: destroy the spawned objects (reverse spawn order), then
|
|
97
|
+
* the map, then release the level's asset claims. Idempotent.
|
|
98
|
+
*
|
|
99
|
+
* The scope goes last because everything before it may still be reading a
|
|
100
|
+
* texture the scope keeps resident.
|
|
101
|
+
*/
|
|
102
|
+
destroy(): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The live side of a {@link MapWorld}: loads and unloads levels on demand, each
|
|
106
|
+
* with its own asset scope and its own lifetime.
|
|
107
|
+
*
|
|
108
|
+
* ExoJS provides the mechanism, not the streaming policy. Nothing here watches
|
|
109
|
+
* a camera or guesses a radius: game code reads
|
|
110
|
+
* {@link MapWorld.getNeighbours} / {@link MapWorld.getLevelsInBounds} and calls
|
|
111
|
+
* {@link loadLevel} and {@link unloadLevel} when it decides to.
|
|
112
|
+
*
|
|
113
|
+
* Scope layout - the runtime owns everything below its own scope and nothing
|
|
114
|
+
* above it:
|
|
115
|
+
*
|
|
116
|
+
* ```text
|
|
117
|
+
* scene.loader (given to the runtime, never destroyed by it)
|
|
118
|
+
* └─ world (the runtime's own scope)
|
|
119
|
+
* ├─ level:forest
|
|
120
|
+
* └─ level:cave
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const runtime = new MapWorldRuntime({
|
|
126
|
+
* world,
|
|
127
|
+
* scope: scene.loader,
|
|
128
|
+
* load: ({ level, scope, signal }) => buildLevelMap(level, scope, signal),
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* const forest = await runtime.loadLevel('forest', { spawner, context });
|
|
132
|
+
* scene.addChild(forest.map.createView().root);
|
|
133
|
+
*
|
|
134
|
+
* runtime.unloadLevel('forest');
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export declare class MapWorldRuntime implements Destroyable {
|
|
138
|
+
/** The world this runtime streams levels out of. */
|
|
139
|
+
readonly world: MapWorld;
|
|
140
|
+
/** The runtime's own scope: parent of every level scope, child of the injected one. */
|
|
141
|
+
readonly scope: LoaderScope;
|
|
142
|
+
private readonly _load;
|
|
143
|
+
private readonly _live;
|
|
144
|
+
private readonly _inFlight;
|
|
145
|
+
private _destroyed;
|
|
146
|
+
constructor(options: MapWorldRuntimeOptions);
|
|
147
|
+
/** Whether {@link destroy} has run. */
|
|
148
|
+
get destroyed(): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Every currently **loaded** level, in load order - not the world's level
|
|
151
|
+
* set, which stays on {@link MapWorld.levels} (reachable as
|
|
152
|
+
* `runtime.world.levels`). Returns a fresh array.
|
|
153
|
+
*/
|
|
154
|
+
get levels(): readonly MapLevelRuntime[];
|
|
155
|
+
/** The loaded level with this id, or `undefined` when it is not loaded. */
|
|
156
|
+
getLevel(id: string): MapLevelRuntime | undefined;
|
|
157
|
+
/** Whether the level is loaded. A load still in flight does not count as loaded. */
|
|
158
|
+
isLoaded(id: string): boolean;
|
|
159
|
+
/** Whether a load for this level is currently running. */
|
|
160
|
+
isLoading(id: string): boolean;
|
|
161
|
+
loadLevel(id: string, options?: MapLevelCancelOptions): Promise<MapLevelRuntime>;
|
|
162
|
+
loadLevel<Context, Result extends Destroyable>(id: string, options: MapLevelLoadOptions<Context, Result>): Promise<MapLevelRuntime<Result>>;
|
|
163
|
+
/**
|
|
164
|
+
* Unload a level, or cancel its load when one is in flight. Returns whether
|
|
165
|
+
* there was anything to unload.
|
|
166
|
+
*
|
|
167
|
+
* A cancelled load rejects with an `AbortError` for whoever started it, and
|
|
168
|
+
* the level is immediately loadable again - a `loadLevel` issued in the same
|
|
169
|
+
* turn starts a fresh load rather than joining the one just cancelled.
|
|
170
|
+
*/
|
|
171
|
+
unloadLevel(id: string): boolean;
|
|
172
|
+
/**
|
|
173
|
+
* Cancel every load in flight, unload every loaded level in reverse load
|
|
174
|
+
* order, and release the runtime's own scope. Idempotent.
|
|
175
|
+
*
|
|
176
|
+
* The scope the runtime was constructed with is left alone - it belongs to
|
|
177
|
+
* whoever passed it in.
|
|
178
|
+
*/
|
|
179
|
+
destroy(): void;
|
|
180
|
+
private _runLoad;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=MapWorldRuntime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MapWorldRuntime.d.ts","sourceRoot":"","sources":["../../src/MapWorldRuntime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { logger } from "@codexo/exojs";
|
|
2
|
+
|
|
3
|
+
//#region src/MapWorldRuntime.ts
|
|
4
|
+
/**
|
|
5
|
+
* One loaded level and everything that load produced.
|
|
6
|
+
*
|
|
7
|
+
* Created by {@link MapWorldRuntime.loadLevel}; destroying it unloads the
|
|
8
|
+
* level. There is nothing else to release afterwards - the map, the spawned
|
|
9
|
+
* objects and the level's asset claims all end here.
|
|
10
|
+
*
|
|
11
|
+
* @typeParam Result - what the level's spawner produced, if it had one.
|
|
12
|
+
*/
|
|
13
|
+
var MapLevelRuntime = class {
|
|
14
|
+
/** {@link MapLevel.id} of the loaded level. */
|
|
15
|
+
id;
|
|
16
|
+
/** Metadata of the loaded level. */
|
|
17
|
+
level;
|
|
18
|
+
/** The level's asset scope. Released last, after everything that could hold an asset is gone. */
|
|
19
|
+
scope;
|
|
20
|
+
/** The level's runtime map, owned by this runtime. */
|
|
21
|
+
map;
|
|
22
|
+
/** What the level's spawner produced, or `null` when the load ran without one. */
|
|
23
|
+
spawns;
|
|
24
|
+
_onDestroy;
|
|
25
|
+
_destroyed = false;
|
|
26
|
+
/** Level runtimes are produced by {@link MapWorldRuntime.loadLevel}. @internal */
|
|
27
|
+
constructor(level, scope, map, spawns, onDestroy) {
|
|
28
|
+
this.id = level.id;
|
|
29
|
+
this.level = level;
|
|
30
|
+
this.scope = scope;
|
|
31
|
+
this.map = map;
|
|
32
|
+
this.spawns = spawns;
|
|
33
|
+
this._onDestroy = onDestroy;
|
|
34
|
+
}
|
|
35
|
+
/** Whether this level has been unloaded. */
|
|
36
|
+
get destroyed() {
|
|
37
|
+
return this._destroyed;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Unload the level: destroy the spawned objects (reverse spawn order), then
|
|
41
|
+
* the map, then release the level's asset claims. Idempotent.
|
|
42
|
+
*
|
|
43
|
+
* The scope goes last because everything before it may still be reading a
|
|
44
|
+
* texture the scope keeps resident.
|
|
45
|
+
*/
|
|
46
|
+
destroy() {
|
|
47
|
+
if (this._destroyed) return;
|
|
48
|
+
this._destroyed = true;
|
|
49
|
+
this._onDestroy(this);
|
|
50
|
+
if (this.spawns !== null) guardedDestroy(this.spawns, `spawn session of level "${this.id}"`);
|
|
51
|
+
guardedDestroy(this.map, `map of level "${this.id}"`);
|
|
52
|
+
this.scope.destroy();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The live side of a {@link MapWorld}: loads and unloads levels on demand, each
|
|
57
|
+
* with its own asset scope and its own lifetime.
|
|
58
|
+
*
|
|
59
|
+
* ExoJS provides the mechanism, not the streaming policy. Nothing here watches
|
|
60
|
+
* a camera or guesses a radius: game code reads
|
|
61
|
+
* {@link MapWorld.getNeighbours} / {@link MapWorld.getLevelsInBounds} and calls
|
|
62
|
+
* {@link loadLevel} and {@link unloadLevel} when it decides to.
|
|
63
|
+
*
|
|
64
|
+
* Scope layout - the runtime owns everything below its own scope and nothing
|
|
65
|
+
* above it:
|
|
66
|
+
*
|
|
67
|
+
* ```text
|
|
68
|
+
* scene.loader (given to the runtime, never destroyed by it)
|
|
69
|
+
* └─ world (the runtime's own scope)
|
|
70
|
+
* ├─ level:forest
|
|
71
|
+
* └─ level:cave
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const runtime = new MapWorldRuntime({
|
|
77
|
+
* world,
|
|
78
|
+
* scope: scene.loader,
|
|
79
|
+
* load: ({ level, scope, signal }) => buildLevelMap(level, scope, signal),
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* const forest = await runtime.loadLevel('forest', { spawner, context });
|
|
83
|
+
* scene.addChild(forest.map.createView().root);
|
|
84
|
+
*
|
|
85
|
+
* runtime.unloadLevel('forest');
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
var MapWorldRuntime = class {
|
|
89
|
+
/** The world this runtime streams levels out of. */
|
|
90
|
+
world;
|
|
91
|
+
/** The runtime's own scope: parent of every level scope, child of the injected one. */
|
|
92
|
+
scope;
|
|
93
|
+
_load;
|
|
94
|
+
_live = /* @__PURE__ */ new Map();
|
|
95
|
+
_inFlight = /* @__PURE__ */ new Map();
|
|
96
|
+
_destroyed = false;
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.world = options.world;
|
|
99
|
+
this._load = options.load;
|
|
100
|
+
this.scope = options.scope.createScope({ name: options.name ?? (this.world.name || "world") });
|
|
101
|
+
}
|
|
102
|
+
/** Whether {@link destroy} has run. */
|
|
103
|
+
get destroyed() {
|
|
104
|
+
return this._destroyed;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Every currently **loaded** level, in load order - not the world's level
|
|
108
|
+
* set, which stays on {@link MapWorld.levels} (reachable as
|
|
109
|
+
* `runtime.world.levels`). Returns a fresh array.
|
|
110
|
+
*/
|
|
111
|
+
get levels() {
|
|
112
|
+
return [...this._live.values()];
|
|
113
|
+
}
|
|
114
|
+
/** The loaded level with this id, or `undefined` when it is not loaded. */
|
|
115
|
+
getLevel(id) {
|
|
116
|
+
return this._live.get(id);
|
|
117
|
+
}
|
|
118
|
+
/** Whether the level is loaded. A load still in flight does not count as loaded. */
|
|
119
|
+
isLoaded(id) {
|
|
120
|
+
return this._live.has(id);
|
|
121
|
+
}
|
|
122
|
+
/** Whether a load for this level is currently running. */
|
|
123
|
+
isLoading(id) {
|
|
124
|
+
return this._inFlight.has(id);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Load a level, or return the one already loaded.
|
|
128
|
+
*
|
|
129
|
+
* At most one runtime exists per level id. Calling this while the level is
|
|
130
|
+
* loaded resolves to that runtime, and calling it while a load is in flight
|
|
131
|
+
* joins that load rather than starting a second one. **Options belong to the
|
|
132
|
+
* call that actually starts the load** - a joining call's `spawner`,
|
|
133
|
+
* `context` and `signal` are not applied, because the level being produced is
|
|
134
|
+
* the one the first call asked for.
|
|
135
|
+
*
|
|
136
|
+
* Pass `{ spawner, context }` to spawn the level's objects as part of the
|
|
137
|
+
* load, or just `{ signal }` to load a level without spawning anything.
|
|
138
|
+
*
|
|
139
|
+
* A failed or aborted load leaves nothing behind: the map, anything the
|
|
140
|
+
* spawner created, and the level's asset claims are all released before the
|
|
141
|
+
* rejection surfaces, and the level can be loaded again.
|
|
142
|
+
*
|
|
143
|
+
* @throws {Error} when the world has no level with this id, or the runtime is destroyed.
|
|
144
|
+
* @throws {DOMException} named `AbortError` when the load is cancelled.
|
|
145
|
+
*/
|
|
146
|
+
loadLevel(id, options) {
|
|
147
|
+
const typed = (promise) => promise;
|
|
148
|
+
if (this._destroyed) return Promise.reject(/* @__PURE__ */ new Error(`MapWorldRuntime: cannot load level "${id}" - the runtime is destroyed.`));
|
|
149
|
+
const live = this._live.get(id);
|
|
150
|
+
if (live !== void 0) return typed(Promise.resolve(live));
|
|
151
|
+
const pending = this._inFlight.get(id);
|
|
152
|
+
if (pending !== void 0) return typed(pending.promise);
|
|
153
|
+
const level = this.world.getLevel(id);
|
|
154
|
+
if (level === void 0) return Promise.reject(/* @__PURE__ */ new Error(`MapWorldRuntime: world "${this.world.name}" has no level with id "${id}".`));
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const signal = options?.signal;
|
|
157
|
+
const forward = () => controller.abort();
|
|
158
|
+
if (signal !== void 0) {
|
|
159
|
+
if (signal.aborted) return Promise.reject(new DOMException(`Level "${id}" load was cancelled.`, "AbortError"));
|
|
160
|
+
signal.addEventListener("abort", forward, { once: true });
|
|
161
|
+
}
|
|
162
|
+
const promise = this._runLoad(level, controller.signal, options).finally(() => {
|
|
163
|
+
signal?.removeEventListener("abort", forward);
|
|
164
|
+
if (this._inFlight.get(id)?.controller === controller) this._inFlight.delete(id);
|
|
165
|
+
});
|
|
166
|
+
this._inFlight.set(id, {
|
|
167
|
+
promise,
|
|
168
|
+
controller
|
|
169
|
+
});
|
|
170
|
+
return typed(promise);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Unload a level, or cancel its load when one is in flight. Returns whether
|
|
174
|
+
* there was anything to unload.
|
|
175
|
+
*
|
|
176
|
+
* A cancelled load rejects with an `AbortError` for whoever started it, and
|
|
177
|
+
* the level is immediately loadable again - a `loadLevel` issued in the same
|
|
178
|
+
* turn starts a fresh load rather than joining the one just cancelled.
|
|
179
|
+
*/
|
|
180
|
+
unloadLevel(id) {
|
|
181
|
+
const pending = this._inFlight.get(id);
|
|
182
|
+
if (pending !== void 0) {
|
|
183
|
+
this._inFlight.delete(id);
|
|
184
|
+
pending.controller.abort();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const live = this._live.get(id);
|
|
188
|
+
if (live !== void 0) {
|
|
189
|
+
live.destroy();
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Cancel every load in flight, unload every loaded level in reverse load
|
|
196
|
+
* order, and release the runtime's own scope. Idempotent.
|
|
197
|
+
*
|
|
198
|
+
* The scope the runtime was constructed with is left alone - it belongs to
|
|
199
|
+
* whoever passed it in.
|
|
200
|
+
*/
|
|
201
|
+
destroy() {
|
|
202
|
+
if (this._destroyed) return;
|
|
203
|
+
this._destroyed = true;
|
|
204
|
+
const pending = [...this._inFlight.values()];
|
|
205
|
+
this._inFlight.clear();
|
|
206
|
+
for (const load of pending) load.controller.abort();
|
|
207
|
+
for (const runtime of [...this._live.values()].reverse()) guardedDestroy(runtime, `level "${runtime.id}"`);
|
|
208
|
+
this._live.clear();
|
|
209
|
+
this.scope.destroy();
|
|
210
|
+
}
|
|
211
|
+
async _runLoad(level, signal, options) {
|
|
212
|
+
const scope = this.scope.createScope({ name: `level:${level.id}` });
|
|
213
|
+
let map;
|
|
214
|
+
let spawns = null;
|
|
215
|
+
try {
|
|
216
|
+
throwIfAborted(level.id, signal);
|
|
217
|
+
map = await this._load({
|
|
218
|
+
level,
|
|
219
|
+
scope,
|
|
220
|
+
signal
|
|
221
|
+
});
|
|
222
|
+
throwIfAborted(level.id, signal);
|
|
223
|
+
if (options !== void 0 && "spawner" in options) {
|
|
224
|
+
spawns = await options.spawner.spawn(map, options.context, { signal });
|
|
225
|
+
throwIfAborted(level.id, signal);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (spawns !== null) guardedDestroy(spawns, `spawn session of level "${level.id}"`);
|
|
229
|
+
if (map !== void 0) guardedDestroy(map, `map of level "${level.id}"`);
|
|
230
|
+
scope.destroy();
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
const runtime = new MapLevelRuntime(level, scope, map, spawns, (destroyed) => {
|
|
234
|
+
if (this._live.get(destroyed.id) === destroyed) this._live.delete(destroyed.id);
|
|
235
|
+
});
|
|
236
|
+
this._live.set(level.id, runtime);
|
|
237
|
+
return runtime;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Teardown step that reports a failure instead of propagating it, so the steps
|
|
242
|
+
* after it still run. Matches how the application reports a subsystem that
|
|
243
|
+
* fails to dispose.
|
|
244
|
+
*/
|
|
245
|
+
const guardedDestroy = (target, what) => {
|
|
246
|
+
try {
|
|
247
|
+
target.destroy();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
logger.error(`MapWorldRuntime: destroying the ${what} failed; continuing teardown.`, {
|
|
250
|
+
source: "tilemap",
|
|
251
|
+
...error instanceof Error && { error }
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const throwIfAborted = (id, signal) => {
|
|
256
|
+
if (signal.aborted) throw new DOMException(`Level "${id}" load was cancelled.`, "AbortError");
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
export { MapLevelRuntime, MapWorldRuntime };
|
|
261
|
+
//# sourceMappingURL=MapWorldRuntime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MapWorldRuntime.js","names":[],"sources":["../../src/MapWorldRuntime.ts"],"sourcesContent":["import type { Destroyable, LoaderScope } from '@codexo/exojs';\nimport { logger } from '@codexo/exojs';\n\nimport type { MapObjectSpawner } from './MapObjectSpawner';\nimport type { MapSpawnSession } from './MapSpawnSession';\nimport type { MapLevel, MapWorld } from './MapWorld';\nimport type { TileMap } from './TileMap';\n\n/** What a {@link MapLevelProvider} is given for one level load. */\nexport interface MapLevelLoadContext {\n /** Metadata of the level being loaded. */\n readonly level: MapLevel;\n /**\n * The level's own {@link LoaderScope}. Assets claimed through it are released\n * when the level unloads, and claiming an asset another level also holds\n * never takes it away from that level.\n *\n * Its lifetime ends when {@link signal} aborts: an unload releases the scope\n * without waiting for a provider still in flight, so a provider that resumes\n * after that point must not claim through it any more. Check `signal.aborted`\n * after every `await` before touching the scope again.\n */\n readonly scope: LoaderScope;\n /** Aborts when the level is unloaded or the load is cancelled. */\n readonly signal: AbortSignal;\n}\n\n/**\n * Produces the runtime {@link TileMap} for one level - the single seam where a\n * source format meets the format-neutral world runtime.\n *\n * Anything awaitable is accepted, so a provider can hand back a\n * {@link LoaderScope.load} queue directly.\n *\n * The returned map becomes **owned by that level runtime**, which destroys it\n * on unload. Return a map built for this load; handing back a shared map (an\n * entry of an eagerly converted document, say) makes the runtime destroy an\n * object other code still holds.\n */\nexport type MapLevelProvider = (context: MapLevelLoadContext) => TileMap | PromiseLike<TileMap>;\n\n/** Options for {@link MapWorldRuntime}. */\nexport interface MapWorldRuntimeOptions {\n /** The world metadata to stream levels out of. */\n readonly world: MapWorld;\n /** Builds a level's runtime map. See {@link MapLevelProvider} for its ownership rule. */\n readonly load: MapLevelProvider;\n /**\n * Parent scope for everything the runtime loads - typically `scene.loader`.\n *\n * The runtime creates its own child scope under it and never destroys the\n * scope it was given: the caller keeps owning that lifetime.\n */\n readonly scope: LoaderScope;\n /** Diagnostic name for the runtime's own scope. Defaults to the world's name. */\n readonly name?: string;\n}\n\n/** Options for a {@link MapWorldRuntime.loadLevel} that spawns nothing. */\nexport interface MapLevelCancelOptions {\n /** Cancels this load. Unloading the level cancels it too. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * Options for a {@link MapWorldRuntime.loadLevel} that also spawns the level's\n * map objects. `spawner` and `context` travel together: the context exists to\n * be handed to that spawner's factories.\n */\nexport interface MapLevelLoadOptions<Context, Result extends Destroyable> extends MapLevelCancelOptions {\n /** Spawns the level's map objects as part of the load. */\n readonly spawner: MapObjectSpawner<Context, Result>;\n /** Value handed to every factory of `spawner`. */\n readonly context: Context;\n}\n\n/**\n * One loaded level and everything that load produced.\n *\n * Created by {@link MapWorldRuntime.loadLevel}; destroying it unloads the\n * level. There is nothing else to release afterwards - the map, the spawned\n * objects and the level's asset claims all end here.\n *\n * @typeParam Result - what the level's spawner produced, if it had one.\n */\nexport class MapLevelRuntime<Result extends Destroyable = Destroyable> implements Destroyable {\n /** {@link MapLevel.id} of the loaded level. */\n public readonly id: string;\n /** Metadata of the loaded level. */\n public readonly level: MapLevel;\n /** The level's asset scope. Released last, after everything that could hold an asset is gone. */\n public readonly scope: LoaderScope;\n /** The level's runtime map, owned by this runtime. */\n public readonly map: TileMap;\n /** What the level's spawner produced, or `null` when the load ran without one. */\n public readonly spawns: MapSpawnSession<Result> | null;\n\n private readonly _onDestroy: (runtime: MapLevelRuntime) => void;\n private _destroyed = false;\n\n /** Level runtimes are produced by {@link MapWorldRuntime.loadLevel}. @internal */\n public constructor(level: MapLevel, scope: LoaderScope, map: TileMap, spawns: MapSpawnSession<Result> | null, onDestroy: (runtime: MapLevelRuntime) => void) {\n this.id = level.id;\n this.level = level;\n this.scope = scope;\n this.map = map;\n this.spawns = spawns;\n this._onDestroy = onDestroy;\n }\n\n /** Whether this level has been unloaded. */\n public get destroyed(): boolean {\n return this._destroyed;\n }\n\n /**\n * Unload the level: destroy the spawned objects (reverse spawn order), then\n * the map, then release the level's asset claims. Idempotent.\n *\n * The scope goes last because everything before it may still be reading a\n * texture the scope keeps resident.\n */\n public destroy(): void {\n if (this._destroyed) return;\n\n this._destroyed = true;\n this._onDestroy(this);\n\n // A failure in one step must not strand the next: the scope is what holds\n // the level's asset claims, and skipping it leaks them for good.\n if (this.spawns !== null) guardedDestroy(this.spawns, `spawn session of level \"${this.id}\"`);\n guardedDestroy(this.map, `map of level \"${this.id}\"`);\n this.scope.destroy();\n }\n}\n\ninterface InFlightLoad {\n readonly promise: Promise<MapLevelRuntime>;\n readonly controller: AbortController;\n}\n\n/**\n * The live side of a {@link MapWorld}: loads and unloads levels on demand, each\n * with its own asset scope and its own lifetime.\n *\n * ExoJS provides the mechanism, not the streaming policy. Nothing here watches\n * a camera or guesses a radius: game code reads\n * {@link MapWorld.getNeighbours} / {@link MapWorld.getLevelsInBounds} and calls\n * {@link loadLevel} and {@link unloadLevel} when it decides to.\n *\n * Scope layout - the runtime owns everything below its own scope and nothing\n * above it:\n *\n * ```text\n * scene.loader (given to the runtime, never destroyed by it)\n * └─ world (the runtime's own scope)\n * ├─ level:forest\n * └─ level:cave\n * ```\n *\n * @example\n * ```ts\n * const runtime = new MapWorldRuntime({\n * world,\n * scope: scene.loader,\n * load: ({ level, scope, signal }) => buildLevelMap(level, scope, signal),\n * });\n *\n * const forest = await runtime.loadLevel('forest', { spawner, context });\n * scene.addChild(forest.map.createView().root);\n *\n * runtime.unloadLevel('forest');\n * ```\n */\nexport class MapWorldRuntime implements Destroyable {\n /** The world this runtime streams levels out of. */\n public readonly world: MapWorld;\n /** The runtime's own scope: parent of every level scope, child of the injected one. */\n public readonly scope: LoaderScope;\n\n private readonly _load: MapLevelProvider;\n private readonly _live = new Map<string, MapLevelRuntime>();\n private readonly _inFlight = new Map<string, InFlightLoad>();\n private _destroyed = false;\n\n public constructor(options: MapWorldRuntimeOptions) {\n this.world = options.world;\n this._load = options.load;\n this.scope = options.scope.createScope({ name: options.name ?? (this.world.name || 'world') });\n }\n\n /** Whether {@link destroy} has run. */\n public get destroyed(): boolean {\n return this._destroyed;\n }\n\n /**\n * Every currently **loaded** level, in load order - not the world's level\n * set, which stays on {@link MapWorld.levels} (reachable as\n * `runtime.world.levels`). Returns a fresh array.\n */\n public get levels(): readonly MapLevelRuntime[] {\n return [...this._live.values()];\n }\n\n /** The loaded level with this id, or `undefined` when it is not loaded. */\n public getLevel(id: string): MapLevelRuntime | undefined {\n return this._live.get(id);\n }\n\n /** Whether the level is loaded. A load still in flight does not count as loaded. */\n public isLoaded(id: string): boolean {\n return this._live.has(id);\n }\n\n /** Whether a load for this level is currently running. */\n public isLoading(id: string): boolean {\n return this._inFlight.has(id);\n }\n\n public loadLevel(id: string, options?: MapLevelCancelOptions): Promise<MapLevelRuntime>;\n public loadLevel<Context, Result extends Destroyable>(id: string, options: MapLevelLoadOptions<Context, Result>): Promise<MapLevelRuntime<Result>>;\n /**\n * Load a level, or return the one already loaded.\n *\n * At most one runtime exists per level id. Calling this while the level is\n * loaded resolves to that runtime, and calling it while a load is in flight\n * joins that load rather than starting a second one. **Options belong to the\n * call that actually starts the load** - a joining call's `spawner`,\n * `context` and `signal` are not applied, because the level being produced is\n * the one the first call asked for.\n *\n * Pass `{ spawner, context }` to spawn the level's objects as part of the\n * load, or just `{ signal }` to load a level without spawning anything.\n *\n * A failed or aborted load leaves nothing behind: the map, anything the\n * spawner created, and the level's asset claims are all released before the\n * rejection surfaces, and the level can be loaded again.\n *\n * @throws {Error} when the world has no level with this id, or the runtime is destroyed.\n * @throws {DOMException} named `AbortError` when the load is cancelled.\n */\n public loadLevel<Context = void, Result extends Destroyable = Destroyable>(\n id: string,\n options?: MapLevelLoadOptions<Context, Result> | MapLevelCancelOptions,\n ): Promise<MapLevelRuntime<Result>> {\n // The live map erases Result - one runtime holds levels loaded with\n // different spawners - while every call site keeps its own through the\n // signature above.\n const typed = (promise: Promise<MapLevelRuntime>): Promise<MapLevelRuntime<Result>> => promise as Promise<MapLevelRuntime<Result>>;\n\n if (this._destroyed) {\n return Promise.reject(new Error(`MapWorldRuntime: cannot load level \"${id}\" - the runtime is destroyed.`));\n }\n\n const live = this._live.get(id);\n if (live !== undefined) return typed(Promise.resolve(live));\n\n const pending = this._inFlight.get(id);\n if (pending !== undefined) return typed(pending.promise);\n\n const level = this.world.getLevel(id);\n if (level === undefined) {\n return Promise.reject(new Error(`MapWorldRuntime: world \"${this.world.name}\" has no level with id \"${id}\".`));\n }\n\n const controller = new AbortController();\n const signal = options?.signal;\n const forward = (): void => controller.abort();\n\n if (signal !== undefined) {\n if (signal.aborted) {\n return Promise.reject(new DOMException(`Level \"${id}\" load was cancelled.`, 'AbortError'));\n }\n\n signal.addEventListener('abort', forward, { once: true });\n }\n\n const promise = this._runLoad(level, controller.signal, options).finally(() => {\n // The caller's signal can outlive the load by a lot - a scene-lifetime\n // signal would otherwise accumulate one listener per level loaded.\n signal?.removeEventListener('abort', forward);\n\n // Only clear the entry this call installed: an unload during the load\n // may already have started a fresh one under the same id.\n if (this._inFlight.get(id)?.controller === controller) this._inFlight.delete(id);\n });\n\n this._inFlight.set(id, { promise, controller });\n\n return typed(promise);\n }\n\n /**\n * Unload a level, or cancel its load when one is in flight. Returns whether\n * there was anything to unload.\n *\n * A cancelled load rejects with an `AbortError` for whoever started it, and\n * the level is immediately loadable again - a `loadLevel` issued in the same\n * turn starts a fresh load rather than joining the one just cancelled.\n */\n public unloadLevel(id: string): boolean {\n const pending = this._inFlight.get(id);\n\n if (pending !== undefined) {\n // Drop the slot before aborting: the abort surfaces as a rejection one\n // microtask later, so a same-turn reload would otherwise join the load\n // that is on its way out and reject with it.\n this._inFlight.delete(id);\n pending.controller.abort();\n return true;\n }\n\n const live = this._live.get(id);\n\n if (live !== undefined) {\n live.destroy();\n return true;\n }\n\n return false;\n }\n\n /**\n * Cancel every load in flight, unload every loaded level in reverse load\n * order, and release the runtime's own scope. Idempotent.\n *\n * The scope the runtime was constructed with is left alone - it belongs to\n * whoever passed it in.\n */\n public destroy(): void {\n if (this._destroyed) return;\n\n this._destroyed = true;\n\n const pending = [...this._inFlight.values()];\n this._inFlight.clear();\n\n for (const load of pending) {\n load.controller.abort();\n }\n\n // One level's teardown must not strand the levels after it, nor the\n // runtime's own scope - the assets those still hold would leak.\n for (const runtime of [...this._live.values()].reverse()) {\n guardedDestroy(runtime, `level \"${runtime.id}\"`);\n }\n\n this._live.clear();\n this.scope.destroy();\n }\n\n private async _runLoad<Context, Result extends Destroyable>(\n level: MapLevel,\n signal: AbortSignal,\n options: MapLevelLoadOptions<Context, Result> | MapLevelCancelOptions | undefined,\n ): Promise<MapLevelRuntime> {\n const scope = this.scope.createScope({ name: `level:${level.id}` });\n let map: TileMap | undefined;\n let spawns: MapSpawnSession<Result> | null = null;\n\n try {\n throwIfAborted(level.id, signal);\n\n map = await this._load({ level, scope, signal });\n\n throwIfAborted(level.id, signal);\n\n if (options !== undefined && 'spawner' in options) {\n spawns = await options.spawner.spawn(map, options.context, { signal });\n throwIfAborted(level.id, signal);\n }\n } catch (error) {\n // The scope must be released even when tearing down what came before it\n // fails, or the level's asset claims outlive the failed load forever.\n if (spawns !== null) guardedDestroy(spawns, `spawn session of level \"${level.id}\"`);\n if (map !== undefined) guardedDestroy(map, `map of level \"${level.id}\"`);\n scope.destroy();\n throw error;\n }\n\n const runtime = new MapLevelRuntime<Result>(level, scope, map, spawns, destroyed => {\n if (this._live.get(destroyed.id) === destroyed) this._live.delete(destroyed.id);\n });\n\n this._live.set(level.id, runtime);\n\n return runtime;\n }\n}\n\n/**\n * Teardown step that reports a failure instead of propagating it, so the steps\n * after it still run. Matches how the application reports a subsystem that\n * fails to dispose.\n */\nconst guardedDestroy = (target: Destroyable, what: string): void => {\n try {\n target.destroy();\n } catch (error) {\n logger.error(`MapWorldRuntime: destroying the ${what} failed; continuing teardown.`, {\n source: 'tilemap',\n ...(error instanceof Error && { error }),\n });\n }\n};\n\nconst throwIfAborted = (id: string, signal: AbortSignal): void => {\n if (signal.aborted) {\n throw new DOMException(`Level \"${id}\" load was cancelled.`, 'AbortError');\n }\n};\n"],"mappings":"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"}
|