2026-10-21 19:40:20 +00:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const assert = require("node:assert/strict");
|
|
|
|
|
const {readFileSync} = require("node:fs");
|
|
|
|
|
const {join} = require("node:path");
|
|
|
|
|
const {test} = require("node:test");
|
|
|
|
|
const vm = require("node:vm");
|
|
|
|
|
|
|
|
|
|
function harness() {
|
|
|
|
|
class ObjectGroup {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.isObjectLayer = true;
|
|
|
|
|
this.offset = {x: 0, y: 0};
|
|
|
|
|
this.objects = [];
|
|
|
|
|
}
|
|
|
|
|
addObject(object) { this.objects.push(object); }
|
|
|
|
|
}
|
|
|
|
|
class MapObject {
|
|
|
|
|
static Point = "point";
|
|
|
|
|
setProperties(properties) { this.props = properties; }
|
2026-10-21 21:36:34 +00:00
|
|
|
setProperty(name, value) {
|
|
|
|
|
this.props ||= {};
|
|
|
|
|
this.props[name] = value;
|
|
|
|
|
}
|
2026-10-21 19:40:20 +00:00
|
|
|
properties() { return this.props; }
|
|
|
|
|
}
|
|
|
|
|
const messages = [];
|
|
|
|
|
const actions = {};
|
2026-10-21 21:36:34 +00:00
|
|
|
const tools = {};
|
|
|
|
|
const dialogState = {values: {}, apply: true};
|
|
|
|
|
class Dialog {
|
|
|
|
|
static SingleWidgetRows = 1;
|
|
|
|
|
constructor() { this.inputs = {}; this.buttons = {}; }
|
|
|
|
|
addLabel() {}
|
|
|
|
|
addCheckBox(name) { return this.inputs[name] = {checked: false}; }
|
|
|
|
|
addTextInput(name, text) { return this.inputs[name] = {text}; }
|
|
|
|
|
addButton(name) {
|
|
|
|
|
return {clicked: {connect: callback => { this.buttons[name] = callback; }}};
|
|
|
|
|
}
|
|
|
|
|
accept() { this.accepted = true; }
|
|
|
|
|
reject() { this.accepted = false; }
|
|
|
|
|
exec() {
|
|
|
|
|
for (const [name, value] of Object.entries(dialogState.values)) {
|
|
|
|
|
if (typeof value === "boolean") this.inputs[name].checked = value;
|
|
|
|
|
else this.inputs[name].text = value;
|
|
|
|
|
}
|
|
|
|
|
this.buttons[dialogState.apply ? "Apply" : "Cancel"]();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class TileMap {
|
|
|
|
|
static Orthogonal = "orthogonal";
|
|
|
|
|
constructor() { this.layers = []; }
|
|
|
|
|
setSize(width, height) { this.width = width; this.height = height; }
|
|
|
|
|
setTileSize(width, height) { this.tileWidth = width; this.tileHeight = height; }
|
|
|
|
|
addLayer(layer) { this.layers.push(layer); }
|
|
|
|
|
addTileset() {}
|
|
|
|
|
}
|
|
|
|
|
class TileLayer {
|
|
|
|
|
constructor() { this.cells = new Map(); }
|
|
|
|
|
edit() {
|
|
|
|
|
return {
|
|
|
|
|
setTile: (x, y, tile) => { this.cells.set(`${x},${y}`, tile); },
|
|
|
|
|
apply() {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-21 19:40:20 +00:00
|
|
|
const context = {
|
2026-10-21 21:36:34 +00:00
|
|
|
MapObject, ObjectGroup, TileMap, TileLayer, Dialog,
|
|
|
|
|
Qt: {LeftButton: 1, RightButton: 2, Key_Escape: 27},
|
2026-10-21 19:40:20 +00:00
|
|
|
tiled: {
|
2026-10-21 21:36:34 +00:00
|
|
|
mapEditor: {tilesetsView: {selectedTiles: []}},
|
2026-10-21 19:40:20 +00:00
|
|
|
registerAction(name, callback) { actions[name] = callback; return {}; },
|
2026-10-21 21:36:34 +00:00
|
|
|
registerTool(name, tool) { tools[name] = tool; return tool; },
|
2026-10-21 19:40:20 +00:00
|
|
|
extendMenu(name, entries) {
|
|
|
|
|
assert.equal(name, "Map");
|
|
|
|
|
assert.equal(entries[0].action, "StationConvertPaintedTiles");
|
|
|
|
|
},
|
|
|
|
|
log(text) { messages.push(text); },
|
|
|
|
|
alert(text) { messages.push(text); },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
vm.createContext(context);
|
|
|
|
|
vm.runInContext(readFileSync(join(__dirname, "../assets/tiled/station.js"), "utf8"), context);
|
|
|
|
|
const map = {
|
|
|
|
|
isTileMap: true, orientation: "orthogonal", infinite: false,
|
|
|
|
|
width: 2, height: 2, tileWidth: 32, tileHeight: 32,
|
|
|
|
|
layers: [], tilesets: [], macros: 0,
|
|
|
|
|
get layerCount() { return this.layers.length; },
|
|
|
|
|
layerAt(index) { return this.layers[index]; },
|
|
|
|
|
addLayer(layer) { this.layers.push(layer); },
|
|
|
|
|
macro(name, callback) {
|
2026-10-21 21:36:34 +00:00
|
|
|
assert.ok(["Convert Station painted tiles to objects", "Paint Station objects"].includes(name));
|
2026-10-21 19:40:20 +00:00
|
|
|
this.macros++;
|
|
|
|
|
const layers = [...this.layers];
|
|
|
|
|
const cells = layers.filter(layer => layer.isTileLayer)
|
|
|
|
|
.map(layer => [layer, new Map(layer.cells)]);
|
2026-10-21 20:20:13 +00:00
|
|
|
const objects = layers.filter(layer => layer.isObjectLayer)
|
|
|
|
|
.map(layer => [layer, [...layer.objects]]);
|
2026-10-21 19:40:20 +00:00
|
|
|
callback();
|
|
|
|
|
this.undo = () => {
|
|
|
|
|
this.layers = layers;
|
|
|
|
|
for (const [layer, snapshot] of cells) layer.cells = snapshot;
|
2026-10-21 20:20:13 +00:00
|
|
|
for (const [layer, snapshot] of objects) layer.objects = snapshot;
|
2026-10-21 19:40:20 +00:00
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
context.tiled.activeAsset = map;
|
2026-10-21 21:36:34 +00:00
|
|
|
tools.StationObjectBrush.map = map;
|
|
|
|
|
for (const name of ["mousePressed", "mouseReleased", "mouseMoved"]) {
|
|
|
|
|
const callback = tools.StationObjectBrush[name];
|
|
|
|
|
tools.StationObjectBrush[name] = function (...args) {
|
|
|
|
|
const index = name === "mouseMoved" ? 0 : 1;
|
|
|
|
|
this.tilePosition = {
|
|
|
|
|
x: Math.floor(args[index] / map.tileWidth),
|
|
|
|
|
y: Math.floor(args[index + 1] / map.tileHeight),
|
|
|
|
|
};
|
|
|
|
|
return callback.apply(this, args);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {map, context, actions, messages, tools, dialogState};
|
2026-10-21 19:40:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tile(properties) {
|
|
|
|
|
return {property: name => properties[name], properties: () => properties};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function layer(entries) {
|
|
|
|
|
return {
|
|
|
|
|
name: "Paint", offset: {x: 0, y: 0}, isTileLayer: true, locked: false,
|
|
|
|
|
cells: new Map(entries.map(([x, y, tile, flags = {}]) => [`${x},${y}`, {tile, flags}])),
|
|
|
|
|
tileAt(x, y) { return this.cells.get(`${x},${y}`)?.tile || null; },
|
|
|
|
|
cellAt(x, y) { return this.cells.get(`${x},${y}`).flags; },
|
|
|
|
|
edit() {
|
|
|
|
|
const pending = [];
|
|
|
|
|
return {
|
|
|
|
|
setTile(x, y, value) { assert.equal(value, null); pending.push(`${x},${y}`); },
|
|
|
|
|
apply: () => { for (const key of pending) this.cells.delete(key); },
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("converts floors and walls together, retaining properties, flags and decorative terrain", () => {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
const floor = tile({
|
|
|
|
|
proto: "floor", "state.layer": '"floor"', "state.health": 17,
|
|
|
|
|
"state.vars": '{"stock":2}', name: "metadata", solid: false,
|
|
|
|
|
"state.flip_h": false, "state.flip_v": false, "state.flip_d": false,
|
|
|
|
|
"machine.progress": 1.25, legacy: 3,
|
|
|
|
|
});
|
|
|
|
|
const ground = layer([
|
|
|
|
|
[0, 0, floor, {flippedHorizontally: true, flippedVertically: true, flippedAntiDiagonally: true}],
|
|
|
|
|
[1, 0, tile({solid: true})],
|
|
|
|
|
]);
|
|
|
|
|
const wall = layer([[0, 0, tile({proto: "wall", "state.layer": '"structure"', "state.health": 300})]]);
|
|
|
|
|
map.layers.push(ground, wall);
|
|
|
|
|
assert.equal(context.stationConvert(map), 2);
|
|
|
|
|
assert.equal(map.macros, 1);
|
2026-10-21 20:20:13 +00:00
|
|
|
assert.deepEqual(map.layers.slice(2).map(layer => layer.name),
|
2026-10-21 20:53:24 +00:00
|
|
|
["Floors", "Wiring/Plumbing", "Triggers", "Ground Items", "Objects"]);
|
|
|
|
|
assert.deepEqual(map.layers.slice(2).map(layer => layer.visible),
|
|
|
|
|
[false, false, true, true, true]);
|
|
|
|
|
const objects = [map.layers[2].objects[0], map.layers[6].objects[0]];
|
2026-10-21 20:20:13 +00:00
|
|
|
assert.equal(map.layers[2].objects.length, 1);
|
|
|
|
|
assert.equal(map.layers[3].objects.length, 0);
|
2026-10-21 20:53:24 +00:00
|
|
|
assert.equal(map.layers[6].objects.length, 1);
|
2026-10-21 19:40:20 +00:00
|
|
|
assert.equal(objects[0].className, "building");
|
|
|
|
|
assert.equal(objects[0].shape, "point");
|
|
|
|
|
assert.equal(objects[0].x, 16);
|
|
|
|
|
assert.equal(objects[0].y, 16);
|
|
|
|
|
assert.equal(objects[0].props["state.health"], 17);
|
|
|
|
|
assert.equal(objects[0].props["state.vars"], '{"stock":2}');
|
|
|
|
|
assert.equal(objects[0].props["machine.progress"], 1.25);
|
|
|
|
|
assert.equal(objects[0].props.legacy, 3);
|
|
|
|
|
assert.equal(objects[0].props["state.flip_h"], true);
|
|
|
|
|
assert.equal(objects[0].props["state.flip_v"], true);
|
|
|
|
|
assert.equal(objects[0].props["state.flip_d"], true);
|
|
|
|
|
assert.equal(objects[0].props.name, undefined);
|
|
|
|
|
assert.equal(objects[0].props.solid, undefined);
|
|
|
|
|
assert.equal(ground.tileAt(0, 0), null);
|
|
|
|
|
assert.notEqual(ground.tileAt(1, 0), null);
|
|
|
|
|
assert.equal(wall.tileAt(0, 0), null);
|
2026-10-21 20:20:13 +00:00
|
|
|
assert.deepEqual([...map.selectedObjects], objects);
|
2026-10-21 19:40:20 +00:00
|
|
|
assert.equal(context.stationConvert(map), 0);
|
|
|
|
|
assert.equal(map.macros, 1);
|
|
|
|
|
map.undo();
|
|
|
|
|
assert.equal(map.layers.length, 2);
|
|
|
|
|
assert.equal(ground.tileAt(0, 0), floor);
|
|
|
|
|
assert.notEqual(wall.tileAt(0, 0), null);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-21 20:20:13 +00:00
|
|
|
test("reuses categorized object layers and undoes additions without changing existing objects", () => {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
const floors = new context.ObjectGroup();
|
|
|
|
|
floors.name = "Floors";
|
2026-10-21 20:53:24 +00:00
|
|
|
floors.visible = true;
|
2026-10-21 20:20:13 +00:00
|
|
|
const existing = {className: "building", x: 48, y: 48, tile: null,
|
|
|
|
|
properties: () => ({proto: "floor", "state.layer": '"floor"'})};
|
|
|
|
|
floors.addObject(existing);
|
|
|
|
|
const objects = new context.ObjectGroup();
|
|
|
|
|
objects.name = "Objects";
|
2026-10-21 20:53:24 +00:00
|
|
|
objects.visible = false;
|
2026-10-21 20:20:13 +00:00
|
|
|
map.layers.push(floors, objects, layer([[0, 0, tile({proto: "floor", "state.layer": '"floor"'})]]));
|
|
|
|
|
assert.equal(context.stationConvert(map), 1);
|
|
|
|
|
assert.equal(map.layers.filter(layer => layer.name === "Floors").length, 1);
|
|
|
|
|
assert.equal(floors.objects.length, 2);
|
|
|
|
|
assert.equal(floors.objects[0], existing);
|
|
|
|
|
assert.equal(objects.objects.length, 0);
|
2026-10-21 20:53:24 +00:00
|
|
|
assert.equal(floors.visible, true);
|
|
|
|
|
assert.equal(objects.visible, false);
|
2026-10-21 20:20:13 +00:00
|
|
|
map.undo();
|
|
|
|
|
assert.equal(floors.objects.length, 1);
|
|
|
|
|
assert.equal(floors.objects[0], existing);
|
|
|
|
|
assert.equal(map.layers.length, 3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("refuses ambiguous or locked destination layers before modifying the map", () => {
|
|
|
|
|
for (const duplicate of [false, true]) {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
const floors = new context.ObjectGroup();
|
|
|
|
|
floors.name = "Floors";
|
|
|
|
|
map.layers.push(floors, layer([[0, 0, tile({proto: "floor", "state.layer": '"floor"'})]]));
|
|
|
|
|
if (duplicate) {
|
|
|
|
|
const second = new context.ObjectGroup();
|
|
|
|
|
second.name = "Floors";
|
|
|
|
|
map.layers.push(second);
|
|
|
|
|
} else {
|
|
|
|
|
floors.locked = true;
|
|
|
|
|
}
|
|
|
|
|
assert.throws(() => context.stationConvert(map), duplicate ? /Multiple object layers/ : /Unlock object layer/);
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
assert.equal(floors.objects.length, 0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-21 19:40:20 +00:00
|
|
|
test("rejects same-layer duplicates before making changes", () => {
|
|
|
|
|
for (const properties of [{proto: "wall"}, {proto: "floor", "state.layer": '"floor"'}]) {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
map.layers.push(layer([[0, 0, tile(properties)]]), layer([[0, 0, tile(properties)]]));
|
|
|
|
|
assert.throws(() => context.stationConvert(map), /Two gameplay objects/);
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
assert.notEqual(map.layers[0].tileAt(0, 0), null);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("detects conflicts with existing tile objects and point objects", () => {
|
|
|
|
|
for (const isTile of [false, true]) {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
const objects = new context.ObjectGroup();
|
|
|
|
|
objects.addObject({
|
|
|
|
|
className: "building", x: 16, y: isTile ? 32 : 16,
|
|
|
|
|
tile: isTile ? tile({proto: "wall"}) : null,
|
|
|
|
|
properties: () => ({proto: "wall"}),
|
|
|
|
|
});
|
|
|
|
|
map.layers.push(objects, layer([[0, 0, tile({proto: "wall"})]]));
|
|
|
|
|
assert.throws(() => context.stationConvert(map), /Two gameplay objects/);
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("uses tileset prototype layers for existing objects with omitted defaults", () => {
|
|
|
|
|
for (const painted of [{proto: "wall"}, {proto: "floor", "state.layer": '"floor"'}]) {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
map.tilesets.push({tiles: [tile({proto: "floor", "state.layer": '"floor"'})]});
|
|
|
|
|
const objects = new context.ObjectGroup();
|
|
|
|
|
objects.addObject({
|
|
|
|
|
className: "building", x: 16, y: 16, tile: null,
|
|
|
|
|
properties: () => ({proto: "floor"}),
|
|
|
|
|
});
|
|
|
|
|
map.layers.push(objects, layer([[0, 0, tile(painted)]]));
|
|
|
|
|
if (painted.proto === "wall")
|
|
|
|
|
assert.equal(context.stationConvert(map), 1);
|
|
|
|
|
else
|
|
|
|
|
assert.throws(() => context.stationConvert(map), /Two gameplay objects/);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("walks groups and refuses unsupported maps, offsets, locks and invalid layer properties", () => {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
const ground = layer([[1, 1, tile({proto: "plating", "state.layer": '"floor"'})]]);
|
|
|
|
|
map.layers.push({isGroupLayer: true, offset: {x: 0, y: 0}, layerCount: 1, layerAt: () => ground});
|
|
|
|
|
assert.equal(context.stationConvert(map), 1);
|
|
|
|
|
assert.equal(map.layers[1].objects[0].x, 48);
|
|
|
|
|
for (const mutate of [
|
|
|
|
|
map => { map.infinite = true; },
|
|
|
|
|
map => { map.orientation = "isometric"; },
|
|
|
|
|
map => { map.width = 0; },
|
|
|
|
|
map => { map.width = 1000001; },
|
|
|
|
|
map => { map.layers[0].offset.x = 32; },
|
|
|
|
|
map => { map.layers[0].locked = true; },
|
|
|
|
|
map => { map.layers[0] = layer([[0, 0, tile({proto: "floor", "state.layer": '"wrong"'})]]); },
|
|
|
|
|
map => { map.layers[0] = layer([[0, 0, tile({proto: ""})]]); },
|
|
|
|
|
]) {
|
|
|
|
|
const {map, context} = harness();
|
|
|
|
|
map.layers.push(layer([[0, 0, tile({proto: "floor"})]]));
|
|
|
|
|
mutate(map);
|
|
|
|
|
assert.throws(() => context.stationConvert(map));
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("menu action reports conversion and validation failures explicitly", () => {
|
|
|
|
|
const {map, actions, messages} = harness();
|
|
|
|
|
actions.StationConvertPaintedTiles();
|
|
|
|
|
assert.match(messages[0], /converted 0/);
|
|
|
|
|
map.infinite = true;
|
|
|
|
|
actions.StationConvertPaintedTiles();
|
|
|
|
|
assert.match(messages[1], /conversion failed/);
|
|
|
|
|
});
|
2026-10-21 21:36:34 +00:00
|
|
|
|
|
|
|
|
test("brush configures future objects without changing tileset or previous object properties", () => {
|
|
|
|
|
const {map, context, actions, dialogState, tools} = harness();
|
|
|
|
|
const defaults = {proto: "wall", "state.layer": '"structure"', "state.health": 300,
|
|
|
|
|
"state.solid": true, "state.vars": "{}", "state.variant": "null"};
|
|
|
|
|
const wall = tile(defaults);
|
|
|
|
|
const brush = tools.StationObjectBrush;
|
|
|
|
|
brush.selectedTile = wall;
|
|
|
|
|
context.tiled.mapEditor.tilesetsView.selectedTiles = [wall];
|
|
|
|
|
dialogState.values = {"state.health": "17", "state.solid": false, "state.vars": '{"stock":3}'};
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
brush.mousePressed(1, 1, 1);
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
assert.equal(brush.preview.layers[0].cells.size, 1);
|
|
|
|
|
brush.mouseReleased(1, 1, 1);
|
|
|
|
|
const object = map.layers[0].objects[0];
|
|
|
|
|
assert.equal(object.props["state.health"], 17);
|
|
|
|
|
assert.equal(object.props["state.solid"], false);
|
|
|
|
|
assert.equal(object.props["state.vars"], '{"stock":3}');
|
|
|
|
|
assert.equal(object.tile, wall);
|
|
|
|
|
assert.equal(object.className, "building");
|
|
|
|
|
assert.equal(object.x, 0);
|
|
|
|
|
assert.equal(object.y, 32);
|
|
|
|
|
assert.equal(object.width, 32);
|
|
|
|
|
assert.equal(defaults["state.health"], 300);
|
|
|
|
|
dialogState.values = {"state.health": "42"};
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
brush.mousePressed(1, 33, 1);
|
|
|
|
|
brush.mouseReleased(1, 33, 1);
|
|
|
|
|
assert.equal(map.layers[0].objects[1].props["state.health"], 42);
|
|
|
|
|
assert.equal(object.props["state.health"], 17);
|
|
|
|
|
map.undo();
|
|
|
|
|
assert.equal(map.layers[0].objects.length, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("brush interpolates fast drags, deduplicates cells and undoes the entire stroke", () => {
|
|
|
|
|
const {map, tools} = harness();
|
|
|
|
|
map.width = 8;
|
|
|
|
|
const brush = tools.StationObjectBrush;
|
|
|
|
|
brush.selectedTile = tile({proto: "floor", "state.layer": '"floor"', "state.health": 100});
|
|
|
|
|
brush.mousePressed(1, 1, 1);
|
|
|
|
|
brush.mouseMoved(225, 1);
|
|
|
|
|
brush.mouseMoved(1, 1);
|
|
|
|
|
brush.mouseReleased(1, 225, 1);
|
|
|
|
|
assert.equal(map.macros, 1);
|
|
|
|
|
assert.equal(map.layers[0].name, "Floors");
|
|
|
|
|
assert.equal(map.layers[0].visible, false);
|
|
|
|
|
assert.equal(map.layers[0].objects.length, 8);
|
|
|
|
|
assert.equal(map.selectedObjects.length, 8);
|
|
|
|
|
assert.equal(brush.stroke, null);
|
|
|
|
|
map.undo();
|
|
|
|
|
assert.equal(map.layers.length, 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("brush refuses occupied, painted, locked, duplicate and invalid maps without partial edits", () => {
|
|
|
|
|
for (const scenario of ["object", "painted", "locked", "duplicate", "offset", "infinite"]) {
|
|
|
|
|
const {map, context, tools, messages} = harness();
|
|
|
|
|
const wall = tile({proto: "wall", "state.layer": '"structure"'});
|
|
|
|
|
const group = new context.ObjectGroup();
|
|
|
|
|
group.name = "Objects";
|
|
|
|
|
map.layers.push(group);
|
|
|
|
|
if (scenario === "object") {
|
|
|
|
|
const object = new context.MapObject();
|
|
|
|
|
Object.assign(object, {className: "building", x: 16, y: 16});
|
|
|
|
|
object.setProperties({proto: "wall"});
|
|
|
|
|
group.addObject(object);
|
|
|
|
|
} else if (scenario === "painted") map.layers.push(layer([[1, 0, wall]]));
|
|
|
|
|
else if (scenario === "locked") group.locked = true;
|
|
|
|
|
else if (scenario === "duplicate") map.layers.push(Object.assign(new context.ObjectGroup(), {name: "Objects"}));
|
|
|
|
|
else if (scenario === "offset") group.offset.x = 10;
|
|
|
|
|
else if (scenario === "infinite") map.infinite = true;
|
|
|
|
|
const brush = tools.StationObjectBrush;
|
|
|
|
|
brush.selectedTile = wall;
|
|
|
|
|
context.tiled.alert = () => { assert.fail("Mouse callbacks must not show modal dialogs"); };
|
|
|
|
|
context.tiled.log = text => {
|
|
|
|
|
assert.equal(brush.stroke, null);
|
|
|
|
|
brush.mouseReleased(1, 33, 1);
|
|
|
|
|
messages.push(text);
|
|
|
|
|
};
|
|
|
|
|
brush.mousePressed(1, 1, 1);
|
|
|
|
|
brush.mouseReleased(1, 33, 1);
|
|
|
|
|
assert.equal(map.macros, 0, scenario);
|
|
|
|
|
assert.equal(messages.length, 1, scenario);
|
|
|
|
|
assert.match(messages[0], /Station brush:/);
|
|
|
|
|
assert.equal(brush.statusInfo, messages[0]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("brush allows floor and structure overlaps and cancels on Escape or map change", () => {
|
|
|
|
|
const {map, tools} = harness();
|
|
|
|
|
const brush = tools.StationObjectBrush;
|
|
|
|
|
brush.selectedTile = tile({proto: "floor", "state.layer": '"floor"'});
|
|
|
|
|
brush.mousePressed(1, 1, 1);
|
|
|
|
|
brush.keyPressed(27);
|
|
|
|
|
brush.mouseReleased(1, 1, 1);
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
brush.mousePressed(1, 1, 1);
|
|
|
|
|
brush.mapChanged();
|
|
|
|
|
brush.mouseReleased(1, 1, 1);
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
brush.mousePressed(2, 1, 1);
|
|
|
|
|
assert.equal(map.macros, 0);
|
|
|
|
|
brush.mousePressed(1, 1, 1);
|
|
|
|
|
brush.mouseReleased(1, 1, 1);
|
|
|
|
|
brush.selectedTile = tile({proto: "wall", "state.layer": '"structure"'});
|
|
|
|
|
brush.mousePressed(1, 1, 1);
|
|
|
|
|
brush.mouseReleased(1, 1, 1);
|
|
|
|
|
assert.equal(map.layers[0].objects.length, 1);
|
|
|
|
|
assert.equal(map.layers[1].objects.length, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("brush reports invalid input and cancellation preserves configured defaults", () => {
|
|
|
|
|
const {context, actions, tools, messages, dialogState} = harness();
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
assert.match(messages.pop(), /Select exactly one/);
|
|
|
|
|
const wall = tile({proto: "wall", "state.health": 300, "state.vars": "{}"});
|
|
|
|
|
context.tiled.mapEditor.tilesetsView.selectedTiles = [wall];
|
|
|
|
|
for (const value of ["", "NaN", "1.5", "9007199254740992", "2147483648", "-2147483649"]) {
|
|
|
|
|
dialogState.values = {"state.health": value};
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
assert.match(messages.pop(), /valid 32-bit integer/);
|
|
|
|
|
}
|
|
|
|
|
dialogState.values = {"state.vars": "invalid json"};
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
assert.match(messages.pop(), /Invalid brush properties/);
|
|
|
|
|
dialogState.values = {"state.health": "17"};
|
|
|
|
|
dialogState.apply = false;
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
assert.equal(context.stationBrushSettings(wall)["state.health"], 300);
|
|
|
|
|
dialogState.apply = true;
|
|
|
|
|
dialogState.values = {"state.health": "2147483647"};
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
assert.equal(context.stationBrushSettings(wall)["state.health"], 2147483647);
|
|
|
|
|
tools.StationObjectBrush.selectedTile = tile({name: "water"});
|
|
|
|
|
tools.StationObjectBrush.mousePressed(1, 1, 1);
|
|
|
|
|
assert.match(messages.pop(), /Select one gameplay/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("brush supports fractional machine progress even when its default is zero", () => {
|
|
|
|
|
const {context, actions, dialogState} = harness();
|
|
|
|
|
const machine = tile({proto: "furnace", "machine.progress": 0});
|
|
|
|
|
context.tiled.mapEditor.tilesetsView.selectedTiles = [machine];
|
|
|
|
|
dialogState.values = {"machine.progress": "1.25"};
|
|
|
|
|
actions.StationBrushProperties();
|
|
|
|
|
assert.equal(context.stationBrushSettings(machine)["machine.progress"], 1.25);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("bundled project loads the shipped extension directory", () => {
|
|
|
|
|
const project = JSON.parse(readFileSync(join(__dirname, "../assets/station.tiled-project"), "utf8"));
|
|
|
|
|
const extension = join(__dirname, "../assets", project.extensionsPath, "station.js");
|
|
|
|
|
assert.match(readFileSync(extension, "utf8"), /StationObjectBrush/);
|
|
|
|
|
for (const folder of project.folders)
|
|
|
|
|
assert.ok(require("node:fs").statSync(join(__dirname, "../assets", folder)).isDirectory());
|
|
|
|
|
});
|