bevy 1.0.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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +4279 -0
  3. data/Cargo.toml +36 -0
  4. data/README.md +226 -0
  5. data/crates/bevy/Cargo.toml +52 -0
  6. data/crates/bevy/src/app.rs +43 -0
  7. data/crates/bevy/src/component.rs +111 -0
  8. data/crates/bevy/src/entity.rs +30 -0
  9. data/crates/bevy/src/error.rs +32 -0
  10. data/crates/bevy/src/event.rs +190 -0
  11. data/crates/bevy/src/input_bridge.rs +300 -0
  12. data/crates/bevy/src/lib.rs +42 -0
  13. data/crates/bevy/src/mesh_renderer.rs +328 -0
  14. data/crates/bevy/src/query.rs +53 -0
  15. data/crates/bevy/src/render_app.rs +689 -0
  16. data/crates/bevy/src/resource.rs +28 -0
  17. data/crates/bevy/src/schedule.rs +186 -0
  18. data/crates/bevy/src/sprite_renderer.rs +355 -0
  19. data/crates/bevy/src/system.rs +44 -0
  20. data/crates/bevy/src/text_renderer.rs +258 -0
  21. data/crates/bevy/src/types/color.rs +114 -0
  22. data/crates/bevy/src/types/dynamic.rs +131 -0
  23. data/crates/bevy/src/types/math.rs +260 -0
  24. data/crates/bevy/src/types/mod.rs +9 -0
  25. data/crates/bevy/src/types/transform.rs +166 -0
  26. data/crates/bevy/src/world.rs +163 -0
  27. data/crates/bevy_ruby_render/Cargo.toml +22 -0
  28. data/crates/bevy_ruby_render/src/asset.rs +360 -0
  29. data/crates/bevy_ruby_render/src/audio.rs +511 -0
  30. data/crates/bevy_ruby_render/src/camera.rs +365 -0
  31. data/crates/bevy_ruby_render/src/gamepad.rs +398 -0
  32. data/crates/bevy_ruby_render/src/lib.rs +26 -0
  33. data/crates/bevy_ruby_render/src/material.rs +310 -0
  34. data/crates/bevy_ruby_render/src/mesh.rs +491 -0
  35. data/crates/bevy_ruby_render/src/sprite.rs +289 -0
  36. data/ext/bevy/Cargo.toml +20 -0
  37. data/ext/bevy/extconf.rb +6 -0
  38. data/ext/bevy/src/conversions.rs +137 -0
  39. data/ext/bevy/src/lib.rs +29 -0
  40. data/ext/bevy/src/ruby_app.rs +65 -0
  41. data/ext/bevy/src/ruby_color.rs +149 -0
  42. data/ext/bevy/src/ruby_component.rs +189 -0
  43. data/ext/bevy/src/ruby_entity.rs +33 -0
  44. data/ext/bevy/src/ruby_math.rs +384 -0
  45. data/ext/bevy/src/ruby_query.rs +64 -0
  46. data/ext/bevy/src/ruby_render_app.rs +779 -0
  47. data/ext/bevy/src/ruby_system.rs +122 -0
  48. data/ext/bevy/src/ruby_world.rs +107 -0
  49. data/lib/bevy/animation.rb +597 -0
  50. data/lib/bevy/app.rb +675 -0
  51. data/lib/bevy/asset.rb +613 -0
  52. data/lib/bevy/audio.rb +545 -0
  53. data/lib/bevy/audio_effects.rb +224 -0
  54. data/lib/bevy/camera.rb +412 -0
  55. data/lib/bevy/component.rb +91 -0
  56. data/lib/bevy/diagnostics.rb +227 -0
  57. data/lib/bevy/ecs_advanced.rb +296 -0
  58. data/lib/bevy/event.rb +199 -0
  59. data/lib/bevy/gizmos.rb +158 -0
  60. data/lib/bevy/gltf.rb +227 -0
  61. data/lib/bevy/hierarchy.rb +444 -0
  62. data/lib/bevy/input.rb +514 -0
  63. data/lib/bevy/lighting.rb +369 -0
  64. data/lib/bevy/material.rb +248 -0
  65. data/lib/bevy/mesh.rb +257 -0
  66. data/lib/bevy/navigation.rb +344 -0
  67. data/lib/bevy/networking.rb +335 -0
  68. data/lib/bevy/particle.rb +337 -0
  69. data/lib/bevy/physics.rb +396 -0
  70. data/lib/bevy/plugins/default_plugins.rb +34 -0
  71. data/lib/bevy/plugins/input_plugin.rb +49 -0
  72. data/lib/bevy/reflect.rb +361 -0
  73. data/lib/bevy/render_graph.rb +210 -0
  74. data/lib/bevy/resource.rb +185 -0
  75. data/lib/bevy/scene.rb +254 -0
  76. data/lib/bevy/shader.rb +319 -0
  77. data/lib/bevy/shape.rb +195 -0
  78. data/lib/bevy/skeletal.rb +248 -0
  79. data/lib/bevy/sprite.rb +152 -0
  80. data/lib/bevy/sprite_sheet.rb +444 -0
  81. data/lib/bevy/state.rb +277 -0
  82. data/lib/bevy/system.rb +206 -0
  83. data/lib/bevy/text.rb +99 -0
  84. data/lib/bevy/text_advanced.rb +455 -0
  85. data/lib/bevy/timer.rb +147 -0
  86. data/lib/bevy/transform.rb +158 -0
  87. data/lib/bevy/ui.rb +454 -0
  88. data/lib/bevy/ui_advanced.rb +568 -0
  89. data/lib/bevy/version.rb +5 -0
  90. data/lib/bevy/visibility.rb +250 -0
  91. data/lib/bevy/window.rb +302 -0
  92. data/lib/bevy.rb +390 -0
  93. metadata +150 -0
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bevy
4
+ class AudioEffect
5
+ attr_accessor :enabled, :mix
6
+
7
+ def initialize(enabled: true, mix: 1.0)
8
+ @enabled = enabled
9
+ @mix = mix.to_f
10
+ end
11
+
12
+ def type_name
13
+ 'AudioEffect'
14
+ end
15
+ end
16
+
17
+ class Reverb < AudioEffect
18
+ attr_accessor :room_size, :damping, :wet_level, :dry_level, :width
19
+
20
+ def initialize(
21
+ room_size: 0.5,
22
+ damping: 0.5,
23
+ wet_level: 0.33,
24
+ dry_level: 0.4,
25
+ width: 1.0,
26
+ **kwargs
27
+ )
28
+ super(**kwargs)
29
+ @room_size = room_size.to_f
30
+ @damping = damping.to_f
31
+ @wet_level = wet_level.to_f
32
+ @dry_level = dry_level.to_f
33
+ @width = width.to_f
34
+ end
35
+
36
+ def type_name
37
+ 'Reverb'
38
+ end
39
+ end
40
+
41
+ class Delay < AudioEffect
42
+ attr_accessor :delay_time, :feedback, :wet_level
43
+
44
+ def initialize(delay_time: 0.5, feedback: 0.5, wet_level: 0.5, **kwargs)
45
+ super(**kwargs)
46
+ @delay_time = delay_time.to_f
47
+ @feedback = feedback.to_f
48
+ @wet_level = wet_level.to_f
49
+ end
50
+
51
+ def type_name
52
+ 'Delay'
53
+ end
54
+ end
55
+
56
+ class LowPassFilter < AudioEffect
57
+ attr_accessor :cutoff, :resonance
58
+
59
+ def initialize(cutoff: 1000.0, resonance: 0.707, **kwargs)
60
+ super(**kwargs)
61
+ @cutoff = cutoff.to_f
62
+ @resonance = resonance.to_f
63
+ end
64
+
65
+ def type_name
66
+ 'LowPassFilter'
67
+ end
68
+ end
69
+
70
+ class HighPassFilter < AudioEffect
71
+ attr_accessor :cutoff, :resonance
72
+
73
+ def initialize(cutoff: 200.0, resonance: 0.707, **kwargs)
74
+ super(**kwargs)
75
+ @cutoff = cutoff.to_f
76
+ @resonance = resonance.to_f
77
+ end
78
+
79
+ def type_name
80
+ 'HighPassFilter'
81
+ end
82
+ end
83
+
84
+ class Compressor < AudioEffect
85
+ attr_accessor :threshold, :ratio, :attack, :release, :makeup_gain
86
+
87
+ def initialize(
88
+ threshold: -20.0,
89
+ ratio: 4.0,
90
+ attack: 0.01,
91
+ release: 0.1,
92
+ makeup_gain: 0.0,
93
+ **kwargs
94
+ )
95
+ super(**kwargs)
96
+ @threshold = threshold.to_f
97
+ @ratio = ratio.to_f
98
+ @attack = attack.to_f
99
+ @release = release.to_f
100
+ @makeup_gain = makeup_gain.to_f
101
+ end
102
+
103
+ def type_name
104
+ 'Compressor'
105
+ end
106
+ end
107
+
108
+ class Equalizer < AudioEffect
109
+ attr_reader :bands
110
+
111
+ def initialize(**kwargs)
112
+ super(**kwargs)
113
+ @bands = []
114
+ end
115
+
116
+ def add_band(frequency:, gain:, q: 1.0)
117
+ @bands << EqualizerBand.new(frequency: frequency, gain: gain, q: q)
118
+ self
119
+ end
120
+
121
+ def type_name
122
+ 'Equalizer'
123
+ end
124
+ end
125
+
126
+ class EqualizerBand
127
+ attr_accessor :frequency, :gain, :q
128
+
129
+ def initialize(frequency:, gain:, q: 1.0)
130
+ @frequency = frequency.to_f
131
+ @gain = gain.to_f
132
+ @q = q.to_f
133
+ end
134
+
135
+ def type_name
136
+ 'EqualizerBand'
137
+ end
138
+ end
139
+
140
+ class Chorus < AudioEffect
141
+ attr_accessor :rate, :depth, :delay, :feedback
142
+
143
+ def initialize(rate: 1.5, depth: 0.02, delay: 0.03, feedback: 0.25, **kwargs)
144
+ super(**kwargs)
145
+ @rate = rate.to_f
146
+ @depth = depth.to_f
147
+ @delay = delay.to_f
148
+ @feedback = feedback.to_f
149
+ end
150
+
151
+ def type_name
152
+ 'Chorus'
153
+ end
154
+ end
155
+
156
+ class Flanger < AudioEffect
157
+ attr_accessor :rate, :depth, :feedback, :delay
158
+
159
+ def initialize(rate: 0.5, depth: 1.0, feedback: 0.5, delay: 0.001, **kwargs)
160
+ super(**kwargs)
161
+ @rate = rate.to_f
162
+ @depth = depth.to_f
163
+ @feedback = feedback.to_f
164
+ @delay = delay.to_f
165
+ end
166
+
167
+ def type_name
168
+ 'Flanger'
169
+ end
170
+ end
171
+
172
+ class Distortion < AudioEffect
173
+ attr_accessor :drive, :range, :blend
174
+
175
+ def initialize(drive: 0.5, range: 1000.0, blend: 0.5, **kwargs)
176
+ super(**kwargs)
177
+ @drive = drive.to_f
178
+ @range = range.to_f
179
+ @blend = blend.to_f
180
+ end
181
+
182
+ def type_name
183
+ 'Distortion'
184
+ end
185
+ end
186
+
187
+ class AudioEffectChain
188
+ attr_reader :effects
189
+
190
+ def initialize
191
+ @effects = []
192
+ end
193
+
194
+ def add(effect)
195
+ @effects << effect
196
+ self
197
+ end
198
+
199
+ def remove(index)
200
+ @effects.delete_at(index)
201
+ end
202
+
203
+ def clear
204
+ @effects = []
205
+ end
206
+
207
+ def process(sample)
208
+ @effects.reduce(sample) do |s, effect|
209
+ effect.enabled ? apply_effect(effect, s) : s
210
+ end
211
+ end
212
+
213
+ def type_name
214
+ 'AudioEffectChain'
215
+ end
216
+
217
+ private
218
+
219
+ def apply_effect(_effect, sample)
220
+ sample
221
+ end
222
+ end
223
+
224
+ end
@@ -0,0 +1,412 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bevy
4
+ class Camera2d
5
+ attr_reader :order, :clear_color, :viewport
6
+
7
+ def initialize(order: 0, clear_color: nil, viewport: nil)
8
+ @order = order
9
+ @clear_color = clear_color || Color.black
10
+ @viewport = viewport
11
+ end
12
+
13
+ def type_name
14
+ 'Camera2d'
15
+ end
16
+
17
+ def with_order(order)
18
+ self.class.new(order: order, clear_color: @clear_color, viewport: @viewport)
19
+ end
20
+
21
+ def with_clear_color(clear_color)
22
+ self.class.new(order: @order, clear_color: clear_color, viewport: @viewport)
23
+ end
24
+
25
+ def with_viewport(viewport)
26
+ self.class.new(order: @order, clear_color: @clear_color, viewport: viewport)
27
+ end
28
+
29
+ def to_native
30
+ native = Component.new('Camera2d')
31
+ native['order'] = @order
32
+ native['clear_color_r'] = @clear_color.r
33
+ native['clear_color_g'] = @clear_color.g
34
+ native['clear_color_b'] = @clear_color.b
35
+ native['clear_color_a'] = @clear_color.a
36
+ if @viewport
37
+ native['viewport_x'] = @viewport.x
38
+ native['viewport_y'] = @viewport.y
39
+ native['viewport_width'] = @viewport.width
40
+ native['viewport_height'] = @viewport.height
41
+ end
42
+ native
43
+ end
44
+
45
+ def self.from_native(native)
46
+ clear_color = Color.new(
47
+ native['clear_color_r'] || 0.0,
48
+ native['clear_color_g'] || 0.0,
49
+ native['clear_color_b'] || 0.0,
50
+ native['clear_color_a'] || 1.0
51
+ )
52
+ viewport = nil
53
+ if native['viewport_width']
54
+ viewport = Viewport.new(
55
+ native['viewport_x'] || 0,
56
+ native['viewport_y'] || 0,
57
+ native['viewport_width'],
58
+ native['viewport_height']
59
+ )
60
+ end
61
+ new(order: native['order'] || 0, clear_color: clear_color, viewport: viewport)
62
+ end
63
+
64
+ def to_h
65
+ h = {
66
+ order: @order,
67
+ clear_color: @clear_color.to_a
68
+ }
69
+ h[:viewport] = @viewport.to_h if @viewport
70
+ h
71
+ end
72
+ end
73
+
74
+ class Camera3d
75
+ attr_reader :order, :clear_color, :fov, :near, :far, :viewport
76
+
77
+ def initialize(order: 0, clear_color: nil, fov: 45.0, near: 0.1, far: 1000.0, viewport: nil)
78
+ @order = order
79
+ @clear_color = clear_color || Color.black
80
+ @fov = fov
81
+ @near = near
82
+ @far = far
83
+ @viewport = viewport
84
+ end
85
+
86
+ def type_name
87
+ 'Camera3d'
88
+ end
89
+
90
+ def with_order(order)
91
+ self.class.new(
92
+ order: order,
93
+ clear_color: @clear_color,
94
+ fov: @fov,
95
+ near: @near,
96
+ far: @far,
97
+ viewport: @viewport
98
+ )
99
+ end
100
+
101
+ def with_clear_color(clear_color)
102
+ self.class.new(
103
+ order: @order,
104
+ clear_color: clear_color,
105
+ fov: @fov,
106
+ near: @near,
107
+ far: @far,
108
+ viewport: @viewport
109
+ )
110
+ end
111
+
112
+ def with_fov(fov)
113
+ self.class.new(
114
+ order: @order,
115
+ clear_color: @clear_color,
116
+ fov: fov,
117
+ near: @near,
118
+ far: @far,
119
+ viewport: @viewport
120
+ )
121
+ end
122
+
123
+ def with_near(near)
124
+ self.class.new(
125
+ order: @order,
126
+ clear_color: @clear_color,
127
+ fov: @fov,
128
+ near: near,
129
+ far: @far,
130
+ viewport: @viewport
131
+ )
132
+ end
133
+
134
+ def with_far(far)
135
+ self.class.new(
136
+ order: @order,
137
+ clear_color: @clear_color,
138
+ fov: @fov,
139
+ near: @near,
140
+ far: far,
141
+ viewport: @viewport
142
+ )
143
+ end
144
+
145
+ def with_viewport(viewport)
146
+ self.class.new(
147
+ order: @order,
148
+ clear_color: @clear_color,
149
+ fov: @fov,
150
+ near: @near,
151
+ far: @far,
152
+ viewport: viewport
153
+ )
154
+ end
155
+
156
+ def to_native
157
+ native = Component.new('Camera3d')
158
+ native['order'] = @order
159
+ native['clear_color_r'] = @clear_color.r
160
+ native['clear_color_g'] = @clear_color.g
161
+ native['clear_color_b'] = @clear_color.b
162
+ native['clear_color_a'] = @clear_color.a
163
+ native['fov'] = @fov
164
+ native['near'] = @near
165
+ native['far'] = @far
166
+ if @viewport
167
+ native['viewport_x'] = @viewport.x
168
+ native['viewport_y'] = @viewport.y
169
+ native['viewport_width'] = @viewport.width
170
+ native['viewport_height'] = @viewport.height
171
+ end
172
+ native
173
+ end
174
+
175
+ def self.from_native(native)
176
+ clear_color = Color.new(
177
+ native['clear_color_r'] || 0.0,
178
+ native['clear_color_g'] || 0.0,
179
+ native['clear_color_b'] || 0.0,
180
+ native['clear_color_a'] || 1.0
181
+ )
182
+ viewport = nil
183
+ if native['viewport_width']
184
+ viewport = Viewport.new(
185
+ native['viewport_x'] || 0,
186
+ native['viewport_y'] || 0,
187
+ native['viewport_width'],
188
+ native['viewport_height']
189
+ )
190
+ end
191
+ new(
192
+ order: native['order'] || 0,
193
+ clear_color: clear_color,
194
+ fov: native['fov'] || 45.0,
195
+ near: native['near'] || 0.1,
196
+ far: native['far'] || 1000.0,
197
+ viewport: viewport
198
+ )
199
+ end
200
+
201
+ def to_h
202
+ h = {
203
+ order: @order,
204
+ clear_color: @clear_color.to_a,
205
+ fov: @fov,
206
+ near: @near,
207
+ far: @far
208
+ }
209
+ h[:viewport] = @viewport.to_h if @viewport
210
+ h
211
+ end
212
+ end
213
+
214
+ class Viewport
215
+ attr_reader :x, :y, :width, :height
216
+
217
+ def initialize(x, y, width, height)
218
+ @x = x
219
+ @y = y
220
+ @width = width
221
+ @height = height
222
+ end
223
+
224
+ def to_h
225
+ { x: @x, y: @y, width: @width, height: @height }
226
+ end
227
+ end
228
+
229
+ class SmoothFollow
230
+ attr_accessor :target, :offset, :smoothness, :enabled
231
+
232
+ def initialize(smoothness: 5.0, offset: nil, enabled: true)
233
+ @smoothness = smoothness
234
+ @offset = offset || Vec3.new(0.0, 0.0, 0.0)
235
+ @enabled = enabled
236
+ @target = nil
237
+ end
238
+
239
+ def type_name
240
+ 'SmoothFollow'
241
+ end
242
+
243
+ def follow(target)
244
+ @target = target
245
+ self
246
+ end
247
+
248
+ def lerp_position(current, delta_time)
249
+ return current unless @target && @enabled
250
+
251
+ desired = Vec3.new(
252
+ @target.x + @offset.x,
253
+ @target.y + @offset.y,
254
+ @target.z + @offset.z
255
+ )
256
+ t = [@smoothness * delta_time, 1.0].min
257
+ Vec3.new(
258
+ current.x + (desired.x - current.x) * t,
259
+ current.y + (desired.y - current.y) * t,
260
+ current.z + (desired.z - current.z) * t
261
+ )
262
+ end
263
+
264
+ def to_native
265
+ native = Component.new('SmoothFollow')
266
+ native['smoothness'] = @smoothness
267
+ native['offset_x'] = @offset.x
268
+ native['offset_y'] = @offset.y
269
+ native['offset_z'] = @offset.z
270
+ native['enabled'] = @enabled
271
+ if @target
272
+ native['target_x'] = @target.x
273
+ native['target_y'] = @target.y
274
+ native['target_z'] = @target.z
275
+ end
276
+ native
277
+ end
278
+ end
279
+
280
+ class CameraShake
281
+ attr_accessor :intensity, :duration, :decay, :frequency
282
+ attr_reader :remaining_time
283
+
284
+ def initialize(frequency: 20.0, decay: 1.0)
285
+ @intensity = 0.0
286
+ @duration = 0.0
287
+ @decay = decay
288
+ @frequency = frequency
289
+ @remaining_time = 0.0
290
+ @time_elapsed = 0.0
291
+ end
292
+
293
+ def type_name
294
+ 'CameraShake'
295
+ end
296
+
297
+ def trigger(intensity, duration, decay: nil)
298
+ @intensity = intensity
299
+ @duration = duration
300
+ @decay = decay if decay
301
+ @remaining_time = duration
302
+ @time_elapsed = 0.0
303
+ end
304
+
305
+ def update(delta_time)
306
+ return Vec2.new(0.0, 0.0) if @remaining_time <= 0.0
307
+
308
+ @remaining_time -= delta_time
309
+ @time_elapsed += delta_time
310
+
311
+ progress = 1.0 - (@remaining_time / @duration)
312
+ decay_factor = (1.0 - progress)**@decay
313
+ current_intensity = @intensity * decay_factor
314
+
315
+ angle = @time_elapsed * @frequency * Math::PI * 2
316
+ x = Math.sin(angle) * current_intensity
317
+ y = Math.cos(angle * 1.3) * current_intensity
318
+
319
+ Vec2.new(x, y)
320
+ end
321
+
322
+ def active?
323
+ @remaining_time > 0.0
324
+ end
325
+
326
+ def stop
327
+ @remaining_time = 0.0
328
+ end
329
+
330
+ def to_native
331
+ native = Component.new('CameraShake')
332
+ native['intensity'] = @intensity
333
+ native['duration'] = @duration
334
+ native['decay'] = @decay
335
+ native['frequency'] = @frequency
336
+ native['remaining_time'] = @remaining_time
337
+ native
338
+ end
339
+ end
340
+
341
+ class CameraBounds
342
+ attr_accessor :min_x, :min_y, :max_x, :max_y, :enabled
343
+
344
+ def initialize(min_x: nil, min_y: nil, max_x: nil, max_y: nil)
345
+ @min_x = min_x || -Float::INFINITY
346
+ @min_y = min_y || -Float::INFINITY
347
+ @max_x = max_x || Float::INFINITY
348
+ @max_y = max_y || Float::INFINITY
349
+ @enabled = min_x || min_y || max_x || max_y ? true : false
350
+ end
351
+
352
+ def type_name
353
+ 'CameraBounds'
354
+ end
355
+
356
+ def clamp(position)
357
+ return position unless @enabled
358
+
359
+ Vec3.new(
360
+ [[position.x, @min_x].max, @max_x].min,
361
+ [[position.y, @min_y].max, @max_y].min,
362
+ position.z
363
+ )
364
+ end
365
+
366
+ def to_native
367
+ native = Component.new('CameraBounds')
368
+ native['min_x'] = @min_x
369
+ native['min_y'] = @min_y
370
+ native['max_x'] = @max_x
371
+ native['max_y'] = @max_y
372
+ native['enabled'] = @enabled
373
+ native
374
+ end
375
+ end
376
+
377
+ class CameraZoom
378
+ attr_accessor :current, :min, :max, :speed
379
+
380
+ def initialize(initial: 1.0, min: 0.1, max: 10.0, speed: 1.0)
381
+ @current = initial
382
+ @min = min
383
+ @max = max
384
+ @speed = speed
385
+ end
386
+
387
+ def type_name
388
+ 'CameraZoom'
389
+ end
390
+
391
+ def zoom_in(amount = 0.1)
392
+ @current = [[@current - amount * @speed, @min].max, @max].min
393
+ end
394
+
395
+ def zoom_out(amount = 0.1)
396
+ @current = [[@current + amount * @speed, @min].max, @max].min
397
+ end
398
+
399
+ def set(value)
400
+ @current = [[value, @min].max, @max].min
401
+ end
402
+
403
+ def to_native
404
+ native = Component.new('CameraZoom')
405
+ native['current'] = @current
406
+ native['min'] = @min
407
+ native['max'] = @max
408
+ native['speed'] = @speed
409
+ native
410
+ end
411
+ end
412
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bevy
4
+ class ComponentDSL
5
+ class << self
6
+ def attribute(name, type, default: nil)
7
+ @attributes ||= {}
8
+ @attributes[name] = { type: type, default: default }
9
+
10
+ define_method(name) do
11
+ @data[name]
12
+ end
13
+
14
+ define_method(:"#{name}=") do |value|
15
+ @data[name] = value
16
+ end
17
+ end
18
+
19
+ def attributes
20
+ @attributes ||= {}
21
+ end
22
+
23
+ def component_name
24
+ name.split('::').last
25
+ end
26
+
27
+ def inherited(subclass)
28
+ super
29
+ subclass.instance_variable_set(:@attributes, attributes.dup)
30
+ end
31
+ end
32
+
33
+ def initialize(**attrs)
34
+ @data = {}
35
+
36
+ self.class.attributes.each do |name, config|
37
+ default = config[:default]
38
+ default_value = default.respond_to?(:call) ? default.call : default
39
+ @data[name] = attrs.fetch(name, default_value)
40
+ end
41
+ end
42
+
43
+ def type_name
44
+ self.class.component_name
45
+ end
46
+
47
+ def to_native
48
+ native = Bevy::Component.new(type_name)
49
+ @data.each do |key, value|
50
+ next unless native_convertible?(value)
51
+
52
+ native[key.to_s] = value
53
+ end
54
+ native
55
+ end
56
+
57
+ def to_h
58
+ @data.dup
59
+ end
60
+
61
+ def [](name)
62
+ @data[name.to_sym]
63
+ end
64
+
65
+ def []=(name, value)
66
+ @data[name.to_sym] = value
67
+ end
68
+
69
+ def update_from_native(native)
70
+ @data.each_key do |key|
71
+ @data[key] = native[key.to_s]
72
+ end
73
+ self
74
+ end
75
+
76
+ private
77
+
78
+ def native_convertible?(value)
79
+ case value
80
+ when NilClass, TrueClass, FalseClass, Integer, Float, String, Symbol
81
+ true
82
+ when Array
83
+ value.all? { |v| native_convertible?(v) }
84
+ when Hash
85
+ value.all? { |k, v| native_convertible?(k) && native_convertible?(v) }
86
+ else
87
+ false
88
+ end
89
+ end
90
+ end
91
+ end