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,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
|
|
@@ -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
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Physics
|
|
3
|
+
# Collision shape attached to a physics body.
|
|
4
|
+
# Supports AABB rectangles and circles.
|
|
5
|
+
# Positions (ax, ay, bx, by) are the top-left corner for rects
|
|
6
|
+
# and center for circles.
|
|
7
|
+
class Collider
|
|
8
|
+
attr_reader :shape, :width, :height, :radius
|
|
9
|
+
|
|
10
|
+
OVERLAP_DISPATCH = {
|
|
11
|
+
%i[rect rect] => :overlap_rect_rect?,
|
|
12
|
+
%i[circle circle] => :overlap_circle_circle?,
|
|
13
|
+
%i[rect circle] => :overlap_rect_circle?,
|
|
14
|
+
%i[circle rect] => :overlap_circle_rect?,
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
SEPARATION_DISPATCH = {
|
|
18
|
+
%i[rect rect] => :separate_rect_rect,
|
|
19
|
+
%i[circle circle] => :separate_circle_circle,
|
|
20
|
+
%i[rect circle] => :separate_rect_circle,
|
|
21
|
+
%i[circle rect] => :separate_circle_rect,
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def self.rect(width:, height:)
|
|
25
|
+
new(shape: :rect, width:, height:, radius: 0.0)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.circle(radius:)
|
|
29
|
+
new(shape: :circle, width: 0.0, height: 0.0, radius:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(shape:, width:, height:, radius:)
|
|
33
|
+
@shape = shape
|
|
34
|
+
@width = width.to_f
|
|
35
|
+
@height = height.to_f
|
|
36
|
+
@radius = radius.to_f
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def overlap?(other:, ax:, ay:, bx:, by:)
|
|
40
|
+
key = [shape, other.shape]
|
|
41
|
+
method_name = OVERLAP_DISPATCH.fetch(key)
|
|
42
|
+
send(method_name, other, ax, ay, bx, by)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def separation(other:, ax:, ay:, bx:, by:)
|
|
46
|
+
key = [shape, other.shape]
|
|
47
|
+
method_name = SEPARATION_DISPATCH.fetch(key, nil)
|
|
48
|
+
return nil unless method_name
|
|
49
|
+
|
|
50
|
+
send(method_name, other, ax, ay, bx, by)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# AABB vs AABB overlap check.
|
|
56
|
+
def overlap_rect_rect?(other, ax, ay, bx, by)
|
|
57
|
+
ax + width > bx && ax < bx + other.width &&
|
|
58
|
+
ay + height > by && ay < by + other.height
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Circle vs Circle overlap check.
|
|
62
|
+
def overlap_circle_circle?(other, ax, ay, bx, by)
|
|
63
|
+
dx = bx - ax
|
|
64
|
+
dy = by - ay
|
|
65
|
+
dist_sq = (dx * dx) + (dy * dy)
|
|
66
|
+
max_dist = radius + other.radius
|
|
67
|
+
dist_sq < max_dist * max_dist
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Rect vs Circle overlap: find closest point on rect to circle center.
|
|
71
|
+
def overlap_rect_circle?(other, ax, ay, bx, by)
|
|
72
|
+
closest_x = bx.clamp(ax, ax + width)
|
|
73
|
+
closest_y = by.clamp(ay, ay + height)
|
|
74
|
+
dx = bx - closest_x
|
|
75
|
+
dy = by - closest_y
|
|
76
|
+
(dx * dx) + (dy * dy) < other.radius * other.radius
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Circle vs Rect: delegate with swapped args.
|
|
80
|
+
def overlap_circle_rect?(other, ax, ay, bx, by)
|
|
81
|
+
other.overlap?(other: self, ax: bx, ay: by, bx: ax, by: ay)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Circle vs Circle separation: push along the line connecting centers.
|
|
85
|
+
def separate_circle_circle(other, ax, ay, bx, by)
|
|
86
|
+
return nil unless overlap_circle_circle?(other, ax, ay, bx, by)
|
|
87
|
+
|
|
88
|
+
dx = bx - ax
|
|
89
|
+
dy = by - ay
|
|
90
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
91
|
+
|
|
92
|
+
# If centers coincide, push along arbitrary axis.
|
|
93
|
+
return { dx: radius + other.radius, dy: 0.0 } if dist < 0.0001
|
|
94
|
+
|
|
95
|
+
overlap = (radius + other.radius) - dist
|
|
96
|
+
nx = dx / dist
|
|
97
|
+
ny = dy / dist
|
|
98
|
+
{ dx: overlap * nx, dy: overlap * ny }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Rect vs Circle separation.
|
|
102
|
+
def separate_rect_circle(other, ax, ay, bx, by)
|
|
103
|
+
return nil unless overlap_rect_circle?(other, ax, ay, bx, by)
|
|
104
|
+
|
|
105
|
+
closest_x = bx.clamp(ax, ax + width)
|
|
106
|
+
closest_y = by.clamp(ay, ay + height)
|
|
107
|
+
dx = bx - closest_x
|
|
108
|
+
dy = by - closest_y
|
|
109
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
110
|
+
|
|
111
|
+
return { dx: other.radius, dy: 0.0 } if dist < 0.0001
|
|
112
|
+
|
|
113
|
+
overlap = other.radius - dist
|
|
114
|
+
nx = dx / dist
|
|
115
|
+
ny = dy / dist
|
|
116
|
+
{ dx: overlap * nx, dy: overlap * ny }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Circle vs Rect: delegate and flip.
|
|
120
|
+
def separate_circle_rect(other, ax, ay, bx, by)
|
|
121
|
+
result = other.separation(other: self, ax: bx, ay: by, bx: ax, by: ay)
|
|
122
|
+
return nil unless result
|
|
123
|
+
|
|
124
|
+
{ dx: -result.fetch(:dx), dy: -result.fetch(:dy) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Minimum translation vector to push b out of a (rect vs rect).
|
|
128
|
+
# Returns { dx:, dy: } or nil if no overlap.
|
|
129
|
+
def separate_rect_rect(other, ax, ay, bx, by) # rubocop:disable Metrics/AbcSize
|
|
130
|
+
return nil unless overlap_rect_rect?(other, ax, ay, bx, by)
|
|
131
|
+
|
|
132
|
+
# Calculate overlap on each axis.
|
|
133
|
+
overlap_x = (ax + width) < (bx + other.width) ? (ax + width - bx) : (bx + other.width - ax)
|
|
134
|
+
overlap_y = (ay + height) < (by + other.height) ? (ay + height - by) : (by + other.height - ay)
|
|
135
|
+
|
|
136
|
+
# Determine separation direction (sign).
|
|
137
|
+
center_ax = ax + (width / 2.0)
|
|
138
|
+
center_bx = bx + (other.width / 2.0)
|
|
139
|
+
center_ay = ay + (height / 2.0)
|
|
140
|
+
center_by = by + (other.height / 2.0)
|
|
141
|
+
|
|
142
|
+
sign_x = center_bx >= center_ax ? 1.0 : -1.0
|
|
143
|
+
sign_y = center_by >= center_ay ? 1.0 : -1.0
|
|
144
|
+
|
|
145
|
+
# Separate along the axis of least penetration.
|
|
146
|
+
return { dx: overlap_x * sign_x, dy: 0.0 } if overlap_x.abs <= overlap_y.abs
|
|
147
|
+
|
|
148
|
+
{ dx: 0.0, dy: overlap_y * sign_y }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Physics
|
|
3
|
+
# Value object representing a collision between two physics bodies.
|
|
4
|
+
class Collision
|
|
5
|
+
attr_reader :body_a, :body_b, :separation_x, :separation_y
|
|
6
|
+
|
|
7
|
+
def initialize(body_a:, body_b:, separation_x:, separation_y:)
|
|
8
|
+
@body_a = body_a
|
|
9
|
+
@body_b = body_b
|
|
10
|
+
@separation_x = separation_x
|
|
11
|
+
@separation_y = separation_y
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|