termfront 0.1.4 → 0.1.5

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: 2169347d66dd50b7734b9de8afc2398a0b979649cd98cb36aa6cc6dcdc9373f5
4
- data.tar.gz: afd578bf2aabf93476c81d96fdcceeb5d55289d6137dade78429b75bdd84753a
3
+ metadata.gz: 68852aaca6be7127d8b644aae4670a56e72d5528b249dc081d72ba0d22c5f74a
4
+ data.tar.gz: 440209c869681acd0f9d77d65fb367e6d055c2ffe3a5a57821dd530912199ecd
5
5
  SHA512:
6
- metadata.gz: 4bf5e4ee9f0c736626f7c73f05ce04a5a2c128f1c94193d9f74ae6c7788297561f24244d7ae65d03f5b6955ba10d772bfe9ba3a017de4d2bac18b8a527020a8b
7
- data.tar.gz: d317a9c0ac3ba86c633c3fb1f8b5af951bda951bcc2418213457535ec32d3e0d2471fca9c3e34588f5f134800c46a495a68cbfa58f86f9349739e1dc206ffe7d
6
+ metadata.gz: 6efb979956fa649ee558ad0190547fd70c62fda5ef8b8ca4dc06f513c15c87c02c74151756486615a0cc21301e6b45d6f62e01e58230ae28b574ceb3570053ae
7
+ data.tar.gz: 12dfe9fde65aa1517facf4e7e21a37b6db9550e5cc99663801efb33db74f1a1b88af2503c6c6c54be70d60374328009f85334422413737514845602091f70893
data/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.5] - 2026-05-25
10
+
11
+ ### Changed
12
+
13
+ - Radar distance culling now compares squared distances against `Config::RADAR_RANGE_SQ` instead of taking a square root per entity per frame
14
+ - Renderer reuses the per-column raycast buffers and the virtual pixel grid across frames instead of allocating fresh arrays each tick, only re-allocating when the terminal is resized
15
+ - Build the radar background grid, horizontal rule, and ANSI-styled radar glyphs once and reuse them across frames
16
+ - Look up 256-color SGR escape sequences from a precomputed table and memoize truecolor SGR sequences to avoid rebuilding the same ANSI strings every cell
17
+ - Reuse renderer sprite collection arrays and the radar line buffer across frames, and sort sprites with a comparator block instead of `sort_by`
18
+ - Cache the terminal `winsize` inside the renderer and invalidate the cache from a `SIGWINCH` handler instead of issuing an ioctl every frame
19
+ - PvP hit raycast now culls candidates beyond `MAX_PVP_RANGE` and skips the line-of-sight check when the candidate is already farther than the current best
20
+ - Multiplayer server `broadcast` now serializes each outgoing message once and writes the same JSON line to every recipient
21
+ - PvP server now aggregates outgoing player state on a 30 Hz server tick instead of relaying each incoming state message immediately, reducing TLS write bursts on small VMs
22
+ - PvP match loop `IO.select` timeout shortened from 500 ms to ~16 ms so the new 30 Hz broadcast tick is not delayed by idle reads
23
+ - PvP client opponent interpolation now converges within ~40 ms (lerp factor raised from 15 to 25) so remote players track the new 30 Hz broadcast tick within a single frame
24
+ - Run the title demo, campaign missions, Wavesfight, and PvP at 60 FPS
25
+
26
+ ### Removed
27
+
28
+ - Stopped emitting DEC 2026 synchronized update escape sequences around each frame and removed the `TERMFRONT_SYNC_UPDATES` environment switch; non-supporting terminals (some SSH paths, older xterm) were paying parsing cost for shapes they ignore, while supporting terminals show no visible regression
29
+
30
+ ### Fixed
31
+
32
+ - Wavesfight wave advance now fully restores each surviving and revived player's shield to `Config::SHIELD_MAX`; previously only a +35 partial recovery was applied, which left revived players at 35%
33
+
9
34
  ## [0.1.34] - 2026-05-25
10
35
 
11
36
  ### Fixed
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Termfront
4
4
  module Config
5
- FRAME_DT = 1.0 / 30.0
5
+ FRAME_DT = 1.0 / 60.0
6
6
  FOV = 66.0 * Math::PI / 180.0
7
7
  PLAYER_RADIUS = 0.2
8
8
  KEY_TIMEOUT = 5
@@ -29,6 +29,7 @@ module Termfront
29
29
 
30
30
  RADAR_RADIUS = 3
