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.
@@ -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