dopairb 0.1.0 → 0.2.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/README.md +24 -3
- data/lib/dopairb/canvas.rb +39 -16
- data/lib/dopairb/cli.rb +2 -2
- data/lib/dopairb/config.rb +7 -3
- data/lib/dopairb/demo.rb +13 -2
- data/lib/dopairb/director.rb +40 -7
- data/lib/dopairb/game.rb +7 -3
- data/lib/dopairb/input_fx.rb +15 -1
- data/lib/dopairb/output_tap.rb +10 -0
- data/lib/dopairb/scene.rb +7 -0
- data/lib/dopairb/scenes/failure.rb +6 -0
- data/lib/dopairb/scenes/finale.rb +286 -0
- data/lib/dopairb/scenes/session.rb +94 -0
- data/lib/dopairb/scenes/success.rb +11 -0
- data/lib/dopairb/session.rb +8 -1
- data/lib/dopairb/sound.rb +365 -0
- data/lib/dopairb/version.rb +1 -1
- data/lib/dopairb.rb +16 -2
- metadata +3 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
module Scenes
|
|
5
|
+
# Full-screen session result: title drop, stats slam in one by one,
|
|
6
|
+
# drum-rolled total score, a rank stamp and fireworks. The :finale sound
|
|
7
|
+
# is one track built on the same timeline (Finale::Timeline).
|
|
8
|
+
class Finale < Scene
|
|
9
|
+
module Timeline
|
|
10
|
+
DROP = 0.35
|
|
11
|
+
ROW0 = 0.55
|
|
12
|
+
ROW_GAP = 0.17
|
|
13
|
+
SLIDE = 0.08
|
|
14
|
+
COUNT = 0.24
|
|
15
|
+
ROWS = 8
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def row_start(i) = ROW0 + i * ROW_GAP
|
|
20
|
+
def row_land(i) = row_start(i) + SLIDE + COUNT
|
|
21
|
+
def score_start = row_land(ROWS - 1) + 0.15
|
|
22
|
+
def score_land = score_start + 0.85
|
|
23
|
+
def rank = score_land + 0.3
|
|
24
|
+
def length = rank + 1.9
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
T = Timeline
|
|
28
|
+
NOTES = [1047, 1175, 1319, 1397, 1568, 1760, 1976, 2093].freeze
|
|
29
|
+
|
|
30
|
+
RANKS = [[15_000, "S"], [6_000, "A"], [2_000, "B"], [0, "C"]].freeze
|
|
31
|
+
RANK_PALETTE = { "S" => Color::NEON, "A" => Color::GOLD, "B" => Color::ICE, "C" => Color::TOXIC }.freeze
|
|
32
|
+
|
|
33
|
+
def self.rank_for(stats)
|
|
34
|
+
RANKS.find { |min, _| stats[:score].to_i >= min }[1]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A cheerful title; never a verdict on the code.
|
|
38
|
+
def self.title_for(s)
|
|
39
|
+
if s[:max_combo].to_i >= 20 then "COMBO LEGEND"
|
|
40
|
+
elsif s[:comebacks].to_i >= 3 then "COMEBACK KID"
|
|
41
|
+
elsif s[:max_combo].to_i >= 10 then "COMBO MASTER"
|
|
42
|
+
elsif s[:failures].to_i >= 3 && s[:successes].to_i > s[:failures].to_i then "NEVER GIVE UP"
|
|
43
|
+
elsif s[:keystrokes].to_i >= 2000 then "IRON FINGERS"
|
|
44
|
+
elsif s[:evals].to_i >= 100 then "MARATHONER"
|
|
45
|
+
elsif s[:best_typing].to_i >= 60 then "SPEED TYPER"
|
|
46
|
+
else "WELL PLAYED"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.fits?(rows, cols) = rows >= 20 && cols >= 61
|
|
51
|
+
|
|
52
|
+
def initialize(ctx, stats)
|
|
53
|
+
@stats = stats
|
|
54
|
+
super(ctx, nil)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def alt? = true
|
|
58
|
+
def big? = true
|
|
59
|
+
def mega? = true
|
|
60
|
+
def height = ctx.rows
|
|
61
|
+
def length = T.length
|
|
62
|
+
def sfx = :finale
|
|
63
|
+
def impact_at = 0.0
|
|
64
|
+
def flash_at = T.score_land
|
|
65
|
+
|
|
66
|
+
def rows_data
|
|
67
|
+
s = @stats
|
|
68
|
+
[
|
|
69
|
+
["EVALS", s[:evals]],
|
|
70
|
+
["HITS", s[:successes]],
|
|
71
|
+
["ERRORS", s[:failures]],
|
|
72
|
+
["COMEBACKS", s[:comebacks].to_i],
|
|
73
|
+
["MAX COMBO", s[:max_combo]],
|
|
74
|
+
["KEYSTROKES", s[:keystrokes]],
|
|
75
|
+
["INTERRUPTS", s[:interrupts]],
|
|
76
|
+
["TIME", s[:time].to_f],
|
|
77
|
+
]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def setup
|
|
81
|
+
@h = ctx.rows
|
|
82
|
+
@rank = self.class.rank_for(@stats)
|
|
83
|
+
@title = self.class.title_for(@stats)
|
|
84
|
+
@rpal = RANK_PALETTE[@rank]
|
|
85
|
+
@tscale = Font.width("RESULT", scale: 2) <= @w - 4 ? 2 : 1
|
|
86
|
+
@two_col = @w >= 80
|
|
87
|
+
@big_score = @h >= 23
|
|
88
|
+
@table_x = @two_col ? @w / 2 - 38 : (@w - 34) / 2
|
|
89
|
+
@table_y = 8
|
|
90
|
+
@panel = [@w / 2 + 4, @table_y, 30, 9] if @two_col
|
|
91
|
+
@score_y = @h - 7
|
|
92
|
+
@score = @stats[:score].to_i
|
|
93
|
+
@stars = Array.new(80) { [@rng.rand(@w), @rng.rand(@h), @rng.rand, 0.4 + @rng.rand] }
|
|
94
|
+
build_particles
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_particles
|
|
98
|
+
@parts = Fx::Particles.new(@rng)
|
|
99
|
+
tw = Font.width("RESULT", scale: @tscale)
|
|
100
|
+
@parts.burst(@w / 2.0, 6, 70, at: T::DROP, speed: 60, life: 0.6, palette: Color::GOLD, gravity: 18,
|
|
101
|
+
angle: -Math::PI / 2, arc: Math::PI * 1.5)
|
|
102
|
+
8.times { |i| @parts.burst((@w - tw) / 2.0 + tw * i / 7.0, 5.5, 5, at: T::DROP, speed: 30, life: 0.45, palette: Color::FIRE, gravity: 20) }
|
|
103
|
+
T::ROWS.times do |i|
|
|
104
|
+
@parts.burst(@table_x + 31, @table_y + i + 0.5, 10, at: T.row_land(i), speed: 22, life: 0.35,
|
|
105
|
+
palette: Color::GOLD, gravity: 8)
|
|
106
|
+
end
|
|
107
|
+
cy = @big_score ? @score_y + 2.5 : @table_y + 4
|
|
108
|
+
@parts.burst(@w / 2.0, cy, 140, at: T.score_land, speed: 90, life: 0.8, palette: Color::GOLD, gravity: 16)
|
|
109
|
+
@parts.burst(@w / 2.0, cy, 50, at: T.score_land + 0.03, speed: 55, life: 0.7, palette: Color::FIRE, gravity: 12,
|
|
110
|
+
glyphs: ["*", "+", Term.glyph("✦", "*"), "'"])
|
|
111
|
+
if @panel
|
|
112
|
+
px, py, pw, ph = @panel
|
|
113
|
+
@parts.burst(px + pw / 2.0, py + ph / 2.0, 60, at: T.rank, speed: 45, life: 0.55,
|
|
114
|
+
palette: [[255, 255, 255], *@rpal[0, 3]], gravity: 14)
|
|
115
|
+
end
|
|
116
|
+
@fire = Fx::Particles.new(@rng)
|
|
117
|
+
pals = [Color::FIRE, Color::ICE, Color::NEON, Color::TOXIC, Color::GOLD]
|
|
118
|
+
10.times do |i|
|
|
119
|
+
at = T.rank + 0.2 + i * 0.13
|
|
120
|
+
x = 4 + @rng.rand * (@w - 8)
|
|
121
|
+
y = 2 + @rng.rand * (@h * 0.45)
|
|
122
|
+
@fire.add(x, @h.to_f, 0, -(@h - y) / 0.15 * 2, at: at - 0.15, life: 0.15, palette: [[255, 255, 255], [255, 200, 120]])
|
|
123
|
+
@fire.burst(x, y, 55, at: at, speed: 50, life: 0.9, palette: pals[i % pals.size], gravity: 9, drag: 2.2)
|
|
124
|
+
end
|
|
125
|
+
90.times do
|
|
126
|
+
@fire.add(@rng.rand * @w, -1, (@rng.rand - 0.5) * 5, 3 + @rng.rand * 5, at: T.rank + @rng.rand * 1.4, life: 1.8,
|
|
127
|
+
palette: [Color.rainbow(@rng.rand), Color.rainbow(@rng.rand)],
|
|
128
|
+
glyph: ["*", "+", "'", ",", Term.glyph("✦", "*")][@rng.rand(5)], gravity: 1.2)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def shake(t)
|
|
133
|
+
[[T::DROP, 4, 0.3], [T.score_land, 6, 0.4], [T.rank, 4, 0.3]].each do |at, amp, len|
|
|
134
|
+
age = t - at
|
|
135
|
+
return ((@rng.rand * 2 - 1) * amp * (1 - age / len)).round if age >= 0 && age < len
|
|
136
|
+
end
|
|
137
|
+
0
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def draw(c, t)
|
|
141
|
+
draw_stars(c, t)
|
|
142
|
+
flash(c, t)
|
|
143
|
+
draw_title(c, t)
|
|
144
|
+
draw_table(c, t)
|
|
145
|
+
draw_score(c, t)
|
|
146
|
+
draw_rank(c, t)
|
|
147
|
+
@parts.draw(c, t)
|
|
148
|
+
@fire.draw(c, t)
|
|
149
|
+
draw_footer(c, t)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def trail
|
|
153
|
+
x = " "
|
|
154
|
+
star = Term.glyph("✦", "*")
|
|
155
|
+
lines = [paint("#{x}#{star} DOPA IRB RESULT #{star}", [255, 210, 110], bold: true)]
|
|
156
|
+
rows_data.each { |k, v| lines << "#{x}#{k.ljust(12)}#{value_text(k, v).rjust(14)}" }
|
|
157
|
+
lines << paint("#{x}#{'SCORE'.ljust(12)}#{Fx.number_with_commas(@score).rjust(14)}", [255, 220, 130], bold: true)
|
|
158
|
+
lines << paint("#{x}#{'RANK'.ljust(12)}#{@rank.rjust(14)} #{@title}", Color.ramp(@rpal, 0.2), bold: true)
|
|
159
|
+
lines.join("\n")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def value_text(k, v)
|
|
165
|
+
k == "TIME" ? clock(v) : Fx.number_with_commas(v.to_i)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def draw_stars(c, t)
|
|
169
|
+
@stars.each do |x, y, ph, sp|
|
|
170
|
+
f = ((t * sp + ph) % 1.0)
|
|
171
|
+
next if f > 0.5
|
|
172
|
+
k = 1 - (f - 0.25).abs * 4
|
|
173
|
+
c.put(x, y, k > 0.7 ? "+" : ".", Color.mix([40, 40, 70], [200, 200, 255], k))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def flash(c, t)
|
|
178
|
+
return if ctx.config.flash == :off
|
|
179
|
+
[[T::DROP, 0.12, [255, 255, 255]], [T.score_land, 0.18, [255, 230, 150]], [T.rank, 0.12, Color.ramp(@rpal, 0.2)]].each do |at, len, col|
|
|
180
|
+
age = t - at
|
|
181
|
+
c.tint_bg(col, 0.55 * (1 - age / len)) if age >= 0 && age < len
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def draw_title(c, t)
|
|
186
|
+
drop = t < T::DROP ? ((1 - Fx.ease_in(t / T::DROP)) * 8).round : 0
|
|
187
|
+
sweep = (t * 0.9) % 1.4 - 0.2
|
|
188
|
+
fn = lambda do |_rx, ry, f|
|
|
189
|
+
base = Color.rainbow(f * 0.7 - t * 0.5 + ry * 0.03, 0.75)
|
|
190
|
+
d = (f - sweep).abs
|
|
191
|
+
d < 0.06 ? Color.mix([255, 255, 255], base, d / 0.06) : base
|
|
192
|
+
end
|
|
193
|
+
Fx.big_text(c, "RESULT", (@w - Font.width("RESULT", scale: @tscale)) / 2, 1 - drop, fn, scale: @tscale)
|
|
194
|
+
Fx.shockwave(c, @w / 2.0, 3.5, t - T::DROP, speed: 110, palette: Color::GOLD, life: 0.6, rings: 3)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def draw_table(c, t)
|
|
198
|
+
rows_data.each_with_index do |(k, v), i|
|
|
199
|
+
st = T.row_start(i)
|
|
200
|
+
next if t < st
|
|
201
|
+
y = @table_y + i
|
|
202
|
+
slide = Fx.ease_out((t - st) / T::SLIDE)
|
|
203
|
+
p = Fx.phase(t, st + T::SLIDE, T.row_land(i))
|
|
204
|
+
landed = t >= T.row_land(i)
|
|
205
|
+
lx = @table_x - ((1 - slide) * 20).round
|
|
206
|
+
vx = @table_x + 14 + ((1 - slide) * 30).round
|
|
207
|
+
age = t - T.row_land(i)
|
|
208
|
+
c.tint_bg([255, 200, 90], 0.45 * (1 - age / 0.25), @table_x - 1, y, 34, 1) if age >= 0 && age < 0.25
|
|
209
|
+
c.put(lx, y, k.ljust(14, "."), [150, 150, 175])
|
|
210
|
+
shown = k == "TIME" ? clock(v * Fx.ease_out(p)) : Fx.number_with_commas((v * Fx.ease_out(p)).round)
|
|
211
|
+
col = if !landed then [255, 255, 255]
|
|
212
|
+
elsif k == "ERRORS" || k == "INTERRUPTS" then [180, 190, 220]
|
|
213
|
+
else Color.ramp(Color::GOLD, 0.2)
|
|
214
|
+
end
|
|
215
|
+
c.put(vx, y, shown.rjust(16), col, bold: true)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def draw_score(c, t)
|
|
220
|
+
return if t < T.score_start
|
|
221
|
+
p = Fx.phase(t, T.score_start, T.score_land)
|
|
222
|
+
landed = t >= T.score_land
|
|
223
|
+
val = landed ? @score : (@score * Fx.ease_in(p) ** 0.7).round
|
|
224
|
+
txt = Fx.number_with_commas(val)
|
|
225
|
+
unless @big_score
|
|
226
|
+
c.put_center(@table_y + T::ROWS + 1, "TOTAL SCORE #{txt}", landed ? Color.rainbow(t) : [150, 220, 255], bold: true)
|
|
227
|
+
return
|
|
228
|
+
end
|
|
229
|
+
c.put_center(@score_y - 1, "T O T A L S C O R E", [170, 170, 200], bold: true)
|
|
230
|
+
scale = Font.width(Fx.number_with_commas(@score), scale: 2) <= @w - 4 ? 2 : 1
|
|
231
|
+
scale = 1 if Font.width(txt, scale: scale) > @w - 2
|
|
232
|
+
fn = lambda do |_rx, ry, f|
|
|
233
|
+
if landed
|
|
234
|
+
base = Color.ramp(Color::GOLD, 0.1 + ry * 0.12)
|
|
235
|
+
sweep = ((t - T.score_land) * 1.5) % 1.4 - 0.2
|
|
236
|
+
d = (f - sweep).abs
|
|
237
|
+
d < 0.08 ? Color.mix([255, 255, 255], base, d / 0.08) : base
|
|
238
|
+
else
|
|
239
|
+
Color.ramp(Color::ICE, 0.15 + ry * 0.1)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
jitter = landed ? 0 : @rng.rand(2)
|
|
243
|
+
Fx.big_text(c, txt, (@w - Font.width(txt, scale: scale)) / 2 + jitter, @score_y, fn, scale: scale)
|
|
244
|
+
Fx.shockwave(c, @w / 2.0, @score_y + 2.5, t - T.score_land, speed: 140, palette: Color::GOLD, life: 0.7, rings: 4)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def draw_rank(c, t)
|
|
248
|
+
if @panel.nil?
|
|
249
|
+
return if t < T.rank
|
|
250
|
+
c.put_center(@big_score ? @score_y - 3 : @table_y + T::ROWS + 3, "RANK #{@rank} #{@title}", Color.ramp(@rpal, 0.15), bold: true)
|
|
251
|
+
return
|
|
252
|
+
end
|
|
253
|
+
px, py, pw, ph = @panel
|
|
254
|
+
return if t < T.rank - 0.14
|
|
255
|
+
if t < T.rank
|
|
256
|
+
grow = ((1 - Fx.ease_in((t - T.rank + 0.14) / 0.14)) * 8).round
|
|
257
|
+
c.fill_bg(Color.scale(Color.ramp(@rpal, 0.5), 0.35), px - grow, py - grow / 2, pw + grow * 2, ph + grow)
|
|
258
|
+
return
|
|
259
|
+
end
|
|
260
|
+
c.fill_bg(Color.scale(Color.ramp(@rpal, 0.6), 0.45), px, py, pw, ph)
|
|
261
|
+
c.fill_bg([12, 12, 22], px + 1, py + 1, pw - 2, ph - 2)
|
|
262
|
+
c.put(px + (pw - 4) / 2, py + 1, "RANK", [200, 200, 220], bold: true)
|
|
263
|
+
lw = Font.width(@rank, scale: 2)
|
|
264
|
+
fn = lambda do |_rx, ry, f|
|
|
265
|
+
@rank == "S" ? Color.rainbow(f * 0.5 - t + ry * 0.05) : Color.ramp(@rpal, 0.05 + ry * 0.1)
|
|
266
|
+
end
|
|
267
|
+
Fx.big_text(c, @rank, px + (pw - lw) / 2, py + 2, fn, scale: 2)
|
|
268
|
+
n = (@title.size * Fx.phase(t, T.rank + 0.15, T.rank + 0.5)).round
|
|
269
|
+
c.put(px + (pw - @title.size) / 2, py + 7, @title[0, n], Color.ramp(@rpal, 0.1), bold: true)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def draw_footer(c, t)
|
|
273
|
+
msg = "thanks for playing #{Term.glyph('✦', '*')} see you next session"
|
|
274
|
+
n = (msg.size * Fx.phase(t, T.rank + 0.9, T.rank + 1.5)).round
|
|
275
|
+
c.put_center(@h - 1, msg[0, n], [170, 170, 200]) if n > 0
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def clock(sec)
|
|
279
|
+
sec = sec.to_i
|
|
280
|
+
h, rest = sec.divmod(3600)
|
|
281
|
+
m, s = rest.divmod(60)
|
|
282
|
+
h > 0 ? format("%dh%02dm%02ds", h, m, s) : format("%dm%02ds", m, s)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -8,6 +8,8 @@ module Dopairb
|
|
|
8
8
|
TAG = "every keystroke counts."
|
|
9
9
|
|
|
10
10
|
def big? = true
|
|
11
|
+
def sfx = :intro
|
|
12
|
+
def impact_at = 0.2
|
|
11
13
|
|
|
12
14
|
def setup
|
|
13
15
|
@scale = best_scale(TITLE)
|
|
@@ -68,6 +70,7 @@ module Dopairb
|
|
|
68
70
|
# Session result screen. Leaves a plain text table behind.
|
|
69
71
|
class Outro < Scene
|
|
70
72
|
def big? = true
|
|
73
|
+
def sfx = :result
|
|
71
74
|
|
|
72
75
|
def initialize(ctx, stats)
|
|
73
76
|
@stats = stats
|
|
@@ -144,3 +147,94 @@ module Dopairb
|
|
|
144
147
|
end
|
|
145
148
|
end
|
|
146
149
|
end
|
|
150
|
+
|
|
151
|
+
module Dopairb
|
|
152
|
+
module Scenes
|
|
153
|
+
# Shown at startup while the startup sound is being synthesized.
|
|
154
|
+
# A cat runs along a pastel progress bar; LOADING bounces; sparkles pop.
|
|
155
|
+
class Loading < Scene
|
|
156
|
+
PASTEL = [[255, 170, 210], [255, 205, 160], [255, 245, 170], [180, 245, 200], [170, 215, 255], [215, 185, 255]].freeze
|
|
157
|
+
MESSAGES = ["tuning the synthesizer", "polishing sparkles", "charging the combo meter",
|
|
158
|
+
"stacking fireworks", "waking up the cat"].freeze
|
|
159
|
+
MIN = 0.7
|
|
160
|
+
FINISH = 0.45
|
|
161
|
+
|
|
162
|
+
def initialize(ctx, ready)
|
|
163
|
+
@ready = ready
|
|
164
|
+
@ready_at = nil
|
|
165
|
+
super(ctx, nil)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def height = 6
|
|
169
|
+
def length = 8.0 # safety net; normally ends via done?
|
|
170
|
+
|
|
171
|
+
def setup
|
|
172
|
+
@parts = Fx::Particles.new(@rng)
|
|
173
|
+
@bar_x = 4
|
|
174
|
+
@bar_w = [[@w - 12, 60].min, 16].max
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def done?(t)
|
|
178
|
+
if @ready_at.nil? && t >= MIN && @ready.call
|
|
179
|
+
@ready_at = t
|
|
180
|
+
@parts.burst(@bar_x + @bar_w, 3, 40, at: t, speed: 40, life: 0.5, palette: PASTEL, gravity: 10)
|
|
181
|
+
end
|
|
182
|
+
@ready_at && t >= @ready_at + FINISH
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def progress(t)
|
|
186
|
+
fake = 1 - Math.exp(-t / 0.9) # creeps towards 1, never gets there
|
|
187
|
+
return 0.9 * fake unless @ready_at
|
|
188
|
+
0.9 * fake + (1 - 0.9 * fake) * Fx.ease_out((t - @ready_at) / 0.2)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def pastel(i, t) = Color.ramp(PASTEL, ((i * 0.13 + t * 0.6) % 1.0))
|
|
192
|
+
|
|
193
|
+
def draw(c, t)
|
|
194
|
+
Fx.sparkles(c, t, 21, 18, palette: PASTEL.reverse)
|
|
195
|
+
draw_word(c, t)
|
|
196
|
+
draw_bar(c, t)
|
|
197
|
+
@parts.draw(c, t)
|
|
198
|
+
msg = if @ready_at then "ready! #{Term.glyph('✦', '*')}"
|
|
199
|
+
else "#{MESSAGES[(t / 0.9).to_i % MESSAGES.size]}#{'.' * ((t * 4).to_i % 4)}"
|
|
200
|
+
end
|
|
201
|
+
c.put(@bar_x, 5, msg, @ready_at ? [255, 240, 170] : [200, 190, 230], bold: !@ready_at.nil?)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def draw_word(c, t)
|
|
207
|
+
word = @ready_at ? "READY!" : "LOADING"
|
|
208
|
+
x = @bar_x
|
|
209
|
+
word.each_char.with_index do |ch, i|
|
|
210
|
+
hop = Math.sin(t * 9 - i * 0.7)
|
|
211
|
+
y = hop > 0.55 ? 0 : 1
|
|
212
|
+
c.put(x, y, ch, pastel(i, t), bold: true)
|
|
213
|
+
x += 2
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def draw_bar(c, t)
|
|
218
|
+
p = progress(t)
|
|
219
|
+
filled = (p * @bar_w).round
|
|
220
|
+
y = 3
|
|
221
|
+
@bar_w.times do |i|
|
|
222
|
+
if i < filled
|
|
223
|
+
shimmer = ((i - t * 40) % 14).abs < 1.5
|
|
224
|
+
c.bg(@bar_x + i, y, shimmer ? [255, 255, 255] : pastel(i / 3, t))
|
|
225
|
+
else
|
|
226
|
+
c.put(@bar_x + i, y, Term.glyph("⣀", "."), [90, 80, 110])
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
run = (t * 10).to_i.even?
|
|
230
|
+
cat = @ready_at ? "=^o^=" : (run ? "=^.^=" : "=^-^=")
|
|
231
|
+
bounce = @ready_at && t - @ready_at < 0.25 ? 1 : (run ? 0 : 1)
|
|
232
|
+
cx = @bar_x + [filled - 2, 0].max
|
|
233
|
+
c.put(cx, y - 1 - bounce, cat, [255, 220, 235], bold: true)
|
|
234
|
+
c.put(cx - 2, y - 1, run ? "~" : "'", [255, 190, 220]) if !@ready_at && t % 0.2 < 0.1
|
|
235
|
+
pct = "#{(p * 100).floor}%"
|
|
236
|
+
c.put(@bar_x + @bar_w + 2, y, pct, [255, 240, 200], bold: true)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -6,6 +6,8 @@ module Dopairb
|
|
|
6
6
|
class Hit < Scene
|
|
7
7
|
def top? = !ctx.input.nil?
|
|
8
8
|
def length = 0.42
|
|
9
|
+
def sfx = :hit
|
|
10
|
+
def impact_at = 0.16
|
|
9
11
|
|
|
10
12
|
def setup
|
|
11
13
|
@target = [[@w - 30, 34].min, 8].max
|
|
@@ -46,6 +48,8 @@ module Dopairb
|
|
|
46
48
|
class Puff < Scene
|
|
47
49
|
def top? = !ctx.input.nil?
|
|
48
50
|
def length = 0.5
|
|
51
|
+
def sfx = :puff
|
|
52
|
+
def impact_at = 0.12
|
|
49
53
|
|
|
50
54
|
def setup
|
|
51
55
|
@parts = Fx::Particles.new(@rng)
|
|
@@ -71,6 +75,7 @@ module Dopairb
|
|
|
71
75
|
|
|
72
76
|
# true / false: an ink stamp slams down.
|
|
73
77
|
class Stamp < Scene
|
|
78
|
+
def sfx = :stamp
|
|
74
79
|
def height = 3
|
|
75
80
|
def length = 0.6
|
|
76
81
|
def top? = !ctx.input.nil?
|
|
@@ -112,6 +117,7 @@ module Dopairb
|
|
|
112
117
|
|
|
113
118
|
# Integers that deserve it: count up in giant digits, then a shockwave.
|
|
114
119
|
class Counter < Scene
|
|
120
|
+
def sfx = :counter
|
|
115
121
|
def big? = true
|
|
116
122
|
def top? = !ctx.input.nil?
|
|
117
123
|
def length = 1.0
|
|
@@ -184,6 +190,7 @@ module Dopairb
|
|
|
184
190
|
|
|
185
191
|
# Long strings: characters pour in while the length counts up.
|
|
186
192
|
class Stream < Scene
|
|
193
|
+
def sfx = :hit
|
|
187
194
|
def height = 2
|
|
188
195
|
def length = 0.75
|
|
189
196
|
def top? = !ctx.input.nil?
|
|
@@ -210,6 +217,7 @@ module Dopairb
|
|
|
210
217
|
|
|
211
218
|
# Arrays and hashes: one block per element pops into place.
|
|
212
219
|
class Crate < Scene
|
|
220
|
+
def sfx = :hit
|
|
213
221
|
def height = 2
|
|
214
222
|
def length = 0.7
|
|
215
223
|
def top? = !ctx.input.nil?
|
|
@@ -255,6 +263,8 @@ module Dopairb
|
|
|
255
263
|
|
|
256
264
|
def big? = true
|
|
257
265
|
def mega? = @mega
|
|
266
|
+
def alt? = @alt && @mega
|
|
267
|
+
def sfx = @mega ? :mega : :banner
|
|
258
268
|
def top? = !ctx.input.nil? && !@alt
|
|
259
269
|
def length = @mega ? 1.9 : 1.05
|
|
260
270
|
|
|
@@ -385,6 +395,7 @@ module Dopairb
|
|
|
385
395
|
|
|
386
396
|
# Floods of stdout: digital rain behind a counter.
|
|
387
397
|
class Rain < Scene
|
|
398
|
+
def sfx = :rain
|
|
388
399
|
def height = 5
|
|
389
400
|
def length = 0.9
|
|
390
401
|
def big? = true
|
data/lib/dopairb/session.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Dopairb
|
|
|
5
5
|
# cycle to the director.
|
|
6
6
|
class Session
|
|
7
7
|
attr_reader :config, :game, :director, :fx
|
|
8
|
+
attr_accessor :cold_start
|
|
8
9
|
|
|
9
10
|
def initialize(config)
|
|
10
11
|
@config = config
|
|
@@ -19,7 +20,13 @@ module Dopairb
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def intro
|
|
22
|
-
|
|
23
|
+
return unless active?
|
|
24
|
+
Sound.warm_up(@config.duration)
|
|
25
|
+
cold = @cold_start || (Sound.available? && !Sound.ready?(:intro, @config.duration))
|
|
26
|
+
if @config.intro && @config.motion && cold
|
|
27
|
+
@director.loading { Sound.ready?(:intro, @config.duration) }
|
|
28
|
+
end
|
|
29
|
+
@director.intro
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
def finish
|