doom 0.2.0 → 0.4.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,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Game
5
+ # Manages animated sector actions (doors, lifts, etc.)
6
+ class SectorActions
7
+ # Door states
8
+ DOOR_CLOSED = 0
9
+ DOOR_OPENING = 1
10
+ DOOR_OPEN = 2
11
+ DOOR_CLOSING = 3
12
+
13
+ # Door speeds (units per tic, 35 tics/sec)
14
+ DOOR_SPEED = 2
15
+ DOOR_WAIT = 150 # Tics to wait when open (~4 seconds)
16
+ PLAYER_HEIGHT = 56 # Player height for door collision
17
+
18
+ def initialize(map)
19
+ @map = map
20
+ @active_doors = {} # sector_index => door_state
21
+ @player_x = 0
22
+ @player_y = 0
23
+ end
24
+
25
+ def update_player_position(x, y)
26
+ @player_x = x
27
+ @player_y = y
28
+ end
29
+
30
+ def update
31
+ update_doors
32
+ end
33
+
34
+ # Try to use a linedef (called when player presses use key)
35
+ def use_linedef(linedef, linedef_idx)
36
+ return false if linedef.special == 0
37
+
38
+ case linedef.special
39
+ when 1 # DR Door Open Wait Close
40
+ activate_door(linedef)
41
+ true
42
+ when 31 # D1 Door Open Stay
43
+ activate_door(linedef, stay_open: true)
44
+ true
45
+ when 26 # DR Blue Door
46
+ activate_door(linedef, key: :blue_card)
47
+ true
48
+ when 27 # DR Yellow Door
49
+ activate_door(linedef, key: :yellow_card)
50
+ true
51
+ when 28 # DR Red Door
52
+ activate_door(linedef, key: :red_card)
53
+ true
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def activate_door(linedef, stay_open: false, key: nil)
62
+ # Find the sector on the back side of the linedef
63
+ return unless linedef.two_sided?
64
+
65
+ back_sidedef_idx = linedef.sidedef_left
66
+ return if back_sidedef_idx == 0xFFFF || back_sidedef_idx < 0
67
+
68
+ back_sidedef = @map.sidedefs[back_sidedef_idx]
69
+ sector_idx = back_sidedef.sector
70
+ sector = @map.sectors[sector_idx]
71
+ return unless sector
72
+
73
+ # Check if door is already active
74
+ if @active_doors[sector_idx]
75
+ door = @active_doors[sector_idx]
76
+ # If closing, reverse direction
77
+ if door[:state] == DOOR_CLOSING
78
+ door[:state] = DOOR_OPENING
79
+ end
80
+ return
81
+ end
82
+
83
+ # Calculate target height (find lowest adjacent ceiling)
84
+ target_height = find_lowest_ceiling_around(sector_idx) - 4
85
+
86
+ # Start the door
87
+ @active_doors[sector_idx] = {
88
+ sector: sector,
89
+ state: DOOR_OPENING,
90
+ target_height: target_height,
91
+ original_height: sector.ceiling_height,
92
+ wait_tics: 0,
93
+ stay_open: stay_open
94
+ }
95
+ end
96
+
97
+ def update_doors
98
+ @active_doors.each do |sector_idx, door|
99
+ case door[:state]
100
+ when DOOR_OPENING
101
+ door[:sector].ceiling_height += DOOR_SPEED
102
+ if door[:sector].ceiling_height >= door[:target_height]
103
+ door[:sector].ceiling_height = door[:target_height]
104
+ if door[:stay_open]
105
+ @active_doors.delete(sector_idx)
106
+ else
107
+ door[:state] = DOOR_OPEN
108
+ door[:wait_tics] = DOOR_WAIT
109
+ end
110
+ end
111
+
112
+ when DOOR_OPEN
113
+ door[:wait_tics] -= 1
114
+ if door[:wait_tics] <= 0
115
+ door[:state] = DOOR_CLOSING
116
+ end
117
+
118
+ when DOOR_CLOSING
119
+ # Check if player is in the door sector
120
+ player_sector = @map.sector_at(@player_x, @player_y)
121
+ if player_sector == door[:sector]
122
+ # Player is in door - reopen it
123
+ door[:state] = DOOR_OPENING
124
+ next
125
+ end
126
+
127
+ door[:sector].ceiling_height -= DOOR_SPEED
128
+ if door[:sector].ceiling_height <= door[:original_height]
129
+ door[:sector].ceiling_height = door[:original_height]
130
+ @active_doors.delete(sector_idx)
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def find_lowest_ceiling_around(sector_idx)
137
+ lowest = Float::INFINITY
138
+
139
+ @map.linedefs.each do |linedef|
140
+ next unless linedef.two_sided?
141
+
142
+ # Check if this linedef touches our sector
143
+ right_sidedef = @map.sidedefs[linedef.sidedef_right]
144
+ left_sidedef = @map.sidedefs[linedef.sidedef_left] if linedef.sidedef_left != 0xFFFF
145
+
146
+ adjacent_sector = nil
147
+ if right_sidedef&.sector == sector_idx && left_sidedef
148
+ adjacent_sector = @map.sectors[left_sidedef.sector]
149
+ elsif left_sidedef&.sector == sector_idx
150
+ adjacent_sector = @map.sectors[right_sidedef.sector]
151
+ end
152
+
153
+ if adjacent_sector
154
+ lowest = [lowest, adjacent_sector.ceiling_height].min
155
+ end
156
+ end
157
+
158
+ lowest == Float::INFINITY ? 128 : lowest
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Map
5
+ Vertex = Struct.new(:x, :y)
6
+
7
+ Thing = Struct.new(:x, :y, :angle, :type, :flags)
8
+
9
+ Linedef = Struct.new(:v1, :v2, :flags, :special, :tag, :sidedef_right, :sidedef_left) do
10
+ FLAGS = {
11
+ BLOCKING: 0x0001,
12
+ BLOCKMONSTERS: 0x0002,
13
+ TWOSIDED: 0x0004,
14
+ DONTPEGTOP: 0x0008,
15
+ DONTPEGBOTTOM: 0x0010,
16
+ SECRET: 0x0020,
17
+ SOUNDBLOCK: 0x0040,
18
+ DONTDRAW: 0x0080,
19
+ MAPPED: 0x0100
20
+ }.freeze
21
+
22
+ def two_sided?
23
+ (flags & FLAGS[:TWOSIDED]) != 0
24
+ end
25
+
26
+ def upper_unpegged?
27
+ (flags & FLAGS[:DONTPEGTOP]) != 0
28
+ end
29
+
30
+ def lower_unpegged?
31
+ (flags & FLAGS[:DONTPEGBOTTOM]) != 0
32
+ end
33
+ end
34
+
35
+ Sidedef = Struct.new(:x_offset, :y_offset, :upper_texture, :lower_texture, :middle_texture, :sector)
36
+
37
+ Sector = Struct.new(:floor_height, :ceiling_height, :floor_texture, :ceiling_texture, :light_level, :special, :tag)
38
+
39
+ Seg = Struct.new(:v1, :v2, :angle, :linedef, :direction, :offset)
40
+
41
+ Subsector = Struct.new(:seg_count, :first_seg)
42
+
43
+ class Node
44
+ SUBSECTOR_FLAG = 0x8000
45
+
46
+ attr_reader :x, :y, :dx, :dy, :bbox_right, :bbox_left, :child_right, :child_left
47
+
48
+ BBox = Struct.new(:top, :bottom, :left, :right)
49
+
50
+ def initialize(x, y, dx, dy, bbox_right, bbox_left, child_right, child_left)
51
+ @x = x
52
+ @y = y
53
+ @dx = dx
54
+ @dy = dy
55
+ @bbox_right = bbox_right
56
+ @bbox_left = bbox_left
57
+ @child_right = child_right
58
+ @child_left = child_left
59
+ end
60
+
61
+ def right_is_subsector?
62
+ (@child_right & SUBSECTOR_FLAG) != 0
63
+ end
64
+
65
+ def left_is_subsector?
66
+ (@child_left & SUBSECTOR_FLAG) != 0
67
+ end
68
+
69
+ def right_index
70
+ @child_right & ~SUBSECTOR_FLAG
71
+ end
72
+
73
+ def left_index
74
+ @child_left & ~SUBSECTOR_FLAG
75
+ end
76
+ end
77
+
78
+ class MapData
79
+ attr_reader :name, :things, :vertices, :linedefs, :sidedefs, :sectors, :segs, :subsectors, :nodes
80
+
81
+ def initialize(name)
82
+ @name = name
83
+ @things = []
84
+ @vertices = []
85
+ @linedefs = []
86
+ @sidedefs = []
87
+ @sectors = []
88
+ @segs = []
89
+ @subsectors = []
90
+ @nodes = []
91
+ end
92
+
93
+ def self.load(wad, map_name)
94
+ map = new(map_name)
95
+
96
+ lump_idx = wad.directory.index { |e| e.name == map_name.upcase }
97
+ raise Error, "Map #{map_name} not found" unless lump_idx
98
+
99
+ map.load_things(wad.read_lump_at(wad.directory[lump_idx + 1]))
100
+ map.load_linedefs(wad.read_lump_at(wad.directory[lump_idx + 2]))
101
+ map.load_sidedefs(wad.read_lump_at(wad.directory[lump_idx + 3]))
102
+ map.load_vertices(wad.read_lump_at(wad.directory[lump_idx + 4]))
103
+ map.load_segs(wad.read_lump_at(wad.directory[lump_idx + 5]))
104
+ map.load_subsectors(wad.read_lump_at(wad.directory[lump_idx + 6]))
105
+ map.load_nodes(wad.read_lump_at(wad.directory[lump_idx + 7]))
106
+ map.load_sectors(wad.read_lump_at(wad.directory[lump_idx + 8]))
107
+
108
+ map
109
+ end
110
+
111
+ def load_things(data)
112
+ count = data.size / 10
113
+ count.times do |i|
114
+ offset = i * 10
115
+ @things << Thing.new(
116
+ data[offset, 2].unpack1('s<'),
117
+ data[offset + 2, 2].unpack1('s<'),
118
+ data[offset + 4, 2].unpack1('v'),
119
+ data[offset + 6, 2].unpack1('v'),
120
+ data[offset + 8, 2].unpack1('v')
121
+ )
122
+ end
123
+ end
124
+
125
+ def load_vertices(data)
126
+ count = data.size / 4
127
+ count.times do |i|
128
+ offset = i * 4
129
+ @vertices << Vertex.new(
130
+ data[offset, 2].unpack1('s<'),
131
+ data[offset + 2, 2].unpack1('s<')
132
+ )
133
+ end
134
+ end
135
+
136
+ def load_linedefs(data)
137
+ count = data.size / 14
138
+ count.times do |i|
139
+ offset = i * 14
140
+ @linedefs << Linedef.new(
141
+ data[offset, 2].unpack1('v'),
142
+ data[offset + 2, 2].unpack1('v'),
143
+ data[offset + 4, 2].unpack1('v'),
144
+ data[offset + 6, 2].unpack1('v'),
145
+ data[offset + 8, 2].unpack1('v'),
146
+ data[offset + 10, 2].unpack1('s<'),
147
+ data[offset + 12, 2].unpack1('s<')
148
+ )
149
+ end
150
+ end
151
+
152
+ def load_sidedefs(data)
153
+ count = data.size / 30
154
+ count.times do |i|
155
+ offset = i * 30
156
+ @sidedefs << Sidedef.new(
157
+ data[offset, 2].unpack1('s<'),
158
+ data[offset + 2, 2].unpack1('s<'),
159
+ data[offset + 4, 8].delete("\x00").strip,
160
+ data[offset + 12, 8].delete("\x00").strip,
161
+ data[offset + 20, 8].delete("\x00").strip,
162
+ data[offset + 28, 2].unpack1('v')
163
+ )
164
+ end
165
+ end
166
+
167
+ def load_sectors(data)
168
+ count = data.size / 26
169
+ count.times do |i|
170
+ offset = i * 26
171
+ @sectors << Sector.new(
172
+ data[offset, 2].unpack1('s<'),
173
+ data[offset + 2, 2].unpack1('s<'),
174
+ data[offset + 4, 8].delete("\x00").strip,
175
+ data[offset + 12, 8].delete("\x00").strip,
176
+ data[offset + 20, 2].unpack1('v'),
177
+ data[offset + 22, 2].unpack1('v'),
178
+ data[offset + 24, 2].unpack1('v')
179
+ )
180
+ end
181
+ end
182
+
183
+ def load_segs(data)
184
+ count = data.size / 12
185
+ count.times do |i|
186
+ offset = i * 12
187
+ @segs << Seg.new(
188
+ data[offset, 2].unpack1('v'),
189
+ data[offset + 2, 2].unpack1('v'),
190
+ data[offset + 4, 2].unpack1('s<'),
191
+ data[offset + 6, 2].unpack1('v'),
192
+ data[offset + 8, 2].unpack1('v'),
193
+ data[offset + 10, 2].unpack1('s<')
194
+ )
195
+ end
196
+ end
197
+
198
+ def load_subsectors(data)
199
+ count = data.size / 4
200
+ count.times do |i|
201
+ offset = i * 4
202
+ @subsectors << Subsector.new(
203
+ data[offset, 2].unpack1('v'),
204
+ data[offset + 2, 2].unpack1('v')
205
+ )
206
+ end
207
+ end
208
+
209
+ def load_nodes(data)
210
+ count = data.size / 28
211
+ count.times do |i|
212
+ offset = i * 28
213
+ bbox_right = Node::BBox.new(
214
+ data[offset + 8, 2].unpack1('s<'),
215
+ data[offset + 10, 2].unpack1('s<'),
216
+ data[offset + 12, 2].unpack1('s<'),
217
+ data[offset + 14, 2].unpack1('s<')
218
+ )
219
+ bbox_left = Node::BBox.new(
220
+ data[offset + 16, 2].unpack1('s<'),
221
+ data[offset + 18, 2].unpack1('s<'),
222
+ data[offset + 20, 2].unpack1('s<'),
223
+ data[offset + 22, 2].unpack1('s<')
224
+ )
225
+ @nodes << Node.new(
226
+ data[offset, 2].unpack1('s<'),
227
+ data[offset + 2, 2].unpack1('s<'),
228
+ data[offset + 4, 2].unpack1('s<'),
229
+ data[offset + 6, 2].unpack1('s<'),
230
+ bbox_right,
231
+ bbox_left,
232
+ data[offset + 24, 2].unpack1('v'),
233
+ data[offset + 26, 2].unpack1('v')
234
+ )
235
+ end
236
+ end
237
+
238
+ def player_start
239
+ @things.find { |t| t.type == 1 }
240
+ end
241
+
242
+ # Find the sector at a given position by traversing the BSP tree
243
+ def sector_at(x, y)
244
+ subsector = subsector_at(x, y)
245
+ return nil unless subsector
246
+
247
+ # Get sector from first seg of subsector
248
+ seg = @segs[subsector.first_seg]
249
+ return nil unless seg
250
+
251
+ linedef = @linedefs[seg.linedef]
252
+ sidedef_idx = seg.direction == 0 ? linedef.sidedef_right : linedef.sidedef_left
253
+ return nil if sidedef_idx < 0
254
+
255
+ @sectors[@sidedefs[sidedef_idx].sector]
256
+ end
257
+
258
+ # Find the subsector containing a point
259
+ def subsector_at(x, y)
260
+ node_idx = @nodes.size - 1
261
+ while (node_idx & Node::SUBSECTOR_FLAG) == 0
262
+ node = @nodes[node_idx]
263
+ side = point_on_side(x, y, node)
264
+ node_idx = side == 0 ? node.child_right : node.child_left
265
+ end
266
+ @subsectors[node_idx & ~Node::SUBSECTOR_FLAG]
267
+ end
268
+
269
+ private
270
+
271
+ def point_on_side(x, y, node)
272
+ dx = x - node.x
273
+ dy = y - node.y
274
+ left = dy * node.dx
275
+ right = dx * node.dy
276
+ right >= left ? 0 : 1
277
+ end
278
+ end
279
+ end
280
+ end