rgame 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Signals — a node announcing that something happened, to nobody in particular.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/signals/main.rb
|
|
8
|
+
#
|
|
9
|
+
# **Enter** stands on the plate. Hold **Space** and hold **E**: both feed one
|
|
10
|
+
# component, at different rates. It exercises:
|
|
11
|
+
# - Signal::DSL — `signal :on_pressed` on a node of your own;
|
|
12
|
+
# - Signal.define(:field) — a signal that carries a payload;
|
|
13
|
+
# - Components::ActionTrigger — one component, several actions, one signal;
|
|
14
|
+
# - the connect handle, and the one place in the engine that can use it.
|
|
15
|
+
#
|
|
16
|
+
# ## The plate does not know what a door is
|
|
17
|
+
#
|
|
18
|
+
# That sentence is the whole reason signals exist. A plate that opened a door
|
|
19
|
+
# would have to be handed a door, which means whatever built the plate would have
|
|
20
|
+
# to know about doors, and a plate with nothing to open would be a special case
|
|
21
|
+
# to write. Instead the plate says *that it was pressed* and stops:
|
|
22
|
+
#
|
|
23
|
+
# on_pressed_signal.emit
|
|
24
|
+
#
|
|
25
|
+
# The door and the lamp connect to it, and neither appears anywhere in the
|
|
26
|
+
# plate. Adding a third thing that reacts is one line, in the third thing.
|
|
27
|
+
#
|
|
28
|
+
# ## Declaring one is a single line
|
|
29
|
+
#
|
|
30
|
+
# class Plate < RGame::Engine::Node2D
|
|
31
|
+
# signal :on_pressed
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# `Node2D` extends `Signal::DSL`, so every node and every component has this
|
|
35
|
+
# available. That one line generates two methods:
|
|
36
|
+
#
|
|
37
|
+
# `on_pressed { ... }` public — subscribe, and get a handle back
|
|
38
|
+
# `on_pressed_signal` private — the Signal itself, to emit on
|
|
39
|
+
#
|
|
40
|
+
# **Emitting is private on purpose.** Only the plate can say the plate was
|
|
41
|
+
# pressed. That is the same "give the user a blank hook, keep the machinery
|
|
42
|
+
# separate" rule that makes `on_draw` overridable while `draw` is not: the thing
|
|
43
|
+
# a stranger is invited to do is the safe one.
|
|
44
|
+
#
|
|
45
|
+
# ## A payload, for signals that have something to say
|
|
46
|
+
#
|
|
47
|
+
# `signal :on_pressed` carries nothing, because "it happened" is all there is.
|
|
48
|
+
# `Signal.define(:action)` builds one that carries a field, and
|
|
49
|
+
# `Components::ActionTrigger` uses exactly that:
|
|
50
|
+
#
|
|
51
|
+
# signal :on_triggered, Engine::Signal.define(:action)
|
|
52
|
+
#
|
|
53
|
+
# The engine allows one component of a class per node, so a node that wanted a
|
|
54
|
+
# separate component per action could not have one. Emitting the action name
|
|
55
|
+
# instead lets a single trigger serve `fire` and `poke` together, and the
|
|
56
|
+
# listener filters. Hold both keys here and watch the two counters move at
|
|
57
|
+
# different rates: one component, two cooldowns.
|
|
58
|
+
#
|
|
59
|
+
# ## The handle, and why almost nothing uses it
|
|
60
|
+
#
|
|
61
|
+
# `on_pressed { ... }` hands back a handle, and there is no public way to give it
|
|
62
|
+
# back — `on_pressed_signal` is private, and `disconnect` lives on the Signal.
|
|
63
|
+
# That is deliberate rather than missing. **A node's listeners die with the
|
|
64
|
+
# node**, so a subtree that goes away takes its subscriptions with it and there
|
|
65
|
+
# is nothing to leak.
|
|
66
|
+
#
|
|
67
|
+
# The exception is the one signal that outlives everything. `Engine::AudioBus` is
|
|
68
|
+
# a *module*, so it is built the other way round — it exposes its Signals
|
|
69
|
+
# publicly rather than through the DSL — and `Engine::AudioDirector` disconnects
|
|
70
|
+
# from them when it is released:
|
|
71
|
+
#
|
|
72
|
+
# @bus.on_play_sound.disconnect(@handles[0])
|
|
73
|
+
#
|
|
74
|
+
# It has to. A module-level hub holds its listeners for as long as the process
|
|
75
|
+
# runs, and the one that was forgotten there held the audio device, which held
|
|
76
|
+
# the asset manager, which held the window. Reach for the public-Signal shape
|
|
77
|
+
# when a hub outlives its subscribers, and for the DSL everywhere else.
|
|
78
|
+
#
|
|
79
|
+
# ## What it does not solve
|
|
80
|
+
#
|
|
81
|
+
# Ordering. Listeners are called in the order they connected, and nothing here
|
|
82
|
+
# offers a priority — if two reactions must happen in a particular order, the
|
|
83
|
+
# thing that cares should own both rather than connecting twice.
|
|
84
|
+
|
|
85
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
86
|
+
require 'rgame/game'
|
|
87
|
+
|
|
88
|
+
Controls = RGame::Util::Controls
|
|
89
|
+
|
|
90
|
+
WIDTH = 640
|
|
91
|
+
HEIGHT = 480
|
|
92
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
93
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
94
|
+
|
|
95
|
+
# A plate that announces presses. It has no idea anything is listening, and
|
|
96
|
+
# nothing below it in this file is named anywhere inside it.
|
|
97
|
+
class Plate < RGame::Engine::Node2D
|
|
98
|
+
signal :on_pressed
|
|
99
|
+
|
|
100
|
+
PLATE_W = 150
|
|
101
|
+
PLATE_H = 34
|
|
102
|
+
UP = RGame::Util::Color.new(110, 120, 140)
|
|
103
|
+
DOWN = RGame::Util::Color.new(210, 190, 110)
|
|
104
|
+
|
|
105
|
+
def initialize(**)
|
|
106
|
+
super(width: PLATE_W, height: PLATE_H, **)
|
|
107
|
+
@down = false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def on_control(actions)
|
|
111
|
+
@down = actions.held?(:stand_on_plate)
|
|
112
|
+
# An edge, not the held state: one press is one event, however long the key
|
|
113
|
+
# stays down. The Signal has no opinion on that — deciding when a thing has
|
|
114
|
+
# happened is the emitter's job.
|
|
115
|
+
on_pressed_signal.emit if actions.pressed?(:stand_on_plate)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def on_draw(renderer, _view)
|
|
119
|
+
renderer.rect(0, 0, PLATE_W, PLATE_H, color: @down ? DOWN : UP)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Reacts by opening. It knows about the plate; the plate does not know about it.
|
|
124
|
+
class Door < RGame::Engine::Node2D
|
|
125
|
+
FRAME = RGame::Util::Color.new(70, 78, 96)
|
|
126
|
+
LEAF = RGame::Util::Color.new(180, 140, 96)
|
|
127
|
+
DOOR_W = 64
|
|
128
|
+
DOOR_H = 96
|
|
129
|
+
SPEED = 2.2 # fractions of the doorway per second
|
|
130
|
+
|
|
131
|
+
def initialize(plate:, **)
|
|
132
|
+
super(width: DOOR_W, height: DOOR_H, **)
|
|
133
|
+
@plate = plate
|
|
134
|
+
@open = false
|
|
135
|
+
@openness = 0.0
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Connecting is the whole of the wiring, and it happens here rather than in
|
|
139
|
+
# `initialize` because the plate has to exist first — which in this scene it
|
|
140
|
+
# does, but a listener built outside the tree would not be able to rely on it.
|
|
141
|
+
def on_add = @plate.on_pressed { @open = !@open }
|
|
142
|
+
|
|
143
|
+
def on_update(dt)
|
|
144
|
+
target = @open ? 1.0 : 0.0
|
|
145
|
+
step = SPEED * dt
|
|
146
|
+
@openness += (target - @openness).clamp(-step, step)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def on_draw(renderer, _view)
|
|
150
|
+
renderer.rect(0, 0, DOOR_W, DOOR_H, color: FRAME)
|
|
151
|
+
# The leaf slides up into the frame, so an open door is a short one.
|
|
152
|
+
leaf = DOOR_H * (1.0 - @openness)
|
|
153
|
+
renderer.rect(0, DOOR_H - leaf, DOOR_W, leaf, color: LEAF) if leaf > 1.0
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Reacts by lighting, and knows nothing about the door reacting to the same
|
|
158
|
+
# thing. Two listeners on one signal, neither aware of the other.
|
|
159
|
+
class Lamp < RGame::Engine::Node2D
|
|
160
|
+
OFF = RGame::Util::Color.new(64, 62, 56)
|
|
161
|
+
ON = RGame::Util::Color.new(255, 226, 130)
|
|
162
|
+
RADIUS = 22
|
|
163
|
+
FADE = 1.6
|
|
164
|
+
|
|
165
|
+
def initialize(plate:, **)
|
|
166
|
+
super(width: RADIUS * 2, height: RADIUS * 2, **)
|
|
167
|
+
@plate = plate
|
|
168
|
+
@glow = 0.0
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def on_add = @plate.on_pressed { @glow = 1.0 }
|
|
172
|
+
|
|
173
|
+
# The glow is state advanced by dt, not a clock read while drawing — the
|
|
174
|
+
# standing rule, and the reason pausing this node would freeze it mid-fade.
|
|
175
|
+
def on_update(dt)
|
|
176
|
+
@glow -= dt * FADE
|
|
177
|
+
@glow = 0.0 if @glow.negative?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def on_draw(renderer, _view)
|
|
181
|
+
renderer.circle(0, 0, RADIUS, color: @glow.positive? ? ON : OFF)
|
|
182
|
+
renderer.circle(0, 0, RADIUS * @glow, color: ON) if @glow.positive?
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# One component, two actions, one signal carrying which. The counters are
|
|
187
|
+
# separate so the two cooldowns are visible as different rates rather than
|
|
188
|
+
# described as them.
|
|
189
|
+
class Repeater < RGame::Engine::Node2D
|
|
190
|
+
FIRE_COOLDOWN = 0.20
|
|
191
|
+
POKE_COOLDOWN = 0.55
|
|
192
|
+
PIP = 12
|
|
193
|
+
GAP = 5
|
|
194
|
+
ROW = 26
|
|
195
|
+
FIRE_COLOR = RGame::Util::Color.new(235, 120, 100)
|
|
196
|
+
POKE_COLOR = RGame::Util::Color.new(120, 200, 255)
|
|
197
|
+
MAX_PIPS = 28
|
|
198
|
+
|
|
199
|
+
def initialize(**)
|
|
200
|
+
super
|
|
201
|
+
@counts = { fire: 0, poke: 0 }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def on_add
|
|
205
|
+
trigger = add_component(
|
|
206
|
+
RGame::Engine::Components::ActionTrigger.new(fire: FIRE_COOLDOWN, poke: POKE_COOLDOWN)
|
|
207
|
+
)
|
|
208
|
+
# The payload is what makes one listener enough. Without it this would need
|
|
209
|
+
# a signal per action, and the component is allowed only one of itself per
|
|
210
|
+
# node to hang them on.
|
|
211
|
+
trigger.on_triggered { |action| @counts[action] += 1 }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def on_draw(renderer, _view)
|
|
215
|
+
draw_row(renderer, @counts[:fire], 0, FIRE_COLOR)
|
|
216
|
+
draw_row(renderer, @counts[:poke], ROW, POKE_COLOR)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
# Counted in rectangles rather than in text, because a row makes two rates
|
|
222
|
+
# comparable at a glance in a way two numbers do not.
|
|
223
|
+
# hot-path
|
|
224
|
+
def draw_row(renderer, count, y, color)
|
|
225
|
+
[count, MAX_PIPS].min.times do |i|
|
|
226
|
+
renderer.rect(i * (PIP + GAP), y, PIP, PIP, color: color)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
class Scene < RGame::Engine::Node2D
|
|
232
|
+
BACKDROP = RGame::Util::Color.new(30, 34, 44)
|
|
233
|
+
|
|
234
|
+
def initialize
|
|
235
|
+
super
|
|
236
|
+
@help_plate = RGame::Engine::Text.new('help.plate')
|
|
237
|
+
@help_unnamed = RGame::Engine::Text.new('help.unnamed')
|
|
238
|
+
@help_cooldowns = RGame::Engine::Text.new('help.cooldowns')
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def on_add
|
|
242
|
+
plate = add_node(Plate.new(x: 60, y: 150))
|
|
243
|
+
# Both are handed the plate and connect themselves. The plate is handed
|
|
244
|
+
# nothing at all.
|
|
245
|
+
add_node(Door.new(plate: plate, x: 300, y: 110))
|
|
246
|
+
add_node(Lamp.new(plate: plate, x: 480, y: 158))
|
|
247
|
+
add_node(Repeater.new(x: 60, y: 330))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def on_draw(renderer, view)
|
|
251
|
+
renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
|
|
252
|
+
|
|
253
|
+
renderer.text(@help_plate, 12, 12)
|
|
254
|
+
renderer.text(@help_unnamed, 12, 34)
|
|
255
|
+
renderer.text(@help_cooldowns, 12, 290)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
game = RGame::Game.new(
|
|
260
|
+
root: Scene.new,
|
|
261
|
+
caption: 'Signals',
|
|
262
|
+
width: WIDTH,
|
|
263
|
+
height: HEIGHT,
|
|
264
|
+
media_root: ASSETS,
|
|
265
|
+
locales: LOCALES,
|
|
266
|
+
# `stand_on_plate` rather than reading `ui_confirm` directly, and the reason is
|
|
267
|
+
# the trap that costs an afternoon: **a key already in the default map keeps
|
|
268
|
+
# doing its old job too.** `ui_confirm` is Enter *and* Space, and Space is
|
|
269
|
+
# `fire`, which the ActionTrigger below reads — so a plate on `ui_confirm`
|
|
270
|
+
# would also be pressed by the key that is supposed to only feed the repeater.
|
|
271
|
+
# Two actions may read one key, both fire, and nothing warns.
|
|
272
|
+
input_map: RGame::Engine::InputMap.default.merge(
|
|
273
|
+
stand_on_plate: { buttons: [Controls::KEY_RETURN] },
|
|
274
|
+
poke: { buttons: [Controls::KEY_E] }
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
game.start
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
bar: Left and right move, Enter (or A) uses — or press 1 to 5
|
|
5
|
+
status:
|
|
6
|
+
focused: "Focused: %{skill}"
|
|
7
|
+
used: "Used: %{skill}"
|
|
8
|
+
nothing_used: "Used: nothing yet"
|
|
9
|
+
skills:
|
|
10
|
+
wand: Wand
|
|
11
|
+
wrench: Wrench
|
|
12
|
+
torch: Torch
|
|
13
|
+
hammer: Hammer
|
|
14
|
+
watering_can: Watering can
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Skill bar — a row of tools, stepped through like a menu and fired by hotkey.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/skill_bar/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Left and right (or the d-pad) move along the bar, and Enter or Space (or A)
|
|
10
|
+
# uses the focused tool. The number keys 1 to 5 use a tool directly, wherever the
|
|
11
|
+
# focus is. Every use clicks. It exercises:
|
|
12
|
+
# - UI::Row — a UI::Menu layout that lines its buttons up side by side;
|
|
13
|
+
# - UI::Stepping — the default navigation, which takes its axis from the
|
|
14
|
+
# layout, so a Row steps with left and right with nothing else said;
|
|
15
|
+
# - UI::Button's `hotkey:` — an action that presses one button, focused or not;
|
|
16
|
+
# - `activate_on: :press` — a use happens on the way down, as a skill should;
|
|
17
|
+
# - UI::IconButton with a caption, on a disc UI::ShapeStyle — a picture with
|
|
18
|
+
# its name underneath;
|
|
19
|
+
# - InputMap.default.merge — declaring the five hotkey actions;
|
|
20
|
+
# - a UI atlas's `images` — the five tools, cut from one strip by name.
|
|
21
|
+
#
|
|
22
|
+
# ## Two ways to press one button
|
|
23
|
+
#
|
|
24
|
+
# A farming sim's tool bar and a combat game's arts are the same menu: a row of
|
|
25
|
+
# buttons, one of them focused. What makes it a *bar* is that each button can
|
|
26
|
+
# also be reached without moving the focus at all. Press 5 with the Torch
|
|
27
|
+
# focused and the Watering can is used, drawn pressed for a moment, and the
|
|
28
|
+
# focus stays on the Torch — a hotkey is a second way to press a button, not a
|
|
29
|
+
# way to move to it.
|
|
30
|
+
#
|
|
31
|
+
# The two ways do not add up. Holding a number key uses the tool once, not once a
|
|
32
|
+
# frame; pressing a tool's number while Enter is already pressing it uses it
|
|
33
|
+
# once. The button remembers which of them started the press, so neither can end
|
|
34
|
+
# the other's.
|
|
35
|
+
#
|
|
36
|
+
# `activate_on: :press` is what a skill wants: the tool is used the frame the key
|
|
37
|
+
# goes down. A settings menu keeps the default, `:release`, where a player can
|
|
38
|
+
# still move away before letting go. Pressed stays visible for
|
|
39
|
+
# `UI::Button::PRESS_FEEDBACK` after a tap, because a single pressed frame is
|
|
40
|
+
# too short to see.
|
|
41
|
+
#
|
|
42
|
+
# ## The caption sits under the disc
|
|
43
|
+
#
|
|
44
|
+
# A captioned IconButton draws its style only in the space above the caption, so
|
|
45
|
+
# "Watering can", wider than the disc, reads on the background in every state.
|
|
46
|
+
#
|
|
47
|
+
# ## What this example does not solve
|
|
48
|
+
#
|
|
49
|
+
# Cooldowns — a sweep over a used tool needs an arc the renderer does not have —
|
|
50
|
+
# and a glyph on each button saying which key uses it. `examples/input_glyphs`
|
|
51
|
+
# draws the glyph for an action, if a bar wants one. The bar is also at a fixed
|
|
52
|
+
# position rather than centred on the view: its buttons are placed when they are
|
|
53
|
+
# added, and the view's size exists only at draw time.
|
|
54
|
+
|
|
55
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
56
|
+
require 'rgame/game'
|
|
57
|
+
|
|
58
|
+
WIDTH = 640
|
|
59
|
+
HEIGHT = 480
|
|
60
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
61
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
62
|
+
Controls = RGame::Util::Controls
|
|
63
|
+
|
|
64
|
+
# The bar, and what was last used from it.
|
|
65
|
+
class SkillBar < RGame::Engine::Node2D
|
|
66
|
+
UI = RGame::Engine::UI
|
|
67
|
+
|
|
68
|
+
SLOT_WIDTH = 100
|
|
69
|
+
SLOT_HEIGHT = 104
|
|
70
|
+
|
|
71
|
+
# Left to right, with the action that uses each without moving focus. Each
|
|
72
|
+
# image names an entry of skills.json's `images`.
|
|
73
|
+
SKILLS = [
|
|
74
|
+
%i[wand wand skill1],
|
|
75
|
+
%i[wrench wrench skill2],
|
|
76
|
+
%i[torch torch skill3],
|
|
77
|
+
%i[hammer hammer skill4],
|
|
78
|
+
%i[watering_can watering_can skill5]
|
|
79
|
+
].freeze
|
|
80
|
+
|
|
81
|
+
DISC = UI::ShapeStyle.new(shape: :disc)
|
|
82
|
+
CLICK = 'blip.ogg'
|
|
83
|
+
|
|
84
|
+
attr_reader :menu, :used
|
|
85
|
+
|
|
86
|
+
def initialize(**)
|
|
87
|
+
super
|
|
88
|
+
@used = nil
|
|
89
|
+
@menu = add_node(UI::Menu.new(layout: UI::Row.new(item_width: SLOT_WIDTH, item_height: SLOT_HEIGHT),
|
|
90
|
+
scope: 'skills'))
|
|
91
|
+
SKILLS.each { |key, image, hotkey| add_skill(key, image, hotkey) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The name of the focused tool, as its button draws it.
|
|
95
|
+
def focused_name = @menu.focused.label
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def add_skill(key, image, hotkey)
|
|
100
|
+
button = UI::IconButton.new(image: image, label: key, hotkey: hotkey, activate_on: :press, style: DISC)
|
|
101
|
+
@menu.add(button).on_activated do
|
|
102
|
+
@used = button.label
|
|
103
|
+
RGame::Engine::AudioBus.play_sound(CLICK)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The captions. Added after the bar, so its last line is the last text of every
|
|
109
|
+
# frame — which is what the drive script reads. Each sentence is one key with the
|
|
110
|
+
# tool's name as a variable, and the name is the button's own label, so a
|
|
111
|
+
# translator writes each name once and drawing an unchanged caption allocates
|
|
112
|
+
# nothing.
|
|
113
|
+
class Caption < RGame::Engine::Node2D
|
|
114
|
+
def initialize(bar:, **)
|
|
115
|
+
super(**)
|
|
116
|
+
@bar = bar
|
|
117
|
+
@help = RGame::Engine::Text.new('help.bar')
|
|
118
|
+
@focused = RGame::Engine::Text.new('status.focused', :skill)
|
|
119
|
+
@used = RGame::Engine::Text.new('status.used', :skill)
|
|
120
|
+
@nothing_used = RGame::Engine::Text.new('status.nothing_used')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def on_draw(renderer, _view)
|
|
124
|
+
renderer.text(@help, 12, 12)
|
|
125
|
+
renderer.text(@focused.with(skill: @bar.focused_name.to_s), 12, HEIGHT - 52)
|
|
126
|
+
used = @bar.used
|
|
127
|
+
renderer.text(used ? @used.with(skill: used.to_s) : @nothing_used, 12, HEIGHT - 30)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class Scene < RGame::Engine::Node2D
|
|
132
|
+
def on_add
|
|
133
|
+
bar = add_node(SkillBar.new(y: 300))
|
|
134
|
+
bar.x = (WIDTH - bar.menu.bounds_width) / 2
|
|
135
|
+
add_node(Caption.new(bar: bar))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
game = RGame::Game.new(
|
|
140
|
+
root: Scene.new,
|
|
141
|
+
caption: 'Skill bar',
|
|
142
|
+
width: WIDTH,
|
|
143
|
+
height: HEIGHT,
|
|
144
|
+
media_root: ASSETS,
|
|
145
|
+
locales: LOCALES,
|
|
146
|
+
input_map: RGame::Engine::InputMap.default.merge(
|
|
147
|
+
skill1: { buttons: [Controls::KEY_1] },
|
|
148
|
+
skill2: { buttons: [Controls::KEY_2] },
|
|
149
|
+
skill3: { buttons: [Controls::KEY_3] },
|
|
150
|
+
skill4: { buttons: [Controls::KEY_4] },
|
|
151
|
+
skill5: { buttons: [Controls::KEY_5] }
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# An image id that is a Symbol is a name rather than a path, so the tools are
|
|
156
|
+
# registered once: the atlas cuts each from the strip and registers it by name.
|
|
157
|
+
game.renderer.register_ui_atlas(game.assets.ui_atlas('skills.json'))
|
|
158
|
+
|
|
159
|
+
game.start
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Sound — a sound effect fired by a button, and the seam it travels through.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/sound/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Space (or A on a pad) plays a blip. Press it fast: each press is another
|
|
10
|
+
# voice, so they overlap rather than cutting each other off. It exercises:
|
|
11
|
+
# - Core::Sample — a decoded, fire-and-forget effect, named by its path;
|
|
12
|
+
# - Engine::AudioBus — where gameplay says *what happened*;
|
|
13
|
+
# - Engine::AudioDirector — what turns that into playback, subscribed by
|
|
14
|
+
# RGame::Game;
|
|
15
|
+
# - Engine::Text — a count on screen, from a key, that costs no String per frame.
|
|
16
|
+
#
|
|
17
|
+
# ## Why a node does not just call the audio device
|
|
18
|
+
#
|
|
19
|
+
# It cannot. A node lives in `RGame::Engine`, and that layer may not name
|
|
20
|
+
# `RGame::Core` at all — not a require, not a constant. So `Scene#on_control`
|
|
21
|
+
# below emits on `AudioBus`, a global signal hub that knows nothing about sound,
|
|
22
|
+
# and an `AudioDirector` forwards it to the real device. `RGame::Game` subscribes
|
|
23
|
+
# that director when it starts and releases it when the loop ends, so this file
|
|
24
|
+
# wires nothing.
|
|
25
|
+
#
|
|
26
|
+
# That looks like ceremony until you notice what it buys: the same scene runs in
|
|
27
|
+
# a headless spec with a recording fake substituted for the device, and the spec
|
|
28
|
+
# asserts on *what was asked for* rather than on what came out of a speaker.
|
|
29
|
+
# `spec/support/shared_examples/an_audio_server.rb` is the contract both sides
|
|
30
|
+
# are held to.
|
|
31
|
+
#
|
|
32
|
+
# ## A Sample is not a Song
|
|
33
|
+
#
|
|
34
|
+
# There are two types on purpose. This is a `Sample`: decoded up front, and every
|
|
35
|
+
# `play_sound` starts a **new voice** layered over whatever is already sounding.
|
|
36
|
+
# It has no `playing?`, because the question has no answer for a sound that may
|
|
37
|
+
# be going five times at once. `examples/music` is the other type.
|
|
38
|
+
#
|
|
39
|
+
# ## Naming a sound is naming a file
|
|
40
|
+
#
|
|
41
|
+
# `'blip.ogg'` is a path, resolved through the asset manager on first use and
|
|
42
|
+
# remembered — the same thing `examples/walk` does with `'hero.json'`. Nothing is
|
|
43
|
+
# registered anywhere in this file.
|
|
44
|
+
#
|
|
45
|
+
# The other id space is a Symbol, which `audio.register_sound(:hit, ...)` binds
|
|
46
|
+
# to a sound the game chose the name for, or assembled rather than loaded. Audio
|
|
47
|
+
# and the renderer take the same two, so one rule covers both: a String is a
|
|
48
|
+
# file, a Symbol is a name.
|
|
49
|
+
|
|
50
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
51
|
+
require 'rgame/game'
|
|
52
|
+
|
|
53
|
+
WIDTH = 640
|
|
54
|
+
HEIGHT = 480
|
|
55
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
56
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
57
|
+
|
|
58
|
+
class Scene < RGame::Engine::Node2D
|
|
59
|
+
RING = RGame::Util::Color.new(120, 200, 255)
|
|
60
|
+
FADE = 3.0 # how fast the flash decays, in units per second
|
|
61
|
+
MIN_R = 18.0 # radius at rest
|
|
62
|
+
GROW = 90.0 # extra radius at full flash
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
super
|
|
66
|
+
@flash = 0.0
|
|
67
|
+
@plays = 0
|
|
68
|
+
# Built once, here, and that is the whole trick: the key's text renders when
|
|
69
|
+
# the count changes rather than when a frame is drawn. See the note above
|
|
70
|
+
# on_draw.
|
|
71
|
+
@plays_label = RGame::Engine::Text.new('hud.plays', :plays)
|
|
72
|
+
@help = RGame::Engine::Text.new('help.press')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def on_control(actions)
|
|
76
|
+
return unless actions.pressed?(:fire)
|
|
77
|
+
|
|
78
|
+
# The whole of "make a noise": a fact, emitted. Nothing here knows whether
|
|
79
|
+
# anything is listening, or what it would play it on.
|
|
80
|
+
RGame::Engine::AudioBus.play_sound('blip.ogg')
|
|
81
|
+
|
|
82
|
+
@flash = 1.0
|
|
83
|
+
@plays += 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The flash is state, advanced by dt — not a clock read at draw time. That is
|
|
87
|
+
# the standing rule (CLAUDE.md, "`draw` renders state"), and it is why pausing
|
|
88
|
+
# this node would freeze the ring mid-fade instead of letting it run on.
|
|
89
|
+
def on_update(dt)
|
|
90
|
+
@flash -= dt * FADE
|
|
91
|
+
@flash = 0.0 if @flash.negative?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A count, drawn as text, allocating nothing.
|
|
95
|
+
#
|
|
96
|
+
# `"plays: #{@plays}"` written here would build a String on every frame
|
|
97
|
+
# forever, and `Game/NoInterpolationInHotPath` is right to refuse it: a steady
|
|
98
|
+
# 60fps frame that allocates is a GC pause waiting to happen.
|
|
99
|
+
# `Engine::Text` holds the last string and renders `plays: %{plays}` from the
|
|
100
|
+
# table again only when the value changes, so pressing the key costs one
|
|
101
|
+
# allocation and the thousand frames between presses cost none.
|
|
102
|
+
#
|
|
103
|
+
# **Reach for it rather than inventing a way round the rule.** A label built
|
|
104
|
+
# from something that changes is common enough that the engine owns the
|
|
105
|
+
# answer — see CLAUDE.md, "A label built from a changing value".
|
|
106
|
+
def on_draw(renderer, _view)
|
|
107
|
+
renderer.circle(WIDTH / 2, HEIGHT / 2, MIN_R + (GROW * @flash), color: RING)
|
|
108
|
+
renderer.text(@help, 12, 12)
|
|
109
|
+
renderer.text(@plays_label.with(plays: @plays), 12, 44)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
game = RGame::Game.new(
|
|
114
|
+
root: Scene.new,
|
|
115
|
+
caption: 'Sound',
|
|
116
|
+
width: WIDTH,
|
|
117
|
+
height: HEIGHT,
|
|
118
|
+
media_root: ASSETS,
|
|
119
|
+
locales: LOCALES
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
game.start
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
keys: "Player one: arrows or WASD. Space waves"
|
|
5
|
+
join: Press A on a controller to join player two
|
|
6
|
+
hud:
|
|
7
|
+
# The number is given once, when the badge is built, and the Text keeps it.
|
|
8
|
+
player: "Player %{number}"
|
|
9
|
+
waves: "waves: %{count}"
|