31
31
  RADAR_RANGE = 12.0
32
+ RADAR_RANGE_SQ = RADAR_RANGE * RADAR_RANGE
32
33
 
33
34
  PVP_PORT = 7777
34
35
  PVP_DEFAULT_ADDRESS = "termfront.gamelinks007.net:443"
@@ -10,6 +10,7 @@ module Termfront
10
10
  @scene_player = ScenePlayer.new(@stdout, audio: @audio)
11
11
  @demo_player = DemoPlayer.new(@stdout, @renderer)
12
12
  @difficulty = nil
13
+ Signal.trap("WINCH") { @renderer.invalidate_size_cache! }
13
14
  end
14
15
 
15
16
  def start
@@ -413,7 +414,7 @@ module Termfront
413
414
  end
414
415
 
415
416
  def replenish_wavesfight_loadout
416
- @player.shield = [@player.shield + 35.0, Config::SHIELD_MAX].min
417
+ @player.shield = Config::SHIELD_MAX
417
418
  @player.health = [@player.health + 20.0, Config::HEALTH_MAX].min
418
419
  @player.last_damage = -Config::SHIELD_DELAY
419
420
  @player.dead = false
@@ -392,7 +392,7 @@ module Termfront
392
392
 
393
393
  def interpolate_opponents(dt)
394
394
  @remotes.each_value do |remote|
395
- remote[:lerp_t] = [remote[:lerp_t] + dt * 15.0, 1.0].min
395
+ remote[:lerp_t] = [remote[:lerp_t] + dt * 25.0, 1.0].min
396
396
  t = remote[:lerp_t]
397
397
  remote[:render].x = remote[:prev].x + (remote[:current].x - remote[:prev].x) * t
398
398
  remote[:render].y = remote[:prev].y + (remote[:current].y - remote[:prev].y) * t
@@ -28,6 +28,10 @@ module Termfront
28
28
  }.freeze
29
29
  DEFAULT_RATE_LIMIT = 10
30
30
  MAX_DROPPED_MSGS = 200
