dama 0.1.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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +227 -0
  4. data/dama-logo.svg +91 -0
  5. data/exe/dama +4 -0
  6. data/ext/dama_native/.cargo/config.toml +3 -0
  7. data/ext/dama_native/Cargo.lock +3575 -0
  8. data/ext/dama_native/Cargo.toml +39 -0
  9. data/ext/dama_native/extconf.rb +72 -0
  10. data/ext/dama_native/src/audio.rs +134 -0
  11. data/ext/dama_native/src/engine.rs +339 -0
  12. data/ext/dama_native/src/lib.rs +396 -0
  13. data/ext/dama_native/src/renderer/screenshot.rs +84 -0
  14. data/ext/dama_native/src/renderer/shape_renderer.rs +507 -0
  15. data/ext/dama_native/src/renderer/text_renderer.rs +192 -0
  16. data/ext/dama_native/src/renderer.rs +563 -0
  17. data/ext/dama_native/src/window.rs +255 -0
  18. data/lib/dama/animation.rb +66 -0
  19. data/lib/dama/asset_cache.rb +56 -0
  20. data/lib/dama/audio.rb +47 -0
  21. data/lib/dama/auto_loader.rb +54 -0
  22. data/lib/dama/backend/base.rb +137 -0
  23. data/lib/dama/backend/native/ffi_bindings.rb +122 -0
  24. data/lib/dama/backend/native.rb +191 -0
  25. data/lib/dama/backend/web.rb +199 -0
  26. data/lib/dama/backend.rb +13 -0
  27. data/lib/dama/camera.rb +68 -0
  28. data/lib/dama/cli/new_project.rb +112 -0
  29. data/lib/dama/cli/release.rb +45 -0
  30. data/lib/dama/cli.rb +22 -0
  31. data/lib/dama/colors.rb +30 -0
  32. data/lib/dama/command_buffer.rb +83 -0
  33. data/lib/dama/component/attribute_definition.rb +13 -0
  34. data/lib/dama/component/attribute_set.rb +32 -0
  35. data/lib/dama/component.rb +28 -0
  36. data/lib/dama/configuration.rb +18 -0
  37. data/lib/dama/debug/frame_controller.rb +35 -0
  38. data/lib/dama/debug/screenshot_tool.rb +19 -0
  39. data/lib/dama/debug.rb +4 -0
  40. data/lib/dama/event_bus.rb +47 -0
  41. data/lib/dama/game/builder.rb +31 -0
  42. data/lib/dama/game/loop.rb +44 -0
  43. data/lib/dama/game.rb +88 -0
  44. data/lib/dama/geometry/circle.rb +28 -0
  45. data/lib/dama/geometry/rect.rb +16 -0
  46. data/lib/dama/geometry/sprite.rb +18 -0
  47. data/lib/dama/geometry/triangle.rb +13 -0
  48. data/lib/dama/geometry.rb +4 -0
  49. data/lib/dama/input/keyboard_state.rb +44 -0
  50. data/lib/dama/input/mouse_state.rb +45 -0
  51. data/lib/dama/input.rb +38 -0
  52. data/lib/dama/keys.rb +67 -0
  53. data/lib/dama/node/component_slot.rb +18 -0
  54. data/lib/dama/node/draw_context.rb +96 -0
  55. data/lib/dama/node.rb +139 -0
  56. data/lib/dama/physics/body.rb +57 -0
  57. data/lib/dama/physics/collider.rb +152 -0
  58. data/lib/dama/physics/collision.rb +15 -0
  59. data/lib/dama/physics/world.rb +125 -0
  60. data/lib/dama/physics.rb +4 -0
  61. data/lib/dama/registry/class_resolver.rb +48 -0
  62. data/lib/dama/registry.rb +21 -0
  63. data/lib/dama/release/archiver.rb +100 -0
  64. data/lib/dama/release/defaults/icon.icns +0 -0
  65. data/lib/dama/release/defaults/icon.ico +0 -0
  66. data/lib/dama/release/defaults/icon.png +0 -0
  67. data/lib/dama/release/dylib_relinker.rb +95 -0
  68. data/lib/dama/release/game_file_copier.rb +35 -0
  69. data/lib/dama/release/game_metadata.rb +61 -0
  70. data/lib/dama/release/icon_provider.rb +36 -0
  71. data/lib/dama/release/native_builder.rb +44 -0
  72. data/lib/dama/release/packager/linux.rb +62 -0
  73. data/lib/dama/release/packager/macos.rb +99 -0
  74. data/lib/dama/release/packager/web.rb +32 -0
  75. data/lib/dama/release/packager/windows.rb +61 -0
  76. data/lib/dama/release/packager.rb +9 -0
  77. data/lib/dama/release/platform_detector.rb +23 -0
  78. data/lib/dama/release/ruby_bundler.rb +163 -0
  79. data/lib/dama/release/stdlib_trimmer.rb +133 -0
  80. data/lib/dama/release/template_renderer.rb +40 -0
  81. data/lib/dama/release/templates/info_plist.xml.erb +19 -0
  82. data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
  83. data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
  84. data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
  85. data/lib/dama/release.rb +7 -0
  86. data/lib/dama/scene/composer.rb +65 -0
  87. data/lib/dama/scene.rb +233 -0
  88. data/lib/dama/scene_graph/class_index.rb +26 -0
  89. data/lib/dama/scene_graph/group_node.rb +27 -0
  90. data/lib/dama/scene_graph/instance_node.rb +30 -0
  91. data/lib/dama/scene_graph/path_selector.rb +25 -0
  92. data/lib/dama/scene_graph/query.rb +34 -0
  93. data/lib/dama/scene_graph/tag_index.rb +26 -0
  94. data/lib/dama/scene_graph/tree.rb +65 -0
  95. data/lib/dama/scene_graph.rb +4 -0
  96. data/lib/dama/sprite_sheet.rb +36 -0
  97. data/lib/dama/tween/easing.rb +31 -0
  98. data/lib/dama/tween/lerp.rb +35 -0
  99. data/lib/dama/tween/manager.rb +28 -0
  100. data/lib/dama/tween.rb +4 -0
  101. data/lib/dama/version.rb +3 -0
  102. data/lib/dama/vertex_batch.rb +35 -0
  103. data/lib/dama/web/entry.rb +79 -0
  104. data/lib/dama/web/static/index.html +142 -0
  105. data/lib/dama/web_builder.rb +232 -0
  106. data/lib/dama.rb +42 -0
  107. metadata +198 -0
