Enhance roster management by implementing cross-role player swapping and updating player movement logic. Introduce rating checks for player roles during roster operations and ensure proper handling of captain assignments. Update frontend components to reflect changes in player movement capabilities.
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit is contained in:
2026-07-19 02:44:30 +03:00
parent f468f9df82
commit 23ba04beef
4 changed files with 107 additions and 12 deletions

View File

@@ -147,7 +147,24 @@ func (s *Service) SwapRoster(ctx context.Context, actor domain.Account, eventID,
if roster.Version != expectedVersion {
return roster, fmt.Errorf("%w: stale roster version", domain.ErrConflict)
}
if err = roster.Swap(teamA, playerA, teamB, playerB); err != nil {
roleA, roleB := rosterPlayerRole(roster, teamA, playerA), rosterPlayerRole(roster, teamB, playerB)
players, err := s.Store.ListPlayers(ctx)
if err != nil {
return roster, err
}
ratingAForB, ratingBForA := 0, 0
for _, player := range players {
if player.ID == playerA {
ratingAForB = ratingForRole(player, roleB)
}
if player.ID == playerB {
ratingBForA = ratingForRole(player, roleA)
}
}
if ratingAForB == 0 || ratingBForA == 0 {
return roster, domain.ErrNotFound
}
if err = roster.SwapAcrossRoles(teamA, playerA, teamB, playerB, ratingBForA, ratingAForB); err != nil {
return roster, err
}
roster, err = s.Store.SaveRoster(ctx, roster, expectedVersion)
@@ -170,7 +187,21 @@ func (s *Service) MoveRosterPlayer(ctx context.Context, actor domain.Account, ev
if roster.Version != expectedVersion {
return roster, fmt.Errorf("%w: stale roster version", domain.ErrConflict)
}
if err = roster.MoveToEmpty(fromTeamID, playerID, toTeamID, role); err != nil {
players, err := s.Store.ListPlayers(ctx)
if err != nil {
return roster, err
}
targetRating := 0
for _, player := range players {
if player.ID == playerID {
targetRating = ratingForRole(player, role)
break
}
}
if targetRating == 0 {
return roster, domain.ErrNotFound
}
if err = roster.MoveToEmpty(fromTeamID, playerID, toTeamID, role, targetRating); err != nil {
return roster, err
}
return s.saveRosterChange(ctx, actor, roster, expectedVersion, "roster.player_moved")
@@ -621,3 +652,17 @@ func ratingForRole(player domain.Player, role domain.Role) int {
return 0
}
}
func rosterPlayerRole(roster domain.RosterDraft, teamID, playerID string) domain.Role {
for _, team := range roster.Teams {
if team.ID != teamID {
continue
}
for _, slot := range team.Slots {
if slot.PlayerID == playerID {
return slot.Role
}
}
}
return ""
}

View File

@@ -82,14 +82,17 @@ func (r *RosterDraft) Swap(teamAID, playerAID, teamBID, playerBID string) error
return fmt.Errorf("%w: rosters are locked", ErrConflict)
}
var a, b *Slot
var teamA, teamB *Team
for teamIndex := range r.Teams {
for slotIndex := range r.Teams[teamIndex].Slots {
slot := &r.Teams[teamIndex].Slots[slotIndex]
if r.Teams[teamIndex].ID == teamAID && slot.PlayerID == playerAID {
a = slot
teamA = &r.Teams[teamIndex]
}
if r.Teams[teamIndex].ID == teamBID && slot.PlayerID == playerBID {
b = slot
teamB = &r.Teams[teamIndex]
}
}
}
@@ -101,10 +104,52 @@ func (r *RosterDraft) Swap(teamAID, playerAID, teamBID, playerBID string) error
}
a.PlayerID, b.PlayerID = b.PlayerID, a.PlayerID
a.Rating, b.Rating = b.Rating, a.Rating
clearMovedCaptains(teamA, playerAID, teamB, playerBID)
r.Version++
return r.Validate(false)
}
func (r *RosterDraft) SwapAcrossRoles(teamAID, playerAID, teamBID, playerBID string, playerBRatingForA, playerARatingForB int) error {
if r.Confirmed {
return fmt.Errorf("%w: rosters are locked", ErrConflict)
}
var a, b *Slot
var teamA, teamB *Team
for teamIndex := range r.Teams {
for slotIndex := range r.Teams[teamIndex].Slots {
slot := &r.Teams[teamIndex].Slots[slotIndex]
if r.Teams[teamIndex].ID == teamAID && slot.PlayerID == playerAID {
a = slot
teamA = &r.Teams[teamIndex]
}
if r.Teams[teamIndex].ID == teamBID && slot.PlayerID == playerBID {
b = slot
teamB = &r.Teams[teamIndex]
}
}
}
if a == nil || b == nil {
return ErrNotFound
}
a.PlayerID, b.PlayerID = b.PlayerID, a.PlayerID
a.Rating, b.Rating = playerBRatingForA, playerARatingForB
clearMovedCaptains(teamA, playerAID, teamB, playerBID)
r.Version++
return r.Validate(false)
}
func clearMovedCaptains(teamA *Team, playerAID string, teamB *Team, playerBID string) {
if teamA == nil || teamB == nil || teamA.ID == teamB.ID {
return
}
if teamA.CaptainPlayerID == playerAID {
teamA.CaptainPlayerID = ""
}
if teamB.CaptainPlayerID == playerBID {
teamB.CaptainPlayerID = ""
}
}
func (r *RosterDraft) Substitute(teamID, outgoingID, reserveID string, rating int) error {
if r.Confirmed {
return fmt.Errorf("%w: rosters are locked", ErrConflict)
@@ -158,7 +203,7 @@ func (r *RosterDraft) PlaceReserve(teamID string, role Role, reserveID string, r
return fmt.Errorf("%w: empty role slot not found", ErrNotFound)
}
func (r *RosterDraft) MoveToEmpty(fromTeamID, playerID, toTeamID string, role Role) error {
func (r *RosterDraft) MoveToEmpty(fromTeamID, playerID, toTeamID string, role Role, targetRating int) error {
if r.Confirmed {
return fmt.Errorf("%w: rosters are locked", ErrConflict)
}
@@ -179,10 +224,7 @@ func (r *RosterDraft) MoveToEmpty(fromTeamID, playerID, toTeamID string, role Ro
if source == nil || target == nil {
return ErrNotFound
}
if source.Role != target.Role {
return fmt.Errorf("%w: only equal roles can be moved", ErrInvalid)
}
target.PlayerID, target.Rating = source.PlayerID, source.Rating
target.PlayerID, target.Rating = source.PlayerID, targetRating
source.PlayerID, source.Rating = "", 0
if sourceTeam.CaptainPlayerID == playerID {
sourceTeam.CaptainPlayerID = ""

View File

@@ -67,12 +67,20 @@ func TestRosterDragOperationsPreserveEmptyRoleSlots(t *testing.T) {
if err := roster.MoveToReserve("b", "b-t"); err != nil {
t.Fatal(err)
}
if err := roster.MoveToEmpty("a", "reserve", "b", Tank); err != nil {
if err := roster.MoveToEmpty("a", "reserve", "b", Tank, 31); err != nil {
t.Fatal(err)
}
if roster.Teams[1].Slots[0].PlayerID != "reserve" {
t.Fatal("dragging into an empty role slot did not move the player")
}
roster.Teams[0].CaptainPlayerID = "a-d1"
roster.Teams[1].CaptainPlayerID = "reserve"
if err := roster.SwapAcrossRoles("a", "a-d1", "b", "reserve", 25, 33); err != nil {
t.Fatalf("cross-role drag swap failed: %v", err)
}
if roster.Teams[0].CaptainPlayerID != "" || roster.Teams[1].CaptainPlayerID != "" {
t.Fatal("captains that moved to another team were not cleared")
}
}
func TestSeriesFSMRunsCoinBansAndResult(t *testing.T) {