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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68852aaca6be7127d8b644aae4670a56e72d5528b249dc081d72ba0d22c5f74a
4
- data.tar.gz: 440209c869681acd0f9d77d65fb367e6d055c2ffe3a5a57821dd530912199ecd
3
+ metadata.gz: d84ea40d760b9d62c0037b83a0b83576725a12a16c3aa2937e16025e6a798015
4
+ data.tar.gz: e33b7fc4bde17b2924ea6a1f30bb11f6f8d30fbba75bd9f9130c773b1b1cf33a
5
5
  SHA512:
6
- metadata.gz: 6efb979956fa649ee558ad0190547fd70c62fda5ef8b8ca4dc06f513c15c87c02c74151756486615a0cc21301e6b45d6f62e01e58230ae28b574ceb3570053ae
7
- data.tar.gz: 12dfe9fde65aa1517facf4e7e21a37b6db9550e5cc99663801efb33db74f1a1b88af2503c6c6c54be70d60374328009f85334422413737514845602091f70893
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
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
5
+
4
6
  require "termfront"
5
7
 
6
8
  Termfront.start
@@ -3,6 +3,7 @@
3
3
  module Termfront
4
4
  module Config
5
5
  FRAME_DT = 1.0 / 60.0
6
+ RENDER_DT = 1.0 / 30.0
6
7
  FOV = 66.0 * Math::PI / 180.0
7
8
  PLAYER_RADIUS = 0.2
8
9
  KEY_TIMEOUT = 5