31
+ MAX_PVP_RANGE = 30.0
32
+ STATE_BROADCAST_HZ = 30
33
+ STATE_BROADCAST_DT = 1.0 / STATE_BROADCAST_HZ
34
+ SELECT_TIMEOUT = 1.0 / 60.0
31
35
  PVP_MAP = [
32
36
  "####################",
33
37
  "#........##........#",
@@ -271,6 +275,7 @@ module Termfront
271
275
  match_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
272
276
  last_activity = match_start
273
277
  last_tick_at = match_start
278
+ last_state_flush_at = match_start
274
279
 
275
280
  loop do
276
281
  sockets = roster.filter_map do |player|
@@ -281,7 +286,7 @@ module Termfront
281
286
  end
282
287
  break if sockets.empty?
283
288
 
284
- readable, = IO.select(sockets, nil, nil, 0.5)
289
+ readable, = IO.select(sockets, nil, nil, SELECT_TIMEOUT)
285
290
 
286
291
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
287
292
  if (reason = match_timeout_reason(now, match_start, last_activity))
@@ -295,6 +300,11 @@ module Termfront
295
300
  roster.each { |player| regen_player(player, dt, now) }
296
301
  last_tick_at = now
297
302
 
303
+ if now - last_state_flush_at >= STATE_BROADCAST_DT
304
+ flush_pending_states(roster)
305
+ last_state_flush_at = now
306
+ end
307
+
298
308
  next unless readable
299
309
 
300
310
  last_activity = now
@@ -388,7 +398,7 @@ module Termfront
388
398
  msg = msg.merge(ff: ff || 0)
389
399
  end
390
400
  msg = msg.merge(s: player[:shield].round(1), h: player[:health].round(1))
391
- broadcast(roster, msg.merge(from: player[:id]), except: player[:id])
401
+ player[:pending_state] = msg.merge(from: player[:id])
392
402
  when "hit"
393
403
  route_hit(roster, player, msg, Process.clock_gettime(Process::CLOCK_MONOTONIC))
394
404
  when "dead"
@@ -430,11 +440,12 @@ module Termfront
430
440
  oy = other[:y] - attacker[:y]
431
441
  dot = ox * dx + oy * dy
432
442
  next if dot < 0.1
443
+ next if dot > MAX_PVP_RANGE
444
+ next unless dot < best_dot
433
445
 
434
446
  perp = (ox * (-dy) + oy * dx).abs
435
447
  next if perp > weapon.hit_width
436
448
  next unless pvp_map.line_of_sight?(attacker[:x], attacker[:y], other[:x], other[:y])
437
- next unless dot < best_dot
438
449
 
439
450
  best = other
440
451
  best_dot = dot
@@ -450,15 +461,30 @@ module Termfront
450
461
  end
451
462
 
452
463
  def broadcast(roster, msg, except: nil)
464
+ line = JSON.generate(msg) + "\n"
453
465
  roster.each do |player|
454
466
  next if player[:id] == except
455
467
 
456
- send_json(player[:socket], msg)
468
+ write_line(player[:socket], line)
469
+ end
470
+ end
471
+
472
+ def flush_pending_states(roster)
473
+ roster.each do |player|
474
+ state = player[:pending_state]
475
+ next unless state
476
+
477
+ broadcast(roster, state, except: player[:id])
478
+ player[:pending_state] = nil
457
479
  end
458
480
  end
459
481
 
460
482
  def send_json(socket, msg)
461
- socket.write(JSON.generate(msg) + "\n")
483
+ write_line(socket, JSON.generate(msg) + "\n")
484
+ end
485
+
486
+ def write_line(socket, line)
487
+ socket.write(line)
462
488
  rescue Errno::EPIPE, Errno::ECONNRESET, IOError, OpenSSL::SSL::SSLError
463
489
  nil
464
490
  end
@@ -919,7 +945,7 @@ module Termfront
919
945
 
920
946
  def replenish_wavesfight_roster(roster, session)
921
947
  roster.each do |player|
922
- player[:shield] = [player[:shield] + 35.0, Config::SHIELD_MAX].min
948
+ player[:shield] = Config::SHIELD_MAX
923
949
  player[:health] = [player[:health] + 20.0, Config::HEALTH_MAX].min
924
950
  player[:last_damage] = session[:clock] - Config::SHIELD_DELAY
925
951
  player[:alive] = true
@@ -2,12 +2,40 @@
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
+ @fg_truecolor_cache = {}
23
+ @bg_truecolor_cache = {}
24
+ @enemy_sprites = []
25
+ @proj_sprites = []
26
+ @drop_sprites = []
27
+ @ally_sprites = []
28
+ @radar_line_buf = +""
29
+ @size_cache = nil
30
+ @size_cache_at = -Float::INFINITY
31
+ end
32
+
33
+ def invalidate_size_cache!
34
+ @size_cache = nil
7
35
  end
8
36
 
9
37
  def render(player:, map:, enemies:, projectiles:, drops:, terminals: [], status_line: nil, allies: [])
10
- rows, cols = @stdout.winsize
38
+ rows, cols = current_size
11
39
  rows = [rows, 6].max
12
40
  cols = [cols, 20].max
13
41
 
@@ -16,38 +44,35 @@ module Termfront
16
44
  view_w = cols
17
45
  virt_h = view_h * 2
18
46
 
47
+ prepare_frame_buffers(view_w, virt_h)
48
+
19
49
  dx = Math.cos(player.angle)
20
50
  dy = Math.sin(player.angle)
21
51
  plane_x = -dy * Math.tan(Config::FOV / 2.0)
22
52
  plane_y = dx * Math.tan(Config::FOV / 2.0)
23
53
 
24
- dists = Array.new(view_w)
25
- sides = Array.new(view_w)
26
54
  view_w.times do |c|
27
55
  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)
56
+ @dists[c], @sides[c] = cast_ray(map, player.x, player.y, dx + plane_x * cam, dy + plane_y * cam)
29
57
  end
30
58
 
31
59
  vmid = virt_h / 2.0
32
- wtop = Array.new(view_w)
33
- wbot = Array.new(view_w)
34
- wcol = Array.new(view_w)
35
60
  view_w.times do |c|
36
- d = dists[c]
61
+ d = @dists[c]
37
62
  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])
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])
41
66
  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)
67
+ build_view_pixels(virt_h, view_w, @wtop, @wbot, @wcol)
68
+ overlay_enemies_3d(@pixels, view_h, view_w, @dists, player, enemies, projectiles, drops)
69
+ overlay_allies_3d(@pixels, view_h, view_w, @dists, player, allies)
70
+ overlay_damage_flash(@pixels, view_h, view_w, player)
46
71
 
47
72
  buf = TerminalOutput.begin_frame(home: true)
48
73
 
