hensei-api/docs/implementation/artifacts-feature-implementation.md

32 KiB

Artifacts Feature Implementation Guide

Overview

This document provides step-by-step instructions for implementing the Artifacts collection tracking feature. Artifacts are character equipment that users can record in their collection and equip to characters in parties.

Implementation Steps

Phase 1: Database Setup

1.1 Create Artifacts Tables Migration

rails generate migration CreateArtifactsSystem
# db/migrate/xxx_create_artifacts_system.rb
class CreateArtifactsSystem < ActiveRecord::Migration[8.0]
  def change
    # Canonical artifact data
    create_table :artifacts, id: :uuid do |t|
      t.string :name_en, null: false
      t.string :name_jp, null: false
      t.integer :series
      t.integer :weapon_specialty
      t.integer :rarity, null: false # 3=R, 4=SR, 5=SSR
      t.boolean :is_quirk, default: false
      t.integer :max_level, null: false # 150 for standard, 200 for quirk
      t.timestamps

      t.index :rarity
      t.index :is_quirk
      t.index :weapon_specialty
    end

    # Canonical skill data
    create_table :artifact_skills, id: :uuid do |t|
      t.string :name_en, null: false
      t.string :name_jp, null: false
      t.integer :skill_group, null: false # 1=Group I, 2=Group II, 3=Group III
      t.string :effect_type
      t.integer :max_level, null: false, default: 15
      t.text :description_en
      t.text :description_jp
      t.timestamps

      t.index :skill_group
      t.index :effect_type
    end
  end
end

1.2 Create Collection Artifacts Migration

rails generate migration CreateCollectionArtifacts
# db/migrate/xxx_create_collection_artifacts.rb
class CreateCollectionArtifacts < ActiveRecord::Migration[8.0]
  def change
    create_table :collection_artifacts, id: :uuid do |t|
      t.uuid :user_id, null: false
      t.uuid :artifact_id, null: false
      t.integer :level, null: false, default: 1

      # Skill slots
      t.uuid :skill1_id
      t.integer :skill1_level, default: 1
      t.uuid :skill2_id
      t.integer :skill2_level, default: 1
      t.uuid :skill3_id
      t.integer :skill3_level, default: 1
      t.uuid :skill4_id # Only for quirk artifacts
      t.integer :skill4_level, default: 1

      t.timestamps

      t.index :user_id
      t.index :artifact_id
      t.index [:user_id, :artifact_id]
    end

    add_foreign_key :collection_artifacts, :users
    add_foreign_key :collection_artifacts, :artifacts
    add_foreign_key :collection_artifacts, :artifact_skills, column: :skill1_id
    add_foreign_key :collection_artifacts, :artifact_skills, column: :skill2_id
    add_foreign_key :collection_artifacts, :artifact_skills, column: :skill3_id
    add_foreign_key :collection_artifacts, :artifact_skills, column: :skill4_id
  end
end

1.3 Create Grid Artifacts Migration

rails generate migration CreateGridArtifacts
# db/migrate/xxx_create_grid_artifacts.rb
class CreateGridArtifacts < ActiveRecord::Migration[8.0]
  def change
    create_table :grid_artifacts, id: :uuid do |t|
      t.uuid :party_id, null: false
      t.uuid :grid_character_id, null: false

      # Reference to collection
      t.uuid :collection_artifact_id

      # Quick-build fields (when not using collection)
      t.uuid :artifact_id
      t.integer :level, default: 1
      t.uuid :skill1_id
      t.integer :skill1_level, default: 1
      t.uuid :skill2_id
      t.integer :skill2_level, default: 1
      t.uuid :skill3_id
      t.integer :skill3_level, default: 1
      t.uuid :skill4_id
      t.integer :skill4_level, default: 1

      t.timestamps

      t.index :party_id
      t.index [:grid_character_id], unique: true
      t.index :collection_artifact_id
      t.index :artifact_id
    end

    add_foreign_key :grid_artifacts, :parties
    add_foreign_key :grid_artifacts, :grid_characters
    add_foreign_key :grid_artifacts, :collection_artifacts
    add_foreign_key :grid_artifacts, :artifacts
    add_foreign_key :grid_artifacts, :artifact_skills, column: :skill1_id
    add_foreign_key :grid_artifacts, :artifact_skills, column: :skill2_id
    add_foreign_key :grid_artifacts, :artifact_skills, column: :skill3_id
    add_foreign_key :grid_artifacts, :artifact_skills, column: :skill4_id
  end
