Class: Doom::Game::MonsterAI
- Inherits:
-
Object
- Object
- Doom::Game::MonsterAI
- Defined in:
- lib/doom/game/monster_ai.rb
Overview
Basic monster AI: idle until seeing player, then chase. Matches Chocolate Doom's A_Look / A_Chase / P_NewChaseDir from p_enemy.c.
Defined Under Namespace
Classes: MonsterState
Constant Summary collapse
- DI_EAST =
8 movement directions + no direction
0- DI_NORTHEAST =
1- DI_NORTH =
2- DI_NORTHWEST =
3- DI_WEST =
4- DI_SOUTHWEST =
5- DI_SOUTH =
6- DI_SOUTHEAST =
7- DI_NODIR =
8- XSPEED =
Movement deltas per direction (map units, 1.0 = FRACUNIT)
[1.0, 0.7071, 0.0, -0.7071, -1.0, -0.7071, 0.0, 0.7071].freeze
- YSPEED =
[0.0, 0.7071, 1.0, 0.7071, 0.0, -0.7071, -1.0, -0.7071].freeze
- OPPOSITE =
[DI_WEST, DI_SOUTHWEST, DI_SOUTH, DI_SOUTHEAST, DI_EAST, DI_NORTHEAST, DI_NORTH, DI_NORTHWEST, DI_NODIR].freeze
- MONSTER_SPEED =
Monster speeds (from mobjinfo)
{ 3004 => 8, 9 => 8, 3001 => 8, 3002 => 10, 58 => 10, 3003 => 8, 69 => 8, 3005 => 8, 3006 => 8, 16 => 16, 7 => 12, 65 => 8, 64 => 15, 71 => 8, 84 => 8 }.freeze
- CHASE_TICS =
Steps between A_Chase calls
4- SIGHT_RANGE =
Max distance for sight check
768.0- MELEE_RANGE =
64.0- MISSILE_RANGE =
768.0- KEEP_DISTANCE =
Ranged monsters prefer to stay this far from player
196.0- MONSTER_ATTACK =
Monster attack definitions (from mobjinfo / A_Chase) Cooldown = attack_anim_tics + avg_movecount(7.5) * chase_tics(4) In DOOM, monsters only attempt attacks when movecount reaches 0, then play full attack animation before returning to chase.
{ 3004 => { type: :hitscan, damage: [3, 15], cooldown: 56 }, # Zombieman 9 => { type: :hitscan, damage: [3, 15], cooldown: 56 }, # Shotgun Guy 3001 => { type: :projectile, cooldown: 52 }, # Imp: fireball 3002 => { type: :melee, damage: [4, 40], cooldown: 42 }, # Demon 58 => { type: :melee, damage: [4, 40], cooldown: 42 }, # Spectre 3003 => { type: :projectile, cooldown: 54 }, # Baron: fireball 69 => { type: :projectile, cooldown: 54 }, # Hell Knight 3005 => { type: :projectile, cooldown: 56 }, # Cacodemon 65 => { type: :hitscan, damage: [3, 15], cooldown: 40 } # Heavy Weapon Dude }.freeze
- REACTIONTIME =
Tics before first attack after activation (from mobjinfo)
8- HITSCAN_ACCURACY =
Hitscan hit probability by distance (DOOM's P_AimLineAttack has bullet spread) The falloff is HITSCAN_ACCURACY * (1 - dist / (MISSILE_RANGE * 2)): close (dist 0) = ~85%, mid (dist 384) = ~64%, far (dist 768) = ~42%.
0.85
- ATTACK_FRAMES =
Attack animation frames per sprite prefix (E, F, G typically)
{ 'POSS' => %w[E F], # Zombieman: raise, fire 'SPOS' => %w[E F], # Shotgun Guy 'TROO' => %w[E F G H], # Imp: raise, fireball, throw, recover 'SARG' => %w[E F G], # Demon: bite 'HEAD' => %w[E F], # Cacodemon 'BOSS' => %w[E F G], # Baron 'BOS2' => %w[E F G], # Hell Knight 'CPOS' => %w[E F] # Heavy Weapon Dude }.freeze
- ATTACK_FRAME_TICS =
Tics per attack animation frame
8- FIRE_FRAME_INDEX =
Which frame index the actual attack happens on (matching Chocolate Doom) Zombieman: A_PosAttack on frame F (index 1) Imp: A_TroopAttack on frame G (index 2) Demon: A_SargAttack on frame F (index 1)
{ 'POSS' => 1, # Zombieman: E=raise, F=fire 'SPOS' => 1, # Shotgun Guy: E=raise, F=fire 'TROO' => 2, # Imp: E=raise, F=aim, G=throw, H=recover 'SARG' => 1, # Demon: E=open, F=bite, G=close 'HEAD' => 1, # Cacodemon: E=charge, F=fire 'BOSS' => 1, # Baron: E=raise, F=throw, G=recover 'BOS2' => 1, # Hell Knight 'CPOS' => 1 # Heavy Weapon Dude }.freeze
Instance Attribute Summary collapse
-
#aggression ⇒ Object
Returns the value of attribute aggression.
-
#damage_multiplier ⇒ Object
Returns the value of attribute damage_multiplier.
-
#monster_by_thing_idx ⇒ Object
readonly
Returns the value of attribute monster_by_thing_idx.
-
#monsters ⇒ Object
readonly
Returns the value of attribute monsters.
Instance Method Summary collapse
-
#initialize(map, combat, sprites_mgr = nil, hidden_things = {}, sound_engine = nil, random: Random.new) ⇒ MonsterAI
constructor
A new instance of MonsterAI.
-
#update(players) ⇒ Object
Called each game tic with every live player.
Constructor Details
#initialize(map, combat, sprites_mgr = nil, hidden_things = {}, sound_engine = nil, random: Random.new) ⇒ MonsterAI
Returns a new instance of MonsterAI.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/doom/game/monster_ai.rb', line 96 def initialize(map, combat, sprites_mgr = nil, hidden_things = {}, sound_engine = nil, random: Random.new) @map = map @combat = combat @random = random @sprites_mgr = sprites_mgr @monsters = [] @aggression = true # Monsters fight back (toggle with C) @damage_multiplier = 1.0 @tic_counter = 0 @sound = sound_engine @monster_by_thing_idx = {} map.things.each_with_index do |thing, idx| next if hidden_things[idx] # Filtered by difficulty next unless Combat::MONSTER_HP[thing.type] next if thing.type == Combat::BARREL_TYPE mon = MonsterState.new( idx, thing.x.to_f, thing.y.to_f, DI_NODIR, 0, false, 0, thing.type, 0, REACTIONTIME, 0, false, 0, false, nil ) @monsters << mon @monster_by_thing_idx[idx] = mon end end |
Instance Attribute Details
#aggression ⇒ Object
Returns the value of attribute aggression.
125 126 127 |
# File 'lib/doom/game/monster_ai.rb', line 125 def aggression @aggression end |
#damage_multiplier ⇒ Object
Returns the value of attribute damage_multiplier.
125 126 127 |
# File 'lib/doom/game/monster_ai.rb', line 125 def damage_multiplier @damage_multiplier end |
#monster_by_thing_idx ⇒ Object (readonly)
Returns the value of attribute monster_by_thing_idx.
124 125 126 |
# File 'lib/doom/game/monster_ai.rb', line 124 def monster_by_thing_idx @monster_by_thing_idx end |
#monsters ⇒ Object (readonly)
Returns the value of attribute monsters.
124 125 126 |
# File 'lib/doom/game/monster_ai.rb', line 124 def monsters @monsters end |
Instance Method Details
#update(players) ⇒ Object
Called each game tic with every live player. Each monster keeps its own target so a second player walking past does not yank a monster off the one it is already fighting; it re-picks only when its target dies or leaves.
131 132 133 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 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/doom/game/monster_ai.rb', line 131 def update(players) players = Array(players) @tic_counter += 1 @monsters.each do |mon| target = select_target(mon, players) next unless target next if @combat.dead?(mon.thing_idx) # Pain state: monster is stunned, skip movement and attacks next if @combat.in_pain?(mon.thing_idx) if mon.active # Attack animation in progress: freeze movement, tick animation if mon.attacking mon.attack_frame_tic += 1 prefix = @sprites_mgr&.prefix_for(mon.type) frames = ATTACK_FRAMES[prefix] total_tics = (frames&.size || 2) * ATTACK_FRAME_TICS # Fire on the correct frame (matching Chocolate Doom) fire_idx = FIRE_FRAME_INDEX[prefix] || 1 fire_tic = fire_idx * ATTACK_FRAME_TICS if !mon.fired && mon.attack_frame_tic >= fire_tic execute_attack(mon, target) mon.fired = true end if mon.attack_frame_tic >= total_tics mon.attacking = false mon.attack_frame_tic = 0 mon.fired = false end next end mon.chase_timer -= 1 if mon.chase_timer <= 0 mon.chase_timer = CHASE_TICS chase(mon, target) end else look(mon, target) end end end |