@@ -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
- @renderer.render(
150
- player: @player, map: @map,
151
- enemies: @enemies, projectiles: @projectiles,
152
- drops: @player.drops, terminals: @terminals
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
- @renderer.render(
195
- player: @player, map: @map,
196
- enemies: @enemies, projectiles: @projectiles,
197
- drops: @player.drops, terminals: @terminals,
198
- status_line: " WAVE #{@wave} #{Enemy::Base::DIFFICULTIES[@difficulty][:name]}"
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
- tc = [(cols - title.size) / 2 + 1, 1].max
552
- lines[1] = TerminalOutput.fit_ansi("#{" " * (tc - 1)}\e[1;38;2;120;140;255m#{title}\e[0m", cols)
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
- diff_colors = ["\e[92m", "\e[93m", "\e[38;2;255;165;0m", "\e[91m"]
556
- diff_label = "< #{diff[:name]} >"
557
- dc = [(cols - diff_label.size) / 2 + 1, 1].max
558
- lines[2] = TerminalOutput.fit_ansi("#{" " * (dc - 1)}#{diff_colors[@difficulty]}#{diff_label}\e[0m", cols)
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
- label = " #{i + 1}. #{m.name}"
564
- lc = [(cols - 40) / 2 + 1, 1].max
565
- text = if i == selected
566
- "\e[1;97;44m> #{label.strip.ljust(38)}\e[0m"
567
- else
568
- "\e[97m #{label.strip.ljust(38)}\e[0m"
569
- end
570
- lines[row - 1] = TerminalOutput.fit_ansi("#{" " * (lc - 1)}#{text}", cols)
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
- m = missions[selected].new
575
- briefing = m.briefing
576
- bc = [(cols - briefing.size) / 2 + 1, 1].max
577
- lines[brief_row - 1] = TerminalOutput.fit_ansi("#{" " * (bc - 1)}\e[38;2;180;180;200m#{briefing}\e[0m", cols)
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
- edefs = m.enemy_defs
581
- base_crawler = edefs.count { |e| e[4] == :crawler }
582
- base_executor = edefs.count { |e| e[4] == :executor }
583
- extra = diff[:extra_enemies]
584
- extra_crawler = 0
585
- extra_executor = 0
586
- extra.times do |i|
587
- src_type = edefs[i % edefs.size][4]
588
- src_type == :crawler ? (extra_crawler += 1) : (extra_executor += 1)
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
- ctrl = "Up/Down: Select Left/Right: Difficulty Enter/1-5: Start Q: Back"
600
- cc = [(cols - ctrl.size) / 2 + 1, 1].max
601
- lines[ctrl_row - 1] = TerminalOutput.fit_ansi("#{" " * (cc - 1)}\e[38;2;100;100;120m#{ctrl}\e[0m", cols)
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
@@ -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
- buf << TerminalOutput.fit_ansi("#{" " * pad}#{shield_str}", cols) << "\r\n"
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
- wcolor = weapon.type_id.to_s.start_with?("shock") ? "\e[96m" : "\e[97m"
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
- buf << TerminalOutput.fit_ansi(line, cols) << "\r\n"
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
- virt_h.times do |vr|
262
- row = @pixels[vr]
263
- view_w.times do |c|
264
- row[c] = if vr < wtop[c]
265
- Config::CEIL_C
266
- elsif vr < wbot[c]
267
- wcol[c]
268
- else
269
- Config::FLOOR_C
270
- end
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 bg_only?(tc)
335
+ if tc.is_a?(Integer)
290
336
  if tc != pbg
291
- buf << ansi_bg(tc)
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 << ansi_fg(tc)
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
- buf << ansi_fg(tc) << ansi_bg(bc)
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
- enemy_cells = {}
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 - player.x
331
- ey = e.y - player.y
332
- next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
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 / Config::RADAR_RANGE * r).round
337
- sy = r + (ry / Config::RADAR_RANGE * r).round
338
- next unless sx.between?(0, diam - 1) && sy.between?(0, diam - 1)
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
- d2 = (sx - r)**2 + (sy - r)**2
341
- next if d2 > r * r
402
+ dxr = sx - r
403
+ dyr = sy - r
404
+ next if dxr * dxr + dyr * dyr > r_sq
342
405
 
343
- enemy_cells[[sy, sx]] = e.sprite_id
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 - player.x
349
- ey = d.y - player.y
350
- next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
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 / Config::RADAR_RANGE * r).round
355
- sy = r + (ry / Config::RADAR_RANGE * r).round
356
- next unless sx.between?(0, diam - 1) && sy.between?(0, diam - 1)
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
- d2 = (sx - r)**2 + (sy - r)**2
359
- next if d2 > r * r
420
+ dxr = sx - r
421
+ dyr = sy - r
422
+ next if dxr * dxr + dyr * dyr > r_sq
360
423
 
361
- drop_cells[[sy, sx]] = d
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] - player.x
367
- ey = terminal[:y] - player.y
368
- next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
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 / Config::RADAR_RANGE * r).round
373
- sy = r + (ry / Config::RADAR_RANGE * r).round
374
- next unless sx.between?(0, diam - 1) && sy.between?(0, diam - 1)
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
- d2 = (sx - r)**2 + (sy - r)**2
377
- next if d2 > r * r
438
+ dxr = sx - r
439
+ dyr = sy - r
440
+ next if dxr * dxr + dyr * dyr > r_sq
378
441
 
379
- terminal_cells[[sy, sx]] = terminal
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 - player.x
385
- ey = ally.y - player.y
386
- next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
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 / Config::RADAR_RANGE * r).round
391
- sy = r + (ry / Config::RADAR_RANGE * r).round
392
- next unless sx.between?(0, diam - 1) && sy.between?(0, diam - 1)
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
- d2 = (sx - r)**2 + (sy - r)**2
395
- next if d2 > r * r
456
+ dxr = sx - r
457
+ dyr = sy - r
458
+ next if dxr * dxr + dyr * dyr > r_sq
396
459
 
397
- ally_cells[[sy, sx]] = true
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" % player.x}, #{"%.1f" % player.y}) T:terminal"
468
+ "Pos: (#{"%.1f" % px}, #{"%.1f" % py}) T:terminal"
406
469
  ]
407
470
 
