doom 0.4.0 → 0.5.0

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.
@@ -53,7 +53,7 @@ module Doom
53
53
  class Renderer
54
54
  attr_reader :framebuffer
55
55
 
56
- def initialize(wad, map, textures, palette, colormap, flats, sprites = nil)
56
+ def initialize(wad, map, textures, palette, colormap, flats, sprites = nil, animations = nil)
57
57
  @wad = wad
58
58
  @map = map
59
59
  @textures = textures
@@ -61,6 +61,9 @@ module Doom
61
61
  @colormap = colormap
62
62
  @flats = flats.to_h { |f| [f.name, f] }
63
63
  @sprites = sprites
64
+ @animations = animations
65
+ @hidden_things = nil
66
+ @combat = nil
64
67
 
65
68
  @framebuffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0)
66
69
 
@@ -100,7 +103,96 @@ module Doom
100
103
  @y_slope_floor = Array.new(HALF_HEIGHT + 1, 0.0)
101
104
  end
102
105
 
103
- attr_reader :player_x, :player_y, :player_z, :sin_angle, :cos_angle
106
+ attr_reader :player_x, :player_y, :player_z, :sin_angle, :cos_angle, :framebuffer
107
+ attr_writer :hidden_things, :combat
108
+
109
+ # Diagnostic: returns info about all sprites and why they are/aren't visible
110
+ def sprite_diagnostics
111
+ return [] unless @sprites
112
+
113
+ results = []
114
+ @map.things.each do |thing|
115
+ prefix = @sprites.prefix_for(thing.type)
116
+ next unless prefix
117
+
118
+ info = { type: thing.type, x: thing.x, y: thing.y, prefix: prefix }
119
+
120
+ view_x, view_y = transform_point(thing.x, thing.y)
121
+ info[:view_x] = view_x.round(1)
122
+ info[:view_y] = view_y.round(1)
123
+
124
+ if view_y <= 0
125
+ info[:status] = "behind_player"
126
+ results << info
127
+ next
128
+ end
129
+
130
+ dist = view_y
131
+ screen_x = HALF_WIDTH + (view_x * @projection / view_y)
132
+ info[:screen_x] = screen_x.round(1)
133
+ info[:dist] = dist.round(1)
134
+
135
+ dx = thing.x - @player_x
136
+ dy = thing.y - @player_y
137
+ angle_to_thing = Math.atan2(dy, dx)
138
+ sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
139
+ unless sprite
140
+ info[:status] = "no_sprite_frame"
141
+ results << info
142
+ next
143
+ end
144
+
145
+ sprite_scale = @projection / dist
146
+ sprite_half_width = (sprite.width * @projection / dist / 2).to_i
147
+ info[:sprite_scale] = sprite_scale.round(3)
148
+
149
+ if screen_x + sprite_half_width < 0
150
+ info[:status] = "off_screen_left"
151
+ elsif screen_x - sprite_half_width >= SCREEN_WIDTH
152
+ info[:status] = "off_screen_right"
153
+ else
154
+ # Check drawseg clipping
155
+ sprite_left = (screen_x - sprite.left_offset * sprite_scale).to_i
156
+ sprite_right = sprite_left + (sprite.width * sprite_scale).to_i - 1
157
+ x1 = [sprite_left, 0].max
158
+ x2 = [sprite_right, SCREEN_WIDTH - 1].min
159
+
160
+ sector = @map.sector_at(thing.x, thing.y)
161
+ thing_floor = sector ? sector.floor_height : 0
162
+ sprite_gz = thing_floor
163
+ sprite_gzt = thing_floor + sprite.top_offset
164
+
165
+ clipping_segs = []
166
+ @drawsegs.reverse_each do |ds|
167
+ next if ds.x1 > x2 || ds.x2 < x1
168
+ next if ds.silhouette == SIL_NONE
169
+
170
+ lowscale = [ds.scale1, ds.scale2].min
171
+ highscale = [ds.scale1, ds.scale2].max
172
+
173
+ if highscale < sprite_scale
174
+ next
175
+ elsif lowscale < sprite_scale
176
+ next unless point_on_seg_side(thing.x, thing.y, ds.curline)
177
+ end
178
+
179
+ clipping_segs << {
180
+ x1: ds.x1, x2: ds.x2,
181
+ scale: "#{ds.scale1.round(3)}..#{ds.scale2.round(3)}",
182
+ sil: ds.silhouette
183
+ }
184
+ end
185
+
186
+ info[:screen_range] = "#{x1}..#{x2}"
187
+ info[:clipping_segs] = clipping_segs.size
188
+ info[:clipping_detail] = clipping_segs
189
+ info[:status] = "visible"
190
+ end
191
+
192
+ results << info
193
+ end
194
+ results
195
+ end
104
196
 
105
197
  def set_player(x, y, z, angle)
106
198
  @player_x = x.to_f
@@ -132,7 +224,10 @@ module Doom
132
224
  # Precompute column angles for floor/ceiling rendering
133
225
  precompute_column_data
134
226
 
135
- # Draw floor/ceiling background first (will be partially overwritten by walls)
227
+ # Draw player's sector floor/ceiling as background fallback.
228
+ # Visplanes (with correct per-sector lighting) overwrite this for sectors
229
+ # with different properties. This only remains visible at gaps between
230
+ # same-property sectors where the light level matches anyway.
136
231
  draw_floor_ceiling_background
137
232
 
138
233
  # Initialize visplanes for tracking visible floor/ceiling spans
@@ -244,7 +339,7 @@ module Doom
244
339
  def draw_span(plane, y, x1, x2)
245
340
  return if x1.nil? || x1 > x2 || y < 0 || y >= SCREEN_HEIGHT
246
341
 
247
- flat = @flats[plane.texture]
342
+ flat = @flats[anim_flat(plane.texture)]
248
343
  return unless flat
249
344
 
250
345
  # Distance from horizon (y=100 for 200-high screen)
@@ -270,6 +365,7 @@ module Doom
270
365
  player_x = @player_x
271
366
  neg_player_y = -@player_y
272
367
  row_offset = y * SCREEN_WIDTH
368
+ flat_pixels = flat.pixels
273
369
 
274
370
  # Clamp to screen bounds
275
371
  x1 = 0 if x1 < 0
@@ -281,13 +377,22 @@ module Doom
281
377
  ray_dist = perp_dist * column_distscale[x]
282
378
  tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
283
379
  tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
284
- color = flat[tex_x, tex_y]
380
+ color = flat_pixels[tex_y * 64 + tex_x]
285
381
  framebuffer[row_offset + x] = cmap[color]
286
382
  x += 1
287
383
  end
288
384
  end
289
385
 
290
386
  # Render sky ceiling as columns (column-based like walls, not spans)
387
+ # Sky texture mid-point: texel row at screen center.
388
+ # Chocolate Doom: skytexturemid = SCREENHEIGHT/2 = 100 (for 200px screen).
389
+ # Scale factor: our 240px screen maps to DOOM's 200px sky coordinates.
390
+ SKY_TEXTUREMID = 100.0
391
+ SKY_YSCALE = 200.0 / SCREEN_HEIGHT # 0.833 - maps our pixels to DOOM's 200px space
392
+ # Chocolate Doom: ANGLETOSKYSHIFT=22 gives 4 sky repetitions per 360 degrees.
393
+ # Full circle (2pi) * 512/pi = 1024 columns, masked to 256 = 4 repetitions.
394
+ SKY_XSCALE = 512.0 / Math::PI
395
+
291
396
  def draw_sky_plane(plane)
292
397
  sky_texture = @textures['SKY1']
293
398
  return unless sky_texture
@@ -295,8 +400,8 @@ module Doom
295
400
  framebuffer = @framebuffer
296
401
  player_angle = @player_angle
297
402
  projection = @projection
298
- sky_width_mask = sky_texture.width - 1 # Power-of-2 textures: & is faster than %
299
- sky_height_mask = sky_texture.height - 1
403
+ sky_width = sky_texture.width
404
+ sky_height = sky_texture.height
300
405
 
301
406
  # Clamp to screen bounds
302
407
  minx = [plane.minx, 0].max
@@ -311,14 +416,17 @@ module Doom
311
416
  y1 = 0 if y1 < 0
312
417
  y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT
