Career data model for cup tournaments and season checkpoints in a soccer career mode. Stores brackets, fixtures, cup results and history, mission rewards, and migration of older save data into the current cup format.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace Socer;
public sealed class CupTournament
{
public string Id { get; set; }="";
public string Name { get; set; }="";
public string Mark { get; set; }="";
public int OpensAfterRound { get; set; }
public int StrengthOffset { get; set; }
public int[] ClubIds {get;set;}=Array.Empty<int>();
public int EntrantsRevision {get;set;}
public int RoundPrize { get; set; }
public int TrophyPrize { get; set; }
public bool Started { get; set; }
public bool Finished { get; set; }
public bool Eliminated { get; set; }
public bool TrophyPaid { get; set; }
public string WorldCompletionError { get; set; }="";
public List<ClubFixture> Games { get; set; }=new();
[JsonIgnore] public bool WorldCompletionPending=>Eliminated && !Finished;
[JsonIgnore] public int CurrentRound=>Games.Count==0?1:Games.Max(f=>f.Round);
[JsonIgnore] public ClubFixture NextNorth=>Finished?null:Games.FirstOrDefault(f=>!f.Played && (f.Home==0 || f.Away==0));
[JsonIgnore] public ClubFixture Final=>Games.FirstOrDefault(f=>f.Round==3);
[JsonIgnore] public bool Won=>Finished && Final?.Winner==0;
[JsonIgnore] public int ChampionClubId=>Finished && Final is {Played:true} final &&
final.Winner>=0 && final.Winner<ClubIds.Length?ClubIds[final.Winner]:-1;
}
public sealed class SeasonCheckpoint
{
public int Round { get; set; }
public int RequiredPoints { get; set; }
public int CoinPrize { get; set; }
public bool Resolved { get; set; }
public bool Earned { get; set; }
public bool Claimed { get; set; }
public string Label { get; set; }="";
}
public sealed class CupMatchResult
{
public int FixtureId {get;set;}
public int Round {get;set;}
public int HomeClubId {get;set;}
public int AwayClubId {get;set;}
public string HomeName {get;set;}="";
public string AwayName {get;set;}="";
public int HomeGoals {get;set;}
public int AwayGoals {get;set;}
public bool Penalties {get;set;}
public int HomePenalties {get;set;}
public int AwayPenalties {get;set;}
public int WinnerClubId {get;set;}=-1;
}
public sealed class CupSeasonResult
{
public int Season { get; set; }
public int Division { get; set; }
public string CupId { get; set; }="";
public string CupName { get; set; }="";
public int Round { get; set; }
public int PlaceFrom { get; set; }
public int PlaceTo { get; set; }
public int PlayedSequence { get; set; }
public int ChampionClubId {get;set;}=-1;
public string ChampionName {get;set;}="";
public List<CupMatchResult> Matches {get;set;}=new();
}
public sealed partial class ClubCareer
{
public List<CupTournament> Tournaments { get; set; }=new();
public List<CupSeasonResult> CupHistory { get; set; }=new();
public List<SeasonCheckpoint> Checkpoints { get; set; }=new();
public string ActiveCupId { get; set; }="";
public int MissionRewardVersion {get;set;}
[JsonIgnore] public int ClaimableMissions=>Checkpoints.Count(c=>c.Earned && !c.Claimed);
public bool ClaimMission(int round)
{
var mission=Checkpoints.FirstOrDefault(c=>c.Round==round);
if(mission==null || !mission.Earned || mission.Claimed)return false;
mission.Claimed=true;Coins+=mission.CoinPrize;return true;
}
[JsonIgnore] public int LeagueRoundCount=>League.Select(f=>f.Round).DefaultIfEmpty().Max();
[JsonIgnore] public int PlayedLeagueRounds=>League.Count(f=>f.Played && (f.Home==0 || f.Away==0));
[JsonIgnore] public CupTournament ActiveCup=>Tournaments.FirstOrDefault(t=>t.Id==ActiveCupId && !t.Finished && !t.Eliminated);
[JsonIgnore] public CupTournament DueCup=>ActiveCup??Tournaments.OrderBy(t=>t.OpensAfterRound).FirstOrDefault(t=>!t.Finished && !t.Eliminated && PlayedLeagueRounds>=t.OpensAfterRound);
[JsonIgnore] public bool CupWindow=>DueCup!=null;
[JsonIgnore] public bool SeasonComplete=>LeagueComplete && (!FinalCupRequired || FinalCup!=null) && Tournaments.All(t=>t.Finished);
[JsonIgnore] public CupTournament MatchCup=>Pending?.Cup==true?Tournaments.FirstOrDefault(t=>t.Id==Pending.CupId)??ActiveCup:null;
[JsonIgnore] public IReadOnlyList<CupSeasonResult> CupResults=>(CupHistory??new()).Concat(
(Tournaments??new()).Select(CupResult).Where(r=>r!=null))
.GroupBy(r=>(r.Season,r.CupId)).Select(g=>g.First())
.OrderByDescending(r=>r.Season).ThenByDescending(r=>r.PlayedSequence).ThenBy(r=>r.CupName).ToArray();
CupSeasonResult CupResult(CupTournament cup)
{
if(!cup.Finished && !cup.Eliminated)return null;
var last=cup.Games.Where(f=>f.Played && (f.Home==0 || f.Away==0)).OrderByDescending(f=>f.Round).FirstOrDefault();
if(last==null || last.Round is <1 or >3)return null;
int outcome=last.ResultFor(0);
if(outcome==0 || (outcome>0 && last.Round!=3))return null;
// This bracket has no placement playoffs: defeated semifinalists share
// 3rd–4th and defeated quarterfinalists share 5th–8th.
int first=outcome>0?1:last.Round==3?2:last.Round==2?3:5;
int final=last.Round==3?first:last.Round==2?4:8;
return new(){Season=Season,Division=Division,CupId=cup.Id,CupName=cup.Name,Round=last.Round,
PlaceFrom=first,PlaceTo=final,PlayedSequence=last.PlayedSequence,
ChampionClubId=cup.ChampionClubId,ChampionName=cup.ChampionClubId>=0?TeamName(cup.Final.Winner,cup):"",
Matches=cup.Games.Where(f=>f.Played).OrderBy(f=>f.Round).ThenBy(f=>f.Id).Select(f=>new CupMatchResult{
FixtureId=f.Id,Round=f.Round,HomeClubId=ClubId(f.Home,cup),AwayClubId=ClubId(f.Away,cup),
HomeName=TeamName(f.Home,cup),AwayName=TeamName(f.Away,cup),HomeGoals=f.HomeGoals,AwayGoals=f.AwayGoals,
Penalties=f.Penalties,HomePenalties=f.HomePenalties,AwayPenalties=f.AwayPenalties,
WinnerClubId=f.Winner>=0?ClubId(f.Winner,cup):-1}).ToList()};
}
void RecordCupResults()
{
CupHistory??=new();
foreach(var cup in Tournaments??new())
{
var result=CupResult(cup);
if(result==null)continue;
var saved=CupHistory.FirstOrDefault(r=>r.Season==result.Season && r.CupId==result.CupId);
if(saved==null)CupHistory.Add(result);
else if(result.Matches.Count>saved.Matches.Count || saved.ChampionClubId<0 && result.ChampionClubId>=0)
{
// The club's finishing place is already settled. Complete only
// its world bracket snapshot; never replay personal rewards.
saved.Matches=result.Matches;saved.ChampionClubId=result.ChampionClubId;saved.ChampionName=result.ChampionName;
}
}
}
public int OpponentRating(int opponent,int playerIndex,CupTournament cup=null)
{
if(opponent==0)return Card(Math.Clamp(playerIndex,0,10))?.Overall??SquadRating;
var squad=SquadFor(opponent,cup);
return PlayerCatalog.Find(squad.Players[Math.Clamp(playerIndex,0,10)]).Overall;
}
void BuildCompetitions()
{
Tournaments=new();ActiveCupId="";MissionRewardVersion=1;
int early=Math.Max(2,LeagueRoundCount/3),late=Math.Max(early+2,LeagueRoundCount*2/3);
Tournaments.Add(CreateTournament("open","Open Cup","✦",early,4,400,1200));
Tournaments.Add(Division<=2
?CreateTournament("county","County Cup","◇",late,-3,260,800)
:CreateTournament("league","League Cup","◆",late,2,480,1500));
int rounds=LeagueRoundCount;
Checkpoints=new()
{
new(){Round=Math.Max(2,rounds/4),RequiredPoints=Math.Max(4,rounds/3),CoinPrize=150,Label="Opening form"},
new(){Round=Math.Max(4,rounds/2),RequiredPoints=Math.Max(8,rounds*2/3),CoinPrize=250,Label="Halfway push"},
new(){Round=Math.Max(6,rounds*3/4),RequiredPoints=Math.Max(12,rounds),CoinPrize=350,Label="Final stretch"}
};
}
CupTournament CreateTournament(string id,string name,string mark,int openRound,int strength,int roundPrize,int trophyPrize)
{
var cup=new CupTournament{Id=id,Name=name,Mark=mark,OpensAfterRound=openRound,
StrengthOffset=0,RoundPrize=roundPrize,TrophyPrize=trophyPrize};
DrawCupEntrants(cup);
// A seeded draw keeps the bracket stable after save/load. NORTH is always in it.
var entrants=Enumerable.Range(0,8).ToArray();
var rng=new Random(unchecked(ClubSeed+Season*1543+id.Sum(c=>c)*97));
for(int i=entrants.Length-1;i>0;i--){int j=rng.Next(i+1);(entrants[i],entrants[j])=(entrants[j],entrants[i]);}
for(int i=0;i<4;i++)cup.Games.Add(new ClubFixture{Id=i,Round=1,Home=entrants[i*2],Away=entrants[i*2+1]});
return cup;
}
void ResolveCheckpoints()
{
if(Checkpoints==null)return;
int points=Standings().First(r=>r.Team==0).Points;
foreach(var checkpoint in Checkpoints.Where(c=>!c.Resolved))
{
if(points<checkpoint.RequiredPoints && PlayedLeagueRounds<checkpoint.Round)continue;
checkpoint.Resolved=true;
checkpoint.Earned=points>=checkpoint.RequiredPoints && PlayedLeagueRounds<=checkpoint.Round;
}
}
void MigrateCompetitions()
{
Tournaments??=new();Checkpoints??=new();
if(MissionRewardVersion<1)
{
foreach(var mission in Checkpoints.Where(c=>c.Earned))mission.Claimed=true;
MissionRewardVersion=1;
}
if(Tournaments.Count==0)
{
// Existing saves keep their eight-team bracket and finished results.
var oldGames=Cup?.Count>0?Cup:new List<ClubFixture>();
var legacy=new CupTournament{Id="open",Name="Open Cup",Mark="✦",OpensAfterRound=Math.Min(4,Math.Max(1,LeagueRoundCount/3)),
StrengthOffset=4,RoundPrize=400,TrophyPrize=1200,Games=oldGames.Count>0?oldGames:CreateTournament("open","Open Cup","✦",0,4,400,1200).Games,
Started=oldGames.Any(f=>f.Played),Finished=CupCompleteLegacy || (oldGames.Any(f=>f.Played && f.Winner!=0 && (f.Home==0 || f.Away==0))),
TrophyPaid=CupPrizePaid};
legacy.Eliminated=legacy.Finished && !legacy.Won;
Tournaments.Add(legacy);
if(Pending?.Cup==true){ActiveCupId="open";Pending.CupId="open";legacy.Started=true;legacy.Finished=false;legacy.Eliminated=false;}
else if(legacy.Started && !legacy.Finished && legacy.NextNorth!=null)ActiveCupId="open";
}
if(Checkpoints.Count==0)
{
int played=PlayedLeagueRounds;
Checkpoints=new(){
new(){Round=Math.Max(2,LeagueRoundCount/4),RequiredPoints=Math.Max(4,LeagueRoundCount/3),CoinPrize=150,Label="Opening form"},
new(){Round=Math.Max(4,LeagueRoundCount/2),RequiredPoints=Math.Max(8,LeagueRoundCount*2/3),CoinPrize=250,Label="Halfway push"},
new(){Round=Math.Max(6,LeagueRoundCount*3/4),RequiredPoints=Math.Max(12,LeagueRoundCount),CoinPrize=350,Label="Final stretch"}
};
// Do not retroactively grant rewards on a loaded career.
foreach(var checkpoint in Checkpoints.Where(c=>c.Round<=played))checkpoint.Resolved=true;
}
if(ActiveCupId!="" && ActiveCup==null)ActiveCupId="";
foreach(var cup in Tournaments)EnsureCupEntrants(cup);
RestoreCupWorldCompletions();
// Old saves retain their current completed bracket. The aggregate Cups
// counter cannot identify previous seasons, so it creates no history.
RecordCupResults();
EnsureFinalCupUnlocked();
}
}