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.
@@ -12,6 +12,17 @@ module Termfront
12
12
  FG_256 = Array.new(256) { |i| "\e[38;5;#{i}m".freeze }.freeze
13
13
  BG_256 = Array.new(256) { |i| "\e[48;5;#{i}m".freeze }.freeze
14
14
 
15
+ EXECUTOR_FALLBACK = Color.rgb_to_256(100, 60, 200)
16
+ CRAWLER_FALLBACK = Color.rgb_to_256(220, 140, 30)
17
+ ENEMY_BAR_FILL = Color.rgb_to_256(0, 200, 0)
18
+ ENEMY_BAR_EMPTY = Color.rgb_to_256(200, 0, 0)
19
+ PROJ_EXECUTOR = Color.rgb_to_256(94, 94, 255)
20
+ PROJ_DEFAULT = Color.rgb_to_256(255, 210, 80)
21
+ ALLY_FALLBACK = Color.rgb_to_256(70, 210, 255)
22
+ ALLY_BAR_FILL = Color.rgb_to_256(0, 180, 255)
23
+ ALLY_BAR_EMPTY = Color.rgb_to_256(80, 20, 20)
24
+ DAMAGE_FLASH_RAMP = Array.new(256) { |i| Color.rgb_to_256(i, 0, 0) }.freeze
25
+
15
26
  def initialize(stdout)
16
27
  @stdout = stdout
17
28
  @buf_view_w = 0
@@ -19,8 +30,10 @@ module Termfront
19
30
  @radar_grid_template = build_radar_grid_template
20
31
  @hrule_cache = Hash.new { |h, c| h[c] = ("\xE2\x94\x80" * c)[0, c * 3].freeze }
21
32
  @radar_drop_glyphs = {}
22
- @fg_truecolor_cache = {}
23
- @bg_truecolor_cache = {}
33
+ @radar_enemy_cells = {}
34
+ @radar_drop_cells = {}
35
+ @radar_terminal_cells = {}
36
+ @radar_ally_cells = {}
24
37
  @enemy_sprites = []
25
38
  @proj_sprites = []
26
39
  @drop_sprites = []
@@ -28,6 +41,10 @@ module Termfront
28
41
  @radar_line_buf = +""
29
42
  @size_cache = nil
30
43
  @size_cache_at = -Float::INFINITY
44
+ @cached_hud_shield_key = nil
45
+ @cached_hud_shield_line = nil
46
+ @cached_hud_ammo_key = nil
47
+ @cached_hud_ammo_line = nil
31
48
  end
32
49
 
33
50
  def invalidate_size_cache!
@@ -46,23 +63,27 @@ module Termfront
46
63
 
47
64
  prepare_frame_buffers(view_w, virt_h)
48
65
 
49
- dx = Math.cos(player.angle)
50
- dy = Math.sin(player.angle)
51
- plane_x = -dy * Math.tan(Config::FOV / 2.0)
52
- plane_y = dx * Math.tan(Config::FOV / 2.0)
66
+ cast_state = [player.x, player.y, player.angle, map.object_id, view_w, virt_h]
67
+ if cast_state != @last_cast_state
68
+ dx = Math.cos(player.angle)
69
+ dy = Math.sin(player.angle)
70
+ plane_x = -dy * Math.tan(Config::FOV / 2.0)
71
+ plane_y = dx * Math.tan(Config::FOV / 2.0)
53
72
 
54
- view_w.times do |c|
55
- cam = 2.0 * c / view_w - 1.0
56
- @dists[c], @sides[c] = cast_ray(map, player.x, player.y, dx + plane_x * cam, dy + plane_y * cam)
57
- end
73
+ view_w.times do |c|
74
+ cam = 2.0 * c / view_w - 1.0
75
+ @dists[c], @sides[c] = cast_ray(map, player.x, player.y, dx + plane_x * cam, dy + plane_y * cam)
76
+ end
58
77
 
