dama 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +227 -0
  4. data/dama-logo.svg +91 -0
  5. data/exe/dama +4 -0
  6. data/ext/dama_native/.cargo/config.toml +3 -0
  7. data/ext/dama_native/Cargo.lock +3575 -0
  8. data/ext/dama_native/Cargo.toml +39 -0
  9. data/ext/dama_native/extconf.rb +72 -0
  10. data/ext/dama_native/src/audio.rs +134 -0
  11. data/ext/dama_native/src/engine.rs +339 -0
  12. data/ext/dama_native/src/lib.rs +396 -0
  13. data/ext/dama_native/src/renderer/screenshot.rs +84 -0
  14. data/ext/dama_native/src/renderer/shape_renderer.rs +507 -0
  15. data/ext/dama_native/src/renderer/text_renderer.rs +192 -0
  16. data/ext/dama_native/src/renderer.rs +563 -0
  17. data/ext/dama_native/src/window.rs +255 -0
  18. data/lib/dama/animation.rb +66 -0
  19. data/lib/dama/asset_cache.rb +56 -0
  20. data/lib/dama/audio.rb +47 -0
  21. data/lib/dama/auto_loader.rb +54 -0
  22. data/lib/dama/backend/base.rb +137 -0
  23. data/lib/dama/backend/native/ffi_bindings.rb +122 -0
  24. data/lib/dama/backend/native.rb +191 -0
  25. data/lib/dama/backend/web.rb +199 -0
  26. data/lib/dama/backend.rb +13 -0
  27. data/lib/dama/camera.rb +68 -0
  28. data/lib/dama/cli/new_project.rb +112 -0
  29. data/lib/dama/cli/release.rb +45 -0
  30. data/lib/dama/cli.rb +22 -0
  31. data/lib/dama/colors.rb +30 -0
  32. data/lib/dama/command_buffer.rb +83 -0
  33. data/lib/dama/component/attribute_definition.rb +13 -0
  34. data/lib/dama/component/attribute_set.rb +32 -0
  35. data/lib/dama/component.rb +28 -0
  36. data/lib/dama/configuration.rb +18 -0
  37. data/lib/dama/debug/frame_controller.rb +35 -0
  38. data/lib/dama/debug/screenshot_tool.rb +19 -0
  39. data/lib/dama/debug.rb +4 -0
  40. data/lib/dama/event_bus.rb +47 -0
  41. data/lib/dama/game/builder.rb +31 -0
  42. data/lib/dama/game/loop.rb +44 -0
  43. data/lib/dama/game.rb +88 -0
  44. data/lib/dama/geometry/circle.rb +28 -0
  45. data/lib/dama/geometry/rect.rb +16 -0
  46. data/lib/dama/geometry/sprite.rb +18 -0
  47. data/lib/dama/geometry/triangle.rb +13 -0
  48. data/lib/dama/geometry.rb +4 -0
  49. data/lib/dama/input/keyboard_state.rb +44 -0
  50. data/lib/dama/input/mouse_state.rb +45 -0
  51. data/lib/dama/input.rb +38 -0
  52. data/lib/dama/keys.rb +67 -0
  53. data/lib/dama/node/component_slot.rb +18 -0
  54. data/lib/dama/node/draw_context.rb +96 -0
  55. data/lib/dama/node.rb +139 -0
  56. data/lib/dama/physics/body.rb +57 -0
  57. data/lib/dama/physics/collider.rb +152 -0
  58. data/lib/dama/physics/collision.rb +15 -0
  59. data/lib/dama/physics/world.rb +125 -0
  60. data/lib/dama/physics.rb +4 -0
  61. data/lib/dama/registry/class_resolver.rb +48 -0
  62. data/lib/dama/registry.rb +21 -0
  63. data/lib/dama/release/archiver.rb +100 -0
  64. data/lib/dama/release/defaults/icon.icns +0 -0
  65. data/lib/dama/release/defaults/icon.ico +0 -0
  66. data/lib/dama/release/defaults/icon.png +0 -0
  67. data/lib/dama/release/dylib_relinker.rb +95 -0
  68. data/lib/dama/release/game_file_copier.rb +35 -0
  69. data/lib/dama/release/game_metadata.rb +61 -0
  70. data/lib/dama/release/icon_provider.rb +36 -0
  71. data/lib/dama/release/native_builder.rb +44 -0
  72. data/lib/dama/release/packager/linux.rb +62 -0
  73. data/lib/dama/release/packager/macos.rb +99 -0
  74. data/lib/dama/release/packager/web.rb +32 -0
  75. data/lib/dama/release/packager/windows.rb +61 -0
  76. data/lib/dama/release/packager.rb +9 -0
  77. data/lib/dama/release/platform_detector.rb +23 -0
  78. data/lib/dama/release/ruby_bundler.rb +163 -0
  79. data/lib/dama/release/stdlib_trimmer.rb +133 -0
  80. data/lib/dama/release/template_renderer.rb +40 -0
  81. data/lib/dama/release/templates/info_plist.xml.erb +19 -0
  82. data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
  83. data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
  84. data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
  85. data/lib/dama/release.rb +7 -0
  86. data/lib/dama/scene/composer.rb +65 -0
  87. data/lib/dama/scene.rb +233 -0
  88. data/lib/dama/scene_graph/class_index.rb +26 -0
  89. data/lib/dama/scene_graph/group_node.rb +27 -0
  90. data/lib/dama/scene_graph/instance_node.rb +30 -0
  91. data/lib/dama/scene_graph/path_selector.rb +25 -0
  92. data/lib/dama/scene_graph/query.rb +34 -0
  93. data/lib/dama/scene_graph/tag_index.rb +26 -0
  94. data/lib/dama/scene_graph/tree.rb +65 -0
  95. data/lib/dama/scene_graph.rb +4 -0
  96. data/lib/dama/sprite_sheet.rb +36 -0
  97. data/lib/dama/tween/easing.rb +31 -0
  98. data/lib/dama/tween/lerp.rb +35 -0
  99. data/lib/dama/tween/manager.rb +28 -0
  100. data/lib/dama/tween.rb +4 -0
  101. data/lib/dama/version.rb +3 -0
  102. data/lib/dama/vertex_batch.rb +35 -0
  103. data/lib/dama/web/entry.rb +79 -0
  104. data/lib/dama/web/static/index.html +142 -0
  105. data/lib/dama/web_builder.rb +232 -0
  106. data/lib/dama.rb +42 -0
  107. metadata +198 -0
