station/assets/tiled/station.js

162 lines
6.7 KiB
JavaScript
Raw Normal View History

/* 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"}]);