termfront 0.1.4 → 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.
@@ -2,12 +2,48 @@
2
2
 
3
3
  module Termfront
4
4
  class Renderer
5
+ RADAR_CRAWLER = "\e[91m*\e[0m"
6
+ RADAR_EXECUTOR = "\e[95m*\e[0m"
7
+ RADAR_ALLY = "\e[96m+\e[0m"
8
+ RADAR_TERMINAL = "\e[96mT\e[0m"
9
+ RADAR_PLAYER = "\e[92m^\e[0m"
10
+ RADAR_WALL = "\e[90m#\e[0m"
11
+
12
+ FG_256 = Array.new(256) { |i| "\e[38;5;#{i}m".freeze }.freeze
13
+ BG_256 = Array.new(256) { |i| "\e[48;5;#{i}m".freeze }.freeze
14
+
5
15
  def initialize(stdout)
6
16
  @stdout = stdout
17
+ @buf_view_w = 0
18
+ @buf_virt_h = 0
19
+ @radar_grid_template = build_radar_grid_template
20
+ @hrule_cache = Hash.new { |h, c| h[c] = ("\xE2\x94\x80" * c)[0, c * 3].freeze }
21
+ @radar_drop_glyphs = {}
22
+ @radar_enemy_cells = {}
23
+ @radar_drop_cells = {}
24
+ @radar_terminal_cells = {}
25
+ @radar_ally_cells = {}
26
+ @fg_truecolor_cache = {}
27
+ @bg_truecolor_cache = {}
28
+ @enemy_sprites = []
29
+ @proj_sprites = []
30
+ @drop_sprites = []
31
+ @ally_sprites = []
32
+ @radar_line_buf = +""
33
+ @size_cache = nil
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
39
+ end
40
+
41
+ def invalidate_size_cache!
42
+ @size_cache = nil
7
43
  end
8
44
 
9
45
  def render(player:, map:, enemies:, projectiles:, drops:, terminals: [], status_line: nil, allies: [])
10
- rows, cols = @stdout.winsize
46
+ rows, cols = current_size
11
47
  rows = [rows, 6].max
12
48
  cols = [cols, 20].max
13
49
 
@@ -16,38 +52,35 @@ module Termfront
16
52
  view_w = cols
17
53
  virt_h = view_h * 2
18
54
 
55
+ prepare_frame_buffers(view_w, virt_h)
56
+
19
57
  dx = Math.cos(player.angle)
20
58
  dy = Math.sin(player.angle)
21
59
  plane_x = -dy * Math.tan(Config::FOV / 2.0)
22
60
  plane_y = dx * Math.tan(Config::FOV / 2.0)
23
61
 
24
- dists = Array.new(view_w)
25
- sides = Array.new(view_w)
26
62
  view_w.times do |c|
27
63
  cam = 2.0 * c / view_w - 1.0
28
- dists[c], sides[c] = cast_ray(map, player.x, player.y, dx + plane_x * cam, dy + plane_y * cam)
64
+ @dists[c], @sides[c] = cast_ray(map, player.x, player.y, dx + plane_x * cam, dy + plane_y * cam)
29
65
  end
30
66
 
31
67
  vmid = virt_h / 2.0
32
- wtop = Array.new(view_w)
33
- wbot = Array.new(view_w)
34
- wcol = Array.new(view_w)
35
68
  view_w.times do |c|
36
- d = dists[c]
69
+ d = @dists[c]
37
70
  lh = d > 0.01 ? (virt_h / d).to_i : virt_h
38
- wtop[c] = [(vmid - lh / 2.0).to_i, 0].max
39
- wbot[c] = [(vmid + lh / 2.0).to_i, virt_h].min
40
- wcol[c] = Sprite.wall_brightness(d, sides[c])
71
+ @wtop[c] = [(vmid - lh / 2.0).to_i, 0].max
72
+ @wbot[c] = [(vmid + lh / 2.0).to_i, virt_h].min
73
+ @wcol[c] = Sprite.wall_brightness(d, @sides[c])
41
74
  end
