2026-10-21 19:40:20 +00:00
|
|
|
/* 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;
|
|
|
|
|
|
2026-10-21 20:20:13 +00:00
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-21 19:40:20 +00:00
|
|
|
map.macro("Convert Station painted tiles to objects", function () {
|
2026-10-21 20:20:13 +00:00
|
|
|
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 = [];
|
2026-10-21 19:40:20 +00:00
|
|
|
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);
|
2026-10-21 20:20:13 +00:00
|
|
|
const name = stationLayer(entry.properties, prototypeLayers) === "floor" ? "Floors" : "Objects";
|
|
|
|
|
destinations.get(name).addObject(object);
|
|
|
|
|
selected.push(object);
|
2026-10-21 19:40:20 +00:00
|
|
|
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();
|
2026-10-21 20:20:13 +00:00
|
|
|
map.selectedObjects = selected;
|
2026-10-21 19:40:20 +00:00
|
|
|
});
|
|
|
|
|
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"}]);
|