update tests and factories for weapon_stat_modifier fks

This commit is contained in:
Justin Edmund 2025-12-30 22:59:41 -08:00
parent 4d87d113dc
commit abc30c151b
11 changed files with 291 additions and 62 deletions

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

@ -1,13 +1,36 @@
# frozen_string_literal: true
class MigrateAxModifiersToFk < ActiveRecord::Migration[8.0]
# Old AX_MAPPING from WeaponProcessor: game_skill_id (string) => internal_value (integer)
# We need the reverse: internal_value => game_skill_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 cache: game_skill_id -> weapon_stat_modifier.id
modifier_lookup = WeaponStatModifier.pluck(:game_skill_id, :id).to_h
modifier_by_game_skill_id = WeaponStatModifier.pluck(:game_skill_id, :id).to_h
# Migrate CollectionWeapon ax_modifier1
CollectionWeapon.where.not(ax_modifier1: nil).find_each do |cw|
modifier_id = modifier_lookup[cw.ax_modifier1]
game_skill_id = OLD_INTERNAL_TO_GAME_SKILL_ID[cw.ax_modifier1]
modifier_id = game_skill_id ? modifier_by_game_skill_id[game_skill_id] : nil
if modifier_id
cw.update_columns(ax_modifier1_ref_id: modifier_id)
else
@ -17,7 +40,8 @@ class MigrateAxModifiersToFk < ActiveRecord::Migration[8.0]
# Migrate CollectionWeapon ax_modifier2
CollectionWeapon.where.not(ax_modifier2: nil).find_each do |cw|
modifier_id = modifier_lookup[cw.ax_modifier2]
game_skill_id = OLD_INTERNAL_TO_GAME_SKILL_ID[cw.ax_modifier2]
modifier_id = game_skill_id ? modifier_by_game_skill_id[game_skill_id] : nil
if modifier_id
cw.update_columns(ax_modifier2_ref_id: modifier_id)
else
@ -27,7 +51,8 @@ class MigrateAxModifiersToFk < ActiveRecord::Migration[8.0]
# Migrate GridWeapon ax_modifier1
GridWeapon.where.not(ax_modifier1: nil).find_each do |gw|
modifier_id = modifier_lookup[gw.ax_modifier1]
game_skill_id = OLD_INTERNAL_TO_GAME_SKILL_ID[gw.ax_modifier1]
modifier_id = game_skill_id ? modifier_by_game_skill_id[game_skill_id] : nil
if modifier_id
gw.update_columns(ax_modifier1_ref_id: modifier_id)
else
@ -37,7 +62,8 @@ class MigrateAxModifiersToFk < ActiveRecord::Migration[8.0]
# Migrate GridWeapon ax_modifier2
GridWeapon.where.not(ax_modifier2: nil).find_each do |gw|
modifier_id = modifier_lookup[gw.ax_modifier2]
game_skill_id = OLD_INTERNAL_TO_GAME_SKILL_ID[gw.ax_modifier2]
modifier_id = game_skill_id ? modifier_by_game_skill_id[game_skill_id] : nil
if modifier_id
gw.update_columns(ax_modifier2_ref_id: modifier_id)
else
@ -47,18 +73,24 @@ class MigrateAxModifiersToFk < ActiveRecord::Migration[8.0]
end
def down
# Reverse: copy FK back to integer columns
# Build reverse lookup: game_skill_id -> old internal value
game_skill_id_to_internal = OLD_INTERNAL_TO_GAME_SKILL_ID.invert
# Reverse: copy FK back to integer columns using old internal values
WeaponStatModifier.find_each do |modifier|
next unless modifier.game_skill_id
internal_value = game_skill_id_to_internal[modifier.game_skill_id]
next unless internal_value
CollectionWeapon.where(ax_modifier1_ref_id: modifier.id)
.update_all(ax_modifier1: modifier.game_skill_id)
.update_all(ax_modifier1: internal_value)
CollectionWeapon.where(ax_modifier2_ref_id: modifier.id)
.update_all(ax_modifier2: modifier.game_skill_id)
.update_all(ax_modifier2: internal_value)
GridWeapon.where(ax_modifier1_ref_id: modifier.id)
.update_all(ax_modifier1: modifier.game_skill_id)
.update_all(ax_modifier1: internal_value)
GridWeapon.where(ax_modifier2_ref_id: modifier.id)
.update_all(ax_modifier2: modifier.game_skill_id)
.update_all(ax_modifier2: internal_value)
end
end
end

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