# frozen_string_literal: true class Character < ApplicationRecord include PgSearch::Model has_many :character_series_memberships, dependent: :destroy has_many :character_series_records, through: :character_series_memberships, source: :character_series multisearchable against: %i[name_en name_jp], additional_attributes: lambda { |character| { name_en: character.name_en, name_jp: character.name_jp, granblue_id: character.granblue_id, element: character.element } } pg_search_scope :en_search, against: %i[name_en nicknames_en], using: { tsearch: { prefix: true, dictionary: 'simple' }, trigram: { threshold: 0.18 } } pg_search_scope :ja_search, against: %i[name_jp nicknames_jp], using: { tsearch: { prefix: true, dictionary: 'simple' } } AWAKENINGS = [ { slug: 'character-balanced', name_en: 'Balanced', name_jp: 'バランス', order: 0 }, { slug: 'character-atk', name_en: 'Attack', name_jp: '攻撃', order: 1 }, { slug: 'character-def', name_en: 'Defense', name_jp: '防御', order: 2 }, { slug: 'character-multi', name_en: 'Multiattack', name_jp: '連続攻撃', order: 3 } ].freeze # Validations validates :season, numericality: { only_integer: true }, inclusion: { in: GranblueEnums::CHARACTER_SEASONS.values }, allow_nil: true validate :validate_series_values # Scopes scope :by_season, ->(season) { where(season: season) } scope :by_series, ->(series) { where('? = ANY(series)', series) } scope :seasonal, -> { where.not(season: [nil, GranblueEnums::CHARACTER_SEASONS[:Standard]]) } def blueprint CharacterBlueprint end def display_resource(character) character.name_en end # Helper methods def seasonal? season.present? && season != GranblueEnums::CHARACTER_SEASONS[:Standard] end def season_name return nil if season.nil? GranblueEnums::CHARACTER_SEASONS.key(season)&.to_s end def series_names # Use new lookup table if available if character_series_records.loaded? ? character_series_records.any? : character_series_records.exists? character_series_records.ordered.pluck(:name_en) elsif series.present? # Legacy fallback series.filter_map { |s| GranblueEnums::CHARACTER_SERIES.key(s)&.to_s } else [] end end def series_objects character_series_records.ordered end def series_slugs character_series_records.pluck(:slug) end # Mapping from legacy integer values to slugs LEGACY_SERIES_TO_SLUG = { 1 => 'grand', 2 => 'zodiac', 3 => 'promo', 4 => 'collab', 5 => 'eternal', 6 => 'evoker', 7 => 'saint', 8 => 'fantasy', 9 => 'summer', 10 => 'yukata', 11 => 'valentine', 12 => 'halloween', 13 => 'formal', 14 => 'holiday', 15 => 'event' }.freeze # Virtual attribute to set character_series by array of IDs, slugs, or legacy integers # Supports multiple formats for flexibility during migration def series=(values) return if values.blank? # Ensure it's an array values = Array(values) values.each do |value| next if value.blank? # Try to find the series record series_record = if value.is_a?(Integer) # Legacy integer - convert to slug first slug = LEGACY_SERIES_TO_SLUG[value] slug ? CharacterSeries.find_by(slug: slug) : nil else # String - try UUID first, then slug CharacterSeries.find_by(id: value) || CharacterSeries.find_by(slug: value) end next unless series_record # Create membership if it doesn't exist character_series_memberships.find_or_initialize_by(character_series: series_record) end end private def validate_series_values return if series.blank? invalid_values = series.reject { |s| GranblueEnums::CHARACTER_SERIES.values.include?(s) } return if invalid_values.empty? errors.add(:series, "contains invalid values: #{invalid_values.join(', ')}") end end