class AttackAction
typed: true
Attributes
advantage_mod[R]
as_reaction[RW]
npc_action[RW]
second_hand[RW]
target[RW]
thrown[RW]
using[RW]
Public Class Methods
apply!(battle, item)
click to toggle source
@param battle [Natural20::Battle]
# 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)
click to toggle source
# 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 = {})
click to toggle source
@param entity [Natural20::Entity] @param battle [Natural20::Battle] @return [Boolean]
# 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)
click to toggle source
@param battle [Natural20::Battle] @param item [Hash]
# 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
Public Instance Methods
build_map()
click to toggle source
# 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
label()
click to toggle source
# 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 = {})
click to toggle source
Build the attack roll information @param session [Natural20::Session] @param map [Natural20::BattleMap] @option opts battle [Natural20::Battle] @option opts target [Natural20::Entity]
# 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_s()
click to toggle source
# File lib/natural_20/actions/attack_action.rb, line 22 def to_s @action_type.to_s.humanize end
with_advantage?()
click to toggle source
# File lib/natural_20/actions/attack_action.rb, line 152 def with_advantage? @advantage_mod.positive? end
with_disadvantage?()
click to toggle source
# File lib/natural_20/actions/attack_action.rb, line 156 def with_disadvantage? @advantage_mod.negative? end
Protected Instance Methods
check_weapon_bonuses(battle, weapon, damage_roll, attack_roll)
click to toggle source
# File lib/natural_20/actions/attack_action.rb, line 355 def check_weapon_bonuses(battle, weapon, damage_roll, attack_roll) if weapon.dig(:bonus, :additional, :restriction) == 'nat20_attack' && attack_roll.nat_20? damage_roll += Natural20::DieRoll.roll(weapon.dig(:bonus, :additional, :die), description: t('dice_roll.special_weapon_damage'), entity: @source, battle: battle) end damage_roll end
evaluate_feature_protection(battle, map, target, adv_info)
click to toggle source
determine eligibility for the 'Protection' fighting style
# File lib/natural_20/actions/attack_action.rb, line 330 def evaluate_feature_protection(battle, map, target, adv_info) melee_sqaures = target.melee_squares(map, adjacent_only: true) melee_sqaures.each do |pos| entity = map.entity_at(*pos) next if entity == @source next if entity == target next unless entity next unless entity.class_feature?('protection') && entity.shield_equipped? && entity.has_reaction?(battle) controller = battle.controller_for(entity) if controller.respond_to?(:reaction) && !controller.reaction(:feature_protection, target: target, source: entity, attacker: @source) next end Natural20::EventManager.received_event(event: :feature_protection, target: target, source: entity, attacker: @source) _advantage, disadvantage = adv_info disadvantage << :protection @advantage_mod = -1 battle.consume(entity, :reaction) end end