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,122 @@
|
|
|
1
|
+
require "ffi"
|
|
2
|
+
|
|
3
|
+
module Dama
|
|
4
|
+
module Backend
|
|
5
|
+
class Native
|
|
6
|
+
# Raw FFI bindings to the Rust cdylib. This is the only place in the
|
|
7
|
+
# Ruby codebase where FFI types appear. All other code interacts
|
|
8
|
+
# through the Backend::Native adapter.
|
|
9
|
+
module FfiBindings
|
|
10
|
+
extend FFI::Library
|
|
11
|
+
|
|
12
|
+
# Platform-specific shared library extension, resolved via hash
|
|
13
|
+
# lookup to avoid conditionals (per project coding guidelines).
|
|
14
|
+
LIBRARY_EXTENSIONS = {
|
|
15
|
+
"darwin" => "dylib",
|
|
16
|
+
"linux" => "so",
|
|
17
|
+
"mingw" => "dll",
|
|
18
|
+
"mswin" => "dll",
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Rust omits the "lib" prefix on Windows cdylibs (dama_native.dll
|
|
22
|
+
# instead of libdama_native.dll), so we need platform-aware names.
|
|
23
|
+
LIBRARY_PREFIXES = {
|
|
24
|
+
"darwin" => "lib",
|
|
25
|
+
"linux" => "lib",
|
|
26
|
+
"mingw" => "",
|
|
27
|
+
"mswin" => "",
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def self.library_filename
|
|
31
|
+
platform_key = LIBRARY_EXTENSIONS.keys.detect { |k| RUBY_PLATFORM.include?(k) }
|
|
32
|
+
prefix = LIBRARY_PREFIXES.fetch(platform_key)
|
|
33
|
+
extension = LIBRARY_EXTENSIONS.fetch(platform_key)
|
|
34
|
+
"#{prefix}dama_native.#{extension}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Library resolution order:
|
|
38
|
+
# 1. DAMA_NATIVE_LIB env var — packaged games set this to their bundled copy
|
|
39
|
+
# 2. lib/dama/native/ — pre-compiled platform gems and source gem extconf.rb
|
|
40
|
+
# install the shared library here
|
|
41
|
+
# 3. ext/dama_native/target/release/ — local development with cargo build
|
|
42
|
+
LIBRARY_PATH_RESOLVERS = [
|
|
43
|
+
lambda {
|
|
44
|
+
path = ENV.fetch("DAMA_NATIVE_LIB", nil)
|
|
45
|
+
path if path && File.exist?(path)
|
|
46
|
+
},
|
|
47
|
+
lambda {
|
|
48
|
+
path = File.expand_path("../../native/#{library_filename}", __dir__)
|
|
49
|
+
path if File.exist?(path)
|
|
50
|
+
},
|
|
51
|
+
lambda {
|
|
52
|
+
path = File.expand_path("../../../../ext/dama_native/target/release/#{library_filename}", __dir__)
|
|
53
|
+
path if File.exist?(path)
|
|
54
|
+
},
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
def self.library_path
|
|
58
|
+
LIBRARY_PATH_RESOLVERS.each do |resolver|
|
|
59
|
+
path = resolver.call
|
|
60
|
+
return path if path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
raise "dama native library not found. Run `cargo build --release` in ext/dama_native/ " \
|
|
64
|
+
"or install a platform-specific gem."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ffi_lib library_path
|
|
68
|
+
|
|
69
|
+
# --- Lifecycle ---
|
|
70
|
+
attach_function :dama_engine_init_headless, %i[uint32 uint32], :int32
|
|
71
|
+
attach_function :dama_engine_init, %i[uint32 uint32 string], :int32
|
|
72
|
+
attach_function :dama_engine_shutdown, [], :int32
|
|
73
|
+
attach_function :dama_engine_poll_events, [], :int32
|
|
74
|
+
attach_function :dama_engine_begin_frame, [], :int32
|
|
75
|
+
attach_function :dama_engine_end_frame, [], :int32
|
|
76
|
+
attach_function :dama_engine_delta_time, [], :double
|
|
77
|
+
attach_function :dama_engine_frame_count, [], :uint64
|
|
78
|
+
attach_function :dama_engine_last_error, [], :string
|
|
79
|
+
|
|
80
|
+
# --- Rendering ---
|
|
81
|
+
attach_function :dama_render_clear, %i[float float float float], :int32
|
|
82
|
+
attach_function :dama_render_vertices, %i[pointer uint32], :int32
|
|
83
|
+
attach_function :dama_render_set_texture, [:uint64], :int32
|
|
84
|
+
attach_function :dama_render_text,
|
|
85
|
+
%i[string float float float
|
|
86
|
+
float float float float], :int32
|
|
87
|
+
|
|
88
|
+
# --- Assets ---
|
|
89
|
+
attach_function :dama_asset_load_texture, %i[pointer uint32], :uint64
|
|
90
|
+
attach_function :dama_asset_unload_texture, [:uint64], :int32
|
|
91
|
+
|
|
92
|
+
# --- Input ---
|
|
93
|
+
attach_function :dama_input_key_pressed, [:uint32], :int32
|
|
94
|
+
attach_function :dama_input_key_just_pressed, [:uint32], :int32
|
|
95
|
+
attach_function :dama_input_key_just_released, [:uint32], :int32
|
|
96
|
+
attach_function :dama_input_mouse_x, [], :float
|
|
97
|
+
attach_function :dama_input_mouse_y, [], :float
|
|
98
|
+
attach_function :dama_input_mouse_button_pressed, [:uint32], :int32
|
|
99
|
+
|
|
100
|
+
# --- Debug ---
|
|
101
|
+
attach_function :dama_debug_screenshot, [:string], :int32
|
|
102
|
+
|
|
103
|
+
# --- Fonts ---
|
|
104
|
+
attach_function :dama_font_load, [:string], :int32
|
|
105
|
+
attach_function :dama_render_text_with_font,
|
|
106
|
+
%i[string float float float
|
|
107
|
+
float float float float string], :int32
|
|
108
|
+
|
|
109
|
+
# --- Audio ---
|
|
110
|
+
attach_function :dama_audio_load_sound, [:string], :uint64
|
|
111
|
+
attach_function :dama_audio_play_sound, %i[uint64 float int32], :int32
|
|
112
|
+
attach_function :dama_audio_stop_all, [], :int32
|
|
113
|
+
attach_function :dama_audio_unload_sound, [:uint64], :int32
|
|
114
|
+
|
|
115
|
+
# --- Shaders ---
|
|
116
|
+
attach_function :dama_shader_load, [:string], :uint64
|
|
117
|
+
attach_function :dama_shader_unload, [:uint64], :int32
|
|
118
|
+
attach_function :dama_render_set_shader, [:uint64], :int32
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Backend
|
|
3
|
+
# Native backend: calls the Rust cdylib through ruby-ffi.
|
|
4
|
+
# Shapes are decomposed into vertices in Ruby and submitted
|
|
5
|
+
# as a single batch per frame via dama_render_vertices.
|
|
6
|
+
class Native < Base
|
|
7
|
+
HEADLESS_INIT = lambda { |bindings, config|
|
|
8
|
+
bindings.dama_engine_init_headless(config.width, config.height)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# :nocov:
|
|
12
|
+
WINDOWED_INIT = ->(bindings, config) { bindings.dama_engine_init(config.width, config.height, config.title) }
|
|
13
|
+
# :nocov:
|
|
14
|
+
|
|
15
|
+
INIT_STRATEGIES = { true => HEADLESS_INIT, false => WINDOWED_INIT }.freeze
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@bindings = Native::FfiBindings
|
|
19
|
+
@vertex_batch = VertexBatch.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize_engine(configuration:)
|
|
23
|
+
strategy = INIT_STRATEGIES.fetch(configuration.headless)
|
|
24
|
+
result = strategy.call(bindings, configuration)
|
|
25
|
+
check_result(result:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shutdown
|
|
29
|
+
check_result(result: bindings.dama_engine_shutdown)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def poll_events
|
|
33
|
+
result = bindings.dama_engine_poll_events
|
|
34
|
+
result == 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def begin_frame
|
|
38
|
+
check_result(result: bindings.dama_engine_begin_frame)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def end_frame
|
|
42
|
+
# Flush accumulated vertices to the GPU in one FFI call.
|
|
43
|
+
vertex_batch.flush(bindings:)
|
|
44
|
+
check_result(result: bindings.dama_engine_end_frame)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delta_time
|
|
48
|
+
bindings.dama_engine_delta_time
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def frame_count
|
|
52
|
+
bindings.dama_engine_frame_count
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clear(color: Dama::Colors::BLACK, r: color.r, g: color.g, b: color.b, a: color.a)
|
|
56
|
+
check_result(result: bindings.dama_render_clear(r, g, b, a))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def draw_triangle(x1:, y1:, x2:, y2:, x3:, y3:, color: Dama::Colors::WHITE,
|
|
60
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
61
|
+
vertex_batch.push(Geometry::Triangle.vertices(x1:, y1:, x2:, y2:, x3:, y3:, r:, g:, b:, a:))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def draw_rect(x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
65
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
66
|
+
vertex_batch.push(Geometry::Rect.vertices(x:, y:, w:, h:, r:, g:, b:, a:))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def draw_circle(cx:, cy:, radius:, color: Dama::Colors::WHITE,
|
|
70
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true, segments: 32)
|
|
71
|
+
vertex_batch.push(Geometry::Circle.vertices(cx:, cy:, radius:, r:, g:, b:, a:, segments:))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def draw_text(text:, x:, y:, size:, color: Dama::Colors::WHITE,
|
|
75
|
+
r: color.r, g: color.g, b: color.b, a: color.a, font: nil)
|
|
76
|
+
vertex_batch.flush(bindings:)
|
|
77
|
+
result = if font
|
|
78
|
+
bindings.dama_render_text_with_font(text, x, y, size, r, g, b, a, font)
|
|
79
|
+
else
|
|
80
|
+
bindings.dama_render_text(text, x, y, size, r, g, b, a)
|
|
81
|
+
end
|
|
82
|
+
check_result(result:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_font(path:)
|
|
86
|
+
check_result(result: bindings.dama_font_load(path))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def draw_sprite(texture_handle:, x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
90
|
+
r: color.r, g: color.g, b: color.b, a: color.a)
|
|
91
|
+
# Flush any untextured vertices, switch texture, push sprite, flush, reset.
|
|
92
|
+
vertex_batch.flush(bindings:)
|
|
93
|
+
check_result(result: bindings.dama_render_set_texture(texture_handle))
|
|
94
|
+
vertex_batch.push(Geometry::Sprite.vertices(x:, y:, w:, h:, r:, g:, b:, a:))
|
|
95
|
+
vertex_batch.flush(bindings:)
|
|
96
|
+
check_result(result: bindings.dama_render_set_texture(0))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def load_texture(bytes:)
|
|
100
|
+
ptr = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
|
|
101
|
+
ptr.put_bytes(0, bytes)
|
|
102
|
+
handle = bindings.dama_asset_load_texture(ptr, bytes.bytesize)
|
|
103
|
+
raise "Failed to load texture" if handle.zero?
|
|
104
|
+
|
|
105
|
+
handle
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_texture_file(path:)
|
|
109
|
+
load_texture(bytes: File.binread(path))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unload_texture(handle:)
|
|
113
|
+
check_result(result: bindings.dama_asset_unload_texture(handle))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def screenshot(output_path:)
|
|
117
|
+
check_result(result: bindings.dama_debug_screenshot(output_path))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def key_pressed?(key_code:)
|
|
121
|
+
bindings.dama_input_key_pressed(key_code) == 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def key_just_pressed?(key_code:)
|
|
125
|
+
bindings.dama_input_key_just_pressed(key_code) == 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def key_just_released?(key_code:)
|
|
129
|
+
bindings.dama_input_key_just_released(key_code) == 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def mouse_x
|
|
133
|
+
bindings.dama_input_mouse_x
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def mouse_y
|
|
137
|
+
bindings.dama_input_mouse_y
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def mouse_button_pressed?(button:)
|
|
141
|
+
bindings.dama_input_mouse_button_pressed(button) == 1
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def load_sound(path:)
|
|
145
|
+
handle = bindings.dama_audio_load_sound(path)
|
|
146
|
+
raise "Failed to load sound: #{bindings.dama_engine_last_error}" if handle.zero?
|
|
147
|
+
|
|
148
|
+
handle
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def play_sound(handle:, volume: 1.0, loop: false)
|
|
152
|
+
looping = loop ? 1 : 0
|
|
153
|
+
check_result(result: bindings.dama_audio_play_sound(handle, volume, looping))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def stop_all_sounds
|
|
157
|
+
check_result(result: bindings.dama_audio_stop_all)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def unload_sound(handle:)
|
|
161
|
+
check_result(result: bindings.dama_audio_unload_sound(handle))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def load_shader(source:)
|
|
165
|
+
bindings.dama_shader_load(source)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def unload_shader(handle:)
|
|
169
|
+
check_result(result: bindings.dama_shader_unload(handle))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def set_shader(handle:)
|
|
173
|
+
# Flush pending vertices before changing shader to ensure
|
|
174
|
+
# they render with the current shader, not the new one.
|
|
175
|
+
vertex_batch.flush(bindings:)
|
|
176
|
+
check_result(result: bindings.dama_render_set_shader(handle))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
attr_reader :bindings, :vertex_batch
|
|
182
|
+
|
|
183
|
+
def check_result(result:)
|
|
184
|
+
return if result >= 0
|
|
185
|
+
|
|
186
|
+
error_msg = bindings.dama_engine_last_error
|
|
187
|
+
raise error_msg || "Unknown native engine error"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Backend
|
|
3
|
+
# Web backend: runs in ruby.wasm, sends high-level draw commands
|
|
4
|
+
# to the Rust wgpu wasm renderer via JavaScript bridge.
|
|
5
|
+
#
|
|
6
|
+
# Instead of decomposing shapes into triangles in Ruby (expensive in wasm),
|
|
7
|
+
# we send compact commands (9-14 floats each) and let Rust decompose them
|
|
8
|
+
# at native speed via dama_render_commands.
|
|
9
|
+
class Web < Base
|
|
10
|
+
def initialize
|
|
11
|
+
@frame_count = 0
|
|
12
|
+
@command_buffer = CommandBuffer.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize_engine(configuration:)
|
|
16
|
+
# On web, the engine is already initialized by index.html.
|
|
17
|
+
# Only call dama_init if not yet ready (avoids double-init replacing shaders).
|
|
18
|
+
w = configuration.width
|
|
19
|
+
h = configuration.height
|
|
20
|
+
::JS.eval("if (!window.__damaReady) { window.damaWgpu.dama_init('game', #{w}, #{h}); }")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def shutdown; end
|
|
24
|
+
def poll_events = false
|
|
25
|
+
|
|
26
|
+
def begin_frame
|
|
27
|
+
command_buffer.clear
|
|
28
|
+
js_renderer.call(:dama_begin_frame)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def end_frame
|
|
32
|
+
flush_commands
|
|
33
|
+
js_renderer.call(:dama_end_frame)
|
|
34
|
+
@frame_count += 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delta_time
|
|
38
|
+
js_time[:delta].to_f
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :frame_count
|
|
42
|
+
|
|
43
|
+
def clear(color: Dama::Colors::BLACK, r: color.r, g: color.g, b: color.b, a: color.a)
|
|
44
|
+
js_renderer.call(:dama_clear, r, g, b, a)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def draw_triangle(x1:, y1:, x2:, y2:, x3:, y3:, color: Dama::Colors::WHITE,
|
|
48
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
49
|
+
command_buffer.push_triangle(x1:, y1:, x2:, y2:, x3:, y3:, r:, g:, b:, a:)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def draw_rect(x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
53
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true)
|
|
54
|
+
command_buffer.push_rect(x:, y:, w:, h:, r:, g:, b:, a:)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def draw_circle(cx:, cy:, radius:, color: Dama::Colors::WHITE,
|
|
58
|
+
r: color.r, g: color.g, b: color.b, a: color.a, filled: true, segments: 32)
|
|
59
|
+
command_buffer.push_circle(cx:, cy:, radius:, r:, g:, b:, a:, segments:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def draw_text(text:, x:, y:, size:, color: Dama::Colors::WHITE,
|
|
63
|
+
r: color.r, g: color.g, b: color.b, a: color.a, font: nil)
|
|
64
|
+
flush_commands
|
|
65
|
+
js_renderer.call(:dama_render_text, text, x, y, size, r, g, b, a)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def draw_sprite(texture_handle:, x:, y:, w:, h:, color: Dama::Colors::WHITE,
|
|
69
|
+
r: color.r, g: color.g, b: color.b, a: color.a)
|
|
70
|
+
command_buffer.push_sprite(
|
|
71
|
+
texture_handle:, x:, y:, w:, h:, r:, g:, b:, a:,
|
|
72
|
+
u_min: 0.0, v_min: 0.0, u_max: 1.0, v_max: 1.0
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def screenshot(output_path:); end
|
|
77
|
+
|
|
78
|
+
def key_pressed?(key_code:)
|
|
79
|
+
js_renderer.call(:dama_key_pressed, key_code).to_s == "true"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def key_just_pressed?(key_code:)
|
|
83
|
+
js_renderer.call(:dama_key_just_pressed, key_code).to_s == "true"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def key_just_released?(key_code:)
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def mouse_x
|
|
91
|
+
js_renderer.call(:dama_mouse_x).to_f
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def mouse_y
|
|
95
|
+
js_renderer.call(:dama_mouse_y).to_f
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def mouse_button_pressed?(button:)
|
|
99
|
+
::JS.eval("return !!window.damaMouseButtons[#{button}]").to_s == "true"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def load_texture(bytes:)
|
|
103
|
+
b64 = [bytes].pack("m0")
|
|
104
|
+
js_array = ::JS.eval("return Uint8Array.from(atob('#{b64}'), c => c.charCodeAt(0))")
|
|
105
|
+
from_bigint(js_renderer.call(:dama_load_texture, js_array))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_texture_file(path:)
|
|
109
|
+
load_texture(bytes: File.binread(path))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unload_texture(handle:)
|
|
113
|
+
js_renderer.call(:dama_unload_texture, handle)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def load_sound(path:)
|
|
117
|
+
@next_sound_handle ||= 0
|
|
118
|
+
@next_sound_handle += 1
|
|
119
|
+
|
|
120
|
+
data = File.binread(path)
|
|
121
|
+
b64 = [data].pack("m0")
|
|
122
|
+
::JS.eval("window.damaSounds = window.damaSounds || {}; " \
|
|
123
|
+
"window.damaSounds[#{@next_sound_handle}] = 'data:audio/wav;base64,#{b64}'")
|
|
124
|
+
@next_sound_handle
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def play_sound(handle:, volume: 1.0, loop: false)
|
|
128
|
+
loop_js = loop ? "a.loop = true;" : ""
|
|
129
|
+
::JS.eval("(() => { const a = new Audio(window.damaSounds[#{handle}]); " \
|
|
130
|
+
"a.volume = #{volume}; #{loop_js} a.play().catch(() => {}); })()")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def stop_all_sounds
|
|
134
|
+
::JS.eval("document.querySelectorAll('audio').forEach(a => { a.pause(); a.currentTime = 0; })")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def unload_sound(handle:)
|
|
138
|
+
::JS.eval("delete window.damaSounds[#{handle}]")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def load_font(path:); end
|
|
142
|
+
|
|
143
|
+
def load_shader(source:)
|
|
144
|
+
# Pass shader source via JS template literal to avoid ruby.wasm
|
|
145
|
+
# JsValue.call data corruption. Escape backticks and backslashes.
|
|
146
|
+
escaped = source.gsub("\\", "\\\\\\\\").gsub("`", "\\`")
|
|
147
|
+
result = ::JS.eval("return String(window.damaWgpu.dama_shader_load(`#{escaped}`))")
|
|
148
|
+
result.to_s.to_i
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def unload_shader(handle:)
|
|
152
|
+
js_renderer.call(:dama_shader_unload, to_bigint(handle))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def set_shader(handle:)
|
|
156
|
+
# Merge shader switch INTO the next flush — don't send it separately.
|
|
157
|
+
# This avoids the ruby.wasm state persistence issue between JS.eval calls.
|
|
158
|
+
command_buffer.push_set_shader(shader_handle: handle)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
attr_reader :command_buffer
|
|
164
|
+
|
|
165
|
+
def js_renderer
|
|
166
|
+
::JS.global[:damaWgpu]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def js_time
|
|
170
|
+
::JS.global[:damaTime]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# wasm-bindgen maps Rust u64 to JS BigInt.
|
|
174
|
+
# Ruby integers must be converted before passing to wasm.
|
|
175
|
+
def to_bigint(value)
|
|
176
|
+
::JS.eval("return BigInt(#{value})")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Convert a JS BigInt (from wasm u64 return) to Ruby integer.
|
|
180
|
+
# Uses BigInt.toString() → Ruby String#to_i for reliable conversion.
|
|
181
|
+
def from_bigint(js_bigint)
|
|
182
|
+
js_bigint.call(:toString).to_s.to_i
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Flush accumulated commands to Rust wasm via a single JS.eval call.
|
|
186
|
+
# We pass the data as a JSON array string and construct the Float32Array
|
|
187
|
+
# entirely in JS, because ruby.wasm's JsValue.call doesn't reliably pass
|
|
188
|
+
# typed arrays to wasm-bindgen functions.
|
|
189
|
+
def flush_commands
|
|
190
|
+
return if command_buffer.empty?
|
|
191
|
+
|
|
192
|
+
floats = command_buffer.to_a
|
|
193
|
+
json = floats.map { |f| f.to_f.to_s }.join(",")
|
|
194
|
+
::JS.eval("window.damaWgpu.dama_render_commands(new Float32Array([#{json}]), #{floats.length})")
|
|
195
|
+
command_buffer.clear
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
data/lib/dama/backend.rb
ADDED
data/lib/dama/camera.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
# 2D camera with position, zoom, follow, and viewport culling.
|
|
3
|
+
# All draw coordinates in a scene are translated/scaled through
|
|
4
|
+
# the camera before reaching the backend.
|
|
5
|
+
class Camera
|
|
6
|
+
MIN_ZOOM = 0.1
|
|
7
|
+
MAX_ZOOM = 10.0
|
|
8
|
+
|
|
9
|
+
attr_reader :x, :y, :zoom, :viewport_width, :viewport_height
|
|
10
|
+
|
|
11
|
+
def initialize(viewport_width:, viewport_height:, x: 0.0, y: 0.0, zoom: 1.0)
|
|
12
|
+
@viewport_width = viewport_width.to_f
|
|
13
|
+
@viewport_height = viewport_height.to_f
|
|
14
|
+
@x = x.to_f
|
|
15
|
+
@y = y.to_f
|
|
16
|
+
@zoom = zoom.to_f.clamp(MIN_ZOOM, MAX_ZOOM)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def move_to(x:, y:)
|
|
20
|
+
@x = x.to_f
|
|
21
|
+
@y = y.to_f
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def move_by(dx:, dy:)
|
|
25
|
+
@x += dx.to_f
|
|
26
|
+
@y += dy.to_f
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def zoom_to(level:)
|
|
30
|
+
@zoom = level.to_f.clamp(MIN_ZOOM, MAX_ZOOM)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Converts world coordinates to screen pixel coordinates.
|
|
34
|
+
def world_to_screen(world_x:, world_y:)
|
|
35
|
+
{
|
|
36
|
+
screen_x: (world_x - x) * zoom,
|
|
37
|
+
screen_y: (world_y - y) * zoom,
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Converts screen pixel coordinates to world coordinates.
|
|
42
|
+
def screen_to_world(screen_x:, screen_y:)
|
|
43
|
+
{
|
|
44
|
+
world_x: (screen_x / zoom) + x,
|
|
45
|
+
world_y: (screen_y / zoom) + y,
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns true if a world-space rectangle overlaps the camera viewport.
|
|
50
|
+
def visible?(x:, y:, width:, height:)
|
|
51
|
+
screen = world_to_screen(world_x: x, world_y: y)
|
|
52
|
+
screen_w = width * zoom
|
|
53
|
+
screen_h = height * zoom
|
|
54
|
+
|
|
55
|
+
(screen.fetch(:screen_x) + screen_w).positive? &&
|
|
56
|
+
screen.fetch(:screen_x) < viewport_width &&
|
|
57
|
+
(screen.fetch(:screen_y) + screen_h).positive? &&
|
|
58
|
+
screen.fetch(:screen_y) < viewport_height
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Centers the camera on a target object (must respond to #x and #y).
|
|
62
|
+
# Use lerp < 1.0 for smooth following.
|
|
63
|
+
def follow(target:, lerp: 1.0)
|
|
64
|
+
@x += (target.x - x) * lerp
|
|
65
|
+
@y += (target.y - y) * lerp
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Dama
|
|
4
|
+
class Cli
|
|
5
|
+
# Generates a new game project in the current directory.
|
|
6
|
+
# Creates the standard directory structure with starter files
|
|
7
|
+
# for a playable game: a red circle moveable with arrow keys.
|
|
8
|
+
class NewProject
|
|
9
|
+
FILE_PERMISSIONS = {
|
|
10
|
+
true => 0o755,
|
|
11
|
+
false => 0o644,
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.run
|
|
15
|
+
new.generate
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate
|
|
19
|
+
puts "Creating new dama game project..."
|
|
20
|
+
|
|
21
|
+
TEMPLATES.each do |path, template|
|
|
22
|
+
write_template(path:, template:)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
create_directory("assets")
|
|
26
|
+
|
|
27
|
+
puts "\nDone! Run bin/dama to start your game."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def write_template(path:, template:)
|
|
33
|
+
full_path = File.join(Dir.pwd, path)
|
|
34
|
+
return puts(" exists #{path}") if File.exist?(full_path)
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
|
37
|
+
File.write(full_path, template.fetch(:content))
|
|
38
|
+
FileUtils.chmod(FILE_PERMISSIONS.fetch(template.fetch(:executable)), full_path)
|
|
39
|
+
puts " create #{path}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create_directory(name)
|
|
43
|
+
return puts(" exists #{name}/") if File.directory?(name)
|
|
44
|
+
|
|
45
|
+
FileUtils.mkdir_p(name)
|
|
46
|
+
puts " create #{name}/"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
TEMPLATES = {
|
|
50
|
+
"config.rb" => {
|
|
51
|
+
content: <<~RUBY,
|
|
52
|
+
GAME = Dama::Game.new do
|
|
53
|
+
settings resolution: [800, 600], title: "My Game"
|
|
54
|
+
start_scene MainScene
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
executable: false,
|
|
58
|
+
},
|
|
59
|
+
"bin/dama" => {
|
|
60
|
+
content: <<~RUBY,
|
|
61
|
+
#!/usr/bin/env ruby
|
|
62
|
+
require "bundler/setup"
|
|
63
|
+
require "dama"
|
|
64
|
+
|
|
65
|
+
Dama::Cli.run(args: ARGV, root: File.expand_path("..", __dir__))
|
|
66
|
+
RUBY
|
|
67
|
+
executable: true,
|
|
68
|
+
},
|
|
69
|
+
"game/components/transform.rb" => {
|
|
70
|
+
content: <<~RUBY,
|
|
71
|
+
class Transform < Dama::Component
|
|
72
|
+
attribute :x, default: 0.0
|
|
73
|
+
attribute :y, default: 0.0
|
|
74
|
+
end
|
|
75
|
+
RUBY
|
|
76
|
+
executable: false,
|
|
77
|
+
},
|
|
78
|
+
"game/nodes/player.rb" => {
|
|
79
|
+
content: <<~RUBY,
|
|
80
|
+
class Player < Dama::Node
|
|
81
|
+
component Transform, as: :transform, x: 400.0, y: 300.0
|
|
82
|
+
|
|
83
|
+
draw do
|
|
84
|
+
circle(transform.x, transform.y, 20.0, color: Dama::Colors::RED)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
RUBY
|
|
88
|
+
executable: false,
|
|
89
|
+
},
|
|
90
|
+
"game/scenes/main_scene.rb" => {
|
|
91
|
+
content: <<~RUBY,
|
|
92
|
+
class MainScene < Dama::Scene
|
|
93
|
+
compose do
|
|
94
|
+
add Player, as: :hero
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
update do |dt, input|
|
|
98
|
+
speed = 200.0
|
|
99
|
+
|
|
100
|
+
hero.transform.x += speed * dt if input.right?
|
|
101
|
+
hero.transform.x -= speed * dt if input.left?
|
|
102
|
+
hero.transform.y += speed * dt if input.down?
|
|
103
|
+
hero.transform.y -= speed * dt if input.up?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
RUBY
|
|
107
|
+
executable: false,
|
|
108
|
+
},
|
|
109
|
+
}.freeze
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|