end

Phase 2: Model Implementation

2.1 Artifact Model

# app/models/artifact.rb
class Artifact < ApplicationRecord
  # Associations
  has_many :collection_artifacts, dependent: :restrict_with_error
  has_many :grid_artifacts, dependent: :restrict_with_error

  # Validations
  validates :name_en, :name_jp, presence: true
  validates :rarity, inclusion: { in: 3..5 }
  validates :max_level, presence: true

  # Scopes
  scope :standard, -> { where(is_quirk: false) }
  scope :quirk, -> { where(is_quirk: true) }
  scope :by_rarity, ->(rarity) { where(rarity: rarity) }
  scope :by_weapon_specialty, ->(spec) { where(weapon_specialty: spec) }

  # Enums
  enum weapon_specialty: {
    sabre: 1,
    dagger: 2,
    spear: 3,
    axe: 4,
    staff: 5,
    gun: 6,
    melee: 7,
    bow: 8,
    harp: 9,
    katana: 10
  }

  enum series: {
    revans: 1,
    sephira: 2,
    arcarum: 3,
    providence: 4
  }

  # Methods
  def max_skill_slots
    is_quirk ? 4 : 3
  end
end

2.2 Artifact Skill Model

# app/models/artifact_skill.rb
class ArtifactSkill < ApplicationRecord
  # Constants
  GROUP_I = 1
  GROUP_II = 2
  GROUP_III = 3

  # Validations
  validates :name_en, :name_jp, presence: true
  validates :skill_group, inclusion: { in: [GROUP_I, GROUP_II, GROUP_III] }
  validates :max_level, presence: true

  # Scopes
  scope :group_i, -> { where(skill_group: GROUP_I) }
  scope :group_ii, -> { where(skill_group: GROUP_II) }
  scope :group_iii, -> { where(skill_group: GROUP_III) }

  # Methods
  def group_name
    case skill_group
    when GROUP_I then "Group I"
    when GROUP_II then "Group II"
    when GROUP_III then "Group III"
    end
  end
end

2.3 Collection Artifact Model

# app/models/collection_artifact.rb
class CollectionArtifact < ApplicationRecord
  # Associations
  belongs_to :user
  belongs_to :artifact
  belongs_to :skill1, class_name: 'ArtifactSkill', optional: true
  belongs_to :skill2, class_name: 'ArtifactSkill', optional: true
  belongs_to :skill3, class_name: 'ArtifactSkill', optional: true
  belongs_to :skill4, class_name: 'ArtifactSkill', optional: true

  has_one :grid_artifact, dependent: :nullify

  # Validations
  validates :level, numericality: {
    greater_than_or_equal_to: 1,
    less_than_or_equal_to: ->(ca) { ca.artifact&.max_level || 200 }
  }

  validates :skill1_level, :skill2_level, :skill3_level,
            numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 15 },
            allow_nil: true

  validates :skill4_level,
            numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 15 },
            allow_nil: true

  validate :validate_skill4_for_quirk_only
  validate :validate_skill_presence

  # Scopes
  scope :for_user, ->(user) { where(user: user) }
  scope :by_artifact, ->(artifact_id) { where(artifact_id: artifact_id) }

  # Methods
  def skills
    [skill1, skill2, skill3, skill4].compact
  end

  def skill_levels
    {
      skill1_id => skill1_level,
      skill2_id => skill2_level,
      skill3_id => skill3_level,
      skill4_id => skill4_level
    }.compact
  end

  private

  def validate_skill4_for_quirk_only
    if skill4_id.present? && artifact && !artifact.is_quirk
      errors.add(:skill4_id, "can only be set for quirk artifacts")
    end
  end

  def validate_skill_presence
    if artifact && artifact.is_quirk
      if [skill1_id, skill2_id, skill3_id, skill4_id].any?(&:blank?)
        errors.add(:base, "Quirk artifacts must have all 4 skills")
      end
    elsif artifact && !artifact.is_quirk
      if [skill1_id, skill2_id, skill3_id].any?(&:blank?)
        errors.add(:base, "Standard artifacts must have 3 skills")
      end
    end
  end
