termfront 0.1.5 → 0.1.6
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 +18 -0
- data/exe/termfront +2 -0
- data/lib/termfront/config.rb +1 -0
- data/lib/termfront/game.rb +74 -49
- data/lib/termfront/renderer.rb +280 -176
- data/lib/termfront/terminal_output.rb +30 -15
- data/lib/termfront/title_screen.rb +51 -23
- data/lib/termfront/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d84ea40d760b9d62c0037b83a0b83576725a12a16c3aa2937e16025e6a798015
|
|
4
|
+
data.tar.gz: e33b7fc4bde17b2924ea6a1f30bb11f6f8d30fbba75bd9f9130c773b1b1cf33a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc5856f2bb1eea72307f2ba41af96e5b3412e7ef893fe8a3609f0841ab88d693653ed797c072eff8a4092d01511c810edd2fc848c12dcd17a07b8803181cc551
|
|
7
|
+
data.tar.gz: 461847cef45b100959a044dcb22cca9c537a806331168834d9f936fd6055c4c668581f376058003ad2eb28abdd805cc3f2d82d6f52a36644b7f7d53a13635f9f
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.6] - 2026-05-26
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- 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
|
|
14
|
+
- 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)
|
|
15
|
+
- 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
|
|
16
|
+
- 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
|
|
17
|
+
- 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
|
|
18
|
+
- Cache the demo mission instance, its tile-map, and its enemy definitions inside `TitleScreen#initialize` instead of rebuilding them every frame
|
|
19
|
+
- 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
|
|
20
|
+
- 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
|
|
21
|
+
- 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
|
|
22
|
+
- 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
|
|
23
|
+
- 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
|
|
24
|
+
- 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
|
|
25
|
+
- 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
|
|
26
|
+
|
|
9
27
|
## [0.1.5] - 2026-05-25
|
|
10
28
|
|
|
11
29
|
### Changed
|
data/exe/termfront
CHANGED
data/lib/termfront/config.rb
CHANGED
data/lib/termfront/game.rb
CHANGED
|
@@ -131,6 +131,7 @@ module Termfront
|
|
|
131
131
|
def run_game_loop(show_complete_banner: true)
|
|
132
132
|
STDIN.raw do |stdin|
|
|
133
133
|
last_time = clock
|
|
134
|
+
last_render = last_time - Config::RENDER_DT
|
|
134
135
|
|
|
135
136
|
loop do
|
|
136
137
|
now = clock
|
|
@@ -146,11 +147,14 @@ module Termfront
|
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
update(dt)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
if now - last_render >= Config::RENDER_DT
|
|
151
|
+
@renderer.render(
|
|
152
|
+
player: @player, map: @map,
|
|
153
|
+
enemies: @enemies, projectiles: @projectiles,
|
|
154
|
+
drops: @player.drops, terminals: @terminals
|
|
155
|
+
)
|
|
156
|
+
last_render = now
|
|
157
|
+
end
|
|
154
158
|
|
|
155
159
|
if @player.dead
|
|
156
160
|
rows, cols = @stdout.winsize
|
|
@@ -176,6 +180,7 @@ module Termfront
|
|
|
176
180
|
def run_wavesfight_loop
|
|
177
181
|
STDIN.raw do |stdin|
|
|
178
182
|
last_time = clock
|
|
183
|
+
last_render = last_time - Config::RENDER_DT
|
|
179
184
|
|
|
180
185
|
loop do
|
|
181
186
|
now = clock
|
|
@@ -191,12 +196,15 @@ module Termfront
|
|
|
191
196
|
end
|
|
192
197
|
|
|
193
198
|
update(dt)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
if now - last_render >= Config::RENDER_DT
|
|
200
|
+
@renderer.render(
|
|
201
|
+
player: @player, map: @map,
|
|
202
|
+
enemies: @enemies, projectiles: @projectiles,
|
|
203
|
+
drops: @player.drops, terminals: @terminals,
|
|
204
|
+
status_line: " WAVE #{@wave} #{Enemy::Base::DIFFICULTIES[@difficulty][:name]}"
|
|
205
|
+
)
|
|
206
|
+
last_render = now
|
|
207
|
+
end
|
|
200
208
|
|
|
201
209
|
if @player.dead
|
|
202
210
|
rows, cols = @stdout.winsize
|
|
@@ -209,6 +217,7 @@ module Termfront
|
|
|
209
217
|
show_wave_clear
|
|
210
218
|
start_wavesfight_wave
|
|
211
219
|
last_time = clock
|
|
220
|
+
last_render = clock - Config::RENDER_DT
|
|
212
221
|
end
|
|
213
222
|
|
|
214
223
|
cap_frame(now)
|
|
@@ -543,62 +552,78 @@ module Termfront
|
|
|
543
552
|
end
|
|
544
553
|
end
|
|
545
554
|
|
|
555
|
+
MISSION_SELECT_DIFF_COLORS = ["\e[92m", "\e[93m", "\e[38;2;255;165;0m", "\e[91m"].freeze
|
|
556
|
+
MISSION_SELECT_CTRL = "Up/Down: Select Left/Right: Difficulty Enter/1-5: Start Q: Back"
|
|
557
|
+
|
|
546
558
|
def render_mission_select(selected, missions, title)
|
|
547
559
|
rows, cols = @stdout.winsize
|
|
548
560
|
buf = TerminalOutput.begin_frame(home: true)
|
|
549
561
|
lines = Array.new(rows) { " " * cols }
|
|
562
|
+
cache = (@mission_select_cache ||= {})
|
|
550
563
|
|
|
551
|
-
|
|
552
|
-
|
|
564
|
+
lines[1] = cache[[:title, cols, title]] ||= begin
|
|
565
|
+
tc = [(cols - title.size) / 2 + 1, 1].max
|
|
566
|
+
TerminalOutput.fit_ansi("#{" " * (tc - 1)}\e[1;38;2;120;140;255m#{title}\e[0m", cols)
|
|
567
|
+
end
|
|
553
568
|
|
|
554
569
|
diff = Enemy::Base::DIFFICULTIES[@difficulty]
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
570
|
+
lines[2] = cache[[:diff, cols, @difficulty]] ||= begin
|
|
571
|
+
diff_label = "< #{diff[:name]} >"
|
|
572
|
+
dc = [(cols - diff_label.size) / 2 + 1, 1].max
|
|
573
|
+
TerminalOutput.fit_ansi("#{" " * (dc - 1)}#{MISSION_SELECT_DIFF_COLORS[@difficulty]}#{diff_label}\e[0m", cols)
|
|
574
|
+
end
|
|
559
575
|
|
|
560
576
|
missions.each_with_index do |klass, i|
|
|
561
|
-
m = klass.new
|
|
562
577
|
row = 5 + i * 2
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
578
|
+
is_selected = i == selected
|
|
579
|
+
lines[row - 1] = cache[[:mission, cols, i, is_selected, klass]] ||= begin
|
|
580
|
+
m = klass.new
|
|
581
|
+
label = " #{i + 1}. #{m.name}"
|
|
582
|
+
lc = [(cols - 40) / 2 + 1, 1].max
|
|
583
|
+
text = if is_selected
|
|
584
|
+
"\e[1;97;44m> #{label.strip.ljust(38)}\e[0m"
|
|
585
|
+
else
|
|
586
|
+
"\e[97m #{label.strip.ljust(38)}\e[0m"
|
|
587
|
+
end
|
|
588
|
+
TerminalOutput.fit_ansi("#{" " * (lc - 1)}#{text}", cols)
|
|
589
|
+
end
|
|
571
590
|
end
|
|
572
591
|
|
|
573
592
|
brief_row = 5 + missions.size * 2 + 1
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
593
|
+
lines[brief_row - 1] = cache[[:brief, cols, missions[selected]]] ||= begin
|
|
594
|
+
m = missions[selected].new
|
|
595
|
+
briefing = m.briefing
|
|
596
|
+
bc = [(cols - briefing.size) / 2 + 1, 1].max
|
|
597
|
+
TerminalOutput.fit_ansi("#{" " * (bc - 1)}\e[38;2;180;180;200m#{briefing}\e[0m", cols)
|
|
598
|
+
end
|
|
578
599
|
|
|
579
600
|
info_row = brief_row + 2
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
601
|
+
lines[info_row - 1] = cache[[:info, cols, @difficulty, missions[selected]]] ||= begin
|
|
602
|
+
m = missions[selected].new
|
|
603
|
+
edefs = m.enemy_defs
|
|
604
|
+
base_crawler = edefs.count { |e| e[4] == :crawler }
|
|
605
|
+
base_executor = edefs.count { |e| e[4] == :executor }
|
|
606
|
+
extra = diff[:extra_enemies]
|
|
607
|
+
extra_crawler = 0
|
|
608
|
+
extra_executor = 0
|
|
609
|
+
extra.times do |i|
|
|
610
|
+
src_type = edefs[i % edefs.size][4]
|
|
611
|
+
src_type == :crawler ? (extra_crawler += 1) : (extra_executor += 1)
|
|
612
|
+
end
|
|
613
|
+
crawler_c = base_crawler + extra_crawler
|
|
614
|
+
executor_c = base_executor + extra_executor
|
|
615
|
+
info = "Enemies: #{crawler_c} Crawler#{crawler_c != 1 ? "s" : ""}"
|
|
616
|
+
info += ", #{executor_c} Executor#{executor_c != 1 ? "s" : ""}" if executor_c > 0
|
|
617
|
+
info += " | HP x#{diff[:hp_mult]}"
|
|
618
|
+
ic = [(cols - info.size) / 2 + 1, 1].max
|
|
619
|
+
TerminalOutput.fit_ansi("#{" " * (ic - 1)}\e[38;2;140;140;160m#{info}\e[0m", cols)
|
|
589
620
|
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
621
|
|
|
598
622
|
ctrl_row = info_row + 2
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
623
|
+
lines[ctrl_row - 1] = cache[[:ctrl, cols]] ||= begin
|
|
624
|
+
cc = [(cols - MISSION_SELECT_CTRL.size) / 2 + 1, 1].max
|
|
625
|
+
TerminalOutput.fit_ansi("#{" " * (cc - 1)}\e[38;2;100;100;120m#{MISSION_SELECT_CTRL}\e[0m", cols)
|
|
626
|
+
end
|
|
602
627
|
|
|
603
628
|
lines.each_with_index do |line, index|
|
|
604
629
|
buf << line
|
data/lib/termfront/renderer.rb
CHANGED
|
@@ -19,6 +19,10 @@ module Termfront
|
|
|
19
19
|
@radar_grid_template = build_radar_grid_template
|
|
20
20
|
@hrule_cache = Hash.new { |h, c| h[c] = ("\xE2\x94\x80" * c)[0, c * 3].freeze }
|
|
21
21
|
@radar_drop_glyphs = {}
|
|
22
|
+
@radar_enemy_cells = {}
|
|
23
|
+
@radar_drop_cells = {}
|
|
24
|
+
@radar_terminal_cells = {}
|
|
25
|
+
@radar_ally_cells = {}
|
|
22
26
|
@fg_truecolor_cache = {}
|
|
23
27
|
@bg_truecolor_cache = {}
|
|
24
28
|
@enemy_sprites = []
|
|
@@ -28,6 +32,10 @@ module Termfront
|
|
|
28
32
|
@radar_line_buf = +""
|
|
29
33
|
@size_cache = nil
|
|
30
34
|
@size_cache_at = -Float::INFINITY
|
|
35
|
+
@cached_hud_shield_key = nil
|
|
36
|
+
@cached_hud_shield_line = nil
|
|
37
|
+
@cached_hud_ammo_key = nil
|
|
38
|
+
@cached_hud_ammo_line = nil
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def invalidate_size_cache!
|
|
@@ -211,6 +219,14 @@ module Termfront
|
|
|
211
219
|
end
|
|
212
220
|
|
|
213
221
|
def render_hud(buf, cols, player, drops, terminals, status_line)
|
|
222
|
+
buf << hud_shield_line(cols, player, status_line) << "\r\n"
|
|
223
|
+
buf << hud_ammo_line(cols, player, drops, terminals) << "\r\n"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def hud_shield_line(cols, player, status_line)
|
|
227
|
+
key = [player.shield.to_i, status_line, cols]
|
|
228
|
+
return @cached_hud_shield_line if @cached_hud_shield_key == key
|
|
229
|
+
|
|
214
230
|
bar_w = [cols - 20, 10].max
|
|
215
231
|
pct = player.shield / Config::SHIELD_MAX.to_f
|
|
216
232
|
filled = (pct * bar_w).to_i
|
|
@@ -226,11 +242,23 @@ module Termfront
|
|
|
226
242
|
shield_str = "SHIELD #{color}#{"█" * filled}#{"░" * empty}\e[0m #{pct_s}"
|
|
227
243
|
shield_str = "#{shield_str}\e[90m#{status_line}\e[0m" if status_line
|
|
228
244
|
pad = [(cols - bar_w - 15) / 2, 0].max
|
|
229
|
-
|
|
245
|
+
line = TerminalOutput.fit_ansi("#{" " * pad}#{shield_str}", cols)
|
|
230
246
|
|
|
247
|
+
@cached_hud_shield_key = key
|
|
248
|
+
@cached_hud_shield_line = line
|
|
249
|
+
line
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def hud_ammo_line(cols, player, drops, terminals)
|
|
231
253
|
weapon = player.current_weapon
|
|
232
|
-
|
|
254
|
+
can_pickup = drops.any? { |d| d.in_range?(player.x, player.y) }
|
|
255
|
+
can_use_terminal = terminals.any? do |terminal|
|
|
256
|
+
(terminal[:x] - player.x)**2 + (terminal[:y] - player.y)**2 < Config::TERMINAL_USE_RADIUS**2
|
|
257
|
+
end
|
|
258
|
+
key = [weapon.type_id, weapon.ammo, can_pickup, can_use_terminal, cols]
|
|
259
|
+
return @cached_hud_ammo_line if @cached_hud_ammo_key == key
|
|
233
260
|
|
|
261
|
+
wcolor = weapon.type_id.to_s.start_with?("shock") ? "\e[96m" : "\e[97m"
|
|
234
262
|
if weapon.max_ammo
|
|
235
263
|
ammo_bar_w = 12
|
|
236
264
|
ammo_pct = weapon.ammo.to_f / weapon.max_ammo
|
|
@@ -241,10 +269,6 @@ module Termfront
|
|
|
241
269
|
ammo_str = "#{wcolor}#{weapon.name}\e[0m [\xe2\x88\x9e]"
|
|
242
270
|
end
|
|
243
271
|
|
|
244
|
-
can_pickup = drops.any? { |d| d.in_range?(player.x, player.y) }
|
|
245
|
-
can_use_terminal = terminals.any? do |terminal|
|
|
246
|
-
(terminal[:x] - player.x)**2 + (terminal[:y] - player.y)**2 < Config::TERMINAL_USE_RADIUS**2
|
|
247
|
-
end
|
|
248
272
|
interact_str = if can_use_terminal
|
|
249
273
|
"\e[1;96m[E]Use Terminal\e[0m"
|
|
250
274
|
elsif can_pickup
|
|
@@ -253,26 +277,48 @@ module Termfront
|
|
|
253
277
|
"E:interact"
|
|
254
278
|
end
|
|
255
279
|
|
|
256
|
-
line = "#{ammo_str} T:swap #{interact_str} Space:fire"
|
|
257
|
-
|
|
280
|
+
line = TerminalOutput.fit_ansi("#{ammo_str} T:swap #{interact_str} Space:fire", cols)
|
|
281
|
+
|
|
282
|
+
@cached_hud_ammo_key = key
|
|
283
|
+
@cached_hud_ammo_line = line
|
|
284
|
+
line
|
|
258
285
|
end
|
|
259
286
|
|
|
260
287
|
def build_view_pixels(virt_h, view_w, wtop, wbot, wcol)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
288
|
+
ceil_c = Config::CEIL_C
|
|
289
|
+
floor_c = Config::FLOOR_C
|
|
290
|
+
pixels = @pixels
|
|
291
|
+
|
|
292
|
+
c = 0
|
|
293
|
+
while c < view_w
|
|
294
|
+
wt = wtop[c]
|
|
295
|
+
wb = wbot[c]
|
|
296
|
+
wc = wcol[c]
|
|
297
|
+
|
|
298
|
+
vr = 0
|
|
299
|
+
while vr < wt && vr < virt_h
|
|
300
|
+
pixels[vr][c] = ceil_c
|
|
301
|
+
vr += 1
|
|
302
|
+
end
|
|
303
|
+
while vr < wb && vr < virt_h
|
|
304
|
+
pixels[vr][c] = wc
|
|
305
|
+
vr += 1
|
|
271
306
|
end
|
|
307
|
+
while vr < virt_h
|
|
308
|
+
pixels[vr][c] = floor_c
|
|
309
|
+
vr += 1
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
c += 1
|
|
272
313
|
end
|
|
273
314
|
end
|
|
274
315
|
|
|
275
316
|
def render_view(buf, view_h, view_w, pixels)
|
|
317
|
+
fg_256 = FG_256
|
|
318
|
+
bg_256 = BG_256
|
|
319
|
+
fg_cache = @fg_truecolor_cache
|
|
320
|
+
bg_cache = @bg_truecolor_cache
|
|
321
|
+
|
|
276
322
|
view_h.times do |r|
|
|
277
323
|
vp0 = r * 2
|
|
278
324
|
vp1 = r * 2 + 1
|
|
@@ -286,16 +332,16 @@ module Termfront
|
|
|
286
332
|
bc = bot_row[c]
|
|
287
333
|
|
|
288
334
|
if tc == bc
|
|
289
|
-
if
|
|
335
|
+
if tc.is_a?(Integer)
|
|
290
336
|
if tc != pbg
|
|
291
|
-
buf <<
|
|
337
|
+
buf << bg_256[tc]
|
|
292
338
|
pbg = tc
|
|
293
339
|
pfg = nil
|
|
294
340
|
end
|
|
295
341
|
buf << " "
|
|
296
342
|
else
|
|
297
343
|
if tc != pfg || pbg
|
|
298
|
-
buf <<
|
|
344
|
+
buf << (fg_cache[tc] ||= "\e[38;2;#{tc}m".freeze)
|
|
299
345
|
pfg = tc
|
|
300
346
|
pbg = nil
|
|
301
347
|
end
|
|
@@ -303,7 +349,9 @@ module Termfront
|
|
|
303
349
|
end
|
|
304
350
|
else
|
|
305
351
|
if tc != pfg || bc != pbg
|
|
306
|
-
|
|
352
|
+
fg = tc.is_a?(Integer) ? fg_256[tc] : (fg_cache[tc] ||= "\e[38;2;#{tc}m".freeze)
|
|
353
|
+
bg = bc.is_a?(Integer) ? bg_256[bc] : (bg_cache[bc] ||= "\e[48;2;#{bc}m".freeze)
|
|
354
|
+
buf << fg << bg
|
|
307
355
|
pfg = tc
|
|
308
356
|
pbg = bc
|
|
309
357
|
end
|
|
@@ -319,82 +367,97 @@ module Termfront
|
|
|
319
367
|
|
|
320
368
|
r = Config::RADAR_RADIUS
|
|
321
369
|
diam = r * 2 + 1
|
|
370
|
+
range = Config::RADAR_RANGE
|
|
371
|
+
range_sq = Config::RADAR_RANGE_SQ
|
|
372
|
+
r_sq = r * r
|
|
322
373
|
grid = @radar_grid_template
|
|
323
374
|
|
|
324
375
|
cos_a = Math.cos(-player.angle + Math::PI / 2)
|
|
325
376
|
sin_a = Math.sin(-player.angle + Math::PI / 2)
|
|
326
|
-
|
|
377
|
+
px = player.x
|
|
378
|
+
py = player.y
|
|
379
|
+
|
|
380
|
+
enemy_cells = @radar_enemy_cells
|
|
381
|
+
drop_cells = @radar_drop_cells
|
|
382
|
+
terminal_cells = @radar_terminal_cells
|
|
383
|
+
ally_cells = @radar_ally_cells
|
|
384
|
+
enemy_cells.clear
|
|
385
|
+
drop_cells.clear
|
|
386
|
+
terminal_cells.clear
|
|
387
|
+
ally_cells.clear
|
|
388
|
+
|
|
327
389
|
enemies.each do |e|
|
|
328
390
|
next unless e.alive
|
|
329
391
|
|
|
330
|
-
ex = e.x -
|
|
331
|
-
ey = e.y -
|
|
332
|
-
next if ex * ex + ey * ey >
|
|
392
|
+
ex = e.x - px
|
|
393
|
+
ey = e.y - py
|
|
394
|
+
next if ex * ex + ey * ey > range_sq
|
|
333
395
|
|
|
334
396
|
rx = -(ex * cos_a - ey * sin_a)
|
|
335
397
|
ry = -(ex * sin_a + ey * cos_a)
|
|
336
|
-
sx = r + (rx /
|
|
337
|
-
sy = r + (ry /
|
|
338
|
-
next
|
|
398
|
+
sx = r + (rx / range * r).round
|
|
399
|
+
sy = r + (ry / range * r).round
|
|
400
|
+
next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
|
|
339
401
|
|
|
340
|
-
|
|
341
|
-
|
|
402
|
+
dxr = sx - r
|
|
403
|
+
dyr = sy - r
|
|
404
|
+
next if dxr * dxr + dyr * dyr > r_sq
|
|
342
405
|
|
|
343
|
-
enemy_cells[
|
|
406
|
+
enemy_cells[sy * diam + sx] = e.sprite_id
|
|
344
407
|
end
|
|
345
408
|
|
|
346
|
-
drop_cells = {}
|
|
347
409
|
drops.each do |d|
|
|
348
|
-
ex = d.x -
|
|
349
|
-
ey = d.y -
|
|
350
|
-
next if ex * ex + ey * ey >
|
|
410
|
+
ex = d.x - px
|
|
411
|
+
ey = d.y - py
|
|
412
|
+
next if ex * ex + ey * ey > range_sq
|
|
351
413
|
|
|
352
414
|
rx = -(ex * cos_a - ey * sin_a)
|
|
353
415
|
ry = -(ex * sin_a + ey * cos_a)
|
|
354
|
-
sx = r + (rx /
|
|
355
|
-
sy = r + (ry /
|
|
356
|
-
next
|
|
416
|
+
sx = r + (rx / range * r).round
|
|
417
|
+
sy = r + (ry / range * r).round
|
|
418
|
+
next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
|
|
357
419
|
|
|
358
|
-
|
|
359
|
-
|
|
420
|
+
dxr = sx - r
|
|
421
|
+
dyr = sy - r
|
|
422
|
+
next if dxr * dxr + dyr * dyr > r_sq
|
|
360
423
|
|
|
361
|
-
drop_cells[
|
|
424
|
+
drop_cells[sy * diam + sx] = d
|
|
362
425
|
end
|
|
363
426
|
|
|
364
|
-
terminal_cells = {}
|
|
365
427
|
terminals.each do |terminal|
|
|
366
|
-
ex = terminal[:x] -
|
|
367
|
-
ey = terminal[:y] -
|
|
368
|
-
next if ex * ex + ey * ey >
|
|
428
|
+
ex = terminal[:x] - px
|
|
429
|
+
ey = terminal[:y] - py
|
|
430
|
+
next if ex * ex + ey * ey > range_sq
|
|
369
431
|
|
|
370
432
|
rx = -(ex * cos_a - ey * sin_a)
|
|
371
433
|
ry = -(ex * sin_a + ey * cos_a)
|
|
372
|
-
sx = r + (rx /
|
|
373
|
-
sy = r + (ry /
|
|
374
|
-
next
|
|
434
|
+
sx = r + (rx / range * r).round
|
|
435
|
+
sy = r + (ry / range * r).round
|
|
436
|
+
next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
|
|
375
437
|
|
|
376
|
-
|
|
377
|
-
|
|
438
|
+
dxr = sx - r
|
|
439
|
+
dyr = sy - r
|
|
440
|
+
next if dxr * dxr + dyr * dyr > r_sq
|
|
378
441
|
|
|
379
|
-
terminal_cells[
|
|
442
|
+
terminal_cells[sy * diam + sx] = terminal
|
|
380
443
|
end
|
|
381
444
|
|
|
382
|
-
ally_cells = {}
|
|
383
445
|
allies.each do |ally|
|
|
384
|
-
ex = ally.x -
|
|
385
|
-
ey = ally.y -
|
|
386
|
-
next if ex * ex + ey * ey >
|
|
446
|
+
ex = ally.x - px
|
|
447
|
+
ey = ally.y - py
|
|
448
|
+
next if ex * ex + ey * ey > range_sq
|
|
387
449
|
|
|
388
450
|
rx = -(ex * cos_a - ey * sin_a)
|
|
389
451
|
ry = -(ex * sin_a + ey * cos_a)
|
|
390
|
-
sx = r + (rx /
|
|
391
|
-
sy = r + (ry /
|
|
392
|
-
next
|
|
452
|
+
sx = r + (rx / range * r).round
|
|
453
|
+
sy = r + (ry / range * r).round
|
|
454
|
+
next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
|
|
393
455
|
|
|
394
|
-
|
|
395
|
-
|
|
456
|
+
dxr = sx - r
|
|
457
|
+
dyr = sy - r
|
|
458
|
+
next if dxr * dxr + dyr * dyr > r_sq
|
|
396
459
|
|
|
397
|
-
ally_cells[
|
|
460
|
+
ally_cells[sy * diam + sx] = true
|
|
398
461
|
end
|
|
399
462
|
|
|
400
463
|
alive_count = enemies.count(&:alive)
|
|
@@ -402,21 +465,25 @@ module Termfront
|
|
|
402
465
|
info_lines = [
|
|
403
466
|
"Enemies: #{alive_count}/#{total_count}",
|
|
404
467
|
"Heading: #{format("%.0f", (player.angle % (Math::PI * 2)) * 180 / Math::PI)}\xC2\xB0",
|
|
405
|
-
"Pos: (#{"%.1f" %
|
|
468
|
+
"Pos: (#{"%.1f" % px}, #{"%.1f" % py}) T:terminal"
|
|
406
469
|
]
|
|
407
470
|
|
|
408
|
-
|
|
471
|
+
row = 0
|
|
472
|
+
while row < radar_h
|
|
409
473
|
line = @radar_line_buf.clear
|
|
410
474
|
if row < diam
|
|
411
475
|
line << " "
|
|
412
|
-
|
|
413
|
-
|
|
476
|
+
cx = 0
|
|
477
|
+
base = row * diam
|
|
478
|
+
while cx < diam
|
|
479
|
+
key = base + cx
|
|
480
|
+
if (etype = enemy_cells[key])
|
|
414
481
|
line << (etype == :executor ? RADAR_EXECUTOR : RADAR_CRAWLER)
|
|
415
|
-
elsif ally_cells[
|
|
482
|
+
elsif ally_cells[key]
|
|
416
483
|
line << RADAR_ALLY
|
|
417
|
-
elsif (drop = drop_cells[
|
|
484
|
+
elsif (drop = drop_cells[key])
|
|
418
485
|
line << radar_drop_glyph(drop)
|
|
419
|
-
elsif terminal_cells[
|
|
486
|
+
elsif terminal_cells[key]
|
|
420
487
|
line << RADAR_TERMINAL
|
|
421
488
|
elsif row == r && cx == r
|
|
422
489
|
line << RADAR_PLAYER
|
|
@@ -425,28 +492,36 @@ module Termfront
|
|
|
425
492
|
else
|
|
426
493
|
line << grid[row][cx]
|
|
427
494
|
end
|
|
495
|
+
cx += 1
|
|
428
496
|
end
|
|
429
497
|
line << (row < info_lines.size ? " #{info_lines[row]}" : "")
|
|
430
498
|
end
|
|
431
499
|
buf << TerminalOutput.fit_ansi(line, cols)
|
|
432
500
|
buf << "\r\n" if row < radar_h - 1
|
|
501
|
+
row += 1
|
|
433
502
|
end
|
|
434
503
|
end
|
|
435
504
|
|
|
436
505
|
def overlay_enemies_3d(pixels, view_h, view_w, dists, player, enemies, projectiles, drops)
|
|
437
506
|
dx = Math.cos(player.angle)
|
|
438
507
|
dy = Math.sin(player.angle)
|
|
439
|
-
|
|
440
|
-
|
|
508
|
+
tan_half_fov = Math.tan(Config::FOV / 2.0)
|
|
509
|
+
px = -dy * tan_half_fov
|
|
510
|
+
py = dx * tan_half_fov
|
|
441
511
|
virt_h = view_h * 2
|
|
512
|
+
half_virt_h = virt_h / 2
|
|
513
|
+
half_view_w = view_w / 2.0
|
|
514
|
+
view_w_last = view_w - 1
|
|
442
515
|
inv = 1.0 / (px * dy - py * dx)
|
|
516
|
+
player_x = player.x
|
|
517
|
+
player_y = player.y
|
|
443
518
|
|
|
444
519
|
@enemy_sprites.clear
|
|
445
520
|
enemies.each do |e|
|
|
446
521
|
next unless e.alive
|
|
447
522
|
|
|
448
|
-
ex = e.x -
|
|
449
|
-
ey = e.y -
|
|
523
|
+
ex = e.x - player_x
|
|
524
|
+
ey = e.y - player_y
|
|
450
525
|
tx = inv * (dy * ex - dx * ey)
|
|
451
526
|
tz = inv * (-py * ex + px * ey)
|
|
452
527
|
next if tz < 0.2
|
|
@@ -456,8 +531,8 @@ module Termfront
|
|
|
456
531
|
|
|
457
532
|
@proj_sprites.clear
|
|
458
533
|
projectiles.each do |p|
|
|
459
|
-
ex = p.x -
|
|
460
|
-
ey = p.y -
|
|
534
|
+
ex = p.x - player_x
|
|
535
|
+
ey = p.y - player_y
|
|
461
536
|
tx = inv * (dy * ex - dx * ey)
|
|
462
537
|
tz = inv * (-py * ex + px * ey)
|
|
463
538
|
next if tz < 0.2
|
|
@@ -468,78 +543,94 @@ module Termfront
|
|
|
468
543
|
@enemy_sprites.sort! { |a, b| b[0] <=> a[0] }
|
|
469
544
|
|
|
470
545
|
@enemy_sprites.each do |tz, tx, e|
|
|
471
|
-
sx = (
|
|
546
|
+
sx = (half_view_w * (1 + tx / tz)).to_i
|
|
472
547
|
sprite_h = (virt_h / tz).to_i
|
|
473
|
-
draw_top =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
548
|
+
draw_top = half_virt_h - sprite_h / 2
|
|
549
|
+
draw_top = 0 if draw_top < 0
|
|
550
|
+
draw_bot = half_virt_h + sprite_h / 2
|
|
551
|
+
draw_bot = virt_h if draw_bot > virt_h
|
|
552
|
+
sprite_w = sprite_h / 2
|
|
553
|
+
start_x = sx - sprite_w / 2
|
|
554
|
+
start_x = 0 if start_x < 0
|
|
555
|
+
end_x = sx + sprite_w / 2
|
|
556
|
+
end_x = view_w_last if end_x > view_w_last
|
|
478
557
|
|
|
479
558
|
actual_h = draw_bot - draw_top
|
|
480
559
|
actual_w = end_x - start_x + 1
|
|
481
560
|
next if actual_h < 1 || actual_w < 1
|
|
482
561
|
|
|
483
|
-
|
|
562
|
+
sprite_id = e.sprite_id
|
|
563
|
+
fallback_color = sprite_id == :executor ? "100;60;200" : "220;140;30"
|
|
484
564
|
use_shape = actual_h >= 6
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
565
|
+
r_top = (draw_top + 1) >> 1
|
|
566
|
+
r_bot = draw_bot >> 1
|
|
567
|
+
actual_h_f = actual_h.to_f
|
|
568
|
+
actual_w_f = actual_w.to_f
|
|
569
|
+
|
|
570
|
+
c = start_x
|
|
571
|
+
while c <= end_x
|
|
572
|
+
if c >= 0 && c < view_w && dists[c] >= tz
|
|
573
|
+
nx = (c - start_x) / actual_w_f
|
|
574
|
+
|
|
575
|
+
r = r_top
|
|
576
|
+
while r < r_bot
|
|
577
|
+
vp0 = r << 1
|
|
578
|
+
vp1 = vp0 + 1
|
|
579
|
+
top_in = vp0 >= draw_top && vp0 < draw_bot
|
|
580
|
+
bot_in = vp1 >= draw_top && vp1 < draw_bot
|
|
581
|
+
|
|
582
|
+
if top_in || bot_in
|
|
583
|
+
if use_shape
|
|
584
|
+
ny0 = top_in ? (vp0 - draw_top) / actual_h_f : nil
|
|
585
|
+
ny1 = bot_in ? (vp1 - draw_top) / actual_h_f : nil
|
|
586
|
+
top_color = ny0 ? Sprite.for(sprite_id, nx, ny0) : nil
|
|
587
|
+
bot_color = ny1 ? Sprite.for(sprite_id, nx, ny1) : nil
|
|
588
|
+
if top_color || bot_color
|
|
589
|
+
pixels[vp0][c] = top_color if top_color
|
|
590
|
+
pixels[vp1][c] = bot_color if bot_color
|
|
591
|
+
end
|
|
592
|
+
else
|
|
593
|
+
pixels[vp0][c] = fallback_color if top_in
|
|
594
|
+
pixels[vp1][c] = fallback_color if bot_in
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
r += 1
|
|
513
598
|
end
|
|
514
599
|
end
|
|
600
|
+
c += 1
|
|
515
601
|
end
|
|
516
602
|
|
|
517
603
|
next unless e.max_hp > 1
|
|
518
604
|
|
|
519
|
-
bar_row =
|
|
605
|
+
bar_row = r_top - 1
|
|
520
606
|
next unless bar_row >= 0 && bar_row < view_h
|
|
521
607
|
|
|
522
|
-
bar_w =
|
|
523
|
-
bar_sx =
|
|
524
|
-
|
|
608
|
+
bar_w = actual_w > 2 ? actual_w : 2
|
|
609
|
+
bar_sx = sx - bar_w / 2
|
|
610
|
+
bar_sx = 0 if bar_sx < 0
|
|
611
|
+
bar_ex = bar_sx + bar_w - 1
|
|
612
|
+
bar_ex = view_w_last if bar_ex > view_w_last
|
|
525
613
|
hp_pct = e.hp.to_f / e.max_hp
|
|
526
614
|
filled = (hp_pct * (bar_ex - bar_sx + 1)).ceil
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
615
|
+
bar_vp0 = bar_row << 1
|
|
616
|
+
bar_vp1 = bar_vp0 + 1
|
|
617
|
+
c = bar_sx
|
|
618
|
+
while c <= bar_ex
|
|
619
|
+
if c >= 0 && c < view_w && dists[c] >= tz
|
|
620
|
+
ci = c - bar_sx
|
|
621
|
+
color = ci < filled ? "0;200;0" : "200;0;0"
|
|
622
|
+
pixels[bar_vp0][c] = color
|
|
623
|
+
pixels[bar_vp1][c] = color
|
|
624
|
+
end
|
|
625
|
+
c += 1
|
|
535
626
|
end
|
|
536
627
|
end
|
|
537
628
|
|
|
538
629
|
# Render weapon drops
|
|
539
630
|
@drop_sprites.clear
|
|
540
631
|
drops.each do |d|
|
|
541
|
-
ex = d.x -
|
|
542
|
-
ey = d.y -
|
|
632
|
+
ex = d.x - player_x
|
|
633
|
+
ey = d.y - player_y
|
|
543
634
|
tx = inv * (dy * ex - dx * ey)
|
|
544
635
|
tz = inv * (-py * ex + px * ey)
|
|
545
636
|
next if tz < 0.2
|
|
@@ -549,70 +640,83 @@ module Termfront
|
|
|
549
640
|
@drop_sprites.sort! { |a, b| b[0] <=> a[0] }
|
|
550
641
|
|
|
551
642
|
@drop_sprites.each do |tz, tx, d|
|
|
552
|
-
sx = (
|
|
553
|
-
sprite_h = (virt_h / tz * 0.3).to_i.clamp(2,
|
|
554
|
-
ground = (
|
|
555
|
-
draw_bot =
|
|
556
|
-
draw_top =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
643
|
+
sx = (half_view_w * (1 + tx / tz)).to_i
|
|
644
|
+
sprite_h = (virt_h / tz * 0.3).to_i.clamp(2, half_virt_h)
|
|
645
|
+
ground = (half_virt_h + virt_h / tz * 0.35).to_i
|
|
646
|
+
draw_bot = ground < virt_h ? ground : virt_h
|
|
647
|
+
draw_top = draw_bot - sprite_h
|
|
648
|
+
draw_top = 0 if draw_top < 0
|
|
649
|
+
sprite_w = (sprite_h / 2).clamp(1, 6)
|
|
650
|
+
start_x = sx - sprite_w / 2
|
|
651
|
+
start_x = 0 if start_x < 0
|
|
652
|
+
end_x = sx + sprite_w / 2
|
|
653
|
+
end_x = view_w_last if end_x > view_w_last
|
|
560
654
|
|
|
561
655
|
color = d.sprite_color
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
656
|
+
r_top = (draw_top + 1) >> 1
|
|
657
|
+
r_bot = draw_bot >> 1
|
|
658
|
+
|
|
659
|
+
c = start_x
|
|
660
|
+
while c <= end_x
|
|
661
|
+
if c >= 0 && c < view_w && dists[c] >= tz
|
|
662
|
+
r = r_top
|
|
663
|
+
while r < r_bot
|
|
664
|
+
if r >= 0 && r < view_h
|
|
665
|
+
vp0 = r << 1
|
|
666
|
+
vp1 = vp0 + 1
|
|
667
|
+
top_in = vp0 >= draw_top && vp0 < draw_bot
|
|
668
|
+
bot_in = vp1 >= draw_top && vp1 < draw_bot
|
|
669
|
+
if top_in || bot_in
|
|
670
|
+
pixels[vp0][c] = color if top_in
|
|
671
|
+
pixels[vp1][c] = color if bot_in
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
r += 1
|
|
675
|
+
end
|
|
580
676
|
end
|
|
677
|
+
c += 1
|
|
581
678
|
end
|
|
582
679
|
end
|
|
583
680
|
|
|
584
681
|
# Render projectiles
|
|
585
682
|
@proj_sprites.sort! { |a, b| b[0] <=> a[0] }
|
|
586
683
|
@proj_sprites.each do |tz, tx, p|
|
|
587
|
-
sx = (
|
|
684
|
+
sx = (half_view_w * (1 + tx / tz)).to_i
|
|
588
685
|
pw = (4.0 / tz).ceil.clamp(1, 5)
|
|
589
686
|
ph = (virt_h / tz * 0.15).ceil.clamp(2, 6)
|
|
590
|
-
|
|
591
|
-
draw_top =
|
|
592
|
-
draw_bot =
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
687
|
+
draw_top = half_virt_h - ph / 2
|
|
688
|
+
draw_top = 0 if draw_top < 0
|
|
689
|
+
draw_bot = half_virt_h + ph / 2
|
|
690
|
+
draw_bot = draw_top + 2 if draw_bot < draw_top + 2
|
|
691
|
+
draw_bot = virt_h if draw_bot > virt_h
|
|
692
|
+
start_x = sx - pw / 2
|
|
693
|
+
start_x = 0 if start_x < 0
|
|
694
|
+
end_x = sx + pw / 2
|
|
695
|
+
end_x = view_w_last if end_x > view_w_last
|
|
696
|
+
proj_color = p.type == :executor ? "94;94;255" : "255;210;80"
|
|
697
|
+
r_top = (draw_top + 1) >> 1
|
|
698
|
+
r_bot = draw_bot >> 1
|
|
699
|
+
r_bot = r_top + 1 if r_bot < r_top + 1
|
|
700
|
+
|
|
701
|
+
c = start_x
|
|
702
|
+
while c <= end_x
|
|
703
|
+
if c >= 0 && c < view_w && dists[c] >= tz
|
|
704
|
+
r = r_top
|
|
705
|
+
while r < r_bot
|
|
706
|
+
if r >= 0 && r < view_h
|
|
707
|
+
vp0 = r << 1
|
|
708
|
+
vp1 = vp0 + 1
|
|
709
|
+
top_in = vp0 >= draw_top && vp0 < draw_bot
|
|
710
|
+
bot_in = vp1 >= draw_top && vp1 < draw_bot
|
|
711
|
+
if top_in || bot_in
|
|
712
|
+
pixels[vp0][c] = proj_color if top_in
|
|
713
|
+
pixels[vp1][c] = proj_color if bot_in
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
r += 1
|
|
717
|
+
end
|
|
615
718
|
end
|
|
719
|
+
c += 1
|
|
616
720
|
end
|
|
617
721
|
end
|
|
618
722
|
end
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module Termfront
|
|
4
4
|
module TerminalOutput
|
|
5
5
|
module_function
|
|
6
|
-
ANSI_PATTERN = /\e\[[0-9;]*[A-Za-z]/.freeze
|
|
7
6
|
|
|
8
7
|
def begin_frame(home: false, clear: false)
|
|
9
8
|
buf = +""
|
|
@@ -17,25 +16,41 @@ module Termfront
|
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
def fit_ansi(text, width)
|
|
20
|
-
visible = 0
|
|
21
19
|
out = +""
|
|
22
|
-
|
|
20
|
+
visible = 0
|
|
21
|
+
any_ansi = false
|
|
22
|
+
i = 0
|
|
23
|
+
len = text.bytesize
|
|
24
|
+
segment_start = 0
|
|
23
25
|
|
|
24
|
-
while
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
next
|
|
29
|
-
end
|
|
26
|
+
while i < len
|
|
27
|
+
byte = text.getbyte(i)
|
|
28
|
+
if byte == 0x1B
|
|
29
|
+
out << text.byteslice(segment_start, i - segment_start) if i > segment_start
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
esc_start = i
|
|
32
|
+
i += 1
|
|
33
|
+
while i < len
|
|
34
|
+
b = text.getbyte(i)
|
|
35
|
+
i += 1
|
|
36
|
+
break if (b >= 65 && b <= 90) || (b >= 97 && b <= 122)
|
|
37
|
+
end
|
|
38
|
+
out << text.byteslice(esc_start, i - esc_start)
|
|
39
|
+
any_ansi = true
|
|
40
|
+
segment_start = i
|
|
41
|
+
else
|
|
42
|
+
if (byte & 0xC0) != 0x80
|
|
43
|
+
break if visible >= width
|
|
44
|
+
|
|
45
|
+
visible += 1
|
|
46
|
+
end
|
|
47
|
+
i += 1
|
|
48
|
+
end
|
|
35
49
|
end
|
|
36
50
|
|
|
37
|
-
out <<
|
|
38
|
-
out <<
|
|
51
|
+
out << text.byteslice(segment_start, i - segment_start) if i > segment_start
|
|
52
|
+
out << "\e[0m" if any_ansi
|
|
53
|
+
(width - visible).times { out << " " } if visible < width
|
|
39
54
|
out
|
|
40
55
|
end
|
|
41
56
|
|
|
@@ -7,12 +7,30 @@ module Termfront
|
|
|
7
7
|
[13.0, 8.0], [10.0, 8.5], [5.0, 8.5], [2.5, 5.0]
|
|
8
8
|
].freeze
|
|
9
9
|
|
|
10
|
+
TITLE_TEXT = "T E R M F R O N T"
|
|
11
|
+
SUB_TEXT = "Terminal FPS"
|
|
12
|
+
MENU_ITEMS = ["[P] PvP", "[F] Wavesfight", "[C] Campaign", "[S] Training", "[Q] Quit"].freeze
|
|
13
|
+
|
|
10
14
|
def initialize(stdout)
|
|
11
15
|
@stdout = stdout
|
|
12
16
|
@title_spin = 0.0
|
|
13
17
|
@demo_wp_idx = 0
|
|
14
18
|
@demo_wp_t = 0.0
|
|
15
19
|
@demo_fire = 0
|
|
20
|
+
|
|
21
|
+
mission_class = Mission::Base.campaign.first
|
|
22
|
+
if mission_class
|
|
23
|
+
m = mission_class.new
|
|
24
|
+
@demo_map = m.map_data.map { |row| row.is_a?(Array) ? row : row.chars }.freeze
|
|
25
|
+
@demo_map_h = @demo_map.size
|
|
26
|
+
@demo_map_w = @demo_map[0].size
|
|
27
|
+
@demo_enemies = m.enemy_defs.freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@static_lines_cols = 0
|
|
31
|
+
@static_title_line = nil
|
|
32
|
+
@static_sub_line = nil
|
|
33
|
+
@static_menu_lines = nil
|
|
16
34
|
end
|
|
17
35
|
|
|
18
36
|
def show
|
|
@@ -24,11 +42,15 @@ module Termfront
|
|
|
24
42
|
TerminalOutput.write_all(@stdout, TerminalOutput.begin_frame(home: true, clear: true) + TerminalOutput.end_frame)
|
|
25
43
|
|
|
26
44
|
STDIN.raw do |stdin|
|
|
45
|
+
last_render = clock - Config::RENDER_DT
|
|
27
46
|
loop do
|
|
28
47
|
now = clock
|
|
29
48
|
@title_spin += 0.015
|
|
30
49
|
|
|
31
|
-
|
|
50
|
+
if now - last_render >= Config::RENDER_DT
|
|
51
|
+
render
|
|
52
|
+
last_render = now
|
|
53
|
+
end
|
|
32
54
|
|
|
33
55
|
while IO.select([stdin], nil, nil, 0)
|
|
34
56
|
begin
|
|
@@ -60,6 +82,20 @@ module Termfront
|
|
|
60
82
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
61
83
|
end
|
|
62
84
|
|
|
85
|
+
def ensure_static_lines(cols)
|
|
86
|
+
return if @static_lines_cols == cols
|
|
87
|
+
|
|
88
|
+
tc = [(cols - TITLE_TEXT.size) / 2 + 1, 1].max
|
|
89
|
+
sc = [(cols - SUB_TEXT.size) / 2 + 1, 1].max
|
|
90
|
+
@static_title_line = TerminalOutput.fit_ansi("#{" " * (tc - 1)}\e[1;38;2;120;140;255m#{TITLE_TEXT}\e[0m", cols)
|
|
91
|
+
@static_sub_line = TerminalOutput.fit_ansi("#{" " * (sc - 1)}\e[38;2;80;80;120m#{SUB_TEXT}\e[0m", cols)
|
|
92
|
+
@static_menu_lines = MENU_ITEMS.map do |item|
|
|
93
|
+
ic = [(cols - item.size) / 2 + 1, 1].max
|
|
94
|
+
TerminalOutput.fit_ansi("#{" " * (ic - 1)}\e[97m#{item}\e[0m", cols)
|
|
95
|
+
end
|
|
96
|
+
@static_lines_cols = cols
|
|
97
|
+
end
|
|
98
|
+
|
|
63
99
|
def render
|
|
64
100
|
rows, cols = @stdout.winsize
|
|
65
101
|
rows = [rows, 10].max
|
|
@@ -76,13 +112,11 @@ module Termfront
|
|
|
76
112
|
virt_h = th * 2
|
|
77
113
|
color = Array.new(tw * virt_h, nil)
|
|
78
114
|
|
|
79
|
-
|
|
80
|
-
return unless mission
|
|
115
|
+
return unless @demo_map
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
dm_w = demo_map[0].size
|
|
117
|
+
demo_map = @demo_map
|
|
118
|
+
dm_h = @demo_map_h
|
|
119
|
+
dm_w = @demo_map_w
|
|
86
120
|
|
|
87
121
|
@demo_wp_t += Config::DEMO_SPEED
|
|
88
122
|
if @demo_wp_t >= 1.0
|
|
@@ -184,7 +218,7 @@ module Termfront
|
|
|
184
218
|
end
|
|
185
219
|
|
|
186
220
|
# Demo enemies
|
|
187
|
-
demo_enemies =
|
|
221
|
+
demo_enemies = @demo_enemies
|
|
188
222
|
ddx = Math.cos(cam_a)
|
|
189
223
|
ddy = Math.sin(cam_a)
|
|
190
224
|
ppx = -ddy * Math.tan(fov / 2.0)
|
|
@@ -269,22 +303,16 @@ module Termfront
|
|
|
269
303
|
lines[r] = TerminalOutput.fit_ansi(line, cols)
|
|
270
304
|
end
|
|
271
305
|
|
|
272
|
-
# Title text
|
|
306
|
+
# Title text + menu items (memoized per cols)
|
|
307
|
+
ensure_static_lines(cols)
|
|
308
|
+
|
|
273
309
|
title_row = [[th + 1, rows - 4].min, 1].max
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
# Menu items
|
|
282
|
-
items = ["[P] PvP", "[F] Wavesfight", "[C] Campaign", "[S] Training", "[Q] Quit"]
|
|
283
|
-
items_count_for_menu = items.size
|
|
284
|
-
menu_row = [[title_row + 2, rows - items_count_for_menu].min, 1].max
|
|
285
|
-
items.each_with_index do |item, i|
|
|
286
|
-
ic = [(cols - item.size) / 2 + 1, 1].max
|
|
287
|
-
lines[menu_row + i - 1] = TerminalOutput.fit_ansi("#{" " * (ic - 1)}\e[97m#{item}\e[0m", cols)
|
|
310
|
+
lines[title_row - 1] = @static_title_line
|
|
311
|
+
lines[title_row] = @static_sub_line
|
|
312
|
+
|
|
313
|
+
menu_row = [[title_row + 2, rows - MENU_ITEMS.size].min, 1].max
|
|
314
|
+
@static_menu_lines.each_with_index do |menu_line, i|
|
|
315
|
+
lines[menu_row + i - 1] = menu_line
|
|
288
316
|
end
|
|
289
317
|
|
|
290
318
|
lines.each_with_index do |line, index|
|
data/lib/termfront/version.rb
CHANGED