42
- pixels = build_view_pixels(virt_h, view_w, wtop, wbot, wcol)
43
- overlay_enemies_3d(pixels, view_h, view_w, dists, player, enemies, projectiles, drops)
44
- overlay_allies_3d(pixels, view_h, view_w, dists, player, allies)
45
- overlay_damage_flash(pixels, view_h, view_w, player)
75
+ build_view_pixels(virt_h, view_w, @wtop, @wbot, @wcol)
76
+ overlay_enemies_3d(@pixels, view_h, view_w, @dists, player, enemies, projectiles, drops)
77
+ overlay_allies_3d(@pixels, view_h, view_w, @dists, player, allies)
78
+ overlay_damage_flash(@pixels, view_h, view_w, player)
46
79
 
47
80
  buf = TerminalOutput.begin_frame(home: true)
48
81
 
49
82
  render_hud(buf, cols, player, drops, terminals, status_line)
50
- render_view(buf, view_h, view_w, pixels)
83
+ render_view(buf, view_h, view_w, @pixels)
51
84
  buf << "\e[#{3 + view_h};1H"
52
85
  render_radar(buf, cols, radar_h, player, enemies, drops, terminals, allies)
53
86
  render_crosshair(buf, view_h, view_w, cols, player)
@@ -132,7 +165,68 @@ module Termfront
132
165
 
133
166
  private
134
167
 
168
+ def current_size
169
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
170
+ if @size_cache.nil? || now - @size_cache_at >= 0.25
171
+ @size_cache = @stdout.winsize
172
+ @size_cache_at = now
173
+ end
174
+ @size_cache
175
+ end
176
+
177
+ def build_radar_grid_template
178
+ r = Config::RADAR_RADIUS
179
+ diam = r * 2 + 1
180
+ Array.new(diam) do |ry|
181
+ Array.new(diam) do |rx|
182
+ if ry == r && rx == r
183
+ "^"
184
+ else
185
+ dx = rx - r
186
+ dy = ry - r
187
+ d2 = dx * dx + dy * dy
188
+ if d2 <= r * r
189
+ "."
190
+ elsif d2 <= (r + 1) * (r + 1)
191
+ "#"
192
+ else
193
+ " "
194
+ end
195
+ end
196
+ end.freeze
197
+ end.freeze
198
+ end
199
+
200
+ def radar_drop_glyph(drop)
201
+ @radar_drop_glyphs[drop.type] ||= begin
202
+ dc = drop.type.to_s.start_with?("shock") ? "\e[96m" : "\e[93m"
203
+ dl = Weapon::Base.registry[drop.type].new.name[0]
204
+ "#{dc}#{dl}\e[0m".freeze
205
+ end
206
+ end
207
+
208
+ def prepare_frame_buffers(view_w, virt_h)
209
+ if @buf_view_w != view_w || @buf_virt_h != virt_h
210
+ @buf_view_w = view_w
211
+ @buf_virt_h = virt_h
212
+ @dists = Array.new(view_w)
213
+ @sides = Array.new(view_w)
214
+ @wtop = Array.new(view_w)
215
+ @wbot = Array.new(view_w)
216
+ @wcol = Array.new(view_w)
217
+ @pixels = Array.new(virt_h) { Array.new(view_w) }
218
+ end
219
+ end
220
+
135
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
+
136
230
  bar_w = [cols - 20, 10].max
137
231
  pct = player.shield / Config::SHIELD_MAX.to_f
138
232
  filled = (pct * bar_w).to_i
@@ -148,11 +242,23 @@ module Termfront
148
242
  shield_str = "SHIELD #{color}#{"█" * filled}#{"░" * empty}\e[0m #{pct_s}"
