Class: AttackAction

Inherits:
Natural20::Action show all
Extended by:
Natural20::ActionDamage
Includes:
Natural20::AttackHelper, Natural20::Cover, Natural20::Weapons
Defined in:
lib/natural_20/actions/attack_action.rb

Overview

typed: true

Direct Known Subclasses

TwoWeaponAttackAction

Instance Attribute Summary collapse

Attributes inherited from Natural20::Action

#action_type, #errors, #result, #session, #source

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Natural20::ActionDamage

damage_event

Methods included from Natural20::AttackHelper

#after_attack_roll_hook, #calculate_cover_ac, #effective_ac

Methods included from Natural20::Weapons

#compute_advantages_and_disadvantages, #damage_modifier, #target_advantage_condition

Methods included from Natural20::Cover

#cover_calculation

Methods inherited from Natural20::Action

#initialize, #name, #validate

Constructor Details

This class inherits a constructor from Natural20::Action

Instance Attribute Details

#advantage_modObject (readonly)

Returns the value of attribute advantage_mod.



9
10
11
# File 'lib/natural_20/actions/attack_action.rb', line 9

def advantage_mod
  @advantage_mod
end

#as_reactionObject

Returns the value of attribute as_reaction.



8
9
10
# File 'lib/natural_20/actions/attack_action.rb', line 8

def as_reaction
  @as_reaction
end

#npc_actionObject

Returns the value of attribute npc_action.



8
9
10
# File 'lib/natural_20/actions/attack_action.rb', line 8

def npc_action
  @npc_action
end

#second_handObject

Returns the value of attribute second_hand.



8
9
10
# File 'lib/natural_20/actions/attack_action.rb', line 8

def second_hand
  @second_hand
end

#targetObject

Returns the value of attribute target.



8
9
10
# File 'lib/natural_20/actions/attack_action.rb', line 8

def target
  @target
end

#thrownObject

Returns the value of attribute thrown.



8
9
10
# File 'lib/natural_20/actions/attack_action.rb', line 8

def thrown
  @thrown
end

#usingObject

Returns the value of attribute using.



8
9
10
# File 'lib/natural_20/actions/attack_action.rb', line 8

def using
  @using
end

Class Method Details

.apply!(battle, item) ⇒ Object

Parameters:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/natural_20/actions/attack_action.rb', line 74

def self.apply!(battle, item)
  if item[:flavor]
    Natural20::EventManager.received_event({ event: :flavor, source: item[:source], target: item[:target],
                                             text: item[:flavor] })
  end
  case (item[:type])
  when :prone
    item[:source].prone!
  when :damage
    damage_event(item, battle)
    consume_resource(battle, item)
  when :miss
    consume_resource(battle, item)
    Natural20::EventManager.received_event({ attack_roll: item[:attack_roll],
                                             attack_name: item[:attack_name],
                                             advantage_mod: item[:advantage_mod],
                                             as_reaction: !!item[:as_reaction],
                                             adv_info: item[:adv_info],
                                             source: item[:source], target: item[:target], event: :miss })
  end
end

.build(session, source) ⇒ Object



68
69
70
71
# File 'lib/natural_20/actions/attack_action.rb', line 68

def self.build(session, source)
  action = AttackAction.new(session, source, :attack)
  action.build_map
end

.can?(entity, battle, options = {}) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


14
15
16
17
18
19
20
# File 'lib/natural_20/actions/attack_action.rb', line 14

def self.can?(entity, battle, options = {})
  return entity.total_reactions(battle).positive? if battle && options[:opportunity_attack]

  battle.nil? || entity.total_actions(battle).positive? || entity.multiattack?(
    battle, options[:npc_action]
  )
end

.consume_resource(battle, item) ⇒ Object

Parameters:



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
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
# File 'lib/natural_20/actions/attack_action.rb', line 98

