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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbc57c42e0e25caed6e783bf761e9edda9822c1d853fa4d55c144ce6704a875e
|
|
4
|
+
data.tar.gz: a3d81e85abe05052cbbf99907162215b4d27796fd0ed14827d83190c0252672e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9522cb64cb142280f6ed6070e0e92884206fb0a7eccd1d289202c64ed9cb28a2cbb14d291a686362ce5797654910dec25f91ab2383193e82782cbd9bcceb7943
|
|
7
|
+
data.tar.gz: 6cae99bcfcdcf7419a2e943fda49cbb97f8603a7e7a09d17f23bf5beb165b8ab85e6cea0f99c6d20f0daa6304dcb13341c5ef9f2781e635642003a4e6dd4f11f
|
data/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
An interactive Ruby shell that answers every keystroke, evaluation, result and exception with loud terminal effects.
|
|
4
4
|
It is built as an IRB extension. The design spec is [spec.md](spec.md) (in Japanese).
|
|
5
5
|
|
|
6
|
+
https://github.com/user-attachments/assets/b45e4a97-ffde-4fb2-be73-ef69d5c8af42
|
|
7
|
+
|
|
8
|
+
A 70-second session with sound (`dopairb`, the defaults); also in [docs/dopairb-demo.mp4](docs/dopairb-demo.mp4). The scripts that recorded it are in [docs/recording](docs/recording).
|
|
9
|
+
|
|
6
10
|
```
|
|
7
11
|
dopairb(main):001> (1..100).sum COMBO 00 SCORE 33 [⣿⣿⣀⣀⣀⣀⣀⣀] x1.5
|
|
8
12
|
⠂⠌⠟⠃ <- sparks, afterglow and HUD on every keystroke
|
|
@@ -29,21 +33,29 @@ To try it without installing, run `ruby exe/dopairb` in the checkout.
|
|
|
29
33
|
## Usage
|
|
30
34
|
|
|
31
35
|
```console
|
|
32
|
-
$ dopairb # start it (IRB options are passed through)
|
|
36
|
+
$ dopairb # start it, at full blast with sound (IRB options are passed through)
|
|
33
37
|
$ dopairb --calm # quieter: low intensity, no flash, shorter effects
|
|
34
|
-
$ dopairb --
|
|
35
|
-
$ dopairb --
|
|
38
|
+
$ dopairb --party # max + full-screen flash + sound effects
|
|
39
|
+
$ dopairb --intensity=normal --no-sound # the tamer pre-0.4.0 defaults
|
|
36
40
|
$ dopairb --no-flash # suppress flashing, available from the very first start
|
|
37
41
|
$ DOPAIRB="intensity=low,flash=off" dopairb
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
To
|
|
44
|
+
To turn your regular `irb` into dopairb, add this to `~/.irbrc`:
|
|
41
45
|
|
|
42
46
|
```ruby
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
begin
|
|
48
|
+
require "dopairb"
|
|
49
|
+
Dopairb.enable # also e.g. Dopairb.enable(sound: :off, intensity: :normal)
|
|
50
|
+
rescue LoadError
|
|
51
|
+
# a Ruby without dopairb keeps plain irb
|
|
52
|
+
end
|
|
45
53
|
```
|
|
46
54
|
|
|
55
|
+
The title (and the loading screen on a cold start) plays right before the first prompt, as with the `dopairb` command.
|
|
56
|
+
Sessions opened by `binding.irb` get the effects too; `Dopairb.enable(binding_irb: false)` keeps them plain.
|
|
57
|
+
Settings for a single run (`dopairb` options and `$DOPAIRB`) win over the ones given to `Dopairb.enable` in `.irbrc`.
|
|
58
|
+
|
|
47
59
|
Inside a session, the `dopa` command changes settings:
|
|
48
60
|
|
|
49
61
|
```
|
|
@@ -51,6 +63,9 @@ dopa show settings and session stats
|
|
|
51
63
|
dopa off|low|normal|max
|
|
52
64
|
dopa calm / dopa party
|
|
53
65
|
dopa demo play every effect once (does not change your score)
|
|
66
|
+
dopa loading show the loading screen
|
|
67
|
+
dopa gallery the bonus art you have collected (`dopa gallery starry_night` shows one)
|
|
68
|
+
dopa gallery tour a full-screen slideshow of all 100; uncollected ones appear as silhouettes
|
|
54
69
|
dopa flash=off duration=0.5 ...
|
|
55
70
|
```
|
|
56
71
|
|
|
@@ -58,10 +73,10 @@ dopa flash=off duration=0.5 ...
|
|
|
58
73
|
|
|
59
74
|
| Setting | Values | Default | Meaning |
|
|
60
75
|
| --- | --- | --- | --- |
|
|
61
|
-
| `intensity` | off / low / normal / max |
|
|
76
|
+
| `intensity` | off / low / normal / max | max | Overall amount. `off` behaves exactly like plain IRB |
|
|
62
77
|
| `motion` | on / off | on | `off` disables animation (badges only) |
|
|
63
78
|
| `flash` | off / soft / full | soft | `soft` flashes only the effect area, `full` inverts the whole screen |
|
|
64
|
-
| `sound` |
|
|
79
|
+
| `sound` | off / bell / sfx | sfx | `sfx` plays synthesized sound effects, `bell` rings the terminal bell on big moments |
|
|
65
80
|
| `hud` | on / off | on | COMBO / SCORE / CHARGE on the right of the prompt line |
|
|
66
81
|
| `duration` | 0.1–5 | 1.0 | Length multiplier for every effect |
|
|
67
82
|
| `color` | auto / truecolor / 256 / 16 / none | auto | `none` when `NO_COLOR` is set |
|
|
@@ -69,6 +84,7 @@ dopa flash=off duration=0.5 ...
|
|
|
69
84
|
| `charge` | 0–0.3 s | 0.12 | Wind-up before a fast result lands; 0 disables it |
|
|
70
85
|
| `intro` | on / off | on | Title animation at startup |
|
|
71
86
|
| `keys` | on / off | on | Per-keystroke effects |
|
|
87
|
+
| `binding_irb` | on / off | on | Effects in sessions opened by `binding.irb` too |
|
|
72
88
|
|
|
73
89
|
## Effects
|
|
74
90
|
|
|
@@ -78,6 +94,7 @@ When several fire at once, only the biggest plays. The smaller ones are folded i
|
|
|
78
94
|
| Moment | Effect |
|
|
79
95
|
| --- | --- |
|
|
80
96
|
| Typing | A spark trail right of the cursor, a brief glow on the typed character, embers falling below, a x1.0–x3.0 multiplier |
|
|
97
|
+
| Typing streak of 8, 16, 32 … 1024 keys | Powers of two, in big half-block letters under the input: `8-BIT RUSH!`, `16-BIT COMBO!`, `32-BIT BLAZE!!`, `64-BIT OVERDRIVE!!`, `128-BIT HYPER MODE!!`, `0x100 OVERFLOW!!!`, `2^9 GODSPEED!!!`, `1KiB TYPING LEGEND!!!`, with a bonus of 4 points per key |
|
|
81
98
|
| Resuming after a pause | Reignites with `CHARGE!` |
|
|
82
99
|
| Backspace / Delete | The deleted character shatters and falls. The next keystroke shows `RECOVERY` |
|
|
83
100
|
| Cursor movement | A light streak in the direction of travel |
|
|
@@ -102,12 +119,48 @@ When several fire at once, only the biggest plays. The smaller ones are folded i
|
|
|
102
119
|
| Other exceptions | A red flash and a glitching `FAILED` |
|
|
103
120
|
| Ctrl-C | The stored charge scatters, `INTERRUPTED` |
|
|
104
121
|
| First eval / COMBO 5, 10, 25… / eval count 10, 50, 100… | `FIRST HIT!` / `COMBO 10!` / `100 EVALS` |
|
|
122
|
+
| Lucky success (about 1 in 10, up to 1 in 5 with a full CHARGE) | `CRITICAL!!`: a slash, a red flash and x2 / x4 / x8 points |
|
|
123
|
+
| Very lucky success (1 in 67) | `JACKPOT!!`: three slot reels stop on 7-7-7, coins rain, x16 points |
|
|
124
|
+
| COMBO 10 and beyond | `FEVER TIME!`: every point counts double until the next error; the HUD glows in rainbow `FEVER!` |
|
|
125
|
+
| Beating your all-time best combo | `BEST COMBO!` |
|
|
126
|
+
| Level up, jackpot, or a lucky drop (see [Career](#career)) | `LEVEL UP!` / `JACKPOT PRIZE` / `ART DROP!`, then a famous painting is unveiled full screen as bonus art |
|
|
105
127
|
| Numeric record (more than doubled) | `NEW RECORD` |
|
|
106
128
|
| Success right after an error | `FIXED!` (after one error) / `COMEBACK!` (after a streak), both huge |
|
|
107
|
-
| Exit | A
|
|
129
|
+
| 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
130
|
|
|
109
131
|
Error effects name the kind of error. They never make fun of the failure.
|
|
110
132
|
|
|
133
|
+
## Career
|
|
134
|
+
|
|
135
|
+
Between sessions dopairb keeps a small profile: lifetime XP (the sum of your scores), level, best score, best combo, a day streak and the bonus art you have collected.
|
|
136
|
+
Levels need powers of two: LV 2 at 512 XP, LV 3 at 1,024, LV 4 at 2,048, and so on.
|
|
137
|
+
Each level-up and each jackpot unveils a masterpiece full screen, and now and then (about 1 success in 50) one simply drops: `ART DROP!`. There are 100 public-domain works to collect, from the Lascaux horses and Hokusai's *Great Wave* through Vermeer, Turner and van Gogh to Klimt, Kandinsky and Mondrian, all painted procedurally in half blocks (no image files ship with the gem). New ones come first; `dopa gallery` lists yours. (Spoilers: all 100 are in [docs/gallery.png](docs/gallery.png).)
|
|
138
|
+
The startup title shows your level and day streak, and the finale shows `NEW BEST!` and an XP bar.
|
|
139
|
+
|
|
140
|
+
The profile is `~/.local/state/dopairb/profile.json` (`$XDG_STATE_HOME` is honored). Set `DOPAIRB_PROFILE=path` to use another file, or `DOPAIRB_PROFILE=off` to keep nothing; levels are then not shown.
|
|
141
|
+
Non-interactive runs never touch it.
|
|
142
|
+
|
|
143
|
+
`DOPAIRB_SEED=n` makes the luck (critical hits, jackpots, particles) repeatable, which helps when recording a demo.
|
|
144
|
+
|
|
145
|
+
## Sound
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
All sounds are synthesized in Ruby at runtime and cached as WAV files in the temp directory. No audio files ship with the gem.
|
|
149
|
+
|
|
150
|
+
They are played by the first player found:
|
|
151
|
+
|
|
152
|
+
- `paplay` (PulseAudio, including WSLg on WSL2)
|
|
153
|
+
- `afplay` (macOS)
|
|
154
|
+
- `aplay` (ALSA)
|
|
155
|
+
- `powershell.exe` on WSL without WSLg
|
|
156
|
+
|
|
157
|
+
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`.
|
|
158
|
+
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.
|
|
159
|
+
Players run as separate processes, so the REPL never waits for them.
|
|
160
|
+
PowerShell takes a few hundred milliseconds to start, so with it only the result effects make sound, not every keystroke.
|
|
161
|
+
When no player is found, `sfx` falls back to the terminal bell on big moments.
|
|
162
|
+
Sound plays on the machine that runs dopairb, so over ssh you will not hear it.
|
|
163
|
+
|
|
111
164
|
## Readable, never broken
|
|
112
165
|
|
|
113
166
|
- IRB evaluates and prints as usual. `=> value`, exception messages and backtraces come from IRB itself.
|
|
@@ -151,7 +204,7 @@ The rest is terminal-independent logic:
|
|
|
151
204
|
- Scores live for the session only and are not saved.
|
|
152
205
|
- Effects use Unicode. Any glyph that is not one column wide is swapped for ASCII or braille at runtime.
|
|
153
206
|
- Start it with the `dopairb` command, or call `Dopairb.enable` in `.irbrc`.
|
|
154
|
-
- IRB sessions entered via `binding.irb` get the same Context hook. The result screen appears only when the outermost session ends.
|
|
207
|
+
- IRB sessions entered via `binding.irb` get the same Context hook (unless `binding_irb=off`; detected with `IRB::Context#from_binding?`). The result screen appears only when the outermost session ends.
|
|
155
208
|
|
|
156
209
|
## Performance
|
|
157
210
|
|
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
|
@@ -15,13 +15,13 @@ module Dopairb
|
|
|
15
15
|
o.separator ""
|
|
16
16
|
o.separator "IRB with fireworks. Every other option is handed to IRB."
|
|
17
17
|
o.separator ""
|
|
18
|
-
o.on("--intensity=LEVEL", Config::INTENSITIES.map(&:to_s), "off / low / normal (default)
|
|
18
|
+
o.on("--intensity=LEVEL", Config::INTENSITIES.map(&:to_s), "off / low / normal / max (default)") { |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
|
-
o.on("--max", "everything at maximum") { settings << [:intensity, :max] }
|
|
21
|
-
o.on("--party", "max + full-screen flash +
|
|
20
|
+
o.on("--max", "everything at maximum (the default)") { settings << [:intensity, :max] }
|
|
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 / bell / sfx (default)") { |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] }
|
|
@@ -61,9 +61,7 @@ module Dopairb
|
|
|
61
61
|
ours, irb_args = split_args(argv.dup)
|
|
62
62
|
settings = []
|
|
63
63
|
parser(settings).parse!(ours)
|
|
64
|
-
settings.each
|
|
65
|
-
k == :string ? Dopairb.config.apply_string(v) : Dopairb.config.set(k, v)
|
|
66
|
-
end
|
|
64
|
+
settings.each { |k, v| Dopairb.override(k, v) }
|
|
67
65
|
run_irb(irb_args)
|
|
68
66
|
rescue OptionParser::ParseError, ArgumentError => e
|
|
69
67
|
warn "dopairb: #{e.message}"
|
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",
|
|
@@ -20,15 +21,16 @@ module Dopairb
|
|
|
20
21
|
charge: "seconds (max 0.3) of wind-up before a fast result lands; 0 disables",
|
|
21
22
|
intro: "on / off -- title animation at startup",
|
|
22
23
|
keys: "on / off -- per-keystroke effects while typing",
|
|
24
|
+
binding_irb: "on / off -- effects in sessions opened by binding.irb too",
|
|
23
25
|
}.freeze
|
|
24
26
|
|
|
25
|
-
attr_reader :intensity, :motion, :flash, :sound, :hud, :duration, :color, :trail, :charge, :intro, :keys
|
|
27
|
+
attr_reader :intensity, :motion, :flash, :sound, :hud, :duration, :color, :trail, :charge, :intro, :keys, :binding_irb
|
|
26
28
|
|
|
27
29
|
def initialize
|
|
28
|
-
@intensity = :
|
|
30
|
+
@intensity = :max
|
|
29
31
|
@motion = true
|
|
30
32
|
@flash = :soft
|
|
31
|
-
@sound =
|
|
33
|
+
@sound = :sfx
|
|
32
34
|
@hud = true
|
|
33
35
|
@duration = 1.0
|
|
34
36
|
@color = :auto
|
|
@@ -36,6 +38,7 @@ module Dopairb
|
|
|
36
38
|
@charge = 0.12
|
|
37
39
|
@intro = true
|
|
38
40
|
@keys = true
|
|
41
|
+
@binding_irb = true
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def level
|
|
@@ -56,7 +59,10 @@ module Dopairb
|
|
|
56
59
|
v = bool_or_nil(value)
|
|
57
60
|
@color = v.nil? ? enum(value, COLORS, name) : (v ? :auto : :none)
|
|
58
61
|
when :trail then @trail = enum(value, TRAILS, name)
|
|
59
|
-
when :
|
|
62
|
+
when :sound
|
|
63
|
+
v = bool_or_nil(value)
|
|
64
|
+
@sound = v.nil? ? enum(value, SOUNDS, name) : (v ? :sfx : :off)
|
|
65
|
+
when :motion, :hud, :intro, :keys, :binding_irb
|
|
60
66
|
v = bool_or_nil(value)
|
|
61
67
|
raise ArgumentError, "#{name} expects on/off, got #{value.inspect}" if v.nil?
|
|
62
68
|
instance_variable_set(:"@#{name}", v)
|
data/lib/dopairb/demo.rb
CHANGED
|
@@ -7,8 +7,9 @@ module Dopairb
|
|
|
7
7
|
|
|
8
8
|
module_function
|
|
9
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
|
|
10
|
+
def ev(kind, info, flags: [], gain: 120, combo: 3, streak: 0, mult: 1, level: 5)
|
|
11
|
+
E.new(kind: kind, tier: nil, info: info, flags: flags, gain: gain, combo: combo, streak: streak, score: 12_480,
|
|
12
|
+
mult: mult, level: level)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def cases
|
|
@@ -33,9 +34,24 @@ module Dopairb
|
|
|
33
34
|
["1 + 1", ev(:success, { type: :integer, value: 2 }, flags: [:comeback], combo: 1, streak: 5, gain: 600)],
|
|
34
35
|
["loop {}", ev(:interrupt, { charge: 60 })],
|
|
35
36
|
["2**128", ev(:success, { type: :integer, value: 2**128 }, flags: [:new_record, :record_jump], gain: 420)],
|
|
37
|
+
["[3, 1, 2].sort", ev(:success, { type: :array, size: 3 }, flags: [:critical], gain: 960, mult: 8)],
|
|
38
|
+
["12.fdiv(5)", ev(:success, { type: :float, value: 2.4 }, flags: [:fever], combo: 12, gain: 1100, mult: 2)],
|
|
39
|
+
["21.0 * 2", ev(:success, { type: :float, value: 42.0 }, flags: [:fever, :combo_best], combo: 13, gain: 1200, mult: 2)],
|
|
40
|
+
["rand(777)", ev(:success, { type: :integer, value: 777 }, flags: [:jackpot], gain: 6_400, mult: 16)],
|
|
41
|
+
["'level' * 2", ev(:success, { type: :string, length: 10, head: "levellevel", art: :great_wave, art_new: true, gallery: [1, 5] },
|
|
42
|
+
flags: [:level_up], gain: 200, level: 6)],
|
|
36
43
|
]
|
|
37
44
|
end
|
|
38
45
|
|
|
46
|
+
# Let the sound ring out so the next one does not step on it.
|
|
47
|
+
def wait_for_sound(config, scene, t0)
|
|
48
|
+
return unless config.sound == :sfx && scene&.sfx && Sound.available?
|
|
49
|
+
ends = (scene.pre + scene.impact_at) * config.duration + Sound.tail(scene.sfx)
|
|
50
|
+
wait = ends - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0)
|
|
51
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
|
|
52
|
+
sleep 0.02 while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline && !Term.input_pending?
|
|
53
|
+
end
|
|
54
|
+
|
|
39
55
|
def run(mod)
|
|
40
56
|
d = mod.session&.director || Director.new(mod.config)
|
|
41
57
|
unless mod.active?
|
|
@@ -51,9 +67,11 @@ module Dopairb
|
|
|
51
67
|
if event.kind == :failure && event.info[:type] == :syntax
|
|
52
68
|
event.info[:column] = [code.size, event.info[:column]].min
|
|
53
69
|
end
|
|
54
|
-
|
|
70
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
71
|
+
scene = d.play_event(event, input: shot, pre: event.kind == :success ? mod.config.charge : 0)
|
|
55
72
|
Term.write("\e[2m=> (demo)\e[0m\r\n")
|
|
56
|
-
|
|
73
|
+
wait_for_sound(mod.config, scene, t0)
|
|
74
|
+
sleep 0.4
|
|
57
75
|
end
|
|
58
76
|
Term.write("\e[0m")
|
|
59
77
|
end
|
data/lib/dopairb/director.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Dopairb
|
|
|
6
6
|
class Director
|
|
7
7
|
FPS = 40.0
|
|
8
8
|
|
|
9
|
-
attr_reader :config
|
|
9
|
+
attr_reader :config, :rng
|
|
10
10
|
|
|
11
11
|
def initialize(config, rng: Random.new)
|
|
12
12
|
@config = config
|
|
@@ -50,20 +50,35 @@ module Dopairb
|
|
|
50
50
|
Scenes::Banner.new(ctx, event, text: text, palette: palette, mega: true, rainbow: rainbow, sub: sub)
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
|
-
|
|
53
|
+
full = lambda { self.ctx(rows: ctx.rows, cols: ctx.w + 1).tap { |c| c.depth = ctx.depth } }
|
|
54
|
+
if event.flag?(:jackpot)
|
|
55
|
+
Scenes::Jackpot.new(ctx, event)
|
|
56
|
+
elsif event.flag?(:level_up) || (event.flag?(:art_drop) && info[:art])
|
|
57
|
+
if @config.motion && @config.level >= 2 && info[:art] && Scenes::Masterpiece.fits?(ctx.rows, ctx.w + 1)
|
|
58
|
+
Scenes::Masterpiece.new(full.(), event)
|
|
59
|
+
elsif event.flag?(:level_up)
|
|
60
|
+
mega.("LEVEL UP!", Color::NEON, rainbow: true, sub: "LV #{event.level}")
|
|
61
|
+
else
|
|
62
|
+
Scenes::Banner.new(ctx, event, text: "ART DROP!", palette: Color::GOLD, sub: Gallery.find(info[:art])&.title)
|
|
63
|
+
end
|
|
64
|
+
elsif event.flag?(:comeback)
|
|
54
65
|
mega.(event.streak >= 2 ? "COMEBACK!" : "FIXED!", Color::GOLD, rainbow: event.streak >= 2)
|
|
55
66
|
elsif event.flag?(:eval_milestone)
|
|
56
67
|
mega.("#{Fx.number_with_commas(event_evals(event))} EVALS", Color::NEON, rainbow: true)
|
|
57
68
|
elsif event.flag?(:record_jump)
|
|
58
69
|
mega.("NEW RECORD", Color::GOLD, rainbow: true, sub: number_text(info))
|
|
59
70
|
elsif event.flag?(:combo_mega)
|
|
60
|
-
mega.("COMBO #{event.combo}!", Color::FIRE, rainbow: event.combo >= 50)
|
|
71
|
+
mega.("COMBO #{event.combo}!", Color::FIRE, rainbow: event.combo >= 50, sub: event.flag?(:fever_start) ? "FEVER TIME! ALL POINTS x2" : nil)
|
|
72
|
+
elsif event.flag?(:combo_best)
|
|
73
|
+
Scenes::Banner.new(ctx, event, text: "BEST COMBO!", palette: Color::FIRE, sub: "ALL-TIME BEST #{event.combo}")
|
|
61
74
|
elsif info[:type] == :definition
|
|
62
75
|
Scenes::Unlock.new(ctx, event)
|
|
63
76
|
elsif event.flag?(:combo_milestone) && event.combo >= 5
|
|
64
77
|
Scenes::Banner.new(ctx, event, text: "COMBO #{event.combo}", palette: Color::FIRE)
|
|
65
78
|
elsif event.flag?(:first_hit)
|
|
66
79
|
Scenes::Banner.new(ctx, event, text: "FIRST HIT!", palette: Color::ICE)
|
|
80
|
+
elsif event.flag?(:critical) && !counter?(event)
|
|
81
|
+
Scenes::Critical.new(ctx, event)
|
|
67
82
|
elsif event.flag?(:output_rain)
|
|
68
83
|
Scenes::Rain.new(ctx, event)
|
|
69
84
|
else
|
|
@@ -71,6 +86,11 @@ module Dopairb
|
|
|
71
86
|
end
|
|
72
87
|
end
|
|
73
88
|
|
|
89
|
+
def counter?(event)
|
|
90
|
+
v = event.info[:value]
|
|
91
|
+
%i[integer float].include?(event.info[:type]) && (v.abs >= 10_000 || event.flag?(:new_record))
|
|
92
|
+
end
|
|
93
|
+
|
|
74
94
|
def value_scene(event, ctx)
|
|
75
95
|
info = event.info
|
|
76
96
|
case info[:type]
|
|
@@ -90,32 +110,76 @@ module Dopairb
|
|
|
90
110
|
return if @config.off?
|
|
91
111
|
c = ctx(input: input, pre: pre)
|
|
92
112
|
c.input = nil if input && input.size + 12 > c.rows
|
|
93
|
-
|
|
113
|
+
scene = scene_for(event, c)
|
|
114
|
+
play(scene)
|
|
115
|
+
encore(event, c) if Scenes::Jackpot === scene
|
|
116
|
+
scene
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A jackpot pays out a masterpiece too: too good to fold away.
|
|
120
|
+
def encore(event, c)
|
|
121
|
+
return unless event.info[:art] && @config.motion && @config.level >= 2
|
|
122
|
+
return if Term.input_pending? || !Scenes::Masterpiece.fits?(c.rows, c.w + 1)
|
|
123
|
+
full = ctx(rows: c.rows, cols: c.w + 1).tap { |x| x.depth = c.depth }
|
|
124
|
+
play(Scenes::Masterpiece.new(full, event))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Cute loading screen until ready.call is true (e.g. the intro sound exists).
|
|
128
|
+
def loading(&ready)
|
|
129
|
+
return if @config.off?
|
|
130
|
+
scene = Scenes::Loading.new(ctx, ready)
|
|
131
|
+
return unless @config.motion && fits?(scene)
|
|
132
|
+
run(scene)
|
|
133
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
134
|
+
raise if SystemExit === e || (SignalException === e && !(Interrupt === e))
|
|
94
135
|
end
|
|
95
136
|
|
|
96
|
-
def intro
|
|
137
|
+
def intro(career = nil)
|
|
97
138
|
return if @config.off? || !@config.intro
|
|
98
|
-
play(Scenes::Intro.new(ctx), force_trail: true)
|
|
139
|
+
play(Scenes::Intro.new(ctx, career), force_trail: true)
|
|
99
140
|
end
|
|
100
141
|
|
|
101
142
|
def outro(stats)
|
|
102
143
|
return if @config.off?
|
|
103
|
-
|
|
144
|
+
c = ctx
|
|
145
|
+
scene = if @config.motion && Scenes::Finale.fits?(c.rows, c.w + 1) && @config.level >= 2
|
|
146
|
+
Scenes::Finale.new(c, stats)
|
|
147
|
+
else
|
|
148
|
+
Scenes::Outro.new(c, stats)
|
|
149
|
+
end
|
|
150
|
+
# render the synced track now so it does not start late
|
|
151
|
+
Sound.file(scene.sfx, @config.duration) if @config.sound == :sfx && scene.sfx && Sound.available?
|
|
152
|
+
play(scene, force_trail: true)
|
|
104
153
|
end
|
|
105
154
|
|
|
106
155
|
def play(scene, force_trail: false)
|
|
107
156
|
animated = @config.motion && scene.ctx.w >= 20 && fits?(scene)
|
|
108
157
|
animated = false if animated && Term.input_pending?
|
|
158
|
+
sound(scene, animated) unless !animated && @config.motion
|
|
109
159
|
run(scene) if animated
|
|
110
|
-
Term.bell if @config.sound && scene.bell? && animated
|
|
111
160
|
emit_trail(scene, force_trail)
|
|
112
161
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
113
162
|
raise if SystemExit === e || (SignalException === e && !(Interrupt === e))
|
|
114
163
|
# Never let an effect break the REPL; Ctrl-C during an effect just skips it.
|
|
164
|
+
Dopairb.debug(e) unless Interrupt === e
|
|
115
165
|
end
|
|
116
166
|
|
|
117
167
|
private
|
|
118
168
|
|
|
169
|
+
def sound(scene, animated)
|
|
170
|
+
case @config.sound
|
|
171
|
+
when :bell
|
|
172
|
+
Term.bell if scene.bell?
|
|
173
|
+
when :sfx
|
|
174
|
+
if Sound.available?
|
|
175
|
+
delay = animated ? (scene.pre + scene.impact_at) * @config.duration : 0.0
|
|
176
|
+
Sound.play(scene.sfx, delay: delay, stretch: @config.duration) if scene.sfx
|
|
177
|
+
elsif scene.bell?
|
|
178
|
+
Term.bell
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
119
183
|
def event_evals(_event)
|
|
120
184
|
Dopairb.session&.game&.evals.to_i
|
|
121
185
|
end
|
|
@@ -126,8 +190,7 @@ module Dopairb
|
|
|
126
190
|
|
|
127
191
|
def fits?(scene)
|
|
128
192
|
top = scene.top? ? scene.ctx.input.size : 0
|
|
129
|
-
|
|
130
|
-
alt || scene.height + top <= scene.ctx.rows - 1
|
|
193
|
+
scene.alt? || scene.height + top <= scene.ctx.rows - 1
|
|
131
194
|
end
|
|
132
195
|
|
|
133
196
|
def emit_trail(scene, force)
|
|
@@ -140,7 +203,7 @@ module Dopairb
|
|
|
140
203
|
end
|
|
141
204
|
|
|
142
205
|
def run(scene)
|
|
143
|
-
alt = scene.
|
|
206
|
+
alt = scene.alt?
|
|
144
207
|
top = !alt && scene.top? ? scene.ctx.input.rows : []
|
|
145
208
|
stage = alt ? AltStage.new(scene.height) : Stage.new(scene.height, top: top)
|
|
146
209
|
depth = scene.depth
|
|
@@ -153,7 +216,7 @@ module Dopairb
|
|
|
153
216
|
loop do
|
|
154
217
|
break if Term.input_pending? || !stage.open?
|
|
155
218
|
t = (now - t0) / @config.duration
|
|
156
|
-
break if t >= scene.total_length
|
|
219
|
+
break if t >= scene.total_length || scene.done?(t)
|
|
157
220
|
lines = []
|
|
158
221
|
unless top.empty?
|
|
159
222
|
tc = Canvas.new(scene.ctx.w, top.size)
|
data/lib/dopairb/fx.rb
CHANGED
|
@@ -111,6 +111,29 @@ module Dopairb
|
|
|
111
111
|
width * scale
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
# The big font squeezed into three rows with half blocks (▀ ▄ █).
|
|
115
|
+
# Returns the width drawn, or nil when the terminal lacks the glyphs.
|
|
116
|
+
def half_text(canvas, text, x, y, color_fn)
|
|
117
|
+
up = Term.glyph("▀", nil)
|
|
118
|
+
down = Term.glyph("▄", nil)
|
|
119
|
+
full = Term.glyph("█", nil)
|
|
120
|
+
return nil unless up && down && full
|
|
121
|
+
bm = Font.bitmap(text)
|
|
122
|
+
width = bm[0].size
|
|
123
|
+
3.times do |r|
|
|
124
|
+
top = bm[r * 2]
|
|
125
|
+
bot = bm[r * 2 + 1] || []
|
|
126
|
+
width.times do |cx|
|
|
127
|
+
a = top[cx]
|
|
128
|
+
b = bot[cx]
|
|
129
|
+
next unless a || b
|
|
130
|
+
ch = a && b ? full : (a ? up : down)
|
|
131
|
+
canvas.put(x + cx, y + r, ch, color_fn.(cx, r, cx.fdiv([width - 1, 1].max)))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
width
|
|
135
|
+
end
|
|
136
|
+
|
|
114
137
|
def text_fits?(text, w, scale = 1)
|
|
115
138
|
Font.width(text, scale: scale) <= w
|
|
116
139
|
end
|