hensei-api/app/models/character.rb

157 lines
4.5 KiB
Ruby

# 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