doom 0.6.0 → 0.8.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/bin/doom +5 -2
- data/lib/doom/game/combat.rb +226 -40
- data/lib/doom/game/intermission.rb +248 -0
- data/lib/doom/game/item_pickup.rb +40 -4
- data/lib/doom/game/menu.rb +342 -0
- data/lib/doom/game/monster_ai.rb +210 -11
- data/lib/doom/game/player_state.rb +10 -1
- data/lib/doom/game/sector_actions.rb +376 -15
- data/lib/doom/game/sound_engine.rb +201 -0
- data/lib/doom/platform/gosu_window.rb +460 -52
- data/lib/doom/render/font.rb +78 -0
- data/lib/doom/render/renderer.rb +361 -33
- data/lib/doom/render/screen_melt.rb +71 -0
- data/lib/doom/render/weapon_renderer.rb +11 -12
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/reader.rb +52 -0
- data/lib/doom/wad/sound.rb +85 -0
- data/lib/doom/wad/sprite.rb +29 -4
- data/lib/doom/wad/texture.rb +1 -1
- data/lib/doom.rb +47 -5
- metadata +7 -1
|
@@ -48,6 +48,7 @@ module Doom
|
|
|
48
48
|
attr_accessor :is_moving
|
|
49
49
|
attr_accessor :dead, :death_tic
|
|
50
50
|
attr_accessor :damage_count # Red flash intensity (0-8), decays each tic
|
|
51
|
+
attr_accessor :god_mode, :infinite_ammo
|
|
51
52
|
|
|
52
53
|
# Smooth step-up/down (matching Chocolate Doom's P_CalcHeight / P_ZMovement)
|
|
53
54
|
VIEWHEIGHT = 41.0
|
|
@@ -114,6 +115,10 @@ module Doom
|
|
|
114
115
|
@death_tic = 0
|
|
115
116
|
@damage_count = 0
|
|
116
117
|
|
|
118
|
+
# Cheats
|
|
119
|
+
@god_mode = false
|
|
120
|
+
@infinite_ammo = false
|
|
121
|
+
|
|
117
122
|
# Weapon bob
|
|
118
123
|
@bob_angle = 0.0
|
|
119
124
|
@bob_amount = 0.0
|
|
@@ -168,6 +173,7 @@ module Doom
|
|
|
168
173
|
|
|
169
174
|
def can_attack?
|
|
170
175
|
return true if @weapon == WEAPON_FIST || @weapon == WEAPON_CHAINSAW
|
|
176
|
+
return true if @infinite_ammo
|
|
171
177
|
|
|
172
178
|
ammo = current_ammo
|
|
173
179
|
ammo && ammo > 0
|
|
@@ -181,7 +187,9 @@ module Doom
|
|
|
181
187
|
@attack_frame = 0
|
|
182
188
|
@attack_tics = 0
|
|
183
189
|
|
|
184
|
-
# Consume ammo
|
|
190
|
+
# Consume ammo (skipped with infinite ammo)
|
|
191
|
+
return if @infinite_ammo
|
|
192
|
+
|
|
185
193
|
case @weapon
|
|
186
194
|
when WEAPON_PISTOL
|
|
187
195
|
@ammo_bullets -= 1 if @ammo_bullets > 0
|
|
@@ -319,6 +327,7 @@ module Doom
|
|
|
319
327
|
# Apply damage (from environment or enemies). Armor absorbs some.
|
|
320
328
|
def take_damage(amount)
|
|
321
329
|
return if @dead
|
|
330
|
+
return if @god_mode
|
|
322
331
|
|
|
323
332
|
absorbed = 0
|
|
324
333
|
if @armor > 0
|
|
@@ -1,5 +1,7 @@
|
|
|
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.)
|
|
@@ -13,13 +15,30 @@ module Doom
|
|
|
13
15
|
# Door speeds (units per tic, 35 tics/sec)
|
|
14
16
|
DOOR_SPEED = 2
|
|
15
17
|
DOOR_WAIT = 150 # Tics to wait when open (~4 seconds)
|
|
16
|
-
PLAYER_HEIGHT = 56
|
|
18
|
+
PLAYER_HEIGHT = 56
|
|
19
|
+
|
|
20
|
+
# Lift constants
|
|
21
|
+
LIFT_SPEED = 4
|
|
22
|
+
LIFT_WAIT = 105 # ~3 seconds
|
|
23
|
+
|
|
24
|
+
attr_reader :exit_triggered, :secrets_found
|
|
17
25
|
|
|
18
|
-
def
|
|
26
|
+
def pop_teleport
|
|
27
|
+
dest = @teleport_dest
|
|
28
|
+
@teleport_dest = nil
|
|
29
|
+
dest
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(map, sound_engine = nil)
|
|
19
33
|
@map = map
|
|
20
|
-
@
|
|
34
|
+
@sound = sound_engine
|
|
35
|
+
@active_doors = {} # sector_index => door_state
|
|
36
|
+
@active_lifts = {} # sector_index => lift_state
|
|
21
37
|
@player_x = 0
|
|
22
38
|
@player_y = 0
|
|
39
|
+
@exit_triggered = nil
|
|
40
|
+
@secrets_found = {} # sector_index => true
|
|
41
|
+
@crossed_linedefs = {}
|
|
23
42
|
end
|
|
24
43
|
|
|
25
44
|
def update_player_position(x, y)
|
|
@@ -29,6 +48,9 @@ module Doom
|
|
|
29
48
|
|
|
30
49
|
def update
|
|
31
50
|
update_doors
|
|
51
|
+
update_lifts
|
|
52
|
+
check_walk_triggers
|
|
53
|
+
check_secrets
|
|
32
54
|
end
|
|
33
55
|
|
|
34
56
|
# Try to use a linedef (called when player presses use key)
|
|
@@ -36,28 +58,156 @@ module Doom
|
|
|
36
58
|
return false if linedef.special == 0
|
|
37
59
|
|
|
38
60
|
case linedef.special
|
|
39
|
-
|
|
61
|
+
# --- Doors ---
|
|
62
|
+
when 1 # DR Door Open Wait Close
|
|
40
63
|
activate_door(linedef)
|
|
41
|
-
|
|
42
|
-
when 31 # D1 Door Open Stay
|
|
43
|
-
activate_door(linedef, stay_open: true)
|
|
44
|
-
true
|
|
45
|
-
when 26 # DR Blue Door
|
|
64
|
+
when 26 # DR Blue Door
|
|
46
65
|
activate_door(linedef, key: :blue_card)
|
|
47
|
-
|
|
48
|
-
when 27 # DR Yellow Door
|
|
66
|
+
when 27 # DR Yellow Door
|
|
49
67
|
activate_door(linedef, key: :yellow_card)
|
|
50
|
-
|
|
51
|
-
when 28 # DR Red Door
|
|
68
|
+
when 28 # DR Red Door
|
|
52
69
|
activate_door(linedef, key: :red_card)
|
|
53
|
-
|
|
70
|
+
when 31 # D1 Door Open Stay
|
|
71
|
+
activate_door(linedef, stay_open: true)
|
|
72
|
+
when 32 # D1 Blue Door Open Stay
|
|
73
|
+
activate_door(linedef, key: :blue_card, stay_open: true)
|
|
74
|
+
when 33 # D1 Red Door Open Stay
|
|
75
|
+
activate_door(linedef, key: :red_card, stay_open: true)
|
|
76
|
+
when 34 # D1 Yellow Door Open Stay
|
|
77
|
+
activate_door(linedef, key: :yellow_card, stay_open: true)
|
|
78
|
+
when 103 # S1 Door Open Wait Close (tagged)
|
|
79
|
+
activate_tagged_door(linedef)
|
|
80
|
+
|
|
81
|
+
# --- Lifts ---
|
|
82
|
+
when 62 # SR Lift Lower Wait Raise (repeatable)
|
|
83
|
+
activate_lift(linedef)
|
|
84
|
+
|
|
85
|
+
# --- Floor changes ---
|
|
86
|
+
when 18 # S1 Raise Floor to Next Higher
|
|
87
|
+
raise_floor_to_next(linedef)
|
|
88
|
+
when 20 # S1 Raise Floor to Next Higher (platform)
|
|
89
|
+
raise_floor_to_next(linedef)
|
|
90
|
+
when 22 # W1 Raise Floor to Next Higher
|
|
91
|
+
raise_floor_to_next(linedef)
|
|
92
|
+
when 23 # S1 Lower Floor to Lowest
|
|
93
|
+
lower_floor_to_lowest(linedef)
|
|
94
|
+
when 36 # S1 Lower Floor to Highest Adjacent - 8
|
|
95
|
+
lower_floor_to_highest(linedef)
|
|
96
|
+
when 70 # SR Lower Floor to Highest Adjacent - 8
|
|
97
|
+
lower_floor_to_highest(linedef)
|
|
98
|
+
|
|
99
|
+
# --- Exits ---
|
|
100
|
+
when 11 # S1 Exit
|
|
101
|
+
@exit_triggered = :normal
|
|
102
|
+
when 51 # S1 Secret Exit
|
|
103
|
+
@exit_triggered = :secret
|
|
104
|
+
|
|
54
105
|
else
|
|
55
|
-
false
|
|
106
|
+
return false
|
|
56
107
|
end
|
|
108
|
+
true
|
|
57
109
|
end
|
|
58
110
|
|
|
59
111
|
private
|
|
60
112
|
|
|
113
|
+
# Walk-over trigger types:
|
|
114
|
+
# W1 = once, WR = repeatable
|
|
115
|
+
WALK_TRIGGERS = {
|
|
116
|
+
2 => :door_open_stay, # W1 Door Open Stay
|
|
117
|
+
5 => :raise_floor, # W1 Raise Floor to Lowest Ceiling
|
|
118
|
+
7 => :stairs, # S1 Build Stairs
|
|
119
|
+
8 => :stairs, # W1 Build Stairs
|
|
120
|
+
52 => :exit, # W1 Exit
|
|
121
|
+
82 => :lower_floor, # WR Lower Floor to Lowest
|
|
122
|
+
86 => :door_open_stay, # WR Door Open Stay
|
|
123
|
+
88 => :lift, # WR Lift Lower Wait Raise
|
|
124
|
+
90 => :door, # WR Door Open Wait Close
|
|
125
|
+
91 => :raise_floor, # WR Raise Floor to Lowest Ceiling
|
|
126
|
+
97 => :teleport, # WR Teleport
|
|
127
|
+
98 => :lower_floor, # WR Lower Floor to Highest - 8
|
|
128
|
+
124 => :secret_exit, # W1 Secret Exit
|
|
129
|
+
}.freeze
|
|
130
|
+
|
|
131
|
+
# W1 types that only trigger once
|
|
132
|
+
W1_TYPES = [2, 5, 7, 8, 52, 124].freeze
|
|
133
|
+
|
|
134
|
+
def check_walk_triggers
|
|
135
|
+
@near_linedefs ||= {}
|
|
136
|
+
|
|
137
|
+
@map.linedefs.each_with_index do |ld, idx|
|
|
138
|
+
next if ld.special == 0
|
|
139
|
+
action = WALK_TRIGGERS[ld.special]
|
|
140
|
+
next unless action
|
|
141
|
+
|
|
142
|
+
# W1 types only trigger once
|
|
143
|
+
if W1_TYPES.include?(ld.special)
|
|
144
|
+
next if @crossed_linedefs[idx]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
v1 = @map.vertices[ld.v1]
|
|
148
|
+
v2 = @map.vertices[ld.v2]
|
|
149
|
+
|
|
150
|
+
# Determine which side of the linedef the player is on
|
|
151
|
+
# DOOM's P_CrossSpecialLine fires when the player transitions sides
|
|
152
|
+
side = line_side(@player_x, @player_y, v1.x, v1.y, v2.x, v2.y)
|
|
153
|
+
dist = point_line_dist(@player_x, @player_y, v1.x, v1.y, v2.x, v2.y)
|
|
154
|
+
|
|
155
|
+
near = dist < 32 # Detection range
|
|
156
|
+
prev_side = @near_linedefs[idx]
|
|
157
|
+
|
|
158
|
+
if near && prev_side && prev_side != side
|
|
159
|
+
# Player crossed the line - trigger!
|
|
160
|
+
@near_linedefs[idx] = side
|
|
161
|
+
elsif near && prev_side.nil?
|
|
162
|
+
# First time near - record side but don't trigger yet
|
|
163
|
+
@near_linedefs[idx] = side
|
|
164
|
+
next
|
|
165
|
+
elsif !near
|
|
166
|
+
@near_linedefs[idx] = nil
|
|
167
|
+
next
|
|
168
|
+
else
|
|
169
|
+
next # Same side, no crossing
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
@crossed_linedefs[idx] = true
|
|
173
|
+
|
|
174
|
+
case action
|
|
175
|
+
when :exit
|
|
176
|
+
@exit_triggered = :normal
|
|
177
|
+
when :secret_exit
|
|
178
|
+
@exit_triggered = :secret
|
|
179
|
+
when :door_open_stay
|
|
180
|
+
activate_tagged_door(ld, stay_open: true)
|
|
181
|
+
when :door
|
|
182
|
+
activate_tagged_door(ld)
|
|
183
|
+
when :lift
|
|
184
|
+
activate_lift(ld)
|
|
185
|
+
when :raise_floor
|
|
186
|
+
raise_floor_to_next(ld)
|
|
187
|
+
when :lower_floor
|
|
188
|
+
lower_floor_to_highest(ld)
|
|
189
|
+
when :teleport
|
|
190
|
+
teleport_player(ld)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Returns which side of a line a point is on (:front or :back)
|
|
196
|
+
def line_side(px, py, x1, y1, x2, y2)
|
|
197
|
+
cross = (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1)
|
|
198
|
+
cross >= 0 ? :front : :back
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def point_line_dist(px, py, x1, y1, x2, y2)
|
|
202
|
+
dx = x2 - x1; dy = y2 - y1
|
|
203
|
+
len_sq = dx * dx + dy * dy
|
|
204
|
+
return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2) if len_sq == 0
|
|
205
|
+
t = ((px - x1) * dx + (py - y1) * dy).to_f / len_sq
|
|
206
|
+
t = [[t, 0.0].max, 1.0].min
|
|
207
|
+
cx = x1 + t * dx; cy = y1 + t * dy
|
|
208
|
+
Math.sqrt((px - cx) ** 2 + (py - cy) ** 2)
|
|
209
|
+
end
|
|
210
|
+
|
|
61
211
|
def activate_door(linedef, stay_open: false, key: nil)
|
|
62
212
|
# Find the sector on the back side of the linedef
|
|
63
213
|
return unless linedef.two_sided?
|
|
@@ -92,6 +242,7 @@ module Doom
|
|
|
92
242
|
wait_tics: 0,
|
|
93
243
|
stay_open: stay_open
|
|
94
244
|
}
|
|
245
|
+
@sound&.door_open
|
|
95
246
|
end
|
|
96
247
|
|
|
97
248
|
def update_doors
|
|
@@ -113,6 +264,7 @@ module Doom
|
|
|
113
264
|
door[:wait_tics] -= 1
|
|
114
265
|
if door[:wait_tics] <= 0
|
|
115
266
|
door[:state] = DOOR_CLOSING
|
|
267
|
+
@sound&.door_close
|
|
116
268
|
end
|
|
117
269
|
|
|
118
270
|
when DOOR_CLOSING
|
|
@@ -157,6 +309,215 @@ module Doom
|
|
|
157
309
|
|
|
158
310
|
lowest == Float::INFINITY ? 128 : lowest
|
|
159
311
|
end
|
|
312
|
+
|
|
313
|
+
# Door activated by tag (for S1/W1/WR tagged doors)
|
|
314
|
+
def activate_tagged_door(linedef, stay_open: false)
|
|
315
|
+
tag = linedef.tag
|
|
316
|
+
return if tag == 0
|
|
317
|
+
|
|
318
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
319
|
+
next unless sector_has_tag?(idx, tag)
|
|
320
|
+
next if @active_doors[idx]
|
|
321
|
+
|
|
322
|
+
target = find_lowest_ceiling_around(idx) - 4
|
|
323
|
+
@active_doors[idx] = {
|
|
324
|
+
sector: sector,
|
|
325
|
+
state: DOOR_OPENING,
|
|
326
|
+
target_height: target,
|
|
327
|
+
original_height: sector.ceiling_height,
|
|
328
|
+
wait_tics: 0,
|
|
329
|
+
stay_open: stay_open,
|
|
330
|
+
}
|
|
331
|
+
end
|
|
332
|
+
@sound&.door_open
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Lift: lower floor to lowest adjacent, wait, raise back
|
|
336
|
+
def activate_lift(linedef)
|
|
337
|
+
tag = linedef.tag
|
|
338
|
+
return if tag == 0
|
|
339
|
+
|
|
340
|
+
activated = false
|
|
341
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
342
|
+
next unless sector_has_tag?(idx, tag)
|
|
343
|
+
next if @active_lifts[idx] # Already moving
|
|
344
|
+
|
|
345
|
+
lowest = find_lowest_floor_around(idx)
|
|
346
|
+
@active_lifts[idx] = {
|
|
347
|
+
sector: sector,
|
|
348
|
+
state: :lowering,
|
|
349
|
+
target_low: lowest,
|
|
350
|
+
original_height: sector.floor_height,
|
|
351
|
+
wait_tics: 0,
|
|
352
|
+
}
|
|
353
|
+
activated = true
|
|
354
|
+
end
|
|
355
|
+
@sound&.platform_start if activated
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def update_lifts
|
|
359
|
+
@active_lifts.each do |idx, lift|
|
|
360
|
+
case lift[:state]
|
|
361
|
+
when :lowering
|
|
362
|
+
lift[:sector].floor_height -= LIFT_SPEED
|
|
363
|
+
if lift[:sector].floor_height <= lift[:target_low]
|
|
364
|
+
lift[:sector].floor_height = lift[:target_low]
|
|
365
|
+
lift[:state] = :waiting
|
|
366
|
+
lift[:wait_tics] = LIFT_WAIT
|
|
367
|
+
@sound&.platform_stop
|
|
368
|
+
end
|
|
369
|
+
when :waiting
|
|
370
|
+
lift[:wait_tics] -= 1
|
|
371
|
+
if lift[:wait_tics] <= 0
|
|
372
|
+
lift[:state] = :raising
|
|
373
|
+
@sound&.platform_start
|
|
374
|
+
end
|
|
375
|
+
when :raising
|
|
376
|
+
lift[:sector].floor_height += LIFT_SPEED
|
|
377
|
+
if lift[:sector].floor_height >= lift[:original_height]
|
|
378
|
+
lift[:sector].floor_height = lift[:original_height]
|
|
379
|
+
@active_lifts.delete(idx)
|
|
380
|
+
@sound&.platform_stop
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def raise_floor_to_next(linedef)
|
|
387
|
+
tag = linedef.tag
|
|
388
|
+
return if tag == 0
|
|
389
|
+
|
|
390
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
391
|
+
next unless sector_has_tag?(idx, tag)
|
|
392
|
+
target = find_next_higher_floor(idx)
|
|
393
|
+
next if target <= sector.floor_height
|
|
394
|
+
|
|
395
|
+
@active_lifts[idx] = {
|
|
396
|
+
sector: sector,
|
|
397
|
+
state: :raising,
|
|
398
|
+
target_low: sector.floor_height,
|
|
399
|
+
original_height: target,
|
|
400
|
+
wait_tics: 0,
|
|
401
|
+
}
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def lower_floor_to_lowest(linedef)
|
|
406
|
+
tag = linedef.tag
|
|
407
|
+
return if tag == 0
|
|
408
|
+
|
|
409
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
410
|
+
next unless sector_has_tag?(idx, tag)
|
|
411
|
+
target = find_lowest_floor_around(idx)
|
|
412
|
+
sector.floor_height = target
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def lower_floor_to_highest(linedef)
|
|
417
|
+
tag = linedef.tag
|
|
418
|
+
return if tag == 0
|
|
419
|
+
|
|
420
|
+
@map.sectors.each_with_index do |sector, idx|
|
|
421
|
+
next unless sector_has_tag?(idx, tag)
|
|
422
|
+
target = find_highest_floor_around(idx) - 8
|
|
423
|
+
sector.floor_height = target if target < sector.floor_height
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def teleport_player(linedef)
|
|
428
|
+
tag = linedef.tag
|
|
429
|
+
return if tag == 0
|
|
430
|
+
|
|
431
|
+
# Find teleport destination thing (type 14) in tagged sector
|
|
432
|
+
@map.things.each do |thing|
|
|
433
|
+
next unless thing.type == 14 # Teleport destination
|
|
434
|
+
sector = @map.sector_at(thing.x, thing.y)
|
|
435
|
+
next unless sector
|
|
436
|
+
sector_idx = @map.sectors.index(sector)
|
|
437
|
+
next unless sector_has_tag?(sector_idx, tag)
|
|
438
|
+
|
|
439
|
+
@teleport_dest = { x: thing.x, y: thing.y, angle: thing.angle }
|
|
440
|
+
return
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def check_secrets
|
|
446
|
+
# Build set of secret sector indices on first call
|
|
447
|
+
@secret_sectors ||= Set.new(
|
|
448
|
+
@map.sectors.each_with_index.filter_map { |s, i| i if s.special == 9 }
|
|
449
|
+
)
|
|
450
|
+
return if @secret_sectors.empty?
|
|
451
|
+
|
|
452
|
+
# Find which sector the player is in via BSP subsector lookup
|
|
453
|
+
subsector = @map.subsector_at(@player_x, @player_y)
|
|
454
|
+
return unless subsector
|
|
455
|
+
|
|
456
|
+
seg = @map.segs[subsector.first_seg]
|
|
457
|
+
return unless seg
|
|
458
|
+
|
|
459
|
+
ld = @map.linedefs[seg.linedef]
|
|
460
|
+
return unless ld
|
|
461
|
+
|
|
462
|
+
sd_idx = seg.direction == 0 ? ld.sidedef_right : ld.sidedef_left
|
|
463
|
+
return if sd_idx == 0xFFFF
|
|
464
|
+
|
|
465
|
+
sector_idx = @map.sidedefs[sd_idx].sector
|
|
466
|
+
return if @secrets_found[sector_idx]
|
|
467
|
+
|
|
468
|
+
if @secret_sectors.include?(sector_idx)
|
|
469
|
+
@secrets_found[sector_idx] = true
|
|
470
|
+
# Clear the special so it doesn't retrigger (matching Chocolate Doom)
|
|
471
|
+
@map.sectors[sector_idx].special = 0
|
|
472
|
+
@secret_sectors.delete(sector_idx)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def sector_has_tag?(sector_idx, tag)
|
|
477
|
+
@map.sectors[sector_idx].tag == tag
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def find_lowest_floor_around(sector_idx)
|
|
481
|
+
lowest = @map.sectors[sector_idx].floor_height
|
|
482
|
+
each_adjacent_sector(sector_idx) do |adj|
|
|
483
|
+
lowest = adj.floor_height if adj.floor_height < lowest
|
|
484
|
+
end
|
|
485
|
+
lowest
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def find_highest_floor_around(sector_idx)
|
|
489
|
+
highest = -32768
|
|
490
|
+
each_adjacent_sector(sector_idx) do |adj|
|
|
491
|
+
highest = adj.floor_height if adj.floor_height > highest
|
|
492
|
+
end
|
|
493
|
+
highest == -32768 ? @map.sectors[sector_idx].floor_height : highest
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def find_next_higher_floor(sector_idx)
|
|
497
|
+
current = @map.sectors[sector_idx].floor_height
|
|
498
|
+
best = Float::INFINITY
|
|
499
|
+
each_adjacent_sector(sector_idx) do |adj|
|
|
500
|
+
if adj.floor_height > current && adj.floor_height < best
|
|
501
|
+
best = adj.floor_height
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
best == Float::INFINITY ? current : best
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def each_adjacent_sector(sector_idx)
|
|
508
|
+
@map.linedefs.each do |ld|
|
|
509
|
+
next unless ld.two_sided?
|
|
510
|
+
right = @map.sidedefs[ld.sidedef_right]
|
|
511
|
+
left = @map.sidedefs[ld.sidedef_left] if ld.sidedef_left != 0xFFFF
|
|
512
|
+
next unless left
|
|
513
|
+
|
|
514
|
+
if right.sector == sector_idx
|
|
515
|
+
yield @map.sectors[left.sector]
|
|
516
|
+
elsif left.sector == sector_idx
|
|
517
|
+
yield @map.sectors[right.sector]
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|
|
160
521
|
end
|
|
161
522
|
end
|
|
162
523
|
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Plays sound effects for game events.
|
|
6
|
+
class SoundEngine
|
|
7
|
+
# Weapon fire sounds
|
|
8
|
+
WEAPON_SOUNDS = {
|
|
9
|
+
PlayerState::WEAPON_FIST => 'DSPUNCH',
|
|
10
|
+
PlayerState::WEAPON_PISTOL => 'DSPISTOL',
|
|
11
|
+
PlayerState::WEAPON_SHOTGUN => 'DSSHOTGN',
|
|
12
|
+
PlayerState::WEAPON_CHAINGUN => 'DSPISTOL',
|
|
13
|
+
PlayerState::WEAPON_ROCKET => 'DSRLAUNC',
|
|
14
|
+
PlayerState::WEAPON_PLASMA => 'DSPLASMA',
|
|
15
|
+
PlayerState::WEAPON_BFG => 'DSBFG',
|
|
16
|
+
PlayerState::WEAPON_CHAINSAW => 'DSSAWIDL',
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# Monster see/activation sounds (from mobjinfo seesound)
|
|
20
|
+
MONSTER_SEE = {
|
|
21
|
+
3004 => 'DSPOSIT1', # Zombieman
|
|
22
|
+
9 => 'DSSGTSIT', # Shotgun Guy
|
|
23
|
+
3001 => 'DSBGSIT1', # Imp
|
|
24
|
+
3002 => 'DSSGTSIT', # Demon
|
|
25
|
+
58 => 'DSSGTSIT', # Spectre
|
|
26
|
+
3003 => 'DSBRSSIT', # Baron
|
|
27
|
+
69 => 'DSBRSSIT', # Hell Knight
|
|
28
|
+
3005 => 'DSCACSIT', # Cacodemon
|
|
29
|
+
3006 => 'DSSKLATK', # Lost Soul
|
|
30
|
+
65 => 'DSPOSIT2', # Heavy Weapon Dude
|
|
31
|
+
16 => 'DSCYBSIT', # Cyberdemon
|
|
32
|
+
7 => 'DSSPISIT', # Spider Mastermind
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Monster death sounds (from mobjinfo deathsound)
|
|
36
|
+
MONSTER_DEATH = {
|
|
37
|
+
3004 => 'DSPODTH1', # Zombieman
|
|
38
|
+
9 => 'DSSGTDTH', # Shotgun Guy
|
|
39
|
+
3001 => 'DSBGDTH1', # Imp
|
|
40
|
+
3002 => 'DSSGTDTH', # Demon
|
|
41
|
+
58 => 'DSSGTDTH', # Spectre
|
|
42
|
+
3003 => 'DSBRSDTH', # Baron
|
|
43
|
+
69 => 'DSBRSDTH', # Hell Knight
|
|
44
|
+
3005 => 'DSCACDTH', # Cacodemon
|
|
45
|
+
3006 => 'DSFIRXPL', # Lost Soul
|
|
46
|
+
65 => 'DSPODTH2', # Heavy Weapon Dude
|
|
47
|
+
16 => 'DSCYBDTH', # Cyberdemon
|
|
48
|
+
7 => 'DSSPIDTH', # Spider Mastermind
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Monster pain sounds (from mobjinfo painsound)
|
|
52
|
+
MONSTER_PAIN = {
|
|
53
|
+
3004 => 'DSPOPAIN', # Zombieman
|
|
54
|
+
9 => 'DSPOPAIN', # Shotgun Guy
|
|
55
|
+
3001 => 'DSDMPAIN', # Imp
|
|
56
|
+
3002 => 'DSDMPAIN', # Demon
|
|
57
|
+
58 => 'DSDMPAIN', # Spectre
|
|
58
|
+
3003 => 'DSDMPAIN', # Baron
|
|
59
|
+
69 => 'DSDMPAIN', # Hell Knight
|
|
60
|
+
3005 => 'DSDMPAIN', # Cacodemon
|
|
61
|
+
3006 => 'DSDMPAIN', # Lost Soul
|
|
62
|
+
65 => 'DSPOPAIN', # Heavy Weapon Dude
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# Monster attack sounds (from mobjinfo attacksound)
|
|
66
|
+
MONSTER_ATTACK = {
|
|
67
|
+
3004 => 'DSPISTOL', # Zombieman: pistol
|
|
68
|
+
9 => 'DSSHOTGN', # Shotgun Guy: shotgun
|
|
69
|
+
3001 => 'DSFIRSHT', # Imp: fireball launch
|
|
70
|
+
3002 => 'DSSGTATK', # Demon: bite
|
|
71
|
+
58 => 'DSSGTATK', # Spectre: bite
|
|
72
|
+
3003 => 'DSFIRSHT', # Baron: fireball
|
|
73
|
+
69 => 'DSFIRSHT', # Hell Knight: fireball
|
|
74
|
+
3005 => 'DSFIRSHT', # Cacodemon: fireball
|
|
75
|
+
65 => 'DSSHOTGN', # Heavy Weapon Dude: chaingun burst
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
def initialize(sound_manager)
|
|
79
|
+
@sounds = sound_manager
|
|
80
|
+
@last_played = {} # Throttle rapid repeats
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def play(name, volume: 1.0, throttle: 0)
|
|
84
|
+
now = Time.now.to_f
|
|
85
|
+
if throttle > 0
|
|
86
|
+
return if @last_played[name] && (now - @last_played[name]) < throttle
|
|
87
|
+
end
|
|
88
|
+
sample = @sounds[name]
|
|
89
|
+
sample&.play(volume)
|
|
90
|
+
@last_played[name] = now
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# --- Menu sounds ---
|
|
94
|
+
def menu_move
|
|
95
|
+
play('DSPSTOP', throttle: 0.05)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def menu_select
|
|
99
|
+
play('DSPISTOL')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def menu_back
|
|
103
|
+
play('DSSWTCHX')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# --- Weapon events ---
|
|
107
|
+
def weapon_fire(weapon)
|
|
108
|
+
sound = WEAPON_SOUNDS[weapon]
|
|
109
|
+
play(sound, throttle: 0.05) if sound
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def shotgun_cock
|
|
113
|
+
play('DSSGCOCK', throttle: 0.3)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def chainsaw_hit
|
|
117
|
+
play('DSSAWFUL', throttle: 0.1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# --- Player events ---
|
|
121
|
+
def player_pain
|
|
122
|
+
play('DSPLPAIN', throttle: 0.3)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def player_death
|
|
126
|
+
play('DSPLDETH')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def item_pickup
|
|
130
|
+
play('DSITEMUP', throttle: 0.1)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def weapon_pickup
|
|
134
|
+
play('DSWPNUP')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def oof
|
|
138
|
+
play('DSOOF', throttle: 0.3)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def noway
|
|
142
|
+
play('DSNOWAY', throttle: 0.3)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# --- Door/environment sounds ---
|
|
146
|
+
def door_open
|
|
147
|
+
play('DSDOROPN', throttle: 0.2)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def door_close
|
|
151
|
+
play('DSDORCLS', throttle: 0.2)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def switch_activate
|
|
155
|
+
play('DSSWTCHN')
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def platform_start
|
|
159
|
+
play('DSPSTART', throttle: 0.2)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def platform_stop
|
|
163
|
+
play('DSPSTOP', throttle: 0.2)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# --- Monster sounds ---
|
|
167
|
+
def monster_see(type)
|
|
168
|
+
sound = MONSTER_SEE[type]
|
|
169
|
+
play(sound, throttle: 0.5) if sound
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def monster_death(type)
|
|
173
|
+
sound = MONSTER_DEATH[type]
|
|
174
|
+
play(sound) if sound
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def monster_pain(type)
|
|
178
|
+
sound = MONSTER_PAIN[type]
|
|
179
|
+
play(sound, throttle: 0.2) if sound
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def monster_attack(type)
|
|
183
|
+
sound = MONSTER_ATTACK[type]
|
|
184
|
+
play(sound, throttle: 0.1) if sound
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# --- Explosions / impacts ---
|
|
188
|
+
def explosion
|
|
189
|
+
play('DSBAREXP')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def rocket_explode
|
|
193
|
+
play('DSRXPLOD')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def fireball_hit
|
|
197
|
+
play('DSFIRXPL', throttle: 0.1)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|