149
243
  shield_str = "#{shield_str}\e[90m#{status_line}\e[0m" if status_line
150
244
  pad = [(cols - bar_w - 15) / 2, 0].max
151
- buf << TerminalOutput.fit_ansi("#{" " * pad}#{shield_str}", cols) << "\r\n"
245
+ line = TerminalOutput.fit_ansi("#{" " * pad}#{shield_str}", cols)
152
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)
153
253
  weapon = player.current_weapon
154
- 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
155
260
 
261
+ wcolor = weapon.type_id.to_s.start_with?("shock") ? "\e[96m" : "\e[97m"
156
262
  if weapon.max_ammo
157
263
  ammo_bar_w = 12
158
264
  ammo_pct = weapon.ammo.to_f / weapon.max_ammo
@@ -163,10 +269,6 @@ module Termfront
163
269
  ammo_str = "#{wcolor}#{weapon.name}\e[0m [\xe2\x88\x9e]"
164
270
  end
165
271
 
166
- can_pickup = drops.any? { |d| d.in_range?(player.x, player.y) }
167
- can_use_terminal = terminals.any? do |terminal|
168
- (terminal[:x] - player.x)**2 + (terminal[:y] - player.y)**2 < Config::TERMINAL_USE_RADIUS**2
169
- end
170
272
  interact_str = if can_use_terminal
171
273
  "\e[1;96m[E]Use Terminal\e[0m"
172
274
  elsif can_pickup
@@ -175,28 +277,48 @@ module Termfront
175
277
  "E:interact"
176
278
  end
177
279
 
178
- line = "#{ammo_str} T:swap #{interact_str} Space:fire"
179
- 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
180
285
  end
181
286
 
182
287
  def build_view_pixels(virt_h, view_w, wtop, wbot, wcol)
183
- pixels = Array.new(virt_h) { Array.new(view_w) }
184
- virt_h.times do |vr|
185
- row = pixels[vr]
186
- view_w.times do |c|
187
- row[c] = if vr < wtop[c]
188
- Config::CEIL_C
189
- elsif vr < wbot[c]
190
- wcol[c]
191
- else
192
- Config::FLOOR_C
193
- 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
194
302
  end
303
+ while vr < wb && vr < virt_h
304
+ pixels[vr][c] = wc
305
+ vr += 1
306
+ end
307
+ while vr < virt_h
308
+ pixels[vr][c] = floor_c
309
+ vr += 1
310
+ end
311
+
312
+ c += 1
195
313
  end
196
- pixels
197
314
  end
198
315
 
199
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
+
200
322
  view_h.times do |r|
201
323
  vp0 = r * 2
202
324
  vp1 = r * 2 + 1
@@ -210,16 +332,16 @@ module Termfront
210
332
  bc = bot_row[c]
211
333
 
212
334
  if tc == bc
213
- if bg_only?(tc)
335
+ if tc.is_a?(Integer)
214
336
  if tc != pbg
215
- buf << ansi_bg(tc)
337
+ buf << bg_256[tc]
216
338
  pbg = tc
217
339
  pfg = nil
218
340
  end
219
341
  buf << " "
220
342
  else
221
343
  if tc != pfg || pbg
222
- buf << ansi_fg(tc)
344
+ buf << (fg_cache[tc] ||= "\e[38;2;#{tc}m".freeze)
223
345
  pfg = tc
224
346
  pbg = nil
225
347
  end
@@ -227,7 +349,9 @@ module Termfront
227
349
  end
228
350
  else
229
351
  if tc != pfg || bc != pbg
230
- 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
231
355
  pfg = tc
232
356
  pbg = bc
233
357
  end
@@ -239,104 +363,101 @@ module Termfront
239
363
  end
240
364
 
241
365
  def render_radar(buf, cols, radar_h, player, enemies, drops, terminals, allies = [])
