@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.
@@ -0,0 +1,246 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import { defineSchema, ShardError, t } from "@aethervtt/shard-core";
3
+ import { TextureAtlases } from "./atlas.js";
4
+ import { Tilemap, TilemapData, TilemapDatas, tileNames } from "./tilemap.js";
5
+ import { layerChunks, readChunkRows, rectRows, TILE_FLAG_NAMES } from "./tilemap-format.js";
6
+ function layerNamed(data, name) {
7
+ const layer = name ? data.layers.find((l) => l.name === name) : data.layers[0];
8
+ if (!layer) {
9
+ throw new ShardError("sprite/no-layer", `The tilemap has no layer ${JSON.stringify(name)}`, {
10
+ hint: `Its layers: ${data.layers.map((l) => l.name).join(", ") || "none"}.`
11
+ });
12
+ }
13
+ return layer;
14
+ }
15
+ function checkRect(layer, r, path) {
16
+ if (r.x + r.w > layer.width || r.y + r.h > layer.height) {
17
+ throw new ShardError(
18
+ "sprite/tile-out-of-range",
19
+ `Rect ${r.x},${r.y} ${r.w}\xD7${r.h} is outside the ${layer.width}\xD7${layer.height} layer "${layer.name}"`,
20
+ { path }
21
+ );
22
+ }
23
+ }
24
+ function chunkRect(data, layer, chunk, path) {
25
+ if (chunk.length !== 2) throw new ShardError("sprite/bad-chunk", "A chunk is [cx, cy]", { path });
26
+ const cs = data.chunkSize;
27
+ const x = chunk[0] * cs;
28
+ const y = chunk[1] * cs;
29
+ checkRect(layer, { x, y, w: 1, h: 1 }, path);
30
+ return { x, y, w: Math.min(cs, layer.width - x), h: Math.min(cs, layer.height - y) };
31
+ }
32
+ function flagsOf(value, path) {
33
+ if (typeof value === "number") return value;
34
+ if (!value) return 0;
35
+ let flags = 0;
36
+ for (const f of String(value).split("+")) {
37
+ const bit = TILE_FLAG_NAMES[f];
38
+ if (!bit) {
39
+ throw new ShardError("sprite/bad-flag", `Unknown tile flag "${f}"`, {
40
+ path,
41
+ hint: "Flags are fx, fy and r90, joined by +."
42
+ });
43
+ }
44
+ flags |= bit;
45
+ }
46
+ return flags;
47
+ }
48
+ function idOf(data, name) {
49
+ return name === "." || name === "" ? 0 : data.tileId(name);
50
+ }
51
+ function readTiles(data, options = {}, atlas) {
52
+ const layer = layerNamed(data, options.layer);
53
+ const names = tileNames(data, atlas);
54
+ const head = {
55
+ layer: layer.name,
56
+ width: layer.width,
57
+ height: layer.height,
58
+ chunkSize: data.chunkSize,
59
+ encoding: data.encoding
60
+ };
61
+ const rect = options.rect;
62
+ if (rect && rect.w > 0 && rect.h > 0) {
63
+ checkRect(layer, rect, "/rect");
64
+ return { ...head, rect, rows: rectRows(layer, names, rect.x, rect.y, rect.w, rect.h).rows };
65
+ }
66
+ if (options.chunk && options.chunk.length > 0) {
67
+ const r = chunkRect(data, layer, options.chunk, "/chunk");
68
+ return {
69
+ ...head,
70
+ chunk: [...options.chunk],
71
+ rows: rectRows(layer, names, r.x, r.y, r.w, r.h).rows
72
+ };
73
+ }
74
+ return { ...head, chunks: layerChunks(layer, names, data.chunkSize) };
75
+ }
76
+ function editTiles(data, options, atlas) {
77
+ const name = layerNamed(data, options.layer).name;
78
+ if (!data.palette) {
79
+ data.copyFrom(TilemapData.fromJson(data.toJson({ atlas: { names: tileNames(data, atlas) } })));
80
+ }
81
+ const layer = layerNamed(data, name);
82
+ let changed = 0;
83
+ const set = (x, y, id, flags, path) => {
84
+ if (x >= layer.width || y >= layer.height) {
85
+ throw new ShardError(
86
+ "sprite/tile-out-of-range",
87
+ `Cell (${x}, ${y}) is outside the ${layer.width}\xD7${layer.height} layer "${layer.name}"`,
88
+ { path }
89
+ );
90
+ }
91
+ const i = y * layer.width + x;
92
+ if (layer.tiles[i] === id && layer.flags[i] === flags) return;
93
+ layer.set(x, y, id, flags);
94
+ changed++;
95
+ };
96
+ const cells = options.cells ?? [];
97
+ for (let k = 0; k < cells.length; k++) {
98
+ const c = cells[k];
99
+ set(c.x, c.y, idOf(data, c.tile), flagsOf(c.flags, `/cells/${k}/flags`), `/cells/${k}`);
100
+ }
101
+ const fill = options.fill;
102
+ if (fill && fill.rect.w > 0 && fill.rect.h > 0) {
103
+ checkRect(layer, fill.rect, "/fill/rect");
104
+ const id = idOf(data, fill.tile);
105
+ for (let y = fill.rect.y; y < fill.rect.y + fill.rect.h; y++)
106
+ for (let x = fill.rect.x; x < fill.rect.x + fill.rect.w; x++) set(x, y, id, 0, "/fill");
107
+ }
108
+ const rows = options.rows;
109
+ if (rows && rows.chunk.length > 0) {
110
+ const r = chunkRect(data, layer, rows.chunk, "/rows/chunk");
111
+ const grid = {
112
+ width: layer.width,
113
+ height: layer.height,
114
+ tiles: layer.tiles.slice(),
115
+ flags: layer.flags.slice()
116
+ };
117
+ for (let y = r.y; y < r.y + r.h; y++) {
118
+ grid.tiles.fill(0, y * layer.width + r.x, y * layer.width + r.x + r.w);
119
+ grid.flags.fill(0, y * layer.width + r.x, y * layer.width + r.x + r.w);
120
+ }
121
+ const palette = data.palette;
122
+ const index = new Map(palette.map((n, i) => [n, i]));
123
+ readChunkRows(
124
+ grid,
125
+ palette,
126
+ index,
127
+ rows.chunk[0],
128
+ rows.chunk[1],
129
+ data.chunkSize,
130
+ rows.rows,
131
+ "/rows/rows"
132
+ );
133
+ for (let y = r.y; y < r.y + r.h; y++)
134
+ for (let x = r.x; x < r.x + r.w; x++) {
135
+ const i = y * layer.width + x;
136
+ set(x, y, grid.tiles[i], grid.flags[i], "/rows");
137
+ }
138
+ }
139
+ return changed;
140
+ }
141
+ function findTilemapData(world, asset) {
142
+ const server = assetServer(world);
143
+ const entry = server.entry(asset);
144
+ const data = server.item(asset);
145
+ if (!(data instanceof TilemapData)) {
146
+ throw new ShardError(
147
+ "sprite/unknown-tilemap",
148
+ entry ? `${asset} is a ${entry.type}, not loaded TilemapData` : `No tilemap data "${asset}" is loaded`,
149
+ { hint: "Name a *.tilemap.json asset by path or guid that a Tilemap in the world uses." }
150
+ );
151
+ }
152
+ const store = world.resource(TilemapDatas);
153
+ let atlas;
154
+ for (const table of world.query({ with: [Tilemap] }).tables) {
155
+ const dataRefs = table.column(Tilemap, "data");
156
+ const atlasRefs = table.column(Tilemap, "atlas");
157
+ for (let row = 0; row < table.count && !atlas; row++) {
158
+ if (store.get(dataRefs[row]) !== data) continue;
159
+ atlas = world.tryResource(TextureAtlases)?.get(atlasRefs[row]);
160
+ }
161
+ }
162
+ return { data, guid: entry?.guid, atlas };
163
+ }
164
+ const Rect = t.struct({
165
+ x: t.u32({ description: "Left column." }),
166
+ y: t.u32({ description: "Top row." }),
167
+ w: t.u32({ description: "Width in cells (0: no rect)." }),
168
+ h: t.u32({ description: "Height in cells." })
169
+ });
170
+ const tilemapMethods = [
171
+ {
172
+ name: "tilemap.read",
173
+ description: "A tilemap layer's cells as rows (0059): runs of name[:flags][*count], '.' empty, flags fx/fy/r90 joined by +. The whole layer by chunk, one chunk, or a rect.",
174
+ params: defineSchema("sprite/TilemapReadParams", {
175
+ asset: t.string({ required: true, description: "The TilemapData asset: path or guid." }),
176
+ layer: t.string({ description: "Layer name (default: the first)." }),
177
+ chunk: t.list(t.u32(), { description: "[cx, cy]: just this chunk." }),
178
+ rect: Rect
179
+ }),
180
+ handler: ({ world }, p) => {
181
+ const { data, atlas } = findTilemapData(world, p.asset);
182
+ return {
183
+ asset: p.asset,
184
+ ...readTiles(
185
+ data,
186
+ {
187
+ layer: p.layer || void 0,
188
+ chunk: p.chunk,
189
+ rect: p.rect
190
+ },
191
+ atlas
192
+ )
193
+ };
194
+ }
195
+ },
196
+ {
197
+ name: "tilemap.edit",
198
+ description: "Edits a tilemap layer's cells by tile name (0059): cells, a filled rect, or a chunk's rows. Applies to the live world (only the touched chunks re-upload); save writes the asset file in its own encoding. Returns the changed cell count.",
199
+ params: defineSchema("sprite/TilemapEditParams", {
200
+ asset: t.string({ required: true, description: "The TilemapData asset: path or guid." }),
201
+ layer: t.string({ description: "Layer name (default: the first)." }),
202
+ cells: t.list(
203
+ t.struct({
204
+ x: t.u32(),
205
+ y: t.u32(),
206
+ tile: t.string({ description: "Palette name; '.' clears the cell." }),
207
+ flags: t.string({ description: "fx, fy and r90, joined by +." })
208
+ }),
209
+ { description: "Cells to set." }
210
+ ),
211
+ fill: t.struct(
212
+ { rect: Rect, tile: t.string({ description: "Palette name; '.' clears." }) },
213
+ { description: "A rect to fill with one tile." }
214
+ ),
215
+ rows: t.struct(
216
+ {
217
+ chunk: t.list(t.u32(), { description: "[cx, cy]" }),
218
+ rows: t.list(t.string(), { description: "The chunk rows, from the top." })
219
+ },
220
+ { description: "Replaces one chunk's cells with rows, as tilemap.read returns them." }
221
+ ),
222
+ save: t.bool({ description: "Also write the asset file, keeping its encoding." })
223
+ }),
224
+ handler: async ({ world }, p) => {
225
+ const { data, guid, atlas } = findTilemapData(world, p.asset);
226
+ const changed = editTiles(
227
+ data,
228
+ {
229
+ layer: p.layer || void 0,
230
+ cells: p.cells,
231
+ fill: p.fill,
232
+ rows: p.rows
233
+ },
234
+ atlas
235
+ );
236
+ const saved = p.save ? await assetServer(world).writeSource(guid ?? p.asset, data.toJson()) : null;
237
+ return { changed, saved };
238
+ }
239
+ }
240
+ ];
241
+ export {
242
+ editTiles,
243
+ findTilemapData,
244
+ readTiles,
245
+ tilemapMethods
246
+ };
package/dist/plugin.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Sprites, atlases, frame animation, and tilemaps, drawn in the render graph: world sprites after
3
3
  * opaque 3D (so 2D gets HDR and post-processing), screen sprites over the finished image. Cameras
