249 lines
10 KiB
JavaScript
249 lines
10 KiB
JavaScript
"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; }
|
|
properties() { return this.props; }
|
|
}
|
|
const messages = [];
|
|
const actions = {};
|
|
const context = {
|
|
MapObject, ObjectGroup,
|
|
TileMap: {Orthogonal: "orthogonal"},
|
|
tiled: {
|
|
mapEditor: {},
|
|
registerAction(name, callback) { actions[name] = callback; return {}; },
|
|
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) {
|
|
assert.equal(name, "Convert Station painted tiles to objects");
|
|
this.macros++;
|
|
const layers = [...this.layers];
|
|
const cells = layers.filter(layer => layer.isTileLayer)
|
|
.map(layer => [layer, new Map(layer.cells)]);
|
|
const objects = layers.filter(layer => layer.isObjectLayer)
|
|
.map(layer => [layer, [...layer.objects]]);
|
|
callback();
|
|
this.undo = () => {
|
|
this.layers = layers;
|
|
for (const [layer, snapshot] of cells) layer.cells = snapshot;
|
|
for (const [layer, snapshot] of objects) layer.objects = snapshot;
|
|
};
|
|
},
|
|
};
|
|
context.tiled.activeAsset = map;
|
|
return {map, context, actions, messages};
|
|
}
|
|
|
|
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);
|
|
assert.deepEqual(map.layers.slice(2).map(layer => layer.name),
|
|
["Floors", "Wiring/Plumbing", "Objects", "Ground Items", "Triggers"]);
|
|
const objects = [map.layers[2].objects[0], map.layers[4].objects[0]];
|
|
assert.equal(map.layers[2].objects.length, 1);
|
|
assert.equal(map.layers[3].objects.length, 0);
|
|
assert.equal(map.layers[4].objects.length, 1);
|
|
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);
|
|
assert.deepEqual([...map.selectedObjects], objects);
|
|
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);
|
|
});
|
|
|
|
test("reuses categorized object layers and undoes additions without changing existing objects", () => {
|
|
const {map, context} = harness();
|
|
const floors = new context.ObjectGroup();
|
|
floors.name = "Floors";
|
|
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";
|
|
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);
|
|
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);
|
|
}
|
|
});
|
|
|
|
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/);
|
|
});
|