class Natural20::PlayerCharacter

Constants

ACTION_LIST

Attributes

class_properties[RW]
experience_points[RW]
hp[RW]
other_counters[RW]
resistances[RW]
spell_slots[RW]

Public Class Methods

load(session, path, override = {}) click to toggle source

Loads a pregen character from path @param session [Natural20::Session] The session to use @param path [String] path to character sheet YAML @apram override [Hash] override attributes @return [Natural20::PlayerCharacter] An instance of PlayerCharacter

# File lib/natural_20/player_character.rb, line 378
def self.load(session, path, override = {})
  Natural20::PlayerCharacter.new(session, YAML.load_file(path).deep_symbolize_keys!.merge(override))
end
new(session, properties) click to toggle source

@param session [Natural20::Session]

# File lib/natural_20/player_character.rb, line 18
def initialize(session, properties)
  @session = session
  @properties = properties.deep_symbolize_keys!
  @spell_slots = {}
  @ability_scores = @properties[:ability]
  @equipped = @properties[:equipped]
  @race_properties = YAML.load_file(File.join(session.root_path, 'races',
                                              "#{@properties[:race]}.yml")).deep_symbolize_keys!
  @inventory = {}
  @color = @properties[:color]
  @properties[:inventory]&.each do |inventory|
    @inventory[inventory[:type].to_sym] ||= OpenStruct.new({ type: inventory[:type], qty: 0 })
    @inventory[inventory[:type].to_sym].qty += inventory[:qty]
  end
  @statuses = Set.new
  @resistances = []
  entity_uid = SecureRandom.uuid

  @max_hit_die = {}
  @current_hit_die = {}

  @class_properties = @properties[:classes].map do |klass, level|
    send(:"#{klass}_level=", level)
    send(:"initialize_#{klass}")

    @max_hit_die[klass] = level

    character_class_properties =
      YAML.load_file(File.join(session.root_path, 'char_classes', "#{klass}.yml")).deep_symbolize_keys!
    hit_die_details = DieRoll.parse(character_class_properties[:hit_die])
    @current_hit_die[hit_die_details.die_type.to_i] = level

    [klass.to_sym, character_class_properties]
  end.to_h

  setup_attributes
end

Public Instance Methods

armor_class() click to toggle source
# File lib/natural_20/player_character.rb, line 68
def armor_class
  current_ac = if has_effect?(:ac_override)
                 eval_effect(:ac_override, armor_class: equipped_ac)
               else
                 equipped_ac
               end
  if has_effect?(:ac_bonus)
    current_ac + eval_effect(:ac_bonus)
  else
    current_ac
  end
end
available_actions(session, battle, opportunity_attack: false) click to toggle source
# File lib/natural_20/player_character.rb, line 259
def available_actions(session, battle, opportunity_attack: false)
  return [] if unconscious?

  if opportunity_attack
    if AttackAction.can?(self, battle, opportunity_attack: true)
      return player_character_attack_actions(battle, opportunity_attack: true)
    else
      return []
    end
  end

  ACTION_LIST.map do |type|
    next unless "#{type.to_s.camelize}Action".constantize.can?(self, battle)

    case type
    when :look
      LookAction.new(session, self, :look)
    when :attack
      player_character_attack_actions(battle)
    when :dodge
      DodgeAction.new(session, self, :dodge)
    when :help
      action = HelpAction.new(session, self, :help)
      action
    when :hide
      HideAction.new(session, self, :hide)
    when :hide_bonus
      action = HideBonusAction.new(session, self, :hide_bonus)
      action.as_bonus_action = true
      action
    when :disengage_bonus
      action = DisengageAction.new(session, self, :disengage_bonus)
      action.as_bonus_action = true
      action
    when :disengage
      DisengageAction.new(session, self, :disengage)
    when :drop_grapple
      DropGrappleAction.new(session, self, :drop_grapple)
    when :grapple
      GrappleAction.new(session, self, :grapple)
    when :escape_grapple
      EscapeGrappleAction.new(session, self, :escape_grapple)
    when :move
      MoveAction.new(session, self, type)
    when :prone
      ProneAction.new(session, self, type)
    when :stand
      StandAction.new(session, self, type)
    when :short_rest
      ShortRestAction.new(session, self, type)
    when :dash_bonus
      action = DashBonusAction.new(session, self, :dash_bonus)
      action.as_bonus_action = true
      action
    when :dash
      action = DashAction.new(session, self, type)
      action
    when :use_item
      UseItemAction.new(session, self, type)
    when :interact
      InteractAction.new(session, self, type)
    when :ground_interact
      GroundInteractAction.new(session, self, type)
    when :inventory
      InventoryAction.new(session, self, type)
    when :first_aid
      FirstAidAction.new(session, self, type)
    when :shove
      action = ShoveAction.new(session, self, type)
      action.knock_prone = true
      action
    when :spell
      SpellAction.new(session, self, type)
    when :two_weapon_attack
      two_weapon_attack_actions(battle)
    when :push
      ShoveAction.new(session, self, type)
    else
      Natural20::Action.new(session, self, type)
    end
  end.compact.flatten + c_class.keys.map { |c| send(:"special_actions_for_#{c}", session, battle) }.flatten
