termfront 0.1.6 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d84ea40d760b9d62c0037b83a0b83576725a12a16c3aa2937e16025e6a798015
4
- data.tar.gz: e33b7fc4bde17b2924ea6a1f30bb11f6f8d30fbba75bd9f9130c773b1b1cf33a
3
+ metadata.gz: b6d91fa7b7758b2151fc31bb9f7862dbda3ad311edea638aa9b11ca50a8b9a8d
4
+ data.tar.gz: 3a8ce081de76ddd04c45949c490ba4b513f42eb94eb39fce37fef292abdbb004
5
5
  SHA512:
6
- metadata.gz: fc5856f2bb1eea72307f2ba41af96e5b3412e7ef893fe8a3609f0841ab88d693653ed797c072eff8a4092d01511c810edd2fc848c12dcd17a07b8803181cc551
7
- data.tar.gz: 461847cef45b100959a044dcb22cca9c537a806331168834d9f936fd6055c4c668581f376058003ad2eb28abdd805cc3f2d82d6f52a36644b7f7d53a13635f9f
6
+ metadata.gz: a79ab596683eea6f8914c17cf88ad000d7a1b33453820526310f25fd43dbabb1f21e6f65f16a390fa92e491d6cd2da86e981c17bc971161316a6d4f7b431c98a
7
+ data.tar.gz: 556667b34ba430c7af848fca7d31f5937e24d328480a29a3191db88ca4286d579081b0f6e18c34622d87cb9ea401c20e19da6aaf0a6fbc2b892f15fdbb9f7b76
data/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.7] - 2026-05-26
10
+
11
+ ### Changed
12
+
13
+ - Restore the in-game and title screen render rate to 60 Hz (`Config::RENDER_DT = 1.0 / 60.0`) after testers reported choppy motion at 30 Hz; the hybrid rendering scaffolding is kept in place so the rate can be tuned later without code changes
14
+ - Cap the per-frame `dt` at `Config::MAX_DT` (50 ms / 20 FPS-equivalent) in the singleplayer, Wavesfight, and PvP game loops so a frame that runs long under a constrained host no longer translates into a single oversized physics step. Movement, weapon timers, and projectile motion now stay smooth — the simulation simply slows for that frame instead of producing a teleporting opponent or instant fire-rate
15
+ - Hoist the sprite shape function out of the per-cell loop in `Renderer#overlay_enemies_3d`: look up `Sprite::REGISTRY[sprite_id]` once per enemy and call it directly from the inner cell loop instead of going through `Sprite.for` (which re-hashed `REGISTRY` and re-checked the result for `nil` on every cell). With several enemies on-screen the per-cell hash lookup overhead disappears
16
+ - Migrate the PvP/Wavesfight client render paths to 256-color SGR sequences as well: add a `Sprite.player_enemy` variant pre-tinted toward red instead of recomputing the tint per cell, replace the per-cell `\e[38;2;…m` (and `\e[48;2;…m` for damage flash) escapes with `\e[38;5;Nm` lookups, drop the `tint_player_color` helper and its per-cell `String#split`/`Array#map` work, and reuse the renderer's shared `DAMAGE_FLASH_RAMP` for the screen-edge flash
17
+ - Hand frame output off to a dedicated writer thread (`AsyncWriter`) so the game loop's `syswrite` becomes a non-blocking `Queue#push` and the actual `IO#syswrite` runs on a background thread that releases the GVL during the PTY write. The main loop no longer pays the wall-clock cost of waiting for the terminal to drain its buffer; when a write is still in progress, the latest frame replaces the queued one so the terminal always sees the freshest frame instead of a backlog
18
+ - Drop truecolor SGR sequences from the in-game and title 3D views; convert every sprite, fallback, bar, projectile, drop, and damage-flash color to an xterm 256-color index once with `Color.rgb_to_256`, simplify `Renderer#render_view` to a pure 256-color path that drops the per-cell `is_a?(Integer)` test and the truecolor SGR caches, and switch the title screen's half-block compositor to look up its escapes in the shared `FG_256` / `BG_256` tables. The 13-byte truecolor SGR shrinks to a 9-byte 256-color SGR (or shorter on cell boundaries via `\e[K`), the renderer's hottest cell loop loses its type dispatch, and the dead `bg_only?` / `ansi_fg` / `ansi_bg` helpers go away
19
+ - Build the half-block rows in `TitleScreen#render` directly into the final per-row string with the column count already capped, skipping the per-row `TerminalOutput.fit_ansi` walk entirely; rows that come out uniformly the same color collapse to `\e[bg]\e[K\e[0m`, fully-blank rows ship as a single space run, and the byte stream the terminal sees for each title frame shrinks substantially
20
+ - Coalesce same-color cell runs in `TitleScreen#render`'s half-block compositor and only emit SGR escapes on transitions instead of wrapping every cell in `\e[…m…\e[0m`; the per-row string handed to `fit_ansi` shrinks from one bracketed SGR per cell to a handful of color changes, cutting both the title frame's byte volume and `fit_ansi`'s per-row scan cost
21
+ - Emit `\e[K` (Erase to End of Line) for rows in `Renderer#render_view` whose top/bottom cells are all the same integer background color; the ceiling-only and floor-only bands now ship as `\e[bg]\e[K\r\n` per row instead of the full per-cell glyph sequence, slashing the ANSI byte volume of those rows from `view_w` bytes plus per-cell SGR transitions down to about a dozen bytes total
22
+ - Memoize `AudioManager#which` results in a class-level cache so the per-command `PATH` walk (and the per-directory `File.executable?` stat) only runs the first time a binary is looked up; subsequent `AudioManager.new` calls during mode transitions hit the cache instead of re-scanning `PATH`, which removes a noticeable stutter on WSL hosts where Windows-mounted `PATH` entries are slow to stat
23
+ - Coalesce same-color runs of cells in `Renderer#render_view` with a single `String#*` instead of one `buf << " "` (or `buf << "█"`) per cell, and rewrite the row/column loops as `while` so the ceiling/floor bands cost one short string copy per row instead of dozens of one-byte appends
24
+ - Skip the per-column DDA in `Renderer#render` when the player position, facing, map, and viewport are unchanged from the previous frame; the cached `@dists`, `@sides`, `@wtop`, `@wbot`, and `@wcol` arrays are reused so `cast_ray` and the wall-column projection only run when something actually moved
25
+ - Bulk-fill the ceiling-only and floor-only rows of `Renderer#build_view_pixels` with `Array#fill` so each fully ceiling or fully floor row is written as a single C-level call, with the column-major wall loop kept for the mixed band in the middle
26
+
27
+ ### Fixed
28
+
29
+ - Treat `Errno::EAGAIN` from `syswrite` the same as `IO::WaitWritable` in `TerminalOutput.write_all` so the renderer keeps draining its frame buffer instead of crashing when the terminal's PTY temporarily refuses additional bytes
30
+
9
31
  ## [0.1.6] - 2026-05-26
