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,45 @@
1
+ module Dama
2
+ class Cli
3
+ # Entry point for `bin/dama release`.
4
+ # Resolves the target platform from args or auto-detection,
5
+ # then dispatches to the appropriate packager via Hash lookup.
6
+ class Release
7
+ PACKAGERS = {
8
+ web: Dama::Release::Packager::Web,
9
+ macos: Dama::Release::Packager::Macos,
10
+ linux: Dama::Release::Packager::Linux,
11
+ windows: Dama::Release::Packager::Windows,
12
+ }.freeze
13
+
14
+ # Maps explicit CLI arguments to platform symbols.
15
+ # Falls back to auto-detection when the arg is not recognized.
16
+ PLATFORM_RESOLVERS = {
17
+ "web" => -> { :web },
18
+ }.freeze
19
+
20
+ DEFAULT_RESOLVER = -> { Dama::Release::PlatformDetector.detect }
21
+
22
+ def self.run(args:, root:)
23
+ new(args:, root:).execute
24
+ end
25
+
26
+ def initialize(args:, root:)
27
+ @args = args
28
+ @root = root
29
+ end
30
+
31
+ def execute
32
+ packager_class = PACKAGERS.fetch(platform)
33
+ packager_class.new(project_root: root).package
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :args, :root
39
+
40
+ def platform
41
+ PLATFORM_RESOLVERS.fetch(args.first, DEFAULT_RESOLVER).call
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/dama/cli.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Dama
2
+ # Command-line interface for the dama gem.
3
+ # Dispatches subcommands to their handlers via Hash lookup.
4
+ #
5
+ # Project binstubs pass root: so the CLI knows the project
6
+ # directory regardless of the caller's working directory.
7
+ # The gem-installed exe/dama omits root:, defaulting to Dir.pwd.
8
+ class Cli
9
+ def self.run(args:, root: Dir.pwd)
10
+ command_name = args.first
11
+ remaining_args = args.drop(1)
12
+ COMMANDS.fetch(command_name, DEFAULT).call(remaining_args, root)
13
+ end
14
+
15
+ COMMANDS = {
16
+ "new" => ->(_args, _root) { Cli::NewProject.run },
17
+ "release" => ->(args, root) { Cli::Release.run(args:, root:) },
18
+ }.freeze
19
+
20
+ DEFAULT = ->(_args, root) { Dama.boot(root:) }
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module Dama
2
+ module Colors
3
+ Color = Data.define(:r, :g, :b, :a) do
4
+ def to_h = { r:, g:, b:, a: }
5
+
6
+ def with_alpha(a:)
7
+ self.class.new(r:, g:, b:, a:)
8
+ end
9
+ end
10
+
11
+ RED = Color.new(r: 0.9, g: 0.2, b: 0.2, a: 1.0)
12
+ DARK_RED = Color.new(r: 0.6, g: 0.1, b: 0.1, a: 1.0)
13
+ WHITE = Color.new(r: 1.0, g: 1.0, b: 1.0, a: 1.0)
14
+ CREAM = Color.new(r: 0.96, g: 0.93, b: 0.87, a: 1.0)
15
+ BLACK = Color.new(r: 0.0, g: 0.0, b: 0.0, a: 1.0)
16
+ GRAY = Color.new(r: 0.5, g: 0.5, b: 0.5, a: 1.0)
17
+ DARK_BROWN = Color.new(r: 0.44, g: 0.26, b: 0.13, a: 1.0)
18
+ LIGHT_TAN = Color.new(r: 0.87, g: 0.72, b: 0.53, a: 1.0)
19
+ GREEN = Color.new(r: 0.2, g: 0.8, b: 0.3, a: 1.0)
20
+ GOLD = Color.new(r: 1.0, g: 0.84, b: 0.0, a: 1.0)
21
+ YELLOW = Color.new(r: 1.0, g: 1.0, b: 0.0, a: 1.0)
22
+ BLUE = Color.new(r: 0.2, g: 0.4, b: 0.9, a: 1.0)
23
+
24
+ # Logo-derived palette — extracted from dama-logo.svg
25
+ LIGHT_GRAY = Color.new(r: 0.96, g: 0.96, b: 0.96, a: 1.0)
26
+ DARK_GRAY = Color.new(r: 0.07, g: 0.07, b: 0.07, a: 1.0)
27
+ CHARCOAL = Color.new(r: 0.32, g: 0.35, b: 0.38, a: 1.0)
28
+ SLATE = Color.new(r: 0.13, g: 0.15, b: 0.17, a: 1.0)
29
+ end
30
+ end
@@ -0,0 +1,83 @@
1
+ module Dama
2
+ # Accumulates high-level draw commands as compact float sequences.
3
+ # Used by Backend::Web to minimize Ruby-side work — geometry decomposition
4
+ # is deferred to Rust wasm via dama_render_commands.
5
+ #
6
+ # Each command starts with a type tag followed by shape-specific data.
7
+ # Rust parses the tag and decomposes shapes into triangles at native speed.
8
+ class CommandBuffer
9
+ COMMAND_TAGS = {
10
+ circle: 0.0,
11
+ rect: 1.0,
12
+ triangle: 2.0,
13
+ sprite: 3.0,
14
+ set_texture: 4.0,
15
+ set_shader: 5.0,
16
+ }.freeze
17
+
18
+ def initialize
19
+ @buffer = []
20
+ end
21
+
22
+ def push_circle(cx:, cy:, radius:, r:, g:, b:, a:, segments:)
23
+ buffer.push(
24
+ COMMAND_TAGS.fetch(:circle),
25
+ cx.to_f, cy.to_f, radius.to_f,
26
+ r.to_f, g.to_f, b.to_f, a.to_f,
27
+ segments.to_f
28
+ )
29
+ end
30
+
31
+ def push_rect(x:, y:, w:, h:, r:, g:, b:, a:)
32
+ buffer.push(
33
+ COMMAND_TAGS.fetch(:rect),
34
+ x.to_f, y.to_f, w.to_f, h.to_f,
35
+ r.to_f, g.to_f, b.to_f, a.to_f
36
+ )
37
+ end
38
+
39
+ def push_triangle(x1:, y1:, x2:, y2:, x3:, y3:, r:, g:, b:, a:)
40
+ buffer.push(
41
+ COMMAND_TAGS.fetch(:triangle),
42
+ x1.to_f, y1.to_f, x2.to_f, y2.to_f, x3.to_f, y3.to_f,
43
+ r.to_f, g.to_f, b.to_f, a.to_f
44
+ )
45
+ end
46
+
47
+ def push_sprite(texture_handle:, x:, y:, w:, h:, r:, g:, b:, a:, u_min:, v_min:, u_max:, v_max:) # rubocop:disable Metrics/ParameterLists
48
+ buffer.push(
49
+ COMMAND_TAGS.fetch(:sprite),
50
+ texture_handle.to_f, x.to_f, y.to_f, w.to_f, h.to_f,
51
+ r.to_f, g.to_f, b.to_f, a.to_f,
52
+ u_min.to_f, v_min.to_f, u_max.to_f, v_max.to_f
53
+ )
54
+ end
55
+
56
+ def push_set_shader(shader_handle:)
57
+ buffer.push(
58
+ COMMAND_TAGS.fetch(:set_shader),
59
+ shader_handle.to_f,
60
+ )
61
+ end
62
+
63
+ def empty?
64
+ buffer.empty?
65
+ end
66
+
67
+ def float_count
68
+ buffer.length
69
+ end
70
+
71
+ def to_a
72
+ buffer.dup
73
+ end
74
+
75
+ def clear
76
+ buffer.clear
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :buffer
82
+ end
83
+ end
@@ -0,0 +1,13 @@
1
+ module Dama
2
+ class Component
3
+ # Value object holding the name and default value of a component attribute.
4
+ class AttributeDefinition
5
+ attr_reader :name, :default
6
+
7
+ def initialize(name:, default:)
8
+ @name = name
9
+ @default = default
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module Dama
2
+ class Component
3
+ # Collection of attribute definitions for a Component subclass.
4
+ # Handles registration, lookup, and dynamic accessor generation
5
+ # on the owning class.
6
+ class AttributeSet
7
+ include Enumerable
8
+
9
+ def initialize(owner:)
10
+ @owner = owner
11
+ @definitions = {}
12
+ end
13
+
14
+ def add(name:, default:)
15
+ definitions[name] = AttributeDefinition.new(name:, default:)
16
+ owner.attr_accessor(name)
17
+ end
18
+
19
+ def each(&)
20
+ definitions.values.each(&)
21
+ end
22
+
23
+ def fetch(name)
24
+ definitions.fetch(name)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :owner, :definitions
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ module Dama
2
+ # Base class for data components. Components are pure data containers
3
+ # with named attributes and default values. They carry no behavior.
4
+ #
5
+ # Example:
6
+ # class Transform < Dama::Component
7
+ # attribute :x, default: 0
8
+ # attribute :y, default: 0
9
+ # end
10
+ class Component
11
+ class << self
12
+ def attribute(name, default: nil)
13
+ attribute_set.add(name:, default:)
14
+ end
15
+
16
+ def attribute_set
17
+ @attribute_set ||= AttributeSet.new(owner: self)
18
+ end
19
+ end
20
+
21
+ def initialize(**values)
22
+ self.class.attribute_set.each do |definition|
23
+ value = values.fetch(definition.name, definition.default)
24
+ instance_variable_set(:"@#{definition.name}", value)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ module Dama
2
+ # Holds game-wide settings like resolution, title, and rendering mode.
3
+ # Passed to the backend to configure window and renderer initialization.
4
+ class Configuration
5
+ attr_reader :width, :height, :title, :headless
6
+
7
+ def initialize(width: 800, height: 600, title: "Dama Game", headless: false)
8
+ @width = width
9
+ @height = height
10
+ @title = title
11
+ @headless = headless
12
+ end
13
+
14
+ def resolution
15
+ [width, height]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ module Dama
2
+ module Debug
3
+ # Controls frame limits for debugging. Supports two modes:
4
+ # - Limited: stops after N frames
5
+ # - Unlimited: runs indefinitely
6
+ # Selected at construction time via factory (no runtime conditionals).
7
+ class FrameController
8
+ # Strategy lambdas selected by mode to avoid runtime conditionals.
9
+ STRATEGIES = {
10
+ limited: ->(current_frame, frame_limit) { current_frame >= frame_limit },
11
+ unlimited: ->(_current_frame, _frame_limit) { false },
12
+ }.freeze
13
+
14
+ def initialize(frame_limit: 0)
15
+ @frame_limit = frame_limit
16
+ @current_frame = 0
17
+ @strategy_key = frame_limit.zero? ? :unlimited : :limited
18
+ end
19
+
20
+ def tick
21
+ @current_frame += 1
22
+ end
23
+
24
+ def frame_limit_reached?
25
+ STRATEGIES.fetch(strategy_key).call(current_frame, frame_limit)
26
+ end
27
+
28
+ attr_reader :current_frame
29
+
30
+ private
31
+
32
+ attr_reader :frame_limit, :strategy_key
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ module Dama
2
+ module Debug
3
+ # Captures the current render target to a PNG file.
4
+ # Delegates to the backend's screenshot capability.
5
+ class ScreenshotTool
6
+ def initialize(backend:)
7
+ @backend = backend
8
+ end
9
+
10
+ def capture(output_path:)
11
+ backend.screenshot(output_path:)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :backend
17
+ end
18
+ end
19
+ end
data/lib/dama/debug.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Dama
2
+ module Debug
3
+ end
4
+ end
@@ -0,0 +1,47 @@
1
+ module Dama
2
+ # Publish/subscribe event system for decoupled communication
3
+ # between game objects. Nodes and scenes can emit events and
4
+ # register handlers without direct references.
5
+ #
6
+ # Usage:
7
+ # bus = EventBus.new
8
+ # bus.on(:damage) { |amount:| puts "Ouch! #{amount}" }
9
+ # bus.emit(:damage, amount: 10)
10
+ class EventBus
11
+ def initialize
12
+ @handlers = Hash.new { |h, k| h[k] = [] }
13
+ end
14
+
15
+ def on(event_name, &handler)
16
+ handlers[event_name] << handler
17
+ end
18
+
19
+ def once(event_name, &handler)
20
+ wrapper = lambda { |**data|
21
+ handler.call(**data)
22
+ off(event_name, &wrapper)
23
+ }
24
+ on(event_name, &wrapper)
25
+ end
26
+
27
+ def emit(event_name, **data)
28
+ handlers[event_name].each { |handler| handler.call(**data) }
29
+ end
30
+
31
+ def off(event_name, &handler)
32
+ handlers[event_name].delete(handler)
33
+ end
34
+
35
+ def clear(event_name)
36
+ handlers.delete(event_name)
37
+ end
38
+
39
+ def clear_all
40
+ handlers.clear
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :handlers
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ module Dama
2
+ class Game
3
+ # Evaluates the Game.new block. Provides `settings` and `start_scene`.
4
+ class Builder
5
+ attr_reader :configuration, :start_scene_class
6
+
7
+ def initialize(registry:)
8
+ @registry = registry
9
+ @configuration = Configuration.new
10
+ @start_scene_class = nil
11
+ end
12
+
13
+ def settings(resolution: [800, 600], title: "Dama Game", headless: false)
14
+ @configuration = Configuration.new(
15
+ width: resolution[0],
16
+ height: resolution[1],
17
+ title:,
18
+ headless:,
19
+ )
20
+ end
21
+
22
+ def start_scene(scene_class)
23
+ @start_scene_class = scene_class
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :registry
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ module Dama
2
+ class Game
3
+ # Main game loop. Ruby controls the cadence; the backend provides timing.
4
+ # The loop runs: poll_events -> update -> begin_frame -> draw -> end_frame.
5
+ class Loop
6
+ def initialize(backend:, scene_provider:, frame_controller:, input:, scene_transition: nil)
7
+ @backend = backend
8
+ @scene_provider = scene_provider
9
+ @frame_controller = frame_controller
10
+ @input = input
11
+ @scene_transition = scene_transition
12
+ end
13
+
14
+ def run
15
+ loop do
16
+ quit = backend.poll_events
17
+ break if quit
18
+
19
+ delta_time = backend.delta_time
20
+ input.update
21
+
22
+ current_scene.perform_update(delta_time:, input:)
23
+ scene_transition&.call
24
+
25
+ backend.begin_frame
26
+ backend.clear
27
+ current_scene.perform_draw(backend:)
28
+ backend.end_frame
29
+
30
+ frame_controller.tick
31
+ break if frame_controller.frame_limit_reached?
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :backend, :scene_provider, :frame_controller, :input, :scene_transition
38
+
39
+ def current_scene
40
+ scene_provider.call
41
+ end
42
+ end
43
+ end
44
+ end
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