rgame 0.2.0 → 0.3.1
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 +144 -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 +3 -5
- 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 +167 -12
- 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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# A jump in a top-down view. Pressing the action lifts the node along a
|
|
7
|
+
# parabola that peaks at `peak` pixels halfway through `duration` seconds,
|
|
8
|
+
# then puts it back on the ground.
|
|
9
|
+
#
|
|
10
|
+
# The height is written to Node2D#elevation, never to `y`. In a top-down view
|
|
11
|
+
# a jump is a *drawing* offset: the sprite arcs above the spot the character
|
|
12
|
+
# stands on, while that spot, and every collider and camera reading it, stays
|
|
13
|
+
# on the ground. So a hop does not carry anyone over a wall. What it
|
|
14
|
+
# crosses is the game's decision, made by reading `airborne?`.
|
|
15
|
+
#
|
|
16
|
+
# node.add_component(Engine::Components::Hop.new(peak: 18, duration: 0.5))
|
|
17
|
+
#
|
|
18
|
+
# The arc is a function of the time accumulated in `update`, not of a clock,
|
|
19
|
+
# so a paused node hangs in the air and a spec can ask for the height at 0.25s.
|
|
20
|
+
# It starts on the action's press edge, so holding the button hops once.
|
|
21
|
+
# `action: nil` leaves only #jump, for something that is not a player.
|
|
22
|
+
class Hop < Engine::Component
|
|
23
|
+
attr_reader :peak, :duration, :height
|
|
24
|
+
|
|
25
|
+
def initialize(peak:, duration:, action: :jump)
|
|
26
|
+
super()
|
|
27
|
+
raise ArgumentError, "peak must be positive, got #{peak.inspect}" unless peak.positive?
|
|
28
|
+
raise ArgumentError, "duration must be positive, got #{duration.inspect}" unless duration.positive?
|
|
29
|
+
|
|
30
|
+
@peak = peak
|
|
31
|
+
@duration = duration
|
|
32
|
+
@action = action
|
|
33
|
+
@height = 0.0
|
|
34
|
+
@elapsed = 0.0
|
|
35
|
+
@airborne = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Attaching lands the node, so a pooled node reused mid-hop starts on the ground.
|
|
39
|
+
def on_attach = land
|
|
40
|
+
|
|
41
|
+
def airborne? = @airborne
|
|
42
|
+
|
|
43
|
+
# Leave the ground. Does nothing while already off it; `airborne?` says which.
|
|
44
|
+
def jump
|
|
45
|
+
return if @airborne
|
|
46
|
+
|
|
47
|
+
@airborne = true
|
|
48
|
+
@elapsed = 0.0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def control(actions)
|
|
52
|
+
jump if @action && actions.pressed?(@action)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def update(dt)
|
|
56
|
+
return unless @airborne
|
|
57
|
+
|
|
58
|
+
@elapsed += dt
|
|
59
|
+
return land if @elapsed >= @duration
|
|
60
|
+
|
|
61
|
+
@height = 4.0 * @peak * @elapsed * (@duration - @elapsed) / (@duration * @duration)
|
|
62
|
+
node.elevation = @height
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def land
|
|
68
|
+
@airborne = false
|
|
69
|
+
@elapsed = 0.0
|
|
70
|
+
@height = 0.0
|
|
71
|
+
node.elevation = 0
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# A stable name for one node, so something outside the tree can refer to it.
|
|
7
|
+
#
|
|
8
|
+
# sheep.add_component(Identity.new(id: 7))
|
|
9
|
+
# Identity.of(sheep) # => 7
|
|
10
|
+
#
|
|
11
|
+
# ## What it is for, and what does not need it
|
|
12
|
+
#
|
|
13
|
+
# Most saving needs no identity at all. A scene is a recipe and a save file
|
|
14
|
+
# is state: the scene rebuilds itself identically, and the save supplies the
|
|
15
|
+
# few facts that differ. A *singular* thing needs no id because the variable
|
|
16
|
+
# holding it is one — `@dog` is set where the dog is built and written
|
|
17
|
+
# straight to on load. *Interchangeable* things need none either, because
|
|
18
|
+
# their order will do: an array of positions restored in order is correct
|
|
19
|
+
# precisely when swapping two of them changes nothing observable.
|
|
20
|
+
#
|
|
21
|
+
# This is for the case those two do not cover:
|
|
22
|
+
#
|
|
23
|
+
# - **members of a collection that can die**, where each survivor keeps
|
|
24
|
+
# state of its own. An array index stops meaning anything the first time
|
|
25
|
+
# the middle of the list is removed;
|
|
26
|
+
# - **a reference from one saved thing to another** — a dog chasing a
|
|
27
|
+
# particular sheep. A node reference cannot be written to a file, and this
|
|
28
|
+
# is what it is written *as*.
|
|
29
|
+
#
|
|
30
|
+
# The second is the one that genuinely forces ids. A collection could always
|
|
31
|
+
# be respawned from its own records; a reference between two of them could
|
|
32
|
+
# not.
|
|
33
|
+
#
|
|
34
|
+
# ## The engine supplies the mechanism, a game supplies the meaning
|
|
35
|
+
#
|
|
36
|
+
# What gets an id, what the ids are, and how they are handed out are the
|
|
37
|
+
# game's business — the same division as `Timer`, which counts down without
|
|
38
|
+
# an opinion about what happens next. Two things follow from that, and both
|
|
39
|
+
# are the game's to get right:
|
|
40
|
+
#
|
|
41
|
+
# **Ids must be unique among the things that can refer to each other.** This
|
|
42
|
+
# component checks nothing; a duplicate is a save that restores the wrong
|
|
43
|
+
# object, silently.
|
|
44
|
+
#
|
|
45
|
+
# **The allocator belongs in the save.** A counter that restarts at 1 on
|
|
46
|
+
# load will reissue ids that the restored objects are already using, and the
|
|
47
|
+
# collision surfaces later as a reference pointing at the wrong thing. Save
|
|
48
|
+
# the next id alongside the objects and restore it too — it is one number,
|
|
49
|
+
# and forgetting it is the classic way this goes wrong.
|
|
50
|
+
class Identity < Engine::Component
|
|
51
|
+
attr_reader :id
|
|
52
|
+
|
|
53
|
+
def initialize(id:)
|
|
54
|
+
super()
|
|
55
|
+
raise ArgumentError, 'an identity needs an id' if id.nil?
|
|
56
|
+
|
|
57
|
+
@id = id
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The id of a node, or nil for one that carries no identity.
|
|
61
|
+
#
|
|
62
|
+
# This is the direction that matters at save time: a component holding a
|
|
63
|
+
# *node* — `Targeting#target` is the worked example — has to turn it into
|
|
64
|
+
# something writable, and it has only the node to go on. Going the other
|
|
65
|
+
# way is a game's own lookup, because only the game knows which
|
|
66
|
+
# collection to search.
|
|
67
|
+
def self.of(node)
|
|
68
|
+
node&.get_component(self)&.id
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# What every component that moves its node has in common: a step computed some way
|
|
7
|
+
# of its own, landing either straight on the node or against whatever may stop it.
|
|
8
|
+
# CharacterBody, Velocity and PathFollow are the three, and they are three classes
|
|
9
|
+
# because walking an intent, integrating a velocity and following a path are three
|
|
10
|
+
# different jobs. What they share is what happens *after* a step is computed, and
|
|
11
|
+
# that is this class.
|
|
12
|
+
#
|
|
13
|
+
# A mover fills in one private hook, `take_step(dt)`, and calls `apply_move(dx, dy)`
|
|
14
|
+
# from it. `update` is not for overriding: it opens the step, calls the hook and
|
|
15
|
+
# reports what stopped being in the way, so no mover can forget either edge.
|
|
16
|
+
#
|
|
17
|
+
# **Why a base class, and not a sibling component or a Node2D method.** A separate
|
|
18
|
+
# `Blocking` component that movers write through was tried, and it is order-dependent:
|
|
19
|
+
# closing the step has to happen after the mover's step, and a sibling can only do
|
|
20
|
+
# that from its own `update`, which runs wherever it sits in the component list — two
|
|
21
|
+
# add orders fired on_unblocked on two different ticks. A `Node2D#move_by` owning
|
|
22
|
+
# `blocked_by` is order-free, but puts collision into the base of every node, HUDs and
|
|
23
|
+
# menus included. A base class is order-free and touches only what moves.
|
|
24
|
+
#
|
|
25
|
+
# ## What stops a step is declared, not subclassed
|
|
26
|
+
#
|
|
27
|
+
# `blocked_by:` lists what a step may not pass through, and the default is nothing:
|
|
28
|
+
# the mover writes the node's position directly and needs no collider and no system
|
|
29
|
+
# on the scene. That free step is not quite free: `update` → `take_step` →
|
|
30
|
+
# `apply_move` is one dispatch more than a component writing `node.x` itself, which
|
|
31
|
+
# measured about 12% on a bare Velocity step (tens of nanoseconds). Inlining the free
|
|
32
|
+
# write into each subclass wins it back, at the cost of every mover copying
|
|
33
|
+
# `apply_move`'s free branch and reading this class's ivars — not taken.
|
|
34
|
+
#
|
|
35
|
+
# Velocity.new(vx: 120) # flies wherever it points
|
|
36
|
+
# Velocity.new(vx: 120, blocked_by: [:wall]) # stops flush against a :wall collider
|
|
37
|
+
# CharacterBody.new(speed: 80, blocked_by: [:tiles]) # slides along the map's solid tiles
|
|
38
|
+
# CharacterBody.new(speed: 80, blocked_by: %i[tiles npc]) # ...and does not walk through NPCs
|
|
39
|
+
# CharacterBody.new(speed: 80, blocked_by: %i[npc bounds]) # ...and cannot leave the world
|
|
40
|
+
#
|
|
41
|
+
# Two names are reserved: **`:tiles`** is the scene's TileWorld, and **`:bounds`** is the
|
|
42
|
+
# edge of the region the scene's WorldBounds describes. **Every other name is a collider
|
|
43
|
+
# layer**, resolved against the scene's CollisionWorld: a mover declaring `:npc` is
|
|
44
|
+
# stopped by any BoxCollider whose `layer` is `:npc`, flush against its edge, exactly the
|
|
45
|
+
# way a solid tile stops it. A layer that is empty, or whose colliders all leave, is not
|
|
46
|
+
# an error — the declaration says what *may* stop this mover, not what does.
|
|
47
|
+
#
|
|
48
|
+
# `:bounds` is declared rather than automatic, and a mover that does not declare it
|
|
49
|
+
# leaves the world. Stopping at the edge is one of three responses to it, beside
|
|
50
|
+
# ScreenWrap and DespawnOffscreen, and a node may carry only one — declaring `:bounds`
|
|
51
|
+
# beside either raises at attach (WorldBounds.one_response!).
|
|
52
|
+
#
|
|
53
|
+
# Blocking is box-versus-box: a CircleCollider on a declared layer reports its contacts
|
|
54
|
+
# as usual and stops nothing, and a mover that declares anything needs a BoxCollider of
|
|
55
|
+
# its own.
|
|
56
|
+
#
|
|
57
|
+
# The two are not alternatives. `blocked_by` and `on_hit` answer different questions —
|
|
58
|
+
# what may I pass through, and what am I touching — and a flush-blocked pair does not
|
|
59
|
+
# overlap, so a mover that must both stop and react needs both.
|
|
60
|
+
#
|
|
61
|
+
# A declaration this scene cannot honour raises at on_attach rather than quietly
|
|
62
|
+
# falling back to free movement — a mover passing through walls looks like a collision
|
|
63
|
+
# bug, and the cause would be a scene three files away that never mounted the system.
|
|
64
|
+
#
|
|
65
|
+
# ## A blocked step slides
|
|
66
|
+
#
|
|
67
|
+
# Where a step lands is decided by Engine::CollisionSystem, which resolves X and then Y,
|
|
68
|
+
# so a diagonal push into a wall keeps the component that is still free. That is the
|
|
69
|
+
# right feel for a character and the wrong one for a bullet — and a bullet does not
|
|
70
|
+
# need a different resolver for it, it needs to react: `on_blocked` fires on the step
|
|
71
|
+
# it hits, and a bullet that queue-frees itself there is gone before it slides anywhere.
|
|
72
|
+
# One that bounces reads which axis was stopped and turns that half of its velocity:
|
|
73
|
+
#
|
|
74
|
+
# velocity = Velocity.new(vx: 120, vy: 80, blocked_by: %i[wall bounds])
|
|
75
|
+
# velocity.on_blocked do |_by, axis|
|
|
76
|
+
# velocity.vx = -velocity.vx unless axis == :y
|
|
77
|
+
# velocity.vy = -velocity.vy unless axis == :x
|
|
78
|
+
# end
|
|
79
|
+
#
|
|
80
|
+
# ## The shape has one owner, and it is not this
|
|
81
|
+
#
|
|
82
|
+
# A blocked step is resolved against the node's **collider** box, read from the
|
|
83
|
+
# sibling BoxCollider (FeetCollider is the one a walking character wants). The box is
|
|
84
|
+
# therefore given once, to the component that *is* a shape, and the same rectangle both
|
|
85
|
+
# stops the step and reports contacts — there is nothing to hand from one component to
|
|
86
|
+
# the other and nothing to keep in sync.
|
|
87
|
+
class Mover < Engine::Component
|
|
88
|
+
# The two edges of being stopped: on_blocked on the step this mover starts being
|
|
89
|
+
# stopped by something, on_unblocked on the step it stops. Each fires once per
|
|
90
|
+
# blocker, so a handler may spend a life or play a sound — the spiky ball that
|
|
91
|
+
# both stops the player and hurts them is the two signals plus an on_hit.
|
|
92
|
+
#
|
|
93
|
+
# The listener gets whatever stopped the step and reads its #layer and #node, so
|
|
94
|
+
# one handler covers every kind: a collider answers its own layer and its owning
|
|
95
|
+
# node, the map's solid tiles answer :tiles and nil (Engine::TileBlockers::TILES),
|
|
96
|
+
# and the world's edge answers :bounds and nil.
|
|
97
|
+
#
|
|
98
|
+
# on_blocked also says which axis of the step it stopped — :x, :y, or :both when one
|
|
99
|
+
# blocker stopped the two at once — which is what a bounce branches on. A listener
|
|
100
|
+
# that names only the blocker, `{ |by| ... }`, never sees it: a block drops the
|
|
101
|
+
# arguments it does not name.
|
|
102
|
+
#
|
|
103
|
+
# on_unblocked has no axis, because what ends is a blocker stopping this mover, not
|
|
104
|
+
# an axis. A blocker that stopped x on one step and y on the next was in the way the
|
|
105
|
+
# whole time, and ends once.
|
|
106
|
+
signal :on_blocked, Engine::Signal.define(:by, :axis)
|
|
107
|
+
signal :on_unblocked, Engine::Signal.define(:by)
|
|
108
|
+
|
|
109
|
+
TILES = :tiles
|
|
110
|
+
BOUNDS = :bounds
|
|
111
|
+
RESERVED = [TILES, BOUNDS].freeze
|
|
112
|
+
|
|
113
|
+
def initialize(blocked_by: [])
|
|
114
|
+
super()
|
|
115
|
+
@blocked_by = Array(blocked_by)
|
|
116
|
+
@collider = nil
|
|
117
|
+
@collision = nil
|
|
118
|
+
@last_move_blocked = false
|
|
119
|
+
@stopped_by = Engine::ContactSet.new
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Resolve each declared blocker and build the resolver that runs them, once the node
|
|
123
|
+
# is in the tree and both the scene's systems and this node's other components are
|
|
124
|
+
# reachable.
|
|
125
|
+
#
|
|
126
|
+
# **The resolver is the mover's own**, rather than something borrowed off the scene.
|
|
127
|
+
# It has to be: a source over other colliders holds this mover's collider and this
|
|
128
|
+
# mover's layer list, so two movers declaring different `blocked_by` cannot share
|
|
129
|
+
# one — and a scene may mount a CollisionWorld with no TileWorld at all, so there is
|
|
130
|
+
# not always a scene-level resolver to borrow in the first place. What is shared is
|
|
131
|
+
# what can be: the TileWorld's own source is borrowed, not rebuilt.
|
|
132
|
+
#
|
|
133
|
+
# Nothing here runs on a frame. The list is built once, and a step only walks it.
|
|
134
|
+
#
|
|
135
|
+
# A subclass that needs its own attach work calls `super` first — PathFollow does,
|
|
136
|
+
# to place its node before walking.
|
|
137
|
+
def on_attach
|
|
138
|
+
@stopped_by.reset
|
|
139
|
+
return if @blocked_by.empty?
|
|
140
|
+
|
|
141
|
+
WorldBounds.one_response!(node) if blocked_by?(BOUNDS)
|
|
142
|
+
|
|
143
|
+
@collider = require_sibling(BoxCollider)
|
|
144
|
+
@collision = Engine::CollisionSystem.new(blockers: resolve_blockers)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Take this step, then report what stopped being in the way.
|
|
148
|
+
#
|
|
149
|
+
# **The set advances once per update, not once per apply_move**, and that is what
|
|
150
|
+
# makes standing still an unblocking: a mover that stops pushing into something
|
|
151
|
+
# records nothing this step, so what it was pressing against ends and on_unblocked
|
|
152
|
+
# fires. It also keeps the bookkeeping where a subclass cannot lose it — a mover
|
|
153
|
+
# that resolves a step in several moves, or overrides apply_move, still opens the
|
|
154
|
+
# step once and still reports its edges.
|
|
155
|
+
def update(dt)
|
|
156
|
+
return take_step(dt) unless @collision
|
|
157
|
+
|
|
158
|
+
@stopped_by.begin_frame
|
|
159
|
+
take_step(dt)
|
|
160
|
+
@stopped_by.each_ended { on_unblocked_signal.emit(it) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Which way this mover's step is going, each axis in -1..1, and 0, 0 when it is not
|
|
164
|
+
# trying to move. A facing rather than a velocity: a mover pressed into a wall still
|
|
165
|
+
# heads into it, so an AnimatedSprite keeps walking against the wall rather than
|
|
166
|
+
# standing. Each subclass answers from what its step is computed out of.
|
|
167
|
+
def heading_x = 0.0
|
|
168
|
+
def heading_y = 0.0
|
|
169
|
+
|
|
170
|
+
# Whether `name` is one of the things this mover declared it may be stopped by.
|
|
171
|
+
def blocked_by?(name) = @blocked_by.include?(name)
|
|
172
|
+
|
|
173
|
+
# Where a step lands. Public, and kept separate from `take_step`, so a mover that
|
|
174
|
+
# resolves a step some other way — a platformer's CharacterBody, with gravity and a
|
|
175
|
+
# jump — inherits everything around it rather than restating it.
|
|
176
|
+
#
|
|
177
|
+
# The branch is on what was declared rather than on a subclass: an unblocked mover
|
|
178
|
+
# writes straight to the node, and a blocked one hands *itself* to its resolver as
|
|
179
|
+
# the actor being moved (see the adapter below).
|
|
180
|
+
def apply_move(dx, dy)
|
|
181
|
+
unless @collision
|
|
182
|
+
node.x += dx
|
|
183
|
+
node.y += dy
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
@collision.move(self, dx, dy)
|
|
188
|
+
blocked_x = @collision.blocked_x
|
|
189
|
+
blocked_y = @collision.blocked_y
|
|
190
|
+
@last_move_blocked = !(blocked_x.nil? && blocked_y.nil?)
|
|
191
|
+
if blocked_x.equal?(blocked_y)
|
|
192
|
+
record_blocker(blocked_x, :both)
|
|
193
|
+
else
|
|
194
|
+
record_blocker(blocked_x, :x)
|
|
195
|
+
record_blocker(blocked_y, :y)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# The actor adapter CollisionSystem#move drives: it reads x/y/collision_box, works
|
|
200
|
+
# out where the step lands, and writes the resolved position back. The box is the
|
|
201
|
+
# sibling collider's — the node's one shape, so retuning `collider.box` retunes
|
|
202
|
+
# what a step collides with.
|
|
203
|
+
#
|
|
204
|
+
# ## The adapter is in world space
|
|
205
|
+
#
|
|
206
|
+
# x and y are the node's **world** position, because that is the space everything
|
|
207
|
+
# else about collision is already in: the tile grid is a world-coordinate grid, and
|
|
208
|
+
# BoxCollider#aabb_x reports `node.world_x + box.offset_x`. A mover under an offset
|
|
209
|
+
# ancestor that reported its local position would resolve against a map shifted by
|
|
210
|
+
# the ancestor, and would be compared against other colliders in a different frame
|
|
211
|
+
# entirely.
|
|
212
|
+
#
|
|
213
|
+
# Writing goes back through Node2D#world_x= / #world_y=, which turn the resolved world
|
|
214
|
+
# position into the local one the node actually lives in.
|
|
215
|
+
#
|
|
216
|
+
# A mover under a rotated ancestor is still outside what an axis-aligned box supports
|
|
217
|
+
# (docs/api/components.md: a thing that spins wants a circle) — the position it lands
|
|
218
|
+
# at is exact, but the box it was resolved with does not turn with the frame.
|
|
219
|
+
def collision_box = @collider.box
|
|
220
|
+
def x = node.world_x
|
|
221
|
+
def y = node.world_y
|
|
222
|
+
|
|
223
|
+
def x=(value)
|
|
224
|
+
node.world_x = value
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def y=(value)
|
|
228
|
+
node.world_y = value
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
def take_step(_dt) = nil
|
|
234
|
+
|
|
235
|
+
def blocking? = !@collision.nil?
|
|
236
|
+
|
|
237
|
+
def last_move_blocked? = @last_move_blocked
|
|
238
|
+
|
|
239
|
+
def record_blocker(by, axis)
|
|
240
|
+
return if by.nil? || @stopped_by.touching?(by)
|
|
241
|
+
|
|
242
|
+
started = @stopped_by.started?(by)
|
|
243
|
+
@stopped_by.add(by)
|
|
244
|
+
on_blocked_signal.emit(by:, axis:) if started
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def resolve_blockers
|
|
248
|
+
sources = []
|
|
249
|
+
sources << tile_blockers if @blocked_by.include?(TILES)
|
|
250
|
+
sources << bounds_blockers if @blocked_by.include?(BOUNDS)
|
|
251
|
+
layers = @blocked_by.reject { RESERVED.include?(it) }
|
|
252
|
+
sources << actor_blockers(layers) unless layers.empty?
|
|
253
|
+
sources
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def tile_blockers
|
|
257
|
+
world = node.system(TileWorld) ||
|
|
258
|
+
raise("#{mover_name} is blocked_by :tiles, and the scene has no TileWorld " \
|
|
259
|
+
'system to resolve a step against. Mount one, or drop blocked_by for ' \
|
|
260
|
+
'a mover with nothing to collide with.')
|
|
261
|
+
world.blockers
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def bounds_blockers
|
|
265
|
+
bounds = node.system(WorldBounds) ||
|
|
266
|
+
raise("#{mover_name} is blocked_by :bounds, and the scene has no world " \
|
|
267
|
+
'bounds to stop at. Mount a World (or a TileWorld, which is one), or ' \
|
|
268
|
+
'drop :bounds for a mover that may leave the world.')
|
|
269
|
+
Engine::BoundsBlockers.new(bounds: bounds)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def actor_blockers(layers)
|
|
273
|
+
world = node.system(CollisionWorld) ||
|
|
274
|
+
raise("#{mover_name} is blocked_by #{layers.map(&:inspect).join(', ')}, which " \
|
|
275
|
+
'names collider layers, and the scene has no CollisionWorld system to ' \
|
|
276
|
+
'find them in. Mount one, or drop those names for a mover that only ' \
|
|
277
|
+
'the map stops.')
|
|
278
|
+
Engine::ActorBlockers.new(world: world, owner: @collider, layers: layers)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def mover_name = self.class.name&.split('::')&.last || self.class.inspect
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# A PathFollow that makes its own paths: `go_to(world_x, world_y)` plans a route
|
|
7
|
+
# over the scene's tile map to the cell containing that point, and walks it.
|
|
8
|
+
#
|
|
9
|
+
# Planning is three steps. The scene's TileWorld#nav_grid finds the cheapest route
|
|
10
|
+
# of cells; the route is **string-pulled** into as few straight segments as the
|
|
11
|
+
# node's own collider can travel; and those corners become the Engine::Path this
|
|
12
|
+
# component walks, exactly as a PathFollow walks one it was handed.
|
|
13
|
+
#
|
|
14
|
+
# ## The route is checked against what will stop the walk
|
|
15
|
+
#
|
|
16
|
+
# A segment is kept only if the map's own blocker source — TileWorld#blockers, the
|
|
17
|
+
# object a mover declaring `:tiles` resolves its steps against — lets the collider
|
|
18
|
+
# box travel it. A test on cells alone would keep a diagonal past a solid tile's
|
|
19
|
+
# corner that a point clears and a 12x6 feet box clips, and a PathFollow does not
|
|
20
|
+
# slide: the walker would stand at that corner.
|
|
21
|
+
#
|
|
22
|
+
# The question is TileBlockers#travel?, which holds for a walker stepping under a
|
|
23
|
+
# quarter tile at a time — 240 px/s at 60 ticks a second on 16 px tiles.
|
|
24
|
+
#
|
|
25
|
+
# Smoothing is greedy: from each corner it extends the segment cell by cell until the
|
|
26
|
+
# next cell would not be clear, and turns there. The next cell after a corner is taken
|
|
27
|
+
# without asking, which is sound only for a collider **no larger than a tile** on
|
|
28
|
+
# either axis — so `go_to` refuses a larger one rather than plan a route it may stall on.
|
|
29
|
+
#
|
|
30
|
+
# So what is walked is measured from the **anchor** — the centre of the sibling
|
|
31
|
+
# BoxCollider's box, or the node's origin on a node with none — and a route ends
|
|
32
|
+
# with the anchor on the centre of the target cell.
|
|
33
|
+
#
|
|
34
|
+
# ## What it does not do
|
|
35
|
+
#
|
|
36
|
+
# It does not replan. A route is planned against the map, and the map is all it
|
|
37
|
+
# knows: a walker declaring other colliders in `blocked_by` waits behind one on its
|
|
38
|
+
# route, the way any PathFollow waits, and a game that wants it to go round calls
|
|
39
|
+
# `go_to` again.
|
|
40
|
+
#
|
|
41
|
+
# The route is planned in world space and walked in the node's parent's space, which
|
|
42
|
+
# agree under an unrotated ancestor chain — the same limit a blocked Mover has.
|
|
43
|
+
class Navigator < PathFollow
|
|
44
|
+
# The cells of the last route `go_to` planned, start to target, before smoothing —
|
|
45
|
+
# `[[col, row], ...]`, or nil before the first one. For drawing; `path` is what is
|
|
46
|
+
# walked.
|
|
47
|
+
attr_reader :cells
|
|
48
|
+
|
|
49
|
+
def initialize(speed:, blocked_by: [])
|
|
50
|
+
super
|
|
51
|
+
@cells = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_attach
|
|
55
|
+
super
|
|
56
|
+
@world = node.system(TileWorld) ||
|
|
57
|
+
raise("#{mover_name} plans routes over the scene's TileWorld, and the scene " \
|
|
58
|
+
'has none. Mount one, or use a PathFollow and hand it a path.')
|
|
59
|
+
@anchor = node.get_component(BoxCollider)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Plan from where the node stands to the cell containing (world_x, world_y), and
|
|
63
|
+
# start walking it from the node's current position — so a walker already on a
|
|
64
|
+
# route turns onto the new one without a jump. Returns true.
|
|
65
|
+
#
|
|
66
|
+
# Returns false, and leaves whatever the node was doing alone, when there is no
|
|
67
|
+
# route: the target is solid, outside the map, or cut off from the node.
|
|
68
|
+
#
|
|
69
|
+
# Raises ArgumentError when the node's collider is wider or taller than a tile:
|
|
70
|
+
# pathfinding for such a collider is not supported.
|
|
71
|
+
#
|
|
72
|
+
# rubocop:disable Naming/PredicateMethod -- a command that reports whether it could be
|
|
73
|
+
# carried out, not a question; `go_to?` would read as "may I go there?".
|
|
74
|
+
def go_to(world_x, world_y)
|
|
75
|
+
unless @world
|
|
76
|
+
raise "#{mover_name}#go_to plans over the scene's TileWorld, so the node has to be " \
|
|
77
|
+
'in the tree first.'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
measure_anchor
|
|
81
|
+
refuse_a_box_larger_than_a_tile
|
|
82
|
+
cells = @world.nav_grid.find(*cell_at(@anchor_x, @anchor_y), *cell_at(world_x, world_y))
|
|
83
|
+
return false unless cells
|
|
84
|
+
|
|
85
|
+
@cells = cells
|
|
86
|
+
follow(Engine::Path.new(waypoints(corners(cells))))
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
# rubocop:enable Naming/PredicateMethod
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def measure_anchor
|
|
94
|
+
box = @anchor&.box
|
|
95
|
+
@box_w = box ? box.width : 0
|
|
96
|
+
@box_h = box ? box.height : 0
|
|
97
|
+
@anchor_x = node.world_x + (box ? box.offset_x + (@box_w / 2.0) : 0.0)
|
|
98
|
+
@anchor_y = node.world_y + (box ? box.offset_y + (@box_h / 2.0) : 0.0)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def refuse_a_box_larger_than_a_tile
|
|
102
|
+
return if @box_w <= @world.tile_width && @box_h <= @world.tile_height
|
|
103
|
+
|
|
104
|
+
raise ArgumentError, "#{mover_name}#go_to plans for a collider no larger than a tile, and this " \
|
|
105
|
+
"node's is #{@box_w}x#{@box_h} over #{@world.tile_width}x" \
|
|
106
|
+
"#{@world.tile_height} tiles. Pathfinding for a larger collider is not supported."
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def cell_at(x, y) = [(x / @world.tile_width).floor, (y / @world.tile_height).floor]
|
|
110
|
+
|
|
111
|
+
def centre_x(cell) = (cell[0] + 0.5) * @world.tile_width
|
|
112
|
+
def centre_y(cell) = (cell[1] + 0.5) * @world.tile_height
|
|
113
|
+
|
|
114
|
+
def corners(cells)
|
|
115
|
+
corners = [[@anchor_x, @anchor_y]]
|
|
116
|
+
turn = -1
|
|
117
|
+
while turn < cells.length - 1
|
|
118
|
+
turn = furthest_clear(*corners.last, cells, turn + 1)
|
|
119
|
+
corners << [centre_x(cells[turn]), centre_y(cells[turn])]
|
|
120
|
+
end
|
|
121
|
+
corners
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def furthest_clear(x, y, cells, index)
|
|
125
|
+
index += 1 while index < cells.length - 1 &&
|
|
126
|
+
box_travels?(x, y, centre_x(cells[index + 1]), centre_y(cells[index + 1]))
|
|
127
|
+
index
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def box_travels?(from_x, from_y, to_x, to_y)
|
|
131
|
+
@world.blockers.travel?(from_x - (@box_w / 2.0), from_y - (@box_h / 2.0), @box_w, @box_h,
|
|
132
|
+
to_x - from_x, to_y - from_y)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def waypoints(corners)
|
|
136
|
+
shift_x = node.x - @anchor_x
|
|
137
|
+
shift_y = node.y - @anchor_y
|
|
138
|
+
points = corners.map { |(x, y)| [x + shift_x, y + shift_y] }
|
|
139
|
+
points[0] = [node.x, node.y]
|
|
140
|
+
points
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|