49
74
  render_hud(buf, cols, player, drops, terminals, status_line)
50
- render_view(buf, view_h, view_w, pixels)
75
+ render_view(buf, view_h, view_w, @pixels)
51
76
  buf << "\e[#{3 + view_h};1H"
52
77
  render_radar(buf, cols, radar_h, player, enemies, drops, terminals, allies)
53
78
  render_crosshair(buf, view_h, view_w, cols, player)
@@ -132,6 +157,59 @@ module Termfront
132
157
 
133
158
  private
134
159
 
160
+ def current_size
161
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
162
+ if @size_cache.nil? || now - @size_cache_at >= 0.25
163
+ @size_cache = @stdout.winsize
164
+ @size_cache_at = now
165
+ end
166
+ @size_cache
167
+ end
168
+
169
+ def build_radar_grid_template
170
+ r = Config::RADAR_RADIUS
171
+ diam = r * 2 + 1
172
+ Array.new(diam) do |ry|
173
+ Array.new(diam) do |rx|
174
+ if ry == r && rx == r
175
+ "^"
176
+ else
177
+ dx = rx - r
178
+ dy = ry - r
179
+ d2 = dx * dx + dy * dy
180
+ if d2 <= r * r
181
+ "."
182
+ elsif d2 <= (r + 1) * (r + 1)
183
+ "#"
184
+ else
185
+ " "
186
+ end
187
+ end
188
+ end.freeze
189
+ end.freeze
190
+ end
191
+
192
+ def radar_drop_glyph(drop)
193
+ @radar_drop_glyphs[drop.type] ||= begin
194
+ dc = drop.type.to_s.start_with?("shock") ? "\e[96m" : "\e[93m"
195
+ dl = Weapon::Base.registry[drop.type].new.name[0]
196
+ "#{dc}#{dl}\e[0m".freeze
197
+ end
198
+ end
199
+
200
+ def prepare_frame_buffers(view_w, virt_h)
201
+ if @buf_view_w != view_w || @buf_virt_h != virt_h
202
+ @buf_view_w = view_w
203
+ @buf_virt_h = virt_h
204
+ @dists = Array.new(view_w)
205
+ @sides = Array.new(view_w)
206
+ @wtop = Array.new(view_w)
207
+ @wbot = Array.new(view_w)
208
+ @wcol = Array.new(view_w)
209
+ @pixels = Array.new(virt_h) { Array.new(view_w) }
210
+ end
211
+ end
212
+
135
213
  def render_hud(buf, cols, player, drops, terminals, status_line)
136
214
  bar_w = [cols - 20, 10].max
137
215
  pct = player.shield / Config::SHIELD_MAX.to_f
@@ -180,9 +258,8 @@ module Termfront
180
258
  end
181
259
 
182
260
  def build_view_pixels(virt_h, view_w, wtop, wbot, wcol)
183
- pixels = Array.new(virt_h) { Array.new(view_w) }
184
261
  virt_h.times do |vr|
185
- row = pixels[vr]
262
+ row = @pixels[vr]
186
263
  view_w.times do |c|
187
264
  row[c] = if vr < wtop[c]
188
265
  Config::CEIL_C
@@ -193,7 +270,6 @@ module Termfront
193
270
  end
194
271
  end
195
272
  end
196
- pixels
197
273
  end
198
274
 
199
275
  def render_view(buf, view_h, view_w, pixels)
@@ -239,25 +315,11 @@ module Termfront
239
315
  end
240
316
 
241
317
  def render_radar(buf, cols, radar_h, player, enemies, drops, terminals, allies = [])
242
- buf << ("\xE2\x94\x80" * cols)[0, cols * 3] << "\r\n"
318
+ buf << @hrule_cache[cols] << "\r\n"
243
319
 
244
320
  r = Config::RADAR_RADIUS
245
321
  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] = "^"
322
+ grid = @radar_grid_template
261
323
 
262
324
  cos_a = Math.cos(-player.angle + Math::PI / 2)
263
325
  sin_a = Math.sin(-player.angle + Math::PI / 2)
@@ -267,8 +329,7 @@ module Termfront
267
329
 
268
330
  ex = e.x - player.x
269
331
  ey = e.y - player.y
270
- dist = Math.sqrt(ex * ex + ey * ey)
271
- next if dist > Config::RADAR_RANGE
332
+ next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
272
333
 
273
334
  rx = -(ex * cos_a - ey * sin_a)
274
335
  ry = -(ex * sin_a + ey * cos_a)
