From 1b0102e2ba72206bc2320a7f3a01ee2eb652ee5d Mon Sep 17 00:00:00 2001 From: Leng Niu <344562942+vtxe02023274@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:17:57 +0800 Subject: [PATCH 1/2] Project isometric collision shapes with the map's tile ratio --- packages/melonjs/CHANGELOG.md | 1 + packages/melonjs/src/level/tiled/TMXObject.js | 16 +++- packages/melonjs/tests/tmxobject.spec.js | 89 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index aa7472a25a..eb6b43c693 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -28,6 +28,7 @@ - Tiled: a tile object from an image collection is drawn on its own box when it is resized or flipped in Tiled. A resized one was drawn above and to the left of it, and a flipped one a whole tile width to the right - Tiled: tile objects are placed by their tileset's object alignment, so a tileset that anchors its objects at the top left, the center or any other point places them there. They were always anchored at the bottom left, or the bottom center on isometric maps - Tiled: a rotated tile object turns about the point its tileset aligns, as in Tiled, which is its bottom left (bottom center on isometric maps) unless the tileset sets another. It turned about another point, so it was drawn and collided away from where Tiled shows it +- Tiled: the collision shapes of rectangle, polygon and polyline objects on isometric maps are projected with the map's tile ratio, as Tiled draws them. They were projected as if the tiles were 2:1, so on a map with wider or narrower tiles they were squeezed or stretched across and no longer lined up with the walls they outline - Tiled: a map with an animated tile object loads, and the object plays its animation. One from a tileset image threw "string parameters for addAnimation are not allowed for standard spritesheet based Texture", and one from an image collection threw a TypeError - Tiled: in a tile layer, tiles from an image collection whose images are not all the same height each sit on the bottom of their cell, as in Tiled. Every tile was placed as if it were as tall as the tallest image of the collection - Tiled: a collision object whose shape reaches left of or above its position, like every rectangle on an isometric map, is found by collision checks, `queryAABB` and `raycast` wherever its shape is. Parts of it were missed, so a character could walk through walls; such an object's `pos` is now the top left corner of its shape diff --git a/packages/melonjs/src/level/tiled/TMXObject.js b/packages/melonjs/src/level/tiled/TMXObject.js index 035c7252bf..7e8427ed69 100644 --- a/packages/melonjs/src/level/tiled/TMXObject.js +++ b/packages/melonjs/src/level/tiled/TMXObject.js @@ -82,6 +82,12 @@ export default class TMXObject { */ #properties = Object.create(null); + /** + * how much wider than 2:1 the map's tiles are, see parseTMXShapes + * @type {number} + */ + #isoRatio = 1; + /** * @param {TMXTileMap} map - the map the object belongs to * @param {object} settings - the object as parsed from the map @@ -196,6 +202,10 @@ export default class TMXObject { */ this.orientation = map.orientation; + if (map.tilewidth > 0 && map.tileheight > 0) { + this.#isoRatio = map.tilewidth / (2 * map.tileheight); + } + /** * the collision shapes defined for this object * @type {object[]} @@ -436,11 +446,15 @@ export default class TMXObject { } // Apply isometric projection, except to a tile object: Tiled draws - // it upright, so its box is already in screen space + // it upright, so its box is already in screen space. toIso() is the + // projection of 2:1 tiles, Tiled uses the map's tile ratio if (this.orientation === "isometric" && this.gid === null) { for (let i = 0; i < shapes.length; i++) { if (typeof shapes[i].toIso === "function") { shapes[i].toIso(); + if (this.#isoRatio !== 1) { + shapes[i].scale(this.#isoRatio, 1); + } } } } diff --git a/packages/melonjs/tests/tmxobject.spec.js b/packages/melonjs/tests/tmxobject.spec.js index 3ec9b5a07e..f39215db95 100644 --- a/packages/melonjs/tests/tmxobject.spec.js +++ b/packages/melonjs/tests/tmxobject.spec.js @@ -352,6 +352,95 @@ describe("TMXObject", () => { expect(pts[1].x).toBeCloseTo(0, 0); expect(pts[1].y).toBeCloseTo(100, 0); }); + + describe("on isometric maps", () => { + // where Tiled draws a point (x, y) of an object relative to the + // object position: (x - y) * tilewidth / (2 * tileheight), (x + y) / 2 + const project = (tw, th, x, y) => { + return [((x - y) * tw) / (2 * th), (x + y) / 2]; + }; + const isoMap = (tilewidth, tileheight) => { + return { ...mockMap("isometric"), tilewidth, tileheight }; + }; + const expectPoints = (shape, expected) => { + const pts = shape.points.map((p) => { + return [p.x + shape.pos.x, p.y + shape.pos.y]; + }); + expect(pts.length).toEqual(expected.length); + for (let i = 0; i < pts.length; i++) { + expect(pts[i][0]).toBeCloseTo(expected[i][0], 6); + expect(pts[i][1]).toBeCloseTo(expected[i][1], 6); + } + }; + + for (const [tw, th] of [ + [64, 32], + [128, 32], + [186, 108], + [32, 32], + ]) { + it(`should project a rectangle with ${tw}x${th} tiles`, () => { + const obj = new TMXObject( + isoMap(tw, th), + { name: "", x: 0, y: 0, width: 96, height: 48 }, + 0, + ); + expectPoints(obj.shapes[0], [ + project(tw, th, 0, 0), + project(tw, th, 96, 0), + project(tw, th, 96, 48), + project(tw, th, 0, 48), + ]); + }); + + it(`should project a polygon with ${tw}x${th} tiles`, () => { + const obj = new TMXObject( + isoMap(tw, th), + { + name: "", + x: 0, + y: 0, + polygon: [ + { x: 0, y: 0 }, + { x: 60, y: 10 }, + { x: 20, y: 70 }, + ], + }, + 0, + ); + expectPoints(obj.shapes[0], [ + project(tw, th, 0, 0), + project(tw, th, 60, 10), + project(tw, th, 20, 70), + ]); + }); + + it(`should project polyline segments with ${tw}x${th} tiles`, () => { + const obj = new TMXObject( + isoMap(tw, th), + { + name: "", + x: 0, + y: 0, + polyline: [ + { x: 0, y: 0 }, + { x: 50, y: 0 }, + { x: 50, y: 40 }, + ], + }, + 0, + ); + expectPoints(obj.shapes[0], [ + project(tw, th, 0, 0), + project(tw, th, 50, 0), + ]); + expectPoints(obj.shapes[1], [ + project(tw, th, 50, 0), + project(tw, th, 50, 40), + ]); + }); + } + }); }); describe("basic properties", () => { From 175056b722b4d30b7b6ddb85881142d503fadb4c Mon Sep 17 00:00:00 2001 From: Olivier Biot Date: Sun, 25 Oct 2026 18:22:07 +0800 Subject: [PATCH 2/2] Changelog: thank the contributor Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx --- packages/melonjs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index eb6b43c693..ccb2c14a39 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -28,7 +28,7 @@ - Tiled: a tile object from an image collection is drawn on its own box when it is resized or flipped in Tiled. A resized one was drawn above and to the left of it, and a flipped one a whole tile width to the right - Tiled: tile objects are placed by their tileset's object alignment, so a tileset that anchors its objects at the top left, the center or any other point places them there. They were always anchored at the bottom left, or the bottom center on isometric maps - Tiled: a rotated tile object turns about the point its tileset aligns, as in Tiled, which is its bottom left (bottom center on isometric maps) unless the tileset sets another. It turned about another point, so it was drawn and collided away from where Tiled shows it -- Tiled: the collision shapes of rectangle, polygon and polyline objects on isometric maps are projected with the map's tile ratio, as Tiled draws them. They were projected as if the tiles were 2:1, so on a map with wider or narrower tiles they were squeezed or stretched across and no longer lined up with the walls they outline +- Tiled: the collision shapes of rectangle, polygon and polyline objects on isometric maps are projected with the map's tile ratio, as Tiled draws them. They were projected as if the tiles were 2:1, so on a map with wider or narrower tiles they were squeezed or stretched across and no longer lined up with the walls they outline (thanks @vtxe02023274) - Tiled: a map with an animated tile object loads, and the object plays its animation. One from a tileset image threw "string parameters for addAnimation are not allowed for standard spritesheet based Texture", and one from an image collection threw a TypeError - Tiled: in a tile layer, tiles from an image collection whose images are not all the same height each sit on the bottom of their cell, as in Tiled. Every tile was placed as if it were as tall as the tallest image of the collection - Tiled: a collision object whose shape reaches left of or above its position, like every rectangle on an isometric map, is found by collision checks, `queryAABB` and `raycast` wherever its shape is. Parts of it were missed, so a character could walk through walls; such an object's `pos` is now the top left corner of its shape