class MoveAction

typed: true

Attributes

as_bonus_action[RW]
as_dash[RW]
jump_index[RW]
move_path[RW]

Public Class Methods

apply!(battle, item) click to toggle source
# File lib/natural_20/actions/move_action.rb, line 147
def self.apply!(battle, item)
    case (item[:type])
    when :state
      item[:params].each do |k, v|
        item[:source].send(:"#{k}=", v)
      end
    when :acrobatics, :athletics
      if item[:success]
        Natural20::EventManager.received_event(source: item[:source], event: item[:type], success: true,
                                               roll: item[:roll])
      else
        Natural20::EventManager.received_event(source: item[:source], event: item[:type], success: false,
                                               roll: item[:roll])
        item[:source].prone!
      end
    when :drop_grapple
      item[:target].escape_grapple_from!(@source)
      Natural20::EventManager.received_event(event: :drop_grapple,
                                             target: item[:target], source: @source,
                                             source_roll: item[:source_roll],
                                             target_roll: item[:target_roll])

    when :move
      item[:map].move_to!(item[:source], *item[:position], battle)
      if item[:as_dash] && item[:as_bonus_action]
        battle.entity_state_for(item[:source])[:bonus_action] -= 1
      elsif item[:as_dash]
        battle.entity_state_for(item[:source])[:action] -= 1
      elsif battle
        battle.entity_state_for(item[:source])[:movement] -= item[:move_cost] * battle.map.feet_per_grid
      end

      Natural20::EventManager.received_event({ event: :move, source: item[:source], position: item[:position], path: item[:path],
                                               feet_per_grid: battle.map&.feet_per_grid,
                                               as_dash: item[:as_dash], as_bonus: item[:as_bonus_action] })
    end
end
build(session, source) click to toggle source
# File lib/natural_20/actions/move_action.rb, line 32
def self.build(session, source)
  action = MoveAction.new(session, source, :move)
  action.build_map
end
can?(entity, battle) click to toggle source
# File lib/natural_20/actions/move_action.rb, line 8
def self.can?(entity, battle)
  battle.nil? || entity.available_movement(battle).positive?
end

Public Instance Methods

build_map() click to toggle source
# File lib/natural_20/actions/move_action.rb, line 12
def build_map
  OpenStruct.new({
                   action: self,
                   param: [
                     {
                       type: :movement
                     }
                   ],
                   next: lambda { |path_and_jump_index|
                     path, jump_index = path_and_jump_index
                     self.move_path = path
                     self.jump_index = jump_index
                     OpenStruct.new({
                                      param: nil,
                                      next: -> { self }
                                    })
                   }
                 })
end
check_opportunity_attacks(entity, move_list, battle, grappled: false) click to toggle source

@param entity [Natural20::Entity] @param move_list [Array<Array<Integer,Integer>>] @param battle [Natural20::Battle]

# File lib/natural_20/actions/move_action.rb, line 133
def check_opportunity_attacks(entity, move_list, battle, grappled: false)
  if battle

    retrieve_opportunity_attacks(entity, move_list, battle).each do |enemy_opporunity|
      original_location = move_list[0...enemy_opporunity[:path]]
      attack_location = original_location.last
      battle.trigger_opportunity_attack(enemy_opporunity[:source], entity, *attack_location)

      return original_location if !grappled && !entity.conscious?
    end
  end
  move_list
end
resolve(_session, map, opts = {}) click to toggle source

@param map [Natural20::BattleMap] @option opts battle [Natural20::Battle]

