author_engine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +7 -0
  4. data/API.md +81 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +26 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +38 -0
  9. data/Rakefile +10 -0
  10. data/SAVEFILE.md +23 -0
  11. data/assets/fonts/Connection.otf +0 -0
  12. data/assets/fonts/ConnectionBold.otf +0 -0
  13. data/assets/fonts/README.md +3 -0
  14. data/assets/fonts/SIL Open Font License.txt +94 -0
  15. data/assets/ui/bucket_icon.png +0 -0
  16. data/assets/ui/code_icon.png +0 -0
  17. data/assets/ui/error_icon.png +0 -0
  18. data/assets/ui/level_icon.png +0 -0
  19. data/assets/ui/loading_icon.png +0 -0
  20. data/assets/ui/lock_icon.png +0 -0
  21. data/assets/ui/pencil_icon.png +0 -0
  22. data/assets/ui/play_icon.png +0 -0
  23. data/assets/ui/sprite_icon.png +0 -0
  24. data/assets/ui/unlock_icon.png +0 -0
  25. data/author_engine.gemspec +41 -0
  26. data/bin/author_engine +4 -0
  27. data/lib/author_engine/button.rb +154 -0
  28. data/lib/author_engine/code_editor/cursor.rb +339 -0
  29. data/lib/author_engine/code_editor/highlighting.rb +49 -0
  30. data/lib/author_engine/container.rb +24 -0
  31. data/lib/author_engine/containers/editor.rb +97 -0
  32. data/lib/author_engine/containers/loader.rb +105 -0
  33. data/lib/author_engine/game/game.rb +26 -0
  34. data/lib/author_engine/game/parts/colors.rb +59 -0
  35. data/lib/author_engine/game/parts/common.rb +13 -0
  36. data/lib/author_engine/game/parts/graphics.rb +41 -0
  37. data/lib/author_engine/game/parts/input.rb +25 -0
  38. data/lib/author_engine/image.rb +53 -0
  39. data/lib/author_engine/palette.rb +114 -0
  40. data/lib/author_engine/save_file.rb +134 -0
  41. data/lib/author_engine/sprite.rb +4 -0
  42. data/lib/author_engine/sprite_picker.rb +154 -0
  43. data/lib/author_engine/support.rb +22 -0
  44. data/lib/author_engine/text.rb +41 -0
  45. data/lib/author_engine/version.rb +3 -0
  46. data/lib/author_engine/view.rb +55 -0
  47. data/lib/author_engine/views/code_editor.rb +154 -0
  48. data/lib/author_engine/views/level_editor.rb +7 -0
  49. data/lib/author_engine/views/play_viewer.rb +163 -0
  50. data/lib/author_engine/views/sprite_editor.rb +333 -0
  51. data/lib/author_engine/window.rb +132 -0
  52. data/lib/author_engine.rb +32 -0
  53. data/test_3.authorengine +519 -0
  54. data/testing.authorengine +578 -0
  55. metadata +169 -0
