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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4672b03d393f17c1cd9ece97869d7239a7af3e91d3ffa47b911b0ba3058f673
|
|
4
|
+
data.tar.gz: 56772f44ced59386c92f5319c0d25dc8612e96263d68ae7e9a240d26508571cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b11b52ed631217b002aae05a4f87b7ff5fcb208485b75dd9077ebcc222c3823c6ca74167e9bc6b7c3605c0481472abbc818e6445c8fa130c744fe8c54da53099
|
|
7
|
+
data.tar.gz: 808176138162ef4e21796dd995a6bd794eee89e4c631032134349746afd468d446b610a0939daf19c26a2706abac22635306d6e3fb1c1c93e9a70282a2114f7d
|
data/README.md
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Animated texture/flat cycling, matching Chocolate Doom's P_InitPicAnims
|
|
6
|
+
# and P_UpdateSpecials from p_spec.c.
|
|
7
|
+
#
|
|
8
|
+
# All animations run at 8 tics per frame (8/35 sec ≈ 0.23s).
|
|
9
|
+
# Frames must be consecutive entries in the WAD; the engine uses
|
|
10
|
+
# start/end names to find the range.
|
|
11
|
+
class Animations
|
|
12
|
+
TICS_PER_FRAME = 8
|
|
13
|
+
|
|
14
|
+
# [is_texture, start_name, end_name]
|
|
15
|
+
# From Chocolate Doom animdefs[] in p_spec.c
|
|
16
|
+
ANIMDEFS = [
|
|
17
|
+
# Animated flats
|
|
18
|
+
[false, 'NUKAGE1', 'NUKAGE3'],
|
|
19
|
+
[false, 'FWATER1', 'FWATER4'],
|
|
20
|
+
[false, 'SWATER1', 'SWATER4'],
|
|
21
|
+
[false, 'LAVA1', 'LAVA4'],
|
|
22
|
+
[false, 'BLOOD1', 'BLOOD3'],
|
|
23
|
+
[false, 'RROCK05', 'RROCK08'],
|
|
24
|
+
[false, 'SLIME01', 'SLIME04'],
|
|
25
|
+
[false, 'SLIME05', 'SLIME08'],
|
|
26
|
+
[false, 'SLIME09', 'SLIME12'],
|
|
27
|
+
# Animated wall textures
|
|
28
|
+
[true, 'BLODGR1', 'BLODGR4'],
|
|
29
|
+
[true, 'SLADRIP1', 'SLADRIP3'],
|
|
30
|
+
[true, 'BLODRIP1', 'BLODRIP4'],
|
|
31
|
+
[true, 'FIREWALA', 'FIREWALL'],
|
|
32
|
+
[true, 'GSTFONT1', 'GSTFONT3'],
|
|
33
|
+
[true, 'FIRELAV3', 'FIRELAVA'],
|
|
34
|
+
[true, 'FIREMAG1', 'FIREMAG3'],
|
|
35
|
+
[true, 'FIREBLU1', 'FIREBLU2'],
|
|
36
|
+
[true, 'ROCKRED1', 'ROCKRED3'],
|
|
37
|
+
[true, 'BFALL1', 'BFALL4'],
|
|
38
|
+
[true, 'SFALL1', 'SFALL4'],
|
|
39
|
+
[true, 'WFALL1', 'WFALL4'],
|
|
40
|
+
[true, 'DBRAIN1', 'DBRAIN4'],
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
attr_reader :flat_translation, :texture_translation
|
|
44
|
+
|
|
45
|
+
def initialize(texture_names, flat_names)
|
|
46
|
+
@flat_translation = {} # flat_name -> current_frame_name
|
|
47
|
+
@texture_translation = {} # texture_name -> current_frame_name
|
|
48
|
+
@anims = []
|
|
49
|
+
|
|
50
|
+
ANIMDEFS.each do |is_texture, start_name, end_name|
|
|
51
|
+
names = is_texture ? texture_names : flat_names
|
|
52
|
+
|
|
53
|
+
start_idx = names.index(start_name)
|
|
54
|
+
end_idx = names.index(end_name)
|
|
55
|
+
next unless start_idx && end_idx
|
|
56
|
+
next if end_idx <= start_idx
|
|
57
|
+
|
|
58
|
+
frames = names[start_idx..end_idx]
|
|
59
|
+
next if frames.size < 2
|
|
60
|
+
|
|
61
|
+
@anims << {
|
|
62
|
+
is_texture: is_texture,
|
|
63
|
+
frames: frames,
|
|
64
|
+
speed: TICS_PER_FRAME,
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Call every game tic (or approximate with leveltime).
|
|
70
|
+
# Matches Chocolate Doom P_UpdateSpecials:
|
|
71
|
+
# pic = basepic + ((leveltime / speed + i) % numpics)
|
|
72
|
+
def update(leveltime)
|
|
73
|
+
@anims.each do |anim|
|
|
74
|
+
frames = anim[:frames]
|
|
75
|
+
numpics = frames.size
|
|
76
|
+
phase = leveltime / anim[:speed]
|
|
77
|
+
translation = anim[:is_texture] ? @texture_translation : @flat_translation
|
|
78
|
+
|
|
79
|
+
numpics.times do |i|
|
|
80
|
+
current_frame = frames[(phase + i) % numpics]
|
|
81
|
+
translation[frames[i]] = current_frame
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Translate a flat name to its current animation frame
|
|
87
|
+
def translate_flat(name)
|
|
88
|
+
@flat_translation[name] || name
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Translate a texture name to its current animation frame
|
|
92
|
+
def translate_texture(name)
|
|
93
|
+
@texture_translation[name] || name
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Hitscan weapon firing and monster state tracking.
|
|
6
|
+
# Matches Chocolate Doom's P_LineAttack / P_AimLineAttack from p_map.c.
|
|
7
|
+
class Combat
|
|
8
|
+
# Monster starting HP (from mobjinfo[] in info.c)
|
|
9
|
+
MONSTER_HP = {
|
|
10
|
+
3004 => 20, # Zombieman
|
|
11
|
+
9 => 30, # Shotgun Guy
|
|
12
|
+
3001 => 60, # Imp
|
|
13
|
+
3002 => 150, # Demon
|
|
14
|
+
58 => 150, # Spectre
|
|
15
|
+
3003 => 1000, # Baron of Hell
|
|
16
|
+
69 => 500, # Hell Knight
|
|
17
|
+
3005 => 400, # Cacodemon
|
|
18
|
+
3006 => 100, # Lost Soul
|
|
19
|
+
16 => 4000, # Cyberdemon
|
|
20
|
+
7 => 3000, # Spider Mastermind
|
|
21
|
+
65 => 70, # Heavy Weapon Dude
|
|
22
|
+
64 => 700, # Archvile
|
|
23
|
+
71 => 400, # Pain Elemental
|
|
24
|
+
84 => 20, # Wolfenstein SS
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
MONSTER_RADIUS = {
|
|
28
|
+
3004 => 20, 9 => 20, 3001 => 20, 3002 => 30, 58 => 30,
|
|
29
|
+
3003 => 24, 69 => 24, 3005 => 31, 3006 => 16, 16 => 40,
|
|
30
|
+
7 => 128, 65 => 20, 64 => 20, 71 => 31, 84 => 20,
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Normal death frame sequences per sprite prefix (rotation 0 only)
|
|
34
|
+
# Identified by sprite heights: frames go from standing height to flat on ground
|
|
35
|
+
DEATH_FRAMES = {
|
|
36
|
+
'POSS' => %w[H I J K L], # Zombieman: 55→46→34→27→17
|
|
37
|
+
'SPOS' => %w[H I J K L], # Shotgun Guy: 60→50→35→27→17
|
|
38
|
+
'TROO' => %w[I J K L M], # Imp: 62→59→54→46→22
|
|
39
|
+
'SARG' => %w[I J K L M N], # Demon/Spectre: 56→56→53→57→46→32
|
|
40
|
+
'BOSS' => %w[H I J K L M N], # Baron
|
|
41
|
+
'BOS2' => %w[H I J K L M N], # Hell Knight
|
|
42
|
+
'HEAD' => %w[G H I J K L], # Cacodemon
|
|
43
|
+
'SKUL' => %w[G H I J K], # Lost Soul
|
|
44
|
+
'CYBR' => %w[I J], # Cyberdemon
|
|
45
|
+
'SPID' => %w[I J K], # Spider Mastermind
|
|
46
|
+
'CPOS' => %w[H I J K L M N], # Heavy Weapon Dude
|
|
47
|
+
'PAIN' => %w[H I J K L M], # Pain Elemental
|
|
48
|
+
'SSWV' => %w[I J K L M], # Wolfenstein SS
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
DEATH_ANIM_TICS = 6 # Tics per death frame
|
|
52
|
+
|
|
53
|
+
# Weapon damage: DOOM does (P_Random()%3 + 1) * multiplier
|
|
54
|
+
# Pistol/chaingun: 1*5..3*5 = 5-15 per bullet
|
|
55
|
+
# Shotgun: 7 pellets, each 1*5..3*5 = 5-15
|
|
56
|
+
# Fist/chainsaw: 1*2..3*2 = 2-10
|
|
57
|
+
|
|
58
|
+
def initialize(map, player_state, sprites)
|
|
59
|
+
@map = map
|
|
60
|
+
@player = player_state
|
|
61
|
+
@sprites = sprites
|
|
62
|
+
@monster_hp = {} # thing_idx => current HP
|
|
63
|
+
@dead_things = {} # thing_idx => { tic: death_start_tic, prefix: sprite_prefix }
|
|
64
|
+
@tic = 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr_reader :dead_things
|
|
68
|
+
|
|
69
|
+
def dead?(thing_idx)
|
|
70
|
+
@dead_things.key?(thing_idx)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get the current death frame sprite for a dead monster
|
|
74
|
+
def death_sprite(thing_idx, thing_type, viewer_angle, thing_angle)
|
|
75
|
+
info = @dead_things[thing_idx]
|
|
76
|
+
return nil unless info
|
|
77
|
+
|
|
78
|
+
frames = DEATH_FRAMES[info[:prefix]]
|
|
79
|
+
return nil unless frames
|
|
80
|
+
|
|
81
|
+
elapsed = @tic - info[:tic]
|
|
82
|
+
frame_idx = (elapsed / DEATH_ANIM_TICS).clamp(0, frames.size - 1)
|
|
83
|
+
frame_letter = frames[frame_idx]
|
|
84
|
+
|
|
85
|
+
@sprites.get_frame(thing_type, frame_letter, viewer_angle, thing_angle)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Called each game tic
|
|
89
|
+
def update
|
|
90
|
+
@tic += 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Fire the current weapon
|
|
94
|
+
def fire(px, py, pz, cos_a, sin_a, weapon)
|
|
95
|
+
case weapon
|
|
96
|
+
when PlayerState::WEAPON_PISTOL, PlayerState::WEAPON_CHAINGUN
|
|
97
|
+
hitscan(px, py, cos_a, sin_a, 1, 0.0, 5)
|
|
98
|
+
when PlayerState::WEAPON_SHOTGUN
|
|
99
|
+
hitscan(px, py, cos_a, sin_a, 7, Math::PI / 32, 5)
|
|
100
|
+
when PlayerState::WEAPON_FIST
|
|
101
|
+
melee(px, py, cos_a, sin_a, 2, 64)
|
|
102
|
+
when PlayerState::WEAPON_CHAINSAW
|
|
103
|
+
melee(px, py, cos_a, sin_a, 2, 64)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def hitscan(px, py, cos_a, sin_a, pellets, spread, multiplier)
|
|
110
|
+
pellets.times do
|
|
111
|
+
# Add random spread
|
|
112
|
+
if spread > 0
|
|
113
|
+
angle = Math.atan2(sin_a, cos_a) + (rand - 0.5) * spread * 2
|
|
114
|
+
ca = Math.cos(angle)
|
|
115
|
+
sa = Math.sin(angle)
|
|
116
|
+
else
|
|
117
|
+
# Slight pistol/chaingun spread
|
|
118
|
+
angle = Math.atan2(sin_a, cos_a) + (rand - 0.5) * 0.04
|
|
119
|
+
ca = Math.cos(angle)
|
|
120
|
+
sa = Math.sin(angle)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
wall_dist = trace_wall(px, py, ca, sa)
|
|
124
|
+
|
|
125
|
+
best_idx = nil
|
|
126
|
+
best_dist = wall_dist
|
|
127
|
+
|
|
128
|
+
@map.things.each_with_index do |thing, idx|
|
|
129
|
+
next unless MONSTER_HP[thing.type]
|
|
130
|
+
next if @dead_things[idx]
|
|
131
|
+
|
|
132
|
+
radius = MONSTER_RADIUS[thing.type] || 20
|
|
133
|
+
hit_dist = ray_circle_hit(px, py, ca, sa, thing.x, thing.y, radius)
|
|
134
|
+
if hit_dist && hit_dist > 0 && hit_dist < best_dist
|
|
135
|
+
best_dist = hit_dist
|
|
136
|
+
best_idx = idx
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if best_idx
|
|
141
|
+
damage = (rand(3) + 1) * multiplier
|
|
142
|
+
apply_damage(best_idx, damage)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def melee(px, py, cos_a, sin_a, multiplier, range)
|
|
148
|
+
best_idx = nil
|
|
149
|
+
best_dist = range.to_f
|
|
150
|
+
|
|
151
|
+
@map.things.each_with_index do |thing, idx|
|
|
152
|
+
next unless MONSTER_HP[thing.type]
|
|
153
|
+
next if @dead_things[idx]
|
|
154
|
+
|
|
155
|
+
dx = thing.x - px
|
|
156
|
+
dy = thing.y - py
|
|
157
|
+
dist = Math.sqrt(dx * dx + dy * dy)
|
|
158
|
+
next if dist > range + (MONSTER_RADIUS[thing.type] || 20)
|
|
159
|
+
|
|
160
|
+
# Check if roughly facing the monster
|
|
161
|
+
dot = dx * cos_a + dy * sin_a
|
|
162
|
+
next if dot < 0
|
|
163
|
+
|
|
164
|
+
if dist < best_dist
|
|
165
|
+
best_dist = dist
|
|
166
|
+
best_idx = idx
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if best_idx
|
|
171
|
+
damage = (rand(3) + 1) * multiplier
|
|
172
|
+
apply_damage(best_idx, damage)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def apply_damage(thing_idx, damage)
|
|
177
|
+
thing = @map.things[thing_idx]
|
|
178
|
+
@monster_hp[thing_idx] ||= MONSTER_HP[thing.type] || 20
|
|
179
|
+
|
|
180
|
+
@monster_hp[thing_idx] -= damage
|
|
181
|
+
|
|
182
|
+
if @monster_hp[thing_idx] <= 0
|
|
183
|
+
prefix = @sprites.prefix_for(thing.type)
|
|
184
|
+
@dead_things[thing_idx] = { tic: @tic, prefix: prefix } if prefix
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def trace_wall(px, py, cos_a, sin_a)
|
|
189
|
+
best_t = 4096.0 # Max hitscan range
|
|
190
|
+
|
|
191
|
+
@map.linedefs.each do |ld|
|
|
192
|
+
v1 = @map.vertices[ld.v1]
|
|
193
|
+
v2 = @map.vertices[ld.v2]
|
|
194
|
+
|
|
195
|
+
# One-sided always blocks; two-sided only if impassable
|
|
196
|
+
blocks = (ld.sidedef_left == 0xFFFF) || (ld.flags & 0x0001 != 0)
|
|
197
|
+
unless blocks
|
|
198
|
+
next unless ld.sidedef_left < 0xFFFF
|
|
199
|
+
front = @map.sidedefs[ld.sidedef_right]
|
|
200
|
+
back = @map.sidedefs[ld.sidedef_left]
|
|
201
|
+
fs = @map.sectors[front.sector]
|
|
202
|
+
bs = @map.sectors[back.sector]
|
|
203
|
+
# Blocks if opening is too small (step or low ceiling)
|
|
204
|
+
max_floor = [fs.floor_height, bs.floor_height].max
|
|
205
|
+
min_ceil = [fs.ceiling_height, bs.ceiling_height].min
|
|
206
|
+
blocks = (min_ceil - max_floor) < 56
|
|
207
|
+
end
|
|
208
|
+
next unless blocks
|
|
209
|
+
|
|
210
|
+
t = ray_segment_intersect(px, py, cos_a, sin_a,
|
|
211
|
+
v1.x, v1.y, v2.x, v2.y)
|
|
212
|
+
best_t = t if t && t > 0 && t < best_t
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
best_t
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def ray_segment_intersect(px, py, dx, dy, x1, y1, x2, y2)
|
|
219
|
+
sx = x2 - x1
|
|
220
|
+
sy = y2 - y1
|
|
221
|
+
denom = dx * sy - dy * sx
|
|
222
|
+
return nil if denom.abs < 0.001
|
|
223
|
+
|
|
224
|
+
t = ((x1 - px) * sy - (y1 - py) * sx) / denom
|
|
225
|
+
u = ((x1 - px) * dy - (y1 - py) * dx) / denom
|
|
226
|
+
|
|
227
|
+
(t > 0 && u >= 0.0 && u <= 1.0) ? t : nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def ray_circle_hit(px, py, cos_a, sin_a, cx, cy, radius)
|
|
231
|
+
dx = cx - px
|
|
232
|
+
dy = cy - py
|
|
233
|
+
proj = dx * cos_a + dy * sin_a
|
|
234
|
+
return nil if proj < 0
|
|
235
|
+
|
|
236
|
+
perp_sq = dx * dx + dy * dy - proj * proj
|
|
237
|
+
return nil if perp_sq > radius * radius
|
|
238
|
+
|
|
239
|
+
chord_half = Math.sqrt([radius * radius - perp_sq, 0].max)
|
|
240
|
+
proj - chord_half
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Handles item pickup when player touches things.
|
|
6
|
+
# Matches Chocolate Doom's P_TouchSpecialThing from p_inter.c.
|
|
7
|
+
class ItemPickup
|
|
8
|
+
PLAYER_RADIUS = 16.0
|
|
9
|
+
THING_RADIUS = 20.0
|
|
10
|
+
PICKUP_DIST = PLAYER_RADIUS + THING_RADIUS # 36 units (bounding box overlap)
|
|
11
|
+
|
|
12
|
+
# Item definitions: type => { category, value, ... }
|
|
13
|
+
ITEMS = {
|
|
14
|
+
# Weapons (give weapon + some ammo)
|
|
15
|
+
2001 => { cat: :weapon, weapon: 2, ammo: :shells, amount: 8 }, # Shotgun
|
|
16
|
+
2002 => { cat: :weapon, weapon: 3, ammo: :bullets, amount: 20 }, # Chaingun
|
|
17
|
+
2003 => { cat: :weapon, weapon: 4, ammo: :rockets, amount: 2 }, # Rocket launcher
|
|
18
|
+
2004 => { cat: :weapon, weapon: 5, ammo: :cells, amount: 40 }, # Plasma rifle
|
|
19
|
+
2006 => { cat: :weapon, weapon: 6, ammo: :cells, amount: 40 }, # BFG9000
|
|
20
|
+
2005 => { cat: :weapon, weapon: 7, ammo: :bullets, amount: 0 }, # Chainsaw
|
|
21
|
+
|
|
22
|
+
# Ammo
|
|
23
|
+
2007 => { cat: :ammo, ammo: :bullets, amount: 10 }, # Clip
|
|
24
|
+
2048 => { cat: :ammo, ammo: :bullets, amount: 50 }, # Box of bullets
|
|
25
|
+
2008 => { cat: :ammo, ammo: :shells, amount: 4 }, # 4 shells
|
|
26
|
+
2049 => { cat: :ammo, ammo: :shells, amount: 20 }, # Box of shells
|
|
27
|
+
2010 => { cat: :ammo, ammo: :rockets, amount: 1 }, # Rocket
|
|
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)
|
|
32
|
+
|
|
33
|
+
# Health
|
|
34
|
+
2014 => { cat: :health, amount: 1, max: 200 }, # Health bonus (+1, up to 200)
|
|
35
|
+
2011 => { cat: :health, amount: 10, max: 100 }, # Stimpack
|
|
36
|
+
2012 => { cat: :health, amount: 25, max: 100 }, # Medikit
|
|
37
|
+
2013 => { cat: :health, amount: 100, max: 200 }, # Soul sphere
|
|
38
|
+
|
|
39
|
+
# Armor
|
|
40
|
+
2015 => { cat: :armor, amount: 1, max: 200 }, # Armor bonus (+1, up to 200)
|
|
41
|
+
2018 => { cat: :armor, amount: 100, armor_type: 1 }, # Green armor (100%, absorbs 1/3)
|
|
42
|
+
2019 => { cat: :armor, amount: 200, armor_type: 2 }, # Blue armor (200%, absorbs 1/2)
|
|
43
|
+
|
|
44
|
+
# Keys
|
|
45
|
+
5 => { cat: :key, key: :blue_card },
|
|
46
|
+
6 => { cat: :key, key: :yellow_card },
|
|
47
|
+
13 => { cat: :key, key: :red_card },
|
|
48
|
+
40 => { cat: :key, key: :blue_skull },
|
|
49
|
+
39 => { cat: :key, key: :yellow_skull },
|
|
50
|
+
38 => { cat: :key, key: :red_skull },
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
attr_reader :picked_up
|
|
54
|
+
|
|
55
|
+
def initialize(map, player_state)
|
|
56
|
+
@map = map
|
|
57
|
+
@player = player_state
|
|
58
|
+
@picked_up = {} # thing index => true (to avoid re-picking)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update(player_x, player_y)
|
|
62
|
+
@map.things.each_with_index do |thing, idx|
|
|
63
|
+
next if @picked_up[idx]
|
|
64
|
+
item = ITEMS[thing.type]
|
|
65
|
+
next unless item
|
|
66
|
+
|
|
67
|
+
# DOOM uses bounding box overlap: abs(dx) < sum_of_radii
|
|
68
|
+
dx = (player_x - thing.x).abs
|
|
69
|
+
dy = (player_y - thing.y).abs
|
|
70
|
+
next if dx >= PICKUP_DIST || dy >= PICKUP_DIST
|
|
71
|
+
|
|
72
|
+
if try_pickup(item)
|
|
73
|
+
@picked_up[idx] = true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def try_pickup(item)
|
|
81
|
+
case item[:cat]
|
|
82
|
+
when :weapon
|
|
83
|
+
give_weapon(item)
|
|
84
|
+
when :ammo
|
|
85
|
+
give_ammo(item[:ammo], item[:amount])
|
|
86
|
+
when :backpack
|
|
87
|
+
give_backpack
|
|
88
|
+
when :health
|
|
89
|
+
give_health(item[:amount], item[:max])
|
|
90
|
+
when :armor
|
|
91
|
+
give_armor(item)
|
|
92
|
+
when :key
|
|
93
|
+
give_key(item[:key])
|
|
94
|
+
else
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def give_weapon(item)
|
|
100
|
+
weapon_idx = item[:weapon]
|
|
101
|
+
had_weapon = @player.has_weapons[weapon_idx]
|
|
102
|
+
|
|
103
|
+
@player.has_weapons[weapon_idx] = true
|
|
104
|
+
ammo_given = item[:ammo] ? give_ammo(item[:ammo], item[:amount]) : false
|
|
105
|
+
|
|
106
|
+
unless had_weapon
|
|
107
|
+
@player.switch_weapon(weapon_idx) unless @player.attacking
|
|
108
|
+
return true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
ammo_given
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def give_ammo(type, amount)
|
|
115
|
+
case type
|
|
116
|
+
when :bullets
|
|
117
|
+
return false if @player.ammo_bullets >= @player.max_bullets
|
|
118
|
+
@player.ammo_bullets = [@player.ammo_bullets + amount, @player.max_bullets].min
|
|
119
|
+
when :shells
|
|
120
|
+
return false if @player.ammo_shells >= @player.max_shells
|
|
121
|
+
@player.ammo_shells = [@player.ammo_shells + amount, @player.max_shells].min
|
|
122
|
+
when :rockets
|
|
123
|
+
return false if @player.ammo_rockets >= @player.max_rockets
|
|
124
|
+
@player.ammo_rockets = [@player.ammo_rockets + amount, @player.max_rockets].min
|
|
125
|
+
when :cells
|
|
126
|
+
return false if @player.ammo_cells >= @player.max_cells
|
|
127
|
+
@player.ammo_cells = [@player.ammo_cells + amount, @player.max_cells].min
|
|
128
|
+
end
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def give_backpack
|
|
133
|
+
@player.max_bullets = 400
|
|
134
|
+
@player.max_shells = 100
|
|
135
|
+
@player.max_rockets = 100
|
|
136
|
+
@player.max_cells = 600
|
|
137
|
+
give_ammo(:bullets, 10)
|
|
138
|
+
give_ammo(:shells, 4)
|
|
139
|
+
give_ammo(:rockets, 1)
|
|
140
|
+
give_ammo(:cells, 20)
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def give_health(amount, max)
|
|
145
|
+
return false if @player.health >= max
|
|
146
|
+
@player.health = [@player.health + amount, max].min
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def give_armor(item)
|
|
151
|
+
if item[:armor_type]
|
|
152
|
+
# Green/blue armor: only pick up if better than current
|
|
153
|
+
return false if @player.armor >= item[:amount]
|
|
154
|
+
@player.armor = item[:amount]
|
|
155
|
+
else
|
|
156
|
+
# Armor bonus: +1, up to max
|
|
157
|
+
return false if @player.armor >= item[:max]
|
|
158
|
+
@player.armor = [@player.armor + item[:amount], item[:max]].min
|
|
159
|
+
end
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def give_key(key)
|
|
164
|
+
return false if @player.keys[key]
|
|
165
|
+
@player.keys[key] = true
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|