59
- vmid = virt_h / 2.0
60
- view_w.times do |c|
61
- d = @dists[c]
62
- lh = d > 0.01 ? (virt_h / d).to_i : virt_h
63
- @wtop[c] = [(vmid - lh / 2.0).to_i, 0].max
64
- @wbot[c] = [(vmid + lh / 2.0).to_i, virt_h].min
65
- @wcol[c] = Sprite.wall_brightness(d, @sides[c])
78
+ vmid = virt_h / 2.0
79
+ view_w.times do |c|
80
+ d = @dists[c]
81
+ lh = d > 0.01 ? (virt_h / d).to_i : virt_h
82
+ @wtop[c] = [(vmid - lh / 2.0).to_i, 0].max
83
+ @wbot[c] = [(vmid + lh / 2.0).to_i, virt_h].min
84
+ @wcol[c] = Sprite.wall_brightness(d, @sides[c])
85
+ end
86
+ @last_cast_state = cast_state
66
87
  end
67
88
  build_view_pixels(virt_h, view_w, @wtop, @wbot, @wcol)
68
89
  overlay_enemies_3d(@pixels, view_h, view_w, @dists, player, enemies, projectiles, drops)
@@ -211,6 +232,14 @@ module Termfront
211
232
  end
212
233
 
213
234
  def render_hud(buf, cols, player, drops, terminals, status_line)
235
+ buf << hud_shield_line(cols, player, status_line) << "\r\n"
236
+ buf << hud_ammo_line(cols, player, drops, terminals) << "\r\n"
237
+ end
238
+
239
+ def hud_shield_line(cols, player, status_line)
240
+ key = [player.shield.to_i, status_line, cols]
241
+ return @cached_hud_shield_line if @cached_hud_shield_key == key
242
+
214
243
  bar_w = [cols - 20, 10].max
215
244
  pct = player.shield / Config::SHIELD_MAX.to_f
216
245
  filled = (pct * bar_w).to_i
@@ -226,11 +255,23 @@ module Termfront
226
255
  shield_str = "SHIELD #{color}#{"█" * filled}#{"░" * empty}\e[0m #{pct_s}"
227
256
  shield_str = "#{shield_str}\e[90m#{status_line}\e[0m" if status_line
228
257
  pad = [(cols - bar_w - 15) / 2, 0].max
229
- buf << TerminalOutput.fit_ansi("#{" " * pad}#{shield_str}", cols) << "\r\n"
258
+ line = TerminalOutput.fit_ansi("#{" " * pad}#{shield_str}", cols)
230
259
 
260
+ @cached_hud_shield_key = key
261
+ @cached_hud_shield_line = line
262
+ line
263
+ end
264
+
265
+ def hud_ammo_line(cols, player, drops, terminals)
231
266
  weapon = player.current_weapon
232
- wcolor = weapon.type_id.to_s.start_with?("shock") ? "\e[96m" : "\e[97m"
267
+ can_pickup = drops.any? { |d| d.in_range?(player.x, player.y) }
268
+ can_use_terminal = terminals.any? do |terminal|
269
+ (terminal[:x] - player.x)**2 + (terminal[:y] - player.y)**2 < Config::TERMINAL_USE_RADIUS**2
270
+ end
271
+ key = [weapon.type_id, weapon.ammo, can_pickup, can_use_terminal, cols]
272
+ return @cached_hud_ammo_line if @cached_hud_ammo_key == key
233
273
 
274
+ wcolor = weapon.type_id.to_s.start_with?("shock") ? "\e[96m" : "\e[97m"
234
275
  if weapon.max_ammo
235
276
  ammo_bar_w = 12
236
277
  ammo_pct = weapon.ammo.to_f / weapon.max_ammo
@@ -241,10 +282,6 @@ module Termfront
241
282
  ammo_str = "#{wcolor}#{weapon.name}\e[0m [\xe2\x88\x9e]"
242
283
  end
243
284
 
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
285
  interact_str = if can_use_terminal
249
286
  "\e[1;96m[E]Use Terminal\e[0m"
250
287
  elsif can_pickup