242
- buf << ("\xE2\x94\x80" * cols)[0, cols * 3] << "\r\n"
366
+ buf << @hrule_cache[cols] << "\r\n"
243
367
 
244
368
  r = Config::RADAR_RADIUS
245
369
  diam = r * 2 + 1
246
-
247
- grid = Array.new(diam) { Array.new(diam, " ") }
248
- diam.times do |ry|
249
- diam.times do |rx|
250
- dx = rx - r
251
- dy = ry - r
252
- d2 = dx * dx + dy * dy
253
- if d2 <= r * r
254
- grid[ry][rx] = "."
255
- elsif d2 <= (r + 1) * (r + 1)
256
- grid[ry][rx] = "#"
257
- end
258
- end
259
- end
260
- grid[r][r] = "^"
370
+ range = Config::RADAR_RANGE
371
+ range_sq = Config::RADAR_RANGE_SQ
372
+ r_sq = r * r
373
+ grid = @radar_grid_template
261
374
 
262
375
  cos_a = Math.cos(-player.angle + Math::PI / 2)
263
376
  sin_a = Math.sin(-player.angle + Math::PI / 2)
264
- 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
+
265
389
  enemies.each do |e|
266
390
  next unless e.alive
267
391
 
268
- ex = e.x - player.x
269
- ey = e.y - player.y
270
- dist = Math.sqrt(ex * ex + ey * ey)
271
- next if dist > Config::RADAR_RANGE
392
+ ex = e.x - px
393
+ ey = e.y - py
394
+ next if ex * ex + ey * ey > range_sq
272
395
 
273
396
  rx = -(ex * cos_a - ey * sin_a)
274
397
  ry = -(ex * sin_a + ey * cos_a)
275
- sx = r + (rx / Config::RADAR_RANGE * r).round
276
- sy = r + (ry / Config::RADAR_RANGE * r).round
277
- 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
278
401
 
279
- d2 = (sx - r)**2 + (sy - r)**2
280
- next if d2 > r * r
402
+ dxr = sx - r
403
+ dyr = sy - r
404
+ next if dxr * dxr + dyr * dyr > r_sq
281
405
 
282
- enemy_cells[[sy, sx]] = e.sprite_id
406
+ enemy_cells[sy * diam + sx] = e.sprite_id
283
407
  end
284
408
 
285
- drop_cells = {}
286
409
  drops.each do |d|
287
- ex = d.x - player.x
288
- ey = d.y - player.y
289
- dist = Math.sqrt(ex * ex + ey * ey)
290
- next if dist > Config::RADAR_RANGE
410
+ ex = d.x - px
411
+ ey = d.y - py
412
+ next if ex * ex + ey * ey > range_sq
291
413
 
292
414
  rx = -(ex * cos_a - ey * sin_a)
293
415
  ry = -(ex * sin_a + ey * cos_a)
294
- sx = r + (rx / Config::RADAR_RANGE * r).round
295
- sy = r + (ry / Config::RADAR_RANGE * r).round
296
- 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
297
419
 
298
- d2 = (sx - r)**2 + (sy - r)**2
299
- next if d2 > r * r
420
+ dxr = sx - r
421
+ dyr = sy - r
422
+ next if dxr * dxr + dyr * dyr > r_sq
300
423
 
301
- drop_cells[[sy, sx]] = d
424
+ drop_cells[sy * diam + sx] = d
302
425
  end
303
426
 
304
- terminal_cells = {}
305
427
  terminals.each do |terminal|
306
- ex = terminal[:x] - player.x
307
- ey = terminal[:y] - player.y
308
- dist = Math.sqrt(ex * ex + ey * ey)
309
- next if dist > Config::RADAR_RANGE
428
+ ex = terminal[:x] - px
429
+ ey = terminal[:y] - py
430
+ next if ex * ex + ey * ey > range_sq
310
431
 
311
432
  rx = -(ex * cos_a - ey * sin_a)
