Spending/Research/Research.cs
using System;
using System.Numerics;
using System.Text.Json.Nodes;
namespace PlanetMeat;
public sealed class Research( Scene scene ) : GameObjectSystem<Research>( scene ), ISaveTarget
{
private readonly List<BaseImprovement> Improvements = [
// drill_gun
new IntEffectImprovement( "drill_dmg", "drill_gun", 5, 5 ) { UpgradeNameIdent = "attack.dmg" },
new PercentageEffectImprovement( "drill_recharge", "drill_gun", 0.8f, 0.1f, 5.0f ) { UpgradeNameIdent = "attack.recharge" },
new IntEffectImprovement( "drill_range", "drill_gun", 1000, 100, Agenda.MAX_SPAWN_DISTANCE ) { UpgradeNameIdent = "targeter.range" },
// drill_hp
new IntEffectImprovement( "drill_hp", "drill_hp", 100, 10 ) { UpgradeNameIdent = "target.hp" },
new PercentageEffectImprovement( "drill_resist_perc", "drill_hp", 0.0f, 0.0025f, 0.2f ) { UpgradeNameIdent = "target.resist_perc", RequiredTech = "resists", CostOffset = 9 },
new IntEffectImprovement( "drill_resist_flat", "drill_hp", 0, 1 ) { UpgradeNameIdent = "target.resist_flat", RequiredTech = "resists", CostOffset = 9 },
new PercentageEffectImprovement( "drill_regen", "drill_hp", 0.0f, 0.005f, 0.1f ) { UpgradeNameIdent = "target.regen", RequiredTech = "regen", CostOffset = 9 },
// flame
new IntEffectImprovement( "flame_dmg", "flame", 1, 1 ) { UpgradeNameIdent = "attack.dmg", CostOffset = 4 },
new PercentageEffectImprovement( "flame_recharge", "flame", 2.0f, 0.2f, 10.0f ) { UpgradeNameIdent = "attack.recharge" },
new IntEffectImprovement( "flame_range", "flame", 350, 10, Agenda.MAX_SPAWN_DISTANCE / 4 ) { UpgradeNameIdent = "targeter.range" },
// mortar
new IntEffectImprovement( "mortar_dmg", "mortar", 8, 2 ) { UpgradeNameIdent = "attack.dmg" },
new IntEffectImprovement( "mortar_size", "mortar", 120, 10, 250 ) { UpgradeNameIdent = "strike.size" },
new PercentageEffectImprovement( "mortar_acc", "mortar", 0.2f, 0.05f, 0.8f ) { UpgradeNameIdent = "strike.acc" },
new FloatEffectImprovement( "mortar_speed", "mortar", 0.25f, 0.05f, 5.0f ) { UpgradeNameIdent = "strike.speed" },
new PercentageEffectImprovement( "mortar_cd", "mortar", 1.0f, 0.1f, 7.5f ) { UpgradeNameIdent = "attack.recharge" },
// heli
new IntEffectImprovement( "heli_dmg", "heli", 1, 1 ) { UpgradeNameIdent = "heli.dmg", CostOffset = 4 },
new PercentageEffectImprovement( "heli_duration", "heli", 0.5f, 0.05f, 3.0f ) { UpgradeNameIdent = "duration" },
new PercentageEffectImprovement( "heli_acc", "heli", 0.5f, 0.05f, 1.0f ) { UpgradeNameIdent = "heli.acc" },
new FloatEffectImprovement( "heli_speed", "heli", 0.3f, 0.05f, 1.0f ) { UpgradeNameIdent = "strike.speed" },
new PercentageEffectImprovement( "heli_cd", "heli", 1.0f, 0.1f, 7.5f ) { UpgradeNameIdent = "attack.recharge" },
// pulse
new IntEffectImprovement( "pulse_dmg", "pulse", 1, 1 ) { UpgradeNameIdent = "attack.dmg" },
new IntEffectImprovement( "pulse_size", "pulse", 50, 10, 500 ) { UpgradeNameIdent = "pulse.size"},
new PercentageEffectImprovement( "pulse_cd", "pulse", 1.0f, 0.1f, 7.5f ) { UpgradeNameIdent = "attack.recharge" },
// goop_econ
new StorageImprovement( "goop_storage", "goop_econ", 100, 25 ) { UpgradeNameIdent = "econ.storage", AccountIdent = "goop" },
new PercentageEffectImprovement( "goop_rate", "goop_econ", 0.0f, 0.01f, 1.0f ) { UpgradeNameIdent = "econ.rate" },
new DoubleEffectImprovement( "goop_cap", "goop_econ", 20, 10 ) { UpgradeNameIdent = "econ.cap" },
new IntEffectImprovement( "goop_income", "goop_econ", 0, 3 ) { UpgradeNameIdent = "econ.income" },
new PercentageEffectImprovement( "goop_bounty", "goop_econ", 1.0f, 0.05f, 5.0f ) { UpgradeNameIdent = "econ.bounty" },
// meat_econ
new IntEffectImprovement( "meat_income", "meat_econ", 0, 1 ) { UpgradeNameIdent = "econ.income", CostOffset = 4 },
new PercentageEffectImprovement( "meat_protect", "meat_econ", 0.4f, 0.01f, 0.8f ) { UpgradeNameIdent = "meat.protect" },
new PercentageEffectImprovement( "meat_rate", "meat_econ", 1.0f, 0.1f ) { UpgradeNameIdent = "meat.rate", CostStart = 49 },
// multishot
new PercentageEffectImprovement( "multishot_rate", "multishot", 0.05f, 0.01f, 0.5f ) { UpgradeNameIdent = "multishot.rate" },
new IntEffectImprovement( "multishot_amt", "multishot", 1, 1, 25 ) { UpgradeNameIdent = "multishot.amt", CostOffset = 9 },
new PercentageEffectImprovement( "multishot_dmg", "multishot", 0.2f, 0.1f, 2.5f ) { UpgradeNameIdent = "multishot.dmg" },
// lifesteal
new PercentageEffectImprovement( "lifesteal_dmg", "lifesteal", 0.01f, 0.01f, 0.25f ) { UpgradeNameIdent = "lifesteal.dmg" },
new IntEffectImprovement( "lifesteal_kill_amt", "lifesteal", 1, 1, 10 ) { UpgradeNameIdent = "lifesteal.kill.amt" },
new PercentageEffectImprovement( "lifesteal_kill_rate", "lifesteal", 0.01f, 0.01f, 0.3f ) { UpgradeNameIdent = "lifesteal.kill.rate" },
// thorns
new PercentageEffectImprovement( "thorns_reflect", "thorns", 0.0f, 0.01f, 2.0f ) { UpgradeNameIdent = "thorns.reflect" },
new IntEffectImprovement( "phoenix_charges", "thorns", 0, 1, 10 ) { UpgradeNameIdent = "phoenix.charges", CostStart = 100 },
new PercentageEffectImprovement( "phoenix_regen", "thorns", 0.25f, 0.05f, 1.0f ) { UpgradeNameIdent = "phoenix.regen" },
new IntEffectImprovement( "phoenix_dmg", "thorns", 50, 25 ) { UpgradeNameIdent = "phoenix.dmg" },
new IntEffectImprovement( "phoenix_range", "thorns", 500, 150, Agenda.MAX_SPAWN_DISTANCE ) { UpgradeNameIdent = "phoenix.range" },
];
private IEnumerable<BaseImprovement> SavedImprovements => Improvements.Append( ImproveResearch );
private readonly Dictionary<string, BaseImprovement> FetchedImprovements = [];
public BaseImprovement GetImprovement( string ident )
{
if ( FetchedImprovements.TryGetValue( ident, out BaseImprovement fetched ) )
return fetched;
var found = Improvements.Find( imp => imp.Ident == ident );
if ( found is null )
Log.Warning( $"No such improvement '{ident}'" );
else
FetchedImprovements[ident] = found;
return found;
}
public T GetImprovementEffect<T>( string ident, T fallback = default ) where T : IComparable<T>, IAdditionOperators<T, T, T>
{
if ( ident != "" )
{
if ( GetImprovement( ident ) is BaseEffectImprovement<T> imp )
return imp.CurrentEffect;
Log.Warning( $"Couldn't find valid '{ident}' improvement, falling back to {fallback}" );
}
return fallback;
}
private void BakeImprovements()
{
Catalogue.Current.BakeUpgrades( Improvements );
}
private readonly Dictionary<string, BaseTech> Techs = new()
{
["mortar"] = new BaseTech( "mortar", 1 ) { ShowTool = true },
["flame"] = new BaseTech( "flame", 1 ) { ShowTool = true },
["heli"] = new BaseTech( "heli", 5 ) { ShowTool = true },
["pulse"] = new BaseTech( "pulse", 9 ) { ShowTool = true },
["skip_rounds"] = new BaseTech( "skip_rounds", 2 ),
["scout"] = new BaseTech( "scout", 1 ),
["resists"] = new BaseTech( "resists", 3 ),
["research_speed"] = new BaseTech( "research_speed", 3 ),
["regen"] = new BaseTech( "regen", 6 ),
["goop_econ"] = new BaseTech( "goop_econ", 6 ),
["multishot"] = new BaseTech( "multishot", 15 ),
["lifesteal"] = new BaseTech( "lifesteal", 18 ),
["meat_econ"] = new BaseTech( "meat_econ", 23 ),
["thorns"] = new BaseTech( "thorns", 30 ),
};
public BaseTech GetTech( string ident ) => Techs.GetValueOrDefault( ident, null );
public List<BaseTech> AvailableTechs
{
get
{
if ( field is null )
BakeTechs();
return field;
}
private set;
} = null;
public List<string> ActiveTechTools
{
get
{
if ( field is null )
BakeTechs();
return field;
}
private set;
} = null;
public BaseTech ResearchTarget
{
get => field ??= GetTech( "mortar" );
set
{
CompletionAlert = false;
IgnoreCompletion = false;
if ( value is null )
{
field = AvailableTechs.FirstOrDefault( t => !t.Completed, field );
}
else if ( !value.Completed )
{
field = value;
}
}
}
public bool IgnoreCompletion { get; private set; } = false;
public bool CompletionAlert { get; private set; } = false;
public ImproveResearch ImproveResearch { get; } = new();
public int ResearchSpeed => ImproveResearch.CurrentEffect;
public int ResearchRoundsRemaining => (int)Math.Ceiling( (float)ResearchTarget.Remaining / ResearchSpeed );
public JsonNode NextSave
{
get
{
var techs = Techs.ToSaveObject();
var improves = new JsonObject();
foreach ( var imp in SavedImprovements )
improves.Add( imp.Ident, imp.NextSave );
return new JsonObject
{
{ "Research", ResearchTarget.Ident },
{ "Techs", techs },
{ "Imps", improves }
};
}
}
public void AdvanceResearchProgress()
{
if ( !ResearchTarget.Completed )
{
ResearchTarget.AdvanceProgress( ResearchSpeed );
if ( ResearchTarget.Completed && !IgnoreCompletion )
{
Career.Current.IncrementStat( "tech" );
CompletionAlert = true;
}
}
}
private void BakeTechs()
{
AvailableTechs = [.. Techs.Values.Where( t => t.Visible )];
AvailableTechs.Sort( ( a, b ) =>
{
var aCompleted = a.Completed;
if ( aCompleted != b.Completed )
return aCompleted ? 1 : -1;
var rem = a.Remaining - b.Remaining;
if ( rem != 0 )
return rem;
return Game.Language.GetPhrase( a.DisplayName ).CompareTo( Game.Language.GetPhrase( b.DisplayName ) );
}
);
ActiveTechTools = [.. Techs.Values.Where( t => t.Completed && t.ShowTool ).Select( t => t.Ident )];
ActiveTechTools.Add( "drill_gun" );
ActiveTechTools.Sort( Catalogue.Current.SortCategories );
}
public bool IsTech( string ident ) => Techs.ContainsKey( ident );
public bool HasTech( string ident ) => ident == "" || GetTech( ident ).Completed;
public void BakeCatalogue()
{
CompletionAlert = false;
IgnoreCompletion = false;
BakeImprovements();
BakeTechs();
if ( ResearchTarget.Completed )
ResearchTarget = null;
}
public void BakeEffects()
{
foreach ( var imp in Improvements )
imp.BakeEffects();
}
public void IgnoreCompletedResearch()
{
IgnoreCompletion = ResearchTarget.Completed;
}
public void LoadFromSave( JsonNode save )
{
if ( save.AsObject() is JsonObject obj )
{
if ( obj.TryGetPropertyNode( "Techs", out JsonObject techs ) )
Techs.LoadFromSaveObject( techs );
if ( obj.TryGetPropertyNode( "Imps", out JsonObject savedImps ) )
{
foreach ( var imp in SavedImprovements )
{
if ( savedImps.TryGetPropertyNode( imp.Ident, out JsonNode savedImp ) )
imp.LoadFromSave( savedImp );
}
}
if ( obj.TryGetTypedProperty( "Research", out string researchIdent ) )
ResearchTarget = GetTech( researchIdent );
BakeCatalogue();
}
}
public void ClearSave()
{
Techs.ClearSavedInfo();
foreach ( var imp in SavedImprovements )
imp.ClearSave();
BakeCatalogue();
ResearchTarget = GetTech( "mortar" );
}
}