@@ -253,64 +290,118 @@ module Termfront
253
290
  "E:interact"
254
291
  end
255
292
 
256
- line = "#{ammo_str} T:swap #{interact_str} Space:fire"
257
- buf << TerminalOutput.fit_ansi(line, cols) << "\r\n"
293
+ line = TerminalOutput.fit_ansi("#{ammo_str} T:swap #{interact_str} Space:fire", cols)
294
+
295
+ @cached_hud_ammo_key = key
296
+ @cached_hud_ammo_line = line
297
+ line
258
298
  end
259
299
 
260
300
  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
301
+ ceil_c = Config::CEIL_C
302
+ floor_c = Config::FLOOR_C
303
+ pixels = @pixels
304
+
305
+ min_wt = wtop.min
306
+ max_wb = wbot.max
307
+ upper_done = min_wt < virt_h ? min_wt : virt_h
308
+ lower_start = max_wb > upper_done ? max_wb : upper_done
309
+ lower_start = virt_h if lower_start > virt_h
310
+
311
+ vr = 0
312
+ while vr < upper_done
313
+ pixels[vr].fill(ceil_c, 0, view_w)
314
+ vr += 1
315
+ end
316
+
317
+ vr_bot = virt_h - 1
318
+ while vr_bot >= lower_start
319
+ pixels[vr_bot].fill(floor_c, 0, view_w)
320
+ vr_bot -= 1
321
+ end
322
+ middle_end = vr_bot + 1
323
+
324
+ c = 0
325
+ while c < view_w
326
+ wt = wtop[c]
327
+ wb = wbot[c]
328
+ wc = wcol[c]
329
+
330
+ vr = upper_done
331
+ while vr < wt && vr < middle_end
332
+ pixels[vr][c] = ceil_c
333
+ vr += 1
334
+ end
335
+ while vr < wb && vr < middle_end
336
+ pixels[vr][c] = wc
337
+ vr += 1
338
+ end
339
+ while vr < middle_end
340
+ pixels[vr][c] = floor_c
341
+ vr += 1
271
342
  end
343
+
344
+ c += 1
272
345
  end
273
346
  end
274
347
 
275
348
  def render_view(buf, view_h, view_w, pixels)
276
- view_h.times do |r|
349
+ fg_256 = FG_256
350
+ bg_256 = BG_256
351
+
352
+ r = 0
353
+ while r < view_h
277
354
  vp0 = r * 2
278
- vp1 = r * 2 + 1
279
- pfg = nil
280
- pbg = nil
355
+ vp1 = vp0 + 1
281
356
  top_row = pixels[vp0]
282
357
  bot_row = pixels[vp1]
283
358
 
284
- view_w.times do |c|
359
+ first = top_row[0]
360
+ if bot_row[0] == first
361
+ uniform = true
362
+ cu = 1
363
+ while cu < view_w
364
+ if top_row[cu] != first || bot_row[cu] != first
365
+ uniform = false
366
+ break
367
+ end
368
+ cu += 1
369
+ end
370
+ if uniform
371
+ buf << bg_256[first] << "\e[K\e[0m\r\n"
372
+ r += 1
373
+ next
374
+ end
375
+ end
376
+
377
+ pbg = nil
378
+
379
+ c = 0
380
+ while c < view_w
285
381
  tc = top_row[c]
286
382
  bc = bot_row[c]
287
383
 
288
384
  if tc == bc
289
- if bg_only?(tc)
290
- if tc != pbg
291
- buf << ansi_bg(tc)
292
- pbg = tc
293
- pfg = nil
294
- end
295
- buf << " "
296
- else
297
- if tc != pfg || pbg
298
- buf << ansi_fg(tc)
299
- pfg = tc
300
- pbg = nil
301
- end
302
- buf << "\xE2\x96\x88"
385
+ run_end = c + 1
386
+ while run_end < view_w && top_row[run_end] == tc && bot_row[run_end] == bc
387
+ run_end += 1
303
388
  end
