You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

193 lines
5.7 KiB
JavaScript

const MAP_WIDTH = 40;
const MAP_HEIGHT = 40;
const TILE_SIZE = 100;
const DIR = FileInfo.path(__filename);
const EXPORT_EMPTY = 100;
const EMPTY_IDS = new Set([100, 200, 500, 1000, 2000, 3000, 4000]);
const do_layer = (map, name, str, ts_name, has_empty) =>
{
const ts_file = `${DIR}/tileset_${ts_name}.tsj`;
// Using the read method on the tileset format will not mark the tileset as
// external, so if the user saves the map, it will have a copy of the tileset.
// tiled.open will properly mark it as external, but it also 1) opens a new
// tab for the tileset, 2) litters recent files. Closing it at the end solves
// 1), but not 2).
//const ts = tiled.tilesetFormat('json').read(ts_file);
const ts = tiled.open(ts_file);
try
{
map.addTileset(ts);
const layer = new TileLayer(name);
layer.width = MAP_WIDTH;
layer.height = MAP_HEIGHT;
const edit = layer.edit();
map.addLayer(layer);
for (let y = 0; y < MAP_HEIGHT; ++y)
for (let x = 0; x < MAP_WIDTH; ++x)
{
const id = str.charCodeAt(y*MAP_WIDTH + x);
if (has_empty && EMPTY_IDS.has(id)) continue;
edit.setTile(x, y, ts.tile(id));
}
edit.apply();
}
finally { tiled.close(ts); }
};
const read = (fn) =>
{
const file = new TextFile(fn, TextFile.ReadOnly);
const content = file.readAll();
file.close();
const json = JSON.parse(LZString.decompressFromBase64(content));
// {Type: 'Always'/'Hybrid', Tiles: '...', Objects: '...'}
const map = new TileMap();
map.setSize(MAP_WIDTH, MAP_HEIGHT);
map.setTileSize(TILE_SIZE, TILE_SIZE);
map.setProperty('is_hybrid', json.Type == 'Hybrid');
do_layer(map, 'Tiles', json.Tiles, 'tile', false);
do_layer(map, 'Objects', json.Objects, 'object', true);
return map;
};
const assert = (expr, err) =>
{
if (expr) return;
throw new Error(err);
};
const get_layer = (map, name) =>
{
for (const l of map.layers)
if (l.name === name)
return l;
throw Error(`Missing layer "${name}"`);
};
const write_layer = (map, name) =>
{
const layer = get_layer(map, name);
const codes = [];
for (let y = 0; y < MAP_HEIGHT; ++y)
for (let x = 0; x < MAP_WIDTH; ++x)
{
const tile = layer.tileAt(x, y);
if (tile == null)
codes.push(EXPORT_EMPTY);
else
{
assert(tile.tileset.name == name,
`Bad tileset ${tile.tileset.name} on layer ${name} at ${x}, ${y}`);
codes.push(tile.id);
}
}
return String.fromCharCode(...codes);
};
const write = (map, fn) =>
{
assert(map.width == MAP_WIDTH && map.height == MAP_HEIGHT, 'Invalid map size');
assert(!map.infinite, 'Map is infinite');
assert(map.layerCount == 2, 'Invalid layer count');
const json = {
Type: map.property('is_hybrid') ? 'Hybrid' : 'Always',
Tiles: write_layer(map, 'Tiles'),
Objects: write_layer(map, 'Objects'),
};
const text = LZString.compressToBase64(JSON.stringify(json));
const file = new TextFile(fn, TextFile.WriteOnly);
file.writeLine(text);
file.commit();
};
tiled.registerMapFormat(
'bondageclub', { name: 'BondageClub map', extension: 'txt', read, write });
const validate_layer = (check, layer) =>
{
check(layer.width == MAP_WIDTH && layer.height == MAP_HEIGHT,
`Layer ${layer.name} is not ${MAP_WIDTH}x${MAP_HEIGHT}`);
const is_objects = layer.name === 'Objects';
for (let y = 0; y < layer.height; ++y)
for (let x = 0; x < layer.width; ++x)
{
const tile = layer.tileAt(x, y);
if (tile == null)
check(is_objects, `Empty tile on layer ${layer.name} at ${x}, ${y}`);
else
{
check(tile.tileset.name == layer.name,
`Bad tileset ${tile.tileset.name} on layer ${layer.name} at ${x}, ${y}`);
}
}
};
const validate = () =>
{
let was_error = false;
const map = tiled.activeAsset;
const check = (expr, err, func) =>
{
if (expr) return;
was_error = true;
tiled.warn(err, func);
};
check(!map.infinite, 'Map is infinite');
check(map.width == MAP_WIDTH && map.height == MAP_HEIGHT,
`Map not ${MAP_WIDTH}x${MAP_HEIGHT}`);
check(map.layerCount == 2, 'Map layer count is not 2');
const layers = {};
for (const l of map.layers)
{
layers[l.name] = l;
if (l.name === 'Objects' || l.name === 'Tiles')
validate_layer(check, l);
else
check(false, `Layer "${l.name}" has invalid name`);
}
if (layers.Objects && layers.Tiles &&
layers.Objects.width === layers.Tiles.width &&
layers.Objects.height === layers.Objects.height)
for (let y = 0; y < layers.Objects.height; ++y)
for (let x = 0; x < layers.Objects.width; ++x)
{
const obj = layers.Objects.tileAt(x, y);
if (obj == null) continue;
const tile = layers.Tiles.tileAt(x, y);
if (tile == null) continue; // error'd in validate_layer
const obj_type = obj.property('type');
const tile_type = tile.property('type');
// validation logic from ChatRoomMapViewUpdateFlag
check(obj_type !== 'floor' || tile_type === 'floor',
`Floor item on not floor at ${x}, ${y}`);
check(obj_type !== 'wall' || tile_type === 'wall',
`Wall item on not wall at ${x}, ${y}`);
if (obj_type === 'door')
{
check(tile_type === 'wall', `Door on not wall at ${x}, ${y}`);
tile_below = layers.Tiles.tileAt(x, y+1);
check(tile_below == null || tile_below.property('type') !== 'wall',
`Place below door is wall at ${x}, ${y}`);
}
}
if (was_error) tiled.alert('Validation failed, check issues for details');
};
const act = tiled.registerAction('bc_validate', validate);
act.text = 'Validate BC map';
tiled.extendMenu('Map', [{action: 'bc_validate', before: 'MapProperties'}]);