Class: Doom::Game::Combat

Inherits:
Object
  • Object
show all
Defined in:
lib/doom/game/combat.rb

Overview

Combat resolution for every weapon and every damageable thing: hitscan (pistol/shotgun/chaingun), melee (fist/chainsaw), rockets and their splash, monster fireballs, barrel chain reactions, player-vs-player damage and frag scoring. Hitscan/aim matches Chocolate Doom's P_LineAttack / P_AimLineAttack (p_map.c); splash matches P_RadiusAttack (p_inter.c).

Defined Under Namespace

Classes: Projectile

Constant Summary collapse

MONSTER_HP =

Monster starting HP (from mobjinfo in info.c)

{
  3004 => 20, # Zombieman
  9 => 30, # Shotgun Guy
  3001 => 60,   # Imp
  3002 => 150,  # Demon
  58 => 150, # Spectre
  3003 => 1000, # Baron of Hell
  69 => 500, # Hell Knight
  3005 => 400,  # Cacodemon
  3006 => 100,  # Lost Soul
  16 => 4000, # Cyberdemon
  7 => 3000, # Spider Mastermind
  65 => 70,   # Heavy Weapon Dude
  64 => 700,  # Archvile
  71 => 400,  # Pain Elemental
  84 => 20,   # Wolfenstein SS
  2035 => 20 # Explosive barrel
}.freeze
MONSTER_RADIUS =
{
  3004 => 20, 9 => 20, 3001 => 20, 3002 => 30, 58 => 30,
  3003 => 24, 69 => 24, 3005 => 31, 3006 => 16, 16 => 40,
  7 => 128, 65 => 20, 64 => 20, 71 => 31, 84 => 20,
  2035 => 10 # Barrel
}.freeze
BARREL_TYPE =

Barrel (explosive, not a monster but damageable). Its HP comes from MONSTER_HP; its explosion reuses the shared SPLASH_* values.

2035
DEATH_FRAMES =

Normal death frame sequences per sprite prefix (rotation 0 only) Identified by sprite heights: frames go from standing height to flat on ground

{
  'POSS' => %w[H I J K L],       # Zombieman: 55→46→34→27→17
  'SPOS' => %w[H I J K L],       # Shotgun Guy: 60→50→35→27→17
  'TROO' => %w[I J K L M],       # Imp: 62→59→54→46→22
  'SARG' => %w[I J K L M N],     # Demon/Spectre: 56→56→53→57→46→32
  'BOSS' => %w[H I J K L M N],   # Baron
  'BOS2' => %w[H I J K L M N],   # Hell Knight
  'HEAD' => %w[G H I J K L],     # Cacodemon
  'SKUL' => %w[G H I J K],       # Lost Soul
  'CYBR' => %w[I J],             # Cyberdemon
  'SPID' => %w[I J K],           # Spider Mastermind
  'CPOS' => %w[H I J K L M N],   # Heavy Weapon Dude
  'PAIN' => %w[H I J K L M],     # Pain Elemental
  'SSWV' => %w[I J K L M],       # Wolfenstein SS
  'BEXP' => %w[A B C D E] # Barrel explosion
}.freeze
DEATH_ANIM_TICS =

Tics per death frame

6
PAIN_CHANCE =

Pain chance per monster (out of 256, from mobjinfo)

{
  3004 => 200, 9 => 170, 3001 => 200, 3002 => 180, 58 => 180,
  3003 => 50, 69 => 50, 3005 => 128, 3006 => 256, 16 => 40,
  7 => 40, 65 => 170, 64 => 10, 71 => 128, 84 => 170
}.freeze
PAIN_DURATION =

Tics monster is stunned when in pain

6
ROCKET_SPEED =

Projectile constants

20.0
ROCKET_DAMAGE =

