Class: Doom::Game::MonsterAI

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

#aggressionObject

Returns the value of attribute aggression.



119
120
121
# File 'lib/doom/game/monster_ai.rb', line 119

def aggression
  @aggression
end

#damage_multiplierObject

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_idxObject (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

#monstersObject (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