313
418
 
314
- # Sky X based on view angle (wraps around 256 degrees)
419
+ # Sky X: 4 repetitions per 360 degrees (matching ANGLETOSKYSHIFT=22)
315
420
  column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
316
- sky_x = (column_angle * 256 / Math::PI).to_i & sky_width_mask
421
+ sky_x = (column_angle * SKY_XSCALE).to_i % sky_width
317
422
  column = sky_texture.column_pixels(sky_x)
318
423
  next unless column
319
424
 
425
+ # Sky Y: texel = skytexturemid + (y - centery) * scale
426
+ # Maps texel 0 to screen top, texel 100 to horizon (1:1 for DOOM's 200px)
320
427
  (y1..y2).each do |y|
321
- color = column[y & sky_height_mask]
428
+ tex_y = (SKY_TEXTUREMID + (y - HALF_HEIGHT) * SKY_YSCALE).to_i % sky_height
429
+ color = column[tex_y]
322
430
  framebuffer[y * SCREEN_WIDTH + x] = color
323
431
  end
324
432
  end
@@ -410,8 +518,10 @@ module Doom
410
518
 
411
519
  ceil_height = (default_sector.ceiling_height - @player_z).abs
412
520
  floor_height = (default_sector.floor_height - @player_z).abs
413
- ceil_flat = @flats[default_sector.ceiling_texture]
414
- floor_flat = @flats[default_sector.floor_texture]
521
+ ceil_flat = @flats[anim_flat(default_sector.ceiling_texture)]
522
+ floor_flat = @flats[anim_flat(default_sector.floor_texture)]
523
+ ceil_pixels = ceil_flat&.pixels
524
+ floor_pixels = floor_flat&.pixels
415
525
  is_sky = default_sector.ceiling_texture == 'F_SKY1'
416
526
  sky_texture = is_sky ? @textures['SKY1'] : nil
417
527
  light_level = default_sector.light_level
@@ -437,13 +547,13 @@ module Doom
437
547
  row_offset = y * SCREEN_WIDTH
438
548
 
439
549
  if is_sky && sky_texture
440
- sky_height_mask = sky_texture.height - 1
441
- sky_width_mask = sky_texture.width - 1
442
- sky_y = y & sky_height_mask
550
+ sky_w = sky_texture.width
551
+ sky_h = sky_texture.height
552
+ sky_y = (SKY_TEXTUREMID + (y - HALF_HEIGHT) * SKY_YSCALE).to_i % sky_h
443
553
  x = 0
444
554
  while x < SCREEN_WIDTH
445
555
  column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
446
- sky_x = (column_angle * 256 / Math::PI).to_i & sky_width_mask
556
+ sky_x = (column_angle * SKY_XSCALE).to_i % sky_w
447
557
  color = sky_texture.column_pixels(sky_x)[sky_y]
448
558
  framebuffer[row_offset + x] = color
449
559
  x += 1
@@ -454,7 +564,7 @@ module Doom
454
564
  ray_dist = perp_dist * column_distscale[x]
455
565
  tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
456
566
  tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
457
- color = ceil_flat[tex_x, tex_y]
567
+ color = ceil_pixels[tex_y * 64 + tex_x]
458
568
  framebuffer[row_offset + x] = cmap[color]
459
569
  x += 1
460
570
  end
@@ -481,7 +591,7 @@ module Doom
481
591
  ray_dist = perp_dist * column_distscale[x]
482
592
  tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
483
593
  tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
484
- color = floor_flat[tex_x, tex_y]
594
+ color = floor_pixels[tex_y * 64 + tex_x]
485
595
  framebuffer[row_offset + x] = cmap[color]
486
596
  x += 1
487
597
  end
@@ -494,6 +604,15 @@ module Doom
494
604
 
495
605
  private
496
606
 
607
+ # Translate flat/texture names through animation system
608
+ def anim_flat(name)
609
+ @animations ? @animations.translate_flat(name) : name
610
+ end
611
+
612
+ def anim_texture(name)
613
+ @animations ? @animations.translate_texture(name) : name
614
+ end
615
+
497
616
  def clear_framebuffer
498
617
  @framebuffer.fill(0)
499
618
  end
