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.
- 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/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/native/libdama_native.so +0 -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 +186 -0
data/lib/dama/camera.rb
ADDED
|
@@ -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
|
data/lib/dama/colors.rb
ADDED
|
@@ -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,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
|