312
433
  ry = -(ex * sin_a + ey * cos_a)
313
- sx = r + (rx / Config::RADAR_RANGE * r).round
314
- sy = r + (ry / Config::RADAR_RANGE * r).round
315
- 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
316
437
 
317
- d2 = (sx - r)**2 + (sy - r)**2
318
- next if d2 > r * r
438
+ dxr = sx - r
439
+ dyr = sy - r
440
+ next if dxr * dxr + dyr * dyr > r_sq
319
441
 
320
- terminal_cells[[sy, sx]] = terminal
442
+ terminal_cells[sy * diam + sx] = terminal
321
443
  end
322
444
 
323
- ally_cells = {}
324
445
  allies.each do |ally|
325
- ex = ally.x - player.x
326
- ey = ally.y - player.y
327
- dist = Math.sqrt(ex * ex + ey * ey)
328
- next if dist > Config::RADAR_RANGE
446
+ ex = ally.x - px
447
+ ey = ally.y - py
448
+ next if ex * ex + ey * ey > range_sq
329
449
 
330
450
  rx = -(ex * cos_a - ey * sin_a)
331
451
  ry = -(ex * sin_a + ey * cos_a)
332
- sx = r + (rx / Config::RADAR_RANGE * r).round
333
- sy = r + (ry / Config::RADAR_RANGE * r).round
334
- 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
335
455
 
336
- d2 = (sx - r)**2 + (sy - r)**2
337
- next if d2 > r * r
456
+ dxr = sx - r
457
+ dyr = sy - r
458
+ next if dxr * dxr + dyr * dyr > r_sq
338
459
 
339
- ally_cells[[sy, sx]] = true
460
+ ally_cells[sy * diam + sx] = true
340
461
  end
341
462
 
342
463
  alive_count = enemies.count(&:alive)
@@ -344,220 +465,258 @@ module Termfront
344
465
  info_lines = [
345
466
  "Enemies: #{alive_count}/#{total_count}",
346
467
  "Heading: #{format("%.0f", (player.angle % (Math::PI * 2)) * 180 / Math::PI)}\xC2\xB0",
347
- "Pos: (#{"%.1f" % player.x}, #{"%.1f" % player.y}) T:terminal"
468
+ "Pos: (#{"%.1f" % px}, #{"%.1f" % py}) T:terminal"
348
469
  ]
349
470
 
350
- radar_h.times do |row|
351
- line = +""
471
+ row = 0
472
+ while row < radar_h
473
+ line = @radar_line_buf.clear
352
474
  if row < diam
353
475
  line << " "
354
- diam.times do |cx|
355
- if (etype = enemy_cells[[row, cx]])
356
- ec = etype == :executor ? "\e[95m" : "\e[91m"
357
- line << "#{ec}*\e[0m"
358
- elsif ally_cells[[row, cx]]
359
- line << "\e[96m+\e[0m"
360
- elsif (drop = drop_cells[[row, cx]])
361
- dc = drop.type.to_s.start_with?("shock") ? "\e[96m" : "\e[93m"
362
- dl = Weapon::Base.registry[drop.type].new.name[0]
363
- line << "#{dc}#{dl}\e[0m"
364
- elsif terminal_cells[[row, cx]]
365
- line << "\e[96mT\e[0m"
476
+ cx = 0
477
+ base = row * diam
478
+ while cx < diam
479
+ key = base + cx
480
+ if (etype = enemy_cells[key])
481
+ line << (etype == :executor ? RADAR_EXECUTOR : RADAR_CRAWLER)
482
+ elsif ally_cells[key]
483
+ line << RADAR_ALLY
484
+ elsif (drop = drop_cells[key])
485
+ line << radar_drop_glyph(drop)
486
+ elsif terminal_cells[key]
487
+ line << RADAR_TERMINAL
366
488
  elsif row == r && cx == r
