dopairb 0.2.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 +41 -9
- data/lib/dopairb/cli.rb +4 -6
- data/lib/dopairb/config.rb +6 -4
- data/lib/dopairb/demo.rb +9 -2
- data/lib/dopairb/director.rb +35 -5
- 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 +84 -7
- data/lib/dopairb/input_fx.rb +63 -9
- data/lib/dopairb/irb_adapter.rb +8 -0
- data/lib/dopairb/profile.rb +106 -0
- data/lib/dopairb/reline_adapter.rb +1 -0
- data/lib/dopairb/scene.rb +5 -0
- data/lib/dopairb/scenes/bonus.rb +279 -0
- data/lib/dopairb/scenes/failure.rb +1 -0
- data/lib/dopairb/scenes/finale.rb +42 -2
- data/lib/dopairb/scenes/session.rb +21 -3
- data/lib/dopairb/scenes/success.rb +11 -4
- data/lib/dopairb/session.rb +50 -6
- data/lib/dopairb/sound.rb +57 -2
- 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 +57 -1
- data/spec.md +3 -3
- metadata +10 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "date"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Dopairb
|
|
8
|
+
# What survives between sessions: lifetime XP (= total score), best score,
|
|
9
|
+
# best combo, the day streak and the collected bonus art. One small JSON
|
|
10
|
+
# file under $XDG_STATE_HOME (~/.local/state/dopairb/profile.json), or
|
|
11
|
+
# $DOPAIRB_PROFILE. DOPAIRB_PROFILE=off keeps nothing.
|
|
12
|
+
class Profile
|
|
13
|
+
FIELDS = %i[xp best_score best_combo sessions streak last_day gallery].freeze
|
|
14
|
+
|
|
15
|
+
attr_accessor(*FIELDS)
|
|
16
|
+
attr_reader :path
|
|
17
|
+
|
|
18
|
+
def self.default_path
|
|
19
|
+
env = ENV["DOPAIRB_PROFILE"]
|
|
20
|
+
return nil if env && %w[off 0 none].include?(env.downcase)
|
|
21
|
+
return env if env && !env.empty?
|
|
22
|
+
base = ENV["XDG_STATE_HOME"].to_s.empty? ? File.join(Dir.home, ".local", "state") : ENV["XDG_STATE_HOME"]
|
|
23
|
+
File.join(base, "dopairb", "profile.json")
|
|
24
|
+
rescue ArgumentError # no home directory
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.load(path = default_path)
|
|
29
|
+
prof = new(path)
|
|
30
|
+
return prof unless path && File.exist?(path)
|
|
31
|
+
data = JSON.parse(File.read(path), symbolize_names: true)
|
|
32
|
+
prof.xp = data[:xp].to_i
|
|
33
|
+
prof.best_score = data[:best_score].to_i
|
|
34
|
+
prof.best_combo = data[:best_combo].to_i
|
|
35
|
+
prof.sessions = data[:sessions].to_i
|
|
36
|
+
prof.streak = data[:streak].to_i
|
|
37
|
+
prof.last_day = data[:last_day]
|
|
38
|
+
prof.gallery = Array(data[:gallery]).map(&:to_sym) & Gallery.ids
|
|
39
|
+
prof
|
|
40
|
+
rescue SystemCallError, JSON::ParserError => e
|
|
41
|
+
Dopairb.debug(e)
|
|
42
|
+
new(path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(path = nil)
|
|
46
|
+
@path = path
|
|
47
|
+
@xp = 0
|
|
48
|
+
@best_score = 0
|
|
49
|
+
@best_combo = 0
|
|
50
|
+
@sessions = 0
|
|
51
|
+
@streak = 0
|
|
52
|
+
@last_day = nil
|
|
53
|
+
@gallery = []
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def level = Game.level_for(@xp)
|
|
57
|
+
def persistent? = !@path.nil?
|
|
58
|
+
|
|
59
|
+
# Count today toward the day streak. => true when the streak grew today.
|
|
60
|
+
def visit(today = Date.today)
|
|
61
|
+
day = today.iso8601
|
|
62
|
+
return false if @last_day == day
|
|
63
|
+
yesterday = (today - 1).iso8601
|
|
64
|
+
@streak = @last_day == yesterday ? @streak + 1 : 1
|
|
65
|
+
@last_day = day
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def collect(id)
|
|
70
|
+
return false if @gallery.include?(id)
|
|
71
|
+
@gallery << id
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Fold a finished session in. Another dopairb may have saved meanwhile,
|
|
76
|
+
# so the file is re-read first and only this session's numbers are added.
|
|
77
|
+
def finish(stats, collected: [])
|
|
78
|
+
disk = persistent? ? Profile.load(@path) : self
|
|
79
|
+
disk.xp += stats[:score].to_i
|
|
80
|
+
disk.best_score = [disk.best_score, stats[:score].to_i].max
|
|
81
|
+
disk.best_combo = [disk.best_combo, stats[:max_combo].to_i].max
|
|
82
|
+
disk.sessions += 1
|
|
83
|
+
if @last_day.to_s > disk.last_day.to_s
|
|
84
|
+
disk.last_day = @last_day
|
|
85
|
+
disk.streak = @streak
|
|
86
|
+
end
|
|
87
|
+
collected.each { |id| disk.collect(id) }
|
|
88
|
+
disk.save
|
|
89
|
+
disk
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_h = FIELDS.to_h { |k| [k, public_send(k)] }
|
|
93
|
+
|
|
94
|
+
def save
|
|
95
|
+
return false unless @path
|
|
96
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
97
|
+
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
98
|
+
File.write(tmp, JSON.pretty_generate(to_h) + "\n")
|
|
99
|
+
File.rename(tmp, @path)
|
|
100
|
+
true
|
|
101
|
+
rescue SystemCallError => e
|
|
102
|
+
Dopairb.debug(e)
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/dopairb/scene.rb
CHANGED
|
@@ -105,6 +105,11 @@ module Dopairb
|
|
|
105
105
|
bits = extra.dup
|
|
106
106
|
return bits unless event
|
|
107
107
|
bits << "+#{Fx.number_with_commas(event.gain)}" if event.gain.to_i > 0
|
|
108
|
+
bits << "JACKPOT x#{Game::JACKPOT_MULT}" if event.flag?(:jackpot)
|
|
109
|
+
bits << "CRITICAL x#{event.mult / (event.flag?(:fever) ? 2 : 1)}" if event.flag?(:critical)
|
|
110
|
+
bits << "FEVER x2" if event.flag?(:fever) && !event.flag?(:fever_start)
|
|
111
|
+
bits << "LEVEL UP LV #{event.level}" if event.flag?(:level_up)
|
|
112
|
+
bits << "ART DROP" if event.flag?(:art_drop) && !event.flag?(:level_up)
|
|
108
113
|
bits << "COMBO #{event.combo}" if event.combo.to_i > 1
|
|
109
114
|
bits << "NEW RECORD" if event.flag?(:new_record) && !bits.include?("NEW RECORD")
|
|
110
115
|
bits << "FIRST HIT" if event.flag?(:first_hit)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
module Scenes
|
|
5
|
+
# A random critical hit: a slash, a red flash and the multiplier punching in.
|
|
6
|
+
class Critical < Scene
|
|
7
|
+
def top? = !ctx.input.nil?
|
|
8
|
+
def big? = true
|
|
9
|
+
def height = 3
|
|
10
|
+
def length = 0.7
|
|
11
|
+
def sfx = :crit
|
|
12
|
+
def impact_at = 0.1
|
|
13
|
+
def flash_at = 0.1
|
|
14
|
+
|
|
15
|
+
def setup
|
|
16
|
+
@x = 4
|
|
17
|
+
@parts = Fx::Particles.new(@rng)
|
|
18
|
+
@parts.burst(@x + 6, 1.5, 34, at: 0.1, speed: 42, life: 0.4, palette: Color::BLOOD, gravity: 8)
|
|
19
|
+
@parts.burst(@x + 6, 1.5, 16, at: 0.12, speed: 26, life: 0.35, palette: Color::GOLD, gravity: 6,
|
|
20
|
+
glyphs: ["*", "+", Term.glyph("✦", "*")])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def shake(t) = t.between?(0.1, 0.22) ? (@rng.rand(3) - 1) * 2 : 0
|
|
24
|
+
|
|
25
|
+
def draw(c, t)
|
|
26
|
+
if t < 0.1
|
|
27
|
+
p = t / 0.1
|
|
28
|
+
(0..2).each do |y|
|
|
29
|
+
x = (@x - 2 + p * 16 + y * 3).round
|
|
30
|
+
c.put(x, y, "\\", [255, 255, 255], bold: true)
|
|
31
|
+
c.put(x - 1, y, "\\", [255, 120, 120])
|
|
32
|
+
end
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
@parts.draw(c, t)
|
|
36
|
+
p = Fx.phase(t, 0.1, 0.7)
|
|
37
|
+
c.tint_bg([255, 40, 40], 0.5 * (1 - Fx.phase(t, 0.1, 0.22)), 0, 0, @w, 3) if ctx.config.flash != :off
|
|
38
|
+
label = "CRITICAL!!"
|
|
39
|
+
label.each_char.with_index do |ch, i|
|
|
40
|
+
flick = ((t * 20).to_i + i).even?
|
|
41
|
+
col = flick ? [255, 70, 60] : [255, 220, 90]
|
|
42
|
+
c.put(@x + i, 1, ch, t < 0.16 ? [255, 255, 255] : col, bold: true)
|
|
43
|
+
end
|
|
44
|
+
mx = @x + label.size + 2
|
|
45
|
+
m = " x#{event.mult} "
|
|
46
|
+
pop = Fx.ease_out_back(Fx.phase(t, 0.12, 0.3))
|
|
47
|
+
bg = Color.mix([255, 255, 255], [200, 30, 30], pop)
|
|
48
|
+
c.put(mx, 1, m, [255, 255, 255], bg: bg, bold: true) if pop > 0.05
|
|
49
|
+
c.put(mx + m.size + 2, 1, "+#{Fx.number_with_commas(event.gain)}", Color.mix([255, 255, 255], [255, 210, 90], p), bold: true)
|
|
50
|
+
sub = sub_bits.reject { |b| b.start_with?("+", "CRITICAL") }.join(" ")
|
|
51
|
+
c.put(@x, 2, sub, [170, 150, 150]) if p > 0.2 && !sub.empty?
|
|
52
|
+
c.put(@x, 0, "=>", [255, 160, 160], bold: true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def trail
|
|
56
|
+
paint("=> #{Term.glyph('✦', '*')} CRITICAL!! x#{event.mult} +#{Fx.number_with_commas(event.gain)}" +
|
|
57
|
+
(event.combo > 1 ? " COMBO #{event.combo}" : ""), [255, 110, 90], bold: true)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The rare jackpot: three slot reels stop on 7-7-7, then coins rain.
|
|
62
|
+
class Jackpot < Scene
|
|
63
|
+
SYMBOLS = %w[7 $ ? ! * #].freeze
|
|
64
|
+
STOPS = [0.45, 0.7, 0.95].freeze
|
|
65
|
+
REEL_W = 9
|
|
66
|
+
|
|
67
|
+
def top? = !ctx.input.nil?
|
|
68
|
+
def big? = true
|
|
69
|
+
def mega? = true
|
|
70
|
+
def length = 2.3
|
|
71
|
+
def sfx = :jackpot
|
|
72
|
+
def impact_at = STOPS.last
|
|
73
|
+
def flash_at = STOPS.last
|
|
74
|
+
def height = @full ? 10 : 2
|
|
75
|
+
|
|
76
|
+
def setup
|
|
77
|
+
@full = !ctx.compact && @w >= REEL_W * 3 + 8
|
|
78
|
+
@x0 = (@w - (REEL_W * 3 + 4)) / 2
|
|
79
|
+
@parts = Fx::Particles.new(@rng)
|
|
80
|
+
@coins = Fx::Particles.new(@rng)
|
|
81
|
+
return unless @full
|
|
82
|
+
STOPS.each_with_index do |at, i|
|
|
83
|
+
@parts.burst(@x0 + i * (REEL_W + 2) + REEL_W / 2.0, 3.5, 14, at: at, speed: 22, life: 0.3, palette: Color::GOLD, gravity: 6)
|
|
84
|
+
end
|
|
85
|
+
@parts.burst(@w / 2.0, 3.5, 90, at: STOPS.last, speed: 70, life: 0.7, palette: Color::GOLD, gravity: 14)
|
|
86
|
+
70.times do
|
|
87
|
+
@coins.add(@rng.rand * @w, -1, (@rng.rand - 0.5) * 6, 6 + @rng.rand * 8, at: STOPS.last + @rng.rand * 1.1, life: 1.1,
|
|
88
|
+
palette: [[255, 250, 200], [255, 210, 60], [200, 140, 0]],
|
|
89
|
+
glyph: ["$", "o", "O", "$", Term.glyph("✦", "*")][@rng.rand(5)], gravity: 6)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def shake(t) = (t - STOPS.last).between?(0, 0.35) ? ((@rng.rand * 2 - 1) * 3 * (1 - (t - STOPS.last) / 0.35)).round : 0
|
|
94
|
+
|
|
95
|
+
def draw(c, t)
|
|
96
|
+
return draw_compact(c, t) unless @full
|
|
97
|
+
c.fill_bg([60, 20, 70], @x0 - 2, 0, REEL_W * 3 + 8, 7)
|
|
98
|
+
lights = (t * 12).to_i
|
|
99
|
+
(REEL_W * 3 + 8).times do |i|
|
|
100
|
+
on = (i + lights).even?
|
|
101
|
+
c.put(@x0 - 2 + i, 0, "o", on ? [255, 230, 120] : [120, 80, 40], bg: [60, 20, 70], bold: on)
|
|
102
|
+
c.put(@x0 - 2 + i, 6, "o", on ? [120, 80, 40] : [255, 230, 120], bg: [60, 20, 70], bold: !on)
|
|
103
|
+
end
|
|
104
|
+
3.times do |i|
|
|
105
|
+
x = @x0 + i * (REEL_W + 2)
|
|
106
|
+
c.fill_bg([250, 245, 235], x, 1, REEL_W, 5)
|
|
107
|
+
stopped = t >= STOPS[i]
|
|
108
|
+
sym = stopped ? "7" : SYMBOLS[((t * 26).to_i + i * 2) % SYMBOLS.size]
|
|
109
|
+
bounce = stopped && t - STOPS[i] < 0.06 ? 1 : 0
|
|
110
|
+
col = if stopped
|
|
111
|
+
t >= STOPS.last ? Color.rainbow(t * 1.5 + i * 0.2) : [220, 30, 40]
|
|
112
|
+
else
|
|
113
|
+
[140, 140, 160]
|
|
114
|
+
end
|
|
115
|
+
gw = Font.width(sym)
|
|
116
|
+
Fx.big_text(c, sym, x + (REEL_W - gw) / 2, 1 + bounce, ->(*) { col }, bg_fn: ->(*) { [250, 245, 235] })
|
|
117
|
+
c.tint_bg([0, 0, 0], 0.25, x, 1, REEL_W, 5) unless stopped
|
|
118
|
+
end
|
|
119
|
+
@parts.draw(c, t)
|
|
120
|
+
@coins.draw(c, t)
|
|
121
|
+
return if t < STOPS.last
|
|
122
|
+
c.tint_bg([255, 230, 120], 0.6 * (1 - Fx.phase(t, STOPS.last, STOPS.last + 0.15)), 0, 0, @w, height) if ctx.config.flash != :off
|
|
123
|
+
label = "J A C K P O T !! x#{event.mult}"
|
|
124
|
+
n = (label.size * Fx.phase(t, STOPS.last, STOPS.last + 0.3)).round
|
|
125
|
+
label[0, n].each_char.with_index do |ch, i|
|
|
126
|
+
c.put((@w - label.size) / 2 + i, 8, ch, Color.rainbow(i / 14.0 - t), bold: true)
|
|
127
|
+
end
|
|
128
|
+
gain = (event.gain * Fx.ease_out(Fx.phase(t, STOPS.last + 0.1, STOPS.last + 0.7))).round
|
|
129
|
+
c.put_center(9, "+#{Fx.number_with_commas(gain)} #{sub_bits.reject { |b| b.start_with?('+', 'JACKPOT') }.join(' ')}",
|
|
130
|
+
[255, 220, 120], bold: true)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def draw_compact(c, t)
|
|
134
|
+
3.times do |i|
|
|
135
|
+
sym = t >= STOPS[i] ? "7" : SYMBOLS[((t * 26).to_i + i) % SYMBOLS.size]
|
|
136
|
+
c.put(1 + i * 4, 0, "[#{sym}]", t >= STOPS[i] ? [255, 80, 80] : [180, 180, 190], bold: true)
|
|
137
|
+
end
|
|
138
|
+
c.put(1, 1, "JACKPOT!! x#{event.mult} +#{Fx.number_with_commas(event.gain)}", Color.rainbow(t), bold: true) if t >= STOPS.last
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def trail
|
|
142
|
+
s = "#{Term.glyph('✦', '*')} [7][7][7] JACKPOT!! x#{event.mult} +#{Fx.number_with_commas(event.gain)}"
|
|
143
|
+
return s if depth == :none
|
|
144
|
+
s.each_char.with_index.map { |ch, i| "#{Color.sgr(Color.rainbow(i / 20.0), nil, true, depth)}#{ch}" }.join + "\e[0m"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# LEVEL UP (or a jackpot prize, or a lucky ART DROP): the title slams in,
|
|
149
|
+
# then a masterpiece is unveiled full screen.
|
|
150
|
+
class Masterpiece < Scene
|
|
151
|
+
LAND = 0.22
|
|
152
|
+
REVEAL = 1.0
|
|
153
|
+
SHOWN = 1.75
|
|
154
|
+
|
|
155
|
+
def self.fits?(rows, cols) = rows >= 16 && cols >= 40
|
|
156
|
+
|
|
157
|
+
def alt? = true
|
|
158
|
+
def big? = true
|
|
159
|
+
def mega? = true
|
|
160
|
+
def height = ctx.rows
|
|
161
|
+
def length = 4.2
|
|
162
|
+
def sfx = :levelup
|
|
163
|
+
def impact_at = LAND
|
|
164
|
+
def flash_at = LAND
|
|
165
|
+
|
|
166
|
+
def setup
|
|
167
|
+
@h = ctx.rows
|
|
168
|
+
@piece = Gallery.find(event.info[:art]) || Gallery::PIECES.first
|
|
169
|
+
@level_up = event.flag?(:level_up)
|
|
170
|
+
@title = if @level_up then "LEVEL UP!"
|
|
171
|
+
elsif event.flag?(:jackpot) then "JACKPOT PRIZE"
|
|
172
|
+
else "ART DROP!"
|
|
173
|
+
end
|
|
174
|
+
@title = "BONUS!" if Font.width(@title) > @w - 2
|
|
175
|
+
@tscale = Font.width(@title, scale: 2) <= @w - 4 ? 2 : 1
|
|
176
|
+
@aw, @ah = Gallery.fit(@piece, @w - 6, @h - 6)
|
|
177
|
+
@ax = (@w - @aw) / 2
|
|
178
|
+
@ay = 2
|
|
179
|
+
@parts = Fx::Particles.new(@rng)
|
|
180
|
+
tw = Font.width(@title, scale: @tscale)
|
|
181
|
+
cy = @h / 2.0 - 1
|
|
182
|
+
10.times do |i|
|
|
183
|
+
@parts.burst((@w - tw) / 2.0 + tw * i / 9.0, cy + 3, 9, at: LAND, speed: 45, life: 0.6, palette: Color::NEON, gravity: 18,
|
|
184
|
+
angle: -Math::PI / 2, arc: Math::PI * 1.6)
|
|
185
|
+
end
|
|
186
|
+
@parts.burst(@w / 2.0, @ay + @ah / 4.0, 120, at: REVEAL + 0.45, speed: 80, life: 0.8, palette: Color::GOLD, gravity: 10)
|
|
187
|
+
@stars = Array.new(60) { [@rng.rand(@w), @rng.rand(@h), @rng.rand, 0.5 + @rng.rand] }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def shake(t)
|
|
191
|
+
[[LAND, 4, 0.35], [REVEAL + 0.45, 2, 0.25]].each do |at, amp, len|
|
|
192
|
+
age = t - at
|
|
193
|
+
return ((@rng.rand * 2 - 1) * amp * (1 - age / len)).round if age >= 0 && age < len
|
|
194
|
+
end
|
|
195
|
+
0
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def draw(c, t)
|
|
199
|
+
draw_stars(c, t)
|
|
200
|
+
t < REVEAL ? draw_title(c, t) : draw_art(c, t)
|
|
201
|
+
@parts.draw(c, t)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def trail
|
|
205
|
+
star = Term.glyph("✦", "*")
|
|
206
|
+
g = event.info[:gallery]
|
|
207
|
+
head = @level_up ? "LEVEL UP! LV #{event.level}" : @title
|
|
208
|
+
s = "#{star} #{head} #{star} BONUS ART: #{@piece.title} (#{@piece.artist}, #{@piece.year})"
|
|
209
|
+
s << (event.info[:art_new] ? " NEW!" : " ENCORE")
|
|
210
|
+
s << " GALLERY #{g[0]}/#{g[1]}" if g
|
|
211
|
+
paint(s, Color.ramp(Color::NEON, 0.15), bold: true)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def draw_stars(c, t)
|
|
217
|
+
@stars.each do |x, y, ph, sp|
|
|
218
|
+
f = (t * sp + ph) % 1.0
|
|
219
|
+
next if f > 0.5
|
|
220
|
+
k = 1 - (f - 0.25).abs * 4
|
|
221
|
+
c.put(x, y, k > 0.7 ? "+" : ".", Color.mix([40, 30, 70], [220, 200, 255], k))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def draw_title(c, t)
|
|
226
|
+
y0 = (@h - Font::HEIGHT) / 2 - 1
|
|
227
|
+
drop = t < LAND ? ((1 - Fx.ease_in(t / LAND)) * (y0 + 6)).round : 0
|
|
228
|
+
sweep = (t * 1.4) % 1.4 - 0.2
|
|
229
|
+
fn = lambda do |_rx, ry, f|
|
|
230
|
+
base = Color.rainbow(f * 0.8 - t + ry * 0.03, 0.8)
|
|
231
|
+
(f - sweep).abs < 0.07 && t >= LAND ? [255, 255, 255] : base
|
|
232
|
+
end
|
|
233
|
+
Fx.big_text(c, @title, (@w - Font.width(@title, scale: @tscale)) / 2, y0 - drop, fn, scale: @tscale)
|
|
234
|
+
return if t < LAND
|
|
235
|
+
c.tint_bg([200, 120, 255], 0.55 * (1 - Fx.phase(t, LAND, LAND + 0.15))) if ctx.config.flash != :off
|
|
236
|
+
Fx.shockwave(c, @w / 2.0, y0 + 2.5, t - LAND, speed: 110, palette: Color::NEON, life: 0.6, rings: 3)
|
|
237
|
+
lv = @level_up ? "LV #{event.level - 1} >> LV #{event.level}" : "A MASTERPIECE APPEARS"
|
|
238
|
+
n = (lv.size * Fx.phase(t, LAND + 0.1, LAND + 0.4)).round
|
|
239
|
+
c.put_center(y0 + Font::HEIGHT + 2, lv[0, n], [255, 230, 140], bold: true)
|
|
240
|
+
msg = "BONUS ART UNLOCKED"
|
|
241
|
+
c.put_center(y0 + Font::HEIGHT + 4, msg, ((t * 8).to_i.even? ? [255, 255, 255] : [255, 200, 90]), bold: true) if t > LAND + 0.45
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def draw_art(c, t)
|
|
245
|
+
p = Fx.phase(t, REVEAL, REVEAL + 0.45)
|
|
246
|
+
rows = @ah / 2
|
|
247
|
+
# gold frame
|
|
248
|
+
frame = Color.mix([255, 240, 170], [190, 130, 30], (Math.sin(t * 6) + 1) / 2)
|
|
249
|
+
c.fill_bg(frame, @ax - 2, @ay - 1, @aw + 4, rows + 2)
|
|
250
|
+
c.fill_bg([70, 45, 10], @ax - 1, @ay, @aw + 2, rows) if p < 1
|
|
251
|
+
head = rows * p
|
|
252
|
+
sheen = (t - REVEAL - 0.5) * 1.2
|
|
253
|
+
Gallery.draw(c, @piece, @ax, @ay, @aw, @ah, reveal: p, depth: depth) do |top, bot, x, r|
|
|
254
|
+
if p < 1 && (r - head).abs < 1.2
|
|
255
|
+
[Color.mix(top, [255, 255, 255], 0.8), Color.mix(bot, [255, 255, 255], 0.8)]
|
|
256
|
+
elsif sheen.between?(0, 1.5) && ((x.fdiv(@aw) + r.fdiv(rows) * 0.5) - sheen).abs < 0.06
|
|
257
|
+
[Color.mix(top, [255, 255, 255], 0.55), Color.mix(bot, [255, 255, 255], 0.55)]
|
|
258
|
+
else
|
|
259
|
+
[top, bot]
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
Fx.sparkles(c, t, 21, 18, palette: Color::GOLD, area: [@ax - 4, @ay - 1, @aw + 8, rows + 2]) if p >= 1
|
|
263
|
+
head = @level_up ? "LEVEL #{event.level}" : @title
|
|
264
|
+
c.put_center(0, "#{Term.glyph('✦', '*')} #{head} #{Term.glyph('✦', '*')} BONUS ART", Color.rainbow(t * 0.7), bold: true)
|
|
265
|
+
return if t < REVEAL + 0.45
|
|
266
|
+
cap = "\"#{@piece.title}\" #{@piece.artist}, #{@piece.year}"
|
|
267
|
+
n = (cap.size * Fx.phase(t, REVEAL + 0.5, REVEAL + 1.0)).round
|
|
268
|
+
c.put_center(@ay + rows + 1, cap[0, n], [240, 230, 210], bold: true)
|
|
269
|
+
g = event.info[:gallery]
|
|
270
|
+
tag = event.info[:art_new] ? "NEW!" : "ENCORE"
|
|
271
|
+
tag += " GALLERY #{g[0]}/#{g[1]}" if g
|
|
272
|
+
if t > REVEAL + 0.9
|
|
273
|
+
col = event.info[:art_new] && (t * 6).to_i.even? ? [255, 90, 90] : [255, 220, 120]
|
|
274
|
+
c.put_center(@ay + rows + 2, tag, col, bold: true)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -64,6 +64,7 @@ module Dopairb
|
|
|
64
64
|
bits = [event.info[:class_name]]
|
|
65
65
|
bits << "x#{event.streak} in a row" if event.streak > 1
|
|
66
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)
|
|
67
68
|
bits.join(" ")
|
|
68
69
|
end
|
|
69
70
|
|
|
@@ -90,6 +90,8 @@ module Dopairb
|
|
|
90
90
|
@panel = [@w / 2 + 4, @table_y, 30, 9] if @two_col
|
|
91
91
|
@score_y = @h - 7
|
|
92
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
|
|
93
95
|
@stars = Array.new(80) { [@rng.rand(@w), @rng.rand(@h), @rng.rand, 0.4 + @rng.rand] }
|
|
94
96
|
build_particles
|
|
95
97
|
end
|
|
@@ -144,6 +146,7 @@ module Dopairb
|
|
|
144
146
|
draw_table(c, t)
|
|
145
147
|
draw_score(c, t)
|
|
146
148
|
draw_rank(c, t)
|
|
149
|
+
draw_xp(c, t)
|
|
147
150
|
@parts.draw(c, t)
|
|
148
151
|
@fire.draw(c, t)
|
|
149
152
|
draw_footer(c, t)
|
|
@@ -156,6 +159,13 @@ module Dopairb
|
|
|
156
159
|
rows_data.each { |k, v| lines << "#{x}#{k.ljust(12)}#{value_text(k, v).rjust(14)}" }
|
|
157
160
|
lines << paint("#{x}#{'SCORE'.ljust(12)}#{Fx.number_with_commas(@score).rjust(14)}", [255, 220, 130], bold: true)
|
|
158
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
|
|
159
169
|
lines.join("\n")
|
|
160
170
|
end
|
|
161
171
|
|
|
@@ -222,11 +232,16 @@ module Dopairb
|
|
|
222
232
|
landed = t >= T.score_land
|
|
223
233
|
val = landed ? @score : (@score * Fx.ease_in(p) ** 0.7).round
|
|
224
234
|
txt = Fx.number_with_commas(val)
|
|
235
|
+
best = landed && @new_best
|
|
225
236
|
unless @big_score
|
|
226
|
-
c.put_center(@table_y + T::ROWS + 1, "TOTAL SCORE #{txt}", landed ? Color.rainbow(t) : [150, 220, 255], bold: true)
|
|
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)
|
|
227
238
|
return
|
|
228
239
|
end
|
|
229
|
-
|
|
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
|
|
230
245
|
scale = Font.width(Fx.number_with_commas(@score), scale: 2) <= @w - 4 ? 2 : 1
|
|
231
246
|
scale = 1 if Font.width(txt, scale: scale) > @w - 2
|
|
232
247
|
fn = lambda do |_rx, ry, f|
|
|
@@ -269,6 +284,31 @@ module Dopairb
|
|
|
269
284
|
c.put(px + (pw - @title.size) / 2, py + 7, @title[0, n], Color.ramp(@rpal, 0.1), bold: true)
|
|
270
285
|
end
|
|
271
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
|
+
|
|
272
312
|
def draw_footer(c, t)
|
|
273
313
|
msg = "thanks for playing #{Term.glyph('✦', '*')} see you next session"
|
|
274
314
|
n = (msg.size * Fx.phase(t, T.rank + 0.9, T.rank + 1.5)).round
|
|
@@ -7,10 +7,24 @@ 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
|
|
11
16
|
def sfx = :intro
|
|
12
17
|
def impact_at = 0.2
|
|
13
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
|
|
27
|
+
|
|
14
28
|
def setup
|
|
15
29
|
@scale = best_scale(TITLE)
|
|
16
30
|
@parts = Fx::Particles.new(@rng)
|
|
@@ -55,15 +69,19 @@ module Dopairb
|
|
|
55
69
|
end
|
|
56
70
|
@parts.draw(c, t)
|
|
57
71
|
Fx.sparkles(c, t, 11, 16, palette: Color::GOLD) if t > 0.7
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
60
76
|
end
|
|
61
77
|
|
|
62
78
|
def trail
|
|
63
79
|
star = Term.glyph("✦", "*")
|
|
64
80
|
title = "#{star} DOPA IRB #{VERSION} #{star}"
|
|
65
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"
|
|
66
|
-
|
|
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])}"
|
|
67
85
|
end
|
|
68
86
|
end
|
|
69
87
|
|
|
@@ -23,10 +23,12 @@ module Dopairb
|
|
|
23
23
|
c.put(0, 0, "=>", hot, bold: true)
|
|
24
24
|
if t < 0.16
|
|
25
25
|
head = 3 + (@target - 3) * Fx.ease_in(t / 0.16)
|
|
26
|
+
fever = event.flag?(:fever)
|
|
26
27
|
9.times do |i|
|
|
27
28
|
x = head - i
|
|
28
29
|
break if x < 3
|
|
29
|
-
|
|
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)
|
|
30
32
|
end
|
|
31
33
|
else
|
|
32
34
|
p = Fx.phase(t, 0.16, 0.42)
|
|
@@ -34,13 +36,18 @@ module Dopairb
|
|
|
34
36
|
txt = "+#{Fx.number_with_commas(event.gain)}"
|
|
35
37
|
col = Color.mix([255, 255, 255], Color.ramp(pal, 0.3), p)
|
|
36
38
|
x = c.put(@target + 2, 0, txt, col, bg: p < 0.25 ? Color.ramp(pal, 0.6) : nil, bold: true)
|
|
37
|
-
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
|
|
38
45
|
end
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def trail
|
|
42
|
-
paint("=> #{Term.glyph('✦', '*')} +#{Fx.number_with_commas(event.gain)}" + (event.combo > 1 ? " COMBO #{event.combo}" : "")
|
|
43
|
-
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)
|
|
44
51
|
end
|
|
45
52
|
end
|
|
46
53
|
|