def self.consume_resource(battle, item)
  # handle ammo
  item[:source].deduct_item(item[:ammo], 1) if item[:ammo]

  # hanle thrown items
  if item[:thrown]
    if item[:source].item_count(item[:weapon]).positive?
      item[:source].deduct_item(item[:weapon], 1)
    else
      item[:source].unequip(item[:weapon], transfer_inventory: false)
    end

    if item[:type] == :damage
      item[:target].add_item(item[:weapon])
    else
      ground_pos = item[:battle].map.entity_or_object_pos(item[:target])
      ground_object = item[:battle].map.objects_at(*ground_pos).detect { |o| o.is_a?(ItemLibrary::Ground) }
      ground_object&.add_item(item[:weapon])
    end
  end

  if item[:as_reaction]
    battle.consume(item[:source], :reaction)
  elsif item[:second_hand]
    battle.consume(item[:source], :bonus_action)
  else
    battle.consume(item[:source], :action)
  end

  item[:source].break_stealth!(battle)

  # handle two-weapon fighting
  weapon = battle.session.load_weapon(item[:weapon]) if item[:weapon]

  if weapon && weapon[:properties]&.include?('light') && !battle.two_weapon_attack?(item[:source]) && !item[:second_hand]
    battle.entity_state_for(item[:source])[:two_weapon] = item[:weapon]
  elsif battle.entity_state_for(item[:source])
    battle.entity_state_for(item[:source])[:two_weapon] = nil
  end

  # handle multiattacks
  if battle.entity_state_for(item[:source])
    battle.entity_state_for(item[:source])[:multiattack]&.each do |_group, attacks|
      if attacks.include?(item[:attack_name])
        attacks.delete(item[:attack_name])
        item[:source].clear_multiattack!(battle) if attacks.empty?
      end
    end
  end

  # dismiss help actions
  battle.dismiss_help_for(item[:target])
end

Instance Method Details

#build_mapObject



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/natural_20/actions/attack_action.rb', line 40

def build_map
  OpenStruct.new({
                   action: self,
                   param: [
                     {
                       type: :select_target,
                       num: 1,
                       weapon: using
                     }
                   ],
                   next: lambda { |target|
                     self.target = target
                     OpenStruct.new({
                                      param: [
                                        { type: :select_weapon }
                                      ],
                                      next: lambda { |weapon|
                                              self.using = weapon
                                              OpenStruct.new({
                                                               param: nil,
                                                               next: -> { self }
                                                             })
                                            }
                                    })
                   }
                 })
end

#labelObject



26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/natural_20/actions/attack_action.rb', line 26

def label
  if @npc_action
    t('action.npc_action', name: @action_type.to_s.humanize, action_name: npc_action[:name])
  else
    weapon = session.load_weapon(@opts[:using] || @using)
    attack_mod = @source.attack_roll_mod(weapon)

    i18n_token = thrown ? 'action.attack_action_throw' : 'action.attack_action'

    t(i18n_token, name: @action_type.to_s.humanize, weapon_name: weapon[:name], mod: attack_mod >= 0 ? "+#{attack_mod}" : attack_mod,
                  dmg: damage_modifier(@source, weapon, second_hand: second_hand))
  end
end

#resolve(_session, map, opts = {}) ⇒ Object

Build the attack roll information

Parameters:

Options Hash (opts):



165
166
167
168
169
170
171
172
173
174
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/natural_20/actions/attack_action.rb', line 165

