doom 0.8.0 → 0.11.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/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +134 -24
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +239 -153
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +22 -39
- data/lib/doom/game/item_pickup.rb +87 -75
- data/lib/doom/game/menu.rb +90 -85
- data/lib/doom/game/monster_ai.rb +150 -136
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +38 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +169 -93
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +33 -58
- data/lib/doom/game/state_hash.rb +125 -0
- data/lib/doom/game/ticcmd.rb +61 -0
- data/lib/doom/game/world.rb +420 -0
- data/lib/doom/map/data.rb +95 -0
- data/lib/doom/net/client.rb +204 -0
- data/lib/doom/net/desync_monitor.rb +101 -0
- data/lib/doom/net/game_server.rb +232 -0
- data/lib/doom/net/lockstep.rb +170 -0
- data/lib/doom/net/protocol.rb +350 -0
- data/lib/doom/net/session.rb +282 -0
- data/lib/doom/net/transport.rb +110 -0
- data/lib/doom/platform/gosu_window.rb +604 -832
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +5 -3
- data/lib/doom/render/hardware_renderer.rb +547 -0
- data/lib/doom/render/ray_tracing/bvh.rb +103 -0
- data/lib/doom/render/ray_tracing/material_state.rb +66 -0
- data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
- data/lib/doom/render/ray_tracing_renderer.rb +940 -0
- data/lib/doom/render/renderer.rb +466 -440
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +7 -7
- data/lib/doom/render/spinel_native/kernel.rb +780 -0
- data/lib/doom/render/spinel_native_renderer.rb +162 -0
- data/lib/doom/render/status_bar.rb +14 -10
- data/lib/doom/render/weapon_renderer.rb +9 -10
- data/lib/doom/render/world_mesh.rb +270 -0
- data/lib/doom/render/zbuffer_renderer.rb +151 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/flat.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +7 -3
- data/lib/doom/wad/palette.rb +3 -3
- data/lib/doom/wad/patch.rb +1 -1
- data/lib/doom/wad/reader.rb +12 -15
- data/lib/doom/wad/sound.rb +14 -6
- data/lib/doom/wad/sprite.rb +36 -35
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +197 -16
- metadata +94 -7
data/lib/doom/render/renderer.rb
CHANGED
|
@@ -19,11 +19,11 @@ module Doom
|
|
|
19
19
|
Drawseg = Struct.new(:x1, :x2, :scale1, :scale2, :scalestep,
|
|
20
20
|
:silhouette, :bsilheight, :tsilheight,
|
|
21
21
|
:sprtopclip, :sprbottomclip,
|
|
22
|
-
:curline,
|
|
23
|
-
:maskedtexturecol,
|
|
24
|
-
:frontsector, :backsector,
|
|
25
|
-
:sidedef,
|
|
26
|
-
:dist1, :dist2, :sx1, :sx2)
|
|
22
|
+
:curline, # seg for point-on-side test
|
|
23
|
+
:maskedtexturecol, # per-column texture X for masked mid textures
|
|
24
|
+
:frontsector, :backsector, # sectors for masked rendering
|
|
25
|
+
:sidedef, # sidedef for texture lookup
|
|
26
|
+
:dist1, :dist2, :sx1, :sx2) # for per-column distance recomputation
|
|
27
27
|
|
|
28
28
|
# VisibleSprite stores sprite data for sorting and rendering
|
|
29
29
|
# Struct is faster than Hash for fixed-field data
|
|
@@ -43,6 +43,7 @@ module Doom
|
|
|
43
43
|
|
|
44
44
|
def mark(x, y1, y2)
|
|
45
45
|
return if y1 > y2
|
|
46
|
+
|
|
46
47
|
top[x] = [top[x], y1].min
|
|
47
48
|
bottom[x] = [bottom[x], y2].max
|
|
48
49
|
self.minx = [minx, x].min
|
|
@@ -55,21 +56,33 @@ module Doom
|
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
class Renderer
|
|
58
|
-
attr_reader :framebuffer
|
|
59
|
-
|
|
60
59
|
def initialize(wad, map, textures, palette, colormap, flats, sprites = nil, animations = nil)
|
|
61
60
|
@wad = wad
|
|
62
61
|
@map = map
|
|
63
62
|
@textures = textures
|
|
64
63
|
@palette = palette
|
|
65
64
|
@colormap = colormap
|
|
65
|
+
@flats_array = flats
|
|
66
66
|
@flats = flats.to_h { |f| [f.name, f] }
|
|
67
|
+
# Opt-in: route the wall/floor/ceiling rasterisation through the
|
|
68
|
+
# spinel_native-compiled port of this renderer's hot path. Everything
|
|
69
|
+
# else (sprites, weapon, HUD, physics, sound) stays the real game.
|
|
70
|
+
@native_kernel = !ENV["DOOM_NATIVE"].to_s.empty? && ENV["DOOM_NATIVE"] != "0"
|
|
71
|
+
if @native_kernel
|
|
72
|
+
warn "[doom] compiling spinel_native render kernel (first run may take a few seconds)..."
|
|
73
|
+
@sn = build_native_renderer
|
|
74
|
+
@native_kernel = !@sn.nil?
|
|
75
|
+
warn "[doom] spinel_native renderer ready" if @sn
|
|
76
|
+
end
|
|
67
77
|
@sprites = sprites
|
|
68
78
|
@animations = animations
|
|
69
79
|
@hidden_things = nil
|
|
70
80
|
@combat = nil
|
|
71
81
|
@monster_ai = nil
|
|
82
|
+
@players = [] # Every player in the world, for drawing the others
|
|
83
|
+
@view_player = nil # The one we are looking through, never drawn
|
|
72
84
|
@leveltime = 0
|
|
85
|
+
@skip_background_fill = false
|
|
73
86
|
|
|
74
87
|
@framebuffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0)
|
|
75
88
|
|
|
@@ -77,6 +90,8 @@ module Doom
|
|
|
77
90
|
@player_y = 0.0
|
|
78
91
|
@player_z = 41.0
|
|
79
92
|
@player_angle = 0.0
|
|
93
|
+
@sin_angle = 0.0
|
|
94
|
+
@cos_angle = 1.0
|
|
80
95
|
|
|
81
96
|
# Projection constant - distance to projection plane
|
|
82
97
|
@projection = HALF_WIDTH / Math.tan(FOV * Math::PI / 360.0)
|
|
@@ -89,13 +104,17 @@ module Doom
|
|
|
89
104
|
# Column distance scale is constant (doesn't depend on player angle)
|
|
90
105
|
@column_distscale = Array.new(SCREEN_WIDTH) do |x|
|
|
91
106
|
dx = x - HALF_WIDTH
|
|
92
|
-
Math.sqrt(dx * dx + @projection * @projection) / @projection
|
|
107
|
+
Math.sqrt((dx * dx) + (@projection * @projection)) / @projection
|
|
93
108
|
end
|
|
94
109
|
|
|
95
110
|
# Clipping arrays
|
|
96
111
|
@ceiling_clip = Array.new(SCREEN_WIDTH, -1)
|
|
97
112
|
@floor_clip = Array.new(SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
98
113
|
|
|
114
|
+
# Per-sprite clip buffers, reset (not reallocated) for each draw_sprite call
|
|
115
|
+
@sprite_clipbot = Array.new(SCREEN_WIDTH, -2)
|
|
116
|
+
@sprite_cliptop = Array.new(SCREEN_WIDTH, -2)
|
|
117
|
+
|
|
99
118
|
# Sprite clip arrays (copy of wall clips for sprite clipping)
|
|
100
119
|
@sprite_ceiling_clip = Array.new(SCREEN_WIDTH, -1)
|
|
101
120
|
@sprite_floor_clip = Array.new(SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
@@ -108,9 +127,10 @@ module Doom
|
|
|
108
127
|
@y_slope_ceil = Array.new(HALF_HEIGHT + 1, 0.0)
|
|
109
128
|
@y_slope_floor = Array.new(HALF_HEIGHT + 1, 0.0)
|
|
110
129
|
end
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
attr_reader :framebuffer, :player_x, :player_y, :player_z, :player_angle, :sin_angle, :cos_angle, :wad, :map,
|
|
131
|
+
:textures, :palette, :colormap, :flats, :sprites, :animations
|
|
113
132
|
attr_writer :hidden_things, :combat, :monster_ai, :leveltime
|
|
133
|
+
attr_accessor :players, :view_player, :skip_background_fill
|
|
114
134
|
|
|
115
135
|
# Diagnostic: returns info about all sprites and why they are/aren't visible
|
|
116
136
|
def sprite_diagnostics
|
|
@@ -128,7 +148,7 @@ module Doom
|
|
|
128
148
|
info[:view_y] = view_y.round(1)
|
|
129
149
|
|
|
130
150
|
if view_y <= 0
|
|
131
|
-
info[:status] =
|
|
151
|
+
info[:status] = 'behind_player'
|
|
132
152
|
results << info
|
|
133
153
|
next
|
|
134
154
|
end
|
|
@@ -143,7 +163,7 @@ module Doom
|
|
|
143
163
|
angle_to_thing = Math.atan2(dy, dx)
|
|
144
164
|
sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
|
|
145
165
|
unless sprite
|
|
146
|
-
info[:status] =
|
|
166
|
+
info[:status] = 'no_sprite_frame'
|
|
147
167
|
results << info
|
|
148
168
|
next
|
|
149
169
|
end
|
|
@@ -153,20 +173,19 @@ module Doom
|
|
|
153
173
|
info[:sprite_scale] = sprite_scale.round(3)
|
|
154
174
|
|
|
155
175
|
if screen_x + sprite_half_width < 0
|
|
156
|
-
info[:status] =
|
|
176
|
+
info[:status] = 'off_screen_left'
|
|
157
177
|
elsif screen_x - sprite_half_width >= SCREEN_WIDTH
|
|
158
|
-
info[:status] =
|
|
178
|
+
info[:status] = 'off_screen_right'
|
|
159
179
|
else
|
|
160
180
|
# Check drawseg clipping
|
|
161
|
-
sprite_left = (screen_x - sprite.left_offset * sprite_scale).to_i
|
|
181
|
+
sprite_left = (screen_x - (sprite.left_offset * sprite_scale)).to_i
|
|
162
182
|
sprite_right = sprite_left + (sprite.width * sprite_scale).to_i - 1
|
|
163
183
|
x1 = [sprite_left, 0].max
|
|
164
184
|
x2 = [sprite_right, SCREEN_WIDTH - 1].min
|
|
165
185
|
|
|
166
186
|
sector = @map.sector_at(thing.x, thing.y)
|
|
167
187
|
thing_floor = sector ? sector.floor_height : 0
|
|
168
|
-
|
|
169
|
-
sprite_gzt = thing_floor + sprite.top_offset
|
|
188
|
+
thing_floor + sprite.top_offset
|
|
170
189
|
|
|
171
190
|
clipping_segs = []
|
|
172
191
|
@drawsegs.reverse_each do |ds|
|
|
@@ -192,7 +211,7 @@ module Doom
|
|
|
192
211
|
info[:screen_range] = "#{x1}..#{x2}"
|
|
193
212
|
info[:clipping_segs] = clipping_segs.size
|
|
194
213
|
info[:clipping_detail] = clipping_segs
|
|
195
|
-
info[:status] =
|
|
214
|
+
info[:status] = 'visible'
|
|
196
215
|
end
|
|
197
216
|
|
|
198
217
|
results << info
|
|
@@ -200,54 +219,61 @@ module Doom
|
|
|
200
219
|
results
|
|
201
220
|
end
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
# Point the camera at a pose. This is the renderer's real input now that
|
|
223
|
+
# Game::Player owns the player: game logic mutates the player, and the
|
|
224
|
+
# camera is aimed once per frame at an interpolated pose. Angle is in
|
|
225
|
+
# radians here, unlike the older degree-based setters below.
|
|
226
|
+
def apply_view(x, y, z, angle_radians)
|
|
227
|
+
@player_x = x
|
|
228
|
+
@player_y = y
|
|
229
|
+
@player_z = z
|
|
230
|
+
@player_angle = angle_radians
|
|
231
|
+
@sin_angle = Math.sin(angle_radians)
|
|
232
|
+
@cos_angle = Math.cos(angle_radians)
|
|
208
233
|
end
|
|
209
234
|
|
|
210
|
-
def
|
|
235
|
+
def set_player(x, y, z, angle)
|
|
211
236
|
@player_x = x.to_f
|
|
212
237
|
@player_y = y.to_f
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def set_z(z)
|
|
216
238
|
@player_z = z.to_f
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
@player_angle += degrees * Math::PI / 180.0
|
|
239
|
+
@player_angle = angle * Math::PI / 180.0
|
|
240
|
+
@sin_angle = Math.sin(@player_angle)
|
|
241
|
+
@cos_angle = Math.cos(@player_angle)
|
|
221
242
|
end
|
|
222
243
|
|
|
223
244
|
def render_frame
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
if @native_kernel
|
|
246
|
+
native_wall_pass
|
|
247
|
+
@drawsegs = []
|
|
248
|
+
else
|
|
249
|
+
clear_framebuffer
|
|
250
|
+
reset_clipping
|
|
226
251
|
|
|
227
|
-
|
|
228
|
-
|
|
252
|
+
@sin_angle = Math.sin(@player_angle)
|
|
253
|
+
@cos_angle = Math.cos(@player_angle)
|
|
229
254
|
|
|
230
|
-
|
|
231
|
-
|
|
255
|
+
# Precompute column angles for floor/ceiling rendering
|
|
256
|
+
precompute_column_data
|
|
232
257
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
258
|
+
# Draw player's sector floor/ceiling as background fallback.
|
|
259
|
+
# Visplanes (with correct per-sector lighting) overwrite this for sectors
|
|
260
|
+
# with different properties. This only remains visible at gaps between
|
|
261
|
+
# same-property sectors where the light level matches anyway.
|
|
262
|
+
draw_floor_ceiling_background
|
|
238
263
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
# Initialize visplanes for tracking visible floor/ceiling spans
|
|
265
|
+
@visplanes = []
|
|
266
|
+
@visplane_hash = {} # Hash for O(1) lookup by (height, texture, light_level, is_ceiling)
|
|
242
267
|
|
|
243
|
-
|
|
244
|
-
|
|
268
|
+
# Initialize drawsegs for sprite clipping
|
|
269
|
+
@drawsegs = []
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
|
|
271
|
+
# Render walls via BSP traversal
|
|
272
|
+
render_bsp_node(@map.nodes.size - 1)
|
|
248
273
|
|
|
249
|
-
|
|
250
|
-
|
|
274
|
+
# Draw visplanes for sectors different from background
|
|
275
|
+
draw_all_visplanes
|
|
276
|
+
end
|
|
251
277
|
|
|
252
278
|
# Save wall clip arrays for sprite clipping (reuse preallocated arrays)
|
|
253
279
|
@sprite_ceiling_clip.replace(@ceiling_clip)
|
|
@@ -258,6 +284,38 @@ module Doom
|
|
|
258
284
|
draw_masked if @sprites
|
|
259
285
|
end
|
|
260
286
|
|
|
287
|
+
# Wall/floor/ceiling/sky rasterisation through the spinel_native-compiled
|
|
288
|
+
# port of this renderer's hot path. Fills @framebuffer plus the clip rows
|
|
289
|
+
# the Ruby sprite pass reads. Falls back to the Ruby path if the kernel
|
|
290
|
+
# cannot be built (e.g. no spinel compiler available).
|
|
291
|
+
def native_wall_pass
|
|
292
|
+
@sn ||= build_native_renderer
|
|
293
|
+
unless @sn
|
|
294
|
+
@native_kernel = false
|
|
295
|
+
return render_frame
|
|
296
|
+
end
|
|
297
|
+
# The sprite pass (kept in Ruby) transforms things with @sin_angle /
|
|
298
|
+
# @cos_angle, so they must be set for the current frame even though the
|
|
299
|
+
# native kernel computes its own trig internally.
|
|
300
|
+
@sin_angle = Math.sin(@player_angle)
|
|
301
|
+
@cos_angle = Math.cos(@player_angle)
|
|
302
|
+
fb = @sn.render_at(@player_x, @player_y, @player_z, @player_angle)
|
|
303
|
+
@framebuffer.replace(fb)
|
|
304
|
+
@ceiling_clip.replace(@sn.ceiling_clip)
|
|
305
|
+
@floor_clip.replace(@sn.floor_clip)
|
|
306
|
+
@wall_depth.replace(@sn.wall_depth)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def build_native_renderer
|
|
310
|
+
# Dynamic form so the spinel AOT compiler ignores this require (it pulls
|
|
311
|
+
# in the spinel_native gem, irrelevant to an AOT build); CRuby is unaffected.
|
|
312
|
+
send(:require_relative, "spinel_native_renderer")
|
|
313
|
+
SpinelNativeRenderer.new(@map, @palette, @colormap, @textures, @flats_array)
|
|
314
|
+
rescue StandardError, LoadError => e
|
|
315
|
+
warn "[doom] spinel_native renderer unavailable, using Ruby renderer (#{e.message.lines.first.to_s.strip})"
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
261
319
|
# Precompute column-based data for floor/ceiling rendering (R_InitLightTables-like)
|
|
262
320
|
# Cached: only recomputes sin/cos when player angle changes
|
|
263
321
|
def precompute_column_data
|
|
@@ -273,6 +331,8 @@ module Doom
|
|
|
273
331
|
end
|
|
274
332
|
|
|
275
333
|
def draw_floor_ceiling_background
|
|
334
|
+
return if @skip_background_fill
|
|
335
|
+
|
|
276
336
|
player_sector = @map.sector_at(@player_x, @player_y)
|
|
277
337
|
return unless player_sector
|
|
278
338
|
|
|
@@ -297,7 +357,7 @@ module Doom
|
|
|
297
357
|
def render_visplane_spans(plane)
|
|
298
358
|
return if plane.minx > plane.maxx
|
|
299
359
|
|
|
300
|
-
spanstart = Array.new(SCREEN_HEIGHT)
|
|
360
|
+
spanstart = Array.new(SCREEN_HEIGHT) # Track where each row's span started
|
|
301
361
|
|
|
302
362
|
# Process columns left to right
|
|
303
363
|
((plane.minx)..(plane.maxx + 1)).each do |x|
|
|
@@ -305,9 +365,10 @@ module Doom
|
|
|
305
365
|
if x <= plane.maxx
|
|
306
366
|
t2 = plane.top[x]
|
|
307
367
|
b2 = plane.bottom[x]
|
|
308
|
-
t2 = SCREEN_HEIGHT if t2 > b2
|
|
368
|
+
t2 = SCREEN_HEIGHT if t2 > b2 # Invalid = empty
|
|
309
369
|
else
|
|
310
|
-
t2
|
|
370
|
+
t2 = SCREEN_HEIGHT
|
|
371
|
+
b2 = -1 # Sentinel for final column
|
|
311
372
|
end
|
|
312
373
|
|
|
313
374
|
# Get previous column bounds
|
|
@@ -316,7 +377,8 @@ module Doom
|
|
|
316
377
|
b1 = plane.bottom[x - 1]
|
|
317
378
|
t1 = SCREEN_HEIGHT if t1 > b1
|
|
318
379
|
else
|
|
319
|
-
t1
|
|
380
|
+
t1 = SCREEN_HEIGHT
|
|
381
|
+
b1 = -1
|
|
320
382
|
end
|
|
321
383
|
|
|
322
384
|
# Close spans that ended (visible in prev column, not in current)
|
|
@@ -333,10 +395,10 @@ module Doom
|
|
|
333
395
|
end
|
|
334
396
|
|
|
335
397
|
# Open new spans (visible in current, not started yet)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
398
|
+
next unless t2 < SCREEN_HEIGHT
|
|
399
|
+
|
|
400
|
+
(t2..b2).each do |y|
|
|
401
|
+
spanstart[y] ||= x
|
|
340
402
|
end
|
|
341
403
|
end
|
|
342
404
|
end
|
|
@@ -381,9 +443,9 @@ module Doom
|
|
|
381
443
|
x = x1
|
|
382
444
|
while x <= x2
|
|
383
445
|
ray_dist = perp_dist * column_distscale[x]
|
|
384
|
-
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
385
|
-
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
386
|
-
color = flat_pixels[tex_y * 64 + tex_x]
|
|
446
|
+
tex_x = (player_x + (ray_dist * column_cos[x])).to_i & 63
|
|
447
|
+
tex_y = (neg_player_y - (ray_dist * column_sin[x])).to_i & 63
|
|
448
|
+
color = flat_pixels[(tex_y * 64) + tex_x]
|
|
387
449
|
framebuffer[row_offset + x] = cmap[color]
|
|
388
450
|
x += 1
|
|
389
451
|
end
|
|
@@ -394,7 +456,7 @@ module Doom
|
|
|
394
456
|
# Chocolate Doom: skytexturemid = SCREENHEIGHT/2 = 100 (for 200px screen).
|
|
395
457
|
# Scale factor: our 240px screen maps to DOOM's 200px sky coordinates.
|
|
396
458
|
SKY_TEXTUREMID = 100.0
|
|
397
|
-
SKY_YSCALE = 200.0 / SCREEN_HEIGHT
|
|
459
|
+
SKY_YSCALE = 200.0 / SCREEN_HEIGHT # 0.833 - maps our pixels to DOOM's 200px space
|
|
398
460
|
# Chocolate Doom: ANGLETOSKYSHIFT=22 gives 4 sky repetitions per 360 degrees.
|
|
399
461
|
# Full circle (2pi) * 512/pi = 1024 columns, masked to 256 = 4 repetitions.
|
|
400
462
|
SKY_XSCALE = 512.0 / Math::PI
|
|
@@ -431,9 +493,9 @@ module Doom
|
|
|
431
493
|
# Sky Y: texel = skytexturemid + (y - centery) * scale
|
|
432
494
|
# Maps texel 0 to screen top, texel 100 to horizon (1:1 for DOOM's 200px)
|
|
433
495
|
(y1..y2).each do |y|
|
|
434
|
-
tex_y = (SKY_TEXTUREMID + (y - HALF_HEIGHT) * SKY_YSCALE).to_i % sky_height
|
|
496
|
+
tex_y = (SKY_TEXTUREMID + ((y - HALF_HEIGHT) * SKY_YSCALE)).to_i % sky_height
|
|
435
497
|
color = column[tex_y]
|
|
436
|
-
framebuffer[y * SCREEN_WIDTH + x] = color
|
|
498
|
+
framebuffer[(y * SCREEN_WIDTH) + x] = color
|
|
437
499
|
end
|
|
438
500
|
end
|
|
439
501
|
end
|
|
@@ -479,13 +541,14 @@ module Doom
|
|
|
479
541
|
overlap = false
|
|
480
542
|
(intrl..intrh).each do |x|
|
|
481
543
|
next if x < 0 || x >= SCREEN_WIDTH
|
|
544
|
+
|
|
482
545
|
if plane.top[x] <= plane.bottom[x]
|
|
483
546
|
overlap = true
|
|
484
547
|
break
|
|
485
548
|
end
|
|
486
549
|
end
|
|
487
550
|
|
|
488
|
-
|
|
551
|
+
unless overlap
|
|
489
552
|
# No overlap - reuse same visplane with expanded range
|
|
490
553
|
plane.minx = unionl if unionl < plane.minx
|
|
491
554
|
plane.maxx = unionh if unionh > plane.maxx
|
|
@@ -555,7 +618,7 @@ module Doom
|
|
|
555
618
|
if is_sky && sky_texture
|
|
556
619
|
sky_w = sky_texture.width
|
|
557
620
|
sky_h = sky_texture.height
|
|
558
|
-
sky_y = (SKY_TEXTUREMID + (y - HALF_HEIGHT) * SKY_YSCALE).to_i % sky_h
|
|
621
|
+
sky_y = (SKY_TEXTUREMID + ((y - HALF_HEIGHT) * SKY_YSCALE)).to_i % sky_h
|
|
559
622
|
x = 0
|
|
560
623
|
while x < SCREEN_WIDTH
|
|
561
624
|
column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
|
|
@@ -568,9 +631,9 @@ module Doom
|
|
|
568
631
|
x = 0
|
|
569
632
|
while x < SCREEN_WIDTH
|
|
570
633
|
ray_dist = perp_dist * column_distscale[x]
|
|
571
|
-
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
572
|
-
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
573
|
-
color = ceil_pixels[tex_y * 64 + tex_x]
|
|
634
|
+
tex_x = (player_x + (ray_dist * column_cos[x])).to_i & 63
|
|
635
|
+
tex_y = (neg_player_y - (ray_dist * column_sin[x])).to_i & 63
|
|
636
|
+
color = ceil_pixels[(tex_y * 64) + tex_x]
|
|
574
637
|
framebuffer[row_offset + x] = cmap[color]
|
|
575
638
|
x += 1
|
|
576
639
|
end
|
|
@@ -595,9 +658,9 @@ module Doom
|
|
|
595
658
|
x = 0
|
|
596
659
|
while x < SCREEN_WIDTH
|
|
597
660
|
ray_dist = perp_dist * column_distscale[x]
|
|
598
|
-
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
599
|
-
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
600
|
-
color = floor_pixels[tex_y * 64 + tex_x]
|
|
661
|
+
tex_x = (player_x + (ray_dist * column_cos[x])).to_i & 63
|
|
662
|
+
tex_y = (neg_player_y - (ray_dist * column_sin[x])).to_i & 63
|
|
663
|
+
color = floor_pixels[(tex_y * 64) + tex_x]
|
|
601
664
|
framebuffer[row_offset + x] = cmap[color]
|
|
602
665
|
x += 1
|
|
603
666
|
end
|
|
@@ -686,6 +749,7 @@ module Doom
|
|
|
686
749
|
x = min_sx
|
|
687
750
|
while x <= max_sx
|
|
688
751
|
return true if @ceiling_clip[x] < @floor_clip[x] - 1
|
|
752
|
+
|
|
689
753
|
x += 1
|
|
690
754
|
end
|
|
691
755
|
|
|
@@ -715,32 +779,28 @@ module Doom
|
|
|
715
779
|
|
|
716
780
|
# Create floor visplane if floor is visible (below eye level)
|
|
717
781
|
# Matches Chocolate Doom: if (frontsector->floorheight < viewz)
|
|
718
|
-
if @current_sector.floor_height < @player_z
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
@current_floor_plane = nil
|
|
728
|
-
end
|
|
782
|
+
@current_floor_plane = if @current_sector.floor_height < @player_z
|
|
783
|
+
find_or_create_visplane(
|
|
784
|
+
@current_sector,
|
|
785
|
+
@current_sector.floor_height,
|
|
786
|
+
@current_sector.floor_texture,
|
|
787
|
+
@current_sector.light_level,
|
|
788
|
+
false
|
|
789
|
+
)
|
|
790
|
+
end
|
|
729
791
|
|
|
730
792
|
# Create ceiling visplane if ceiling is visible (above eye level or sky)
|
|
731
793
|
# Matches Chocolate Doom: if (frontsector->ceilingheight > viewz || frontsector->ceilingpic == skyflatnum)
|
|
732
794
|
is_sky = @current_sector.ceiling_texture == 'F_SKY1'
|
|
733
|
-
if @current_sector.ceiling_height > @player_z || is_sky
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
@current_ceiling_plane = nil
|
|
743
|
-
end
|
|
795
|
+
@current_ceiling_plane = if @current_sector.ceiling_height > @player_z || is_sky
|
|
796
|
+
find_or_create_visplane(
|
|
797
|
+
@current_sector,
|
|
798
|
+
@current_sector.ceiling_height,
|
|
799
|
+
@current_sector.ceiling_texture,
|
|
800
|
+
@current_sector.light_level,
|
|
801
|
+
true
|
|
802
|
+
)
|
|
803
|
+
end
|
|
744
804
|
|
|
745
805
|
# Process all segs in this subsector
|
|
746
806
|
subsector.seg_count.times do |i|
|
|
@@ -768,11 +828,11 @@ module Doom
|
|
|
768
828
|
return
|
|
769
829
|
elsif y1 < near
|
|
770
830
|
t = (near - y1) / (y2 - y1)
|
|
771
|
-
x1
|
|
831
|
+
x1 += (t * (x2 - x1))
|
|
772
832
|
y1 = near
|
|
773
833
|
elsif y2 < near
|
|
774
834
|
t = (near - y1) / (y2 - y1)
|
|
775
|
-
x2 = x1 + t * (x2 - x1)
|
|
835
|
+
x2 = x1 + (t * (x2 - x1))
|
|
776
836
|
y2 = near
|
|
777
837
|
end
|
|
778
838
|
end
|
|
@@ -816,7 +876,7 @@ module Doom
|
|
|
816
876
|
# Calculate seg length for texture mapping
|
|
817
877
|
seg_v1 = @map.vertices[seg.v1]
|
|
818
878
|
seg_v2 = @map.vertices[seg.v2]
|
|
819
|
-
seg_length = Math.sqrt((seg_v2.x - seg_v1.x)**2 + (seg_v2.y - seg_v1.y)**2)
|
|
879
|
+
seg_length = Math.sqrt(((seg_v2.x - seg_v1.x)**2) + ((seg_v2.y - seg_v1.y)**2))
|
|
820
880
|
|
|
821
881
|
draw_seg_range(x1i, x2i, sx1, sx2, y1, y2, sector, back_sector, sidedef, linedef, seg, seg_length)
|
|
822
882
|
end
|
|
@@ -828,8 +888,8 @@ module Doom
|
|
|
828
888
|
|
|
829
889
|
# Rotate - transform world to view space
|
|
830
890
|
# View space: +Y forward (in direction of angle), +X is right
|
|
831
|
-
x = dx * @sin_angle - dy * @cos_angle
|
|
832
|
-
y = dx * @cos_angle + dy * @sin_angle
|
|
891
|
+
x = (dx * @sin_angle) - (dy * @cos_angle)
|
|
892
|
+
y = (dx * @cos_angle) + (dy * @sin_angle)
|
|
833
893
|
|
|
834
894
|
[x, y]
|
|
835
895
|
end
|
|
@@ -882,16 +942,16 @@ module Doom
|
|
|
882
942
|
cos_a = @cos_angle
|
|
883
943
|
proj = @projection
|
|
884
944
|
|
|
885
|
-
c1 = cos_a * proj - sin_a * HALF_WIDTH
|
|
886
|
-
c2 = sin_a * proj + cos_a * HALF_WIDTH
|
|
945
|
+
c1 = (cos_a * proj) - (sin_a * HALF_WIDTH)
|
|
946
|
+
c2 = (sin_a * proj) + (cos_a * HALF_WIDTH)
|
|
887
947
|
|
|
888
948
|
# denom(x) = seg_dy * ray_dx - seg_dx * ray_dy = A * x + B
|
|
889
|
-
tex_a = seg_dy * sin_a + seg_dx * cos_a
|
|
890
|
-
tex_b = seg_dy * c1 - seg_dx * c2
|
|
949
|
+
tex_a = (seg_dy * sin_a) + (seg_dx * cos_a)
|
|
950
|
+
tex_b = (seg_dy * c1) - (seg_dx * c2)
|
|
891
951
|
|
|
892
952
|
# numer(x) = py * ray_dx - px * ray_dy = E * x + F
|
|
893
|
-
tex_e = py * sin_a + px * cos_a
|
|
894
|
-
tex_f = py * c1 - px * c2
|
|
953
|
+
tex_e = (py * sin_a) + (px * cos_a)
|
|
954
|
+
tex_f = (py * c1) - (px * c2)
|
|
895
955
|
|
|
896
956
|
tex_offset = seg.offset + sidedef.x_offset
|
|
897
957
|
|
|
@@ -949,22 +1009,18 @@ module Doom
|
|
|
949
1009
|
end
|
|
950
1010
|
|
|
951
1011
|
# Check planes for this seg range (R_CheckPlane equivalent)
|
|
952
|
-
if @current_floor_plane
|
|
953
|
-
|
|
954
|
-
end
|
|
955
|
-
if @current_ceiling_plane
|
|
956
|
-
@current_ceiling_plane = check_plane(@current_ceiling_plane, x1, x2)
|
|
957
|
-
end
|
|
1012
|
+
@current_floor_plane = check_plane(@current_floor_plane, x1, x2) if @current_floor_plane
|
|
1013
|
+
@current_ceiling_plane = check_plane(@current_ceiling_plane, x1, x2) if @current_ceiling_plane
|
|
958
1014
|
|
|
959
1015
|
(x1..x2).each do |x|
|
|
960
1016
|
next if @ceiling_clip[x] >= @floor_clip[x] - 1
|
|
961
1017
|
|
|
962
1018
|
# Screen-space interpolation for distance
|
|
963
|
-
t = sx2
|
|
1019
|
+
t = sx2 == sx1 ? 0 : (x - sx1) / (sx2 - sx1)
|
|
964
1020
|
t = t.clamp(0.0, 1.0)
|
|
965
1021
|
|
|
966
1022
|
if dist1 > 0 && dist2 > 0
|
|
967
|
-
inv_dist = (1.0 - t) / dist1 + t / dist2
|
|
1023
|
+
inv_dist = ((1.0 - t) / dist1) + (t / dist2)
|
|
968
1024
|
dist = 1.0 / inv_dist
|
|
969
1025
|
else
|
|
970
1026
|
dist = dist1 > 0 ? dist1 : dist2
|
|
@@ -972,13 +1028,13 @@ module Doom
|
|
|
972
1028
|
|
|
973
1029
|
# Ray-seg intersection for texture column
|
|
974
1030
|
# s = (E * x + F) / (A * x + B) gives position along seg in world units
|
|
975
|
-
denom = tex_a * x + tex_b
|
|
976
|
-
if denom.abs < 0.001
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
tex_col = (tex_offset + s * seg_length).to_i
|
|
1031
|
+
denom = (tex_a * x) + tex_b
|
|
1032
|
+
s = if denom.abs < 0.001
|
|
1033
|
+
0.0
|
|
1034
|
+
else
|
|
1035
|
+
((tex_e * x) + tex_f) / denom
|
|
1036
|
+
end
|
|
1037
|
+
tex_col = (tex_offset + (s * seg_length)).to_i
|
|
982
1038
|
|
|
983
1039
|
# Skip if too close
|
|
984
1040
|
next if dist < 1
|
|
@@ -994,176 +1050,19 @@ module Doom
|
|
|
994
1050
|
# Chocolate Doom rounds ceiling UP and floor DOWN to avoid 1-pixel gaps:
|
|
995
1051
|
# yl = (topfrac+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for front ceiling)
|
|
996
1052
|
# yh = bottomfrac>>HEIGHTBITS (floor for front floor)
|
|
997
|
-
front_ceil_y = (HALF_HEIGHT - front_ceil * scale).ceil
|
|
998
|
-
front_floor_y = (HALF_HEIGHT - front_floor * scale).to_i
|
|
1053
|
+
front_ceil_y = (HALF_HEIGHT - (front_ceil * scale)).ceil
|
|
1054
|
+
front_floor_y = (HALF_HEIGHT - (front_floor * scale)).to_i
|
|
999
1055
|
|
|
1000
1056
|
# Clamp to current clip bounds
|
|
1001
1057
|
ceil_y = [front_ceil_y, @ceiling_clip[x] + 1].max
|
|
1002
1058
|
floor_y = [front_floor_y, @floor_clip[x] - 1].min
|
|
1003
1059
|
|
|
1004
1060
|
if back_sector
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
back_ceil = back_sector.ceiling_height - @player_z
|
|
1008
|
-
|
|
1009
|
-
# Chocolate Doom rounding:
|
|
1010
|
-
# pixhigh>>HEIGHTBITS (truncate for back ceiling / upper wall end)
|
|
1011
|
-
# (pixlow+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for back floor / lower wall start)
|
|
1012
|
-
back_ceil_y = (HALF_HEIGHT - back_ceil * scale).to_i
|
|
1013
|
-
back_floor_y = (HALF_HEIGHT - back_floor * scale).ceil
|
|
1014
|
-
|
|
1015
|
-
# Determine visible ceiling/floor boundaries (the opening between sectors)
|
|
1016
|
-
# high_ceil = top of the opening on screen (max Y = lower world ceiling)
|
|
1017
|
-
# low_floor = bottom of the opening on screen (min Y = higher world floor)
|
|
1018
|
-
high_ceil = [ceil_y, back_ceil_y].max
|
|
1019
|
-
low_floor = [floor_y, back_floor_y].min
|
|
1020
|
-
|
|
1021
|
-
# Check for closed door (no opening between sectors)
|
|
1022
|
-
# Matches Chocolate Doom: backsector->ceilingheight <= frontsector->floorheight
|
|
1023
|
-
# || backsector->floorheight >= frontsector->ceilingheight
|
|
1024
|
-
closed_door = back_sector.ceiling_height <= sector.floor_height ||
|
|
1025
|
-
back_sector.floor_height >= sector.ceiling_height
|
|
1026
|
-
|
|
1027
|
-
both_sky = sector.ceiling_texture == 'F_SKY1' && back_sector.ceiling_texture == 'F_SKY1'
|
|
1028
|
-
|
|
1029
|
-
# Determine whether to mark ceiling/floor visplanes.
|
|
1030
|
-
# Match Chocolate Doom: only mark when properties differ, plus force
|
|
1031
|
-
# for closed doors. The background fill covers same-property gaps.
|
|
1032
|
-
# Chocolate Doom sky hack: "worldtop = worldhigh" makes the front
|
|
1033
|
-
# ceiling equal to the back ceiling when both are sky. This prevents
|
|
1034
|
-
# the low sky ceiling from clipping walls in adjacent sectors.
|
|
1035
|
-
effective_front_ceil = both_sky ? back_sector.ceiling_height : sector.ceiling_height
|
|
1036
|
-
|
|
1037
|
-
if closed_door
|
|
1038
|
-
should_mark_ceiling = true
|
|
1039
|
-
should_mark_floor = true
|
|
1040
|
-
else
|
|
1041
|
-
should_mark_ceiling = effective_front_ceil != back_sector.ceiling_height ||
|
|
1042
|
-
sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1043
|
-
sector.light_level != back_sector.light_level
|
|
1044
|
-
should_mark_floor = sector.floor_height != back_sector.floor_height ||
|
|
1045
|
-
sector.floor_texture != back_sector.floor_texture ||
|
|
1046
|
-
sector.light_level != back_sector.light_level
|
|
1047
|
-
end
|
|
1048
|
-
|
|
1049
|
-
# Mark ceiling visplane
|
|
1050
|
-
if @current_ceiling_plane && should_mark_ceiling
|
|
1051
|
-
mark_top = @ceiling_clip[x] + 1
|
|
1052
|
-
mark_bottom = ceil_y - 1
|
|
1053
|
-
mark_bottom = [@floor_clip[x] - 1, mark_bottom].min
|
|
1054
|
-
if mark_top <= mark_bottom
|
|
1055
|
-
@current_ceiling_plane.mark(x, mark_top, mark_bottom)
|
|
1056
|
-
end
|
|
1057
|
-
end
|
|
1058
|
-
|
|
1059
|
-
# Mark floor visplane
|
|
1060
|
-
if @current_floor_plane && should_mark_floor
|
|
1061
|
-
mark_top = floor_y + 1
|
|
1062
|
-
mark_bottom = @floor_clip[x] - 1
|
|
1063
|
-
mark_top = [@ceiling_clip[x] + 1, mark_top].max
|
|
1064
|
-
if mark_top <= mark_bottom
|
|
1065
|
-
@current_floor_plane.mark(x, mark_top, mark_bottom)
|
|
1066
|
-
end
|
|
1067
|
-
end
|
|
1068
|
-
|
|
1069
|
-
# Upper wall (ceiling step down) - skip if both sectors have sky
|
|
1070
|
-
if !both_sky && sector.ceiling_height > back_sector.ceiling_height
|
|
1071
|
-
if linedef.upper_unpegged?
|
|
1072
|
-
upper_tex_y = sidedef.y_offset
|
|
1073
|
-
else
|
|
1074
|
-
texture = @textures[sidedef.upper_texture]
|
|
1075
|
-
tex_height = texture ? texture.height : 128
|
|
1076
|
-
upper_tex_y = sidedef.y_offset + back_sector.ceiling_height - sector.ceiling_height + tex_height
|
|
1077
|
-
end
|
|
1078
|
-
draw_wall_column_ex(x, ceil_y, back_ceil_y, sidedef.upper_texture, dist,
|
|
1079
|
-
sector.light_level, tex_col, upper_tex_y, scale, sector.ceiling_height, back_sector.ceiling_height)
|
|
1080
|
-
end
|
|
1081
|
-
|
|
1082
|
-
# Lower wall (floor step up)
|
|
1083
|
-
if sector.floor_height < back_sector.floor_height
|
|
1084
|
-
if linedef.lower_unpegged?
|
|
1085
|
-
lower_tex_y = sidedef.y_offset + sector.ceiling_height - back_sector.floor_height
|
|
1086
|
-
else
|
|
1087
|
-
lower_tex_y = sidedef.y_offset
|
|
1088
|
-
end
|
|
1089
|
-
draw_wall_column_ex(x, back_floor_y, floor_y, sidedef.lower_texture, dist,
|
|
1090
|
-
sector.light_level, tex_col, lower_tex_y, scale, back_sector.floor_height, sector.floor_height)
|
|
1091
|
-
end
|
|
1092
|
-
|
|
1093
|
-
# Store masked texture column for deferred rendering (grates, bars)
|
|
1094
|
-
if @current_masked_cols
|
|
1095
|
-
@current_masked_cols[x - x1] = tex_col
|
|
1096
|
-
end
|
|
1097
|
-
|
|
1098
|
-
# Update clip bounds
|
|
1099
|
-
if closed_door
|
|
1100
|
-
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
1101
|
-
@ceiling_clip[x] = SCREEN_HEIGHT
|
|
1102
|
-
@floor_clip[x] = -1
|
|
1103
|
-
else
|
|
1104
|
-
# Ceiling clip (uses effective_front_ceil for sky hack)
|
|
1105
|
-
if effective_front_ceil > back_sector.ceiling_height
|
|
1106
|
-
@ceiling_clip[x] = [back_ceil_y, @ceiling_clip[x]].max
|
|
1107
|
-
elsif effective_front_ceil < back_sector.ceiling_height
|
|
1108
|
-
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1109
|
-
elsif sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1110
|
-
sector.light_level != back_sector.light_level
|
|
1111
|
-
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1112
|
-
end
|
|
1113
|
-
|
|
1114
|
-
# Floor clip
|
|
1115
|
-
if sector.floor_height < back_sector.floor_height
|
|
1116
|
-
@floor_clip[x] = [back_floor_y, @floor_clip[x]].min
|
|
1117
|
-
elsif sector.floor_height > back_sector.floor_height
|
|
1118
|
-
# Chocolate Doom: else if (markfloor) floorclip = yh + 1
|
|
1119
|
-
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1120
|
-
elsif sector.floor_texture != back_sector.floor_texture ||
|
|
1121
|
-
sector.light_level != back_sector.light_level
|
|
1122
|
-
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1123
|
-
end
|
|
1124
|
-
end
|
|
1061
|
+
draw_two_sided_seg_column(x, x1, ceil_y, floor_y, scale, dist, tex_col,
|
|
1062
|
+
sector, back_sector, sidedef, linedef)
|
|
1125
1063
|
else
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
# Clamp to floor_clip to prevent ceiling bleeding through portal openings
|
|
1129
|
-
if @current_ceiling_plane
|
|
1130
|
-
mark_top = @ceiling_clip[x] + 1
|
|
1131
|
-
mark_bottom = [ceil_y - 1, @floor_clip[x] - 1].min
|
|
1132
|
-
if mark_top <= mark_bottom
|
|
1133
|
-
@current_ceiling_plane.mark(x, mark_top, mark_bottom)
|
|
1134
|
-
end
|
|
1135
|
-
end
|
|
1136
|
-
|
|
1137
|
-
# Mark floor visplane (from wall's floor to previous floor clip)
|
|
1138
|
-
# Clamp to ceiling_clip to prevent floor bleeding through portal openings
|
|
1139
|
-
if @current_floor_plane
|
|
1140
|
-
mark_top = [floor_y + 1, @ceiling_clip[x] + 1].max
|
|
1141
|
-
mark_bottom = @floor_clip[x] - 1
|
|
1142
|
-
if mark_top <= mark_bottom
|
|
1143
|
-
@current_floor_plane.mark(x, mark_top, mark_bottom)
|
|
1144
|
-
end
|
|
1145
|
-
end
|
|
1146
|
-
|
|
1147
|
-
# Draw wall (from clipped ceiling to clipped floor)
|
|
1148
|
-
# Middle texture Y offset depends on DONTPEGBOTTOM flag
|
|
1149
|
-
# With DONTPEGBOTTOM: texture bottom aligns with floor
|
|
1150
|
-
# Without: texture top aligns with ceiling
|
|
1151
|
-
if linedef.lower_unpegged?
|
|
1152
|
-
texture = @textures[sidedef.middle_texture]
|
|
1153
|
-
tex_height = texture ? texture.height : 128
|
|
1154
|
-
mid_tex_y = sidedef.y_offset + tex_height - (sector.ceiling_height - sector.floor_height)
|
|
1155
|
-
else
|
|
1156
|
-
mid_tex_y = sidedef.y_offset
|
|
1157
|
-
end
|
|
1158
|
-
draw_wall_column_ex(x, ceil_y, floor_y, sidedef.middle_texture, dist,
|
|
1159
|
-
sector.light_level, tex_col, mid_tex_y, scale, sector.ceiling_height, sector.floor_height)
|
|
1160
|
-
|
|
1161
|
-
# Track wall depth for sprite clipping (solid wall occludes this column)
|
|
1162
|
-
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
1163
|
-
|
|
1164
|
-
# Fully occluded
|
|
1165
|
-
@ceiling_clip[x] = SCREEN_HEIGHT
|
|
1166
|
-
@floor_clip[x] = -1
|
|
1064
|
+
draw_one_sided_seg_column(x, ceil_y, floor_y, scale, dist, tex_col,
|
|
1065
|
+
sector, sidedef, linedef)
|
|
1167
1066
|
end
|
|
1168
1067
|
end
|
|
1169
1068
|
|
|
@@ -1171,7 +1070,7 @@ module Doom
|
|
|
1171
1070
|
if silhouette != SIL_NONE || has_masked
|
|
1172
1071
|
sprtopclip = @ceiling_clip[x1..x2].dup
|
|
1173
1072
|
sprbottomclip = @floor_clip[x1..x2].dup
|
|
1174
|
-
scalestep =
|
|
1073
|
+
scalestep = x2 > x1 ? (scale2 - scale1) / (x2 - x1) : 0
|
|
1175
1074
|
|
|
1176
1075
|
drawseg = Drawseg.new(
|
|
1177
1076
|
x1, x2,
|
|
@@ -1189,127 +1088,173 @@ module Doom
|
|
|
1189
1088
|
@current_masked_cols = nil
|
|
1190
1089
|
end
|
|
1191
1090
|
|
|
1192
|
-
#
|
|
1193
|
-
#
|
|
1194
|
-
#
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
#
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
#
|
|
1218
|
-
|
|
1091
|
+
# One screen column of a two-sided seg: mark the ceiling/floor visplanes
|
|
1092
|
+
# of the opening between the two sectors, draw the upper and lower wall
|
|
1093
|
+
# steps, remember the masked-middle column, and tighten the clip bounds.
|
|
1094
|
+
def draw_two_sided_seg_column(x, x1, ceil_y, floor_y, scale, dist, tex_col,
|
|
1095
|
+
sector, back_sector, sidedef, linedef)
|
|
1096
|
+
back_floor = back_sector.floor_height - @player_z
|
|
1097
|
+
back_ceil = back_sector.ceiling_height - @player_z
|
|
1098
|
+
|
|
1099
|
+
# Chocolate Doom rounding:
|
|
1100
|
+
# pixhigh>>HEIGHTBITS (truncate for back ceiling / upper wall end)
|
|
1101
|
+
# (pixlow+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for back floor / lower wall start)
|
|
1102
|
+
back_ceil_y = (HALF_HEIGHT - (back_ceil * scale)).to_i
|
|
1103
|
+
back_floor_y = (HALF_HEIGHT - (back_floor * scale)).ceil
|
|
1104
|
+
|
|
1105
|
+
# Check for closed door (no opening between sectors)
|
|
1106
|
+
# Matches Chocolate Doom: backsector->ceilingheight <= frontsector->floorheight
|
|
1107
|
+
# || backsector->floorheight >= frontsector->ceilingheight
|
|
1108
|
+
closed_door = back_sector.ceiling_height <= sector.floor_height ||
|
|
1109
|
+
back_sector.floor_height >= sector.ceiling_height
|
|
1110
|
+
|
|
1111
|
+
both_sky = sector.ceiling_texture == 'F_SKY1' && back_sector.ceiling_texture == 'F_SKY1'
|
|
1112
|
+
|
|
1113
|
+
# Determine whether to mark ceiling/floor visplanes.
|
|
1114
|
+
# Match Chocolate Doom: only mark when properties differ, plus force
|
|
1115
|
+
# for closed doors. The background fill covers same-property gaps.
|
|
1116
|
+
# Chocolate Doom sky hack: "worldtop = worldhigh" makes the front
|
|
1117
|
+
# ceiling equal to the back ceiling when both are sky. This prevents
|
|
1118
|
+
# the low sky ceiling from clipping walls in adjacent sectors.
|
|
1119
|
+
effective_front_ceil = both_sky ? back_sector.ceiling_height : sector.ceiling_height
|
|
1120
|
+
|
|
1121
|
+
if closed_door
|
|
1122
|
+
should_mark_ceiling = true
|
|
1123
|
+
should_mark_floor = true
|
|
1124
|
+
else
|
|
1125
|
+
should_mark_ceiling = effective_front_ceil != back_sector.ceiling_height ||
|
|
1126
|
+
sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1127
|
+
sector.light_level != back_sector.light_level
|
|
1128
|
+
should_mark_floor = sector.floor_height != back_sector.floor_height ||
|
|
1129
|
+
sector.floor_texture != back_sector.floor_texture ||
|
|
1130
|
+
sector.light_level != back_sector.light_level
|
|
1131
|
+
end
|
|
1219
1132
|
|
|
1220
|
-
#
|
|
1221
|
-
|
|
1222
|
-
|
|
1133
|
+
# Mark ceiling visplane
|
|
1134
|
+
if @current_ceiling_plane && should_mark_ceiling
|
|
1135
|
+
mark_top = @ceiling_clip[x] + 1
|
|
1136
|
+
mark_bottom = ceil_y - 1
|
|
1137
|
+
mark_bottom = [@floor_clip[x] - 1, mark_bottom].min
|
|
1138
|
+
@current_ceiling_plane.mark(x, mark_top, mark_bottom) if mark_top <= mark_bottom
|
|
1139
|
+
end
|
|
1223
1140
|
|
|
1224
|
-
#
|
|
1225
|
-
|
|
1141
|
+
# Mark floor visplane
|
|
1142
|
+
if @current_floor_plane && should_mark_floor
|
|
1143
|
+
mark_top = floor_y + 1
|
|
1144
|
+
mark_bottom = @floor_clip[x] - 1
|
|
1145
|
+
mark_top = [@ceiling_clip[x] + 1, mark_top].max
|
|
1146
|
+
@current_floor_plane.mark(x, mark_top, mark_bottom) if mark_top <= mark_bottom
|
|
1147
|
+
end
|
|
1226
1148
|
|
|
1227
|
-
#
|
|
1228
|
-
|
|
1149
|
+
# Upper wall (ceiling step down) - skip if both sectors have sky
|
|
1150
|
+
if !both_sky && sector.ceiling_height > back_sector.ceiling_height
|
|
1151
|
+
if linedef.upper_unpegged?
|
|
1152
|
+
upper_tex_y = sidedef.y_offset
|
|
1153
|
+
else
|
|
1154
|
+
texture = @textures[sidedef.upper_texture]
|
|
1155
|
+
tex_height = texture ? texture.height : 128
|
|
1156
|
+
upper_tex_y = sidedef.y_offset + back_sector.ceiling_height - sector.ceiling_height + tex_height
|
|
1157
|
+
end
|
|
1158
|
+
draw_wall_column_ex(x, ceil_y, back_ceil_y, sidedef.upper_texture, dist,
|
|
1159
|
+
sector.light_level, tex_col, upper_tex_y, scale, sector.ceiling_height)
|
|
1160
|
+
end
|
|
1229
1161
|
|
|
1230
|
-
#
|
|
1231
|
-
|
|
1162
|
+
# Lower wall (floor step up)
|
|
1163
|
+
if sector.floor_height < back_sector.floor_height
|
|
1164
|
+
lower_tex_y = if linedef.lower_unpegged?
|
|
1165
|
+
sidedef.y_offset + sector.ceiling_height - back_sector.floor_height
|
|
1166
|
+
else
|
|
1167
|
+
sidedef.y_offset
|
|
1168
|
+
end
|
|
1169
|
+
draw_wall_column_ex(x, back_floor_y, floor_y, sidedef.lower_texture, dist,
|
|
1170
|
+
sector.light_level, tex_col, lower_tex_y, scale, back_sector.floor_height)
|
|
1171
|
+
end
|
|
1232
1172
|
|
|
1233
|
-
#
|
|
1234
|
-
|
|
1235
|
-
y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT
|
|
1173
|
+
# Store masked texture column for deferred rendering (grates, bars)
|
|
1174
|
+
@current_masked_cols[x - x1] = tex_col if @current_masked_cols
|
|
1236
1175
|
|
|
1237
|
-
#
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1176
|
+
# Update clip bounds
|
|
1177
|
+
if closed_door
|
|
1178
|
+
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
1179
|
+
@ceiling_clip[x] = SCREEN_HEIGHT
|
|
1180
|
+
@floor_clip[x] = -1
|
|
1181
|
+
else
|
|
1182
|
+
# Ceiling clip (uses effective_front_ceil for sky hack)
|
|
1183
|
+
if effective_front_ceil > back_sector.ceiling_height
|
|
1184
|
+
@ceiling_clip[x] = [back_ceil_y, @ceiling_clip[x]].max
|
|
1185
|
+
elsif effective_front_ceil < back_sector.ceiling_height
|
|
1186
|
+
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1187
|
+
elsif sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1188
|
+
sector.light_level != back_sector.light_level
|
|
1189
|
+
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1190
|
+
end
|
|
1242
1191
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1192
|
+
# Floor clip
|
|
1193
|
+
if sector.floor_height < back_sector.floor_height
|
|
1194
|
+
@floor_clip[x] = [back_floor_y, @floor_clip[x]].min
|
|
1195
|
+
elsif sector.floor_height > back_sector.floor_height
|
|
1196
|
+
# Chocolate Doom: else if (markfloor) floorclip = yh + 1
|
|
1197
|
+
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1198
|
+
elsif sector.floor_texture != back_sector.floor_texture ||
|
|
1199
|
+
sector.light_level != back_sector.light_level
|
|
1200
|
+
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1201
|
+
end
|
|
1246
1202
|
end
|
|
1247
1203
|
end
|
|
1248
1204
|
|
|
1249
|
-
#
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1205
|
+
# One screen column of a one-sided (solid) wall: mark the ceiling and
|
|
1206
|
+
# floor visplanes down to the wall, draw the full-height middle texture,
|
|
1207
|
+
# and fully occlude the column for anything behind it.
|
|
1208
|
+
def draw_one_sided_seg_column(x, ceil_y, floor_y, scale, dist, tex_col,
|
|
1209
|
+
sector, sidedef, linedef)
|
|
1210
|
+
# Mark ceiling visplane (from previous clip to wall's ceiling)
|
|
1211
|
+
# Clamp to floor_clip to prevent ceiling bleeding through portal openings
|
|
1212
|
+
if @current_ceiling_plane
|
|
1213
|
+
mark_top = @ceiling_clip[x] + 1
|
|
1214
|
+
mark_bottom = [ceil_y - 1, @floor_clip[x] - 1].min
|
|
1215
|
+
@current_ceiling_plane.mark(x, mark_top, mark_bottom) if mark_top <= mark_bottom
|
|
1257
1216
|
end
|
|
1258
|
-
end
|
|
1259
1217
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
# AND final sprite clips (to prevent far grates drawing over near walls)
|
|
1268
|
-
# Take the tightest combination of both
|
|
1269
|
-
y1 = [md[:clip_top] + 1, @sprite_ceiling_clip[x] + 1, md[:y1]].max
|
|
1270
|
-
y2 = [md[:clip_bottom] - 1, @sprite_floor_clip[x] - 1, md[:y2]].min
|
|
1271
|
-
return if y1 > y2
|
|
1272
|
-
|
|
1273
|
-
# Depth test: far grate behind a near solid wall
|
|
1274
|
-
return if md[:dist] > @sprite_wall_depth[x]
|
|
1275
|
-
|
|
1276
|
-
texture = @textures[anim_texture(tex_name)]
|
|
1277
|
-
return unless texture
|
|
1278
|
-
|
|
1279
|
-
light = calculate_light(md[:light], md[:dist])
|
|
1280
|
-
cmap = @colormap.maps[light]
|
|
1281
|
-
framebuffer = @framebuffer
|
|
1282
|
-
tex_width = texture.width
|
|
1283
|
-
tex_height = texture.height
|
|
1284
|
-
|
|
1285
|
-
tex_x = md[:tex_col].to_i % tex_width
|
|
1286
|
-
column = texture.column_pixels(tex_x)
|
|
1287
|
-
return unless column
|
|
1218
|
+
# Mark floor visplane (from wall's floor to previous floor clip)
|
|
1219
|
+
# Clamp to ceiling_clip to prevent floor bleeding through portal openings
|
|
1220
|
+
if @current_floor_plane
|
|
1221
|
+
mark_top = [floor_y + 1, @ceiling_clip[x] + 1].max
|
|
1222
|
+
mark_bottom = @floor_clip[x] - 1
|
|
1223
|
+
@current_floor_plane.mark(x, mark_top, mark_bottom) if mark_top <= mark_bottom
|
|
1224
|
+
end
|
|
1288
1225
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1226
|
+
# Draw wall (from clipped ceiling to clipped floor)
|
|
1227
|
+
# Middle texture Y offset depends on DONTPEGBOTTOM flag
|
|
1228
|
+
# With DONTPEGBOTTOM: texture bottom aligns with floor
|
|
1229
|
+
# Without: texture top aligns with ceiling
|
|
1230
|
+
if linedef.lower_unpegged?
|
|
1231
|
+
texture = @textures[sidedef.middle_texture]
|
|
1232
|
+
tex_height = texture ? texture.height : 128
|
|
1233
|
+
mid_tex_y = sidedef.y_offset + tex_height - (sector.ceiling_height - sector.floor_height)
|
|
1234
|
+
else
|
|
1235
|
+
mid_tex_y = sidedef.y_offset
|
|
1236
|
+
end
|
|
1237
|
+
draw_wall_column_ex(x, ceil_y, floor_y, sidedef.middle_texture, dist,
|
|
1238
|
+
sector.light_level, tex_col, mid_tex_y, scale, sector.ceiling_height)
|
|
1293
1239
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1240
|
+
# Track wall depth for sprite clipping (solid wall occludes this column)
|
|
1241
|
+
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
1296
1242
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i % tex_height
|
|
1301
|
-
color = column[tex_y]
|
|
1302
|
-
framebuffer[y * SCREEN_WIDTH + x] = cmap[color] if color
|
|
1303
|
-
y += 1
|
|
1304
|
-
end
|
|
1243
|
+
# Fully occluded
|
|
1244
|
+
@ceiling_clip[x] = SCREEN_HEIGHT
|
|
1245
|
+
@floor_clip[x] = -1
|
|
1305
1246
|
end
|
|
1306
1247
|
|
|
1307
|
-
#
|
|
1308
|
-
#
|
|
1309
|
-
|
|
1248
|
+
# Wall column drawing with proper texture mapping
|
|
1249
|
+
# tex_col: texture column (X coordinate in texture)
|
|
1250
|
+
# tex_y_start: starting Y coordinate in texture (accounts for pegging)
|
|
1251
|
+
# scale: projection scale for this column (projection / distance)
|
|
1252
|
+
# world_top: world height of the top of this wall section
|
|
1253
|
+
def draw_wall_column_ex(x, y1, y2, texture_name, dist, light_level, tex_col, tex_y_start, scale, world_top)
|
|
1310
1254
|
return if y1 > y2
|
|
1311
1255
|
return if texture_name.nil? || texture_name.empty? || texture_name == '-'
|
|
1312
1256
|
|
|
1257
|
+
# Clip to visible range (ceiling_clip/floor_clip)
|
|
1313
1258
|
clip_top = @ceiling_clip[x] + 1
|
|
1314
1259
|
clip_bottom = @floor_clip[x] - 1
|
|
1315
1260
|
y1 = [y1, clip_top].max
|
|
@@ -1325,26 +1270,34 @@ module Doom
|
|
|
1325
1270
|
tex_width = texture.width
|
|
1326
1271
|
tex_height = texture.height
|
|
1327
1272
|
|
|
1273
|
+
# Texture X coordinate (wrap around texture width)
|
|
1328
1274
|
tex_x = tex_col.to_i % tex_width
|
|
1275
|
+
|
|
1276
|
+
# Get the column of pixels
|
|
1329
1277
|
column = texture.column_pixels(tex_x)
|
|
1330
1278
|
return unless column
|
|
1331
1279
|
|
|
1280
|
+
# Texture step per screen pixel
|
|
1332
1281
|
tex_step = 1.0 / scale
|
|
1333
|
-
unclipped_y1 = HALF_HEIGHT - (world_top - @player_z) * scale
|
|
1334
|
-
tex_y_at_y1 = tex_y_start + (y1 - unclipped_y1) * tex_step
|
|
1335
1282
|
|
|
1283
|
+
# Calculate where the unclipped wall top would be on screen
|
|
1284
|
+
unclipped_y1 = HALF_HEIGHT - ((world_top - @player_z) * scale)
|
|
1285
|
+
|
|
1286
|
+
# Adjust tex_y_start for any clipping at the top
|
|
1287
|
+
tex_y_at_y1 = tex_y_start + ((y1 - unclipped_y1) * tex_step)
|
|
1288
|
+
|
|
1289
|
+
# Clamp to screen bounds
|
|
1336
1290
|
y1 = 0 if y1 < 0
|
|
1337
1291
|
y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT
|
|
1338
1292
|
|
|
1293
|
+
# Draw wall column using while loop
|
|
1339
1294
|
y = y1
|
|
1340
1295
|
while y <= y2
|
|
1341
1296
|
screen_offset = y - y1
|
|
1342
|
-
tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i % tex_height
|
|
1297
|
+
tex_y = (tex_y_at_y1 + (screen_offset * tex_step)).to_i % tex_height
|
|
1298
|
+
|
|
1343
1299
|
color = column[tex_y]
|
|
1344
|
-
|
|
1345
|
-
if color
|
|
1346
|
-
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
1347
|
-
end
|
|
1300
|
+
framebuffer[(y * SCREEN_WIDTH) + x] = cmap[color] if color
|
|
1348
1301
|
y += 1
|
|
1349
1302
|
end
|
|
1350
1303
|
end
|
|
@@ -1442,7 +1395,7 @@ module Doom
|
|
|
1442
1395
|
# Get sprite: death frame > walking frame > idle frame
|
|
1443
1396
|
if @combat && @combat.dead?(thing_idx)
|
|
1444
1397
|
sprite = @combat.death_sprite(thing_idx, thing.type, angle_to_thing, thing.angle)
|
|
1445
|
-
next unless sprite
|
|
1398
|
+
next unless sprite # Barrel disappeared after explosion
|
|
1446
1399
|
elsif @monster_ai
|
|
1447
1400
|
mon = @monster_ai.monster_by_thing_idx[thing_idx]
|
|
1448
1401
|
if mon && mon.active
|
|
@@ -1477,6 +1430,8 @@ module Doom
|
|
|
1477
1430
|
visible_sprites << VisibleSprite.new(thing, sprite, view_x, view_y, dist, screen_x)
|
|
1478
1431
|
end
|
|
1479
1432
|
|
|
1433
|
+
collect_player_sprites(visible_sprites)
|
|
1434
|
+
|
|
1480
1435
|
# Sort by distance (back to front for proper overdraw)
|
|
1481
1436
|
visible_sprites.sort_by! { |s| -s.dist }
|
|
1482
1437
|
|
|
@@ -1488,14 +1443,15 @@ module Doom
|
|
|
1488
1443
|
# Draw remaining masked segs not triggered by sprites
|
|
1489
1444
|
@drawsegs.reverse_each do |ds|
|
|
1490
1445
|
next unless ds.maskedtexturecol
|
|
1446
|
+
|
|
1491
1447
|
render_masked_seg_range(ds, ds.x1, ds.x2)
|
|
1492
1448
|
end
|
|
1493
1449
|
|
|
1494
1450
|
# Draw projectiles, explosions, and bullet puffs
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1451
|
+
return unless @combat
|
|
1452
|
+
|
|
1453
|
+
render_projectiles
|
|
1454
|
+
render_puffs
|
|
1499
1455
|
end
|
|
1500
1456
|
|
|
1501
1457
|
# Chocolate Doom R_DrawMasked: interleave sprites with masked drawsegs
|
|
@@ -1503,6 +1459,60 @@ module Doom
|
|
|
1503
1459
|
render_sprites
|
|
1504
1460
|
end
|
|
1505
1461
|
|
|
1462
|
+
# Other players, drawn with the PLAY sprites. They join the same
|
|
1463
|
+
# distance-sorted list as map things rather than being drawn afterwards,
|
|
1464
|
+
# so they occlude and are occluded correctly.
|
|
1465
|
+
PLAYER_SPRITE_PREFIX = 'PLAY'
|
|
1466
|
+
PLAYER_WALK_FRAMES = %w[A B C D].freeze
|
|
1467
|
+
PLAYER_DEATH_FRAMES = %w[H I J K L M N].freeze
|
|
1468
|
+
PLAYER_DEATH_FRAME_TICS = 6
|
|
1469
|
+
PLAYER_ATTACK_FRAME = 'E'
|
|
1470
|
+
PLAYER_MOVING_SPEED = 1.0 # units/sec below which a player reads as still
|
|
1471
|
+
|
|
1472
|
+
def collect_player_sprites(visible_sprites)
|
|
1473
|
+
return if @players.nil? || @players.empty?
|
|
1474
|
+
|
|
1475
|
+
@players.each do |player|
|
|
1476
|
+
# Never draw the player whose eyes we are looking through.
|
|
1477
|
+
next if player.equal?(@view_player)
|
|
1478
|
+
|
|
1479
|
+
view_x, view_y = transform_point(player.x, player.y)
|
|
1480
|
+
next if view_y <= 0
|
|
1481
|
+
|
|
1482
|
+
angle_to_player = Math.atan2(player.y - @player_y, player.x - @player_x)
|
|
1483
|
+
sprite = player_sprite(player, angle_to_player)
|
|
1484
|
+
next unless sprite
|
|
1485
|
+
|
|
1486
|
+
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1487
|
+
sprite_half_width = (sprite.width * @projection / view_y / 2).to_i
|
|
1488
|
+
next if screen_x + sprite_half_width < 0
|
|
1489
|
+
next if screen_x - sprite_half_width >= SCREEN_WIDTH
|
|
1490
|
+
|
|
1491
|
+
# z is the player's eye height; the sprite is anchored at the feet.
|
|
1492
|
+
feet_z = player.z - Game::PlayerState::VIEWHEIGHT
|
|
1493
|
+
stub = ProjectileStub.new(player.x, player.y, 0, 0, 0, feet_z)
|
|
1494
|
+
visible_sprites << VisibleSprite.new(stub, sprite, view_x, view_y, view_y, screen_x)
|
|
1495
|
+
end
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1498
|
+
def player_sprite(player, angle_to_player)
|
|
1499
|
+
state = player.state
|
|
1500
|
+
facing = player.angle * 180.0 / Math::PI
|
|
1501
|
+
|
|
1502
|
+
if state.dead
|
|
1503
|
+
idx = (state.death_tic / PLAYER_DEATH_FRAME_TICS).clamp(0, PLAYER_DEATH_FRAMES.size - 1)
|
|
1504
|
+
frame = PLAYER_DEATH_FRAMES[idx]
|
|
1505
|
+
elsif state.attacking
|
|
1506
|
+
frame = PLAYER_ATTACK_FRAME
|
|
1507
|
+
elsif Math.hypot(player.momx, player.momy) > PLAYER_MOVING_SPEED
|
|
1508
|
+
frame = PLAYER_WALK_FRAMES[@leveltime / 4 % PLAYER_WALK_FRAMES.size]
|
|
1509
|
+
else
|
|
1510
|
+
frame = PLAYER_WALK_FRAMES.first
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
@sprites.get_frame_rotated(PLAYER_SPRITE_PREFIX, frame, angle_to_player, facing)
|
|
1514
|
+
end
|
|
1515
|
+
|
|
1506
1516
|
# Draw sprite, rendering masked drawsegs behind it first (R_DrawSprite)
|
|
1507
1517
|
def draw_sprite_with_masking(spr)
|
|
1508
1518
|
sprite_scale = @projection / spr.dist
|
|
@@ -1513,18 +1523,18 @@ module Doom
|
|
|
1513
1523
|
next if ds.silhouette == SIL_NONE && ds.maskedtexturecol.nil?
|
|
1514
1524
|
|
|
1515
1525
|
ds_max_scale = [ds.scale1, ds.scale2].max
|
|
1516
|
-
|
|
1526
|
+
[ds.scale1, ds.scale2].min
|
|
1517
1527
|
|
|
1518
1528
|
# Is the drawseg behind the sprite?
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
next # Don't clip against things behind
|
|
1529
|
+
next unless ds_max_scale < sprite_scale
|
|
1530
|
+
|
|
1531
|
+
# Draw masked texture behind the sprite
|
|
1532
|
+
if ds.maskedtexturecol
|
|
1533
|
+
r1 = [ds.x1, spr.screen_x.to_i - spr.sprite.width].max
|
|
1534
|
+
r2 = [ds.x2, spr.screen_x.to_i + spr.sprite.width].min
|
|
1535
|
+
render_masked_seg_range(ds, r1, r2)
|
|
1527
1536
|
end
|
|
1537
|
+
next # Don't clip against things behind
|
|
1528
1538
|
end
|
|
1529
1539
|
|
|
1530
1540
|
draw_sprite(spr)
|
|
@@ -1566,22 +1576,23 @@ module Doom
|
|
|
1566
1576
|
next if col_idx < 0 || col_idx >= ds.maskedtexturecol.size
|
|
1567
1577
|
|
|
1568
1578
|
tex_col = ds.maskedtexturecol[col_idx]
|
|
1569
|
-
next unless tex_col
|
|
1579
|
+
next unless tex_col # nil = already drawn or empty
|
|
1570
1580
|
|
|
1571
1581
|
# Per-column distance using same 1/z interpolation as wall renderer
|
|
1572
1582
|
if ds.dist1 && ds.dist2 && ds.dist1 > 0 && ds.dist2 > 0 && ds.sx2 != ds.sx1
|
|
1573
1583
|
t = ((x - ds.sx1) / (ds.sx2 - ds.sx1)).clamp(0.0, 1.0)
|
|
1574
|
-
inv_dist = (1.0 - t) / ds.dist1 + t / ds.dist2
|
|
1584
|
+
inv_dist = ((1.0 - t) / ds.dist1) + (t / ds.dist2)
|
|
1575
1585
|
col_dist = 1.0 / inv_dist
|
|
1576
1586
|
else
|
|
1577
1587
|
col_dist = ds.dist1 || 1.0
|
|
1578
1588
|
end
|
|
1579
1589
|
next if col_dist < 1
|
|
1590
|
+
|
|
1580
1591
|
spryscale = @projection / col_dist
|
|
1581
1592
|
spryscale = [spryscale, 64.0].min
|
|
1582
1593
|
|
|
1583
1594
|
# Screen Y of texture top
|
|
1584
|
-
sprtopscreen = HALF_HEIGHT - dc_texturemid * spryscale
|
|
1595
|
+
sprtopscreen = HALF_HEIGHT - (dc_texturemid * spryscale)
|
|
1585
1596
|
|
|
1586
1597
|
# Get clip bounds from drawseg
|
|
1587
1598
|
clip_idx = x - ds.x1
|
|
@@ -1604,23 +1615,23 @@ module Doom
|
|
|
1604
1615
|
(0..tex_height).each do |ty|
|
|
1605
1616
|
color = ty < tex_height ? column[ty] : nil
|
|
1606
1617
|
if color
|
|
1607
|
-
y_start
|
|
1618
|
+
y_start ||= ty
|
|
1608
1619
|
elsif y_start
|
|
1609
1620
|
# Draw run from y_start to ty-1
|
|
1610
|
-
top_y = (sprtopscreen + y_start * spryscale).to_i
|
|
1611
|
-
bot_y = (sprtopscreen + ty * spryscale).to_i - 1
|
|
1621
|
+
top_y = (sprtopscreen + (y_start * spryscale)).to_i
|
|
1622
|
+
bot_y = (sprtopscreen + (ty * spryscale)).to_i - 1
|
|
1612
1623
|
top_y = [top_y, mceilingclip + 1].max
|
|
1613
1624
|
bot_y = [bot_y, mfloorclip - 1].min
|
|
1614
1625
|
|
|
1615
1626
|
if top_y <= bot_y
|
|
1616
1627
|
top_y = [top_y, 0].max
|
|
1617
1628
|
bot_y = [bot_y, SCREEN_HEIGHT - 1].min
|
|
1618
|
-
tex_frac = y_start + (top_y - (sprtopscreen + y_start * spryscale)) * iscale
|
|
1629
|
+
tex_frac = y_start + ((top_y - (sprtopscreen + (y_start * spryscale))) * iscale)
|
|
1619
1630
|
y = top_y
|
|
1620
1631
|
while y <= bot_y
|
|
1621
|
-
t = (
|
|
1632
|
+
t = (tex_frac.to_i % tex_height)
|
|
1622
1633
|
c = column[t]
|
|
1623
|
-
@framebuffer[y * SCREEN_WIDTH + x] = cmap[c] if c
|
|
1634
|
+
@framebuffer[(y * SCREEN_WIDTH) + x] = cmap[c] if c
|
|
1624
1635
|
tex_frac += iscale
|
|
1625
1636
|
y += 1
|
|
1626
1637
|
end
|
|
@@ -1649,7 +1660,7 @@ module Doom
|
|
|
1649
1660
|
rocket_angle = Math.atan2(proj.dy, proj.dx)
|
|
1650
1661
|
viewer_angle = Math.atan2(proj.y - @player_y, proj.x - @player_x)
|
|
1651
1662
|
angle_diff = (viewer_angle - rocket_angle) % (2 * Math::PI)
|
|
1652
|
-
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
1663
|
+
rotation = (((angle_diff + (Math::PI / 8)) / (Math::PI / 4)).to_i % 8) + 1
|
|
1653
1664
|
|
|
1654
1665
|
# Animate flight: cycle A/B frames
|
|
1655
1666
|
flight_frame = %w[A B][@leveltime / 4 % 2]
|
|
@@ -1668,14 +1679,16 @@ module Doom
|
|
|
1668
1679
|
@combat.explosions.each do |expl|
|
|
1669
1680
|
view_x, view_y = transform_point(expl[:x], expl[:y])
|
|
1670
1681
|
next if view_y <= 0
|
|
1682
|
+
|
|
1671
1683
|
elapsed = (@combat.instance_variable_get(:@tic) - expl[:tic])
|
|
1672
1684
|
frame_idx = (elapsed / 4).clamp(0, 2)
|
|
1673
1685
|
|
|
1674
1686
|
prefix = expl[:sprite] || 'MISL'
|
|
1675
|
-
frame_letter =
|
|
1687
|
+
frame_letter = prefix == 'MISL' ? %w[B C D][frame_idx] : %w[C D E][frame_idx]
|
|
1676
1688
|
|
|
1677
1689
|
expl_sprite = @sprites.send(:load_sprite_frame, prefix, frame_letter, 0)
|
|
1678
1690
|
next unless expl_sprite
|
|
1691
|
+
|
|
1679
1692
|
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1680
1693
|
expl_z = expl[:z] || @player_z
|
|
1681
1694
|
stub = ProjectileStub.new(expl[:x], expl[:y], 0, 0, 0, expl_z)
|
|
@@ -1688,11 +1701,13 @@ module Doom
|
|
|
1688
1701
|
@combat.puffs.each do |puff|
|
|
1689
1702
|
view_x, view_y = transform_point(puff[:x], puff[:y])
|
|
1690
1703
|
next if view_y <= 0
|
|
1704
|
+
|
|
1691
1705
|
elapsed = @combat.instance_variable_get(:@tic) - puff[:tic]
|
|
1692
1706
|
frame_idx = (elapsed / 3).clamp(0, 3)
|
|
1693
1707
|
frame_letter = %w[A B C D][frame_idx]
|
|
1694
1708
|
puff_sprite = @sprites.send(:load_sprite_frame, 'PUFF', frame_letter, 0)
|
|
1695
1709
|
next unless puff_sprite
|
|
1710
|
+
|
|
1696
1711
|
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1697
1712
|
stub = ProjectileStub.new(puff[:x], puff[:y], 0, 0, 0, puff[:z])
|
|
1698
1713
|
visible = VisibleSprite.new(stub, puff_sprite, view_x, view_y, view_y, screen_x)
|
|
@@ -1721,7 +1736,7 @@ module Doom
|
|
|
1721
1736
|
light = calculate_light(light_level, dist)
|
|
1722
1737
|
|
|
1723
1738
|
# Sprite screen bounds
|
|
1724
|
-
sprite_left = (screen_x - sprite.left_offset * sprite_scale).to_i
|
|
1739
|
+
sprite_left = (screen_x - (sprite.left_offset * sprite_scale)).to_i
|
|
1725
1740
|
sprite_right = sprite_left + sprite_screen_width - 1
|
|
1726
1741
|
|
|
1727
1742
|
# Clamp to screen
|
|
@@ -1733,17 +1748,22 @@ module Doom
|
|
|
1733
1748
|
# gzt = mobj->z + spritetopoffset (top of sprite)
|
|
1734
1749
|
# gz = gzt - spriteheight (bottom of sprite, 1:1 pixel:unit)
|
|
1735
1750
|
thing_floor = sector ? sector.floor_height : 0
|
|
1736
|
-
if thing.is_a?(ProjectileStub) && thing.z
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1751
|
+
base_z = if thing.is_a?(ProjectileStub) && thing.z
|
|
1752
|
+
thing.z
|
|
1753
|
+
else
|
|
1754
|
+
thing_floor
|
|
1755
|
+
end
|
|
1741
1756
|
sprite_gzt = base_z + sprite.top_offset
|
|
1742
1757
|
sprite_gz = sprite_gzt - sprite.height
|
|
1743
1758
|
|
|
1744
|
-
#
|
|
1745
|
-
|
|
1746
|
-
|
|
1759
|
+
# Reuse per-frame clip buffers (-2 = not yet clipped). Only reset the
|
|
1760
|
+
# range we'll touch ([x1, x2]); leftover values past x2 are ignored.
|
|
1761
|
+
clipbot = @sprite_clipbot
|
|
1762
|
+
cliptop = @sprite_cliptop
|
|
1763
|
+
x1.upto(x2) do |i|
|
|
1764
|
+
clipbot[i] = -2
|
|
1765
|
+
cliptop[i] = -2
|
|
1766
|
+
end
|
|
1747
1767
|
|
|
1748
1768
|
# Scan drawsegs from back to front for obscuring segs
|
|
1749
1769
|
@drawsegs.reverse_each do |ds|
|
|
@@ -1775,26 +1795,22 @@ module Doom
|
|
|
1775
1795
|
silhouette = ds.silhouette
|
|
1776
1796
|
|
|
1777
1797
|
# If sprite bottom is at or above the bottom silhouette height, don't clip bottom
|
|
1778
|
-
if sprite_gz >= ds.bsilheight
|
|
1779
|
-
silhouette &= ~SIL_BOTTOM
|
|
1780
|
-
end
|
|
1798
|
+
silhouette &= ~SIL_BOTTOM if sprite_gz >= ds.bsilheight
|
|
1781
1799
|
|
|
1782
1800
|
# If sprite top is at or below the top silhouette height, don't clip top
|
|
1783
|
-
if sprite_gzt <= ds.tsilheight
|
|
1784
|
-
silhouette &= ~SIL_TOP
|
|
1785
|
-
end
|
|
1801
|
+
silhouette &= ~SIL_TOP if sprite_gzt <= ds.tsilheight
|
|
1786
1802
|
|
|
1787
1803
|
# Apply clipping for each column in the overlap
|
|
1788
1804
|
(r1..r2).each do |x|
|
|
1789
1805
|
# Index into drawseg's clip arrays (0-based from ds.x1)
|
|
1790
1806
|
ds_idx = x - ds.x1
|
|
1791
1807
|
|
|
1792
|
-
if (silhouette & SIL_BOTTOM) != 0 && clipbot[x] == -2
|
|
1793
|
-
clipbot[x] = ds.sprbottomclip[ds_idx]
|
|
1808
|
+
if (silhouette & SIL_BOTTOM) != 0 && clipbot[x] == -2 && ds.sprbottomclip && ds_idx < ds.sprbottomclip.length
|
|
1809
|
+
clipbot[x] = ds.sprbottomclip[ds_idx]
|
|
1794
1810
|
end
|
|
1795
1811
|
|
|
1796
|
-
if (silhouette & SIL_TOP) != 0 && cliptop[x] == -2
|
|
1797
|
-
cliptop[x] = ds.sprtopclip[ds_idx]
|
|
1812
|
+
if (silhouette & SIL_TOP) != 0 && cliptop[x] == -2 && ds.sprtopclip && ds_idx < ds.sprtopclip.length
|
|
1813
|
+
cliptop[x] = ds.sprtopclip[ds_idx]
|
|
1798
1814
|
end
|
|
1799
1815
|
end
|
|
1800
1816
|
end
|
|
@@ -1805,11 +1821,21 @@ module Doom
|
|
|
1805
1821
|
cliptop[x] = -1 if cliptop[x] == -2
|
|
1806
1822
|
end
|
|
1807
1823
|
|
|
1824
|
+
# The native wall pass emits no drawsegs (the silhouette bookkeeping is
|
|
1825
|
+
# not ported), so the drawseg loop above clipped nothing and sprites
|
|
1826
|
+
# would draw over the walls in front of them. Occlude, per column, any
|
|
1827
|
+
# sprite that sits behind the nearest solid wall recorded by the wall
|
|
1828
|
+
# pass -- the common case (a monster behind a wall or the far background).
|
|
1829
|
+
if @native_kernel
|
|
1830
|
+
swd = @sprite_wall_depth
|
|
1831
|
+
(x1..x2).each { |x| cliptop[x] = SCREEN_HEIGHT if swd[x] < dist }
|
|
1832
|
+
end
|
|
1833
|
+
|
|
1808
1834
|
# Calculate sprite screen Y positions
|
|
1809
1835
|
sprite_top_world = sprite_gzt - @player_z
|
|
1810
1836
|
sprite_bottom_world = sprite_gz - @player_z
|
|
1811
|
-
sprite_top_screen = (HALF_HEIGHT - sprite_top_world * sprite_scale).to_i
|
|
1812
|
-
sprite_bottom_screen = (HALF_HEIGHT - sprite_bottom_world * sprite_scale).to_i
|
|
1837
|
+
sprite_top_screen = (HALF_HEIGHT - (sprite_top_world * sprite_scale)).to_i
|
|
1838
|
+
sprite_bottom_screen = (HALF_HEIGHT - (sprite_bottom_world * sprite_scale)).to_i
|
|
1813
1839
|
|
|
1814
1840
|
# Cache for inner loop
|
|
1815
1841
|
framebuffer = @framebuffer
|
|
@@ -1847,7 +1873,7 @@ module Doom
|
|
|
1847
1873
|
next unless color
|
|
1848
1874
|
|
|
1849
1875
|
# Apply lighting and write directly to framebuffer
|
|
1850
|
-
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
1876
|
+
framebuffer[(y * SCREEN_WIDTH) + x] = cmap[color]
|
|
1851
1877
|
end
|
|
1852
1878
|
end
|
|
1853
1879
|
end
|