rgame 0.1.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 +228 -0
- data/README.md +142 -243
- data/docs/api/README.md +141 -90
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -166
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1045 -254
- data/docs/api/drawing.md +187 -119
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +165 -45
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +344 -85
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +503 -123
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +197 -64
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +400 -147
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +234 -19
- 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/README.md +6 -5
- data/ext/rgame_core/app/app.c +215 -11
- 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 +113 -2
- data/ext/rgame_core/example.rb +18 -56
- data/ext/rgame_core/extconf.rb +16 -113
- data/ext/rgame_core/graphics/canvas.c +45 -4
- data/ext/rgame_core/graphics/canvas.h +65 -10
- data/ext/rgame_core/graphics/clip.c +22 -13
- data/ext/rgame_core/include/rgame/core.h +151 -5
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +46 -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 +42 -19
- data/ext/rgame_util/color_ext.c +12 -3
- 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 -42
- 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/input.rb +35 -41
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +75 -84
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +77 -65
- 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 +55 -10
- 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 +35 -1
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +31 -23
- 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 +21 -41
- 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 +22 -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 +70 -29
- 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 +45 -0
- data/lib/rgame/engine/debug_overlay.rb +23 -18
- 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 +76 -22
- data/lib/rgame/engine/input/actions.rb +63 -12
- 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 +364 -80
- data/lib/rgame/engine/path.rb +4 -6
- 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/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +29 -7
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +53 -8
- 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 +82 -0
- 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 +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 +43 -25
- data/lib/rgame/game.rb +164 -22
- 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 +106 -44
- 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 +123 -0
- data/lib/rgame/util.rb +5 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +176 -20
- 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/camera_view.rb +0 -28
- 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
|
@@ -8,59 +8,100 @@ module RGame
|
|
|
8
8
|
# everything an actor needs from it — collision against the solid tiles, the world
|
|
9
9
|
# bounds, and drawing the map through the scene's camera.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# **It does not resolve a step.** What it owns is the map's solid tiles as a blocker
|
|
12
|
+
# source (#blockers, an Engine::TileBlockers); the actor that wants to be stopped by
|
|
13
|
+
# them borrows it and resolves against it — see Components::Mover, which
|
|
14
|
+
# builds its own Engine::CollisionSystem out of the sources its `blocked_by` names.
|
|
15
|
+
# A mover may be blocked by tiles, by other actors, or by both, and only the mover
|
|
16
|
+
# knows which, so the resolver is the mover's and the grid is this system's. The tile
|
|
17
|
+
# solidity itself is whatever the map's tileset reports (baked per-tile in Tiled).
|
|
18
|
+
# The same solidity, viewed as a graph for planning routes, is #nav_grid.
|
|
13
19
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# drawing.
|
|
20
|
+
# **Solidity is read from the map once**, into one Util::SolidGrid, the first time
|
|
21
|
+
# anything asks — and #blockers, #nav_grid and #solid? all read that store, never the
|
|
22
|
+
# map. So they cannot disagree about a cell, and a resolve costs a byte lookup rather
|
|
23
|
+
# than a walk through the map's layers and tileset.
|
|
19
24
|
#
|
|
20
|
-
# It
|
|
25
|
+
# **It does not draw.** Drawing the map is RGame::Engine::TileMapLayer, one
|
|
26
|
+
# node per Tiled layer, mounted inside the WorldView so the map is drawn
|
|
27
|
+
# once per viewport like the rest of the world. This stays a system — the
|
|
28
|
+
# thing actors ask about collision and bounds — and a system that also
|
|
29
|
+
# drew was always the odd part of it.
|
|
30
|
+
#
|
|
31
|
+
# It owns the map's **animation clock**. Nothing below reads a wall clock —
|
|
21
32
|
# see CLAUDE.md, "`draw` renders state; time enters through `update`" — so the
|
|
22
33
|
# elapsed seconds animated tiles run on are accumulated here and handed down at
|
|
23
34
|
# draw time. Stop calling `update` and the water freezes, which is what pausing
|
|
24
35
|
# should look like.
|
|
25
36
|
class TileWorld < Engine::Component
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
include WorldBounds
|
|
38
|
+
|
|
39
|
+
attr_reader :tilemap_id, :elapsed
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
# `cameras` are the cameras this map bounds — every player's, normally.
|
|
42
|
+
# A camera may not show past the world's edges, and this is what knows
|
|
43
|
+
# how big the world is; the cameras themselves are owned by players.
|
|
44
|
+
def initialize(map:, tilemap_id:, cameras: [])
|
|
30
45
|
super()
|
|
31
46
|
@map = map
|
|
32
47
|
@tilemap_id = tilemap_id
|
|
33
|
-
@camera = camera
|
|
34
48
|
@elapsed = 0.0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
Array(cameras).each { |camera| bound(camera) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The map's solid tiles as a blocker source, for a body that declared
|
|
53
|
+
# `blocked_by: [:tiles]`. The same source every time, so every body on the map
|
|
54
|
+
# shares one.
|
|
55
|
+
def blockers
|
|
56
|
+
@blockers ||= Engine::TileBlockers.new(grid: solid_grid, tile_width: @map.tile_width,
|
|
57
|
+
tile_height: @map.tile_height)
|
|
42
58
|
end
|
|
43
59
|
|
|
44
60
|
def world_width = @map.pixel_width
|
|
45
61
|
def world_height = @map.pixel_height
|
|
46
62
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
def
|
|
63
|
+
# The size of one cell, in pixels — what turns a world position into the cell
|
|
64
|
+
# #nav_grid plans over.
|
|
65
|
+
def tile_width = @map.tile_width
|
|
66
|
+
def tile_height = @map.tile_height
|
|
67
|
+
|
|
68
|
+
def layer_count = @map.layer_count
|
|
69
|
+
|
|
70
|
+
# The first layer Tiled flags `above`, or the layer count if none is —
|
|
71
|
+
# which is where TileMapLayer.mount leaves the gap for the actors, so a
|
|
72
|
+
# map with no flag puts them over everything. Read once at mount rather
|
|
73
|
+
# than per frame: which layers cover the actors is a fact about the
|
|
74
|
+
# scene's arrangement, and the arrangement is made once.
|
|
75
|
+
def first_above_layer
|
|
76
|
+
layer_count.times.find { |index| @map.above_layer?(index) } || layer_count
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Clamp a camera to this map's edges. Called for each camera the scene
|
|
80
|
+
# hands over, and again for one that arrives later (a player joining).
|
|
81
|
+
def bound(camera)
|
|
82
|
+
camera.world_width = @map.pixel_width
|
|
83
|
+
camera.world_height = @map.pixel_height
|
|
84
|
+
camera
|
|
85
|
+
end
|
|
50
86
|
|
|
51
|
-
def solid?(col, row) =
|
|
87
|
+
def solid?(col, row) = solid_grid.solid?(col, row)
|
|
88
|
+
|
|
89
|
+
# The map's solid tiles as an Engine::NavGrid, for planning a route rather than
|
|
90
|
+
# resolving a step — over the same store #blockers reads, as a second view. Built on
|
|
91
|
+
# first ask, and the same grid every time after.
|
|
92
|
+
def nav_grid
|
|
93
|
+
@nav_grid ||= Engine::NavGrid.new(grid: solid_grid)
|
|
94
|
+
end
|
|
52
95
|
|
|
53
96
|
# Advances the tile animations. Seconds, like every other duration here.
|
|
54
97
|
def update(dt)
|
|
55
98
|
@elapsed += dt
|
|
56
99
|
end
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@camera.viewport_width, @camera.viewport_height,
|
|
63
|
-
z: OVERLAY_Z, elapsed: @elapsed)
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def solid_grid
|
|
104
|
+
@solid_grid ||= Util::SolidGrid.build(@map.width, @map.height) { |col, row| @map.solid_tile?(col, row) }
|
|
64
105
|
end
|
|
65
106
|
end
|
|
66
107
|
end
|
|
@@ -26,7 +26,7 @@ module RGame
|
|
|
26
26
|
# When a node needs more than one (a spawn cadence and a wave cadence), give them
|
|
27
27
|
# distinct `as:` names — a node holds one component per slot.
|
|
28
28
|
class Timer < Engine::Component
|
|
29
|
-
signal :on_timeout
|
|
29
|
+
signal :on_timeout
|
|
30
30
|
|
|
31
31
|
def initialize(interval, repeating: true)
|
|
32
32
|
super()
|
|
@@ -5,20 +5,36 @@ module RGame
|
|
|
5
5
|
module Components
|
|
6
6
|
# Integrates linear and angular velocity into the node's transform each step.
|
|
7
7
|
# Free-moving entities (rocks, bullets) use it alone; a controller component
|
|
8
|
-
# (or the node's own control hook) writes vx/vy/spin as intent.
|
|
9
|
-
|
|
8
|
+
# (or the node's own control hook) writes vx/vy/spin as intent.
|
|
9
|
+
#
|
|
10
|
+
# A Mover, so `blocked_by:` stops it the way it stops a CharacterBody — see Mover's
|
|
11
|
+
# header. A blocked step keeps `vx`/`vy` as they were: they are the intent, and what
|
|
12
|
+
# a stop should do to them (nothing, zero them, bounce) is the game's call, made in
|
|
13
|
+
# an `on_blocked` handler.
|
|
14
|
+
class Velocity < Mover
|
|
10
15
|
attr_accessor :vx, :vy, :spin
|
|
11
16
|
|
|
12
|
-
def initialize(vx: 0.0, vy: 0.0, spin: 0.0)
|
|
13
|
-
super()
|
|
17
|
+
def initialize(vx: 0.0, vy: 0.0, spin: 0.0, blocked_by: [])
|
|
18
|
+
super(blocked_by: blocked_by)
|
|
14
19
|
@vx = vx
|
|
15
20
|
@vy = vy
|
|
16
21
|
@spin = spin
|
|
17
22
|
end
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
# The velocity scaled so its larger axis is ±1 — the direction, without the square
|
|
25
|
+
# root a unit vector would cost.
|
|
26
|
+
def heading_x = scale_to_heading(@vx)
|
|
27
|
+
def heading_y = scale_to_heading(@vy)
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def scale_to_heading(axis)
|
|
32
|
+
larger = [@vx.abs, @vy.abs].max
|
|
33
|
+
larger.zero? ? 0.0 : axis / larger.to_f
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def take_step(dt)
|
|
37
|
+
apply_move(@vx * dt, @vy * dt)
|
|
22
38
|
node.angle += @spin * dt
|
|
23
39
|
end
|
|
24
40
|
end
|
|
@@ -7,6 +7,10 @@ module RGame
|
|
|
7
7
|
# direction (one of eight, or idle) and holds it until a timer elapses — or until a
|
|
8
8
|
# wall blocks it, at which point it re-rolls early instead of pushing into the wall.
|
|
9
9
|
# The RNG is injected so behaviour is deterministic in tests.
|
|
10
|
+
#
|
|
11
|
+
# "Blocked" is measured as *the node did not move while intending to*, not asked of
|
|
12
|
+
# a collision world — so this works over any CharacterBody, and simply never fires
|
|
13
|
+
# for one whose steps always land.
|
|
10
14
|
class WanderController < Engine::Component
|
|
11
15
|
MOVED_EPS = 1e-6
|
|
12
16
|
DIRECTIONS = [
|
|
@@ -19,11 +23,11 @@ module RGame
|
|
|
19
23
|
@rng = rng
|
|
20
24
|
@change_interval = change_interval
|
|
21
25
|
@idle_chance = idle_chance
|
|
22
|
-
@timer = 0.0
|
|
26
|
+
@timer = 0.0
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def on_attach
|
|
26
|
-
@body =
|
|
30
|
+
@body = require_sibling(CharacterBody)
|
|
27
31
|
@last_x = node.x
|
|
28
32
|
@last_y = node.y
|
|
29
33
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# How big the world is — the contract, not an implementation.
|
|
7
|
+
#
|
|
8
|
+
# Two systems answer it: `World` below, which is nothing *but* the answer,
|
|
9
|
+
# and `TileWorld`, which derives it from the map it parsed. A component
|
|
10
|
+
# that needs bounds asks for the contract — `node.system(WorldBounds)` —
|
|
11
|
+
# and gets whichever of the two the scene mounted. That works because
|
|
12
|
+
# Node2D#get_component matches with `is_a?`, which matches an included
|
|
13
|
+
# module as readily as a class.
|
|
14
|
+
#
|
|
15
|
+
# Naming the contract is what keeps the two from drifting: ScreenWrap does
|
|
16
|
+
# not know which kind of world it is wrapping inside, and must not need to.
|
|
17
|
+
#
|
|
18
|
+
# ## The bounds are in world coordinates
|
|
19
|
+
#
|
|
20
|
+
# The world runs from (0, 0) to (world_width, world_height) in **world**
|
|
21
|
+
# space — the space `Node2D#world_x` answers in, and the one the tile grid,
|
|
22
|
+
# the collision world and the camera already use. So everything that
|
|
23
|
+
# compares a node against these bounds compares `world_x`/`world_y`, never
|
|
24
|
+
# the node's local `x`/`y`: an entity grouped under an offset container is
|
|
25
|
+
# still inside the world when it is, whatever its position in the container.
|
|
26
|
+
# spec/support/shared_examples/a_world_edge_response.rb holds every reader to it.
|
|
27
|
+
module WorldBounds
|
|
28
|
+
def world_width = raise(NotImplementedError, "#{self.class} must define #world_width")
|
|
29
|
+
def world_height = raise(NotImplementedError, "#{self.class} must define #world_height")
|
|
30
|
+
|
|
31
|
+
# The bounds a component should use: whatever it was handed, falling back
|
|
32
|
+
# to the world system on `node` for either axis left nil. The single place
|
|
33
|
+
# that fallback is written, so the components sharing it cannot drift.
|
|
34
|
+
#
|
|
35
|
+
# Call it from `on_attach`, not `initialize` — a node has no scene to ask
|
|
36
|
+
# until it is in the tree, and a pooled entity is built long before it is.
|
|
37
|
+
def self.resolve(node, width, height)
|
|
38
|
+
return [width, height] if width && height
|
|
39
|
+
|
|
40
|
+
bounds = node.system(self)
|
|
41
|
+
if bounds.nil?
|
|
42
|
+
raise 'no world bounds in scope: mount a World (or TileWorld) system on the scene, ' \
|
|
43
|
+
'or pass explicit width:/height:'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
[width || bounds.world_width, height || bounds.world_height]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Refuse a node carrying more than one response to the edge of the world:
|
|
50
|
+
# ScreenWrap, DespawnOffscreen, or a Mover declaring `blocked_by: [:bounds]`.
|
|
51
|
+
#
|
|
52
|
+
# Stopping at the edge, wrapping past it and being freed beyond it are three
|
|
53
|
+
# answers to one question, and no two of them mean anything together. Stop and
|
|
54
|
+
# wrap disagree about where the node ends up; wrap and despawn race on which
|
|
55
|
+
# margin is reached first; and the stop works on the collision box while the
|
|
56
|
+
# other two test the node's origin, so a box with an offset holds the origin
|
|
57
|
+
# just past the edge and the other response fires on a node that was held.
|
|
58
|
+
# Rather than a rule each game has to remember, it raises.
|
|
59
|
+
#
|
|
60
|
+
# Folding wrap and despawn into Mover as one `at_edge:` option would make two
|
|
61
|
+
# responses inexpressible too, but they act on the node however it moved — a reset,
|
|
62
|
+
# an ancestor, a direct write — and every option there is a branch on the free
|
|
63
|
+
# step's path. A refusal at attach costs no frame anything. It is also the place
|
|
64
|
+
# to change if a game wants wrapping on one axis and stopping on the other: the
|
|
65
|
+
# rule then becomes one response per axis.
|
|
66
|
+
#
|
|
67
|
+
# Every response calls this from its own on_attach, which is what makes it
|
|
68
|
+
# order-free: a node assembled outside the tree already holds all its
|
|
69
|
+
# components when the first attaches, and on a live node each attaches on
|
|
70
|
+
# arrival, so the second response always finds the first.
|
|
71
|
+
#
|
|
72
|
+
# A pooled node re-attaches on every spawn, so this walks the components
|
|
73
|
+
# without allocating, and builds a message only when it raises.
|
|
74
|
+
def self.one_response!(node)
|
|
75
|
+
first = nil
|
|
76
|
+
node.components.each do |component|
|
|
77
|
+
next unless edge_response?(component)
|
|
78
|
+
next first = component if first.nil?
|
|
79
|
+
|
|
80
|
+
raise "#{node.class} has two responses to the edge of the world: " \
|
|
81
|
+
"#{describe_response(first)} and #{describe_response(component)}. " \
|
|
82
|
+
'A node can stop at the edge, wrap past it or be freed beyond it, but ' \
|
|
83
|
+
'only one of those — keep the one this node is for.'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.edge_response?(component)
|
|
88
|
+
component.is_a?(ScreenWrap) || component.is_a?(DespawnOffscreen) ||
|
|
89
|
+
(component.is_a?(Mover) && component.blocked_by?(Mover::BOUNDS))
|
|
90
|
+
end
|
|
91
|
+
private_class_method :edge_response?
|
|
92
|
+
|
|
93
|
+
def self.describe_response(component)
|
|
94
|
+
name = component.class.name&.split('::')&.last || component.class.inspect
|
|
95
|
+
component.is_a?(Mover) ? "#{name} (blocked_by :bounds)" : name
|
|
96
|
+
end
|
|
97
|
+
private_class_method :describe_response
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The scene-scoped world: how big it is, and nothing else yet.
|
|
101
|
+
#
|
|
102
|
+
# A game whose world is a plain rectangle mounts one on its scene, and the
|
|
103
|
+
# components that need bounds — ScreenWrap, DespawnOffscreen — find it
|
|
104
|
+
# rather than having the numbers threaded through every constructor between
|
|
105
|
+
# the scene and the entity. That threading is what this replaces, and it was
|
|
106
|
+
# worse than it looks: a pooled entity is built outside the tree, so its
|
|
107
|
+
# factory had to close over the size, and every scene above the factory had
|
|
108
|
+
# to carry the size in order to build it.
|
|
109
|
+
#
|
|
110
|
+
# It is deliberately **not** the window size. The two coincide in a
|
|
111
|
+
# single-screen game, which is exactly what makes the mistake easy to make
|
|
112
|
+
# and hard to see: bind wrapping to the viewport and the world silently
|
|
113
|
+
# changes shape when the window is resized, or when the screen is split and
|
|
114
|
+
# each half is its own viewport. Ask `Viewports` (or the `View` a draw is
|
|
115
|
+
# handed) how big the *window* is; ask this how big the *world* is.
|
|
116
|
+
#
|
|
117
|
+
# The bounds are immutable, which is what lets the components resolve them
|
|
118
|
+
# once at attach rather than re-reading them every frame. A world that
|
|
119
|
+
# genuinely changes size is a new scene.
|
|
120
|
+
class World < Engine::Component
|
|
121
|
+
include WorldBounds
|
|
122
|
+
|
|
123
|
+
attr_reader :world_width, :world_height
|
|
124
|
+
|
|
125
|
+
def initialize(width:, height:)
|
|
126
|
+
super()
|
|
127
|
+
@world_width = width
|
|
128
|
+
@world_height = height
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The colliders one collider is touching, this step and last. CollisionWorld owns
|
|
6
|
+
# one of these per registered collider and drives it, and together they are what
|
|
7
|
+
# turns a per-step overlap test into the two edges a game actually wants: the step
|
|
8
|
+
# a contact starts and the step it ends. Pure logic; no graphics.
|
|
9
|
+
#
|
|
10
|
+
# Nothing here is about overlapping, though, and it has a second user:
|
|
11
|
+
# every Components::Mover keeps one of *what stopped its step*, and gets on_blocked
|
|
12
|
+
# and on_unblocked out of it on exactly the same terms. What this class is, underneath
|
|
13
|
+
# its names, is "the set of things that were true this step and last".
|
|
14
|
+
#
|
|
15
|
+
# Two arrays, swapped rather than reallocated. `begin_frame` makes this step's list
|
|
16
|
+
# into last step's and empties the other one for refilling, so after the first few
|
|
17
|
+
# frames nothing here allocates — `Array#clear` keeps the capacity it grew to, the
|
|
18
|
+
# same trick SpatialHash#clear relies on.
|
|
19
|
+
#
|
|
20
|
+
# Membership is a linear scan by identity, and deliberately so. A Set or a Hash
|
|
21
|
+
# allocates on insert, and both are slower than scanning a handful of entries; a
|
|
22
|
+
# collider touching more than a handful of things at once is a design problem
|
|
23
|
+
# somewhere else, not a reason to index this.
|
|
24
|
+
class ContactSet
|
|
25
|
+
def initialize
|
|
26
|
+
@current = []
|
|
27
|
+
@previous = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Start a step: this step's contacts become the ones to compare against, and the
|
|
31
|
+
# list being filled is emptied. Swapped through a temporary rather than by
|
|
32
|
+
# `@current, @previous = @previous, @current`, which would allocate an Array every
|
|
33
|
+
# step — see .rubocop.yml on Style/SwapValues.
|
|
34
|
+
def begin_frame
|
|
35
|
+
filled = @current
|
|
36
|
+
@current = @previous
|
|
37
|
+
@previous = filled
|
|
38
|
+
@current.clear
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Forget everything, both steps. CollisionWorld calls this when a collider
|
|
42
|
+
# registers, so a pooled entity that died mid-contact cannot come back still
|
|
43
|
+
# holding the contacts it had in its previous life — which would otherwise
|
|
44
|
+
# surface as one spurious `on_separated` on its first step.
|
|
45
|
+
def reset
|
|
46
|
+
@current.clear
|
|
47
|
+
@previous.clear
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Record a contact for this step.
|
|
51
|
+
def add(other) = @current << other
|
|
52
|
+
|
|
53
|
+
# Already recorded this step? The broadphase offers a pair once per cell the two
|
|
54
|
+
# share, so this is what keeps one overlap from being counted twice.
|
|
55
|
+
def touching?(other) = @current.include?(other)
|
|
56
|
+
|
|
57
|
+
# Is this the *start* of a contact — a pair that was not touching last step? It
|
|
58
|
+
# reads only last step's list, so it gives the same answer either side of #add,
|
|
59
|
+
# and asking before recording is what reads as a guard.
|
|
60
|
+
def started?(other) = !@previous.include?(other)
|
|
61
|
+
|
|
62
|
+
# Yield every collider that was in contact last step and is not in contact now —
|
|
63
|
+
# the ending edge. Call it once every pair for the step has been recorded.
|
|
64
|
+
def each_ended
|
|
65
|
+
i = 0
|
|
66
|
+
while i < @previous.size
|
|
67
|
+
other = @previous[i]
|
|
68
|
+
i += 1
|
|
69
|
+
yield other unless @current.include?(other)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -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
|
|
@@ -35,27 +35,33 @@ module RGame
|
|
|
35
35
|
OBJ_LABEL = 'OBJ'
|
|
36
36
|
DELTA_LABEL = 'Δ/f'
|
|
37
37
|
|
|
38
|
-
COLOR = [80, 255, 120].freeze
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
GAP = 8 # space between a label and its number
|
|
38
|
+
COLOR = [80, 255, 120].freeze
|
|
39
|
+
PAD = 8
|
|
40
|
+
GAP = 8
|
|
42
41
|
|
|
43
42
|
def initialize
|
|
44
43
|
@visible = false
|
|
45
44
|
@prev_allocated = 0
|
|
46
|
-
@digit_widths = Array.new(10)
|
|
47
|
-
@label_widths = {}
|
|
45
|
+
@digit_widths = Array.new(10)
|
|
46
|
+
@label_widths = {}
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
def visible? = @visible
|
|
51
50
|
|
|
52
51
|
def toggle
|
|
53
52
|
@visible = !@visible
|
|
54
|
-
# Baseline the counter on reveal so the first Δ/f isn't the whole run's history.
|
|
55
53
|
@prev_allocated = GC.stat(:total_allocated_objects) if @visible
|
|
56
54
|
end
|
|
57
55
|
|
|
58
|
-
|
|
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)
|
|
59
65
|
return unless @visible
|
|
60
66
|
|
|
61
67
|
allocated = GC.stat(:total_allocated_objects)
|
|
@@ -63,24 +69,23 @@ module RGame
|
|
|
63
69
|
@prev_allocated = allocated
|
|
64
70
|
|
|
65
71
|
line_h = renderer.text_height
|
|
66
|
-
right_x = width - PAD
|
|
67
|
-
top_y = height - PAD - (line_h * 3)
|
|
72
|
+
right_x = view.width - PAD
|
|
73
|
+
top_y = view.height - PAD - (line_h * 3)
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
private
|
|
75
83
|
|
|
76
|
-
# A right-aligned "label number" row ending at right_x.
|
|
77
84
|
def draw_line(renderer, label, value, right_x, y)
|
|
78
85
|
number_left = draw_uint(renderer, value, right_x, y)
|
|
79
|
-
renderer.text(label, number_left - GAP - label_width(renderer, label), y,
|
|
86
|
+
renderer.text(label, number_left - GAP - label_width(renderer, label), y, color: COLOR)
|
|
80
87
|
end
|
|
81
88
|
|
|
82
|
-
# Draw a non-negative integer right-aligned ending at right_x, digit by digit so no
|
|
83
|
-
# String is built per frame. Returns the x of the leftmost digit.
|
|
84
89
|
def draw_uint(renderer, value, right_x, y)
|
|
85
90
|
x = right_x
|
|
86
91
|
more = true
|
|
@@ -88,7 +93,7 @@ module RGame
|
|
|
88
93
|
digit = value % 10
|
|
89
94
|
value /= 10
|
|
90
95
|
x -= digit_width(renderer, digit)
|
|
91
|
-
renderer.text(DIGITS[digit], x, y,
|
|
96
|
+
renderer.text(DIGITS[digit], x, y, color: COLOR)
|
|
92
97
|
more = value.positive?
|
|
93
98
|
end
|
|
94
99
|
x
|
|
@@ -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
|