rgame 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +144 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +3 -5
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +167 -12
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# How big the world is — the contract, not an implementation.
|
|
7
|
+
#
|
|
8
|
+
# Two systems answer it: `World` below, which is nothing *but* the answer,
|
|
9
|
+
# and `TileWorld`, which derives it from the map it parsed. A component
|
|
10
|
+
# that needs bounds asks for the contract — `node.system(WorldBounds)` —
|
|
11
|
+
# and gets whichever of the two the scene mounted. That works because
|
|
12
|
+
# Node2D#get_component matches with `is_a?`, which matches an included
|
|
13
|
+
# module as readily as a class.
|
|
14
|
+
#
|
|
15
|
+
# Naming the contract is what keeps the two from drifting: ScreenWrap does
|
|
16
|
+
# not know which kind of world it is wrapping inside, and must not need to.
|
|
17
|
+
#
|
|
18
|
+
# ## The bounds are in world coordinates
|
|
19
|
+
#
|
|
20
|
+
# The world runs from (0, 0) to (world_width, world_height) in **world**
|
|
21
|
+
# space — the space `Node2D#world_x` answers in, and the one the tile grid,
|
|
22
|
+
# the collision world and the camera already use. So everything that
|
|
23
|
+
# compares a node against these bounds compares `world_x`/`world_y`, never
|
|
24
|
+
# the node's local `x`/`y`: an entity grouped under an offset container is
|
|
25
|
+
# still inside the world when it is, whatever its position in the container.
|
|
26
|
+
# spec/support/shared_examples/a_world_edge_response.rb holds every reader to it.
|
|
27
|
+
module WorldBounds
|
|
28
|
+
def world_width = raise(NotImplementedError, "#{self.class} must define #world_width")
|
|
29
|
+
def world_height = raise(NotImplementedError, "#{self.class} must define #world_height")
|
|
30
|
+
|
|
31
|
+
# The bounds a component should use: whatever it was handed, falling back
|
|
32
|
+
# to the world system on `node` for either axis left nil. The single place
|
|
33
|
+
# that fallback is written, so the components sharing it cannot drift.
|
|
34
|
+
#
|
|
35
|
+
# Call it from `on_attach`, not `initialize` — a node has no scene to ask
|
|
36
|
+
# until it is in the tree, and a pooled entity is built long before it is.
|
|
37
|
+
def self.resolve(node, width, height)
|
|
38
|
+
return [width, height] if width && height
|
|
39
|
+
|
|
40
|
+
bounds = node.system(self)
|
|
41
|
+
if bounds.nil?
|
|
42
|
+
raise 'no world bounds in scope: mount a World (or TileWorld) system on the scene, ' \
|
|
43
|
+
'or pass explicit width:/height:'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
[width || bounds.world_width, height || bounds.world_height]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Refuse a node carrying more than one response to the edge of the world:
|
|
50
|
+
# ScreenWrap, DespawnOffscreen, or a Mover declaring `blocked_by: [:bounds]`.
|
|
51
|
+
#
|
|
52
|
+
# Stopping at the edge, wrapping past it and being freed beyond it are three
|
|
53
|
+
# answers to one question, and no two of them mean anything together. Stop and
|
|
54
|
+
# wrap disagree about where the node ends up; wrap and despawn race on which
|
|
55
|
+
# margin is reached first; and the stop works on the collision box while the
|
|
56
|
+
# other two test the node's origin, so a box with an offset holds the origin
|
|
57
|
+
# just past the edge and the other response fires on a node that was held.
|
|
58
|
+
# Rather than a rule each game has to remember, it raises.
|
|
59
|
+
#
|
|
60
|
+
# Folding wrap and despawn into Mover as one `at_edge:` option would make two
|
|
61
|
+
# responses inexpressible too, but they act on the node however it moved — a reset,
|
|
62
|
+
# an ancestor, a direct write — and every option there is a branch on the free
|
|
63
|
+
# step's path. A refusal at attach costs no frame anything. It is also the place
|
|
64
|
+
# to change if a game wants wrapping on one axis and stopping on the other: the
|
|
65
|
+
# rule then becomes one response per axis.
|
|
66
|
+
#
|
|
67
|
+
# Every response calls this from its own on_attach, which is what makes it
|
|
68
|
+
# order-free: a node assembled outside the tree already holds all its
|
|
69
|
+
# components when the first attaches, and on a live node each attaches on
|
|
70
|
+
# arrival, so the second response always finds the first.
|
|
71
|
+
#
|
|
72
|
+
# A pooled node re-attaches on every spawn, so this walks the components
|
|
73
|
+
# without allocating, and builds a message only when it raises.
|
|
74
|
+
def self.one_response!(node)
|
|
75
|
+
first = nil
|
|
76
|
+
node.components.each do |component|
|
|
77
|
+
next unless edge_response?(component)
|
|
78
|
+
next first = component if first.nil?
|
|
79
|
+
|
|
80
|
+
raise "#{node.class} has two responses to the edge of the world: " \
|
|
81
|
+
"#{describe_response(first)} and #{describe_response(component)}. " \
|
|
82
|
+
'A node can stop at the edge, wrap past it or be freed beyond it, but ' \
|
|
83
|
+
'only one of those — keep the one this node is for.'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.edge_response?(component)
|
|
88
|
+
component.is_a?(ScreenWrap) || component.is_a?(DespawnOffscreen) ||
|
|
89
|
+
(component.is_a?(Mover) && component.blocked_by?(Mover::BOUNDS))
|
|
90
|
+
end
|
|
91
|
+
private_class_method :edge_response?
|
|
92
|
+
|
|
93
|
+
def self.describe_response(component)
|
|
94
|
+
name = component.class.name&.split('::')&.last || component.class.inspect
|
|
95
|
+
component.is_a?(Mover) ? "#{name} (blocked_by :bounds)" : name
|
|
96
|
+
end
|
|
97
|
+
private_class_method :describe_response
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The scene-scoped world: how big it is, and nothing else yet.
|
|
101
|
+
#
|
|
102
|
+
# A game whose world is a plain rectangle mounts one on its scene, and the
|
|
103
|
+
# components that need bounds — ScreenWrap, DespawnOffscreen — find it
|
|
104
|
+
# rather than having the numbers threaded through every constructor between
|
|
105
|
+
# the scene and the entity. That threading is what this replaces, and it was
|
|
106
|
+
# worse than it looks: a pooled entity is built outside the tree, so its
|
|
107
|
+
# factory had to close over the size, and every scene above the factory had
|
|
108
|
+
# to carry the size in order to build it.
|
|
109
|
+
#
|
|
110
|
+
# It is deliberately **not** the window size. The two coincide in a
|
|
111
|
+
# single-screen game, which is exactly what makes the mistake easy to make
|
|
112
|
+
# and hard to see: bind wrapping to the viewport and the world silently
|
|
113
|
+
# changes shape when the window is resized, or when the screen is split and
|
|
114
|
+
# each half is its own viewport. Ask `Viewports` (or the `View` a draw is
|
|
115
|
+
# handed) how big the *window* is; ask this how big the *world* is.
|
|
116
|
+
#
|
|
117
|
+
# The bounds are immutable, which is what lets the components resolve them
|
|
118
|
+
# once at attach rather than re-reading them every frame. A world that
|
|
119
|
+
# genuinely changes size is a new scene.
|
|
120
|
+
class World < Engine::Component
|
|
121
|
+
include WorldBounds
|
|
122
|
+
|
|
123
|
+
attr_reader :world_width, :world_height
|
|
124
|
+
|
|
125
|
+
def initialize(width:, height:)
|
|
126
|
+
super()
|
|
127
|
+
@world_width = width
|
|
128
|
+
@world_height = height
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The colliders one collider is touching, this step and last. CollisionWorld owns
|
|
6
|
+
# one of these per registered collider and drives it, and together they are what
|
|
7
|
+
# turns a per-step overlap test into the two edges a game actually wants: the step
|
|
8
|
+
# a contact starts and the step it ends. Pure logic; no graphics.
|
|
9
|
+
#
|
|
10
|
+
# Nothing here is about overlapping, though, and it has a second user:
|
|
11
|
+
# every Components::Mover keeps one of *what stopped its step*, and gets on_blocked
|
|
12
|
+
# and on_unblocked out of it on exactly the same terms. What this class is, underneath
|
|
13
|
+
# its names, is "the set of things that were true this step and last".
|
|
14
|
+
#
|
|
15
|
+
# Two arrays, swapped rather than reallocated. `begin_frame` makes this step's list
|
|
16
|
+
# into last step's and empties the other one for refilling, so after the first few
|
|
17
|
+
# frames nothing here allocates — `Array#clear` keeps the capacity it grew to, the
|
|
18
|
+
# same trick SpatialHash#clear relies on.
|
|
19
|
+
#
|
|
20
|
+
# Membership is a linear scan by identity, and deliberately so. A Set or a Hash
|
|
21
|
+
# allocates on insert, and both are slower than scanning a handful of entries; a
|
|
22
|
+
# collider touching more than a handful of things at once is a design problem
|
|
23
|
+
# somewhere else, not a reason to index this.
|
|
24
|
+
class ContactSet
|
|
25
|
+
def initialize
|
|
26
|
+
@current = []
|
|
27
|
+
@previous = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Start a step: this step's contacts become the ones to compare against, and the
|
|
31
|
+
# list being filled is emptied. Swapped through a temporary rather than by
|
|
32
|
+
# `@current, @previous = @previous, @current`, which would allocate an Array every
|
|
33
|
+
# step — see .rubocop.yml on Style/SwapValues.
|
|
34
|
+
def begin_frame
|
|
35
|
+
filled = @current
|
|
36
|
+
@current = @previous
|
|
37
|
+
@previous = filled
|
|
38
|
+
@current.clear
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Forget everything, both steps. CollisionWorld calls this when a collider
|
|
42
|
+
# registers, so a pooled entity that died mid-contact cannot come back still
|
|
43
|
+
# holding the contacts it had in its previous life — which would otherwise
|
|
44
|
+
# surface as one spurious `on_separated` on its first step.
|
|
45
|
+
def reset
|
|
46
|
+
@current.clear
|
|
47
|
+
@previous.clear
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Record a contact for this step.
|
|
51
|
+
def add(other) = @current << other
|
|
52
|
+
|
|
53
|
+
# Already recorded this step? The broadphase offers a pair once per cell the two
|
|
54
|
+
# share, so this is what keeps one overlap from being counted twice.
|
|
55
|
+
def touching?(other) = @current.include?(other)
|
|
56
|
+
|
|
57
|
+
# Is this the *start* of a contact — a pair that was not touching last step? It
|
|
58
|
+
# reads only last step's list, so it gives the same answer either side of #add,
|
|
59
|
+
# and asking before recording is what reads as a guard.
|
|
60
|
+
def started?(other) = !@previous.include?(other)
|
|
61
|
+
|
|
62
|
+
# Yield every collider that was in contact last step and is not in contact now —
|
|
63
|
+
# the ending edge. Call it once every pair for the step has been recorded.
|
|
64
|
+
def each_ended
|
|
65
|
+
i = 0
|
|
66
|
+
while i < @previous.size
|
|
67
|
+
other = @previous[i]
|
|
68
|
+
i += 1
|
|
69
|
+
yield other unless @current.include?(other)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/rgame/engine/culling.rb
CHANGED
|
@@ -22,22 +22,20 @@ module RGame
|
|
|
22
22
|
#
|
|
23
23
|
# - **No size means no culling.** `node.width`/`height` are what a drawable's
|
|
24
24
|
# footprint is measured from, and a node that never set them (nothing
|
|
25
|
-
# requires it
|
|
26
|
-
#
|
|
25
|
+
# requires it) reads as 0×0, which would otherwise cull everything
|
|
26
|
+
# instantly. Unknown means draw.
|
|
27
27
|
# - **A rotated node is measured generously.** `Node2D#draw` rotates about the
|
|
28
|
-
# node's
|
|
28
|
+
# node's own origin, so a rotated footprint can reach further than its
|
|
29
29
|
# box in any direction. The margin is `width + height`, which is always at
|
|
30
30
|
# least the diagonal `hypot(width, height)` and costs no square root on a
|
|
31
31
|
# path that runs once per drawable per viewport.
|
|
32
32
|
module Culling
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
-
# Can this box be skipped for `view`? Coordinates are in the space the
|
|
36
|
-
# caller draws in — world coordinates under a camera.
|
|
37
35
|
# hot-path
|
|
38
36
|
def culled?(view, x, y, width, height)
|
|
39
37
|
return false if width.zero? || height.zero?
|
|
40
|
-
return !view.visible?(x, y, width, height) if node.
|
|
38
|
+
return !view.visible?(x, y, width, height) if node.world_angle.zero?
|
|
41
39
|
|
|
42
40
|
margin = width + height
|
|
43
41
|
!view.visible?(x - margin, y - margin, width + (margin * 2), height + (margin * 2))
|
|
@@ -35,22 +35,21 @@ module RGame
|
|
|
35
35
|
OBJ_LABEL = 'OBJ'
|
|
36
36
|
DELTA_LABEL = 'Δ/f'
|
|
37
37
|
|
|
38
|
-
COLOR = [80, 255, 120].freeze
|
|
39
|
-
PAD = 8
|
|
40
|
-
GAP = 8
|
|
38
|
+
COLOR = [80, 255, 120].freeze
|
|
39
|
+
PAD = 8
|
|
40
|
+
GAP = 8
|
|
41
41
|
|
|
42
42
|
def initialize
|
|
43
43
|
@visible = false
|
|
44
44
|
@prev_allocated = 0
|
|
45
|
-
@digit_widths = Array.new(10)
|
|
46
|
-
@label_widths = {}
|
|
45
|
+
@digit_widths = Array.new(10)
|
|
46
|
+
@label_widths = {}
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def visible? = @visible
|
|
50
50
|
|
|
51
51
|
def toggle
|
|
52
52
|
@visible = !@visible
|
|
53
|
-
# Baseline the counter on reveal so the first Δ/f isn't the whole run's history.
|
|
54
53
|
@prev_allocated = GC.stat(:total_allocated_objects) if @visible
|
|
55
54
|
end
|
|
56
55
|
|
|
@@ -70,8 +69,6 @@ module RGame
|
|
|
70
69
|
@prev_allocated = allocated
|
|
71
70
|
|
|
72
71
|
line_h = renderer.text_height
|
|
73
|
-
# The view's size, not its position: inside a region that has been
|
|
74
|
-
# translated to its corner, adding `view.x` would offset a second time.
|
|
75
72
|
right_x = view.width - PAD
|
|
76
73
|
top_y = view.height - PAD - (line_h * 3)
|
|
77
74
|
|
|
@@ -84,14 +81,11 @@ module RGame
|
|
|
84
81
|
|
|
85
82
|
private
|
|
86
83
|
|
|
87
|
-
# A right-aligned "label number" row ending at right_x.
|
|
88
84
|
def draw_line(renderer, label, value, right_x, y)
|
|
89
85
|
number_left = draw_uint(renderer, value, right_x, y)
|
|
90
86
|
renderer.text(label, number_left - GAP - label_width(renderer, label), y, color: COLOR)
|
|
91
87
|
end
|
|
92
88
|
|
|
93
|
-
# Draw a non-negative integer right-aligned ending at right_x, digit by digit so no
|
|
94
|
-
# String is built per frame. Returns the x of the leftmost digit.
|
|
95
89
|
def draw_uint(renderer, value, right_x, y)
|
|
96
90
|
x = right_x
|
|
97
91
|
more = true
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module I18n
|
|
6
|
+
# A translation that varies with `count`: one `Template` per CLDR category
|
|
7
|
+
# the table spells out, and the locale of the table it came from, whose
|
|
8
|
+
# language decides which category a count falls into.
|
|
9
|
+
#
|
|
10
|
+
# Internal to `I18n`. A nested Hash in a table compiles to a Plural when
|
|
11
|
+
# every key is a category, every value is text, and `other` is among them;
|
|
12
|
+
# any other Hash is a level of nesting.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
class Plural
|
|
16
|
+
CATEGORIES = %i[zero one two few many other].freeze
|
|
17
|
+
CATEGORY_NAMES = CATEGORIES.map(&:name).freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :locale
|
|
20
|
+
|
|
21
|
+
# The variable names any form uses, each once, and `count`, which every
|
|
22
|
+
# form is chosen by whether it prints it or not.
|
|
23
|
+
attr_reader :names
|
|
24
|
+
|
|
25
|
+
def self.forms?(entries)
|
|
26
|
+
entries.key?('other') &&
|
|
27
|
+
entries.all? { |name, value| CATEGORY_NAMES.include?(name) && value.is_a?(String) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(locale, entries)
|
|
31
|
+
@locale = locale
|
|
32
|
+
@forms = entries.to_h { |name, source| [name.to_sym, Template.compile(source)] }.freeze
|
|
33
|
+
@names = @forms.values.flat_map(&:names).push(:count).uniq.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The template for `count`, whose category under this table's language
|
|
37
|
+
# is `category`. An explicit `zero` form wins for 0 in every language,
|
|
38
|
+
# and a category the table leaves out reads `other`.
|
|
39
|
+
def template_for(count, category)
|
|
40
|
+
(count == 0 && @forms[:zero]) || @forms[category] || @forms.fetch(:other) # rubocop:disable Style/NumericPredicate -- count may be any object
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module I18n
|
|
6
|
+
# CLDR plural categories for whole numbers, one rule per language. A rule
|
|
7
|
+
# takes the `count` a caller passed and returns one of `Plural::CATEGORIES`.
|
|
8
|
+
# Every built-in rule answers `:other` for a count that is not an Integer,
|
|
9
|
+
# because fractional counts have categories of their own that no built-in
|
|
10
|
+
# table spells out.
|
|
11
|
+
#
|
|
12
|
+
# Rules are keyed by language, and `I18n.plural_rule` adds or replaces one;
|
|
13
|
+
# a language with no rule counts like English.
|
|
14
|
+
#
|
|
15
|
+
# Source: the CLDR plural rules, https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
|
|
16
|
+
module PluralRules
|
|
17
|
+
TWO_TO_FOUR = (2..4)
|
|
18
|
+
TWELVE_TO_FOURTEEN = (12..14)
|
|
19
|
+
|
|
20
|
+
def self.whole(&rule) = ->(count) { count.is_a?(Integer) ? rule.call(count.abs) : :other } # rubocop:disable Performance/RedundantBlockCall -- the rule runs after whole has returned, where yield has no block
|
|
21
|
+
|
|
22
|
+
def self.slavic_few?(count) = TWO_TO_FOUR.cover?(count % 10) && !TWELVE_TO_FOURTEEN.cover?(count % 100)
|
|
23
|
+
|
|
24
|
+
def self.million(count) = count.positive? && (count % 1_000_000).zero? ? :many : :other
|
|
25
|
+
|
|
26
|
+
private_class_method :whole, :slavic_few?, :million
|
|
27
|
+
|
|
28
|
+
ONE_OTHER = whole { |n| n == 1 ? :one : :other }
|
|
29
|
+
ONE_MILLION_OTHER = whole { |n| n == 1 ? :one : million(n) }
|
|
30
|
+
ZERO_ONE_MILLION_OTHER = whole { |n| n <= 1 ? :one : million(n) }
|
|
31
|
+
|
|
32
|
+
EAST_SLAVIC = whole do |n|
|
|
33
|
+
if n % 10 == 1 && n % 100 != 11 then :one
|
|
34
|
+
elsif slavic_few?(n) then :few
|
|
35
|
+
else :many
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
POLISH = whole do |n|
|
|
40
|
+
if n == 1 then :one
|
|
41
|
+
elsif slavic_few?(n) then :few
|
|
42
|
+
else :many
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
CZECH = whole do |n|
|
|
47
|
+
if n == 1 then :one
|
|
48
|
+
elsif TWO_TO_FOUR.cover?(n) then :few
|
|
49
|
+
else :other
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ARABIC = whole do |n|
|
|
54
|
+
case n
|
|
55
|
+
when 0 then :zero
|
|
56
|
+
when 1 then :one
|
|
57
|
+
when 2 then :two
|
|
58
|
+
else
|
|
59
|
+
case n % 100
|
|
60
|
+
when 3..10 then :few
|
|
61
|
+
when 11..99 then :many
|
|
62
|
+
else :other
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
OTHER = ->(_count) { :other }
|
|
68
|
+
|
|
69
|
+
BUILT_IN = {
|
|
70
|
+
en: ONE_OTHER, de: ONE_OTHER, nl: ONE_OTHER, sv: ONE_OTHER, da: ONE_OTHER, nb: ONE_OTHER, fi: ONE_OTHER,
|
|
71
|
+
it: ONE_MILLION_OTHER, es: ONE_MILLION_OTHER,
|
|
72
|
+
fr: ZERO_ONE_MILLION_OTHER, pt: ZERO_ONE_MILLION_OTHER,
|
|
73
|
+
ru: EAST_SLAVIC, uk: EAST_SLAVIC, pl: POLISH, cs: CZECH,
|
|
74
|
+
ja: OTHER, zh: OTHER, ko: OTHER,
|
|
75
|
+
ar: ARABIC
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
DEFAULT = ONE_OTHER
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module I18n
|
|
6
|
+
# One translated string, compiled once at load: the literal runs and the
|
|
7
|
+
# `%{name}` placeholders between them, so rendering is an append per part
|
|
8
|
+
# rather than a parse. `%%{` in the source is a literal `%{`.
|
|
9
|
+
#
|
|
10
|
+
# Internal to `I18n`; a game reaches translations through `I18n.t` or a
|
|
11
|
+
# `Text`, never through a Template.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class Template
|
|
15
|
+
PLACEHOLDER = /%%\{|%\{([A-Za-z_]\w*)\}/
|
|
16
|
+
|
|
17
|
+
# The variable names the source uses, each once, in order of appearance.
|
|
18
|
+
attr_reader :names
|
|
19
|
+
|
|
20
|
+
def self.compile(source)
|
|
21
|
+
parts = []
|
|
22
|
+
literal = +''
|
|
23
|
+
position = 0
|
|
24
|
+
while (match = PLACEHOLDER.match(source, position))
|
|
25
|
+
literal << source[position...match.begin(0)]
|
|
26
|
+
if match[1]
|
|
27
|
+
parts << literal.freeze unless literal.empty?
|
|
28
|
+
parts << match[1].to_sym
|
|
29
|
+
literal = +''
|
|
30
|
+
else
|
|
31
|
+
literal << '%{'
|
|
32
|
+
end
|
|
33
|
+
position = match.end(0)
|
|
34
|
+
end
|
|
35
|
+
literal << source[position..]
|
|
36
|
+
parts << literal.freeze unless literal.empty?
|
|
37
|
+
new(parts)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(parts)
|
|
41
|
+
@parts = parts.freeze
|
|
42
|
+
@names = parts.grep(Symbol).uniq.freeze
|
|
43
|
+
@constant = @names.empty? ? -parts.join : nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The string with each placeholder replaced by `vars[name].to_s`. A
|
|
47
|
+
# template without placeholders returns the same frozen String every
|
|
48
|
+
# time. Every name in `names` must be a key of `vars`.
|
|
49
|
+
def render(vars)
|
|
50
|
+
return @constant if @constant
|
|
51
|
+
|
|
52
|
+
@parts.each_with_object(+'') do |part, out|
|
|
53
|
+
out << (part.is_a?(Symbol) ? vars.fetch(part).to_s : part)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|