367
- line << "\e[92m^\e[0m"
489
+ line << RADAR_PLAYER
368
490
  elsif grid[row][cx] == "#"
369
- line << "\e[90m#\e[0m"
491
+ line << RADAR_WALL
370
492
  else
371
493
  line << grid[row][cx]
372
494
  end
495
+ cx += 1
373
496
  end
374
497
  line << (row < info_lines.size ? " #{info_lines[row]}" : "")
375
498
  end
376
499
  buf << TerminalOutput.fit_ansi(line, cols)
377
500
  buf << "\r\n" if row < radar_h - 1
501
+ row += 1
378
502
  end
379
503
  end
380
504
 
381
505
  def overlay_enemies_3d(pixels, view_h, view_w, dists, player, enemies, projectiles, drops)
382
506
  dx = Math.cos(player.angle)
383
507
  dy = Math.sin(player.angle)
384
- px = -dy * Math.tan(Config::FOV / 2.0)
385
- 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
386
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
387
515
  inv = 1.0 / (px * dy - py * dx)
516
+ player_x = player.x
517
+ player_y = player.y
388
518
 
389
- sprites = []
519
+ @enemy_sprites.clear
390
520
  enemies.each do |e|
391
521
  next unless e.alive
392
522
 
393
- ex = e.x - player.x
394
- ey = e.y - player.y
523
+ ex = e.x - player_x
524
+ ey = e.y - player_y
395
525
  tx = inv * (dy * ex - dx * ey)
396
526
  tz = inv * (-py * ex + px * ey)
397
527
  next if tz < 0.2
398
528
 
399
- sprites << [tz, tx, e]
529
+ @enemy_sprites << [tz, tx, e]
400
530
  end
401
531
 
402
- proj_sprites = []
532
+ @proj_sprites.clear
403
533
  projectiles.each do |p|
404
- ex = p.x - player.x
405
- ey = p.y - player.y
534
+ ex = p.x - player_x
535
+ ey = p.y - player_y
406
536
  tx = inv * (dy * ex - dx * ey)
407
537
  tz = inv * (-py * ex + px * ey)
408
538
  next if tz < 0.2
409
539
 
410
- proj_sprites << [tz, tx, p]
540
+ @proj_sprites << [tz, tx, p]
411
541
  end
412
542
 
413
- sprites.sort_by! { |s| -s[0] }
543
+ @enemy_sprites.sort! { |a, b| b[0] <=> a[0] }
414
544
 
415
- sprites.each do |tz, tx, e|
416
- sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
545
+ @enemy_sprites.each do |tz, tx, e|
546
+ sx = (half_view_w * (1 + tx / tz)).to_i
417
547
  sprite_h = (virt_h / tz).to_i
418
- draw_top = [(virt_h / 2 - sprite_h / 2), 0].max
419
- draw_bot = [(virt_h / 2 + sprite_h / 2), virt_h].min
420
- sprite_w = (sprite_h / 2.0).to_i
421
- start_x = [sx - sprite_w / 2, 0].max
422
- 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
423
557
 
424
558
  actual_h = draw_bot - draw_top
425
559
  actual_w = end_x - start_x + 1
426
560
  next if actual_h < 1 || actual_w < 1
427
561
 
428
- 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"
429
564
  use_shape = actual_h >= 6
