From 9f32152fb81496fa2e630988e79204499dcbeeb4 Mon Sep 17 00:00:00 2001 From: Nicola Date: Sat, 9 May 2026 12:22:04 +0200 Subject: [PATCH] Added survival mechanic that consumes inventory items if needed. --- Scenes/Game.tscn | 33 ++++- Scripts/Core/GameData.cs | 1 + Scripts/DSL/Nodes/ExploreNode.cs | 6 +- Scripts/DSL/Nodes/MoveNode.cs | 6 +- Scripts/Gameplay/Crafting/Inventory.cs | 44 +++++- Scripts/Gameplay/Robots/Robot.cs | 119 ++++++++++++++-- Scripts/Gameplay/Survival/SurvivalState.cs | 134 ++++++++++++++++++ .../Gameplay/Survival/SurvivalState.cs.uid | 1 + Scripts/UI/Common/UIHandler.cs | 18 +++ Scripts/UI/Robots/RobotDisplay.cs | 5 +- Scripts/World/World.cs | 14 ++ 11 files changed, 356 insertions(+), 25 deletions(-) create mode 100644 Scripts/Gameplay/Survival/SurvivalState.cs create mode 100644 Scripts/Gameplay/Survival/SurvivalState.cs.uid diff --git a/Scenes/Game.tscn b/Scenes/Game.tscn index e949c93..8198fc7 100644 --- a/Scenes/Game.tscn +++ b/Scenes/Game.tscn @@ -18,6 +18,7 @@ [ext_resource type="Script" uid="uid://drscsrkfphpy7" path="res://Scripts/UI/Research/ResearchList.cs" id="12_4q8tf"] [ext_resource type="Texture2D" uid="uid://dt84awx33mulb" path="res://Assets/Images/ResearchSymbol.png" id="13_alh3a"] [ext_resource type="Texture2D" uid="uid://bmcpkt6mae2qi" path="res://Assets/Images/AlarmSign.png" id="13_x3xnh"] +[ext_resource type="Texture2D" path="res://Assets/Images/Resources/MushroomSymbol.png" id="14_food"] [sub_resource type="CompressedTexture2D" id="CompressedTexture2D_u44n3"] @@ -67,7 +68,7 @@ environment = SubResource("Environment_sb48q") [node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=1558432386] follow_viewport_enabled = true -[node name="UIHandler" type="Control" parent="CanvasLayer" unique_id=1713248285 node_paths=PackedStringArray("codingWindow", "robotList", "mainCam", "map", "FPS", "RAM", "options", "uiContent", "menu", "inventory", "researchList", "robotAlarm")] +[node name="UIHandler" type="Control" parent="CanvasLayer" unique_id=1713248285 node_paths=PackedStringArray("codingWindow", "robotList", "mainCam", "map", "FPS", "RAM", "options", "uiContent", "menu", "inventory", "researchList", "robotAlarm", "energyLabel", "waterLabel", "hungerLabel", "survivalStatus")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -91,6 +92,10 @@ menu = NodePath("MainUI/Content/Menu") inventory = NodePath("MainUI/Content/Inventory") researchList = NodePath("MainUI/Content/Research") robotAlarm = NodePath("MainUI/FooterContainer/HBoxContainer/RobotAlarm") +energyLabel = NodePath("MainUI/HeaderContainer/VBoxContainer/RowEnergy/RichTextLabel") +waterLabel = NodePath("MainUI/HeaderContainer/VBoxContainer/RowWater/RichTextLabel") +hungerLabel = NodePath("MainUI/HeaderContainer/VBoxContainer/RowHunger/RichTextLabel") +survivalStatus = NodePath("MainUI/HeaderContainer/VBoxContainer/SurvivalStatus") [node name="MainUI" type="VBoxContainer" parent="CanvasLayer/UIHandler" unique_id=1437975209] layout_mode = 1 @@ -141,6 +146,32 @@ autowrap_mode = 0 horizontal_alignment = 1 vertical_alignment = 1 +[node name="RowHunger" type="HBoxContainer" parent="CanvasLayer/UIHandler/MainUI/HeaderContainer/VBoxContainer"] +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="CanvasLayer/UIHandler/MainUI/HeaderContainer/VBoxContainer/RowHunger"] +layout_mode = 2 +texture = ExtResource("14_food") +expand_mode = 2 + +[node name="RichTextLabel" type="RichTextLabel" parent="CanvasLayer/UIHandler/MainUI/HeaderContainer/VBoxContainer/RowHunger"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Food: 100/100" +fit_content = true +autowrap_mode = 0 +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="SurvivalStatus" type="RichTextLabel" parent="CanvasLayer/UIHandler/MainUI/HeaderContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Survival stable" +fit_content = true +autowrap_mode = 0 +horizontal_alignment = 1 +vertical_alignment = 1 + [node name="Content" type="Control" parent="CanvasLayer/UIHandler/MainUI" unique_id=45665557] layout_mode = 2 size_flags_vertical = 3 diff --git a/Scripts/Core/GameData.cs b/Scripts/Core/GameData.cs index c92e435..65cba23 100644 --- a/Scripts/Core/GameData.cs +++ b/Scripts/Core/GameData.cs @@ -20,6 +20,7 @@ public partial class GameData public static float tileHeight = 4; public static SortedDictionary availableItems = ResourceLoader.LoadItems(); public static Dictionary availableResearch = ResourceLoader.LoadResearch(); + public static SurvivalState survival = new SurvivalState(); public static Color primaryColor = new Color("#276ac2"); public static Color lightColor = new Color("#7efff5"); diff --git a/Scripts/DSL/Nodes/ExploreNode.cs b/Scripts/DSL/Nodes/ExploreNode.cs index 773e8ef..f064a97 100644 --- a/Scripts/DSL/Nodes/ExploreNode.cs +++ b/Scripts/DSL/Nodes/ExploreNode.cs @@ -42,7 +42,9 @@ public class ExploreNode : ProgramNode Vector3 target = pathPoints[0] - startPosition; float distance = target.Length(); - if (distance < 0.1f * Mathf.Sqrt(GameData.robotSpeed)) + float movementSpeed = robot.GetMovementSpeed(); + + if (distance < 0.1f * Mathf.Sqrt(movementSpeed)) { robot.Position = pathPoints[0]; Vector3I mapIndex = Pathfinding.GetClosestStartPoint(robot.Position); @@ -70,7 +72,7 @@ public class ExploreNode : ProgramNode { robot.LookAt(robot.GlobalPosition + lookDirection, Vector3.Up); } - robot.GlobalPosition += direction * (float)delta * GameData.robotSpeed; + robot.GlobalPosition += direction * (float)delta * movementSpeed; return NodeResult.RUNNING; } diff --git a/Scripts/DSL/Nodes/MoveNode.cs b/Scripts/DSL/Nodes/MoveNode.cs index 78fc252..1c2fcf2 100644 --- a/Scripts/DSL/Nodes/MoveNode.cs +++ b/Scripts/DSL/Nodes/MoveNode.cs @@ -24,7 +24,9 @@ public class MoveNode : ProgramNode Vector3 target = pathPoints[0] - startPosition; float distance = target.Length(); - if (distance < 0.1f) + float movementSpeed = robot.GetMovementSpeed(); + + if (distance < 0.1f * Mathf.Sqrt(movementSpeed)) { robot.Position = pathPoints[0]; Vector3I mapIndex = Pathfinding.GetClosestStartPoint(robot.Position); @@ -51,7 +53,7 @@ public class MoveNode : ProgramNode { robot.LookAt(robot.GlobalPosition + lookDirection, Vector3.Up); } - robot.GlobalPosition += direction * (float)delta * GameData.robotSpeed; + robot.GlobalPosition += direction * (float)delta * movementSpeed; return NodeResult.RUNNING; } diff --git a/Scripts/Gameplay/Crafting/Inventory.cs b/Scripts/Gameplay/Crafting/Inventory.cs index 6236c2e..c5c42f8 100644 --- a/Scripts/Gameplay/Crafting/Inventory.cs +++ b/Scripts/Gameplay/Crafting/Inventory.cs @@ -45,12 +45,48 @@ public class Inventory public void RemoveItem(string id, int amount) { - Item item = items.Find(x => x.data.Id == id && x.currentAmount >= amount); - if (item != null) + TryRemoveItem(id, amount); + } + + public bool TryRemoveItem(string id, int amount) + { + if (GetItemAmount(id) < amount) { - item.currentAmount -= amount; - NotifyInventoryChanged(); + return false; } + + int remainingAmount = amount; + for (int i = items.Count - 1; i >= 0 && remainingAmount > 0; i--) + { + Item item = items[i]; + if (item.data.Id != id) continue; + + int removedAmount = Math.Min(item.currentAmount, remainingAmount); + item.currentAmount -= removedAmount; + remainingAmount -= removedAmount; + + if (item.currentAmount <= 0) + { + items.RemoveAt(i); + } + } + + NotifyInventoryChanged(); + return true; + } + + public int GetItemAmount(string id) + { + int amount = 0; + foreach (Item item in items) + { + if (item.data.Id == id) + { + amount += item.currentAmount; + } + } + + return amount; } private void NotifyInventoryChanged() diff --git a/Scripts/Gameplay/Robots/Robot.cs b/Scripts/Gameplay/Robots/Robot.cs index b848d47..78cd756 100644 --- a/Scripts/Gameplay/Robots/Robot.cs +++ b/Scripts/Gameplay/Robots/Robot.cs @@ -4,37 +4,52 @@ using Godot; public partial class Robot : Node3D { + private const float EnergyUsePerSecond = 0.2f; + private const float HeatGainPerSecond = 7.5f; + private const float ActiveHeatLossPerSecond = 18f; + private const float IdleHeatLossPerSecond = 9f; + private const float CooldownTarget = 35f; + private const float MaintenanceLossPerSecond = 0.04f; + private List nodes = new List(); private bool isExecuting = false; private ProgramNode currentNode; public string currentProgram; public string currentMessage = ""; + public float heat = 0f; + public float maintenance = 100f; + public bool isCoolingDown = false; + public bool isBroken = false; public override void _Process(double delta) { if (isExecuting) { - switch (currentNode.Execute(this, delta)) + if (CanExecute(delta)) { - case NodeResult.SUCCESS: - currentNode = currentNode.nextNode; - if (currentNode == null) - { + switch (currentNode.Execute(this, delta)) + { + case NodeResult.SUCCESS: + currentNode = currentNode.nextNode; + if (currentNode == null) + { + isExecuting = false; + } + break; + case NodeResult.FAILURE: isExecuting = false; - } - break; - case NodeResult.FAILURE: - isExecuting = false; - currentMessage = "(FAILED)" + currentNode.lastExecutionMessage; - break; - case NodeResult.RUNNING: - currentMessage = ""; - break; + currentMessage = "(FAILED)" + currentNode.lastExecutionMessage; + break; + case NodeResult.RUNNING: + currentMessage = ""; + break; + } } } else if (currentMessage.Length <= 0) { + CoolDown(delta, IdleHeatLossPerSecond); currentMessage = "No script executing"; } @@ -50,4 +65,80 @@ public partial class Robot : Node3D isExecuting = true; currentNode = nodes[0]; } + + public float GetMovementSpeed() + { + return GameData.robotSpeed * GetWorkEfficiency(); + } + + public float GetWorkEfficiency() + { + if (isBroken) return 0f; + if (maintenance >= 50f) return 1f; + + return Math.Clamp(0.35f + maintenance / 100f, 0.35f, 1f); + } + + public void Maintain() + { + maintenance = 100f; + isBroken = false; + currentMessage = ""; + } + + private bool CanExecute(double delta) + { + if (GameData.survival.isDead) + { + currentMessage = "Survival failed"; + return false; + } + + if (isBroken) + { + currentMessage = "Maintenance required"; + return false; + } + + if (isCoolingDown) + { + CoolDown(delta, ActiveHeatLossPerSecond); + currentMessage = $"Cooling down ({heat:0}%)"; + return false; + } + + if (!GameData.survival.TryConsumeEnergy(EnergyUsePerSecond * (float)delta)) + { + currentMessage = "Not enough energy"; + return false; + } + + heat = Math.Clamp(heat + HeatGainPerSecond * (float)delta, 0f, 100f); + maintenance = Math.Clamp(maintenance - MaintenanceLossPerSecond * (float)delta, 0f, 100f); + + if (heat >= 100f) + { + isCoolingDown = true; + currentMessage = "Overheated"; + return false; + } + + if (maintenance <= 0f) + { + isBroken = true; + currentMessage = "Maintenance required"; + return false; + } + + return true; + } + + private void CoolDown(double delta, float heatLossPerSecond) + { + heat = Math.Clamp(heat - heatLossPerSecond * (float)delta, 0f, 100f); + if (heat <= CooldownTarget) + { + isCoolingDown = false; + } + } } diff --git a/Scripts/Gameplay/Survival/SurvivalState.cs b/Scripts/Gameplay/Survival/SurvivalState.cs new file mode 100644 index 0000000..235d11f --- /dev/null +++ b/Scripts/Gameplay/Survival/SurvivalState.cs @@ -0,0 +1,134 @@ +using System; + +public class SurvivalState +{ + private const float HungerDrainPerSecond = 0.035f; + private const float ThirstDrainPerSecond = 0.055f; + private const float PassiveEnergyDrainPerSecond = 0.025f; + private const float AutoConsumeThreshold = 35f; + + public float hunger = 100f; + public float thirst = 100f; + public float energy = 100f; + + public float maxHunger = 100f; + public float maxThirst = 100f; + public float maxEnergy = 100f; + + public bool isDead = false; + public string deathReason = ""; + public string currentStatus = ""; + + 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); + + TryAutoConsumeFood(); + TryAutoConsumeWater(); + TryAutoConsumeEnergy(); + UpdateStatus(); + CheckDeath(); + } + + public bool TryConsumeEnergy(float amount) + { + if (amount <= 0f) return true; + if (energy < amount) return false; + + energy -= amount; + return true; + } + + private void TryAutoConsumeFood() + { + if (hunger > AutoConsumeThreshold) return; + if (!GameData.inventory.TryRemoveItem("mushroom", 1)) return; + + hunger = Math.Clamp(hunger + 35f, 0f, maxHunger); + } + + private void TryAutoConsumeWater() + { + if (thirst > AutoConsumeThreshold) return; + if (!GameData.inventory.TryRemoveItem("water", 1)) return; + + thirst = Math.Clamp(thirst + 40f, 0f, maxThirst); + } + + private void TryAutoConsumeEnergy() + { + if (energy > AutoConsumeThreshold) return; + + if (GameData.inventory.TryRemoveItem("battery_v2", 1)) + { + energy = Math.Clamp(energy + 70f, 0f, maxEnergy); + return; + } + + if (GameData.inventory.TryRemoveItem("battery_v1", 1)) + { + energy = Math.Clamp(energy + 45f, 0f, maxEnergy); + return; + } + + if (GameData.inventory.TryRemoveItem("steam", 1)) + { + energy = Math.Clamp(energy + 25f, 0f, maxEnergy); + } + } + + private void UpdateStatus() + { + if (hunger <= AutoConsumeThreshold) + { + currentStatus = "Food supply critical"; + return; + } + + if (thirst <= AutoConsumeThreshold) + { + currentStatus = "Water supply critical"; + return; + } + + if (energy <= AutoConsumeThreshold) + { + currentStatus = "Energy reserves critical"; + return; + } + + currentStatus = "Survival stable"; + } + + private void CheckDeath() + { + if (hunger <= 0f) + { + KillPlayer("Starved"); + return; + } + + if (thirst <= 0f) + { + KillPlayer("Died of thirst"); + return; + } + + if (energy <= 0f) + { + KillPlayer("Life support lost power"); + } + } + + private void KillPlayer(string reason) + { + isDead = true; + deathReason = reason; + currentStatus = "Survival failed: " + reason; + GameData.canMove = false; + } +} diff --git a/Scripts/Gameplay/Survival/SurvivalState.cs.uid b/Scripts/Gameplay/Survival/SurvivalState.cs.uid new file mode 100644 index 0000000..357a62e --- /dev/null +++ b/Scripts/Gameplay/Survival/SurvivalState.cs.uid @@ -0,0 +1 @@ +uid://fpqk4b1v48xy diff --git a/Scripts/UI/Common/UIHandler.cs b/Scripts/UI/Common/UIHandler.cs index ccc2fd2..5ca67f2 100644 --- a/Scripts/UI/Common/UIHandler.cs +++ b/Scripts/UI/Common/UIHandler.cs @@ -16,6 +16,10 @@ public partial class UIHandler : Control [Export] PanelContainer inventory; [Export] ResearchList researchList; [Export] TextureRect robotAlarm; + [Export] RichTextLabel energyLabel; + [Export] RichTextLabel waterLabel; + [Export] RichTextLabel hungerLabel; + [Export] RichTextLabel survivalStatus; private bool receivedRobotJumpSignal = false; public override void _Ready() @@ -89,6 +93,7 @@ public partial class UIHandler : Control double memory = Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024); string memoryDisplay = memory > 1024 ? Math.Round(memory / 1024, 2).ToString() + " GB" : memory.ToString() + " MB"; RAM.Text = memoryDisplay; + DisplaySurvivalStats(); } public void ExitGame() @@ -121,6 +126,11 @@ public partial class UIHandler : Control private void DisplayRobotAlarm() { string messages = ""; + if (GameData.survival.isDead) + { + messages += GameData.survival.currentStatus + "\r"; + } + foreach (Robot robot in GameData.robots) { if (robot.currentMessage.Length > 0) @@ -141,4 +151,12 @@ public partial class UIHandler : Control codingWindow.SetRobot(robot); OpenUIElement(codingWindow); } + + private void DisplaySurvivalStats() + { + energyLabel.Text = $"Energy: {GameData.survival.energy:0}/{GameData.survival.maxEnergy:0}"; + waterLabel.Text = $"Water: {GameData.survival.thirst:0}/{GameData.survival.maxThirst:0}"; + hungerLabel.Text = $"Food: {GameData.survival.hunger:0}/{GameData.survival.maxHunger:0}"; + survivalStatus.Text = GameData.survival.currentStatus; + } } diff --git a/Scripts/UI/Robots/RobotDisplay.cs b/Scripts/UI/Robots/RobotDisplay.cs index 66a6031..347f21c 100644 --- a/Scripts/UI/Robots/RobotDisplay.cs +++ b/Scripts/UI/Robots/RobotDisplay.cs @@ -11,9 +11,10 @@ public partial class RobotDisplay : PanelContainer public override void _Process(double delta) { string programName = robot.currentProgram ?? ""; - if (programName != currentScript.Text) + string status = $"{programName} | Heat {robot.heat:0}% | Maintenance {robot.maintenance:0}%"; + if (status != currentScript.Text) { - currentScript.Text = programName; + currentScript.Text = status; } } diff --git a/Scripts/World/World.cs b/Scripts/World/World.cs index 01db278..9c7e3a5 100644 --- a/Scripts/World/World.cs +++ b/Scripts/World/World.cs @@ -15,6 +15,7 @@ public partial class World : Node3D public override void _Ready() { + ResetRunState(); WFC.FillAdjacencies(); tileMeshes = ResourceLoader.LoadTiles(); @@ -78,6 +79,7 @@ public partial class World : Node3D public override void _Process(double delta) { + GameData.survival.Update(delta); if (!canMove) return; if (Input.IsActionJustPressed("layer_up") && currentLayer > 0) currentLayer--; @@ -89,6 +91,18 @@ public partial class World : Node3D } } + private void ResetRunState() + { + survival = new SurvivalState(); + inventory = new Inventory(); + availableResearch = ResourceLoader.LoadResearch(); + robots.Clear(); + currentLayer = 0; + visibleLayer = 0; + lowestLayer = 0; + canMove = true; + } + private void GenerateWorld() { for (int layer = 0; layer < ruinSize; layer++)