@aethervtt/shard-sprite 0.0.1 → 0.1.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/dist/baseline/lights2d.d.ts +6 -0
- package/dist/baseline/lights2d.js +114 -0
- package/dist/ground.d.ts +81 -0
- package/dist/ground.js +394 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +52 -2
- package/dist/lighting-shaders.js +4 -4
- package/dist/lights2d.d.ts +23 -7
- package/dist/lights2d.js +78 -40
- package/dist/methods.d.ts +82 -0
- package/dist/methods.js +246 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +43 -6
- package/dist/render.d.ts +19 -7
- package/dist/render.js +60 -51
- package/dist/shaders.js +11 -6
- package/dist/tilemap-format.d.ts +39 -0
- package/dist/tilemap-format.js +177 -0
- package/dist/tilemap.d.ts +83 -12
- package/dist/tilemap.js +311 -58
- package/dist/validate.d.ts +11 -0
- package/dist/validate.js +49 -0
- package/package.json +9 -9
package/dist/tilemap.js
CHANGED
|
@@ -8,20 +8,51 @@ import {
|
|
|
8
8
|
} from "@aethervtt/shard-core";
|
|
9
9
|
import { Visibility } from "@aethervtt/shard-render";
|
|
10
10
|
import { Transform } from "@aethervtt/shard-transform";
|
|
11
|
+
import {
|
|
12
|
+
flagsFromBase64,
|
|
13
|
+
layerChunks,
|
|
14
|
+
readChunkRows,
|
|
15
|
+
rowsCanName,
|
|
16
|
+
tilesFromBase64,
|
|
17
|
+
tilesToBase64,
|
|
18
|
+
toBase64
|
|
19
|
+
} from "./tilemap-format.js";
|
|
11
20
|
const TileFlags = { FlipX: 1, FlipY: 2, Rotate90: 4 };
|
|
12
21
|
const TilemapDataSchema = defineSchema(
|
|
13
22
|
"sprite/TilemapData",
|
|
14
23
|
{
|
|
24
|
+
version: t.u8({
|
|
25
|
+
default: 1,
|
|
26
|
+
min: 1,
|
|
27
|
+
max: 2,
|
|
28
|
+
description: "2: tiles are named through the palette. 1 (old files): atlas region + 1."
|
|
29
|
+
}),
|
|
30
|
+
encoding: t.enum(["base64", "rows"], {
|
|
31
|
+
description: "rows: layers hold chunks of run-length rows (readable, one-line diffs). base64: tiles and flags as base64."
|
|
32
|
+
}),
|
|
33
|
+
chunkSize: t.u16({
|
|
34
|
+
default: 16,
|
|
35
|
+
min: 1,
|
|
36
|
+
max: 256,
|
|
37
|
+
description: "Tiles per chunk side in the rows encoding."
|
|
38
|
+
}),
|
|
39
|
+
palette: t.list(t.string, {
|
|
40
|
+
description: "Version 2: atlas region names; tile id N is palette entry N \u2212 1."
|
|
41
|
+
}),
|
|
15
42
|
layers: t.list(
|
|
16
43
|
t.struct({
|
|
17
44
|
name: t.string({ description: 'Layer name, e.g. "ground".' }),
|
|
18
45
|
width: t.u32({ min: 1, description: "Tiles across." }),
|
|
19
46
|
height: t.u32({ min: 1, description: "Tiles down." }),
|
|
20
47
|
tiles: t.string({
|
|
21
|
-
description: "
|
|
48
|
+
description: "base64 encoding: width \xD7 height little-endian u16s, row by row from the top: palette index + 1 (version 1: atlas region + 1); 0 is empty."
|
|
22
49
|
}),
|
|
23
50
|
flags: t.string({
|
|
24
|
-
description: "
|
|
51
|
+
description: "base64 encoding: width \xD7 height bytes: 1 flip x, 2 flip y, 4 rotate 90\xB0. Empty: none."
|
|
52
|
+
}),
|
|
53
|
+
chunks: t.json({
|
|
54
|
+
default: {},
|
|
55
|
+
description: 'rows encoding: "cx,cy" \u2192 rows from the top of the chunk, each space-separated runs of name[:fx+fy+r90][*count], "." empty. Chunks with no tiles are left out.'
|
|
25
56
|
}),
|
|
26
57
|
occludes: t.bool({
|
|
27
58
|
description: "Filled cells block 2D light (shadowed PointLight2d and SpotLight2d)."
|
|
@@ -31,8 +62,10 @@ const TilemapDataSchema = defineSchema(
|
|
|
31
62
|
),
|
|
32
63
|
animations: t.list(
|
|
33
64
|
t.struct({
|
|
34
|
-
tile: t.
|
|
35
|
-
|
|
65
|
+
tile: t.json({
|
|
66
|
+
description: "The tile that animates, everywhere it appears: a palette name (v1: a tile id)."
|
|
67
|
+
}),
|
|
68
|
+
frames: t.list(t.json, { description: "Tiles it cycles through (names; v1: ids)." }),
|
|
36
69
|
frameTime: t.f32({
|
|
37
70
|
default: 0.2,
|
|
38
71
|
min: 1e-3,
|
|
@@ -44,7 +77,7 @@ const TilemapDataSchema = defineSchema(
|
|
|
44
77
|
)
|
|
45
78
|
},
|
|
46
79
|
{
|
|
47
|
-
description: "Tile layers for a Tilemap:
|
|
80
|
+
description: "Tile layers for a Tilemap: tiles named through a palette of atlas regions, per-tile flags, animated tiles. Saved as readable rows or compact base64."
|
|
48
81
|
}
|
|
49
82
|
);
|
|
50
83
|
const MAX_EDITS = 4096;
|
|
@@ -93,66 +126,243 @@ class TileLayer {
|
|
|
93
126
|
class TilemapData {
|
|
94
127
|
layers;
|
|
95
128
|
animations;
|
|
129
|
+
/**
|
|
130
|
+
* Atlas region names: tile id N draws the region named `palette[N - 1]` (0059). Undefined for
|
|
131
|
+
* maps made before palettes, whose ids are atlas regions + 1.
|
|
132
|
+
*/
|
|
133
|
+
palette;
|
|
134
|
+
/** How the map is saved: readable rows (the default) or compact base64. */
|
|
135
|
+
encoding = "rows";
|
|
136
|
+
/** Tiles per chunk side in the rows encoding. */
|
|
137
|
+
chunkSize = 16;
|
|
96
138
|
/** Bumps when the layers are replaced (hot reload). */
|
|
97
139
|
version = 0;
|
|
98
|
-
constructor(layers = [], animations = []) {
|
|
140
|
+
constructor(layers = [], animations = [], palette) {
|
|
99
141
|
this.layers = layers;
|
|
100
142
|
this.animations = animations;
|
|
143
|
+
this.palette = palette;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* An empty map of `layers` layers, width × height tiles each. With a palette, tile ids name its
|
|
147
|
+
* entries; without, they're atlas regions + 1.
|
|
148
|
+
*/
|
|
149
|
+
static create(width, height, layers = ["main"], palette) {
|
|
150
|
+
return new TilemapData(
|
|
151
|
+
layers.map((name) => new TileLayer(name, width, height)),
|
|
152
|
+
[],
|
|
153
|
+
palette ? [...palette] : void 0
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
/** The id of a palette name, adding it to the palette if it's new. Needs a palette. */
|
|
157
|
+
tileId(name) {
|
|
158
|
+
if (!this.palette) {
|
|
159
|
+
throw new ShardError("sprite/no-palette", "This tilemap names no tiles (version 1)", {
|
|
160
|
+
hint: "Save it once with its atlas (tilemapToJson(data, { atlas })) to give it a palette."
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
let i = this.palette.indexOf(name);
|
|
164
|
+
if (i === -1) {
|
|
165
|
+
i = this.palette.length;
|
|
166
|
+
this.palette.push(name);
|
|
167
|
+
}
|
|
168
|
+
return i + 1;
|
|
101
169
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
170
|
+
/** The name a tile id draws: through the palette, or the atlas for version 1 maps. */
|
|
171
|
+
tileName(id, atlas) {
|
|
172
|
+
if (id === 0) return void 0;
|
|
173
|
+
return this.palette ? this.palette[id - 1] : atlas?.names[id - 1];
|
|
105
174
|
}
|
|
175
|
+
/** A map from its file's JSON (`$schema`, an editor's pointer, is ignored). */
|
|
106
176
|
static fromJson(json) {
|
|
177
|
+
if (json && typeof json === "object" && "$schema" in json) {
|
|
178
|
+
const { $schema: _, ...body } = json;
|
|
179
|
+
json = body;
|
|
180
|
+
}
|
|
107
181
|
const v = TilemapDataSchema.deserialize(json);
|
|
182
|
+
const named = v.version >= 2;
|
|
183
|
+
const palette = named ? [...v.palette] : void 0;
|
|
184
|
+
const index = new Map((palette ?? []).map((name, i) => [name, i]));
|
|
108
185
|
const layers = v.layers.map((l, i) => {
|
|
109
186
|
const n = l.width * l.height;
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
throw new ShardError(
|
|
113
|
-
"sprite/invalid-tilemap",
|
|
114
|
-
`Layer "${l.name}" has ${tileBytes.length / 2} tiles; ${l.width}\xD7${l.height} needs ${n}`,
|
|
115
|
-
{ path: `/layers/${i}/tiles` }
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
const tiles = new Uint16Array(n);
|
|
119
|
-
const view = new DataView(tileBytes.buffer, tileBytes.byteOffset, tileBytes.byteLength);
|
|
120
|
-
for (let k = 0; k < n; k++) tiles[k] = view.getUint16(k * 2, true);
|
|
187
|
+
const path = `/layers/${i}`;
|
|
188
|
+
let tiles;
|
|
121
189
|
let flags;
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
190
|
+
if (named && v.encoding === "rows") {
|
|
191
|
+
tiles = new Uint16Array(n);
|
|
192
|
+
flags = new Uint8Array(n);
|
|
193
|
+
const grid = { width: l.width, height: l.height, tiles, flags };
|
|
194
|
+
const chunks = l.chunks ?? {};
|
|
195
|
+
if (typeof chunks !== "object" || Array.isArray(chunks)) {
|
|
196
|
+
throw new ShardError("sprite/invalid-tilemap", 'Expected chunks by "cx,cy"', {
|
|
197
|
+
path: `${path}/chunks`
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
for (const [key, rows] of Object.entries(chunks)) {
|
|
201
|
+
const m = /^(\d+),(\d+)$/.exec(key);
|
|
202
|
+
const at = `${path}/chunks/${key}`;
|
|
203
|
+
if (!m) {
|
|
204
|
+
throw new ShardError("sprite/invalid-tilemap", `Bad chunk key "${key}"`, {
|
|
205
|
+
path: at,
|
|
206
|
+
hint: 'Chunk keys are "cx,cy": the chunk column and row from the top left.'
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
readChunkRows(grid, palette, index, Number(m[1]), Number(m[2]), v.chunkSize, rows, at);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
tiles = tilesFromBase64(l.tiles, n, `${path}/tiles`, l.name);
|
|
213
|
+
flags = flagsFromBase64(l.flags, n, `${path}/flags`, l.name);
|
|
214
|
+
if (palette) {
|
|
215
|
+
for (let k = 0; k < n; k++) {
|
|
216
|
+
if (tiles[k] > palette.length) {
|
|
217
|
+
throw new ShardError(
|
|
218
|
+
"sprite/invalid-tilemap",
|
|
219
|
+
`Tile id ${tiles[k]} at (${k % l.width}, ${Math.floor(k / l.width)}) has no palette entry`,
|
|
220
|
+
{ path: `${path}/tiles` }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
130
224
|
}
|
|
131
225
|
}
|
|
132
226
|
const layer = new TileLayer(l.name, l.width, l.height, tiles, flags);
|
|
133
227
|
layer.occludes = l.occludes;
|
|
134
228
|
return layer;
|
|
135
229
|
});
|
|
136
|
-
|
|
230
|
+
const idOf = (value, at) => {
|
|
231
|
+
if (!named) {
|
|
232
|
+
if (typeof value !== "number") {
|
|
233
|
+
throw new ShardError("sprite/invalid-tilemap", "Expected a tile id", { path: at });
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
if (typeof value !== "string") {
|
|
238
|
+
throw new ShardError("sprite/invalid-tilemap", "Expected a palette name", { path: at });
|
|
239
|
+
}
|
|
240
|
+
let p = index.get(value);
|
|
241
|
+
if (p === void 0) {
|
|
242
|
+
p = palette.length;
|
|
243
|
+
palette.push(value);
|
|
244
|
+
index.set(value, p);
|
|
245
|
+
}
|
|
246
|
+
return p + 1;
|
|
247
|
+
};
|
|
248
|
+
const animations = v.animations.map((a, i) => ({
|
|
249
|
+
tile: idOf(a.tile, `/animations/${i}/tile`),
|
|
250
|
+
frames: a.frames.map((f, k) => idOf(f, `/animations/${i}/frames/${k}`)),
|
|
251
|
+
frameTime: a.frameTime
|
|
252
|
+
}));
|
|
253
|
+
const data = new TilemapData(layers, animations, palette);
|
|
254
|
+
data.encoding = named ? v.encoding : "base64";
|
|
255
|
+
data.chunkSize = v.chunkSize;
|
|
256
|
+
return data;
|
|
137
257
|
}
|
|
138
|
-
|
|
258
|
+
/**
|
|
259
|
+
* The map as version 2 JSON, in its encoding (or `options.encoding`). A version 1 map needs its
|
|
260
|
+
* atlas to name its tiles; its palette lists them in first use order. A map with a palette keeps
|
|
261
|
+
* it in order, so an edit that uses a known tile changes one row.
|
|
262
|
+
*/
|
|
263
|
+
toJson(options = {}) {
|
|
264
|
+
const encoding = options.encoding ?? this.encoding;
|
|
265
|
+
let palette;
|
|
266
|
+
let remap;
|
|
267
|
+
if (this.palette) palette = this.palette;
|
|
268
|
+
else {
|
|
269
|
+
const atlas = options.atlas;
|
|
270
|
+
if (!atlas) {
|
|
271
|
+
throw new ShardError(
|
|
272
|
+
"sprite/tilemap-needs-atlas",
|
|
273
|
+
"Saving a version 1 tilemap needs its atlas",
|
|
274
|
+
{
|
|
275
|
+
hint: "Its tiles are atlas regions: pass the atlas so they can be named (the palette)."
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
palette = [];
|
|
280
|
+
const byRegion = /* @__PURE__ */ new Map();
|
|
281
|
+
const use = (id) => {
|
|
282
|
+
if (id === 0) return 0;
|
|
283
|
+
let p = byRegion.get(id);
|
|
284
|
+
if (p === void 0) {
|
|
285
|
+
const name2 = atlas.names[id - 1];
|
|
286
|
+
if (name2 === void 0) {
|
|
287
|
+
throw new ShardError(
|
|
288
|
+
"sprite/unknown-tile",
|
|
289
|
+
`Tile ${id} is past the atlas's ${atlas.names.length} regions`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
p = palette.length;
|
|
293
|
+
palette.push(name2);
|
|
294
|
+
byRegion.set(id, p);
|
|
295
|
+
}
|
|
296
|
+
return p + 1;
|
|
297
|
+
};
|
|
298
|
+
for (const l of this.layers) for (let k = 0; k < l.tiles.length; k++) use(l.tiles[k]);
|
|
299
|
+
for (const a of this.animations) {
|
|
300
|
+
use(a.tile);
|
|
301
|
+
for (const f of a.frames) use(f);
|
|
302
|
+
}
|
|
303
|
+
remap = use;
|
|
304
|
+
}
|
|
305
|
+
if (encoding === "rows") {
|
|
306
|
+
for (const name2 of palette) {
|
|
307
|
+
if (!rowsCanName(name2)) {
|
|
308
|
+
throw new ShardError(
|
|
309
|
+
"sprite/unencodable-tile",
|
|
310
|
+
`Tile "${name2}" can't be written in rows`,
|
|
311
|
+
{
|
|
312
|
+
hint: 'Region names in rows have no spaces, ":", "*" or "+". Save with encoding base64.'
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const idsOf = (l) => {
|
|
319
|
+
if (!remap) return l.tiles;
|
|
320
|
+
const out = new Uint16Array(l.tiles.length);
|
|
321
|
+
for (let k = 0; k < out.length; k++) out[k] = remap(l.tiles[k]);
|
|
322
|
+
return out;
|
|
323
|
+
};
|
|
324
|
+
const name = (id) => palette[(remap ? remap(id) : id) - 1];
|
|
139
325
|
return {
|
|
326
|
+
version: 2,
|
|
327
|
+
encoding,
|
|
328
|
+
...encoding === "rows" ? { chunkSize: this.chunkSize } : {},
|
|
329
|
+
palette: [...palette],
|
|
140
330
|
layers: this.layers.map((l) => {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
331
|
+
const tiles = idsOf(l);
|
|
332
|
+
const head = { name: l.name, width: l.width, height: l.height };
|
|
333
|
+
const cells = encoding === "rows" ? {
|
|
334
|
+
chunks: layerChunks(
|
|
335
|
+
{ width: l.width, height: l.height, tiles, flags: l.flags },
|
|
336
|
+
palette,
|
|
337
|
+
this.chunkSize
|
|
338
|
+
)
|
|
339
|
+
} : {
|
|
340
|
+
tiles: tilesToBase64(tiles),
|
|
341
|
+
flags: l.flags.some((f) => f !== 0) ? toBase64(l.flags) : ""
|
|
151
342
|
};
|
|
343
|
+
return { ...head, ...cells, ...l.occludes ? { occludes: true } : {} };
|
|
152
344
|
}),
|
|
153
|
-
animations: this.animations
|
|
345
|
+
animations: this.animations.map((a) => ({
|
|
346
|
+
tile: name(a.tile),
|
|
347
|
+
frames: a.frames.map(name),
|
|
348
|
+
frameTime: a.frameTime
|
|
349
|
+
}))
|
|
154
350
|
};
|
|
155
351
|
}
|
|
352
|
+
sameAs(other) {
|
|
353
|
+
if (this.encoding !== other.encoding || this.chunkSize !== other.chunkSize || this.layers.length !== other.layers.length || JSON.stringify(this.palette) !== JSON.stringify(other.palette) || JSON.stringify(this.animations) !== JSON.stringify(other.animations))
|
|
354
|
+
return false;
|
|
355
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
356
|
+
const a = this.layers[i];
|
|
357
|
+
const b = other.layers[i];
|
|
358
|
+
if (a.name !== b.name || a.width !== b.width || a.height !== b.height) return false;
|
|
359
|
+
if (a.occludes !== b.occludes) return false;
|
|
360
|
+
for (let k = 0; k < a.tiles.length; k++) {
|
|
361
|
+
if (a.tiles[k] !== b.tiles[k] || a.flags[k] !== b.flags[k]) return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
156
366
|
/** The largest tile index any layer uses (for validating against an atlas). */
|
|
157
367
|
maxTile() {
|
|
158
368
|
let max = 0;
|
|
@@ -160,24 +370,28 @@ class TilemapData {
|
|
|
160
370
|
for (let k = 0; k < l.tiles.length; k++) max = Math.max(max, l.tiles[k]);
|
|
161
371
|
return max;
|
|
162
372
|
}
|
|
373
|
+
/** Takes another map's content (hot reload). Identical content (a save reloading) changes nothing. */
|
|
163
374
|
copyFrom(other) {
|
|
375
|
+
if (this.sameAs(other)) return;
|
|
164
376
|
this.layers = other.layers;
|
|
165
377
|
this.animations = other.animations;
|
|
378
|
+
this.palette = other.palette;
|
|
379
|
+
this.encoding = other.encoding;
|
|
380
|
+
this.chunkSize = other.chunkSize;
|
|
166
381
|
this.version++;
|
|
167
382
|
}
|
|
168
383
|
}
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
384
|
+
function tileNames(data, atlas) {
|
|
385
|
+
if (data.palette) return data.palette;
|
|
386
|
+
if (atlas) return atlas.names;
|
|
387
|
+
throw new ShardError("sprite/tilemap-needs-atlas", "This tilemap names no tiles (version 1)", {
|
|
388
|
+
hint: "Give a Tilemap using it an atlas, or save it once with one (tilemapToText(data, { atlas }))."
|
|
389
|
+
});
|
|
174
390
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
return btoa(s);
|
|
391
|
+
function tilemapToText(data, options = {}) {
|
|
392
|
+
const json = data.toJson(options);
|
|
393
|
+
return `${JSON.stringify(options.schema === void 0 ? json : { $schema: options.schema, ...json }, null, 2)}
|
|
394
|
+
`;
|
|
181
395
|
}
|
|
182
396
|
class TilemapDataStore extends AssetStore {
|
|
183
397
|
constructor() {
|
|
@@ -216,14 +430,20 @@ const Tilemap = defineComponent(
|
|
|
216
430
|
layer: t.i16({
|
|
217
431
|
description: "Draw-order band, like Sprite.layer (tilemaps draw first in a band)."
|
|
218
432
|
}),
|
|
219
|
-
lit: t.
|
|
220
|
-
|
|
221
|
-
description: "Lit by 2D lights under a Lighting2d camera."
|
|
433
|
+
lit: t.enum(["2d", "3d", "none"], {
|
|
434
|
+
description: "2d: lit by 2D lights under a Lighting2d camera. 3d: lit by 3D lights and shadows like a floor. none: drawn as the atlas is. On the ground (with a GroundLayer) there are no 2D lights: 2d and 3d both light it like a floor."
|
|
222
435
|
})
|
|
223
436
|
},
|
|
224
437
|
{
|
|
225
|
-
description: "Tile layers drawn over the entity's XY plane: tile (x, y) covers [x, x+1] \xD7 [\u2212y\u22121, \u2212y] tile sizes, rows going down from the top.",
|
|
226
|
-
requires: [Transform, Visibility]
|
|
438
|
+
description: "Tile layers drawn over the entity's XY plane: tile (x, y) covers [x, x+1] \xD7 [\u2212y\u22121, \u2212y] tile sizes, rows going down from the top. With a GroundLayer (0059), they draw in the ground phase among the tabletop's bands (lay them flat with tilemapOnGround).",
|
|
439
|
+
requires: [Transform, Visibility],
|
|
440
|
+
version: 2,
|
|
441
|
+
// Version 1: lit was a flag for 2D lighting.
|
|
442
|
+
migrate: (_from, json) => {
|
|
443
|
+
const v = { ...json };
|
|
444
|
+
if (typeof v.lit === "boolean") v.lit = v.lit ? "2d" : "none";
|
|
445
|
+
return v;
|
|
446
|
+
}
|
|
227
447
|
}
|
|
228
448
|
);
|
|
229
449
|
function layerOf(world, tilemap, layer) {
|
|
@@ -252,6 +472,36 @@ function setTile(world, tilemap, x, y, tile, options = {}) {
|
|
|
252
472
|
function tileAt(world, tilemap, x, y, layer = 0) {
|
|
253
473
|
return layerOf(world, tilemap, layer).get(x, y);
|
|
254
474
|
}
|
|
475
|
+
function unknownTiles(data, atlas) {
|
|
476
|
+
const out = [];
|
|
477
|
+
if (!data.palette) return out;
|
|
478
|
+
for (let i = 0; i < data.palette.length; i++) {
|
|
479
|
+
const name = data.palette[i];
|
|
480
|
+
if (atlas.region(name) !== -1) continue;
|
|
481
|
+
const id = i + 1;
|
|
482
|
+
let where;
|
|
483
|
+
for (let li = 0; li < data.layers.length && !where; li++) {
|
|
484
|
+
const l2 = data.layers[li];
|
|
485
|
+
const at = l2.tiles.indexOf(id);
|
|
486
|
+
if (at !== -1) where = { layer: li, x: at % l2.width, y: Math.floor(at / l2.width) };
|
|
487
|
+
}
|
|
488
|
+
if (!where) continue;
|
|
489
|
+
const l = data.layers[where.layer];
|
|
490
|
+
const cs = data.chunkSize;
|
|
491
|
+
const path = data.encoding === "rows" ? `/layers/${where.layer}/chunks/${Math.floor(where.x / cs)},${Math.floor(where.y / cs)}/${where.y % cs}` : `/layers/${where.layer}/tiles`;
|
|
492
|
+
out.push(
|
|
493
|
+
new ShardError(
|
|
494
|
+
"sprite/unknown-tile",
|
|
495
|
+
`Layer "${l.name}", cell (${where.x}, ${where.y}): tile "${name}" isn't a region of the atlas`,
|
|
496
|
+
{
|
|
497
|
+
path,
|
|
498
|
+
hint: "Rename it to a region the atlas has, or add the region to the atlas."
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
255
505
|
export {
|
|
256
506
|
TileFlags,
|
|
257
507
|
TileLayer,
|
|
@@ -263,5 +513,8 @@ export {
|
|
|
263
513
|
TilemapDataStore,
|
|
264
514
|
TilemapDatas,
|
|
265
515
|
setTile,
|
|
266
|
-
tileAt
|
|
516
|
+
tileAt,
|
|
517
|
+
tileNames,
|
|
518
|
+
tilemapToText,
|
|
519
|
+
unknownTiles
|
|
267
520
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ShardError, World } from '@aethervtt/shard-core';
|
|
2
|
+
/**
|
|
3
|
+
* Checks each Tilemap's data against its atlas (0059): a palette name the atlas has no region
|
|
4
|
+
* for is `sprite/unknown-tile`, naming the layer, the cell and the name, with a JSON pointer to
|
|
5
|
+
* the cell's row in the data file. Scenes and prefabs say which atlas draws which data, so the
|
|
6
|
+
* check runs over their files. Loads what it checks.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateTilemaps(world: World, files: readonly unknown[]): Promise<{
|
|
9
|
+
source: string;
|
|
10
|
+
errors: ShardError[];
|
|
11
|
+
}[]>;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import { TextureAtlas } from "./atlas.js";
|
|
3
|
+
import { TilemapData, unknownTiles } from "./tilemap.js";
|
|
4
|
+
function tilemapsIn(json, out) {
|
|
5
|
+
if (Array.isArray(json)) {
|
|
6
|
+
for (const item of json) tilemapsIn(item, out);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (!json || typeof json !== "object") return;
|
|
10
|
+
for (const [key, value] of Object.entries(json)) {
|
|
11
|
+
if (key === "sprite/Tilemap" && value && typeof value === "object") {
|
|
12
|
+
const v = value;
|
|
13
|
+
if (v.data && v.atlas) out.push({ data: v.data, atlas: v.atlas });
|
|
14
|
+
} else tilemapsIn(value, out);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function validateTilemaps(world, files) {
|
|
18
|
+
const server = assetServer(world);
|
|
19
|
+
const found = [];
|
|
20
|
+
for (const file of files) tilemapsIn(file, found);
|
|
21
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22
|
+
const out = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const pair of found) {
|
|
24
|
+
const data = server.entry(pair.data);
|
|
25
|
+
const atlas = server.entry(pair.atlas);
|
|
26
|
+
if (!data || !atlas || data.type !== "TilemapData" || atlas.type !== "TextureAtlas") continue;
|
|
27
|
+
const key = `${data.guid}|${atlas.guid}`;
|
|
28
|
+
if (seen.has(key)) continue;
|
|
29
|
+
seen.add(key);
|
|
30
|
+
try {
|
|
31
|
+
await Promise.all([server.load(data.guid), server.load(atlas.guid)]);
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const d = server.item(data.guid);
|
|
36
|
+
const a = server.item(atlas.guid);
|
|
37
|
+
if (!(d instanceof TilemapData) || !(a instanceof TextureAtlas)) continue;
|
|
38
|
+
const errors = unknownTiles(d, a);
|
|
39
|
+
if (errors.length === 0) continue;
|
|
40
|
+
const source = data.source ?? data.path;
|
|
41
|
+
const list = out.get(source) ?? [];
|
|
42
|
+
for (const e of errors) if (!list.some((x) => x.message === e.message)) list.push(e);
|
|
43
|
+
out.set(source, list);
|
|
44
|
+
}
|
|
45
|
+
return [...out].map(([source, errors]) => ({ source, errors }));
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
validateTilemaps
|
|
49
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-sprite",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@aethervtt/shard-assets": "0.0
|
|
20
|
-
"@aethervtt/shard-core": "0.0
|
|
21
|
-
"@aethervtt/shard-gpu": "0.0
|
|
22
|
-
"@aethervtt/shard-render": "0.0
|
|
23
|
-
"@aethervtt/shard-runtime": "0.0
|
|
24
|
-
"@aethervtt/shard-texture": "0.0
|
|
25
|
-
"@aethervtt/shard-transform": "0.0
|
|
19
|
+
"@aethervtt/shard-assets": "0.1.0",
|
|
20
|
+
"@aethervtt/shard-core": "0.1.0",
|
|
21
|
+
"@aethervtt/shard-gpu": "0.1.0",
|
|
22
|
+
"@aethervtt/shard-render": "0.1.0",
|
|
23
|
+
"@aethervtt/shard-runtime": "0.1.0",
|
|
24
|
+
"@aethervtt/shard-texture": "0.1.0",
|
|
25
|
+
"@aethervtt/shard-transform": "0.1.0"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"provenance": true
|
|
35
35
|
},
|
|
36
36
|
"shard": {
|
|
37
|
-
"buildSha": "
|
|
37
|
+
"buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
|
|
38
38
|
}
|
|
39
39
|
}
|