dama 0.1.0-x86_64-linux

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 (96) 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/lib/dama/animation.rb +66 -0
  7. data/lib/dama/asset_cache.rb +56 -0
  8. data/lib/dama/audio.rb +47 -0
  9. data/lib/dama/auto_loader.rb +54 -0
  10. data/lib/dama/backend/base.rb +137 -0
  11. data/lib/dama/backend/native/ffi_bindings.rb +122 -0
  12. data/lib/dama/backend/native.rb +191 -0
  13. data/lib/dama/backend/web.rb +199 -0
  14. data/lib/dama/backend.rb +13 -0
  15. data/lib/dama/camera.rb +68 -0
  16. data/lib/dama/cli/new_project.rb +112 -0
  17. data/lib/dama/cli/release.rb +45 -0
  18. data/lib/dama/cli.rb +22 -0
  19. data/lib/dama/colors.rb +30 -0
  20. data/lib/dama/command_buffer.rb +83 -0
  21. data/lib/dama/component/attribute_definition.rb +13 -0
  22. data/lib/dama/component/attribute_set.rb +32 -0
  23. data/lib/dama/component.rb +28 -0
  24. data/lib/dama/configuration.rb +18 -0
  25. data/lib/dama/debug/frame_controller.rb +35 -0
  26. data/lib/dama/debug/screenshot_tool.rb +19 -0
  27. data/lib/dama/debug.rb +4 -0
  28. data/lib/dama/event_bus.rb +47 -0
  29. data/lib/dama/game/builder.rb +31 -0
  30. data/lib/dama/game/loop.rb +44 -0
  31. data/lib/dama/game.rb +88 -0
  32. data/lib/dama/geometry/circle.rb +28 -0
  33. data/lib/dama/geometry/rect.rb +16 -0
  34. data/lib/dama/geometry/sprite.rb +18 -0
  35. data/lib/dama/geometry/triangle.rb +13 -0
  36. data/lib/dama/geometry.rb +4 -0
  37. data/lib/dama/input/keyboard_state.rb +44 -0
  38. data/lib/dama/input/mouse_state.rb +45 -0
  39. data/lib/dama/input.rb +38 -0
  40. data/lib/dama/keys.rb +67 -0
  41. data/lib/dama/native/libdama_native.so +0 -0
  42. data/lib/dama/node/component_slot.rb +18 -0
  43. data/lib/dama/node/draw_context.rb +96 -0
  44. data/lib/dama/node.rb +139 -0
  45. data/lib/dama/physics/body.rb +57 -0
  46. data/lib/dama/physics/collider.rb +152 -0
  47. data/lib/dama/physics/collision.rb +15 -0
  48. data/lib/dama/physics/world.rb +125 -0
  49. data/lib/dama/physics.rb +4 -0
  50. data/lib/dama/registry/class_resolver.rb +48 -0
  51. data/lib/dama/registry.rb +21 -0
  52. data/lib/dama/release/archiver.rb +100 -0
  53. data/lib/dama/release/defaults/icon.icns +0 -0
  54. data/lib/dama/release/defaults/icon.ico +0 -0
  55. data/lib/dama/release/defaults/icon.png +0 -0
  56. data/lib/dama/release/dylib_relinker.rb +95 -0
  57. data/lib/dama/release/game_file_copier.rb +35 -0
  58. data/lib/dama/release/game_metadata.rb +61 -0
  59. data/lib/dama/release/icon_provider.rb +36 -0
  60. data/lib/dama/release/native_builder.rb +44 -0
  61. data/lib/dama/release/packager/linux.rb +62 -0
  62. data/lib/dama/release/packager/macos.rb +99 -0
  63. data/lib/dama/release/packager/web.rb +32 -0
  64. data/lib/dama/release/packager/windows.rb +61 -0
  65. data/lib/dama/release/packager.rb +9 -0
  66. data/lib/dama/release/platform_detector.rb +23 -0
  67. data/lib/dama/release/ruby_bundler.rb +163 -0
  68. data/lib/dama/release/stdlib_trimmer.rb +133 -0
  69. data/lib/dama/release/template_renderer.rb +40 -0
  70. data/lib/dama/release/templates/info_plist.xml.erb +19 -0
  71. data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
  72. data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
  73. data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
  74. data/lib/dama/release.rb +7 -0
  75. data/lib/dama/scene/composer.rb +65 -0
  76. data/lib/dama/scene.rb +233 -0
  77. data/lib/dama/scene_graph/class_index.rb +26 -0
  78. data/lib/dama/scene_graph/group_node.rb +27 -0
  79. data/lib/dama/scene_graph/instance_node.rb +30 -0
  80. data/lib/dama/scene_graph/path_selector.rb +25 -0
  81. data/lib/dama/scene_graph/query.rb +34 -0
  82. data/lib/dama/scene_graph/tag_index.rb +26 -0
  83. data/lib/dama/scene_graph/tree.rb +65 -0
  84. data/lib/dama/scene_graph.rb +4 -0
  85. data/lib/dama/sprite_sheet.rb +36 -0
  86. data/lib/dama/tween/easing.rb +31 -0
  87. data/lib/dama/tween/lerp.rb +35 -0
  88. data/lib/dama/tween/manager.rb +28 -0
  89. data/lib/dama/tween.rb +4 -0
  90. data/lib/dama/version.rb +3 -0
  91. data/lib/dama/vertex_batch.rb +35 -0
  92. data/lib/dama/web/entry.rb +79 -0
  93. data/lib/dama/web/static/index.html +142 -0
  94. data/lib/dama/web_builder.rb +232 -0
  95. data/lib/dama.rb +42 -0
  96. metadata +186 -0
@@ -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
@@ -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
@@ -0,0 +1,13 @@
1
+ module Dama
2
+ module Backend
3
+ PLATFORMS = {
4
+ web: -> { Backend::Web.new },
5
+ native: -> { Backend::Native.new },
6
+ }.freeze
7
+
8
+ def self.for
9
+ platform = defined?(JS) ? :web : :native
10
+ PLATFORMS.fetch(platform).call
11
+ end
12
+ end
13
+ end