Class: AiController::Standard

Inherits:
Natural20::Controller show all
Includes:
Natural20::MovementHelper, Natural20::Navigation
Defined in:
lib/natural_20/ai_controller/standard.rb

Overview

Class used for handling “Standard” NPC AI

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Natural20::Navigation

#candidate_squares, #evaluate_square

Methods included from Natural20::Weapons

#compute_advantages_and_disadvantages, #damage_modifier, #target_advantage_condition

Methods included from Natural20::MovementHelper

#compute_actual_moves, #requires_squeeze?, #retrieve_opportunity_attacks, #valid_move_path?

Methods inherited from Natural20::Controller

#roll_for

Constructor Details

#initializeStandard

Returns a new instance of Standard.



10
11
12
# File 'lib/natural_20/ai_controller/standard.rb', line 10

def initialize
  @battle_data = {}
end

Instance Attribute Details

#battle_dataObject (readonly)

Returns the value of attribute battle_data.



8
9
10
# File 'lib/natural_20/ai_controller/standard.rb', line 8

def battle_data
  @battle_data
end

Instance Method Details

#action_listener(battle, action, _opt) ⇒ Object

Parameters:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/natural_20/ai_controller/standard.rb', line 60

def action_listener(battle, action, _opt)
  # handle unaware npcs
  new_npcs = battle.map.unaware_npcs.map do |unaware_npc_info|
    npc = unaware_npc_info[:entity]
    next unless npc.conscious?
    next unless battle.map.can_see?(npc, action.source)

    register_handlers_on(npc) # attach this AI controller to this NPC
    battle.add(npc, unaware_npc_info[:group])
    update_enemy_known_position(battle, npc, action.source, battle.map.entity_squares(action.source).first)
    npc
  end
  new_npcs.each { |npc| battle.map.unaware_npcs.delete(npc) }
end

#attack_listener(battle, target) ⇒ Object



49
50
51
52
53
54
55
56
# File 'lib/natural_20/ai_controller/standard.rb', line 49

def attack_listener(battle, target)
  unaware_npc_info = battle.map.unaware_npcs.detect { |n| n[:entity] == target }
  return unless unaware_npc_info

  register_handlers_on(target) # attach this AI controller to this NPC
  battle.add(target, unaware_npc_info[:group])
  battle.map.unaware_npcs.delete(target)
end

#has_appropriate_weapon?Boolean

tests if npc has an appropriate weapon to at least one visible enemy

Returns:

  • (Boolean)


109
# File 'lib/natural_20/ai_controller/standard.rb', line 109

def has_appropriate_weapon?; end

#move_for(entity, battle) ⇒ Array

Tells AI to compute moves for an entity

Parameters:

Returns:

  • (Array)


115
116
117
118
119
120
121
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/natural_20/ai_controller/standard.rb', line 115

