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.
- checksums.yaml +4 -4
- data/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +134 -24
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +239 -153
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +22 -39
- data/lib/doom/game/item_pickup.rb +87 -75
- data/lib/doom/game/menu.rb +90 -85
- data/lib/doom/game/monster_ai.rb +150 -136
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +38 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +169 -93
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +33 -58
- data/lib/doom/game/state_hash.rb +125 -0
- data/lib/doom/game/ticcmd.rb +61 -0
- data/lib/doom/game/world.rb +420 -0
- data/lib/doom/map/data.rb +95 -0
- data/lib/doom/net/client.rb +204 -0
- data/lib/doom/net/desync_monitor.rb +101 -0
- data/lib/doom/net/game_server.rb +232 -0
- data/lib/doom/net/lockstep.rb +170 -0
- data/lib/doom/net/protocol.rb +350 -0
- data/lib/doom/net/session.rb +282 -0
- data/lib/doom/net/transport.rb +110 -0
- data/lib/doom/platform/gosu_window.rb +604 -832
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +5 -3
- data/lib/doom/render/hardware_renderer.rb +547 -0
- data/lib/doom/render/ray_tracing/bvh.rb +103 -0
- data/lib/doom/render/ray_tracing/material_state.rb +66 -0
- data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
- data/lib/doom/render/ray_tracing_renderer.rb +940 -0
- data/lib/doom/render/renderer.rb +466 -440
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +7 -7
- data/lib/doom/render/spinel_native/kernel.rb +780 -0
- data/lib/doom/render/spinel_native_renderer.rb +162 -0
- data/lib/doom/render/status_bar.rb +14 -10
- data/lib/doom/render/weapon_renderer.rb +9 -10
- data/lib/doom/render/world_mesh.rb +270 -0
- data/lib/doom/render/zbuffer_renderer.rb +151 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/flat.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +7 -3
- data/lib/doom/wad/palette.rb +3 -3
- data/lib/doom/wad/patch.rb +1 -1
- data/lib/doom/wad/reader.rb +12 -15
- data/lib/doom/wad/sound.rb +14 -6
- data/lib/doom/wad/sprite.rb +36 -35
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +197 -16
- metadata +94 -7
data/lib/doom/game/combat.rb
CHANGED
|
@@ -2,41 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
module Doom
|
|
4
4
|
module Game
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Combat resolution for every weapon and every damageable thing: hitscan
|
|
6
|
+
# (pistol/shotgun/chaingun), melee (fist/chainsaw), rockets and their splash,
|
|
7
|
+
# monster fireballs, barrel chain reactions, player-vs-player damage and frag
|
|
8
|
+
# scoring. Hitscan/aim matches Chocolate Doom's P_LineAttack / P_AimLineAttack
|
|
9
|
+
# (p_map.c); splash matches P_RadiusAttack (p_inter.c).
|
|
7
10
|
class Combat
|
|
8
11
|
# Monster starting HP (from mobjinfo[] in info.c)
|
|
9
12
|
MONSTER_HP = {
|
|
10
|
-
3004 => 20,
|
|
11
|
-
9
|
|
13
|
+
3004 => 20, # Zombieman
|
|
14
|
+
9 => 30, # Shotgun Guy
|
|
12
15
|
3001 => 60, # Imp
|
|
13
16
|
3002 => 150, # Demon
|
|
14
|
-
58
|
|
17
|
+
58 => 150, # Spectre
|
|
15
18
|
3003 => 1000, # Baron of Hell
|
|
16
|
-
69
|
|
19
|
+
69 => 500, # Hell Knight
|
|
17
20
|
3005 => 400, # Cacodemon
|
|
18
21
|
3006 => 100, # Lost Soul
|
|
19
|
-
16
|
|
20
|
-
7
|
|
21
|
-
65
|
|
22
|
-
64
|
|
23
|
-
71
|
|
24
|
-
84
|
|
25
|
-
2035 => 20
|
|
22
|
+
16 => 4000, # Cyberdemon
|
|
23
|
+
7 => 3000, # Spider Mastermind
|
|
24
|
+
65 => 70, # Heavy Weapon Dude
|
|
25
|
+
64 => 700, # Archvile
|
|
26
|
+
71 => 400, # Pain Elemental
|
|
27
|
+
84 => 20, # Wolfenstein SS
|
|
28
|
+
2035 => 20 # Explosive barrel
|
|
26
29
|
}.freeze
|
|
27
30
|
|
|
28
31
|
MONSTER_RADIUS = {
|
|
29
32
|
3004 => 20, 9 => 20, 3001 => 20, 3002 => 30, 58 => 30,
|
|
30
33
|
3003 => 24, 69 => 24, 3005 => 31, 3006 => 16, 16 => 40,
|
|
31
34
|
7 => 128, 65 => 20, 64 => 20, 71 => 31, 84 => 20,
|
|
32
|
-
2035 => 10
|
|
35
|
+
2035 => 10 # Barrel
|
|
33
36
|
}.freeze
|
|
34
37
|
|
|
35
|
-
# Barrel (explosive, not a monster but damageable)
|
|
38
|
+
# Barrel (explosive, not a monster but damageable). Its HP comes from
|
|
39
|
+
# MONSTER_HP[BARREL_TYPE]; its explosion reuses the shared SPLASH_* values.
|
|
36
40
|
BARREL_TYPE = 2035
|
|
37
|
-
BARREL_HP = 20
|
|
38
|
-
BARREL_SPLASH_RADIUS = 128.0
|
|
39
|
-
BARREL_SPLASH_DAMAGE = 128
|
|
40
41
|
|
|
41
42
|
# Normal death frame sequences per sprite prefix (rotation 0 only)
|
|
42
43
|
# Identified by sprite heights: frames go from standing height to flat on ground
|
|
@@ -54,18 +55,18 @@ module Doom
|
|
|
54
55
|
'CPOS' => %w[H I J K L M N], # Heavy Weapon Dude
|
|
55
56
|
'PAIN' => %w[H I J K L M], # Pain Elemental
|
|
56
57
|
'SSWV' => %w[I J K L M], # Wolfenstein SS
|
|
57
|
-
'BEXP' => %w[A B C D E]
|
|
58
|
+
'BEXP' => %w[A B C D E] # Barrel explosion
|
|
58
59
|
}.freeze
|
|
59
60
|
|
|
60
|
-
DEATH_ANIM_TICS = 6
|
|
61
|
+
DEATH_ANIM_TICS = 6 # Tics per death frame
|
|
61
62
|
|
|
62
63
|
# Pain chance per monster (out of 256, from mobjinfo)
|
|
63
64
|
PAIN_CHANCE = {
|
|
64
65
|
3004 => 200, 9 => 170, 3001 => 200, 3002 => 180, 58 => 180,
|
|
65
66
|
3003 => 50, 69 => 50, 3005 => 128, 3006 => 256, 16 => 40,
|
|
66
|
-
7 => 40, 65 => 170, 64 => 10, 71 => 128, 84 => 170
|
|
67
|
+
7 => 40, 65 => 170, 64 => 10, 71 => 128, 84 => 170
|
|
67
68
|
}.freeze
|
|
68
|
-
PAIN_DURATION = 6
|
|
69
|
+
PAIN_DURATION = 6 # Tics monster is stunned when in pain
|
|
69
70
|
|
|
70
71
|
# Projectile constants
|
|
71
72
|
ROCKET_SPEED = 20.0 # Map units per tic (matches DOOM's mobjinfo MISSILESPEED)
|
|
@@ -74,31 +75,43 @@ module Doom
|
|
|
74
75
|
SPLASH_RADIUS = 128.0 # Splash damage radius
|
|
75
76
|
SPLASH_DAMAGE = 128 # Max splash damage at center
|
|
76
77
|
|
|
77
|
-
# Monster projectile definitions
|
|
78
|
+
# Monster projectile definitions. :radius is the projectile's own collision
|
|
79
|
+
# radius, added to the player radius when testing a hit.
|
|
78
80
|
MONSTER_PROJECTILES = {
|
|
79
|
-
imp:
|
|
80
|
-
baron:
|
|
81
|
-
caco:
|
|
81
|
+
imp: { sprite: 'BAL1', speed: 10.0, damage: [3, 24], radius: 6 },
|
|
82
|
+
baron: { sprite: 'BAL7', speed: 15.0, damage: [8, 64], radius: 6 },
|
|
83
|
+
caco: { sprite: 'BAL2', speed: 10.0, damage: [5, 40], radius: 6 }
|
|
82
84
|
}.freeze
|
|
83
85
|
|
|
84
86
|
# Map monster type to projectile type
|
|
85
87
|
MONSTER_PROJECTILE_TYPE = {
|
|
86
88
|
3001 => :imp, # Imp
|
|
87
89
|
3003 => :baron, # Baron
|
|
88
|
-
69
|
|
89
|
-
3005 => :caco
|
|
90
|
+
69 => :baron, # Hell Knight
|
|
91
|
+
3005 => :caco # Cacodemon
|
|
90
92
|
}.freeze
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
# Radius used when a shot or a rocket is tested against a player. Matches
|
|
95
|
+
# the 16 the monster-projectile collision already assumed.
|
|
96
|
+
PLAYER_RADIUS = 16
|
|
97
|
+
|
|
98
|
+
# `owner` is the Player who fired, so a kill can be credited. Monster
|
|
99
|
+
# projectiles leave it nil: DOOM gives no frag for a death by monster.
|
|
100
|
+
# `damage_multiplier` scales the damage a monster fireball deals on impact,
|
|
101
|
+
# so a skill's damage factor reaches the fireball it spawned. Player
|
|
102
|
+
# projectiles (rockets) carry 1.0.
|
|
103
|
+
Projectile = Struct.new(:x, :y, :z, :dx, :dy, :dz, :type, :spawn_tic, :sprite_prefix,
|
|
104
|
+
:target, :owner, :damage_multiplier)
|
|
93
105
|
|
|
94
106
|
# Weapon damage: DOOM does (P_Random()%3 + 1) * multiplier
|
|
95
107
|
# Pistol/chaingun: 1*5..3*5 = 5-15 per bullet
|
|
96
108
|
# Shotgun: 7 pellets, each 1*5..3*5 = 5-15
|
|
97
109
|
# Fist/chainsaw: 1*2..3*2 = 2-10
|
|
98
110
|
|
|
99
|
-
def initialize(map,
|
|
111
|
+
def initialize(map, sprites = nil, hidden_things = {}, sound_engine = nil,
|
|
112
|
+
random: Random.new)
|
|
100
113
|
@map = map
|
|
101
|
-
@
|
|
114
|
+
@random = random
|
|
102
115
|
@sprites = sprites
|
|
103
116
|
@hidden_things = hidden_things
|
|
104
117
|
@sound = sound_engine
|
|
@@ -108,30 +121,26 @@ module Doom
|
|
|
108
121
|
@projectiles = [] # Active projectiles in flight
|
|
109
122
|
@explosions = [] # Active explosions (for rendering)
|
|
110
123
|
@puffs = [] # Bullet puff effects
|
|
111
|
-
@
|
|
112
|
-
@player_y = 0.0
|
|
113
|
-
@player_z = 0.0
|
|
124
|
+
@players = []
|
|
114
125
|
@tic = 0
|
|
115
126
|
end
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@player_z = z if z
|
|
121
|
-
end
|
|
128
|
+
# Every player the world knows about. Projectile collision, splash damage
|
|
129
|
+
# and monster aim all consider all of them, not one bound player.
|
|
130
|
+
attr_accessor :players
|
|
122
131
|
|
|
123
132
|
# Spawn a monster projectile (fireball, etc.)
|
|
124
133
|
# Matches Chocolate Doom's P_SpawnMissile: calculates momz for vertical aim
|
|
125
|
-
def spawn_monster_projectile(monster_x, monster_y, monster_z, monster_type, damage_multiplier)
|
|
134
|
+
def spawn_monster_projectile(monster_x, monster_y, monster_z, monster_type, damage_multiplier, target)
|
|
126
135
|
proj_type = MONSTER_PROJECTILE_TYPE[monster_type]
|
|
127
136
|
return unless proj_type
|
|
128
137
|
|
|
129
138
|
info = MONSTER_PROJECTILES[proj_type]
|
|
130
139
|
return unless info
|
|
131
140
|
|
|
132
|
-
dx =
|
|
133
|
-
dy =
|
|
134
|
-
dist = Math.sqrt(dx * dx + dy * dy)
|
|
141
|
+
dx = target.x - monster_x
|
|
142
|
+
dy = target.y - monster_y
|
|
143
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
135
144
|
return if dist < 1
|
|
136
145
|
|
|
137
146
|
# Normalize direction and apply speed
|
|
@@ -141,18 +150,18 @@ module Doom
|
|
|
141
150
|
|
|
142
151
|
# P_SpawnMissile: momz = (target.z - source.z) / (dist / speed)
|
|
143
152
|
# This makes the projectile arc toward the target's height
|
|
144
|
-
target_z =
|
|
153
|
+
target_z = target.z - 16 # Aim at player center (z + height/2, roughly)
|
|
145
154
|
travel_tics = dist / speed
|
|
146
155
|
travel_tics = 1.0 if travel_tics < 1.0
|
|
147
156
|
ndz = (target_z - monster_z) / travel_tics
|
|
148
157
|
|
|
149
158
|
@projectiles << Projectile.new(
|
|
150
|
-
monster_x + ndx * 2, monster_y + ndy * 2, monster_z,
|
|
151
|
-
ndx, ndy, ndz, proj_type, @tic, info[:sprite], :player
|
|
159
|
+
monster_x + (ndx * 2), monster_y + (ndy * 2), monster_z,
|
|
160
|
+
ndx, ndy, ndz, proj_type, @tic, info[:sprite], :player, nil, damage_multiplier
|
|
152
161
|
)
|
|
153
162
|
end
|
|
154
163
|
|
|
155
|
-
attr_reader :dead_things, :projectiles, :explosions, :puffs
|
|
164
|
+
attr_reader :dead_things, :projectiles, :explosions, :puffs, :sprites
|
|
156
165
|
|
|
157
166
|
def in_pain?(thing_idx)
|
|
158
167
|
@pain_until[thing_idx] && @tic < @pain_until[thing_idx]
|
|
@@ -175,19 +184,17 @@ module Doom
|
|
|
175
184
|
frame_idx = elapsed / DEATH_ANIM_TICS
|
|
176
185
|
|
|
177
186
|
# Barrels disappear after explosion animation (S_NULL in Chocolate Doom)
|
|
178
|
-
if thing_type == BARREL_TYPE && frame_idx >= frames.size
|
|
179
|
-
return nil
|
|
180
|
-
end
|
|
187
|
+
return nil if thing_type == BARREL_TYPE && frame_idx >= frames.size
|
|
181
188
|
|
|
182
189
|
frame_idx = frame_idx.clamp(0, frames.size - 1)
|
|
183
190
|
frame_letter = frames[frame_idx]
|
|
184
191
|
|
|
185
192
|
# Use prefix directly if it differs from the thing's sprite (e.g. BEXP for barrels)
|
|
186
193
|
thing_prefix = @sprites.prefix_for(thing_type)
|
|
187
|
-
if prefix
|
|
188
|
-
@sprites.get_frame_by_prefix(prefix, frame_letter)
|
|
189
|
-
else
|
|
194
|
+
if prefix == thing_prefix
|
|
190
195
|
@sprites.get_frame(thing_type, frame_letter, viewer_angle, thing_angle)
|
|
196
|
+
else
|
|
197
|
+
@sprites.get_frame_by_prefix(prefix, frame_letter)
|
|
191
198
|
end
|
|
192
199
|
end
|
|
193
200
|
|
|
@@ -199,32 +206,71 @@ module Doom
|
|
|
199
206
|
@puffs.reject! { |p| @tic - p[:tic] > 12 }
|
|
200
207
|
end
|
|
201
208
|
|
|
202
|
-
# Fire the current weapon
|
|
203
|
-
|
|
209
|
+
# Fire the current weapon. `shooter` is the Player pulling the trigger,
|
|
210
|
+
# needed to credit a deathmatch kill and to stop a shot from hitting the
|
|
211
|
+
# person who fired it.
|
|
212
|
+
def fire(px, py, pz, cos_a, sin_a, weapon, shooter = nil)
|
|
204
213
|
case weapon
|
|
205
214
|
when PlayerState::WEAPON_PISTOL, PlayerState::WEAPON_CHAINGUN
|
|
206
|
-
hitscan(px, py, cos_a, sin_a, 1, 0.0, 5)
|
|
215
|
+
hitscan(px, py, pz, cos_a, sin_a, 1, 0.0, 5, shooter)
|
|
207
216
|
when PlayerState::WEAPON_SHOTGUN
|
|
208
|
-
hitscan(px, py, cos_a, sin_a, 7, Math::PI / 32, 5)
|
|
217
|
+
hitscan(px, py, pz, cos_a, sin_a, 7, Math::PI / 32, 5, shooter)
|
|
209
218
|
when PlayerState::WEAPON_ROCKET
|
|
210
|
-
spawn_rocket(px, py, pz, cos_a, sin_a)
|
|
219
|
+
spawn_rocket(px, py, pz, cos_a, sin_a, shooter)
|
|
211
220
|
when PlayerState::WEAPON_FIST
|
|
212
|
-
melee(px, py, cos_a, sin_a, 2, 64)
|
|
221
|
+
melee(px, py, cos_a, sin_a, 2, 64, shooter)
|
|
213
222
|
when PlayerState::WEAPON_CHAINSAW
|
|
214
|
-
melee(px, py, cos_a, sin_a, 2, 64)
|
|
223
|
+
melee(px, py, cos_a, sin_a, 2, 64, shooter)
|
|
215
224
|
end
|
|
216
225
|
end
|
|
217
226
|
|
|
218
227
|
private
|
|
219
228
|
|
|
220
|
-
def spawn_rocket(px, py, pz, cos_a, sin_a)
|
|
229
|
+
def spawn_rocket(px, py, pz, cos_a, sin_a, shooter = nil)
|
|
221
230
|
@projectiles << Projectile.new(
|
|
222
|
-
px + cos_a * 20, py + sin_a * 20, pz,
|
|
231
|
+
px + (cos_a * 20), py + (sin_a * 20), pz,
|
|
223
232
|
cos_a * ROCKET_SPEED, sin_a * ROCKET_SPEED, 0.0,
|
|
224
|
-
:rocket, @tic, 'MISL', :monsters
|
|
233
|
+
:rocket, @tic, 'MISL', :monsters, shooter, 1.0
|
|
225
234
|
)
|
|
226
235
|
end
|
|
227
236
|
|
|
237
|
+
# Players a shot fired by `shooter` may hit: everyone alive except the
|
|
238
|
+
# shooter, who cannot hit themselves with a bullet or a fist.
|
|
239
|
+
#
|
|
240
|
+
# Not gated on the mode. Vanilla DOOM has no friendly-fire switch --
|
|
241
|
+
# P_DamageMobj checks nothing about who is shooting whom -- so co-op
|
|
242
|
+
# players hurt each other exactly as deathmatch ones do. In single player
|
|
243
|
+
# this is empty anyway, the only player being the shooter.
|
|
244
|
+
def player_targets(shooter)
|
|
245
|
+
@players.reject { |pl| pl.state.dead || pl.equal?(shooter) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Damage a player and score the kill in the same place, so no damage path
|
|
249
|
+
# can quietly forget to. `attacker` is the Player responsible, or nil when
|
|
250
|
+
# a monster or the level itself did it.
|
|
251
|
+
def damage_player(victim, damage, attacker)
|
|
252
|
+
return if damage <= 0
|
|
253
|
+
return if victim.state.dead
|
|
254
|
+
|
|
255
|
+
victim.state.take_damage(damage)
|
|
256
|
+
return unless victim.state.dead
|
|
257
|
+
|
|
258
|
+
record_frag(victim, attacker)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# DOOM's scoring: killing someone else is worth a frag, killing yourself
|
|
262
|
+
# costs one, and being killed by a monster or the environment is worth
|
|
263
|
+
# nothing either way.
|
|
264
|
+
def record_frag(victim, attacker)
|
|
265
|
+
return unless attacker
|
|
266
|
+
|
|
267
|
+
if attacker.equal?(victim)
|
|
268
|
+
victim.frags -= 1
|
|
269
|
+
else
|
|
270
|
+
attacker.frags += 1
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
228
274
|
def update_projectiles
|
|
229
275
|
@projectiles.reject! do |proj|
|
|
230
276
|
new_x = proj.x + proj.dx
|
|
@@ -235,9 +281,7 @@ module Doom
|
|
|
235
281
|
|
|
236
282
|
# Check if projectile hit the floor or ceiling
|
|
237
283
|
sector = @map.sector_at(new_x, new_y)
|
|
238
|
-
if sector
|
|
239
|
-
hit_wall = true if new_z <= sector.floor_height || new_z >= sector.ceiling_height
|
|
240
|
-
end
|
|
284
|
+
hit_wall = true if sector && (new_z <= sector.floor_height || new_z >= sector.ceiling_height)
|
|
241
285
|
hit = false
|
|
242
286
|
|
|
243
287
|
if proj.target == :monsters
|
|
@@ -246,32 +290,52 @@ module Doom
|
|
|
246
290
|
@map.things.each_with_index do |thing, idx|
|
|
247
291
|
next unless MONSTER_HP[thing.type]
|
|
248
292
|
next if @dead_things[idx]
|
|
293
|
+
|
|
249
294
|
radius = (MONSTER_RADIUS[thing.type] || 20) + ROCKET_RADIUS
|
|
250
295
|
dx = new_x - thing.x
|
|
251
296
|
dy = new_y - thing.y
|
|
252
|
-
if dx * dx + dy * dy < radius * radius
|
|
297
|
+
if (dx * dx) + (dy * dy) < radius * radius
|
|
253
298
|
hit_monster = idx
|
|
254
299
|
break
|
|
255
300
|
end
|
|
256
301
|
end
|
|
257
302
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
303
|
+
# In deathmatch a rocket also detonates on a player. There is no
|
|
304
|
+
# separate direct-hit damage: exploding on top of someone already
|
|
305
|
+
# deals full splash, which is close enough to DOOM's direct hit and
|
|
306
|
+
# keeps the RNG draws the same as any other detonation.
|
|
307
|
+
hit_player = player_targets(proj.owner).find do |pl|
|
|
308
|
+
radius = PLAYER_RADIUS + ROCKET_RADIUS
|
|
309
|
+
dx = new_x - pl.x
|
|
310
|
+
dy = new_y - pl.y
|
|
311
|
+
(dx * dx) + (dy * dy) < radius * radius
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
if hit_wall || hit_monster || hit_player
|
|
315
|
+
# Only rockets target :monsters, so the detonation is always a
|
|
316
|
+
# rocket explosion (direct hit plus splash).
|
|
317
|
+
explode(new_x, new_y, hit_monster, proj.owner)
|
|
261
318
|
hit = true
|
|
262
319
|
end
|
|
263
320
|
elsif proj.target == :player
|
|
264
|
-
# Monster projectile:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
321
|
+
# Monster projectile: hits whichever player it reaches first, not
|
|
322
|
+
# only the one it was aimed at -- a fireball meant for someone else
|
|
323
|
+
# still hurts if you walk into it.
|
|
324
|
+
info = MONSTER_PROJECTILES[proj.type]
|
|
325
|
+
hit_radius = PLAYER_RADIUS + (info ? info[:radius] : 6)
|
|
326
|
+
struck = @players.find do |pl|
|
|
327
|
+
next false if pl.state.dead
|
|
328
|
+
|
|
329
|
+
dx = new_x - pl.x
|
|
330
|
+
dy = new_y - pl.y
|
|
331
|
+
(dx * dx) + (dy * dy) < hit_radius**2
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
if hit_wall || struck
|
|
335
|
+
if !hit_wall && struck && info
|
|
336
|
+
min_d, max_d = info[:damage]
|
|
337
|
+
damage = (@random.rand(min_d..max_d) * (proj.damage_multiplier || 1.0)).to_i
|
|
338
|
+
damage_player(struck, damage, nil)
|
|
275
339
|
end
|
|
276
340
|
# Spawn fireball explosion
|
|
277
341
|
@explosions << { x: new_x, y: new_y, z: proj.z, tic: @tic, sprite: proj.sprite_prefix }
|
|
@@ -290,31 +354,53 @@ module Doom
|
|
|
290
354
|
end
|
|
291
355
|
end
|
|
292
356
|
|
|
293
|
-
def explode(x, y, direct_hit_idx)
|
|
357
|
+
def explode(x, y, direct_hit_idx, owner = nil)
|
|
294
358
|
# Direct hit damage
|
|
295
359
|
if direct_hit_idx
|
|
296
|
-
damage = (rand(8) + 1) * ROCKET_DAMAGE
|
|
360
|
+
damage = (@random.rand(8) + 1) * ROCKET_DAMAGE
|
|
297
361
|
apply_damage(direct_hit_idx, damage)
|
|
298
362
|
end
|
|
299
363
|
|
|
300
|
-
|
|
364
|
+
splash_monsters(x, y, SPLASH_RADIUS, SPLASH_DAMAGE, direct_hit_idx)
|
|
365
|
+
splash_players(x, y, SPLASH_RADIUS, SPLASH_DAMAGE, owner)
|
|
366
|
+
|
|
367
|
+
# Spawn explosion visual
|
|
368
|
+
@explosions << { x: x, y: y, tic: @tic, sprite: 'MISL' }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Splash damage to every monster (and barrel, for chain reactions) in
|
|
372
|
+
# radius, falling off linearly, skipping any dead thing and the excluded
|
|
373
|
+
# index (a rocket's direct-hit target or the barrel that just went off).
|
|
374
|
+
def splash_monsters(x, y, radius, max_damage, exclude_idx)
|
|
301
375
|
@map.things.each_with_index do |thing, idx|
|
|
302
376
|
next unless MONSTER_HP[thing.type]
|
|
303
377
|
next if @dead_things[idx]
|
|
304
|
-
next if idx ==
|
|
378
|
+
next if idx == exclude_idx
|
|
305
379
|
|
|
306
380
|
dx = x - thing.x
|
|
307
381
|
dy = y - thing.y
|
|
308
|
-
dist = Math.sqrt(dx * dx + dy * dy)
|
|
309
|
-
next if dist >=
|
|
382
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
383
|
+
next if dist >= radius
|
|
310
384
|
|
|
311
|
-
|
|
312
|
-
damage = ((SPLASH_DAMAGE * (1.0 - dist / SPLASH_RADIUS))).to_i
|
|
385
|
+
damage = (max_damage * (1.0 - (dist / radius))).to_i
|
|
313
386
|
apply_damage(idx, damage) if damage > 0
|
|
314
387
|
end
|
|
388
|
+
end
|
|
315
389
|
|
|
316
|
-
|
|
317
|
-
|
|
390
|
+
# Splash reaches the shooter too, which is how a rocket suicide happens.
|
|
391
|
+
#
|
|
392
|
+
# In every mode, single player included. PIT_RadiusAttack damages every
|
|
393
|
+
# shootable thing in range and never excludes the bomb source, which is
|
|
394
|
+
# why firing a rocket at your own feet has always been fatal in DOOM.
|
|
395
|
+
def splash_players(x, y, radius, max_damage, attacker)
|
|
396
|
+
@players.each do |pl|
|
|
397
|
+
next if pl.state.dead
|
|
398
|
+
|
|
399
|
+
dist = Math.hypot(x - pl.x, y - pl.y)
|
|
400
|
+
next unless dist < radius
|
|
401
|
+
|
|
402
|
+
damage_player(pl, (max_damage * (1.0 - (dist / radius))).to_i, attacker)
|
|
403
|
+
end
|
|
318
404
|
end
|
|
319
405
|
|
|
320
406
|
def update_explosions
|
|
@@ -336,7 +422,7 @@ module Doom
|
|
|
336
422
|
bs = @map.sectors[back.sector]
|
|
337
423
|
max_floor = [fs.floor_height, bs.floor_height].max
|
|
338
424
|
min_ceil = [fs.ceiling_height, bs.ceiling_height].min
|
|
339
|
-
blocks = (min_ceil - max_floor) < 1
|
|
425
|
+
blocks = (min_ceil - max_floor) < 1 # Closed door/wall
|
|
340
426
|
else
|
|
341
427
|
next
|
|
342
428
|
end
|
|
@@ -344,34 +430,21 @@ module Doom
|
|
|
344
430
|
|
|
345
431
|
v1 = @map.vertices[ld.v1]
|
|
346
432
|
v2 = @map.vertices[ld.v2]
|
|
347
|
-
if segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
348
|
-
return true
|
|
349
|
-
end
|
|
433
|
+
return true if Geometry.segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
350
434
|
end
|
|
351
435
|
false
|
|
352
436
|
end
|
|
353
437
|
|
|
354
|
-
def
|
|
355
|
-
d1x = ax2 - ax1; d1y = ay2 - ay1
|
|
356
|
-
d2x = bx2 - bx1; d2y = by2 - by1
|
|
357
|
-
denom = d1x * d2y - d1y * d2x
|
|
358
|
-
return false if denom.abs < 0.001
|
|
359
|
-
dx = bx1 - ax1; dy = by1 - ay1
|
|
360
|
-
t = (dx * d2y - dy * d2x).to_f / denom
|
|
361
|
-
u = (dx * d1y - dy * d1x).to_f / denom
|
|
362
|
-
t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def hitscan(px, py, cos_a, sin_a, pellets, spread, multiplier)
|
|
438
|
+
def hitscan(px, py, pz, cos_a, sin_a, pellets, spread, multiplier, shooter = nil)
|
|
366
439
|
pellets.times do
|
|
367
440
|
# Add random spread
|
|
368
441
|
if spread > 0
|
|
369
|
-
angle = Math.atan2(sin_a, cos_a) + (rand - 0.5) * spread * 2
|
|
442
|
+
angle = Math.atan2(sin_a, cos_a) + ((@random.rand - 0.5) * spread * 2)
|
|
370
443
|
ca = Math.cos(angle)
|
|
371
444
|
sa = Math.sin(angle)
|
|
372
445
|
else
|
|
373
446
|
# Slight pistol/chaingun spread
|
|
374
|
-
angle = Math.atan2(sin_a, cos_a) + (rand - 0.5) * 0.04
|
|
447
|
+
angle = Math.atan2(sin_a, cos_a) + ((@random.rand - 0.5) * 0.04)
|
|
375
448
|
ca = Math.cos(angle)
|
|
376
449
|
sa = Math.sin(angle)
|
|
377
450
|
end
|
|
@@ -379,6 +452,7 @@ module Doom
|
|
|
379
452
|
wall_dist = trace_wall(px, py, ca, sa)
|
|
380
453
|
|
|
381
454
|
best_idx = nil
|
|
455
|
+
best_player = nil
|
|
382
456
|
best_dist = wall_dist
|
|
383
457
|
|
|
384
458
|
@map.things.each_with_index do |thing, idx|
|
|
@@ -394,21 +468,36 @@ module Doom
|
|
|
394
468
|
end
|
|
395
469
|
end
|
|
396
470
|
|
|
471
|
+
# Players compete with monsters for the same ray: whoever is nearest
|
|
472
|
+
# takes the bullet, so you cannot shoot through someone to hit the
|
|
473
|
+
# imp behind them.
|
|
474
|
+
player_targets(shooter).each do |pl|
|
|
475
|
+
hit_dist = ray_circle_hit(px, py, ca, sa, pl.x, pl.y, PLAYER_RADIUS)
|
|
476
|
+
next unless hit_dist && hit_dist > 0 && hit_dist < best_dist
|
|
477
|
+
|
|
478
|
+
best_dist = hit_dist
|
|
479
|
+
best_player = pl
|
|
480
|
+
best_idx = nil
|
|
481
|
+
end
|
|
482
|
+
|
|
397
483
|
# Spawn bullet puff at hit location
|
|
398
|
-
puff_x = px + ca * best_dist
|
|
399
|
-
puff_y = py + sa * best_dist
|
|
400
|
-
puff_z =
|
|
484
|
+
puff_x = px + (ca * best_dist)
|
|
485
|
+
puff_y = py + (sa * best_dist)
|
|
486
|
+
puff_z = pz
|
|
401
487
|
@puffs << { x: puff_x, y: puff_y, z: puff_z, tic: @tic }
|
|
402
488
|
|
|
403
|
-
if
|
|
404
|
-
|
|
489
|
+
if best_player
|
|
490
|
+
damage_player(best_player, (@random.rand(3) + 1) * multiplier, shooter)
|
|
491
|
+
elsif best_idx
|
|
492
|
+
damage = (@random.rand(3) + 1) * multiplier
|
|
405
493
|
apply_damage(best_idx, damage)
|
|
406
494
|
end
|
|
407
495
|
end
|
|
408
496
|
end
|
|
409
497
|
|
|
410
|
-
def melee(px, py, cos_a, sin_a, multiplier, range)
|
|
498
|
+
def melee(px, py, cos_a, sin_a, multiplier, range, shooter = nil)
|
|
411
499
|
best_idx = nil
|
|
500
|
+
best_player = nil
|
|
412
501
|
best_dist = range.to_f
|
|
413
502
|
|
|
414
503
|
@map.things.each_with_index do |thing, idx|
|
|
@@ -417,11 +506,11 @@ module Doom
|
|
|
417
506
|
|
|
418
507
|
dx = thing.x - px
|
|
419
508
|
dy = thing.y - py
|
|
420
|
-
dist = Math.sqrt(dx * dx + dy * dy)
|
|
509
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
421
510
|
next if dist > range + (MONSTER_RADIUS[thing.type] || 20)
|
|
422
511
|
|
|
423
512
|
# Check if roughly facing the monster
|
|
424
|
-
dot = dx * cos_a + dy * sin_a
|
|
513
|
+
dot = (dx * cos_a) + (dy * sin_a)
|
|
425
514
|
next if dot < 0
|
|
426
515
|
|
|
427
516
|
if dist < best_dist
|
|
@@ -430,8 +519,23 @@ module Doom
|
|
|
430
519
|
end
|
|
431
520
|
end
|
|
432
521
|
|
|
433
|
-
|
|
434
|
-
|
|
522
|
+
player_targets(shooter).each do |pl|
|
|
523
|
+
dx = pl.x - px
|
|
524
|
+
dy = pl.y - py
|
|
525
|
+
dist = Math.hypot(dx, dy)
|
|
526
|
+
next if dist > range + PLAYER_RADIUS
|
|
527
|
+
next if (dx * cos_a) + (dy * sin_a) < 0 # behind the swing
|
|
528
|
+
next unless dist < best_dist
|
|
529
|
+
|
|
530
|
+
best_dist = dist
|
|
531
|
+
best_player = pl
|
|
532
|
+
best_idx = nil
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
if best_player
|
|
536
|
+
damage_player(best_player, (@random.rand(3) + 1) * multiplier, shooter)
|
|
537
|
+
elsif best_idx
|
|
538
|
+
damage = (@random.rand(3) + 1) * multiplier
|
|
435
539
|
apply_damage(best_idx, damage)
|
|
436
540
|
end
|
|
437
541
|
end
|
|
@@ -443,7 +547,7 @@ module Doom
|
|
|
443
547
|
@monster_hp[thing_idx] -= damage
|
|
444
548
|
|
|
445
549
|
if @monster_hp[thing_idx] <= 0
|
|
446
|
-
return if @dead_things[thing_idx]
|
|
550
|
+
return if @dead_things[thing_idx] # Already dead
|
|
447
551
|
|
|
448
552
|
if thing.type == BARREL_TYPE
|
|
449
553
|
@dead_things[thing_idx] = { tic: @tic, prefix: 'BEXP' }
|
|
@@ -458,7 +562,7 @@ module Doom
|
|
|
458
562
|
# Pain state: monster flinches (not barrels)
|
|
459
563
|
if thing.type != BARREL_TYPE
|
|
460
564
|
pain_chance = PAIN_CHANCE[thing.type] || 128
|
|
461
|
-
if rand(256) < pain_chance
|
|
565
|
+
if @random.rand(256) < pain_chance
|
|
462
566
|
@pain_until[thing_idx] = @tic + PAIN_DURATION
|
|
463
567
|
@sound&.monster_pain(thing.type)
|
|
464
568
|
end
|
|
@@ -469,33 +573,15 @@ module Doom
|
|
|
469
573
|
def barrel_explode(x, y, barrel_idx)
|
|
470
574
|
@explosions << { x: x, y: y, tic: @tic, sprite: 'MISL' }
|
|
471
575
|
|
|
472
|
-
# Splash damage to monsters and other barrels (chain reactions!)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
dx = x - thing.x
|
|
479
|
-
dy = y - thing.y
|
|
480
|
-
dist = Math.sqrt(dx * dx + dy * dy)
|
|
481
|
-
next if dist >= BARREL_SPLASH_RADIUS
|
|
482
|
-
|
|
483
|
-
damage = ((BARREL_SPLASH_DAMAGE * (1.0 - dist / BARREL_SPLASH_RADIUS))).to_i
|
|
484
|
-
apply_damage(idx, damage) if damage > 0
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
# Splash damage to player
|
|
488
|
-
dx = x - @player_x
|
|
489
|
-
dy = y - @player_y
|
|
490
|
-
dist = Math.sqrt(dx * dx + dy * dy)
|
|
491
|
-
if dist < BARREL_SPLASH_RADIUS
|
|
492
|
-
damage = ((BARREL_SPLASH_DAMAGE * (1.0 - dist / BARREL_SPLASH_RADIUS))).to_i
|
|
493
|
-
@player.take_damage(damage) if damage > 0
|
|
494
|
-
end
|
|
576
|
+
# Splash damage to monsters and other barrels (chain reactions!), then to
|
|
577
|
+
# every player in radius including the one who set it off. Nobody is
|
|
578
|
+
# credited: the barrel is not a player, and we do not track who shot it.
|
|
579
|
+
splash_monsters(x, y, SPLASH_RADIUS, SPLASH_DAMAGE, barrel_idx)
|
|
580
|
+
splash_players(x, y, SPLASH_RADIUS, SPLASH_DAMAGE, nil)
|
|
495
581
|
end
|
|
496
582
|
|
|
497
583
|
def trace_wall(px, py, cos_a, sin_a)
|
|
498
|
-
best_t = 4096.0
|
|
584
|
+
best_t = 4096.0 # Max hitscan range
|
|
499
585
|
|
|
500
586
|
@map.linedefs.each do |ld|
|
|
501
587
|
v1 = @map.vertices[ld.v1]
|
|
@@ -520,7 +606,7 @@ module Doom
|
|
|
520
606
|
next unless blocks
|
|
521
607
|
|
|
522
608
|
t = ray_segment_intersect(px, py, cos_a, sin_a,
|
|
523
|
-
|
|
609
|
+
v1.x, v1.y, v2.x, v2.y)
|
|
524
610
|
best_t = t if t && t > 0 && t < best_t
|
|
525
611
|
end
|
|
526
612
|
|
|
@@ -530,25 +616,25 @@ module Doom
|
|
|
530
616
|
def ray_segment_intersect(px, py, dx, dy, x1, y1, x2, y2)
|
|
531
617
|
sx = x2 - x1
|
|
532
618
|
sy = y2 - y1
|
|
533
|
-
denom = dx * sy - dy * sx
|
|
619
|
+
denom = (dx * sy) - (dy * sx)
|
|
534
620
|
return nil if denom.abs < 0.001
|
|
535
621
|
|
|
536
|
-
t = ((x1 - px) * sy - (y1 - py) * sx) / denom
|
|
537
|
-
u = ((x1 - px) * dy - (y1 - py) * dx) / denom
|
|
622
|
+
t = (((x1 - px) * sy) - ((y1 - py) * sx)) / denom
|
|
623
|
+
u = (((x1 - px) * dy) - ((y1 - py) * dx)) / denom
|
|
538
624
|
|
|
539
|
-
|
|
625
|
+
t > 0 && u >= 0.0 && u <= 1.0 ? t : nil
|
|
540
626
|
end
|
|
541
627
|
|
|
542
628
|
def ray_circle_hit(px, py, cos_a, sin_a, cx, cy, radius)
|
|
543
629
|
dx = cx - px
|
|
544
630
|
dy = cy - py
|
|
545
|
-
proj = dx * cos_a + dy * sin_a
|
|
631
|
+
proj = (dx * cos_a) + (dy * sin_a)
|
|
546
632
|
return nil if proj < 0
|
|
547
633
|
|
|
548
|
-
perp_sq = dx * dx + dy * dy - proj * proj
|
|
634
|
+
perp_sq = (dx * dx) + (dy * dy) - (proj * proj)
|
|
549
635
|
return nil if perp_sq > radius * radius
|
|
550
636
|
|
|
551
|
-
chord_half = Math.sqrt([radius * radius - perp_sq, 0].max)
|
|
637
|
+
chord_half = Math.sqrt([(radius * radius) - perp_sq, 0].max)
|
|
552
638
|
proj - chord_half
|
|
553
639
|
end
|
|
554
640
|
end
|