10
32
 
11
33
  ### Changed
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ class AsyncWriter
5
+ def initialize(io)
6
+ @io = io
7
+ @queue = Queue.new
8
+ @closed = false
9
+ @thread = Thread.new do
10
+ Thread.current.report_on_exception = false
11
+ while (data = @queue.pop)
12
+ begin
13
+ TerminalOutput.write_all(@io, data)
14
+ rescue IOError, Errno::EBADF, Errno::EPIPE
15
+ break
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def syswrite(data)
22
+ raise IOError, "writer closed" if @closed
23
+
24
+ @queue.clear if @queue.size >= 1
25
+ @queue.push(data)
26
+ data.bytesize
27
+ end
28
+
29
+ def winsize
30
+ @io.winsize
31
+ end
32
+
33
+ def raw(&block)
34
+ @io.raw(&block)
35
+ end
36
+
37
+ def close
38
+ return if @closed
39
+
40
+ @closed = true
41
+ @queue.push(nil)
42
+ @thread.join
43
+ end
44
+ end
45
+ end
@@ -8,6 +8,8 @@ module Termfront
8
8
  class AudioManager
9
9
  Player = Struct.new(:command, :supports_loop, keyword_init: true)
10
10
 
11
+ WHICH_CACHE = {}
12
+
11
13
  def initialize
12
14
  @manifest = load_manifest
13
15
  @bgm_player = detect_player(%w[ffplay afplay paplay aplay], prefer_loop: true)
@@ -160,12 +162,18 @@ module Termfront
160
162
  end
161
163
 
162
164
  def which(command)
165
+ return WHICH_CACHE[command] if WHICH_CACHE.key?(command)
166
+
167
+ resolved = nil
163
168
  ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
164
169
  candidate = File.join(dir, command)
165
- return candidate if File.executable?(candidate) && !File.directory?(candidate)
170
+ if File.executable?(candidate) && !File.directory?(candidate)
171
+ resolved = candidate
172
+ break
173
+ end
166
174
  end
167
175
 
168
- nil
176
+ WHICH_CACHE[command] = resolved
169
177
  end
170
178
 
171
179
  def asset_path(kind, name)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Color
5
+ module_function
6
+
7
+ def rgb_to_256(r, g, b)
8
+ if (r - g).abs < 8 && (g - b).abs < 8 && (r - b).abs < 8
9
+ avg = (r + g + b) / 3
10
+ return 16 if avg < 8
11
+ return 231 if avg > 247
12
+
13
+ 232 + (avg - 8) * 23 / 240
14
+ else
15
+ 16 + (r * 5 / 255) * 36 + (g * 5 / 255) * 6 + (b * 5 / 255)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -3,7 +3,8 @@
3
3
  module Termfront
4
4
  module Config
5
5
  FRAME_DT = 1.0 / 60.0
6
- RENDER_DT = 1.0 / 30.0
6
+ RENDER_DT = 1.0 / 60.0
7
+ MAX_DT = 1.0 / 20.0
7
8
  FOV = 66.0 * Math::PI / 180.0
8
9
  PLAYER_RADIUS = 0.2
9
10
  KEY_TIMEOUT = 5
@@ -22,8 +22,11 @@ module Termfront
22
22
  end
23
23
  end
24
24
 
25
+ SHOCK_COLOR = Color.rgb_to_256(60, 200, 220)
26
+ NORMAL_COLOR = Color.rgb_to_256(220, 200, 60)
27
+
25
28
  def sprite_color
26
- @type.to_s.start_with?("shock") ? "60;200;220" : "220;200;60"
29
+ @type.to_s.start_with?("shock") ? SHOCK_COLOR : NORMAL_COLOR
27
30
  end
28
31
 
29
32
  def radar_color
@@ -3,7 +3,7 @@
3
3
  module Termfront
4
4
  class Game
5
5
  def initialize
6
- @stdout = STDOUT
6
+ @stdout = AsyncWriter.new(STDOUT)
7
7
  @audio = AudioManager.new
8
8
  @renderer = Renderer.new(@stdout)
9
9
  @input = Input.new
@@ -36,6 +36,7 @@ module Termfront
36
36
  @crash = e
37
37
  ensure
38
38
  @audio.close
39
+ @stdout.close if @stdout.respond_to?(:close) && !@stdout.equal?(STDOUT)
39
40
  leave_alt_screen
40
41
  if @crash
41
42
  warn "#{@crash.class}: #{@crash.message}"
@@ -136,6 +137,7 @@ module Termfront
136
137
  loop do
137
138
  now = clock
138
139
  dt = now - last_time
140
+ dt = Config::MAX_DT if dt > Config::MAX_DT
139
141
  last_time = now
140
142
 
141
143
  keys = @input.process(stdin, player: @player)
@@ -185,6 +187,7 @@ module Termfront
185
187
  loop do
186
188
  now = clock
187
189
  dt = now - last_time
190
+ dt = Config::MAX_DT if dt > Config::MAX_DT
188
191
  last_time = now
189
192
 
190
193
  keys = @input.process(stdin, player: @player)
@@ -6,6 +6,14 @@ module Termfront
6
6
  TEAM_SIZES = [1, 2, 4].freeze
7
7
  ALLOWED_WEAPONS = %w[pistol ar].freeze
