doom 0.3.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 +313 -0
- data/lib/doom/game/sector_actions.rb +162 -0
- data/lib/doom/game/sector_effects.rb +179 -0
- data/lib/doom/platform/gosu_window.rb +706 -59
- data/lib/doom/render/renderer.rb +397 -136
- data/lib/doom/render/status_bar.rb +218 -0
- data/lib/doom/render/weapon_renderer.rb +99 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/colormap.rb +0 -6
- data/lib/doom/wad/flat.rb +0 -21
- data/lib/doom/wad/hud_graphics.rb +257 -0
- data/lib/doom/wad/sprite.rb +95 -22
- data/lib/doom/wad/texture.rb +23 -22
- data/lib/doom/wad_downloader.rb +2 -2
- data/lib/doom.rb +27 -2
- metadata +12 -6
data/lib/doom/render/renderer.rb
CHANGED
|
@@ -18,7 +18,12 @@ module Doom
|
|
|
18
18
|
# Matches Chocolate Doom's drawseg_t structure
|
|
19
19
|
Drawseg = Struct.new(:x1, :x2, :scale1, :scale2,
|
|
20
20
|
:silhouette, :bsilheight, :tsilheight,
|
|
21
|
-
:sprtopclip, :sprbottomclip
|
|
21
|
+
:sprtopclip, :sprbottomclip,
|
|
22
|
+
:curline) # seg for point-on-side test
|
|
23
|
+
|
|
24
|
+
# VisibleSprite stores sprite data for sorting and rendering
|
|
25
|
+
# Struct is faster than Hash for fixed-field data
|
|
26
|
+
VisibleSprite = Struct.new(:thing, :sprite, :view_x, :view_y, :dist, :screen_x)
|
|
22
27
|
|
|
23
28
|
# Visplane stores floor/ceiling rendering info for a sector
|
|
24
29
|
# Matches Chocolate Doom's visplane_t structure from r_plane.c
|
|
@@ -48,7 +53,7 @@ module Doom
|
|
|
48
53
|
class Renderer
|
|
49
54
|
attr_reader :framebuffer
|
|
50
55
|
|
|
51
|
-
def initialize(wad, map, textures, palette, colormap, flats, sprites = nil)
|
|
56
|
+
def initialize(wad, map, textures, palette, colormap, flats, sprites = nil, animations = nil)
|
|
52
57
|
@wad = wad
|
|
53
58
|
@map = map
|
|
54
59
|
@textures = textures
|
|
@@ -56,6 +61,9 @@ module Doom
|
|
|
56
61
|
@colormap = colormap
|
|
57
62
|
@flats = flats.to_h { |f| [f.name, f] }
|
|
58
63
|
@sprites = sprites
|
|
64
|
+
@animations = animations
|
|
65
|
+
@hidden_things = nil
|
|
66
|
+
@combat = nil
|
|
59
67
|
|
|
60
68
|
@framebuffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0)
|
|
61
69
|
|
|
@@ -67,6 +75,17 @@ module Doom
|
|
|
67
75
|
# Projection constant - distance to projection plane
|
|
68
76
|
@projection = HALF_WIDTH / Math.tan(FOV * Math::PI / 360.0)
|
|
69
77
|
|
|
78
|
+
# Precomputed column data (cached based on player angle)
|
|
79
|
+
@column_cos = Array.new(SCREEN_WIDTH)
|
|
80
|
+
@column_sin = Array.new(SCREEN_WIDTH)
|
|
81
|
+
@cached_player_angle = nil
|
|
82
|
+
|
|
83
|
+
# Column distance scale is constant (doesn't depend on player angle)
|
|
84
|
+
@column_distscale = Array.new(SCREEN_WIDTH) do |x|
|
|
85
|
+
dx = x - HALF_WIDTH
|
|
86
|
+
Math.sqrt(dx * dx + @projection * @projection) / @projection
|
|
87
|
+
end
|
|
88
|
+
|
|
70
89
|
# Clipping arrays
|
|
71
90
|
@ceiling_clip = Array.new(SCREEN_WIDTH, -1)
|
|
72
91
|
@floor_clip = Array.new(SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
@@ -77,9 +96,103 @@ module Doom
|
|
|
77
96
|
|
|
78
97
|
# Wall depth array - tracks distance to nearest wall at each column
|
|
79
98
|
@wall_depth = Array.new(SCREEN_WIDTH, Float::INFINITY)
|
|
99
|
+
@sprite_wall_depth = Array.new(SCREEN_WIDTH, Float::INFINITY)
|
|
100
|
+
|
|
101
|
+
# Preallocated y_slope arrays for floor/ceiling rendering (avoids per-frame allocation)
|
|
102
|
+
@y_slope_ceil = Array.new(HALF_HEIGHT + 1, 0.0)
|
|
103
|
+
@y_slope_floor = Array.new(HALF_HEIGHT + 1, 0.0)
|
|
80
104
|
end
|
|
81
105
|
|
|
82
|
-
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
|
|
83
196
|
|
|
84
197
|
def set_player(x, y, z, angle)
|
|
85
198
|
@player_x = x.to_f
|
|
@@ -111,11 +224,15 @@ module Doom
|
|
|
111
224
|
# Precompute column angles for floor/ceiling rendering
|
|
112
225
|
precompute_column_data
|
|
113
226
|
|
|
114
|
-
# 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.
|
|
115
231
|
draw_floor_ceiling_background
|
|
116
232
|
|
|
117
233
|
# Initialize visplanes for tracking visible floor/ceiling spans
|
|
118
234
|
@visplanes = []
|
|
235
|
+
@visplane_hash = {} # Hash for O(1) lookup by (height, texture, light_level, is_ceiling)
|
|
119
236
|
|
|
120
237
|
# Initialize drawsegs for sprite clipping
|
|
121
238
|
@drawsegs = []
|
|
@@ -126,27 +243,26 @@ module Doom
|
|
|
126
243
|
# Draw visplanes for sectors different from background
|
|
127
244
|
draw_all_visplanes
|
|
128
245
|
|
|
129
|
-
# Save wall clip arrays for sprite clipping
|
|
130
|
-
@sprite_ceiling_clip
|
|
131
|
-
@sprite_floor_clip
|
|
132
|
-
@sprite_wall_depth
|
|
246
|
+
# Save wall clip arrays for sprite clipping (reuse preallocated arrays)
|
|
247
|
+
@sprite_ceiling_clip.replace(@ceiling_clip)
|
|
248
|
+
@sprite_floor_clip.replace(@floor_clip)
|
|
249
|
+
@sprite_wall_depth.replace(@wall_depth)
|
|
133
250
|
|
|
134
251
|
# Render sprites
|
|
135
252
|
render_sprites if @sprites
|
|
136
253
|
end
|
|
137
254
|
|
|
138
255
|
# Precompute column-based data for floor/ceiling rendering (R_InitLightTables-like)
|
|
256
|
+
# Cached: only recomputes sin/cos when player angle changes
|
|
139
257
|
def precompute_column_data
|
|
140
|
-
@
|
|
141
|
-
|
|
142
|
-
@
|
|
258
|
+
return if @cached_player_angle == @player_angle
|
|
259
|
+
|
|
260
|
+
@cached_player_angle = @player_angle
|
|
143
261
|
|
|
144
262
|
SCREEN_WIDTH.times do |x|
|
|
145
|
-
|
|
146
|
-
column_angle = @player_angle - Math.atan2(dx, @projection)
|
|
263
|
+
column_angle = @player_angle - Math.atan2(x - HALF_WIDTH, @projection)
|
|
147
264
|
@column_cos[x] = Math.cos(column_angle)
|
|
148
265
|
@column_sin[x] = Math.sin(column_angle)
|
|
149
|
-
@column_distscale[x] = Math.sqrt(dx * dx + @projection * @projection) / @projection
|
|
150
266
|
end
|
|
151
267
|
end
|
|
152
268
|
|
|
@@ -223,7 +339,7 @@ module Doom
|
|
|
223
339
|
def draw_span(plane, y, x1, x2)
|
|
224
340
|
return if x1.nil? || x1 > x2 || y < 0 || y >= SCREEN_HEIGHT
|
|
225
341
|
|
|
226
|
-
flat = @flats[plane.texture]
|
|
342
|
+
flat = @flats[anim_flat(plane.texture)]
|
|
227
343
|
return unless flat
|
|
228
344
|
|
|
229
345
|
# Distance from horizon (y=100 for 200-high screen)
|
|
@@ -249,6 +365,7 @@ module Doom
|
|
|
249
365
|
player_x = @player_x
|
|
250
366
|
neg_player_y = -@player_y
|
|
251
367
|
row_offset = y * SCREEN_WIDTH
|
|
368
|
+
flat_pixels = flat.pixels
|
|
252
369
|
|
|
253
370
|
# Clamp to screen bounds
|
|
254
371
|
x1 = 0 if x1 < 0
|
|
@@ -260,13 +377,22 @@ module Doom
|
|
|
260
377
|
ray_dist = perp_dist * column_distscale[x]
|
|
261
378
|
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
262
379
|
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
263
|
-
color =
|
|
380
|
+
color = flat_pixels[tex_y * 64 + tex_x]
|
|
264
381
|
framebuffer[row_offset + x] = cmap[color]
|
|
265
382
|
x += 1
|
|
266
383
|
end
|
|
267
384
|
end
|
|
268
385
|
|
|
269
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
|
+
|
|
270
396
|
def draw_sky_plane(plane)
|
|
271
397
|
sky_texture = @textures['SKY1']
|
|
272
398
|
return unless sky_texture
|
|
@@ -290,31 +416,31 @@ module Doom
|
|
|
290
416
|
y1 = 0 if y1 < 0
|
|
291
417
|
y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT
|
|
292
418
|
|
|
293
|
-
# Sky X
|
|
419
|
+
# Sky X: 4 repetitions per 360 degrees (matching ANGLETOSKYSHIFT=22)
|
|
294
420
|
column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
|
|
295
|
-
sky_x = (
|
|
421
|
+
sky_x = (column_angle * SKY_XSCALE).to_i % sky_width
|
|
296
422
|
column = sky_texture.column_pixels(sky_x)
|
|
297
423
|
next unless column
|
|
298
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)
|
|
299
427
|
(y1..y2).each do |y|
|
|
300
|
-
|
|
428
|
+
tex_y = (SKY_TEXTUREMID + (y - HALF_HEIGHT) * SKY_YSCALE).to_i % sky_height
|
|
429
|
+
color = column[tex_y]
|
|
301
430
|
framebuffer[y * SCREEN_WIDTH + x] = color
|
|
302
431
|
end
|
|
303
432
|
end
|
|
304
433
|
end
|
|
305
434
|
|
|
306
435
|
def find_or_create_visplane(sector, height, texture, light_level, is_ceiling)
|
|
307
|
-
#
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
vp.texture == texture &&
|
|
311
|
-
vp.light_level == light_level &&
|
|
312
|
-
vp.is_ceiling == is_ceiling
|
|
313
|
-
end
|
|
436
|
+
# O(1) hash lookup instead of O(n) linear search
|
|
437
|
+
key = [height, texture, light_level, is_ceiling]
|
|
438
|
+
plane = @visplane_hash[key]
|
|
314
439
|
|
|
315
440
|
unless plane
|
|
316
441
|
plane = Visplane.new(sector, height, texture, light_level, is_ceiling)
|
|
317
442
|
@visplanes << plane
|
|
443
|
+
@visplane_hash[key] = plane
|
|
318
444
|
end
|
|
319
445
|
|
|
320
446
|
plane
|
|
@@ -371,6 +497,11 @@ module Doom
|
|
|
371
497
|
new_plane.minx = start_x
|
|
372
498
|
new_plane.maxx = stop_x
|
|
373
499
|
@visplanes << new_plane
|
|
500
|
+
|
|
501
|
+
# Update hash to point to the new plane (for subsequent lookups)
|
|
502
|
+
key = [plane.height, plane.texture, plane.light_level, plane.is_ceiling]
|
|
503
|
+
@visplane_hash[key] = new_plane
|
|
504
|
+
|
|
374
505
|
new_plane
|
|
375
506
|
end
|
|
376
507
|
|
|
@@ -387,16 +518,18 @@ module Doom
|
|
|
387
518
|
|
|
388
519
|
ceil_height = (default_sector.ceiling_height - @player_z).abs
|
|
389
520
|
floor_height = (default_sector.floor_height - @player_z).abs
|
|
390
|
-
ceil_flat = @flats[default_sector.ceiling_texture]
|
|
391
|
-
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
|
|
392
525
|
is_sky = default_sector.ceiling_texture == 'F_SKY1'
|
|
393
526
|
sky_texture = is_sky ? @textures['SKY1'] : nil
|
|
394
527
|
light_level = default_sector.light_level
|
|
395
528
|
colormap_maps = @colormap.maps
|
|
396
529
|
|
|
397
|
-
#
|
|
398
|
-
y_slope_ceil =
|
|
399
|
-
y_slope_floor =
|
|
530
|
+
# Compute y_slope for each row (perpendicular distance) - reuse preallocated arrays
|
|
531
|
+
y_slope_ceil = @y_slope_ceil
|
|
532
|
+
y_slope_floor = @y_slope_floor
|
|
400
533
|
(1..HALF_HEIGHT).each do |dy|
|
|
401
534
|
y_slope_ceil[dy] = ceil_height * projection / dy.to_f
|
|
402
535
|
y_slope_floor[dy] = floor_height * projection / dy.to_f
|
|
@@ -414,14 +547,14 @@ module Doom
|
|
|
414
547
|
row_offset = y * SCREEN_WIDTH
|
|
415
548
|
|
|
416
549
|
if is_sky && sky_texture
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
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
|
|
420
553
|
x = 0
|
|
421
554
|
while x < SCREEN_WIDTH
|
|
422
555
|
column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
|
|
423
|
-
sky_x = (
|
|
424
|
-
color = sky_texture.column_pixels(sky_x)[sky_y]
|
|
556
|
+
sky_x = (column_angle * SKY_XSCALE).to_i % sky_w
|
|
557
|
+
color = sky_texture.column_pixels(sky_x)[sky_y]
|
|
425
558
|
framebuffer[row_offset + x] = color
|
|
426
559
|
x += 1
|
|
427
560
|
end
|
|
@@ -431,7 +564,7 @@ module Doom
|
|
|
431
564
|
ray_dist = perp_dist * column_distscale[x]
|
|
432
565
|
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
433
566
|
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
434
|
-
color =
|
|
567
|
+
color = ceil_pixels[tex_y * 64 + tex_x]
|
|
435
568
|
framebuffer[row_offset + x] = cmap[color]
|
|
436
569
|
x += 1
|
|
437
570
|
end
|
|
@@ -458,7 +591,7 @@ module Doom
|
|
|
458
591
|
ray_dist = perp_dist * column_distscale[x]
|
|
459
592
|
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
460
593
|
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
461
|
-
color =
|
|
594
|
+
color = floor_pixels[tex_y * 64 + tex_x]
|
|
462
595
|
framebuffer[row_offset + x] = cmap[color]
|
|
463
596
|
x += 1
|
|
464
597
|
end
|
|
@@ -471,6 +604,15 @@ module Doom
|
|
|
471
604
|
|
|
472
605
|
private
|
|
473
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
|
+
|
|
474
616
|
def clear_framebuffer
|
|
475
617
|
@framebuffer.fill(0)
|
|
476
618
|
end
|
|
@@ -492,13 +634,58 @@ module Doom
|
|
|
492
634
|
|
|
493
635
|
if side == 0
|
|
494
636
|
render_bsp_node(node.child_right)
|
|
495
|
-
|
|
637
|
+
back_bbox = node.bbox_left
|
|
638
|
+
render_bsp_node(node.child_left) if check_bbox(back_bbox)
|
|
496
639
|
else
|
|
497
640
|
render_bsp_node(node.child_left)
|
|
498
|
-
|
|
641
|
+
back_bbox = node.bbox_right
|
|
642
|
+
render_bsp_node(node.child_right) if check_bbox(back_bbox)
|
|
499
643
|
end
|
|
500
644
|
end
|
|
501
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
|
+
|
|
502
689
|
def point_on_side(x, y, node)
|
|
503
690
|
dx = x - node.x
|
|
504
691
|
dy = y - node.y
|
|
@@ -641,11 +828,67 @@ module Doom
|
|
|
641
828
|
[x, y]
|
|
642
829
|
end
|
|
643
830
|
|
|
831
|
+
# Determine which side of a seg a point is on (R_PointOnSegSide from Chocolate Doom)
|
|
832
|
+
# Returns true if point is on back side, false if on front side
|
|
833
|
+
def point_on_seg_side(x, y, seg)
|
|
834
|
+
v1 = @map.vertices[seg.v1]
|
|
835
|
+
v2 = @map.vertices[seg.v2]
|
|
836
|
+
|
|
837
|
+
lx = v1.x
|
|
838
|
+
ly = v1.y
|
|
839
|
+
ldx = v2.x - lx
|
|
840
|
+
ldy = v2.y - ly
|
|
841
|
+
|
|
842
|
+
# Handle axis-aligned lines
|
|
843
|
+
if ldx == 0
|
|
844
|
+
return x <= lx ? ldy > 0 : ldy < 0
|
|
845
|
+
end
|
|
846
|
+
if ldy == 0
|
|
847
|
+
return y <= ly ? ldx < 0 : ldx > 0
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
dx = x - lx
|
|
851
|
+
dy = y - ly
|
|
852
|
+
|
|
853
|
+
# Cross product to determine side
|
|
854
|
+
left = ldy * dx
|
|
855
|
+
right = dy * ldx
|
|
856
|
+
|
|
857
|
+
right >= left
|
|
858
|
+
end
|
|
859
|
+
|
|
644
860
|
def draw_seg_range(x1, x2, sx1, sx2, dist1, dist2, sector, back_sector, sidedef, linedef, seg, seg_length)
|
|
645
|
-
# Get seg vertices in world space for texture coordinate calculation
|
|
646
861
|
seg_v1 = @map.vertices[seg.v1]
|
|
647
862
|
seg_v2 = @map.vertices[seg.v2]
|
|
648
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
|
+
|
|
649
892
|
# Calculate scales for drawseg (scale = projection / distance)
|
|
650
893
|
scale1 = dist1 > 0 ? @projection / dist1 : Float::INFINITY
|
|
651
894
|
scale2 = dist2 > 0 ? @projection / dist2 : Float::INFINITY
|
|
@@ -691,11 +934,10 @@ module Doom
|
|
|
691
934
|
(x1..x2).each do |x|
|
|
692
935
|
next if @ceiling_clip[x] >= @floor_clip[x] - 1
|
|
693
936
|
|
|
694
|
-
# Screen-space interpolation
|
|
937
|
+
# Screen-space interpolation for distance
|
|
695
938
|
t = sx2 != sx1 ? (x - sx1) / (sx2 - sx1) : 0
|
|
696
939
|
t = t.clamp(0.0, 1.0)
|
|
697
940
|
|
|
698
|
-
# Perspective-correct interpolation for distance
|
|
699
941
|
if dist1 > 0 && dist2 > 0
|
|
700
942
|
inv_dist = (1.0 - t) / dist1 + t / dist2
|
|
701
943
|
dist = 1.0 / inv_dist
|
|
@@ -703,18 +945,15 @@ module Doom
|
|
|
703
945
|
dist = dist1 > 0 ? dist1 : dist2
|
|
704
946
|
end
|
|
705
947
|
|
|
706
|
-
#
|
|
707
|
-
#
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
# Perspective-correct interpolation: interpolate tex/z, then multiply by z
|
|
712
|
-
if dist1 > 0 && dist2 > 0
|
|
713
|
-
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
|
|
714
953
|
else
|
|
715
|
-
|
|
954
|
+
s = (tex_e * x + tex_f) / denom
|
|
716
955
|
end
|
|
717
|
-
tex_col =
|
|
956
|
+
tex_col = (tex_offset + s * seg_length).to_i
|
|
718
957
|
|
|
719
958
|
# Skip if too close
|
|
720
959
|
next if dist < 1
|
|
@@ -727,7 +966,10 @@ module Doom
|
|
|
727
966
|
front_ceil = sector.ceiling_height - @player_z
|
|
728
967
|
|
|
729
968
|
# Project to screen Y (Y increases downward on screen)
|
|
730
|
-
|
|
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
|
|
731
973
|
front_floor_y = (HALF_HEIGHT - front_floor * scale).to_i
|
|
732
974
|
|
|
733
975
|
# Clamp to current clip bounds
|
|
@@ -739,8 +981,11 @@ module Doom
|
|
|
739
981
|
back_floor = back_sector.floor_height - @player_z
|
|
740
982
|
back_ceil = back_sector.ceiling_height - @player_z
|
|
741
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)
|
|
742
987
|
back_ceil_y = (HALF_HEIGHT - back_ceil * scale).to_i
|
|
743
|
-
back_floor_y = (HALF_HEIGHT - back_floor * scale).
|
|
988
|
+
back_floor_y = (HALF_HEIGHT - back_floor * scale).ceil
|
|
744
989
|
|
|
745
990
|
# Determine visible ceiling/floor boundaries (the opening between sectors)
|
|
746
991
|
# high_ceil = top of the opening on screen (max Y = lower world ceiling)
|
|
@@ -748,12 +993,35 @@ module Doom
|
|
|
748
993
|
high_ceil = [ceil_y, back_ceil_y].max
|
|
749
994
|
low_floor = [floor_y, back_floor_y].min
|
|
750
995
|
|
|
751
|
-
#
|
|
752
|
-
# Matches Chocolate Doom:
|
|
753
|
-
#
|
|
754
|
-
|
|
755
|
-
|
|
996
|
+
# Check for closed door (no opening between sectors)
|
|
997
|
+
# Matches Chocolate Doom: backsector->ceilingheight <= frontsector->floorheight
|
|
998
|
+
# || backsector->floorheight >= frontsector->ceilingheight
|
|
999
|
+
closed_door = back_sector.ceiling_height <= sector.floor_height ||
|
|
1000
|
+
back_sector.floor_height >= sector.ceiling_height
|
|
1001
|
+
|
|
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 ||
|
|
756
1021
|
sector.light_level != back_sector.light_level
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
# Mark ceiling visplane
|
|
757
1025
|
if @current_ceiling_plane && should_mark_ceiling
|
|
758
1026
|
mark_top = @ceiling_clip[x] + 1
|
|
759
1027
|
mark_bottom = ceil_y - 1
|
|
@@ -763,12 +1031,7 @@ module Doom
|
|
|
763
1031
|
end
|
|
764
1032
|
end
|
|
765
1033
|
|
|
766
|
-
# Mark floor visplane
|
|
767
|
-
# Matches Chocolate Doom: mark from yh+1 (front floor) to floorclip-1
|
|
768
|
-
# (yh is clamped to floorclip-1, so we use floor_y which is already clamped)
|
|
769
|
-
should_mark_floor = sector.floor_height != back_sector.floor_height ||
|
|
770
|
-
sector.floor_texture != back_sector.floor_texture ||
|
|
771
|
-
sector.light_level != back_sector.light_level
|
|
1034
|
+
# Mark floor visplane
|
|
772
1035
|
if @current_floor_plane && should_mark_floor
|
|
773
1036
|
mark_top = floor_y + 1
|
|
774
1037
|
mark_bottom = @floor_clip[x] - 1
|
|
@@ -778,11 +1041,8 @@ module Doom
|
|
|
778
1041
|
end
|
|
779
1042
|
end
|
|
780
1043
|
|
|
781
|
-
# Upper wall (ceiling step down)
|
|
782
|
-
if sector.ceiling_height > back_sector.ceiling_height
|
|
783
|
-
# Upper texture Y offset depends on DONTPEGTOP flag
|
|
784
|
-
# With DONTPEGTOP: texture top aligns with front ceiling
|
|
785
|
-
# 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
|
|
786
1046
|
if linedef.upper_unpegged?
|
|
787
1047
|
upper_tex_y = sidedef.y_offset
|
|
788
1048
|
else
|
|
@@ -790,63 +1050,68 @@ module Doom
|
|
|
790
1050
|
tex_height = texture ? texture.height : 128
|
|
791
1051
|
upper_tex_y = sidedef.y_offset + back_sector.ceiling_height - sector.ceiling_height + tex_height
|
|
792
1052
|
end
|
|
793
|
-
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,
|
|
794
1054
|
sector.light_level, tex_col, upper_tex_y, scale, sector.ceiling_height, back_sector.ceiling_height)
|
|
795
|
-
# Note: Upper walls don't fully occlude - sprites can be visible through openings
|
|
796
1055
|
end
|
|
797
1056
|
|
|
798
1057
|
# Lower wall (floor step up)
|
|
799
1058
|
if sector.floor_height < back_sector.floor_height
|
|
800
|
-
# Lower texture Y offset depends on DONTPEGBOTTOM flag
|
|
801
|
-
# With DONTPEGBOTTOM: texture bottom aligns with lower floor
|
|
802
|
-
# Without: texture top aligns with higher floor
|
|
803
1059
|
if linedef.lower_unpegged?
|
|
804
1060
|
lower_tex_y = sidedef.y_offset + sector.ceiling_height - back_sector.floor_height
|
|
805
1061
|
else
|
|
806
1062
|
lower_tex_y = sidedef.y_offset
|
|
807
1063
|
end
|
|
808
|
-
draw_wall_column_ex(x, back_floor_y
|
|
1064
|
+
draw_wall_column_ex(x, back_floor_y, floor_y, sidedef.lower_texture, dist,
|
|
809
1065
|
sector.light_level, tex_col, lower_tex_y, scale, back_sector.floor_height, sector.floor_height)
|
|
810
|
-
# Note: Lower walls don't fully occlude - sprites can be visible through openings
|
|
811
1066
|
end
|
|
812
1067
|
|
|
813
|
-
# Update clip bounds
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
@
|
|
818
|
-
|
|
819
|
-
# Ceiling
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1068
|
+
# Update clip bounds
|
|
1069
|
+
if closed_door
|
|
1070
|
+
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
1071
|
+
@ceiling_clip[x] = SCREEN_HEIGHT
|
|
1072
|
+
@floor_clip[x] = -1
|
|
1073
|
+
else
|
|
1074
|
+
# Ceiling clip (uses effective_front_ceil for sky hack)
|
|
1075
|
+
if effective_front_ceil > back_sector.ceiling_height
|
|
1076
|
+
@ceiling_clip[x] = [back_ceil_y, @ceiling_clip[x]].max
|
|
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
|
|
1081
|
+
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1082
|
+
end
|
|
826
1083
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1084
|
+
# Floor clip
|
|
1085
|
+
if sector.floor_height < back_sector.floor_height
|
|
1086
|
+
@floor_clip[x] = [back_floor_y, @floor_clip[x]].min
|
|
1087
|
+
elsif sector.floor_height > back_sector.floor_height
|
|
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
|
|
1092
|
+
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1093
|
+
end
|
|
838
1094
|
end
|
|
839
1095
|
else
|
|
840
1096
|
# One-sided (solid) wall
|
|
841
1097
|
# Mark ceiling visplane (from previous clip to wall's ceiling)
|
|
842
|
-
|
|
843
|
-
|
|
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
|
|
844
1105
|
end
|
|
845
1106
|
|
|
846
1107
|
# Mark floor visplane (from wall's floor to previous floor clip)
|
|
847
|
-
#
|
|
848
|
-
if @current_floor_plane
|
|
849
|
-
|
|
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
|
|
850
1115
|
end
|
|
851
1116
|
|
|
852
1117
|
# Draw wall (from clipped ceiling to clipped floor)
|
|
@@ -883,7 +1148,8 @@ module Doom
|
|
|
883
1148
|
scale1, scale2,
|
|
884
1149
|
silhouette,
|
|
885
1150
|
bsilheight, tsilheight,
|
|
886
|
-
sprtopclip, sprbottomclip
|
|
1151
|
+
sprtopclip, sprbottomclip,
|
|
1152
|
+
seg
|
|
887
1153
|
)
|
|
888
1154
|
@drawsegs << drawseg
|
|
889
1155
|
end
|
|
@@ -905,7 +1171,7 @@ module Doom
|
|
|
905
1171
|
y2 = [y2, clip_bottom].min
|
|
906
1172
|
return if y1 > y2
|
|
907
1173
|
|
|
908
|
-
texture = @textures[texture_name]
|
|
1174
|
+
texture = @textures[anim_texture(texture_name)]
|
|
909
1175
|
return unless texture
|
|
910
1176
|
|
|
911
1177
|
light = calculate_light(light_level, dist)
|
|
@@ -916,7 +1182,6 @@ module Doom
|
|
|
916
1182
|
|
|
917
1183
|
# Texture X coordinate (wrap around texture width)
|
|
918
1184
|
tex_x = tex_col.to_i % tex_width
|
|
919
|
-
tex_x += tex_width if tex_x < 0
|
|
920
1185
|
|
|
921
1186
|
# Get the column of pixels
|
|
922
1187
|
column = texture.column_pixels(tex_x)
|
|
@@ -940,9 +1205,8 @@ module Doom
|
|
|
940
1205
|
while y <= y2
|
|
941
1206
|
screen_offset = y - y1
|
|
942
1207
|
tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i % tex_height
|
|
943
|
-
tex_y += tex_height if tex_y < 0
|
|
944
1208
|
|
|
945
|
-
color = column[tex_y]
|
|
1209
|
+
color = column[tex_y]
|
|
946
1210
|
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
947
1211
|
y += 1
|
|
948
1212
|
end
|
|
@@ -1017,7 +1281,10 @@ module Doom
|
|
|
1017
1281
|
# Collect visible sprites with their distances
|
|
1018
1282
|
visible_sprites = []
|
|
1019
1283
|
|
|
1020
|
-
@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
|
+
|
|
1021
1288
|
# Check if we have a sprite for this thing type
|
|
1022
1289
|
next unless @sprites.prefix_for(thing.type)
|
|
1023
1290
|
|
|
@@ -1035,8 +1302,12 @@ module Doom
|
|
|
1035
1302
|
dy = thing.y - @player_y
|
|
1036
1303
|
angle_to_thing = Math.atan2(dy, dx)
|
|
1037
1304
|
|
|
1038
|
-
# Get
|
|
1039
|
-
|
|
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
|
|
1040
1311
|
next unless sprite
|
|
1041
1312
|
|
|
1042
1313
|
# Project to screen X
|
|
@@ -1047,18 +1318,11 @@ module Doom
|
|
|
1047
1318
|
next if screen_x + sprite_half_width < 0
|
|
1048
1319
|
next if screen_x - sprite_half_width >= SCREEN_WIDTH
|
|
1049
1320
|
|
|
1050
|
-
visible_sprites <<
|
|
1051
|
-
thing: thing,
|
|
1052
|
-
sprite: sprite,
|
|
1053
|
-
view_x: view_x,
|
|
1054
|
-
view_y: view_y,
|
|
1055
|
-
dist: dist,
|
|
1056
|
-
screen_x: screen_x
|
|
1057
|
-
}
|
|
1321
|
+
visible_sprites << VisibleSprite.new(thing, sprite, view_x, view_y, dist, screen_x)
|
|
1058
1322
|
end
|
|
1059
1323
|
|
|
1060
1324
|
# Sort by distance (back to front for proper overdraw)
|
|
1061
|
-
visible_sprites.sort_by! { |s| -s
|
|
1325
|
+
visible_sprites.sort_by! { |s| -s.dist }
|
|
1062
1326
|
|
|
1063
1327
|
# Draw each sprite
|
|
1064
1328
|
visible_sprites.each do |vs|
|
|
@@ -1067,10 +1331,10 @@ module Doom
|
|
|
1067
1331
|
end
|
|
1068
1332
|
|
|
1069
1333
|
def draw_sprite(vs)
|
|
1070
|
-
sprite = vs
|
|
1071
|
-
dist = vs
|
|
1072
|
-
screen_x = vs
|
|
1073
|
-
thing = vs
|
|
1334
|
+
sprite = vs.sprite
|
|
1335
|
+
dist = vs.dist
|
|
1336
|
+
screen_x = vs.screen_x
|
|
1337
|
+
thing = vs.thing
|
|
1074
1338
|
|
|
1075
1339
|
# Calculate scale (inverse of distance, used for depth comparison)
|
|
1076
1340
|
sprite_scale = @projection / dist
|
|
@@ -1120,12 +1384,14 @@ module Doom
|
|
|
1120
1384
|
lowscale = [ds.scale1, ds.scale2].min
|
|
1121
1385
|
highscale = [ds.scale1, ds.scale2].max
|
|
1122
1386
|
|
|
1123
|
-
#
|
|
1387
|
+
# Chocolate Doom logic: skip if seg is behind sprite
|
|
1388
|
+
# If highscale < sprite_scale: wall entirely behind sprite
|
|
1389
|
+
# OR if lowscale < sprite_scale AND sprite is on front side of wall
|
|
1124
1390
|
if highscale < sprite_scale
|
|
1125
1391
|
next
|
|
1126
1392
|
elsif lowscale < sprite_scale
|
|
1127
|
-
#
|
|
1128
|
-
|
|
1393
|
+
# Partial overlap - check if sprite is in front of the seg
|
|
1394
|
+
next unless point_on_seg_side(thing.x, thing.y, ds.curline)
|
|
1129
1395
|
end
|
|
1130
1396
|
|
|
1131
1397
|
# Determine which silhouettes apply based on sprite Z
|
|
@@ -1208,11 +1474,6 @@ module Doom
|
|
|
1208
1474
|
end
|
|
1209
1475
|
end
|
|
1210
1476
|
end
|
|
1211
|
-
|
|
1212
|
-
def set_pixel(x, y, color)
|
|
1213
|
-
return if x < 0 || x >= SCREEN_WIDTH || y < 0 || y >= SCREEN_HEIGHT
|
|
1214
|
-
@framebuffer[y * SCREEN_WIDTH + x] = color
|
|
1215
|
-
end
|
|
1216
1477
|
end
|
|
1217
1478
|
end
|
|
1218
1479
|
end
|