end
available_interactions(_entity, _battle) click to toggle source
# File lib/natural_20/player_character.rb, line 359
def available_interactions(_entity, _battle)
  []
end
available_spells(battle) click to toggle source

Returns the available spells for the current user @param battle [Natural20::Battle] @return [Hash]

# File lib/natural_20/player_character.rb, line 426
def available_spells(battle)
  prepared_spells.map do |spell|
    details = session.load_spell(spell)
    next unless details

    _qty, resource = details[:casting_time].split(':')

    disable_reason = []
    disable_reason << :no_action if resource == 'action' && battle && battle.ongoing? && total_actions(battle).zero?
    if resource == 'bonus_action' && battle.ongoing? && total_bonus_actions(battle).zero?
      disable_reason << :no_bonus_action
    end
    disable_reason << :no_spell_slot if details[:level].positive? && spell_slots(details[:level]).zero?

    [spell, details.merge(disabled: disable_reason)]
  end.compact.to_h
end
c_class() click to toggle source
# File lib/natural_20/player_character.rb, line 120
def c_class
  @properties[:classes]
end
class_feature?(feature) click to toggle source
# File lib/natural_20/player_character.rb, line 363
def class_feature?(feature)
  return true if @properties[:class_features]&.include?(feature)
  return true if @properties[:attributes]&.include?(feature)
  return true if @race_properties[:race_features]&.include?(feature)
  return true if subrace && @race_properties.dig(:subrace, subrace.to_sym, :class_features)&.include?(feature)
  return true if subrace && @race_properties.dig(:subrace, subrace.to_sym, :race_features)&.include?(feature)

  @class_properties.values.detect { |p| p[:class_features]&.include?(feature) }
end
consume_spell_slot!(level, character_class = nil, qty = 1) click to toggle source

Consumes a characters spell slot

# File lib/natural_20/player_character.rb, line 408
def consume_spell_slot!(level, character_class = nil, qty = 1)
  character_class = @spell_slots.keys.first if character_class.nil?
  if @spell_slots[character_class][level]
    @spell_slots[character_class][level] = [@spell_slots[character_class][level] - qty, 0].max
  end
end
darkvision?(distance) click to toggle source
Calls superclass method Natural20::Entity#darkvision?
# File lib/natural_20/player_character.rb, line 217
def darkvision?(distance)
  return true if super

  !!(@race_properties[:darkvision] && @race_properties[:darkvision] >= distance)
end
insight_proficiency() click to toggle source
# File lib/natural_20/player_character.rb, line 144
def insight_proficiency
  insight_proficient? ? proficiency_bonus : 0
end
investigation_proficiency() click to toggle source
# File lib/natural_20/player_character.rb, line 140
def investigation_proficiency
  investigation_proficient? ? proficiency_bonus : 0
end
languages() click to toggle source
Calls superclass method Natural20::Entity#languages
# File lib/natural_20/player_character.rb, line 109
def languages
  class_languages = []
  @class_properties.values.each do |prop|
    class_languages += prop[:languages] || []
  end

  racial_languages = @race_properties[:languages] || []

  (super + class_languages + racial_languages).sort
end
level() click to toggle source
# File lib/natural_20/player_character.rb, line 81
def level
  @properties[:level]
end
max_hp() click to toggle source
# File lib/natural_20/player_character.rb, line 60
def max_hp
  if class_feature?('dwarven_toughness')
    @properties[:max_hp] + level
  else
    @properties[:max_hp]
  end
