Add batch grid update endpoint
- POST /parties/:id/grid_update for atomic multi-operations - Support move, swap, and remove operations - Validate all operations before executing - Use transaction for atomicity - Optional character sequence maintenance
This commit is contained in:
parent
311e8254d0
commit
8c05cd838c
1 changed files with 167 additions and 3 deletions
|
|
@ -32,9 +32,9 @@ module Api
|
||||||
# Default maximum clear time in seconds
|
# Default maximum clear time in seconds
|
||||||
DEFAULT_MAX_CLEAR_TIME = 5400
|
DEFAULT_MAX_CLEAR_TIME = 5400
|
||||||
|
|
||||||
before_action :set_from_slug, except: %w[create destroy update index favorites]
|
before_action :set_from_slug, except: %w[create destroy update index favorites grid_update]
|
||||||
before_action :set, only: %w[update destroy]
|
before_action :set, only: %w[update destroy grid_update]
|
||||||
before_action :authorize_party!, only: %w[update destroy]
|
before_action :authorize_party!, only: %w[update destroy grid_update]
|
||||||
|
|
||||||
# Primary CRUD Actions
|
# Primary CRUD Actions
|
||||||
|
|
||||||
|
|
@ -103,6 +103,44 @@ module Api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Batch updates grid items (weapons, characters, summons) atomically.
|
||||||
|
def grid_update
|
||||||
|
operations = grid_update_params[:operations]
|
||||||
|
options = grid_update_params[:options] || {}
|
||||||
|
|
||||||
|
# Validate all operations first
|
||||||
|
validation_errors = validate_grid_operations(operations)
|
||||||
|
if validation_errors.any?
|
||||||
|
return render_unprocessable_entity_response(
|
||||||
|
Api::V1::GranblueError.new("Validation failed: #{validation_errors.join(', ')}")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
operations.each do |operation|
|
||||||
|
change = apply_grid_operation(operation)
|
||||||
|
changes << change if change
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compact character positions if needed
|
||||||
|
if options[:maintain_character_sequence]
|
||||||
|
compact_party_character_positions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
party: PartyBlueprint.render_as_hash(@party.reload, view: :full),
|
||||||
|
operations_applied: changes.count,
|
||||||
|
changes: changes
|
||||||
|
}, status: :ok
|
||||||
|
rescue StandardError => e
|
||||||
|
render_unprocessable_entity_response(
|
||||||
|
Api::V1::GranblueError.new("Grid update failed: #{e.message}")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# Lists parties based on query parameters.
|
# Lists parties based on query parameters.
|
||||||
def index
|
def index
|
||||||
query = build_filtered_query(build_common_base_query)
|
query = build_filtered_query(build_common_base_query)
|
||||||
|
|
@ -195,6 +233,132 @@ module Api
|
||||||
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 ax_modifier2 ax_strength1 ax_strength2 awakening_id awakening_level]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Permits parameters for grid update operation.
|
||||||
|
def grid_update_params
|
||||||
|
params.permit(
|
||||||
|
operations: [:type, :entity, :id, :source_id, :target_id, :position, :container],
|
||||||
|
options: [:maintain_character_sequence, :validate_before_execute]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validates grid operations before executing.
|
||||||
|
def validate_grid_operations(operations)
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
operations.each_with_index do |op, index|
|
||||||
|
case op[:type]
|
||||||
|
when 'move'
|
||||||
|
errors << "Operation #{index}: missing id" unless op[:id].present?
|
||||||
|
errors << "Operation #{index}: missing position" unless op[:position].present?
|
||||||
|
when 'swap'
|
||||||
|
errors << "Operation #{index}: missing source_id" unless op[:source_id].present?
|
||||||
|
errors << "Operation #{index}: missing target_id" unless op[:target_id].present?
|
||||||
|
when 'remove'
|
||||||
|
errors << "Operation #{index}: missing id" unless op[:id].present?
|
||||||
|
else
|
||||||
|
errors << "Operation #{index}: unknown operation type #{op[:type]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless %w[weapon character summon].include?(op[:entity])
|
||||||
|
errors << "Operation #{index}: invalid entity type #{op[:entity]}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
errors
|
||||||
|
end
|
||||||
|
|
||||||
|
# Applies a single grid operation.
|
||||||
|
def apply_grid_operation(operation)
|
||||||
|
case operation[:type]
|
||||||
|
when 'move'
|
||||||
|
apply_move_operation(operation)
|
||||||
|
when 'swap'
|
||||||
|
apply_swap_operation(operation)
|
||||||
|
when 'remove'
|
||||||
|
apply_remove_operation(operation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Applies a move operation.
|
||||||
|
def apply_move_operation(operation)
|
||||||
|
model_class = grid_model_for_entity(operation[:entity])
|
||||||
|
item = model_class.find_by(id: operation[:id], party_id: @party.id)
|
||||||
|
|
||||||
|
return nil unless item
|
||||||
|
|
||||||
|
old_position = item.position
|
||||||
|
item.update!(position: operation[:position])
|
||||||
|
|
||||||
|
{
|
||||||
|
entity: operation[:entity],
|
||||||
|
id: operation[:id],
|
||||||
|
action: 'moved',
|
||||||
|
from: old_position,
|
||||||
|
to: operation[:position]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Applies a swap operation.
|
||||||
|
def apply_swap_operation(operation)
|
||||||
|
model_class = grid_model_for_entity(operation[:entity])
|
||||||
|
source = model_class.find_by(id: operation[:source_id], party_id: @party.id)
|
||||||
|
target = model_class.find_by(id: operation[:target_id], party_id: @party.id)
|
||||||
|
|
||||||
|
return nil unless source && target
|
||||||
|
|
||||||
|
source_pos = source.position
|
||||||
|
target_pos = target.position
|
||||||
|
|
||||||
|
# Use a temporary position to avoid conflicts
|
||||||
|
source.update!(position: -999)
|
||||||
|
target.update!(position: source_pos)
|
||||||
|
source.update!(position: target_pos)
|
||||||
|
|
||||||
|
{
|
||||||
|
entity: operation[:entity],
|
||||||
|
id: operation[:source_id],
|
||||||
|
action: 'swapped',
|
||||||
|
with: operation[:target_id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Applies a remove operation.
|
||||||
|
def apply_remove_operation(operation)
|
||||||
|
model_class = grid_model_for_entity(operation[:entity])
|
||||||
|
item = model_class.find_by(id: operation[:id], party_id: @party.id)
|
||||||
|
|
||||||
|
return nil unless item
|
||||||
|
|
||||||
|
item.destroy
|
||||||
|
|
||||||
|
{
|
||||||
|
entity: operation[:entity],
|
||||||
|
id: operation[:id],
|
||||||
|
action: 'removed'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the model class for a given entity type.
|
||||||
|
def grid_model_for_entity(entity)
|
||||||
|
case entity
|
||||||
|
when 'weapon'
|
||||||
|
GridWeapon
|
||||||
|
when 'character'
|
||||||
|
GridCharacter
|
||||||
|
when 'summon'
|
||||||
|
GridSummon
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compacts character positions to maintain sequential filling.
|
||||||
|
def compact_party_character_positions
|
||||||
|
main_characters = @party.characters.where(position: 0..4).order(:position)
|
||||||
|
|
||||||
|
main_characters.each_with_index do |char, index|
|
||||||
|
char.update!(position: index) if char.position != index
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue