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,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# RGB helpers and SGR encoding for truecolor / 256 / 16 / no-color terminals.
|
|
5
|
+
module Color
|
|
6
|
+
FIRE = [[255, 255, 230], [255, 236, 110], [255, 160, 20], [235, 60, 10], [120, 10, 30], [40, 0, 20]].freeze
|
|
7
|
+
ICE = [[240, 255, 255], [130, 230, 255], [40, 140, 255], [30, 50, 190], [20, 10, 80]].freeze
|
|
8
|
+
GOLD = [[255, 255, 240], [255, 230, 120], [255, 190, 40], [200, 120, 0], [90, 40, 0]].freeze
|
|
9
|
+
TOXIC = [[240, 255, 230], [150, 255, 120], [40, 220, 90], [0, 130, 70], [0, 50, 30]].freeze
|
|
10
|
+
BLOOD = [[255, 230, 230], [255, 90, 90], [220, 20, 40], [130, 0, 20], [50, 0, 10]].freeze
|
|
11
|
+
NEON = [[255, 240, 255], [255, 110, 230], [190, 60, 255], [80, 40, 220], [30, 10, 90]].freeze
|
|
12
|
+
SMOKE = [[235, 235, 235], [170, 170, 180], [110, 110, 125], [60, 60, 70], [30, 30, 35]].freeze
|
|
13
|
+
STEEL = [[230, 240, 255], [150, 170, 200], [90, 105, 140], [50, 60, 85], [25, 28, 40]].freeze
|
|
14
|
+
|
|
15
|
+
BASIC16 = [
|
|
16
|
+
[0, 0, 0], [205, 0, 0], [0, 205, 0], [205, 205, 0], [0, 0, 238], [205, 0, 205], [0, 205, 205], [229, 229, 229],
|
|
17
|
+
[127, 127, 127], [255, 0, 0], [0, 255, 0], [255, 255, 0], [92, 92, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255],
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def clamp(x)
|
|
23
|
+
x = x.to_i
|
|
24
|
+
x < 0 ? 0 : (x > 255 ? 255 : x)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def mix(a, b, t)
|
|
28
|
+
t = t < 0 ? 0.0 : (t > 1 ? 1.0 : t)
|
|
29
|
+
[clamp(a[0] + (b[0] - a[0]) * t), clamp(a[1] + (b[1] - a[1]) * t), clamp(a[2] + (b[2] - a[2]) * t)]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def scale(c, k)
|
|
33
|
+
[clamp(c[0] * k), clamp(c[1] * k), clamp(c[2] * k)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def hsv(h, s = 1.0, v = 1.0)
|
|
37
|
+
h = (h % 360) / 60.0
|
|
38
|
+
i = h.floor
|
|
39
|
+
f = h - i
|
|
40
|
+
p = v * (1 - s)
|
|
41
|
+
q = v * (1 - s * f)
|
|
42
|
+
t = v * (1 - s * (1 - f))
|
|
43
|
+
r, g, b = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i % 6]
|
|
44
|
+
[clamp(r * 255), clamp(g * 255), clamp(b * 255)]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rainbow(t, s = 0.85, v = 1.0)
|
|
48
|
+
hsv(t * 360, s, v)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# t in 0..1 walks through the stops
|
|
52
|
+
def ramp(stops, t)
|
|
53
|
+
t = t < 0 ? 0.0 : (t > 1 ? 1.0 : t)
|
|
54
|
+
pos = t * (stops.size - 1)
|
|
55
|
+
i = pos.floor
|
|
56
|
+
return stops[-1] if i >= stops.size - 1
|
|
57
|
+
mix(stops[i], stops[i + 1], pos - i)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def sgr(fg, bg, bold, depth)
|
|
61
|
+
codes = ["0"]
|
|
62
|
+
codes << "1" if bold
|
|
63
|
+
if depth == :none
|
|
64
|
+
codes << "7" if bg && luminance(bg) > 90
|
|
65
|
+
else
|
|
66
|
+
codes << fg_code(fg, depth) if fg
|
|
67
|
+
codes << bg_code(bg, depth) if bg
|
|
68
|
+
end
|
|
69
|
+
"\e[#{codes.join(';')}m"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fg(c, depth, bold: false)
|
|
73
|
+
sgr(c, nil, bold, depth)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def reset = "\e[0m"
|
|
77
|
+
|
|
78
|
+
def fg_code(c, depth)
|
|
79
|
+
case depth
|
|
80
|
+
when :truecolor then "38;2;#{c[0]};#{c[1]};#{c[2]}"
|
|
81
|
+
when :"256" then "38;5;#{to256(c)}"
|
|
82
|
+
else
|
|
83
|
+
i = to16(c)
|
|
84
|
+
(i < 8 ? 30 + i : 82 + i).to_s
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def bg_code(c, depth)
|
|
89
|
+
case depth
|
|
90
|
+
when :truecolor then "48;2;#{c[0]};#{c[1]};#{c[2]}"
|
|
91
|
+
when :"256" then "48;5;#{to256(c)}"
|
|
92
|
+
else
|
|
93
|
+
i = to16(c)
|
|
94
|
+
(i < 8 ? 40 + i : 92 + i).to_s
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def luminance(c)
|
|
99
|
+
0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def to256(c)
|
|
103
|
+
r, g, b = c
|
|
104
|
+
if (r - g).abs < 10 && (g - b).abs < 10
|
|
105
|
+
return 16 if r < 8
|
|
106
|
+
return 231 if r > 246
|
|
107
|
+
return 232 + ((r - 8) / 10.0).round.clamp(0, 23)
|
|
108
|
+
end
|
|
109
|
+
q = ->(v) { v < 48 ? 0 : (v < 115 ? 1 : ((v - 35) / 40).clamp(0, 5)) }
|
|
110
|
+
16 + 36 * q.(r) + 6 * q.(g) + q.(b)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to16(c)
|
|
114
|
+
best = 0
|
|
115
|
+
bd = Float::INFINITY
|
|
116
|
+
BASIC16.each_with_index do |bc, i|
|
|
117
|
+
d = (bc[0] - c[0])**2 * 2 + (bc[1] - c[1])**2 * 4 + (bc[2] - c[2])**2 * 3
|
|
118
|
+
if d < bd
|
|
119
|
+
bd = d
|
|
120
|
+
best = i
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
best
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# User-tunable knobs. See `dopa help` or README for the meaning of each.
|
|
5
|
+
class Config
|
|
6
|
+
INTENSITIES = %i[off low normal max].freeze
|
|
7
|
+
FLASHES = %i[off soft full].freeze
|
|
8
|
+
COLORS = %i[auto truecolor 256 16 none].freeze
|
|
9
|
+
TRAILS = %i[none big all].freeze
|
|
10
|
+
|
|
11
|
+
DESCRIPTIONS = {
|
|
12
|
+
intensity: "off / low / normal / max -- overall amount of effects",
|
|
13
|
+
motion: "on / off -- animations (off = static badges only)",
|
|
14
|
+
flash: "off / soft / full -- soft = region only, full = whole screen",
|
|
15
|
+
sound: "on / off -- terminal bell on big moments",
|
|
16
|
+
hud: "on / off -- COMBO / SCORE / CHARGE next to the prompt",
|
|
17
|
+
duration: "float multiplier for every effect length (e.g. 0.5, 2)",
|
|
18
|
+
color: "auto / truecolor / 256 / 16 / none",
|
|
19
|
+
trail: "none / big / all -- which effects leave a one-line badge",
|
|
20
|
+
charge: "seconds (max 0.3) of wind-up before a fast result lands; 0 disables",
|
|
21
|
+
intro: "on / off -- title animation at startup",
|
|
22
|
+
keys: "on / off -- per-keystroke effects while typing",
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :intensity, :motion, :flash, :sound, :hud, :duration, :color, :trail, :charge, :intro, :keys
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@intensity = :normal
|
|
29
|
+
@motion = true
|
|
30
|
+
@flash = :soft
|
|
31
|
+
@sound = false
|
|
32
|
+
@hud = true
|
|
33
|
+
@duration = 1.0
|
|
34
|
+
@color = :auto
|
|
35
|
+
@trail = :big
|
|
36
|
+
@charge = 0.12
|
|
37
|
+
@intro = true
|
|
38
|
+
@keys = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def level
|
|
42
|
+
INTENSITIES.index(@intensity)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def off? = @intensity == :off
|
|
46
|
+
def max? = @intensity == :max
|
|
47
|
+
|
|
48
|
+
def set(name, value)
|
|
49
|
+
name = name.to_s.strip.downcase.tr("-", "_").to_sym
|
|
50
|
+
case name
|
|
51
|
+
when :intensity then @intensity = enum(value, INTENSITIES, name)
|
|
52
|
+
when :flash
|
|
53
|
+
v = bool_or_nil(value)
|
|
54
|
+
@flash = v.nil? ? enum(value, FLASHES, name) : (v ? :soft : :off)
|
|
55
|
+
when :color
|
|
56
|
+
v = bool_or_nil(value)
|
|
57
|
+
@color = v.nil? ? enum(value, COLORS, name) : (v ? :auto : :none)
|
|
58
|
+
when :trail then @trail = enum(value, TRAILS, name)
|
|
59
|
+
when :motion, :sound, :hud, :intro, :keys
|
|
60
|
+
v = bool_or_nil(value)
|
|
61
|
+
raise ArgumentError, "#{name} expects on/off, got #{value.inspect}" if v.nil?
|
|
62
|
+
instance_variable_set(:"@#{name}", v)
|
|
63
|
+
when :duration
|
|
64
|
+
f = Float(value.to_s)
|
|
65
|
+
raise ArgumentError, "duration must be in 0.1..5" unless (0.1..5).cover?(f)
|
|
66
|
+
@duration = f
|
|
67
|
+
when :charge
|
|
68
|
+
f = Float(value.to_s)
|
|
69
|
+
raise ArgumentError, "charge must be in 0..0.3 (seconds)" unless (0..0.3).cover?(f)
|
|
70
|
+
@charge = f
|
|
71
|
+
else
|
|
72
|
+
raise ArgumentError, "unknown setting #{name.inspect} (known: #{DESCRIPTIONS.keys.join(', ')})"
|
|
73
|
+
end
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# "intensity=max,flash=off" (DOPAIRB env var / --set option)
|
|
78
|
+
def apply_string(str)
|
|
79
|
+
str.to_s.split(/[,\s]+/).reject(&:empty?).each do |pair|
|
|
80
|
+
k, v = pair.split("=", 2)
|
|
81
|
+
raise ArgumentError, "expected key=value, got #{pair.inspect}" unless v
|
|
82
|
+
set(k, v)
|
|
83
|
+
end
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_h
|
|
88
|
+
DESCRIPTIONS.keys.to_h { |k| [k, public_send(k)] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def describe
|
|
92
|
+
to_h.map { |k, v| format(" %-10s %-8s %s", k, show(v), DESCRIPTIONS[k]) }.join("\n")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def show(v)
|
|
98
|
+
case v
|
|
99
|
+
when true then "on"
|
|
100
|
+
when false then "off"
|
|
101
|
+
else v.to_s
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def enum(value, list, name)
|
|
106
|
+
sym = value.to_s.strip.downcase.to_sym
|
|
107
|
+
return sym if list.include?(sym)
|
|
108
|
+
raise ArgumentError, "#{name} must be one of #{list.join(' / ')}, got #{value.inspect}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def bool_or_nil(value)
|
|
112
|
+
case value.to_s.strip.downcase
|
|
113
|
+
when "on", "true", "yes", "1" then true
|
|
114
|
+
when "off", "false", "no", "0" then false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/dopairb/demo.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# `dopa demo`: every scene once, with made-up events. Does not touch the score.
|
|
5
|
+
module Demo
|
|
6
|
+
E = Game::Event
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def ev(kind, info, flags: [], gain: 120, combo: 3, streak: 0)
|
|
11
|
+
E.new(kind: kind, tier: nil, info: info, flags: flags, gain: gain, combo: combo, streak: streak, score: 12_480)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cases
|
|
15
|
+
[
|
|
16
|
+
["(1..100).sum", ev(:success, { type: :integer, value: 5050 })],
|
|
17
|
+
["[].first", ev(:success, { type: :nil })],
|
|
18
|
+
["1 < 2", ev(:success, { type: :true })],
|
|
19
|
+
["[] .any?", ev(:success, { type: :false })],
|
|
20
|
+
["2**64", ev(:success, { type: :integer, value: 2**64 })],
|
|
21
|
+
["'dopamine ' * 12", ev(:success, { type: :string, length: 108, head: "dopamine " * 12 })],
|
|
22
|
+
["(1..24).to_a", ev(:success, { type: :array, size: 24 })],
|
|
23
|
+
["def boost = :on", ev(:definition, { type: :definition, what: :method, name: "boost" })],
|
|
24
|
+
["3.times { puts }", ev(:success, { type: :nil, out_lines: 120 }, flags: [:output_rain])],
|
|
25
|
+
["x = 1", ev(:success, { type: :integer, value: 1 }, flags: [:first_hit], combo: 1)],
|
|
26
|
+
["10.times.sum", ev(:success, { type: :integer, value: 45 }, flags: [:combo_milestone, :combo_mega], combo: 10, gain: 1350)],
|
|
27
|
+
["1 + ", ev(:failure, { type: :syntax, class_name: "SyntaxError", line: 1, column: 4, detail: "unexpected end-of-input; expected an expression after the operator" })],
|
|
28
|
+
["undefined_thing", ev(:failure, { type: :name, class_name: "NameError", name: "undefined_thing", message: "undefined local variable or method 'undefined_thing' for main" }, combo: 0, streak: 1)],
|
|
29
|
+
["1.shout", ev(:failure, { type: :nomethod, class_name: "NoMethodError", name: "shout", receiver: "Integer", message: "undefined method 'shout' for an instance of Integer" }, combo: 0, streak: 2)],
|
|
30
|
+
["1 + '1'", ev(:failure, { type: :type, class_name: "TypeError", sides: %w[String Integer], message: "String can't be coerced into Integer" }, combo: 0, streak: 3)],
|
|
31
|
+
["[1].first(1, 2)", ev(:failure, { type: :argument, class_name: "ArgumentError", sides: ["given 2", "expected 0..1"], message: "wrong number of arguments (given 2, expected 0..1)" }, combo: 0, streak: 4)],
|
|
32
|
+
["raise 'boom'", ev(:failure, { type: :other, class_name: "RuntimeError", message: "boom" }, combo: 0, streak: 5)],
|
|
33
|
+
["1 + 1", ev(:success, { type: :integer, value: 2 }, flags: [:comeback], combo: 1, streak: 5, gain: 600)],
|
|
34
|
+
["loop {}", ev(:interrupt, { charge: 60 })],
|
|
35
|
+
["2**128", ev(:success, { type: :integer, value: 2**128 }, flags: [:new_record, :record_jump], gain: 420)],
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run(mod)
|
|
40
|
+
d = mod.session&.director || Director.new(mod.config)
|
|
41
|
+
unless mod.active?
|
|
42
|
+
puts "dopairb: not active (no terminal or intensity=off)"
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
prompt = "\e[1;35mdopa\e[0m> "
|
|
46
|
+
cases.each do |code, event|
|
|
47
|
+
break if Term.input_pending?
|
|
48
|
+
line = "#{prompt}#{IRB::Color.colorize_code(code, complete: true)}"
|
|
49
|
+
Term.write("#{line}\r\n")
|
|
50
|
+
shot = InputShot.new(rows: [line], lines: [[6, code]], code: code, screen_width: Term.cols)
|
|
51
|
+
if event.kind == :failure && event.info[:type] == :syntax
|
|
52
|
+
event.info[:column] = [code.size, event.info[:column]].min
|
|
53
|
+
end
|
|
54
|
+
d.play_event(event, input: shot, pre: event.kind == :success ? mod.config.charge : 0)
|
|
55
|
+
Term.write("\e[2m=> (demo)\e[0m\r\n")
|
|
56
|
+
sleep 0.15
|
|
57
|
+
end
|
|
58
|
+
Term.write("\e[0m")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# Picks a scene for each event and plays it. Bigger moments win; smaller
|
|
5
|
+
# ones are folded into the subtitle instead of being queued.
|
|
6
|
+
class Director
|
|
7
|
+
FPS = 40.0
|
|
8
|
+
|
|
9
|
+
attr_reader :config
|
|
10
|
+
|
|
11
|
+
def initialize(config, rng: Random.new)
|
|
12
|
+
@config = config
|
|
13
|
+
@rng = rng
|
|
14
|
+
@last_flash = -10.0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ctx(input: nil, pre: 0.0, rows: nil, cols: nil)
|
|
18
|
+
r, c = Term.size
|
|
19
|
+
Ctx.new(w: (cols || c) - 1, rows: rows || r, depth: Term.depth(@config), config: @config, rng: @rng,
|
|
20
|
+
input: input, pre: pre, compact: @config.level <= 1 || (cols || c) < 40)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def scene_for(event, ctx)
|
|
24
|
+
info = event.info
|
|
25
|
+
case event.kind
|
|
26
|
+
when :interrupt then Scenes::Scatter.new(ctx, event)
|
|
27
|
+
when :failure then failure_scene(info[:type], ctx, event)
|
|
28
|
+
else success_scene(event, ctx)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def failure_scene(type, ctx, event)
|
|
33
|
+
case type
|
|
34
|
+
when :syntax then Scenes::Crack.new(ctx, event)
|
|
35
|
+
when :name then Scenes::Spotlight.new(ctx, event)
|
|
36
|
+
when :nomethod then Scenes::Snap.new(ctx, event)
|
|
37
|
+
when :type, :argument then Scenes::Clash.new(ctx, event)
|
|
38
|
+
else Scenes::FailureScene.new(ctx, event)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def success_scene(event, ctx)
|
|
43
|
+
info = event.info
|
|
44
|
+
alt = @config.max? && @config.motion
|
|
45
|
+
mega = lambda do |text, palette, rainbow: false, sub: nil|
|
|
46
|
+
if alt
|
|
47
|
+
Scenes::Banner.new(self.ctx(rows: ctx.rows, cols: ctx.w + 1).tap { |c| c.depth = ctx.depth }, event, text: text, palette: palette, mega: true,
|
|
48
|
+
rainbow: rainbow, sub: sub, alt: true)
|
|
49
|
+
else
|
|
50
|
+
Scenes::Banner.new(ctx, event, text: text, palette: palette, mega: true, rainbow: rainbow, sub: sub)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
if event.flag?(:comeback)
|
|
54
|
+
mega.(event.streak >= 2 ? "COMEBACK!" : "FIXED!", Color::GOLD, rainbow: event.streak >= 2)
|
|
55
|
+
elsif event.flag?(:eval_milestone)
|
|
56
|
+
mega.("#{Fx.number_with_commas(event_evals(event))} EVALS", Color::NEON, rainbow: true)
|
|
57
|
+
elsif event.flag?(:record_jump)
|
|
58
|
+
mega.("NEW RECORD", Color::GOLD, rainbow: true, sub: number_text(info))
|
|
59
|
+
elsif event.flag?(:combo_mega)
|
|
60
|
+
mega.("COMBO #{event.combo}!", Color::FIRE, rainbow: event.combo >= 50)
|
|
61
|
+
elsif info[:type] == :definition
|
|
62
|
+
Scenes::Unlock.new(ctx, event)
|
|
63
|
+
elsif event.flag?(:combo_milestone) && event.combo >= 5
|
|
64
|
+
Scenes::Banner.new(ctx, event, text: "COMBO #{event.combo}", palette: Color::FIRE)
|
|
65
|
+
elsif event.flag?(:first_hit)
|
|
66
|
+
Scenes::Banner.new(ctx, event, text: "FIRST HIT!", palette: Color::ICE)
|
|
67
|
+
elsif event.flag?(:output_rain)
|
|
68
|
+
Scenes::Rain.new(ctx, event)
|
|
69
|
+
else
|
|
70
|
+
value_scene(event, ctx)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def value_scene(event, ctx)
|
|
75
|
+
info = event.info
|
|
76
|
+
case info[:type]
|
|
77
|
+
when :nil then Scenes::Puff.new(ctx, event)
|
|
78
|
+
when :true, :false then Scenes::Stamp.new(ctx, event)
|
|
79
|
+
when :integer, :float
|
|
80
|
+
info[:value].abs >= 10_000 || event.flag?(:new_record) ? Scenes::Counter.new(ctx, event) : Scenes::Hit.new(ctx, event)
|
|
81
|
+
when :huge
|
|
82
|
+
Scenes::Banner.new(ctx, event, text: "HUGE", palette: Color::GOLD, sub: "#{Fx.number_with_commas(info[:digits])} DIGITS")
|
|
83
|
+
when :string then info[:length] >= 40 ? Scenes::Stream.new(ctx, event) : Scenes::Hit.new(ctx, event)
|
|
84
|
+
when :array, :hash then info[:size] > 0 ? Scenes::Crate.new(ctx, event) : Scenes::Hit.new(ctx, event)
|
|
85
|
+
else Scenes::Hit.new(ctx, event)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def play_event(event, input: nil, pre: 0.0)
|
|
90
|
+
return if @config.off?
|
|
91
|
+
c = ctx(input: input, pre: pre)
|
|
92
|
+
c.input = nil if input && input.size + 12 > c.rows
|
|
93
|
+
play(scene_for(event, c))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def intro
|
|
97
|
+
return if @config.off? || !@config.intro
|
|
98
|
+
play(Scenes::Intro.new(ctx), force_trail: true)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def outro(stats)
|
|
102
|
+
return if @config.off?
|
|
103
|
+
play(Scenes::Outro.new(ctx, stats), force_trail: true)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def play(scene, force_trail: false)
|
|
107
|
+
animated = @config.motion && scene.ctx.w >= 20 && fits?(scene)
|
|
108
|
+
animated = false if animated && Term.input_pending?
|
|
109
|
+
run(scene) if animated
|
|
110
|
+
Term.bell if @config.sound && scene.bell? && animated
|
|
111
|
+
emit_trail(scene, force_trail)
|
|
112
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
113
|
+
raise if SystemExit === e || (SignalException === e && !(Interrupt === e))
|
|
114
|
+
# Never let an effect break the REPL; Ctrl-C during an effect just skips it.
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def event_evals(_event)
|
|
120
|
+
Dopairb.session&.game&.evals.to_i
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def number_text(info)
|
|
124
|
+
Integer === info[:value] ? Fx.number_with_commas(info[:value]) : info[:value].to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def fits?(scene)
|
|
128
|
+
top = scene.top? ? scene.ctx.input.size : 0
|
|
129
|
+
alt = scene.is_a?(Scenes::Banner) && scene.height == scene.ctx.rows
|
|
130
|
+
alt || scene.height + top <= scene.ctx.rows - 1
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def emit_trail(scene, force)
|
|
134
|
+
tr = scene.trail
|
|
135
|
+
return unless tr
|
|
136
|
+
ok = force || @config.trail == :all || (@config.trail == :big && scene.big?)
|
|
137
|
+
return unless ok
|
|
138
|
+
tr = tr.gsub(/\e\[[\d;]*m/, "") if scene.depth == :none
|
|
139
|
+
Term.write(tr.gsub("\n", "\r\n") + "\e[0m\r\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def run(scene)
|
|
143
|
+
alt = scene.is_a?(Scenes::Banner) && scene.height == scene.ctx.rows && scene.mega?
|
|
144
|
+
top = !alt && scene.top? ? scene.ctx.input.rows : []
|
|
145
|
+
stage = alt ? AltStage.new(scene.height) : Stage.new(scene.height, top: top)
|
|
146
|
+
depth = scene.depth
|
|
147
|
+
flash_off = nil
|
|
148
|
+
Term.raw do
|
|
149
|
+
stage.open
|
|
150
|
+
t0 = now
|
|
151
|
+
flashed = false
|
|
152
|
+
begin
|
|
153
|
+
loop do
|
|
154
|
+
break if Term.input_pending? || !stage.open?
|
|
155
|
+
t = (now - t0) / @config.duration
|
|
156
|
+
break if t >= scene.total_length
|
|
157
|
+
lines = []
|
|
158
|
+
unless top.empty?
|
|
159
|
+
tc = Canvas.new(scene.ctx.w, top.size)
|
|
160
|
+
lines.concat(scene.draw_top(tc, t) ? tc.render(depth) : top)
|
|
161
|
+
end
|
|
162
|
+
ts = t - scene.pre
|
|
163
|
+
c = Canvas.new(scene.ctx.w, scene.height)
|
|
164
|
+
scene.draw(c, ts) if ts >= 0
|
|
165
|
+
lines.concat(c.render(depth, shake: ts >= 0 ? scene.shake(ts) : 0))
|
|
166
|
+
if !flashed && scene.flash_at && ts >= scene.flash_at
|
|
167
|
+
flashed = true
|
|
168
|
+
if @config.flash == :full && now - @last_flash > 0.4
|
|
169
|
+
@last_flash = now
|
|
170
|
+
Term.write("\e[?5h")
|
|
171
|
+
flash_off = now + 0.05
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
stage.draw(lines)
|
|
175
|
+
if flash_off && now >= flash_off
|
|
176
|
+
Term.write("\e[?5l")
|
|
177
|
+
flash_off = nil
|
|
178
|
+
end
|
|
179
|
+
sleep(1 / FPS)
|
|
180
|
+
end
|
|
181
|
+
ensure
|
|
182
|
+
Term.write("\e[?5l") if flash_off
|
|
183
|
+
stage.close
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
189
|
+
end
|
|
190
|
+
end
|
data/lib/dopairb/font.rb
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# 5-row bitmap font for the big banners.
|
|
5
|
+
module Font
|
|
6
|
+
HEIGHT = 5
|
|
7
|
+
|
|
8
|
+
RAW = {
|
|
9
|
+
"A" => [" ### ", "# #", "#####", "# #", "# #"],
|
|
10
|
+
"B" => ["#### ", "# #", "#### ", "# #", "#### "],
|
|
11
|
+
"C" => [" ####", "# ", "# ", "# ", " ####"],
|
|
12
|
+
"D" => ["#### ", "# #", "# #", "# #", "#### "],
|
|
13
|
+
"E" => ["#####", "# ", "#### ", "# ", "#####"],
|
|
14
|
+
"F" => ["#####", "# ", "#### ", "# ", "# "],
|
|
15
|
+
"G" => [" ####", "# ", "# ##", "# #", " ####"],
|
|
16
|
+
"H" => ["# #", "# #", "#####", "# #", "# #"],
|
|
17
|
+
"I" => ["###", " # ", " # ", " # ", "###"],
|
|
18
|
+
"J" => [" ###", " # ", " # ", "# # ", " ## "],
|
|
19
|
+
"K" => ["# #", "# # ", "### ", "# # ", "# #"],
|
|
20
|
+
"L" => ["# ", "# ", "# ", "# ", "#####"],
|
|
21
|
+
"M" => ["# #", "## ##", "# # #", "# #", "# #"],
|
|
22
|
+
"N" => ["# #", "## #", "# # #", "# ##", "# #"],
|
|
23
|
+
"O" => [" ### ", "# #", "# #", "# #", " ### "],
|
|
24
|
+
"P" => ["#### ", "# #", "#### ", "# ", "# "],
|
|
25
|
+
"Q" => [" ### ", "# #", "# # #", "# # ", " ## #"],
|
|
26
|
+
"R" => ["#### ", "# #", "#### ", "# # ", "# #"],
|
|
27
|
+
"S" => [" ####", "# ", " ### ", " #", "#### "],
|
|
28
|
+
"T" => ["#####", " # ", " # ", " # ", " # "],
|
|
29
|
+
"U" => ["# #", "# #", "# #", "# #", " ### "],
|
|
30
|
+
"V" => ["# #", "# #", "# #", " # # ", " # "],
|
|
31
|
+
"W" => ["# #", "# #", "# # #", "## ##", "# #"],
|
|
32
|
+
"X" => ["# #", " # # ", " # ", " # # ", "# #"],
|
|
33
|
+
"Y" => ["# #", " # # ", " # ", " # ", " # "],
|
|
34
|
+
"Z" => ["#####", " # ", " # ", " # ", "#####"],
|
|
35
|
+
"0" => [" ### ", "# ##", "# # #", "## #", " ### "],
|
|
36
|
+
"1" => [" # ", "## ", " # ", " # ", "###"],
|
|
37
|
+
"2" => ["#### ", " #", " ### ", "# ", "#####"],
|
|
38
|
+
"3" => ["#### ", " #", " ### ", " #", "#### "],
|
|
39
|
+
"4" => ["# #", "# #", "#####", " #", " #"],
|
|
40
|
+
"5" => ["#####", "# ", "#### ", " #", "#### "],
|
|
41
|
+
"6" => [" ### ", "# ", "#### ", "# #", " ### "],
|
|
42
|
+
"7" => ["#####", " #", " # ", " # ", " # "],
|
|
43
|
+
"8" => [" ### ", "# #", " ### ", "# #", " ### "],
|
|
44
|
+
"9" => [" ### ", "# #", " ####", " #", " ### "],
|
|
45
|
+
"!" => ["#", "#", "#", " ", "#"],
|
|
46
|
+
"?" => ["### ", " #", " ## ", " ", " # "],
|
|
47
|
+
"." => [" ", " ", " ", " ", "#"],
|
|
48
|
+
"," => [" ", " ", " ", " #", "# "],
|
|
49
|
+
":" => [" ", "#", " ", "#", " "],
|
|
50
|
+
"'" => ["#", "#", " ", " ", " "],
|
|
51
|
+
"+" => [" ", " # ", "#####", " # ", " "],
|
|
52
|
+
"-" => [" ", " ", "####", " ", " "],
|
|
53
|
+
"=" => [" ", "####", " ", "####", " "],
|
|
54
|
+
"x" => [" ", "# #", " # ", "# #", " "],
|
|
55
|
+
"/" => [" #", " # ", " # ", " # ", "# "],
|
|
56
|
+
">" => ["# ", " # ", " # ", " # ", "# "],
|
|
57
|
+
"<" => [" #", " # ", " # ", " # ", " #"],
|
|
58
|
+
"^" => [" # ", "# #", " ", " ", " "],
|
|
59
|
+
"#" => [" # # ", "#####", " # # ", "#####", " # # "],
|
|
60
|
+
"%" => ["# #", " # ", " # ", " # ", "# #"],
|
|
61
|
+
"*" => [" ", "# # #", " ### ", "# # #", " "],
|
|
62
|
+
" " => [" ", " ", " ", " ", " "],
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
GLYPHS = RAW.transform_values { |rows| rows.map { |r| r.chars.map { |c| c == "#" } }.freeze }.freeze
|
|
66
|
+
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
def glyph(ch)
|
|
70
|
+
GLYPHS[ch] || GLYPHS[ch.upcase] || GLYPHS["?"]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# => array of HEIGHT rows, each an array of booleans
|
|
74
|
+
def bitmap(text, spacing: 1)
|
|
75
|
+
rows = Array.new(HEIGHT) { [] }
|
|
76
|
+
text.each_char.with_index do |ch, i|
|
|
77
|
+
g = glyph(ch)
|
|
78
|
+
HEIGHT.times do |y|
|
|
79
|
+
rows[y].concat([false] * spacing) if i > 0
|
|
80
|
+
rows[y].concat(g[y])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
rows
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def width(text, scale: 1, spacing: 1)
|
|
87
|
+
return 0 if text.empty?
|
|
88
|
+
(text.each_char.sum { |c| glyph(c)[0].size } + spacing * (text.size - 1)) * scale
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|