doom 0.6.0 → 0.8.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/bin/doom +5 -2
- data/lib/doom/game/combat.rb +226 -40
- data/lib/doom/game/intermission.rb +248 -0
- data/lib/doom/game/item_pickup.rb +40 -4
- data/lib/doom/game/menu.rb +342 -0
- data/lib/doom/game/monster_ai.rb +210 -11
- data/lib/doom/game/player_state.rb +10 -1
- data/lib/doom/game/sector_actions.rb +376 -15
- data/lib/doom/game/sound_engine.rb +201 -0
- data/lib/doom/platform/gosu_window.rb +460 -52
- data/lib/doom/render/font.rb +78 -0
- data/lib/doom/render/renderer.rb +361 -33
- data/lib/doom/render/screen_melt.rb +71 -0
- data/lib/doom/render/weapon_renderer.rb +11 -12
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/reader.rb +52 -0
- data/lib/doom/wad/sound.rb +85 -0
- data/lib/doom/wad/sprite.rb +29 -4
- data/lib/doom/wad/texture.rb +1 -1
- data/lib/doom.rb +47 -5
- metadata +7 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Render
|
|
5
|
+
# DOOM's built-in font loaded from STCFN patches (ASCII 33-121).
|
|
6
|
+
# Uppercase only -- lowercase is auto-uppercased.
|
|
7
|
+
class Font
|
|
8
|
+
SPACE_WIDTH = 4
|
|
9
|
+
|
|
10
|
+
def initialize(wad, hud_graphics)
|
|
11
|
+
@chars = {}
|
|
12
|
+
(33..121).each do |ascii|
|
|
13
|
+
name = "STCFN%03d" % ascii
|
|
14
|
+
sprite = hud_graphics.send(:load_graphic, name)
|
|
15
|
+
@chars[ascii] = sprite if sprite
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Draw text into framebuffer at (x, y). Returns width drawn.
|
|
20
|
+
def draw_text(framebuffer, text, x, y, screen_width: 320, screen_height: 240)
|
|
21
|
+
cursor_x = x
|
|
22
|
+
text.upcase.each_char do |char|
|
|
23
|
+
if char == ' '
|
|
24
|
+
cursor_x += SPACE_WIDTH
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sprite = @chars[char.ord]
|
|
29
|
+
next unless sprite
|
|
30
|
+
|
|
31
|
+
draw_char(framebuffer, sprite, cursor_x, y, screen_width, screen_height)
|
|
32
|
+
cursor_x += sprite.width
|
|
33
|
+
end
|
|
34
|
+
cursor_x - x
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Measure text width without drawing
|
|
38
|
+
def text_width(text)
|
|
39
|
+
width = 0
|
|
40
|
+
text.upcase.each_char do |char|
|
|
41
|
+
if char == ' '
|
|
42
|
+
width += SPACE_WIDTH
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
sprite = @chars[char.ord]
|
|
46
|
+
width += sprite.width if sprite
|
|
47
|
+
end
|
|
48
|
+
width
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Draw text centered horizontally
|
|
52
|
+
def draw_centered(framebuffer, text, y, screen_width: 320, screen_height: 240)
|
|
53
|
+
w = text_width(text)
|
|
54
|
+
x = (screen_width - w) / 2
|
|
55
|
+
draw_text(framebuffer, text, x, y, screen_width: screen_width, screen_height: screen_height)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def draw_char(framebuffer, sprite, x, y, screen_width, screen_height)
|
|
61
|
+
sprite.width.times do |col_x|
|
|
62
|
+
sx = x + col_x
|
|
63
|
+
next if sx < 0 || sx >= screen_width
|
|
64
|
+
|
|
65
|
+
col = sprite.column_pixels(col_x)
|
|
66
|
+
next unless col
|
|
67
|
+
|
|
68
|
+
col.each_with_index do |color, col_y|
|
|
69
|
+
next unless color
|
|
70
|
+
sy = y + col_y
|
|
71
|
+
next if sy < 0 || sy >= screen_height
|
|
72
|
+
framebuffer[sy * screen_width + sx] = color
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
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
|
|
@@ -250,8 +254,8 @@ module Doom
|
|
|
250
254
|
@sprite_floor_clip.replace(@floor_clip)
|
|
251
255
|
@sprite_wall_depth.replace(@wall_depth)
|
|
252
256
|
|
|
253
|
-
#
|
|
254
|
-
|
|
257
|
+
# R_DrawMasked: render sprites and masked middle textures interleaved
|
|
258
|
+
draw_masked if @sprites
|
|
255
259
|
end
|
|
256
260
|
|
|
257
261
|
# Precompute column-based data for floor/ceiling rendering (R_InitLightTables-like)
|
|
@@ -924,8 +928,27 @@ module Doom
|
|
|
924
928
|
tsilheight = -Float::INFINITY
|
|
925
929
|
end
|
|
926
930
|
|
|
931
|
+
# Detect masked middle texture (grates, bars, fences)
|
|
932
|
+
has_masked = false
|
|
933
|
+
@current_masked_cols = nil
|
|
934
|
+
if back_sector && sidedef
|
|
935
|
+
mid_tex = sidedef.middle_texture
|
|
936
|
+
if mid_tex && mid_tex != '-' && !mid_tex.empty?
|
|
937
|
+
has_masked = true
|
|
938
|
+
@current_masked_cols = Array.new(x2 - x1 + 1)
|
|
939
|
+
# Force silhouette so sprites get clipped against this seg
|
|
940
|
+
if (silhouette & SIL_TOP) == 0
|
|
941
|
+
silhouette |= SIL_TOP
|
|
942
|
+
tsilheight = -Float::INFINITY
|
|
943
|
+
end
|
|
944
|
+
if (silhouette & SIL_BOTTOM) == 0
|
|
945
|
+
silhouette |= SIL_BOTTOM
|
|
946
|
+
bsilheight = Float::INFINITY
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
|
|
927
951
|
# Check planes for this seg range (R_CheckPlane equivalent)
|
|
928
|
-
# This may create new visplanes if column ranges would overlap
|
|
929
952
|
if @current_floor_plane
|
|
930
953
|
@current_floor_plane = check_plane(@current_floor_plane, x1, x2)
|
|
931
954
|
end
|
|
@@ -1067,6 +1090,11 @@ module Doom
|
|
|
1067
1090
|
sector.light_level, tex_col, lower_tex_y, scale, back_sector.floor_height, sector.floor_height)
|
|
1068
1091
|
end
|
|
1069
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
|
+
|
|
1070
1098
|
# Update clip bounds
|
|
1071
1099
|
if closed_door
|
|
1072
1100
|
@wall_depth[x] = [@wall_depth[x], dist].min
|
|
@@ -1139,22 +1167,26 @@ module Doom
|
|
|
1139
1167
|
end
|
|
1140
1168
|
end
|
|
1141
1169
|
|
|
1142
|
-
# Save drawseg for sprite clipping
|
|
1143
|
-
|
|
1144
|
-
if silhouette != SIL_NONE
|
|
1170
|
+
# Save drawseg for sprite clipping and masked rendering
|
|
1171
|
+
if silhouette != SIL_NONE || has_masked
|
|
1145
1172
|
sprtopclip = @ceiling_clip[x1..x2].dup
|
|
1146
1173
|
sprbottomclip = @floor_clip[x1..x2].dup
|
|
1174
|
+
scalestep = (x2 > x1) ? (scale2 - scale1) / (x2 - x1) : 0
|
|
1147
1175
|
|
|
1148
1176
|
drawseg = Drawseg.new(
|
|
1149
1177
|
x1, x2,
|
|
1150
|
-
scale1, scale2,
|
|
1178
|
+
scale1, scale2, scalestep,
|
|
1151
1179
|
silhouette,
|
|
1152
1180
|
bsilheight, tsilheight,
|
|
1153
1181
|
sprtopclip, sprbottomclip,
|
|
1154
|
-
seg
|
|
1182
|
+
seg,
|
|
1183
|
+
has_masked ? @current_masked_cols : nil,
|
|
1184
|
+
sector, back_sector, sidedef,
|
|
1185
|
+
dist1, dist2, sx1.to_f, sx2.to_f
|
|
1155
1186
|
)
|
|
1156
1187
|
@drawsegs << drawseg
|
|
1157
1188
|
end
|
|
1189
|
+
@current_masked_cols = nil
|
|
1158
1190
|
end
|
|
1159
1191
|
|
|
1160
1192
|
# Wall column drawing with proper texture mapping
|
|
@@ -1209,7 +1241,110 @@ module Doom
|
|
|
1209
1241
|
tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i % tex_height
|
|
1210
1242
|
|
|
1211
1243
|
color = column[tex_y]
|
|
1212
|
-
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
1244
|
+
framebuffer[y * SCREEN_WIDTH + x] = cmap[color] if color
|
|
1245
|
+
y += 1
|
|
1246
|
+
end
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
# Cache which textures have transparent pixels
|
|
1250
|
+
def texture_has_transparency?(texture)
|
|
1251
|
+
@transparency_cache ||= {}
|
|
1252
|
+
name = texture.name
|
|
1253
|
+
return @transparency_cache[name] if @transparency_cache.key?(name)
|
|
1254
|
+
@transparency_cache[name] = texture.width.times.any? do |x|
|
|
1255
|
+
col = texture.column_pixels(x)
|
|
1256
|
+
col&.any?(&:nil?)
|
|
1257
|
+
end
|
|
1258
|
+
end
|
|
1259
|
+
|
|
1260
|
+
# Draw a deferred masked column using per-drawseg clip values saved at BSP time
|
|
1261
|
+
def draw_wall_column_masked_deferred(md)
|
|
1262
|
+
x = md[:x]
|
|
1263
|
+
tex_name = md[:tex]
|
|
1264
|
+
return if tex_name.nil? || tex_name.empty? || tex_name == '-'
|
|
1265
|
+
|
|
1266
|
+
# Use saved clips (from BSP time, before far walls closed them)
|
|
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
|
|
1288
|
+
|
|
1289
|
+
scale = md[:scale]
|
|
1290
|
+
tex_step = 1.0 / scale
|
|
1291
|
+
unclipped_y1 = HALF_HEIGHT - (md[:world_top] - @player_z) * scale
|
|
1292
|
+
tex_y_at_y1 = md[:tex_y] + (y1 - unclipped_y1) * tex_step
|
|
1293
|
+
|
|
1294
|
+
y1 = 0 if y1 < 0
|
|
1295
|
+
y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT
|
|
1296
|
+
|
|
1297
|
+
y = y1
|
|
1298
|
+
while y <= y2
|
|
1299
|
+
screen_offset = y - y1
|
|
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
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
# Draw a masked (transparent) wall column for middle textures on two-sided linedefs.
|
|
1308
|
+
# Unlike draw_wall_column_ex, skips nil pixels (transparent areas).
|
|
1309
|
+
def draw_wall_column_masked(x, y1, y2, texture_name, dist, light_level, tex_col, tex_y_start, scale, world_top)
|
|
1310
|
+
return if y1 > y2
|
|
1311
|
+
return if texture_name.nil? || texture_name.empty? || texture_name == '-'
|
|
1312
|
+
|
|
1313
|
+
clip_top = @ceiling_clip[x] + 1
|
|
1314
|
+
clip_bottom = @floor_clip[x] - 1
|
|
1315
|
+
y1 = [y1, clip_top].max
|
|
1316
|
+
y2 = [y2, clip_bottom].min
|
|
1317
|
+
return if y1 > y2
|
|
1318
|
+
|
|
1319
|
+
texture = @textures[anim_texture(texture_name)]
|
|
1320
|
+
return unless texture
|
|
1321
|
+
|
|
1322
|
+
light = calculate_light(light_level, dist)
|
|
1323
|
+
cmap = @colormap.maps[light]
|
|
1324
|
+
framebuffer = @framebuffer
|
|
1325
|
+
tex_width = texture.width
|
|
1326
|
+
tex_height = texture.height
|
|
1327
|
+
|
|
1328
|
+
tex_x = tex_col.to_i % tex_width
|
|
1329
|
+
column = texture.column_pixels(tex_x)
|
|
1330
|
+
return unless column
|
|
1331
|
+
|
|
1332
|
+
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
|
+
|
|
1336
|
+
y1 = 0 if y1 < 0
|
|
1337
|
+
y2 = SCREEN_HEIGHT - 1 if y2 >= SCREEN_HEIGHT
|
|
1338
|
+
|
|
1339
|
+
y = y1
|
|
1340
|
+
while y <= y2
|
|
1341
|
+
screen_offset = y - y1
|
|
1342
|
+
tex_y = (tex_y_at_y1 + screen_offset * tex_step).to_i % tex_height
|
|
1343
|
+
color = column[tex_y]
|
|
1344
|
+
# Skip transparent pixels (nil in patch data)
|
|
1345
|
+
if color
|
|
1346
|
+
framebuffer[y * SCREEN_WIDTH + x] = cmap[color]
|
|
1347
|
+
end
|
|
1213
1348
|
y += 1
|
|
1214
1349
|
end
|
|
1215
1350
|
end
|
|
@@ -1307,12 +1442,23 @@ module Doom
|
|
|
1307
1442
|
# Get sprite: death frame > walking frame > idle frame
|
|
1308
1443
|
if @combat && @combat.dead?(thing_idx)
|
|
1309
1444
|
sprite = @combat.death_sprite(thing_idx, thing.type, angle_to_thing, thing.angle)
|
|
1445
|
+
next unless sprite # Barrel disappeared after explosion
|
|
1310
1446
|
elsif @monster_ai
|
|
1311
|
-
mon = @monster_ai.
|
|
1447
|
+
mon = @monster_ai.monster_by_thing_idx[thing_idx]
|
|
1312
1448
|
if mon && mon.active
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1449
|
+
if mon.attacking
|
|
1450
|
+
# Attack animation: show attack frames (E, F, G...)
|
|
1451
|
+
prefix = @sprites.prefix_for(thing.type)
|
|
1452
|
+
atk_frames = Game::MonsterAI::ATTACK_FRAMES[prefix]
|
|
1453
|
+
if atk_frames
|
|
1454
|
+
frame_idx = (mon.attack_frame_tic / Game::MonsterAI::ATTACK_FRAME_TICS).clamp(0, atk_frames.size - 1)
|
|
1455
|
+
sprite = @sprites.get_frame(thing.type, atk_frames[frame_idx], angle_to_thing, thing.angle)
|
|
1456
|
+
end
|
|
1457
|
+
else
|
|
1458
|
+
# Walking animation: cycle through frames A-D
|
|
1459
|
+
walk_frame = %w[A B C D][@leveltime / 4 % 4]
|
|
1460
|
+
sprite = @sprites.get_frame(thing.type, walk_frame, angle_to_thing, thing.angle)
|
|
1461
|
+
end
|
|
1316
1462
|
end
|
|
1317
1463
|
sprite ||= @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
|
|
1318
1464
|
else
|
|
@@ -1334,47 +1480,222 @@ module Doom
|
|
|
1334
1480
|
# Sort by distance (back to front for proper overdraw)
|
|
1335
1481
|
visible_sprites.sort_by! { |s| -s.dist }
|
|
1336
1482
|
|
|
1337
|
-
# Draw each sprite
|
|
1483
|
+
# Draw each sprite with drawseg interleaving
|
|
1338
1484
|
visible_sprites.each do |vs|
|
|
1339
|
-
|
|
1485
|
+
draw_sprite_with_masking(vs)
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
# Draw remaining masked segs not triggered by sprites
|
|
1489
|
+
@drawsegs.reverse_each do |ds|
|
|
1490
|
+
next unless ds.maskedtexturecol
|
|
1491
|
+
render_masked_seg_range(ds, ds.x1, ds.x2)
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
# Draw projectiles, explosions, and bullet puffs
|
|
1495
|
+
if @combat
|
|
1496
|
+
render_projectiles
|
|
1497
|
+
render_puffs
|
|
1340
1498
|
end
|
|
1499
|
+
end
|
|
1341
1500
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1501
|
+
# Chocolate Doom R_DrawMasked: interleave sprites with masked drawsegs
|
|
1502
|
+
def draw_masked
|
|
1503
|
+
render_sprites
|
|
1344
1504
|
end
|
|
1345
1505
|
|
|
1506
|
+
# Draw sprite, rendering masked drawsegs behind it first (R_DrawSprite)
|
|
1507
|
+
def draw_sprite_with_masking(spr)
|
|
1508
|
+
sprite_scale = @projection / spr.dist
|
|
1509
|
+
|
|
1510
|
+
# Scan drawsegs newest to oldest (nearest to farthest)
|
|
1511
|
+
@drawsegs.reverse_each do |ds|
|
|
1512
|
+
next if ds.x1 > spr.screen_x.to_i + spr.sprite.width || ds.x2 < spr.screen_x.to_i - spr.sprite.width
|
|
1513
|
+
next if ds.silhouette == SIL_NONE && ds.maskedtexturecol.nil?
|
|
1514
|
+
|
|
1515
|
+
ds_max_scale = [ds.scale1, ds.scale2].max
|
|
1516
|
+
ds_min_scale = [ds.scale1, ds.scale2].min
|
|
1517
|
+
|
|
1518
|
+
# Is the drawseg behind the sprite?
|
|
1519
|
+
if ds_max_scale < sprite_scale
|
|
1520
|
+
# Draw masked texture behind the sprite
|
|
1521
|
+
if ds.maskedtexturecol
|
|
1522
|
+
r1 = [ds.x1, spr.screen_x.to_i - spr.sprite.width].max
|
|
1523
|
+
r2 = [ds.x2, spr.screen_x.to_i + spr.sprite.width].min
|
|
1524
|
+
render_masked_seg_range(ds, r1, r2)
|
|
1525
|
+
end
|
|
1526
|
+
next # Don't clip against things behind
|
|
1527
|
+
end
|
|
1528
|
+
end
|
|
1529
|
+
|
|
1530
|
+
draw_sprite(spr)
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
# Chocolate Doom R_RenderMaskedSegRange
|
|
1534
|
+
def render_masked_seg_range(ds, rx1, rx2)
|
|
1535
|
+
return unless ds.maskedtexturecol
|
|
1536
|
+
return unless ds.sidedef
|
|
1537
|
+
|
|
1538
|
+
mid_tex_name = ds.sidedef.middle_texture
|
|
1539
|
+
return if mid_tex_name.nil? || mid_tex_name == '-' || mid_tex_name.empty?
|
|
1540
|
+
|
|
1541
|
+
texture = @textures[anim_texture(mid_tex_name)]
|
|
1542
|
+
return unless texture
|
|
1543
|
+
|
|
1544
|
+
front = ds.frontsector
|
|
1545
|
+
back = ds.backsector
|
|
1546
|
+
return unless front && back
|
|
1547
|
+
|
|
1548
|
+
# Texture anchoring (matching Chocolate Doom)
|
|
1549
|
+
if ds.curline && @map.linedefs[ds.curline.linedef].lower_unpegged?
|
|
1550
|
+
higher_floor = [front.floor_height, back.floor_height].max
|
|
1551
|
+
dc_texturemid = higher_floor + texture.height - @player_z
|
|
1552
|
+
else
|
|
1553
|
+
lower_ceiling = [front.ceiling_height, back.ceiling_height].min
|
|
1554
|
+
dc_texturemid = lower_ceiling - @player_z
|
|
1555
|
+
end
|
|
1556
|
+
dc_texturemid += ds.sidedef.y_offset
|
|
1557
|
+
|
|
1558
|
+
light_level = front.light_level
|
|
1559
|
+
|
|
1560
|
+
# Iterate columns
|
|
1561
|
+
rx1 = [rx1, ds.x1].max
|
|
1562
|
+
rx2 = [rx2, ds.x2].min
|
|
1563
|
+
|
|
1564
|
+
(rx1..rx2).each do |x|
|
|
1565
|
+
col_idx = x - ds.x1
|
|
1566
|
+
next if col_idx < 0 || col_idx >= ds.maskedtexturecol.size
|
|
1567
|
+
|
|
1568
|
+
tex_col = ds.maskedtexturecol[col_idx]
|
|
1569
|
+
next unless tex_col # nil = already drawn or empty
|
|
1570
|
+
|
|
1571
|
+
# Per-column distance using same 1/z interpolation as wall renderer
|
|
1572
|
+
if ds.dist1 && ds.dist2 && ds.dist1 > 0 && ds.dist2 > 0 && ds.sx2 != ds.sx1
|
|
1573
|
+
t = ((x - ds.sx1) / (ds.sx2 - ds.sx1)).clamp(0.0, 1.0)
|
|
1574
|
+
inv_dist = (1.0 - t) / ds.dist1 + t / ds.dist2
|
|
1575
|
+
col_dist = 1.0 / inv_dist
|
|
1576
|
+
else
|
|
1577
|
+
col_dist = ds.dist1 || 1.0
|
|
1578
|
+
end
|
|
1579
|
+
next if col_dist < 1
|
|
1580
|
+
spryscale = @projection / col_dist
|
|
1581
|
+
spryscale = [spryscale, 64.0].min
|
|
1582
|
+
|
|
1583
|
+
# Screen Y of texture top
|
|
1584
|
+
sprtopscreen = HALF_HEIGHT - dc_texturemid * spryscale
|
|
1585
|
+
|
|
1586
|
+
# Get clip bounds from drawseg
|
|
1587
|
+
clip_idx = x - ds.x1
|
|
1588
|
+
mceilingclip = ds.sprtopclip[clip_idx]
|
|
1589
|
+
mfloorclip = ds.sprbottomclip[clip_idx]
|
|
1590
|
+
|
|
1591
|
+
# Calculate column draw range
|
|
1592
|
+
tex_x = tex_col.to_i % texture.width
|
|
1593
|
+
column = texture.column_pixels(tex_x)
|
|
1594
|
+
next unless column
|
|
1595
|
+
|
|
1596
|
+
iscale = 1.0 / spryscale
|
|
1597
|
+
|
|
1598
|
+
light = calculate_light(light_level, col_dist)
|
|
1599
|
+
cmap = @colormap.maps[light]
|
|
1600
|
+
|
|
1601
|
+
# Draw each non-nil run of pixels (transparency)
|
|
1602
|
+
tex_height = texture.height
|
|
1603
|
+
y_start = nil
|
|
1604
|
+
(0..tex_height).each do |ty|
|
|
1605
|
+
color = ty < tex_height ? column[ty] : nil
|
|
1606
|
+
if color
|
|
1607
|
+
y_start = ty unless y_start
|
|
1608
|
+
elsif y_start
|
|
1609
|
+
# 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
|
|
1612
|
+
top_y = [top_y, mceilingclip + 1].max
|
|
1613
|
+
bot_y = [bot_y, mfloorclip - 1].min
|
|
1614
|
+
|
|
1615
|
+
if top_y <= bot_y
|
|
1616
|
+
top_y = [top_y, 0].max
|
|
1617
|
+
bot_y = [bot_y, SCREEN_HEIGHT - 1].min
|
|
1618
|
+
tex_frac = y_start + (top_y - (sprtopscreen + y_start * spryscale)) * iscale
|
|
1619
|
+
y = top_y
|
|
1620
|
+
while y <= bot_y
|
|
1621
|
+
t = ((tex_frac).to_i % tex_height)
|
|
1622
|
+
c = column[t]
|
|
1623
|
+
@framebuffer[y * SCREEN_WIDTH + x] = cmap[c] if c
|
|
1624
|
+
tex_frac += iscale
|
|
1625
|
+
y += 1
|
|
1626
|
+
end
|
|
1627
|
+
end
|
|
1628
|
+
y_start = nil
|
|
1629
|
+
end
|
|
1630
|
+
end
|
|
1631
|
+
|
|
1632
|
+
# Mark column as drawn
|
|
1633
|
+
ds.maskedtexturecol[col_idx] = nil
|
|
1634
|
+
end
|
|
1635
|
+
end
|
|
1636
|
+
|
|
1637
|
+
# Stub for projectiles/explosions that carries a z height
|
|
1638
|
+
ProjectileStub = Struct.new(:x, :y, :angle, :type, :flags, :z)
|
|
1639
|
+
|
|
1346
1640
|
def render_projectiles
|
|
1347
|
-
# Render
|
|
1641
|
+
# Render all projectiles in flight
|
|
1348
1642
|
@combat.projectiles.each do |proj|
|
|
1349
1643
|
view_x, view_y = transform_point(proj.x, proj.y)
|
|
1350
1644
|
next if view_y <= 0
|
|
1351
1645
|
|
|
1352
|
-
|
|
1646
|
+
prefix = proj.sprite_prefix || 'MISL'
|
|
1647
|
+
|
|
1648
|
+
# Try rotation-based sprite first (rockets, baron fireballs)
|
|
1353
1649
|
rocket_angle = Math.atan2(proj.dy, proj.dx)
|
|
1354
1650
|
viewer_angle = Math.atan2(proj.y - @player_y, proj.x - @player_x)
|
|
1355
1651
|
angle_diff = (viewer_angle - rocket_angle) % (2 * Math::PI)
|
|
1356
1652
|
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
1357
|
-
|
|
1358
|
-
|
|
1653
|
+
|
|
1654
|
+
# Animate flight: cycle A/B frames
|
|
1655
|
+
flight_frame = %w[A B][@leveltime / 4 % 2]
|
|
1656
|
+
proj_sprite = @sprites.send(:load_sprite_frame, prefix, flight_frame, rotation)
|
|
1657
|
+
proj_sprite ||= @sprites.send(:load_sprite_frame, prefix, flight_frame, 0)
|
|
1658
|
+
proj_sprite ||= @sprites.send(:load_sprite_frame, prefix, 'A', 0)
|
|
1659
|
+
next unless proj_sprite
|
|
1359
1660
|
|
|
1360
1661
|
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1361
|
-
|
|
1362
|
-
visible = VisibleSprite.new(
|
|
1662
|
+
stub = ProjectileStub.new(proj.x, proj.y, 0, 0, 0, proj.z)
|
|
1663
|
+
visible = VisibleSprite.new(stub, proj_sprite, view_x, view_y, view_y, screen_x)
|
|
1363
1664
|
draw_sprite(visible)
|
|
1364
1665
|
end
|
|
1365
1666
|
|
|
1366
|
-
# Render explosions
|
|
1667
|
+
# Render explosions at projectile height
|
|
1367
1668
|
@combat.explosions.each do |expl|
|
|
1368
1669
|
view_x, view_y = transform_point(expl[:x], expl[:y])
|
|
1369
1670
|
next if view_y <= 0
|
|
1370
1671
|
elapsed = (@combat.instance_variable_get(:@tic) - expl[:tic])
|
|
1371
1672
|
frame_idx = (elapsed / 4).clamp(0, 2)
|
|
1372
|
-
|
|
1373
|
-
|
|
1673
|
+
|
|
1674
|
+
prefix = expl[:sprite] || 'MISL'
|
|
1675
|
+
frame_letter = (prefix == 'MISL') ? %w[B C D][frame_idx] : %w[C D E][frame_idx]
|
|
1676
|
+
|
|
1677
|
+
expl_sprite = @sprites.send(:load_sprite_frame, prefix, frame_letter, 0)
|
|
1374
1678
|
next unless expl_sprite
|
|
1375
1679
|
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1376
|
-
|
|
1377
|
-
|
|
1680
|
+
expl_z = expl[:z] || @player_z
|
|
1681
|
+
stub = ProjectileStub.new(expl[:x], expl[:y], 0, 0, 0, expl_z)
|
|
1682
|
+
visible = VisibleSprite.new(stub, expl_sprite, view_x, view_y, view_y, screen_x)
|
|
1683
|
+
draw_sprite(visible)
|
|
1684
|
+
end
|
|
1685
|
+
end
|
|
1686
|
+
|
|
1687
|
+
def render_puffs
|
|
1688
|
+
@combat.puffs.each do |puff|
|
|
1689
|
+
view_x, view_y = transform_point(puff[:x], puff[:y])
|
|
1690
|
+
next if view_y <= 0
|
|
1691
|
+
elapsed = @combat.instance_variable_get(:@tic) - puff[:tic]
|
|
1692
|
+
frame_idx = (elapsed / 3).clamp(0, 3)
|
|
1693
|
+
frame_letter = %w[A B C D][frame_idx]
|
|
1694
|
+
puff_sprite = @sprites.send(:load_sprite_frame, 'PUFF', frame_letter, 0)
|
|
1695
|
+
next unless puff_sprite
|
|
1696
|
+
screen_x = HALF_WIDTH + (view_x * @projection / view_y)
|
|
1697
|
+
stub = ProjectileStub.new(puff[:x], puff[:y], 0, 0, 0, puff[:z])
|
|
1698
|
+
visible = VisibleSprite.new(stub, puff_sprite, view_x, view_y, view_y, screen_x)
|
|
1378
1699
|
draw_sprite(visible)
|
|
1379
1700
|
end
|
|
1380
1701
|
end
|
|
@@ -1408,10 +1729,17 @@ module Doom
|
|
|
1408
1729
|
x2 = [sprite_right, SCREEN_WIDTH - 1].min
|
|
1409
1730
|
return if x1 > x2
|
|
1410
1731
|
|
|
1411
|
-
# Calculate sprite world Z positions
|
|
1732
|
+
# Calculate sprite world Z positions (matching Chocolate Doom R_ProjectSprite)
|
|
1733
|
+
# gzt = mobj->z + spritetopoffset (top of sprite)
|
|
1734
|
+
# gz = gzt - spriteheight (bottom of sprite, 1:1 pixel:unit)
|
|
1412
1735
|
thing_floor = sector ? sector.floor_height : 0
|
|
1413
|
-
|
|
1414
|
-
|
|
1736
|
+
if thing.is_a?(ProjectileStub) && thing.z
|
|
1737
|
+
base_z = thing.z
|
|
1738
|
+
else
|
|
1739
|
+
base_z = thing_floor
|
|
1740
|
+
end
|
|
1741
|
+
sprite_gzt = base_z + sprite.top_offset
|
|
1742
|
+
sprite_gz = sprite_gzt - sprite.height
|
|
1415
1743
|
|
|
1416
1744
|
# Initialize per-column clip arrays (-2 = not yet clipped)
|
|
1417
1745
|
clipbot = Array.new(SCREEN_WIDTH, -2)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Render
|
|
5
|
+
# DOOM's screen melt/wipe effect from wipe.c.
|
|
6
|
+
# Each column slides down at a slightly different speed,
|
|
7
|
+
# revealing the new screen underneath.
|
|
8
|
+
class ScreenMelt
|
|
9
|
+
WIDTH = SCREEN_WIDTH
|
|
10
|
+
HEIGHT = SCREEN_HEIGHT
|
|
11
|
+
MELT_SPEED = HEIGHT / 30 # ~8 pixels per frame (Chocolate Doom does HEIGHT/15 at 35fps)
|
|
12
|
+
|
|
13
|
+
def initialize(old_screen, new_screen)
|
|
14
|
+
# Snapshot both screens (arrays of palette indices, 320x240)
|
|
15
|
+
@old = old_screen.dup
|
|
16
|
+
@new = new_screen.dup
|
|
17
|
+
@done = false
|
|
18
|
+
|
|
19
|
+
# Initialize column offsets (from wipe_initMelt in wipe.c)
|
|
20
|
+
# Column 0 gets random negative offset, each subsequent column
|
|
21
|
+
# varies by -1/0/+1 from previous, creating a jagged melt line
|
|
22
|
+
@y = Array.new(WIDTH)
|
|
23
|
+
@y[0] = -(rand(16))
|
|
24
|
+
(1...WIDTH).each do |i|
|
|
25
|
+
@y[i] = @y[i - 1] + (rand(3) - 1)
|
|
26
|
+
@y[i] = -15 if @y[i] < -15
|
|
27
|
+
@y[i] = 0 if @y[i] > 0
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def done?
|
|
32
|
+
@done
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Advance one tic. Returns the composited framebuffer.
|
|
36
|
+
def update(framebuffer)
|
|
37
|
+
all_done = true
|
|
38
|
+
|
|
39
|
+
WIDTH.times do |x|
|
|
40
|
+
if @y[x] < 0
|
|
41
|
+
@y[x] += 1
|
|
42
|
+
all_done = false
|
|
43
|
+
# Column hasn't started melting yet - show old screen
|
|
44
|
+
HEIGHT.times { |row| framebuffer[row * WIDTH + x] = @old[row * WIDTH + x] }
|
|
45
|
+
elsif @y[x] < HEIGHT
|
|
46
|
+
all_done = false
|
|
47
|
+
dy = @y[x]
|
|
48
|
+
|
|
49
|
+
# Top part: new screen revealed
|
|
50
|
+
dy.times do |row|
|
|
51
|
+
framebuffer[row * WIDTH + x] = @new[row * WIDTH + x]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Bottom part: old screen shifted down
|
|
55
|
+
(dy...HEIGHT).each do |row|
|
|
56
|
+
src_row = row - dy
|
|
57
|
+
framebuffer[row * WIDTH + x] = @old[src_row * WIDTH + x]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@y[x] += MELT_SPEED
|
|
61
|
+
else
|
|
62
|
+
# Column fully melted - show new screen
|
|
63
|
+
HEIGHT.times { |row| framebuffer[row * WIDTH + x] = @new[row * WIDTH + x] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@done = all_done
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|