Added testing and save/load mechanic to the game. Game is now entering final phase.
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://k530yuk4xt1x
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://quury78jfutk
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bm6a1hivjtc8e
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user