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