end

2.4 Grid Artifact Model

# app/models/grid_artifact.rb
class GridArtifact < ApplicationRecord
  # Associations
  belongs_to :party
  belongs_to :grid_character
  belongs_to :collection_artifact, optional: true
  belongs_to :artifact, optional: true
  belongs_to :skill1, class_name: 'ArtifactSkill', optional: true
  belongs_to :skill2, class_name: 'ArtifactSkill', optional: true
  belongs_to :skill3, class_name: 'ArtifactSkill', optional: true
  belongs_to :skill4, class_name: 'ArtifactSkill', optional: true

  # Validations
  validates :grid_character_id, uniqueness: true
  validate :validate_artifact_source
  validate :validate_party_ownership

  # Callbacks
  before_validation :sync_from_collection, if: :from_collection?

  # Methods
  def from_collection?
    collection_artifact_id.present?
  end

  def artifact_details
    if from_collection?
      collection_artifact.artifact
    else
      artifact
    end
  end

  def skills
    if from_collection?
      collection_artifact.skills
    else
      [skill1, skill2, skill3, skill4].compact
    end
  end

  def skill_levels
    if from_collection?
      collection_artifact.skill_levels
    else
      {
        skill1_id => skill1_level,
        skill2_id => skill2_level,
        skill3_id => skill3_level,
        skill4_id => skill4_level
      }.compact
    end
  end

  private

  def sync_from_collection
    return unless collection_artifact

    self.artifact_id = collection_artifact.artifact_id
    self.level = collection_artifact.level
    self.skill1_id = collection_artifact.skill1_id
    self.skill1_level = collection_artifact.skill1_level
    self.skill2_id = collection_artifact.skill2_id
    self.skill2_level = collection_artifact.skill2_level
    self.skill3_id = collection_artifact.skill3_id
    self.skill3_level = collection_artifact.skill3_level
    self.skill4_id = collection_artifact.skill4_id
    self.skill4_level = collection_artifact.skill4_level
  end

  def validate_artifact_source
    if collection_artifact_id.blank? && artifact_id.blank?
      errors.add(:base, "Must specify either collection artifact or quick-build artifact")
    end
  end

  def validate_party_ownership
    if grid_character && grid_character.party_id != party_id
      errors.add(:grid_character, "must belong to the same party")
    end
  end
end

2.5 Model Updates

# app/models/user.rb (additions)
class User < ApplicationRecord
  # ... existing code ...

  has_many :collection_artifacts, dependent: :destroy
end

# app/models/grid_character.rb (additions)
class GridCharacter < ApplicationRecord
  # ... existing code ...

  has_one :grid_artifact, dependent: :destroy

  def has_artifact?
    grid_artifact.present?
  end
end

# app/models/party.rb (additions)
class Party < ApplicationRecord
  # ... existing code ...

  has_many :grid_artifacts, dependent: :destroy
end

Phase 3: API Implementation

3.1 Collection Artifacts Controller

