Add summon position update and swap endpoints

- PUT /parties/:party_id/grid_summons/:id/position
- POST /parties/:party_id/grid_summons/swap
- Restrict main and friend summon positions
- Validate sub/subaura slot transitions
This commit is contained in:
Justin Edmund 2025-09-16 03:32:45 -07:00
parent 5ed8a68ab6
commit 311e8254d0

View file

@ -10,12 +10,14 @@ module Api
#
# @see Api::V1::ApiController for shared API behavior.
class GridSummonsController < Api::V1::ApiController
include IdResolvable
attr_reader :party, :incoming_summon
before_action :find_grid_summon, only: %i[update update_uncap_level update_quick_summon resolve destroy]
before_action :find_party, only: %i[create update update_uncap_level update_quick_summon resolve destroy]
before_action :find_grid_summon, only: %i[update update_uncap_level update_quick_summon update_position resolve destroy]
before_action :find_party, only: %i[create update update_uncap_level update_quick_summon update_position swap resolve destroy]
before_action :find_incoming_summon, only: :create
before_action :authorize_party_edit!, only: %i[create update update_uncap_level update_quick_summon destroy]
before_action :authorize_party_edit!, only: %i[create update update_uncap_level update_quick_summon update_position swap destroy]
##
# Creates a new grid summon.
@ -114,6 +116,97 @@ module Api
render json: GridSummonBlueprint.render(summons, view: :nested, root: :summons)
end
##
# Updates the position of a GridSummon.
#
# Moves a grid summon to a new position, optionally changing its container.
# Validates that the target position is empty and within allowed bounds.
#
# @return [void]
def update_position
new_position = position_params[:position].to_i
new_container = position_params[:container]
# Validate position bounds (-1 main, 0-3 sub, 4-5 subaura, 6 friend)
unless valid_summon_position?(new_position)
return render_unprocessable_entity_response(
Api::V1::InvalidPositionError.new("Invalid position #{new_position} for summon")
)
end
# Check if position is restricted (main summon, friend)
if restricted_summon_position?(new_position)
return render_unprocessable_entity_response(
Api::V1::InvalidPositionError.new("Cannot move summon to restricted position #{new_position}")
)
end
# Check if target position is occupied
if GridSummon.exists?(party_id: @party.id, position: new_position)
return render_unprocessable_entity_response(
Api::V1::PositionOccupiedError.new("Position #{new_position} is already occupied")
)
end
@grid_summon.position = new_position
if @grid_summon.save
render json: {
party: PartyBlueprint.render_as_hash(@party.reload, view: :full),
grid_summon: GridSummonBlueprint.render_as_hash(@grid_summon.reload, view: :nested)
}, status: :ok
else
render_validation_error_response(@grid_summon)
end
end
##
# Swaps positions between two GridSummons.
#
# Exchanges the positions of two grid summons within the same party.
# Both summons must belong to the same party and not be in restricted positions.
#
# @return [void]
def swap
source_id = swap_params[:source_id]
target_id = swap_params[:target_id]
source = GridSummon.find_by(id: source_id, party_id: @party.id)
target = GridSummon.find_by(id: target_id, party_id: @party.id)
unless source && target
return render_not_found_response('grid_summon')
end
# Check if either position is restricted
if restricted_summon_position?(source.position) || restricted_summon_position?(target.position)
return render_unprocessable_entity_response(
Api::V1::InvalidPositionError.new("Cannot swap summons in restricted positions")
)
end
# Perform the swap
ActiveRecord::Base.transaction do
temp_position = -999
source_pos = source.position
target_pos = target.position
source.update!(position: temp_position)
target.update!(position: source_pos)
source.update!(position: target_pos)
end
render json: {
party: PartyBlueprint.render_as_hash(@party.reload, view: :full),
swapped: {
source: GridSummonBlueprint.render_as_hash(source.reload, view: :nested),
target: GridSummonBlueprint.render_as_hash(target.reload, view: :nested)
}
}, status: :ok
rescue ActiveRecord::RecordInvalid => e
render_validation_error_response(e.record)
end
#
# Destroys a grid summon.
#
@ -214,7 +307,7 @@ module Api
#
# @return [void]
def find_incoming_summon
@incoming_summon = Summon.find_by(id: summon_params[:summon_id])
@incoming_summon = find_by_any_id(Summon, summon_params[:summon_id])
end
##
@ -331,6 +424,26 @@ module Api
ActiveSupport::SecurityUtils.secure_compare(provided_edit_key, party_edit_key)
end
##
# Validates if a summon position is valid.
#
# @param position [Integer] the position to validate.
# @return [Boolean] true if the position is valid; false otherwise.
def valid_summon_position?(position)
# Main (-1), sub slots (0-3), subaura (4-5), friend (6)
position == -1 || (0..6).cover?(position)
end
##
# Checks if a summon position is restricted (cannot be drag-drop target).
#
# @param position [Integer] the position to check.
# @return [Boolean] true if the position is restricted; false otherwise.
def restricted_summon_position?(position)
# Main summon (-1) and friend summon (6) are restricted
position == -1 || position == 6
end
##
# Defines and permits the whitelisted parameters for a grid summon.
#
@ -339,6 +452,22 @@ module Api
params.require(:summon).permit(:id, :party_id, :summon_id, :position, :main, :friend,
:quick_summon, :uncap_level, :transcendence_step)
end
##
# Specifies and permits the position update parameters.
#
# @return [ActionController::Parameters] the permitted parameters.
def position_params
params.permit(:position, :container)
end
##
# Specifies and permits the swap parameters.
#
# @return [ActionController::Parameters] the permitted parameters.
def swap_params
params.permit(:source_id, :target_id)
end
end
end
end