doom 0.6.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 +138 -25
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +377 -105
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +231 -0
- data/lib/doom/game/item_pickup.rb +108 -60
- data/lib/doom/game/menu.rb +347 -0
- data/lib/doom/game/monster_ai.rb +304 -91
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +47 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +466 -29
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +176 -0
- 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 +820 -640
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +80 -0
- 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 +684 -330
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +71 -0
- 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 +15 -17
- 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 +59 -10
- data/lib/doom/wad/sound.rb +93 -0
- data/lib/doom/wad/sprite.rb +60 -34
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +235 -12
- metadata +100 -7
data/lib/doom/render/renderer.rb
CHANGED
|
@@ -16,10 +16,14 @@ module Doom
|
|
|
16
16
|
|
|
17
17
|
# Drawseg stores wall segment info for sprite clipping
|
|
18
18
|
# Matches Chocolate Doom's drawseg_t structure
|
|
19
|
-
Drawseg = Struct.new(:x1, :x2, :scale1, :scale2,
|
|
19
|
+
Drawseg = Struct.new(:x1, :x2, :scale1, :scale2, :scalestep,
|
|
20
20
|
:silhouette, :bsilheight, :tsilheight,
|
|
21
21
|
:sprtopclip, :sprbottomclip,
|
|
22
|
-
:curline
|
|
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
|
|
23
27
|
|
|
24
28
|
# VisibleSprite stores sprite data for sorting and rendering
|
|
25
29
|
# Struct is faster than Hash for fixed-field data
|
|
@@ -39,6 +43,7 @@ module Doom
|
|
|
39
43
|
|
|
40
44
|
def mark(x, y1, y2)
|
|
41
45
|
return if y1 > y2
|
|
46
|
+
|
|
42
47
|
top[x] = [top[x], y1].min
|
|
43
48
|
bottom[x] = [bottom[x], y2].max
|
|
44
49
|
self.minx = [minx, x].min
|
|
@@ -51,21 +56,33 @@ module Doom
|
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
class Renderer
|
|
54
|
-
attr_reader :framebuffer
|
|
55
|
-
|
|
56
59
|
def initialize(wad, map, textures, palette, colormap, flats, sprites = nil, animations = nil)
|
|
57
60
|
@wad = wad
|
|
58
61
|
@map = map
|
|
59
62
|
@textures = textures
|
|
60
63
|
@palette = palette
|
|
61
64
|
@colormap = colormap
|
|
65
|
+
@flats_array = flats
|
|
62
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
|
|
63
77
|
@sprites = sprites
|
|
64
78
|
@animations = animations
|
|
65
79
|
@hidden_things = nil
|
|
66
80
|
@combat = nil
|
|
67
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
|
|
68
84
|
@leveltime = 0
|
|
85
|
+
@skip_background_fill = false
|
|
69
86
|
|
|
70
87
|
@framebuffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0)
|
|
71
88
|
|
|
@@ -73,6 +90,8 @@ module Doom
|
|
|
73
90
|
@player_y = 0.0
|
|
74
91
|
@player_z = 41.0
|
|
75
92
|
@player_angle = 0.0
|
|
93
|
+
@sin_angle = 0.0
|
|
94
|
+
@cos_angle = 1.0
|
|
76
95
|
|
|
77
96
|
# Projection constant - distance to projection plane
|
|
78
97
|
@projection = HALF_WIDTH / Math.tan(FOV * Math::PI / 360.0)
|
|
@@ -85,13 +104,17 @@ module Doom
|
|
|
85
104
|
# Column distance scale is constant (doesn't depend on player angle)
|
|
86
105
|
@column_distscale = Array.new(SCREEN_WIDTH) do |x|
|
|
87
106
|
dx = x - HALF_WIDTH
|
|
88
|
-
Math.sqrt(dx * dx + @projection * @projection) / @projection
|
|
107
|
+
Math.sqrt((dx * dx) + (@projection * @projection)) / @projection
|
|
89
108
|
end
|
|
90
109
|
|
|
91
110
|
# Clipping arrays
|
|
92
111
|
@ceiling_clip = Array.new(SCREEN_WIDTH, -1)
|
|
93
112
|
@floor_clip = Array.new(SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
94
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
|
+
|
|
95
118
|
# Sprite clip arrays (copy of wall clips for sprite clipping)
|
|
96
119
|
@sprite_ceiling_clip = Array.new(SCREEN_WIDTH, -1)
|
|
97
120
|
@sprite_floor_clip = Array.new(SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
@@ -104,9 +127,10 @@ module Doom
|
|
|
104
127
|
@y_slope_ceil = Array.new(HALF_HEIGHT + 1, 0.0)
|
|
105
128
|
@y_slope_floor = Array.new(HALF_HEIGHT + 1, 0.0)
|
|
106
129
|
end
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
109
132
|
attr_writer :hidden_things, :combat, :monster_ai, :leveltime
|
|
133
|
+
attr_accessor :players, :view_player, :skip_background_fill
|
|
110
134
|
|
|
111
135
|
# Diagnostic: returns info about all sprites and why they are/aren't visible
|
|
112
136
|
def sprite_diagnostics
|
|
@@ -124,7 +148,7 @@ module Doom
|
|
|
124
148
|
info[:view_y] = view_y.round(1)
|
|
125
149
|
|
|
126
150
|
if view_y <= 0
|
|
127
|
-
info[:status] =
|
|
151
|
+
info[:status] = 'behind_player'
|
|
128
152
|
results << info
|
|
129
153
|
next
|
|
130
154
|
end
|
|
@@ -139,7 +163,7 @@ module Doom
|
|
|
139
163
|
angle_to_thing = Math.atan2(dy, dx)
|
|
140
164
|
sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
|
|
141
165
|
unless sprite
|
|
142
|
-
info[:status] =
|
|
166
|
+
info[:status] = 'no_sprite_frame'
|
|
143
167
|
results << info
|
|
144
168
|
next
|
|
145
169
|
end
|
|
@@ -149,20 +173,19 @@ module Doom
|
|
|
149
173
|
info[:sprite_scale] = sprite_scale.round(3)
|
|
150
174
|
|
|
151
175
|
if screen_x + sprite_half_width < 0
|
|
152
|
-
info[:status] =
|
|
176
|
+
info[:status] = 'off_screen_left'
|
|
153
177
|
elsif screen_x - sprite_half_width >= SCREEN_WIDTH
|
|
154
|
-
info[:status] =
|
|
178
|
+
info[:status] = 'off_screen_right'
|
|
155
179
|
else
|
|
156
180
|
# Check drawseg clipping
|
|
157
|
-
sprite_left = (screen_x - sprite.left_offset * sprite_scale).to_i
|
|
181
|
+
sprite_left = (screen_x - (sprite.left_offset * sprite_scale)).to_i
|
|
158
182
|
sprite_right = sprite_left + (sprite.width * sprite_scale).to_i - 1
|
|
159
183
|
x1 = [sprite_left, 0].max
|
|
160
184
|
x2 = [sprite_right, SCREEN_WIDTH - 1].min
|
|
161
185
|
|
|
162
186
|
sector = @map.sector_at(thing.x, thing.y)
|
|
163
187
|
thing_floor = sector ? sector.floor_height : 0
|
|
164
|
-
|
|
165
|
-
sprite_gzt = thing_floor + sprite.top_offset
|
|
188
|
+
thing_floor + sprite.top_offset
|
|
166
189
|
|
|
167
190
|
clipping_segs = []
|
|
168
191
|
@drawsegs.reverse_each do |ds|
|
|
@@ -188,7 +211,7 @@ module Doom
|
|
|
188
211
|
info[:screen_range] = "#{x1}..#{x2}"
|
|
189
212
|
info[:clipping_segs] = clipping_segs.size
|
|
190
213
|
info[:clipping_detail] = clipping_segs
|
|
191
|
-
info[:status] =
|
|
214
|
+
info[:status] = 'visible'
|
|
192
215
|
end
|
|
193
216
|
|
|
194
217
|
results << info
|
|
@@ -196,62 +219,101 @@ module Doom
|
|
|
196
219
|
results
|
|
197
220
|
end
|
|
198
221
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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)
|
|
204
233
|
end
|
|
205
234
|
|
|
206
|
-
def
|
|
235
|
+
def set_player(x, y, z, angle)
|
|
207
236
|
@player_x = x.to_f
|
|
208
237
|
@player_y = y.to_f
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def set_z(z)
|
|
212
238
|
@player_z = z.to_f
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
@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)
|
|
217
242
|
end
|
|
218
243
|
|
|
219
244
|
def render_frame
|
|
220
|
-
|
|
221
|
-
|
|
245
|
+
if @native_kernel
|
|
246
|
+
native_wall_pass
|
|
247
|
+
@drawsegs = []
|
|
248
|
+
else
|
|
249
|
+
clear_framebuffer
|
|
250
|
+
reset_clipping
|
|
222
251
|
|
|
223
|
-
|
|
224
|
-
|
|
252
|
+
@sin_angle = Math.sin(@player_angle)
|
|
253
|
+
@cos_angle = Math.cos(@player_angle)
|
|
225
254
|
|
|
226
|
-
|
|
227
|
-
|
|
255
|
+
# Precompute column angles for floor/ceiling rendering
|
|
256
|
+
precompute_column_data
|
|
228
257
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
234
263
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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)
|
|
238
267
|
|
|
239
|
-
|
|
240
|
-
|
|
268
|
+
# Initialize drawsegs for sprite clipping
|
|
269
|
+
@drawsegs = []
|
|
241
270
|
|
|
242
|
-
|
|
243
|
-
|
|
271
|
+
# Render walls via BSP traversal
|
|
272
|
+
render_bsp_node(@map.nodes.size - 1)
|
|
244
273
|
|
|
245
|
-
|
|
246
|
-
|
|
274
|
+
# Draw visplanes for sectors different from background
|
|
275
|
+
draw_all_visplanes
|
|
276
|
+
end
|
|
247
277
|
|
|
248
278
|
# Save wall clip arrays for sprite clipping (reuse preallocated arrays)
|
|
249
279
|
@sprite_ceiling_clip.replace(@ceiling_clip)
|
|
250
280
|
@sprite_floor_clip.replace(@floor_clip)
|
|
251
281
|
@sprite_wall_depth.replace(@wall_depth)
|
|
252
282
|
|
|
253
|
-
#
|
|
254
|
-
|
|
283
|
+
# R_DrawMasked: render sprites and masked middle textures interleaved
|
|
284
|
+
draw_masked if @sprites
|
|
285
|
+
end
|
|
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
|
|
255
317
|
end
|
|
256
318
|
|
|
257
319
|
# Precompute column-based data for floor/ceiling rendering (R_InitLightTables-like)
|
|
@@ -269,6 +331,8 @@ module Doom
|
|
|
269
331
|
end
|
|
270
332
|
|
|
271
333
|
def draw_floor_ceiling_background
|
|
334
|
+
return if @skip_background_fill
|
|
335
|
+
|
|
272
336
|
player_sector = @map.sector_at(@player_x, @player_y)
|
|
273
337
|
return unless player_sector
|
|
274
338
|
|
|
@@ -293,7 +357,7 @@ module Doom
|
|
|
293
357
|
def render_visplane_spans(plane)
|
|
294
358
|
return if plane.minx > plane.maxx
|
|
295
359
|
|
|
296
|
-
spanstart = Array.new(SCREEN_HEIGHT)
|
|
360
|
+
spanstart = Array.new(SCREEN_HEIGHT) # Track where each row's span started
|
|
297
361
|
|
|
298
362
|
# Process columns left to right
|
|
299
363
|
((plane.minx)..(plane.maxx + 1)).each do |x|
|
|
@@ -301,9 +365,10 @@ module Doom
|
|
|
301
365
|
if x <= plane.maxx
|
|
302
366
|
t2 = plane.top[x]
|
|
303
367
|
b2 = plane.bottom[x]
|
|
304
|
-
t2 = SCREEN_HEIGHT if t2 > b2
|
|
368
|
+
t2 = SCREEN_HEIGHT if t2 > b2 # Invalid = empty
|
|
305
369
|
else
|
|
306
|
-
t2
|
|
370
|
+
t2 = SCREEN_HEIGHT
|
|
371
|
+
b2 = -1 # Sentinel for final column
|
|
307
372
|
end
|
|
308
373
|
|
|
309
374
|
# Get previous column bounds
|
|
@@ -312,7 +377,8 @@ module Doom
|
|
|
312
377
|
b1 = plane.bottom[x - 1]
|
|
313
378
|
t1 = SCREEN_HEIGHT if t1 > b1
|
|
314
379
|
else
|
|
315
|
-
t1
|
|
380
|
+
t1 = SCREEN_HEIGHT
|
|
381
|
+
b1 = -1
|
|
316
382
|
end
|
|
317
383
|
|
|
318
384
|
# Close spans that ended (visible in prev column, not in current)
|
|
@@ -329,10 +395,10 @@ module Doom
|
|
|
329
395
|
end
|
|
330
396
|
|
|
331
397
|
# Open new spans (visible in current, not started yet)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
398
|
+
next unless t2 < SCREEN_HEIGHT
|
|
399
|
+
|
|
400
|
+
(t2..b2).each do |y|
|
|
401
|
+
spanstart[y] ||= x
|
|
336
402
|
end
|
|
337
403
|
end
|
|
338
404
|
end
|
|
@@ -377,9 +443,9 @@ module Doom
|
|
|
377
443
|
x = x1
|
|
378
444
|
while x <= x2
|
|
379
445
|
ray_dist = perp_dist * column_distscale[x]
|
|
380
|
-
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
381
|
-
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
382
|
-
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]
|
|
383
449
|
framebuffer[row_offset + x] = cmap[color]
|
|
384
450
|
x += 1
|
|
385
451
|
end
|
|
@@ -390,7 +456,7 @@ module Doom
|
|
|
390
456
|
# Chocolate Doom: skytexturemid = SCREENHEIGHT/2 = 100 (for 200px screen).
|
|
391
457
|
# Scale factor: our 240px screen maps to DOOM's 200px sky coordinates.
|
|
392
458
|
SKY_TEXTUREMID = 100.0
|
|
393
|
-
SKY_YSCALE = 200.0 / SCREEN_HEIGHT
|
|
459
|
+
SKY_YSCALE = 200.0 / SCREEN_HEIGHT # 0.833 - maps our pixels to DOOM's 200px space
|
|
394
460
|
# Chocolate Doom: ANGLETOSKYSHIFT=22 gives 4 sky repetitions per 360 degrees.
|
|
395
461
|
# Full circle (2pi) * 512/pi = 1024 columns, masked to 256 = 4 repetitions.
|
|
396
462
|
SKY_XSCALE = 512.0 / Math::PI
|
|
@@ -427,9 +493,9 @@ module Doom
|
|
|
427
493
|
# Sky Y: texel = skytexturemid + (y - centery) * scale
|
|
428
494
|
# Maps texel 0 to screen top, texel 100 to horizon (1:1 for DOOM's 200px)
|
|
429
495
|
(y1..y2).each do |y|
|
|
430
|
-
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
|
|
431
497
|
color = column[tex_y]
|
|
432
|
-
framebuffer[y * SCREEN_WIDTH + x] = color
|
|
498
|
+
framebuffer[(y * SCREEN_WIDTH) + x] = color
|
|
433
499
|
end
|
|
434
500
|
end
|
|
435
501
|
end
|
|
@@ -475,13 +541,14 @@ module Doom
|
|
|
475
541
|
overlap = false
|
|
476
542
|
(intrl..intrh).each do |x|
|
|
477
543
|
next if x < 0 || x >= SCREEN_WIDTH
|
|
544
|
+
|
|
478
545
|
if plane.top[x] <= plane.bottom[x]
|
|
479
546
|
overlap = true
|
|
480
547
|
break
|
|
481
548
|
end
|
|
482
549
|
end
|
|
483
550
|
|
|
484
|
-
|
|
551
|
+
unless overlap
|
|
485
552
|
# No overlap - reuse same visplane with expanded range
|
|
486
553
|
plane.minx = unionl if unionl < plane.minx
|
|
487
554
|
plane.maxx = unionh if unionh > plane.maxx
|
|
@@ -551,7 +618,7 @@ module Doom
|
|
|
551
618
|
if is_sky && sky_texture
|
|
552
619
|
sky_w = sky_texture.width
|
|
553
620
|
sky_h = sky_texture.height
|
|
554
|
-
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
|
|
555
622
|
x = 0
|
|
556
623
|
while x < SCREEN_WIDTH
|
|
557
624
|
column_angle = player_angle - Math.atan2(x - HALF_WIDTH, projection)
|
|
@@ -564,9 +631,9 @@ module Doom
|
|
|
564
631
|
x = 0
|
|
565
632
|
while x < SCREEN_WIDTH
|
|
566
633
|
ray_dist = perp_dist * column_distscale[x]
|
|
567
|
-
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
568
|
-
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
569
|
-
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]
|
|
570
637
|
framebuffer[row_offset + x] = cmap[color]
|
|
571
638
|
x += 1
|
|
572
639
|
end
|
|
@@ -591,9 +658,9 @@ module Doom
|
|
|
591
658
|
x = 0
|
|
592
659
|
while x < SCREEN_WIDTH
|
|
593
660
|
ray_dist = perp_dist * column_distscale[x]
|
|
594
|
-
tex_x = (player_x + ray_dist * column_cos[x]).to_i & 63
|
|
595
|
-
tex_y = (neg_player_y - ray_dist * column_sin[x]).to_i & 63
|
|
596
|
-
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]
|
|
597
664
|
framebuffer[row_offset + x] = cmap[color]
|
|
598
665
|
x += 1
|
|
599
666
|
end
|
|
@@ -682,6 +749,7 @@ module Doom
|
|
|
682
749
|
x = min_sx
|
|
683
750
|
while x <= max_sx
|
|
684
751
|
return true if @ceiling_clip[x] < @floor_clip[x] - 1
|
|
752
|
+
|
|
685
753
|
x += 1
|
|
686
754
|
end
|
|
687
755
|
|
|
@@ -711,32 +779,28 @@ module Doom
|
|
|
711
779
|
|
|
712
780
|
# Create floor visplane if floor is visible (below eye level)
|
|
713
781
|
# Matches Chocolate Doom: if (frontsector->floorheight < viewz)
|
|
714
|
-
if @current_sector.floor_height < @player_z
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
@current_floor_plane = nil
|
|
724
|
-
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
|
|
725
791
|
|
|
726
792
|
# Create ceiling visplane if ceiling is visible (above eye level or sky)
|
|
727
793
|
# Matches Chocolate Doom: if (frontsector->ceilingheight > viewz || frontsector->ceilingpic == skyflatnum)
|
|
728
794
|
is_sky = @current_sector.ceiling_texture == 'F_SKY1'
|
|
729
|
-
if @current_sector.ceiling_height > @player_z || is_sky
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
@current_ceiling_plane = nil
|
|
739
|
-
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
|
|
740
804
|
|
|
741
805
|
# Process all segs in this subsector
|
|
742
806
|
subsector.seg_count.times do |i|
|
|
@@ -764,11 +828,11 @@ module Doom
|
|
|
764
828
|
return
|
|
765
829
|
elsif y1 < near
|
|
766
830
|
t = (near - y1) / (y2 - y1)
|
|
767
|
-
x1
|
|
831
|
+
x1 += (t * (x2 - x1))
|
|
768
832
|
y1 = near
|
|
769
833
|
elsif y2 < near
|
|
770
834
|
t = (near - y1) / (y2 - y1)
|
|
771
|
-
x2 = x1 + t * (x2 - x1)
|
|
835
|
+
x2 = x1 + (t * (x2 - x1))
|
|
772
836
|
y2 = near
|
|
773
837
|
end
|
|
774
838
|
end
|
|
@@ -812,7 +876,7 @@ module Doom
|
|
|
812
876
|
# Calculate seg length for texture mapping
|
|
813
877
|
seg_v1 = @map.vertices[seg.v1]
|
|
814
878
|
seg_v2 = @map.vertices[seg.v2]
|
|
815
|
-
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))
|
|
816
880
|
|
|
817
881
|
draw_seg_range(x1i, x2i, sx1, sx2, y1, y2, sector, back_sector, sidedef, linedef, seg, seg_length)
|
|
818
882
|
end
|
|
@@ -824,8 +888,8 @@ module Doom
|
|
|
824
888
|
|
|
825
889
|
# Rotate - transform world to view space
|
|
826
890
|
# View space: +Y forward (in direction of angle), +X is right
|
|
827
|
-
x = dx * @sin_angle - dy * @cos_angle
|
|
828
|
-
y = dx * @cos_angle + dy * @sin_angle
|
|
891
|
+
x = (dx * @sin_angle) - (dy * @cos_angle)
|
|
892
|
+
y = (dx * @cos_angle) + (dy * @sin_angle)
|
|
829
893
|
|
|
830
894
|
[x, y]
|
|
831
895
|
end
|
|
@@ -878,16 +942,16 @@ module Doom
|
|
|
878
942
|
cos_a = @cos_angle
|
|
879
943
|
proj = @projection
|
|
880
944
|
|
|
881
|
-
c1 = cos_a * proj - sin_a * HALF_WIDTH
|
|
882
|
-
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)
|
|
883
947
|
|
|
884
948
|
# denom(x) = seg_dy * ray_dx - seg_dx * ray_dy = A * x + B
|
|
885
|
-
tex_a = seg_dy * sin_a + seg_dx * cos_a
|
|
886
|
-
tex_b = seg_dy * c1 - seg_dx * c2
|
|
949
|
+
tex_a = (seg_dy * sin_a) + (seg_dx * cos_a)
|
|
950
|
+
tex_b = (seg_dy * c1) - (seg_dx * c2)
|
|
887
951
|
|
|
888
952
|
# numer(x) = py * ray_dx - px * ray_dy = E * x + F
|
|
889
|
-
tex_e = py * sin_a + px * cos_a
|
|
890
|
-
tex_f = py * c1 - px * c2
|
|
953
|
+
tex_e = (py * sin_a) + (px * cos_a)
|
|
954
|
+
tex_f = (py * c1) - (px * c2)
|
|
891
955
|
|
|
892
956
|
tex_offset = seg.offset + sidedef.x_offset
|
|
893
957
|
|
|
@@ -924,24 +988,39 @@ module Doom
|
|
|
924
988
|
tsilheight = -Float::INFINITY
|
|
925
989
|
end
|
|
926
990
|
|
|
927
|
-
#
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
991
|
+
# Detect masked middle texture (grates, bars, fences)
|
|
992
|
+
has_masked = false
|
|
993
|
+
@current_masked_cols = nil
|
|
994
|
+
if back_sector && sidedef
|
|
995
|
+
mid_tex = sidedef.middle_texture
|
|
996
|
+
if mid_tex && mid_tex != '-' && !mid_tex.empty?
|
|
997
|
+
has_masked = true
|
|
998
|
+
@current_masked_cols = Array.new(x2 - x1 + 1)
|
|
999
|
+
# Force silhouette so sprites get clipped against this seg
|
|
1000
|
+
if (silhouette & SIL_TOP) == 0
|
|
1001
|
+
silhouette |= SIL_TOP
|
|
1002
|
+
tsilheight = -Float::INFINITY
|
|
1003
|
+
end
|
|
1004
|
+
if (silhouette & SIL_BOTTOM) == 0
|
|
1005
|
+
silhouette |= SIL_BOTTOM
|
|
1006
|
+
bsilheight = Float::INFINITY
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
934
1009
|
end
|
|
935
1010
|
|
|
1011
|
+
# Check planes for this seg range (R_CheckPlane equivalent)
|
|
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
|
|
1014
|
+
|
|
936
1015
|
(x1..x2).each do |x|
|
|
937
1016
|
next if @ceiling_clip[x] >= @floor_clip[x] - 1
|
|
938
1017
|
|
|
939
1018
|
# Screen-space interpolation for distance
|
|
940
|
-
t = sx2
|
|
1019
|
+
t = sx2 == sx1 ? 0 : (x - sx1) / (sx2 - sx1)
|
|
941
1020
|
t = t.clamp(0.0, 1.0)
|
|
942
1021
|
|
|
943
1022
|
if dist1 > 0 && dist2 > 0
|
|
944
|
-
inv_dist = (1.0 - t) / dist1 + t / dist2
|
|
1023
|
+
inv_dist = ((1.0 - t) / dist1) + (t / dist2)
|
|
945
1024
|
dist = 1.0 / inv_dist
|
|
946
1025
|
else
|
|
947
1026
|
dist = dist1 > 0 ? dist1 : dist2
|
|
@@ -949,13 +1028,13 @@ module Doom
|
|
|
949
1028
|
|
|
950
1029
|
# Ray-seg intersection for texture column
|
|
951
1030
|
# s = (E * x + F) / (A * x + B) gives position along seg in world units
|
|
952
|
-
denom = tex_a * x + tex_b
|
|
953
|
-
if denom.abs < 0.001
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
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
|
|
959
1038
|
|
|
960
1039
|
# Skip if too close
|
|
961
1040
|
next if dist < 1
|
|
@@ -971,198 +1050,207 @@ module Doom
|
|
|
971
1050
|
# Chocolate Doom rounds ceiling UP and floor DOWN to avoid 1-pixel gaps:
|
|
972
1051
|
# yl = (topfrac+HEIGHTUNIT-1)>>HEIGHTBITS (ceil for front ceiling)
|
|
973
1052
|
# yh = bottomfrac>>HEIGHTBITS (floor for front floor)
|
|
974
|
-
front_ceil_y = (HALF_HEIGHT - front_ceil * scale).ceil
|
|
975
|
-
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
|
|
976
1055
|
|
|
977
1056
|
# Clamp to current clip bounds
|
|
978
1057
|
ceil_y = [front_ceil_y, @ceiling_clip[x] + 1].max
|
|
979
1058
|
floor_y = [front_floor_y, @floor_clip[x] - 1].min
|
|
980
1059
|
|
|
981
1060
|
if back_sector
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
back_ceil_y = (HALF_HEIGHT - back_ceil * scale).to_i
|
|
990
|
-
back_floor_y = (HALF_HEIGHT - back_floor * scale).ceil
|
|
991
|
-
|
|
992
|
-
# Determine visible ceiling/floor boundaries (the opening between sectors)
|
|
993
|
-
# high_ceil = top of the opening on screen (max Y = lower world ceiling)
|
|
994
|
-
# low_floor = bottom of the opening on screen (min Y = higher world floor)
|
|
995
|
-
high_ceil = [ceil_y, back_ceil_y].max
|
|
996
|
-
low_floor = [floor_y, back_floor_y].min
|
|
997
|
-
|
|
998
|
-
# Check for closed door (no opening between sectors)
|
|
999
|
-
# Matches Chocolate Doom: backsector->ceilingheight <= frontsector->floorheight
|
|
1000
|
-
# || backsector->floorheight >= frontsector->ceilingheight
|
|
1001
|
-
closed_door = back_sector.ceiling_height <= sector.floor_height ||
|
|
1002
|
-
back_sector.floor_height >= sector.ceiling_height
|
|
1003
|
-
|
|
1004
|
-
both_sky = sector.ceiling_texture == 'F_SKY1' && back_sector.ceiling_texture == 'F_SKY1'
|
|
1005
|
-
|
|
1006
|
-
# Determine whether to mark ceiling/floor visplanes.
|
|
1007
|
-
# Match Chocolate Doom: only mark when properties differ, plus force
|
|
1008
|
-
# for closed doors. The background fill covers same-property gaps.
|
|
1009
|
-
# Chocolate Doom sky hack: "worldtop = worldhigh" makes the front
|
|
1010
|
-
# ceiling equal to the back ceiling when both are sky. This prevents
|
|
1011
|
-
# the low sky ceiling from clipping walls in adjacent sectors.
|
|
1012
|
-
effective_front_ceil = both_sky ? back_sector.ceiling_height : sector.ceiling_height
|
|
1013
|
-
|
|
1014
|
-
if closed_door
|
|
1015
|
-
should_mark_ceiling = true
|
|
1016
|
-
should_mark_floor = true
|
|
1017
|
-
else
|
|
1018
|
-
should_mark_ceiling = effective_front_ceil != back_sector.ceiling_height ||
|
|
1019
|
-
sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1020
|
-
sector.light_level != back_sector.light_level
|
|
1021
|
-
should_mark_floor = sector.floor_height != back_sector.floor_height ||
|
|
1022
|
-
sector.floor_texture != back_sector.floor_texture ||
|
|
1023
|
-
sector.light_level != back_sector.light_level
|
|
1024
|
-
end
|
|
1061
|
+
draw_two_sided_seg_column(x, x1, ceil_y, floor_y, scale, dist, tex_col,
|
|
1062
|
+
sector, back_sector, sidedef, linedef)
|
|
1063
|
+
else
|
|
1064
|
+
draw_one_sided_seg_column(x, ceil_y, floor_y, scale, dist, tex_col,
|
|
1065
|
+
sector, sidedef, linedef)
|
|
1066
|
+
end
|
|
1067
|
+
end
|
|
1025
1068
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
if mark_top <= mark_bottom
|
|
1032
|
-
@current_ceiling_plane.mark(x, mark_top, mark_bottom)
|
|
1033
|
-
end
|
|
1034
|
-
end
|
|
1069
|
+
# Save drawseg for sprite clipping and masked rendering
|
|
1070
|
+
if silhouette != SIL_NONE || has_masked
|
|
1071
|
+
sprtopclip = @ceiling_clip[x1..x2].dup
|
|
1072
|
+
sprbottomclip = @floor_clip[x1..x2].dup
|
|
1073
|
+
scalestep = x2 > x1 ? (scale2 - scale1) / (x2 - x1) : 0
|
|
1035
1074
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1075
|
+
drawseg = Drawseg.new(
|
|
1076
|
+
x1, x2,
|
|
1077
|
+
scale1, scale2, scalestep,
|
|
1078
|
+
silhouette,
|
|
1079
|
+
bsilheight, tsilheight,
|
|
1080
|
+
sprtopclip, sprbottomclip,
|
|
1081
|
+
seg,
|
|
1082
|
+
has_masked ? @current_masked_cols : nil,
|
|
1083
|
+
sector, back_sector, sidedef,
|
|
1084
|
+
dist1, dist2, sx1.to_f, sx2.to_f
|
|
1085
|
+
)
|
|
1086
|
+
@drawsegs << drawseg
|
|
1087
|
+
end
|
|
1088
|
+
@current_masked_cols = nil
|
|
1089
|
+
end
|
|
1045
1090
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
1058
1132
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
draw_wall_column_ex(x, back_floor_y, floor_y, sidedef.lower_texture, dist,
|
|
1067
|
-
sector.light_level, tex_col, lower_tex_y, scale, back_sector.floor_height, sector.floor_height)
|
|
1068
|
-
end
|
|
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
|
|
1069
1140
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
if effective_front_ceil > back_sector.ceiling_height
|
|
1078
|
-
@ceiling_clip[x] = [back_ceil_y, @ceiling_clip[x]].max
|
|
1079
|
-
elsif effective_front_ceil < back_sector.ceiling_height
|
|
1080
|
-
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1081
|
-
elsif sector.ceiling_texture != back_sector.ceiling_texture ||
|
|
1082
|
-
sector.light_level != back_sector.light_level
|
|
1083
|
-
@ceiling_clip[x] = [ceil_y - 1, @ceiling_clip[x]].max
|
|
1084
|
-
end
|
|
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
|
|
1085
1148
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
# Chocolate Doom: else if (markfloor) floorclip = yh + 1
|
|
1091
|
-
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1092
|
-
elsif sector.floor_texture != back_sector.floor_texture ||
|
|
1093
|
-
sector.light_level != back_sector.light_level
|
|
1094
|
-
@floor_clip[x] = [floor_y + 1, @floor_clip[x]].min
|
|
1095
|
-
end
|
|
1096
|
-
end
|
|
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
|
|
1097
1153
|
else
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
@current_ceiling_plane.mark(x, mark_top, mark_bottom)
|
|
1106
|
-
end
|
|
1107
|
-
end
|
|
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
|
|
1108
1161
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
|
1118
1172
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
# With DONTPEGBOTTOM: texture bottom aligns with floor
|
|
1122
|
-
# Without: texture top aligns with ceiling
|
|
1123
|
-
if linedef.lower_unpegged?
|
|
1124
|
-
texture = @textures[sidedef.middle_texture]
|
|
1125
|
-
tex_height = texture ? texture.height : 128
|
|
1126
|
-
mid_tex_y = sidedef.y_offset + tex_height - (sector.ceiling_height - sector.floor_height)
|
|
1127
|
-
else
|
|
1128
|
-
mid_tex_y = sidedef.y_offset
|
|
1129
|
-
end
|
|
1130
|
-
draw_wall_column_ex(x, ceil_y, floor_y, sidedef.middle_texture, dist,
|
|
1131
|
-
sector.light_level, tex_col, mid_tex_y, scale, sector.ceiling_height, sector.floor_height)
|
|
1173
|
+
# Store masked texture column for deferred rendering (grates, bars)
|
|
1174
|
+
@current_masked_cols[x - x1] = tex_col if @current_masked_cols
|
|
1132
1175
|
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
|
1135
1191
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
@floor_clip[x] =
|
|
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
|
|
1139
1201
|
end
|
|
1140
1202
|
end
|
|
1203
|
+
end
|
|
1141
1204
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
|
1216
|
+
end
|
|
1147
1217
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
seg
|
|
1155
|
-
)
|
|
1156
|
-
@drawsegs << drawseg
|
|
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
|
|
1157
1224
|
end
|
|
1225
|
+
|
|
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)
|
|
1239
|
+
|
|
1240
|
+
# Track wall depth for sprite clipping (solid wall occludes this column)
|
|
1241
|
+
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
1242
|
+
|
|
1243
|
+
# Fully occluded
|
|
1244
|
+
@ceiling_clip[x] = SCREEN_HEIGHT
|
|
1245
|
+
@floor_clip[x] = -1
|
|
1158
1246
|
end
|
|
1159
1247
|
|
|
1160
1248
|
# Wall column drawing with proper texture mapping
|
|
1161
1249
|
# tex_col: texture column (X coordinate in texture)
|
|
1162
1250
|
# tex_y_start: starting Y coordinate in texture (accounts for pegging)
|
|
1163
1251
|
# scale: projection scale for this column (projection / distance)
|
|
1164
|
-
# world_top
|
|
1165
|
-
def draw_wall_column_ex(x, y1, y2, texture_name, dist, light_level, tex_col, tex_y_start, scale, world_top
|
|
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)
|
|
1166
1254
|
return if y1 > y2
|
|
1167
1255
|
return if texture_name.nil? || texture_name.empty? || texture_name == '-'
|
|
1168
1256
|
|
|
@@ -1193,10 +1281,10 @@ module Doom
|
|
|
1193
1281
|
tex_step = 1.0 / scale
|
|
1194
1282
|
|
|
1195
1283
|
# Calculate where the unclipped wall top would be on screen
|
|
1196
|
-
unclipped_y1 = HALF_HEIGHT - (world_top - @player_z) * scale
|
|
1284
|
+
unclipped_y1 = HALF_HEIGHT - ((world_top - @player_z) * scale)
|
|
1197
1285
|
|
|
1198
1286
|
# Adjust tex_y_start for any clipping at the top
|
|
1199
|
-
tex_y_at_y1 = tex_y_start + (y1 - unclipped_y1) * tex_step
|
|
1287
|
+
tex_y_at_y1 = tex_y_start + ((y1 - unclipped_y1) * tex_step)
|
|
1200
1288
|
|
|
1201
1289
|
# Clamp to screen bounds
|
|
1202
1290
|
y1 = 0 if y1 < 0
|
|
@@ -1206,10 +1294,10 @@ module Doom
|
|
|
1206
1294
|
y = y1
|
|
1207
1295
|
while y <= y2
|
|
1208
1296
|
screen_offset = y - y1
|
|
1209
|
-
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
|
|
1210
1298
|
|
|
1211
1299
|
color = column[tex_y]
|
|
1212
|
-
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
1300
|
+
framebuffer[(y * SCREEN_WIDTH) + x] = cmap[color] if color
|
|
1213
1301
|
y += 1
|
|
1214
1302
|
end
|
|
1215
1303
|
end
|
|
@@ -1307,12 +1395,23 @@ module Doom
|
|
|
1307
1395
|
# Get sprite: death frame > walking frame > idle frame
|
|
1308
1396
|
if @combat && @combat.dead?(thing_idx)
|
|
1309
1397
|
sprite = @combat.death_sprite(thing_idx, thing.type, angle_to_thing, thing.angle)
|
|
1398
|
+
next unless sprite # Barrel disappeared after explosion
|
|
1310
1399
|
elsif @monster_ai
|
|
1311
|
-
mon = @monster_ai.
|
|
1400
|
+
mon = @monster_ai.monster_by_thing_idx[thing_idx]
|
|
1312
1401
|
if mon && mon.active
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1402
|
+
if mon.attacking
|
|
1403
|
+
# Attack animation: show attack frames (E, F, G...)
|
|
1404
|
+
prefix = @sprites.prefix_for(thing.type)
|
|
1405
|
+
atk_frames = Game::MonsterAI::ATTACK_FRAMES[prefix]
|
|
1406
|
+
if atk_frames
|
|
1407
|
+
frame_idx = (mon.attack_frame_tic / Game::MonsterAI::ATTACK_FRAME_TICS).clamp(0, atk_frames.size - 1)
|
|
1408
|
+
sprite = @sprites.get_frame(thing.type, atk_frames[frame_idx], angle_to_thing, thing.angle)
|
|
1409
|
+
end
|
|
1410
|
+
else
|
|
1411
|
+
# Walking animation: cycle through frames A-D
|
|
1412
|
+
walk_frame = %w[A B C D][@leveltime / 4 % 4]
|
|
1413
|
+
sprite = @sprites.get_frame(thing.type, walk_frame, angle_to_thing, thing.angle)
|
|
1414
|
+
end
|
|
1316
1415
|
end
|
|
1317
1416
|
sprite ||= @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
|
|
1318
1417
|
else
|
|
@@ -1331,50 +1430,287 @@ module Doom
|
|
|
1331
1430
|
visible_sprites << VisibleSprite.new(thing, sprite, view_x, view_y, dist, screen_x)
|
|
1332
1431
|
end
|
|
1333
1432
|
|
|
1433
|
+
collect_player_sprites(visible_sprites)
|
|
1434
|
+
|
|
1334
1435
|
# Sort by distance (back to front for proper overdraw)
|
|
1335
1436
|
visible_sprites.sort_by! { |s| -s.dist }
|
|
1336
1437
|
|
|
1337
|
-
# Draw each sprite
|
|
1438
|
+
# Draw each sprite with drawseg interleaving
|
|
1338
1439
|
visible_sprites.each do |vs|
|
|
1339
|
-
|
|
1440
|
+
draw_sprite_with_masking(vs)
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
# Draw remaining masked segs not triggered by sprites
|
|
1444
|
+
@drawsegs.reverse_each do |ds|
|
|
1445
|
+
next unless ds.maskedtexturecol
|
|
1446
|
+
|
|
1447
|
+
render_masked_seg_range(ds, ds.x1, ds.x2)
|
|
1340
1448
|
end
|
|
1341
1449
|
|
|
1342
|
-
# Draw projectiles and
|
|
1343
|
-
|
|
1450
|
+
# Draw projectiles, explosions, and bullet puffs
|
|
1451
|
+
return unless @combat
|
|
1452
|
+
|
|
1453
|
+
render_projectiles
|
|
1454
|
+
render_puffs
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1457
|
+
# Chocolate Doom R_DrawMasked: interleave sprites with masked drawsegs
|
|
1458
|
+
def draw_masked
|
|
1459
|
+
render_sprites
|
|
1344
1460
|
end
|
|
1345
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
|
+
|
|
1516
|
+
# Draw sprite, rendering masked drawsegs behind it first (R_DrawSprite)
|
|
1517
|
+
def draw_sprite_with_masking(spr)
|
|
1518
|
+
sprite_scale = @projection / spr.dist
|
|
1519
|
+
|
|
1520
|
+
# Scan drawsegs newest to oldest (nearest to farthest)
|
|
1521
|
+
@drawsegs.reverse_each do |ds|
|
|
1522
|
+
next if ds.x1 > spr.screen_x.to_i + spr.sprite.width || ds.x2 < spr.screen_x.to_i - spr.sprite.width
|
|
1523
|
+
next if ds.silhouette == SIL_NONE && ds.maskedtexturecol.nil?
|
|
1524
|
+
|
|
1525
|
+
ds_max_scale = [ds.scale1, ds.scale2].max
|
|
1526
|
+
[ds.scale1, ds.scale2].min
|
|
1527
|
+
|
|
1528
|
+
# Is the drawseg behind the sprite?
|
|
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)
|
|
1536
|
+
end
|
|
1537
|
+
next # Don't clip against things behind
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1540
|
+
draw_sprite(spr)
|
|
1541
|
+
end
|
|
1542
|
+
|
|
1543
|
+
# Chocolate Doom R_RenderMaskedSegRange
|
|
1544
|
+
def render_masked_seg_range(ds, rx1, rx2)
|
|
1545
|
+
return unless ds.maskedtexturecol
|
|
1546
|
+
return unless ds.sidedef
|
|
1547
|
+
|
|
1548
|
+
mid_tex_name = ds.sidedef.middle_texture
|
|
1549
|
+
return if mid_tex_name.nil? || mid_tex_name == '-' || mid_tex_name.empty?
|
|
1550
|
+
|
|
1551
|
+
texture = @textures[anim_texture(mid_tex_name)]
|
|
1552
|
+
return unless texture
|
|
1553
|
+
|
|
1554
|
+
front = ds.frontsector
|
|
1555
|
+
back = ds.backsector
|
|
1556
|
+
return unless front && back
|
|
1557
|
+
|
|
1558
|
+
# Texture anchoring (matching Chocolate Doom)
|
|
1559
|
+
if ds.curline && @map.linedefs[ds.curline.linedef].lower_unpegged?
|
|
1560
|
+
higher_floor = [front.floor_height, back.floor_height].max
|
|
1561
|
+
dc_texturemid = higher_floor + texture.height - @player_z
|
|
1562
|
+
else
|
|
1563
|
+
lower_ceiling = [front.ceiling_height, back.ceiling_height].min
|
|
1564
|
+
dc_texturemid = lower_ceiling - @player_z
|
|
1565
|
+
end
|
|
1566
|
+
dc_texturemid += ds.sidedef.y_offset
|
|
1567
|
+
|
|
1568
|
+
light_level = front.light_level
|
|
1569
|
+
|
|
1570
|
+
# Iterate columns
|
|
1571
|
+
rx1 = [rx1, ds.x1].max
|
|
1572
|
+
rx2 = [rx2, ds.x2].min
|
|
1573
|
+
|
|
1574
|
+
(rx1..rx2).each do |x|
|
|
1575
|
+
col_idx = x - ds.x1
|
|
1576
|
+
next if col_idx < 0 || col_idx >= ds.maskedtexturecol.size
|
|
1577
|
+
|
|
1578
|
+
tex_col = ds.maskedtexturecol[col_idx]
|
|
1579
|
+
next unless tex_col # nil = already drawn or empty
|
|
1580
|
+
|
|
1581
|
+
# Per-column distance using same 1/z interpolation as wall renderer
|
|
1582
|
+
if ds.dist1 && ds.dist2 && ds.dist1 > 0 && ds.dist2 > 0 && ds.sx2 != ds.sx1
|
|
1583
|
+
t = ((x - ds.sx1) / (ds.sx2 - ds.sx1)).clamp(0.0, 1.0)
|
|
1584
|
+
inv_dist = ((1.0 - t) / ds.dist1) + (t / ds.dist2)
|
|
1585
|
+
col_dist = 1.0 / inv_dist
|
|
1586
|
+
else
|
|
1587
|
+
col_dist = ds.dist1 || 1.0
|
|
1588
|
+
end
|
|
1589
|
+
next if col_dist < 1
|
|
1590
|
+
|
|
1591
|
+
spryscale = @projection / col_dist
|
|
1592
|
+
spryscale = [spryscale, 64.0].min
|
|
1593
|
+
|
|
1594
|
+
# Screen Y of texture top
|
|
1595
|
+
sprtopscreen = HALF_HEIGHT - (dc_texturemid * spryscale)
|
|
1596
|
+
|
|
1597
|
+
# Get clip bounds from drawseg
|
|
1598
|
+
clip_idx = x - ds.x1
|
|
1599
|
+
mceilingclip = ds.sprtopclip[clip_idx]
|
|
1600
|
+
mfloorclip = ds.sprbottomclip[clip_idx]
|
|
1601
|
+
|
|
1602
|
+
# Calculate column draw range
|
|
1603
|
+
tex_x = tex_col.to_i % texture.width
|
|
1604
|
+
column = texture.column_pixels(tex_x)
|
|
1605
|
+
next unless column
|
|
1606
|
+
|
|
1607
|
+
iscale = 1.0 / spryscale
|
|
1608
|
+
|
|
1609
|
+
light = calculate_light(light_level, col_dist)
|
|
1610
|
+
cmap = @colormap.maps[light]
|
|
1611
|
+
|
|
1612
|
+
# Draw each non-nil run of pixels (transparency)
|
|
1613
|
+
tex_height = texture.height
|
|
1614
|
+
y_start = nil
|
|
1615
|
+
(0..tex_height).each do |ty|
|
|
1616
|
+
color = ty < tex_height ? column[ty] : nil
|
|
1617
|
+
if color
|
|
1618
|
+
y_start ||= ty
|
|
1619
|
+
elsif y_start
|
|
1620
|
+
# Draw run from y_start to ty-1
|
|
1621
|
+
top_y = (sprtopscreen + (y_start * spryscale)).to_i
|
|
1622
|
+
bot_y = (sprtopscreen + (ty * spryscale)).to_i - 1
|
|
1623
|
+
top_y = [top_y, mceilingclip + 1].max
|
|
1624
|
+
bot_y = [bot_y, mfloorclip - 1].min
|
|
1625
|
+
|
|
1626
|
+
if top_y <= bot_y
|
|
1627
|
+
top_y = [top_y, 0].max
|
|
1628
|
+
bot_y = [bot_y, SCREEN_HEIGHT - 1].min
|
|
1629
|
+
tex_frac = y_start + ((top_y - (sprtopscreen + (y_start * spryscale))) * iscale)
|
|
1630
|
+
y = top_y
|
|
1631
|
+
while y <= bot_y
|
|
1632
|
+
t = (tex_frac.to_i % tex_height)
|
|
1633
|
+
c = column[t]
|
|
1634
|
+
@framebuffer[(y * SCREEN_WIDTH) + x] = cmap[c] if c
|
|
1635
|
+
tex_frac += iscale
|
|
1636
|
+
y += 1
|
|
1637
|
+
end
|
|
1638
|
+
end
|
|
1639
|
+
y_start = nil
|
|
1640
|
+
end
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
# Mark column as drawn
|
|
1644
|
+
ds.maskedtexturecol[col_idx] = nil
|
|
1645
|
+
end
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
# Stub for projectiles/explosions that carries a z height
|
|
1649
|
+
ProjectileStub = Struct.new(:x, :y, :angle, :type, :flags, :z)
|
|
1650
|
+
|
|
1346
1651
|
def render_projectiles
|
|
1347
|
-
# Render
|
|
1652
|
+
# Render all projectiles in flight
|
|
1348
1653
|
@combat.projectiles.each do |proj|
|
|
1349
1654
|
view_x, view_y = transform_point(proj.x, proj.y)
|
|
1350
1655
|
next if view_y <= 0
|
|
1351
1656
|
|
|
1352
|
-
|
|
1657
|
+
prefix = proj.sprite_prefix || 'MISL'
|
|
1658
|
+
|
|
1659
|
+
# Try rotation-based sprite first (rockets, baron fireballs)
|
|
1353
1660
|
rocket_angle = Math.atan2(proj.dy, proj.dx)
|
|
1354
1661
|
viewer_angle = Math.atan2(proj.y - @player_y, proj.x - @player_x)
|
|
1355
1662
|
angle_diff = (viewer_angle - rocket_angle) % (2 * Math::PI)
|
|
1356
|
-
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
1357
|
-
|
|
1358
|
-
|
|
1663
|
+
rotation = (((angle_diff + (Math::PI / 8)) / (Math::PI / 4)).to_i % 8) + 1
|
|
1664
|
+
|
|
1665
|
+
# Animate flight: cycle A/B frames
|
|
1666
|
+
flight_frame = %w[A B][@leveltime / 4 % 2]
|
|
1667
|
+
proj_sprite = @sprites.send(:load_sprite_frame, prefix, flight_frame, rotation)
|
|
1668
|
+
proj_sprite ||= @sprites.send(:load_sprite_frame, prefix, flight_frame, 0)
|
|
1669
|
+
proj_sprite ||= @sprites.send(:load_sprite_frame, prefix, 'A', 0)
|
|
1670
|
+
next unless proj_sprite
|
|
1359
1671
|
|
|
1360
1672
|
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1361
|
-
|
|
1362
|
-
visible = VisibleSprite.new(
|
|
1673
|
+
stub = ProjectileStub.new(proj.x, proj.y, 0, 0, 0, proj.z)
|
|
1674
|
+
visible = VisibleSprite.new(stub, proj_sprite, view_x, view_y, view_y, screen_x)
|
|
1363
1675
|
draw_sprite(visible)
|
|
1364
1676
|
end
|
|
1365
1677
|
|
|
1366
|
-
# Render explosions
|
|
1678
|
+
# Render explosions at projectile height
|
|
1367
1679
|
@combat.explosions.each do |expl|
|
|
1368
1680
|
view_x, view_y = transform_point(expl[:x], expl[:y])
|
|
1369
1681
|
next if view_y <= 0
|
|
1682
|
+
|
|
1370
1683
|
elapsed = (@combat.instance_variable_get(:@tic) - expl[:tic])
|
|
1371
1684
|
frame_idx = (elapsed / 4).clamp(0, 2)
|
|
1372
|
-
|
|
1373
|
-
|
|
1685
|
+
|
|
1686
|
+
prefix = expl[:sprite] || 'MISL'
|
|
1687
|
+
frame_letter = prefix == 'MISL' ? %w[B C D][frame_idx] : %w[C D E][frame_idx]
|
|
1688
|
+
|
|
1689
|
+
expl_sprite = @sprites.send(:load_sprite_frame, prefix, frame_letter, 0)
|
|
1374
1690
|
next unless expl_sprite
|
|
1691
|
+
|
|
1375
1692
|
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1376
|
-
|
|
1377
|
-
|
|
1693
|
+
expl_z = expl[:z] || @player_z
|
|
1694
|
+
stub = ProjectileStub.new(expl[:x], expl[:y], 0, 0, 0, expl_z)
|
|
1695
|
+
visible = VisibleSprite.new(stub, expl_sprite, view_x, view_y, view_y, screen_x)
|
|
1696
|
+
draw_sprite(visible)
|
|
1697
|
+
end
|
|
1698
|
+
end
|
|
1699
|
+
|
|
1700
|
+
def render_puffs
|
|
1701
|
+
@combat.puffs.each do |puff|
|
|
1702
|
+
view_x, view_y = transform_point(puff[:x], puff[:y])
|
|
1703
|
+
next if view_y <= 0
|
|
1704
|
+
|
|
1705
|
+
elapsed = @combat.instance_variable_get(:@tic) - puff[:tic]
|
|
1706
|
+
frame_idx = (elapsed / 3).clamp(0, 3)
|
|
1707
|
+
frame_letter = %w[A B C D][frame_idx]
|
|
1708
|
+
puff_sprite = @sprites.send(:load_sprite_frame, 'PUFF', frame_letter, 0)
|
|
1709
|
+
next unless puff_sprite
|
|
1710
|
+
|
|
1711
|
+
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1712
|
+
stub = ProjectileStub.new(puff[:x], puff[:y], 0, 0, 0, puff[:z])
|
|
1713
|
+
visible = VisibleSprite.new(stub, puff_sprite, view_x, view_y, view_y, screen_x)
|
|
1378
1714
|
draw_sprite(visible)
|
|
1379
1715
|
end
|
|
1380
1716
|
end
|
|
@@ -1400,7 +1736,7 @@ module Doom
|
|
|
1400
1736
|
light = calculate_light(light_level, dist)
|
|
1401
1737
|
|
|
1402
1738
|
# Sprite screen bounds
|
|
1403
|
-
sprite_left = (screen_x - sprite.left_offset * sprite_scale).to_i
|
|
1739
|
+
sprite_left = (screen_x - (sprite.left_offset * sprite_scale)).to_i
|
|
1404
1740
|
sprite_right = sprite_left + sprite_screen_width - 1
|
|
1405
1741
|
|
|
1406
1742
|
# Clamp to screen
|
|
@@ -1408,14 +1744,26 @@ module Doom
|
|
|
1408
1744
|
x2 = [sprite_right, SCREEN_WIDTH - 1].min
|
|
1409
1745
|
return if x1 > x2
|
|
1410
1746
|
|
|
1411
|
-
# Calculate sprite world Z positions
|
|
1747
|
+
# Calculate sprite world Z positions (matching Chocolate Doom R_ProjectSprite)
|
|
1748
|
+
# gzt = mobj->z + spritetopoffset (top of sprite)
|
|
1749
|
+
# gz = gzt - spriteheight (bottom of sprite, 1:1 pixel:unit)
|
|
1412
1750
|
thing_floor = sector ? sector.floor_height : 0
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1751
|
+
base_z = if thing.is_a?(ProjectileStub) && thing.z
|
|
1752
|
+
thing.z
|
|
1753
|
+
else
|
|
1754
|
+
thing_floor
|
|
1755
|
+
end
|
|
1756
|
+
sprite_gzt = base_z + sprite.top_offset
|
|
1757
|
+
sprite_gz = sprite_gzt - sprite.height
|
|
1758
|
+
|
|
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
|
|
1419
1767
|
|
|
1420
1768
|
# Scan drawsegs from back to front for obscuring segs
|
|
1421
1769
|
@drawsegs.reverse_each do |ds|
|
|
@@ -1447,26 +1795,22 @@ module Doom
|
|
|
1447
1795
|
silhouette = ds.silhouette
|
|
1448
1796
|
|
|
1449
1797
|
# If sprite bottom is at or above the bottom silhouette height, don't clip bottom
|
|
1450
|
-
if sprite_gz >= ds.bsilheight
|
|
1451
|
-
silhouette &= ~SIL_BOTTOM
|
|
1452
|
-
end
|
|
1798
|
+
silhouette &= ~SIL_BOTTOM if sprite_gz >= ds.bsilheight
|
|
1453
1799
|
|
|
1454
1800
|
# If sprite top is at or below the top silhouette height, don't clip top
|
|
1455
|
-
if sprite_gzt <= ds.tsilheight
|
|
1456
|
-
silhouette &= ~SIL_TOP
|
|
1457
|
-
end
|
|
1801
|
+
silhouette &= ~SIL_TOP if sprite_gzt <= ds.tsilheight
|
|
1458
1802
|
|
|
1459
1803
|
# Apply clipping for each column in the overlap
|
|
1460
1804
|
(r1..r2).each do |x|
|
|
1461
1805
|
# Index into drawseg's clip arrays (0-based from ds.x1)
|
|
1462
1806
|
ds_idx = x - ds.x1
|
|
1463
1807
|
|
|
1464
|
-
if (silhouette & SIL_BOTTOM) != 0 && clipbot[x] == -2
|
|
1465
|
-
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]
|
|
1466
1810
|
end
|
|
1467
1811
|
|
|
1468
|
-
if (silhouette & SIL_TOP) != 0 && cliptop[x] == -2
|
|
1469
|
-
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]
|
|
1470
1814
|
end
|
|
1471
1815
|
end
|
|
1472
1816
|
end
|
|
@@ -1477,11 +1821,21 @@ module Doom
|
|
|
1477
1821
|
cliptop[x] = -1 if cliptop[x] == -2
|
|
1478
1822
|
end
|
|
1479
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
|
+
|
|
1480
1834
|
# Calculate sprite screen Y positions
|
|
1481
1835
|
sprite_top_world = sprite_gzt - @player_z
|
|
1482
1836
|
sprite_bottom_world = sprite_gz - @player_z
|
|
1483
|
-
sprite_top_screen = (HALF_HEIGHT - sprite_top_world * sprite_scale).to_i
|
|
1484
|
-
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
|
|
1485
1839
|
|
|
1486
1840
|
# Cache for inner loop
|
|
1487
1841
|
framebuffer = @framebuffer
|
|
@@ -1519,7 +1873,7 @@ module Doom
|
|
|
1519
1873
|
next unless color
|
|
1520
1874
|
|
|
1521
1875
|
# Apply lighting and write directly to framebuffer
|
|
1522
|
-
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
1876
|
+
framebuffer[(y * SCREEN_WIDTH) + x] = cmap[color]
|
|
1523
1877
|
end
|
|
1524
1878
|
end
|
|
1525
1879
|
end
|