module Natural20::Entity
Constants
- ALL_SKILLS
- ATTRIBUTE_TYPES
- ATTRIBUTE_TYPES_ABBV
- SKILL_AND_ABILITY_MAP
Attributes
Public Instance Methods
# 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
# File lib/natural_20/concerns/entity.rb, line 1003 def acrobatics_proficient? proficient?('acrobatics') end
Checks if an entity still has an action available @param battle [Natural20::Battle]
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 1236 def add_casted_effect(effect) @casted_effects << effect end
Adds an item to your inventory @param ammo_type [Symbol,String] @param amount [Integer] @param source_item [Object]
# 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
# 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
# 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
checks if at least one class feature matches @param features [Array<String>] @return [Boolean]
# File lib/natural_20/concerns/entity.rb, line 1034 def any_class_feature?(features) !features.select { |f| class_feature?(f) }.empty? end
# File lib/natural_20/concerns/entity.rb, line 1007 def athletics_proficient? proficient?('athletics') end
# 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
# 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
# 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
@param battle [Natural::20] @return [Integer]
# File lib/natural_20/concerns/entity.rb, line 487 def available_movement(battle) grappled? ? 0 : battle.entity_state_for(self)[:movement] end
# File lib/natural_20/concerns/entity.rb, line 491 def available_spells [] end
# 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
check if current entity can see target at a certain location
# 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
returns the carrying capacity of an entity in lbs @return [Float] carrying capacity in lbs
# File lib/natural_20/concerns/entity.rb, line 979 def carry_capacity @ability_scores.fetch(:str, 1) * 15.0 end
# File lib/natural_20/concerns/entity.rb, line 515 def cha_mod modifier_table(@ability_scores.fetch(:cha)) end
Checks if item can be equipped @param item_name [String,Symbol] @return [Symbol]
# 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
# File lib/natural_20/concerns/entity.rb, line 1027 def class_feature?(feature) @properties[:attributes]&.include?(feature) end
# File lib/natural_20/concerns/entity.rb, line 507 def con_mod modifier_table(@ability_scores.fetch(:con)) end
# File lib/natural_20/concerns/entity.rb, line 150 def conscious! @statuses.delete(:unconscious) @statuses.delete(:stable) end
# File lib/natural_20/concerns/entity.rb, line 126 def conscious? !dead? && !unconscious? end
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 118 def dead? @statuses.include?(:dead) end
Perform a death saving throw
# 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
Removes Item from inventory @param ammo_type [Symbol,String] @param amount [Integer] @return [OpenStruct]
# 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
# File lib/natural_20/concerns/entity.rb, line 289 def description @properties[:description].presence || '' end
# File lib/natural_20/concerns/entity.rb, line 523 def dex_mod modifier_table(@ability_scores.fetch(:dex)) end
# 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
# 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
# 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
# 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
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 703 def drop_grapple! @grappling ||= [] @grappling.each do |target| ungrapple(target) end end
@param battle [Natural20::Batle @param item_and_counts [Array<Array<OpenStruct,Integer>>]
# 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
convenience method used to determine if a creature entered or is at melee range of another
# 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
Equips an item @param item_name [String,Symbol]
# 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
Checks if an item is equipped @param item_name [String,Symbol] @return [Boolean]
# File lib/natural_20/concerns/entity.rb, line 814 def equipped?(item_name) equipped_items.map(&:name).include?(item_name.to_sym) end
returns equipped items @return [Array<OpenStruct>] A List of items
# 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
# File lib/natural_20/concerns/entity.rb, line 901 def equipped_weapons equipped_items.select do |item| item.subtype == 'weapon' end.map(&:name) end
# 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
# File lib/natural_20/concerns/entity.rb, line 557 def expertise @properties.fetch(:expertise, []) end
# File lib/natural_20/concerns/entity.rb, line 32 def expertise?(prof) @properties[:expertise]&.include?(prof.to_s) end
# 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
# File lib/natural_20/concerns/entity.rb, line 922 def grappled? @statuses.include?(:grappled) end
@param grappler [Natural20::Entity]
# File lib/natural_20/concerns/entity.rb, line 361 def grappled_by!(grappler) @statuses.add(:grappled) @grapples ||= [] @grapples << grappler grappler.grappling(self) end
# File lib/natural_20/concerns/entity.rb, line 375 def grapples @grapples || [] end
@param target [Natural20::Entity]
# File lib/natural_20/concerns/entity.rb, line 679 def grappling(target) @grappling ||= [] @grappling << target end
# File lib/natural_20/concerns/entity.rb, line 684 def grappling? @grappling ||= [] !@grappling.empty? end
# File lib/natural_20/concerns/entity.rb, line 690 def grappling_targets @grappling ||= [] @grappling end
# 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
# File lib/natural_20/concerns/entity.rb, line 499 def has_reaction?(battle) (battle.entity_state_for(self)[:reaction].presence || 0).positive? end
# 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
# File lib/natural_20/concerns/entity.rb, line 417 def has_spells? return false unless @properties[:prepared_spells] !@properties[:prepared_spells].empty? end
# 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
# 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
# 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
@param battle [Natural20::Battle] @param stealth [Integer]
# 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
# 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
Returns the character hit die @return [Hash<Integer,Integer>]
# File lib/natural_20/concerns/entity.rb, line 1097 def hit_die @current_hit_die end
# File lib/natural_20/concerns/entity.rb, line 918 def incapacitated? @statuses.include?(:unconscious) || @statuses.include?(:sleep) || @statuses.include?(:dead) end
# 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
# File lib/natural_20/concerns/entity.rb, line 995 def insight_proficient? proficient?('insight') end
# File lib/natural_20/concerns/entity.rb, line 519 def int_mod modifier_table(@ability_scores.fetch(:int)) end
Returns items in the “backpack” of the entity @return [Array]
# 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
# File lib/natural_20/concerns/entity.rb, line 793 def inventory_count @inventory.values.inject(0) do |total, item| total + item[:qty] end end
returns in lbs the weight of all items in the inventory @return [Float] weight in lbs
# 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
# File lib/natural_20/concerns/entity.rb, line 991 def investigation_proficient? proficient?('investigation') end
Retrieves the item count of an item in the entities inventory @param inventory_type [Symbol] @return [Integer]
# 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
# File lib/natural_20/concerns/entity.rb, line 983 def items_label I18n.t(:"entity.#{self.class}.item_label", default: "#{name} Items") end
# File lib/natural_20/concerns/entity.rb, line 12 def label I18n.exists?(name, :en) ? I18n.t(name) : name.humanize end
# File lib/natural_20/concerns/entity.rb, line 270 def languages @properties[:languages] || [] end
# 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
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 553 def long_jump_distance @ability_scores.fetch(:str) end
# File lib/natural_20/concerns/entity.rb, line 1209 def max_spell_slots(_level) 0 end
# 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
# File lib/natural_20/concerns/entity.rb, line 1011 def medicine_proficient? proficient?('medicine') end
# File lib/natural_20/concerns/entity.rb, line 182 def melee_distance 0 end
@param map [Natural20::BattleMap] @param target_position [Array<Integer,Integer>] @param adjacent_only [Boolean] If false uses melee distance otherwise uses fixed 1 square away
# 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
# File lib/natural_20/concerns/entity.rb, line 138 def npc? false end
# File lib/natural_20/concerns/entity.rb, line 134 def object? false end
# File lib/natural_20/concerns/entity.rb, line 914 def opened? false end
# File lib/natural_20/concerns/entity.rb, line 545 def passive_perception @properties[:passive_perception] || 10 + wis_mod end
# File lib/natural_20/concerns/entity.rb, line 142 def pc? false end
# File lib/natural_20/concerns/entity.rb, line 987 def perception_proficient? proficient?('perception') end
Returns tghe proficiency bonus of this entity @return [Integer]
# File lib/natural_20/concerns/entity.rb, line 928 def proficiency_bonus @properties[:proficiency_bonus].presence || 2 end
# 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
# 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
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 104 def prone! Natural20::EventManager.received_event({ source: self, event: :prone }) @statuses.add(:prone) end
# File lib/natural_20/concerns/entity.rb, line 114 def prone? @statuses.include?(:prone) end
@param map [Natural20::BattleMap]
# 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
# File lib/natural_20/concerns/entity.rb, line 16 def race @properties[:race] end
# 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
# 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
# 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
@param battle [Natural20::Battle] @return [Hash]
# 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
# File lib/natural_20/concerns/entity.rb, line 90 def resistant_to?(damage_type) @resistances.include?(damage_type) end
# 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
# File lib/natural_20/concerns/entity.rb, line 146 def sentient? npc? || pc? end
# 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
@param hit_die_num [Integer] number of hit die to use
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 495 def speed @properties[:speed] end
# File lib/natural_20/concerns/entity.rb, line 1205 def spell_slots(_level) 0 end
# File lib/natural_20/concerns/entity.rb, line 1101 def squeezed! @statuses.add(:squeezed) end
# File lib/natural_20/concerns/entity.rb, line 1109 def squeezed? @statuses.include?(:squeezed) end
# File lib/natural_20/concerns/entity.rb, line 155 def stable! @statuses.add(:stable) @death_fails = 0 @death_saves = 0 end
# File lib/natural_20/concerns/entity.rb, line 130 def stable? @statuses.include?(:stable) end
# File lib/natural_20/concerns/entity.rb, line 109 def stand! Natural20::EventManager.received_event({ source: self, event: :stand }) @statuses.delete(:prone) end
# File lib/natural_20/concerns/entity.rb, line 549 def standing_jump_distance (@ability_scores.fetch(:str) / 2).floor end
# File lib/natural_20/concerns/entity.rb, line 999 def stealth_proficient? proficient?('stealth') end
# File lib/natural_20/concerns/entity.rb, line 503 def str_mod modifier_table(@ability_scores.fetch(:str)) end
# 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
@option damage_params damage [Natural20::DieRoll] @option damage_params sneak_attack [Natural20::DieRoll] @param battle [Natural20::Battle]
# 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
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 467 def total_actions(battle) battle.entity_state_for(self)[:action] end
# File lib/natural_20/concerns/entity.rb, line 481 def total_bonus_actions(battle) battle.entity_state_for(self)[:bonus_action] end
# File lib/natural_20/concerns/entity.rb, line 471 def total_reactions(battle) battle.entity_state_for(self)[:reaction] end
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 122 def unconscious? !dead? && @statuses.include?(:unconscious) end
Unequips a weapon @param item_name [String,Symbol] @param transfer_inventory [Boolean] Add this item to the inventory?
# 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
removes all equiped. Used for tests
# File lib/natural_20/concerns/entity.rb, line 807 def unequip_all @properties[:equipped].clear end
@param target [Natural20::Entity]
# 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
# File lib/natural_20/concerns/entity.rb, line 1105 def unsqueeze @statuses.delete(:squeezed) end
# 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
Show usable objects near the entity @param map [Natural20::BattleMap] @param battle [Natural20::Battle] @return [Array]
# File lib/natural_20/concerns/entity.rb, line 771 def usable_objects(map, battle) map.objects_near(self, battle) end
# 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
# 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
# File lib/natural_20/concerns/entity.rb, line 877 def wearing_armor? !!equipped_items.detect { |t| %w[armor shield].include?(t[:type]) } end
# File lib/natural_20/concerns/entity.rb, line 511 def wis_mod modifier_table(@ability_scores.fetch(:wis)) end
# 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
Protected Instance Methods
# File lib/natural_20/concerns/entity.rb, line 1345 def character_advancement_table [ [0, 1, 2], [300, 2, 2], [900, 3, 2], [2700, 4, 2], [6500, 5, 3], [14_000, 6, 3], [23_000, 7, 3], [34_000, 8, 3], [48_000, 9, 4], [64_000, 10, 4], [85_000, 11, 4], [100_000, 12, 4], [120_000, 13, 5], [140_000, 14, 5], [165_000, 15, 5], [195_000, 16, 5], [225_000, 17, 6], [265_000, 18, 6], [305_000, 19, 6], [355_000, 20, 6] ] end
# File lib/natural_20/concerns/entity.rb, line 1251 def cleanup_effects @effects = @effects.map do |k, value| delete_effects = value.select do |f| f[:expiration] && f[:expiration] <= @session.game_time end [k, value - delete_effects] end.to_h @entity_event_hooks = @entity_event_hooks.map do |k, value| delete_hooks = value.select do |f| f[:expiration] && f[:expiration] <= @session.game_time end [k, value - delete_hooks] end.to_h @casted_effects = @casted_effects.select do |f| f[:expiration].blank? || f[:expiration] > @session.game_time end end
# File lib/natural_20/concerns/entity.rb, line 1282 def eval_effect(effect_type, opts = {}) active_effect = @effects[effect_type.to_sym].reject do |effect| effect[:expiration] && effect[:expiration] <= @session.game_time end.last active_effect[:handler].send(active_effect[:method], self, opts.merge(effect: active_effect[:effect])) if active_effect end
# File lib/natural_20/concerns/entity.rb, line 1271 def has_effect?(effect_type) return false unless @effects.key?(effect_type.to_sym) return false if @effects[effect_type.to_sym].empty? active_effects = @effects[effect_type.to_sym].reject do |effect| effect[:expiration] && effect[:expiration] <= @session.game_time end !active_effects.empty? end
# File lib/natural_20/concerns/entity.rb, line 1321 def modifier_table(value) mod_table = [[1, 1, -5], [2, 3, -4], [4, 5, -3], [6, 7, -2], [8, 9, -1], [10, 11, 0], [12, 13, 1], [14, 15, 2], [16, 17, 3], [18, 19, 4], [20, 21, 5], [22, 23, 6], [24, 25, 7], [26, 27, 8], [28, 29, 9], [30, 30, 10]] mod_table.each do |row| low, high, mod = row return mod if value.between?(low, high) end end
# File lib/natural_20/concerns/entity.rb, line 1316 def on_take_damage(battle, _damage_params) controller = battle.controller_for(self) controller.attack_listener(battle, self) if controller && controller.respond_to?(:attack_listener) end
# File lib/natural_20/concerns/entity.rb, line 1290 def resolve_trigger(event_type) return unless @entity_event_hooks[event_type.to_sym] active_hook = @entity_event_hooks[event_type.to_sym].reject do |effect| effect[:expiration] && effect[:expiration] <= @session.game_time end.last active_hook[:handler].send(active_hook[:method], self, effect: active_hook[:effect]) if active_hook end
# File lib/natural_20/concerns/entity.rb, line 1308 def setup_attributes @death_saves = 0 @death_fails = 0 @entity_event_hooks = {} @effects = {} @casted_effects = [] end
Localization helper @param token [Symbol, String] @param options [Hash] @return [String]
# File lib/natural_20/concerns/entity.rb, line 1304 def t(token, options = {}) I18n.t(token, **options) end