430
-
431
- start_x.upto(end_x) do |c|
432
- next if c < 0 || c >= view_w
433
- next if dists[c] < tz
434
-
435
- nx = (c - start_x).to_f / actual_w
436
-
437
- r_top = (draw_top / 2.0).ceil
438
- r_bot = (draw_bot / 2.0).floor
439
- r_top.upto(r_bot - 1) do |r|
440
- vp0 = r * 2
441
- vp1 = r * 2 + 1
442
- top_in = vp0 >= draw_top && vp0 < draw_bot
443
- bot_in = vp1 >= draw_top && vp1 < draw_bot
444
- next unless top_in || bot_in
445
-
446
- if use_shape
447
- ny0 = top_in ? (vp0 - draw_top).to_f / actual_h : nil
448
- ny1 = bot_in ? (vp1 - draw_top).to_f / actual_h : nil
449
- top_color = ny0 ? Sprite.for(e.sprite_id, nx, ny0) : nil
450
- bot_color = ny1 ? Sprite.for(e.sprite_id, nx, ny1) : nil
451
- next unless top_color || bot_color
452
-
453
- pixels[vp0][c] = top_color if top_color
454
- pixels[vp1][c] = bot_color if bot_color
455
- else
456
- pixels[vp0][c] = fallback_color if top_in
457
- 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
458
598
  end
459
599
  end
600
+ c += 1
460
601
  end
461
602
 
462
603
  next unless e.max_hp > 1
463
604
 
464
- bar_row = (draw_top / 2.0).ceil - 1
605
+ bar_row = r_top - 1
465
606
  next unless bar_row >= 0 && bar_row < view_h
466
607
 
467
- bar_w = [actual_w, 2].max
468
- bar_sx = [sx - bar_w / 2, 0].max
469
- 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
470
613
  hp_pct = e.hp.to_f / e.max_hp
471
614
  filled = (hp_pct * (bar_ex - bar_sx + 1)).ceil
472
- bar_sx.upto(bar_ex) do |c|
473
- next if c < 0 || c >= view_w
474
- next if dists[c] < tz
475
-
476
- ci = c - bar_sx
477
- color = ci < filled ? "0;200;0" : "200;0;0"
478
- pixels[bar_row * 2][c] = color
479
- 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
480
626
  end
481
627
  end
482
628
 
483
629
  # Render weapon drops
484
- drop_sprites = []
630
+ @drop_sprites.clear
485
631
  drops.each do |d|
486
- ex = d.x - player.x
487
- ey = d.y - player.y
632
+ ex = d.x - player_x
633
+ ey = d.y - player_y
488
634
  tx = inv * (dy * ex - dx * ey)
489
635
  tz = inv * (-py * ex + px * ey)
490
636
  next if tz < 0.2
491
637
 
492
- drop_sprites << [tz, tx, d]
638
+ @drop_sprites << [tz, tx, d]
493
639
  end
494
- drop_sprites.sort_by! { |s| -s[0] }
495
-
496
- drop_sprites.each do |tz, tx, d|
497
- sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
498
- sprite_h = (virt_h / tz * 0.3).to_i.clamp(2, virt_h / 2)
499
- ground = (virt_h / 2 + virt_h / tz * 0.35).to_i
500
- draw_bot = [ground, virt_h].min
501
- draw_top = [draw_bot - sprite_h, 0].max
502
- sprite_w = (sprite_h / 2.0).to_i.clamp(1, 6)
503
- start_x = [sx - sprite_w / 2, 0].max
504
- end_x = [sx + sprite_w / 2, view_w - 1].min
640
+ @drop_sprites.sort! { |a, b| b[0] <=> a[0] }
641
+
642
+ @drop_sprites.each do |tz, tx, d|
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
505
654
 
506
655
  color = d.sprite_color
507
-
508
- start_x.upto(end_x) do |c|
509
- next if c < 0 || c >= view_w
510
- next if dists[c] < tz
511
-
512
- r_top = (draw_top / 2.0).ceil
513
- r_bot = (draw_bot / 2.0).floor
514
- r_top.upto(r_bot - 1) do |r|
515
- next if r < 0 || r >= view_h
516
-
517
- vp0 = r * 2
518
- vp1 = r * 2 + 1
519
- top_in = vp0 >= draw_top && vp0 < draw_bot
520
- bot_in = vp1 >= draw_top && vp1 < draw_bot
521
- next unless top_in || bot_in
522
-
523
- pixels[vp0][c] = color if top_in
524
- 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
525
676
  end
