@aethervtt/shard-sprite 0.0.1

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,2 @@
1
+ /** WGSL for sprites and tilemaps (see render.ts), and 2D lighting (see lights2d.ts). */
2
+ export declare const SPRITE_SHADERS: Record<string, string>;
@@ -0,0 +1,263 @@
1
+ import { LIGHT2D_SHADERS } from "./lighting-shaders.js";
2
+ const SPRITE_SHADERS = {
3
+ ...LIGHT2D_SHADERS,
4
+ "shard::sprite::common": `
5
+ import shard::color::linear_to_srgb;
6
+ @if(LIT) import shard::sprite::light2d::light2d;
7
+
8
+ struct SpriteView {
9
+ view_proj: mat4x4f,
10
+ /** width, height, 1 / width, 1 / height. */
11
+ viewport: vec4f,
12
+ /** Texel snap (pixels per unit, 0: off), 0, 0, 0. */
13
+ snap: vec4f,
14
+ }
15
+
16
+ struct Batch {
17
+ /** 1 when the texture is already premultiplied. */
18
+ premultiplied: u32,
19
+ /** 0 alpha, 1 additive, 2 opaque. */
20
+ blend: u32,
21
+ _pad0: u32,
22
+ _pad1: u32,
23
+ }
24
+
25
+ @group(0) @binding(0) var<uniform> sprite_view: SpriteView;
26
+ @group(2) @binding(0) var sprite_texture: texture_2d<f32>;
27
+ @group(2) @binding(1) var sprite_sampler: sampler;
28
+ @group(2) @binding(2) var<uniform> batch: Batch;
29
+ /** Lit views: the batch's normal map (a flat 1\xD71 when it has none). */
30
+ @if(LIT) @group(2) @binding(3) var normal_texture: texture_2d<f32>;
31
+
32
+ struct SpriteOutput {
33
+ @builtin(position) clip: vec4f,
34
+ @location(0) uv: vec2f,
35
+ @location(1) color: vec4f,
36
+ /** Picking: the entity and the world normal (the sprite's +Z). */
37
+ @location(2) @interpolate(flat) entity: u32,
38
+ @location(3) normal: vec3f,
39
+ /** Lit views: world XY, and where the normal map's +X and +Y point in world XY. */
40
+ @if(LIT) @location(4) world: vec2f,
41
+ @if(LIT) @location(5) basis: vec4f,
42
+ /** Lit views: flags (1 lit, 2 normal map, light-layer band << 8), emissive and normal strength (f16 \xD7 2). */
43
+ @if(LIT) @location(6) @interpolate(flat) light: vec2u,
44
+ }
45
+
46
+ /** Corner k (0..5) of a quad as two triangles: (0,0) top left to (1,1) bottom right. */
47
+ fn corner(k: u32) -> vec2f {
48
+ let c = array<vec2f, 6>(vec2f(0.0, 0.0), vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0), vec2f(1.0, 1.0));
49
+ return c[k];
50
+ }
51
+
52
+ /** Unpacks RGBA8 (straight) and premultiplies, so tints compose with premultiplied texels. */
53
+ fn unpack_color(packed: u32) -> vec4f {
54
+ let c = unpack4x8unorm(packed);
55
+ return vec4f(c.rgb * c.a, c.a);
56
+ }
57
+
58
+ /** The shaded texel, premultiplied. */
59
+ fn shade(uv: vec2f, tint: vec4f) -> vec4f {
60
+ var t = textureSample(sprite_texture, sprite_sampler, uv);
61
+ if (batch.premultiplied == 0u) { t = vec4f(t.rgb * t.a, t.a); }
62
+ return t * tint;
63
+ }
64
+
65
+ /** The batch's normal map at uv, in tangent space (+Y up). Sample before any branching. */
66
+ @if(LIT)
67
+ fn sample_normal(uv: vec2f) -> vec3f {
68
+ return textureSample(normal_texture, sprite_sampler, uv).xyz * 2.0 - 1.0;
69
+ }
70
+
71
+ /** Premultiplied color lit by the view's 2D lights (unlit sprites pass through). */
72
+ @if(LIT)
73
+ fn apply_light2d(in: SpriteOutput, c: vec4f, tangent: vec3f) -> vec4f {
74
+ let flags = in.light.x;
75
+ // Unlit sprites, and texels nothing shows through, skip the light loop.
76
+ if ((flags & 1u) == 0u || c.a <= 0.0) { return c; }
77
+ let extra = unpack2x16float(in.light.y);
78
+ let has_normal = (flags & 2u) != 0u;
79
+ var n = vec3f(0.0, 0.0, 1.0);
80
+ if (has_normal) {
81
+ let t = vec3f(tangent.xy * extra.y, tangent.z);
82
+ n = normalize(vec3f(t.x * in.basis.xy + t.y * in.basis.zw, t.z));
83
+ }
84
+ let light = light2d(in.clip.xy, in.world, n, has_normal, 1u << ((flags >> 8u) & 31u));
85
+ return vec4f(c.rgb * (light + extra.x), c.a);
86
+ }
87
+
88
+ /** Display targets without hardware sRGB encoding get it here. */
89
+ fn encode(c: vec4f) -> vec4f {
90
+ @if(SRGB_TARGET) return c;
91
+ @if(!SRGB_TARGET) {
92
+ // Unpremultiply, encode, premultiply again: blending happens in display space here.
93
+ let rgb = select(vec3f(0.0), c.rgb / c.a, c.a > 0.0);
94
+ return vec4f(linear_to_srgb(rgb) * c.a, c.a);
95
+ }
96
+ }`,
97
+ "shard::sprite": `
98
+ import shard::sprite::common::{ SpriteOutput, sprite_view, batch, corner, unpack_color, shade, encode };
99
+ @if(LIT) import shard::sprite::common::{ sample_normal, apply_light2d };
100
+ import shard::pick::types::PickOutput;
101
+
102
+ /** One sprite: affine rows, uv rect, size, anchor, packed color. */
103
+ struct SpriteRecord {
104
+ row0: vec4f,
105
+ row1: vec4f,
106
+ row2: vec4f,
107
+ uv: vec4f,
108
+ size: vec2f,
109
+ anchor: vec2f,
110
+ color: u32,
111
+ flags: u32,
112
+ entity: u32,
113
+ _pad1: u32,
114
+ }
115
+
116
+ @group(1) @binding(0) var<storage, read> sprites: array<SpriteRecord>;
117
+ @group(1) @binding(1) var<storage, read> order: array<u32>;
118
+
119
+ @vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> SpriteOutput {
120
+ let s = sprites[order[i]];
121
+ let c = corner(v);
122
+ // Local space: x right, y up, the anchor at the origin.
123
+ let local = vec4f((c.x - s.anchor.x) * s.size.x, (s.anchor.y - c.y) * s.size.y, 0.0, 1.0);
124
+ var world = vec3f(dot(s.row0, local), dot(s.row1, local), dot(s.row2, local));
125
+ if (sprite_view.snap.x > 0.0) {
126
+ // Pixel-perfect: positions land on the texel grid.
127
+ world = vec3f(round(world.xy * sprite_view.snap.x) / sprite_view.snap.x, world.z);
128
+ }
129
+ var out: SpriteOutput;
130
+ @if(SCREEN) {
131
+ // Screen space: pixels from the top left, y down.
132
+ let px = vec2f(world.x, -world.y);
133
+ out.clip = vec4f(px.x * sprite_view.viewport.z * 2.0 - 1.0, 1.0 - px.y * sprite_view.viewport.w * 2.0, 0.5, 1.0);
134
+ }
135
+ @if(!SCREEN) {
136
+ out.clip = sprite_view.view_proj * vec4f(world, 1.0);
137
+ }
138
+ out.uv = mix(s.uv.xy, s.uv.zw, c);
139
+ out.color = unpack_color(s.color);
140
+ out.entity = s.entity;
141
+ out.normal = vec3f(s.row0.z, s.row1.z, s.row2.z);
142
+ @if(LIT) {
143
+ out.world = world.xy;
144
+ // Flips swap the uv rect's ends; the normal map's axes turn with them.
145
+ let fx = select(1.0, -1.0, s.uv.x > s.uv.z);
146
+ let fy = select(1.0, -1.0, s.uv.y > s.uv.w);
147
+ let ax = vec2f(s.row0.x, s.row1.x);
148
+ let ay = vec2f(s.row0.y, s.row1.y);
149
+ out.basis = vec4f(ax / max(length(ax), 1e-8) * fx, ay / max(length(ay), 1e-8) * fy);
150
+ out.light = vec2u(s.flags, s._pad1);
151
+ }
152
+ return out;
153
+ }
154
+
155
+ @fragment fn fs_pick(in: SpriteOutput, @builtin(front_facing) front: bool) -> PickOutput {
156
+ let c = shade(in.uv, in.color);
157
+ if (c.a < 0.5) { discard; }
158
+ var out: PickOutput;
159
+ out.id = in.entity;
160
+ let n = normalize(in.normal);
161
+ out.normal = vec4f(select(-n, n, front), in.clip.z);
162
+ return out;
163
+ }
164
+
165
+ @fragment fn fs(in: SpriteOutput) -> @location(0) vec4f {
166
+ var c = shade(in.uv, in.color);
167
+ @if(LIT) {
168
+ let tangent = sample_normal(in.uv);
169
+ c = apply_light2d(in, c, tangent);
170
+ }
171
+ if (batch.blend == 2u) {
172
+ if (c.a < 0.5) { discard; }
173
+ c = vec4f(c.rgb / c.a, 1.0);
174
+ }
175
+ @if(SCREEN) { c = encode(c); }
176
+ return c;
177
+ }`,
178
+ "shard::tilemap": `
179
+ import shard::sprite::common::{ SpriteOutput, sprite_view, corner, shade };
180
+ @if(LIT) import shard::sprite::common::{ sample_normal, apply_light2d };
181
+
182
+ struct TilemapParams {
183
+ row0: vec4f,
184
+ row1: vec4f,
185
+ row2: vec4f,
186
+ /** Tile size (world, xy), layer width and height in tiles (zw). */
187
+ tile: vec4f,
188
+ /** Chunk size, chunks across, light flags (1 lit, 2 normal map, band << 8), 0. */
189
+ chunks: vec4u,
190
+ }
191
+
192
+ @group(1) @binding(0) var<storage, read> tiles: array<u32>;
193
+ @group(1) @binding(1) var<storage, read> visible_chunks: array<u32>;
194
+ @group(1) @binding(2) var<storage, read> regions: array<vec4f>;
195
+ @group(1) @binding(3) var<storage, read> remap: array<u32>;
196
+ @group(1) @binding(4) var<uniform> map: TilemapParams;
197
+
198
+ /**
199
+ * One tile per instance: instance / chunk\xB2 picks the visible chunk (first_instance offsets into
200
+ * this draw's list), the remainder the tile in it. Empty tiles collapse to nothing.
201
+ */
202
+ @vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> SpriteOutput {
203
+ let cs = map.chunks.x;
204
+ let per = cs * cs;
205
+ let chunk = visible_chunks[i / per];
206
+ let local = i % per;
207
+ let cx = chunk % map.chunks.y;
208
+ let cy = chunk / map.chunks.y;
209
+ let tx = cx * cs + local % cs;
210
+ let ty = cy * cs + local / cs;
211
+ var out: SpriteOutput;
212
+ out.color = vec4f(1.0);
213
+ let value = tiles[chunk * per + local];
214
+ let raw = value & 0xffffu;
215
+ if (raw == 0u || f32(tx) >= map.tile.z || f32(ty) >= map.tile.w) {
216
+ out.clip = vec4f(0.0, 0.0, 0.0, 0.0);
217
+ return out;
218
+ }
219
+ let tile = remap[min(raw, arrayLength(&remap) - 1u)];
220
+ let flags = value >> 16u;
221
+ let c = corner(v);
222
+ // Tile (x, y) covers [x, x + 1] \xD7 [-y - 1, -y] in tile units.
223
+ let p = vec4f((f32(tx) + c.x) * map.tile.x, -(f32(ty) + c.y) * map.tile.y, 0.0, 1.0);
224
+ let world = vec3f(dot(map.row0, p), dot(map.row1, p), dot(map.row2, p));
225
+ out.clip = sprite_view.view_proj * vec4f(world, 1.0);
226
+ var t = c;
227
+ if ((flags & 4u) != 0u) { t = vec2f(t.y, 1.0 - t.x); }
228
+ if ((flags & 1u) != 0u) { t.x = 1.0 - t.x; }
229
+ if ((flags & 2u) != 0u) { t.y = 1.0 - t.y; }
230
+ let r = regions[tile - 1u];
231
+ out.uv = mix(r.xy, r.zw, t);
232
+ @if(LIT) {
233
+ out.world = world.xy;
234
+ // The tile's uv transform m (quad \u2192 texture, y down), inverted (it's orthogonal) to turn the
235
+ // normal map's axes back into the map's local X and Y.
236
+ var m = mat2x2f(1.0, 0.0, 0.0, 1.0);
237
+ if ((flags & 4u) != 0u) { m = mat2x2f(0.0, -1.0, 1.0, 0.0) * m; }
238
+ if ((flags & 1u) != 0u) { m = mat2x2f(-1.0, 0.0, 0.0, 1.0) * m; }
239
+ if ((flags & 2u) != 0u) { m = mat2x2f(1.0, 0.0, 0.0, -1.0) * m; }
240
+ let a = vec2f(m[0].x, m[1].x);
241
+ let b = vec2f(m[0].y, m[1].y);
242
+ let mx = vec2f(map.row0.x, map.row1.x);
243
+ let my = vec2f(map.row0.y, map.row1.y);
244
+ let lx = mx / max(length(mx), 1e-8);
245
+ let ly = my / max(length(my), 1e-8);
246
+ out.basis = vec4f(a.x * lx - a.y * ly, -b.x * lx + b.y * ly);
247
+ out.light = vec2u(map.chunks.z, pack2x16float(vec2f(0.0, 1.0)));
248
+ }
249
+ return out;
250
+ }
251
+
252
+ @fragment fn fs(in: SpriteOutput) -> @location(0) vec4f {
253
+ var c = shade(in.uv, in.color);
254
+ @if(LIT) {
255
+ let tangent = sample_normal(in.uv);
256
+ c = apply_light2d(in, c, tangent);
257
+ }
258
+ return c;
259
+ }`
260
+ };
261
+ export {
262
+ SPRITE_SHADERS
263
+ };
@@ -0,0 +1,26 @@
1
+ /** The sprite's slot in the sprite renderer's persistent buffer. Managed by the renderer. */
2
+ export declare const SpriteSlot: import("@aethervtt/shard-core").ComponentDef<{
3
+ readonly slot: import("@aethervtt/shard-core").FieldType<number, "u32">;
4
+ }>;
5
+ export declare const SPRITE_BLENDS: readonly ['alpha', 'additive', 'opaque'];
6
+ export declare const SPRITE_SPACES: readonly ['world', 'screen'];
7
+ export declare const Sprite: import("@aethervtt/shard-core").ComponentDef<{
8
+ readonly texture: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Texture"> | null, "object">;
9
+ readonly atlas: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"TextureAtlas"> | null, "object">;
10
+ readonly region: import("@aethervtt/shard-core").FieldType<string, "object">;
11
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
12
+ readonly flipX: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
13
+ readonly flipY: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
14
+ readonly anchor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
15
+ readonly size: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
16
+ readonly layer: import("@aethervtt/shard-core").FieldType<number, "i16">;
17
+ readonly blend: import("@aethervtt/shard-core").FieldType<"additive" | "alpha" | "opaque", "u8">;
18
+ readonly space: import("@aethervtt/shard-core").FieldType<"screen" | "world", "u8">;
19
+ readonly lit: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
20
+ }>;
21
+ export declare const SPRITE_SORTS: readonly ['z', 'y'];
22
+ export interface Sprite2dSettingsValue {
23
+ pixelsPerUnit: number;
24
+ sort: (typeof SPRITE_SORTS)[number];
25
+ }
26
+ export declare const Sprite2dSettings: import("@aethervtt/shard-core").ResourceDef<Sprite2dSettingsValue>;
package/dist/sprite.js ADDED
@@ -0,0 +1,67 @@
1
+ import { defineComponent, defineResource, t } from "@aethervtt/shard-core";
2
+ import { Visibility } from "@aethervtt/shard-render";
3
+ import { Transform } from "@aethervtt/shard-transform";
4
+ const SpriteSlot = defineComponent(
5
+ "sprite/SpriteSlot",
6
+ {
7
+ slot: t.u32({ readonly: true, description: "Slot + 1 (0 = none yet)." })
8
+ },
9
+ {
10
+ description: "The sprite's slot in the renderer's sprite buffer. Do not write.",
11
+ serialize: false
12
+ }
13
+ );
14
+ const SPRITE_BLENDS = ["alpha", "additive", "opaque"];
15
+ const SPRITE_SPACES = ["world", "screen"];
16
+ const Sprite = defineComponent(
17
+ "sprite/Sprite",
18
+ {
19
+ texture: t.handle("Texture", {
20
+ description: "The image to draw (ignored when atlas and region are set)."
21
+ }),
22
+ atlas: t.handle("TextureAtlas", { description: "An atlas to draw a region of." }),
23
+ region: t.string({ description: 'Atlas region name, e.g. "hero/idle_0".' }),
24
+ color: t.color({ default: [1, 1, 1, 1], description: "Tint, multiplied in (linear)." }),
25
+ flipX: t.bool({ description: "Mirror left to right." }),
26
+ flipY: t.bool({ description: "Mirror top to bottom." }),
27
+ anchor: t.vec2({
28
+ default: [0.5, 0.5],
29
+ description: "Pivot in normalized sprite space (0, 0 top left): where the entity's position sits. An atlas region with its own pivot (not the center) uses that instead."
30
+ }),
31
+ size: t.vec2({
32
+ min: 0,
33
+ unit: "m",
34
+ description: "World size. Zero: the image (or region) size in pixels \xF7 Sprite2dSettings.pixelsPerUnit. Screen sprites: pixels."
35
+ }),
36
+ layer: t.i16({
37
+ description: "Draw-order band: higher draws over lower. Within a band, sprites sort by Sprite2dSettings.sort."
38
+ }),
39
+ blend: t.enum(SPRITE_BLENDS, {
40
+ description: "alpha: premultiplied blending, sorted. additive: glows. opaque: alpha-tested at 0.5, writes depth, no sorting needed."
41
+ }),
42
+ space: t.enum(SPRITE_SPACES, {
43
+ description: "world: in the scene, under post-processing (lit by 2D lights under a Lighting2d camera). screen: an overlay after tonemapping, positioned in pixels from the view's top left."
44
+ }),
45
+ lit: t.bool({
46
+ default: true,
47
+ description: "Lit by 2D lights under a Lighting2d camera. Off: drawn at its own color (UI, glowing effects)."
48
+ })
49
+ },
50
+ {
51
+ description: "A textured quad in the entity's XY plane: a texture or an atlas region, tinted, flipped, and anchored.",
52
+ requires: [Transform, Visibility, SpriteSlot]
53
+ }
54
+ );
55
+ const SPRITE_SORTS = ["z", "y"];
56
+ const Sprite2dSettings = defineResource("sprite/Sprite2dSettings", {
57
+ description: "Sprite defaults: pixelsPerUnit (image pixels per world unit for sprites without a size, default 100) and sort within a layer: 'z' (higher z over lower) or 'y' (lower y over higher, for top-down scenes).",
58
+ init: () => ({ pixelsPerUnit: 100, sort: "z" })
59
+ });
60
+ export {
61
+ SPRITE_BLENDS,
62
+ SPRITE_SORTS,
63
+ SPRITE_SPACES,
64
+ Sprite,
65
+ Sprite2dSettings,
66
+ SpriteSlot
67
+ };
@@ -0,0 +1,94 @@
1
+ import { AssetStore } from '@aethervtt/shard-assets';
2
+ import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
3
+ /** Tile flag bits. */
4
+ export declare const TileFlags: {
5
+ readonly FlipX: 1;
6
+ readonly FlipY: 2;
7
+ readonly Rotate90: 4;
8
+ };
9
+ export declare const TilemapDataSchema: import("@aethervtt/shard-core").ComponentDef<{
10
+ readonly layers: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
11
+ name: import("@aethervtt/shard-core").FieldType<string, "object">;
12
+ width: import("@aethervtt/shard-core").FieldType<number, "u32">;
13
+ height: import("@aethervtt/shard-core").FieldType<number, "u32">;
14
+ tiles: import("@aethervtt/shard-core").FieldType<string, "object">;
15
+ flags: import("@aethervtt/shard-core").FieldType<string, "object">;
16
+ occludes: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
17
+ }>[], "object">;
18
+ readonly animations: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
19
+ tile: import("@aethervtt/shard-core").FieldType<number, "u16">;
20
+ frames: import("@aethervtt/shard-core").FieldType<number[], "object">;
21
+ frameTime: import("@aethervtt/shard-core").FieldType<number, "f32">;
22
+ }>[], "object">;
23
+ }>;
24
+ export declare class TileLayer {
25
+ readonly name: string;
26
+ readonly width: number;
27
+ readonly height: number;
28
+ readonly tiles: Uint16Array;
29
+ readonly flags: Uint8Array;
30
+ /** Tile indices (y · width + x) edited since `editBase`: renderers upload their chunks. */
31
+ edits: number[];
32
+ /** Bumps when the edit log overflows: renderers re-upload everything. */
33
+ editBase: number;
34
+ /** Filled cells block 2D light. */
35
+ occludes: boolean;
36
+ constructor(name: string, width: number, height: number, tiles?: Uint16Array, flags?: Uint8Array);
37
+ get(x: number, y: number): number;
38
+ set(x: number, y: number, tile: number, flags?: number): void;
39
+ }
40
+ /** Tile layers and animated tiles, shared by any number of Tilemaps. */
41
+ export declare class TilemapData {
42
+ layers: TileLayer[];
43
+ animations: {
44
+ tile: number;
45
+ frames: number[];
46
+ frameTime: number;
47
+ }[];
48
+ /** Bumps when the layers are replaced (hot reload). */
49
+ version: number;
50
+ constructor(layers?: TileLayer[], animations?: TilemapData['animations']);
51
+ /** An empty map of `layers` layers, width × height tiles each. */
52
+ static create(width: number, height: number, layers?: string[]): TilemapData;
53
+ static fromJson(json: unknown): TilemapData;
54
+ toJson(): {
55
+ layers: {
56
+ name: string;
57
+ width: number;
58
+ height: number;
59
+ tiles: string;
60
+ flags: string;
61
+ occludes?: boolean | undefined;
62
+ }[];
63
+ animations: {
64
+ tile: number;
65
+ frames: number[];
66
+ frameTime: number;
67
+ }[];
68
+ };
69
+ /** The largest tile index any layer uses (for validating against an atlas). */
70
+ maxTile(): number;
71
+ copyFrom(other: TilemapData): void;
72
+ }
73
+ export declare class TilemapDataStore extends AssetStore<TilemapData, 'TilemapData'> {
74
+ constructor();
75
+ }
76
+ export declare const TilemapDatas: import("@aethervtt/shard-core").ResourceDef<TilemapDataStore>;
77
+ export declare const TilemapDataAssetType: import("@aethervtt/shard-assets").AssetTypeDef<TilemapData>;
78
+ /** `*.tilemap.json`: tile layers for a Tilemap. */
79
+ export declare const TilemapDataImporter: import("@aethervtt/shard-assets").ImporterDef;
80
+ export declare const Tilemap: import("@aethervtt/shard-core").ComponentDef<{
81
+ readonly atlas: import("@aethervtt/shard-core").FieldType<AssetRef<"TextureAtlas"> | null, "object">;
82
+ readonly data: import("@aethervtt/shard-core").FieldType<AssetRef<"TilemapData"> | null, "object">;
83
+ readonly tileSize: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
84
+ readonly chunkSize: import("@aethervtt/shard-core").FieldType<number, "u16">;
85
+ readonly layer: import("@aethervtt/shard-core").FieldType<number, "i16">;
86
+ readonly lit: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
87
+ }>;
88
+ /** Sets a tile (atlas region + 1, 0 to clear) and its flags. Only its chunk re-uploads. */
89
+ export declare function setTile(world: World, tilemap: Entity, x: number, y: number, tile: number, options?: {
90
+ layer?: number | string;
91
+ flags?: number;
92
+ }): void;
93
+ /** The tile at (x, y): atlas region + 1, or 0 for empty or outside the map. */
94
+ export declare function tileAt(world: World, tilemap: Entity, x: number, y: number, layer?: number | string): number;