doom 0.2.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +159 -0
- data/bin/console +15 -0
- data/bin/doom +70 -0
- data/bin/setup +8 -0
- data/bin/wad +152 -0
- data/lib/doom/bsp_renderer.rb +90 -0
- data/lib/doom/game.rb +84 -0
- data/lib/doom/hud.rb +80 -0
- data/lib/doom/map_loader.rb +255 -0
- data/lib/doom/renderer.rb +32 -0
- data/lib/doom/sprite_loader.rb +88 -0
- data/lib/doom/sprite_renderer.rb +56 -0
- data/lib/doom/texture_loader.rb +138 -0
- data/lib/doom/texture_mapper.rb +57 -0
- data/lib/doom/version.rb +5 -0
- data/lib/doom/wad_loader.rb +106 -0
- data/lib/doom/window.rb +41 -0
- data/lib/doom.rb +46 -0
- metadata +105 -0
@@ -0,0 +1,255 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doom
|
4
|
+
# MapLoader is responsible for loading and parsing map data from a WAD file
|
5
|
+
class MapLoader
|
6
|
+
# Map lumps that make up a complete map
|
7
|
+
MAP_LUMPS = %w[THINGS LINEDEFS SIDEDEFS VERTEXES SEGS SSECTORS NODES SECTORS REJECT BLOCKMAP].freeze
|
8
|
+
|
9
|
+
attr_reader :maps, :current_map
|
10
|
+
|
11
|
+
# Initialize a new MapLoader with the given WADLoader
|
12
|
+
# @param wad_loader [WADLoader] the WAD loader to use
|
13
|
+
def initialize(wad_loader)
|
14
|
+
@wad_loader = wad_loader
|
15
|
+
@maps = find_maps
|
16
|
+
@current_map = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Find all maps in the WAD file
|
20
|
+
# @return [Array<String>] array of map names
|
21
|
+
def find_maps
|
22
|
+
map_names = []
|
23
|
+
|
24
|
+
# In Doom, maps are identified by their name (e.g., E1M1, MAP01)
|
25
|
+
# followed by the map lumps (THINGS, LINEDEFS, etc.)
|
26
|
+
@wad_loader.directory.each_with_index do |entry, index|
|
27
|
+
# Check if this entry is followed by map lumps
|
28
|
+
if is_map_start?(index)
|
29
|
+
map_names << entry[:name]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
map_names
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check if the entry at the given index is the start of a map
|
37
|
+
# @param index [Integer] the index in the directory
|
38
|
+
# @return [Boolean] true if the entry is the start of a map
|
39
|
+
def is_map_start?(index)
|
40
|
+
# Check if there are enough entries left
|
41
|
+
return false if index + MAP_LUMPS.size >= @wad_loader.directory.size
|
42
|
+
|
43
|
+
# Check if the next entries match the map lumps
|
44
|
+
MAP_LUMPS.each_with_index do |lump_name, i|
|
45
|
+
return false unless @wad_loader.directory[index + i + 1][:name] == lump_name
|
46
|
+
end
|
47
|
+
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Load a specific map
|
52
|
+
# @param map_name [String] the name of the map to load
|
53
|
+
# @return [Hash, nil] the map data or nil if not found
|
54
|
+
def load_map(map_name)
|
55
|
+
return nil unless @maps.include?(map_name)
|
56
|
+
|
57
|
+
@current_map = {
|
58
|
+
name: map_name,
|
59
|
+
things: load_things(map_name),
|
60
|
+
linedefs: load_linedefs(map_name),
|
61
|
+
sidedefs: load_sidedefs(map_name),
|
62
|
+
vertexes: load_vertexes(map_name),
|
63
|
+
sectors: load_sectors(map_name)
|
64
|
+
# Other map components can be added as needed
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Load the THINGS lump for a map
|
69
|
+
# @param map_name [String] the name of the map
|
70
|
+
# @return [Array<Hash>] array of things
|
71
|
+
def load_things(map_name)
|
72
|
+
lump_data = get_map_lump(map_name, "THINGS")
|
73
|
+
return [] unless lump_data
|
74
|
+
|
75
|
+
things = []
|
76
|
+
offset = 0
|
77
|
+
|
78
|
+
# Each thing is 10 bytes
|
79
|
+
while offset < lump_data.size
|
80
|
+
thing_data = lump_data[offset..offset + 9]
|
81
|
+
x_pos = thing_data[0..1].unpack("s<")[0]
|
82
|
+
y_pos = thing_data[2..3].unpack("s<")[0]
|
83
|
+
angle = thing_data[4..5].unpack("s<")[0]
|
84
|
+
type = thing_data[6..7].unpack("s<")[0]
|
85
|
+
flags = thing_data[8..9].unpack("s<")[0]
|
86
|
+
|
87
|
+
things << {
|
88
|
+
x: x_pos,
|
89
|
+
y: y_pos,
|
90
|
+
angle: angle,
|
91
|
+
type: type,
|
92
|
+
flags: flags
|
93
|
+
}
|
94
|
+
|
95
|
+
offset += 10
|
96
|
+
end
|
97
|
+
|
98
|
+
things
|
99
|
+
end
|
100
|
+
|
101
|
+
# Load the LINEDEFS lump for a map
|
102
|
+
# @param map_name [String] the name of the map
|
103
|
+
# @return [Array<Hash>] array of linedefs
|
104
|
+
def load_linedefs(map_name)
|
105
|
+
lump_data = get_map_lump(map_name, "LINEDEFS")
|
106
|
+
return [] unless lump_data
|
107
|
+
|
108
|
+
linedefs = []
|
109
|
+
offset = 0
|
110
|
+
|
111
|
+
# Each linedef is 14 bytes
|
112
|
+
while offset < lump_data.size
|
113
|
+
linedef_data = lump_data[offset..offset + 13]
|
114
|
+
start_vertex = linedef_data[0..1].unpack("s<")[0]
|
115
|
+
end_vertex = linedef_data[2..3].unpack("s<")[0]
|
116
|
+
flags = linedef_data[4..5].unpack("s<")[0]
|
117
|
+
special_type = linedef_data[6..7].unpack("s<")[0]
|
118
|
+
sector_tag = linedef_data[8..9].unpack("s<")[0]
|
119
|
+
right_sidedef = linedef_data[10..11].unpack("s<")[0]
|
120
|
+
left_sidedef = linedef_data[12..13].unpack("s<")[0]
|
121
|
+
|
122
|
+
linedefs << {
|
123
|
+
start_vertex: start_vertex,
|
124
|
+
end_vertex: end_vertex,
|
125
|
+
flags: flags,
|
126
|
+
special_type: special_type,
|
127
|
+
sector_tag: sector_tag,
|
128
|
+
right_sidedef: right_sidedef,
|
129
|
+
left_sidedef: left_sidedef
|
130
|
+
}
|
131
|
+
|
132
|
+
offset += 14
|
133
|
+
end
|
134
|
+
|
135
|
+
linedefs
|
136
|
+
end
|
137
|
+
|
138
|
+
# Load the SIDEDEFS lump for a map
|
139
|
+
# @param map_name [String] the name of the map
|
140
|
+
# @return [Array<Hash>] array of sidedefs
|
141
|
+
def load_sidedefs(map_name)
|
142
|
+
lump_data = get_map_lump(map_name, "SIDEDEFS")
|
143
|
+
return [] unless lump_data
|
144
|
+
|
145
|
+
sidedefs = []
|
146
|
+
offset = 0
|
147
|
+
|
148
|
+
# Each sidedef is 30 bytes
|
149
|
+
while offset < lump_data.size
|
150
|
+
sidedef_data = lump_data[offset..offset + 29]
|
151
|
+
x_offset = sidedef_data[0..1].unpack("s<")[0]
|
152
|
+
y_offset = sidedef_data[2..3].unpack("s<")[0]
|
153
|
+
upper_texture = sidedef_data[4..11].unpack("Z*")[0]
|
154
|
+
lower_texture = sidedef_data[12..19].unpack("Z*")[0]
|
155
|
+
middle_texture = sidedef_data[20..27].unpack("Z*")[0]
|
156
|
+
sector = sidedef_data[28..29].unpack("s<")[0]
|
157
|
+
|
158
|
+
sidedefs << {
|
159
|
+
x_offset: x_offset,
|
160
|
+
y_offset: y_offset,
|
161
|
+
upper_texture: upper_texture,
|
162
|
+
lower_texture: lower_texture,
|
163
|
+
middle_texture: middle_texture,
|
164
|
+
sector: sector
|
165
|
+
}
|
166
|
+
|
167
|
+
offset += 30
|
168
|
+
end
|
169
|
+
|
170
|
+
sidedefs
|
171
|
+
end
|
172
|
+
|
173
|
+
# Load the VERTEXES lump for a map
|
174
|
+
# @param map_name [String] the name of the map
|
175
|
+
# @return [Array<Hash>] array of vertexes
|
176
|
+
def load_vertexes(map_name)
|
177
|
+
lump_data = get_map_lump(map_name, "VERTEXES")
|
178
|
+
return [] unless lump_data
|
179
|
+
|
180
|
+
vertexes = []
|
181
|
+
offset = 0
|
182
|
+
|
183
|
+
# Each vertex is 4 bytes
|
184
|
+
while offset < lump_data.size
|
185
|
+
vertex_data = lump_data[offset..offset + 3]
|
186
|
+
x_pos = vertex_data[0..1].unpack("s<")[0]
|
187
|
+
y_pos = vertex_data[2..3].unpack("s<")[0]
|
188
|
+
|
189
|
+
vertexes << {
|
190
|
+
x: x_pos,
|
191
|
+
y: y_pos
|
192
|
+
}
|
193
|
+
|
194
|
+
offset += 4
|
195
|
+
end
|
196
|
+
|
197
|
+
vertexes
|
198
|
+
end
|
199
|
+
|
200
|
+
# Load the SECTORS lump for a map
|
201
|
+
# @param map_name [String] the name of the map
|
202
|
+
# @return [Array<Hash>] array of sectors
|
203
|
+
def load_sectors(map_name)
|
204
|
+
lump_data = get_map_lump(map_name, "SECTORS")
|
205
|
+
return [] unless lump_data
|
206
|
+
|
207
|
+
sectors = []
|
208
|
+
offset = 0
|
209
|
+
|
210
|
+
# Each sector is 26 bytes
|
211
|
+
while offset < lump_data.size
|
212
|
+
sector_data = lump_data[offset..offset + 25]
|
213
|
+
floor_height = sector_data[0..1].unpack("s<")[0]
|
214
|
+
ceiling_height = sector_data[2..3].unpack("s<")[0]
|
215
|
+
floor_texture = sector_data[4..11].unpack("Z*")[0]
|
216
|
+
ceiling_texture = sector_data[12..19].unpack("Z*")[0]
|
217
|
+
light_level = sector_data[20..21].unpack("s<")[0]
|
218
|
+
special_type = sector_data[22..23].unpack("s<")[0]
|
219
|
+
tag = sector_data[24..25].unpack("s<")[0]
|
220
|
+
|
221
|
+
sectors << {
|
222
|
+
floor_height: floor_height,
|
223
|
+
ceiling_height: ceiling_height,
|
224
|
+
floor_texture: floor_texture,
|
225
|
+
ceiling_texture: ceiling_texture,
|
226
|
+
light_level: light_level,
|
227
|
+
special_type: special_type,
|
228
|
+
tag: tag
|
229
|
+
}
|
230
|
+
|
231
|
+
offset += 26
|
232
|
+
end
|
233
|
+
|
234
|
+
sectors
|
235
|
+
end
|
236
|
+
|
237
|
+
# Get a specific map lump
|
238
|
+
# @param map_name [String] the name of the map
|
239
|
+
# @param lump_name [String] the name of the lump
|
240
|
+
# @return [String, nil] the lump data or nil if not found
|
241
|
+
def get_map_lump(map_name, lump_name)
|
242
|
+
map_index = @wad_loader.directory.find_index { |entry| entry[:name] == map_name }
|
243
|
+
return nil unless map_index
|
244
|
+
|
245
|
+
lump_index = MAP_LUMPS.find_index(lump_name)
|
246
|
+
return nil unless lump_index
|
247
|
+
|
248
|
+
# The lump is at map_index + lump_index + 1
|
249
|
+
entry = @wad_loader.directory[map_index + lump_index + 1]
|
250
|
+
return nil unless entry && entry[:name] == lump_name
|
251
|
+
|
252
|
+
@wad_loader.get_lump(lump_name)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doom
|
4
|
+
# Base class for all renderers
|
5
|
+
class Renderer
|
6
|
+
attr_reader :window
|
7
|
+
|
8
|
+
# Initialize a new Renderer with the given window
|
9
|
+
# @param window [Doom::Window] the window to render to
|
10
|
+
def initialize(window)
|
11
|
+
@window = window
|
12
|
+
end
|
13
|
+
|
14
|
+
# Render the current frame
|
15
|
+
# This is an abstract method that should be overridden by subclasses
|
16
|
+
def render
|
17
|
+
raise NotImplementedError, "Subclasses must implement render"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Clear the screen
|
21
|
+
def clear
|
22
|
+
# In Gosu, drawing is done directly to the window
|
23
|
+
# This method is a placeholder for potential buffer clearing operations
|
24
|
+
end
|
25
|
+
|
26
|
+
# Flush the rendered frame to the screen
|
27
|
+
def flush
|
28
|
+
# In Gosu, drawing is done directly to the window
|
29
|
+
# This method is a placeholder for potential buffer flushing operations
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doom
|
4
|
+
# SpriteLoader is responsible for loading and parsing sprite data from a WAD file
|
5
|
+
class SpriteLoader
|
6
|
+
attr_reader :sprites
|
7
|
+
|
8
|
+
# Initialize a new SpriteLoader with the given WADLoader
|
9
|
+
# @param wad_loader [WADLoader] the WAD loader to use
|
10
|
+
def initialize(wad_loader)
|
11
|
+
@wad_loader = wad_loader
|
12
|
+
@sprites = {}
|
13
|
+
load_sprites
|
14
|
+
end
|
15
|
+
|
16
|
+
# Load all sprites from the WAD file
|
17
|
+
def load_sprites
|
18
|
+
# In Doom, sprites are stored between S_START and S_END markers
|
19
|
+
start_index = @wad_loader.directory.find_index { |entry| entry[:name] == "S_START" }
|
20
|
+
end_index = @wad_loader.directory.find_index { |entry| entry[:name] == "S_END" }
|
21
|
+
|
22
|
+
return unless start_index && end_index && start_index < end_index
|
23
|
+
|
24
|
+
# Extract all sprite lumps between S_START and S_END
|
25
|
+
(start_index + 1...end_index).each do |i|
|
26
|
+
entry = @wad_loader.directory[i]
|
27
|
+
next if entry[:size] == 0 # Skip markers
|
28
|
+
|
29
|
+
sprite_name = entry[:name]
|
30
|
+
sprite_data = @wad_loader.get_lump(sprite_name)
|
31
|
+
|
32
|
+
if sprite_data
|
33
|
+
@sprites[sprite_name] = parse_sprite(sprite_data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parse a sprite lump
|
39
|
+
# @param sprite_data [String] the sprite lump data
|
40
|
+
# @return [Hash] the parsed sprite data
|
41
|
+
def parse_sprite(sprite_data)
|
42
|
+
# Doom sprites are stored in the Picture format
|
43
|
+
# Picture format header:
|
44
|
+
# - 2 bytes: width
|
45
|
+
# - 2 bytes: height
|
46
|
+
# - 2 bytes: left offset
|
47
|
+
# - 2 bytes: top offset
|
48
|
+
# - variable: column data
|
49
|
+
|
50
|
+
width = sprite_data[0..1].unpack("v")[0]
|
51
|
+
height = sprite_data[2..3].unpack("v")[0]
|
52
|
+
left_offset = sprite_data[4..5].unpack("s<")[0]
|
53
|
+
top_offset = sprite_data[6..7].unpack("s<")[0]
|
54
|
+
|
55
|
+
# The rest of the data is column data, which we'll store as raw bytes for now
|
56
|
+
# In a full implementation, we would parse this into a usable format for rendering
|
57
|
+
column_data = sprite_data[8..-1]
|
58
|
+
|
59
|
+
{
|
60
|
+
width: width,
|
61
|
+
height: height,
|
62
|
+
left_offset: left_offset,
|
63
|
+
top_offset: top_offset,
|
64
|
+
column_data: column_data
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get a specific sprite by name
|
69
|
+
# @param name [String] the name of the sprite
|
70
|
+
# @return [Hash, nil] the sprite data or nil if not found
|
71
|
+
def get_sprite(name)
|
72
|
+
@sprites[name]
|
73
|
+
end
|
74
|
+
|
75
|
+
# List all sprite names
|
76
|
+
# @return [Array<String>] array of sprite names
|
77
|
+
def sprite_names
|
78
|
+
@sprites.keys
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check if a sprite exists
|
82
|
+
# @param name [String] the name of the sprite
|
83
|
+
# @return [Boolean] true if the sprite exists
|
84
|
+
def sprite_exists?(name)
|
85
|
+
@sprites.key?(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doom
|
4
|
+
# SpriteRenderer is responsible for rendering sprites (enemies, items, etc.)
|
5
|
+
class SpriteRenderer
|
6
|
+
attr_reader :sprite_loader
|
7
|
+
|
8
|
+
# Initialize a new SpriteRenderer with the given sprite loader
|
9
|
+
# @param sprite_loader [Doom::SpriteLoader] the sprite loader to use
|
10
|
+
def initialize(sprite_loader)
|
11
|
+
@sprite_loader = sprite_loader
|
12
|
+
@sprites = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Load a specific sprite
|
16
|
+
# @param sprite_name [String] the name of the sprite to load
|
17
|
+
# @return [Hash, nil] the sprite data or nil if not found
|
18
|
+
def load_sprite(sprite_name)
|
19
|
+
return @sprites[sprite_name] if @sprites.key?(sprite_name)
|
20
|
+
|
21
|
+
sprite_data = @sprite_loader.get_sprite(sprite_name)
|
22
|
+
return nil unless sprite_data
|
23
|
+
|
24
|
+
# In a real implementation, this would convert the sprite data
|
25
|
+
# into a format that can be efficiently rendered
|
26
|
+
@sprites[sprite_name] = sprite_data
|
27
|
+
end
|
28
|
+
|
29
|
+
# Render a sprite
|
30
|
+
# @param sprite_name [String] the name of the sprite to render
|
31
|
+
# @param x [Integer] the x coordinate to render at
|
32
|
+
# @param y [Integer] the y coordinate to render at
|
33
|
+
# @param scale [Float] the scale to render at
|
34
|
+
# @param window [Doom::Window] the window to render to
|
35
|
+
def render_sprite(sprite_name, x, y, scale, window)
|
36
|
+
sprite = load_sprite(sprite_name)
|
37
|
+
return unless sprite
|
38
|
+
|
39
|
+
# In a real implementation, this would:
|
40
|
+
# 1. Calculate the screen coordinates for the sprite
|
41
|
+
# 2. Apply any necessary transformations (scaling, rotation)
|
42
|
+
# 3. Draw the sprite with proper transparency
|
43
|
+
|
44
|
+
# For now, we'll just draw a placeholder rectangle
|
45
|
+
width = sprite[:width] * scale
|
46
|
+
height = sprite[:height] * scale
|
47
|
+
color = Gosu::Color.new(255, 255, 0, 0) # Semi-transparent red
|
48
|
+
window.draw_quad(
|
49
|
+
x - width / 2, y - height, color,
|
50
|
+
x + width / 2, y - height, color,
|
51
|
+
x + width / 2, y, color,
|
52
|
+
x - width / 2, y, color
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doom
|
4
|
+
# TextureLoader is responsible for loading and parsing texture data from a WAD file
|
5
|
+
class TextureLoader
|
6
|
+
attr_reader :textures, :patch_names
|
7
|
+
|
8
|
+
# Initialize a new TextureLoader with the given WADLoader
|
9
|
+
# @param wad_loader [WADLoader] the WAD loader to use
|
10
|
+
def initialize(wad_loader)
|
11
|
+
@wad_loader = wad_loader
|
12
|
+
@textures = {}
|
13
|
+
@patch_names = []
|
14
|
+
load_patch_names
|
15
|
+
load_textures
|
16
|
+
end
|
17
|
+
|
18
|
+
# Load the PNAMES lump which contains patch names
|
19
|
+
def load_patch_names
|
20
|
+
pnames_data = @wad_loader.get_lump("PNAMES")
|
21
|
+
return unless pnames_data
|
22
|
+
|
23
|
+
# First 4 bytes is the number of patch names
|
24
|
+
num_patches = pnames_data[0..3].unpack("V")[0]
|
25
|
+
|
26
|
+
# Each patch name is 8 bytes
|
27
|
+
num_patches.times do |i|
|
28
|
+
offset = 4 + (i * 8)
|
29
|
+
patch_name = pnames_data[offset..offset + 7].unpack("Z*")[0]
|
30
|
+
@patch_names << patch_name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Load the TEXTURE1 and TEXTURE2 lumps which contain texture definitions
|
35
|
+
def load_textures
|
36
|
+
load_texture_lump("TEXTURE1")
|
37
|
+
load_texture_lump("TEXTURE2")
|
38
|
+
end
|
39
|
+
|
40
|
+
# Load a specific texture lump
|
41
|
+
# @param lump_name [String] the name of the texture lump (TEXTURE1 or TEXTURE2)
|
42
|
+
def load_texture_lump(lump_name)
|
43
|
+
texture_data = @wad_loader.get_lump(lump_name)
|
44
|
+
return unless texture_data
|
45
|
+
|
46
|
+
# First 4 bytes is the number of textures
|
47
|
+
num_textures = texture_data[0..3].unpack("V")[0]
|
48
|
+
|
49
|
+
# Next num_textures * 4 bytes are offsets to each texture definition
|
50
|
+
offsets = []
|
51
|
+
num_textures.times do |i|
|
52
|
+
offset = 4 + (i * 4)
|
53
|
+
texture_offset = texture_data[offset..offset + 3].unpack("V")[0]
|
54
|
+
offsets << texture_offset
|
55
|
+
end
|
56
|
+
|
57
|
+
# Parse each texture definition
|
58
|
+
offsets.each do |offset|
|
59
|
+
parse_texture_definition(texture_data, offset)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parse a texture definition
|
64
|
+
# @param texture_data [String] the texture lump data
|
65
|
+
# @param offset [Integer] the offset to the texture definition
|
66
|
+
def parse_texture_definition(texture_data, offset)
|
67
|
+
# Texture definition structure:
|
68
|
+
# - 8 bytes: texture name
|
69
|
+
# - 4 bytes: flags (unused)
|
70
|
+
# - 2 bytes: width
|
71
|
+
# - 2 bytes: height
|
72
|
+
# - 4 bytes: column directory (unused)
|
73
|
+
# - 2 bytes: number of patches
|
74
|
+
# - patches: array of patch definitions
|
75
|
+
|
76
|
+
texture_name = texture_data[offset..offset + 7].unpack("Z*")[0]
|
77
|
+
# Skip flags (4 bytes)
|
78
|
+
width = texture_data[offset + 12..offset + 13].unpack("v")[0]
|
79
|
+
height = texture_data[offset + 14..offset + 15].unpack("v")[0]
|
80
|
+
# Skip column directory (4 bytes)
|
81
|
+
num_patches = texture_data[offset + 20..offset + 21].unpack("v")[0]
|
82
|
+
|
83
|
+
patches = []
|
84
|
+
|
85
|
+
# Parse each patch definition
|
86
|
+
num_patches.times do |i|
|
87
|
+
patch_offset = offset + 22 + (i * 10)
|
88
|
+
|
89
|
+
# Patch definition structure:
|
90
|
+
# - 2 bytes: x offset
|
91
|
+
# - 2 bytes: y offset
|
92
|
+
# - 2 bytes: patch number (index into PNAMES)
|
93
|
+
# - 2 bytes: step dir (unused)
|
94
|
+
# - 2 bytes: color map (unused)
|
95
|
+
|
96
|
+
x_offset = texture_data[patch_offset..patch_offset + 1].unpack("s<")[0]
|
97
|
+
y_offset = texture_data[patch_offset + 2..patch_offset + 3].unpack("s<")[0]
|
98
|
+
patch_num = texture_data[patch_offset + 4..patch_offset + 5].unpack("v")[0]
|
99
|
+
|
100
|
+
patch_name = @patch_names[patch_num] if patch_num < @patch_names.size
|
101
|
+
|
102
|
+
patches << {
|
103
|
+
x_offset: x_offset,
|
104
|
+
y_offset: y_offset,
|
105
|
+
patch_num: patch_num,
|
106
|
+
patch_name: patch_name
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
@textures[texture_name] = {
|
111
|
+
name: texture_name,
|
112
|
+
width: width,
|
113
|
+
height: height,
|
114
|
+
patches: patches
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get a specific texture by name
|
119
|
+
# @param name [String] the name of the texture
|
120
|
+
# @return [Hash, nil] the texture data or nil if not found
|
121
|
+
def get_texture(name)
|
122
|
+
@textures[name]
|
123
|
+
end
|
124
|
+
|
125
|
+
# List all texture names
|
126
|
+
# @return [Array<String>] array of texture names
|
127
|
+
def texture_names
|
128
|
+
@textures.keys
|
129
|
+
end
|
130
|
+
|
131
|
+
# Check if a texture exists
|
132
|
+
# @param name [String] the name of the texture
|
133
|
+
# @return [Boolean] true if the texture exists
|
134
|
+
def texture_exists?(name)
|
135
|
+
@textures.key?(name)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doom
|
4
|
+
# TextureMapper is responsible for mapping textures onto walls
|
5
|
+
class TextureMapper
|
6
|
+
attr_reader :texture_loader
|
7
|
+
|
8
|
+
# Initialize a new TextureMapper with the given texture loader
|
9
|
+
# @param texture_loader [Doom::TextureLoader] the texture loader to use
|
10
|
+
def initialize(texture_loader)
|
11
|
+
@texture_loader = texture_loader
|
12
|
+
@textures = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Load a specific texture
|
16
|
+
# @param texture_name [String] the name of the texture to load
|
17
|
+
# @return [Hash, nil] the texture data or nil if not found
|
18
|
+
def load_texture(texture_name)
|
19
|
+
return @textures[texture_name] if @textures.key?(texture_name)
|
20
|
+
|
21
|
+
texture_data = @texture_loader.get_texture(texture_name)
|
22
|
+
return nil unless texture_data
|
23
|
+
|
24
|
+
# In a real implementation, this would convert the texture data
|
25
|
+
# into a format that can be efficiently rendered
|
26
|
+
@textures[texture_name] = texture_data
|
27
|
+
end
|
28
|
+
|
29
|
+
# Map a texture onto a wall
|
30
|
+
# @param texture_name [String] the name of the texture to map
|
31
|
+
# @param x [Integer] the x coordinate to start mapping
|
32
|
+
# @param y [Integer] the y coordinate to start mapping
|
33
|
+
# @param width [Integer] the width of the wall
|
34
|
+
# @param height [Integer] the height of the wall
|
35
|
+
# @param u_offset [Integer] the u offset into the texture
|
36
|
+
# @param v_offset [Integer] the v offset into the texture
|
37
|
+
# @param window [Doom::Window] the window to render to
|
38
|
+
def map_texture(texture_name, x, y, width, height, u_offset, v_offset, window)
|
39
|
+
texture = load_texture(texture_name)
|
40
|
+
return unless texture
|
41
|
+
|
42
|
+
# In a real implementation, this would:
|
43
|
+
# 1. Calculate the texture coordinates for each pixel of the wall
|
44
|
+
# 2. Sample the texture at those coordinates
|
45
|
+
# 3. Draw the resulting pixel to the screen
|
46
|
+
|
47
|
+
# For now, we'll just draw a placeholder rectangle
|
48
|
+
color = Gosu::Color.new(255, 128, 128, 128) # Semi-transparent gray
|
49
|
+
window.draw_quad(
|
50
|
+
x, y, color,
|
51
|
+
x + width, y, color,
|
52
|
+
x + width, y + height, color,
|
53
|
+
x, y + height, color
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|