# File lib/natural_20/actions/move_action.rb, line 39
def resolve(_session, map, opts = {})
  raise 'no path specified' if (move_path.nil? || move_path.empty?) && opts[:move_path].nil?

  @result = []
  # check for melee opportunity attacks
  battle = opts[:battle]

  current_moves = move_path.presence || opts[:move_path]
  jumps = jump_index || []

  actual_moves = []
  additional_effects = []

  movement_budget = if as_dash
                      (@source.speed / 5).floor
                    else
                      (@source.available_movement(battle) / 5).floor
                    end

  movement = compute_actual_moves(@source, current_moves, map, battle, movement_budget, manual_jump: jumps)
  actual_moves = movement.movement

  actual_moves.pop while actual_moves.last && !map.placeable?(@source, *actual_moves.last, battle)

  actual_moves = check_opportunity_attacks(@source, actual_moves, battle)

  # check if movement requires athletics checks
  actual_moves = check_movement_athletics(actual_moves, movement.athletics_check_locations, battle, map)

  # check if movement requires dex checks, e.g. jumping and landing on difficult terrain
  actual_moves = check_movement_acrobatics(actual_moves, movement.acrobatics_check_locations, battle)

  # calculate for area based triggers
  cutoff = false

  safe_moves = []
  actual_moves.each_with_index do |move, _index|
    is_flying_or_jumping = movement.jump_locations.include?(move)
    trigger_results = map.area_trigger!(@source, move, is_flying_or_jumping)
    if trigger_results.empty?
      safe_moves << move
    else
      safe_moves << move
      additional_effects += trigger_results
      break
    end
  end

  movement = compute_actual_moves(@source, safe_moves, map, battle, movement_budget, manual_jump: jumps)

  # compute grappled entity movement
  if @source.grappling?
    grappled_movement = movement.movement.dup
    grappled_movement.pop

    @source.grappling_targets.each do |grappling_target|
      start_pos = map.entity_or_object_pos(grappling_target)
      grappled_entity_movement = [start_pos] + grappled_movement

      additional_effects << {
        source: grappling_target,
        map: map,
        battle: battle,
        type: :move,
        path: grappled_entity_movement,
        as_dash: as_dash,
        as_bonus_action: as_bonus_action,
        move_cost: 0,
        position: grappled_entity_movement.last
      }

      grappled_movement.pop
    end
  end

  @result << {
    source: @source,
    map: map,
    battle: battle,
    as_dash: as_dash,
    as_bonus_action: as_bonus_action,
    type: :move,
    path: movement.movement,
    move_cost: movement_budget - movement.budget,
    position: movement.movement.last
  }
  @result += additional_effects

  self
end

Private Instance Methods

check_movement_acrobatics(actual_moves, dexterity_checks, battle) click to toggle source

@param actual_moves [Array] @param dexterity_checks [Array] @param battle [Natural20::Battle] @return [Array]

# File lib/natural_20/actions/move_action.rb, line 191
def check_movement_acrobatics(actual_moves, dexterity_checks, battle)
  cutoff = actual_moves.size - 1
  actual_moves.each_with_index do |m, index|
    next unless dexterity_checks.include?(m)

    acrobatics_roll = @source.acrobatics_check!(battle)
    if acrobatics_roll.result >= 10
      @result << { source: @source, type: :acrobatics, success: true, roll: acrobatics_roll, location: m }
    else
      @result << { source: @source, type: :acrobatics, success: false, roll: acrobatics_roll, location: m }
      cutoff = index
      break
    end
  end
  actual_moves[0..cutoff]
end
check_movement_athletics(actual_moves, athletics_checks, battle, map) click to toggle source

@param actual_moves [Array] @param athletics_checks [Array] @param battle [Natural20::Battle] @param map [Natural20::BattleMap] @return [Array]

# File lib/natural_20/actions/move_action.rb, line 213
def check_movement_athletics(actual_moves, athletics_checks, battle, map)
  cutoff = actual_moves.size - 1
  actual_moves.each_with_index do |m, index|
    next unless athletics_checks.include?(m)

    athletics_roll = @source.athletics_check!(battle)
    if athletics_roll.result >= 10
      @result << { source: @source, type: :athletics, success: true, roll: athletics_roll, location: m }
    else
      @result << { source: @source, type: :athletics, success: false, roll: athletics_roll, location: m }
      cutoff = index - 1
      cutoff -= 1 while cutoff >= 0 && !map.placeable?(@source, *actual_moves[cutoff], battle)
      break
    end
  end

  actual_moves[0..cutoff]
end