diff --git a/Scripts/Core/SaveGameData.cs b/Scripts/Core/SaveGameData.cs index 1ea0d79..281c07f 100644 --- a/Scripts/Core/SaveGameData.cs +++ b/Scripts/Core/SaveGameData.cs @@ -23,6 +23,7 @@ public class SurvivalSaveData public bool IsDead { get; set; } public string DeathReason { get; set; } public string CurrentStatus { get; set; } + public double ElapsedSeconds { get; set; } } public class ItemSaveData diff --git a/Scripts/Core/SaveGameManager.cs b/Scripts/Core/SaveGameManager.cs index 3a7b155..0757918 100644 --- a/Scripts/Core/SaveGameManager.cs +++ b/Scripts/Core/SaveGameManager.cs @@ -95,7 +95,8 @@ public static class SaveGameManager Energy = GameData.survival.energy, IsDead = GameData.survival.isDead, DeathReason = GameData.survival.deathReason, - CurrentStatus = GameData.survival.currentStatus + CurrentStatus = GameData.survival.currentStatus, + ElapsedSeconds = GameData.survival.elapsedSeconds }; } @@ -282,6 +283,7 @@ public static class SaveGameManager GameData.survival.isDead = survival.IsDead; GameData.survival.deathReason = survival.DeathReason ?? ""; GameData.survival.currentStatus = survival.CurrentStatus ?? ""; + GameData.survival.elapsedSeconds = survival.ElapsedSeconds; } private static void ApplyInventoryData(List savedItems) diff --git a/Scripts/Gameplay/Survival/SurvivalState.cs b/Scripts/Gameplay/Survival/SurvivalState.cs index ccb1a19..f298bf2 100644 --- a/Scripts/Gameplay/Survival/SurvivalState.cs +++ b/Scripts/Gameplay/Survival/SurvivalState.cs @@ -6,6 +6,7 @@ public class SurvivalState private const float ThirstDrainPerSecond = 0.018f; private const float PassiveEnergyDrainPerSecond = 0.01f; private const float AutoConsumeThreshold = 30f; + private const double GracePeriodSeconds = 900.0; public float hunger = 100f; public float thirst = 100f; @@ -18,14 +19,19 @@ public class SurvivalState public bool isDead = false; public string deathReason = ""; public string currentStatus = ""; + public double elapsedSeconds = 0; + public bool gracePeriodEnabled = true; public void Update(double delta) { if (isDead) return; - hunger = Math.Clamp(hunger - HungerDrainPerSecond * (float)delta, 0f, maxHunger); - thirst = Math.Clamp(thirst - ThirstDrainPerSecond * (float)delta, 0f, maxThirst); - energy = Math.Clamp(energy - PassiveEnergyDrainPerSecond * (float)delta, 0f, maxEnergy); + elapsedSeconds += delta; + float drainModifier = IsInGracePeriod() ? 0.35f : 1f; + + hunger = Math.Clamp(hunger - HungerDrainPerSecond * drainModifier * (float)delta, 0f, maxHunger); + thirst = Math.Clamp(thirst - ThirstDrainPerSecond * drainModifier * (float)delta, 0f, maxThirst); + energy = Math.Clamp(energy - PassiveEnergyDrainPerSecond * drainModifier * (float)delta, 0f, maxEnergy); TryAutoConsumeFood(); TryAutoConsumeWater(); @@ -106,6 +112,14 @@ public class SurvivalState private void CheckDeath() { + if (IsInGracePeriod()) + { + hunger = Math.Max(hunger, 1f); + thirst = Math.Max(thirst, 1f); + energy = Math.Max(energy, 1f); + return; + } + if (hunger <= 0f) { KillPlayer("Starved"); @@ -131,4 +145,9 @@ public class SurvivalState currentStatus = "Survival failed: " + reason; GameData.canMove = false; } + + private bool IsInGracePeriod() + { + return gracePeriodEnabled && elapsedSeconds < GracePeriodSeconds; + } } diff --git a/Scripts/Tests/TestRunner.cs b/Scripts/Tests/TestRunner.cs index 40ad6f1..253bedf 100644 --- a/Scripts/Tests/TestRunner.cs +++ b/Scripts/Tests/TestRunner.cs @@ -12,6 +12,7 @@ public partial class TestRunner : Node Run("Inventory adds, stacks and removes items", TestInventoryStacksAndRemovesItems); Run("Inventory crafting checks stacked totals", TestInventoryCanCraftAcrossStacks); Run("Survival consumes stored food and water", TestSurvivalConsumesFoodAndWater); + Run("Survival grace period prevents early death", TestSurvivalGracePeriodPreventsEarlyDeath); Run("Survival death disables movement", TestSurvivalDeathDisablesMovement); Run("Robot research effects change robot stats", TestRobotResearchEffects); Run("Research completion applies effects once", TestResearchCompletionAppliesEffectsOnce); @@ -21,6 +22,7 @@ public partial class TestRunner : Node Run("Resource extraction and save data roundtrip", TestResourceSaveRoundtrip); Run("Robot save data roundtrip keeps robot state", TestRobotSaveRoundtrip); Run("Save data captures and restores global state", TestSaveDataRestoresGlobalState); + Run("Save data restores survival timer", TestSaveDataRestoresSurvivalTimer); Run("Split save files store and load data", TestSplitSaveFilesRoundtrip); Run("Split save files include one file per saved layer", TestSplitSaveFilesIncludeLayerFiles); Run("If node evaluates inventory comparisons", TestIfNodeEvaluatesInventoryComparisons); @@ -95,6 +97,7 @@ public partial class TestRunner : Node private void TestSurvivalDeathDisablesMovement() { GameData.canMove = true; + GameData.survival.gracePeriodEnabled = false; GameData.survival.energy = 0.01f; GameData.survival.Update(2.0); @@ -103,6 +106,22 @@ public partial class TestRunner : Node AssertFalse(GameData.canMove, "movement should be disabled"); } + private void TestSurvivalGracePeriodPreventsEarlyDeath() + { + GameData.canMove = true; + GameData.survival.hunger = 0f; + GameData.survival.thirst = 0f; + GameData.survival.energy = 0f; + + GameData.survival.Update(1.0); + + AssertFalse(GameData.survival.isDead, "survival should not fail during grace period"); + AssertTrue(GameData.canMove, "movement should stay enabled during grace period"); + AssertTrue(GameData.survival.hunger > 0f, "hunger should be clamped above zero"); + AssertTrue(GameData.survival.thirst > 0f, "thirst should be clamped above zero"); + AssertTrue(GameData.survival.energy > 0f, "energy should be clamped above zero"); + } + private void TestRobotResearchEffects() { RobotStats stats = new RobotStats(); @@ -202,6 +221,18 @@ public partial class TestRunner : Node AssertEqual(ResearchState.RESEARCHED, GameData.availableResearch["stoneage"].state, "saved research"); } + private void TestSaveDataRestoresSurvivalTimer() + { + GameData.survival.elapsedSeconds = 321.5; + + SaveGameData saveData = SaveGameManager.CreateSaveData(); + + GameData.ResetRunState(); + SaveGameManager.ApplyWorldData(saveData); + + AssertClose(321.5f, (float)GameData.survival.elapsedSeconds, 0.001f, "saved survival timer"); + } + private void TestResearchExecutionPaysResourcesAndFinishes() { GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone"] }, 5);