408
- radar_h.times do |row|
471
+ row = 0
472
+ while row < radar_h
409
473
  line = @radar_line_buf.clear
410
474
  if row < diam
411
475
  line << " "
412
- diam.times do |cx|
413
- if (etype = enemy_cells[[row, cx]])
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[[row, cx]]
482
+ elsif ally_cells[key]
416
483
  line << RADAR_ALLY
417
- elsif (drop = drop_cells[[row, cx]])
484
+ elsif (drop = drop_cells[key])
418
485
  line << radar_drop_glyph(drop)
419
- elsif terminal_cells[[row, cx]]
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
- px = -dy * Math.tan(Config::FOV / 2.0)
440
- py = dx * Math.tan(Config::FOV / 2.0)
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 - player.x
449
- ey = e.y - player.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 - player.x
460
- ey = p.y - player.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 = ((view_w / 2.0) * (1 + tx / tz)).to_i
546
+ sx = (half_view_w * (1 + tx / tz)).to_i
472
547
  sprite_h = (virt_h / tz).to_i
473
- draw_top = [(virt_h / 2 - sprite_h / 2), 0].max
474
- draw_bot = [(virt_h / 2 + sprite_h / 2), virt_h].min
475
- sprite_w = (sprite_h / 2.0).to_i
476
- start_x = [sx - sprite_w / 2, 0].max
477
- end_x = [sx + sprite_w / 2, view_w - 1].min
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
- fallback_color = e.sprite_id == :executor ? "100;60;200" : "220;140;30"
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
- start_x.upto(end_x) do |c|
487
- next if c < 0 || c >= view_w
488
- next if dists[c] < tz
489
-
490
- nx = (c - start_x).to_f / actual_w
491
-
492
- r_top = (draw_top / 2.0).ceil
493
- r_bot = (draw_bot / 2.0).floor
494
- r_top.upto(r_bot - 1) do |r|
495
- vp0 = r * 2
496
- vp1 = r * 2 + 1
497
- top_in = vp0 >= draw_top && vp0 < draw_bot
498
- bot_in = vp1 >= draw_top && vp1 < draw_bot
499
- next unless top_in || bot_in
500
-
501
- if use_shape
502
- ny0 = top_in ? (vp0 - draw_top).to_f / actual_h : nil
503
- ny1 = bot_in ? (vp1 - draw_top).to_f / actual_h : nil
504
- top_color = ny0 ? Sprite.for(e.sprite_id, nx, ny0) : nil
505
- bot_color = ny1 ? Sprite.for(e.sprite_id, nx, ny1) : nil
506
- next unless top_color || bot_color
507
-
508
- pixels[vp0][c] = top_color if top_color
509
- pixels[vp1][c] = bot_color if bot_color
510
- else
511
- pixels[vp0][c] = fallback_color if top_in
512
- pixels[vp1][c] = fallback_color if bot_in
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 = (draw_top / 2.0).ceil - 1
605
+ bar_row = r_top - 1
520
606
  next unless bar_row >= 0 && bar_row < view_h
521
607
 