304
- else
305
- if tc != pfg || bc != pbg
306
- buf << ansi_fg(tc) << ansi_bg(bc)
307
- pfg = tc
308
- pbg = bc
389
+ n = run_end - c
390
+
391
+ if tc != pbg
392
+ buf << bg_256[tc]
393
+ pbg = tc
309
394
  end
310
- buf << "\xE2\x96\x80"
395
+ buf << (n == 1 ? " " : " " * n)
396
+ c = run_end
397
+ else
398
+ buf << fg_256[tc] << bg_256[bc] << "\xE2\x96\x80"
399
+ pbg = bc
400
+ c += 1
311
401
  end
312
402
  end
313
403
  buf << "\e[0m\r\n"
404
+ r += 1
314
405
  end
315
406
  end
316
407
 
@@ -319,82 +410,97 @@ module Termfront
319
410
 
320
411
  r = Config::RADAR_RADIUS
321
412
  diam = r * 2 + 1
413
+ range = Config::RADAR_RANGE
414
+ range_sq = Config::RADAR_RANGE_SQ
415
+ r_sq = r * r
322
416
  grid = @radar_grid_template
323
417
 
324
418
  cos_a = Math.cos(-player.angle + Math::PI / 2)
325
419
  sin_a = Math.sin(-player.angle + Math::PI / 2)
326
- enemy_cells = {}
420
+ px = player.x
421
+ py = player.y
422
+
423
+ enemy_cells = @radar_enemy_cells
424
+ drop_cells = @radar_drop_cells
425
+ terminal_cells = @radar_terminal_cells
426
+ ally_cells = @radar_ally_cells
427
+ enemy_cells.clear
428
+ drop_cells.clear
429
+ terminal_cells.clear
430
+ ally_cells.clear
431
+
327
432
  enemies.each do |e|
328
433
  next unless e.alive
329
434
 
330
- ex = e.x - player.x
331
- ey = e.y - player.y
332
- next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
435
+ ex = e.x - px
436
+ ey = e.y - py
437
+ next if ex * ex + ey * ey > range_sq
333
438
 
334
439
  rx = -(ex * cos_a - ey * sin_a)
335
440
  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)
441
+ sx = r + (rx / range * r).round
442
+ sy = r + (ry / range * r).round
443
+ next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
339
444
 
340
- d2 = (sx - r)**2 + (sy - r)**2
341
- next if d2 > r * r
445
+ dxr = sx - r
446
+ dyr = sy - r
447
+ next if dxr * dxr + dyr * dyr > r_sq
342
448
 
343
- enemy_cells[[sy, sx]] = e.sprite_id
449
+ enemy_cells[sy * diam + sx] = e.sprite_id
344
450
  end
345
451
 
346
- drop_cells = {}
347
452
  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
453
+ ex = d.x - px
454
+ ey = d.y - py
455
+ next if ex * ex + ey * ey > range_sq
351
456
 
352
457
  rx = -(ex * cos_a - ey * sin_a)
353
458
  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)
459
+ sx = r + (rx / range * r).round
460
+ sy = r + (ry / range * r).round
461
+ next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
357
462
 
358
- d2 = (sx - r)**2 + (sy - r)**2
359
- next if d2 > r * r
463
+ dxr = sx - r
464
+ dyr = sy - r
465
+ next if dxr * dxr + dyr * dyr > r_sq
360
466
 
361
- drop_cells[[sy, sx]] = d
467
+ drop_cells[sy * diam + sx] = d
362
468
  end
363
469
 
364
- terminal_cells = {}
365
470
  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
471
+ ex = terminal[:x] - px
472
+ ey = terminal[:y] - py
473
+ next if ex * ex + ey * ey > range_sq
369
474
 
370
475
  rx = -(ex * cos_a - ey * sin_a)
371
476
  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)
477
+ sx = r + (rx / range * r).round
478
+ sy = r + (ry / range * r).round
479
+ next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
375
480
 
