/* global tiled, TileMap, MapObject, ObjectGroup */ "use strict"; function stationGameplayProperties(properties) { const result = {}; for (const name of Object.keys(properties)) { if (name === "proto" || name.startsWith("state.") || name.startsWith("machine.") || (name !== "solid" && Number.isInteger(properties[name]))) { result[name] = properties[name]; } } return result; } function stationLayer(properties, defaults) { const layer = properties["state.layer"] === undefined ? (defaults && defaults.get(properties.proto)) || "structure" : JSON.parse(properties["state.layer"]); if (layer !== "floor" && layer !== "structure") throw new Error("state.layer must be the JSON string \"floor\" or \"structure\"."); return layer; } function stationConvert(map) { if (!map || !map.isTileMap) throw new Error("Open a Station map first."); if (map.infinite || map.orientation !== TileMap.Orthogonal) throw new Error("Station supports finite orthogonal maps only."); if (map.width <= 0 || map.height <= 0 || map.width * map.height > 1000000) throw new Error("Station maps must contain between 1 and 1,000,000 cells."); const layers = []; function visit(parent) { for (let i = 0; i < parent.layerCount; ++i) { const layer = parent.layerAt(i); if (layer.offset.x !== 0 || layer.offset.y !== 0) throw new Error("Station does not support layer offsets: " + layer.name); if (layer.isGroupLayer) visit(layer); else layers.push(layer); } } visit(map); const prototypeLayers = new Map(); for (const tileset of map.tilesets) { for (const tile of tileset.tiles) { const properties = tile.properties(); if (properties.proto === undefined || properties["state.layer"] === undefined) continue; const layer = stationLayer(properties); if (prototypeLayers.has(properties.proto) && prototypeLayers.get(properties.proto) !== layer) throw new Error("Conflicting state.layer defaults for " + properties.proto); prototypeLayers.set(properties.proto, layer); } } const occupied = new Set(); function reserve(x, y, properties) { const key = x + "," + y + "," + stationLayer(properties, prototypeLayers); if (occupied.has(key)) throw new Error("Two gameplay objects occupy " + key + ". Nothing was converted."); occupied.add(key); } for (const layer of layers.filter(layer => layer.isObjectLayer)) { for (const object of layer.objects) { if (object.className !== "building") continue; const properties = Object.assign({}, object.tile ? stationGameplayProperties(object.tile.properties()) : {}, object.properties()); reserve(Math.floor(object.x / map.tileWidth), Math.floor((object.y - (object.tile ? 1 : 0)) / map.tileHeight), properties); } } const planned = []; for (const layer of layers.filter(layer => layer.isTileLayer)) { if (layer.locked) throw new Error("Unlock tile layer before converting: " + layer.name); for (let y = 0; y < map.height; ++y) { for (let x = 0; x < map.width; ++x) { const tile = layer.tileAt(x, y); if (!tile || tile.property("proto") === undefined) continue; const properties = stationGameplayProperties(tile.properties()); if (typeof properties.proto !== "string" || !properties.proto) throw new Error("Tile proto must be a nonempty building ID."); const cell = layer.cellAt(x, y); for (const [key, value] of [ ["state.flip_h", cell.flippedHorizontally], ["state.flip_v", cell.flippedVertically], ["state.flip_d", cell.flippedAntiDiagonally], ]) { if (value) properties[key] = true; } reserve(x, y, properties); planned.push({layer, x, y, properties}); } } } if (planned.length === 0) return 0; const names = ["Floors", "Wiring/Plumbing", "Objects", "Ground Items", "Triggers"]; const destinations = new Map(); for (const name of names) { const matches = layers.filter(layer => layer.isObjectLayer && layer.name === name); if (matches.length > 1) throw new Error("Multiple object layers named " + name + "; rename them before converting."); if (matches.length === 1) { if (matches[0].locked) throw new Error("Unlock object layer before converting: " + name); destinations.set(name, matches[0]); } } map.macro("Convert Station painted tiles to objects", function () { for (const name of names) { if (!destinations.has(name)) { const objects = new ObjectGroup(); objects.name = name; map.addLayer(objects); destinations.set(name, objects); } } const selected = []; const edits = new Map(); for (const entry of planned) { const object = new MapObject(); object.name = entry.properties.proto; object.className = "building"; object.shape = MapObject.Point; object.x = (entry.x + 0.5) * map.tileWidth; object.y = (entry.y + 0.5) * map.tileHeight; object.setProperties(entry.properties); const name = stationLayer(entry.properties, prototypeLayers) === "floor" ? "Floors" : "Objects"; destinations.get(name).addObject(object); selected.push(object); if (!edits.has(entry.layer)) edits.set(entry.layer, entry.layer.edit()); edits.get(entry.layer).setTile(entry.x, entry.y, null); } for (const edit of edits.values()) edit.apply(); map.selectedObjects = selected; }); return planned.length; } const stationConvertAction = tiled.registerAction("StationConvertPaintedTiles", function () { try { const count = stationConvert(tiled.activeAsset); tiled.log("Station: converted " + count + " painted tiles to editable point objects."); } catch (error) { tiled.alert("Station conversion failed: " + error.message); } }); stationConvertAction.text = "Station: Convert Painted Tiles to Objects"; if (tiled.mapEditor) tiled.extendMenu("Map", [{action: "StationConvertPaintedTiles"}]);