rgame 0.4.0-x86_64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- 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/lib/rgame/boot.rb +3 -0
- 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 +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- 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 +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- 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 +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- 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 +290 -0
- 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/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- 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 +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- 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 +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- metadata +309 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# Flag a String literal drawn or measured as a label: the first argument of
|
|
7
|
+
# `text` or `text_width`, on any receiver.
|
|
8
|
+
#
|
|
9
|
+
# Text a player reads belongs in a translation table, where `RGame::Game`
|
|
10
|
+
# loads it and a language switch reaches it. A literal at the call site is
|
|
11
|
+
# the one place no table can: it shows the same words in every language, and
|
|
12
|
+
# it is the form a reader copies out of an example. Build an `Engine::Text`
|
|
13
|
+
# from a key once, off the per-frame path, and pass it as it is.
|
|
14
|
+
#
|
|
15
|
+
# This sees the call site only. A constant or ivar holding a String passes,
|
|
16
|
+
# so a table of Strings chosen by state is not caught; whether a constant
|
|
17
|
+
# holds words is not a question syntax can answer.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# # bad
|
|
21
|
+
# renderer.text('Press Space to jump', 12, 12)
|
|
22
|
+
# renderer.text("Lives: #{lives}", 12, 34)
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# @help = Engine::Text.new('help.jump') # in initialize
|
|
26
|
+
# renderer.text(@help, 12, 12)
|
|
27
|
+
class NoLiteralText < RuboCop::Cop::Base
|
|
28
|
+
MSG = 'Draw an Engine::Text built from a key, not a String literal: ' \
|
|
29
|
+
'text a player reads belongs in a translation table.'
|
|
30
|
+
|
|
31
|
+
RESTRICT_ON_SEND = %i[text text_width].freeze
|
|
32
|
+
|
|
33
|
+
def on_send(node)
|
|
34
|
+
label = node.first_argument
|
|
35
|
+
add_offense(label) if label&.type?(:str, :dstr)
|
|
36
|
+
end
|
|
37
|
+
alias on_csend on_send
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'hot_path'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Game
|
|
8
|
+
# Flag fresh Array / Range literals that are needless per-frame allocations.
|
|
9
|
+
#
|
|
10
|
+
# Two triggers:
|
|
11
|
+
# * Anywhere — a literal used as a method-call *receiver*: `[a, b].sum`,
|
|
12
|
+
# `(a..b).any? { ... }`. Each call allocates the collection just to reduce or
|
|
13
|
+
# iterate it; compare directly or loop with an index instead.
|
|
14
|
+
# * Inside a per-frame method (a lifecycle hook or a `# hot-path`-tagged helper) —
|
|
15
|
+
# *any* Array/Range literal, e.g. returning `[x, y, w, h]` for the caller to
|
|
16
|
+
# decompose. Expose the parts separately (see `Engine::AnimationSet` row/col/flip_x).
|
|
17
|
+
#
|
|
18
|
+
# Allowed: an empty `[]` (idiomatic mutable-state seed), a frozen `[...].freeze`
|
|
19
|
+
# (allocated once), a parallel-assignment RHS (`a, b = c, d`, which the VM does
|
|
20
|
+
# not allocate), and the four sends the VM rewrites to `opt_newarray_send`.
|
|
21
|
+
#
|
|
22
|
+
# That last one is not a concession, it is a correction. `[a, b].min` compiles to
|
|
23
|
+
# a single `opt_newarray_send` instruction that reads the operands off the stack
|
|
24
|
+
# and never builds an Array — measured at 0 allocations over 200,000 calls, and
|
|
25
|
+
# visible in `RubyVM::InstructionSequence.compile("[a, b].min").disasm`. Flagging
|
|
26
|
+
# it cost real clarity: `Style/MinMaxComparison` asks for exactly this form, so
|
|
27
|
+
# code was being written around a conflict that does not exist.
|
|
28
|
+
#
|
|
29
|
+
# The optimisation is narrow, which is why OPTIMISED_SENDS is a list rather than a
|
|
30
|
+
# guess. `sum`, `first`, `last` and everything else still emit a plain `newarray`,
|
|
31
|
+
# and even `min` falls back to one the moment it is given a block or an argument
|
|
32
|
+
# (`[a, b].min { ... }`, `[a, b].min(1)`). Ranges never had an Array to build.
|
|
33
|
+
class NoNeedlessAllocation < RuboCop::Cop::Base
|
|
34
|
+
include HotPath
|
|
35
|
+
|
|
36
|
+
MSG_RECEIVER = 'Needless %{kind} allocation: this literal is built every call ' \
|
|
37
|
+
'just to call `%{method}` on it — rewrite without the literal.'
|
|
38
|
+
|
|
39
|
+
OPTIMISED_SENDS = { min: 0, max: 0, hash: 0, include?: 1 }.freeze
|
|
40
|
+
MSG_HOT_PATH = 'Needless %{kind} allocation in a per-frame method: this literal ' \
|
|
41
|
+
'is built every frame — build it once or expose the parts directly.'
|
|
42
|
+
|
|
43
|
+
def on_array(node)
|
|
44
|
+
check(node, 'array')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_irange(node)
|
|
48
|
+
check(node, 'range')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_erange(node)
|
|
52
|
+
check(node, 'range')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def check(node, kind)
|
|
58
|
+
return if allowed?(node)
|
|
59
|
+
|
|
60
|
+
send = receiving_send(node)
|
|
61
|
+
if send
|
|
62
|
+
add_offense(node, message: format(MSG_RECEIVER, kind: kind, method: send.method_name))
|
|
63
|
+
elsif in_hot_path?(node)
|
|
64
|
+
add_offense(node, message: format(MSG_HOT_PATH, kind: kind))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def allowed?(node)
|
|
69
|
+
return true if node.array_type? && node.children.empty?
|
|
70
|
+
return true if frozen?(node)
|
|
71
|
+
return true if node.parent&.masgn_type?
|
|
72
|
+
return true if optimised_send?(node)
|
|
73
|
+
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def optimised_send?(node)
|
|
78
|
+
return false unless node.array_type?
|
|
79
|
+
return false if node.children.any?(&:splat_type?)
|
|
80
|
+
|
|
81
|
+
send = receiving_send(node)
|
|
82
|
+
return false if send.nil? || send.block_node
|
|
83
|
+
|
|
84
|
+
arity = OPTIMISED_SENDS[send.method_name]
|
|
85
|
+
!arity.nil? && send.arguments.size == arity
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def frozen?(node)
|
|
89
|
+
send = receiving_send(node)
|
|
90
|
+
send&.method?(:freeze)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def receiving_send(node)
|
|
94
|
+
receiver = node
|
|
95
|
+
parent = node.parent
|
|
96
|
+
if parent&.begin_type? && parent.children.one?
|
|
97
|
+
receiver = parent
|
|
98
|
+
parent = parent.parent
|
|
99
|
+
end
|
|
100
|
+
return unless parent&.send_type? && parent.receiver == receiver
|
|
101
|
+
|
|
102
|
+
parent
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def in_hot_path?(node)
|
|
106
|
+
enclosing = node.each_ancestor(:def).first
|
|
107
|
+
enclosing && hot_path_def?(enclosing)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# The Game/ cops' defaults, for any project that loads the plugin. Each cop's
|
|
2
|
+
# class comment, under cop/game/, says what it catches and why.
|
|
3
|
+
#
|
|
4
|
+
# Specs are test code: their draw stubs and fixture arrays are not a per-frame
|
|
5
|
+
# path the game ships, so the two allocation guards skip them. The draw-space
|
|
6
|
+
# and translation rules still apply there, because a spec that draws in the
|
|
7
|
+
# wrong place describes a game that does too.
|
|
8
|
+
#
|
|
9
|
+
# The two layer cops are off by default. Each needs to be told which files form
|
|
10
|
+
# a layer, and that is a fact about a project's layout rather than about rgame.
|
|
11
|
+
# A project from `rgame new` enables NoCoreInEngineLayer for its nodes/ and spec/.
|
|
12
|
+
|
|
13
|
+
Game/DrawInLocalSpace:
|
|
14
|
+
Description: A node's draw methods must not read its own position, which is already applied.
|
|
15
|
+
Enabled: true
|
|
16
|
+
|
|
17
|
+
Game/NoInterpolationInHotPath:
|
|
18
|
+
Description: No string interpolation in a per-frame method.
|
|
19
|
+
Enabled: true
|
|
20
|
+
Exclude:
|
|
21
|
+
- '**/spec/**/*'
|
|
22
|
+
|
|
23
|
+
Game/NoNeedlessAllocation:
|
|
24
|
+
Description: No throwaway Array or Range literal on a per-frame path.
|
|
25
|
+
Enabled: true
|
|
26
|
+
Exclude:
|
|
27
|
+
- '**/spec/**/*'
|
|
28
|
+
|
|
29
|
+
Game/NoLiteralText:
|
|
30
|
+
Description: Draw text from a translation key, not a String literal.
|
|
31
|
+
Enabled: true
|
|
32
|
+
|
|
33
|
+
Game/NoCoreInEngineLayer:
|
|
34
|
+
Description: Headless code must not name or require RGame::Core.
|
|
35
|
+
Enabled: false
|
|
36
|
+
|
|
37
|
+
Game/NoEngineInCoreLayer:
|
|
38
|
+
Description: RGame::Core must not name or require the engine layer.
|
|
39
|
+
Enabled: false
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../version'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Game
|
|
7
|
+
# Hands RuboCop the `Game/` cops' default configuration, in `default.yml`
|
|
8
|
+
# beside this file.
|
|
9
|
+
#
|
|
10
|
+
# A project loads it by path and class, from `.rubocop.yml`:
|
|
11
|
+
#
|
|
12
|
+
# plugins:
|
|
13
|
+
# - rgame/rubocop:
|
|
14
|
+
# plugin_class_name: RuboCop::Game::Plugin
|
|
15
|
+
#
|
|
16
|
+
# The bare gem name would make RuboCop `require 'rgame'` and load the whole
|
|
17
|
+
# engine, compiled extension included, just to lint. `lint_roller` comes
|
|
18
|
+
# with every RuboCop that supports plugins, so the gem needs no dependency.
|
|
19
|
+
#
|
|
20
|
+
# It lives under `RuboCop` rather than `RGame`, like every other RuboCop
|
|
21
|
+
# extension, because RuboCop's process is the only one that loads it.
|
|
22
|
+
class Plugin < LintRoller::Plugin
|
|
23
|
+
def about
|
|
24
|
+
LintRoller::About.new(
|
|
25
|
+
name: 'rgame',
|
|
26
|
+
version: RGame::VERSION,
|
|
27
|
+
homepage: 'https://github.com/psuessenb/rgame',
|
|
28
|
+
description: 'Per-frame allocation, draw-space, layering and translation rules for rgame games.'
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def supported?(context)
|
|
33
|
+
context.engine == :rubocop
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def rules(_context)
|
|
37
|
+
LintRoller::Rules.new(
|
|
38
|
+
type: :path,
|
|
39
|
+
config_format: :rubocop,
|
|
40
|
+
value: File.join(__dir__, 'default.yml')
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lint_roller'
|
|
4
|
+
|
|
5
|
+
require_relative 'rubocop/plugin'
|
|
6
|
+
require_relative 'rubocop/cop/game/draw_in_local_space'
|
|
7
|
+
require_relative 'rubocop/cop/game/no_core_in_engine_layer'
|
|
8
|
+
require_relative 'rubocop/cop/game/no_engine_in_core_layer'
|
|
9
|
+
require_relative 'rubocop/cop/game/no_interpolation_in_hot_path'
|
|
10
|
+
require_relative 'rubocop/cop/game/no_literal_text'
|
|
11
|
+
require_relative 'rubocop/cop/game/no_needless_allocation'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame/util_ext'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Util
|
|
7
|
+
class Color
|
|
8
|
+
RED = new(255, 0, 0)
|
|
9
|
+
GREEN = new(0, 255, 0)
|
|
10
|
+
BLUE = new(0, 0, 255)
|
|
11
|
+
YELLOW = new(255, 255, 0)
|
|
12
|
+
CYAN = new(0, 255, 255)
|
|
13
|
+
MAGENTA = new(255, 0, 255)
|
|
14
|
+
ORANGE = new(255, 165, 0)
|
|
15
|
+
PURPLE = new(128, 0, 128)
|
|
16
|
+
BROWN = new(139, 69, 19)
|
|
17
|
+
PINK = new(255, 192, 203)
|
|
18
|
+
GRAY = new(128, 128, 128)
|
|
19
|
+
LIGHT_GRAY = new(192, 192, 192)
|
|
20
|
+
DARK_GRAY = new(64, 64, 64)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Util
|
|
5
|
+
# The vocabulary of physical inputs: which integer means "the left arrow
|
|
6
|
+
# key", "the A button", "the left stick's X axis", "player 2's controller".
|
|
7
|
+
#
|
|
8
|
+
# These are values — plain integers with no window, GPU or OS handle behind
|
|
9
|
+
# them — so they live in Util rather than Core. That is what lets the engine
|
|
10
|
+
# layer and a game's own configuration name a physical input without
|
|
11
|
+
# touching RGame::Core, which they may not do:
|
|
12
|
+
#
|
|
13
|
+
# controls = RGame::Util::Controls
|
|
14
|
+
# map = RGame::Engine::InputMap.default.merge(
|
|
15
|
+
# fire: { buttons: [controls::KEY_SPACE, controls::PAD_A] }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# This module is the **vocabulary** only. What an id *means* to a game — the
|
|
19
|
+
# binding table — is RGame::Engine::InputMap, one per player, because two
|
|
20
|
+
# players share a game's actions but not the buttons that trigger them.
|
|
21
|
+
#
|
|
22
|
+
# The same numbers exist as `#define`s in
|
|
23
|
+
# ext/rgame_core/include/rgame/core.h, because the C engine and the
|
|
24
|
+
# standalone binary need them too and cannot see Ruby. Two definitions means
|
|
25
|
+
# a drift risk, so it is checked: the C side is tied to SDL's own scancodes
|
|
26
|
+
# by _Static_assert at compile time, and spec/rgame/util/controls_spec.rb
|
|
27
|
+
# parses that header and compares every value here against it.
|
|
28
|
+
module Controls
|
|
29
|
+
KEY_A = 4
|
|
30
|
+
KEY_B = 5
|
|
31
|
+
KEY_C = 6
|
|
32
|
+
KEY_D = 7
|
|
33
|
+
KEY_E = 8
|
|
34
|
+
KEY_F = 9
|
|
35
|
+
KEY_G = 10
|
|
36
|
+
KEY_H = 11
|
|
37
|
+
KEY_I = 12
|
|
38
|
+
KEY_J = 13
|
|
39
|
+
KEY_K = 14
|
|
40
|
+
KEY_L = 15
|
|
41
|
+
KEY_M = 16
|
|
42
|
+
KEY_N = 17
|
|
43
|
+
KEY_O = 18
|
|
44
|
+
KEY_P = 19
|
|
45
|
+
KEY_Q = 20
|
|
46
|
+
KEY_R = 21
|
|
47
|
+
KEY_S = 22
|
|
48
|
+
KEY_T = 23
|
|
49
|
+
KEY_U = 24
|
|
50
|
+
KEY_V = 25
|
|
51
|
+
KEY_W = 26
|
|
52
|
+
KEY_X = 27
|
|
53
|
+
KEY_Y = 28
|
|
54
|
+
KEY_Z = 29
|
|
55
|
+
|
|
56
|
+
KEY_1 = 30
|
|
57
|
+
KEY_2 = 31
|
|
58
|
+
KEY_3 = 32
|
|
59
|
+
KEY_4 = 33
|
|
60
|
+
KEY_5 = 34
|
|
61
|
+
KEY_6 = 35
|
|
62
|
+
KEY_7 = 36
|
|
63
|
+
KEY_8 = 37
|
|
64
|
+
KEY_9 = 38
|
|
65
|
+
KEY_0 = 39
|
|
66
|
+
|
|
67
|
+
KEY_RETURN = 40
|
|
68
|
+
KEY_ESCAPE = 41
|
|
69
|
+
KEY_BACKSPACE = 42
|
|
70
|
+
KEY_TAB = 43
|
|
71
|
+
KEY_SPACE = 44
|
|
72
|
+
|
|
73
|
+
KEY_MINUS = 45
|
|
74
|
+
KEY_EQUALS = 46
|
|
75
|
+
KEY_LEFTBRACKET = 47
|
|
76
|
+
KEY_RIGHTBRACKET = 48
|
|
77
|
+
KEY_BACKSLASH = 49
|
|
78
|
+
KEY_SEMICOLON = 51
|
|
79
|
+
KEY_APOSTROPHE = 52
|
|
80
|
+
KEY_GRAVE = 53
|
|
81
|
+
KEY_COMMA = 54
|
|
82
|
+
KEY_PERIOD = 55
|
|
83
|
+
KEY_SLASH = 56
|
|
84
|
+
|
|
85
|
+
KEY_CAPSLOCK = 57
|
|
86
|
+
KEY_F1 = 58
|
|
87
|
+
KEY_F2 = 59
|
|
88
|
+
KEY_F3 = 60
|
|
89
|
+
KEY_F4 = 61
|
|
90
|
+
KEY_F5 = 62
|
|
91
|
+
KEY_F6 = 63
|
|
92
|
+
KEY_F7 = 64
|
|
93
|
+
KEY_F8 = 65
|
|
94
|
+
KEY_F9 = 66
|
|
95
|
+
KEY_F10 = 67
|
|
96
|
+
KEY_F11 = 68
|
|
97
|
+
KEY_F12 = 69
|
|
98
|
+
|
|
99
|
+
KEY_INSERT = 73
|
|
100
|
+
KEY_HOME = 74
|
|
101
|
+
KEY_PAGEUP = 75
|
|
102
|
+
KEY_DELETE = 76
|
|
103
|
+
KEY_END = 77
|
|
104
|
+
KEY_PAGEDOWN = 78
|
|
105
|
+
|
|
106
|
+
KEY_RIGHT = 79
|
|
107
|
+
KEY_LEFT = 80
|
|
108
|
+
KEY_DOWN = 81
|
|
109
|
+
KEY_UP = 82
|
|
110
|
+
|
|
111
|
+
KEY_LCTRL = 224
|
|
112
|
+
KEY_LSHIFT = 225
|
|
113
|
+
KEY_LALT = 226
|
|
114
|
+
KEY_RCTRL = 228
|
|
115
|
+
KEY_RSHIFT = 229
|
|
116
|
+
KEY_RALT = 230
|
|
117
|
+
|
|
118
|
+
BUTTON_GAMEPAD_FIRST = 0x1000
|
|
119
|
+
|
|
120
|
+
PAD_A = 4096
|
|
121
|
+
PAD_B = 4097
|
|
122
|
+
PAD_X = 4098
|
|
123
|
+
PAD_Y = 4099
|
|
124
|
+
PAD_BACK = 4100
|
|
125
|
+
PAD_GUIDE = 4101
|
|
126
|
+
PAD_START = 4102
|
|
127
|
+
PAD_LEFT_STICK = 4103
|
|
128
|
+
PAD_RIGHT_STICK = 4104
|
|
129
|
+
PAD_LEFT_SHOULDER = 4105
|
|
130
|
+
PAD_RIGHT_SHOULDER = 4106
|
|
131
|
+
PAD_DPAD_UP = 4107
|
|
132
|
+
PAD_DPAD_DOWN = 4108
|
|
133
|
+
PAD_DPAD_LEFT = 4109
|
|
134
|
+
PAD_DPAD_RIGHT = 4110
|
|
135
|
+
PAD_MISC1 = 4111
|
|
136
|
+
PAD_PADDLE1 = 4112
|
|
137
|
+
PAD_PADDLE2 = 4113
|
|
138
|
+
PAD_PADDLE3 = 4114
|
|
139
|
+
PAD_PADDLE4 = 4115
|
|
140
|
+
PAD_TOUCHPAD = 4116
|
|
141
|
+
|
|
142
|
+
AXIS_LEFT_X = 0
|
|
143
|
+
AXIS_LEFT_Y = 1
|
|
144
|
+
AXIS_RIGHT_X = 2
|
|
145
|
+
AXIS_RIGHT_Y = 3
|
|
146
|
+
AXIS_TRIGGER_LEFT = 4
|
|
147
|
+
AXIS_TRIGGER_RIGHT = 5
|
|
148
|
+
|
|
149
|
+
KEYBOARD = 0
|
|
150
|
+
GAMEPAD_FIRST = 1
|
|
151
|
+
MAX_GAMEPADS = 4
|
|
152
|
+
|
|
153
|
+
# The device id for a player slot: gamepad(0) is the first controller.
|
|
154
|
+
def self.gamepad(slot) = GAMEPAD_FIRST + slot
|
|
155
|
+
|
|
156
|
+
# Is this *device* a controller rather than the keyboard? What a game asks
|
|
157
|
+
# when the answer changes what it shows the player — the prompt for an
|
|
158
|
+
# action is a key cap or a face button depending on what they last used.
|
|
159
|
+
def self.gamepad?(device) = device >= GAMEPAD_FIRST
|
|
160
|
+
|
|
161
|
+
# Is this *button* one a controller has rather than one a keyboard has?
|
|
162
|
+
# The mirror of the question above, on the other id space, and the pair is
|
|
163
|
+
# what lets a caller keep the two apart: an action bound to both lists its
|
|
164
|
+
# keys and its pad buttons together, and only a device can say which half
|
|
165
|
+
# of that list applies right now.
|
|
166
|
+
def self.pad_button?(id) = id >= BUTTON_GAMEPAD_FIRST
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame/util_ext'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Util
|
|
7
|
+
# Connected regions and A* routes over a SolidGrid, in C. The search reads its grid and
|
|
8
|
+
# never writes it, and keeps it alive; any number of searches may read one grid.
|
|
9
|
+
#
|
|
10
|
+
# The grid is 8-connected with octile costs (1 straight, √2 diagonal), and a diagonal
|
|
11
|
+
# step is allowed only when both orthogonal cells beside it are open, so a route never
|
|
12
|
+
# cuts the corner of a solid cell. Routes are cells, `[[col, row], ...]`, start and goal
|
|
13
|
+
# included — or nil when either end is solid, outside the grid, or in another region.
|
|
14
|
+
#
|
|
15
|
+
# Regions are labelled on the first ask after the grid's #revision moves, so a goal in
|
|
16
|
+
# another region is a lookup rather than a search that exhausts the map, and it stays
|
|
17
|
+
# right when the grid changes. Everything else a search keeps between queries is reused,
|
|
18
|
+
# so a repeated query allocates nothing but its result. One search is therefore not safe
|
|
19
|
+
# to use from two threads at once; two searches over one grid are.
|
|
20
|
+
#
|
|
21
|
+
# grid = RGame::Util::SolidGrid.build(8, 3) { |col, row| col == 3 && row < 2 }
|
|
22
|
+
# search = RGame::Util::RouteSearch.new(grid)
|
|
23
|
+
# search.find(0, 0, 7, 0) # => [[0, 0], [1, 1], [2, 2], [3, 2], [4, 2], [5, 1], [6, 0], [7, 0]]
|
|
24
|
+
class RouteSearch
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RGame
|
|
7
|
+
module Util
|
|
8
|
+
# A game's saved state, as one JSON file.
|
|
9
|
+
#
|
|
10
|
+
# save = SaveFile.new('slot1.json', game: 'sheepdog')
|
|
11
|
+
# save.write(dog: [120, 80], sheep: [[40, 40], [90, 30]])
|
|
12
|
+
# save.read # => { dog: [120, 80], sheep: [...] }
|
|
13
|
+
# save.read.fetch(:dog, nil) # => nil when there is no save yet
|
|
14
|
+
#
|
|
15
|
+
# It holds no open handle — it opens, reads or writes, and closes — which is
|
|
16
|
+
# why this is `Util` rather than `Core` (see CLAUDE.md, "Value objects go in
|
|
17
|
+
# Util"). JSON comes from the standard library, so nothing here adds a
|
|
18
|
+
# runtime dependency.
|
|
19
|
+
#
|
|
20
|
+
# ## Reading never raises
|
|
21
|
+
#
|
|
22
|
+
# `read` answers with the default for a file that is missing, empty,
|
|
23
|
+
# truncated, not JSON, or JSON that is not an object. That is deliberate and
|
|
24
|
+
# it is the whole reason this class exists rather than three lines of
|
|
25
|
+
# `JSON.parse(File.read(...))` at a call site.
|
|
26
|
+
#
|
|
27
|
+
# A save file is the one input a game has that it did not produce this run.
|
|
28
|
+
# It survives crashes, disk-full, a killed process mid-write, an editor, and
|
|
29
|
+
# a copy from someone else's machine. A game that raises on any of those is
|
|
30
|
+
# a game that cannot be started again, and the player's only fix is to find
|
|
31
|
+
# and delete a file they were never told about. Losing a save is bad;
|
|
32
|
+
# refusing to launch is worse.
|
|
33
|
+
#
|
|
34
|
+
# **Writing still raises.** A read failure has a sensible answer — "there is
|
|
35
|
+
# no save" — and a write failure does not: silently discarding the player's
|
|
36
|
+
# progress is the failure that gets noticed hours later. A caller that wants
|
|
37
|
+
# to survive a full disk can rescue; it cannot un-swallow an exception this
|
|
38
|
+
# class never raised.
|
|
39
|
+
#
|
|
40
|
+
# ## Writing is atomic
|
|
41
|
+
#
|
|
42
|
+
# `write` writes a temporary file beside the target and renames it over the
|
|
43
|
+
# top. `File.rename` within one directory is atomic on every platform this
|
|
44
|
+
# engine supports, so a save is either the old one or the new one and never
|
|
45
|
+
# half of each.
|
|
46
|
+
#
|
|
47
|
+
# This matters more than it sounds: the moment a game most wants to save is
|
|
48
|
+
# on the way out, which is also the moment it is most likely to be killed.
|
|
49
|
+
# A plain `File.write` truncates first and fills in after, so a crash in
|
|
50
|
+
# between leaves a zero-byte file where the player's afternoon used to be.
|
|
51
|
+
class SaveFile
|
|
52
|
+
# Where saves go, per platform, following each one's own convention rather
|
|
53
|
+
# than dropping a dotfile in the home directory:
|
|
54
|
+
#
|
|
55
|
+
# Linux $XDG_DATA_HOME/<game>, or ~/.local/share/<game>
|
|
56
|
+
# macOS ~/Library/Application Support/<game>
|
|
57
|
+
# Windows %APPDATA%\<game>
|
|
58
|
+
#
|
|
59
|
+
# `game` names the directory, so two games do not share a save.
|
|
60
|
+
def self.directory(game)
|
|
61
|
+
case RbConfig::CONFIG['host_os']
|
|
62
|
+
when /darwin/ then File.join(Dir.home, 'Library', 'Application Support', game)
|
|
63
|
+
when /mswin|mingw|cygwin/ then File.join(ENV.fetch('APPDATA', Dir.home), game)
|
|
64
|
+
else File.join(ENV.fetch('XDG_DATA_HOME', File.join(Dir.home, '.local', 'share')), game)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
attr_reader :path
|
|
69
|
+
|
|
70
|
+
# `name` is a file name inside the game's save directory; pass `dir:` to
|
|
71
|
+
# put it somewhere else, which is what a spec does.
|
|
72
|
+
def initialize(name, game: 'rgame', dir: nil)
|
|
73
|
+
@path = File.join(dir || SaveFile.directory(game), name)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def exist? = File.exist?(@path)
|
|
77
|
+
|
|
78
|
+
# The saved state, or `default` if there is nothing usable to read. Keys
|
|
79
|
+
# come back as Symbols, so a game writes and reads the same shape.
|
|
80
|
+
def read(default = {})
|
|
81
|
+
parsed = JSON.parse(File.read(@path), symbolize_names: true)
|
|
82
|
+
parsed.is_a?(Hash) ? parsed : default
|
|
83
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR, JSON::ParserError
|
|
84
|
+
default
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Writes `state` as JSON, atomically. Creates the directory if it is not
|
|
88
|
+
# there — a first save on a fresh machine is the normal case, not an error.
|
|
89
|
+
def write(state)
|
|
90
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
91
|
+
temporary = "#{@path}.#{Process.pid}.tmp"
|
|
92
|
+
File.write(temporary, JSON.pretty_generate(state))
|
|
93
|
+
File.rename(temporary, @path)
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Removes the save, and says nothing if there was not one. "Delete my
|
|
98
|
+
# save" and "there was no save" leave the player in the same place.
|
|
99
|
+
def delete
|
|
100
|
+
File.unlink(@path)
|
|
101
|
+
self
|
|
102
|
+
rescue Errno::ENOENT
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame/util_ext'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Util
|
|
7
|
+
# Which cells of a tile grid are solid: one byte a cell, in C. A value with no handle
|
|
8
|
+
# behind it, which is why it lives in Util and why the engine layer may own one.
|
|
9
|
+
#
|
|
10
|
+
# It is the one store a tile world's solidity lives in. The blockers that stop a walker
|
|
11
|
+
# and the RouteSearch that plans one both read it, so they cannot disagree about a wall,
|
|
12
|
+
# and a cell changed with #set_solid is changed for both at once.
|
|
13
|
+
#
|
|
14
|
+
# #revision counts the changes: it moves on a #set_solid that changes a cell and on no
|
|
15
|
+
# other, so anything derived from the cells — a search's region labels — can tell
|
|
16
|
+
# whether it is stale without being told.
|
|
17
|
+
#
|
|
18
|
+
# Coordinates are Integers, and anything else is a TypeError. Outside the grid is open,
|
|
19
|
+
# including an Integer too large to be inside any grid.
|
|
20
|
+
#
|
|
21
|
+
# grid = RGame::Util::SolidGrid.build(8, 3) { |col, _row| col == 3 }
|
|
22
|
+
# grid.solid?(3, 1) # => true
|
|
23
|
+
# grid.solid?(-1, 0) # => false
|
|
24
|
+
# grid.set_solid(3, 1, false)
|
|
25
|
+
# grid.revision # => 1
|
|
26
|
+
class SolidGrid
|
|
27
|
+
# A grid with each cell's solidity read from the block, once per cell, row by row.
|
|
28
|
+
def self.build(width, height)
|
|
29
|
+
grid = new(width, height)
|
|
30
|
+
height.times do |row|
|
|
31
|
+
width.times { |col| grid.set_solid(col, row, true) if yield(col, row) }
|
|
32
|
+
end
|
|
33
|
+
grid
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame/util_ext'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Util
|
|
7
|
+
# An axis-aligned box against the solid tiles of a SolidGrid, in C. It reads its grid and
|
|
8
|
+
# never writes it, and keeps it alive; any number of sweeps may read one grid, so a change
|
|
9
|
+
# made with SolidGrid#set_solid is seen by all of them at once.
|
|
10
|
+
#
|
|
11
|
+
# It answers two questions with one piece of arithmetic, so they cannot disagree:
|
|
12
|
+
#
|
|
13
|
+
# - **#resolve_x / #resolve_y** — where the box's left (top) edge lands moving dx (dy). A
|
|
14
|
+
# step that would enter a solid tile snaps the box flush against that tile's edge. Each
|
|
15
|
+
# axis is resolved on its own. The step is assumed smaller than a tile; a longer one can
|
|
16
|
+
# pass through a tile without seeing it.
|
|
17
|
+
# - **#travel?** — whether the box can move by (dx, dy) without any resolve along the way
|
|
18
|
+
# falling short of where it was heading. The box is swept in overlapping windows half a
|
|
19
|
+
# tile long, a quarter tile apart, each resolved X-then-Y and Y-then-X, which makes the
|
|
20
|
+
# answer sound for a walker that resolves its own steps and steps less than a quarter tile
|
|
21
|
+
# at a time.
|
|
22
|
+
#
|
|
23
|
+
# Boxes are given by their top-left corner and size, in pixels. Outside the grid is open.
|
|
24
|
+
# Results are Floats. A coordinate that is not a number is a TypeError; a non-finite one is
|
|
25
|
+
# a FloatDomainError, except in a resolve with no movement, which returns the box where it
|
|
26
|
+
# is.
|
|
27
|
+
#
|
|
28
|
+
# grid = RGame::Util::SolidGrid.build(20, 15) { |col, _row| col == 5 } # a wall at x 80..96
|
|
29
|
+
# sweep = RGame::Util::TileSweep.new(grid, 16, 16)
|
|
30
|
+
# sweep.resolve_x(58.0, 32.0, 12, 6, 14.0) # => 68.0, flush against the wall
|
|
31
|
+
# sweep.travel?(10.0, 32.0, 12, 6, 50.0, 0) # => true
|
|
32
|
+
# sweep.travel?(10.0, 32.0, 12, 6, 90.0, 0) # => false
|
|
33
|
+
class TileSweep
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|