376
- d2 = (sx - r)**2 + (sy - r)**2
377
- next if d2 > r * r
481
+ dxr = sx - r
482
+ dyr = sy - r
483
+ next if dxr * dxr + dyr * dyr > r_sq
378
484
 
379
- terminal_cells[[sy, sx]] = terminal
485
+ terminal_cells[sy * diam + sx] = terminal
380
486
  end
381
487
 
382
- ally_cells = {}
383
488
  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
489
+ ex = ally.x - px
490
+ ey = ally.y - py
491
+ next if ex * ex + ey * ey > range_sq
387
492
 
388
493
  rx = -(ex * cos_a - ey * sin_a)
389
494
  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)
495
+ sx = r + (rx / range * r).round
496
+ sy = r + (ry / range * r).round
497
+ next if sx < 0 || sx >= diam || sy < 0 || sy >= diam
393
498
 
394
- d2 = (sx - r)**2 + (sy - r)**2
395
- next if d2 > r * r
499
+ dxr = sx - r
500
+ dyr = sy - r
501
+ next if dxr * dxr + dyr * dyr > r_sq
396
502
 
397
- ally_cells[[sy, sx]] = true
503
+ ally_cells[sy * diam + sx] = true
398
504
  end
399
505
 
400
506
  alive_count = enemies.count(&:alive)
@@ -402,21 +508,25 @@ module Termfront
402
508
  info_lines = [
403
509
  "Enemies: #{alive_count}/#{total_count}",
404
510
  "Heading: #{format("%.0f", (player.angle % (Math::PI * 2)) * 180 / Math::PI)}\xC2\xB0",
405
- "Pos: (#{"%.1f" % player.x}, #{"%.1f" % player.y}) T:terminal"
511
+ "Pos: (#{"%.1f" % px}, #{"%.1f" % py}) T:terminal"
406
512
  ]
407
513
 
408
- radar_h.times do |row|
514
+ row = 0
515
+ while row < radar_h
409
516
  line = @radar_line_buf.clear
410
517
  if row < diam
411
518
  line << " "
412
- diam.times do |cx|
413
- if (etype = enemy_cells[[row, cx]])
519
+ cx = 0
520
+ base = row * diam
521
+ while cx < diam
522
+ key = base + cx
523
+ if (etype = enemy_cells[key])
414
524
  line << (etype == :executor ? RADAR_EXECUTOR : RADAR_CRAWLER)
415
- elsif ally_cells[[row, cx]]
525
+ elsif ally_cells[key]
416
526
  line << RADAR_ALLY
417
- elsif (drop = drop_cells[[row, cx]])
527
+ elsif (drop = drop_cells[key])
418
528
  line << radar_drop_glyph(drop)
419
- elsif terminal_cells[[row, cx]]
529
+ elsif terminal_cells[key]
420
530
  line << RADAR_TERMINAL
421
531
  elsif row == r && cx == r
422
532
  line << RADAR_PLAYER
@@ -425,28 +535,36 @@ module Termfront
425
535
  else
426
536
  line << grid[row][cx]
427
537
  end
538
+ cx += 1
428
539
  end
429
540
  line << (row < info_lines.size ? " #{info_lines[row]}" : "")
430
541
  end
431
542
  buf << TerminalOutput.fit_ansi(line, cols)
432
543
  buf << "\r\n" if row < radar_h - 1
544
+ row += 1
433
545
  end
434
546
  end
435
547
 
436
548
  def overlay_enemies_3d(pixels, view_h, view_w, dists, player, enemies, projectiles, drops)
437
549
  dx = Math.cos(player.angle)
438
550
  dy = Math.sin(player.angle)
439
- px = -dy * Math.tan(Config::FOV / 2.0)
440
- py = dx * Math.tan(Config::FOV / 2.0)
551
+ tan_half_fov = Math.tan(Config::FOV / 2.0)
552
+ px = -dy * tan_half_fov
553
+ py = dx * tan_half_fov
441
554
  virt_h = view_h * 2
