rich_engine 0.0.0 → 0.1.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/.github/workflows/tests-and-linter.yml +9 -8
- data/.gitignore +7 -1
- data/.yardopts +6 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +42 -33
- data/README.md +318 -17
- data/examples/background.rb +1 -1
- data/examples/command_line_fps.rb +620 -0
- data/lib/rich_engine/animation.rb +137 -0
- data/lib/rich_engine/canvas/slot.rb +72 -0
- data/lib/rich_engine/canvas.rb +109 -4
- data/lib/rich_engine/chance.rb +17 -0
- data/lib/rich_engine/cooldown.rb +23 -4
- data/lib/rich_engine/enum/mixin.rb +34 -0
- data/lib/rich_engine/enum/value.rb +31 -0
- data/lib/rich_engine/enum.rb +26 -2
- data/lib/rich_engine/game.rb +68 -5
- data/lib/rich_engine/io.rb +39 -16
- data/lib/rich_engine/matrix.rb +57 -0
- data/lib/rich_engine/string_colors.rb +218 -125
- data/lib/rich_engine/terminal/cursor.rb +15 -0
- data/lib/rich_engine/terminal.rb +19 -0
- data/lib/rich_engine/timer/every.rb +15 -0
- data/lib/rich_engine/timer.rb +17 -0
- data/lib/rich_engine/ui/textures.rb +32 -0
- data/lib/rich_engine/version.rb +2 -1
- data/lib/rich_engine.rb +8 -0
- data/mise.toml +1 -1
- data/rich_engine.gemspec +1 -1
- metadata +12 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RichEngine
|
|
4
|
+
# Plays a sequence of string frames (sprites) at a fixed frames-per-second.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# frames = [
|
|
8
|
+
# "A",
|
|
9
|
+
# "B"
|
|
10
|
+
# ]
|
|
11
|
+
# anim = RichEngine::Animation.new(frames: frames, fps: 8, loop: true)
|
|
12
|
+
#
|
|
13
|
+
# # Inside your Game loop:
|
|
14
|
+
# anim.update(elapsed_time)
|
|
15
|
+
# anim.draw(@canvas, x: 10, y: 2, fg: :yellow)
|
|
16
|
+
class Animation
|
|
17
|
+
# @return [Array<String>] the frame sprites, in playback order.
|
|
18
|
+
# @return [Integer] the index of the frame currently shown.
|
|
19
|
+
# @return [Integer, Float] the configured frames per second.
|
|
20
|
+
attr_reader :frames, :frame_index, :fps
|
|
21
|
+
|
|
22
|
+
# @return [Symbol, String, Array<Integer>, Integer] default foreground
|
|
23
|
+
# color used when drawing.
|
|
24
|
+
attr_accessor :fg
|
|
25
|
+
|
|
26
|
+
# Builds an animation from a sequence of string frames.
|
|
27
|
+
#
|
|
28
|
+
# @param frames [Array<String>] each string is a frame; can be multi-line.
|
|
29
|
+
# Spaces are transparent.
|
|
30
|
+
# @param fps [Integer, Float] how many frames per second to advance.
|
|
31
|
+
# @param loop [Boolean] if true, wrap to the first frame after the last;
|
|
32
|
+
# otherwise stop at the last frame.
|
|
33
|
+
# @param fg [Symbol, String, Array<Integer>, Integer] default foreground
|
|
34
|
+
# color when drawing.
|
|
35
|
+
# @example
|
|
36
|
+
# anim = RichEngine::Animation.new(frames: ["A", "B"], fps: 8, loop: true)
|
|
37
|
+
def initialize(frames:, fps: 12, loop: true, fg: :white)
|
|
38
|
+
@frames = frames
|
|
39
|
+
@fps = fps
|
|
40
|
+
@loop = loop
|
|
41
|
+
@fg = fg
|
|
42
|
+
@frame_index = 0
|
|
43
|
+
@playing = true
|
|
44
|
+
@stepper = RichEngine::Timer.every(seconds: frame_interval)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Changes playback speed at runtime.
|
|
48
|
+
#
|
|
49
|
+
# @param value [Integer, Float] the new frames per second.
|
|
50
|
+
# @return [Integer, Float] the assigned value.
|
|
51
|
+
def fps=(value)
|
|
52
|
+
@fps = value
|
|
53
|
+
@stepper.interval = frame_interval
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Boolean] whether the animation wraps after the last frame.
|
|
57
|
+
def loop? = @loop
|
|
58
|
+
|
|
59
|
+
# @return [Boolean] whether the animation is currently advancing frames.
|
|
60
|
+
def playing? = @playing
|
|
61
|
+
|
|
62
|
+
# Stops playback and rewinds to the first frame.
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
def stop!
|
|
66
|
+
@playing = false
|
|
67
|
+
reset!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Pauses playback on the current frame.
|
|
71
|
+
#
|
|
72
|
+
# @return [void]
|
|
73
|
+
def pause!
|
|
74
|
+
@playing = false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Resumes playback.
|
|
78
|
+
#
|
|
79
|
+
# @return [void]
|
|
80
|
+
def play!
|
|
81
|
+
@playing = true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Rewinds to the first frame and restarts the frame timer.
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
def reset!
|
|
88
|
+
@frame_index = 0
|
|
89
|
+
@stepper.interval = frame_interval
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [String] the frame string currently shown.
|
|
93
|
+
def current_frame
|
|
94
|
+
@frames[@frame_index]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Advances the internal timer; call once per frame with the elapsed time.
|
|
98
|
+
#
|
|
99
|
+
# @param elapsed_time [Float] seconds since the last frame.
|
|
100
|
+
# @return [void]
|
|
101
|
+
def update(elapsed_time)
|
|
102
|
+
return unless @playing
|
|
103
|
+
return if @frames.size <= 1
|
|
104
|
+
|
|
105
|
+
@stepper.update(elapsed_time)
|
|
106
|
+
@stepper.when_ready { advance_frame }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Renders the current frame to the canvas.
|
|
110
|
+
#
|
|
111
|
+
# @param canvas [RichEngine::Canvas] the canvas to draw on.
|
|
112
|
+
# @param x [Integer] left column to draw at.
|
|
113
|
+
# @param y [Integer] top row to draw at.
|
|
114
|
+
# @param fg [Symbol, String, Array<Integer>, Integer, nil] foreground
|
|
115
|
+
# color; falls back to the animation's default when nil.
|
|
116
|
+
# @return [void]
|
|
117
|
+
def draw(canvas, x:, y:, fg: nil)
|
|
118
|
+
canvas.draw_sprite(current_frame, x: x, y: y, fg: fg || @fg)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def frame_interval
|
|
124
|
+
1.0 / @fps.to_f
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def advance_frame
|
|
128
|
+
if @loop
|
|
129
|
+
@frame_index = (@frame_index + 1) % @frames.size
|
|
130
|
+
elsif @frame_index < @frames.size - 1
|
|
131
|
+
@frame_index += 1
|
|
132
|
+
else
|
|
133
|
+
@playing = false
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RichEngine
|
|
4
|
+
class Canvas
|
|
5
|
+
# A sub-section of a parent Canvas. All drawing operations are translated by
|
|
6
|
+
# the slot's origin (x, y).
|
|
7
|
+
class Slot < Canvas
|
|
8
|
+
# @return [String, nil] the slot's background fill, or nil to inherit the
|
|
9
|
+
# parent's
|
|
10
|
+
attr_reader :bg
|
|
11
|
+
|
|
12
|
+
# @param parent [RichEngine::Canvas] the canvas this slot draws onto
|
|
13
|
+
# @param x [Integer] the slot's left column on the parent canvas
|
|
14
|
+
# @param y [Integer] the slot's top row on the parent canvas
|
|
15
|
+
# @param width [Integer] the slot width
|
|
16
|
+
# @param height [Integer] the slot height
|
|
17
|
+
# @param bg [String, nil] the slot's background fill, or nil to inherit
|
|
18
|
+
# the parent's
|
|
19
|
+
def initialize(parent, x, y, width, height, bg: nil)
|
|
20
|
+
@parent = parent
|
|
21
|
+
@offset_x = x
|
|
22
|
+
@offset_y = y
|
|
23
|
+
@width = width
|
|
24
|
+
@height = height
|
|
25
|
+
@bg = bg
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Read the cell at the slot-local (x, y), translated to parent space.
|
|
29
|
+
#
|
|
30
|
+
# @param x [Integer] the slot-local column
|
|
31
|
+
# @param y [Integer] the slot-local row
|
|
32
|
+
# @return [String, nil] the cell contents, or nil if out of the slot's
|
|
33
|
+
# bounds
|
|
34
|
+
def [](x, y)
|
|
35
|
+
return nil if out_of_bounds?(x, y)
|
|
36
|
+
@parent[@offset_x + x, @offset_y + y]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Write the cell at the slot-local (x, y), translated to parent space.
|
|
40
|
+
# Writes outside the slot are clipped.
|
|
41
|
+
#
|
|
42
|
+
# @param x [Integer] the slot-local column
|
|
43
|
+
# @param y [Integer] the slot-local row
|
|
44
|
+
# @param value [String] the cell contents to write
|
|
45
|
+
# @return [void]
|
|
46
|
+
def []=(x, y, value)
|
|
47
|
+
return if out_of_bounds?(x, y)
|
|
48
|
+
@parent[@offset_x + x, @offset_y + y] = value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Clear the slot, filling it with its background (or the parent's if the
|
|
52
|
+
# slot has none).
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def clear
|
|
56
|
+
fill_char = @bg || @parent.bg
|
|
57
|
+
each_coord do |x, y|
|
|
58
|
+
self[x, y] = fill_char
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Change the slot's background fill character and clear the slot.
|
|
63
|
+
#
|
|
64
|
+
# @param bg [String] the new background fill character
|
|
65
|
+
# @return [void]
|
|
66
|
+
def bg=(bg)
|
|
67
|
+
@bg = bg
|
|
68
|
+
clear
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/rich_engine/canvas.rb
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "string_colors"
|
|
4
|
+
require_relative "canvas/slot"
|
|
4
5
|
|
|
5
6
|
module RichEngine
|
|
7
|
+
# A 2D character grid that you draw to each frame, with colored text and
|
|
8
|
+
# shapes. Cells are stored row-major in a flat array.
|
|
6
9
|
class Canvas
|
|
7
10
|
using StringColors
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
# @return [Array<String>] the flat, row-major array of cell contents
|
|
13
|
+
# @return [String] the background fill character
|
|
14
|
+
# @return [Integer] the canvas width in characters
|
|
15
|
+
# @return [Integer] the canvas height in characters
|
|
16
|
+
attr_reader :canvas, :bg, :width, :height
|
|
10
17
|
|
|
18
|
+
# @param width [Integer] the canvas width in characters
|
|
19
|
+
# @param height [Integer] the canvas height in characters
|
|
20
|
+
# @param bg [String] the background fill character
|
|
11
21
|
def initialize(width, height, bg: " ")
|
|
12
22
|
@width = width
|
|
13
23
|
@height = height
|
|
@@ -15,10 +25,18 @@ module RichEngine
|
|
|
15
25
|
clear
|
|
16
26
|
end
|
|
17
27
|
|
|
28
|
+
# The canvas dimensions as a [width, height] pair.
|
|
29
|
+
#
|
|
30
|
+
# @return [Array(Integer, Integer)] the width and height
|
|
18
31
|
def dimensions
|
|
19
32
|
[@width, @height]
|
|
20
33
|
end
|
|
21
34
|
|
|
35
|
+
# Yield every (x, y) coordinate in the canvas.
|
|
36
|
+
#
|
|
37
|
+
# @yieldparam x [Integer] the column
|
|
38
|
+
# @yieldparam y [Integer] the row
|
|
39
|
+
# @return [void]
|
|
22
40
|
def each_coord(&block)
|
|
23
41
|
(0...@width).each do |x|
|
|
24
42
|
(0...@height).each do |y|
|
|
@@ -27,27 +45,55 @@ module RichEngine
|
|
|
27
45
|
end
|
|
28
46
|
end
|
|
29
47
|
|
|
48
|
+
# Enumerate the canvas one row at a time.
|
|
49
|
+
#
|
|
50
|
+
# @return [Enumerator] an enumerator of rows, each an array of cells
|
|
30
51
|
def rows
|
|
31
52
|
@canvas.each_slice(@width)
|
|
32
53
|
end
|
|
33
54
|
|
|
34
|
-
|
|
55
|
+
# Draw a multi-line string as a sprite; spaces are treated as transparent
|
|
56
|
+
# and left untouched.
|
|
57
|
+
#
|
|
58
|
+
# @param sprite [String] the multi-line sprite to draw
|
|
59
|
+
# @param x [Integer] the left column to start drawing at
|
|
60
|
+
# @param y [Integer] the top row to start drawing at
|
|
61
|
+
# @param fg [Symbol, String, Array<Integer>, Integer] the foreground color
|
|
62
|
+
# @param bg [Symbol, String, Array<Integer>, Integer] the background color
|
|
63
|
+
# @return [void]
|
|
64
|
+
def draw_sprite(sprite, x: 0, y: 0, fg: :white, bg: :transparent)
|
|
35
65
|
sprite.split("\n").each.with_index do |line, i|
|
|
36
66
|
line.each_char.with_index do |char, j|
|
|
37
67
|
next if char == " "
|
|
38
68
|
|
|
39
|
-
self[x + j, y + i] = char.fg(fg)
|
|
69
|
+
self[x + j, y + i] = char.fg(fg).bg(bg)
|
|
40
70
|
end
|
|
41
71
|
end
|
|
42
72
|
end
|
|
43
73
|
|
|
74
|
+
# Write colored text at a position. Pass :center for x or y to center the
|
|
75
|
+
# text along that axis. A single color applies to every character; an array
|
|
76
|
+
# of colors cycles per character.
|
|
77
|
+
#
|
|
78
|
+
# @param str [String] the text to write
|
|
79
|
+
# @param x [Integer, Symbol] the left column, or :center to horizontally
|
|
80
|
+
# center the text
|
|
81
|
+
# @param y [Integer, Symbol] the row, or :center to vertically center the
|
|
82
|
+
# text
|
|
83
|
+
# @param fg [Symbol, String, Array<Integer>, Integer, Array] the foreground
|
|
84
|
+
# color, or an array of color specs to cycle per character
|
|
85
|
+
# @param bg [Symbol, String, Array<Integer>, Integer, Array] the background
|
|
86
|
+
# color, or an array of color specs to cycle per character
|
|
87
|
+
# @return [void]
|
|
88
|
+
# @example Cycle foreground colors per character
|
|
89
|
+
# canvas.write_string("Hello", x: :center, y: 1, fg: [:red, :green, :blue])
|
|
44
90
|
def write_string(str, x: 0, y: 0, fg: :white, bg: :transparent)
|
|
45
91
|
if x == :center
|
|
46
92
|
x = (@width - str.length) / 2
|
|
47
93
|
end
|
|
48
94
|
|
|
49
95
|
if y == :center
|
|
50
|
-
y = (@height -
|
|
96
|
+
y = (@height - 1) / 2
|
|
51
97
|
end
|
|
52
98
|
|
|
53
99
|
fg = Array(fg).cycle
|
|
@@ -58,6 +104,15 @@ module RichEngine
|
|
|
58
104
|
end
|
|
59
105
|
end
|
|
60
106
|
|
|
107
|
+
# Draw a filled rectangle. Coordinates and sizes are rounded to integers.
|
|
108
|
+
#
|
|
109
|
+
# @param x [Integer] the left column
|
|
110
|
+
# @param y [Integer] the top row
|
|
111
|
+
# @param width [Integer] the rectangle width
|
|
112
|
+
# @param height [Integer] the rectangle height
|
|
113
|
+
# @param char [String] the character to fill with
|
|
114
|
+
# @param color [Symbol, String, Array<Integer>, Integer] the fill color
|
|
115
|
+
# @return [void]
|
|
61
116
|
def draw_rect(x:, y:, width:, height:, char: "█", color: :white)
|
|
62
117
|
x = x.round
|
|
63
118
|
y = y.round
|
|
@@ -71,6 +126,15 @@ module RichEngine
|
|
|
71
126
|
end
|
|
72
127
|
end
|
|
73
128
|
|
|
129
|
+
# Draw a filled circle centered on (x, y). The center is rounded to
|
|
130
|
+
# integers.
|
|
131
|
+
#
|
|
132
|
+
# @param x [Integer] the center column
|
|
133
|
+
# @param y [Integer] the center row
|
|
134
|
+
# @param radius [Integer] the circle radius
|
|
135
|
+
# @param char [String] the character to fill with
|
|
136
|
+
# @param color [Symbol, String, Array<Integer>, Integer] the fill color
|
|
137
|
+
# @return [void]
|
|
74
138
|
def draw_circle(x:, y:, radius:, char: "█", color: :white)
|
|
75
139
|
x = x.round
|
|
76
140
|
y = y.round
|
|
@@ -84,6 +148,11 @@ module RichEngine
|
|
|
84
148
|
end
|
|
85
149
|
end
|
|
86
150
|
|
|
151
|
+
# Whether the given coordinate falls outside the canvas.
|
|
152
|
+
#
|
|
153
|
+
# @param x [Integer] the column
|
|
154
|
+
# @param y [Integer] the row
|
|
155
|
+
# @return [Boolean] true if (x, y) is outside the canvas bounds
|
|
87
156
|
def out_of_bounds?(x, y)
|
|
88
157
|
return true if x < 0
|
|
89
158
|
return true if x >= @width
|
|
@@ -93,6 +162,11 @@ module RichEngine
|
|
|
93
162
|
false
|
|
94
163
|
end
|
|
95
164
|
|
|
165
|
+
# Read the cell at (x, y). Coordinates are rounded to integers.
|
|
166
|
+
#
|
|
167
|
+
# @param x [Integer] the column
|
|
168
|
+
# @param y [Integer] the row
|
|
169
|
+
# @return [String, nil] the cell contents
|
|
96
170
|
def [](x, y)
|
|
97
171
|
x = x.round
|
|
98
172
|
y = y.round
|
|
@@ -100,6 +174,13 @@ module RichEngine
|
|
|
100
174
|
@canvas[at(x, y)]
|
|
101
175
|
end
|
|
102
176
|
|
|
177
|
+
# Write a cell at (x, y), ignoring out-of-bounds writes. Coordinates are
|
|
178
|
+
# rounded to integers.
|
|
179
|
+
#
|
|
180
|
+
# @param x [Integer] the column
|
|
181
|
+
# @param y [Integer] the row
|
|
182
|
+
# @param value [String] the cell contents to write
|
|
183
|
+
# @return [void]
|
|
103
184
|
def []=(x, y, value)
|
|
104
185
|
x = x.round
|
|
105
186
|
y = y.round
|
|
@@ -108,15 +189,39 @@ module RichEngine
|
|
|
108
189
|
@canvas[at(x, y)] = value
|
|
109
190
|
end
|
|
110
191
|
|
|
192
|
+
# Clear the entire canvas, resetting every cell to the background fill.
|
|
193
|
+
#
|
|
194
|
+
# @return [void]
|
|
111
195
|
def clear
|
|
112
196
|
@canvas = create_blank_canvas
|
|
113
197
|
end
|
|
114
198
|
|
|
199
|
+
# Change the background fill character and clear the canvas.
|
|
200
|
+
#
|
|
201
|
+
# @param bg [String] the new background fill character
|
|
202
|
+
# @return [void]
|
|
115
203
|
def bg=(bg)
|
|
116
204
|
@bg = bg
|
|
117
205
|
clear
|
|
118
206
|
end
|
|
119
207
|
|
|
208
|
+
# Define a logical sub-region of this canvas that translates local
|
|
209
|
+
# coordinates into the parent canvas space and clips drawing to the region.
|
|
210
|
+
#
|
|
211
|
+
# @param x [Integer] the region's left column on the parent canvas
|
|
212
|
+
# @param y [Integer] the region's top row on the parent canvas
|
|
213
|
+
# @param width [Integer] the region width
|
|
214
|
+
# @param height [Integer] the region height
|
|
215
|
+
# @param bg [String, nil] the slot's background fill, or nil to inherit the
|
|
216
|
+
# parent's
|
|
217
|
+
# @return [RichEngine::Canvas::Slot] the sub-region canvas
|
|
218
|
+
# @example
|
|
219
|
+
# log = canvas.slot(x: 80, y: 0, width: 20, height: 10)
|
|
220
|
+
# log.write_string("Hello", x: 1, y: 1) # => writes at (81, 1)
|
|
221
|
+
def slot(x:, y:, width:, height:, bg: nil)
|
|
222
|
+
Slot.new(self, x, y, width, height, bg: bg)
|
|
223
|
+
end
|
|
224
|
+
|
|
120
225
|
private
|
|
121
226
|
|
|
122
227
|
def at(x, y)
|
data/lib/rich_engine/chance.rb
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RichEngine
|
|
4
|
+
# Random helpers for probability checks.
|
|
4
5
|
module Chance
|
|
6
|
+
# Returns true with the given probability.
|
|
7
|
+
#
|
|
8
|
+
# @param value [Integer, Float] a probability as a fraction (0.2) or as a
|
|
9
|
+
# percentage (20); values greater than 1 are treated as percentages.
|
|
10
|
+
# @param rand_gen [#call] a generator returning a float in [0, 1).
|
|
11
|
+
# @return [Boolean] whether the chance succeeded.
|
|
12
|
+
# @example
|
|
13
|
+
# RichEngine::Chance.of(0.2) # 20% chance
|
|
14
|
+
# RichEngine::Chance.of(20) # also 20% (percent form)
|
|
5
15
|
def self.of(value, rand_gen: method(:rand))
|
|
6
16
|
percent = if value > 1
|
|
7
17
|
value / 100.0
|
|
@@ -12,6 +22,13 @@ module RichEngine
|
|
|
12
22
|
rand_gen.call < percent
|
|
13
23
|
end
|
|
14
24
|
|
|
25
|
+
# Returns true with a one-in-+value+ probability.
|
|
26
|
+
#
|
|
27
|
+
# @param value [Integer, Float] the denominator of the odds.
|
|
28
|
+
# @param rand_gen [#call] a generator returning a float in [0, 1).
|
|
29
|
+
# @return [Boolean] whether the chance succeeded.
|
|
30
|
+
# @example
|
|
31
|
+
# RichEngine::Chance.of_one_in(10) # 1 in 10 chance
|
|
15
32
|
def self.of_one_in(value, rand_gen: method(:rand))
|
|
16
33
|
of(1 / value.to_f, rand_gen: rand_gen)
|
|
17
34
|
end
|
data/lib/rich_engine/cooldown.rb
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RichEngine
|
|
4
|
+
# Tracks a fixed delay and reports when it has elapsed.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# shoot_cd = RichEngine::Cooldown.new(0.25) # seconds
|
|
8
|
+
# shoot_cd.update(dt)
|
|
9
|
+
# if shoot_cd.ready?
|
|
10
|
+
# shoot!
|
|
11
|
+
# shoot_cd.reset!
|
|
12
|
+
# end
|
|
4
13
|
class Cooldown
|
|
14
|
+
# @param target_time [Integer, Float] the cooldown duration in seconds.
|
|
5
15
|
def initialize(target_time)
|
|
6
|
-
@timer = 0
|
|
7
16
|
@target_time = target_time
|
|
17
|
+
@timer = target_time
|
|
8
18
|
end
|
|
9
19
|
|
|
20
|
+
# Counts down by the elapsed time.
|
|
21
|
+
#
|
|
22
|
+
# @param dt [Float] seconds since the last frame.
|
|
23
|
+
# @return [Float] the remaining time.
|
|
10
24
|
def update(dt)
|
|
11
|
-
@timer
|
|
25
|
+
@timer -= dt
|
|
12
26
|
end
|
|
13
27
|
|
|
28
|
+
# @return [Float] the remaining time in seconds (negative once finished).
|
|
14
29
|
def get
|
|
15
30
|
@timer
|
|
16
31
|
end
|
|
17
32
|
|
|
33
|
+
# Restarts the cooldown at its full duration.
|
|
34
|
+
#
|
|
35
|
+
# @return [Integer, Float] the reset remaining time.
|
|
18
36
|
def reset!
|
|
19
|
-
@timer =
|
|
37
|
+
@timer = @target_time
|
|
20
38
|
end
|
|
21
39
|
|
|
40
|
+
# @return [Boolean] whether the cooldown has elapsed.
|
|
22
41
|
def finished?
|
|
23
|
-
@timer
|
|
42
|
+
@timer <= 0
|
|
24
43
|
end
|
|
25
44
|
alias_method :ready?, :finished?
|
|
26
45
|
end
|
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
module RichEngine
|
|
2
2
|
class Enum
|
|
3
|
+
# Adds enum support to a class. Include it, then declare enums with
|
|
4
|
+
# {ClassMethods#enum} to get a class-level enum accessor and an
|
|
5
|
+
# instance-level reader that resolves the instance variable into an
|
|
6
|
+
# {Enum::Value}.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# class Player
|
|
10
|
+
# include RichEngine::Enum::Mixin
|
|
11
|
+
# enum :state, {idle: 0, running: 1, paused: 2}
|
|
12
|
+
#
|
|
13
|
+
# def initialize
|
|
14
|
+
# @state = :idle
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Player.states #=> RichEngine::Enum
|
|
19
|
+
# Player.new.state.idle? #=> true
|
|
3
20
|
module Mixin
|
|
21
|
+
# Extends the including class with {ClassMethods}.
|
|
22
|
+
#
|
|
23
|
+
# @param base [Class] the class including this module
|
|
24
|
+
# @return [void]
|
|
4
25
|
def self.included(base)
|
|
5
26
|
base.extend ClassMethods
|
|
6
27
|
end
|
|
7
28
|
|
|
29
|
+
# Class-level methods made available by including {Mixin}.
|
|
8
30
|
module ClassMethods
|
|
31
|
+
# Declares an enum on the class. Defines a class method returning the
|
|
32
|
+
# {Enum} (named after +enum_name+) and an instance method (named
|
|
33
|
+
# +name+) that reads the +@name+ instance variable and returns the
|
|
34
|
+
# matching {Enum::Value}.
|
|
35
|
+
#
|
|
36
|
+
# @param name [Symbol] the name of the enum and instance reader; the
|
|
37
|
+
# value is read from the +@name+ instance variable
|
|
38
|
+
# @param enum_options [Array, Hash] the enum options, as accepted by
|
|
39
|
+
# {Enum#initialize}
|
|
40
|
+
# @param enum_name [String] the name of the class-level enum accessor;
|
|
41
|
+
# defaults to the pluralized +name+
|
|
42
|
+
# @return [void]
|
|
9
43
|
def enum(name, enum_options, enum_name: "#{name}s")
|
|
10
44
|
define_singleton_method(enum_name) do
|
|
11
45
|
Enum.new(name, enum_options)
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
module RichEngine
|
|
2
2
|
class Enum
|
|
3
|
+
# A single selected option of an {Enum}. Values are comparable (by their
|
|
4
|
+
# underlying option value) and expose a query method per option, e.g.
|
|
5
|
+
# +#idle?+. Only values from the same enum can be compared or are equal.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# state = RichEngine::Enum.new(:state, {idle: 0, running: 1})
|
|
9
|
+
# value = state.running
|
|
10
|
+
# value.running? #=> true
|
|
11
|
+
# value.value #=> 1
|
|
3
12
|
class Value
|
|
4
13
|
include Comparable
|
|
5
14
|
|
|
15
|
+
# @return [Enum] the enum this value belongs to
|
|
16
|
+
# @return [Symbol] the selected option
|
|
6
17
|
attr_reader :enum, :selected
|
|
7
18
|
|
|
19
|
+
# Builds a value for a selected option and defines a query method per
|
|
20
|
+
# option (e.g. +#idle?+).
|
|
21
|
+
#
|
|
22
|
+
# @param enum [Enum] the enum this value belongs to
|
|
23
|
+
# @param selected [Symbol] the selected option; must be a valid option of
|
|
24
|
+
# +enum+
|
|
25
|
+
# @raise [ArgumentError] if +selected+ is not an option of +enum+
|
|
8
26
|
def initialize(enum:, selected:)
|
|
9
27
|
@enum = enum
|
|
10
28
|
@selected = selected
|
|
@@ -15,16 +33,29 @@ module RichEngine
|
|
|
15
33
|
freeze
|
|
16
34
|
end
|
|
17
35
|
|
|
36
|
+
# The underlying value of the selected option.
|
|
37
|
+
#
|
|
38
|
+
# @return [Object] the value mapped to the selected option
|
|
18
39
|
def value
|
|
19
40
|
@enum[@selected]
|
|
20
41
|
end
|
|
21
42
|
|
|
43
|
+
# Compares two values from the same enum by their underlying values.
|
|
44
|
+
#
|
|
45
|
+
# @param other [Value] another value from the same enum
|
|
46
|
+
# @return [Integer] -1, 0, or 1
|
|
47
|
+
# @raise [ArgumentError] if +other+ belongs to a different enum
|
|
22
48
|
def <=>(other)
|
|
23
49
|
raise ArgumentError, "Can't compare values from different enums" if enum != other.enum
|
|
24
50
|
|
|
25
51
|
value <=> other.value
|
|
26
52
|
end
|
|
27
53
|
|
|
54
|
+
# Two values are equal when they come from the same enum and have the
|
|
55
|
+
# same selected option.
|
|
56
|
+
#
|
|
57
|
+
# @param other [Object] the object to compare against
|
|
58
|
+
# @return [Boolean] whether the values are equal
|
|
28
59
|
def ==(other)
|
|
29
60
|
return @enum == other.enum && selected == other.selected if other.is_a? self.class
|
|
30
61
|
|
data/lib/rich_engine/enum.rb
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
1
|
+
require_relative "enum/value"
|
|
2
|
+
require_relative "enum/mixin"
|
|
3
3
|
|
|
4
4
|
module RichEngine
|
|
5
|
+
# An ergonomic, comparable enum with query methods. Options may be given as an
|
|
6
|
+
# array (auto-indexed from 0) or as a hash mapping each option to its value. A
|
|
7
|
+
# reader method is defined for every option that returns an {Enum::Value}.
|
|
8
|
+
#
|
|
9
|
+
# @example Array of options (auto-indexed)
|
|
10
|
+
# colors = RichEngine::Enum.new(:colors, [:red, :green, :blue])
|
|
11
|
+
# colors.options #=> {red: 0, green: 1, blue: 2}
|
|
12
|
+
# colors.red.value #=> 0
|
|
13
|
+
#
|
|
14
|
+
# @example Hash of options with explicit values
|
|
15
|
+
# state = RichEngine::Enum.new(:state, {idle: 0, running: 1, paused: 2})
|
|
16
|
+
# state.running > state.idle #=> true
|
|
5
17
|
class Enum
|
|
18
|
+
# @return [Symbol] the name of the enum
|
|
19
|
+
# @return [Hash] the options as a hash mapping each option to its value
|
|
6
20
|
attr_reader :name, :options
|
|
7
21
|
|
|
22
|
+
# Builds an enum and defines a reader method for each option.
|
|
23
|
+
#
|
|
24
|
+
# @param name [Symbol] the name of the enum
|
|
25
|
+
# @param options [Array, Hash] option names; an array is auto-indexed from
|
|
26
|
+
# 0, a hash maps each option name to its value
|
|
8
27
|
def initialize(name, options)
|
|
9
28
|
@name = name
|
|
10
29
|
@options = if options.respond_to? :each_pair
|
|
@@ -22,6 +41,11 @@ module RichEngine
|
|
|
22
41
|
freeze
|
|
23
42
|
end
|
|
24
43
|
|
|
44
|
+
# Looks up the value of an option.
|
|
45
|
+
#
|
|
46
|
+
# @param option [Symbol] the option name
|
|
47
|
+
# @return [Object] the value mapped to that option
|
|
48
|
+
# @raise [KeyError] if the option is unknown
|
|
25
49
|
def [](option)
|
|
26
50
|
@options.fetch(option)
|
|
27
51
|
end
|