Added testing and save/load mechanic to the game. Game is now entering final phase.

This commit is contained in:
2026-05-09 21:25:36 +02:00
parent e7de2433de
commit 7e70471227
18 changed files with 1073 additions and 46 deletions
+10
View File
@@ -427,6 +427,14 @@ text = "Continue"
layout_mode = 2 layout_mode = 2
text = "Options" text = "Options"
[node name="SaveGame" type="Button" parent="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer"]
layout_mode = 2
text = "Save Game"
[node name="LoadGame" type="Button" parent="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer"]
layout_mode = 2
text = "Load Game"
[node name="Button3" type="Button" parent="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer" unique_id=2028306785] [node name="Button3" type="Button" parent="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer" unique_id=2028306785]
layout_mode = 2 layout_mode = 2
text = "Exit" text = "Exit"
@@ -600,6 +608,8 @@ texture_normal = ExtResource("12_3so38")
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/RobotList/VBoxContainer/Spawning/Button" to="CanvasLayer/UIHandler/MainUI/Content/RobotList" method="SpawnRobot"] [connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/RobotList/VBoxContainer/Spawning/Button" to="CanvasLayer/UIHandler/MainUI/Content/RobotList" method="SpawnRobot"]
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/Button" to="CanvasLayer/UIHandler" method="HandleMenu"] [connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/Button" to="CanvasLayer/UIHandler" method="HandleMenu"]
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/Button2" to="CanvasLayer/UIHandler" method="ShowOptions"] [connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/Button2" to="CanvasLayer/UIHandler" method="ShowOptions"]
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/SaveGame" to="CanvasLayer/UIHandler" method="SaveGame"]
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/LoadGame" to="CanvasLayer/UIHandler" method="LoadGame"]
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/Button3" to="CanvasLayer/UIHandler" method="ExitGame"] [connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/Menu/VBoxContainer/Button3" to="CanvasLayer/UIHandler" method="ExitGame"]
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/GameOver/VBoxContainer/HBoxContainer/Negative" to="CanvasLayer/UIHandler" method="ExitGame"] [connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/GameOver/VBoxContainer/HBoxContainer/Negative" to="CanvasLayer/UIHandler" method="ExitGame"]
[connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/GameOver/VBoxContainer/HBoxContainer/Positive" to="CanvasLayer/UIHandler" method="HideGameOver"] [connection signal="pressed" from="CanvasLayer/UIHandler/MainUI/Content/GameOver/VBoxContainer/HBoxContainer/Positive" to="CanvasLayer/UIHandler" method="HideGameOver"]
+7
View File
@@ -123,6 +123,12 @@ theme_override_styles/normal = SubResource("StyleBoxFlat_bnhvo")
theme_override_styles/hover = SubResource("StyleBoxFlat_tt5f1") theme_override_styles/hover = SubResource("StyleBoxFlat_tt5f1")
text = "Start Game" text = "Start Game"
[node name="btnLoad" type="Button" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_styles/normal = SubResource("StyleBoxFlat_bnhvo")
theme_override_styles/hover = SubResource("StyleBoxFlat_tt5f1")
text = "Load Game"
[node name="btnOptions" type="Button" parent="CenterContainer/VBoxContainer" unique_id=891656915] [node name="btnOptions" type="Button" parent="CenterContainer/VBoxContainer" unique_id=891656915]
layout_mode = 2 layout_mode = 2
theme_override_styles/normal = SubResource("StyleBoxFlat_bnhvo") theme_override_styles/normal = SubResource("StyleBoxFlat_bnhvo")
@@ -136,4 +142,5 @@ theme_override_styles/hover = SubResource("StyleBoxFlat_tt5f1")
text = "Exit Game" text = "Exit Game"
[connection signal="button_up" from="CenterContainer/VBoxContainer/btnPlay" to="." method="OnPlayPressed"] [connection signal="button_up" from="CenterContainer/VBoxContainer/btnPlay" to="." method="OnPlayPressed"]
[connection signal="button_up" from="CenterContainer/VBoxContainer/btnLoad" to="." method="OnLoadPressed"]
[connection signal="button_up" from="CenterContainer/VBoxContainer/btnExit" to="." method="OnQuitPressed"] [connection signal="button_up" from="CenterContainer/VBoxContainer/btnExit" to="." method="OnQuitPressed"]
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://Scripts/Tests/TestRunner.cs" id="1_test_runner"]
[node name="TestRunner" type="Node"]
script = ExtResource("1_test_runner")
+33
View File
@@ -23,6 +23,7 @@ public partial class GameData
public static SurvivalState survival = new SurvivalState(); public static SurvivalState survival = new SurvivalState();
public static RobotStats robotStats = new RobotStats(); public static RobotStats robotStats = new RobotStats();
public static Dictionary<int, List<Ingredient>> gateUnlocks; public static Dictionary<int, List<Ingredient>> gateUnlocks;
public static bool loadSaveOnStart = false;
public static Color primaryColor = new Color("#276ac2"); public static Color primaryColor = new Color("#276ac2");
public static Color lightColor = new Color("#7efff5"); public static Color lightColor = new Color("#7efff5");
@@ -32,4 +33,36 @@ public partial class GameData
public static int seed = 12345; public static int seed = 12345;
public static Inventory inventory = new Inventory(); public static Inventory inventory = new Inventory();
public static void ResetRunState()
{
seed = 12345;
ruinSize = 10;
layerSize = 20;
rand = new Random(seed);
survival = new SurvivalState();
robotStats = new RobotStats();
inventory = new Inventory();
availableResearch = ResourceLoader.LoadResearch();
robots.Clear();
currentLayer = 0;
visibleLayer = 0;
lowestLayer = 0;
maxRobotCount = 10;
canMove = true;
}
public static void RebuildRobotStatsFromResearch()
{
robotStats = new RobotStats();
maxRobotCount = 10;
foreach (Research research in availableResearch.Values)
{
if (research.state == ResearchState.RESEARCHED)
{
robotStats.Apply(research.data.Effects);
}
}
}
} }
+8 -8
View File
@@ -99,14 +99,14 @@ public partial class ResourceLoader
{ {
Dictionary<string, float[]> weights = new Dictionary<string, float[]>() Dictionary<string, float[]> weights = new Dictionary<string, float[]>()
{ {
{ "iron_ore", [0.05f,1] }, { "iron_ore", new float[] { 0.05f, 1f } },
{ "tin_ore", [0.3f,0.7f] }, { "tin_ore", new float[] { 0.3f, 0.7f } },
{ "copper_ore", [0.3f,0.7f] }, { "copper_ore", new float[] { 0.3f, 0.7f } },
{ "mushroom", [0.3f,0.1f] }, { "mushroom", new float[] { 0.3f, 0.1f } },
{ "spider_silk", [0.8f,0.4f] }, { "spider_silk", new float[] { 0.8f, 0.4f } },
{ "coal", [1,0.3f] }, { "coal", new float[] { 1f, 0.3f } },
{ "water", [0.4f,0.2f] }, { "water", new float[] { 0.4f, 0.2f } },
{ "stone", [1,0.5f] }, { "stone", new float[] { 1f, 0.5f } },
}; };
return weights; return weights;
} }
+87
View File
@@ -0,0 +1,87 @@
using System.Collections.Generic;
public class SaveGameData
{
public int Seed { get; set; }
public int CurrentLayer { get; set; }
public int VisibleLayer { get; set; }
public int LowestLayer { get; set; }
public int MaxRobotCount { get; set; }
public bool CanMove { get; set; }
public SurvivalSaveData Survival { get; set; }
public List<ItemSaveData> Inventory { get; set; }
public List<ResearchSaveData> Research { get; set; }
public List<LayerSaveData> Layers { get; set; }
public List<RobotSaveData> Robots { get; set; }
}
public class SurvivalSaveData
{
public float Hunger { get; set; }
public float Thirst { get; set; }
public float Energy { get; set; }
public bool IsDead { get; set; }
public string DeathReason { get; set; }
public string CurrentStatus { get; set; }
}
public class ItemSaveData
{
public string Id { get; set; }
public int Amount { get; set; }
}
public class ResearchSaveData
{
public string Id { get; set; }
public ResearchState State { get; set; }
public double ElapsedResearchTime { get; set; }
public bool PaidResources { get; set; }
}
public class LayerSaveData
{
public int Level { get; set; }
public bool IsGateOpen { get; set; }
public bool HasContentGenerated { get; set; }
public List<Ingredient> GateIngredients { get; set; }
public List<string> CurrentResources { get; set; }
public List<TileSaveData> Tiles { get; set; }
}
public class TileSaveData
{
public int X { get; set; }
public int Y { get; set; }
public string CollapsedMesh { get; set; }
public bool ContainsLight { get; set; }
public bool ContainsDecoration { get; set; }
public bool ContainsResource { get; set; }
public bool WasVisited { get; set; }
public ResourceSaveData Resource { get; set; }
}
public class ResourceSaveData
{
public string Name { get; set; }
public int CurrentAmount { get; set; }
public int MaxAmount { get; set; }
public bool IsEndless { get; set; }
public float ExtractionSpeed { get; set; }
public double TimeSinceLastExtraction { get; set; }
}
public class RobotSaveData
{
public string Name { get; set; }
public string CurrentProgram { get; set; }
public string CurrentMessage { get; set; }
public string RobotType { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public float Heat { get; set; }
public float Maintenance { get; set; }
public bool IsCoolingDown { get; set; }
public bool IsBroken { get; set; }
}
+1
View File
@@ -0,0 +1 @@
uid://k530yuk4xt1x
+357
View File
@@ -0,0 +1,357 @@
using Godot;
using System.Collections.Generic;
using System.Text.Json;
public static class SaveGameManager
{
private const string SaveDirectory = "user://savegame";
private const string GameDataPath = SaveDirectory + "/gamedata.json";
private const string RobotsPath = SaveDirectory + "/robots.json";
private const string ResearchPath = SaveDirectory + "/research.json";
private const string LayerPrefix = SaveDirectory + "/layer_";
private const string JsonExtension = ".json";
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public static bool SaveExists()
{
return FileAccess.FileExists(GameDataPath);
}
public static void SaveGame()
{
SaveGameData saveGame = CreateSaveData();
CreateSaveDirectory();
ClearOldLayerFiles();
SaveJson(GameDataPath, CreateCoreSaveData(saveGame));
SaveJson(RobotsPath, saveGame.Robots);
SaveJson(ResearchPath, saveGame.Research);
foreach (LayerSaveData layer in saveGame.Layers)
{
SaveJson(GetLayerPath(layer.Level), layer);
}
}
public static SaveGameData LoadSaveData()
{
if (!SaveExists()) return null;
SaveGameData saveGame = LoadJson<SaveGameData>(GameDataPath);
if (saveGame == null) return null;
saveGame.Robots = LoadJson<List<RobotSaveData>>(RobotsPath) ?? new List<RobotSaveData>();
saveGame.Research = LoadJson<List<ResearchSaveData>>(ResearchPath) ?? new List<ResearchSaveData>();
saveGame.Layers = LoadLayerSaveData();
return saveGame;
}
public static SaveGameData CreateSaveData()
{
return new SaveGameData
{
Seed = GameData.seed,
CurrentLayer = GameData.currentLayer,
VisibleLayer = GameData.visibleLayer,
LowestLayer = GameData.lowestLayer,
MaxRobotCount = GameData.maxRobotCount,
CanMove = GameData.canMove,
Survival = CreateSurvivalSaveData(),
Inventory = CreateInventorySaveData(),
Research = CreateResearchSaveData(),
Layers = CreateLayerSaveData(),
Robots = CreateRobotSaveData()
};
}
public static void ApplyWorldData(SaveGameData saveGame)
{
if (saveGame == null) return;
GameData.seed = saveGame.Seed;
GameData.currentLayer = saveGame.CurrentLayer;
GameData.visibleLayer = saveGame.VisibleLayer;
GameData.lowestLayer = saveGame.LowestLayer;
GameData.maxRobotCount = saveGame.MaxRobotCount;
GameData.canMove = saveGame.CanMove;
ApplySurvivalData(saveGame.Survival);
ApplyInventoryData(saveGame.Inventory);
ApplyResearchData(saveGame.Research);
ApplyLayerData(saveGame.Layers);
}
private static SurvivalSaveData CreateSurvivalSaveData()
{
return new SurvivalSaveData
{
Hunger = GameData.survival.hunger,
Thirst = GameData.survival.thirst,
Energy = GameData.survival.energy,
IsDead = GameData.survival.isDead,
DeathReason = GameData.survival.deathReason,
CurrentStatus = GameData.survival.currentStatus
};
}
private static List<ItemSaveData> CreateInventorySaveData()
{
List<ItemSaveData> result = new List<ItemSaveData>();
foreach (Item item in GameData.inventory.items)
{
result.Add(new ItemSaveData
{
Id = item.data.Id,
Amount = item.currentAmount
});
}
return result;
}
private static List<ResearchSaveData> CreateResearchSaveData()
{
List<ResearchSaveData> result = new List<ResearchSaveData>();
foreach (Research research in GameData.availableResearch.Values)
{
result.Add(new ResearchSaveData
{
Id = research.data.Id,
State = research.state,
ElapsedResearchTime = research.elapsedResearchTime,
PaidResources = research.paidResources
});
}
return result;
}
private static List<LayerSaveData> CreateLayerSaveData()
{
List<LayerSaveData> result = new List<LayerSaveData>();
if (GameData.map == null) return result;
foreach (Layer layer in GameData.map)
{
if (layer == null) continue;
result.Add(new LayerSaveData
{
Level = layer.level,
IsGateOpen = layer.isGateOpen,
HasContentGenerated = layer.hasContentGenerated,
GateIngredients = new List<Ingredient>(layer.gateIngredients),
CurrentResources = new List<string>(layer.currentResources),
Tiles = CreateTileSaveData(layer)
});
}
return result;
}
private static List<TileSaveData> CreateTileSaveData(Layer layer)
{
List<TileSaveData> result = new List<TileSaveData>();
foreach (Tile tile in layer.tiles)
{
result.Add(new TileSaveData
{
X = tile.GridPosition.X,
Y = tile.GridPosition.Y,
CollapsedMesh = tile.collapsedMesh,
ContainsLight = tile.containsLight,
ContainsDecoration = tile.containsDecoration,
ContainsResource = tile.containsResource,
WasVisited = tile.wasVisited,
Resource = tile.resource == null ? null : tile.resource.CreateSaveData()
});
}
return result;
}
private static List<RobotSaveData> CreateRobotSaveData()
{
List<RobotSaveData> result = new List<RobotSaveData>();
foreach (Robot robot in GameData.robots)
{
result.Add(robot.CreateSaveData());
}
return result;
}
private static SaveGameData CreateCoreSaveData(SaveGameData saveGame)
{
return new SaveGameData
{
Seed = saveGame.Seed,
CurrentLayer = saveGame.CurrentLayer,
VisibleLayer = saveGame.VisibleLayer,
LowestLayer = saveGame.LowestLayer,
MaxRobotCount = saveGame.MaxRobotCount,
CanMove = saveGame.CanMove,
Survival = saveGame.Survival,
Inventory = saveGame.Inventory,
Research = new List<ResearchSaveData>(),
Layers = new List<LayerSaveData>(),
Robots = new List<RobotSaveData>()
};
}
private static List<LayerSaveData> LoadLayerSaveData()
{
List<LayerSaveData> result = new List<LayerSaveData>();
for (int i = 0; i < GameData.ruinSize; i++)
{
string path = GetLayerPath(i);
if (!FileAccess.FileExists(path)) continue;
LayerSaveData layer = LoadJson<LayerSaveData>(path);
if (layer != null)
{
result.Add(layer);
}
}
return result;
}
private static string GetLayerPath(int level)
{
return LayerPrefix + level + JsonExtension;
}
private static void CreateSaveDirectory()
{
DirAccess.MakeDirRecursiveAbsolute(SaveDirectory);
}
private static void ClearOldLayerFiles()
{
DirAccess directory = DirAccess.Open(SaveDirectory);
if (directory == null) return;
directory.ListDirBegin();
while (true)
{
string fileName = directory.GetNext();
if (fileName == "") break;
if (directory.CurrentIsDir()) continue;
if (!fileName.StartsWith("layer_") || !fileName.EndsWith(JsonExtension)) continue;
directory.Remove(fileName);
}
directory.ListDirEnd();
}
private static void SaveJson<T>(string path, T data)
{
string json = JsonSerializer.Serialize(data, JsonOptions);
FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
file.StoreString(json);
file.Flush();
}
private static T LoadJson<T>(string path)
{
if (!FileAccess.FileExists(path)) return default;
FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
string json = file.GetAsText();
return JsonSerializer.Deserialize<T>(json);
}
private static void ApplySurvivalData(SurvivalSaveData survival)
{
if (survival == null) return;
GameData.survival.hunger = survival.Hunger;
GameData.survival.thirst = survival.Thirst;
GameData.survival.energy = survival.Energy;
GameData.survival.isDead = survival.IsDead;
GameData.survival.deathReason = survival.DeathReason ?? "";
GameData.survival.currentStatus = survival.CurrentStatus ?? "";
}
private static void ApplyInventoryData(List<ItemSaveData> savedItems)
{
GameData.inventory = new Inventory();
if (savedItems == null) return;
foreach (ItemSaveData savedItem in savedItems)
{
if (!GameData.availableItems.ContainsKey(savedItem.Id)) continue;
GameData.inventory.AddItem(
new Item { data = GameData.availableItems[savedItem.Id] },
savedItem.Amount
);
}
}
private static void ApplyResearchData(List<ResearchSaveData> savedResearch)
{
if (savedResearch == null) return;
foreach (ResearchSaveData savedState in savedResearch)
{
if (!GameData.availableResearch.ContainsKey(savedState.Id)) continue;
Research research = GameData.availableResearch[savedState.Id];
research.state = savedState.State;
research.elapsedResearchTime = savedState.ElapsedResearchTime;
research.paidResources = savedState.PaidResources;
}
GameData.RebuildRobotStatsFromResearch();
}
private static void ApplyLayerData(List<LayerSaveData> savedLayers)
{
if (savedLayers == null || GameData.map == null) return;
foreach (LayerSaveData savedLayer in savedLayers)
{
if (savedLayer.Level < 0 || savedLayer.Level >= GameData.map.Length) continue;
Layer layer = GameData.map[savedLayer.Level];
layer.isGateOpen = savedLayer.IsGateOpen;
layer.hasContentGenerated = savedLayer.HasContentGenerated;
layer.gateIngredients = savedLayer.GateIngredients ?? new List<Ingredient>();
layer.currentResources = savedLayer.CurrentResources ?? new List<string>();
ApplyTileData(layer, savedLayer.Tiles);
}
}
private static void ApplyTileData(Layer layer, List<TileSaveData> savedTiles)
{
if (savedTiles == null) return;
foreach (TileSaveData savedTile in savedTiles)
{
if (savedTile.X < 0 || savedTile.X >= GameData.layerSize) continue;
if (savedTile.Y < 0 || savedTile.Y >= GameData.layerSize) continue;
Tile tile = layer.tiles[savedTile.X, savedTile.Y];
tile.collapsedMesh = savedTile.CollapsedMesh;
tile.containsLight = savedTile.ContainsLight;
tile.containsDecoration = savedTile.ContainsDecoration;
tile.containsResource = savedTile.ContainsResource;
tile.wasVisited = savedTile.WasVisited;
tile.resource = savedTile.Resource == null ? null : GameResource.FromSaveData(savedTile.Resource);
tile.ContentNode.Visible = savedTile.WasVisited || tile.collapsedMesh == "gate";
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://quury78jfutk
+24
View File
@@ -19,6 +19,30 @@ public class GameResource
item = GameData.availableItems[name]; item = GameData.availableItems[name];
} }
public static GameResource FromSaveData(ResourceSaveData saveData)
{
GameResource resource = new GameResource(saveData.Name);
resource.currentAmount = saveData.CurrentAmount;
resource.maxAmount = saveData.MaxAmount;
resource.isEndless = saveData.IsEndless;
resource.extractionSpeed = saveData.ExtractionSpeed;
resource.timeSinceLastExtraction = saveData.TimeSinceLastExtraction;
return resource;
}
public ResourceSaveData CreateSaveData()
{
return new ResourceSaveData
{
Name = name,
CurrentAmount = currentAmount,
MaxAmount = maxAmount,
IsEndless = isEndless,
ExtractionSpeed = extractionSpeed,
TimeSinceLastExtraction = timeSinceLastExtraction
};
}
public bool Extract(double delta) public bool Extract(double delta)
{ {
timeSinceLastExtraction += delta; timeSinceLastExtraction += delta;
+48 -13
View File
@@ -10,31 +10,49 @@ public class Inventory
public bool AddItem(Item item, int amount) public bool AddItem(Item item, int amount)
{ {
Item inventoryItem = items.Find(x => x.data.Id == item.data.Id && x.currentAmount + amount <= x.data.StackSize); if (GetFreeCapacity(item.data.Id, item.data.StackSize) < amount)
if (inventoryItem != null)
{ {
inventoryItem.currentAmount += amount; return false;
NotifyInventoryChanged();
return true;
} }
if (items.Count < maxInventorySize * GameData.maxRobotCount) int remainingAmount = amount;
foreach (Item inventoryItem in items)
{ {
items.Add(item); if (inventoryItem.data.Id != item.data.Id) continue;
items[items.Count - 1].currentAmount += amount; if (inventoryItem.currentAmount >= inventoryItem.data.StackSize) continue;
NotifyInventoryChanged();
return true; int amountToAdd = Math.Min(
remainingAmount,
inventoryItem.data.StackSize - inventoryItem.currentAmount
);
inventoryItem.currentAmount += amountToAdd;
remainingAmount -= amountToAdd;
if (remainingAmount <= 0)
{
NotifyInventoryChanged();
return true;
}
} }
return false; while (remainingAmount > 0 && items.Count < maxInventorySize * GameData.maxRobotCount)
{
int amountToAdd = Math.Min(remainingAmount, item.data.StackSize);
items.Add(new Item { data = item.data, currentAmount = amountToAdd });
remainingAmount -= amountToAdd;
}
NotifyInventoryChanged();
return remainingAmount <= 0;
} }
public bool CanCraft(List<Ingredient> neededIngredients, int amount) public bool CanCraft(List<Ingredient> neededIngredients, int amount)
{ {
foreach (Ingredient ingredient in neededIngredients) foreach (Ingredient ingredient in neededIngredients)
{ {
Item item = items.Find(x => x.data.Id == ingredient.Item && x.currentAmount >= ingredient.Amount * amount); if (GetItemAmount(ingredient.Item) < ingredient.Amount * amount)
if (item == null)
{ {
return false; return false;
} }
@@ -93,4 +111,21 @@ public class Inventory
{ {
OnInventoryUpdate?.Invoke(this, EventArgs.Empty); OnInventoryUpdate?.Invoke(this, EventArgs.Empty);
} }
private int GetFreeCapacity(string id, int stackSize)
{
int freeCapacity = 0;
foreach (Item item in items)
{
if (item.data.Id == id)
{
freeCapacity += stackSize - item.currentAmount;
}
}
int freeSlots = maxInventorySize * GameData.maxRobotCount - items.Count;
freeCapacity += freeSlots * stackSize;
return freeCapacity;
}
} }
+32 -1
View File
@@ -155,6 +155,37 @@ public partial class Robot : Node3D
currentMessage = ""; currentMessage = "";
} }
public RobotSaveData CreateSaveData()
{
return new RobotSaveData
{
Name = Name,
CurrentProgram = currentProgram,
CurrentMessage = currentMessage,
RobotType = robotType,
X = Position.X,
Y = Position.Y,
Z = Position.Z,
Heat = heat,
Maintenance = maintenance,
IsCoolingDown = isCoolingDown,
IsBroken = isBroken
};
}
public void LoadSaveData(RobotSaveData saveData)
{
Name = saveData.Name;
currentProgram = saveData.CurrentProgram;
currentMessage = saveData.CurrentMessage ?? "";
robotType = saveData.RobotType ?? "stone_robot";
Position = new Vector3(saveData.X, saveData.Y, saveData.Z);
heat = saveData.Heat;
maintenance = saveData.Maintenance;
isCoolingDown = saveData.IsCoolingDown;
isBroken = saveData.IsBroken;
}
private bool CanExecute(double delta) private bool CanExecute(double delta)
{ {
if (GameData.survival.isDead) if (GameData.survival.isDead)
@@ -238,4 +269,4 @@ public partial class Robot : Node3D
isCoolingDown = false; isCoolingDown = false;
} }
} }
} }
+7 -7
View File
@@ -3,13 +3,13 @@ using System.Collections.Generic;
public class RobotStats public class RobotStats
{ {
public readonly Dictionary<string, RobotTypeStats> RobotTypes = new() public readonly Dictionary<string, RobotTypeStats> RobotTypes = new Dictionary<string, RobotTypeStats>
{ {
["stone_robot"] = new RobotTypeStats(0.75f, 0.60f, 0.80f, 0.80f, 1.40f, -0.10f), { "stone_robot", new RobotTypeStats(0.75f, 0.60f, 0.80f, 0.80f, 1.40f, -0.10f) },
["copper_robot"] = new RobotTypeStats(1.00f, 1.00f, 1.00f, 1.00f, 1.00f, 0.00f), { "copper_robot", new RobotTypeStats(1.00f, 1.00f, 1.00f, 1.00f, 1.00f, 0.00f) },
["tin_robot"] = new RobotTypeStats(1.00f, 1.00f, 1.00f, 1.00f, 1.00f, 0.00f), { "tin_robot", new RobotTypeStats(1.00f, 1.00f, 1.00f, 1.00f, 1.00f, 0.00f) },
["bronze_robot"] = new RobotTypeStats(1.15f, 1.10f, 0.90f, 1.10f, 0.80f, 0.05f), { "bronze_robot", new RobotTypeStats(1.15f, 1.10f, 0.90f, 1.10f, 0.80f, 0.05f) },
["iron_robot"] = new RobotTypeStats(1.35f, 1.25f, 1.15f, 1.20f, 0.65f, 0.10f) { "iron_robot", new RobotTypeStats(1.35f, 1.25f, 1.15f, 1.20f, 0.65f, 0.10f) }
}; };
private const float BaseMinimumEfficiency = 0.35f; private const float BaseMinimumEfficiency = 0.35f;
@@ -86,7 +86,7 @@ public class RobotStats
break; break;
case "robot_count_increase": case "robot_count_increase":
GameData.maxRobotCount += (int)effect.Value; GameData.maxRobotCount += (int)effect.Value;
break; break;
} }
} }
} }
+383
View File
@@ -0,0 +1,383 @@
using Godot;
using System;
using System.Collections.Generic;
public partial class TestRunner : Node
{
private int passedTests = 0;
private int failedTests = 0;
public override void _Ready()
{
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 death disables movement", TestSurvivalDeathDisablesMovement);
Run("Robot research effects change robot stats", TestRobotResearchEffects);
Run("Research completion applies effects once", TestResearchCompletionAppliesEffectsOnce);
Run("Research execution pays resources and finishes", TestResearchExecutionPaysResourcesAndFinishes);
Run("Inventory add failure keeps inventory unchanged", TestInventoryAddFailureKeepsInventoryUnchanged);
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("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);
Run("While node reports false conditions", TestWhileNodeReportsFalseConditions);
Run("For node stops after configured amount", TestForNodeStopsAfterConfiguredAmount);
Run("Item data readable names are stable", TestItemDataReadableNames);
Run("Resource files load core game data", TestResourceFilesLoadCoreData);
GD.Print($"Tests passed: {passedTests}, failed: {failedTests}");
GetTree().Quit(failedTests == 0 ? 0 : 1);
}
private void Run(string name, Action test)
{
try
{
GameData.ResetRunState();
test();
passedTests++;
GD.Print("[PASS] " + name);
}
catch (Exception exception)
{
failedTests++;
GD.PrintErr("[FAIL] " + name + ": " + exception.Message);
}
}
private void TestInventoryStacksAndRemovesItems()
{
ItemData stone = GameData.availableItems["stone"];
GameData.inventory.AddItem(new Item { data = stone }, stone.StackSize + 5);
AssertEqual(stone.StackSize + 5, GameData.inventory.GetItemAmount("stone"), "stone amount");
AssertEqual(2, GameData.inventory.items.Count, "stone stack count");
bool removed = GameData.inventory.TryRemoveItem("stone", stone.StackSize);
AssertTrue(removed, "remove should succeed");
AssertEqual(5, GameData.inventory.GetItemAmount("stone"), "remaining stone amount");
}
private void TestInventoryCanCraftAcrossStacks()
{
ItemData stone = GameData.availableItems["stone"];
stone.StackSize = 5;
GameData.inventory.AddItem(new Item { data = stone }, 8);
List<Ingredient> ingredients = new List<Ingredient>
{
new Ingredient { Item = "stone", Amount = 8 }
};
AssertTrue(GameData.inventory.CanCraft(ingredients, 1), "crafting should see both stacks");
}
private void TestSurvivalConsumesFoodAndWater()
{
GameData.inventory.AddItem(new Item { data = GameData.availableItems["mushroom"] }, 1);
GameData.inventory.AddItem(new Item { data = GameData.availableItems["water"] }, 1);
GameData.survival.hunger = 30f;
GameData.survival.thirst = 30f;
GameData.survival.Update(0.1);
AssertTrue(GameData.survival.hunger > 60f, "hunger should recover");
AssertTrue(GameData.survival.thirst > 65f, "thirst should recover");
AssertEqual(0, GameData.inventory.GetItemAmount("mushroom"), "mushroom consumed");
AssertEqual(0, GameData.inventory.GetItemAmount("water"), "water consumed");
}
private void TestSurvivalDeathDisablesMovement()
{
GameData.canMove = true;
GameData.survival.energy = 0.01f;
GameData.survival.Update(2.0);
AssertTrue(GameData.survival.isDead, "survival should be dead");
AssertFalse(GameData.canMove, "movement should be disabled");
}
private void TestRobotResearchEffects()
{
RobotStats stats = new RobotStats();
List<ResearchEffect> effects = new List<ResearchEffect>
{
new ResearchEffect { Stat = "robot_speed_bonus", Value = 0.25f },
new ResearchEffect { Stat = "robot_energy_use_reduction", Value = 0.20f },
new ResearchEffect { Stat = "robot_cooling_bonus", Value = 0.50f }
};
stats.Apply(effects);
AssertClose(12.5f, stats.GetMovementSpeed(10f), 0.001f, "movement speed");
AssertClose(8f, stats.GetEnergyUse(10f), 0.001f, "energy use");
AssertClose(15f, stats.GetCoolingRate(10f), 0.001f, "cooling");
}
private void TestResearchCompletionAppliesEffectsOnce()
{
ResearchData data = new ResearchData
{
Id = "test_research",
Inputs = new List<Ingredient>(),
Research = "basics",
CraftTime = 1.0,
Texture = "",
Effects = new List<ResearchEffect>
{
new ResearchEffect { Stat = "robot_speed_bonus", Value = 0.10f }
}
};
Research research = new Research(data);
research.Complete();
research.Complete();
AssertClose(11f, GameData.robotStats.GetMovementSpeed(10f), 0.001f, "research effect applied once");
}
private void TestResourceSaveRoundtrip()
{
GameResource resource = new GameResource("stone");
resource.Extract(2.0);
ResourceSaveData saveData = resource.CreateSaveData();
GameResource loadedResource = GameResource.FromSaveData(saveData);
AssertEqual(saveData.Name, loadedResource.CreateSaveData().Name, "resource name");
AssertEqual(saveData.CurrentAmount, loadedResource.CreateSaveData().CurrentAmount, "resource amount");
}
private void TestRobotSaveRoundtrip()
{
Robot robot = new Robot
{
Name = "Ada",
Position = new Vector3(1f, 2f, 3f),
heat = 44f,
maintenance = 55f,
isBroken = true,
isCoolingDown = true,
currentProgram = "Mining",
currentMessage = "Needs care",
robotType = "bronze_robot"
};
RobotSaveData saveData = robot.CreateSaveData();
Robot loadedRobot = new Robot();
loadedRobot.LoadSaveData(saveData);
AssertEqual("Ada", loadedRobot.Name.ToString(), "robot name");
AssertEqual("Mining", loadedRobot.currentProgram, "robot program");
AssertEqual("bronze_robot", loadedRobot.robotType, "robot type");
AssertClose(44f, loadedRobot.heat, 0.001f, "robot heat");
AssertClose(55f, loadedRobot.maintenance, 0.001f, "robot maintenance");
AssertTrue(loadedRobot.isBroken, "robot broken state");
AssertTrue(loadedRobot.isCoolingDown, "robot cooling state");
}
private void TestSaveDataRestoresGlobalState()
{
GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone"] }, 12);
GameData.survival.hunger = 42f;
GameData.survival.thirst = 43f;
GameData.survival.energy = 44f;
GameData.lowestLayer = 3;
GameData.availableResearch["stoneage"].Complete();
SaveGameData saveData = SaveGameManager.CreateSaveData();
GameData.ResetRunState();
SaveGameManager.ApplyWorldData(saveData);
AssertEqual(12, GameData.inventory.GetItemAmount("stone"), "saved inventory");
AssertClose(42f, GameData.survival.hunger, 0.001f, "saved hunger");
AssertEqual(3, GameData.lowestLayer, "saved layer");
AssertEqual(ResearchState.RESEARCHED, GameData.availableResearch["stoneage"].state, "saved research");
}
private void TestResearchExecutionPaysResourcesAndFinishes()
{
GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone"] }, 5);
ResearchData researchData = new ResearchData
{
Id = "stone_counterweight",
Inputs = new List<Ingredient>
{
new Ingredient { Item = "stone", Amount = 3 }
},
Research = "basics",
CraftTime = 1.0,
Texture = "",
Effects = new List<ResearchEffect>()
};
Research research = new Research(researchData);
ResearchResult result = research.Execute(1.0);
AssertEqual(ResearchResult.FINISHED, result, "research result");
AssertEqual(2, GameData.inventory.GetItemAmount("stone"), "research cost");
AssertEqual(ResearchState.RESEARCHED, research.state, "research state");
}
private void TestInventoryAddFailureKeepsInventoryUnchanged()
{
GameData.maxRobotCount = 1;
GameData.inventory.maxInventorySize = 1;
ItemData stone = GameData.availableItems["stone"];
bool result = GameData.inventory.AddItem(new Item { data = stone }, stone.StackSize + 1);
AssertFalse(result, "add should fail");
AssertEqual(0, GameData.inventory.GetItemAmount("stone"), "failed add should be atomic");
AssertEqual(0, GameData.inventory.items.Count, "failed add should not create stacks");
}
private void TestSplitSaveFilesRoundtrip()
{
GameData.inventory.AddItem(new Item { data = GameData.availableItems["water"] }, 7);
GameData.survival.energy = 77f;
SaveGameManager.SaveGame();
SaveGameData saveData = SaveGameManager.LoadSaveData();
AssertTrue(SaveGameManager.SaveExists(), "save folder should exist");
AssertTrue(FileAccess.FileExists("user://savegame/gamedata.json"), "gamedata file");
AssertTrue(FileAccess.FileExists("user://savegame/robots.json"), "robots file");
AssertTrue(FileAccess.FileExists("user://savegame/research.json"), "research file");
AssertEqual(7, saveData.Inventory[0].Amount, "saved file inventory");
AssertClose(77f, saveData.Survival.Energy, 0.001f, "saved file energy");
}
private void TestSplitSaveFilesIncludeLayerFiles()
{
GameData.ruinSize = 2;
GameData.map = new Layer[2];
GameData.map[0] = CreateTestLayer(0, "spawn");
GameData.map[1] = CreateTestLayer(1, "gate");
SaveGameManager.SaveGame();
SaveGameData saveData = SaveGameManager.LoadSaveData();
AssertTrue(FileAccess.FileExists("user://savegame/layer_0.json"), "layer 0 file");
AssertTrue(FileAccess.FileExists("user://savegame/layer_1.json"), "layer 1 file");
AssertEqual(2, saveData.Layers.Count, "loaded layer count");
AssertEqual("spawn", saveData.Layers[0].Tiles[0].CollapsedMesh, "layer 0 tile");
AssertEqual("gate", saveData.Layers[1].Tiles[0].CollapsedMesh, "layer 1 tile");
}
private void TestIfNodeEvaluatesInventoryComparisons()
{
GameData.inventory.AddItem(new Item { data = GameData.availableItems["stone"] }, 5);
IfNode node = new IfNode
{
selectedItem = new Item { data = GameData.availableItems["stone"] },
amount = 3,
comparator = "is bigger than"
};
AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "if condition true");
node.amount = 8;
AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "if condition false");
}
private void TestWhileNodeReportsFalseConditions()
{
GameData.inventory.AddItem(new Item { data = GameData.availableItems["water"] }, 2);
WhileNode node = new WhileNode
{
selectedItem = new Item { data = GameData.availableItems["water"] },
amount = 5,
comparator = "is bigger than or equal to"
};
AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "while condition false");
}
private void TestForNodeStopsAfterConfiguredAmount()
{
ForNode node = new ForNode
{
amount = 2
};
AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "first iteration");
AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "second iteration");
AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "loop finished");
}
private void TestItemDataReadableNames()
{
AssertEqual("Iron gear", ItemData.GetReadableName("iron_gear"), "readable name");
AssertEqual("iron_gear", ItemData.GetIndex("Iron Gear"), "index name");
}
private void TestResourceFilesLoadCoreData()
{
AssertTrue(GameData.availableItems.ContainsKey("stone"), "stone item loaded");
AssertTrue(GameData.availableItems.ContainsKey("water"), "water item loaded");
AssertTrue(GameData.availableResearch.ContainsKey("basics"), "basics research loaded");
AssertTrue(GameData.availableResearch.ContainsKey("iron_robotics"), "iron robotics research loaded");
}
private Layer CreateTestLayer(int level, string collapsedMesh)
{
Layer layer = new Layer
{
level = level,
currentResources = new List<string> { "stone" },
gateIngredients = new List<Ingredient>(),
tiles = new Tile[1, 1]
};
Tile tile = new Tile
{
GridPosition = new Vector2I(0, 0),
Position = Vector3.Zero,
collapsedMesh = collapsedMesh,
containsResource = true,
resource = new GameResource("stone")
};
layer.tiles[0, 0] = tile;
return layer;
}
private void AssertTrue(bool value, string message)
{
if (!value)
{
throw new Exception(message);
}
}
private void AssertFalse(bool value, string message)
{
if (value)
{
throw new Exception(message);
}
}
private void AssertEqual<T>(T expected, T actual, string message)
{
if (!EqualityComparer<T>.Default.Equals(expected, actual))
{
throw new Exception($"{message}: expected {expected}, got {actual}");
}
}
private void AssertClose(float expected, float actual, float tolerance, string message)
{
if (Math.Abs(expected - actual) > tolerance)
{
throw new Exception($"{message}: expected {expected}, got {actual}");
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://bm6a1hivjtc8e
+13
View File
@@ -107,6 +107,19 @@ public partial class UIHandler : Control
GetTree().ChangeSceneToFile("res://Scenes/MainMenu.tscn"); GetTree().ChangeSceneToFile("res://Scenes/MainMenu.tscn");
} }
public void SaveGame()
{
SaveGameManager.SaveGame();
}
public void LoadGame()
{
if (!SaveGameManager.SaveExists()) return;
GameData.loadSaveOnStart = true;
GetTree().ChangeSceneToFile("res://Scenes/Game.tscn");
}
public void OpenUIElement(Control element) public void OpenUIElement(Control element)
{ {
if (element.Visible) if (element.Visible)
+9
View File
@@ -4,6 +4,15 @@ public partial class MainMenu : Control
{ {
public void OnPlayPressed() public void OnPlayPressed()
{ {
GameData.loadSaveOnStart = false;
GetTree().ChangeSceneToFile("res://Scenes/Game.tscn");
}
public void OnLoadPressed()
{
if (!SaveGameManager.SaveExists()) return;
GameData.loadSaveOnStart = true;
GetTree().ChangeSceneToFile("res://Scenes/Game.tscn"); GetTree().ChangeSceneToFile("res://Scenes/Game.tscn");
} }
+46 -17
View File
@@ -16,6 +16,13 @@ public partial class World : Node3D
public override void _Ready() public override void _Ready()
{ {
bool shouldLoadSave = loadSaveOnStart && SaveGameManager.SaveExists();
SaveGameData saveGame = shouldLoadSave ? SaveGameManager.LoadSaveData() : null;
if (saveGame != null)
{
seed = saveGame.Seed;
}
ResetRunState(); ResetRunState();
WFC.FillAdjacencies(); WFC.FillAdjacencies();
@@ -41,18 +48,27 @@ public partial class World : Node3D
map = new Layer[ruinSize]; map = new Layer[ruinSize];
GenerateWorld(); GenerateWorld();
SetGateRequirements();
if (shouldLoadSave && saveGame != null)
{
SaveGameManager.ApplyWorldData(saveGame);
}
Pathfinding.BuildAStarGraph(); Pathfinding.BuildAStarGraph();
HandleRenderData(BuildRenderData(0)); HandleRenderData(BuildRenderData(visibleLayer));
Robot robot = ResourceLoader.LoadRobotPrefab().Instantiate<Robot>(); if (shouldLoadSave && saveGame != null)
robot.Name = "Bob"; {
robot.Position = map[0].tiles[0, 0].Position; SpawnSavedRobots(saveGame.Robots);
AddChild(robot); }
robots.Add(robot); else
{
SpawnDefaultRobot();
}
SetGateRequirements(); loadSaveOnStart = false;
} }
private Dictionary<string, MultiMeshInstance3D> CreateMultiMeshes(Dictionary<string, Mesh> meshLibrary) private Dictionary<string, MultiMeshInstance3D> CreateMultiMeshes(Dictionary<string, Mesh> meshLibrary)
@@ -94,17 +110,30 @@ public partial class World : Node3D
} }
} }
private void ResetRunState() private void SpawnDefaultRobot()
{ {
survival = new SurvivalState(); Robot robot = ResourceLoader.LoadRobotPrefab().Instantiate<Robot>();
robotStats = new RobotStats(); robot.Name = "Bob";
inventory = new Inventory(); robot.Position = map[0].tiles[0, 0].Position;
availableResearch = ResourceLoader.LoadResearch(); AddChild(robot);
robots.Clear(); robots.Add(robot);
currentLayer = 0; }
visibleLayer = 0;
lowestLayer = 0; private void SpawnSavedRobots(List<RobotSaveData> savedRobots)
canMove = true; {
if (savedRobots == null || savedRobots.Count <= 0)
{
SpawnDefaultRobot();
return;
}
foreach (RobotSaveData savedRobot in savedRobots)
{
Robot robot = ResourceLoader.LoadRobotPrefab().Instantiate<Robot>();
robot.LoadSaveData(savedRobot);
AddChild(robot);
robots.Add(robot);
}
} }
private void GenerateWorld() private void GenerateWorld()