@@ -0,0 +1,255 @@
1
+ // Native-only: winit window management and input tracking.
2
+ #[cfg(not(target_arch = "wasm32"))]
3
+ mod native {
4
+ use std::collections::HashSet;
5
+ use std::sync::Arc;
6
+
7
+ use winit::application::ApplicationHandler;
8
+ use winit::event::{ElementState, WindowEvent};
9
+ use winit::event_loop::ActiveEventLoop;
10
+ use winit::keyboard::PhysicalKey;
11
+ use winit::window::{Window, WindowId};
12
+
13
+ pub struct WindowState {
14
+ window: Option<Arc<Window>>,
15
+ surface: Option<wgpu::Surface<'static>>,
16
+ surface_config: Option<wgpu::SurfaceConfiguration>,
17
+ current_texture: Option<wgpu::SurfaceTexture>,
18
+ title: String,
19
+ width: u32,
20
+ height: u32,
21
+ quit_requested: bool,
22
+ keys_pressed: HashSet<u32>,
23
+ keys_just_pressed: HashSet<u32>,
24
+ keys_just_released: HashSet<u32>,
25
+ mouse_x: f32,
26
+ mouse_y: f32,
27
+ mouse_buttons: HashSet<u32>,
28
+ }
29
+
30
+ impl WindowState {
31
+ pub fn new(title: String, width: u32, height: u32) -> Self {
32
+ Self {
33
+ window: None, surface: None, surface_config: None, current_texture: None,
34
+ title, width, height, quit_requested: false,
35
+ keys_pressed: HashSet::new(), keys_just_pressed: HashSet::new(),
36
+ keys_just_released: HashSet::new(),
37
+ mouse_x: 0.0, mouse_y: 0.0, mouse_buttons: HashSet::new(),
38
+ }
39
+ }
40
+
41
+ pub fn quit_requested(&self) -> bool { self.quit_requested }
42
+ pub fn width(&self) -> u32 { self.width }
43
+ pub fn height(&self) -> u32 { self.height }
44
+
45
+ pub fn begin_input_frame(&mut self) {
46
+ self.keys_just_pressed.clear();
47
+ self.keys_just_released.clear();
48
+ }
49
+
50
+ pub fn is_key_pressed(&self, key_code: u32) -> bool { self.keys_pressed.contains(&key_code) }
51
+ pub fn is_key_just_pressed(&self, key_code: u32) -> bool { self.keys_just_pressed.contains(&key_code) }
52
+ pub fn is_key_just_released(&self, key_code: u32) -> bool { self.keys_just_released.contains(&key_code) }
53
+ pub fn mouse_x(&self) -> f32 { self.mouse_x }
54
+ pub fn mouse_y(&self) -> f32 { self.mouse_y }
55
+ pub fn is_mouse_button_pressed(&self, button: u32) -> bool { self.mouse_buttons.contains(&button) }
56
+
57
+ pub fn surface_config(&self) -> Option<&wgpu::SurfaceConfiguration> { self.surface_config.as_ref() }
58
+
59
+ pub fn create_surface(
60
+ &mut self, instance: &wgpu::Instance, device: &wgpu::Device, adapter: &wgpu::Adapter,
61
+ ) -> Result<(), String> {
62
+ let window = self.window.as_ref().ok_or("Window not yet created")?.clone();
63
+ let surface = instance.create_surface(window.clone()).map_err(|e| format!("Failed to create surface: {e}"))?;
64
+ let caps = surface.get_capabilities(adapter);
65
+
66
+ // Use a non-sRGB format so colors are passed through without gamma correction.
67
+ // This ensures the RGBA values from Ruby render as-is (vivid, matching web).
68
+ let format = caps.formats.iter()
69
+ .find(|f| !f.is_srgb())
70
+ .copied()
71
+ .unwrap_or(caps.formats[0]);
72
+
73
+ // Use physical pixel dimensions for Retina/HiDPI sharpness.
74
+ let physical = window.inner_size();
75
+
76
+ let config = wgpu::SurfaceConfiguration {
77
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format,
78
+ width: physical.width, height: physical.height,
79
+ present_mode: wgpu::PresentMode::AutoVsync,
80
+ alpha_mode: caps.alpha_modes[0],
81
+ view_formats: vec![], desired_maximum_frame_latency: 2,
82
+ };
83
+ surface.configure(device, &config);
84
+ self.surface = Some(surface);
85
+ self.surface_config = Some(config);
86
+
87
+ // Update stored dimensions to physical for correct projection matrix.
88
+ self.width = physical.width;
89
+ self.height = physical.height;
90
+
91
+ Ok(())
92
+ }
93
+
94
+ pub fn acquire_texture(&mut self) -> Result<&wgpu::SurfaceTexture, String> {
95
+ let surface = self.surface.as_ref().ok_or("No surface")?;
96
+ let texture = surface.get_current_texture().map_err(|e| format!("Failed to get surface texture: {e}"))?;
97
+ self.current_texture = Some(texture);
98
+ self.current_texture.as_ref().ok_or("Texture not acquired".to_string())
99
+ }
100
+
101
+ pub fn present(&mut self) {
102
+ if let Some(texture) = self.current_texture.take() { texture.present(); }
103
+ }
104
+ }
105
+
106
+ type WindowCallback<'a> = Box<dyn FnOnce(&Arc<Window>) + 'a>;
107
+
108
+ pub struct DamaAppHandler<'a> {
109
+ pub window_state: &'a mut WindowState,
110
+ pub on_window_created: Option<WindowCallback<'a>>,
111
+ }
112
+
113
+ impl ApplicationHandler for DamaAppHandler<'_> {
114
+ fn resumed(&mut self, event_loop: &ActiveEventLoop) {
115
+ if self.window_state.window.is_some() { return; }
116
+ let attrs = Window::default_attributes()
117
+ .with_title(&self.window_state.title)
118
+ .with_inner_size(winit::dpi::LogicalSize::new(self.window_state.width, self.window_state.height));
119
+ let window = Arc::new(event_loop.create_window(attrs).expect("Failed to create window"));
120
+ self.window_state.window = Some(window.clone());
121
+ if let Some(callback) = self.on_window_created.take() { callback(&window); }
122
+ }
123
+
124
+ fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
125
+ match event {
126
+ WindowEvent::CloseRequested => { self.window_state.quit_requested = true; event_loop.exit(); }
127
+ WindowEvent::Resized(size) => { self.window_state.width = size.width; self.window_state.height = size.height; }
128
+ WindowEvent::KeyboardInput { event, .. } => {
129
+ if let PhysicalKey::Code(code) = event.physical_key {
130
+ let key_code = code as u32;
131
+ match event.state {
132
+ ElementState::Pressed => {
133
+ if !self.window_state.keys_pressed.contains(&key_code) {
134
+ self.window_state.keys_just_pressed.insert(key_code);
135
+ }
136
+ self.window_state.keys_pressed.insert(key_code);
137
+ }
138
+ ElementState::Released => {
139
+ self.window_state.keys_pressed.remove(&key_code);
140
+ self.window_state.keys_just_released.insert(key_code);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ WindowEvent::CursorMoved { position, .. } => {
146
+ // Convert physical pixels to logical pixels using the window's scale factor.
147
+ let scale = self.window_state.window.as_ref()
148
+ .map(|w| w.scale_factor())
149
+ .unwrap_or(1.0);
150
+ self.window_state.mouse_x = (position.x / scale) as f32;
151
+ self.window_state.mouse_y = (position.y / scale) as f32;
152
+ }
153
+ WindowEvent::MouseInput { state, button, .. } => {
154
+ let btn = match button {
155
+ winit::event::MouseButton::Left => 0,
156
+ winit::event::MouseButton::Right => 1,
157
+ winit::event::MouseButton::Middle => 2,
158
+ winit::event::MouseButton::Back => 3,
159
+ winit::event::MouseButton::Forward => 4,
160
+ winit::event::MouseButton::Other(n) => n as u32 + 5,
161
+ };
162
+ match state {
163
+ ElementState::Pressed => { self.window_state.mouse_buttons.insert(btn); }
164
+ ElementState::Released => { self.window_state.mouse_buttons.remove(&btn); }
165
+ }
166
+ }
167
+ _ => {}
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Web-only: minimal input state, updated via wasm_bindgen exports from JS.
174
+ #[cfg(target_arch = "wasm32")]
175
+ mod web {
176
+ use std::collections::HashSet;
177
+ use std::sync::Mutex;
178
+
179
+ static INPUT: Mutex<Option<InputState>> = Mutex::new(None);
180
+
181
+ pub struct InputState {
182
+ keys_pressed: HashSet<u32>,
183
+ keys_just_pressed: HashSet<u32>,
184
+ keys_just_released: HashSet<u32>,
185
+ mouse_x: f32,
186
+ mouse_y: f32,
187
+ mouse_buttons: HashSet<u32>,
188
+ }
189
+
190
+ impl InputState {
191
+ pub fn init() {
192
+ let mut guard = INPUT.lock().unwrap();
193
+ *guard = Some(InputState {
194
+ keys_pressed: HashSet::new(), keys_just_pressed: HashSet::new(),
195
+ keys_just_released: HashSet::new(),
196
+ mouse_x: 0.0, mouse_y: 0.0, mouse_buttons: HashSet::new(),
197
+ });
198
+ }
199
+
200
+ pub fn with<F, T>(f: F) -> T where F: FnOnce(&InputState) -> T {
201
+ let guard = INPUT.lock().unwrap();
202
+ f(guard.as_ref().unwrap())
203
+ }
204
+
205
+ pub fn with_mut<F, T>(f: F) -> T where F: FnOnce(&mut InputState) -> T {
206
+ let mut guard = INPUT.lock().unwrap();
207
+ f(guard.as_mut().unwrap())
208
+ }
209
+
210
+ pub fn begin_frame() {
211
+ Self::with_mut(|s| {
212
+ s.keys_just_pressed.clear();
213
+ s.keys_just_released.clear();
214
+ });
215
+ }
216
+
217
+ pub fn set_key(key_code: u32, pressed: bool) {
218
+ Self::with_mut(|s| {
219
+ if pressed {
220
+ if !s.keys_pressed.contains(&key_code) {
221
+ s.keys_just_pressed.insert(key_code);
222
+ }
223
+ s.keys_pressed.insert(key_code);
224
+ } else {
225
+ s.keys_pressed.remove(&key_code);
226
+ s.keys_just_released.insert(key_code);
227
+ }
228
+ });
229
+ }
230
+
231
+ pub fn set_mouse(x: f32, y: f32) {
232
+ Self::with_mut(|s| { s.mouse_x = x; s.mouse_y = y; });
233
+ }
234
+
235
+ pub fn set_mouse_button(button: u32, pressed: bool) {
236
+ Self::with_mut(|s| {
237
+ if pressed { s.mouse_buttons.insert(button); }
238
+ else { s.mouse_buttons.remove(&button); }
239
+ });
240
+ }
241
+
242
+ pub fn is_key_pressed(&self, key_code: u32) -> bool { self.keys_pressed.contains(&key_code) }
243
+ pub fn is_key_just_pressed(&self, key_code: u32) -> bool { self.keys_just_pressed.contains(&key_code) }
244
+ pub fn is_key_just_released(&self, key_code: u32) -> bool { self.keys_just_released.contains(&key_code) }
245
+ pub fn mouse_x(&self) -> f32 { self.mouse_x }
246
+ pub fn mouse_y(&self) -> f32 { self.mouse_y }
247
+ pub fn is_mouse_button_pressed(&self, button: u32) -> bool { self.mouse_buttons.contains(&button) }
248
+ }
249
+ }
250
+
251
+ #[cfg(not(target_arch = "wasm32"))]
252
+ pub use native::*;
253
+
254
+ #[cfg(target_arch = "wasm32")]
255
+ pub use web::*;
@@ -0,0 +1,66 @@
1
+ module Dama
2
+ # Cycles through sprite sheet frames over time.
3
+ # Supports looping and one-shot animations.
4
+ class Animation
5
+ attr_reader :fps
6
+
7
+ def initialize(frames:, fps:, loop: true, on_complete: nil)
8
+ @frame_indices = frames.to_a
9
+ @fps = fps.to_f
10
+ @looping = loop
11
+ @on_complete = on_complete
12
+ @elapsed = 0.0
13
+ @frame_position = 0
14
+ @completed = false
15
+ end
16
+
17
+ def update(delta_time:)
18
+ return if completed
19
+
20
+ @elapsed += delta_time
21
+ frame_duration = 1.0 / fps
22
+ frames_advanced = (elapsed / frame_duration).to_i
23
+
24
+ return unless frames_advanced > frame_position
25
+
26
+ @frame_position = frames_advanced
27
+ handle_frame_advancement
28
+ end
29
+
30
+ def complete?
31
+ completed
32
+ end
33
+
34
+ def reset
35
+ @elapsed = 0.0
36
+ @frame_position = 0
37
+ @completed = false
38
+ end
39
+
40
+ def current_frame
41
+ frame_indices[frame_position % frame_indices.size]
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :frame_indices, :looping, :on_complete, :elapsed, :frame_position, :completed
47
+
48
+ def handle_frame_advancement
49
+ return if frame_position < frame_indices.size
50
+
51
+ finish_animation
52
+ end
53
+
54
+ def finish_animation
55
+ return loop_animation if looping
56
+
57
+ @frame_position = frame_indices.size - 1
58
+ @completed = true
59
+ on_complete&.call
60
+ end
61
+
62
+ def loop_animation
63
+ @frame_position = frame_position % frame_indices.size
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,56 @@
1
+ module Dama
2
+ # Reference-counted texture cache. Textures are loaded once and shared
3
+ # across all nodes that declare the same path. When the last node using
4
+ # a texture is removed, the texture is unloaded from the GPU.
5
+ class AssetCache
6
+ def initialize(backend:)
7
+ @backend = backend
8
+ @entries = {}
9
+ end
10
+
11
+ # Acquire a texture handle for the given path. Loads from disk on first
12
+ # use; subsequent calls increment the reference count and return the
13
+ # same handle.
14
+ def acquire(path:)
15
+ entry = entries[path]
16
+
17
+ return increment_and_return(entry) if entry
18
+
19
+ handle = backend.load_texture_file(path:)
20
+ entries[path] = { handle:, ref_count: 1 }
21
+ handle
22
+ end
23
+
24
+ # Release one reference to the texture at the given path. When the
25
+ # reference count reaches zero, the texture is unloaded from the GPU.
26
+ def release(path:)
27
+ entry = entries[path]
28
+ return unless entry
29
+
30
+ entry[:ref_count] -= 1
31
+ return unless entry[:ref_count] <= 0
32
+
33
+ backend.unload_texture(handle: entry.fetch(:handle))
34
+ entries.delete(path)
35
+ end
36
+
37
+ # Release all textures regardless of reference count.
38
+ def release_all
39
+ entries.each_value { |entry| backend.unload_texture(handle: entry.fetch(:handle)) }
40
+ entries.clear
41
+ end
42
+
43
+ def handle_for(path:)
44
+ entries.dig(path, :handle)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :backend, :entries
50
+
51
+ def increment_and_return(entry)
52
+ entry[:ref_count] += 1
53
+ entry.fetch(:handle)
54
+ end
55
+ end
56
+ end
data/lib/dama/audio.rb ADDED
@@ -0,0 +1,47 @@
1
+ module Dama
2
+ # High-level audio interface for loading and playing sounds.
3
+ # Manages sound handles with reference counting (like AssetCache).
4
+ #
5
+ # Usage in a Node:
6
+ # sound :jump, path: "assets/jump.wav"
7
+ #
8
+ # Usage in update:
9
+ # Audio.play(:jump)
10
+ # Audio.play(:theme, volume: 0.5, loop: true)
11
+ class Audio
12
+ attr_reader :backend
13
+
14
+ def initialize(backend:)
15
+ @backend = backend
16
+ @sounds = {}
17
+ end
18
+
19
+ def load(name:, path:)
20
+ handle = backend.load_sound(path:)
21
+ sounds[name] = handle
22
+ end
23
+
24
+ def play(name, volume: 1.0, loop: false)
25
+ handle = sounds.fetch(name)
26
+ backend.play_sound(handle:, volume:, loop:)
27
+ end
28
+
29
+ def stop_all
30
+ backend.stop_all_sounds
31
+ end
32
+
33
+ def unload(name)
34
+ handle = sounds.delete(name)
35
+ backend.unload_sound(handle:) if handle
36
+ end
37
+
38
+ def unload_all
39
+ sounds.each_value { |handle| backend.unload_sound(handle:) }
40
+ sounds.clear
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :sounds
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ module Dama
2
+ # Discovers and loads all Ruby files in a game project directory.
3
+ # Handles dependency ordering automatically by retrying files
4
+ # that fail due to undefined constants.
5
+ class AutoLoader
6
+ MAX_PASSES = 10
7
+
8
+ def initialize(game_dir:)
9
+ @game_dir = game_dir
10
+ end
11
+
12
+ def load_all
13
+ files = discover_files
14
+ load_with_retries(files:)
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :game_dir
20
+
21
+ def discover_files
22
+ Dir[File.join(game_dir, "**", "*.rb")]
23
+ end
24
+
25
+ # Repeatedly attempt to load files, retrying those that fail
26
+ # with NameError (undefined constant — dependency not yet loaded).
27
+ # Stops when all files are loaded or no progress is made.
28
+ def load_with_retries(files:)
29
+ remaining = files.dup
30
+
31
+ MAX_PASSES.times do
32
+ failed = []
33
+
34
+ remaining.each do |file|
35
+ require file
36
+ rescue NameError
37
+ failed << file
38
+ end
39
+
40
+ return if failed.empty?
41
+
42
+ # No progress — the remaining files have unresolvable errors.
43
+ raise_load_error(failed:) if failed.size == remaining.size
44
+
45
+ remaining = failed
46
+ end
47
+ end
48
+
49
+ def raise_load_error(failed:)
50
+ # Try loading each failed file one more time to get the real error message.
51
+ failed.each { |file| require file }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,137 @@
1
+ module Dama
2
+ module Backend
3
+ # Abstract interface for rendering backends. Defines the contract
4
+ # that all backends (native, web, etc.) must implement.
5
+ # Each method raises NotImplementedError by default.
6
+ class Base
7
+ def initialize_engine(configuration:)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def shutdown
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def poll_events
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def begin_frame
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def end_frame
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def delta_time
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def frame_count
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def clear(color: Dama::Colors::BLACK, r: color.r, g: color.g, b: color.b, a: color.a)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def draw_triangle(x1:, y1:, x2:, y2:, x3:, y3:, color: Dama::Colors::WHITE,
40
+ r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def draw_rect(x:, y:, w:, h:, color: Dama::Colors::WHITE,
45
+ r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def draw_circle(cx:, cy:, radius:, color: Dama::Colors::WHITE,
50
+ r: color.r, g: color.g, b: color.b, a: color.a, filled: true, segments: 32)
51
+ raise NotImplementedError
52
+ end
53
+
54
+ def draw_text(text:, x:, y:, size:, color: Dama::Colors::WHITE,
55
+ r: color.r, g: color.g, b: color.b, a: color.a, font: nil)
56
+ raise NotImplementedError
57
+ end
58
+
59
+ def load_font(path:)
60
+ raise NotImplementedError
61
+ end
62
+
63
+ def draw_sprite(texture_handle:, x:, y:, w:, h:, color: Dama::Colors::WHITE,
64
+ r: color.r, g: color.g, b: color.b, a: color.a)
65
+ raise NotImplementedError
66
+ end
67
+
68
+ def load_texture(bytes:)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ def load_texture_file(path:)
73
+ raise NotImplementedError
74
+ end
75
+
76
+ def unload_texture(handle:)
77
+ raise NotImplementedError
78
+ end
79
+
80
+ def screenshot(output_path:)
81
+ raise NotImplementedError
82
+ end
83
+
84
+ def key_pressed?(key_code:)
85
+ raise NotImplementedError
86
+ end
87
+
88
+ def key_just_pressed?(key_code:)
89
+ raise NotImplementedError
90
+ end
91
+
92
+ def key_just_released?(key_code:)
93
+ raise NotImplementedError
94
+ end
95
+
96
+ def mouse_x
97
+ raise NotImplementedError
98
+ end
99
+
100
+ def mouse_y
101
+ raise NotImplementedError
102
+ end
103
+
104
+ def mouse_button_pressed?(button:)
105
+ raise NotImplementedError
106
+ end
107
+
108
+ def load_sound(path:)
109
+ raise NotImplementedError
110
+ end
111
+
112
+ def play_sound(handle:, volume: 1.0, loop: false)
113
+ raise NotImplementedError
114
+ end
115
+
116
+ def stop_all_sounds
117
+ raise NotImplementedError
118
+ end
119
+
120
+ def unload_sound(handle:)
121
+ raise NotImplementedError
122
+ end
123
+
124
+ def load_shader(source:)
125
+ raise NotImplementedError
126
+ end
127
+
128
+ def unload_shader(handle:)
129
+ raise NotImplementedError
130
+ end
131
+
132
+ def set_shader(handle:)
133
+ raise NotImplementedError
134
+ end
135
+ end
136
+ end
137
+ end