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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- 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
|
|
8
|
-
#
|
|
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
|
-
|
|
16
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
@finished = false
|
|
38
|
-
place_at(0)
|
|
67
|
+
super
|
|
68
|
+
restart
|
|
39
69
|
end
|
|
40
70
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
114
|
+
return true if @segment >= @path.count - 1
|
|
56
115
|
end
|
|
57
|
-
|
|
116
|
+
false
|
|
58
117
|
end
|
|
59
118
|
|
|
60
|
-
|
|
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
|
|
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 =
|
|
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))
|
|
@@ -3,23 +3,45 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
module Components
|
|
6
|
-
# Wraps the node's position toroidally within
|
|
7
|
-
# entity leaving one edge reappears on the opposite one.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
|
23
|
+
def initialize(width: nil, height: nil, margin: 0.0)
|
|
12
24
|
super()
|
|
13
|
-
@
|
|
14
|
-
@
|
|
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
|
-
|
|
20
|
-
node.
|
|
21
|
-
node.
|
|
22
|
-
|
|
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
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
|
|
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,
|
|
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
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
#
|
|
12
|
-
#
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
9
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
26
|
+
@timer = 0.0
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def on_attach
|
|
26
|
-
@body =
|
|
30
|
+
@body = require_sibling(CharacterBody)
|
|
27
31
|
@last_x = node.x
|
|
28
32
|
@last_y = node.y
|
|
29
33
|
end
|