Big project cleanup with overhaul of file responsibilities (KISS) and code (DRY, YAGNI)

This commit is contained in:
2026-05-14 11:17:02 +02:00
parent bd6cdeb97b
commit 300c8f5a42
54 changed files with 2030 additions and 1745 deletions
+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)
+14 -15
View File
@@ -1,23 +1,22 @@
using Godot;
using System.Collections.Generic;
public class LightHandler
public static class LightHandler
{
public static List<OmniLight3D> lights = new List<OmniLight3D>();
public static List<OmniLight3D> lights = new List<OmniLight3D>();
public static void RedrawLights(Color color)
{
List<OmniLight3D> availableLights = new List<OmniLight3D>();
public static void RedrawLights(Color color)
{
List<OmniLight3D> availableLights = new List<OmniLight3D>();
foreach (OmniLight3D light in lights)
{
if (GodotObject.IsInstanceValid(light))
{
light.LightColor = color;
availableLights.Add(light);
}
}
foreach (OmniLight3D light in lights)
{
if (!GodotObject.IsInstanceValid(light)) continue;
lights = availableLights;
}
light.LightColor = color;
availableLights.Add(light);
}
lights = availableLights;
}
}
+108 -103
View File
@@ -1,131 +1,136 @@
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>();
private static Dictionary<long, Vector3I> idToCoord = new Dictionary<long, Vector3I>();
private static long nextId = 1;
private static Dictionary<int, (long fromId, long toId)> verticalConnections = new Dictionary<int, (long fromId, long toId)>();
private static AStar3D aStar = new AStar3D();
private static Dictionary<Vector3I, long> coordToId = new Dictionary<Vector3I, long>();
private static Dictionary<long, Vector3I> idToCoord = new Dictionary<long, Vector3I>();
private static long nextId = 1;
private static Dictionary<int, (long fromId, long toId)> verticalConnections = new Dictionary<int, (long fromId, long toId)>();
private static long GetOrCreateId(Vector3I coord)
{
if (coordToId.TryGetValue(coord, out long id))
return id;
private static long GetOrCreateId(Vector3I coord)
{
if (coordToId.TryGetValue(coord, out long id)) return id;
id = nextId++;
coordToId[coord] = id;
idToCoord[id] = coord;
id = nextId++;
coordToId[coord] = id;
idToCoord[id] = coord;
return id;
}
return id;
}
public static void BuildAStarGraph()
{
aStar.Clear();
coordToId.Clear();
idToCoord.Clear();
verticalConnections.Clear();
nextId = 1;
public static void BuildAStarGraph()
{
aStar.Clear();
coordToId.Clear();
idToCoord.Clear();
verticalConnections.Clear();
nextId = 1;
for (int y = 0; y < GameData.ruinSize; y++)
{
for (int x = 0; x < GameData.layerSize; x++)
{
for (int z = 0; z < GameData.layerSize; z++)
{
Vector3I coord = new Vector3I(x, y, z);
Tile tile = GameData.map[y].tiles[x, z];
for (int y = 0; y < GameData.ruinSize; y++)
{
for (int x = 0; x < GameData.layerSize; x++)
{
for (int z = 0; z < GameData.layerSize; z++)
{
AddPointIfValid(x, y, z);
}
}
}
if (tile == null || tile.collapsedMesh == null) continue;
foreach (KeyValuePair<Vector3I, long> kvp in coordToId)
{
ConnectPoint(kvp.Key, kvp.Value);
}
long id = GetOrCreateId(coord);
for (int y = 0; y < GameData.ruinSize; y++)
{
UpdateGatePoint(y, false);
}
}
aStar.AddPoint(id, tile.Position);
}
}
}
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;
foreach (KeyValuePair<Vector3I, long> kvp in coordToId)
{
Vector3I from = kvp.Key;
long fromId = kvp.Value;
long id = GetOrCreateId(coord);
aStar.AddPoint(id, tile.Position);
}
foreach (Vector3I offset in WFC.offsets3D)
{
Vector3I to = new Vector3I(
from.X + offset.X,
from.Y + offset.Y,
from.Z + offset.Z
);
private static void ConnectPoint(Vector3I from, long fromId)
{
foreach (Vector3I offset in WFC.offsets3D)
{
Vector3I to = new Vector3I(
from.X + offset.X,
from.Y + offset.Y,
from.Z + offset.Z
);
if (!coordToId.ContainsKey(to)) continue;
if (!coordToId.ContainsKey(to)) continue;
if (!WFC.CanWalk3D(from, to)) continue;
if (!WFC.CanWalk3D(from, to)) continue;
long toId = coordToId[to];
if (TryRegisterGateConnection(from, to, fromId, toId)) continue;
long toId = coordToId[to];
ConnectPointsIfNeeded(fromId, toId);
}
}
if (from.Y != to.Y && GameData.map[from.Y].tiles[from.X, from.Z].collapsedMesh == "gate")
{
verticalConnections[from.Y] = (fromId, toId);
if (GameData.map[from.Y].isGateOpen)
{
if (!aStar.ArePointsConnected(fromId, toId))
{
aStar.ConnectPoints(fromId, toId, true);
}
}
continue;
}
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;
if (!aStar.ArePointsConnected(fromId, toId))
{
aStar.ConnectPoints(fromId, toId, true);
}
}
}
verticalConnections[from.Y] = (fromId, toId);
if (GameData.map[from.Y].isGateOpen)
{
ConnectPointsIfNeeded(fromId, toId);
}
for (int y = 0; y < GameData.ruinSize; y++)
{
UpdateGatePoint(y, false);
}
}
return true;
}
public static void UpdateGatePoint(int layer, bool isOpen)
{
if (!verticalConnections.ContainsKey(layer)) return;
private static void ConnectPointsIfNeeded(long fromId, long toId)
{
if (aStar.ArePointsConnected(fromId, toId)) return;
(long fromId, long toId) = verticalConnections[layer];
aStar.ConnectPoints(fromId, toId, true);
}
if (isOpen)
{
if (!aStar.ArePointsConnected(fromId, toId))
{
aStar.ConnectPoints(fromId, toId, true);
}
}
else
{
if (aStar.ArePointsConnected(fromId, toId))
{
aStar.DisconnectPoints(fromId, toId);
}
}
}
public static void UpdateGatePoint(int layer, bool isOpen)
{
if (!verticalConnections.ContainsKey(layer)) return;
public static List<Vector3> GetPath(Vector3I start, Vector3I end)
{
if (!coordToId.ContainsKey(start) || !coordToId.ContainsKey(end)) return new List<Vector3>();
(long fromId, long toId) = verticalConnections[layer];
long startId = coordToId[start];
long endId = coordToId[end];
if (isOpen)
{
ConnectPointsIfNeeded(fromId, toId);
return;
}
return new List<Vector3>(aStar.GetPointPath(startId, endId));
}
if (aStar.ArePointsConnected(fromId, toId))
{
aStar.DisconnectPoints(fromId, toId);
}
}
public static Vector3I GetClosestStartPoint(Vector3 robotPosition)
{
return idToCoord[aStar.GetClosestPoint(robotPosition)];
}
public static List<Vector3> GetPath(Vector3I start, Vector3I end)
{
if (!coordToId.ContainsKey(start) || !coordToId.ContainsKey(end)) return new List<Vector3>();
long startId = coordToId[start];
long endId = coordToId[end];
return new List<Vector3>(aStar.GetPointPath(startId, endId));
}
public static Vector3I GetClosestStartPoint(Vector3 robotPosition)
{
return idToCoord[aStar.GetClosestPoint(robotPosition)];
}
}
+17 -17
View File
@@ -1,40 +1,40 @@
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();
public static Dictionary<string, Texture2D> resources = ResourceLoader.LoadResourceSymbols();
public static Dictionary<string, float[]> weights = ResourceLoader.LoadResourceWeights();
public static string GetResource(int layer)
{
return ChooseWeighted(layer);
}
public static string GetResource(int layer)
{
return ChooseWeighted(layer);
}
private static string ChooseWeighted(int layer)
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);
}
}
+31 -41
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 result;
return false;
}
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;
}
}
}
}