termfront 0.1.5 → 0.1.7
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/CHANGELOG.md +40 -0
- data/exe/termfront +2 -0
- data/lib/termfront/async_writer.rb +45 -0
- data/lib/termfront/audio_manager.rb +10 -2
- data/lib/termfront/color.rb +19 -0
- data/lib/termfront/config.rb +2 -0
- data/lib/termfront/drop_item/weapon.rb +4 -1
- data/lib/termfront/game.rb +78 -50
- data/lib/termfront/network/client.rb +32 -31
- data/lib/termfront/network/wavesfight_client.rb +2 -1
- data/lib/termfront/renderer.rb +369 -239
- data/lib/termfront/sprite.rb +60 -24
- data/lib/termfront/terminal_output.rb +31 -16
- data/lib/termfront/title_screen.rb +192 -49
- data/lib/termfront/version.rb +1 -1
- data/lib/termfront.rb +2 -0
- 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: b6d91fa7b7758b2151fc31bb9f7862dbda3ad311edea638aa9b11ca50a8b9a8d
|
|
4
|
+
data.tar.gz: 3a8ce081de76ddd04c45949c490ba4b513f42eb94eb39fce37fef292abdbb004
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a79ab596683eea6f8914c17cf88ad000d7a1b33453820526310f25fd43dbabb1f21e6f65f16a390fa92e491d6cd2da86e981c17bc971161316a6d4f7b431c98a
|
|
7
|
+
data.tar.gz: 556667b34ba430c7af848fca7d31f5937e24d328480a29a3191db88ca4286d579081b0f6e18c34622d87cb9ea401c20e19da6aaf0a6fbc2b892f15fdbb9f7b76
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,46 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.7] - 2026-05-26
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Restore the in-game and title screen render rate to 60 Hz (`Config::RENDER_DT = 1.0 / 60.0`) after testers reported choppy motion at 30 Hz; the hybrid rendering scaffolding is kept in place so the rate can be tuned later without code changes
|
|
14
|
+
- Cap the per-frame `dt` at `Config::MAX_DT` (50 ms / 20 FPS-equivalent) in the singleplayer, Wavesfight, and PvP game loops so a frame that runs long under a constrained host no longer translates into a single oversized physics step. Movement, weapon timers, and projectile motion now stay smooth — the simulation simply slows for that frame instead of producing a teleporting opponent or instant fire-rate
|
|
15
|
+
- Hoist the sprite shape function out of the per-cell loop in `Renderer#overlay_enemies_3d`: look up `Sprite::REGISTRY[sprite_id]` once per enemy and call it directly from the inner cell loop instead of going through `Sprite.for` (which re-hashed `REGISTRY` and re-checked the result for `nil` on every cell). With several enemies on-screen the per-cell hash lookup overhead disappears
|
|
16
|
+
- Migrate the PvP/Wavesfight client render paths to 256-color SGR sequences as well: add a `Sprite.player_enemy` variant pre-tinted toward red instead of recomputing the tint per cell, replace the per-cell `\e[38;2;…m` (and `\e[48;2;…m` for damage flash) escapes with `\e[38;5;Nm` lookups, drop the `tint_player_color` helper and its per-cell `String#split`/`Array#map` work, and reuse the renderer's shared `DAMAGE_FLASH_RAMP` for the screen-edge flash
|
|
17
|
+
- Hand frame output off to a dedicated writer thread (`AsyncWriter`) so the game loop's `syswrite` becomes a non-blocking `Queue#push` and the actual `IO#syswrite` runs on a background thread that releases the GVL during the PTY write. The main loop no longer pays the wall-clock cost of waiting for the terminal to drain its buffer; when a write is still in progress, the latest frame replaces the queued one so the terminal always sees the freshest frame instead of a backlog
|
|
18
|
+
- Drop truecolor SGR sequences from the in-game and title 3D views; convert every sprite, fallback, bar, projectile, drop, and damage-flash color to an xterm 256-color index once with `Color.rgb_to_256`, simplify `Renderer#render_view` to a pure 256-color path that drops the per-cell `is_a?(Integer)` test and the truecolor SGR caches, and switch the title screen's half-block compositor to look up its escapes in the shared `FG_256` / `BG_256` tables. The 13-byte truecolor SGR shrinks to a 9-byte 256-color SGR (or shorter on cell boundaries via `\e[K`), the renderer's hottest cell loop loses its type dispatch, and the dead `bg_only?` / `ansi_fg` / `ansi_bg` helpers go away
|
|
19
|
+
- Build the half-block rows in `TitleScreen#render` directly into the final per-row string with the column count already capped, skipping the per-row `TerminalOutput.fit_ansi` walk entirely; rows that come out uniformly the same color collapse to `\e[bg]\e[K\e[0m`, fully-blank rows ship as a single space run, and the byte stream the terminal sees for each title frame shrinks substantially
|
|
20
|
+
- Coalesce same-color cell runs in `TitleScreen#render`'s half-block compositor and only emit SGR escapes on transitions instead of wrapping every cell in `\e[…m…\e[0m`; the per-row string handed to `fit_ansi` shrinks from one bracketed SGR per cell to a handful of color changes, cutting both the title frame's byte volume and `fit_ansi`'s per-row scan cost
|
|
21
|
+
- Emit `\e[K` (Erase to End of Line) for rows in `Renderer#render_view` whose top/bottom cells are all the same integer background color; the ceiling-only and floor-only bands now ship as `\e[bg]\e[K\r\n` per row instead of the full per-cell glyph sequence, slashing the ANSI byte volume of those rows from `view_w` bytes plus per-cell SGR transitions down to about a dozen bytes total
|
|
22
|
+
- Memoize `AudioManager#which` results in a class-level cache so the per-command `PATH` walk (and the per-directory `File.executable?` stat) only runs the first time a binary is looked up; subsequent `AudioManager.new` calls during mode transitions hit the cache instead of re-scanning `PATH`, which removes a noticeable stutter on WSL hosts where Windows-mounted `PATH` entries are slow to stat
|
|
23
|
+
- Coalesce same-color runs of cells in `Renderer#render_view` with a single `String#*` instead of one `buf << " "` (or `buf << "█"`) per cell, and rewrite the row/column loops as `while` so the ceiling/floor bands cost one short string copy per row instead of dozens of one-byte appends
|
|
24
|
+
- Skip the per-column DDA in `Renderer#render` when the player position, facing, map, and viewport are unchanged from the previous frame; the cached `@dists`, `@sides`, `@wtop`, `@wbot`, and `@wcol` arrays are reused so `cast_ray` and the wall-column projection only run when something actually moved
|
|
25
|
+
- Bulk-fill the ceiling-only and floor-only rows of `Renderer#build_view_pixels` with `Array#fill` so each fully ceiling or fully floor row is written as a single C-level call, with the column-major wall loop kept for the mixed band in the middle
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Treat `Errno::EAGAIN` from `syswrite` the same as `IO::WaitWritable` in `TerminalOutput.write_all` so the renderer keeps draining its frame buffer instead of crashing when the terminal's PTY temporarily refuses additional bytes
|
|
30
|
+
|
|
31
|
+
## [0.1.6] - 2026-05-26
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Memoize the HUD shield and ammo lines so `fit_ansi` only re-runs when the shown shield value, weapon, ammo, pickup hint, or terminal width actually change
|
|
36
|
+
- Inline the color-mode branching and SGR lookups into `Renderer#render_view` to remove per-cell `bg_only?`, `ansi_fg`, and `ansi_bg` method calls (about 72,000 calls per second at 30 Hz on an 80-wide terminal)
|
|
37
|
+
- Enable YJIT on startup so the hot Ruby methods in the renderer (`build_view_pixels`, `render_view`, `cast_ray`, etc.) get JIT-compiled instead of interpreted
|
|
38
|
+
- Rewrite `TerminalOutput.fit_ansi` to walk the input by byte index with `getbyte` and `byteslice` instead of scanning with a regular expression, removing the per-line `Regexp#match` / `MatchData` allocations that dominated the renderer's CPU profile under YJIT
|
|
39
|
+
- Render the title screen at 30 Hz while still advancing its spin animation and polling input at 60 Hz, so the per-frame renderer cost halves without making the demo motion feel choppy
|
|
40
|
+
- Cache the demo mission instance, its tile-map, and its enemy definitions inside `TitleScreen#initialize` instead of rebuilding them every frame
|
|
41
|
+
- Render singleplayer and Wavesfight at 30 Hz while still running the update step, physics, and input polling at 60 Hz, halving the in-game renderer cost without slowing the simulation or input response
|
|
42
|
+
- Rewrite `Renderer#build_view_pixels` with column-major `while` loops that read `wtop`/`wbot`/`wcol` once per column and fill the ceiling / wall / floor segments without per-cell branching, hoisting `Config::CEIL_C` / `Config::FLOOR_C` and `@pixels` as locals so YJIT keeps the hot loop tight
|
|
43
|
+
- Memoize the title screen's static lines (logo, subtitle, and the five menu entries) per terminal width so `fit_ansi` runs once instead of seven times per title-screen frame
|
|
44
|
+
- Memoize every line of `Game#render_mission_select` keyed by terminal width, current selection, difficulty, and mission class, so the nine `fit_ansi` calls per frame collapse to cache lookups while the cursor is idle
|
|
45
|
+
- Reuse the radar's enemy / drop / terminal / ally cell hashes between frames and key them by integer (`sy * diam + sx`) to drop per-cell `Array` allocations, hoisting `Config::RADAR_RANGE`, `RADAR_RANGE_SQ`, `r*r`, and the player position as locals so YJIT keeps the radar projection loops tight
|
|
46
|
+
- Tighten the enemy half of `Renderer#overlay_enemies_3d`: hoist the FOV tangent, player position, and half-frame constants as locals, replace the `upto` loops with `while`, use integer shift instead of `to_f` / `.ceil` / `.floor` for half-block row indexing, and cache the float versions of `actual_h` / `actual_w` so the per-cell sprite division loop stops allocating new floats every iteration
|
|
47
|
+
- Apply the same `while`-loop and integer-math pattern to the drop and projectile branches of `Renderer#overlay_enemies_3d`, reusing the hoisted player position and half-frame constants and computing the projectile color directly instead of through an intermediate ANSI code string
|
|
48
|
+
|
|
9
49
|
## [0.1.5] - 2026-05-25
|
|
10
50
|
|
|
11
51
|
### Changed
|
data/exe/termfront
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Termfront
|
|
4
|
+
class AsyncWriter
|
|
5
|
+
def initialize(io)
|
|
6
|
+
@io = io
|
|
7
|
+
@queue = Queue.new
|
|
8
|
+
@closed = false
|
|
9
|
+
@thread = Thread.new do
|
|
10
|
+
Thread.current.report_on_exception = false
|
|
11
|
+
while (data = @queue.pop)
|
|
12
|
+
begin
|
|
13
|
+
TerminalOutput.write_all(@io, data)
|
|
14
|
+
rescue IOError, Errno::EBADF, Errno::EPIPE
|
|
15
|
+
break
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def syswrite(data)
|
|
22
|
+
raise IOError, "writer closed" if @closed
|
|
23
|
+
|
|
24
|
+
@queue.clear if @queue.size >= 1
|
|
25
|
+
@queue.push(data)
|
|
26
|
+
data.bytesize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def winsize
|
|
30
|
+
@io.winsize
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def raw(&block)
|
|
34
|
+
@io.raw(&block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
return if @closed
|
|
39
|
+
|
|
40
|
+
@closed = true
|
|
41
|
+
@queue.push(nil)
|
|
42
|
+
@thread.join
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -8,6 +8,8 @@ module Termfront
|
|
|
8
8
|
class AudioManager
|
|
9
9
|
Player = Struct.new(:command, :supports_loop, keyword_init: true)
|
|
10
10
|
|
|
11
|
+
WHICH_CACHE = {}
|
|
12
|
+
|
|
11
13
|
def initialize
|
|
12
14
|
@manifest = load_manifest
|
|
13
15
|
@bgm_player = detect_player(%w[ffplay afplay paplay aplay], prefer_loop: true)
|
|
@@ -160,12 +162,18 @@ module Termfront
|
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
def which(command)
|
|
165
|
+
return WHICH_CACHE[command] if WHICH_CACHE.key?(command)
|
|
166
|
+
|
|
167
|
+
resolved = nil
|
|
163
168
|
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
164
169
|
candidate = File.join(dir, command)
|
|
165
|
-
|
|
170
|
+
if File.executable?(candidate) && !File.directory?(candidate)
|
|
171
|
+
resolved = candidate
|
|
172
|
+
break
|
|
173
|
+
end
|
|
166
174
|
end
|
|
167
175
|
|
|
168
|
-
|
|
176
|
+
WHICH_CACHE[command] = resolved
|
|
169
177
|
end
|
|
170
178
|
|
|
171
179
|
def asset_path(kind, name)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Termfront
|
|
4
|
+
module Color
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def rgb_to_256(r, g, b)
|
|
8
|
+
if (r - g).abs < 8 && (g - b).abs < 8 && (r - b).abs < 8
|
|
9
|
+
avg = (r + g + b) / 3
|
|
10
|
+
return 16 if avg < 8
|
|
11
|
+
return 231 if avg > 247
|
|
12
|
+
|
|
13
|
+
232 + (avg - 8) * 23 / 240
|
|
14
|
+
else
|
|
15
|
+
16 + (r * 5 / 255) * 36 + (g * 5 / 255) * 6 + (b * 5 / 255)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/termfront/config.rb
CHANGED
|
@@ -22,8 +22,11 @@ module Termfront
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
SHOCK_COLOR = Color.rgb_to_256(60, 200, 220)
|
|
26
|
+
NORMAL_COLOR = Color.rgb_to_256(220, 200, 60)
|
|
27
|
+
|
|
25
28
|
def sprite_color
|
|
26
|
-
@type.to_s.start_with?("shock") ?
|
|
29
|
+
@type.to_s.start_with?("shock") ? SHOCK_COLOR : NORMAL_COLOR
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def radar_color
|
data/lib/termfront/game.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Termfront
|
|
4
4
|
class Game
|
|
5
5
|
def initialize
|
|
6
|
-
@stdout = STDOUT
|
|
6
|
+
@stdout = AsyncWriter.new(STDOUT)
|
|
7
7
|
@audio = AudioManager.new
|
|
8
8
|
@renderer = Renderer.new(@stdout)
|
|
9
9
|
@input = Input.new
|
|
@@ -36,6 +36,7 @@ module Termfront
|
|
|
36
36
|
@crash = e
|
|
37
37
|
ensure
|
|
38
38
|
@audio.close
|
|
39
|
+
@stdout.close if @stdout.respond_to?(:close) && !@stdout.equal?(STDOUT)
|
|
39
40
|
leave_alt_screen
|
|
40
41
|
if @crash
|
|
41
42
|
warn "#{@crash.class}: #{@crash.message}"
|
|
@@ -131,10 +132,12 @@ module Termfront
|
|
|
131
132
|
def run_game_loop(show_complete_banner: true)
|
|
132
133
|
STDIN.raw do |stdin|
|
|
133
134
|
last_time = clock
|
|
135
|
+
last_render = last_time - Config::RENDER_DT
|
|
134
136
|
|
|
135
137
|
loop do
|
|
136
138
|
now = clock
|
|
137
139
|
dt = now - last_time
|
|
140
|
+
dt = Config::MAX_DT if dt > Config::MAX_DT
|
|
138
141
|
last_time = now
|
|
139
142
|
|
|
140
143
|
keys = @input.process(stdin, player: @player)
|
|
@@ -146,11 +149,14 @@ module Termfront
|
|
|
146
149
|
end
|
|
147
150
|
|
|
148
151
|
update(dt)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if now - last_render >= Config::RENDER_DT
|
|
153
|
+
@renderer.render(
|
|
154
|
+
player: @player, map: @map,
|
|
155
|
+
enemies: @enemies, projectiles: @projectiles,
|
|
156
|
+
drops: @player.drops, terminals: @terminals
|
|
157
|
+
)
|
|
158
|
+
last_render = now
|
|
159
|
+
end
|
|
154
160
|
|
|
155
161
|
if @player.dead
|
|
156
162
|
rows, cols = @stdout.winsize
|
|
@@ -176,10 +182,12 @@ module Termfront
|
|
|
176
182
|
def run_wavesfight_loop
|
|
177
183
|
STDIN.raw do |stdin|
|
|
178
184
|
last_time = clock
|
|
185
|
+
last_render = last_time - Config::RENDER_DT
|
|
179
186
|
|
|
180
187
|
loop do
|
|
181
188
|
now = clock
|
|
182
189
|
dt = now - last_time
|
|
190
|
+
dt = Config::MAX_DT if dt > Config::MAX_DT
|
|
183
191
|
last_time = now
|
|
184
192
|
|
|
185
193
|
keys = @input.process(stdin, player: @player)
|
|
@@ -191,12 +199,15 @@ module Termfront
|
|
|
191
199
|
end
|
|
192
200
|
|
|
193
201
|
update(dt)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
if now - last_render >= Config::RENDER_DT
|
|
203
|
+
@renderer.render(
|
|
204
|
+
player: @player, map: @map,
|
|
205
|
+
enemies: @enemies, projectiles: @projectiles,
|
|
206
|
+
drops: @player.drops, terminals: @terminals,
|
|
207
|
+
status_line: " WAVE #{@wave} #{Enemy::Base::DIFFICULTIES[@difficulty][:name]}"
|
|
208
|
+
)
|
|
209
|
+
last_render = now
|
|
210
|
+
end
|
|
200
211
|
|
|
201
212
|
if @player.dead
|
|
202
213
|
rows, cols = @stdout.winsize
|
|
@@ -209,6 +220,7 @@ module Termfront
|
|
|
209
220
|
show_wave_clear
|
|
210
221
|
start_wavesfight_wave
|
|
211
222
|
last_time = clock
|
|
223
|
+
last_render = clock - Config::RENDER_DT
|
|
212
224
|
end
|
|
213
225
|
|
|
214
226
|
cap_frame(now)
|
|
@@ -543,62 +555,78 @@ module Termfront
|
|
|
543
555
|
end
|
|
544
556
|
end
|
|
545
557
|
|
|
558
|
+
MISSION_SELECT_DIFF_COLORS = ["\e[92m", "\e[93m", "\e[38;2;255;165;0m", "\e[91m"].freeze
|
|
559
|
+
MISSION_SELECT_CTRL = "Up/Down: Select Left/Right: Difficulty Enter/1-5: Start Q: Back"
|
|
560
|
+
|
|
546
561
|
def render_mission_select(selected, missions, title)
|
|
547
562
|
rows, cols = @stdout.winsize
|
|
548
563
|
buf = TerminalOutput.begin_frame(home: true)
|
|
549
564
|
lines = Array.new(rows) { " " * cols }
|
|
565
|
+
cache = (@mission_select_cache ||= {})
|
|
550
566
|
|
|
551
|
-
|
|
552
|
-
|
|
567
|
+
lines[1] = cache[[:title, cols, title]] ||= begin
|
|
568
|
+
tc = [(cols - title.size) / 2 + 1, 1].max
|
|
569
|
+
TerminalOutput.fit_ansi("#{" " * (tc - 1)}\e[1;38;2;120;140;255m#{title}\e[0m", cols)
|
|
570
|
+
end
|
|
553
571
|
|
|
554
572
|
diff = Enemy::Base::DIFFICULTIES[@difficulty]
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
573
|
+
lines[2] = cache[[:diff, cols, @difficulty]] ||= begin
|
|
574
|
+
diff_label = "< #{diff[:name]} >"
|
|
575
|
+
dc = [(cols - diff_label.size) / 2 + 1, 1].max
|
|
576
|
+
TerminalOutput.fit_ansi("#{" " * (dc - 1)}#{MISSION_SELECT_DIFF_COLORS[@difficulty]}#{diff_label}\e[0m", cols)
|
|
577
|
+
end
|
|
559
578
|
|
|
560
579
|
missions.each_with_index do |klass, i|
|
|
561
|
-
m = klass.new
|
|
562
580
|
row = 5 + i * 2
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
581
|
+
is_selected = i == selected
|
|
582
|
+
lines[row - 1] = cache[[:mission, cols, i, is_selected, klass]] ||= begin
|
|
583
|
+
m = klass.new
|
|
584
|
+
label = " #{i + 1}. #{m.name}"
|
|
585
|
+
lc = [(cols - 40) / 2 + 1, 1].max
|
|
586
|
+
text = if is_selected
|
|
587
|
+
"\e[1;97;44m> #{label.strip.ljust(38)}\e[0m"
|
|
588
|
+
else
|
|
589
|
+
"\e[97m #{label.strip.ljust(38)}\e[0m"
|
|
590
|
+
end
|
|
591
|
+
TerminalOutput.fit_ansi("#{" " * (lc - 1)}#{text}", cols)
|
|
592
|
+
end
|
|
571
593
|
end
|
|
572
594
|
|
|
573
595
|
brief_row = 5 + missions.size * 2 + 1
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
596
|
+
lines[brief_row - 1] = cache[[:brief, cols, missions[selected]]] ||= begin
|
|
597
|
+
m = missions[selected].new
|
|
598
|
+
briefing = m.briefing
|
|
599
|
+
bc = [(cols - briefing.size) / 2 + 1, 1].max
|
|
600
|
+
TerminalOutput.fit_ansi("#{" " * (bc - 1)}\e[38;2;180;180;200m#{briefing}\e[0m", cols)
|
|
601
|
+
end
|
|
578
602
|
|
|
579
603
|
info_row = brief_row + 2
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
604
|
+
lines[info_row - 1] = cache[[:info, cols, @difficulty, missions[selected]]] ||= begin
|
|
605
|
+
m = missions[selected].new
|
|
606
|
+
edefs = m.enemy_defs
|
|
607
|
+
base_crawler = edefs.count { |e| e[4] == :crawler }
|
|
608
|
+
base_executor = edefs.count { |e| e[4] == :executor }
|
|
609
|
+
extra = diff[:extra_enemies]
|
|
610
|
+
extra_crawler = 0
|
|
611
|
+
extra_executor = 0
|
|
612
|
+
extra.times do |i|
|
|
613
|
+
src_type = edefs[i % edefs.size][4]
|
|
614
|
+
src_type == :crawler ? (extra_crawler += 1) : (extra_executor += 1)
|
|
615
|
+
end
|
|
616
|
+
crawler_c = base_crawler + extra_crawler
|
|
617
|
+
executor_c = base_executor + extra_executor
|
|
618
|
+
info = "Enemies: #{crawler_c} Crawler#{crawler_c != 1 ? "s" : ""}"
|
|
619
|
+
info += ", #{executor_c} Executor#{executor_c != 1 ? "s" : ""}" if executor_c > 0
|
|
620
|
+
info += " | HP x#{diff[:hp_mult]}"
|
|
621
|
+
ic = [(cols - info.size) / 2 + 1, 1].max
|
|
622
|
+
TerminalOutput.fit_ansi("#{" " * (ic - 1)}\e[38;2;140;140;160m#{info}\e[0m", cols)
|
|
589
623
|
end
|
|
590
|
-
crawler_c = base_crawler + extra_crawler
|
|
591
|
-
executor_c = base_executor + extra_executor
|
|
592
|
-
info = "Enemies: #{crawler_c} Crawler#{crawler_c != 1 ? "s" : ""}"
|
|
593
|
-
info += ", #{executor_c} Executor#{executor_c != 1 ? "s" : ""}" if executor_c > 0
|
|
594
|
-
info += " | HP x#{diff[:hp_mult]}"
|
|
595
|
-
ic = [(cols - info.size) / 2 + 1, 1].max
|
|
596
|
-
lines[info_row - 1] = TerminalOutput.fit_ansi("#{" " * (ic - 1)}\e[38;2;140;140;160m#{info}\e[0m", cols)
|
|
597
624
|
|
|
598
625
|
ctrl_row = info_row + 2
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
626
|
+
lines[ctrl_row - 1] = cache[[:ctrl, cols]] ||= begin
|
|
627
|
+
cc = [(cols - MISSION_SELECT_CTRL.size) / 2 + 1, 1].max
|
|
628
|
+
TerminalOutput.fit_ansi("#{" " * (cc - 1)}\e[38;2;100;100;120m#{MISSION_SELECT_CTRL}\e[0m", cols)
|
|
629
|
+
end
|
|
602
630
|
|
|
603
631
|
lines.each_with_index do |line, index|
|
|
604
632
|
buf << line
|
|
@@ -6,6 +6,14 @@ module Termfront
|
|
|
6
6
|
TEAM_SIZES = [1, 2, 4].freeze
|
|
7
7
|
ALLOWED_WEAPONS = %w[pistol ar].freeze
|
|
8
8
|
|
|
9
|
+
ALLY_PLAYER_FALLBACK = Color.rgb_to_256(70, 210, 255)
|
|
10
|
+
ENEMY_PLAYER_FALLBACK = Color.rgb_to_256(255, 110, 80)
|
|
11
|
+
ALLY_BAR_FILL = Color.rgb_to_256(0, 180, 255)
|
|
12
|
+
ENEMY_BAR_FILL = Color.rgb_to_256(255, 80, 80)
|
|
13
|
+
BAR_EMPTY = Color.rgb_to_256(80, 20, 20)
|
|
14
|
+
PROJ_SHOCK = Color.rgb_to_256(80, 220, 255)
|
|
15
|
+
PROJ_NORMAL = Color.rgb_to_256(255, 210, 80)
|
|
16
|
+
|
|
9
17
|
def initialize(stdout)
|
|
10
18
|
@stdout = stdout
|
|
11
19
|
@conn = Connection.new
|
|
@@ -199,6 +207,7 @@ module Termfront
|
|
|
199
207
|
loop do
|
|
200
208
|
now = clock
|
|
201
209
|
dt = now - last_time
|
|
210
|
+
dt = Config::MAX_DT if dt > Config::MAX_DT
|
|
202
211
|
last_time = now
|
|
203
212
|
|
|
204
213
|
keys = @input.process(stdin, player: @player)
|
|
@@ -548,33 +557,34 @@ module Termfront
|
|
|
548
557
|
next unless top_in || bot_in
|
|
549
558
|
|
|
550
559
|
if use_shape
|
|
560
|
+
sprite_fn = color_mode == :ally ? Sprite.method(:player) : Sprite.method(:player_enemy)
|
|
551
561
|
ny0 = top_in ? (vp0 - draw_top).to_f / actual_h : nil
|
|
552
562
|
ny1 = bot_in ? (vp1 - draw_top).to_f / actual_h : nil
|
|
553
|
-
top_color = ny0 ?
|
|
554
|
-
bot_color = ny1 ?
|
|
563
|
+
top_color = ny0 ? sprite_fn.call(nx, ny0) : nil
|
|
564
|
+
bot_color = ny1 ? sprite_fn.call(nx, ny1) : nil
|
|
555
565
|
next unless top_color || bot_color
|
|
556
566
|
|
|
557
567
|
buf << "\e[#{3 + r};#{c + 1}H"
|
|
558
568
|
buf << if top_color && bot_color
|
|
559
569
|
if top_color == bot_color
|
|
560
|
-
"\e[38;
|
|
570
|
+
"\e[38;5;#{top_color}m\xE2\x96\x88\e[0m"
|
|
561
571
|
else
|
|
562
|
-
"\e[38;
|
|
572
|
+
"\e[38;5;#{top_color};48;5;#{bot_color}m\xE2\x96\x80\e[0m"
|
|
563
573
|
end
|
|
564
574
|
elsif top_color
|
|
565
|
-
"\e[38;
|
|
575
|
+
"\e[38;5;#{top_color}m\xE2\x96\x80\e[0m"
|
|
566
576
|
else
|
|
567
|
-
"\e[38;
|
|
577
|
+
"\e[38;5;#{bot_color}m\xE2\x96\x84\e[0m"
|
|
568
578
|
end
|
|
569
579
|
else
|
|
570
|
-
fc = color_mode == :ally ?
|
|
580
|
+
fc = color_mode == :ally ? ALLY_PLAYER_FALLBACK : ENEMY_PLAYER_FALLBACK
|
|
571
581
|
buf << "\e[#{3 + r};#{c + 1}H"
|
|
572
582
|
buf << if top_in && bot_in
|
|
573
|
-
"\e[38;
|
|
583
|
+
"\e[38;5;#{fc}m\xE2\x96\x88\e[0m"
|
|
574
584
|
elsif top_in
|
|
575
|
-
"\e[38;
|
|
585
|
+
"\e[38;5;#{fc}m\xE2\x96\x80\e[0m"
|
|
576
586
|
else
|
|
577
|
-
"\e[38;
|
|
587
|
+
"\e[38;5;#{fc}m\xE2\x96\x84\e[0m"
|
|
578
588
|
end
|
|
579
589
|
end
|
|
580
590
|
end
|
|
@@ -590,15 +600,15 @@ module Termfront
|
|
|
590
600
|
max_total = Config::SHIELD_MAX + Config::HEALTH_MAX
|
|
591
601
|
hp_pct = total.to_f / max_total
|
|
592
602
|
filled = (hp_pct * (bar_ex - bar_sx + 1)).ceil
|
|
593
|
-
fill_color = color_mode == :ally ?
|
|
603
|
+
fill_color = color_mode == :ally ? ALLY_BAR_FILL : ENEMY_BAR_FILL
|
|
594
604
|
|
|
595
605
|
bar_sx.upto(bar_ex) do |c|
|
|
596
606
|
next if c < 0 || c >= view_w
|
|
597
607
|
next if dists[c] < tz
|
|
598
608
|
|
|
599
609
|
ci = c - bar_sx
|
|
600
|
-
color = ci < filled ? fill_color :
|
|
601
|
-
buf << "\e[#{3 + bar_row};#{c + 1}H\e[38;
|
|
610
|
+
color = ci < filled ? fill_color : BAR_EMPTY
|
|
611
|
+
buf << "\e[#{3 + bar_row};#{c + 1}H\e[38;5;#{color}m\xE2\x96\x88\e[0m"
|
|
602
612
|
end
|
|
603
613
|
end
|
|
604
614
|
|
|
@@ -649,28 +659,17 @@ module Termfront
|
|
|
649
659
|
proj_color = projectile[:color]
|
|
650
660
|
buf << "\e[#{3 + r};#{c + 1}H"
|
|
651
661
|
buf << if top_in && bot_in
|
|
652
|
-
"\e[38;
|
|
662
|
+
"\e[38;5;#{proj_color}m\xE2\x96\x88\e[0m"
|
|
653
663
|
elsif top_in
|
|
654
|
-
"\e[38;
|
|
664
|
+
"\e[38;5;#{proj_color}m\xE2\x96\x80\e[0m"
|
|
655
665
|
else
|
|
656
|
-
"\e[38;
|
|
666
|
+
"\e[38;5;#{proj_color}m\xE2\x96\x84\e[0m"
|
|
657
667
|
end
|
|
658
668
|
end
|
|
659
669
|
end
|
|
660
670
|
end
|
|
661
671
|
end
|
|
662
672
|
|
|
663
|
-
def tint_player_color(color, mode)
|
|
664
|
-
return nil unless color
|
|
665
|
-
return color if mode == :ally
|
|
666
|
-
|
|
667
|
-
r, g, b = color.split(";").map(&:to_i)
|
|
668
|
-
nr = [[r + 70, 255].min, 0].max
|
|
669
|
-
ng = [[g - 50, 0].max, 0].max
|
|
670
|
-
nb = [[b - 90, 0].max, 0].max
|
|
671
|
-
"#{nr};#{ng};#{nb}"
|
|
672
|
-
end
|
|
673
|
-
|
|
674
673
|
def render_pvp_hud(buf, cols)
|
|
675
674
|
bar_w = [cols - 30, 10].max
|
|
676
675
|
pct = @player.shield / Config::SHIELD_MAX.to_f
|
|
@@ -806,13 +805,15 @@ module Termfront
|
|
|
806
805
|
def render_damage_flash(buf, view_h, view_w)
|
|
807
806
|
return unless @player.damage_flash > 0
|
|
808
807
|
|
|
809
|
-
intensity = @player.damage_flash * 60
|
|
808
|
+
intensity = (@player.damage_flash * 60).to_i
|
|
809
|
+
intensity = 255 if intensity > 255
|
|
810
|
+
color = Renderer::DAMAGE_FLASH_RAMP[intensity]
|
|
810
811
|
flash_w = 2
|
|
811
812
|
|
|
812
813
|
view_h.times do |r|
|
|
813
|
-
buf << "\e[#{3 + r};1H\e[48;
|
|
814
|
+
buf << "\e[#{3 + r};1H\e[48;5;#{color}m#{" " * flash_w}\e[0m"
|
|
814
815
|
rc = [view_w - flash_w + 1, 1].max
|
|
815
|
-
buf << "\e[#{3 + r};#{rc}H\e[48;
|
|
816
|
+
buf << "\e[#{3 + r};#{rc}H\e[48;5;#{color}m#{" " * flash_w}\e[0m"
|
|
816
817
|
end
|
|
817
818
|
end
|
|
818
819
|
|
|
@@ -849,7 +850,7 @@ module Termfront
|
|
|
849
850
|
|
|
850
851
|
def spawn_remote_projectile_effect(msg)
|
|
851
852
|
shock = msg[:w].to_s.start_with?("shock")
|
|
852
|
-
color = shock ?
|
|
853
|
+
color = shock ? PROJ_SHOCK : PROJ_NORMAL
|
|
853
854
|
speed = shock ? 18.0 : 14.0
|
|
854
855
|
angle = msg[:a]
|
|
855
856
|
@projectiles << {
|
|
@@ -57,7 +57,7 @@ module Termfront
|
|
|
57
57
|
lines[rows / 2 - 2] = TerminalOutput.fit_ansi("#{" " * (mc - 1)}\e[1;93m#{msg}\e[0m", cols)
|
|
58
58
|
detail = "#{queued_mission_name} | #{queued_difficulty_name}"
|
|
59
59
|
dc = [(cols - detail.size) / 2 + 1, 1].max
|
|
60
|
-
lines[rows / 2] = TerminalOutput.fit_ansi("#{" " * (dc - 1)}\e[38;
|
|
60
|
+
lines[rows / 2] = TerminalOutput.fit_ansi("#{" " * (dc - 1)}\e[38;5;#{Color.rgb_to_256(170, 170, 190)}m#{detail}\e[0m", cols)
|
|
61
61
|
hint = "(ESC to cancel)"
|
|
62
62
|
hc = [(cols - hint.size) / 2 + 1, 1].max
|
|
63
63
|
lines[rows / 2 + 2] = TerminalOutput.fit_ansi("#{" " * (hc - 1)}\e[90m#{hint}\e[0m", cols)
|
|
@@ -121,6 +121,7 @@ module Termfront
|
|
|
121
121
|
loop do
|
|
122
122
|
now = clock
|
|
123
123
|
dt = now - last_time
|
|
124
|
+
dt = Config::MAX_DT if dt > Config::MAX_DT
|
|
124
125
|
last_time = now
|
|
125
126
|
|
|
126
127
|
keys = @input.process(stdin, player: @player)
|