# app/controllers/api/v1/collection/artifacts_controller.rb
module Api
  module V1
    module Collection
      class ArtifactsController < ApplicationController
        before_action :authenticate_user!
        before_action :set_collection_artifact, only: [:show, :update, :destroy]

        # GET /api/v1/collection/artifacts
        def index
          artifacts = current_user.collection_artifacts
                                  .includes(:artifact, :skill1, :skill2, :skill3, :skill4)

          artifacts = artifacts.by_artifact(params[:artifact_id]) if params[:artifact_id].present?

          render json: CollectionArtifactBlueprint.render(
            artifacts.page(params[:page]).per(params[:per_page] || 50),
            root: :collection_artifacts,
            meta: pagination_meta(artifacts)
          )
        end

        # GET /api/v1/collection/artifacts/:id
        def show
          render json: CollectionArtifactBlueprint.render(
            @collection_artifact,
            root: :collection_artifact,
            view: :extended
          )
        end

        # POST /api/v1/collection/artifacts
        def create
          @collection_artifact = current_user.collection_artifacts.build(collection_artifact_params)

          if @collection_artifact.save
            render json: CollectionArtifactBlueprint.render(
              @collection_artifact,
              root: :collection_artifact
            ), status: :created
          else
            render json: { errors: @collection_artifact.errors }, status: :unprocessable_entity
          end
        end

        # PUT /api/v1/collection/artifacts/:id
        def update
          if @collection_artifact.update(collection_artifact_params)
            render json: CollectionArtifactBlueprint.render(
              @collection_artifact,
              root: :collection_artifact
            )
          else
            render json: { errors: @collection_artifact.errors }, status: :unprocessable_entity
          end
        end

        # DELETE /api/v1/collection/artifacts/:id
        def destroy
          if @collection_artifact.destroy
            head :no_content
          else
            render json: { errors: @collection_artifact.errors }, status: :unprocessable_entity
          end
        end

        # GET /api/v1/collection/statistics
        def statistics
          stats = {
            total_artifacts: current_user.collection_artifacts.count,
            breakdown_by_rarity: current_user.collection_artifacts
              .joins(:artifact)
              .group('artifacts.rarity')
              .count,
            breakdown_by_level: current_user.collection_artifacts
              .group(:level)
              .count
          }

          render json: stats
        end

        private

        def set_collection_artifact
          @collection_artifact = current_user.collection_artifacts.find(params[:id])
        end

        def collection_artifact_params
          params.require(:collection_artifact).permit(
            :artifact_id, :level,
            :skill1_id, :skill1_level,
            :skill2_id, :skill2_level,
            :skill3_id, :skill3_level,
            :skill4_id, :skill4_level
          )
        end
      end
    end
  end
end

3.2 Grid Artifacts Controller

# app/controllers/api/v1/grid_artifacts_controller.rb
module Api
  module V1
    class GridArtifactsController < ApplicationController
      before_action :authenticate_user!
      before_action :set_party
      before_action :authorize_party_edit!
      before_action :set_grid_artifact, only: [:show, :update, :destroy]

      # GET /api/v1/parties/:party_id/grid_artifacts
      def index
        artifacts = @party.grid_artifacts
                          .includes(:grid_character, :artifact, :collection_artifact,
                                    :skill1, :skill2, :skill3, :skill4)

        render json: GridArtifactBlueprint.render(
          artifacts,
          root: :grid_artifacts
        )
      end

      # GET /api/v1/parties/:party_id/grid_artifacts/:id
      def show
        render json: GridArtifactBlueprint.render(
          @grid_artifact,
          root: :grid_artifact,
          view: :extended
        )
      end

      # POST /api/v1/parties/:party_id/grid_artifacts
      def create
        @grid_artifact = @party.grid_artifacts.build(grid_artifact_params)

        if @grid_artifact.save
          render json: GridArtifactBlueprint.render(
            @grid_artifact,
            root: :grid_artifact
          ), status: :created
        else
          render json: { errors: @grid_artifact.errors }, status: :unprocessable_entity
        end
      end

      # PUT /api/v1/parties/:party_id/grid_artifacts/:id
      def update
        if @grid_artifact.update(grid_artifact_params)
          render json: GridArtifactBlueprint.render(
            @grid_artifact,
            root: :grid_artifact
          )
        else
          render json: { errors: @grid_artifact.errors }, status: :unprocessable_entity
        end
      end

      # DELETE /api/v1/parties/:party_id/grid_artifacts/:id
      def destroy
        if @grid_artifact.destroy
          head :no_content
        else
          render json: { errors: @grid_artifact.errors }, status: :unprocessable_entity
        end
      end

      private

      def set_party
        @party = current_user.parties.find(params[:party_id])
      end

      def set_grid_artifact
        @grid_artifact = @party.grid_artifacts.find(params[:id])
      end

      def authorize_party_edit!
        unless @party.user == current_user
          render json: { error: "Not authorized" }, status: :forbidden
        end
      end

      def grid_artifact_params
        params.require(:grid_artifact).permit(
          :grid_character_id, :collection_artifact_id, :artifact_id, :level,
          :skill1_id, :skill1_level,
          :skill2_id, :skill2_level,
          :skill3_id, :skill3_level,
          :skill4_id, :skill4_level
        )
      end
    end
  end
