cyberarm_engine 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/assets/shaders/fragment/g_buffer.glsl +30 -0
  3. data/assets/shaders/fragment/lighting.glsl +69 -0
  4. data/assets/shaders/include/light_struct.glsl +11 -0
  5. data/assets/shaders/include/material_struct.glsl +16 -0
  6. data/assets/shaders/vertex/g_buffer.glsl +28 -0
  7. data/assets/shaders/vertex/lighting.glsl +24 -0
  8. data/lib/cyberarm_engine/background_image.rb +1 -1
  9. data/lib/cyberarm_engine/builtin/intro_state.rb +3 -3
  10. data/lib/cyberarm_engine/common.rb +14 -2
  11. data/lib/cyberarm_engine/console.rb +10 -10
  12. data/lib/cyberarm_engine/game_object.rb +1 -1
  13. data/lib/cyberarm_engine/game_state.rb +4 -0
  14. data/lib/cyberarm_engine/gosu_ext/draw_arc.rb +98 -0
  15. data/lib/cyberarm_engine/gosu_ext/draw_circle.rb +31 -0
  16. data/lib/cyberarm_engine/gosu_ext/draw_path.rb +17 -0
  17. data/lib/cyberarm_engine/model.rb +7 -6
  18. data/lib/cyberarm_engine/notification.rb +83 -0
  19. data/lib/cyberarm_engine/notification_manager.rb +242 -0
  20. data/lib/cyberarm_engine/opengl/renderer/g_buffer.rb +1 -0
  21. data/lib/cyberarm_engine/opengl/renderer/opengl_renderer.rb +16 -10
  22. data/lib/cyberarm_engine/opengl/renderer/renderer.rb +12 -1
  23. data/lib/cyberarm_engine/opengl/shader.rb +2 -2
  24. data/lib/cyberarm_engine/opengl.rb +13 -1
  25. data/lib/cyberarm_engine/stats.rb +181 -10
  26. data/lib/cyberarm_engine/text.rb +3 -0
  27. data/lib/cyberarm_engine/ui/border_canvas.rb +2 -2
  28. data/lib/cyberarm_engine/ui/element.rb +74 -26
  29. data/lib/cyberarm_engine/ui/elements/container.rb +95 -25
  30. data/lib/cyberarm_engine/ui/elements/edit_line.rb +6 -0
  31. data/lib/cyberarm_engine/ui/elements/image.rb +2 -2
  32. data/lib/cyberarm_engine/ui/elements/progress.rb +5 -0
  33. data/lib/cyberarm_engine/ui/elements/slider.rb +6 -3
  34. data/lib/cyberarm_engine/ui/elements/text_block.rb +19 -1
  35. data/lib/cyberarm_engine/ui/gui_state.rb +53 -27
  36. data/lib/cyberarm_engine/ui/style.rb +2 -1
  37. data/lib/cyberarm_engine/vector.rb +35 -16
  38. data/lib/cyberarm_engine/version.rb +1 -1
  39. data/lib/cyberarm_engine/window.rb +40 -8
  40. data/lib/cyberarm_engine.rb +8 -2
  41. data/mrbgem.rake +29 -0
  42. metadata +15 -3
