CareerAchievements.cs

Partial ClubCareer class for a football career mode. Stores league season history, club achievements and per-season challenges, and resolves/unlocks them from completed league and cup results, including claiming coin and training card rewards.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

namespace Socer;

public sealed class LeagueSeasonResult
{
    public int Season {get;set;}
    public int Division {get;set;}
    public int Place {get;set;}
    public int Played {get;set;}
    public int Points {get;set;}
    public int Wins {get;set;}
    public int Draws {get;set;}
    public int Losses {get;set;}
    public int GoalsFor {get;set;}
    public int GoalsAgainst {get;set;}
}
public sealed class ClubAchievement
{
    public string Id {get;set;}="";
    public int EarnedSeason {get;set;}
    public bool Claimed {get;set;}
    public int CoinReward {get;set;}
    public int TrainingGrade {get;set;}
    public int TrainingCount {get;set;}
    [JsonIgnore] public bool RewardAvailable=>!Claimed && (CoinReward>0 || TrainingCount>0);
}
public sealed class CareerSeasonChallenge
{
    public string Id {get;set;}="";
    public int Season {get;set;}
    public string Objective {get;set;}="";
    public int Target {get;set;}
    public int Progress {get;set;}
    public bool Resolved {get;set;}
    public bool Earned {get;set;}
    public bool Claimed {get;set;}
    public int CoinReward {get;set;}=400;
    public int TrainingGrade {get;set;}=2;
    public int TrainingCount {get;set;}=1;
}

public sealed partial class ClubCareer
{
    public int AchievementVersion {get;set;}
    public List<LeagueSeasonResult> LeagueHistory {get;set;}=new();
    public List<ClubAchievement> Achievements {get;set;}=new();
    public List<CareerSeasonChallenge> SeasonChallenges {get;set;}=new();
    [JsonIgnore] public bool PremierChampion=>PremierTitleCount>0;
    [JsonIgnore] public int FirstPremierTitleSeason=>(LeagueHistory??new()).Where(r=>r.Division==5 && r.Place==1).Select(r=>r.Season).DefaultIfEmpty().Min();
    [JsonIgnore] public int PremierTitleCount=>(LeagueHistory??new()).Count(r=>r.Division==5 && r.Place==1);
    [JsonIgnore] public CareerSeasonChallenge CurrentSeasonChallenge=>SeasonChallenges?.FirstOrDefault(c=>c.Season==Season);
    [JsonIgnore] public int ClaimableCareerRewards=>(Achievements??new()).Count(a=>a.RewardAvailable)+(SeasonChallenges??new()).Count(c=>c.Earned && !c.Claimed);