end

3.3 Artifacts Controller (Canonical Data)

# app/controllers/api/v1/artifacts_controller.rb
module Api
  module V1
    class ArtifactsController < ApplicationController
      before_action :set_artifact, only: [:show]

      # GET /api/v1/artifacts
      def index
        artifacts = Artifact.all
        artifacts = artifacts.quirk if params[:is_quirk] == 'true'
        artifacts = artifacts.standard if params[:is_quirk] == 'false'
        artifacts = artifacts.by_weapon_specialty(params[:weapon_specialty]) if params[:weapon_specialty].present?

        render json: ArtifactBlueprint.render(
          artifacts.page(params[:page]).per(params[:per_page] || 50),
          root: :artifacts,
          meta: pagination_meta(artifacts)
        )
      end

      # GET /api/v1/artifacts/:id
      def show
        render json: ArtifactBlueprint.render(
          @artifact,
          root: :artifact
        )
      end

      private

      def set_artifact
        @artifact = Artifact.find(params[:id])
      end
    end
  end
end

# app/controllers/api/v1/artifact_skills_controller.rb
module Api
  module V1
    class ArtifactSkillsController < ApplicationController
      before_action :set_artifact_skill, only: [:show]

      # GET /api/v1/artifact_skills
      def index
        skills = ArtifactSkill.all
        skills = skills.where(skill_group: params[:skill_group]) if params[:skill_group].present?

        render json: ArtifactSkillBlueprint.render(
          skills.page(params[:page]).per(params[:per_page] || 50),
          root: :artifact_skills,
          meta: pagination_meta(skills)
        )
      end

      # GET /api/v1/artifact_skills/:id
      def show
        render json: ArtifactSkillBlueprint.render(
          @artifact_skill,
          root: :artifact_skill
        )
      end

      private

      def set_artifact_skill
        @artifact_skill = ArtifactSkill.find(params[:id])
      end
    end
  end
end

3.4 User Collections Controller

# app/controllers/api/v1/users/collection/artifacts_controller.rb
module Api
  module V1
    module Users
      module Collection
        class ArtifactsController < ApplicationController
          before_action :set_user

          # GET /api/v1/users/:user_id/collection/artifacts
          def index
            unless can_view_collection?(@user)
              render json: { error: "You do not have permission to view this collection" },
                     status: :forbidden
              return
            end

            artifacts = @user.collection_artifacts
                             .includes(:artifact, :skill1, :skill2, :skill3, :skill4)

            render json: CollectionArtifactBlueprint.render(
              artifacts.page(params[:page]).per(params[:per_page] || 50),
              root: :collection_artifacts,
              meta: pagination_meta(artifacts)
            )
          end

          private

          def set_user
            @user = User.find(params[:user_id])
          end

          def can_view_collection?(user)
            case user.collection_privacy
            when 'public'
              true
            when 'crew_only'
              # Check if viewer is in same crew (when crew feature is implemented)
              current_user && current_user.crew_id == user.crew_id
            when 'private'
              current_user && current_user.id == user.id
            else
              false
            end
          end
        end
      end
    end
  end
end

Phase 4: Blueprints

# app/blueprints/artifact_blueprint.rb
class ArtifactBlueprint < ApplicationBlueprint
  identifier :id

  fields :name_en, :name_jp, :series, :weapon_specialty, :rarity, :is_quirk, :max_level

  field :max_skill_slots do |artifact|
    artifact.max_skill_slots
  end
end

# app/blueprints/artifact_skill_blueprint.rb
class ArtifactSkillBlueprint < ApplicationBlueprint
  identifier :id

  fields :name_en, :name_jp, :skill_group, :effect_type, :max_level,
         :description_en, :description_jp

  field :group_name do |skill|
    skill.group_name
  end
end