8
8
 
9
+ ALLY_PLAYER_FALLBACK = Color.rgb_to_256(70, 210, 255)
10
+ ENEMY_PLAYER_FALLBACK = Color.rgb_to_256(255, 110, 80)
11
+ ALLY_BAR_FILL = Color.rgb_to_256(0, 180, 255)
12
+ ENEMY_BAR_FILL = Color.rgb_to_256(255, 80, 80)
13
+ BAR_EMPTY = Color.rgb_to_256(80, 20, 20)
14
+ PROJ_SHOCK = Color.rgb_to_256(80, 220, 255)
15
+ PROJ_NORMAL = Color.rgb_to_256(255, 210, 80)
16
+
9
17
  def initialize(stdout)
10
18
  @stdout = stdout
11
19
  @conn = Connection.new
@@ -199,6 +207,7 @@ module Termfront
199
207
  loop do
200
208
  now = clock
201
209
  dt = now - last_time
210
+ dt = Config::MAX_DT if dt > Config::MAX_DT
202
211
  last_time = now
203
212
 
204
213
  keys = @input.process(stdin, player: @player)
@@ -548,33 +557,34 @@ module Termfront
548
557
  next unless top_in || bot_in
549
558
 
550
559
  if use_shape
560
+ sprite_fn = color_mode == :ally ? Sprite.method(:player) : Sprite.method(:player_enemy)
551
561
  ny0 = top_in ? (vp0 - draw_top).to_f / actual_h : nil
552
562
  ny1 = bot_in ? (vp1 - draw_top).to_f / actual_h : nil
553
- top_color = ny0 ? tint_player_color(Sprite.player(nx, ny0), color_mode) : nil
554
- bot_color = ny1 ? tint_player_color(Sprite.player(nx, ny1), color_mode) : nil
563
+ top_color = ny0 ? sprite_fn.call(nx, ny0) : nil
564
+ bot_color = ny1 ? sprite_fn.call(nx, ny1) : nil
555
565
  next unless top_color || bot_color
556
566
 
557
567
  buf << "\e[#{3 + r};#{c + 1}H"
558
568
  buf << if top_color && bot_color
559
569
  if top_color == bot_color
560
- "\e[38;2;#{top_color}m\xE2\x96\x88\e[0m"
570
+ "\e[38;5;#{top_color}m\xE2\x96\x88\e[0m"
561
571
  else
562
- "\e[38;2;#{top_color};48;2;#{bot_color}m\xE2\x96\x80\e[0m"
572
+ "\e[38;5;#{top_color};48;5;#{bot_color}m\xE2\x96\x80\e[0m"
563
573
  end
564
574
  elsif top_color
565
- "\e[38;2;#{top_color}m\xE2\x96\x80\e[0m"
575
+ "\e[38;5;#{top_color}m\xE2\x96\x80\e[0m"
566
576
  else
567
- "\e[38;2;#{bot_color}m\xE2\x96\x84\e[0m"
577
+ "\e[38;5;#{bot_color}m\xE2\x96\x84\e[0m"
568
578
  end
569
579
  else
570
- fc = color_mode == :ally ? "70;210;255" : "255;110;80"
580
+ fc = color_mode == :ally ? ALLY_PLAYER_FALLBACK : ENEMY_PLAYER_FALLBACK
571
581
  buf << "\e[#{3 + r};#{c + 1}H"
572
582
  buf << if top_in && bot_in
573
- "\e[38;2;#{fc}m\xE2\x96\x88\e[0m"
583
+ "\e[38;5;#{fc}m\xE2\x96\x88\e[0m"
574
584
  elsif top_in
575
- "\e[38;2;#{fc}m\xE2\x96\x80\e[0m"
585
+ "\e[38;5;#{fc}m\xE2\x96\x80\e[0m"
576
586
  else
577
- "\e[38;2;#{fc}m\xE2\x96\x84\e[0m"
587
+ "\e[38;5;#{fc}m\xE2\x96\x84\e[0m"
578
588
  end
579
589
  end
580
590
  end
@@ -590,15 +600,15 @@ module Termfront
590
600
  max_total = Config::SHIELD_MAX + Config::HEALTH_MAX
591
601
  hp_pct = total.to_f / max_total
592
602
  filled = (hp_pct * (bar_ex - bar_sx + 1)).ceil
593
- fill_color = color_mode == :ally ? "0;180;255" : "255;80;80"
603
+ fill_color = color_mode == :ally ? ALLY_BAR_FILL : ENEMY_BAR_FILL
594
604
 
595
605
  bar_sx.upto(bar_ex) do |c|
596
606
  next if c < 0 || c >= view_w
597
607
  next if dists[c] < tz
598
608
 
599
609
  ci = c - bar_sx
600
- color = ci < filled ? fill_color : "80;20;20"
601
- buf << "\e[#{3 + bar_row};#{c + 1}H\e[38;2;#{color}m\xE2\x96\x88\e[0m"
610
+ color = ci < filled ? fill_color : BAR_EMPTY
611
+ buf << "\e[#{3 + bar_row};#{c + 1}H\e[38;5;#{color}m\xE2\x96\x88\e[0m"
602
612
  end
603
613
  end
604
614
 
@@ -649,28 +659,17 @@ module Termfront
649
659
  proj_color = projectile[:color]
650
660
  buf << "\e[#{3 + r};#{c + 1}H"
651
661
  buf << if top_in && bot_in
652
- "\e[38;2;#{proj_color}m\xE2\x96\x88\e[0m"
662
+ "\e[38;5;#{proj_color}m\xE2\x96\x88\e[0m"
653
663
  elsif top_in
654
- "\e[38;2;#{proj_color}m\xE2\x96\x80\e[0m"
664
+ "\e[38;5;#{proj_color}m\xE2\x96\x80\e[0m"
655
665
  else
656
- "\e[38;2;#{proj_color}m\xE2\x96\x84\e[0m"
666
+ "\e[38;5;#{proj_color}m\xE2\x96\x84\e[0m"
657
667
  end
658
668
  end
659
669
  end
660
670
  end
661
671
  end