@@ -0,0 +1,242 @@
1
+ module CyberarmEngine
2
+ class NotificationManager
3
+ EDGE_TOP = :top
4
+ EDGE_BOTTOM = :bottom
5
+ EDGE_RIGHT = :right
6
+ EDGE_LEFT = :left
7
+
8
+ MODE_DEFAULT = :slide
9
+ MODE_CIRCLE = :circle
10
+
11
+ attr_reader :edge, :mode, :max_visible, :notifications
12
+ def initialize(edge: EDGE_RIGHT, mode: MODE_DEFAULT, window:, max_visible: 1)
13
+ @edge = edge
14
+ @mode = mode
15
+ @window = window
16
+ @max_visible = max_visible
17
+
18
+ @notifications = []
19
+ @drivers = []
20
+ @slots = Array.new(max_visible, nil)
21
+ end
22
+
23
+ def draw
24
+ @drivers.each do |driver|
25
+ case @edge
26
+ when :left, :right
27
+ x = @edge == :right ? @window.width + driver.x : -Notification::WIDTH + driver.x
28
+ y = driver.y + Notification::HEIGHT / 2
29
+
30
+ Gosu.translate(x, y + (Notification::HEIGHT + Notification::PADDING) * driver.slot) do
31
+ driver.draw
32
+ end
33
+
34
+ when :top, :bottom
35
+ x = @window.width / 2 - Notification::WIDTH / 2
36
+ y = @edge == :top ? driver.y - Notification::HEIGHT : @window.height + driver.y
37
+ slot_position = (Notification::HEIGHT + Notification::PADDING) * driver.slot
38
+ slot_position *= -1 if @edge == :bottom
39
+
40
+ Gosu.translate(x, y + slot_position) do
41
+ driver.draw
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def update
48
+ show_next_notification if @drivers.size < @max_visible
49
+ @drivers.each do |driver|
50
+ if driver.done?
51
+ @slots[driver.slot] = nil
52
+ @drivers.delete(driver)
53
+ end
54
+ end
55
+
56
+ @drivers.each(&:update)
57
+ end
58
+
59
+ def show_next_notification
60
+ notification = @notifications.sort { |n| n.priority }.reverse.shift
61
+ return unless notification
62
+ return if available_slot_index < lowest_used_slot
63
+ @notifications.delete(notification)
64
+
65
+ @drivers << Driver.new(edge: @edge, mode: @mode, notification: notification, slot: available_slot_index)
66
+ slot = @slots[available_slot_index] = @drivers.last
67
+ end
68
+
69
+ def available_slot_index
70
+ @slots.each_with_index do |slot, i|
71
+ return i unless slot
72
+ end
73
+
74
+ return -1
75
+ end
76
+
77
+ def lowest_used_slot
78
+ @slots.each_with_index do |slot, i|
79
+ return i if slot
80
+ end
81
+
82
+ return -1
83
+ end
84
+
85
+ def highest_used_slot
86
+ _slot = -1
87
+ @slots.each_with_index do |slot, i|
88
+ _slot = i if slot
89
+ end
90
+
91
+ return _slot
92
+ end
93
+
94
+ def create_notification(**args)
95
+ notification = Notification.new(host: self, **args)
96
+ @notifications << notification
97
+ end
98
+
99
+ class Driver
100
+ attr_reader :x, :y, :notification, :slot
101
+ def initialize(edge:, mode:, notification:, slot:)
102
+ @edge = edge
103
+ @mode = mode
104
+ @notification = notification
105
+ @slot = slot
106
+
107
+ @x, @y = 0, 0
108
+ @delta = Gosu.milliseconds
109
+ @accumulator = 0.0
110
+
111
+ @born_at = Gosu.milliseconds
112
+ @duration_completed_at = Float::INFINITY
113
+ @transition_completed_at = Float::INFINITY
114
+ end
115
+
116
+ def transition_in_complete?
117
+ Gosu.milliseconds - @born_at >= @notification.transition_duration
118
+ end
119
+
120
+ def duration_completed?
121
+ Gosu.milliseconds - @transition_completed_at >= @notification.time_to_live
122
+ end
123
+
124
+ def done?
125
+ Gosu.milliseconds - @duration_completed_at >= @notification.transition_duration
126
+ end
127
+
128
+ def draw
129
+ ratio = 0.0
130
+
131
+ if not transition_in_complete?
132
+ ratio = animation_ratio
133
+ elsif transition_in_complete? and not duration_completed?
134
+ ratio = 1.0
135
+ elsif duration_completed?
136
+ ratio = 1.0 - animation_ratio
137
+ end
138
+
139
+ case @mode
140
+ when MODE_DEFAULT
141
+ Gosu.clip_to(0, 0, Notification::WIDTH, Notification::HEIGHT * ratio) do
142
+ @notification.draw
143
+ end
144
+ when MODE_CIRCLE
145
+ half = Notification::WIDTH / 2
146
+
147
+ Gosu.clip_to(half - (half * ratio), 0, Notification::WIDTH * ratio, Notification::HEIGHT) do
148
+ @notification.draw
149
+ end
150
+ end
151
+ end
152
+
153
+ def update
154
+ case @mode
155
+ when MODE_DEFAULT
156
+ update_default
157
+ when MODE_CIRCLE
158
+ update_circle
159
+ end
160
+
161
+ @accumulator += Gosu.milliseconds - @delta
162
+ @delta = Gosu.milliseconds
163
+ end
164
+
165
+
166
+ def update_default
167
+ case @edge
168
+ when :left, :right
169
+ if not transition_in_complete? # Slide In
170
+ @x = @edge == :right ? -x_offset : x_offset
171
+ elsif transition_in_complete? and not duration_completed?
172
+ @x = @edge == :right ? -Notification::WIDTH : Notification::WIDTH if @x.abs != Notification::WIDTH
173
+ @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY
174
+ @accumulator = 0.0
175
+ elsif duration_completed? # Slide Out
176
+ @x = @edge == :right ? x_offset - Notification::WIDTH : Notification::WIDTH - x_offset
177
+ @x = 0 if @edge == :left and @x <= 0
178
+ @x = 0 if @edge == :right and @x >= 0
179
+ @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY
180
+ end
181
+
182
+ when :top, :bottom
183
+ if not transition_in_complete? # Slide In
184
+ @y = @edge == :top ? y_offset : -y_offset
185
+ elsif transition_in_complete? and not duration_completed?
186
+ @y = @edge == :top ? Notification::HEIGHT : -Notification::HEIGHT if @x.abs != Notification::HEIGHT
187
+ @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY
188
+ @accumulator = 0.0
189
+ elsif duration_completed? # Slide Out
190
+ @y = @edge == :top ? Notification::HEIGHT - y_offset : y_offset - Notification::HEIGHT
191
+ @y = 0 if @edge == :top and @y <= 0
192
+ @y = 0 if @edge == :bottom and @y >= 0
193
+ @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY
194
+ end
195
+ end
196
+ end
197
+
198
+ def update_circle
199
+ case @edge
200
+ when :top, :bottom
201
+ @y = @edge == :top ? Notification::HEIGHT : -Notification::HEIGHT
202
+ when :left, :right
203
+ @x = @edge == :right ? -Notification::WIDTH : Notification::WIDTH
204
+ end
205
+
206
+ if transition_in_complete? and not duration_completed?
207
+ @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY
208
+ @accumulator = 0.0
209
+ elsif duration_completed?
210
+ @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY
211
+ end
212
+ end
213
+
214
+ def animation_ratio
215
+ x = (@accumulator / @notification.transition_duration)
216
+
217
+ case @notification.transition_type
218
+ when Notification::LINEAR_TRANSITION
219
+ x.clamp(0.0, 1.0)
220
+ when Notification::EASE_IN_OUT_TRANSITION # https://easings.net/#easeInOutQuint
221
+ (x < 0.5 ? 16 * x * x * x * x * x : 1 - ((-2 * x + 2) ** 5) / 2).clamp(0.0, 1.0)
222
+ end
223
+ end
224
+
225
+ def x_offset
226
+ if not transition_in_complete? or duration_completed?
227
+ Notification::WIDTH * animation_ratio
228
+ else
229
+ 0
230
+ end
231
+ end
232
+
233
+ def y_offset
234
+ if not transition_in_complete? or duration_completed?
235
+ Notification::HEIGHT * animation_ratio
236
+ else
237
+ 0
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -1,6 +1,7 @@
1
1
  module CyberarmEngine
