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.2

    Hammurabi
    Fixed a bug that allowed health and stamina regeneration to get "queued up" by going into a menu.

    Prior to this bugfix, when you exited the menu, you would suddenly get health and stamina, at the current regeneration rate, for all of the time spent in the menu, ignoring any caps set by the health and stamina ranges.

    This was caused by the time of the last check only being stored when the regeneration actually happened, rather than during each tick, so when you left the menu the mod would inaccurately calculate a very long elapsed time between ticks.


    New source code:
    Code:
    using System;
    using StardewModdingAPI;
    using StardewModdingAPI.Events;
    using StardewValley;
    
    namespace Regeneration {
    
       public class ModConfig {
         public float healthRegenAbsolutePerSecond { get; set; }
         public float healthRegenPercentagePerSecond { get; set; }
         public double healthIdleSeconds { get; set; }
         public float regenHealthWhileRunningRate { get; set; }
         public bool loseFractionalHealthWhenInjured { get; set; }
    
         public float exhaustionHealthRegenRate { get; set; }
         public double exhaustionHealthIdleMultiplier { get; set; }
    
         public HealthRange[] HealthRanges { get; set; }
    
         public float staminaRegenAbsolutePerSecond { get; set; }
         public float staminaRegenPercentagePerSecond { get; set; }
         public double staminaIdleSeconds { get; set; }
         public float regenStaminaWhileRunningRate { get; set; }
    
         public float exhaustionStaminaRegenRate { get; set; }
         public double exhaustionStaminaIdleMultiplier { get; set; }
    
         public StaminaRange[] StaminaRanges { get; set; }
    
         public bool removeExhaustion { get; set; }
         public float removeExhaustionStaminaPercentage { get; set; }
    
    
         public ModConfig() {
           healthRegenAbsolutePerSecond = 0.1f;
           healthRegenPercentagePerSecond = 0.0f;
           healthIdleSeconds = 20.0;
           regenHealthWhileRunningRate = 0.4f;
           loseFractionalHealthWhenInjured = false;
    
           exhaustionHealthRegenRate = 1.0f;
           exhaustionHealthIdleMultiplier = 1.0;
    
           HealthRanges = new HealthRange[] {
             new HealthRange() { MaxHealthPercentage = (0.25f), RegenRateMultiplier = 0.5f, IdleTimeMultiplier = (3f/2f), RunRateMulitiplier = 0.5f },
             new HealthRange() { MinHealthPercentage = (0.5f), RegenRateMultiplier = 0 }
           };
    
           staminaRegenAbsolutePerSecond = 0.0f;
           staminaRegenPercentagePerSecond = (1.0f / 270.0f); // Recover 1 stamina per second at game start, but increase as max stamina increases.
           staminaIdleSeconds = 10.0;
           regenStaminaWhileRunningRate = 0.25f;
    
           exhaustionStaminaRegenRate = 0.75f;
           exhaustionStaminaIdleMultiplier = 2;
    
           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 }
           };
    
           removeExhaustion = true;
           removeExhaustionStaminaPercentage = 0.745f;
         }
       }
    
       public struct HealthRange {
         public int MinHealthAbsolute { get; set; }
         public float MinHealthPercentage { get; set; }
         public float 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; }
         }
       }
    
       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; }
         }
       }
    
       public class Regeneration : Mod {
         // Health values
         int lastHealth;
         float healthAccum;  // Accumulated health regenerated while running.
         double healthRegenStartTime;  // The time when health regeneration should next begin. Updated after taking damage.
    
         // Stamina values
         float lastStamina;  // Last recorded player stamina value.
         double staminaRegenStartTime;       // 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;
    
             // If game is running, and time can pass (i.e., are not in an event/cutscene/menu/festival)
             if (Game1.shouldTimePass() && Game1.player.canMove) {
    
               // 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;
                   }
                 }
                 healthRegenStartTime = currentTime + IdleTime;
                 if (myConfig.loseFractionalHealthWhenInjured) { healthAccum = 0; }
               }
    
               // 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;
                   }
                 }
                 staminaRegenStartTime = currentTime + IdleTime;
               }
    
               /* 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; }
    
               // Process health regeneration.
               if (healthRegenStartTime <= currentTime && 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;
                 }
               }
    
               // Process stamina regeneration.
               if (staminaRegenStartTime <= currentTime && 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 health/stamina values.
               lastHealth = Player.health;
               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...