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- DIR_ANGLES =
Direction to angle (for sprite facing)
[0, 45, 90, 135, 180, 225, 270, 315].freeze
- 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) Close = ~85%, mid = ~60%, far = ~35%
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, player_state, sprites_mgr = nil, hidden_things = {}, sound_engine = nil) ⇒ MonsterAI
constructor
A new instance of MonsterAI.
-
#update(player_x, player_y) ⇒ Object
Called each game tic.
Constructor Details
#initialize(map, combat, player_state, sprites_mgr = nil, hidden_things = {}, sound_engine = nil) ⇒ MonsterAI
Returns a new instance of MonsterAI.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/doom/game/monster_ai.rb', line 92 def initialize(map, combat, player_state, sprites_mgr = nil, hidden_things = {}, sound_engine = nil) @map = map @combat = combat @player = player_state @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 ) @monsters << mon @monster_by_thing_idx[idx] = mon end end |
Instance Attribute Details
#aggression ⇒ Object
Returns the value of attribute aggression.
119 120 121 |
# File 'lib/doom/game/monster_ai.rb', line 119 def aggression @aggression end |
#damage_multiplier ⇒ Object
Returns the value of attribute damage_multiplier.
119 120 121 |
# File 'lib/doom/game/monster_ai.rb', line 119 def damage_multiplier @damage_multiplier end |
#monster_by_thing_idx ⇒ Object (readonly)
Returns the value of attribute monster_by_thing_idx.
118 119 120 |
# File 'lib/doom/game/monster_ai.rb', line 118 def monster_by_thing_idx @monster_by_thing_idx end |
#monsters ⇒ Object (readonly)
Returns the value of attribute monsters.
118 119 120 |
# File 'lib/doom/game/monster_ai.rb', line 118 def monsters @monsters end |
Instance Method Details
#update(player_x, player_y) ⇒ Object
Called each game tic
122 123 124 125 126 127 128 129 130 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 |
# File 'lib/doom/game/monster_ai.rb', line 122 def update(player_x, player_y) @tic_counter += 1 @monsters.each do |mon| 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, player_x, player_y) 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, player_x, player_y) end else look(mon, player_x, player_y) end end end |