555
+ half_virt_h = virt_h / 2
556
+ half_view_w = view_w / 2.0
557
+ view_w_last = view_w - 1
442
558
  inv = 1.0 / (px * dy - py * dx)
559
+ player_x = player.x
560
+ player_y = player.y
443
561
 
444
562
  @enemy_sprites.clear
445
563
  enemies.each do |e|
446
564
  next unless e.alive
447
565
 
448
- ex = e.x - player.x
449
- ey = e.y - player.y
566
+ ex = e.x - player_x
567
+ ey = e.y - player_y
450
568
  tx = inv * (dy * ex - dx * ey)
451
569
  tz = inv * (-py * ex + px * ey)
452
570
  next if tz < 0.2
@@ -456,8 +574,8 @@ module Termfront
456
574
 
457
575
  @proj_sprites.clear
458
576
  projectiles.each do |p|
459
- ex = p.x - player.x
460
- ey = p.y - player.y
577
+ ex = p.x - player_x
578
+ ey = p.y - player_y
461
579
  tx = inv * (dy * ex - dx * ey)
462
580
  tz = inv * (-py * ex + px * ey)
463
581
  next if tz < 0.2
@@ -468,78 +586,95 @@ module Termfront
468
586
  @enemy_sprites.sort! { |a, b| b[0] <=> a[0] }
469
587
 
470
588
  @enemy_sprites.each do |tz, tx, e|
471
- sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
589
+ sx = (half_view_w * (1 + tx / tz)).to_i
472
590
  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
591
+ draw_top = half_virt_h - sprite_h / 2
592
+ draw_top = 0 if draw_top < 0
593
+ draw_bot = half_virt_h + sprite_h / 2
594
+ draw_bot = virt_h if draw_bot > virt_h
595
+ sprite_w = sprite_h / 2
596
+ start_x = sx - sprite_w / 2
597
+ start_x = 0 if start_x < 0
598
+ end_x = sx + sprite_w / 2
599
+ end_x = view_w_last if end_x > view_w_last
478
600
 
479
601
  actual_h = draw_bot - draw_top
480
602
  actual_w = end_x - start_x + 1
481
603
  next if actual_h < 1 || actual_w < 1
482
604
 
483
- fallback_color = e.sprite_id == :executor ? "100;60;200" : "220;140;30"
484
- 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
605
+ sprite_id = e.sprite_id
606
+ sprite_fn = Sprite::REGISTRY[sprite_id]
607
+ fallback_color = sprite_id == :executor ? EXECUTOR_FALLBACK : CRAWLER_FALLBACK
608
+ use_shape = actual_h >= 6 && sprite_fn
609
+ r_top = (draw_top + 1) >> 1
610
+ r_bot = draw_bot >> 1
611
+ actual_h_f = actual_h.to_f
612
+ actual_w_f = actual_w.to_f
613
+
614
+ c = start_x
615
+ while c <= end_x
616
+ if c >= 0 && c < view_w && dists[c] >= tz
617
+ nx = (c - start_x) / actual_w_f
618
+
619
+ r = r_top
620
+ while r < r_bot
621
+ vp0 = r << 1
622
+ vp1 = vp0 + 1
623
+ top_in = vp0 >= draw_top && vp0 < draw_bot
624
+ bot_in = vp1 >= draw_top && vp1 < draw_bot
625
+
626
+ if top_in || bot_in
627
+ if use_shape
628
+ ny0 = top_in ? (vp0 - draw_top) / actual_h_f : nil
629
+ ny1 = bot_in ? (vp1 - draw_top) / actual_h_f : nil
630
+ top_color = ny0 ? sprite_fn.call(nx, ny0) : nil
631
+ bot_color = ny1 ? sprite_fn.call(nx, ny1) : nil
632
+ if top_color || bot_color
633
+ pixels[vp0][c] = top_color if top_color
634
+ pixels[vp1][c] = bot_color if bot_color
635
+ end
636
+ else
637
+ pixels[vp0][c] = fallback_color if top_in
638
+ pixels[vp1][c] = fallback_color if bot_in
639
+ end
640
+ end
641
+ r += 1
513
642
  end