def resolve(_session, map, opts = {})
  @result.clear
  target = opts[:target] || @target
  raise 'target is a required option for :attack' if target.nil?

  npc_action = opts[:npc_action] || @npc_action
  battle = opts[:battle]
  using = opts[:using] || @using
  raise 'using or npc_action is a required option for :attack' if using.nil? && npc_action.nil?

  attack_name = nil
  damage_roll = nil
  sneak_attack_roll = nil
  ammo_type = nil

  npc_action = @source.npc_actions.detect { |a| a[:name].downcase == using.downcase } if @source.npc? && using

  if @source.npc?

    if npc_action.nil?
      npc_action = @source.properties[actions].detect do |action|
        action[:name].downcase == using.to_s.downcase
      end
    end
    weapon = npc_action
    attack_name = npc_action[:name]
    attack_mod = npc_action[:attack]
    damage_roll = npc_action[:damage_die]
    ammo_type = npc_action[:ammo]
  else
    weapon = session.load_weapon(using.to_sym)
    attack_name = weapon[:name]
    ammo_type = weapon[:ammo]
    attack_mod = @source.attack_roll_mod(weapon)
    damage_roll = damage_modifier(@source, weapon, second_hand: second_hand)
  end

  # DnD 5e advantage/disadvantage checks
  @advantage_mod, adv_info = target_advantage_condition(battle, @source, target, weapon)

  # determine eligibility for the 'Protection' fighting style
  evaluate_feature_protection(battle, map, target, adv_info) if map

  # perform the dice rolls
  attack_roll = Natural20::DieRoll.roll("1d20+#{attack_mod}", disadvantage: with_disadvantage?,
                                                              advantage: with_advantage?,
                                                              description: t('dice_roll.attack'), entity: @source, battle: battle)

  # handle the lucky feat
  attack_roll = attack_roll.reroll(lucky: true) if @source.class_feature?('lucky') && attack_roll.nat_1?
  target_ac, _cover_ac = effective_ac(battle, target)
  after_attack_roll_hook(battle, target, source, attack_roll, target_ac)

  if @source.class_feature?('sneak_attack') && (weapon[:properties]&.include?('finesse') || weapon[:type] == 'ranged_attack') && (with_advantage? || battle.enemy_in_melee_range?(
    target, [@source]
  ))
    sneak_attack_roll = Natural20::DieRoll.roll(@source.sneak_attack_level, crit: attack_roll.nat_20?,
                                                                            description: t('dice_roll.sneak_attack'), entity: @source, battle: battle)
  end

  damage = Natural20::DieRoll.roll(damage_roll, crit: attack_roll.nat_20?, description: t('dice_roll.damage'),
                                                entity: @source, battle: battle)

  if @source.class_feature?('great_weapon_fighting') && (weapon[:properties]&.include?('two_handed') || (weapon[:properties]&.include?('versatile') && entity.used_hand_slots <= 1.0))
    damage.rolls.map do |roll|
      if [1, 2].include?(roll)
        r = Natural20::DieRoll.roll("1d#{damage.die_sides}", description: t('dice_roll.great_weapon_fighting_reroll'),
                                                             entity: @source, battle: battle)
        Natural20::EventManager.received_event({ roll: r, prev_roll: roll,
                                                 source: item[:source], event: :great_weapon_fighting_roll })
        r.result
      else
        roll
      end
    end
  end

  # apply weapon bonus attacks
  damage = check_weapon_bonuses(battle, weapon, damage, attack_roll)

  cover_ac_adjustments = 0
  hit = if attack_roll.nat_20?
          true
        elsif attack_roll.nat_1?
          false
        else
          target_ac, cover_ac_adjustments = effective_ac(battle, target)
          attack_roll.result >= target_ac
        end

  if hit
    @result << {
      source: @source,
      target: target,
      type: :damage,
      thrown: thrown,
      weapon: using,
      battle: battle,
      advantage_mod: @advantage_mod,
      damage_roll: damage_roll,
      attack_name: attack_name,
      attack_roll: attack_roll,
      sneak_attack: sneak_attack_roll,
      target_ac: target.armor_class,
      cover_ac: cover_ac_adjustments,
      adv_info: adv_info,
      hit?: hit,
      damage_type: weapon[:damage_type],
      damage: damage,
      ammo: ammo_type,
      as_reaction: !!as_reaction,
      second_hand: second_hand,
      npc_action: npc_action
    }
    unless weapon[:on_hit].blank?
      weapon[:on_hit].each do |effect|
        next if effect[:if] && !@source.eval_if(effect[:if], weapon: weapon, target: target)

        if effect[:save_dc]
          save_type, dc = effect[:save_dc].split(':')
          raise 'invalid values: save_dc should be of the form <save>:<dc>' if save_type.blank? || dc.blank?
          raise 'invalid save type' unless Natural20::Entity::ATTRIBUTE_TYPES.include?(save_type)

          save_roll = target.saving_throw!(save_type, battle: battle)
          if save_roll.result >= dc.to_i
            if effect[:success]
              @result << target.apply_effect(effect[:success], battle: battle,
                                                               flavor: effect[:flavor_success])
            end
          elsif effect[:fail]
            @result << target.apply_effect(effect[:fail], battle: battle, flavor: effect[:flavor_fail])
          end
        else
          target.apply_effect(effect[:effect])
        end
      end
    end
  else
    @result << {
      attack_name: attack_name,
      source: @source,
      target: target,
      weapon: using,
      battle: battle,
      thrown: thrown,
      type: :miss,
      advantage_mod: @advantage_mod,
      adv_info: adv_info,
      second_hand: second_hand,
      damage_roll: damage_roll,
      attack_roll: attack_roll,
      as_reaction: !!as_reaction,
      target_ac: target.armor_class,
      cover_ac: cover_ac_adjustments,
      ammo: ammo_type,
      npc_action: npc_action
    }
  end

  self
end

#to_sObject



22
23
24
# File 'lib/natural_20/actions/attack_action.rb', line 22

def to_s
  @action_type.to_s.humanize
end

#with_advantage?Boolean

Returns:

  • (Boolean)


152
153
154
# File 'lib/natural_20/actions/attack_action.rb', line 152

def with_advantage?
  @advantage_mod.positive?
end

#with_disadvantage?Boolean

Returns:

  • (Boolean)


156
157
158
# File 'lib/natural_20/actions/attack_action.rb', line 156

def with_disadvantage?
  @advantage_mod.negative?
end