rgame 0.4.0-x86_64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/lib/rgame/boot.rb +3 -0
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +290 -0
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- metadata +309 -0
|
@@ -0,0 +1,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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# Skipping a drawable that the viewport cannot show.
|
|
6
|
+
#
|
|
7
|
+
# Mixed into the components that draw a node's own footprint, which are the
|
|
8
|
+
# ones that know both where they put it and how big it is.
|
|
9
|
+
#
|
|
10
|
+
# ## Why it is worth doing at all
|
|
11
|
+
#
|
|
12
|
+
# It was not, while the world was drawn once: a handful of skipped draws
|
|
13
|
+
# against a frame's worth of work. Split-screen changes the arithmetic —
|
|
14
|
+
# the world is walked once per viewport, and an actor at one player's end of
|
|
15
|
+
# the map is off the *other* player's view every frame. What used to be a
|
|
16
|
+
# small saving becomes a saving multiplied by the number of people playing.
|
|
17
|
+
#
|
|
18
|
+
# ## Conservative on purpose
|
|
19
|
+
#
|
|
20
|
+
# Culling one frame too eagerly is a sprite popping in at the edge of the
|
|
21
|
+
# screen, which is worse than the draw it saved. So two rules:
|
|
22
|
+
#
|
|
23
|
+
# - **No size means no culling.** `node.width`/`height` are what a drawable's
|
|
24
|
+
# footprint is measured from, and a node that never set them (nothing
|
|
25
|
+
# requires it) reads as 0×0, which would otherwise cull everything
|
|
26
|
+
# instantly. Unknown means draw.
|
|
27
|
+
# - **A rotated node is measured generously.** `Node2D#draw` rotates about the
|
|
28
|
+
# node's own origin, so a rotated footprint can reach further than its
|
|
29
|
+
# box in any direction. The margin is `width + height`, which is always at
|
|
30
|
+
# least the diagonal `hypot(width, height)` and costs no square root on a
|
|
31
|
+
# path that runs once per drawable per viewport.
|
|
32
|
+
module Culling
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# hot-path
|
|
36
|
+
def culled?(view, x, y, width, height)
|
|
37
|
+
return false if width.zero? || height.zero?
|
|
38
|
+
return !view.visible?(x, y, width, height) if node.world_angle.zero?
|
|
39
|
+
|
|
40
|
+
margin = width + height
|
|
41
|
+
!view.visible?(x - margin, y - margin, width + (margin * 2), height + (margin * 2))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# A hard-wired development overlay, toggled by F1 (see RGame::Game), reporting
|
|
6
|
+
# runtime health in the bottom-right corner: frame rate (FPS), the process'
|
|
7
|
+
# cumulative allocated-object count (OBJ), and the objects allocated since the last
|
|
8
|
+
# frame (Δ/f).
|
|
9
|
+
#
|
|
10
|
+
# **Δ/f is the one to watch, and it is a standing guard rather than a
|
|
11
|
+
# diagnostic for one past bug.** A clean per-frame path holds it near zero; a
|
|
12
|
+
# steady nonzero number is a garbage collection being scheduled. The cost is
|
|
13
|
+
# invisible by every other measure — nothing looks wrong, nothing is slower,
|
|
14
|
+
# until a pause lands mid-frame — so without a number on screen it is not
|
|
15
|
+
# noticed at all.
|
|
16
|
+
#
|
|
17
|
+
# Every layer can break it and each has its own way of doing so. Core can
|
|
18
|
+
# allocate in a binding, the engine layer in a component's `draw`, and game
|
|
19
|
+
# code in a scene that builds a string or an array per frame. RuboCop's
|
|
20
|
+
# `Game/NoInterpolationInHotPath` and `Game/NoNeedlessAllocation` catch the
|
|
21
|
+
# shapes they can see in *this* repo; they cannot see a game built on top,
|
|
22
|
+
# and they cannot see an allocation that happens inside a method they think
|
|
23
|
+
# is cheap. This can.
|
|
24
|
+
#
|
|
25
|
+
# Pure: it reads only GC.stat and draws against the renderer interface, so it stays
|
|
26
|
+
# headless-testable. The crux is that it must not allocate while drawing — the values
|
|
27
|
+
# change every frame, so the usual "cache the string, rebuild on change" trick would
|
|
28
|
+
# allocate a String per frame, and the meter would be measuring itself. Instead
|
|
29
|
+
# numbers are drawn digit-by-digit from a fixed set of pre-built single-character
|
|
30
|
+
# strings, which the font caches per glyph.
|
|
31
|
+
class DebugOverlay
|
|
32
|
+
DIGITS = %w[0 1 2 3 4 5 6 7 8 9].freeze
|
|
33
|
+
|
|
34
|
+
FPS_LABEL = 'FPS'
|
|
35
|
+
OBJ_LABEL = 'OBJ'
|
|
36
|
+
DELTA_LABEL = 'Δ/f'
|
|
37
|
+
|
|
38
|
+
COLOR = [80, 255, 120].freeze
|
|
39
|
+
PAD = 8
|
|
40
|
+
GAP = 8
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@visible = false
|
|
44
|
+
@prev_allocated = 0
|
|
45
|
+
@digit_widths = Array.new(10)
|
|
46
|
+
@label_widths = {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def visible? = @visible
|
|
50
|
+
|
|
51
|
+
def toggle
|
|
52
|
+
@visible = !@visible
|
|
53
|
+
@prev_allocated = GC.stat(:total_allocated_objects) if @visible
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Laid out against the view it is drawn into rather than against the
|
|
57
|
+
# window, so it stays in the corner of whatever region it is given. It
|
|
58
|
+
# takes `fps` rather than reading it, for the same reason nothing here
|
|
59
|
+
# reads a clock: the number is measured by the shell that owns the loop
|
|
60
|
+
# and handed down. See CLAUDE.md, "`draw` renders state".
|
|
61
|
+
# Its own `:debug` band, which is the last one, so this lands over every
|
|
62
|
+
# other thing in the frame however the scene is arranged. Not a node, so
|
|
63
|
+
# it opens its own layer rather than being given one by the traversal.
|
|
64
|
+
def draw(renderer, view, fps)
|
|
65
|
+
return unless @visible
|
|
66
|
+
|
|
67
|
+
allocated = GC.stat(:total_allocated_objects)
|
|
68
|
+
delta = allocated - @prev_allocated
|
|
69
|
+
@prev_allocated = allocated
|
|
70
|
+
|
|
71
|
+
line_h = renderer.text_height
|
|
72
|
+
right_x = view.width - PAD
|
|
73
|
+
top_y = view.height - PAD - (line_h * 3)
|
|
74
|
+
|
|
75
|
+
renderer.layered(:debug) do
|
|
76
|
+
draw_line(renderer, FPS_LABEL, fps, right_x, top_y)
|
|
77
|
+
draw_line(renderer, OBJ_LABEL, allocated, right_x, top_y + line_h)
|
|
78
|
+
draw_line(renderer, DELTA_LABEL, delta, right_x, top_y + (line_h * 2))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def draw_line(renderer, label, value, right_x, y)
|
|
85
|
+
number_left = draw_uint(renderer, value, right_x, y)
|
|
86
|
+
renderer.text(label, number_left - GAP - label_width(renderer, label), y, color: COLOR)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def draw_uint(renderer, value, right_x, y)
|
|
90
|
+
x = right_x
|
|
91
|
+
more = true
|
|
92
|
+
while more
|
|
93
|
+
digit = value % 10
|
|
94
|
+
value /= 10
|
|
95
|
+
x -= digit_width(renderer, digit)
|
|
96
|
+
renderer.text(DIGITS[digit], x, y, color: COLOR)
|
|
97
|
+
more = value.positive?
|
|
98
|
+
end
|
|
99
|
+
x
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def digit_width(renderer, digit)
|
|
103
|
+
@digit_widths[digit] ||= renderer.text_width(DIGITS[digit])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def label_width(renderer, label)
|
|
107
|
+
@label_widths[label] ||= renderer.text_width(label)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -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
|