@@ -0,0 +1,65 @@
1
+ module Dama
2
+ class Scene
3
+ # Evaluates compose blocks to build the scene graph.
4
+ # Supports multiple forms of `add`:
5
+ # add PlayerClass, as: :hero # by Class
6
+ # add :player, as: :hero # by Symbol (auto-discover)
7
+ # add "player", as: :hero # by String (auto-discover)
8
+ # add Player.new, as: :hero # by Instance
9
+ # add Player, as: :hero, tags: [:enemy] # with explicit tags
10
+ class Composer
11
+ RESOLVE_STRATEGIES = {
12
+ Symbol => ->(name, registry) { registry.resolve(name:, category: :node) },
13
+ String => ->(name, registry) { registry.resolve(name: name.to_sym, category: :node) },
14
+ }.freeze
15
+
16
+ def initialize(scene_graph:, registry:, scene:)
17
+ @scene_graph = scene_graph
18
+ @registry = registry
19
+ @scene = scene
20
+ @current_group = nil
21
+ end
22
+
23
+ def add(class_or_name_or_instance, as:, tags: [], **props, &block)
24
+ node_instance = resolve_and_build(class_or_name_or_instance, props)
25
+ instance_node = SceneGraph::InstanceNode.new(id: as, node: node_instance, tags:)
26
+ scene_graph.add(instance_node:, parent_group: current_group)
27
+
28
+ # Register the named accessor on the scene so `hero` works in update/enter.
29
+ scene.register_named_node(name: as, instance_node:)
30
+
31
+ instance_node.instance_eval(&block) if block
32
+ end
33
+
34
+ def camera(**)
35
+ scene.enable_camera(**)
36
+ end
37
+
38
+ def physics(gravity: [0.0, 0.0])
39
+ scene.enable_physics(gravity_x: gravity[0], gravity_y: gravity[1])
40
+ end
41
+
42
+ def group(name, &)
43
+ scene_graph.add_group(name:)
44
+ previous_group = current_group
45
+ @current_group = name
46
+ instance_eval(&)
47
+ @current_group = previous_group
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :scene_graph, :registry, :scene, :current_group
53
+
54
+ def resolve_and_build(class_or_name_or_instance, props)
55
+ return class_or_name_or_instance if class_or_name_or_instance.is_a?(Dama::Node)
56
+
57
+ return class_or_name_or_instance.new(**props) if class_or_name_or_instance.is_a?(Class)
58
+
59
+ strategy = RESOLVE_STRATEGIES.fetch(class_or_name_or_instance.class)
60
+ klass = strategy.call(class_or_name_or_instance, registry)
61
+ klass.new(**props)
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/dama/scene.rb ADDED
@@ -0,0 +1,233 @@
1
+ module Dama
2
+ # Base class for game scenes. Scenes have a compose/enter/update
3
+ # lifecycle and manage a scene graph of nodes.
4
+ #
5
+ # Example:
6
+ # class Level1 < Dama::Scene
7
+ # sound :jump, path: "assets/sfx/jump.wav"
8
+ #
9
+ # compose do
10
+ # camera viewport_width: 800, viewport_height: 600
11
+ # add Player, as: :hero
12
+ # end
13
+ #
14
+ # update do |dt, input|
15
+ # hero.transform.x += 100 * dt if input.right?
16
+ # play(:jump) if input.space?
17
+ # end
18
+ # end
19
+ class Scene # rubocop:disable Metrics/ClassLength
20
+ class << self
21
+ def compose(&block)
22
+ @compose_block = block
23
+ end
24
+
25
+ def enter(&block)
26
+ @enter_block = block
27
+ end
28
+
29
+ def update(&block)
30
+ @update_block = block
31
+ end
32
+
33
+ # Declare a sound asset at the class level.
34
+ # Loaded automatically during scene composition.
35
+ def sound(name, path:)
36
+ sound_declarations[name] = path
37
+ end
38
+
39
+ def sound_declarations
40
+ @sound_declarations ||= {}
41
+ end
42
+
43
+ attr_reader :compose_block, :enter_block, :update_block
44
+ end
45
+
46
+ attr_reader :camera
47
+
48
+ def initialize(registry:, asset_cache: nil, scene_switcher: nil, backend: nil)
49
+ @registry = registry
50
+ @asset_cache = asset_cache
51
+ @scene_switcher = scene_switcher
52
+ @backend = backend
53
+ @scene_graph = SceneGraph::Tree.new
54
+ @named_nodes = {}
55
+ @camera = nil
56
+ @audio = nil
57
+ @physics_world = nil
58
+ @collision_handlers = {}
59
+ end
60
+
61
+ # Play a sound declared via the `sound` class DSL.
62
+ def play(name)
63
+ audio&.play(name)
64
+ end
65
+
66
+ def enable_camera(viewport_width:, viewport_height:, **)
67
+ @camera = Camera.new(viewport_width:, viewport_height:, **)
68
+ end
69
+
70
+ # Enable physics for this scene. Called by `physics` DSL in composer.
71
+ def enable_physics(gravity_x: 0.0, gravity_y: 0.0)
72
+ event_bus = EventBus.new
73
+ @physics_world = Physics::World.new(gravity_x:, gravity_y:, event_bus:)
74
+
75
+ # Wire collision events to named handlers.
76
+ event_bus.on(:collision) do |collision:|
77
+ dispatch_collision(collision)
78
+ end
79
+ end
80
+
81
+ # Register a collision handler between two named nodes.
82
+ def on_collision(name_a, name_b, &block)
83
+ key = [name_a, name_b].sort
84
+ collision_handlers[key] = block
85
+ end
86
+
87
+ # Request a scene transition. The game applies it between frames.
88
+ def switch_to(scene_class)
89
+ scene_switcher&.call(scene_class)
90
+ end
91
+
92
+ def perform_compose
93
+ load_sounds
94
+ return unless self.class.compose_block
95
+
96
+ composer = Scene::Composer.new(scene_graph:, registry:, scene: self)
97
+ composer.instance_eval(&self.class.compose_block)
98
+ end
99
+
100
+ def perform_enter
101
+ return unless self.class.enter_block
102
+
103
+ instance_eval(&self.class.enter_block)
104
+ end
105
+
106
+ def perform_update(delta_time:, input:)
107
+ instance_exec(delta_time, input, &self.class.update_block) if self.class.update_block
108
+ physics_world&.step(delta_time:)
109
+ end
110
+
111
+ def perform_draw(backend:)
112
+ scene_graph.each_node do |instance_node|
113
+ draw_block = instance_node.node.class.draw_block
114
+ next unless draw_block
115
+
116
+ context = Node::DrawContext.new(node: instance_node.node, backend:, camera:)
117
+ context.instance_eval(&draw_block)
118
+ end
119
+ end
120
+
121
+ # Register a named node, load textures/shaders, and create physics body.
122
+ def register_named_node(name:, instance_node:)
123
+ named_nodes[name] = instance_node
124
+ define_singleton_method(name) { named_nodes.fetch(name) }
125
+
126
+ instance_node.node.load_textures(asset_cache:) if asset_cache
127
+ instance_node.node.load_shaders(backend:) if backend
128
+ create_physics_body(name:, node: instance_node.node)
129
+ end
130
+
131
+ # --- Query methods ---
132
+
133
+ def ref!(path)
134
+ scene_graph.query.by_path(path:)
135
+ end
136
+
137
+ def all(klass)
138
+ scene_graph.query.by_class(klass:)
139
+ end
140
+
141
+ def by_tag(tag)
142
+ scene_graph.query.by_tag(tag:)
143
+ end
144
+
145
+ def each(klass, &)
146
+ all(klass).each(&)
147
+ end
148
+
149
+ # Add a node dynamically at runtime (during update or enter).
150
+ def add(class_or_instance, as:, tags: [], group: nil, **props)
151
+ node_instance = build_node(class_or_instance, props)
152
+ instance_node = SceneGraph::InstanceNode.new(id: as, node: node_instance, tags: Array(tags))
153
+ scene_graph.add(instance_node:, parent_group: group)
154
+ register_named_node(name: as, instance_node:)
155
+ instance_node
156
+ end
157
+
158
+ # Remove a node, its physics body, and release its textures/shaders.
159
+ def remove(name)
160
+ instance_node = named_nodes.delete(name)
161
+ return unless instance_node
162
+
163
+ node = instance_node.node
164
+ node.unload_textures(asset_cache:) if asset_cache
165
+ node.unload_shaders(backend:) if backend
166
+ physics_world&.remove(node.physics) if node.physics
167
+
168
+ scene_graph.remove(id: name)
169
+ end
170
+
171
+ private
172
+
173
+ attr_reader :registry, :scene_graph, :named_nodes, :asset_cache,
174
+ :scene_switcher, :backend, :audio, :physics_world,
175
+ :collision_handlers
176
+
177
+ NODE_BUILDERS = {
178
+ Class => ->(klass, props) { klass.new(**props) },
179
+ :instance => ->(instance, _props) { instance },
180
+ }.freeze
181
+
182
+ def build_node(class_or_instance, props)
183
+ builder_key = class_or_instance.is_a?(Class) ? Class : :instance
184
+ NODE_BUILDERS.fetch(builder_key).call(class_or_instance, props)
185
+ end
186
+
187
+ # Load all class-level sound declarations via Audio.
188
+ def load_sounds
189
+ return if self.class.sound_declarations.empty?
190
+ return unless backend
191
+
192
+ @audio = Audio.new(backend:)
193
+ self.class.sound_declarations.each do |name, path|
194
+ audio.load(name:, path:)
195
+ end
196
+ end
197
+
198
+ COLLIDER_FACTORIES = {
199
+ rect: ->(opts) { Physics::Collider.rect(width: opts.fetch(:width), height: opts.fetch(:height)) },
200
+ circle: ->(opts) { Physics::Collider.circle(radius: opts.fetch(:radius)) },
201
+ }.freeze
202
+
203
+ # Create a physics body for a node if it declares `physics_body`.
204
+ def create_physics_body(name:, node:)
205
+ opts = node.class.physics_body_options
206
+ return unless opts && physics_world
207
+
208
+ collider_shape = opts.fetch(:collider, :rect)
209
+ collider = COLLIDER_FACTORIES.fetch(collider_shape).call(opts)
210
+ body = Physics::Body.new(
211
+ type: opts.fetch(:type, :dynamic),
212
+ mass: opts.fetch(:mass, 1.0),
213
+ collider:,
214
+ node:,
215
+ restitution: opts.fetch(:restitution, 0.0),
216
+ )
217
+ node.physics = body
218
+ physics_world.add(body)
219
+ end
220
+
221
+ # Route a collision event to the appropriate named handler.
222
+ def dispatch_collision(collision)
223
+ # Find node names for the two bodies.
224
+ name_a = named_nodes.key(named_nodes.values.find { |n| n.node == collision.body_a.node })
225
+ name_b = named_nodes.key(named_nodes.values.find { |n| n.node == collision.body_b.node })
226
+ return unless name_a && name_b
227
+
228
+ key = [name_a, name_b].sort
229
+ handler = collision_handlers[key]
230
+ handler&.call
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,26 @@
1
+ module Dama
2
+ module SceneGraph
3
+ # Indexes scene graph nodes by their Node class for fast lookup.
4
+ class ClassIndex
5
+ def initialize
6
+ @index = Hash.new { |h, k| h[k] = [] }
7
+ end
8
+
9
+ def register(node:)
10
+ index[node.node.class] << node
11
+ end
12
+
13
+ def unregister(node:)
14
+ index[node.node.class].delete(node)
15
+ end
16
+
17
+ def by_class(klass:)
18
+ index.fetch(klass, [])
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :index
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ module Dama
2
+ module SceneGraph
3
+ # A named container in the scene graph. Holds child nodes
4
+ # and supports polymorphic traversal.
5
+ class GroupNode
6
+ attr_reader :name, :children
7
+
8
+ def initialize(name:)
9
+ @name = name
10
+ @children = []
11
+ end
12
+
13
+ def <<(node)
14
+ children << node
15
+ end
16
+
17
+ def [](id)
18
+ children.detect { |child| child.id == id }
19
+ end
20
+
21
+ # Polymorphic traversal: delegates to each child.
22
+ def traverse(&)
23
+ children.each { |child| child.traverse(&) }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ module Dama
2
+ module SceneGraph
3
+ # Wraps a live Node instance within the scene graph.
4
+ # Holds the id, tags, and delegates to the underlying Node.
5
+ class InstanceNode
6
+ attr_reader :id, :node, :tags
7
+
8
+ def initialize(id:, node:, tags: [])
9
+ @id = id
10
+ @node = node
11
+ @tags = tags
12
+ end
13
+
14
+ # Polymorphic traversal: yields self to the block.
15
+ def traverse(&block)
16
+ block.call(self)
17
+ end
18
+
19
+ def method_missing(method_name, ...)
20
+ return super unless node.respond_to?(method_name)
21
+
22
+ node.public_send(method_name, ...)
23
+ end
24
+
25
+ def respond_to_missing?(method_name, include_private = false)
26
+ node.respond_to?(method_name, include_private) || super
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ module Dama
2
+ module SceneGraph
3
+ # Resolves "group/node_id" path strings to nodes in the scene graph.
4
+ # Uses regex with named groups to parse the path (no split per CLAUDE.md).
5
+ class PathSelector
6
+ SEGMENT_PATTERN = %r{\A(?<group>[^/]+)/(?<node_id>[^/]+)\z}
7
+
8
+ def initialize(groups:)
9
+ @groups = groups
10
+ end
11
+
12
+ def resolve(path:)
13
+ match = path.match(SEGMENT_PATTERN)
14
+ raise ArgumentError, "Invalid path format: #{path}" unless match
15
+
16
+ group = groups.fetch(match[:group].to_sym)
17
+ group[match[:node_id].to_sym]
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :groups
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ module Dama
2
+ module SceneGraph
3
+ # Unified query API for the scene graph. Provides by_id, by_class,
4
+ # by_tag, and by_path lookups.
5
+ class Query
6
+ def initialize(id_index:, tag_index:, class_index:, groups:)
7
+ @id_index = id_index
8
+ @tag_index = tag_index
9
+ @class_index = class_index
10
+ @path_selector = PathSelector.new(groups:)
11
+ end
12
+
13
+ def by_id(id:)
14
+ id_index.fetch(id)
15
+ end
16
+
17
+ def by_class(klass:)
18
+ class_index.by_class(klass:)
19
+ end
20
+
21
+ def by_tag(tag:)
22
+ tag_index.by_tag(tag:)
23
+ end
24
+
25
+ def by_path(path:)
26
+ path_selector.resolve(path:)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :id_index, :tag_index, :class_index, :path_selector
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ module Dama
2
+ module SceneGraph
3
+ # Indexes scene graph nodes by their tags for fast lookup.
4
+ class TagIndex
5
+ def initialize
6
+ @index = Hash.new { |h, k| h[k] = [] }
7
+ end
8
+
9
+ def register(node:)
10
+ node.tags.each { |tag| index[tag] << node }
11
+ end
12
+
13
+ def unregister(node:)
14
+ node.tags.each { |tag| index[tag].delete(node) }
15
+ end
16
+
17
+ def by_tag(tag:)
18
+ index.fetch(tag, [])
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :index
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,65 @@
1
+ module Dama
2
+ module SceneGraph
3
+ # Root container of the scene graph. Maintains flat indexes
4
+ # (by id, tag, class) and a hierarchy of groups for traversal.
5
+ class Tree
6
+ def initialize
7
+ @root_children = []
8
+ @id_index = {}
9
+ @tag_index = TagIndex.new
10
+ @class_index = ClassIndex.new
11
+ @groups = {}
12
+ end
13
+
14
+ def add(instance_node:, parent_group: nil)
15
+ target = parent_group ? groups.fetch(parent_group) : self
16
+ target << instance_node
17
+ id_index[instance_node.id] = instance_node
18
+ tag_index.register(node: instance_node)
19
+ class_index.register(node: instance_node)
20
+ end
21
+
22
+ def add_group(name:)
23
+ group = GroupNode.new(name:)
24
+ groups[name] = group
25
+ root_children << group
26
+ group
27
+ end
28
+
29
+ def <<(node)
30
+ root_children << node
31
+ end
32
+
33
+ def remove(id:)
34
+ node = id_index.delete(id)
35
+ return unless node
36
+
37
+ tag_index.unregister(node:)
38
+ class_index.unregister(node:)
39
+ remove_from_children(node:)
40
+ end
41
+
42
+ def query
43
+ @query ||= Query.new(id_index:, tag_index:, class_index:, groups:)
44
+ end
45
+
46
+ def each_node(&)
47
+ root_children.each { |child| child.traverse(&) }
48
+ end
49
+
50
+ # InstanceNode-like traversal protocol so Tree can be a container.
51
+ def traverse(&)
52
+ each_node(&)
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :root_children, :id_index, :tag_index, :class_index, :groups
58
+
59
+ def remove_from_children(node:)
60
+ root_children.delete(node)
61
+ groups.each_value { |group| group.children.delete(node) }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,4 @@
1
+ module Dama
2
+ module SceneGraph
3
+ end
4
+ end
@@ -0,0 +1,36 @@
1
+ module Dama
2
+ # Calculates UV coordinates for individual frames within a
3
+ # sprite sheet / texture atlas.
4
+ class SpriteSheet
5
+ attr_reader :frame_width, :frame_height, :columns, :rows, :frame_count
6
+
7
+ def initialize(texture_width:, texture_height:, frame_width:, frame_height:)
8
+ @frame_width = frame_width.to_f
9
+ @frame_height = frame_height.to_f
10
+ @texture_width = texture_width.to_f
11
+ @texture_height = texture_height.to_f
12
+ @columns = (texture_width / frame_width).to_i
13
+ @rows = (texture_height / frame_height).to_i
14
+ @frame_count = columns * rows
15
+ end
16
+
17
+ # Returns normalized UV coordinates for a given frame index.
18
+ # frame: 0-based index, left-to-right then top-to-bottom.
19
+ def frame_uv(frame:)
20
+ clamped = frame.clamp(0, frame_count - 1)
21
+ col = clamped % columns
22
+ row = clamped / columns
23
+
24
+ u = (col * frame_width) / texture_width
25
+ v = (row * frame_height) / texture_height
26
+ u2 = ((col + 1) * frame_width) / texture_width
27
+ v2 = ((row + 1) * frame_height) / texture_height
28
+
29
+ { u:, v:, u2:, v2: }
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :texture_width, :texture_height
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ module Dama
2
+ class Tween
3
+ # Standard easing functions for use with Tween::Lerp.
4
+ # Each function maps a progress value t (0.0..1.0) to an eased value.
5
+ #
6
+ # Usage:
7
+ # Tween::Lerp.new(target:, attribute:, from:, to:, duration:,
8
+ # easing: :ease_in_out_quad)
9
+ module Easing
10
+ FUNCTIONS = {
11
+ linear: ->(t) { t },
12
+
13
+ ease_in_quad: ->(t) { t * t },
14
+ ease_out_quad: ->(t) { t * (2.0 - t) },
15
+ ease_in_out_quad: ->(t) { t < 0.5 ? 2.0 * t * t : -1.0 + ((4.0 - (2.0 * t)) * t) },
16
+
17
+ ease_in_cubic: ->(t) { t * t * t },
18
+ ease_out_cubic: ->(t) { ((t - 1.0)**3) + 1.0 },
19
+ ease_in_out_cubic: lambda { |t|
20
+ t < 0.5 ? 4.0 * t * t * t : ((t - 1.0) * ((2.0 * t) - 2.0) * ((2.0 * t) - 2.0)) + 1.0
21
+ },
22
+
23
+ ease_in_out_sine: ->(t) { -(Math.cos(Math::PI * t) - 1.0) / 2.0 },
24
+ }.freeze
25
+
26
+ def self.fetch(name:)
27
+ FUNCTIONS.fetch(name)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ module Dama
2
+ class Tween
3
+ # Interpolates a single attribute on a target object from a start value
4
+ # to an end value over a given duration, with optional easing.
5
+ class Lerp
6
+ def initialize(target:, attribute:, from:, to:, duration:, easing: :linear, on_complete: nil)
7
+ @target = target
8
+ @attribute = attribute
9
+ @from = from.to_f
10
+ @to = to.to_f
11
+ @duration = duration.to_f
12
+ @elapsed = 0.0
13
+ @easing_fn = Easing.fetch(name: easing)
14
+ @on_complete = on_complete
15
+ end
16
+
17
+ def update(delta_time:)
18
+ @elapsed += delta_time
19
+ linear_progress = [elapsed / duration, 1.0].min
20
+ eased_progress = easing_fn.call(linear_progress)
21
+ value = from + ((to - from) * eased_progress)
22
+ target.public_send(:"#{attribute}=", value)
23
+ on_complete&.call if complete?
24
+ end
25
+
26
+ def complete?
27
+ elapsed >= duration
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :target, :attribute, :from, :to, :duration, :elapsed, :easing_fn, :on_complete
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ module Dama
2
+ class Tween
3
+ # Manages a collection of active tweens, updating them each
4
+ # frame and automatically removing completed ones.
5
+ class Manager
6
+ def initialize
7
+ @active_tweens = []
8
+ end
9
+
10
+ def add(tween:)
11
+ active_tweens << tween
12
+ end
13
+
14
+ def update(delta_time:)
15
+ active_tweens.each { |tween| tween.update(delta_time:) }
16
+ active_tweens.reject!(&:complete?)
17
+ end
18
+
19
+ def active?
20
+ active_tweens.any?
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :active_tweens
26
+ end
27
+ end
28
+ end
data/lib/dama/tween.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Dama
2
+ class Tween
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module Dama
2
+ VERSION = "0.1.0".freeze
3
+ end