rgame 0.2.0 → 0.3.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 (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +2 -3
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +152 -11
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
@@ -4,26 +4,58 @@ module RGame
4
4
  module Engine
5
5
  module Components
6
6
  # Walks the owning node along an Engine::Path at a constant speed, segment by
7
- # segment, and emits `on_finished` once it reaches the final waypoint — the seam a
8
- # tower defense game uses to leak a life when an enemy reaches the base.
7
+ # segment, and emits `on_finished` once it reaches the final waypoint — the seam for
8
+ # whatever should happen when a walker arrives.
9
9
  #
10
10
  # The walk is allocation-free: it tracks the current segment and the distance into
11
11
  # it, advancing through as many segments as one step crosses (so a fast mover over
12
12
  # short segments still lands correctly), then interpolates the node's position from
13
13
  # the segment endpoints. Movement is driven purely by `speed * dt`; this is not a
14
14
  # Velocity integrator and ignores the node's angle.
15
- class PathFollow < Engine::Component
16
- signal :on_finished # emits no payload; the owning node identifies which follower finished
15
+ #
16
+ # ## Blocked, the walk waits
17
+ #
18
+ # A Mover, so `blocked_by:` stops it the way it stops a CharacterBody — see Mover's
19
+ # header. Declaring nothing keeps the walk exactly as described above: the node is
20
+ # *placed* on the path each step.
21
+ #
22
+ # Declaring something turns the placement into a move: the walk still works out
23
+ # where the node should be, and then gets there through `apply_move`. **A step that
24
+ # is stopped short does not advance the walk** — progress goes back to where the
25
+ # step began. The obvious alternative, letting progress run on and the node catch up,
26
+ # is wrong twice over: a follower held behind something for a second would race
27
+ # ahead to where it "should" be once let go, and `on_finished` could fire for a
28
+ # walker still standing in front of the obstacle.
29
+ #
30
+ # One consequence is easy to miss: **a held follower does not slide along what stopped
31
+ # it** the way a Velocity does. A step's free axis still moves, but the rewind aims the
32
+ # next step at the same point on the path, from a little closer, so a follower pressed
33
+ # diagonally against a wall creeps to rest instead of sliding to the corner. A walker
34
+ # meant to get round something replans its path rather than relying on the slide.
35
+ #
36
+ # `on_attach` still places the node on the first waypoint absolutely, blocked or
37
+ # not: where a walker starts is a placement, not a step.
38
+ #
39
+ # ## A new route, and none
40
+ #
41
+ # `path: nil` builds an idle follower, which moves nothing and never finishes, and
42
+ # `follow(path)` hands any follower a route to walk from its start — the same restart
43
+ # entering the tree gives, so a walker that finished one route walks the next and
44
+ # finishes again, and one still walking drops its old route at once.
45
+ #
46
+ # Its heading is the unit direction of the segment it is on, worked out when the walk
47
+ # crosses into a segment rather than on every read.
48
+ class PathFollow < Mover
49
+ signal :on_finished
17
50
 
18
51
  attr_accessor :speed
52
+ attr_reader :path, :heading_x, :heading_y
19
53
 
20
- def initialize(path:, speed:)
21
- super()
54
+ def initialize(speed:, path: nil, blocked_by: [])
55
+ super(blocked_by: blocked_by)
22
56
  @path = path
23
57
  @speed = speed
24
- @segment = 0 # walking from waypoint @segment to @segment + 1
25
- @distance = 0.0 # distance travelled into the current segment
26
- @finished = false
58
+ restart
27
59
  end
28
60
 
29
61
  def finished? = @finished
@@ -32,52 +64,112 @@ module RGame
32
64
  # progress cleared — so a pooled follower reacquired and re-added begins a fresh walk
33
65
  # rather than resuming (or staying finished) where its previous life ended.
34
66
  def on_attach
35
- @segment = 0
36
- @distance = 0.0
37
- @finished = false
38
- place_at(0)
67
+ super
68
+ restart
39
69
  end
40
70
 
41
- def update(dt)
42
- return if @finished
71
+ # Walk `path` from its first waypoint, where the node is placed, whatever this
72
+ # follower was doing — idle, finished, or halfway along another route. `nil` stops
73
+ # it where it stands.
74
+ def follow(path)
75
+ @path = path
76
+ restart
77
+ end
78
+
79
+ private
80
+
81
+ def take_step(dt)
82
+ return if @finished || @path.nil?
83
+
84
+ from_segment = @segment
85
+ from_distance = @distance
86
+
87
+ if advance_to_end?(@speed * dt)
88
+ last = @path.count - 1
89
+ return finish if moved_to?(@path.x_at(last), @path.y_at(last))
90
+
91
+ rewind(from_segment, from_distance)
92
+ else
93
+ seg_len = @path.segment_length(@segment)
94
+ t = seg_len.zero? ? 0.0 : @distance / seg_len
95
+ sx = @path.x_at(@segment)
96
+ sy = @path.y_at(@segment)
97
+ reached = moved_to?(sx + ((@path.x_at(@segment + 1) - sx) * t),
98
+ sy + ((@path.y_at(@segment + 1) - sy) * t))
99
+ rewind(from_segment, from_distance) unless reached
100
+ end
101
+ aim
102
+ end
43
103
 
44
- remaining = @speed * dt
104
+ def advance_to_end?(remaining)
45
105
  while remaining.positive?
46
106
  left = @path.segment_length(@segment) - @distance
47
107
  if remaining < left
48
108
  @distance += remaining
49
- break
109
+ return false
50
110
  end
51
- # Consume the rest of this segment and step onto the next waypoint.
52
111
  remaining -= left
53
112
  @segment += 1
54
113
  @distance = 0.0
55
- return finish if @segment >= @path.count - 1
114
+ return true if @segment >= @path.count - 1
56
115
  end
57
- place_along_segment
116
+ false
58
117
  end
59
118
 
60
- private
119
+ def moved_to?(x, y)
120
+ unless blocking?
121
+ node.x = x
122
+ node.y = y
123
+ return true
124
+ end
125
+
126
+ apply_move(x - node.x, y - node.y)
127
+ !last_move_blocked?
128
+ end
129
+
130
+ def rewind(segment, distance)
131
+ @segment = segment
132
+ @distance = distance
133
+ end
61
134
 
62
135
  def finish
63
- place_at(@path.count - 1)
64
136
  @finished = true
137
+ head_nowhere
65
138
  on_finished_signal.emit
66
139
  end
67
140
 
141
+ def restart
142
+ @segment = 0
143
+ @distance = 0.0
144
+ @finished = false
145
+ head_nowhere
146
+ return unless @path
147
+
148
+ aim
149
+ place_at(0) if node
150
+ end
151
+
152
+ def aim
153
+ return if @segment == @aimed_segment
154
+
155
+ @aimed_segment = @segment
156
+ length = @path.segment_length(@segment)
157
+ return head_nowhere if length.zero?
158
+
159
+ @heading_x = (@path.x_at(@segment + 1) - @path.x_at(@segment)) / length
160
+ @heading_y = (@path.y_at(@segment + 1) - @path.y_at(@segment)) / length
161
+ end
162
+
163
+ def head_nowhere
164
+ @aimed_segment = nil
165
+ @heading_x = 0.0
166
+ @heading_y = 0.0
167
+ end
168
+
68
169
  def place_at(waypoint)
69
170
  node.x = @path.x_at(waypoint)
70
171
  node.y = @path.y_at(waypoint)
71
172
  end
72
-
73
- def place_along_segment
74
- seg_len = @path.segment_length(@segment)
75
- t = seg_len.zero? ? 0.0 : @distance / seg_len
76
- sx = @path.x_at(@segment)
77
- sy = @path.y_at(@segment)
78
- node.x = sx + ((@path.x_at(@segment + 1) - sx) * t)
79
- node.y = sy + ((@path.y_at(@segment + 1) - sy) * t)
80
- end
81
173
  end
82
174
  end
83
175
  end
@@ -5,7 +5,10 @@ module RGame
5
5
  module Components
6
6
  # Drives a CharacterBody sibling from two input axes — direct 8-way walking, no
7
7
  # inertia (unlike ThrustController). Each frame it copies the axis snapshot into
8
- # the body's movement intent; the body does the collision-checked move.
8
+ # the body's movement intent; what the body then does with it is the body's
9
+ # business — an unblocked CharacterBody moves the node, a blocked one resolves
10
+ # the step against the map. The lookup names the base class and get_component
11
+ # matches by ancestry, so this needs to know nothing about which is there.
9
12
  class PlayerController < Engine::Component
10
13
  def initialize(x_axis: :move_x, y_axis: :move_y)
11
14
  super()
@@ -13,7 +16,7 @@ module RGame
13
16
  @y_axis = y_axis
14
17
  end
15
18
 
16
- def on_attach = @body = node.get_component(CharacterBody)
19
+ def on_attach = @body = require_sibling(CharacterBody)
17
20
 
18
21
  def control(actions)
19
22
  @body.set_intent(actions.axis(@x_axis), actions.axis(@y_axis))
@@ -37,7 +37,7 @@ module RGame
37
37
  @pool.reclaim_if do |child|
38
38
  next false unless child.freed?
39
39
 
40
- node.remove_node(child) # idempotent if the deferred-free sweep already detached it
40
+ node.remove_node(child)
41
41
  true
42
42
  end
43
43
  end
@@ -3,23 +3,45 @@
3
3
  module RGame
4
4
  module Engine
5
5
  module Components
6
- # Wraps the node's position toroidally within a rectangle (plus margin), so an
7
- # entity leaving one edge reappears on the opposite one. From Body#wrap!. The
8
- # bounds are passed in; in a scrolling game they'd come from a scene-scoped
9
- # World system instead.
6
+ # Wraps the node's position toroidally within the world bounds (plus margin), so
7
+ # an entity leaving one edge reappears on the opposite one.
8
+ #
9
+ # The bounds come from the scene's world system — anything answering the
10
+ # WorldBounds contract, so Components::World or Components::TileWorld — and are
11
+ # resolved when the node enters the tree. Passing `width:`/`height:` overrides
12
+ # that for a node whose wrap region is not the whole world.
13
+ #
14
+ # The node is tested and placed in **world space**, which is the frame WorldBounds
15
+ # is stated in: a node under an offset container wraps at the world's edge, not at
16
+ # an edge shifted by wherever the container sits. The write goes through
17
+ # Node2D#world_x=, so the node's local position is what actually changes.
18
+ #
19
+ # A wrap is a placement, not a step: it does not ask a sibling Mover's `blocked_by`
20
+ # whether the far edge is free, so a node wrapped onto a blocker is left pressed
21
+ # against it.
10
22
  class ScreenWrap < Engine::Component
11
- def initialize(width:, height:, margin: 0.0)
23
+ def initialize(width: nil, height: nil, margin: 0.0)
12
24
  super()
13
- @width = width
14
- @height = height
25
+ @given_width = width
26
+ @given_height = height
15
27
  @margin = margin
16
28
  end
17
29
 
30
+ # Re-resolved on every entry rather than cached from the first, so a pooled
31
+ # entity recycled into a differently sized scene wraps against that scene.
32
+ def on_attach
33
+ WorldBounds.one_response!(node)
34
+ @width, @height = WorldBounds.resolve(node, @given_width, @given_height)
35
+ end
36
+
18
37
  def update(_dt)
19
- node.x = @width + @margin if node.x < -@margin
20
- node.x = -@margin if node.x > @width + @margin
21
- node.y = @height + @margin if node.y < -@margin
22
- node.y = -@margin if node.y > @height + @margin
38
+ x = node.world_x
39
+ node.world_x = @width + @margin if x < -@margin
40
+ node.world_x = -@margin if x > @width + @margin
41
+
42
+ y = node.world_y
43
+ node.world_y = @height + @margin if y < -@margin
44
+ node.world_y = -@margin if y > @height + @margin
23
45
  end
24
46
  end
25
47
  end
@@ -3,10 +3,10 @@
3
3
  module RGame
4
4
  module Engine
5
5
  module Components
6
- # Draws a single registered image centered on the node's absolute origin. It
7
- # passes NO angle to the renderer: Node2D#draw already wraps a node's own draws
8
- # in renderer.rotated(abs_angle, abs_x, abs_y), so the node's rotation orients
9
- # the sprite passing an angle here too would rotate it twice.
6
+ # Draws a single registered image centred on the node's own origin. It passes
7
+ # NO angle and NO position to the renderer: Node2D#draw has already pushed the
8
+ # node's transform, so drawing at (0, 0) *is* drawing at the node, correctly
9
+ # rotated. Passing either would apply it a second time.
10
10
  #
11
11
  # `scale` is writable so a pooled entity (e.g. a multi-tier rock) can retune it on
12
12
  # reset. `z` is the render layer (NOT the node's transform z / abs_z) — kept under
@@ -27,13 +27,19 @@ module RGame
27
27
  # Centred on the node's origin and scaled, so the footprint to cull
28
28
  # against is the node's box scaled and offset back by half of itself. A
29
29
  # node that never set a size is never culled — see Culling.
30
+ #
31
+ # Culling is the one thing here still stated in **world** coordinates:
32
+ # it compares against the camera, which is nowhere near this node's
33
+ # local space. Drawing is local, culling is world, and the two are
34
+ # deliberately different arguments.
30
35
  def draw(renderer, view)
31
36
  width = node.width * @scale
32
37
  height = node.height * @scale
33
- return if culled?(view, node.abs_x - (width / 2.0), node.abs_y - (height / 2.0),
38
+ lift = node.elevation
39
+ return if culled?(view, node.world_x - (width / 2.0), node.world_y - lift - (height / 2.0),
34
40
  width, height)
35
41
 
36
- renderer.image(@id, node.abs_x, node.abs_y, scale: @scale, z: @layer)
42
+ renderer.image(@id, 0, -lift, scale: @scale, z: @layer)
37
43
  end
38
44
  end
39
45
  end
@@ -3,17 +3,17 @@
3
3
  module RGame
4
4
  module Engine
5
5
  module Components
6
- # Picks an enemy for the owning node (a tower) to aim at: each update it queries the
7
- # scene's CollisionWorld around the node's world origin and exposes the chosen target
8
- # via #target (the enemy node, or nil). The owner reads it to fire — Targeting only
6
+ # Picks a node for the owning node to aim at: each update it queries the scene's
7
+ # CollisionWorld around the node's world origin and exposes the chosen target via
8
+ # #target (the candidate's node, or nil). The owner reads it to act — Targeting only
9
9
  # selects, it never moves or shoots.
10
10
  #
11
- # The CollisionWorld it queries is the same broadphase enemies register with through
12
- # their CircleCollider, so targeting needs no enemy list of its own. The `layer`
13
- # restricts candidates (a tower wants :enemy, not other towers or projectiles).
11
+ # The CollisionWorld it queries is the same broadphase every collider registers with,
12
+ # so targeting needs no candidate list of its own. The `layer` restricts candidates
13
+ # (pass :enemy to pick only colliders on that layer, not allies or projectiles).
14
14
  #
15
15
  # Policies (how to choose among the candidates in range):
16
- # - :nearest — the closest enemy (the default; one broadphase nearest-lookup).
16
+ # - :nearest — the closest candidate (the default; one broadphase nearest-lookup).
17
17
  # More policies (e.g. furthest-along-path) arrive with the state they need.
18
18
  class Targeting < Engine::Component
19
19
  POLICIES = %i[nearest].freeze
@@ -28,7 +28,7 @@ module RGame
28
28
  @target = nil
29
29
  end
30
30
 
31
- # The chosen enemy node, or nil when nothing is in range. Refreshed every update.
31
+ # The chosen node, or nil when nothing is in range. Refreshed every update.
32
32
  attr_reader :target
33
33
 
34
34
  # Pull the broadphase once it's reachable (scene scope, resolved up the tree).
@@ -41,11 +41,9 @@ module RGame
41
41
 
42
42
  private
43
43
 
44
- # Allocation-free: a symbol dispatch over a known-small policy set, each delegating
45
- # to an allocation-free CollisionWorld query.
46
44
  def pick
47
45
  case @policy
48
- when :nearest then @world.nearest(node.abs_x, node.abs_y, @range, layer: @layer)
46
+ when :nearest then @world.nearest(node.world_x, node.world_y, @range, layer: @layer)
49
47
  end
50
48
  end
51
49
  end
@@ -26,7 +26,7 @@ module RGame
26
26
  end
27
27
 
28
28
  # The Velocity sibling is only guaranteed present once attached to a node.
29
- def on_attach = @velocity = node.get_component(Velocity)
29
+ def on_attach = @velocity = require_sibling(Velocity)
30
30
 
31
31
  def control(actions)
32
32
  @velocity.spin = actions.axis(@turn_action) * @turn_speed
@@ -8,8 +8,19 @@ module RGame
8
8
  # everything an actor needs from it — collision against the solid tiles, the world
9
9
  # bounds, and drawing the map through the scene's camera.
10
10
  #
11
- # Collision reuses Engine::CollisionSystem (TileCollision + a world-bounds clamp);
12
- # the tile solidity is whatever the map's tileset reports (baked per-tile in Tiled).
11
+ # **It does not resolve a step.** What it owns is the map's solid tiles as a blocker
12
+ # source (#blockers, an Engine::TileBlockers); the actor that wants to be stopped by
13
+ # them borrows it and resolves against it — see Components::Mover, which
14
+ # builds its own Engine::CollisionSystem out of the sources its `blocked_by` names.
15
+ # A mover may be blocked by tiles, by other actors, or by both, and only the mover
16
+ # knows which, so the resolver is the mover's and the grid is this system's. The tile
17
+ # solidity itself is whatever the map's tileset reports (baked per-tile in Tiled).
18
+ # The same solidity, viewed as a graph for planning routes, is #nav_grid.
19
+ #
20
+ # **Solidity is read from the map once**, into one Util::SolidGrid, the first time
21
+ # anything asks — and #blockers, #nav_grid and #solid? all read that store, never the
22
+ # map. So they cannot disagree about a cell, and a resolve costs a byte lookup rather
23
+ # than a walk through the map's layers and tileset.
13
24
  #
14
25
  # **It does not draw.** Drawing the map is RGame::Engine::TileMapLayer, one
15
26
  # node per Tiled layer, mounted inside the WorldView so the map is drawn
@@ -23,6 +34,8 @@ module RGame
23
34
  # draw time. Stop calling `update` and the water freezes, which is what pausing
24
35
  # should look like.
25
36
  class TileWorld < Engine::Component
37
+ include WorldBounds
38
+
26
39
  attr_reader :tilemap_id, :elapsed
27
40
 
28
41
  # `cameras` are the cameras this map bounds — every player's, normally.
@@ -34,18 +47,24 @@ module RGame
34
47
  @tilemap_id = tilemap_id
35
48
  @elapsed = 0.0
36
49
  Array(cameras).each { |camera| bound(camera) }
37
- @collision = Engine::CollisionSystem.new(
38
- tile_collision: Engine::TileCollision.new(
39
- tile_width: map.tile_width, tile_height: map.tile_height,
40
- solid: ->(col, row) { map.solid_tile?(col, row) }
41
- ),
42
- world_width: map.pixel_width, world_height: map.pixel_height
43
- )
50
+ end
51
+
52
+ # The map's solid tiles as a blocker source, for a body that declared
53
+ # `blocked_by: [:tiles]`. The same source every time, so every body on the map
54
+ # shares one.
55
+ def blockers
56
+ @blockers ||= Engine::TileBlockers.new(grid: solid_grid, tile_width: @map.tile_width,
57
+ tile_height: @map.tile_height)
44
58
  end
45
59
 
46
60
  def world_width = @map.pixel_width
47
61
  def world_height = @map.pixel_height
48
62
 
63
+ # The size of one cell, in pixels — what turns a world position into the cell
64
+ # #nav_grid plans over.
65
+ def tile_width = @map.tile_width
66
+ def tile_height = @map.tile_height
67
+
49
68
  def layer_count = @map.layer_count
50
69
 
51
70
  # The first layer Tiled flags `above`, or the layer count if none is —
@@ -65,16 +84,25 @@ module RGame
65
84
  camera
66
85
  end
67
86
 
68
- # Move an actor (responds to x/y/collision_box) by (dx, dy), sliding along solids
69
- # and clamped inside the world. Delegates to the reused collision system.
70
- def move(actor, dx, dy) = @collision.move(actor, dx, dy)
87
+ def solid?(col, row) = solid_grid.solid?(col, row)
71
88
 
72
- def solid?(col, row) = @map.solid_tile?(col, row)
89
+ # The map's solid tiles as an Engine::NavGrid, for planning a route rather than
90
+ # resolving a step — over the same store #blockers reads, as a second view. Built on
91
+ # first ask, and the same grid every time after.
92
+ def nav_grid
93
+ @nav_grid ||= Engine::NavGrid.new(grid: solid_grid)
94
+ end
73
95
 
74
96
  # Advances the tile animations. Seconds, like every other duration here.
75
97
  def update(dt)
76
98
  @elapsed += dt
77
99
  end
100
+
101
+ private
102
+
103
+ def solid_grid
104
+ @solid_grid ||= Util::SolidGrid.build(@map.width, @map.height) { |col, row| @map.solid_tile?(col, row) }
105
+ end
78
106
  end
79
107
  end
80
108
  end
@@ -26,7 +26,7 @@ module RGame
26
26
  # When a node needs more than one (a spawn cadence and a wave cadence), give them
27
27
  # distinct `as:` names — a node holds one component per slot.
28
28
  class Timer < Engine::Component
29
- signal :on_timeout # emits no payload
29
+ signal :on_timeout
30
30
 
31
31
  def initialize(interval, repeating: true)
32
32
  super()
@@ -5,20 +5,36 @@ module RGame
5
5
  module Components
6
6
  # Integrates linear and angular velocity into the node's transform each step.
7
7
  # Free-moving entities (rocks, bullets) use it alone; a controller component
8
- # (or the node's own control hook) writes vx/vy/spin as intent. From Body#integrate.
9
- class Velocity < Engine::Component
8
+ # (or the node's own control hook) writes vx/vy/spin as intent.
9
+ #
10
+ # A Mover, so `blocked_by:` stops it the way it stops a CharacterBody — see Mover's
11
+ # header. A blocked step keeps `vx`/`vy` as they were: they are the intent, and what
12
+ # a stop should do to them (nothing, zero them, bounce) is the game's call, made in
13
+ # an `on_blocked` handler.
14
+ class Velocity < Mover
10
15
  attr_accessor :vx, :vy, :spin
11
16
 
12
- def initialize(vx: 0.0, vy: 0.0, spin: 0.0)
13
- super()
17
+ def initialize(vx: 0.0, vy: 0.0, spin: 0.0, blocked_by: [])
18
+ super(blocked_by: blocked_by)
14
19
  @vx = vx
15
20
  @vy = vy
16
21
  @spin = spin
17
22
  end
18
23
 
19
- def update(dt)
20
- node.x += @vx * dt
21
- node.y += @vy * dt
24
+ # The velocity scaled so its larger axis is ±1 — the direction, without the square
25
+ # root a unit vector would cost.
26
+ def heading_x = scale_to_heading(@vx)
27
+ def heading_y = scale_to_heading(@vy)
28
+
29
+ private
30
+
31
+ def scale_to_heading(axis)
32
+ larger = [@vx.abs, @vy.abs].max
33
+ larger.zero? ? 0.0 : axis / larger.to_f
34
+ end
35
+
36
+ def take_step(dt)
37
+ apply_move(@vx * dt, @vy * dt)
22
38
  node.angle += @spin * dt
23
39
  end
24
40
  end
@@ -7,6 +7,10 @@ module RGame
7
7
  # direction (one of eight, or idle) and holds it until a timer elapses — or until a
8
8
  # wall blocks it, at which point it re-rolls early instead of pushing into the wall.
9
9
  # The RNG is injected so behaviour is deterministic in tests.
10
+ #
11
+ # "Blocked" is measured as *the node did not move while intending to*, not asked of
12
+ # a collision world — so this works over any CharacterBody, and simply never fires
13
+ # for one whose steps always land.
10
14
  class WanderController < Engine::Component
11
15
  MOVED_EPS = 1e-6
12
16
  DIRECTIONS = [
@@ -19,11 +23,11 @@ module RGame
19
23
  @rng = rng
20
24
  @change_interval = change_interval
21
25
  @idle_chance = idle_chance
22
- @timer = 0.0 # rolls a direction on the first update
26
+ @timer = 0.0
23
27
  end
24
28
 
25
29
  def on_attach
26
- @body = node.get_component(CharacterBody)
30
+ @body = require_sibling(CharacterBody)
27
31
  @last_x = node.x
28
32
  @last_y = node.y
29
33
  end