Fix filters and add processors (#181)
* Update test csvs * Fix count filters and refactor apply_filters * Update party_querying_concern.rb * +tests/-debug logs * Make party association optional in Job * Updates for weapon series - Change to new series numbers - Add static method for querying whether the weapon's element is changeable - Add a new method to return a text slug for the weapon's series * Add and update test data - Updates canonical.rb for loading multiple types of data with multiple types of associations - Adds test data for Guidebooks, Job Accessories, Job Skills, and Jobs - Updates test data for Weapons and Summons * Migrations - Adds series of migrations for changing the weapon's series to the values used by Cygames - Shuffled around some foreign keys * Implement BaseProcessor Processors are in charge of processing deck data straight from Granblue. * Implement CharacterProcessor Process character data from deck * Implement WeaponProcessor Process weapon data from deck * Implement JobProcessor Process job, job skill, and job accessory data from deck * Implement SummonProcessor Process summon data from deck * Update SummonProcessor to work like the others * ImportController should use processors * Process element for changeable weapons
This commit is contained in:
parent
60f153a169
commit
3cdd925162
38 changed files with 7634 additions and 321 deletions
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
|
##
|
||||||
|
# ImportController is responsible for importing game data (e.g. deck data)
|
||||||
|
# and creating a new Party along with associated records (job, characters, weapons, summons, etc.).
|
||||||
|
#
|
||||||
|
# The controller expects a JSON payload whose top-level key is "import". If not wrapped,
|
||||||
|
# the controller will wrap the raw data automatically.
|
||||||
|
#
|
||||||
|
# @example Valid payload structure
|
||||||
|
# {
|
||||||
|
# "import": {
|
||||||
|
# "deck": { "name": "My Party", ... },
|
||||||
|
# "pc": { "job": { "master": { "name": "Warrior" } }, ... }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
class ImportController < Api::V1::ApiController
|
class ImportController < Api::V1::ApiController
|
||||||
ELEMENT_MAPPING = {
|
ELEMENT_MAPPING = {
|
||||||
0 => nil,
|
0 => nil,
|
||||||
|
|
@ -13,263 +27,84 @@ module Api
|
||||||
6 => 5
|
6 => 5
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes an import request.
|
||||||
|
#
|
||||||
|
# It reads and parses the raw JSON, wraps the data under the "import" key if necessary,
|
||||||
|
# transforms the deck data using BaseDeckTransformer, validates that the transformed data
|
||||||
|
# contains required fields, and then creates a new Party record (and its associated objects)
|
||||||
|
# inside a transaction.
|
||||||
|
#
|
||||||
|
# @return [void] Renders JSON response with a party shortcode or an error message.
|
||||||
def create
|
def create
|
||||||
Rails.logger.info "[IMPORT] Starting import..."
|
Rails.logger.info '[IMPORT] Checking input...'
|
||||||
|
|
||||||
# Parse JSON request body
|
body = parse_request_body
|
||||||
raw_body = request.raw_post
|
return unless body
|
||||||
begin
|
|
||||||
raw_params = JSON.parse(raw_body) if raw_body.present?
|
raw_params = body['import']
|
||||||
Rails.logger.info "[IMPORT] Raw game data: #{raw_params.inspect}"
|
unless raw_params.is_a?(Hash)
|
||||||
rescue JSON::ParserError => e
|
Rails.logger.error "[IMPORT] 'import' key is missing or not a hash."
|
||||||
Rails.logger.error "[IMPORT] Invalid JSON in request body: #{e.message}"
|
return render json: { error: 'Invalid JSON data' }, status: :unprocessable_content
|
||||||
render json: { error: 'Invalid JSON data' }, status: :bad_request
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if raw_params.nil? || !raw_params.is_a?(Hash)
|
unless raw_params['deck'].is_a?(Hash) &&
|
||||||
Rails.logger.error "[IMPORT] Missing or invalid game data"
|
raw_params['deck'].key?('pc') &&
|
||||||
render json: { error: 'Missing or invalid game data' }, status: :bad_request
|
raw_params['deck'].key?('npc')
|
||||||
return
|
Rails.logger.error "[IMPORT] Deck data incomplete or missing."
|
||||||
|
return render json: { error: 'Invalid deck data' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
# Transform game data
|
Rails.logger.info '[IMPORT] Starting import...'
|
||||||
transformer = ::Granblue::Transformers::BaseDeckTransformer.new(raw_params)
|
|
||||||
transformed_data = transformer.transform
|
|
||||||
Rails.logger.info "[IMPORT] Transformed data: #{transformed_data.inspect}"
|
|
||||||
|
|
||||||
# Validate transformed data
|
return if performed? # Rendered an error response already
|
||||||
unless transformed_data[:name].present? && transformed_data[:lang].present?
|
|
||||||
Rails.logger.error "[IMPORT] Missing required fields in transformed data"
|
|
||||||
render json: { error: 'Missing required fields name or lang' }, status: :unprocessable_entity
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create party
|
party = Party.create(user: current_user)
|
||||||
party = Party.new(user: current_user)
|
deck_data = raw_params['import']
|
||||||
|
process_data(party, deck_data)
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
# Basic party data
|
|
||||||
party.name = transformed_data[:name]
|
|
||||||
party.extra = transformed_data[:extra]
|
|
||||||
party.save!
|
|
||||||
|
|
||||||
# Process job and skills
|
|
||||||
if transformed_data[:class].present?
|
|
||||||
process_job(party, transformed_data[:class], transformed_data[:subskills])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process characters
|
|
||||||
if transformed_data[:characters].present?
|
|
||||||
process_characters(party, transformed_data[:characters])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process weapons
|
|
||||||
if transformed_data[:weapons].present?
|
|
||||||
process_weapons(party, transformed_data[:weapons])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process summons
|
|
||||||
if transformed_data[:summons].present?
|
|
||||||
process_summons(party, transformed_data[:summons], transformed_data[:friend_summon])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process sub summons
|
|
||||||
if transformed_data[:sub_summons].present?
|
|
||||||
process_sub_summons(party, transformed_data[:sub_summons])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return shortcode for redirection
|
|
||||||
render json: { shortcode: party.shortcode }, status: :created
|
render json: { shortcode: party.shortcode }, status: :created
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "[IMPORT] Error processing import: #{e.message}"
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
Rails.logger.error "[IMPORT] Backtrace: #{e.backtrace.join("\n")}"
|
|
||||||
render json: { error: 'Error processing import' }, status: :unprocessable_entity
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_job(party, job_name, subskills)
|
##
|
||||||
return unless job_name
|
# Reads and parses the raw JSON request body.
|
||||||
job = Job.find_by("name_en = ? OR name_jp = ?", job_name, job_name)
|
#
|
||||||
unless job
|
# @return [Hash] Parsed JSON data.
|
||||||
Rails.logger.warn "[IMPORT] Could not find job: #{job_name}"
|
# @raise [JSON::ParserError] If the JSON is invalid.
|
||||||
return
|
def parse_request_body
|
||||||
|
raw_body = request.raw_post
|
||||||
|
JSON.parse(raw_body)
|
||||||
|
rescue JSON::ParserError => e
|
||||||
|
Rails.logger.error "[IMPORT] Invalid JSON: #{e.message}"
|
||||||
|
render json: { error: 'Invalid JSON data' }, status: :bad_request and return
|
||||||
end
|
end
|
||||||
|
|
||||||
party.job = job
|
##
|
||||||
party.save!
|
# Ensures that the provided data is wrapped under an "import" key.
|
||||||
Rails.logger.info "[IMPORT] Assigned job=#{job_name} to party_id=#{party.id}"
|
#
|
||||||
|
# @param data [Hash] The parsed JSON data.
|
||||||
return unless subskills&.any?
|
# @return [Hash] Data wrapped under the "import" key.
|
||||||
subskills.each_with_index do |skill_name, idx|
|
def wrap_import_data(data)
|
||||||
next if skill_name.blank?
|
data.key?('import') ? data : { 'import' => data }
|
||||||
skill = JobSkill.find_by("(name_en = ? OR name_jp = ?) AND job_id = ?", skill_name, skill_name, job.id)
|
|
||||||
unless skill
|
|
||||||
Rails.logger.warn "[IMPORT] Could not find skill=#{skill_name} for job_id=#{job.id}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
party["skill#{idx + 1}_id"] = skill.id
|
|
||||||
Rails.logger.info "[IMPORT] Assigned skill=#{skill_name} at position #{idx + 1}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_characters(party, characters)
|
##
|
||||||
return unless characters&.any?
|
# Processes the deck data using processors.
|
||||||
Rails.logger.info "[IMPORT] Processing #{characters.length} characters"
|
#
|
||||||
|
# @param party [Party] The party to insert data into
|
||||||
|
# @param data [Hash] The wrapped data.
|
||||||
|
# @return [Hash] The transformed deck data.
|
||||||
|
def process_data(party, data)
|
||||||
|
Rails.logger.info '[IMPORT] Transforming deck data'
|
||||||
|
|
||||||
characters.each_with_index do |char_data, idx|
|
Processors::JobProcessor.new(party, data).process
|
||||||
character = Character.find_by(granblue_id: char_data[:id])
|
Processors::CharacterProcessor.new(party, data).process
|
||||||
unless character
|
Processors::SummonProcessor.new(party, data).process
|
||||||
Rails.logger.warn "[IMPORT] Character not found: #{char_data[:id]}"
|
Processors::WeaponProcessor.new(party, data).process
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
GridCharacter.create!(
|
|
||||||
party: party,
|
|
||||||
character_id: character.id,
|
|
||||||
position: idx,
|
|
||||||
uncap_level: char_data[:uncap],
|
|
||||||
perpetuity: char_data[:ringed] || false,
|
|
||||||
transcendence_step: char_data[:transcend] || 0
|
|
||||||
)
|
|
||||||
Rails.logger.info "[IMPORT] Added character: #{character.name_en} at position #{idx}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_weapons(party, weapons)
|
|
||||||
return unless weapons&.any?
|
|
||||||
Rails.logger.info "[IMPORT] Processing #{weapons.length} weapons"
|
|
||||||
|
|
||||||
weapons.each_with_index do |weapon_data, idx|
|
|
||||||
weapon = Weapon.find_by(granblue_id: weapon_data[:id])
|
|
||||||
unless weapon
|
|
||||||
Rails.logger.warn "[IMPORT] Weapon not found: #{weapon_data[:id]}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_weapon = GridWeapon.create!(
|
|
||||||
party: party,
|
|
||||||
weapon_id: weapon.id,
|
|
||||||
position: idx - 1,
|
|
||||||
mainhand: idx.zero?,
|
|
||||||
uncap_level: weapon_data[:uncap],
|
|
||||||
transcendence_step: weapon_data[:transcend] || 0,
|
|
||||||
element: weapon_data[:attr] ? ELEMENT_MAPPING[weapon_data[:attr]] : nil
|
|
||||||
)
|
|
||||||
|
|
||||||
process_weapon_keys(grid_weapon, weapon_data[:keys]) if weapon_data[:keys]
|
|
||||||
process_weapon_ax(grid_weapon, weapon_data[:ax]) if weapon_data[:ax]
|
|
||||||
|
|
||||||
Rails.logger.info "[IMPORT] Added weapon: #{weapon.name_en} at position #{idx - 1}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_weapon_keys(grid_weapon, keys)
|
|
||||||
keys.each_with_index do |key_id, idx|
|
|
||||||
key = WeaponKey.find_by(granblue_id: key_id)
|
|
||||||
unless key
|
|
||||||
Rails.logger.warn "[IMPORT] WeaponKey not found: #{key_id}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
grid_weapon["weapon_key#{idx + 1}_id"] = key.id
|
|
||||||
grid_weapon.save!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_weapon_ax(grid_weapon, ax_skills)
|
|
||||||
ax_skills.each_with_index do |ax, idx|
|
|
||||||
grid_weapon["ax_modifier#{idx + 1}"] = ax[:id].to_i
|
|
||||||
grid_weapon["ax_strength#{idx + 1}"] = ax[:val].to_s.gsub(/[+%]/, '').to_i
|
|
||||||
end
|
|
||||||
grid_weapon.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_summons(party, summons, friend_summon = nil)
|
|
||||||
return unless summons&.any?
|
|
||||||
Rails.logger.info "[IMPORT] Processing #{summons.length} summons"
|
|
||||||
|
|
||||||
# Main and sub summons
|
|
||||||
summons.each_with_index do |summon_data, idx|
|
|
||||||
summon = Summon.find_by(granblue_id: summon_data[:id])
|
|
||||||
unless summon
|
|
||||||
Rails.logger.warn "[IMPORT] Summon not found: #{summon_data[:id]}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_summon = GridSummon.new(
|
|
||||||
party: party,
|
|
||||||
summon_id: summon.id,
|
|
||||||
position: idx,
|
|
||||||
main: idx.zero?,
|
|
||||||
friend: false,
|
|
||||||
uncap_level: summon_data[:uncap],
|
|
||||||
transcendence_step: summon_data[:transcend] || 0,
|
|
||||||
quick_summon: summon_data[:qs] || false
|
|
||||||
)
|
|
||||||
|
|
||||||
if grid_summon.save
|
|
||||||
Rails.logger.info "[IMPORT] Added summon: #{summon.name_en} at position #{idx}"
|
|
||||||
else
|
|
||||||
Rails.logger.error "[IMPORT] Failed to save summon: #{grid_summon.errors.full_messages}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Friend summon if provided
|
|
||||||
process_friend_summon(party, friend_summon) if friend_summon.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_friend_summon(party, friend_summon)
|
|
||||||
friend = Summon.find_by("name_en = ? OR name_jp = ?", friend_summon, friend_summon)
|
|
||||||
unless friend
|
|
||||||
Rails.logger.warn "[IMPORT] Friend summon not found: #{friend_summon}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_summon = GridSummon.new(
|
|
||||||
party: party,
|
|
||||||
summon_id: friend.id,
|
|
||||||
position: 6,
|
|
||||||
main: false,
|
|
||||||
friend: true,
|
|
||||||
uncap_level: friend.ulb ? 5 : (friend.flb ? 4 : 3)
|
|
||||||
)
|
|
||||||
|
|
||||||
if grid_summon.save
|
|
||||||
Rails.logger.info "[IMPORT] Added friend summon: #{friend.name_en}"
|
|
||||||
else
|
|
||||||
Rails.logger.error "[IMPORT] Failed to save friend summon: #{grid_summon.errors.full_messages}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_sub_summons(party, sub_summons)
|
|
||||||
return unless sub_summons&.any?
|
|
||||||
Rails.logger.info "[IMPORT] Processing #{sub_summons.length} sub summons"
|
|
||||||
|
|
||||||
sub_summons.each_with_index do |summon_data, idx|
|
|
||||||
summon = Summon.find_by(granblue_id: summon_data[:id])
|
|
||||||
unless summon
|
|
||||||
Rails.logger.warn "[IMPORT] Sub summon not found: #{summon_data[:id]}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_summon = GridSummon.new(
|
|
||||||
party: party,
|
|
||||||
summon_id: summon.id,
|
|
||||||
position: idx + 5,
|
|
||||||
main: false,
|
|
||||||
friend: false,
|
|
||||||
uncap_level: summon_data[:uncap],
|
|
||||||
transcendence_step: summon_data[:transcend] || 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if grid_summon.save
|
|
||||||
Rails.logger.info "[IMPORT] Added sub summon: #{summon.name_en} at position #{idx + 5}"
|
|
||||||
else
|
|
||||||
Rails.logger.error "[IMPORT] Failed to save sub summon: #{grid_summon.errors.full_messages}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ module PartyQueryingConcern
|
||||||
|
|
||||||
# Uses PartyQueryBuilder to apply additional filters (includes, excludes, date ranges, etc.)
|
# Uses PartyQueryBuilder to apply additional filters (includes, excludes, date ranges, etc.)
|
||||||
def build_filtered_query(base_query)
|
def build_filtered_query(base_query)
|
||||||
PartyQueryBuilder.new(base_query, params: params, current_user: current_user).build
|
PartyQueryBuilder.new(base_query,
|
||||||
|
params: params,
|
||||||
|
current_user: current_user,
|
||||||
|
options: { apply_defaults: true }).build
|
||||||
end
|
end
|
||||||
|
|
||||||
# Renders paginated parties using PartyBlueprint.
|
# Renders paginated parties using PartyBlueprint.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class Job < ApplicationRecord
|
class Job < ApplicationRecord
|
||||||
include PgSearch::Model
|
include PgSearch::Model
|
||||||
|
|
||||||
belongs_to :party
|
belongs_to :party, optional: true
|
||||||
has_many :skills, class_name: 'JobSkill'
|
has_many :skills, class_name: 'JobSkill'
|
||||||
|
|
||||||
multisearchable against: %i[name_en name_jp],
|
multisearchable against: %i[name_en name_jp],
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,54 @@ class Weapon < ApplicationRecord
|
||||||
has_many :weapon_awakenings
|
has_many :weapon_awakenings
|
||||||
has_many :awakenings, through: :weapon_awakenings
|
has_many :awakenings, through: :weapon_awakenings
|
||||||
|
|
||||||
|
SERIES_SLUGS = {
|
||||||
|
1 => 'seraphic',
|
||||||
|
2 => 'grand',
|
||||||
|
3 => 'dark-opus',
|
||||||
|
4 => 'revenant',
|
||||||
|
5 => 'primal',
|
||||||
|
6 => 'beast',
|
||||||
|
7 => 'regalia',
|
||||||
|
8 => 'omega',
|
||||||
|
9 => 'olden-primal',
|
||||||
|
10 => 'hollowsky',
|
||||||
|
11 => 'xeno',
|
||||||
|
12 => 'rose',
|
||||||
|
13 => 'ultima',
|
||||||
|
14 => 'bahamut',
|
||||||
|
15 => 'epic',
|
||||||
|
16 => 'cosmos',
|
||||||
|
17 => 'superlative',
|
||||||
|
18 => 'vintage',
|
||||||
|
19 => 'class-champion',
|
||||||
|
20 => 'replica',
|
||||||
|
21 => 'relic',
|
||||||
|
22 => 'rusted',
|
||||||
|
23 => 'sephira',
|
||||||
|
24 => 'vyrmament',
|
||||||
|
25 => 'upgrader',
|
||||||
|
26 => 'astral',
|
||||||
|
27 => 'draconic',
|
||||||
|
28 => 'eternal-splendor',
|
||||||
|
29 => 'ancestral',
|
||||||
|
30 => 'new-world-foundation',
|
||||||
|
31 => 'ennead',
|
||||||
|
32 => 'militis',
|
||||||
|
33 => 'malice',
|
||||||
|
34 => 'menace',
|
||||||
|
35 => 'illustrious',
|
||||||
|
36 => 'proven',
|
||||||
|
37 => 'revans',
|
||||||
|
38 => 'world',
|
||||||
|
39 => 'exo',
|
||||||
|
40 => 'draconic-providence',
|
||||||
|
41 => 'celestial',
|
||||||
|
42 => 'omega-rebirth',
|
||||||
|
43 => 'collab',
|
||||||
|
98 => 'event',
|
||||||
|
99 => 'gacha'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def blueprint
|
def blueprint
|
||||||
WeaponBlueprint
|
WeaponBlueprint
|
||||||
end
|
end
|
||||||
|
|
@ -51,11 +99,23 @@ class Weapon < ApplicationRecord
|
||||||
|
|
||||||
# Returns whether the weapon is included in the Draconic or Dark Opus series
|
# Returns whether the weapon is included in the Draconic or Dark Opus series
|
||||||
def opus_or_draconic?
|
def opus_or_draconic?
|
||||||
[2, 3].include?(series)
|
[3, 27].include?(series)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns whether the weapon belongs to the Draconic Weapon series or the Draconic Weapon Providence series
|
# Returns whether the weapon belongs to the Draconic Weapon series or the Draconic Weapon Providence series
|
||||||
def draconic_or_providence?
|
def draconic_or_providence?
|
||||||
[3, 34].include?(series)
|
[27, 40].include?(series)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.element_changeable?(series)
|
||||||
|
[4, 13, 17, 19].include?(series.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def series_slug
|
||||||
|
# Assuming series is an array, take the first value
|
||||||
|
series_number = series.first
|
||||||
|
SERIES_SLUGS[series_number]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -46,15 +46,11 @@ class PartyQueryBuilder
|
||||||
# Example edge case: If the request does not specify 'characters_count',
|
# Example edge case: If the request does not specify 'characters_count',
|
||||||
# then the default (e.g. 3) will be used, with the upper bound coming from a constant.
|
# then the default (e.g. 3) will be used, with the upper bound coming from a constant.
|
||||||
def apply_filters(query)
|
def apply_filters(query)
|
||||||
conditions = build_filters
|
query = apply_base_filters(query)
|
||||||
query = query.where(conditions)
|
query = apply_name_quality_filter(query)
|
||||||
# If name_quality filtering is enabled via params, apply it.
|
query = apply_count_filters(query)
|
||||||
query = query.where(name_quality) if @params[:name_quality].present?
|
|
||||||
query.where(
|
query
|
||||||
weapons_count: build_count(@params[:weapons_count], default_weapons_count)..max_weapons,
|
|
||||||
characters_count: build_count(@params[:characters_count], default_characters_count)..max_characters,
|
|
||||||
summons_count: build_count(@params[:summons_count], default_summons_count)..max_summons
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Example callback method: if no explicit status filter is provided, we may want
|
# Example callback method: if no explicit status filter is provided, we may want
|
||||||
|
|
@ -148,6 +144,63 @@ class PartyQueryBuilder
|
||||||
query
|
query
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Applies base filtering conditions from build_filters to the query.
|
||||||
|
# @param query [ActiveRecord::QueryMethods::WhereChain] The current query.
|
||||||
|
# @return [ActiveRecord::Relation] The query with base filters applied.
|
||||||
|
def apply_base_filters(query)
|
||||||
|
query.where(build_filters)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Applies the name quality filter to the query if the parameter is present.
|
||||||
|
# @param query [ActiveRecord::QueryMethods::WhereChain] The current query.
|
||||||
|
# @return [ActiveRecord::Relation] The query with the name quality filter applied.
|
||||||
|
def apply_name_quality_filter(query)
|
||||||
|
@params[:name_quality].present? ? query.where(name_quality) : query
|
||||||
|
end
|
||||||
|
|
||||||
|
# Applies count filters to the query based on provided parameters or default options.
|
||||||
|
# If apply_defaults is set in options, default ranges are applied.
|
||||||
|
# Otherwise, count ranges are built from provided parameters.
|
||||||
|
# @param query [ | ActiveRecord::QueryMethods::WhereChain] The current query.
|
||||||
|
# @return [ActiveRecord::Relation] The query with count filters applied.
|
||||||
|
def apply_count_filters(query)
|
||||||
|
if @options[:apply_defaults]
|
||||||
|
query.where(
|
||||||
|
weapons_count: default_weapons_count..max_weapons,
|
||||||
|
characters_count: default_characters_count..max_characters,
|
||||||
|
summons_count: default_summons_count..max_summons
|
||||||
|
)
|
||||||
|
elsif count_filter_provided?
|
||||||
|
query.where(build_count_conditions)
|
||||||
|
else
|
||||||
|
query
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Determines if any count filter parameters have been provided.
|
||||||
|
# @return [Boolean] True if any count filters are provided, false otherwise.
|
||||||
|
def count_filter_provided?
|
||||||
|
@params.key?(:weapons_count) || @params.key?(:characters_count) || @params.key?(:summons_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds a hash of count conditions based on the count filter parameters.
|
||||||
|
# @return [Hash] A hash with keys :weapons_count, :characters_count, and :summons_count.
|
||||||
|
def build_count_conditions
|
||||||
|
{
|
||||||
|
weapons_count: build_range(@params[:weapons_count], max_weapons),
|
||||||
|
characters_count: build_range(@params[:characters_count], max_characters),
|
||||||
|
summons_count: build_range(@params[:summons_count], max_summons)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constructs a range for a given count parameter.
|
||||||
|
# @param param_value [String, nil] The count filter parameter value.
|
||||||
|
# @param max_value [Integer] The maximum allowed value for the count.
|
||||||
|
# @return [Range] A range from the provided count (or 0 if blank) to the max_value.
|
||||||
|
def build_range(param_value, max_value)
|
||||||
|
param_value.present? ? param_value.to_i..max_value : 0..max_value
|
||||||
|
end
|
||||||
|
|
||||||
# Maps an ID’s first character to the corresponding grid table and object table names.
|
# Maps an ID’s first character to the corresponding grid table and object table names.
|
||||||
#
|
#
|
||||||
# For example:
|
# For example:
|
||||||
|
|
@ -170,27 +223,27 @@ class PartyQueryBuilder
|
||||||
|
|
||||||
# Default values and maximum limits for counts.
|
# Default values and maximum limits for counts.
|
||||||
def default_weapons_count
|
def default_weapons_count
|
||||||
@options[:default_weapons_count] || 5;
|
@options[:default_weapons_count] || 5
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_characters_count
|
def default_characters_count
|
||||||
@options[:default_characters_count] || 3;
|
@options[:default_characters_count] || 3
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_summons_count
|
def default_summons_count
|
||||||
@options[:default_summons_count] || 2;
|
@options[:default_summons_count] || 2
|
||||||
end
|
end
|
||||||
|
|
||||||
def max_weapons
|
def max_weapons
|
||||||
@options[:max_weapons] || 13;
|
@options[:max_weapons] || 13
|
||||||
end
|
end
|
||||||
|
|
||||||
def max_characters
|
def max_characters
|
||||||
@options[:max_characters] || 5;
|
@options[:max_characters] || 5
|
||||||
end
|
end
|
||||||
|
|
||||||
def max_summons
|
def max_summons
|
||||||
@options[:max_summons] || 8;
|
@options[:max_summons] || 8
|
||||||
end
|
end
|
||||||
|
|
||||||
# Stub method for name quality filtering.
|
# Stub method for name quality filtering.
|
||||||
|
|
|
||||||
44
app/services/processors/base_processor.rb
Normal file
44
app/services/processors/base_processor.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Processors
|
||||||
|
##
|
||||||
|
# BaseProcessor provides shared functionality for processing transformed deck data
|
||||||
|
# into new party records. Subclasses must implement the +process+ method.
|
||||||
|
#
|
||||||
|
# @abstract
|
||||||
|
class BaseProcessor
|
||||||
|
##
|
||||||
|
# Initializes the processor.
|
||||||
|
#
|
||||||
|
# @param party [Party] the Party record to which the component will be added.
|
||||||
|
# @param data [Object] the transformed data for this component.
|
||||||
|
# @param options [Hash] optional additional options.
|
||||||
|
def initialize(party, data, options = {})
|
||||||
|
@party = party
|
||||||
|
@data = data
|
||||||
|
@options = options
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Process the given data and create associated records.
|
||||||
|
#
|
||||||
|
# @abstract Subclasses must implement this method.
|
||||||
|
# @return [void]
|
||||||
|
def process
|
||||||
|
raise NotImplementedError, "#{self.class} must implement the process method"
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
attr_reader :party, :data, :options
|
||||||
|
|
||||||
|
##
|
||||||
|
# Logs a message to Rails.logger.
|
||||||
|
#
|
||||||
|
# @param message [String] the message to log.
|
||||||
|
# @return [void]
|
||||||
|
def log(message)
|
||||||
|
Rails.logger.info "[PROCESSOR][#{self.class.name}] #{message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
89
app/services/processors/character_processor.rb
Normal file
89
app/services/processors/character_processor.rb
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Processors
|
||||||
|
##
|
||||||
|
# CharacterProcessor processes an array of character data and creates GridCharacter records.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# processor = Processors::CharacterProcessor.new(party, transformed_characters_array)
|
||||||
|
# processor.process
|
||||||
|
class CharacterProcessor < BaseProcessor
|
||||||
|
def initialize(party, data, type = :normal, options = {})
|
||||||
|
super(party, data, options)
|
||||||
|
@party = party
|
||||||
|
@data = data
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes character data.
|
||||||
|
#
|
||||||
|
# Iterates over each character hash in +data+ and creates a new GridCharacter record.
|
||||||
|
# Expects each character hash to include keys such as :id, :position, :uncap, etc.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def process
|
||||||
|
unless @data.is_a?(Hash)
|
||||||
|
Rails.logger.error "[CHARACTER] Invalid data format: expected a Hash, got #{@data.class}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @data.key?('deck') && @data['deck'].key?('npc')
|
||||||
|
Rails.logger.error '[CHARACTER] Missing npc data in deck JSON'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@data = @data.with_indifferent_access
|
||||||
|
characters_data = @data.dig('deck', 'npc')
|
||||||
|
|
||||||
|
grid_characters = process_characters(characters_data)
|
||||||
|
grid_characters.each do |grid_character|
|
||||||
|
begin
|
||||||
|
grid_character.save!
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
Rails.logger.error "[CHARACTER] Failed to create GridCharacter: #{e.record.errors.full_messages.join(', ')}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue StandardError => e
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_characters(characters_data)
|
||||||
|
characters_data.map do |key, raw_character|
|
||||||
|
next if raw_character.nil? || raw_character['param'].nil? || raw_character['master'].nil?
|
||||||
|
|
||||||
|
position = key.to_i - 1
|
||||||
|
|
||||||
|
# Find the Character record by its granblue_id.
|
||||||
|
character_id = raw_character.dig('master', 'id')
|
||||||
|
character = Character.find_by(granblue_id: character_id)
|
||||||
|
|
||||||
|
unless character
|
||||||
|
Rails.logger.error "[CHARACTER] Character not found with id #{character_id}"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
# The deck doesn't have Awakening data, so use the default
|
||||||
|
awakening = Awakening.where(slug: 'character-balanced').first
|
||||||
|
grid_character = GridCharacter.create(
|
||||||
|
party_id: @party.id,
|
||||||
|
character_id: character.id,
|
||||||
|
uncap_level: raw_character.dig('param', 'evolution').to_i,
|
||||||
|
transcendence_step: raw_character.dig('param', 'phase').to_i,
|
||||||
|
position: position,
|
||||||
|
perpetuity: raw_character.dig('param', 'has_npcaugment_constant'),
|
||||||
|
awakening: awakening
|
||||||
|
)
|
||||||
|
|
||||||
|
grid_character
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
# Converts a value to a boolean.
|
||||||
|
def parse_boolean(val)
|
||||||
|
val.to_s.downcase == 'true'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
127
app/services/processors/job_processor.rb
Normal file
127
app/services/processors/job_processor.rb
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Processors
|
||||||
|
##
|
||||||
|
# JobProcessor is responsible for processing job data from the transformed deck data.
|
||||||
|
# It finds a Job record by the master’s id and assigns it (and its job skills) to the Party.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# raw_data = { 'job' => { "master": { "id": '130401', ... }, ... }, 'set_action': [ ... ] }
|
||||||
|
# processor = Processors::JobProcessor.new(party, raw_data, language: 'en')
|
||||||
|
# processor.process
|
||||||
|
class JobProcessor < BaseProcessor
|
||||||
|
##
|
||||||
|
# Initializes a new JobProcessor.
|
||||||
|
#
|
||||||
|
# @param party [Party] the Party record.
|
||||||
|
# @param data [Hash] the raw JSON data.
|
||||||
|
# @param options [Hash] options hash; e.g. expects :language.
|
||||||
|
def initialize(party, data, options = {})
|
||||||
|
super(party, options)
|
||||||
|
@party = party
|
||||||
|
@data = data
|
||||||
|
@language = options[:language] || 'en'
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes job data.
|
||||||
|
#
|
||||||
|
# Finds a Job record using a case‐insensitive search on +name_en+ or +name_jp+.
|
||||||
|
# If found, it assigns the job to the party and (if provided) assigns subskills.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def process
|
||||||
|
if @data.is_a?(Hash)
|
||||||
|
@data = @data.with_indifferent_access
|
||||||
|
else
|
||||||
|
Rails.logger.error "[JOB] Invalid data format: expected a Hash, got #{@data.class}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @data.key?('deck') && @data['deck'].key?('pc') && @data['deck']['pc'].key?('job')
|
||||||
|
Rails.logger.error '[JOB] Missing job data in deck JSON'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extract job data
|
||||||
|
job_data = @data.dig('deck', 'pc', 'job', 'master')
|
||||||
|
job_skills = @data.dig('deck', 'pc', 'set_action')
|
||||||
|
job_accessory_id = @data.dig('deck', 'pc', 'familiar_id') || @data.dig('deck', 'pc', 'shield_id')
|
||||||
|
|
||||||
|
# Look up and set the Job and its main skill
|
||||||
|
process_core_job(job_data)
|
||||||
|
|
||||||
|
# Look up and set the job skills.
|
||||||
|
if job_skills.present?
|
||||||
|
skills = process_job_skills(job_skills)
|
||||||
|
party.update(skill1: skills[0], skill2: skills[1], skill3: skills[2])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Look up and set the job accessory.
|
||||||
|
accessory = process_job_accessory(job_accessory_id)
|
||||||
|
party.update(accessory: accessory)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "[JOB] Exception during job processing: #{e.message}"
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
##
|
||||||
|
# Updates the party with the corresponding job and its main skill.
|
||||||
|
#
|
||||||
|
# This method attempts to locate a Job using the provided job_data's 'id' (which represents
|
||||||
|
# the granblue_id). If the job is found, it retrieves the job's main
|
||||||
|
# skill (i.e. the JobSkill record where `main` is true) and updates the party with the job
|
||||||
|
# and its main skill. If no job is found, the method returns without updating.
|
||||||
|
#
|
||||||
|
# @param [Hash] job_data A hash containing job information.
|
||||||
|
# It must include the key 'id', which holds the granblue_id for the job.
|
||||||
|
# @return [void]
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# job_data = { 'id' => 42 }
|
||||||
|
# process_core_job(job_data)
|
||||||
|
def process_core_job(job_data)
|
||||||
|
# Look up the Job by granblue_id (the job master id).
|
||||||
|
job = Job.find_by(granblue_id: job_data['id'])
|
||||||
|
return unless job
|
||||||
|
|
||||||
|
main_skill = JobSkill.find_by(job_id: job.id, main: true)
|
||||||
|
|
||||||
|
party.update(job: job, skill0: main_skill)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes and associates job skills with a given job.
|
||||||
|
#
|
||||||
|
# This method first removes any existing skills from the job. It then iterates over the provided
|
||||||
|
# array of skill names, attempting to find a matching JobSkill record by comparing the provided
|
||||||
|
# name against both the English and Japanese name fields. Any found JobSkill records are then
|
||||||
|
# associated with the job. Finally, the method logs the processed job skill names.
|
||||||
|
#
|
||||||
|
# @param job_skills [Array<String>] an array of job skill names.
|
||||||
|
# @return [Array<JobSkill>] an array of JobSkill records that were associated with the job.
|
||||||
|
def process_job_skills(job_skills)
|
||||||
|
job_skills.map do |skill|
|
||||||
|
name = skill['name']
|
||||||
|
JobSkill.find_by(name_en: name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes raw data to find the currently set job accessory
|
||||||
|
#
|
||||||
|
# Searches JobAccessories for the given `granblue_id`
|
||||||
|
#
|
||||||
|
# @param accessory_id [String] the granblue_id of the accessory
|
||||||
|
def process_job_accessory(accessory_id)
|
||||||
|
JobAccessory.find_by(granblue_id: accessory_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Converts a value (string or boolean) to a boolean.
|
||||||
|
def to_boolean(val)
|
||||||
|
val.to_s.downcase == 'true'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
201
app/services/processors/summon_processor.rb
Normal file
201
app/services/processors/summon_processor.rb
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Processors
|
||||||
|
##
|
||||||
|
# SummonProcessor processes an array of summon data and creates GridSummon records.
|
||||||
|
# It handles different summon types based on the +type+ parameter:
|
||||||
|
# - :normal => standard summons
|
||||||
|
# - :friend => friend summon (fixed position and uncap logic)
|
||||||
|
# - :sub => sub summons (position based on order)
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# normal_processor = SummonProcessor.new(party, summons_array, :normal, quick_summon_id)
|
||||||
|
# normal_processor.process
|
||||||
|
#
|
||||||
|
# friend_processor = SummonProcessor.new(party, [friend_summon_name], :friend)
|
||||||
|
# friend_processor.process
|
||||||
|
class SummonProcessor < BaseProcessor
|
||||||
|
TRANSCENDENCE_LEVELS = [200, 210, 220, 230, 240, 250].freeze
|
||||||
|
|
||||||
|
##
|
||||||
|
# Initializes a new SummonProcessor.
|
||||||
|
#
|
||||||
|
# @param party [Party] the Party record.
|
||||||
|
# @param data [Hash] the deck hash.
|
||||||
|
# @param type [Symbol] the type of summon (:normal, :friend, or :sub).
|
||||||
|
# @param quick_summon_id [String, nil] (optional) the quick summon identifier.
|
||||||
|
# @param options [Hash] additional options.
|
||||||
|
def initialize(party, data, type = :normal, options = {})
|
||||||
|
super(party, data, options)
|
||||||
|
@party = party
|
||||||
|
@data = data
|
||||||
|
@type = type
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes summon data and creates GridSummon records.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def process
|
||||||
|
unless @data.is_a?(Hash)
|
||||||
|
Rails.logger.error "[SUMMON] Invalid data format: expected a Hash, got #{@data.class}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @data.key?('deck') && @data['deck'].key?('pc')
|
||||||
|
Rails.logger.error '[SUMMON] Missing npc data in deck JSON'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@data = @data.with_indifferent_access
|
||||||
|
summons_data = @data.dig('deck', 'pc', 'summons')
|
||||||
|
sub_summons_data = @data.dig('deck', 'pc', 'sub_summons')
|
||||||
|
|
||||||
|
grid_summons = process_summons(summons_data, sub: false)
|
||||||
|
friend_summon = process_friend_summon
|
||||||
|
sub_summons = process_summons(sub_summons_data, sub: true)
|
||||||
|
|
||||||
|
summons = [*grid_summons, friend_summon, *sub_summons]
|
||||||
|
|
||||||
|
summons.each do |summon|
|
||||||
|
summon.save!
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
Rails.logger.error "[SUMMON] Failed to create GridSummon: #{e.record.errors.full_messages.join(', ')}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :type
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes a set of summon data and creates GridSummon records.
|
||||||
|
#
|
||||||
|
# @param summons [Hash] the summon data
|
||||||
|
# @param sub [Boolean] true if we are polling sub summons
|
||||||
|
# @return [Array<GridSummon>]
|
||||||
|
def process_summons(summons, sub: false)
|
||||||
|
internal_quick_summon_id = @data['quick_user_summon_id'].to_i if sub
|
||||||
|
|
||||||
|
summons.map do |key, raw_summon|
|
||||||
|
summon_params = raw_summon['param']
|
||||||
|
summon_id = raw_summon['master']['id']
|
||||||
|
summon = Summon.find_by(granblue_id: transform_id(summon_id))
|
||||||
|
|
||||||
|
position = if sub
|
||||||
|
key.to_i + 4
|
||||||
|
else
|
||||||
|
key.to_i == 1 ? -1 : key.to_i - 2
|
||||||
|
end
|
||||||
|
|
||||||
|
GridSummon.new({
|
||||||
|
party: @party,
|
||||||
|
summon: summon,
|
||||||
|
position: position,
|
||||||
|
main: key.to_i == 1,
|
||||||
|
friend: false,
|
||||||
|
quick_summon: summon_params['id'].to_i == internal_quick_summon_id,
|
||||||
|
uncap_level: summon_params['evolution'].to_i,
|
||||||
|
transcendence_step: level_to_transcendence(summon_params['level'].to_i),
|
||||||
|
created_at: Time.now,
|
||||||
|
updated_at: Time.now
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes friend summon data and creates a GridSummon record.
|
||||||
|
#
|
||||||
|
# @return [GridSummon]
|
||||||
|
def process_friend_summon
|
||||||
|
summon_name = @data.dig('deck', 'pc', 'damage_info', 'summon_name')
|
||||||
|
summon = Summon.find_by('name_en = ? OR name_jp = ?', summon_name, summon_name)
|
||||||
|
|
||||||
|
GridSummon.new({
|
||||||
|
party: @party,
|
||||||
|
summon: summon,
|
||||||
|
position: 4,
|
||||||
|
main: false,
|
||||||
|
friend: true,
|
||||||
|
quick_summon: false,
|
||||||
|
uncap_level: determine_uncap_level(summon),
|
||||||
|
transcendence_step: summon.transcendence ? 5 : 0,
|
||||||
|
created_at: Time.now,
|
||||||
|
updated_at: Time.now
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Determines the numeric uncap level of a given Summon
|
||||||
|
#
|
||||||
|
# @param summon [Summon] the canonical summon
|
||||||
|
# @return [Integer]
|
||||||
|
def determine_uncap_level(summon)
|
||||||
|
if summon.transcendence
|
||||||
|
6
|
||||||
|
elsif summon.ulb
|
||||||
|
5
|
||||||
|
elsif summon.flb
|
||||||
|
4
|
||||||
|
else
|
||||||
|
3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Determines the uncap level for a friend summon based on its ULb and FLb flags.
|
||||||
|
#
|
||||||
|
# @param summon_data [Hash] the summon data.
|
||||||
|
# @return [Integer] the computed uncap level.
|
||||||
|
def determine_friend_uncap(summon_data)
|
||||||
|
if summon_data[:ulb]
|
||||||
|
5
|
||||||
|
elsif summon_data[:flb]
|
||||||
|
4
|
||||||
|
else
|
||||||
|
3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Converts a given level, rounded down to the nearest 10,
|
||||||
|
# to its corresponding transcendence step.
|
||||||
|
#
|
||||||
|
# If level is 200, returns 0; if level is 250, returns 5.
|
||||||
|
#
|
||||||
|
# @param level [Integer] the summon's level
|
||||||
|
# @return [Integer] the transcendence step
|
||||||
|
def level_to_transcendence(level)
|
||||||
|
return 0 if level < 200
|
||||||
|
|
||||||
|
floored_level = (level / 10).floor * 10
|
||||||
|
TRANSCENDENCE_LEVELS.index(floored_level)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Transforms 5★ Arcarum-series summon IDs into their 4★ variants,
|
||||||
|
# as that's what is stored in the database.
|
||||||
|
#
|
||||||
|
# If an unrelated ID, or the 4★ ID is passed, then returns the input.
|
||||||
|
#
|
||||||
|
# @param id [String] the ID to match
|
||||||
|
# @return [String] the resulting ID
|
||||||
|
def transform_id(id)
|
||||||
|
mapping = {
|
||||||
|
'2040315000' => '2040238000',
|
||||||
|
'2040316000' => '2040239000',
|
||||||
|
'2040314000' => '2040237000',
|
||||||
|
'2040313000' => '2040236000',
|
||||||
|
'2040321000' => '2040244000',
|
||||||
|
'2040319000' => '2040242000',
|
||||||
|
'2040317000' => '2040240000',
|
||||||
|
'2040322000' => '2040245000',
|
||||||
|
'2040318000' => '2040241000',
|
||||||
|
'2040320000' => '2040243000'
|
||||||
|
}
|
||||||
|
|
||||||
|
# If the id is a key, return the mapped value; otherwise, return the id.
|
||||||
|
mapping[id] || id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
395
app/services/processors/weapon_processor.rb
Normal file
395
app/services/processors/weapon_processor.rb
Normal file
|
|
@ -0,0 +1,395 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Processors
|
||||||
|
##
|
||||||
|
# WeaponProcessor processes weapon data from a deck JSON and creates GridWeapon records.
|
||||||
|
# It follows a similar error‐handling and implementation strategy as SummonProcessor.
|
||||||
|
#
|
||||||
|
# Expected data format (excerpt):
|
||||||
|
# {
|
||||||
|
# "deck": {
|
||||||
|
# "pc": {
|
||||||
|
# "weapons": {
|
||||||
|
# "1": {
|
||||||
|
# "param": {
|
||||||
|
# "uncap": 3,
|
||||||
|
# "level": "150",
|
||||||
|
# "augment_skill_info": [ [ { "skill_id": 1588, "effect_value": "3", "show_value": "3%" }, ... ] ],
|
||||||
|
# "arousal": {
|
||||||
|
# "is_arousal_weapon": true,
|
||||||
|
# "level": 4,
|
||||||
|
# "skill": [ { "skill_id": 1896, ... }, ... ]
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# },
|
||||||
|
# "master": {
|
||||||
|
# "id": "1040215100",
|
||||||
|
# "name": "Wamdus's Cnidocyte",
|
||||||
|
# "attribute": "2",
|
||||||
|
# ...
|
||||||
|
# },
|
||||||
|
# "keys": [ "..." ] // optional
|
||||||
|
# },
|
||||||
|
# "2": { ... },
|
||||||
|
# ...
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# The processor also uses an AX_MAPPING to convert in‐game AX skill IDs to our stored values.
|
||||||
|
class WeaponProcessor < BaseProcessor
|
||||||
|
TRANSCENDENCE_LEVELS = [200, 210, 220, 230, 240, 250].freeze
|
||||||
|
|
||||||
|
# Mapping from in‑game AX skill IDs (as strings) to our internal modifier values.
|
||||||
|
AX_MAPPING = {
|
||||||
|
'1588' => 2,
|
||||||
|
'1589' => 0,
|
||||||
|
'1590' => 1,
|
||||||
|
'1591' => 3,
|
||||||
|
'1592' => 4,
|
||||||
|
'1593' => 9,
|
||||||
|
'1594' => 13,
|
||||||
|
'1595' => 10,
|
||||||
|
'1596' => 5,
|
||||||
|
'1597' => 6,
|
||||||
|
'1599' => 8,
|
||||||
|
'1600' => 12,
|
||||||
|
'1601' => 11,
|
||||||
|
'1719' => 15,
|
||||||
|
'1720' => 16,
|
||||||
|
'1721' => 17,
|
||||||
|
'1722' => 14
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# KEY_MAPPING maps the raw key value (as a string) to a canonical range or value.
|
||||||
|
# For example, in our test we want a raw key "10001" to be interpreted as any key whose
|
||||||
|
# canonical granblue_id is between 697 and 706.
|
||||||
|
KEY_MAPPING = {
|
||||||
|
'10001' => %w[697 698 699 700 701 702 703 704 705 706],
|
||||||
|
'10002' => %w[707 708 709 710 711 712 713 714 715 716],
|
||||||
|
'10003' => %w[717 718 719 720 721 722 723 724 725 726],
|
||||||
|
'10004' => %w[727 728 729 730 731 732 733 734 735 736],
|
||||||
|
'10005' => %w[737 738 739 740 741 742 743 744 745 746],
|
||||||
|
'10006' => %w[747 748 749 750 751 752 753 754 755 756],
|
||||||
|
'11001' => '758',
|
||||||
|
'11002' => '759',
|
||||||
|
'11003' => '760',
|
||||||
|
'11004' => '760',
|
||||||
|
'13001' => %w[1240 2204 2208], # α Pendulum
|
||||||
|
'13002' => %w[1241 2205 2209], # β Pendulum
|
||||||
|
'13003' => %w[1242 2206 2210], # γ Pendulum
|
||||||
|
'13004' => %w[1243 2207 2211], # Δ Pendulum
|
||||||
|
'14001' => %w[502 503 504 505 506 507 1213 1214 1215 1216 1217 1218], # Pendulum of Strength
|
||||||
|
'14002' => %w[130 131 132 133 134 135 71 72 73 74 75 76], # Pendulum of Zeal
|
||||||
|
'14003' => %w[1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271], # Pendulum of Strife
|
||||||
|
'14004' => %w[1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210], # Pendulum of Prosperity
|
||||||
|
'14005' => %w[2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223], # Pendulum of Extremity
|
||||||
|
'14006' => %w[2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235], # Pendulum of Sagacity
|
||||||
|
'14007' => %w[2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247], # Pendulum of Supremacy
|
||||||
|
'14011' => %w[322 323 324 325 326 327 1310 1311 1312 1313 1314 1315], # Chain of Temperament
|
||||||
|
'14012' => %w[764 765 766 767 768 769 1731 1732 1733 1734 1735 948], # Chain of Restoration
|
||||||
|
'14013' => %w[1171 1172 1173 1174 1175 1176 1736 1737 1738 1739 1740 1741], # Chain of Glorification
|
||||||
|
'14014' => '1723', # Chain of Temptation
|
||||||
|
'14015' => '1724', # Chain of Forbiddance
|
||||||
|
'14016' => '1725', # Chain of Depravity
|
||||||
|
'14017' => '1726', # Chain of Falsehood
|
||||||
|
'15001' => '1446',
|
||||||
|
'15002' => '1447',
|
||||||
|
'15003' => '1448', # Abyss Teluma
|
||||||
|
'15004' => '1449', # Crag Teluma
|
||||||
|
'15005' => '1450', # Tempest Teluma
|
||||||
|
'15006' => '1451',
|
||||||
|
'15007' => '1452', # Malice Teluma
|
||||||
|
'15008' => %w[2043 2044 2045 2046 2047 2048],
|
||||||
|
'15009' => %w[2049 2050 2051 2052 2053 2054], # Oblivion Teluma
|
||||||
|
'16001' => %w[1228 1229 1230 1231 1232 1233], # Optimus Teluma
|
||||||
|
'16002' => %w[1234 1235 1236 1237 1238 1239], # Omega Teluma
|
||||||
|
'17001' => '1807',
|
||||||
|
'17002' => '1808',
|
||||||
|
'17003' => '1809',
|
||||||
|
'17004' => '1810',
|
||||||
|
# Emblems (series {24})
|
||||||
|
'3' => '3',
|
||||||
|
'2' => '2',
|
||||||
|
'1' => '1'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
AWAKENING_MAPPING = {
|
||||||
|
'1' => 'weapon-atk',
|
||||||
|
'2' => 'weapon-def',
|
||||||
|
'3' => 'weapon-special',
|
||||||
|
'4' => 'weapon-ca',
|
||||||
|
'5' => 'weapon-skill',
|
||||||
|
'6' => 'weapon-heal',
|
||||||
|
'7' => 'weapon-multi'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
ELEMENTAL_WEAPON_MAPPING = %w[1040914600 1040810100 1040506800 1040312000 1040513800 1040810900 1040910300
|
||||||
|
1040114200 1040027000 1040807600 1040120300 1040318500 1040710000 1040608100
|
||||||
|
1040812100 1040307200 1040410200 1040510600 1040018100 1040113400 1040017300
|
||||||
|
1040011900 1040412200 1040508000 1040512600 1040609100 1040411600 1040208800
|
||||||
|
1040906900 1040909300 1040509700 1040014400 1040308400 1040613100 1040013200
|
||||||
|
1040011300 1040413400 1040607500 1040504400 1040703600 1040406000 1040601700
|
||||||
|
1040904300 1040109700 1040900300 1040002000 1040807200 1040102900 1040203000
|
||||||
|
1040402800 1040507400 1040200900 1040307800 1040501600 1040706900 1040604200
|
||||||
|
1040103000 1040003500 1040300100 1040907500 1040105500 1040106600 1040503500
|
||||||
|
1040801300 1040410800 1040702700 1040006200 1040302300 1040803700 1040900400
|
||||||
|
1040406900 1040109100 1040111600 1040706300 1040806400 1040209700 1040707500
|
||||||
|
1040208200 1040214000 1040021100 1040417200 1040012600 1040317500 1040402900].freeze
|
||||||
|
ELEMENTAL_WEAPON_MAPPING_INT = ELEMENTAL_WEAPON_MAPPING.map(&:to_i).sort.freeze
|
||||||
|
|
||||||
|
ELEMENT_MAPPING = {
|
||||||
|
0 => nil,
|
||||||
|
1 => 4, # Wind -> Earth
|
||||||
|
2 => 2, # Fire -> Fire
|
||||||
|
3 => 3, # Water -> Water
|
||||||
|
4 => 1, # Earth -> Wind
|
||||||
|
5 => 6, # Dark -> Light
|
||||||
|
6 => 5 # Light -> Dark
|
||||||
|
}.freeze
|
||||||
|
##
|
||||||
|
# Initializes a new WeaponProcessor.
|
||||||
|
#
|
||||||
|
# @param party [Party] the Party record.
|
||||||
|
# @param data [Hash] the full deck JSON.
|
||||||
|
# @param type [Symbol] (optional) processing type.
|
||||||
|
# @param options [Hash] additional options.
|
||||||
|
def initialize(party, data, type = :normal, options = {})
|
||||||
|
super(party, data, options)
|
||||||
|
@party = party
|
||||||
|
@data = data
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes the deck’s weapon data and creates GridWeapon records.
|
||||||
|
#
|
||||||
|
# It expects the incoming data to be a Hash that contains:
|
||||||
|
# "deck" → "pc" → "weapons"
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def process
|
||||||
|
unless @data.is_a?(Hash)
|
||||||
|
Rails.logger.error "[WEAPON] Invalid data format: expected a Hash, got #{@data.class}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @data.key?('deck') && @data['deck'].key?('pc') && @data['deck']['pc'].key?('weapons')
|
||||||
|
Rails.logger.error '[WEAPON] Missing weapons data in deck JSON'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@data = @data.with_indifferent_access
|
||||||
|
weapons_data = @data['deck']['pc']['weapons']
|
||||||
|
|
||||||
|
grid_weapons = process_weapons(weapons_data)
|
||||||
|
|
||||||
|
grid_weapons.each do |grid_weapon|
|
||||||
|
begin
|
||||||
|
grid_weapon.save!
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
Rails.logger.error "[WEAPON] Failed to create GridWeapon: #{e.record.errors.full_messages.join(', ')}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes a hash of raw weapon data and returns an array of GridWeapon records.
|
||||||
|
#
|
||||||
|
# @param weapons_data [Hash] the raw weapons data (keyed by slot number).
|
||||||
|
# @return [Array<GridWeapon>]
|
||||||
|
def process_weapons(weapons_data)
|
||||||
|
weapons_data.map do |key, raw_weapon|
|
||||||
|
next if raw_weapon.nil? || raw_weapon['param'].nil? || raw_weapon['master'].nil?
|
||||||
|
|
||||||
|
position = key.to_i == 1 ? -1 : key.to_i - 2
|
||||||
|
mainhand = (position == -1)
|
||||||
|
|
||||||
|
uncap_level = raw_weapon.dig('param', 'uncap').to_i
|
||||||
|
level = raw_weapon.dig('param', 'level').to_i
|
||||||
|
transcendence_step = level_to_transcendence(level)
|
||||||
|
series = raw_weapon.dig('master', 'series_id')
|
||||||
|
weapon_id = raw_weapon.dig('master', 'id')
|
||||||
|
|
||||||
|
processed_weapon_id = if Weapon.element_changeable?(series)
|
||||||
|
process_elemental_weapon(weapon_id)
|
||||||
|
else
|
||||||
|
weapon_id
|
||||||
|
end
|
||||||
|
|
||||||
|
processed_element = if Weapon.element_changeable?(series)
|
||||||
|
ELEMENT_MAPPING[raw_weapon.dig('master', 'attribute')]
|
||||||
|
end
|
||||||
|
|
||||||
|
weapon = Weapon.find_by(granblue_id: processed_weapon_id)
|
||||||
|
|
||||||
|
unless weapon
|
||||||
|
Rails.logger.error "[WEAPON] Weapon not found with id #{processed_weapon_id}"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
grid_weapon = GridWeapon.new(
|
||||||
|
party: @party,
|
||||||
|
weapon: weapon,
|
||||||
|
position: position,
|
||||||
|
mainhand: mainhand,
|
||||||
|
uncap_level: uncap_level,
|
||||||
|
transcendence_step: transcendence_step,
|
||||||
|
element: processed_element
|
||||||
|
)
|
||||||
|
|
||||||
|
arousal_data = raw_weapon.dig('param', 'arousal')
|
||||||
|
if arousal_data && arousal_data['is_arousal_weapon']
|
||||||
|
grid_weapon.awakening_id = map_arousal_to_awakening(arousal_data)
|
||||||
|
grid_weapon.awakening_level = arousal_data['level'].to_i.positive? ? arousal_data['level'].to_i : 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extract skill IDs and convert into weapon keys
|
||||||
|
skill_ids = [raw_weapon['skill1'], raw_weapon['skill2'], raw_weapon['skill3']].compact.map { |s| s['id'] }
|
||||||
|
process_weapon_keys(grid_weapon, skill_ids) if skill_ids.length.positive?
|
||||||
|
|
||||||
|
if raw_weapon.dig('param', 'augment_skill_info').present?
|
||||||
|
process_weapon_ax(grid_weapon, raw_weapon.dig('param', 'augment_skill_info'))
|
||||||
|
end
|
||||||
|
|
||||||
|
grid_weapon
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Converts a given weapon level to a transcendence step.
|
||||||
|
#
|
||||||
|
# If the level is less than 200, returns 0; otherwise, floors the level
|
||||||
|
# to the nearest 10 and returns its index in TRANSCENDENCE_LEVELS.
|
||||||
|
#
|
||||||
|
# @param level [Integer] the weapon’s level.
|
||||||
|
# @return [Integer] the transcendence step.
|
||||||
|
def level_to_transcendence(level)
|
||||||
|
return 0 if level < 200
|
||||||
|
|
||||||
|
floored_level = (level / 10).floor * 10
|
||||||
|
TRANSCENDENCE_LEVELS.index(floored_level) || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes weapon key data and assigns them to the grid_weapon.
|
||||||
|
#
|
||||||
|
# @param grid_weapon [GridWeapon] the grid weapon record being built.
|
||||||
|
# @param skill_ids [Array<String>] an array of key identifiers.
|
||||||
|
# @return [void]
|
||||||
|
def process_weapon_keys(grid_weapon, skill_ids)
|
||||||
|
series = grid_weapon.weapon.series.to_i
|
||||||
|
|
||||||
|
skill_ids.each_with_index do |skill_id, idx|
|
||||||
|
# Go to the next iteration unless the key under which `skill_id` exists
|
||||||
|
mapping_pair = KEY_MAPPING.find { |key, value| Array(value).include?(skill_id) }
|
||||||
|
next unless mapping_pair
|
||||||
|
|
||||||
|
# Fetch the key from the mapping_pair and find the weapon key based on the weapon series
|
||||||
|
mapping_value = mapping_pair.first
|
||||||
|
candidate = WeaponKey.where('granblue_id = ? AND ? = ANY(series)', mapping_value, series).first
|
||||||
|
|
||||||
|
if candidate
|
||||||
|
grid_weapon["weapon_key#{idx + 1}_id"] = candidate.id
|
||||||
|
else
|
||||||
|
Rails.logger.warn "[WEAPON] No matching WeaponKey found for raw key #{skill_id} using mapping #{mapping_value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns true if the candidate key (a string) matches the mapping entry.
|
||||||
|
#
|
||||||
|
# If mapping_entry includes a dash, it is interpreted as a range (e.g. "697-706").
|
||||||
|
# Otherwise, it must match exactly.
|
||||||
|
#
|
||||||
|
# @param candidate_key [String] the candidate WeaponKey.granblue_id.
|
||||||
|
# @param mapping_entry [String] the mapping entry.
|
||||||
|
# @return [Boolean]
|
||||||
|
def matches_key?(candidate_key, mapping_entry)
|
||||||
|
if mapping_entry.include?('-')
|
||||||
|
left, right = mapping_entry.split('-').map(&:to_i)
|
||||||
|
candidate_key.to_i >= left && candidate_key.to_i <= right
|
||||||
|
else
|
||||||
|
candidate_key == mapping_entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes AX (augment) skill data.
|
||||||
|
#
|
||||||
|
# The deck stores AX skills in an array of arrays under "augment_skill_info".
|
||||||
|
# This method flattens the data and assigns each skill’s modifier and strength.
|
||||||
|
#
|
||||||
|
# @param grid_weapon [GridWeapon] the grid weapon record being built.
|
||||||
|
# @param ax_skill_info [Array] the raw AX skill info.
|
||||||
|
# @return [void]
|
||||||
|
def process_weapon_ax(grid_weapon, ax_skill_info)
|
||||||
|
# Flatten the nested array structure.
|
||||||
|
ax_skills = ax_skill_info.flatten
|
||||||
|
ax_skills.each_with_index do |ax, idx|
|
||||||
|
ax_id = ax['skill_id'].to_s
|
||||||
|
ax_mod = AX_MAPPING[ax_id] || ax_id.to_i
|
||||||
|
strength = ax['effect_value'].to_s.gsub(/[+%]/, '').to_i
|
||||||
|
grid_weapon["ax_modifier#{idx + 1}"] = ax_mod
|
||||||
|
grid_weapon["ax_strength#{idx + 1}"] = strength
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Maps the in‑game awakening data (stored under "arousal") to our Awakening record.
|
||||||
|
#
|
||||||
|
# This method looks at the "skill" array inside the arousal data and uses the first
|
||||||
|
# awakening’s skill_id to find the corresponding Awakening record.
|
||||||
|
#
|
||||||
|
# @param arousal_data [Hash] the raw arousal (awakening) data.
|
||||||
|
# @return [String, nil] the database awakening id or nil if not found.
|
||||||
|
def map_arousal_to_awakening(arousal_data)
|
||||||
|
raw_data = arousal_data.with_indifferent_access
|
||||||
|
|
||||||
|
return nil if raw_data.nil?
|
||||||
|
return nil unless raw_data.is_a?(Hash)
|
||||||
|
return nil unless raw_data.has_key?('form')
|
||||||
|
|
||||||
|
id = (raw_data['form']).to_s
|
||||||
|
return unless AWAKENING_MAPPING.key?(id)
|
||||||
|
|
||||||
|
slug = AWAKENING_MAPPING[id]
|
||||||
|
awakening = Awakening.find_by(slug: slug)
|
||||||
|
|
||||||
|
awakening&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_elemental_weapon(granblue_id)
|
||||||
|
granblue_int = granblue_id.to_i
|
||||||
|
|
||||||
|
# Find the index of the first element that is >= granblue_int.
|
||||||
|
idx = ELEMENTAL_WEAPON_MAPPING_INT.bsearch_index { |x| x >= granblue_int }
|
||||||
|
|
||||||
|
# We'll check the candidate at idx and the one immediately before it.
|
||||||
|
candidates = []
|
||||||
|
if idx
|
||||||
|
candidate = ELEMENTAL_WEAPON_MAPPING_INT[idx]
|
||||||
|
candidates << candidate if (granblue_int - candidate).abs <= 500
|
||||||
|
# Check the candidate just before, if it exists.
|
||||||
|
if idx > 0
|
||||||
|
candidate_prev = ELEMENTAL_WEAPON_MAPPING_INT[idx - 1]
|
||||||
|
candidates << candidate_prev if (granblue_int - candidate_prev).abs <= 500
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# If idx is nil, then granblue_int is greater than all mapped values.
|
||||||
|
candidate = ELEMENTAL_WEAPON_MAPPING_INT.last
|
||||||
|
candidates << candidate if (granblue_int - candidate).abs <= 500
|
||||||
|
end
|
||||||
|
|
||||||
|
# If no candidate is close enough, return the original input.
|
||||||
|
return granblue_id if candidates.empty?
|
||||||
|
|
||||||
|
# Choose the candidate with the smallest difference.
|
||||||
|
best_match = candidates.min_by { |x| (granblue_int - x).abs }
|
||||||
|
best_match.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
134
db/data/20250218023335_migrate_weapon_series.rb
Normal file
134
db/data/20250218023335_migrate_weapon_series.rb
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MigrateWeaponSeries < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
Weapon.transaction do
|
||||||
|
puts 'Starting weapon series migration...'
|
||||||
|
|
||||||
|
puts 'Updating Seraphic Weapons (0 -> 1)...'
|
||||||
|
Weapon.find_by(series: 0).update!(new_series: 1)
|
||||||
|
|
||||||
|
puts 'Updating Grand Weapons (1 -> 2)...'
|
||||||
|
Weapon.find_by(series: 1).update!(new_series: 2)
|
||||||
|
|
||||||
|
puts 'Updating Dark Opus Weapons (2 -> 3)...'
|
||||||
|
Weapon.find_by(series: 2).update!(new_series: 3)
|
||||||
|
|
||||||
|
puts 'Updating Revenant Weapons (4 -> 4)...'
|
||||||
|
Weapon.find_by(series: 4).update!(new_series: 4)
|
||||||
|
|
||||||
|
puts 'Updating Primal Weapons (6 -> 5)...'
|
||||||
|
Weapon.find_by(series: 6).update!(new_series: 5)
|
||||||
|
|
||||||
|
puts 'Updating Beast Weapons (5, 7 -> 6)...'
|
||||||
|
Weapon.find_by(series: 5).update!(new_series: 6)
|
||||||
|
Weapon.find_by(series: 7).update!(new_series: 6)
|
||||||
|
|
||||||
|
puts 'Updating Regalia Weapons (8 -> 7)...'
|
||||||
|
Weapon.find_by(series: 8).update!(new_series: 7)
|
||||||
|
|
||||||
|
puts 'Updating Omega Weapons (9 -> 8)...'
|
||||||
|
Weapon.find_by(series: 9).update!(new_series: 8)
|
||||||
|
|
||||||
|
puts 'Updating Olden Primal Weapons (10 -> 9)...'
|
||||||
|
Weapon.find_by(series: 10).update!(new_series: 9)
|
||||||
|
|
||||||
|
puts 'Updating Hollowsky Weapons (12 -> 10)...'
|
||||||
|
Weapon.find_by(series: 12).update!(new_series: 10)
|
||||||
|
|
||||||
|
puts 'Updating Xeno Weapons (13 -> 11)...'
|
||||||
|
Weapon.find_by(series: 13).update!(new_series: 11)
|
||||||
|
|
||||||
|
puts 'Updating Rose Weapons (15 -> 12)...'
|
||||||
|
Weapon.find_by(series: 15).update!(new_series: 12)
|
||||||
|
|
||||||
|
puts 'Updating Ultima Weapons (17 -> 13)...'
|
||||||
|
Weapon.find_by(series: 17).update!(new_series: 13)
|
||||||
|
|
||||||
|
puts 'Updating Bahamut Weapons (16 -> 14)...'
|
||||||
|
Weapon.find_by(series: 16).update!(new_series: 14)
|
||||||
|
|
||||||
|
puts 'Updating Epic Weapons (18 -> 15)...'
|
||||||
|
Weapon.find_by(series: 18).update!(new_series: 15)
|
||||||
|
|
||||||
|
puts 'Updating Cosmos Weapons (20 -> 16)...'
|
||||||
|
Weapon.find_by(series: 20).update!(new_series: 16)
|
||||||
|
|
||||||
|
puts 'Updating Superlative Weapons (22 -> 17)...'
|
||||||
|
Weapon.find_by(series: 22).update!(new_series: 17)
|
||||||
|
|
||||||
|
puts 'Updating Vintage Weapons (23 -> 18)...'
|
||||||
|
Weapon.find_by(series: 23).update!(new_series: 18)
|
||||||
|
|
||||||
|
puts 'Updating Class Champion Weapons (24 -> 19)...'
|
||||||
|
Weapon.find_by(series: 24).update!(new_series: 19)
|
||||||
|
|
||||||
|
puts 'Updating Sephira Weapons (28 -> 23)...'
|
||||||
|
Weapon.find_by(series: 28).update!(new_series: 23)
|
||||||
|
|
||||||
|
puts 'Updating Astral Weapons (14 -> 26)...'
|
||||||
|
Weapon.find_by(series: 14).update!(new_series: 26)
|
||||||
|
|
||||||
|
puts 'Updating Draconic Weapons (3 -> 27)...'
|
||||||
|
Weapon.find_by(series: 3).update!(new_series: 27)
|
||||||
|
|
||||||
|
puts 'Updating Ancestral Weapons (21 -> 29)...'
|
||||||
|
Weapon.find_by(series: 21).update!(new_series: 29)
|
||||||
|
|
||||||
|
puts 'Updating New World Foundation (29 -> 30)...'
|
||||||
|
Weapon.find_by(series: 29).update!(new_series: 30)
|
||||||
|
|
||||||
|
puts 'Updating Ennead Weapons (19 -> 31)...'
|
||||||
|
Weapon.find_by(series: 19).update!(new_series: 31)
|
||||||
|
|
||||||
|
puts 'Updating Militis Weapons (11 -> 32)...'
|
||||||
|
Weapon.find_by(series: 11).update!(new_series: 32)
|
||||||
|
|
||||||
|
puts 'Updating Malice Weapons (26 -> 33)...'
|
||||||
|
Weapon.find_by(series: 26).update!(new_series: 33)
|
||||||
|
|
||||||
|
puts 'Updating Menace Weapons (26 -> 34)...'
|
||||||
|
Weapon.find_by(series: 26).update!(new_series: 34)
|
||||||
|
|
||||||
|
puts 'Updating Illustrious Weapons (31 -> 35)...'
|
||||||
|
Weapon.find_by(series: 31).update!(new_series: 35)
|
||||||
|
|
||||||
|
puts 'Updating Proven Weapons (25 -> 36)...'
|
||||||
|
Weapon.find_by(series: 25).update!(new_series: 36)
|
||||||
|
|
||||||
|
puts 'Updating Revans Weapons (30 -> 37)...'
|
||||||
|
Weapon.find_by(series: 30).update!(new_series: 37)
|
||||||
|
|
||||||
|
puts 'Updating World Weapons (32 -> 38)...'
|
||||||
|
Weapon.find_by(series: 32).update!(new_series: 38)
|
||||||
|
|
||||||
|
puts 'Updating Exo Weapons (33 -> 39)...'
|
||||||
|
Weapon.find_by(series: 33).update!(new_series: 39)
|
||||||
|
|
||||||
|
puts 'Updating Draconic Weapons Providence (34 -> 40)...'
|
||||||
|
Weapon.find_by(series: 34).update!(new_series: 40)
|
||||||
|
|
||||||
|
puts 'Updating Celestial Weapons (37 -> 41)...'
|
||||||
|
Weapon.find_by(series: 37).update!(new_series: 41)
|
||||||
|
|
||||||
|
puts 'Updating Omega Rebirth Weapons (38 -> 42)...'
|
||||||
|
Weapon.find_by(series: 38).update!(new_series: 42)
|
||||||
|
|
||||||
|
puts 'Updating Event Weapons (34 -> 98)...'
|
||||||
|
Weapon.find_by(series: 34).update!(new_series: 98) # Event
|
||||||
|
|
||||||
|
puts 'Updating Gacha Weapons (36 -> 99)...'
|
||||||
|
Weapon.find_by(series: 36).update!(new_series: 99) # Gacha
|
||||||
|
|
||||||
|
puts 'Migration completed successfully!'
|
||||||
|
rescue StandardError => e
|
||||||
|
puts "Error occurred during migration: #{e.message}"
|
||||||
|
puts "Backtrace: #{e.backtrace}"
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
||||||
37
db/data/20250218025755_migrate_series_on_weapon_key.rb
Normal file
37
db/data/20250218025755_migrate_series_on_weapon_key.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MigrateSeriesOnWeaponKey < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
WeaponKey.transaction do
|
||||||
|
puts 'Starting weapon key series migration...'
|
||||||
|
|
||||||
|
puts 'Updating Telumas (3 -> 27)...'
|
||||||
|
WeaponKey.where('? = ANY(series)', 3).update_all('series = array_replace(series, 3, 27)')
|
||||||
|
|
||||||
|
puts 'Updating Providence Telumas (34 -> 40)...'
|
||||||
|
WeaponKey.where('? = ANY(series)', 34).update_all('series = array_replace(series, 34, 40)')
|
||||||
|
|
||||||
|
puts 'Updating Gauph Keys (17 -> 13)...'
|
||||||
|
WeaponKey.where('? = ANY(series)', 17).update_all('series = array_replace(series, 17, 13)')
|
||||||
|
|
||||||
|
puts 'Updating Pendulums (2 -> 3)...'
|
||||||
|
WeaponKey.where('? = ANY(series)', 2).update_all('series = array_replace(series, 2, 3)')
|
||||||
|
|
||||||
|
puts 'Updating Chains (2 -> 3)...'
|
||||||
|
WeaponKey.where('? = ANY(series)', 2).update_all('series = array_replace(series, 2, 3)')
|
||||||
|
|
||||||
|
puts 'Updating Emblems (24 -> 19)...'
|
||||||
|
WeaponKey.where('? = ANY(series)', 24).update_all('series = array_replace(series, 24, 19)')
|
||||||
|
|
||||||
|
puts 'Migration completed successfully!'
|
||||||
|
rescue StandardError => e
|
||||||
|
puts "Error occurred during migration: #{e.message}"
|
||||||
|
puts "Backtrace: #{e.backtrace}"
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1 +1 @@
|
||||||
DataMigrate::Data.define(version: 20250115094623)
|
DataMigrate::Data.define(version: 20250218025755)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
class MakeJobForeignKeyDeferrable < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
remove_foreign_key :jobs, column: :base_job_id
|
||||||
|
add_foreign_key :jobs, :jobs, column: :base_job_id, deferrable: :deferred, initially_deferred: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
class RemoveForeignKeyConstraintOnJobsBaseJobId < ActiveRecord::Migration[8.0]
|
||||||
|
# Removes the self-referential foreign key constraint on jobs.base_job_id.
|
||||||
|
# This constraint was causing issues when seeding job records via CSV.
|
||||||
|
def change
|
||||||
|
# Check if the foreign key exists before removing it
|
||||||
|
if foreign_key_exists?(:jobs, column: :base_job_id)
|
||||||
|
remove_foreign_key :jobs, column: :base_job_id
|
||||||
|
Rails.logger.info 'Removed foreign key constraint on jobs.base_job_id'
|
||||||
|
else
|
||||||
|
Rails.logger.info 'No foreign key on jobs.base_job_id found'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20250218023315_add_new_series_to_weapons.rb
Normal file
5
db/migrate/20250218023315_add_new_series_to_weapons.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddNewSeriesToWeapons < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :weapons, :new_series, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
6
db/migrate/20250218025315_move_new_series_to_series.rb
Normal file
6
db/migrate/20250218025315_move_new_series_to_series.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
class MoveNewSeriesToSeries < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
remove_column :weapons, :series
|
||||||
|
rename_column :weapons, :new_series, :series
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_02_01_170037) do
|
ActiveRecord::Schema[8.0].define(version: 2025_02_18_025315) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
@ -465,7 +465,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_01_170037) do
|
||||||
t.integer "rarity"
|
t.integer "rarity"
|
||||||
t.integer "element"
|
t.integer "element"
|
||||||
t.integer "proficiency"
|
t.integer "proficiency"
|
||||||
t.integer "series", default: -1, null: false
|
|
||||||
t.boolean "flb", default: false, null: false
|
t.boolean "flb", default: false, null: false
|
||||||
t.boolean "ulb", default: false, null: false
|
t.boolean "ulb", default: false, null: false
|
||||||
t.integer "max_level", default: 100, null: false
|
t.integer "max_level", default: 100, null: false
|
||||||
|
|
@ -495,6 +494,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_01_170037) do
|
||||||
t.boolean "transcendence", default: false
|
t.boolean "transcendence", default: false
|
||||||
t.date "transcendence_date"
|
t.date "transcendence_date"
|
||||||
t.string "recruits"
|
t.string "recruits"
|
||||||
|
t.integer "series"
|
||||||
t.index ["granblue_id"], name: "index_weapons_on_granblue_id"
|
t.index ["granblue_id"], name: "index_weapons_on_granblue_id"
|
||||||
t.index ["name_en"], name: "index_weapons_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
t.index ["name_en"], name: "index_weapons_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
||||||
t.index ["recruits"], name: "index_weapons_on_recruits"
|
t.index ["recruits"], name: "index_weapons_on_recruits"
|
||||||
|
|
@ -511,7 +511,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_01_170037) do
|
||||||
add_foreign_key "grid_weapons", "parties"
|
add_foreign_key "grid_weapons", "parties"
|
||||||
add_foreign_key "grid_weapons", "weapon_keys", column: "weapon_key3_id"
|
add_foreign_key "grid_weapons", "weapon_keys", column: "weapon_key3_id"
|
||||||
add_foreign_key "grid_weapons", "weapons"
|
add_foreign_key "grid_weapons", "weapons"
|
||||||
add_foreign_key "jobs", "jobs", column: "base_job_id"
|
|
||||||
add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id"
|
add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id"
|
||||||
add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"
|
add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"
|
||||||
add_foreign_key "parties", "guidebooks", column: "guidebook1_id"
|
add_foreign_key "parties", "guidebooks", column: "guidebook1_id"
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,98 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# canonical.rb - Loads canonical seed data from CSV files into the database.
|
||||||
|
#
|
||||||
|
# This file is used to load canonical data for various models from CSV files
|
||||||
|
# located in db/seed/test. For models that reference other models by fixed IDs
|
||||||
|
# (e.g. Job, Guidebook, etc.), use the `use_id: true` option to preserve the CSV
|
||||||
|
# provided IDs (so that inter-model references remain correct).
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# load_csv_for(Character, 'characters_test.csv', :granblue_id)
|
||||||
|
#
|
||||||
|
# # For objects that need to preserve the CSV "id" column:
|
||||||
|
# load_csv_for(Job, 'jobs_test.csv', :granblue_id, use_id: true)
|
||||||
|
#
|
||||||
require 'csv'
|
require 'csv'
|
||||||
|
|
||||||
# Helper: Process boolean columns.
|
##
|
||||||
|
# Processes specified columns in an attributes hash to booleans.
|
||||||
|
#
|
||||||
|
# @param attrs [Hash] The attributes hash.
|
||||||
|
# @param columns [Array<Symbol>] The list of columns to cast to boolean.
|
||||||
def process_booleans(attrs, columns)
|
def process_booleans(attrs, columns)
|
||||||
columns.each do |col|
|
columns.each do |col|
|
||||||
next unless attrs.key?(col) && attrs[col].present?
|
next unless attrs.key?(col) && attrs[col].present?
|
||||||
|
# Use ActiveModel::Type::Boolean to cast the value.
|
||||||
attrs[col] = ActiveModel::Type::Boolean.new.cast(attrs[col])
|
attrs[col] = ActiveModel::Type::Boolean.new.cast(attrs[col])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper: Process date columns.
|
##
|
||||||
|
# Processes specified columns in an attributes hash to dates.
|
||||||
|
#
|
||||||
|
# @param attrs [Hash] The attributes hash.
|
||||||
|
# @param columns [Array<Symbol>] The list of columns to parse as dates.
|
||||||
def process_dates(attrs, columns)
|
def process_dates(attrs, columns)
|
||||||
columns.each do |col|
|
columns.each do |col|
|
||||||
next unless attrs.key?(col) && attrs[col].present?
|
next unless attrs.key?(col) && attrs[col].present?
|
||||||
|
# Parse the date, or assign nil if parsing fails.
|
||||||
attrs[col] = Date.parse(attrs[col]) rescue nil
|
attrs[col] = Date.parse(attrs[col]) rescue nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Simplified CSV loader for a given model.
|
##
|
||||||
def load_csv_for(model_class, csv_filename, unique_key = :granblue_id)
|
# Loads CSV data for the given model class.
|
||||||
|
#
|
||||||
|
# Reads a CSV file from the db/seed/test directory and uses the given unique_key
|
||||||
|
# to determine whether a record already exists. If the record exists, its attributes
|
||||||
|
# are not overwritten; otherwise, a new record is created.
|
||||||
|
#
|
||||||
|
# @param model_class [Class] The ActiveRecord model class to load data for.
|
||||||
|
# @param csv_filename [String] The CSV filename (located in db/seed/test).
|
||||||
|
# @param unique_key [Symbol] The attribute used to uniquely identify a record (default: :granblue_id).
|
||||||
|
# @param use_id [Boolean] If true, preserves the CSV id field instead of removing it (default: false).
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def load_csv_for(model_class, csv_filename, unique_key = :granblue_id, use_id: false)
|
||||||
csv_file = Rails.root.join('db', 'seed', 'test', csv_filename)
|
csv_file = Rails.root.join('db', 'seed', 'test', csv_filename)
|
||||||
puts "Loading #{model_class.name} data from #{csv_file}..."
|
# puts "Loading #{model_class.name} data from #{csv_file}..."
|
||||||
|
|
||||||
CSV.foreach(csv_file, headers: true) do |row|
|
CSV.foreach(csv_file, headers: true) do |row|
|
||||||
|
# Convert CSV row to a hash with symbolized keys.
|
||||||
attrs = row.to_hash.symbolize_keys
|
attrs = row.to_hash.symbolize_keys
|
||||||
|
|
||||||
|
# Process known boolean columns.
|
||||||
process_booleans(attrs, %i[flb ulb subaura limit transcendence])
|
process_booleans(attrs, %i[flb ulb subaura limit transcendence])
|
||||||
process_dates(attrs, %i[release_date flb_date ulb_date transcendence_date])
|
# Process known date columns. Extend this list as needed.
|
||||||
|
process_dates(attrs, %i[release_date flb_date ulb_date transcendence_date created_at])
|
||||||
|
|
||||||
|
# Clean up attribute values: trim whitespace and convert empty strings to nil.
|
||||||
attrs.each { |k, v| attrs[k] = nil if v.is_a?(String) && v.strip.empty? }
|
attrs.each { |k, v| attrs[k] = nil if v.is_a?(String) && v.strip.empty? }
|
||||||
attrs.except!(:id)
|
|
||||||
model_class.find_or_create_by!(unique_key => attrs[unique_key]) do |r|
|
# Remove the :id attribute unless we want to preserve it (for fixed canonical IDs).
|
||||||
|
attrs.except!(:id) unless use_id
|
||||||
|
|
||||||
|
# Find or create the record based on the unique key.
|
||||||
|
record = model_class.find_or_create_by!(unique_key => attrs[unique_key]) do |r|
|
||||||
|
# Assign all attributes except the unique_key.
|
||||||
r.assign_attributes(attrs.except(unique_key))
|
r.assign_attributes(attrs.except(unique_key))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# puts "Loaded #{model_class.name}: #{record.public_send(unique_key)}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load canonical data for each model.
|
# Load canonical data for core models.
|
||||||
load_csv_for(Awakening, 'awakenings_test.csv', :slug)
|
load_csv_for(Awakening, 'awakenings_test.csv', :id, use_id: true)
|
||||||
load_csv_for(Summon, 'summons_test.csv', :granblue_id)
|
load_csv_for(Summon, 'summons_test.csv', :id, use_id: true)
|
||||||
load_csv_for(Weapon, 'weapons_test.csv', :granblue_id)
|
load_csv_for(Weapon, 'weapons_test.csv', :id, use_id: true)
|
||||||
load_csv_for(Character, 'characters_test.csv', :granblue_id)
|
load_csv_for(Character, 'characters_test.csv', :id, use_id: true)
|
||||||
|
|
||||||
|
# Load additional canonical data that require preserving the provided IDs.
|
||||||
|
load_csv_for(Job, 'jobs_test.csv', :id, use_id: true)
|
||||||
|
load_csv_for(Guidebook, 'guidebooks_test.csv', :id, use_id: true)
|
||||||
|
load_csv_for(JobAccessory, 'job_accessories_test.csv', :id, use_id: true)
|
||||||
|
load_csv_for(JobSkill, 'job_skills_test.csv', :id, use_id: true)
|
||||||
|
load_csv_for(WeaponAwakening, 'weapon_awakenings_test.csv', :id, use_id: true)
|
||||||
|
load_csv_for(WeaponKey, 'weapon_keys_test.csv', :id, use_id: true)
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,14 @@
|
||||||
"d5fb1b79-483f-44cf-8437-92ce31f5f2b2","Rosamia (SR)","ロザミア(SR)","3030049000","2014-12-31","Rosamia (SR)","%A5%ED%A5%B6%A5%DF%A5%A2%20%28SR%29",2,6,1,,2,1,,FALSE,260,1300,,260,1300,,7,3,3.5,,FALSE,FALSE,,,"{1018}","{}","{}",,,"21090","SR/ロザミア"
|
"d5fb1b79-483f-44cf-8437-92ce31f5f2b2","Rosamia (SR)","ロザミア(SR)","3030049000","2014-12-31","Rosamia (SR)","%A5%ED%A5%B6%A5%DF%A5%A2%20%28SR%29",2,6,1,,2,1,,FALSE,260,1300,,260,1300,,7,3,3.5,,FALSE,FALSE,,,"{1018}","{}","{}",,,"21090","SR/ロザミア"
|
||||||
"24bf1c09-509f-4db1-b953-b95ebcc69fb9","Seofon","シエテ","3040036000","2015-04-16","Seofon","%A5%B7%A5%A8%A5%C6%20%28SSR%29",3,1,1,,1,1,,TRUE,237,1477,1777,1777,10777,12777,10,5,4.5,5,FALSE,TRUE,14777,1977,"{4007}","{siete}","{}","2017-03-20","2021-06-29","21117","SSR/シエテ"
|
"24bf1c09-509f-4db1-b953-b95ebcc69fb9","Seofon","シエテ","3040036000","2015-04-16","Seofon","%A5%B7%A5%A8%A5%C6%20%28SSR%29",3,1,1,,1,1,,TRUE,237,1477,1777,1777,10777,12777,10,5,4.5,5,FALSE,TRUE,14777,1977,"{4007}","{siete}","{}","2017-03-20","2021-06-29","21117","SSR/シエテ"
|
||||||
"b1eae4fe-e35c-44da-aa4f-7ca1c3e5863f","Zeta","ゼタ","3040028000","2014-12-31","Zeta","%A5%BC%A5%BF%20%28SSR%29",3,2,4,,2,1,,TRUE,240,1280,1520,240,1280,1520,10,5,5,,FALSE,FALSE,,,"{3024}","{}","{}","2020-01-28",,"21290","SSR/ゼタ"
|
"b1eae4fe-e35c-44da-aa4f-7ca1c3e5863f","Zeta","ゼタ","3040028000","2014-12-31","Zeta","%A5%BC%A5%BF%20%28SSR%29",3,2,4,,2,1,,TRUE,240,1280,1520,240,1280,1520,10,5,5,,FALSE,FALSE,,,"{3024}","{}","{}","2020-01-28",,"21290","SSR/ゼタ"
|
||||||
|
"03b0bee9-e75b-4d5f-9ffc-0ad8cc2df7f0","Percival (Grand)","パーシヴァル(リミテッドver)","3040425000","2022-09-30","Percival (Grand)","%A5%D1%A1%BC%A5%B7%A5%F4%A5%A1%A5%EB%20%28SSR%29%A5%EA%A5%DF%A5%C6%A5%C3%A5%C9%A5%D0%A1%BC%A5%B8%A5%E7%A5%F3",3,2,1,,1,1,,FALSE,230,1340,,2010,10120,,,,,,FALSE,FALSE,,,"{3042}","{}","{}",,,"366171","SSR/リミテッドパーシヴァル"
|
||||||
|
"c96d4ba1-8a99-49fd-acde-d8eec45f6161","Fenie (Grand)","フェニー(リミテッドver)","3040519000","2024-03-15","Fenie (Grand)","",3,2,6,,2,0,,FALSE,294,1724,,1380,6580,,,,,,FALSE,FALSE,,,"{3246}","{}","{}",,,"",""
|
||||||
|
"3e7c163c-c92f-404e-8ec1-fe73bce6d6c3","Alanaan","アラナン","3040167000","2019-03-10","Alanaan","%A5%A2%A5%E9%A5%CA%A5%F3%20%28SSR%29",3,2,6,,1,2,,TRUE,219,1319,1519,1605,9705,11305,10,5,4.5,,FALSE,FALSE,,,"{3106}","{}","{}",,,"144742","SSR/アラナン"
|
||||||
|
"427f3e8a-8148-4b76-8982-f6a625a0f1e6","Zeta (Grand)","ゼタ(リミテッドver)","3040499000","2023-12-28","Zeta (Grand)","",3,2,4,,2,1,,FALSE,,1100,,,10500,,,,,,FALSE,FALSE,,,"{3024}","{}","{}",,,"",""
|
||||||
|
"437ddfde-7c39-469f-b75e-102f30595880","Fraux","フラウ","3040161000","2019-03-10","Fraux","%A5%D5%A5%E9%A5%A6%20%28SSR%29",3,2,7,,2,2,,TRUE,215,1315,,1608,9808,,10,5,4.5,,FALSE,FALSE,,,"{3100}","{}","{}","2023-08-16",,"144749","SSR/フラウ"
|
||||||
|
"76fe3ab2-e192-42f5-b063-920a2e406fb4","Michael","ミカエル","3040440000","2022-12-31","Michael","%A5%DF%A5%AB%A5%A8%A5%EB%20%28SSR%29",3,2,1,,2,5,,FALSE,240,1256,,2200,11320,,,,,,FALSE,FALSE,,,"{3217}","{}","{}",,,"381021","SSR/ミカエル"
|
||||||
|
"336f11a7-35b7-4a69-8041-c747a0c10b53","Fediel","フェディエル(リミテッドver)","3040376000","2021-12-31","Fediel","%A5%D5%A5%A7%A5%C7%A5%A3%A5%A8%A5%EB%20%28SSR%29",3,5,1,7,0,3,,FALSE,224,1200,,2015,10720,,,,,,FALSE,FALSE,,,"{3191}","{}","{}",,,"311659","SSR/フェディエル"
|
||||||
|
"180527e3-58ad-4e90-91ed-c70fa91798f7","Tikoh","ティコ","3040337000","2021-05-18","Tikoh","%A5%C6%A5%A3%A5%B3%20%28SSR%29",3,6,6,,2,2,,FALSE,367,1794,,730,4710,,,,,,FALSE,FALSE,,,"{3179}","{}","{}",,,"277461","SSR/ティコ"
|
||||||
|
"83ef5ef3-5180-465b-981e-6a121894aaec","Halluel and Malluel","ハールート・マールート(リミテッドver)","3040443000","2023-01-19","Halluel and Malluel","%A5%CF%A1%BC%A5%EB%A1%BC%A5%C8%A1%A6%A5%DE%A1%BC%A5%EB%A1%BC%A5%C8%20%28SSR%29",3,5,2,,2,5,,FALSE,290,800,,1500,4400,,,,,,FALSE,FALSE,,,"{3138}","{}","{}",,,"384939","SSR/リミテッドハールートマールート"
|
||||||
|
"8dbebe0d-12ed-4334-b3d7-8f516b8b2e23","Lich","リッチ(リミテッドver)","3040357000","2021-09-15","Lich","%A5%EA%A5%C3%A5%C1%20%28SSR%29",3,5,6,,2,5,,FALSE,260,1300,,1550,8600,,,,,,FALSE,FALSE,,,"{3184}","{}","{}",,,"294327","SSR/リッチ"
|
||||||
|
"e9bb4639-d4f2-4299-b3ed-d396760a30eb","Nier","ニーア","3040169000","2019-03-10","Nier","%A5%CB%A1%BC%A5%A2%20%28SSR%29",3,5,3,2,2,2,,TRUE,200,1313,1513,1476,8906,10306,7,3,4.5,,FALSE,FALSE,,,"{3108}","{}","{}","2023-06-07",,"144747","SSR/ニーア"
|
||||||
|
|
|
||||||
|
9
db/seed/test/guidebooks_test.csv
Normal file
9
db/seed/test/guidebooks_test.csv
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"id","granblue_id","name_en","name_jp","description_en","description_jp","created_at"
|
||||||
|
"3905ccba-fc56-44ef-890d-94b858ded339","6","Acuity's Guidebook","鋭撃の導本","Grants Stamina and more","渾身効果などを得られる","2023-04-17 23:19:19.425728"
|
||||||
|
"4285b593-31ff-45e3-bd96-55c972199753","8","Insight's Guidebook","啓示の導本","Improves debuff resistance and success rate","弱体効果に強くなる","2023-04-17 23:19:19.425728"
|
||||||
|
"794b2e5f-9eec-4d27-93ee-c7971eb25862","16","Shockwave's Guidebook","激震の導本","Greatly improves normal attacks","通常攻撃を大幅に強化する効果が得られる","2023-04-17 23:19:19.425728"
|
||||||
|
"8453e4e8-1c86-4a92-a164-41762e5f5e49","5","Tenebrosity's Guidebook","暗夜の導本","Improves multiattack rate and more","連続攻撃確率アップなどの効果が得られる","2023-04-17 23:19:19.425728"
|
||||||
|
"a35af3f7-3e37-46f5-9fef-615819b8492b","4","Valor's Guidebook","勇気の導本","Grant Bonus DMG effect and more","追撃などの効果が得られる","2023-04-17 23:19:19.425728"
|
||||||
|
"a9313de5-092c-4f72-a5bb-e7f09f550961","7","Fortitude's Guidebook","守護者の導本","Greatly improves survivability","耐久効果を多く得られる","2023-04-17 23:19:19.425728"
|
||||||
|
"af73e2ad-aae4-47dc-8f4e-c9c0d4225a84","15","Sanctum's Guidebook","聖域の導本","Greatly improves battle longevity","継戦能力が大きく高まる効果を得られる","2023-04-17 23:19:19.425728"
|
||||||
|
"bbd6368d-567c-4d23-aa75-c2fe2c6818ff","10","Adept's Guidebook","魔刃の導本","Improves skills","アビリティを強化する効果が得られる","2023-04-17 23:19:19.425728"
|
||||||
|
9
db/seed/test/job_accessories_test.csv
Normal file
9
db/seed/test/job_accessories_test.csv
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"id","job_id","name_en","name_jp","granblue_id","rarity","release_date","accessory_type"
|
||||||
|
"32295cc2-c1ed-4e1b-9273-baa79262bf66","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Bahamut Mino","バハムート・ミニステル","5",0,"2022-01-25",2
|
||||||
|
"32786311-6d8f-4b3b-99f7-7dd53343a0f3","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Yggdrasil Mino","ユグドラシル・ミニステル","4",0,"2022-01-25",2
|
||||||
|
"824c06c8-0d4c-485a-9cc6-3e72e58a5588","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Mini Mimic","ミニック","7",0,"2022-01-25",2
|
||||||
|
"8490d389-3f41-47f5-9ae5-c5bcf7f39965","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Lu Woh Mino","ル・オー・ミニステル","6",0,"2022-01-25",2
|
||||||
|
"a2cf6934-deab-4082-8eb8-6ec3c9c0d53e","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Ouroboros Mino","ウロボロス・ミニステル","8",3,"2022-09-06",2
|
||||||
|
"aee2ee5b-7847-45af-aab4-ba210a199bcb","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Leviathan Mino","リヴァイアサン・ミニステル","3",0,"2022-01-25",2
|
||||||
|
"af013d1b-cc40-43ec-9d34-3a0ea0592e52","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Burlona","ブルロネ","1",0,"2022-01-25",2
|
||||||
|
"dce5f041-b709-4cf4-aa71-bec44727d6ce","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Schalk","シャルク","2",0,"2022-01-25",2
|
||||||
|
18
db/seed/test/job_skills_test.csv
Normal file
18
db/seed/test/job_skills_test.csv
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"id","job_id","name_en","name_jp","slug","color","main","sub","emp","order","base"
|
||||||
|
"589d1718-887f-4837-9a7b-93e9ce33bbf3","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Aether Siphon","エーテルサクション","aether-siphon",2,TRUE,FALSE,FALSE,0,FALSE
|
||||||
|
"b0fa1cbd-1761-49f7-b250-d601a98fddac","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Wild Magica","ワイルドマギカ","wild-magica",2,FALSE,FALSE,TRUE,1,FALSE
|
||||||
|
"0cdd20ec-5869-4bff-8016-35a4a48e897a","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Secret Triad","シークレットトライアド","secret-triad",0,FALSE,FALSE,TRUE,2,FALSE
|
||||||
|
"a42211a5-e7fd-4cdb-80a9-f2fb3ccce7f2","a5d6fca3-5649-4e12-a6db-5fcec49150ee","Overtrance","オーバートランス","overtrance",0,FALSE,FALSE,TRUE,3,FALSE
|
||||||
|
"b0a50aec-6f88-4a0f-900a-c26e84fd09c6","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Full Arsenal III","ウェポンバーストIII","full-arsenal-iii",0,TRUE,FALSE,FALSE,0,FALSE
|
||||||
|
"fdfdee6d-6ead-4504-9add-a04776546b15","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Armor Break II","レイジIV","armor-break-ii",2,FALSE,FALSE,TRUE,1,FALSE
|
||||||
|
"4df00bf2-aab1-4bd4-a399-fcad942c7daf","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Rage IV","アーマーブレイクII","rage-iv",0,FALSE,FALSE,TRUE,2,FALSE
|
||||||
|
"e705ef94-4d70-4e24-b505-4a1e8b0038f0","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Rampage II","ランページII","rampage-ii",0,FALSE,FALSE,TRUE,3,FALSE
|
||||||
|
"30df2315-457a-414c-9eef-3980b72b17c2","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Ulfhedinn","ウールヴヘジン","ulfhedinn",0,FALSE,FALSE,TRUE,4,FALSE
|
||||||
|
"3b862283-c2b0-42ab-abf8-83f7b71d5fb5","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Ferocious Roar","フェロシティロアー","ferocious-roar",0,FALSE,FALSE,TRUE,5,FALSE
|
||||||
|
"a1491902-838f-4e7d-8a4a-572a5653537f","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Beast Fang","ビーストファング","beast-fang",2,FALSE,FALSE,TRUE,6,FALSE
|
||||||
|
"0d2987b1-2322-48b6-a071-e6b60699889b","1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Bloodzerker","狂戦の血","bloodzerker",0,FALSE,FALSE,TRUE,7,FALSE
|
||||||
|
"e76fbe2a-4dc7-4c29-9db7-7feee06559fb","43652444-64bb-4938-85d7-aafdfc503d66","Miserable Mist","ミゼラブルミスト","miserable-mist",1,TRUE,TRUE,FALSE,1,FALSE
|
||||||
|
"37218a55-3335-4457-98c3-4d8367af3d7c","d27a4f29-5f0b-4bc6-b75a-1bd187e1a529","Splitting Spirit","他心陣","splitting-spirit",0,TRUE,TRUE,FALSE,2,FALSE
|
||||||
|
"61e9a862-41f1-477a-9131-72b366c359be","2abbab55-5bf7-49f8-9ed6-1fe8a3991cca","Clarity","クリアオール","clarity",3,TRUE,TRUE,FALSE,1,FALSE
|
||||||
|
"4a00259a-9e2b-4239-bca2-2afdc2f52be7","c128944b-cc79-45b4-bfed-17c8b68db612","Dispel","ディスペル","dispel",1,TRUE,TRUE,FALSE,1,FALSE
|
||||||
|
"67a126d1-5515-492f-aeaf-7f88b25e2e26","667bf041-61c9-4568-bdd3-ce6e43f40603","Dark Haze","ブラックヘイズ","dark-haze",1,FALSE,FALSE,TRUE,1,FALSE
|
||||||
|
15
db/seed/test/jobs_test.csv
Normal file
15
db/seed/test/jobs_test.csv
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
"id","name_en","name_jp","proficiency1","proficiency2","row","master_level","order","base_job_id","granblue_id","accessory","accessory_type","ultimate_mastery"
|
||||||
|
"67899c05-e73a-43ee-a83b-30fcd6e8ccf8","Fighter","ファイター",1,3,"1",FALSE,1,"67899c05-e73a-43ee-a83b-30fcd6e8ccf8","100001",FALSE,0,FALSE
|
||||||
|
"56aa0f3e-5cc1-49e7-a12d-a4d506064c9a","Warrior","ウオーリア",1,3,"2",FALSE,1,"67899c05-e73a-43ee-a83b-30fcd6e8ccf8","100001",FALSE,0,FALSE
|
||||||
|
"6283eb60-234f-4851-8cc7-7ea36e42def2","Weapon Master","ウェポンマスター",1,3,"3",FALSE,1,"67899c05-e73a-43ee-a83b-30fcd6e8ccf8","100201",FALSE,0,FALSE
|
||||||
|
"0e0c149d-8021-4f1e-a9d4-e2c77fd9e59a","Viking","ヴァイキング",1,3,"5",FALSE,1,"67899c05-e73a-43ee-a83b-30fcd6e8ccf8","100401",FALSE,0,FALSE
|
||||||
|
"2b0cfead-50b3-4acd-8cb0-18aab243fdd1","Wizard","ウィザード",6,2,"1",FALSE,4,"2b0cfead-50b3-4acd-8cb0-18aab243fdd1","130001",FALSE,0,FALSE
|
||||||
|
"0b536736-669c-48d2-9b3c-a450f5183951","Sorcerer","ソーサラー",6,2,"2",FALSE,4,"2b0cfead-50b3-4acd-8cb0-18aab243fdd1","130001",FALSE,0,FALSE
|
||||||
|
"667bf041-61c9-4568-bdd3-ce6e43f40603","Warlock","ウォーロック",6,2,"4",TRUE,4,"2b0cfead-50b3-4acd-8cb0-18aab243fdd1","130301",FALSE,0,TRUE
|
||||||
|
"1eb55dd3-3278-4da1-8940-c4fc50c1a0f5","Berserker","ベルセルク",1,3,"4",TRUE,1,"67899c05-e73a-43ee-a83b-30fcd6e8ccf8","100301",FALSE,0,TRUE
|
||||||
|
"a5d6fca3-5649-4e12-a6db-5fcec49150ee","Manadiver","マナダイバー",6,2,"5",FALSE,4,"2b0cfead-50b3-4acd-8cb0-18aab243fdd1","130401",TRUE,2,FALSE
|
||||||
|
"43652444-64bb-4938-85d7-aafdfc503d66","Dark Fencer","ダークフェンサー",1,2,"3",FALSE,6,"21dff3a3-22bc-4863-9861-af0a1b41a5f0","150201",FALSE,0,FALSE
|
||||||
|
"d27a4f29-5f0b-4bc6-b75a-1bd187e1a529","Mystic","賢者",6,6,"ex1",FALSE,6,"d27a4f29-5f0b-4bc6-b75a-1bd187e1a529","250201",FALSE,0,FALSE
|
||||||
|
"2abbab55-5bf7-49f8-9ed6-1fe8a3991cca","Cleric","クレリック",6,4,"2",FALSE,3,"950f659b-0521-4a79-b578-7f3b05cb3102","120001",FALSE,0,FALSE
|
||||||
|
"667bf041-61c9-4568-bdd3-ce6e43f40603","Warlock","ウォーロック",6,2,"4",TRUE,4,"2b0cfead-50b3-4acd-8cb0-18aab243fdd1","130301",FALSE,0,TRUE
|
||||||
|
"c128944b-cc79-45b4-bfed-17c8b68db612","Bishop","ビショップ",6,4,"3",FALSE,3,"950f659b-0521-4a79-b578-7f3b05cb3102","120201",FALSE,0,FALSE
|
||||||
|
|
|
@ -8,3 +8,16 @@
|
||||||
"0156d098-f5e7-4d72-8d14-34e6adff9280","The Moon","ザ・ムーン","2040243000",3,3,"6",TRUE,TRUE,200,110,668,952,1094,110,668,952,1094,TRUE,FALSE,FALSE,,,,"2017-11-29","2018-03-22","2019-03-10","The Moon (SSR)","%BE%A4%B4%AD%C0%D0%2F%A5%B6%A1%A6%A5%E0%A1%BC%A5%F3%20%28SSR%29","81835","SSR/ザ・ムーン",,"{}","{}"
|
"0156d098-f5e7-4d72-8d14-34e6adff9280","The Moon","ザ・ムーン","2040243000",3,3,"6",TRUE,TRUE,200,110,668,952,1094,110,668,952,1094,TRUE,FALSE,FALSE,,,,"2017-11-29","2018-03-22","2019-03-10","The Moon (SSR)","%BE%A4%B4%AD%C0%D0%2F%A5%B6%A1%A6%A5%E0%A1%BC%A5%F3%20%28SSR%29","81835","SSR/ザ・ムーン",,"{}","{}"
|
||||||
"05214d59-2765-40c3-9a1d-6c29c6bdc6d6","Colossus Omega","コロッサス・マグナ","2040034000",3,2,"2",TRUE,TRUE,200,103,648,778,921,275,1635,1965,2315,TRUE,FALSE,TRUE,2665,1064,2001,"2014-10-08","2018-03-10","2020-08-08","Colossus Omega","%BE%A4%B4%AD%C0%D0%2F%A5%B3%A5%ED%A5%C3%A5%B5%A5%B9%A1%A6%A5%DE%A5%B0%A5%CA%20%28SSR%29","21736","SSR/コロッサス・マグナ","2024-05-02","{}","{}"
|
"05214d59-2765-40c3-9a1d-6c29c6bdc6d6","Colossus Omega","コロッサス・マグナ","2040034000",3,2,"2",TRUE,TRUE,200,103,648,778,921,275,1635,1965,2315,TRUE,FALSE,TRUE,2665,1064,2001,"2014-10-08","2018-03-10","2020-08-08","Colossus Omega","%BE%A4%B4%AD%C0%D0%2F%A5%B3%A5%ED%A5%C3%A5%B5%A5%B9%A1%A6%A5%DE%A5%B0%A5%CA%20%28SSR%29","21736","SSR/コロッサス・マグナ","2024-05-02","{}","{}"
|
||||||
"d27d0d9a-3b38-4dcd-89a5-4016c2906249","Bahamut","バハムート","2040003000",3,5,"0",TRUE,TRUE,200,140,850,1210,1390,140,850,1210,1390,TRUE,FALSE,TRUE,,,,"2014-04-30","2017-03-10","2019-03-22","Bahamut","%BE%A4%B4%AD%C0%D0%2F%A5%D0%A5%CF%A5%E0%A1%BC%A5%C8%20%28SSR%29","21612","SSR/バハムート",,"{}","{}"
|
"d27d0d9a-3b38-4dcd-89a5-4016c2906249","Bahamut","バハムート","2040003000",3,5,"0",TRUE,TRUE,200,140,850,1210,1390,140,850,1210,1390,TRUE,FALSE,TRUE,,,,"2014-04-30","2017-03-10","2019-03-22","Bahamut","%BE%A4%B4%AD%C0%D0%2F%A5%D0%A5%CF%A5%E0%A1%BC%A5%C8%20%28SSR%29","21612","SSR/バハムート",,"{}","{}"
|
||||||
|
"fbeaa551-7ea1-48f3-982f-f4569d14fb45","Agni","アグニス","2040094000",3,2,"3",TRUE,TRUE,250,127,770,1092,1253,127,770,1092,3685,TRUE,FALSE,TRUE,,,,"2015-09-30","2019-08-27","2021-03-22","Agni","%BE%A4%B4%AD%C0%D0%2F%A5%A2%A5%B0%A5%CB%A5%B9%20%28SSR%29","147663","SSR/アグニス",,"{}","{}"
|
||||||
|
"04302038-a8dc-4860-a09a-257c0a6ac2a9","Lucifer","ルシフェル","2040056000",3,6,"0",TRUE,TRUE,250,136,900,1280,1470,136,900,1280,1470,FALSE,FALSE,TRUE,,,,"2014-12-31","2017-03-10","2019-03-22","Lucifer","%BE%A4%B4%AD%C0%D0%2F%A5%EB%A5%B7%A5%D5%A5%A7%A5%EB%20%28SSR%29","21599",,,"{}","{}"
|
||||||
|
"49bd4739-486a-4eca-990d-88431279ac3a","Qilin","黒麒麟","2040158000",3,5,,FALSE,FALSE,100,107,649,,,107,649,,,FALSE,FALSE,FALSE,,,,"2016-06-23",,,"Qilin",,,,,"{}","{}"
|
||||||
|
"4f6b3ccd-c906-43d6-9720-8328317cf6b2","The Sun","ザ・サン","2040244000",3,2,"6",TRUE,TRUE,200,106,664,948,1090,106,664,948,1090,TRUE,FALSE,FALSE,,,,"2017-11-29","2018-03-22","2019-03-10","The Sun (SSR)","%BE%A4%B4%AD%C0%D0%2F%A5%B6%A1%A6%A5%B5%A5%F3%20%28SSR%29","81834","SSR/ザ・サン",,"{}","{}"
|
||||||
|
"5c007586-588b-4eea-9bc5-d099f94af737","Wilnas","ウィルナス","2040398000",3,2,"12",TRUE,FALSE,100,127,771,1093,,399,2349,3324,,TRUE,FALSE,FALSE,,,,"2022-01-31",,,"Wilnas","%A5%A6%A5%A3%A5%EB%A5%CA%A5%B9%20%28SSR%29","317298","SSR/ウィルナス",,"{}","{}"
|
||||||
|
"597d6c56-73e3-424f-9971-8a237700fe08","Michael","ミカエル","2040306000",3,2,"5",TRUE,FALSE,150,129,832,1184,,359,2240,3181,,TRUE,FALSE,FALSE,,,,"2022-12-31",,,"Michael","%A5%DF%A5%AB%A5%A8%A5%EB%20%28SSR%29","381021","SSR/ミカエル",,"{}","{}"
|
||||||
|
"ad21f1d7-2b0a-4cd6-beb7-ce624d381f36","Triple Zero","トリプルゼロ","2040425000",3,6,"0",TRUE,FALSE,150,130,1140,,,367,2947,,,TRUE,TRUE,FALSE,,,,"2023-12-31","2023-12-31",,"Triple Zero","","","",,"{}","{}"
|
||||||
|
"1e3be2f2-803c-4cff-802d-785f3b682cfb","Hades","ハデス","2040090000",3,5,"3",TRUE,TRUE,250,123,755,1071,1229,123,755,1071,3798,TRUE,FALSE,TRUE,,,,"2015-08-31","2019-08-27","2021-03-22","Hades","%BE%A4%B4%AD%C0%D0%2F%A5%CF%A5%C7%A5%B9%20%28SSR%29","147575","SSR/ハデス",,"{}","{}"
|
||||||
|
"d27d0d9a-3b38-4dcd-89a5-4016c2906249","Bahamut","バハムート","2040003000",3,5,"0",TRUE,TRUE,200,140,850,1210,1390,140,850,1210,1390,TRUE,FALSE,TRUE,,,,"2014-04-30","2017-03-10","2019-03-22","Bahamut","%BE%A4%B4%AD%C0%D0%2F%A5%D0%A5%CF%A5%E0%A1%BC%A5%C8%20%28SSR%29","21612","SSR/バハムート",,"{}","{}"
|
||||||
|
"0fcfa02d-879f-4166-bc70-1f86a99a45ca","Sariel","サリエル","2040327000",3,5,"5",TRUE,FALSE,150,132,790,1119,,357,2155,3054,,TRUE,FALSE,FALSE,,,,"2019-04-30","2022-03-24",,"Sariel","%BE%A4%B4%AD%C0%D0%2F%A5%B5%A5%EA%A5%A8%A5%EB%20%28SSR%29","149228","SSR/サリエル",,"{}","{}"
|
||||||
|
"b203a9bc-9453-4090-91cc-84532b709d58","Zirnitra","ジルニトラ","2040385000",3,5,,FALSE,FALSE,100,128,806,,,128,806,,,TRUE,FALSE,FALSE,,,,"2020-09-30",,,"Zirnitra",,"230823",,,"{}","{}"
|
||||||
|
"83a6dfcb-0b74-4354-948c-2cff49b5b2b9","Fediel","フェディエル","2040418000",3,5,"12",TRUE,FALSE,150,132,810,1149,,374,2154,3044,,TRUE,FALSE,FALSE,,,,"2021-12-31",,,"Fediel","%A5%D5%A5%A7%A5%C7%A5%A3%A5%A8%A5%EB%20%28SSR%29","311659","SSR/フェディエル",,"{}","{}"
|
||||||
|
"fbde7c76-be0c-4a42-8479-046ed9715db9","Death","デス","2040238000",3,5,"6",TRUE,TRUE,200,109,695,984,1128,109,695,984,1128,TRUE,FALSE,FALSE,,,,"2017-11-29","2018-03-22","2019-03-10","Death (SSR)","%BE%A4%B4%AD%C0%D0%2F%A5%C7%A5%B9%20%28SSR%29","81843","SSR/デス",,"{}","{}"
|
||||||
|
|
|
||||||
|
14
db/seed/test/weapon_awakenings_test.csv
Normal file
14
db/seed/test/weapon_awakenings_test.csv
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"id","weapon_id","awakening_id"
|
||||||
|
"3f8be70e-db9f-41c0-91a7-b07cca9ed263","706438c4-a5eb-4f7c-a145-0c2a3e7e6fbe","18ab5007-3fcb-4f83-a7a0-879a9a4a7ad7"
|
||||||
|
"59af97e7-8828-432e-9ff7-b2c792d08d70","ba7af3b3-c62f-4f85-a420-0321c776ba00","275c9de5-db1e-4c66-8210-660505fd1af4"
|
||||||
|
"97c4b396-597f-4622-9f6d-ee9536a6629b","ba7af3b3-c62f-4f85-a420-0321c776ba00","d691a61c-dc7e-4d92-a8e6-98c04608353c"
|
||||||
|
"b6b911bb-ee89-435f-b325-9df53a1ce6ea","ba7af3b3-c62f-4f85-a420-0321c776ba00","969d37db-5f14-4d1a-bef4-59ba5a016674"
|
||||||
|
"1dbff135-b401-4619-973d-740f4504ee3a","a2f0db22-baf1-4640-8c2e-6d283375744f","d691a61c-dc7e-4d92-a8e6-98c04608353c"
|
||||||
|
"d48fd874-484d-41c5-bff0-709cb714f7b0","a2f0db22-baf1-4640-8c2e-6d283375744f","275c9de5-db1e-4c66-8210-660505fd1af4"
|
||||||
|
"e793cc76-025d-4b6d-975a-58c56ff19141","47208685-e87a-4e07-b328-fb9ac3888718","d691a61c-dc7e-4d92-a8e6-98c04608353c"
|
||||||
|
"42ba1467-971e-40bd-b701-07538678cc95","e7a05d2e-a3ec-4620-98a5-d8472d474971","d691a61c-dc7e-4d92-a8e6-98c04608353c"
|
||||||
|
"6e94080f-1bbf-4171-8d77-40328c1daf1f","e7a05d2e-a3ec-4620-98a5-d8472d474971","969d37db-5f14-4d1a-bef4-59ba5a016674"
|
||||||
|
"714e3575-d536-4a77-870b-b5e2d8b31b68","e7a05d2e-a3ec-4620-98a5-d8472d474971","275c9de5-db1e-4c66-8210-660505fd1af4"
|
||||||
|
"5daffb43-f456-41db-8e04-dadc42bea788","8137294e-6bf1-4bac-a1e0-38cdc542622b","d691a61c-dc7e-4d92-a8e6-98c04608353c"
|
||||||
|
"ab83344b-b4ee-4aad-8e9b-1b7a8169575b","8137294e-6bf1-4bac-a1e0-38cdc542622b","275c9de5-db1e-4c66-8210-660505fd1af4"
|
||||||
|
"e26dbd37-b4d1-49f2-a5f2-36525a57b998","8137294e-6bf1-4bac-a1e0-38cdc542622b","969d37db-5f14-4d1a-bef4-59ba5a016674"
|
||||||
|
47
db/seed/test/weapon_keys_test.csv
Normal file
47
db/seed/test/weapon_keys_test.csv
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"id","name_en","name_jp","slot","group","order","slug","granblue_id","series"
|
||||||
|
"02b40c48-b0d4-4df6-a27f-da2bc58fdd0f","Pendulum of Strife","闘争のペンデュラム",1,0,2,"pendulum-strife",14003,"{3}"
|
||||||
|
"0946e421-db65-403b-946f-5e2285e963f5","Pendulum of Sagacity","窮理のペンデュラム",1,2,2,"pendulum-sagacity",14006,"{3}"
|
||||||
|
"14534be3-defa-44cd-9096-09bae07565c8","Chain of Temperament","技錬のチェイン",1,1,0,"chain-temperament",14011,"{3}"
|
||||||
|
"1e2a1e5b-75f4-4e00-85d5-e5ef474dd6d7","Chain of Depravity","邪罪のチェイン",1,1,5,"chain-depravity",14016,"{3}"
|
||||||
|
"3faafaf1-5fc5-4aa8-8c65-894bbe1c615f","α Pendulum","アルファ・ペンデュラム",0,0,0,"pendulum-alpha",13001,"{3}"
|
||||||
|
"562c89bd-68cf-4a33-8609-d82e017130d6","Chain of Restoration","賦活のチェイン",1,1,1,"chain-restoration",14012,"{3}"
|
||||||
|
"5936e870-61a1-40a4-8c52-b85b9ab96967","Pendulum of Prosperity","隆盛のペンデュラム",1,0,3,"pendulum-prosperity",14004,"{3}"
|
||||||
|
"653477b7-5321-4ea4-8b6f-42218e67a090","Pendulum of Zeal","激情のペンデュラム",1,0,1,"pendulum-zeal",14002,"{3}"
|
||||||
|
"6ded911e-81d6-4fae-a3e7-682a5d18f2fc","Chain of Glorification","謳歌のチェイン",1,1,2,"chain-glorification",14013,"{3}"
|
||||||
|
"b3d8d4d8-8bf6-4e03-9f21-547653bf7574","Pendulum of Strength","強壮のペンデュラム",1,0,0,"pendulum-strength",14001,"{3}"
|
||||||
|
"c7a65d1f-c6a5-4c12-a90e-f3a31dc9d8f9","Pendulum of Extremity","絶涯のペンデュラム",1,2,1,"pendulum-extremity",14005,"{3}"
|
||||||
|
"d5b81056-fd58-45b6-b6ef-a43b45a15194","Chain of Temptation","誘惑のチェイン",1,1,3,"chain-temptation",14014,"{3}"
|
||||||
|
"d5ed9765-263e-4e28-b46a-a1f6bf8c6615","Pendulum of Supremacy","天髄のペンデュラム",1,2,3,"pendulum-supremacy",14007,"{3}"
|
||||||
|
"e719de37-500e-44cd-98a4-2d9af71e0809","Δ Pendulum","デルタ・ペンデュラム",0,0,4,"pendulum-delta",13004,"{3}"
|
||||||
|
"ebe424a0-7370-4b07-bd37-7eeee9b8425c","Chain of Falsehood","虚詐のチェイン",1,1,6,"chain-falsehood",14017,"{3}"
|
||||||
|
"ed19dcef-8579-4125-8607-5a43922d0999","β Pendulum","ベータ・ペンデュラム",0,0,1,"pendulum-beta",13002,"{3}"
|
||||||
|
"f5d711d8-f2f8-4909-9a64-ce6dc3584e03","γ Pendulum","ガンマ・ペンデュラム",0,0,2,"pendulum-gamma",13003,"{3}"
|
||||||
|
"f81ec8e8-acc8-4ad3-8460-b628e90cd29d","Chain of Forbiddance","禁忌のチェイン",1,1,4,"chain-forbiddance",14015,"{3}"
|
||||||
|
"0b696acb-baf4-4ad8-9caa-4255b338b13b","Gauph Key of Vitality","ガフスキー【生命】",0,3,2,"gauph-vitality",10003,"{13}"
|
||||||
|
"148e3323-395f-417c-b18a-96fd9421cfe6","Gauph Key of Strife","ガフスキー【闘争】",0,3,1,"gauph-strife",10002,"{13}"
|
||||||
|
"2ebe966e-0339-4464-acb9-0db138c3e2e7","Gauph Key of Will","ガフスキー【戦意】",0,3,0,"gauph-will",10001,"{13}"
|
||||||
|
"3ca1a71c-66bf-464a-8ad2-254c52169e8e","Gauph Key γ","ガフスキー【γ】",1,3,2,"gauph-gamma",11003,"{13}"
|
||||||
|
"3d5d610a-3734-444d-8818-fce2024a190b","Gauph Key Tria","ガフスキー【トリア】",2,3,2,"gauph-tria",17003,"{13}"
|
||||||
|
"4d6fefb6-09e6-4c92-98b0-a48b35ddd738","Gauph Key β","ガフスキー【β】",1,3,1,"gauph-beta",11002,"{13}"
|
||||||
|
"606632e3-3391-4223-8147-07060fe6f2e4","Gauph Key of Courage","ガフスキー【勇気】",0,3,5,"gauph-courage",10006,"{13}"
|
||||||
|
"6d03b9c2-08d8-49ea-8522-5507e9243ccc","Gauph Key α","ガフスキー【α】",1,3,0,"gauph-alpha",11001,"{13}"
|
||||||
|
"98a358bc-d123-40c9-8c0e-7953467c9a27","Gauph Key Δ","ガフスキー【Δ】",1,3,3,"gauph-delta",11004,"{13}"
|
||||||
|
"a1613dcd-dcc1-4290-95e7-3f9dfc28dd06","Gauph Key Tessera","ガフスキー【テーセラ】",2,3,3,"gauph-tessera",17004,"{13}"
|
||||||
|
"abd48244-8398-4159-ada6-9062803189f1","Gauph Key of Strength","ガフスキー【強壮】",0,3,3,"gauph-strength",10004,"{13}"
|
||||||
|
"cdd87f62-2d29-4698-b09d-8eef3f7b4406","Gauph Key Ena","ガフスキー【エナ】",2,3,0,"gauph-ena",17001,"{13}"
|
||||||
|
"d0dd2b46-cb55-4c2f-beb6-e2ee380bdb5e","Gauph Key Dio","ガフスキー【ディオ】",2,3,1,"gauph-dio",17002,"{13}"
|
||||||
|
"d6c0afdb-f6f3-4473-ada3-d505228ee348","Gauph Key of Zeal","ガフスキー【激情】",0,3,4,"gauph-zeal",10005,"{13}"
|
||||||
|
"44c2b0ba-642e-4edc-9680-1a34abe20418","Emblem of Devilry","魔獄のエンブレム",0,4,2,"emblem-devilry",3,"{19}"
|
||||||
|
"5ac2ad0a-f8da-403a-b098-7831d354f8e0","Emblem of Divinity","天聖のエンブレム",0,4,1,"emblem-divinity",2,"{19}"
|
||||||
|
"c2f1e5bc-9f8b-4af1-821c-2b32a9fb5f1f","Emblem of Humanity","英勇のエンブレム",0,4,0,"emblem-humanity",1,"{19}"
|
||||||
|
"0c6ce91c-864c-4c62-8c9b-be61e8fae47f","Optimus Teluma","オプティマス・テルマ",1,2,0,"teluma-optimus",16001,"{27,40}"
|
||||||
|
"1929bfa8-6bbd-4918-9ad7-594525b5e2c6","Crag Teluma","巨岩のテルマ",0,2,3,"teluma-crag",15004,"{27,40}"
|
||||||
|
"3fa65774-1ed1-4a16-86cd-9133adca2232","Omega Teluma","マグナ・テルマ",1,2,1,"teluma-omega",16002,"{27,40}"
|
||||||
|
"49f46e22-1796-435e-bce2-d9fdfe76d6c5","Tempest Teluma","暴風のテルマ",0,2,4,"teluma-tempest",15005,"{27,40}"
|
||||||
|
"81950efb-a4e1-4d45-8572-ddb604246212","Malice Teluma","闇禍のテルマ",0,2,6,"teluma-malice",15007,"{27,40}"
|
||||||
|
"d14e933e-630d-4cd6-9d61-dbdfd6e9332e","Abyss Teluma","深海のテルマ",0,2,2,"teluma-abyss",15003,"{27,40}"
|
||||||
|
"dc96edb7-8bee-4721-94c2-daa6508aaed8","Inferno Teluma","炎獄のテルマ",0,2,1,"teluma-inferno",15002,"{27,40}"
|
||||||
|
"e36950be-1ea9-4642-af94-164187e38e6c","Aureole Teluma","後光のテルマ",0,2,5,"teluma-aureole",15006,"{27,40}"
|
||||||
|
"ee80ff09-71c0-48bb-90ff-45e138df7481","Endurance Teluma","剛堅のテルマ",0,2,0,"teluma-endurance",15001,"{27,40}"
|
||||||
|
"b0b6d3be-7203-437e-8acd-2a59c2b5506a","Oblivion Teluma","冥烈のテルマ",0,2,8,"teluma-oblivion",15009,"{40}"
|
||||||
|
"d79558df-53fb-4c24-963b-e0b67040afc7","Salvation Teluma","燦護のテルマ",0,2,7,"teluma-salvation",15008,"{40}"
|
||||||
|
|
|
@ -1,42 +1,24 @@
|
||||||
"id","name_en","name_jp","granblue_id","rarity","element","proficiency","series","flb","ulb","max_level","max_skill_level","min_hp","max_hp","max_hp_flb","max_hp_ulb","min_atk","max_atk","max_atk_flb","max_atk_ulb","extra","ax_type","limit","ax","nicknames_en","nicknames_jp","max_awakening_level","release_date","flb_date","ulb_date","wiki_en","wiki_ja","gamewith","kamigame","transcendence","transcendence_date","recruits"
|
"id","name_en","name_jp","granblue_id","rarity","element","proficiency","series","flb","ulb","max_level","max_skill_level","min_hp","max_hp","max_hp_flb","max_hp_ulb","min_atk","max_atk","max_atk_flb","max_atk_ulb","extra","ax_type","limit","ax","nicknames_en","nicknames_jp","max_awakening_level","release_date","flb_date","ulb_date","wiki_en","wiki_ja","gamewith","kamigame","transcendence","transcendence_date","recruits"
|
||||||
"6c4f29c8-f43b-43f1-9fc5-967fb85c816e","Gauntlet of Proudearth","揺るがぬ大地の拳","1040611300",3,4,7,0,TRUE,FALSE,150,15,35,240,290,,390,2300,2780,,FALSE,0,FALSE,FALSE,"{}","{}",,"2019-04-24","2019-04-24",,"Gauntlet of Proudearth",,,,FALSE,,
|
"6c4f29c8-f43b-43f1-9fc5-967fb85c816e","Gauntlet of Proudearth","揺るがぬ大地の拳","1040611300",3,4,7,,TRUE,FALSE,150,15,35,240,290,,390,2300,2780,,FALSE,0,FALSE,FALSE,"{}","{}",,"2019-04-24","2019-04-24",,"Gauntlet of Proudearth",,,,FALSE,,
|
||||||
"302ded88-b5c9-4570-b422-66fc40277c4f","Ixaba","イクサバ","1040906400",3,2,10,1,TRUE,FALSE,150,15,30,195,236,,502,3000,3620,,FALSE,0,FALSE,FALSE,"{}","{}",4,"2017-03-31","2018-05-21",,"Ixaba",,"72189","イクサバ",FALSE,,"3040115000"
|
"302ded88-b5c9-4570-b422-66fc40277c4f","Ixaba","イクサバ","1040906400",3,2,10,,TRUE,FALSE,150,15,30,195,236,,502,3000,3620,,FALSE,0,FALSE,FALSE,"{}","{}",4,"2017-03-31","2018-05-21",,"Ixaba",,"72189","イクサバ",FALSE,,"3040115000"
|
||||||
"b540fbaf-48c9-41c0-981f-05953319b409","Skeletal Eclipse","呪蝕の骸槍","1040216900",3,5,4,1,TRUE,FALSE,150,15,43,280,339,,441,2547,3074,,FALSE,0,FALSE,FALSE,"{}","{}",,"2021-12-31","2021-12-31",,"Skeletal Eclipse","%C9%F0%B4%EF%2F%BC%F6%BF%AA%A4%CE%B3%BC%C1%E4%20%28SSR%29","314295","呪蝕の骸槍",FALSE,,"3040376000"
|
"b540fbaf-48c9-41c0-981f-05953319b409","Skeletal Eclipse","呪蝕の骸槍","1040216900",3,5,4,,TRUE,FALSE,150,15,43,280,339,,441,2547,3074,,FALSE,0,FALSE,FALSE,"{}","{}",,"2021-12-31","2021-12-31",,"Skeletal Eclipse","%C9%F0%B4%EF%2F%BC%F6%BF%AA%A4%CE%B3%BC%C1%E4%20%28SSR%29","314295","呪蝕の骸槍",FALSE,,"3040376000"
|
||||||
"aa6f8b9b-ed78-4b1a-8693-acefd5b455fc","Scythe of Repudiation","絶対否定の大鎌","1040310600",3,2,3,2,TRUE,TRUE,200,20,30,195,236,277,450,2730,3300,3870,FALSE,0,TRUE,FALSE,"{}","{}",,"2019-04-11","2019-04-11","2019-04-11","Scythe of Repudiation","{{{link_jpwiki|%C9%F0%B4%EF%B3%B5%CD%D7%2F%BD%AA%CB%F6%A4%CE%BF%C0%B4%EF%A5%B7%A5%EA%A1%BC%A5%BA}}}","{{{link_gamewith|146896}}}","{{{link_kamigame|{{{jpname|}}}}}}",TRUE,"2024-01-15",
|
"a2025b78-5c72-4efa-9fbf-c9fdc2aa2364","Katana of Repudiation","絶対否定の太刀","1040911000",3,5,10,,TRUE,TRUE,200,20,28,189,229,269,465,2765,3340,3915,FALSE,0,TRUE,FALSE,"{}","{}",,"2019-04-11","2019-04-11","2019-04-11","Katana of Repudiation","{{{link_jpwiki|%C9%F0%B4%EF%B3%B5%CD%D7%2F%BD%AA%CB%F6%A4%CE%BF%C0%B4%EF%A5%B7%A5%EA%A1%BC%A5%BA}}}","{{{link_gamewith|146896}}}","{{{link_kamigame|{{{jpname|}}}}}}",TRUE,"2024-01-15",
|
||||||
"c6e4eeaa-bd19-466e-81ea-58310ed5cf25","Draconic Blade","ドラゴニックブレイド","1040912100",3,6,10,3,TRUE,TRUE,200,20,32,193,233,273,445,2744,3319,3894,FALSE,0,TRUE,FALSE,"{}","{}",,"2020-03-10","2020-03-10","2020-03-10","Draconic Blade","%C9%F0%B4%EF%2F%A5%C9%A5%E9%A5%B4%A5%CB%A5%C3%A5%AF%A5%D6%A5%EC%A5%A4%A5%C9%20%28SSR%29","190367",,FALSE,,
|
"aa6f8b9b-ed78-4b1a-8693-acefd5b455fc","Scythe of Repudiation","絶対否定の大鎌","1040310600",3,2,3,,TRUE,TRUE,200,20,30,195,236,277,450,2730,3300,3870,FALSE,0,TRUE,FALSE,"{}","{}",,"2019-04-11","2019-04-11","2019-04-11","Scythe of Repudiation","{{{link_jpwiki|%C9%F0%B4%EF%B3%B5%CD%D7%2F%BD%AA%CB%F6%A4%CE%BF%C0%B4%EF%A5%B7%A5%EA%A1%BC%A5%BA}}}","{{{link_gamewith|146896}}}","{{{link_kamigame|{{{jpname|}}}}}}",TRUE,"2024-01-15",
|
||||||
|
"c6e4eeaa-bd19-466e-81ea-58310ed5cf25","Draconic Blade","ドラゴニックブレイド","1040912100",3,6,10,,TRUE,TRUE,200,20,32,193,233,273,445,2744,3319,3894,FALSE,0,TRUE,FALSE,"{}","{}",,"2020-03-10","2020-03-10","2020-03-10","Draconic Blade","%C9%F0%B4%EF%2F%A5%C9%A5%E9%A5%B4%A5%CB%A5%C3%A5%AF%A5%D6%A5%EC%A5%A4%A5%C9%20%28SSR%29","190367",,FALSE,,
|
||||||
"1cedbb93-79ef-41ef-915f-94961ef9eba8","Nine-Realm Harp (Awakened)","九界琴・覚醒","1040801400",3,0,8,4,FALSE,FALSE,100,10,75,275,,,380,2470,,,FALSE,0,FALSE,FALSE,"{}","{}",,"2014-03-10",,,"Nine-Realm Harp (Awakened)",,,,FALSE,,
|
"1cedbb93-79ef-41ef-915f-94961ef9eba8","Nine-Realm Harp (Awakened)","九界琴・覚醒","1040801400",3,0,8,4,FALSE,FALSE,100,10,75,275,,,380,2470,,,FALSE,0,FALSE,FALSE,"{}","{}",,"2014-03-10",,,"Nine-Realm Harp (Awakened)",,,,FALSE,,
|
||||||
"a5d72b41-6dea-4179-9996-36c01d2dad32","Winter's Frostnettle","冬ノ霜柱","1040111300",3,3,2,5,TRUE,FALSE,150,15,21,189,228,,290,1857,2249,,FALSE,0,FALSE,FALSE,"{}","{}",,"2019-07-12","2019-07-12",,"Winter%27s Frostnettle","%C9%F0%B4%EF%2F%C5%DF%A5%CE%C1%FA%C3%EC%20%28SSR%29","158278","冬ノ霜柱",FALSE,,
|
"a5d72b41-6dea-4179-9996-36c01d2dad32","Winter's Frostnettle","冬ノ霜柱","1040111300",3,3,2,,TRUE,FALSE,150,15,21,189,228,,290,1857,2249,,FALSE,0,FALSE,FALSE,"{}","{}",,"2019-07-12","2019-07-12",,"Winter%27s Frostnettle","%C9%F0%B4%EF%2F%C5%DF%A5%CE%C1%FA%C3%EC%20%28SSR%29","158278","冬ノ霜柱",FALSE,,
|
||||||
"620fbcd5-7c2e-4949-8cad-bbfb0908b00f","Ecke Sachs","エッケザックス","1040007100",3,2,1,6,TRUE,FALSE,150,15,106,664,800,,278,1677,2030,,FALSE,3,FALSE,TRUE,"{}","{}",,"2014-03-10","2020-04-07",,"Ecke Sachs","%C9%F0%B4%EF%2F%A5%A8%A5%C3%A5%B1%A5%B6%A5%C3%A5%AF%A5%B9%20%28SSR%29","71702","エッケザックス",FALSE,,
|
"620fbcd5-7c2e-4949-8cad-bbfb0908b00f","Ecke Sachs","エッケザックス","1040007100",3,2,1,,TRUE,FALSE,150,15,106,664,800,,278,1677,2030,,FALSE,3,FALSE,TRUE,"{}","{}",,"2014-03-10","2020-04-07",,"Ecke Sachs","%C9%F0%B4%EF%2F%A5%A8%A5%C3%A5%B1%A5%B6%A5%C3%A5%AF%A5%B9%20%28SSR%29","71702","エッケザックス",FALSE,,
|
||||||
"8cebe3c3-be12-4985-b45d-3e9db8204e6e","Ray of Zhuque Malus","朱雀光剣・邪","1040906700",3,2,10,7,TRUE,TRUE,200,20,22,145,175,205,345,2090,2530,2970,FALSE,0,TRUE,FALSE,"{}","{}",,"2017-04-10","2017-04-10","2022-04-07","Ray of Zhuque Malus",,"75564","朱雀光剣・邪",FALSE,,
|
"8cebe3c3-be12-4985-b45d-3e9db8204e6e","Ray of Zhuque Malus","朱雀光剣・邪","1040906700",3,2,10,,TRUE,TRUE,200,20,22,145,175,205,345,2090,2530,2970,FALSE,0,TRUE,FALSE,"{}","{}",,"2017-04-10","2017-04-10","2022-04-07","Ray of Zhuque Malus",,"75564","朱雀光剣・邪",FALSE,,
|
||||||
"4380828f-1acc-46cd-b7eb-1cb8d34ca9ec","Last Storm Harp","ラストストームハープ","1040808300",3,1,8,8,TRUE,FALSE,150,15,62,223,260,,337,2059,2400,,FALSE,0,FALSE,FALSE,"{}","{}",,"2018-03-10","2018-03-10",,"Last Storm Harp",,,,FALSE,,
|
"4380828f-1acc-46cd-b7eb-1cb8d34ca9ec","Last Storm Harp","ラストストームハープ","1040808300",3,1,8,,TRUE,FALSE,150,15,62,223,260,,337,2059,2400,,FALSE,0,FALSE,FALSE,"{}","{}",,"2018-03-10","2018-03-10",,"Last Storm Harp",,,,FALSE,,
|
||||||
"ec3ba18a-9417-4ebe-a898-a74d5f15385f","Pillar of Flame","炎の柱","1040215200",3,6,4,8,TRUE,FALSE,150,15,37,213,250,,341,2250,2630,,FALSE,0,FALSE,FALSE,"{}","{}",,"2020-08-31","2020-08-31",,"Pillar of Flame","%C9%F0%B4%EF%2F%B1%EA%A4%CE%C3%EC%20%28SSR%29","225789","炎の柱",FALSE,,
|
"ec3ba18a-9417-4ebe-a898-a74d5f15385f","Pillar of Flame","炎の柱","1040215200",3,6,4,,TRUE,FALSE,150,15,37,213,250,,341,2250,2630,,FALSE,0,FALSE,FALSE,"{}","{}",,"2020-08-31","2020-08-31",,"Pillar of Flame","%C9%F0%B4%EF%2F%B1%EA%A4%CE%C3%EC%20%28SSR%29","225789","炎の柱",FALSE,,
|
||||||
"d61ee84f-4520-4064-8ff9-42a899273316","Luminiera Sword Omega","シュヴァリエソード・マグナ","1040007200",3,6,1,9,TRUE,TRUE,200,20,31,195,228,244,370,2275,2660,2850,FALSE,1,FALSE,TRUE,"{}","{}",,"2014-03-10",,"2018-03-10","Luminiera Sword Omega",,,,FALSE,,
|
"d61ee84f-4520-4064-8ff9-42a899273316","Luminiera Sword Omega","シュヴァリエソード・マグナ","1040007200",3,6,1,,TRUE,TRUE,200,20,31,195,228,244,370,2275,2660,2850,FALSE,1,FALSE,TRUE,"{}","{}",,"2014-03-10",,"2018-03-10","Luminiera Sword Omega",,,,FALSE,,
|
||||||
"9f94d1e5-a117-432f-9da4-f3a5022b666d","Bow of Sigurd","シグルズの弓","1040705100",3,3,5,10,TRUE,FALSE,150,15,36,214,250,,365,2311,2701,,FALSE,3,FALSE,TRUE,"{}","{}",,"2014-03-10","2018-07-15",,"Bow of Sigurd",,,,FALSE,,
|
"9f94d1e5-a117-432f-9da4-f3a5022b666d","Bow of Sigurd","シグルズの弓","1040705100",3,3,5,,TRUE,FALSE,150,15,36,214,250,,365,2311,2701,,FALSE,3,FALSE,TRUE,"{}","{}",,"2014-03-10","2018-07-15",,"Bow of Sigurd",,,,FALSE,,
|
||||||
"82deb08e-8f92-44eb-8671-22426f89564e","Sword of Pallas Militis","パラスソード・ミーレス","1040022600",3,2,1,11,FALSE,FALSE,100,10,28,182,,,355,2153,,,TRUE,0,FALSE,FALSE,"{}","{}",,"2022-02-21",,,"Sword of Pallas Militis","%C9%F0%B4%EF%2F%A5%D1%A5%E9%A5%B9%A5%BD%A1%BC%A5%C9%A1%A6%A5%DF%A1%BC%A5%EC%A5%B9%20%28SSR%29","319816",,FALSE,,
|
"a4441a22-4704-4fbc-a543-77d3b952e921","Pain and Suffering","ペイン・アンド・ストレイン","1040314300",3,5,3,,TRUE,FALSE,150,15,50,410,500,,410,1890,2260,,FALSE,0,FALSE,FALSE,"{pns}","{}",,"2021-09-15","2021-09-15",,"Pain and Suffering","%C9%F0%B4%EF%2F%A5%DA%A5%A4%A5%F3%A1%A6%A5%A2%A5%F3%A5%C9%A1%A6%A5%B9%A5%C8%A5%EC%A5%A4%A5%F3%20%28SSR%29","294337","ペイン・アンド・ストレイン",FALSE,,"3040357000"
|
||||||
"88492bc9-8085-4651-8a9d-305ab03d0710","Hollowsky Bow","虚空の歪弦","1040708900",3,6,5,12,TRUE,FALSE,150,15,37,234,280,,420,2580,3120,,FALSE,0,TRUE,FALSE,"{}","{}",,"2018-12-18","2018-12-18",,"Hollowsky Bow",,"134591","虚空の歪弦",FALSE,,
|
"f4460b37-ab5b-4252-bd79-009a8819ee25","Eternal Signature","永遠の落款","1040116600",3,5,2,,TRUE,FALSE,150,15,40,259,,,459,2562,,,FALSE,,FALSE,FALSE,"{es,""halmal dagger""}","{}",,"2023-01-19","2023-01-19",,"Eternal Signature","%C9%F0%B4%EF%2F%B1%CA%B1%F3%A4%CE%CD%EE%B4%BE%20%28SSR%29","384946",,FALSE,,"3040443000"
|
||||||
"9adb22c7-eb09-47e6-b100-783e0cefaf95","Last Sahrivar","ラスト・シャフレワル","1040015800",3,4,1,13,TRUE,TRUE,200,20,39,200,232,264,391,2240,2611,2982,FALSE,2,FALSE,TRUE,"{}","{}",,"2018-05-15","2018-05-15","2022-06-07","Last Sahrivar",,"105147",,FALSE,,
|
"07dd062a-640c-4f00-9943-614b9f031271","Ultima Claw","オメガクロー","1040608100",3,0,7,,TRUE,TRUE,200,20,35,277,313,349,393,2717,3066,3415,TRUE,0,TRUE,FALSE,"{}","{}",,"2014-03-10","2021-12-03","2021-12-03","Ultima Claw",,,,FALSE,,
|
||||||
"6d4b41c6-2807-4aa6-9f69-14f5c2c68f37","Claws of Terror","黒銀の滅爪","1040612500",3,5,7,14,TRUE,TRUE,200,20,33,227,266,305,372,2196,2561,2926,FALSE,0,FALSE,FALSE,"{}","{}",,"2020-03-03","2020-03-03","2020-05-25","Claws of Terror",,"187437",,FALSE,,
|
"33d75927-70e9-49ba-8494-fb67b4567540","Blutgang","ブルトガング","1040008700",3,5,1,,TRUE,FALSE,150,15,36,234,280,,480,2790,3370,,FALSE,0,FALSE,FALSE,"{}","{}",4,"2016-04-28","2017-11-17",,"Blutgang","%C9%F0%B4%EF%2F%A5%D6%A5%EB%A5%C8%A5%AC%A5%F3%A5%B0%20%28SSR%29","71711",,FALSE,,"3040082000"
|
||||||
"874eaf0b-5561-49d4-8983-ded494642a84","Rose Crystal Sword","ローズクリスタルソード","1040009700",3,3,1,15,FALSE,FALSE,100,10,34,204,,,365,2320,,,FALSE,0,FALSE,FALSE,"{}","{}",,"2017-03-10",,,"Rose Crystal Sword",,,,FALSE,,
|
"1b3b84fd-eefa-4845-8fd0-b4452482e716","Bab-el-Mandeb","バブ・エル・マンデブ","1040311600",3,5,3,,TRUE,FALSE,150,15,31,207,251,,503,2915,3518,,FALSE,0,FALSE,FALSE,"{}","{}",,"2019-12-28","2019-12-28",,"Bab-el-Mandeb",,,,FALSE,,"3040251000"
|
||||||
"e65ddc21-b8e9-45ee-8c0f-06013b4187a1","Spear of Bahamut Coda","バハムートスピア・フツルス","1040205400",3,5,4,16,TRUE,TRUE,200,20,37,248,290,332,380,2260,2640,3020,TRUE,0,TRUE,FALSE,"{}","{}",,"2014-03-10",,"2021-12-03","Spear of Bahamut Coda",,,,FALSE,,
|
"dd199867-ec7b-4067-8886-1fa02e1095b4","Celeste Zaghnal Omega","セレストザグナル・マグナ","1040301400",3,5,3,,TRUE,TRUE,200,20,24,169,198,213,405,2405,2810,3010,FALSE,1,FALSE,TRUE,"{}","{}",,"2014-03-10",,"2018-03-10","Celeste Zaghnal Omega",,"71937",,FALSE,,
|
||||||
"07dd062a-640c-4f00-9943-614b9f031271","Ultima Claw","オメガクロー","1040608100",3,0,7,17,TRUE,TRUE,200,20,35,277,313,349,393,2717,3066,3415,TRUE,0,TRUE,FALSE,"{}","{}",,"2014-03-10","2021-12-03","2021-12-03","Ultima Claw",,,,FALSE,,
|
"cddf9de4-ee8f-4978-9901-0ec7f2601927","Pain of Death","ペイン・オブ・デス","1040113200",3,5,2,,TRUE,TRUE,200,20,32,218,265,312,379,2241,2707,3173,TRUE,0,FALSE,FALSE,"{}","{}",,"2020-12-04","2022-02-21","2022-12-26","Pain of Death","%C9%F0%B4%EF%2F%A5%DA%A5%A4%A5%F3%A1%A6%A5%AA%A5%D6%A1%A6%A5%C7%A5%B9%20%28SSR%29","{{{link_gamewith|220273}}}",,FALSE,,
|
||||||
"0c21542c-ce18-471e-ac80-1378fc97bec8","Scales of Dominion","支配の天秤","1040415800",3,5,6,18,TRUE,FALSE,150,15,38,261,320,,345,2020,2440,,FALSE,0,FALSE,FALSE,"{}","{}",,"2017-07-24","2019-08-06",,"Scales of Dominion",,"161169",,FALSE,,
|
"38df4067-db48-4dbc-b1cf-c26e019137d8","Parazonium","パラゾニウム","1040108700",3,5,2,,TRUE,FALSE,150,15,40,259,310,,459,2652,3200,,FALSE,0,FALSE,FALSE,"{}","{}",4,"2017-02-28","2018-02-14",,"Parazonium","%C9%F0%B4%EF%2F%A5%D1%A5%E9%A5%BE%A5%CB%A5%A6%A5%E0%20%28SSR%29","71768","パラゾニウム",FALSE,,"3040111000"
|
||||||
"54c220d4-9cee-4f42-b184-5057cb1cb24a","Esna","エスナ","1040420600",3,3,6,19,TRUE,FALSE,150,15,50,294,355,,288,1859,2252,,FALSE,0,FALSE,FALSE,"{}","{}",15,"2022-07-20","2022-07-20",,"Esna","%C9%F0%B4%EF%2F%A5%A8%A5%B9%A5%CA%20%28SSR%29","352615",,FALSE,,
|
"36959849-1ff6-4317-992e-2287b31138eb","Dagger of Bahamut Coda","バハムートダガー・フツルス","1040106700",3,5,2,,TRUE,TRUE,200,20,34,229,268,307,395,2355,2750,3145,TRUE,0,TRUE,FALSE,"{}","{}",,"2014-03-10",,"2021-12-03","Dagger of Bahamut Coda",,,,FALSE,,
|
||||||
"742f29a3-2fa0-40f9-9275-126d892501b3","Cosmic Blade","ブレイド・オブ・コスモス","1040911800",3,6,10,20,TRUE,FALSE,150,15,31,184,222,,423,2610,3157,,FALSE,0,FALSE,FALSE,"{}","{}",,"2016-03-10","2019-12-19",,"Cosmic Blade",,,,FALSE,,
|
|
||||||
"c250d5c7-0208-49b5-9c88-fc51117dd7d3","Ewiyar's Beak","イーウィヤピーク","1040912400",3,1,10,21,TRUE,FALSE,150,15,31,192,224,,450,2749,3209,,FALSE,1,FALSE,TRUE,"{}","{}",,"2020-07-27","2020-07-27",,"Ewiyar%27s Beak",,"218570",,FALSE,,
|
|
||||||
"52d41363-16b1-42af-b185-ed1ba1308891","Ameno Habakiri","天羽々斬","1040904300",3,0,10,22,TRUE,TRUE,200,20,37,213,250,287,504,3024,3530,4036,FALSE,0,FALSE,FALSE,"{}","{}",,"2014-03-10","2018-04-17","2020-05-11","Ameno Habakiri","%C9%F0%B4%EF%C9%BE%B2%C1%2F%A5%B9%A5%DA%A5%EA%A5%AA%A5%EB%A5%B7%A5%EA%A1%BC%A5%BA","75523","天羽々斬",FALSE,,
|
|
||||||
"a273dcbf-4d85-4898-89ac-41cc80c262d7","Gisla","グラーシーザー","1040200700",3,5,4,23,TRUE,FALSE,150,15,39,255,309,,404,2325,2810,,FALSE,0,FALSE,FALSE,"{}","{}",,"2014-05-14","2015-07-27",,"Gisla",,,,FALSE,,
|
|
||||||
"eeb5882d-63a1-4852-a753-32166b4b9b7f","Wasserspeier","ヴァッサーシュパイア","1040018100",3,0,1,24,TRUE,TRUE,200,20,32,205,240,275,390,2410,2814,3218,FALSE,0,FALSE,FALSE,"{}","{}",,"2019-05-10","2019-05-10","2019-05-10","Wasserspeier","%C9%F0%B4%EF%2F%A5%F4%A5%A1%A5%C3%A5%B5%A1%BC%A5%B7%A5%E5%A5%D1%A5%A4%A5%A2%A1%BC%20%28SSR%29","150388","ヴァッサーシュパイアー",FALSE,,
|
|
||||||
"8137294e-6bf1-4bac-a1e0-38cdc542622b","Clarion","クラリオン","1040511200",3,3,9,25,TRUE,TRUE,200,20,27,163,200,237,385,2425,2940,3455,FALSE,0,FALSE,FALSE,"{}","{}",15,"2019-06-04","2019-06-04","2022-08-19","Clarion","%C9%F0%B4%EF%2F%A5%AF%A5%E9%A5%EA%A5%AA%A5%F3%20%28SSR%29","153291",,FALSE,,
|
|
||||||
"63a066c7-9f23-4c12-a921-ec56f584b0ed","Kaladanda","カラダンダ","1040416300",3,2,6,26,TRUE,FALSE,150,15,41,264,320,,333,2009,2428,,FALSE,0,FALSE,FALSE,"{}","{}",,"2019-12-19","2019-12-19",,"Kaladanda","%C9%F0%B4%EF%2F%A5%AB%A5%E9%A5%C0%A5%F3%A5%C0%20%28SSR%29","180277","カラダンダ",FALSE,,
|
|
||||||
"ac8da736-4041-45e2-b413-f859e6fae828","Magma Rush","マグマストリーム","1040408100",3,2,6,27,FALSE,FALSE,150,15,36,232,,,295,1770,,,FALSE,0,FALSE,FALSE,"{}","{}",,"2016-06-09","2023-07-12",,"Magma Rush",,"72005",,FALSE,,
|
|
||||||
"aec45e41-9874-465b-b668-9129a49d40c5","Sephira Emerald Duke","セフィラの翠甲","1040610000",3,4,7,28,TRUE,FALSE,150,15,38,232,271,,350,2175,2540,,TRUE,0,FALSE,FALSE,"{}","{}",,"2018-03-22","2020-12-04",,"Sephira Emerald Duke",,,,FALSE,,
|
|
||||||
"7f70e52a-d18c-4353-a135-1a841d3b7bf2","Rise of Justice","ライズ・オブ・ジャスティス","1040020800",3,3,1,29,TRUE,TRUE,200,20,34,202,244,286,367,2322,2811,3300,TRUE,0,FALSE,FALSE,"{}","{}",,"2020-12-04","2022-02-21","2022-12-26","Rise of Justice","%C9%F0%B4%EF%2F%A5%E9%A5%A4%A5%BA%A1%A6%A5%AA%A5%D6%A1%A6%A5%B8%A5%E3%A5%B9%A5%C6%A5%A3%A5%B9%20%28SSR%29","{{{link_gamewith|220273}}}",,FALSE,,
|
|
||||||
"e7a05d2e-a3ec-4620-98a5-d8472d474971","Fang of the Dragonslayer Mk II","竜伐の剛牙・再誕","1040117700",3,4,2,30,TRUE,FALSE,150,15,41,247,325,,394,2456,3230,,FALSE,,FALSE,FALSE,"{}","{}",20,"2023-11-09","2023-11-09",,"Fang of the Dragonslayer Mk II","","","",FALSE,,
|
|
||||||
"af83ceee-3a24-48c7-8cae-9f83276ced81","Hraesvelgr","フレズヴェルク","1040515200",3,3,9,31,TRUE,FALSE,150,15,37,211,246,,514,3131,3654,,FALSE,0,FALSE,FALSE,"{}","{}",,"2022-12-26","2022-12-26",,"Hraesvelgr","%C9%F0%B4%EF%2F%A5%D5%A5%EC%A5%BA%A5%F4%A5%A7%A5%EB%A5%AF%20%28SSR%29","366906",,FALSE,,
|
|
||||||
"47208685-e87a-4e07-b328-fb9ac3888718","Worldscathing Leon","レオン・オブ・ワールド","1040815100",3,2,8,32,TRUE,FALSE,150,15,44,281,350,,379,2286,2763,,TRUE,,FALSE,FALSE,"{}","{}",10,"2023-03-30","2023-12-19",,"Worldscathing Leon",,"393901",,FALSE,,
|
|
||||||
"a2f0db22-baf1-4640-8c2e-6d283375744f","Exo Antaeus","神銃エクス・アンタイオス","1040516300",3,3,9,33,TRUE,FALSE,150,15,29,169,204,,394,2488,3012,,FALSE,,FALSE,FALSE,"{}","{}",10,"2023-09-07","2023-09-07",,"Exo Antaeus","","","",FALSE,,
|
|
||||||
"b9522d2d-1d29-4a2b-b58c-d3b7c781feb6","Prayer of Grand Gales","狂飆と至高の祈り","1040422200",3,1,6,34,TRUE,TRUE,200,20,39,236,285,439,412,2566,3105,3302,TRUE,,TRUE,FALSE,"{draconic}","{ドラポン}",,"2023-10-23",,,"Prayer of Grand Gales","","","",FALSE,,
|
|
||||||
"81b9845a-a6d5-4aec-bbcf-1678277c1d79","Albacore Body","アルバコアボディ","1040423500",3,5,6,35,FALSE,FALSE,100,10,32,224,,,303,1759,,,FALSE,,FALSE,FALSE,"{}","{}",,"2024-07-29",,,"Albacore Body","アルバコアボディ (SSR)","458282","アルバコアボディ",FALSE,,
|
|
||||||
"f0d13eb4-f462-48d8-8705-16f91c351cb2","Syringe or Treat","シリンジ・オア・トリート","1040516400",3,5,9,36,FALSE,FALSE,100,10,19,130,,,504,2930,,,FALSE,,FALSE,FALSE,"{}","{}",,"2023-10-18",,,"Syringe or Treat","","","",FALSE,,"3040487000"
|
|
||||||
"3d9fad4c-a34f-4133-9d5e-c382a747eeec","Demolition-Tiger Axe","絶壊・威寅斧","1040319100",3,4,3,37,TRUE,FALSE,150,15,35,207,250,,456,2834,3429,,FALSE,,FALSE,FALSE,"{}","{}",,"2024-11-07","2024-11-07",,"Demolition-Tiger_Axe","絶壊・威寅斧 (SSR)","471097","絶壊・威寅斧",FALSE,,
|
|
||||||
"4110e59e-5b4c-40f8-ad83-2e62f5d60fc2","Yggdrasil Crystal Blade Arbos","世界樹の晶剣・アルボス","1040026300",3,4,1,38,TRUE,FALSE,150,15,32,200,242,,377,2332,2821,,FALSE,,FALSE,FALSE,"{}","{}",,"2024-06-03","2024-06-03",,"Yggdrasil Crystal Blade Arbos","","","",FALSE,,
|
|
||||||
|
|
|
||||||
|
3066
spec/fixtures/deck_sample.json
vendored
Normal file
3066
spec/fixtures/deck_sample.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
2564
spec/fixtures/deck_sample2.json
vendored
Normal file
2564
spec/fixtures/deck_sample2.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
72
spec/requests/import_controller_spec.rb
Normal file
72
spec/requests/import_controller_spec.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'ImportController', type: :request do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:access_token) do
|
||||||
|
Doorkeeper::AccessToken.create!(resource_owner_id: user.id, expires_in: 30.days, scopes: 'public')
|
||||||
|
end
|
||||||
|
let(:headers) do
|
||||||
|
{ 'Authorization' => "Bearer #{access_token.token}", 'Content-Type' => 'application/json' }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load raw deck JSON from fixture and wrap it under the "import" key.
|
||||||
|
let(:raw_deck_data) do
|
||||||
|
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample.json')
|
||||||
|
JSON.parse(File.read(file_path))
|
||||||
|
end
|
||||||
|
let(:valid_deck_json) { { 'import' => raw_deck_data }.to_json }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/import' do
|
||||||
|
context 'with valid deck data' do
|
||||||
|
it 'creates a new party and returns a shortcode' do
|
||||||
|
expect {
|
||||||
|
post '/api/v1/import', params: valid_deck_json, headers: headers
|
||||||
|
}.to change(Party, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response).to have_key('shortcode')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid JSON' do
|
||||||
|
it 'returns a bad request error' do
|
||||||
|
post '/api/v1/import', params: 'this is not json', headers: headers
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Invalid JSON data')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with missing required fields in transformed data' do
|
||||||
|
it 'returns unprocessable entity error' do
|
||||||
|
# Here we simulate missing required fields by sending an import hash
|
||||||
|
# where the 'deck' key is present but missing required subkeys.
|
||||||
|
invalid_data = { 'import' => { 'deck' => { 'name' => '', 'pc' => nil } } }.to_json
|
||||||
|
post '/api/v1/import', params: invalid_data, headers: headers
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Invalid deck data')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when an error occurs during processing' do
|
||||||
|
it 'returns unprocessable entity status with error details' do
|
||||||
|
# Stub the transformer to raise an error when transform is called.
|
||||||
|
allow_any_instance_of(Processors::CharacterProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
allow_any_instance_of(Processors::WeaponProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
allow_any_instance_of(Processors::SummonProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
allow_any_instance_of(Processors::JobProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
post '/api/v1/import', params: valid_deck_json, headers: headers
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Error processing import')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -107,6 +107,31 @@ RSpec.describe 'Parties API', type: :request do
|
||||||
expect(json['results']).to be_an(Array)
|
expect(json['results']).to be_an(Array)
|
||||||
expect(json['meta']).to have_key('count')
|
expect(json['meta']).to have_key('count')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# For index, assume the controller builds the query with defaults turned on.
|
||||||
|
# Create one party that meets the default thresholds and one that does not.
|
||||||
|
# Defaults: weapons_count >= 5, characters_count >= 3, summons_count >= 2.
|
||||||
|
@good_party = create(:party, user: user,
|
||||||
|
weapons_count: 5,
|
||||||
|
characters_count: 4,
|
||||||
|
summons_count: 2,
|
||||||
|
visibility: 1)
|
||||||
|
@bad_party = create(:party, user: user,
|
||||||
|
weapons_count: 2, # below default threshold
|
||||||
|
characters_count: 2,
|
||||||
|
summons_count: 1,
|
||||||
|
visibility: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only parties meeting the default filters' do
|
||||||
|
get '/api/v1/parties', headers: headers
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
party_ids = json['results'].map { |p| p['id'] }
|
||||||
|
expect(party_ids).to include(@good_party.id)
|
||||||
|
expect(party_ids).not_to include(@bad_party.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /api/v1/parties/favorites' do
|
describe 'GET /api/v1/parties/favorites' do
|
||||||
|
|
@ -123,19 +148,12 @@ RSpec.describe 'Parties API', type: :request do
|
||||||
create_list(:grid_summon, 2, party: party)
|
create_list(:grid_summon, 2, party: party)
|
||||||
party.reload # Reload to update counter caches.
|
party.reload # Reload to update counter caches.
|
||||||
|
|
||||||
ap "DEBUG: Party counts - characters: #{party.characters_count}, weapons: #{party.weapons_count}, summons: #{party.summons_count}"
|
|
||||||
|
|
||||||
create(:favorite, user: user, party: party)
|
create(:favorite, user: user, party: party)
|
||||||
end
|
end
|
||||||
|
|
||||||
before { create(:favorite, user: user, party: party) }
|
before { create(:favorite, user: user, party: party) }
|
||||||
|
|
||||||
it 'lists parties favorited by the current user' do
|
it 'lists parties favorited by the current user' do
|
||||||
# Debug: print IDs returned by the join query (this code can be removed later)
|
|
||||||
favorite_ids = Party.joins(:favorites).where(favorites: { user_id: user.id }).distinct.pluck(:id)
|
|
||||||
ap "DEBUG: Created party id: #{party.id}"
|
|
||||||
ap "DEBUG: Favorite party ids: #{favorite_ids.inspect}"
|
|
||||||
|
|
||||||
get '/api/v1/parties/favorites', headers: headers
|
get '/api/v1/parties/favorites', headers: headers
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ RSpec.describe PartyQueryBuilder, type: :model do
|
||||||
it 'returns an ActiveRecord::Relation with filters applied' do
|
it 'returns an ActiveRecord::Relation with filters applied' do
|
||||||
query = subject.build
|
query = subject.build
|
||||||
sql = query.to_sql
|
sql = query.to_sql
|
||||||
|
|
||||||
# Expect the element filter to be applied (converted to integer)
|
# Expect the element filter to be applied (converted to integer)
|
||||||
expect(sql).to include('"parties"."element" = 3')
|
expect(sql).to include('"parties"."element" = 3')
|
||||||
# Expect the raid filter to be applied
|
# Expect the raid filter to be applied
|
||||||
|
|
@ -192,6 +191,61 @@ RSpec.describe PartyQueryBuilder, type: :model do
|
||||||
expect(sql).not_to include('NOT EXISTS (')
|
expect(sql).not_to include('NOT EXISTS (')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when apply_defaults option is true' do
|
||||||
|
subject do
|
||||||
|
described_class.new(
|
||||||
|
base_query,
|
||||||
|
params: params,
|
||||||
|
current_user: current_user,
|
||||||
|
options: { apply_defaults: true }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds count filters to the query' do
|
||||||
|
query = subject.build
|
||||||
|
sql = query.to_sql
|
||||||
|
expect(sql).to include('"weapons_count" BETWEEN')
|
||||||
|
expect(sql).to include('"characters_count" BETWEEN')
|
||||||
|
expect(sql).to include('"summons_count" BETWEEN')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when apply_defaults option is false (or not provided)' do
|
||||||
|
let(:blanked_params) do
|
||||||
|
{
|
||||||
|
element: '3',
|
||||||
|
raid: '123e4567-e89b-12d3-a456-426614174000',
|
||||||
|
recency: '3600',
|
||||||
|
full_auto: '1',
|
||||||
|
auto_guard: '0',
|
||||||
|
charge_attack: '1',
|
||||||
|
weapons_count: '', # blank => should use default
|
||||||
|
characters_count: '',
|
||||||
|
summons_count: '',
|
||||||
|
includes: '300001,200002',
|
||||||
|
excludes: '100003',
|
||||||
|
name_quality: '1' # dummy flag for testing name_quality clause
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do
|
||||||
|
described_class.new(
|
||||||
|
base_query,
|
||||||
|
params: blanked_params,
|
||||||
|
current_user: current_user,
|
||||||
|
options: {}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not add count filters to the query' do
|
||||||
|
query = subject.build
|
||||||
|
sql = query.to_sql
|
||||||
|
expect(sql).not_to include('weapons_count BETWEEN')
|
||||||
|
expect(sql).not_to include('characters_count BETWEEN')
|
||||||
|
expect(sql).not_to include('summons_count BETWEEN')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
45
spec/services/processors/base_processor_spec.rb
Normal file
45
spec/services/processors/base_processor_spec.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module Processors
|
||||||
|
class DummyBaseProcessor < BaseProcessor
|
||||||
|
# A dummy implementation of process.
|
||||||
|
def process
|
||||||
|
"processed"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Expose the protected log method as public for testing.
|
||||||
|
def public_log(message)
|
||||||
|
log(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.describe Processors::DummyBaseProcessor, type: :model do
|
||||||
|
# Note: BaseProcessor.new expects (party, data, options = {})
|
||||||
|
let(:dummy_party) { nil }
|
||||||
|
let(:dummy_data) { {} }
|
||||||
|
let(:processor) { described_class.new(dummy_party, dummy_data) }
|
||||||
|
|
||||||
|
describe '#process' do
|
||||||
|
it 'returns the dummy processed value' do
|
||||||
|
expect(processor.process).to eq("processed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#public_log' do
|
||||||
|
it 'logs a message containing the processor class name' do
|
||||||
|
message = "Test log message"
|
||||||
|
expect(Rails.logger).to receive(:info).with(a_string_including("DummyBaseProcessor", message))
|
||||||
|
processor.public_log(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:each) do |example|
|
||||||
|
if example.exception
|
||||||
|
puts "\nDEBUG [DummyBaseProcessor]: #{example.full_description} failed with error: #{example.exception.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
52
spec/services/processors/character_processor_spec.rb
Normal file
52
spec/services/processors/character_processor_spec.rb
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Processors::CharacterProcessor, type: :model do
|
||||||
|
before(:each) do
|
||||||
|
@party = create(:party)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use canonical data loaded via canonical.rb.
|
||||||
|
let(:deck_data) do
|
||||||
|
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample2.json')
|
||||||
|
JSON.parse(File.read(file_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
subject! { described_class.new(@party, deck_data, language: 'en') }
|
||||||
|
|
||||||
|
context 'with valid character data' do
|
||||||
|
it 'creates the correct number of GridCharacter records' do
|
||||||
|
expect { subject.process }.to change(GridCharacter, :count).by(5)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates GridCharacters with the correct attributes' do
|
||||||
|
subject.process
|
||||||
|
grid_chars = GridCharacter.where(party_id: @party.id).order(:position)
|
||||||
|
|
||||||
|
# We assume the processor uses the character id from raw_data.
|
||||||
|
expect(grid_chars[0].character.granblue_id).to eq(deck_data.dig('deck', 'npc', '1', 'master', 'id'))
|
||||||
|
expect(grid_chars[3].uncap_level).to eq(deck_data.dig('deck', 'npc', '4', 'param', 'evolution').to_i)
|
||||||
|
expect(grid_chars[4].position).to eq(4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid character data' do
|
||||||
|
let(:deck_data) { 'invalid data' }
|
||||||
|
it 'does not create any GridCharacter and logs an error containing "CHARACTER"' do
|
||||||
|
expect { subject.process }.not_to change(GridCharacter, :count)
|
||||||
|
|
||||||
|
begin
|
||||||
|
subject.process
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:each) do |example|
|
||||||
|
if example.exception
|
||||||
|
puts "\nDEBUG [CharacterProcessor]: #{example.full_description} failed with error: #{example.exception.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
86
spec/services/processors/job_processor_spec.rb
Normal file
86
spec/services/processors/job_processor_spec.rb
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Processors::JobProcessor, type: :model do
|
||||||
|
let(:party) { create(:party) }
|
||||||
|
# Use a job that has associated job skills.
|
||||||
|
# In our seed/canonical data this job (by its ID) has several associated skills.
|
||||||
|
let!(:job_record) { Job.find_by!(granblue_id: '130401') }
|
||||||
|
|
||||||
|
# Build the raw data hash that mimics the transformed structure.
|
||||||
|
# The master section includes the job's basic information.
|
||||||
|
# The param section includes level data and the subskills derived from the job's associated job skills.
|
||||||
|
let(:deck_data) do
|
||||||
|
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample2.json')
|
||||||
|
JSON.parse(File.read(file_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(party, deck_data, language: 'en') }
|
||||||
|
|
||||||
|
context 'with valid job data' do
|
||||||
|
it 'assigns the job to the party' do
|
||||||
|
# Before processing, the party should not have a job.
|
||||||
|
expect(party.job).to be_nil
|
||||||
|
|
||||||
|
# Process the job data.
|
||||||
|
subject.process
|
||||||
|
party.reload
|
||||||
|
|
||||||
|
# The party's job should now be set to the job_record.
|
||||||
|
expect(party.job).to eq(job_record)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'assigns the correct main skill to the party' do
|
||||||
|
# Before processing, the party should not have a job.
|
||||||
|
expect(party.job).to be_nil
|
||||||
|
|
||||||
|
# Process the job data.
|
||||||
|
subject.process
|
||||||
|
party.reload
|
||||||
|
|
||||||
|
main_skill = party.job.skills.where(main: true).first
|
||||||
|
expect(party.skill0.id).to eq(main_skill.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'associates the correct job skills' do
|
||||||
|
# Before processing, the party should not have a job.
|
||||||
|
expect(party.job).to be_nil
|
||||||
|
|
||||||
|
# Process the job data.
|
||||||
|
subject.process
|
||||||
|
party.reload
|
||||||
|
|
||||||
|
# We assume that the processor assigns up to four subskills to party attributes,
|
||||||
|
# for example, party.skill0, party.skill1, etc.
|
||||||
|
# Get the expected subskills (using order and taking the first four).
|
||||||
|
data = deck_data.with_indifferent_access
|
||||||
|
expected_subskills = data.dig('deck', 'pc', 'set_action').pluck(:name)
|
||||||
|
actual_subskills = [party.skill1.name_en, party.skill2.name_en, party.skill3.name_en]
|
||||||
|
expect(actual_subskills).to eq(expected_subskills)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'assigns the correct accessory to the party' do
|
||||||
|
# Process the job data.
|
||||||
|
subject.process
|
||||||
|
party.reload
|
||||||
|
|
||||||
|
expect(party.accessory.granblue_id).to eq(1.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid job data' do
|
||||||
|
let(:invalid_data) { 'invalid data' }
|
||||||
|
subject { described_class.new(party, invalid_data, language: 'en') }
|
||||||
|
|
||||||
|
it 'logs an error and does not assign a job' do
|
||||||
|
expect { subject.process }.not_to(change { party.reload.job })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:each) do |example|
|
||||||
|
if example.exception
|
||||||
|
puts "\nDEBUG [JobProcessor]: #{example.full_description} failed with error: #{example.exception.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
37
spec/services/processors/summon_processor_spec.rb
Normal file
37
spec/services/processors/summon_processor_spec.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Processors::SummonProcessor, type: :model do
|
||||||
|
let(:party) { create(:party) }
|
||||||
|
let(:deck_data) do
|
||||||
|
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample.json')
|
||||||
|
JSON.parse(File.read(file_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(party, deck_data, language: 'en') }
|
||||||
|
|
||||||
|
context 'with valid summon data' do
|
||||||
|
it 'creates the correct number of GridSummon records' do
|
||||||
|
expect { subject.process }.to change(GridSummon, :count).by(7)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid summon data' do
|
||||||
|
let(:deck_data) { "invalid data" }
|
||||||
|
it 'does not create any GridSummon and logs an error containing "SUMMON"' do
|
||||||
|
expect { subject.process }.not_to change(GridSummon, :count)
|
||||||
|
begin
|
||||||
|
subject.process
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:each) do |example|
|
||||||
|
if example.exception
|
||||||
|
puts "\nDEBUG [SummonProcessor]: #{example.full_description} failed with error: #{example.exception.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
137
spec/services/processors/weapon_processor_spec.rb
Normal file
137
spec/services/processors/weapon_processor_spec.rb
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Processors::WeaponProcessor, type: :model do
|
||||||
|
let(:party) { create(:party) }
|
||||||
|
# Minimal deck data for testing private methods.
|
||||||
|
let(:dummy_deck_data) { { 'deck' => { 'pc' => { 'weapons' => {} } } } }
|
||||||
|
let(:processor) { described_class.new(party, dummy_deck_data) }
|
||||||
|
|
||||||
|
describe '#level_to_transcendence' do
|
||||||
|
it 'returns 0 for levels less than 200' do
|
||||||
|
expect(processor.send(:level_to_transcendence, 150)).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the correct transcendence step for levels >= 200' do
|
||||||
|
expect(processor.send(:level_to_transcendence, 200)).to eq(0)
|
||||||
|
expect(processor.send(:level_to_transcendence, 215)).to eq(1)
|
||||||
|
expect(processor.send(:level_to_transcendence, 250)).to eq(5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#matches_key?' do
|
||||||
|
it 'returns true if candidate key falls within a range' do
|
||||||
|
expect(processor.send(:matches_key?, '700', '697-706')).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if candidate key is below the range' do
|
||||||
|
expect(processor.send(:matches_key?, '696', '697-706')).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if candidate key is above the range' do
|
||||||
|
expect(processor.send(:matches_key?, '707', '697-706')).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true if candidate key exactly matches the mapping' do
|
||||||
|
expect(processor.send(:matches_key?, '700', '700')).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#process_weapon_ax' do
|
||||||
|
let(:grid_weapon) { build(:grid_weapon, party: party) }
|
||||||
|
it 'flattens nested augment_skill_info and assigns ax_modifier and ax_strength' do
|
||||||
|
ax_skill_info = [
|
||||||
|
[
|
||||||
|
{ 'skill_id' => '1588', 'effect_value' => '3', 'show_value' => '3%' },
|
||||||
|
{ 'skill_id' => '1591', 'effect_value' => '5', 'show_value' => '5%' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
processor.send(:process_weapon_ax, grid_weapon, ax_skill_info)
|
||||||
|
expect(grid_weapon.ax_modifier1).to eq(2) # from 1588 → 2
|
||||||
|
expect(grid_weapon.ax_strength1).to eq(3)
|
||||||
|
expect(grid_weapon.ax_modifier2).to eq(3) # from 1591 → 3
|
||||||
|
expect(grid_weapon.ax_strength2).to eq(5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#map_arousal_to_awakening' do
|
||||||
|
it 'returns nil if there is no form key' do
|
||||||
|
arousal_data = {}
|
||||||
|
expect(processor.send(:map_arousal_to_awakening, arousal_data)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the awakening id if found' do
|
||||||
|
arousal_data = {
|
||||||
|
"is_arousal_weapon": true,
|
||||||
|
"level": 4,
|
||||||
|
"form": 2,
|
||||||
|
"form_name": 'Defense',
|
||||||
|
"remain_for_next_level": 0,
|
||||||
|
"width": 100,
|
||||||
|
"is_complete_condition": true,
|
||||||
|
"max_level": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
awakening = Awakening.find_by(slug: 'weapon-def')
|
||||||
|
expect(processor.send(:map_arousal_to_awakening, arousal_data)).to eq(awakening.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#process_weapon_keys' do
|
||||||
|
let(:deck_data) do
|
||||||
|
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample2.json')
|
||||||
|
JSON.parse(File.read(file_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:deck_weapon) do
|
||||||
|
deck_data['deck']['pc']['weapons']['7']
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:canonical_weapon) do
|
||||||
|
Weapon.find_by(granblue_id: deck_weapon['master']['id'])
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:grid_weapon) do
|
||||||
|
create(:grid_weapon, weapon: canonical_weapon, party: party)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the raw key is provided via KEY_MAPPING' do
|
||||||
|
it 'assigns the mapped WeaponKey' do
|
||||||
|
skill_ids = [deck_weapon['skill1'], deck_weapon['skill2'], deck_weapon['skill3']].compact.map { |s| s['id'] }
|
||||||
|
processor.send(:process_weapon_keys, grid_weapon, skill_ids)
|
||||||
|
expect(grid_weapon.weapon_key1_id).to be_nil
|
||||||
|
expect(grid_weapon.weapon_key2_id).to eq(WeaponKey.find_by(slug: 'pendulum-beta').id)
|
||||||
|
expect(grid_weapon.weapon_key3_id).to eq(WeaponKey.find_by(slug: 'pendulum-extremity').id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no matching WeaponKey is found' do
|
||||||
|
it 'logs a warning and does not assign the key' do
|
||||||
|
processor.send(:process_weapon_keys, grid_weapon, ['unknown'])
|
||||||
|
expect(grid_weapon.weapon_key1_id).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'processing a complete canonical deck' do
|
||||||
|
let(:deck_data) do
|
||||||
|
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample2.json')
|
||||||
|
JSON.parse(File.read(file_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(party, deck_data) }
|
||||||
|
|
||||||
|
it 'processes the deck and creates the expected number of GridWeapon records' do
|
||||||
|
# Assume the canonical records are already loaded (via canonical.rb).
|
||||||
|
expect { subject.process }.to change(GridWeapon, :count).by(13)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates the correct main weapon' do
|
||||||
|
# In this canonical deck, the main weapon (slot 1) should be Parazonium.
|
||||||
|
main_weapon = GridWeapon.find_by(position: -1)
|
||||||
|
expect(main_weapon).not_to be_nil
|
||||||
|
expect(main_weapon.weapon.granblue_id).to eq('1040108700')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue