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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/doom/game/animations.rb +97 -0
- data/lib/doom/game/combat.rb +244 -0
- data/lib/doom/game/item_pickup.rb +170 -0
- data/lib/doom/game/player_state.rb +101 -9
- data/lib/doom/game/sector_effects.rb +179 -0
- data/lib/doom/platform/gosu_window.rb +536 -66
- data/lib/doom/render/renderer.rb +297 -90
- data/lib/doom/render/status_bar.rb +74 -22
- data/lib/doom/render/weapon_renderer.rb +25 -28
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +70 -2
- data/lib/doom/wad/sprite.rb +95 -22
- data/lib/doom/wad/texture.rb +23 -12
- data/lib/doom.rb +13 -2
- metadata +7 -6
data/lib/doom/render/renderer.rb
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
299
|
-
|
|
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
|
|
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 *
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
sky_y = y
|
|
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 *
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
759
|
-
#
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
954
|
+
s = (tex_e * x + tex_f) / denom
|
|
768
955
|
end
|
|
769
|
-
tex_col =
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
#
|
|
812
|
-
|
|
813
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
879
|
-
if
|
|
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
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
|
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
|
-
#
|
|
897
|
-
@floor_clip[x] = [floor_y, @floor_clip[x]].min
|
|
898
|
-
elsif
|
|
899
|
-
|
|
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
|
-
|
|
908
|
-
|
|
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
|
-
#
|
|
913
|
-
if @current_floor_plane
|
|
914
|
-
|
|
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
|
-
|
|
981
|
-
|
|
1180
|
+
tex_width = texture.width
|
|
1181
|
+
tex_height = texture.height
|
|
982
1182
|
|
|
983
|
-
# Texture X coordinate (wrap around texture width
|
|
984
|
-
tex_x = tex_col.to_i
|
|
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
|
|
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.
|
|
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
|
|
1103
|
-
|
|
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
|