1 local plugin_key = "bts/puzzlescript"
3 local sprite = app.sprite
4 local pc = app.pixelColor
9 return string.format("#%02x%02x%02x", c.red, c.green, c.blue)
12 function findOrInsertIdx(table, value)
14 if table[i] == value then
18 table[#table + 1] = value
22 function findOrAdd(table, idx, default)
23 if table[idx] == nil then
24 table[idx] = default or {}
29 function get_layers_flat(output, layer_container)
30 for _, layer in ipairs(layer_container.layers) do
31 table.insert(output, layer)
33 get_layers_flat(output, layer)
42 local PuzzleScriptSection = {}
44 function PuzzleScriptSection:new(o)
52 function PuzzleScriptSection:add_item(item)
53 self.items[#self.items + 1] = item
58 local PuzzleScriptRawItem = {}
60 function PuzzleScriptRawItem:new(o)
68 function PuzzleScriptRawItem:add_line(line)
69 self.lines[#self.lines + 1] = line
72 function PuzzleScriptRawItem:export(script_file)
73 for _, section_item_line in pairs(self.lines) do
74 script_file:write(section_item_line)
75 script_file:write("\n")
81 local PuzzleScriptObjectItem = {}
83 function PuzzleScriptObjectItem:new(o)
92 function PuzzleScriptObjectItem:set_color(color)
93 if color.alpha == 255 then
94 self.colors = { hexColor(color) }
96 self.colors = { "transparent" }
101 function PuzzleScriptObjectItem:set_pixel(x, y, color)
102 local pixel_idx = y * 5 + x + 1
103 local pixel_value = "."
104 if color.alpha == 255 then
105 pixel_value = findOrInsertIdx(self.colors, hexColor(color)) - 1
108 self.pixels[pixel_idx] = pixel_value
111 function PuzzleScriptObjectItem:export(script_file)
112 script_file:write(self.id)
113 script_file:write("\n")
115 script_file:write(table.concat(self.colors, " "))
116 script_file:write("\n")
121 script_file:write(self.pixels[y * 5 + x + 1])
123 script_file:write("\n")
130 local PuzzleScriptLayerItem = {}
132 function PuzzleScriptLayerItem:new(o)
134 setmetatable(o, self)
140 function PuzzleScriptLayerItem:add_object(object)
141 self.objects[#self.objects + 1] = object
144 function PuzzleScriptLayerItem:export(script_file)
145 local object_ids = { self.id }
146 for i, obj in ipairs(self.objects) do
147 object_ids[i] = obj.id
149 script_file:write(table.concat(object_ids, ", "))
150 script_file:write("\n")
155 local PuzzleScriptLegendItem = {}
157 function PuzzleScriptLegendItem:new(o)
159 setmetatable(o, self)
165 function PuzzleScriptLegendItem:obj_match(objects)
166 if objects == nil and self.objects == nil then return true end
167 if objects == nil or self.objects == nil then return false end
168 if #objects ~= #self.objects then return false end
169 for i = 1, #self.objects do
170 if objects[i] ~= self.objects[i] then return false end
175 function PuzzleScriptLegendItem:set_objects(objects)
177 for _, obj in pairs(objects) do
178 table.insert(self.objects, obj)
182 function PuzzleScriptLegendItem:export(script_file)
183 local object_ids = {}
186 for i, obj in ipairs(self.objects) do
187 object_ids[i] = obj.id
190 object_ids = { "Background" } -- FIXME
193 script_file:write(self.id)
194 script_file:write(" = ")
195 script_file:write(table.concat(object_ids, " and "))
197 script_file:write("\n")
202 local PuzzleScriptLegendMapping = {}
204 function PuzzleScriptLegendMapping:new(o)
206 setmetatable(o, self)
211 function PuzzleScriptLegendMapping:add_objects(objects)
212 self.objects = self.objects or {}
213 for _, obj in pairs(objects) do
214 table.insert(self.objects, obj)
218 function PuzzleScriptLegendMapping:export(script_file)
219 local object_ids = {}
222 for i, obj in ipairs(self.objects) do
223 object_ids[i] = obj.id
227 -- Failsafe for matching name and single item
228 if #object_ids == 1 and object_ids[1] == self.id then return end
230 script_file:write(self.id)
231 script_file:write(" = ")
232 script_file:write(table.concat(object_ids, " or "))
234 script_file:write("\n")
240 local PuzzleScriptLevelItem = {}
242 function PuzzleScriptLevelItem:new(o)
244 setmetatable(o, self)
250 function PuzzleScriptLevelItem:set_tile(tile_x, tile_y, value)
251 local tile_idx = tile_y * self.width + tile_x
252 if self.tiles[tile_idx] == nil then
253 self.tiles[tile_idx] = {}
256 table.insert(self.tiles[tile_idx], value)
259 function PuzzleScriptLevelItem:map_to_legend(legend_section, legend_icons)
260 self.tile_legends = {}
262 for tile_y = 0, self.height - 1 do
263 for tile_x = 0, self.width - 1 do
264 local tile_idx = tile_y * self.width + tile_x
266 local legend_item = nil
267 for _, test_legend_item in pairs(legend_section.items) do
268 if test_legend_item:obj_match(self.tiles[tile_idx]) then
269 legend_item = test_legend_item
274 if legend_item == nil then
275 legend_item = PuzzleScriptLegendItem:new()
276 assert(#legend_icons > 0)
277 legend_item.id = legend_icons[1]
278 table.remove(legend_icons, 1)
279 legend_item:set_objects(self.tiles[tile_idx])
280 legend_section:add_item(legend_item)
283 self.tile_legends[tile_idx] = legend_item
288 function PuzzleScriptLevelItem:export(script_file)
289 for tile_y = 0, self.height - 1 do
290 for tile_x = 0, self.width - 1 do
291 local tile_idx = tile_y * self.width + tile_x
292 script_file:write(self.tile_legends[tile_idx].id)
294 script_file:write("\n")
300 local PuzzleScriptFile = {}
302 function PuzzleScriptFile:new(o)
304 setmetatable(o, self)
310 function PuzzleScriptFile:get_section(id)
311 id = string.upper(id)
312 return findOrAdd(self.sections, id, PuzzleScriptSection:new{})
315 function PuzzleScriptFile:read(filename)
316 local script_file = io.open(filename, "r")
319 local current_section = self:get_section("PRELUDE")
320 local current_item = nil
322 local line = script_file:read()
323 if line == nil then break end
324 if string.match(line, "^=+$") then
325 local current_section_name = script_file:read()
326 current_section = self:get_section(current_section_name)
327 script_file:read() -- Skip the next line as it's just the closing ===
330 elseif string.match(line, "^[ \t\r\n]*$") then
333 if current_item == nil then
334 current_item = PuzzleScriptRawItem:new{}
335 current_section:add_item(current_item)
338 current_item:add_line(line)
342 io.close(script_file)
346 function PuzzleScriptFile:export(filename)
347 local script_file = io.open(filename, "w")
349 local ordered_section_names = {
360 for _, section_name in pairs(ordered_section_names) do
361 local section = self:get_section(section_name)
362 if section ~= nil then
363 if section_name ~= "PRELUDE" then
364 local separator = string.rep("=", string.len(section_name))
365 script_file:write(separator)
366 script_file:write("\n")
367 script_file:write(section_name)
368 script_file:write("\n")
369 script_file:write(separator)
370 script_file:write("\n\n")
373 for _, section_item in pairs(section.items) do
374 section_item:export(script_file)
376 if section_name ~= "COLLISIONLAYERS" and section_name ~= "LEGEND" then -- fixme, table??
377 script_file:write("\n")
381 if section_name == "COLLISIONLAYERS" or section_name == "LEGEND" then -- fixme, table??
382 script_file:write("\n")
387 io.close(script_file)
393 local name = app.fs.fileTitle(sprite.filename)
395 local previous_export_filename = sprite.properties(plugin_key).previous_export_filename
396 local previous_merge_filename = sprite.properties(plugin_key).previous_merge_filename
397 local previous_export_lofi = sprite.properties(plugin_key).previous_export_lofi
399 if previous_export_filename == nil then
400 local default_path = app.fs.filePath(sprite.filename)
401 local default_filename = name..".txt"
402 previous_export_filename = app.fs.joinPath(default_path, default_filename)
406 dlg:file{ id="script_filename", label="Output:", filename=previous_export_filename, filetypes=".txt", save=true }
407 dlg:file{ id="merge_filename", label="Merge script (optional):", filename=previous_merge_filename, filetypes=".txt" }
408 dlg:check{ id="lofi", label="Prototype export (layer colours)", selected=previous_export_lofi }
409 dlg:button{ id="confirm", text="Confirm" }
410 dlg:button{ id="cancel", text="Cancel" }
413 local data = dlg.data
414 if data.confirm == false then
418 local layers = get_layers_flat({}, sprite)
420 local pz_file = PuzzleScriptFile:new{}
423 local object_section = pz_file:get_section("OBJECTS")
425 local layer_objects = {}
427 for _, layer in ipairs(layers) do
428 if layer.isVisible then
429 if layer.isImage == true and (layer.isTilemap == false or data.lofi) then
430 local object_item = PuzzleScriptObjectItem:new()
431 object_item.id = layer.name
432 object_section:add_item(object_item)
433 object_item:set_color(layer.color)
434 elseif layer.isTilemap == true then
435 local tileset = layer.tileset
437 local used_tiles = {}
438 for _, frame in ipairs(sprite.frames) do
439 local cel = layer:cel(frame)
441 for pixel in cel.image:pixels() do
442 p = pc.tileI(pixel())
450 local object_names = {}
451 for object_name in string.gmatch(layer.data, "[^,]+") do
452 if object_name ~= "" then
453 object_names[#object_names + 1] = object_name
454 used_tiles[#object_names] = true -- Always export named tiles
458 for tile_idx, _ in pairs(used_tiles) do
459 local object_item = PuzzleScriptObjectItem:new()
460 object_section:add_item(object_item)
462 if object_names[tile_idx] then
463 object_item.id = object_names[tile_idx]
465 object_item.id = layer.name..tile_idx
468 object_item.layer = layer
470 local tile = tileset:tile(tile_idx)
473 local layer_tiles = findOrAdd(layer_objects, layer)
474 layer_tiles[tile_idx] = object_item
478 for pixel in tile.image:pixels() do
479 object_item:set_pixel(pixel.x, pixel.y, Color(pixel()))
482 assert(false, "Missing tile: " .. layer.name .. " " .. tile_idx)
489 local levels_section = pz_file:get_section("LEVELS")
491 local tiles_w = math.floor(sprite.width / 5)
492 local tiles_h = math.floor(sprite.height / 5)
494 for _, frame in ipairs(sprite.frames) do
495 local level_item = PuzzleScriptLevelItem:new()
496 levels_section:add_item(level_item)
497 level_item.width = tiles_w
498 level_item.height = tiles_h
500 for tile_y = 0, tiles_h - 1 do
501 for tile_x = 0, tiles_w - 1 do
502 for _, layer in ipairs(layers) do
503 if layer.isVisible and layer.isTilemap == true then
504 local cel = layer:cel(frame)
506 local image = cel.image
508 local offset_x = math.floor(cel.bounds.x / 5)
509 local offset_y = math.floor(cel.bounds.y / 5)
510 local cel_width = math.floor(cel.bounds.width / 5)
511 local cel_height = math.floor(cel.bounds.height / 5)
513 local actual_tile_x = tile_x - offset_x
514 local actual_tile_y = tile_y - offset_y
515 if actual_tile_x >= 0 and actual_tile_y >= 0 and actual_tile_x < cel_width and actual_tile_y < cel_height then
516 local pixel_value = image:getPixel(actual_tile_x, actual_tile_y)
517 local tile_idx = pc.tileI(pixel_value)
519 assert(layer_objects[layer][tile_idx], "Looking for: " .. layer.name)
520 level_item:set_tile(tile_x, tile_y, layer_objects[layer][tile_idx])
530 local legend_section = pz_file:get_section("LEGEND")
532 local background_legend = PuzzleScriptLegendItem:new()
533 background_legend.id = "."
534 legend_section:add_item(background_legend)
536 local legend_icons = {}
537 for icon in string.gmatch("!\"#$%&'*+,-/\\0123456789:;?@_`abcdefghijklmnopqrstuvwzyz~", ".") do
538 legend_icons[#legend_icons + 1] = icon
541 for _, level in pairs(levels_section.items) do
542 level:map_to_legend(legend_section, legend_icons)
548 local layers_section = pz_file:get_section("COLLISIONLAYERS")
549 for _, layer in ipairs(sprite.layers) do
550 local layer_item = PuzzleScriptLayerItem:new()
551 layer_item.id = layer.name
552 layers_section:add_item(layer_item)
557 for layer, objects in pairs(layer_objects) do
558 local legend_map = PuzzleScriptLegendMapping:new()
559 legend_map.id = layer.name
560 legend_map:add_objects(objects)
561 legend_section:add_item(legend_map)
564 for _, layer in ipairs(layers) do
565 if layer.isGroup and #layer.layers > 0 then
566 local legend_map = PuzzleScriptLegendMapping:new()
567 legend_map.id = layer.name
568 for _, sub_layer in ipairs(layer.layers) do
569 legend_map:add_objects({ {id=sub_layer.name} })
571 legend_section:add_item(legend_map)
575 -- for _, objects in pairs(layer_objects) do
576 -- for _, object in pairs(objects) do
577 -- local layer_item = PuzzleScriptLayerItem:new()
578 -- layers_section:add_item(layer_item)
579 -- layer_item.id = object.layer.name
584 -- for _, layer in ipairs(layers) do
585 -- if layer.isVisible and layer.parent == sprite then
589 --PuzzleScriptLegendMapping:new()
591 -- local layer_item = PuzzleScriptLayerItem:new()
592 -- layers_section:add_item(layer_item)
593 -- layer_item.id = layer.name
598 --legend stuff again after...
602 -- Merge after making everything else
603 pz_file:read(data.merge_filename)
605 pz_file:export(data.script_filename)
607 local export_filename_changed = sprite.properties(plugin_key).previous_export_filename ~= data.script_filename
608 local merge_filename_changed = sprite.properties(plugin_key).previous_merge_filename ~= data.merge_filename
609 local lofi_changed = sprite.properties(plugin_key).previous_export_lofi ~= data.lofi
611 if export_filename_changed or merge_filename_changed or lofi_changed then
612 sprite.properties(plugin_key).previous_export_filename = data.script_filename
613 sprite.properties(plugin_key).previous_merge_filename = data.merge_filename
614 sprite.properties(plugin_key).previous_export_lofi = data.lofi