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
|
@@ -38,9 +38,6 @@ module RGame
|
|
|
38
38
|
@height = height
|
|
39
39
|
@solo_camera = nil
|
|
40
40
|
@pending = nil
|
|
41
|
-
# One View per possible player for the world, one more each for their own
|
|
42
|
-
# screen space, and one for the whole window. Built once and mutated in
|
|
43
|
-
# place. See View: these are reused, never rebuilt.
|
|
44
41
|
@pool = []
|
|
45
42
|
@screen_pool = []
|
|
46
43
|
@views = []
|
|
@@ -114,6 +111,8 @@ module RGame
|
|
|
114
111
|
#
|
|
115
112
|
# Allocation-free once the pool has grown: Layout yields its rects rather
|
|
116
113
|
# than building them, and the Views are mutated in place.
|
|
114
|
+
#
|
|
115
|
+
# @api private
|
|
117
116
|
def refresh
|
|
118
117
|
@screen.set(0, 0, @width, @height)
|
|
119
118
|
@solo_camera ? refresh_solo : refresh_split
|
|
@@ -148,8 +147,6 @@ module RGame
|
|
|
148
147
|
end
|
|
149
148
|
end
|
|
150
149
|
|
|
151
|
-
# `each_active` is a filter, so it cannot be indexed without building an
|
|
152
|
-
# Array. Walking it costs nothing and this runs once per frame.
|
|
153
150
|
def active_at(index)
|
|
154
151
|
found = nil
|
|
155
152
|
i = 0
|
|
@@ -18,10 +18,11 @@ module RGame
|
|
|
18
18
|
#
|
|
19
19
|
# ## The subtree does not know how many times it is drawn
|
|
20
20
|
#
|
|
21
|
-
# Children draw
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# *inside* the world could not
|
|
21
|
+
# Children draw in their own local space and never see a camera. This node
|
|
22
|
+
# supplies the view — one clip and one translate around the whole subtree —
|
|
23
|
+
# so the same world serves one player or four with nothing below it changing,
|
|
24
|
+
# which is exactly what a camera owned by a node *inside* the world could not
|
|
25
|
+
# do, and why cameras belong to players.
|
|
25
26
|
#
|
|
26
27
|
# ## Only `draw` multiplies, and deliberately
|
|
27
28
|
#
|
data/lib/rgame/engine.rb
CHANGED
|
@@ -1,71 +1,76 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# RGame::Engine — the scene graph a game is written against: nodes, components,
|
|
4
|
-
# signals, scenes, collision, tile maps, pathing, pooling.
|
|
5
|
-
#
|
|
6
|
-
# Pure Ruby, and it links nothing. It may hold `RGame::Util` values but may not
|
|
7
|
-
# name `RGame::Core` at all, reaching the platform only through objects handed
|
|
8
|
-
# to it — a `renderer` passed into `draw`, an audio server behind
|
|
9
|
-
# `AudioDirector`. See CLAUDE.md, "The three layers, and who may talk to whom".
|
|
10
|
-
#
|
|
11
|
-
# That is what lets the whole layer be specified with no window, no GPU and no
|
|
12
|
-
# clock: `spec/` drives `update(dt)` directly and can run a simulated hour in
|
|
13
|
-
# milliseconds.
|
|
14
|
-
#
|
|
15
|
-
# Loaded by `require "rgame"` along with Util, since between them they are
|
|
16
|
-
# everything that runs without a window. Requirable on its own too — hence the
|
|
17
|
-
# `util` require below rather than relying on `rgame.rb` to have got there
|
|
18
|
-
# first: `TileMap` holds a `Util::Tensor`, which is exactly the dependency the
|
|
19
|
-
# three-layer rule allows and this file therefore has to declare.
|
|
20
3
|
require_relative 'util'
|
|
21
4
|
|
|
22
5
|
require_relative 'engine/culling'
|
|
23
|
-
require_relative 'engine/matrix'
|
|
24
6
|
require_relative 'engine/signal'
|
|
7
|
+
require_relative 'engine/sealed_privates'
|
|
25
8
|
require_relative 'engine/node2d'
|
|
26
9
|
require_relative 'engine/component'
|
|
27
10
|
require_relative 'engine/animation_set'
|
|
28
11
|
require_relative 'engine/animator'
|
|
29
12
|
require_relative 'engine/camera'
|
|
30
13
|
require_relative 'engine/layout'
|
|
14
|
+
require_relative 'engine/presentation'
|
|
31
15
|
require_relative 'engine/view'
|
|
32
16
|
require_relative 'engine/world_view'
|
|
33
17
|
require_relative 'engine/player_layer'
|
|
34
|
-
require_relative 'engine/ui/
|
|
18
|
+
require_relative 'engine/ui/button'
|
|
19
|
+
require_relative 'engine/ui/nine_slice_style'
|
|
20
|
+
require_relative 'engine/ui/shape_style'
|
|
21
|
+
require_relative 'engine/ui/text_button'
|
|
22
|
+
require_relative 'engine/ui/panel_button'
|
|
23
|
+
require_relative 'engine/ui/option_button'
|
|
24
|
+
require_relative 'engine/ui/icon_button'
|
|
25
|
+
require_relative 'engine/ui/stack'
|
|
26
|
+
require_relative 'engine/ui/column'
|
|
27
|
+
require_relative 'engine/ui/row'
|
|
28
|
+
require_relative 'engine/ui/ring'
|
|
29
|
+
require_relative 'engine/ui/navigation'
|
|
30
|
+
require_relative 'engine/ui/stepping'
|
|
31
|
+
require_relative 'engine/ui/pointing'
|
|
35
32
|
require_relative 'engine/ui/menu'
|
|
33
|
+
require_relative 'engine/ui/panel_menu'
|
|
34
|
+
require_relative 'engine/ui/radial_menu'
|
|
36
35
|
require_relative 'engine/path'
|
|
37
36
|
require_relative 'engine/timer'
|
|
38
37
|
require_relative 'engine/tileset'
|
|
39
38
|
require_relative 'engine/tile_map'
|
|
40
|
-
require_relative 'engine/
|
|
39
|
+
require_relative 'engine/tile_blockers'
|
|
40
|
+
require_relative 'engine/nav_grid'
|
|
41
|
+
require_relative 'engine/actor_blockers'
|
|
42
|
+
require_relative 'engine/bounds_blockers'
|
|
41
43
|
require_relative 'engine/collision_box'
|
|
42
44
|
require_relative 'engine/collision_system'
|
|
43
45
|
require_relative 'engine/circle_collider'
|
|
44
46
|
require_relative 'engine/spatial_hash'
|
|
47
|
+
require_relative 'engine/contact_set'
|
|
45
48
|
require_relative 'engine/pool'
|
|
46
|
-
require_relative 'engine/resettable'
|
|
47
49
|
require_relative 'engine/audio_bus'
|
|
48
50
|
require_relative 'engine/audio_director'
|
|
49
51
|
require_relative 'engine/i18n'
|
|
50
|
-
require_relative 'engine/
|
|
52
|
+
require_relative 'engine/text'
|
|
51
53
|
require_relative 'engine/debug_overlay'
|
|
52
|
-
require_relative 'engine/body'
|
|
53
|
-
require_relative 'engine/actor'
|
|
54
54
|
require_relative 'engine/input/actions'
|
|
55
55
|
require_relative 'engine/input/input_map'
|
|
56
56
|
require_relative 'engine/input/action_mapper'
|
|
57
|
-
require_relative 'engine/input/player_controller'
|
|
58
57
|
require_relative 'engine/player'
|
|
59
58
|
require_relative 'engine/players'
|
|
60
59
|
require_relative 'engine/viewports'
|
|
61
60
|
require_relative 'engine/scene/scene_stack'
|
|
61
|
+
require_relative 'engine/components/identity'
|
|
62
|
+
require_relative 'engine/components/mover'
|
|
62
63
|
require_relative 'engine/components/velocity'
|
|
63
64
|
require_relative 'engine/components/pool'
|
|
64
65
|
require_relative 'engine/components/path_follow'
|
|
66
|
+
require_relative 'engine/components/navigator'
|
|
65
67
|
require_relative 'engine/components/timer'
|
|
68
|
+
require_relative 'engine/components/world'
|
|
66
69
|
require_relative 'engine/components/screen_wrap'
|
|
67
70
|
require_relative 'engine/components/despawn_offscreen'
|
|
68
71
|
require_relative 'engine/components/circle_collider'
|
|
72
|
+
require_relative 'engine/components/box_collider'
|
|
73
|
+
require_relative 'engine/components/feet_collider'
|
|
69
74
|
require_relative 'engine/components/collision_world'
|
|
70
75
|
require_relative 'engine/components/targeting'
|
|
71
76
|
require_relative 'engine/components/sprite'
|
|
@@ -74,6 +79,7 @@ require_relative 'engine/components/action_trigger'
|
|
|
74
79
|
require_relative 'engine/components/tile_world'
|
|
75
80
|
require_relative 'engine/components/character_body'
|
|
76
81
|
require_relative 'engine/components/player_controller'
|
|
82
|
+
require_relative 'engine/components/hop'
|
|
77
83
|
require_relative 'engine/components/wander_controller'
|
|
78
84
|
require_relative 'engine/components/animated_sprite'
|
|
79
85
|
require_relative 'engine/components/camera_follow'
|
data/lib/rgame/game.rb
CHANGED
|
@@ -55,18 +55,45 @@ module RGame
|
|
|
55
55
|
# `Controls.gamepad(slot)` for a controller.
|
|
56
56
|
#
|
|
57
57
|
# `input:` overrides the input backend. It exists so a harness can drive a
|
|
58
|
-
# game from a script instead of from hardware — see
|
|
59
|
-
# and CLAUDE.md's "The
|
|
60
|
-
# is why driving one has to be possible
|
|
61
|
-
# gets the real thing.
|
|
58
|
+
# game from a script instead of from hardware — see
|
|
59
|
+
# tools/drive_test_project.rb, and CLAUDE.md's "The test projects are the
|
|
60
|
+
# acceptance test for wiring", which is why driving one has to be possible
|
|
61
|
+
# at all. A game passes nothing and gets the real thing.
|
|
62
62
|
# `players:` is how many seats the game has, and therefore the most people
|
|
63
63
|
# who can play it. Player 0 starts on `device:`; the rest start empty and
|
|
64
64
|
# are filled when someone uses a controller — see RGame::Engine::Players for
|
|
65
65
|
# why that is a press rather than a plug.
|
|
66
|
+
# `fullscreen:` opens the window fullscreen rather than switching after it is
|
|
67
|
+
# already up, so a game that always runs fullscreen never flashes a windowed
|
|
68
|
+
# frame at startup. `width` and `height` still matter: they are the size the
|
|
69
|
+
# window takes when it leaves fullscreen, whether or not this game offers a
|
|
70
|
+
# way to do that.
|
|
71
|
+
#
|
|
72
|
+
# `scale_mode:` decides what `width` and `height` *mean*, and the default
|
|
73
|
+
# answer is "the resolution the game is designed in". Under `:letterbox` the
|
|
74
|
+
# view a node draws into is always that size, whatever the window is doing,
|
|
75
|
+
# so a layout written against fixed numbers keeps working at any window size
|
|
76
|
+
# and in fullscreen — which is what almost every game wants and what nothing
|
|
77
|
+
# in the engine can supply for it afterwards.
|
|
78
|
+
#
|
|
79
|
+
# `:disabled` is the opt-out, and it is the right answer for something that
|
|
80
|
+
# should genuinely use whatever space it is given: a tool, a HUD-shaped
|
|
81
|
+
# program, an editor. It hands the window straight through as the view, and
|
|
82
|
+
# skips the clip, translate and scale that every other mode pushes. See
|
|
83
|
+
# RGame::Engine::Presentation.
|
|
84
|
+
#
|
|
85
|
+
# `locales:` is the directory the translation tables are in, relative to
|
|
86
|
+
# `media_root` unless absolute. Every `.yml` under it is loaded through the
|
|
87
|
+
# asset manager here, in sorted order, and the language is chosen from the
|
|
88
|
+
# player's OS preferences — so a game writes no i18n setup, and a language
|
|
89
|
+
# the player saved is set after `new` and before `start`. A directory that
|
|
90
|
+
# does not exist loads nothing, and every key shows as itself.
|
|
66
91
|
def initialize(root:, width: WIDTH, height: HEIGHT, caption: 'RGame',
|
|
67
92
|
media_root: 'media', input_map: nil, device: Controls::KEYBOARD,
|
|
68
|
-
players: 1, input: nil
|
|
69
|
-
|
|
93
|
+
players: 1, input: nil, fullscreen: false, scale_mode: :letterbox,
|
|
94
|
+
locales: 'locales')
|
|
95
|
+
super(width: width, height: height, caption: caption, media_root: media_root,
|
|
96
|
+
fullscreen: fullscreen)
|
|
70
97
|
|
|
71
98
|
@root = root
|
|
72
99
|
@renderer = RGame::Core::Renderer.new(self)
|
|
@@ -77,11 +104,16 @@ module RGame
|
|
|
77
104
|
input_map: input_map)
|
|
78
105
|
end
|
|
79
106
|
)
|
|
80
|
-
@
|
|
81
|
-
|
|
82
|
-
@
|
|
107
|
+
@presentation = RGame::Engine::Presentation.new(width: width, height: height,
|
|
108
|
+
mode: scale_mode)
|
|
109
|
+
@presentation.fit(self.width, self.height)
|
|
110
|
+
@viewports = RGame::Engine::Viewports.new(@players, width: @presentation.width,
|
|
111
|
+
height: @presentation.height)
|
|
112
|
+
@debug = RGame::Engine::DebugOverlay.new
|
|
113
|
+
@dirty = true
|
|
83
114
|
|
|
84
115
|
install_asset_loaders
|
|
116
|
+
load_locales(locales)
|
|
85
117
|
end
|
|
86
118
|
|
|
87
119
|
# The player registry, also reachable from any node as
|
|
@@ -105,12 +137,13 @@ module RGame
|
|
|
105
137
|
# anything being threaded through its constructor.
|
|
106
138
|
def start
|
|
107
139
|
@root.context = self
|
|
108
|
-
# Root-scoped systems, mounted before the tree comes alive so that an
|
|
109
|
-
# on_add anywhere in it can already resolve node.system(...) for either.
|
|
110
140
|
@root.add_component(@players)
|
|
111
141
|
@root.add_component(@viewports)
|
|
112
|
-
@
|
|
142
|
+
@audio_director = Engine::AudioDirector.new(audio).subscribe
|
|
143
|
+
@root.enter_tree
|
|
113
144
|
run
|
|
145
|
+
ensure
|
|
146
|
+
@audio_director&.unsubscribe
|
|
114
147
|
end
|
|
115
148
|
|
|
116
149
|
# One fixed simulation tick. `dt` is always the engine's fixed step, so the
|
|
@@ -130,11 +163,9 @@ module RGame
|
|
|
130
163
|
# one `pressed?`, which is what a caller means.
|
|
131
164
|
def update(dt)
|
|
132
165
|
@players.poll(@input)
|
|
133
|
-
# The registry, not one player's snapshot: each node resolves the actions
|
|
134
|
-
# of whoever owns it, and a node that claims nobody gets the primary.
|
|
135
166
|
@root.control(@players)
|
|
136
167
|
@root.update(dt)
|
|
137
|
-
@root.sweep_freed
|
|
168
|
+
@root.sweep_freed
|
|
138
169
|
@dirty = true
|
|
139
170
|
end
|
|
140
171
|
|
|
@@ -152,14 +183,38 @@ module RGame
|
|
|
152
183
|
# rather than a shape the platform imposes. That is also what keeps
|
|
153
184
|
# `node.root` meaning the game's own root: nothing is inserted above it.
|
|
154
185
|
def draw
|
|
155
|
-
@viewports.refresh
|
|
156
|
-
|
|
157
|
-
|
|
186
|
+
@viewports.refresh
|
|
187
|
+
if @presentation.scaled?
|
|
188
|
+
presented { draw_tree }
|
|
189
|
+
else
|
|
190
|
+
draw_tree
|
|
191
|
+
end
|
|
158
192
|
@dirty = false
|
|
159
193
|
end
|
|
160
194
|
|
|
161
|
-
#
|
|
162
|
-
|
|
195
|
+
# How `width` and `height` are mapped onto the window: `:disabled`,
|
|
196
|
+
# `:stretch`, `:letterbox` or `:integer`. See RGame::Engine::Presentation.
|
|
197
|
+
def scale_mode = @presentation.mode
|
|
198
|
+
|
|
199
|
+
# Switchable while the game runs, for a settings screen.
|
|
200
|
+
#
|
|
201
|
+
# The viewports are resized as well as the presentation refitted, because
|
|
202
|
+
# the two modes disagree about what the logical size *is*: leaving `:integer`
|
|
203
|
+
# for `:disabled` turns a fixed 640x480 back into the window's own size, and
|
|
204
|
+
# a viewport still holding the old one would lay out into a corner.
|
|
205
|
+
def scale_mode=(mode)
|
|
206
|
+
@presentation.mode = mode
|
|
207
|
+
@viewports.resize(@presentation.width, @presentation.height)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The window changed size, so the presentation is refitted and every rect and
|
|
211
|
+
# camera clamp follows. Under a scaling mode the logical size does not move,
|
|
212
|
+
# so the viewports are handed the same numbers again and only the transform
|
|
213
|
+
# in `draw` changes — which is the whole point of the mode.
|
|
214
|
+
def resize(width, height)
|
|
215
|
+
@presentation.fit(width, height)
|
|
216
|
+
@viewports.resize(@presentation.width, @presentation.height)
|
|
217
|
+
end
|
|
163
218
|
|
|
164
219
|
# Hot-plug is bookkeeping, not seating: a controller arriving becomes a
|
|
165
220
|
# device the registry watches, and it is someone *using* it that gives it to
|
|
@@ -179,14 +234,31 @@ module RGame
|
|
|
179
234
|
|
|
180
235
|
private
|
|
181
236
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
237
|
+
def draw_tree
|
|
238
|
+
@root.draw(@renderer, @viewports.screen)
|
|
239
|
+
@debug.draw(@renderer, @viewports.screen, fps)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def presented(&)
|
|
243
|
+
@renderer.clipped(@presentation.offset_x, @presentation.offset_y,
|
|
244
|
+
(@presentation.width * @presentation.scale_x).round,
|
|
245
|
+
(@presentation.height * @presentation.scale_y).round) do
|
|
246
|
+
@renderer.translated(@presentation.offset_x, @presentation.offset_y) do
|
|
247
|
+
@renderer.scaled(@presentation.scale_x, @presentation.scale_y, &)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def load_locales(directory)
|
|
253
|
+
assets.glob(File.join(directory, '**', '*.yml')).each { |path| assets.locale(path) }
|
|
254
|
+
RGame::Engine::I18n.locale = RGame::Engine::I18n.choose(RGame::Core.preferred_locales)
|
|
255
|
+
end
|
|
256
|
+
|
|
189
257
|
def install_asset_loaders
|
|
258
|
+
assets.add_loader(:locale) do |path|
|
|
259
|
+
RGame::Engine::I18n.load(File.read(path), source: path)
|
|
260
|
+
end
|
|
261
|
+
|
|
190
262
|
game = self
|
|
191
263
|
assets.add_loader(:tilemap) do |path|
|
|
192
264
|
map, image_path = RGame::Engine::TileMap.load(path)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# A node's draw methods must not read the node's own position — in either
|
|
7
|
+
# space, because both are already applied by the time they run.
|
|
8
|
+
#
|
|
9
|
+
# `Node2D#draw` pushes the node's transform onto the renderer and runs
|
|
10
|
+
# everything below it inside that, so within `on_draw` the origin *is* the
|
|
11
|
+
# node, turned the way the node is turned. Drawing is done at (0, 0), or at
|
|
12
|
+
# an offset from it that means something to the node itself.
|
|
13
|
+
#
|
|
14
|
+
# Both spellings of "where am I" are therefore wrong there, and differ only
|
|
15
|
+
# in how far off they put the result:
|
|
16
|
+
#
|
|
17
|
+
# - `world_x` is where the node sits on the map, which every ancestor's
|
|
18
|
+
# transform — the camera's included — has already contributed to. Passing
|
|
19
|
+
# it applies the whole chain twice.
|
|
20
|
+
# - `x` is the node's offset inside its parent, which its own transform has
|
|
21
|
+
# already applied. Passing it offsets by that much a second time.
|
|
22
|
+
#
|
|
23
|
+
# Both are silent: they draw, they just draw in the wrong place, and only
|
|
24
|
+
# once something is nested under an offset or rotated parent — which is
|
|
25
|
+
# exactly the case a spec at the origin cannot see.
|
|
26
|
+
#
|
|
27
|
+
# A **component** drawing for its node is on this path too, and asks the
|
|
28
|
+
# node by name: `node.world_x` for a cull test is a different object's
|
|
29
|
+
# coordinate and is not flagged. Only a bare, receiverless read of the
|
|
30
|
+
# node's own transform is.
|
|
31
|
+
#
|
|
32
|
+
# `width` and `height` are not positions and are ordinary here — a node
|
|
33
|
+
# drawing its own box says `renderer.rect(0, 0, width, height)`.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# # bad — the traversal has already placed the renderer on this node
|
|
37
|
+
# def on_draw(renderer, _view)
|
|
38
|
+
# renderer.rect(world_x, world_y, width, height)
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # bad — same mistake, a smaller distance
|
|
42
|
+
# def on_draw(renderer, _view)
|
|
43
|
+
# renderer.rect(x, y, width, height)
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# # good — its own origin
|
|
47
|
+
# def on_draw(renderer, _view)
|
|
48
|
+
# renderer.rect(0, 0, width, height)
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # good — a component culling against the camera, in world space, by name
|
|
52
|
+
# def draw(renderer, view)
|
|
53
|
+
# return if culled?(view, node.world_x, node.world_y, node.width, node.height)
|
|
54
|
+
#
|
|
55
|
+
# renderer.image(@id, 0, 0)
|
|
56
|
+
# end
|
|
57
|
+
class DrawInLocalSpace < RuboCop::Cop::Base
|
|
58
|
+
MSG = 'Draw in local space: `%{name}` is already applied by the time ' \
|
|
59
|
+
'`%{method}` runs, so passing it places this %{distance} a second ' \
|
|
60
|
+
'time. Draw at your own origin (0, 0), or an offset from it.'
|
|
61
|
+
|
|
62
|
+
METHODS = %i[draw on_draw _draw_content draw_children].freeze
|
|
63
|
+
|
|
64
|
+
RELATIVE = %i[x y angle].freeze
|
|
65
|
+
WORLD = %i[world_x world_y world_angle].freeze
|
|
66
|
+
IVARS = { :@rel_x => :x, :@rel_y => :y, :@rel_angle => :angle,
|
|
67
|
+
:@world_x => :world_x, :@world_y => :world_y,
|
|
68
|
+
:@world_angle => :world_angle }.freeze
|
|
69
|
+
|
|
70
|
+
def on_def(node)
|
|
71
|
+
return unless METHODS.include?(node.method_name)
|
|
72
|
+
|
|
73
|
+
node.each_descendant(:ivar, :send) do |read|
|
|
74
|
+
name = own_transform_read(read)
|
|
75
|
+
next unless name
|
|
76
|
+
|
|
77
|
+
add_offense(read, message: format(MSG, name: name, method: node.method_name,
|
|
78
|
+
distance: distance_for(name)))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def own_transform_read(node)
|
|
85
|
+
if node.ivar_type?
|
|
86
|
+
IVARS[node.children.first]
|
|
87
|
+
elsif self_read?(node)
|
|
88
|
+
name = node.method_name
|
|
89
|
+
name if RELATIVE.include?(name) || WORLD.include?(name)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self_read?(node)
|
|
94
|
+
node.send_type? && node.receiver.nil? && node.arguments.empty? && !node.block_literal?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def distance_for(name)
|
|
98
|
+
WORLD.include?(name) ? 'on the map' : 'inside its parent'
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# Shared definition of the per-frame "hot path" for the allocation-guard cops.
|
|
7
|
+
#
|
|
8
|
+
# The hot path is the set of methods that run every frame: the engine's six
|
|
9
|
+
# lifecycle hooks, plus any method an author opts into with a `# hot-path` magic
|
|
10
|
+
# comment on the line directly above its `def`. The opt-in covers per-frame
|
|
11
|
+
# *helpers* the lifecycle methods call, where an allocation is just as costly but
|
|
12
|
+
# the method name alone can't reveal it.
|
|
13
|
+
module HotPath
|
|
14
|
+
METHODS = %i[update control draw draw_children _draw_content
|
|
15
|
+
on_update on_draw on_control].freeze
|
|
16
|
+
|
|
17
|
+
# True for a `def` that runs per frame: a lifecycle method by name, or one tagged
|
|
18
|
+
# `# hot-path` on the line directly above it.
|
|
19
|
+
def hot_path_def?(node)
|
|
20
|
+
return false unless node.def_type?
|
|
21
|
+
|
|
22
|
+
METHODS.include?(node.method_name) || hot_path_tagged?(node)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def hot_path_tagged?(node)
|
|
28
|
+
line_above = node.source_range.first_line - 1
|
|
29
|
+
processed_source.comments.any? do |comment|
|
|
30
|
+
comment.location.line == line_above && comment.text.match?(/\A#\s*hot-path\b/)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# Shared by the two cops that police a layer boundary —
|
|
7
|
+
# `NoCoreInEngineLayer` and `NoEngineInCoreLayer`. They are mirror images:
|
|
8
|
+
# each forbids one namespace inside the other's files, and the only thing
|
|
9
|
+
# that differs is which namespace and which message.
|
|
10
|
+
#
|
|
11
|
+
# Kept in one place because the interesting part is not the comparison but
|
|
12
|
+
# the *walk*: `RGame::Core::Renderer` is three nested `const` nodes, and a
|
|
13
|
+
# naive check reports the same reference three times. Getting that right
|
|
14
|
+
# once beats getting it right twice and then fixing only one of them.
|
|
15
|
+
module LayerBoundary
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def const_path(node)
|
|
19
|
+
names = []
|
|
20
|
+
current = node
|
|
21
|
+
while current&.const_type?
|
|
22
|
+
names.unshift(current.short_name.to_s)
|
|
23
|
+
current = current.namespace
|
|
24
|
+
end
|
|
25
|
+
names
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def opens_namespace?(node, prefixes)
|
|
29
|
+
return false unless under?(node, prefixes)
|
|
30
|
+
|
|
31
|
+
!(node.parent&.const_type? && under?(node.parent, prefixes))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def under?(node, prefixes)
|
|
35
|
+
return false unless node.const_type?
|
|
36
|
+
|
|
37
|
+
path = const_path(node)
|
|
38
|
+
prefixes.any? { |prefix| path.first(prefix.length) == prefix }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'layer_boundary'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Game
|
|
8
|
+
# The engine layer must never name `RGame::Core`.
|
|
9
|
+
#
|
|
10
|
+
# `RGame::Engine` holds the game concepts — scene tree, signals, sprites,
|
|
11
|
+
# tile maps — and its whole value is that it can be specified with no
|
|
12
|
+
# window, no GPU and no clock. It reaches the platform only through
|
|
13
|
+
# objects handed to it: a node's `draw` receives a `renderer` and calls
|
|
14
|
+
# methods on it by name, never storing it and never asking its class.
|
|
15
|
+
#
|
|
16
|
+
# Engine specs enforce this at runtime by simply never loading
|
|
17
|
+
# `rgame/core`, so a stray reference raises `NameError`. That only catches
|
|
18
|
+
# code a test run actually executes, though — this cop covers the
|
|
19
|
+
# branches it doesn't reach.
|
|
20
|
+
#
|
|
21
|
+
# A game has the same line: its nodes and specs run headless, and only its
|
|
22
|
+
# glue class loads the window. So `require 'rgame/game'` is refused too,
|
|
23
|
+
# since it loads everything `rgame/core` does.
|
|
24
|
+
#
|
|
25
|
+
# The short spelling `Core::Image` is flagged only inside `module RGame`,
|
|
26
|
+
# the one place it resolves to `RGame::Core`. Anywhere else it names the
|
|
27
|
+
# project's own `Core`.
|
|
28
|
+
#
|
|
29
|
+
# `RGame::Util` is fine anywhere: those are shareable value types with no
|
|
30
|
+
# OS handle behind them, which is exactly why they live in Util.
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# # bad — names the class
|
|
34
|
+
# def draw(renderer)
|
|
35
|
+
# RGame::Core::Renderer.new
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # bad — the same thing, resolved through the enclosing RGame
|
|
39
|
+
# module RGame
|
|
40
|
+
# Core::Image.new(app, path)
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# # bad — a require pulls SDL into the process
|
|
44
|
+
# require 'rgame/core'
|
|
45
|
+
#
|
|
46
|
+
# # good — duck-typed against whatever it is handed
|
|
47
|
+
# def draw(renderer, _view)
|
|
48
|
+
# renderer.sprite(:hero, 0, 0, 0, 0)
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # good — Util types may be held as attributes
|
|
52
|
+
# @grid = RGame::Util::Tensor.new(w, h, d)
|
|
53
|
+
class NoCoreInEngineLayer < RuboCop::Cop::Base
|
|
54
|
+
include LayerBoundary
|
|
55
|
+
|
|
56
|
+
MSG = 'Headless code must not name `RGame::Core`; receive the object ' \
|
|
57
|
+
'and call it by method name instead.'
|
|
58
|
+
MSG_REQUIRE = 'Headless code must not require `%{path}` — that loads ' \
|
|
59
|
+
'SDL/OpenGL and breaks headless specs.'
|
|
60
|
+
|
|
61
|
+
QUALIFIED = [%w[RGame Core]].freeze
|
|
62
|
+
WITHIN_RGAME = [%w[RGame Core], %w[Core]].freeze
|
|
63
|
+
RESTRICTED_REQUIRE = %r{\Argame/(core|game)(/|\z)|\Argame/core_ext\z}
|
|
64
|
+
|
|
65
|
+
# `require "rgame/core"` and friends.
|
|
66
|
+
# @!method core_require(node)
|
|
67
|
+
def_node_matcher :core_require, <<~PATTERN
|
|
68
|
+
(send nil? {:require :require_relative} (str $_))
|
|
69
|
+
PATTERN
|
|
70
|
+
|
|
71
|
+
def on_send(node)
|
|
72
|
+
core_require(node) do |path|
|
|
73
|
+
next unless RESTRICTED_REQUIRE.match?(path.to_s.delete_prefix('./'))
|
|
74
|
+
|
|
75
|
+
add_offense(node, message: format(MSG_REQUIRE, path: path))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Any constant path under RGame::Core. The walk, and reporting one
|
|
80
|
+
# offence per written reference rather than one per path segment, is in
|
|
81
|
+
# LayerBoundary — shared with this cop's mirror.
|
|
82
|
+
def on_const(node)
|
|
83
|
+
add_offense(node) if opens_namespace?(node, prefixes_for(node))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def prefixes_for(node)
|
|
89
|
+
within_rgame?(node) ? WITHIN_RGAME : QUALIFIED
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def within_rgame?(node)
|
|
93
|
+
node.each_ancestor(:module, :class).any? do |scope|
|
|
94
|
+
const_path(scope.identifier).first == 'RGame'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|