    // The complete current table is evidence. Season number, division, coins or
    // cumulative wins cannot prove old titles and never create historical ones.
    public void NormalizeCareerAchievements()
    {
        LeagueHistory??=new();Achievements??=new();SeasonChallenges??=new();
        LeagueHistory=LeagueHistory.Where(r=>r!=null && r.Season>0 && r.Division is >=1 and <=5 && r.Place is >=1 and <=8 && r.Played>0)
            .GroupBy(r=>r.Season).Select(g=>g.First()).OrderBy(r=>r.Season).ToList();
        Achievements=Achievements.Where(a=>a!=null && !string.IsNullOrWhiteSpace(a.Id)).GroupBy(a=>a.Id).Select(g=>g.First()).ToList();
        SeasonChallenges=SeasonChallenges.Where(c=>c!=null && c.Season>0 && !string.IsNullOrWhiteSpace(c.Id)).GroupBy(c=>c.Season).Select(g=>g.First()).ToList();
        AchievementVersion=1;
        ResolveCareerAchievements();
        EnsureSeasonChallenge();
    }
    public void ResolveCareerAchievements()
    {
        LeagueHistory??=new();Achievements??=new();SeasonChallenges??=new();
        if(LeagueComplete && !LeagueHistory.Any(r=>r.Season==Season))
        {
            var table=Standings();var own=table.First(r=>r.Team==0);
            LeagueHistory.Add(new(){Season=Season,Division=Division,Place=table.FindIndex(r=>r.Team==0)+1,
                Played=own.Played,Points=own.Points,Wins=own.Wins,Draws=own.Draws,Losses=own.Losses,GoalsFor=own.For,GoalsAgainst=own.Against});
        }
        foreach(var result in LeagueHistory.Where(r=>r.Place==1).OrderBy(r=>r.Season))
        {
            UnlockAchievement("first-league-title",result.Season);
            if(result.Division!=5)continue;
            UnlockAchievement("premier-champion",result.Season,1000,3,1);
            if(result.Losses==0)UnlockAchievement("unbeaten-champion",result.Season);
            if(CupResults.Any(c=>c.Season==result.Season && c.PlaceFrom==1 && c.PlaceTo==1))UnlockAchievement("domestic-double",result.Season);
        }
        foreach(var cup in CupResults.Where(c=>c.PlaceFrom==1 && c.PlaceTo==1).OrderBy(c=>c.Season))UnlockAchievement("cup-winner",cup.Season);
        ResolveSeasonChallenge();
    }
    void UnlockAchievement(string id,int season,int coins=0,int grade=0,int training=0)
    {
        if(Achievements.Any(a=>a.Id==id))return;
        Achievements.Add(new(){Id=id,EarnedSeason=season,CoinReward=coins,TrainingGrade=grade,TrainingCount=training,Claimed=coins==0 && training==0});
    }
    void EnsureSeasonChallenge()
    {
        if(!PremierChampion || Season<=FirstPremierTitleSeason || CurrentSeasonChallenge!=null)return;
        // A new challenge starts with a new season. Do not backfill a loaded
        // partially played season with retroactive progress or extra prizes.
        if(PlayedLeagueRounds>0 || Tournaments.Any(t=>t.Started || t.Finished))return;
        string[] objectives={"defend-title","strong-season","double","clean-sheets"};
        string objective=Division<5?(Division==4?"return-premier":"strong-season"):objectives[(int)((PlayerCatalog.Hash($"champion-{ClubSeed}")+(uint)Season)%4)];
        int games=League.Count(f=>f.Home==0 || f.Away==0);
        int target=objective=="strong-season"?(int)Math.Ceiling(games*3*.72):objective=="clean-sheets"?Math.Max(1,(int)Math.Ceiling(games*.4)):1;
        SeasonChallenges.Add(new(){Id=$"season-{Season}-{objective}",Season=Season,Objective=objective,Target=target});
    }
    void ResolveSeasonChallenge()
    {
        var challenge=CurrentSeasonChallenge;if(challenge==null || challenge.Resolved)return;
        var own=Standings().First(r=>r.Team==0);
        var result=LeagueHistory.FirstOrDefault(r=>r.Season==Season);
        bool champion=result?.Division==5 && result.Place==1;
        var games=League.Where(f=>f.Played && (f.Home==0 || f.Away==0));
        challenge.Progress=challenge.Objective switch
        {
            "defend-title"=>champion?1:0,
            "double"=>champion && CupResults.Any(c=>c.Season==Season && c.PlaceFrom==1 && c.PlaceTo==1)?1:0,
            "return-premier"=>result?.Division==4 && result.Place<=2?1:0,
            "strong-season"=>Math.Min(challenge.Target,own.Points),
            "clean-sheets"=>Math.Min(challenge.Target,games.Count(f=>(f.Home==0?f.AwayGoals:f.HomeGoals)==0)),
            _=>0
        };
        if(challenge.Progress>=challenge.Target){challenge.Earned=true;challenge.Resolved=true;}
        else if(SeasonComplete)challenge.Resolved=true;
    }
    public bool ClaimCareerReward(string id)
    {
        if(Pending!=null || string.IsNullOrWhiteSpace(id))return false;
        var achievement=Achievements?.FirstOrDefault(a=>a.Id==id);
        var challenge=SeasonChallenges?.FirstOrDefault(c=>c.Id==id);
        int coins,grade,count;
        if(achievement?.RewardAvailable==true)
        {
            coins=achievement.CoinReward;grade=achievement.TrainingGrade;count=achievement.TrainingCount;
        }
        else if(challenge is {Earned:true,Claimed:false})
        {
            coins=challenge.CoinReward;grade=challenge.TrainingGrade;count=challenge.TrainingCount;
        }
        else return false;
        if(!CanReceiveTrainingCards(count))return false;
        if(achievement?.RewardAvailable==true)achievement.Claimed=true;else challenge.Claimed=true;
        Coins=(int)Math.Min(int.MaxValue,(long)Coins+Math.Max(0,coins));
        TrainingInventory??=new();NextTrainingCardId=Math.Max(NextTrainingCardId,TrainingInventory.Select(c=>c.Id).DefaultIfEmpty().Max()+1);
        for(int i=0;i<count;i++)TrainingInventory.Add(new(){Id=NextTrainingCardId++,Grade=Math.Clamp(grade,1,3),Focus=0,Xp=new[]{45,90,150}[Math.Clamp(grade,1,3)-1]});
        return true;
    }
}