1179 lines
No EOL
32 KiB
Markdown
1179 lines
No EOL
32 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
rails generate migration CreateArtifactsSystem
|
|
```
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```bash
|
|
rails generate migration CreateCollectionArtifacts
|
|
```
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```bash
|
|
rails generate migration CreateGridArtifacts
|
|
```
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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)
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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 |