doom 0.8.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +17 -0
  3. data/README.md +65 -4
  4. data/bin/doom +134 -24
  5. data/lib/doom/benchmark.rb +282 -0
  6. data/lib/doom/game/animations.rb +9 -2
  7. data/lib/doom/game/combat.rb +239 -153
  8. data/lib/doom/game/framebuffer_blitter.rb +38 -0
  9. data/lib/doom/game/geometry.rb +68 -0
  10. data/lib/doom/game/intermission.rb +22 -39
  11. data/lib/doom/game/item_pickup.rb +87 -75
  12. data/lib/doom/game/menu.rb +90 -85
  13. data/lib/doom/game/monster_ai.rb +150 -136
  14. data/lib/doom/game/player.rb +105 -0
  15. data/lib/doom/game/player_physics.rb +384 -0
  16. data/lib/doom/game/player_state.rb +38 -64
  17. data/lib/doom/game/random.rb +82 -0
  18. data/lib/doom/game/sector_actions.rb +169 -93
  19. data/lib/doom/game/sector_effects.rb +25 -18
  20. data/lib/doom/game/snapshot.rb +584 -0
  21. data/lib/doom/game/sound_engine.rb +33 -58
  22. data/lib/doom/game/state_hash.rb +125 -0
  23. data/lib/doom/game/ticcmd.rb +61 -0
  24. data/lib/doom/game/world.rb +420 -0
  25. data/lib/doom/map/data.rb +95 -0
  26. data/lib/doom/net/client.rb +204 -0
  27. data/lib/doom/net/desync_monitor.rb +101 -0
  28. data/lib/doom/net/game_server.rb +232 -0
  29. data/lib/doom/net/lockstep.rb +170 -0
  30. data/lib/doom/net/protocol.rb +350 -0
  31. data/lib/doom/net/session.rb +282 -0
  32. data/lib/doom/net/transport.rb +110 -0
  33. data/lib/doom/platform/gosu_window.rb +604 -832
  34. data/lib/doom/platform/sdl.rb +74 -0
  35. data/lib/doom/platform/window_logic.rb +61 -0
  36. data/lib/doom/render/font.rb +5 -3
  37. data/lib/doom/render/hardware_renderer.rb +547 -0
  38. data/lib/doom/render/ray_tracing/bvh.rb +103 -0
  39. data/lib/doom/render/ray_tracing/material_state.rb +66 -0
  40. data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
  41. data/lib/doom/render/ray_tracing_renderer.rb +940 -0
  42. data/lib/doom/render/renderer.rb +466 -440
  43. data/lib/doom/render/renderer_factory.rb +50 -0
  44. data/lib/doom/render/screen_melt.rb +7 -7
  45. data/lib/doom/render/spinel_native/kernel.rb +780 -0
  46. data/lib/doom/render/spinel_native_renderer.rb +162 -0
  47. data/lib/doom/render/status_bar.rb +14 -10
  48. data/lib/doom/render/weapon_renderer.rb +9 -10
  49. data/lib/doom/render/world_mesh.rb +270 -0
  50. data/lib/doom/render/zbuffer_renderer.rb +151 -0
  51. data/lib/doom/version.rb +1 -1
  52. data/lib/doom/wad/flat.rb +1 -1
  53. data/lib/doom/wad/hud_graphics.rb +7 -3
  54. data/lib/doom/wad/palette.rb +3 -3
  55. data/lib/doom/wad/patch.rb +1 -1
  56. data/lib/doom/wad/reader.rb +12 -15
  57. data/lib/doom/wad/sound.rb +14 -6
  58. data/lib/doom/wad/sprite.rb +36 -35
  59. data/lib/doom/wad/texture.rb +5 -5
  60. data/lib/doom/wad_downloader.rb +38 -56
  61. data/lib/doom.rb +197 -16
  62. metadata +94 -7
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Game
5
+ # Shared framebuffer blitting for the full-screen menu and intermission
6
+ # screens. Both draw palette-index sprites into a flat 320x240 framebuffer,
7
+ # so the clipping loop lived duplicated in each. It lives here once and uses
8
+ # the renderer's screen constants instead of hardcoded 320/240.
9
+ #
10
+ # A "sprite" here is anything exposing #width and #column_pixels(x), where a
11
+ # column is an array of palette indices (nil = transparent).
12
+ module FramebufferBlitter
13
+ # Blit a sprite's opaque pixels at (x, y), clipping to the screen.
14
+ def blit_sprite(framebuffer, sprite, x, y)
15
+ return unless sprite
16
+
17
+ w = Render::SCREEN_WIDTH
18
+ h = Render::SCREEN_HEIGHT
19
+ sprite.width.times do |col_x|
20
+ sx = x + col_x
21
+ next if sx < 0 || sx >= w
22
+
23
+ col = sprite.column_pixels(col_x)
24
+ next unless col
25
+
26
+ col.each_with_index do |color, col_y|
27
+ next unless color
28
+
29
+ sy = y + col_y
30
+ next if sy < 0 || sy >= h
31
+
32
+ framebuffer[(sy * w) + sx] = color
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Game
5
+ # Shared 2D geometry for the simulation. These three helpers were copy-pasted
6
+ # across combat, monster AI, player physics, world and sector actions (the
7
+ # last under the name point_line_dist); consolidated here so line-of-sight,
8
+ # collision and use-line checks share one definition.
9
+ #
10
+ # Pure functions of their arguments -- no state, no RNG, no wall clock -- so
11
+ # they stay deterministic and every peer computes the same result.
12
+ module Geometry
13
+ module_function
14
+
15
+ # Do the open segments A (ax1,ay1)->(ax2,ay2) and B (bx1,by1)->(bx2,by2)
16
+ # cross? Used for line-of-sight and hitscan against walls. Parallel or
17
+ # barely-crossing segments (tiny determinant) count as no intersection.
18
+ def segments_intersect?(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
19
+ d1x = ax2 - ax1
20
+ d1y = ay2 - ay1
21
+ d2x = bx2 - bx1
22
+ d2y = by2 - by1
23
+ denom = (d1x * d2y) - (d1y * d2x)
24
+ return false if denom.abs < 0.001
25
+
26
+ dx = bx1 - ax1
27
+ dy = by1 - ay1
28
+ t = ((dx * d2y) - (dy * d2x)).to_f / denom
29
+ u = ((dx * d1y) - (dy * d1x)).to_f / denom
30
+ t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
31
+ end
32
+
33
+ # Does the segment (x1,y1)->(x2,y2) pass within `radius` of the circle
34
+ # centred at (cx,cy)? The collision test for a body of that radius sliding
35
+ # against a wall.
36
+ def line_circle_intersect?(x1, y1, x2, y2, cx, cy, radius)
37
+ dx = cx - x1
38
+ dy = cy - y1
39
+ line_dx = x2 - x1
40
+ line_dy = y2 - y1
41
+ line_len_sq = (line_dx * line_dx) + (line_dy * line_dy)
42
+ return false if line_len_sq.zero?
43
+
44
+ t = (((dx * line_dx) + (dy * line_dy)).to_f / line_len_sq).clamp(0.0, 1.0)
45
+ closest_x = x1 + (t * line_dx)
46
+ closest_y = y1 + (t * line_dy)
47
+ dist_sq = ((cx - closest_x)**2) + ((cy - closest_y)**2)
48
+ dist_sq < radius * radius
49
+ end
50
+
51
+ # Shortest distance from point (px,py) to the segment (x1,y1)->(x2,y2),
52
+ # clamped to the segment ends. Used for use-line and trigger proximity.
53
+ def point_to_segment_distance(px, py, x1, y1, x2, y2)
54
+ dx = px - x1
55
+ dy = py - y1
56
+ line_dx = x2 - x1
57
+ line_dy = y2 - y1
58
+ line_len_sq = (line_dx * line_dx) + (line_dy * line_dy)
59
+ return Math.sqrt((dx * dx) + (dy * dy)) if line_len_sq.zero?
60
+
61
+ t = (((dx * line_dx) + (dy * line_dy)).to_f / line_len_sq).clamp(0.0, 1.0)
62
+ closest_x = x1 + (t * line_dx)
63
+ closest_y = y1 + (t * line_dy)
64
+ Math.sqrt(((px - closest_x)**2) + ((py - closest_y)**2))
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,21 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'framebuffer_blitter'
4
+
3
5
  module Doom
4
6
  module Game
5
7
  # Intermission screen shown between levels.
6
8
  # Displays kill%, item%, secret%, time, and par time.
7
9
  class Intermission
10
+ include FramebufferBlitter
11
+
8
12
  # Episode 1 par times in seconds (from Chocolate Doom)
9
13
  PAR_TIMES = {
10
14
  'E1M1' => 30, 'E1M2' => 75, 'E1M3' => 120, 'E1M4' => 90,
11
- 'E1M5' => 165, 'E1M6' => 180, 'E1M7' => 180, 'E1M8' => 30, 'E1M9' => 165,
15
+ 'E1M5' => 165, 'E1M6' => 180, 'E1M7' => 180, 'E1M8' => 30, 'E1M9' => 165
12
16
  }.freeze
13
17
 
14
18
  # Next map progression
15
19
  NEXT_MAP = {
16
20
  'E1M1' => 'E1M2', 'E1M2' => 'E1M3', 'E1M3' => 'E1M4', 'E1M4' => 'E1M5',
17
21
  'E1M5' => 'E1M6', 'E1M6' => 'E1M7', 'E1M7' => 'E1M8', 'E1M8' => nil,
18
- 'E1M9' => 'E1M4',
22
+ 'E1M9' => 'E1M4'
19
23
  }.freeze
20
24
 
21
25
  # Counter animation speed (percentage points per tic)
@@ -24,10 +28,9 @@ module Doom
24
28
 
25
29
  attr_reader :finished, :next_map
26
30
 
27
- def initialize(wad, hud_graphics, stats)
28
- @wad = wad
31
+ def initialize(_wad, hud_graphics, stats)
29
32
  @gfx = hud_graphics
30
- @stats = stats # { map:, kills:, total_kills:, items:, total_items:, secrets:, total_secrets:, time_tics: }
33
+ @stats = stats # { map:, kills:, total_kills:, items:, total_items:, secrets:, total_secrets:, time_tics: }
31
34
  @finished = false
32
35
  @next_map = NEXT_MAP[stats[:map]]
33
36
  @tic = 0
@@ -110,11 +113,11 @@ module Doom
110
113
  end
111
114
 
112
115
  # "Press any key" hint after counting
113
- if @counting_done && (@tic / 17) % 2 == 0
114
- # Blink hint via skull
115
- skull = @skulls[@tic / 8 % 2]
116
- draw_sprite(framebuffer, skull, 144, 210) if skull
117
- end
116
+ return unless @counting_done && (@tic / 17).even?
117
+
118
+ # Blink hint via skull
119
+ skull = @skulls[@tic / 8 % 2]
120
+ draw_sprite(framebuffer, skull, 144, 210) if skull
118
121
  end
119
122
 
120
123
  def handle_key
@@ -134,7 +137,8 @@ module Doom
134
137
 
135
138
  def map_to_level_index(map_name)
136
139
  return 0 unless map_name
137
- map_name[3].to_i - 1 # E1M1 -> 0, E1M2 -> 1, etc.
140
+
141
+ map_name[3].to_i - 1 # E1M1 -> 0, E1M2 -> 1, etc.
138
142
  end
139
143
 
140
144
  def load_graphics
@@ -164,29 +168,20 @@ module Doom
164
168
  end
165
169
 
166
170
  def load_patch(name)
167
- @gfx.send(:load_graphic, name)
171
+ @gfx.load_graphic(name)
168
172
  end
169
173
 
170
174
  def draw_background(framebuffer)
171
175
  return unless @wimap
176
+
172
177
  draw_fullscreen(framebuffer, @wimap)
173
178
  end
174
179
 
175
180
  def draw_fullscreen(framebuffer, sprite)
176
181
  return unless sprite
177
- y_offset = (240 - sprite.height) / 2
178
- y_offset = [y_offset, 0].max
179
- sprite.width.times do |x|
180
- next if x >= 320
181
- col = sprite.column_pixels(x)
182
- next unless col
183
- col.each_with_index do |color, y|
184
- next unless color
185
- sy = y + y_offset
186
- next if sy < 0 || sy >= 240
187
- framebuffer[sy * 320 + x] = color
188
- end
189
- end
182
+
183
+ y_offset = [(Render::SCREEN_HEIGHT - sprite.height) / 2, 0].max
184
+ blit_sprite(framebuffer, sprite, 0, y_offset)
190
185
  end
191
186
 
192
187
  def draw_percent(framebuffer, right_x, y, value)
@@ -205,7 +200,7 @@ module Doom
205
200
  draw_num_right(framebuffer, right_x, y, secs, pad: 2)
206
201
 
207
202
  # Colon
208
- colon_x = right_x - num_width * 2 - 4
203
+ colon_x = right_x - (num_width * 2) - 4
209
204
  draw_sprite(framebuffer, @colon, colon_x, y) if @colon
210
205
 
211
206
  # Minutes
@@ -229,19 +224,7 @@ module Doom
229
224
  end
230
225
 
231
226
  def draw_sprite(framebuffer, sprite, x, y)
232
- return unless sprite
233
- sprite.width.times do |col_x|
234
- sx = x + col_x
235
- next if sx < 0 || sx >= 320
236
- col = sprite.column_pixels(col_x)
237
- next unless col
238
- col.each_with_index do |color, col_y|
239
- next unless color
240
- sy = y + col_y
241
- next if sy < 0 || sy >= 240
242
- framebuffer[sy * 320 + sx] = color
243
- end
244
- end
227
+ blit_sprite(framebuffer, sprite, x, y)
245
228
  end
246
229
  end
247
230
  end
@@ -7,7 +7,7 @@ module Doom
7
7
  class ItemPickup
8
8
  PLAYER_RADIUS = 16.0
9
9
  THING_RADIUS = 20.0
10
- PICKUP_DIST = PLAYER_RADIUS + THING_RADIUS # 36 units (bounding box overlap)
10
+ PICKUP_DIST = PLAYER_RADIUS + THING_RADIUS # 36 units (bounding box overlap)
11
11
 
12
12
  # Item definitions: type => { category, value, ... }
13
13
  ITEMS = {
@@ -26,9 +26,9 @@ module Doom
26
26
  2049 => { cat: :ammo, ammo: :shells, amount: 20 }, # Box of shells
27
27
  2010 => { cat: :ammo, ammo: :rockets, amount: 1 }, # Rocket
28
28
  2046 => { cat: :ammo, ammo: :rockets, amount: 5 }, # Box of rockets
29
- 17 => { cat: :ammo, ammo: :cells, amount: 20 }, # Cell charge
30
- 2047 => { cat: :ammo, ammo: :cells, amount: 100 }, # Cell pack
31
- 8 => { cat: :backpack }, # Backpack (doubles max ammo + some ammo)
29
+ 17 => { cat: :ammo, ammo: :cells, amount: 20 }, # Cell charge
30
+ 2047 => { cat: :ammo, ammo: :cells, amount: 100 }, # Cell pack
31
+ 8 => { cat: :backpack }, # Backpack (doubles max ammo + some ammo)
32
32
 
33
33
  # Health
34
34
  2014 => { cat: :health, amount: 1, max: 200 }, # Health bonus (+1, up to 200)
@@ -37,17 +37,17 @@ module Doom
37
37
  2013 => { cat: :health, amount: 100, max: 200 }, # Soul sphere
38
38
 
39
39
  # Armor
40
- 2015 => { cat: :armor, amount: 1, max: 200 }, # Armor bonus (+1, up to 200)
40
+ 2015 => { cat: :armor, amount: 1, max: 200 }, # Armor bonus (+1, up to 200)
41
41
  2018 => { cat: :armor, amount: 100, armor_type: 1 }, # Green armor (100%, absorbs 1/3)
42
42
  2019 => { cat: :armor, amount: 200, armor_type: 2 }, # Blue armor (200%, absorbs 1/2)
43
43
 
44
44
  # Keys
45
- 5 => { cat: :key, key: :blue_card },
46
- 6 => { cat: :key, key: :yellow_card },
45
+ 5 => { cat: :key, key: :blue_card },
46
+ 6 => { cat: :key, key: :yellow_card },
47
47
  13 => { cat: :key, key: :red_card },
48
48
  40 => { cat: :key, key: :blue_skull },
49
49
  39 => { cat: :key, key: :yellow_skull },
50
- 38 => { cat: :key, key: :red_skull },
50
+ 38 => { cat: :key, key: :red_skull }
51
51
  }.freeze
52
52
 
53
53
  MESSAGETICS = 140 # 4 * TICRATE (4 seconds, matching Chocolate Doom)
@@ -56,9 +56,8 @@ module Doom
56
56
  attr_reader :picked_up, :pickup_message, :pickup_flash, :message_tics
57
57
  attr_accessor :ammo_multiplier, :hidden_things
58
58
 
59
- def initialize(map, player_state, hidden_things = {})
59
+ def initialize(map, hidden_things = {})
60
60
  @map = map
61
- @player = player_state
62
61
  @picked_up = {}
63
62
  @hidden_things = hidden_things
64
63
  @ammo_multiplier = 1
@@ -73,132 +72,145 @@ module Doom
73
72
  @message_tics -= 1 if @message_tics > 0
74
73
  end
75
74
 
76
- def update(player_x, player_y)
75
+ # Pick up for one player. `picked_up` stays global on purpose: in coop an
76
+ # item taken by anyone is gone for everyone, as in DOOM. Deathmatch
77
+ # respawning is a separate concern.
78
+ def update(player)
79
+ state = player.state
77
80
  @map.things.each_with_index do |thing, idx|
78
81
  next if @hidden_things[idx]
79
82
  next if @picked_up[idx]
83
+
80
84
  item = ITEMS[thing.type]
81
85
  next unless item
82
86
 
83
- dx = (player_x - thing.x).abs
84
- dy = (player_y - thing.y).abs
87
+ dx = (player.x - thing.x).abs
88
+ dy = (player.y - thing.y).abs
85
89
  next if dx >= PICKUP_DIST || dy >= PICKUP_DIST
86
90
 
87
- if try_pickup(item)
88
- @picked_up[idx] = true
89
- @pickup_message = PICKUP_MESSAGES[thing.type]
90
- @pickup_flash = FLASH_TICS
91
- @message_tics = MESSAGETICS
92
- end
91
+ next unless try_pickup(item, state)
92
+
93
+ @picked_up[idx] = true
94
+ @pickup_message = PICKUP_MESSAGES[thing.type]
95
+ @pickup_flash = FLASH_TICS
96
+ @message_tics = MESSAGETICS
93
97
  end
94
98
  end
95
99
 
96
100
  PICKUP_MESSAGES = {
97
- 2001 => "A SHOTGUN!", 2002 => "A CHAINGUN!", 2003 => "A ROCKET LAUNCHER!",
98
- 2004 => "A PLASMA RIFLE!", 2005 => "A CHAINSAW!", 2006 => "A BFG9000!",
99
- 2007 => "PICKED UP A CLIP.", 2048 => "PICKED UP A BOX OF BULLETS.",
100
- 2008 => "PICKED UP 4 SHOTGUN SHELLS.", 2049 => "PICKED UP A BOX OF SHELLS.",
101
- 2010 => "PICKED UP A ROCKET.", 2046 => "PICKED UP A BOX OF ROCKETS.",
102
- 17 => "PICKED UP AN ENERGY CELL.", 2047 => "PICKED UP AN ENERGY CELL PACK.",
103
- 8 => "PICKED UP A BACKPACK FULL OF AMMO!",
104
- 2011 => "PICKED UP A STIMPACK.", 2012 => "PICKED UP A MEDIKIT.",
105
- 2014 => "PICKED UP A HEALTH BONUS.", 2015 => "PICKED UP AN ARMOR BONUS.",
106
- 2018 => "PICKED UP THE ARMOR.", 2019 => "PICKED UP THE MEGAARMOR!",
107
- 2013 => "SUPERCHARGE!",
108
- 5 => "PICKED UP A BLUE KEYCARD.", 6 => "PICKED UP A YELLOW KEYCARD.",
109
- 13 => "PICKED UP A RED KEYCARD.", 40 => "PICKED UP A BLUE SKULL KEY.",
110
- 39 => "PICKED UP A YELLOW SKULL KEY.", 38 => "PICKED UP A RED SKULL KEY.",
101
+ 2001 => 'A SHOTGUN!', 2002 => 'A CHAINGUN!', 2003 => 'A ROCKET LAUNCHER!',
102
+ 2004 => 'A PLASMA RIFLE!', 2005 => 'A CHAINSAW!', 2006 => 'A BFG9000!',
103
+ 2007 => 'PICKED UP A CLIP.', 2048 => 'PICKED UP A BOX OF BULLETS.',
104
+ 2008 => 'PICKED UP 4 SHOTGUN SHELLS.', 2049 => 'PICKED UP A BOX OF SHELLS.',
105
+ 2010 => 'PICKED UP A ROCKET.', 2046 => 'PICKED UP A BOX OF ROCKETS.',
106
+ 17 => 'PICKED UP AN ENERGY CELL.', 2047 => 'PICKED UP AN ENERGY CELL PACK.',
107
+ 8 => 'PICKED UP A BACKPACK FULL OF AMMO!',
108
+ 2011 => 'PICKED UP A STIMPACK.', 2012 => 'PICKED UP A MEDIKIT.',
109
+ 2014 => 'PICKED UP A HEALTH BONUS.', 2015 => 'PICKED UP AN ARMOR BONUS.',
110
+ 2018 => 'PICKED UP THE ARMOR.', 2019 => 'PICKED UP THE MEGAARMOR!',
111
+ 2013 => 'SUPERCHARGE!',
112
+ 5 => 'PICKED UP A BLUE KEYCARD.', 6 => 'PICKED UP A YELLOW KEYCARD.',
113
+ 13 => 'PICKED UP A RED KEYCARD.', 40 => 'PICKED UP A BLUE SKULL KEY.',
114
+ 39 => 'PICKED UP A YELLOW SKULL KEY.', 38 => 'PICKED UP A RED SKULL KEY.'
111
115
  }.freeze
112
116
 
113
117
  private
114
118
 
115
- def try_pickup(item)
119
+ def try_pickup(item, state)
116
120
  case item[:cat]
117
121
  when :weapon
118
- give_weapon(item)
122
+ give_weapon(item, state)
119
123
  when :ammo
120
- give_ammo(item[:ammo], item[:amount])
124
+ give_ammo(item[:ammo], item[:amount], state)
121
125
  when :backpack
122
- give_backpack
126
+ give_backpack(state)
123
127
  when :health
124
- give_health(item[:amount], item[:max])
128
+ give_health(item[:amount], item[:max], state)
125
129
  when :armor
126
- give_armor(item)
130
+ give_armor(item, state)
127
131
  when :key
128
- give_key(item[:key])
132
+ give_key(item[:key], state)
129
133
  else
130
134
  false
131
135
  end
132
136
  end
133
137
 
134
- def give_weapon(item)
138
+ def give_weapon(item, state)
135
139
  weapon_idx = item[:weapon]
136
- had_weapon = @player.has_weapons[weapon_idx]
140
+ had_weapon = state.has_weapons[weapon_idx]
137
141
 
138
- @player.has_weapons[weapon_idx] = true
139
- ammo_given = item[:ammo] ? give_ammo(item[:ammo], item[:amount]) : false
142
+ state.has_weapons[weapon_idx] = true
143
+ ammo_given = item[:ammo] ? give_ammo(item[:ammo], item[:amount], state) : false
140
144
 
141
145
  unless had_weapon
142
- @player.switch_weapon(weapon_idx) unless @player.attacking
146
+ state.switch_weapon(weapon_idx) unless state.attacking
143
147
  return true
144
148
  end
145
149
 
146
150
  ammo_given
147
151
  end
148
152
 
149
- def give_ammo(type, amount)
150
- amount = amount * @ammo_multiplier
153
+ def give_ammo(type, amount, state)
154
+ amount *= @ammo_multiplier
151
155
  case type
152
156
  when :bullets
153
- return false if @player.ammo_bullets >= @player.max_bullets
154
- @player.ammo_bullets = [@player.ammo_bullets + amount, @player.max_bullets].min
157
+ return false if state.ammo_bullets >= state.max_bullets
158
+
159
+ state.ammo_bullets = [state.ammo_bullets + amount, state.max_bullets].min
155
160
  when :shells
156
- return false if @player.ammo_shells >= @player.max_shells
157
- @player.ammo_shells = [@player.ammo_shells + amount, @player.max_shells].min
161
+ return false if state.ammo_shells >= state.max_shells
162
+
163
+ state.ammo_shells = [state.ammo_shells + amount, state.max_shells].min
158
164
  when :rockets
159
- return false if @player.ammo_rockets >= @player.max_rockets
160
- @player.ammo_rockets = [@player.ammo_rockets + amount, @player.max_rockets].min
165
+ return false if state.ammo_rockets >= state.max_rockets
166
+
167
+ state.ammo_rockets = [state.ammo_rockets + amount, state.max_rockets].min
161
168
  when :cells
162
- return false if @player.ammo_cells >= @player.max_cells
163
- @player.ammo_cells = [@player.ammo_cells + amount, @player.max_cells].min
169
+ return false if state.ammo_cells >= state.max_cells
170
+
171
+ state.ammo_cells = [state.ammo_cells + amount, state.max_cells].min
164
172
  end
165
173
  true
166
174
  end
167
175
 
168
- def give_backpack
169
- @player.max_bullets = 400
170
- @player.max_shells = 100
171
- @player.max_rockets = 100
172
- @player.max_cells = 600
173
- give_ammo(:bullets, 10)
174
- give_ammo(:shells, 4)
175
- give_ammo(:rockets, 1)
176
- give_ammo(:cells, 20)
176
+ def give_backpack(state)
177
+ state.max_bullets = 400
178
+ state.max_shells = 100
179
+ state.max_rockets = 100
180
+ state.max_cells = 600
181
+ give_ammo(:bullets, 10, state)
182
+ give_ammo(:shells, 4, state)
183
+ give_ammo(:rockets, 1, state)
184
+ give_ammo(:cells, 20, state)
177
185
  true
178
186
  end
179
187
 
180
- def give_health(amount, max)
181
- return false if @player.health >= max
182
- @player.health = [@player.health + amount, max].min
188
+ def give_health(amount, max, state)
189
+ return false if state.health >= max
190
+
191
+ state.health = [state.health + amount, max].min
183
192
  true
184
193
  end
185
194
 
186
- def give_armor(item)
195
+ def give_armor(item, state)
187
196
  if item[:armor_type]
188
197
  # Green/blue armor: only pick up if better than current
189
- return false if @player.armor >= item[:amount]
190
- @player.armor = item[:amount]
198
+ return false if state.armor >= item[:amount]
199
+
200
+ state.armor = item[:amount]
191
201
  else
192
202
  # Armor bonus: +1, up to max
193
- return false if @player.armor >= item[:max]
194
- @player.armor = [@player.armor + item[:amount], item[:max]].min
203
+ return false if state.armor >= item[:max]
204
+
205
+ state.armor = [state.armor + item[:amount], item[:max]].min
195
206
  end
196
207
  true
197
208
  end
198
209
 
199
- def give_key(key)
200
- return false if @player.keys[key]
201
- @player.keys[key] = true
210
+ def give_key(key, state)
211
+ return false if state.keys[key]
212
+
213
+ state.keys[key] = true
202
214
  true
203
215
  end
204
216
  end