# app/blueprints/collection_artifact_blueprint.rb
class CollectionArtifactBlueprint < ApplicationBlueprint
  identifier :id

  fields :level, :created_at, :updated_at

  association :artifact, blueprint: ArtifactBlueprint

  field :skills do |ca|
    skills = []

    if ca.skill1
      skills << {
        slot: 1,
        skill: ArtifactSkillBlueprint.render_as_hash(ca.skill1),
        level: ca.skill1_level
      }
    end

    if ca.skill2
      skills << {
        slot: 2,
        skill: ArtifactSkillBlueprint.render_as_hash(ca.skill2),
        level: ca.skill2_level
      }
    end

    if ca.skill3
      skills << {
        slot: 3,
        skill: ArtifactSkillBlueprint.render_as_hash(ca.skill3),
        level: ca.skill3_level
      }
    end

    if ca.skill4
      skills << {
        slot: 4,
        skill: ArtifactSkillBlueprint.render_as_hash(ca.skill4),
        level: ca.skill4_level
      }
    end

    skills
  end

  view :extended do
    field :equipped_in_parties do |ca|
      ca.grid_artifact&.party_id
    end
  end
end

# app/blueprints/grid_artifact_blueprint.rb
class GridArtifactBlueprint < ApplicationBlueprint
  identifier :id

  field :from_collection do |ga|
    ga.from_collection?
  end

  field :level do |ga|
    ga.from_collection? ? ga.collection_artifact.level : ga.level
  end

  association :grid_character, blueprint: GridCharacterBlueprint

  field :artifact do |ga|
    ArtifactBlueprint.render_as_hash(ga.artifact_details)
  end

  field :skills do |ga|
    skills = []
    skill_levels = ga.skill_levels

    ga.skills.each_with_index do |skill, index|
      next unless skill
      skills << {
        slot: index + 1,
        skill: ArtifactSkillBlueprint.render_as_hash(skill),
        level: skill_levels[skill.id] || 1
      }
    end

    skills
  end

  view :extended do
    association :collection_artifact, blueprint: CollectionArtifactBlueprint,
                if: ->(ga) { ga.from_collection? }
  end
end

Phase 5: Routes Configuration

# config/routes.rb (additions)
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      # Canonical artifact data
      resources :artifacts, only: [:index, :show]
      resources :artifact_skills, only: [:index, :show]

      # User collection
      namespace :collection do
        resources :artifacts do
          collection do
            get 'statistics'
          end
        end
      end

      # Grid artifacts (nested under parties)
      resources :parties do
        resources :grid_artifacts
      end

      # View other users' collections
      resources :users, only: [] do
        namespace :collection do
          resources :artifacts, only: [:index]
        end
      end
    end
  end
end

Phase 6: Seed Data

# db/seeds/artifacts.rb

# Create artifact skills
puts "Creating artifact skills..."

# Group I Skills
group_i_skills = [
  { name_en: "ATK Up", name_jp: "攻撃力アップ", skill_group: 1, effect_type: "atk", max_level: 15 },
  { name_en: "HP Up", name_jp: "HPアップ", skill_group: 1, effect_type: "hp", max_level: 15 },
  { name_en: "Critical Hit Rate", name_jp: "クリティカル確率", skill_group: 1, effect_type: "crit", max_level: 15 },
  { name_en: "Double Attack Rate", name_jp: "連続攻撃確率", skill_group: 1, effect_type: "da", max_level: 15 },
  { name_en: "Triple Attack Rate", name_jp: "トリプルアタック確率", skill_group: 1, effect_type: "ta", max_level: 15 }
]

# Group II Skills
group_ii_skills = [
  { name_en: "Enmity", name_jp: "背水", skill_group: 2, effect_type: "enmity", max_level: 10 },
  { name_en: "Stamina", name_jp: "渾身", skill_group: 2, effect_type: "stamina", max_level: 10 },
  { name_en: "Charge Bar Gain", name_jp: "奥義ゲージ上昇量", skill_group: 2, effect_type: "charge", max_level: 10 }
]

