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,35 @@
1
+ require "ffi"
2
+
3
+ module Dama
4
+ # Accumulates vertex data per frame and flushes it to the Rust backend
5
+ # in a single FFI call. Each vertex is 8 floats: [x, y, r, g, b, a, u, v].
6
+ class VertexBatch
7
+ FLOATS_PER_VERTEX = 8
8
+
9
+ def initialize
10
+ @buffer = []
11
+ end
12
+
13
+ def push(vertex_floats)
14
+ buffer.concat(vertex_floats)
15
+ end
16
+
17
+ def vertex_count
18
+ buffer.length / FLOATS_PER_VERTEX
19
+ end
20
+
21
+ def flush(bindings:)
22
+ count = vertex_count
23
+ return if count.zero?
24
+
25
+ ptr = FFI::MemoryPointer.new(:float, buffer.length)
26
+ ptr.write_array_of_float(buffer)
27
+ bindings.dama_render_vertices(ptr, count)
28
+ buffer.clear
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :buffer
34
+ end
35
+ end
@@ -0,0 +1,79 @@
1
+ # Web entry point: loads engine, game code, and wires up the JS frame loop.
2
+ #
3
+ # This file uses global variables ($backend, $scene, etc.) because ruby.wasm
4
+ # requires globals for JS interop — the JS frame loop invokes $dama_tick.call
5
+ # each frame, and closures over local variables don't persist across ruby.wasm
6
+ # JS boundary calls.
7
+
8
+ require "js"
9
+ require_relative "dama_core"
10
+
11
+ # Auto-load game files with retry (handles dependency ordering).
12
+ game_files = Dir["/src/game/**/*.rb"]
13
+ remaining = game_files.dup
14
+ 10.times do
15
+ failed = []
16
+ remaining.each do |f|
17
+ require f
18
+ rescue NameError
19
+ failed << f
20
+ end
21
+ break if failed.empty?
22
+ break if failed.size == remaining.size
23
+
24
+ remaining = failed
25
+ end
26
+ require "/src/config"
27
+
28
+ # Boot the game scene using the web backend.
29
+ $backend = Dama::Backend::Web.new
30
+ config = GAME.configuration
31
+ $backend.initialize_engine(configuration: config)
32
+
33
+ $asset_cache = Dama::AssetCache.new(backend: $backend)
34
+ $scene = GAME.send(:start_scene_class).new(
35
+ registry: GAME.registry,
36
+ asset_cache: $asset_cache,
37
+ backend: $backend,
38
+ scene_switcher: ->(scene_class) { $pending_scene = scene_class },
39
+ )
40
+ $scene.perform_compose
41
+ $scene.perform_enter
42
+
43
+ $input = Dama::Input.new(backend: $backend)
44
+ $pending_scene = nil
45
+
46
+ # Expose game state and error tracking for integration tests.
47
+ JS.eval("window.__damaErrors = []")
48
+ JS.eval("window.__damaState = { frameCount: 0, sceneName: '' }")
49
+ JS.eval("window.addEventListener('error', e => window.__damaErrors.push(e.message))")
50
+
51
+ $dama_tick = lambda {
52
+ dt = $backend.delta_time
53
+ $input.update
54
+
55
+ $scene.perform_update(delta_time: dt, input: $input)
56
+
57
+ # Handle scene transitions.
58
+ if $pending_scene
59
+ $scene = $pending_scene.new(
60
+ registry: GAME.registry,
61
+ asset_cache: $asset_cache,
62
+ backend: $backend,
63
+ scene_switcher: ->(sc) { $pending_scene = sc },
64
+ )
65
+ $scene.perform_compose
66
+ $scene.perform_enter
67
+ $pending_scene = nil
68
+ end
69
+
70
+ $backend.begin_frame
71
+ $backend.clear
72
+ $scene.perform_draw(backend: $backend)
73
+
74
+ $backend.end_frame
75
+
76
+ # Expose state for integration tests.
77
+ JS.eval("window.__damaState.frameCount = #{$backend.frame_count}")
78
+ JS.eval("window.__damaState.sceneName = '#{$scene.class.name}'")
79
+ }
@@ -0,0 +1,142 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>dama-rb: Web Demo (WebGPU)</title>
6
+ <style>
7
+ body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; }
8
+ canvas { border: 1px solid #333; }
9
+ #loading { color: #fff; font-family: sans-serif; font-size: 24px; position: absolute; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="loading">Loading...</div>
14
+ <canvas id="game"></canvas>
15
+
16
+ <script type="module">
17
+ // 1. Load the Rust wgpu renderer (compiled to wasm).
18
+ const loading = document.getElementById("loading");
19
+ loading.textContent = "Loading renderer...";
20
+
21
+ // Set canvas to physical pixel resolution for Retina sharpness.
22
+ const canvas = document.getElementById("game");
23
+ const dpr = window.devicePixelRatio || 1;
24
+ const logicalWidth = 800;
25
+ const logicalHeight = 600;
26
+ canvas.width = logicalWidth * dpr;
27
+ canvas.height = logicalHeight * dpr;
28
+ canvas.style.width = logicalWidth + "px";
29
+ canvas.style.height = logicalHeight + "px";
30
+
31
+ let renderer;
32
+ try {
33
+ const mod = await import("./pkg/dama_native.js");
34
+ await mod.default();
35
+ renderer = mod;
36
+ } catch (e) {
37
+ loading.textContent = "Failed to load renderer: " + e.message;
38
+ console.error(e);
39
+ throw e;
40
+ }
41
+
42
+ // Expose renderer exports globally (for ruby.wasm to call via JS.global).
43
+ window.damaWgpu = renderer;
44
+
45
+ // Initialize the Rust renderer with logical dimensions.
46
+ // The canvas backing buffer is already set to physical (dpr × logical).
47
+ renderer.dama_init("game", logicalWidth, logicalHeight);
48
+
49
+ // Wait for async GPU init to complete.
50
+ // Rust's spawn_local sets window.__damaReady when the engine is fully initialized.
51
+ await new Promise(resolve => {
52
+ const check = setInterval(() => {
53
+ if (window.__damaReady) { clearInterval(check); resolve(); }
54
+ }, 10);
55
+ setTimeout(() => { clearInterval(check); resolve(); }, 10000);
56
+ });
57
+
58
+ // Input: forward browser events to Rust wasm.
59
+ const KEY_MAP = {
60
+ ArrowLeft: 80, ArrowRight: 81, ArrowUp: 82, ArrowDown: 79,
61
+ Space: 62, Enter: 57, Escape: 114, Backspace: 52, Tab: 63,
62
+ ShiftLeft: 60, ShiftRight: 61, ControlLeft: 55, ControlRight: 56,
63
+ AltLeft: 50, AltRight: 51,
64
+ KeyA: 19, KeyB: 20, KeyC: 21, KeyD: 22, KeyE: 23, KeyF: 24,
65
+ KeyG: 25, KeyH: 26, KeyI: 27, KeyJ: 28, KeyK: 29, KeyL: 30,
66
+ KeyM: 31, KeyN: 32, KeyO: 33, KeyP: 34, KeyQ: 35, KeyR: 36,
67
+ KeyS: 37, KeyT: 38, KeyU: 39, KeyV: 40, KeyW: 41, KeyX: 42,
68
+ KeyY: 43, KeyZ: 44,
69
+ Digit0: 5, Digit1: 6, Digit2: 7, Digit3: 8, Digit4: 9,
70
+ Digit5: 10, Digit6: 11, Digit7: 12, Digit8: 13, Digit9: 14,
71
+ Equal: 15, Minus: 45,
72
+ };
73
+
74
+ document.addEventListener("keydown", e => {
75
+ const c = KEY_MAP[e.code]; if (c !== undefined) { e.preventDefault(); renderer.dama_input_set_key(c, true); }
76
+ });
77
+ document.addEventListener("keyup", e => {
78
+ const c = KEY_MAP[e.code]; if (c !== undefined) renderer.dama_input_set_key(c, false);
79
+ });
80
+ canvas.addEventListener("mousemove", e => {
81
+ const r = canvas.getBoundingClientRect();
82
+ renderer.dama_input_set_mouse(e.clientX - r.left, e.clientY - r.top);
83
+ });
84
+ // Track mouse button state in JS for Ruby to read without re-entering Rust wasm.
85
+ window.damaMouseButtons = {};
86
+ canvas.addEventListener("mousedown", e => {
87
+ renderer.dama_input_set_mouse_button(e.button, true);
88
+ window.damaMouseButtons[e.button] = true;
89
+ });
90
+ canvas.addEventListener("mouseup", e => {
91
+ renderer.dama_input_set_mouse_button(e.button, false);
92
+ window.damaMouseButtons[e.button] = false;
93
+ });
94
+
95
+ // Timing state for Ruby to read.
96
+ window.damaTime = { delta: 0.016 };
97
+
98
+ // 2. Load ruby.wasm + game code.
99
+ loading.textContent = "Loading ruby.wasm...";
100
+
101
+ let vm;
102
+ try {
103
+ const { DefaultRubyVM } = await import("./ruby_wasm.js");
104
+ const response = await fetch("game.wasm");
105
+ const module = await WebAssembly.compile(await response.arrayBuffer());
106
+ const result = await DefaultRubyVM(module);
107
+ vm = result.vm;
108
+ } catch (e) {
109
+ loading.textContent = "Failed to load ruby.wasm: " + e.message;
110
+ console.error(e);
111
+ throw e;
112
+ }
113
+
114
+ loading.textContent = "Starting game...";
115
+
116
+ vm.eval('require "/src/app"');
117
+ loading.style.display = "none";
118
+
119
+ // 3. JS-driven frame loop.
120
+ let lastTime = performance.now();
121
+ function gameLoop(timestamp) {
122
+ const dt = (timestamp - lastTime) / 1000;
123
+ lastTime = timestamp;
124
+ window.damaTime.delta = dt;
125
+
126
+ renderer.dama_input_begin_frame();
127
+
128
+ try {
129
+ vm.eval("$dama_tick.call");
130
+ } catch (e) {
131
+ console.error("Ruby tick error:", e);
132
+ loading.textContent = "Error: " + e.message;
133
+ loading.style.display = "block";
134
+ return;
135
+ }
136
+
137
+ requestAnimationFrame(gameLoop);
138
+ }
139
+ requestAnimationFrame(gameLoop);
140
+ </script>
141
+ </body>
142
+ </html>
@@ -0,0 +1,232 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+
4
+ module Dama
5
+ # Builds and serves the web version of a dama game.
6
+ # Handles: Rust wasm compilation, wasm-bindgen, rbwasm build/pack,
7
+ # static file copying, and WEBrick HTTP serving.
8
+ class WebBuilder # rubocop:disable Metrics/ClassLength
9
+ RUST_CRATE_PATH = File.expand_path("../../ext/dama_native", __dir__)
10
+ WEB_ENTRY_PATH = File.expand_path("web/entry.rb", __dir__)
11
+ WEB_STATIC_PATH = File.expand_path("web/static", __dir__)
12
+ WASM_TARGET = "wasm32-unknown-unknown".freeze
13
+
14
+ HOST_OS_TRIPLES = {
15
+ "darwin" => "apple-darwin",
16
+ "linux" => "unknown-linux-gnu",
17
+ "mingw" => "pc-windows-msvc",
18
+ "mswin" => "pc-windows-msvc",
19
+ }.freeze
20
+
21
+ def self.build_and_serve(project_root:, port: 8080)
22
+ begin
23
+ require "webrick"
24
+ rescue LoadError
25
+ raise LoadError,
26
+ "webrick gem is required for web development server. Install it with: gem install webrick"
27
+ end
28
+ builder = new(project_root:)
29
+ builder.build
30
+ builder.serve(port:)
31
+ end
32
+
33
+ def initialize(project_root:)
34
+ @project_root = project_root
35
+ @dist_dir = File.join(project_root, "dist")
36
+ end
37
+
38
+ def build
39
+ FileUtils.mkdir_p(dist_dir)
40
+ FileUtils.mkdir_p(File.join(dist_dir, "pkg"))
41
+
42
+ build_rust_wasm
43
+ run_wasm_bindgen
44
+ build_ruby_wasm
45
+ copy_static_files
46
+
47
+ puts "Build complete: #{dist_dir}"
48
+ end
49
+
50
+ def serve(port:)
51
+ kill_existing_server(port:)
52
+
53
+ puts "Serving at http://localhost:#{port}"
54
+ puts "Press Ctrl+C to stop."
55
+
56
+ server = WEBrick::HTTPServer.new(
57
+ Port: port,
58
+ DocumentRoot: dist_dir,
59
+ Logger: WEBrick::Log.new($stdout, WEBrick::Log::WARN),
60
+ AccessLog: [],
61
+ )
62
+
63
+ trap("INT") { server.shutdown }
64
+ server.start
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :project_root, :dist_dir
70
+
71
+ PORT_LISTER_COMMANDS = {
72
+ "mingw" => ->(port) { `netstat -ano | findstr :#{port}`.scan(/\s(\d+)\s*$/).flatten },
73
+ "mswin" => ->(port) { `netstat -ano | findstr :#{port}`.scan(/\s(\d+)\s*$/).flatten },
74
+ }.freeze
75
+
76
+ DEFAULT_PORT_LISTER = ->(port) { `lsof -ti:#{port} 2>/dev/null`.strip.split("\n") }
77
+
78
+ # Terminate any process already listening on the target port.
79
+ def kill_existing_server(port:)
80
+ validated_port = Integer(port)
81
+ platform_key = PORT_LISTER_COMMANDS.keys.detect { |k| RUBY_PLATFORM.include?(k) }
82
+ lister = PORT_LISTER_COMMANDS.fetch(platform_key, DEFAULT_PORT_LISTER)
83
+ pids = lister.call(validated_port).reject(&:empty?)
84
+ return if pids.empty?
85
+
86
+ puts "Killing existing server on port #{validated_port}..."
87
+ pids.each { |pid| Process.kill("TERM", Integer(pid)) }
88
+ rescue Errno::ESRCH
89
+ # Process already exited.
90
+ end
91
+
92
+ def build_rust_wasm
93
+ puts "=== Building Rust renderer for wasm32 ==="
94
+ run_command(
95
+ "cargo build --release --target #{WASM_TARGET}",
96
+ dir: RUST_CRATE_PATH,
97
+ env: { "RUSTFLAGS" => "--cfg=web_sys_unstable_apis" },
98
+ )
99
+ end
100
+
101
+ def run_wasm_bindgen
102
+ puts "=== Generating JS glue ==="
103
+ wasm_path = File.join(RUST_CRATE_PATH, "target", WASM_TARGET, "release", "dama_native.wasm")
104
+ pkg_dir = File.join(dist_dir, "pkg")
105
+ run_command("wasm-bindgen --target web --out-dir #{pkg_dir} #{wasm_path}")
106
+ end
107
+
108
+ def build_ruby_wasm
109
+ puts "=== Building ruby.wasm + packing game code ==="
110
+
111
+ Dir.mktmpdir do |pack_dir|
112
+ generate_dama_core(output: File.join(pack_dir, "dama_core.rb"))
113
+ FileUtils.cp(WEB_ENTRY_PATH, File.join(pack_dir, "app.rb"))
114
+
115
+ game_dir = File.join(project_root, "game")
116
+ FileUtils.cp_r(game_dir, File.join(pack_dir, "game")) if File.directory?(game_dir)
117
+
118
+ config_file = File.join(project_root, "config.rb")
119
+ FileUtils.cp(config_file, pack_dir) if File.exist?(config_file)
120
+
121
+ assets_dir = File.join(project_root, "assets")
122
+ FileUtils.cp_r(assets_dir, File.join(pack_dir, "assets")) if File.directory?(assets_dir)
123
+
124
+ ruby_wasm = File.join(dist_dir, "ruby_base.wasm")
125
+ game_wasm = File.join(dist_dir, "game.wasm")
126
+
127
+ build_base_ruby_wasm(ruby_wasm) unless File.exist?(ruby_wasm)
128
+
129
+ Bundler.with_unbundled_env do
130
+ run_command("rbwasm pack #{ruby_wasm} --dir #{pack_dir}::/src -o #{game_wasm}")
131
+ end
132
+ end
133
+
134
+ copy_ruby_wasm_js unless File.exist?(File.join(dist_dir, "ruby_wasm.js"))
135
+ end
136
+
137
+ def build_base_ruby_wasm(output_path)
138
+ puts " Downloading pre-built ruby.wasm via npm..."
139
+ Dir.mktmpdir do |tmp|
140
+ run_command("npm pack ruby-head-wasm-wasi", dir: tmp)
141
+ tarball = Dir["#{tmp}/ruby-head-wasm-wasi-*.tgz"].first
142
+ run_command("tar xf #{tarball}", dir: tmp)
143
+ FileUtils.cp(File.join(tmp, "package", "dist", "ruby.wasm"), output_path)
144
+ FileUtils.cp(
145
+ File.join(tmp, "package", "dist", "browser.esm.js"),
146
+ File.join(dist_dir, "ruby_wasm.js"),
147
+ )
148
+ end
149
+ end
150
+
151
+ def copy_ruby_wasm_js
152
+ # Already copied during build_base_ruby_wasm.
153
+ end
154
+
155
+ def copy_static_files
156
+ puts "=== Copying static files ==="
157
+ Dir[File.join(WEB_STATIC_PATH, "*")].each do |f|
158
+ FileUtils.cp(f, dist_dir)
159
+ end
160
+ end
161
+
162
+ # Auto-generate dama_core.rb by concatenating all pure-Ruby engine files.
163
+ # This replaces the manual bash script and ensures the web build always
164
+ # has the latest engine code.
165
+ CORE_FILES = %w[
166
+ version configuration keys colors
167
+ component/attribute_definition component/attribute_set component
168
+ node/component_slot node/draw_context node
169
+ scene_graph scene_graph/instance_node scene_graph/group_node
170
+ scene_graph/tag_index scene_graph/class_index scene_graph/path_selector
171
+ scene_graph/query scene_graph/tree
172
+ registry/class_resolver registry
173
+ scene/composer scene
174
+ backend/base command_buffer
175
+ geometry geometry/triangle geometry/rect geometry/circle geometry/sprite
176
+ asset_cache input/keyboard_state input/mouse_state input
177
+ camera audio event_bus sprite_sheet
178
+ physics physics/collider physics/body physics/collision physics/world
179
+ tween tween/easing tween/lerp tween/manager
180
+ animation
181
+ debug debug/frame_controller
182
+ game/builder game
183
+ backend/web
184
+ ].freeze
185
+
186
+ def generate_dama_core(output:)
187
+ lib_dir = File.expand_path("../..", __dir__)
188
+
189
+ File.open(output, "w") do |f|
190
+ f.puts "# Auto-generated at build time from lib/dama/**/*.rb"
191
+ f.puts "# DO NOT EDIT — changes will be overwritten by WebBuilder."
192
+ f.puts ""
193
+ f.puts "module Dama; def self.root = '/src'; end"
194
+ f.puts ""
195
+
196
+ CORE_FILES.each do |name|
197
+ path = File.join(lib_dir, "lib", "dama", "#{name}.rb")
198
+ next unless File.exist?(path)
199
+
200
+ f.puts "\n# --- #{File.basename(path)} ---"
201
+ f.puts File.read(path)
202
+ end
203
+
204
+ f.puts "\nmodule Dama; module Backend; def self.for = Backend::Web.new; end; end"
205
+ end
206
+ end
207
+
208
+ def run_command(cmd, dir: nil, env: {})
209
+ full_env = ENV.to_h.merge(env)
210
+ full_env["PATH"] = rust_enhanced_path(full_env.fetch("PATH", ""))
211
+ opts = dir ? { chdir: dir } : {}
212
+ success = system(full_env, cmd, **opts)
213
+ raise "Command failed: #{cmd}" unless success
214
+ end
215
+
216
+ def rust_enhanced_path(existing_path)
217
+ home = Dir.home
218
+ rust_dirs = [
219
+ File.join(home, ".cargo", "bin"),
220
+ File.join(home, ".rustup", "toolchains", "stable-#{rust_host_triple}", "bin"),
221
+ ].select { |d| File.directory?(d) }
222
+
223
+ (rust_dirs + existing_path.split(File::PATH_SEPARATOR)).join(File::PATH_SEPARATOR)
224
+ end
225
+
226
+ def rust_host_triple
227
+ arch = RUBY_PLATFORM.include?("arm64") || RUBY_PLATFORM.include?("aarch64") ? "aarch64" : "x86_64"
228
+ os_key = HOST_OS_TRIPLES.keys.detect { |k| RUBY_PLATFORM.include?(k) }
229
+ "#{arch}-#{HOST_OS_TRIPLES.fetch(os_key, "unknown-linux-gnu")}"
230
+ end
231
+ end
232
+ end
data/lib/dama.rb ADDED
@@ -0,0 +1,42 @@
1
+ require "zeitwerk"
2
+
3
+ module Dama
4
+ class << self
5
+ def loader
6
+ @loader ||= begin
7
+ loader = Zeitwerk::Loader.for_gem
8
+ loader.setup
9
+ loader
10
+ end
11
+ end
12
+
13
+ def root
14
+ File.expand_path("..", __dir__)
15
+ end
16
+
17
+ # Boot a game project. Auto-loads game files, requires config,
18
+ # and either starts the native game or builds/serves the web version.
19
+ #
20
+ # @param root [String] Path to the game project root (contains game/, config.rb, bin/)
21
+ def boot(root:)
22
+ game_dir = File.join(root, "game")
23
+ config_file = File.join(root, "config.rb")
24
+
25
+ AutoLoader.new(game_dir:).load_all
26
+ require config_file
27
+
28
+ web_requested = ARGV[0] == "web"
29
+ BOOT_ACTIONS.fetch(web_requested).call(root)
30
+ end
31
+
32
+ BOOT_ACTIONS = {
33
+ true => lambda { |root|
34
+ puts "Building and serving web version..."
35
+ WebBuilder.build_and_serve(project_root: root, port: 8080)
36
+ },
37
+ false => ->(_root) { GAME.start },
38
+ }.freeze
39
+ end
40
+ end
41
+
42
+ Dama.loader