Class: Natural20::PlayerCharacter

Inherits:
Object
  • Object
show all
Includes:
Multiattack, Entity, FighterClass, HealthFlavor, Lootable, RogueClass, WizardClass
Defined in:
lib/natural_20/player_character.rb

Constant Summary collapse

ACTION_LIST =
%i[spell first_aid look attack move dash hide help dodge disengage use_item interact ground_interact inventory disengage_bonus
dash_bonus hide_bonus grapple escape_grapple drop_grapple shove push prone stand short_rest two_weapon_attack].freeze

Constants included from WizardClass

WizardClass::WIZARD_SPELL_SLOT_TABLE

Constants included from Entity

Entity::ALL_SKILLS, Entity::ATTRIBUTE_TYPES, Entity::ATTRIBUTE_TYPES_ABBV, Entity::SKILL_AND_ABILITY_MAP

Instance Attribute Summary collapse

Attributes included from WizardClass

#arcane_recovery, #wizard_level, #wizard_spell_slots

Attributes included from FighterClass

#fighter_level, #second_wind_count

Attributes included from RogueClass

#rogue_level

Attributes included from Entity

#casted_effects, #color, #current_hit_die, #death_fails, #death_saves, #effects, #entity_event_hooks, #entity_uid, #max_hit_die, #session, #statuses

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Multiattack

#clear_multiattack!, #multi_attack_actions, #multiattack?, #reset_turn!

Methods included from HealthFlavor

#describe_health

Methods included from WizardClass

#initialize_wizard, #max_slots_for_wizard, #short_rest_for_wizard, #special_actions_for_wizard, #spell_attack_modifier

Methods included from FighterClass

#initialize_fighter, #second_wind!, #second_wind_die, #short_rest_for_fighter, #special_actions_for_fighter

Methods included from RogueClass

#initialize_rogue, #sneak_attack_level, #special_actions_for_rogue

Methods included from Entity

#ability_mod, #acrobatics_proficient?, #action?, #active_effects, #add_casted_effect, #add_item, #all_ability_mods, #all_ability_scores, #any_class_feature?, #athletics_proficient?, #attach_handler, #attack_ability_mod, #attack_roll_mod, #available_movement, #break_stealth!, #can_see?, #carry_capacity, #cha_mod, #check_equip, #con_mod, #conscious!, #conscious?, #dead!, #dead?, #death_saving_throw!, #deduct_item, #description, #dex_mod, #dexterity_check!, #disengage!, #disengage?, #dismiss_effect!, #dodge?, #dodging!, #drop_grapple!, #drop_items!, #entered_melee?, #equip, #equipped?, #equipped_items, #equipped_weapons, #escape_grapple_from!, #expertise, #expertise?, #free_object_interaction?, #grappled?, #grappled_by!, #grapples, #grappling, #grappling?, #grappling_targets, #hand_slots_required, #has_reaction?, #has_spell_effect?, #has_spells?, #heal!, #help!, #help?, #hiding!, #hiding?, #hit_die, #incapacitated?, #initiative!, #insight_proficient?, #int_mod, #inventory, #inventory_count, #inventory_weight, #investigation_proficient?, #item_count, #items_label, #label, #light_properties, #locate_melee_positions, #lockpick!, #long_jump_distance, #medicine_check!, #medicine_proficient?, #melee_squares, #object?, #opened?, #perception_proficient?, #proficient_with_armor?, #proficient_with_equipped_armor?, #prone!, #prone?, #push_from, #ranged_spell_attack!, #register_effect, #register_event_hook, #reset_turn!, #resistant_to?, #saving_throw!, #sentient?, #shield_equipped?, #size_identifier, #squeezed!, #squeezed?, #stable!, #stable?, #stand!, #standing_jump_distance, #stealth_proficient?, #str_mod, #strength_check!, #take_damage!, #to_item, #token_size, #total_actions, #total_bonus_actions, #total_reactions, #trigger_event, #unconscious!, #unconscious?, #unequip, #unequip_all, #ungrapple, #unsqueeze, #usable_items, #usable_objects, #use_hit_die!, #used_hand_slots, #wearing_armor?, #wis_mod, #wisdom_check!

Methods included from EntityStateEvaluator

#apply_effect, #eval_if

Methods included from Lootable

#build_map, #interactable?, #resolve, #use!

Methods included from Container

#retrieve, #store

Constructor Details

#initialize(session, properties) ⇒ PlayerCharacter

Returns a new instance of PlayerCharacter.

Parameters:



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 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

Instance Attribute Details

#class_propertiesObject

Returns the value of attribute class_properties.



12
13
14
# File 'lib/natural_20/player_character.rb', line 12

def class_properties
  @class_properties
end

#experience_pointsObject

Returns the value of attribute experience_points.



12
13
14
# File 'lib/natural_20/player_character.rb', line 12

def experience_points
  @experience_points
end

#hpObject

Returns the value of attribute hp.



12
13
14
# File 'lib/natural_20/player_character.rb', line 12

def hp
  @hp
end

#other_countersObject

Returns the value of attribute other_counters.



