32 KiB
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
- Deploy database migrations
- Run seed data for artifacts and skills
- Deploy application code
- Verify artifact endpoints
- Test collection and grid functionality
Post-deployment
- Monitor error rates
- Check database performance
- Verify user collections are accessible
- Test party integration
Performance Considerations
- Database Indexes: All foreign keys and common query patterns are indexed
- Eager Loading: Use includes() to prevent N+1 queries
- Pagination: All list endpoints support pagination
Security Notes
- Authorization: Users can only modify their own collection
- Privacy: Collection viewing respects user privacy settings
- Validation: Strict validation at model level
- Party Ownership: Only party owners can modify grid artifacts