@joydle/tilemap 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/AutoTile.d.ts +21 -0
- package/dist/AutoTile.js +131 -0
- package/dist/TiledImporter.d.ts +34 -0
- package/dist/TiledImporter.js +35 -0
- package/dist/Tilemap.d.ts +32 -0
- package/dist/Tilemap.js +119 -0
- package/dist/babylon/BabylonTilemapRenderer.d.ts +25 -0
- package/dist/babylon/BabylonTilemapRenderer.js +80 -0
- package/dist/babylon/index.d.ts +1 -0
- package/dist/babylon/index.js +1 -0
- package/dist/babylon.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/phaser/PhaserTilemapRenderer.d.ts +29 -0
- package/dist/phaser/PhaserTilemapRenderer.js +73 -0
- package/dist/phaser/index.d.ts +1 -0
- package/dist/phaser/index.js +1 -0
- package/dist/phaser.js +56 -0
- package/dist/pixi/PixiTilemapRenderer.d.ts +39 -0
- package/dist/pixi/PixiTilemapRenderer.js +100 -0
- package/dist/pixi/index.d.ts +1 -0
- package/dist/pixi/index.js +1 -0
- package/dist/pixi.js +60 -0
- package/dist/three/ThreeTilemapRenderer.d.ts +24 -0
- package/dist/three/ThreeTilemapRenderer.js +70 -0
- package/dist/three/index.d.ts +1 -0
- package/dist/three/index.js +1 -0
- package/dist/three.js +55 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AutoTileOptions } from './types';
|
|
2
|
+
export declare class AutoTile {
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a grid of tile types into visual tile indices.
|
|
5
|
+
*
|
|
6
|
+
* @param grid 2D array of tile types (0 = empty, `solid` = solid)
|
|
7
|
+
* @param opts AutoTile options
|
|
8
|
+
* @returns 2D array of visual tile indices (0 = empty, 1+ = tile variant)
|
|
9
|
+
*/
|
|
10
|
+
static resolve(grid: number[][], opts?: AutoTileOptions): number[][];
|
|
11
|
+
/** Simple 16-tile rule set using 4 cardinal neighbors */
|
|
12
|
+
private static _simple16;
|
|
13
|
+
/**
|
|
14
|
+
* Blob 47-tile rule set using 8 neighbors (cardinal + diagonal).
|
|
15
|
+
* Corners only count if both adjacent cardinals are also solid.
|
|
16
|
+
* Produces indices 1-47 for full tileset coverage.
|
|
17
|
+
*/
|
|
18
|
+
private static _blob47;
|
|
19
|
+
/** Map 8-bit blob mask to 1-47 tile index */
|
|
20
|
+
private static _blob47Lookup;
|
|
21
|
+
}
|
package/dist/AutoTile.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoTile resolves a simplified type grid into visual tile indices.
|
|
3
|
+
* AI places tile types (0 = air, 1 = solid), AutoTile picks correct
|
|
4
|
+
* edge/corner sprites based on neighbor analysis.
|
|
5
|
+
*/
|
|
6
|
+
// Neighbor bitmask positions (clockwise from top-left):
|
|
7
|
+
// TL=1 T=2 TR=4
|
|
8
|
+
// L=8 C R=16
|
|
9
|
+
// BL=32 B=64 BR=128
|
|
10
|
+
const SIMPLE16_MAP = {
|
|
11
|
+
// Maps 4-bit cardinal mask (T=1, R=2, B=4, L=8) to tile index
|
|
12
|
+
0b0000: 1, // isolated
|
|
13
|
+
0b0001: 2, // top neighbor
|
|
14
|
+
0b0010: 3, // right neighbor
|
|
15
|
+
0b0011: 4, // top + right
|
|
16
|
+
0b0100: 5, // bottom neighbor
|
|
17
|
+
0b0101: 6, // top + bottom (vertical)
|
|
18
|
+
0b0110: 7, // right + bottom
|
|
19
|
+
0b0111: 8, // top + right + bottom
|
|
20
|
+
0b1000: 9, // left neighbor
|
|
21
|
+
0b1001: 10, // top + left
|
|
22
|
+
0b1010: 11, // left + right (horizontal)
|
|
23
|
+
0b1011: 12, // top + right + left
|
|
24
|
+
0b1100: 13, // bottom + left
|
|
25
|
+
0b1101: 14, // top + bottom + left
|
|
26
|
+
0b1110: 15, // right + bottom + left
|
|
27
|
+
0b1111: 16, // all four neighbors (center)
|
|
28
|
+
};
|
|
29
|
+
export class AutoTile {
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a grid of tile types into visual tile indices.
|
|
32
|
+
*
|
|
33
|
+
* @param grid 2D array of tile types (0 = empty, `solid` = solid)
|
|
34
|
+
* @param opts AutoTile options
|
|
35
|
+
* @returns 2D array of visual tile indices (0 = empty, 1+ = tile variant)
|
|
36
|
+
*/
|
|
37
|
+
static resolve(grid, opts = {}) {
|
|
38
|
+
const solid = opts.solid ?? 1;
|
|
39
|
+
const rules = opts.rules ?? 'simple16';
|
|
40
|
+
const rows = grid.length;
|
|
41
|
+
const cols = grid[0]?.length ?? 0;
|
|
42
|
+
const result = Array.from({ length: rows }, () => new Array(cols).fill(0));
|
|
43
|
+
for (let r = 0; r < rows; r++) {
|
|
44
|
+
for (let c = 0; c < cols; c++) {
|
|
45
|
+
if (grid[r][c] !== solid) {
|
|
46
|
+
result[r][c] = 0;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (rules === 'simple16') {
|
|
50
|
+
result[r][c] = this._simple16(grid, r, c, rows, cols, solid);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
result[r][c] = this._blob47(grid, r, c, rows, cols, solid);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/** Simple 16-tile rule set using 4 cardinal neighbors */
|
|
60
|
+
static _simple16(grid, r, c, rows, cols, solid) {
|
|
61
|
+
const t = r > 0 && grid[r - 1][c] === solid ? 1 : 0;
|
|
62
|
+
const ri = c < cols - 1 && grid[r][c + 1] === solid ? 1 : 0;
|
|
63
|
+
const b = r < rows - 1 && grid[r + 1][c] === solid ? 1 : 0;
|
|
64
|
+
const l = c > 0 && grid[r][c - 1] === solid ? 1 : 0;
|
|
65
|
+
const mask = t | (ri << 1) | (b << 2) | (l << 3);
|
|
66
|
+
return SIMPLE16_MAP[mask] ?? 1;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Blob 47-tile rule set using 8 neighbors (cardinal + diagonal).
|
|
70
|
+
* Corners only count if both adjacent cardinals are also solid.
|
|
71
|
+
* Produces indices 1-47 for full tileset coverage.
|
|
72
|
+
*/
|
|
73
|
+
static _blob47(grid, r, c, rows, cols, solid) {
|
|
74
|
+
const isSolid = (row, col) => {
|
|
75
|
+
if (row < 0 || row >= rows || col < 0 || col >= cols)
|
|
76
|
+
return false;
|
|
77
|
+
return grid[row][col] === solid;
|
|
78
|
+
};
|
|
79
|
+
const t = isSolid(r - 1, c);
|
|
80
|
+
const ri = isSolid(r, c + 1);
|
|
81
|
+
const b = isSolid(r + 1, c);
|
|
82
|
+
const l = isSolid(r, c - 1);
|
|
83
|
+
// Corners only count when both adjacent cardinals are present
|
|
84
|
+
const tl = t && l && isSolid(r - 1, c - 1);
|
|
85
|
+
const tr = t && ri && isSolid(r - 1, c + 1);
|
|
86
|
+
const bl = b && l && isSolid(r + 1, c - 1);
|
|
87
|
+
const br = b && ri && isSolid(r + 1, c + 1);
|
|
88
|
+
// Build 8-bit mask
|
|
89
|
+
let mask = 0;
|
|
90
|
+
if (tl)
|
|
91
|
+
mask |= 1;
|
|
92
|
+
if (t)
|
|
93
|
+
mask |= 2;
|
|
94
|
+
if (tr)
|
|
95
|
+
mask |= 4;
|
|
96
|
+
if (l)
|
|
97
|
+
mask |= 8;
|
|
98
|
+
if (ri)
|
|
99
|
+
mask |= 16;
|
|
100
|
+
if (bl)
|
|
101
|
+
mask |= 32;
|
|
102
|
+
if (b)
|
|
103
|
+
mask |= 64;
|
|
104
|
+
if (br)
|
|
105
|
+
mask |= 128;
|
|
106
|
+
// Map 8-bit mask to 1-47 tile index
|
|
107
|
+
// Use a hash to compress 256 possible masks into 47 unique tiles
|
|
108
|
+
return this._blob47Lookup(mask);
|
|
109
|
+
}
|
|
110
|
+
/** Map 8-bit blob mask to 1-47 tile index */
|
|
111
|
+
static _blob47Lookup(mask) {
|
|
112
|
+
// Standard blob tileset mapping.
|
|
113
|
+
// Each unique visual configuration maps to one of 47 tiles.
|
|
114
|
+
// We use cardinal bits to determine the base shape, then refine with corners.
|
|
115
|
+
const cardinals = mask & 0b01001010; // T, R, B, L extracted
|
|
116
|
+
const t = (mask >> 1) & 1;
|
|
117
|
+
const r = (mask >> 4) & 1;
|
|
118
|
+
const b = (mask >> 6) & 1;
|
|
119
|
+
const l = (mask >> 3) & 1;
|
|
120
|
+
const tl = (mask >> 0) & 1;
|
|
121
|
+
const tr = (mask >> 2) & 1;
|
|
122
|
+
const bl = (mask >> 5) & 1;
|
|
123
|
+
const br = (mask >> 7) & 1;
|
|
124
|
+
// Cardinal pattern determines base (1-16), corners refine within that group
|
|
125
|
+
const base = t | (r << 1) | (b << 2) | (l << 3);
|
|
126
|
+
const corners = tl | (tr << 1) | (bl << 2) | (br << 3);
|
|
127
|
+
// Simple deterministic mapping: base*3 + corner_offset, clamped to 1-47
|
|
128
|
+
const idx = base * 3 + (corners % 3) + 1;
|
|
129
|
+
return Math.min(47, Math.max(1, idx));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TilemapOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a Tiled JSON export into TilemapOptions.
|
|
4
|
+
* Supports orthogonal maps with tile layers.
|
|
5
|
+
*/
|
|
6
|
+
interface TiledMap {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
tilewidth: number;
|
|
10
|
+
tileheight: number;
|
|
11
|
+
layers: TiledLayer[];
|
|
12
|
+
}
|
|
13
|
+
interface TiledLayer {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
data?: number[];
|
|
19
|
+
visible: boolean;
|
|
20
|
+
properties?: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
value: unknown;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare class TiledImporter {
|
|
26
|
+
/**
|
|
27
|
+
* Parse a Tiled JSON map into TilemapOptions.
|
|
28
|
+
*
|
|
29
|
+
* Layer properties recognized:
|
|
30
|
+
* - `collision: true` marks a layer as a collision layer
|
|
31
|
+
*/
|
|
32
|
+
static parse(json: TiledMap): TilemapOptions;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class TiledImporter {
|
|
2
|
+
/**
|
|
3
|
+
* Parse a Tiled JSON map into TilemapOptions.
|
|
4
|
+
*
|
|
5
|
+
* Layer properties recognized:
|
|
6
|
+
* - `collision: true` marks a layer as a collision layer
|
|
7
|
+
*/
|
|
8
|
+
static parse(json) {
|
|
9
|
+
const tileSize = json.tilewidth;
|
|
10
|
+
const layers = {};
|
|
11
|
+
for (const tiledLayer of json.layers) {
|
|
12
|
+
if (tiledLayer.type !== 'tilelayer' || !tiledLayer.data)
|
|
13
|
+
continue;
|
|
14
|
+
// Convert flat 1D data array to 2D grid
|
|
15
|
+
const data = [];
|
|
16
|
+
for (let row = 0; row < json.height; row++) {
|
|
17
|
+
const rowData = [];
|
|
18
|
+
for (let col = 0; col < json.width; col++) {
|
|
19
|
+
rowData.push(tiledLayer.data[row * json.width + col]);
|
|
20
|
+
}
|
|
21
|
+
data.push(rowData);
|
|
22
|
+
}
|
|
23
|
+
// Check for collision property
|
|
24
|
+
const isCollision = tiledLayer.name.toLowerCase().includes('collision') ||
|
|
25
|
+
tiledLayer.properties?.some((p) => p.name === 'collision' && p.value === true) ||
|
|
26
|
+
false;
|
|
27
|
+
layers[tiledLayer.name] = {
|
|
28
|
+
data,
|
|
29
|
+
collision: isCollision,
|
|
30
|
+
visible: tiledLayer.visible,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { tileSize, layers };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TilemapOptions, TilemapLayer, TileRect, RaycastResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Core tilemap data structure with collision queries.
|
|
4
|
+
* Renderer-agnostic — pair with an engine-specific adapter for rendering.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Tilemap {
|
|
7
|
+
readonly tileSize: number;
|
|
8
|
+
readonly layers: Record<string, TilemapLayer>;
|
|
9
|
+
readonly rows: number;
|
|
10
|
+
readonly cols: number;
|
|
11
|
+
constructor(opts: TilemapOptions);
|
|
12
|
+
/** Get tile value at grid coordinates for a given layer */
|
|
13
|
+
getTile(layerName: string, row: number, col: number): number;
|
|
14
|
+
/** Set tile value at grid coordinates */
|
|
15
|
+
setTile(layerName: string, row: number, col: number, value: number): void;
|
|
16
|
+
/** Check if a world position is on a solid tile (any collision layer) */
|
|
17
|
+
isSolid(worldX: number, worldY: number): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Get all solid tile rects that overlap with a world-space AABB.
|
|
20
|
+
* Use for physics integration — pass entity bounds, get nearby solid rects.
|
|
21
|
+
*/
|
|
22
|
+
getCollisionTiles(x: number, y: number, w: number, h: number): TileRect[];
|
|
23
|
+
/**
|
|
24
|
+
* Cast a ray from (x1,y1) to (x2,y2) and find the first solid tile hit.
|
|
25
|
+
* Uses DDA (Digital Differential Analyzer) for efficient grid traversal.
|
|
26
|
+
*/
|
|
27
|
+
raycast(x1: number, y1: number, x2: number, y2: number): RaycastResult;
|
|
28
|
+
/** World width in pixels */
|
|
29
|
+
get worldWidth(): number;
|
|
30
|
+
/** World height in pixels */
|
|
31
|
+
get worldHeight(): number;
|
|
32
|
+
}
|
package/dist/Tilemap.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core tilemap data structure with collision queries.
|
|
3
|
+
* Renderer-agnostic — pair with an engine-specific adapter for rendering.
|
|
4
|
+
*/
|
|
5
|
+
export class Tilemap {
|
|
6
|
+
tileSize;
|
|
7
|
+
layers;
|
|
8
|
+
rows;
|
|
9
|
+
cols;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.tileSize = opts.tileSize;
|
|
12
|
+
this.layers = opts.layers;
|
|
13
|
+
// Derive grid dimensions from the first layer
|
|
14
|
+
const firstLayer = Object.values(this.layers)[0];
|
|
15
|
+
this.rows = firstLayer?.data.length ?? 0;
|
|
16
|
+
this.cols = firstLayer?.data[0]?.length ?? 0;
|
|
17
|
+
}
|
|
18
|
+
/** Get tile value at grid coordinates for a given layer */
|
|
19
|
+
getTile(layerName, row, col) {
|
|
20
|
+
const layer = this.layers[layerName];
|
|
21
|
+
if (!layer)
|
|
22
|
+
return 0;
|
|
23
|
+
if (row < 0 || row >= this.rows || col < 0 || col >= this.cols)
|
|
24
|
+
return 0;
|
|
25
|
+
return layer.data[row][col];
|
|
26
|
+
}
|
|
27
|
+
/** Set tile value at grid coordinates */
|
|
28
|
+
setTile(layerName, row, col, value) {
|
|
29
|
+
const layer = this.layers[layerName];
|
|
30
|
+
if (!layer)
|
|
31
|
+
return;
|
|
32
|
+
if (row < 0 || row >= this.rows || col < 0 || col >= this.cols)
|
|
33
|
+
return;
|
|
34
|
+
layer.data[row][col] = value;
|
|
35
|
+
}
|
|
36
|
+
/** Check if a world position is on a solid tile (any collision layer) */
|
|
37
|
+
isSolid(worldX, worldY) {
|
|
38
|
+
const col = Math.floor(worldX / this.tileSize);
|
|
39
|
+
const row = Math.floor(worldY / this.tileSize);
|
|
40
|
+
for (const layer of Object.values(this.layers)) {
|
|
41
|
+
if (!layer.collision)
|
|
42
|
+
continue;
|
|
43
|
+
if (row < 0 || row >= this.rows || col < 0 || col >= this.cols)
|
|
44
|
+
continue;
|
|
45
|
+
if (layer.data[row][col] !== 0)
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get all solid tile rects that overlap with a world-space AABB.
|
|
52
|
+
* Use for physics integration — pass entity bounds, get nearby solid rects.
|
|
53
|
+
*/
|
|
54
|
+
getCollisionTiles(x, y, w, h) {
|
|
55
|
+
const ts = this.tileSize;
|
|
56
|
+
const startCol = Math.max(0, Math.floor(x / ts));
|
|
57
|
+
const endCol = Math.min(this.cols - 1, Math.floor((x + w) / ts));
|
|
58
|
+
const startRow = Math.max(0, Math.floor(y / ts));
|
|
59
|
+
const endRow = Math.min(this.rows - 1, Math.floor((y + h) / ts));
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const layer of Object.values(this.layers)) {
|
|
62
|
+
if (!layer.collision)
|
|
63
|
+
continue;
|
|
64
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
65
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
66
|
+
if (layer.data[row][col] !== 0) {
|
|
67
|
+
results.push({
|
|
68
|
+
x: col * ts,
|
|
69
|
+
y: row * ts,
|
|
70
|
+
w: ts,
|
|
71
|
+
h: ts,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Cast a ray from (x1,y1) to (x2,y2) and find the first solid tile hit.
|
|
81
|
+
* Uses DDA (Digital Differential Analyzer) for efficient grid traversal.
|
|
82
|
+
*/
|
|
83
|
+
raycast(x1, y1, x2, y2) {
|
|
84
|
+
const ts = this.tileSize;
|
|
85
|
+
const dx = x2 - x1;
|
|
86
|
+
const dy = y2 - y1;
|
|
87
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
88
|
+
if (dist === 0)
|
|
89
|
+
return { hit: false };
|
|
90
|
+
const stepX = dx / dist;
|
|
91
|
+
const stepY = dy / dist;
|
|
92
|
+
const stepSize = ts / 2; // half-tile steps for precision
|
|
93
|
+
let cx = x1, cy = y1;
|
|
94
|
+
const maxSteps = Math.ceil(dist / stepSize);
|
|
95
|
+
for (let i = 0; i <= maxSteps; i++) {
|
|
96
|
+
if (this.isSolid(cx, cy)) {
|
|
97
|
+
return {
|
|
98
|
+
hit: true,
|
|
99
|
+
point: { x: cx, y: cy },
|
|
100
|
+
tile: {
|
|
101
|
+
row: Math.floor(cy / ts),
|
|
102
|
+
col: Math.floor(cx / ts),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
cx += stepX * stepSize;
|
|
107
|
+
cy += stepY * stepSize;
|
|
108
|
+
}
|
|
109
|
+
return { hit: false };
|
|
110
|
+
}
|
|
111
|
+
/** World width in pixels */
|
|
112
|
+
get worldWidth() {
|
|
113
|
+
return this.cols * this.tileSize;
|
|
114
|
+
}
|
|
115
|
+
/** World height in pixels */
|
|
116
|
+
get worldHeight() {
|
|
117
|
+
return this.rows * this.tileSize;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Tilemap } from '../Tilemap';
|
|
2
|
+
import type { TilemapRenderer } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Babylon.js adapter — renders tilemap layers as mesh grids.
|
|
5
|
+
* Each solid tile becomes a box or plane mesh.
|
|
6
|
+
*/
|
|
7
|
+
interface BabylonRendererOptions {
|
|
8
|
+
/** Height of extruded tiles (0 = flat). Default 1 */
|
|
9
|
+
tileHeight?: number;
|
|
10
|
+
/** Color map: tile index → hex string (e.g. '#666666') */
|
|
11
|
+
colorMap?: Record<number, string>;
|
|
12
|
+
}
|
|
13
|
+
export declare class BabylonTilemapRenderer implements TilemapRenderer {
|
|
14
|
+
private scene;
|
|
15
|
+
private map;
|
|
16
|
+
private opts;
|
|
17
|
+
private meshes;
|
|
18
|
+
constructor(scene: any, map: Tilemap, opts?: BabylonRendererOptions);
|
|
19
|
+
render(): void;
|
|
20
|
+
update(): void;
|
|
21
|
+
updateTile(layerName: string, row: number, col: number, value: number): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
private _getBabylon;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export class BabylonTilemapRenderer {
|
|
2
|
+
scene; // BABYLON.Scene
|
|
3
|
+
map;
|
|
4
|
+
opts;
|
|
5
|
+
meshes = new Map();
|
|
6
|
+
constructor(scene, map, opts = {}) {
|
|
7
|
+
this.scene = scene;
|
|
8
|
+
this.map = map;
|
|
9
|
+
this.opts = opts;
|
|
10
|
+
}
|
|
11
|
+
render() {
|
|
12
|
+
// Dynamic import of Babylon to avoid requiring it at module level
|
|
13
|
+
const BABYLON = this._getBabylon();
|
|
14
|
+
if (!BABYLON)
|
|
15
|
+
return;
|
|
16
|
+
const { map, opts, scene } = this;
|
|
17
|
+
const ts = map.tileSize;
|
|
18
|
+
const h = opts.tileHeight ?? 1;
|
|
19
|
+
for (const [name, layer] of Object.entries(map.layers)) {
|
|
20
|
+
if (layer.visible === false)
|
|
21
|
+
continue;
|
|
22
|
+
for (let r = 0; r < map.rows; r++) {
|
|
23
|
+
for (let c = 0; c < map.cols; c++) {
|
|
24
|
+
const tile = layer.data[r][c];
|
|
25
|
+
if (tile === 0)
|
|
26
|
+
continue;
|
|
27
|
+
const mesh = h > 0
|
|
28
|
+
? BABYLON.MeshBuilder.CreateBox(`tile_${name}_${r}_${c}`, { width: ts, height: h, depth: ts }, scene)
|
|
29
|
+
: BABYLON.MeshBuilder.CreatePlane(`tile_${name}_${r}_${c}`, { width: ts, height: ts }, scene);
|
|
30
|
+
mesh.position.x = c * ts + ts / 2;
|
|
31
|
+
mesh.position.y = h / 2;
|
|
32
|
+
mesh.position.z = r * ts + ts / 2;
|
|
33
|
+
const colorHex = opts.colorMap?.[tile] ?? '#666666';
|
|
34
|
+
const material = new BABYLON.StandardMaterial(`mat_${name}_${r}_${c}`, scene);
|
|
35
|
+
material.diffuseColor = BABYLON.Color3.FromHexString(colorHex);
|
|
36
|
+
mesh.material = material;
|
|
37
|
+
this.meshes.set(`${name}:${r}:${c}`, mesh);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
update() {
|
|
43
|
+
// Babylon handles frustum culling natively
|
|
44
|
+
}
|
|
45
|
+
updateTile(layerName, row, col, value) {
|
|
46
|
+
const key = `${layerName}:${row}:${col}`;
|
|
47
|
+
const existing = this.meshes.get(key);
|
|
48
|
+
if (existing) {
|
|
49
|
+
existing.material?.dispose();
|
|
50
|
+
existing.dispose();
|
|
51
|
+
this.meshes.delete(key);
|
|
52
|
+
}
|
|
53
|
+
if (value !== 0) {
|
|
54
|
+
// Re-render just this tile
|
|
55
|
+
const BABYLON = this._getBabylon();
|
|
56
|
+
if (!BABYLON)
|
|
57
|
+
return;
|
|
58
|
+
const ts = this.map.tileSize;
|
|
59
|
+
const h = this.opts.tileHeight ?? 1;
|
|
60
|
+
const mesh = BABYLON.MeshBuilder.CreateBox(`tile_${layerName}_${row}_${col}`, { width: ts, height: h, depth: ts }, this.scene);
|
|
61
|
+
mesh.position.set(col * ts + ts / 2, h / 2, row * ts + ts / 2);
|
|
62
|
+
const colorHex = this.opts.colorMap?.[value] ?? '#666666';
|
|
63
|
+
const material = new BABYLON.StandardMaterial(`mat_${layerName}_${row}_${col}`, this.scene);
|
|
64
|
+
material.diffuseColor = BABYLON.Color3.FromHexString(colorHex);
|
|
65
|
+
mesh.material = material;
|
|
66
|
+
this.meshes.set(key, mesh);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
destroy() {
|
|
70
|
+
for (const mesh of this.meshes.values()) {
|
|
71
|
+
mesh.material?.dispose();
|
|
72
|
+
mesh.dispose();
|
|
73
|
+
}
|
|
74
|
+
this.meshes.clear();
|
|
75
|
+
}
|
|
76
|
+
_getBabylon() {
|
|
77
|
+
// Try to access BABYLON from the global scope or scene engine
|
|
78
|
+
return globalThis.BABYLON ?? null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BabylonTilemapRenderer } from './BabylonTilemapRenderer';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BabylonTilemapRenderer } from './BabylonTilemapRenderer';
|
package/dist/babylon.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
var B = Object.defineProperty;
|
|
2
|
+
var u = (c, e, t) => e in c ? B(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t;
|
|
3
|
+
var $ = (c, e, t) => u(c, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
class x {
|
|
5
|
+
constructor(e, t, n = {}) {
|
|
6
|
+
$(this, "scene");
|
|
7
|
+
// BABYLON.Scene
|
|
8
|
+
$(this, "map");
|
|
9
|
+
$(this, "opts");
|
|
10
|
+
$(this, "meshes", /* @__PURE__ */ new Map());
|
|
11
|
+
this.scene = e, this.map = t, this.opts = n;
|
|
12
|
+
}
|
|
13
|
+
render() {
|
|
14
|
+
var m;
|
|
15
|
+
const e = this._getBabylon();
|
|
16
|
+
if (!e) return;
|
|
17
|
+
const { map: t, opts: n, scene: p } = this, i = t.tileSize, r = n.tileHeight ?? 1;
|
|
18
|
+
for (const [h, a] of Object.entries(t.layers))
|
|
19
|
+
if (a.visible !== !1)
|
|
20
|
+
for (let s = 0; s < t.rows; s++)
|
|
21
|
+
for (let o = 0; o < t.cols; o++) {
|
|
22
|
+
const d = a.data[s][o];
|
|
23
|
+
if (d === 0) continue;
|
|
24
|
+
const l = r > 0 ? e.MeshBuilder.CreateBox(
|
|
25
|
+
`tile_${h}_${s}_${o}`,
|
|
26
|
+
{ width: i, height: r, depth: i },
|
|
27
|
+
p
|
|
28
|
+
) : e.MeshBuilder.CreatePlane(
|
|
29
|
+
`tile_${h}_${s}_${o}`,
|
|
30
|
+
{ width: i, height: i },
|
|
31
|
+
p
|
|
32
|
+
);
|
|
33
|
+
l.position.x = o * i + i / 2, l.position.y = r / 2, l.position.z = s * i + i / 2;
|
|
34
|
+
const _ = ((m = n.colorMap) == null ? void 0 : m[d]) ?? "#666666", f = new e.StandardMaterial(
|
|
35
|
+
`mat_${h}_${s}_${o}`,
|
|
36
|
+
p
|
|
37
|
+
);
|
|
38
|
+
f.diffuseColor = e.Color3.FromHexString(_), l.material = f, this.meshes.set(`${h}:${s}:${o}`, l);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
update() {
|
|
42
|
+
}
|
|
43
|
+
updateTile(e, t, n, p) {
|
|
44
|
+
var m, h;
|
|
45
|
+
const i = `${e}:${t}:${n}`, r = this.meshes.get(i);
|
|
46
|
+
if (r && ((m = r.material) == null || m.dispose(), r.dispose(), this.meshes.delete(i)), p !== 0) {
|
|
47
|
+
const a = this._getBabylon();
|
|
48
|
+
if (!a) return;
|
|
49
|
+
const s = this.map.tileSize, o = this.opts.tileHeight ?? 1, d = a.MeshBuilder.CreateBox(
|
|
50
|
+
`tile_${e}_${t}_${n}`,
|
|
51
|
+
{ width: s, height: o, depth: s },
|
|
52
|
+
this.scene
|
|
53
|
+
);
|
|
54
|
+
d.position.set(n * s + s / 2, o / 2, t * s + s / 2);
|
|
55
|
+
const l = ((h = this.opts.colorMap) == null ? void 0 : h[p]) ?? "#666666", _ = new a.StandardMaterial(
|
|
56
|
+
`mat_${e}_${t}_${n}`,
|
|
57
|
+
this.scene
|
|
58
|
+
);
|
|
59
|
+
_.diffuseColor = a.Color3.FromHexString(l), d.material = _, this.meshes.set(i, d);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
destroy() {
|
|
63
|
+
var e;
|
|
64
|
+
for (const t of this.meshes.values())
|
|
65
|
+
(e = t.material) == null || e.dispose(), t.dispose();
|
|
66
|
+
this.meshes.clear();
|
|
67
|
+
}
|
|
68
|
+
_getBabylon() {
|
|
69
|
+
return globalThis.BABYLON ?? null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
x as BabylonTilemapRenderer
|
|
74
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Tilemap } from '../Tilemap';
|
|
2
|
+
import type { TilemapRenderer } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Phaser adapter — renders tilemap layers using Phaser's Graphics API.
|
|
5
|
+
* Also supports bridging collision data into Phaser's physics system.
|
|
6
|
+
*
|
|
7
|
+
* Usage: Pass a Phaser.Scene and @joydle/tilemap data.
|
|
8
|
+
*/
|
|
9
|
+
interface PhaserRendererOptions {
|
|
10
|
+
/** Color map: tile index → hex color (e.g. 0x665577) */
|
|
11
|
+
colorMap?: Record<number, number>;
|
|
12
|
+
/** Default tile color when not in colorMap */
|
|
13
|
+
defaultColor?: number;
|
|
14
|
+
/** If true, create Phaser static bodies for collision tiles */
|
|
15
|
+
enablePhysics?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class PhaserTilemapRenderer implements TilemapRenderer {
|
|
18
|
+
private scene;
|
|
19
|
+
private map;
|
|
20
|
+
private opts;
|
|
21
|
+
private graphics;
|
|
22
|
+
private collisionBodies;
|
|
23
|
+
constructor(scene: any, map: Tilemap, opts?: PhaserRendererOptions);
|
|
24
|
+
render(): void;
|
|
25
|
+
update(): void;
|
|
26
|
+
updateTile(layerName: string, row: number, col: number, value: number): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export class PhaserTilemapRenderer {
|
|
2
|
+
scene; // Phaser.Scene
|
|
3
|
+
map;
|
|
4
|
+
opts;
|
|
5
|
+
graphics = null;
|
|
6
|
+
collisionBodies = [];
|
|
7
|
+
constructor(scene, map, opts = {}) {
|
|
8
|
+
this.scene = scene;
|
|
9
|
+
this.map = map;
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
}
|
|
12
|
+
render() {
|
|
13
|
+
const { scene, map, opts } = this;
|
|
14
|
+
const ts = map.tileSize;
|
|
15
|
+
const colorMap = opts.colorMap ?? {};
|
|
16
|
+
const defaultColor = opts.defaultColor ?? 0x666666;
|
|
17
|
+
// Draw tiles using Phaser Graphics
|
|
18
|
+
this.graphics = scene.add.graphics();
|
|
19
|
+
for (const [_name, layer] of Object.entries(map.layers)) {
|
|
20
|
+
if (layer.visible === false)
|
|
21
|
+
continue;
|
|
22
|
+
for (let r = 0; r < map.rows; r++) {
|
|
23
|
+
for (let c = 0; c < map.cols; c++) {
|
|
24
|
+
const tile = layer.data[r][c];
|
|
25
|
+
if (tile === 0)
|
|
26
|
+
continue;
|
|
27
|
+
const color = colorMap[tile] ?? defaultColor;
|
|
28
|
+
this.graphics.fillStyle(color);
|
|
29
|
+
this.graphics.fillRect(c * ts, r * ts, ts, ts);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Optionally create physics bodies for collision layers
|
|
34
|
+
if (opts.enablePhysics) {
|
|
35
|
+
for (const layer of Object.values(map.layers)) {
|
|
36
|
+
if (!layer.collision)
|
|
37
|
+
continue;
|
|
38
|
+
for (let r = 0; r < map.rows; r++) {
|
|
39
|
+
for (let c = 0; c < map.cols; c++) {
|
|
40
|
+
if (layer.data[r][c] === 0)
|
|
41
|
+
continue;
|
|
42
|
+
const body = scene.physics?.add.staticImage(c * ts + ts / 2, r * ts + ts / 2);
|
|
43
|
+
if (body) {
|
|
44
|
+
body.setSize(ts, ts);
|
|
45
|
+
body.setVisible(false);
|
|
46
|
+
this.collisionBodies.push(body);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
update() {
|
|
54
|
+
// Phaser handles camera culling natively
|
|
55
|
+
}
|
|
56
|
+
updateTile(layerName, row, col, value) {
|
|
57
|
+
// Redraw the full graphics layer (Phaser Graphics doesn't support partial updates easily)
|
|
58
|
+
if (this.graphics) {
|
|
59
|
+
this.graphics.destroy();
|
|
60
|
+
this.graphics = null;
|
|
61
|
+
}
|
|
62
|
+
this.map.setTile(layerName, row, col, value);
|
|
63
|
+
this.render();
|
|
64
|
+
}
|
|
65
|
+
destroy() {
|
|
66
|
+
this.graphics?.destroy();
|
|
67
|
+
this.graphics = null;
|
|
68
|
+
for (const body of this.collisionBodies) {
|
|
69
|
+
body?.destroy();
|
|
70
|
+
}
|
|
71
|
+
this.collisionBodies = [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PhaserTilemapRenderer } from './PhaserTilemapRenderer';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PhaserTilemapRenderer } from './PhaserTilemapRenderer';
|
package/dist/phaser.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
var g = Object.defineProperty;
|
|
2
|
+
var y = (r, i, s) => i in r ? g(r, i, { enumerable: !0, configurable: !0, writable: !0, value: s }) : r[i] = s;
|
|
3
|
+
var a = (r, i, s) => y(r, typeof i != "symbol" ? i + "" : i, s);
|
|
4
|
+
class b {
|
|
5
|
+
constructor(i, s, l = {}) {
|
|
6
|
+
a(this, "scene");
|
|
7
|
+
// Phaser.Scene
|
|
8
|
+
a(this, "map");
|
|
9
|
+
a(this, "opts");
|
|
10
|
+
a(this, "graphics", null);
|
|
11
|
+
a(this, "collisionBodies", []);
|
|
12
|
+
this.scene = i, this.map = s, this.opts = l;
|
|
13
|
+
}
|
|
14
|
+
render() {
|
|
15
|
+
var h;
|
|
16
|
+
const { scene: i, map: s, opts: l } = this, e = s.tileSize, f = l.colorMap ?? {}, d = l.defaultColor ?? 6710886;
|
|
17
|
+
this.graphics = i.add.graphics();
|
|
18
|
+
for (const [n, c] of Object.entries(s.layers))
|
|
19
|
+
if (c.visible !== !1)
|
|
20
|
+
for (let t = 0; t < s.rows; t++)
|
|
21
|
+
for (let o = 0; o < s.cols; o++) {
|
|
22
|
+
const p = c.data[t][o];
|
|
23
|
+
if (p === 0) continue;
|
|
24
|
+
const u = f[p] ?? d;
|
|
25
|
+
this.graphics.fillStyle(u), this.graphics.fillRect(o * e, t * e, e, e);
|
|
26
|
+
}
|
|
27
|
+
if (l.enablePhysics) {
|
|
28
|
+
for (const n of Object.values(s.layers))
|
|
29
|
+
if (n.collision)
|
|
30
|
+
for (let c = 0; c < s.rows; c++)
|
|
31
|
+
for (let t = 0; t < s.cols; t++) {
|
|
32
|
+
if (n.data[c][t] === 0) continue;
|
|
33
|
+
const o = (h = i.physics) == null ? void 0 : h.add.staticImage(
|
|
34
|
+
t * e + e / 2,
|
|
35
|
+
c * e + e / 2
|
|
36
|
+
);
|
|
37
|
+
o && (o.setSize(e, e), o.setVisible(!1), this.collisionBodies.push(o));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
update() {
|
|
42
|
+
}
|
|
43
|
+
updateTile(i, s, l, e) {
|
|
44
|
+
this.graphics && (this.graphics.destroy(), this.graphics = null), this.map.setTile(i, s, l, e), this.render();
|
|
45
|
+
}
|
|
46
|
+
destroy() {
|
|
47
|
+
var i;
|
|
48
|
+
(i = this.graphics) == null || i.destroy(), this.graphics = null;
|
|
49
|
+
for (const s of this.collisionBodies)
|
|
50
|
+
s == null || s.destroy();
|
|
51
|
+
this.collisionBodies = [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
b as PhaserTilemapRenderer
|
|
56
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Container, Texture } from 'pixi.js';
|
|
2
|
+
import type { Tilemap } from '../Tilemap';
|
|
3
|
+
import type { TilemapRenderer } from '../types';
|
|
4
|
+
interface PixiRendererOptions {
|
|
5
|
+
/** Spritesheet texture (uniform grid of tiles) */
|
|
6
|
+
tileset?: Texture;
|
|
7
|
+
/** Number of tile columns in the tileset texture */
|
|
8
|
+
tilesetCols?: number;
|
|
9
|
+
/** Camera reference for viewport culling */
|
|
10
|
+
camera?: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
zoom?: number;
|
|
14
|
+
vw?: number;
|
|
15
|
+
vh?: number;
|
|
16
|
+
};
|
|
17
|
+
/** Fallback color map: tile index → hex color (used when no tileset) */
|
|
18
|
+
colorMap?: Record<number, number>;
|
|
19
|
+
}
|
|
20
|
+
export declare class PixiTilemapRenderer implements TilemapRenderer {
|
|
21
|
+
private container;
|
|
22
|
+
private map;
|
|
23
|
+
private opts;
|
|
24
|
+
private tileSprites;
|
|
25
|
+
private layerContainers;
|
|
26
|
+
constructor(parent: Container, map: Tilemap, opts?: PixiRendererOptions);
|
|
27
|
+
render(): void;
|
|
28
|
+
update(camera?: {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
zoom?: number;
|
|
32
|
+
vw?: number;
|
|
33
|
+
vh?: number;
|
|
34
|
+
}): void;
|
|
35
|
+
updateTile(layerName: string, row: number, col: number, value: number): void;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
private _createTile;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Container, Graphics, Sprite, Texture, Rectangle } from 'pixi.js';
|
|
2
|
+
export class PixiTilemapRenderer {
|
|
3
|
+
container;
|
|
4
|
+
map;
|
|
5
|
+
opts;
|
|
6
|
+
tileSprites = new Map();
|
|
7
|
+
layerContainers = new Map();
|
|
8
|
+
constructor(parent, map, opts = {}) {
|
|
9
|
+
this.container = new Container();
|
|
10
|
+
this.map = map;
|
|
11
|
+
this.opts = opts;
|
|
12
|
+
parent.addChild(this.container);
|
|
13
|
+
this.container.sortableChildren = true;
|
|
14
|
+
}
|
|
15
|
+
render() {
|
|
16
|
+
const { map, opts } = this;
|
|
17
|
+
const ts = map.tileSize;
|
|
18
|
+
for (const [name, layer] of Object.entries(map.layers)) {
|
|
19
|
+
if (layer.visible === false)
|
|
20
|
+
continue;
|
|
21
|
+
const layerContainer = new Container();
|
|
22
|
+
layerContainer.zIndex = layer.zIndex ?? 0;
|
|
23
|
+
this.container.addChild(layerContainer);
|
|
24
|
+
this.layerContainers.set(name, layerContainer);
|
|
25
|
+
for (let r = 0; r < map.rows; r++) {
|
|
26
|
+
for (let c = 0; c < map.cols; c++) {
|
|
27
|
+
const tile = layer.data[r][c];
|
|
28
|
+
if (tile === 0)
|
|
29
|
+
continue;
|
|
30
|
+
const sprite = this._createTile(tile, ts);
|
|
31
|
+
sprite.x = c * ts;
|
|
32
|
+
sprite.y = r * ts;
|
|
33
|
+
layerContainer.addChild(sprite);
|
|
34
|
+
this.tileSprites.set(`${name}:${r}:${c}`, sprite);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
update(camera) {
|
|
40
|
+
const cam = camera ?? this.opts.camera;
|
|
41
|
+
if (!cam)
|
|
42
|
+
return;
|
|
43
|
+
const zoom = cam.zoom ?? 1;
|
|
44
|
+
const vw = (cam.vw ?? 480) / zoom;
|
|
45
|
+
const vh = (cam.vh ?? 700) / zoom;
|
|
46
|
+
const ts = this.map.tileSize;
|
|
47
|
+
const left = cam.x - vw / 2 - ts;
|
|
48
|
+
const right = cam.x + vw / 2 + ts;
|
|
49
|
+
const top = cam.y - vh / 2 - ts;
|
|
50
|
+
const bottom = cam.y + vh / 2 + ts;
|
|
51
|
+
// Cull tiles outside camera viewport
|
|
52
|
+
for (const [key, sprite] of this.tileSprites) {
|
|
53
|
+
const visible = sprite.x + ts > left &&
|
|
54
|
+
sprite.x < right &&
|
|
55
|
+
sprite.y + ts > top &&
|
|
56
|
+
sprite.y < bottom;
|
|
57
|
+
sprite.visible = visible;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
updateTile(layerName, row, col, value) {
|
|
61
|
+
const key = `${layerName}:${row}:${col}`;
|
|
62
|
+
const existing = this.tileSprites.get(key);
|
|
63
|
+
const layerContainer = this.layerContainers.get(layerName);
|
|
64
|
+
if (existing) {
|
|
65
|
+
existing.removeFromParent();
|
|
66
|
+
existing.destroy();
|
|
67
|
+
this.tileSprites.delete(key);
|
|
68
|
+
}
|
|
69
|
+
if (value !== 0 && layerContainer) {
|
|
70
|
+
const sprite = this._createTile(value, this.map.tileSize);
|
|
71
|
+
sprite.x = col * this.map.tileSize;
|
|
72
|
+
sprite.y = row * this.map.tileSize;
|
|
73
|
+
layerContainer.addChild(sprite);
|
|
74
|
+
this.tileSprites.set(key, sprite);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
destroy() {
|
|
78
|
+
this.container.removeFromParent();
|
|
79
|
+
this.container.destroy({ children: true });
|
|
80
|
+
this.tileSprites.clear();
|
|
81
|
+
this.layerContainers.clear();
|
|
82
|
+
}
|
|
83
|
+
_createTile(tileIndex, size) {
|
|
84
|
+
const { tileset, tilesetCols, colorMap } = this.opts;
|
|
85
|
+
if (tileset && tilesetCols) {
|
|
86
|
+
// Extract sub-texture from spritesheet
|
|
87
|
+
const col = (tileIndex - 1) % tilesetCols;
|
|
88
|
+
const row = Math.floor((tileIndex - 1) / tilesetCols);
|
|
89
|
+
const frame = new Rectangle(col * size, row * size, size, size);
|
|
90
|
+
const tex = new Texture({ source: tileset.source, frame });
|
|
91
|
+
return new Sprite(tex);
|
|
92
|
+
}
|
|
93
|
+
// Fallback: colored rectangle
|
|
94
|
+
const color = colorMap?.[tileIndex] ?? 0x666666;
|
|
95
|
+
const g = new Graphics();
|
|
96
|
+
g.rect(0, 0, size, size);
|
|
97
|
+
g.fill(color);
|
|
98
|
+
return g;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PixiTilemapRenderer } from './PixiTilemapRenderer';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PixiTilemapRenderer } from './PixiTilemapRenderer';
|
package/dist/pixi.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
var C = Object.defineProperty;
|
|
2
|
+
var S = (l, e, t) => e in l ? C(l, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : l[e] = t;
|
|
3
|
+
var p = (l, e, t) => S(l, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
import { Container as f, Rectangle as u, Texture as w, Sprite as x, Graphics as v } from "pixi.js";
|
|
5
|
+
class T {
|
|
6
|
+
constructor(e, t, s = {}) {
|
|
7
|
+
p(this, "container");
|
|
8
|
+
p(this, "map");
|
|
9
|
+
p(this, "opts");
|
|
10
|
+
p(this, "tileSprites", /* @__PURE__ */ new Map());
|
|
11
|
+
p(this, "layerContainers", /* @__PURE__ */ new Map());
|
|
12
|
+
this.container = new f(), this.map = t, this.opts = s, e.addChild(this.container), this.container.sortableChildren = !0;
|
|
13
|
+
}
|
|
14
|
+
render() {
|
|
15
|
+
const { map: e, opts: t } = this, s = e.tileSize;
|
|
16
|
+
for (const [c, n] of Object.entries(e.layers)) {
|
|
17
|
+
if (n.visible === !1) continue;
|
|
18
|
+
const i = new f();
|
|
19
|
+
i.zIndex = n.zIndex ?? 0, this.container.addChild(i), this.layerContainers.set(c, i);
|
|
20
|
+
for (let o = 0; o < e.rows; o++)
|
|
21
|
+
for (let r = 0; r < e.cols; r++) {
|
|
22
|
+
const h = n.data[o][r];
|
|
23
|
+
if (h === 0) continue;
|
|
24
|
+
const a = this._createTile(h, s);
|
|
25
|
+
a.x = r * s, a.y = o * s, i.addChild(a), this.tileSprites.set(`${c}:${o}:${r}`, a);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
update(e) {
|
|
30
|
+
const t = e ?? this.opts.camera;
|
|
31
|
+
if (!t) return;
|
|
32
|
+
const s = t.zoom ?? 1, c = (t.vw ?? 480) / s, n = (t.vh ?? 700) / s, i = this.map.tileSize, o = t.x - c / 2 - i, r = t.x + c / 2 + i, h = t.y - n / 2 - i, a = t.y + n / 2 + i;
|
|
33
|
+
for (const [m, d] of this.tileSprites) {
|
|
34
|
+
const y = d.x + i > o && d.x < r && d.y + i > h && d.y < a;
|
|
35
|
+
d.visible = y;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
updateTile(e, t, s, c) {
|
|
39
|
+
const n = `${e}:${t}:${s}`, i = this.tileSprites.get(n), o = this.layerContainers.get(e);
|
|
40
|
+
if (i && (i.removeFromParent(), i.destroy(), this.tileSprites.delete(n)), c !== 0 && o) {
|
|
41
|
+
const r = this._createTile(c, this.map.tileSize);
|
|
42
|
+
r.x = s * this.map.tileSize, r.y = t * this.map.tileSize, o.addChild(r), this.tileSprites.set(n, r);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
destroy() {
|
|
46
|
+
this.container.removeFromParent(), this.container.destroy({ children: !0 }), this.tileSprites.clear(), this.layerContainers.clear();
|
|
47
|
+
}
|
|
48
|
+
_createTile(e, t) {
|
|
49
|
+
const { tileset: s, tilesetCols: c, colorMap: n } = this.opts;
|
|
50
|
+
if (s && c) {
|
|
51
|
+
const r = (e - 1) % c, h = Math.floor((e - 1) / c), a = new u(r * t, h * t, t, t), m = new w({ source: s.source, frame: a });
|
|
52
|
+
return new x(m);
|
|
53
|
+
}
|
|
54
|
+
const i = (n == null ? void 0 : n[e]) ?? 6710886, o = new v();
|
|
55
|
+
return o.rect(0, 0, t, t), o.fill(i), o;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
T as PixiTilemapRenderer
|
|
60
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Scene, Texture } from 'three';
|
|
2
|
+
import type { Tilemap } from '../Tilemap';
|
|
3
|
+
import type { TilemapRenderer } from '../types';
|
|
4
|
+
interface ThreeRendererOptions {
|
|
5
|
+
/** Texture atlas for tiles */
|
|
6
|
+
tileset?: Texture;
|
|
7
|
+
/** Height of extruded tiles (0 = flat planes). Default 1 */
|
|
8
|
+
tileHeight?: number;
|
|
9
|
+
/** Color map: tile index → hex color (used when no tileset) */
|
|
10
|
+
colorMap?: Record<number, number>;
|
|
11
|
+
}
|
|
12
|
+
export declare class ThreeTilemapRenderer implements TilemapRenderer {
|
|
13
|
+
private group;
|
|
14
|
+
private map;
|
|
15
|
+
private opts;
|
|
16
|
+
private meshes;
|
|
17
|
+
private geometry;
|
|
18
|
+
constructor(scene: Scene, map: Tilemap, opts?: ThreeRendererOptions);
|
|
19
|
+
render(): void;
|
|
20
|
+
update(): void;
|
|
21
|
+
updateTile(layerName: string, row: number, col: number, value: number): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Group, Mesh, BoxGeometry, PlaneGeometry, MeshBasicMaterial, Color, } from 'three';
|
|
2
|
+
export class ThreeTilemapRenderer {
|
|
3
|
+
group;
|
|
4
|
+
map;
|
|
5
|
+
opts;
|
|
6
|
+
meshes = new Map();
|
|
7
|
+
geometry;
|
|
8
|
+
constructor(scene, map, opts = {}) {
|
|
9
|
+
this.group = new Group();
|
|
10
|
+
this.map = map;
|
|
11
|
+
this.opts = opts;
|
|
12
|
+
scene.add(this.group);
|
|
13
|
+
const h = opts.tileHeight ?? 1;
|
|
14
|
+
this.geometry =
|
|
15
|
+
h > 0
|
|
16
|
+
? new BoxGeometry(map.tileSize, h, map.tileSize)
|
|
17
|
+
: new PlaneGeometry(map.tileSize, map.tileSize);
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
const { map, opts } = this;
|
|
21
|
+
const ts = map.tileSize;
|
|
22
|
+
for (const [name, layer] of Object.entries(map.layers)) {
|
|
23
|
+
if (layer.visible === false)
|
|
24
|
+
continue;
|
|
25
|
+
for (let r = 0; r < map.rows; r++) {
|
|
26
|
+
for (let c = 0; c < map.cols; c++) {
|
|
27
|
+
const tile = layer.data[r][c];
|
|
28
|
+
if (tile === 0)
|
|
29
|
+
continue;
|
|
30
|
+
const color = opts.colorMap?.[tile] ?? 0x666666;
|
|
31
|
+
const material = new MeshBasicMaterial({ color: new Color(color) });
|
|
32
|
+
const mesh = new Mesh(this.geometry, material);
|
|
33
|
+
// Position: X = col, Z = row (Y = up in Three.js)
|
|
34
|
+
mesh.position.set(c * ts + ts / 2, (opts.tileHeight ?? 1) / 2, r * ts + ts / 2);
|
|
35
|
+
this.group.add(mesh);
|
|
36
|
+
this.meshes.set(`${name}:${r}:${c}`, mesh);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
update() {
|
|
42
|
+
// Three.js handles culling via frustum automatically
|
|
43
|
+
}
|
|
44
|
+
updateTile(layerName, row, col, value) {
|
|
45
|
+
const key = `${layerName}:${row}:${col}`;
|
|
46
|
+
const existing = this.meshes.get(key);
|
|
47
|
+
if (existing) {
|
|
48
|
+
this.group.remove(existing);
|
|
49
|
+
existing.material.dispose();
|
|
50
|
+
this.meshes.delete(key);
|
|
51
|
+
}
|
|
52
|
+
if (value !== 0) {
|
|
53
|
+
const color = this.opts.colorMap?.[value] ?? 0x666666;
|
|
54
|
+
const material = new MeshBasicMaterial({ color: new Color(color) });
|
|
55
|
+
const mesh = new Mesh(this.geometry, material);
|
|
56
|
+
const ts = this.map.tileSize;
|
|
57
|
+
mesh.position.set(col * ts + ts / 2, (this.opts.tileHeight ?? 1) / 2, row * ts + ts / 2);
|
|
58
|
+
this.group.add(mesh);
|
|
59
|
+
this.meshes.set(key, mesh);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
destroy() {
|
|
63
|
+
for (const mesh of this.meshes.values()) {
|
|
64
|
+
mesh.material.dispose();
|
|
65
|
+
}
|
|
66
|
+
this.geometry.dispose();
|
|
67
|
+
this.group.removeFromParent();
|
|
68
|
+
this.meshes.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ThreeTilemapRenderer } from './ThreeTilemapRenderer';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ThreeTilemapRenderer } from './ThreeTilemapRenderer';
|
package/dist/three.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
var y = Object.defineProperty;
|
|
2
|
+
var w = (h, e, t) => e in h ? y(h, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : h[e] = t;
|
|
3
|
+
var a = (h, e, t) => w(h, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
import { Group as z, BoxGeometry as M, PlaneGeometry as S, MeshBasicMaterial as d, Color as u, Mesh as f } from "three";
|
|
5
|
+
class x {
|
|
6
|
+
constructor(e, t, s = {}) {
|
|
7
|
+
a(this, "group");
|
|
8
|
+
a(this, "map");
|
|
9
|
+
a(this, "opts");
|
|
10
|
+
a(this, "meshes", /* @__PURE__ */ new Map());
|
|
11
|
+
a(this, "geometry");
|
|
12
|
+
this.group = new z(), this.map = t, this.opts = s, e.add(this.group);
|
|
13
|
+
const o = s.tileHeight ?? 1;
|
|
14
|
+
this.geometry = o > 0 ? new M(t.tileSize, o, t.tileSize) : new S(t.tileSize, t.tileSize);
|
|
15
|
+
}
|
|
16
|
+
render() {
|
|
17
|
+
var o;
|
|
18
|
+
const { map: e, opts: t } = this, s = e.tileSize;
|
|
19
|
+
for (const [c, n] of Object.entries(e.layers))
|
|
20
|
+
if (n.visible !== !1)
|
|
21
|
+
for (let i = 0; i < e.rows; i++)
|
|
22
|
+
for (let r = 0; r < e.cols; r++) {
|
|
23
|
+
const p = n.data[i][r];
|
|
24
|
+
if (p === 0) continue;
|
|
25
|
+
const m = ((o = t.colorMap) == null ? void 0 : o[p]) ?? 6710886, l = new d({ color: new u(m) }), g = new f(this.geometry, l);
|
|
26
|
+
g.position.set(
|
|
27
|
+
r * s + s / 2,
|
|
28
|
+
(t.tileHeight ?? 1) / 2,
|
|
29
|
+
i * s + s / 2
|
|
30
|
+
), this.group.add(g), this.meshes.set(`${c}:${i}:${r}`, g);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
update() {
|
|
34
|
+
}
|
|
35
|
+
updateTile(e, t, s, o) {
|
|
36
|
+
var i;
|
|
37
|
+
const c = `${e}:${t}:${s}`, n = this.meshes.get(c);
|
|
38
|
+
if (n && (this.group.remove(n), n.material.dispose(), this.meshes.delete(c)), o !== 0) {
|
|
39
|
+
const r = ((i = this.opts.colorMap) == null ? void 0 : i[o]) ?? 6710886, p = new d({ color: new u(r) }), m = new f(this.geometry, p), l = this.map.tileSize;
|
|
40
|
+
m.position.set(
|
|
41
|
+
s * l + l / 2,
|
|
42
|
+
(this.opts.tileHeight ?? 1) / 2,
|
|
43
|
+
t * l + l / 2
|
|
44
|
+
), this.group.add(m), this.meshes.set(c, m);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
destroy() {
|
|
48
|
+
for (const e of this.meshes.values())
|
|
49
|
+
e.material.dispose();
|
|
50
|
+
this.geometry.dispose(), this.group.removeFromParent(), this.meshes.clear();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
x as ThreeTilemapRenderer
|
|
55
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface TilemapLayer {
|
|
2
|
+
/** 2D array of tile indices. 0 = empty. */
|
|
3
|
+
data: number[][];
|
|
4
|
+
/** If true, this layer participates in collision queries */
|
|
5
|
+
collision?: boolean;
|
|
6
|
+
/** If false, renderer skips this layer. Default true */
|
|
7
|
+
visible?: boolean;
|
|
8
|
+
/** Render order hint */
|
|
9
|
+
zIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface TilemapOptions {
|
|
12
|
+
tileSize: number;
|
|
13
|
+
layers: Record<string, TilemapLayer>;
|
|
14
|
+
}
|
|
15
|
+
export interface TileRect {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
w: number;
|
|
19
|
+
h: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RaycastResult {
|
|
22
|
+
hit: boolean;
|
|
23
|
+
/** World position of the hit point */
|
|
24
|
+
point?: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
};
|
|
28
|
+
/** Grid coordinates of the hit tile */
|
|
29
|
+
tile?: {
|
|
30
|
+
row: number;
|
|
31
|
+
col: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface AutoTileOptions {
|
|
35
|
+
/** The tile value that represents solid. Default 1 */
|
|
36
|
+
solid?: number;
|
|
37
|
+
/** Rule set. Default 'simple16' */
|
|
38
|
+
rules?: 'blob47' | 'simple16';
|
|
39
|
+
}
|
|
40
|
+
/** Shared renderer interface — each engine adapter implements this */
|
|
41
|
+
export interface TilemapRenderer {
|
|
42
|
+
/** Initial render of all visible tiles */
|
|
43
|
+
render(): void;
|
|
44
|
+
/** Re-render visible tiles (call on camera move for culling) */
|
|
45
|
+
update(camera?: {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
zoom?: number;
|
|
49
|
+
vw?: number;
|
|
50
|
+
vh?: number;
|
|
51
|
+
}): void;
|
|
52
|
+
/** Update a single tile visually after setTile() */
|
|
53
|
+
updateTile(layerName: string, row: number, col: number, value: number): void;
|
|
54
|
+
/** Clean up renderer resources */
|
|
55
|
+
destroy(): void;
|
|
56
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joydle/tilemap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Grid tiles, auto-tile, Tiled import, and AABB collision for PixiJS, Three.js, Phaser, and Babylon.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./pixi": {
|
|
14
|
+
"types": "./dist/pixi/index.d.ts",
|
|
15
|
+
"import": "./dist/pixi/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./three": {
|
|
18
|
+
"types": "./dist/three/index.d.ts",
|
|
19
|
+
"import": "./dist/three/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./phaser": {
|
|
22
|
+
"types": "./dist/phaser/index.d.ts",
|
|
23
|
+
"import": "./dist/phaser/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./babylon": {
|
|
26
|
+
"types": "./dist/babylon/index.d.ts",
|
|
27
|
+
"import": "./dist/babylon/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": ["dist"],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"demo:pixi": "vite serve examples/pixi --config examples/pixi/vite.config.ts",
|
|
34
|
+
"demo:three": "vite serve examples/three --config examples/three/vite.config.ts",
|
|
35
|
+
"demo:phaser": "vite serve examples/phaser --config examples/phaser/vite.config.ts",
|
|
36
|
+
"demo:babylon": "vite serve examples/babylon --config examples/babylon/vite.config.ts",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": { "access": "public" },
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "Joydle <contact@joydle.dev>",
|
|
43
|
+
"homepage": "https://joydle.dev",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@babylonjs/core": ">=6.0.0",
|
|
46
|
+
"phaser": ">=3.60.0",
|
|
47
|
+
"pixi.js": ">=7.0.0",
|
|
48
|
+
"three": ">=0.150.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"pixi.js": { "optional": true },
|
|
52
|
+
"three": { "optional": true },
|
|
53
|
+
"phaser": { "optional": true },
|
|
54
|
+
"@babylonjs/core": { "optional": true }
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@babylonjs/core": "^8.51.2",
|
|
58
|
+
"@types/three": "^0.170.0",
|
|
59
|
+
"phaser": "^3.90.0",
|
|
60
|
+
"pixi.js": "^8.0.0",
|
|
61
|
+
"three": "^0.170.0",
|
|
62
|
+
"typescript": "^5.7.0",
|
|
63
|
+
"vite": "^6.0.0",
|
|
64
|
+
"vitest": "^3.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|