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

    Hammurabi
    Added exhaustion-related options to health regen.

    Added missing hooks for the exhaustion-related stamina regen settings (whoops!).

    Removed some unused config options that I'd missed previously.

    Added more settings validation checks.


    ----------

    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) {
           Player = Game1.player;
    
           // If game is running, and time can pass (i.e., are not in an event/cutscene/menu/festival)
           if (Game1.hasLoadedGame && Game1.shouldTimePass()) {
             // Make sure we know exactly how much time has elapsed
             double currentTime = Game1.currentGameTime.TotalGameTime.TotalSeconds;
             float timeElapsed = (float) (currentTime - lastTickTime);
             lastTickTime = currentTime;
    
             // 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...