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
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,4 @@
1
+ module Dama
2
+ module Geometry
3
+ end
4
+ 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