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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Render
|
|
5
|
+
# Experimental renderer path. It deliberately reuses the proven BSP
|
|
6
|
+
# visibility and texture sampling code while replacing the visibility
|
|
7
|
+
# target with a full, per-pixel depth buffer. This makes it possible to add
|
|
8
|
+
# mesh rasterisation and ray traced passes without changing game logic.
|
|
9
|
+
class ZBufferRenderer < Renderer
|
|
10
|
+
attr_reader :depth_buffer
|
|
11
|
+
|
|
12
|
+
MAX_DYNAMIC_LIGHTS = 8
|
|
13
|
+
LIGHT_LEVELS = 16
|
|
14
|
+
Light = Struct.new(:x, :y, :z, :radius, :energy)
|
|
15
|
+
|
|
16
|
+
def initialize(...)
|
|
17
|
+
super
|
|
18
|
+
@depth_buffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, Float::INFINITY)
|
|
19
|
+
@light_lut = build_light_lut
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_frame
|
|
23
|
+
@depth_buffer.fill(Float::INFINITY)
|
|
24
|
+
@dynamic_lights = collect_dynamic_lights
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Opaque walls are the first geometry producer for the new depth target.
|
|
29
|
+
# BSP clipping remains the visibility algorithm for this initial stage;
|
|
30
|
+
# later raster passes can consume the per-pixel depth values written here.
|
|
31
|
+
def draw_wall_column_ex(x, y1, y2, texture_name, dist, light_level,
|
|
32
|
+
tex_col, tex_y_start, scale, world_top)
|
|
33
|
+
clip_top = @ceiling_clip[x] + 1
|
|
34
|
+
clip_bottom = @floor_clip[x] - 1
|
|
35
|
+
first = [[y1, clip_top, 0].max, SCREEN_HEIGHT].min
|
|
36
|
+
last = [[y2, clip_bottom, SCREEN_HEIGHT - 1].min, -1].max
|
|
37
|
+
|
|
38
|
+
super
|
|
39
|
+
|
|
40
|
+
return if first > last || texture_name.nil? || texture_name.empty? || texture_name == '-'
|
|
41
|
+
return unless @textures[anim_texture(texture_name)]
|
|
42
|
+
|
|
43
|
+
# Reconstruct the world-space wall hit. Each dynamic source casts a
|
|
44
|
+
# visibility ray to it; one-sided linedefs produce hard shadows.
|
|
45
|
+
ray_distance = dist * @column_distscale[x]
|
|
46
|
+
hit_x = @player_x + (ray_distance * @column_cos[x])
|
|
47
|
+
hit_y = @player_y - (ray_distance * @column_sin[x])
|
|
48
|
+
visible_lights = @dynamic_lights.select do |light|
|
|
49
|
+
dx = light.x - hit_x
|
|
50
|
+
dy = light.y - hit_y
|
|
51
|
+
(dx * dx) + (dy * dy) < light.radius * light.radius && light_visible?(hit_x, hit_y, light)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
y = first
|
|
55
|
+
while y <= last
|
|
56
|
+
offset = (y * SCREEN_WIDTH) + x
|
|
57
|
+
@depth_buffer[offset] = dist if dist < @depth_buffer[offset]
|
|
58
|
+
unless visible_lights.empty?
|
|
59
|
+
world_z = world_top - ((y - (HALF_HEIGHT - ((world_top - @player_z) * scale))) / scale)
|
|
60
|
+
intensity = visible_lights.sum do |light|
|
|
61
|
+
distance = Math.sqrt(((light.x - hit_x)**2) + ((light.y - hit_y)**2) + ((light.z - world_z)**2))
|
|
62
|
+
distance < light.radius ? light.energy * ((1.0 - (distance / light.radius))**2) : 0.0
|
|
63
|
+
end
|
|
64
|
+
level = (intensity * (LIGHT_LEVELS - 1)).to_i.clamp(0, LIGHT_LEVELS - 1)
|
|
65
|
+
@framebuffer[offset] = @light_lut[level][@framebuffer[offset]] if level.positive?
|
|
66
|
+
end
|
|
67
|
+
y += 1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def collect_dynamic_lights
|
|
74
|
+
return [] unless @combat
|
|
75
|
+
|
|
76
|
+
lights = @combat.projectiles.map do |projectile|
|
|
77
|
+
Light.new(projectile.x, projectile.y, projectile.z, 224.0, 1.0)
|
|
78
|
+
end
|
|
79
|
+
# Hitscan weapons have no projectile object. Their first attack frames
|
|
80
|
+
# still produce a real, short-lived muzzle light at the camera.
|
|
81
|
+
if @view_player&.state&.attacking && @view_player.state.attack_frame <= 1
|
|
82
|
+
lights << Light.new(
|
|
83
|
+
@view_player.x + (@view_player.cos_angle * 24.0),
|
|
84
|
+
@view_player.y + (@view_player.sin_angle * 24.0),
|
|
85
|
+
@view_player.z,
|
|
86
|
+
448.0,
|
|
87
|
+
1.8
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
@combat.puffs.each do |puff|
|
|
91
|
+
age = @leveltime - puff[:tic]
|
|
92
|
+
energy = (1.0 - (age / 12.0)).clamp(0.0, 1.0)
|
|
93
|
+
lights << Light.new(puff[:x], puff[:y], puff[:z], 192.0, energy * 0.8)
|
|
94
|
+
end
|
|
95
|
+
@combat.explosions.each do |explosion|
|
|
96
|
+
age = @leveltime - explosion[:tic]
|
|
97
|
+
energy = (1.0 - (age / 20.0)).clamp(0.0, 1.0)
|
|
98
|
+
lights << Light.new(explosion[:x], explosion[:y], explosion[:z] || @player_z,
|
|
99
|
+
384.0, energy * 1.5)
|
|
100
|
+
end
|
|
101
|
+
lights.sort_by { |light| ((light.x - @player_x)**2) + ((light.y - @player_y)**2) }
|
|
102
|
+
.first(MAX_DYNAMIC_LIGHTS)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A 2D ray query matches Doom's extruded sector geometry. Portal-aware
|
|
106
|
+
# height tests can later replace this conservative one-sided-wall test.
|
|
107
|
+
def light_visible?(from_x, from_y, light)
|
|
108
|
+
@map.linedefs.none? do |line|
|
|
109
|
+
next false if line.two_sided?
|
|
110
|
+
|
|
111
|
+
a = @map.vertices[line.v1]
|
|
112
|
+
b = @map.vertices[line.v2]
|
|
113
|
+
segments_intersect?(from_x, from_y, light.x, light.y, a.x, a.y, b.x, b.y)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def segments_intersect?(ax, ay, bx, by, cx, cy, dx, dy)
|
|
118
|
+
abx = bx - ax
|
|
119
|
+
aby = by - ay
|
|
120
|
+
cdx = dx - cx
|
|
121
|
+
cdy = dy - cy
|
|
122
|
+
denominator = (abx * cdy) - (aby * cdx)
|
|
123
|
+
return false if denominator.abs < 1e-9
|
|
124
|
+
|
|
125
|
+
acx = cx - ax
|
|
126
|
+
acy = cy - ay
|
|
127
|
+
t = ((acx * cdy) - (acy * cdx)) / denominator.to_f
|
|
128
|
+
u = ((acx * aby) - (acy * abx)) / denominator.to_f
|
|
129
|
+
t > 1e-4 && t < 0.9999 && u >= 0.0 && u <= 1.0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Doom's target is palette-indexed. Precompute nearest palette entries
|
|
133
|
+
# for a warm additive light so the geometry pass remains deterministic.
|
|
134
|
+
def build_light_lut
|
|
135
|
+
colors = @palette.colors
|
|
136
|
+
LIGHT_LEVELS.times.map do |level|
|
|
137
|
+
amount = level.to_f / (LIGHT_LEVELS - 1)
|
|
138
|
+
colors.map do |red, green, blue|
|
|
139
|
+
target = [red + (255 * amount), green + (176 * amount), blue + (72 * amount)]
|
|
140
|
+
.map { |channel| channel.clamp(0, 255) }
|
|
141
|
+
colors.each_index.min_by do |index|
|
|
142
|
+
candidate = colors[index]
|
|
143
|
+
((candidate[0] - target[0])**2) + ((candidate[1] - target[1])**2) +
|
|
144
|
+
((candidate[2] - target[2])**2)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/doom/version.rb
CHANGED
data/lib/doom/wad/flat.rb
CHANGED
|
@@ -22,8 +22,9 @@ module Doom
|
|
|
22
22
|
@cache[name]
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# Load (and cache) a single named patch/graphic lump. Returns a HudSprite
|
|
26
|
+
# or nil if the lump is missing or too small. Public so the menu,
|
|
27
|
+
# intermission and font renderers can pull graphics by name.
|
|
27
28
|
def load_graphic(name)
|
|
28
29
|
return @cache[name] if @cache[name]
|
|
29
30
|
|
|
@@ -38,6 +39,8 @@ module Doom
|
|
|
38
39
|
sprite
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
private
|
|
43
|
+
|
|
41
44
|
def parse_patch(name, data)
|
|
42
45
|
width = data[0, 2].unpack1('v')
|
|
43
46
|
height = data[2, 2].unpack1('v')
|
|
@@ -46,7 +49,7 @@ module Doom
|
|
|
46
49
|
|
|
47
50
|
# Read column offsets
|
|
48
51
|
column_offsets = width.times.map do |i|
|
|
49
|
-
data[8 + i * 4, 4].unpack1('V')
|
|
52
|
+
data[8 + (i * 4), 4].unpack1('V')
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
# Build column data
|
|
@@ -63,6 +66,7 @@ module Doom
|
|
|
63
66
|
|
|
64
67
|
loop do
|
|
65
68
|
break if pos >= data.size
|
|
69
|
+
|
|
66
70
|
top_delta = data[pos].ord
|
|
67
71
|
break if top_delta == 0xFF
|
|
68
72
|
|
data/lib/doom/wad/palette.rb
CHANGED
|
@@ -24,9 +24,9 @@ module Doom
|
|
|
24
24
|
offset = palette_index * PALETTE_SIZE
|
|
25
25
|
colors = COLORS.times.map do |i|
|
|
26
26
|
[
|
|
27
|
-
data[offset + i * 3].ord,
|
|
28
|
-
data[offset + i * 3 + 1].ord,
|
|
29
|
-
data[offset + i * 3 + 2].ord
|
|
27
|
+
data[offset + (i * 3)].ord,
|
|
28
|
+
data[offset + (i * 3) + 1].ord,
|
|
29
|
+
data[offset + (i * 3) + 2].ord
|
|
30
30
|
]
|
|
31
31
|
end
|
|
32
32
|
|
data/lib/doom/wad/patch.rb
CHANGED
data/lib/doom/wad/reader.rb
CHANGED
|
@@ -26,17 +26,15 @@ module Doom
|
|
|
26
26
|
entry = find_lump(name)
|
|
27
27
|
return nil unless entry
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
# Route through read_lump_at, which reads from the file that actually
|
|
30
|
+
# owns the entry. After merge_pwad a lump the PWAD overrode (PLAYPAL,
|
|
31
|
+
# COLORMAP, TEXTUREx, PNAMES, a patch) lives in the PWAD file, so seeking
|
|
32
|
+
# this reader's own IWAD handle at the entry's offset would read garbage.
|
|
33
|
+
data = read_lump_at(entry)
|
|
31
34
|
@lump_cache[name] = data
|
|
32
35
|
data
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def read_lump_at(entry)
|
|
36
|
-
@file.seek(entry.offset)
|
|
37
|
-
@file.read(entry.size)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
38
|
def lumps_between(start_marker, end_marker)
|
|
41
39
|
start_idx = @directory.index { |e| e.name == start_marker }
|
|
42
40
|
end_idx = @directory.index { |e| e.name == end_marker }
|
|
@@ -45,8 +43,59 @@ module Doom
|
|
|
45
43
|
@directory[start_idx + 1...end_idx]
|
|
46
44
|
end
|
|
47
45
|
|
|
46
|
+
def pwad?
|
|
47
|
+
@type == PWAD
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def iwad?
|
|
51
|
+
@type == IWAD
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Merge a PWAD on top of this IWAD.
|
|
55
|
+
# PWAD lumps override IWAD lumps with the same name.
|
|
56
|
+
# Map lumps (between map markers) are replaced as a group.
|
|
57
|
+
def merge_pwad(pwad)
|
|
58
|
+
raise Error, 'Can only merge a PWAD' unless pwad.pwad?
|
|
59
|
+
|
|
60
|
+
@lump_cache.clear
|
|
61
|
+
|
|
62
|
+
pwad.directory.each do |pwad_entry|
|
|
63
|
+
# Check if this lump already exists in the IWAD
|
|
64
|
+
existing_idx = @directory.index { |e| e.name == pwad_entry.name }
|
|
65
|
+
if existing_idx
|
|
66
|
+
# Replace it -- but we need to read from the PWAD file
|
|
67
|
+
@directory[existing_idx] = pwad_entry
|
|
68
|
+
else
|
|
69
|
+
# Append new lump
|
|
70
|
+
@directory << pwad_entry
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Keep the PWAD file open for reading its lumps
|
|
75
|
+
@pwad_files ||= []
|
|
76
|
+
@pwad_files << pwad
|
|
77
|
+
@num_lumps = @directory.size
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Read a lump by directory entry, checking if it belongs to a PWAD file
|
|
81
|
+
def read_lump_at(entry)
|
|
82
|
+
# Try PWAD files first (they may own this entry)
|
|
83
|
+
(@pwad_files || []).each do |pwad|
|
|
84
|
+
return pwad.read_lump_own(entry) if pwad.directory.include?(entry)
|
|
85
|
+
end
|
|
86
|
+
@file.seek(entry.offset)
|
|
87
|
+
@file.read(entry.size)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Read a lump that belongs to this WAD's own file
|
|
91
|
+
def read_lump_own(entry)
|
|
92
|
+
@file.seek(entry.offset)
|
|
93
|
+
@file.read(entry.size)
|
|
94
|
+
end
|
|
95
|
+
|
|
48
96
|
def close
|
|
49
97
|
@file.close
|
|
98
|
+
(@pwad_files || []).each(&:close)
|
|
50
99
|
end
|
|
51
100
|
|
|
52
101
|
private
|
|
@@ -58,9 +107,9 @@ module Doom
|
|
|
58
107
|
@directory_offset = data[8, 4].unpack1('V')
|
|
59
108
|
@lump_cache = {}
|
|
60
109
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
return if [@type == IWAD, @type == PWAD].any?
|
|
111
|
+
|
|
112
|
+
raise Error, "Invalid WAD type: #{@type}"
|
|
64
113
|
end
|
|
65
114
|
|
|
66
115
|
def read_directory
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Doom
|
|
7
|
+
module Wad
|
|
8
|
+
# Loads DOOM sound effects from WAD and converts to WAV for Gosu playback.
|
|
9
|
+
# DOOM sound format: 8-byte header (2 format, 2 sample_rate, 4 num_samples)
|
|
10
|
+
# followed by unsigned 8-bit PCM samples.
|
|
11
|
+
class SoundManager
|
|
12
|
+
def initialize(wad)
|
|
13
|
+
@wad = wad
|
|
14
|
+
@cache = {} # name => Gosu::Sample
|
|
15
|
+
@temp_dir = File.join(Dir.tmpdir, "doom_rb_sounds_#{Process.pid}")
|
|
16
|
+
Dir.mkdir(@temp_dir) unless Dir.exist?(@temp_dir)
|
|
17
|
+
|
|
18
|
+
# Sounds are decoded into a per-process temp dir; clean it up when the
|
|
19
|
+
# process exits so repeated runs do not leak WAV files.
|
|
20
|
+
at_exit { cleanup }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get or load a sound effect. Returns a Gosu::Sample or nil.
|
|
24
|
+
def [](name)
|
|
25
|
+
return @cache[name] if @cache.key?(name)
|
|
26
|
+
|
|
27
|
+
lump_name = name.start_with?('DS') ? name : "DS#{name}"
|
|
28
|
+
entry = @wad.find_lump(lump_name)
|
|
29
|
+
return @cache[name] = nil unless entry
|
|
30
|
+
|
|
31
|
+
data = @wad.read_lump_at(entry)
|
|
32
|
+
return @cache[name] = nil if data.size < 8
|
|
33
|
+
|
|
34
|
+
# Parse DOOM sound header
|
|
35
|
+
_format = data[0, 2].unpack1('v')
|
|
36
|
+
sample_rate = data[2, 2].unpack1('v')
|
|
37
|
+
num_samples = data[4, 4].unpack1('V')
|
|
38
|
+
|
|
39
|
+
# PCM data starts at offset 8, skip 16 padding bytes at start and end
|
|
40
|
+
pcm_start = 8 + 16
|
|
41
|
+
pcm_end = 8 + num_samples - 16
|
|
42
|
+
pcm_data = data[pcm_start...pcm_end]
|
|
43
|
+
return @cache[name] = nil unless pcm_data && pcm_data.size > 0
|
|
44
|
+
|
|
45
|
+
# Convert to WAV file for Gosu
|
|
46
|
+
wav_path = File.join(@temp_dir, "#{lump_name}.wav")
|
|
47
|
+
write_wav(wav_path, pcm_data, sample_rate) unless File.exist?(wav_path)
|
|
48
|
+
|
|
49
|
+
@cache[name] = Gosu::Sample.new(wav_path)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
# A bad or unplayable lump should silence that one sound, not crash the
|
|
52
|
+
# game -- but say so rather than swallowing the error entirely.
|
|
53
|
+
warn "SoundManager: could not load '#{name}': #{e.class}: #{e.message}"
|
|
54
|
+
@cache[name] = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cleanup
|
|
58
|
+
FileUtils.rm_rf(@temp_dir) if @temp_dir && Dir.exist?(@temp_dir)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Write unsigned 8-bit PCM data as a WAV file
|
|
64
|
+
def write_wav(path, pcm_data, sample_rate)
|
|
65
|
+
num_samples = pcm_data.size
|
|
66
|
+
data_size = num_samples
|
|
67
|
+
file_size = 36 + data_size
|
|
68
|
+
|
|
69
|
+
File.open(path, 'wb') do |f|
|
|
70
|
+
# RIFF header
|
|
71
|
+
f.write('RIFF')
|
|
72
|
+
f.write([file_size].pack('V'))
|
|
73
|
+
f.write('WAVE')
|
|
74
|
+
|
|
75
|
+
# fmt chunk
|
|
76
|
+
f.write('fmt ')
|
|
77
|
+
f.write([16].pack('V')) # chunk size
|
|
78
|
+
f.write([1].pack('v')) # PCM format
|
|
79
|
+
f.write([1].pack('v')) # mono
|
|
80
|
+
f.write([sample_rate].pack('V')) # sample rate
|
|
81
|
+
f.write([sample_rate].pack('V')) # byte rate (sample_rate * 1 * 1)
|
|
82
|
+
f.write([1].pack('v')) # block align
|
|
83
|
+
f.write([8].pack('v')) # bits per sample
|
|
84
|
+
|
|
85
|
+
# data chunk
|
|
86
|
+
f.write('data')
|
|
87
|
+
f.write([data_size].pack('V'))
|
|
88
|
+
f.write(pcm_data)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/doom/wad/sprite.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Doom
|
|
4
4
|
module Wad
|
|
5
5
|
class Sprite
|
|
6
|
-
attr_reader :name, :width, :height, :left_offset, :top_offset
|
|
6
|
+
attr_reader :name, :width, :height, :left_offset, :top_offset, :columns
|
|
7
7
|
|
|
8
8
|
def initialize(name, width, height, left_offset, top_offset, columns)
|
|
9
9
|
@name = name
|
|
@@ -34,20 +34,21 @@ module Doom
|
|
|
34
34
|
# Read column offsets
|
|
35
35
|
column_offsets = []
|
|
36
36
|
width.times do |i|
|
|
37
|
-
column_offsets << data[8 + i * 4, 4].unpack1('V')
|
|
37
|
+
column_offsets << data[8 + (i * 4), 4].unpack1('V')
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Read columns
|
|
41
41
|
columns = []
|
|
42
42
|
width.times do |x|
|
|
43
|
-
column = Array.new(height, nil)
|
|
43
|
+
column = Array.new(height, nil) # nil = transparent
|
|
44
44
|
offset = column_offsets[x]
|
|
45
45
|
|
|
46
46
|
# Read posts for this column
|
|
47
47
|
loop do
|
|
48
48
|
break if offset >= data.size
|
|
49
|
+
|
|
49
50
|
row_start = data[offset].ord
|
|
50
|
-
break if row_start == 255
|
|
51
|
+
break if row_start == 255 # End of column marker
|
|
51
52
|
|
|
52
53
|
post_height = data[offset + 1].ord
|
|
53
54
|
break if post_height == 0 || offset + 3 + post_height > data.size
|
|
@@ -55,12 +56,10 @@ module Doom
|
|
|
55
56
|
# Skip padding byte, read pixels, skip trailing padding
|
|
56
57
|
post_height.times do |i|
|
|
57
58
|
y = row_start + i
|
|
58
|
-
if y < height
|
|
59
|
-
column[y] = data[offset + 3 + i].ord
|
|
60
|
-
end
|
|
59
|
+
column[y] = data[offset + 3 + i].ord if y < height
|
|
61
60
|
end
|
|
62
61
|
|
|
63
|
-
offset += post_height + 4
|
|
62
|
+
offset += post_height + 4 # row_start + count + padding + pixels + padding
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
columns << column
|
|
@@ -120,6 +119,12 @@ module Doom
|
|
|
120
119
|
48 => 'ELEC', # Tall tech column
|
|
121
120
|
35 => 'CBRA', # Candelabra
|
|
122
121
|
|
|
122
|
+
# Dead bodies / gore decorations
|
|
123
|
+
10 => 'PLAY', # Bloody mess
|
|
124
|
+
12 => 'PLAY', # Bloody mess 2
|
|
125
|
+
15 => 'PLAY', # Dead player
|
|
126
|
+
24 => 'POL5', # Pool of blood and flesh
|
|
127
|
+
|
|
123
128
|
# Barrels
|
|
124
129
|
2035 => 'BAR1', # Exploding barrel
|
|
125
130
|
|
|
@@ -133,7 +138,18 @@ module Doom
|
|
|
133
138
|
3005 => 'HEAD', # Cacodemon
|
|
134
139
|
3006 => 'SKUL', # Lost soul
|
|
135
140
|
7 => 'SPID', # Spider Mastermind
|
|
136
|
-
16 => 'CYBR'
|
|
141
|
+
16 => 'CYBR' # Cyberdemon
|
|
142
|
+
}.freeze
|
|
143
|
+
|
|
144
|
+
# Things that use a specific frame instead of 'A'
|
|
145
|
+
# From DOOM info.h mobjinfo spawnstate:
|
|
146
|
+
# MT_MISC10 (type 10) -> S_PLAY_XDIE9 = PLAY W (gibbed mess)
|
|
147
|
+
# MT_MISC12 (type 12) -> S_PLAY_DIE7 = PLAY N (dead body)
|
|
148
|
+
# MT_MISC15 (type 15) -> S_PLAY_DIE7 = PLAY N (dead body)
|
|
149
|
+
THING_DEFAULT_FRAME = {
|
|
150
|
+
10 => 'W', # Bloody mess (gibbed)
|
|
151
|
+
12 => 'N', # Bloody mess 2 (dead body flat)
|
|
152
|
+
15 => 'N' # Dead player (dead body flat)
|
|
137
153
|
}.freeze
|
|
138
154
|
|
|
139
155
|
def initialize(wad)
|
|
@@ -154,8 +170,9 @@ module Doom
|
|
|
154
170
|
prefix = THING_SPRITES[thing_type]
|
|
155
171
|
return nil unless prefix
|
|
156
172
|
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
frame = THING_DEFAULT_FRAME[thing_type] || 'A'
|
|
174
|
+
sprite = load_sprite_frame(prefix, frame, 0) ||
|
|
175
|
+
load_sprite_frame(prefix, frame, 1)
|
|
159
176
|
|
|
160
177
|
@cache[thing_type] = sprite
|
|
161
178
|
sprite
|
|
@@ -173,19 +190,23 @@ module Doom
|
|
|
173
190
|
prefix = THING_SPRITES[thing_type]
|
|
174
191
|
return nil unless prefix
|
|
175
192
|
|
|
193
|
+
frame = THING_DEFAULT_FRAME[thing_type] || 'A'
|
|
194
|
+
|
|
176
195
|
# Check for rotation 0 (all angles) sprite first
|
|
177
|
-
sprite = load_sprite_frame(prefix,
|
|
196
|
+
sprite = load_sprite_frame(prefix, frame, 0)
|
|
178
197
|
return sprite if sprite
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
199
|
+
load_sprite_frame(prefix, frame, rotation_for(viewer_angle, thing_angle)) || @cache[thing_type]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Which of the 8 rotation frames faces the viewer.
|
|
203
|
+
# DOOM: rot = (R_PointToAngle(thing) - thing->angle + ANG45/2*9) >> 29
|
|
204
|
+
# Rotation 1 = front (viewer faces the thing's front), 5 = back.
|
|
205
|
+
def rotation_for(viewer_angle, thing_angle)
|
|
183
206
|
angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0) + Math::PI
|
|
184
|
-
angle_diff
|
|
207
|
+
angle_diff %= (2 * Math::PI)
|
|
185
208
|
angle_diff += 2 * Math::PI if angle_diff < 0
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
load_sprite_frame(prefix, 'A', rotation) || @cache[thing_type]
|
|
209
|
+
(((angle_diff + (Math::PI / 8)) / (Math::PI / 4)).to_i % 8) + 1
|
|
189
210
|
end
|
|
190
211
|
|
|
191
212
|
# Get a specific frame (for death animations, etc.)
|
|
@@ -193,17 +214,22 @@ module Doom
|
|
|
193
214
|
prefix = THING_SPRITES[thing_type]
|
|
194
215
|
return nil unless prefix
|
|
195
216
|
|
|
196
|
-
|
|
217
|
+
get_frame_rotated(prefix, frame_letter, viewer_angle, thing_angle)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Get a frame by explicit prefix (for barrel explosions where prefix differs from thing type)
|
|
221
|
+
def get_frame_by_prefix(prefix, frame_letter)
|
|
222
|
+
load_sprite_frame(prefix, frame_letter, 0)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Get a frame by explicit prefix, honouring rotations. Players are drawn
|
|
226
|
+
# through this: PLAY has no thing type of its own, and its walk and
|
|
227
|
+
# attack frames are rotated while its death frames are not.
|
|
228
|
+
def get_frame_rotated(prefix, frame_letter, viewer_angle, thing_angle)
|
|
197
229
|
sprite = load_sprite_frame(prefix, frame_letter, 0)
|
|
198
230
|
return sprite if sprite
|
|
199
231
|
|
|
200
|
-
|
|
201
|
-
angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0) + Math::PI
|
|
202
|
-
angle_diff = angle_diff % (2 * Math::PI)
|
|
203
|
-
angle_diff += 2 * Math::PI if angle_diff < 0
|
|
204
|
-
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
205
|
-
|
|
206
|
-
load_sprite_frame(prefix, frame_letter, rotation)
|
|
232
|
+
load_sprite_frame(prefix, frame_letter, rotation_for(viewer_angle, thing_angle))
|
|
207
233
|
end
|
|
208
234
|
|
|
209
235
|
private
|
|
@@ -222,12 +248,12 @@ module Doom
|
|
|
222
248
|
@sprite_index[key] = [name, false]
|
|
223
249
|
|
|
224
250
|
# Check for mirrored second rotation (e.g., SPOSA2A8)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
next unless name.length >= 8
|
|
252
|
+
|
|
253
|
+
frame2 = name[6]
|
|
254
|
+
rot2 = name[7].to_i
|
|
255
|
+
key2 = "#{prefix}#{frame2}#{rot2}"
|
|
256
|
+
@sprite_index[key2] = [name, true]
|
|
231
257
|
end
|
|
232
258
|
end
|
|
233
259
|
|
|
@@ -265,7 +291,7 @@ module Doom
|
|
|
265
291
|
|
|
266
292
|
def mirror_sprite(sprite)
|
|
267
293
|
# Flip columns horizontally, adjust left_offset
|
|
268
|
-
mirrored_columns = sprite.
|
|
294
|
+
mirrored_columns = sprite.columns.reverse
|
|
269
295
|
mirrored_left = sprite.width - sprite.left_offset
|
|
270
296
|
Sprite.new(
|
|
271
297
|
"#{sprite.name}_M",
|
data/lib/doom/wad/texture.rb
CHANGED
|
@@ -38,14 +38,14 @@ module Doom
|
|
|
38
38
|
|
|
39
39
|
count = data[0, 4].unpack1('V')
|
|
40
40
|
count.times.map do |i|
|
|
41
|
-
data[4 + i * 8, 8].delete("\x00").upcase
|
|
41
|
+
data[4 + (i * 8), 8].delete("\x00").upcase
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def self.parse_texture_lump(data)
|
|
46
46
|
num_textures = data[0, 4].unpack1('V')
|
|
47
47
|
offsets = num_textures.times.map do |i|
|
|
48
|
-
data[4 + i * 4, 4].unpack1('V')
|
|
48
|
+
data[4 + (i * 4), 4].unpack1('V')
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
offsets.map do |offset|
|
|
@@ -60,7 +60,7 @@ module Doom
|
|
|
60
60
|
patch_count = data[offset + 20, 2].unpack1('v')
|
|
61
61
|
|
|
62
62
|
patch_refs = patch_count.times.map do |i|
|
|
63
|
-
po = offset + 22 + i * 10
|
|
63
|
+
po = offset + 22 + (i * 10)
|
|
64
64
|
PatchRef.new(
|
|
65
65
|
data[po, 2].unpack1('s<'),
|
|
66
66
|
data[po + 2, 2].unpack1('s<'),
|
|
@@ -139,7 +139,7 @@ module Doom
|
|
|
139
139
|
precompute_columns
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
-
def column_pixels(x
|
|
142
|
+
def column_pixels(x)
|
|
143
143
|
@column_cache[x & (@width - 1)]
|
|
144
144
|
end
|
|
145
145
|
|
|
@@ -148,7 +148,7 @@ module Doom
|
|
|
148
148
|
def precompute_columns
|
|
149
149
|
@width.times do |x|
|
|
150
150
|
posts = @columns[x]
|
|
151
|
-
pixels = Array.new(@height
|
|
151
|
+
pixels = Array.new(@height) # nil = transparent
|
|
152
152
|
posts.each do |post|
|
|
153
153
|
td = post.top_delta
|
|
154
154
|
post.pixels.each_with_index do |color, i|
|