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,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require_relative 'i18n/template'
|
|
5
|
+
require_relative 'i18n/plural'
|
|
6
|
+
require_relative 'i18n/plural_rules'
|
|
7
|
+
|
|
8
|
+
module RGame
|
|
9
|
+
module Engine
|
|
10
|
+
# Translation tables and the current language, as one global module so a
|
|
11
|
+
# node can resolve text from its constructor, before it is in any tree.
|
|
12
|
+
#
|
|
13
|
+
# Tables are read in Rails' YAML format — the top-level key is the locale,
|
|
14
|
+
# nested keys below it — and compiled at load into one flat Hash per locale,
|
|
15
|
+
# keyed by the dotted key, whose values are pre-parsed `%{var}` templates or,
|
|
16
|
+
# for a key whose nested keys are CLDR plural categories, a set of them.
|
|
17
|
+
# `generation` moves whenever what a key resolves to may have changed, so
|
|
18
|
+
# cached text compares one Integer instead of looking anything up.
|
|
19
|
+
#
|
|
20
|
+
# `I18n` parses Strings and never opens a file: finding and reading locale
|
|
21
|
+
# files is the asset manager's job.
|
|
22
|
+
module I18n
|
|
23
|
+
# Raised for a key no link of the chain has, under `missing = :raise`.
|
|
24
|
+
class MissingKey < StandardError
|
|
25
|
+
attr_reader :key, :chain
|
|
26
|
+
|
|
27
|
+
def initialize(key, chain)
|
|
28
|
+
@key = key
|
|
29
|
+
@chain = chain
|
|
30
|
+
super("no translation for #{key.inspect} in #{chain.join(', ')}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Raised under `missing = :raise` when a `Text` declares other variables
|
|
35
|
+
# than its translation uses: a placeholder the `Text` does not declare, or
|
|
36
|
+
# a declared name the translation never prints.
|
|
37
|
+
class VariableMismatch < StandardError
|
|
38
|
+
attr_reader :key
|
|
39
|
+
|
|
40
|
+
def initialize(key, message)
|
|
41
|
+
@key = key
|
|
42
|
+
super("#{key}: #{message}")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
MISSING_POLICIES = %i[key raise].freeze
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# An Integer that moves on every `load`, every change of locale or
|
|
50
|
+
# default, every `plural_rule`, and every `reset`.
|
|
51
|
+
attr_reader :generation
|
|
52
|
+
|
|
53
|
+
attr_reader :locale, :default
|
|
54
|
+
|
|
55
|
+
# The locales a key is looked up in, in order: the current locale, each
|
|
56
|
+
# shorter prefix of it, then the default — `[:'de-AT', :de, :en]`.
|
|
57
|
+
# Rebuilt when the locale or the default changes, not per lookup.
|
|
58
|
+
attr_reader :chain
|
|
59
|
+
|
|
60
|
+
# What a key no link of the chain has resolves to: `:key` (the start)
|
|
61
|
+
# shows the key itself, `:raise` raises `MissingKey`, and a callable is
|
|
62
|
+
# called with the key and the chain and its return value is shown.
|
|
63
|
+
attr_reader :missing
|
|
64
|
+
|
|
65
|
+
# Merges a YAML document in Rails' format into the loaded tables. One
|
|
66
|
+
# document may hold several locales, and a locale loaded twice is merged
|
|
67
|
+
# key by key rather than replaced. `source` names the file in errors.
|
|
68
|
+
def load(yaml, source: nil)
|
|
69
|
+
merge(YAML.safe_load(yaml, aliases: true, filename: source), source || 'translations')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `load` for a Hash already in memory: `load_hash(en: { menu: { title: 'Menu' } })`.
|
|
73
|
+
def load_hash(hash) = merge(hash, 'translations')
|
|
74
|
+
|
|
75
|
+
# Switches the language. `de_AT`, `'de-at'` and `:'de-AT'` name the same
|
|
76
|
+
# locale. A locale with no table of its own is allowed and resolves
|
|
77
|
+
# through its chain, so `:'de-CH'` reads the `de` table.
|
|
78
|
+
def locale=(locale)
|
|
79
|
+
locale = normalize(locale)
|
|
80
|
+
return if locale == @locale
|
|
81
|
+
|
|
82
|
+
@locale = locale
|
|
83
|
+
relink
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The last link of every chain, and the locale `choose` falls back to.
|
|
87
|
+
def default=(locale)
|
|
88
|
+
locale = normalize(locale)
|
|
89
|
+
return if locale == @default
|
|
90
|
+
|
|
91
|
+
@default = locale
|
|
92
|
+
relink
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The canonical Symbol for a locale identifier: the language lowercase,
|
|
96
|
+
# a region uppercase, a script capitalized, joined by hyphens —
|
|
97
|
+
# `normalize('zh_hant_tw') # => :'zh-Hant-TW'`.
|
|
98
|
+
def normalize(locale)
|
|
99
|
+
language, *subtags = locale.to_s.split(/[-_]/)
|
|
100
|
+
raise ArgumentError, "not a locale: #{locale.inspect}" if language.nil? || language.empty?
|
|
101
|
+
|
|
102
|
+
subtags.map! { |subtag| subtag.length == 4 ? subtag.capitalize : subtag.upcase }
|
|
103
|
+
subtags.unshift(language.downcase).join('-').to_sym
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The first of `preferred` — the player's languages, most wanted first —
|
|
107
|
+
# that has a table for itself or a shorter prefix of itself, normalized
|
|
108
|
+
# but not shortened: `choose(['fr-CA', 'de-AT'])` with a `de` table is
|
|
109
|
+
# `:'de-AT'`. The default when none has.
|
|
110
|
+
def choose(preferred)
|
|
111
|
+
preferred.each do |candidate|
|
|
112
|
+
locale = normalize(candidate)
|
|
113
|
+
return locale if lineage(locale).any? { |link| @tables.key?(link) }
|
|
114
|
+
end
|
|
115
|
+
@default
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def missing=(policy)
|
|
119
|
+
unless MISSING_POLICIES.include?(policy) || policy.respond_to?(:call)
|
|
120
|
+
raise ArgumentError, "missing must be :key, :raise or a callable, not #{policy.inspect}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
@missing = policy
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The keys the default locale has that no table in `locale`'s own chain
|
|
127
|
+
# does, in the default table's order. A key `locale` gets from a parent
|
|
128
|
+
# (`de-AT` from `de`) is not missing; one it would get only from the
|
|
129
|
+
# default is.
|
|
130
|
+
def missing_keys(locale)
|
|
131
|
+
links = lineage(normalize(locale))
|
|
132
|
+
defaults = @tables.fetch(@default, {}).keys
|
|
133
|
+
defaults.reject { |key| links.any? { |link| @tables[link]&.key?(key) } }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Sets how `language` (or a regional locale, which then wins over its
|
|
137
|
+
# language) sorts a count into a plural category. The block receives
|
|
138
|
+
# the count and returns one of `Plural::CATEGORIES`. Replaces a built-in
|
|
139
|
+
# rule; lasts until `reset`.
|
|
140
|
+
def plural_rule(language, &rule)
|
|
141
|
+
raise ArgumentError, 'plural_rule needs a block' unless rule
|
|
142
|
+
|
|
143
|
+
@plural_rules[normalize(language)] = rule
|
|
144
|
+
@rule_for.clear
|
|
145
|
+
@generation += 1
|
|
146
|
+
self
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The locales that have a table, in load order.
|
|
150
|
+
def available = @tables.keys
|
|
151
|
+
|
|
152
|
+
# The translation of `key` (or `scope.key`) with `vars` interpolated. A
|
|
153
|
+
# pluralized key needs `count:`, and picks its form by the plural rule
|
|
154
|
+
# of the table that supplied it, which is not always the current locale.
|
|
155
|
+
# Allocates on every call: it is for code off the per-frame path.
|
|
156
|
+
def t(key, scope: nil, **vars)
|
|
157
|
+
key = scope ? "#{scope}.#{key}" : key.to_s
|
|
158
|
+
template = lookup(key)
|
|
159
|
+
return answer_missing(key) unless template
|
|
160
|
+
|
|
161
|
+
template = pluralize(key, template, vars) if template.is_a?(Plural)
|
|
162
|
+
|
|
163
|
+
missing = template.names.find { |name| !vars.key?(name) }
|
|
164
|
+
raise ArgumentError, "#{key} needs %{#{missing}}" if missing
|
|
165
|
+
|
|
166
|
+
template.render(vars)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# What a `Text` declaring `names` shows for the dotted `key`, with `vars`
|
|
170
|
+
# — a Hash holding exactly those names — interpolated. It differs from
|
|
171
|
+
# `t` in one way: a translation whose placeholders are not the declared
|
|
172
|
+
# names is a bug in the table or the code, and the missing policy
|
|
173
|
+
# answers it, raising `VariableMismatch` under `:raise`.
|
|
174
|
+
def render(key, names, vars)
|
|
175
|
+
entry = lookup(key)
|
|
176
|
+
return answer_missing(key) unless entry
|
|
177
|
+
|
|
178
|
+
problem = mismatch(entry, names)
|
|
179
|
+
return answer_mismatch(key, problem) if problem
|
|
180
|
+
|
|
181
|
+
entry = pluralize(key, entry, vars) if entry.is_a?(Plural)
|
|
182
|
+
entry.render(vars)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Forgets every table and plural rule added, and restores the starting
|
|
186
|
+
# locale and default, `:en`, and the `:key` missing policy. The
|
|
187
|
+
# generation moves rather than restarting, so text cached before a
|
|
188
|
+
# reset never mistakes itself for current.
|
|
189
|
+
def reset
|
|
190
|
+
@tables = {}
|
|
191
|
+
@sources = {}
|
|
192
|
+
@locale = :en
|
|
193
|
+
@default = :en
|
|
194
|
+
@generation = (@generation || 0) + 1
|
|
195
|
+
@chain = chain_for(@locale)
|
|
196
|
+
@plural_rules = PluralRules::BUILT_IN.dup
|
|
197
|
+
@rule_for = {}
|
|
198
|
+
@missing = :key
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def relink
|
|
204
|
+
@chain = chain_for(@locale)
|
|
205
|
+
@generation += 1
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def chain_for(locale) = (lineage(locale) << @default).uniq.freeze
|
|
209
|
+
|
|
210
|
+
def lineage(locale)
|
|
211
|
+
subtags = locale.name.split('-')
|
|
212
|
+
subtags.size.downto(1).map { |length| subtags.first(length).join('-').to_sym }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def answer_missing(key)
|
|
216
|
+
case @missing
|
|
217
|
+
when :key then key
|
|
218
|
+
when :raise then raise MissingKey.new(key, @chain)
|
|
219
|
+
else @missing.call(key, @chain)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def mismatch(entry, names)
|
|
224
|
+
undeclared = entry.names.find { |name| !names.include?(name) }
|
|
225
|
+
if undeclared == :count && entry.is_a?(Plural)
|
|
226
|
+
'is pluralized, and the Text does not declare :count'
|
|
227
|
+
elsif undeclared
|
|
228
|
+
"uses %{#{undeclared}}, which the Text does not declare"
|
|
229
|
+
elsif (unused = names.find { |name| !entry.names.include?(name) })
|
|
230
|
+
"never uses :#{unused}, which the Text declares"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def answer_mismatch(key, problem)
|
|
235
|
+
case @missing
|
|
236
|
+
when :key then key
|
|
237
|
+
when :raise then raise VariableMismatch.new(key, problem)
|
|
238
|
+
else @missing.call(key, @chain)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def pluralize(key, plural, vars)
|
|
243
|
+
raise ArgumentError, "#{key} is pluralized and needs count:" unless vars.key?(:count)
|
|
244
|
+
|
|
245
|
+
count = vars[:count]
|
|
246
|
+
plural.template_for(count, plural_category(plural.locale, count))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def plural_category(locale, count)
|
|
250
|
+
rule = @rule_for[locale] ||= begin
|
|
251
|
+
language = lineage(locale).find { |link| @plural_rules.key?(link) }
|
|
252
|
+
@plural_rules.fetch(language, PluralRules::DEFAULT)
|
|
253
|
+
end
|
|
254
|
+
rule.call(count)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def lookup(key)
|
|
258
|
+
@chain.each do |link|
|
|
259
|
+
entry = @tables[link]&.[](key)
|
|
260
|
+
return entry if entry
|
|
261
|
+
end
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def merge(hash, source)
|
|
266
|
+
raise ArgumentError, "#{source}: expected a Hash of locales" unless hash.is_a?(Hash)
|
|
267
|
+
|
|
268
|
+
hash.each do |locale, entries|
|
|
269
|
+
raise ArgumentError, "#{source}: #{locale} must hold a Hash of keys" unless entries.is_a?(Hash)
|
|
270
|
+
|
|
271
|
+
locale = normalize(locale)
|
|
272
|
+
@sources[locale] = deep_merge(@sources.fetch(locale, {}), stringify(entries, locale.to_s, source))
|
|
273
|
+
@tables[locale] = compile(@sources[locale], locale)
|
|
274
|
+
end
|
|
275
|
+
@generation += 1
|
|
276
|
+
self
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def stringify(entries, path, source)
|
|
280
|
+
entries.to_h do |name, value|
|
|
281
|
+
name = scalar_name(name, path, source)
|
|
282
|
+
here = "#{path}.#{name}"
|
|
283
|
+
[name, value.is_a?(Hash) ? stringify(value, here, source) : scalar_value(value, here, source)]
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def scalar_name(name, path, source)
|
|
288
|
+
return name.to_s if name.is_a?(String) || name.is_a?(Symbol) || name.is_a?(Integer)
|
|
289
|
+
|
|
290
|
+
raise ArgumentError, "#{source}: #{path} has the key #{name.inspect}; " \
|
|
291
|
+
'YAML reads yes/no/on/off/true/false/~ unquoted, so quote it'
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def scalar_value(value, path, source)
|
|
295
|
+
return value.to_s if value.is_a?(String) || value.is_a?(Numeric)
|
|
296
|
+
|
|
297
|
+
raise ArgumentError, "#{source}: #{path} is #{value.inspect}, not text; " \
|
|
298
|
+
'quote it if it is meant as a string'
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def deep_merge(into, from)
|
|
302
|
+
into.merge(from) do |_name, old, new|
|
|
303
|
+
old.is_a?(Hash) && new.is_a?(Hash) ? deep_merge(old, new) : new
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def compile(entries, locale, prefix = nil, table = {})
|
|
308
|
+
entries.each do |name, value|
|
|
309
|
+
key = prefix ? "#{prefix}.#{name}" : name
|
|
310
|
+
if !value.is_a?(Hash) then table[key.freeze] = Template.compile(value)
|
|
311
|
+
elsif Plural.forms?(value) then table[key.freeze] = Plural.new(locale, value)
|
|
312
|
+
else compile(value, locale, key, table)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
prefix ? table : table.freeze
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
reset
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# Polls one player's device through an InputMap and produces their Actions
|
|
6
|
+
# snapshot.
|
|
7
|
+
#
|
|
8
|
+
# mapper = ActionMapper.new(input_map, device: Controls.gamepad(0))
|
|
9
|
+
# actions = mapper.poll(input)
|
|
10
|
+
#
|
|
11
|
+
# **One of these per player.** The device is what makes that work: every
|
|
12
|
+
# query carries it, so two mappers over the same map read two different
|
|
13
|
+
# controllers, and each keeps its own previous-frame state so their edge
|
|
14
|
+
# queries are independent.
|
|
15
|
+
#
|
|
16
|
+
# Pure logic. The backend is duck-typed and the whole interface is
|
|
17
|
+
# `down?(physical_id, device:)` and `axis(axis_id, device:)` — a spec passes
|
|
18
|
+
# a fake and a game passes RGame::Core::Input.
|
|
19
|
+
class ActionMapper
|
|
20
|
+
DEAD_ZONE = 0.15
|
|
21
|
+
|
|
22
|
+
attr_reader :map, :actions
|
|
23
|
+
attr_accessor :device, :dead_zone
|
|
24
|
+
|
|
25
|
+
def initialize(map, device: RGame::Util::Controls::KEYBOARD, dead_zone: DEAD_ZONE)
|
|
26
|
+
@map = map
|
|
27
|
+
@device = device
|
|
28
|
+
@dead_zone = dead_zone
|
|
29
|
+
|
|
30
|
+
@held = {}
|
|
31
|
+
@prev_held = {}
|
|
32
|
+
@axes = {}
|
|
33
|
+
map.bindings.each_key do |name|
|
|
34
|
+
@held[name] = false
|
|
35
|
+
@prev_held[name] = false
|
|
36
|
+
@axes[name] = 0.0
|
|
37
|
+
end
|
|
38
|
+
@actions = Actions.new(held: @held, axes: @axes, prev_held: @prev_held)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def poll(backend)
|
|
42
|
+
@held.each { |name, down| @prev_held[name] = down }
|
|
43
|
+
|
|
44
|
+
return rest if @device.nil?
|
|
45
|
+
|
|
46
|
+
@map.bindings.each do |name, binding|
|
|
47
|
+
@held[name] = any_down?(backend, binding.buttons) if binding.buttons
|
|
48
|
+
@axes[name] = axis_value(backend, binding) if binding.pairs || binding.stick
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@actions
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def rest
|
|
57
|
+
@held.each_key { |name| @held[name] = false }
|
|
58
|
+
@axes.each_key { |name| @axes[name] = 0.0 }
|
|
59
|
+
@actions
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# hot-path
|
|
63
|
+
def any_down?(backend, ids)
|
|
64
|
+
ids.any? { |id| backend.down?(id, device: @device) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# hot-path
|
|
68
|
+
def axis_value(backend, binding)
|
|
69
|
+
digital = digital_axis(backend, binding)
|
|
70
|
+
return digital unless binding.stick
|
|
71
|
+
|
|
72
|
+
analog = dead_zoned(backend.axis(binding.stick, device: @device))
|
|
73
|
+
digital.abs >= analog.abs ? digital : analog
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# hot-path
|
|
77
|
+
def digital_axis(backend, binding)
|
|
78
|
+
pairs = binding.pairs
|
|
79
|
+
return 0.0 if pairs.nil?
|
|
80
|
+
|
|
81
|
+
best = 0.0
|
|
82
|
+
pairs.each do |pair|
|
|
83
|
+
value = (backend.down?(pair[1], device: @device) ? 1.0 : 0.0) -
|
|
84
|
+
(backend.down?(pair[0], device: @device) ? 1.0 : 0.0)
|
|
85
|
+
best = value if value.abs > best.abs
|
|
86
|
+
end
|
|
87
|
+
best
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# hot-path
|
|
91
|
+
def dead_zoned(value)
|
|
92
|
+
magnitude = value.abs
|
|
93
|
+
return 0.0 if magnitude <= @dead_zone
|
|
94
|
+
|
|
95
|
+
scaled = (magnitude - @dead_zone) / (1.0 - @dead_zone)
|
|
96
|
+
value.negative? ? -scaled : scaled
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# An immutable per-frame snapshot of abstract action state. Game logic reads
|
|
6
|
+
# this, never physical keys. Built by ActionMapper (or constructed directly
|
|
7
|
+
# in a spec).
|
|
8
|
+
#
|
|
9
|
+
# Edge queries (`pressed?`/`released?`) compare against the previous frame's
|
|
10
|
+
# held state, so a one-shot action (menu confirm, jump) fires exactly once
|
|
11
|
+
# per press rather than every frame it is held.
|
|
12
|
+
#
|
|
13
|
+
# ## Reading an action nobody declared raises
|
|
14
|
+
#
|
|
15
|
+
# A game declares its actions once, in an InputMap. Asking for one that is
|
|
16
|
+
# not in it is a typo, not a question with an answer, so it raises KeyError
|
|
17
|
+
# rather than reading `false` forever:
|
|
18
|
+
#
|
|
19
|
+
# actions.pressed?(:fyre) # KeyError: no such action :fyre
|
|
20
|
+
#
|
|
21
|
+
# This matters because the failure it replaces is silent and remote. A
|
|
22
|
+
# misspelled action reads as "never pressed", and what the player sees is a
|
|
23
|
+
# button that does nothing — a bug that looks like it lives in the code that
|
|
24
|
+
# *would* have run. RGame::Core::Input used to raise KeyError for an unbound
|
|
25
|
+
# action and no longer can: it takes physical ids now, and binding moved up
|
|
26
|
+
# to InputMap. This is where that guarantee went.
|
|
27
|
+
#
|
|
28
|
+
# **The hashes are the declaration.** ActionMapper seeds all three from its
|
|
29
|
+
# map at construction, so every action the map knows answers and nothing
|
|
30
|
+
# else does. A spec constructing one directly declares whatever it passes:
|
|
31
|
+
#
|
|
32
|
+
# Actions.new(axes: { move_x: 1.0, move_y: 0.0 }) # both answer
|
|
33
|
+
# Actions.new(axes: { move_x: 1.0 }).axis(:move_y) # KeyError
|
|
34
|
+
#
|
|
35
|
+
# That is stricter than it needs to be for a spec, and deliberately: a spec
|
|
36
|
+
# that has not said what the action set is cannot claim a component reads
|
|
37
|
+
# the right part of it.
|
|
38
|
+
class Actions
|
|
39
|
+
# `held`, `axes` and `prev_held` are mutable hashes the mapper updates in
|
|
40
|
+
# place each poll, so the snapshot stays a single reused, allocation-free
|
|
41
|
+
# object.
|
|
42
|
+
def initialize(held: {}, axes: {}, prev_held: {})
|
|
43
|
+
@held = held
|
|
44
|
+
@axes = axes
|
|
45
|
+
@prev_held = prev_held
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Every action this snapshot can answer for.
|
|
49
|
+
def declared = @held.keys | @axes.keys
|
|
50
|
+
|
|
51
|
+
# A snapshot is also a degenerate input *source*: it answers for whichever
|
|
52
|
+
# player is asking, because there is only one answer. That is what lets
|
|
53
|
+
# `node.control(actions)` keep working unchanged — a tree with nobody
|
|
54
|
+
# claiming ownership, or a spec that has only one player in mind, passes
|
|
55
|
+
# the snapshot itself where a Players registry would otherwise go.
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
# hot-path
|
|
59
|
+
def actions_for(_player) = self
|
|
60
|
+
|
|
61
|
+
# hot-path
|
|
62
|
+
def held?(name)
|
|
63
|
+
@held.fetch(name) { undeclared(name) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# True only on the frame the action transitions up→down.
|
|
67
|
+
# hot-path
|
|
68
|
+
def pressed?(name)
|
|
69
|
+
@held.fetch(name) { undeclared(name) } && !@prev_held.fetch(name, false)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# True only on the frame the action transitions down→up.
|
|
73
|
+
# hot-path
|
|
74
|
+
def released?(name)
|
|
75
|
+
!@held.fetch(name) { undeclared(name) } && @prev_held.fetch(name, false)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Analog value in [-1.0, 1.0]; 0.0 for a declared action at rest.
|
|
79
|
+
# hot-path
|
|
80
|
+
def axis(name)
|
|
81
|
+
@axes.fetch(name) { undeclared(name) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def undeclared(name)
|
|
87
|
+
raise KeyError, "no such action #{name.inspect} — declare it in the InputMap " \
|
|
88
|
+
"(this snapshot has #{declared.inspect})"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|