diff --git a/package.json b/package.json index 67ec8f08..a2318614 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@playwright/test": "1.40.1", "@types/jasmine": "5.1.4", "@types/json-diff": "1.0.3", + "@types/jsdom": "21.1.6", "@types/node": "20.10.6", "@types/pako": "1.0.7", "@types/webpack-env": "1.18.4", @@ -77,6 +78,9 @@ "peerDependencies": { "excalibur": "~0.28.5" }, + "optionalDependencies": { + "jsdom": "^23.2.0" + }, "overrides": { "webpack-dev-server": { "webpack-dev-middleware": "7.0.0" diff --git a/src/parser/tiled-parser.ts b/src/parser/tiled-parser.ts index de53ace2..eb49dff3 100644 --- a/src/parser/tiled-parser.ts +++ b/src/parser/tiled-parser.ts @@ -1,4 +1,6 @@ import { z } from 'zod'; +import * as jsdom from 'jsdom'; + const TiledIntProperty = z.object({ name: z.string(), type: z.literal('int'), @@ -497,6 +499,32 @@ export class TiledParser { } } + /** + * Takes an xml string and uses an available parser (DOMParser in browser or JSDOM in Node.js) + * to produce a DOM object compatible with at least DOM Level 3. + * @param xml + * @returns + */ + _parseToDocument(xml: string): Document { + if (typeof DOMParser !== 'undefined') { + const domParser = new DOMParser(); + return domParser.parseFromString(xml, 'application/xml'); + } + + try { + const { JSDOM } = require('jsdom'); + const dom = new JSDOM(xml, { + contentType: 'application/xml', + encoding: 'utf-8', + }); + return dom.window.document as Document; + } catch (e) { /* ignored */ } + + const error = new Error('Could not find DOM parser'); + console.error(error.message, error); + throw error; + } + parseObject(objectNode: Element, strict = true): TiledObject { const object: any = {}; object.type = ''; @@ -874,8 +902,7 @@ export class TiledParser { } parseExternalTemplate(txXml: string, strict = true): TiledTemplate { - const domParser = new DOMParser(); - const doc = domParser.parseFromString(txXml, 'application/xml'); + const doc = this._parseToDocument(txXml); const templateElement = doc.querySelector('template') as Element; const template: any = {}; template.type = 'template'; @@ -905,8 +932,7 @@ export class TiledParser { * @param tsxXml */ parseExternalTileset(tsxXml: string, strict = true): TiledTilesetFile { - const domParser = new DOMParser(); - const doc = domParser.parseFromString(tsxXml, 'application/xml'); + const doc = this._parseToDocument(tsxXml); const tilesetElement = doc.querySelector('tileset') as Element; const tileset = this.parseTileset(tilesetElement, strict); @@ -933,9 +959,7 @@ export class TiledParser { * @returns */ parse(tmxXml: string, strict = true): TiledMap { - const domParser = new DOMParser(); - const doc = domParser.parseFromString(tmxXml, 'application/xml'); - + const doc = this._parseToDocument(tmxXml); const mapElement = doc.querySelector('map') as Element; const tiledMap: any = {}; diff --git a/src/resource/object-layer.ts b/src/resource/object-layer.ts index 315e0318..6f7ad553 100644 --- a/src/resource/object-layer.ts +++ b/src/resource/object-layer.ts @@ -96,6 +96,7 @@ export class ObjectLayer implements Layer { } _actorFromObject(object: PluginObject, newActor: Actor, tileset?: Tileset): void { + const headless = this.resource.headless; const hasTint = !!this.tiledObjectLayer.tintcolor; const tint = this.tiledObjectLayer.tintcolor ? Color.fromHex(this.tiledObjectLayer.tintcolor) : Color.White; @@ -109,26 +110,28 @@ export class ObjectLayer implements Layer { const scaleY = (object.tiledObject.width ?? this.resource.map.tilewidth) / this.resource.map.tilewidth; const scale = vec(scaleX, scaleY); - // need to clone because we are modify sprite properties, sprites are shared by default - const sprite = tileset.getSpriteForGid(object.gid).clone(); - sprite.destSize.width = object.tiledObject.width ?? sprite.width; - sprite.destSize.height = object.tiledObject.height ?? sprite.height; - if (hasTint) { - sprite.tint = tint; - } - - newActor.graphics.use(sprite); - newActor.graphics.offset = tileset.tileOffset; - - const animation = tileset.getAnimationForGid(object.gid); - if (animation) { - const animationScaled = animation.clone(); - animationScaled.scale = scale; + if (!headless) { + // need to clone because we are modify sprite properties, sprites are shared by default + const sprite = tileset.getSpriteForGid(object.gid).clone(); + sprite.destSize.width = object.tiledObject.width ?? sprite.width; + sprite.destSize.height = object.tiledObject.height ?? sprite.height; if (hasTint) { - animationScaled.tint = tint; + sprite.tint = tint; } - newActor.graphics.use(animationScaled); + + newActor.graphics.use(sprite); newActor.graphics.offset = tileset.tileOffset; + + const animation = tileset.getAnimationForGid(object.gid); + if (animation) { + const animationScaled = animation.clone(); + animationScaled.scale = scale; + if (hasTint) { + animationScaled.tint = tint; + } + newActor.graphics.use(animationScaled); + newActor.graphics.offset = tileset.tileOffset; + } } // insertable tiles have an x, y, width, height, gid diff --git a/src/resource/tile-layer.ts b/src/resource/tile-layer.ts index 129e9231..c76f13ce 100644 --- a/src/resource/tile-layer.ts +++ b/src/resource/tile-layer.ts @@ -108,12 +108,16 @@ export class TileLayer implements Layer { } const tileset = this.resource.getTilesetForTileGid(gid); - let sprite = tileset.getSpriteForGid(gid); - if (hasTint) { - sprite = sprite.clone(); - sprite.tint = tint; + const headless = this.resource.headless; + + if (!headless) { + let sprite = tileset.getSpriteForGid(gid); + if (hasTint) { + sprite = sprite.clone(); + sprite.tint = tint; + } + tile.addGraphic(sprite, { offset: tileset.tileOffset }); } - tile.addGraphic(sprite, { offset: tileset.tileOffset }); // the whole tilemap uses a giant composite collider relative to the Tilemap @@ -123,7 +127,7 @@ export class TileLayer implements Layer { tile.addCollider(collider); } - let animation = tileset.getAnimationForGid(gid); + let animation = headless ? null : tileset.getAnimationForGid(gid); if (animation) { if (hasTint) { animation = animation.clone(); diff --git a/src/resource/tileset-resource.ts b/src/resource/tileset-resource.ts index a1cd1268..189af9b7 100644 --- a/src/resource/tileset-resource.ts +++ b/src/resource/tileset-resource.ts @@ -59,27 +59,27 @@ export class TilesetResource implements Loadable { if (isTiledTilesetSingleImage(tileset)) { const imagePath = pathRelativeToBase(this.path, tileset.image, this.pathMap); - const image = this.imageLoader.getOrAdd(imagePath); - if (image) { - this.data = new Tileset({ - name: tileset.name, - tiledTileset: tileset, - firstGid: this.firstGid, - image - }); - } + const image = this.headless ? undefined : this.imageLoader.getOrAdd(imagePath); + this.data = new Tileset({ + name: tileset.name, + tiledTileset: tileset, + firstGid: this.firstGid, + ...({ image }), + }); } if (isTiledTilesetCollectionOfImages(tileset)) { - const tileToImage = new Map(); - const images: ImageSource[] = []; - if (tileset.tiles) { - for (let tile of tileset.tiles) { - if (tile.image) { - const imagePath = pathRelativeToBase(this.path, tile.image, this.pathMap); - const image = this.imageLoader.getOrAdd(imagePath); - tileToImage.set(tile, image); - images.push(image); + const tileToImage = this.headless ? undefined : new Map(); + if (tileToImage) { + const images: ImageSource[] = []; + if (tileset.tiles) { + for (let tile of tileset.tiles) { + if (tile.image) { + const imagePath = pathRelativeToBase(this.path, tile.image, this.pathMap); + const image = this.imageLoader.getOrAdd(imagePath); + tileToImage.set(tile, image); + images.push(image); + } } } } @@ -89,7 +89,7 @@ export class TilesetResource implements Loadable { name: tileset.name, tiledTileset: tileset, firstGid: this.firstGid, - tileToImage: tileToImage + ...({ tileToImage }), }); } diff --git a/src/resource/tileset.ts b/src/resource/tileset.ts index fe7ddd39..4d8867f4 100644 --- a/src/resource/tileset.ts +++ b/src/resource/tileset.ts @@ -84,7 +84,7 @@ export class Tileset implements Properties { this.tiledTileset = tiledTileset; this.firstGid = firstGid; - if (isTiledTilesetSingleImage(tiledTileset) && image) { + if (isTiledTilesetSingleImage(tiledTileset)) { mapProps(this, tiledTileset.properties); const spacing = tiledTileset.spacing; const columns = Math.floor((tiledTileset.imagewidth + spacing) / (tiledTileset.tilewidth + spacing)); @@ -95,25 +95,27 @@ export class Tileset implements Properties { this.verticalFlipTransform = AffineMatrix.identity().translate(0, tiledTileset.tileheight).scale(1, -1); this.diagonalFlipTransform = AffineMatrix.identity().translate(0, 0).rotate(-Math.PI / 2).scale(-1, 1); this.objectalignment = tiledTileset.objectalignment ?? (this.orientation === 'orthogonal' ? 'bottomleft' : 'bottom'); - this.spritesheet = SpriteSheet.fromImageSource({ - image, - grid: { - rows, - columns, - spriteWidth: tiledTileset.tilewidth, - spriteHeight: tiledTileset.tileheight - }, - spacing: { - originOffset: { - x: tiledTileset.margin ?? 0, - y: tiledTileset.margin ?? 0 + if (image) { + this.spritesheet = SpriteSheet.fromImageSource({ + image, + grid: { + rows, + columns, + spriteWidth: tiledTileset.tilewidth, + spriteHeight: tiledTileset.tileheight }, - margin: { - x: tiledTileset.spacing ?? 0, - y: tiledTileset.spacing ?? 0 + spacing: { + originOffset: { + x: tiledTileset.margin ?? 0, + y: tiledTileset.margin ?? 0 + }, + margin: { + x: tiledTileset.spacing ?? 0, + y: tiledTileset.spacing ?? 0 + } } - } - }); + }); + } this.tileCount = tiledTileset.tilecount; this.tileWidth = tiledTileset.tilewidth; this.tileHeight = tiledTileset.tileheight; @@ -125,12 +127,13 @@ export class Tileset implements Properties { this.tiles.push(new Tile({ id: tile.id, tileset: this, - tiledTile: tile + tiledTile: tile, + ...({ image }) })) } } } - if (isTiledTilesetCollectionOfImages(tiledTileset) && tiledTileset.firstgid !== undefined && tileToImage) { + if (isTiledTilesetCollectionOfImages(tiledTileset) && tiledTileset.firstgid !== undefined) { this.horizontalFlipTransform = AffineMatrix.identity().translate(tiledTileset.tilewidth, 0).scale(-1, 1); this.verticalFlipTransform = AffineMatrix.identity().translate(0, tiledTileset.tileheight).scale(1, -1); this.diagonalFlipTransform = AffineMatrix.identity().translate(0, 0).rotate(-Math.PI / 2).scale(-1, 1); @@ -145,19 +148,21 @@ export class Tileset implements Properties { let sprites: Sprite[] = [] if (tiledTileset.tiles) { for (const tile of tiledTileset.tiles) { - const image = tileToImage.get(tile); + const image = tileToImage?.get(tile); if (image) { - this.tiles.push(new Tile({ - id: tile.id, - tileset: this, - tiledTile: tile, - image - })) sprites.push(image.toSprite()) } + this.tiles.push(new Tile({ + id: tile.id, + tileset: this, + tiledTile: tile, + ...({ image }) + })) } } - this.spritesheet = new SpriteSheet({ sprites }); + if (tileToImage) { + this.spritesheet = new SpriteSheet({ sprites }); + } } } diff --git a/webpack.config.js b/webpack.config.js index eeb21317..b1959e03 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -48,6 +48,12 @@ module.exports = { commonjs2: "excalibur", amd: "excalibur", root: "ex" + }, + "jsdom": { + commonjs: "JSDOM", + commonjs2: "JSDOM", + amd: "JSDOM", + root: "JSDOM" } }, plugins: [