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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83be2be298c392a6739e1fdef9293579796f7d80dd3a5b3aaccc65cf34f60d4c
|
|
4
|
+
data.tar.gz: 8afedc6a382a671a5e3dc13720f1934599757cadd6aacb28b88b69cd1986b0ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7aed8c09308468b4e3ed6e992cba5d4245d349c66bf88abfa37a4d6a214a1f457dd4610db944f49e5d53c1e024ebfc4bed7bf82fa81ef319d65e158547139633
|
|
7
|
+
data.tar.gz: 2c6609aeb7f8939fe95c0278592bf45cc67f68eb52d715e3527837074ecea19dda912505d630715e2a5ef760d630b33a3b778f07a39faba81aef5def8fbe1119
|
data/README.md
CHANGED
|
@@ -32,7 +32,8 @@ To try it without installing, run `ruby exe/dopairb` in the checkout.
|
|
|
32
32
|
$ dopairb # start it (IRB options are passed through)
|
|
33
33
|
$ dopairb --calm # quieter: low intensity, no flash, shorter effects
|
|
34
34
|
$ dopairb --max # everything at maximum; the biggest moments go full screen
|
|
35
|
-
$ dopairb --party # max + full-screen flash +
|
|
35
|
+
$ dopairb --party # max + full-screen flash + sound effects
|
|
36
|
+
$ dopairb --sound # synthesized sound effects (--sound=bell for the terminal bell)
|
|
36
37
|
$ dopairb --no-flash # suppress flashing, available from the very first start
|
|
37
38
|
$ DOPAIRB="intensity=low,flash=off" dopairb
|
|
38
39
|
```
|
|
@@ -51,6 +52,7 @@ dopa show settings and session stats
|
|
|
51
52
|
dopa off|low|normal|max
|
|
52
53
|
dopa calm / dopa party
|
|
53
54
|
dopa demo play every effect once (does not change your score)
|
|
55
|
+
dopa loading show the loading screen
|
|
54
56
|
dopa flash=off duration=0.5 ...
|
|
55
57
|
```
|
|
56
58
|
|
|
@@ -61,7 +63,7 @@ dopa flash=off duration=0.5 ...
|
|
|
61
63
|
| `intensity` | off / low / normal / max | normal | Overall amount. `off` behaves exactly like plain IRB |
|
|
62
64
|
| `motion` | on / off | on | `off` disables animation (badges only) |
|
|
63
65
|
| `flash` | off / soft / full | soft | `soft` flashes only the effect area, `full` inverts the whole screen |
|
|
64
|
-
| `sound` |
|
|
66
|
+
| `sound` | off / bell / sfx | off | `sfx` plays synthesized sound effects, `bell` rings the terminal bell on big moments |
|
|
65
67
|
| `hud` | on / off | on | COMBO / SCORE / CHARGE on the right of the prompt line |
|
|
66
68
|
| `duration` | 0.1–5 | 1.0 | Length multiplier for every effect |
|
|
67
69
|
| `color` | auto / truecolor / 256 / 16 / none | auto | `none` when `NO_COLOR` is set |
|
|
@@ -104,10 +106,29 @@ When several fire at once, only the biggest plays. The smaller ones are folded i
|
|
|
104
106
|
| First eval / COMBO 5, 10, 25… / eval count 10, 50, 100… | `FIRST HIT!` / `COMBO 10!` / `100 EVALS` |
|
|
105
107
|
| Numeric record (more than doubled) | `NEW RECORD` |
|
|
106
108
|
| Success right after an error | `FIXED!` (after one error) / `COMEBACK!` (after a streak), both huge |
|
|
107
|
-
| Exit | A
|
|
109
|
+
| Exit | A full-screen finale: the title drops, stats slam in one by one, the total score counts up under a drum roll, a rank is stamped (S/A/B/C with a cheerful title), then fireworks. The result is left behind as a plain table |
|
|
108
110
|
|
|
109
111
|
Error effects name the kind of error. They never make fun of the failure.
|
|
110
112
|
|
|
113
|
+
## Sound
|
|
114
|
+
|
|
115
|
+
With `sound=sfx`, every effect has a sound: key clicks, `NICE!` chimes, a comet ping, a stamp thump, a count-up that ends in a boom, fanfares and explosions for the big moments, a buzz or crack for errors.
|
|
116
|
+
All sounds are synthesized in Ruby at runtime and cached as WAV files in the temp directory. No audio files ship with the gem.
|
|
117
|
+
|
|
118
|
+
They are played by the first player found:
|
|
119
|
+
|
|
120
|
+
- `paplay` (PulseAudio, including WSLg on WSL2)
|
|
121
|
+
- `afplay` (macOS)
|
|
122
|
+
- `aplay` (ALSA)
|
|
123
|
+
- `powershell.exe` on WSL without WSLg
|
|
124
|
+
|
|
125
|
+
The finale's sound is one track synthesized on the same timeline as the animation (a tick and a rising note per stat row, a drum roll, a boom, a fanfare, fireworks) and stretched with `duration`.
|
|
126
|
+
Sounds are rendered once and reused by later sessions. At startup, a Ractor renders the missing ones in parallel with the REPL, so typing is not slowed down (a thread is used where Ractor is unavailable). The startup sound is rendered first. On a start without cached sounds (the first run, or after an upgrade), a loading screen (a cat running along a pastel progress bar) is shown until it is ready. `dopa loading` shows it any time.
|
|
127
|
+
Players run as separate processes, so the REPL never waits for them.
|
|
128
|
+
PowerShell takes a few hundred milliseconds to start, so with it only the result effects make sound, not every keystroke.
|
|
129
|
+
When no player is found, `sfx` falls back to the terminal bell on big moments.
|
|
130
|
+
Sound plays on the machine that runs dopairb, so over ssh you will not hear it.
|
|
131
|
+
|
|
111
132
|
## Readable, never broken
|
|
112
133
|
|
|
113
134
|
- IRB evaluates and prints as usual. `=> value`, exception messages and backtraces come from IRB itself.
|
data/lib/dopairb/canvas.rb
CHANGED
|
@@ -120,39 +120,48 @@ module Dopairb
|
|
|
120
120
|
@bold[y][x] = bold
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
BRAILLE_CHARS = Array.new(256) { |i| (0x2800 + i).chr(Encoding::UTF_8) }.freeze
|
|
124
|
+
|
|
123
125
|
def render_row(y, depth, shake)
|
|
124
126
|
chs = @ch[y]
|
|
127
|
+
fgs = @fg[y]
|
|
128
|
+
bgs = @bg[y]
|
|
129
|
+
bolds = @bold[y]
|
|
130
|
+
dots = @dots[y]
|
|
131
|
+
dotc = @dotc[y]
|
|
132
|
+
utf8 = Term.utf8?
|
|
125
133
|
last = @w - 1
|
|
126
|
-
last -= 1 while last >= 0 && chs[last].nil? &&
|
|
134
|
+
last -= 1 while last >= 0 && chs[last].nil? && dots[last] == 0 && bgs[last].nil?
|
|
127
135
|
out = +""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
out << (" " * shake)
|
|
131
|
-
end
|
|
132
|
-
start = shake < 0 ? -shake : 0
|
|
136
|
+
out << (" " * shake) if shake > 0
|
|
137
|
+
x = shake < 0 ? -shake : 0
|
|
133
138
|
limit = shake > 0 ? [last, @w - 1 - shake].min : last
|
|
134
|
-
|
|
139
|
+
cur = nil
|
|
135
140
|
while x <= limit
|
|
136
141
|
c = chs[x]
|
|
137
142
|
if c == CONT
|
|
138
143
|
x += 1
|
|
139
144
|
next
|
|
140
145
|
end
|
|
141
|
-
fg =
|
|
142
|
-
|
|
146
|
+
fg = fgs[x]
|
|
147
|
+
bg = bgs[x]
|
|
148
|
+
bold = bolds[x]
|
|
143
149
|
if c.nil?
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
d = dots[x]
|
|
151
|
+
if d != 0
|
|
152
|
+
c = utf8 ? BRAILLE_CHARS[d] : "."
|
|
153
|
+
fg = dotc[x]
|
|
147
154
|
bold = false
|
|
148
155
|
else
|
|
149
156
|
c = " "
|
|
157
|
+
fg = nil
|
|
158
|
+
bold = false
|
|
150
159
|
end
|
|
151
160
|
end
|
|
152
|
-
|
|
153
|
-
if
|
|
154
|
-
out << (
|
|
155
|
-
cur =
|
|
161
|
+
key = style_key(fg, bg, bold)
|
|
162
|
+
if key != cur
|
|
163
|
+
out << sgr_for(key, fg, bg, bold, depth)
|
|
164
|
+
cur = key
|
|
156
165
|
end
|
|
157
166
|
out << c
|
|
158
167
|
x += 1
|
|
@@ -160,5 +169,19 @@ module Dopairb
|
|
|
160
169
|
out << "\e[0m" if cur
|
|
161
170
|
out
|
|
162
171
|
end
|
|
172
|
+
|
|
173
|
+
def style_key(fg, bg, bold)
|
|
174
|
+
f = fg ? (fg[0] << 16) | (fg[1] << 8) | fg[2] : 0x1000000
|
|
175
|
+
b = bg ? (bg[0] << 16) | (bg[1] << 8) | bg[2] : 0x1000000
|
|
176
|
+
(f << 26) | (b << 1) | (bold ? 1 : 0)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
SGR_CACHE = Hash.new { |h, d| h[d] = {} }
|
|
180
|
+
|
|
181
|
+
def sgr_for(key, fg, bg, bold, depth)
|
|
182
|
+
cache = SGR_CACHE[depth]
|
|
183
|
+
cache.clear if cache.size > 50_000
|
|
184
|
+
cache[key] ||= (depth == :none && !bold && bg.nil? ? "\e[0m" : Color.sgr(fg, bg, bold, depth)).freeze
|
|
185
|
+
end
|
|
163
186
|
end
|
|
164
187
|
end
|
data/lib/dopairb/cli.rb
CHANGED
|
@@ -18,10 +18,10 @@ module Dopairb
|
|
|
18
18
|
o.on("--intensity=LEVEL", Config::INTENSITIES.map(&:to_s), "off / low / normal (default) / max") { |v| settings << [:intensity, v] }
|
|
19
19
|
o.on("--calm", "low intensity, no flash, shorter effects") { settings.concat([[:intensity, :low], [:flash, :off], [:duration, 0.7]]) }
|
|
20
20
|
o.on("--max", "everything at maximum") { settings << [:intensity, :max] }
|
|
21
|
-
o.on("--party", "max + full-screen flash +
|
|
21
|
+
o.on("--party", "max + full-screen flash + sound effects") { settings.concat([[:intensity, :max], [:flash, :full], [:sound, :sfx]]) }
|
|
22
22
|
o.on("--[no-]flash[=MODE]", "flash: off / soft (default) / full") { |v| settings << [:flash, v.nil? ? :soft : (v == false ? :off : v)] }
|
|
23
23
|
o.on("--[no-]motion", "animations (default on)") { |v| settings << [:motion, v] }
|
|
24
|
-
o.on("--[no-]sound", "
|
|
24
|
+
o.on("--[no-]sound[=MODE]", "sound: off (default) / bell / sfx") { |v| settings << [:sound, v.nil? ? :sfx : (v == false ? :off : v)] }
|
|
25
25
|
o.on("--[no-]hud", "COMBO / SCORE next to the prompt (default on)") { |v| settings << [:hud, v] }
|
|
26
26
|
o.on("--[no-]keys", "per-keystroke effects (default on)") { |v| settings << [:keys, v] }
|
|
27
27
|
o.on("--[no-]intro", "title animation (default on)") { |v| settings << [:intro, v] }
|
data/lib/dopairb/config.rb
CHANGED
|
@@ -7,12 +7,13 @@ module Dopairb
|
|
|
7
7
|
FLASHES = %i[off soft full].freeze
|
|
8
8
|
COLORS = %i[auto truecolor 256 16 none].freeze
|
|
9
9
|
TRAILS = %i[none big all].freeze
|
|
10
|
+
SOUNDS = %i[off bell sfx].freeze
|
|
10
11
|
|
|
11
12
|
DESCRIPTIONS = {
|
|
12
13
|
intensity: "off / low / normal / max -- overall amount of effects",
|
|
13
14
|
motion: "on / off -- animations (off = static badges only)",
|
|
14
15
|
flash: "off / soft / full -- soft = region only, full = whole screen",
|
|
15
|
-
sound: "
|
|
16
|
+
sound: "off / bell / sfx -- sfx = synthesized effects (paplay/aplay/afplay/WSL), bell = terminal bell",
|
|
16
17
|
hud: "on / off -- COMBO / SCORE / CHARGE next to the prompt",
|
|
17
18
|
duration: "float multiplier for every effect length (e.g. 0.5, 2)",
|
|
18
19
|
color: "auto / truecolor / 256 / 16 / none",
|
|
@@ -28,7 +29,7 @@ module Dopairb
|
|
|
28
29
|
@intensity = :normal
|
|
29
30
|
@motion = true
|
|
30
31
|
@flash = :soft
|
|
31
|
-
@sound =
|
|
32
|
+
@sound = :off
|
|
32
33
|
@hud = true
|
|
33
34
|
@duration = 1.0
|
|
34
35
|
@color = :auto
|
|
@@ -56,7 +57,10 @@ module Dopairb
|
|
|
56
57
|
v = bool_or_nil(value)
|
|
57
58
|
@color = v.nil? ? enum(value, COLORS, name) : (v ? :auto : :none)
|
|
58
59
|
when :trail then @trail = enum(value, TRAILS, name)
|
|
59
|
-
when :
|
|
60
|
+
when :sound
|
|
61
|
+
v = bool_or_nil(value)
|
|
62
|
+
@sound = v.nil? ? enum(value, SOUNDS, name) : (v ? :sfx : :off)
|
|
63
|
+
when :motion, :hud, :intro, :keys
|
|
60
64
|
v = bool_or_nil(value)
|
|
61
65
|
raise ArgumentError, "#{name} expects on/off, got #{value.inspect}" if v.nil?
|
|
62
66
|
instance_variable_set(:"@#{name}", v)
|
data/lib/dopairb/demo.rb
CHANGED
|
@@ -36,6 +36,15 @@ module Dopairb
|
|
|
36
36
|
]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Let the sound ring out so the next one does not step on it.
|
|
40
|
+
def wait_for_sound(config, scene, t0)
|
|
41
|
+
return unless config.sound == :sfx && scene&.sfx && Sound.available?
|
|
42
|
+
ends = (scene.pre + scene.impact_at) * config.duration + Sound.tail(scene.sfx)
|
|
43
|
+
wait = ends - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0)
|
|
44
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
|
|
45
|
+
sleep 0.02 while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline && !Term.input_pending?
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
def run(mod)
|
|
40
49
|
d = mod.session&.director || Director.new(mod.config)
|
|
41
50
|
unless mod.active?
|
|
@@ -51,9 +60,11 @@ module Dopairb
|
|
|
51
60
|
if event.kind == :failure && event.info[:type] == :syntax
|
|
52
61
|
event.info[:column] = [code.size, event.info[:column]].min
|
|
53
62
|
end
|
|
54
|
-
|
|
63
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
64
|
+
scene = d.play_event(event, input: shot, pre: event.kind == :success ? mod.config.charge : 0)
|
|
55
65
|
Term.write("\e[2m=> (demo)\e[0m\r\n")
|
|
56
|
-
|
|
66
|
+
wait_for_sound(mod.config, scene, t0)
|
|
67
|
+
sleep 0.4
|
|
57
68
|
end
|
|
58
69
|
Term.write("\e[0m")
|
|
59
70
|
end
|
data/lib/dopairb/director.rb
CHANGED
|
@@ -90,7 +90,19 @@ module Dopairb
|
|
|
90
90
|
return if @config.off?
|
|
91
91
|
c = ctx(input: input, pre: pre)
|
|
92
92
|
c.input = nil if input && input.size + 12 > c.rows
|
|
93
|
-
|
|
93
|
+
scene = scene_for(event, c)
|
|
94
|
+
play(scene)
|
|
95
|
+
scene
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Cute loading screen until ready.call is true (e.g. the intro sound exists).
|
|
99
|
+
def loading(&ready)
|
|
100
|
+
return if @config.off?
|
|
101
|
+
scene = Scenes::Loading.new(ctx, ready)
|
|
102
|
+
return unless @config.motion && fits?(scene)
|
|
103
|
+
run(scene)
|
|
104
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
105
|
+
raise if SystemExit === e || (SignalException === e && !(Interrupt === e))
|
|
94
106
|
end
|
|
95
107
|
|
|
96
108
|
def intro
|
|
@@ -100,14 +112,22 @@ module Dopairb
|
|
|
100
112
|
|
|
101
113
|
def outro(stats)
|
|
102
114
|
return if @config.off?
|
|
103
|
-
|
|
115
|
+
c = ctx
|
|
116
|
+
scene = if @config.motion && Scenes::Finale.fits?(c.rows, c.w + 1) && @config.level >= 2
|
|
117
|
+
Scenes::Finale.new(c, stats)
|
|
118
|
+
else
|
|
119
|
+
Scenes::Outro.new(c, stats)
|
|
120
|
+
end
|
|
121
|
+
# render the synced track now so it does not start late
|
|
122
|
+
Sound.file(scene.sfx, @config.duration) if @config.sound == :sfx && scene.sfx && Sound.available?
|
|
123
|
+
play(scene, force_trail: true)
|
|
104
124
|
end
|
|
105
125
|
|
|
106
126
|
def play(scene, force_trail: false)
|
|
107
127
|
animated = @config.motion && scene.ctx.w >= 20 && fits?(scene)
|
|
108
128
|
animated = false if animated && Term.input_pending?
|
|
129
|
+
sound(scene, animated) unless !animated && @config.motion
|
|
109
130
|
run(scene) if animated
|
|
110
|
-
Term.bell if @config.sound && scene.bell? && animated
|
|
111
131
|
emit_trail(scene, force_trail)
|
|
112
132
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
113
133
|
raise if SystemExit === e || (SignalException === e && !(Interrupt === e))
|
|
@@ -116,6 +136,20 @@ module Dopairb
|
|
|
116
136
|
|
|
117
137
|
private
|
|
118
138
|
|
|
139
|
+
def sound(scene, animated)
|
|
140
|
+
case @config.sound
|
|
141
|
+
when :bell
|
|
142
|
+
Term.bell if scene.bell?
|
|
143
|
+
when :sfx
|
|
144
|
+
if Sound.available?
|
|
145
|
+
delay = animated ? (scene.pre + scene.impact_at) * @config.duration : 0.0
|
|
146
|
+
Sound.play(scene.sfx, delay: delay, stretch: @config.duration) if scene.sfx
|
|
147
|
+
elsif scene.bell?
|
|
148
|
+
Term.bell
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
119
153
|
def event_evals(_event)
|
|
120
154
|
Dopairb.session&.game&.evals.to_i
|
|
121
155
|
end
|
|
@@ -126,8 +160,7 @@ module Dopairb
|
|
|
126
160
|
|
|
127
161
|
def fits?(scene)
|
|
128
162
|
top = scene.top? ? scene.ctx.input.size : 0
|
|
129
|
-
|
|
130
|
-
alt || scene.height + top <= scene.ctx.rows - 1
|
|
163
|
+
scene.alt? || scene.height + top <= scene.ctx.rows - 1
|
|
131
164
|
end
|
|
132
165
|
|
|
133
166
|
def emit_trail(scene, force)
|
|
@@ -140,7 +173,7 @@ module Dopairb
|
|
|
140
173
|
end
|
|
141
174
|
|
|
142
175
|
def run(scene)
|
|
143
|
-
alt = scene.
|
|
176
|
+
alt = scene.alt?
|
|
144
177
|
top = !alt && scene.top? ? scene.ctx.input.rows : []
|
|
145
178
|
stage = alt ? AltStage.new(scene.height) : Stage.new(scene.height, top: top)
|
|
146
179
|
depth = scene.depth
|
|
@@ -153,7 +186,7 @@ module Dopairb
|
|
|
153
186
|
loop do
|
|
154
187
|
break if Term.input_pending? || !stage.open?
|
|
155
188
|
t = (now - t0) / @config.duration
|
|
156
|
-
break if t >= scene.total_length
|
|
189
|
+
break if t >= scene.total_length || scene.done?(t)
|
|
157
190
|
lines = []
|
|
158
191
|
unless top.empty?
|
|
159
192
|
tc = Canvas.new(scene.ctx.w, top.size)
|
data/lib/dopairb/game.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Dopairb
|
|
|
16
16
|
|
|
17
17
|
attr_reader :score, :combo, :max_combo, :evals, :successes, :failures, :interrupts,
|
|
18
18
|
:charge, :error_streak, :record, :typing_combo, :started_at, :last_key_at,
|
|
19
|
-
:keystrokes, :best_typing
|
|
19
|
+
:keystrokes, :best_typing, :comebacks
|
|
20
20
|
|
|
21
21
|
def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
22
22
|
@clock = clock
|
|
@@ -38,6 +38,7 @@ module Dopairb
|
|
|
38
38
|
@same_char = 0
|
|
39
39
|
@typing_points = 0
|
|
40
40
|
@keystrokes = 0
|
|
41
|
+
@comebacks = 0
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def now = @clock.call
|
|
@@ -106,7 +107,10 @@ module Dopairb
|
|
|
106
107
|
@successes += 1
|
|
107
108
|
flags = []
|
|
108
109
|
flags << :first_hit if @successes == 1
|
|
109
|
-
|
|
110
|
+
if @error_streak > 0
|
|
111
|
+
flags << :comeback
|
|
112
|
+
@comebacks += 1
|
|
113
|
+
end
|
|
110
114
|
streak = @error_streak
|
|
111
115
|
@error_streak = 0
|
|
112
116
|
@combo += 1
|
|
@@ -166,7 +170,7 @@ module Dopairb
|
|
|
166
170
|
|
|
167
171
|
def stats
|
|
168
172
|
{ evals: @evals, successes: @successes, failures: @failures, interrupts: @interrupts,
|
|
169
|
-
max_combo: @max_combo, score: @score, keystrokes: @keystrokes, best_typing: @best_typing,
|
|
173
|
+
max_combo: @max_combo, score: @score, comebacks: @comebacks, keystrokes: @keystrokes, best_typing: @best_typing,
|
|
170
174
|
time: elapsed }
|
|
171
175
|
end
|
|
172
176
|
|
data/lib/dopairb/input_fx.rb
CHANGED
|
@@ -25,8 +25,9 @@ module Dopairb
|
|
|
25
25
|
|
|
26
26
|
attr_reader :game
|
|
27
27
|
|
|
28
|
-
def initialize(config, game, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, rng: Random.new)
|
|
28
|
+
def initialize(config, game, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, rng: Random.new, sound: nil)
|
|
29
29
|
@config = config
|
|
30
|
+
@sound = sound || ->(name) { Sound.play(name) if @config.sound == :sfx && Sound.fast? }
|
|
30
31
|
@game = game
|
|
31
32
|
@clock = clock
|
|
32
33
|
@rng = rng
|
|
@@ -304,7 +305,9 @@ module Dopairb
|
|
|
304
305
|
@highlights << Highlight.new(off, off + ch.bytesize, t, 0.18, :flash)
|
|
305
306
|
end
|
|
306
307
|
spark(cur_x, t, full? ? 4 : 1)
|
|
308
|
+
sfx(@game.typing_combo >= 20 ? :key_hot : :key)
|
|
307
309
|
if resumed == :resume
|
|
310
|
+
sfx(:charge)
|
|
308
311
|
pop("CHARGE!", cur_x - 3, t, Color::FIRE, life: 0.7, rise: true, prio: 3)
|
|
309
312
|
@parts.burst(cur_x, 0.0, 26, at: t, speed: 34, life: 0.55, palette: Color::FIRE, gravity: 10) if full?
|
|
310
313
|
elsif recovering
|
|
@@ -328,6 +331,7 @@ module Dopairb
|
|
|
328
331
|
@highlights << Highlight.new(off, off + 1, t, 0.6, :pair)
|
|
329
332
|
@highlights.reject! { |h| h.kind == :flash }
|
|
330
333
|
pop("NICE!", cur_x - 3, t, Color::GOLD, life: 0.6, rise: true, prio: 2)
|
|
334
|
+
sfx(:nice)
|
|
331
335
|
@parts.burst(cur_x - 1, 0.0, 12, at: t, speed: 26, life: 0.4, palette: Color::GOLD, gravity: 8) if full?
|
|
332
336
|
elsif (range = match_string(tokens, off))
|
|
333
337
|
@game.key(:string)
|
|
@@ -340,6 +344,7 @@ module Dopairb
|
|
|
340
344
|
@game.key(:block)
|
|
341
345
|
@highlights << Highlight.new(start, off + 1, t, 0.55, :range)
|
|
342
346
|
pop("SEALED!", cur_x - 4, t, Color::NEON, life: 0.7, rise: true, prio: 2)
|
|
347
|
+
sfx(:sealed)
|
|
343
348
|
@parts.burst(cur_x - 2, 0.0, 16, at: t, speed: 30, life: 0.5, palette: Color::NEON, gravity: 8) if full?
|
|
344
349
|
end
|
|
345
350
|
end
|
|
@@ -413,6 +418,7 @@ module Dopairb
|
|
|
413
418
|
@last_kind = :delete
|
|
414
419
|
@highlights.clear
|
|
415
420
|
return unless lively?
|
|
421
|
+
sfx(:delete)
|
|
416
422
|
glyph = removed.gsub(/\s/, "").chars.last || "."
|
|
417
423
|
glyph = "." unless Term.char_width(glyph) == 1
|
|
418
424
|
@parts.add(cur_x, 0.0, (@rng.rand - 0.5) * 6, 3, at: t, life: 0.45, palette: [[255, 255, 255], [255, 120, 120], [120, 40, 40]], glyph: glyph, gravity: 16)
|
|
@@ -443,6 +449,7 @@ module Dopairb
|
|
|
443
449
|
@highlights.clear
|
|
444
450
|
@highlights << Highlight.new(0, new.bytesize, t, 0.35, :reveal) if new.size < 4000
|
|
445
451
|
pop("<< REWIND", cur_x - 10, t, Color::ICE, life: 0.5)
|
|
452
|
+
sfx(:rewind)
|
|
446
453
|
end
|
|
447
454
|
|
|
448
455
|
def completed(off, added, cur_x, t)
|
|
@@ -452,6 +459,7 @@ module Dopairb
|
|
|
452
459
|
@highlights.reject! { |h| h.kind == :flash }
|
|
453
460
|
@highlights << Highlight.new(off, off + added.bytesize, t, 0.5, :lock)
|
|
454
461
|
@inline = Popup.new("<< LOCK ON", cur_x + 1, t, 0.6 * @config.duration, Color::ICE, false, 2)
|
|
462
|
+
sfx(:lock)
|
|
455
463
|
@parts.burst(cur_x, 0.2, 10, at: t, speed: 28, life: 0.3, palette: Color::ICE, gravity: 0) if full?
|
|
456
464
|
end
|
|
457
465
|
|
|
@@ -463,6 +471,12 @@ module Dopairb
|
|
|
463
471
|
pop("PASTE x#{n}", (cur_x || 0) - 6, t, Color::NEON, life: 0.8, rise: true) if lively?
|
|
464
472
|
end
|
|
465
473
|
|
|
474
|
+
def sfx(name)
|
|
475
|
+
@sound.call(name)
|
|
476
|
+
rescue StandardError => e
|
|
477
|
+
Dopairb.debug(e)
|
|
478
|
+
end
|
|
479
|
+
|
|
466
480
|
def spark(cur_x, t, n)
|
|
467
481
|
return if n <= 0
|
|
468
482
|
@parts.burst(cur_x - 0.5, -0.3, n, at: t, speed: 14, life: 0.35, palette: Color::FIRE, gravity: 20, angle: Math::PI / 2, arc: Math::PI * 1.2)
|
data/lib/dopairb/output_tap.rb
CHANGED
|
@@ -45,8 +45,18 @@ module Dopairb
|
|
|
45
45
|
nil
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Our own helper processes (sound players) do not count as output.
|
|
49
|
+
def quietly
|
|
50
|
+
prev = Thread.current[:dopairb_quiet]
|
|
51
|
+
Thread.current[:dopairb_quiet] = true
|
|
52
|
+
yield
|
|
53
|
+
ensure
|
|
54
|
+
Thread.current[:dopairb_quiet] = prev
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
# A child process may write straight to the tty: stop decorating.
|
|
49
58
|
def spawned!
|
|
59
|
+
return if Thread.current[:dopairb_quiet]
|
|
50
60
|
@spawned = true if @counting
|
|
51
61
|
Term::LOCK.synchronize { Term.yield_to_output } if Term.overlay
|
|
52
62
|
end
|
data/lib/dopairb/scene.rb
CHANGED
|
@@ -67,15 +67,22 @@ module Dopairb
|
|
|
67
67
|
def length = 0.4
|
|
68
68
|
def top? = false
|
|
69
69
|
def big? = false
|
|
70
|
+
# Played on the alternate screen (full screen, scrollback untouched).
|
|
71
|
+
def alt? = false
|
|
70
72
|
def mega? = false
|
|
71
73
|
def bell? = mega?
|
|
72
74
|
def trail = nil
|
|
73
75
|
def shake(_t) = 0
|
|
74
76
|
def flash_at = nil
|
|
77
|
+
# Sound patch name and when (scene time) its impact should be heard.
|
|
78
|
+
def sfx = nil
|
|
79
|
+
def impact_at = flash_at || 0.0
|
|
75
80
|
|
|
76
81
|
# Wind-up before the main animation (input row glow); 0 when not wanted.
|
|
77
82
|
def pre = top? ? ctx.pre.to_f : 0.0
|
|
78
83
|
def total_length = pre + length
|
|
84
|
+
# Scenes that wait on something can end early.
|
|
85
|
+
def done?(_t) = false
|
|
79
86
|
|
|
80
87
|
# Top rows (the input) as canvas, or nil to leave them untouched.
|
|
81
88
|
def draw_top(c, t)
|
|
@@ -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)
|
|
@@ -84,6 +85,7 @@ module Dopairb
|
|
|
84
85
|
# SyntaxError: the input line cracks at the error position.
|
|
85
86
|
class Crack < FailureScene
|
|
86
87
|
def title = "SYNTAX BREAK"
|
|
88
|
+
def sfx = :crack
|
|
87
89
|
|
|
88
90
|
def setup
|
|
89
91
|
super
|
|
@@ -174,6 +176,8 @@ module Dopairb
|
|
|
174
176
|
# NoMethodError: the link between receiver and method snaps.
|
|
175
177
|
class Snap < FailureScene
|
|
176
178
|
def title = "NO METHOD"
|
|
179
|
+
def sfx = :snap
|
|
180
|
+
def impact_at = 0.28
|
|
177
181
|
def palette = Color::NEON
|
|
178
182
|
def height = 4
|
|
179
183
|
def length = 0.9
|
|
@@ -231,6 +235,7 @@ module Dopairb
|
|
|
231
235
|
def title = event.info[:type] == :argument ? "ARGUMENT CLASH" : "TYPE CLASH"
|
|
232
236
|
def palette = Color::FIRE
|
|
233
237
|
def length = 0.95
|
|
238
|
+
def sfx = :crack
|
|
234
239
|
|
|
235
240
|
def setup
|
|
236
241
|
super
|
|
@@ -280,6 +285,7 @@ module Dopairb
|
|
|
280
285
|
|
|
281
286
|
# Ctrl-C: the collected charge scatters and fades.
|
|
282
287
|
class Scatter < Scene
|
|
288
|
+
def sfx = :interrupt
|
|
283
289
|
def height = 2
|
|
284
290
|
def length = 0.65
|
|
285
291
|
def big? = true
|