@@ -515,13 +634,58 @@ module Doom
515
634
 
516
635
  if side == 0
517
636
  render_bsp_node(node.child_right)
518
- render_bsp_node(node.child_left)
637
+ back_bbox = node.bbox_left
638
+ render_bsp_node(node.child_left) if check_bbox(back_bbox)
519
639
  else
520
640
  render_bsp_node(node.child_left)
521
- render_bsp_node(node.child_right)
641
+ back_bbox = node.bbox_right
642
+ render_bsp_node(node.child_right) if check_bbox(back_bbox)
522
643
  end
523
644
  end
524
645
 
646
+ # R_CheckBBox - check if a bounding box is potentially visible.
647
+ # Projects the bbox corners to screen columns and checks if any
648
+ # column in that range is not fully occluded.
649
+ def check_bbox(bbox)
650
+ # Transform all 4 corners to view space
651
+ near = 1.0
652
+ all_in_front = true
653
+ min_sx = SCREEN_WIDTH
654
+ max_sx = -1
655
+
656
+ [[bbox.left, bbox.bottom], [bbox.right, bbox.bottom],
657
+ [bbox.left, bbox.top], [bbox.right, bbox.top]].each do |wx, wy|
658
+ vx, vy = transform_point(wx, wy)
659
+
660
+ if vy < near
661
+ # Any corner behind the near plane - bbox is too close to cull safely
662
+ all_in_front = false
663
+ else
664
+ sx = HALF_WIDTH + (vx * @projection / vy)
665
+ sx_i = sx.to_i
666
+ min_sx = sx_i if sx_i < min_sx
667
+ max_sx = sx_i if sx_i > max_sx
668
+ end
669
+ end
670
+
671
+ # If any corner is behind the near plane, conservatively assume visible.
672
+ # Only cull when all 4 corners are cleanly in front and we can check occlusion.
673
+ return true unless all_in_front
674
+
675
+ min_sx = 0 if min_sx < 0
676
+ max_sx = SCREEN_WIDTH - 1 if max_sx >= SCREEN_WIDTH
677
+ return false if min_sx > max_sx
678
+
679
+ # Check if any column in the range is not fully occluded
680
+ x = min_sx
681
+ while x <= max_sx
682
+ return true if @ceiling_clip[x] < @floor_clip[x] - 1
683
+ x += 1
684
+ end
685
+
686
+ false
687
+ end
688
+
525
689
  def point_on_side(x, y, node)
526
690
  dx = x - node.x
527
691
  dy = y - node.y
@@ -694,10 +858,37 @@ module Doom
694
858
  end
695
859
 
696
860
  def draw_seg_range(x1, x2, sx1, sx2, dist1, dist2, sector, back_sector, sidedef, linedef, seg, seg_length)
697
- # Get seg vertices in world space for texture coordinate calculation
698
861
  seg_v1 = @map.vertices[seg.v1]
699
862
  seg_v2 = @map.vertices[seg.v2]
700
863
 
864
+ # Precompute ray-seg intersection coefficients for per-column texture mapping.
865
+ # For screen column x, the view ray direction in world space is:
866
+ # ray = (sin_a * dx_col + cos_a * proj, -cos_a * dx_col + sin_a * proj)
867
+ # where dx_col = x - HALF_WIDTH.
868
+ # The ray-seg intersection parameter s (position along seg) is:
869
+ # s = (E * x + F) / (A * x + B)
870
+ # where A, B, E, F are precomputed per-seg constants.
871
+ seg_dx = (seg_v2.x - seg_v1.x).to_f
872
+ seg_dy = (seg_v2.y - seg_v1.y).to_f
873
+ px = @player_x - seg_v1.x.to_f
874
+ py = @player_y - seg_v1.y.to_f
875
+ sin_a = @sin_angle
876
+ cos_a = @cos_angle
877
+ proj = @projection
878
+
879
+ c1 = cos_a * proj - sin_a * HALF_WIDTH
880
+ c2 = sin_a * proj + cos_a * HALF_WIDTH
881
+
882
+ # denom(x) = seg_dy * ray_dx - seg_dx * ray_dy = A * x + B
883
+ tex_a = seg_dy * sin_a + seg_dx * cos_a
884
+ tex_b = seg_dy * c1 - seg_dx * c2
885
+
886
+ # numer(x) = py * ray_dx - px * ray_dy = E * x + F
887
+ tex_e = py * sin_a + px * cos_a
888
+ tex_f = py * c1 - px * c2
889
+
890
+ tex_offset = seg.offset + sidedef.x_offset
891
+
701
892
  # Calculate scales for drawseg (scale = projection / distance)
