dopairb 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 +7 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/exe/dopairb +6 -0
- data/lib/dopairb/canvas.rb +164 -0
- data/lib/dopairb/charge_line.rb +107 -0
- data/lib/dopairb/cli.rb +90 -0
- data/lib/dopairb/color.rb +126 -0
- data/lib/dopairb/config.rb +118 -0
- data/lib/dopairb/demo.rb +61 -0
- data/lib/dopairb/director.rb +190 -0
- data/lib/dopairb/font.rb +91 -0
- data/lib/dopairb/fx.rb +154 -0
- data/lib/dopairb/game.rb +182 -0
- data/lib/dopairb/input_fx.rb +499 -0
- data/lib/dopairb/irb_adapter.rb +65 -0
- data/lib/dopairb/output_tap.rb +94 -0
- data/lib/dopairb/probe.rb +159 -0
- data/lib/dopairb/reline_adapter.rb +247 -0
- data/lib/dopairb/scene.rb +154 -0
- data/lib/dopairb/scenes/failure.rb +306 -0
- data/lib/dopairb/scenes/session.rb +146 -0
- data/lib/dopairb/scenes/success.rb +418 -0
- data/lib/dopairb/session.rb +79 -0
- data/lib/dopairb/stage.rb +129 -0
- data/lib/dopairb/term.rb +131 -0
- data/lib/dopairb/version.rb +5 -0
- data/lib/dopairb.rb +126 -0
- data/spec.md +203 -0
- metadata +111 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
module Scenes
|
|
5
|
+
# Shared bits for error scenes: red tint on the input, big glitching title.
|
|
6
|
+
class FailureScene < Scene
|
|
7
|
+
def big? = true
|
|
8
|
+
def top? = !ctx.input.nil?
|
|
9
|
+
def pre = 0.0
|
|
10
|
+
def bell? = true
|
|
11
|
+
|
|
12
|
+
def title = "FAILED"
|
|
13
|
+
def palette = Color::BLOOD
|
|
14
|
+
|
|
15
|
+
def setup
|
|
16
|
+
@scale = best_scale(title)
|
|
17
|
+
@scale = 1 if @scale == 2 && Font.width(title, scale: 2) > @w * 0.7
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def height = @scale ? 7 : 2
|
|
21
|
+
def length = 0.85
|
|
22
|
+
def flash_at = 0.0
|
|
23
|
+
|
|
24
|
+
def shake(t) = t < 0.22 ? ((@rng.rand * 2 - 1) * 3 * (1 - t / 0.22)).round : 0
|
|
25
|
+
|
|
26
|
+
def draw_top(c, t)
|
|
27
|
+
return nil if t > 0.45
|
|
28
|
+
k = 1 - Fx.phase(t, 0.1, 0.45)
|
|
29
|
+
ox = t < 0.2 ? (@rng.rand(5) - 2) : 0
|
|
30
|
+
ctx.input.plain.each_with_index do |r, y|
|
|
31
|
+
c.put(ox, y, r, Color.mix([220, 220, 225], [255, 70, 80], k), bold: k > 0.5)
|
|
32
|
+
end
|
|
33
|
+
mark_top(c, t)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def mark_top(_c, _t); end
|
|
38
|
+
|
|
39
|
+
def draw(c, t)
|
|
40
|
+
draw_title(c, t)
|
|
41
|
+
draw_detail(c, t)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def draw_title(c, t)
|
|
45
|
+
if ctx.config.flash != :off && t < 0.1
|
|
46
|
+
c.tint_bg([160, 0, 20], 0.6 * (1 - t / 0.1), 0, 0, @w, height)
|
|
47
|
+
end
|
|
48
|
+
if @scale
|
|
49
|
+
glitch = t < 0.35 ? 0.35 * (1 - t / 0.35) : 0.0
|
|
50
|
+
tear = (t * 17).floor
|
|
51
|
+
fn = lambda do |rx, ry, _f|
|
|
52
|
+
flick = ((rx * 3 + ry * 5 + tear) % 11).zero? && t < 0.4
|
|
53
|
+
flick ? [255, 255, 255] : Color.ramp(palette, 0.15 + ry * 0.12)
|
|
54
|
+
end
|
|
55
|
+
x = (@w - Font.width(title, scale: @scale)) / 2
|
|
56
|
+
Fx.big_text(c, title, x, 0, fn, scale: @scale, glitch: glitch, rng: @rng)
|
|
57
|
+
else
|
|
58
|
+
c.put(1, 0, title.chars.join(" "), Color.ramp(palette, 0.2), bold: true)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def detail_text
|
|
63
|
+
bits = [event.info[:class_name]]
|
|
64
|
+
bits << "x#{event.streak} in a row" if event.streak > 1
|
|
65
|
+
bits << "combo reset (#{event.info[:broken_combo]})" if event.info[:broken_combo].to_i >= 3
|
|
66
|
+
bits.join(" ")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def draw_detail(c, t)
|
|
70
|
+
return if t < 0.15
|
|
71
|
+
txt = detail_text
|
|
72
|
+
n = (txt.size * Fx.phase(t, 0.15, 0.45)).round
|
|
73
|
+
y = @scale ? 6 : 1
|
|
74
|
+
c.put_center(y, txt[0, n], [255, 170, 170], bold: true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def trail
|
|
78
|
+
s = "#{Term.glyph('✖', 'x')} #{title}"
|
|
79
|
+
s << " x#{event.streak}" if event.streak > 1
|
|
80
|
+
paint(s, Color.ramp(palette, 0.25), bold: true)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# SyntaxError: the input line cracks at the error position.
|
|
85
|
+
class Crack < FailureScene
|
|
86
|
+
def title = "SYNTAX BREAK"
|
|
87
|
+
|
|
88
|
+
def setup
|
|
89
|
+
super
|
|
90
|
+
@shards = Fx::Particles.new(@rng)
|
|
91
|
+
@pos = ctx.input && event.info[:line] ? ctx.input.locate(event.info[:line] - 1, event.info[:column].to_i) : nil
|
|
92
|
+
@pos ||= ctx.input ? [ctx.input.size - 1, [ctx.input.plain[-1].size, 1].max - 1] : nil
|
|
93
|
+
@path = []
|
|
94
|
+
return unless @pos
|
|
95
|
+
x = @pos[1].to_f
|
|
96
|
+
rows = (ctx.input.size - @pos[0]) + height
|
|
97
|
+
(rows * 4).times do |i|
|
|
98
|
+
x += (@rng.rand - 0.5) * 2.4
|
|
99
|
+
@path << [x, @pos[0] + i / 4.0]
|
|
100
|
+
end
|
|
101
|
+
@shards.burst(@pos[1], 0.3, 24, at: 0.05, speed: 25, life: 0.6, palette: [[255, 255, 255], [255, 80, 80], [90, 20, 30]],
|
|
102
|
+
gravity: 22, glyphs: ["/", "\\", "'", ",", "."])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def mark_top(c, t)
|
|
106
|
+
return unless @pos
|
|
107
|
+
row, x = @pos
|
|
108
|
+
c.bg(x, row, t < 0.3 ? [255, 40, 60] : [120, 20, 30])
|
|
109
|
+
draw_crack(c, t, 0)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def draw(c, t)
|
|
113
|
+
super
|
|
114
|
+
return unless @pos
|
|
115
|
+
draw_crack(c, t, -ctx.input.size)
|
|
116
|
+
@shards.draw(c, t)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def detail_text
|
|
120
|
+
d = event.info[:detail]
|
|
121
|
+
loc = event.info[:line] ? "line #{event.info[:line]}:#{event.info[:column].to_i + 1}" : nil
|
|
122
|
+
[loc, d && d[0, [@w - 20, 20].max], ("x#{event.streak} in a row" if event.streak > 1)].compact.join(" ")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def draw_crack(c, t, yoff)
|
|
128
|
+
n = (@path.size * Fx.phase(t, 0, 0.18)).round
|
|
129
|
+
@path.first(n).each_with_index do |(x, y), i|
|
|
130
|
+
col = Color.mix([255, 255, 255], [255, 60, 70], i.fdiv(@path.size) + Fx.phase(t, 0.2, 0.8))
|
|
131
|
+
c.dot(x * 2, (y + yoff) * 4, col)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# NameError: the unknown name is put under a spotlight.
|
|
137
|
+
class Spotlight < FailureScene
|
|
138
|
+
def title = "UNKNOWN SYMBOL"
|
|
139
|
+
def palette = [[255, 250, 220], [255, 210, 90], [230, 140, 30], [140, 60, 20], [50, 20, 10]]
|
|
140
|
+
|
|
141
|
+
def setup
|
|
142
|
+
super
|
|
143
|
+
@hits = ctx.input ? ctx.input.find(event.info[:name]) : []
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def draw_top(c, t)
|
|
147
|
+
return nil if t > 0.75
|
|
148
|
+
ctx.input.plain.each_with_index { |r, y| c.put(0, y, r, [90, 90, 100]) }
|
|
149
|
+
pulse = 0.5 + 0.5 * Math.sin(t * 30)
|
|
150
|
+
@hits.each do |row, x, wd|
|
|
151
|
+
name = event.info[:name].to_s
|
|
152
|
+
c.put(x, row, name, [20, 10, 0], bg: Color.mix([255, 200, 60], [255, 255, 200], pulse), bold: true) if wd
|
|
153
|
+
end
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def draw(c, t)
|
|
158
|
+
super
|
|
159
|
+
@hits.first(3).each do |_row, x, wd|
|
|
160
|
+
height.times do |y|
|
|
161
|
+
k = (1 - y.fdiv(height)) * 0.45 * (1 - Fx.phase(t, 0.5, 0.85))
|
|
162
|
+
c.tint_bg([255, 220, 120], k, x - y / 2, y, wd + y, 1)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def detail_text
|
|
168
|
+
n = event.info[:name]
|
|
169
|
+
base = n ? "`#{n}` is not defined here" : event.info[:message].to_s[0, 60]
|
|
170
|
+
[base, ("x#{event.streak} in a row" if event.streak > 1)].compact.join(" ")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# NoMethodError: the link between receiver and method snaps.
|
|
175
|
+
class Snap < FailureScene
|
|
176
|
+
def title = "NO METHOD"
|
|
177
|
+
def palette = Color::NEON
|
|
178
|
+
def height = 4
|
|
179
|
+
def length = 0.9
|
|
180
|
+
|
|
181
|
+
def setup
|
|
182
|
+
super
|
|
183
|
+
@scale = nil
|
|
184
|
+
@left = " #{event.info[:receiver]} "
|
|
185
|
+
@right = " .#{event.info[:name]} "
|
|
186
|
+
@parts = Fx::Particles.new(@rng)
|
|
187
|
+
@mid = @w / 2.0
|
|
188
|
+
@parts.burst(@mid, 1.5, 40, at: 0.28, speed: 30, life: 0.45, palette: [[255, 255, 255], [255, 230, 120], [255, 90, 200], [60, 20, 80]], gravity: 12)
|
|
189
|
+
@hits = ctx.input ? ctx.input.find(event.info[:name]) : []
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def draw_top(c, t)
|
|
193
|
+
return nil if t > 0.6
|
|
194
|
+
ctx.input.plain.each_with_index { |r, y| c.put(0, y, r, [120, 120, 135]) }
|
|
195
|
+
@hits.each { |row, x, _| c.put(x, row, event.info[:name].to_s, [255, 255, 255], bg: [150, 40, 170], bold: true) }
|
|
196
|
+
true
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def draw(c, t)
|
|
200
|
+
gap = t < 0.28 ? 0 : (Fx.ease_out(Fx.phase(t, 0.28, 0.6)) * 8).round
|
|
201
|
+
lx = [(@mid - 14 - gap - @left.size).round, 0].max
|
|
202
|
+
rx = [(@mid + 14 + gap).round, @w - @right.size - 1].min
|
|
203
|
+
c.put(lx, 1, @left, [255, 255, 255], bg: [40, 90, 200], bold: true)
|
|
204
|
+
c.put(rx, 1, @right, [255, 255, 255], bg: [170, 40, 160], bold: true)
|
|
205
|
+
a = lx + @left.size
|
|
206
|
+
b = rx - 1
|
|
207
|
+
if t < 0.28
|
|
208
|
+
reach = a + (b - a) * Fx.ease_out(t / 0.2)
|
|
209
|
+
(a..reach.round).each do |x|
|
|
210
|
+
wob = Math.sin(x * 1.3 + t * 60) * 0.35
|
|
211
|
+
c.dot(x * 2, (1.5 + wob) * 4, Color.mix([150, 220, 255], [255, 255, 255], @rng.rand))
|
|
212
|
+
end
|
|
213
|
+
else
|
|
214
|
+
fall = Fx.phase(t, 0.28, 0.9)
|
|
215
|
+
(a..(@mid - gap / 2.0).round).each { |x| c.dot(x * 2, (1.5 + fall * fall * 2 * ((x - a) / 10.0)) * 4, [120, 170, 220]) }
|
|
216
|
+
((@mid + gap / 2.0).round..b).each { |x| c.dot(x * 2, (1.5 + fall * fall * 2 * ((b - x) / 10.0)) * 4, [200, 120, 210]) }
|
|
217
|
+
@parts.draw(c, t)
|
|
218
|
+
c.put_center(0, "N O M E T H O D", Color.ramp(palette, 0.15), bold: true)
|
|
219
|
+
txt = detail_text
|
|
220
|
+
c.put_center(3, txt[0, (txt.size * Fx.phase(t, 0.35, 0.65)).round], [230, 190, 255])
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def detail_text
|
|
225
|
+
[event.info[:message].to_s[0, [@w - 4, 20].max], ("x#{event.streak} in a row" if event.streak > 1)].compact.join(" ")
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# TypeError / ArgumentError: the two sides collide.
|
|
230
|
+
class Clash < FailureScene
|
|
231
|
+
def title = event.info[:type] == :argument ? "ARGUMENT CLASH" : "TYPE CLASH"
|
|
232
|
+
def palette = Color::FIRE
|
|
233
|
+
def length = 0.95
|
|
234
|
+
|
|
235
|
+
def setup
|
|
236
|
+
super
|
|
237
|
+
@scale = nil
|
|
238
|
+
l, r = event.info[:sides]
|
|
239
|
+
@l = " #{l} "
|
|
240
|
+
@r = " #{r} "
|
|
241
|
+
@parts = Fx::Particles.new(@rng)
|
|
242
|
+
@cx = @w / 2.0
|
|
243
|
+
@parts.burst(@cx, 2, 70, at: 0.3, speed: 60, life: 0.55, palette: Color::FIRE, gravity: 16)
|
|
244
|
+
@parts.burst(@cx, 2, 16, at: 0.32, speed: 35, life: 0.5, palette: Color::SMOKE, gravity: 4, glyphs: ["*", "#", "%", "@"])
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def height = 5
|
|
248
|
+
def shake(t) = t.between?(0.3, 0.5) ? ((@rng.rand * 2 - 1) * 4 * (1 - Fx.phase(t, 0.3, 0.5))).round : 0
|
|
249
|
+
def flash_at = 0.3
|
|
250
|
+
|
|
251
|
+
def draw_top(c, t) = t < 0.3 ? nil : super
|
|
252
|
+
|
|
253
|
+
def draw(c, t)
|
|
254
|
+
if t < 0.3
|
|
255
|
+
p = Fx.ease_in(t / 0.3)
|
|
256
|
+
lx = (@cx - @l.size) * p
|
|
257
|
+
rx = @w - @r.size - (@w - @r.size - @cx) * p
|
|
258
|
+
c.put(lx, 2, @l, [255, 255, 255], bg: [30, 120, 220], bold: true)
|
|
259
|
+
c.put(rx, 2, @r, [255, 255, 255], bg: [220, 90, 20], bold: true)
|
|
260
|
+
(0..3).each { |k| c.put(lx - k - 1, 2, "-", Color.scale([30, 120, 220], 1 - k / 4.0)) }
|
|
261
|
+
(0..3).each { |k| c.put(rx + @r.size + k, 2, "-", Color.scale([220, 90, 20], 1 - k / 4.0)) }
|
|
262
|
+
else
|
|
263
|
+
p = Fx.phase(t, 0.3, 0.95)
|
|
264
|
+
back = Fx.ease_out(p) * 10
|
|
265
|
+
c.put(@cx - @l.size - back, 2, @l, Color.mix([255, 255, 255], [90, 90, 100], p), bg: Color.mix([30, 120, 220], [20, 30, 50], p))
|
|
266
|
+
c.put(@cx + back, 2, @r, Color.mix([255, 255, 255], [90, 90, 100], p), bg: Color.mix([220, 90, 20], [50, 25, 10], p))
|
|
267
|
+
Fx.shockwave(c, @cx, 2, t - 0.3, speed: 80, palette: Color::FIRE, life: 0.4, rings: 3)
|
|
268
|
+
@parts.draw(c, t)
|
|
269
|
+
c.tint_bg([255, 200, 120], 0.6 * (1 - Fx.phase(t, 0.3, 0.42)), 0, 0, @w, height) if ctx.config.flash != :off
|
|
270
|
+
c.put_center(0, title.chars.join(" "), Color.ramp(palette, 0.2), bold: true) if t > 0.36
|
|
271
|
+
txt = detail_text
|
|
272
|
+
c.put_center(4, txt[0, (txt.size * Fx.phase(t, 0.4, 0.7)).round], [255, 200, 170]) if t > 0.4
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def detail_text
|
|
277
|
+
[event.info[:message].to_s[0, [@w - 4, 20].max], ("x#{event.streak} in a row" if event.streak > 1)].compact.join(" ")
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Ctrl-C: the collected charge scatters and fades.
|
|
282
|
+
class Scatter < Scene
|
|
283
|
+
def height = 2
|
|
284
|
+
def length = 0.65
|
|
285
|
+
def big? = true
|
|
286
|
+
|
|
287
|
+
def setup
|
|
288
|
+
@parts = Fx::Particles.new(@rng)
|
|
289
|
+
n = [(event.info[:charge].to_f / 4).round, 6].max
|
|
290
|
+
n.times do |i|
|
|
291
|
+
@parts.burst(2 + i, 0.5, 4, at: 0.05, speed: 30, life: 0.5, palette: Color::STEEL, gravity: 6)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def draw(c, t)
|
|
296
|
+
@parts.draw(c, t)
|
|
297
|
+
word = "INTERRUPTED"
|
|
298
|
+
sp = ((1 - Fx.ease_out(Fx.phase(t, 0, 0.35))) * 3).round + 1
|
|
299
|
+
txt = word.chars.join(" " * sp)
|
|
300
|
+
c.put(2, 1, txt[0, @w - 3], Color.mix([255, 255, 255], [120, 140, 180], Fx.phase(t, 0.1, 0.6)), bold: true)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def trail = paint("#{Term.glyph('✖', 'x')} INTERRUPTED", [140, 160, 200], bold: true)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
module Scenes
|
|
5
|
+
# Startup title: letters drop in one by one, each with its own impact.
|
|
6
|
+
class Intro < Scene
|
|
7
|
+
TITLE = "DOPA IRB"
|
|
8
|
+
TAG = "every keystroke counts."
|
|
9
|
+
|
|
10
|
+
def big? = true
|
|
11
|
+
|
|
12
|
+
def setup
|
|
13
|
+
@scale = best_scale(TITLE)
|
|
14
|
+
@parts = Fx::Particles.new(@rng)
|
|
15
|
+
return unless @scale
|
|
16
|
+
@letters = []
|
|
17
|
+
x = (@w - Font.width(TITLE, scale: @scale)) / 2
|
|
18
|
+
TITLE.each_char.with_index do |ch, i|
|
|
19
|
+
lw = Font.width(ch, scale: @scale)
|
|
20
|
+
at = 0.08 + i * 0.07
|
|
21
|
+
@letters << [ch, x, at]
|
|
22
|
+
unless ch == " "
|
|
23
|
+
@parts.burst(x + lw / 2.0, 6, 12, at: at + 0.12, speed: 30, life: 0.45, palette: Color.const_get(%i[FIRE GOLD NEON ICE TOXIC][i % 5]),
|
|
24
|
+
gravity: 20, angle: -Math::PI / 2, arc: Math::PI * 1.4)
|
|
25
|
+
end
|
|
26
|
+
x += lw + @scale
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def height = @scale ? 9 : 2
|
|
31
|
+
def length = 1.35
|
|
32
|
+
|
|
33
|
+
def shake(t)
|
|
34
|
+
return 0 unless @letters
|
|
35
|
+
@letters.any? { |_, _, at| (t - at - 0.12).between?(0, 0.05) } ? (@rng.rand(3) - 1) : 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def draw(c, t)
|
|
39
|
+
unless @scale
|
|
40
|
+
c.put(1, 0, "D O P A I R B", Color.rainbow(t), bold: true)
|
|
41
|
+
c.put(1, 1, TAG, [200, 200, 210]) if t > 0.4
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
@letters.each_with_index do |(ch, x, at), i|
|
|
45
|
+
lt = t - at
|
|
46
|
+
next if lt < 0
|
|
47
|
+
y = lt < 0.12 ? 1 - ((1 - Fx.ease_in(lt / 0.12)) * 7).round : 1
|
|
48
|
+
fn = lambda do |_rx, ry, _f|
|
|
49
|
+
base = Color.rainbow(i / 9.0 - t * 0.6 + ry * 0.02, 0.75)
|
|
50
|
+
lt < 0.2 ? Color.mix([255, 255, 255], base, lt / 0.2) : base
|
|
51
|
+
end
|
|
52
|
+
Fx.big_text(c, ch, x, y, fn, scale: @scale)
|
|
53
|
+
end
|
|
54
|
+
@parts.draw(c, t)
|
|
55
|
+
Fx.sparkles(c, t, 11, 16, palette: Color::GOLD) if t > 0.7
|
|
56
|
+
n = (TAG.size * Fx.phase(t, 0.75, 1.1)).round
|
|
57
|
+
c.put_center(7, TAG[0, n], [230, 230, 240], bold: true) if n > 0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def trail
|
|
61
|
+
star = Term.glyph("✦", "*")
|
|
62
|
+
title = "#{star} DOPA IRB #{VERSION} #{star}"
|
|
63
|
+
colored = depth == :none ? title : title.each_char.with_index.map { |ch, i| "#{Color.sgr(Color.rainbow(i / 22.0), nil, true, depth)}#{ch}" }.join + "\e[0m"
|
|
64
|
+
"#{colored} #{paint("Ruby #{RUBY_VERSION} -- type `dopa` for effect settings", [150, 150, 165])}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Session result screen. Leaves a plain text table behind.
|
|
69
|
+
class Outro < Scene
|
|
70
|
+
def big? = true
|
|
71
|
+
|
|
72
|
+
def initialize(ctx, stats)
|
|
73
|
+
@stats = stats
|
|
74
|
+
super(ctx, nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def rows_data
|
|
78
|
+
s = @stats
|
|
79
|
+
[
|
|
80
|
+
["EVALS", s[:evals]],
|
|
81
|
+
["HITS", s[:successes]],
|
|
82
|
+
["ERRORS", s[:failures]],
|
|
83
|
+
["INTERRUPTS", s[:interrupts]],
|
|
84
|
+
["MAX COMBO", s[:max_combo]],
|
|
85
|
+
["KEYSTROKES", s[:keystrokes]],
|
|
86
|
+
["SCORE", s[:score]],
|
|
87
|
+
]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def setup
|
|
91
|
+
@scale = Fx.text_fits?("RESULT", @w) ? 1 : nil
|
|
92
|
+
@parts = Fx::Particles.new(@rng)
|
|
93
|
+
@parts.burst(@w / 2.0, 3, 60, at: 0.25, speed: 50, life: 0.7, palette: Color::GOLD, gravity: 12)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def height = (@scale ? 6 : 1) + rows_data.size + 2
|
|
97
|
+
def length = 1.6
|
|
98
|
+
def flash_at = 0.25
|
|
99
|
+
|
|
100
|
+
def draw(c, t)
|
|
101
|
+
y = 0
|
|
102
|
+
if @scale
|
|
103
|
+
fn = ->(_rx, ry, f) { Color.rainbow(f * 0.6 - t * 0.5 + ry * 0.03, 0.7) }
|
|
104
|
+
drop = t < 0.25 ? ((1 - Fx.ease_in(t / 0.25)) * 6).round : 0
|
|
105
|
+
Fx.big_text(c, "RESULT", (@w - Font.width("RESULT")) / 2, 0 - drop, fn)
|
|
106
|
+
@parts.draw(c, t)
|
|
107
|
+
y = 6
|
|
108
|
+
else
|
|
109
|
+
c.put(1, 0, "R E S U L T", Color.rainbow(t), bold: true)
|
|
110
|
+
y = 1
|
|
111
|
+
end
|
|
112
|
+
x = [(@w - 34) / 2, 1].max
|
|
113
|
+
rows_data.each_with_index do |(k, v), i|
|
|
114
|
+
at = 0.3 + i * 0.1
|
|
115
|
+
next if t < at
|
|
116
|
+
p = Fx.phase(t, at, at + 0.35)
|
|
117
|
+
shown = Fx.number_with_commas((v * Fx.ease_out(p)).round)
|
|
118
|
+
c.put(x, y + i, k.ljust(14, "."), [160, 160, 175])
|
|
119
|
+
col = k == "SCORE" ? Color.rainbow(t) : (p < 1 ? [255, 255, 255] : [255, 220, 130])
|
|
120
|
+
c.put(x + 14, y + i, shown.rjust(16), col, bold: true)
|
|
121
|
+
end
|
|
122
|
+
at = 0.3 + rows_data.size * 0.1
|
|
123
|
+
c.put(x, y + rows_data.size, "TIME".ljust(14, "."), [160, 160, 175]) if t > at
|
|
124
|
+
c.put(x + 14, y + rows_data.size, clock(@stats[:time]).rjust(16), [255, 220, 130], bold: true) if t > at
|
|
125
|
+
Fx.sparkles(c, t, 5, 14, palette: Color::GOLD) if t > 0.8
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def trail
|
|
129
|
+
x = " "
|
|
130
|
+
lines = [paint("#{x}#{Term.glyph('✦', '*')} DOPA IRB RESULT #{Term.glyph('✦', '*')}", [255, 210, 110], bold: true)]
|
|
131
|
+
rows_data.each { |k, v| lines << "#{x}#{k.ljust(12)}#{Fx.number_with_commas(v).rjust(14)}" }
|
|
132
|
+
lines << "#{x}#{'TIME'.ljust(12)}#{clock(@stats[:time]).rjust(14)}"
|
|
133
|
+
lines.join("\n")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def clock(sec)
|
|
139
|
+
sec = sec.to_i
|
|
140
|
+
h, rest = sec.divmod(3600)
|
|
141
|
+
m, s = rest.divmod(60)
|
|
142
|
+
h > 0 ? format("%dh%02dm%02ds", h, m, s) : format("%dm%02ds", m, s)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|