514
643
  end
644
+ c += 1
515
645
  end
516
646
 
517
647
  next unless e.max_hp > 1
518
648
 
519
- bar_row = (draw_top / 2.0).ceil - 1
649
+ bar_row = r_top - 1
520
650
  next unless bar_row >= 0 && bar_row < view_h
521
651
 
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
652
+ bar_w = actual_w > 2 ? actual_w : 2
653
+ bar_sx = sx - bar_w / 2
654
+ bar_sx = 0 if bar_sx < 0
655
+ bar_ex = bar_sx + bar_w - 1
656
+ bar_ex = view_w_last if bar_ex > view_w_last
525
657
  hp_pct = e.hp.to_f / e.max_hp
526
658
  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
659
+ bar_vp0 = bar_row << 1
660
+ bar_vp1 = bar_vp0 + 1
661
+ c = bar_sx
662
+ while c <= bar_ex
663
+ if c >= 0 && c < view_w && dists[c] >= tz
664
+ ci = c - bar_sx
665
+ color = ci < filled ? ENEMY_BAR_FILL : ENEMY_BAR_EMPTY
666
+ pixels[bar_vp0][c] = color
667
+ pixels[bar_vp1][c] = color
668
+ end
669
+ c += 1
535
670
  end
536
671
  end
537
672
 
538
673
  # Render weapon drops
539
674
  @drop_sprites.clear
540
675
  drops.each do |d|
541
- ex = d.x - player.x
542
- ey = d.y - player.y
676
+ ex = d.x - player_x
677
+ ey = d.y - player_y
543
678
  tx = inv * (dy * ex - dx * ey)
544
679
  tz = inv * (-py * ex + px * ey)
545
680
  next if tz < 0.2
@@ -549,70 +684,83 @@ module Termfront
549
684
  @drop_sprites.sort! { |a, b| b[0] <=> a[0] }
550
685
 
551
686
  @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
687
+ sx = (half_view_w * (1 + tx / tz)).to_i
688
+ sprite_h = (virt_h / tz * 0.3).to_i.clamp(2, half_virt_h)
689
+ ground = (half_virt_h + virt_h / tz * 0.35).to_i
690
+ draw_bot = ground < virt_h ? ground : virt_h
691
+ draw_top = draw_bot - sprite_h
692
+ draw_top = 0 if draw_top < 0
693
+ sprite_w = (sprite_h / 2).clamp(1, 6)
694
+ start_x = sx - sprite_w / 2
695
+ start_x = 0 if start_x < 0
696
+ end_x = sx + sprite_w / 2
697
+ end_x = view_w_last if end_x > view_w_last
560
698
 
561
699
  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
700
+ r_top = (draw_top + 1) >> 1
701
+ r_bot = draw_bot >> 1
702
+
703
+ c = start_x
704
+ while c <= end_x
705
+ if c >= 0 && c < view_w && dists[c] >= tz
706
+ r = r_top
707
+ while r < r_bot
708
+ if r >= 0 && r < view_h
709
+ vp0 = r << 1
710
+ vp1 = vp0 + 1
711
+ top_in = vp0 >= draw_top && vp0 < draw_bot
712
+ bot_in = vp1 >= draw_top && vp1 < draw_bot
713
+ if top_in || bot_in
714
+ pixels[vp0][c] = color if top_in
715
+ pixels[vp1][c] = color if bot_in
716
+ end
717
+ end
718
+ r += 1
719
+ end
580
720
  end
721
+ c += 1
581
722
  end
582
723
  end
583
724
 
584
725
  # Render projectiles
585
726
  @proj_sprites.sort! { |a, b| b[0] <=> a[0] }
586
727
  @proj_sprites.each do |tz, tx, p|
587
- sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
728
+ sx = (half_view_w * (1 + tx / tz)).to_i
588
729
  pw = (4.0 / tz).ceil.clamp(1, 5)
589
730
  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
