add artifact models with skill validations
This commit is contained in:
parent
210af50477
commit
c19259c84a
7 changed files with 378 additions and 0 deletions
36
app/models/artifact.rb
Normal file
36
app/models/artifact.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Artifact < ApplicationRecord
|
||||||
|
# Enums - using GranblueEnums::PROFICIENCY values (excluding None: 0)
|
||||||
|
# Sabre: 1, Dagger: 2, Axe: 3, Spear: 4, Bow: 5, Staff: 6, Melee: 7, Harp: 8, Gun: 9, Katana: 10
|
||||||
|
enum :proficiency, {
|
||||||
|
sabre: 1,
|
||||||
|
dagger: 2,
|
||||||
|
axe: 3,
|
||||||
|
spear: 4,
|
||||||
|
bow: 5,
|
||||||
|
staff: 6,
|
||||||
|
melee: 7,
|
||||||
|
harp: 8,
|
||||||
|
gun: 9,
|
||||||
|
katana: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
enum :rarity, { standard: 0, quirk: 1 }
|
||||||
|
|
||||||
|
# Associations
|
||||||
|
has_many :collection_artifacts, dependent: :restrict_with_error
|
||||||
|
has_many :grid_artifacts, dependent: :restrict_with_error
|
||||||
|
|
||||||
|
# Validations
|
||||||
|
validates :granblue_id, presence: true, uniqueness: true
|
||||||
|
validates :name_en, presence: true
|
||||||
|
validates :proficiency, presence: true, if: :standard?
|
||||||
|
validates :proficiency, absence: true, if: :quirk?
|
||||||
|
validates :rarity, presence: true
|
||||||
|
|
||||||
|
# Scopes
|
||||||
|
scope :standard_artifacts, -> { where(rarity: :standard) }
|
||||||
|
scope :quirk_artifacts, -> { where(rarity: :quirk) }
|
||||||
|
scope :by_proficiency, ->(prof) { where(proficiency: prof) }
|
||||||
|
end
|
||||||
74
app/models/artifact_skill.rb
Normal file
74
app/models/artifact_skill.rb
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ArtifactSkill < ApplicationRecord
|
||||||
|
# Enums
|
||||||
|
enum :skill_group, { group_i: 1, group_ii: 2, group_iii: 3 }
|
||||||
|
enum :polarity, { positive: 'positive', negative: 'negative' }
|
||||||
|
|
||||||
|
# Validations
|
||||||
|
validates :skill_group, presence: true
|
||||||
|
validates :modifier, presence: true, uniqueness: { scope: :skill_group }
|
||||||
|
validates :name_en, presence: true
|
||||||
|
validates :name_jp, presence: true
|
||||||
|
validates :base_values, presence: true
|
||||||
|
validates :polarity, presence: true
|
||||||
|
|
||||||
|
# Scopes
|
||||||
|
scope :for_slot, ->(slot) {
|
||||||
|
case slot
|
||||||
|
when 1, 2 then group_i
|
||||||
|
when 3 then group_ii
|
||||||
|
when 4 then group_iii
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
# Class methods for caching skill lookups
|
||||||
|
class << self
|
||||||
|
def cached_skills
|
||||||
|
@cached_skills ||= all.index_by { |s| [s.skill_group, s.modifier] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_skill(group, modifier)
|
||||||
|
# Convert group number to enum key
|
||||||
|
group_key = case group
|
||||||
|
when 1 then 'group_i'
|
||||||
|
when 2 then 'group_ii'
|
||||||
|
when 3 then 'group_iii'
|
||||||
|
else group.to_s
|
||||||
|
end
|
||||||
|
cached_skills[[group_key, modifier]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_cache!
|
||||||
|
@cached_skills = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculate the current value of a skill given base strength and skill level
|
||||||
|
# @param base_strength [Numeric] The base strength value of the skill
|
||||||
|
# @param skill_level [Integer] The current skill level (1-5)
|
||||||
|
# @return [Numeric, nil] The calculated value
|
||||||
|
def calculate_value(base_strength, skill_level)
|
||||||
|
return base_strength if growth.nil?
|
||||||
|
|
||||||
|
base_strength + (growth * (skill_level - 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Format a value with the appropriate suffix
|
||||||
|
# @param value [Numeric] The value to format
|
||||||
|
# @param locale [Symbol] :en or :jp
|
||||||
|
# @return [String] The formatted value with suffix
|
||||||
|
def format_value(value, locale = :en)
|
||||||
|
suffix = locale == :jp ? suffix_jp : suffix_en
|
||||||
|
"#{value}#{suffix}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if a strength value is valid for this skill
|
||||||
|
# @param strength [Numeric] The strength value to validate
|
||||||
|
# @return [Boolean]
|
||||||
|
def valid_strength?(strength)
|
||||||
|
return true if base_values.include?(nil) # Unknown values are always valid
|
||||||
|
|
||||||
|
base_values.include?(strength)
|
||||||
|
end
|
||||||
|
end
|
||||||
60
app/models/collection_artifact.rb
Normal file
60
app/models/collection_artifact.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CollectionArtifact < ApplicationRecord
|
||||||
|
include ArtifactSkillValidations
|
||||||
|
|
||||||
|
# Associations
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :artifact
|
||||||
|
|
||||||
|
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
||||||
|
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
||||||
|
enum :element, {
|
||||||
|
wind: 1,
|
||||||
|
fire: 2,
|
||||||
|
water: 3,
|
||||||
|
earth: 4,
|
||||||
|
dark: 5,
|
||||||
|
light: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proficiency enum - only used for quirk artifacts (game assigns random proficiency)
|
||||||
|
enum :proficiency, {
|
||||||
|
sabre: 1,
|
||||||
|
dagger: 2,
|
||||||
|
axe: 3,
|
||||||
|
spear: 4,
|
||||||
|
bow: 5,
|
||||||
|
staff: 6,
|
||||||
|
melee: 7,
|
||||||
|
harp: 8,
|
||||||
|
gun: 9,
|
||||||
|
katana: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validations
|
||||||
|
validates :element, presence: true
|
||||||
|
validates :level, presence: true, inclusion: { in: 1..5 }
|
||||||
|
validates :nickname, length: { maximum: 50 }, allow_blank: true
|
||||||
|
validates :proficiency, presence: true, if: :quirk_artifact?
|
||||||
|
validates :proficiency, absence: true, unless: :quirk_artifact?
|
||||||
|
|
||||||
|
# Scopes
|
||||||
|
scope :by_element, ->(el) { where(element: el) }
|
||||||
|
scope :by_artifact, ->(artifact_id) { where(artifact_id: artifact_id) }
|
||||||
|
scope :by_proficiency, ->(prof) { where(proficiency: prof) }
|
||||||
|
scope :by_rarity, ->(rar) { joins(:artifact).where(artifacts: { rarity: rar }) }
|
||||||
|
scope :standard_only, -> { joins(:artifact).where(artifacts: { rarity: :standard }) }
|
||||||
|
scope :quirk_only, -> { joins(:artifact).where(artifacts: { rarity: :quirk }) }
|
||||||
|
|
||||||
|
# Returns the effective proficiency - from instance for quirk, from artifact for standard
|
||||||
|
def effective_proficiency
|
||||||
|
quirk_artifact? ? proficiency : artifact&.proficiency
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def quirk_artifact?
|
||||||
|
artifact&.quirk?
|
||||||
|
end
|
||||||
|
end
|
||||||
108
app/models/concerns/artifact_skill_validations.rb
Normal file
108
app/models/concerns/artifact_skill_validations.rb
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ArtifactSkillValidations
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
validate :validate_skill1_group_i
|
||||||
|
validate :validate_skill2_group_i
|
||||||
|
validate :validate_skill3_group_ii
|
||||||
|
validate :validate_skill4_group_iii
|
||||||
|
validate :validate_duplicate_skills
|
||||||
|
validate :validate_skill_levels_sum, unless: :quirk_artifact?
|
||||||
|
validate :validate_quirk_artifact_constraints, if: :quirk_artifact?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def quirk_artifact?
|
||||||
|
artifact&.quirk?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_skill_in_group(skill_data, group_number, slot_name)
|
||||||
|
return if skill_data.blank? || skill_data == {}
|
||||||
|
return if quirk_artifact?
|
||||||
|
|
||||||
|
modifier = skill_data['modifier']
|
||||||
|
strength = skill_data['strength']
|
||||||
|
skill_level = skill_data['level']
|
||||||
|
|
||||||
|
unless modifier && strength && skill_level
|
||||||
|
errors.add(slot_name, 'must have modifier, strength, and level')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
skill_def = ArtifactSkill.find_skill(group_number, modifier)
|
||||||
|
unless skill_def
|
||||||
|
errors.add(slot_name, "has invalid modifier #{modifier}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless (1..5).cover?(skill_level)
|
||||||
|
errors.add(slot_name, 'level must be between 1 and 5')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate strength is a valid base value for this skill
|
||||||
|
unless skill_def.valid_strength?(strength)
|
||||||
|
errors.add(slot_name, "has invalid base strength #{strength}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_skill1_group_i
|
||||||
|
validate_skill_in_group(skill1, 1, :skill1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_skill2_group_i
|
||||||
|
validate_skill_in_group(skill2, 1, :skill2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_skill3_group_ii
|
||||||
|
validate_skill_in_group(skill3, 2, :skill3)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_skill4_group_iii
|
||||||
|
validate_skill_in_group(skill4, 3, :skill4)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_duplicate_skills
|
||||||
|
return if quirk_artifact?
|
||||||
|
|
||||||
|
# Skills 1 and 2 are both from Group I and cannot have the same modifier
|
||||||
|
return if skill1.blank? || skill1 == {} || skill2.blank? || skill2 == {}
|
||||||
|
|
||||||
|
if skill1['modifier'] == skill2['modifier']
|
||||||
|
errors.add(:base, 'Skill 1 and Skill 2 cannot have the same modifier')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_skill_levels_sum
|
||||||
|
# For standard artifacts, skill levels must sum to (artifact_level + 3)
|
||||||
|
# At level 1: all skills level 1, sum = 4
|
||||||
|
# At level 5: skills sum = 8 (distributed among 4 skills)
|
||||||
|
return if level.nil?
|
||||||
|
|
||||||
|
skills = [skill1, skill2, skill3, skill4]
|
||||||
|
|
||||||
|
# Skip validation if any skill is empty (incomplete artifact)
|
||||||
|
return if skills.any? { |s| s.blank? || s == {} }
|
||||||
|
|
||||||
|
total = skills.sum { |s| s['level'].to_i }
|
||||||
|
expected = level + 3
|
||||||
|
|
||||||
|
return if total == expected
|
||||||
|
|
||||||
|
errors.add(:base, "Skill levels must sum to #{expected} for artifact level #{level}, got #{total}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_quirk_artifact_constraints
|
||||||
|
errors.add(:level, 'must be 1 for quirk artifacts') unless level == 1
|
||||||
|
|
||||||
|
# Quirk artifacts don't store skills
|
||||||
|
[skill1, skill2, skill3, skill4].each_with_index do |skill, idx|
|
||||||
|
next if skill.blank? || skill == {}
|
||||||
|
|
||||||
|
errors.add(:"skill#{idx + 1}", 'must be empty for quirk artifacts')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
95
app/models/grid_artifact.rb
Normal file
95
app/models/grid_artifact.rb
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class GridArtifact < ApplicationRecord
|
||||||
|
include ArtifactSkillValidations
|
||||||
|
|
||||||
|
# Associations
|
||||||
|
belongs_to :grid_character
|
||||||
|
belongs_to :artifact
|
||||||
|
|
||||||
|
has_one :party, through: :grid_character
|
||||||
|
has_one :character, through: :grid_character
|
||||||
|
|
||||||
|
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
||||||
|
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
||||||
|
enum :element, {
|
||||||
|
wind: 1,
|
||||||
|
fire: 2,
|
||||||
|
water: 3,
|
||||||
|
earth: 4,
|
||||||
|
dark: 5,
|
||||||
|
light: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proficiency enum - only used for quirk artifacts (game assigns random proficiency)
|
||||||
|
enum :proficiency, {
|
||||||
|
sabre: 1,
|
||||||
|
dagger: 2,
|
||||||
|
axe: 3,
|
||||||
|
spear: 4,
|
||||||
|
bow: 5,
|
||||||
|
staff: 6,
|
||||||
|
melee: 7,
|
||||||
|
harp: 8,
|
||||||
|
gun: 9,
|
||||||
|
katana: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validations
|
||||||
|
validates :element, presence: true
|
||||||
|
validates :level, presence: true, inclusion: { in: 1..5 }
|
||||||
|
validates :proficiency, presence: true, if: :quirk_artifact?
|
||||||
|
validates :proficiency, absence: true, unless: :quirk_artifact?
|
||||||
|
|
||||||
|
validate :validate_character_compatibility
|
||||||
|
|
||||||
|
# Amoeba configuration for party duplication
|
||||||
|
amoeba do
|
||||||
|
enable
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the effective proficiency - from instance for quirk, from artifact for standard
|
||||||
|
def effective_proficiency
|
||||||
|
quirk_artifact? ? proficiency : artifact&.proficiency
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def quirk_artifact?
|
||||||
|
artifact&.quirk?
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates that the artifact's element and proficiency match the character's requirements.
|
||||||
|
#
|
||||||
|
# - Element must match the character's element (unless character has variable element like Lyria)
|
||||||
|
# - Artifact's proficiency must match one of the character's proficiencies
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def validate_character_compatibility
|
||||||
|
return unless grid_character&.character && artifact
|
||||||
|
|
||||||
|
char = grid_character.character
|
||||||
|
|
||||||
|
# Check element compatibility
|
||||||
|
# Characters with element=0 (Null) can equip any element artifact (e.g., Lyria)
|
||||||
|
if char.element.present? && char.element != 0
|
||||||
|
char_element = GranblueEnums::ELEMENTS.key(char.element)&.to_s&.downcase
|
||||||
|
unless char_element == element
|
||||||
|
errors.add(:element, "must match character's element (#{char_element})")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check proficiency compatibility
|
||||||
|
# Use effective_proficiency to get the right value for both standard and quirk artifacts
|
||||||
|
eff_prof = effective_proficiency
|
||||||
|
return unless eff_prof # Skip if no proficiency available
|
||||||
|
|
||||||
|
prof_value = Artifact.proficiencies[eff_prof]
|
||||||
|
char_proficiencies = [char.proficiency1, char.proficiency2].compact
|
||||||
|
|
||||||
|
unless char_proficiencies.include?(prof_value)
|
||||||
|
errors.add(:artifact, "proficiency must match one of the character's proficiencies")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -24,6 +24,8 @@ class GridCharacter < ApplicationRecord
|
||||||
counter_cache: :characters_count,
|
counter_cache: :characters_count,
|
||||||
inverse_of: :characters
|
inverse_of: :characters
|
||||||
|
|
||||||
|
has_one :grid_artifact, dependent: :destroy
|
||||||
|
|
||||||
# Validations
|
# Validations
|
||||||
validates_presence_of :party
|
validates_presence_of :party
|
||||||
|
|
||||||
|
|
@ -42,6 +44,8 @@ class GridCharacter < ApplicationRecord
|
||||||
|
|
||||||
##### Amoeba configuration
|
##### Amoeba configuration
|
||||||
amoeba do
|
amoeba do
|
||||||
|
enable
|
||||||
|
include_association :grid_artifact
|
||||||
set ring1: { modifier: nil, strength: nil }
|
set ring1: { modifier: nil, strength: nil }
|
||||||
set ring2: { modifier: nil, strength: nil }
|
set ring2: { modifier: nil, strength: nil }
|
||||||
set ring3: { modifier: nil, strength: nil }
|
set ring3: { modifier: nil, strength: nil }
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class User < ApplicationRecord
|
||||||
has_many :collection_weapons, dependent: :destroy
|
has_many :collection_weapons, dependent: :destroy
|
||||||
has_many :collection_summons, dependent: :destroy
|
has_many :collection_summons, dependent: :destroy
|
||||||
has_many :collection_job_accessories, dependent: :destroy
|
has_many :collection_job_accessories, dependent: :destroy
|
||||||
|
has_many :collection_artifacts, dependent: :destroy
|
||||||
|
|
||||||
# Note: The crew association will be added when crews feature is implemented
|
# Note: The crew association will be added when crews feature is implemented
|
||||||
# belongs_to :crew, optional: true
|
# belongs_to :crew, optional: true
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue