From 538d7d950127ac46264d5fb1bec18e39543d90f3 Mon Sep 17 00:00:00 2001 From: Matt Chambers Date: Thu, 9 Jul 2026 20:23:02 -0400 Subject: [PATCH] Add contract-earned permanent training-rate bonus CrewHandler gains a persisted TrainingRateBonus multiplier, contributed to the RateTraining currency query (OnCurrencyModifierQuery) so it flows through CalculateBuildRate's existing rate line and composes with leader/strategy training modifiers. Covers proficiency and mission training, and recalculates in-progress courses when it changes. New BoostTrainingRate contract behaviour multiplies the bonus on completion (cfg multiplier, stacks multiplicatively). --- Source/RP0/Crew/BoostTrainingRate.cs | 40 +++++++++++++++++++++ Source/RP0/Crew/BoostTrainingRateFactory.cs | 24 +++++++++++++ Source/RP0/Crew/CrewHandler.cs | 39 ++++++++++++++++++++ Source/RP0/RP0.csproj | 2 ++ 4 files changed, 105 insertions(+) create mode 100644 Source/RP0/Crew/BoostTrainingRate.cs create mode 100644 Source/RP0/Crew/BoostTrainingRateFactory.cs 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 @@ + +