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.
- 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/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/native/libdama_native.so +0 -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 +186 -0
data/lib/dama/game.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
# Top-level orchestrator for a Dama game.
|
|
3
|
+
#
|
|
4
|
+
# Example:
|
|
5
|
+
# game = Dama::Game.new do
|
|
6
|
+
# settings resolution: [1280, 720], title: "My Game"
|
|
7
|
+
# start_scene MenuScene
|
|
8
|
+
# end
|
|
9
|
+
# game.start
|
|
10
|
+
class Game
|
|
11
|
+
attr_reader :registry, :configuration, :backend, :asset_cache
|
|
12
|
+
|
|
13
|
+
def initialize(&)
|
|
14
|
+
@registry = Registry.new
|
|
15
|
+
@builder = Game::Builder.new(registry:)
|
|
16
|
+
builder.instance_eval(&)
|
|
17
|
+
@configuration = builder.configuration
|
|
18
|
+
@start_scene_class = builder.start_scene_class
|
|
19
|
+
@backend = Backend.for
|
|
20
|
+
@asset_cache = AssetCache.new(backend:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start
|
|
24
|
+
run_game(frame_controller: Debug::FrameController.new)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Run exactly N frames, then stop. For debugging and testing.
|
|
28
|
+
def run_frames(count)
|
|
29
|
+
run_game(frame_controller: Debug::FrameController.new(frame_limit: count))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def screenshot(output_path)
|
|
33
|
+
screenshot_tool.capture(output_path:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :builder, :start_scene_class, :current_scene, :pending_scene_class
|
|
39
|
+
|
|
40
|
+
def run_game(frame_controller:)
|
|
41
|
+
backend.initialize_engine(configuration:)
|
|
42
|
+
load_initial_scene
|
|
43
|
+
game_loop(frame_controller:).run
|
|
44
|
+
ensure
|
|
45
|
+
asset_cache.release_all
|
|
46
|
+
backend.shutdown
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def load_initial_scene
|
|
50
|
+
@current_scene = build_scene(start_scene_class)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_scene(scene_class)
|
|
54
|
+
scene = scene_class.new(
|
|
55
|
+
registry:, asset_cache:, backend:,
|
|
56
|
+
scene_switcher: method(:request_scene_switch)
|
|
57
|
+
)
|
|
58
|
+
scene.perform_compose
|
|
59
|
+
scene.perform_enter
|
|
60
|
+
scene
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def request_scene_switch(scene_class)
|
|
64
|
+
@pending_scene_class = scene_class
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def apply_pending_scene_switch
|
|
68
|
+
return unless pending_scene_class
|
|
69
|
+
|
|
70
|
+
@current_scene = build_scene(pending_scene_class)
|
|
71
|
+
@pending_scene_class = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def game_loop(frame_controller:)
|
|
75
|
+
@game_loop ||= Game::Loop.new(
|
|
76
|
+
backend:,
|
|
77
|
+
scene_provider: -> { current_scene },
|
|
78
|
+
frame_controller:,
|
|
79
|
+
input: Input.new(backend:),
|
|
80
|
+
scene_transition: method(:apply_pending_scene_switch),
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def screenshot_tool
|
|
85
|
+
@screenshot_tool ||= Debug::ScreenshotTool.new(backend:)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Geometry
|
|
3
|
+
# Decomposes a circle into a triangle fan (segments × 3 vertices).
|
|
4
|
+
# Each vertex is [x, y, r, g, b, a, u, v] — 8 floats in pixel coordinates.
|
|
5
|
+
class Circle
|
|
6
|
+
def self.vertices(cx:, cy:, radius:, r:, g:, b:, a:, segments: 32)
|
|
7
|
+
result = []
|
|
8
|
+
angle_step = (2.0 * Math::PI) / segments
|
|
9
|
+
|
|
10
|
+
segments.times do |i|
|
|
11
|
+
angle1 = angle_step * i
|
|
12
|
+
angle2 = angle_step * (i + 1)
|
|
13
|
+
|
|
14
|
+
x1 = cx + (radius * Math.cos(angle1))
|
|
15
|
+
y1 = cy + (radius * Math.sin(angle1))
|
|
16
|
+
x2 = cx + (radius * Math.cos(angle2))
|
|
17
|
+
y2 = cy + (radius * Math.sin(angle2))
|
|
18
|
+
|
|
19
|
+
result.push(cx, cy, r, g, b, a, 0.0, 0.0,
|
|
20
|
+
x1, y1, r, g, b, a, 0.0, 0.0,
|
|
21
|
+
x2, y2, r, g, b, a, 0.0, 0.0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Geometry
|
|
3
|
+
# Decomposes a rectangle into 2 triangles (6 vertices).
|
|
4
|
+
# Each vertex is [x, y, r, g, b, a, u, v] — 8 floats in pixel coordinates.
|
|
5
|
+
class Rect
|
|
6
|
+
def self.vertices(x:, y:, w:, h:, r:, g:, b:, a:)
|
|
7
|
+
[x, y, r, g, b, a, 0.0, 0.0,
|
|
8
|
+
x + w, y, r, g, b, a, 0.0, 0.0,
|
|
9
|
+
x, y + h, r, g, b, a, 0.0, 0.0,
|
|
10
|
+
x + w, y, r, g, b, a, 0.0, 0.0,
|
|
11
|
+
x + w, y + h, r, g, b, a, 0.0, 0.0,
|
|
12
|
+
x, y + h, r, g, b, a, 0.0, 0.0]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Geometry
|
|
3
|
+
# Decomposes a textured sprite quad into 2 triangles (6 vertices).
|
|
4
|
+
# Each vertex is [x, y, r, g, b, a, u, v] — 8 floats in pixel coordinates.
|
|
5
|
+
# UV maps the full texture (0,0)→(1,1) by default; override for atlas sub-regions.
|
|
6
|
+
class Sprite
|
|
7
|
+
def self.vertices(x:, y:, w:, h:, r: 1.0, g: 1.0, b: 1.0, a: 1.0,
|
|
8
|
+
u_min: 0.0, v_min: 0.0, u_max: 1.0, v_max: 1.0)
|
|
9
|
+
[x, y, r, g, b, a, u_min, v_min,
|
|
10
|
+
x + w, y, r, g, b, a, u_max, v_min,
|
|
11
|
+
x, y + h, r, g, b, a, u_min, v_max,
|
|
12
|
+
x + w, y, r, g, b, a, u_max, v_min,
|
|
13
|
+
x + w, y + h, r, g, b, a, u_max, v_max,
|
|
14
|
+
x, y + h, r, g, b, a, u_min, v_max]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Geometry
|
|
3
|
+
# Decomposes a triangle into 3 vertices.
|
|
4
|
+
# Each vertex is [x, y, r, g, b, a, u, v] — 8 floats in pixel coordinates.
|
|
5
|
+
class Triangle
|
|
6
|
+
def self.vertices(x1:, y1:, x2:, y2:, x3:, y3:, r:, g:, b:, a:)
|
|
7
|
+
[x1, y1, r, g, b, a, 0.0, 0.0,
|
|
8
|
+
x2, y2, r, g, b, a, 0.0, 0.0,
|
|
9
|
+
x3, y3, r, g, b, a, 0.0, 0.0]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
class Input
|
|
3
|
+
# Maps symbolic key names to winit KeyCode values and queries
|
|
4
|
+
# the backend for current key state.
|
|
5
|
+
class KeyboardState
|
|
6
|
+
# Maps symbolic key names to Dama::Keys constants.
|
|
7
|
+
KEY_CODES = {
|
|
8
|
+
left: Keys::ARROW_LEFT,
|
|
9
|
+
right: Keys::ARROW_RIGHT,
|
|
10
|
+
up: Keys::ARROW_UP,
|
|
11
|
+
down: Keys::ARROW_DOWN,
|
|
12
|
+
space: Keys::SPACE,
|
|
13
|
+
enter: Keys::ENTER,
|
|
14
|
+
escape: Keys::ESCAPE,
|
|
15
|
+
a: Keys::KEY_A,
|
|
16
|
+
b: Keys::KEY_B,
|
|
17
|
+
c: Keys::KEY_C,
|
|
18
|
+
d: Keys::KEY_D,
|
|
19
|
+
w: Keys::KEY_W,
|
|
20
|
+
s: Keys::KEY_S,
|
|
21
|
+
equal: Keys::EQUAL,
|
|
22
|
+
minus: Keys::MINUS,
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def initialize(backend:)
|
|
26
|
+
@backend = backend
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pressed?(key:)
|
|
30
|
+
code = KEY_CODES.fetch(key)
|
|
31
|
+
backend.key_pressed?(key_code: code)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def just_pressed?(key:)
|
|
35
|
+
code = KEY_CODES.fetch(key)
|
|
36
|
+
backend.key_just_pressed?(key_code: code)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_reader :backend
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
class Input
|
|
3
|
+
# Tracks mouse position and button state with edge detection.
|
|
4
|
+
# Call #update once per frame before querying.
|
|
5
|
+
class MouseState
|
|
6
|
+
BUTTON_CODES = {
|
|
7
|
+
left: 0,
|
|
8
|
+
right: 1,
|
|
9
|
+
middle: 2,
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(backend:)
|
|
13
|
+
@backend = backend
|
|
14
|
+
@previous_pressed = Hash.new(false)
|
|
15
|
+
@current_pressed = Hash.new(false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def x = backend.mouse_x
|
|
19
|
+
def y = backend.mouse_y
|
|
20
|
+
|
|
21
|
+
def pressed?(button:)
|
|
22
|
+
code = BUTTON_CODES.fetch(button)
|
|
23
|
+
backend.mouse_button_pressed?(button: code)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns true only on the frame the button transitions
|
|
27
|
+
# from released to pressed (edge detection).
|
|
28
|
+
def just_pressed?(button:)
|
|
29
|
+
current_pressed.fetch(button, false) && !previous_pressed.fetch(button, false)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Must be called once per frame to track state transitions.
|
|
33
|
+
def update
|
|
34
|
+
BUTTON_CODES.each_key do |button|
|
|
35
|
+
previous_pressed[button] = current_pressed[button]
|
|
36
|
+
current_pressed[button] = pressed?(button:)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :backend, :previous_pressed, :current_pressed
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/dama/input.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
# Snapshot of input state for a single frame.
|
|
3
|
+
# Provides convenience methods for common input queries.
|
|
4
|
+
class Input
|
|
5
|
+
def initialize(backend:)
|
|
6
|
+
@keyboard = Input::KeyboardState.new(backend:)
|
|
7
|
+
@mouse = Input::MouseState.new(backend:)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Generic key queries — works for any named key.
|
|
11
|
+
def key_pressed?(key) = keyboard.pressed?(key:)
|
|
12
|
+
def key_just_pressed?(key) = keyboard.just_pressed?(key:)
|
|
13
|
+
|
|
14
|
+
# Keyboard convenience methods.
|
|
15
|
+
def left? = keyboard.pressed?(key: :left)
|
|
16
|
+
def right? = keyboard.pressed?(key: :right)
|
|
17
|
+
def up? = keyboard.pressed?(key: :up)
|
|
18
|
+
def down? = keyboard.pressed?(key: :down)
|
|
19
|
+
def space? = keyboard.pressed?(key: :space)
|
|
20
|
+
def escape? = keyboard.pressed?(key: :escape)
|
|
21
|
+
|
|
22
|
+
# Mouse convenience methods.
|
|
23
|
+
def mouse_x = mouse.x
|
|
24
|
+
def mouse_y = mouse.y
|
|
25
|
+
def mouse_pressed?(button) = mouse.pressed?(button:)
|
|
26
|
+
def mouse_just_pressed?(button) = mouse.just_pressed?(button:)
|
|
27
|
+
def mouse_clicked? = mouse.just_pressed?(button: :left)
|
|
28
|
+
|
|
29
|
+
# Must be called once per frame to track mouse button transitions.
|
|
30
|
+
def update
|
|
31
|
+
mouse.update
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :keyboard, :mouse
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/dama/keys.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
# Named constants for keyboard key codes matching winit's KeyCode enum.
|
|
3
|
+
# Use these instead of raw numeric values when querying input state.
|
|
4
|
+
#
|
|
5
|
+
# Values are the Rust enum discriminant indices for winit::keyboard::KeyCode.
|
|
6
|
+
module Keys
|
|
7
|
+
ARROW_LEFT = 80
|
|
8
|
+
ARROW_RIGHT = 81
|
|
9
|
+
ARROW_UP = 82
|
|
10
|
+
ARROW_DOWN = 79
|
|
11
|
+
|
|
12
|
+
SPACE = 62
|
|
13
|
+
ENTER = 57
|
|
14
|
+
ESCAPE = 114
|
|
15
|
+
|
|
16
|
+
BACKSPACE = 52
|
|
17
|
+
TAB = 63
|
|
18
|
+
|
|
19
|
+
LEFT_SHIFT = 60
|
|
20
|
+
LEFT_CTRL = 55
|
|
21
|
+
LEFT_ALT = 50
|
|
22
|
+
RIGHT_SHIFT = 61
|
|
23
|
+
RIGHT_CTRL = 56
|
|
24
|
+
RIGHT_ALT = 51
|
|
25
|
+
|
|
26
|
+
KEY_A = 19
|
|
27
|
+
KEY_B = 20
|
|
28
|
+
KEY_C = 21
|
|
29
|
+
KEY_D = 22
|
|
30
|
+
KEY_E = 23
|
|
31
|
+
KEY_F = 24
|
|
32
|
+
KEY_G = 25
|
|
33
|
+
KEY_H = 26
|
|
34
|
+
KEY_I = 27
|
|
35
|
+
KEY_J = 28
|
|
36
|
+
KEY_K = 29
|
|
37
|
+
KEY_L = 30
|
|
38
|
+
KEY_M = 31
|
|
39
|
+
KEY_N = 32
|
|
40
|
+
KEY_O = 33
|
|
41
|
+
KEY_P = 34
|
|
42
|
+
KEY_Q = 35
|
|
43
|
+
KEY_R = 36
|
|
44
|
+
KEY_S = 37
|
|
45
|
+
KEY_T = 38
|
|
46
|
+
KEY_U = 39
|
|
47
|
+
KEY_V = 40
|
|
48
|
+
KEY_W = 41
|
|
49
|
+
KEY_X = 42
|
|
50
|
+
KEY_Y = 43
|
|
51
|
+
KEY_Z = 44
|
|
52
|
+
|
|
53
|
+
EQUAL = 15
|
|
54
|
+
MINUS = 45
|
|
55
|
+
|
|
56
|
+
DIGIT_0 = 5
|
|
57
|
+
DIGIT_1 = 6
|
|
58
|
+
DIGIT_2 = 7
|
|
59
|
+
DIGIT_3 = 8
|
|
60
|
+
DIGIT_4 = 9
|
|
61
|
+
DIGIT_5 = 10
|
|
62
|
+
DIGIT_6 = 11
|
|
63
|
+
DIGIT_7 = 12
|
|
64
|
+
DIGIT_8 = 13
|
|
65
|
+
DIGIT_9 = 14
|
|
66
|
+
end
|
|
67
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
class Node
|
|
3
|
+
# Binds a Component class to its default attribute values.
|
|
4
|
+
# When a Node is instantiated, each slot builds a component instance.
|
|
5
|
+
class ComponentSlot
|
|
6
|
+
attr_reader :component_class, :defaults
|
|
7
|
+
|
|
8
|
+
def initialize(component_class:, defaults:)
|
|
9
|
+
@component_class = component_class
|
|
10
|
+
@defaults = defaults
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build(**overrides)
|
|
14
|
+
component_class.new(**defaults, **overrides)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
class Node
|
|
3
|
+
# Evaluation context for a Node's draw block. Provides drawing
|
|
4
|
+
# primitives (rect, triangle, circle) and exposes all node
|
|
5
|
+
# attributes and component accessors as direct methods.
|
|
6
|
+
#
|
|
7
|
+
# When a camera is present, all coordinates are automatically
|
|
8
|
+
# transformed from world space to screen space.
|
|
9
|
+
#
|
|
10
|
+
# Custom shaders can be applied to any shape via the `shader:` keyword:
|
|
11
|
+
# rect(x, y, w, h, color: Dama::Colors::RED, shader: glow)
|
|
12
|
+
class DrawContext
|
|
13
|
+
def initialize(node:, backend:, camera: nil)
|
|
14
|
+
@node = node
|
|
15
|
+
@backend = backend
|
|
16
|
+
@camera = camera
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def rect(x, y, w, h, color: Dama::Colors::WHITE, r: color.r, g: color.g, b: color.b, a: color.a, shader: nil)
|
|
20
|
+
sx, sy = apply_camera(x, y)
|
|
21
|
+
sw = w * zoom_factor
|
|
22
|
+
sh = h * zoom_factor
|
|
23
|
+
with_shader(shader) { backend.draw_rect(x: sx, y: sy, w: sw, h: sh, r:, g:, b:, a:) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def triangle(x1, y1, x2, y2, x3, y3, color: Dama::Colors::WHITE,
|
|
27
|
+
r: color.r, g: color.g, b: color.b, a: color.a, shader: nil)
|
|
28
|
+
sx1, sy1 = apply_camera(x1, y1)
|
|
29
|
+
sx2, sy2 = apply_camera(x2, y2)
|
|
30
|
+
sx3, sy3 = apply_camera(x3, y3)
|
|
31
|
+
with_shader(shader) do
|
|
32
|
+
backend.draw_triangle(x1: sx1, y1: sy1, x2: sx2, y2: sy2, x3: sx3, y3: sy3, r:, g:, b:, a:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def circle(cx, cy, radius, color: Dama::Colors::WHITE,
|
|
37
|
+
r: color.r, g: color.g, b: color.b, a: color.a, segments: 32, shader: nil)
|
|
38
|
+
sx, sy = apply_camera(cx, cy)
|
|
39
|
+
with_shader(shader) do
|
|
40
|
+
backend.draw_circle(cx: sx, cy: sy, radius: radius * zoom_factor, r:, g:, b:, a:, segments:)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def text(content, x, y, size: 24.0, color: Dama::Colors::WHITE,
|
|
45
|
+
r: color.r, g: color.g, b: color.b, a: color.a, font: nil)
|
|
46
|
+
sx, sy = apply_camera(x, y)
|
|
47
|
+
backend.draw_text(text: content.to_s, x: sx, y: sy, size: size * zoom_factor, r:, g:, b:, a:, font:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sprite(texture_handle, x, y, w, h, color: Dama::Colors::WHITE,
|
|
51
|
+
r: color.r, g: color.g, b: color.b, a: color.a, shader: nil)
|
|
52
|
+
sx, sy = apply_camera(x, y)
|
|
53
|
+
sw = w * zoom_factor
|
|
54
|
+
sh = h * zoom_factor
|
|
55
|
+
with_shader(shader) do
|
|
56
|
+
backend.draw_sprite(texture_handle:, x: sx, y: sy, w: sw, h: sh, r:, g:, b:, a:)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def method_missing(method_name, ...)
|
|
61
|
+
return super unless node.respond_to?(method_name)
|
|
62
|
+
|
|
63
|
+
node.public_send(method_name, ...)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
67
|
+
node.respond_to?(method_name, include_private) || super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
attr_reader :node, :backend, :camera
|
|
73
|
+
|
|
74
|
+
def apply_camera(world_x, world_y)
|
|
75
|
+
return [world_x, world_y] unless camera
|
|
76
|
+
|
|
77
|
+
result = camera.world_to_screen(world_x:, world_y:)
|
|
78
|
+
[result.fetch(:screen_x), result.fetch(:screen_y)]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def zoom_factor
|
|
82
|
+
camera ? camera.zoom : 1.0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Wraps a draw call with shader activation/deactivation.
|
|
86
|
+
# If no shader is specified, the block executes unchanged.
|
|
87
|
+
def with_shader(shader_handle)
|
|
88
|
+
return yield unless shader_handle
|
|
89
|
+
|
|
90
|
+
backend.set_shader(handle: shader_handle)
|
|
91
|
+
yield
|
|
92
|
+
backend.set_shader(handle: 0)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/dama/node.rb
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
# Base class for game entities. Nodes compose Components via named
|
|
3
|
+
# accessors, declare attributes, textures, and define draw behavior.
|
|
4
|
+
#
|
|
5
|
+
# Example:
|
|
6
|
+
# class Player < Dama::Node
|
|
7
|
+
# component Transform, as: :transform, x: 50.0, y: 50.0
|
|
8
|
+
# texture :sprite, path: "assets/player.png"
|
|
9
|
+
# attribute :name, default: "Player"
|
|
10
|
+
#
|
|
11
|
+
# draw do
|
|
12
|
+
# sprite(sprite, transform.x, transform.y, 32, 32)
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
class Node
|
|
16
|
+
class << self
|
|
17
|
+
def component(component_class, as:, **defaults)
|
|
18
|
+
component_slots[as] = ComponentSlot.new(component_class:, defaults:)
|
|
19
|
+
define_method(as) { components.fetch(as) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attribute(name, default: nil)
|
|
23
|
+
attribute_definitions[name] = default
|
|
24
|
+
attr_accessor name
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Declare a texture asset with a named accessor.
|
|
28
|
+
# The texture is loaded via AssetCache during scene composition
|
|
29
|
+
# and the GPU handle is accessible as a method on the node.
|
|
30
|
+
def texture(name, path:)
|
|
31
|
+
texture_declarations[name] = path
|
|
32
|
+
define_method(name) { texture_handles.fetch(name) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Declare a custom WGSL fragment shader with a named accessor.
|
|
36
|
+
# The shader is loaded via the backend during scene composition
|
|
37
|
+
# and the handle is accessible as a method on the node.
|
|
38
|
+
#
|
|
39
|
+
# Example:
|
|
40
|
+
# shader :glow, path: "assets/shaders/glow.wgsl"
|
|
41
|
+
# shader :invert, source: "@fragment\nfn fs_main(...) { ... }"
|
|
42
|
+
def shader(name, path: nil, source: nil)
|
|
43
|
+
shader_declarations[name] = { path:, source: }
|
|
44
|
+
define_method(name) { shader_handles.fetch(name) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def draw(&block)
|
|
48
|
+
@draw_block = block
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def component_slots
|
|
52
|
+
@component_slots ||= {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def attribute_definitions
|
|
56
|
+
@attribute_definitions ||= {}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def texture_declarations
|
|
60
|
+
@texture_declarations ||= {}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def shader_declarations
|
|
64
|
+
@shader_declarations ||= {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Declare a physics body for this node.
|
|
68
|
+
# The body is created during scene composition if the scene has physics enabled.
|
|
69
|
+
def physics_body(**options)
|
|
70
|
+
@physics_body_options = options
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
attr_reader :physics_body_options, :draw_block
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attr_accessor :physics
|
|
77
|
+
|
|
78
|
+
def initialize(**values)
|
|
79
|
+
@components = {}
|
|
80
|
+
@texture_handles = {}
|
|
81
|
+
@shader_handles = {}
|
|
82
|
+
@physics = nil
|
|
83
|
+
initialize_components(values)
|
|
84
|
+
initialize_attributes(values)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Load all declared textures via the AssetCache.
|
|
88
|
+
def load_textures(asset_cache:)
|
|
89
|
+
self.class.texture_declarations.each do |name, path|
|
|
90
|
+
texture_handles[name] = asset_cache.acquire(path:)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Release all declared textures from the AssetCache.
|
|
95
|
+
def unload_textures(asset_cache:)
|
|
96
|
+
self.class.texture_declarations.each_value do |path|
|
|
97
|
+
asset_cache.release(path:)
|
|
98
|
+
end
|
|
99
|
+
texture_handles.clear
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Load all declared shaders via the backend.
|
|
103
|
+
def load_shaders(backend:)
|
|
104
|
+
self.class.shader_declarations.each do |name, declaration|
|
|
105
|
+
source = declaration[:source] || File.read(declaration.fetch(:path))
|
|
106
|
+
shader_handles[name] = backend.load_shader(source:)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Unload all declared shaders via the backend.
|
|
111
|
+
def unload_shaders(backend:)
|
|
112
|
+
shader_handles.each_value do |handle|
|
|
113
|
+
backend.unload_shader(handle:)
|
|
114
|
+
end
|
|
115
|
+
shader_handles.clear
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
attr_reader :components, :texture_handles, :shader_handles
|
|
121
|
+
|
|
122
|
+
def initialize_components(values)
|
|
123
|
+
self.class.component_slots.each do |name, slot|
|
|
124
|
+
# Allow constructor values to override component defaults.
|
|
125
|
+
# e.g., PieceNode.new(x: 100.0) overrides Transform's default x.
|
|
126
|
+
component_attrs = slot.component_class.attribute_set.map(&:name)
|
|
127
|
+
overrides = values.slice(*component_attrs)
|
|
128
|
+
components[name] = slot.build(**overrides)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def initialize_attributes(values)
|
|
133
|
+
self.class.attribute_definitions.each do |name, default|
|
|
134
|
+
value = values.fetch(name, default)
|
|
135
|
+
public_send(:"#{name}=", value)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Physics
|
|
3
|
+
# A physics body attached to a node. Tracks velocity, acceleration,
|
|
4
|
+
# and collider shape. Updated by Physics::World each frame.
|
|
5
|
+
class Body
|
|
6
|
+
BODY_TYPES = %i[dynamic static kinematic].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :type, :mass, :collider, :node
|
|
9
|
+
attr_accessor :velocity_x, :velocity_y, :acceleration_x, :acceleration_y,
|
|
10
|
+
:restitution
|
|
11
|
+
|
|
12
|
+
def initialize(type:, collider:, mass: 1.0, node: nil, restitution: 0.0)
|
|
13
|
+
@type = type
|
|
14
|
+
@mass = mass.to_f
|
|
15
|
+
@collider = collider
|
|
16
|
+
@node = node
|
|
17
|
+
@velocity_x = 0.0
|
|
18
|
+
@velocity_y = 0.0
|
|
19
|
+
@acceleration_x = 0.0
|
|
20
|
+
@acceleration_y = 0.0
|
|
21
|
+
@restitution = restitution.to_f
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dynamic? = type == :dynamic
|
|
25
|
+
def static? = type == :static
|
|
26
|
+
def kinematic? = type == :kinematic
|
|
27
|
+
|
|
28
|
+
# Current position from the node's transform component.
|
|
29
|
+
def x
|
|
30
|
+
node.transform.x
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def y
|
|
34
|
+
node.transform.y
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def x=(value)
|
|
38
|
+
node.transform.x = value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def y=(value)
|
|
42
|
+
node.transform.y = value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Integrate velocity and acceleration over delta_time.
|
|
46
|
+
def integrate(delta_time:, gravity_x: 0.0, gravity_y: 0.0)
|
|
47
|
+
return unless dynamic?
|
|
48
|
+
|
|
49
|
+
self.velocity_x += (acceleration_x + gravity_x) * delta_time
|
|
50
|
+
self.velocity_y += (acceleration_y + gravity_y) * delta_time
|
|
51
|
+
|
|
52
|
+
self.x = x + (velocity_x * delta_time)
|
|
53
|
+
self.y = y + (velocity_y * delta_time)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|