@@ -286,8 +347,7 @@ module Termfront
286
347
  drops.each do |d|
287
348
  ex = d.x - player.x
288
349
  ey = d.y - player.y
289
- dist = Math.sqrt(ex * ex + ey * ey)
290
- next if dist > Config::RADAR_RANGE
350
+ next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
291
351
 
292
352
  rx = -(ex * cos_a - ey * sin_a)
293
353
  ry = -(ex * sin_a + ey * cos_a)
@@ -305,8 +365,7 @@ module Termfront
305
365
  terminals.each do |terminal|
306
366
  ex = terminal[:x] - player.x
307
367
  ey = terminal[:y] - player.y
308
- dist = Math.sqrt(ex * ex + ey * ey)
309
- next if dist > Config::RADAR_RANGE
368
+ next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
310
369
 
311
370
  rx = -(ex * cos_a - ey * sin_a)
312
371
  ry = -(ex * sin_a + ey * cos_a)
@@ -324,8 +383,7 @@ module Termfront
324
383
  allies.each do |ally|
325
384
  ex = ally.x - player.x
326
385
  ey = ally.y - player.y
327
- dist = Math.sqrt(ex * ex + ey * ey)
328
- next if dist > Config::RADAR_RANGE
386
+ next if ex * ex + ey * ey > Config::RADAR_RANGE_SQ
329
387
 
330
388
  rx = -(ex * cos_a - ey * sin_a)
331
389
  ry = -(ex * sin_a + ey * cos_a)
@@ -348,25 +406,22 @@ module Termfront
348
406
  ]
349
407
 
350
408
  radar_h.times do |row|
351
- line = +""
409
+ line = @radar_line_buf.clear
352
410
  if row < diam
353
411
  line << " "
354
412
  diam.times do |cx|
355
413
  if (etype = enemy_cells[[row, cx]])
356
- ec = etype == :executor ? "\e[95m" : "\e[91m"
357
- line << "#{ec}*\e[0m"
414
+ line << (etype == :executor ? RADAR_EXECUTOR : RADAR_CRAWLER)
358
415
  elsif ally_cells[[row, cx]]
359
- line << "\e[96m+\e[0m"
416
+ line << RADAR_ALLY
360
417
  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"
418
+ line << radar_drop_glyph(drop)
364
419
  elsif terminal_cells[[row, cx]]
365
- line << "\e[96mT\e[0m"
420
+ line << RADAR_TERMINAL
366
421
  elsif row == r && cx == r
367
- line << "\e[92m^\e[0m"
422
+ line << RADAR_PLAYER
368
423
  elsif grid[row][cx] == "#"
369
- line << "\e[90m#\e[0m"
424
+ line << RADAR_WALL
370
425
  else
371
426
  line << grid[row][cx]
372
427
  end
@@ -386,7 +441,7 @@ module Termfront
386
441
  virt_h = view_h * 2
387
442
  inv = 1.0 / (px * dy - py * dx)
388
443
 
389
- sprites = []
444
+ @enemy_sprites.clear
390
445
  enemies.each do |e|
391
446
  next unless e.alive
392
447
 
@@ -396,10 +451,10 @@ module Termfront
396
451
  tz = inv * (-py * ex + px * ey)
397
452
  next if tz < 0.2
398
453
 
399
- sprites << [tz, tx, e]
454
+ @enemy_sprites << [tz, tx, e]
400
455
  end
401
456
 
402
- proj_sprites = []
457
+ @proj_sprites.clear
403
458
  projectiles.each do |p|
404
459
  ex = p.x - player.x
405
460
  ey = p.y - player.y
@@ -407,12 +462,12 @@ module Termfront
407
462
  tz = inv * (-py * ex + px * ey)
408
463
  next if tz < 0.2
409
464
 
410
- proj_sprites << [tz, tx, p]
465
+ @proj_sprites << [tz, tx, p]
411
466
  end
412
467
 
413
- sprites.sort_by! { |s| -s[0] }
468
+ @enemy_sprites.sort! { |a, b| b[0] <=> a[0] }
414
469
 
415
- sprites.each do |tz, tx, e|
470
+ @enemy_sprites.each do |tz, tx, e|
416
471
  sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
417
472
  sprite_h = (virt_h / tz).to_i
418
473
  draw_top = [(virt_h / 2 - sprite_h / 2), 0].max
@@ -481,7 +536,7 @@ module Termfront
481
536
  end
482
537
 