4
- * with Lighting2d light them with 2D lights and shadows.
4
+ * with Lighting2d light them with 2D lights and shadows. A tilemap with a GroundLayer draws on the
5
+ * ground instead, as chunk meshes among the tabletop's bands (0059).
5
6
  */
6
7
  export declare const spritePlugin: import("@aethervtt/shard-runtime").Plugin;
package/dist/plugin.js CHANGED
@@ -1,13 +1,31 @@
1
- import { Last, Update } from "@aethervtt/shard-core";
2
- import { Gpu, Graph, Picking, RenderDescribers, RenderSet, Shaders } from "@aethervtt/shard-render";
1
+ import { Last, PostUpdate, Update } from "@aethervtt/shard-core";
2
+ import {
3
+ addRenderFeatures,
4
+ Gpu,
5
+ Graph,
6
+ Picking,
7
+ RenderDescribers,
8
+ RenderSet,
9
+ Shaders
10
+ } from "@aethervtt/shard-render";
3
11
  import { definePlugin } from "@aethervtt/shard-runtime";
12
+ import { TransformSystems } from "@aethervtt/shard-transform";
4
13
  import * as atlasModule from "./atlas.js";
5
14
  import { TextureAtlases } from "./atlas.js";
6
15
  import * as clipModule from "./clip.js";
