Agenda/Agenda.cs
using System;
namespace PlanetMeat;
public sealed partial class Agenda : GameObjectSystem<Agenda>
{
public const int MAX_SPAWN_DISTANCE = 2000;
public const int ROUND_LENGTH = 3;
private class EnemyGroup( float spawnAngle, float spawnSpread, int count, float delay )
{
public readonly int budget = count;
public readonly float untilNext = delay;
public float Spawn()
{
float spawnDistance = MAX_SPAWN_DISTANCE;
var round = Current.RoundNum;
if ( round < 3 )
spawnDistance *= 0.75f + 0.25f * (round - 1) / 3.0f;
var allSpecials = Current.PresentSpecials;
foreach ( var v in allSpecials )
v.BeginGroup();
var groupSpecials = allSpecials.Where( v => v.InGroup );
var groupFallback = Current.BaseVariant;
var cost = 0;
while ( cost < budget )
{
var v = groupSpecials.FirstOrDefault( s => s.InSpawn, groupFallback );
v.SpawnWithGroup( Vector2.FromDegrees( spawnAngle + Game.Random.Float( spawnSpread ) ) * spawnDistance );
cost += v.SpawnCost;
foreach ( var other in groupSpecials.Where( s => s != v ) )
other.OtherSpawned();
}
foreach ( var v in allSpecials )
v.EndGroup();
return untilNext;
}
}
public enum Phases { Startup, Home, Wave, Upgrade, Failed, EndOfRound };
public Phases Phase
{
get;
private set
{
var prev = field;
field = value;
foreach ( var pl in Scene.GetAll<IAgendaPhaseListener>() )
pl.OnTimelinePhase( prev, value );
}
} = Phases.Startup;
private float NextGroupIn { get; set; } = 0.0f;
public int RoundNum { get; set; } = 1;
public int WaveNum { get; set; } = 1;
public double MeatBounty { get; set; } = 0.0;
public double MeatFailureLoss => Math.Ceiling( MeatBounty * (1.0f - AggregateMeatProtection.Total) );
public double GetMeatBounty( int round ) => Math.Pow( 2, round - 1 ).MultiplyCapped( AggregateMeatRate.Total );
public double CurrentMeatBounty => GetMeatBounty( RoundNum );
public double NextMeatBounty => GetMeatBounty( RoundNum + 1 );
public ValueAggregator<float> AggregateMeatProtection => field ??= ValueAggregator<float>.FromImprovement( "meat_protect" );
public ValueAggregator<float> AggregateMeatRate => field ??= ValueAggregator<float>.FromImprovement( "meat_rate" );
public ValueAggregator<int> AggregateMeatIncome => field ??= ValueAggregator<int>.FromImprovement( "meat_income" );
public bool DeferWaveEndCheck { get; set; } = false;
public int NumActiveTracked
{
get;
set
{
var zeroed = value == 0 && field > 0;
field = value;
if ( DeferWaveEndCheck )
DeferWaveEndCheck = false;
else if ( zeroed && Phase == Phases.Wave && waitingGroups.Count == 0 )
EndWave();
}
} = 0;
private readonly Queue<EnemyGroup> waitingGroups = [];
private EnemyVariant BaseVariant => field ??= new( "zomsauge", true );
private Dictionary<string, EnemyVariant> VariantsById => field ??= new()
{
["zomsauge"] = BaseVariant,
["morsel"] = new( "morsel" ) { SpawnSize = 3, WaveContributionPerLevel = 3 },
["feast"] = new( "feast" ) { SpawnCost = 5 },
["slinger"] = new( "slinger" ) { WaveContributionPerLevel = 2 },
["burster"] = new( "burster" ) { SpawnCost = 2 },
};
private IEnumerable<EnemyVariant> SpecialVariants => field ??= VariantsById.Values.Where( v => v != BaseVariant );
private IEnumerable<EnemyVariant> PresentSpecials => SpecialVariants.Where( v => v.IsPresent );
private IEnumerable<EnemyVariant> PresentEnemyVariants => AllVariants.Where( v => v.IsPresent );
public List<EnemyVariant> BakedEnemyVariants { get; } = [];
public List<EnemyVariant> AllVariants
{
get
{
if ( field is null )
{
field = [.. VariantsById.Values];
field.Sort( SortVariants );
}
return field;
}
}
public IEnumerable<string> AllVariantIdents => AllVariants.Select( v => v.Ident );
public EnemyVariant GetEnemyVariant( string ident ) => VariantsById.GetValueOrDefault( ident, null );
public ScoutReport ScoutReport { get; private set; }
public bool ShowScoutReport { get; private set; } = false;
public int ScoutReportId { get; private set; } = 0;
public List<Challenge> OfferedChallenges { get; } = [];
public Agenda( Scene scene ) : base( scene )
{
if ( !Scene.IsEditor )
Listen( Stage.StartUpdate, 0, OnUpdate, "AgendaStep" );
}
private void Reset()
{
Bank.Current.GetAccount( "goop" ).Clear();
Difficulty.Current.ResetChallenges();
RoundNum = 1;
WaveNum = 1;
MeatBounty = 0.0;
}
public static IEnumerable<int> GetGroupSizes( float debtStep, int groupCount, int groupSizeSmall )
{
var debt = debtStep;
for ( var g = 0; g < groupCount; g++ )
{
var big = debt > 0.5f;
debt += big ? -1.0f : debtStep;
yield return big ? groupSizeSmall + 1 : groupSizeSmall;
}
}
public void TryStartWave()
{
if ( Phase != Phases.Upgrade )
return;
Phase = Phases.Wave;
foreach ( var t in Scene.GetAll<WaveTracked>() )
t.FindNearestUpwards<Targetable>().Instakill();
NumActiveTracked = 0;
var groupDelay = ScoutReport.groupDelay;
var groupInternalSpread = ScoutReport.groupSpread;
var groupExternalSpread = MathF.Min( 180.0f, 20.0f * Difficulty.Current.GetMult( "GroupSpread" ) );
var groupSpawnAngle = Game.Random.Float( 360.0f );
foreach ( var size in GetGroupSizes( ScoutReport.groupSizeDebt, ScoutReport.groupCount, ScoutReport.groupSizeMin ) )
{
waitingGroups.Enqueue( new(
groupSpawnAngle,
groupInternalSpread,
size,
groupDelay
) );
groupSpawnAngle += Game.Random.Float( -groupExternalSpread, groupExternalSpread );
}
NextGroupIn = 0.25f;
var numGroups = waitingGroups.Count;
foreach ( var v in SpecialVariants )
v.BeginWave( numGroups );
PlayerTool.BakeTools( Scene.GetAllComponents<PlayerTool>() );
foreach ( var tool in PlayerTool.Toolbox )
tool.EndCooldown();
}
private void EndWave()
{
Career.Current.IncrementStat( "wave" );
if ( WaveNum == ROUND_LENGTH )
{
EndRound();
}
else
{
WaveNum++;
StartUpgrade();
}
}
private int SortVariants( EnemyVariant a, EnemyVariant b )
{
if ( a == BaseVariant )
return -1;
if ( b == BaseVariant )
return 1;
return Game.Language.GetPhrase( a.DisplayName ).CompareTo( Game.Language.GetPhrase( b.DisplayName ) );
}
private void StartUpgrade()
{
foreach ( var v in SpecialVariants )
v.BakeStats();
BakedEnemyVariants.Clear();
BakedEnemyVariants.AddRange( PresentEnemyVariants );
BakedEnemyVariants.Sort( SortVariants );
MeatBounty = MeatBounty.AddCapped( AggregateMeatIncome.Total );
ScoutReport = new( PresentEnemyVariants );
ScoutReportId++;
Catalogue.Current.BakeUpgrades( Scene.GetAllComponents<BaseUpgrade>() );
Catalogue.Current.CurrentTab = "upgrades";
Phase = Phases.Upgrade;
}
private void EndRound()
{
MeatBounty += CurrentMeatBounty;
Career.Current.IncrementStat( "round" );
Career.Current.HighestRoundCompleted = RoundNum;
if ( RoundNum >= 1 )
{
Sandbox.Services.Achievements.Unlock( "round_1" );
if ( RoundNum >= 11 )
{
Sandbox.Services.Achievements.Unlock( "round_11" );
if ( RoundNum >= 111 )
{
Sandbox.Services.Achievements.Unlock( "round_111" );
}
}
}
Research.Current.AdvanceResearchProgress();
Phase = Phases.EndOfRound;
}
public void DecideEndOfRound( bool nextRound )
{
if ( nextRound )
{
OfferedChallenges.Clear();
OfferedChallenges.AddRange( Difficulty.Current.CreateOffers( RoundNum ) );
if ( OfferedChallenges.Count == 0 )
ChooseOfferedChallenge( null );
}
else
{
RelocateToHq();
}
}
public void RelocateToHq()
{
if ( Phase != Phases.Failed )
Career.Current.IncrementStat( "runs_banked" );
var meatGained = MeatBounty;
Career.Current.IncrementStat( "meat_banked", meatGained );
Bank.Current.GetAccount( "meat" ).Profit( meatGained );
Career.Current.CommitRunStats();
Research.Current.BakeCatalogue();
Catalogue.Current.CloseTab();
Scene.LoadFromFile( "scenes/hq.scene" );
Reset();
Phase = Phases.Home;
}
public void RelocateToArena( int startRound = 1 )
{
if ( startRound >= 3 )
Sandbox.Services.Achievements.Unlock( "skip_3" );
Research.Current.IgnoreCompletedResearch();
Research.Current.BakeEffects();
Catalogue.Current.CloseTab();
Scene.LoadFromFile( "scenes/arena.scene" );
Reset();
RoundNum = Math.Max( 1, startRound );
ShowScoutReport = Research.Current.HasTech( "scout" );
StartUpgrade();
}
public void OnDefenseFailed()
{
if ( Phase != Phases.Failed )
{
Phase = Phases.Failed;
foreach ( var t in Scene.GetAll<Targetable>() )
t.Instakill();
var loss = MeatFailureLoss;
MeatBounty -= loss;
Career.Current.IncrementStat( "meat_lost", loss );
Career.Current.IncrementStat( "runs_lost" );
}
}
public void ChooseOfferedChallenge( Challenge challenge )
{
if ( challenge is not null )
Difficulty.Current.AddChallenge( challenge );
OfferedChallenges.Clear();
Career.Current.CommitRoundStats();
RoundNum++;
WaveNum = 1;
StartUpgrade();
}
private void OnUpdate()
{
if ( Phase == Phases.Wave )
{
var dt = Time.Delta;
if ( NextGroupIn > 0.0f )
{
NextGroupIn -= dt;
if ( NextGroupIn <= 0.01f )
{
var delay = NextGroupIn;
while ( delay <= 0.01f && waitingGroups.Count > 0 )
delay += waitingGroups.Dequeue().Spawn();
if ( waitingGroups.Count > 0 )
NextGroupIn = delay;
}
}
}
}
}