677
+ c += 1
526
678
  end
527
679
  end
528
680
 
529
681
  # Render projectiles
530
- proj_sprites.sort_by! { |s| -s[0] }
531
- proj_sprites.each do |tz, tx, p|
532
- sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
682
+ @proj_sprites.sort! { |a, b| b[0] <=> a[0] }
683
+ @proj_sprites.each do |tz, tx, p|
684
+ sx = (half_view_w * (1 + tx / tz)).to_i
533
685
  pw = (4.0 / tz).ceil.clamp(1, 5)
534
686
  ph = (virt_h / tz * 0.15).ceil.clamp(2, 6)
535
- vmid = virt_h / 2
536
- draw_top = [(vmid - ph / 2), 0].max
537
- draw_bot = [(vmid + ph / 2).clamp(draw_top + 2, virt_h), virt_h].min
538
- start_x = [sx - pw / 2, 0].max
539
- end_x = [sx + pw / 2, view_w - 1].min
540
- col_code = p.type == :executor ? "94" : "93"
541
-
542
- start_x.upto(end_x) do |c|
543
- next if c < 0 || c >= view_w
544
- next if dists[c] < tz
545
-
546
- r_top = (draw_top / 2.0).ceil
547
- r_bot = [(draw_bot / 2.0).floor, r_top + 1].max
548
- r_top.upto(r_bot - 1) do |r|
549
- next if r < 0 || r >= view_h
550
-
551
- vp0 = r * 2
552
- vp1 = r * 2 + 1
553
- top_in = vp0 >= draw_top && vp0 < draw_bot
554
- bot_in = vp1 >= draw_top && vp1 < draw_bot
555
- next unless top_in || bot_in
556
-
557
- proj_color = col_code == "94" ? "94;94;255" : "255;210;80"
558
- pixels[vp0][c] = proj_color if top_in
559
- 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
560
718
  end
719
+ c += 1
561
720
  end
562
721
  end
563
722
  end
@@ -570,7 +729,7 @@ module Termfront
570
729
  virt_h = view_h * 2
571
730
  inv = 1.0 / (px * dy - py * dx)
572
731
 
573
- sprites = []
732
+ @ally_sprites.clear
574
733
  allies.each do |ally|
575
734
  ex = ally.x - player.x
576
735
  ey = ally.y - player.y
@@ -578,11 +737,11 @@ module Termfront
578
737
  tz = inv * (-py * ex + px * ey)
579
738
  next if tz < 0.2
580
739
 
581
- sprites << [tz, tx, ally]
740
+ @ally_sprites << [tz, tx, ally]
582
741
  end
583
- sprites.sort_by! { |s| -s[0] }
742
+ @ally_sprites.sort! { |a, b| b[0] <=> a[0] }
584
743
 
585
- sprites.each do |tz, tx, ally|
744
+ @ally_sprites.each do |tz, tx, ally|
586
745
  sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
587
746
  sprite_h = (virt_h / tz).to_i
588
747
  draw_top = [(virt_h / 2 - sprite_h / 2), 0].max
@@ -690,17 +849,17 @@ module Termfront
690
849
 
691
850
  def ansi_fg(color)
692
851
  if color.is_a?(Integer)
693
- "\e[38;5;#{color}m"
852
+ FG_256[color]
694
853
  else
695
- "\e[38;2;#{color}m"
854
+ @fg_truecolor_cache[color] ||= "\e[38;2;#{color}m".freeze
696
855
  end
697
856
  end
698
857
 
699
858
  def ansi_bg(color)
700
859
  if color.is_a?(Integer)
701
- "\e[48;5;#{color}m"
860
+ BG_256[color]
702
861
  else
703
- "\e[48;2;#{color}m"
862
+ @bg_truecolor_cache[color] ||= "\e[48;2;#{color}m".freeze
704
863
  end
705
864
  end
706
865
  end