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
|
@@ -1,64 +1,251 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
module Doom
|
|
4
6
|
module Game
|
|
5
7
|
# Manages animated sector actions (doors, lifts, etc.)
|
|
6
8
|
class SectorActions
|
|
7
|
-
# Door states
|
|
8
|
-
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).
|
|
9
11
|
DOOR_OPENING = 1
|
|
10
12
|
DOOR_OPEN = 2
|
|
11
13
|
DOOR_CLOSING = 3
|
|
12
14
|
|
|
13
15
|
# Door speeds (units per tic, 35 tics/sec)
|
|
14
16
|
DOOR_SPEED = 2
|
|
15
|
-
DOOR_WAIT = 150
|
|
16
|
-
|
|
17
|
+
DOOR_WAIT = 150 # Tics to wait when open (~4 seconds)
|
|
18
|
+
|
|
19
|
+
# Lift constants
|
|
20
|
+
LIFT_SPEED = 4
|
|
21
|
+
LIFT_WAIT = 105 # ~3 seconds
|
|
22
|
+
|
|
23
|
+
attr_reader :exit_triggered, :secrets_found
|
|
24
|
+
|
|
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
|
|
33
|
+
end
|
|
17
34
|
|
|
18
|
-
def initialize(map)
|
|
35
|
+
def initialize(map, sound_engine = nil)
|
|
19
36
|
@map = map
|
|
20
|
-
@
|
|
37
|
+
@sound = sound_engine
|
|
38
|
+
@active_doors = {} # sector_index => door_state
|
|
39
|
+
@active_lifts = {} # sector_index => lift_state
|
|
21
40
|
@player_x = 0
|
|
22
41
|
@player_y = 0
|
|
42
|
+
@player_positions = [] # [[player_id, x, y], ...] in id order
|
|
43
|
+
@exit_triggered = nil
|
|
44
|
+
@secrets_found = {} # sector_index => true
|
|
45
|
+
@crossed_linedefs = {}
|
|
46
|
+
@near_linedefs = {} # player_id => { linedef_idx => side }
|
|
47
|
+
@teleport_dests = {} # player_id => { x:, y:, angle: }
|
|
23
48
|
end
|
|
24
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.
|
|
25
52
|
def update_player_position(x, y)
|
|
26
53
|
@player_x = x
|
|
27
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]
|
|
28
68
|
end
|
|
29
69
|
|
|
30
70
|
def update
|
|
31
71
|
update_doors
|
|
72
|
+
update_lifts
|
|
73
|
+
check_walk_triggers
|
|
74
|
+
check_secrets
|
|
32
75
|
end
|
|
33
76
|
|
|
34
|
-
# Try to use a linedef (called when player presses use key)
|
|
35
|
-
|
|
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 = {})
|
|
36
81
|
return false if linedef.special == 0
|
|
37
82
|
|
|
38
83
|
case linedef.special
|
|
39
|
-
|
|
84
|
+
# --- Doors ---
|
|
85
|
+
when 1 # DR Door Open Wait Close
|
|
40
86
|
activate_door(linedef)
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
when 26 # DR Blue Door
|
|
88
|
+
activate_door(linedef, key: :blue_card, keys: keys)
|
|
89
|
+
when 27 # DR Yellow Door
|
|
90
|
+
activate_door(linedef, key: :yellow_card, keys: keys)
|
|
91
|
+
when 28 # DR Red Door
|
|
92
|
+
activate_door(linedef, key: :red_card, keys: keys)
|
|
93
|
+
when 31 # D1 Door Open Stay
|
|
43
94
|
activate_door(linedef, stay_open: true)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
true
|
|
48
|
-
when
|
|
49
|
-
activate_door(linedef, key: :yellow_card)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
95
|
+
when 32 # D1 Blue Door Open Stay
|
|
96
|
+
activate_door(linedef, key: :blue_card, stay_open: true, keys: keys)
|
|
97
|
+
when 33 # D1 Red Door Open Stay
|
|
98
|
+
activate_door(linedef, key: :red_card, stay_open: true, keys: keys)
|
|
99
|
+
when 34 # D1 Yellow Door Open Stay
|
|
100
|
+
activate_door(linedef, key: :yellow_card, stay_open: true, keys: keys)
|
|
101
|
+
when 103 # S1 Door Open Wait Close (tagged)
|
|
102
|
+
activate_tagged_door(linedef)
|
|
103
|
+
|
|
104
|
+
# --- Lifts ---
|
|
105
|
+
when 62 # SR Lift Lower Wait Raise (repeatable)
|
|
106
|
+
activate_lift(linedef)
|
|
107
|
+
|
|
108
|
+
# --- Floor changes ---
|
|
109
|
+
when 18 # S1 Raise Floor to Next Higher
|
|
110
|
+
raise_floor_to_next(linedef)
|
|
111
|
+
when 20 # S1 Raise Floor to Next Higher (platform)
|
|
112
|
+
raise_floor_to_next(linedef)
|
|
113
|
+
when 22 # W1 Raise Floor to Next Higher
|
|
114
|
+
raise_floor_to_next(linedef)
|
|
115
|
+
when 23 # S1 Lower Floor to Lowest
|
|
116
|
+
lower_floor_to_lowest(linedef)
|
|
117
|
+
when 36 # S1 Lower Floor to Highest Adjacent - 8
|
|
118
|
+
lower_floor_to_highest(linedef)
|
|
119
|
+
when 70 # SR Lower Floor to Highest Adjacent - 8
|
|
120
|
+
lower_floor_to_highest(linedef)
|
|
121
|
+
|
|
122
|
+
# --- Exits ---
|
|
123
|
+
when 11 # S1 Exit
|
|
124
|
+
@exit_triggered = :normal
|
|
125
|
+
when 51 # S1 Secret Exit
|
|
126
|
+
@exit_triggered = :secret
|
|
127
|
+
|
|
54
128
|
else
|
|
55
|
-
false
|
|
129
|
+
return false
|
|
56
130
|
end
|
|
131
|
+
true
|
|
57
132
|
end
|
|
58
133
|
|
|
59
134
|
private
|
|
60
135
|
|
|
61
|
-
|
|
136
|
+
# Walk-over trigger types:
|
|
137
|
+
# W1 = once, WR = repeatable
|
|
138
|
+
# Note: stair builders (specials 7 and 8) are not modeled, so they are
|
|
139
|
+
# intentionally absent here.
|
|
140
|
+
WALK_TRIGGERS = {
|
|
141
|
+
2 => :door_open_stay, # W1 Door Open Stay
|
|
142
|
+
5 => :raise_floor, # W1 Raise Floor to Lowest Ceiling
|
|
143
|
+
52 => :exit, # W1 Exit
|
|
144
|
+
82 => :lower_floor, # WR Lower Floor to Lowest
|
|
145
|
+
86 => :door_open_stay, # WR Door Open Stay
|
|
146
|
+
88 => :lift, # WR Lift Lower Wait Raise
|
|
147
|
+
90 => :door, # WR Door Open Wait Close
|
|
148
|
+
91 => :raise_floor, # WR Raise Floor to Lowest Ceiling
|
|
149
|
+
97 => :teleport, # WR Teleport
|
|
150
|
+
98 => :lower_floor, # WR Lower Floor to Highest - 8
|
|
151
|
+
124 => :secret_exit # W1 Secret Exit
|
|
152
|
+
}.freeze
|
|
153
|
+
|
|
154
|
+
# W1 types that only trigger once
|
|
155
|
+
W1_TYPES = [2, 5, 52, 124].freeze
|
|
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).
|
|
161
|
+
def check_walk_triggers
|
|
162
|
+
@near_linedefs ||= {}
|
|
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)
|
|
171
|
+
@map.linedefs.each_with_index do |ld, idx|
|
|
172
|
+
next if ld.special == 0
|
|
173
|
+
|
|
174
|
+
action = WALK_TRIGGERS[ld.special]
|
|
175
|
+
next unless action
|
|
176
|
+
|
|
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]
|
|
180
|
+
|
|
181
|
+
v1 = @map.vertices[ld.v1]
|
|
182
|
+
v2 = @map.vertices[ld.v2]
|
|
183
|
+
|
|
184
|
+
# Determine which side of the linedef the player is on
|
|
185
|
+
# DOOM's P_CrossSpecialLine fires when the player transitions sides
|
|
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)
|
|
188
|
+
|
|
189
|
+
near = dist < 32 # Detection range
|
|
190
|
+
prev_side = sides[idx]
|
|
191
|
+
|
|
192
|
+
if near && prev_side && prev_side != side
|
|
193
|
+
# Player crossed the line - trigger!
|
|
194
|
+
sides[idx] = side
|
|
195
|
+
elsif near && prev_side.nil?
|
|
196
|
+
# First time near - record side but don't trigger yet
|
|
197
|
+
sides[idx] = side
|
|
198
|
+
next
|
|
199
|
+
elsif !near
|
|
200
|
+
sides[idx] = nil
|
|
201
|
+
next
|
|
202
|
+
else
|
|
203
|
+
next # Same side, no crossing
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
@crossed_linedefs[idx] = true
|
|
207
|
+
fire_walk_action(action, ld, pid)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
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)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
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.
|
|
235
|
+
def line_side(px, py, x1, y1, x2, y2)
|
|
236
|
+
cross = ((x2 - x1) * (py - y1)) - ((y2 - y1) * (px - x1))
|
|
237
|
+
cross >= 0 ? 1 : 0
|
|
238
|
+
end
|
|
239
|
+
|
|
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
|
|
248
|
+
|
|
62
249
|
# Find the sector on the back side of the linedef
|
|
63
250
|
return unless linedef.two_sided?
|
|
64
251
|
|
|
@@ -74,9 +261,7 @@ module Doom
|
|
|
74
261
|
if @active_doors[sector_idx]
|
|
75
262
|
door = @active_doors[sector_idx]
|
|
76
263
|
# If closing, reverse direction
|
|
77
|
-
if door[:state] == DOOR_CLOSING
|
|
78
|
-
door[:state] = DOOR_OPENING
|
|
79
|
-
end
|
|
264
|
+
door[:state] = DOOR_OPENING if door[:state] == DOOR_CLOSING
|
|
80
265
|
return
|
|
81
266
|
end
|
|
82
267
|
|
|
@@ -92,6 +277,7 @@ module Doom
|
|
|
92
277
|
wait_tics: 0,
|
|
93
278
|
stay_open: stay_open
|
|
94
279
|
}
|
|
280
|
+
@sound&.door_open
|
|
95
281
|
end
|
|
96
282
|
|
|
97
283
|
def update_doors
|
|
@@ -113,6 +299,7 @@ module Doom
|
|
|
113
299
|
door[:wait_tics] -= 1
|
|
114
300
|
if door[:wait_tics] <= 0
|
|
115
301
|
door[:state] = DOOR_CLOSING
|
|
302
|
+
@sound&.door_close
|
|
116
303
|
end
|
|
117
304
|
|
|
118
305
|
when DOOR_CLOSING
|
|
@@ -150,13 +337,263 @@ module Doom
|
|
|
150
337
|
adjacent_sector = @map.sectors[right_sidedef.sector]
|
|
151
338
|
end
|
|
152
339
|
|
|
153
|
-
if adjacent_sector
|
|
154
|
-
lowest = [lowest, adjacent_sector.ceiling_height].min
|
|
155
|
-
end
|
|
340
|
+
lowest = [lowest, adjacent_sector.ceiling_height].min if adjacent_sector
|
|
156
341
|
end
|
|
157
342
|
|
|
158
343
|
lowest == Float::INFINITY ? 128 : lowest
|
|
159
344
|
end
|
|
345
|
+
|
|
346
|
+
# Door activated by tag (for S1/W1/WR tagged doors)
|
|
347
|
+
def activate_tagged_door(linedef, stay_open: false)
|
|
348
|
+
tag = linedef.tag
|
|
349
|
+
return if tag == 0
|
|
350
|
+
|
|
351
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
352
|
+
next unless sector_has_tag?(idx, tag)
|
|
353
|
+
next if @active_doors[idx]
|
|
354
|
+
|
|
355
|
+
target = find_lowest_ceiling_around(idx) - 4
|
|
356
|
+
@active_doors[idx] = {
|
|
357
|
+
sector: sector,
|
|
358
|
+
state: DOOR_OPENING,
|
|
359
|
+
target_height: target,
|
|
360
|
+
original_height: sector.ceiling_height,
|
|
361
|
+
wait_tics: 0,
|
|
362
|
+
stay_open: stay_open
|
|
363
|
+
}
|
|
364
|
+
end
|
|
365
|
+
@sound&.door_open
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Lift: lower floor to lowest adjacent, wait, raise back
|
|
369
|
+
def activate_lift(linedef)
|
|
370
|
+
tag = linedef.tag
|
|
371
|
+
return if tag == 0
|
|
372
|
+
|
|
373
|
+
activated = false
|
|
374
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
375
|
+
next unless sector_has_tag?(idx, tag)
|
|
376
|
+
next if @active_lifts[idx] # Already moving
|
|
377
|
+
|
|
378
|
+
lowest = find_lowest_floor_around(idx)
|
|
379
|
+
@active_lifts[idx] = {
|
|
380
|
+
sector: sector,
|
|
381
|
+
state: :lowering,
|
|
382
|
+
target_low: lowest,
|
|
383
|
+
original_height: sector.floor_height,
|
|
384
|
+
wait_tics: 0
|
|
385
|
+
}
|
|
386
|
+
activated = true
|
|
387
|
+
end
|
|
388
|
+
@sound&.platform_start if activated
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def update_lifts
|
|
392
|
+
@active_lifts.each do |idx, lift|
|
|
393
|
+
case lift[:state]
|
|
394
|
+
when :lowering
|
|
395
|
+
lift[:sector].floor_height -= LIFT_SPEED
|
|
396
|
+
if lift[:sector].floor_height <= lift[:target_low]
|
|
397
|
+
lift[:sector].floor_height = lift[:target_low]
|
|
398
|
+
lift[:state] = :waiting
|
|
399
|
+
lift[:wait_tics] = LIFT_WAIT
|
|
400
|
+
@sound&.platform_stop
|
|
401
|
+
end
|
|
402
|
+
when :waiting
|
|
403
|
+
lift[:wait_tics] -= 1
|
|
404
|
+
if lift[:wait_tics] <= 0
|
|
405
|
+
lift[:state] = :raising
|
|
406
|
+
@sound&.platform_start
|
|
407
|
+
end
|
|
408
|
+
when :raising
|
|
409
|
+
lift[:sector].floor_height += LIFT_SPEED
|
|
410
|
+
if lift[:sector].floor_height >= lift[:original_height]
|
|
411
|
+
lift[:sector].floor_height = lift[:original_height]
|
|
412
|
+
@active_lifts.delete(idx)
|
|
413
|
+
@sound&.platform_stop
|
|
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
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def raise_floor_to_next(linedef)
|
|
427
|
+
tag = linedef.tag
|
|
428
|
+
return if tag == 0
|
|
429
|
+
|
|
430
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
431
|
+
next unless sector_has_tag?(idx, tag)
|
|
432
|
+
|
|
433
|
+
target = find_next_higher_floor(idx)
|
|
434
|
+
next if target <= sector.floor_height
|
|
435
|
+
|
|
436
|
+
@active_lifts[idx] = {
|
|
437
|
+
sector: sector,
|
|
438
|
+
state: :raising,
|
|
439
|
+
target_low: sector.floor_height,
|
|
440
|
+
original_height: target,
|
|
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
|
|
465
|
+
}
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def lower_floor_to_lowest(linedef)
|
|
470
|
+
tag = linedef.tag
|
|
471
|
+
return if tag == 0
|
|
472
|
+
|
|
473
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
474
|
+
next unless sector_has_tag?(idx, tag)
|
|
475
|
+
next if @active_lifts[idx] # Already moving
|
|
476
|
+
|
|
477
|
+
target = find_lowest_floor_around(idx)
|
|
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
|
+
}
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def lower_floor_to_highest(linedef)
|
|
492
|
+
tag = linedef.tag
|
|
493
|
+
return if tag == 0
|
|
494
|
+
|
|
495
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
496
|
+
next unless sector_has_tag?(idx, tag)
|
|
497
|
+
|
|
498
|
+
target = find_highest_floor_around(idx) - 8
|
|
499
|
+
sector.floor_height = target if target < sector.floor_height
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def teleport_player(linedef, player_id)
|
|
504
|
+
tag = linedef.tag
|
|
505
|
+
return if tag == 0
|
|
506
|
+
|
|
507
|
+
# Find teleport destination thing (type 14) in tagged sector
|
|
508
|
+
@map.things.each do |thing|
|
|
509
|
+
next unless thing.type == 14 # Teleport destination
|
|
510
|
+
|
|
511
|
+
sector = @map.sector_at(thing.x, thing.y)
|
|
512
|
+
next unless sector
|
|
513
|
+
|
|
514
|
+
sector_idx = @map.sectors.index(sector)
|
|
515
|
+
next unless sector_has_tag?(sector_idx, tag)
|
|
516
|
+
|
|
517
|
+
@teleport_dests[player_id] = { x: thing.x, y: thing.y, angle: thing.angle }
|
|
518
|
+
return
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def check_secrets
|
|
523
|
+
# Build set of secret sector indices on first call
|
|
524
|
+
@secret_sectors ||= Set.new(
|
|
525
|
+
@map.sectors.each_with_index.filter_map { |s, i| i if s.special == 9 }
|
|
526
|
+
)
|
|
527
|
+
return if @secret_sectors.empty?
|
|
528
|
+
|
|
529
|
+
# Find which sector the player is in via BSP subsector lookup
|
|
530
|
+
subsector = @map.subsector_at(@player_x, @player_y)
|
|
531
|
+
return unless subsector
|
|
532
|
+
|
|
533
|
+
seg = @map.segs[subsector.first_seg]
|
|
534
|
+
return unless seg
|
|
535
|
+
|
|
536
|
+
ld = @map.linedefs[seg.linedef]
|
|
537
|
+
return unless ld
|
|
538
|
+
|
|
539
|
+
sd_idx = seg.direction == 0 ? ld.sidedef_right : ld.sidedef_left
|
|
540
|
+
return if sd_idx == 0xFFFF
|
|
541
|
+
|
|
542
|
+
sector_idx = @map.sidedefs[sd_idx].sector
|
|
543
|
+
return if @secrets_found[sector_idx]
|
|
544
|
+
|
|
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)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def sector_has_tag?(sector_idx, tag)
|
|
554
|
+
@map.sectors[sector_idx].tag == tag
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def find_lowest_floor_around(sector_idx)
|
|
558
|
+
lowest = @map.sectors[sector_idx].floor_height
|
|
559
|
+
each_adjacent_sector(sector_idx) do |adj|
|
|
560
|
+
lowest = adj.floor_height if adj.floor_height < lowest
|
|
561
|
+
end
|
|
562
|
+
lowest
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def find_highest_floor_around(sector_idx)
|
|
566
|
+
highest = -32_768
|
|
567
|
+
each_adjacent_sector(sector_idx) do |adj|
|
|
568
|
+
highest = adj.floor_height if adj.floor_height > highest
|
|
569
|
+
end
|
|
570
|
+
highest == -32_768 ? @map.sectors[sector_idx].floor_height : highest
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def find_next_higher_floor(sector_idx)
|
|
574
|
+
current = @map.sectors[sector_idx].floor_height
|
|
575
|
+
best = Float::INFINITY
|
|
576
|
+
each_adjacent_sector(sector_idx) do |adj|
|
|
577
|
+
best = adj.floor_height if adj.floor_height > current && adj.floor_height < best
|
|
578
|
+
end
|
|
579
|
+
best == Float::INFINITY ? current : best
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def each_adjacent_sector(sector_idx)
|
|
583
|
+
@map.linedefs.each do |ld|
|
|
584
|
+
next unless ld.two_sided?
|
|
585
|
+
|
|
586
|
+
right = @map.sidedefs[ld.sidedef_right]
|
|
587
|
+
left = @map.sidedefs[ld.sidedef_left] if ld.sidedef_left != 0xFFFF
|
|
588
|
+
next unless left
|
|
589
|
+
|
|
590
|
+
if right.sector == sector_idx
|
|
591
|
+
yield @map.sectors[left.sector]
|
|
592
|
+
elsif left.sector == sector_idx
|
|
593
|
+
yield @map.sectors[right.sector]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
160
597
|
end
|
|
161
598
|
end
|
|
162
599
|
end
|
|
@@ -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
|