Merge branch 'next-main' of https://github.com/jedmund/hensei-api into next-main
This commit is contained in:
commit
c6d117fc09
51 changed files with 2174 additions and 146 deletions
|
|
@ -7,10 +7,28 @@ module Api
|
||||||
:created_at, :updated_at
|
:created_at, :updated_at
|
||||||
|
|
||||||
field :ax, if: ->(_, obj, _) { obj.ax_modifier1.present? } do |obj|
|
field :ax, if: ->(_, obj, _) { obj.ax_modifier1.present? } do |obj|
|
||||||
[
|
skills = []
|
||||||
{ modifier: obj.ax_modifier1, strength: obj.ax_strength1 },
|
if obj.ax_modifier1.present?
|
||||||
{ modifier: obj.ax_modifier2, strength: obj.ax_strength2 }
|
skills << {
|
||||||
].compact_blank
|
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
|
end
|
||||||
|
|
||||||
field :awakening, if: ->(_, obj, _) { obj.awakening.present? } do |obj|
|
field :awakening, if: ->(_, obj, _) { obj.awakening.present? } do |obj|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridArtifactBlueprint < ApiBlueprint
|
class GridArtifactBlueprint < ApiBlueprint
|
||||||
fields :level, :reroll_slot
|
fields :level, :reroll_slot, :orphaned
|
||||||
|
|
||||||
field :collection_artifact_id
|
field :collection_artifact_id
|
||||||
field :out_of_sync, if: ->(_field, ga, _options) { ga.collection_artifact_id.present? } do |ga|
|
field :out_of_sync, if: ->(_field, ga, _options) { ga.collection_artifact_id.present? } do |ga|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridSummonBlueprint < ApiBlueprint
|
class GridSummonBlueprint < ApiBlueprint
|
||||||
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step
|
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step, :orphaned
|
||||||
|
|
||||||
field :collection_summon_id
|
field :collection_summon_id
|
||||||
field :out_of_sync, if: ->(_field, gs, _options) { gs.collection_summon_id.present? } do |gs|
|
field :out_of_sync, if: ->(_field, gs, _options) { gs.collection_summon_id.present? } do |gs|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridWeaponBlueprint < ApiBlueprint
|
class GridWeaponBlueprint < ApiBlueprint
|
||||||
fields :mainhand, :position, :uncap_level, :transcendence_step, :element
|
fields :mainhand, :position, :uncap_level, :transcendence_step, :element, :orphaned
|
||||||
|
|
||||||
field :collection_weapon_id
|
field :collection_weapon_id
|
||||||
field :out_of_sync, if: ->(_field, gw, _options) { gw.collection_weapon_id.present? } do |gw|
|
field :out_of_sync, if: ->(_field, gw, _options) { gw.collection_weapon_id.present? } do |gw|
|
||||||
|
|
@ -15,11 +15,29 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
view :nested do
|
view :nested do
|
||||||
field :ax, if: ->(_field_name, w, _options) { w.weapon.present? && w.weapon.ax } do |w|
|
field :ax, if: ->(_field_name, w, _options) { w.ax_modifier1.present? } do |w|
|
||||||
[
|
skills = []
|
||||||
{ modifier: w.ax_modifier1, strength: w.ax_strength1 },
|
if w.ax_modifier1.present?
|
||||||
{ modifier: w.ax_modifier2, strength: w.ax_strength2 }
|
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
|
end
|
||||||
|
|
||||||
field :awakening, if: ->(_field_name, w, _options) { w.awakening.present? } do |w|
|
field :awakening, if: ->(_field_name, w, _options) { w.awakening.present? } do |w|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ module Api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :has_orphaned_items do |party|
|
||||||
|
party.has_orphaned_items?
|
||||||
|
end
|
||||||
|
|
||||||
# For collection views
|
# For collection views
|
||||||
view :preview do
|
view :preview do
|
||||||
include_view :preview_objects # Characters, Weapons, Summons
|
include_view :preview_objects # Characters, Weapons, Summons
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ module Api
|
||||||
},
|
},
|
||||||
has_weapon_keys: w.weapon_series.has_weapon_keys,
|
has_weapon_keys: w.weapon_series.has_weapon_keys,
|
||||||
has_awakening: w.weapon_series.has_awakening,
|
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,
|
extra: w.weapon_series.extra,
|
||||||
element_changeable: w.weapon_series.element_changeable
|
element_changeable: w.weapon_series.element_changeable
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
fields :slug, :order, :extra, :element_changeable, :has_weapon_keys,
|
fields :slug, :order, :extra, :element_changeable, :has_weapon_keys,
|
||||||
:has_awakening, :has_ax_skills
|
:has_awakening, :augment_type
|
||||||
|
|
||||||
view :full do
|
view :full do
|
||||||
field :weapon_count do |ws|
|
field :weapon_count do |ws|
|
||||||
|
|
|
||||||
10
app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb
Normal file
10
app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
class WeaponStatModifierBlueprint < Blueprinter::Base
|
||||||
|
identifier :id
|
||||||
|
fields :slug, :name_en, :name_jp, :category, :stat, :polarity, :suffix
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -9,7 +9,7 @@ module Api
|
||||||
before_action :set_collection_artifact_for_read, only: %i[show]
|
before_action :set_collection_artifact_for_read, only: %i[show]
|
||||||
|
|
||||||
# Write actions: require auth, use current_user
|
# Write actions: require auth, use current_user
|
||||||
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
|
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
|
||||||
before_action :set_collection_artifact_for_write, only: %i[update destroy]
|
before_action :set_collection_artifact_for_write, only: %i[update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -109,6 +109,8 @@ module Api
|
||||||
#
|
#
|
||||||
# @param data [Hash] Game data containing artifact list
|
# @param data [Hash] Game data containing artifact list
|
||||||
# @param update_existing [Boolean] Whether to update existing artifacts (default: false)
|
# @param update_existing [Boolean] Whether to update existing artifacts (default: false)
|
||||||
|
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
|
||||||
|
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
|
||||||
def import
|
def import
|
||||||
game_data = import_params[:data]
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
|
@ -119,7 +121,10 @@ module Api
|
||||||
service = ArtifactImportService.new(
|
service = ArtifactImportService.new(
|
||||||
current_user,
|
current_user,
|
||||||
game_data,
|
game_data,
|
||||||
update_existing: import_params[:update_existing] == true
|
update_existing: import_params[:update_existing] == true,
|
||||||
|
is_full_inventory: import_params[:is_full_inventory] == true,
|
||||||
|
reconcile_deletions: import_params[:reconcile_deletions] == true,
|
||||||
|
filter: import_params[:filter]
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
@ -131,10 +136,42 @@ module Api
|
||||||
created: result.created&.size || 0,
|
created: result.created&.size || 0,
|
||||||
updated: result.updated&.size || 0,
|
updated: result.updated&.size || 0,
|
||||||
skipped: result.skipped&.size || 0,
|
skipped: result.skipped&.size || 0,
|
||||||
errors: result.errors || []
|
errors: result.errors || [],
|
||||||
|
reconciliation: result.reconciliation
|
||||||
}, status: status
|
}, status: status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /collection/artifacts/preview_sync
|
||||||
|
# Previews what would be deleted in a full sync operation
|
||||||
|
#
|
||||||
|
# @param data [Hash] Game data containing artifact list
|
||||||
|
# @return [JSON] List of items that would be deleted
|
||||||
|
def preview_sync
|
||||||
|
game_data = import_params[:data]
|
||||||
|
filter = import_params[:filter]
|
||||||
|
|
||||||
|
unless game_data.present?
|
||||||
|
return render json: { error: 'No data provided' }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
service = ArtifactImportService.new(current_user, game_data, filter: filter)
|
||||||
|
items_to_delete = service.preview_deletions
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
will_delete: items_to_delete.map do |ca|
|
||||||
|
{
|
||||||
|
id: ca.id,
|
||||||
|
game_id: ca.game_id,
|
||||||
|
name: ca.artifact&.name_en,
|
||||||
|
granblue_id: ca.artifact&.granblue_id,
|
||||||
|
element: ca.element,
|
||||||
|
level: ca.level
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
count: items_to_delete.size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# DELETE /collection/artifacts/batch_destroy
|
# DELETE /collection/artifacts/batch_destroy
|
||||||
# Deletes multiple collection artifacts in a single request
|
# Deletes multiple collection artifacts in a single request
|
||||||
def batch_destroy
|
def batch_destroy
|
||||||
|
|
@ -197,7 +234,10 @@ module Api
|
||||||
def import_params
|
def import_params
|
||||||
{
|
{
|
||||||
update_existing: params[:update_existing],
|
update_existing: params[:update_existing],
|
||||||
data: params[:data]&.to_unsafe_h
|
is_full_inventory: params[:is_full_inventory],
|
||||||
|
reconcile_deletions: params[:reconcile_deletions],
|
||||||
|
data: params[:data]&.to_unsafe_h,
|
||||||
|
filter: params[:filter]&.to_unsafe_h
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ module Api
|
||||||
before_action :set_collection_summon_for_read, only: %i[show]
|
before_action :set_collection_summon_for_read, only: %i[show]
|
||||||
|
|
||||||
# Write actions: require auth, use current_user
|
# Write actions: require auth, use current_user
|
||||||
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
|
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
|
||||||
before_action :set_collection_summon_for_write, only: %i[update destroy]
|
before_action :set_collection_summon_for_write, only: %i[update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -113,6 +113,8 @@ module Api
|
||||||
#
|
#
|
||||||
# @param data [Hash] Game data containing summon list
|
# @param data [Hash] Game data containing summon list
|
||||||
# @param update_existing [Boolean] Whether to update existing summons (default: false)
|
# @param update_existing [Boolean] Whether to update existing summons (default: false)
|
||||||
|
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
|
||||||
|
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
|
||||||
def import
|
def import
|
||||||
game_data = import_params[:data]
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
|
@ -123,7 +125,10 @@ module Api
|
||||||
service = SummonImportService.new(
|
service = SummonImportService.new(
|
||||||
current_user,
|
current_user,
|
||||||
game_data,
|
game_data,
|
||||||
update_existing: import_params[:update_existing] == true
|
update_existing: import_params[:update_existing] == true,
|
||||||
|
is_full_inventory: import_params[:is_full_inventory] == true,
|
||||||
|
reconcile_deletions: import_params[:reconcile_deletions] == true,
|
||||||
|
filter: import_params[:filter]
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
@ -135,10 +140,42 @@ module Api
|
||||||
created: result.created.size,
|
created: result.created.size,
|
||||||
updated: result.updated.size,
|
updated: result.updated.size,
|
||||||
skipped: result.skipped.size,
|
skipped: result.skipped.size,
|
||||||
errors: result.errors
|
errors: result.errors,
|
||||||
|
reconciliation: result.reconciliation
|
||||||
}, status: status
|
}, status: status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /collection/summons/preview_sync
|
||||||
|
# Previews what would be deleted in a full sync operation
|
||||||
|
#
|
||||||
|
# @param data [Hash] Game data containing summon list
|
||||||
|
# @return [JSON] List of items that would be deleted
|
||||||
|
def preview_sync
|
||||||
|
game_data = import_params[:data]
|
||||||
|
filter = import_params[:filter]
|
||||||
|
|
||||||
|
unless game_data.present?
|
||||||
|
return render json: { error: 'No data provided' }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
service = SummonImportService.new(current_user, game_data, filter: filter)
|
||||||
|
items_to_delete = service.preview_deletions
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
will_delete: items_to_delete.map do |cs|
|
||||||
|
{
|
||||||
|
id: cs.id,
|
||||||
|
game_id: cs.game_id,
|
||||||
|
name: cs.summon&.name_en,
|
||||||
|
granblue_id: cs.summon&.granblue_id,
|
||||||
|
uncap_level: cs.uncap_level,
|
||||||
|
transcendence_step: cs.transcendence_step
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
count: items_to_delete.size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_target_user
|
def set_target_user
|
||||||
|
|
@ -181,7 +218,10 @@ module Api
|
||||||
def import_params
|
def import_params
|
||||||
{
|
{
|
||||||
update_existing: params[:update_existing],
|
update_existing: params[:update_existing],
|
||||||
data: params[:data]&.to_unsafe_h
|
is_full_inventory: params[:is_full_inventory],
|
||||||
|
reconcile_deletions: params[:reconcile_deletions],
|
||||||
|
data: params[:data]&.to_unsafe_h,
|
||||||
|
filter: params[:filter]&.to_unsafe_h
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,16 @@ module Api
|
||||||
before_action :set_collection_weapon_for_read, only: %i[show]
|
before_action :set_collection_weapon_for_read, only: %i[show]
|
||||||
|
|
||||||
# Write actions: require auth, use current_user
|
# Write actions: require auth, use current_user
|
||||||
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
|
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
|
||||||
before_action :set_collection_weapon_for_write, only: %i[update destroy]
|
before_action :set_collection_weapon_for_write, only: %i[update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@collection_weapons = @target_user.collection_weapons
|
@collection_weapons = @target_user.collection_weapons
|
||||||
.includes(:weapon, :awakening,
|
.includes(:weapon, :awakening,
|
||||||
:weapon_key1, :weapon_key2,
|
: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)
|
# Apply filters (array_param splits comma-separated values for OR logic)
|
||||||
@collection_weapons = @collection_weapons.by_weapon(params[:weapon_id]) if params[:weapon_id]
|
@collection_weapons = @collection_weapons.by_weapon(params[:weapon_id]) if params[:weapon_id]
|
||||||
|
|
@ -119,6 +121,8 @@ module Api
|
||||||
#
|
#
|
||||||
# @param data [Hash] Game data containing weapon list
|
# @param data [Hash] Game data containing weapon list
|
||||||
# @param update_existing [Boolean] Whether to update existing weapons (default: false)
|
# @param update_existing [Boolean] Whether to update existing weapons (default: false)
|
||||||
|
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
|
||||||
|
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
|
||||||
def import
|
def import
|
||||||
game_data = import_params[:data]
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
|
@ -129,7 +133,10 @@ module Api
|
||||||
service = WeaponImportService.new(
|
service = WeaponImportService.new(
|
||||||
current_user,
|
current_user,
|
||||||
game_data,
|
game_data,
|
||||||
update_existing: import_params[:update_existing] == true
|
update_existing: import_params[:update_existing] == true,
|
||||||
|
is_full_inventory: import_params[:is_full_inventory] == true,
|
||||||
|
reconcile_deletions: import_params[:reconcile_deletions] == true,
|
||||||
|
filter: import_params[:filter]
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
@ -141,10 +148,42 @@ module Api
|
||||||
created: result.created.size,
|
created: result.created.size,
|
||||||
updated: result.updated.size,
|
updated: result.updated.size,
|
||||||
skipped: result.skipped.size,
|
skipped: result.skipped.size,
|
||||||
errors: result.errors
|
errors: result.errors,
|
||||||
|
reconciliation: result.reconciliation
|
||||||
}, status: status
|
}, status: status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /collection/weapons/preview_sync
|
||||||
|
# Previews what would be deleted in a full sync operation
|
||||||
|
#
|
||||||
|
# @param data [Hash] Game data containing weapon list
|
||||||
|
# @return [JSON] List of items that would be deleted
|
||||||
|
def preview_sync
|
||||||
|
game_data = import_params[:data]
|
||||||
|
filter = import_params[:filter]
|
||||||
|
|
||||||
|
unless game_data.present?
|
||||||
|
return render json: { error: 'No data provided' }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
service = WeaponImportService.new(current_user, game_data, filter: filter)
|
||||||
|
items_to_delete = service.preview_deletions
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
will_delete: items_to_delete.map do |cw|
|
||||||
|
{
|
||||||
|
id: cw.id,
|
||||||
|
game_id: cw.game_id,
|
||||||
|
name: cw.weapon&.name_en,
|
||||||
|
granblue_id: cw.weapon&.granblue_id,
|
||||||
|
uncap_level: cw.uncap_level,
|
||||||
|
transcendence_step: cw.transcendence_step
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
count: items_to_delete.size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_target_user
|
def set_target_user
|
||||||
|
|
@ -177,7 +216,8 @@ module Api
|
||||||
:weapon_id, :uncap_level, :transcendence_step,
|
:weapon_id, :uncap_level, :transcendence_step,
|
||||||
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
|
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
|
||||||
:awakening_id, :awakening_level,
|
: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
|
:element
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
@ -187,7 +227,8 @@ module Api
|
||||||
:weapon_id, :uncap_level, :transcendence_step,
|
:weapon_id, :uncap_level, :transcendence_step,
|
||||||
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
|
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
|
||||||
:awakening_id, :awakening_level,
|
: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
|
:element
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
@ -195,7 +236,10 @@ module Api
|
||||||
def import_params
|
def import_params
|
||||||
{
|
{
|
||||||
update_existing: params[:update_existing],
|
update_existing: params[:update_existing],
|
||||||
data: params[:data]&.to_unsafe_h
|
is_full_inventory: params[:is_full_inventory],
|
||||||
|
reconcile_deletions: params[:reconcile_deletions],
|
||||||
|
data: params[:data]&.to_unsafe_h,
|
||||||
|
filter: params[:filter]&.to_unsafe_h
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,13 +291,13 @@ module Api
|
||||||
##
|
##
|
||||||
# Normalizes the AX modifier fields for the weapon parameters.
|
# 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]
|
# @return [void]
|
||||||
def normalize_ax_fields!
|
def normalize_ax_fields!
|
||||||
params[:weapon][:ax_modifier1] = nil if weapon_params[:ax_modifier1].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][:ax_modifier2] = nil if weapon_params[:ax_modifier2].to_i == -1
|
params[:weapon][:befoulment_modifier_id] = nil if weapon_params[:befoulment_modifier_id].to_i == -1
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -515,7 +515,8 @@ module Api
|
||||||
:id, :party_id, :weapon_id, :collection_weapon_id,
|
:id, :party_id, :weapon_id, :collection_weapon_id,
|
||||||
:position, :mainhand, :uncap_level, :transcendence_step, :element,
|
:position, :mainhand, :uncap_level, :transcendence_step, :element,
|
||||||
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id,
|
: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
|
:awakening_id, :awakening_level
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,10 @@ module Api
|
||||||
awakening: {},
|
awakening: {},
|
||||||
weapon_key1: {},
|
weapon_key1: {},
|
||||||
weapon_key2: {},
|
weapon_key2: {},
|
||||||
weapon_key3: {}
|
weapon_key3: {},
|
||||||
|
ax_modifier1: {},
|
||||||
|
ax_modifier2: {},
|
||||||
|
befoulment_modifier: {}
|
||||||
} },
|
} },
|
||||||
{ summons: :summon },
|
{ summons: :summon },
|
||||||
:guidebook1, :guidebook2, :guidebook3,
|
: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],
|
{ ring1: %i[modifier strength], ring2: %i[modifier strength], ring3: %i[modifier strength], ring4: %i[modifier strength],
|
||||||
earring: %i[modifier strength] }],
|
earring: %i[modifier strength] }],
|
||||||
summons_attributes: %i[id party_id summon_id position main friend quick_summon uncap_level transcendence_step],
|
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
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ module Api
|
||||||
params.require(:weapon_series).permit(
|
params.require(:weapon_series).permit(
|
||||||
:name_en, :name_jp, :slug, :order,
|
:name_en, :name_jp, :slug, :order,
|
||||||
:extra, :element_changeable, :has_weapon_keys,
|
:extra, :element_changeable, :has_weapon_keys,
|
||||||
:has_awakening, :has_ax_skills
|
:has_awakening, :augment_type
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
21
app/controllers/api/v1/weapon_stat_modifiers_controller.rb
Normal file
21
app/controllers/api/v1/weapon_stat_modifiers_controller.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
class WeaponStatModifiersController < Api::V1::ApiController
|
||||||
|
# GET /weapon_stat_modifiers
|
||||||
|
def index
|
||||||
|
@modifiers = WeaponStatModifier.all
|
||||||
|
@modifiers = @modifiers.where(category: params[:category]) if params[:category].present?
|
||||||
|
|
||||||
|
render json: WeaponStatModifierBlueprint.render(@modifiers, root: :weapon_stat_modifiers)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /weapon_stat_modifiers/:id
|
||||||
|
def show
|
||||||
|
@modifier = WeaponStatModifier.find(params[:id])
|
||||||
|
render json: WeaponStatModifierBlueprint.render(@modifier)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -7,6 +7,10 @@ class CollectionArtifact < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :artifact
|
belongs_to :artifact
|
||||||
|
|
||||||
|
has_many :grid_artifacts, dependent: :nullify
|
||||||
|
|
||||||
|
before_destroy :orphan_grid_items
|
||||||
|
|
||||||
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
||||||
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
||||||
enum :element, {
|
enum :element, {
|
||||||
|
|
@ -77,4 +81,12 @@ class CollectionArtifact < ApplicationRecord
|
||||||
def quirk_artifact?
|
def quirk_artifact?
|
||||||
artifact&.quirk?
|
artifact&.quirk?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks all linked grid artifacts as orphaned before destroying this collection artifact.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def orphan_grid_items
|
||||||
|
grid_artifacts.update_all(orphaned: true, collection_artifact_id: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ class CollectionSummon < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :summon
|
belongs_to :summon
|
||||||
|
|
||||||
|
has_many :grid_summons, dependent: :nullify
|
||||||
|
|
||||||
|
before_destroy :orphan_grid_items
|
||||||
|
|
||||||
validates :uncap_level, inclusion: { in: 0..5 }
|
validates :uncap_level, inclusion: { in: 0..5 }
|
||||||
validates :transcendence_step, inclusion: { in: 0..10 }
|
validates :transcendence_step, inclusion: { in: 0..10 }
|
||||||
|
|
||||||
|
|
@ -31,4 +35,12 @@ class CollectionSummon < ApplicationRecord
|
||||||
errors.add(:transcendence_step, "not available for this summon")
|
errors.add(:transcendence_step, "not available for this summon")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks all linked grid summons as orphaned before destroying this collection summon.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def orphan_grid_items
|
||||||
|
grid_summons.update_all(orphaned: true, collection_summon_id: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -8,15 +8,25 @@ class CollectionWeapon < ApplicationRecord
|
||||||
belongs_to :weapon_key3, class_name: 'WeaponKey', optional: true
|
belongs_to :weapon_key3, class_name: 'WeaponKey', optional: true
|
||||||
belongs_to :weapon_key4, 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
|
||||||
|
|
||||||
# Set defaults before validation so database defaults don't cause validation failures
|
# Set defaults before validation so database defaults don't cause validation failures
|
||||||
attribute :awakening_level, :integer, default: 1
|
attribute :awakening_level, :integer, default: 1
|
||||||
|
|
||||||
validates :uncap_level, inclusion: { in: 0..5 }
|
validates :uncap_level, inclusion: { in: 0..5 }
|
||||||
validates :transcendence_step, inclusion: { in: 0..10 }
|
validates :transcendence_step, inclusion: { in: 0..10 }
|
||||||
validates :awakening_level, inclusion: { in: 1..20 }
|
validates :awakening_level, inclusion: { in: 1..20 }
|
||||||
|
validates :exorcism_level, inclusion: { in: 0..5 }, allow_nil: true
|
||||||
|
|
||||||
validate :validate_weapon_keys
|
validate :validate_weapon_keys
|
||||||
validate :validate_ax_skills
|
validate :validate_ax_skills
|
||||||
|
validate :validate_befoulment
|
||||||
validate :validate_element_change
|
validate :validate_element_change
|
||||||
validate :validate_awakening_compatibility
|
validate :validate_awakening_compatibility
|
||||||
validate :validate_awakening_level
|
validate :validate_awakening_level
|
||||||
|
|
@ -25,7 +35,7 @@ class CollectionWeapon < ApplicationRecord
|
||||||
scope :by_weapon, ->(weapon_id) { where(weapon_id: weapon_id) }
|
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 :by_series, ->(series_id) { joins(:weapon).where(weapons: { weapon_series_id: series_id }) }
|
||||||
scope :with_keys, -> { where.not(weapon_key1_id: nil) }
|
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_element, ->(element) { joins(:weapon).where(weapons: { element: element }) }
|
||||||
scope :by_rarity, ->(rarity) { joins(:weapon).where(weapons: { rarity: rarity }) }
|
scope :by_rarity, ->(rarity) { joins(:weapon).where(weapons: { rarity: rarity }) }
|
||||||
scope :by_proficiency, ->(proficiency) { joins(:weapon).where(weapons: { proficiency: proficiency }) }
|
scope :by_proficiency, ->(proficiency) { joins(:weapon).where(weapons: { proficiency: proficiency }) }
|
||||||
|
|
@ -83,16 +93,43 @@ class CollectionWeapon < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_ax_skills
|
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?) ||
|
if (ax_modifier1.present? && ax_strength1.blank?) ||
|
||||||
(ax_modifier1.blank? && ax_strength1.present?)
|
(ax_modifier1.blank? && ax_strength1.present?)
|
||||||
errors.add(:base, "AX skill 1 must have both modifier and strength")
|
errors.add(:base, "AX skill 1 must have both modifier and strength")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# AX skill 2: must have both modifier and strength
|
||||||
if (ax_modifier2.present? && ax_strength2.blank?) ||
|
if (ax_modifier2.present? && ax_strength2.blank?) ||
|
||||||
(ax_modifier2.blank? && ax_strength2.present?)
|
(ax_modifier2.blank? && ax_strength2.present?)
|
||||||
errors.add(:base, "AX skill 2 must have both modifier and strength")
|
errors.add(:base, "AX skill 2 must have both modifier and strength")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def validate_element_change
|
def validate_element_change
|
||||||
|
|
@ -129,4 +166,12 @@ class CollectionWeapon < ApplicationRecord
|
||||||
errors.add(:transcendence_step, "not available for this weapon") if transcendence_step > 0
|
errors.add(:transcendence_step, "not available for this weapon") if transcendence_step > 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks all linked grid weapons as orphaned before destroying this collection weapon.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def orphan_grid_items
|
||||||
|
grid_weapons.update_all(orphaned: true, collection_weapon_id: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -11,6 +11,10 @@ class GridArtifact < ApplicationRecord
|
||||||
has_one :party, through: :grid_character
|
has_one :party, through: :grid_character
|
||||||
has_one :character, through: :grid_character
|
has_one :character, through: :grid_character
|
||||||
|
|
||||||
|
# Orphan status scopes
|
||||||
|
scope :orphaned, -> { where(orphaned: true) }
|
||||||
|
scope :not_orphaned, -> { where(orphaned: false) }
|
||||||
|
|
||||||
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
||||||
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
||||||
enum :element, {
|
enum :element, {
|
||||||
|
|
@ -55,6 +59,14 @@ class GridArtifact < ApplicationRecord
|
||||||
quirk_artifact? ? proficiency : artifact&.proficiency
|
quirk_artifact? ? proficiency : artifact&.proficiency
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks this grid artifact as orphaned and clears its collection link.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the update succeeded
|
||||||
|
def mark_orphaned!
|
||||||
|
update!(orphaned: true, collection_artifact_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Syncs customizations from the linked collection artifact.
|
# Syncs customizations from the linked collection artifact.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class GridSummon < ApplicationRecord
|
||||||
belongs_to :collection_summon, optional: true
|
belongs_to :collection_summon, optional: true
|
||||||
validates_presence_of :party
|
validates_presence_of :party
|
||||||
|
|
||||||
|
# Orphan status scopes
|
||||||
|
scope :orphaned, -> { where(orphaned: true) }
|
||||||
|
scope :not_orphaned, -> { where(orphaned: false) }
|
||||||
|
|
||||||
# Validate that position is provided.
|
# Validate that position is provided.
|
||||||
validates :position, presence: true
|
validates :position, presence: true
|
||||||
validate :compatible_with_position, on: :create
|
validate :compatible_with_position, on: :create
|
||||||
|
|
@ -40,6 +44,14 @@ class GridSummon < ApplicationRecord
|
||||||
GridSummonBlueprint
|
GridSummonBlueprint
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks this grid summon as orphaned and clears its collection link.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the update succeeded
|
||||||
|
def mark_orphaned!
|
||||||
|
update!(orphaned: true, collection_summon_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Syncs customizations from the linked collection summon.
|
# Syncs customizations from the linked collection summon.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,14 @@ class GridWeapon < ApplicationRecord
|
||||||
belongs_to :awakening, optional: true
|
belongs_to :awakening, optional: true
|
||||||
belongs_to :collection_weapon, 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) }
|
||||||
|
|
||||||
# Validate that uncap_level is present and numeric, transcendence_step is optional but must be numeric if present.
|
# Validate that uncap_level is present and numeric, transcendence_step is optional but must be numeric if present.
|
||||||
validates :uncap_level, presence: true, numericality: { only_integer: true }
|
validates :uncap_level, presence: true, numericality: { only_integer: true }
|
||||||
validates :transcendence_step, numericality: { only_integer: true }, allow_nil: true
|
validates :transcendence_step, numericality: { only_integer: true }, allow_nil: true
|
||||||
|
|
@ -51,10 +59,13 @@ class GridWeapon < ApplicationRecord
|
||||||
|
|
||||||
##### Amoeba configuration
|
##### Amoeba configuration
|
||||||
amoeba do
|
amoeba do
|
||||||
nullify :ax_modifier1
|
nullify :ax_modifier1_id
|
||||||
nullify :ax_modifier2
|
nullify :ax_modifier2_id
|
||||||
nullify :ax_strength1
|
nullify :ax_strength1
|
||||||
nullify :ax_strength2
|
nullify :ax_strength2
|
||||||
|
nullify :befoulment_modifier_id
|
||||||
|
nullify :befoulment_strength
|
||||||
|
nullify :exorcism_level
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -80,16 +91,27 @@ class GridWeapon < ApplicationRecord
|
||||||
weapon_key2_id: collection_weapon.weapon_key2_id,
|
weapon_key2_id: collection_weapon.weapon_key2_id,
|
||||||
weapon_key3_id: collection_weapon.weapon_key3_id,
|
weapon_key3_id: collection_weapon.weapon_key3_id,
|
||||||
weapon_key4_id: collection_weapon.weapon_key4_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_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,
|
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_id: collection_weapon.awakening_id,
|
||||||
awakening_level: collection_weapon.awakening_level
|
awakening_level: collection_weapon.awakening_level
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks this grid weapon as orphaned and clears its collection link.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the update succeeded
|
||||||
|
def mark_orphaned!
|
||||||
|
update!(orphaned: true, collection_weapon_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Checks if grid weapon is out of sync with collection.
|
# Checks if grid weapon is out of sync with collection.
|
||||||
#
|
#
|
||||||
|
|
@ -104,10 +126,13 @@ class GridWeapon < ApplicationRecord
|
||||||
weapon_key2_id != collection_weapon.weapon_key2_id ||
|
weapon_key2_id != collection_weapon.weapon_key2_id ||
|
||||||
weapon_key3_id != collection_weapon.weapon_key3_id ||
|
weapon_key3_id != collection_weapon.weapon_key3_id ||
|
||||||
weapon_key4_id != collection_weapon.weapon_key4_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_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 ||
|
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_id != collection_weapon.awakening_id ||
|
||||||
awakening_level != collection_weapon.awakening_level
|
awakening_level != collection_weapon.awakening_level
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,19 @@ class Party < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checks if the party has any orphaned grid items.
|
||||||
|
#
|
||||||
|
# An orphaned item is one whose linked collection item has been deleted,
|
||||||
|
# indicating the user no longer has that item in their game inventory.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the party has orphaned weapons, summons, or artifacts.
|
||||||
|
def has_orphaned_items?
|
||||||
|
weapons.orphaned.exists? ||
|
||||||
|
summons.orphaned.exists? ||
|
||||||
|
characters.joins(:grid_artifact).where(grid_artifacts: { orphaned: true }).exists?
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Determines if the party meets the minimum requirements for preview generation.
|
# Determines if the party meets the minimum requirements for preview generation.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ class WeaponSeries < ApplicationRecord
|
||||||
has_many :weapon_key_series, dependent: :destroy
|
has_many :weapon_key_series, dependent: :destroy
|
||||||
has_many :weapon_keys, through: :weapon_key_series
|
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_en, presence: true
|
||||||
validates :name_jp, presence: true
|
validates :name_jp, presence: true
|
||||||
validates :slug, presence: true, uniqueness: true
|
validates :slug, presence: true, uniqueness: true
|
||||||
|
|
@ -15,7 +17,8 @@ class WeaponSeries < ApplicationRecord
|
||||||
scope :element_changeable, -> { where(element_changeable: true) }
|
scope :element_changeable, -> { where(element_changeable: true) }
|
||||||
scope :with_weapon_keys, -> { where(has_weapon_keys: true) }
|
scope :with_weapon_keys, -> { where(has_weapon_keys: true) }
|
||||||
scope :with_awakening, -> { where(has_awakening: 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
|
# Slug constants for commonly referenced series
|
||||||
DARK_OPUS = 'dark-opus'
|
DARK_OPUS = 'dark-opus'
|
||||||
|
|
|
||||||
31
app/models/weapon_stat_modifier.rb
Normal file
31
app/models/weapon_stat_modifier.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reference table for weapon stat modifiers (AX skills and befoulments).
|
||||||
|
#
|
||||||
|
# AX skills are positive modifiers that can be applied to certain weapons.
|
||||||
|
# Befoulments are negative modifiers that appear on Odiant weapons.
|
||||||
|
#
|
||||||
|
class WeaponStatModifier < ApplicationRecord
|
||||||
|
CATEGORIES = %w[ax befoulment].freeze
|
||||||
|
|
||||||
|
validates :slug, presence: true, uniqueness: true
|
||||||
|
validates :name_en, presence: true
|
||||||
|
validates :category, presence: true, inclusion: { in: CATEGORIES }
|
||||||
|
validates :polarity, inclusion: { in: [-1, 1] }
|
||||||
|
|
||||||
|
scope :ax_skills, -> { where(category: 'ax') }
|
||||||
|
scope :befoulments, -> { where(category: 'befoulment') }
|
||||||
|
|
||||||
|
def self.find_by_game_skill_id(id)
|
||||||
|
find_by(game_skill_id: id.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def buff?
|
||||||
|
polarity == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def debuff?
|
||||||
|
polarity == -1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class ArtifactImportService
|
class ArtifactImportService
|
||||||
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
|
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
|
||||||
|
|
||||||
# Game element values to our element enum values
|
# Game element values to our element enum values
|
||||||
# Game: 1=Fire, 2=Water, 3=Earth, 4=Wind, 5=Light, 6=Dark
|
# Game: 1=Fire, 2=Water, 3=Earth, 4=Wind, 5=Light, 6=Dark
|
||||||
|
|
@ -30,10 +30,44 @@ class ArtifactImportService
|
||||||
@user = user
|
@user = user
|
||||||
@game_data = game_data
|
@game_data = game_data
|
||||||
@update_existing = options[:update_existing] || false
|
@update_existing = options[:update_existing] || false
|
||||||
|
@is_full_inventory = options[:is_full_inventory] || false
|
||||||
|
@reconcile_deletions = options[:reconcile_deletions] || false
|
||||||
|
@filter = options[:filter] # { elements: [...], proficiencies: [...] }
|
||||||
@created = []
|
@created = []
|
||||||
@updated = []
|
@updated = []
|
||||||
@skipped = []
|
@skipped = []
|
||||||
@errors = []
|
@errors = []
|
||||||
|
@processed_game_ids = []
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Previews what would be deleted in a sync operation.
|
||||||
|
# Does not modify any data, just returns items that would be removed.
|
||||||
|
# When a filter is active, only considers items matching that filter.
|
||||||
|
#
|
||||||
|
# @return [Array<CollectionArtifact>] Collection artifacts that would be deleted
|
||||||
|
def preview_deletions
|
||||||
|
items = extract_items
|
||||||
|
return [] if items.empty?
|
||||||
|
|
||||||
|
# Extract all game_ids from the import data
|
||||||
|
# Artifacts use 'id' directly (not nested in 'param')
|
||||||
|
game_ids = items.filter_map do |item|
|
||||||
|
data = item.is_a?(Hash) ? item.with_indifferent_access : item
|
||||||
|
data['id'].to_s if data['id'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
return [] if game_ids.empty?
|
||||||
|
|
||||||
|
# Find collection artifacts with game_ids NOT in the import
|
||||||
|
# Scoped to filter criteria if present
|
||||||
|
scope = @user.collection_artifacts
|
||||||
|
.includes(:artifact)
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: game_ids)
|
||||||
|
|
||||||
|
scope = apply_filter_scope(scope)
|
||||||
|
scope
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -43,7 +77,14 @@ class ArtifactImportService
|
||||||
def import
|
def import
|
||||||
items = extract_items
|
items = extract_items
|
||||||
if items.empty?
|
if items.empty?
|
||||||
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No artifact items found in data'])
|
return Result.new(
|
||||||
|
success?: false,
|
||||||
|
created: [],
|
||||||
|
updated: [],
|
||||||
|
skipped: [],
|
||||||
|
errors: ['No artifact items found in data'],
|
||||||
|
reconciliation: nil
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Preload artifacts and existing collection artifacts to avoid N+1 queries
|
# Preload artifacts and existing collection artifacts to avoid N+1 queries
|
||||||
|
|
@ -58,12 +99,19 @@ class ArtifactImportService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle deletion reconciliation if requested
|
||||||
|
reconciliation_result = nil
|
||||||
|
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
|
||||||
|
reconciliation_result = reconcile_deletions
|
||||||
|
end
|
||||||
|
|
||||||
Result.new(
|
Result.new(
|
||||||
success?: @errors.empty?,
|
success?: @errors.empty?,
|
||||||
created: @created,
|
created: @created,
|
||||||
updated: @updated,
|
updated: @updated,
|
||||||
skipped: @skipped,
|
skipped: @skipped,
|
||||||
errors: @errors
|
errors: @errors,
|
||||||
|
reconciliation: reconciliation_result
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -92,6 +140,10 @@ class ArtifactImportService
|
||||||
# Handle both string and symbol keys from params
|
# Handle both string and symbol keys from params
|
||||||
data = item.is_a?(Hash) ? item.with_indifferent_access : item
|
data = item.is_a?(Hash) ? item.with_indifferent_access : item
|
||||||
|
|
||||||
|
# Track this game_id as processed (for reconciliation)
|
||||||
|
game_id = data['id']
|
||||||
|
@processed_game_ids << game_id.to_s if game_id.present?
|
||||||
|
|
||||||
artifact = find_artifact(data['artifact_id'])
|
artifact = find_artifact(data['artifact_id'])
|
||||||
unless artifact
|
unless artifact
|
||||||
@errors << { game_id: data['id'], artifact_id: data['artifact_id'], error: 'Artifact not found' }
|
@errors << { game_id: data['id'], artifact_id: data['artifact_id'], error: 'Artifact not found' }
|
||||||
|
|
@ -193,4 +245,62 @@ class ArtifactImportService
|
||||||
'level' => level.to_i
|
'level' => level.to_i
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reconciles deletions by removing collection artifacts not in the processed list.
|
||||||
|
# Only called when @is_full_inventory and @reconcile_deletions are both true.
|
||||||
|
# When a filter is active, only deletes items matching that filter.
|
||||||
|
#
|
||||||
|
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
|
||||||
|
def reconcile_deletions
|
||||||
|
# Find collection artifacts with game_ids NOT in our processed list
|
||||||
|
# Scoped to filter criteria if present
|
||||||
|
scope = @user.collection_artifacts
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: @processed_game_ids)
|
||||||
|
|
||||||
|
scope = apply_filter_scope(scope)
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
orphaned_grid_item_ids = []
|
||||||
|
|
||||||
|
scope.find_each do |coll_artifact|
|
||||||
|
# Collect IDs of grid items that will be orphaned
|
||||||
|
grid_artifact_ids = GridArtifact.where(collection_artifact_id: coll_artifact.id).pluck(:id)
|
||||||
|
orphaned_grid_item_ids.concat(grid_artifact_ids)
|
||||||
|
|
||||||
|
# The before_destroy callback on CollectionArtifact will mark grid items as orphaned
|
||||||
|
coll_artifact.destroy
|
||||||
|
deleted_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
deleted: deleted_count,
|
||||||
|
orphaned_grid_items: orphaned_grid_item_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Applies element and proficiency filters to a collection artifacts scope.
|
||||||
|
# Used to scope deletion checks to only items matching the current game filter.
|
||||||
|
#
|
||||||
|
# @param scope [ActiveRecord::Relation] The collection artifacts relation to filter
|
||||||
|
# @return [ActiveRecord::Relation] Filtered relation
|
||||||
|
def apply_filter_scope(scope)
|
||||||
|
return scope unless @filter.present?
|
||||||
|
|
||||||
|
# Filter by elements if specified
|
||||||
|
if @filter[:elements].present? || @filter['elements'].present?
|
||||||
|
elements = @filter[:elements] || @filter['elements']
|
||||||
|
scope = scope.where(element: elements)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Filter by proficiencies if specified
|
||||||
|
if @filter[:proficiencies].present? || @filter['proficiencies'].present?
|
||||||
|
proficiencies = @filter[:proficiencies] || @filter['proficiencies']
|
||||||
|
scope = scope.where(proficiency: proficiencies)
|
||||||
|
end
|
||||||
|
|
||||||
|
scope
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -41,26 +41,6 @@ module Processors
|
||||||
class WeaponProcessor < BaseProcessor
|
class WeaponProcessor < BaseProcessor
|
||||||
TRANSCENDENCE_LEVELS = [200, 210, 220, 230, 240, 250].freeze
|
TRANSCENDENCE_LEVELS = [200, 210, 220, 230, 240, 250].freeze
|
||||||
|
|
||||||
# Mapping from in‑game AX skill IDs (as strings) to our internal modifier values.
|
|
||||||
AX_MAPPING = {
|
|
||||||
'1588' => 2,
|
|
||||||
'1589' => 0,
|
|
||||||
'1590' => 1,
|
|
||||||
'1591' => 3,
|
|
||||||
'1592' => 4,
|
|
||||||
'1593' => 9,
|
|
||||||
'1594' => 13,
|
|
||||||
'1595' => 10,
|
|
||||||
'1596' => 5,
|
|
||||||
'1597' => 6,
|
|
||||||
'1599' => 8,
|
|
||||||
'1600' => 12,
|
|
||||||
'1601' => 11,
|
|
||||||
'1719' => 15,
|
|
||||||
'1720' => 16,
|
|
||||||
'1721' => 17,
|
|
||||||
'1722' => 14
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
# KEY_MAPPING maps the raw key value (as a string) to a canonical range or value.
|
# 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
|
# 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.
|
# Processes AX (augment) skill data.
|
||||||
#
|
#
|
||||||
# The deck stores AX skills in an array of arrays under "augment_skill_info".
|
# The deck stores AX skills in an array of arrays under "augment_skill_info".
|
||||||
# This method flattens the data and assigns each skill’s modifier and strength.
|
# This method flattens the data and assigns each skill's modifier and strength.
|
||||||
|
# Modifiers are now looked up by game_skill_id in the weapon_stat_modifiers table.
|
||||||
#
|
#
|
||||||
# @param grid_weapon [GridWeapon] the grid weapon record being built.
|
# @param grid_weapon [GridWeapon] the grid weapon record being built.
|
||||||
# @param ax_skill_info [Array] the raw AX skill info.
|
# @param ax_skill_info [Array] the raw AX skill info.
|
||||||
|
|
@ -340,14 +321,59 @@ module Processors
|
||||||
# Flatten the nested array structure.
|
# Flatten the nested array structure.
|
||||||
ax_skills = ax_skill_info.flatten
|
ax_skills = ax_skill_info.flatten
|
||||||
ax_skills.each_with_index do |ax, idx|
|
ax_skills.each_with_index do |ax, idx|
|
||||||
ax_id = ax['skill_id'].to_s
|
break if idx >= 2 # Only 2 AX skill slots
|
||||||
ax_mod = AX_MAPPING[ax_id] || ax_id.to_i
|
|
||||||
strength = ax['effect_value'].to_s.gsub(/[+%]/, '').to_i
|
game_skill_id = ax['skill_id'].to_i
|
||||||
grid_weapon["ax_modifier#{idx + 1}"] = ax_mod
|
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
|
grid_weapon["ax_strength#{idx + 1}"] = strength
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Finds a WeaponStatModifier by its game_skill_id.
|
||||||
|
# Uses memoization to cache lookups.
|
||||||
|
#
|
||||||
|
# @param game_skill_id [Integer] the game's skill ID.
|
||||||
|
# @return [WeaponStatModifier, nil]
|
||||||
|
def find_modifier_by_game_skill_id(game_skill_id)
|
||||||
|
@modifier_cache ||= {}
|
||||||
|
@modifier_cache[game_skill_id] ||= WeaponStatModifier.find_by(game_skill_id: game_skill_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Parses the strength value from effect_value or show_value.
|
||||||
|
#
|
||||||
|
# @param effect_value [String, nil] the effect_value field.
|
||||||
|
# @param show_value [String, nil] the show_value field.
|
||||||
|
# @return [Float, nil]
|
||||||
|
def parse_augment_strength(effect_value, show_value)
|
||||||
|
if effect_value.present?
|
||||||
|
# Handle "1_3" format (seems to be "tier_value")
|
||||||
|
if effect_value.to_s.include?('_')
|
||||||
|
return effect_value.to_s.split('_').last.to_f
|
||||||
|
end
|
||||||
|
return effect_value.to_f if effect_value.to_s.match?(/\A[\d.]+\z/)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Try show_value (e.g., "3%")
|
||||||
|
if show_value.present?
|
||||||
|
return show_value.to_s.gsub('%', '').to_f
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Maps the in‑game awakening data (stored under "arousal") to our Awakening record.
|
# Maps the in‑game awakening data (stored under "arousal") to our Awakening record.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,49 @@
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class SummonImportService
|
class SummonImportService
|
||||||
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
|
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
|
||||||
|
|
||||||
def initialize(user, game_data, options = {})
|
def initialize(user, game_data, options = {})
|
||||||
@user = user
|
@user = user
|
||||||
@game_data = game_data
|
@game_data = game_data
|
||||||
@update_existing = options[:update_existing] || false
|
@update_existing = options[:update_existing] || false
|
||||||
|
@is_full_inventory = options[:is_full_inventory] || false
|
||||||
|
@reconcile_deletions = options[:reconcile_deletions] || false
|
||||||
|
@filter = options[:filter] # { elements: [...] }
|
||||||
@created = []
|
@created = []
|
||||||
@updated = []
|
@updated = []
|
||||||
@skipped = []
|
@skipped = []
|
||||||
@errors = []
|
@errors = []
|
||||||
|
@processed_game_ids = []
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Previews what would be deleted in a sync operation.
|
||||||
|
# Does not modify any data, just returns items that would be removed.
|
||||||
|
# When a filter is active, only considers items matching that filter.
|
||||||
|
#
|
||||||
|
# @return [Array<CollectionSummon>] Collection summons that would be deleted
|
||||||
|
def preview_deletions
|
||||||
|
items = extract_items
|
||||||
|
return [] if items.empty?
|
||||||
|
|
||||||
|
# Extract all game_ids from the import data
|
||||||
|
game_ids = items.filter_map do |item|
|
||||||
|
param = item['param'] || {}
|
||||||
|
param['id'].to_s if param['id'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
return [] if game_ids.empty?
|
||||||
|
|
||||||
|
# Find collection summons with game_ids NOT in the import
|
||||||
|
# Scoped to filter criteria if present
|
||||||
|
scope = @user.collection_summons
|
||||||
|
.includes(:summon)
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: game_ids)
|
||||||
|
|
||||||
|
scope = apply_filter_scope(scope)
|
||||||
|
scope
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -30,7 +63,16 @@ class SummonImportService
|
||||||
# @return [Result] Import result with counts and errors
|
# @return [Result] Import result with counts and errors
|
||||||
def import
|
def import
|
||||||
items = extract_items
|
items = extract_items
|
||||||
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No summon items found in data']) if items.empty?
|
if items.empty?
|
||||||
|
return Result.new(
|
||||||
|
success?: false,
|
||||||
|
created: [],
|
||||||
|
updated: [],
|
||||||
|
skipped: [],
|
||||||
|
errors: ['No summon items found in data'],
|
||||||
|
reconciliation: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
items.each_with_index do |item, index|
|
items.each_with_index do |item, index|
|
||||||
|
|
@ -40,12 +82,19 @@ class SummonImportService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle deletion reconciliation if requested
|
||||||
|
reconciliation_result = nil
|
||||||
|
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
|
||||||
|
reconciliation_result = reconcile_deletions
|
||||||
|
end
|
||||||
|
|
||||||
Result.new(
|
Result.new(
|
||||||
success?: @errors.empty?,
|
success?: @errors.empty?,
|
||||||
created: @created,
|
created: @created,
|
||||||
updated: @updated,
|
updated: @updated,
|
||||||
skipped: @skipped,
|
skipped: @skipped,
|
||||||
errors: @errors
|
errors: @errors,
|
||||||
|
reconciliation: reconciliation_result
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -68,6 +117,9 @@ class SummonImportService
|
||||||
granblue_id = image_id || master['id']
|
granblue_id = image_id || master['id']
|
||||||
game_id = param['id']
|
game_id = param['id']
|
||||||
|
|
||||||
|
# Track this game_id as processed (for reconciliation)
|
||||||
|
@processed_game_ids << game_id.to_s if game_id.present?
|
||||||
|
|
||||||
summon = find_summon(granblue_id)
|
summon = find_summon(granblue_id)
|
||||||
unless summon
|
unless summon
|
||||||
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Summon not found' }
|
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Summon not found' }
|
||||||
|
|
@ -143,4 +195,57 @@ class SummonImportService
|
||||||
value = phase.to_i
|
value = phase.to_i
|
||||||
value.clamp(0, 10)
|
value.clamp(0, 10)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reconciles deletions by removing collection summons not in the processed list.
|
||||||
|
# Only called when @is_full_inventory and @reconcile_deletions are both true.
|
||||||
|
# When a filter is active, only deletes items matching that filter.
|
||||||
|
#
|
||||||
|
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
|
||||||
|
def reconcile_deletions
|
||||||
|
# Find collection summons with game_ids NOT in our processed list
|
||||||
|
# Scoped to filter criteria if present
|
||||||
|
scope = @user.collection_summons
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: @processed_game_ids)
|
||||||
|
|
||||||
|
scope = apply_filter_scope(scope)
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
orphaned_grid_item_ids = []
|
||||||
|
|
||||||
|
scope.find_each do |coll_summon|
|
||||||
|
# Collect IDs of grid items that will be orphaned
|
||||||
|
grid_summon_ids = GridSummon.where(collection_summon_id: coll_summon.id).pluck(:id)
|
||||||
|
orphaned_grid_item_ids.concat(grid_summon_ids)
|
||||||
|
|
||||||
|
# The before_destroy callback on CollectionSummon will mark grid items as orphaned
|
||||||
|
coll_summon.destroy
|
||||||
|
deleted_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
deleted: deleted_count,
|
||||||
|
orphaned_grid_items: orphaned_grid_item_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Applies element filter to a collection summons scope.
|
||||||
|
# Used to scope deletion checks to only items matching the current game filter.
|
||||||
|
#
|
||||||
|
# @param scope [ActiveRecord::Relation] The collection summons relation to filter
|
||||||
|
# @return [ActiveRecord::Relation] Filtered relation
|
||||||
|
def apply_filter_scope(scope)
|
||||||
|
return scope unless @filter.present?
|
||||||
|
|
||||||
|
# Element: always join through summon (no element on collection_summons)
|
||||||
|
if @filter[:elements].present? || @filter['elements'].present?
|
||||||
|
elements = @filter[:elements] || @filter['elements']
|
||||||
|
scope = scope.joins(:summon).where(summons: { element: elements })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Summons don't have proficiency - ignore if present in filter
|
||||||
|
scope
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class WeaponImportService
|
class WeaponImportService
|
||||||
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
|
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
|
||||||
|
|
||||||
# Game awakening form to our slug mapping
|
# Game awakening form to our slug mapping
|
||||||
AWAKENING_FORM_MAPPING = {
|
AWAKENING_FORM_MAPPING = {
|
||||||
|
|
@ -28,11 +28,45 @@ class WeaponImportService
|
||||||
@user = user
|
@user = user
|
||||||
@game_data = game_data
|
@game_data = game_data
|
||||||
@update_existing = options[:update_existing] || false
|
@update_existing = options[:update_existing] || false
|
||||||
|
@is_full_inventory = options[:is_full_inventory] || false
|
||||||
|
@reconcile_deletions = options[:reconcile_deletions] || false
|
||||||
|
@filter = options[:filter] # { elements: [...], proficiencies: [...] }
|
||||||
@created = []
|
@created = []
|
||||||
@updated = []
|
@updated = []
|
||||||
@skipped = []
|
@skipped = []
|
||||||
@errors = []
|
@errors = []
|
||||||
@awakening_cache = {}
|
@awakening_cache = {}
|
||||||
|
@modifier_cache = {}
|
||||||
|
@processed_game_ids = []
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Previews what would be deleted in a sync operation.
|
||||||
|
# Does not modify any data, just returns items that would be removed.
|
||||||
|
# When a filter is active, only considers items matching that filter.
|
||||||
|
#
|
||||||
|
# @return [Array<CollectionWeapon>] Collection weapons that would be deleted
|
||||||
|
def preview_deletions
|
||||||
|
items = extract_items
|
||||||
|
return [] if items.empty?
|
||||||
|
|
||||||
|
# Extract all game_ids from the import data
|
||||||
|
game_ids = items.filter_map do |item|
|
||||||
|
param = item['param'] || {}
|
||||||
|
param['id'].to_s if param['id'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
return [] if game_ids.empty?
|
||||||
|
|
||||||
|
# Find collection weapons with game_ids NOT in the import
|
||||||
|
# Scoped to filter criteria if present
|
||||||
|
scope = @user.collection_weapons
|
||||||
|
.includes(:weapon)
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: game_ids)
|
||||||
|
|
||||||
|
scope = apply_filter_scope(scope)
|
||||||
|
scope
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -41,7 +75,16 @@ class WeaponImportService
|
||||||
# @return [Result] Import result with counts and errors
|
# @return [Result] Import result with counts and errors
|
||||||
def import
|
def import
|
||||||
items = extract_items
|
items = extract_items
|
||||||
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No weapon items found in data']) if items.empty?
|
if items.empty?
|
||||||
|
return Result.new(
|
||||||
|
success?: false,
|
||||||
|
created: [],
|
||||||
|
updated: [],
|
||||||
|
skipped: [],
|
||||||
|
errors: ['No weapon items found in data'],
|
||||||
|
reconciliation: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
items.each_with_index do |item, index|
|
items.each_with_index do |item, index|
|
||||||
|
|
@ -51,12 +94,19 @@ class WeaponImportService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle deletion reconciliation if requested
|
||||||
|
reconciliation_result = nil
|
||||||
|
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
|
||||||
|
reconciliation_result = reconcile_deletions
|
||||||
|
end
|
||||||
|
|
||||||
Result.new(
|
Result.new(
|
||||||
success?: @errors.empty?,
|
success?: @errors.empty?,
|
||||||
created: @created,
|
created: @created,
|
||||||
updated: @updated,
|
updated: @updated,
|
||||||
skipped: @skipped,
|
skipped: @skipped,
|
||||||
errors: @errors
|
errors: @errors,
|
||||||
|
reconciliation: reconciliation_result
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -77,6 +127,9 @@ class WeaponImportService
|
||||||
granblue_id = param['image_id'] || master['id']
|
granblue_id = param['image_id'] || master['id']
|
||||||
game_id = param['id']
|
game_id = param['id']
|
||||||
|
|
||||||
|
# Track this game_id as processed (for reconciliation)
|
||||||
|
@processed_game_ids << game_id.to_s if game_id.present?
|
||||||
|
|
||||||
weapon = find_weapon(granblue_id)
|
weapon = find_weapon(granblue_id)
|
||||||
unless weapon
|
unless weapon
|
||||||
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Weapon not found' }
|
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Weapon not found' }
|
||||||
|
|
@ -146,9 +199,18 @@ class WeaponImportService
|
||||||
awakening_attrs = parse_awakening(param['arousal'])
|
awakening_attrs = parse_awakening(param['arousal'])
|
||||||
attrs.merge!(awakening_attrs) if awakening_attrs
|
attrs.merge!(awakening_attrs) if awakening_attrs
|
||||||
|
|
||||||
# Parse AX skills if present
|
# Check if this is an Odiant (befoulment) weapon
|
||||||
ax_attrs = parse_ax_skills(param['augment_skill_info'])
|
odiant = param['odiant']
|
||||||
attrs.merge!(ax_attrs) if ax_attrs
|
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
|
attrs
|
||||||
end
|
end
|
||||||
|
|
@ -207,18 +269,18 @@ class WeaponImportService
|
||||||
|
|
||||||
# First AX skill
|
# First AX skill
|
||||||
if skills[0].is_a?(Hash)
|
if skills[0].is_a?(Hash)
|
||||||
ax1 = parse_single_ax_skill(skills[0])
|
ax1 = parse_single_augment_skill(skills[0])
|
||||||
if ax1
|
if ax1
|
||||||
attrs[:ax_modifier1] = ax1[:modifier]
|
attrs[:ax_modifier1_id] = ax1[:modifier_id]
|
||||||
attrs[:ax_strength1] = ax1[:strength]
|
attrs[:ax_strength1] = ax1[:strength]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Second AX skill
|
# Second AX skill
|
||||||
if skills[1].is_a?(Hash)
|
if skills[1].is_a?(Hash)
|
||||||
ax2 = parse_single_ax_skill(skills[1])
|
ax2 = parse_single_augment_skill(skills[1])
|
||||||
if ax2
|
if ax2
|
||||||
attrs[:ax_modifier2] = ax2[:modifier]
|
attrs[:ax_modifier2_id] = ax2[:modifier_id]
|
||||||
attrs[:ax_strength2] = ax2[:strength]
|
attrs[:ax_strength2] = ax2[:strength]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -227,26 +289,60 @@ class WeaponImportService
|
||||||
end
|
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
|
# @param augment_skill_info [Array] The game's augment skill data
|
||||||
# @return [Hash, nil] { modifier:, strength: } or nil
|
# @return [Hash, nil] Befoulment attributes or nil if no befoulment
|
||||||
def parse_single_ax_skill(skill)
|
def parse_befoulment(augment_skill_info)
|
||||||
return nil unless skill['skill_id'].present?
|
return nil if augment_skill_info.blank? || !augment_skill_info.is_a?(Array)
|
||||||
|
|
||||||
# The skill_id maps to our AX modifier
|
skills = augment_skill_info.first
|
||||||
modifier = skill['skill_id'].to_i
|
return nil if skills.blank? || !skills.is_a?(Array)
|
||||||
|
|
||||||
# Parse strength from effect_value (may be "3" or "1_3" format)
|
skill = skills.first
|
||||||
# or from show_value (may be "3%" format)
|
return nil unless skill.is_a?(Hash)
|
||||||
strength = parse_ax_strength(skill['effect_value'], skill['show_value'])
|
|
||||||
|
|
||||||
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
|
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
|
# Try effect_value first
|
||||||
if effect_value.present?
|
if effect_value.present?
|
||||||
# Handle "1_3" format (seems to be "tier_value")
|
# Handle "1_3" format (seems to be "tier_value")
|
||||||
|
|
@ -263,4 +359,66 @@ class WeaponImportService
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reconciles deletions by removing collection weapons not in the processed list.
|
||||||
|
# Only called when @is_full_inventory and @reconcile_deletions are both true.
|
||||||
|
# When a filter is active, only deletes items matching that filter.
|
||||||
|
#
|
||||||
|
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
|
||||||
|
def reconcile_deletions
|
||||||
|
# Find collection weapons with game_ids NOT in our processed list
|
||||||
|
# Scoped to filter criteria if present
|
||||||
|
scope = @user.collection_weapons
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: @processed_game_ids)
|
||||||
|
|
||||||
|
scope = apply_filter_scope(scope)
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
orphaned_grid_item_ids = []
|
||||||
|
|
||||||
|
scope.find_each do |coll_weapon|
|
||||||
|
# Collect IDs of grid items that will be orphaned
|
||||||
|
grid_weapon_ids = GridWeapon.where(collection_weapon_id: coll_weapon.id).pluck(:id)
|
||||||
|
orphaned_grid_item_ids.concat(grid_weapon_ids)
|
||||||
|
|
||||||
|
# The before_destroy callback on CollectionWeapon will mark grid items as orphaned
|
||||||
|
coll_weapon.destroy
|
||||||
|
deleted_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
deleted: deleted_count,
|
||||||
|
orphaned_grid_items: orphaned_grid_item_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Applies element and proficiency filters to a collection weapons scope.
|
||||||
|
# Used to scope deletion checks to only items matching the current game filter.
|
||||||
|
#
|
||||||
|
# @param scope [ActiveRecord::Relation] The collection weapons relation to filter
|
||||||
|
# @return [ActiveRecord::Relation] Filtered relation
|
||||||
|
def apply_filter_scope(scope)
|
||||||
|
return scope unless @filter.present?
|
||||||
|
|
||||||
|
# Element: check collection_weapon.element first (for element-changeable weapons),
|
||||||
|
# fall back to weapon.element if nil
|
||||||
|
if @filter[:elements].present? || @filter['elements'].present?
|
||||||
|
elements = @filter[:elements] || @filter['elements']
|
||||||
|
scope = scope.joins(:weapon).where(
|
||||||
|
'collection_weapons.element IN (?) OR (collection_weapons.element IS NULL AND weapons.element IN (?))',
|
||||||
|
elements, elements
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Proficiency: join through weapon
|
||||||
|
if @filter[:proficiencies].present? || @filter['proficiencies'].present?
|
||||||
|
proficiencies = @filter[:proficiencies] || @filter['proficiencies']
|
||||||
|
scope = scope.joins(:weapon).where(weapons: { proficiency: proficiencies })
|
||||||
|
end
|
||||||
|
|
||||||
|
scope
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ Rails.application.routes.draw do
|
||||||
get 'for_slot/:slot', action: :for_slot, as: :for_slot
|
get 'for_slot/:slot', action: :for_slot, as: :for_slot
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
resources :weapon_stat_modifiers, only: %i[index show]
|
||||||
|
|
||||||
# Grid artifacts
|
# Grid artifacts
|
||||||
resources :grid_artifacts, only: %i[create update destroy] do
|
resources :grid_artifacts, only: %i[create update destroy] do
|
||||||
|
|
@ -275,6 +276,7 @@ Rails.application.routes.draw do
|
||||||
post :batch
|
post :batch
|
||||||
delete :batch_destroy
|
delete :batch_destroy
|
||||||
post :import
|
post :import
|
||||||
|
post :preview_sync
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :summons, only: [:create, :update, :destroy], controller: '/api/v1/collection_summons' do
|
resources :summons, only: [:create, :update, :destroy], controller: '/api/v1/collection_summons' do
|
||||||
|
|
@ -282,6 +284,7 @@ Rails.application.routes.draw do
|
||||||
post :batch
|
post :batch
|
||||||
delete :batch_destroy
|
delete :batch_destroy
|
||||||
post :import
|
post :import
|
||||||
|
post :preview_sync
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :job_accessories, controller: '/api/v1/collection_job_accessories',
|
resources :job_accessories, controller: '/api/v1/collection_job_accessories',
|
||||||
|
|
@ -291,6 +294,7 @@ Rails.application.routes.draw do
|
||||||
post :batch
|
post :batch
|
||||||
delete :batch_destroy
|
delete :batch_destroy
|
||||||
post :import
|
post :import
|
||||||
|
post :preview_sync
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
55
db/data/20251230000001_seed_weapon_stat_modifiers.rb
Normal file
55
db/data/20251230000001_seed_weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SeedWeaponStatModifiers < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
ax_skills = [
|
||||||
|
# Primary AX Skills
|
||||||
|
{ slug: 'ax_hp', name_en: 'HP', name_jp: 'HP', category: 'ax', stat: 'hp', polarity: 1, suffix: '%', base_min: 1, base_max: 11, game_skill_id: 1588 },
|
||||||
|
{ slug: 'ax_atk', name_en: 'ATK', name_jp: '攻撃', category: 'ax', stat: 'atk', polarity: 1, suffix: '%', base_min: 1, base_max: 3.5, game_skill_id: 1589 },
|
||||||
|
{ slug: 'ax_def', name_en: 'DEF', name_jp: '防御', category: 'ax', stat: 'def', polarity: 1, suffix: '%', base_min: 1, base_max: 8, game_skill_id: 1590 },
|
||||||
|
{ slug: 'ax_ca_dmg', name_en: 'C.A. DMG', name_jp: '奥義ダメ', category: 'ax', stat: 'ca_dmg', polarity: 1, suffix: '%', base_min: 2, base_max: 8.5, game_skill_id: 1591 },
|
||||||
|
{ slug: 'ax_multiattack', name_en: 'Multiattack Rate', name_jp: '連撃率', category: 'ax', stat: 'multiattack', polarity: 1, suffix: '%', base_min: 1, base_max: 4, game_skill_id: 1592 },
|
||||||
|
|
||||||
|
# Secondary AX Skills
|
||||||
|
{ slug: 'ax_debuff_res', name_en: 'Debuff Resistance', name_jp: '弱体耐性', category: 'ax', stat: 'debuff_res', polarity: 1, suffix: '%', base_min: 1, base_max: 3, game_skill_id: 1593 },
|
||||||
|
{ slug: 'ax_ele_atk', name_en: 'Elemental ATK', name_jp: '全属性攻撃力', category: 'ax', stat: 'ele_atk', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1594 },
|
||||||
|
{ slug: 'ax_healing', name_en: 'Healing', name_jp: '回復性能', category: 'ax', stat: 'healing', polarity: 1, suffix: '%', base_min: 2, base_max: 5, game_skill_id: 1595 },
|
||||||
|
{ slug: 'ax_da', name_en: 'Double Attack Rate', name_jp: 'DA確率', category: 'ax', stat: 'da', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1596 },
|
||||||
|
{ slug: 'ax_ta', name_en: 'Triple Attack Rate', name_jp: 'TA確率', category: 'ax', stat: 'ta', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1597 },
|
||||||
|
{ slug: 'ax_ca_cap', name_en: 'C.A. DMG Cap', name_jp: '奥義上限', category: 'ax', stat: 'ca_cap', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1599 },
|
||||||
|
{ slug: 'ax_stamina', name_en: 'Stamina', name_jp: '渾身', category: 'ax', stat: 'stamina', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1600 },
|
||||||
|
{ slug: 'ax_enmity', name_en: 'Enmity', name_jp: '背水', category: 'ax', stat: 'enmity', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1601 },
|
||||||
|
|
||||||
|
# Extended AX Skills (axType 2)
|
||||||
|
{ slug: 'ax_skill_supp', name_en: 'Supplemental Skill DMG', name_jp: 'アビ与ダメ上昇', category: 'ax', stat: 'skill_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1719 },
|
||||||
|
{ slug: 'ax_ca_supp', name_en: 'Supplemental C.A. DMG', name_jp: '奥義与ダメ上昇', category: 'ax', stat: 'ca_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1720 },
|
||||||
|
{ slug: 'ax_ele_dmg_red', name_en: 'Elemental DMG Reduction', name_jp: '属性ダメ軽減', category: 'ax', stat: 'ele_dmg_red', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1721 },
|
||||||
|
{ slug: 'ax_na_cap', name_en: 'Normal ATK DMG Cap', name_jp: '通常ダメ上限', category: 'ax', stat: 'na_cap', polarity: 1, suffix: '%', base_min: 0.5, base_max: 1.5, game_skill_id: 1722 },
|
||||||
|
|
||||||
|
# Utility AX Skills (axType 3)
|
||||||
|
{ slug: 'ax_exp', name_en: 'EXP Gain', name_jp: 'EXP UP', category: 'ax', stat: 'exp', polarity: 1, suffix: '%', base_min: 5, base_max: 10, game_skill_id: 1837 },
|
||||||
|
{ slug: 'ax_rupie', name_en: 'Rupie Gain', name_jp: '獲得ルピ', category: 'ax', stat: 'rupie', polarity: 1, suffix: '%', base_min: 10, base_max: 20, game_skill_id: 1838 }
|
||||||
|
]
|
||||||
|
|
||||||
|
befoulments = [
|
||||||
|
# Befoulments - game_skill_ids from game data (2873-2881, 2876 doesn't exist)
|
||||||
|
{ slug: 'befoul_atk_down', name_en: 'ATK Down', name_jp: '攻撃力DOWN', category: 'befoulment', stat: 'atk', polarity: -1, suffix: '%', base_min: -12, base_max: -6, game_skill_id: 2873 },
|
||||||
|
{ slug: 'befoul_ability_dmg_down', name_en: 'Ability DMG Down', name_jp: 'アビリティダメージDOWN', category: 'befoulment', stat: 'ability_dmg', polarity: -1, suffix: '%', base_min: -50, base_max: -50, game_skill_id: 2874 },
|
||||||
|
{ slug: 'befoul_ca_dmg_down', name_en: 'CA DMG Down', name_jp: '奥義ダメージDOWN', category: 'befoulment', stat: 'ca_dmg', polarity: -1, suffix: '%', base_min: -38, base_max: -26, game_skill_id: 2875 },
|
||||||
|
{ slug: 'befoul_da_ta_down', name_en: 'DA/TA Down', name_jp: '連撃率DOWN', category: 'befoulment', stat: 'da_ta', polarity: -1, suffix: '%', base_min: -22, base_max: -19, game_skill_id: 2877 },
|
||||||
|
{ slug: 'befoul_debuff_down', name_en: 'Debuff Success Down', name_jp: '弱体成功率DOWN', category: 'befoulment', stat: 'debuff_success', polarity: -1, suffix: '%', base_min: -16, base_max: -6, game_skill_id: 2878 },
|
||||||
|
{ slug: 'befoul_hp_down', name_en: 'Max HP Down', name_jp: '最大HP減少', category: 'befoulment', stat: 'hp', polarity: -1, suffix: '%', base_min: -50, base_max: -26, game_skill_id: 2879 },
|
||||||
|
{ slug: 'befoul_def_down', name_en: 'DEF Down', name_jp: '防御力DOWN', category: 'befoulment', stat: 'def', polarity: -1, suffix: '%', base_min: -25, base_max: -21, game_skill_id: 2880 },
|
||||||
|
{ slug: 'befoul_dot', name_en: 'Damage Over Time', name_jp: '毎ターンダメージ', category: 'befoulment', stat: 'dot', polarity: -1, suffix: '%', base_min: 6, base_max: 16, game_skill_id: 2881 }
|
||||||
|
]
|
||||||
|
|
||||||
|
now = Time.current
|
||||||
|
(ax_skills + befoulments).each do |attrs|
|
||||||
|
WeaponStatModifier.create!(attrs.merge(created_at: now, updated_at: now))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
WeaponStatModifier.delete_all
|
||||||
|
end
|
||||||
|
end
|
||||||
115
db/data/20251230000002_migrate_ax_modifiers_to_fk.rb
Normal file
115
db/data/20251230000002_migrate_ax_modifiers_to_fk.rb
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MigrateAxModifiersToFk < ActiveRecord::Migration[8.0]
|
||||||
|
# Old AX_MAPPING from WeaponProcessor stored internal integer values (0, 1, 2...)
|
||||||
|
# We need to map: old internal_value => game_skill_id => weapon_stat_modifier.id
|
||||||
|
OLD_INTERNAL_TO_GAME_SKILL_ID = {
|
||||||
|
2 => 1588, # HP
|
||||||
|
0 => 1589, # ATK
|
||||||
|
1 => 1590, # DEF
|
||||||
|
3 => 1591, # C.A. DMG
|
||||||
|
4 => 1592, # Multiattack Rate
|
||||||
|
9 => 1593, # Debuff Resistance
|
||||||
|
13 => 1594, # Elemental ATK
|
||||||
|
10 => 1595, # Healing
|
||||||
|
5 => 1596, # Double Attack Rate
|
||||||
|
6 => 1597, # Triple Attack Rate
|
||||||
|
8 => 1599, # C.A. DMG Cap
|
||||||
|
12 => 1600, # Stamina
|
||||||
|
11 => 1601, # Enmity
|
||||||
|
15 => 1719, # Supplemental Skill DMG
|
||||||
|
16 => 1720, # Supplemental C.A. DMG
|
||||||
|
17 => 1721, # Elemental DMG Reduction
|
||||||
|
14 => 1722 # Normal ATK DMG Cap
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def up
|
||||||
|
# Build lookup: old_internal_value -> new FK id
|
||||||
|
modifier_by_game_skill_id = WeaponStatModifier.pluck(:game_skill_id, :id).to_h
|
||||||
|
old_to_new_id = OLD_INTERNAL_TO_GAME_SKILL_ID.transform_values do |game_skill_id|
|
||||||
|
modifier_by_game_skill_id[game_skill_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use raw SQL to query old integer columns (ax_modifier1, ax_modifier2)
|
||||||
|
# and update the new FK columns (ax_modifier1_id, ax_modifier2_id)
|
||||||
|
|
||||||
|
# Migrate CollectionWeapon
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE collection_weapons
|
||||||
|
SET ax_modifier1_id = CASE ax_modifier1
|
||||||
|
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier1 IS NOT NULL
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE collection_weapons
|
||||||
|
SET ax_modifier2_id = CASE ax_modifier2
|
||||||
|
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier2 IS NOT NULL
|
||||||
|
SQL
|
||||||
|
|
||||||
|
# Migrate GridWeapon
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE grid_weapons
|
||||||
|
SET ax_modifier1_id = CASE ax_modifier1
|
||||||
|
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier1 IS NOT NULL
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE grid_weapons
|
||||||
|
SET ax_modifier2_id = CASE ax_modifier2
|
||||||
|
#{old_to_new_id.map { |old_val, new_id| "WHEN #{old_val} THEN #{new_id}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier2 IS NOT NULL
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# Build reverse lookup: new FK id -> old internal value
|
||||||
|
modifier_by_game_skill_id = WeaponStatModifier.pluck(:game_skill_id, :id).to_h
|
||||||
|
game_skill_id_to_internal = OLD_INTERNAL_TO_GAME_SKILL_ID.invert
|
||||||
|
new_id_to_old = modifier_by_game_skill_id.each_with_object({}) do |(game_skill_id, new_id), hash|
|
||||||
|
old_val = game_skill_id_to_internal[game_skill_id]
|
||||||
|
hash[new_id] = old_val if old_val
|
||||||
|
end
|
||||||
|
|
||||||
|
return if new_id_to_old.empty?
|
||||||
|
|
||||||
|
# Reverse: copy FK back to old integer columns
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE collection_weapons
|
||||||
|
SET ax_modifier1 = CASE ax_modifier1_id
|
||||||
|
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier1_id IS NOT NULL
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE collection_weapons
|
||||||
|
SET ax_modifier2 = CASE ax_modifier2_id
|
||||||
|
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier2_id IS NOT NULL
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE grid_weapons
|
||||||
|
SET ax_modifier1 = CASE ax_modifier1_id
|
||||||
|
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier1_id IS NOT NULL
|
||||||
|
SQL
|
||||||
|
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE grid_weapons
|
||||||
|
SET ax_modifier2 = CASE ax_modifier2_id
|
||||||
|
#{new_id_to_old.map { |new_id, old_val| "WHEN #{new_id} THEN #{old_val}" }.join(' ')}
|
||||||
|
END
|
||||||
|
WHERE ax_modifier2_id IS NOT NULL
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
38
db/data/20251230000003_add_befoulment_game_skill_ids.rb
Normal file
38
db/data/20251230000003_add_befoulment_game_skill_ids.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddBefoulmentGameSkillIds < ActiveRecord::Migration[8.0]
|
||||||
|
# Befoulment game_skill_id mapping from game data:
|
||||||
|
# 2873: ex_skill_atk_down | ATK Lowered
|
||||||
|
# 2874: ex_skill_ab_atk_down | Skill DMG Lowered
|
||||||
|
# 2875: ex_skill_sp_atk_down | C.A. DMG Lowered
|
||||||
|
# 2876: (doesn't exist)
|
||||||
|
# 2877: ex_skill_ta_down | Multiattack Rate Lowered
|
||||||
|
# 2878: ex_skill_ailment_enhance_down | Debuff Success Rate Lowered
|
||||||
|
# 2879: ex_skill_hp_down | HP Cut
|
||||||
|
# 2880: ex_skill_def_down | Def Lowered (already mapped)
|
||||||
|
# 2881: ex_skill_turn_damage | Turn DMG
|
||||||
|
|
||||||
|
BEFOULMENT_GAME_SKILL_IDS = {
|
||||||
|
'befoul_atk_down' => 2873,
|
||||||
|
'befoul_ability_dmg_down' => 2874,
|
||||||
|
'befoul_ca_dmg_down' => 2875,
|
||||||
|
'befoul_da_ta_down' => 2877,
|
||||||
|
'befoul_debuff_down' => 2878,
|
||||||
|
'befoul_hp_down' => 2879,
|
||||||
|
'befoul_def_down' => 2880, # Already set, but include for completeness
|
||||||
|
'befoul_dot' => 2881
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def up
|
||||||
|
BEFOULMENT_GAME_SKILL_IDS.each do |slug, game_skill_id|
|
||||||
|
WeaponStatModifier.where(slug: slug).update_all(game_skill_id: game_skill_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# Clear game_skill_ids for befoulments (except def_down which was already set)
|
||||||
|
BEFOULMENT_GAME_SKILL_IDS.except('befoul_def_down').each_key do |slug|
|
||||||
|
WeaponStatModifier.where(slug: slug).update_all(game_skill_id: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1 +1 @@
|
||||||
DataMigrate::Data.define(version: 20251214193836)
|
DataMigrate::Data.define(version: 20251230000002)
|
||||||
|
|
|
||||||
13
db/migrate/20251223000000_add_orphaned_to_grid_items.rb
Normal file
13
db/migrate/20251223000000_add_orphaned_to_grid_items.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddOrphanedToGridItems < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :grid_weapons, :orphaned, :boolean, default: false, null: false
|
||||||
|
add_column :grid_summons, :orphaned, :boolean, default: false, null: false
|
||||||
|
add_column :grid_artifacts, :orphaned, :boolean, default: false, null: false
|
||||||
|
|
||||||
|
add_index :grid_weapons, :orphaned
|
||||||
|
add_index :grid_summons, :orphaned
|
||||||
|
add_index :grid_artifacts, :orphaned
|
||||||
|
end
|
||||||
|
end
|
||||||
24
db/migrate/20251230000001_create_weapon_stat_modifiers.rb
Normal file
24
db/migrate/20251230000001_create_weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateWeaponStatModifiers < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :weapon_stat_modifiers do |t|
|
||||||
|
t.string :slug, null: false
|
||||||
|
t.string :name_en, null: false
|
||||||
|
t.string :name_jp
|
||||||
|
t.string :category, null: false
|
||||||
|
t.string :stat
|
||||||
|
t.integer :polarity, default: 1, null: false
|
||||||
|
t.string :suffix
|
||||||
|
t.float :base_min
|
||||||
|
t.float :base_max
|
||||||
|
t.integer :game_skill_id
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :weapon_stat_modifiers, :slug, unique: true
|
||||||
|
add_index :weapon_stat_modifiers, :game_skill_id, unique: true
|
||||||
|
add_index :weapon_stat_modifiers, :category
|
||||||
|
end
|
||||||
|
end
|
||||||
36
db/migrate/20251230000002_add_weapon_stat_modifier_fks.rb
Normal file
36
db/migrate/20251230000002_add_weapon_stat_modifier_fks.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddWeaponStatModifierFks < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
# collection_weapons - add FK columns
|
||||||
|
# Note: old ax_modifier1/ax_modifier2 integer columns still exist for data migration
|
||||||
|
add_column :collection_weapons, :ax_modifier1_id, :bigint
|
||||||
|
add_column :collection_weapons, :ax_modifier2_id, :bigint
|
||||||
|
add_column :collection_weapons, :befoulment_modifier_id, :bigint
|
||||||
|
add_column :collection_weapons, :befoulment_strength, :float
|
||||||
|
add_column :collection_weapons, :exorcism_level, :integer, default: 0
|
||||||
|
|
||||||
|
add_index :collection_weapons, :ax_modifier1_id
|
||||||
|
add_index :collection_weapons, :ax_modifier2_id
|
||||||
|
add_index :collection_weapons, :befoulment_modifier_id
|
||||||
|
|
||||||
|
add_foreign_key :collection_weapons, :weapon_stat_modifiers, column: :ax_modifier1_id
|
||||||
|
add_foreign_key :collection_weapons, :weapon_stat_modifiers, column: :ax_modifier2_id
|
||||||
|
add_foreign_key :collection_weapons, :weapon_stat_modifiers, column: :befoulment_modifier_id
|
||||||
|
|
||||||
|
# grid_weapons - same pattern
|
||||||
|
add_column :grid_weapons, :ax_modifier1_id, :bigint
|
||||||
|
add_column :grid_weapons, :ax_modifier2_id, :bigint
|
||||||
|
add_column :grid_weapons, :befoulment_modifier_id, :bigint
|
||||||
|
add_column :grid_weapons, :befoulment_strength, :float
|
||||||
|
add_column :grid_weapons, :exorcism_level, :integer, default: 0
|
||||||
|
|
||||||
|
add_index :grid_weapons, :ax_modifier1_id
|
||||||
|
add_index :grid_weapons, :ax_modifier2_id
|
||||||
|
add_index :grid_weapons, :befoulment_modifier_id
|
||||||
|
|
||||||
|
add_foreign_key :grid_weapons, :weapon_stat_modifiers, column: :ax_modifier1_id
|
||||||
|
add_foreign_key :grid_weapons, :weapon_stat_modifiers, column: :ax_modifier2_id
|
||||||
|
add_foreign_key :grid_weapons, :weapon_stat_modifiers, column: :befoulment_modifier_id
|
||||||
|
end
|
||||||
|
end
|
||||||
11
db/migrate/20251230000003_finalize_ax_modifier_columns.rb
Normal file
11
db/migrate/20251230000003_finalize_ax_modifier_columns.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FinalizeAxModifierColumns < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
# Remove old integer columns (data has been migrated to ax_modifier1_id/ax_modifier2_id FKs)
|
||||||
|
remove_column :collection_weapons, :ax_modifier1, :integer
|
||||||
|
remove_column :collection_weapons, :ax_modifier2, :integer
|
||||||
|
remove_column :grid_weapons, :ax_modifier1, :integer
|
||||||
|
remove_column :grid_weapons, :ax_modifier2, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddAugmentTypeToWeaponSeries < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
add_column :weapon_series, :augment_type, :integer, default: 0, null: false
|
||||||
|
|
||||||
|
# Migrate existing has_ax_skills: true to augment_type: 1 (ax)
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE weapon_series
|
||||||
|
SET augment_type = 1
|
||||||
|
WHERE has_ax_skills = true
|
||||||
|
SQL
|
||||||
|
|
||||||
|
remove_column :weapon_series, :has_ax_skills
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_column :weapon_series, :has_ax_skills, :boolean, default: false, null: false
|
||||||
|
|
||||||
|
# Migrate augment_type: 1 (ax) back to has_ax_skills: true
|
||||||
|
execute <<-SQL.squish
|
||||||
|
UPDATE weapon_series
|
||||||
|
SET has_ax_skills = true
|
||||||
|
WHERE augment_type = 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
remove_column :weapon_series, :augment_type
|
||||||
|
end
|
||||||
|
end
|
||||||
54
db/schema.rb
54
db/schema.rb
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
@ -235,15 +235,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_21_210000) do
|
||||||
t.uuid "weapon_key4_id"
|
t.uuid "weapon_key4_id"
|
||||||
t.uuid "awakening_id"
|
t.uuid "awakening_id"
|
||||||
t.integer "awakening_level", default: 1, null: false
|
t.integer "awakening_level", default: 1, null: false
|
||||||
t.integer "ax_modifier1"
|
|
||||||
t.float "ax_strength1"
|
t.float "ax_strength1"
|
||||||
t.integer "ax_modifier2"
|
|
||||||
t.float "ax_strength2"
|
t.float "ax_strength2"
|
||||||
t.integer "element"
|
t.integer "element"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "game_id"
|
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 ["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", "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", "weapon_id"], name: "index_collection_weapons_on_user_id_and_weapon_id"
|
||||||
t.index ["user_id"], name: "index_collection_weapons_on_user_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.datetime "updated_at", null: false
|
||||||
t.integer "reroll_slot"
|
t.integer "reroll_slot"
|
||||||
t.uuid "collection_artifact_id"
|
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 ["artifact_id"], name: "index_grid_artifacts_on_artifact_id"
|
||||||
t.index ["collection_artifact_id"], name: "index_grid_artifacts_on_collection_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 ["grid_character_id"], name: "index_grid_artifacts_on_grid_character_id", unique: true
|
||||||
|
t.index ["orphaned"], name: "index_grid_artifacts_on_orphaned"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "grid_characters", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
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.integer "transcendence_step", default: 0, null: false
|
||||||
t.boolean "quick_summon", default: false
|
t.boolean "quick_summon", default: false
|
||||||
t.uuid "collection_summon_id"
|
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 ["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", "position"], name: "index_grid_summons_on_party_id_and_position"
|
||||||
t.index ["party_id"], name: "index_grid_summons_on_party_id"
|
t.index ["party_id"], name: "index_grid_summons_on_party_id"
|
||||||
t.index ["summon_id"], name: "index_grid_summons_on_summon_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 "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.uuid "weapon_key3_id"
|
t.uuid "weapon_key3_id"
|
||||||
t.integer "ax_modifier1"
|
|
||||||
t.float "ax_strength1"
|
t.float "ax_strength1"
|
||||||
t.integer "ax_modifier2"
|
|
||||||
t.float "ax_strength2"
|
t.float "ax_strength2"
|
||||||
t.integer "element"
|
t.integer "element"
|
||||||
t.integer "awakening_level", default: 1, null: false
|
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.integer "transcendence_step", default: 0
|
||||||
t.string "weapon_key4_id"
|
t.string "weapon_key4_id"
|
||||||
t.uuid "collection_weapon_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 ["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 ["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", "position"], name: "index_grid_weapons_on_party_id_and_position"
|
||||||
t.index ["party_id"], name: "index_grid_weapons_on_party_id"
|
t.index ["party_id"], name: "index_grid_weapons_on_party_id"
|
||||||
t.index ["weapon_id"], name: "index_grid_weapons_on_weapon_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 "element_changeable", default: false, null: false
|
||||||
t.boolean "has_weapon_keys", default: false, null: false
|
t.boolean "has_weapon_keys", default: false, null: false
|
||||||
t.boolean "has_awakening", 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 ["order"], name: "index_weapon_series_on_order"
|
||||||
t.index ["slug"], name: "index_weapon_series_on_slug", unique: true
|
t.index ["slug"], name: "index_weapon_series_on_slug", unique: true
|
||||||
end
|
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"
|
t.index ["weapon_granblue_id"], name: "index_weapon_skills_on_weapon_granblue_id"
|
||||||
end
|
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|
|
create_table "weapons", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.string "name_en"
|
t.string "name_en"
|
||||||
t.string "name_jp"
|
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_key2_id"
|
||||||
add_foreign_key "collection_weapons", "weapon_keys", column: "weapon_key3_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_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 "collection_weapons", "weapons"
|
||||||
add_foreign_key "crew_gw_participations", "crews"
|
add_foreign_key "crew_gw_participations", "crews"
|
||||||
add_foreign_key "crew_gw_participations", "gw_events"
|
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", "collection_weapons"
|
||||||
add_foreign_key "grid_weapons", "parties"
|
add_foreign_key "grid_weapons", "parties"
|
||||||
add_foreign_key "grid_weapons", "weapon_keys", column: "weapon_key3_id"
|
add_foreign_key "grid_weapons", "weapon_keys", column: "weapon_key3_id"
|
||||||
|
add_foreign_key "grid_weapons", "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 "grid_weapons", "weapons"
|
||||||
add_foreign_key "gw_crew_scores", "crew_gw_participations"
|
add_foreign_key "gw_crew_scores", "crew_gw_participations"
|
||||||
add_foreign_key "gw_individual_scores", "crew_gw_participations"
|
add_foreign_key "gw_individual_scores", "crew_gw_participations"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
id,slug,name_en,name_jp,order,extra,element_changeable,has_weapon_keys,has_awakening,has_ax_skills
|
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,false
|
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,false
|
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,false
|
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,false
|
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,false
|
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,false
|
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,false
|
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,false
|
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,false
|
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,false
|
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,false
|
00000000-0001-0000-0000-000000000011,event,Event Weapons,イベント武器,99,false,false,false,false,none
|
||||||
|
|
|
||||||
|
56
db/seeds/weapon_stat_modifiers.rb
Normal file
56
db/seeds/weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
##
|
||||||
|
# Seeds for WeaponStatModifier - AX skills and befoulments
|
||||||
|
#
|
||||||
|
|
||||||
|
puts 'Seeding weapon stat modifiers...'
|
||||||
|
|
||||||
|
ax_skills = [
|
||||||
|
# Primary AX Skills
|
||||||
|
{ slug: 'ax_hp', name_en: 'HP', name_jp: 'HP', category: 'ax', stat: 'hp', polarity: 1, suffix: '%', base_min: 1, base_max: 11, game_skill_id: 1588 },
|
||||||
|
{ slug: 'ax_atk', name_en: 'ATK', name_jp: '攻撃', category: 'ax', stat: 'atk', polarity: 1, suffix: '%', base_min: 1, base_max: 3.5, game_skill_id: 1589 },
|
||||||
|
{ slug: 'ax_def', name_en: 'DEF', name_jp: '防御', category: 'ax', stat: 'def', polarity: 1, suffix: '%', base_min: 1, base_max: 8, game_skill_id: 1590 },
|
||||||
|
{ slug: 'ax_ca_dmg', name_en: 'C.A. DMG', name_jp: '奥義ダメ', category: 'ax', stat: 'ca_dmg', polarity: 1, suffix: '%', base_min: 2, base_max: 8.5, game_skill_id: 1591 },
|
||||||
|
{ slug: 'ax_multiattack', name_en: 'Multiattack Rate', name_jp: '連撃率', category: 'ax', stat: 'multiattack', polarity: 1, suffix: '%', base_min: 1, base_max: 4, game_skill_id: 1592 },
|
||||||
|
|
||||||
|
# Secondary AX Skills
|
||||||
|
{ slug: 'ax_debuff_res', name_en: 'Debuff Resistance', name_jp: '弱体耐性', category: 'ax', stat: 'debuff_res', polarity: 1, suffix: '%', base_min: 1, base_max: 3, game_skill_id: 1593 },
|
||||||
|
{ slug: 'ax_ele_atk', name_en: 'Elemental ATK', name_jp: '全属性攻撃力', category: 'ax', stat: 'ele_atk', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1594 },
|
||||||
|
{ slug: 'ax_healing', name_en: 'Healing', name_jp: '回復性能', category: 'ax', stat: 'healing', polarity: 1, suffix: '%', base_min: 2, base_max: 5, game_skill_id: 1595 },
|
||||||
|
{ slug: 'ax_da', name_en: 'Double Attack Rate', name_jp: 'DA確率', category: 'ax', stat: 'da', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1596 },
|
||||||
|
{ slug: 'ax_ta', name_en: 'Triple Attack Rate', name_jp: 'TA確率', category: 'ax', stat: 'ta', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1597 },
|
||||||
|
{ slug: 'ax_ca_cap', name_en: 'C.A. DMG Cap', name_jp: '奥義上限', category: 'ax', stat: 'ca_cap', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1599 },
|
||||||
|
{ slug: 'ax_stamina', name_en: 'Stamina', name_jp: '渾身', category: 'ax', stat: 'stamina', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1600 },
|
||||||
|
{ slug: 'ax_enmity', name_en: 'Enmity', name_jp: '背水', category: 'ax', stat: 'enmity', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1601 },
|
||||||
|
|
||||||
|
# Extended AX Skills (axType 2)
|
||||||
|
{ slug: 'ax_skill_supp', name_en: 'Supplemental Skill DMG', name_jp: 'アビ与ダメ上昇', category: 'ax', stat: 'skill_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1719 },
|
||||||
|
{ slug: 'ax_ca_supp', name_en: 'Supplemental C.A. DMG', name_jp: '奥義与ダメ上昇', category: 'ax', stat: 'ca_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1720 },
|
||||||
|
{ slug: 'ax_ele_dmg_red', name_en: 'Elemental DMG Reduction', name_jp: '属性ダメ軽減', category: 'ax', stat: 'ele_dmg_red', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1721 },
|
||||||
|
{ slug: 'ax_na_cap', name_en: 'Normal ATK DMG Cap', name_jp: '通常ダメ上限', category: 'ax', stat: 'na_cap', polarity: 1, suffix: '%', base_min: 0.5, base_max: 1.5, game_skill_id: 1722 },
|
||||||
|
|
||||||
|
# Utility AX Skills (axType 3)
|
||||||
|
{ slug: 'ax_exp', name_en: 'EXP Gain', name_jp: 'EXP UP', category: 'ax', stat: 'exp', polarity: 1, suffix: '%', base_min: 5, base_max: 10, game_skill_id: 1837 },
|
||||||
|
{ slug: 'ax_rupie', name_en: 'Rupie Gain', name_jp: '獲得ルピ', category: 'ax', stat: 'rupie', polarity: 1, suffix: '%', base_min: 10, base_max: 20, game_skill_id: 1838 }
|
||||||
|
]
|
||||||
|
|
||||||
|
befoulments = [
|
||||||
|
# Befoulments - game_skill_ids from game data (2873-2881, 2876 doesn't exist)
|
||||||
|
{ slug: 'befoul_atk_down', name_en: 'ATK Down', name_jp: '攻撃力DOWN', category: 'befoulment', stat: 'atk', polarity: -1, suffix: '%', base_min: -12, base_max: -6, game_skill_id: 2873 },
|
||||||
|
{ slug: 'befoul_ability_dmg_down', name_en: 'Ability DMG Down', name_jp: 'アビリティダメージDOWN', category: 'befoulment', stat: 'ability_dmg', polarity: -1, suffix: '%', base_min: -50, base_max: -50, game_skill_id: 2874 },
|
||||||
|
{ slug: 'befoul_ca_dmg_down', name_en: 'CA DMG Down', name_jp: '奥義ダメージDOWN', category: 'befoulment', stat: 'ca_dmg', polarity: -1, suffix: '%', base_min: -38, base_max: -26, game_skill_id: 2875 },
|
||||||
|
{ slug: 'befoul_da_ta_down', name_en: 'DA/TA Down', name_jp: '連撃率DOWN', category: 'befoulment', stat: 'da_ta', polarity: -1, suffix: '%', base_min: -22, base_max: -19, game_skill_id: 2877 },
|
||||||
|
{ slug: 'befoul_debuff_down', name_en: 'Debuff Success Down', name_jp: '弱体成功率DOWN', category: 'befoulment', stat: 'debuff_success', polarity: -1, suffix: '%', base_min: -16, base_max: -6, game_skill_id: 2878 },
|
||||||
|
{ slug: 'befoul_hp_down', name_en: 'Max HP Down', name_jp: '最大HP減少', category: 'befoulment', stat: 'hp', polarity: -1, suffix: '%', base_min: -50, base_max: -26, game_skill_id: 2879 },
|
||||||
|
{ slug: 'befoul_def_down', name_en: 'DEF Down', name_jp: '防御力DOWN', category: 'befoulment', stat: 'def', polarity: -1, suffix: '%', base_min: -25, base_max: -21, game_skill_id: 2880 },
|
||||||
|
{ slug: 'befoul_dot', name_en: 'Damage Over Time', name_jp: '毎ターンダメージ', category: 'befoulment', stat: 'dot', polarity: -1, suffix: '%', base_min: 6, base_max: 16, game_skill_id: 2881 }
|
||||||
|
]
|
||||||
|
|
||||||
|
(ax_skills + befoulments).each do |attrs|
|
||||||
|
modifier = WeaponStatModifier.find_or_initialize_by(slug: attrs[:slug])
|
||||||
|
modifier.assign_attributes(attrs)
|
||||||
|
modifier.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Created/updated #{WeaponStatModifier.count} weapon stat modifiers"
|
||||||
671
docs/planning/befoulments-feature-plan.md
Normal file
671
docs/planning/befoulments-feature-plan.md
Normal file
|
|
@ -0,0 +1,671 @@
|
||||||
|
# Befoulments & WeaponStatModifier Consolidation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement Befoulments (魔蝕) and consolidate with AX skills using a new `WeaponStatModifier` table.
|
||||||
|
|
||||||
|
### Terminology
|
||||||
|
- **Odiant (禁禍武器)** = A weapon series that has befoulments instead of AX skills
|
||||||
|
- **Befoulments (魔蝕)** = Negative stat augments that appear on Odiant weapons
|
||||||
|
- **Exorcism Level (退魔Lv)** = Level 0-5 that reduces befoulment strength
|
||||||
|
|
||||||
|
**Key insight:** `augment_skill_info` contains:
|
||||||
|
- **AX skills** for regular weapons (`is_odiant_weapon: false`)
|
||||||
|
- **Befoulments** for Odiant weapons (`is_odiant_weapon: true`)
|
||||||
|
|
||||||
|
They are mutually exclusive - a weapon has either AX skills OR befoulments, never both.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### 1. New Table: `weapon_stat_modifiers`
|
||||||
|
|
||||||
|
Centralized definition of all weapon stat modifiers (AX skills + befoulments):
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
create_table :weapon_stat_modifiers do |t|
|
||||||
|
t.string :slug, null: false, index: { unique: true } # 'ax_atk', 'befoul_def_down'
|
||||||
|
t.string :name_en, null: false
|
||||||
|
t.string :name_jp
|
||||||
|
t.string :category, null: false # 'ax' or 'befoulment'
|
||||||
|
t.string :stat # 'atk', 'def', 'da_ta', 'ca_dmg', 'hp', 'dot', etc.
|
||||||
|
t.integer :polarity, default: 1 # 1 = buff, -1 = debuff
|
||||||
|
t.string :suffix # '%' for percentages, nil for flat values
|
||||||
|
t.float :base_min # Known min initial value (informational)
|
||||||
|
t.float :base_max # Known max initial value (informational)
|
||||||
|
t.integer :game_skill_id # Maps to game's skill_id for import
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Icon convention:** Frontend derives icon path from `slug`:
|
||||||
|
- `ax_atk` → `weapon-stat-modifier/ax_atk.png`
|
||||||
|
- `befoul_def_down` → `weapon-stat-modifier/befoul_def_down.png`
|
||||||
|
|
||||||
|
Rename game icons (e.g., `ex_skill_def_down` → `befoul_def_down`) to match slugs.
|
||||||
|
|
||||||
|
### 2. Refactor AX & Add Befoulment Columns (Foreign Keys)
|
||||||
|
|
||||||
|
Replace raw integer `ax_modifier1/2` with foreign keys, and add befoulment FK:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# collection_weapons
|
||||||
|
# Remove old integer columns
|
||||||
|
remove_column :collection_weapons, :ax_modifier1, :integer
|
||||||
|
remove_column :collection_weapons, :ax_modifier2, :integer
|
||||||
|
|
||||||
|
# Add FK columns referencing weapon_stat_modifiers
|
||||||
|
add_reference :collection_weapons, :ax_modifier1, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||||
|
add_reference :collection_weapons, :ax_modifier2, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||||
|
add_reference :collection_weapons, :befoulment_modifier, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||||
|
add_column :collection_weapons, :befoulment_strength, :float
|
||||||
|
add_column :collection_weapons, :exorcism_level, :integer, default: 0
|
||||||
|
|
||||||
|
# grid_weapons - same pattern
|
||||||
|
add_reference :grid_weapons, :ax_modifier1, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||||
|
add_reference :grid_weapons, :ax_modifier2, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||||
|
add_reference :grid_weapons, :befoulment_modifier, foreign_key: { to_table: :weapon_stat_modifiers }
|
||||||
|
add_column :grid_weapons, :befoulment_strength, :float
|
||||||
|
add_column :grid_weapons, :exorcism_level, :integer, default: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2b. Data Migration for Existing AX Skills
|
||||||
|
|
||||||
|
Migrate existing `ax_modifier1/2` integer values to FK references:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Run after weapon_stat_modifiers table is seeded
|
||||||
|
CollectionWeapon.where.not(ax_modifier1: nil).find_each do |cw|
|
||||||
|
modifier = WeaponStatModifier.find_by(game_skill_id: cw.ax_modifier1)
|
||||||
|
cw.update_columns(ax_modifier1_id: modifier&.id) if modifier
|
||||||
|
end
|
||||||
|
|
||||||
|
CollectionWeapon.where.not(ax_modifier2: nil).find_each do |cw|
|
||||||
|
modifier = WeaponStatModifier.find_by(game_skill_id: cw.ax_modifier2)
|
||||||
|
cw.update_columns(ax_modifier2_id: modifier&.id) if modifier
|
||||||
|
end
|
||||||
|
|
||||||
|
# Same for GridWeapon
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Strength values (`ax_strength1/2`) are preserved as-is since they store the actual value.
|
||||||
|
|
||||||
|
### 3. WeaponSeries `augment_type` Enum
|
||||||
|
|
||||||
|
Replace `has_ax_skills` boolean with an enum that enforces mutual exclusivity:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Migration
|
||||||
|
remove_column :weapon_series, :has_ax_skills, :boolean
|
||||||
|
add_column :weapon_series, :augment_type, :integer, default: 0
|
||||||
|
|
||||||
|
# Data migration
|
||||||
|
WeaponSeries.where(has_ax_skills: true).update_all(augment_type: 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Model
|
||||||
|
enum :augment_type, { none: 0, ax: 1, befoulment: 2 }, default: :none
|
||||||
|
|
||||||
|
scope :with_ax_skills, -> { where(augment_type: :ax) }
|
||||||
|
scope :with_befoulments, -> { where(augment_type: :befoulment) }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seed Data
|
||||||
|
|
||||||
|
### AX Skill Modifiers (Complete List)
|
||||||
|
|
||||||
|
All AX skills from game data with their game_skill_id values:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
WeaponStatModifier.create!([
|
||||||
|
# Primary AX Skills
|
||||||
|
{ slug: 'ax_atk', name_en: 'ATK', name_jp: '攻撃', category: 'ax', stat: 'atk', polarity: 1, suffix: '%', base_min: 1, base_max: 3.5, game_skill_id: 1589 },
|
||||||
|
{ slug: 'ax_def', name_en: 'DEF', name_jp: '防御', category: 'ax', stat: 'def', polarity: 1, suffix: '%', base_min: 1, base_max: 8, game_skill_id: 1590 },
|
||||||
|
{ slug: 'ax_hp', name_en: 'HP', name_jp: 'HP', category: 'ax', stat: 'hp', polarity: 1, suffix: '%', base_min: 1, base_max: 11, game_skill_id: 1588 },
|
||||||
|
{ slug: 'ax_ca_dmg', name_en: 'C.A. DMG', name_jp: '奥義ダメ', category: 'ax', stat: 'ca_dmg', polarity: 1, suffix: '%', base_min: 2, base_max: 8.5, game_skill_id: 1591 },
|
||||||
|
{ slug: 'ax_multiattack', name_en: 'Multiattack Rate', name_jp: '連撃率', category: 'ax', stat: 'multiattack', polarity: 1, suffix: '%', base_min: 1, base_max: 4, game_skill_id: 1592 },
|
||||||
|
|
||||||
|
# Secondary AX Skills
|
||||||
|
{ slug: 'ax_debuff_res', name_en: 'Debuff Resistance', name_jp: '弱体耐性', category: 'ax', stat: 'debuff_res', polarity: 1, suffix: '%', base_min: 1, base_max: 3, game_skill_id: 1593 },
|
||||||
|
{ slug: 'ax_ele_atk', name_en: 'Elemental ATK', name_jp: '全属性攻撃力', category: 'ax', stat: 'ele_atk', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1594 },
|
||||||
|
{ slug: 'ax_healing', name_en: 'Healing', name_jp: '回復性能', category: 'ax', stat: 'healing', polarity: 1, suffix: '%', base_min: 2, base_max: 5, game_skill_id: 1595 },
|
||||||
|
{ slug: 'ax_da', name_en: 'Double Attack Rate', name_jp: 'DA確率', category: 'ax', stat: 'da', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1596 },
|
||||||
|
{ slug: 'ax_ta', name_en: 'Triple Attack Rate', name_jp: 'TA確率', category: 'ax', stat: 'ta', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1597 },
|
||||||
|
{ slug: 'ax_ca_cap', name_en: 'C.A. DMG Cap', name_jp: '奥義上限', category: 'ax', stat: 'ca_cap', polarity: 1, suffix: '%', base_min: 1, base_max: 2, game_skill_id: 1599 },
|
||||||
|
{ slug: 'ax_stamina', name_en: 'Stamina', name_jp: '渾身', category: 'ax', stat: 'stamina', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1600 },
|
||||||
|
{ slug: 'ax_enmity', name_en: 'Enmity', name_jp: '背水', category: 'ax', stat: 'enmity', polarity: 1, suffix: nil, base_min: 1, base_max: 3, game_skill_id: 1601 },
|
||||||
|
|
||||||
|
# Extended AX Skills (axType 2)
|
||||||
|
{ slug: 'ax_skill_supp', name_en: 'Supplemental Skill DMG', name_jp: 'アビ与ダメ上昇', category: 'ax', stat: 'skill_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1719 },
|
||||||
|
{ slug: 'ax_ca_supp', name_en: 'Supplemental C.A. DMG', name_jp: '奥義与ダメ上昇', category: 'ax', stat: 'ca_supp', polarity: 1, suffix: nil, base_min: 1, base_max: 5, game_skill_id: 1720 },
|
||||||
|
{ slug: 'ax_ele_dmg_red', name_en: 'Elemental DMG Reduction', name_jp: '属性ダメ軽減', category: 'ax', stat: 'ele_dmg_red', polarity: 1, suffix: '%', base_min: 1, base_max: 5, game_skill_id: 1721 },
|
||||||
|
{ slug: 'ax_na_cap', name_en: 'Normal ATK DMG Cap', name_jp: '通常ダメ上限', category: 'ax', stat: 'na_cap', polarity: 1, suffix: '%', base_min: 0.5, base_max: 1.5, game_skill_id: 1722 },
|
||||||
|
|
||||||
|
# Utility AX Skills (axType 3)
|
||||||
|
{ slug: 'ax_exp', name_en: 'EXP Gain', name_jp: 'EXP UP', category: 'ax', stat: 'exp', polarity: 1, suffix: '%', base_min: 5, base_max: 10, game_skill_id: 1837 },
|
||||||
|
{ slug: 'ax_rupie', name_en: 'Rupie Gain', name_jp: '獲得ルピ', category: 'ax', stat: 'rupie', polarity: 1, suffix: '%', base_min: 10, base_max: 20, game_skill_id: 1838 },
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Befoulment Modifiers
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# game_skill_id values will be populated as we discover them - we know 2880 = DEF Down
|
||||||
|
WeaponStatModifier.create!([
|
||||||
|
{ slug: 'befoul_atk_down', name_en: 'ATK Down', name_jp: '攻撃力DOWN', category: 'befoulment', stat: 'atk', polarity: -1, suffix: '%', base_min: -12, base_max: -6 },
|
||||||
|
{ slug: 'befoul_def_down', name_en: 'DEF Down', name_jp: '防御力DOWN', category: 'befoulment', stat: 'def', polarity: -1, suffix: '%', base_min: -25, base_max: -21, game_skill_id: 2880 },
|
||||||
|
{ slug: 'befoul_da_ta_down', name_en: 'DA/TA Down', name_jp: '連撃率DOWN', category: 'befoulment', stat: 'da_ta', polarity: -1, suffix: '%', base_min: -22, base_max: -19 },
|
||||||
|
{ slug: 'befoul_ca_dmg_down', name_en: 'CA DMG Down', name_jp: '奥義ダメージDOWN', category: 'befoulment', stat: 'ca_dmg', polarity: -1, suffix: '%', base_min: -38, base_max: -26 },
|
||||||
|
{ slug: 'befoul_dot', name_en: 'Damage Over Time', name_jp: '毎ターンダメージ', category: 'befoulment', stat: 'dot', polarity: -1, suffix: '%', base_min: 6, base_max: 16 },
|
||||||
|
{ slug: 'befoul_hp_down', name_en: 'Max HP Down', name_jp: '最大HP減少', category: 'befoulment', stat: 'hp', polarity: -1, suffix: '%', base_min: -50, base_max: -26 },
|
||||||
|
{ slug: 'befoul_debuff_down', name_en: 'Debuff Success Down', name_jp: '弱体成功率DOWN', category: 'befoulment', stat: 'debuff_success', polarity: -1, suffix: '%', base_min: -16, base_max: -6 },
|
||||||
|
{ slug: 'befoul_ability_dmg_down', name_en: 'Ability DMG Down', name_jp: 'アビリティダメージDOWN', category: 'befoulment', stat: 'ability_dmg', polarity: -1, suffix: '%', base_min: -50, base_max: -50 },
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model Changes
|
||||||
|
|
||||||
|
### WeaponStatModifier Model
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/models/weapon_stat_modifier.rb
|
||||||
|
class WeaponStatModifier < ApplicationRecord
|
||||||
|
validates :slug, presence: true, uniqueness: true
|
||||||
|
validates :name_en, presence: true
|
||||||
|
validates :category, presence: true, inclusion: { in: %w[ax befoulment] }
|
||||||
|
validates :polarity, inclusion: { in: [-1, 1] }
|
||||||
|
|
||||||
|
scope :ax_skills, -> { where(category: 'ax') }
|
||||||
|
scope :befoulments, -> { where(category: 'befoulment') }
|
||||||
|
|
||||||
|
def self.find_by_game_skill_id(id)
|
||||||
|
find_by(game_skill_id: id.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### CollectionWeapon Changes
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/models/collection_weapon.rb
|
||||||
|
|
||||||
|
# Replace old integer references with proper associations
|
||||||
|
belongs_to :ax_modifier1, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
|
||||||
|
validates :exorcism_level, numericality: {
|
||||||
|
only_integer: true,
|
||||||
|
greater_than_or_equal_to: 0,
|
||||||
|
less_than_or_equal_to: 5
|
||||||
|
}, allow_nil: true
|
||||||
|
|
||||||
|
validate :validate_ax_skills
|
||||||
|
validate :validate_befoulment_fields
|
||||||
|
|
||||||
|
def validate_ax_skills
|
||||||
|
# AX skill 1: must have both modifier and strength
|
||||||
|
if (ax_modifier1.present? && ax_strength1.blank?) ||
|
||||||
|
(ax_modifier1.blank? && ax_strength1.present?)
|
||||||
|
errors.add(:base, "AX skill 1 must have both modifier and strength")
|
||||||
|
end
|
||||||
|
|
||||||
|
# AX skill 2: must have both modifier and strength
|
||||||
|
if (ax_modifier2.present? && ax_strength2.blank?) ||
|
||||||
|
(ax_modifier2.blank? && ax_strength2.present?)
|
||||||
|
errors.add(:base, "AX skill 2 must have both modifier and strength")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate category is 'ax'
|
||||||
|
if ax_modifier1.present? && ax_modifier1.category != 'ax'
|
||||||
|
errors.add(:ax_modifier1, "must be an AX skill modifier")
|
||||||
|
end
|
||||||
|
if ax_modifier2.present? && ax_modifier2.category != 'ax'
|
||||||
|
errors.add(:ax_modifier2, "must be an AX skill modifier")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_befoulment_fields
|
||||||
|
if (befoulment_modifier.present? && befoulment_strength.blank?) ||
|
||||||
|
(befoulment_modifier.blank? && befoulment_strength.present?)
|
||||||
|
errors.add(:base, "Befoulment must have both modifier and strength")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate category is 'befoulment'
|
||||||
|
if befoulment_modifier.present? && befoulment_modifier.category != 'befoulment'
|
||||||
|
errors.add(:befoulment_modifier, "must be a befoulment modifier")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### GridWeapon Changes
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/models/grid_weapon.rb
|
||||||
|
|
||||||
|
# Same associations as CollectionWeapon
|
||||||
|
belongs_to :ax_modifier1, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
|
||||||
|
# Same validations as CollectionWeapon
|
||||||
|
```
|
||||||
|
|
||||||
|
- Update `sync_from_collection!` to include befoulment fields
|
||||||
|
- Update `out_of_sync?` to check befoulment fields
|
||||||
|
- Update Amoeba config to nullify befoulment fields on copy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Changes
|
||||||
|
|
||||||
|
### Blueprint Serialization
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# collection_weapon_blueprint.rb
|
||||||
|
|
||||||
|
# AX skills - now with full modifier object
|
||||||
|
field :ax, if: ->(_, obj, _) { obj.ax_modifier1.present? } do |obj|
|
||||||
|
skills = []
|
||||||
|
if obj.ax_modifier1.present?
|
||||||
|
skills << {
|
||||||
|
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.ax_modifier1),
|
||||||
|
strength: obj.ax_strength1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if obj.ax_modifier2.present?
|
||||||
|
skills << {
|
||||||
|
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.ax_modifier2),
|
||||||
|
strength: obj.ax_strength2
|
||||||
|
}
|
||||||
|
end
|
||||||
|
skills
|
||||||
|
end
|
||||||
|
|
||||||
|
# Befoulment - with full modifier object
|
||||||
|
field :befoulment, if: ->(_, obj, _) { obj.befoulment_modifier.present? } do |obj|
|
||||||
|
{
|
||||||
|
modifier: WeaponStatModifierBlueprint.render_as_hash(obj.befoulment_modifier),
|
||||||
|
strength: obj.befoulment_strength,
|
||||||
|
exorcism_level: obj.exorcism_level
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# grid_weapon_blueprint.rb - similar
|
||||||
|
```
|
||||||
|
|
||||||
|
### WeaponStatModifierBlueprint
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb
|
||||||
|
class Api::V1::WeaponStatModifierBlueprint < Blueprinter::Base
|
||||||
|
identifier :id
|
||||||
|
fields :slug, :name_en, :name_jp, :category, :stat, :polarity, :suffix
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Endpoint: GET /weapon_stat_modifiers
|
||||||
|
|
||||||
|
Return all weapon stat modifiers for frontend reference:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/controllers/api/v1/weapon_stat_modifiers_controller.rb
|
||||||
|
def index
|
||||||
|
@modifiers = WeaponStatModifier.all
|
||||||
|
render json: WeaponStatModifierBlueprint.render(@modifiers, root: :weapon_stat_modifiers)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller Params
|
||||||
|
|
||||||
|
Update `weapon_params` in both controllers:
|
||||||
|
```ruby
|
||||||
|
# Replace :ax_modifier1, :ax_modifier2 with FK versions
|
||||||
|
:ax_modifier1_id, :ax_strength1, :ax_modifier2_id, :ax_strength2,
|
||||||
|
:befoulment_modifier_id, :befoulment_strength, :exorcism_level
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Import Service
|
||||||
|
|
||||||
|
### Game JSON Structure
|
||||||
|
|
||||||
|
**Odiant weapon (with befoulment):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"augment_skill_info": [[{ "skill_id": 2880, "effect_value": "25", "show_value": "-25%" }]],
|
||||||
|
"odiant": {
|
||||||
|
"is_odiant_weapon": true,
|
||||||
|
"exorcision_level": 1,
|
||||||
|
"max_exorcision_level": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regular weapon (with AX skills):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"augment_skill_info": [[
|
||||||
|
{ "skill_id": 1589, "effect_value": "3", "show_value": "+3%" },
|
||||||
|
{ "skill_id": 1719, "effect_value": "1_2000", "show_value": "+3" }
|
||||||
|
]],
|
||||||
|
"odiant": {
|
||||||
|
"is_odiant_weapon": false,
|
||||||
|
"exorcision_level": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key insight:** Same `augment_skill_info` structure, differentiated by `is_odiant_weapon`:
|
||||||
|
- `true` → parse as befoulment (skill_id 2880+)
|
||||||
|
- `false` → parse as AX skills (skill_id 1588-1722)
|
||||||
|
|
||||||
|
### WeaponImportService Updates
|
||||||
|
|
||||||
|
Add a cache for WeaponStatModifier lookups:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def initialize(user, game_data, options = {})
|
||||||
|
# ... existing code ...
|
||||||
|
@modifier_cache = {} # Cache for WeaponStatModifier lookups
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_collection_weapon_attrs(item, weapon)
|
||||||
|
param = item['param'] || {}
|
||||||
|
|
||||||
|
attrs = {
|
||||||
|
weapon: weapon,
|
||||||
|
game_id: param['id'].to_s,
|
||||||
|
# ... existing attrs ...
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if this is an Odiant (befoulment) weapon
|
||||||
|
odiant = param['odiant']
|
||||||
|
if odiant && odiant['is_odiant_weapon'] == true
|
||||||
|
# Parse befoulment from augment_skill_info
|
||||||
|
befoulment = parse_befoulment(param['augment_skill_info'])
|
||||||
|
if befoulment
|
||||||
|
attrs[:befoulment_modifier_id] = befoulment[:modifier_id]
|
||||||
|
attrs[:befoulment_strength] = befoulment[:strength]
|
||||||
|
end
|
||||||
|
attrs[:exorcism_level] = odiant['exorcision_level'].to_i
|
||||||
|
else
|
||||||
|
# Regular weapon - parse AX skills
|
||||||
|
ax_attrs = parse_ax_skills(param['augment_skill_info'])
|
||||||
|
attrs.merge!(ax_attrs) if ax_attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
# ... rest of existing code (awakening, etc) ...
|
||||||
|
attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
# Updated to return FK id instead of raw game_skill_id
|
||||||
|
def parse_ax_skills(augment_skill_info)
|
||||||
|
return nil if augment_skill_info.blank? || !augment_skill_info.is_a?(Array)
|
||||||
|
|
||||||
|
skills = augment_skill_info.first
|
||||||
|
return nil if skills.blank? || !skills.is_a?(Array)
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
# First AX skill
|
||||||
|
if skills[0].is_a?(Hash)
|
||||||
|
ax1 = parse_single_ax_skill(skills[0])
|
||||||
|
if ax1
|
||||||
|
attrs[:ax_modifier1_id] = ax1[:modifier_id]
|
||||||
|
attrs[:ax_strength1] = ax1[:strength]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Second AX skill
|
||||||
|
if skills[1].is_a?(Hash)
|
||||||
|
ax2 = parse_single_ax_skill(skills[1])
|
||||||
|
if ax2
|
||||||
|
attrs[:ax_modifier2_id] = ax2[:modifier_id]
|
||||||
|
attrs[:ax_strength2] = ax2[:strength]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs.empty? ? nil : attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_single_ax_skill(skill)
|
||||||
|
return nil unless skill['skill_id'].present?
|
||||||
|
|
||||||
|
game_skill_id = skill['skill_id'].to_i
|
||||||
|
modifier = find_modifier_by_game_skill_id(game_skill_id)
|
||||||
|
return nil unless modifier
|
||||||
|
|
||||||
|
strength = parse_ax_strength(skill['effect_value'], skill['show_value'])
|
||||||
|
return nil unless strength
|
||||||
|
|
||||||
|
{ modifier_id: modifier.id, strength: strength }
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_befoulment(augment_skill_info)
|
||||||
|
return nil if augment_skill_info.blank? || !augment_skill_info.is_a?(Array)
|
||||||
|
|
||||||
|
skills = augment_skill_info.first
|
||||||
|
return nil if skills.blank? || !skills.is_a?(Array)
|
||||||
|
|
||||||
|
skill = skills.first
|
||||||
|
return nil unless skill.is_a?(Hash) && skill['skill_id'].present?
|
||||||
|
|
||||||
|
game_skill_id = skill['skill_id'].to_i
|
||||||
|
modifier = find_modifier_by_game_skill_id(game_skill_id)
|
||||||
|
return nil unless modifier
|
||||||
|
|
||||||
|
{
|
||||||
|
modifier_id: modifier.id,
|
||||||
|
strength: parse_befoulment_strength(skill['effect_value'], skill['show_value'])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_modifier_by_game_skill_id(game_skill_id)
|
||||||
|
@modifier_cache[game_skill_id] ||= WeaponStatModifier.find_by(game_skill_id: game_skill_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_befoulment_strength(effect_value, show_value)
|
||||||
|
# show_value has the sign: "-25%"
|
||||||
|
# effect_value is unsigned: "25"
|
||||||
|
if show_value.present?
|
||||||
|
show_value.to_s.gsub('%', '').to_f
|
||||||
|
elsif effect_value.present?
|
||||||
|
-effect_value.to_f
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Unknown `game_skill_id` values will be skipped (modifier not found). This is acceptable - we can add new modifiers to the seed data as we discover them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
| File | Action |
|
||||||
|
|------|--------|
|
||||||
|
| **Migrations** | |
|
||||||
|
| `db/migrate/xxx_create_weapon_stat_modifiers.rb` | Create - new reference table |
|
||||||
|
| `db/migrate/xxx_refactor_ax_and_add_befoulments.rb` | Create - replace ax_modifier integers with FKs, add befoulment FKs |
|
||||||
|
| `db/migrate/xxx_replace_has_ax_skills_with_augment_type.rb` | Create - enum migration with data migration |
|
||||||
|
| `db/data/xxx_migrate_ax_modifiers_to_fk.rb` | Create - data migration to convert existing ax_modifier values to FK references |
|
||||||
|
| **Models** | |
|
||||||
|
| `app/models/weapon_stat_modifier.rb` | Create |
|
||||||
|
| `app/models/collection_weapon.rb` | Modify - add befoulment validation |
|
||||||
|
| `app/models/grid_weapon.rb` | Modify - add befoulment fields, sync |
|
||||||
|
| `app/models/weapon_series.rb` | Modify - replace has_ax_skills with augment_type enum |
|
||||||
|
| **Blueprints** | |
|
||||||
|
| `app/blueprints/api/v1/weapon_stat_modifier_blueprint.rb` | Create |
|
||||||
|
| `app/blueprints/api/v1/collection_weapon_blueprint.rb` | Modify - add befoulment serialization |
|
||||||
|
| `app/blueprints/api/v1/grid_weapon_blueprint.rb` | Modify - add befoulment serialization |
|
||||||
|
| `app/blueprints/api/v1/weapon_series_blueprint.rb` | Modify - replace has_ax_skills with augment_type |
|
||||||
|
| `app/blueprints/api/v1/weapon_blueprint.rb` | Modify - update series.has_ax_skills reference |
|
||||||
|
| **Controllers** | |
|
||||||
|
| `app/controllers/api/v1/weapon_stat_modifiers_controller.rb` | Create |
|
||||||
|
| `app/controllers/api/v1/collection_weapons_controller.rb` | Modify - permit befoulment params |
|
||||||
|
| `app/controllers/api/v1/grid_weapons_controller.rb` | Modify - permit befoulment params |
|
||||||
|
| `app/controllers/api/v1/weapon_series_controller.rb` | Modify - permit augment_type instead of has_ax_skills |
|
||||||
|
| **Services & Seeds** | |
|
||||||
|
| `app/services/weapon_import_service.rb` | Modify - parse befoulments |
|
||||||
|
| `db/seeds/weapon_stat_modifiers.rb` | Create |
|
||||||
|
| **Config & Tests** | |
|
||||||
|
| `config/routes.rb` | Modify - add weapon_stat_modifiers route |
|
||||||
|
| `spec/factories/weapon_series.rb` | Modify - replace has_ax_skills with augment_type |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Breaking Changes
|
||||||
|
|
||||||
|
### 1. WeaponSeries: `has_ax_skills` → `augment_type`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```json
|
||||||
|
{ "has_ax_skills": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```json
|
||||||
|
{ "augment_type": "ax" } // or "befoulment" or "none"
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend: `augment_type === 'ax'` instead of `has_ax_skills === true`.
|
||||||
|
|
||||||
|
### 2. Collection/GridWeapon: AX skill structure changed
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ax_modifier1": 1589,
|
||||||
|
"ax_strength1": 3.0,
|
||||||
|
"ax_modifier2": 1719,
|
||||||
|
"ax_strength2": 2000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ax": [
|
||||||
|
{
|
||||||
|
"modifier": { "id": 1, "slug": "ax_atk", "name_en": "ATK Up", "category": "ax", ... },
|
||||||
|
"strength": 3.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modifier": { "id": 5, "slug": "ax_ability_dmg", "name_en": "Ability DMG Up", "category": "ax", ... },
|
||||||
|
"strength": 2000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend: Access via `weapon.ax[0].modifier.slug` instead of `weapon.ax_modifier1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Considerations
|
||||||
|
|
||||||
|
### Migration Order
|
||||||
|
|
||||||
|
Migrations must be deployed in this order:
|
||||||
|
|
||||||
|
1. `create_weapon_stat_modifiers` - Create table + seed data
|
||||||
|
2. `add_ax_and_befoulment_fk_columns` - Add new FK columns (keep old integer columns)
|
||||||
|
3. `migrate_ax_modifiers_to_fk` - Data migration: lookup existing values → FK refs
|
||||||
|
4. `remove_old_ax_modifier_columns` - Remove old integer columns
|
||||||
|
5. `replace_has_ax_skills_with_augment_type` - WeaponSeries enum change
|
||||||
|
|
||||||
|
### GridWeapon Amoeba Config
|
||||||
|
|
||||||
|
Update nullify list for party cloning:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
amoeba do
|
||||||
|
nullify :ax_modifier1_id
|
||||||
|
nullify :ax_modifier2_id
|
||||||
|
nullify :ax_strength1
|
||||||
|
nullify :ax_strength2
|
||||||
|
nullify :befoulment_modifier_id
|
||||||
|
nullify :befoulment_strength
|
||||||
|
nullify :exorcism_level
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unknown Skill ID Logging
|
||||||
|
|
||||||
|
When import encounters unknown `game_skill_id`, log with icon image for discovery:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Rails.logger.warn(
|
||||||
|
"[WeaponImportService] Unknown augment skill_id=#{game_skill_id} " \
|
||||||
|
"icon=#{skill['augment_skill_icon_image']}"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Indexes
|
||||||
|
|
||||||
|
Add index on `weapon_stat_modifiers.game_skill_id` for fast import lookups:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
add_index :weapon_stat_modifiers, :game_skill_id, unique: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coordinated Release
|
||||||
|
|
||||||
|
Frontend and backend releases will be coordinated. No backwards compatibility layer needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ **Game JSON samples** - received for both Odiant and regular weapons
|
||||||
|
2. **Identify Odiant weapon series** in database (set `augment_type: :befoulment`)
|
||||||
|
3. **Discover remaining game_skill_ids** for other befoulment types (we know 2880 = DEF Down)
|
||||||
|
4. **Verify AX skill IDs** match between AX_MAPPING and game data
|
||||||
|
|
||||||
|
## Known Skill IDs
|
||||||
|
|
||||||
|
### AX Skills (Complete)
|
||||||
|
|
||||||
|
| skill_id | Stat | Name (EN) | Name (JP) | Suffix |
|
||||||
|
|----------|------|-----------|-----------|--------|
|
||||||
|
| 1588 | hp | HP | HP | % |
|
||||||
|
| 1589 | atk | ATK | 攻撃 | % |
|
||||||
|
| 1590 | def | DEF | 防御 | % |
|
||||||
|
| 1591 | ca_dmg | C.A. DMG | 奥義ダメ | % |
|
||||||
|
| 1592 | multiattack | Multiattack Rate | 連撃率 | % |
|
||||||
|
| 1593 | debuff_res | Debuff Resistance | 弱体耐性 | % |
|
||||||
|
| 1594 | ele_atk | Elemental ATK | 全属性攻撃力 | % |
|
||||||
|
| 1595 | healing | Healing | 回復性能 | % |
|
||||||
|
| 1596 | da | Double Attack Rate | DA確率 | % |
|
||||||
|
| 1597 | ta | Triple Attack Rate | TA確率 | % |
|
||||||
|
| 1599 | ca_cap | C.A. DMG Cap | 奥義上限 | % |
|
||||||
|
| 1600 | stamina | Stamina | 渾身 | - |
|
||||||
|
| 1601 | enmity | Enmity | 背水 | - |
|
||||||
|
| 1719 | skill_supp | Supplemental Skill DMG | アビ与ダメ上昇 | - |
|
||||||
|
| 1720 | ca_supp | Supplemental C.A. DMG | 奥義与ダメ上昇 | - |
|
||||||
|
| 1721 | ele_dmg_red | Elemental DMG Reduction | 属性ダメ軽減 | % |
|
||||||
|
| 1722 | na_cap | Normal ATK DMG Cap | 通常ダメ上限 | % |
|
||||||
|
| 1837 | exp | EXP Gain | EXP UP | % |
|
||||||
|
| 1838 | rupie | Rupie Gain | 獲得ルピ | % |
|
||||||
|
|
||||||
|
### Befoulments (Partial - more to discover)
|
||||||
|
|
||||||
|
| skill_id | Stat | Name (EN) | Name (JP) |
|
||||||
|
|----------|------|-----------|-----------|
|
||||||
|
| 2880 | def | DEF Down | 防御力DOWN |
|
||||||
|
| ? | atk | ATK Down | 攻撃力DOWN |
|
||||||
|
| ? | da_ta | DA/TA Down | 連撃率DOWN |
|
||||||
|
| ? | ca_dmg | CA DMG Down | 奥義ダメージDOWN |
|
||||||
|
| ? | dot | Damage Over Time | 毎ターンダメージ |
|
||||||
|
| ? | hp | Max HP Down | 最大HP減少 |
|
||||||
|
| ? | debuff_success | Debuff Success Down | 弱体成功率DOWN |
|
||||||
|
| ? | ability_dmg | Ability DMG Down | アビリティダメージDOWN |
|
||||||
|
|
||||||
|
(Befoulment skill_ids will be discovered as users import Odiant weapons)
|
||||||
|
|
@ -8,12 +8,17 @@ FactoryBot.define do
|
||||||
awakening_level { 1 }
|
awakening_level { 1 }
|
||||||
element { nil } # Only used for element-changeable weapons
|
element { nil } # Only used for element-changeable weapons
|
||||||
|
|
||||||
# AX skills
|
# AX skills (FK to weapon_stat_modifiers)
|
||||||
ax_modifier1 { nil }
|
ax_modifier1 { nil }
|
||||||
ax_strength1 { nil }
|
ax_strength1 { nil }
|
||||||
ax_modifier2 { nil }
|
ax_modifier2 { nil }
|
||||||
ax_strength2 { nil }
|
ax_strength2 { nil }
|
||||||
|
|
||||||
|
# Befoulment (FK to weapon_stat_modifiers)
|
||||||
|
befoulment_modifier { nil }
|
||||||
|
befoulment_strength { nil }
|
||||||
|
exorcism_level { 0 }
|
||||||
|
|
||||||
# Weapon keys
|
# Weapon keys
|
||||||
weapon_key1 { nil }
|
weapon_key1 { nil }
|
||||||
weapon_key2 { nil }
|
weapon_key2 { nil }
|
||||||
|
|
@ -75,10 +80,24 @@ FactoryBot.define do
|
||||||
|
|
||||||
# Trait for AX weapon with skills
|
# Trait for AX weapon with skills
|
||||||
trait :with_ax do
|
trait :with_ax do
|
||||||
ax_modifier1 { 1 } # Attack modifier
|
|
||||||
ax_strength1 { 3.5 }
|
ax_strength1 { 3.5 }
|
||||||
ax_modifier2 { 2 } # HP modifier
|
|
||||||
ax_strength2 { 10.0 }
|
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
|
end
|
||||||
|
|
||||||
# Trait for element-changed weapon (Revans weapons)
|
# Trait for element-changed weapon (Revans weapons)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,39 @@ FactoryBot.define do
|
||||||
transcendence_step { 0 }
|
transcendence_step { 0 }
|
||||||
mainhand { false }
|
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.
|
# 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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ FactoryBot.define do
|
||||||
element_changeable { false }
|
element_changeable { false }
|
||||||
has_weapon_keys { false }
|
has_weapon_keys { false }
|
||||||
has_awakening { false }
|
has_awakening { false }
|
||||||
has_ax_skills { false }
|
augment_type { :none }
|
||||||
|
|
||||||
trait :gacha do
|
trait :gacha do
|
||||||
slug { 'gacha' }
|
slug { 'gacha' }
|
||||||
|
|
@ -96,5 +96,21 @@ FactoryBot.define do
|
||||||
trait :with_weapon_keys do
|
trait :with_weapon_keys do
|
||||||
has_weapon_keys { true }
|
has_weapon_keys { true }
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
47
spec/factories/weapon_stat_modifiers.rb
Normal file
47
spec/factories/weapon_stat_modifiers.rb
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :weapon_stat_modifier do
|
||||||
|
sequence(:slug) { |n| "ax-modifier-#{n}" }
|
||||||
|
sequence(:name_en) { |n| "AX Modifier #{n}" }
|
||||||
|
category { 'ax' }
|
||||||
|
polarity { 1 }
|
||||||
|
|
||||||
|
trait :ax_atk do
|
||||||
|
slug { 'ax_atk' }
|
||||||
|
name_en { 'ATK' }
|
||||||
|
category { 'ax' }
|
||||||
|
stat { 'atk' }
|
||||||
|
polarity { 1 }
|
||||||
|
suffix { '%' }
|
||||||
|
game_skill_id { 1589 }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :ax_hp do
|
||||||
|
slug { 'ax_hp' }
|
||||||
|
name_en { 'HP' }
|
||||||
|
category { 'ax' }
|
||||||
|
stat { 'hp' }
|
||||||
|
polarity { 1 }
|
||||||
|
suffix { '%' }
|
||||||
|
game_skill_id { 1588 }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :befoulment do
|
||||||
|
sequence(:slug) { |n| "befoul-modifier-#{n}" }
|
||||||
|
sequence(:name_en) { |n| "Befoulment #{n}" }
|
||||||
|
category { 'befoulment' }
|
||||||
|
polarity { -1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :befoul_def_down do
|
||||||
|
slug { 'befoul_def_down' }
|
||||||
|
name_en { 'DEF Down' }
|
||||||
|
category { 'befoulment' }
|
||||||
|
stat { 'def' }
|
||||||
|
polarity { -1 }
|
||||||
|
suffix { '%' }
|
||||||
|
game_skill_id { 2880 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -76,9 +76,14 @@ RSpec.describe CollectionWeapon, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'AX skill validations' do
|
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
|
context 'when AX skill has only modifier' do
|
||||||
it 'is invalid' 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).not_to be_valid
|
||||||
expect(collection_weapon.errors[:base]).to include('AX skill 1 must have both modifier and strength')
|
expect(collection_weapon.errors[:base]).to include('AX skill 1 must have both modifier and strength')
|
||||||
end
|
end
|
||||||
|
|
@ -94,7 +99,7 @@ RSpec.describe CollectionWeapon, type: :model do
|
||||||
|
|
||||||
context 'when AX skill has both modifier and strength' do
|
context 'when AX skill has both modifier and strength' do
|
||||||
it 'is valid' 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
|
expect(collection_weapon).to be_valid
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -215,9 +220,11 @@ RSpec.describe CollectionWeapon, type: :model do
|
||||||
ax_weapon = create(:collection_weapon, :with_ax)
|
ax_weapon = create(:collection_weapon, :with_ax)
|
||||||
|
|
||||||
aggregate_failures do
|
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_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)
|
expect(ax_weapon.ax_strength2).to eq(10.0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -162,11 +162,16 @@ RSpec.describe 'Collection Weapons API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates weapon with AX skills' do
|
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(
|
ax_attributes = valid_attributes.deep_merge(
|
||||||
collection_weapon: {
|
collection_weapon: {
|
||||||
ax_modifier1: 1,
|
ax_modifier1_id: ax_atk.id,
|
||||||
ax_strength1: 3.5,
|
ax_strength1: 3.5,
|
||||||
ax_modifier2: 2,
|
ax_modifier2_id: ax_hp.id,
|
||||||
ax_strength2: 2.0
|
ax_strength2: 2.0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -176,14 +181,17 @@ RSpec.describe 'Collection Weapons API', type: :request do
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
expect(json['ax']).to be_present
|
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)
|
expect(json['ax'].first['strength']).to eq(3.5)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns error with incomplete AX skills' do
|
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(
|
invalid_ax = valid_attributes.deep_merge(
|
||||||
collection_weapon: {
|
collection_weapon: {
|
||||||
ax_modifier1: 1
|
ax_modifier1_id: ax_atk.id
|
||||||
# Missing ax_strength1
|
# Missing ax_strength1
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,8 @@ RSpec.describe 'GridWeapons API', type: :request do
|
||||||
weapon_key1_id: nil,
|
weapon_key1_id: nil,
|
||||||
weapon_key2_id: nil,
|
weapon_key2_id: nil,
|
||||||
weapon_key3_id: nil,
|
weapon_key3_id: nil,
|
||||||
ax_modifier1: nil,
|
ax_modifier1_id: nil,
|
||||||
ax_modifier2: nil,
|
ax_modifier2_id: nil,
|
||||||
ax_strength1: nil,
|
ax_strength1: nil,
|
||||||
ax_strength2: nil,
|
ax_strength2: nil,
|
||||||
awakening_id: nil,
|
awakening_id: nil,
|
||||||
|
|
@ -197,8 +197,8 @@ RSpec.describe 'GridWeapons API', type: :request do
|
||||||
weapon_key1_id: nil,
|
weapon_key1_id: nil,
|
||||||
weapon_key2_id: nil,
|
weapon_key2_id: nil,
|
||||||
weapon_key3_id: nil,
|
weapon_key3_id: nil,
|
||||||
ax_modifier1: nil,
|
ax_modifier1_id: nil,
|
||||||
ax_modifier2: nil,
|
ax_modifier2_id: nil,
|
||||||
ax_strength1: nil,
|
ax_strength1: nil,
|
||||||
ax_strength2: nil,
|
ax_strength2: nil,
|
||||||
awakening_id: nil,
|
awakening_id: nil,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,25 @@ RSpec.describe Processors::WeaponProcessor, type: :model do
|
||||||
|
|
||||||
describe '#process_weapon_ax' do
|
describe '#process_weapon_ax' do
|
||||||
let(:grid_weapon) { build(:grid_weapon, party: party) }
|
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 = [
|
ax_skill_info = [
|
||||||
[
|
[
|
||||||
{ 'skill_id' => '1588', 'effect_value' => '3', 'show_value' => '3%' },
|
{ '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)
|
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_modifier1).to eq(ax_hp_modifier)
|
||||||
expect(grid_weapon.ax_strength1).to eq(3)
|
expect(grid_weapon.ax_strength1).to eq(3.0)
|
||||||
expect(grid_weapon.ax_modifier2).to eq(3) # from 1591 → 3
|
expect(grid_weapon.ax_modifier2).to eq(ax_ca_dmg_modifier)
|
||||||
expect(grid_weapon.ax_strength2).to eq(5)
|
expect(grid_weapon.ax_strength2).to eq(5.0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,29 @@ RSpec.describe WeaponImportService, type: :service do
|
||||||
create(:awakening, :for_weapon, slug: 'weapon-heal', name_en: 'Healing')
|
create(:awakening, :for_weapon, slug: 'weapon-heal', name_en: 'Healing')
|
||||||
end
|
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
|
before do
|
||||||
standard_weapon
|
standard_weapon
|
||||||
transcendable_weapon
|
transcendable_weapon
|
||||||
|
|
@ -279,12 +302,12 @@ RSpec.describe WeaponImportService, type: :service do
|
||||||
'augment_skill_info' => [
|
'augment_skill_info' => [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'skill_id' => 1,
|
'skill_id' => 1589, # ATK modifier
|
||||||
'effect_value' => '7',
|
'effect_value' => '7',
|
||||||
'show_value' => '7%'
|
'show_value' => '7%'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'skill_id' => 2,
|
'skill_id' => 1588, # HP modifier
|
||||||
'effect_value' => '2_4',
|
'effect_value' => '2_4',
|
||||||
'show_value' => '4%'
|
'show_value' => '4%'
|
||||||
}
|
}
|
||||||
|
|
@ -302,7 +325,7 @@ RSpec.describe WeaponImportService, type: :service do
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
||||||
weapon = result.created.first
|
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)
|
expect(weapon.ax_strength1).to eq(7.0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -311,7 +334,7 @@ RSpec.describe WeaponImportService, type: :service do
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
||||||
weapon = result.created.first
|
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)
|
expect(weapon.ax_strength2).to eq(4.0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -329,7 +352,7 @@ RSpec.describe WeaponImportService, type: :service do
|
||||||
'augment_skill_info' => [
|
'augment_skill_info' => [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'skill_id' => 3,
|
'skill_id' => 1591, # C.A. DMG modifier
|
||||||
'effect_value' => nil,
|
'effect_value' => nil,
|
||||||
'show_value' => '5.5%'
|
'show_value' => '5.5%'
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +370,7 @@ RSpec.describe WeaponImportService, type: :service do
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
||||||
weapon = result.created.first
|
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)
|
expect(weapon.ax_strength1).to eq(5.5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue