rgame 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# How a game's own resolution is presented on a window that may be a
|
|
6
|
+
# different size. Pure arithmetic: a size, a mode, and the scale and offset
|
|
7
|
+
# they imply. No renderer, no window, nothing to draw with.
|
|
8
|
+
#
|
|
9
|
+
# presentation = Presentation.new(width: 640, height: 480, mode: :integer)
|
|
10
|
+
# presentation.fit(1920, 1080)
|
|
11
|
+
# presentation.scale_x # => 2.0
|
|
12
|
+
# presentation.offset_x # => 320 (1920 - 640*2, halved)
|
|
13
|
+
#
|
|
14
|
+
# ## The problem it exists for
|
|
15
|
+
#
|
|
16
|
+
# A game laid out against fixed numbers — a 500x500 board centred by writing
|
|
17
|
+
# `x: 150` in a 800x600 window — keeps those numbers when the window grows.
|
|
18
|
+
# It does not re-centre; it sits in the top-left corner with the new space
|
|
19
|
+
# piled up to its right. Fullscreen makes that happen to every game that has
|
|
20
|
+
# not thought about it, because fullscreen is the screen's resolution and
|
|
21
|
+
# never the one the game asked for.
|
|
22
|
+
#
|
|
23
|
+
# There are two honest answers, and this is the second one:
|
|
24
|
+
#
|
|
25
|
+
# - **Re-layout.** Read the size you are drawing into and place things
|
|
26
|
+
# relative to its edges. Right for a HUD or a menu, where extra space is
|
|
27
|
+
# extra room. `examples/fullscreen` does this.
|
|
28
|
+
# - **Scale.** Keep the design size, and map it onto whatever the window is.
|
|
29
|
+
# Right for a play area, where extra space should mean a bigger picture of
|
|
30
|
+
# the same thing rather than more world. This class.
|
|
31
|
+
#
|
|
32
|
+
# A game usually wants both at once, which works: the scale applies to
|
|
33
|
+
# everything, and nodes still read their view's size — that size is just the
|
|
34
|
+
# *logical* one now, so it stops changing.
|
|
35
|
+
#
|
|
36
|
+
# ## The modes
|
|
37
|
+
#
|
|
38
|
+
# | | |
|
|
39
|
+
# |---|---|
|
|
40
|
+
# | `:disabled` | No scaling. The logical size *is* the window size, so it changes as the window does. |
|
|
41
|
+
# | `:stretch` | Fill the window exactly, distorting if the aspect ratios differ. |
|
|
42
|
+
# | `:letterbox` | Largest scale that fits, equal on both axes, centred. Bars on two sides. |
|
|
43
|
+
# | `:integer` | The same, rounded down to a whole number. |
|
|
44
|
+
#
|
|
45
|
+
# `:integer` is the one pixel art wants. A sprite drawn at 1.875x with nearest
|
|
46
|
+
# filtering has some source pixels covering two screen pixels and some
|
|
47
|
+
# covering one, and the unevenness crawls as anything moves. A whole-number
|
|
48
|
+
# factor makes every source pixel exactly the same square, which is what
|
|
49
|
+
# "pixel perfect" means.
|
|
50
|
+
#
|
|
51
|
+
# **It costs screen, sometimes a lot of it.** The factor is bounded by the
|
|
52
|
+
# tighter axis, and rounding that down can throw away most of a window. A
|
|
53
|
+
# 640x480 design measured against real screens:
|
|
54
|
+
#
|
|
55
|
+
# | window | `:integer` | `:letterbox` |
|
|
56
|
+
# |---|---|---|
|
|
57
|
+
# | 1920x1080 | 2x, 320px border | 2.25x |
|
|
58
|
+
# | 2560x1440 | 3x, 320px border | 3x |
|
|
59
|
+
# | 1600x900 | **1x**, 480px border | 1.875x |
|
|
60
|
+
# | 1280x720 | **1x**, 320px border | 1.5x |
|
|
61
|
+
#
|
|
62
|
+
# The 16:9 rows are the ones to look at before choosing this mode: 900 and
|
|
63
|
+
# 720 pixels of height are both short of the 960 that 2x needs, so the whole
|
|
64
|
+
# window drops to 1x and the game sits in a small island. A design height
|
|
65
|
+
# that divides common screen heights — 360 rather than 480 — is what avoids
|
|
66
|
+
# that, and it is a decision about the game's art, not about this class.
|
|
67
|
+
#
|
|
68
|
+
# ## What `:integer` does when the window is too small
|
|
69
|
+
#
|
|
70
|
+
# It scales by 1 and lets the content overrun, rather than dropping below a
|
|
71
|
+
# whole number. So the design size is a **minimum**: shrink the window under
|
|
72
|
+
# it and you see part of the game rather than all of it, smaller.
|
|
73
|
+
#
|
|
74
|
+
# That is the same choice SDL makes, and the alternative is worse for the
|
|
75
|
+
# case the mode exists for — falling back to a fractional scale would
|
|
76
|
+
# reintroduce exactly the uneven pixels the game asked to avoid, at the
|
|
77
|
+
# moment nobody is looking. Nothing here enforces a minimum window size;
|
|
78
|
+
# that would need SDL, and this class is pure.
|
|
79
|
+
class Presentation
|
|
80
|
+
MODES = %i[disabled stretch letterbox integer].freeze
|
|
81
|
+
|
|
82
|
+
# The logical size a game draws in. Under `:disabled` these follow the
|
|
83
|
+
# window; under every other mode they are fixed and the window does not
|
|
84
|
+
# matter to anything above this class.
|
|
85
|
+
attr_reader :width, :height
|
|
86
|
+
|
|
87
|
+
attr_reader :mode, :scale_x, :scale_y, :offset_x, :offset_y
|
|
88
|
+
|
|
89
|
+
def initialize(width:, height:, mode: :disabled)
|
|
90
|
+
raise ArgumentError, "unknown scale mode #{mode.inspect} — one of #{MODES.inspect}" \
|
|
91
|
+
unless MODES.include?(mode)
|
|
92
|
+
raise ArgumentError, 'a logical size needs a positive width and height' \
|
|
93
|
+
unless width.positive? && height.positive?
|
|
94
|
+
|
|
95
|
+
@logical_width = width
|
|
96
|
+
@logical_height = height
|
|
97
|
+
@mode = mode
|
|
98
|
+
fit(width, height)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Switches mode and refits against the window it last saw, so a settings
|
|
102
|
+
# screen can change this while the game runs.
|
|
103
|
+
#
|
|
104
|
+
# The window size is remembered for exactly this: a caller changing the
|
|
105
|
+
# mode knows which mode it wants and has no reason to also know how big
|
|
106
|
+
# the window currently is, and asking it to pass one would be asking it to
|
|
107
|
+
# keep a copy of something this object already has.
|
|
108
|
+
def mode=(mode)
|
|
109
|
+
raise ArgumentError, "unknown scale mode #{mode.inspect} — one of #{MODES.inspect}" \
|
|
110
|
+
unless MODES.include?(mode)
|
|
111
|
+
|
|
112
|
+
@mode = mode
|
|
113
|
+
fit(@window_width, @window_height)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Whether anything is being scaled. `:disabled` means the caller can skip
|
|
117
|
+
# pushing a transform at all rather than pushing an identity one.
|
|
118
|
+
#
|
|
119
|
+
# @api private
|
|
120
|
+
def scaled? = @mode != :disabled
|
|
121
|
+
|
|
122
|
+
# Recomputes for a window of this size. Mutates rather than returning a
|
|
123
|
+
# new object: it is read every frame and recomputed only on a resize, so
|
|
124
|
+
# there is no reason to allocate.
|
|
125
|
+
def fit(window_width, window_height)
|
|
126
|
+
@window_width = window_width
|
|
127
|
+
@window_height = window_height
|
|
128
|
+
case @mode
|
|
129
|
+
when :disabled then fit_disabled(window_width, window_height)
|
|
130
|
+
when :stretch then fit_stretch(window_width, window_height)
|
|
131
|
+
else fit_uniform(window_width, window_height)
|
|
132
|
+
end
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def fit_disabled(window_width, window_height)
|
|
139
|
+
@width = window_width
|
|
140
|
+
@height = window_height
|
|
141
|
+
@scale_x = @scale_y = 1.0
|
|
142
|
+
@offset_x = @offset_y = 0.0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def fit_stretch(window_width, window_height)
|
|
146
|
+
@width = @logical_width
|
|
147
|
+
@height = @logical_height
|
|
148
|
+
@scale_x = window_width.to_f / @logical_width
|
|
149
|
+
@scale_y = window_height.to_f / @logical_height
|
|
150
|
+
@offset_x = @offset_y = 0.0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def fit_uniform(window_width, window_height)
|
|
154
|
+
@width = @logical_width
|
|
155
|
+
@height = @logical_height
|
|
156
|
+
|
|
157
|
+
fitting = [window_width.to_f / @logical_width, window_height.to_f / @logical_height].min
|
|
158
|
+
scale = @mode == :integer ? integer_scale(fitting) : fitting
|
|
159
|
+
|
|
160
|
+
@scale_x = @scale_y = scale
|
|
161
|
+
@offset_x = ((window_width - (@logical_width * scale)) / 2.0).floor
|
|
162
|
+
@offset_y = ((window_height - (@logical_height * scale)) / 2.0).floor
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def integer_scale(fitting)
|
|
166
|
+
floored = fitting.floor
|
|
167
|
+
floored < 1 ? 1.0 : floored.to_f
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -16,9 +16,9 @@ module RGame
|
|
|
16
16
|
|
|
17
17
|
def push(scene)
|
|
18
18
|
@stack.push(scene)
|
|
19
|
-
scene.parent = node
|
|
20
|
-
scene.scene = scene
|
|
21
|
-
scene.enter_tree
|
|
19
|
+
scene.parent = node
|
|
20
|
+
scene.scene = scene
|
|
21
|
+
scene.enter_tree
|
|
22
22
|
self
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ module RGame
|
|
|
26
26
|
scene = @stack.pop
|
|
27
27
|
return self unless scene
|
|
28
28
|
|
|
29
|
-
scene.exit_tree
|
|
29
|
+
scene.exit_tree
|
|
30
30
|
scene.scene = nil
|
|
31
31
|
scene.parent = nil
|
|
32
32
|
self
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The rule for the two classes a game subclasses, Node2D and Component: a
|
|
6
|
+
# non-public method whose name starts with `_` is the engine's machinery, and
|
|
7
|
+
# a subclass defining a method of the same name raises where it is defined.
|
|
8
|
+
#
|
|
9
|
+
# class Hop < RGame::Engine::Node2D
|
|
10
|
+
# def _draw_content(renderer, view) = ... # NameError: would replace Node2D#_draw_content
|
|
11
|
+
# def draw_children(renderer, view) = ... # fine: no underscore, meant to be overridden
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# `private` in Ruby says who may *call* a method, not who may *replace* one: a
|
|
15
|
+
# subclass method of the same name is found first, and the base class quietly
|
|
16
|
+
# stops doing what that method did — for that subclass only, with the failure
|
|
17
|
+
# somewhere else entirely. So the underscore is the opt-in. A private or
|
|
18
|
+
# protected method *without* one is a seam, meant to be overridden with
|
|
19
|
+
# `super`, and nothing guards it.
|
|
20
|
+
#
|
|
21
|
+
# Only the base class's own methods are sealed. A private hook an engine
|
|
22
|
+
# subclass writes for its own subclasses — Components::Mover's `take_step` —
|
|
23
|
+
# is not covered, and the underscore convention does not apply outside these
|
|
24
|
+
# two classes.
|
|
25
|
+
#
|
|
26
|
+
# What it cannot see: a method arriving through `include` or `prepend` of a
|
|
27
|
+
# module, which does not pass through `method_added`.
|
|
28
|
+
module SealedPrivates
|
|
29
|
+
def self.extended(base)
|
|
30
|
+
base.define_singleton_method(:sealed_base) { base }
|
|
31
|
+
base.singleton_class.send(:private, :sealed_base)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Every method a subclass may not define: the base's non-public methods
|
|
35
|
+
# whose names start with `_`.
|
|
36
|
+
def sealed_methods
|
|
37
|
+
base = sealed_base
|
|
38
|
+
(base.private_instance_methods(false) + base.protected_instance_methods(false))
|
|
39
|
+
.select { |name| name.start_with?('_') }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def method_added(name)
|
|
43
|
+
super
|
|
44
|
+
base = sealed_base
|
|
45
|
+
return if equal?(base) || !name.start_with?('_')
|
|
46
|
+
return unless base.private_method_defined?(name, false) || base.protected_method_defined?(name, false)
|
|
47
|
+
|
|
48
|
+
raise NameError.new("#{self}##{name} would replace #{base}##{name}, which #{base} calls " \
|
|
49
|
+
"itself, and #{base} would silently stop doing its job for this class. " \
|
|
50
|
+
'Give the method another name.', name)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -13,22 +13,6 @@ module RGame
|
|
|
13
13
|
# rocks.each { |r| hash.insert(r, *r.aabb) }
|
|
14
14
|
# hash.query(*bullet.aabb) { |rock| ...narrowphase... }
|
|
15
15
|
class SpatialHash
|
|
16
|
-
# Cell (col, row) → one integer key. The offset keeps negative cells (objects
|
|
17
|
-
# off-screen / mid-wrap) non-negative; the stride keeps pairs unique. Both are
|
|
18
|
-
# sized to keep every packed key inside a tagged Fixnum, so keying allocates
|
|
19
|
-
# nothing on the query path (CLAUDE.md: never allocate on the per-frame path).
|
|
20
|
-
#
|
|
21
|
-
# The ceiling that sizes them is Windows, not Linux/macOS: CRuby's immediate
|
|
22
|
-
# Fixnum range comes from a C `long`, which is 64 bits on LP64 (Linux, macOS)
|
|
23
|
-
# but stays 32 bits on Windows' LLP64 even in a 64-bit process — so a value
|
|
24
|
-
# comfortably inside Fixnum range on Linux (this packing used to run up to
|
|
25
|
-
# ~2**42) silently becomes a heap-allocated Bignum on Windows instead, one
|
|
26
|
-
# allocation per cell per query. OFFSET/STRIDE here keep the largest possible
|
|
27
|
-
# packed key (both coordinates at the far corner) under 2**28 — well inside
|
|
28
|
-
# Windows' ~2**30 Fixnum ceiling — while still allowing cell coordinates out
|
|
29
|
-
# to +/-8192, i.e. a world some sixteen million pixels wide at this file's own
|
|
30
|
-
# cell_size: 64 example. A game whose world exceeds that wraps into a
|
|
31
|
-
# neighbouring cell's key instead of raising; see #each_cell below.
|
|
32
16
|
OFFSET = 1 << 13
|
|
33
17
|
STRIDE = 1 << 14
|
|
34
18
|
|
|
@@ -46,6 +30,24 @@ module RGame
|
|
|
46
30
|
each_cell(x, y, w, h) { |key| @buckets[key] << item }
|
|
47
31
|
end
|
|
48
32
|
|
|
33
|
+
# Un-bucket an item from the cells the given box covers — the exact inverse of
|
|
34
|
+
# #insert, so the caller passes the box the item was **inserted at**, not where it
|
|
35
|
+
# is now. That is what keeps this stateless: nothing here remembers where anything
|
|
36
|
+
# was bucketed, and a mover that knows where it started can undo its own insert.
|
|
37
|
+
#
|
|
38
|
+
# An item that was never inserted (or a box that covers none of its cells) is a
|
|
39
|
+
# no-op rather than an error — removing something twice, or removing something that
|
|
40
|
+
# left the tree between two steps, is ordinary rather than a mistake.
|
|
41
|
+
#
|
|
42
|
+
# Allocation-free: `fetch` sidesteps the bucket Hash's default block, which would
|
|
43
|
+
# *create* an empty bucket for every cell walked.
|
|
44
|
+
def remove(item, x, y, w, h)
|
|
45
|
+
each_cell(x, y, w, h) do |key|
|
|
46
|
+
bucket = @buckets.fetch(key, nil)
|
|
47
|
+
bucket&.delete(item)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
49
51
|
# Yield every item whose buckets overlap the region. Dedup contract: an item
|
|
50
52
|
# spanning several cells may be yielded more than once. Narrowphase callers
|
|
51
53
|
# must already guard with `next if a.dead? || b.dead?` to make hits idempotent,
|
|
@@ -66,19 +68,49 @@ module RGame
|
|
|
66
68
|
query(cx - r, cy - r, d, d, &)
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
# Is the one cell containing the point (x, y) holding nothing? A point, not a
|
|
72
|
+
# region: pass any coordinate inside the cell you mean.
|
|
73
|
+
#
|
|
74
|
+
# An item is bucketed by its *bounding box*, and the cell walk is half-open on the
|
|
75
|
+
# far edge (see #last_cell), so "bucketed here" means "overlaps this cell's area"
|
|
76
|
+
# exactly: a box filling one cell fills that cell's bucket and no other, and one
|
|
77
|
+
# covering four cells fills four. So this answers occupancy, not just candidacy —
|
|
78
|
+
# it is true when nothing overlaps the cell at all. What it cannot know is whether
|
|
79
|
+
# an occupant still counts; `CollisionWorld#cell_empty?` adds that, skipping
|
|
80
|
+
# colliders whose node is queued for removal.
|
|
81
|
+
#
|
|
82
|
+
# Reads whatever the most recent inserts left behind: after `clear` every cell is
|
|
83
|
+
# empty until something is inserted again. Allocation-free — `fetch` deliberately
|
|
84
|
+
# sidesteps the bucket Hash's default block, which would *create* the bucket.
|
|
85
|
+
def cell_empty?(x, y)
|
|
86
|
+
bucket = @buckets.fetch(cell_key((x / @cell_size).floor, (y / @cell_size).floor), nil)
|
|
87
|
+
bucket.nil? || bucket.empty?
|
|
88
|
+
end
|
|
89
|
+
|
|
69
90
|
private
|
|
70
91
|
|
|
92
|
+
def cell_key(col, row) = (col + OFFSET) * STRIDE + (row + OFFSET)
|
|
93
|
+
|
|
71
94
|
def each_cell(x, y, w, h)
|
|
72
95
|
col0 = (x / @cell_size).floor
|
|
73
96
|
row0 = (y / @cell_size).floor
|
|
74
|
-
col1 = (
|
|
75
|
-
row1 = (
|
|
97
|
+
col1 = last_cell(x + w, col0)
|
|
98
|
+
row1 = last_cell(y + h, row0)
|
|
76
99
|
row0.upto(row1) do |row|
|
|
77
100
|
col0.upto(col1) do |col|
|
|
78
|
-
yield (col
|
|
101
|
+
yield cell_key(col, row)
|
|
79
102
|
end
|
|
80
103
|
end
|
|
81
104
|
end
|
|
105
|
+
|
|
106
|
+
def last_cell(edge, first)
|
|
107
|
+
cell = (edge / @cell_size).floor
|
|
108
|
+
cell -= 1 if (edge % @cell_size).zero?
|
|
109
|
+
# rubocop:disable Style/MinMaxComparison -- the [cell, first].max it asks for is
|
|
110
|
+
# what the project's own Game/NoNeedlessAllocation rejects on a per-frame path.
|
|
111
|
+
cell < first ? first : cell
|
|
112
|
+
# rubocop:enable Style/MinMaxComparison
|
|
113
|
+
end
|
|
82
114
|
end
|
|
83
115
|
end
|
|
84
116
|
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'i18n'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Engine
|
|
7
|
+
# The string a node draws: a translation key, resolved through `I18n` and
|
|
8
|
+
# kept until one of its inputs changes — a variable, or the language.
|
|
9
|
+
#
|
|
10
|
+
# @score = Engine::Text.new('hud.score', :score) # built once, e.g. in initialize
|
|
11
|
+
# renderer.text(@score.with(score: @points), 12, 10) # every frame: cached, no allocation
|
|
12
|
+
#
|
|
13
|
+
# The names a `Text` is built with become the keywords of its `with`, so a
|
|
14
|
+
# forgotten or misspelt variable is Ruby's own `ArgumentError`. `to_s` reads
|
|
15
|
+
# a `Text` with the values its last `with` was given — so whatever draws it,
|
|
16
|
+
# a button's label among them, needs no values of its own — and raises
|
|
17
|
+
# before the first `with`. A `Text` with no names is read with `to_s`
|
|
18
|
+
# alone. Reading one whose variables and
|
|
19
|
+
# `I18n.generation` are unchanged returns the same frozen String and
|
|
20
|
+
# allocates nothing; otherwise it renders again. A `Text` never subscribes to
|
|
21
|
+
# `I18n`: it compares one Integer, so it holds nothing that could keep it
|
|
22
|
+
# alive.
|
|
23
|
+
#
|
|
24
|
+
# A `Text` answers `to_str`, so a renderer takes one where it takes a String,
|
|
25
|
+
# and it is `==` to the String it reads.
|
|
26
|
+
#
|
|
27
|
+
# `Text.literal` is a string that is never translated, and `Text.computed` a
|
|
28
|
+
# string a block builds. Both answer `with` and `to_s` the same way, so
|
|
29
|
+
# whatever draws a `Text` never asks which kind it holds.
|
|
30
|
+
class Text
|
|
31
|
+
NAME = /\A[a-z_][A-Za-z0-9_]*\z/
|
|
32
|
+
private_constant :NAME
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# A `Text` that shows `string` in every language.
|
|
36
|
+
def literal(string) = Literal.new(string)
|
|
37
|
+
|
|
38
|
+
# A `Text` whose string the block builds from the keywords `names`, for
|
|
39
|
+
# text that is formatted rather than looked up. The block runs when a
|
|
40
|
+
# keyword or `I18n.generation` changes, never on an unchanged read, so a
|
|
41
|
+
# block that calls `I18n.t` follows the language too:
|
|
42
|
+
#
|
|
43
|
+
# @clock = Engine::Text.computed(:seconds) { |seconds:| format_clock(seconds) }
|
|
44
|
+
def computed(*names, &block) = Computed.new(names, block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Generates the module that gives a `Text` its `with` for one list of
|
|
48
|
+
# names, the first time that list is seen, and shares it after.
|
|
49
|
+
module Accessors
|
|
50
|
+
@cache = {}
|
|
51
|
+
|
|
52
|
+
def self.for(names)
|
|
53
|
+
@cache[names] ||= generate(names)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def generate(names)
|
|
60
|
+
names.each do |name|
|
|
61
|
+
raise ArgumentError, "#{name.inspect} cannot be a variable name" unless NAME.match?(name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Module.new.tap { |mod| mod.module_eval(accessor_source(names), __FILE__, __LINE__) }
|
|
65
|
+
rescue SyntaxError
|
|
66
|
+
raise ArgumentError, "#{names.inspect} cannot all be variable names"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def accessor_source(names)
|
|
70
|
+
return <<~RUBY if names.empty?
|
|
71
|
+
def with = to_s
|
|
72
|
+
|
|
73
|
+
def to_s
|
|
74
|
+
return @string if @generation == ::RGame::Engine::I18n.generation
|
|
75
|
+
|
|
76
|
+
refresh({})
|
|
77
|
+
end
|
|
78
|
+
RUBY
|
|
79
|
+
|
|
80
|
+
<<~RUBY
|
|
81
|
+
def with(#{names.map { "#{it}:" }.join(', ')})
|
|
82
|
+
if @generation == ::RGame::Engine::I18n.generation && #{names.map { "@_var_#{it} == #{it}" }.join(' && ')}
|
|
83
|
+
return @string
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
#{names.map { "@_var_#{it} = #{it}" }.join('; ')}
|
|
87
|
+
refresh({ #{names.map { "#{it}: #{it}" }.join(', ')} })
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_s
|
|
91
|
+
return @string if @generation == ::RGame::Engine::I18n.generation
|
|
92
|
+
unless defined?(@_var_#{names.first})
|
|
93
|
+
raise ArgumentError, "\#{describe} needs #{names.map { "#{it}:" }.join(', ')}; call with first"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
refresh({ #{names.map { "#{it}: @_var_#{it}" }.join(', ')} })
|
|
97
|
+
end
|
|
98
|
+
RUBY
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_constant :Accessors
|
|
103
|
+
|
|
104
|
+
# The key as given, without its scope.
|
|
105
|
+
attr_reader :key
|
|
106
|
+
|
|
107
|
+
# The variable names `with` takes, sorted.
|
|
108
|
+
attr_reader :names
|
|
109
|
+
|
|
110
|
+
attr_reader :scope
|
|
111
|
+
|
|
112
|
+
# A `Text` for `key`, whose translation prints the variables `names`.
|
|
113
|
+
# `scope: 'title_menu'` with key `'play'` resolves `'title_menu.play'`.
|
|
114
|
+
def initialize(key, *names, scope: nil)
|
|
115
|
+
@key = key.to_s.freeze
|
|
116
|
+
self.scope = scope
|
|
117
|
+
declare(names)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Changes the scope the key resolves under; the next read resolves again.
|
|
121
|
+
def scope=(scope)
|
|
122
|
+
@scope = scope&.to_s&.freeze
|
|
123
|
+
@path = (@scope ? "#{@scope}.#{@key}" : @key).freeze
|
|
124
|
+
@generation = nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The same String as `to_s`, so a `Text` goes wherever a String is
|
|
128
|
+
# expected: `renderer.text(@title, 12, 10)`. Before the first `with`, a
|
|
129
|
+
# `Text` with names raises `ArgumentError` here too.
|
|
130
|
+
def to_str = to_s
|
|
131
|
+
|
|
132
|
+
# Equal to a String that reads the same, from either side, so a spy that
|
|
133
|
+
# recorded a `Text` matches the String it drew. Against anything else,
|
|
134
|
+
# including another `Text`, it compares by identity.
|
|
135
|
+
def ==(other) = other.is_a?(String) ? to_s == other : super
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def declare(names)
|
|
140
|
+
@names = names.map(&:to_sym).sort.freeze
|
|
141
|
+
raise ArgumentError, "#{describe} declares a variable twice" if @names.uniq.size != @names.size
|
|
142
|
+
|
|
143
|
+
extend(Accessors.for(@names))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def refresh(vars)
|
|
147
|
+
generation = I18n.generation
|
|
148
|
+
@string = render(vars)
|
|
149
|
+
@generation = generation
|
|
150
|
+
@string
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def render(vars) = I18n.render(@path, @names, vars).freeze
|
|
154
|
+
|
|
155
|
+
def describe = "Text #{@path.inspect}"
|
|
156
|
+
|
|
157
|
+
# A `Text` that is not translated: the same `with` and `to_s`, answering
|
|
158
|
+
# with one String.
|
|
159
|
+
class Literal < Text
|
|
160
|
+
def initialize(string) # rubocop:disable Lint/MissingSuper -- a literal has no key to resolve
|
|
161
|
+
@string = -string.to_s
|
|
162
|
+
@names = [].freeze
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def with = @string
|
|
166
|
+
def to_s = @string
|
|
167
|
+
|
|
168
|
+
# A literal ignores a scope: there is no key to put one in front of.
|
|
169
|
+
def scope=(_scope); end
|
|
170
|
+
end
|
|
171
|
+
private_constant :Literal
|
|
172
|
+
|
|
173
|
+
# A `Text` built by a block from its keywords instead of from a key.
|
|
174
|
+
class Computed < Text
|
|
175
|
+
def initialize(names, block) # rubocop:disable Lint/MissingSuper -- a computed text has no key to resolve
|
|
176
|
+
raise ArgumentError, 'Text.computed needs a block' unless block
|
|
177
|
+
|
|
178
|
+
@block = block
|
|
179
|
+
declare(names)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A computed text ignores a scope: there is no key to put one in front of.
|
|
183
|
+
def scope=(_scope); end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def render(vars) = @block.call(**vars)
|
|
188
|
+
|
|
189
|
+
def describe = 'Text.computed'
|
|
190
|
+
end
|
|
191
|
+
private_constant :Computed
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The tile grid as a blocker source: axis-separated AABB-vs-tile collision
|
|
6
|
+
# resolution over a Util::SolidGrid, at a tile size. Assumes per-step movement smaller
|
|
7
|
+
# than a tile (no tunneling), which holds for our speeds.
|
|
8
|
+
#
|
|
9
|
+
# It answers the blocker-source question — "where does this box land moving dx" —
|
|
10
|
+
# which CollisionSystem asks of every source it holds; see its header for the
|
|
11
|
+
# protocol. Resolving X then Y (with the X result) is what gives wall-sliding, and
|
|
12
|
+
# that ordering lives in the system rather than here, so every source shares it.
|
|
13
|
+
#
|
|
14
|
+
# It also answers the protocol's optional fourth question, #travel? — whether a box can
|
|
15
|
+
# move along a segment without any resolve falling short — which is how a route is
|
|
16
|
+
# checked against the resolver that will stop the walker on it. It is the only source
|
|
17
|
+
# that answers it today.
|
|
18
|
+
#
|
|
19
|
+
# The arithmetic for both is Util::TileSweep's, in C: this class is its blocker-source
|
|
20
|
+
# face, adding what the protocol asks beyond the numbers. The grid is read, never copied,
|
|
21
|
+
# so a cell changed with SolidGrid#set_solid stops the next step.
|
|
22
|
+
class TileBlockers
|
|
23
|
+
# What a TileBlockers reports as having stopped a step. One object for the life of
|
|
24
|
+
# the process, answering the same two questions a collider does — so a handler reads
|
|
25
|
+
# `by.layer` whatever stopped it and never branches on what kind of thing it was. A
|
|
26
|
+
# tile has no node, so `node` is nil.
|
|
27
|
+
#
|
|
28
|
+
# It holds no state at all, which is what makes one TileBlockers safe to share
|
|
29
|
+
# between every body on the map: there is nothing here for two of them to race over.
|
|
30
|
+
class Tiles
|
|
31
|
+
def layer = :tiles
|
|
32
|
+
def node = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
TILES = Tiles.new.freeze
|
|
36
|
+
|
|
37
|
+
def initialize(grid:, tile_width:, tile_height:)
|
|
38
|
+
@sweep = Util::TileSweep.new(grid, tile_width, tile_height)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def grid = @sweep.grid
|
|
42
|
+
|
|
43
|
+
# Move an AABB (top-left x, y; size w, h) by dx, snapping flush against solids. A Float.
|
|
44
|
+
def resolve_x(x, y, w, h, dx) = @sweep.resolve_x(x, y, w, h, dx)
|
|
45
|
+
|
|
46
|
+
def resolve_y(x, y, w, h, dy) = @sweep.resolve_y(x, y, w, h, dy)
|
|
47
|
+
|
|
48
|
+
# Whether the box travels (dx, dy) without any resolve along the way falling short of
|
|
49
|
+
# where it was heading — so a walker resolving its own steps against this source, each
|
|
50
|
+
# under a quarter tile, is never stopped on that segment. A landing *past* the intended
|
|
51
|
+
# one does not count as stopped, which is how CollisionSystem reads every source.
|
|
52
|
+
def travel?(x, y, w, h, dx, dy) = @sweep.travel?(x, y, w, h, dx, dy)
|
|
53
|
+
|
|
54
|
+
# The grid stopped it, and the grid is the same for everybody — so this is a
|
|
55
|
+
# constant rather than a record of the last step. See TILES.
|
|
56
|
+
def blocker = TILES
|
|
57
|
+
|
|
58
|
+
# A grid does not move, so there is nothing to re-index. Part of the blocker-source
|
|
59
|
+
# protocol Engine::CollisionSystem calls after every step; see its header.
|
|
60
|
+
def moved(_actor, _from_x, _from_y, _w, _h) = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -20,7 +20,7 @@ module RGame
|
|
|
20
20
|
# this class (see CLAUDE.md, "The rule points both ways"). So `load` hands
|
|
21
21
|
# back a path and stops there.
|
|
22
22
|
class TileMap
|
|
23
|
-
FLIP_MASK = 0x1FFFFFFF
|
|
23
|
+
FLIP_MASK = 0x1FFFFFFF
|
|
24
24
|
|
|
25
25
|
attr_reader :width, :height, :tile_width, :tile_height,
|
|
26
26
|
:pixel_width, :pixel_height, :tileset_source, :firstgid
|
|
@@ -71,7 +71,6 @@ module RGame
|
|
|
71
71
|
)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
# True if a layer carries a Tiled bool custom property `name` set to "true".
|
|
75
74
|
def self.layer_flag?(layer_el, name)
|
|
76
75
|
props = layer_el.elements['properties']
|
|
77
76
|
return false unless props
|
|
@@ -81,6 +80,7 @@ module RGame
|
|
|
81
80
|
end
|
|
82
81
|
false
|
|
83
82
|
end
|
|
83
|
+
private_class_method :layer_flag?
|
|
84
84
|
|
|
85
85
|
def initialize(width:, height:, tile_width:, tile_height:, tileset_source:, firstgid:, layers:, above: [])
|
|
86
86
|
@width = width
|
|
@@ -133,7 +133,6 @@ module RGame
|
|
|
133
133
|
|
|
134
134
|
private
|
|
135
135
|
|
|
136
|
-
# Pack the parsed per-layer gid rows into one flat Tensor ([col, row, layer]).
|
|
137
136
|
def build_tiles(layers)
|
|
138
137
|
tiles = Util::Tensor.new(@width, @height, layers.length, initial: 0)
|
|
139
138
|
layers.each_with_index do |gids, layer|
|
|
@@ -50,8 +50,6 @@ module RGame
|
|
|
50
50
|
gap = under || world.first_above_layer
|
|
51
51
|
|
|
52
52
|
world.layer_count.times do |index|
|
|
53
|
-
# A layer at or past the gap sits above the actors; z is only ever
|
|
54
|
-
# compared to a sibling's, so the +1 is a gap, not a magnitude.
|
|
55
53
|
parent.add_node(new(layer: index, z: index < gap ? index : index + 1))
|
|
56
54
|
end
|
|
57
55
|
parent.add_node(Node2D.new(z: gap))
|
data/lib/rgame/engine/tileset.rb
CHANGED
|
@@ -15,7 +15,7 @@ module RGame
|
|
|
15
15
|
Frame = Struct.new(:tile_id, :duration)
|
|
16
16
|
|
|
17
17
|
attr_reader :firstgid, :columns, :tile_width, :tile_height, :image_source, :animations
|
|
18
|
-
attr_accessor :solid_ids
|
|
18
|
+
attr_accessor :solid_ids
|
|
19
19
|
|
|
20
20
|
def self.parse(tsx_string, firstgid:)
|
|
21
21
|
root = REXML::Document.new(tsx_string).root
|
|
@@ -40,7 +40,6 @@ module RGame
|
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Frames for an animated tile, or nil (compacted away) for a static one.
|
|
44
43
|
def self.parse_animation(tile_el)
|
|
45
44
|
anim = tile_el.elements['animation']
|
|
46
45
|
return unless anim
|
|
@@ -50,12 +49,11 @@ module RGame
|
|
|
50
49
|
end
|
|
51
50
|
end
|
|
52
51
|
|
|
53
|
-
# A tile is solid if it carries a Tiled collision shape — an `<objectgroup>`
|
|
54
|
-
# with at least one object (the per-tile collision editor's output).
|
|
55
52
|
def self.collision_shape?(tile_el)
|
|
56
53
|
group = tile_el.elements['objectgroup']
|
|
57
54
|
!group.nil? && !group.elements['object'].nil?
|
|
58
55
|
end
|
|
56
|
+
private_class_method :parse_animation, :collision_shape?
|
|
59
57
|
|
|
60
58
|
def initialize(firstgid:, columns:, tile_width:, tile_height:, image_source:, animations:, solid_ids: Set.new)
|
|
61
59
|
@firstgid = firstgid
|