2
2
  class GBuffer
3
3
  attr_reader :screen_vbo, :vertices, :uvs
4
+ attr_reader :width, :height
4
5
 
5
6
  def initialize(width:, height:)
6
7
  @width = width
@@ -44,7 +44,7 @@ module CyberarmEngine
44
44
  shader.uniform_transform("projection", camera.projection_matrix)
45
45
  shader.uniform_transform("view", camera.view_matrix)
46
46
  shader.uniform_transform("model", entity.model_matrix)
47
- shader.uniform_vec3("cameraPosition", camera.position)
47
+ shader.uniform_vector3("camera_position", camera.position)
48
48
 
49
49
  gl_error?
50
50
  draw_model(entity.model, shader)
@@ -154,15 +154,21 @@ module CyberarmEngine
154
154
  glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:depth))
155
155
  shader.uniform_integer("depth", 4)
156
156
 
157
- lights.each_with_index do |light, _i|
158
- shader.uniform_integer("light[0].type", light.type)
159
- shader.uniform_vec3("light[0].direction", light.direction)
160
- shader.uniform_vec3("light[0].position", light.position)
161
- shader.uniform_vec3("light[0].diffuse", light.diffuse)
162
- shader.uniform_vec3("light[0].ambient", light.ambient)
163
- shader.uniform_vec3("light[0].specular", light.specular)
157
+ # FIXME: Try to figure out how to up this to 32 and/or beyond
158
+ # (currently fails with more then 7 lights passed in to shader)
159
+ lights.each_slice(7).each do |light_group|
160
+ light_group.each_with_index do |light, _i|
161
+ shader.uniform_integer("light_count", light_group.size)
164
162
 
