Finished first EA Version #1

Merged
Nicola merged 110 commits from dev into main 2026-05-19 20:01:13 +02:00
54 changed files with 2030 additions and 1745 deletions
Showing only changes of commit 300c8f5a42 - Show all commits
+9
View File
@@ -225,6 +225,7 @@ layout_mode = 2
size_flags_vertical = 3
[node name="CodingWindow" type="PanelContainer" parent="CanvasLayer/UIHandler/MainUI/Content" unique_id=1576652491 node_paths=PackedStringArray("codeBlocks", "editorWindow", "availableScripts", "scriptName", "nameInput")]
visible = false
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
@@ -293,6 +294,14 @@ size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 10
[node name="RichTextLabel" type="RichTextLabel" parent="CanvasLayer/UIHandler/MainUI/Content/CodingWindow/VBoxContainer/Scripting/CodeBlocks/VBoxContainer" unique_id=1914788051]
layout_mode = 2
text = "Click to add"
fit_content = true
autowrap_mode = 0
horizontal_alignment = 1
vertical_alignment = 1
[node name="EditorWindow" type="VBoxContainer" parent="CanvasLayer/UIHandler/MainUI/Content/CodingWindow/VBoxContainer/Scripting" unique_id=919757187]
layout_mode = 2
size_flags_horizontal = 3
+10 -6
View File
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using Godot;
public class FileHandler
public static class FileHandler
{
private const string ScriptDirectory = "user://scripts";
private const string ScriptExtension = ".json";
@@ -17,7 +17,10 @@ public class FileHandler
string path = GetProgramPath(filename);
FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
if (file == null) return;
file.StoreString(content);
file.Flush();
}
public static List<string> LoadProgramNames()
@@ -35,14 +38,13 @@ public class FileHandler
while (true)
{
string fileName = dir.GetNext();
if (fileName == "")
break;
if (fileName == "") break;
if (dir.CurrentIsDir()) continue;
if (!fileName.EndsWith(ScriptExtension)) continue;
if (!dir.CurrentIsDir() && fileName.EndsWith(ScriptExtension))
{
programs.Add(fileName.Replace(ScriptExtension, ""));
}
}
dir.ListDirEnd();
return programs;
@@ -59,6 +61,8 @@ public class FileHandler
}
FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
if (file == null) return "";
return file.GetAsText();
}
+7 -5
View File
@@ -2,7 +2,7 @@ using Godot;
using System.Collections.Generic;
using System.Text.Json;
public partial class ResourceLoader
public static class ResourceLoader
{
private const string LayerPrefabPath = "res://Prefabs/Layer.tscn";
private const string RobotPrefabPath = "res://Prefabs/Robot/Robot.tscn";
@@ -39,7 +39,7 @@ public partial class ResourceLoader
public static Dictionary<string, MeshInstance3D> LoadTiles()
{
Dictionary<string, MeshInstance3D> tileMeshes = new Dictionary<string, MeshInstance3D>();
PackedScene tileCollection = GD.Load<PackedScene>($"res://Assets/Objects/Tiles.glb");
PackedScene tileCollection = GD.Load<PackedScene>("res://Assets/Objects/Tiles.glb");
Node root = tileCollection.Instantiate();
foreach (MeshInstance3D child in root.GetChildren())
{
@@ -52,7 +52,7 @@ public partial class ResourceLoader
public static Dictionary<string, MeshInstance3D> LoadDecorations()
{
Dictionary<string, MeshInstance3D> decorationMeshes = new Dictionary<string, MeshInstance3D>();
PackedScene decorationCollection = GD.Load<PackedScene>($"res://Assets/Objects/Decorations.glb");
PackedScene decorationCollection = GD.Load<PackedScene>("res://Assets/Objects/Decorations.glb");
Node root = decorationCollection.Instantiate();
foreach (MeshInstance3D child in root.GetChildren())
{
@@ -114,8 +114,9 @@ public partial class ResourceLoader
public static SortedDictionary<string, ItemData> LoadItems()
{
FileAccess file = FileAccess.Open(RecipesPath, FileAccess.ModeFlags.Read);
if (file == null) return new SortedDictionary<string, ItemData>();
string json = file.GetAsText();
SortedDictionary<string, ItemData> result = new SortedDictionary<string, ItemData>();
@@ -133,8 +134,9 @@ public partial class ResourceLoader
public static Dictionary<string, Research> LoadResearch()
{
FileAccess file = FileAccess.Open(ResearchPath, FileAccess.ModeFlags.Read);
if (file == null) return new Dictionary<string, Research>();
string json = file.GetAsText();
Dictionary<string, Research> result = new Dictionary<string, Research>();
+148
View File
@@ -0,0 +1,148 @@
using Godot;
using System.Collections.Generic;
public static class SaveGameDataApplier
{
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;
ApplySettingsData(saveGame.Settings);
ApplySurvivalData(saveGame.Survival);
ApplyInventoryData(saveGame.Inventory);
ApplyResearchData(saveGame.Research);
ApplyLayerData(saveGame.Layers);
}
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 ?? "";
GameData.survival.elapsedSeconds = survival.ElapsedSeconds;
}
private static void ApplySettingsData(SettingsSaveData settings)
{
if (settings == null) return;
GameData.screenMode = settings.ScreenMode;
GameData.soundVolume = settings.SoundVolume;
GameData.lightColor = new Color(
settings.LightColorR,
settings.LightColorG,
settings.LightColorB,
settings.LightColorA
);
ApplyScreenMode(settings.ScreenMode);
SoundManager.SetMasterVolume(settings.SoundVolume);
LightHandler.RedrawLights(GameData.lightColor);
}
private static void ApplyScreenMode(int screenMode)
{
switch (screenMode)
{
case 0:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false);
break;
case 1:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false);
break;
case 2:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true);
break;
default:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false);
break;
}
}
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" && !layer.isGateOpen);
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://jv7k2npga7u0
+160
View File
@@ -0,0 +1,160 @@
using System.Collections.Generic;
public static class SaveGameDataFactory
{
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,
Settings = CreateSettingsSaveData(),
Survival = CreateSurvivalSaveData(),
Inventory = CreateInventorySaveData(),
Research = CreateResearchSaveData(),
Layers = CreateLayerSaveData(),
Robots = CreateRobotSaveData()
};
}
public 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,
Settings = saveGame.Settings,
Survival = saveGame.Survival,
Inventory = saveGame.Inventory,
Research = new List<ResearchSaveData>(),
Layers = new List<LayerSaveData>(),
Robots = new List<RobotSaveData>()
};
}
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,
ElapsedSeconds = GameData.survival.elapsedSeconds
};
}
private static SettingsSaveData CreateSettingsSaveData()
{
return new SettingsSaveData
{
ScreenMode = GameData.screenMode,
SoundVolume = GameData.soundVolume,
LightColorR = GameData.lightColor.R,
LightColorG = GameData.lightColor.G,
LightColorB = GameData.lightColor.B,
LightColorA = GameData.lightColor.A
};
}
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;
}
}
+1
View File
@@ -0,0 +1 @@
uid://b87q17gdv4pfh
+7 -288
View File
@@ -27,7 +27,7 @@ public static class SaveGameManager
CreateSaveDirectory();
ClearOldLayerFiles();
SaveJson(GameDataPath, CreateCoreSaveData(saveGame));
SaveJson(GameDataPath, SaveGameDataFactory.CreateCoreSaveData(saveGame));
SaveJson(RobotsPath, saveGame.Robots);
SaveJson(ResearchPath, saveGame.Research);
@@ -53,176 +53,12 @@ public static class SaveGameManager
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,
Settings = CreateSettingsSaveData(),
Survival = CreateSurvivalSaveData(),
Inventory = CreateInventorySaveData(),
Research = CreateResearchSaveData(),
Layers = CreateLayerSaveData(),
Robots = CreateRobotSaveData()
};
return SaveGameDataFactory.CreateSaveData();
}
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;
ApplySettingsData(saveGame.Settings);
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,
ElapsedSeconds = GameData.survival.elapsedSeconds
};
}
private static SettingsSaveData CreateSettingsSaveData()
{
return new SettingsSaveData
{
ScreenMode = GameData.screenMode,
SoundVolume = GameData.soundVolume,
LightColorR = GameData.lightColor.R,
LightColorG = GameData.lightColor.G,
LightColorB = GameData.lightColor.B,
LightColorA = GameData.lightColor.A
};
}
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,
Settings = saveGame.Settings,
Survival = saveGame.Survival,
Inventory = saveGame.Inventory,
Research = new List<ResearchSaveData>(),
Layers = new List<LayerSaveData>(),
Robots = new List<RobotSaveData>()
};
SaveGameDataApplier.ApplyWorldData(saveGame);
}
private static List<LayerSaveData> LoadLayerSaveData()
@@ -276,6 +112,8 @@ public static class SaveGameManager
{
string json = JsonSerializer.Serialize(data, JsonOptions);
FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
if (file == null) return;
file.StoreString(json);
file.Flush();
}
@@ -285,128 +123,9 @@ public static class SaveGameManager
if (!FileAccess.FileExists(path)) return default;
FileAccess file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
if (file == null) return default;
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 ?? "";
GameData.survival.elapsedSeconds = survival.ElapsedSeconds;
}
private static void ApplySettingsData(SettingsSaveData settings)
{
if (settings == null) return;
GameData.screenMode = settings.ScreenMode;
GameData.soundVolume = settings.SoundVolume;
GameData.lightColor = new Color(
settings.LightColorR,
settings.LightColorG,
settings.LightColorB,
settings.LightColorA
);
ApplyScreenMode(settings.ScreenMode);
SoundManager.SetMasterVolume(settings.SoundVolume);
LightHandler.RedrawLights(GameData.lightColor);
}
private static void ApplyScreenMode(int screenMode)
{
switch (screenMode)
{
case 0:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false);
break;
case 1:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false);
break;
case 2:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true);
break;
}
}
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" && !layer.isGateOpen);
}
}
}
-3
View File
@@ -13,12 +13,9 @@ public partial class SteamworksHandler : Node
SteamInitExStatus status = Steam.SteamInitEx(false).Status;
if (status != 0)
{
GD.Print("Steam not initialized!");
return;
}
GD.Print("Steam initialized!");
GD.Print("User: " + Steam.GetPersonaName());
isSteamInitialized = true;
}
+71 -34
View File
@@ -7,30 +7,26 @@ public class ExploreNode : ProgramNode
public Vector3 startPosition;
public Vector3I targetPosition;
public List<Vector3> pathPoints;
public ExploreNode()
{
DisplayText = "Explore";
}
public override NodeResult Execute(Robot robot, double delta)
{
if (pathPoints == null)
{
int safetyCounter = 0;
int layerRange = Math.Max(GameData.lowestLayer, 1);
while (true)
{
targetPosition = new Vector3I(GameData.rand.Next(GameData.layerSize), GameData.rand.Next(layerRange), GameData.rand.Next(GameData.layerSize));
if (!GameData.map[targetPosition.Y].tiles[targetPosition.X, targetPosition.Z].wasVisited) break;
safetyCounter++;
if (safetyCounter > Math.Pow(GameData.layerSize, 2) * 2)
if (pathPoints == null && !TrySelectTarget())
{
lastExecutionMessage = "No tiles left to explore";
return NodeResult.SUCCESS;
}
}
}
pathPoints ??= new List<Vector3>(Pathfinding.GetPath(Pathfinding.GetClosestStartPoint(robot.Position), targetPosition));
if (pathPoints == null)
{
pathPoints = new List<Vector3>(
Pathfinding.GetPath(Pathfinding.GetClosestStartPoint(robot.Position), targetPosition)
);
}
if (pathPoints.Count <= 0)
{
@@ -38,52 +34,93 @@ public class ExploreNode : ProgramNode
return NodeResult.FAILURE;
}
return MoveAlongPath(robot, delta);
}
private bool TrySelectTarget()
{
int safetyCounter = 0;
int layerRange = Math.Max(GameData.lowestLayer, 1);
int maximumAttempts = (int)Math.Pow(GameData.layerSize, 2) * 2;
while (safetyCounter <= maximumAttempts)
{
targetPosition = new Vector3I(
GameData.rand.Next(GameData.layerSize),
GameData.rand.Next(layerRange),
GameData.rand.Next(GameData.layerSize)
);
if (!GameData.map[targetPosition.Y].tiles[targetPosition.X, targetPosition.Z].wasVisited)
{
return true;
}
safetyCounter++;
}
return false;
}
private NodeResult MoveAlongPath(Robot robot, double delta)
{
startPosition = robot.Position;
Vector3 target = pathPoints[0] - startPosition;
float distance = target.Length();
float movementSpeed = robot.GetMovementSpeed();
if (distance < 0.1f * Mathf.Sqrt(movementSpeed))
{
return FinishCurrentStep(robot);
}
Vector3 direction = target / distance;
RotateRobot(robot, direction);
robot.GlobalPosition += direction * (float)delta * movementSpeed;
return NodeResult.RUNNING;
}
private NodeResult FinishCurrentStep(Robot robot)
{
robot.Position = pathPoints[0];
VisitCurrentTile(robot);
pathPoints.RemoveAt(0);
if (pathPoints.Count > 0)
{
lastExecutionMessage = "";
return NodeResult.RUNNING;
}
lastExecutionMessage = "Current exploration finished";
pathPoints = null;
return NodeResult.RUNNING;
}
private void VisitCurrentTile(Robot robot)
{
Vector3I mapIndex = Pathfinding.GetClosestStartPoint(robot.Position);
Tile tile = GameData.map[mapIndex.Y].tiles[mapIndex.X, mapIndex.Z];
if (!tile.wasVisited)
{
tile.VisitTile();
}
}
pathPoints.Remove(pathPoints[0]);
if (pathPoints.Count <= 0)
private void RotateRobot(Robot robot, Vector3 direction)
{
lastExecutionMessage = "Current exploration finished";
pathPoints = null;
return NodeResult.RUNNING;
}
lastExecutionMessage = "";
return NodeResult.RUNNING;
}
Vector3 direction = target / distance;
Vector3 lookDirection = new Vector3(direction.X, 0, direction.Z);
if (lookDirection.Length() > 0.1f)
{
robot.LookAt(robot.GlobalPosition + lookDirection, Vector3.Up);
}
robot.GlobalPosition += direction * (float)delta * movementSpeed;
if (lookDirection.Length() <= 0.1f) return;
return NodeResult.RUNNING;
robot.LookAt(robot.GlobalPosition + lookDirection, Vector3.Up);
}
public override ProgramNode Duplicate()
{
ExploreNode duplicate = new ExploreNode
return new ExploreNode
{
targetPosition = targetPosition
};
return duplicate;
}
public override string Save()
+2 -16
View File
@@ -9,6 +9,7 @@ public class ForNode : ProgramNode
{
DisplayText = "For";
}
public override NodeResult Execute(Robot robot, double delta)
{
bool isConditionFulfilled = DetermineCondition();
@@ -41,21 +42,6 @@ public class ForNode : ProgramNode
Dictionary<StringName, ProgramNode> availableNodes
)
{
nextNode = null;
NegativeNode = null;
foreach (Godot.Collections.Dictionary connection in connections)
{
int port = (int)connection["from_port"];
ProgramNode connectedNode = GetConnectedNode(connection, availableNodes);
if (port == 0)
{
nextNode = connectedNode;
}
else
{
NegativeNode = connectedNode;
}
}
SetBranchNodes(connections, availableNodes);
}
}
+7 -10
View File
@@ -6,12 +6,13 @@ public class HarvestNode : ProgramNode
{
DisplayText = "Harvest";
}
public override NodeResult Execute(Robot robot, double delta)
{
Vector3I mapIndex = Pathfinding.GetClosestStartPoint(robot.Position);
Tile tile = GameData.map[mapIndex.Y].tiles[mapIndex.X, mapIndex.Z];
if (!tile.containsResource)
if (!tile.containsResource || tile.resource == null)
{
lastExecutionMessage = "No resource on this tile";
return NodeResult.FAILURE;
@@ -23,26 +24,22 @@ public class HarvestNode : ProgramNode
return NodeResult.SUCCESS;
}
if (tile.resource.Extract(delta))
{
if (!tile.resource.Extract(delta)) return NodeResult.RUNNING;
SoundManager.PlayMining();
if (!GameData.inventory.AddItem(new Item { data = tile.resource.item }, 1))
{
lastExecutionMessage = "Not enough space";
return NodeResult.FAILURE;
}
else
{
lastExecutionMessage = "";
return NodeResult.SUCCESS;
}
}
return NodeResult.RUNNING;
}
public override ProgramNode Duplicate()
{
HarvestNode duplicate = new HarvestNode();
return duplicate;
return new HarvestNode();
}
public override string Save()
+10 -30
View File
@@ -10,6 +10,7 @@ public class IfNode : ProgramNode
{
DisplayText = "If";
}
public override NodeResult Execute(Robot robot, double delta)
{
if (selectedItem == null)
@@ -25,21 +26,15 @@ public class IfNode : ProgramNode
private bool DetermineCondition()
{
int inventoryAmount = GameData.inventory.GetItemAmount(selectedItem.data.Id);
switch (comparator)
return comparator switch
{
case "is bigger than":
return inventoryAmount > amount;
case "is less than":
return inventoryAmount < amount;
case "is not":
return inventoryAmount != amount;
case "is less than or equal to":
return inventoryAmount <= amount;
case "is bigger than or equal to":
return inventoryAmount >= amount;
default:
return inventoryAmount == amount;
}
"is bigger than" => inventoryAmount > amount,
"is less than" => inventoryAmount < amount,
"is not" => inventoryAmount != amount,
"is less than or equal to" => inventoryAmount <= amount,
"is bigger than or equal to" => inventoryAmount >= amount,
_ => inventoryAmount == amount
};
}
public override ProgramNode Duplicate()
@@ -63,21 +58,6 @@ public class IfNode : ProgramNode
Dictionary<StringName, ProgramNode> availableNodes
)
{
nextNode = null;
NegativeNode = null;
foreach (Godot.Collections.Dictionary connection in connections)
{
int port = (int)connection["from_port"];
ProgramNode connectedNode = GetConnectedNode(connection, availableNodes);
if (port == 0)
{
nextNode = connectedNode;
}
else
{
NegativeNode = connectedNode;
}
}
SetBranchNodes(connections, availableNodes);
}
}
+41 -22
View File
@@ -6,14 +6,19 @@ public class MoveNode : ProgramNode
public Vector3 startPosition;
public Vector3I targetPosition;
public List<Vector3> pathPoints;
public MoveNode()
{
DisplayText = "Move";
}
public override NodeResult Execute(Robot robot, double delta)
{
Vector3I closestPosition = Pathfinding.GetClosestStartPoint(robot.Position);
pathPoints ??= new List<Vector3>(Pathfinding.GetPath(closestPosition, targetPosition));
if (pathPoints == null)
{
pathPoints = new List<Vector3>(Pathfinding.GetPath(closestPosition, targetPosition));
}
if (pathPoints.Count <= 0)
{
@@ -22,56 +27,70 @@ public class MoveNode : ProgramNode
lastExecutionMessage = "";
return NodeResult.SUCCESS;
}
lastExecutionMessage = "No path available";
return NodeResult.FAILURE;
}
return MoveAlongPath(robot, delta);
}
private NodeResult MoveAlongPath(Robot robot, double delta)
{
startPosition = robot.Position;
Vector3 target = pathPoints[0] - startPosition;
float distance = target.Length();
float movementSpeed = robot.GetMovementSpeed();
if (distance < 0.1f * Mathf.Sqrt(movementSpeed))
{
return FinishCurrentStep(robot);
}
Vector3 direction = target / distance;
RotateRobot(robot, direction);
robot.GlobalPosition += direction * (float)delta * movementSpeed;
return NodeResult.RUNNING;
}
private NodeResult FinishCurrentStep(Robot robot)
{
robot.Position = pathPoints[0];
VisitCurrentTile(robot);
pathPoints.RemoveAt(0);
lastExecutionMessage = "";
if (pathPoints.Count > 0) return NodeResult.RUNNING;
pathPoints = null;
return NodeResult.SUCCESS;
}
private void VisitCurrentTile(Robot robot)
{
Vector3I mapIndex = Pathfinding.GetClosestStartPoint(robot.Position);
Tile tile = GameData.map[mapIndex.Y].tiles[mapIndex.X, mapIndex.Z];
if (!tile.wasVisited)
{
tile.VisitTile();
}
}
pathPoints.Remove(pathPoints[0]);
if (pathPoints.Count <= 0)
private void RotateRobot(Robot robot, Vector3 direction)
{
pathPoints = null;
lastExecutionMessage = "";
return NodeResult.SUCCESS;
}
lastExecutionMessage = "";
return NodeResult.RUNNING;
}
Vector3 direction = target / distance;
Vector3 lookDirection = new Vector3(direction.X, 0, direction.Z);
if (lookDirection.Length() > 0.1f)
{
robot.LookAt(robot.GlobalPosition + lookDirection, Vector3.Up);
}
robot.GlobalPosition += direction * (float)delta * movementSpeed;
if (lookDirection.Length() <= 0.1f) return;
return NodeResult.RUNNING;
robot.LookAt(robot.GlobalPosition + lookDirection, Vector3.Up);
}
public override ProgramNode Duplicate()
{
MoveNode duplicate = new MoveNode
return new MoveNode
{
targetPosition = targetPosition
};
return duplicate;
}
public override string Save()
+22
View File
@@ -24,6 +24,28 @@ public abstract class ProgramNode
nextNode = GetConnectedNode(connections[0], availableNodes);
}
protected void SetBranchNodes(
List<Godot.Collections.Dictionary> connections,
Dictionary<StringName, ProgramNode> availableNodes
)
{
nextNode = null;
NegativeNode = null;
foreach (Godot.Collections.Dictionary connection in connections)
{
ProgramNode connectedNode = GetConnectedNode(connection, availableNodes);
if ((int)connection["from_port"] == 0)
{
nextNode = connectedNode;
}
else
{
NegativeNode = connectedNode;
}
}
}
protected ProgramNode GetConnectedNode(
Godot.Collections.Dictionary connection,
Dictionary<StringName, ProgramNode> availableNodes
+10 -31
View File
@@ -10,9 +10,9 @@ public class WhileNode : ProgramNode
{
DisplayText = "While";
}
public override NodeResult Execute(Robot robot, double delta)
{
if (selectedItem == null)
{
lastExecutionMessage = "No Item selected";
@@ -26,21 +26,15 @@ public class WhileNode : ProgramNode
private bool DetermineCondition()
{
int inventoryAmount = GameData.inventory.GetItemAmount(selectedItem.data.Id);
switch (comparator)
return comparator switch
{
case "is bigger than":
return inventoryAmount > amount;
case "is less than":
return inventoryAmount < amount;
case "is not":
return inventoryAmount != amount;
case "is less than or equal to":
return inventoryAmount <= amount;
case "is bigger than or equal to":
return inventoryAmount >= amount;
default:
return inventoryAmount == amount;
}
"is bigger than" => inventoryAmount > amount,
"is less than" => inventoryAmount < amount,
"is not" => inventoryAmount != amount,
"is less than or equal to" => inventoryAmount <= amount,
"is bigger than or equal to" => inventoryAmount >= amount,
_ => inventoryAmount == amount
};
}
public override ProgramNode Duplicate()
@@ -64,21 +58,6 @@ public class WhileNode : ProgramNode
Dictionary<StringName, ProgramNode> availableNodes
)
{
nextNode = null;
NegativeNode = null;
foreach (Godot.Collections.Dictionary connection in connections)
{
int port = (int)connection["from_port"];
ProgramNode connectedNode = GetConnectedNode(connection, availableNodes);
if (port == 0)
{
nextNode = connectedNode;
}
else
{
NegativeNode = connectedNode;
}
}
SetBranchNodes(connections, availableNodes);
}
}
-3
View File
@@ -1,3 +0,0 @@
public class Building
{
}
@@ -1 +0,0 @@
uid://cl2yvllo35qbb
+15 -12
View File
@@ -29,13 +29,10 @@ public class GameResource
currentAmount = saveData.CurrentAmount,
maxAmount = saveData.MaxAmount,
isEndless = saveData.IsEndless,
extractionSpeed = saveData.ExtractionSpeed
extractionSpeed = saveData.ExtractionSpeed,
timeSinceLastExtraction = saveData.TimeSinceLastExtraction
};
if (resource.isEndless && resource.extractionSpeed <= NormalExtractionSpeed)
{
resource.extractionSpeed = EndlessExtractionSpeed;
}
resource.timeSinceLastExtraction = saveData.TimeSinceLastExtraction;
resource.NormalizeExtractionSpeed();
return resource;
}
@@ -60,18 +57,16 @@ public class GameResource
timeSinceLastExtraction = 0;
if (isEndless) return true;
if (currentAmount > 0)
{
if (currentAmount <= 0) return false;
currentAmount--;
return true;
}
return false;
}
public bool CanExtract()
{
return (isEndless || currentAmount > 0) && GameData.availableResearch[item.Research].state == ResearchState.RESEARCHED;
return (isEndless || currentAmount > 0)
&& GameData.availableResearch[item.Research].state == ResearchState.RESEARCHED;
}
public void MakeEndless()
@@ -89,4 +84,12 @@ public class GameResource
{
return extractionSpeed;
}
private void NormalizeExtractionSpeed()
{
if (!isEndless) return;
if (extractionSpeed > NormalExtractionSpeed) return;
extractionSpeed = EndlessExtractionSpeed;
}
}
+5 -4
View File
@@ -29,12 +29,13 @@ public class ItemData
public string GetReadableName()
{
string noUnderscore = Id.Replace("_", " ").ToLower();
return char.ToUpper(noUnderscore[0]) + noUnderscore.Substring(1);
return GetReadableName(Id);
}
public static string GetReadableName(string input)
{
if (string.IsNullOrEmpty(input)) return "";
string noUnderscore = input.Replace("_", " ").ToLower();
return char.ToUpper(noUnderscore[0]) + noUnderscore.Substring(1);
}
@@ -46,6 +47,8 @@ public class ItemData
public string GetCraftingDisplay()
{
if (Inputs.Count <= 0) return GetReadableName() + ": \r";
string result = GetReadableName() + ": \r";
foreach (Ingredient ingredient in Inputs)
@@ -53,8 +56,6 @@ public class ItemData
result += $"{GetReadableName(ingredient.Item)} ({ingredient.Amount}),\r";
}
if (Inputs.Count <= 0) return result;
result = result.Remove(result.Length - 2);
return result;
}
+1 -2
View File
@@ -53,7 +53,6 @@ public class Research
public static string GetReadableName(string input)
{
string noUnderscore = input.Replace("_", " ").ToLower();
return char.ToUpper(noUnderscore[0]) + noUnderscore.Substring(1);
return ItemData.GetReadableName(input);
}
}
+1 -2
View File
@@ -23,8 +23,7 @@ public class ResearchData
public string GetReadableName()
{
string noUnderscore = Id.Replace("_", " ").ToLower();
return char.ToUpper(noUnderscore[0]) + noUnderscore.Substring(1);
return ItemData.GetReadableName(Id);
}
public static string GetIndex(string readable)
+66 -44
View File
@@ -33,16 +33,34 @@ public partial class Robot : Node3D
if (isExecuting)
{
if (CanExecute(delta))
UpdateExecution(delta);
}
else
{
switch (currentNode.Execute(this, delta))
UpdateIdle(delta);
}
Visible = Math.Round(Math.Abs(Position.Y / GameData.tileHeight), 0) == GameData.visibleLayer;
}
private void UpdateExecution(double delta)
{
if (!CanExecute(delta)) return;
NodeResult result = currentNode.Execute(this, delta);
ApplyNodeResult(result);
}
private void ApplyNodeResult(NodeResult result)
{
switch (result)
{
case NodeResult.SUCCESS:
currentNode = currentNode.nextNode;
if (currentNode == null)
{
isExecuting = false;
}
MoveToNextNode(currentNode.nextNode);
break;
case NodeResult.CONDITIONFALSE:
MoveToNextNode(currentNode.NegativeNode);
break;
case NodeResult.FAILURE:
@@ -53,17 +71,19 @@ public partial class Robot : Node3D
case NodeResult.RUNNING:
currentMessage = "";
break;
case NodeResult.CONDITIONFALSE:
currentNode = currentNode.NegativeNode;
}
}
private void MoveToNextNode(ProgramNode nextNode)
{
currentNode = nextNode;
if (currentNode == null)
{
isExecuting = false;
}
break;
}
}
}
else if (currentMessage.Length <= 0)
private void UpdateIdle(double delta)
{
CoolDown(
delta,
@@ -71,18 +91,10 @@ public partial class Robot : Node3D
* TypeStats.CoolingMultiplier
);
if (currentMessage.Length <= 0)
{
currentMessage = "No script executing";
}
else
{
CoolDown(
delta,
GameData.robotStats.GetCoolingRate(IdleHeatLossPerSecond)
* TypeStats.CoolingMultiplier
);
}
Visible = Math.Round(Math.Abs(Position.Y / GameData.tileHeight), 0) == GameData.visibleLayer;
}
public void SetupExecution(List<ProgramNode> nodes)
@@ -200,32 +212,13 @@ public partial class Robot : Node3D
return false;
}
float energyUse =
GameData.robotStats.GetEnergyUse(EnergyUsePerSecond)
* TypeStats.EnergyUseMultiplier
* (float)delta;
if (!GameData.survival.TryConsumeEnergy(energyUse))
if (!TryConsumeEnergy(delta))
{
currentMessage = "Not enough energy";
return false;
}
heat = Math.Clamp(
heat + GameData.robotStats.GetHeatGain(HeatGainPerSecond)
* TypeStats.HeatGainMultiplier
* (float)delta,
0f,
100f
);
maintenance = Math.Clamp(
maintenance - GameData.robotStats.GetMaintenanceLoss(MaintenanceLossPerSecond)
* TypeStats.MaintenanceLossMultiplier
* (float)delta,
0f,
100f
);
ApplyWear(delta);
if (heat >= 100f)
{
@@ -244,6 +237,35 @@ public partial class Robot : Node3D
return true;
}
private bool TryConsumeEnergy(double delta)
{
float energyUse =
GameData.robotStats.GetEnergyUse(EnergyUsePerSecond)
* TypeStats.EnergyUseMultiplier
* (float)delta;
return GameData.survival.TryConsumeEnergy(energyUse);
}
private void ApplyWear(double delta)
{
heat = Math.Clamp(
heat + GameData.robotStats.GetHeatGain(HeatGainPerSecond)
* TypeStats.HeatGainMultiplier
* (float)delta,
0f,
100f
);
maintenance = Math.Clamp(
maintenance - GameData.robotStats.GetMaintenanceLoss(MaintenanceLossPerSecond)
* TypeStats.MaintenanceLossMultiplier
* (float)delta,
0f,
100f
);
}
private void CoolDown(double delta, float heatLossPerSecond)
{
heat = Math.Clamp(
-94
View File
@@ -629,98 +629,4 @@ public partial class TestRunner : Node
AssertTrue(hasStartNode, "start node prefab 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 bool IsGeneratedLayerValid(Layer layer)
{
bool hasGate = false;
foreach (Tile tile in layer.tiles)
{
if (tile.collapsedMesh == null)
{
return false;
}
if (tile.collapsedMesh == "gate")
{
hasGate = true;
}
}
if (!hasGate)
{
return false;
}
return WFC.IsMapConnected(layer.tiles, 1f);
}
private Godot.Collections.Dictionary CreateConnection(
string fromNode,
int fromPort,
string toNode,
int toPort
)
{
Godot.Collections.Dictionary connection = new Godot.Collections.Dictionary();
connection["from_node"] = new StringName(fromNode);
connection["from_port"] = fromPort;
connection["to_node"] = new StringName(toNode);
connection["to_port"] = toPort;
return connection;
}
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}");
}
}
}
+101
View File
@@ -0,0 +1,101 @@
using Godot;
using System;
using System.Collections.Generic;
public partial class TestRunner
{
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 bool IsGeneratedLayerValid(Layer layer)
{
bool hasGate = false;
foreach (Tile tile in layer.tiles)
{
if (tile.collapsedMesh == null)
{
return false;
}
if (tile.collapsedMesh == "gate")
{
hasGate = true;
}
}
if (!hasGate)
{
return false;
}
return WFC.IsMapConnected(layer.tiles, 1f);
}
private Godot.Collections.Dictionary CreateConnection(
string fromNode,
int fromPort,
string toNode,
int toPort
)
{
Godot.Collections.Dictionary connection = new Godot.Collections.Dictionary();
connection["from_node"] = new StringName(fromNode);
connection["from_port"] = fromPort;
connection["to_node"] = new StringName(toNode);
connection["to_port"] = toPort;
return connection;
}
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://dn38ysswhpy04
+3 -8
View File
@@ -17,8 +17,7 @@ public partial class Camera3d : Camera3D
{
Control focused = GetViewport().GuiGetFocusOwner();
if (focused is LineEdit || focused is TextEdit)
return;
if (focused is LineEdit || focused is TextEdit) return;
if (canMove) MoveCamera(delta);
}
@@ -38,19 +37,15 @@ public partial class Camera3d : Camera3D
if (direction != Vector3.Zero)
{
if(robot != null) robot = null;
robot = null;
direction = direction.Normalized() * Speed * (Input.IsActionPressed("sprint") ? 2.5f : 1) * d;
Translate(direction);
}
else
{
if(robot != null)
else if (robot != null)
{
Position = new Vector3(robot.Position.X, 10 - visibleLayer * 4, robot.Position.Z + 4f);
}
}
if (Position.Y != 10 - visibleLayer * 4)
{
Position = new Vector3(Position.X, 10 - visibleLayer * 4, Position.Z);
+14 -97
View File
@@ -1,5 +1,3 @@
using System;
using System.Diagnostics;
using Godot;
public partial class UIHandler : Control
@@ -46,10 +44,7 @@ public partial class UIHandler : Control
DisplayStats();
DisplayRobotAlarm();
Control focused = GetViewport().GuiGetFocusOwner();
if (focused is LineEdit || focused is TextEdit)
return;
if (IsTextInputFocused()) return;
if (Input.IsActionJustPressed("map")) HandleMapButton();
if (Input.IsActionJustPressed("menu")) HandleMenuButton();
@@ -58,22 +53,29 @@ public partial class UIHandler : Control
if (Input.IsActionJustPressed("research")) HandleResearchButton();
}
private bool IsTextInputFocused()
{
Control focused = GetViewport().GuiGetFocusOwner();
return focused is LineEdit || focused is TextEdit;
}
public void HandleMenuButton()
{
if (GameData.survival.isDead) return;
OpenUIElement(menu);
GameData.isPaused = menu.Visible || options.Visible;
}
public void HandleMenu()
{
if(GameData.survival.isDead) return;
HandleMenuButton();
}
public void ShowOptions()
{
if (GameData.survival.isDead) return;
menu.Hide();
OpenUIElement(options);
GameData.isPaused = options.Visible;
@@ -82,6 +84,7 @@ public partial class UIHandler : Control
public void HandleMapButton()
{
if (GameData.survival.isDead) return;
OpenUIElement(map);
if (map.Visible) map.ShowMap();
}
@@ -89,6 +92,7 @@ public partial class UIHandler : Control
public void HandleRobotListButton()
{
if (GameData.survival.isDead) return;
receivedRobotFollowSignal = false;
receivedRobotJumpSignal = false;
OpenUIElement(robotList);
@@ -97,27 +101,18 @@ public partial class UIHandler : Control
public void HandleInventoryButton()
{
if (GameData.survival.isDead) return;
OpenUIElement(inventory);
}
public void HandleResearchButton()
{
if (GameData.survival.isDead) return;
OpenUIElement(researchList);
if (researchList.Visible) researchList.SetupGraph();
}
public void DisplayStats()
{
FPS.Text = Engine.GetFramesPerSecond().ToString() + " FPS";
double memory = Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024);
string memoryDisplay = memory > 1024 ? Math.Round(memory / 1024, 2).ToString() + " GB" : memory.ToString() + " MB";
RAM.Text = memoryDisplay;
DisplaySurvivalStats();
DisplayWorldStats();
DisplayLoseCondition();
}
public void ExitGame()
{
GetTree().ChangeSceneToFile("res://Scenes/MainMenu.tscn");
@@ -139,14 +134,7 @@ public partial class UIHandler : Control
public void OpenUIElement(Control element)
{
SoundManager.PlayButton();
if (element.Visible)
{
element.Hide();
}
else
{
element.Show();
}
element.Visible = !element.Visible;
HideUIElements(element);
}
@@ -164,25 +152,6 @@ public partial class UIHandler : Control
}
}
private void DisplayRobotAlarm()
{
string messages = "";
if (GameData.survival.isDead)
{
messages += GameData.survival.currentStatus + "\r";
}
foreach (Robot robot in GameData.robots)
{
if (robot.currentMessage.Length > 0)
{
messages += $"{robot.Name}: {robot.currentMessage}\r";
}
}
robotAlarm.Visible = messages.Length > 0;
robotAlarm.TooltipText = messages;
}
private void OnRobotJumpTo(Robot robot)
{
if (receivedRobotJumpSignal) return;
@@ -201,42 +170,6 @@ public partial class UIHandler : Control
mainCam.Follow(robot);
}
private void DisplaySurvivalStats()
{
energyLabel.Text = $"Energy: {GameData.survival.energy:0}/{GameData.survival.maxEnergy:0}";
waterLabel.Text = $"Water: {GameData.survival.thirst:0}/{GameData.survival.maxThirst:0}";
hungerLabel.Text = $"Food: {GameData.survival.hunger:0}/{GameData.survival.maxHunger:0}";
survivalStatus.Text = GameData.survival.currentStatus;
survivalStatus.Modulate = GameData.survival.currentStatus.Contains("critical")
? UIStyle.GetWarningColor()
: Colors.White;
if (GameData.survival.isDead)
{
ShowGameOver();
}
}
private void DisplayWorldStats()
{
currentLayer.Text = $"Layer: {GameData.currentLayer + 1}/{GameData.ruinSize}";
deepestLayer.Text = $"Gate depth: {GameData.lowestLayer}";
if (GameData.lowestLayer == GameData.ruinSize)
{
unlockLayer.Visible = false;
return;
}
unlockLayer.TooltipText = "Gate requirements:\r" + GameData.map[GameData.lowestLayer].DisplayGateIngredients();
unlockLayer.Disabled = !GameData.inventory.CanCraft(GameData.map[GameData.lowestLayer].gateIngredients, 1);
}
private void DisplayLoseCondition()
{
if (!GameData.HasNoRobotRecovery()) return;
ShowGameOver("No robots remain and no robot can be spawned from inventory.");
}
public void UnlockLayer()
{
if (GameData.inventory.CanCraft(GameData.map[GameData.lowestLayer].gateIngredients, 1))
@@ -259,20 +192,4 @@ public partial class UIHandler : Control
}
}
public void ShowGameOver()
{
ShowGameOver($"You died!\rReason: {GameData.survival.deathReason}\rBetter luck next time.");
}
public void ShowGameOver(string message)
{
if (gameOver.Visible) return;
gameOver.GetNode<RichTextLabel>("./VBoxContainer/Content").Text = $"[font_size=32]{message}\r";
gameOver.Show();
}
public void HideGameOver()
{
gameOver.Hide();
}
}
+98
View File
@@ -0,0 +1,98 @@
using System;
using System.Diagnostics;
using Godot;
public partial class UIHandler
{
public void DisplayStats()
{
FPS.Text = Engine.GetFramesPerSecond().ToString() + " FPS";
RAM.Text = GetMemoryDisplay();
DisplaySurvivalStats();
DisplayWorldStats();
DisplayLoseCondition();
}
private string GetMemoryDisplay()
{
double memory = Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024);
if (memory > 1024)
{
return Math.Round(memory / 1024, 2).ToString() + " GB";
}
return memory.ToString() + " MB";
}
private void DisplayRobotAlarm()
{
string messages = "";
if (GameData.survival.isDead)
{
messages += GameData.survival.currentStatus + "\r";
}
foreach (Robot robot in GameData.robots)
{
if (robot.currentMessage.Length > 0)
{
messages += $"{robot.Name}: {robot.currentMessage}\r";
}
}
robotAlarm.Visible = messages.Length > 0;
robotAlarm.TooltipText = messages;
}
private void DisplaySurvivalStats()
{
energyLabel.Text = $"Energy: {GameData.survival.energy:0}/{GameData.survival.maxEnergy:0}";
waterLabel.Text = $"Water: {GameData.survival.thirst:0}/{GameData.survival.maxThirst:0}";
hungerLabel.Text = $"Food: {GameData.survival.hunger:0}/{GameData.survival.maxHunger:0}";
survivalStatus.Text = GameData.survival.currentStatus;
survivalStatus.Modulate = GameData.survival.currentStatus.Contains("critical")
? UIStyle.GetWarningColor()
: Colors.White;
if (GameData.survival.isDead)
{
ShowGameOver();
}
}
private void DisplayWorldStats()
{
currentLayer.Text = $"Layer: {GameData.currentLayer + 1}/{GameData.ruinSize}";
deepestLayer.Text = $"Gate depth: {GameData.lowestLayer}";
if (GameData.lowestLayer == GameData.ruinSize)
{
unlockLayer.Visible = false;
return;
}
unlockLayer.TooltipText = "Gate requirements:\r" + GameData.map[GameData.lowestLayer].DisplayGateIngredients();
unlockLayer.Disabled = !GameData.inventory.CanCraft(GameData.map[GameData.lowestLayer].gateIngredients, 1);
}
private void DisplayLoseCondition()
{
if (!GameData.HasNoRobotRecovery()) return;
ShowGameOver("No robots remain and no robot can be spawned from inventory.");
}
public void ShowGameOver()
{
ShowGameOver($"You died!\rReason: {GameData.survival.deathReason}\rBetter luck next time.");
}
public void ShowGameOver(string message)
{
if (gameOver.Visible) return;
gameOver.GetNode<RichTextLabel>("./VBoxContainer/Content").Text = $"[font_size=32]{message}\r";
gameOver.Show();
}
public void HideGameOver()
{
gameOver.Hide();
}
}
@@ -0,0 +1 @@
uid://b2q7e88lokg3l
+65 -322
View File
@@ -13,15 +13,6 @@ public partial class CodingWindow : PanelContainer
[Export] LineEdit nameInput;
public System.Collections.Generic.Dictionary<ProgramNode, PackedScene> DSLNodes;
private System.Collections.Generic.Dictionary<StringName, ProgramNode> availableNodes;
private class ScriptConnection
{
public string FromNodeId;
public int FromPort;
public string ToNodeId;
public int ToPort;
}
public override void _Ready()
{
@@ -51,6 +42,7 @@ public partial class CodingWindow : PanelContainer
{
availableScripts.Clear();
availableScripts.AddItem("Select script to load...");
List<string> scripts = FileHandler.LoadProgramNames();
scripts.Sort((a, b) => a.CompareTo(b));
foreach (string script in scripts)
@@ -73,10 +65,9 @@ public partial class CodingWindow : PanelContainer
public void GenerateCodingBlocks()
{
Button nodeListButton;
foreach (ProgramNode nodeTemplate in DSLNodes.Keys)
{
nodeListButton = new Button
Button nodeListButton = new Button
{
Name = nodeTemplate.DisplayText,
Text = nodeTemplate.DisplayText
@@ -92,17 +83,14 @@ public partial class CodingWindow : PanelContainer
private void AddEditorNode(ProgramNode node)
{
NodeDisplay editorDisplay = DSLNodes[node].Instantiate<NodeDisplay>();
editorDisplay.PositionOffset = (editorWindow.ScrollOffset + editorWindow.Size / 2) / editorWindow.Zoom - editorDisplay.Size / 2;
editorDisplay.PositionOffset = GetVisibleGraphCenter() - editorDisplay.Size / 2f;
editorWindow.AddChild(editorDisplay);
RegisterEditorNode(editorDisplay);
}
private void MoveNodeToVisibleGraphCenter(NodeDisplay nodeDisplay)
private Vector2 GetVisibleGraphCenter()
{
Vector2 visibleCenter = editorWindow.ScrollOffset
+ editorWindow.Size / (2f * editorWindow.Zoom);
nodeDisplay.PositionOffset = visibleCenter - nodeDisplay.Size / (2f * editorWindow.Zoom);
return (editorWindow.ScrollOffset + editorWindow.Size / 2f) / editorWindow.Zoom;
}
private void RegisterEditorNode(NodeDisplay editorDisplay)
@@ -115,6 +103,13 @@ public partial class CodingWindow : PanelContainer
}
public void ClearWindow()
{
DisconnectAllNodes();
RemoveEditorNodes();
scriptName.Text = "";
}
private void DisconnectAllNodes()
{
foreach (Dictionary connection in editorWindow.GetConnectionList())
{
@@ -125,109 +120,41 @@ public partial class CodingWindow : PanelContainer
(int)connection["to_port"]
);
}
}
private void RemoveEditorNodes()
{
foreach (Node child in editorWindow.GetChildren())
{
if (child is GraphNode)
{
if (child is not GraphNode) continue;
editorWindow.RemoveChild(child);
child.QueueFree();
}
}
scriptName.Text = "";
}
public void CompileProgram()
{
if (robot == null) return;
NodeDisplay startNode = FindStartNode();
if (startNode == null)
ScriptGraphCompiler compiler = new ScriptGraphCompiler(editorWindow);
string errorMessage;
List<ProgramNode> nodes = compiler.BuildProgram(out errorMessage);
if (errorMessage.Length > 0)
{
robot.StopExecution("(FAILED) Script needs exactly one Start node");
robot.StopExecution(errorMessage);
return;
}
BuildAvailableNodeLookup();
List<ProgramNode> nodes = BuildScriptOrder(
startNode,
new List<ProgramNode>(),
new HashSet<StringName>()
);
if (nodes.Count > 0) robot.SetupExecution(nodes);
robot.currentProgram = scriptName.Text.Length <= 0 ? $"Script{availableScripts.ItemCount}" : scriptName.Text;
robot.currentProgram = GetCurrentScriptName();
}
private void BuildAvailableNodeLookup()
private string GetCurrentScriptName()
{
availableNodes = new System.Collections.Generic.Dictionary<StringName, ProgramNode>();
if (scriptName.Text.Length > 0) return scriptName.Text;
for (int i = 0; i < editorWindow.GetChildCount(); i++)
{
NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay;
if (nodeDisplay == null) continue;
nodeDisplay.ReadParameters();
availableNodes.Add(nodeDisplay.Name, nodeDisplay.node);
}
}
private List<ProgramNode> BuildScriptOrder(
NodeDisplay node,
List<ProgramNode> program,
HashSet<StringName> visitedNodes
)
{
if (node == null) return program;
if (visitedNodes.Contains(node.Name)) return program;
visitedNodes.Add(node.Name);
program.Add(node.node);
if (editorWindow.GetConnectionListFromNode(node.Name).Count <= 0) return program;
List<Dictionary> nextConnections = CheckNodeConnections(node);
if (nextConnections.Count <= 0) return program;
node.node.SetNextNode(nextConnections, availableNodes);
foreach (Dictionary connection in nextConnections)
{
NodeDisplay nextNode = editorWindow.GetNodeOrNull<NodeDisplay>(
new NodePath(connection["to_node"].AsStringName())
);
program = BuildScriptOrder(
nextNode,
program,
visitedNodes
);
}
return program;
}
private List<Dictionary> CheckNodeConnections(NodeDisplay node)
{
List<Dictionary> result = new List<Dictionary>();
Array<Dictionary> connections = editorWindow.GetConnectionListFromNode(node.Name);
for (int i = 0; i < connections.Count; i++)
{
if (connections[i]["from_node"].AsStringName() == node.Name)
{
result.Add(connections[i]);
}
}
return result;
}
private NodeDisplay FindStartNode()
{
NodeDisplay startNode = null;
for (int i = 0; i < editorWindow.GetChildCount(); i++)
{
NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay;
if (nodeDisplay == null) continue;
if (nodeDisplay.node is not StartNode) continue;
if (startNode != null) return null;
startNode = nodeDisplay;
}
return startNode;
return $"Script{availableScripts.ItemCount}";
}
public void SetRobot(Robot robot)
@@ -241,131 +168,15 @@ public partial class CodingWindow : PanelContainer
ClearWindow();
string scriptContent = FileHandler.LoadProgram(availableScripts.GetItemText(index));
LoadStructuredProgram(scriptContent);
CreateSerializer().Load(scriptContent);
scriptName.Text = availableScripts.GetItemText(index);
availableScripts.Select(0);
}
private void LoadStructuredProgram(string scriptContent)
private void AddLoadedNode(NodeDisplay nodeDisplay)
{
Variant parsedScript = Json.ParseString(scriptContent);
if (parsedScript.VariantType != Variant.Type.Dictionary) return;
Dictionary scriptData = parsedScript.AsGodotDictionary();
if (!scriptData.ContainsKey("Nodes")) return;
System.Collections.Generic.Dictionary<string, NodeDisplay> loadedNodes =
new System.Collections.Generic.Dictionary<string, NodeDisplay>();
Array nodes = scriptData["Nodes"].AsGodotArray();
for (int i = 0; i < nodes.Count; i++)
{
Dictionary nodeData = nodes[i].AsGodotDictionary();
NodeDisplay nodeDisplay = LoadStructuredNode(nodeData);
if (nodeDisplay == null) continue;
editorWindow.AddChild(nodeDisplay);
RegisterEditorNode(nodeDisplay);
RegisterLoadedNode(nodeData, nodeDisplay, loadedNodes);
}
LoadStructuredConnections(scriptData, loadedNodes);
}
private void RegisterLoadedNode(
Dictionary nodeData,
NodeDisplay nodeDisplay,
System.Collections.Generic.Dictionary<string, NodeDisplay> loadedNodes
)
{
string nodeId = nodeDisplay.Name.ToString();
if (nodeData.ContainsKey("Id"))
{
nodeId = nodeData["Id"].AsString();
}
if (!loadedNodes.ContainsKey(nodeId))
{
loadedNodes.Add(nodeId, nodeDisplay);
}
}
private NodeDisplay LoadStructuredNode(Dictionary nodeData)
{
if (!nodeData.ContainsKey("Type")) return null;
if (!nodeData.ContainsKey("Content")) return null;
string type = nodeData["Type"].AsString();
string content = nodeData["Content"].AsString();
NodeDisplay nodeDisplay = NodeDisplay.Load(type, content, DSLNodes);
if (nodeDisplay == null) return null;
if (nodeData.ContainsKey("Id"))
{
nodeDisplay.Name = nodeData["Id"].AsString();
}
if (nodeData.ContainsKey("PositionX") && nodeData.ContainsKey("PositionY"))
{
float positionX = (float)nodeData["PositionX"].AsDouble();
float positionY = (float)nodeData["PositionY"].AsDouble();
nodeDisplay.PositionOffset = new Vector2(positionX, positionY);
}
return nodeDisplay;
}
private void LoadStructuredConnections(
Dictionary scriptData,
System.Collections.Generic.Dictionary<string, NodeDisplay> loadedNodes
)
{
if (!scriptData.ContainsKey("Connections")) return;
Array connectionData = scriptData["Connections"].AsGodotArray();
for (int i = 0; i < connectionData.Count; i++)
{
Dictionary savedConnection = connectionData[i].AsGodotDictionary();
if (!savedConnection.ContainsKey("From")) continue;
if (!savedConnection.ContainsKey("To")) continue;
if (!savedConnection.ContainsKey("FromPort")) continue;
if (!savedConnection.ContainsKey("ToPort")) continue;
ScriptConnection connection = new ScriptConnection
{
FromNodeId = savedConnection["From"].AsString(),
FromPort = savedConnection["FromPort"].AsInt32(),
ToNodeId = savedConnection["To"].AsString(),
ToPort = savedConnection["ToPort"].AsInt32()
};
if (!loadedNodes.ContainsKey(connection.FromNodeId)) continue;
if (!loadedNodes.ContainsKey(connection.ToNodeId)) continue;
NodeDisplay fromDisplay = loadedNodes[connection.FromNodeId];
NodeDisplay toDisplay = loadedNodes[connection.ToNodeId];
if (ConnectionExists(fromDisplay.Name, connection.FromPort, toDisplay.Name, connection.ToPort)) continue;
editorWindow.ConnectNode(
fromDisplay.Name,
connection.FromPort,
toDisplay.Name,
connection.ToPort
);
}
}
private bool ConnectionExists(StringName fromNode, int fromPort, StringName toNode, int toPort)
{
foreach (Dictionary connection in editorWindow.GetConnectionList())
{
if (connection["from_node"].AsStringName() != fromNode) continue;
if ((int)connection["from_port"] != fromPort) continue;
if (connection["to_node"].AsStringName() != toNode) continue;
if ((int)connection["to_port"] != toPort) continue;
return true;
}
return false;
}
public void LoadTemporaryProgram()
@@ -373,65 +184,16 @@ public partial class CodingWindow : PanelContainer
if (robot == null) return;
if (robot.currentNode == null) return;
System.Collections.Generic.Dictionary<ProgramNode, NodeDisplay> loadedNodes =
new System.Collections.Generic.Dictionary<ProgramNode, NodeDisplay>();
LoadTemporaryNode(robot.currentNode, loadedNodes);
RunningProgramGraphBuilder builder = new RunningProgramGraphBuilder(
DSLNodes,
AddLoadedNode,
ConnectNodes
);
builder.Load(robot.currentNode);
scriptName.Text = robot.currentProgram ?? "";
}
private NodeDisplay LoadTemporaryNode(
ProgramNode programNode,
System.Collections.Generic.Dictionary<ProgramNode, NodeDisplay> loadedNodes
)
{
if (programNode == null) return null;
if (loadedNodes.ContainsKey(programNode)) return loadedNodes[programNode];
NodeDisplay nodeDisplay = NodeDisplay.Load(
programNode.DisplayText,
programNode.Save(),
DSLNodes
);
if (nodeDisplay == null) return null;
editorWindow.AddChild(nodeDisplay);
RegisterEditorNode(nodeDisplay);
loadedNodes.Add(programNode, nodeDisplay);
ConnectTemporaryNode(nodeDisplay, 0, programNode.nextNode, loadedNodes);
ConnectTemporaryNode(nodeDisplay, 1, programNode.NegativeNode, loadedNodes);
return nodeDisplay;
}
private void ConnectTemporaryNode(
NodeDisplay fromDisplay,
int fromPort,
ProgramNode targetNode,
System.Collections.Generic.Dictionary<ProgramNode, NodeDisplay> loadedNodes
)
{
NodeDisplay toDisplay = LoadTemporaryNode(targetNode, loadedNodes);
if (toDisplay == null) return;
ScriptConnection connection = new ScriptConnection
{
FromNodeId = fromDisplay.Name.ToString(),
FromPort = fromPort,
ToNodeId = toDisplay.Name.ToString(),
ToPort = 0
};
if (ConnectionExists(fromDisplay.Name, connection.FromPort, toDisplay.Name, connection.ToPort)) return;
editorWindow.ConnectNode(
fromDisplay.Name,
connection.FromPort,
toDisplay.Name,
connection.ToPort
);
}
public void DeleteProgram()
{
string filename = scriptName.Text;
@@ -450,69 +212,50 @@ public partial class CodingWindow : PanelContainer
public void SaveProgram()
{
Array<Dictionary> savedNodes = BuildSavedNodes();
if (savedNodes.Count <= 0) return;
Dictionary scriptData = new Dictionary();
scriptData["Nodes"] = savedNodes;
scriptData["Connections"] = BuildSavedConnections();
string result = Json.Stringify(scriptData);
string result = CreateSerializer().Save();
if (result.Length <= 0) return;
string filename = scriptName.Text.Length <= 0 ? $"Script{availableScripts.ItemCount}" : scriptName.Text;
FileHandler.SaveProgram(filename, result);
FileHandler.SaveProgram(GetCurrentScriptName(), result);
SetupScriptOptions();
}
private Array<Dictionary> BuildSavedNodes()
{
Array<Dictionary> savedNodes = new Array<Dictionary>();
for (int i = 0; i < editorWindow.GetChildCount(); i++)
{
NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay;
if (nodeDisplay == null) continue;
nodeDisplay.ReadParameters();
Dictionary savedNode = new Dictionary();
savedNode["Id"] = nodeDisplay.Name.ToString();
savedNode["Type"] = nodeDisplay.node.DisplayText.ToLower();
savedNode["Content"] = nodeDisplay.node.Save();
savedNode["PositionX"] = nodeDisplay.PositionOffset.X;
savedNode["PositionY"] = nodeDisplay.PositionOffset.Y;
savedNodes.Add(savedNode);
}
return savedNodes;
}
private Array<Dictionary> BuildSavedConnections()
{
Array<Dictionary> savedConnections = new Array<Dictionary>();
foreach (Dictionary connection in editorWindow.GetConnectionList())
{
Dictionary savedConnection = new Dictionary();
savedConnection["From"] = connection["from_node"].AsStringName().ToString();
savedConnection["FromPort"] = (int)connection["from_port"];
savedConnection["To"] = connection["to_node"].AsStringName().ToString();
savedConnection["ToPort"] = (int)connection["to_port"];
savedConnections.Add(savedConnection);
}
return savedConnections;
}
public void OnNodeConnect(StringName from, int fromPort, StringName to, int toPort)
{
if (to == from) return;
foreach (Dictionary connection in editorWindow.GetConnectionList())
{
if (connection["from_node"].AsStringName() == from && (int)connection["from_port"] == fromPort) return;
ConnectNodes(from, fromPort, to, toPort);
}
private void ConnectNodes(StringName from, int fromPort, StringName to, int toPort)
{
if (HasOutputConnection(from, fromPort)) return;
editorWindow.ConnectNode(from, fromPort, to, toPort);
}
private bool HasOutputConnection(StringName from, int fromPort)
{
foreach (Dictionary connection in editorWindow.GetConnectionList())
{
if (connection["from_node"].AsStringName() != from) continue;
if ((int)connection["from_port"] != fromPort) continue;
return true;
}
return false;
}
private ScriptGraphSerializer CreateSerializer()
{
return new ScriptGraphSerializer(
editorWindow,
DSLNodes,
AddLoadedNode,
ConnectNodes
);
}
public void OnNodeDisconnect(StringName from, int fromPort, StringName to, int toPort)
{
editorWindow.DisconnectNode(from, fromPort, to, toPort);
@@ -0,0 +1,65 @@
using Godot;
using System;
using System.Collections.Generic;
public class RunningProgramGraphBuilder
{
private readonly Dictionary<ProgramNode, PackedScene> dslNodes;
private readonly Action<NodeDisplay> addNode;
private readonly Action<StringName, int, StringName, int> connectNodes;
public RunningProgramGraphBuilder(
Dictionary<ProgramNode, PackedScene> dslNodes,
Action<NodeDisplay> addNode,
Action<StringName, int, StringName, int> connectNodes
)
{
this.dslNodes = dslNodes;
this.addNode = addNode;
this.connectNodes = connectNodes;
}
public void Load(ProgramNode startNode)
{
Dictionary<ProgramNode, NodeDisplay> loadedNodes =
new Dictionary<ProgramNode, NodeDisplay>();
LoadNode(startNode, loadedNodes);
}
private NodeDisplay LoadNode(
ProgramNode programNode,
Dictionary<ProgramNode, NodeDisplay> loadedNodes
)
{
if (programNode == null) return null;
if (loadedNodes.ContainsKey(programNode)) return loadedNodes[programNode];
NodeDisplay nodeDisplay = NodeDisplay.Load(
programNode.DisplayText,
programNode.Save(),
dslNodes
);
if (nodeDisplay == null) return null;
addNode(nodeDisplay);
loadedNodes.Add(programNode, nodeDisplay);
ConnectNode(nodeDisplay, 0, programNode.nextNode, loadedNodes);
ConnectNode(nodeDisplay, 1, programNode.NegativeNode, loadedNodes);
return nodeDisplay;
}
private void ConnectNode(
NodeDisplay fromDisplay,
int fromPort,
ProgramNode targetNode,
Dictionary<ProgramNode, NodeDisplay> loadedNodes
)
{
NodeDisplay toDisplay = LoadNode(targetNode, loadedNodes);
if (toDisplay == null) return;
connectNodes(fromDisplay.Name, fromPort, toDisplay.Name, 0);
}
}
@@ -0,0 +1 @@
uid://deaptod52fe2s
+7
View File
@@ -0,0 +1,7 @@
public class ScriptConnection
{
public string FromNodeId;
public int FromPort;
public string ToNodeId;
public int ToPort;
}
+1
View File
@@ -0,0 +1 @@
uid://cvhobhufyp2ni
+108
View File
@@ -0,0 +1,108 @@
using Godot;
using Godot.Collections;
using System.Collections.Generic;
public class ScriptGraphCompiler
{
private readonly GraphEdit editorWindow;
private System.Collections.Generic.Dictionary<StringName, ProgramNode> availableNodes;
public ScriptGraphCompiler(GraphEdit editorWindow)
{
this.editorWindow = editorWindow;
}
public List<ProgramNode> BuildProgram(out string errorMessage)
{
errorMessage = "";
NodeDisplay startNode = FindStartNode();
if (startNode == null)
{
errorMessage = "(FAILED) Script needs exactly one Start node";
return new List<ProgramNode>();
}
BuildAvailableNodeLookup();
return BuildScriptOrder(
startNode,
new List<ProgramNode>(),
new HashSet<StringName>()
);
}
private void BuildAvailableNodeLookup()
{
availableNodes = new System.Collections.Generic.Dictionary<StringName, ProgramNode>();
for (int i = 0; i < editorWindow.GetChildCount(); i++)
{
NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay;
if (nodeDisplay == null) continue;
nodeDisplay.ReadParameters();
availableNodes.Add(nodeDisplay.Name, nodeDisplay.node);
}
}
private List<ProgramNode> BuildScriptOrder(
NodeDisplay node,
List<ProgramNode> program,
HashSet<StringName> visitedNodes
)
{
if (node == null) return program;
if (visitedNodes.Contains(node.Name)) return program;
visitedNodes.Add(node.Name);
program.Add(node.node);
List<Dictionary> nextConnections = GetOutgoingConnections(node);
if (nextConnections.Count <= 0) return program;
node.node.SetNextNode(nextConnections, availableNodes);
foreach (Dictionary connection in nextConnections)
{
NodeDisplay nextNode = editorWindow.GetNodeOrNull<NodeDisplay>(
new NodePath(connection["to_node"].AsStringName())
);
program = BuildScriptOrder(
nextNode,
program,
visitedNodes
);
}
return program;
}
private List<Dictionary> GetOutgoingConnections(NodeDisplay node)
{
List<Dictionary> result = new List<Dictionary>();
Array<Dictionary> connections = editorWindow.GetConnectionListFromNode(node.Name);
for (int i = 0; i < connections.Count; i++)
{
if (connections[i]["from_node"].AsStringName() == node.Name)
{
result.Add(connections[i]);
}
}
return result;
}
private NodeDisplay FindStartNode()
{
NodeDisplay startNode = null;
for (int i = 0; i < editorWindow.GetChildCount(); i++)
{
NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay;
if (nodeDisplay == null) continue;
if (nodeDisplay.node is not StartNode) continue;
if (startNode != null) return null;
startNode = nodeDisplay;
}
return startNode;
}
}
@@ -0,0 +1 @@
uid://d122nx50nj3wc
+189
View File
@@ -0,0 +1,189 @@
using Godot;
using Godot.Collections;
using System;
public class ScriptGraphSerializer
{
private readonly GraphEdit editorWindow;
private readonly System.Collections.Generic.Dictionary<ProgramNode, PackedScene> dslNodes;
private readonly Action<NodeDisplay> addNode;
private readonly Action<StringName, int, StringName, int> connectNodes;
public ScriptGraphSerializer(
GraphEdit editorWindow,
System.Collections.Generic.Dictionary<ProgramNode, PackedScene> dslNodes,
Action<NodeDisplay> addNode,
Action<StringName, int, StringName, int> connectNodes
)
{
this.editorWindow = editorWindow;
this.dslNodes = dslNodes;
this.addNode = addNode;
this.connectNodes = connectNodes;
}
public void Load(string scriptContent)
{
Variant parsedScript = Json.ParseString(scriptContent);
if (parsedScript.VariantType != Variant.Type.Dictionary) return;
Dictionary scriptData = parsedScript.AsGodotDictionary();
if (!scriptData.ContainsKey("Nodes")) return;
System.Collections.Generic.Dictionary<string, NodeDisplay> loadedNodes =
new System.Collections.Generic.Dictionary<string, NodeDisplay>();
Godot.Collections.Array nodes = scriptData["Nodes"].AsGodotArray();
for (int i = 0; i < nodes.Count; i++)
{
Dictionary nodeData = nodes[i].AsGodotDictionary();
NodeDisplay nodeDisplay = LoadNode(nodeData);
if (nodeDisplay == null) continue;
addNode(nodeDisplay);
RegisterLoadedNode(nodeData, nodeDisplay, loadedNodes);
}
LoadConnections(scriptData, loadedNodes);
}
public string Save()
{
Array<Dictionary> savedNodes = BuildSavedNodes();
if (savedNodes.Count <= 0) return "";
Dictionary scriptData = new Dictionary();
scriptData["Nodes"] = savedNodes;
scriptData["Connections"] = BuildSavedConnections();
return Json.Stringify(scriptData);
}
private NodeDisplay LoadNode(Dictionary nodeData)
{
if (!nodeData.ContainsKey("Type")) return null;
if (!nodeData.ContainsKey("Content")) return null;
string type = nodeData["Type"].AsString();
string content = nodeData["Content"].AsString();
NodeDisplay nodeDisplay = NodeDisplay.Load(type, content, dslNodes);
if (nodeDisplay == null) return null;
if (nodeData.ContainsKey("Id"))
{
nodeDisplay.Name = nodeData["Id"].AsString();
}
if (nodeData.ContainsKey("PositionX") && nodeData.ContainsKey("PositionY"))
{
float positionX = (float)nodeData["PositionX"].AsDouble();
float positionY = (float)nodeData["PositionY"].AsDouble();
nodeDisplay.PositionOffset = new Vector2(positionX, positionY);
}
return nodeDisplay;
}
private void RegisterLoadedNode(
Dictionary nodeData,
NodeDisplay nodeDisplay,
System.Collections.Generic.Dictionary<string, NodeDisplay> loadedNodes
)
{
string nodeId = nodeDisplay.Name.ToString();
if (nodeData.ContainsKey("Id"))
{
nodeId = nodeData["Id"].AsString();
}
if (!loadedNodes.ContainsKey(nodeId))
{
loadedNodes.Add(nodeId, nodeDisplay);
}
}
private void LoadConnections(
Dictionary scriptData,
System.Collections.Generic.Dictionary<string, NodeDisplay> loadedNodes
)
{
if (!scriptData.ContainsKey("Connections")) return;
Godot.Collections.Array connectionData = scriptData["Connections"].AsGodotArray();
for (int i = 0; i < connectionData.Count; i++)
{
Dictionary savedConnection = connectionData[i].AsGodotDictionary();
if (!IsConnectionDataValid(savedConnection)) continue;
ScriptConnection connection = CreateConnection(savedConnection);
ConnectLoadedNodes(connection, loadedNodes);
}
}
private bool IsConnectionDataValid(Dictionary savedConnection)
{
return savedConnection.ContainsKey("From")
&& savedConnection.ContainsKey("To")
&& savedConnection.ContainsKey("FromPort")
&& savedConnection.ContainsKey("ToPort");
}
private ScriptConnection CreateConnection(Dictionary savedConnection)
{
return new ScriptConnection
{
FromNodeId = savedConnection["From"].AsString(),
FromPort = savedConnection["FromPort"].AsInt32(),
ToNodeId = savedConnection["To"].AsString(),
ToPort = savedConnection["ToPort"].AsInt32()
};
}
private void ConnectLoadedNodes(
ScriptConnection connection,
System.Collections.Generic.Dictionary<string, NodeDisplay> loadedNodes
)
{
if (!loadedNodes.ContainsKey(connection.FromNodeId)) return;
if (!loadedNodes.ContainsKey(connection.ToNodeId)) return;
NodeDisplay fromDisplay = loadedNodes[connection.FromNodeId];
NodeDisplay toDisplay = loadedNodes[connection.ToNodeId];
connectNodes(fromDisplay.Name, connection.FromPort, toDisplay.Name, connection.ToPort);
}
private Array<Dictionary> BuildSavedNodes()
{
Array<Dictionary> savedNodes = new Array<Dictionary>();
for (int i = 0; i < editorWindow.GetChildCount(); i++)
{
NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay;
if (nodeDisplay == null) continue;
nodeDisplay.ReadParameters();
Dictionary savedNode = new Dictionary();
savedNode["Id"] = nodeDisplay.Name.ToString();
savedNode["Type"] = nodeDisplay.node.DisplayText.ToLower();
savedNode["Content"] = nodeDisplay.node.Save();
savedNode["PositionX"] = nodeDisplay.PositionOffset.X;
savedNode["PositionY"] = nodeDisplay.PositionOffset.Y;
savedNodes.Add(savedNode);
}
return savedNodes;
}
private Array<Dictionary> BuildSavedConnections()
{
Array<Dictionary> savedConnections = new Array<Dictionary>();
foreach (Dictionary connection in editorWindow.GetConnectionList())
{
Dictionary savedConnection = new Dictionary();
savedConnection["From"] = connection["from_node"].AsStringName().ToString();
savedConnection["FromPort"] = (int)connection["from_port"];
savedConnection["To"] = connection["to_node"].AsStringName().ToString();
savedConnection["ToPort"] = (int)connection["to_port"];
savedConnections.Add(savedConnection);
}
return savedConnections;
}
}
@@ -0,0 +1 @@
uid://dsrkrw6524c
+16 -7
View File
@@ -3,7 +3,8 @@ using Godot;
public partial class InventoryDisplay : PanelContainer
{
private PackedScene itemDisplayPrefab = ResourceLoader.LoadItemDisplay();
private readonly PackedScene itemDisplayPrefab = ResourceLoader.LoadItemDisplay();
[Export] VBoxContainer itemList;
[Export] RichTextLabel inventorySpace;
@@ -34,24 +35,32 @@ public partial class InventoryDisplay : PanelContainer
}
public void ReloadItems()
{
ClearItems();
foreach (Item item in GameData.inventory.items)
{
itemList.AddChild(CreateItemDisplay(item));
}
}
private void ClearItems()
{
foreach (Node node in itemList.GetChildren())
{
itemList.RemoveChild(node);
node.QueueFree();
}
}
ItemDisplay display;
foreach (Item item in GameData.inventory.items)
private ItemDisplay CreateItemDisplay(Item item)
{
display = itemDisplayPrefab.Instantiate<ItemDisplay>();
ItemDisplay display = itemDisplayPrefab.Instantiate<ItemDisplay>();
display.item = item;
display.text.Text = item.data.GetReadableName();
display.amount.Text = $"{item.currentAmount}/{item.data.StackSize}";
display.texture.Texture = ResourceLoader.LoadPath(item.data.Texture);
itemList.AddChild(display);
}
return display;
}
public void OnInventoryUpdate(object sender, EventArgs args)
+4
View File
@@ -50,6 +50,10 @@ public partial class OptionsMenu : PanelContainer
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true);
break;
default:
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false);
break;
}
}
+26 -20
View File
@@ -10,7 +10,6 @@ public partial class ResearchList : PanelContainer
private bool hasArrangedNodes = false;
private List<Research> currentResearch = new List<Research>();
private List<Research> toDelete = new List<Research>();
public override void _Ready()
{
@@ -20,28 +19,35 @@ public partial class ResearchList : PanelContainer
public override void _Process(double delta)
{
if(currentResearch.Count > 0) toDelete = new List<Research>();
if (currentResearch.Count <= 0) return;
List<Research> finishedResearch = new List<Research>();
foreach (Research research in currentResearch)
{
ResearchResult result = research.Execute(delta);
if (result == ResearchResult.FINISHED)
if (!IsResearchFinished(research, result)) continue;
finishedResearch.Add(research);
}
if (finishedResearch.Count <= 0) return;
foreach (Research research in finishedResearch)
{
toDelete.Add(research);
currentResearch.Remove(research);
}
RecalculateResearchStates();
SetupGraph();
}
else if (result == ResearchResult.FAILED)
private bool IsResearchFinished(Research research, ResearchResult result)
{
if (result == ResearchResult.FINISHED) return true;
if (result != ResearchResult.FAILED) return false;
research.state = ResearchState.AVAILABLE;
toDelete.Add(research);
RecalculateResearchStates();
SetupGraph();
}
}
foreach (Research delete in toDelete)
{
currentResearch.Remove(delete);
}
return true;
}
public void SetupGraph()
@@ -83,13 +89,12 @@ public partial class ResearchList : PanelContainer
foreach (Node child in researchGraph.GetChildren())
{
if (child is GraphNode)
{
if (child is not GraphNode) continue;
researchGraph.RemoveChild(child);
child.QueueFree();
}
}
}
private void CreateResearchNodes()
{
@@ -127,9 +132,10 @@ public partial class ResearchList : PanelContainer
private GraphNode CreateResearchNode(string id, string texturePath, ResearchState state)
{
Research research = GameData.availableResearch[id];
Texture2D texture = GD.Load<Texture2D>(texturePath);
Color stateColor = GetColorByState(state);
string tooltipText = GetResearchTooltip(GameData.availableResearch[id]);
string tooltipText = GetResearchTooltip(research);
TextureRect icon = new TextureRect
{
@@ -141,8 +147,8 @@ public partial class ResearchList : PanelContainer
Button button = new Button
{
Text = GetResearchButtonText(GameData.availableResearch[id], state),
Disabled = state != ResearchState.AVAILABLE || !GameData.availableResearch[id].CanStart(),
Text = GetResearchButtonText(research, state),
Disabled = state != ResearchState.AVAILABLE || !research.CanStart(),
TooltipText = tooltipText
};
+7 -2
View File
@@ -12,14 +12,19 @@ public partial class RobotDisplay : PanelContainer
public override void _Process(double delta)
{
string programName = robot.currentProgram ?? "";
string status = $"{programName} | Heat {robot.heat:0}% | Maintenance {robot.maintenance:0}%";
string status = GetStatusText();
if (status != currentScript.Text)
{
currentScript.Text = status;
}
}
private string GetStatusText()
{
string programName = robot.currentProgram ?? "";
return $"{programName} | Heat {robot.heat:0}% | Maintenance {robot.maintenance:0}%";
}
public void OnJumpToClicked()
{
EmitSignal(SignalName.OnRobotJumpTo, robot);
+24 -10
View File
@@ -30,31 +30,44 @@ public partial class RobotList : PanelContainer
}
public void ReloadRobots()
{
ClearRobotList();
foreach (Robot robotObject in GameData.robots)
{
robotList.AddChild(CreateRobotDisplay(robotObject));
}
}
private void ClearRobotList()
{
foreach (Node node in robotList.GetChildren())
{
robotList.RemoveChild(node);
node.QueueFree();
}
RobotDisplay display;
}
foreach (Robot robotObject in GameData.robots)
private RobotDisplay CreateRobotDisplay(Robot robotObject)
{
display = robotDisplayPrefab.Instantiate<RobotDisplay>();
RobotDisplay display = robotDisplayPrefab.Instantiate<RobotDisplay>();
display.robot = robotObject;
display.listItem.Text = robotObject.Name;
display.OnRobotJumpTo += (robot) =>
display.OnRobotJumpTo += HandleRobotJumpTo;
display.OnRobotFollow += HandleRobotFollow;
return display;
}
private void HandleRobotJumpTo(Robot robot)
{
EmitSignal(SignalName.OnRobotJumpTo, robot);
Visible = false;
};
display.OnRobotFollow += (robot) =>
}
private void HandleRobotFollow(Robot robot)
{
EmitSignal(SignalName.OnRobotFollow, robot);
Visible = false;
};
robotList.AddChild(display);
}
}
public void ReloadSelectableRobots()
@@ -82,6 +95,7 @@ public partial class RobotList : PanelContainer
public void SpawnRobot()
{
if (spawnId.Length <= 0) return;
GameData.inventory.RemoveItem(spawnId, 1);
Robot robot = ResourceLoader.LoadRobotPrefab().Instantiate<Robot>();
robot.Name = $"Robot #{GameData.robots.Count}";
@@ -97,8 +111,8 @@ public partial class RobotList : PanelContainer
public void OnRobotSelect(int index)
{
//Selected option is the "please select..." option
if (index == 0) return;
spawnId = ItemData.GetIndex(selectableRobots.GetItemText(index));
}
}
+9 -9
View File
@@ -35,24 +35,24 @@ public partial class TutorialBubble : PanelContainer
{
"Welcome to the ruin. I am B.O.B.",
"B.O.B. means Building Operation Buddy. I will keep the lamps on while you teach the robots what to do.",
"The last remaining unit in this ruin saved you from your fall and brought you here. If you want to leave, you have to explore the ruin",
"The last remaining unit in this ruin saved you from your fall and brought you here. If you want to leave, you have to explore the ruin.",
"You do not walk through the ruin yourself. Your robots explore, harvest, craft and carry progress for you.",
"The top bar shows survival pressure: energy, water and food. If those run out, the expedition ends.",
"The necessary resources will be auto-consumed during your time here.",
"For Energy: Steam, Battery v1 and Battery v2; For Thirst: Water; For Food: Mushrooms",
"So try to keep a certain stock of those items to avoid dying in this ruin",
"For energy: steam, battery v1 and battery v2. For thirst: water. For food: mushrooms.",
"Try to keep a stock of those items to avoid dying in this ruin.",
"Open the robot panel (Default: [R]) to inspect your robots. A robot can overheat, lose maintenance and slow down if ignored.",
"A robot that overheated has to cool down for a certain amount of time and cannot execute scripts",
"An overheated robot has to cool down for a while and cannot execute scripts.",
"Use the script editor (Clicking 'Jump to' from the robot panel) to give robots commands. Start simple: move, explore, harvest, then craft.",
"Research unlocks better tools, buildings, robot upgrades and deeper progression. The graph is your technology map (Default: [T]).",
"The necessary ingredients can be found when hovering over the respective technologies in your technology map",
"The required ingredients can be found by hovering over technologies in your technology map.",
"The inventory (Default: [I]) stores everything your robots collect. Gates and research both consume items from it.",
"Each gate blocks the next layer. When you have the required items, use Open Gate in the top bar.",
"The necessary ingredients can be found when hovering over the 'Open Gate' button",
"The required ingredients can be found by hovering over the Open Gate button.",
"The map (Default: [M]) shows what your robots have discovered. Exploration matters because resources are hidden in the ruin.",
"Depper layers contain more advanced resources and unlocking the gate at the lowest point allows you to leave the ruin",
"Be careful: The resources are not endless from the beginning. Converting a robot to a drill-unit (Using the node 'Sacrifice') makes them endless",
"Two things to keep in mind: 1)Endless resources have a lower rate of extraction 2)Sacrificing your last robot without backups in your inventory makes you unable to do anything",
"Deeper layers contain more advanced resources. Unlocking the gate at the lowest point allows you to leave the ruin.",
"Be careful: resources are not endless at the beginning. Converting a robot to a drill unit with Sacrifice makes them endless.",
"Two things to keep in mind: endless resources extract slower, and sacrificing your last robot without a backup in your inventory leaves you stranded.",
"That is enough briefing. Build a loop, keep the robots alive, and open the lower gates. B.O.B. believes in organized chaos."
};
}
+121
View File
@@ -0,0 +1,121 @@
using Godot;
using System.Collections.Generic;
using System.Linq;
public static class GateRequirementGenerator
{
public static void ApplyGateRequirements(Layer[] layers)
{
List<string> availableResources = new List<string>();
foreach (Layer layer in layers)
{
GateRequirementOptions options = BuildRequirementOptions(layer, availableResources);
ApplyLayerRequirements(layer, options);
}
}
private static GateRequirementOptions BuildRequirementOptions(Layer layer, List<string> availableResources)
{
GateRequirementOptions options = new GateRequirementOptions();
foreach (string resource in layer.currentResources)
{
if (availableResources.Contains(resource)) continue;
availableResources.Add(resource);
}
bool addedNewItem;
do
{
addedNewItem = false;
foreach (ItemData item in GameData.availableItems.Values)
{
if (options.PossibleIngredients.Any(existing => existing.Id == item.Id)) continue;
if (!CanCraftItem(item, availableResources)) continue;
options.PossibleIngredients.Add(item);
availableResources.Add(item.Id);
options.LowestCraftTime = Mathf.Min(options.LowestCraftTime, item.CraftTime);
options.HighestCraftTime = Mathf.Max(options.HighestCraftTime, item.CraftTime);
addedNewItem = true;
}
} while (addedNewItem);
return options;
}
private static bool CanCraftItem(ItemData item, List<string> availableResources)
{
return item.Inputs.All(input => availableResources.Contains(input.Item));
}
private static void ApplyLayerRequirements(Layer layer, GateRequirementOptions options)
{
double goalCraftTime = GetGoalCraftTime(layer, options);
int ingredientAmount = Mathf.Clamp(1 + layer.level / 3, 1, 4);
float craftTimeModifier = 0f;
for (int i = 0; i < ingredientAmount; i++)
{
List<ItemData> validIngredients = GetValidIngredients(layer, options, goalCraftTime, craftTimeModifier);
if (validIngredients.Count == 0)
{
i--;
craftTimeModifier += 0.05f;
continue;
}
AddGateIngredient(layer, validIngredients);
craftTimeModifier = 0f;
}
}
private static double GetGoalCraftTime(Layer layer, GateRequirementOptions options)
{
return Mathf.Lerp(
options.LowestCraftTime,
options.HighestCraftTime,
Mathf.Clamp(layer.level / (float)GameData.ruinSize, 0, 1)
);
}
private static List<ItemData> GetValidIngredients(
Layer layer,
GateRequirementOptions options,
double goalCraftTime,
float craftTimeModifier
)
{
double craftTimeLower = goalCraftTime - goalCraftTime * craftTimeModifier;
double craftTimeUpper = goalCraftTime + goalCraftTime * craftTimeModifier;
return options.PossibleIngredients
.Where(item =>
item.CraftTime >= craftTimeLower &&
item.CraftTime <= craftTimeUpper &&
!layer.gateIngredients.Any(ingredient => ingredient.Item == item.Id))
.ToList();
}
private static void AddGateIngredient(Layer layer, List<ItemData> validIngredients)
{
ItemData item = validIngredients[GameData.rand.Next(validIngredients.Count)];
layer.gateIngredients.Add(new Ingredient
{
Item = item.Id,
Amount = GameData.rand.Next(3 + layer.level * 2, 9 + layer.level * 4)
});
}
private class GateRequirementOptions
{
public List<ItemData> PossibleIngredients = new List<ItemData>();
public double HighestCraftTime = 0;
public double LowestCraftTime = double.MaxValue;
}
}
@@ -0,0 +1 @@
uid://bct2we4yxah6v
+4 -9
View File
@@ -16,7 +16,7 @@ public partial class Layer : Node3D
public Vector2I gateCoordinate;
public List<string> currentResources;
public bool isGateOpen = false;
public List<Ingredient> gateIngredients = new();
public List<Ingredient> gateIngredients = new List<Ingredient>();
public override void _Ready()
{
@@ -125,14 +125,12 @@ public partial class Layer : Node3D
for (int z = 0; z < layerSize; z++)
{
if (x == 0 && z == 0 && level == 0) continue;
if (!IsBorder(x, z))
continue;
if (!IsBorder(x, z)) continue;
Tile tile = tiles[x, z];
List<string> possibilities = GetBorderPossibilities(x, z);
if (possibilities.Count == 0)
continue;
if (possibilities.Count == 0) continue;
tile.Collapse(possibilities[GameData.rand.Next(possibilities.Count)]);
Propagate(new Vector2I(x, z));
@@ -147,7 +145,6 @@ public partial class Layer : Node3D
private void GenerateNecessaryTiles()
{
//Generate spawn only in the first layer
if (level == 0)
{
tiles[0, 0].Collapse("spawn");
@@ -197,9 +194,7 @@ public partial class Layer : Node3D
safetyCounter++;
if (safetyCounter == layerSize * layerSize) return false;
}
if (updateFailed) return false;
if (!WFC.IsMapConnected(tiles, 1f)) return false;
return true;
return !updateFailed && WFC.IsMapConnected(tiles, 1f);
}
private void Propagate(Vector2I startPos)
+3 -4
View File
@@ -1,7 +1,7 @@
using Godot;
using System.Collections.Generic;
public class LightHandler
public static class LightHandler
{
public static List<OmniLight3D> lights = new List<OmniLight3D>();
@@ -11,12 +11,11 @@ public class LightHandler
foreach (OmniLight3D light in lights)
{
if (GodotObject.IsInstanceValid(light))
{
if (!GodotObject.IsInstanceValid(light)) continue;
light.LightColor = color;
availableLights.Add(light);
}
}
lights = availableLights;
}
+41 -36
View File
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using Godot;
public class Pathfinding
public static class Pathfinding
{
private static AStar3D aStar = new AStar3D();
private static Dictionary<Vector3I, long> coordToId = new Dictionary<Vector3I, long>();
@@ -11,8 +11,7 @@ public class Pathfinding
private static long GetOrCreateId(Vector3I coord)
{
if (coordToId.TryGetValue(coord, out long id))
return id;
if (coordToId.TryGetValue(coord, out long id)) return id;
id = nextId++;
coordToId[coord] = id;
@@ -35,23 +34,34 @@ public class Pathfinding
{
for (int z = 0; z < GameData.layerSize; z++)
{
Vector3I coord = new Vector3I(x, y, z);
Tile tile = GameData.map[y].tiles[x, z];
if (tile == null || tile.collapsedMesh == null) continue;
long id = GetOrCreateId(coord);
aStar.AddPoint(id, tile.Position);
AddPointIfValid(x, y, z);
}
}
}
foreach (KeyValuePair<Vector3I, long> kvp in coordToId)
{
Vector3I from = kvp.Key;
long fromId = kvp.Value;
ConnectPoint(kvp.Key, kvp.Value);
}
for (int y = 0; y < GameData.ruinSize; y++)
{
UpdateGatePoint(y, false);
}
}
private static void AddPointIfValid(int x, int y, int z)
{
Vector3I coord = new Vector3I(x, y, z);
Tile tile = GameData.map[y].tiles[x, z];
if (tile == null || tile.collapsedMesh == null) return;
long id = GetOrCreateId(coord);
aStar.AddPoint(id, tile.Position);
}
private static void ConnectPoint(Vector3I from, long fromId)
{
foreach (Vector3I offset in WFC.offsets3D)
{
Vector3I to = new Vector3I(
@@ -61,35 +71,34 @@ public class Pathfinding
);
if (!coordToId.ContainsKey(to)) continue;
if (!WFC.CanWalk3D(from, to)) continue;
long toId = coordToId[to];
if (TryRegisterGateConnection(from, to, fromId, toId)) continue;
if (from.Y != to.Y && GameData.map[from.Y].tiles[from.X, from.Z].collapsedMesh == "gate")
ConnectPointsIfNeeded(fromId, toId);
}
}
private static bool TryRegisterGateConnection(Vector3I from, Vector3I to, long fromId, long toId)
{
if (from.Y == to.Y) return false;
if (GameData.map[from.Y].tiles[from.X, from.Z].collapsedMesh != "gate") return false;
verticalConnections[from.Y] = (fromId, toId);
if (GameData.map[from.Y].isGateOpen)
{
if (!aStar.ArePointsConnected(fromId, toId))
{
aStar.ConnectPoints(fromId, toId, true);
}
}
continue;
ConnectPointsIfNeeded(fromId, toId);
}
if (!aStar.ArePointsConnected(fromId, toId))
{
aStar.ConnectPoints(fromId, toId, true);
}
}
return true;
}
for (int y = 0; y < GameData.ruinSize; y++)
private static void ConnectPointsIfNeeded(long fromId, long toId)
{
UpdateGatePoint(y, false);
}
if (aStar.ArePointsConnected(fromId, toId)) return;
aStar.ConnectPoints(fromId, toId, true);
}
public static void UpdateGatePoint(int layer, bool isOpen)
@@ -100,19 +109,15 @@ public class Pathfinding
if (isOpen)
{
if (!aStar.ArePointsConnected(fromId, toId))
{
aStar.ConnectPoints(fromId, toId, true);
ConnectPointsIfNeeded(fromId, toId);
return;
}
}
else
{
if (aStar.ArePointsConnected(fromId, toId))
{
aStar.DisconnectPoints(fromId, toId);
}
}
}
public static List<Vector3> GetPath(Vector3I start, Vector3I end)
{
+10 -10
View File
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using Godot;
public class ResourceDistributor
public static class ResourceDistributor
{
public static Dictionary<string, Texture2D> resources = ResourceLoader.LoadResourceSymbols();
public static Dictionary<string, float[]> weights = ResourceLoader.LoadResourceWeights();
@@ -14,27 +14,27 @@ public class ResourceDistributor
private static string ChooseWeighted(int layer)
{
float totalWeight = 0f;
float weightLerp;
foreach (string resource in resources.Keys)
{
weightLerp = Mathf.Lerp(weights[resource][0], weights[resource][1], layer);
totalWeight += weightLerp;
totalWeight += GetWeight(resource, layer);
}
float r = (float)(GameData.rand.NextDouble() * totalWeight);
float randomWeight = (float)(GameData.rand.NextDouble() * totalWeight);
float cumulative = 0f;
foreach (string resource in resources.Keys)
{
weightLerp = Mathf.Lerp(weights[resource][0], weights[resource][1], layer);
cumulative += weightLerp;
cumulative += GetWeight(resource, layer);
if (r <= cumulative)
return resource;
if (randomWeight <= cumulative) return resource;
}
return "stone";
}
private static float GetWeight(string resource, int layer)
{
return Mathf.Lerp(weights[resource][0], weights[resource][1], layer);
}
}
+30 -40
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Godot;
public class WFC
public static class WFC
{
public static Dictionary<string, Dictionary<Direction, List<string>>> adjacency = new Dictionary<string, Dictionary<Direction, List<string>>>();
public enum Direction
@@ -122,8 +122,6 @@ public class WFC
return aOpen == bOpen;
}
public static void FillAdjacencies()
{
foreach (string tile in tileConnections.Keys)
@@ -136,8 +134,7 @@ public class WFC
foreach (string other in tileConnections.Keys)
{
if (CanConnect(tile, other, dir, false))
valid.Add(other);
if (CanConnect(tile, other, dir, false)) valid.Add(other);
}
adjacency[tile][dir] = valid;
@@ -156,8 +153,7 @@ public class WFC
Tile fromTile = layer[from.X, from.Y];
Tile toTile = layer[to.X, to.Y];
if (!IsWalkable(toTile))
return false;
if (!IsWalkable(toTile)) return false;
return CanConnect(fromTile.collapsedMesh, toTile.collapsedMesh, dir, true);
}
@@ -167,29 +163,21 @@ public class WFC
Tile fromTile = GameData.map[from.Y].tiles[from.X, from.Z];
Tile toTile = GameData.map[to.Y].tiles[to.X, to.Z];
if (fromTile == null || toTile == null)
return false;
if (fromTile == null || toTile == null) return false;
if (from.Y != to.Y)
{
if (Math.Abs(from.Y - to.Y) != 1)
return false;
if (Math.Abs(from.Y - to.Y) != 1) return false;
if (from.Y > to.Y)
{
return toTile.collapsedMesh == "gate";
}
else
{
return fromTile.collapsedMesh == "gate";
}
return from.Y > to.Y
? toTile.collapsedMesh == "gate"
: fromTile.collapsedMesh == "gate";
}
int dx = to.X - from.X;
int dz = to.Z - from.Z;
if (Math.Abs(dx) + Math.Abs(dz) != 1)
return false;
if (Math.Abs(dx) + Math.Abs(dz) != 1) return false;
Direction dir;
@@ -207,29 +195,23 @@ public class WFC
);
}
public static bool IsMapConnected(Tile[,] layer, float accessibilityThreshhold)
public static bool IsMapConnected(Tile[,] layer, float accessibilityThreshold)
{
bool result = false;
HashSet<Vector2I> visited = new HashSet<Vector2I>();
List<Vector2I> toCheck = new List<Vector2I>();
Vector2I position;
toCheck.Add(new Vector2I(1, 1));
int safetyCounter = 0;
while (true)
while (toCheck.Count > 0)
{
if (toCheck.Count <= 0) break;
int index = GameData.rand.Next(toCheck.Count);
position = toCheck[index];
toCheck[index] = toCheck[^1];
toCheck.RemoveAt(toCheck.Count - 1);
Vector2I position = TakeRandomPosition(toCheck);
if (!visited.Add(position)) continue;
for (int i = 0; i < offsets2D.Length; i++)
{
Vector2I next = position + offsets2D[i];
if (!InBounds(next, layer.GetLength(0)))
continue;
if (!InBounds(next, layer.GetLength(0))) continue;
if (CanWalk(layer, position, next, dirs[i]))
{
@@ -238,22 +220,30 @@ public class WFC
}
safetyCounter++;
if (safetyCounter > layer.Length * 2) break;
if (visited.Count >= Math.Pow(layer.GetLength(0) - 1, 2) * accessibilityThreshhold)
if (visited.Count >= Math.Pow(layer.GetLength(0) - 1, 2) * accessibilityThreshold)
{
result = true;
break;
return true;
}
}
return false;
}
return result;
private static Vector2I TakeRandomPosition(List<Vector2I> positions)
{
int index = GameData.rand.Next(positions.Count);
Vector2I position = positions[index];
positions[index] = positions[positions.Count - 1];
positions.RemoveAt(positions.Count - 1);
return position;
}
public static bool InBounds(Vector2I pos, int layerSize)
{
return pos.X > 0 &&
pos.Y > 0 &&
pos.X < layerSize &&
pos.Y < layerSize;
return pos.X > 0
&& pos.Y > 0
&& pos.X < layerSize
&& pos.Y < layerSize;
}
public static List<string> GetBorderPossibilities(int x, int z)
+1 -81
View File
@@ -1,6 +1,5 @@
using Godot;
using System.Collections.Generic;
using System.Linq;
using static GameData;
public partial class World : Node3D
@@ -48,7 +47,7 @@ public partial class World : Node3D
map = new Layer[ruinSize];
GenerateWorld();
SetGateRequirements();
GateRequirementGenerator.ApplyGateRequirements(map);
if (shouldLoadSave && saveGame != null)
{
@@ -288,83 +287,4 @@ public partial class World : Node3D
}
}
private void SetGateRequirements()
{
List<string> availableResources = new List<string>();
List<ItemData> possibleIngredients;
bool canCraft;
double highestCraftTime;
double lowestCraftTime;
foreach (Layer layer in map)
{
highestCraftTime = 0;
lowestCraftTime = double.MaxValue;
possibleIngredients = new List<ItemData>();
//Step 1: Determine all possible resources for this and all previous layers combined
foreach (string resource in layer.currentResources)
{
if (availableResources.Contains(resource)) continue;
availableResources.Add(resource);
}
//Step 2: Check which items can be crafted with those items, repeat until no further items are added to the list
bool addedNewItem;
do
{
addedNewItem = false;
foreach (ItemData item in availableItems.Values)
{
if (possibleIngredients.Any(existing => existing.Id == item.Id))
continue;
canCraft = item.Inputs.All(input => availableResources.Contains(input.Item));
if (!canCraft)
continue;
possibleIngredients.Add(item);
availableResources.Add(item.Id);
lowestCraftTime = Mathf.Min(lowestCraftTime, item.CraftTime);
highestCraftTime = Mathf.Max(highestCraftTime, item.CraftTime);
addedNewItem = true;
}
} while (addedNewItem);
//Step 3: Choose gate items needed based on crafting time and layer it is for (Lower layers -> More advanced items -> More crafting time)
double goalCraftTime = Mathf.Lerp(lowestCraftTime, highestCraftTime, Mathf.Clamp(layer.level/(float)ruinSize, 0, 1));
int ingredientAmount = Mathf.Clamp(1 + layer.level / 3, 1, 4);
float craftTimeModifier = 0f;
double craftTimeLower, craftTimeUpper;
for (int i = 0; i < ingredientAmount; i++)
{
craftTimeLower = goalCraftTime - goalCraftTime * craftTimeModifier;
craftTimeUpper = goalCraftTime + goalCraftTime * craftTimeModifier;
List<ItemData> validIngredients = possibleIngredients
.Where(item =>
item.CraftTime >= craftTimeLower &&
item.CraftTime <= craftTimeUpper &&
!layer.gateIngredients.Any(ingredient => ingredient.Item == item.Id))
.ToList();
if (validIngredients.Count == 0)
{
i--;
craftTimeModifier += 0.05f;
continue;
}
ItemData item = validIngredients[rand.Next(validIngredients.Count)];
layer.gateIngredients.Add(new Ingredient
{
Item = item.Id,
Amount = rand.Next(3 + layer.level * 2, 9 + layer.level * 4)
});
craftTimeModifier = 0f;
}
}
}
}