]> git.bts.cx Git - aseprite-tools.git/blob - export-puzzlescript.lua
Added support for specific frame export
[aseprite-tools.git] / export-puzzlescript.lua
1 local plugin_key = "bts/puzzlescript"
2
3 local sprite = app.sprite
4 local pc = app.pixelColor
5
6
7
8 function hexColor(c)
9         return string.format("#%02x%02x%02x", c.red, c.green, c.blue)
10 end
11
12 function findOrInsertIdx(table, value)
13         for i=1,#table do
14                 if table[i] == value then
15                         return i
16                 end
17         end
18         table[#table + 1] = value
19         return #table
20 end
21
22 function findOrAdd(table, idx, default)
23         if table[idx] == nil then
24                 table[idx] = default or {}
25         end
26         return table[idx]
27 end
28
29 function get_layers_flat(output, layer_container)
30         for _, layer in ipairs(layer_container.layers) do
31                 table.insert(output, layer)
32                 if layer.isGroup then
33                         get_layers_flat(output, layer)
34                 end
35         end
36
37         return output
38 end
39
40
41
42 local PuzzleScriptSection = {}
43
44 function PuzzleScriptSection:new(o)
45         o = o or {}
46         setmetatable(o, self)
47         self.__index = self
48         o.items = {}
49         return o
50 end
51
52 function PuzzleScriptSection:add_item(item)
53         self.items[#self.items + 1] = item
54 end
55
56
57
58 local PuzzleScriptRawItem = {}
59
60 function PuzzleScriptRawItem:new(o)
61         o = o or {}
62         setmetatable(o, self)
63         self.__index = self
64         o.lines = {}
65         return o
66 end
67
68 function PuzzleScriptRawItem:add_line(line)
69         self.lines[#self.lines + 1] = line
70 end
71
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")
76         end
77 end
78
79
80
81 local PuzzleScriptObjectItem = {}
82
83 function PuzzleScriptObjectItem:new(o)
84         o = o or {}
85         setmetatable(o, self)
86         self.__index = self
87         o.colors = {}
88         o.pixels = {}
89         return o
90 end
91
92 function PuzzleScriptObjectItem:set_color(color)
93         if color.alpha == 255 then
94                 self.colors = { hexColor(color) }
95         else
96                 self.colors = {}
97         end
98         self.pixels = nil
99 end
100
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
106         end
107
108         self.pixels[pixel_idx] = pixel_value
109 end
110
111 function PuzzleScriptObjectItem:export(script_file)
112         script_file:write(self.id)
113         script_file:write("\n")
114
115         if #self.colors > 0 then
116                 script_file:write(table.concat(self.colors, " "))
117                 script_file:write("\n")
118
119                 if self.pixels then
120                         for y = 0, 4 do
121                                 for x = 0, 4 do
122                                         script_file:write(self.pixels[y * 5 + x + 1])
123                                 end
124                                 script_file:write("\n")
125                         end
126                 end
127         else
128                 script_file:write("transparent\n")
129         end
130 end
131
132
133
134 local PuzzleScriptLayerItem = {}
135
136 function PuzzleScriptLayerItem:new(o)
137         o = o or {}
138         setmetatable(o, self)
139         self.__index = self
140         o.objects = {}
141         return o
142 end
143
144 function PuzzleScriptLayerItem:add_object(object)
145         self.objects[#self.objects + 1] = object
146 end
147
148 function PuzzleScriptLayerItem:export(script_file)
149         local object_ids = { self.id }
150         for i, obj in ipairs(self.objects) do
151                 object_ids[i] = obj.id
152         end
153         script_file:write(table.concat(object_ids, ", "))
154         script_file:write("\n")
155 end
156
157
158
159 local PuzzleScriptLegendItem = {}
160
161 function PuzzleScriptLegendItem:new(o)
162         o = o or {}
163         setmetatable(o, self)
164         self.__index = self
165 --      o.objects = {}
166         return o
167 end
168
169 function PuzzleScriptLegendItem:obj_match(objects)
170         if objects == nil and self.objects == nil then return true end
171         if objects == nil or self.objects == nil then return false end
172         if #objects ~= #self.objects then return false end
173         for i = 1, #self.objects do
174                 if objects[i] ~= self.objects[i] then return false end
175         end
176         return true     
177 end
178
179 function PuzzleScriptLegendItem:set_objects(objects)
180         self.objects = {}
181         for _, obj in pairs(objects) do
182                 table.insert(self.objects, obj)
183         end
184 end
185
186 function PuzzleScriptLegendItem:export(script_file)
187         local object_ids = {}
188
189         if self.objects then
190                 for i, obj in ipairs(self.objects) do
191                         object_ids[i] = obj.id
192                 end
193         else
194                 object_ids = { "Background" } -- FIXME
195         end
196
197         script_file:write(self.id)
198         script_file:write(" = ")
199         script_file:write(table.concat(object_ids, " and "))
200
201         script_file:write("\n")
202 end
203
204
205
206 local PuzzleScriptLegendMapping = {}
207
208 function PuzzleScriptLegendMapping:new(o)
209         o = o or {}
210         setmetatable(o, self)
211         self.__index = self
212         return o
213 end
214
215 function PuzzleScriptLegendMapping:add_objects(objects)
216         self.objects = self.objects or {}
217         for _, obj in pairs(objects) do
218                 table.insert(self.objects, obj)
219         end
220 end
221
222 function PuzzleScriptLegendMapping:export(script_file)
223         local object_ids = {}
224
225         if self.objects then
226                 for i, obj in ipairs(self.objects) do
227                         object_ids[i] = obj.id
228                 end
229         end
230
231         -- Failsafe for matching name and single item
232         if #object_ids == 1 and object_ids[1] == self.id then return end
233
234         script_file:write(self.id)
235         script_file:write(" = ")
236         script_file:write(table.concat(object_ids, " or "))
237
238         script_file:write("\n")
239 end
240
241
242
243
244 local PuzzleScriptLevelItem = {}
245
246 function PuzzleScriptLevelItem:new(o)
247         o = o or {}
248         setmetatable(o, self)
249         self.__index = self
250         o.tiles = {}
251         return o
252 end
253
254 function PuzzleScriptLevelItem:set_tile(tile_x, tile_y, value)
255         local tile_idx = tile_y * self.width + tile_x
256         if self.tiles[tile_idx] == nil then
257                 self.tiles[tile_idx] = {}
258         end
259
260         table.insert(self.tiles[tile_idx], value)
261 end
262
263 function PuzzleScriptLevelItem:map_to_legend(legend_section, legend_icons)
264         self.tile_legends = {}
265
266         for tile_y = 0, self.height - 1 do
267                 for tile_x = 0, self.width - 1 do
268                         local tile_idx = tile_y * self.width + tile_x
269
270                         local legend_item = nil
271                         for _, test_legend_item in pairs(legend_section.items) do
272                                 if test_legend_item:obj_match(self.tiles[tile_idx]) then
273                                         legend_item = test_legend_item
274                                         break
275                                 end
276                         end
277
278                         if legend_item == nil then
279                                 legend_item = PuzzleScriptLegendItem:new()
280                                 assert(#legend_icons > 0)
281                                 legend_item.id = legend_icons[1]
282                                 table.remove(legend_icons, 1)
283                                 legend_item:set_objects(self.tiles[tile_idx])
284                                 legend_section:add_item(legend_item)
285                         end
286
287                         self.tile_legends[tile_idx] = legend_item
288                 end
289         end
290 end
291
292 function PuzzleScriptLevelItem:export(script_file)
293         for tile_y = 0, self.height - 1 do
294                 for tile_x = 0, self.width - 1 do
295                         local tile_idx = tile_y * self.width + tile_x
296                         script_file:write(self.tile_legends[tile_idx].id)
297                 end
298                 script_file:write("\n")
299         end
300 end
301
302
303
304 local PuzzleScriptFile = {}
305
306 function PuzzleScriptFile:new(o)
307         o = o or {}
308         setmetatable(o, self)
309         self.__index = self
310         o.sections = {}
311         return o
312 end
313
314 function PuzzleScriptFile:get_section(id)
315         id = string.upper(id)
316         return findOrAdd(self.sections, id, PuzzleScriptSection:new{})
317 end
318
319 function PuzzleScriptFile:read(filename)
320         local script_file = io.open(filename, "r")
321
322         if script_file then
323                 local current_section = self:get_section("PRELUDE")
324                 local current_item = nil
325                 while true do
326                         local line = script_file:read()
327                         if line == nil then break end
328                         if string.match(line, "^=+$") then
329                                 local current_section_name = script_file:read()
330                                 current_section = self:get_section(current_section_name)
331                                 script_file:read() -- Skip the next line as it's just the closing ===
332
333                                 current_item = nil
334                         elseif string.match(line, "^[ \t\r\n]*$") then
335                                 current_item = nil
336                         else
337                                 if current_item == nil then
338                                         current_item = PuzzleScriptRawItem:new{}
339                                         current_section:add_item(current_item)
340                                 end
341
342                                 current_item:add_line(line)
343                         end
344                 end
345
346                 io.close(script_file)
347         end
348 end
349
350 function PuzzleScriptFile:export(filename)
351         local script_file = io.open(filename, "w")
352
353         local ordered_section_names = {
354                 "PRELUDE",
355                 "OBJECTS",
356                 "LEGEND",
357                 "SOUNDS",
358                 "COLLISIONLAYERS",
359                 "RULES",
360                 "WINCONDITIONS",
361                 "LEVELS"
362         }
363
364         for _, section_name in pairs(ordered_section_names) do
365                 local section = self:get_section(section_name)
366                 if section ~= nil then
367                         if section_name ~= "PRELUDE" then
368                                 local separator = string.rep("=", string.len(section_name))
369                                 script_file:write(separator)
370                                 script_file:write("\n")
371                                 script_file:write(section_name)
372                                 script_file:write("\n")
373                                 script_file:write(separator)
374                                 script_file:write("\n\n")
375                         end
376
377                         for _, section_item in pairs(section.items) do
378                                 section_item:export(script_file)
379
380                                 if section_name ~= "COLLISIONLAYERS" and section_name ~= "LEGEND" then -- fixme, table??
381                                         script_file:write("\n")
382                                 end
383                         end
384
385                         if section_name == "COLLISIONLAYERS" or section_name == "LEGEND" then -- fixme, table??
386                                 script_file:write("\n")
387                         end
388                 end
389         end
390         
391         io.close(script_file)
392 end
393
394
395
396 function main()
397         local name = app.fs.fileTitle(sprite.filename)
398
399         local previous_export_filename = sprite.properties(plugin_key).previous_export_filename
400         local previous_merge_filename = sprite.properties(plugin_key).previous_merge_filename
401         local previous_export_selected_only = sprite.properties(plugin_key).previous_export_selected_only
402         local previous_export_lofi = sprite.properties(plugin_key).previous_export_lofi
403
404         if previous_export_filename == nil then
405                 local default_path = app.fs.filePath(sprite.filename)
406                 local default_filename = name..".txt"
407                 previous_export_filename = app.fs.joinPath(default_path, default_filename)
408         end
409
410         local dlg = Dialog()
411         dlg:file{ id="script_filename", label="Output:", filename=previous_export_filename, filetypes=".txt", save=true }
412         dlg:file{ id="merge_filename", label="Merge script (optional):", filename=previous_merge_filename, filetypes=".txt" }
413         dlg:check{ id="selected_only", label="Export selected level(s) only", selected=previous_export_selected_only }
414         dlg:check{ id="lofi", label="Prototype export (layer colours)", selected=previous_export_lofi }
415         dlg:button{ id="confirm", text="Confirm" }
416         dlg:button{ id="cancel", text="Cancel" }
417         dlg:show()
418
419         local data = dlg.data
420         if data.confirm == false then
421                 return
422         end
423
424         local layers = get_layers_flat({}, sprite)
425
426         local pz_file = PuzzleScriptFile:new{}
427
428
429         local object_section = pz_file:get_section("OBJECTS")
430
431         local layer_objects = {}
432
433         for _, layer in ipairs(layers) do
434                 if layer.isVisible then
435                         if layer.isImage == true and (layer.isTilemap == false or data.lofi) then
436                                 local object_item = PuzzleScriptObjectItem:new()
437                                 object_item.id = layer.name
438                                 object_section:add_item(object_item)
439                                 object_item:set_color(layer.color)
440                         elseif layer.isTilemap == true then
441                                 local tileset = layer.tileset
442
443                                 local used_tiles = {}
444                                 for _, frame in ipairs(sprite.frames) do
445                                         local cel = layer:cel(frame)
446                                         if cel ~= nil then
447                                                 for pixel in cel.image:pixels() do
448                                                         p = pc.tileI(pixel())
449                                                         if p > 0 then
450                                                                 used_tiles[p] = true
451                                                         end
452                                                 end
453                                         end
454                                 end
455
456                                 local object_names = {}
457                                 for object_name in string.gmatch(layer.data, "[^,]+") do
458                                         if object_name ~= "" then
459                                                 object_names[#object_names + 1] = object_name
460                                                 used_tiles[#object_names] = true -- Always export named tiles
461                                         end
462                                 end
463
464                                 for tile_idx, _ in pairs(used_tiles) do
465                                         local object_item = PuzzleScriptObjectItem:new()
466                                         object_section:add_item(object_item)
467
468                                         if object_names[tile_idx] then
469                                                 object_item.id = object_names[tile_idx]
470                                         else
471                                                 object_item.id = layer.name..tile_idx
472                                         end
473
474                                         object_item.layer = layer
475
476                                         local tile = tileset:tile(tile_idx)
477
478                                         if tile then
479                                                 local layer_tiles = findOrAdd(layer_objects, layer)
480                                                 layer_tiles[tile_idx] = object_item
481
482                                                 local colors = {}
483                                                 local pixels = {}
484                                                 for pixel in tile.image:pixels() do
485                                                         object_item:set_pixel(pixel.x, pixel.y, Color(pixel()))
486                                                 end
487                                         else
488                                                 assert(false, "Missing tile: " .. layer.name .. "  " .. tile_idx)
489                                         end
490                                 end
491                         end
492                 end
493         end
494
495         local levels_section = pz_file:get_section("LEVELS")
496
497         local tiles_w = math.floor(sprite.width / 5)
498         local tiles_h = math.floor(sprite.height / 5)
499
500         local exported_frames = sprite.frames
501
502         if data.selected_only then
503                 if app.range.isEmpty == false then
504                         exported_frames = app.range.frames
505                 else
506                         exported_frames = { app.frame }
507                 end
508         end
509
510         for _, frame in ipairs(exported_frames) do
511                 local level_item = PuzzleScriptLevelItem:new()
512                 levels_section:add_item(level_item)
513                 level_item.width = tiles_w
514                 level_item.height = tiles_h
515
516                 for tile_y = 0, tiles_h - 1 do
517                         for tile_x = 0, tiles_w - 1 do
518                                 for _, layer in ipairs(layers) do
519                                         if layer.isVisible and layer.isTilemap == true then
520                                                 local cel = layer:cel(frame)
521                                                 if cel ~= nil then
522                                                         local image = cel.image
523
524                                                         local offset_x = math.floor(cel.bounds.x / 5)
525                                                         local offset_y = math.floor(cel.bounds.y / 5)
526                                                         local cel_width = math.floor(cel.bounds.width / 5)
527                                                         local cel_height = math.floor(cel.bounds.height / 5)
528
529                                                         local actual_tile_x = tile_x - offset_x
530                                                         local actual_tile_y = tile_y - offset_y
531                                                         if actual_tile_x >= 0 and actual_tile_y >= 0 and actual_tile_x < cel_width and actual_tile_y < cel_height then
532                                                                 local pixel_value = image:getPixel(actual_tile_x, actual_tile_y)
533                                                                 local tile_idx = pc.tileI(pixel_value)
534                                                                 if tile_idx > 0 then
535                                                                         assert(layer_objects[layer][tile_idx], "Looking for: " .. layer.name)
536                                                                         level_item:set_tile(tile_x, tile_y, layer_objects[layer][tile_idx])
537                                                                 end
538                                                         end
539                                                 end
540                                         end
541                                 end
542                         end
543                 end
544         end
545
546         local legend_section = pz_file:get_section("LEGEND")
547
548         local background_legend = PuzzleScriptLegendItem:new()
549         background_legend.id = "."
550         legend_section:add_item(background_legend)
551
552         local legend_icons = {}
553         for icon in string.gmatch("!\"#$%&'*+,-/\\0123456789:;?@_`abcdefghijklmnopqrstuvwzyz~", ".") do
554                 legend_icons[#legend_icons + 1] = icon
555         end
556
557         for _, level in pairs(levels_section.items) do
558                 level:map_to_legend(legend_section, legend_icons)
559         end
560
561         local layers_section = pz_file:get_section("COLLISIONLAYERS")
562         for _, layer in ipairs(sprite.layers) do
563                 local layer_item = PuzzleScriptLayerItem:new()
564                 layer_item.id = layer.name
565                 layers_section:add_item(layer_item)
566         end
567
568
569
570         for layer, objects in pairs(layer_objects) do
571                 local legend_map = PuzzleScriptLegendMapping:new()
572                 legend_map.id = layer.name
573                 legend_map:add_objects(objects)
574                 legend_section:add_item(legend_map)
575         end
576
577         for _, layer in ipairs(layers) do
578                 if layer.isGroup and #layer.layers > 0 then
579                         local legend_map = PuzzleScriptLegendMapping:new()
580                         legend_map.id = layer.name
581                         for _, sub_layer in ipairs(layer.layers) do
582                                 legend_map:add_objects({ {id=sub_layer.name} })
583                         end
584                         legend_section:add_item(legend_map)
585                 end
586         end
587
588         -- Merge after making everything else
589         pz_file:read(data.merge_filename)
590
591         pz_file:export(data.script_filename)
592
593         local export_filename_changed = sprite.properties(plugin_key).previous_export_filename ~= data.script_filename
594         local merge_filename_changed = sprite.properties(plugin_key).previous_merge_filename ~= data.merge_filename
595         local selected_only_changed = sprite.properties(plugin_key).previous_export_selected_only ~= data.selected_only
596         local lofi_changed = sprite.properties(plugin_key).previous_export_lofi ~= data.lofi
597
598         if export_filename_changed or merge_filename_changed or selected_only_changed or lofi_changed then
599                 sprite.properties(plugin_key).previous_export_filename = data.script_filename
600                 sprite.properties(plugin_key).previous_merge_filename = data.merge_filename
601                 sprite.properties(plugin_key).previous_export_selected_only = data.selected_only
602                 sprite.properties(plugin_key).previous_export_lofi = data.lofi
603         end
604 end
605
606 main()