165
- glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
163
+ shader.uniform_integer("lights[#{_i}].type", light.type)
164
+ shader.uniform_vector3("lights[#{_i}].direction", light.direction)
165
+ shader.uniform_vector3("lights[#{_i}].position", light.position)
166
+ shader.uniform_vector3("lights[#{_i}].diffuse", light.diffuse)
167
+ shader.uniform_vector3("lights[#{_i}].ambient", light.ambient)
168
+ shader.uniform_vector3("lights[#{_i}].specular", light.specular)
169
+
170
+ glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
171
+ end
166
172
  end
167
173
 
168
174
  glBindVertexArray(0)
@@ -215,7 +221,7 @@ module CyberarmEngine
215
221
 
216
222
  offset = 0
217
223
  model.objects.each do |object|
218
- shader.uniform_boolean("hasTexture", object.has_texture?)
224
+ shader.uniform_boolean("has_texture", object.has_texture?)
219
225
 
220
226
  if object.has_texture?
221
227
  glBindTexture(GL_TEXTURE_2D, object.materials.find { |mat| mat.texture_id }.texture_id)
@@ -8,8 +8,19 @@ module CyberarmEngine
8
8
  end
9
9
 
10
10
  def draw(camera, lights, entities)
11
+ Stats.frame.start_timing(:opengl_renderer)
12
+
13
+ Stats.frame.start_timing(:opengl_model_renderer)
11
14
  @opengl_renderer.render(camera, lights, entities)
12
- @bounding_box_renderer.render(entities) if @show_bounding_boxes
15
+ Stats.frame.end_timing(:opengl_model_renderer)
16
+
17
+ if @show_bounding_boxes
18
+ Stats.frame.start_timing(:opengl_boundingbox_renderer)
19
+ @bounding_box_renderer.render(entities)
20
+ Stats.frame.end_timing(:opengl_boundingbox_renderer)
21
+ end
22
+
23
+ Stats.frame.end_timing(:opengl_renderer)
13
24
  end
14
25
 
15
26
  def canvas_size_changed
@@ -385,7 +385,7 @@ module CyberarmEngine
385
385
  # @param value [Vector]
386
386
  # @param location [Integer]
387
387
  # @return [void]
388
- def uniform_vec3(variable, value, location = nil)
388
+ def uniform_vector3(variable, value, location = nil)
389
389
  attr_loc = location || attribute_location(variable)
390
390
 
391
391
  glUniform3f(attr_loc, *value.to_a[0..2])
@@ -397,7 +397,7 @@ module CyberarmEngine
397
397
  # @param value [Vector]
398
398
  # @param location [Integer]
399
399
  # @return [void]
400
- def uniform_vec4(variable, value, location = nil)
400
+ def uniform_vector4(variable, value, location = nil)
401
401
  attr_loc = location || attribute_location(variable)
402
402
 
403
403
  glUniform4f(attr_loc, *value.to_a)
@@ -11,7 +11,19 @@ module CyberarmEngine
11
11
  if e != GL_NO_ERROR
12
12
  warn "OpenGL error detected by handler at: #{caller[0]}"
13
13
  warn " #{gluErrorString(e)} (#{e})\n"
14
- exit if window.exit_on_opengl_error?
14
+ exit if Window.instance&.exit_on_opengl_error?
15
+ end
16
+ end
17
+
18
+ def preload_default_shaders
19
+ shaders = %w[g_buffer lighting]
20
+ shaders.each do |shader|
21
+ Shader.new(
22
+ name: shader,
23
+ includes_dir: "#{CYBERARM_ENGINE_ROOT_PATH}/assets/shaders/include",
24
+ vertex: "#{CYBERARM_ENGINE_ROOT_PATH}/assets/shaders/vertex/#{shader}.glsl",
25
+ fragment: "#{CYBERARM_ENGINE_ROOT_PATH}/assets/shaders/fragment/#{shader}.glsl"
26
+ )
15
27
  end
16
28
  end
17
29
  end
@@ -1,20 +1,191 @@
1
1
  module CyberarmEngine
2
2
  class Stats
