rgame 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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +406 -0
  4. data/docs/api/README.md +167 -0
  5. data/docs/api/app.md +192 -0
  6. data/docs/api/assets.md +426 -0
  7. data/docs/api/audio.md +208 -0
  8. data/docs/api/components.md +321 -0
  9. data/docs/api/drawing.md +330 -0
  10. data/docs/api/game.md +99 -0
  11. data/docs/api/images.md +118 -0
  12. data/docs/api/input.md +179 -0
  13. data/docs/api/internals.md +110 -0
  14. data/docs/api/scene_graph.md +159 -0
  15. data/docs/api/signals.md +142 -0
  16. data/docs/api/systems.md +98 -0
  17. data/docs/api/text.md +116 -0
  18. data/docs/api/toolbox.md +240 -0
  19. data/docs/api/values.md +101 -0
  20. data/ext/README.md +225 -0
  21. data/ext/rgame_core/app/app.c +721 -0
  22. data/ext/rgame_core/app/app_gl.h +64 -0
  23. data/ext/rgame_core/app/frame_loop.c +42 -0
  24. data/ext/rgame_core/app/frame_loop.h +54 -0
  25. data/ext/rgame_core/audio/audio.c +466 -0
  26. data/ext/rgame_core/audio/audio_internal.h +45 -0
  27. data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
  28. data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
  29. data/ext/rgame_core/example.rb +188 -0
  30. data/ext/rgame_core/extconf.rb +167 -0
  31. data/ext/rgame_core/graphics/backend.c +52 -0
  32. data/ext/rgame_core/graphics/backend.h +64 -0
  33. data/ext/rgame_core/graphics/canvas.c +247 -0
  34. data/ext/rgame_core/graphics/canvas.h +143 -0
  35. data/ext/rgame_core/graphics/clip.c +87 -0
  36. data/ext/rgame_core/graphics/clip.h +89 -0
  37. data/ext/rgame_core/graphics/draw_queue.c +216 -0
  38. data/ext/rgame_core/graphics/draw_queue.h +174 -0
  39. data/ext/rgame_core/graphics/gl_backend.c +122 -0
  40. data/ext/rgame_core/graphics/gl_backend.h +43 -0
  41. data/ext/rgame_core/graphics/image.c +304 -0
  42. data/ext/rgame_core/graphics/image_internal.h +30 -0
  43. data/ext/rgame_core/graphics/primitives.c +189 -0
  44. data/ext/rgame_core/graphics/primitives.h +111 -0
  45. data/ext/rgame_core/graphics/recording.c +119 -0
  46. data/ext/rgame_core/graphics/recording.h +88 -0
  47. data/ext/rgame_core/graphics/texture.c +181 -0
  48. data/ext/rgame_core/graphics/texture.h +165 -0
  49. data/ext/rgame_core/graphics/transform.c +128 -0
  50. data/ext/rgame_core/graphics/transform.h +106 -0
  51. data/ext/rgame_core/include/rgame/core.h +577 -0
  52. data/ext/rgame_core/input/device_slots.c +103 -0
  53. data/ext/rgame_core/input/device_slots.h +93 -0
  54. data/ext/rgame_core/input/gamepad.c +145 -0
  55. data/ext/rgame_core/input/gamepad.h +63 -0
  56. data/ext/rgame_core/input/input.c +109 -0
  57. data/ext/rgame_core/input/input.h +99 -0
  58. data/ext/rgame_core/ruby/audio_ext.c +321 -0
  59. data/ext/rgame_core/ruby/core_ext.c +513 -0
  60. data/ext/rgame_core/ruby/core_ext.h +51 -0
  61. data/ext/rgame_core/ruby/font_ext.c +168 -0
  62. data/ext/rgame_core/ruby/image_ext.c +230 -0
  63. data/ext/rgame_core/ruby/recording_ext.c +186 -0
  64. data/ext/rgame_core/ruby/renderer_ext.c +376 -0
  65. data/ext/rgame_core/text/atlas.c +59 -0
  66. data/ext/rgame_core/text/atlas.h +85 -0
  67. data/ext/rgame_core/text/font.c +281 -0
  68. data/ext/rgame_core/text/font.h +139 -0
  69. data/ext/rgame_core/text/font_atlas.c +385 -0
  70. data/ext/rgame_core/text/font_internal.h +47 -0
  71. data/ext/rgame_core/text/glyph_cache.c +142 -0
  72. data/ext/rgame_core/text/glyph_cache.h +89 -0
  73. data/ext/rgame_core/vendor/README.md +159 -0
  74. data/ext/rgame_core/vendor/miniaudio.h +95864 -0
  75. data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
  76. data/ext/rgame_core/vendor/stb_image.h +7988 -0
  77. data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
  78. data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
  79. data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
  80. data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
  81. data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
  82. data/ext/rgame_util/color.c +19 -0
  83. data/ext/rgame_util/color.h +60 -0
  84. data/ext/rgame_util/color_ext.c +156 -0
  85. data/ext/rgame_util/extconf.rb +27 -0
  86. data/ext/rgame_util/tensor.c +186 -0
  87. data/ext/rgame_util/util_ext.c +27 -0
  88. data/ext/rgame_util/util_ext.h +16 -0
  89. data/lib/rgame/boot.rb +13 -0
  90. data/lib/rgame/core/app.rb +82 -0
  91. data/lib/rgame/core/asset_manager.rb +224 -0
  92. data/lib/rgame/core/audio.rb +124 -0
  93. data/lib/rgame/core/font.rb +49 -0
  94. data/lib/rgame/core/gamepad.rb +55 -0
  95. data/lib/rgame/core/image.rb +55 -0
  96. data/lib/rgame/core/input.rb +77 -0
  97. data/lib/rgame/core/nine_slice.rb +163 -0
  98. data/lib/rgame/core/recording.rb +52 -0
  99. data/lib/rgame/core/renderer.rb +363 -0
  100. data/lib/rgame/core/sprite_sheet.rb +108 -0
  101. data/lib/rgame/core/tile_map_renderer.rb +160 -0
  102. data/lib/rgame/core/ui_atlas.rb +86 -0
  103. data/lib/rgame/core.rb +24 -0
  104. data/lib/rgame/engine/actor.rb +53 -0
  105. data/lib/rgame/engine/animation_set.rb +49 -0
  106. data/lib/rgame/engine/animator.rb +44 -0
  107. data/lib/rgame/engine/audio_bus.rb +24 -0
  108. data/lib/rgame/engine/audio_director.rb +29 -0
  109. data/lib/rgame/engine/body.rb +49 -0
  110. data/lib/rgame/engine/cached_label.rb +33 -0
  111. data/lib/rgame/engine/camera.rb +33 -0
  112. data/lib/rgame/engine/camera_view.rb +28 -0
  113. data/lib/rgame/engine/circle_collider.rb +32 -0
  114. data/lib/rgame/engine/collision_box.rb +34 -0
  115. data/lib/rgame/engine/collision_system.rb +44 -0
  116. data/lib/rgame/engine/component.rb +30 -0
  117. data/lib/rgame/engine/components/action_trigger.rb +41 -0
  118. data/lib/rgame/engine/components/animated_sprite.rb +63 -0
  119. data/lib/rgame/engine/components/character_body.rb +70 -0
  120. data/lib/rgame/engine/components/circle_collider.rb +44 -0
  121. data/lib/rgame/engine/components/collision_world.rb +103 -0
  122. data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
  123. data/lib/rgame/engine/components/path_follow.rb +84 -0
  124. data/lib/rgame/engine/components/player_controller.rb +24 -0
  125. data/lib/rgame/engine/components/pool.rb +53 -0
  126. data/lib/rgame/engine/components/screen_wrap.rb +27 -0
  127. data/lib/rgame/engine/components/sprite.rb +31 -0
  128. data/lib/rgame/engine/components/targeting.rb +54 -0
  129. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  130. data/lib/rgame/engine/components/tile_world.rb +68 -0
  131. data/lib/rgame/engine/components/timer.rb +75 -0
  132. data/lib/rgame/engine/components/velocity.rb +27 -0
  133. data/lib/rgame/engine/components/wander_controller.rb +60 -0
  134. data/lib/rgame/engine/debug_overlay.rb +106 -0
  135. data/lib/rgame/engine/i18n.rb +97 -0
  136. data/lib/rgame/engine/input/action_mapper.rb +46 -0
  137. data/lib/rgame/engine/input/actions.rb +41 -0
  138. data/lib/rgame/engine/input/player_controller.rb +14 -0
  139. data/lib/rgame/engine/matrix.rb +32 -0
  140. data/lib/rgame/engine/node2d.rb +271 -0
  141. data/lib/rgame/engine/path.rb +78 -0
  142. data/lib/rgame/engine/pool.rb +51 -0
  143. data/lib/rgame/engine/resettable.rb +67 -0
  144. data/lib/rgame/engine/scene/scene_stack.rb +65 -0
  145. data/lib/rgame/engine/signal.rb +75 -0
  146. data/lib/rgame/engine/spatial_hash.rb +71 -0
  147. data/lib/rgame/engine/tile_collision.rb +78 -0
  148. data/lib/rgame/engine/tile_map.rb +149 -0
  149. data/lib/rgame/engine/tileset.rb +101 -0
  150. data/lib/rgame/engine/timer.rb +51 -0
  151. data/lib/rgame/engine.rb +68 -0
  152. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  153. data/lib/rgame/fonts/OFL.txt +102 -0
  154. data/lib/rgame/game.rb +129 -0
  155. data/lib/rgame/util/color.rb +27 -0
  156. data/lib/rgame/util/controls.rb +107 -0
  157. data/lib/rgame/util/tensor.rb +12 -0
  158. data/lib/rgame/util.rb +8 -0
  159. data/lib/rgame/version.rb +12 -0
  160. data/lib/rgame.rb +20 -0
  161. metadata +215 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RGame
