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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +227 -0
- data/dama-logo.svg +91 -0
- data/exe/dama +4 -0
- data/ext/dama_native/.cargo/config.toml +3 -0
- data/ext/dama_native/Cargo.lock +3575 -0
- data/ext/dama_native/Cargo.toml +39 -0
- data/ext/dama_native/extconf.rb +72 -0
- data/ext/dama_native/src/audio.rs +134 -0
- data/ext/dama_native/src/engine.rs +339 -0
- data/ext/dama_native/src/lib.rs +396 -0
- data/ext/dama_native/src/renderer/screenshot.rs +84 -0
- data/ext/dama_native/src/renderer/shape_renderer.rs +507 -0
- data/ext/dama_native/src/renderer/text_renderer.rs +192 -0
- data/ext/dama_native/src/renderer.rs +563 -0
- data/ext/dama_native/src/window.rs +255 -0
- data/lib/dama/animation.rb +66 -0
- data/lib/dama/asset_cache.rb +56 -0
- data/lib/dama/audio.rb +47 -0
- data/lib/dama/auto_loader.rb +54 -0
- data/lib/dama/backend/base.rb +137 -0
- data/lib/dama/backend/native/ffi_bindings.rb +122 -0
- data/lib/dama/backend/native.rb +191 -0
- data/lib/dama/backend/web.rb +199 -0
- data/lib/dama/backend.rb +13 -0
- data/lib/dama/camera.rb +68 -0
- data/lib/dama/cli/new_project.rb +112 -0
- data/lib/dama/cli/release.rb +45 -0
- data/lib/dama/cli.rb +22 -0
- data/lib/dama/colors.rb +30 -0
- data/lib/dama/command_buffer.rb +83 -0
- data/lib/dama/component/attribute_definition.rb +13 -0
- data/lib/dama/component/attribute_set.rb +32 -0
- data/lib/dama/component.rb +28 -0
- data/lib/dama/configuration.rb +18 -0
- data/lib/dama/debug/frame_controller.rb +35 -0
- data/lib/dama/debug/screenshot_tool.rb +19 -0
- data/lib/dama/debug.rb +4 -0
- data/lib/dama/event_bus.rb +47 -0
- data/lib/dama/game/builder.rb +31 -0
- data/lib/dama/game/loop.rb +44 -0
- data/lib/dama/game.rb +88 -0
- data/lib/dama/geometry/circle.rb +28 -0
- data/lib/dama/geometry/rect.rb +16 -0
- data/lib/dama/geometry/sprite.rb +18 -0
- data/lib/dama/geometry/triangle.rb +13 -0
- data/lib/dama/geometry.rb +4 -0
- data/lib/dama/input/keyboard_state.rb +44 -0
- data/lib/dama/input/mouse_state.rb +45 -0
- data/lib/dama/input.rb +38 -0
- data/lib/dama/keys.rb +67 -0
- data/lib/dama/node/component_slot.rb +18 -0
- data/lib/dama/node/draw_context.rb +96 -0
- data/lib/dama/node.rb +139 -0
- data/lib/dama/physics/body.rb +57 -0
- data/lib/dama/physics/collider.rb +152 -0
- data/lib/dama/physics/collision.rb +15 -0
- data/lib/dama/physics/world.rb +125 -0
- data/lib/dama/physics.rb +4 -0
- data/lib/dama/registry/class_resolver.rb +48 -0
- data/lib/dama/registry.rb +21 -0
- data/lib/dama/release/archiver.rb +100 -0
- data/lib/dama/release/defaults/icon.icns +0 -0
- data/lib/dama/release/defaults/icon.ico +0 -0
- data/lib/dama/release/defaults/icon.png +0 -0
- data/lib/dama/release/dylib_relinker.rb +95 -0
- data/lib/dama/release/game_file_copier.rb +35 -0
- data/lib/dama/release/game_metadata.rb +61 -0
- data/lib/dama/release/icon_provider.rb +36 -0
- data/lib/dama/release/native_builder.rb +44 -0
- data/lib/dama/release/packager/linux.rb +62 -0
- data/lib/dama/release/packager/macos.rb +99 -0
- data/lib/dama/release/packager/web.rb +32 -0
- data/lib/dama/release/packager/windows.rb +61 -0
- data/lib/dama/release/packager.rb +9 -0
- data/lib/dama/release/platform_detector.rb +23 -0
- data/lib/dama/release/ruby_bundler.rb +163 -0
- data/lib/dama/release/stdlib_trimmer.rb +133 -0
- data/lib/dama/release/template_renderer.rb +40 -0
- data/lib/dama/release/templates/info_plist.xml.erb +19 -0
- data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
- data/lib/dama/release.rb +7 -0
- data/lib/dama/scene/composer.rb +65 -0
- data/lib/dama/scene.rb +233 -0
- data/lib/dama/scene_graph/class_index.rb +26 -0
- data/lib/dama/scene_graph/group_node.rb +27 -0
- data/lib/dama/scene_graph/instance_node.rb +30 -0
- data/lib/dama/scene_graph/path_selector.rb +25 -0
- data/lib/dama/scene_graph/query.rb +34 -0
- data/lib/dama/scene_graph/tag_index.rb +26 -0
- data/lib/dama/scene_graph/tree.rb +65 -0
- data/lib/dama/scene_graph.rb +4 -0
- data/lib/dama/sprite_sheet.rb +36 -0
- data/lib/dama/tween/easing.rb +31 -0
- data/lib/dama/tween/lerp.rb +35 -0
- data/lib/dama/tween/manager.rb +28 -0
- data/lib/dama/tween.rb +4 -0
- data/lib/dama/version.rb +3 -0
- data/lib/dama/vertex_batch.rb +35 -0
- data/lib/dama/web/entry.rb +79 -0
- data/lib/dama/web/static/index.html +142 -0
- data/lib/dama/web_builder.rb +232 -0
- data/lib/dama.rb +42 -0
- 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
|