bc_tiled_plugin

Tiled plugin to edit Bondage Club maps
git clone https://git.neptards.moe/u3shit/bc_tiled_plugin.git
Log | Files | Refs | README | LICENSE

bc_plugin.js (7182B)


      1 const MAP_WIDTH = 40;
      2 const MAP_HEIGHT = 40;
      3 const TILE_SIZE = 100;
      4 const DIR = FileInfo.path(__filename);
      5 
      6 const EXPORT_EMPTY = 100;
      7 const EMPTY_IDS = new Set([100, 200, 500, 1000, 2000, 3000, 4000]);
      8 
      9 const read_clipboard = () =>
     10 {
     11   const d = new Dialog();
     12   const t = d.addTextEdit(null);
     13   t.paste();
     14   return t.plainText;
     15 };
     16 
     17 const write_clipboard = (str) =>
     18 {
     19   const d = new Dialog();
     20   const t = d.addTextEdit(null, str);
     21   t.selectAll();
     22   t.copy();
     23 };
     24 
     25 const do_layer = (map, name, str, ts_name, has_empty) =>
     26 {
     27   const ts_file = `${DIR}/tileset_${ts_name}.tsj`;
     28   // Using the read method on the tileset format will not mark the tileset as
     29   // external, so if the user saves the map, it will have a copy of the tileset.
     30   // tiled.open will properly mark it as external, but it also 1) opens a new
     31   // tab for the tileset, 2) litters recent files. Closing it at the end solves
     32   // 1), but not 2).
     33   //const ts = tiled.tilesetFormat('json').read(ts_file);
     34   const ts = tiled.open(ts_file);
     35   try
     36   {
     37     map.addTileset(ts);
     38 
     39     const layer = new TileLayer(name);
     40     layer.width = MAP_WIDTH;
     41     layer.height = MAP_HEIGHT;
     42     const edit = layer.edit();
     43     map.addLayer(layer);
     44 
     45     for (let y = 0; y < MAP_HEIGHT; ++y)
     46       for (let x = 0; x < MAP_WIDTH; ++x)
     47       {
     48         const id = str.charCodeAt(y*MAP_WIDTH + x);
     49         if (has_empty && EMPTY_IDS.has(id)) continue;
     50         edit.setTile(x, y, ts.tile(id));
     51       }
     52 
     53     edit.apply();
     54   }
     55   finally { tiled.close(ts); }
     56 };
     57 
     58 const impor = (content) =>
     59 {
     60   // accept both compressed and uncompressed input
     61   if (content[0] !== '{') content = LZString.decompressFromBase64(content);
     62   const json = JSON.parse(content);
     63   // {Type: 'Always'/'Hybrid', Tiles: '...', Objects: '...'}
     64   const map = new TileMap();
     65   map.setSize(MAP_WIDTH, MAP_HEIGHT);
     66   map.setTileSize(TILE_SIZE, TILE_SIZE);
     67   map.setProperty('is_hybrid', json.Type == 'Hybrid');
     68   do_layer(map, 'Tiles', json.Tiles, 'tile', false);
     69   do_layer(map, 'Objects', json.Objects, 'object', true);
     70 
     71   return map;
     72 };
     73 
     74 const read = (fn) =>
     75 {
     76   const file = new TextFile(fn, TextFile.ReadOnly);
     77   const content = file.readAll();
     78   file.close();
     79 
     80   return impor(content);
     81 };
     82 
     83 const assert = (expr, err) =>
     84 {
     85   if (expr) return;
     86   throw new Error(err);
     87 };
     88 
     89 const get_layer = (map, name) =>
     90 {
     91   for (const l of map.layers)
     92     if (l.name === name)
     93       return l;
     94   throw Error(`Missing layer "${name}"`);
     95 };
     96 
     97 const write_layer = (map, name) =>
     98 {
     99   const layer = get_layer(map, name);
    100   const codes = [];
    101   for (let y = 0; y < MAP_HEIGHT; ++y)
    102     for (let x = 0; x < MAP_WIDTH; ++x)
    103     {
    104       const tile = layer.tileAt(x, y);
    105       if (tile == null)
    106         codes.push(EXPORT_EMPTY);
    107       else
    108       {
    109         assert(tile.tileset.name == name,
    110                `Bad tileset ${tile.tileset.name} on layer ${name} at ${x}, ${y}`);
    111         codes.push(tile.id);
    112       }
    113     }
    114   return String.fromCharCode(...codes);
    115 };
    116 
    117 const expor = (map, compress = true) =>
    118 {
    119   assert(map.width == MAP_WIDTH && map.height == MAP_HEIGHT, 'Invalid map size');
    120   assert(!map.infinite, 'Map is infinite');
    121   assert(map.layerCount == 2, 'Invalid layer count');
    122 
    123   const json = {
    124     Type: map.property('is_hybrid') ? 'Hybrid' : 'Always',
    125     Tiles: write_layer(map, 'Tiles'),
    126     Objects: write_layer(map, 'Objects'),
    127   };
    128   let res = JSON.stringify(json);
    129   if (compress) res = LZString.compressToBase64(res);
    130   // force escape non-ascii characters...
    131   else res = res.replace(/[\u007F-\uFFFF]/g, (chr) =>
    132     '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).substr(-4));
    133   return res;
    134 };
    135 
    136 const write = (map, fn) =>
    137 {
    138   const text = expor(map);
    139   const file = new TextFile(fn, TextFile.WriteOnly);
    140   file.writeLine(text);
    141   file.commit();
    142 };
    143 
    144 tiled.registerMapFormat(
    145   'bondageclub', { name: 'BondageClub map', extension: 'txt', read, write });
    146 
    147 const validate_layer = (check, layer) =>
    148 {
    149   check(layer.width == MAP_WIDTH && layer.height == MAP_HEIGHT,
    150         `Layer ${layer.name} is not ${MAP_WIDTH}x${MAP_HEIGHT}`);
    151   const is_objects = layer.name === 'Objects';
    152 
    153   for (let y = 0; y < layer.height; ++y)
    154     for (let x = 0; x < layer.width; ++x)
    155     {
    156       const tile = layer.tileAt(x, y);
    157       if (tile == null)
    158         check(is_objects, `Empty tile on layer ${layer.name} at ${x}, ${y}`);
    159       else
    160       {
    161         check(tile.tileset.name == layer.name,
    162               `Bad tileset ${tile.tileset.name} on layer ${layer.name} at ${x}, ${y}`);
    163       }
    164     }
    165 };
    166 
    167 const validate = () =>
    168 {
    169   let was_error = false;
    170   const map = tiled.activeAsset;
    171   const check = (expr, err, func) =>
    172   {
    173     if (expr) return;
    174     was_error = true;
    175     tiled.warn(err, func);
    176   };
    177 
    178   check(!map.infinite, 'Map is infinite');
    179   check(map.width == MAP_WIDTH && map.height == MAP_HEIGHT,
    180          `Map not ${MAP_WIDTH}x${MAP_HEIGHT}`);
    181   check(map.layerCount == 2, 'Map layer count is not 2');
    182 
    183   const layers = {};
    184   for (const l of map.layers)
    185   {
    186     layers[l.name] = l;
    187     if (l.name === 'Objects' || l.name === 'Tiles')
    188       validate_layer(check, l);
    189     else
    190       check(false, `Layer "${l.name}" has invalid name`);
    191   }
    192 
    193   if (layers.Objects && layers.Tiles &&
    194       layers.Objects.width === layers.Tiles.width &&
    195       layers.Objects.height === layers.Objects.height)
    196     for (let y = 0; y < layers.Objects.height; ++y)
    197       for (let x = 0; x < layers.Objects.width; ++x)
    198       {
    199         const obj = layers.Objects.tileAt(x, y);
    200         if (obj == null) continue;
    201         const tile = layers.Tiles.tileAt(x, y);
    202         if (tile == null) continue; // error'd in validate_layer
    203 
    204         const obj_type = obj.property('type');
    205         const tile_type = tile.property('type');
    206         // validation logic from ChatRoomMapViewUpdateFlag
    207         check(obj_type !== 'floor' || tile_type === 'floor',
    208               `Floor item on not floor at ${x}, ${y}`);
    209         check(obj_type !== 'wall' || tile_type === 'wall',
    210               `Wall item on not wall at ${x}, ${y}`);
    211         if (obj_type === 'door')
    212         {
    213           check(tile_type === 'wall', `Door on not wall at ${x}, ${y}`);
    214           tile_below = layers.Tiles.tileAt(x, y+1);
    215           check(tile_below == null || tile_below.property('type') !== 'wall',
    216                 `Place below door is wall at ${x}, ${y}`);
    217         }
    218       }
    219 
    220   if (was_error) tiled.alert('Validation failed, check issues for details');
    221 };
    222 
    223 {
    224   const act = tiled.registerAction('bc_validate', validate);
    225   act.text = 'Validate BC map';
    226   tiled.extendMenu('Map', [{action: 'bc_validate', before: 'MapProperties'}]);
    227 }
    228 
    229 {
    230   const imp = tiled.registerAction(
    231     'bc_from_clipboard', () => tiled.activeAsset = impor(read_clipboard()));
    232   imp.text = 'BC from clipboard';
    233   const exp = tiled.registerAction(
    234     'bc_to_clipboard', () => write_clipboard(expor(tiled.activeAsset)));
    235   exp.text = 'BC to clipboard';
    236   const exp2 = tiled.registerAction(
    237     'bc_to_clipboard2', () => write_clipboard(expor(tiled.activeAsset, false)));
    238   exp2.text = 'BC to clipboard (uncompressed)';
    239 
    240   tiled.extendMenu('File', [
    241     {action: 'bc_from_clipboard', before: 'Close'},
    242     {action: 'bc_to_clipboard'},
    243     {action: 'bc_to_clipboard2'},
    244     {separator: true},
    245   ]);
    246 }