Added survival mechanic that consumes inventory items if needed.

This commit is contained in:
2026-05-09 12:22:04 +02:00
parent 8a79a3474e
commit 9f32152fb8
11 changed files with 356 additions and 25 deletions
+1
View File
@@ -20,6 +20,7 @@ public partial class GameData
public static float tileHeight = 4;
public static SortedDictionary<string, ItemData> availableItems = ResourceLoader.LoadItems();
public static Dictionary<string, Research> availableResearch = ResourceLoader.LoadResearch();
public static SurvivalState survival = new SurvivalState();
public static Color primaryColor = new Color("#276ac2");
public static Color lightColor = new Color("#7efff5");
+4 -2
View File
@@ -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;
}
+4 -2
View File
@@ -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;
}
+40 -4
View File
@@ -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()
+105 -14
View File
@@ -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<ProgramNode> nodes = new List<ProgramNode>();
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;
}
}
}
+134
View File
@@ -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;
}
}
@@ -0,0 +1 @@
uid://fpqk4b1v48xy
+18
View File
@@ -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;
}
}
+3 -2
View File
@@ -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;
}
}
+14
View File
@@ -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++)