3
- @@hash = {
4
- gui_recalculations_last_frame: 0
5
- }
3
+ @frames = []
4
+ @frame_index = -1
5
+ @max_frame_history = 1024
6
6
 
7
- def self.get(key)
8
- @@hash.dig(key)
7
+ def self.new_frame
8
+ if @frames.size < @max_frame_history
9
+ @frames << Frame.new
10
+ else
11
+ @frames[@frame_index] = Frame.new
12
+ end
13
+ end
14
+
15
+ def self.frame
16
+ @frames[@frame_index]
17
+ end
18
+
19
+ def self.end_frame
20
+ frame&.complete
21
+
22
+ @frame_index += 1
23
+ @frame_index %= @max_frame_history
24
+ end
25
+
26
+ def self.frames
27
+ if @frames.size < @max_frame_history
28
+ @frames
29
+ else
30
+ @frames.rotate(@frame_index - (@max_frame_history - (@frames.size - 1)))
31
+ end
32
+ end
33
+
34
+ def self.frame_index
35
+ @frame_index
9
36
  end
10
37
 
11
- def self.increment(key, n)
12
- @@hash[key] += n
38
+ def self.max_frame_history
39
+ @max_frame_history
13
40
  end
14
41
 
15
- def self.clear
16
- @@hash.each do |key, _value|
17
- @@hash[key] = 0
42
+ class Frame
43
+ Timing = Struct.new(:start_time, :end_time, :duration)
44
+
45
+ attr_reader :frame_timing, :counters, :timings, :multitimings
46
+ def initialize
47
+ @frame_timing = Timing.new(Gosu.milliseconds, -1, -1)
48
+ @attempted_multitiming = false
49
+
50
+ @counters = {
51
+ gui_recalculations: 0
52
+ }
53
+
54
+ @timings = {}
55
+ @multitimings = {}
56
+ end
57
+
58
+ def increment(key, number = 1)
59
+ @counters[key] ||= 0
60
+ @counters[key] += number
61
+ end
62
+
63
+ def start_timing(key)
64
+ raise "key must be a symbol!" unless key.is_a?(Symbol)
65
+ if @timings[key]
66
+ # FIXME: Make it not spammy...
67
+ # warn "Only one timing per key per frame. (Timing for #{key.inspect} already exists!)"
68
+ @attempted_multitiming = true
69
+ @multitimings[key] = true
70
+
71
+ return
72
+ end
73
+
74
+ @timings[key] = Timing.new(Gosu.milliseconds, -1, -1)
75
+ end
76
+
77
+ def end_timing(key)
78
+ timing = @timings[key]
79
+
80
+ # FIXME: Make it not spammy...
81
+ # warn "Timing #{key.inspect} already ended!" if timing.end_time != -1
82
+
83
+ timing.end_time = Gosu.milliseconds
84
+ timing.duration = timing.end_time - timing.start_time
85
+ end
86
+
87
+ def complete
88
+ @frame_timing.end_time = Gosu.milliseconds
89
+ @frame_timing.duration = @frame_timing.end_time - @frame_timing.start_time
90
+
91
+ # Lock data structures
92
+ @frame_timing.freeze
93
+ @counters.freeze
94
+ @timings.freeze
95
+ @multitimings.freeze
96
+ end
97
+
98
+ def complete?
99
+ @frame_timing.duration != -1
100
+ end
101
+
102
+ def attempted_multitiming?
103
+ @attempted_multitiming
104
+ end
105
+ end
106
+
107
+ class StatsPlotter
108
+ attr_reader :position
109
+
110
+ def initialize(x, y, z = Float::INFINITY, width = 128, height = 128)
111
+ @position = Vector.new(x, y, z)
112
+ @width = width
113
+ @height = height
114
+
115
+ @padding = 2
116
+ @text_size = 16
117
+
118
+ @max_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding, z: z, size: @text_size, border: true)
119
+ @avg_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @padding + @height / 2 - @text_size / 2, z: z, size: @text_size, border: true)
120
+ @min_timing_label = CyberarmEngine::Text.new("", x: x + @padding + 1, y: y + @height - (@text_size + @padding / 2), z: z, size: @text_size, border: true)
121
+
122
+ @timings_label = CyberarmEngine::Text.new("", x: x + @padding + @width + @padding, y: y + @padding, z: z, size: @text_size, border: true)
123
+
124
+ @frame_stats = []
125
+ @graphs = {
126
+ frame_timings: []
127
+ }
128
+ end
129
+
130
+ def calculate_graphs
131
+ calculate_frame_timings_graph
132
+ end
133
+
134
+ def calculate_frame_timings_graph
135
+ @graphs[:frame_timings].clear
136
+
137
+ samples = @width - @padding
138
+ nodes = Array.new(samples.ceil) { [] }
139
+
140
+ slice = 0
141
+ @frame_stats.each_slice((CyberarmEngine::Stats.max_frame_history / samples.to_f).ceil) do |bucket|
142
+ bucket.each do |frame|
143
+ nodes[slice] << frame.frame_timing.duration
144
+ end
145
+
146
+ slice += 1
147
+ end
148
+
149
+ nodes.each_with_index do |cluster, i|
150
+ break if cluster.empty?
151
+
152
+ @graphs[:frame_timings] << CyberarmEngine::Vector.new(@position.x + @padding + 1 * i, (@position.y + @height - @padding) - cluster.max)
153
+ end
154
+ end
155
+
156
+ def draw
157
+ @frame_stats = CyberarmEngine::Stats.frames.select(&:complete?)
158
+ return if @frame_stats.empty?
159
+
160
+ calculate_graphs
161
+
162
+ @max_timing_label.text = "Max: #{@frame_stats.map { |f| f.frame_timing.duration }.max.to_s.rjust(3, " ")}ms"
163
+ @avg_timing_label.text = "Avg: #{(@frame_stats.map { |f| f.frame_timing.duration }.sum / @frame_stats.size).to_s.rjust(3, " ")}ms"
164
+ @min_timing_label.text = "Min: #{@frame_stats.map { |f| f.frame_timing.duration }.min.to_s.rjust(3, " ")}ms"
165
+
166
+ Gosu.draw_rect(@position.x, @position.y, @width, @height, 0xaa_222222, @position.z)
167
+ Gosu.draw_rect(@position.x + @padding, @position.y + @padding, @width - @padding * 2, @height - @padding * 2, 0xaa_222222, @position.z)
168
+
169
+ draw_graphs
170
+
171
+ @max_timing_label.draw
172
+ @avg_timing_label.draw
173
+ @min_timing_label.draw
174
+
175
+ # TODO: Make this optional
176
+ draw_timings
177
+ end
178
+
179
+ def draw_graphs
180
+ Gosu.draw_path(@graphs[:frame_timings], Gosu::Color::WHITE, Float::INFINITY)
181
+ end
182
+
183
+ def draw_timings
184
+ frame = @frame_stats.last
185
+
186
+ @timings_label.text = "#{frame.attempted_multitiming? ? "<c=d00>Attempted Multitiming!\nTimings may be inaccurate for:\n#{frame.multitimings.map { |m, _| m}.join("\n") }</c>\n\n" : ''}#{frame.timings.map { |t, v| "#{t}: #{v.duration}ms" }.join("\n")}"
187
+ Gosu.draw_rect(@timings_label.x - @padding, @timings_label.y - @padding, @timings_label.width + @padding * 2, @timings_label.height + @padding * 2, 0xdd_222222, @position.z)
188
+ @timings_label.draw
18
189
  end
19
190
  end
20
191
  end
@@ -81,6 +81,7 @@ module CyberarmEngine
81
81
  @size = size
82
82
  @font = font_name
83
83
 
84
+ invalidate_cache!
84
85
  @textobject = check_cache(size, font_name)
85
86
  end
86
87
  end
@@ -149,6 +150,8 @@ module CyberarmEngine
149
150
  end
150
151
 
151
152
  def markup_width(text = @text)
153
+ text = text.to_s
154
+
152
155
  spacing = 0
153
156
  spacing += @border_size if @border
154
157
  spacing += @shadow_size if @shadow
@@ -62,11 +62,11 @@ module CyberarmEngine
62
62
 
63
63
  def update
64
64
  # TOP
65
- @top.x = @element.x # + @element.border_thickness_left
65
+ @top.x = @element.x + @element.style.border_thickness_left
66
66
  @top.y = @element.y
67
67
  @top.z = @element.z
68
68
 
69
- @top.width = @element.width
69
+ @top.width = @element.width - @element.style.border_thickness_left
70
70
  @top.height = @element.style.border_thickness_top
71
71
 
72
72
  # RIGHT