7
16
  import { animateSprites, SpriteClips } from "./clip.js";
17
+ import * as groundModule from "./ground.js";
18
+ import {
19
+ GROUND_TILE_SHADERS,
20
+ GroundTiles,
21
+ observeGroundTiles,
22
+ syncGroundTiles,
23
+ tileDetail
24
+ } from "./ground.js";
8
25
  import * as lightingModule from "./lighting.js";
9
26
  import * as lights2dModule from "./lights2d.js";
10
- import { lights2dNode, prepareLights2d } from "./lights2d.js";
27
+ import { lights2dNode, loadBaselineLighting, prepareLights2d } from "./lights2d.js";
28
+ import { tilemapMethods } from "./methods.js";
11
29
  import * as renderModule from "./render.js";
12
30
  import {
13
31
  describeSprites,
@@ -30,6 +48,7 @@ const spritePlugin = definePlugin({
30
48
  provides: [
31
49
  atlasModule,
32
50
  clipModule,
51
+ groundModule,
33
52
  lightingModule,
34
53
  lights2dModule,
35
54
  renderModule,
@@ -44,22 +63,40 @@ const spritePlugin = definePlugin({
44
63
  w.initResource(TilemapDatas);
45
64
  w.initResource(Sprite2dSettings);
46
65
  w.initResource(Tilemaps);
66
+ w.initResource(GroundTiles);
47
67
  observeSpriteRemovals(w);
48
- app.addSystems(Update, animateSprites).addSystems(Last, prepareSprites.inSet(RenderSet.Prepare)).addSystems(Last, prepareLights2d.inSet(RenderSet.Prepare));
68
+ observeGroundTiles(w);
69
+ app.addSystems(Update, animateSprites).addSystems(PostUpdate, syncGroundTiles.before(TransformSystems));
70
+ app.addMethod(...tilemapMethods).addSystems(Last, prepareSprites.inSet(RenderSet.Prepare)).addSystems(Last, prepareLights2d.inSet(RenderSet.Prepare));
49
71
  },
50
72
  ready(app) {
51
73
  const world = app.world;
52
74
  app.insertResource(Sprites, new SpriteStore(world.resource(Gpu)));
53
75
  const shaders = world.resource(Shaders);
54
- for (const [path, source] of Object.entries(SPRITE_SHADERS)) {
76
+ for (const [path, source] of Object.entries({ ...SPRITE_SHADERS, ...GROUND_TILE_SHADERS })) {
55
77
  shaders.register(path, source, `engine:${path}`);
56
78
  }
57
79
  const graph = world.resource(Graph);
80
+ addRenderFeatures(world, {
81
+ name: "sprite/lights2d",
82
+ description: "2D lighting: light tiles, occluder shadows, soft-shadow coarse maps.",
83
+ nodes: ["sprites/lights2d"],
84
+ baseline: { strategy: "Light binning and shadow rows on the CPU, uploaded as data textures" }
85
+ });
86
+ addRenderFeatures(world, {
87
+ name: "sprite",
88
+ description: "Sprites and tilemaps, in the scene and in the overlay.",
89
+ nodes: ["sprites", "sprites/overlay"],
90
+ baseline: { strategy: "Sprite, tile and chunk records in data textures" }
91
+ });
58
92
  graph.addNode("sprites/lights2d", lights2dNode(world));
59
93
  graph.addNode("sprites", spriteNode(world));
60
94
  graph.addNode("sprites/overlay", overlayNode(world));
61
95
  world.initResource(RenderDescribers).set("sprites", (w) => describeSprites(w));
62
- world.initResource(Picking).drawers.set("sprites", drawSpritePicks);
96
+ const picking = world.initResource(Picking);
97
+ picking.drawers.set("sprites", drawSpritePicks);
98
+ picking.detailers.set("tiles", tileDetail);
99
+ if (world.resource(Gpu).tier === "baseline") return loadBaselineLighting();
63
100
  }
64
101
  });
65
102
  export {
package/dist/render.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
2
2
  import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
3
- import { type CameraData, type NodeContext, type NodeDescriptor } from '@aethervtt/shard-render';
3
+ import { type CameraData, DataStore, type NodeContext, type NodeDescriptor } from '@aethervtt/shard-render';
4
4
  import { type Texture } from '@aethervtt/shard-texture';
5
+ import { type TilemapData } from './tilemap.js';
5
6
  /** Floats per sprite record: affine rows (12), uv rect (4), size (2), anchor (2), color, flags, pad. */
6
7
  export declare const SPRITE_FLOATS = 24;
7
8
  /** Sprites sharing a texture, blend, and space: one draw per contiguous run in sorted order. */
@@ -55,8 +56,8 @@ export declare class SpriteStore {
55
56
  /** Bytes of sprite records uploaded this frame. */
56
57
  uploadedBytes: number;
57
58
  sorts: number;
58
- readonly records: GpuBuffer;
59
- readonly orderBuffer: GpuBuffer;
59
+ readonly records: DataStore;
60
+ readonly orderBuffer: DataStore;
60
61
  private orderUploaded;
61
62
  /** Hidden sprites per ECS table id, from the last visit. */
62
63
  readonly tableSeen: number[];
@@ -93,7 +94,7 @@ export declare const prepareSprites: import("@aethervtt/shard-core").SystemDef<{
93
94
  /** Frees a sprite's slot when the Sprite goes away (including on despawn). */
94
95
  export declare function observeSpriteRemovals(world: World): void;
95
96
  interface LayerGpu {
96
- buffer: GpuBuffer;
97
+ buffer: DataStore;
97
98
  chunksX: number;
98
99
  chunksY: number;
99
100
  cursor: number;
@@ -106,10 +107,12 @@ interface TilemapGpu {
106
107
  version: number;
107
108
  chunkSize: number;
108
109
  layers: LayerGpu[];
109
- regions: GpuBuffer;
110
+ regions: DataStore;
110
111
  regionKey: string;
111
- remap: GpuBuffer;
112
+ remap: DataStore;
112
113
  remapData: Uint32Array;
114
+ /** Tile id → atlas region + 1, before animation. */
115
+ baseRemap: Uint32Array;
113
116
  layer: number;
114
117
  tileSize: [number, number];
115
118
  affine: Float32Array;
@@ -133,6 +136,15 @@ export declare class TilemapStore {
133
136
  }>;
134
137
  }
135
138
  export declare const Tilemaps: import("@aethervtt/shard-core").ResourceDef<TilemapStore>;
139
+ /**
140
+ * Tile id → atlas region + 1 for a map (0059): each palette name looked up in the atlas once, so a
141
+ * re-packed atlas draws the same map. Version 1 ids are regions + 1 already (clamped to the atlas).
142
+ * A name the atlas lacks draws nothing and is reported once (`sprite/unknown-tile`, see unknownTiles).
143
+ */
144
+ export declare function resolveTiles(world: World, data: TilemapData, atlas: {
145
+ count: number;
146
+ region(name: string): number;
147
+ }): Uint32Array;
136
148
  interface DrawCaches {
137
149
  generation: number;
138
150
  layouts?: {
@@ -152,7 +164,7 @@ interface DrawCaches {
152
164
  }>;
153
165
  views: Map<string, {
154
166
  uniform: GpuBuffer;
155
- chunks: GpuBuffer;
167
+ chunks: DataStore;
156
168
  }>;
157
169
  batchParams: Map<SpriteBatch, GpuBuffer>;
158
170
  samplers?: {
package/dist/render.js CHANGED
@@ -7,14 +7,17 @@ import { GpuBuffer } from "@aethervtt/shard-gpu";
7
7
  import {
8
8
  ComputedVisibility,
9
9
  cameraOf,
10
+ DataStore,
11
+ dataEntry,
10
12
  Gpu,
11
13
  GpuAssetsResource,
14
+ GroundLayer,
12
15
  PICK_TARGETS,
13
16
  RenderPhase,
14
17
  Shaders,
15
18
  sceneColor
16
19
  } from "@aethervtt/shard-render";
17
- import { Time } from "@aethervtt/shard-runtime";
20
+ import { LogResource, Time } from "@aethervtt/shard-runtime";
18
21
  import { Textures } from "@aethervtt/shard-texture";
19
22
  import { GlobalTransform } from "@aethervtt/shard-transform";
20
23
  import { TextureAtlases } from "./atlas.js";
@@ -26,7 +29,7 @@ import {
26
29
  litView
27
30
  } from "./lights2d.js";
28
31
  import { Sprite, Sprite2dSettings, SpriteSlot } from "./sprite.js";
29
- import { Tilemap, TilemapDatas } from "./tilemap.js";
32
+ import { Tilemap, TilemapDatas, unknownTiles } from "./tilemap.js";
30
33
  const SPRITE_FLOATS = 24;
31
34
  const SPRITE_BYTES = SPRITE_FLOATS * 4;
32
35
  const BLEND_ALPHA = 0;
@@ -80,13 +83,8 @@ class SpriteStore {
80
83
  constructor(gpu) {
81
84
  this.gpu = gpu;
82
85
  this.generation = gpu.generation;
83
- const storage = GPUBufferUsage.STORAGE;
84
- this.records = new GpuBuffer(gpu, {
85
- label: "sprites",
86
- usage: storage,
87
- size: SPRITE_BYTES * 256
88
- });
89
- this.orderBuffer = new GpuBuffer(gpu, { label: "sprites/order", usage: storage, size: 1024 });
86
+ this.records = new DataStore(gpu, { label: "sprites", size: SPRITE_BYTES * 256 });
87
+ this.orderBuffer = new DataStore(gpu, { label: "sprites/order", size: 1024 });
90
88
  this.grow(256);
91
89
  }
92
90
  get live() {
@@ -553,9 +551,8 @@ function syncTilemaps(world) {
553
551
  const chunksX = Math.ceil(l.width / cs);
554
552
  const chunksY = Math.ceil(l.height / cs);
555
553
  return {
556
- buffer: new GpuBuffer(gpu, {
554
+ buffer: new DataStore(gpu, {
557
555
  label: `tilemap/${entity}/${i}`,
558
- usage: GPUBufferUsage.STORAGE,
559
556
  size: Math.max(16, chunksX * chunksY * cs * cs * 4)
560
557
  }),
561
558
  chunksX,
@@ -569,18 +566,11 @@ function syncTilemaps(world) {
569
566
  })
570
567
  };
571
568
  }),
572
- regions: map?.regions ?? new GpuBuffer(gpu, {
573
- label: `tilemap/${entity}/regions`,
574
- usage: GPUBufferUsage.STORAGE,
575
- size: 256
576
- }),
569
+ regions: map?.regions ?? new DataStore(gpu, { label: `tilemap/${entity}/regions`, size: 256 }),
577
570
  regionKey: "",
578
- remap: map?.remap ?? new GpuBuffer(gpu, {
579
- label: `tilemap/${entity}/remap`,
580
- usage: GPUBufferUsage.STORAGE,
581
- size: 256
582
- }),
571
+ remap: map?.remap ?? new DataStore(gpu, { label: `tilemap/${entity}/remap`, size: 256 }),
583
572
  remapData: new Uint32Array(0),
573
+ baseRemap: new Uint32Array(0),
584
574
  layer: 0,
585
575
  tileSize: [1, 1],
586
576
  affine: new Float32Array(12),
@@ -647,7 +637,7 @@ function syncTilemaps(world) {
647
637
  chunkBounds(g, x0, y0, x1, y1, b, chunk * 6);
648
638
  }
649
639
  });
650
- const key = `${atlas.version}/${texture.width}x${texture.height}/${atlas.count}`;
640
+ const key = `${atlas.version}/${texture.width}x${texture.height}/${atlas.count}/${data.version}/${data.palette?.length ?? -1}`;
651
641
  if (map.regionKey !== key) {
652
642
  const uv = new Float32Array(Math.max(4, atlas.count * 4));
653
643
  for (let r = 0; r < atlas.count; r++) {
@@ -658,18 +648,16 @@ function syncTilemaps(world) {
658
648
  }
659
649
  map.regions.write(uv);
660
650
  map.regionKey = key;
661
- map.remapData = new Uint32Array(atlas.count + 1);
662
- for (let t = 0; t < map.remapData.length; t++) map.remapData[t] = t;
651
+ map.baseRemap = resolveTiles(world, data, atlas);
652
+ map.remapData = map.baseRemap.slice();
663
653
  map.remap.write(map.remapData);
664
654
  }
665
655
  if (data.animations.length > 0) {
666
656
  let changed = false;
667
657
  for (const a of data.animations) {
668
658
  if (a.frames.length === 0 || a.tile >= map.remapData.length) continue;
669
- const frame = Math.min(
670
- a.frames[Math.floor(time / a.frameTime) % a.frames.length],
671
- atlas.count
672
- );
659
+ const id = a.frames[Math.floor(time / a.frameTime) % a.frames.length];
660
+ const frame = map.baseRemap[Math.min(id, map.baseRemap.length - 1)];
673
661
  if (map.remapData[a.tile] !== frame) {
674
662
  map.remapData[a.tile] = frame;
675
663
  changed = true;
@@ -680,6 +668,27 @@ function syncTilemaps(world) {
680
668
  }
681
669
  for (const entity of tilemaps.maps.keys()) if (!seen.has(entity)) tilemaps.maps.delete(entity);
682
670
  }
671
+ const unknownChecked = /* @__PURE__ */ new WeakMap();
672
+ function resolveTiles(world, data, atlas) {
673
+ if (!data.palette) {
674
+ const out2 = new Uint32Array(atlas.count + 1);
675
+ for (let t = 0; t < out2.length; t++) out2[t] = t;
676
+ return out2;
677
+ }
678
+ const out = new Uint32Array(data.palette.length + 1);
679
+ let unknown = false;
680
+ for (let i = 0; i < data.palette.length; i++) {
681
+ const region = atlas.region(data.palette[i]);
682
+ out[i + 1] = region + 1;
683
+ if (region === -1) unknown = true;
684
+ }
685
+ if (unknown && unknownChecked.get(data) !== data.palette.length) {
686
+ unknownChecked.set(data, data.palette.length);
687
+ const log = world.tryResource(LogResource);
688
+ for (const error of unknownTiles(data, atlas)) log?.error(error);
689
+ }
690
+ return out;
691
+ }
683
692
  function chunkBounds(g, x0, y0, x1, y1, out, o) {
684
693
  out[o] = out[o + 1] = out[o + 2] = Number.POSITIVE_INFINITY;
685
694
  out[o + 3] = out[o + 4] = out[o + 5] = Number.NEGATIVE_INFINITY;
@@ -706,7 +715,10 @@ function boxInFrustum(planes, b, o) {
706
715
  return true;
707
716
  }
708
717
  function* queryTilemaps(world) {
709
- for (const table of world.query({ with: [Tilemap, GlobalTransform, ComputedVisibility] }).tables) {
718
+ for (const table of world.query({
719
+ with: [Tilemap, GlobalTransform, ComputedVisibility],
720
+ without: [GroundLayer]
721
+ }).tables) {
710
722
  const g = table.column(GlobalTransform, "matrix");
711
723
  const vis = table.column(ComputedVisibility, "visible");
712
724
  const atlas = table.column(Tilemap, "atlas");
@@ -725,7 +737,8 @@ function* queryTilemaps(world) {
725
737
  tileSize: [tileSize[i * 2], tileSize[i * 2 + 1]],
726
738
  chunkSize: chunkSize[i],
727
739
  layer: layer[i],
728
- lit: lit[i] !== 0
740
+ // Enum index: 0 is '2d', the only mode the sprite pass lights.
741
+ lit: lit[i] === 0
729
742
  },
730
743
  g.subarray(i * 12, i * 12 + 12)
731
744
  ];
@@ -752,6 +765,9 @@ function idOf(o) {
752
765
  }
753
766
  return id;
754
767
  }
768
+ function dataKey(d) {
769
+ return `${idOf(d)}:${d.version}`;
770
+ }
755
771
  function check(ctx) {
756
772
  const gpu = ctx.gpu;
757
773
  const caches = ctx.world.initResource(SpriteDrawCaches);
@@ -768,11 +784,7 @@ function check(ctx) {
768
784
  if (!caches.layouts) {
769
785
  const V = GPUShaderStage.VERTEX;
770
786
  const F = GPUShaderStage.FRAGMENT;
771
- const storage = (binding) => ({
772
- binding,
773
- visibility: V,
774
- buffer: { type: "read-only-storage" }
775
- });
787
+ const storage = (binding) => dataEntry(gpu, binding, V);
776
788
  caches.layouts = {
777
789
  view: gpu.layouts.bindGroupLayout({
778
790
  label: "sprites/view",
@@ -900,11 +912,7 @@ function viewGroup(ctx, cam) {
900
912
  usage: GPUBufferUsage.UNIFORM,
901
913
  size: 96
902
914
  }),
903
- chunks: new GpuBuffer(gpu, {
904
- label: `${ctx.view.name}/tilemap-chunks`,
905
- usage: GPUBufferUsage.STORAGE,
906
- size: 1024
907
- })
915
+ chunks: new DataStore(gpu, { label: `${ctx.view.name}/tilemap-chunks`, size: 1024 })
908
916
  };
909
917
  c.views.set(ctx.view.name, v);
910
918
  }
@@ -1047,13 +1055,13 @@ function drawSpace(ctx, space) {
1047
1055
  group(
1048
1056
  ctx,
1049
1057
  `${ctx.view.name}/tilemap/${d.map.entity}/${d.layer}`,
1050
- `${idOf(layer.buffer.buffer)}/${idOf(v.chunks.buffer)}/${idOf(d.map.regions.buffer)}/${idOf(d.map.remap.buffer)}/${idOf(layer.params.buffer)}`,
1058
+ `${dataKey(layer.buffer)}/${dataKey(v.chunks)}/${dataKey(d.map.regions)}/${dataKey(d.map.remap)}/${idOf(layer.params.buffer)}`,
1051
1059
  c.layouts.tilemap,
1052
1060
  () => [
1053
- { binding: 0, resource: { buffer: layer.buffer.buffer } },
1054
- { binding: 1, resource: { buffer: v.chunks.buffer } },
1055
- { binding: 2, resource: { buffer: d.map.regions.buffer } },
1056
- { binding: 3, resource: { buffer: d.map.remap.buffer } },
1061
+ { binding: 0, resource: layer.buffer.resource() },
1062
+ { binding: 1, resource: v.chunks.resource() },
1063
+ { binding: 2, resource: d.map.regions.resource() },
1064
+ { binding: 3, resource: d.map.remap.resource() },
1057
1065
  { binding: 4, resource: { buffer: layer.params.buffer } }
1058
1066
  ]
1059
1067
  )
@@ -1098,11 +1106,11 @@ function drawSpace(ctx, space) {
1098
1106
  group(
1099
1107
  ctx,
1100
1108
  "sprites/records",
1101
- `${idOf(store.records.buffer)}/${idOf(store.orderBuffer.buffer)}`,
1109
+ `${dataKey(store.records)}/${dataKey(store.orderBuffer)}`,
1102
1110
  c.layouts.sprites,
1103
1111
  () => [
1104
- { binding: 0, resource: { buffer: store.records.buffer } },
1105
- { binding: 1, resource: { buffer: store.orderBuffer.buffer } }
1112
+ { binding: 0, resource: store.records.resource() },
1113
+ { binding: 1, resource: store.orderBuffer.resource() }
1106
1114
  ]
1107
1115
  )
1108
1116
  );
@@ -1138,11 +1146,11 @@ function drawSpritePicks(ctx, cam) {
1138
1146
  group(
1139
1147
  ctx,
1140
1148
  "sprites/records",
1141
- `${idOf(store.records.buffer)}/${idOf(store.orderBuffer.buffer)}`,
1149
+ `${dataKey(store.records)}/${dataKey(store.orderBuffer)}`,
1142
1150
  c.layouts.sprites,
1143
1151
  () => [
1144
- { binding: 0, resource: { buffer: store.records.buffer } },
1145
- { binding: 1, resource: { buffer: store.orderBuffer.buffer } }
1152
+ { binding: 0, resource: store.records.resource() },
1153
+ { binding: 1, resource: store.orderBuffer.resource() }
1146
1154
  ]
1147
1155
  )
1148
1156
  );
@@ -1212,5 +1220,6 @@ export {
1212
1220
  observeSpriteRemovals,
1213
1221
  overlayNode,
1214
1222
  prepareSprites,
1223
+ resolveTiles,
1215
1224
  spriteNode
1216
1225
  };