662
672
 
663
- def tint_player_color(color, mode)
664
- return nil unless color
665
- return color if mode == :ally
666
-
667
- r, g, b = color.split(";").map(&:to_i)
668
- nr = [[r + 70, 255].min, 0].max
669
- ng = [[g - 50, 0].max, 0].max
670
- nb = [[b - 90, 0].max, 0].max
671
- "#{nr};#{ng};#{nb}"
672
- end
673
-
674
673
  def render_pvp_hud(buf, cols)
675
674
  bar_w = [cols - 30, 10].max
676
675
  pct = @player.shield / Config::SHIELD_MAX.to_f
@@ -806,13 +805,15 @@ module Termfront
806
805
  def render_damage_flash(buf, view_h, view_w)
807
806
  return unless @player.damage_flash > 0
808
807
 
809
- intensity = @player.damage_flash * 60
808
+ intensity = (@player.damage_flash * 60).to_i
809
+ intensity = 255 if intensity > 255
810
+ color = Renderer::DAMAGE_FLASH_RAMP[intensity]
810
811
  flash_w = 2
811
812
 
812
813
  view_h.times do |r|
813
- buf << "\e[#{3 + r};1H\e[48;2;#{intensity};0;0m#{" " * flash_w}\e[0m"
814
+ buf << "\e[#{3 + r};1H\e[48;5;#{color}m#{" " * flash_w}\e[0m"
814
815
  rc = [view_w - flash_w + 1, 1].max
815
- buf << "\e[#{3 + r};#{rc}H\e[48;2;#{intensity};0;0m#{" " * flash_w}\e[0m"
816
+ buf << "\e[#{3 + r};#{rc}H\e[48;5;#{color}m#{" " * flash_w}\e[0m"
816
817
  end
817
818
  end
818
819
 
@@ -849,7 +850,7 @@ module Termfront
849
850
 
850
851
  def spawn_remote_projectile_effect(msg)
851
852
  shock = msg[:w].to_s.start_with?("shock")
852
- color = shock ? "80;220;255" : "255;210;80"
853
+ color = shock ? PROJ_SHOCK : PROJ_NORMAL
853
854
  speed = shock ? 18.0 : 14.0
854
855
  angle = msg[:a]
855
856
  @projectiles << {
@@ -57,7 +57,7 @@ module Termfront
57
57
  lines[rows / 2 - 2] = TerminalOutput.fit_ansi("#{" " * (mc - 1)}\e[1;93m#{msg}\e[0m", cols)
58
58
  detail = "#{queued_mission_name} | #{queued_difficulty_name}"
59
59
  dc = [(cols - detail.size) / 2 + 1, 1].max
60
- lines[rows / 2] = TerminalOutput.fit_ansi("#{" " * (dc - 1)}\e[38;2;170;170;190m#{detail}\e[0m", cols)
60
+ lines[rows / 2] = TerminalOutput.fit_ansi("#{" " * (dc - 1)}\e[38;5;#{Color.rgb_to_256(170, 170, 190)}m#{detail}\e[0m", cols)
61
61
  hint = "(ESC to cancel)"
62
62
  hc = [(cols - hint.size) / 2 + 1, 1].max
63
63
  lines[rows / 2 + 2] = TerminalOutput.fit_ansi("#{" " * (hc - 1)}\e[90m#{hint}\e[0m", cols)
@@ -121,6 +121,7 @@ module Termfront
121
121
  loop do
122
122
  now = clock
123
123
  dt = now - last_time
124
+ dt = Config::MAX_DT if dt > Config::MAX_DT
124
125
  last_time = now
125
126
 
126
127
  keys = @input.process(stdin, player: @player)
@@ -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
@@ -23,8 +34,6 @@ module Termfront
23
34
  @radar_drop_cells = {}
24
35
  @radar_terminal_cells = {}
25
36
  @radar_ally_cells = {}
26
- @fg_truecolor_cache = {}
27
- @bg_truecolor_cache = {}
28
37
  @enemy_sprites = []
29
38
  @proj_sprites = []
30
39
  @drop_sprites = []
@@ -54,23 +63,27 @@ module Termfront
54
63
 
55
64
  prepare_frame_buffers(view_w, virt_h)
56
65
 
57
- dx = Math.cos(player.angle)
58
- dy = Math.sin(player.angle)
59
- plane_x = -dy * Math.tan(Config::FOV / 2.0)
60
- 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)
61
72
 
62
- view_w.times do |c|
63
- cam = 2.0 * c / view_w - 1.0
64
- @dists[c], @sides[c] = cast_ray(map, player.x, player.y, dx + plane_x * cam, dy + plane_y * cam)
65
- 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
66
77
 
67
- vmid = virt_h / 2.0
68
- view_w.times do |c|
69
- d = @dists[c]
70
- lh = d > 0.01 ? (virt_h / d).to_i : virt_h
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])
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
74
87
  end
75
88
  build_view_pixels(virt_h, view_w, @wtop, @wbot, @wcol)
76
89
  overlay_enemies_3d(@pixels, view_h, view_w, @dists, player, enemies, projectiles, drops)
@@ -289,22 +302,41 @@ module Termfront
289
302
  floor_c = Config::FLOOR_C
290
303
  pixels = @pixels
291
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
+
292
324
  c = 0
293
325
  while c < view_w
294
326
  wt = wtop[c]
295
327
  wb = wbot[c]
296
328
  wc = wcol[c]
297
329
 
298
- vr = 0
299
- while vr < wt && vr < virt_h
330
+ vr = upper_done
331
+ while vr < wt && vr < middle_end
300
332
  pixels[vr][c] = ceil_c
301
333
  vr += 1
302
334
  end
303
- while vr < wb && vr < virt_h
335
+ while vr < wb && vr < middle_end
304
336
  pixels[vr][c] = wc
305
337
  vr += 1
306
338
  end
307
- while vr < virt_h
339
+ while vr < middle_end
308
340
  pixels[vr][c] = floor_c
309
341
  vr += 1
310
342
  end
@@ -316,49 +348,60 @@ module Termfront
316
348
  def render_view(buf, view_h, view_w, pixels)
