From 8c05cd838c6173821645b841ca87b2c78e1aaf5e Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 16 Sep 2025 03:33:02 -0700 Subject: [PATCH] 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 --- app/controllers/api/v1/parties_controller.rb | 170 ++++++++++++++++++- 1 file changed, 167 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/parties_controller.rb b/app/controllers/api/v1/parties_controller.rb index 1fce768..02b938a 100644 --- a/app/controllers/api/v1/parties_controller.rb +++ b/app/controllers/api/v1/parties_controller.rb @@ -32,9 +32,9 @@ module Api # Default maximum clear time in seconds DEFAULT_MAX_CLEAR_TIME = 5400 - before_action :set_from_slug, except: %w[create destroy update index favorites] - before_action :set, only: %w[update destroy] - before_action :authorize_party!, only: %w[update destroy] + before_action :set_from_slug, except: %w[create destroy update index favorites grid_update] + before_action :set, only: %w[update destroy grid_update] + before_action :authorize_party!, only: %w[update destroy grid_update] # Primary CRUD Actions @@ -103,6 +103,44 @@ module Api 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. def index 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] ) 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