12
13
14
# File 'lib/natural_20/player_character.rb', line 12

def other_counters
  @other_counters
end

#resistancesObject

Returns the value of attribute resistances.



12
13
14
# File 'lib/natural_20/player_character.rb', line 12

def resistances
  @resistances
end

#spell_slots(level, character_class = nil) ⇒ Integer

Returns the number of spell slots

Parameters:

Returns:



391
392
393
# File 'lib/natural_20/player_character.rb', line 391

def spell_slots
  @spell_slots
end

Class Method Details

.load(session, path, override = {}) ⇒ Natural20::PlayerCharacter

Loads a pregen character from path

Parameters:

  • The session to use

  • path to character sheet YAML

Returns:

  • An instance of PlayerCharacter



378
379
380
# 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

Instance Method Details

#armor_classObject



68
69
70
71
72
73
74
75
76
77
78
79
# 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) ⇒ Object



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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# 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) ⇒ Object



359
360
361
# File 'lib/natural_20/player_character.rb', line 359

def available_interactions(_entity, _battle)
  []
end

#available_spells(battle) ⇒ Hash

Returns the available spells for the current user

Parameters:

Returns:



426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# 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_classObject



120
121
122
# File 'lib/natural_20/player_character.rb', line 120

def c_class
  @properties[:classes]
end

#class_feature?(feature) ⇒ Boolean

Returns:



363
364
365
366
367
368
369
370
371
# 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) ⇒ Object

Consumes a characters spell slot



408
409
410
411
412
413
# 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) ⇒ Boolean

Returns:



217
218
219
220
221
# 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_proficiencyObject



144
145
146
# File 'lib/natural_20/player_character.rb', line 144

def insight_proficiency
  insight_proficient? ? proficiency_bonus : 0
end

#investigation_proficiencyObject



140
141
142
# File 'lib/natural_20/player_character.rb', line 140

def investigation_proficiency
  investigation_proficient? ? proficiency_bonus : 0
end

#languagesObject



109
110
111
112
113
114
115
116
117
118
# 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

#levelObject



81
82
83
# File 'lib/natural_20/player_character.rb', line 81

def level
  @properties[:level]
end

#max_hpObject



60
61
62
63
64
65
66
# 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) ⇒ Integer

Returns the number of spell slots

Parameters:

Returns:



399
400
401
402
403
404
405
# 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_distanceObject



207
208
209
210
211
212
213
214
215
# 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

#nameObject



56
57
58
# File 'lib/natural_20/player_character.rb', line 56

def name
  @properties[:name]
end

#npc?Boolean

returns if an npc or a player character

Returns:



384
385
386
# File 'lib/natural_20/player_character.rb', line 384

def npc?
  false
end

#passive_insightObject



132
133
134
# File 'lib/natural_20/player_character.rb', line 132

def passive_insight
  10 + wis_mod + insight_proficiency
end

#passive_investigationObject



128
129
130
# File 'lib/natural_20/player_character.rb', line 128

def passive_investigation
  10 + int_mod + investigation_proficiency
end

#passive_perceptionObject



124
125
126
# File 'lib/natural_20/player_character.rb', line 124

def passive_perception
  10 + wis_mod + wisdom_proficiency
end

#pc?Boolean

Returns:



415
416
417
# File 'lib/natural_20/player_character.rb', line 415

def pc?
  true
end

#player_character_attack_actions(_battle, opportunity_attack: false) ⇒ Object



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
# 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_spellsObject



419
420
421
# File 'lib/natural_20/player_character.rb', line 419

def prepared_spells
  @properties.fetch(:cantrips, []) + @properties.fetch(:prepared_spells, [])
end

#proficiency_bonusObject



148
149
150
# File 'lib/natural_20/player_character.rb', line 148

def proficiency_bonus
  proficiency_bonus_table[level - 1]
end

#proficient?(prof) ⇒ Boolean

Returns:



152
153
154
155
156
157
158
# 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) ⇒ Boolean

Returns:



160
161
162
163
164
165
166
167
168
169
170
# 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

#raceObject



101
102
103
# File 'lib/natural_20/player_character.rb', line 101

def race
  @properties[:race]
end

#short_rest!(battle, prompt: false) ⇒ Object

Parameters:

  • number of hit die to use



445
446
447
448
449
450
# 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

#sizeObject



85
86
87
# File 'lib/natural_20/player_character.rb', line 85

def size
  @properties[:size] || @race_properties[:size]
end

#speedObject



93
94
95
96
97
98
99
# 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

#subraceObject



105
106
107
# File 'lib/natural_20/player_character.rb', line 105

def subrace
  @properties[:subrace]
end

#to_hObject



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# 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

#tokenObject



89
90
91
# File 'lib/natural_20/player_character.rb', line 89

def token
  @properties[:token]
end

#two_weapon_attack_actions(battle) ⇒ Object



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# 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_proficienciesObject



172
173
174
175
176
177
178
179
180
181
182
183
184
# 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_proficiencyObject



136
137
138
# File 'lib/natural_20/player_character.rb', line 136

def wisdom_proficiency
  perception_proficient? ? proficiency_bonus : 0
end