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 }