dama 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +227 -0
- data/dama-logo.svg +91 -0
- data/exe/dama +4 -0
- data/ext/dama_native/.cargo/config.toml +3 -0
- data/ext/dama_native/Cargo.lock +3575 -0
- data/ext/dama_native/Cargo.toml +39 -0
- data/ext/dama_native/extconf.rb +72 -0
- data/ext/dama_native/src/audio.rs +134 -0
- data/ext/dama_native/src/engine.rs +339 -0
- data/ext/dama_native/src/lib.rs +396 -0
- data/ext/dama_native/src/renderer/screenshot.rs +84 -0
- data/ext/dama_native/src/renderer/shape_renderer.rs +507 -0
- data/ext/dama_native/src/renderer/text_renderer.rs +192 -0
- data/ext/dama_native/src/renderer.rs +563 -0
- data/ext/dama_native/src/window.rs +255 -0
- data/lib/dama/animation.rb +66 -0
- data/lib/dama/asset_cache.rb +56 -0
- data/lib/dama/audio.rb +47 -0
- data/lib/dama/auto_loader.rb +54 -0
- data/lib/dama/backend/base.rb +137 -0
- data/lib/dama/backend/native/ffi_bindings.rb +122 -0
- data/lib/dama/backend/native.rb +191 -0
- data/lib/dama/backend/web.rb +199 -0
- data/lib/dama/backend.rb +13 -0
- data/lib/dama/camera.rb +68 -0
- data/lib/dama/cli/new_project.rb +112 -0
- data/lib/dama/cli/release.rb +45 -0
- data/lib/dama/cli.rb +22 -0
- data/lib/dama/colors.rb +30 -0
- data/lib/dama/command_buffer.rb +83 -0
- data/lib/dama/component/attribute_definition.rb +13 -0
- data/lib/dama/component/attribute_set.rb +32 -0
- data/lib/dama/component.rb +28 -0
- data/lib/dama/configuration.rb +18 -0
- data/lib/dama/debug/frame_controller.rb +35 -0
- data/lib/dama/debug/screenshot_tool.rb +19 -0
- data/lib/dama/debug.rb +4 -0
- data/lib/dama/event_bus.rb +47 -0
- data/lib/dama/game/builder.rb +31 -0
- data/lib/dama/game/loop.rb +44 -0
- data/lib/dama/game.rb +88 -0
- data/lib/dama/geometry/circle.rb +28 -0
- data/lib/dama/geometry/rect.rb +16 -0
- data/lib/dama/geometry/sprite.rb +18 -0
- data/lib/dama/geometry/triangle.rb +13 -0
- data/lib/dama/geometry.rb +4 -0
- data/lib/dama/input/keyboard_state.rb +44 -0
- data/lib/dama/input/mouse_state.rb +45 -0
- data/lib/dama/input.rb +38 -0
- data/lib/dama/keys.rb +67 -0
- data/lib/dama/node/component_slot.rb +18 -0
- data/lib/dama/node/draw_context.rb +96 -0
- data/lib/dama/node.rb +139 -0
- data/lib/dama/physics/body.rb +57 -0
- data/lib/dama/physics/collider.rb +152 -0
- data/lib/dama/physics/collision.rb +15 -0
- data/lib/dama/physics/world.rb +125 -0
- data/lib/dama/physics.rb +4 -0
- data/lib/dama/registry/class_resolver.rb +48 -0
- data/lib/dama/registry.rb +21 -0
- data/lib/dama/release/archiver.rb +100 -0
- data/lib/dama/release/defaults/icon.icns +0 -0
- data/lib/dama/release/defaults/icon.ico +0 -0
- data/lib/dama/release/defaults/icon.png +0 -0
- data/lib/dama/release/dylib_relinker.rb +95 -0
- data/lib/dama/release/game_file_copier.rb +35 -0
- data/lib/dama/release/game_metadata.rb +61 -0
- data/lib/dama/release/icon_provider.rb +36 -0
- data/lib/dama/release/native_builder.rb +44 -0
- data/lib/dama/release/packager/linux.rb +62 -0
- data/lib/dama/release/packager/macos.rb +99 -0
- data/lib/dama/release/packager/web.rb +32 -0
- data/lib/dama/release/packager/windows.rb +61 -0
- data/lib/dama/release/packager.rb +9 -0
- data/lib/dama/release/platform_detector.rb +23 -0
- data/lib/dama/release/ruby_bundler.rb +163 -0
- data/lib/dama/release/stdlib_trimmer.rb +133 -0
- data/lib/dama/release/template_renderer.rb +40 -0
- data/lib/dama/release/templates/info_plist.xml.erb +19 -0
- data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
- data/lib/dama/release.rb +7 -0
- data/lib/dama/scene/composer.rb +65 -0
- data/lib/dama/scene.rb +233 -0
- data/lib/dama/scene_graph/class_index.rb +26 -0
- data/lib/dama/scene_graph/group_node.rb +27 -0
- data/lib/dama/scene_graph/instance_node.rb +30 -0
- data/lib/dama/scene_graph/path_selector.rb +25 -0
- data/lib/dama/scene_graph/query.rb +34 -0
- data/lib/dama/scene_graph/tag_index.rb +26 -0
- data/lib/dama/scene_graph/tree.rb +65 -0
- data/lib/dama/scene_graph.rb +4 -0
- data/lib/dama/sprite_sheet.rb +36 -0
- data/lib/dama/tween/easing.rb +31 -0
- data/lib/dama/tween/lerp.rb +35 -0
- data/lib/dama/tween/manager.rb +28 -0
- data/lib/dama/tween.rb +4 -0
- data/lib/dama/version.rb +3 -0
- data/lib/dama/vertex_batch.rb +35 -0
- data/lib/dama/web/entry.rb +79 -0
- data/lib/dama/web/static/index.html +142 -0
- data/lib/dama/web_builder.rb +232 -0
- data/lib/dama.rb +42 -0
- metadata +198 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
class 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
|
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,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
|