1. Welcome to the official Starbound Mod repository, Guest! Not sure how to install your mods? Check out the installation guide or check out the modding help thread for more guides.
    Outdated Mods have been moved to their own category! If you update your mod please let a moderator know so we can move it back to the active section.
    Dismiss Notice

Regeneration 1.1.3

Provides configurable health and stamina regeneration.

  1. v1.1.3

    Hammurabi
    An unintended consequence of the v1.1.2 bugfix was that regeneration no longer happened during fishing (the bit where you're waiting for a fish to bite), which I felt seemed far too harsh during playtesting. I've now added options for whether health and stamina should regenerate while fishing.

    Additionally, the way I had the idle times handled in code, they were still able to be reduced by waiting in a menu (or otherwise not really "waiting"). I've reworked them so that they are now durations rather than target times, and they are only reduced during ticks that you would otherwise be regenerating health or stamina (and this is affected by the fishing options).

    Lastly, I added a little bit more config validation, this time making sure that the maximums of the health and stamina ranges, if defined, are greater than the minimums.

    New source code:
    Code:
    using System;
    using StardewModdingAPI;
    using StardewModdingAPI.Events;
    using StardewValley;
    using StardewValley.Tools;
    
    namespace Regeneration {
    
       public class ModConfig {
         public float healthRegenAbsolutePerSecond { get; set; } = 0.1f;
         public float healthRegenPercentagePerSecond { get; set; } = 0.0f;
         public double healthIdleSeconds { get; set; } = 20.0;
         public float regenHealthWhileRunningRate { get; set; } = 0.4f;
         public bool loseFractionalHealthWhenInjured { get; set; } = false;
    
         public float exhaustionHealthRegenRate { get; set; } = 1.0f;
         public double exhaustionHealthIdleMultiplier { get; set; } = 1.0;
    
         public bool regenHealthWhileFishing { get; set; } = true;
    
         public HealthRange[] HealthRanges { get; set; }
    
         public float staminaRegenAbsolutePerSecond { get; set; } = 0.0f;
         public float staminaRegenPercentagePerSecond { get; set; } = (1.0f / 270.0f); // Recover 1 stamina per second at game start, but increase as max stamina increases.
         public double staminaIdleSeconds { get; set; } = 10.0;
         public float regenStaminaWhileRunningRate { get; set; } = 0.4f;
    
         public float exhaustionStaminaRegenRate { get; set; } = 0.75f;
         public double exhaustionStaminaIdleMultiplier { get; set; } = 2;
    
         public bool regenStaminaWhileFishing { get; set; } = true;
    
         public StaminaRange[] StaminaRanges { get; set; }
    
         public bool removeExhaustion { get; set; } = true;
         public float removeExhaustionStaminaPercentage { get; set; } = 0.745f;
    
    
         public ModConfig() {
           HealthRanges = new HealthRange[] {
             new HealthRange() { MaxHealthPercentage = (0.25f), RegenRateMultiplier = 0.5f, IdleTimeMultiplier = (3f/2f), RunRateMulitiplier = 0.5f },
             new HealthRange() { MinHealthPercentage = (0.5f), RegenRateMultiplier = 0 }
           };
    
           StaminaRanges = new StaminaRange[] {
             new StaminaRange() { MaxStaminaPercentage = 0.5f, RegenRateMultiplier = 0.8f, IdleTimeMultiplier = 1.25, RunRateMulitiplier = 0.75f },
             new StaminaRange() { MaxStaminaPercentage = 0.25f, RegenRateMultiplier = 0.75f, IdleTimeMultiplier = 1.4, RunRateMulitiplier = (2f/3f) },
             new StaminaRange() { MinStaminaPercentage = 0.75f, RegenRateMultiplier = 0 }
           };
         }
       }
    
       public struct HealthRange {
         public int MinHealthAbsolute { get; set; }
         public float MinHealthPercentage { get; set; }
         public int MaxHealthAbsolute { get; set; }
         public float MaxHealthPercentage { get; set; }
         public float RegenRateMultiplier { get; set; }
         public float RunRateMulitiplier { get; set; }
         public double IdleTimeMultiplier { get; set; }
    
         public bool WithinRange(int Health, int MaxHealth) {
           if (Health >= MinHealthAbsolute && Health >= (MinHealthPercentage * MaxHealth)) {
             if ((MaxHealthAbsolute == 0 || Health < MaxHealthAbsolute) && (MaxHealthPercentage == 0 || Health < (MaxHealthPercentage * MaxHealth))) {
               return true;
             }
           }
           return false;
         }
    
         public void Validate() {
           if (MinHealthAbsolute < 0) { MinHealthAbsolute = 0; }
           if (MinHealthPercentage < 0) { MinHealthPercentage = 0; }
           else if (MinHealthPercentage > 1) { MinHealthPercentage = 1; }
           if (MaxHealthAbsolute < 0) { MaxHealthAbsolute = 0; }
           if (MaxHealthPercentage < 0) { MaxHealthPercentage = 0; }
           else if (MaxHealthPercentage > 1) { MaxHealthPercentage = 1; }
           if (RunRateMulitiplier < 0) { RunRateMulitiplier = 0; }
           if (IdleTimeMultiplier < 0) { IdleTimeMultiplier = 0; }
    
           if (MaxHealthAbsolute > 0 && MinHealthAbsolute > MaxHealthAbsolute) {
             int Temp = MaxHealthAbsolute;
             MaxHealthAbsolute = MinHealthAbsolute;
             MinHealthAbsolute = Temp;
           }
    
           if (MaxHealthPercentage > 0 && MinHealthPercentage > MaxHealthPercentage) {
             float Temp = MaxHealthPercentage;
             MaxHealthPercentage = MinHealthPercentage;
             MinHealthPercentage = Temp;
           }
         }
       }
    
       public struct StaminaRange {
         public float MinStaminaAbsolute { get; set; }
         public float MinStaminaPercentage { get; set; }
         public float MaxStaminaAbsolute { get; set; }
         public float MaxStaminaPercentage { get; set; }
         public float RegenRateMultiplier { get; set; }
         public float RunRateMulitiplier { get; set; }
         public double IdleTimeMultiplier { get; set; }
    
         public bool WithinRange(float Stamina, float MaxStamina) {
           if (Stamina >= MinStaminaAbsolute && Stamina >= (MinStaminaPercentage * MaxStamina)) {
             if ((MaxStaminaAbsolute == 0 || Stamina < MaxStaminaAbsolute) && (MaxStaminaPercentage == 0 || Stamina < (MaxStaminaPercentage * MaxStamina))) {
               return true;
             }
           }
           return false;
         }
    
         public void Validate() {
           if (MinStaminaAbsolute < 0) { MinStaminaAbsolute = 0; }
           if (MinStaminaPercentage < 0) { MinStaminaPercentage = 0; }
           else if (MinStaminaPercentage > 1) { MinStaminaPercentage = 1; }
           if (MaxStaminaAbsolute < 0) { MaxStaminaAbsolute = 0; }
           if (MaxStaminaPercentage < 0) { MaxStaminaPercentage = 0; }
           else if (MaxStaminaPercentage > 1) { MaxStaminaPercentage = 1; }
           if (RunRateMulitiplier < 0) { RunRateMulitiplier = 0; }
           if (IdleTimeMultiplier < 0) { IdleTimeMultiplier = 0; }
    
           if (MaxStaminaAbsolute > 0 && MinStaminaAbsolute > MaxStaminaAbsolute) {
             float Temp = MaxStaminaAbsolute;
             MaxStaminaAbsolute = MinStaminaAbsolute;
             MinStaminaAbsolute = Temp;
           }
    
           if (MaxStaminaPercentage > 0 && MinStaminaPercentage > MaxStaminaPercentage) {
             float Temp = MaxStaminaPercentage;
             MaxStaminaPercentage = MinStaminaPercentage;
             MinStaminaPercentage = Temp;
           }
         }
       }
    
       public class Regeneration : Mod {
         // Health values
         int lastHealth;
         float healthAccum;  // Accumulated health regenerated while running.
         double healthRegenIdleTime;  // The time when health regeneration should next begin. Updated after taking damage.
    
         // Stamina values
         float lastStamina;  // Last recorded player stamina value.
         double staminaRegenIdleTime;       // The time when stamina regeneration should next begin. Updated after losing stamina.
    
         // Control values
         double lastTickTime;  // The time at the last tick processed.
         bool playerIsRunning;  // Whether the player has run since the preceeding update tick.
    
         Farmer Player;  // Our player.
         ModConfig myConfig;  // Config data.
    
         public override void Entry(IModHelper helper) {
           myConfig = helper.ReadConfig<ModConfig>();
           ValidateConfig();
           helper.WriteConfig<ModConfig>(myConfig);
    
           healthAccum = 0.0f;
           lastHealth = 0;
           lastStamina = 0;
           lastTickTime = 0.0;
           playerIsRunning = false;
    
           GameEvents.UpdateTick += OnUpdateTick;
         }
    
         public void ValidateConfig() {
           // Percentage values should be betweeen 0.0 (0%) and 1.0 (100%).
           myConfig.healthRegenPercentagePerSecond = Math.Min(1.0f, Math.Max(0.0f, myConfig.healthRegenPercentagePerSecond));
           myConfig.staminaRegenPercentagePerSecond = Math.Min(1.0f, Math.Max(0.0f, myConfig.staminaRegenPercentagePerSecond));
    
           myConfig.regenHealthWhileRunningRate = Math.Min(1.0f, Math.Max(0.0f, myConfig.regenHealthWhileRunningRate));
           myConfig.regenStaminaWhileRunningRate = Math.Min(1.0f, Math.Max(0.0f, myConfig.regenStaminaWhileRunningRate));
    
           myConfig.exhaustionHealthRegenRate = Math.Min(1.0f, Math.Max(0.0f, myConfig.exhaustionHealthRegenRate));
           myConfig.exhaustionStaminaRegenRate = Math.Min(1.0f, Math.Max(0.0f, myConfig.exhaustionStaminaRegenRate));
    
           myConfig.removeExhaustionStaminaPercentage = Math.Min(1.0f, Math.Max(0.0f, myConfig.removeExhaustionStaminaPercentage));
    
           // Idle times can't be negative.
           if (myConfig.healthIdleSeconds < 0.0) { myConfig.healthIdleSeconds = 0.0; }
           if (myConfig.staminaIdleSeconds < 0.0) { myConfig.staminaIdleSeconds = 0.0; }
    
           // Exhausted idle times can't be less than base.
           if (myConfig.exhaustionHealthIdleMultiplier < 1.0) { myConfig.exhaustionHealthIdleMultiplier = 1.0; }
           if (myConfig.exhaustionStaminaIdleMultiplier < 1.0) { myConfig.exhaustionStaminaIdleMultiplier = 1.0; }
    
           foreach (HealthRange h in myConfig.HealthRanges) { h.Validate(); }
           foreach (StaminaRange s in myConfig.StaminaRanges) { s.Validate(); }
         }
    
         private void OnUpdateTick(object sender, EventArgs e) {
           if (Game1.hasLoadedGame) {
             Player = Game1.player;
    
             // Make sure we know exactly how much time has elapsed
             double currentTime = Game1.currentGameTime.TotalGameTime.TotalSeconds;
             float timeElapsed = (float) (currentTime - lastTickTime);
             lastTickTime = currentTime;
    
             /* Determine whether movement status will block normal regeneration: If player is...
              * 1. running, and
              * 2. has moved recently, and
              * 3. is not on horseback, then
              * movement blocks normal regen and the running rate prevails. (If the running rate is 0, there is no regeneration.)
             */
             if (Player.running && Player.movedDuringLastTick() && !Player.isRidingHorse()) { playerIsRunning = true; }
             else { playerIsRunning = false; }
    
             bool Fishing = Player.usingTool && Player.CurrentTool is FishingRod && ((FishingRod) Player.CurrentTool).isFishing;
    
             // If game is running, and time can pass (i.e., are not in an event/cutscene/menu/festival), then regenerate health
             if (Game1.shouldTimePass() && (Game1.player.canMove || (Fishing && myConfig.regenHealthWhileFishing))) {
               // Check for player injury. If player has been injured since last tick, recalculate the time when health regeneration should start.
               if (Player.health < lastHealth) {
                 double IdleTime = myConfig.healthIdleSeconds;
                 if (Player.exhausted) { IdleTime *= myConfig.exhaustionHealthIdleMultiplier; }
                 foreach (HealthRange h in myConfig.HealthRanges) {
                   if (h.WithinRange(Player.health, Player.maxHealth)) {
                     IdleTime *= h.IdleTimeMultiplier;
                   }
                 }
                 healthRegenIdleTime = IdleTime;
                 if (myConfig.loseFractionalHealthWhenInjured) { healthAccum = 0; }
               }
               else if (healthRegenIdleTime > 0) {
                 healthRegenIdleTime -= timeElapsed;
               }
    
               // Process health regeneration.
               if (healthRegenIdleTime <= 0 && Player.health < Player.maxHealth) {
                 float absRegen = myConfig.healthRegenAbsolutePerSecond * timeElapsed;
                 float percRegen = myConfig.healthRegenPercentagePerSecond * Player.maxHealth * timeElapsed;
                 float runningModifier = playerIsRunning ? myConfig.regenHealthWhileRunningRate : 1;
                 float regenRangeModifier = Player.exhausted ? myConfig.exhaustionHealthRegenRate : 1;
    
                 foreach (HealthRange h in myConfig.HealthRanges) {
                   if (h.WithinRange(Player.health, Player.maxHealth)) {
                     runningModifier *= playerIsRunning ? h.RunRateMulitiplier : 1;
                     regenRangeModifier *= h.RegenRateMultiplier;
                   }
                 }
    
                 healthAccum += (absRegen + percRegen) * runningModifier * regenRangeModifier;
                 if (healthAccum >= 1) {
                   Player.health += 1;
                   healthAccum -= 1;
                 }
               }
    
               // Updated stored health value.
               lastHealth = Player.health;
             }
    
             // If game is running, and time can pass (i.e., are not in an event/cutscene/menu/festival), then regenerate stamina
             if (Game1.shouldTimePass() && (Game1.player.canMove || (Fishing && myConfig.regenStaminaWhileFishing))) {
               // Check for player exertion. If player has used stamina since last tick, recalculate the time when stamina regeneration should start.
               if (Player.stamina < lastStamina) {
                 double IdleTime = myConfig.staminaIdleSeconds;
                 if (Player.exhausted) { IdleTime *= myConfig.exhaustionStaminaIdleMultiplier; }
                 foreach (StaminaRange s in myConfig.StaminaRanges) {
                   if (s.WithinRange(Player.stamina, Player.maxStamina)) {
                     IdleTime *= s.IdleTimeMultiplier;
                   }
                 }
                 staminaRegenIdleTime = IdleTime;
               }
               else if (staminaRegenIdleTime > 0) {
                 staminaRegenIdleTime -= timeElapsed;
               }
    
               // Process stamina regeneration.
               if (staminaRegenIdleTime <= 0 && Player.stamina < Player.maxStamina) {
                 float absRegen = myConfig.staminaRegenAbsolutePerSecond * timeElapsed;
                 float percRegen = myConfig.staminaRegenPercentagePerSecond * Player.maxStamina * timeElapsed;
                 float runningModifier = playerIsRunning ? myConfig.regenStaminaWhileRunningRate : 1;
                 float regenRangeModifier = Player.exhausted ? myConfig.exhaustionStaminaRegenRate : 1;
    
                 foreach (StaminaRange s in myConfig.StaminaRanges) {
                   if (s.WithinRange(Player.stamina, Player.maxStamina)) {
                     runningModifier *= playerIsRunning ? s.RunRateMulitiplier : 1;
                     regenRangeModifier *= s.RegenRateMultiplier;
                   }
                 }
    
                 Player.stamina += (absRegen + percRegen) * runningModifier * regenRangeModifier;
    
                 // Final sanity check
                 if (Player.stamina > Player.maxStamina) { Player.stamina = Player.maxStamina; }
               }
    
               // Updated stored stamina value.
               lastStamina = Player.stamina;
    
               // Remove exhaustion if the player is sufficiently recovered.
               if (myConfig.removeExhaustion && (Player.stamina / Player.maxStamina) > myConfig.removeExhaustionStaminaPercentage) {
                 Player.exhausted = false;
               }
             }
           }
         }
       }
    }
    
Return to update list...