Module: Natural20::Entity

Includes:
EntityStateEvaluator
Included in:
ItemLibrary::Object, Npc, PlayerCharacter
Defined in:
lib/natural_20/concerns/entity.rb

Constant Summary collapse

ATTRIBUTE_TYPES =
%w[strength dexterity constitution intelligence wisdom charisma].freeze
ATTRIBUTE_TYPES_ABBV =
%w[str dex con int wis cha].freeze
ALL_SKILLS =
i[acrobatics animal_handling arcana athletics deception history insight intimidation
investigation medicine nature perception performance persuasion religion sleight_of_hand stealth survival]
SKILL_AND_ABILITY_MAP =
{
  dex: i[acrobatics sleight_of_hand stealth],
  wis: i[animal_handling insight medicine perception survival],
  int: i[arcana history investigation nature religion],
  con: [],
  str: [:athletics],
  cha: i[deception intimidation performance persuasion]
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from EntityStateEvaluator

#apply_effect, #eval_if

Instance Attribute Details

#casted_effectsObject (readonly)

Returns the value of attribute casted_effects.



8
9
10
# File 'lib/natural_20/concerns/entity.rb', line 8

def casted_effects
  @casted_effects
end

#colorObject

Returns the value of attribute color.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def color
  @color
end

#current_hit_dieObject

Returns the value of attribute current_hit_die.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def current_hit_die
  @current_hit_die
end

#death_failsObject

Returns the value of attribute death_fails.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def death_fails
  @death_fails
end

#death_savesObject

Returns the value of attribute death_saves.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def death_saves
  @death_saves
end

#effectsObject

Returns the value of attribute effects.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def effects
  @effects
end

#entity_event_hooksObject

Returns the value of attribute entity_event_hooks.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def entity_event_hooks
  @entity_event_hooks
end

#entity_uidObject

Returns the value of attribute entity_uid.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def entity_uid
  @entity_uid
end

#max_hit_dieObject

Returns the value of attribute max_hit_die.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def max_hit_die
  @max_hit_die
end

#sessionObject

Returns the value of attribute session.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def session
  @session
end

#statusesObject

Returns the value of attribute statuses.



6
7
8
# File 'lib/natural_20/concerns/entity.rb', line 6

def statuses
  @statuses
end

Instance Method Details

#ability_mod(type) ⇒ Object



527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'lib/natural_20/concerns/entity.rb', line 527

def ability_mod(type)
  mod_type = case type.to_sym
             when :wisdom, :wis
               :wis
             when :dexterity, :dex
               :dex
             when :constitution, :con
               :con
             when :intelligence, :int
               :int
             when :charisma, :cha
               :cha
             when :strength, :str
               :str
             end
  modifier_table(@ability_scores.fetch(mod_type))
end

#acrobatics_proficient?Boolean



1003
1004
1005
# File 'lib/natural_20/concerns/entity.rb', line 1003

def acrobatics_proficient?
  proficient?('acrobatics')
end

#action?(battle = nil) ⇒ Boolean

Checks if an entity still has an action available



461
462
463
464
465
# File 'lib/natural_20/concerns/entity.rb', line 461

def action?(battle = nil)
  return true if battle.nil?

  (battle.entity_state_for(self)[:action].presence || 0).positive?
end

#active_effectsObject



1175
1176
1177
1178
1179
# File 'lib/natural_20/concerns/entity.rb', line 1175

def active_effects
  @effects.values.flatten.reject do |effect|
    effect[:expiration] && effect[:expiration] <= @session.game_time
  end.uniq
end

#add_casted_effect(effect) ⇒ Object



1236
1237
1238
# File 'lib/natural_20/concerns/entity.rb', line 1236

def add_casted_effect(effect)
  @casted_effects << effect
end

#add_item(ammo_type, amount = 1, source_item = nil) ⇒ Object

Adds an item to your inventory



727
728
729
730
731
732
733
734
735
# File 'lib/natural_20/concerns/entity.rb', line 727

def add_item(ammo_type, amount = 1, source_item = nil)
  if @inventory[ammo_type.to_sym].nil?
    @inventory[ammo_type.to_sym] =
      OpenStruct.new(qty: 0, type: source_item&.type || ammo_type.to_sym)
  end

  qty = @inventory[ammo_type.to_sym].qty
  @inventory[ammo_type.to_sym].qty = qty + amount
end

#all_ability_modsObject



26
27
28
29
30
# File 'lib/natural_20/concerns/entity.rb', line 26

def all_ability_mods
  i[str dex con int wis cha].map do |att|
    modifier_table(@ability_scores.fetch(att))
  end
end

#all_ability_scoresObject



20
21
22
23
24
# File 'lib/natural_20/concerns/entity.rb', line 20

def all_ability_scores
  i[str dex con int wis cha].map do |att|
    @ability_scores[att]
  end
end

#any_class_feature?(features) ⇒ Boolean

checks if at least one class feature matches



1034
1035
1036
# File 'lib/natural_20/concerns/entity.rb', line 1034

def any_class_feature?(features)
  !features.select { |f| class_feature?(f) }.empty?
end

#athletics_proficient?Boolean



1007
1008
1009
# File 'lib/natural_20/concerns/entity.rb', line 1007

def athletics_proficient?
  proficient?('athletics')
end

#attach_handler(event_name, object, callback) ⇒ Object



665
666
667
668
# File 'lib/natural_20/concerns/entity.rb', line 665

def attach_handler(event_name, object, callback)
  @event_handlers ||= {}
  @event_handlers[event_name.to_sym] = [object, callback]
end

#attack_ability_mod(weapon) ⇒ Object



1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
# File 'lib/natural_20/concerns/entity.rb', line 1068

def attack_ability_mod(weapon)
  modifier = 0

  modifier += case (weapon[:type])
              when 'melee_attack'
                weapon[:properties]&.include?('finesse') ? [str_mod, dex_mod].max : str_mod
              when 'ranged_attack'
                if class_feature?('archery')
                  dex_mod + 2
                else
                  dex_mod
                end
              end

  modifier
end

#attack_roll_mod(weapon) ⇒ Object



1060
1061
1062
1063
1064
1065
1066
# File 'lib/natural_20/concerns/entity.rb', line 1060

def attack_roll_mod(weapon)
  modifier = attack_ability_mod(weapon)

  modifier += proficiency_bonus if proficient_with_weapon?(weapon)

  modifier
end

#available_movement(battle) ⇒ Integer



487
488
489
# File 'lib/natural_20/concerns/entity.rb', line 487

def available_movement(battle)
  grappled? ? 0 : battle.entity_state_for(self)[:movement]
end

#available_spellsObject



491
492
493
# File 'lib/natural_20/concerns/entity.rb', line 491

def available_spells
  []
end

#break_stealth!(battle) ⇒ Object



392
393
394
395
396
397
398
# File 'lib/natural_20/concerns/entity.rb', line 392

def break_stealth!(battle)
  entity_state = battle.entity_state_for(self)
  return unless entity_state

  entity_state[:statuses].delete(:hiding)
  entity_state[:stealth] = 0
end

#can_see?(cur_pos_x, cur_pos_y, _target_entity, pos_x, pos_y, battle) ⇒ Boolean

check if current entity can see target at a certain location



443
444
445
446
447
448
# File 'lib/natural_20/concerns/entity.rb', line 443

def can_see?(cur_pos_x, cur_pos_y, _target_entity, pos_x, pos_y, battle)
  battle.map.line_of_sight?(cur_pos_x, cur_pos_y, pos_x, pos_y)

  # TODO, check invisiblity etc, range
  true
end

#carry_capacityFloat

returns the carrying capacity of an entity in lbs



979
980
981
# File 'lib/natural_20/concerns/entity.rb', line 979

def carry_capacity
  @ability_scores.fetch(:str, 1) * 15.0
end

#cha_modObject



515
516
517
# File 'lib/natural_20/concerns/entity.rb', line 515

def cha_mod
  modifier_table(@ability_scores.fetch(:cha))
end

#check_equip(item_name) ⇒ Symbol

Checks if item can be equipped



838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
# File 'lib/natural_20/concerns/entity.rb', line 838

def check_equip(item_name)
  item_name = item_name.to_sym
  weapon = @session.load_thing(item_name)
  return :unequippable unless weapon && weapon[:subtype] == 'weapon' || %w[shield armor].include?(weapon[:type])

  hand_slots = used_hand_slots + hand_slots_required(to_item(item_name, weapon))

  armor_slots = equipped_items.select do |item|
    item.type == 'armor'
  end.size

  return :hands_full if hand_slots > 2.0
  return :armor_equipped if armor_slots >= 1 && weapon[:type] == 'armor'

  :ok
end

#class_feature?(feature) ⇒ Boolean



1027
1028
1029
# File 'lib/natural_20/concerns/entity.rb', line 1027

def class_feature?(feature)
  @properties[:attributes]&.include?(feature)
end

#con_modObject



507
508
509
# File 'lib/natural_20/concerns/entity.rb', line 507

def con_mod
  modifier_table(@ability_scores.fetch(:con))
end

#conscious!Object



150
151
152
153
# File 'lib/natural_20/concerns/entity.rb', line 150

def conscious!
  @statuses.delete(:unconscious)
  @statuses.delete(:stable)
end

#conscious?Boolean



126
127
128
# File 'lib/natural_20/concerns/entity.rb', line 126

def conscious?
  !dead? && !unconscious?
end

#darkvision?(distance) ⇒ Boolean



274
275
276
277
278
279
# File 'lib/natural_20/concerns/entity.rb', line 274

def darkvision?(distance)
  return false unless @properties[:darkvision]
  return false if @properties[:darkvision] < distance

  true
end

#dead!Object



94
95
96
97
98
99
100
101
102
# File 'lib/natural_20/concerns/entity.rb', line 94

def dead!
  unless dead?
    Natural20::EventManager.received_event({ source: self, event: :died })
    drop_grapple!
    @statuses.add(:dead)
    @statuses.delete(:stable)
    @statuses.delete(:unconscious)
  end
end

#dead?Boolean



118
119
120
# File 'lib/natural_20/concerns/entity.rb', line 118

def dead?
  @statuses.include?(:dead)
end

#death_saving_throw!(battle = nil) ⇒ Object

Perform a death saving throw



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
# File 'lib/natural_20/concerns/entity.rb', line 302

def death_saving_throw!(battle = nil)
  roll = Natural20::DieRoll.roll('1d20', description: t('dice_roll.death_saving_throw'), entity: self,
                                         battle: battle)
  if roll.nat_20?
    conscious!
    heal!(1)

    Natural20::EventManager.received_event({ source: self, event: :death_save, roll: roll, saves: @death_saves,
                                             fails: death_fails, complete: true, stable: true, success: true })
  elsif roll.result >= 10
    @death_saves += 1
    complete = false

    if @death_saves >= 3
      complete = true
      @death_saves = 0
      @death_fails = 0
      stable!
    end
    Natural20::EventManager.received_event({ source: self, event: :death_save, roll: roll, saves: @death_saves,
                                             fails: @death_fails, complete: complete, stable: complete })
  else
    @death_fails += roll.nat_1? ? 2 : 1
    complete = false
    if @death_fails >= 3
      complete = true
      dead!
      @death_saves = 0
      @death_fails = 0
    end

    Natural20::EventManager.received_event({ source: self, event: :death_fail, roll: roll, saves:  @death_saves,
                                             fails: @death_fails, complete: complete })
  end
end

#deduct_item(ammo_type, amount = 1) ⇒ OpenStruct

Removes Item from inventory



714
715
716
717
718
719
720
721
# File 'lib/natural_20/concerns/entity.rb', line 714

def deduct_item(ammo_type, amount = 1)
  return if @inventory[ammo_type.to_sym].nil?

  qty = @inventory[ammo_type.to_sym].qty
  @inventory[ammo_type.to_sym].qty = [qty - amount, 0].max

  @inventory[ammo_type.to_sym]
end

#descriptionObject



289
290
291
# File 'lib/natural_20/concerns/entity.rb', line 289

def description
  @properties[:description].presence || ''
end

#dex_modObject



523
524
525
# File 'lib/natural_20/concerns/entity.rb', line 523

def dex_mod
  modifier_table(@ability_scores.fetch(:dex))
end

#dexterity_check!(bonus = 0, battle: nil, description: nil) ⇒ Object



643
644
645
646
647
# File 'lib/natural_20/concerns/entity.rb', line 643

def dexterity_check!(bonus = 0, battle: nil, description: nil)
  disadvantage = !proficient_with_equipped_armor? ? true : false
  DieRoll.roll_with_lucky(self, "1d20+#{dex_mod + bonus}", disadvantage: disadvantage, description: description || t('dice_roll.dexterity'),
                                                           battle: battle)
end

#disengage!(battle) ⇒ Object



400
401
402
403
# File 'lib/natural_20/concerns/entity.rb', line 400

def disengage!(battle)
  entity_state = battle.entity_state_for(self)
  entity_state[:statuses].add(:disengage)
end

#disengage?(battle) ⇒ Boolean



405
406
407
408
# File 'lib/natural_20/concerns/entity.rb', line 405

def disengage?(battle)
  entity_state = battle.entity_state_for(self)
  entity_state[:statuses]&.include?(:disengage)
end

#dismiss_effect!(effect) ⇒ Object



1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
# File 'lib/natural_20/concerns/entity.rb', line 1213

def dismiss_effect!(effect)
  dismiss_count = 0
  effect.source.casted_effects.reject! { |f| f[:effect] == effect }

  @effects = @effects.map do |k, value|
    delete_effects = value.select do |f|
      f[:effect] == effect
    end
    dismiss_count += delete_effects.size
    [k, value - delete_effects]
  end.to_h

  @entity_event_hooks = @entity_event_hooks.map do |k, value|
    delete_hooks = value.select do |f|
      f[:effect] == effect
    end
    dismiss_count += delete_hooks.size
    [k, value - delete_hooks]
  end.to_h

  dismiss_count
end

#dodge?(battle) ⇒ Boolean



410
411
412
413
414
415
# File 'lib/natural_20/concerns/entity.rb', line 410

def dodge?(battle)
  entity_state = battle.entity_state_for(self)
  return false unless entity_state

  entity_state[:statuses]&.include?(:dodge)
end

#dodging!(battle) ⇒ Object



379
380
381
382
# File 'lib/natural_20/concerns/entity.rb', line 379

def dodging!(battle)
  entity_state = battle.entity_state_for(self)
  entity_state[:statuses].add(:dodge)
end

#drop_grapple!Object



703
704
705
706
707
708
# File 'lib/natural_20/concerns/entity.rb', line 703

def drop_grapple!
  @grappling ||= []
  @grappling.each do |target|
    ungrapple(target)
  end
end

#drop_items!(battle, item_and_counts = []) ⇒ Object



762
763
764
765
# File 'lib/natural_20/concerns/entity.rb', line 762

def drop_items!(battle, item_and_counts = [])
  ground = battle.map.ground_at(*battle.map.entity_or_object_pos(self))
  ground&.store(battle, self, ground, item_and_counts)
end

#entered_melee?(map, entity, pos_x, pos_y) ⇒ Boolean

convenience method used to determine if a creature entered or is at melee range of another



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/natural_20/concerns/entity.rb', line 163

def entered_melee?(map, entity, pos_x, pos_y)
  entity_1_sq = map.entity_squares(self)
  entity_2_sq = map.entity_squares_at_pos(entity, pos_x, pos_y)

  entity_1_sq.each do |entity_1_pos|
    entity_2_sq.each do |entity_2_pos|
      cur_x, cur_y = entity_1_pos
      pos_x, pos_y = entity_2_pos

      distance = Math.sqrt((cur_x - pos_x)**2 + (cur_y - pos_y)**2).floor * map.feet_per_grid # one square - 5 ft

      # determine melee options
      return true if distance <= melee_distance
    end
  end

  false
end

#equip(item_name, ignore_inventory: false) ⇒ Object

Equips an item



820
821
822
823
824
825
826
827
828
829
830
831
832
833
# File 'lib/natural_20/concerns/entity.rb', line 820

def equip(item_name, ignore_inventory: false)
  @properties[:equipped] ||= []
  if ignore_inventory
    @properties[:equipped] << item_name.to_s
    resolve_trigger(:equip)
    return
  end

  item = deduct_item(item_name)
  if item
    @properties[:equipped] << item_name.to_s
    resolve_trigger(:equip)
  end
end

#equipped?(item_name) ⇒ Boolean

Checks if an item is equipped



814
815
816
# File 'lib/natural_20/concerns/entity.rb', line 814

def equipped?(item_name)
  equipped_items.map(&:name).include?(item_name.to_sym)
end

#equipped_itemsArray<OpenStruct>

returns equipped items



942
943
944
945
946
947
948
949
950
# File 'lib/natural_20/concerns/entity.rb', line 942

def equipped_items
  equipped_arr = @properties[:equipped] || []
  equipped_arr.map do |k|
    item = @session.load_thing(k)
    raise "unknown item #{k}" unless item

    to_item(k, item)
  end
end

#equipped_weaponsObject



901
902
903
904
905
# File 'lib/natural_20/concerns/entity.rb', line 901

def equipped_weapons
  equipped_items.select do |item|
    item.subtype == 'weapon'
  end.map(&:name)
end

#escape_grapple_from!(grappler) ⇒ Object



368
369
370
371
372
373
# File 'lib/natural_20/concerns/entity.rb', line 368

def escape_grapple_from!(grappler)
  @grapples ||= []
  @grapples.delete(grappler)
  @statuses.delete(:grappled) if @grapples.empty?
  grappler.ungrapple(self)
end

#expertiseObject



557
558
559
# File 'lib/natural_20/concerns/entity.rb', line 557

def expertise
  @properties.fetch(:expertise, [])
end

#expertise?(prof) ⇒ Boolean



32
33
34
# File 'lib/natural_20/concerns/entity.rb', line 32

def expertise?(prof)
  @properties[:expertise]&.include?(prof.to_s)
end

#free_object_interaction?(battle) ⇒ Boolean



475
476
477
478
479
# File 'lib/natural_20/concerns/entity.rb', line 475

def free_object_interaction?(battle)
  return true unless battle

  (battle.entity_state_for(self)[:free_object_interaction].presence || 0).positive?
end

#grappled?Boolean



922
923
924
# File 'lib/natural_20/concerns/entity.rb', line 922

def grappled?
  @statuses.include?(:grappled)
end

#grappled_by!(grappler) ⇒ Object



361
362
363
364
365
366
# File 'lib/natural_20/concerns/entity.rb', line 361

def grappled_by!(grappler)
  @statuses.add(:grappled)
  @grapples ||= []
  @grapples << grappler
  grappler.grappling(self)
end

#grapplesObject



375
376
377
# File 'lib/natural_20/concerns/entity.rb', line 375

def grapples
  @grapples || []
end

#grappling(target) ⇒ Object



679
680
681
682
# File 'lib/natural_20/concerns/entity.rb', line 679

def grappling(target)
  @grappling ||= []
  @grappling << target
end

#grappling?Boolean



684
685
686
687
688
# File 'lib/natural_20/concerns/entity.rb', line 684

def grappling?
  @grappling ||= []

  !@grappling.empty?
end

#grappling_targetsObject



690
691
692
693
# File 'lib/natural_20/concerns/entity.rb', line 690

def grappling_targets
  @grappling ||= []
  @grappling
end

#hand_slots_required(item) ⇒ Object



881
882
883
884
885
886
887
888
889
890
891
# File 'lib/natural_20/concerns/entity.rb', line 881

def hand_slots_required(item)
  return 0.0 if item.type == 'armor'

  if item.light
    0.5
  elsif item.two_handed
    2.0
  else
    1.0
  end
end

#has_reaction?(battle) ⇒ Boolean



499
500
501
# File 'lib/natural_20/concerns/entity.rb', line 499

def has_reaction?(battle)
  (battle.entity_state_for(self)[:reaction].presence || 0).positive?
end

#has_spell_effect?(spell) ⇒ Boolean



1240
1241
1242
1243
1244
1245
1246
1247
# File 'lib/natural_20/concerns/entity.rb', line 1240

def has_spell_effect?(spell)
  active_effects = @effects.values.flatten.reject do |effect|
    effect[:expiration] && effect[:expiration] <= @session.game_time
  end
  !!active_effects.detect { |effect|
    effect[:effect].id.to_sym == spell.to_sym
  }
end

#has_spells?Boolean



417
418
419
420
421
# File 'lib/natural_20/concerns/entity.rb', line 417

def has_spells?
  return false unless @properties[:prepared_spells]

  !@properties[:prepared_spells].empty?
end

#heal!(amt) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
# File 'lib/natural_20/concerns/entity.rb', line 36

def heal!(amt)
  return if dead?

  prev_hp = @hp
  @death_saves = 0
  @death_fails = 0
  @hp = [max_hp, @hp + amt].min

  conscious!
  Natural20::EventManager.received_event({ source: self, event: :heal, previous: prev_hp, new: @hp, value: amt })
end

#help!(battle, target) ⇒ Object



450
451
452
453
454
455
456
457
# File 'lib/natural_20/concerns/entity.rb', line 450

def help!(battle, target)
  target_state = battle.entity_state_for(target)
  target_state[:target_effect][self] = if battle.opposing?(self, target)
                                         :help
                                       else
                                         :help_ability_check
                                       end
end

#help?(battle, target) ⇒ Boolean



435
436
437
438
439
440
# File 'lib/natural_20/concerns/entity.rb', line 435

def help?(battle, target)
  entity_state = battle.entity_state_for(target)
  return entity_state[:target_effect][self] == :help if entity_state[:target_effect]&.key?(self)

  false
end

#hiding!(battle, stealth) ⇒ Object



386
387
388
389
390
# File 'lib/natural_20/concerns/entity.rb', line 386

def hiding!(battle, stealth)
  entity_state = battle.entity_state_for(self)
  entity_state[:statuses].add(:hiding)
  entity_state[:stealth] = stealth
end

#hiding?(battle) ⇒ Boolean



428
429
430
431
432
433
# File 'lib/natural_20/concerns/entity.rb', line 428

def hiding?(battle)
  entity_state = battle.entity_state_for(self)
  return false unless entity_state

  entity_state[:statuses]&.include?(:hiding)
end

#hit_dieHash<Integer,Integer>

Returns the character hit die



1097
1098
1099
# File 'lib/natural_20/concerns/entity.rb', line 1097

def hit_die
  @current_hit_die
end

#incapacitated?Boolean



918
919
920
# File 'lib/natural_20/concerns/entity.rb', line 918

def incapacitated?
  @statuses.include?(:unconscious) || @statuses.include?(:sleep) || @statuses.include?(:dead)
end

#initiative!(battle = nil) ⇒ Object



293
294
295
296
297
298
299
# File 'lib/natural_20/concerns/entity.rb', line 293

def initiative!(battle = nil)
  roll = Natural20::DieRoll.roll("1d20+#{dex_mod}", description: t('dice_roll.initiative'), entity: self,
                                                    battle: battle)
  value = roll.result.to_f + @ability_scores.fetch(:dex) / 100.to_f
  Natural20::EventManager.received_event({ source: self, event: :initiative, roll: roll, value: value })
  value
end

#insight_proficient?Boolean



995
996
997
# File 'lib/natural_20/concerns/entity.rb', line 995

def insight_proficient?
  proficient?('insight')
end

#int_modObject



519
520
521
# File 'lib/natural_20/concerns/entity.rb', line 519

def int_mod
  modifier_table(@ability_scores.fetch(:int))
end

#inventoryArray

Returns items in the “backpack” of the entity



777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
# File 'lib/natural_20/concerns/entity.rb', line 777

def inventory
  @inventory.map do |k, v|
    item = @session.load_thing k
    raise "unable to load unknown item #{k}" if item.nil?
    next unless v[:qty].positive?

    OpenStruct.new(
      name: k.to_sym,
      label: v[:label].presence || k.to_s.humanize,
      qty: v[:qty],
      equipped: false,
      weight: item[:weight]
    )
  end.compact
end

#inventory_countObject



793
794
795
796
797
# File 'lib/natural_20/concerns/entity.rb', line 793

def inventory_count
  @inventory.values.inject(0) do |total, item|
    total + item[:qty]
  end
end

#inventory_weightFloat

returns in lbs the weight of all items in the inventory



934
935
936
937
938
# File 'lib/natural_20/concerns/entity.rb', line 934

def inventory_weight
  (inventory + equipped_items).inject(0.0) do |sum, item|
    sum + (item.weight.presence || '0').to_f * item.qty
  end
end

#investigation_proficient?Boolean



991
992
993
# File 'lib/natural_20/concerns/entity.rb', line 991

def investigation_proficient?
  proficient?('investigation')
end

#item_count(inventory_type) ⇒ Integer

Retrieves the item count of an item in the entities inventory



740
741
742
743
744
# File 'lib/natural_20/concerns/entity.rb', line 740

def item_count(inventory_type)
  return 0 if @inventory[inventory_type.to_sym].nil?

  @inventory[inventory_type.to_sym][:qty]
end

#items_labelObject



983
984
985
# File 'lib/natural_20/concerns/entity.rb', line 983

def items_label
  I18n.t(:"entity.#{self.class}.item_label", default: "#{name} Items")
end

#labelObject



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

def label
  I18n.exists?(name, :en) ? I18n.t(name) : name.humanize
end

#languagesObject



270
271
272
# File 'lib/natural_20/concerns/entity.rb', line 270

def languages
  @properties[:languages] || []
end

#light_propertiesObject



1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
# File 'lib/natural_20/concerns/entity.rb', line 1038

def light_properties
  return nil if equipped_items.blank?

  bright = [0]
  dim = [0]

  equipped_items.map do |item|
    next unless item.light_properties

    bright << item.light_properties.fetch(:bright, 0)
    dim << item.light_properties.fetch(:dim, 0)
  end

  bright = bright.max
  dim = dim.max

  return nil unless [dim, bright].sum.positive?

  { dim: dim,
    bright: bright }
end

#locate_melee_positions(map, target_position, battle = nil) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/natural_20/concerns/entity.rb', line 242

def locate_melee_positions(map, target_position, battle = nil)
  result = []
  step = melee_distance / map.feet_per_grid
  cur_x, cur_y = target_position
  (-step..step).each do |x_off|
    (-step..step).each do |y_off|
      next if x_off.zero? && y_off.zero?

      # adjust melee position based on token size
      adjusted_x_off = x_off
      adjusted_y_off = y_off

      adjusted_x_off -= token_size - 1 if x_off < 0
      adjusted_y_off -= token_size - 1 if y_off < 0

      position = [cur_x + adjusted_x_off, cur_y + adjusted_y_off]

      if position[0].negative? || position[0] >= map.size[0] || position[1].negative? || position[1] >= map.size[1]
        next
      end
      next unless map.placeable?(self, *position, battle)

      result << position
    end
  end
  result
end

#lockpick!(battle = nil) ⇒ Object



1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
# File 'lib/natural_20/concerns/entity.rb', line 1015

def lockpick!(battle = nil)
  proficiency_mod = dex_mod
  bonus = if proficient?(:thieves_tools)
            expertise?(:thieves_tools) ? proficiency_bonus * 2 : proficiency_bonus
          else
            0
          end
  proficiency_mod += bonus
  Natural20::DieRoll.roll("1d20+#{proficiency_mod}", description: t('dice_roll.thieves_tools'), battle: battle,
                                                     entity: self)
end

#long_jump_distanceObject



553
554
555
# File 'lib/natural_20/concerns/entity.rb', line 553

def long_jump_distance
  @ability_scores.fetch(:str)
end

#max_spell_slots(_level) ⇒ Object



1209
1210
1211
# File 'lib/natural_20/concerns/entity.rb', line 1209

def max_spell_slots(_level)
  0
end

#medicine_check!(battle = nil, description: nil) ⇒ Object



660
661
662
663
# File 'lib/natural_20/concerns/entity.rb', line 660

def medicine_check!(battle = nil, description: nil)
  wisdom_check!(medicine_proficient? ? proficiency_bonus : 0, battle: battle,
                                                              description: description || t('dice_roll.medicine'))
end

#medicine_proficient?Boolean



1011
1012
1013
# File 'lib/natural_20/concerns/entity.rb', line 1011

def medicine_proficient?
  proficient?('medicine')
end

#melee_distanceObject



182
183
184
# File 'lib/natural_20/concerns/entity.rb', line 182

def melee_distance
  0
end

#melee_squares(map, target_position: nil, adjacent_only: false) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/natural_20/concerns/entity.rb', line 215

def melee_squares(map, target_position: nil, adjacent_only: false)
  result = []
  step = adjacent_only ? 1 : melee_distance / map.feet_per_grid
  cur_x, cur_y = target_position || map.entity_or_object_pos(self)
  (-step..step).each do |x_off|
    (-step..step).each do |y_off|
      next if x_off.zero? && y_off.zero?

      # adjust melee position based on token size
      adjusted_x_off = x_off
      adjusted_y_off = y_off

      adjusted_x_off -= token_size - 1 if x_off.negative?
      adjusted_y_off -= token_size - 1 if y_off.negative?

      position = [cur_x + adjusted_x_off, cur_y + adjusted_y_off]

      if position[0].negative? || position[0] >= map.size[0] || position[1].negative? || position[1] >= map.size[1]
        next
      end

      result << position
    end
  end
  result
end

#npc?Boolean



138
139
140
# File 'lib/natural_20/concerns/entity.rb', line 138

def npc?
  false
end

#object?Boolean



134
135
136
# File 'lib/natural_20/concerns/entity.rb', line 134

def object?
  false
end

#opened?Boolean



914
915
916
# File 'lib/natural_20/concerns/entity.rb', line 914

def opened?
  false
end

#passive_perceptionObject



545
546
547
# File 'lib/natural_20/concerns/entity.rb', line 545

def passive_perception
  @properties[:passive_perception] || 10 + wis_mod
end

#pc?Boolean



142
143
144
# File 'lib/natural_20/concerns/entity.rb', line 142

def pc?
  false
end

#perception_proficient?Boolean



987
988
989
# File 'lib/natural_20/concerns/entity.rb', line 987

def perception_proficient?
  proficient?('perception')
end

#proficiency_bonusInteger

Returns tghe proficiency bonus of this entity



928
929
930
# File 'lib/natural_20/concerns/entity.rb', line 928

def proficiency_bonus
  @properties[:proficiency_bonus].presence || 2
end

#proficient?(prof) ⇒ Boolean



907
908
909
910
911
912
# File 'lib/natural_20/concerns/entity.rb', line 907

def proficient?(prof)
  @properties[:skills]&.include?(prof.to_s) ||
    @properties[:tools]&.include?(prof.to_s) ||

    @properties[:saving_throw_proficiencies]&.map { |s| "#{s}_save" }&.include?(prof.to_s)
end

#proficient_with_armor?(item) ⇒ Boolean



855
856
857
858
859
860
861
862
863
864
# File 'lib/natural_20/concerns/entity.rb', line 855

def proficient_with_armor?(item)
  armor = @session.load_thing(item)
  raise "unknown item #{item}" unless armor
  raise "not armor #{item}" unless %w[armor shield].include?(armor[:type])

  return proficient?("#{armor[:subtype]}_armor") if armor[:type] == 'armor'
  return proficient?('shields') if armor[:type] == 'shield'

  false
end

#proficient_with_equipped_armor?Boolean



866
867
868
869
870
871
872
873
874
875
# File 'lib/natural_20/concerns/entity.rb', line 866

def proficient_with_equipped_armor?
  shields_and_armor = equipped_items.select { |t| %w[armor shield].include?(t[:type]) }
  return true if shields_and_armor.empty?

  shields_and_armor.each do |item|
    return false unless proficient_with_armor?(item.name)
  end

  true
end

#proficient_with_weapon?(weapon) ⇒ Boolean



1085
1086
1087
1088
1089
1090
1091
1092
1093
# File 'lib/natural_20/concerns/entity.rb', line 1085

def proficient_with_weapon?(weapon)
  weapon = @session.load_thing weapon if weapon.is_a?(String)

  return true if weapon[:name] == 'Unarmed Attack'

  @properties[:weapon_proficiencies]&.detect do |prof|
    weapon[:proficiency_type]&.include?(prof) || weapon[:proficiency_type]&.include?(weapon[:name].underscore)
  end
end

#prone!Object



104
105
106
107
# File 'lib/natural_20/concerns/entity.rb', line 104

def prone!
  Natural20::EventManager.received_event({ source: self, event: :prone })
  @statuses.add(:prone)
end

#prone?Boolean



114
115
116
# File 'lib/natural_20/concerns/entity.rb', line 114

def prone?
  @statuses.include?(:prone)
end

#push_from(map, pos_x, pos_y, distance = 5) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/natural_20/concerns/entity.rb', line 187

def push_from(map, pos_x, pos_y, distance = 5)
  x, y = map.entity_or_object_pos(self)
  effective_token_size = token_size - 1
  ofs_x, ofs_y = if pos_x.between?(x, x + effective_token_size) && !pos_y.between?(y, y + effective_token_size)
                   [0, y - pos_y > 0 ? distance : -distance]
                 elsif pos_y.between?(y, y + effective_token_size) && !pos_x.between?(x, x + effective_token_size)
                   [x - pos_x > 0 ? distance : -distance, 0]
                 elsif [pos_x, pos_y] == [x - 1, y - 1]
                   [distance, distance]
                 elsif [pos_x, pos_y] == [x + effective_token_size + 1, y - 1]
                   [-distance, distance]
                 elsif [pos_x, pos_y] == [x - 1, y + effective_token_size + 1]
                   [distance, -distance]
                 elsif [pos_x, pos_y] == [x + effective_token_size + 1, y + effective_token_size + 1]
                   [-disance, -distance]
                 else
                   raise "invalid source position #{pos_x}, #{pos_y}"
                 end
  # convert to squares
  ofs_x /= map.feet_per_grid
  ofs_y /= map.feet_per_grid

  [x + ofs_x, y + ofs_y] if map.placeable?(self, x + ofs_x, y + ofs_y)
end

#raceObject



16
17
18
# File 'lib/natural_20/concerns/entity.rb', line 16

def race
  @properties[:race]
end

#ranged_spell_attack!(battle, spell, advantage: false, disadvantage: false) ⇒ Object



423
424
425
426
# File 'lib/natural_20/concerns/entity.rb', line 423

def ranged_spell_attack!(battle, spell, advantage: false, disadvantage: false)
  DieRoll.roll("1d20+#{spell_attack_modifier}", description: t('dice_roll.ranged_spell_attack', spell: spell),
                                                entity: self, battle: battle, advantage: advantage, disadvantage: disadvantage)
end

#register_effect(effect_type, handler, method_name = nil, effect: nil, source: nil, duration: nil) ⇒ Object



1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
# File 'lib/natural_20/concerns/entity.rb', line 1181

def register_effect(effect_type, handler, method_name = nil, effect: nil, source: nil, duration: nil)
  @effects[effect_type.to_sym] ||= []
  effect_descriptor = {
    handler: handler,
    method: method_name.nil? ? effect_type : method_name,
    effect: effect,
    source: source
  }
  effect_descriptor[:expiration] = @session.game_time + duration.to_i
  @effects[effect_type.to_sym] << effect_descriptor
end

#register_event_hook(event_type, handler, method_name = nil, source: nil, effect: nil, duration: nil) ⇒ Object



1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
# File 'lib/natural_20/concerns/entity.rb', line 1193

def register_event_hook(event_type, handler, method_name = nil, source: nil, effect: nil, duration: nil)
  @entity_event_hooks[event_type.to_sym] ||= []
  event_hook_descriptor = {
    handler: handler,
    method: method_name.nil? ? event_type : method_name,
    effect: effect,
    source: source
  }
  event_hook_descriptor[:expiration] = @session.game_time + duration.to_i if duration
  @entity_event_hooks[event_type.to_sym] << event_hook_descriptor
end

#reset_turn!(battle) ⇒ Hash



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/natural_20/concerns/entity.rb', line 340

def reset_turn!(battle)
  entity_state = battle.entity_state_for(self)
  entity_state.merge!({
                        action: 1,
                        bonus_action: 1,
                        reaction: 1,
                        movement: speed,
                        free_object_interaction: 1,
                        active_perception: 0,
                        active_perception_disadvantage: 0,
                        two_weapon: nil
                      })
  entity_state[:statuses].delete(:dodge)
  entity_state[:statuses].delete(:disengage)
  battle.dismiss_help_actions_for(self)
  resolve_trigger(:start_of_turn)
  cleanup_effects
  entity_state
end

#resistant_to?(damage_type) ⇒ Boolean



90
91
92
# File 'lib/natural_20/concerns/entity.rb', line 90

def resistant_to?(damage_type)
  @resistances.include?(damage_type)
end

#saving_throw!(save_type, battle: nil) ⇒ Object



1166
1167
1168
1169
1170
1171
1172
1173
# File 'lib/natural_20/concerns/entity.rb', line 1166

def saving_throw!(save_type, battle: nil)
  modifier = ability_mod(save_type)
  modifier += proficiency_bonus if proficient?("#{save_type}_save")
  op = modifier >= 0 ? '+' : ''
  disadvantage = i[dex str].include?(save_type.to_sym) && !proficient_with_equipped_armor? ? true : false
  DieRoll.roll("d20#{op}#{modifier}", disadvantage: disadvantage, battle: battle, entity: self,
                                      description: t("dice_roll.#{save_type}_saving_throw"))
end

#sentient?Boolean



146
147
148
# File 'lib/natural_20/concerns/entity.rb', line 146

def sentient?
  npc? || pc?
end

#shield_equipped?Boolean



952
953
954
955
956
957
958
959
# File 'lib/natural_20/concerns/entity.rb', line 952

def shield_equipped?
  @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
  !!equipped_meta.detect do |s|
    s[:type] == 'shield'
  end
end

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



1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
# File 'lib/natural_20/concerns/entity.rb', line 1114

def short_rest!(battle, prompt: false)
  controller = battle&.controller_for(self)

  # hit die management
  if prompt && controller && controller.respond_to?(:prompt_hit_die_roll)
    loop do
      break unless @current_hit_die.values.inject(0) { |sum, d| sum + d }.positive?

      ans = battle.controller_for(self)&.try(:prompt_hit_die_roll, self, @current_hit_die.select do |_k, v|
                                                                           v.positive?
                                                                         end.keys)

      if ans == :skip
        break
      else
        use_hit_die!(ans, battle: battle)
      end
    end
  else
    while @hp < max_hp
      available_die = @current_hit_die.map do |die, num|
        next unless num.positive?

        die
      end.compact.sort

      break if available_die.empty?

      old_hp = @hp

      use_hit_die!(available_die.first, battle: battle)

      break if @hp == old_hp # break if unable to heal
    end
  end

  heal!(1) if unconscious? && stable?
end

#size_identifierObject



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
# File 'lib/natural_20/concerns/entity.rb', line 577

def size_identifier
  square_size = size.to_sym
  case square_size
  when :small
    1
  when :medium
    2
  when :large
    3
  when :huge
    4
  when :gargantuan
    5
  else
    raise "invalid size #{square_size}"
  end
end

#speedObject



495
496
497
# File 'lib/natural_20/concerns/entity.rb', line 495

def speed
  @properties[:speed]
end

#spell_slots(_level) ⇒ Object



1205
1206
1207
# File 'lib/natural_20/concerns/entity.rb', line 1205

def spell_slots(_level)
  0
end

#squeezed!Object



1101
1102
1103
# File 'lib/natural_20/concerns/entity.rb', line 1101

def squeezed!
  @statuses.add(:squeezed)
end

#squeezed?Boolean



1109
1110
1111
# File 'lib/natural_20/concerns/entity.rb', line 1109

def squeezed?
  @statuses.include?(:squeezed)
end

#stable!Object



155
156
157
158
159
# File 'lib/natural_20/concerns/entity.rb', line 155

def stable!
  @statuses.add(:stable)
  @death_fails = 0
  @death_saves = 0
end

#stable?Boolean



130
131
132
# File 'lib/natural_20/concerns/entity.rb', line 130

def stable?
  @statuses.include?(:stable)
end

#stand!Object



109
110
111
112
# File 'lib/natural_20/concerns/entity.rb', line 109

def stand!
  Natural20::EventManager.received_event({ source: self, event: :stand })
  @statuses.delete(:prone)
end

#standing_jump_distanceObject



549
550
551
# File 'lib/natural_20/concerns/entity.rb', line 549

def standing_jump_distance
  (@ability_scores.fetch(:str) / 2).floor
end

#stealth_proficient?Boolean



999
1000
1001
# File 'lib/natural_20/concerns/entity.rb', line 999

def stealth_proficient?
  proficient?('stealth')
end

#str_modObject



503
504
505
# File 'lib/natural_20/concerns/entity.rb', line 503

def str_mod
  modifier_table(@ability_scores.fetch(:str))
end

#strength_check!(bonus = 0, battle: nil, description: nil) ⇒ Object



649
650
651
652
653
# File 'lib/natural_20/concerns/entity.rb', line 649

def strength_check!(bonus = 0, battle: nil, description: nil)
  disadvantage = !proficient_with_equipped_armor? ? true : false
  DieRoll.roll_with_lucky(self, "1d20+#{str_mod + bonus}", disadvantage: disadvantage, description: description || t('dice_roll.stength_check'),
                                                           battle: battle)
end

#take_damage!(damage_params, battle = nil) ⇒ Object

Options Hash (damage_params):



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/natural_20/concerns/entity.rb', line 51

def take_damage!(damage_params, battle = nil)
  dmg = damage_params[:damage].is_a?(Natural20::DieRoll) ? damage_params[:damage].result : damage_params[:damage]
  dmg += damage_params[:sneak_attack].result unless damage_params[:sneak_attack].nil?

  dmg = (dmg / 2.to_f).floor if resistant_to?(damage_params[:damage_type])
  @hp -= dmg

  if unconscious?
    @statuses.delete(:stable)
    @death_fails += if damage_params[:attack_roll]&.nat_20?
                      2
                    else
                      1
                    end

    complete = false
    if @death_fails >= 3
      complete = true
      dead!
      @death_saves = 0
      @death_fails = 0
    end
    Natural20::EventManager.received_event({ source: self, event: :death_fail, saves: @death_saves,
                                             fails: @death_fails, complete: complete })
  end

  if @hp.negative? && @hp.abs >= @properties[:max_hp]
    dead!
  elsif @hp <= 0
    npc? ? dead! : unconscious!
  end

  @hp = 0 if @hp <= 0

  on_take_damage(battle, damage_params) if battle

  Natural20::EventManager.received_event({ source: self, event: :damage, value: dmg })
end

#to_item(k, item) ⇒ Object



961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
# File 'lib/natural_20/concerns/entity.rb', line 961

def to_item(k, item)
  OpenStruct.new(
    name: k.to_sym,
    label: item[:label].presence || k.to_s.humanize,
    type: item[:type],
    subtype: item[:subtype],
    light: item[:properties].try(:include?, 'light'),
    two_handed: item[:properties].try(:include?, 'two_handed'),
    light_properties: item[:light],
    proficiency_type: item[:proficiency_type],
    qty: 1,
    equipped: true,
    weight: item[:weight]
  )
end

#token_sizeObject



561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/natural_20/concerns/entity.rb', line 561

def token_size
  square_size = size.to_sym
  case square_size
  when :small
    1
  when :medium
    1
  when :large
    2
  when :huge
    3
  else
    raise "invalid size #{square_size}"
  end
end

#total_actions(battle) ⇒ Object



467
468
469
# File 'lib/natural_20/concerns/entity.rb', line 467

def total_actions(battle)
  battle.entity_state_for(self)[:action]
end

#total_bonus_actions(battle) ⇒ Object



481
482
483
# File 'lib/natural_20/concerns/entity.rb', line 481

def total_bonus_actions(battle)
  battle.entity_state_for(self)[:bonus_action]
end

#total_reactions(battle) ⇒ Object



471
472
473
# File 'lib/natural_20/concerns/entity.rb', line 471

def total_reactions(battle)
  battle.entity_state_for(self)[:reaction]
end

#trigger_event(event_name, battle, session, map, event) ⇒ Object



670
671
672
673
674
675
676
# File 'lib/natural_20/concerns/entity.rb', line 670

def trigger_event(event_name, battle, session, map, event)
  @event_handlers ||= {}
  return unless @event_handlers.key?(event_name.to_sym)

  object, method_name = @event_handlers[event_name.to_sym]
  object.send(method_name.to_sym, battle, session, self, map, event)
end

#unconscious!Object



281
282
283
284
285
286
287
# File 'lib/natural_20/concerns/entity.rb', line 281

def unconscious!
  return if unconscious? || dead?

  drop_grapple!
  Natural20::EventManager.received_event({ source: self, event: :unconscious })
  @statuses.add(:unconscious)
end

#unconscious?Boolean



122
123
124
# File 'lib/natural_20/concerns/entity.rb', line 122

def unconscious?
  !dead? && @statuses.include?(:unconscious)
end

#unequip(item_name, transfer_inventory: true) ⇒ Object

Unequips a weapon



802
803
804
# File 'lib/natural_20/concerns/entity.rb', line 802

def unequip(item_name, transfer_inventory: true)
  add_item(item_name.to_sym) if @properties[:equipped].delete(item_name.to_s) && transfer_inventory
end

#unequip_allObject

removes all equiped. Used for tests



807
808
809
# File 'lib/natural_20/concerns/entity.rb', line 807

def unequip_all
  @properties[:equipped].clear
end

#ungrapple(target) ⇒ Object



696
697
698
699
700
701
# File 'lib/natural_20/concerns/entity.rb', line 696

def ungrapple(target)
  @grappling ||= []
  @grappling.delete(target)
  target.grapples.delete(self)
  target.statuses.delete(:grappled) if target.grapples.empty?
end

#unsqueezeObject



1105
1106
1107
# File 'lib/natural_20/concerns/entity.rb', line 1105

def unsqueeze
  @statuses.delete(:squeezed)
end

#usable_itemsObject



746
747
748
749
750
751
752
753
754
755
756
757
758
# File 'lib/natural_20/concerns/entity.rb', line 746

def usable_items
  @inventory.map do |k, v|
    item_details =
      session.load_equipment(v.type)

    next unless item_details
    next unless item_details[:usable]
    next if item_details[:consumable] && v.qty.zero?

    { name: k.to_s, label: item_details[:name] || k, item: item_details, qty: v.qty,
      consumable: item_details[:consumable] }
  end.compact
end

#usable_objects(map, battle) ⇒ Array

Show usable objects near the entity



771
772
773
# File 'lib/natural_20/concerns/entity.rb', line 771

def usable_objects(map, battle)
  map.objects_near(self, battle)
end

#use_hit_die!(die_type, battle: nil) ⇒ Object



1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
# File 'lib/natural_20/concerns/entity.rb', line 1153

def use_hit_die!(die_type, battle: nil)
  return unless @current_hit_die.key? die_type
  return unless @current_hit_die[die_type].positive?

  @current_hit_die[die_type] -= 1

  hit_die_roll = DieRoll.roll("d#{die_type}", battle: battle, entity: self, description: t('dice_roll.hit_die'))

  EventManager.received_event({ source: self, event: :hit_die, roll: hit_die_roll })

  heal!(hit_die_roll.result)
end

#used_hand_slots(weapon_only: false) ⇒ Object



893
894
895
896
897
898
899
# File 'lib/natural_20/concerns/entity.rb', line 893

def used_hand_slots(weapon_only: false)
  equipped_items.select do |item|
    item.subtype == 'weapon' || (!weapon_only && item.type == 'shield')
  end.inject(0.0) do |slot, item|
    slot + hand_slots_required(item)
  end
end

#wearing_armor?Boolean



877
878
879
# File 'lib/natural_20/concerns/entity.rb', line 877

def wearing_armor?
  !!equipped_items.detect { |t| %w[armor shield].include?(t[:type]) }
end

#wis_modObject



511
512
513
# File 'lib/natural_20/concerns/entity.rb', line 511

def wis_mod
  modifier_table(@ability_scores.fetch(:wis))
end

#wisdom_check!(bonus = 0, battle: nil, description: nil) ⇒ Object



655
656
657
658
# File 'lib/natural_20/concerns/entity.rb', line 655

def wisdom_check!(bonus = 0, battle: nil, description: nil)
  DieRoll.roll_with_lucky(self, "1d20+#{wis_mod + bonus}", description: description || t('dice_roll.wisdom_check'),
                                                           battle: battle)
end