add Nodule Building
This commit is contained in:
17
AGENTS.md
Normal file
17
AGENTS.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Slimepire Project — Agent Rules
|
||||||
|
|
||||||
|
## Before editing ANY file: ALWAYS re-read it first
|
||||||
|
|
||||||
|
I often edit code manually while the agent is running. If you edit a file based on
|
||||||
|
stale content from a previous read, you will overwrite my manual changes.
|
||||||
|
|
||||||
|
**Rule:** Before calling `edit` or `write` on any existing file, read it again with
|
||||||
|
`read` to get the current state. Never rely on file contents from earlier turns or
|
||||||
|
from a previous compaction/context window.
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
- Godot 4.7 C# project targeting .NET 10.0
|
||||||
|
- Uses `GodotSharp.SourceGenerators` — provides `[Singleton]`, `[SceneTree]`,
|
||||||
|
`[ResourceTree]`, and `Instantiator.Instantiate<T>()`
|
||||||
|
- Keep things simple, avoid over-engineering
|
||||||
@@ -87,7 +87,10 @@ public partial class BuildGhost : Node2D
|
|||||||
{
|
{
|
||||||
var nodule = NoduleScene.Instantiate<Nodule>();
|
var nodule = NoduleScene.Instantiate<Nodule>();
|
||||||
nodule.Position = Position;
|
nodule.Position = Position;
|
||||||
|
nodule.EnergyBuildCost = 5f; // TODO: make configurable per nodule type
|
||||||
|
nodule.ConstructionProgress = 0;
|
||||||
GetParent()?.AddChild(nodule);
|
GetParent()?.AddChild(nodule);
|
||||||
|
NoduleManager.Instance.RequestConstruction(nodule);
|
||||||
CleanupBuildMode();
|
CleanupBuildMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/Gameplay/Components/Energy/EnergyPackage.cs
Normal file
49
src/Gameplay/Components/Energy/EnergyPackage.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Slimepire.Gameplay.Components;
|
||||||
|
|
||||||
|
[SceneTree]
|
||||||
|
public partial class EnergyPackage : Node2D
|
||||||
|
{
|
||||||
|
[Export]
|
||||||
|
public float TravelSpeed { get; set; } = 200f;
|
||||||
|
|
||||||
|
private Vector2[] _waypoints = [];
|
||||||
|
private int _currentWaypointIndex;
|
||||||
|
|
||||||
|
public event Action? Arrived;
|
||||||
|
|
||||||
|
public void SetWaypoints(Vector2[] waypoints)
|
||||||
|
{
|
||||||
|
_waypoints = waypoints;
|
||||||
|
if (_waypoints.Length > 0)
|
||||||
|
{
|
||||||
|
GlobalPosition = _waypoints[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (_waypoints.Length == 0 || _currentWaypointIndex >= _waypoints.Length - 1)
|
||||||
|
{
|
||||||
|
Arrived?.Invoke();
|
||||||
|
QueueFree();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = _waypoints[_currentWaypointIndex + 1];
|
||||||
|
var direction = target - GlobalPosition;
|
||||||
|
var distance = direction.Length();
|
||||||
|
var step = TravelSpeed * (float)delta;
|
||||||
|
|
||||||
|
if (step >= distance)
|
||||||
|
{
|
||||||
|
GlobalPosition = target;
|
||||||
|
_currentWaypointIndex++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GlobalPosition += direction.Normalized() * step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/Gameplay/Components/Energy/EnergyPackage.cs.uid
Normal file
1
src/Gameplay/Components/Energy/EnergyPackage.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://nk5tjlmcrpn2
|
||||||
29
src/Gameplay/Components/Energy/EnergyPackage.tscn
Normal file
29
src/Gameplay/Components/Energy/EnergyPackage.tscn
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dwdbqktsl34sm"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://nk5tjlmcrpn2" path="res://src/Gameplay/Components/Energy/EnergyPackage.cs" id="1_kmtmh"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bjl6gqj0bvhhp" path="res://assets/energy_package.png" id="2_psum7"]
|
||||||
|
|
||||||
|
[sub_resource type="Curve" id="Curve_psum7"]
|
||||||
|
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1e-05, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
|
||||||
|
point_count = 3
|
||||||
|
|
||||||
|
[sub_resource type="CurveTexture" id="CurveTexture_psum7"]
|
||||||
|
curve = SubResource("Curve_psum7")
|
||||||
|
|
||||||
|
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_psum7"]
|
||||||
|
particle_flag_disable_z = true
|
||||||
|
inherit_velocity_ratio = 0.625
|
||||||
|
gravity = Vector3(0, 10, 0)
|
||||||
|
scale_curve = SubResource("CurveTexture_psum7")
|
||||||
|
|
||||||
|
[node name="EnergyPackage" type="Node2D" unique_id=869544034]
|
||||||
|
z_index = 2
|
||||||
|
script = ExtResource("1_kmtmh")
|
||||||
|
|
||||||
|
[node name="GPUParticles2D" type="GPUParticles2D" parent="." unique_id=388560926]
|
||||||
|
modulate = Color(1, 0.7764706, 0, 1)
|
||||||
|
texture = ExtResource("2_psum7")
|
||||||
|
lifetime = 0.3
|
||||||
|
use_fixed_seed = true
|
||||||
|
seed = 874795105
|
||||||
|
process_material = SubResource("ParticleProcessMaterial_psum7")
|
||||||
@@ -16,20 +16,22 @@ public partial class EnergyStorage : Node
|
|||||||
get;
|
get;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (field + value > MaxEnergy)
|
field = Mathf.Clamp(field + value, 0, MaxEnergy);
|
||||||
{
|
|
||||||
field = MaxEnergy;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
field += value;
|
|
||||||
}
|
|
||||||
|
|
||||||
field = value;
|
|
||||||
UpdateLabel();
|
UpdateLabel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryConsume(float amount)
|
||||||
|
{
|
||||||
|
if (Energy >= amount)
|
||||||
|
{
|
||||||
|
Energy = -amount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
UpdateLabel();
|
UpdateLabel();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
|
using Slimepire.Gameplay.Components;
|
||||||
using Slimepire.Gameplay.Nodules;
|
using Slimepire.Gameplay.Nodules;
|
||||||
|
|
||||||
namespace Slimepire.Gameplay;
|
namespace Slimepire.Gameplay;
|
||||||
@@ -6,11 +7,16 @@ namespace Slimepire.Gameplay;
|
|||||||
[Singleton]
|
[Singleton]
|
||||||
public partial class NoduleManager
|
public partial class NoduleManager
|
||||||
{
|
{
|
||||||
|
private const float PackageSendInterval = 0.3f;
|
||||||
|
private const float ConstructionRetryInterval = 2f;
|
||||||
|
|
||||||
public bool Building { get; set; }
|
public bool Building { get; set; }
|
||||||
|
|
||||||
private AStar2D _aStar = new();
|
private AStar2D _aStar = new();
|
||||||
private readonly Dictionary<long, Nodule> _nodules = [];
|
private readonly Dictionary<long, Nodule> _nodules = [];
|
||||||
private readonly HashSet<NoduleConnection> _connections = [];
|
private readonly HashSet<NoduleConnection> _connections = [];
|
||||||
|
private readonly List<RootNodule> _rootNodules = [];
|
||||||
|
private readonly Dictionary<long, ConstructionRequest> _activeConstructions = [];
|
||||||
|
|
||||||
// GFX
|
// GFX
|
||||||
private readonly Dictionary<NoduleConnection, Tube> _tubeConnections = new();
|
private readonly Dictionary<NoduleConnection, Tube> _tubeConnections = new();
|
||||||
@@ -20,6 +26,11 @@ public partial class NoduleManager
|
|||||||
_nodules.Add(nodule.NoduleId, nodule);
|
_nodules.Add(nodule.NoduleId, nodule);
|
||||||
_aStar.AddPoint(nodule.NoduleId, nodule.Position);
|
_aStar.AddPoint(nodule.NoduleId, nodule.Position);
|
||||||
|
|
||||||
|
if (nodule is RootNodule root && !_rootNodules.Contains(root))
|
||||||
|
{
|
||||||
|
_rootNodules.Add(root);
|
||||||
|
}
|
||||||
|
|
||||||
UpdateNoduleConnections();
|
UpdateNoduleConnections();
|
||||||
UpdateTubesAndAstar();
|
UpdateTubesAndAstar();
|
||||||
}
|
}
|
||||||
@@ -30,6 +41,176 @@ public partial class NoduleManager
|
|||||||
UpdateTubesAndAstar();
|
UpdateTubesAndAstar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnNoduleConstructed(Nodule nodule)
|
||||||
|
{
|
||||||
|
// Add all AStar connections that were skipped during construction
|
||||||
|
foreach (var neighbor in nodule.Connections)
|
||||||
|
{
|
||||||
|
_aStar.ConnectPoints(nodule.NoduleId, neighbor.NoduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert any ghost tubes connected to this nodule to real tubes.
|
||||||
|
// Tubes are not always children of this nodule (depends on hash ordering in NoduleConnection),
|
||||||
|
// so iterating children alone isn't enough.
|
||||||
|
foreach (var (conn, tube) in _tubeConnections)
|
||||||
|
{
|
||||||
|
if (tube.IsGhost && (ReferenceEquals(conn.A, nodule) || ReferenceEquals(conn.B, nodule)))
|
||||||
|
{
|
||||||
|
tube.SetGhost(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestConstruction(Nodule target)
|
||||||
|
{
|
||||||
|
if (!target.IsConstructing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Defer to allow connections to be established (physics frame overlap detection)
|
||||||
|
var tree = target.GetTree();
|
||||||
|
tree.CreateTimer(0.1f).Timeout += () => TryStartConstruction(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryStartConstruction(Nodule target)
|
||||||
|
{
|
||||||
|
// Constructing nodule has no AStar connections yet — route via neighbors
|
||||||
|
var (sourceRoot, waypoints) = FindPathViaNeighbors(target);
|
||||||
|
if (sourceRoot == null || waypoints.Length == 0)
|
||||||
|
{
|
||||||
|
// No path yet — retry periodically until a RootNodule becomes reachable
|
||||||
|
// TODO: ist das hier richtig?
|
||||||
|
var tree = target.GetTree();
|
||||||
|
tree.CreateTimer(ConstructionRetryInterval).Timeout += () =>
|
||||||
|
{
|
||||||
|
if (target.IsConstructing && !_activeConstructions.ContainsKey(target.NoduleId))
|
||||||
|
TryStartConstruction(target);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new ConstructionRequest
|
||||||
|
{
|
||||||
|
Target = target,
|
||||||
|
SourceRoot = sourceRoot,
|
||||||
|
Waypoints = waypoints,
|
||||||
|
};
|
||||||
|
|
||||||
|
_activeConstructions[target.NoduleId] = request;
|
||||||
|
SendNextPackage(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendNextPackage(ConstructionRequest request)
|
||||||
|
{
|
||||||
|
var target = request.Target;
|
||||||
|
|
||||||
|
if (!target.IsConstructing)
|
||||||
|
{
|
||||||
|
_activeConstructions.Remove(target.NoduleId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var energyStorage = request.SourceRoot.GetNode<EnergyStorage>("EnergyStorage");
|
||||||
|
if (energyStorage == null || !energyStorage.TryConsume(1))
|
||||||
|
{
|
||||||
|
// Source empty — retry after a delay
|
||||||
|
var tree = request.SourceRoot.GetTree();
|
||||||
|
tree.CreateTimer(PackageSendInterval).Timeout += () =>
|
||||||
|
{
|
||||||
|
if (_activeConstructions.ContainsKey(target.NoduleId))
|
||||||
|
SendNextPackage(request);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var package = Instantiator.Instantiate<EnergyPackage>();
|
||||||
|
package.SetWaypoints(request.Waypoints);
|
||||||
|
|
||||||
|
// Add to tree as sibling of source root so it's visible
|
||||||
|
var parent = request.SourceRoot.GetParent();
|
||||||
|
parent?.AddChild(package);
|
||||||
|
package.GlobalPosition = request.Waypoints[0];
|
||||||
|
|
||||||
|
package.Arrived += () =>
|
||||||
|
{
|
||||||
|
target.ReceiveConstructionEnergy(1);
|
||||||
|
|
||||||
|
if (!target.IsConstructing)
|
||||||
|
{
|
||||||
|
_activeConstructions.Remove(target.NoduleId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send next package after a short interval
|
||||||
|
var tree = target.GetTree();
|
||||||
|
tree.CreateTimer(PackageSendInterval).Timeout += () =>
|
||||||
|
{
|
||||||
|
if (_activeConstructions.TryGetValue(target.NoduleId, out var req))
|
||||||
|
SendNextPackage(req);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private (RootNodule? source, Vector2[] waypoints) FindPathViaNeighbors(Nodule target)
|
||||||
|
{
|
||||||
|
RootNodule? bestRoot = null;
|
||||||
|
Vector2[] bestWaypoints = [];
|
||||||
|
float bestDist = float.MaxValue;
|
||||||
|
|
||||||
|
foreach (var neighbor in target.Connections)
|
||||||
|
{
|
||||||
|
foreach (var root in _rootNodules)
|
||||||
|
{
|
||||||
|
var pointPath = _aStar.GetPointPath(root.NoduleId, neighbor.NoduleId);
|
||||||
|
if (pointPath.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var dist = 0f;
|
||||||
|
for (var i = 0; i < pointPath.Length - 1; i++)
|
||||||
|
dist += pointPath[i].DistanceTo(pointPath[i + 1]);
|
||||||
|
dist += pointPath[^1].DistanceTo(target.GlobalPosition);
|
||||||
|
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = dist;
|
||||||
|
bestRoot = root;
|
||||||
|
bestWaypoints = [.. pointPath, target.GlobalPosition];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bestRoot, bestWaypoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (RootNodule? source, long[] path) FindNearestRootNodule(Nodule target)
|
||||||
|
{
|
||||||
|
RootNodule? nearest = null;
|
||||||
|
float shortestDist = float.MaxValue;
|
||||||
|
long[] shortestIds = [];
|
||||||
|
|
||||||
|
foreach (var root in _rootNodules)
|
||||||
|
{
|
||||||
|
var ids = _aStar.GetIdPath(root.NoduleId, target.NoduleId);
|
||||||
|
if (ids.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var path = _aStar.GetPointPath(root.NoduleId, target.NoduleId);
|
||||||
|
var dist = 0f;
|
||||||
|
for (var i = 0; i < path.Length - 1; i++)
|
||||||
|
{
|
||||||
|
dist += path[i].DistanceTo(path[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dist < shortestDist)
|
||||||
|
{
|
||||||
|
shortestDist = dist;
|
||||||
|
nearest = root;
|
||||||
|
shortestIds = [.. ids];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (nearest, shortestIds);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateNoduleConnections()
|
private void UpdateNoduleConnections()
|
||||||
{
|
{
|
||||||
foreach (var nodule in _nodules.Values)
|
foreach (var nodule in _nodules.Values)
|
||||||
@@ -53,10 +234,19 @@ public partial class NoduleManager
|
|||||||
conn.A.AddChild(tube);
|
conn.A.AddChild(tube);
|
||||||
tube.SetTarget(conn.B.GlobalPosition);
|
tube.SetTarget(conn.B.GlobalPosition);
|
||||||
|
|
||||||
|
// Ghost tubes for constructing nodules — visual only, no AStar
|
||||||
|
var eitherConstructing = conn.A.IsConstructing || conn.B.IsConstructing;
|
||||||
|
if (eitherConstructing)
|
||||||
|
{
|
||||||
|
tube.SetGhost(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
_aStar.ConnectPoints(conn.A.NoduleId, conn.B.NoduleId);
|
_aStar.ConnectPoints(conn.A.NoduleId, conn.B.NoduleId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record NoduleConnection
|
private record NoduleConnection
|
||||||
{
|
{
|
||||||
@@ -89,4 +279,11 @@ public partial class NoduleManager
|
|||||||
return HashCode.Combine(A, B);
|
return HashCode.Combine(A, B);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ConstructionRequest
|
||||||
|
{
|
||||||
|
public required Nodule Target { get; init; }
|
||||||
|
public required RootNodule SourceRoot { get; init; }
|
||||||
|
public required Vector2[] Waypoints { get; init; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,19 @@ public partial class Nodule : Node2D
|
|||||||
[Export]
|
[Export]
|
||||||
private AnimatedSprite2D _gfx = null!;
|
private AnimatedSprite2D _gfx = null!;
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public float EnergyBuildCost { get; set; } = 0f;
|
||||||
|
|
||||||
|
public float ConstructionProgress { get; set; }
|
||||||
|
|
||||||
|
public bool IsConstructing => EnergyBuildCost > 0 && ConstructionProgress < EnergyBuildCost;
|
||||||
|
|
||||||
public List<Nodule> Connections { get; } = [];
|
public List<Nodule> Connections { get; } = [];
|
||||||
|
|
||||||
protected AnimatedSprite2D Gfx => _gfx;
|
protected AnimatedSprite2D Gfx => _gfx;
|
||||||
|
|
||||||
public long NoduleId => (long)GetInstanceId();
|
public long NoduleId => (long)GetInstanceId();
|
||||||
|
|
||||||
public void AttachTube(Tube tube)
|
|
||||||
{
|
|
||||||
AddChild(tube);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
_tubeConnectionArea.AreaEntered += OnAreaEntered;
|
_tubeConnectionArea.AreaEntered += OnAreaEntered;
|
||||||
@@ -32,8 +34,16 @@ public partial class Nodule : Node2D
|
|||||||
|
|
||||||
// render above tubes
|
// render above tubes
|
||||||
ZIndex = 1;
|
ZIndex = 1;
|
||||||
|
|
||||||
|
if (IsConstructing)
|
||||||
|
{
|
||||||
|
_gfx.Modulate = new Color(1, 1, 1, 0.4f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
_gfx.Play();
|
_gfx.Play();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnAreaEntered(Area2D area)
|
private void OnAreaEntered(Area2D area)
|
||||||
{
|
{
|
||||||
@@ -57,4 +67,25 @@ public partial class Nodule : Node2D
|
|||||||
{
|
{
|
||||||
_gfx.SelfModulate = Colors.DarkOliveGreen;
|
_gfx.SelfModulate = Colors.DarkOliveGreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ReceiveConstructionEnergy(float amount)
|
||||||
|
{
|
||||||
|
if (!IsConstructing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ConstructionProgress = Mathf.Min(ConstructionProgress + amount, EnergyBuildCost);
|
||||||
|
|
||||||
|
if (ConstructionProgress >= EnergyBuildCost)
|
||||||
|
{
|
||||||
|
OnConstructionComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnConstructionComplete()
|
||||||
|
{
|
||||||
|
_gfx.Modulate = Colors.White;
|
||||||
|
_gfx.Play();
|
||||||
|
|
||||||
|
NoduleManager.Instance.OnNoduleConstructed(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,17 @@ public partial class Tube : Node2D
|
|||||||
GenerateTube();
|
GenerateTube();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _isGhost;
|
||||||
|
public bool IsGhost => _isGhost;
|
||||||
|
|
||||||
|
public void SetGhost(bool ghost)
|
||||||
|
{
|
||||||
|
_isGhost = ghost;
|
||||||
|
Line2D.DefaultColor = ghost
|
||||||
|
? new Color(0.3f, 0.49f, 0.24f, 0.3f)
|
||||||
|
: new Color(0.30588236f, 0.4862745f, 0.23921569f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
Line2D.Points = [];
|
Line2D.Points = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user