]> git.bts.cx Git - aseprite-tools.git/blob - export-puzzlescript.lua
31ef326a853ccd236d35cd7ffc62c73cba9cdb71
[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 = { "transparent" }
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         script_file:write(table.concat(self.colors, " "))
116         script_file:write("\n")
117
118         if self.pixels then
119                 for y = 0, 4 do
120                         for x = 0, 4 do
121                                 script_file:write(self.pixels[y * 5 + x + 1])
122                         end
123                         script_file:write("\n")
124                 end
125         end
126 end
127
128
129
130 local PuzzleScriptLayerItem = {}
131
132 function PuzzleScriptLayerItem:new(o)
133         o = o or {}
134         setmetatable(o, self)
135         self.__index = self
136         o.objects = {}
137         return o
138 end
139
140 function PuzzleScriptLayerItem:add_object(object)
141         self.objects[#self.objects + 1] = object
142 end
143
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
148         end
149         script_file:write(table.concat(object_ids, ", "))
150         script_file:write("\n")
151 end
152
153
154
155 local PuzzleScriptLegendItem = {}
156
157 function PuzzleScriptLegendItem:new(o)
158         o = o or {}
159         setmetatable(o, self)
160         self.__index = self
161 --      o.objects = {}
162         return o
163 end
164
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
171         end
172         return true     
173 end
174
175 function PuzzleScriptLegendItem:set_objects(objects)
176         self.objects = {}
177         for _, obj in pairs(objects) do
178                 table.insert(self.objects, obj)
179         end
180 end
181
182 function PuzzleScriptLegendItem:export(script_file)
183         local object_ids = {}
184
185         if self.objects then
186                 for i, obj in ipairs(self.objects) do
187                         object_ids[i] = obj.id
188                 end
189         else
190                 object_ids = { "Background" } -- FIXME
191         end
192
193         script_file:write(self.id)
194         script_file:write(" = ")
195         script_file:write(table.concat(object_ids, " and "))
196
197         script_file:write("\n")
198 end
199
200
201
202 local PuzzleScriptLegendMapping = {}
203
204 function PuzzleScriptLegendMapping:new(o)
205         o = o or {}
206         setmetatable(o, self)
207         self.__index = self
208         return o
209 end
210
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)
215         end
216 end
217
218 function PuzzleScriptLegendMapping:export(script_file)
219         local object_ids = {}
220
221         if self.objects then
222                 for i, obj in ipairs(self.objects) do
223                         object_ids[i] = obj.id
224                 end
225         end
226
227         -- Failsafe for matching name and single item
228         if #object_ids == 1 and object_ids[1] == self.id then return end
229
230         script_file:write(self.id)
231         script_file:write(" = ")
232         script_file:write(table.concat(object_ids, " or "))
233
234         script_file:write("\n")
235 end
236
237
238
239
240 local PuzzleScriptLevelItem = {}
241
242 function PuzzleScriptLevelItem:new(o)
243         o = o or {}
244         setmetatable(o, self)
245         self.__index = self
246         o.tiles = {}
247         return o
248 end
249
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] = {}
254         end
255
256         table.insert(self.tiles[tile_idx], value)
257 end
258
259 function PuzzleScriptLevelItem:map_to_legend(legend_section, legend_icons)
260         self.tile_legends = {}
261
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
265
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
270                                         break
271                                 end
272                         end
273
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)
281                         end
282
283                         self.tile_legends[tile_idx] = legend_item
284                 end
285         end
286 end
287
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)
293                 end
294                 script_file:write("\n")
295         end
296 end
297
298
299
300 local PuzzleScriptFile = {}
301
302 function PuzzleScriptFile:new(o)
303         o = o or {}
304         setmetatable(o, self)
305         self.__index = self
306         o.sections = {}
307         return o
308 end
309
310 function PuzzleScriptFile:get_section(id)
311         id = string.upper(id)
312         return findOrAdd(self.sections, id, PuzzleScriptSection:new{})
313 end
314
315 function PuzzleScriptFile:read(filename)
316         local script_file = io.open(filename, "r")
317
318         if script_file then
319                 local current_section = self:get_section("PRELUDE")
320                 local current_item = nil
321                 while true do
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 ===
328
329                                 current_item = nil
330                         elseif string.match(line, "^[ \t\r\n]*$") then
331                                 current_item = nil
332                         else
333                                 if current_item == nil then
334                                         current_item = PuzzleScriptRawItem:new{}
335                                         current_section:add_item(current_item)
336                                 end
337
338                                 current_item:add_line(line)
339                         end
340                 end
341
342                 io.close(script_file)
343         end
344 end
345
346 function PuzzleScriptFile:export(filename)
347         local script_file = io.open(filename, "w")
348
349         local ordered_section_names = {
350                 "PRELUDE",
351                 "OBJECTS",
352                 "LEGEND",
353                 "SOUNDS",
354                 "COLLISIONLAYERS",
355                 "RULES",
356                 "WINCONDITIONS",
357                 "LEVELS"
358         }
359
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")
371                         end
372
373                         for _, section_item in pairs(section.items) do
374                                 section_item:export(script_file)
375
376                                 if section_name ~= "COLLISIONLAYERS" and section_name ~= "LEGEND" then -- fixme, table??
377                                         script_file:write("\n")
378                                 end
379                         end
380
381                         if section_name == "COLLISIONLAYERS" or section_name == "LEGEND" then -- fixme, table??
382                                 script_file:write("\n")
383                         end
384                 end
385         end
386         
387         io.close(script_file)
388 end
389
390
391
392 function main()
393         local name = app.fs.fileTitle(sprite.filename)
394
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
398
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)
403         end
404
405         local dlg = Dialog()
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" }
411         dlg:show()
412
413         local data = dlg.data
414         if data.confirm == false then
415                 return
416         end
417
418         local layers = get_layers_flat({}, sprite)
419
420         local pz_file = PuzzleScriptFile:new{}
421
422
423         local object_section = pz_file:get_section("OBJECTS")
424
425         local layer_objects = {}
426
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
436
437                                 local used_tiles = {}
438                                 for _, frame in ipairs(sprite.frames) do
439                                         local cel = layer:cel(frame)
440                                         if cel ~= nil then
441                                                 for pixel in cel.image:pixels() do
442                                                         p = pc.tileI(pixel())
443                                                         if p > 0 then
444                                                                 used_tiles[p] = true
445                                                         end
446                                                 end
447                                         end
448                                 end
449
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
455                                         end
456                                 end
457
458                                 for tile_idx, _ in pairs(used_tiles) do
459                                         local object_item = PuzzleScriptObjectItem:new()
460                                         object_section:add_item(object_item)
461
462                                         if object_names[tile_idx] then
463                                                 object_item.id = object_names[tile_idx]
464                                         else
465                                                 object_item.id = layer.name..tile_idx
466                                         end
467
468                                         object_item.layer = layer
469
470                                         local tile = tileset:tile(tile_idx)
471
472                                         if tile then
473                                                 local layer_tiles = findOrAdd(layer_objects, layer)
474                                                 layer_tiles[tile_idx] = object_item
475
476                                                 local colors = {}
477                                                 local pixels = {}
478                                                 for pixel in tile.image:pixels() do
479                                                         object_item:set_pixel(pixel.x, pixel.y, Color(pixel()))
480                                                 end
481                                         else
482                                                 assert(false, "Missing tile: " .. layer.name .. "  " .. tile_idx)
483                                         end
484                                 end
485                         end
486                 end
487         end
488
489         local levels_section = pz_file:get_section("LEVELS")
490
491         local tiles_w = math.floor(sprite.width / 5)
492         local tiles_h = math.floor(sprite.height / 5)
493
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
499
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)
505                                                 if cel ~= nil then
506                                                         local image = cel.image
507
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)
512
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)
518                                                                 if tile_idx > 0 then
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])
521                                                                 end
522                                                         end
523                                                 end
524                                         end
525                                 end
526                         end
527                 end
528         end
529
530         local legend_section = pz_file:get_section("LEGEND")
531
532         local background_legend = PuzzleScriptLegendItem:new()
533         background_legend.id = "."
534         legend_section:add_item(background_legend)
535
536         local legend_icons = {}
537         for icon in string.gmatch("!\"#$%&'*+,-/\\0123456789:;?@_`abcdefghijklmnopqrstuvwzyz~", ".") do
538                 legend_icons[#legend_icons + 1] = icon
539         end
540
541         for _, level in pairs(levels_section.items) do
542                 level:map_to_legend(legend_section, legend_icons)
543         end
544
545
546
547
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)
553         end
554
555
556
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)
562         end
563
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} })
570                         end
571                         legend_section:add_item(legend_map)
572                 end
573         end
574
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
580 --              end
581 --      end
582
583
584 --      for _, layer in ipairs(layers) do
585 --              if layer.isVisible and layer.parent == sprite then
586
587
588
589 --PuzzleScriptLegendMapping:new()
590
591 --                      local layer_item = PuzzleScriptLayerItem:new()
592 --                      layers_section:add_item(layer_item)
593 --                      layer_item.id = layer.name
594 --              end
595 --      end
596
597
598 --legend stuff again after...
599
600
601
602         -- Merge after making everything else
603         pz_file:read(data.merge_filename)
604
605         pz_file:export(data.script_filename)
606
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
610
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
615         end
616 end
617
618 main()