317
349
  fg_256 = FG_256
318
350
  bg_256 = BG_256
319
- fg_cache = @fg_truecolor_cache
320
- bg_cache = @bg_truecolor_cache
321
351
 
322
- view_h.times do |r|
352
+ r = 0
353
+ while r < view_h
323
354
  vp0 = r * 2
324
- vp1 = r * 2 + 1
325
- pfg = nil
326
- pbg = nil
355
+ vp1 = vp0 + 1
327
356
  top_row = pixels[vp0]
328
357
  bot_row = pixels[vp1]
329
358
 
330
- 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
331
381
  tc = top_row[c]
332
382
  bc = bot_row[c]
333
383
 
334
384
  if tc == bc
335
- if tc.is_a?(Integer)
336
- if tc != pbg
337
- buf << bg_256[tc]
338
- pbg = tc
339
- pfg = nil
340
- end
341
- buf << " "
342
- else
343
- if tc != pfg || pbg
344
- buf << (fg_cache[tc] ||= "\e[38;2;#{tc}m".freeze)
345
- pfg = tc
346
- pbg = nil
347
- end
348
- 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
349
388
  end
350
- else
351
- if tc != pfg || bc != pbg
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
355
- pfg = tc
356
- pbg = bc
389
+ n = run_end - c
390
+
391
+ if tc != pbg
392
+ buf << bg_256[tc]
393
+ pbg = tc
357
394
  end
358
- 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
359
401
  end
360
402
  end
361
403
  buf << "\e[0m\r\n"
404
+ r += 1
362
405
  end
363
406
  end
364
407
 
@@ -560,8 +603,9 @@ module Termfront
560
603
  next if actual_h < 1 || actual_w < 1
561
604
 
562
605
  sprite_id = e.sprite_id
563
- fallback_color = sprite_id == :executor ? "100;60;200" : "220;140;30"
564
- use_shape = actual_h >= 6
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
565
609
  r_top = (draw_top + 1) >> 1
566
610
  r_bot = draw_bot >> 1
567
611
  actual_h_f = actual_h.to_f
@@ -583,8 +627,8 @@ module Termfront
583
627
  if use_shape
584
628
  ny0 = top_in ? (vp0 - draw_top) / actual_h_f : nil
585
629
  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
630
+ top_color = ny0 ? sprite_fn.call(nx, ny0) : nil
631
+ bot_color = ny1 ? sprite_fn.call(nx, ny1) : nil
588
632
  if top_color || bot_color
589
633
  pixels[vp0][c] = top_color if top_color
590
634
  pixels[vp1][c] = bot_color if bot_color
@@ -618,7 +662,7 @@ module Termfront
618
662
  while c <= bar_ex
619
663
  if c >= 0 && c < view_w && dists[c] >= tz
620
664
  ci = c - bar_sx
621
- color = ci < filled ? "0;200;0" : "200;0;0"
665
+ color = ci < filled ? ENEMY_BAR_FILL : ENEMY_BAR_EMPTY
622
666
  pixels[bar_vp0][c] = color
623
667
  pixels[bar_vp1][c] = color
624
668
  end
@@ -693,7 +737,7 @@ module Termfront
693
737
  start_x = 0 if start_x < 0
694
738
  end_x = sx + pw / 2
695
739
  end_x = view_w_last if end_x > view_w_last
696
- proj_color = p.type == :executor ? "94;94;255" : "255;210;80"
740
+ proj_color = p.type == :executor ? PROJ_EXECUTOR : PROJ_DEFAULT
697
741
  r_top = (draw_top + 1) >> 1
698
742
  r_bot = draw_bot >> 1
699
743
  r_bot = r_top + 1 if r_bot < r_top + 1
@@ -781,8 +825,8 @@ module Termfront
781
825
  pixels[vp0][c] = top_color if top_color
782
826
  pixels[vp1][c] = bot_color if bot_color
783
827
  else
784
- pixels[vp0][c] = "70;210;255" if top_in
785
- 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
786
830
  end
787
831
  end
788
832
  end
@@ -802,7 +846,7 @@ module Termfront
802
846
  next if dists[c] < tz
803
847
 
804
848
  ci = c - bar_sx
805
- color = ci < filled ? "0;180;255" : "80;20;20"
849
+ color = ci < filled ? ALLY_BAR_FILL : ALLY_BAR_EMPTY
806
850
  pixels[bar_row * 2][c] = color
807
851
  pixels[bar_row * 2 + 1][c] = color
808
852
  end
@@ -812,9 +856,10 @@ module Termfront
812
856
  def overlay_damage_flash(pixels, view_h, view_w, player)
813
857
  return unless player.damage_flash > 0
814
858
 
815
- intensity = player.damage_flash * 60
859
+ intensity = (player.damage_flash * 60).to_i
860
+ intensity = 255 if intensity > 255
816
861
  flash_w = 2
817
- color = "#{intensity};0;0"
862
+ color = DAMAGE_FLASH_RAMP[intensity]
818
863
 
819
864
  view_h.times do |r|
820
865
  vp0 = r * 2
@@ -843,24 +888,5 @@ module Termfront
843
888
  buf << "\e[#{cr};#{fs}H\e[93m#{"*" * (fe - fs + 1)}\e[0m"
844
889
  end
845
890
 
846
- def bg_only?(color)
847
- color.is_a?(Integer)
848
- end
849
-
850
- def ansi_fg(color)
851
- if color.is_a?(Integer)
852
- FG_256[color]
853
- else
854
- @fg_truecolor_cache[color] ||= "\e[38;2;#{color}m".freeze
855
- end
856
- end
857
-
858
- def ansi_bg(color)
859
- if color.is_a?(Integer)
860
- BG_256[color]
861
- else
862
- @bg_truecolor_cache[color] ||= "\e[48;2;#{color}m".freeze
863
- end
864
- end
865
891
  end
866
892
  end
@@ -4,46 +4,82 @@ module Termfront
4
4
  module Sprite
5
5
  module_function
6
6
 
