doom 0.6.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 +138 -25
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +377 -105
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +231 -0
- data/lib/doom/game/item_pickup.rb +108 -60
- data/lib/doom/game/menu.rb +347 -0
- data/lib/doom/game/monster_ai.rb +304 -91
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +47 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +466 -29
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +176 -0
- 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 +820 -640
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +80 -0
- 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 +684 -330
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +71 -0
- 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 +15 -17
- 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 +59 -10
- data/lib/doom/wad/sound.rb +93 -0
- data/lib/doom/wad/sprite.rb +60 -34
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +235 -12
- metadata +100 -7
data/lib/doom/game/combat.rb
CHANGED
|
@@ -2,34 +2,43 @@
|
|
|
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
|
|
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
|
|
25
29
|
}.freeze
|
|
26
30
|
|
|
27
31
|
MONSTER_RADIUS = {
|
|
28
32
|
3004 => 20, 9 => 20, 3001 => 20, 3002 => 30, 58 => 30,
|
|
29
33
|
3003 => 24, 69 => 24, 3005 => 31, 3006 => 16, 16 => 40,
|
|
30
34
|
7 => 128, 65 => 20, 64 => 20, 71 => 31, 84 => 20,
|
|
35
|
+
2035 => 10 # Barrel
|
|
31
36
|
}.freeze
|
|
32
37
|
|
|
38
|
+
# Barrel (explosive, not a monster but damageable). Its HP comes from
|
|
39
|
+
# MONSTER_HP[BARREL_TYPE]; its explosion reuses the shared SPLASH_* values.
|
|
40
|
+
BARREL_TYPE = 2035
|
|
41
|
+
|
|
33
42
|
# Normal death frame sequences per sprite prefix (rotation 0 only)
|
|
34
43
|
# Identified by sprite heights: frames go from standing height to flat on ground
|
|
35
44
|
DEATH_FRAMES = {
|
|
@@ -46,9 +55,18 @@ module Doom
|
|
|
46
55
|
'CPOS' => %w[H I J K L M N], # Heavy Weapon Dude
|
|
47
56
|
'PAIN' => %w[H I J K L M], # Pain Elemental
|
|
48
57
|
'SSWV' => %w[I J K L M], # Wolfenstein SS
|
|
58
|
+
'BEXP' => %w[A B C D E] # Barrel explosion
|
|
49
59
|
}.freeze
|
|
50
60
|
|
|
51
|
-
DEATH_ANIM_TICS = 6
|
|
61
|
+
DEATH_ANIM_TICS = 6 # Tics per death frame
|
|
62
|
+
|
|
63
|
+
# Pain chance per monster (out of 256, from mobjinfo)
|
|
64
|
+
PAIN_CHANCE = {
|
|
65
|
+
3004 => 200, 9 => 170, 3001 => 200, 3002 => 180, 58 => 180,
|
|
66
|
+
3003 => 50, 69 => 50, 3005 => 128, 3006 => 256, 16 => 40,
|
|
67
|
+
7 => 40, 65 => 170, 64 => 10, 71 => 128, 84 => 170
|
|
68
|
+
}.freeze
|
|
69
|
+
PAIN_DURATION = 6 # Tics monster is stunned when in pain
|
|
52
70
|
|
|
53
71
|
# Projectile constants
|
|
54
72
|
ROCKET_SPEED = 20.0 # Map units per tic (matches DOOM's mobjinfo MISSILESPEED)
|
|
@@ -57,43 +75,127 @@ module Doom
|
|
|
57
75
|
SPLASH_RADIUS = 128.0 # Splash damage radius
|
|
58
76
|
SPLASH_DAMAGE = 128 # Max splash damage at center
|
|
59
77
|
|
|
60
|
-
|
|
78
|
+
# Monster projectile definitions. :radius is the projectile's own collision
|
|
79
|
+
# radius, added to the player radius when testing a hit.
|
|
80
|
+
MONSTER_PROJECTILES = {
|
|
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 }
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
# Map monster type to projectile type
|
|
87
|
+
MONSTER_PROJECTILE_TYPE = {
|
|
88
|
+
3001 => :imp, # Imp
|
|
89
|
+
3003 => :baron, # Baron
|
|
90
|
+
69 => :baron, # Hell Knight
|
|
91
|
+
3005 => :caco # Cacodemon
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
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)
|
|
61
105
|
|
|
62
106
|
# Weapon damage: DOOM does (P_Random()%3 + 1) * multiplier
|
|
63
107
|
# Pistol/chaingun: 1*5..3*5 = 5-15 per bullet
|
|
64
108
|
# Shotgun: 7 pellets, each 1*5..3*5 = 5-15
|
|
65
109
|
# Fist/chainsaw: 1*2..3*2 = 2-10
|
|
66
110
|
|
|
67
|
-
def initialize(map,
|
|
111
|
+
def initialize(map, sprites = nil, hidden_things = {}, sound_engine = nil,
|
|
112
|
+
random: Random.new)
|
|
68
113
|
@map = map
|
|
69
|
-
@
|
|
114
|
+
@random = random
|
|
70
115
|
@sprites = sprites
|
|
116
|
+
@hidden_things = hidden_things
|
|
117
|
+
@sound = sound_engine
|
|
71
118
|
@monster_hp = {} # thing_idx => current HP
|
|
72
119
|
@dead_things = {} # thing_idx => { tic: death_start_tic, prefix: sprite_prefix }
|
|
120
|
+
@pain_until = {} # thing_idx => tic when pain ends
|
|
73
121
|
@projectiles = [] # Active projectiles in flight
|
|
74
122
|
@explosions = [] # Active explosions (for rendering)
|
|
123
|
+
@puffs = [] # Bullet puff effects
|
|
124
|
+
@players = []
|
|
75
125
|
@tic = 0
|
|
76
126
|
end
|
|
77
127
|
|
|
78
|
-
|
|
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
|
|
131
|
+
|
|
132
|
+
# Spawn a monster projectile (fireball, etc.)
|
|
133
|
+
# Matches Chocolate Doom's P_SpawnMissile: calculates momz for vertical aim
|
|
134
|
+
def spawn_monster_projectile(monster_x, monster_y, monster_z, monster_type, damage_multiplier, target)
|
|
135
|
+
proj_type = MONSTER_PROJECTILE_TYPE[monster_type]
|
|
136
|
+
return unless proj_type
|
|
137
|
+
|
|
138
|
+
info = MONSTER_PROJECTILES[proj_type]
|
|
139
|
+
return unless info
|
|
140
|
+
|
|
141
|
+
dx = target.x - monster_x
|
|
142
|
+
dy = target.y - monster_y
|
|
143
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
144
|
+
return if dist < 1
|
|
145
|
+
|
|
146
|
+
# Normalize direction and apply speed
|
|
147
|
+
speed = info[:speed]
|
|
148
|
+
ndx = dx / dist * speed
|
|
149
|
+
ndy = dy / dist * speed
|
|
150
|
+
|
|
151
|
+
# P_SpawnMissile: momz = (target.z - source.z) / (dist / speed)
|
|
152
|
+
# This makes the projectile arc toward the target's height
|
|
153
|
+
target_z = target.z - 16 # Aim at player center (z + height/2, roughly)
|
|
154
|
+
travel_tics = dist / speed
|
|
155
|
+
travel_tics = 1.0 if travel_tics < 1.0
|
|
156
|
+
ndz = (target_z - monster_z) / travel_tics
|
|
157
|
+
|
|
158
|
+
@projectiles << Projectile.new(
|
|
159
|
+
monster_x + (ndx * 2), monster_y + (ndy * 2), monster_z,
|
|
160
|
+
ndx, ndy, ndz, proj_type, @tic, info[:sprite], :player, nil, damage_multiplier
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
attr_reader :dead_things, :projectiles, :explosions, :puffs, :sprites
|
|
165
|
+
|
|
166
|
+
def in_pain?(thing_idx)
|
|
167
|
+
@pain_until[thing_idx] && @tic < @pain_until[thing_idx]
|
|
168
|
+
end
|
|
79
169
|
|
|
80
170
|
def dead?(thing_idx)
|
|
81
171
|
@dead_things.key?(thing_idx)
|
|
82
172
|
end
|
|
83
173
|
|
|
84
|
-
# Get the current death frame sprite for a dead monster
|
|
174
|
+
# Get the current death frame sprite for a dead monster/barrel
|
|
85
175
|
def death_sprite(thing_idx, thing_type, viewer_angle, thing_angle)
|
|
86
176
|
info = @dead_things[thing_idx]
|
|
87
177
|
return nil unless info
|
|
88
178
|
|
|
89
|
-
|
|
179
|
+
prefix = info[:prefix]
|
|
180
|
+
frames = DEATH_FRAMES[prefix]
|
|
90
181
|
return nil unless frames
|
|
91
182
|
|
|
92
183
|
elapsed = @tic - info[:tic]
|
|
93
|
-
frame_idx =
|
|
184
|
+
frame_idx = elapsed / DEATH_ANIM_TICS
|
|
185
|
+
|
|
186
|
+
# Barrels disappear after explosion animation (S_NULL in Chocolate Doom)
|
|
187
|
+
return nil if thing_type == BARREL_TYPE && frame_idx >= frames.size
|
|
188
|
+
|
|
189
|
+
frame_idx = frame_idx.clamp(0, frames.size - 1)
|
|
94
190
|
frame_letter = frames[frame_idx]
|
|
95
191
|
|
|
96
|
-
|
|
192
|
+
# Use prefix directly if it differs from the thing's sprite (e.g. BEXP for barrels)
|
|
193
|
+
thing_prefix = @sprites.prefix_for(thing_type)
|
|
194
|
+
if prefix == thing_prefix
|
|
195
|
+
@sprites.get_frame(thing_type, frame_letter, viewer_angle, thing_angle)
|
|
196
|
+
else
|
|
197
|
+
@sprites.get_frame_by_prefix(prefix, frame_letter)
|
|
198
|
+
end
|
|
97
199
|
end
|
|
98
200
|
|
|
99
201
|
# Called each game tic
|
|
@@ -101,94 +203,204 @@ module Doom
|
|
|
101
203
|
@tic += 1
|
|
102
204
|
update_projectiles
|
|
103
205
|
update_explosions
|
|
206
|
+
@puffs.reject! { |p| @tic - p[:tic] > 12 }
|
|
104
207
|
end
|
|
105
208
|
|
|
106
|
-
# Fire the current weapon
|
|
107
|
-
|
|
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)
|
|
108
213
|
case weapon
|
|
109
214
|
when PlayerState::WEAPON_PISTOL, PlayerState::WEAPON_CHAINGUN
|
|
110
|
-
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)
|
|
111
216
|
when PlayerState::WEAPON_SHOTGUN
|
|
112
|
-
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)
|
|
113
218
|
when PlayerState::WEAPON_ROCKET
|
|
114
|
-
spawn_rocket(px, py, pz, cos_a, sin_a)
|
|
219
|
+
spawn_rocket(px, py, pz, cos_a, sin_a, shooter)
|
|
115
220
|
when PlayerState::WEAPON_FIST
|
|
116
|
-
melee(px, py, cos_a, sin_a, 2, 64)
|
|
221
|
+
melee(px, py, cos_a, sin_a, 2, 64, shooter)
|
|
117
222
|
when PlayerState::WEAPON_CHAINSAW
|
|
118
|
-
melee(px, py, cos_a, sin_a, 2, 64)
|
|
223
|
+
melee(px, py, cos_a, sin_a, 2, 64, shooter)
|
|
119
224
|
end
|
|
120
225
|
end
|
|
121
226
|
|
|
122
227
|
private
|
|
123
228
|
|
|
124
|
-
def spawn_rocket(px, py, pz, cos_a, sin_a)
|
|
125
|
-
# Spawn slightly ahead of the player
|
|
229
|
+
def spawn_rocket(px, py, pz, cos_a, sin_a, shooter = nil)
|
|
126
230
|
@projectiles << Projectile.new(
|
|
127
|
-
px + cos_a * 20, py + sin_a * 20, pz,
|
|
128
|
-
cos_a * ROCKET_SPEED, sin_a * ROCKET_SPEED,
|
|
129
|
-
:rocket, @tic
|
|
231
|
+
px + (cos_a * 20), py + (sin_a * 20), pz,
|
|
232
|
+
cos_a * ROCKET_SPEED, sin_a * ROCKET_SPEED, 0.0,
|
|
233
|
+
:rocket, @tic, 'MISL', :monsters, shooter, 1.0
|
|
130
234
|
)
|
|
131
235
|
end
|
|
132
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
|
+
|
|
133
274
|
def update_projectiles
|
|
134
275
|
@projectiles.reject! do |proj|
|
|
135
|
-
# Move projectile
|
|
136
276
|
new_x = proj.x + proj.dx
|
|
137
277
|
new_y = proj.y + proj.dy
|
|
278
|
+
new_z = proj.z + (proj.dz || 0)
|
|
138
279
|
|
|
139
|
-
# Check wall collision
|
|
140
280
|
hit_wall = hits_wall?(proj.x, proj.y, new_x, new_y)
|
|
141
281
|
|
|
142
|
-
# Check
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
282
|
+
# Check if projectile hit the floor or ceiling
|
|
283
|
+
sector = @map.sector_at(new_x, new_y)
|
|
284
|
+
hit_wall = true if sector && (new_z <= sector.floor_height || new_z >= sector.ceiling_height)
|
|
285
|
+
hit = false
|
|
286
|
+
|
|
287
|
+
if proj.target == :monsters
|
|
288
|
+
# Player projectile: check monster collision
|
|
289
|
+
hit_monster = nil
|
|
290
|
+
@map.things.each_with_index do |thing, idx|
|
|
291
|
+
next unless MONSTER_HP[thing.type]
|
|
292
|
+
next if @dead_things[idx]
|
|
293
|
+
|
|
294
|
+
radius = (MONSTER_RADIUS[thing.type] || 20) + ROCKET_RADIUS
|
|
295
|
+
dx = new_x - thing.x
|
|
296
|
+
dy = new_y - thing.y
|
|
297
|
+
if (dx * dx) + (dy * dy) < radius * radius
|
|
298
|
+
hit_monster = idx
|
|
299
|
+
break
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
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)
|
|
318
|
+
hit = true
|
|
319
|
+
end
|
|
320
|
+
elsif proj.target == :player
|
|
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)
|
|
339
|
+
end
|
|
340
|
+
# Spawn fireball explosion
|
|
341
|
+
@explosions << { x: new_x, y: new_y, z: proj.z, tic: @tic, sprite: proj.sprite_prefix }
|
|
342
|
+
hit = true
|
|
153
343
|
end
|
|
154
344
|
end
|
|
155
345
|
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
true # Remove projectile
|
|
346
|
+
if hit
|
|
347
|
+
true
|
|
159
348
|
else
|
|
160
349
|
proj.x = new_x
|
|
161
350
|
proj.y = new_y
|
|
162
|
-
|
|
351
|
+
proj.z = new_z
|
|
352
|
+
false
|
|
163
353
|
end
|
|
164
354
|
end
|
|
165
355
|
end
|
|
166
356
|
|
|
167
|
-
def explode(x, y, direct_hit_idx)
|
|
357
|
+
def explode(x, y, direct_hit_idx, owner = nil)
|
|
168
358
|
# Direct hit damage
|
|
169
359
|
if direct_hit_idx
|
|
170
|
-
damage = (rand(8) + 1) * ROCKET_DAMAGE
|
|
360
|
+
damage = (@random.rand(8) + 1) * ROCKET_DAMAGE
|
|
171
361
|
apply_damage(direct_hit_idx, damage)
|
|
172
362
|
end
|
|
173
363
|
|
|
174
|
-
|
|
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)
|
|
175
375
|
@map.things.each_with_index do |thing, idx|
|
|
176
376
|
next unless MONSTER_HP[thing.type]
|
|
177
377
|
next if @dead_things[idx]
|
|
178
|
-
next if idx ==
|
|
378
|
+
next if idx == exclude_idx
|
|
179
379
|
|
|
180
380
|
dx = x - thing.x
|
|
181
381
|
dy = y - thing.y
|
|
182
|
-
dist = Math.sqrt(dx * dx + dy * dy)
|
|
183
|
-
next if dist >=
|
|
382
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
383
|
+
next if dist >= radius
|
|
184
384
|
|
|
185
|
-
|
|
186
|
-
damage = ((SPLASH_DAMAGE * (1.0 - dist / SPLASH_RADIUS))).to_i
|
|
385
|
+
damage = (max_damage * (1.0 - (dist / radius))).to_i
|
|
187
386
|
apply_damage(idx, damage) if damage > 0
|
|
188
387
|
end
|
|
388
|
+
end
|
|
189
389
|
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
192
404
|
end
|
|
193
405
|
|
|
194
406
|
def update_explosions
|
|
@@ -198,50 +410,41 @@ module Doom
|
|
|
198
410
|
|
|
199
411
|
def hits_wall?(x1, y1, x2, y2)
|
|
200
412
|
@map.linedefs.each do |ld|
|
|
201
|
-
# One-sided walls always block
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
413
|
+
# One-sided walls always block projectiles
|
|
414
|
+
if ld.sidedef_left == 0xFFFF
|
|
415
|
+
blocks = true
|
|
416
|
+
elsif ld.sidedef_left < 0xFFFF
|
|
417
|
+
# Two-sided: only block if opening is too small for a projectile
|
|
418
|
+
# BLOCKING flag (0x0001) stops players/monsters but NOT projectiles
|
|
205
419
|
front = @map.sidedefs[ld.sidedef_right]
|
|
206
420
|
back = @map.sidedefs[ld.sidedef_left]
|
|
207
421
|
fs = @map.sectors[front.sector]
|
|
208
422
|
bs = @map.sectors[back.sector]
|
|
209
423
|
max_floor = [fs.floor_height, bs.floor_height].max
|
|
210
424
|
min_ceil = [fs.ceiling_height, bs.ceiling_height].min
|
|
211
|
-
blocks = (min_ceil - max_floor) <
|
|
425
|
+
blocks = (min_ceil - max_floor) < 1 # Closed door/wall
|
|
426
|
+
else
|
|
427
|
+
next
|
|
212
428
|
end
|
|
213
429
|
next unless blocks
|
|
214
430
|
|
|
215
431
|
v1 = @map.vertices[ld.v1]
|
|
216
432
|
v2 = @map.vertices[ld.v2]
|
|
217
|
-
if segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
218
|
-
return true
|
|
219
|
-
end
|
|
433
|
+
return true if Geometry.segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
220
434
|
end
|
|
221
435
|
false
|
|
222
436
|
end
|
|
223
437
|
|
|
224
|
-
def
|
|
225
|
-
d1x = ax2 - ax1; d1y = ay2 - ay1
|
|
226
|
-
d2x = bx2 - bx1; d2y = by2 - by1
|
|
227
|
-
denom = d1x * d2y - d1y * d2x
|
|
228
|
-
return false if denom.abs < 0.001
|
|
229
|
-
dx = bx1 - ax1; dy = by1 - ay1
|
|
230
|
-
t = (dx * d2y - dy * d2x).to_f / denom
|
|
231
|
-
u = (dx * d1y - dy * d1x).to_f / denom
|
|
232
|
-
t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
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)
|
|
236
439
|
pellets.times do
|
|
237
440
|
# Add random spread
|
|
238
441
|
if spread > 0
|
|
239
|
-
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)
|
|
240
443
|
ca = Math.cos(angle)
|
|
241
444
|
sa = Math.sin(angle)
|
|
242
445
|
else
|
|
243
446
|
# Slight pistol/chaingun spread
|
|
244
|
-
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)
|
|
245
448
|
ca = Math.cos(angle)
|
|
246
449
|
sa = Math.sin(angle)
|
|
247
450
|
end
|
|
@@ -249,9 +452,11 @@ module Doom
|
|
|
249
452
|
wall_dist = trace_wall(px, py, ca, sa)
|
|
250
453
|
|
|
251
454
|
best_idx = nil
|
|
455
|
+
best_player = nil
|
|
252
456
|
best_dist = wall_dist
|
|
253
457
|
|
|
254
458
|
@map.things.each_with_index do |thing, idx|
|
|
459
|
+
next if @hidden_things[idx]
|
|
255
460
|
next unless MONSTER_HP[thing.type]
|
|
256
461
|
next if @dead_things[idx]
|
|
257
462
|
|
|
@@ -263,15 +468,36 @@ module Doom
|
|
|
263
468
|
end
|
|
264
469
|
end
|
|
265
470
|
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
|
|
483
|
+
# Spawn bullet puff at hit location
|
|
484
|
+
puff_x = px + (ca * best_dist)
|
|
485
|
+
puff_y = py + (sa * best_dist)
|
|
486
|
+
puff_z = pz
|
|
487
|
+
@puffs << { x: puff_x, y: puff_y, z: puff_z, tic: @tic }
|
|
488
|
+
|
|
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
|
|
268
493
|
apply_damage(best_idx, damage)
|
|
269
494
|
end
|
|
270
495
|
end
|
|
271
496
|
end
|
|
272
497
|
|
|
273
|
-
def melee(px, py, cos_a, sin_a, multiplier, range)
|
|
498
|
+
def melee(px, py, cos_a, sin_a, multiplier, range, shooter = nil)
|
|
274
499
|
best_idx = nil
|
|
500
|
+
best_player = nil
|
|
275
501
|
best_dist = range.to_f
|
|
276
502
|
|
|
277
503
|
@map.things.each_with_index do |thing, idx|
|
|
@@ -280,11 +506,11 @@ module Doom
|
|
|
280
506
|
|
|
281
507
|
dx = thing.x - px
|
|
282
508
|
dy = thing.y - py
|
|
283
|
-
dist = Math.sqrt(dx * dx + dy * dy)
|
|
509
|
+
dist = Math.sqrt((dx * dx) + (dy * dy))
|
|
284
510
|
next if dist > range + (MONSTER_RADIUS[thing.type] || 20)
|
|
285
511
|
|
|
286
512
|
# Check if roughly facing the monster
|
|
287
|
-
dot = dx * cos_a + dy * sin_a
|
|
513
|
+
dot = (dx * cos_a) + (dy * sin_a)
|
|
288
514
|
next if dot < 0
|
|
289
515
|
|
|
290
516
|
if dist < best_dist
|
|
@@ -293,8 +519,23 @@ module Doom
|
|
|
293
519
|
end
|
|
294
520
|
end
|
|
295
521
|
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
298
539
|
apply_damage(best_idx, damage)
|
|
299
540
|
end
|
|
300
541
|
end
|
|
@@ -306,35 +547,66 @@ module Doom
|
|
|
306
547
|
@monster_hp[thing_idx] -= damage
|
|
307
548
|
|
|
308
549
|
if @monster_hp[thing_idx] <= 0
|
|
309
|
-
|
|
310
|
-
|
|
550
|
+
return if @dead_things[thing_idx] # Already dead
|
|
551
|
+
|
|
552
|
+
if thing.type == BARREL_TYPE
|
|
553
|
+
@dead_things[thing_idx] = { tic: @tic, prefix: 'BEXP' }
|
|
554
|
+
@sound&.explosion
|
|
555
|
+
barrel_explode(thing.x, thing.y, thing_idx)
|
|
556
|
+
else
|
|
557
|
+
prefix = @sprites.prefix_for(thing.type)
|
|
558
|
+
@dead_things[thing_idx] = { tic: @tic, prefix: prefix } if prefix
|
|
559
|
+
@sound&.monster_death(thing.type)
|
|
560
|
+
end
|
|
561
|
+
else
|
|
562
|
+
# Pain state: monster flinches (not barrels)
|
|
563
|
+
if thing.type != BARREL_TYPE
|
|
564
|
+
pain_chance = PAIN_CHANCE[thing.type] || 128
|
|
565
|
+
if @random.rand(256) < pain_chance
|
|
566
|
+
@pain_until[thing_idx] = @tic + PAIN_DURATION
|
|
567
|
+
@sound&.monster_pain(thing.type)
|
|
568
|
+
end
|
|
569
|
+
end
|
|
311
570
|
end
|
|
312
571
|
end
|
|
313
572
|
|
|
573
|
+
def barrel_explode(x, y, barrel_idx)
|
|
574
|
+
@explosions << { x: x, y: y, tic: @tic, sprite: 'MISL' }
|
|
575
|
+
|
|
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)
|
|
581
|
+
end
|
|
582
|
+
|
|
314
583
|
def trace_wall(px, py, cos_a, sin_a)
|
|
315
|
-
best_t = 4096.0
|
|
584
|
+
best_t = 4096.0 # Max hitscan range
|
|
316
585
|
|
|
317
586
|
@map.linedefs.each do |ld|
|
|
318
587
|
v1 = @map.vertices[ld.v1]
|
|
319
588
|
v2 = @map.vertices[ld.v2]
|
|
320
589
|
|
|
321
|
-
# One-sided always blocks
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
590
|
+
# One-sided always blocks hitscan
|
|
591
|
+
if ld.sidedef_left == 0xFFFF
|
|
592
|
+
blocks = true
|
|
593
|
+
elsif ld.sidedef_left < 0xFFFF
|
|
594
|
+
# Two-sided: only blocks if opening is too small
|
|
595
|
+
# BLOCKING flag does NOT stop hitscan (only affects movement)
|
|
325
596
|
front = @map.sidedefs[ld.sidedef_right]
|
|
326
597
|
back = @map.sidedefs[ld.sidedef_left]
|
|
327
598
|
fs = @map.sectors[front.sector]
|
|
328
599
|
bs = @map.sectors[back.sector]
|
|
329
|
-
# Blocks if opening is too small (step or low ceiling)
|
|
330
600
|
max_floor = [fs.floor_height, bs.floor_height].max
|
|
331
601
|
min_ceil = [fs.ceiling_height, bs.ceiling_height].min
|
|
332
602
|
blocks = (min_ceil - max_floor) < 56
|
|
603
|
+
else
|
|
604
|
+
next
|
|
333
605
|
end
|
|
334
606
|
next unless blocks
|
|
335
607
|
|
|
336
608
|
t = ray_segment_intersect(px, py, cos_a, sin_a,
|
|
337
|
-
|
|
609
|
+
v1.x, v1.y, v2.x, v2.y)
|
|
338
610
|
best_t = t if t && t > 0 && t < best_t
|
|
339
611
|
end
|
|
340
612
|
|
|
@@ -344,25 +616,25 @@ module Doom
|
|
|
344
616
|
def ray_segment_intersect(px, py, dx, dy, x1, y1, x2, y2)
|
|
345
617
|
sx = x2 - x1
|
|
346
618
|
sy = y2 - y1
|
|
347
|
-
denom = dx * sy - dy * sx
|
|
619
|
+
denom = (dx * sy) - (dy * sx)
|
|
348
620
|
return nil if denom.abs < 0.001
|
|
349
621
|
|
|
350
|
-
t = ((x1 - px) * sy - (y1 - py) * sx) / denom
|
|
351
|
-
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
|
|
352
624
|
|
|
353
|
-
|
|
625
|
+
t > 0 && u >= 0.0 && u <= 1.0 ? t : nil
|
|
354
626
|
end
|
|
355
627
|
|
|
356
628
|
def ray_circle_hit(px, py, cos_a, sin_a, cx, cy, radius)
|
|
357
629
|
dx = cx - px
|
|
358
630
|
dy = cy - py
|
|
359
|
-
proj = dx * cos_a + dy * sin_a
|
|
631
|
+
proj = (dx * cos_a) + (dy * sin_a)
|
|
360
632
|
return nil if proj < 0
|
|
361
633
|
|
|
362
|
-
perp_sq = dx * dx + dy * dy - proj * proj
|
|
634
|
+
perp_sq = (dx * dx) + (dy * dy) - (proj * proj)
|
|
363
635
|
return nil if perp_sq > radius * radius
|
|
364
636
|
|
|
365
|
-
chord_half = Math.sqrt([radius * radius - perp_sq, 0].max)
|
|
637
|
+
chord_half = Math.sqrt([(radius * radius) - perp_sq, 0].max)
|
|
366
638
|
proj - chord_half
|
|
367
639
|
end
|
|
368
640
|
end
|