731
+ draw_top = half_virt_h - ph / 2
732
+ draw_top = 0 if draw_top < 0
733
+ draw_bot = half_virt_h + ph / 2
734
+ draw_bot = draw_top + 2 if draw_bot < draw_top + 2
735
+ draw_bot = virt_h if draw_bot > virt_h
736
+ start_x = sx - pw / 2
737
+ start_x = 0 if start_x < 0
738
+ end_x = sx + pw / 2
739
+ end_x = view_w_last if end_x > view_w_last
740
+ proj_color = p.type == :executor ? PROJ_EXECUTOR : PROJ_DEFAULT
741
+ r_top = (draw_top + 1) >> 1
742
+ r_bot = draw_bot >> 1
743
+ r_bot = r_top + 1 if r_bot < r_top + 1
744
+
745
+ c = start_x
746
+ while c <= end_x
747
+ if c >= 0 && c < view_w && dists[c] >= tz
748
+ r = r_top
749
+ while r < r_bot
750
+ if r >= 0 && r < view_h
751
+ vp0 = r << 1
752
+ vp1 = vp0 + 1
753
+ top_in = vp0 >= draw_top && vp0 < draw_bot
754
+ bot_in = vp1 >= draw_top && vp1 < draw_bot
755
+ if top_in || bot_in
756
+ pixels[vp0][c] = proj_color if top_in
757
+ pixels[vp1][c] = proj_color if bot_in
758
+ end
759
+ end
760
+ r += 1
761
+ end
615
762
  end
763
+ c += 1
616
764
  end
617
765
  end
618
766
  end
@@ -677,8 +825,8 @@ module Termfront
677
825
  pixels[vp0][c] = top_color if top_color
678
826
  pixels[vp1][c] = bot_color if bot_color
679
827
  else
680
- pixels[vp0][c] = "70;210;255" if top_in
681
- pixels[vp1][c] = "70;210;255" if bot_in
828
+ pixels[vp0][c] = ALLY_FALLBACK if top_in
829
+ pixels[vp1][c] = ALLY_FALLBACK if bot_in
682
830
  end
683
831
  end
684
832
  end
@@ -698,7 +846,7 @@ module Termfront
698
846
  next if dists[c] < tz
699
847
 
700
848
  ci = c - bar_sx
701
- color = ci < filled ? "0;180;255" : "80;20;20"
849
+ color = ci < filled ? ALLY_BAR_FILL : ALLY_BAR_EMPTY
702
850
  pixels[bar_row * 2][c] = color
703
851
  pixels[bar_row * 2 + 1][c] = color
704
852
  end
@@ -708,9 +856,10 @@ module Termfront
708
856
  def overlay_damage_flash(pixels, view_h, view_w, player)
709
857
  return unless player.damage_flash > 0
710
858
 
711
- intensity = player.damage_flash * 60
859
+ intensity = (player.damage_flash * 60).to_i
860
+ intensity = 255 if intensity > 255
712
861
  flash_w = 2
713
- color = "#{intensity};0;0"
862
+ color = DAMAGE_FLASH_RAMP[intensity]
714
863
 
715
864
  view_h.times do |r|
716
865
  vp0 = r * 2
@@ -739,24 +888,5 @@ module Termfront
739
888
  buf << "\e[#{cr};#{fs}H\e[93m#{"*" * (fe - fs + 1)}\e[0m"
740
889
  end
741
890
 
742
- def bg_only?(color)
743
- color.is_a?(Integer)
744
- end
745
-
746
- def ansi_fg(color)
747
- if color.is_a?(Integer)
748
- FG_256[color]
749
- else
750
- @fg_truecolor_cache[color] ||= "\e[38;2;#{color}m".freeze
751
- end
752
- end
753
-
754
- def ansi_bg(color)
755
- if color.is_a?(Integer)
756
- BG_256[color]
757
- else
758
- @bg_truecolor_cache[color] ||= "\e[48;2;#{color}m".freeze
759
- end
760
- end
761
891
  end
762
892
  end