class Character < ApplicationRecord belongs_to :user belongs_to :activity, optional: true has_many :title_awards has_many :titles, through: :title_awards belongs_to :active_title, class_name: "Title", optional: true has_one :hearth has_many :equipment has_many :character_items has_many :learned_activities has_many :items, through: :character_items has_many :character_skills has_many :chat_messages has_many :bazaar_orders validates :name, presence: true validates_length_of :name, maximum: 15, message: "can't be longer than 15 characters" validates_uniqueness_of :name, message: "is already being used" validates_format_of :name, with: /\A[a-z]+\z/i, message: "must consist of letters only" attribute :wounds, :integer, default: 0 enum offensive_style: [:balanced, :precise, :brutal] enum defensive_style: [:centered, :elusive, :protective] after_create :create_skills, :set_combat_styles after_create { Hearth.create(character: self) } def beastslay_level; skill_level("beastslay"); end def fluxseethe_level; skill_level("fluxseethe"); end def havencast_level; skill_level("havencast"); end def hexcarve_level; skill_level("hexcarve"); end def magiculture_level; skill_level("magiculture"); end def manatrawl_level; skill_level("manatrawl"); end def omenbind_level; skill_level("omenbind"); end def otherforge_level; skill_level("otherforge"); end def planequarry_level; skill_level("planequarry"); end def spicework_level; skill_level("spicework"); end def synthsever_level; skill_level("synthsever"); end def veilreach_level; skill_level("veilreach"); end def wealdreap_level; skill_level("wealdreap"); end def wildscour_level; skill_level("wildscour"); end def worldcall_level; skill_level("worldcall"); end def vestige vestige = self.character_items.find_by(item: Item.find_by_gid("vestige")) vestige ? vestige.quantity : 0 end def shift_item(item, amount) item = Item.find_by_gid(item) if item.is_a? String CharacterItem.transaction do ci = self.character_items.find_or_initialize_by(item: item) ci.increment(:quantity, amount) ci.save! end end def pay_cost_for(activity) CharacterItem.transaction do activity.whatnot[:cost]&.each do |cost| case cost[:type] when "item" self.shift_item(cost[:gid], -cost[:quantity]) end end end end def has_item?(item, quantity = 1) item = Item.find_by_gid(item) if item.is_a? String ci = self.character_items.find_by(item: item) ci && ci.quantity >= quantity end def open_slots_for(item) full_slots = self.equipment.map { |e| e.slot } item.equip_slots.reject { |slot| full_slots.include?(slot) } end def equip(item) Character.transaction do open_slots = self.open_slots_for(item) raise EquipmentError unless open_slots.any? self.shift_item(item, -1) self.equipment.create!(item: item, slot: open_slots.first) end end def unequip(slot) Character.transaction do equipment = self.equipment.find_by(slot: slot) raise EquipmentError unless equipment item = equipment.item equipment.destroy! self.shift_item(item, 1) end end def add_skill_xp(skill, amount) skill = Skill.find_by_gid(skill) if skill.is_a? String Character.transaction do cs = self.character_skills.find_by(skill: skill) cs.xp += amount cs.save! end end def skill_level(skill) skill = Skill.find_by_gid(skill) if skill.is_a? String self.character_skills.find_by(skill: skill).level end def activity_time_remaining return nil unless self.activity duration_data = self.activity.whatnot[:duration] duration = duration_data[:base] duration_data[:scaling]&.each do |scaling_entry| case scaling_entry[:type] when "skill" duration -= self.skill_level(scaling_entry[:gid]) * scaling_entry[:scale_value] when "stat" duration -= self.total_stat_change(scaling_entry[:gid]) * scaling_entry[:scale_value] else raise "Invalid scaling type." # TODO: Improve this end end duration = [duration, duration_data[:minimum] || 10].max duration - (Time.now - self.activity_started_at) end def can_do_activity?(activity) return false unless activity.innate? || self.learned_activities.exists?(activity: activity) activity.whatnot[:cost]&.each do |cost| case cost[:type] when "item" return false unless self.has_item?(cost[:gid], cost[:quantity]) end end activity.whatnot[:requirements]&.each do |requirement| case requirement[:type] when "hearth_amenity" return false unless self.hearth.has_amenity?(requirement[:gid], requirement[:level]) end end true end def start_activity(activity, queued_actions: nil) if self.can_do_activity?(activity) self.update(activity: activity, activity_started_at: Time.now, queued_actions: queued_actions) else false end end def stop_activity self.update(activity: nil, activity_started_at: nil, queued_actions: nil) end def award_title(title) title = Title.find_by_gid(title) if title.is_a? String # TODO: Simplify these lines? return false if self.title_awards.exists?(title: title) self.title_awards.create(title: title) end def equipment_stats stats = {} self.equipment.each do |eq| eq.item.whatnot[:equip_effects]&.each do |effect| if effect[:type] == "stat" stats[effect[:gid]] ||= 0 stats[effect[:gid]] += effect[:modifier] end end end stats end def effects # TODO: Review this filter_map to see if it can be simplified self.hearth.built_hearth_amenities.filter_map { |a| a.effects if a.effects }.flatten end def total_stat_change(stat) effects.filter_map { |e| e[:modifier] if e[:type] == "stat_change" && e[:stat] == stat }.sum end def can_fight? self.wounds < max_wounds end def max_wounds 1 + total_stat_change("max_wounds") end def max_hp 9 + self.beastslay_level + (equipment_stats["max_hp"] || 0) end def speed self.beastslay_level + (equipment_stats["speed"] || 0) end def accuracy(with_combat_style: false) base = self.beastslay_level + (equipment_stats["accuracy"] || 0) if with_combat_style && self.precise? base = (base * 1.25).floor elsif with_combat_style && self.brutal? base = (base * 0.75).ceil end base end def power(with_combat_style: false) base = self.beastslay_level + (equipment_stats["power"] || 0) if with_combat_style && self.precise? base = (base * 0.75).ceil elsif with_combat_style && self.brutal? base = (base * 1.25).floor end base end def evasion(with_combat_style: false) base = self.beastslay_level + (equipment_stats["evasion"] || 0) if with_combat_style && self.elusive? base = (base * 1.25).floor elsif with_combat_style && self.protective? base = (base * 0.75).ceil end base end def block(with_combat_style: false) base = self.beastslay_level + (equipment_stats["block"] || 0) if with_combat_style && self.elusive? base = (base * 0.75).ceil elsif with_combat_style && self.protective? base = (base * 1.25).floor end base end def block_value equipment_stats["block_value"] || 0 end private def create_skills Skill.all.each { |skill| self.character_skills.create(skill: skill, xp: 0) } end def set_combat_styles self.update(offensive_style: :balanced, defensive_style: :centered) end end