702
893
  scale1 = dist1 > 0 ? @projection / dist1 : Float::INFINITY
703
894
  scale2 = dist2 > 0 ? @projection / dist2 : Float::INFINITY
@@ -743,11 +934,10 @@ module Doom
743
934
  (x1..x2).each do |x|
744
935
  next if @ceiling_clip[x] >= @floor_clip[x] - 1
745
936
 
746
- # Screen-space interpolation factor for distance only
937
+ # Screen-space interpolation for distance
747
938
  t = sx2 != sx1 ? (x - sx1) / (sx2 - sx1) : 0
748
939
  t = t.clamp(0.0, 1.0)
749
940
 
750
- # Perspective-correct interpolation for distance
751
941
  if dist1 > 0 && dist2 > 0
752
942
  inv_dist = (1.0 - t) / dist1 + t / dist2
753
943
  dist = 1.0 / inv_dist
@@ -755,18 +945,15 @@ module Doom
755
945
  dist = dist1 > 0 ? dist1 : dist2
756
946
  end
757
947
 
758
- # Texture column using perspective-correct interpolation
759
- # tex_col_1 is texture coordinate at seg v1, tex_col_2 at v2
760
- tex_col_1 = seg.offset + sidedef.x_offset
761
- tex_col_2 = seg.offset + seg_length + sidedef.x_offset
762
-
763
- # Perspective-correct interpolation: interpolate tex/z, then multiply by z
764
- if dist1 > 0 && dist2 > 0
765
- tex_col = ((tex_col_1 / dist1) * (1.0 - t) + (tex_col_2 / dist2) * t) / inv_dist
948
+ # Ray-seg intersection for texture column
949
+ # s = (E * x + F) / (A * x + B) gives position along seg in world units
950
+ denom = tex_a * x + tex_b
951
+ if denom.abs < 0.001
952
+ s = 0.0
766
953
  else
767
- tex_col = tex_col_1 * (1.0 - t) + tex_col_2 * t
954
+ s = (tex_e * x + tex_f) / denom
768
955
  end
769
- tex_col = tex_col.to_i
956
+ tex_col = (tex_offset + s * seg_length).to_i
770
957
 
771
958
  # Skip if too close
772
959
  next if dist < 1
@@ -779,7 +966,10 @@ module Doom
779
966
  front_ceil = sector.ceiling_height - @player_z
780
967
 
781
968
  # Project to screen Y (Y increases downward on screen)
782
- front_ceil_y = (HALF_HEIGHT - front_ceil * scale).to_i
969
+ # Chocolate Doom rounds ceiling UP and floor DOWN to avoid 1-pixel gaps:
970
+ # yl = (topfrac+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for front ceiling)
971
+ # yh = bottomfrac>>HEIGHTBITS (floor for front floor)
972
+ front_ceil_y = (HALF_HEIGHT - front_ceil * scale).ceil
783
973
  front_floor_y = (HALF_HEIGHT - front_floor * scale).to_i
784
974
 
785
975
  # Clamp to current clip bounds
@@ -791,8 +981,11 @@ module Doom
791
981
  back_floor = back_sector.floor_height - @player_z
792
982
  back_ceil = back_sector.ceiling_height - @player_z
793
983
 
984
+ # Chocolate Doom rounding:
985
+ # pixhigh>>HEIGHTBITS (truncate for back ceiling / upper wall end)
986
+ # (pixlow+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for back floor / lower wall start)
794
987
  back_ceil_y = (HALF_HEIGHT - back_ceil * scale).to_i
795
- back_floor_y = (HALF_HEIGHT - back_floor * scale).to_i
988
+ back_floor_y = (HALF_HEIGHT - back_floor * scale).ceil
796
989
 