end
max_spell_slots(level, character_class = nil) click to toggle source

Returns the number of spell slots @param level [Integer] @return [Integer]

# File lib/natural_20/player_character.rb, line 399
def max_spell_slots(level, character_class = nil)
  character_class = @spell_slots.keys.first if character_class.nil?

  return send(:"max_slots_for_#{character_class}", level) if respond_to?(:"max_slots_for_#{character_class}")

  0
end
melee_distance() click to toggle source
# File lib/natural_20/player_character.rb, line 207
def melee_distance
  (@properties[:equipped].map do |item|
    weapon_detail = session.load_weapon(item)
    next if weapon_detail.nil?
    next unless weapon_detail[:type] == 'melee_attack'

    weapon_detail[:range]
  end.compact + [5]).max
end
name() click to toggle source
# File lib/natural_20/player_character.rb, line 56
def name
  @properties[:name]
end
npc?() click to toggle source

returns if an npc or a player character @return [Boolean]

# File lib/natural_20/player_character.rb, line 384
def npc?
  false
end
passive_insight() click to toggle source
# File lib/natural_20/player_character.rb, line 132
def passive_insight
  10 + wis_mod + insight_proficiency
end
passive_investigation() click to toggle source
# File lib/natural_20/player_character.rb, line 128
def passive_investigation
  10 + int_mod + investigation_proficiency
end
passive_perception() click to toggle source
# File lib/natural_20/player_character.rb, line 124
def passive_perception
  10 + wis_mod + wisdom_proficiency
end
pc?() click to toggle source
# File lib/natural_20/player_character.rb, line 415
def pc?
  true
end
player_character_attack_actions(_battle, opportunity_attack: false) click to toggle source
# File lib/natural_20/player_character.rb, line 223
def player_character_attack_actions(_battle, opportunity_attack: false)
  # check all equipped and create attack for each
  valid_weapon_types = if opportunity_attack
                         %w[melee_attack]
                       else
                         %w[ranged_attack melee_attack]
                       end

  weapon_attacks = @properties[:equipped]&.map do |item|
    weapon_detail = session.load_weapon(item)
    next if weapon_detail.nil?
    next unless valid_weapon_types.include?(weapon_detail[:type])
    next if weapon_detail[:ammo] && !item_count(weapon_detail[:ammo]).positive?

    attacks = []

    action = AttackAction.new(session, self, :attack)
    action.using = item
    attacks << action

    if !opportunity_attack && weapon_detail[:properties] && weapon_detail[:properties].include?('thrown')
      action = AttackAction.new(session, self, :attack)
      action.using = item
      action.thrown = true
      attacks << action
    end

    attacks
  end&.flatten&.compact || []

  unarmed_attack = AttackAction.new(session, self, :attack)
  unarmed_attack.using = 'unarmed_attack'

  weapon_attacks + [unarmed_attack]
end
prepared_spells() click to toggle source
# File lib/natural_20/player_character.rb, line 419
def prepared_spells
  @properties.fetch(:cantrips, []) + @properties.fetch(:prepared_spells, [])
end
proficiency_bonus() click to toggle source
# File lib/natural_20/player_character.rb, line 148
def proficiency_bonus
  proficiency_bonus_table[level - 1]
end
proficient?(prof) click to toggle source
Calls superclass method Natural20::Entity#proficient?
# File lib/natural_20/player_character.rb, line 152
def proficient?(prof)
  return true if @class_properties.values.detect { |c| c[:proficiencies]&.include?(prof) }
  return true if @race_properties[:skills]&.include?(prof)
  return true if weapon_proficiencies.include?(prof)

  super
end
proficient_with_weapon?(weapon) click to toggle source
# File lib/natural_20/player_character.rb, line 160
def proficient_with_weapon?(weapon)
  weapon = @session.load_thing weapon if weapon.is_a?(String)

  all_weapon_proficiencies = weapon_proficiencies

  return true if all_weapon_proficiencies.include?(weapon[:name].to_s.underscore)

  all_weapon_proficiencies&.detect do |prof|
    weapon[:proficiency_type]&.include?(prof)
  end
end
race() click to toggle source
# File lib/natural_20/player_character.rb, line 101
def race
  @properties[:race]
end
short_rest!(battle, prompt: false) click to toggle source

