doom 0.4.0 → 0.6.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 +44 -22
- data/bin/doom +5 -3
- data/lib/doom/game/animations.rb +97 -0
- data/lib/doom/game/combat.rb +370 -0
- data/lib/doom/game/item_pickup.rb +170 -0
- data/lib/doom/game/monster_ai.rb +295 -0
- data/lib/doom/game/player_state.rb +144 -9
- data/lib/doom/game/sector_effects.rb +179 -0
- data/lib/doom/platform/gosu_window.rb +673 -71
- data/lib/doom/render/renderer.rb +346 -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 +98 -24
- data/lib/doom/wad/texture.rb +23 -12
- data/lib/doom/wad_downloader.rb +10 -1
- data/lib/doom.rb +15 -2
- metadata +9 -7
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,11 @@ 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
|
|
67
|
+
@monster_ai = nil
|
|
68
|
+
@leveltime = 0
|
|
64
69
|
|
|
65
70
|
@framebuffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0)
|
|
66
71
|
|
|
@@ -100,7 +105,96 @@ module Doom
|
|
|
100
105
|
@y_slope_floor = Array.new(HALF_HEIGHT + 1, 0.0)
|
|
101
106
|
end
|
|
102
107
|
|
|
103
|
-
attr_reader :player_x, :player_y, :player_z, :sin_angle, :cos_angle
|
|
108
|
+
attr_reader :player_x, :player_y, :player_z, :sin_angle, :cos_angle, :framebuffer
|
|
109
|
+
attr_writer :hidden_things, :combat, :monster_ai, :leveltime
|
|
110
|
+
|
|
111
|
+
# Diagnostic: returns info about all sprites and why they are/aren't visible
|
|
112
|
+
def sprite_diagnostics
|
|
113
|
+
return [] unless @sprites
|
|
114
|
+
|
|
115
|
+
results = []
|
|
116
|
+
@map.things.each do |thing|
|
|
117
|
+
prefix = @sprites.prefix_for(thing.type)
|
|
118
|
+
next unless prefix
|
|
119
|
+
|
|
120
|
+
info = { type: thing.type, x: thing.x, y: thing.y, prefix: prefix }
|
|
121
|
+
|
|
122
|
+
view_x, view_y = transform_point(thing.x, thing.y)
|
|
123
|
+
info[:view_x] = view_x.round(1)
|
|
124
|
+
info[:view_y] = view_y.round(1)
|
|
125
|
+
|
|
126
|
+
if view_y <= 0
|
|
127
|
+
info[:status] = "behind_player"
|
|
128
|
+
results << info
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
dist = view_y
|
|
133
|
+
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
134
|
+
info[:screen_x] = screen_x.round(1)
|
|
135
|
+
info[:dist] = dist.round(1)
|
|
136
|
+
|
|
137
|
+
dx = thing.x - @player_x
|
|
138
|
+
dy = thing.y - @player_y
|
|
139
|
+
angle_to_thing = Math.atan2(dy, dx)
|
|
140
|
+
sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
|
|
141
|
+
unless sprite
|
|
142
|
+
info[:status] = "no_sprite_frame"
|
|
143
|
+
results << info
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
sprite_scale = @projection / dist
|
|
148
|
+
sprite_half_width = (sprite.width * @projection / dist / 2).to_i
|
|
149
|
+
info[:sprite_scale] = sprite_scale.round(3)
|
|
150
|
+
|
|
151
|
+
if screen_x + sprite_half_width < 0
|
|
152
|
+
info[:status] = "off_screen_left"
|
|
153
|
+
elsif screen_x - sprite_half_width >= SCREEN_WIDTH
|
|
154
|
+
info[:status] = "off_screen_right"
|
|
155
|
+
else
|
|
156
|
+
# Check drawseg clipping
|
|
157
|
+
sprite_left = (screen_x - sprite.left_offset * sprite_scale).to_i
|
|
158
|
+
sprite_right = sprite_left + (sprite.width * sprite_scale).to_i - 1
|
|
159
|
+
x1 = [sprite_left, 0].max
|
|
160
|
+
x2 = [sprite_right, SCREEN_WIDTH - 1].min
|
|
161
|
+
|
|
162
|
+
sector = @map.sector_at(thing.x, thing.y)
|
|
163
|
+
thing_floor = sector ? sector.floor_height : 0
|
|
164
|
+
sprite_gz = thing_floor
|
|
165
|
+
sprite_gzt = thing_floor + sprite.top_offset
|
|
166
|
+
|
|
167
|
+
clipping_segs = []
|
|
168
|
+
@drawsegs.reverse_each do |ds|
|
|
169
|
+
next if ds.x1 > x2 || ds.x2 < x1
|
|
170
|
+
next if ds.silhouette == SIL_NONE
|
|
171
|
+
|
|
172
|
+
lowscale = [ds.scale1, ds.scale2].min
|
|
173
|
+
highscale = [ds.scale1, ds.scale2].max
|
|
174
|
+
|
|
175
|
+
if highscale < sprite_scale
|
|
176
|
+
next
|
|
177
|
+
elsif lowscale < sprite_scale
|
|
178
|
+
next unless point_on_seg_side(thing.x, thing.y, ds.curline)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
clipping_segs << {
|
|
182
|
+
x1: ds.x1, x2: ds.x2,
|
|
183
|
+
scale: "#{ds.scale1.round(3)}..#{ds.scale2.round(3)}",
|
|
184
|
+
sil: ds.silhouette
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
info[:screen_range] = "#{x1}..#{x2}"
|
|
189
|
+
info[:clipping_segs] = clipping_segs.size
|
|
190
|
+
info[:clipping_detail] = clipping_segs
|
|
191
|
+
info[:status] = "visible"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
results << info
|
|
195
|
+
end
|
|
196
|
+
results
|
|
197
|
+
end
|
|
104
198
|
|
|
105
199
|
def set_player(x, y, z, angle)
|
|
106
200
|
@player_x = x.to_f
|
|
@@ -132,7 +226,10 @@ module Doom
|
|
|
132
226
|
# Precompute column angles for floor/ceiling rendering
|
|
133
227
|
precompute_column_data
|
|
134
228
|
|
|
135
|
-
# Draw floor/ceiling background
|
|
229
|
+
# Draw player's sector floor/ceiling as background fallback.
|
|
230
|
+
# Visplanes (with correct per-sector lighting) overwrite this for sectors
|
|
231
|
+
# with different properties. This only remains visible at gaps between
|
|
232
|
+
# same-property sectors where the light level matches anyway.
|
|
136
233
|
draw_floor_ceiling_background
|
|
137
234
|
|
|
138
235
|
# Initialize visplanes for tracking visible floor/ceiling spans
|
|
@@ -244,7 +341,7 @@ module Doom
|
|
|
244
341
|
def draw_span(plane, y, x1, x2)
|
|
245
342
|
return if x1.nil? || x1 > x2 || y < 0 || y >= SCREEN_HEIGHT
|
|
246
343
|
|
|
247
|
-
flat = @flats[plane.texture]
|
|
344
|
+
flat = @flats[anim_flat(plane.texture)]
|
|
248
345
|
return unless flat
|
|
249
346
|
|
|
250
347
|
# Distance from horizon (y=100 for 200-high screen)
|
|
@@ -270,6 +367,7 @@ module Doom
|
|
|
270
367
|
player_x = @player_x
|
|
271
368
|
neg_player_y = -@player_y
|
|
272
369
|
row_offset = y * SCREEN_WIDTH
|
|
370
|
+
flat_pixels = flat.pixels
|
|
273
371
|
|
|
274
372
|
# Clamp to screen bounds
|
|
275
373
|
x1 = 0 if x1 < 0
|
|
@@ -281,13 +379,22 @@ module Doom
|
|
|
281
379
|
ray_dist = perp_dist * column_distscale[x]
|
|
282
380
|
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
283
381
|
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
284
|
-
color =
|
|
382
|
+
color = flat_pixels[tex_y * 64 + tex_x]
|
|
285
383
|
framebuffer[row_offset + x] = cmap[color]
|
|
286
384
|
x += 1
|
|
287
385
|
end
|
|
288
386
|
end
|
|
289
387
|
|
|
290
388
|
# Render sky ceiling as columns (column-based like walls, not spans)
|
|
389
|
+
# Sky texture mid-point: texel row at screen center.
|
|
390
|
+
# Chocolate Doom: skytexturemid = SCREENHEIGHT/2 = 100 (for 200px screen).
|
|
391
|
+
# Scale factor: our 240px screen maps to DOOM's 200px sky coordinates.
|
|
392
|
+
SKY_TEXTUREMID = 100.0
|
|
393
|
+
SKY_YSCALE = 200.0 / SCREEN_HEIGHT # 0.833 - maps our pixels to DOOM's 200px space
|
|
394
|
+
# Chocolate Doom: ANGLETOSKYSHIFT=22 gives 4 sky repetitions per 360 degrees.
|
|
395
|
+
# Full circle (2pi) * 512/pi = 1024 columns, masked to 256 = 4 repetitions.
|
|
396
|
+
SKY_XSCALE = 512.0 / Math::PI
|
|
397
|
+
|
|
291
398
|
def draw_sky_plane(plane)
|
|
292
399
|
sky_texture = @textures['SKY1']
|
|
293
400
|
return unless sky_texture
|
|
@@ -295,8 +402,8 @@ module Doom
|
|
|
295
402
|
framebuffer = @framebuffer
|
|
296
403
|
player_angle = @player_angle
|
|
297
404
|
projection = @projection
|
|
298
|
-
|
|
299
|
-
|
|
405
|
+
sky_width = sky_texture.width
|
|
406
|
+
sky_height = sky_texture.height
|
|
300
407
|
|
|
301
408
|
# Clamp to screen bounds
|
|
302
409
|
minx = [plane.minx, 0].max
|
|
@@ -311,14 +418,17 @@ module Doom
|
|
|
311
418
|
y1 = 0 if y1 < 0
|
|
312
419
|
y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT
|
|
313
420
|
|
|
314
|
-
# Sky X
|
|
421
|
+
# Sky X: 4 repetitions per 360 degrees (matching ANGLETOSKYSHIFT=22)
|
|
315
422
|
column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
|
|
316
|
-
sky_x = (column_angle *
|
|
423
|
+
sky_x = (column_angle * SKY_XSCALE).to_i % sky_width
|
|
317
424
|
column = sky_texture.column_pixels(sky_x)
|
|
318
425
|
next unless column
|
|
319
426
|
|
|
427
|
+
# Sky Y: texel = skytexturemid + (y - centery) * scale
|
|
428
|
+
# Maps texel 0 to screen top, texel 100 to horizon (1:1 for DOOM's 200px)
|
|
320
429
|
(y1..y2).each do |y|
|
|
321
|
-
|
|
430
|
+
tex_y = (SKY_TEXTUREMID + (y - HALF_HEIGHT) * SKY_YSCALE).to_i % sky_height
|
|
431
|
+
color = column[tex_y]
|
|
322
432
|
framebuffer[y * SCREEN_WIDTH + x] = color
|
|
323
433
|
end
|
|
324
434
|
end
|
|
@@ -410,8 +520,10 @@ module Doom
|
|
|
410
520
|
|
|
411
521
|
ceil_height = (default_sector.ceiling_height - @player_z).abs
|
|
412
522
|
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]
|
|
523
|
+
ceil_flat = @flats[anim_flat(default_sector.ceiling_texture)]
|
|
524
|
+
floor_flat = @flats[anim_flat(default_sector.floor_texture)]
|
|
525
|
+
ceil_pixels = ceil_flat&.pixels
|
|
526
|
+
floor_pixels = floor_flat&.pixels
|
|
415
527
|
is_sky = default_sector.ceiling_texture == 'F_SKY1'
|
|
416
528
|
sky_texture = is_sky ? @textures['SKY1'] : nil
|
|
417
529
|
light_level = default_sector.light_level
|
|
@@ -437,13 +549,13 @@ module Doom
|
|
|
437
549
|
row_offset = y * SCREEN_WIDTH
|
|
438
550
|
|
|
439
551
|
if is_sky && sky_texture
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
sky_y = y
|
|
552
|
+
sky_w = sky_texture.width
|
|
553
|
+
sky_h = sky_texture.height
|
|
554
|
+
sky_y = (SKY_TEXTUREMID + (y - HALF_HEIGHT) * SKY_YSCALE).to_i % sky_h
|
|
443
555
|
x = 0
|
|
444
556
|
while x < SCREEN_WIDTH
|
|
445
557
|
column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
|
|
446
|
-
sky_x = (column_angle *
|
|
558
|
+
sky_x = (column_angle * SKY_XSCALE).to_i % sky_w
|
|
447
559
|
color = sky_texture.column_pixels(sky_x)[sky_y]
|
|
448
560
|
framebuffer[row_offset + x] = color
|
|
449
561
|
x += 1
|
|
@@ -454,7 +566,7 @@ module Doom
|
|
|
454
566
|
ray_dist = perp_dist * column_distscale[x]
|
|
455
567
|
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
456
568
|
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
457
|
-
color =
|
|
569
|
+
color = ceil_pixels[tex_y * 64 + tex_x]
|
|
458
570
|
framebuffer[row_offset + x] = cmap[color]
|
|
459
571
|
x += 1
|
|
460
572
|
end
|
|
@@ -481,7 +593,7 @@ module Doom
|
|
|
481
593
|
ray_dist = perp_dist * column_distscale[x]
|
|
482
594
|
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
483
595
|
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
484
|
-
color =
|
|
596
|
+
color = floor_pixels[tex_y * 64 + tex_x]
|
|
485
597
|
framebuffer[row_offset + x] = cmap[color]
|
|
486
598
|
x += 1
|
|
487
599
|
end
|
|
@@ -494,6 +606,15 @@ module Doom
|
|
|
494
606
|
|
|
495
607
|
private
|
|
496
608
|
|
|
609
|
+
# Translate flat/texture names through animation system
|
|
610
|
+
def anim_flat(name)
|
|
611
|
+
@animations ? @animations.translate_flat(name) : name
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def anim_texture(name)
|
|
615
|
+
@animations ? @animations.translate_texture(name) : name
|
|
616
|
+
end
|
|
617
|
+
|
|
497
618
|
def clear_framebuffer
|
|
498
619
|
@framebuffer.fill(0)
|
|
499
620
|
end
|
|
@@ -515,11 +636,56 @@ module Doom
|
|
|
515
636
|
|
|
516
637
|
if side == 0
|
|
517
638
|
render_bsp_node(node.child_right)
|
|
518
|
-
|
|
639
|
+
back_bbox = node.bbox_left
|
|
640
|
+
render_bsp_node(node.child_left) if check_bbox(back_bbox)
|
|
519
641
|
else
|
|
520
642
|
render_bsp_node(node.child_left)
|
|
521
|
-
|
|
643
|
+
back_bbox = node.bbox_right
|
|
644
|
+
render_bsp_node(node.child_right) if check_bbox(back_bbox)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# R_CheckBBox - check if a bounding box is potentially visible.
|
|
649
|
+
# Projects the bbox corners to screen columns and checks if any
|
|
650
|
+
# column in that range is not fully occluded.
|
|
651
|
+
def check_bbox(bbox)
|
|
652
|
+
# Transform all 4 corners to view space
|
|
653
|
+
near = 1.0
|
|
654
|
+
all_in_front = true
|
|
655
|
+
min_sx = SCREEN_WIDTH
|
|
656
|
+
max_sx = -1
|
|
657
|
+
|
|
658
|
+
[[bbox.left, bbox.bottom], [bbox.right, bbox.bottom],
|
|
659
|
+
[bbox.left, bbox.top], [bbox.right, bbox.top]].each do |wx, wy|
|
|
660
|
+
vx, vy = transform_point(wx, wy)
|
|
661
|
+
|
|
662
|
+
if vy < near
|
|
663
|
+
# Any corner behind the near plane - bbox is too close to cull safely
|
|
664
|
+
all_in_front = false
|
|
665
|
+
else
|
|
666
|
+
sx = HALF_WIDTH + (vx * @projection / vy)
|
|
667
|
+
sx_i = sx.to_i
|
|
668
|
+
min_sx = sx_i if sx_i < min_sx
|
|
669
|
+
max_sx = sx_i if sx_i > max_sx
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# If any corner is behind the near plane, conservatively assume visible.
|
|
674
|
+
# Only cull when all 4 corners are cleanly in front and we can check occlusion.
|
|
675
|
+
return true unless all_in_front
|
|
676
|
+
|
|
677
|
+
min_sx = 0 if min_sx < 0
|
|
678
|
+
max_sx = SCREEN_WIDTH - 1 if max_sx >= SCREEN_WIDTH
|
|
679
|
+
return false if min_sx > max_sx
|
|
680
|
+
|
|
681
|
+
# Check if any column in the range is not fully occluded
|
|
682
|
+
x = min_sx
|
|
683
|
+
while x <= max_sx
|
|
684
|
+
return true if @ceiling_clip[x] < @floor_clip[x] - 1
|
|
685
|
+
x += 1
|
|
522
686
|
end
|
|
687
|
+
|
|
688
|
+
false
|
|
523
689
|
end
|
|
524
690
|
|
|
525
691
|
def point_on_side(x, y, node)
|
|
@@ -694,10 +860,37 @@ module Doom
|
|
|
694
860
|
end
|
|
695
861
|
|
|
696
862
|
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
863
|
seg_v1 = @map.vertices[seg.v1]
|
|
699
864
|
seg_v2 = @map.vertices[seg.v2]
|
|
700
865
|
|
|
866
|
+
# Precompute ray-seg intersection coefficients for per-column texture mapping.
|
|
867
|
+
# For screen column x, the view ray direction in world space is:
|
|
868
|
+
# ray = (sin_a * dx_col + cos_a * proj, -cos_a * dx_col + sin_a * proj)
|
|
869
|
+
# where dx_col = x - HALF_WIDTH.
|
|
870
|
+
# The ray-seg intersection parameter s (position along seg) is:
|
|
871
|
+
# s = (E * x + F) / (A * x + B)
|
|
872
|
+
# where A, B, E, F are precomputed per-seg constants.
|
|
873
|
+
seg_dx = (seg_v2.x - seg_v1.x).to_f
|
|
874
|
+
seg_dy = (seg_v2.y - seg_v1.y).to_f
|
|
875
|
+
px = @player_x - seg_v1.x.to_f
|
|
876
|
+
py = @player_y - seg_v1.y.to_f
|
|
877
|
+
sin_a = @sin_angle
|
|
878
|
+
cos_a = @cos_angle
|
|
879
|
+
proj = @projection
|
|
880
|
+
|
|
881
|
+
c1 = cos_a * proj - sin_a * HALF_WIDTH
|
|
882
|
+
c2 = sin_a * proj + cos_a * HALF_WIDTH
|
|
883
|
+
|
|
884
|
+
# denom(x) = seg_dy * ray_dx - seg_dx * ray_dy = A * x + B
|
|
885
|
+
tex_a = seg_dy * sin_a + seg_dx * cos_a
|
|
886
|
+
tex_b = seg_dy * c1 - seg_dx * c2
|
|
887
|
+
|
|
888
|
+
# numer(x) = py * ray_dx - px * ray_dy = E * x + F
|
|
889
|
+
tex_e = py * sin_a + px * cos_a
|
|
890
|
+
tex_f = py * c1 - px * c2
|
|
891
|
+
|
|
892
|
+
tex_offset = seg.offset + sidedef.x_offset
|
|
893
|
+
|
|
701
894
|
# Calculate scales for drawseg (scale = projection / distance)
|
|
702
895
|
scale1 = dist1 > 0 ? @projection / dist1 : Float::INFINITY
|
|
703
896
|
scale2 = dist2 > 0 ? @projection / dist2 : Float::INFINITY
|
|
@@ -743,11 +936,10 @@ module Doom
|
|
|
743
936
|
(x1..x2).each do |x|
|
|
744
937
|
next if @ceiling_clip[x] >= @floor_clip[x] - 1
|
|
745
938
|
|
|
746
|
-
# Screen-space interpolation
|
|
939
|
+
# Screen-space interpolation for distance
|
|
747
940
|
t = sx2 != sx1 ? (x - sx1) / (sx2 - sx1) : 0
|
|
748
941
|
t = t.clamp(0.0, 1.0)
|
|
749
942
|
|
|
750
|
-
# Perspective-correct interpolation for distance
|
|
751
943
|
if dist1 > 0 && dist2 > 0
|
|
752
944
|
inv_dist = (1.0 - t) / dist1 + t / dist2
|
|
753
945
|
dist = 1.0 / inv_dist
|
|
@@ -755,18 +947,15 @@ module Doom
|
|
|
755
947
|
dist = dist1 > 0 ? dist1 : dist2
|
|
756
948
|
end
|
|
757
949
|
|
|
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
|
|
950
|
+
# Ray-seg intersection for texture column
|
|
951
|
+
# s = (E * x + F) / (A * x + B) gives position along seg in world units
|
|
952
|
+
denom = tex_a * x + tex_b
|
|
953
|
+
if denom.abs < 0.001
|
|
954
|
+
s = 0.0
|
|
766
955
|
else
|
|
767
|
-
|
|
956
|
+
s = (tex_e * x + tex_f) / denom
|
|
768
957
|
end
|
|
769
|
-
tex_col =
|
|
958
|
+
tex_col = (tex_offset + s * seg_length).to_i
|
|
770
959
|
|
|
771
960
|
# Skip if too close
|
|
772
961
|
next if dist < 1
|
|
@@ -779,7 +968,10 @@ module Doom
|
|
|
779
968
|
front_ceil = sector.ceiling_height - @player_z
|
|
780
969
|
|
|
781
970
|
# Project to screen Y (Y increases downward on screen)
|
|
782
|
-
|
|
971
|
+
# Chocolate Doom rounds ceiling UP and floor DOWN to avoid 1-pixel gaps:
|
|
972
|
+
# yl = (topfrac+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for front ceiling)
|
|
973
|
+
# yh = bottomfrac>>HEIGHTBITS (floor for front floor)
|
|
974
|
+
front_ceil_y = (HALF_HEIGHT - front_ceil * scale).ceil
|
|
783
975
|
front_floor_y = (HALF_HEIGHT - front_floor * scale).to_i
|
|
784
976
|
|
|
785
977
|
# Clamp to current clip bounds
|
|
@@ -791,8 +983,11 @@ module Doom
|
|
|
791
983
|
back_floor = back_sector.floor_height - @player_z
|
|
792
984
|
back_ceil = back_sector.ceiling_height - @player_z
|
|
793
985
|
|
|
986
|
+
# Chocolate Doom rounding:
|
|
987
|
+
# pixhigh>>HEIGHTBITS (truncate for back ceiling / upper wall end)
|
|
988
|
+
# (pixlow+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for back floor / lower wall start)
|
|
794
989
|
back_ceil_y = (HALF_HEIGHT - back_ceil * scale).to_i
|
|
795
|
-
back_floor_y = (HALF_HEIGHT - back_floor * scale).
|
|
990
|
+
back_floor_y = (HALF_HEIGHT - back_floor * scale).ceil
|
|
796
991
|
|
|
797
992
|
# Determine visible ceiling/floor boundaries (the opening between sectors)
|
|
798
993
|
# high_ceil = top of the opening on screen (max Y = lower world ceiling)
|
|
@@ -806,12 +1001,29 @@ module Doom
|
|
|
806
1001
|
closed_door = back_sector.ceiling_height <= sector.floor_height ||
|
|
807
1002
|
back_sector.floor_height >= sector.ceiling_height
|
|
808
1003
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
#
|
|
812
|
-
|
|
813
|
-
|
|
1004
|
+
both_sky = sector.ceiling_texture == 'F_SKY1' && back_sector.ceiling_texture == 'F_SKY1'
|
|
1005
|
+
|
|
1006
|
+
# Determine whether to mark ceiling/floor visplanes.
|
|
1007
|
+
# Match Chocolate Doom: only mark when properties differ, plus force
|
|
1008
|
+
# for closed doors. The background fill covers same-property gaps.
|
|
1009
|
+
# Chocolate Doom sky hack: "worldtop = worldhigh" makes the front
|
|
1010
|
+
# ceiling equal to the back ceiling when both are sky. This prevents
|
|
1011
|
+
# the low sky ceiling from clipping walls in adjacent sectors.
|
|
1012
|
+
effective_front_ceil = both_sky ? back_sector.ceiling_height : sector.ceiling_height
|
|
1013
|
+
|
|
1014
|
+
if closed_door
|
|
1015
|
+
should_mark_ceiling = true
|
|
1016
|
+
should_mark_floor = true
|
|
1017
|
+
else
|
|
1018
|
+
should_mark_ceiling = effective_front_ceil != back_sector.ceiling_height ||
|
|
1019
|
+
sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1020
|
+
sector.light_level != back_sector.light_level
|
|
1021
|
+
should_mark_floor = sector.floor_height != back_sector.floor_height ||
|
|
1022
|
+
sector.floor_texture != back_sector.floor_texture ||
|
|
814
1023
|
sector.light_level != back_sector.light_level
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
# Mark ceiling visplane
|
|
815
1027
|
if @current_ceiling_plane && should_mark_ceiling
|
|
816
1028
|
mark_top = @ceiling_clip[x] + 1
|
|
817
1029
|
mark_bottom = ceil_y - 1
|
|
@@ -821,12 +1033,7 @@ module Doom
|
|
|
821
1033
|
end
|
|
822
1034
|
end
|
|
823
1035
|
|
|
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
|
|
1036
|
+
# Mark floor visplane
|
|
830
1037
|
if @current_floor_plane && should_mark_floor
|
|
831
1038
|
mark_top = floor_y + 1
|
|
832
1039
|
mark_bottom = @floor_clip[x] - 1
|
|
@@ -836,11 +1043,8 @@ module Doom
|
|
|
836
1043
|
end
|
|
837
1044
|
end
|
|
838
1045
|
|
|
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)
|
|
1046
|
+
# Upper wall (ceiling step down) - skip if both sectors have sky
|
|
1047
|
+
if !both_sky && sector.ceiling_height > back_sector.ceiling_height
|
|
844
1048
|
if linedef.upper_unpegged?
|
|
845
1049
|
upper_tex_y = sidedef.y_offset
|
|
846
1050
|
else
|
|
@@ -848,70 +1052,68 @@ module Doom
|
|
|
848
1052
|
tex_height = texture ? texture.height : 128
|
|
849
1053
|
upper_tex_y = sidedef.y_offset + back_sector.ceiling_height - sector.ceiling_height + tex_height
|
|
850
1054
|
end
|
|
851
|
-
draw_wall_column_ex(x, ceil_y, back_ceil_y
|
|
1055
|
+
draw_wall_column_ex(x, ceil_y, back_ceil_y, sidedef.upper_texture, dist,
|
|
852
1056
|
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
1057
|
end
|
|
855
1058
|
|
|
856
1059
|
# Lower wall (floor step up)
|
|
857
1060
|
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
1061
|
if linedef.lower_unpegged?
|
|
862
1062
|
lower_tex_y = sidedef.y_offset + sector.ceiling_height - back_sector.floor_height
|
|
863
1063
|
else
|
|
864
1064
|
lower_tex_y = sidedef.y_offset
|
|
865
1065
|
end
|
|
866
|
-
draw_wall_column_ex(x, back_floor_y
|
|
1066
|
+
draw_wall_column_ex(x, back_floor_y, floor_y, sidedef.lower_texture, dist,
|
|
867
1067
|
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
1068
|
end
|
|
870
1069
|
|
|
871
|
-
# Update clip bounds
|
|
1070
|
+
# Update clip bounds
|
|
872
1071
|
if closed_door
|
|
873
|
-
# Closed door - fully occlude this column
|
|
874
1072
|
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
875
1073
|
@ceiling_clip[x] = SCREEN_HEIGHT
|
|
876
1074
|
@floor_clip[x] = -1
|
|
877
1075
|
else
|
|
878
|
-
# Ceiling clip
|
|
879
|
-
if
|
|
880
|
-
# Upper wall drawn - clip ceiling to back ceiling
|
|
1076
|
+
# Ceiling clip (uses effective_front_ceil for sky hack)
|
|
1077
|
+
if effective_front_ceil > back_sector.ceiling_height
|
|
881
1078
|
@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;
|
|
1079
|
+
elsif effective_front_ceil < back_sector.ceiling_height
|
|
1080
|
+
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1081
|
+
elsif sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1082
|
+
sector.light_level != back_sector.light_level
|
|
888
1083
|
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
889
1084
|
end
|
|
890
1085
|
|
|
891
|
-
# Floor clip
|
|
1086
|
+
# Floor clip
|
|
892
1087
|
if sector.floor_height < back_sector.floor_height
|
|
893
|
-
# Lower wall drawn - clip floor to back floor
|
|
894
1088
|
@floor_clip[x] = [back_floor_y, @floor_clip[x]].min
|
|
895
1089
|
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;
|
|
1090
|
+
# Chocolate Doom: else if (markfloor) floorclip = yh + 1
|
|
1091
|
+
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1092
|
+
elsif sector.floor_texture != back_sector.floor_texture ||
|
|
1093
|
+
sector.light_level != back_sector.light_level
|
|
901
1094
|
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
902
1095
|
end
|
|
903
1096
|
end
|
|
904
1097
|
else
|
|
905
1098
|
# One-sided (solid) wall
|
|
906
1099
|
# Mark ceiling visplane (from previous clip to wall's ceiling)
|
|
907
|
-
|
|
908
|
-
|
|
1100
|
+
# Clamp to floor_clip to prevent ceiling bleeding through portal openings
|
|
1101
|
+
if @current_ceiling_plane
|
|
1102
|
+
mark_top = @ceiling_clip[x] + 1
|
|
1103
|
+
mark_bottom = [ceil_y - 1, @floor_clip[x] - 1].min
|
|
1104
|
+
if mark_top <= mark_bottom
|
|
1105
|
+
@current_ceiling_plane.mark(x, mark_top, mark_bottom)
|
|
1106
|
+
end
|
|
909
1107
|
end
|
|
910
1108
|
|
|
911
1109
|
# Mark floor visplane (from wall's floor to previous floor clip)
|
|
912
|
-
#
|
|
913
|
-
if @current_floor_plane
|
|
914
|
-
|
|
1110
|
+
# Clamp to ceiling_clip to prevent floor bleeding through portal openings
|
|
1111
|
+
if @current_floor_plane
|
|
1112
|
+
mark_top = [floor_y + 1, @ceiling_clip[x] + 1].max
|
|
1113
|
+
mark_bottom = @floor_clip[x] - 1
|
|
1114
|
+
if mark_top <= mark_bottom
|
|
1115
|
+
@current_floor_plane.mark(x, mark_top, mark_bottom)
|
|
1116
|
+
end
|
|
915
1117
|
end
|
|
916
1118
|
|
|
917
1119
|
# Draw wall (from clipped ceiling to clipped floor)
|
|
@@ -971,17 +1173,17 @@ module Doom
|
|
|
971
1173
|
y2 = [y2, clip_bottom].min
|
|
972
1174
|
return if y1 > y2
|
|
973
1175
|
|
|
974
|
-
texture = @textures[texture_name]
|
|
1176
|
+
texture = @textures[anim_texture(texture_name)]
|
|
975
1177
|
return unless texture
|
|
976
1178
|
|
|
977
1179
|
light = calculate_light(light_level, dist)
|
|
978
1180
|
cmap = @colormap.maps[light]
|
|
979
1181
|
framebuffer = @framebuffer
|
|
980
|
-
|
|
981
|
-
|
|
1182
|
+
tex_width = texture.width
|
|
1183
|
+
tex_height = texture.height
|
|
982
1184
|
|
|
983
|
-
# Texture X coordinate (wrap around texture width
|
|
984
|
-
tex_x = tex_col.to_i
|
|
1185
|
+
# Texture X coordinate (wrap around texture width)
|
|
1186
|
+
tex_x = tex_col.to_i % tex_width
|
|
985
1187
|
|
|
986
1188
|
# Get the column of pixels
|
|
987
1189
|
column = texture.column_pixels(tex_x)
|
|
@@ -1004,7 +1206,7 @@ module Doom
|
|
|
1004
1206
|
y = y1
|
|
1005
1207
|
while y <= y2
|
|
1006
1208
|
screen_offset = y - y1
|
|
1007
|
-
tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i
|
|
1209
|
+
tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i % tex_height
|
|
1008
1210
|
|
|
1009
1211
|
color = column[tex_y]
|
|
1010
1212
|
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
@@ -1081,7 +1283,10 @@ module Doom
|
|
|
1081
1283
|
# Collect visible sprites with their distances
|
|
1082
1284
|
visible_sprites = []
|
|
1083
1285
|
|
|
1084
|
-
@map.things.
|
|
1286
|
+
@map.things.each_with_index do |thing, thing_idx|
|
|
1287
|
+
# Skip picked-up items
|
|
1288
|
+
next if @hidden_things && @hidden_things[thing_idx]
|
|
1289
|
+
|
|
1085
1290
|
# Check if we have a sprite for this thing type
|
|
1086
1291
|
next unless @sprites.prefix_for(thing.type)
|
|
1087
1292
|
|
|
@@ -1099,8 +1304,20 @@ module Doom
|
|
|
1099
1304
|
dy = thing.y - @player_y
|
|
1100
1305
|
angle_to_thing = Math.atan2(dy, dx)
|
|
1101
1306
|
|
|
1102
|
-
# Get
|
|
1103
|
-
|
|
1307
|
+
# Get sprite: death frame > walking frame > idle frame
|
|
1308
|
+
if @combat && @combat.dead?(thing_idx)
|
|
1309
|
+
sprite = @combat.death_sprite(thing_idx, thing.type, angle_to_thing, thing.angle)
|
|
1310
|
+
elsif @monster_ai
|
|
1311
|
+
mon = @monster_ai.monsters.find { |m| m.thing_idx == thing_idx }
|
|
1312
|
+
if mon && mon.active
|
|
1313
|
+
# Walking animation: cycle through frames A-D based on leveltime
|
|
1314
|
+
walk_frame = %w[A B C D][@leveltime / 4 % 4]
|
|
1315
|
+
sprite = @sprites.get_frame(thing.type, walk_frame, angle_to_thing, thing.angle)
|
|
1316
|
+
end
|
|
1317
|
+
sprite ||= @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
|
|
1318
|
+
else
|
|
1319
|
+
sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
|
|
1320
|
+
end
|
|
1104
1321
|
next unless sprite
|
|
1105
1322
|
|
|
1106
1323
|
# Project to screen X
|
|
@@ -1121,6 +1338,45 @@ module Doom
|
|
|
1121
1338
|
visible_sprites.each do |vs|
|
|
1122
1339
|
draw_sprite(vs)
|
|
1123
1340
|
end
|
|
1341
|
+
|
|
1342
|
+
# Draw projectiles and explosions
|
|
1343
|
+
render_projectiles if @combat
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
def render_projectiles
|
|
1347
|
+
# Render rockets in flight
|
|
1348
|
+
@combat.projectiles.each do |proj|
|
|
1349
|
+
view_x, view_y = transform_point(proj.x, proj.y)
|
|
1350
|
+
next if view_y <= 0
|
|
1351
|
+
|
|
1352
|
+
# Rocket has rotations 1-8 based on travel direction relative to viewer
|
|
1353
|
+
rocket_angle = Math.atan2(proj.dy, proj.dx)
|
|
1354
|
+
viewer_angle = Math.atan2(proj.y - @player_y, proj.x - @player_x)
|
|
1355
|
+
angle_diff = (viewer_angle - rocket_angle) % (2 * Math::PI)
|
|
1356
|
+
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
1357
|
+
rocket_sprite = @sprites.send(:load_sprite_frame, 'MISL', 'A', rotation)
|
|
1358
|
+
next unless rocket_sprite
|
|
1359
|
+
|
|
1360
|
+
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1361
|
+
thing_stub = Map::Thing.new(proj.x, proj.y, 0, 0, 0)
|
|
1362
|
+
visible = VisibleSprite.new(thing_stub, rocket_sprite, view_x, view_y, view_y, screen_x)
|
|
1363
|
+
draw_sprite(visible)
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
# Render explosions (frames B, C, D - all rotation 0)
|
|
1367
|
+
@combat.explosions.each do |expl|
|
|
1368
|
+
view_x, view_y = transform_point(expl[:x], expl[:y])
|
|
1369
|
+
next if view_y <= 0
|
|
1370
|
+
elapsed = (@combat.instance_variable_get(:@tic) - expl[:tic])
|
|
1371
|
+
frame_idx = (elapsed / 4).clamp(0, 2)
|
|
1372
|
+
frame_letter = %w[B C D][frame_idx]
|
|
1373
|
+
expl_sprite = @sprites.send(:load_sprite_frame, 'MISL', frame_letter, 0)
|
|
1374
|
+
next unless expl_sprite
|
|
1375
|
+
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1376
|
+
thing_stub = Map::Thing.new(expl[:x], expl[:y], 0, 0, 0)
|
|
1377
|
+
visible = VisibleSprite.new(thing_stub, expl_sprite, view_x, view_y, view_y, screen_x)
|
|
1378
|
+
draw_sprite(visible)
|
|
1379
|
+
end
|
|
1124
1380
|
end
|
|
1125
1381
|
|
|
1126
1382
|
def draw_sprite(vs)
|