Map units per tic (matches DOOM's mobjinfo MISSILESPEED)

20
ROCKET_RADIUS =

Direct hit base (DOOM: 1d8 * 20)

11
SPLASH_RADIUS =

Collision radius

128.0
SPLASH_DAMAGE =

Splash damage radius

128
MONSTER_PROJECTILES =

Monster projectile definitions. :radius is the projectile's own collision radius, added to the player radius when testing a hit.

{
  imp: { sprite: 'BAL1', speed: 10.0, damage: [3, 24], radius: 6 },
  baron: { sprite: 'BAL7', speed: 15.0, damage: [8, 64], radius: 6 },
  caco: { sprite: 'BAL2', speed: 10.0, damage: [5, 40], radius: 6 }
}.freeze
MONSTER_PROJECTILE_TYPE =

Map monster type to projectile type

{
  3001 => :imp,     # Imp
  3003 => :baron,   # Baron
  69 => :baron, # Hell Knight
  3005 => :caco # Cacodemon
}.freeze
PLAYER_RADIUS =

Radius used when a shot or a rocket is tested against a player. Matches the 16 the monster-projectile collision already assumed.

16

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(map, sprites = nil, hidden_things = {}, sound_engine = nil, random: Random.new) ⇒ Combat

Weapon damage: DOOM does (P_Random()%3 + 1) * multiplier Pistol/chaingun: 15..35 = 5-15 per bullet Shotgun: 7 pellets, each 15..35 = 5-15 Fist/chainsaw: 12..32 = 2-10



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/doom/game/combat.rb', line 111

def initialize(map, sprites = nil, hidden_things = {}, sound_engine = nil,
               random: Random.new)
  @map = map
  @random = random
  @sprites = sprites
  @hidden_things = hidden_things
  @sound = sound_engine
  @monster_hp = {}     # thing_idx => current HP
  @dead_things = {}    # thing_idx => { tic: death_start_tic, prefix: sprite_prefix }
  @pain_until = {}     # thing_idx => tic when pain ends
  @projectiles = []    # Active projectiles in flight
  @explosions = []     # Active explosions (for rendering)
  @puffs = []          # Bullet puff effects
  @players = []
  @tic = 0
end

Instance Attribute Details

#dead_thingsObject (readonly)

Returns the value of attribute dead_things.



164
165
166
# File 'lib/doom/game/combat.rb', line 164

def dead_things
  @dead_things
end

#explosionsObject (readonly)

Returns the value of attribute explosions.



164
165
166
# File 'lib/doom/game/combat.rb', line 164

def explosions
  @explosions
end

#playersObject

Every player the world knows about. Projectile collision, splash damage and monster aim all consider all of them, not one bound player.



130
131
132
# File 'lib/doom/game/combat.rb', line 130

def players
  @players
end

#projectilesObject (readonly)

Returns the value of attribute projectiles.



164
165
166
# File 'lib/doom/game/combat.rb', line 164

def projectiles
  @projectiles
end

#puffsObject (readonly)

Returns the value of attribute puffs.



164
165
166
# File 'lib/doom/game/combat.rb', line 164

def puffs
  @puffs
end

#spritesObject (readonly)

Returns the value of attribute sprites.



164
165
166
# File 'lib/doom/game/combat.rb', line 164

def sprites
  @sprites
end

Instance Method Details

#dead?(thing_idx) ⇒ Boolean

Returns:

  • (Boolean)


170
171
172
# File 'lib/doom/game/combat.rb', line 170

def dead?(thing_idx)
  @dead_things.key?(thing_idx)
end

#death_sprite(thing_idx, thing_type, viewer_angle, thing_angle) ⇒ Object

Get the current death frame sprite for a dead monster/barrel



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/doom/game/combat.rb', line 175

def death_sprite(thing_idx, thing_type, viewer_angle, thing_angle)
  info = @dead_things[thing_idx]
  return nil unless info

  prefix = info[:prefix]
  frames = DEATH_FRAMES[prefix]
  return nil unless frames

  elapsed = @tic - info[:tic]
  frame_idx = elapsed / DEATH_ANIM_TICS

  # Barrels disappear after explosion animation (S_NULL in Chocolate Doom)
  return nil if thing_type == BARREL_TYPE && frame_idx >= frames.size

  frame_idx = frame_idx.clamp(0, frames.size - 1)
  frame_letter = frames[frame_idx]

  # Use prefix directly if it differs from the thing's sprite (e.g. BEXP for barrels)
  thing_prefix = @sprites.prefix_for(thing_type)
  if prefix == thing_prefix
    @sprites.get_frame(thing_type, frame_letter, viewer_angle, thing_angle)
  else
    @sprites.get_frame_by_prefix(prefix, frame_letter)
  end
end

#fire(px, py, pz, cos_a, sin_a, weapon, shooter = nil) ⇒ Object

Fire the current weapon. shooter is the Player pulling the trigger, needed to credit a deathmatch kill and to stop a shot from hitting the person who fired it.



212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/doom/game/combat.rb', line 212

def fire(px, py, pz, cos_a, sin_a, weapon, shooter = nil)
  case weapon
  when PlayerState::WEAPON_PISTOL, PlayerState::WEAPON_CHAINGUN
    hitscan(px, py, pz, cos_a, sin_a, 1, 0.0, 5, shooter)
  when PlayerState::WEAPON_SHOTGUN
    hitscan(px, py, pz, cos_a, sin_a, 7, Math::PI / 32, 5, shooter)
  when PlayerState::WEAPON_ROCKET
    spawn_rocket(px, py, pz, cos_a, sin_a, shooter)
  when PlayerState::WEAPON_FIST
    melee(px, py, cos_a, sin_a, 2, 64, shooter)
  when PlayerState::WEAPON_CHAINSAW
    melee(px, py, cos_a, sin_a, 2, 64, shooter)
  end
end

#in_pain?(thing_idx) ⇒ Boolean

Returns:

  • (Boolean)


166
167
168
# File 'lib/doom/game/combat.rb', line 166

def in_pain?(thing_idx)
  @pain_until[thing_idx] && @tic < @pain_until[thing_idx]
end

#spawn_monster_projectile(monster_x, monster_y, monster_z, monster_type, damage_multiplier, target) ⇒ Object

Spawn a monster projectile (fireball, etc.) Matches Chocolate Doom's P_SpawnMissile: calculates momz for vertical aim



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/doom/game/combat.rb', line 134

def spawn_monster_projectile(monster_x, monster_y, monster_z, monster_type, damage_multiplier, target)
  proj_type = MONSTER_PROJECTILE_TYPE[monster_type]
  return unless proj_type

  info = MONSTER_PROJECTILES[proj_type]
  return unless info

  dx = target.x - monster_x
  dy = target.y - monster_y
  dist = Math.sqrt((dx * dx) + (dy * dy))
  return if dist < 1

  # Normalize direction and apply speed
  speed = info[:speed]
  ndx = dx / dist * speed
  ndy = dy / dist * speed

  # P_SpawnMissile: momz = (target.z - source.z) / (dist / speed)
  # This makes the projectile arc toward the target's height
  target_z = target.z - 16 # Aim at player center (z + height/2, roughly)
  travel_tics = dist / speed
  travel_tics = 1.0 if travel_tics < 1.0
  ndz = (target_z - monster_z) / travel_tics

  @projectiles << Projectile.new(
    monster_x + (ndx * 2), monster_y + (ndy * 2), monster_z,
    ndx, ndy, ndz, proj_type, @tic, info[:sprite], :player, nil, damage_multiplier
  )
end

#updateObject

Called each game tic



202
203
204
205
206
207
# File 'lib/doom/game/combat.rb', line 202

def update
  @tic += 1
  update_projectiles
  update_explosions
  @puffs.reject! { |p| @tic - p[:tic] > 12 }
end