--- /dev/null
+local pluginKey = "bts/metrics"
+
+local sprite = app.sprite
+
+local name = app.fs.fileTitle(sprite.filename)
+
+local previousExportFilename = sprite.properties(pluginKey).previousExportFilename
+
+if previousExportFilename == nil then
+ local defaultPath = app.fs.filePath(sprite.filename)
+ local defaultFilename = name..".lua"
+ previousExportFilename = app.fs.joinPath(defaultPath, defaultFilename)
+end
+
+local dlg = Dialog()
+dlg:file{ id="metrics_filename", label="Metrics output:", filename=previousExportFilename, filetypes=".lua", save=true }
+dlg:button{ id="confirm", text="Confirm" }
+dlg:button{ id="cancel", text="Cancel" }
+dlg:show()
+
+local data = dlg.data
+if data.confirm then
+ name = app.fs.fileTitle(data.metrics_filename)
+
+ local basePath = app.fs.filePath(data.metrics_filename)
+ local imagePath = app.fs.joinPath(basePath, name)
+
+ local layerLookup = {}
+ local originalLayerVisible = {}
+
+ for i, layer in ipairs(sprite.layers) do
+ if layer.parent == layer.sprite then
+ originalLayerVisible[layer.name] = layer.isVisible
+ layer.isVisible = false
+
+ layerLookup[layer.name] = layer
+ end
+ end
+
+ local imageDetails = {}
+
+ for i, slice in ipairs(sprite.slices) do
+ local id = slice.name
+
+ if layerLookup[id] then
+ local layer = layerLookup[id]
+ local filename = app.fs.joinPath(imagePath, id..".png")
+
+ layer.isVisible = true
+ app.command.SaveFileCopyAs{ ui=false, filename=filename, slice=id }
+ layer.isVisible = false
+
+ table.insert(imageDetails, id)
+ end
+ end
+
+ local metricsDetails = {}
+
+ for i, slice in ipairs(sprite.slices) do
+ local id = slice.name
+ local sliceMetrics = {}
+
+ sliceMetrics["origin"] = { slice.bounds.x, slice.bounds.y}
+ sliceMetrics["size"] = { slice.bounds.width, slice.bounds.height}
+ sliceMetrics["center"] = { (slice.bounds.x + slice.bounds.width / 2), (slice.bounds.y + slice.bounds.height / 2)}
+
+ metricsDetails[id] = sliceMetrics
+ end
+
+ --[[if false then
+ local metricsFilename = app.fs.joinPath(metricsPath, name..".ui.metrics.json")
+ local metricsFile = io.open(metricsFilename, "w")
+ local metricsJson = json.encode(metricsDetails)
+ metricsFile:write(metricsJson)
+ io.close(metricsFile)
+ end]]
+
+ if true then
+ --local metricsFilename = app.fs.joinPath(metricsPath, data.metrics_filename)
+ local metricsFile = io.open(data.metrics_filename, "w")
+
+ metricsFile:write(name.."ImagesTable = {\n")
+ for k,v in ipairs(imageDetails) do
+ metricsFile:write("\t\"" .. v .. "\",\n")
+ end
+ metricsFile:write("}\n")
+ metricsFile:write("\n")
+
+ metricsFile:write(name.."MetricsTable = {\n")
+ for k,v in pairs(metricsDetails) do
+ metricsFile:write("\n")
+ metricsFile:write("\t[\"" .. k .. ".origin\"] = { x=" .. v["origin"][1] .. ", y=" .. v["origin"][2] .. " },\n")
+ metricsFile:write("\t[\"" .. k .. ".size\"] = { w=" .. v["size"][1] .. ", h=" .. v["size"][2] .. " },\n")
+ metricsFile:write("\t[\"" .. k .. ".center\"] = { x=" .. v["center"][1] .. ", y=" .. v["center"][2] .. " },\n")
+ end
+ metricsFile:write("\n")
+ metricsFile:write("}\n")
+ metricsFile:write("\n")
+ metricsFile:write("registerMetrics(\""..name.."\", "..name.."ImagesTable, "..name.."MetricsTable)\n")
+
+ io.close(metricsFile)
+ end
+
+ for k, v in pairs(originalLayerVisible) do
+ local layer = layerLookup[k]
+ layer.isVisible = v
+ end
+
+ sprite.properties(pluginKey).previousExportFilename = data.metrics_filename
+end
--- /dev/null
+local plugin_key = "bts/puzzlescript"
+
+local sprite = app.sprite
+local pc = app.pixelColor
+
+
+
+function hexColor(c)
+ return string.format("#%02x%02x%02x", c.red, c.green, c.blue)
+end
+
+function findOrInsertIdx(table, value)
+ for i=1,#table do
+ if table[i] == value then
+ return i
+ end
+ end
+ table[#table + 1] = value
+ return #table
+end
+
+function findOrAdd(table, idx, default)
+ if table[idx] == nil then
+ table[idx] = default or {}
+ end
+ return table[idx]
+end
+
+function get_layers_flat(output, layer_container)
+ for _, layer in ipairs(layer_container.layers) do
+ table.insert(output, layer)
+ if layer.isGroup then
+ get_layers_flat(output, layer)
+ end
+ end
+
+ return output
+end
+
+
+
+local PuzzleScriptSection = {}
+
+function PuzzleScriptSection:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ o.items = {}
+ return o
+end
+
+function PuzzleScriptSection:add_item(item)
+ self.items[#self.items + 1] = item
+end
+
+
+
+local PuzzleScriptRawItem = {}
+
+function PuzzleScriptRawItem:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ o.lines = {}
+ return o
+end
+
+function PuzzleScriptRawItem:add_line(line)
+ self.lines[#self.lines + 1] = line
+end
+
+function PuzzleScriptRawItem:export(script_file)
+ for _, section_item_line in pairs(self.lines) do
+ script_file:write(section_item_line)
+ script_file:write("\n")
+ end
+end
+
+
+
+local PuzzleScriptObjectItem = {}
+
+function PuzzleScriptObjectItem:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ o.colors = {}
+ o.pixels = {}
+ return o
+end
+
+function PuzzleScriptObjectItem:set_color(color)
+ if color.alpha == 255 then
+ self.colors = { hexColor(color) }
+ else
+ self.colors = { "transparent" }
+ end
+ self.pixels = nil
+end
+
+function PuzzleScriptObjectItem:set_pixel(x, y, color)
+ local pixel_idx = y * 5 + x + 1
+ local pixel_value = "."
+ if color.alpha == 255 then
+ pixel_value = findOrInsertIdx(self.colors, hexColor(color)) - 1
+ end
+
+ self.pixels[pixel_idx] = pixel_value
+end
+
+function PuzzleScriptObjectItem:export(script_file)
+ script_file:write(self.id)
+ script_file:write("\n")
+
+ script_file:write(table.concat(self.colors, " "))
+ script_file:write("\n")
+
+ if self.pixels then
+ for y = 0, 4 do
+ for x = 0, 4 do
+ script_file:write(self.pixels[y * 5 + x + 1])
+ end
+ script_file:write("\n")
+ end
+ end
+end
+
+
+
+local PuzzleScriptLayerItem = {}
+
+function PuzzleScriptLayerItem:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ o.objects = {}
+ return o
+end
+
+function PuzzleScriptLayerItem:add_object(object)
+ self.objects[#self.objects + 1] = object
+end
+
+function PuzzleScriptLayerItem:export(script_file)
+ local object_ids = { self.id }
+ for i, obj in ipairs(self.objects) do
+ object_ids[i] = obj.id
+ end
+ script_file:write(table.concat(object_ids, ", "))
+ script_file:write("\n")
+end
+
+
+
+local PuzzleScriptLegendItem = {}
+
+function PuzzleScriptLegendItem:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+-- o.objects = {}
+ return o
+end
+
+function PuzzleScriptLegendItem:obj_match(objects)
+ if objects == nil and self.objects == nil then return true end
+ if objects == nil or self.objects == nil then return false end
+ if #objects ~= #self.objects then return false end
+ for i = 1, #self.objects do
+ if objects[i] ~= self.objects[i] then return false end
+ end
+ return true
+end
+
+function PuzzleScriptLegendItem:set_objects(objects)
+ self.objects = {}
+ for _, obj in pairs(objects) do
+ table.insert(self.objects, obj)
+ end
+end
+
+function PuzzleScriptLegendItem:export(script_file)
+ local object_ids = {}
+
+ if self.objects then
+ for i, obj in ipairs(self.objects) do
+ object_ids[i] = obj.id
+ end
+ else
+ object_ids = { "Background" } -- FIXME
+ end
+
+ script_file:write(self.id)
+ script_file:write(" = ")
+ script_file:write(table.concat(object_ids, " and "))
+
+ script_file:write("\n")
+end
+
+
+
+local PuzzleScriptLegendMapping = {}
+
+function PuzzleScriptLegendMapping:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+end
+
+function PuzzleScriptLegendMapping:add_objects(objects)
+ self.objects = self.objects or {}
+ for _, obj in pairs(objects) do
+ table.insert(self.objects, obj)
+ end
+end
+
+function PuzzleScriptLegendMapping:export(script_file)
+ local object_ids = {}
+
+ if self.objects then
+ for i, obj in ipairs(self.objects) do
+ object_ids[i] = obj.id
+ end
+ end
+
+ -- Failsafe for matching name and single item
+ if #object_ids == 1 and object_ids[1] == self.id then return end
+
+ script_file:write(self.id)
+ script_file:write(" = ")
+ script_file:write(table.concat(object_ids, " or "))
+
+ script_file:write("\n")
+end
+
+
+
+
+local PuzzleScriptLevelItem = {}
+
+function PuzzleScriptLevelItem:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ o.tiles = {}
+ return o
+end
+
+function PuzzleScriptLevelItem:set_tile(tile_x, tile_y, value)
+ local tile_idx = tile_y * self.width + tile_x
+ if self.tiles[tile_idx] == nil then
+ self.tiles[tile_idx] = {}
+ end
+
+ table.insert(self.tiles[tile_idx], value)
+end
+
+function PuzzleScriptLevelItem:map_to_legend(legend_section, legend_icons)
+ self.tile_legends = {}
+
+ for tile_y = 0, self.height - 1 do
+ for tile_x = 0, self.width - 1 do
+ local tile_idx = tile_y * self.width + tile_x
+
+ local legend_item = nil
+ for _, test_legend_item in pairs(legend_section.items) do
+ if test_legend_item:obj_match(self.tiles[tile_idx]) then
+ legend_item = test_legend_item
+ break
+ end
+ end
+
+ if legend_item == nil then
+ legend_item = PuzzleScriptLegendItem:new()
+ assert(#legend_icons > 0)
+ legend_item.id = legend_icons[1]
+ table.remove(legend_icons, 1)
+ legend_item:set_objects(self.tiles[tile_idx])
+ legend_section:add_item(legend_item)
+ end
+
+ self.tile_legends[tile_idx] = legend_item
+ end
+ end
+end
+
+function PuzzleScriptLevelItem:export(script_file)
+ for tile_y = 0, self.height - 1 do
+ for tile_x = 0, self.width - 1 do
+ local tile_idx = tile_y * self.width + tile_x
+ script_file:write(self.tile_legends[tile_idx].id)
+ end
+ script_file:write("\n")
+ end
+end
+
+
+
+local PuzzleScriptFile = {}
+
+function PuzzleScriptFile:new(o)
+ o = o or {}
+ setmetatable(o, self)
+ self.__index = self
+ o.sections = {}
+ return o
+end
+
+function PuzzleScriptFile:get_section(id)
+ id = string.upper(id)
+ return findOrAdd(self.sections, id, PuzzleScriptSection:new{})
+end
+
+function PuzzleScriptFile:read(filename)
+ local script_file = io.open(filename, "r")
+
+ if script_file then
+ local current_section = self:get_section("PRELUDE")
+ local current_item = nil
+ while true do
+ local line = script_file:read()
+ if line == nil then break end
+ if string.match(line, "^=+$") then
+ local current_section_name = script_file:read()
+ current_section = self:get_section(current_section_name)
+ script_file:read() -- Skip the next line as it's just the closing ===
+
+ current_item = nil
+ elseif string.match(line, "^[ \t\r\n]*$") then
+ current_item = nil
+ else
+ if current_item == nil then
+ current_item = PuzzleScriptRawItem:new{}
+ current_section:add_item(current_item)
+ end
+
+ current_item:add_line(line)
+ end
+ end
+
+ io.close(script_file)
+ end
+end
+
+function PuzzleScriptFile:export(filename)
+ local script_file = io.open(filename, "w")
+
+ local ordered_section_names = {
+ "PRELUDE",
+ "OBJECTS",
+ "LEGEND",
+ "SOUNDS",
+ "COLLISIONLAYERS",
+ "RULES",
+ "WINCONDITIONS",
+ "LEVELS"
+ }
+
+ for _, section_name in pairs(ordered_section_names) do
+ local section = self:get_section(section_name)
+ if section ~= nil then
+ if section_name ~= "PRELUDE" then
+ local separator = string.rep("=", string.len(section_name))
+ script_file:write(separator)
+ script_file:write("\n")
+ script_file:write(section_name)
+ script_file:write("\n")
+ script_file:write(separator)
+ script_file:write("\n\n")
+ end
+
+ for _, section_item in pairs(section.items) do
+ section_item:export(script_file)
+
+ if section_name ~= "COLLISIONLAYERS" and section_name ~= "LEGEND" then -- fixme, table??
+ script_file:write("\n")
+ end
+ end
+
+ if section_name == "COLLISIONLAYERS" or section_name == "LEGEND" then -- fixme, table??
+ script_file:write("\n")
+ end
+ end
+ end
+
+ io.close(script_file)
+end
+
+
+
+function main()
+ local name = app.fs.fileTitle(sprite.filename)
+
+ local previous_export_filename = sprite.properties(plugin_key).previous_export_filename
+ local previous_merge_filename = sprite.properties(plugin_key).previous_merge_filename
+ local previous_export_lofi = sprite.properties(plugin_key).previous_export_lofi
+
+ if previous_export_filename == nil then
+ local default_path = app.fs.filePath(sprite.filename)
+ local default_filename = name..".txt"
+ previous_export_filename = app.fs.joinPath(default_path, default_filename)
+ end
+
+ local dlg = Dialog()
+ dlg:file{ id="script_filename", label="Output:", filename=previous_export_filename, filetypes=".txt", save=true }
+ dlg:file{ id="merge_filename", label="Merge script (optional):", filename=previous_merge_filename, filetypes=".txt" }
+ dlg:check{ id="lofi", label="Prototype export (layer colours)", selected=previous_export_lofi }
+ dlg:button{ id="confirm", text="Confirm" }
+ dlg:button{ id="cancel", text="Cancel" }
+ dlg:show()
+
+ local data = dlg.data
+ if data.confirm == false then
+ return
+ end
+
+ local layers = get_layers_flat({}, sprite)
+
+ local pz_file = PuzzleScriptFile:new{}
+
+
+ local object_section = pz_file:get_section("OBJECTS")
+
+ local layer_objects = {}
+
+ for _, layer in ipairs(layers) do
+ if layer.isVisible then
+ if layer.isImage == true and (layer.isTilemap == false or data.lofi) then
+ local object_item = PuzzleScriptObjectItem:new()
+ object_item.id = layer.name
+ object_section:add_item(object_item)
+ object_item:set_color(layer.color)
+ elseif layer.isTilemap == true then
+ local tileset = layer.tileset
+
+ local used_tiles = {}
+ for _, frame in ipairs(sprite.frames) do
+ local cel = layer:cel(frame)
+ if cel ~= nil then
+ for pixel in cel.image:pixels() do
+ p = pc.tileI(pixel())
+ if p > 0 then
+ used_tiles[p] = true
+ end
+ end
+ end
+ end
+
+ local object_names = {}
+ for object_name in string.gmatch(layer.data, "[^,]+") do
+ if object_name ~= "" then
+ object_names[#object_names + 1] = object_name
+ used_tiles[#object_names] = true -- Always export named tiles
+ end
+ end
+
+ for tile_idx, _ in pairs(used_tiles) do
+ local object_item = PuzzleScriptObjectItem:new()
+ object_section:add_item(object_item)
+
+ if object_names[tile_idx] then
+ object_item.id = object_names[tile_idx]
+ else
+ object_item.id = layer.name..tile_idx
+ end
+
+ object_item.layer = layer
+
+ local tile = tileset:tile(tile_idx)
+
+ if tile then
+ local layer_tiles = findOrAdd(layer_objects, layer)
+ layer_tiles[tile_idx] = object_item
+
+ local colors = {}
+ local pixels = {}
+ for pixel in tile.image:pixels() do
+ object_item:set_pixel(pixel.x, pixel.y, Color(pixel()))
+ end
+ else
+ assert(false, "Missing tile: " .. layer.name .. " " .. tile_idx)
+ end
+ end
+ end
+ end
+ end
+
+ local levels_section = pz_file:get_section("LEVELS")
+
+ local tiles_w = math.floor(sprite.width / 5)
+ local tiles_h = math.floor(sprite.height / 5)
+
+ for _, frame in ipairs(sprite.frames) do
+ local level_item = PuzzleScriptLevelItem:new()
+ levels_section:add_item(level_item)
+ level_item.width = tiles_w
+ level_item.height = tiles_h
+
+ for tile_y = 0, tiles_h - 1 do
+ for tile_x = 0, tiles_w - 1 do
+ for _, layer in ipairs(layers) do
+ if layer.isVisible and layer.isTilemap == true then
+ local cel = layer:cel(frame)
+ if cel ~= nil then
+ local image = cel.image
+
+ local offset_x = math.floor(cel.bounds.x / 5)
+ local offset_y = math.floor(cel.bounds.y / 5)
+ local cel_width = math.floor(cel.bounds.width / 5)
+ local cel_height = math.floor(cel.bounds.height / 5)
+
+ local actual_tile_x = tile_x - offset_x
+ local actual_tile_y = tile_y - offset_y
+ if actual_tile_x >= 0 and actual_tile_y >= 0 and actual_tile_x < cel_width and actual_tile_y < cel_height then
+ local pixel_value = image:getPixel(actual_tile_x, actual_tile_y)
+ local tile_idx = pc.tileI(pixel_value)
+ if tile_idx > 0 then
+ assert(layer_objects[layer][tile_idx], "Looking for: " .. layer.name)
+ level_item:set_tile(tile_x, tile_y, layer_objects[layer][tile_idx])
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ local legend_section = pz_file:get_section("LEGEND")
+
+ local background_legend = PuzzleScriptLegendItem:new()
+ background_legend.id = "."
+ legend_section:add_item(background_legend)
+
+ local legend_icons = {}
+ for icon in string.gmatch("!\"#$%&'*+,-/\\0123456789:;?@_`abcdefghijklmnopqrstuvwzyz~", ".") do
+ legend_icons[#legend_icons + 1] = icon
+ end
+
+ for _, level in pairs(levels_section.items) do
+ level:map_to_legend(legend_section, legend_icons)
+ end
+
+
+
+
+ local layers_section = pz_file:get_section("COLLISIONLAYERS")
+ for _, layer in ipairs(sprite.layers) do
+ local layer_item = PuzzleScriptLayerItem:new()
+ layer_item.id = layer.name
+ layers_section:add_item(layer_item)
+ end
+
+
+
+ for layer, objects in pairs(layer_objects) do
+ local legend_map = PuzzleScriptLegendMapping:new()
+ legend_map.id = layer.name
+ legend_map:add_objects(objects)
+ legend_section:add_item(legend_map)
+ end
+
+ for _, layer in ipairs(layers) do
+ if layer.isGroup and #layer.layers > 0 then
+ local legend_map = PuzzleScriptLegendMapping:new()
+ legend_map.id = layer.name
+ for _, sub_layer in ipairs(layer.layers) do
+ legend_map:add_objects({ {id=sub_layer.name} })
+ end
+ legend_section:add_item(legend_map)
+ end
+ end
+
+-- for _, objects in pairs(layer_objects) do
+-- for _, object in pairs(objects) do
+-- local layer_item = PuzzleScriptLayerItem:new()
+-- layers_section:add_item(layer_item)
+-- layer_item.id = object.layer.name
+-- end
+-- end
+
+
+-- for _, layer in ipairs(layers) do
+-- if layer.isVisible and layer.parent == sprite then
+
+
+
+--PuzzleScriptLegendMapping:new()
+
+-- local layer_item = PuzzleScriptLayerItem:new()
+-- layers_section:add_item(layer_item)
+-- layer_item.id = layer.name
+-- end
+-- end
+
+
+--legend stuff again after...
+
+
+
+ -- Merge after making everything else
+ pz_file:read(data.merge_filename)
+
+ pz_file:export(data.script_filename)
+
+ local export_filename_changed = sprite.properties(plugin_key).previous_export_filename ~= data.script_filename
+ local merge_filename_changed = sprite.properties(plugin_key).previous_merge_filename ~= data.merge_filename
+ local lofi_changed = sprite.properties(plugin_key).previous_export_lofi ~= data.lofi
+
+ if export_filename_changed or merge_filename_changed or lofi_changed then
+ sprite.properties(plugin_key).previous_export_filename = data.script_filename
+ sprite.properties(plugin_key).previous_merge_filename = data.merge_filename
+ sprite.properties(plugin_key).previous_export_lofi = data.lofi
+ end
+end
+
+main()