doom 0.8.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 +134 -24
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +239 -153
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +22 -39
- data/lib/doom/game/item_pickup.rb +87 -75
- data/lib/doom/game/menu.rb +90 -85
- data/lib/doom/game/monster_ai.rb +150 -136
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +38 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +169 -93
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +33 -58
- 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 +604 -832
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +5 -3
- 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 +466 -440
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +7 -7
- 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 +9 -10
- 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 +12 -15
- data/lib/doom/wad/sound.rb +14 -6
- data/lib/doom/wad/sprite.rb +36 -35
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +197 -16
- metadata +94 -7
|
@@ -6,27 +6,30 @@ module Doom
|
|
|
6
6
|
module Game
|
|
7
7
|
# Manages animated sector actions (doors, lifts, etc.)
|
|
8
8
|
class SectorActions
|
|
9
|
-
# Door states
|
|
10
|
-
DOOR_CLOSED
|
|
9
|
+
# Door states (doors start at DOOR_OPENING and are deleted once fully
|
|
10
|
+
# closed, so there is no resting DOOR_CLOSED state to model).
|
|
11
11
|
DOOR_OPENING = 1
|
|
12
12
|
DOOR_OPEN = 2
|
|
13
13
|
DOOR_CLOSING = 3
|
|
14
14
|
|
|
15
15
|
# Door speeds (units per tic, 35 tics/sec)
|
|
16
16
|
DOOR_SPEED = 2
|
|
17
|
-
DOOR_WAIT = 150
|
|
18
|
-
PLAYER_HEIGHT = 56
|
|
17
|
+
DOOR_WAIT = 150 # Tics to wait when open (~4 seconds)
|
|
19
18
|
|
|
20
19
|
# Lift constants
|
|
21
20
|
LIFT_SPEED = 4
|
|
22
|
-
LIFT_WAIT = 105
|
|
21
|
+
LIFT_WAIT = 105 # ~3 seconds
|
|
23
22
|
|
|
24
23
|
attr_reader :exit_triggered, :secrets_found
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
# Pending teleports, drained by the world once per tic: a map of
|
|
26
|
+
# player_id => destination. Each player teleports to its own destination,
|
|
27
|
+
# so a single shared slot (which only ever moved player 0) is not enough
|
|
28
|
+
# in a multiplayer world.
|
|
29
|
+
def pop_teleports
|
|
30
|
+
dests = @teleport_dests
|
|
31
|
+
@teleport_dests = {}
|
|
32
|
+
dests
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def initialize(map, sound_engine = nil)
|
|
@@ -36,14 +39,32 @@ module Doom
|
|
|
36
39
|
@active_lifts = {} # sector_index => lift_state
|
|
37
40
|
@player_x = 0
|
|
38
41
|
@player_y = 0
|
|
42
|
+
@player_positions = [] # [[player_id, x, y], ...] in id order
|
|
39
43
|
@exit_triggered = nil
|
|
40
44
|
@secrets_found = {} # sector_index => true
|
|
41
45
|
@crossed_linedefs = {}
|
|
46
|
+
@near_linedefs = {} # player_id => { linedef_idx => side }
|
|
47
|
+
@teleport_dests = {} # player_id => { x:, y:, angle: }
|
|
42
48
|
end
|
|
43
49
|
|
|
50
|
+
# Single-player convenience used by the unit specs and by the world's
|
|
51
|
+
# use-line ray cast: treat the given point as player 0's position.
|
|
44
52
|
def update_player_position(x, y)
|
|
45
53
|
@player_x = x
|
|
46
54
|
@player_y = y
|
|
55
|
+
@player_positions = [[0, x, y]]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Feed every player's position (ordered by id) so walk triggers fire for
|
|
59
|
+
# each player independently and reproducibly. The primary (first) position
|
|
60
|
+
# also drives the single-player-only checks (secrets, door reopen).
|
|
61
|
+
def update_players(positions)
|
|
62
|
+
@player_positions = positions
|
|
63
|
+
first = positions.first
|
|
64
|
+
return unless first
|
|
65
|
+
|
|
66
|
+
@player_x = first[1]
|
|
67
|
+
@player_y = first[2]
|
|
47
68
|
end
|
|
48
69
|
|
|
49
70
|
def update
|
|
@@ -53,8 +74,10 @@ module Doom
|
|
|
53
74
|
check_secrets
|
|
54
75
|
end
|
|
55
76
|
|
|
56
|
-
# Try to use a linedef (called when player presses use key)
|
|
57
|
-
|
|
77
|
+
# Try to use a linedef (called when player presses use key). `keys` is the
|
|
78
|
+
# acting player's key ring (a hash like {blue_card: true}); locked doors
|
|
79
|
+
# consult it and refuse to open without the matching card.
|
|
80
|
+
def use_linedef(linedef, _linedef_idx, keys = {})
|
|
58
81
|
return false if linedef.special == 0
|
|
59
82
|
|
|
60
83
|
case linedef.special
|
|
@@ -62,24 +85,24 @@ module Doom
|
|
|
62
85
|
when 1 # DR Door Open Wait Close
|
|
63
86
|
activate_door(linedef)
|
|
64
87
|
when 26 # DR Blue Door
|
|
65
|
-
activate_door(linedef, key: :blue_card)
|
|
88
|
+
activate_door(linedef, key: :blue_card, keys: keys)
|
|
66
89
|
when 27 # DR Yellow Door
|
|
67
|
-
activate_door(linedef, key: :yellow_card)
|
|
90
|
+
activate_door(linedef, key: :yellow_card, keys: keys)
|
|
68
91
|
when 28 # DR Red Door
|
|
69
|
-
activate_door(linedef, key: :red_card)
|
|
92
|
+
activate_door(linedef, key: :red_card, keys: keys)
|
|
70
93
|
when 31 # D1 Door Open Stay
|
|
71
94
|
activate_door(linedef, stay_open: true)
|
|
72
95
|
when 32 # D1 Blue Door Open Stay
|
|
73
|
-
activate_door(linedef, key: :blue_card, stay_open: true)
|
|
96
|
+
activate_door(linedef, key: :blue_card, stay_open: true, keys: keys)
|
|
74
97
|
when 33 # D1 Red Door Open Stay
|
|
75
|
-
activate_door(linedef, key: :red_card, stay_open: true)
|
|
98
|
+
activate_door(linedef, key: :red_card, stay_open: true, keys: keys)
|
|
76
99
|
when 34 # D1 Yellow Door Open Stay
|
|
77
|
-
activate_door(linedef, key: :yellow_card, stay_open: true)
|
|
100
|
+
activate_door(linedef, key: :yellow_card, stay_open: true, keys: keys)
|
|
78
101
|
when 103 # S1 Door Open Wait Close (tagged)
|
|
79
102
|
activate_tagged_door(linedef)
|
|
80
103
|
|
|
81
104
|
# --- Lifts ---
|
|
82
|
-
when 62
|
|
105
|
+
when 62 # SR Lift Lower Wait Raise (repeatable)
|
|
83
106
|
activate_lift(linedef)
|
|
84
107
|
|
|
85
108
|
# --- Floor changes ---
|
|
@@ -112,11 +135,11 @@ module Doom
|
|
|
112
135
|
|
|
113
136
|
# Walk-over trigger types:
|
|
114
137
|
# W1 = once, WR = repeatable
|
|
138
|
+
# Note: stair builders (specials 7 and 8) are not modeled, so they are
|
|
139
|
+
# intentionally absent here.
|
|
115
140
|
WALK_TRIGGERS = {
|
|
116
|
-
2
|
|
117
|
-
5
|
|
118
|
-
7 => :stairs, # S1 Build Stairs
|
|
119
|
-
8 => :stairs, # W1 Build Stairs
|
|
141
|
+
2 => :door_open_stay, # W1 Door Open Stay
|
|
142
|
+
5 => :raise_floor, # W1 Raise Floor to Lowest Ceiling
|
|
120
143
|
52 => :exit, # W1 Exit
|
|
121
144
|
82 => :lower_floor, # WR Lower Floor to Lowest
|
|
122
145
|
86 => :door_open_stay, # WR Door Open Stay
|
|
@@ -125,90 +148,104 @@ module Doom
|
|
|
125
148
|
91 => :raise_floor, # WR Raise Floor to Lowest Ceiling
|
|
126
149
|
97 => :teleport, # WR Teleport
|
|
127
150
|
98 => :lower_floor, # WR Lower Floor to Highest - 8
|
|
128
|
-
124 => :secret_exit
|
|
151
|
+
124 => :secret_exit # W1 Secret Exit
|
|
129
152
|
}.freeze
|
|
130
153
|
|
|
131
154
|
# W1 types that only trigger once
|
|
132
|
-
W1_TYPES = [2, 5,
|
|
155
|
+
W1_TYPES = [2, 5, 52, 124].freeze
|
|
133
156
|
|
|
157
|
+
# Walk triggers fire per player: every player must be able to cross a line
|
|
158
|
+
# and set it off. Players are processed in a fixed id order so the tic is
|
|
159
|
+
# reproducible, and each player's side memory is tracked separately (a
|
|
160
|
+
# shared record would let one player's crossing state clobber another's).
|
|
134
161
|
def check_walk_triggers
|
|
135
162
|
@near_linedefs ||= {}
|
|
136
163
|
|
|
164
|
+
@player_positions.each do |(pid, px, py)|
|
|
165
|
+
sides = (@near_linedefs[pid] ||= {})
|
|
166
|
+
check_walk_triggers_for(pid, px, py, sides)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def check_walk_triggers_for(pid, px, py, sides)
|
|
137
171
|
@map.linedefs.each_with_index do |ld, idx|
|
|
138
172
|
next if ld.special == 0
|
|
173
|
+
|
|
139
174
|
action = WALK_TRIGGERS[ld.special]
|
|
140
175
|
next unless action
|
|
141
176
|
|
|
142
|
-
# W1 types only trigger once
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
end
|
|
177
|
+
# W1 types only trigger once (level-global: once any player crosses,
|
|
178
|
+
# it is spent), so the crossed record stays shared across players.
|
|
179
|
+
next if W1_TYPES.include?(ld.special) && @crossed_linedefs[idx]
|
|
146
180
|
|
|
147
181
|
v1 = @map.vertices[ld.v1]
|
|
148
182
|
v2 = @map.vertices[ld.v2]
|
|
149
183
|
|
|
150
184
|
# Determine which side of the linedef the player is on
|
|
151
185
|
# DOOM's P_CrossSpecialLine fires when the player transitions sides
|
|
152
|
-
side = line_side(
|
|
153
|
-
dist =
|
|
186
|
+
side = line_side(px, py, v1.x, v1.y, v2.x, v2.y)
|
|
187
|
+
dist = Geometry.point_to_segment_distance(px, py, v1.x, v1.y, v2.x, v2.y)
|
|
154
188
|
|
|
155
|
-
near = dist < 32
|
|
156
|
-
prev_side =
|
|
189
|
+
near = dist < 32 # Detection range
|
|
190
|
+
prev_side = sides[idx]
|
|
157
191
|
|
|
158
192
|
if near && prev_side && prev_side != side
|
|
159
193
|
# Player crossed the line - trigger!
|
|
160
|
-
|
|
194
|
+
sides[idx] = side
|
|
161
195
|
elsif near && prev_side.nil?
|
|
162
196
|
# First time near - record side but don't trigger yet
|
|
163
|
-
|
|
197
|
+
sides[idx] = side
|
|
164
198
|
next
|
|
165
199
|
elsif !near
|
|
166
|
-
|
|
200
|
+
sides[idx] = nil
|
|
167
201
|
next
|
|
168
202
|
else
|
|
169
|
-
next
|
|
203
|
+
next # Same side, no crossing
|
|
170
204
|
end
|
|
171
205
|
|
|
172
206
|
@crossed_linedefs[idx] = true
|
|
207
|
+
fire_walk_action(action, ld, pid)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
173
210
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
def fire_walk_action(action, ld, pid)
|
|
212
|
+
case action
|
|
213
|
+
when :exit
|
|
214
|
+
@exit_triggered = :normal
|
|
215
|
+
when :secret_exit
|
|
216
|
+
@exit_triggered = :secret
|
|
217
|
+
when :door_open_stay
|
|
218
|
+
activate_tagged_door(ld, stay_open: true)
|
|
219
|
+
when :door
|
|
220
|
+
activate_tagged_door(ld)
|
|
221
|
+
when :lift
|
|
222
|
+
activate_lift(ld)
|
|
223
|
+
when :raise_floor
|
|
224
|
+
raise_floor_to_lowest_ceiling(ld)
|
|
225
|
+
when :lower_floor
|
|
226
|
+
lower_floor_to_highest(ld)
|
|
227
|
+
when :teleport
|
|
228
|
+
teleport_player(ld, pid)
|
|
192
229
|
end
|
|
193
230
|
end
|
|
194
231
|
|
|
195
|
-
# Returns which side of a line a point is on (
|
|
232
|
+
# Returns which side of a line a point is on, as an integer (1 = front,
|
|
233
|
+
# 0 = back). An integer rather than a symbol so the value serializes
|
|
234
|
+
# cleanly into the snapshot's per-player side memory.
|
|
196
235
|
def line_side(px, py, x1, y1, x2, y2)
|
|
197
|
-
cross = (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1)
|
|
198
|
-
cross >= 0 ?
|
|
236
|
+
cross = ((x2 - x1) * (py - y1)) - ((y2 - y1) * (px - x1))
|
|
237
|
+
cross >= 0 ? 1 : 0
|
|
199
238
|
end
|
|
200
239
|
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
end
|
|
240
|
+
def activate_door(linedef, stay_open: false, key: nil, keys: {})
|
|
241
|
+
# Locked doors refuse to open without the matching key. Refusal changes
|
|
242
|
+
# no state (deterministic), and plays the "no way" grunt if a sound
|
|
243
|
+
# engine is attached.
|
|
244
|
+
if key && !keys[key]
|
|
245
|
+
@sound&.noway
|
|
246
|
+
return
|
|
247
|
+
end
|
|
210
248
|
|
|
211
|
-
def activate_door(linedef, stay_open: false, key: nil)
|
|
212
249
|
# Find the sector on the back side of the linedef
|
|
213
250
|
return unless linedef.two_sided?
|
|
214
251
|
|
|
@@ -224,9 +261,7 @@ module Doom
|
|
|
224
261
|
if @active_doors[sector_idx]
|
|
225
262
|
door = @active_doors[sector_idx]
|
|
226
263
|
# If closing, reverse direction
|
|
227
|
-
if door[:state] == DOOR_CLOSING
|
|
228
|
-
door[:state] = DOOR_OPENING
|
|
229
|
-
end
|
|
264
|
+
door[:state] = DOOR_OPENING if door[:state] == DOOR_CLOSING
|
|
230
265
|
return
|
|
231
266
|
end
|
|
232
267
|
|
|
@@ -302,9 +337,7 @@ module Doom
|
|
|
302
337
|
adjacent_sector = @map.sectors[right_sidedef.sector]
|
|
303
338
|
end
|
|
304
339
|
|
|
305
|
-
if adjacent_sector
|
|
306
|
-
lowest = [lowest, adjacent_sector.ceiling_height].min
|
|
307
|
-
end
|
|
340
|
+
lowest = [lowest, adjacent_sector.ceiling_height].min if adjacent_sector
|
|
308
341
|
end
|
|
309
342
|
|
|
310
343
|
lowest == Float::INFINITY ? 128 : lowest
|
|
@@ -326,7 +359,7 @@ module Doom
|
|
|
326
359
|
target_height: target,
|
|
327
360
|
original_height: sector.ceiling_height,
|
|
328
361
|
wait_tics: 0,
|
|
329
|
-
stay_open: stay_open
|
|
362
|
+
stay_open: stay_open
|
|
330
363
|
}
|
|
331
364
|
end
|
|
332
365
|
@sound&.door_open
|
|
@@ -340,7 +373,7 @@ module Doom
|
|
|
340
373
|
activated = false
|
|
341
374
|
@map.sectors.each_with_index do |sector, idx|
|
|
342
375
|
next unless sector_has_tag?(idx, tag)
|
|
343
|
-
next if @active_lifts[idx]
|
|
376
|
+
next if @active_lifts[idx] # Already moving
|
|
344
377
|
|
|
345
378
|
lowest = find_lowest_floor_around(idx)
|
|
346
379
|
@active_lifts[idx] = {
|
|
@@ -348,7 +381,7 @@ module Doom
|
|
|
348
381
|
state: :lowering,
|
|
349
382
|
target_low: lowest,
|
|
350
383
|
original_height: sector.floor_height,
|
|
351
|
-
wait_tics: 0
|
|
384
|
+
wait_tics: 0
|
|
352
385
|
}
|
|
353
386
|
activated = true
|
|
354
387
|
end
|
|
@@ -379,6 +412,13 @@ module Doom
|
|
|
379
412
|
@active_lifts.delete(idx)
|
|
380
413
|
@sound&.platform_stop
|
|
381
414
|
end
|
|
415
|
+
when :floor_lowering
|
|
416
|
+
# One-way floor lower (e.g. special 23): lower to target and stay.
|
|
417
|
+
lift[:sector].floor_height -= LIFT_SPEED
|
|
418
|
+
if lift[:sector].floor_height <= lift[:target_low]
|
|
419
|
+
lift[:sector].floor_height = lift[:target_low]
|
|
420
|
+
@active_lifts.delete(idx)
|
|
421
|
+
end
|
|
382
422
|
end
|
|
383
423
|
end
|
|
384
424
|
end
|
|
@@ -389,6 +429,7 @@ module Doom
|
|
|
389
429
|
|
|
390
430
|
@map.sectors.each_with_index do |sector, idx|
|
|
391
431
|
next unless sector_has_tag?(idx, tag)
|
|
432
|
+
|
|
392
433
|
target = find_next_higher_floor(idx)
|
|
393
434
|
next if target <= sector.floor_height
|
|
394
435
|
|
|
@@ -397,7 +438,30 @@ module Doom
|
|
|
397
438
|
state: :raising,
|
|
398
439
|
target_low: sector.floor_height,
|
|
399
440
|
original_height: target,
|
|
400
|
-
wait_tics: 0
|
|
441
|
+
wait_tics: 0
|
|
442
|
+
}
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Specials 5 (W1) and 91 (WR): DOOM's raiseFloor mover raises the floor
|
|
447
|
+
# to the lowest ceiling among surrounding sectors. Both specials use the
|
|
448
|
+
# same mover in vanilla, so they are not split here.
|
|
449
|
+
def raise_floor_to_lowest_ceiling(linedef)
|
|
450
|
+
tag = linedef.tag
|
|
451
|
+
return if tag == 0
|
|
452
|
+
|
|
453
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
454
|
+
next unless sector_has_tag?(idx, tag)
|
|
455
|
+
|
|
456
|
+
target = find_lowest_ceiling_around(idx)
|
|
457
|
+
next if target <= sector.floor_height
|
|
458
|
+
|
|
459
|
+
@active_lifts[idx] = {
|
|
460
|
+
sector: sector,
|
|
461
|
+
state: :raising,
|
|
462
|
+
target_low: sector.floor_height,
|
|
463
|
+
original_height: target,
|
|
464
|
+
wait_tics: 0
|
|
401
465
|
}
|
|
402
466
|
end
|
|
403
467
|
end
|
|
@@ -408,8 +472,19 @@ module Doom
|
|
|
408
472
|
|
|
409
473
|
@map.sectors.each_with_index do |sector, idx|
|
|
410
474
|
next unless sector_has_tag?(idx, tag)
|
|
475
|
+
next if @active_lifts[idx] # Already moving
|
|
476
|
+
|
|
411
477
|
target = find_lowest_floor_around(idx)
|
|
412
|
-
sector.floor_height
|
|
478
|
+
next if target >= sector.floor_height
|
|
479
|
+
|
|
480
|
+
# Animate the lower (matching the raise movers) instead of snapping.
|
|
481
|
+
@active_lifts[idx] = {
|
|
482
|
+
sector: sector,
|
|
483
|
+
state: :floor_lowering,
|
|
484
|
+
target_low: target,
|
|
485
|
+
original_height: sector.floor_height,
|
|
486
|
+
wait_tics: 0
|
|
487
|
+
}
|
|
413
488
|
end
|
|
414
489
|
end
|
|
415
490
|
|
|
@@ -419,29 +494,31 @@ module Doom
|
|
|
419
494
|
|
|
420
495
|
@map.sectors.each_with_index do |sector, idx|
|
|
421
496
|
next unless sector_has_tag?(idx, tag)
|
|
497
|
+
|
|
422
498
|
target = find_highest_floor_around(idx) - 8
|
|
423
499
|
sector.floor_height = target if target < sector.floor_height
|
|
424
500
|
end
|
|
425
501
|
end
|
|
426
502
|
|
|
427
|
-
def teleport_player(linedef)
|
|
503
|
+
def teleport_player(linedef, player_id)
|
|
428
504
|
tag = linedef.tag
|
|
429
505
|
return if tag == 0
|
|
430
506
|
|
|
431
507
|
# Find teleport destination thing (type 14) in tagged sector
|
|
432
508
|
@map.things.each do |thing|
|
|
433
|
-
next unless thing.type == 14
|
|
509
|
+
next unless thing.type == 14 # Teleport destination
|
|
510
|
+
|
|
434
511
|
sector = @map.sector_at(thing.x, thing.y)
|
|
435
512
|
next unless sector
|
|
513
|
+
|
|
436
514
|
sector_idx = @map.sectors.index(sector)
|
|
437
515
|
next unless sector_has_tag?(sector_idx, tag)
|
|
438
516
|
|
|
439
|
-
@
|
|
517
|
+
@teleport_dests[player_id] = { x: thing.x, y: thing.y, angle: thing.angle }
|
|
440
518
|
return
|
|
441
519
|
end
|
|
442
520
|
end
|
|
443
521
|
|
|
444
|
-
|
|
445
522
|
def check_secrets
|
|
446
523
|
# Build set of secret sector indices on first call
|
|
447
524
|
@secret_sectors ||= Set.new(
|
|
@@ -465,12 +542,12 @@ module Doom
|
|
|
465
542
|
sector_idx = @map.sidedefs[sd_idx].sector
|
|
466
543
|
return if @secrets_found[sector_idx]
|
|
467
544
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
545
|
+
return unless @secret_sectors.include?(sector_idx)
|
|
546
|
+
|
|
547
|
+
@secrets_found[sector_idx] = true
|
|
548
|
+
# Clear the special so it doesn't retrigger (matching Chocolate Doom)
|
|
549
|
+
@map.sectors[sector_idx].special = 0
|
|
550
|
+
@secret_sectors.delete(sector_idx)
|
|
474
551
|
end
|
|
475
552
|
|
|
476
553
|
def sector_has_tag?(sector_idx, tag)
|
|
@@ -486,20 +563,18 @@ module Doom
|
|
|
486
563
|
end
|
|
487
564
|
|
|
488
565
|
def find_highest_floor_around(sector_idx)
|
|
489
|
-
highest = -
|
|
566
|
+
highest = -32_768
|
|
490
567
|
each_adjacent_sector(sector_idx) do |adj|
|
|
491
568
|
highest = adj.floor_height if adj.floor_height > highest
|
|
492
569
|
end
|
|
493
|
-
highest == -
|
|
570
|
+
highest == -32_768 ? @map.sectors[sector_idx].floor_height : highest
|
|
494
571
|
end
|
|
495
572
|
|
|
496
573
|
def find_next_higher_floor(sector_idx)
|
|
497
574
|
current = @map.sectors[sector_idx].floor_height
|
|
498
575
|
best = Float::INFINITY
|
|
499
576
|
each_adjacent_sector(sector_idx) do |adj|
|
|
500
|
-
if adj.floor_height > current && adj.floor_height < best
|
|
501
|
-
best = adj.floor_height
|
|
502
|
-
end
|
|
577
|
+
best = adj.floor_height if adj.floor_height > current && adj.floor_height < best
|
|
503
578
|
end
|
|
504
579
|
best == Float::INFINITY ? current : best
|
|
505
580
|
end
|
|
@@ -507,6 +582,7 @@ module Doom
|
|
|
507
582
|
def each_adjacent_sector(sector_idx)
|
|
508
583
|
@map.linedefs.each do |ld|
|
|
509
584
|
next unless ld.two_sided?
|
|
585
|
+
|
|
510
586
|
right = @map.sidedefs[ld.sidedef_right]
|
|
511
587
|
left = @map.sidedefs[ld.sidedef_left] if ld.sidedef_left != 0xFFFF
|
|
512
588
|
next unless left
|
|
@@ -10,8 +10,9 @@ module Doom
|
|
|
10
10
|
FASTDARK = 15 # Dark duration for fast strobe (tics)
|
|
11
11
|
SLOWDARK = 35 # Dark duration for slow strobe (tics)
|
|
12
12
|
|
|
13
|
-
def initialize(map)
|
|
13
|
+
def initialize(map, random: Random.new)
|
|
14
14
|
@map = map
|
|
15
|
+
@random = random
|
|
15
16
|
@effects = []
|
|
16
17
|
@scroll_sides = []
|
|
17
18
|
spawn_specials
|
|
@@ -29,27 +30,28 @@ module Doom
|
|
|
29
30
|
@map.sectors.each do |sector|
|
|
30
31
|
case sector.special
|
|
31
32
|
when 1 # Flickering lights
|
|
32
|
-
@effects << LightFlash.new(sector, find_min_light(sector))
|
|
33
|
+
@effects << LightFlash.new(sector, find_min_light(sector), @random)
|
|
33
34
|
when 2 # Fast strobe
|
|
34
|
-
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, false)
|
|
35
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, false, @random)
|
|
35
36
|
when 3 # Slow strobe
|
|
36
|
-
@effects << StrobeFlash.new(sector, find_min_light(sector), SLOWDARK, false)
|
|
37
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), SLOWDARK, false, @random)
|
|
37
38
|
when 4 # Fast strobe + 20% damage
|
|
38
|
-
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, false)
|
|
39
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, false, @random)
|
|
39
40
|
when 8 # Glowing light
|
|
40
41
|
@effects << Glow.new(sector, find_min_light(sector))
|
|
41
42
|
when 12 # Sync strobe slow
|
|
42
|
-
@effects << StrobeFlash.new(sector, find_min_light(sector), SLOWDARK, true)
|
|
43
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), SLOWDARK, true, @random)
|
|
43
44
|
when 13 # Sync strobe fast
|
|
44
|
-
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, true)
|
|
45
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, true, @random)
|
|
45
46
|
when 17 # Fire flicker
|
|
46
|
-
@effects << FireFlicker.new(sector, find_min_light(sector))
|
|
47
|
+
@effects << FireFlicker.new(sector, find_min_light(sector), @random)
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
# Linedef type 48: scrolling wall (front side scrolls +1 unit/tic)
|
|
51
52
|
@map.linedefs.each do |linedef|
|
|
52
53
|
next unless linedef.special == 48
|
|
54
|
+
|
|
53
55
|
side = @map.sidedefs[linedef.sidedef_right]
|
|
54
56
|
@scroll_sides << side if side
|
|
55
57
|
end
|
|
@@ -64,8 +66,10 @@ module Doom
|
|
|
64
66
|
@map.linedefs.each do |ld|
|
|
65
67
|
right = @map.sidedefs[ld.sidedef_right]
|
|
66
68
|
next unless right
|
|
69
|
+
|
|
67
70
|
left_idx = ld.sidedef_left
|
|
68
71
|
next if left_idx >= 0xFFFF
|
|
72
|
+
|
|
69
73
|
left = @map.sidedefs[left_idx]
|
|
70
74
|
next unless left
|
|
71
75
|
|
|
@@ -82,11 +86,12 @@ module Doom
|
|
|
82
86
|
|
|
83
87
|
# T_LightFlash (type 1): mostly bright with brief random dark flickers
|
|
84
88
|
class LightFlash
|
|
85
|
-
def initialize(sector, minlight)
|
|
89
|
+
def initialize(sector, minlight, random)
|
|
86
90
|
@sector = sector
|
|
91
|
+
@random = random
|
|
87
92
|
@maxlight = sector.light_level
|
|
88
93
|
@minlight = minlight
|
|
89
|
-
@count =
|
|
94
|
+
@count = @random.rand(65) + 1
|
|
90
95
|
end
|
|
91
96
|
|
|
92
97
|
def update
|
|
@@ -95,24 +100,25 @@ module Doom
|
|
|
95
100
|
|
|
96
101
|
if @sector.light_level == @maxlight
|
|
97
102
|
@sector.light_level = @minlight
|
|
98
|
-
@count =
|
|
103
|
+
@count = @random.rand(8) + 1 # dark for 1-8 tics
|
|
99
104
|
else
|
|
100
105
|
@sector.light_level = @maxlight
|
|
101
|
-
@count = (rand(2) == 0 ? 1 : 65)
|
|
106
|
+
@count = (@random.rand(2) == 0 ? 1 : 65) # bright for 1 or 65 tics (P_Random()&64)
|
|
102
107
|
end
|
|
103
108
|
end
|
|
104
109
|
end
|
|
105
110
|
|
|
106
111
|
# T_StrobeFlash (types 2, 3, 4, 12, 13): regular strobe blink
|
|
107
112
|
class StrobeFlash
|
|
108
|
-
def initialize(sector, minlight, darktime, in_sync)
|
|
113
|
+
def initialize(sector, minlight, darktime, in_sync, random)
|
|
109
114
|
@sector = sector
|
|
115
|
+
@random = random
|
|
110
116
|
@maxlight = sector.light_level
|
|
111
117
|
@minlight = minlight
|
|
112
118
|
@minlight = 0 if @minlight == @maxlight
|
|
113
119
|
@darktime = darktime
|
|
114
120
|
@brighttime = STROBEBRIGHT
|
|
115
|
-
@count = in_sync ? 1 :
|
|
121
|
+
@count = in_sync ? 1 : @random.rand(8) + 1
|
|
116
122
|
end
|
|
117
123
|
|
|
118
124
|
def update
|
|
@@ -135,7 +141,7 @@ module Doom
|
|
|
135
141
|
@sector = sector
|
|
136
142
|
@maxlight = sector.light_level
|
|
137
143
|
@minlight = minlight
|
|
138
|
-
@direction = -1
|
|
144
|
+
@direction = -1 # start dimming
|
|
139
145
|
end
|
|
140
146
|
|
|
141
147
|
def update
|
|
@@ -157,10 +163,11 @@ module Doom
|
|
|
157
163
|
|
|
158
164
|
# T_FireFlicker (type 17): random fire-like flickering
|
|
159
165
|
class FireFlicker
|
|
160
|
-
def initialize(sector, minlight)
|
|
166
|
+
def initialize(sector, minlight, random)
|
|
161
167
|
@sector = sector
|
|
162
168
|
@maxlight = sector.light_level
|
|
163
|
-
@minlight = minlight + 16
|
|
169
|
+
@minlight = minlight + 16 # fire doesn't go as dark
|
|
170
|
+
@random = random
|
|
164
171
|
@count = 4
|
|
165
172
|
end
|
|
166
173
|
|
|
@@ -168,7 +175,7 @@ module Doom
|
|
|
168
175
|
@count -= 1
|
|
169
176
|
return if @count > 0
|
|
170
177
|
|
|
171
|
-
amount =
|
|
178
|
+
amount = @random.rand(4) * 16 # 0, 16, 32, or 48
|
|
172
179
|
level = @maxlight - amount
|
|
173
180
|
@sector.light_level = level < @minlight ? @minlight : level
|
|
174
181
|
@count = 4
|