522
- bar_w = [actual_w, 2].max
523
- bar_sx = [sx - bar_w / 2, 0].max
524
- bar_ex = [bar_sx + bar_w - 1, view_w - 1].min
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
- bar_sx.upto(bar_ex) do |c|
528
- next if c < 0 || c >= view_w
529
- next if dists[c] < tz
530
-
531
- ci = c - bar_sx
532
- color = ci < filled ? "0;200;0" : "200;0;0"
533
- pixels[bar_row * 2][c] = color
534
- pixels[bar_row * 2 + 1][c] = color
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 - player.x
542
- ey = d.y - player.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 = ((view_w / 2.0) * (1 + tx / tz)).to_i
553
- sprite_h = (virt_h / tz * 0.3).to_i.clamp(2, virt_h / 2)
554
- ground = (virt_h / 2 + virt_h / tz * 0.35).to_i
555
- draw_bot = [ground, virt_h].min
556
- draw_top = [draw_bot - sprite_h, 0].max
557
- sprite_w = (sprite_h / 2.0).to_i.clamp(1, 6)
558
- start_x = [sx - sprite_w / 2, 0].max
559
- end_x = [sx + sprite_w / 2, view_w - 1].min
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
- start_x.upto(end_x) do |c|
564
- next if c < 0 || c >= view_w
565
- next if dists[c] < tz
566
-
567
- r_top = (draw_top / 2.0).ceil
568
- r_bot = (draw_bot / 2.0).floor
569
- r_top.upto(r_bot - 1) do |r|
570
- next if r < 0 || r >= view_h
571
-
572
- vp0 = r * 2
573
- vp1 = r * 2 + 1
574
- top_in = vp0 >= draw_top && vp0 < draw_bot
575
- bot_in = vp1 >= draw_top && vp1 < draw_bot
576
- next unless top_in || bot_in
577
-
578
- pixels[vp0][c] = color if top_in
579
- pixels[vp1][c] = color if bot_in
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 = ((view_w / 2.0) * (1 + tx / tz)).to_i
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
- vmid = virt_h / 2
591
- draw_top = [(vmid - ph / 2), 0].max
592
- draw_bot = [(vmid + ph / 2).clamp(draw_top + 2, virt_h), virt_h].min
593
- start_x = [sx - pw / 2, 0].max
594
- end_x = [sx + pw / 2, view_w - 1].min
595
- col_code = p.type == :executor ? "94" : "93"
596
-
597
- start_x.upto(end_x) do |c|
598
- next if c < 0 || c >= view_w
599
- next if dists[c] < tz
600
-
601
- r_top = (draw_top / 2.0).ceil
602
- r_bot = [(draw_bot / 2.0).floor, r_top + 1].max
603
- r_top.upto(r_bot - 1) do |r|
604
- next if r < 0 || r >= view_h
605
-
606
- vp0 = r * 2
607
- vp1 = r * 2 + 1
608
- top_in = vp0 >= draw_top && vp0 < draw_bot
609
- bot_in = vp1 >= draw_top && vp1 < draw_bot
610
- next unless top_in || bot_in
611
-
612
- proj_color = col_code == "94" ? "94;94;255" : "255;210;80"
613
- pixels[vp0][c] = proj_color if top_in
614
- pixels[vp1][c] = proj_color if bot_in
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
- index = 0
20
+ visible = 0
21
+ any_ansi = false
22
+ i = 0
23
+ len = text.bytesize
24
+ segment_start = 0
23
25
 
24
- while index < text.length && visible < width
25
- if (match = ANSI_PATTERN.match(text, index)) && match.begin(0) == index
26
- out << match[0]
27
- index = match.end(0)
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
- char = text[index]
32
- out << char
33
- visible += 1
34
- index += 1
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 << "\e[0m" if out.include?("\e[")
38
- out << (" " * (width - visible)) if visible < width
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
- render
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
- mission = Mission::Base.campaign.first
80
- return unless mission
115
+ return unless @demo_map
81
116
 
82
- m = mission.new
83
- demo_map = m.map_data.map { |r| r.is_a?(Array) ? r : r.chars }
84
- dm_h = demo_map.size
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 = m.enemy_defs
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
- title = "T E R M F R O N T"
275
- sub = "Terminal FPS"
276
- tc = [(cols - title.size) / 2 + 1, 1].max
277
- sc = [(cols - sub.size) / 2 + 1, 1].max
278
- lines[title_row - 1] = TerminalOutput.fit_ansi("#{" " * (tc - 1)}\e[1;38;2;120;140;255m#{title}\e[0m", cols)
279
- lines[title_row] = TerminalOutput.fit_ansi("#{" " * (sc - 1)}\e[38;2;80;80;120m#{sub}\e[0m", cols)
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|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Termfront
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: termfront
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - S-H-GAMELINKS