diff --git a/Source/RP0/Crew/BoostTrainingRate.cs b/Source/RP0/Crew/BoostTrainingRate.cs
new file mode 100644
index 00000000000..c78abde6229
--- /dev/null
+++ b/Source/RP0/Crew/BoostTrainingRate.cs
@@ -0,0 +1,40 @@
+using Contracts;
+using ContractConfigurator;
+
+namespace ContractConfigurator.RP0
+{
+ ///
+ /// On contract completion, permanently multiplies the astronaut training rate (both proficiency and
+ /// mission training) by multiplier. The bonus is stored on CrewHandler and persists with the save;
+ /// it stacks multiplicatively if more than one such contract is completed.
+ ///
+ public class BoostTrainingRate : ContractBehaviour
+ {
+ protected double multiplier = 1d;
+
+ public BoostTrainingRate() { }
+
+ public BoostTrainingRate(double multiplier)
+ {
+ this.multiplier = multiplier;
+ }
+
+ protected override void OnLoad(ConfigNode node)
+ {
+ base.OnLoad(node);
+ node.TryGetValue("multiplier", ref multiplier);
+ }
+
+ protected override void OnSave(ConfigNode node)
+ {
+ base.OnSave(node);
+ node.AddValue("multiplier", multiplier);
+ }
+
+ protected override void OnCompleted()
+ {
+ base.OnCompleted();
+ global::RP0.Crew.CrewHandler.Instance?.ApplyPermanentTrainingRateBonus(multiplier);
+ }
+ }
+}
diff --git a/Source/RP0/Crew/BoostTrainingRateFactory.cs b/Source/RP0/Crew/BoostTrainingRateFactory.cs
new file mode 100644
index 00000000000..1a31e62b6ad
--- /dev/null
+++ b/Source/RP0/Crew/BoostTrainingRateFactory.cs
@@ -0,0 +1,24 @@
+using Contracts;
+using ContractConfigurator;
+
+namespace ContractConfigurator.RP0
+{
+ public class BoostTrainingRateFactory : BehaviourFactory
+ {
+ protected double multiplier;
+
+ public override bool Load(ConfigNode node)
+ {
+ bool valid = base.Load(node);
+
+ valid &= ConfigNodeUtil.ParseValue(node, "multiplier", x => multiplier = x, this, 1.5, x => Validation.GT(x, 0.0));
+
+ return valid;
+ }
+
+ public override ContractBehaviour Generate(ConfiguredContract contract)
+ {
+ return new BoostTrainingRate(multiplier);
+ }
+ }
+}
diff --git a/Source/RP0/Crew/CrewHandler.cs b/Source/RP0/Crew/CrewHandler.cs
index d1197eccd91..5e65c8178dc 100644
--- a/Source/RP0/Crew/CrewHandler.cs
+++ b/Source/RP0/Crew/CrewHandler.cs
@@ -59,6 +59,11 @@ private static bool ComputeTrainingTypes()
public bool IsMissionTrainingEnabled;
public double ProfTrainRate;
public double MissionTrainRate;
+ // Permanent multiplier on training build rate, earned via contracts (see the BoostTrainingRate
+ // behaviour). 1.0 = no bonus. Persisted with the save, and contributed to the RateTraining currency
+ // query (OnCurrencyQuery) so it flows through CalculateBuildRate's existing rate line and composes
+ // with leader/strategy training modifiers.
+ public double TrainingRateBonus = 1d;
private EventData onKctTechQueuedEvent;
private HashSet _toRemove = new HashSet();
@@ -143,6 +148,7 @@ public override void OnAwake()
GameEvents.OnPartPurchased.Add(OnPartPurchased);
GameEvents.OnGameSettingsApplied.Add(LoadSettings);
GameEvents.onGameStateLoad.Add(LoadSettings);
+ GameEvents.Modifiers.OnCurrencyModifierQuery.Add(OnCurrencyQuery);
TrainingDatabase.EnsureInitialized();
}
@@ -164,12 +170,44 @@ public override void OnLoad(ConfigNode node)
LoadedSaveVersion = VERSION;
+ TrainingRateBonus = 1d;
+ node.TryGetValue(nameof(TrainingRateBonus), ref TrainingRateBonus);
+
KACWrapper.InitKACWrapper();
}
public override void OnSave(ConfigNode node)
{
base.OnSave(node);
+
+ node.AddValue(nameof(TrainingRateBonus), TrainingRateBonus);
+ }
+
+ ///
+ /// Permanently multiplies the astronaut training rate by (stacks
+ /// multiplicatively). Applied to both proficiency and mission training via CalculateBuildRate.
+ ///
+ public void ApplyPermanentTrainingRateBonus(double multiplier)
+ {
+ if (multiplier <= 0d) return;
+ TrainingRateBonus *= multiplier;
+ RP0Debug.Log($"Applied permanent training rate bonus x{multiplier}; total is now x{TrainingRateBonus}");
+
+ foreach (var course in TrainingCourses)
+ course.RecalculateBuildRate();
+ }
+
+ // Contribute the earned training-rate bonus to any RateTraining currency query, the same way leader
+ // and strategy effects do (see Leaders/Effects/CurrencyModifier). CalculateBuildRate already folds in
+ // CurrencyUtils.Rate(RateTraining), so no direct multiply is needed there.
+ private void OnCurrencyQuery(CurrencyModifierQuery qry)
+ {
+ if (TrainingRateBonus == 1d) return;
+ if (qry is CurrencyModifierQueryRP0 qryRP0 &&
+ (qryRP0.reasonRP0 & TransactionReasonsRP0.RateTraining) != 0)
+ {
+ qryRP0.Multiply(CurrencyRP0.Rate, TrainingRateBonus);
+ }
}
public void Process(double UTDiff)
@@ -210,6 +248,7 @@ public void OnDestroy()
GameEvents.OnPartPurchased.Remove(OnPartPurchased);
GameEvents.OnGameSettingsApplied.Remove(LoadSettings);
GameEvents.onGameStateLoad.Remove(LoadSettings);
+ GameEvents.Modifiers.OnCurrencyModifierQuery.Remove(OnCurrencyQuery);
if (onKctTechQueuedEvent != null) onKctTechQueuedEvent.Remove(AddCoursesForQueuedTechNode);
}
diff --git a/Source/RP0/RP0.csproj b/Source/RP0/RP0.csproj
index 5415f33bbbf..ca4ca5692f5 100644
--- a/Source/RP0/RP0.csproj
+++ b/Source/RP0/RP0.csproj
@@ -177,6 +177,8 @@
+
+