def move_for(entity, battle)
  initialize_battle_data(battle, entity)

  known_enemy_positions = @battle_data[battle][entity][:known_enemy_positions]
  hiding_spots = @battle_data[battle][entity][:hiding_spots]
  investigate_location = @battle_data[battle][entity][:investigate_location]

  enemy_positions = {}
  observe_enemies(battle, entity, enemy_positions)

  available_actions = entity.available_actions(@session, battle)

  # generate available targets
  valid_actions = []

  if enemy_positions.empty? && investigate_location.empty? && LookAction.can?(entity, battle)
    action = LookAction.new(battle.session, entity, :look)
    return action
  end

  # try to stand if prone
  valid_actions << StandAction.new(@session, entity, :stand) if entity.prone? && StandAction.can?(entity, battle)

  available_actions.select { |a| a.action_type == :attack }.each do |action|
    next unless action.npc_action

    valid_targets = battle.valid_targets_for(entity, action)
    unless valid_targets.first.nil?
      action.target = valid_targets.first
      valid_actions << action
    end
  end

  # movement planner if no more attack options and enemies are in sight
  if valid_actions.empty? && !enemy_positions.empty?
    valid_actions += generate_moves_for_positions(battle, entity, enemy_positions)
  end

  # attempt to investigate last seen positions
  if enemy_positions.empty?
    my_group = battle.entity_group_for(entity)
    investigate_location = known_enemy_positions.map do |enemy, position|
      group = battle.entity_group_for(enemy)
      next if my_group == group

      [enemy, position]
    end.compact.to_h

    valid_actions += generate_moves_for_positions(battle, entity, investigate_location)
  end

  if HideBonusAction.can?(entity, battle) # bonus action hide if able
    hide_action = HideBonusAction.new(battle.session, entity, :hide_bonus)
    hide_action.as_bonus_action = true
    valid_actions << hide_action
  end

  if valid_actions.first&.action_type == :move && DisengageBonusAction.can?(entity,
                                                                            battle) && !retrieve_opportunity_attacks(
                                                                              entity, valid_actions.first.move_path, battle
                                                                            ).empty?
    return DisengageBonusAction.new(battle.session, entity, :disengage_bonus)
  end

  valid_actions << DodgeAction.new(battle.session, entity, :dodge) if entity.action?(battle)

  return valid_actions.first unless valid_actions.empty?
end

#movement_listener(battle, entity, position) ⇒ Object

Parameters:



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/natural_20/ai_controller/standard.rb', line 19

def movement_listener(battle, entity, position)
  move_path = position[:move_path]

  return if move_path.nil?

  # handle unaware npcs
  new_npcs = battle.map.unaware_npcs.map do |unaware_npc_info|
    npc = unaware_npc_info[:entity]
    seen = !!move_path.reverse.detect do |path|
      npc.conscious? && battle.map.can_see?(npc, entity, entity_2_pos: path)
    end

    next unless seen

    register_handlers_on(npc) # attach this AI controller to this NPC
    battle.add(npc, unaware_npc_info[:group])
    npc
  end.compact
  new_npcs.each { |npc| battle.map.unaware_npcs.delete(npc) }

  # update seen info for each npc
  battle.entities.each_key do |e|
    loc = move_path.reverse.detect do |location|
      battle.map.can_see?(entity, e, entity_2_pos: location)
    end
    # include friendlies as well since they can turn on us at one point in time :)
    update_enemy_known_position(e, entity, *loc) if loc
  end
end

#opportunity_attack_listener(battle, session, entity, map, event) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/natural_20/ai_controller/standard.rb', line 75

def opportunity_attack_listener(battle, session, entity, map, event)
  entity_x, entity_y = map.position_of(entity)
  target_x, target_y = event[:position]

  distance = Math.sqrt((target_x - entity_x)**2 + (target_y - entity_y)**2).ceil

  action = entity.available_actions(session, battle, opportunity_attack: true).select do |s|
    distance <= s.npc_action[:range]
  end.first

  if action
    action.target = event[:target]
    action.as_reaction = true
  end
  action
end

#register_battle_listeners(battle) ⇒ Object



92
93
94
95
96
97
98
99
100
101
# File 'lib/natural_20/ai_controller/standard.rb', line 92

def register_battle_listeners(battle)
  # detects noisy things
  battle.add_battlefield_event_listener(:sound, self, :sound_listener)

  # detects line of sight movement
  battle.add_battlefield_event_listener(:movement, self, :movement_listener)

  # actions listener (check if doors opened etc)
  battle.add_battlefield_event_listener(:interact, self, :action_listener)
end

#register_handlers_on(entity) ⇒ Object

Parameters:



104
105
106
# File 'lib/natural_20/ai_controller/standard.rb', line 104

def register_handlers_on(entity)
  entity.attach_handler(:opportunity_attack, self, :opportunity_attack_listener)
end

#sound_listener(battle, entity, position, stealth) ⇒ Object



14
# File 'lib/natural_20/ai_controller/standard.rb', line 14

def sound_listener(battle, entity, position, stealth); end