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:
Justin Edmund 2025-12-31 22:20:00 -08:00 committed by GitHub
parent a6b7e26210
commit 1f80e4189f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1596 additions and 122 deletions

View file

@ -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|

View file

@ -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|

View file

@ -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
}

View file

@ -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|

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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'

View 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

View file

@ -41,26 +41,6 @@ module Processors
class WeaponProcessor < BaseProcessor
TRANSCENDENCE_LEVELS = [200, 210, 220, 230, 240, 250].freeze
# Mapping from ingame 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 skills 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 ingame awakening data (stored under "arousal") to our Awakening record.
#

View file

@ -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")

View file

@ -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

View 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

View 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

View 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

View file

@ -1 +1 @@
DataMigrate::Data.define(version: 20251214193836)
DataMigrate::Data.define(version: 20251230000002)

View 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

View 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

View 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

View file

@ -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

View file

@ -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"

View file

@ -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

1 id slug name_en name_jp order extra element_changeable has_weapon_keys has_awakening has_ax_skills augment_type
2 00000000-0001-0000-0000-000000000001 gacha Gacha Weapons ガチャ武器 0 false false false false false none
3 00000000-0001-0000-0000-000000000002 grand Grand Weapons リミテッドシリーズ 2 false false true false false none
4 00000000-0001-0000-0000-000000000003 dark-opus Dark Opus Weapons 終末の神器 3 false false true true false none
5 00000000-0001-0000-0000-000000000004 revenant Revenant Weapons 天星器 21 false true false false false none
6 00000000-0001-0000-0000-000000000005 xeno Xeno Weapons 六道武器 23 true false false false false none
7 00000000-0001-0000-0000-000000000006 ultima Ultima Weapons オメガウェポン 9 false true true false false none
8 00000000-0001-0000-0000-000000000007 superlative Superlative Weapons スペリオシリーズ 27 true true true false false none
9 00000000-0001-0000-0000-000000000008 draconic Draconic Weapons ドラコニックウェポン・オリジン 5 false false true true false none
10 00000000-0001-0000-0000-000000000009 draconic-providence Draconic Weapons Providence ドラコニックウェポン 6 false false true true false none
11 00000000-0001-0000-0000-000000000010 primal Primal Weapons プライマルシリーズ 32 false false false false false none
12 00000000-0001-0000-0000-000000000011 event Event Weapons イベント武器 99 false false false false false none

View 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"

View 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)

View file

@ -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)

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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
}
)

View file

@ -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,

View file

@ -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

View file

@ -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