# Group III Skills
group_iii_skills = [
  { name_en: "Skill DMG Cap Up", name_jp: "アビリティダメージ上限", skill_group: 3, effect_type: "skill_cap", max_level: 5 },
  { name_en: "C.A. DMG Cap Up", name_jp: "奥義ダメージ上限", skill_group: 3, effect_type: "ca_cap", max_level: 5 },
  { name_en: "Normal Attack Cap Up", name_jp: "通常攻撃上限", skill_group: 3, effect_type: "auto_cap", max_level: 5 }
]

(group_i_skills + group_ii_skills + group_iii_skills).each do |skill_data|
  ArtifactSkill.find_or_create_by!(
    name_en: skill_data[:name_en]
  ) do |skill|
    skill.assign_attributes(skill_data)
  end
end

# Create artifacts
puts "Creating artifacts..."

standard_artifacts = [
  { name_en: "Revans Gauntlet", name_jp: "レヴァンスガントレット", series: 1, weapon_specialty: 7, rarity: 5, is_quirk: false, max_level: 150 },
  { name_en: "Revans Armor", name_jp: "レヴァンスアーマー", series: 1, weapon_specialty: 1, rarity: 5, is_quirk: false, max_level: 150 },
  { name_en: "Sephira Ring", name_jp: "セフィラリング", series: 2, weapon_specialty: 5, rarity: 5, is_quirk: false, max_level: 150 },
  { name_en: "Arcarum Card", name_jp: "アーカルムカード", series: 3, weapon_specialty: 2, rarity: 4, is_quirk: false, max_level: 150 }
]

quirk_artifacts = [
  { name_en: "Quirk: Crimson Finger", name_jp: "絆器:クリムゾンフィンガー", series: 4, weapon_specialty: 6, rarity: 5, is_quirk: true, max_level: 200 },
  { name_en: "Quirk: Blue Sphere", name_jp: "絆器:ブルースフィア", series: 4, weapon_specialty: 5, rarity: 5, is_quirk: true, max_level: 200 }
]

(standard_artifacts + quirk_artifacts).each do |artifact_data|
  Artifact.find_or_create_by!(
    name_en: artifact_data[:name_en]
  ) do |artifact|
    artifact.assign_attributes(artifact_data)
  end
end

puts "Seeded #{ArtifactSkill.count} artifact skills and #{Artifact.count} artifacts"

Phase 7: Testing

7.1 Model Specs

# spec/models/artifact_spec.rb
require 'rails_helper'

RSpec.describe Artifact, type: :model do
  describe 'validations' do
    it { should validate_presence_of(:name_en) }
    it { should validate_presence_of(:name_jp) }
    it { should validate_inclusion_of(:rarity).in_array([3, 4, 5]) }
  end

  describe 'associations' do
    it { should have_many(:collection_artifacts) }
    it { should have_many(:grid_artifacts) }
  end

  describe '#max_skill_slots' do
    it 'returns 3 for standard artifacts' do
      artifact = build(:artifact, is_quirk: false)
      expect(artifact.max_skill_slots).to eq(3)
    end

    it 'returns 4 for quirk artifacts' do
      artifact = build(:artifact, is_quirk: true)
      expect(artifact.max_skill_slots).to eq(4)
    end
  end
end

# spec/models/collection_artifact_spec.rb
require 'rails_helper'

RSpec.describe CollectionArtifact, type: :model do
  let(:user) { create(:user) }
  let(:standard_artifact) { create(:artifact, is_quirk: false) }
  let(:quirk_artifact) { create(:artifact, is_quirk: true) }

  describe 'validations' do
    it 'validates level is within artifact max level' do
      ca = build(:collection_artifact, artifact: standard_artifact, level: 151)
      expect(ca).not_to be_valid
      expect(ca.errors[:level]).to be_present
    end

    it 'prevents skill4 on standard artifacts' do
      ca = build(:collection_artifact,
        artifact: standard_artifact,
        skill4: create(:artifact_skill)
      )
      expect(ca).not_to be_valid
      expect(ca.errors[:skill4_id]).to include("can only be set for quirk artifacts")
    end

    it 'allows skill4 on quirk artifacts' do
      ca = build(:collection_artifact,
        artifact: quirk_artifact,
        skill1: create(:artifact_skill, skill_group: 1),
        skill2: create(:artifact_skill, skill_group: 1),
        skill3: create(:artifact_skill, skill_group: 2),
        skill4: create(:artifact_skill, skill_group: 3)
      )
      expect(ca).to be_valid
    end
  end
end

# spec/models/grid_artifact_spec.rb
require 'rails_helper'

RSpec.describe GridArtifact, type: :model do
  let(:party) { create(:party) }
  let(:grid_character) { create(:grid_character, party: party) }
  let(:collection_artifact) { create(:collection_artifact) }

  describe 'validations' do
    it 'ensures one artifact per character' do
      create(:grid_artifact, party: party, grid_character: grid_character)
      duplicate = build(:grid_artifact, party: party, grid_character: grid_character)

      expect(duplicate).not_to be_valid
      expect(duplicate.errors[:grid_character_id]).to be_present
    end

    it 'requires either collection or quick-build artifact' do
      ga = build(:grid_artifact, party: party, grid_character: grid_character)
      expect(ga).not_to be_valid
      expect(ga.errors[:base]).to include("Must specify either collection artifact or quick-build artifact")
    end
  end

  describe '#from_collection?' do
    it 'returns true when using collection artifact' do
      ga = build(:grid_artifact, collection_artifact: collection_artifact)
      expect(ga.from_collection?).to be true
    end

    it 'returns false when quick-building' do
      ga = build(:grid_artifact, artifact: create(:artifact))
      expect(ga.from_collection?).to be false
    end
  end
end

7.2 Controller Specs

# spec/controllers/api/v1/collection/artifacts_controller_spec.rb
require 'rails_helper'

RSpec.describe Api::V1::Collection::ArtifactsController, type: :controller do
  let(:user) { create(:user) }
  let(:artifact) { create(:artifact) }
  let(:skill1) { create(:artifact_skill, skill_group: 1) }
  let(:skill2) { create(:artifact_skill, skill_group: 1) }
  let(:skill3) { create(:artifact_skill, skill_group: 2) }

  before { sign_in user }

  describe 'GET #index' do
    let!(:collection_artifacts) { create_list(:collection_artifact, 3, user: user) }

    it 'returns user collection artifacts' do
      get :index
      expect(response).to have_http_status(:success)
      json = JSON.parse(response.body)
      expect(json['collection_artifacts'].size).to eq(3)
    end
  end

  describe 'POST #create' do
    let(:valid_params) do
      {
        collection_artifact: {
          artifact_id: artifact.id,
          level: 50,
          skill1_id: skill1.id,
          skill1_level: 5,
          skill2_id: skill2.id,
          skill2_level: 3,
          skill3_id: skill3.id,
          skill3_level: 2
        }
      }
    end

    it 'creates a new collection artifact' do
      expect {
        post :create, params: valid_params
      }.to change(CollectionArtifact, :count).by(1)

      expect(response).to have_http_status(:created)
    end
  end

  describe 'DELETE #destroy' do
    let!(:collection_artifact) { create(:collection_artifact, user: user) }

    it 'deletes the artifact' do
      expect {
        delete :destroy, params: { id: collection_artifact.id }
      }.to change(CollectionArtifact, :count).by(-1)

      expect(response).to have_http_status(:no_content)
    end
  end
end

Deployment Checklist

Pre-deployment

  • Run all migrations in development
  • Seed artifact and skill data
  • Test all CRUD operations
  • Verify privacy controls work correctly

Deployment

  1. Deploy database migrations
  2. Run seed data for artifacts and skills
  3. Deploy application code
  4. Verify artifact endpoints
  5. Test collection and grid functionality

Post-deployment

  • Monitor error rates
  • Check database performance
  • Verify user collections are accessible
  • Test party integration

Performance Considerations

  1. Database Indexes: All foreign keys and common query patterns are indexed
  2. Eager Loading: Use includes() to prevent N+1 queries
  3. Pagination: All list endpoints support pagination

Security Notes

  1. Authorization: Users can only modify their own collection
  2. Privacy: Collection viewing respects user privacy settings
  3. Validation: Strict validation at model level
  4. Party Ownership: Only party owners can modify grid artifacts