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,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
|