797
990
  # Determine visible ceiling/floor boundaries (the opening between sectors)
798
991
  # high_ceil = top of the opening on screen (max Y = lower world ceiling)
@@ -806,12 +999,29 @@ module Doom
806
999
  closed_door = back_sector.ceiling_height <= sector.floor_height ||
807
1000
  back_sector.floor_height >= sector.ceiling_height
808
1001
 
809
- # Mark ceiling visplane - mark front sector's ceiling for two-sided lines
810
- # Matches Chocolate Doom: mark from ceilingclip+1 to yl-1 (front ceiling)
811
- # (yl is clamped to ceilingclip+1, so we use ceil_y which is already clamped)
812
- should_mark_ceiling = sector.ceiling_height != back_sector.ceiling_height ||
813
- sector.ceiling_texture != back_sector.ceiling_texture ||
1002
+ both_sky = sector.ceiling_texture == 'F_SKY1' && back_sector.ceiling_texture == 'F_SKY1'
1003
+
1004
+ # Determine whether to mark ceiling/floor visplanes.
1005
+ # Match Chocolate Doom: only mark when properties differ, plus force
1006
+ # for closed doors. The background fill covers same-property gaps.
1007
+ # Chocolate Doom sky hack: "worldtop = worldhigh" makes the front
1008
+ # ceiling equal to the back ceiling when both are sky. This prevents
1009
+ # the low sky ceiling from clipping walls in adjacent sectors.
1010
+ effective_front_ceil = both_sky ? back_sector.ceiling_height : sector.ceiling_height
1011
+
1012
+ if closed_door
1013
+ should_mark_ceiling = true
1014
+ should_mark_floor = true
1015
+ else
1016
+ should_mark_ceiling = effective_front_ceil != back_sector.ceiling_height ||
1017
+ sector.ceiling_texture != back_sector.ceiling_texture ||
1018
+ sector.light_level != back_sector.light_level
1019
+ should_mark_floor = sector.floor_height != back_sector.floor_height ||
1020
+ sector.floor_texture != back_sector.floor_texture ||
814
1021
  sector.light_level != back_sector.light_level
1022
+ end
1023
+
1024
+ # Mark ceiling visplane
815
1025
  if @current_ceiling_plane && should_mark_ceiling
816
1026
  mark_top = @ceiling_clip[x] + 1
817
1027
  mark_bottom = ceil_y - 1
@@ -821,12 +1031,7 @@ module Doom
821
1031
  end
822
1032
  end
823
1033
 
824
- # Mark floor visplane - mark front sector's floor for two-sided lines
825
- # Matches Chocolate Doom: mark from yh+1 (front floor) to floorclip-1
826
- # (yh is clamped to floorclip-1, so we use floor_y which is already clamped)
827
- should_mark_floor = sector.floor_height != back_sector.floor_height ||
828
- sector.floor_texture != back_sector.floor_texture ||
829
- sector.light_level != back_sector.light_level
1034
+ # Mark floor visplane
830
1035
  if @current_floor_plane && should_mark_floor
831
1036
  mark_top = floor_y + 1
832
1037
  mark_bottom = @floor_clip[x] - 1
@@ -836,11 +1041,8 @@ module Doom
836
1041
  end
837
1042
  end
838
1043
 
839
- # Upper wall (ceiling step down)
840
- if sector.ceiling_height > back_sector.ceiling_height
841
- # Upper texture Y offset depends on DONTPEGTOP flag
842
- # With DONTPEGTOP: texture top aligns with front ceiling
843
- # Without: texture bottom aligns with back ceiling (for doors opening)
1044
+ # Upper wall (ceiling step down) - skip if both sectors have sky
1045
+ if !both_sky && sector.ceiling_height > back_sector.ceiling_height
844
1046
  if linedef.upper_unpegged?
845
1047
  upper_tex_y = sidedef.y_offset
846
1048
  else
@@ -848,70 +1050,68 @@ module Doom
848
1050
  tex_height = texture ? texture.height : 128
849
1051
  upper_tex_y = sidedef.y_offset + back_sector.ceiling_height - sector.ceiling_height + tex_height
850
1052
  end
