@rpgjs/tiledmap 5.0.0-alpha.9 → 5.0.0-beta.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/tiledmap",
3
- "version": "5.0.0-alpha.9",
3
+ "version": "5.0.0-beta.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "exports": {
@@ -22,24 +22,24 @@
22
22
  "license": "MIT",
23
23
  "description": "RPGJS is a framework for creating RPG/MMORPG games",
24
24
  "peerDependencies": {
25
- "@canvasengine/presets": "2.0.0-beta.25",
26
- "@rpgjs/client": "5.0.0-alpha.9",
27
- "@rpgjs/common": "5.0.0-alpha.9",
28
- "@rpgjs/server": "5.0.0-alpha.9",
29
- "@rpgjs/vite": "5.0.0-alpha.9",
30
- "canvasengine": "2.0.0-beta.25"
25
+ "@canvasengine/presets": "*",
26
+ "@rpgjs/client": "5.0.0-beta.1",
27
+ "@rpgjs/common": "5.0.0-beta.1",
28
+ "@rpgjs/server": "5.0.0-beta.1",
29
+ "@rpgjs/vite": "5.0.0-beta.1",
30
+ "canvasengine": "*"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
35
  "devDependencies": {
36
- "@canvasengine/compiler": "2.0.0-beta.21",
37
- "vite": "^6.2.5",
38
- "vite-plugin-dts": "^4.5.3"
36
+ "@canvasengine/compiler": "^2.0.0-beta.44",
37
+ "vite": "^7.3.0",
38
+ "vite-plugin-dts": "^4.5.4"
39
39
  },
40
40
  "type": "module",
41
41
  "dependencies": {
42
- "@canvasengine/tiled": "2.0.0-beta.25"
42
+ "@canvasengine/tiled": "^2.0.0-beta.44"
43
43
  },
44
44
  "scripts": {
45
45
  "dev": "vite build --watch",
package/src/client.ts CHANGED
@@ -1,6 +1,12 @@
1
- import { RpgClient, RpgClientEngine } from "@rpgjs/client";
1
+ import { RpgClient } from "@rpgjs/client";
2
2
  import { defineModule } from "@rpgjs/common";
3
+ import { prepareTiledPhysicsData } from "./physics";
3
4
 
4
5
  export default defineModule<RpgClient>({
5
- effects: []
6
- })
6
+ componentAnimations: [],
7
+ sceneMap: {
8
+ onPhysicsInit(map: any, context: { mapData: any }) {
9
+ prepareTiledPhysicsData(context?.mapData, map);
10
+ },
11
+ },
12
+ });
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { createModule } from "@rpgjs/common";
4
4
  import { provideLoadMap } from "@rpgjs/client";
5
5
  import { TiledParser } from "@canvasengine/tiled";
6
6
  import Tiled from "./tiled.ce";
7
+ import { prepareTiledPhysicsData } from "./physics";
7
8
 
