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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ VERSION = "0.2.0"
5
+ end