doom 0.3.0 → 0.5.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/README.md +1 -1
- data/lib/doom/game/animations.rb +97 -0
- data/lib/doom/game/combat.rb +244 -0
- data/lib/doom/game/item_pickup.rb +170 -0
- data/lib/doom/game/player_state.rb +313 -0
- data/lib/doom/game/sector_actions.rb +162 -0
- data/lib/doom/game/sector_effects.rb +179 -0
- data/lib/doom/platform/gosu_window.rb +706 -59
- data/lib/doom/render/renderer.rb +397 -136
- data/lib/doom/render/status_bar.rb +218 -0
- data/lib/doom/render/weapon_renderer.rb +99 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/colormap.rb +0 -6
- data/lib/doom/wad/flat.rb +0 -21
- data/lib/doom/wad/hud_graphics.rb +257 -0
- data/lib/doom/wad/sprite.rb +95 -22
- data/lib/doom/wad/texture.rb +23 -22
- data/lib/doom/wad_downloader.rb +2 -2
- data/lib/doom.rb +27 -2
- metadata +12 -6
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Tracks player state for HUD display and weapon rendering
|
|
6
|
+
class PlayerState
|
|
7
|
+
# Weapons
|
|
8
|
+
WEAPON_FIST = 0
|
|
9
|
+
WEAPON_PISTOL = 1
|
|
10
|
+
WEAPON_SHOTGUN = 2
|
|
11
|
+
WEAPON_CHAINGUN = 3
|
|
12
|
+
WEAPON_ROCKET = 4
|
|
13
|
+
WEAPON_PLASMA = 5
|
|
14
|
+
WEAPON_BFG = 6
|
|
15
|
+
WEAPON_CHAINSAW = 7
|
|
16
|
+
|
|
17
|
+
# Weapon symbols for graphics lookup
|
|
18
|
+
WEAPON_NAMES = {
|
|
19
|
+
WEAPON_FIST => :fist,
|
|
20
|
+
WEAPON_PISTOL => :pistol,
|
|
21
|
+
WEAPON_SHOTGUN => :shotgun,
|
|
22
|
+
WEAPON_CHAINGUN => :chaingun,
|
|
23
|
+
WEAPON_ROCKET => :rocket,
|
|
24
|
+
WEAPON_PLASMA => :plasma,
|
|
25
|
+
WEAPON_BFG => :bfg,
|
|
26
|
+
WEAPON_CHAINSAW => :chainsaw
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Attack durations in tics (at 35fps), matching DOOM's weapon state sequences
|
|
30
|
+
ATTACK_DURATIONS = {
|
|
31
|
+
WEAPON_FIST => 14, # Punch windup + swing
|
|
32
|
+
WEAPON_PISTOL => 16, # S_PISTOL: 6+4+5+1 tics
|
|
33
|
+
WEAPON_SHOTGUN => 40, # Pump action cycle
|
|
34
|
+
WEAPON_CHAINGUN => 8, # Rapid fire (2 shots per cycle)
|
|
35
|
+
WEAPON_ROCKET => 20, # Rocket launch + recovery
|
|
36
|
+
WEAPON_PLASMA => 8, # Fast energy weapon
|
|
37
|
+
WEAPON_BFG => 60, # Long charge + fire
|
|
38
|
+
WEAPON_CHAINSAW => 6 # Fast melee
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
attr_accessor :health, :armor, :max_health, :max_armor
|
|
42
|
+
attr_accessor :ammo_bullets, :ammo_shells, :ammo_rockets, :ammo_cells
|
|
43
|
+
attr_accessor :max_bullets, :max_shells, :max_rockets, :max_cells
|
|
44
|
+
attr_accessor :weapon, :has_weapons
|
|
45
|
+
attr_accessor :keys
|
|
46
|
+
attr_accessor :attacking, :attack_frame, :attack_tics
|
|
47
|
+
attr_accessor :bob_angle, :bob_amount
|
|
48
|
+
attr_accessor :is_moving
|
|
49
|
+
|
|
50
|
+
# Smooth step-up/down (matching Chocolate Doom's P_CalcHeight / P_ZMovement)
|
|
51
|
+
VIEWHEIGHT = 41.0
|
|
52
|
+
VIEWHEIGHT_HALF = VIEWHEIGHT / 2.0
|
|
53
|
+
DELTA_ACCEL = 0.25 # deltaviewheight += FRACUNIT/4 per tic
|
|
54
|
+
attr_reader :viewheight, :deltaviewheight
|
|
55
|
+
|
|
56
|
+
# View bob (camera bounce when walking, matching Chocolate Doom's
|
|
57
|
+
# P_CalcHeight + P_XYMovement + P_Thrust from p_user.c / p_mobj.c)
|
|
58
|
+
MAXBOB = 16.0 # Maximum bob amplitude (0x100000 in fixed-point = 16 map units)
|
|
59
|
+
STOPSPEED = 0.0625 # Snap-to-zero threshold (0x1000 in fixed-point)
|
|
60
|
+
# Continuous-time equivalents of DOOM's per-tic constants (35 fps tic rate):
|
|
61
|
+
# FRICTION = 0xE800/0x10000 = 0.90625 per tic
|
|
62
|
+
# decay_rate = -ln(0.90625) * 35 = 3.44/sec
|
|
63
|
+
# walk thrust = forwardmove(25) * 2048 / 65536 = 0.78 map units/tic = 27.3/sec
|
|
64
|
+
# terminal velocity = 27.3 / 3.44 = 7.56 -> bob = 7.56^2/4 = 14.3 (89% of MAXBOB)
|
|
65
|
+
BOB_DECAY_RATE = 3.44 # Friction as continuous decay rate (1/sec)
|
|
66
|
+
BOB_THRUST = 26.0 # Walk thrust (map units/sec), gives terminal ~7.5
|
|
67
|
+
BOB_FREQUENCY = 11.0 # Bob cycle frequency (rad/sec): FINEANGLES/20 * 35 / 8192 * 2*PI
|
|
68
|
+
attr_reader :view_bob_offset
|
|
69
|
+
|
|
70
|
+
def initialize
|
|
71
|
+
reset
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reset
|
|
75
|
+
@health = 100
|
|
76
|
+
@armor = 0
|
|
77
|
+
@max_health = 100
|
|
78
|
+
@max_armor = 200
|
|
79
|
+
|
|
80
|
+
# Ammo
|
|
81
|
+
@ammo_bullets = 50
|
|
82
|
+
@ammo_shells = 0
|
|
83
|
+
@ammo_rockets = 0
|
|
84
|
+
@ammo_cells = 0
|
|
85
|
+
|
|
86
|
+
@max_bullets = 200
|
|
87
|
+
@max_shells = 50
|
|
88
|
+
@max_rockets = 50
|
|
89
|
+
@max_cells = 300
|
|
90
|
+
|
|
91
|
+
# Start with fist and pistol
|
|
92
|
+
@weapon = WEAPON_PISTOL
|
|
93
|
+
@has_weapons = [true, true, false, false, false, false, false, false]
|
|
94
|
+
|
|
95
|
+
# No keys
|
|
96
|
+
@keys = {
|
|
97
|
+
blue_card: false,
|
|
98
|
+
yellow_card: false,
|
|
99
|
+
red_card: false,
|
|
100
|
+
blue_skull: false,
|
|
101
|
+
yellow_skull: false,
|
|
102
|
+
red_skull: false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Attack state
|
|
106
|
+
@attacking = false
|
|
107
|
+
@attack_frame = 0
|
|
108
|
+
@attack_tics = 0
|
|
109
|
+
|
|
110
|
+
# Weapon bob
|
|
111
|
+
@bob_angle = 0.0
|
|
112
|
+
@bob_amount = 0.0
|
|
113
|
+
@is_moving = false
|
|
114
|
+
|
|
115
|
+
# Smooth step height (P_CalcHeight viewheight/deltaviewheight)
|
|
116
|
+
@viewheight = VIEWHEIGHT
|
|
117
|
+
@deltaviewheight = 0.0
|
|
118
|
+
|
|
119
|
+
# View bob (camera bounce) - simulated momentum for P_CalcHeight
|
|
120
|
+
@view_bob_offset = 0.0
|
|
121
|
+
@momx = 0.0 # Simulated X momentum (map units/sec, not actual movement)
|
|
122
|
+
@momy = 0.0 # Simulated Y momentum
|
|
123
|
+
@thrust_x = 0.0 # Per-frame thrust input (raw, before normalization)
|
|
124
|
+
@thrust_y = 0.0
|
|
125
|
+
@view_bob_angle = 0.0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def weapon_name
|
|
129
|
+
WEAPON_NAMES[@weapon]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def current_ammo
|
|
133
|
+
case @weapon
|
|
134
|
+
when WEAPON_PISTOL, WEAPON_CHAINGUN
|
|
135
|
+
@ammo_bullets
|
|
136
|
+
when WEAPON_SHOTGUN
|
|
137
|
+
@ammo_shells
|
|
138
|
+
when WEAPON_ROCKET
|
|
139
|
+
@ammo_rockets
|
|
140
|
+
when WEAPON_PLASMA, WEAPON_BFG
|
|
141
|
+
@ammo_cells
|
|
142
|
+
else
|
|
143
|
+
nil # Fist/chainsaw don't use ammo
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def max_ammo_for_weapon
|
|
148
|
+
case @weapon
|
|
149
|
+
when WEAPON_PISTOL, WEAPON_CHAINGUN
|
|
150
|
+
@max_bullets
|
|
151
|
+
when WEAPON_SHOTGUN
|
|
152
|
+
@max_shells
|
|
153
|
+
when WEAPON_ROCKET
|
|
154
|
+
@max_rockets
|
|
155
|
+
when WEAPON_PLASMA, WEAPON_BFG
|
|
156
|
+
@max_cells
|
|
157
|
+
else
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def can_attack?
|
|
163
|
+
return true if @weapon == WEAPON_FIST || @weapon == WEAPON_CHAINSAW
|
|
164
|
+
|
|
165
|
+
ammo = current_ammo
|
|
166
|
+
ammo && ammo > 0
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def start_attack
|
|
170
|
+
return unless can_attack?
|
|
171
|
+
return if @attacking
|
|
172
|
+
|
|
173
|
+
@attacking = true
|
|
174
|
+
@attack_frame = 0
|
|
175
|
+
@attack_tics = 0
|
|
176
|
+
|
|
177
|
+
# Consume ammo
|
|
178
|
+
case @weapon
|
|
179
|
+
when WEAPON_PISTOL
|
|
180
|
+
@ammo_bullets -= 1 if @ammo_bullets > 0
|
|
181
|
+
when WEAPON_SHOTGUN
|
|
182
|
+
@ammo_shells -= 1 if @ammo_shells > 0
|
|
183
|
+
when WEAPON_CHAINGUN
|
|
184
|
+
@ammo_bullets -= 1 if @ammo_bullets > 0
|
|
185
|
+
when WEAPON_ROCKET
|
|
186
|
+
@ammo_rockets -= 1 if @ammo_rockets > 0
|
|
187
|
+
when WEAPON_PLASMA
|
|
188
|
+
@ammo_cells -= 1 if @ammo_cells > 0
|
|
189
|
+
when WEAPON_BFG
|
|
190
|
+
@ammo_cells -= 40 if @ammo_cells >= 40
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def update_attack
|
|
195
|
+
return unless @attacking
|
|
196
|
+
|
|
197
|
+
@attack_tics += 1
|
|
198
|
+
|
|
199
|
+
# Calculate which frame we're on based on tics
|
|
200
|
+
duration = ATTACK_DURATIONS[@weapon] || 8
|
|
201
|
+
frame_count = @weapon == WEAPON_FIST ? 3 : 4
|
|
202
|
+
|
|
203
|
+
tics_per_frame = duration / frame_count
|
|
204
|
+
@attack_frame = (@attack_tics / tics_per_frame).to_i
|
|
205
|
+
|
|
206
|
+
# Attack finished?
|
|
207
|
+
if @attack_tics >= duration
|
|
208
|
+
@attacking = false
|
|
209
|
+
@attack_frame = 0
|
|
210
|
+
@attack_tics = 0
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def update_bob(delta_time)
|
|
215
|
+
if @is_moving
|
|
216
|
+
# Increase bob while moving
|
|
217
|
+
@bob_angle += delta_time * 10.0
|
|
218
|
+
@bob_amount = [@bob_amount + delta_time * 16.0, 6.0].min
|
|
219
|
+
else
|
|
220
|
+
# Decay bob when stopped
|
|
221
|
+
@bob_amount = [@bob_amount - delta_time * 12.0, 0.0].max
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Called when player moves onto a different floor height.
|
|
226
|
+
# Matches Chocolate Doom P_ZMovement: reduce viewheight by the step amount
|
|
227
|
+
# so the camera doesn't snap, then let P_CalcHeight recover it smoothly.
|
|
228
|
+
def notify_step(step_amount)
|
|
229
|
+
return if step_amount == 0
|
|
230
|
+
@viewheight -= step_amount
|
|
231
|
+
@deltaviewheight = (VIEWHEIGHT - @viewheight) / 8.0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Gradually restore viewheight to VIEWHEIGHT (called each tic).
|
|
235
|
+
# Matches Chocolate Doom P_CalcHeight viewheight recovery loop.
|
|
236
|
+
# For step-up: viewheight < 41, delta > 0, accelerates upward.
|
|
237
|
+
# For step-down: viewheight > 41, delta < 0, decelerates then recovers.
|
|
238
|
+
def update_viewheight
|
|
239
|
+
@viewheight += @deltaviewheight
|
|
240
|
+
|
|
241
|
+
if @viewheight > VIEWHEIGHT && @deltaviewheight >= 0
|
|
242
|
+
@viewheight = VIEWHEIGHT
|
|
243
|
+
@deltaviewheight = 0.0
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
if @viewheight < VIEWHEIGHT_HALF
|
|
247
|
+
@viewheight = VIEWHEIGHT_HALF
|
|
248
|
+
@deltaviewheight = 1.0 if @deltaviewheight <= 0
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if @deltaviewheight != 0
|
|
252
|
+
@deltaviewheight += DELTA_ACCEL
|
|
253
|
+
@deltaviewheight = 0.0 if @deltaviewheight.abs < 0.01 && (@viewheight - VIEWHEIGHT).abs < 0.5
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Set movement momentum directly (called from GosuWindow with actual
|
|
258
|
+
# movement momentum, which already has thrust + friction applied).
|
|
259
|
+
def set_movement_momentum(momx, momy)
|
|
260
|
+
@momx = momx
|
|
261
|
+
@momy = momy
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Compute view bob from actual movement momentum.
|
|
265
|
+
# Matches Chocolate Doom P_CalcHeight:
|
|
266
|
+
# bob = (momx*momx + momy*momy) >> 2, capped at MAXBOB
|
|
267
|
+
# viewz += finesine[angle] * bob/2
|
|
268
|
+
# Momentum is in units/sec; DOOM's bob uses units/tic (divide by 35).
|
|
269
|
+
BOB_MOM_SCALE = 1.0 / (35.0 * 35.0 * 4.0) # (mom/35)^2 / 4
|
|
270
|
+
|
|
271
|
+
def update_view_bob(delta_time)
|
|
272
|
+
dt = delta_time.clamp(0.001, 0.05)
|
|
273
|
+
|
|
274
|
+
# P_CalcHeight: bob = (momx_per_tic^2 + momy_per_tic^2) / 4, capped at MAXBOB
|
|
275
|
+
bob = (@momx * @momx + @momy * @momy) * BOB_MOM_SCALE
|
|
276
|
+
bob = MAXBOB if bob > MAXBOB
|
|
277
|
+
|
|
278
|
+
# Advance bob sine wave (FINEANGLES/20 per tic = ~11 rad/sec)
|
|
279
|
+
@view_bob_angle += BOB_FREQUENCY * dt
|
|
280
|
+
|
|
281
|
+
# viewz offset: sin(angle) * bob/2
|
|
282
|
+
@view_bob_offset = Math.sin(@view_bob_angle) * bob / 2.0
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def weapon_bob_x
|
|
286
|
+
Math.cos(@bob_angle) * @bob_amount
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def weapon_bob_y
|
|
290
|
+
Math.sin(@bob_angle * 2) * @bob_amount * 0.5
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def health_level
|
|
294
|
+
# 0 = dying, 4 = full health
|
|
295
|
+
case @health
|
|
296
|
+
when 80..200 then 4
|
|
297
|
+
when 60..79 then 3
|
|
298
|
+
when 40..59 then 2
|
|
299
|
+
when 20..39 then 1
|
|
300
|
+
else 0
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def switch_weapon(weapon_num)
|
|
305
|
+
return unless weapon_num >= 0 && weapon_num < 8
|
|
306
|
+
return unless @has_weapons[weapon_num]
|
|
307
|
+
return if @attacking
|
|
308
|
+
|
|
309
|
+
@weapon = weapon_num
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
@@ -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,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Sector light specials and scrolling walls, matching Chocolate Doom's
|
|
6
|
+
# P_SpawnSpecials (p_spec.c) and p_lights.c.
|
|
7
|
+
class SectorEffects
|
|
8
|
+
GLOWSPEED = 8 # Light units per tic for glow
|
|
9
|
+
STROBEBRIGHT = 5 # Bright duration for strobes (tics)
|
|
10
|
+
FASTDARK = 15 # Dark duration for fast strobe (tics)
|
|
11
|
+
SLOWDARK = 35 # Dark duration for slow strobe (tics)
|
|
12
|
+
|
|
13
|
+
def initialize(map)
|
|
14
|
+
@map = map
|
|
15
|
+
@effects = []
|
|
16
|
+
@scroll_sides = []
|
|
17
|
+
spawn_specials
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Called every game tic (35/sec)
|
|
21
|
+
def update
|
|
22
|
+
@effects.each(&:update)
|
|
23
|
+
@scroll_sides.each { |side| side.x_offset += 1 }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def spawn_specials
|
|
29
|
+
@map.sectors.each do |sector|
|
|
30
|
+
case sector.special
|
|
31
|
+
when 1 # Flickering lights
|
|
32
|
+
@effects << LightFlash.new(sector, find_min_light(sector))
|
|
33
|
+
when 2 # Fast strobe
|
|
34
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, false)
|
|
35
|
+
when 3 # Slow strobe
|
|
36
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), SLOWDARK, false)
|
|
37
|
+
when 4 # Fast strobe + 20% damage
|
|
38
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, false)
|
|
39
|
+
when 8 # Glowing light
|
|
40
|
+
@effects << Glow.new(sector, find_min_light(sector))
|
|
41
|
+
when 12 # Sync strobe slow
|
|
42
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), SLOWDARK, true)
|
|
43
|
+
when 13 # Sync strobe fast
|
|
44
|
+
@effects << StrobeFlash.new(sector, find_min_light(sector), FASTDARK, true)
|
|
45
|
+
when 17 # Fire flicker
|
|
46
|
+
@effects << FireFlicker.new(sector, find_min_light(sector))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Linedef type 48: scrolling wall (front side scrolls +1 unit/tic)
|
|
51
|
+
@map.linedefs.each do |linedef|
|
|
52
|
+
next unless linedef.special == 48
|
|
53
|
+
side = @map.sidedefs[linedef.sidedef_right]
|
|
54
|
+
@scroll_sides << side if side
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# P_FindMinSurroundingLight: find lowest light level among adjacent sectors
|
|
59
|
+
def find_min_light(sector)
|
|
60
|
+
min = sector.light_level
|
|
61
|
+
sector_idx = @map.sectors.index(sector)
|
|
62
|
+
return min unless sector_idx
|
|
63
|
+
|
|
64
|
+
@map.linedefs.each do |ld|
|
|
65
|
+
right = @map.sidedefs[ld.sidedef_right]
|
|
66
|
+
next unless right
|
|
67
|
+
left_idx = ld.sidedef_left
|
|
68
|
+
next if left_idx >= 0xFFFF
|
|
69
|
+
left = @map.sidedefs[left_idx]
|
|
70
|
+
next unless left
|
|
71
|
+
|
|
72
|
+
if right.sector == sector_idx && left.sector != sector_idx
|
|
73
|
+
other_light = @map.sectors[left.sector].light_level
|
|
74
|
+
min = other_light if other_light < min
|
|
75
|
+
elsif left.sector == sector_idx && right.sector != sector_idx
|
|
76
|
+
other_light = @map.sectors[right.sector].light_level
|
|
77
|
+
min = other_light if other_light < min
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
min
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# T_LightFlash (type 1): mostly bright with brief random dark flickers
|
|
84
|
+
class LightFlash
|
|
85
|
+
def initialize(sector, minlight)
|
|
86
|
+
@sector = sector
|
|
87
|
+
@maxlight = sector.light_level
|
|
88
|
+
@minlight = minlight
|
|
89
|
+
@count = (rand(65)) + 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def update
|
|
93
|
+
@count -= 1
|
|
94
|
+
return if @count > 0
|
|
95
|
+
|
|
96
|
+
if @sector.light_level == @maxlight
|
|
97
|
+
@sector.light_level = @minlight
|
|
98
|
+
@count = (rand(8)) + 1 # dark for 1-8 tics
|
|
99
|
+
else
|
|
100
|
+
@sector.light_level = @maxlight
|
|
101
|
+
@count = (rand(2) == 0 ? 1 : 65) # bright for 1 or 65 tics (P_Random()&64)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# T_StrobeFlash (types 2, 3, 4, 12, 13): regular strobe blink
|
|
107
|
+
class StrobeFlash
|
|
108
|
+
def initialize(sector, minlight, darktime, in_sync)
|
|
109
|
+
@sector = sector
|
|
110
|
+
@maxlight = sector.light_level
|
|
111
|
+
@minlight = minlight
|
|
112
|
+
@minlight = 0 if @minlight == @maxlight
|
|
113
|
+
@darktime = darktime
|
|
114
|
+
@brighttime = STROBEBRIGHT
|
|
115
|
+
@count = in_sync ? 1 : (rand(8)) + 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def update
|
|
119
|
+
@count -= 1
|
|
120
|
+
return if @count > 0
|
|
121
|
+
|
|
122
|
+
if @sector.light_level == @minlight
|
|
123
|
+
@sector.light_level = @maxlight
|
|
124
|
+
@count = @brighttime
|
|
125
|
+
else
|
|
126
|
+
@sector.light_level = @minlight
|
|
127
|
+
@count = @darktime
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# T_Glow (type 8): smooth triangle-wave oscillation
|
|
133
|
+
class Glow
|
|
134
|
+
def initialize(sector, minlight)
|
|
135
|
+
@sector = sector
|
|
136
|
+
@maxlight = sector.light_level
|
|
137
|
+
@minlight = minlight
|
|
138
|
+
@direction = -1 # start dimming
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def update
|
|
142
|
+
if @direction == -1
|
|
143
|
+
@sector.light_level -= GLOWSPEED
|
|
144
|
+
if @sector.light_level <= @minlight
|
|
145
|
+
@sector.light_level += GLOWSPEED
|
|
146
|
+
@direction = 1
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
@sector.light_level += GLOWSPEED
|
|
150
|
+
if @sector.light_level >= @maxlight
|
|
151
|
+
@sector.light_level -= GLOWSPEED
|
|
152
|
+
@direction = -1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# T_FireFlicker (type 17): random fire-like flickering
|
|
159
|
+
class FireFlicker
|
|
160
|
+
def initialize(sector, minlight)
|
|
161
|
+
@sector = sector
|
|
162
|
+
@maxlight = sector.light_level
|
|
163
|
+
@minlight = minlight + 16 # fire doesn't go as dark
|
|
164
|
+
@count = 4
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def update
|
|
168
|
+
@count -= 1
|
|
169
|
+
return if @count > 0
|
|
170
|
+
|
|
171
|
+
amount = (rand(4)) * 16 # 0, 16, 32, or 48
|
|
172
|
+
level = @maxlight - amount
|
|
173
|
+
@sector.light_level = level < @minlight ? @minlight : level
|
|
174
|
+
@count = 4
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|