dopairb 0.1.0 → 0.4.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 +63 -10
- data/lib/dopairb/canvas.rb +39 -16
- data/lib/dopairb/cli.rb +5 -7
- data/lib/dopairb/config.rb +11 -5
- data/lib/dopairb/demo.rb +22 -4
- data/lib/dopairb/director.rb +75 -12
- data/lib/dopairb/fx.rb +23 -0
- data/lib/dopairb/gallery/impressionism.rb +818 -0
- data/lib/dopairb/gallery/japan.rb +693 -0
- data/lib/dopairb/gallery/modern_ancient.rb +774 -0
- data/lib/dopairb/gallery/renaissance.rb +829 -0
- data/lib/dopairb/gallery/romantic.rb +703 -0
- data/lib/dopairb/gallery.rb +349 -0
- data/lib/dopairb/game.rb +90 -9
- data/lib/dopairb/input_fx.rb +78 -10
- data/lib/dopairb/irb_adapter.rb +8 -0
- data/lib/dopairb/output_tap.rb +10 -0
- data/lib/dopairb/profile.rb +106 -0
- data/lib/dopairb/reline_adapter.rb +1 -0
- data/lib/dopairb/scene.rb +12 -0
- data/lib/dopairb/scenes/bonus.rb +279 -0
- data/lib/dopairb/scenes/failure.rb +7 -0
- data/lib/dopairb/scenes/finale.rb +326 -0
- data/lib/dopairb/scenes/session.rb +115 -3
- data/lib/dopairb/scenes/success.rb +22 -4
- data/lib/dopairb/session.rb +57 -6
- data/lib/dopairb/sound.rb +420 -0
- data/lib/dopairb/term.rb +3 -1
- data/lib/dopairb/tour.rb +89 -0
- data/lib/dopairb/version.rb +1 -1
- data/lib/dopairb.rb +73 -3
- data/spec.md +3 -3
- metadata +12 -1
|
@@ -11,6 +11,7 @@ module Dopairb
|
|
|
11
11
|
|
|
12
12
|
def title = "FAILED"
|
|
13
13
|
def palette = Color::BLOOD
|
|
14
|
+
def sfx = :error
|
|
14
15
|
|
|
15
16
|
def setup
|
|
16
17
|
@scale = best_scale(title)
|
|
@@ -63,6 +64,7 @@ module Dopairb
|
|
|
63
64
|
bits = [event.info[:class_name]]
|
|
64
65
|
bits << "x#{event.streak} in a row" if event.streak > 1
|
|
65
66
|
bits << "combo reset (#{event.info[:broken_combo]})" if event.info[:broken_combo].to_i >= 3
|
|
67
|
+
bits << "FEVER over" if event.flag?(:fever_end)
|
|
66
68
|
bits.join(" ")
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -84,6 +86,7 @@ module Dopairb
|
|
|
84
86
|
# SyntaxError: the input line cracks at the error position.
|
|
85
87
|
class Crack < FailureScene
|
|
86
88
|
def title = "SYNTAX BREAK"
|
|
89
|
+
def sfx = :crack
|
|
87
90
|
|
|
88
91
|
def setup
|
|
89
92
|
super
|
|
@@ -174,6 +177,8 @@ module Dopairb
|
|
|
174
177
|
# NoMethodError: the link between receiver and method snaps.
|
|
175
178
|
class Snap < FailureScene
|
|
176
179
|
def title = "NO METHOD"
|
|
180
|
+
def sfx = :snap
|
|
181
|
+
def impact_at = 0.28
|
|
177
182
|
def palette = Color::NEON
|
|
178
183
|
def height = 4
|
|
179
184
|
def length = 0.9
|
|
@@ -231,6 +236,7 @@ module Dopairb
|
|
|
231
236
|
def title = event.info[:type] == :argument ? "ARGUMENT CLASH" : "TYPE CLASH"
|
|
232
237
|
def palette = Color::FIRE
|
|
233
238
|
def length = 0.95
|
|
239
|
+
def sfx = :crack
|
|
234
240
|
|
|
235
241
|
def setup
|
|
236
242
|
super
|
|
@@ -280,6 +286,7 @@ module Dopairb
|
|
|
280
286
|
|
|
281
287
|
# Ctrl-C: the collected charge scatters and fades.
|
|
282
288
|
class Scatter < Scene
|
|
289
|
+
def sfx = :interrupt
|
|
283
290
|
def height = 2
|
|
284
291
|
def length = 0.65
|
|
285
292
|
def big? = true
|
|
@@ -0,0 +1,326 @@
|
|
|
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
|
+
@new_best = !@stats[:first_session].nil? && !@stats[:first_session] && @score > @stats[:best_before].to_i
|
|
94
|
+
@xp_y = @big_score ? @h - 2 : @table_y + T::ROWS + 2
|
|
95
|
+
@stars = Array.new(80) { [@rng.rand(@w), @rng.rand(@h), @rng.rand, 0.4 + @rng.rand] }
|
|
96
|
+
build_particles
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_particles
|
|
100
|
+
@parts = Fx::Particles.new(@rng)
|
|
101
|
+
tw = Font.width("RESULT", scale: @tscale)
|
|
102
|
+
@parts.burst(@w / 2.0, 6, 70, at: T::DROP, speed: 60, life: 0.6, palette: Color::GOLD, gravity: 18,
|
|
103
|
+
angle: -Math::PI / 2, arc: Math::PI * 1.5)
|
|
104
|
+
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) }
|
|
105
|
+
T::ROWS.times do |i|
|
|
106
|
+
@parts.burst(@table_x + 31, @table_y + i + 0.5, 10, at: T.row_land(i), speed: 22, life: 0.35,
|
|
107
|
+
palette: Color::GOLD, gravity: 8)
|
|
108
|
+
end
|
|
109
|
+
cy = @big_score ? @score_y + 2.5 : @table_y + 4
|
|
110
|
+
@parts.burst(@w / 2.0, cy, 140, at: T.score_land, speed: 90, life: 0.8, palette: Color::GOLD, gravity: 16)
|
|
111
|
+
@parts.burst(@w / 2.0, cy, 50, at: T.score_land + 0.03, speed: 55, life: 0.7, palette: Color::FIRE, gravity: 12,
|
|
112
|
+
glyphs: ["*", "+", Term.glyph("✦", "*"), "'"])
|
|
113
|
+
if @panel
|
|
114
|
+
px, py, pw, ph = @panel
|
|
115
|
+
@parts.burst(px + pw / 2.0, py + ph / 2.0, 60, at: T.rank, speed: 45, life: 0.55,
|
|
116
|
+
palette: [[255, 255, 255], *@rpal[0, 3]], gravity: 14)
|
|
117
|
+
end
|
|
118
|
+
@fire = Fx::Particles.new(@rng)
|
|
119
|
+
pals = [Color::FIRE, Color::ICE, Color::NEON, Color::TOXIC, Color::GOLD]
|
|
120
|
+
10.times do |i|
|
|
121
|
+
at = T.rank + 0.2 + i * 0.13
|
|
122
|
+
x = 4 + @rng.rand * (@w - 8)
|
|
123
|
+
y = 2 + @rng.rand * (@h * 0.45)
|
|
124
|
+
@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]])
|
|
125
|
+
@fire.burst(x, y, 55, at: at, speed: 50, life: 0.9, palette: pals[i % pals.size], gravity: 9, drag: 2.2)
|
|
126
|
+
end
|
|
127
|
+
90.times do
|
|
128
|
+
@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,
|
|
129
|
+
palette: [Color.rainbow(@rng.rand), Color.rainbow(@rng.rand)],
|
|
130
|
+
glyph: ["*", "+", "'", ",", Term.glyph("✦", "*")][@rng.rand(5)], gravity: 1.2)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def shake(t)
|
|
135
|
+
[[T::DROP, 4, 0.3], [T.score_land, 6, 0.4], [T.rank, 4, 0.3]].each do |at, amp, len|
|
|
136
|
+
age = t - at
|
|
137
|
+
return ((@rng.rand * 2 - 1) * amp * (1 - age / len)).round if age >= 0 && age < len
|
|
138
|
+
end
|
|
139
|
+
0
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def draw(c, t)
|
|
143
|
+
draw_stars(c, t)
|
|
144
|
+
flash(c, t)
|
|
145
|
+
draw_title(c, t)
|
|
146
|
+
draw_table(c, t)
|
|
147
|
+
draw_score(c, t)
|
|
148
|
+
draw_rank(c, t)
|
|
149
|
+
draw_xp(c, t)
|
|
150
|
+
@parts.draw(c, t)
|
|
151
|
+
@fire.draw(c, t)
|
|
152
|
+
draw_footer(c, t)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def trail
|
|
156
|
+
x = " "
|
|
157
|
+
star = Term.glyph("✦", "*")
|
|
158
|
+
lines = [paint("#{x}#{star} DOPA IRB RESULT #{star}", [255, 210, 110], bold: true)]
|
|
159
|
+
rows_data.each { |k, v| lines << "#{x}#{k.ljust(12)}#{value_text(k, v).rjust(14)}" }
|
|
160
|
+
lines << paint("#{x}#{'SCORE'.ljust(12)}#{Fx.number_with_commas(@score).rjust(14)}", [255, 220, 130], bold: true)
|
|
161
|
+
lines << paint("#{x}#{'RANK'.ljust(12)}#{@rank.rjust(14)} #{@title}", Color.ramp(@rpal, 0.2), bold: true)
|
|
162
|
+
if @stats[:level]
|
|
163
|
+
up = @stats[:level].to_i - @stats[:level_before].to_i
|
|
164
|
+
lv = "LV #{@stats[:level]}#{up > 0 ? " (+#{up})" : ''}"
|
|
165
|
+
lines << paint("#{x}#{'LEVEL'.ljust(12)}#{lv.rjust(14)} #{Fx.number_with_commas(@stats[:xp].to_i)} XP", [200, 160, 255], bold: true)
|
|
166
|
+
end
|
|
167
|
+
lines << paint("#{x}#{'GALLERY'.ljust(12)}#{"#{@stats[:gallery]}/#{@stats[:gallery_total]}".rjust(14)}", [240, 220, 180]) if @stats[:gallery].to_i > 0
|
|
168
|
+
lines << paint("#{x}#{star} NEW BEST SCORE! (was #{Fx.number_with_commas(@stats[:best_before].to_i)}) #{star}", [255, 120, 160], bold: true) if @new_best
|
|
169
|
+
lines.join("\n")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def value_text(k, v)
|
|
175
|
+
k == "TIME" ? clock(v) : Fx.number_with_commas(v.to_i)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def draw_stars(c, t)
|
|
179
|
+
@stars.each do |x, y, ph, sp|
|
|
180
|
+
f = ((t * sp + ph) % 1.0)
|
|
181
|
+
next if f > 0.5
|
|
182
|
+
k = 1 - (f - 0.25).abs * 4
|
|
183
|
+
c.put(x, y, k > 0.7 ? "+" : ".", Color.mix([40, 40, 70], [200, 200, 255], k))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def flash(c, t)
|
|
188
|
+
return if ctx.config.flash == :off
|
|
189
|
+
[[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|
|
|
190
|
+
age = t - at
|
|
191
|
+
c.tint_bg(col, 0.55 * (1 - age / len)) if age >= 0 && age < len
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def draw_title(c, t)
|
|
196
|
+
drop = t < T::DROP ? ((1 - Fx.ease_in(t / T::DROP)) * 8).round : 0
|
|
197
|
+
sweep = (t * 0.9) % 1.4 - 0.2
|
|
198
|
+
fn = lambda do |_rx, ry, f|
|
|
199
|
+
base = Color.rainbow(f * 0.7 - t * 0.5 + ry * 0.03, 0.75)
|
|
200
|
+
d = (f - sweep).abs
|
|
201
|
+
d < 0.06 ? Color.mix([255, 255, 255], base, d / 0.06) : base
|
|
202
|
+
end
|
|
203
|
+
Fx.big_text(c, "RESULT", (@w - Font.width("RESULT", scale: @tscale)) / 2, 1 - drop, fn, scale: @tscale)
|
|
204
|
+
Fx.shockwave(c, @w / 2.0, 3.5, t - T::DROP, speed: 110, palette: Color::GOLD, life: 0.6, rings: 3)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def draw_table(c, t)
|
|
208
|
+
rows_data.each_with_index do |(k, v), i|
|
|
209
|
+
st = T.row_start(i)
|
|
210
|
+
next if t < st
|
|
211
|
+
y = @table_y + i
|
|
212
|
+
slide = Fx.ease_out((t - st) / T::SLIDE)
|
|
213
|
+
p = Fx.phase(t, st + T::SLIDE, T.row_land(i))
|
|
214
|
+
landed = t >= T.row_land(i)
|
|
215
|
+
lx = @table_x - ((1 - slide) * 20).round
|
|
216
|
+
vx = @table_x + 14 + ((1 - slide) * 30).round
|
|
217
|
+
age = t - T.row_land(i)
|
|
218
|
+
c.tint_bg([255, 200, 90], 0.45 * (1 - age / 0.25), @table_x - 1, y, 34, 1) if age >= 0 && age < 0.25
|
|
219
|
+
c.put(lx, y, k.ljust(14, "."), [150, 150, 175])
|
|
220
|
+
shown = k == "TIME" ? clock(v * Fx.ease_out(p)) : Fx.number_with_commas((v * Fx.ease_out(p)).round)
|
|
221
|
+
col = if !landed then [255, 255, 255]
|
|
222
|
+
elsif k == "ERRORS" || k == "INTERRUPTS" then [180, 190, 220]
|
|
223
|
+
else Color.ramp(Color::GOLD, 0.2)
|
|
224
|
+
end
|
|
225
|
+
c.put(vx, y, shown.rjust(16), col, bold: true)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def draw_score(c, t)
|
|
230
|
+
return if t < T.score_start
|
|
231
|
+
p = Fx.phase(t, T.score_start, T.score_land)
|
|
232
|
+
landed = t >= T.score_land
|
|
233
|
+
val = landed ? @score : (@score * Fx.ease_in(p) ** 0.7).round
|
|
234
|
+
txt = Fx.number_with_commas(val)
|
|
235
|
+
best = landed && @new_best
|
|
236
|
+
unless @big_score
|
|
237
|
+
c.put_center(@table_y + T::ROWS + 1, "TOTAL SCORE #{txt}#{' NEW BEST!' if best}", landed ? Color.rainbow(t) : [150, 220, 255], bold: true)
|
|
238
|
+
return
|
|
239
|
+
end
|
|
240
|
+
if best
|
|
241
|
+
c.put_center(@score_y - 1, "#{Term.glyph('✦', '*')} N E W B E S T ! #{Term.glyph('✦', '*')}", Color.rainbow(t * 1.5), bold: true)
|
|
242
|
+
else
|
|
243
|
+
c.put_center(@score_y - 1, "T O T A L S C O R E", [170, 170, 200], bold: true)
|
|
244
|
+
end
|
|
245
|
+
scale = Font.width(Fx.number_with_commas(@score), scale: 2) <= @w - 4 ? 2 : 1
|
|
246
|
+
scale = 1 if Font.width(txt, scale: scale) > @w - 2
|
|
247
|
+
fn = lambda do |_rx, ry, f|
|
|
248
|
+
if landed
|
|
249
|
+
base = Color.ramp(Color::GOLD, 0.1 + ry * 0.12)
|
|
250
|
+
sweep = ((t - T.score_land) * 1.5) % 1.4 - 0.2
|
|
251
|
+
d = (f - sweep).abs
|
|
252
|
+
d < 0.08 ? Color.mix([255, 255, 255], base, d / 0.08) : base
|
|
253
|
+
else
|
|
254
|
+
Color.ramp(Color::ICE, 0.15 + ry * 0.1)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
jitter = landed ? 0 : @rng.rand(2)
|
|
258
|
+
Fx.big_text(c, txt, (@w - Font.width(txt, scale: scale)) / 2 + jitter, @score_y, fn, scale: scale)
|
|
259
|
+
Fx.shockwave(c, @w / 2.0, @score_y + 2.5, t - T.score_land, speed: 140, palette: Color::GOLD, life: 0.7, rings: 4)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def draw_rank(c, t)
|
|
263
|
+
if @panel.nil?
|
|
264
|
+
return if t < T.rank
|
|
265
|
+
c.put_center(@big_score ? @score_y - 3 : @table_y + T::ROWS + 3, "RANK #{@rank} #{@title}", Color.ramp(@rpal, 0.15), bold: true)
|
|
266
|
+
return
|
|
267
|
+
end
|
|
268
|
+
px, py, pw, ph = @panel
|
|
269
|
+
return if t < T.rank - 0.14
|
|
270
|
+
if t < T.rank
|
|
271
|
+
grow = ((1 - Fx.ease_in((t - T.rank + 0.14) / 0.14)) * 8).round
|
|
272
|
+
c.fill_bg(Color.scale(Color.ramp(@rpal, 0.5), 0.35), px - grow, py - grow / 2, pw + grow * 2, ph + grow)
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
c.fill_bg(Color.scale(Color.ramp(@rpal, 0.6), 0.45), px, py, pw, ph)
|
|
276
|
+
c.fill_bg([12, 12, 22], px + 1, py + 1, pw - 2, ph - 2)
|
|
277
|
+
c.put(px + (pw - 4) / 2, py + 1, "RANK", [200, 200, 220], bold: true)
|
|
278
|
+
lw = Font.width(@rank, scale: 2)
|
|
279
|
+
fn = lambda do |_rx, ry, f|
|
|
280
|
+
@rank == "S" ? Color.rainbow(f * 0.5 - t + ry * 0.05) : Color.ramp(@rpal, 0.05 + ry * 0.1)
|
|
281
|
+
end
|
|
282
|
+
Fx.big_text(c, @rank, px + (pw - lw) / 2, py + 2, fn, scale: 2)
|
|
283
|
+
n = (@title.size * Fx.phase(t, T.rank + 0.15, T.rank + 0.5)).round
|
|
284
|
+
c.put(px + (pw - @title.size) / 2, py + 7, @title[0, n], Color.ramp(@rpal, 0.1), bold: true)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# LV 7 [=========-----] 4,210 / 8,192 XP: fills from the start of the session.
|
|
288
|
+
def draw_xp(c, t)
|
|
289
|
+
return unless @stats[:level] && t >= T.rank + 0.3
|
|
290
|
+
p = Fx.ease_out(Fx.phase(t, T.rank + 0.3, T.rank + 1.2))
|
|
291
|
+
xp = (@stats[:xp_before].to_i + (@stats[:xp].to_i - @stats[:xp_before].to_i) * p).round
|
|
292
|
+
lv = Game.level_for(xp)
|
|
293
|
+
lo = Game.xp_for(lv)
|
|
294
|
+
hi = Game.xp_for(lv + 1)
|
|
295
|
+
bar_w = [[@w - 50, 30].min, 10].max
|
|
296
|
+
fill = ((xp - lo).fdiv(hi - lo) * bar_w).round
|
|
297
|
+
label = "LV #{lv} "
|
|
298
|
+
tail = " #{Fx.number_with_commas(xp)} / #{Fx.number_with_commas(hi)} XP"
|
|
299
|
+
up = lv > @stats[:level_before].to_i
|
|
300
|
+
tail += " LEVEL UP!" if up
|
|
301
|
+
total = label.size + bar_w + tail.size
|
|
302
|
+
x = (@w - total) / 2
|
|
303
|
+
c.put(x, @xp_y, label, [210, 170, 255], bold: true)
|
|
304
|
+
bar_w.times do |i|
|
|
305
|
+
on = i < fill
|
|
306
|
+
col = on ? Color.rainbow(i.fdiv(bar_w) - t) : [60, 50, 80]
|
|
307
|
+
c.put(x + label.size + i, @xp_y, on ? Term.glyph("█", "#") : Term.glyph("░", "."), col)
|
|
308
|
+
end
|
|
309
|
+
c.put(x + label.size + bar_w, @xp_y, tail, up && (t * 8).to_i.even? ? [255, 255, 255] : [210, 170, 255], bold: up)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def draw_footer(c, t)
|
|
313
|
+
msg = "thanks for playing #{Term.glyph('✦', '*')} see you next session"
|
|
314
|
+
n = (msg.size * Fx.phase(t, T.rank + 0.9, T.rank + 1.5)).round
|
|
315
|
+
c.put_center(@h - 1, msg[0, n], [170, 170, 200]) if n > 0
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def clock(sec)
|
|
319
|
+
sec = sec.to_i
|
|
320
|
+
h, rest = sec.divmod(3600)
|
|
321
|
+
m, s = rest.divmod(60)
|
|
322
|
+
h > 0 ? format("%dh%02dm%02ds", h, m, s) : format("%dm%02ds", m, s)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
@@ -7,7 +7,23 @@ module Dopairb
|
|
|
7
7
|
TITLE = "DOPA IRB"
|
|
8
8
|
TAG = "every keystroke counts."
|
|
9
9
|
|
|
10
|
+
def initialize(ctx, career = nil)
|
|
11
|
+
@career = career
|
|
12
|
+
super(ctx, nil)
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
def big? = true
|
|
16
|
+
def sfx = :intro
|
|
17
|
+
def impact_at = 0.2
|
|
18
|
+
|
|
19
|
+
# "LV 5 * DAY 3 STREAK!" or nil on a fresh profile.
|
|
20
|
+
def career_line
|
|
21
|
+
return nil unless @career
|
|
22
|
+
bits = []
|
|
23
|
+
bits << "LV #{@career[:level]}" if @career[:level].to_i > 1
|
|
24
|
+
bits << "DAY #{@career[:streak]} STREAK#{'!' if @career[:streak_up]}" if @career[:streak].to_i >= 2
|
|
25
|
+
bits.empty? ? nil : bits.join(" #{Term.glyph('✦', '*')} ")
|
|
26
|
+
end
|
|
11
27
|
|
|
12
28
|
def setup
|
|
13
29
|
@scale = best_scale(TITLE)
|
|
@@ -53,21 +69,26 @@ module Dopairb
|
|
|
53
69
|
end
|
|
54
70
|
@parts.draw(c, t)
|
|
55
71
|
Fx.sparkles(c, t, 11, 16, palette: Color::GOLD) if t > 0.7
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
tag = [career_line, TAG].compact.join(" ")
|
|
73
|
+
tag = TAG if tag.size > @w
|
|
74
|
+
n = (tag.size * Fx.phase(t, 0.75, 1.1)).round
|
|
75
|
+
c.put_center(7, tag[0, n], [230, 230, 240], bold: true) if n > 0
|
|
58
76
|
end
|
|
59
77
|
|
|
60
78
|
def trail
|
|
61
79
|
star = Term.glyph("✦", "*")
|
|
62
80
|
title = "#{star} DOPA IRB #{VERSION} #{star}"
|
|
63
81
|
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
|
-
|
|
82
|
+
career = career_line
|
|
83
|
+
career = career ? "#{paint(career, [255, 215, 120], bold: true)} " : ""
|
|
84
|
+
"#{colored} #{career}#{paint("Ruby #{RUBY_VERSION} -- type `dopa` for effect settings", [150, 150, 165])}"
|
|
65
85
|
end
|
|
66
86
|
end
|
|
67
87
|
|
|
68
88
|
# Session result screen. Leaves a plain text table behind.
|
|
69
89
|
class Outro < Scene
|
|
70
90
|
def big? = true
|
|
91
|
+
def sfx = :result
|
|
71
92
|
|
|
72
93
|
def initialize(ctx, stats)
|
|
73
94
|
@stats = stats
|
|
@@ -144,3 +165,94 @@ module Dopairb
|
|
|
144
165
|
end
|
|
145
166
|
end
|
|
146
167
|
end
|
|
168
|
+
|
|
169
|
+
module Dopairb
|
|
170
|
+
module Scenes
|
|
171
|
+
# Shown at startup while the startup sound is being synthesized.
|
|
172
|
+
# A cat runs along a pastel progress bar; LOADING bounces; sparkles pop.
|
|
173
|
+
class Loading < Scene
|
|
174
|
+
PASTEL = [[255, 170, 210], [255, 205, 160], [255, 245, 170], [180, 245, 200], [170, 215, 255], [215, 185, 255]].freeze
|
|
175
|
+
MESSAGES = ["tuning the synthesizer", "polishing sparkles", "charging the combo meter",
|
|
176
|
+
"stacking fireworks", "waking up the cat"].freeze
|
|
177
|
+
MIN = 0.7
|
|
178
|
+
FINISH = 0.45
|
|
179
|
+
|
|
180
|
+
def initialize(ctx, ready)
|
|
181
|
+
@ready = ready
|
|
182
|
+
@ready_at = nil
|
|
183
|
+
super(ctx, nil)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def height = 6
|
|
187
|
+
def length = 8.0 # safety net; normally ends via done?
|
|
188
|
+
|
|
189
|
+
def setup
|
|
190
|
+
@parts = Fx::Particles.new(@rng)
|
|
191
|
+
@bar_x = 4
|
|
192
|
+
@bar_w = [[@w - 12, 60].min, 16].max
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def done?(t)
|
|
196
|
+
if @ready_at.nil? && t >= MIN && @ready.call
|
|
197
|
+
@ready_at = t
|
|
198
|
+
@parts.burst(@bar_x + @bar_w, 3, 40, at: t, speed: 40, life: 0.5, palette: PASTEL, gravity: 10)
|
|
199
|
+
end
|
|
200
|
+
@ready_at && t >= @ready_at + FINISH
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def progress(t)
|
|
204
|
+
fake = 1 - Math.exp(-t / 0.9) # creeps towards 1, never gets there
|
|
205
|
+
return 0.9 * fake unless @ready_at
|
|
206
|
+
0.9 * fake + (1 - 0.9 * fake) * Fx.ease_out((t - @ready_at) / 0.2)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def pastel(i, t) = Color.ramp(PASTEL, ((i * 0.13 + t * 0.6) % 1.0))
|
|
210
|
+
|
|
211
|
+
def draw(c, t)
|
|
212
|
+
Fx.sparkles(c, t, 21, 18, palette: PASTEL.reverse)
|
|
213
|
+
draw_word(c, t)
|
|
214
|
+
draw_bar(c, t)
|
|
215
|
+
@parts.draw(c, t)
|
|
216
|
+
msg = if @ready_at then "ready! #{Term.glyph('✦', '*')}"
|
|
217
|
+
else "#{MESSAGES[(t / 0.9).to_i % MESSAGES.size]}#{'.' * ((t * 4).to_i % 4)}"
|
|
218
|
+
end
|
|
219
|
+
c.put(@bar_x, 5, msg, @ready_at ? [255, 240, 170] : [200, 190, 230], bold: !@ready_at.nil?)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def draw_word(c, t)
|
|
225
|
+
word = @ready_at ? "READY!" : "LOADING"
|
|
226
|
+
x = @bar_x
|
|
227
|
+
word.each_char.with_index do |ch, i|
|
|
228
|
+
hop = Math.sin(t * 9 - i * 0.7)
|
|
229
|
+
y = hop > 0.55 ? 0 : 1
|
|
230
|
+
c.put(x, y, ch, pastel(i, t), bold: true)
|
|
231
|
+
x += 2
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def draw_bar(c, t)
|
|
236
|
+
p = progress(t)
|
|
237
|
+
filled = (p * @bar_w).round
|
|
238
|
+
y = 3
|
|
239
|
+
@bar_w.times do |i|
|
|
240
|
+
if i < filled
|
|
241
|
+
shimmer = ((i - t * 40) % 14).abs < 1.5
|
|
242
|
+
c.bg(@bar_x + i, y, shimmer ? [255, 255, 255] : pastel(i / 3, t))
|
|
243
|
+
else
|
|
244
|
+
c.put(@bar_x + i, y, Term.glyph("⣀", "."), [90, 80, 110])
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
run = (t * 10).to_i.even?
|
|
248
|
+
cat = @ready_at ? "=^o^=" : (run ? "=^.^=" : "=^-^=")
|
|
249
|
+
bounce = @ready_at && t - @ready_at < 0.25 ? 1 : (run ? 0 : 1)
|
|
250
|
+
cx = @bar_x + [filled - 2, 0].max
|
|
251
|
+
c.put(cx, y - 1 - bounce, cat, [255, 220, 235], bold: true)
|
|
252
|
+
c.put(cx - 2, y - 1, run ? "~" : "'", [255, 190, 220]) if !@ready_at && t % 0.2 < 0.1
|
|
253
|
+
pct = "#{(p * 100).floor}%"
|
|
254
|
+
c.put(@bar_x + @bar_w + 2, y, pct, [255, 240, 200], bold: true)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
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
|
|
@@ -21,10 +23,12 @@ module Dopairb
|
|
|
21
23
|
c.put(0, 0, "=>", hot, bold: true)
|
|
22
24
|
if t < 0.16
|
|
23
25
|
head = 3 + (@target - 3) * Fx.ease_in(t / 0.16)
|
|
26
|
+
fever = event.flag?(:fever)
|
|
24
27
|
9.times do |i|
|
|
25
28
|
x = head - i
|
|
26
29
|
break if x < 3
|
|
27
|
-
|
|
30
|
+
col = fever ? Color.rainbow(i / 9.0 - t * 4) : Color.ramp(pal, i / 9.0)
|
|
31
|
+
c.put(x, 0, i.zero? ? Term.glyph("✦", "*") : (i < 3 ? "=" : "-"), col, bold: i < 2)
|
|
28
32
|
end
|
|
29
33
|
else
|
|
30
34
|
p = Fx.phase(t, 0.16, 0.42)
|
|
@@ -32,13 +36,18 @@ module Dopairb
|
|
|
32
36
|
txt = "+#{Fx.number_with_commas(event.gain)}"
|
|
33
37
|
col = Color.mix([255, 255, 255], Color.ramp(pal, 0.3), p)
|
|
34
38
|
x = c.put(@target + 2, 0, txt, col, bg: p < 0.25 ? Color.ramp(pal, 0.6) : nil, bold: true)
|
|
35
|
-
c.put(x + 2, 0, "COMBO #{event.combo}", combo_color(t), bold: event.combo >= 5) if event.combo > 1
|
|
39
|
+
x = c.put(x + 2, 0, "COMBO #{event.combo}", combo_color(t), bold: event.combo >= 5) if event.combo > 1
|
|
40
|
+
if event.flag?(:fever) && x
|
|
41
|
+
" FEVER x2 ".each_char.with_index do |ch, i|
|
|
42
|
+
x = c.put(x + (i.zero? ? 2 : 0), 0, ch, [20, 10, 30], bg: Color.rainbow(i / 10.0 - t * 2), bold: true)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
36
45
|
end
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
def trail
|
|
40
|
-
paint("=> #{Term.glyph('✦', '*')} +#{Fx.number_with_commas(event.gain)}" + (event.combo > 1 ? " COMBO #{event.combo}" : "")
|
|
41
|
-
combo_color)
|
|
49
|
+
paint("=> #{Term.glyph('✦', '*')} +#{Fx.number_with_commas(event.gain)}" + (event.combo > 1 ? " COMBO #{event.combo}" : "") +
|
|
50
|
+
(event.flag?(:fever) ? " FEVER x2" : ""), combo_color)
|
|
42
51
|
end
|
|
43
52
|
end
|
|
44
53
|
|
|
@@ -46,6 +55,8 @@ module Dopairb
|
|
|
46
55
|
class Puff < Scene
|
|
47
56
|
def top? = !ctx.input.nil?
|
|
48
57
|
def length = 0.5
|
|
58
|
+
def sfx = :puff
|
|
59
|
+
def impact_at = 0.12
|
|
49
60
|
|
|
50
61
|
def setup
|
|
51
62
|
@parts = Fx::Particles.new(@rng)
|
|
@@ -71,6 +82,7 @@ module Dopairb
|
|
|
71
82
|
|
|
72
83
|
# true / false: an ink stamp slams down.
|
|
73
84
|
class Stamp < Scene
|
|
85
|
+
def sfx = :stamp
|
|
74
86
|
def height = 3
|
|
75
87
|
def length = 0.6
|
|
76
88
|
def top? = !ctx.input.nil?
|
|
@@ -112,6 +124,7 @@ module Dopairb
|
|
|
112
124
|
|
|
113
125
|
# Integers that deserve it: count up in giant digits, then a shockwave.
|
|
114
126
|
class Counter < Scene
|
|
127
|
+
def sfx = :counter
|
|
115
128
|
def big? = true
|
|
116
129
|
def top? = !ctx.input.nil?
|
|
117
130
|
def length = 1.0
|
|
@@ -184,6 +197,7 @@ module Dopairb
|
|
|
184
197
|
|
|
185
198
|
# Long strings: characters pour in while the length counts up.
|
|
186
199
|
class Stream < Scene
|
|
200
|
+
def sfx = :hit
|
|
187
201
|
def height = 2
|
|
188
202
|
def length = 0.75
|
|
189
203
|
def top? = !ctx.input.nil?
|
|
@@ -210,6 +224,7 @@ module Dopairb
|
|
|
210
224
|
|
|
211
225
|
# Arrays and hashes: one block per element pops into place.
|
|
212
226
|
class Crate < Scene
|
|
227
|
+
def sfx = :hit
|
|
213
228
|
def height = 2
|
|
214
229
|
def length = 0.7
|
|
215
230
|
def top? = !ctx.input.nil?
|
|
@@ -255,6 +270,8 @@ module Dopairb
|
|
|
255
270
|
|
|
256
271
|
def big? = true
|
|
257
272
|
def mega? = @mega
|
|
273
|
+
def alt? = @alt && @mega
|
|
274
|
+
def sfx = @mega ? :mega : :banner
|
|
258
275
|
def top? = !ctx.input.nil? && !@alt
|
|
259
276
|
def length = @mega ? 1.9 : 1.05
|
|
260
277
|
|
|
@@ -385,6 +402,7 @@ module Dopairb
|
|
|
385
402
|
|
|
386
403
|
# Floods of stdout: digital rain behind a counter.
|
|
387
404
|
class Rain < Scene
|
|
405
|
+
def sfx = :rain
|
|
388
406
|
def height = 5
|
|
389
407
|
def length = 0.9
|
|
390
408
|
def big? = true
|