275 lines
8.9 KiB
Ruby
275 lines
8.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
##
|
|
# Service for importing artifacts from game JSON data.
|
|
# Parses the game's skill_id format and converts to our (group, modifier) format.
|
|
#
|
|
# @example Import artifacts for a user
|
|
# service = ArtifactImportService.new(user, game_data)
|
|
# result = service.import
|
|
# if result.success?
|
|
# puts "Imported #{result.created_count} artifacts"
|
|
# end
|
|
#
|
|
class ArtifactImportService
|
|
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
|
|
|
|
# Mapping from game skill base ID (skill_id / 10) to [group, modifier]
|
|
# Built from analyzing game data samples and verified against artifact_skills.json
|
|
GAME_SKILL_MAPPING = {
|
|
# === GROUP I (Lines 1-2) ===
|
|
1001 => [1, 1], # ATK
|
|
2001 => [1, 2], # HP
|
|
3001 => [1, 6], # Critical Hit Rate
|
|
3002 => [1, 3], # C.A. DMG
|
|
3003 => [1, 4], # Skill DMG
|
|
3004 => [1, 13], # Debuff Success Rate
|
|
3005 => [1, 7], # Double Attack Rate
|
|
3006 => [1, 8], # Triple Attack Rate
|
|
3007 => [1, 9], # DEF
|
|
3008 => [1, 14], # Debuff Resistance
|
|
3009 => [1, 11], # Dodge Rate
|
|
3010 => [1, 12], # Healing
|
|
3011 => [1, 5], # Elemental ATK
|
|
3012 => [1, 10], # Superior Element Reduction
|
|
|
|
# === GROUP II (Line 3) ===
|
|
3013 => [2, 1], # N.A. DMG Cap
|
|
3014 => [2, 2], # Skill DMG Cap
|
|
3015 => [2, 3], # C.A. DMG Cap
|
|
3016 => [2, 9], # Supplemental N.A. DMG
|
|
3017 => [2, 10], # Supplemental Skill DMG
|
|
3018 => [2, 11], # Supplemental C.A. DMG
|
|
3019 => [2, 4], # Special C.A. DMG Cap
|
|
3020 => [2, 6], # N.A. DMG cap boost tradeoff
|
|
3021 => [2, 18], # Turn-Based DMG Reduction
|
|
3022 => [2, 17], # Regeneration
|
|
3023 => [2, 14], # Amplify DMG at 100% HP
|
|
3024 => [2, 13], # Boost TA at 50%+ HP
|
|
3025 => [2, 16], # DMG reduction when at or below 50% HP
|
|
3026 => [2, 5], # Boost DMG cap for critical hits
|
|
3027 => [2, 15], # Max HP boost for a 70% hit to DEF
|
|
3028 => [2, 12], # Chain DMG Amplify
|
|
3029 => [2, 7], # Skill DMG cap boost tradeoff
|
|
3030 => [2, 8], # C.A. DMG cap boost tradeoff
|
|
4000 => [2, 19], # Chance to remove 1 debuff before attacking
|
|
4001 => [2, 20], # Chance to cancel incoming dispels
|
|
|
|
# === GROUP III (Line 4) ===
|
|
3031 => [3, 28], # Boost item drop rate
|
|
3032 => [3, 27], # Boost EXP earned
|
|
5001 => [3, 2], # At battle start: Gain x random buff(s)
|
|
5002 => [3, 8], # Upon using a debuff skill: Amplify foe's DMG taken
|
|
5003 => [3, 9], # Upon using a healing skill: Ally bonus
|
|
5004 => [3, 12], # Cut linked skill cooldowns
|
|
5005 => [3, 10], # Gain 1% DMG Cap Up (Stackable)
|
|
5006 => [3, 11], # After using a skill with a cooldown of 10+ turns
|
|
5007 => [3, 22], # Gain Supplemental Skill DMG (Stackable)
|
|
5008 => [3, 13], # Gain Supplemental DMG based on charge bar spent
|
|
5009 => [3, 20], # Gain Flurry (3-hit)
|
|
5010 => [3, 21], # Plain DMG based on HP lost
|
|
5011 => [3, 23], # Upon single attacks: Gain random buff
|
|
5012 => [3, 25], # When foe has 3 or fewer debuffs: Armored
|
|
5013 => [3, 6], # When foe HP at 50% or lower: Restore HP
|
|
5014 => [3, 26], # When a sub ally: random debuff to foes
|
|
5015 => [3, 19], # Gain 20% Bonus DMG after being targeted
|
|
5016 => [3, 14], # At end of turn if didn't attack: Gain buff
|
|
5017 => [3, 24], # Upon using potion: Boost FC bar
|
|
5018 => [3, 3], # Start with 20% HP consumed / Cap Up
|
|
5019 => [3, 4], # When knocked out: All allies gain buffs
|
|
5020 => [3, 5], # When switching to main ally: Amplify DMG
|
|
5021 => [3, 1], # At battle start: Gain DMG Mitigation
|
|
5022 => [3, 18], # Chance to gain Flurry (6-hit)
|
|
5023 => [3, 17], # At battle start and every 5 turns: Shield
|
|
5024 => [3, 16], # Chance of turn progressing by 5
|
|
5025 => [3, 7], # Upon first-slot skill: Cut CD
|
|
5026 => [3, 15], # Chance to remove all buffs from foe
|
|
5029 => [3, 29] # May find earrings
|
|
}.freeze
|
|
|
|
# Game element values to our element enum values
|
|
# Game: 1=Fire, 2=Water, 3=Earth, 4=Wind, 5=Light, 6=Dark
|
|
# Ours: wind=1, fire=2, water=3, earth=4, dark=5, light=6
|
|
ELEMENT_MAPPING = {
|
|
'1' => 2, # Fire
|
|
'2' => 3, # Water
|
|
'3' => 4, # Earth
|
|
'4' => 1, # Wind
|
|
'5' => 6, # Light
|
|
'6' => 5 # Dark
|
|
}.freeze
|
|
|
|
def initialize(user, game_data, options = {})
|
|
@user = user
|
|
@game_data = game_data
|
|
@update_existing = options[:update_existing] || false
|
|
@created = []
|
|
@updated = []
|
|
@skipped = []
|
|
@errors = []
|
|
end
|
|
|
|
##
|
|
# Imports artifacts from game data.
|
|
#
|
|
# @return [Result] Import result with counts and errors
|
|
def import
|
|
items = extract_items
|
|
return Result.new(success?: false, errors: ['No artifact items found in data']) if items.empty?
|
|
|
|
ActiveRecord::Base.transaction do
|
|
items.each_with_index do |item, index|
|
|
import_item(item, index)
|
|
rescue StandardError => e
|
|
@errors << { index: index, game_id: item['id'], error: e.message }
|
|
end
|
|
end
|
|
|
|
Result.new(
|
|
success?: @errors.empty?,
|
|
created: @created,
|
|
updated: @updated,
|
|
skipped: @skipped,
|
|
errors: @errors
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
def extract_items
|
|
return @game_data if @game_data.is_a?(Array)
|
|
return @game_data['list'] if @game_data.is_a?(Hash) && @game_data['list'].is_a?(Array)
|
|
|
|
[]
|
|
end
|
|
|
|
def import_item(item, _index)
|
|
artifact = find_artifact(item['artifact_id'])
|
|
unless artifact
|
|
@errors << { game_id: item['id'], artifact_id: item['artifact_id'], error: 'Artifact not found' }
|
|
return
|
|
end
|
|
|
|
# Check for existing collection artifact with same game ID
|
|
existing = @user.collection_artifacts.find_by(game_id: item['id'].to_s)
|
|
|
|
if existing
|
|
if @update_existing
|
|
update_existing_artifact(existing, item, artifact)
|
|
else
|
|
@skipped << { game_id: item['id'], reason: 'Already exists' }
|
|
end
|
|
return
|
|
end
|
|
|
|
create_collection_artifact(item, artifact)
|
|
end
|
|
|
|
def find_artifact(artifact_id)
|
|
Artifact.find_by(granblue_id: artifact_id.to_s)
|
|
end
|
|
|
|
def create_collection_artifact(item, artifact)
|
|
attrs = build_collection_artifact_attrs(item, artifact)
|
|
|
|
collection_artifact = @user.collection_artifacts.build(attrs)
|
|
|
|
if collection_artifact.save
|
|
@created << collection_artifact
|
|
else
|
|
@errors << {
|
|
game_id: item['id'],
|
|
artifact_id: item['artifact_id'],
|
|
error: collection_artifact.errors.full_messages.join(', ')
|
|
}
|
|
end
|
|
end
|
|
|
|
def update_existing_artifact(existing, item, artifact)
|
|
attrs = build_collection_artifact_attrs(item, artifact)
|
|
|
|
if existing.update(attrs)
|
|
@updated << existing
|
|
else
|
|
@errors << {
|
|
game_id: item['id'],
|
|
artifact_id: item['artifact_id'],
|
|
error: existing.errors.full_messages.join(', ')
|
|
}
|
|
end
|
|
end
|
|
|
|
def build_collection_artifact_attrs(item, artifact)
|
|
{
|
|
artifact: artifact,
|
|
game_id: item['id'].to_s,
|
|
element: map_element(item['attribute']),
|
|
proficiency: artifact.quirk? ? map_proficiency(item['kind']) : nil,
|
|
level: item['level'].to_i,
|
|
skill1: parse_skill(item['skill1_info']),
|
|
skill2: parse_skill(item['skill2_info']),
|
|
skill3: parse_skill(item['skill3_info']),
|
|
skill4: parse_skill(item['skill4_info'])
|
|
}
|
|
end
|
|
|
|
def map_element(game_element)
|
|
ELEMENT_MAPPING[game_element.to_s]
|
|
end
|
|
|
|
def map_proficiency(kind)
|
|
# Game 'kind' field maps directly to proficiency enum (1-10)
|
|
kind.to_i
|
|
end
|
|
|
|
def parse_skill(skill_info)
|
|
return {} if skill_info.blank?
|
|
|
|
skill_id = skill_info['skill_id']
|
|
quality = skill_info['skill_quality'] || skill_info['level']
|
|
level = skill_info['level'] || 1
|
|
|
|
group, modifier = decode_skill_id(skill_id)
|
|
return {} unless group && modifier
|
|
|
|
# Get the strength value from ArtifactSkill
|
|
strength = calculate_strength(group, modifier, quality.to_i)
|
|
|
|
{
|
|
'modifier' => modifier,
|
|
'strength' => strength,
|
|
'level' => level.to_i
|
|
}
|
|
end
|
|
|
|
##
|
|
# Decodes a game skill_id to [group, modifier].
|
|
# skill_id format: {base_id}{quality} where last digit is quality (1-5)
|
|
#
|
|
# @param skill_id [Integer] The game's skill ID
|
|
# @return [Array<Integer, Integer>, nil] [group, modifier] or nil if unknown
|
|
def decode_skill_id(skill_id)
|
|
base_id = skill_id.to_i / 10
|
|
GAME_SKILL_MAPPING[base_id]
|
|
end
|
|
|
|
##
|
|
# Calculates the strength value based on quality tier.
|
|
# Quality 1-5 maps to base_values[0-4] in ArtifactSkill.
|
|
#
|
|
# @param group [Integer] Skill group (1, 2, or 3)
|
|
# @param modifier [Integer] Skill modifier within group
|
|
# @param quality [Integer] Quality tier (1-5)
|
|
# @return [Float, Integer, nil] The strength value
|
|
def calculate_strength(group, modifier, quality)
|
|
skill = ArtifactSkill.find_skill(group, modifier)
|
|
return nil unless skill
|
|
|
|
base_values = skill.base_values
|
|
return nil if base_values.blank?
|
|
|
|
# Quality 1-5 maps to index 0-4
|
|
index = (quality - 1).clamp(0, base_values.size - 1)
|
|
base_values[index]
|
|
end
|
|
end
|