4
+ module Engine
5
+ # A uniform-grid spatial hash for broadphase collision: bucket colliders into
6
+ # fixed-size cells, then test only candidates that share a cell instead of every
7
+ # pair. Pure logic; no graphics.
8
+ #
9
+ # Typical per-frame use: `clear`, `insert` every collider of the static set
10
+ # (here: rocks), then `query` around each moving collider (bullets, the ship).
11
+ #
12
+ # hash.clear
13
+ # rocks.each { |r| hash.insert(r, *r.aabb) }
14
+ # hash.query(*bullet.aabb) { |rock| ...narrowphase... }
15
+ class SpatialHash
16
+ # Cell (col, row) → one integer key. The offset keeps negative cells (objects
17
+ # off-screen / mid-wrap) non-negative; the stride keeps pairs unique. Both fit
18
+ # in a tagged Fixnum, so keying allocates nothing.
19
+ OFFSET = 1 << 20
20
+ STRIDE = 1 << 21
21
+
22
+ def initialize(cell_size:)
23
+ @cell_size = cell_size
24
+ @buckets = Hash.new { |h, key| h[key] = [] }
25
+ end
26
+
27
+ # Reuse the bucket arrays across frames (clear contents, keep capacity).
28
+ def clear
29
+ @buckets.each_value(&:clear)
30
+ end
31
+
32
+ def insert(item, x, y, w, h)
33
+ each_cell(x, y, w, h) { |key| @buckets[key] << item }
34
+ end
35
+
36
+ # Yield every item whose buckets overlap the region. Dedup contract: an item
37
+ # spanning several cells may be yielded more than once. Narrowphase callers
38
+ # must already guard with `next if a.dead? || b.dead?` to make hits idempotent,
39
+ # so we skip a per-query visited set and stay allocation-free.
40
+ def query(x, y, w, h, &)
41
+ each_cell(x, y, w, h) do |key|
42
+ bucket = @buckets[key]
43
+ bucket.each(&) unless bucket.empty?
44
+ end
45
+ end
46
+
47
+ # Broadphase a circle: yield every item bucketed in a cell the circle's bounding
48
+ # box covers — the radial counterpart to #query, for range/nearest lookups. Same
49
+ # dedup contract (an item may be yielded more than once; the narrowphase caller
50
+ # refines by true distance). Allocation-free.
51
+ def query_circle(cx, cy, r, &)
52
+ d = r * 2
53
+ query(cx - r, cy - r, d, d, &)
54
+ end
55
+
56
+ private
57
+
58
+ def each_cell(x, y, w, h)
59
+ col0 = (x / @cell_size).floor
60
+ row0 = (y / @cell_size).floor
61
+ col1 = ((x + w) / @cell_size).floor
62
+ row1 = ((y + h) / @cell_size).floor
63
+ row0.upto(row1) do |row|
64
+ col0.upto(col1) do |col|
65
+ yield (col + OFFSET) * STRIDE + (row + OFFSET)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RGame
4
+ module Engine
5
+ # Axis-separated AABB-vs-tile collision resolution (pure). `solid` is a callable
6
+ # `solid.call(col, row) -> bool`. Resolve X then Y (with the X result) to get
7
+ # wall-sliding. Assumes per-step movement smaller than a tile (no tunneling),
8
+ # which holds for our speeds.
9
+ class TileCollision
10
+ EPS = 1e-9
11
+
12
+ def initialize(tile_width:, tile_height:, solid:)
13
+ @tile_width = tile_width
14
+ @tile_height = tile_height
15
+ @solid = solid
16
+ end
17
+
18
+ # Move an AABB (top-left x, y; size w, h) by dx, snapping flush against solids.
19
+ def resolve_x(x, y, w, h, dx)
20
+ nx = x + dx
21
+ return nx if dx.zero?
22
+
23
+ first_row = (y / @tile_height).floor
24
+ last_row = ((y + h - EPS) / @tile_height).floor
25
+
26
+ if dx.positive?
27
+ col = ((nx + w - EPS) / @tile_width).floor
28
+ return col * @tile_width - w if solid_in_rows?(col, first_row, last_row)
29
+ else
30
+ col = (nx / @tile_width).floor
31
+ return (col + 1) * @tile_width if solid_in_rows?(col, first_row, last_row)
32
+ end
33
+ nx
34
+ end
35
+
36
+ def resolve_y(x, y, w, h, dy)
37
+ ny = y + dy
38
+ return ny if dy.zero?
39
+
40
+ first_col = (x / @tile_width).floor
41
+ last_col = ((x + w - EPS) / @tile_width).floor
42
+
43
+ if dy.positive?
44
+ row = ((ny + h - EPS) / @tile_height).floor
45
+ return row * @tile_height - h if solid_in_cols?(row, first_col, last_col)
46
+ else
47
+ row = (ny / @tile_height).floor
48
+ return (row + 1) * @tile_height if solid_in_cols?(row, first_col, last_col)
49
+ end
50
+ ny
51
+ end
52
+
53
+ private
54
+
55
+ # Plain integer loops rather than (a..b).any? { ... }: the Range literal would
56
+ # allocate on this per-frame collision path.
57
+ def solid_in_rows?(col, first_row, last_row)
58
+ row = first_row
59
+ while row <= last_row
60
+ return true if @solid.call(col, row)
61
+
62
+ row += 1
63
+ end
64
+ false
65
+ end
66
+
67
+ def solid_in_cols?(row, first_col, last_col)
68
+ col = first_col
69
+ while col <= last_col
70
+ return true if @solid.call(col, row)
71
+
72
+ col += 1
73
+ end
74
+ false
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rexml/document'
4
+ require 'base64'
5
+ require 'zlib'
6
+
7
+ require_relative '../util'
8
+ require_relative 'tileset'
9
+
10
+ module RGame
11
+ module Engine
12
+ # An orthogonal tile map parsed from a Tiled .tmx. Holds the per-layer gid
13
+ # arrays and geometry, and answers collision queries via its Tileset.
14
+ #
15
+ # `parse` takes a *string*, so the parsing itself needs no filesystem at all;
16
+ # `load` adds the file plumbing on top — reading the .tmx, following it to the
17
+ # .tsx it names, and working out where the tileset image sits relative to
18
+ # that. Both belong here. What does *not* is the image: a texture is a GPU
19
+ # handle, and the renderer that owns one lives a layer below and may not name
20
+ # this class (see CLAUDE.md, "The rule points both ways"). So `load` hands
21
+ # back a path and stops there.
22
+ class TileMap
23
+ FLIP_MASK = 0x1FFFFFFF # strip Tiled's flip/rotation flags from a gid
24
+
25
+ attr_reader :width, :height, :tile_width, :tile_height,
26
+ :pixel_width, :pixel_height, :tileset_source, :firstgid
27
+ attr_accessor :tileset
28
+
29
+ # Reads a .tmx and everything it points at, returning `[map, image_path]`.
30
+ #
31
+ # Two values rather than one because they are two kinds of thing: the map is
32
+ # the grid, and the path is where its pixels happen to live. Keeping the
33
+ # second off the map means a stand-in map in a spec has one less method to
34
+ # answer, and the renderer's protocol stays "things about the grid".
35
+ #
36
+ # Every path is resolved relative to the file that named it — the .tsx
37
+ # relative to the .tmx, the image relative to the .tsx — which is what Tiled
38
+ # itself writes and what lets a map be moved as a set.
39
+ def self.load(tmx_path)
40
+ map = parse(File.read(tmx_path))
41
+
42
+ tsx_path = File.join(File.dirname(tmx_path), map.tileset_source)
43
+ map.tileset = Tileset.parse(File.read(tsx_path), firstgid: map.firstgid)
44
+
45
+ [map, File.join(File.dirname(tsx_path), map.tileset.image_source)]
46
+ end
47
+
48
+ def self.parse(tmx_string)
49
+ root = REXML::Document.new(tmx_string).root
50
+ tileset_el = root.elements['tileset']
51
+
52
+ layers = []
53
+ above = []
54
+ root.each_element('layer') do |layer_el|
55
+ raw = Base64.decode64(layer_el.elements['data'].text.strip)
56
+ gids = Zlib::Inflate.inflate(raw).unpack('V*')
57
+ gids.map! { |g| g & FLIP_MASK }
58
+ layers << gids
59
+ above << layer_flag?(layer_el, 'above')
60
+ end
61
+
62
+ new(
63
+ width: root.attributes['width'].to_i,
64
+ height: root.attributes['height'].to_i,
65
+ tile_width: root.attributes['tilewidth'].to_i,
66
+ tile_height: root.attributes['tileheight'].to_i,
67
+ tileset_source: tileset_el.attributes['source'],
68
+ firstgid: tileset_el.attributes['firstgid'].to_i,
69
+ layers: layers,
70
+ above: above
71
+ )
72
+ end
73
+
74
+ # True if a layer carries a Tiled bool custom property `name` set to "true".
75
+ def self.layer_flag?(layer_el, name)
76
+ props = layer_el.elements['properties']
77
+ return false unless props
78
+
79
+ props.each_element('property') do |property|
80
+ return property.attributes['value'] == 'true' if property.attributes['name'] == name
81
+ end
82
+ false
83
+ end
84
+
85
+ def initialize(width:, height:, tile_width:, tile_height:, tileset_source:, firstgid:, layers:, above: [])
86
+ @width = width
87
+ @height = height
88
+ @tile_width = tile_width
89
+ @tile_height = tile_height
90
+ @pixel_width = width * tile_width
91
+ @pixel_height = height * tile_height
92
+ @tileset_source = tileset_source
93
+ @firstgid = firstgid
94
+ @above = above
95
+ @tileset = nil
96
+ @tiles = build_tiles(layers)
97
+ end
98
+
99
+ def layer_count
100
+ @tiles.depth
101
+ end
102
+
103
+ # Whether a layer is drawn *above* the actors (a tree canopy, roof, etc.) rather
104
+ # than beneath them. Marked in Tiled with a bool layer property `above`; layers
105
+ # without it default to below. Purely a rendering distinction (collision still
106
+ # considers every layer).
107
+ def above_layer?(index)
108
+ @above[index] || false
109
+ end
110
+
111
+ def in_bounds?(col, row)
112
+ col >= 0 && row >= 0 && col < @width && row < @height
113
+ end
114
+
115
+ def gid(layer_index, col, row)
116
+ return 0 unless in_bounds?(col, row)
117
+
118
+ @tiles[col, row, layer_index]
119
+ end
120
+
121
+ # Solid if any layer has a solid tile at (col, row). Out of bounds is not solid
122
+ # — the camera/bounds clamp keeps the player inside the map.
123
+ def solid_tile?(col, row)
124
+ return false unless in_bounds?(col, row)
125
+
126
+ @tiles.depth.times { |layer| return true if @tileset.solid?(@tiles[col, row, layer]) }
127
+ false
128
+ end
129
+
130
+ def solid_at?(world_x, world_y)
131
+ solid_tile?((world_x / @tile_width).floor, (world_y / @tile_height).floor)
132
+ end
133
+
134
+ private
135
+
136
+ # Pack the parsed per-layer gid rows into one flat Tensor ([col, row, layer]).
137
+ def build_tiles(layers)
138
+ tiles = Util::Tensor.new(@width, @height, layers.length, initial: 0)
139
+ layers.each_with_index do |gids, layer|
140
+ @height.times do |row|
141
+ base = row * @width
142
+ @width.times { |col| tiles[col, row, layer] = gids[base + col] }
143
+ end
144
+ end
145
+ tiles
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rexml/document'
4
+
5
+ module RGame
6
+ module Engine
7
+ # Tile-type data parsed from a Tiled .tsx (pure; no graphics). Knows tile geometry,
8
+ # sheet columns, per-tile animations, and which tiles are solid. Solidity is
9
+ # *baked into the asset*: a tile is solid when it carries a Tiled collision shape
10
+ # (a per-tile `<objectgroup>` with at least one object), authored in Tiled's
11
+ # collision editor — so the map owns its collision, not the engine. `solid_ids`
12
+ # stays writable for games that want to override or augment it.
13
+ # Animation frames resolve from elapsed time so the renderer stays a thin layer.
14
+ class Tileset
15
+ Frame = Struct.new(:tile_id, :duration)
16
+
17
+ attr_reader :firstgid, :columns, :tile_width, :tile_height, :image_source, :animations
18
+ attr_accessor :solid_ids # Set of *local* tile ids treated as solid
19
+
20
+ def self.parse(tsx_string, firstgid:)
21
+ root = REXML::Document.new(tsx_string).root
22
+
23
+ animations = {}
24
+ solid = Set.new
25
+ root.each_element('tile') do |tile_el|
26
+ id = tile_el.attributes['id'].to_i
27
+ animations[id] = parse_animation(tile_el)
28
+ solid << id if collision_shape?(tile_el)
29
+ end
30
+ animations.compact!
31
+
32
+ new(
33
+ firstgid: firstgid,
34
+ columns: root.attributes['columns'].to_i,
35
+ tile_width: root.attributes['tilewidth'].to_i,
36
+ tile_height: root.attributes['tileheight'].to_i,
37
+ image_source: root.elements['image'].attributes['source'],
38
+ animations: animations,
39
+ solid_ids: solid
40
+ )
41
+ end
42
+
43
+ # Frames for an animated tile, or nil (compacted away) for a static one.
44
+ def self.parse_animation(tile_el)
45
+ anim = tile_el.elements['animation']
46
+ return unless anim
47
+
48
+ anim.get_elements('frame').map do |f|
49
+ Frame.new(f.attributes['tileid'].to_i, f.attributes['duration'].to_i)
50
+ end
51
+ end
52
+
53
+ # A tile is solid if it carries a Tiled collision shape — an `<objectgroup>`
54
+ # with at least one object (the per-tile collision editor's output).
55
+ def self.collision_shape?(tile_el)
56
+ group = tile_el.elements['objectgroup']
57
+ !group.nil? && !group.elements['object'].nil?
58
+ end
59
+
60
+ def initialize(firstgid:, columns:, tile_width:, tile_height:, image_source:, animations:, solid_ids: Set.new)
61
+ @firstgid = firstgid
62
+ @columns = columns
63
+ @tile_width = tile_width
64
+ @tile_height = tile_height
65
+ @image_source = image_source
66
+ @animations = animations
67
+ @solid_ids = solid_ids
68
+ end
69
+
70
+ def animated_ids
71
+ @animations.keys
72
+ end
73
+
74
+ def local_id(gid)
75
+ gid - @firstgid
76
+ end
77
+
78
+ def solid?(gid)
79
+ return false if gid.zero?
80
+
81
+ @solid_ids.include?(local_id(gid))
82
+ end
83
+
84
+ # The local tile id to draw for `local` at time `ms`, following its animation
85
+ # (or `local` itself if the tile isn't animated).
86
+ def frame_local_id(local, ms)
87
+ frames = @animations[local]
88
+ return local unless frames
89
+
90
+ total = frames.sum(&:duration)
91
+ t = ms % total
92
+ frames.each do |frame|
93
+ return frame.tile_id if t < frame.duration
94
+
95
+ t -= frame.duration
96
+ end
97
+ frames.last.tile_id
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RGame
4
+ module Engine
5
+ # A repeating interval timer for periodic events that aren't driven by input — a
6
+ # spawner emitting an enemy every N seconds, a tower's fire rate, a wave clock.
7
+ #
8
+ # It only accumulates time; the owner decides what each elapsed interval means. That
9
+ # split lets one primitive serve both styles: "act automatically" (consume every ready
10
+ # interval) and "stay loaded until conditions allow" (check `ready?`, but `consume`
11
+ # only when actually acting — so a tower with no target keeps its shot ready instead of
12
+ # wasting it). Pure and allocation-free, so it ticks on the per-frame path.
13
+ #
14
+ # timer = Engine::Timer.new(0.8)
15
+ # timer.update(dt)
16
+ # if timer.ready? # at least one whole interval has passed
17
+ # timer.consume # deduct it (remainder carries forward, so timing won't drift)
18
+ # spawn_enemy
19
+ # end
20
+ class Timer
21
+ attr_accessor :interval
22
+
23
+ def initialize(interval)
24
+ @interval = interval
25
+ @elapsed = 0.0
26
+ end
27
+
28
+ # Advance by one timestep. Returns self so callers can chain.
29
+ def update(dt)
30
+ @elapsed += dt
31
+ self
32
+ end
33
+
34
+ # Has at least one whole interval accumulated since the last consume?
35
+ def ready? = @elapsed >= @interval
36
+
37
+ # Deduct one interval after acting on a ready timer. Carries the remainder forward
38
+ # (rather than zeroing) so a long-running cadence doesn't drift. Returns self.
39
+ def consume
40
+ @elapsed -= @interval
41
+ self
42
+ end
43
+
44
+ # Drop any accumulated time — e.g. after retuning the interval. Returns self.
45
+ def reset
46
+ @elapsed = 0.0
47
+ self
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RGame::Engine — the scene graph a game is written against: nodes, components,
4
+ # signals, scenes, collision, tile maps, pathing, pooling.
5
+ #
6
+ # Pure Ruby, and it links nothing. It may hold `RGame::Util` values but may not
7
+ # name `RGame::Core` at all, reaching the platform only through objects handed
8
+ # to it — a `renderer` passed into `draw`, an audio server behind
9
+ # `AudioDirector`. See CLAUDE.md, "The three layers, and who may talk to whom".
10
+ #
11
+ # That is what lets the whole layer be specified with no window, no GPU and no
12
+ # clock: `spec/` drives `update(dt)` directly and can run a simulated hour in
13
+ # milliseconds.
14
+ #
15
+ # Loaded by `require "rgame"` along with Util, since between them they are
16
+ # everything that runs without a window. Requirable on its own too — hence the
17
+ # `util` require below rather than relying on `rgame.rb` to have got there
18
+ # first: `TileMap` holds a `Util::Tensor`, which is exactly the dependency the
19
+ # three-layer rule allows and this file therefore has to declare.
20
+ require_relative 'util'
21
+
22
+ require_relative 'engine/matrix'
23
+ require_relative 'engine/signal'
24
+ require_relative 'engine/node2d'
25
+ require_relative 'engine/component'
26
+ require_relative 'engine/animation_set'
27
+ require_relative 'engine/animator'
28
+ require_relative 'engine/camera'
29
+ require_relative 'engine/camera_view'
30
+ require_relative 'engine/path'
31
+ require_relative 'engine/timer'
32
+ require_relative 'engine/tileset'
33
+ require_relative 'engine/tile_map'
34
+ require_relative 'engine/tile_collision'
35
+ require_relative 'engine/collision_box'
36
+ require_relative 'engine/collision_system'
37
+ require_relative 'engine/circle_collider'
38
+ require_relative 'engine/spatial_hash'
39
+ require_relative 'engine/pool'
40
+ require_relative 'engine/resettable'
41
+ require_relative 'engine/audio_bus'
42
+ require_relative 'engine/audio_director'
43
+ require_relative 'engine/i18n'
44
+ require_relative 'engine/cached_label'
45
+ require_relative 'engine/debug_overlay'
46
+ require_relative 'engine/body'
47
+ require_relative 'engine/actor'
48
+ require_relative 'engine/input/actions'
49
+ require_relative 'engine/input/action_mapper'
50
+ require_relative 'engine/input/player_controller'
51
+ require_relative 'engine/scene/scene_stack'
52
+ require_relative 'engine/components/velocity'
53
+ require_relative 'engine/components/pool'
54
+ require_relative 'engine/components/path_follow'
55
+ require_relative 'engine/components/timer'
56
+ require_relative 'engine/components/screen_wrap'
57
+ require_relative 'engine/components/despawn_offscreen'
58
+ require_relative 'engine/components/circle_collider'
59
+ require_relative 'engine/components/collision_world'
60
+ require_relative 'engine/components/targeting'
61
+ require_relative 'engine/components/sprite'
62
+ require_relative 'engine/components/thrust_controller'
63
+ require_relative 'engine/components/action_trigger'
64
+ require_relative 'engine/components/tile_world'
65
+ require_relative 'engine/components/character_body'
66
+ require_relative 'engine/components/player_controller'
67
+ require_relative 'engine/components/wander_controller'
68
+ require_relative 'engine/components/animated_sprite'
@@ -0,0 +1,102 @@
1
+ Digitized data copyright (c) 2010 Google Corporation
2
+ with Reserved Font Arimo, Tinos and Cousine.
3
+ Copyright (c) 2012 Red Hat, Inc.
4
+ with Reserved Font Name Liberation.
5
+
6
+ This Font Software is licensed under the SIL Open Font License,
7
+ Version 1.1.
8
+
9
+ This license is copied below, and is also available with a FAQ at:
10
+ http://scripts.sil.org/OFL
11
+
12
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
13
+
14
+ PREAMBLE The goals of the Open Font License (OFL) are to stimulate
15
+ worldwide development of collaborative font projects, to support the font
16
+ creation efforts of academic and linguistic communities, and to provide
17
+ a free and open framework in which fonts may be shared and improved in
18
+ partnership with others.
19
+
20
+ The OFL allows the licensed fonts to be used, studied, modified and
21
+ redistributed freely as long as they are not sold by themselves.
22
+ The fonts, including any derivative works, can be bundled, embedded,
23
+ redistributed and/or sold with any software provided that any reserved
24
+ names are not used by derivative works. The fonts and derivatives,
25
+ however, cannot be released under any other type of license. The
26
+ requirement for fonts to remain under this license does not apply to
27
+ any document created using the fonts or their derivatives.
28
+
29
+
30
+
31
+ DEFINITIONS
32
+ "Font Software" refers to the set of files released by the Copyright
33
+ Holder(s) under this license and clearly marked as such.
34
+ This may include source files, build scripts and documentation.
35
+
36
+ "Reserved Font Name" refers to any names specified as such after the
37
+ copyright statement(s).
38
+
39
+ "Original Version" refers to the collection of Font Software components
40
+ as distributed by the Copyright Holder(s).
41
+
42
+ "Modified Version" refers to any derivative made by adding to, deleting,
43
+ or substituting ? in part or in whole ?
44
+ any of the components of the Original Version, by changing formats or
45
+ by porting the Font Software to a new environment.
46
+
47
+ "Author" refers to any designer, engineer, programmer, technical writer
48
+ or other person who contributed to the Font Software.
49
+
50
+
51
+ PERMISSION & CONDITIONS
52
+
53
+ Permission is hereby granted, free of charge, to any person obtaining a
54
+ copy of the Font Software, to use, study, copy, merge, embed, modify,
55
+ redistribute, and sell modified and unmodified copies of the Font
56
+ Software, subject to the following conditions:
57
+
58
+ 1) Neither the Font Software nor any of its individual components,in
59
+ Original or Modified Versions, may be sold by itself.
60
+
61
+ 2) Original or Modified Versions of the Font Software may be bundled,
62
+ redistributed and/or sold with any software, provided that each copy
63
+ contains the above copyright notice and this license. These can be
64
+ included either as stand-alone text files, human-readable headers or
65
+ in the appropriate machine-readable metadata fields within text or
66
+ binary files as long as those fields can be easily viewed by the user.
67
+
68
+ 3) No Modified Version of the Font Software may use the Reserved Font
69
+ Name(s) unless explicit written permission is granted by the
70
+ corresponding Copyright Holder. This restriction only applies to the
71
+ primary font name as presented to the users.
72
+
73
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
74
+ Software shall not be used to promote, endorse or advertise any
75
+ Modified Version, except to acknowledge the contribution(s) of the
76
+ Copyright Holder(s) and the Author(s) or with their explicit written
77
+ permission.
78
+
79
+ 5) The Font Software, modified or unmodified, in part or in whole, must
80
+ be distributed entirely under this license, and must not be distributed
81
+ under any other license. The requirement for fonts to remain under
82
+ this license does not apply to any document created using the Font
83
+ Software.
84
+
85
+
86
+
87
+ TERMINATION
88
+ This license becomes null and void if any of the above conditions are not met.
89
+
90
+
91
+
92
+ DISCLAIMER
93
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
94
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
95
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
96
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
97
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
98
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
99
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
100
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
101
+ DEALINGS IN THE FONT SOFTWARE.
102
+