7
+ EXECUTOR_EYE = Color.rgb_to_256(180, 120, 255)
8
+ EXECUTOR_HEAD = Color.rgb_to_256(130, 80, 220)
9
+ EXECUTOR_NECK = Color.rgb_to_256(90, 50, 180)
10
+ EXECUTOR_BODY = Color.rgb_to_256(80, 40, 160)
11
+
12
+ CRAWLER_EYE = Color.rgb_to_256(255, 240, 100)
13
+ CRAWLER_BODY = Color.rgb_to_256(220, 140, 30)
14
+ CRAWLER_LEG = Color.rgb_to_256(160, 100, 20)
15
+
16
+ PLAYER_EYE = Color.rgb_to_256(140, 220, 255)
17
+ PLAYER_HEAD = Color.rgb_to_256(40, 130, 180)
18
+ PLAYER_NECK = Color.rgb_to_256(30, 100, 160)
19
+ PLAYER_BODY = Color.rgb_to_256(25, 80, 140)
20
+
21
+ PLAYER_ENEMY_EYE = Color.rgb_to_256(210, 170, 165)
22
+ PLAYER_ENEMY_HEAD = Color.rgb_to_256(110, 80, 90)
23
+ PLAYER_ENEMY_NECK = Color.rgb_to_256(100, 50, 70)
24
+ PLAYER_ENEMY_BODY = Color.rgb_to_256(95, 30, 50)
25
+
26
+ DUMMY_HEAD = Color.rgb_to_256(235, 80, 80)
27
+ DUMMY_TORSO = Color.rgb_to_256(210, 210, 210)
28
+ DUMMY_LOWER = Color.rgb_to_256(200, 200, 200)
29
+ DUMMY_LEG = Color.rgb_to_256(180, 180, 180)
30
+
7
31
  def executor(nx, ny)