483
538
  # Render weapon drops
484
- drop_sprites = []
539
+ @drop_sprites.clear
485
540
  drops.each do |d|
486
541
  ex = d.x - player.x
487
542
  ey = d.y - player.y
@@ -489,11 +544,11 @@ module Termfront
489
544
  tz = inv * (-py * ex + px * ey)
490
545
  next if tz < 0.2
491
546
 
492
- drop_sprites << [tz, tx, d]
547
+ @drop_sprites << [tz, tx, d]
493
548
  end
494
- drop_sprites.sort_by! { |s| -s[0] }
549
+ @drop_sprites.sort! { |a, b| b[0] <=> a[0] }
495
550
 
496
- drop_sprites.each do |tz, tx, d|
551
+ @drop_sprites.each do |tz, tx, d|
497
552
  sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
498
553
  sprite_h = (virt_h / tz * 0.3).to_i.clamp(2, virt_h / 2)
499
554
  ground = (virt_h / 2 + virt_h / tz * 0.35).to_i
@@ -527,8 +582,8 @@ module Termfront
527
582
  end
528
583
 
529
584
  # Render projectiles
530
- proj_sprites.sort_by! { |s| -s[0] }
531
- proj_sprites.each do |tz, tx, p|
585
+ @proj_sprites.sort! { |a, b| b[0] <=> a[0] }
586
+ @proj_sprites.each do |tz, tx, p|
532
587
  sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
533
588
  pw = (4.0 / tz).ceil.clamp(1, 5)
534
589
  ph = (virt_h / tz * 0.15).ceil.clamp(2, 6)
@@ -570,7 +625,7 @@ module Termfront
570
625
  virt_h = view_h * 2
571
626
  inv = 1.0 / (px * dy - py * dx)
572
627
 
573
- sprites = []
628
+ @ally_sprites.clear
574
629
  allies.each do |ally|
575
630
  ex = ally.x - player.x
576
631
  ey = ally.y - player.y
@@ -578,11 +633,11 @@ module Termfront
578
633
  tz = inv * (-py * ex + px * ey)
579
634
  next if tz < 0.2
580
635
 
581
- sprites << [tz, tx, ally]
636
+ @ally_sprites << [tz, tx, ally]
582
637
  end
583
- sprites.sort_by! { |s| -s[0] }
638
+ @ally_sprites.sort! { |a, b| b[0] <=> a[0] }
584
639
 
585
- sprites.each do |tz, tx, ally|
640
+ @ally_sprites.each do |tz, tx, ally|
586
641
  sx = ((view_w / 2.0) * (1 + tx / tz)).to_i
587
642
  sprite_h = (virt_h / tz).to_i
588
643
  draw_top = [(virt_h / 2 - sprite_h / 2), 0].max
@@ -690,17 +745,17 @@ module Termfront
690
745
 
691
746
  def ansi_fg(color)
692
747
  if color.is_a?(Integer)
693
- "\e[38;5;#{color}m"
748
+ FG_256[color]
694
749
  else
695
- "\e[38;2;#{color}m"
750
+ @fg_truecolor_cache[color] ||= "\e[38;2;#{color}m".freeze
696
751
  end
697
752
  end
698
753
 
699
754
  def ansi_bg(color)
700
755
  if color.is_a?(Integer)
701
- "\e[48;5;#{color}m"
756
+ BG_256[color]
702
757
  else
703
- "\e[48;2;#{color}m"
758
+ @bg_truecolor_cache[color] ||= "\e[48;2;#{color}m".freeze
704
759
  end
705
760
  end
706
761
  end
@@ -5,20 +5,15 @@ module Termfront
5
5
  module_function
6
6
  ANSI_PATTERN = /\e\[[0-9;]*[A-Za-z]/.freeze
7
7
 
8
- def sync_updates?
9
- ENV.fetch("TERMFRONT_SYNC_UPDATES", "1") == "1"
10
- end
11
-
12
8
  def begin_frame(home: false, clear: false)
13
9
  buf = +""
14
- buf << "\e[?2026h" if sync_updates?
15
10
  buf << "\e[H" if home
16
11
  buf << "\e[2J" if clear
17
12
  buf
18
13
  end
19
14
 
20
15
  def end_frame
21
- sync_updates? ? "\e[?2026l" : ""
16
+ ""
22
17
  end
23
18
 
24
19
  def fit_ansi(text, width)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Termfront
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
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.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - S-H-GAMELINKS