@@ -0,0 +1,154 @@
1
+ class AuthorEngine
2
+ class CodeEditor < View
3
+
4
+ class CodeInput < Gosu::TextInput
5
+ def filter(text_in)
6
+ return text_in
7
+ end
8
+ end
9
+
10
+ DEFAULT_STRING = <<-EOF
11
+ def init
12
+ @size = 10
13
+ @x = width/2 - @size/2
14
+ @y = height/2 - @size/2
15
+ end
16
+
17
+ def draw
18
+ rect(@x, @y, @size, @size, pink)
19
+ text("x: \#{@x}, y: \#{@y}", 0, 0, 8)
20
+ end
21
+
22
+ def update
23
+ @x+=1 if button?("right")
24
+ @x-=1 if button?("left")
25
+ @y-=1 if button?("up")
26
+ @y+=1 if button?("down")
27
+
28
+ @x = 0 if @x < 0
29
+ @x = 128-@size if @x > 128-@size
30
+
31
+ @y = 0 if @y < 0
32
+ @y = 128-@size if @y > 128-@size
33
+ end
34
+ EOF
35
+
36
+
37
+ attr_accessor :x_offset, :y_offset
38
+ def setup
39
+ @font_size = 5 * window.square_scale.floor
40
+ @font = Gosu::Font.new(@font_size, name: Text::FONT_DEFAULT_BOLD) # "Consolas"
41
+ @line_numbers_spacing = "00"
42
+ @line_numbers_width = @font.text_width(@line_numbers_spacing)
43
+
44
+ @text_input = CodeInput.new
45
+ if window.container.savefile.code.nil?
46
+ @text_input.text = DEFAULT_STRING
47
+ else
48
+ @text_input.text = window.container.savefile.code
49
+ end
50
+ @text = AuthorEngine::Text.new(message: "", size: @font_size, x: @line_numbers_width+@x_padding, y: window.container.header_height, font: Text::FONT_DEFAULT) # "DejaVu Sans Mono"
51
+
52
+ @cursor = Cursor.new(view: self, text_input: @text_input, text: @text)
53
+ @highlighting = Highlighting.new
54
+
55
+ @x_offset, @y_offset = 0, 0
56
+ end
57
+
58
+ def focus
59
+ window.text_input = @text_input
60
+ window.caption = "Code Editor"
61
+ end
62
+
63
+ def blur
64
+ window.text_input = nil
65
+ end
66
+
67
+ def draw
68
+ # Gosu.draw_rect(0, window.container.header_height, @width, @height, white)
69
+ super
70
+ Gosu.clip_to(0, window.container.header_height, window.width, window.height - window.container.header_height) do
71
+ Gosu.draw_rect(0, window.container.header_height, @line_numbers_width, @height, dark_gray)
72
+
73
+ Gosu.translate(0, @y_offset) do
74
+ min_width = @font.text_width("0")+@x_padding
75
+ (@text.message.lines.map(&:chomp)).each_with_index do |line, index|
76
+ min_width = @font.text_width("#{index+1}") if @font.text_width("#{index+1}") > min_width
77
+
78
+ @font.draw_text("#{index+1}", 1, window.container.header_height + (@font.height * index), 0)
79
+ end
80
+
81
+ @line_numbers_width = min_width
82
+ @text.x = @line_numbers_width+@x_padding
83
+ end
84
+ end
85
+
86
+ Gosu.clip_to(@line_numbers_width, window.container.header_height, window.width, @height) do
87
+ Gosu.translate(@x_offset, @y_offset) do
88
+ @text.draw_markup
89
+ @cursor.draw
90
+ end
91
+ end
92
+ end
93
+
94
+ def update
95
+ super
96
+ # @text_input.text+="\n" if Gosu.button_down?(Gosu::KbEnter) || Gosu.button_down?(Gosu::KbReturn) # FIXME
97
+ @caret_pos = @text_input.caret_pos
98
+ @highlighting.highlight(string: @text_input.text, text: @text)
99
+
100
+ @cursor.update
101
+ end
102
+
103
+ def code; @text_input.text; end
104
+
105
+ def button_down(id)
106
+ @cursor.button_down(id)
107
+ end
108
+
109
+ def button_up(id)
110
+ cursor_pos = @text_input.caret_pos # get a copy of the current cursor location
111
+
112
+ if id == Gosu::KbEnter || id == Gosu::KbReturn
113
+ # raise if @caret_pos != @text_input.caret_pos
114
+ @text_input.text = @text_input.text.insert(@text_input.caret_pos, "\n")
115
+ @cursor.set_position(cursor_pos+1)
116
+ end
117
+
118
+ @cursor.move(:up) if id == Gosu::KbUp
119
+ @cursor.move(:down) if id == Gosu::KbDown
120
+
121
+ if id == Gosu::KbTab
122
+ text = @text_input.text
123
+
124
+ if window.shift_button_down?
125
+ # FIXME: remove spaces behide cursor
126
+
127
+ # chars = @text_input.text.chars
128
+
129
+ # if text[cursor_pos] == " " && text[cursor_pos-1] == " "
130
+ # chars.delete_at(cursor_pos-1)
131
+ # chars.delete_at(cursor_pos)
132
+ # @cursor.set_position(cursor_pos-2)
133
+
134
+ # text = chars.join
135
+ # elsif text[cursor_pos] == " "
136
+ # chars.delete_at(cursor_pos-1)
137
+ # @cursor.set_position(cursor_pos-1)
138
+
139
+ # text = chars.join
140
+ # else
141
+ # p text[cursor_pos]
142
+ # end
143
+
144
+ else
145
+ @text_input.text = @text_input.text.insert(cursor_pos, " ")
146
+ p cursor_pos+2
147
+ @cursor.set_position(cursor_pos+2)
148
+ end
149
+ end
150
+
151
+ @cursor.button_up(id)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,7 @@
1
+ class AuthorEngine
2
+ class LevelEditor < View
3
+ def focus
4
+ window.caption = "Level Editor"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,163 @@
1
+ class AuthorEngine
2
+ class PlayViewer < View
3
+ def setup
4
+ @running = false
5
+
6
+ @error_icon = AuthorEngine::Image.new("assets/ui/error_icon.png", retro: true)
7
+ @error_icon_color = 0
8
+ @error_icon_colors = [yellow, orange]
9
+
10
+ @loading_icon = AuthorEngine::Image.new("assets/ui/loading_icon.png", retro: true)
11
+ @loading_icon_angle = 0
12
+ @loading_icon_rot_step = 45
13
+
14
+ @icon_rot_period = 500
15
+ @last_icon_rot = Gosu.milliseconds
16
+
17
+ @error_message = Text.new(message: "", x: @x_padding, y: window.container.header_height, z: Float::INFINITY)
18
+
19
+ @text = Text.new(message: "Press [Control+P] to Play")
20
+ @text.x = window.width/2 - @text.width/2
21
+ @text.y = window.height/2 + @text.height + @loading_icon.height * window.square_scale
22
+
23
+ @last_error = nil
24
+ end
25
+
26
+ def run(code: nil)
27
+ @last_error = nil
28
+ @game = Game.new(code: code)
29
+
30
+ @running = true
31
+ window.show_cursor = false
32
+ window.container.lock
33
+
34
+ @game.init
35
+ end
36
+
37
+ def catch_error(&block)
38
+ begin
39
+ block.call if block
40
+ rescue => error
41
+ error_response(error)
42
+ rescue SyntaxError => error
43
+ error_response(error)
44
+ end
45
+ end
46
+
47
+ def error_response(error)
48
+ stop
49
+
50
+ @last_error = error
51
+ format_error(text: @error_message, error: error)
52
+ end
53
+
54
+ def format_error(text:, error:)
55
+ puts "#{error.class}: #{error.message}"
56
+ puts error.backtrace.join("\n")
57
+
58
+ max_width = window.width - (@x_padding*2)
59
+ char_width= text.font.text_width("0")
60
+ chars_line= (max_width.to_f / char_width.to_f).ceil
61
+
62
+ backtrace = "<c=#{xml_color(orange)}>#{error.class}</c>\nBacktrace:\n"
63
+ error.backtrace.each {|trace| next unless trace.include?("(eval)"); backtrace+=" #{trace}\n"}
64
+ trace_buffer = "#{error.message}"
65
+ buffer = ""
66
+
67
+ trace_buffer.lines do |line|
68
+ line.chomp.chars.each_slice(chars_line).each do |slice|
69
+ string = slice.join
70
+ buffer += " #{string}\n"
71
+ end
72
+ end
73
+
74
+ text.message = "#{backtrace}#{buffer}"
75
+ end
76
+
77
+ def focus
78
+ window.caption = "Play"
79
+ play
80
+ end
81
+
82
+ def play
83
+ catch_error do
84
+ run(code: code_editor.code)
85
+ end
86
+ end
87
+
88
+ def draw
89
+ if @running
90
+ Gosu.draw_rect(0, 0, window.width, window.height, @background)
91
+ Gosu.flush
92
+ catch_error do
93
+ draw_game
94
+ end
95
+ else
96
+ super
97
+ if @last_error
98
+ Gosu.scale(2,2, (window.width / 2) - (@error_icon.width / 2), (window.height / 2) - (@error_icon.height / 2)) do
99
+ @error_icon.draw_rot(window.width / 2, window.height / 2, 10, 0, 0.5, 0.5, 1 * window.square_scale, 1 * window.square_scale, @error_icon_colors[@error_icon_color])
100
+ end
101
+ @error_message.draw_markup
102
+ else
103
+ @loading_icon.draw_rot(window.width / 2, window.height / 2, 10, @loading_icon_angle, 0.5, 0.5, 1 * window.square_scale, 1 * window.square_scale)
104
+ @text.draw
105
+ end
106
+ end
107
+ end
108
+
109
+ def update
110
+ # super
111
+ if @running
112
+ catch_error do
113
+ update_game
114
+ end
115
+ else
116
+ if (Gosu.milliseconds - @last_icon_rot) > @icon_rot_period
117
+ @loading_icon_angle = (@loading_icon_angle + @loading_icon_rot_step) % 360
118
+ @error_icon_color += 1
119
+ @error_icon_color = 0 if @error_icon_color == @error_icon_colors.size
120
+ @last_icon_rot = Gosu.milliseconds
121
+ end
122
+ end
123
+ end
124
+
125
+ def draw_game
126
+ x_offset = 0
127
+ ratio = Window::VIEW_WIDTH / window.width
128
+ if window.scale_x != window.scale_y
129
+ x_offset = window.width / 2 - (window.width * ratio * window.square_scale) / 2
130
+ end
131
+
132
+ Gosu.clip_to(x_offset, 0, (window.base_size * window.square_scale), (window.base_size * window.square_scale)) do
133
+ Gosu.translate(x_offset, 0) do
134
+ Gosu.scale(window.square_scale, window.square_scale) do
135
+ @game.draw_background
136
+ @game.draw
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def update_game
143
+ @game.update if @game
144
+ end
145
+
146
+ def stop
147
+ @running = false
148
+ window.show_cursor = true
149
+ window.container.unlock
150
+ end
151
+
152
+ def button_up(id)
153
+ if id == Gosu::MsLeft && window.mouse_x.between?((window.width / 2) - (@loading_icon.width/2) * window.square_scale, (window.width / 2) + (@loading_icon.width/2) * window.square_scale)
154
+ if window.mouse_y.between?((window.height / 2) - (@loading_icon.height/2) * window.square_scale, (window.height / 2) + (@loading_icon.height/2) * window.square_scale)
155
+ play
156
+ end
157
+ end
158
+
159
+ stop if id == Gosu::KbEscape
160
+ play if id == Gosu::KbP && window.control_button_down?
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,333 @@
1
+ class AuthorEngine
2
+ class SpriteEditor < View
3
+ BLANK_COLOR = Gosu::Color.rgba(0,0,0, 0)
4
+ class Pixel
5
+ attr_accessor :x, :y, :width, :height, :color
6
+ def initialize(x, y, width, height, color)
7
+ @x, @y, @width, @height, @color = x, y, width, height, color
8
+ end
9
+
10
+ def draw
11
+ Gosu.draw_rect(
12
+ @x, @y,
13
+ @width, @height,
14
+ @color,
15
+ 5
16
+ )
17
+ end
18
+ end
19
+
20
+ def setup
21
+ @pixels = []
22
+ @active_color = Gosu::Color.rgba(101,1,101, 255)
23
+
24
+ create_grid(16, 16, 4)
25
+ @canvas_changed = false
26
+ @scale = 1 * window.square_scale
27
+ @palette = Palette.new(x: @grid_x + @grid_width + @grid_pixel_size, y: @grid_y)
28
+ @sprites_picker = SpritePicker.new(y: @grid_y + @grid_height + (@grid_pixel_size * 2))
29
+ @coordinates = Text.new(message: "1:1", x: @scale, y: @grid_y)
30
+
31
+ @sprites= Array.new(@sprites_picker.rows*@sprites_picker.columns, nil)
32
+
33
+ @tools = []
34
+ @pixel_lock = false
35
+ @pixel_floodfill = false # aka bucket tool
36
+
37
+ @tools << Button.new(image: "assets/ui/unlock_icon.png", tooltip: "Toggle pixel lock", x: @palette.x, y: @palette.y + @palette.height + (window.square_scale * 2), color: dark_purple) do |b|
38
+ @unlock_icon ||= b.image
39
+ @lock_icon ||= AuthorEngine::Image.new("assets/ui/lock_icon.png", retro: true)
40
+
41
+ @pixel_lock = !@pixel_lock
42
+
43
+ if @pixel_lock
44
+ b.image = @lock_icon
45
+ else
46
+ b.image = @unlock_icon
47
+ end
48
+ end
49
+
50
+ @pencil_icon = AuthorEngine::Image.new("assets/ui/pencil_icon.png", retro: true)
51
+ @bucket_icon = AuthorEngine::Image.new("assets/ui/bucket_icon.png", retro: true)
52
+
53
+ @tools << Button.new(image: @pencil_icon, tooltip: "Toggle pencil/bucket", x: @palette.x + @tools.first.width + 1, y: @palette.y + @palette.height + (window.square_scale * 2), color: dark_purple) do |b|
54
+ @pixel_floodfill = !@pixel_floodfill
55
+
56
+ if @pixel_floodfill
57
+ b.image = @bucket_icon
58
+ else
59
+ b.image = @pencil_icon
60
+ end
61
+ end
62
+
63
+ import_spritesheet(window.container.savefile.sprites)
64
+ end
65
+
66
+ def focus
67
+ window.show_cursor = true
68
+ window.caption = "Sprite Editor"
69
+ end
70
+
71
+ def draw
72
+ super
73
+ @pixels.each(&:draw)
74
+ highlight_pixel
75
+ @coordinates.draw
76
+
77
+ Gosu.draw_rect(@grid_x-window.square_scale, @grid_y-window.square_scale, @grid_width+(window.square_scale*2), @grid_height+(window.square_scale*2), Gosu::Color::WHITE)
78
+ Gosu.draw_rect(@grid_x, @grid_y, @grid_width, @grid_height, Gosu::Color.rgba(10, 10, 10, 200))
79
+ @palette.draw
80
+ @sprites_picker.draw
81
+
82
+ @tools.each(&:draw)
83
+
84
+ Gosu.clip_to(@grid_x, @grid_y, @grid_width, @grid_height) do
85
+ if @pixel_floodfill
86
+ @bucket_icon.draw(window.mouse_x, window.mouse_y - (@bucket_icon.width * @scale), 1000, @scale, @scale)
87
+ else
88
+ @pencil_icon.draw(window.mouse_x, window.mouse_y - (@pencil_icon.width * @scale), 1000, @scale, @scale)
89
+ end
90
+ end
91
+ end
92
+
93
+ def update
94
+ super
95
+ unless @pixel_floodfill
96
+ paint if Gosu.button_down?(Gosu::MsLeft)
97
+ erase if Gosu.button_down?(Gosu::MsRight)
98
+ end
99
+ @palette.update
100
+
101
+ update_coordinates
102
+ end
103
+
104
+ def update_coordinates
105
+ x = normalize_x(window.mouse_x)
106
+ y = normalize_y(window.mouse_y)
107
+ return if (x >= @grid_columns || y >= @grid_rows)
108
+ return if (x < 0 || y < 0)
109
+
110
+ @coordinates.message = "#{x+1}:#{y+1}"
111
+ end
112
+
113
+ def create_grid(x, y, size)
114
+ size = size * window.square_scale
115
+
116
+ @grid_x = (window.width / 2) - (((size * x) / 2) + size*2 )
117
+ @grid_y = window.container.header_height + size
118
+ @grid_width = x * size
119
+ @grid_height = y * size
120
+ @grid_pixel_size = size
121
+ @grid_columns = x
122
+ @grid_rows = y
123
+
124
+ y.times do |_y|
125
+ x.times do |_x|
126
+ @pixels << Pixel.new(
127
+ @grid_x+(_x*size), @grid_y+(_y*size),
128
+ size, size,
129
+ BLANK_COLOR
130
+ )
131
+ end
132
+ end
133
+ end
134
+
135
+ def highlight_pixel
136
+ return unless @palette.color
137
+
138
+ pixel = get_pixel_at(window.mouse_x, window.mouse_y)
139
+ return unless pixel
140
+ Gosu.draw_rect(
141
+ pixel.x, pixel.y,
142
+ pixel.width, pixel.height,
143
+ @palette.color,
144
+ 6
145
+ )
146
+
147
+ Gosu.draw_rect(
148
+ pixel.x, pixel.y,
149
+ pixel.width, pixel.height,
150
+ Gosu::Color.rgba(255,255,255, 100),
151
+ 6
152
+ )
153
+ end
154
+
155
+ def paint(color = @palette.color)
156
+ pixel = get_pixel_at(window.mouse_x, window.mouse_y)
157
+
158
+ return unless pixel
159
+ return if color.nil?
160
+ return if pixel.color != BLANK_COLOR && @pixel_lock
161
+
162
+ pixel.color = color
163
+ @canvas_changed = true
164
+ end
165
+
166
+ def erase
167
+ paint(BLANK_COLOR)
168
+ end
169
+
170
+ def get_pixel_at(x, y)
171
+ return if (x >= @grid_x+@grid_width || y >= @grid_y+@grid_height)
172
+ x = normalize_x(x)
173
+ y = normalize_y(y)
174
+ return if (x < 0 || y < 0)
175
+
176
+ @pixels[(x + (@grid_columns * y))]
177
+ end
178
+
179
+ def normalize_x(int)
180
+ return ((int - @grid_x) / @grid_pixel_size).floor
181
+ end
182
+
183
+ def normalize_y(int)
184
+ return ((int - @grid_y) / @grid_pixel_size).floor
185
+ end
186
+
187
+ # AKA The Bucket Tool
188
+ # @param pixel [Pixel]
189
+ # @param target_color [Gosu::Color] color to search and replace with replacement_color
190
+ # @param replacement_color [Gosu::Color] color to replace Pixel's current color
191
+ def floodfill(pixel, target_color, replacement_color)
192
+ return unless pixel
193
+ return if pixel.color == replacement_color
194
+ return if pixel.color != target_color
195
+ return if target_color != BLANK_COLOR && @pixel_lock # don't replace non-blank pixels with color if pixels are locked
196
+
197
+ pixel.color = replacement_color
198
+ @canvas_changed = true
199
+
200
+ # UP
201
+ _pixel = get_pixel_at(pixel.x, pixel.y - @grid_pixel_size)
202
+ floodfill(_pixel, target_color, replacement_color)
203
+
204
+ # DOWN
205
+ _pixel = get_pixel_at(pixel.x, pixel.y + @grid_pixel_size)
206
+ floodfill(_pixel, target_color, replacement_color)
207
+
208
+ # LEFT
209
+ _pixel = get_pixel_at(pixel.x - @grid_pixel_size, pixel.y)
210
+ floodfill(_pixel, target_color, replacement_color)
211
+
212
+ # RIGHT
213
+ _pixel = get_pixel_at(pixel.x + @grid_pixel_size, pixel.y)
214
+ floodfill(_pixel, target_color, replacement_color)
215
+ end
216
+
217
+ def sprites
218
+ @sprites
219
+ end
220
+
221
+ def set_sprite
222
+ if @sprites[@sprites_picker.active_sprite]
223
+ sprite_pixels(@sprites[@sprites_picker.active_sprite])
224
+ else
225
+ @pixels.each do |pixel|
226
+ pixel.color = BLANK_COLOR
227
+ end
228
+ end
229
+ end
230
+
231
+ def sprite_pixels(image)
232
+ pixels = []
233
+ image.to_blob.bytes.to_a.each_slice(4).each_with_index do |pixel|
234
+ buffer = []
235
+ pixel.each do |chunk|
236
+ buffer << Integer(chunk)
237
+ end
238
+ pixels << buffer
239
+ end
240
+
241
+ pixels.each_with_index do |pixel, index|
242
+ @pixels[index].color = Gosu::Color.rgba(pixel[0], pixel[1], pixel[2], pixel[3])
243
+ end
244
+ end
245
+
246
+ def update_sprite
247
+ list = []
248
+
249
+ @pixels.each_slice(window.sprite_size).each do |row|
250
+ list << row
251
+ end
252
+
253
+ image = Gosu.render(window.sprite_size, window.sprite_size, retro: true) do
254
+ list.each_with_index do |row, y|
255
+ row.each_with_index do |pixel, x|
256
+ Gosu.draw_rect(x, y, 1, 1, pixel.color)
257
+ end
258
+ end
259
+ end
260
+
261
+ @sprites[@sprites_picker.active_sprite] = nil # release image for garbage collection?
262
+ @sprites[@sprites_picker.active_sprite] = image
263
+ @canvas_changed = false
264
+ end
265
+
266
+ def copy_sprite
267
+ @copied_pixels = []
268
+ @pixels.each {|pixel| @copied_pixels << pixel.dup}
269
+ end
270
+
271
+ def paste_sprite
272
+ if @copied_pixels
273
+ @pixels.each_with_index do |pixel, i|
274
+ pixel.color = @copied_pixels[i].color
275
+ end
276
+ @canvas_changed = true
277
+ update_sprite
278
+ end
279
+
280
+ @copied_pixels = nil
281
+ end
282
+
283
+ def spritesheet
284
+ sheet = Gosu.render(512, 128, retro: true) do
285
+ @sprites.each_slice(512/16).each_with_index do |row, y|
286
+ row.each_with_index do |sprite, x|
287
+ next if sprite.nil?
288
+ sprite.draw(x * 16, y * 16, 0)
289
+ end
290
+ end
291
+ end
292
+
293
+ return sheet
294
+ end
295
+
296
+ def import_spritesheet(data)
297
+ return if data.to_blob.size < 4
298
+ sprites = Gosu::Image.load_tiles(data, 16, 16, retro: true, tileable: true)
299
+ @sprites.clear
300
+
301
+ sprites.each do |sprite|
302
+ @sprites.push(sprite)
303
+ end
304
+
305
+ set_sprite
306
+ end
307
+
308
+ def button_down(id)
309
+ super
310
+
311
+ copy_sprite if window.control_button_down? && id == Gosu::KbC
312
+ paste_sprite if window.control_button_down? && id == Gosu::KbV
313
+ end
314
+
315
+ def button_up(id)
316
+ super
317
+ @palette.button_up(id)
318
+ @sprites_picker.button_up(id)
319
+
320
+ @tools.each{ |b| b.button_up(id) }
321
+
322
+ if @pixel_floodfill
323
+ pixel = get_pixel_at(window.mouse_x, window.mouse_y)
324
+ if pixel
325
+ floodfill(pixel, pixel.color, @palette.color) if id == Gosu::MsLeft if @palette.color
326
+ floodfill(pixel, pixel.color, BLANK_COLOR) if id == Gosu::MsRight
327
+ end
328
+ end
329
+
330
+ update_sprite if (id == Gosu::MsLeft || id == Gosu::MsRight) && @canvas_changed
331
+ end
332
+ end
333
+ end