@param hit_die_num [Integer] number of hit die to use

Calls superclass method Natural20::Entity#short_rest!
# File lib/natural_20/player_character.rb, line 445
def short_rest!(battle, prompt: false)
  super
  @class_properties.keys.each do |klass|
    send(:"short_rest_for_#{klass}", battle) if respond_to?(:"short_rest_for_#{klass}")
  end
end
size() click to toggle source
# File lib/natural_20/player_character.rb, line 85
def size
  @properties[:size] || @race_properties[:size]
end
speed() click to toggle source
# File lib/natural_20/player_character.rb, line 93
def speed
  if subrace
    return (@race_properties.dig(:subrace, subrace.to_sym,
                                 :base_speed) || @race_properties[:base_speed])
  end
  @race_properties[:base_speed]
end
subrace() click to toggle source
# File lib/natural_20/player_character.rb, line 105
def subrace
  @properties[:subrace]
end
to_h() click to toggle source
# File lib/natural_20/player_character.rb, line 186
def to_h
  {
    name: name,
    classes: c_class,
    hp: hp,
    ability: {
      str: @ability_scores.fetch(:str),
      dex: @ability_scores.fetch(:dex),
      con: @ability_scores.fetch(:con),
      int: @ability_scores.fetch(:int),
      wis: @ability_scores.fetch(:wis),
      cha: @ability_scores.fetch(:cha)
    },
    passive: {
      perception: passive_perception,
      investigation: passive_investigation,
      insight: passive_insight
    }
  }
end
token() click to toggle source
# File lib/natural_20/player_character.rb, line 89
def token
  @properties[:token]
end
two_weapon_attack_actions(battle) click to toggle source
# File lib/natural_20/player_character.rb, line 342
def two_weapon_attack_actions(battle)
  @properties[:equipped]&.each do |item|
    weapon_detail = session.load_weapon(item)
    next if weapon_detail.nil?
    next unless weapon_detail[:type] == 'melee_attack'

    next unless weapon_detail[:properties] && weapon_detail[:properties].include?('light') && TwoWeaponAttackAction.can?(
      self, battle, weapon: item
    )

    action = TwoWeaponAttackAction.new(session, self, :attack, weapon: item)
    action.using = item
    return action
  end
  nil
end
weapon_proficiencies() click to toggle source
# File lib/natural_20/player_character.rb, line 172
def weapon_proficiencies
  all_weapon_proficiencies = @class_properties.values.map do |p|
    p[:weapon_proficiencies]
  end.compact.flatten + @properties.fetch(:weapon_proficiencies, [])

  all_weapon_proficiencies += @race_properties.fetch(:weapon_proficiencies, [])
  if subrace
    all_weapon_proficiencies += (@race_properties.dig(:subrace, subrace.to_sym,
                                                      :weapon_proficiencies) || [])
  end

  all_weapon_proficiencies
end
wisdom_proficiency() click to toggle source
# File lib/natural_20/player_character.rb, line 136
def wisdom_proficiency
  perception_proficient? ? proficiency_bonus : 0
end

Private Instance Methods

equipped_ac() click to toggle source
# File lib/natural_20/player_character.rb, line 463
def equipped_ac
  @equipments ||= YAML.load_file(File.join(session.root_path, 'items', 'equipment.yml')).deep_symbolize_keys!

  equipped_meta = @equipped&.map { |e| @equipments[e.to_sym] }&.compact || []
  armor = equipped_meta.detect do |equipment|
    equipment[:type] == 'armor'
  end

  shield = equipped_meta.detect { |e| e[:type] == 'shield' }

  armor_ac = if armor.nil?
               10 + dex_mod
             else
               armor[:ac] + (if armor[:mod_cap]
                               [dex_mod,
                                armor[:mod_cap]].min
                             else
                               dex_mod
                             end) + (class_feature?('defense') ? 1 : 0)
             end

  armor_ac + (shield.nil? ? 0 : shield[:bonus_ac])
end
proficiency_bonus_table() click to toggle source
# File lib/natural_20/player_character.rb, line 454
def proficiency_bonus_table
  [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6]
end
setup_attributes() click to toggle source
Calls superclass method Multiattack#setup_attributes
# File lib/natural_20/player_character.rb, line 458
def setup_attributes
  super
  @hp = max_hp
end