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
data/lib/bevy/audio.rb ADDED
@@ -0,0 +1,545 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bevy
4
+ class AudioSource
5
+ attr_reader :path
6
+
7
+ def initialize(path)
8
+ @path = path
9
+ end
10
+
11
+ def type_name
12
+ 'AudioSource'
13
+ end
14
+
15
+ def to_native
16
+ native = Component.new('AudioSource')
17
+ native['path'] = @path
18
+ native
19
+ end
20
+
21
+ def self.from_native(native)
22
+ new(native['path'])
23
+ end
24
+ end
25
+
26
+ module PlaybackMode
27
+ ONCE = 'Once'
28
+ LOOP = 'Loop'
29
+ DESPAWN = 'Despawn'
30
+ end
31
+
32
+ class PlaybackSettings
33
+ attr_reader :mode, :volume, :speed, :paused
34
+
35
+ def initialize(mode: PlaybackMode::ONCE, volume: 1.0, speed: 1.0, paused: false)
36
+ @mode = mode
37
+ @volume = volume.clamp(0.0, 1.0)
38
+ @speed = speed.clamp(0.1, 10.0)
39
+ @paused = paused
40
+ end
41
+
42
+ def self.once
43
+ new(mode: PlaybackMode::ONCE)
44
+ end
45
+
46
+ def self.loop
47
+ new(mode: PlaybackMode::LOOP)
48
+ end
49
+
50
+ def self.despawn
51
+ new(mode: PlaybackMode::DESPAWN)
52
+ end
53
+
54
+ def with_volume(volume)
55
+ self.class.new(mode: @mode, volume: volume, speed: @speed, paused: @paused)
56
+ end
57
+
58
+ def with_speed(speed)
59
+ self.class.new(mode: @mode, volume: @volume, speed: speed, paused: @paused)
60
+ end
61
+
62
+ def with_paused(paused)
63
+ self.class.new(mode: @mode, volume: @volume, speed: @speed, paused: paused)
64
+ end
65
+
66
+ def paused?
67
+ @paused
68
+ end
69
+
70
+ def looping?
71
+ @mode == PlaybackMode::LOOP
72
+ end
73
+
74
+ def to_native
75
+ native = Component.new('PlaybackSettings')
76
+ native['mode'] = @mode
77
+ native['volume'] = @volume
78
+ native['speed'] = @speed
79
+ native['paused'] = @paused
80
+ native
81
+ end
82
+
83
+ def self.from_native(native)
84
+ new(
85
+ mode: native['mode'] || PlaybackMode::ONCE,
86
+ volume: native['volume'] || 1.0,
87
+ speed: native['speed'] || 1.0,
88
+ paused: native['paused'] || false
89
+ )
90
+ end
91
+
92
+ def to_h
93
+ {
94
+ mode: @mode,
95
+ volume: @volume,
96
+ speed: @speed,
97
+ paused: @paused
98
+ }
99
+ end
100
+ end
101
+
102
+ class AudioPlayer
103
+ attr_reader :source, :settings
104
+
105
+ def initialize(source:, settings: nil)
106
+ @source = source.is_a?(String) ? AudioSource.new(source) : source
107
+ @settings = settings || PlaybackSettings.once
108
+ end
109
+
110
+ def type_name
111
+ 'AudioPlayer'
112
+ end
113
+
114
+ def with_settings(settings)
115
+ self.class.new(source: @source, settings: settings)
116
+ end
117
+
118
+ def with_volume(volume)
119
+ with_settings(@settings.with_volume(volume))
120
+ end
121
+
122
+ def with_speed(speed)
123
+ with_settings(@settings.with_speed(speed))
124
+ end
125
+
126
+ def to_native
127
+ native = Component.new('AudioPlayer')
128
+ native['source_path'] = @source.path
129
+ native['mode'] = @settings.mode
130
+ native['volume'] = @settings.volume
131
+ native['speed'] = @settings.speed
132
+ native['paused'] = @settings.paused
133
+ native
134
+ end
135
+
136
+ def self.from_native(native)
137
+ source = AudioSource.new(native['source_path'])
138
+ settings = PlaybackSettings.new(
139
+ mode: native['mode'] || PlaybackMode::ONCE,
140
+ volume: native['volume'] || 1.0,
141
+ speed: native['speed'] || 1.0,
142
+ paused: native['paused'] || false
143
+ )
144
+ new(source: source, settings: settings)
145
+ end
146
+
147
+ def to_h
148
+ {
149
+ source: @source.path,
150
+ settings: @settings.to_h
151
+ }
152
+ end
153
+ end
154
+
155
+ class AudioBundle
156
+ attr_reader :player
157
+
158
+ def initialize(source:, settings: nil)
159
+ @player = AudioPlayer.new(source: source, settings: settings)
160
+ end
161
+
162
+ def self.from_source(path, settings: nil)
163
+ source = AudioSource.new(path)
164
+ new(source: source, settings: settings)
165
+ end
166
+
167
+ def components
168
+ [@player]
169
+ end
170
+ end
171
+
172
+ class GlobalVolume
173
+ attr_reader :volume
174
+
175
+ def initialize(volume: 1.0)
176
+ @volume = volume.clamp(0.0, 1.0)
177
+ end
178
+
179
+ def with_volume(volume)
180
+ self.class.new(volume: volume)
181
+ end
182
+
183
+ def muted?
184
+ @volume == 0.0
185
+ end
186
+
187
+ def to_h
188
+ { volume: @volume }
189
+ end
190
+ end
191
+
192
+ class SpatialAudioSettings
193
+ attr_reader :max_distance, :reference_distance, :rolloff_factor
194
+ attr_reader :cone_inner_angle, :cone_outer_angle, :cone_outer_gain
195
+
196
+ def initialize(
197
+ max_distance: 100.0,
198
+ reference_distance: 1.0,
199
+ rolloff_factor: 1.0,
200
+ cone_inner_angle: 360.0,
201
+ cone_outer_angle: 360.0,
202
+ cone_outer_gain: 0.0
203
+ )
204
+ @max_distance = max_distance
205
+ @reference_distance = reference_distance
206
+ @rolloff_factor = rolloff_factor
207
+ @cone_inner_angle = cone_inner_angle
208
+ @cone_outer_angle = cone_outer_angle
209
+ @cone_outer_gain = cone_outer_gain
210
+ end
211
+
212
+ def with_max_distance(distance)
213
+ dup.tap { |s| s.instance_variable_set(:@max_distance, distance) }
214
+ end
215
+
216
+ def with_reference_distance(distance)
217
+ dup.tap { |s| s.instance_variable_set(:@reference_distance, distance) }
218
+ end
219
+
220
+ def with_rolloff_factor(factor)
221
+ dup.tap { |s| s.instance_variable_set(:@rolloff_factor, factor) }
222
+ end
223
+
224
+ def with_cone(inner, outer, outer_gain)
225
+ dup.tap do |s|
226
+ s.instance_variable_set(:@cone_inner_angle, inner.clamp(0.0, 360.0))
227
+ s.instance_variable_set(:@cone_outer_angle, outer.clamp(0.0, 360.0))
228
+ s.instance_variable_set(:@cone_outer_gain, outer_gain.clamp(0.0, 1.0))
229
+ end
230
+ end
231
+
232
+ def calculate_attenuation(distance)
233
+ return 1.0 if distance <= @reference_distance
234
+ return 0.0 if distance >= @max_distance
235
+
236
+ d = [[distance, @reference_distance].max, @max_distance].min
237
+ @reference_distance / (@reference_distance + @rolloff_factor * (d - @reference_distance))
238
+ end
239
+
240
+ def to_native
241
+ native = Component.new('SpatialAudioSettings')
242
+ native['max_distance'] = @max_distance
243
+ native['reference_distance'] = @reference_distance
244
+ native['rolloff_factor'] = @rolloff_factor
245
+ native['cone_inner_angle'] = @cone_inner_angle
246
+ native['cone_outer_angle'] = @cone_outer_angle
247
+ native['cone_outer_gain'] = @cone_outer_gain
248
+ native
249
+ end
250
+
251
+ def self.from_native(native)
252
+ new(
253
+ max_distance: native['max_distance'] || 100.0,
254
+ reference_distance: native['reference_distance'] || 1.0,
255
+ rolloff_factor: native['rolloff_factor'] || 1.0,
256
+ cone_inner_angle: native['cone_inner_angle'] || 360.0,
257
+ cone_outer_angle: native['cone_outer_angle'] || 360.0,
258
+ cone_outer_gain: native['cone_outer_gain'] || 0.0
259
+ )
260
+ end
261
+
262
+ def to_h
263
+ {
264
+ max_distance: @max_distance,
265
+ reference_distance: @reference_distance,
266
+ rolloff_factor: @rolloff_factor,
267
+ cone_inner_angle: @cone_inner_angle,
268
+ cone_outer_angle: @cone_outer_angle,
269
+ cone_outer_gain: @cone_outer_gain
270
+ }
271
+ end
272
+ end
273
+
274
+ class FadeSettings
275
+ attr_accessor :duration, :elapsed, :target_volume
276
+
277
+ def initialize(duration, target_volume: 1.0)
278
+ @duration = duration
279
+ @elapsed = 0.0
280
+ @target_volume = target_volume
281
+ end
282
+
283
+ def progress
284
+ return 1.0 if @duration <= 0.0
285
+
286
+ (@elapsed / @duration).clamp(0.0, 1.0)
287
+ end
288
+
289
+ def complete?
290
+ @elapsed >= @duration
291
+ end
292
+
293
+ def update(delta)
294
+ @elapsed += delta
295
+ end
296
+ end
297
+
298
+ class AudioTrack
299
+ attr_reader :path
300
+ attr_accessor :settings, :current_time, :duration
301
+
302
+ def initialize(path, settings: nil)
303
+ @path = path
304
+ @settings = settings || PlaybackSettings.once
305
+ @current_time = 0.0
306
+ @duration = nil
307
+ @current_fade = nil
308
+ end
309
+
310
+ def fade_in(duration)
311
+ @current_fade = { type: :in, settings: FadeSettings.new(duration) }
312
+ end
313
+
314
+ def fade_out(duration)
315
+ @current_fade = { type: :out, settings: FadeSettings.new(duration, target_volume: 0.0) }
316
+ end
317
+
318
+ def update(delta)
319
+ return unless @current_fade
320
+
321
+ @current_fade[:settings].update(delta)
322
+ @current_fade = nil if @current_fade[:settings].complete?
323
+ end
324
+
325
+ def effective_volume
326
+ base = @settings.volume
327
+ return base unless @current_fade
328
+
329
+ case @current_fade[:type]
330
+ when :in
331
+ base * @current_fade[:settings].progress
332
+ when :out
333
+ base * (1.0 - @current_fade[:settings].progress)
334
+ else
335
+ base
336
+ end
337
+ end
338
+
339
+ def fading?
340
+ !@current_fade.nil?
341
+ end
342
+ end
343
+
344
+ class AudioChannel
345
+ attr_accessor :name, :volume, :muted
346
+
347
+ def initialize(name, volume: 1.0)
348
+ @name = name
349
+ @volume = volume.clamp(0.0, 2.0)
350
+ @muted = false
351
+ @track_ids = []
352
+ end
353
+
354
+ def mute
355
+ @muted = true
356
+ end
357
+
358
+ def unmute
359
+ @muted = false
360
+ end
361
+
362
+ def effective_volume
363
+ @muted ? 0.0 : @volume
364
+ end
365
+
366
+ def track_ids
367
+ @track_ids.dup
368
+ end
369
+
370
+ def add_track(id)
371
+ @track_ids << id
372
+ end
373
+
374
+ def remove_track(id)
375
+ @track_ids.delete(id)
376
+ end
377
+ end
378
+
379
+ class AudioMixer
380
+ attr_accessor :master_volume, :muted
381
+
382
+ def initialize
383
+ @master_volume = 1.0
384
+ @muted = false
385
+ @channels = {}
386
+ @tracks = {}
387
+ @next_track_id = 0
388
+ add_channel('music')
389
+ add_channel('sfx')
390
+ add_channel('voice')
391
+ end
392
+
393
+ def add_channel(name, volume: 1.0)
394
+ @channels[name] ||= AudioChannel.new(name, volume: volume)
395
+ end
396
+
397
+ def channel(name)
398
+ @channels[name]
399
+ end
400
+
401
+ def set_channel_volume(name, volume)
402
+ @channels[name]&.tap { |c| c.volume = volume.clamp(0.0, 2.0) }
403
+ end
404
+
405
+ def mute_channel(name)
406
+ @channels[name]&.mute
407
+ end
408
+
409
+ def unmute_channel(name)
410
+ @channels[name]&.unmute
411
+ end
412
+
413
+ def play(path, channel: 'sfx', settings: nil)
414
+ track_id = @next_track_id
415
+ @next_track_id += 1
416
+
417
+ track = AudioTrack.new(path, settings: settings)
418
+ @tracks[track_id] = { track: track, channel: channel }
419
+ @channels[channel]&.add_track(track_id)
420
+
421
+ track_id
422
+ end
423
+
424
+ def stop(track_id)
425
+ entry = @tracks.delete(track_id)
426
+ return unless entry
427
+
428
+ @channels[entry[:channel]]&.remove_track(track_id)
429
+ end
430
+
431
+ def stop_with_fade(track_id, duration)
432
+ entry = @tracks[track_id]
433
+ return unless entry
434
+
435
+ entry[:track].fade_out(duration)
436
+ end
437
+
438
+ def pause(track_id)
439
+ entry = @tracks[track_id]
440
+ return unless entry
441
+
442
+ entry[:track].settings = entry[:track].settings.with_paused(true)
443
+ end
444
+
445
+ def resume(track_id)
446
+ entry = @tracks[track_id]
447
+ return unless entry
448
+
449
+ entry[:track].settings = entry[:track].settings.with_paused(false)
450
+ end
451
+
452
+ def track(track_id)
453
+ @tracks[track_id]&.dig(:track)
454
+ end
455
+
456
+ def update(delta)
457
+ completed = []
458
+ @tracks.each do |id, entry|
459
+ entry[:track].update(delta)
460
+ if entry[:track].fading? && entry[:track].effective_volume <= 0.0
461
+ completed << id
462
+ end
463
+ end
464
+ completed.each { |id| stop(id) }
465
+ end
466
+
467
+ def effective_volume(track_id)
468
+ return 0.0 if @muted
469
+
470
+ entry = @tracks[track_id]
471
+ return 0.0 unless entry
472
+
473
+ channel_vol = @channels[entry[:channel]]&.effective_volume || 1.0
474
+ track_vol = entry[:track].effective_volume
475
+
476
+ @master_volume * channel_vol * track_vol
477
+ end
478
+ end
479
+
480
+ class AudioQueue
481
+ attr_accessor :loop_queue, :shuffle
482
+ attr_reader :current_index
483
+
484
+ def initialize
485
+ @tracks = []
486
+ @current_index = 0
487
+ @loop_queue = false
488
+ @shuffle = false
489
+ end
490
+
491
+ def add(path)
492
+ @tracks << path
493
+ end
494
+
495
+ def add_all(paths)
496
+ @tracks.concat(paths)
497
+ end
498
+
499
+ def current
500
+ @tracks[@current_index]
501
+ end
502
+
503
+ def next
504
+ return nil if @tracks.empty?
505
+
506
+ @current_index += 1
507
+ if @current_index >= @tracks.size
508
+ return nil unless @loop_queue
509
+
510
+ @current_index = 0
511
+ end
512
+ current
513
+ end
514
+
515
+ def previous
516
+ return nil if @tracks.empty?
517
+
518
+ if @current_index.zero?
519
+ return nil unless @loop_queue
520
+
521
+ @current_index = @tracks.size - 1
522
+ else
523
+ @current_index -= 1
524
+ end
525
+ current
526
+ end
527
+
528
+ def clear
529
+ @tracks.clear
530
+ @current_index = 0
531
+ end
532
+
533
+ def size
534
+ @tracks.size
535
+ end
536
+
537
+ def empty?
538
+ @tracks.empty?
539
+ end
540
+
541
+ def to_a
542
+ @tracks.dup
543
+ end
544
+ end
545
+ end