851
- draw_wall_column_ex(x, ceil_y, back_ceil_y - 1, sidedef.upper_texture, dist,
1053
+ draw_wall_column_ex(x, ceil_y, back_ceil_y, sidedef.upper_texture, dist,
852
1054
  sector.light_level, tex_col, upper_tex_y, scale, sector.ceiling_height, back_sector.ceiling_height)
853
- # Note: Upper walls don't fully occlude - sprites can be visible through openings
854
1055
  end
855
1056
 
856
1057
  # Lower wall (floor step up)
857
1058
  if sector.floor_height < back_sector.floor_height
858
- # Lower texture Y offset depends on DONTPEGBOTTOM flag
859
- # With DONTPEGBOTTOM: texture bottom aligns with lower floor
860
- # Without: texture top aligns with higher floor
861
1059
  if linedef.lower_unpegged?
862
1060
  lower_tex_y = sidedef.y_offset + sector.ceiling_height - back_sector.floor_height
863
1061
  else
864
1062
  lower_tex_y = sidedef.y_offset
865
1063
  end
866
- draw_wall_column_ex(x, back_floor_y + 1, floor_y, sidedef.lower_texture, dist,
1064
+ draw_wall_column_ex(x, back_floor_y, floor_y, sidedef.lower_texture, dist,
867
1065
  sector.light_level, tex_col, lower_tex_y, scale, back_sector.floor_height, sector.floor_height)
868
- # Note: Lower walls don't fully occlude - sprites can be visible through openings
869
1066
  end
870
1067
 
871
- # Update clip bounds after marking
1068
+ # Update clip bounds
872
1069
  if closed_door
873
- # Closed door - fully occlude this column
874
1070
  @wall_depth[x] = [@wall_depth[x], dist].min
875
1071
  @ceiling_clip[x] = SCREEN_HEIGHT
876
1072
  @floor_clip[x] = -1
877
1073
  else
878
- # Ceiling clip increases (moves down) as ceiling is marked
879
- if sector.ceiling_height > back_sector.ceiling_height
880
- # Upper wall drawn - clip ceiling to back ceiling
1074
+ # Ceiling clip (uses effective_front_ceil for sky hack)
1075
+ if effective_front_ceil > back_sector.ceiling_height
881
1076
  @ceiling_clip[x] = [back_ceil_y, @ceiling_clip[x]].max
882
- elsif sector.ceiling_height < back_sector.ceiling_height
883
- # Ceiling step up - clip to front ceiling
884
- @ceiling_clip[x] = [ceil_y, @ceiling_clip[x]].max
885
- elsif should_mark_ceiling
886
- # Same height but different texture/light - still update clip
887
- # Matches Chocolate Doom: if (markceiling) ceilingclip[rw_x] = yl-1;
1077
+ elsif effective_front_ceil < back_sector.ceiling_height
1078
+ @ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
1079
+ elsif sector.ceiling_texture != back_sector.ceiling_texture ||
1080
+ sector.light_level != back_sector.light_level
888
1081
  @ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
889
1082
  end
890
1083
 
891
- # Floor clip decreases (moves up) as floor is marked
1084
+ # Floor clip
892
1085
  if sector.floor_height < back_sector.floor_height
893
- # Lower wall drawn - clip floor to back floor
894
1086
  @floor_clip[x] = [back_floor_y, @floor_clip[x]].min
895
1087
  elsif sector.floor_height > back_sector.floor_height
896
- # Floor step down - clip to front floor to allow back sector to mark later
897
- @floor_clip[x] = [floor_y, @floor_clip[x]].min
898
- elsif should_mark_floor
899
- # Same height but different texture/light - still update clip
900
- # Matches Chocolate Doom: if (markfloor) floorclip[rw_x] = yh+1;
1088
+ # Chocolate Doom: else if (markfloor) floorclip = yh + 1
1089
+ @floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
1090
+ elsif sector.floor_texture != back_sector.floor_texture ||
1091
+ sector.light_level != back_sector.light_level
901
1092
  @floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
902
1093
  end
903
1094
  end
904
1095
  else
905
1096
  # One-sided (solid) wall
906
1097
  # Mark ceiling visplane (from previous clip to wall's ceiling)
907
- if @current_ceiling_plane && @ceiling_clip[x] + 1 <= ceil_y - 1
908
- @current_ceiling_plane.mark(x, @ceiling_clip[x] + 1, ceil_y - 1)
1098
+ # Clamp to floor_clip to prevent ceiling bleeding through portal openings
1099
+ if @current_ceiling_plane
1100
+ mark_top = @ceiling_clip[x] + 1
1101
+ mark_bottom = [ceil_y - 1, @floor_clip[x] - 1].min
1102
+ if mark_top <= mark_bottom
1103
+ @current_ceiling_plane.mark(x, mark_top, mark_bottom)
1104
+ end
909
1105
  end
910
1106
 
911
1107
  # Mark floor visplane (from wall's floor to previous floor clip)
912
- # Floor is visible BELOW the wall (from floor_y+1 to floor_clip-1)
913
- if @current_floor_plane && floor_y + 1 <= @floor_clip[x] - 1
914
- @current_floor_plane.mark(x, floor_y + 1, @floor_clip[x] - 1)
1108
+ # Clamp to ceiling_clip to prevent floor bleeding through portal openings
1109
+ if @current_floor_plane
1110
+ mark_top = [floor_y + 1, @ceiling_clip[x] + 1].max
1111
+ mark_bottom = @floor_clip[x] - 1
1112
+ if mark_top <= mark_bottom
1113
+ @current_floor_plane.mark(x, mark_top, mark_bottom)
1114
+ end
915
1115
  end
916
1116
 
917
1117
  # Draw wall (from clipped ceiling to clipped floor)
@@ -971,17 +1171,17 @@ module Doom
971
1171
  y2 = [y2, clip_bottom].min
972
1172
  return if y1 > y2
973
1173
 
974
- texture = @textures[texture_name]
1174
+ texture = @textures[anim_texture(texture_name)]
975
1175
  return unless texture
976
1176
 
977
1177
  light = calculate_light(light_level, dist)
978
1178
  cmap = @colormap.maps[light]
979
1179
  framebuffer = @framebuffer
980
- tex_width_mask = texture.width - 1 # For power-of-2 textures: & mask is faster than %
981
- tex_height_mask = texture.height - 1
1180
+ tex_width = texture.width
1181
+ tex_height = texture.height
982
1182
 
983
- # Texture X coordinate (wrap around texture width using bitwise AND)
984
- tex_x = tex_col.to_i & tex_width_mask
1183
+ # Texture X coordinate (wrap around texture width)
1184
+ tex_x = tex_col.to_i % tex_width
985
1185
 
986
1186
  # Get the column of pixels
987
1187
  column = texture.column_pixels(tex_x)
@@ -1004,7 +1204,7 @@ module Doom
1004
1204
  y = y1
1005
1205
  while y <= y2
1006
1206
  screen_offset = y - y1
1007
- tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i & tex_height_mask
1207
+ tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i % tex_height
1008
1208
 
1009
1209
  color = column[tex_y]
1010
1210
  framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
@@ -1081,7 +1281,10 @@ module Doom
1081
1281
  # Collect visible sprites with their distances
1082
1282
  visible_sprites = []
1083
1283
 
1084
- @map.things.each do |thing|
1284
+ @map.things.each_with_index do |thing, thing_idx|
1285
+ # Skip picked-up items
1286
+ next if @hidden_things && @hidden_things[thing_idx]
1287
+
1085
1288
  # Check if we have a sprite for this thing type
1086
1289
  next unless @sprites.prefix_for(thing.type)
1087
1290
 
@@ -1099,8 +1302,12 @@ module Doom
1099
1302
  dy = thing.y - @player_y
1100
1303
  angle_to_thing = Math.atan2(dy, dx)
1101
1304
 
1102
- # Get the correct rotated sprite
1103
- sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
1305
+ # Get sprite - use death frame if monster is dead
1306
+ if @combat && @combat.dead?(thing_idx)
1307
+ sprite = @combat.death_sprite(thing_idx, thing.type, angle_to_thing, thing.angle)
1308
+ else
1309
+ sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
1310
+ end
1104
1311
  next unless sprite
1105
1312
 
1106
1313
  # Project to screen X