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>();
|
||||
nodule.Position = Position;
|
||||
nodule.EnergyBuildCost = 5f; // TODO: make configurable per nodule type
|
||||
nodule.ConstructionProgress = 0;
|
||||
GetParent()?.AddChild(nodule);
|
||||
NoduleManager.Instance.RequestConstruction(nodule);
|
||||
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;
|
||||
set
|
||||
{
|
||||
if (field + value > MaxEnergy)
|
||||
{
|
||||
field = MaxEnergy;
|
||||
}
|
||||
else
|
||||
{
|
||||
field += value;
|
||||
}
|
||||
|
||||
field = value;
|
||||
field = Mathf.Clamp(field + value, 0, MaxEnergy);
|
||||
UpdateLabel();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryConsume(float amount)
|
||||
{
|
||||
if (Energy >= amount)
|
||||
{
|
||||
Energy = -amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
UpdateLabel();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Godot;
|
||||
using Slimepire.Gameplay.Components;
|
||||
using Slimepire.Gameplay.Nodules;
|
||||
|
||||
namespace Slimepire.Gameplay;
|
||||
@@ -6,11 +7,16 @@ namespace Slimepire.Gameplay;
|
||||
[Singleton]
|
||||
public partial class NoduleManager
|
||||
{
|
||||
private const float PackageSendInterval = 0.3f;
|
||||
private const float ConstructionRetryInterval = 2f;
|
||||
|
||||
public bool Building { get; set; }
|
||||
|
||||
private AStar2D _aStar = new();
|
||||
private readonly Dictionary<long, Nodule> _nodules = [];
|
||||
private readonly HashSet<NoduleConnection> _connections = [];
|
||||
private readonly List<RootNodule> _rootNodules = [];
|
||||
private readonly Dictionary<long, ConstructionRequest> _activeConstructions = [];
|
||||
|
||||
// GFX
|
||||
private readonly Dictionary<NoduleConnection, Tube> _tubeConnections = new();
|
||||
@@ -20,6 +26,11 @@ public partial class NoduleManager
|
||||
_nodules.Add(nodule.NoduleId, nodule);
|
||||
_aStar.AddPoint(nodule.NoduleId, nodule.Position);
|
||||
|
||||
if (nodule is RootNodule root && !_rootNodules.Contains(root))
|
||||
{
|
||||
_rootNodules.Add(root);
|
||||
}
|
||||
|
||||
UpdateNoduleConnections();
|
||||
UpdateTubesAndAstar();
|
||||
}
|
||||
@@ -30,6 +41,176 @@ public partial class NoduleManager
|
||||
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()
|
||||
{
|
||||
foreach (var nodule in _nodules.Values)
|
||||
@@ -53,7 +234,16 @@ public partial class NoduleManager
|
||||
conn.A.AddChild(tube);
|
||||
tube.SetTarget(conn.B.GlobalPosition);
|
||||
|
||||
_aStar.ConnectPoints(conn.A.NoduleId, conn.B.NoduleId);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,4 +279,11 @@ public partial class NoduleManager
|
||||
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]
|
||||
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; } = [];
|
||||
|
||||
protected AnimatedSprite2D Gfx => _gfx;
|
||||
|
||||
public long NoduleId => (long)GetInstanceId();
|
||||
|
||||
public void AttachTube(Tube tube)
|
||||
{
|
||||
AddChild(tube);
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_tubeConnectionArea.AreaEntered += OnAreaEntered;
|
||||
@@ -32,7 +34,15 @@ public partial class Nodule : Node2D
|
||||
|
||||
// render above tubes
|
||||
ZIndex = 1;
|
||||
_gfx.Play();
|
||||
|
||||
if (IsConstructing)
|
||||
{
|
||||
_gfx.Modulate = new Color(1, 1, 1, 0.4f);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gfx.Play();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAreaEntered(Area2D area)
|
||||
@@ -57,4 +67,25 @@ public partial class Nodule : Node2D
|
||||
{
|
||||
_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();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
Line2D.Points = [];
|
||||
|
||||
Reference in New Issue
Block a user