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
@@ -0,0 +1,68 @@
1
+ module Dama
2
+ # 2D camera with position, zoom, follow, and viewport culling.
3
+ # All draw coordinates in a scene are translated/scaled through
4
+ # the camera before reaching the backend.
5
+ class Camera
6
+ MIN_ZOOM = 0.1
7
+ MAX_ZOOM = 10.0
8
+
9
+ attr_reader :x, :y, :zoom, :viewport_width, :viewport_height
10
+
11
+ def initialize(viewport_width:, viewport_height:, x: 0.0, y: 0.0, zoom: 1.0)
12
+ @viewport_width = viewport_width.to_f
13
+ @viewport_height = viewport_height.to_f
14
+ @x = x.to_f
15
+ @y = y.to_f
16
+ @zoom = zoom.to_f.clamp(MIN_ZOOM, MAX_ZOOM)
17
+ end
18
+
19
+ def move_to(x:, y:)
20
+ @x = x.to_f
21
+ @y = y.to_f
22
+ end
23
+
24
+ def move_by(dx:, dy:)
25
+ @x += dx.to_f
26
+ @y += dy.to_f
27
+ end
28
+
29
+ def zoom_to(level:)
30
+ @zoom = level.to_f.clamp(MIN_ZOOM, MAX_ZOOM)
31
+ end
32
+
33
+ # Converts world coordinates to screen pixel coordinates.
34
+ def world_to_screen(world_x:, world_y:)
35
+ {
36
+ screen_x: (world_x - x) * zoom,
37
+ screen_y: (world_y - y) * zoom,
38
+ }
39
+ end
40
+
41
+ # Converts screen pixel coordinates to world coordinates.
42
+ def screen_to_world(screen_x:, screen_y:)
43
+ {
44
+ world_x: (screen_x / zoom) + x,
45
+ world_y: (screen_y / zoom) + y,
46
+ }
47
+ end
48
+
49
+ # Returns true if a world-space rectangle overlaps the camera viewport.
50
+ def visible?(x:, y:, width:, height:)
51
+ screen = world_to_screen(world_x: x, world_y: y)
52
+ screen_w = width * zoom
53
+ screen_h = height * zoom
54
+
55
+ (screen.fetch(:screen_x) + screen_w).positive? &&
56
+ screen.fetch(:screen_x) < viewport_width &&
57
+ (screen.fetch(:screen_y) + screen_h).positive? &&
58
+ screen.fetch(:screen_y) < viewport_height
59
+ end
60
+
61
+ # Centers the camera on a target object (must respond to #x and #y).
62
+ # Use lerp < 1.0 for smooth following.
63
+ def follow(target:, lerp: 1.0)
64
+ @x += (target.x - x) * lerp
65
+ @y += (target.y - y) * lerp
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,112 @@
1
+ require "fileutils"
2
+
3
+ module Dama
4
+ class Cli
5
+ # Generates a new game project in the current directory.
6
+ # Creates the standard directory structure with starter files
7
+ # for a playable game: a red circle moveable with arrow keys.
8
+ class NewProject
9
+ FILE_PERMISSIONS = {
10
+ true => 0o755,
11
+ false => 0o644,
12
+ }.freeze
13
+
14
+ def self.run
15
+ new.generate
16
+ end
17
+
18
+ def generate
19
+ puts "Creating new dama game project..."
20
+
21
+ TEMPLATES.each do |path, template|
22
+ write_template(path:, template:)
23
+ end
24
+
25
+ create_directory("assets")
26
+
27
+ puts "\nDone! Run bin/dama to start your game."
28
+ end
29
+
30
+ private
31
+
32
+ def write_template(path:, template:)
33
+ full_path = File.join(Dir.pwd, path)
34
+ return puts(" exists #{path}") if File.exist?(full_path)
35
+
36
+ FileUtils.mkdir_p(File.dirname(full_path))
37
+ File.write(full_path, template.fetch(:content))
38
+ FileUtils.chmod(FILE_PERMISSIONS.fetch(template.fetch(:executable)), full_path)
39
+ puts " create #{path}"
40
+ end
41
+
42
+ def create_directory(name)
43
+ return puts(" exists #{name}/") if File.directory?(name)
44
+
45
+ FileUtils.mkdir_p(name)
46
+ puts " create #{name}/"
47
+ end
48
+
49
+ TEMPLATES = {
50
+ "config.rb" => {
51
+ content: <<~RUBY,
52
+ GAME = Dama::Game.new do
53
+ settings resolution: [800, 600], title: "My Game"
54
+ start_scene MainScene
55
+ end
56
+ RUBY
57
+ executable: false,
58
+ },
59
+ "bin/dama" => {
60
+ content: <<~RUBY,
61
+ #!/usr/bin/env ruby
62
+ require "bundler/setup"
63
+ require "dama"
64
+
65
+ Dama::Cli.run(args: ARGV, root: File.expand_path("..", __dir__))
66
+ RUBY
67
+ executable: true,
68
+ },
69
+ "game/components/transform.rb" => {
70
+ content: <<~RUBY,
71
+ class Transform < Dama::Component
72
+ attribute :x, default: 0.0
73
+ attribute :y, default: 0.0
74
+ end
75
+ RUBY
76
+ executable: false,
77
+ },
78
+ "game/nodes/player.rb" => {
79
+ content: <<~RUBY,
80
+ class Player < Dama::Node
81
+ component Transform, as: :transform, x: 400.0, y: 300.0
82
+
83
+ draw do
84
+ circle(transform.x, transform.y, 20.0, color: Dama::Colors::RED)
85
+ end
86
+ end
87
+ RUBY
88
+ executable: false,
89
+ },
90
+ "game/scenes/main_scene.rb" => {
91
+ content: <<~RUBY,
92
+ class MainScene < Dama::Scene
93
+ compose do
94
+ add Player, as: :hero
95
+ end
96
+
97
+ update do |dt, input|
98
+ speed = 200.0
99
+
100
+ hero.transform.x += speed * dt if input.right?
101
+ hero.transform.x -= speed * dt if input.left?
102
+ hero.transform.y += speed * dt if input.down?
103
+ hero.transform.y -= speed * dt if input.up?
104
+ end
105
+ end
106
+ RUBY
107
+ executable: false,
108
+ },
109
+ }.freeze
110
+ end
111
+ end
112
+ end
@@ -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