8
9
  export function provideTiledMap(options: {
9
10
  basePath: string;
@@ -35,7 +36,7 @@ export function provideTiledMap(options: {
35
36
  }
36
37
  parsedMap.tilesets = tilesets;
37
38
 
38
- const obj = {
39
+ const obj: any = {
39
40
  data: mapData,
40
41
  component: Tiled,
41
42
  parsedMap,
@@ -45,6 +46,9 @@ export function provideTiledMap(options: {
45
46
  },
46
47
  };
47
48
 
49
+ // Populate dimensions and static hitboxes before the first loadPhysic() call.
50
+ prepareTiledPhysicsData(obj, obj);
51
+
48
52
  if (options.onLoadMap) {
49
53
  await options.onLoadMap(map);
50
54
  }
@@ -0,0 +1,90 @@
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
+ private blockedTiles: Set<string>;
12
+
13
+ constructor(parsedMap: any) {
14
+ this.width = parsedMap.width ?? 0;
15
+ this.height = parsedMap.height ?? 0;
16
+ this.tilewidth = parsedMap.tilewidth ?? 32;
17
+ this.tileheight = parsedMap.tileheight ?? 32;
18
+ this.widthPx = this.width * this.tilewidth;
19
+ this.heightPx = this.height * this.tileheight;
20
+ this.blockedTiles = new Set(parsedMap.blockedTiles ?? []);
21
+ }
22
+
23
+ getTileByPosition(x: number, y: number) {
24
+ const tileX = Math.floor(x / this.tilewidth);
25
+ const tileY = Math.floor(y / this.tileheight);
26
+ return {
27
+ hasCollision: this.blockedTiles.has(`${tileX},${tileY}`),
28
+ };
29
+ }
30
+ }
31
+
32
+ return { MapClass };
33
+ });
34
+
35
+ import { prepareTiledPhysicsData } from "./physics";
36
+
37
+ describe("prepareTiledPhysicsData", () => {
38
+ it("adds tiled collision hitboxes without duplicating them on repeated preparation", () => {
39
+ const mapData = {
40
+ parsedMap: {
41
+ width: 3,
42
+ height: 2,
43
+ tilewidth: 16,
44
+ tileheight: 20,
45
+ blockedTiles: ["1,0", "2,1"],
46
+ },
47
+ hitboxes: [{ id: "custom-hitbox", x: 4, y: 5, width: 6, height: 7 }],
48
+ };
49
+ const map: any = {};
50
+ const expectedHitboxes = [
51
+ { id: "custom-hitbox", x: 4, y: 5, width: 6, height: 7 },
52
+ { id: "__tiled_collision__:1,0", x: 16, y: 0, width: 16, height: 20 },
53
+ { id: "__tiled_collision__:2,1", x: 32, y: 20, width: 16, height: 20 },
54
+ ];
55
+
56
+ prepareTiledPhysicsData(mapData, map);
57
+
58
+ expect(mapData.width).toBe(48);
59
+ expect(mapData.height).toBe(40);
60
+ expect(mapData.hitboxes).toEqual(expectedHitboxes);
61
+
62
+ prepareTiledPhysicsData(mapData, map);
63
+
64
+ expect(mapData.hitboxes).toEqual(expectedHitboxes);
65
+ });
66
+
67
+ it("removes stale generated tiled hitboxes when blocked tiles change", () => {
68
+ const mapData = {
69
+ parsedMap: {
70
+ width: 2,
71
+ height: 1,
72
+ tilewidth: 32,
73
+ tileheight: 32,
74
+ blockedTiles: ["0,0"],
75
+ },
76
+ hitboxes: [],
77
+ };
78
+ const map: any = {};
79
+
80
+ prepareTiledPhysicsData(mapData, map);
81
+ expect(mapData.hitboxes).toEqual([
82
+ { id: "__tiled_collision__:0,0", x: 0, y: 0, width: 32, height: 32 },
83
+ ]);
84
+
85
+ mapData.parsedMap.blockedTiles = [];
86
+ prepareTiledPhysicsData(mapData, map);
87
+
88
+ expect(mapData.hitboxes).toEqual([]);
89
+ });
90
+ });
package/src/physics.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { MapClass } from "@canvasengine/tiled";
2
+
3
+ type AnyMap = {
4
+ tiled?: MapClass;
5
+ };
6
+
7
+ type RectHitbox = {
8
+ id: string;
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ };
14
+
15
+ const TILED_HITBOX_ID_PREFIX = "__tiled_collision__:";
16
+
17
+ export function prepareTiledPhysicsData(mapData: any, map: AnyMap): void {
18
+ if (!mapData?.parsedMap) {
19
+ return;
20
+ }
21
+
22
+ const tiledMap = new MapClass(mapData.parsedMap);
23
+ map.tiled = tiledMap;
24
+
25
+ const tiledHitboxes = collectBlockedTileHitboxes(tiledMap);
26
+ mapData.hitboxes = mergeTiledHitboxes(mapData.hitboxes, tiledHitboxes);
27
+ mapData.width = tiledMap.widthPx;
28
+ mapData.height = tiledMap.heightPx;
29
+ }
30
+
31
+ export function applyTiledPointEvents(mapData: any): void {
32
+ const objects = mapData?.parsedMap?.objects;
33
+ if (!Array.isArray(objects) || !Array.isArray(mapData?.events)) {
34
+ return;
35
+ }
36
+
37
+ for (const obj of objects) {
38
+ if (!obj?.point) {
39
+ continue;
40
+ }
41
+
42
+ mapData.events = mapData.events
43
+ .map((eventEntry: any) => {
44
+ if (eventEntry?.name === obj.name) {
45
+ return {
46
+ event: eventEntry,
47
+ x: obj.x,
48
+ y: obj.y,
49
+ };
50
+ }
51
+ return eventEntry;
52
+ })
53
+ .filter((eventEntry: any) => eventEntry !== null);
54
+ }
55
+ }
56
+
57
+ function collectBlockedTileHitboxes(tiledMap: MapClass): RectHitbox[] {
58
+ const hitboxes: RectHitbox[] = [];
59
+ const mapWidth = tiledMap.width;
60
+ const mapHeight = tiledMap.height;
61
+ const tileWidth = tiledMap.tilewidth;
62
+ const tileHeight = tiledMap.tileheight;
63
+
64
+ for (let y = 0; y < mapHeight; y++) {
65
+ for (let x = 0; x < mapWidth; x++) {
66
+ const tileInfo = tiledMap.getTileByPosition(x * tileWidth, y * tileHeight, [0, 0], {
67
+ populateTiles: true,
68
+ });
69
+ if (tileInfo.hasCollision) {
70
+ hitboxes.push({
71
+ id: createTiledHitboxId(x, y),
72
+ x: x * tileWidth,
73
+ y: y * tileHeight,
74
+ width: tileWidth,
75
+ height: tileHeight,
76
+ });
77
+ }
78
+ }
79
+ }
80
+
81
+ return hitboxes;
82
+ }
83
+
84
+ function mergeTiledHitboxes(existingHitboxes: any, tiledHitboxes: RectHitbox[]): any[] {
85
+ const preservedHitboxes = Array.isArray(existingHitboxes)
86
+ ? existingHitboxes.filter((hitbox) => !isGeneratedTiledHitbox(hitbox))
87
+ : [];
88
+
89
+ return [...preservedHitboxes, ...tiledHitboxes];
90
+ }
91
+
92
+ function isGeneratedTiledHitbox(hitbox: any): boolean {
93
+ return typeof hitbox?.id === "string" && hitbox.id.startsWith(TILED_HITBOX_ID_PREFIX);
94
+ }
95
+
96
+ function createTiledHitboxId(x: number, y: number): string {
97
+ return `${TILED_HITBOX_ID_PREFIX}${x},${y}`;
98
+ }
package/src/server.ts CHANGED
@@ -1,173 +1,27 @@
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
8
  tiled?: MapClass;
9
9
  }
10
10
  }
11
11
 
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
12
  export interface RpgTiledMap extends RpgMap {
19
13
  tiled: MapClass;
20
14
  }
21
15
 
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
16
  export default defineModule<RpgServer>({
70
17
  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
18
  onBeforeUpdate<T = RpgMap>(mapData: any, map: T): T {
107
- const tiledMap = new MapClass(mapData.parsedMap);
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
- }
19
+ prepareTiledPhysicsData(mapData, map as any);
20
+ applyTiledPointEvents(mapData);
170
21
  return map;
171
22
  },
23
+ onPhysicsInit(map: any, context: { mapData: any }) {
24
+ prepareTiledPhysicsData(context?.mapData, map);
25
+ },
172
26
  },
173
27
  });
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>