Add weapon stat modifiers for AX skills and befoulments (#202)
* add weapon_stat_modifiers table for ax skills and befoulments * add fk columns for ax modifiers and befoulments, replace has_ax_skills with augment_type * update models for weapon_stat_modifier fks and befoulments * update blueprints for weapon_stat_modifier serialization * update import service for weapon_stat_modifier fks and befoulments * add weapon_stat_modifiers controller and update params for fks * update tests and factories for weapon_stat_modifier fks * fix remaining has_ax_skills and ax_modifier references * add ax_modifier and befoulment_modifier to eager loading * fix ax modifier column naming and migration approach * add game_skill_ids for befoulment modifiers
This commit is contained in:
parent
a6b7e26210
commit
1f80e4189f
38 changed files with 1596 additions and 122 deletions
|
|
@ -7,10 +7,28 @@ module Api
|
|||
:created_at, :updated_at
|
||||
|
||||
field :ax, if: ->(_, obj, _) { obj.ax_modifier1.present? } do |obj|
|
||||
[
|
||||
{ modifier: obj.ax_modifier1, strength: obj.ax_strength1 },
|
||||
{ modifier: obj.ax_modifier2, strength: obj.ax_strength2 }
|
||||
].compact_blank
|
||||
skills = []
|
||||
if obj.ax_modifier1.present?
|
||||
skills << {
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.ax_modifier1),
|
||||
strength: obj.ax_strength1
|
||||
}
|
||||
end
|
||||
if obj.ax_modifier2.present?
|
||||
skills << {
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.ax_modifier2),
|
||||
strength: obj.ax_strength2
|
||||
}
|
||||
end
|
||||
skills
|
||||
end
|
||||
|
||||
field :befoulment, if: ->(_, obj, _) { obj.befoulment_modifier.present? } do |obj|
|
||||
{
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.befoulment_modifier),
|
||||
strength: obj.befoulment_strength,
|
||||
exorcism_level: obj.exorcism_level
|
||||
}
|
||||
end
|
||||
|
||||
field :awakening, if: ->(_, obj, _) { obj.awakening.present? } do |obj|
|
||||
|
|
|
|||
|
|
@ -15,11 +15,29 @@ module Api
|
|||
end
|
||||
|
||||
view :nested do
|
||||
field :ax, if: ->(_field_name, w, _options) { w.weapon.present? && w.weapon.ax } do |w|
|
||||
[
|
||||
{ modifier: w.ax_modifier1, strength: w.ax_strength1 },
|
||||
{ modifier: w.ax_modifier2, strength: w.ax_strength2 }
|
||||
]
|
||||
field :ax, if: ->(_field_name, w, _options) { w.ax_modifier1.present? } do |w|
|
||||
skills = []
|
||||
if w.ax_modifier1.present?
|
||||
skills << {
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(w.ax_modifier1),
|
||||
strength: w.ax_strength1
|
||||
}
|
||||
end
|
||||
if w.ax_modifier2.present?
|
||||
skills << {
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(w.ax_modifier2),
|
||||
strength: w.ax_strength2
|
||||
}
|
||||
end
|
||||
skills
|
||||
end
|
||||
|
||||
field :befoulment, if: ->(_field_name, w, _options) { w.befoulment_modifier.present? } do |w|
|
||||
{
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(w.befoulment_modifier),
|
||||
strength: w.befoulment_strength,
|
||||
exorcism_level: w.exorcism_level
|
||||
}
|
||||
end
|
||||
|
||||
field :awakening, if: ->(_field_name, w, _options) { w.awakening.present? } do |w|
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module Api
|
|||
},
|
||||
has_weapon_keys: w.weapon_series.has_weapon_keys,
|
||||
has_awakening: w.weapon_series.has_awakening,
|
||||
has_ax_skills: w.weapon_series.has_ax_skills,
|
||||
augment_type: w.weapon_series.augment_type,
|
||||
extra: w.weapon_series.extra,
|
||||
element_changeable: w.weapon_series.element_changeable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Api
|
|||
end
|
||||
|
||||
fields :slug, :order, :extra, :element_changeable, :has_weapon_keys,
|
||||
:has_awakening, :has_ax_skills
|
||||
:has_awakening, :augment_type
|
||||
|
||||
view :full do
|
||||
field :weapon_count do |ws|
|
||||
|
|
|
|||
10
app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb
Normal file
10
app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class WeaponStatModifierBlueprint < Blueprinter::Base
|
||||
identifier :id
|
||||
fields :slug, :name_en, :name_jp, :category, :stat, :polarity, :suffix
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -14,7 +14,9 @@ module Api
|
|||
@collection_weapons = @target_user.collection_weapons
|
||||
.includes(:weapon, :awakening,
|
||||
:weapon_key1, :weapon_key2,
|
||||
:weapon_key3, :weapon_key4)
|
||||
:weapon_key3, :weapon_key4,
|
||||
:ax_modifier1, :ax_modifier2,
|
||||
:befoulment_modifier)
|
||||
|
||||
# Apply filters (array_param splits comma-separated values for OR logic)
|
||||
@collection_weapons = @collection_weapons.by_weapon(params[:weapon_id]) if params[:weapon_id]
|
||||
|
|
@ -212,7 +214,8 @@ module Api
|
|||
:weapon_id, :uncap_level, :transcendence_step,
|
||||
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
|
||||
:awakening_id, :awakening_level,
|
||||
:ax_modifier1, :ax_strength1, :ax_modifier2, :ax_strength2,
|
||||
:ax_modifier1_id, :ax_strength1, :ax_modifier2_id, :ax_strength2,
|
||||
:befoulment_modifier_id, :befoulment_strength, :exorcism_level,
|
||||
:element
|
||||
)
|
||||
end
|
||||
|
|
@ -222,7 +225,8 @@ module Api
|
|||
:weapon_id, :uncap_level, :transcendence_step,
|
||||
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
|
||||
:awakening_id, :awakening_level,
|
||||
:ax_modifier1, :ax_strength1, :ax_modifier2, :ax_strength2,
|
||||
:ax_modifier1_id, :ax_strength1, :ax_modifier2_id, :ax_strength2,
|
||||
:befoulment_modifier_id, :befoulment_strength, :exorcism_level,
|
||||
:element
|
||||
])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -291,13 +291,13 @@ module Api
|
|||
##
|
||||
# Normalizes the AX modifier fields for the weapon parameters.
|
||||
#
|
||||
# Sets ax_modifier1 and ax_modifier2 to nil if their integer values equal -1.
|
||||
# Sets ax_modifier1_id and ax_modifier2_id to nil if their integer values equal -1.
|
||||
#
|
||||
# @return [void]
|
||||
def normalize_ax_fields!
|
||||
params[:weapon][:ax_modifier1] = nil if weapon_params[:ax_modifier1].to_i == -1
|
||||
|
||||
params[:weapon][:ax_modifier2] = nil if weapon_params[:ax_modifier2].to_i == -1
|
||||
params[:weapon][:ax_modifier1_id] = nil if weapon_params[:ax_modifier1_id].to_i == -1
|
||||
params[:weapon][:ax_modifier2_id] = nil if weapon_params[:ax_modifier2_id].to_i == -1
|
||||
params[:weapon][:befoulment_modifier_id] = nil if weapon_params[:befoulment_modifier_id].to_i == -1
|
||||
end
|
||||
|
||||
##
|
||||
|
|
@ -515,7 +515,8 @@ module Api
|
|||
:id, :party_id, :weapon_id, :collection_weapon_id,
|
||||
:position, :mainhand, :uncap_level, :transcendence_step, :element,
|
||||
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id,
|
||||
:ax_modifier1, :ax_modifier2, :ax_strength1, :ax_strength2,
|
||||
:ax_modifier1_id, :ax_modifier2_id, :ax_strength1, :ax_strength2,
|
||||
:befoulment_modifier_id, :befoulment_strength, :exorcism_level,
|
||||
:awakening_id, :awakening_level
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -266,7 +266,10 @@ module Api
|
|||
awakening: {},
|
||||
weapon_key1: {},
|
||||
weapon_key2: {},
|
||||
weapon_key3: {}
|
||||
weapon_key3: {},
|
||||
ax_modifier1: {},
|
||||
ax_modifier2: {},
|
||||
befoulment_modifier: {}
|
||||
} },
|
||||
{ summons: :summon },
|
||||
:guidebook1, :guidebook2, :guidebook3,
|
||||
|
|
@ -294,7 +297,7 @@ module Api
|
|||
{ ring1: %i[modifier strength], ring2: %i[modifier strength], ring3: %i[modifier strength], ring4: %i[modifier strength],
|
||||
earring: %i[modifier strength] }],
|
||||
summons_attributes: %i[id party_id summon_id position main friend quick_summon uncap_level transcendence_step],
|
||||
weapons_attributes: %i[id party_id weapon_id position mainhand uncap_level transcendence_step element weapon_key1_id weapon_key2_id weapon_key3_id ax_modifier1 ax_modifier2 ax_strength1 ax_strength2 awakening_id awakening_level]
|
||||
weapons_attributes: %i[id party_id weapon_id position mainhand uncap_level transcendence_step element weapon_key1_id weapon_key2_id weapon_key3_id ax_modifier1_id ax_modifier2_id ax_strength1 ax_strength2 befoulment_modifier_id befoulment_strength exorcism_level awakening_id awakening_level]
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ module Api
|
|||
params.require(:weapon_series).permit(
|
||||
:name_en, :name_jp, :slug, :order,
|
||||
:extra, :element_changeable, :has_weapon_keys,
|
||||
:has_awakening, :has_ax_skills
|
||||
:has_awakening, :augment_type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
21
app/controllers/api/v1/weapon_stat_modifiers_controller.rb
Normal file
21
app/controllers/api/v1/weapon_stat_modifiers_controller.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class WeaponStatModifiersController < Api::V1::ApiController
|
||||
# GET /weapon_stat_modifiers
|
||||
def index
|
||||
@modifiers = WeaponStatModifier.all
|
||||
@modifiers = @modifiers.where(category: params[:category]) if params[:category].present?
|
||||
|
||||
render json: WeaponStatModifierBlueprint.render(@modifiers, root: :weapon_stat_modifiers)
|
||||
end
|
||||
|
||||
# GET /weapon_stat_modifiers/:id
|
||||
def show
|
||||
@modifier = WeaponStatModifier.find(params[:id])
|
||||
render json: WeaponStatModifierBlueprint.render(@modifier)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,10 @@ class CollectionWeapon < ApplicationRecord
|
|||
belongs_to :weapon_key3, class_name: 'WeaponKey', optional: true
|
||||
belongs_to :weapon_key4, class_name: 'WeaponKey', optional: true
|
||||
|
||||
belongs_to :ax_modifier1, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||
|
||||
has_many :grid_weapons, dependent: :nullify
|
||||
|
||||
before_destroy :orphan_grid_items
|
||||
|
|
@ -18,9 +22,11 @@ class CollectionWeapon < ApplicationRecord
|
|||
validates :uncap_level, inclusion: { in: 0..5 }
|
||||
validates :transcendence_step, inclusion: { in: 0..10 }
|
||||
validates :awakening_level, inclusion: { in: 1..20 }
|
||||
validates :exorcism_level, inclusion: { in: 0..5 }, allow_nil: true
|
||||
|
||||
validate :validate_weapon_keys
|
||||
validate :validate_ax_skills
|
||||
validate :validate_befoulment
|
||||
validate :validate_element_change
|
||||
validate :validate_awakening_compatibility
|
||||
validate :validate_awakening_level
|
||||
|
|
@ -29,7 +35,7 @@ class CollectionWeapon < ApplicationRecord
|
|||
scope :by_weapon, ->(weapon_id) { where(weapon_id: weapon_id) }
|
||||
scope :by_series, ->(series_id) { joins(:weapon).where(weapons: { weapon_series_id: series_id }) }
|
||||
scope :with_keys, -> { where.not(weapon_key1_id: nil) }
|
||||
scope :with_ax, -> { where.not(ax_modifier1: nil) }
|
||||
scope :with_ax, -> { where.not(ax_modifier1_id: nil) }
|
||||
scope :by_element, ->(element) { joins(:weapon).where(weapons: { element: element }) }
|
||||
scope :by_rarity, ->(rarity) { joins(:weapon).where(weapons: { rarity: rarity }) }
|
||||
scope :by_proficiency, ->(proficiency) { joins(:weapon).where(weapons: { proficiency: proficiency }) }
|
||||
|
|
@ -87,16 +93,43 @@ class CollectionWeapon < ApplicationRecord
|
|||
end
|
||||
|
||||
def validate_ax_skills
|
||||
# Check for incomplete AX skills regardless of weapon.ax
|
||||
# AX skill 1: must have both modifier and strength
|
||||
if (ax_modifier1.present? && ax_strength1.blank?) ||
|
||||
(ax_modifier1.blank? && ax_strength1.present?)
|
||||
errors.add(:base, "AX skill 1 must have both modifier and strength")
|
||||
end
|
||||
|
||||
# AX skill 2: must have both modifier and strength
|
||||
if (ax_modifier2.present? && ax_strength2.blank?) ||
|
||||
(ax_modifier2.blank? && ax_strength2.present?)
|
||||
errors.add(:base, "AX skill 2 must have both modifier and strength")
|
||||
end
|
||||
|
||||
# Validate category is 'ax'
|
||||
if ax_modifier1.present? && ax_modifier1.category != 'ax'
|
||||
errors.add(:ax_modifier1, "must be an AX skill modifier")
|
||||
end
|
||||
if ax_modifier2.present? && ax_modifier2.category != 'ax'
|
||||
errors.add(:ax_modifier2, "must be an AX skill modifier")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_befoulment
|
||||
# Befoulment: must have both modifier and strength
|
||||
if (befoulment_modifier.present? && befoulment_strength.blank?) ||
|
||||
(befoulment_modifier.blank? && befoulment_strength.present?)
|
||||
errors.add(:base, "Befoulment must have both modifier and strength")
|
||||
end
|
||||
|
||||
# Validate category is 'befoulment'
|
||||
if befoulment_modifier.present? && befoulment_modifier.category != 'befoulment'
|
||||
errors.add(:befoulment_modifier, "must be a befoulment modifier")
|
||||
end
|
||||
|
||||
# Exorcism level only makes sense with befoulment
|
||||
if exorcism_level.present? && exorcism_level > 0 && befoulment_modifier.blank?
|
||||
errors.add(:exorcism_level, "cannot be set without a befoulment")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_element_change
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ class GridWeapon < ApplicationRecord
|
|||
belongs_to :awakening, optional: true
|
||||
belongs_to :collection_weapon, optional: true
|
||||
|
||||
belongs_to :ax_modifier1, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||
|
||||
# Orphan status scopes
|
||||
scope :orphaned, -> { where(orphaned: true) }
|
||||
scope :not_orphaned, -> { where(orphaned: false) }
|
||||
|
|
@ -55,10 +59,13 @@ class GridWeapon < ApplicationRecord
|
|||
|
||||
##### Amoeba configuration
|
||||
amoeba do
|
||||
nullify :ax_modifier1
|
||||
nullify :ax_modifier2
|
||||
nullify :ax_modifier1_id
|
||||
nullify :ax_modifier2_id
|
||||
nullify :ax_strength1
|
||||
nullify :ax_strength2
|
||||
nullify :befoulment_modifier_id
|
||||
nullify :befoulment_strength
|
||||
nullify :exorcism_level
|
||||
end
|
||||
|
||||
##
|
||||
|
|
@ -84,10 +91,13 @@ class GridWeapon < ApplicationRecord
|
|||
weapon_key2_id: collection_weapon.weapon_key2_id,
|
||||
weapon_key3_id: collection_weapon.weapon_key3_id,
|
||||
weapon_key4_id: collection_weapon.weapon_key4_id,
|
||||
ax_modifier1: collection_weapon.ax_modifier1,
|
||||
ax_modifier1_id: collection_weapon.ax_modifier1_id,
|
||||
ax_strength1: collection_weapon.ax_strength1,
|
||||
ax_modifier2: collection_weapon.ax_modifier2,
|
||||
ax_modifier2_id: collection_weapon.ax_modifier2_id,
|
||||
ax_strength2: collection_weapon.ax_strength2,
|
||||
befoulment_modifier_id: collection_weapon.befoulment_modifier_id,
|
||||
befoulment_strength: collection_weapon.befoulment_strength,
|
||||
exorcism_level: collection_weapon.exorcism_level,
|
||||
awakening_id: collection_weapon.awakening_id,
|
||||
awakening_level: collection_weapon.awakening_level
|
||||
)
|
||||
|
|
@ -116,10 +126,13 @@ class GridWeapon < ApplicationRecord
|
|||
weapon_key2_id != collection_weapon.weapon_key2_id ||
|
||||
weapon_key3_id != collection_weapon.weapon_key3_id ||
|
||||
weapon_key4_id != collection_weapon.weapon_key4_id ||
|
||||
ax_modifier1 != collection_weapon.ax_modifier1 ||
|
||||
ax_modifier1_id != collection_weapon.ax_modifier1_id ||
|
||||
ax_strength1 != collection_weapon.ax_strength1 ||
|
||||
ax_modifier2 != collection_weapon.ax_modifier2 ||
|
||||
ax_modifier2_id != collection_weapon.ax_modifier2_id ||
|
||||
ax_strength2 != collection_weapon.ax_strength2 ||
|
||||
befoulment_modifier_id != collection_weapon.befoulment_modifier_id ||
|
||||
befoulment_strength != collection_weapon.befoulment_strength ||
|
||||
exorcism_level != collection_weapon.exorcism_level ||
|
||||
awakening_id != collection_weapon.awakening_id ||
|
||||
awakening_level != collection_weapon.awakening_level
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ class WeaponSeries < ApplicationRecord
|
|||
has_many :weapon_key_series, dependent: :destroy
|
||||
has_many :weapon_keys, through: :weapon_key_series
|
||||
|
||||
enum :augment_type, { none: 0, ax: 1, befoulment: 2 }, default: :none
|
||||
|
||||
validates :name_en, presence: true
|
||||
validates :name_jp, presence: true
|
||||
validates :slug, presence: true, uniqueness: true
|
||||
|
|
@ -15,7 +17,8 @@ class WeaponSeries < ApplicationRecord
|
|||
scope :element_changeable, -> { where(element_changeable: true) }
|
||||
scope :with_weapon_keys, -> { where(has_weapon_keys: true) }
|
||||
scope :with_awakening, -> { where(has_awakening: true) }
|
||||
scope :with_ax_skills, -> { where(has_ax_skills: true) }
|
||||
scope :with_ax_skills, -> { where(augment_type: :ax) }
|
||||
scope :with_befoulments, -> { where(augment_type: :befoulment) }
|
||||
|
||||
# Slug constants for commonly referenced series
|
||||
DARK_OPUS = 'dark-opus'
|
||||
|
|
|
|||
31
app/models/weapon_stat_modifier.rb
Normal file
31
app/models/weapon_stat_modifier.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
##
|
||||
# Reference table for weapon stat modifiers (AX skills and befoulments).
|
||||
#
|
||||
# AX skills are positive modifiers that can be applied to certain weapons.
|
||||
# Befoulments are negative modifiers that appear on Odiant weapons.
|
||||
#
|
||||
class WeaponStatModifier < ApplicationRecord
|
||||
CATEGORIES = %w[ax befoulment].freeze
|
||||
|
||||
validates :slug, presence: true, uniqueness: true
|
||||
validates :name_en, presence: true
|
||||
validates :category, presence: true, inclusion: { in: CATEGORIES }
|
||||
validates :polarity, inclusion: { in: [-1, 1] }
|
||||
|
||||
scope :ax_skills, -> { where(category: 'ax') }
|
||||
scope :befoulments, -> { where(category: 'befoulment') }
|
||||
|
||||
def self.find_by_game_skill_id(id)
|
||||
find_by(game_skill_id: id.to_i)
|
||||
end
|
||||
|
||||
def buff?
|
||||
polarity == 1
|
||||
end
|
||||
|
||||
def debuff?
|
||||
polarity == -1
|
||||
end
|
||||
end
|
||||
|
|
@ -41,26 +41,6 @@ module Processors
|
|||
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
|
||||
|
|
@ -331,7 +311,8 @@ module Processors
|
|||
# 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.
|
||||
# This method flattens the data and assigns each skill's modifier and strength.
|
||||
# Modifiers are now looked up by game_skill_id in the weapon_stat_modifiers table.
|
||||
#
|
||||
# @param grid_weapon [GridWeapon] the grid weapon record being built.
|
||||
# @param ax_skill_info [Array] the raw AX skill info.
|
||||
|
|
@ -340,14 +321,59 @@ module Processors
|
|||
# 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
|
||||
break if idx >= 2 # Only 2 AX skill slots
|
||||
|
||||
game_skill_id = ax['skill_id'].to_i
|
||||
modifier = find_modifier_by_game_skill_id(game_skill_id)
|
||||
|
||||
unless modifier
|
||||
Rails.logger.warn(
|
||||
"[WeaponProcessor] Unknown augment skill_id=#{game_skill_id} " \
|
||||
"icon=#{ax['augment_skill_icon_image']}"
|
||||
)
|
||||
next
|
||||
end
|
||||
|
||||
strength = parse_augment_strength(ax['effect_value'], ax['show_value'])
|
||||
grid_weapon["ax_modifier#{idx + 1}_id"] = modifier.id
|
||||
grid_weapon["ax_strength#{idx + 1}"] = strength
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Finds a WeaponStatModifier by its game_skill_id.
|
||||
# Uses memoization to cache lookups.
|
||||
#
|
||||
# @param game_skill_id [Integer] the game's skill ID.
|
||||
# @return [WeaponStatModifier, nil]
|
||||
def find_modifier_by_game_skill_id(game_skill_id)
|
||||
@modifier_cache ||= {}
|
||||
@modifier_cache[game_skill_id] ||= WeaponStatModifier.find_by(game_skill_id: game_skill_id)
|
||||
end
|
||||
|
||||
##
|
||||
# Parses the strength value from effect_value or show_value.
|
||||
#
|
||||
# @param effect_value [String, nil] the effect_value field.
|
||||
# @param show_value [String, nil] the show_value field.
|
||||
# @return [Float, nil]
|
||||
def parse_augment_strength(effect_value, show_value)
|
||||
if effect_value.present?
|
||||
# Handle "1_3" format (seems to be "tier_value")
|
||||
if effect_value.to_s.include?('_')
|
||||
return effect_value.to_s.split('_').last.to_f
|
||||
end
|
||||
return effect_value.to_f if effect_value.to_s.match?(/\A[\d.]+\z/)
|
||||
end
|
||||
|
||||
# Try show_value (e.g., "3%")
|
||||
if show_value.present?
|
||||
return show_value.to_s.gsub('%', '').to_f
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
##
|
||||
# Maps the in‑game awakening data (stored under "arousal") to our Awakening record.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class WeaponImportService
|
|||
@skipped = []
|
||||
@errors = []
|
||||
@awakening_cache = {}
|
||||
@modifier_cache = {}
|
||||
@processed_game_ids = []
|
||||
end
|
||||
|
||||
|
|
@ -192,9 +193,18 @@ class WeaponImportService
|
|||
awakening_attrs = parse_awakening(param['arousal'])
|
||||
attrs.merge!(awakening_attrs) if awakening_attrs
|
||||
|
||||
# Parse AX skills if present
|
||||
ax_attrs = parse_ax_skills(param['augment_skill_info'])
|
||||
attrs.merge!(ax_attrs) if ax_attrs
|
||||
# Check if this is an Odiant (befoulment) weapon
|
||||
odiant = param['odiant']
|
||||
if odiant && odiant['is_odiant_weapon'] == true
|
||||
# Parse befoulment from augment_skill_info
|
||||
befoulment_attrs = parse_befoulment(param['augment_skill_info'])
|
||||
attrs.merge!(befoulment_attrs) if befoulment_attrs
|
||||
attrs[:exorcism_level] = odiant['exorcision_level'].to_i.clamp(0, 5)
|
||||
else
|
||||
# Regular weapon - parse AX skills
|
||||
ax_attrs = parse_ax_skills(param['augment_skill_info'])
|
||||
attrs.merge!(ax_attrs) if ax_attrs
|
||||
end
|
||||
|
||||
attrs
|
||||
end
|
||||
|
|
@ -253,18 +263,18 @@ class WeaponImportService
|
|||
|
||||
# First AX skill
|
||||
if skills[0].is_a?(Hash)
|
||||
ax1 = parse_single_ax_skill(skills[0])
|
||||
ax1 = parse_single_augment_skill(skills[0])
|
||||
if ax1
|
||||
attrs[:ax_modifier1] = ax1[:modifier]
|
||||
attrs[:ax_modifier1_id] = ax1[:modifier_id]
|
||||
attrs[:ax_strength1] = ax1[:strength]
|
||||
end
|
||||
end
|
||||
|
||||
# Second AX skill
|
||||
if skills[1].is_a?(Hash)
|
||||
ax2 = parse_single_ax_skill(skills[1])
|
||||
ax2 = parse_single_augment_skill(skills[1])
|
||||
if ax2
|
||||
attrs[:ax_modifier2] = ax2[:modifier]
|
||||
attrs[:ax_modifier2_id] = ax2[:modifier_id]
|
||||
attrs[:ax_strength2] = ax2[:strength]
|
||||
end
|
||||
end
|
||||
|
|
@ -273,26 +283,60 @@ class WeaponImportService
|
|||
end
|
||||
|
||||
##
|
||||
# Parses a single AX skill from game data.
|
||||
# Parses befoulment data from game format.
|
||||
# Odiant weapons have a single befoulment in augment_skill_info.
|
||||
#
|
||||
# @param skill [Hash] Single AX skill data with skill_id and effect_value
|
||||
# @return [Hash, nil] { modifier:, strength: } or nil
|
||||
def parse_single_ax_skill(skill)
|
||||
return nil unless skill['skill_id'].present?
|
||||
# @param augment_skill_info [Array] The game's augment skill data
|
||||
# @return [Hash, nil] Befoulment attributes or nil if no befoulment
|
||||
def parse_befoulment(augment_skill_info)
|
||||
return nil if augment_skill_info.blank? || !augment_skill_info.is_a?(Array)
|
||||
|
||||
# The skill_id maps to our AX modifier
|
||||
modifier = skill['skill_id'].to_i
|
||||
skills = augment_skill_info.first
|
||||
return nil if skills.blank? || !skills.is_a?(Array)
|
||||
|
||||
# Parse strength from effect_value (may be "3" or "1_3" format)
|
||||
# or from show_value (may be "3%" format)
|
||||
strength = parse_ax_strength(skill['effect_value'], skill['show_value'])
|
||||
skill = skills.first
|
||||
return nil unless skill.is_a?(Hash)
|
||||
|
||||
return nil unless strength
|
||||
result = parse_single_augment_skill(skill)
|
||||
return nil unless result
|
||||
|
||||
{ modifier: modifier, strength: strength }
|
||||
{
|
||||
befoulment_modifier_id: result[:modifier_id],
|
||||
befoulment_strength: result[:strength]
|
||||
}
|
||||
end
|
||||
|
||||
def parse_ax_strength(effect_value, show_value)
|
||||
##
|
||||
# Parses a single augment skill (AX or befoulment) from game data.
|
||||
#
|
||||
# @param skill [Hash] Single skill data with skill_id and effect_value
|
||||
# @return [Hash, nil] { modifier_id:, strength: } or nil
|
||||
def parse_single_augment_skill(skill)
|
||||
return nil unless skill['skill_id'].present?
|
||||
|
||||
game_skill_id = skill['skill_id'].to_i
|
||||
modifier = find_modifier_by_game_skill_id(game_skill_id)
|
||||
|
||||
unless modifier
|
||||
# Log unknown skill ID with icon for discovery
|
||||
Rails.logger.warn(
|
||||
"[WeaponImportService] Unknown augment skill_id=#{game_skill_id} " \
|
||||
"icon=#{skill['augment_skill_icon_image']}"
|
||||
)
|
||||
return nil
|
||||
end
|
||||
|
||||
strength = parse_augment_strength(skill['effect_value'], skill['show_value'])
|
||||
return nil unless strength
|
||||
|
||||
{ modifier_id: modifier.id, strength: strength }
|
||||
end
|
||||
|
||||
def find_modifier_by_game_skill_id(game_skill_id)
|
||||
@modifier_cache[game_skill_id] ||= WeaponStatModifier.find_by(game_skill_id: game_skill_id)
|
||||
end
|
||||
|
||||
def parse_augment_strength(effect_value, show_value)
|
||||
# Try effect_value first
|
||||
if effect_value.present?
|
||||
# Handle "1_3" format (seems to be "tier_value")
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ Rails.application.routes.draw do
|
|||
get 'for_slot/:slot', action: :for_slot, as: :for_slot
|
||||
end
|
||||
end
|
||||
resources :weapon_stat_modifiers, only: %i[index show]
|
||||
|
||||
# Grid artifacts
|
||||
resources :grid_artifacts, only: %i[create update destroy] do
|
||||
|
|
|
|||
55
db/data/20251230000001_seed_weapon_stat_modifiers.rb
Normal file
55
db/data/20251230000001_seed_weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SeedWeaponStatModifiers < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
ax_skills = [
|
||||
# Primary AX Skills
|
||||
{ slug: 'ax_hp', name_en: 'HP', name_jp: 'HP', category: 'ax', stat: 'hp', polarity: 1, suffix: '%', base_min: 1, base_max: 11, game_skill_id: 1588 },
|
||||
{ slug: 'ax_atk', name_en: 'ATK', name_jp: '攻撃', category: 'ax', stat: 'atk', polarity: 1, suffix: '%', base_min: 1, base_max: 3.5, game_skill_id: 1589 },
|
||||
{ slug: 'ax_def', name_en: 'DEF', name_jp: '防御', category: 'ax', stat: 'def', polarity: 1, suffix: '%', base_min: 1, base_max: 8, game_skill_id: 1590 },
|
||||
{ slug: 'ax_ca_dmg', name_en: 'C.A. DMG', name_jp: '奥義ダメ', category: 'ax', stat: 'ca_dmg', polarity: 1, suffix: '%', base_min: 2, base_max: 8.5, game_skill_id: 1591 },
|
||||
{ slug: 'ax_multiattack', name_en: 'Multiattack Rate', name_jp: '連撃率', category: 'ax', stat: 'multiattack', polarity: 1, suffix: '%', base_min: 1, base_max: 4, game_skill_id: 1592 },
|
||||
|
||||
# Secondary AX Skills
|
||||
{ slug: 'ax_debuff_res', name_en: 'Debuff Resistance', name_jp: '弱体耐性', category: 'ax', stat: 'debuff_res', polarity: 1, suffix: '%', base_min: 1, base_max: 3, game_skill_id: 1593 },
|
||||
{ slug: 'ax_ele_atk', name_en: 'Elemental ATK', name_jp: '全属性攻撃力', category: 'ax', stat: 'ele_atk', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1594 },
|
||||
{ slug: 'ax_healing', name_en: 'Healing', name_jp: '回復性能', category: 'ax', stat: 'healing', polarity: 1, suffix: '%', base_min: 2, base_max: 5, game_skill_id: 1595 },
|
||||
{ slug: 'ax_da', name_en: 'Double Attack Rate', name_jp: 'DA確率', category: 'ax', stat: 'da', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1596 },
|
||||
{ slug: 'ax_ta', name_en: 'Triple Attack Rate', name_jp: 'TA確率', category: 'ax', stat: 'ta', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1597 },
|
||||
{ slug: 'ax_ca_cap', name_en: 'C.A. DMG Cap', name_jp: '奥義上限', category: 'ax', stat: 'ca_cap', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1599 },
|
||||
{ slug: 'ax_stamina', name_en: 'Stamina', name_jp: '渾身', category: 'ax', stat: 'stamina', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1600 },
|
||||
{ slug: 'ax_enmity', name_en: 'Enmity', name_jp: '背水', category: 'ax', stat: 'enmity', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1601 },
|
||||
|
||||
# Extended AX Skills (axType 2)
|
||||
{ slug: 'ax_skill_supp', name_en: 'Supplemental Skill DMG', name_jp: 'アビ与ダメ上昇', category: 'ax', stat: 'skill_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1719 },
|
||||
{ slug: 'ax_ca_supp', name_en: 'Supplemental C.A. DMG', name_jp: '奥義与ダメ上昇', category: 'ax', stat: 'ca_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1720 },
|
||||
{ slug: 'ax_ele_dmg_red', name_en: 'Elemental DMG Reduction', name_jp: '属性ダメ軽減', category: 'ax', stat: 'ele_dmg_red', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1721 },
|
||||
{ slug: 'ax_na_cap', name_en: 'Normal ATK DMG Cap', name_jp: '通常ダメ上限', category: 'ax', stat: 'na_cap', polarity: 1, suffix: '%', base_min: 0.5, base_max: 1.5, game_skill_id: 1722 },
|
||||
|
||||
# Utility AX Skills (axType 3)
|
||||
{ slug: 'ax_exp', name_en: 'EXP Gain', name_jp: 'EXP UP', category: 'ax', stat: 'exp', polarity: 1, suffix: '%', base_min: 5, base_max: 10, game_skill_id: 1837 },
|
||||
{ slug: 'ax_rupie', name_en: 'Rupie Gain', name_jp: '獲得ルピ', category: 'ax', stat: 'rupie', polarity: 1, suffix: '%', base_min: 10, base_max: 20, game_skill_id: 1838 }
|
||||
]
|
||||
|
||||
befoulments = [
|
||||
# Befoulments - game_skill_ids from game data (2873-2881, 2876 doesn't exist)
|
||||
{ slug: 'befoul_atk_down', name_en: 'ATK Down', name_jp: '攻撃力DOWN', category: 'befoulment', stat: 'atk', polarity: -1, suffix: '%', base_min: -12, base_max: -6, game_skill_id: 2873 },
|
||||
{ slug: 'befoul_ability_dmg_down', name_en: 'Ability DMG Down', name_jp: 'アビリティダメージDOWN', category: 'befoulment', stat: 'ability_dmg', polarity: -1, suffix: '%', base_min: -50, base_max: -50, game_skill_id: 2874 },
|
||||
{ slug: 'befoul_ca_dmg_down', name_en: 'CA DMG Down', name_jp: '奥義ダメージDOWN', category: 'befoulment', stat: 'ca_dmg', polarity: -1, suffix: '%', base_min: -38, base_max: -26, game_skill_id: 2875 },
|
||||
{ slug: 'befoul_da_ta_down', name_en: 'DA/TA Down', name_jp: '連撃率DOWN', category: 'befoulment', stat: 'da_ta', polarity: -1, suffix: '%', base_min: -22, base_max: -19, game_skill_id: 2877 },
|
||||
{ slug: 'befoul_debuff_down', name_en: 'Debuff Success Down', name_jp: '弱体成功率DOWN', category: 'befoulment', stat: 'debuff_success', polarity: -1, suffix: '%', base_min: -16, base_max: -6, game_skill_id: 2878 },
|
||||
{ slug: 'befoul_hp_down', name_en: 'Max HP Down', name_jp: '最大HP減少', category: 'befoulment', stat: 'hp', polarity: -1, suffix: '%', base_min: -50, base_max: -26, game_skill_id: 2879 },
|
||||
{ slug: 'befoul_def_down', name_en: 'DEF Down', name_jp: '防御力DOWN', category: 'befoulment', stat: 'def', polarity: -1, suffix: '%', base_min: -25, base_max: -21, game_skill_id: 2880 },
|
||||
{ slug: 'befoul_dot', name_en: 'Damage Over Time', name_jp: '毎ターンダメージ', category: 'befoulment', stat: 'dot', polarity: -1, suffix: '%', base_min: 6, base_max: 16, game_skill_id: 2881 }
|
||||
]
|
||||
|
||||
now = Time.current
|
||||
(ax_skills + befoulments).each do |attrs|
|
||||
WeaponStatModifier.create!(attrs.merge(created_at: now, updated_at: now))
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
WeaponStatModifier.delete_all
|
||||
end
|
||||
end
|
||||
115
db/data/20251230000002_migrate_ax_modifiers_to_fk.rb
Normal file
115
db/data/20251230000002_migrate_ax_modifiers_to_fk.rb
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MigrateAxModifiersToFk < ActiveRecord::Migration[8.0]
|
||||
# Old AX_MAPPING from WeaponProcessor stored internal integer values (0, 1, 2...)
|
||||
# We need to map: old internal_value => game_skill_id => weapon_stat_modifier.id
|
||||
OLD_INTERNAL_TO_GAME_SKILL_ID = {
|
||||
2 => 1588, # HP
|
||||
0 => 1589, # ATK
|
||||
1 => 1590, # DEF
|
||||
3 => 1591, # C.A. DMG
|
||||
4 => 1592, # Multiattack Rate
|
||||
9 => 1593, # Debuff Resistance
|
||||
13 => 1594, # Elemental ATK
|
||||
10 => 1595, # Healing
|
||||
5 => 1596, # Double Attack Rate
|
||||
6 => 1597, # Triple Attack Rate
|
||||
8 => 1599, # C.A. DMG Cap
|
||||
12 => 1600, # Stamina
|
||||
11 => 1601, # Enmity
|
||||
15 => 1719, # Supplemental Skill DMG
|
||||
16 => 1720, # Supplemental C.A. DMG
|
||||
17 => 1721, # Elemental DMG Reduction
|
||||
14 => 1722 # Normal ATK DMG Cap
|
||||
}.freeze
|
||||
|
||||
def up
|
||||
# Build lookup: old_internal_value -> new FK id
|
||||
modifier_by_game_skill_id = WeaponStatModifier.pluck(:game_skill_id, :id).to_h
|
||||
old_to_new_id = OLD_INTERNAL_TO_GAME_SKILL_ID.transform_values do |game_skill_id|
|
||||
modifier_by_game_skill_id[game_skill_id]
|
||||
end
|
||||
|
||||
# Use raw SQL to query old integer columns (ax_modifier1, ax_modifier2)
|
||||
# and update the new FK columns (ax_modifier1_id, ax_modifier2_id)
|
||||
|
||||
# Migrate CollectionWeapon
|
||||
execute <<-SQL.squish
|
||||
UPDATE collection_weapons
|
||||
SET ax_modifier1_id = CASE ax_modifier1
|
||||
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier1 IS NOT NULL
|
||||
SQL
|
||||
|
||||
execute <<-SQL.squish
|
||||
UPDATE collection_weapons
|
||||
SET ax_modifier2_id = CASE ax_modifier2
|
||||
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier2 IS NOT NULL
|
||||
SQL
|
||||
|
||||
# Migrate GridWeapon
|
||||
execute <<-SQL.squish
|
||||
UPDATE grid_weapons
|
||||
SET ax_modifier1_id = CASE ax_modifier1
|
||||
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier1 IS NOT NULL
|
||||
SQL
|
||||
|
||||
execute <<-SQL.squish
|
||||
UPDATE grid_weapons
|
||||
SET ax_modifier2_id = CASE ax_modifier2
|
||||
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier2 IS NOT NULL
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
# Build reverse lookup: new FK id -> old internal value
|
||||
modifier_by_game_skill_id = WeaponStatModifier.pluck(:game_skill_id, :id).to_h
|
||||
game_skill_id_to_internal = OLD_INTERNAL_TO_GAME_SKILL_ID.invert
|
||||
new_id_to_old = modifier_by_game_skill_id.each_with_object({}) do |(game_skill_id, new_id), hash|
|
||||
old_val = game_skill_id_to_internal[game_skill_id]
|
||||
hash[new_id] = old_val if old_val
|
||||
end
|
||||
|
||||
return if new_id_to_old.empty?
|
||||
|
||||
# Reverse: copy FK back to old integer columns
|
||||
execute <<-SQL.squish
|
||||
UPDATE collection_weapons
|
||||
SET ax_modifier1 = CASE ax_modifier1_id
|
||||
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier1_id IS NOT NULL
|
||||
SQL
|
||||
|
||||
execute <<-SQL.squish
|
||||
UPDATE collection_weapons
|
||||
SET ax_modifier2 = CASE ax_modifier2_id
|
||||
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier2_id IS NOT NULL
|
||||
SQL
|
||||
|
||||
execute <<-SQL.squish
|
||||
UPDATE grid_weapons
|
||||
SET ax_modifier1 = CASE ax_modifier1_id
|
||||
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier1_id IS NOT NULL
|
||||
SQL
|
||||
|
||||
execute <<-SQL.squish
|
||||
UPDATE grid_weapons
|
||||
SET ax_modifier2 = CASE ax_modifier2_id
|
||||
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||
END
|
||||
WHERE ax_modifier2_id IS NOT NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
38
db/data/20251230000003_add_befoulment_game_skill_ids.rb
Normal file
38
db/data/20251230000003_add_befoulment_game_skill_ids.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddBefoulmentGameSkillIds < ActiveRecord::Migration[8.0]
|
||||
# Befoulment game_skill_id mapping from game data:
|
||||
# 2873: ex_skill_atk_down | ATK Lowered
|
||||
# 2874: ex_skill_ab_atk_down | Skill DMG Lowered
|
||||
# 2875: ex_skill_sp_atk_down | C.A. DMG Lowered
|
||||
# 2876: (doesn't exist)
|
||||
# 2877: ex_skill_ta_down | Multiattack Rate Lowered
|
||||
# 2878: ex_skill_ailment_enhance_down | Debuff Success Rate Lowered
|
||||
# 2879: ex_skill_hp_down | HP Cut
|
||||
# 2880: ex_skill_def_down | Def Lowered (already mapped)
|
||||
# 2881: ex_skill_turn_damage | Turn DMG
|
||||
|
||||
BEFOULMENT_GAME_SKILL_IDS = {
|
||||
'befoul_atk_down' => 2873,
|
||||
'befoul_ability_dmg_down' => 2874,
|
||||
'befoul_ca_dmg_down' => 2875,
|
||||
'befoul_da_ta_down' => 2877,
|
||||
'befoul_debuff_down' => 2878,
|
||||
'befoul_hp_down' => 2879,
|
||||
'befoul_def_down' => 2880, # Already set, but include for completeness
|
||||
'befoul_dot' => 2881
|
||||
}.freeze
|
||||
|
||||
def up
|
||||
BEFOULMENT_GAME_SKILL_IDS.each do |slug, game_skill_id|
|
||||
WeaponStatModifier.where(slug: slug).update_all(game_skill_id: game_skill_id)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# Clear game_skill_ids for befoulments (except def_down which was already set)
|
||||
BEFOULMENT_GAME_SKILL_IDS.except('befoul_def_down').each_key do |slug|
|
||||
WeaponStatModifier.where(slug: slug).update_all(game_skill_id: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1 +1 @@
|
|||
DataMigrate::Data.define(version: 20251214193836)
|
||||
DataMigrate::Data.define(version: 20251230000002)
|
||||
|
|
|
|||
24
db/migrate/20251230000001_create_weapon_stat_modifiers.rb
Normal file
24
db/migrate/20251230000001_create_weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateWeaponStatModifiers < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :weapon_stat_modifiers do |t|
|
||||
t.string :slug, null: false
|
||||
t.string :name_en, null: false
|
||||
t.string :name_jp
|
||||
t.string :category, null: false
|
||||
t.string :stat
|
||||
t.integer :polarity, default: 1, null: false
|
||||
t.string :suffix
|
||||
t.float :base_min
|
||||
t.float :base_max
|
||||
t.integer :game_skill_id
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :weapon_stat_modifiers, :slug, unique: true
|
||||
add_index :weapon_stat_modifiers, :game_skill_id, unique: true
|
||||
add_index :weapon_stat_modifiers, :category
|
||||
end
|
||||
end
|
||||
36
db/migrate/20251230000002_add_weapon_stat_modifier_fks.rb
Normal file
36
db/migrate/20251230000002_add_weapon_stat_modifier_fks.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddWeaponStatModifierFks < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
# collection_weapons - add FK columns
|
||||
# Note: old ax_modifier1/ax_modifier2 integer columns still exist for data migration
|
||||
add_column :collection_weapons, :ax_modifier1_id, :bigint
|
||||
add_column :collection_weapons, :ax_modifier2_id, :bigint
|
||||
add_column :collection_weapons, :befoulment_modifier_id, :bigint
|
||||
add_column :collection_weapons, :befoulment_strength, :float
|
||||
add_column :collection_weapons, :exorcism_level, :integer, default: 0
|
||||
|
||||
add_index :collection_weapons, :ax_modifier1_id
|
||||
add_index :collection_weapons, :ax_modifier2_id
|
||||
add_index :collection_weapons, :befoulment_modifier_id
|
||||
|
||||
add_foreign_key :collection_weapons, :weapon_stat_modifiers, column: :ax_modifier1_id
|
||||
add_foreign_key :collection_weapons, :weapon_stat_modifiers, column: :ax_modifier2_id
|
||||
add_foreign_key :collection_weapons, :weapon_stat_modifiers, column: :befoulment_modifier_id
|
||||
|
||||
# grid_weapons - same pattern
|
||||
add_column :grid_weapons, :ax_modifier1_id, :bigint
|
||||
add_column :grid_weapons, :ax_modifier2_id, :bigint
|
||||
add_column :grid_weapons, :befoulment_modifier_id, :bigint
|
||||
add_column :grid_weapons, :befoulment_strength, :float
|
||||
add_column :grid_weapons, :exorcism_level, :integer, default: 0
|
||||
|
||||
add_index :grid_weapons, :ax_modifier1_id
|
||||
add_index :grid_weapons, :ax_modifier2_id
|
||||
add_index :grid_weapons, :befoulment_modifier_id
|
||||
|
||||
add_foreign_key :grid_weapons, :weapon_stat_modifiers, column: :ax_modifier1_id
|
||||
add_foreign_key :grid_weapons, :weapon_stat_modifiers, column: :ax_modifier2_id
|
||||
add_foreign_key :grid_weapons, :weapon_stat_modifiers, column: :befoulment_modifier_id
|
||||
end
|
||||
end
|
||||
11
db/migrate/20251230000003_finalize_ax_modifier_columns.rb
Normal file
11
db/migrate/20251230000003_finalize_ax_modifier_columns.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeAxModifierColumns < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
# Remove old integer columns (data has been migrated to ax_modifier1_id/ax_modifier2_id FKs)
|
||||
remove_column :collection_weapons, :ax_modifier1, :integer
|
||||
remove_column :collection_weapons, :ax_modifier2, :integer
|
||||
remove_column :grid_weapons, :ax_modifier1, :integer
|
||||
remove_column :grid_weapons, :ax_modifier2, :integer
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddAugmentTypeToWeaponSeries < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
add_column :weapon_series, :augment_type, :integer, default: 0, null: false
|
||||
|
||||
# Migrate existing has_ax_skills: true to augment_type: 1 (ax)
|
||||
execute <<-SQL.squish
|
||||
UPDATE weapon_series
|
||||
SET augment_type = 1
|
||||
WHERE has_ax_skills = true
|
||||
SQL
|
||||
|
||||
remove_column :weapon_series, :has_ax_skills
|
||||
end
|
||||
|
||||
def down
|
||||
add_column :weapon_series, :has_ax_skills, :boolean, default: false, null: false
|
||||
|
||||
# Migrate augment_type: 1 (ax) back to has_ax_skills: true
|
||||
execute <<-SQL.squish
|
||||
UPDATE weapon_series
|
||||
SET has_ax_skills = true
|
||||
WHERE augment_type = 1
|
||||
SQL
|
||||
|
||||
remove_column :weapon_series, :augment_type
|
||||
end
|
||||
end
|
||||
54
db/schema.rb
54
db/schema.rb
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_12_30_000004) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "btree_gin"
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
|
@ -235,15 +235,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
t.uuid "weapon_key4_id"
|
||||
t.uuid "awakening_id"
|
||||
t.integer "awakening_level", default: 1, null: false
|
||||
t.integer "ax_modifier1"
|
||||
t.float "ax_strength1"
|
||||
t.integer "ax_modifier2"
|
||||
t.float "ax_strength2"
|
||||
t.integer "element"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "game_id"
|
||||
t.bigint "ax_modifier1_id"
|
||||
t.bigint "ax_modifier2_id"
|
||||
t.bigint "befoulment_modifier_id"
|
||||
t.float "befoulment_strength"
|
||||
t.integer "exorcism_level", default: 0
|
||||
t.index ["awakening_id"], name: "index_collection_weapons_on_awakening_id"
|
||||
t.index ["ax_modifier1_id"], name: "index_collection_weapons_on_ax_modifier1_id"
|
||||
t.index ["ax_modifier2_id"], name: "index_collection_weapons_on_ax_modifier2_id"
|
||||
t.index ["befoulment_modifier_id"], name: "index_collection_weapons_on_befoulment_modifier_id"
|
||||
t.index ["user_id", "game_id"], name: "index_collection_weapons_on_user_id_and_game_id", unique: true, where: "(game_id IS NOT NULL)"
|
||||
t.index ["user_id", "weapon_id"], name: "index_collection_weapons_on_user_id_and_weapon_id"
|
||||
t.index ["user_id"], name: "index_collection_weapons_on_user_id"
|
||||
|
|
@ -381,9 +387,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.integer "reroll_slot"
|
||||
t.uuid "collection_artifact_id"
|
||||
t.boolean "orphaned", default: false, null: false
|
||||
t.index ["artifact_id"], name: "index_grid_artifacts_on_artifact_id"
|
||||
t.index ["collection_artifact_id"], name: "index_grid_artifacts_on_collection_artifact_id"
|
||||
t.index ["grid_character_id"], name: "index_grid_artifacts_on_grid_character_id", unique: true
|
||||
t.index ["orphaned"], name: "index_grid_artifacts_on_orphaned"
|
||||
end
|
||||
|
||||
create_table "grid_characters", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
|
@ -422,7 +430,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
t.integer "transcendence_step", default: 0, null: false
|
||||
t.boolean "quick_summon", default: false
|
||||
t.uuid "collection_summon_id"
|
||||
t.boolean "orphaned", default: false, null: false
|
||||
t.index ["collection_summon_id"], name: "index_grid_summons_on_collection_summon_id"
|
||||
t.index ["orphaned"], name: "index_grid_summons_on_orphaned"
|
||||
t.index ["party_id", "position"], name: "index_grid_summons_on_party_id_and_position"
|
||||
t.index ["party_id"], name: "index_grid_summons_on_party_id"
|
||||
t.index ["summon_id"], name: "index_grid_summons_on_summon_id"
|
||||
|
|
@ -439,9 +449,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.uuid "weapon_key3_id"
|
||||
t.integer "ax_modifier1"
|
||||
t.float "ax_strength1"
|
||||
t.integer "ax_modifier2"
|
||||
t.float "ax_strength2"
|
||||
t.integer "element"
|
||||
t.integer "awakening_level", default: 1, null: false
|
||||
|
|
@ -449,8 +457,18 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
t.integer "transcendence_step", default: 0
|
||||
t.string "weapon_key4_id"
|
||||
t.uuid "collection_weapon_id"
|
||||
t.boolean "orphaned", default: false, null: false
|
||||
t.bigint "ax_modifier1_id"
|
||||
t.bigint "ax_modifier2_id"
|
||||
t.bigint "befoulment_modifier_id"
|
||||
t.float "befoulment_strength"
|
||||
t.integer "exorcism_level", default: 0
|
||||
t.index ["awakening_id"], name: "index_grid_weapons_on_awakening_id"
|
||||
t.index ["ax_modifier1_id"], name: "index_grid_weapons_on_ax_modifier1_id"
|
||||
t.index ["ax_modifier2_id"], name: "index_grid_weapons_on_ax_modifier2_id"
|
||||
t.index ["befoulment_modifier_id"], name: "index_grid_weapons_on_befoulment_modifier_id"
|
||||
t.index ["collection_weapon_id"], name: "index_grid_weapons_on_collection_weapon_id"
|
||||
t.index ["orphaned"], name: "index_grid_weapons_on_orphaned"
|
||||
t.index ["party_id", "position"], name: "index_grid_weapons_on_party_id_and_position"
|
||||
t.index ["party_id"], name: "index_grid_weapons_on_party_id"
|
||||
t.index ["weapon_id"], name: "index_grid_weapons_on_weapon_id"
|
||||
|
|
@ -924,7 +942,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
t.boolean "element_changeable", default: false, null: false
|
||||
t.boolean "has_weapon_keys", default: false, null: false
|
||||
t.boolean "has_awakening", default: false, null: false
|
||||
t.boolean "has_ax_skills", default: false, null: false
|
||||
t.integer "augment_type", default: 0, null: false
|
||||
t.index ["order"], name: "index_weapon_series_on_order"
|
||||
t.index ["slug"], name: "index_weapon_series_on_slug", unique: true
|
||||
end
|
||||
|
|
@ -945,6 +963,24 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
t.index ["weapon_granblue_id"], name: "index_weapon_skills_on_weapon_granblue_id"
|
||||
end
|
||||
|
||||
create_table "weapon_stat_modifiers", force: :cascade do |t|
|
||||
t.string "slug", null: false
|
||||
t.string "name_en", null: false
|
||||
t.string "name_jp"
|
||||
t.string "category", null: false
|
||||
t.string "stat"
|
||||
t.integer "polarity", default: 1, null: false
|
||||
t.string "suffix"
|
||||
t.float "base_min"
|
||||
t.float "base_max"
|
||||
t.integer "game_skill_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["category"], name: "index_weapon_stat_modifiers_on_category"
|
||||
t.index ["game_skill_id"], name: "index_weapon_stat_modifiers_on_game_skill_id", unique: true
|
||||
t.index ["slug"], name: "index_weapon_stat_modifiers_on_slug", unique: true
|
||||
end
|
||||
|
||||
create_table "weapons", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "name_en"
|
||||
t.string "name_jp"
|
||||
|
|
@ -1024,6 +1060,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
add_foreign_key "collection_weapons", "weapon_keys", column: "weapon_key2_id"
|
||||
add_foreign_key "collection_weapons", "weapon_keys", column: "weapon_key3_id"
|
||||
add_foreign_key "collection_weapons", "weapon_keys", column: "weapon_key4_id"
|
||||
add_foreign_key "collection_weapons", "weapon_stat_modifiers", column: "ax_modifier1_id"
|
||||
add_foreign_key "collection_weapons", "weapon_stat_modifiers", column: "ax_modifier2_id"
|
||||
add_foreign_key "collection_weapons", "weapon_stat_modifiers", column: "befoulment_modifier_id"
|
||||
add_foreign_key "collection_weapons", "weapons"
|
||||
add_foreign_key "crew_gw_participations", "crews"
|
||||
add_foreign_key "crew_gw_participations", "gw_events"
|
||||
|
|
@ -1049,6 +1088,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
|||
add_foreign_key "grid_weapons", "collection_weapons"
|
||||
add_foreign_key "grid_weapons", "parties"
|
||||
add_foreign_key "grid_weapons", "weapon_keys", column: "weapon_key3_id"
|
||||
add_foreign_key "grid_weapons", "weapon_stat_modifiers", column: "ax_modifier1_id"
|
||||
add_foreign_key "grid_weapons", "weapon_stat_modifiers", column: "ax_modifier2_id"
|
||||
add_foreign_key "grid_weapons", "weapon_stat_modifiers", column: "befoulment_modifier_id"
|
||||
add_foreign_key "grid_weapons", "weapons"
|
||||
add_foreign_key "gw_crew_scores", "crew_gw_participations"
|
||||
add_foreign_key "gw_individual_scores", "crew_gw_participations"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
id,slug,name_en,name_jp,order,extra,element_changeable,has_weapon_keys,has_awakening,has_ax_skills
|
||||
00000000-0001-0000-0000-000000000001,gacha,Gacha Weapons,ガチャ武器,0,false,false,false,false,false
|
||||
00000000-0001-0000-0000-000000000002,grand,Grand Weapons,リミテッドシリーズ,2,false,false,true,false,false
|
||||
00000000-0001-0000-0000-000000000003,dark-opus,Dark Opus Weapons,終末の神器,3,false,false,true,true,false
|
||||
00000000-0001-0000-0000-000000000004,revenant,Revenant Weapons,天星器,21,false,true,false,false,false
|
||||
00000000-0001-0000-0000-000000000005,xeno,Xeno Weapons,六道武器,23,true,false,false,false,false
|
||||
00000000-0001-0000-0000-000000000006,ultima,Ultima Weapons,オメガウェポン,9,false,true,true,false,false
|
||||
00000000-0001-0000-0000-000000000007,superlative,Superlative Weapons,スペリオシリーズ,27,true,true,true,false,false
|
||||
00000000-0001-0000-0000-000000000008,draconic,Draconic Weapons,ドラコニックウェポン・オリジン,5,false,false,true,true,false
|
||||
00000000-0001-0000-0000-000000000009,draconic-providence,Draconic Weapons Providence,ドラコニックウェポン,6,false,false,true,true,false
|
||||
00000000-0001-0000-0000-000000000010,primal,Primal Weapons,プライマルシリーズ,32,false,false,false,false,false
|
||||
00000000-0001-0000-0000-000000000011,event,Event Weapons,イベント武器,99,false,false,false,false,false
|
||||
id,slug,name_en,name_jp,order,extra,element_changeable,has_weapon_keys,has_awakening,augment_type
|
||||
00000000-0001-0000-0000-000000000001,gacha,Gacha Weapons,ガチャ武器,0,false,false,false,false,none
|
||||
00000000-0001-0000-0000-000000000002,grand,Grand Weapons,リミテッドシリーズ,2,false,false,true,false,none
|
||||
00000000-0001-0000-0000-000000000003,dark-opus,Dark Opus Weapons,終末の神器,3,false,false,true,true,none
|
||||
00000000-0001-0000-0000-000000000004,revenant,Revenant Weapons,天星器,21,false,true,false,false,none
|
||||
00000000-0001-0000-0000-000000000005,xeno,Xeno Weapons,六道武器,23,true,false,false,false,none
|
||||
00000000-0001-0000-0000-000000000006,ultima,Ultima Weapons,オメガウェポン,9,false,true,true,false,none
|
||||
00000000-0001-0000-0000-000000000007,superlative,Superlative Weapons,スペリオシリーズ,27,true,true,true,false,none
|
||||
00000000-0001-0000-0000-000000000008,draconic,Draconic Weapons,ドラコニックウェポン・オリジン,5,false,false,true,true,none
|
||||
00000000-0001-0000-0000-000000000009,draconic-providence,Draconic Weapons Providence,ドラコニックウェポン,6,false,false,true,true,none
|
||||
00000000-0001-0000-0000-000000000010,primal,Primal Weapons,プライマルシリーズ,32,false,false,false,false,none
|
||||
00000000-0001-0000-0000-000000000011,event,Event Weapons,イベント武器,99,false,false,false,false,none
|
||||
|
|
|
|||
|
56
db/seeds/weapon_stat_modifiers.rb
Normal file
56
db/seeds/weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
##
|
||||
# Seeds for WeaponStatModifier - AX skills and befoulments
|
||||
#
|
||||
|
||||
puts 'Seeding weapon stat modifiers...'
|
||||
|
||||
ax_skills = [
|
||||
# Primary AX Skills
|
||||
{ slug: 'ax_hp', name_en: 'HP', name_jp: 'HP', category: 'ax', stat: 'hp', polarity: 1, suffix: '%', base_min: 1, base_max: 11, game_skill_id: 1588 },
|
||||
{ slug: 'ax_atk', name_en: 'ATK', name_jp: '攻撃', category: 'ax', stat: 'atk', polarity: 1, suffix: '%', base_min: 1, base_max: 3.5, game_skill_id: 1589 },
|
||||
{ slug: 'ax_def', name_en: 'DEF', name_jp: '防御', category: 'ax', stat: 'def', polarity: 1, suffix: '%', base_min: 1, base_max: 8, game_skill_id: 1590 },
|
||||
{ slug: 'ax_ca_dmg', name_en: 'C.A. DMG', name_jp: '奥義ダメ', category: 'ax', stat: 'ca_dmg', polarity: 1, suffix: '%', base_min: 2, base_max: 8.5, game_skill_id: 1591 },
|
||||
{ slug: 'ax_multiattack', name_en: 'Multiattack Rate', name_jp: '連撃率', category: 'ax', stat: 'multiattack', polarity: 1, suffix: '%', base_min: 1, base_max: 4, game_skill_id: 1592 },
|
||||
|
||||
# Secondary AX Skills
|
||||
{ slug: 'ax_debuff_res', name_en: 'Debuff Resistance', name_jp: '弱体耐性', category: 'ax', stat: 'debuff_res', polarity: 1, suffix: '%', base_min: 1, base_max: 3, game_skill_id: 1593 },
|
||||
{ slug: 'ax_ele_atk', name_en: 'Elemental ATK', name_jp: '全属性攻撃力', category: 'ax', stat: 'ele_atk', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1594 },
|
||||
{ slug: 'ax_healing', name_en: 'Healing', name_jp: '回復性能', category: 'ax', stat: 'healing', polarity: 1, suffix: '%', base_min: 2, base_max: 5, game_skill_id: 1595 },
|
||||
{ slug: 'ax_da', name_en: 'Double Attack Rate', name_jp: 'DA確率', category: 'ax', stat: 'da', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1596 },
|
||||
{ slug: 'ax_ta', name_en: 'Triple Attack Rate', name_jp: 'TA確率', category: 'ax', stat: 'ta', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1597 },
|
||||
{ slug: 'ax_ca_cap', name_en: 'C.A. DMG Cap', name_jp: '奥義上限', category: 'ax', stat: 'ca_cap', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1599 },
|
||||
{ slug: 'ax_stamina', name_en: 'Stamina', name_jp: '渾身', category: 'ax', stat: 'stamina', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1600 },
|
||||
{ slug: 'ax_enmity', name_en: 'Enmity', name_jp: '背水', category: 'ax', stat: 'enmity', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1601 },
|
||||
|
||||
# Extended AX Skills (axType 2)
|
||||
{ slug: 'ax_skill_supp', name_en: 'Supplemental Skill DMG', name_jp: 'アビ与ダメ上昇', category: 'ax', stat: 'skill_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1719 },
|
||||
{ slug: 'ax_ca_supp', name_en: 'Supplemental C.A. DMG', name_jp: '奥義与ダメ上昇', category: 'ax', stat: 'ca_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1720 },
|
||||
{ slug: 'ax_ele_dmg_red', name_en: 'Elemental DMG Reduction', name_jp: '属性ダメ軽減', category: 'ax', stat: 'ele_dmg_red', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1721 },
|
||||
{ slug: 'ax_na_cap', name_en: 'Normal ATK DMG Cap', name_jp: '通常ダメ上限', category: 'ax', stat: 'na_cap', polarity: 1, suffix: '%', base_min: 0.5, base_max: 1.5, game_skill_id: 1722 },
|
||||
|
||||
# Utility AX Skills (axType 3)
|
||||
{ slug: 'ax_exp', name_en: 'EXP Gain', name_jp: 'EXP UP', category: 'ax', stat: 'exp', polarity: 1, suffix: '%', base_min: 5, base_max: 10, game_skill_id: 1837 },
|
||||
{ slug: 'ax_rupie', name_en: 'Rupie Gain', name_jp: '獲得ルピ', category: 'ax', stat: 'rupie', polarity: 1, suffix: '%', base_min: 10, base_max: 20, game_skill_id: 1838 }
|
||||
]
|
||||
|
||||
befoulments = [
|
||||
# Befoulments - game_skill_ids from game data (2873-2881, 2876 doesn't exist)
|
||||
{ slug: 'befoul_atk_down', name_en: 'ATK Down', name_jp: '攻撃力DOWN', category: 'befoulment', stat: 'atk', polarity: -1, suffix: '%', base_min: -12, base_max: -6, game_skill_id: 2873 },
|
||||
{ slug: 'befoul_ability_dmg_down', name_en: 'Ability DMG Down', name_jp: 'アビリティダメージDOWN', category: 'befoulment', stat: 'ability_dmg', polarity: -1, suffix: '%', base_min: -50, base_max: -50, game_skill_id: 2874 },
|
||||
{ slug: 'befoul_ca_dmg_down', name_en: 'CA DMG Down', name_jp: '奥義ダメージDOWN', category: 'befoulment', stat: 'ca_dmg', polarity: -1, suffix: '%', base_min: -38, base_max: -26, game_skill_id: 2875 },
|
||||
{ slug: 'befoul_da_ta_down', name_en: 'DA/TA Down', name_jp: '連撃率DOWN', category: 'befoulment', stat: 'da_ta', polarity: -1, suffix: '%', base_min: -22, base_max: -19, game_skill_id: 2877 },
|
||||
{ slug: 'befoul_debuff_down', name_en: 'Debuff Success Down', name_jp: '弱体成功率DOWN', category: 'befoulment', stat: 'debuff_success', polarity: -1, suffix: '%', base_min: -16, base_max: -6, game_skill_id: 2878 },
|
||||
{ slug: 'befoul_hp_down', name_en: 'Max HP Down', name_jp: '最大HP減少', category: 'befoulment', stat: 'hp', polarity: -1, suffix: '%', base_min: -50, base_max: -26, game_skill_id: 2879 },
|
||||
{ slug: 'befoul_def_down', name_en: 'DEF Down', name_jp: '防御力DOWN', category: 'befoulment', stat: 'def', polarity: -1, suffix: '%', base_min: -25, base_max: -21, game_skill_id: 2880 },
|
||||
{ slug: 'befoul_dot', name_en: 'Damage Over Time', name_jp: '毎ターンダメージ', category: 'befoulment', stat: 'dot', polarity: -1, suffix: '%', base_min: 6, base_max: 16, game_skill_id: 2881 }
|
||||
]
|
||||
|
||||
(ax_skills + befoulments).each do |attrs|
|
||||
modifier = WeaponStatModifier.find_or_initialize_by(slug: attrs[:slug])
|
||||
modifier.assign_attributes(attrs)
|
||||
modifier.save!
|
||||
end
|
||||
|
||||
puts "Created/updated #{WeaponStatModifier.count} weapon stat modifiers"
|
||||
671
docs/planning/befoulments-feature-plan.md
Normal file
671
docs/planning/befoulments-feature-plan.md
Normal file
|
|
@ -0,0 +1,671 @@
|
|||
# Befoulments & WeaponStatModifier Consolidation
|
||||
|
||||
## Overview
|
||||
|
||||
Implement Befoulments (魔蝕) and consolidate with AX skills using a new `WeaponStatModifier` table.
|
||||
|
||||
### Terminology
|
||||
- **Odiant (禁禍武器)** = A weapon series that has befoulments instead of AX skills
|
||||
- **Befoulments (魔蝕)** = Negative stat augments that appear on Odiant weapons
|
||||
- **Exorcism Level (退魔Lv)** = Level 0-5 that reduces befoulment strength
|
||||
|
||||
**Key insight:** `augment_skill_info` contains:
|
||||
- **AX skills** for regular weapons (`is_odiant_weapon: false`)
|
||||
- **Befoulments** for Odiant weapons (`is_odiant_weapon: true`)
|
||||
|
||||
They are mutually exclusive - a weapon has either AX skills OR befoulments, never both.
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### 1. New Table: `weapon_stat_modifiers`
|
||||
|
||||
Centralized definition of all weapon stat modifiers (AX skills + befoulments):
|
||||
|
||||
```ruby
|
||||
create_table :weapon_stat_modifiers do |t|
|
||||
t.string :slug, null: false, index: { unique: true } # 'ax_atk', 'befoul_def_down'
|
||||
t.string :name_en, null: false
|
||||
t.string :name_jp
|
||||
t.string :category, null: false # 'ax' or 'befoulment'
|
||||
t.string :stat # 'atk', 'def', 'da_ta', 'ca_dmg', 'hp', 'dot', etc.
|
||||
t.integer :polarity, default: 1 # 1 = buff, -1 = debuff
|
||||
t.string :suffix # '%' for percentages, nil for flat values
|
||||
t.float :base_min # Known min initial value (informational)
|
||||
t.float :base_max # Known max initial value (informational)
|
||||
t.integer :game_skill_id # Maps to game's skill_id for import
|
||||
t.timestamps
|
||||
end
|
||||
```
|
||||
|
||||
**Icon convention:** Frontend derives icon path from `slug`:
|
||||
- `ax_atk` → `weapon-stat-modifier/ax_atk.png`
|
||||
- `befoul_def_down` → `weapon-stat-modifier/befoul_def_down.png`
|
||||
|
||||
Rename game icons (e.g., `ex_skill_def_down` → `befoul_def_down`) to match slugs.
|
||||
|
||||
### 2. Refactor AX & Add Befoulment Columns (Foreign Keys)
|
||||
|
||||
Replace raw integer `ax_modifier1/2` with foreign keys, and add befoulment FK:
|
||||
|
||||
```ruby
|
||||
# collection_weapons
|
||||
# Remove old integer columns
|
||||
remove_column :collection_weapons, :ax_modifier1, :integer
|
||||
remove_column :collection_weapons, :ax_modifier2, :integer
|
||||
|
||||
# Add FK columns referencing weapon_stat_modifiers
|
||||
add_reference :collection_weapons, :ax_modifier1, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||
add_reference :collection_weapons, :ax_modifier2, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||
add_reference :collection_weapons, :befoulment_modifier, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||
add_column :collection_weapons, :befoulment_strength, :float
|
||||
add_column :collection_weapons, :exorcism_level, :integer, default: 0
|
||||
|
||||
# grid_weapons - same pattern
|
||||
add_reference :grid_weapons, :ax_modifier1, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||
add_reference :grid_weapons, :ax_modifier2, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||
add_reference :grid_weapons, :befoulment_modifier, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||
add_column :grid_weapons, :befoulment_strength, :float
|
||||
add_column :grid_weapons, :exorcism_level, :integer, default: 0
|
||||
```
|
||||
|
||||
### 2b. Data Migration for Existing AX Skills
|
||||
|
||||
Migrate existing `ax_modifier1/2` integer values to FK references:
|
||||
|
||||
```ruby
|
||||
# Run after weapon_stat_modifiers table is seeded
|
||||
CollectionWeapon.where.not(ax_modifier1: nil).find_each do |cw|
|
||||
modifier = WeaponStatModifier.find_by(game_skill_id: cw.ax_modifier1)
|
||||
cw.update_columns(ax_modifier1_id: modifier&.id) if modifier
|
||||
end
|
||||
|
||||
CollectionWeapon.where.not(ax_modifier2: nil).find_each do |cw|
|
||||
modifier = WeaponStatModifier.find_by(game_skill_id: cw.ax_modifier2)
|
||||
cw.update_columns(ax_modifier2_id: modifier&.id) if modifier
|
||||
end
|
||||
|
||||
# Same for GridWeapon
|
||||
```
|
||||
|
||||
**Note:** Strength values (`ax_strength1/2`) are preserved as-is since they store the actual value.
|
||||
|
||||
### 3. WeaponSeries `augment_type` Enum
|
||||
|
||||
Replace `has_ax_skills` boolean with an enum that enforces mutual exclusivity:
|
||||
|
||||
```ruby
|
||||
# Migration
|
||||
remove_column :weapon_series, :has_ax_skills, :boolean
|
||||
add_column :weapon_series, :augment_type, :integer, default: 0
|
||||
|
||||
# Data migration
|
||||
WeaponSeries.where(has_ax_skills: true).update_all(augment_type: 1)
|
||||
```
|
||||
|
||||
```ruby
|
||||
# Model
|
||||
enum :augment_type, { none: 0, ax: 1, befoulment: 2 }, default: :none
|
||||
|
||||
scope :with_ax_skills, -> { where(augment_type: :ax) }
|
||||
scope :with_befoulments, -> { where(augment_type: :befoulment) }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Seed Data
|
||||
|
||||
### AX Skill Modifiers (Complete List)
|
||||
|
||||
All AX skills from game data with their game_skill_id values:
|
||||
|
||||
```ruby
|
||||
WeaponStatModifier.create!([
|
||||
# Primary AX Skills
|
||||
{ slug: 'ax_atk', name_en: 'ATK', name_jp: '攻撃', category: 'ax', stat: 'atk', polarity: 1, suffix: '%', base_min: 1, base_max: 3.5, game_skill_id: 1589 },
|
||||
{ slug: 'ax_def', name_en: 'DEF', name_jp: '防御', category: 'ax', stat: 'def', polarity: 1, suffix: '%', base_min: 1, base_max: 8, game_skill_id: 1590 },
|
||||
{ slug: 'ax_hp', name_en: 'HP', name_jp: 'HP', category: 'ax', stat: 'hp', polarity: 1, suffix: '%', base_min: 1, base_max: 11, game_skill_id: 1588 },
|
||||
{ slug: 'ax_ca_dmg', name_en: 'C.A. DMG', name_jp: '奥義ダメ', category: 'ax', stat: 'ca_dmg', polarity: 1, suffix: '%', base_min: 2, base_max: 8.5, game_skill_id: 1591 },
|
||||
{ slug: 'ax_multiattack', name_en: 'Multiattack Rate', name_jp: '連撃率', category: 'ax', stat: 'multiattack', polarity: 1, suffix: '%', base_min: 1, base_max: 4, game_skill_id: 1592 },
|
||||
|
||||
# Secondary AX Skills
|
||||
{ slug: 'ax_debuff_res', name_en: 'Debuff Resistance', name_jp: '弱体耐性', category: 'ax', stat: 'debuff_res', polarity: 1, suffix: '%', base_min: 1, base_max: 3, game_skill_id: 1593 },
|
||||
{ slug: 'ax_ele_atk', name_en: 'Elemental ATK', name_jp: '全属性攻撃力', category: 'ax', stat: 'ele_atk', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1594 },
|
||||
{ slug: 'ax_healing', name_en: 'Healing', name_jp: '回復性能', category: 'ax', stat: 'healing', polarity: 1, suffix: '%', base_min: 2, base_max: 5, game_skill_id: 1595 },
|
||||
{ slug: 'ax_da', name_en: 'Double Attack Rate', name_jp: 'DA確率', category: 'ax', stat: 'da', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1596 },
|
||||
{ slug: 'ax_ta', name_en: 'Triple Attack Rate', name_jp: 'TA確率', category: 'ax', stat: 'ta', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1597 },
|
||||
{ slug: 'ax_ca_cap', name_en: 'C.A. DMG Cap', name_jp: '奥義上限', category: 'ax', stat: 'ca_cap', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1599 },
|
||||
{ slug: 'ax_stamina', name_en: 'Stamina', name_jp: '渾身', category: 'ax', stat: 'stamina', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1600 },
|
||||
{ slug: 'ax_enmity', name_en: 'Enmity', name_jp: '背水', category: 'ax', stat: 'enmity', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1601 },
|
||||
|
||||
# Extended AX Skills (axType 2)
|
||||
{ slug: 'ax_skill_supp', name_en: 'Supplemental Skill DMG', name_jp: 'アビ与ダメ上昇', category: 'ax', stat: 'skill_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1719 },
|
||||
{ slug: 'ax_ca_supp', name_en: 'Supplemental C.A. DMG', name_jp: '奥義与ダメ上昇', category: 'ax', stat: 'ca_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1720 },
|
||||
{ slug: 'ax_ele_dmg_red', name_en: 'Elemental DMG Reduction', name_jp: '属性ダメ軽減', category: 'ax', stat: 'ele_dmg_red', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1721 },
|
||||
{ slug: 'ax_na_cap', name_en: 'Normal ATK DMG Cap', name_jp: '通常ダメ上限', category: 'ax', stat: 'na_cap', polarity: 1, suffix: '%', base_min: 0.5, base_max: 1.5, game_skill_id: 1722 },
|
||||
|
||||
# Utility AX Skills (axType 3)
|
||||
{ slug: 'ax_exp', name_en: 'EXP Gain', name_jp: 'EXP UP', category: 'ax', stat: 'exp', polarity: 1, suffix: '%', base_min: 5, base_max: 10, game_skill_id: 1837 },
|
||||
{ slug: 'ax_rupie', name_en: 'Rupie Gain', name_jp: '獲得ルピ', category: 'ax', stat: 'rupie', polarity: 1, suffix: '%', base_min: 10, base_max: 20, game_skill_id: 1838 },
|
||||
])
|
||||
```
|
||||
|
||||
### Befoulment Modifiers
|
||||
|
||||
```ruby
|
||||
# game_skill_id values will be populated as we discover them - we know 2880 = DEF Down
|
||||
WeaponStatModifier.create!([
|
||||
{ slug: 'befoul_atk_down', name_en: 'ATK Down', name_jp: '攻撃力DOWN', category: 'befoulment', stat: 'atk', polarity: -1, suffix: '%', base_min: -12, base_max: -6 },
|
||||
{ slug: 'befoul_def_down', name_en: 'DEF Down', name_jp: '防御力DOWN', category: 'befoulment', stat: 'def', polarity: -1, suffix: '%', base_min: -25, base_max: -21, game_skill_id: 2880 },
|
||||
{ slug: 'befoul_da_ta_down', name_en: 'DA/TA Down', name_jp: '連撃率DOWN', category: 'befoulment', stat: 'da_ta', polarity: -1, suffix: '%', base_min: -22, base_max: -19 },
|
||||
{ slug: 'befoul_ca_dmg_down', name_en: 'CA DMG Down', name_jp: '奥義ダメージDOWN', category: 'befoulment', stat: 'ca_dmg', polarity: -1, suffix: '%', base_min: -38, base_max: -26 },
|
||||
{ slug: 'befoul_dot', name_en: 'Damage Over Time', name_jp: '毎ターンダメージ', category: 'befoulment', stat: 'dot', polarity: -1, suffix: '%', base_min: 6, base_max: 16 },
|
||||
{ slug: 'befoul_hp_down', name_en: 'Max HP Down', name_jp: '最大HP減少', category: 'befoulment', stat: 'hp', polarity: -1, suffix: '%', base_min: -50, base_max: -26 },
|
||||
{ slug: 'befoul_debuff_down', name_en: 'Debuff Success Down', name_jp: '弱体成功率DOWN', category: 'befoulment', stat: 'debuff_success', polarity: -1, suffix: '%', base_min: -16, base_max: -6 },
|
||||
{ slug: 'befoul_ability_dmg_down', name_en: 'Ability DMG Down', name_jp: 'アビリティダメージDOWN', category: 'befoulment', stat: 'ability_dmg', polarity: -1, suffix: '%', base_min: -50, base_max: -50 },
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Changes
|
||||
|
||||
### WeaponStatModifier Model
|
||||
|
||||
```ruby
|
||||
# app/models/weapon_stat_modifier.rb
|
||||
class WeaponStatModifier < ApplicationRecord
|
||||
validates :slug, presence: true, uniqueness: true
|
||||
validates :name_en, presence: true
|
||||
validates :category, presence: true, inclusion: { in: %w[ax befoulment] }
|
||||
validates :polarity, inclusion: { in: [-1, 1] }
|
||||
|
||||
scope :ax_skills, -> { where(category: 'ax') }
|
||||
scope :befoulments, -> { where(category: 'befoulment') }
|
||||
|
||||
def self.find_by_game_skill_id(id)
|
||||
find_by(game_skill_id: id.to_i)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### CollectionWeapon Changes
|
||||
|
||||
```ruby
|
||||
# app/models/collection_weapon.rb
|
||||
|
||||
# Replace old integer references with proper associations
|
||||
belongs_to :ax_modifier1, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||
|
||||
validates :exorcism_level, numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 0,
|
||||
less_than_or_equal_to: 5
|
||||
}, allow_nil: true
|
||||
|
||||
validate :validate_ax_skills
|
||||
validate :validate_befoulment_fields
|
||||
|
||||
def validate_ax_skills
|
||||
# AX skill 1: must have both modifier and strength
|
||||
if (ax_modifier1.present? && ax_strength1.blank?) ||
|
||||
(ax_modifier1.blank? && ax_strength1.present?)
|
||||
errors.add(:base, "AX skill 1 must have both modifier and strength")
|
||||
end
|
||||
|
||||
# AX skill 2: must have both modifier and strength
|
||||
if (ax_modifier2.present? && ax_strength2.blank?) ||
|
||||
(ax_modifier2.blank? && ax_strength2.present?)
|
||||
errors.add(:base, "AX skill 2 must have both modifier and strength")
|
||||
end
|
||||
|
||||
# Validate category is 'ax'
|
||||
if ax_modifier1.present? && ax_modifier1.category != 'ax'
|
||||
errors.add(:ax_modifier1, "must be an AX skill modifier")
|
||||
end
|
||||
if ax_modifier2.present? && ax_modifier2.category != 'ax'
|
||||
errors.add(:ax_modifier2, "must be an AX skill modifier")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_befoulment_fields
|
||||
if (befoulment_modifier.present? && befoulment_strength.blank?) ||
|
||||
(befoulment_modifier.blank? && befoulment_strength.present?)
|
||||
errors.add(:base, "Befoulment must have both modifier and strength")
|
||||
end
|
||||
|
||||
# Validate category is 'befoulment'
|
||||
if befoulment_modifier.present? && befoulment_modifier.category != 'befoulment'
|
||||
errors.add(:befoulment_modifier, "must be a befoulment modifier")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### GridWeapon Changes
|
||||
|
||||
```ruby
|
||||
# app/models/grid_weapon.rb
|
||||
|
||||
# Same associations as CollectionWeapon
|
||||
belongs_to :ax_modifier1, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||
|
||||
# Same validations as CollectionWeapon
|
||||
```
|
||||
|
||||
- Update `sync_from_collection!` to include befoulment fields
|
||||
- Update `out_of_sync?` to check befoulment fields
|
||||
- Update Amoeba config to nullify befoulment fields on copy
|
||||
|
||||
---
|
||||
|
||||
## API Changes
|
||||
|
||||
### Blueprint Serialization
|
||||
|
||||
```ruby
|
||||
# collection_weapon_blueprint.rb
|
||||
|
||||
# AX skills - now with full modifier object
|
||||
field :ax, if: ->(_, obj, _) { obj.ax_modifier1.present? } do |obj|
|
||||
skills = []
|
||||
if obj.ax_modifier1.present?
|
||||
skills << {
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.ax_modifier1),
|
||||
strength: obj.ax_strength1
|
||||
}
|
||||
end
|
||||
if obj.ax_modifier2.present?
|
||||
skills << {
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.ax_modifier2),
|
||||
strength: obj.ax_strength2
|
||||
}
|
||||
end
|
||||
skills
|
||||
end
|
||||
|
||||
# Befoulment - with full modifier object
|
||||
field :befoulment, if: ->(_, obj, _) { obj.befoulment_modifier.present? } do |obj|
|
||||
{
|
||||
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.befoulment_modifier),
|
||||
strength: obj.befoulment_strength,
|
||||
exorcism_level: obj.exorcism_level
|
||||
}
|
||||
end
|
||||
|
||||
# grid_weapon_blueprint.rb - similar
|
||||
```
|
||||
|
||||
### WeaponStatModifierBlueprint
|
||||
|
||||
```ruby
|
||||
# app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb
|
||||
class Api::V1::WeaponStatModifierBlueprint < Blueprinter::Base
|
||||
identifier :id
|
||||
fields :slug, :name_en, :name_jp, :category, :stat, :polarity, :suffix
|
||||
end
|
||||
```
|
||||
|
||||
### New Endpoint: GET /weapon_stat_modifiers
|
||||
|
||||
Return all weapon stat modifiers for frontend reference:
|
||||
|
||||
```ruby
|
||||
# app/controllers/api/v1/weapon_stat_modifiers_controller.rb
|
||||
def index
|
||||
@modifiers = WeaponStatModifier.all
|
||||
render json: WeaponStatModifierBlueprint.render(@modifiers, root: :weapon_stat_modifiers)
|
||||
end
|
||||
```
|
||||
|
||||
### Controller Params
|
||||
|
||||
Update `weapon_params` in both controllers:
|
||||
```ruby
|
||||
# Replace :ax_modifier1, :ax_modifier2 with FK versions
|
||||
:ax_modifier1_id, :ax_strength1, :ax_modifier2_id, :ax_strength2,
|
||||
:befoulment_modifier_id, :befoulment_strength, :exorcism_level
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Import Service
|
||||
|
||||
### Game JSON Structure
|
||||
|
||||
**Odiant weapon (with befoulment):**
|
||||
```json
|
||||
{
|
||||
"augment_skill_info": [[{ "skill_id": 2880, "effect_value": "25", "show_value": "-25%" }]],
|
||||
"odiant": {
|
||||
"is_odiant_weapon": true,
|
||||
"exorcision_level": 1,
|
||||
"max_exorcision_level": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Regular weapon (with AX skills):**
|
||||
```json
|
||||
{
|
||||
"augment_skill_info": [[
|
||||
{ "skill_id": 1589, "effect_value": "3", "show_value": "+3%" },
|
||||
{ "skill_id": 1719, "effect_value": "1_2000", "show_value": "+3" }
|
||||
]],
|
||||
"odiant": {
|
||||
"is_odiant_weapon": false,
|
||||
"exorcision_level": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key insight:** Same `augment_skill_info` structure, differentiated by `is_odiant_weapon`:
|
||||
- `true` → parse as befoulment (skill_id 2880+)
|
||||
- `false` → parse as AX skills (skill_id 1588-1722)
|
||||
|
||||
### WeaponImportService Updates
|
||||
|
||||
Add a cache for WeaponStatModifier lookups:
|
||||
|
||||
```ruby
|
||||
def initialize(user, game_data, options = {})
|
||||
# ... existing code ...
|
||||
@modifier_cache = {} # Cache for WeaponStatModifier lookups
|
||||
end
|
||||
|
||||
def build_collection_weapon_attrs(item, weapon)
|
||||
param = item['param'] || {}
|
||||
|
||||
attrs = {
|
||||
weapon: weapon,
|
||||
game_id: param['id'].to_s,
|
||||
# ... existing attrs ...
|
||||
}
|
||||
|
||||
# Check if this is an Odiant (befoulment) weapon
|
||||
odiant = param['odiant']
|
||||
if odiant && odiant['is_odiant_weapon'] == true
|
||||
# Parse befoulment from augment_skill_info
|
||||
befoulment = parse_befoulment(param['augment_skill_info'])
|
||||
if befoulment
|
||||
attrs[:befoulment_modifier_id] = befoulment[:modifier_id]
|
||||
attrs[:befoulment_strength] = befoulment[:strength]
|
||||
end
|
||||
attrs[:exorcism_level] = odiant['exorcision_level'].to_i
|
||||
else
|
||||
# Regular weapon - parse AX skills
|
||||
ax_attrs = parse_ax_skills(param['augment_skill_info'])
|
||||
attrs.merge!(ax_attrs) if ax_attrs
|
||||
end
|
||||
|
||||
# ... rest of existing code (awakening, etc) ...
|
||||
attrs
|
||||
end
|
||||
|
||||
# Updated to return FK id instead of raw game_skill_id
|
||||
def parse_ax_skills(augment_skill_info)
|
||||
return nil if augment_skill_info.blank? || !augment_skill_info.is_a?(Array)
|
||||
|
||||
skills = augment_skill_info.first
|
||||
return nil if skills.blank? || !skills.is_a?(Array)
|
||||
|
||||
attrs = {}
|
||||
|
||||
# First AX skill
|
||||
if skills[0].is_a?(Hash)
|
||||
ax1 = parse_single_ax_skill(skills[0])
|
||||
if ax1
|
||||
attrs[:ax_modifier1_id] = ax1[:modifier_id]
|
||||
attrs[:ax_strength1] = ax1[:strength]
|
||||
end
|
||||
end
|
||||
|
||||
# Second AX skill
|
||||
if skills[1].is_a?(Hash)
|
||||
ax2 = parse_single_ax_skill(skills[1])
|
||||
if ax2
|
||||
attrs[:ax_modifier2_id] = ax2[:modifier_id]
|
||||
attrs[:ax_strength2] = ax2[:strength]
|
||||
end
|
||||
end
|
||||
|
||||
attrs.empty? ? nil : attrs
|
||||
end
|
||||
|
||||
def parse_single_ax_skill(skill)
|
||||
return nil unless skill['skill_id'].present?
|
||||
|
||||
game_skill_id = skill['skill_id'].to_i
|
||||
modifier = find_modifier_by_game_skill_id(game_skill_id)
|
||||
return nil unless modifier
|
||||
|
||||
strength = parse_ax_strength(skill['effect_value'], skill['show_value'])
|
||||
return nil unless strength
|
||||
|
||||
{ modifier_id: modifier.id, strength: strength }
|
||||
end
|
||||
|
||||
def parse_befoulment(augment_skill_info)
|
||||
return nil if augment_skill_info.blank? || !augment_skill_info.is_a?(Array)
|
||||
|
||||
skills = augment_skill_info.first
|
||||
return nil if skills.blank? || !skills.is_a?(Array)
|
||||
|
||||
skill = skills.first
|
||||
return nil unless skill.is_a?(Hash) && skill['skill_id'].present?
|
||||
|
||||
game_skill_id = skill['skill_id'].to_i
|
||||
modifier = find_modifier_by_game_skill_id(game_skill_id)
|
||||
return nil unless modifier
|
||||
|
||||
{
|
||||
modifier_id: modifier.id,
|
||||
strength: parse_befoulment_strength(skill['effect_value'], skill['show_value'])
|
||||
}
|
||||
end
|
||||
|
||||
def find_modifier_by_game_skill_id(game_skill_id)
|
||||
@modifier_cache[game_skill_id] ||= WeaponStatModifier.find_by(game_skill_id: game_skill_id)
|
||||
end
|
||||
|
||||
def parse_befoulment_strength(effect_value, show_value)
|
||||
# show_value has the sign: "-25%"
|
||||
# effect_value is unsigned: "25"
|
||||
if show_value.present?
|
||||
show_value.to_s.gsub('%', '').to_f
|
||||
elsif effect_value.present?
|
||||
-effect_value.to_f
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Note:** Unknown `game_skill_id` values will be skipped (modifier not found). This is acceptable - we can add new modifiers to the seed data as we discover them.
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| **Migrations** | |
|
||||
| `db/migrate/xxx_create_weapon_stat_modifiers.rb` | Create - new reference table |
|
||||
| `db/migrate/xxx_refactor_ax_and_add_befoulments.rb` | Create - replace ax_modifier integers with FKs, add befoulment FKs |
|
||||
| `db/migrate/xxx_replace_has_ax_skills_with_augment_type.rb` | Create - enum migration with data migration |
|
||||
| `db/data/xxx_migrate_ax_modifiers_to_fk.rb` | Create - data migration to convert existing ax_modifier values to FK references |
|
||||
| **Models** | |
|
||||
| `app/models/weapon_stat_modifier.rb` | Create |
|
||||
| `app/models/collection_weapon.rb` | Modify - add befoulment validation |
|
||||
| `app/models/grid_weapon.rb` | Modify - add befoulment fields, sync |
|
||||
| `app/models/weapon_series.rb` | Modify - replace has_ax_skills with augment_type enum |
|
||||
| **Blueprints** | |
|
||||
| `app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb` | Create |
|
||||
| `app/blueprints/api/v1/collection_weapon_blueprint.rb` | Modify - add befoulment serialization |
|
||||
| `app/blueprints/api/v1/grid_weapon_blueprint.rb` | Modify - add befoulment serialization |
|
||||
| `app/blueprints/api/v1/weapon_series_blueprint.rb` | Modify - replace has_ax_skills with augment_type |
|
||||
| `app/blueprints/api/v1/weapon_blueprint.rb` | Modify - update series.has_ax_skills reference |
|
||||
| **Controllers** | |
|
||||
| `app/controllers/api/v1/weapon_stat_modifiers_controller.rb` | Create |
|
||||
| `app/controllers/api/v1/collection_weapons_controller.rb` | Modify - permit befoulment params |
|
||||
| `app/controllers/api/v1/grid_weapons_controller.rb` | Modify - permit befoulment params |
|
||||
| `app/controllers/api/v1/weapon_series_controller.rb` | Modify - permit augment_type instead of has_ax_skills |
|
||||
| **Services & Seeds** | |
|
||||
| `app/services/weapon_import_service.rb` | Modify - parse befoulments |
|
||||
| `db/seeds/weapon_stat_modifiers.rb` | Create |
|
||||
| **Config & Tests** | |
|
||||
| `config/routes.rb` | Modify - add weapon_stat_modifiers route |
|
||||
| `spec/factories/weapon_series.rb` | Modify - replace has_ax_skills with augment_type |
|
||||
|
||||
---
|
||||
|
||||
## API Breaking Changes
|
||||
|
||||
### 1. WeaponSeries: `has_ax_skills` → `augment_type`
|
||||
|
||||
**Before:**
|
||||
```json
|
||||
{ "has_ax_skills": true }
|
||||
```
|
||||
|
||||
**After:**
|
||||
```json
|
||||
{ "augment_type": "ax" } // or "befoulment" or "none"
|
||||
```
|
||||
|
||||
Frontend: `augment_type === 'ax'` instead of `has_ax_skills === true`.
|
||||
|
||||
### 2. Collection/GridWeapon: AX skill structure changed
|
||||
|
||||
**Before:**
|
||||
```json
|
||||
{
|
||||
"ax_modifier1": 1589,
|
||||
"ax_strength1": 3.0,
|
||||
"ax_modifier2": 1719,
|
||||
"ax_strength2": 2000
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```json
|
||||
{
|
||||
"ax": [
|
||||
{
|
||||
"modifier": { "id": 1, "slug": "ax_atk", "name_en": "ATK Up", "category": "ax", ... },
|
||||
"strength": 3.0
|
||||
},
|
||||
{
|
||||
"modifier": { "id": 5, "slug": "ax_ability_dmg", "name_en": "Ability DMG Up", "category": "ax", ... },
|
||||
"strength": 2000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Frontend: Access via `weapon.ax[0].modifier.slug` instead of `weapon.ax_modifier1`.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### Migration Order
|
||||
|
||||
Migrations must be deployed in this order:
|
||||
|
||||
1. `create_weapon_stat_modifiers` - Create table + seed data
|
||||
2. `add_ax_and_befoulment_fk_columns` - Add new FK columns (keep old integer columns)
|
||||
3. `migrate_ax_modifiers_to_fk` - Data migration: lookup existing values → FK refs
|
||||
4. `remove_old_ax_modifier_columns` - Remove old integer columns
|
||||
5. `replace_has_ax_skills_with_augment_type` - WeaponSeries enum change
|
||||
|
||||
### GridWeapon Amoeba Config
|
||||
|
||||
Update nullify list for party cloning:
|
||||
|
||||
```ruby
|
||||
amoeba do
|
||||
nullify :ax_modifier1_id
|
||||
nullify :ax_modifier2_id
|
||||
nullify :ax_strength1
|
||||
nullify :ax_strength2
|
||||
nullify :befoulment_modifier_id
|
||||
nullify :befoulment_strength
|
||||
nullify :exorcism_level
|
||||
end
|
||||
```
|
||||
|
||||
### Unknown Skill ID Logging
|
||||
|
||||
When import encounters unknown `game_skill_id`, log with icon image for discovery:
|
||||
|
||||
```ruby
|
||||
Rails.logger.warn(
|
||||
"[WeaponImportService] Unknown augment skill_id=#{game_skill_id} " \
|
||||
"icon=#{skill['augment_skill_icon_image']}"
|
||||
)
|
||||
```
|
||||
|
||||
### Database Indexes
|
||||
|
||||
Add index on `weapon_stat_modifiers.game_skill_id` for fast import lookups:
|
||||
|
||||
```ruby
|
||||
add_index :weapon_stat_modifiers, :game_skill_id, unique: true
|
||||
```
|
||||
|
||||
### Coordinated Release
|
||||
|
||||
Frontend and backend releases will be coordinated. No backwards compatibility layer needed.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ **Game JSON samples** - received for both Odiant and regular weapons
|
||||
2. **Identify Odiant weapon series** in database (set `augment_type: :befoulment`)
|
||||
3. **Discover remaining game_skill_ids** for other befoulment types (we know 2880 = DEF Down)
|
||||
4. **Verify AX skill IDs** match between AX_MAPPING and game data
|
||||
|
||||
## Known Skill IDs
|
||||
|
||||
### AX Skills (Complete)
|
||||
|
||||
| skill_id | Stat | Name (EN) | Name (JP) | Suffix |
|
||||
|----------|------|-----------|-----------|--------|
|
||||
| 1588 | hp | HP | HP | % |
|
||||
| 1589 | atk | ATK | 攻撃 | % |
|
||||
| 1590 | def | DEF | 防御 | % |
|
||||
| 1591 | ca_dmg | C.A. DMG | 奥義ダメ | % |
|
||||
| 1592 | multiattack | Multiattack Rate | 連撃率 | % |
|
||||
| 1593 | debuff_res | Debuff Resistance | 弱体耐性 | % |
|
||||
| 1594 | ele_atk | Elemental ATK | 全属性攻撃力 | % |
|
||||
| 1595 | healing | Healing | 回復性能 | % |
|
||||
| 1596 | da | Double Attack Rate | DA確率 | % |
|
||||
| 1597 | ta | Triple Attack Rate | TA確率 | % |
|
||||
| 1599 | ca_cap | C.A. DMG Cap | 奥義上限 | % |
|
||||
| 1600 | stamina | Stamina | 渾身 | - |
|
||||
| 1601 | enmity | Enmity | 背水 | - |
|
||||
| 1719 | skill_supp | Supplemental Skill DMG | アビ与ダメ上昇 | - |
|
||||
| 1720 | ca_supp | Supplemental C.A. DMG | 奥義与ダメ上昇 | - |
|
||||
| 1721 | ele_dmg_red | Elemental DMG Reduction | 属性ダメ軽減 | % |
|
||||
| 1722 | na_cap | Normal ATK DMG Cap | 通常ダメ上限 | % |
|
||||
| 1837 | exp | EXP Gain | EXP UP | % |
|
||||
| 1838 | rupie | Rupie Gain | 獲得ルピ | % |
|
||||
|
||||
### Befoulments (Partial - more to discover)
|
||||
|
||||
| skill_id | Stat | Name (EN) | Name (JP) |
|
||||
|----------|------|-----------|-----------|
|
||||
| 2880 | def | DEF Down | 防御力DOWN |
|
||||
| ? | atk | ATK Down | 攻撃力DOWN |
|
||||
| ? | da_ta | DA/TA Down | 連撃率DOWN |
|
||||
| ? | ca_dmg | CA DMG Down | 奥義ダメージDOWN |
|
||||
| ? | dot | Damage Over Time | 毎ターンダメージ |
|
||||
| ? | hp | Max HP Down | 最大HP減少 |
|
||||
| ? | debuff_success | Debuff Success Down | 弱体成功率DOWN |
|
||||
| ? | ability_dmg | Ability DMG Down | アビリティダメージDOWN |
|
||||
|
||||
(Befoulment skill_ids will be discovered as users import Odiant weapons)
|
||||
|
|
@ -8,12 +8,17 @@ FactoryBot.define do
|
|||
awakening_level { 1 }
|
||||
element { nil } # Only used for element-changeable weapons
|
||||
|
||||
# AX skills
|
||||
# AX skills (FK to weapon_stat_modifiers)
|
||||
ax_modifier1 { nil }
|
||||
ax_strength1 { nil }
|
||||
ax_modifier2 { nil }
|
||||
ax_strength2 { nil }
|
||||
|
||||
# Befoulment (FK to weapon_stat_modifiers)
|
||||
befoulment_modifier { nil }
|
||||
befoulment_strength { nil }
|
||||
exorcism_level { 0 }
|
||||
|
||||
# Weapon keys
|
||||
weapon_key1 { nil }
|
||||
weapon_key2 { nil }
|
||||
|
|
@ -75,10 +80,24 @@ FactoryBot.define do
|
|||
|
||||
# Trait for AX weapon with skills
|
||||
trait :with_ax do
|
||||
ax_modifier1 { 1 } # Attack modifier
|
||||
ax_strength1 { 3.5 }
|
||||
ax_modifier2 { 2 } # HP modifier
|
||||
ax_strength2 { 10.0 }
|
||||
after(:build) do |collection_weapon|
|
||||
collection_weapon.ax_modifier1 = WeaponStatModifier.find_by(slug: 'ax_atk') ||
|
||||
FactoryBot.create(:weapon_stat_modifier, :ax_atk)
|
||||
collection_weapon.ax_modifier2 = WeaponStatModifier.find_by(slug: 'ax_hp') ||
|
||||
FactoryBot.create(:weapon_stat_modifier, :ax_hp)
|
||||
end
|
||||
end
|
||||
|
||||
# Trait for Odiant weapon with befoulment
|
||||
trait :with_befoulment do
|
||||
befoulment_strength { 23.0 }
|
||||
exorcism_level { 2 }
|
||||
after(:build) do |collection_weapon|
|
||||
collection_weapon.befoulment_modifier = WeaponStatModifier.find_by(slug: 'befoul_def_down') ||
|
||||
FactoryBot.create(:weapon_stat_modifier, :befoul_def_down)
|
||||
end
|
||||
end
|
||||
|
||||
# Trait for element-changed weapon (Revans weapons)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,39 @@ FactoryBot.define do
|
|||
transcendence_step { 0 }
|
||||
mainhand { false }
|
||||
|
||||
# AX skills (FK to weapon_stat_modifiers)
|
||||
ax_modifier1 { nil }
|
||||
ax_strength1 { nil }
|
||||
ax_modifier2 { nil }
|
||||
ax_strength2 { nil }
|
||||
|
||||
# Befoulment (FK to weapon_stat_modifiers)
|
||||
befoulment_modifier { nil }
|
||||
befoulment_strength { nil }
|
||||
exorcism_level { 0 }
|
||||
|
||||
# Optional associations for weapon keys and awakening are left as nil by default.
|
||||
|
||||
# Trait for AX weapon with skills
|
||||
trait :with_ax do
|
||||
ax_strength1 { 3.5 }
|
||||
ax_strength2 { 10.0 }
|
||||
after(:build) do |grid_weapon|
|
||||
grid_weapon.ax_modifier1 = WeaponStatModifier.find_by(slug: 'ax_atk') ||
|
||||
FactoryBot.create(:weapon_stat_modifier, :ax_atk)
|
||||
grid_weapon.ax_modifier2 = WeaponStatModifier.find_by(slug: 'ax_hp') ||
|
||||
FactoryBot.create(:weapon_stat_modifier, :ax_hp)
|
||||
end
|
||||
end
|
||||
|
||||
# Trait for Odiant weapon with befoulment
|
||||
trait :with_befoulment do
|
||||
befoulment_strength { 23.0 }
|
||||
exorcism_level { 2 }
|
||||
after(:build) do |grid_weapon|
|
||||
grid_weapon.befoulment_modifier = WeaponStatModifier.find_by(slug: 'befoul_def_down') ||
|
||||
FactoryBot.create(:weapon_stat_modifier, :befoul_def_down)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ FactoryBot.define do
|
|||
element_changeable { false }
|
||||
has_weapon_keys { false }
|
||||
has_awakening { false }
|
||||
has_ax_skills { false }
|
||||
augment_type { :none }
|
||||
|
||||
trait :gacha do
|
||||
slug { 'gacha' }
|
||||
|
|
@ -96,5 +96,21 @@ FactoryBot.define do
|
|||
trait :with_weapon_keys do
|
||||
has_weapon_keys { true }
|
||||
end
|
||||
|
||||
trait :with_ax_skills do
|
||||
augment_type { :ax }
|
||||
end
|
||||
|
||||
trait :with_befoulments do
|
||||
augment_type { :befoulment }
|
||||
end
|
||||
|
||||
trait :odiant do
|
||||
slug { 'odiant' }
|
||||
name_en { 'Odiant' }
|
||||
name_jp { '禁禍武器' }
|
||||
order { 50 }
|
||||
augment_type { :befoulment }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
47
spec/factories/weapon_stat_modifiers.rb
Normal file
47
spec/factories/weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :weapon_stat_modifier do
|
||||
sequence(:slug) { |n| "ax-modifier-#{n}" }
|
||||
sequence(:name_en) { |n| "AX Modifier #{n}" }
|
||||
category { 'ax' }
|
||||
polarity { 1 }
|
||||
|
||||
trait :ax_atk do
|
||||
slug { 'ax_atk' }
|
||||
name_en { 'ATK' }
|
||||
category { 'ax' }
|
||||
stat { 'atk' }
|
||||
polarity { 1 }
|
||||
suffix { '%' }
|
||||
game_skill_id { 1589 }
|
||||
end
|
||||
|
||||
trait :ax_hp do
|
||||
slug { 'ax_hp' }
|
||||
name_en { 'HP' }
|
||||
category { 'ax' }
|
||||
stat { 'hp' }
|
||||
polarity { 1 }
|
||||
suffix { '%' }
|
||||
game_skill_id { 1588 }
|
||||
end
|
||||
|
||||
trait :befoulment do
|
||||
sequence(:slug) { |n| "befoul-modifier-#{n}" }
|
||||
sequence(:name_en) { |n| "Befoulment #{n}" }
|
||||
category { 'befoulment' }
|
||||
polarity { -1 }
|
||||
end
|
||||
|
||||
trait :befoul_def_down do
|
||||
slug { 'befoul_def_down' }
|
||||
name_en { 'DEF Down' }
|
||||
category { 'befoulment' }
|
||||
stat { 'def' }
|
||||
polarity { -1 }
|
||||
suffix { '%' }
|
||||
game_skill_id { 2880 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -76,9 +76,14 @@ RSpec.describe CollectionWeapon, type: :model do
|
|||
end
|
||||
|
||||
describe 'AX skill validations' do
|
||||
let(:ax_modifier) do
|
||||
WeaponStatModifier.find_by(slug: 'ax_atk') ||
|
||||
create(:weapon_stat_modifier, :ax_atk)
|
||||
end
|
||||
|
||||
context 'when AX skill has only modifier' do
|
||||
it 'is invalid' do
|
||||
collection_weapon = build(:collection_weapon, ax_modifier1: 1, ax_strength1: nil)
|
||||
collection_weapon = build(:collection_weapon, ax_modifier1: ax_modifier, ax_strength1: nil)
|
||||
expect(collection_weapon).not_to be_valid
|
||||
expect(collection_weapon.errors[:base]).to include('AX skill 1 must have both modifier and strength')
|
||||
end
|
||||
|
|
@ -94,7 +99,7 @@ RSpec.describe CollectionWeapon, type: :model do
|
|||
|
||||
context 'when AX skill has both modifier and strength' do
|
||||
it 'is valid' do
|
||||
collection_weapon = build(:collection_weapon, ax_modifier1: 1, ax_strength1: 3.5)
|
||||
collection_weapon = build(:collection_weapon, ax_modifier1: ax_modifier, ax_strength1: 3.5)
|
||||
expect(collection_weapon).to be_valid
|
||||
end
|
||||
end
|
||||
|
|
@ -215,9 +220,11 @@ RSpec.describe CollectionWeapon, type: :model do
|
|||
ax_weapon = create(:collection_weapon, :with_ax)
|
||||
|
||||
aggregate_failures do
|
||||
expect(ax_weapon.ax_modifier1).to eq(1)
|
||||
expect(ax_weapon.ax_modifier1).to be_a(WeaponStatModifier)
|
||||
expect(ax_weapon.ax_modifier1.slug).to eq('ax_atk')
|
||||
expect(ax_weapon.ax_strength1).to eq(3.5)
|
||||
expect(ax_weapon.ax_modifier2).to eq(2)
|
||||
expect(ax_weapon.ax_modifier2).to be_a(WeaponStatModifier)
|
||||
expect(ax_weapon.ax_modifier2.slug).to eq('ax_hp')
|
||||
expect(ax_weapon.ax_strength2).to eq(10.0)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -162,11 +162,16 @@ RSpec.describe 'Collection Weapons API', type: :request do
|
|||
end
|
||||
|
||||
it 'creates weapon with AX skills' do
|
||||
ax_atk = WeaponStatModifier.find_by(slug: 'ax_atk') ||
|
||||
create(:weapon_stat_modifier, :ax_atk)
|
||||
ax_hp = WeaponStatModifier.find_by(slug: 'ax_hp') ||
|
||||
create(:weapon_stat_modifier, :ax_hp)
|
||||
|
||||
ax_attributes = valid_attributes.deep_merge(
|
||||
collection_weapon: {
|
||||
ax_modifier1: 1,
|
||||
ax_modifier1_id: ax_atk.id,
|
||||
ax_strength1: 3.5,
|
||||
ax_modifier2: 2,
|
||||
ax_modifier2_id: ax_hp.id,
|
||||
ax_strength2: 2.0
|
||||
}
|
||||
)
|
||||
|
|
@ -176,14 +181,17 @@ RSpec.describe 'Collection Weapons API', type: :request do
|
|||
expect(response).to have_http_status(:created)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['ax']).to be_present
|
||||
expect(json['ax'].first['modifier']).to eq(1)
|
||||
expect(json['ax'].first['modifier']['slug']).to eq('ax_atk')
|
||||
expect(json['ax'].first['strength']).to eq(3.5)
|
||||
end
|
||||
|
||||
it 'returns error with incomplete AX skills' do
|
||||
ax_atk = WeaponStatModifier.find_by(slug: 'ax_atk') ||
|
||||
create(:weapon_stat_modifier, :ax_atk)
|
||||
|
||||
invalid_ax = valid_attributes.deep_merge(
|
||||
collection_weapon: {
|
||||
ax_modifier1: 1
|
||||
ax_modifier1_id: ax_atk.id
|
||||
# Missing ax_strength1
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ RSpec.describe 'GridWeapons API', type: :request do
|
|||
weapon_key1_id: nil,
|
||||
weapon_key2_id: nil,
|
||||
weapon_key3_id: nil,
|
||||
ax_modifier1: nil,
|
||||
ax_modifier2: nil,
|
||||
ax_modifier1_id: nil,
|
||||
ax_modifier2_id: nil,
|
||||
ax_strength1: nil,
|
||||
ax_strength2: nil,
|
||||
awakening_id: nil,
|
||||
|
|
@ -197,8 +197,8 @@ RSpec.describe 'GridWeapons API', type: :request do
|
|||
weapon_key1_id: nil,
|
||||
weapon_key2_id: nil,
|
||||
weapon_key3_id: nil,
|
||||
ax_modifier1: nil,
|
||||
ax_modifier2: nil,
|
||||
ax_modifier1_id: nil,
|
||||
ax_modifier2_id: nil,
|
||||
ax_strength1: nil,
|
||||
ax_strength2: nil,
|
||||
awakening_id: nil,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,25 @@ RSpec.describe Processors::WeaponProcessor, type: :model do
|
|||
|
||||
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
|
||||
|
||||
let!(:ax_hp_modifier) do
|
||||
WeaponStatModifier.find_by(slug: 'ax_hp') ||
|
||||
create(:weapon_stat_modifier, :ax_hp)
|
||||
end
|
||||
|
||||
let!(:ax_ca_dmg_modifier) do
|
||||
WeaponStatModifier.find_by(slug: 'ax_ca_dmg') ||
|
||||
create(:weapon_stat_modifier,
|
||||
slug: 'ax_ca_dmg',
|
||||
name_en: 'C.A. DMG',
|
||||
category: 'ax',
|
||||
stat: 'ca_dmg',
|
||||
polarity: 1,
|
||||
suffix: '%',
|
||||
game_skill_id: 1591)
|
||||
end
|
||||
|
||||
it 'flattens nested augment_skill_info and assigns ax_modifier_id and ax_strength' do
|
||||
ax_skill_info = [
|
||||
[
|
||||
{ 'skill_id' => '1588', 'effect_value' => '3', 'show_value' => '3%' },
|
||||
|
|
@ -48,10 +66,10 @@ RSpec.describe Processors::WeaponProcessor, type: :model do
|
|||
]
|
||||
]
|
||||
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)
|
||||
expect(grid_weapon.ax_modifier1).to eq(ax_hp_modifier)
|
||||
expect(grid_weapon.ax_strength1).to eq(3.0)
|
||||
expect(grid_weapon.ax_modifier2).to eq(ax_ca_dmg_modifier)
|
||||
expect(grid_weapon.ax_strength2).to eq(5.0)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,29 @@ RSpec.describe WeaponImportService, type: :service do
|
|||
create(:awakening, :for_weapon, slug: 'weapon-heal', name_en: 'Healing')
|
||||
end
|
||||
|
||||
# Create weapon stat modifiers for AX skill tests
|
||||
let!(:ax_atk_modifier) do
|
||||
WeaponStatModifier.find_by(slug: 'ax_atk') ||
|
||||
create(:weapon_stat_modifier, :ax_atk)
|
||||
end
|
||||
|
||||
let!(:ax_hp_modifier) do
|
||||
WeaponStatModifier.find_by(slug: 'ax_hp') ||
|
||||
create(:weapon_stat_modifier, :ax_hp)
|
||||
end
|
||||
|
||||
let!(:ax_ca_dmg_modifier) do
|
||||
WeaponStatModifier.find_by(slug: 'ax_ca_dmg') ||
|
||||
create(:weapon_stat_modifier,
|
||||
slug: 'ax_ca_dmg',
|
||||
name_en: 'C.A. DMG',
|
||||
category: 'ax',
|
||||
stat: 'ca_dmg',
|
||||
polarity: 1,
|
||||
suffix: '%',
|
||||
game_skill_id: 1591)
|
||||
end
|
||||
|
||||
before do
|
||||
standard_weapon
|
||||
transcendable_weapon
|
||||
|
|
@ -279,12 +302,12 @@ RSpec.describe WeaponImportService, type: :service do
|
|||
'augment_skill_info' => [
|
||||
[
|
||||
{
|
||||
'skill_id' => 1,
|
||||
'skill_id' => 1589, # ATK modifier
|
||||
'effect_value' => '7',
|
||||
'show_value' => '7%'
|
||||
},
|
||||
{
|
||||
'skill_id' => 2,
|
||||
'skill_id' => 1588, # HP modifier
|
||||
'effect_value' => '2_4',
|
||||
'show_value' => '4%'
|
||||
}
|
||||
|
|
@ -302,7 +325,7 @@ RSpec.describe WeaponImportService, type: :service do
|
|||
result = service.import
|
||||
|
||||
weapon = result.created.first
|
||||
expect(weapon.ax_modifier1).to eq(1)
|
||||
expect(weapon.ax_modifier1).to eq(ax_atk_modifier)
|
||||
expect(weapon.ax_strength1).to eq(7.0)
|
||||
end
|
||||
|
||||
|
|
@ -311,7 +334,7 @@ RSpec.describe WeaponImportService, type: :service do
|
|||
result = service.import
|
||||
|
||||
weapon = result.created.first
|
||||
expect(weapon.ax_modifier2).to eq(2)
|
||||
expect(weapon.ax_modifier2).to eq(ax_hp_modifier)
|
||||
expect(weapon.ax_strength2).to eq(4.0)
|
||||
end
|
||||
end
|
||||
|
|
@ -329,7 +352,7 @@ RSpec.describe WeaponImportService, type: :service do
|
|||
'augment_skill_info' => [
|
||||
[
|
||||
{
|
||||
'skill_id' => 3,
|
||||
'skill_id' => 1591, # C.A. DMG modifier
|
||||
'effect_value' => nil,
|
||||
'show_value' => '5.5%'
|
||||
}
|
||||
|
|
@ -347,7 +370,7 @@ RSpec.describe WeaponImportService, type: :service do
|
|||
result = service.import
|
||||
|
||||
weapon = result.created.first
|
||||
expect(weapon.ax_modifier1).to eq(3)
|
||||
expect(weapon.ax_modifier1).to eq(ax_ca_dmg_modifier)
|
||||
expect(weapon.ax_strength1).to eq(5.5)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue