@rpgjs/tiledmap 5.0.0-alpha.9 → 5.0.0-beta.10
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/CHANGELOG.md +38 -0
- package/LICENSE +19 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/compat.d.ts +19 -0
- package/dist/{index.d.ts → client/index.d.ts} +1 -1
- package/dist/client/index.js +35 -48
- package/dist/client/index2.js +3998 -6
- package/dist/client/index3.js +120 -4641
- package/dist/client/index4.js +64 -15
- package/dist/client/index5.js +11 -0
- package/dist/client/index6.js +22 -0
- package/dist/client/physics.d.ts +7 -0
- package/dist/server/compat.d.ts +19 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +12 -14
- package/dist/server/index2.js +3997 -93
- package/dist/server/index3.js +131 -5232
- package/dist/server/index4.js +93 -0
- package/dist/server/index5.js +15 -0
- package/dist/server/physics.d.ts +7 -0
- package/dist/server/server.d.ts +54 -0
- package/package.json +11 -11
- package/src/client.ts +9 -3
- package/src/compat.ts +221 -0
- package/src/index.ts +5 -1
- package/src/physics.spec.ts +285 -0
- package/src/physics.ts +171 -0
- package/src/server.ts +45 -152
- package/src/tiled.ce +5 -1
- package/dist/client.d.ts +0 -2
- package/dist/server.d.ts +0 -65
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@canvasengine/tiled", () => {
|
|
4
|
+
class MapClass {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
tilewidth: number;
|
|
8
|
+
tileheight: number;
|
|
9
|
+
widthPx: number;
|
|
10
|
+
heightPx: number;
|
|
11
|
+
layers: any[];
|
|
12
|
+
tilesets: any[];
|
|
13
|
+
private blockedTiles: Set<string>;
|
|
14
|
+
|
|
15
|
+
constructor(parsedMap: any) {
|
|
16
|
+
this.width = parsedMap.width ?? 0;
|
|
17
|
+
this.height = parsedMap.height ?? 0;
|
|
18
|
+
this.tilewidth = parsedMap.tilewidth ?? 32;
|
|
19
|
+
this.tileheight = parsedMap.tileheight ?? 32;
|
|
20
|
+
this.widthPx = this.width * this.tilewidth;
|
|
21
|
+
this.heightPx = this.height * this.tileheight;
|
|
22
|
+
this.layers = parsedMap.layers;
|
|
23
|
+
this.tilesets = parsedMap.tilesets;
|
|
24
|
+
this.blockedTiles = new Set(parsedMap.blockedTiles ?? []);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getTileByPosition(x: number, y: number) {
|
|
28
|
+
const tileX = Math.floor(x / this.tilewidth);
|
|
29
|
+
const tileY = Math.floor(y / this.tileheight);
|
|
30
|
+
return {
|
|
31
|
+
hasCollision: this.blockedTiles.has(`${tileX},${tileY}`),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { MapClass };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
import { applyTiledPointEvents, prepareTiledPhysicsData } from "./physics";
|
|
40
|
+
|
|
41
|
+
describe("prepareTiledPhysicsData", () => {
|
|
42
|
+
it("adds tiled collision hitboxes without duplicating them on repeated preparation", () => {
|
|
43
|
+
const mapData = {
|
|
44
|
+
parsedMap: {
|
|
45
|
+
width: 3,
|
|
46
|
+
height: 2,
|
|
47
|
+
tilewidth: 16,
|
|
48
|
+
tileheight: 20,
|
|
49
|
+
blockedTiles: ["1,0", "2,1"],
|
|
50
|
+
},
|
|
51
|
+
hitboxes: [{ id: "custom-hitbox", x: 4, y: 5, width: 6, height: 7 }],
|
|
52
|
+
};
|
|
53
|
+
const map: any = {};
|
|
54
|
+
const expectedHitboxes = [
|
|
55
|
+
{ id: "custom-hitbox", x: 4, y: 5, width: 6, height: 7 },
|
|
56
|
+
{ id: "__tiled_collision__:1,0", x: 16, y: 0, width: 16, height: 20 },
|
|
57
|
+
{ id: "__tiled_collision__:2,1", x: 32, y: 20, width: 16, height: 20 },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
prepareTiledPhysicsData(mapData, map);
|
|
61
|
+
|
|
62
|
+
expect(mapData.width).toBe(48);
|
|
63
|
+
expect(mapData.height).toBe(40);
|
|
64
|
+
expect(mapData.hitboxes).toEqual(expectedHitboxes);
|
|
65
|
+
|
|
66
|
+
prepareTiledPhysicsData(mapData, map);
|
|
67
|
+
|
|
68
|
+
expect(mapData.hitboxes).toEqual(expectedHitboxes);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("removes stale generated tiled hitboxes when blocked tiles change", () => {
|
|
72
|
+
const mapData = {
|
|
73
|
+
parsedMap: {
|
|
74
|
+
width: 2,
|
|
75
|
+
height: 1,
|
|
76
|
+
tilewidth: 32,
|
|
77
|
+
tileheight: 32,
|
|
78
|
+
blockedTiles: ["0,0"],
|
|
79
|
+
},
|
|
80
|
+
hitboxes: [],
|
|
81
|
+
};
|
|
82
|
+
const map: any = {};
|
|
83
|
+
|
|
84
|
+
prepareTiledPhysicsData(mapData, map);
|
|
85
|
+
expect(mapData.hitboxes).toEqual([
|
|
86
|
+
{ id: "__tiled_collision__:0,0", x: 0, y: 0, width: 32, height: 32 },
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
mapData.parsedMap.blockedTiles = [];
|
|
90
|
+
prepareTiledPhysicsData(mapData, map);
|
|
91
|
+
|
|
92
|
+
expect(mapData.hitboxes).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("adds v4 map helpers backed by Tiled data", () => {
|
|
96
|
+
const groundLayer = { name: "Ground", data: [1, 0, 2, 3] };
|
|
97
|
+
const mapData = {
|
|
98
|
+
parsedMap: {
|
|
99
|
+
width: 2,
|
|
100
|
+
height: 2,
|
|
101
|
+
tilewidth: 16,
|
|
102
|
+
tileheight: 20,
|
|
103
|
+
layers: [groundLayer],
|
|
104
|
+
tilesets: [{ firstgid: 1, name: "base" }],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const map: any = {};
|
|
108
|
+
|
|
109
|
+
prepareTiledPhysicsData(mapData, map);
|
|
110
|
+
|
|
111
|
+
expect(map.layers).toBe(mapData.parsedMap.layers);
|
|
112
|
+
expect(map.zTileHeight).toBe(20);
|
|
113
|
+
expect(map.getLayerByName("Ground")).toBe(groundLayer);
|
|
114
|
+
expect(map.getTileIndex(1, 1)).toBe(3);
|
|
115
|
+
expect(map.getTileOriginPosition(1, 1)).toEqual({ x: 16, y: 20 });
|
|
116
|
+
expect(map.getTileByPosition(16, 20)).toEqual({ hasCollision: false });
|
|
117
|
+
expect(map.getTileByIndex(2)).toMatchObject({ gid: 2, id: 2, index: 2, layer: groundLayer });
|
|
118
|
+
|
|
119
|
+
expect(map.setTile(1, 0, "Ground", { gid: 8 })).toEqual({ gid: 8 });
|
|
120
|
+
expect(groundLayer.data[1]).toBe(8);
|
|
121
|
+
|
|
122
|
+
expect(map.updateTileset({ firstgid: 1, name: "base", tilecount: 4 })).toEqual({
|
|
123
|
+
firstgid: 1,
|
|
124
|
+
name: "base",
|
|
125
|
+
tilecount: 4,
|
|
126
|
+
});
|
|
127
|
+
expect(map.tiled.tilesets).toEqual([{ firstgid: 1, name: "base", tilecount: 4 }]);
|
|
128
|
+
expect(mapData.parsedMap.tilesets).toEqual([{ firstgid: 1, name: "base", tilecount: 4 }]);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("tiled point positions", () => {
|
|
133
|
+
it("extracts named point objects as map positions", () => {
|
|
134
|
+
const mapData = {
|
|
135
|
+
parsedMap: {
|
|
136
|
+
width: 1,
|
|
137
|
+
height: 1,
|
|
138
|
+
objects: [
|
|
139
|
+
{ point: true, name: "start", x: 10, y: 20 },
|
|
140
|
+
{ point: true, name: "entrance", x: 30, y: 40 },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
prepareTiledPhysicsData(mapData, {});
|
|
146
|
+
|
|
147
|
+
expect(mapData.positions).toEqual({
|
|
148
|
+
start: { x: 10, y: 20 },
|
|
149
|
+
entrance: { x: 30, y: 40 },
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("uses class or type as a fallback for the special start position", () => {
|
|
154
|
+
const mapData = {
|
|
155
|
+
parsedMap: {
|
|
156
|
+
width: 1,
|
|
157
|
+
height: 1,
|
|
158
|
+
objects: [
|
|
159
|
+
{ point: true, class: "start", x: 10, y: 20 },
|
|
160
|
+
{ point: true, type: "start", x: 30, y: 40 },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
prepareTiledPhysicsData(mapData, {});
|
|
166
|
+
|
|
167
|
+
expect(mapData.positions).toEqual({
|
|
168
|
+
start: { x: 10, y: 20 },
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("keeps explicitly named start before class or type fallback", () => {
|
|
173
|
+
const mapData = {
|
|
174
|
+
parsedMap: {
|
|
175
|
+
width: 1,
|
|
176
|
+
height: 1,
|
|
177
|
+
objects: [
|
|
178
|
+
{ point: true, name: "start", x: 10, y: 20 },
|
|
179
|
+
{ point: true, class: "start", x: 30, y: 40 },
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
prepareTiledPhysicsData(mapData, {});
|
|
185
|
+
|
|
186
|
+
expect(mapData.positions).toEqual({
|
|
187
|
+
start: { x: 10, y: 20 },
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("applyTiledPointEvents", () => {
|
|
193
|
+
it("places direct object events by matching their name with a tiled point", () => {
|
|
194
|
+
const mapData = {
|
|
195
|
+
parsedMap: {
|
|
196
|
+
objects: [
|
|
197
|
+
{ point: true, name: "EV-1", x: 10, y: 20 },
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
events: [
|
|
201
|
+
{ name: "EV-1", onInit() {} },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
applyTiledPointEvents(mapData);
|
|
206
|
+
|
|
207
|
+
expect(mapData.events).toEqual([
|
|
208
|
+
{
|
|
209
|
+
event: { name: "EV-1", onInit: expect.any(Function) },
|
|
210
|
+
x: 10,
|
|
211
|
+
y: 20,
|
|
212
|
+
},
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("places wrapped object events by matching the wrapped event name", () => {
|
|
217
|
+
const mapData = {
|
|
218
|
+
parsedMap: {
|
|
219
|
+
objects: [
|
|
220
|
+
{ point: true, name: "EV-1", x: 10, y: 20 },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
events: [
|
|
224
|
+
{ id: "event-id", event: { name: "EV-1", onInit() {} } },
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
applyTiledPointEvents(mapData);
|
|
229
|
+
|
|
230
|
+
expect(mapData.events).toEqual([
|
|
231
|
+
{
|
|
232
|
+
id: "event-id",
|
|
233
|
+
event: { name: "EV-1", onInit: expect.any(Function) },
|
|
234
|
+
x: 10,
|
|
235
|
+
y: 20,
|
|
236
|
+
},
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("places class events decorated with EventData metadata", () => {
|
|
241
|
+
class NpcEvent {}
|
|
242
|
+
(NpcEvent as any)._name = "EV-1";
|
|
243
|
+
(NpcEvent as any).prototype._name = "EV-1";
|
|
244
|
+
|
|
245
|
+
const mapData = {
|
|
246
|
+
parsedMap: {
|
|
247
|
+
objects: [
|
|
248
|
+
{ point: true, name: "EV-1", x: 10, y: 20 },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
events: [
|
|
252
|
+
{ event: NpcEvent },
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
applyTiledPointEvents(mapData);
|
|
257
|
+
|
|
258
|
+
expect(mapData.events).toEqual([
|
|
259
|
+
{
|
|
260
|
+
event: NpcEvent,
|
|
261
|
+
x: 10,
|
|
262
|
+
y: 20,
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("does not override explicit event coordinates", () => {
|
|
268
|
+
const mapData = {
|
|
269
|
+
parsedMap: {
|
|
270
|
+
objects: [
|
|
271
|
+
{ point: true, name: "EV-1", x: 10, y: 20 },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
events: [
|
|
275
|
+
{ x: 50, y: 60, event: { name: "EV-1" } },
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
applyTiledPointEvents(mapData);
|
|
280
|
+
|
|
281
|
+
expect(mapData.events).toEqual([
|
|
282
|
+
{ x: 50, y: 60, event: { name: "EV-1" } },
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
});
|
package/src/physics.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { MapClass } from "@canvasengine/tiled";
|
|
2
|
+
import { applyTiledMapCompat } from "./compat";
|
|
3
|
+
|
|
4
|
+
type AnyMap = {
|
|
5
|
+
tiled?: MapClass;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type RectHitbox = {
|
|
9
|
+
id: string;
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const TILED_HITBOX_ID_PREFIX = "__tiled_collision__:";
|
|
17
|
+
const START_POSITION_NAME = "start";
|
|
18
|
+
|
|
19
|
+
export function prepareTiledPhysicsData(mapData: any, map: AnyMap): void {
|
|
20
|
+
if (!mapData?.parsedMap) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tiledMap = new MapClass(mapData.parsedMap);
|
|
25
|
+
map.tiled = tiledMap;
|
|
26
|
+
applyTiledMapCompat(map, mapData.parsedMap);
|
|
27
|
+
|
|
28
|
+
const tiledHitboxes = collectBlockedTileHitboxes(tiledMap);
|
|
29
|
+
mapData.hitboxes = mergeTiledHitboxes(mapData.hitboxes, tiledHitboxes);
|
|
30
|
+
mapData.width = tiledMap.widthPx;
|
|
31
|
+
mapData.height = tiledMap.heightPx;
|
|
32
|
+
mapData.positions = mergeTiledPositions(mapData.positions, collectTiledPointPositions(mapData.parsedMap.objects));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function applyTiledPointEvents(mapData: any): void {
|
|
36
|
+
const objects = mapData?.parsedMap?.objects;
|
|
37
|
+
if (!Array.isArray(objects) || !Array.isArray(mapData?.events)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const obj of objects) {
|
|
42
|
+
if (!obj?.point) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
mapData.events = mapData.events
|
|
47
|
+
.map((eventEntry: any) => {
|
|
48
|
+
if (
|
|
49
|
+
obj.name &&
|
|
50
|
+
eventEntry?.x === undefined &&
|
|
51
|
+
eventEntry?.y === undefined &&
|
|
52
|
+
getEventName(eventEntry) === obj.name
|
|
53
|
+
) {
|
|
54
|
+
const isWrappedEvent = eventEntry && typeof eventEntry === "object" && "event" in eventEntry;
|
|
55
|
+
return {
|
|
56
|
+
...(isWrappedEvent ? eventEntry : { event: eventEntry }),
|
|
57
|
+
x: obj.x,
|
|
58
|
+
y: obj.y,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return eventEntry;
|
|
62
|
+
})
|
|
63
|
+
.filter((eventEntry: any) => eventEntry !== null);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function collectTiledPointPositions(objects: any): Record<string, { x: number; y: number }> {
|
|
68
|
+
if (!Array.isArray(objects)) {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const positions: Record<string, { x: number; y: number }> = {};
|
|
73
|
+
|
|
74
|
+
for (const obj of objects) {
|
|
75
|
+
if (!obj?.point || typeof obj.x !== "number" || typeof obj.y !== "number") {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof obj.name === "string" && obj.name.length > 0) {
|
|
80
|
+
positions[obj.name] = { x: obj.x, y: obj.y };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
!positions[START_POSITION_NAME] &&
|
|
85
|
+
(obj.class === START_POSITION_NAME || obj.type === START_POSITION_NAME)
|
|
86
|
+
) {
|
|
87
|
+
positions[START_POSITION_NAME] = { x: obj.x, y: obj.y };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return positions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mergeTiledPositions(existingPositions: any, tiledPositions: Record<string, { x: number; y: number }>) {
|
|
95
|
+
return {
|
|
96
|
+
...(existingPositions && typeof existingPositions === "object" ? existingPositions : {}),
|
|
97
|
+
...tiledPositions,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getEventName(eventEntry: any): string | undefined {
|
|
102
|
+
const event = eventEntry?.event ?? eventEntry;
|
|
103
|
+
|
|
104
|
+
if (typeof event === "function") {
|
|
105
|
+
const staticEventName = (event as any)._name;
|
|
106
|
+
const prototypeEventName = (event as any).prototype?._name;
|
|
107
|
+
const staticName = (event as any).name;
|
|
108
|
+
const prototypeName = (event as any).prototype?.name;
|
|
109
|
+
if (typeof prototypeEventName === "string") {
|
|
110
|
+
return prototypeEventName;
|
|
111
|
+
}
|
|
112
|
+
if (typeof staticEventName === "string") {
|
|
113
|
+
return staticEventName;
|
|
114
|
+
}
|
|
115
|
+
if (typeof prototypeName === "string") {
|
|
116
|
+
return prototypeName;
|
|
117
|
+
}
|
|
118
|
+
if (typeof staticName === "string") {
|
|
119
|
+
return staticName;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof event?.name === "string") {
|
|
124
|
+
return event.name;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collectBlockedTileHitboxes(tiledMap: MapClass): RectHitbox[] {
|
|
131
|
+
const hitboxes: RectHitbox[] = [];
|
|
132
|
+
const mapWidth = tiledMap.width;
|
|
133
|
+
const mapHeight = tiledMap.height;
|
|
134
|
+
const tileWidth = tiledMap.tilewidth;
|
|
135
|
+
const tileHeight = tiledMap.tileheight;
|
|
136
|
+
|
|
137
|
+
for (let y = 0; y < mapHeight; y++) {
|
|
138
|
+
for (let x = 0; x < mapWidth; x++) {
|
|
139
|
+
const tileInfo = tiledMap.getTileByPosition(x * tileWidth, y * tileHeight, [0, 0], {
|
|
140
|
+
populateTiles: true,
|
|
141
|
+
});
|
|
142
|
+
if (tileInfo.hasCollision) {
|
|
143
|
+
hitboxes.push({
|
|
144
|
+
id: createTiledHitboxId(x, y),
|
|
145
|
+
x: x * tileWidth,
|
|
146
|
+
y: y * tileHeight,
|
|
147
|
+
width: tileWidth,
|
|
148
|
+
height: tileHeight,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return hitboxes;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mergeTiledHitboxes(existingHitboxes: any, tiledHitboxes: RectHitbox[]): any[] {
|
|
158
|
+
const preservedHitboxes = Array.isArray(existingHitboxes)
|
|
159
|
+
? existingHitboxes.filter((hitbox) => !isGeneratedTiledHitbox(hitbox))
|
|
160
|
+
: [];
|
|
161
|
+
|
|
162
|
+
return [...preservedHitboxes, ...tiledHitboxes];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isGeneratedTiledHitbox(hitbox: any): boolean {
|
|
166
|
+
return typeof hitbox?.id === "string" && hitbox.id.startsWith(TILED_HITBOX_ID_PREFIX);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createTiledHitboxId(x: number, y: number): string {
|
|
170
|
+
return `${TILED_HITBOX_ID_PREFIX}${x},${y}`;
|
|
171
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,173 +1,66 @@
|
|
|
1
1
|
import { RpgMap, RpgServer } from "@rpgjs/server";
|
|
2
2
|
import { MapClass } from "@canvasengine/tiled";
|
|
3
3
|
import { defineModule } from "@rpgjs/common";
|
|
4
|
+
import { applyTiledPointEvents, prepareTiledPhysicsData } from "./physics";
|
|
4
5
|
|
|
5
|
-
// Extend RpgMap interface to include tiled property
|
|
6
6
|
declare module "@rpgjs/server" {
|
|
7
7
|
interface RpgMap {
|
|
8
|
+
/**
|
|
9
|
+
* CanvasEngine Tiled map instance attached by `@rpgjs/tiledmap`.
|
|
10
|
+
*/
|
|
8
11
|
tiled?: MapClass;
|
|
12
|
+
/**
|
|
13
|
+
* Tiled layers from the parsed map.
|
|
14
|
+
*/
|
|
15
|
+
layers?: any[];
|
|
16
|
+
/**
|
|
17
|
+
* Height used by tile depth calculations. Defaults to the Tiled tile height.
|
|
18
|
+
*/
|
|
19
|
+
zTileHeight?: number;
|
|
20
|
+
/**
|
|
21
|
+
* v4 compatibility helper. Returns a Tiled layer by its name.
|
|
22
|
+
*/
|
|
23
|
+
getLayerByName?(name: string): any;
|
|
24
|
+
/**
|
|
25
|
+
* v4 compatibility helper. Returns the one-dimensional tile index for tile coordinates.
|
|
26
|
+
*/
|
|
27
|
+
getTileIndex?(x: number, y: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* v4 compatibility helper. Returns the pixel origin of a tile coordinate.
|
|
30
|
+
*/
|
|
31
|
+
getTileOriginPosition?(x: number, y: number): { x: number; y: number };
|
|
32
|
+
/**
|
|
33
|
+
* v4 compatibility helper. Returns Tiled tile information from pixel coordinates.
|
|
34
|
+
*/
|
|
35
|
+
getTileByPosition?(x: number, y: number, z?: number): any;
|
|
36
|
+
/**
|
|
37
|
+
* v4 compatibility helper. Returns Tiled tile information from a one-dimensional index.
|
|
38
|
+
*/
|
|
39
|
+
getTileByIndex?(tileIndex: number): any;
|
|
40
|
+
/**
|
|
41
|
+
* v4 compatibility helper. Updates a tile in a Tiled tile layer.
|
|
42
|
+
*/
|
|
43
|
+
setTile?(x: number, y: number, layer: string | number, tileInfo: any): any;
|
|
44
|
+
/**
|
|
45
|
+
* v4 compatibility helper. Updates the parsed Tiled tileset list.
|
|
46
|
+
*/
|
|
47
|
+
updateTileset?(tileset: any): any;
|
|
9
48
|
}
|
|
10
49
|
}
|
|
11
50
|
|
|
12
|
-
/**
|
|
13
|
-
* Interface for an RpgMap extended with Tiled functionality
|
|
14
|
-
*
|
|
15
|
-
* @description This interface combines RpgMap with MapClass to enable
|
|
16
|
-
* the use of Tiled methods on RPG maps
|
|
17
|
-
*/
|
|
18
51
|
export interface RpgTiledMap extends RpgMap {
|
|
19
52
|
tiled: MapClass;
|
|
20
53
|
}
|
|
21
54
|
|
|
22
|
-
/**
|
|
23
|
-
* Tiled Module for RPGJS
|
|
24
|
-
*
|
|
25
|
-
* @description This module extends RPGJS maps with Tiled functionality,
|
|
26
|
-
* allowing TMX map parsing and automatic hitbox creation
|
|
27
|
-
* based on collisions defined in Tiled
|
|
28
|
-
*
|
|
29
|
-
* ## Features
|
|
30
|
-
*
|
|
31
|
-
* - **Automatic parsing**: Parses TMX files from Tiled Map Editor
|
|
32
|
-
* - **Collision detection**: Scans all tiles to detect collisions
|
|
33
|
-
* - **Hitbox creation**: Automatically generates hitboxes for each collision tile
|
|
34
|
-
* - **RpgMap extension**: Adds the `tiled` property to all RpgMap instances
|
|
35
|
-
*
|
|
36
|
-
* ## Usage
|
|
37
|
-
*
|
|
38
|
-
* Once this module is activated, you can use Tiled methods on your maps:
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```ts
|
|
42
|
-
* // In a map class
|
|
43
|
-
* class MyMap extends RpgMap {
|
|
44
|
-
* onLoad() {
|
|
45
|
-
* // Access Tiled functionality
|
|
46
|
-
* const tiles = this.tiled.getTileByPosition(100, 100);
|
|
47
|
-
*
|
|
48
|
-
* if (tiles.hasCollision) {
|
|
49
|
-
* console.log('This position has a collision');
|
|
50
|
-
* }
|
|
51
|
-
*
|
|
52
|
-
* // Iterate through all tiles by index
|
|
53
|
-
* for (let i = 0; i < this.tiled.width * this.tiled.height; i++) {
|
|
54
|
-
* const tileInfo = this.tiled.getTileByIndex(i);
|
|
55
|
-
* if (tileInfo.hasCollision) {
|
|
56
|
-
* console.log(`Tile ${i} has collision`);
|
|
57
|
-
* }
|
|
58
|
-
* }
|
|
59
|
-
*
|
|
60
|
-
* // Get information about a specific layer
|
|
61
|
-
* const layer = this.tiled.getLayerByName('Collision');
|
|
62
|
-
* if (layer) {
|
|
63
|
-
* console.log('Collision layer found:', layer);
|
|
64
|
-
* }
|
|
65
|
-
* }
|
|
66
|
-
* }
|
|
67
|
-
* ```
|
|
68
|
-
*/
|
|
69
55
|
export default defineModule<RpgServer>({
|
|
70
56
|
map: {
|
|
71
|
-
/**
|
|
72
|
-
* Hook called before map update
|
|
73
|
-
*
|
|
74
|
-
* @description Parses Tiled data and creates collision hitboxes
|
|
75
|
-
* automatically by iterating through all tiles on the map.
|
|
76
|
-
*
|
|
77
|
-
* This method:
|
|
78
|
-
* 1. Parses TMX data with TiledParser
|
|
79
|
-
* 2. Creates a MapClass instance with parsed data
|
|
80
|
-
* 3. Attaches the Tiled instance to the RpgMap
|
|
81
|
-
* 4. Scans all tiles to detect collisions
|
|
82
|
-
* 5. Automatically creates hitboxes for each collision tile
|
|
83
|
-
*
|
|
84
|
-
* @param mapData - Map data containing TMX information
|
|
85
|
-
* @param map - RpgMap instance to extend
|
|
86
|
-
* @returns The modified map instance with tiled property
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* ```ts
|
|
90
|
-
* // Created hitboxes will have this structure:
|
|
91
|
-
* {
|
|
92
|
-
* id: 'collision_x_y', // Unique identifier
|
|
93
|
-
* x: x * tileWidth, // X position in pixels
|
|
94
|
-
* y: y * tileHeight, // Y position in pixels
|
|
95
|
-
* width: tileWidth, // Tile width
|
|
96
|
-
* height: tileHeight, // Tile height
|
|
97
|
-
* properties: {
|
|
98
|
-
* type: 'collision', // Hitbox type
|
|
99
|
-
* tileX: x, // X position in tiles
|
|
100
|
-
* tileY: y, // Y position in tiles
|
|
101
|
-
* tileIndex: tileIndex // Tile index
|
|
102
|
-
* }
|
|
103
|
-
* }
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
57
|
onBeforeUpdate<T = RpgMap>(mapData: any, map: T): T {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Attach Tiled instance to the map
|
|
110
|
-
(map as any).tiled = tiledMap;
|
|
111
|
-
|
|
112
|
-
// Initialize hitboxes array
|
|
113
|
-
mapData.hitboxes = mapData.hitboxes || [];
|
|
114
|
-
mapData.width = tiledMap.widthPx;
|
|
115
|
-
mapData.height = tiledMap.heightPx;
|
|
116
|
-
|
|
117
|
-
// Iterate through all map tiles to detect collisions
|
|
118
|
-
const mapWidth = tiledMap.width;
|
|
119
|
-
const mapHeight = tiledMap.height;
|
|
120
|
-
const tileWidth = tiledMap.tilewidth;
|
|
121
|
-
const tileHeight = tiledMap.tileheight;
|
|
122
|
-
|
|
123
|
-
// Iterate through each tile on the map
|
|
124
|
-
for (let y = 0; y < mapHeight; y++) {
|
|
125
|
-
for (let x = 0; x < mapWidth; x++) {
|
|
126
|
-
// Use getTileByPosition which is simpler and handles pixel coordinates directly
|
|
127
|
-
const pixelX = x * tileWidth;
|
|
128
|
-
const pixelY = y * tileHeight;
|
|
129
|
-
const tileInfo = tiledMap.getTileByPosition(pixelX, pixelY, [0, 0], {
|
|
130
|
-
populateTiles: true,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// If tile has collision, create a hitbox
|
|
134
|
-
if (tileInfo.hasCollision) {
|
|
135
|
-
const hitbox = {
|
|
136
|
-
id: `collision_${x}_${y}`,
|
|
137
|
-
x: pixelX,
|
|
138
|
-
y: pixelY,
|
|
139
|
-
width: tileWidth,
|
|
140
|
-
height: tileHeight,
|
|
141
|
-
properties: {
|
|
142
|
-
type: "collision",
|
|
143
|
-
tileX: x,
|
|
144
|
-
tileY: y,
|
|
145
|
-
tileIndex: tileInfo.tileIndex,
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
mapData.hitboxes.push(hitbox);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
for (let obj of mapData.parsedMap.objects) {
|
|
155
|
-
if (obj.point) {
|
|
156
|
-
mapData.events = mapData.events
|
|
157
|
-
.map((e) => {
|
|
158
|
-
if (e.name === obj.name) {
|
|
159
|
-
return {
|
|
160
|
-
event: e,
|
|
161
|
-
x: obj.x,
|
|
162
|
-
y: obj.y,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
return e;
|
|
166
|
-
})
|
|
167
|
-
.filter((e) => e !== null);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
58
|
+
prepareTiledPhysicsData(mapData, map as any);
|
|
59
|
+
applyTiledPointEvents(mapData);
|
|
170
60
|
return map;
|
|
171
61
|
},
|
|
62
|
+
onPhysicsInit(map: any, context: { mapData: any }) {
|
|
63
|
+
prepareTiledPhysicsData(context?.mapData, map);
|
|
64
|
+
},
|
|
172
65
|
},
|
|
173
66
|
});
|
package/src/tiled.ce
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
<script>
|
|
6
6
|
import { EventLayerComponent } from "@rpgjs/client";
|
|
7
7
|
import { TiledMap } from "@canvasengine/presets";
|
|
8
|
-
import { signal } from "canvasengine";
|
|
8
|
+
import { signal, effect } from "canvasengine";
|
|
9
9
|
|
|
10
10
|
const { data, params } = defineProps()
|
|
11
11
|
|
|
12
12
|
const map = signal(data())
|
|
13
13
|
const basePath = signal(params().basePath)
|
|
14
|
+
|
|
15
|
+
effect(() => {
|
|
16
|
+
map.set(data())
|
|
17
|
+
})
|
|
14
18
|
</script>
|
package/dist/client.d.ts
DELETED