8
- return "180;120;255" if ((nx - 0.43) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
9
- return "180;120;255" if ((nx - 0.57) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
10
- return "130;80;220" if ((nx - 0.5) / 0.18)**2 + ((ny - 0.12) / 0.12)**2 <= 1.0
11
- return "90;50;180" if ((nx - 0.5) / 0.38)**2 + ((ny - 0.30) / 0.08)**2 <= 1.0
12
- return "80;40;160" if ((nx - 0.5) / 0.25)**2 + ((ny - 0.50) / 0.22)**2 <= 1.0
13
- return "80;40;160" if ((nx - 0.38) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
14
- return "80;40;160" if ((nx - 0.62) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
32
+ return EXECUTOR_EYE if ((nx - 0.43) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
33
+ return EXECUTOR_EYE if ((nx - 0.57) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
34
+ return EXECUTOR_HEAD if ((nx - 0.5) / 0.18)**2 + ((ny - 0.12) / 0.12)**2 <= 1.0
35
+ return EXECUTOR_NECK if ((nx - 0.5) / 0.38)**2 + ((ny - 0.30) / 0.08)**2 <= 1.0
36
+ return EXECUTOR_BODY if ((nx - 0.5) / 0.25)**2 + ((ny - 0.50) / 0.22)**2 <= 1.0
37
+ return EXECUTOR_BODY if ((nx - 0.38) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
38
+ return EXECUTOR_BODY if ((nx - 0.62) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
15
39
 
16
40
  nil
17
41
  end
18
42
 
19
43
  def crawler(nx, ny)
20
- return "255;240;100" if ((nx - 0.36) / 0.063)**2 + ((ny - 0.28) / 0.063)**2 <= 1.0
21
- return "255;240;100" if ((nx - 0.64) / 0.063)**2 + ((ny - 0.28) / 0.063)**2 <= 1.0
22
- return "220;140;30" if ((nx - 0.5) / 0.40)**2 + ((ny - 0.40) / 0.40)**2 <= 1.0
23
- return "160;100;20" if ((nx - 0.35) / 0.12)**2 + ((ny - 0.90) / 0.10)**2 <= 1.0
24
- return "160;100;20" if ((nx - 0.65) / 0.12)**2 + ((ny - 0.90) / 0.10)**2 <= 1.0
44
+ return CRAWLER_EYE if ((nx - 0.36) / 0.063)**2 + ((ny - 0.28) / 0.063)**2 <= 1.0
45
+ return CRAWLER_EYE if ((nx - 0.64) / 0.063)**2 + ((ny - 0.28) / 0.063)**2 <= 1.0
46
+ return CRAWLER_BODY if ((nx - 0.5) / 0.40)**2 + ((ny - 0.40) / 0.40)**2 <= 1.0
47
+ return CRAWLER_LEG if ((nx - 0.35) / 0.12)**2 + ((ny - 0.90) / 0.10)**2 <= 1.0
48
+ return CRAWLER_LEG if ((nx - 0.65) / 0.12)**2 + ((ny - 0.90) / 0.10)**2 <= 1.0
25
49
 
26
50
  nil
27
51
  end
28
52
 
29
53
  def player(nx, ny)
30
- return "140;220;255" if ((nx - 0.43) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
31
- return "140;220;255" if ((nx - 0.57) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
32
- return "40;130;180" if ((nx - 0.5) / 0.18)**2 + ((ny - 0.12) / 0.12)**2 <= 1.0
33
- return "30;100;160" if ((nx - 0.5) / 0.38)**2 + ((ny - 0.30) / 0.08)**2 <= 1.0
34
- return "25;80;140" if ((nx - 0.5) / 0.25)**2 + ((ny - 0.50) / 0.22)**2 <= 1.0
35
- return "25;80;140" if ((nx - 0.38) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
36
- return "25;80;140" if ((nx - 0.62) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
54
+ return PLAYER_EYE if ((nx - 0.43) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
55
+ return PLAYER_EYE if ((nx - 0.57) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
56
+ return PLAYER_HEAD if ((nx - 0.5) / 0.18)**2 + ((ny - 0.12) / 0.12)**2 <= 1.0
57
+ return PLAYER_NECK if ((nx - 0.5) / 0.38)**2 + ((ny - 0.30) / 0.08)**2 <= 1.0
58
+ return PLAYER_BODY if ((nx - 0.5) / 0.25)**2 + ((ny - 0.50) / 0.22)**2 <= 1.0
59
+ return PLAYER_BODY if ((nx - 0.38) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
60
+ return PLAYER_BODY if ((nx - 0.62) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
61
+
62
+ nil
63
+ end
64
+
65
+ def player_enemy(nx, ny)
66
+ return PLAYER_ENEMY_EYE if ((nx - 0.43) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
67
+ return PLAYER_ENEMY_EYE if ((nx - 0.57) / 0.045)**2 + ((ny - 0.11) / 0.045)**2 <= 1.0
68
+ return PLAYER_ENEMY_HEAD if ((nx - 0.5) / 0.18)**2 + ((ny - 0.12) / 0.12)**2 <= 1.0
69
+ return PLAYER_ENEMY_NECK if ((nx - 0.5) / 0.38)**2 + ((ny - 0.30) / 0.08)**2 <= 1.0
70
+ return PLAYER_ENEMY_BODY if ((nx - 0.5) / 0.25)**2 + ((ny - 0.50) / 0.22)**2 <= 1.0
71
+ return PLAYER_ENEMY_BODY if ((nx - 0.38) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
72
+ return PLAYER_ENEMY_BODY if ((nx - 0.62) / 0.10)**2 + ((ny - 0.85) / 0.15)**2 <= 1.0
37
73
 
38
74
  nil
39
75
  end
40
76
 
41
77
  def training_dummy(nx, ny)
42
- return "235;80;80" if ((nx - 0.5) / 0.18)**2 + ((ny - 0.18) / 0.14)**2 <= 1.0
43
- return "210;210;210" if ((nx - 0.5) / 0.08)**2 + ((ny - 0.42) / 0.14)**2 <= 1.0
44
- return "200;200;200" if ((nx - 0.5) / 0.22)**2 + ((ny - 0.66) / 0.12)**2 <= 1.0
45
- return "180;180;180" if ((nx - 0.38) / 0.08)**2 + ((ny - 0.90) / 0.12)**2 <= 1.0
46
- return "180;180;180" if ((nx - 0.62) / 0.08)**2 + ((ny - 0.90) / 0.12)**2 <= 1.0
78
+ return DUMMY_HEAD if ((nx - 0.5) / 0.18)**2 + ((ny - 0.18) / 0.14)**2 <= 1.0
79
+ return DUMMY_TORSO if ((nx - 0.5) / 0.08)**2 + ((ny - 0.42) / 0.14)**2 <= 1.0
80
+ return DUMMY_LOWER if ((nx - 0.5) / 0.22)**2 + ((ny - 0.66) / 0.12)**2 <= 1.0
81
+ return DUMMY_LEG if ((nx - 0.38) / 0.08)**2 + ((ny - 0.90) / 0.12)**2 <= 1.0
82
+ return DUMMY_LEG if ((nx - 0.62) / 0.08)**2 + ((ny - 0.90) / 0.12)**2 <= 1.0
47
83
 
48
84
  nil
49
85
  end
@@ -62,7 +62,7 @@ module Termfront
62
62
  begin
63
63
  written = io.syswrite(data.byteslice(total, bytes - total))
64
64
  total += written
65
- rescue IO::WaitWritable
65
+ rescue IO::WaitWritable, Errno::EAGAIN
66
66
  IO.select(nil, [io])
67
67
  end
68
68
  end
@@ -11,6 +11,11 @@ module Termfront
11
11
  SUB_TEXT = "Terminal FPS"
12
12
  MENU_ITEMS = ["[P] PvP", "[F] Wavesfight", "[C] Campaign", "[S] Training", "[Q] Quit"].freeze
13
13
 
14
+ TITLE_CEIL_C = Color.rgb_to_256(0, 0, 95)
15
+ TITLE_FLOOR_C = Color.rgb_to_256(28, 28, 28)
16
+ TITLE_EXECUTOR_FALLBACK = Color.rgb_to_256(100, 60, 200)
17
+ TITLE_CRAWLER_FALLBACK = Color.rgb_to_256(220, 140, 30)
18
+
14
19
  def initialize(stdout)
15
20
  @stdout = stdout
16
21
  @title_spin = 0.0
@@ -143,8 +148,8 @@ module Termfront
143
148
  dists = Array.new(tw, 100.0)
144
149
  horizon = (virt_h / 2 + bob).to_i
145
150
 
146
- ceil_c = "0;0;95"
147
- floor_c = "28;28;28"
151
+ ceil_c = TITLE_CEIL_C
152
+ floor_c = TITLE_FLOOR_C
148
153
 
149
154
  tw.times do |col|
150
155
  ray_a = cam_a - half_fov + fov * col.to_f / tw
@@ -201,9 +206,9 @@ module Termfront
201
206
  flash = @demo_fire / 4.0 * (1.0 - dist / 4.0)
202
207
  rr = (grey + flash * 160).to_i.clamp(0, 255)
203
208
  gg = (grey + flash * 60).to_i.clamp(0, 255)
204
- wall_c = "#{rr};#{gg};#{grey}"
209
+ wall_c = Color.rgb_to_256(rr, gg, grey)
205
210
  else
206
- wall_c = "#{grey};#{grey};#{grey}"
211
+ wall_c = Color.rgb_to_256(grey, grey, grey)
207
212
  end
208
213
 
209
214
  virt_h.times do |vr|
@@ -271,36 +276,146 @@ module Termfront
271
276
  sc = Sprite.for(type, nx, ny)
272
277
  next unless sc
273
278
  else
274
- sc = type == :executor ? "100;60;200" : "220;140;30"
279
+ sc = type == :executor ? TITLE_EXECUTOR_FALLBACK : TITLE_CRAWLER_FALLBACK
275
280
  end
276
281
  color[vr * tw + c] = sc
277
282
  end
278
283
  end
279
284
  end
280
285
 
281
- # Half-block rendering
282
- th.times do |r|
283
- vp0 = r * 2
284
- vp1 = r * 2 + 1
286
+ # Half-block rendering - build each line directly to skip fit_ansi
287
+ fg_256 = Renderer::FG_256
288
+ bg_256 = Renderer::BG_256
289
+ cap_w = tw < cols ? tw : cols
290
+
291
+ r = 0
292
+ while r < th
293
+ vp0_offset = (r * 2) * tw
294
+ vp1_offset = vp0_offset + tw
295
+
296
+ first_tc = color[vp0_offset]
297
+ first_bc = color[vp1_offset]
298
+
299
+ if first_tc && first_tc == first_bc
300
+ uniform = true
301
+ cu = 1
302
+ while cu < cap_w
303
+ if color[vp0_offset + cu] != first_tc || color[vp1_offset + cu] != first_bc
304
+ uniform = false
305
+ break
306
+ end
307
+ cu += 1
308
+ end
309
+ if uniform
310
+ lines[r] = +bg_256[first_tc] << "\e[K\e[0m"
311
+ r += 1
312
+ next
313
+ end
314
+ elsif first_tc.nil? && first_bc.nil?
315
+ all_nil = true
316
+ cu = 1
317
+ while cu < cap_w
318
+ if !color[vp0_offset + cu].nil? || !color[vp1_offset + cu].nil?
319
+ all_nil = false
320
+ break
321
+ end
322
+ cu += 1
323
+ end
324
+ if all_nil
325
+ lines[r] = " " * cols
326
+ r += 1
327
+ next
328
+ end
329
+ end
330
+
285
331
  line = +""
286
- tw.times do |c|
287
- tc = color[vp0 * tw + c]
288
- bc = color[vp1 * tw + c]
289
- line << if tc && bc
290
- if tc == bc
291
- "\e[38;2;#{tc}m\xE2\x96\x88\e[0m"
292
- else
293
- "\e[38;2;#{tc};48;2;#{bc}m\xE2\x96\x80\e[0m"
294
- end
295
- elsif tc
296
- "\e[38;2;#{tc}m\xE2\x96\x80\e[0m"
297
- elsif bc
298
- "\e[38;2;#{bc}m\xE2\x96\x84\e[0m"
299
- else
300
- " "
301
- end
332
+ pfg = nil
333
+ pbg = nil
334
+ visible = 0
335
+
336
+ c = 0
337
+ while c < tw && visible < cap_w
338
+ tc = color[vp0_offset + c]
339
+ bc = color[vp1_offset + c]
340
+
341
+ if tc == bc
342
+ run_end = c + 1
343
+ while run_end < tw &&
344
+ color[vp0_offset + run_end] == tc &&
345
+ color[vp1_offset + run_end] == bc
346
+ run_end += 1
347
+ end
348
+ n = run_end - c
349
+ n = cap_w - visible if visible + n > cap_w
350
+
351
+ if tc.nil?
352
+ if pfg || pbg
353
+ line << "\e[0m"
354
+ pfg = nil
355
+ pbg = nil
356
+ end
357
+ line << (n == 1 ? " " : " " * n)
358
+ else
359
+ if pfg != tc || pbg
360
+ line << fg_256[tc]
361
+ line << "\e[49m" if pbg
362
+ pfg = tc
363
+ pbg = nil
364
+ end
365
+ line << (n == 1 ? "\xE2\x96\x88" : "\xE2\x96\x88" * n)
366
+ end
367
+ visible += n
368
+ c += n
369
+ else
370
+ if tc && bc
371
+ if pfg != tc || pbg != bc
372
+ line << fg_256[tc]
373
+ line << bg_256[bc]
374
+ pfg = tc
375
+ pbg = bc
376
+ end
377
+ line << "\xE2\x96\x80"
378
+ elsif tc
379
+ if pfg != tc || pbg
380
+ line << fg_256[tc]
381
+ line << "\e[49m" if pbg
382
+ pfg = tc
383
+ pbg = nil
384
+ end
385
+ line << "\xE2\x96\x80"
386
+ elsif bc
387
+ if pfg != bc || pbg
388
+ line << fg_256[bc]
389
+ line << "\e[49m" if pbg
390
+ pfg = bc
391
+ pbg = nil
392
+ end
393
+ line << "\xE2\x96\x84"
394
+ else
395
+ if pfg || pbg
396
+ line << "\e[0m"
397
+ pfg = nil
398
+ pbg = nil
399
+ end
400
+ line << " "
401
+ end
402
+ visible += 1
403
+ c += 1
404
+ end
302
405
  end
303
- lines[r] = TerminalOutput.fit_ansi(line, cols)
406
+
407
+ if visible < cols
408
+ if pfg || pbg
409
+ line << "\e[0m"
410
+ pfg = nil
411
+ pbg = nil
412
+ end
413
+ line << (" " * (cols - visible))
414
+ end
415
+ line << "\e[0m" if pfg || pbg
416
+
417
+ lines[r] = line
418
+ r += 1
304
419
  end
305
420
 
306
421
  # Title text + menu items (memoized per cols)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Termfront
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/termfront.rb CHANGED
@@ -4,6 +4,7 @@ require "io/console"
4
4
 
5
5
  require_relative "termfront/version"
6
6
  require_relative "termfront/config"
7
+ require_relative "termfront/color"
7
8
  require_relative "termfront/map"
8
9
  require_relative "termfront/weapon/base"
9
10
  require_relative "termfront/weapon/pistol"
@@ -32,6 +33,7 @@ require_relative "termfront/remote_enemy"
32
33
  require_relative "termfront/sprite"
33
34
  require_relative "termfront/input"
34
35
  require_relative "termfront/terminal_output"
36
+ require_relative "termfront/async_writer"
35
37
  require_relative "termfront/renderer"
36
38
  require_relative "termfront/demo_player"
37
39
  require_relative "termfront/scene_player"
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.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - S-H-GAMELINKS
@@ -51,7 +51,9 @@ files:
51
51
  - exe/termfront
52
52
  - exe/termfront-server
53
53
  - lib/termfront.rb
54
+ - lib/termfront/async_writer.rb
54
55
  - lib/termfront/audio_manager.rb
56
+ - lib/termfront/color.rb
55
57
  - lib/termfront/config.rb
56
58
  - lib/termfront/demo_player.rb
57
59
  - lib/termfront/drop_item/base.rb