Compare commits

...

4 Commits

Author SHA1 Message Date
5714d027b7 GOO SLOP 2026-06-25 23:52:25 +02:00
b593bd64bd add Nodule Building 2026-06-24 15:49:08 +02:00
390ac95638 Simplify Energy Stuff 2026-06-24 14:39:02 +02:00
bfee5f4025 remove data for now 2026-06-24 13:22:44 +02:00
47 changed files with 680 additions and 250 deletions

17
AGENTS.md Normal file
View 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

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cgameplay_005Ccomponents_005Cenergy/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANode_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7eeb6a6bf50d444f81dce3b50d9c468b580800_003F55_003Faaefec1f_003FNode_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARect2_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F20fdbba659cf4839ac49151c015340545a7800_003F6a_003F71fa77df_003FRect2_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASceneTreeAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe3fd2c31500d4d359fd6e9d5ca458884e0400_003F2f_003Faabb8bdb_003FSceneTreeAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASingletonAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe3fd2c31500d4d359fd6e9d5ca458884e0400_003F77_003F87840b05_003FSingletonAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASlimepire_002Ecore_002Emain_005Fgame_002EMainGame_002Eg_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FLocal_003FTemp_003FSourceGeneratedDocuments_003FC94839DE7EFB2DDB55DF1241_003FGodotSharp_002ESourceGenerators_003FGodotSharp_002ESourceGenerators_002ESceneTreeExtensions_002ESceneTreeSourceGenerator_003FSlimepire_002Ecore_002Emain_005Fgame_002EMainGame_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

BIN
assets/gradient_linear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b2y7oc421yoyd"
path="res://.godot/imported/gradient_linear.png-f1664a719d29c2fd7ef75dfe3ee8f4a4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/gradient_linear.png"
dest_files=["res://.godot/imported/gradient_linear.png-f1664a719d29c2fd7ef75dfe3ee8f4a4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/metaball.kra Normal file

Binary file not shown.

BIN
assets/metaball.kra~ Normal file

Binary file not shown.

BIN
assets/metaball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,41 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://byk86jmfv7p64"
path.s3tc="res://.godot/imported/metaball.png-a6b8702ab5ec503295d5f109cf071bd1.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://assets/metaball.png"
dest_files=["res://.godot/imported/metaball.png-a6b8702ab5ec503295d5f109cf071bd1.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

BIN
assets/metaball.png~ Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,37 @@
shader_type canvas_item;
varying vec2 quad_uv;
uniform vec2 ball_0;
uniform vec2 ball_1;
uniform vec2 ball_2;
uniform vec2 rect_size = vec2(1920, 1080);
uniform float ball_radius = 12.0;
uniform float cam_zoom = 1.0;
uniform vec2 cam_pos = vec2(0.0);
uniform float threshold : hint_range(0.0, 1.0) = 0.5;
uniform vec4 color : source_color = vec4(0.3, 0.5, 0.2, 1.0);
uniform sampler2D noise_tex : repeat_enable;
void vertex() {
quad_uv = UV;
}
float field(vec2 pos) {
return (ball_radius * cam_zoom) / max(length(quad_uv * rect_size - pos), 1.0);
}
void fragment() {
float sum = field(ball_0) + field(ball_1) + field(ball_2);
float a = smoothstep(threshold - 0.05, threshold, sum);
vec2 screen_px = quad_uv * rect_size;
vec2 world_px = (screen_px - rect_size * 0.5) / cam_zoom + cam_pos;
float n1 = texture(noise_tex, world_px * 0.0005).r;
float n2 = texture(noise_tex, world_px * 0.0008 + 0.33).r;
float hue_shift = (n1 - 0.5) * 0.12;
float value_shift = (n2 - 0.5) * 0.15;
vec3 shifted = color.rgb + vec3(hue_shift * 0.5, hue_shift, -hue_shift * 0.5) + value_shift;
COLOR = vec4(shifted, a * color.a);
}

View File

@@ -0,0 +1 @@
uid://8xu2s3gyxp0v

View File

@@ -21,8 +21,8 @@ buses/default_bus_layout="uid://djkq0k3jsuxcg"
[display]
window/size/viewport_width=480
window/size/viewport_height=320
window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"

View File

@@ -1,44 +0,0 @@
using Godot;
using Slimepire.Gameplay.Components;
namespace Slimepire.Core;
[Singleton]
public partial class EnergyManager
{
private List<EnergyProducer> _energyProducers = [];
private List<EnergyStorage> _energyStorages = [];
public void RegisterEnergyProducer(EnergyProducer energyProducer)
{
_energyProducers.Add(energyProducer);
}
public void RemoveEnergyProducer(EnergyProducer energyProducer)
{
_energyProducers.Remove(energyProducer);
}
public void RegisterEnergyStorage(EnergyStorage energyStorage)
{
_energyStorages.Add(energyStorage);
}
public void RemoveEnergyStorage(EnergyStorage energyStorage)
{
_energyStorages.Remove(energyStorage);
}
public void TransferEnergy(float energy)
{
foreach (var energyStorage in _energyStorages)
{
if (energyStorage.Energy + energy <= energyStorage.MaxEnergy)
{
energyStorage.AcceptEnergy(energy);
}
}
}
public void RequestEnergy(float energy) { }
}

View File

@@ -1 +0,0 @@
uid://boaiqjlh7qq3o

View File

@@ -2,5 +2,5 @@ using Godot;
namespace Slimepire.Core;
[ResourceTree("/src/Data")]
[ResourceTree("/src")]
public static partial class GameResources;

View File

@@ -1,43 +0,0 @@
[gd_resource type="Resource" script_class="NoduleResource" format=3 uid="uid://0d8vif4ejtp1"]
[ext_resource type="Texture2D" uid="uid://ds1idectpqbnh" path="res://assets/energy_storage_nodule.png" id="1_237jx"]
[ext_resource type="PackedScene" uid="uid://by6iw05ovnhq8" path="res://src/Gameplay/Components/EnergyStorage.tscn" id="1_dusyu"]
[ext_resource type="Script" uid="uid://bnf6qmk64i2wj" path="res://src/Data/NoduleStats.cs" id="2_dusyu"]
[sub_resource type="AtlasTexture" id="AtlasTexture_dusyu"]
atlas = ExtResource("1_237jx")
region = Rect2(0, 0, 32, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_rmsh8"]
atlas = ExtResource("1_237jx")
region = Rect2(32, 0, 32, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_lmxtc"]
atlas = ExtResource("1_237jx")
region = Rect2(64, 0, 32, 32)
[sub_resource type="SpriteFrames" id="SpriteFrames_j6wbi"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_dusyu")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_rmsh8")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_lmxtc")
}],
"loop": 1,
"name": &"default",
"speed": 5.0
}]
[resource]
script = ExtResource("2_dusyu")
SpriteFrames = SubResource("SpriteFrames_j6wbi")
Color = Color(0.60033447, 0.5019659, 0.0981792, 1)
NoduleName = "Energy Nodule"
MaxEnergy = 100.0
Components = Array[PackedScene]([ExtResource("1_dusyu")])
metadata/_custom_type_script = "uid://bnf6qmk64i2wj"

View File

@@ -1,16 +0,0 @@
using Godot;
namespace Slimepire.Data;
[GlobalClass]
public partial class NoduleStats : Resource
{
[Export]
public SpriteFrames SpriteFrames = null!;
[Export]
public Color Color;
[Export]
public string NoduleName = "Nodule";
}

View File

@@ -1 +0,0 @@
uid://bnf6qmk64i2wj

View File

@@ -1,40 +0,0 @@
[gd_resource type="Resource" script_class="NoduleStats" format=3 uid="uid://5gk0giodp6g"]
[ext_resource type="Texture2D" uid="uid://cecg47efx7gw8" path="res://assets/simple_nodule.png" id="1_j6wbi"]
[ext_resource type="Script" uid="uid://bnf6qmk64i2wj" path="res://src/Data/NoduleStats.cs" id="1_otepm"]
[sub_resource type="AtlasTexture" id="AtlasTexture_j6wbi"]
atlas = ExtResource("1_j6wbi")
region = Rect2(0, 0, 32, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_q8pab"]
atlas = ExtResource("1_j6wbi")
region = Rect2(64, 0, 32, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_dh8qd"]
atlas = ExtResource("1_j6wbi")
region = Rect2(32, 0, 32, 32)
[sub_resource type="SpriteFrames" id="SpriteFrames_x44k2"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_j6wbi")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_q8pab")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_dh8qd")
}],
"loop": 1,
"name": &"default",
"speed": 3.0
}]
[resource]
script = ExtResource("1_otepm")
SpriteFrames = SubResource("SpriteFrames_x44k2")
Color = Color(0.2642648, 0.46582836, 0.36528692, 1)
NoduleName = "Simple Nodule"
metadata/_custom_type_script = "uid://bnf6qmk64i2wj"

View File

@@ -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();
}
}

View 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;
}
}
}

View File

@@ -0,0 +1 @@
uid://nk5tjlmcrpn2

View 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")

View File

@@ -0,0 +1,17 @@
using Godot;
namespace Slimepire.Gameplay.Components;
public partial class EnergyProducer : Node
{
[Export]
public EnergyStorage EnergyStorage = null!;
[Export]
public float ProductionRate = 1;
public override void _Process(double delta)
{
EnergyStorage.Energy += ProductionRate * (float)delta;
}
}

View File

@@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://t0gdf172f65v"]
[ext_resource type="Script" uid="uid://bku1vey63ew65" path="res://src/Gameplay/Components/EnergyProducer.cs" id="1_4q7h4"]
[ext_resource type="Script" uid="uid://bku1vey63ew65" path="res://src/Gameplay/Components/Energy/EnergyProducer.cs" id="1_4q7h4"]
[node name="EnergyProducer" type="Node" unique_id=1184182162]
script = ExtResource("1_4q7h4")

View File

@@ -0,0 +1,49 @@
using Godot;
namespace Slimepire.Gameplay.Components;
[GlobalClass]
public partial class EnergyStorage : Node
{
[Export]
public Label EnergyLabel = null!;
[Export]
public float MaxEnergy = 100;
public float Energy
{
get;
set
{
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();
}
public override void _ExitTree()
{
// EnergyManager.Instance.RemoveEnergyStorage(this);
}
private void UpdateLabel()
{
EnergyLabel.Text = $"{Energy:0}/{MaxEnergy}";
}
}

View File

@@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://by6iw05ovnhq8"]
[ext_resource type="Script" uid="uid://dgkeo1wsicv13" path="res://src/Gameplay/Components/EnergyStorage.cs" id="1_vetx7"]
[ext_resource type="Script" uid="uid://dgkeo1wsicv13" path="res://src/Gameplay/Components/Energy/EnergyStorage.cs" id="1_vetx7"]
[node name="EnergyStorage" type="Node" unique_id=1765924301]
script = ExtResource("1_vetx7")

View File

@@ -1,32 +0,0 @@
using Godot;
using Slimepire.Core;
namespace Slimepire.Gameplay.Components;
public partial class EnergyProducer : Node
{
[Export]
public float ProductionRate = 1;
private float _energy;
public override void _Ready()
{
EnergyManager.Instance.RegisterEnergyProducer(this);
}
public override void _Process(double delta)
{
_energy += ProductionRate * (float)delta;
if (_energy > 1)
{
EnergyManager.Instance.TransferEnergy(_energy);
_energy = 0;
}
}
public override void _ExitTree()
{
EnergyManager.Instance.RemoveEnergyProducer(this);
}
}

View File

@@ -1,28 +0,0 @@
using Godot;
using Slimepire.Core;
namespace Slimepire.Gameplay.Components;
public partial class EnergyStorage : Node
{
[Export]
public float MaxEnergy = 100;
public float Energy { get; private set; }
public override void _Ready()
{
EnergyManager.Instance.RegisterEnergyStorage(this);
}
public override void _ExitTree()
{
EnergyManager.Instance.RemoveEnergyStorage(this);
}
public float AcceptEnergy(float energy)
{
Energy += energy;
return Energy;
}
}

View File

@@ -0,0 +1,65 @@
using Godot;
using Slimepire.Levels;
public partial class GooSubViewPort : Node
{
private readonly Vector2[] _worldPositions =
[
new(400, 300),
new(430, 310),
new(380, 330),
];
private ShaderMaterial _material = null!;
private ColorRect _colorRect = null!;
private DemoLevel _level = null!;
public override void _Ready()
{
_level = GetParent<DemoLevel>();
var canvasLayer = _level.GetNode<CanvasLayer>("CanvasLayer");
_colorRect = canvasLayer.GetNode<ColorRect>("ColorRect");
_material = new ShaderMaterial
{
Shader = GD.Load<Shader>("res://assets/shaders/metaball.gdshader"),
};
_material.SetShaderParameter("threshold", 0.5f);
_material.SetShaderParameter("color", new Color(0.3f, 0.5f, 0.2f, 1f));
_material.SetShaderParameter("ball_radius", 12f);
_material.SetShaderParameter("rect_size", GetViewport().GetVisibleRect().Size);
_material.SetShaderParameter("cam_zoom", 1f);
_material.SetShaderParameter("noise_tex", GenerateNoiseTexture(32));
_colorRect.Material = _material;
}
public override void _Process(double delta)
{
var cam = _level.Camera;
var viewSize = GetViewport().GetVisibleRect().Size;
_material.SetShaderParameter("cam_zoom", cam.Zoom.X);
_material.SetShaderParameter("cam_pos", cam.GlobalPosition);
_material.SetShaderParameter("rect_size", GetViewport().GetVisibleRect().Size);
for (var i = 0; i < _worldPositions.Length && i < 3; i++)
{
var screenPos = (_worldPositions[i] - cam.GlobalPosition) * cam.Zoom.X + viewSize / 2;
_material.SetShaderParameter($"ball_{i}", screenPos);
}
}
private static ImageTexture GenerateNoiseTexture(int size)
{
var img = Image.CreateEmpty(size, size, false, Image.Format.Rgba8);
var rng = new Random(42);
for (var y = 0; y < size; y++)
for (var x = 0; x < size; x++)
{
var v = (float)rng.NextDouble();
img.SetPixel(x, y, new Color(v, v, v, 1));
}
return ImageTexture.CreateFromImage(img);
}
}

View File

@@ -0,0 +1 @@
uid://bft481hoodv5d

View File

@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://ce2frpb21ct5l"]
[ext_resource type="Script" uid="uid://bft481hoodv5d" path="res://src/Gameplay/GooSubViewPort.cs" id="1_xo53p"]
[node name="GooSubViewPort" type="Node" unique_id=348721885]
script = ExtResource("1_xo53p")

11
src/Gameplay/Metaball.cs Normal file
View File

@@ -0,0 +1,11 @@
using System;
using Godot;
public partial class Metaball : Control
{
// Called when the node enters the scene tree for the first time.
public override void _Ready() { }
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta) { }
}

View File

@@ -0,0 +1 @@
uid://cpj1q44qfwsps

View File

@@ -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,175 @@ 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 ghost tubes where BOTH sides are now fully constructed
foreach (var (conn, tube) in _tubeConnections)
{
if (tube.IsGhost && (ReferenceEquals(conn.A, nodule) || ReferenceEquals(conn.B, nodule)))
{
if (!conn.A.IsConstructing && !conn.B.IsConstructing)
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,10 +233,19 @@ public partial class NoduleManager
conn.A.AddChild(tube);
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);
}
}
}
}
private record NoduleConnection
{
@@ -89,4 +278,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; }
}
}

View File

@@ -37,19 +37,11 @@ animations = [{
"speed": 2.0
}]
[sub_resource type="LabelSettings" id="LabelSettings_x44k2"]
font_size = 12
outline_size = 3
outline_color = Color(0, 0, 0, 1)
shadow_size = 3
shadow_color = Color(0, 0, 0, 1)
[node name="BasicNodule" type="Node2D" unique_id=1637542829 node_paths=PackedStringArray("_tubeConnectionArea", "_gfx", "_energyLabel")]
[node name="BasicNodule" type="Node2D" unique_id=1637542829 node_paths=PackedStringArray("_tubeConnectionArea", "_gfx")]
z_index = 1
script = ExtResource("1_wbqr8")
_tubeConnectionArea = NodePath("TubeConnectionArea")
_gfx = NodePath("Gfx")
_energyLabel = NodePath("EnergyLabel")
[node name="TubeConnectionArea" parent="." unique_id=1821668398 instance=ExtResource("2_dehe6")]
@@ -64,18 +56,3 @@ texture_filter = 1
sprite_frames = SubResource("SpriteFrames_xyjsl")
autoplay = "default"
frame_progress = 0.120879106
[node name="EnergyLabel" type="Label" parent="." unique_id=466024399]
unique_name_in_owner = true
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -20.0
offset_top = 14.0
offset_right = 20.0
offset_bottom = 31.0
grow_horizontal = 2
text = "5"
label_settings = SubResource("LabelSettings_x44k2")
horizontal_alignment = 1
vertical_alignment = 1

View File

@@ -12,7 +12,11 @@ public partial class Nodule : Node2D
private AnimatedSprite2D _gfx = null!;
[Export]
private Label _energyLabel = null!;
public float EnergyBuildCost { get; set; } = 0f;
public float ConstructionProgress { get; set; }
public bool IsConstructing => EnergyBuildCost > 0 && ConstructionProgress < EnergyBuildCost;
public List<Nodule> Connections { get; } = [];
@@ -20,11 +24,6 @@ public partial class Nodule : Node2D
public long NoduleId => (long)GetInstanceId();
public void AttachTube(Tube tube)
{
AddChild(tube);
}
public override void _Ready()
{
_tubeConnectionArea.AreaEntered += OnAreaEntered;
@@ -35,8 +34,16 @@ public partial class Nodule : Node2D
// render above tubes
ZIndex = 1;
if (IsConstructing)
{
_gfx.Modulate = new Color(1, 1, 1, 0.4f);
}
else
{
_gfx.Play();
}
}
private void OnAreaEntered(Area2D area)
{
@@ -60,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);
}
}

View File

@@ -2,6 +2,8 @@
[ext_resource type="PackedScene" uid="uid://bjesfu1mfx8l5" path="res://src/Gameplay/Components/TubeConnectionArea.tscn" id="1_b03xe"]
[ext_resource type="Script" uid="uid://y4cpi50hsaj4" path="res://src/Gameplay/Nodules/RootNodule/RootNodule.cs" id="1_root"]
[ext_resource type="PackedScene" uid="uid://t0gdf172f65v" path="res://src/Gameplay/Components/Energy/EnergyProducer.tscn" id="2_cy5pc"]
[ext_resource type="PackedScene" uid="uid://by6iw05ovnhq8" path="res://src/Gameplay/Components/Energy/EnergyStorage.tscn" id="2_gke5o"]
[ext_resource type="PackedScene" uid="uid://3x5pqm4t5cbl" path="res://src/Gameplay/Components/GrowArea.tscn" id="2_tg6hi"]
[ext_resource type="Texture2D" uid="uid://tomd3kojw0qy" path="res://assets/root_nodule.png" id="3_b03xe"]
@@ -26,12 +28,17 @@ outline_color = Color(0, 0, 0, 1)
shadow_size = 3
shadow_color = Color(0, 0, 0, 1)
[node name="RootNodule" type="Node2D" unique_id=1797926942 node_paths=PackedStringArray("_tubeConnectionArea", "_gfx", "_energyLabel")]
[node name="RootNodule" type="Node2D" unique_id=1797926942 node_paths=PackedStringArray("_tubeConnectionArea", "_gfx")]
z_index = 1
script = ExtResource("1_root")
_tubeConnectionArea = NodePath("TubeConnectionArea")
_gfx = NodePath("Gfx")
_energyLabel = NodePath("EnergyLabel")
[node name="EnergyProducer" parent="." unique_id=1184182162 node_paths=PackedStringArray("EnergyStorage") instance=ExtResource("2_cy5pc")]
EnergyStorage = NodePath("../EnergyStorage")
[node name="EnergyStorage" parent="." unique_id=1765924301 node_paths=PackedStringArray("EnergyLabel") instance=ExtResource("2_gke5o")]
EnergyLabel = NodePath("../EnergyLabel")
[node name="TubeConnectionArea" parent="." unique_id=1821668398 instance=ExtResource("1_b03xe")]

View File

@@ -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 = [];

View File

@@ -0,0 +1,32 @@
[gd_scene format=3 uid="uid://ghg1o288grwq"]
[ext_resource type="Script" uid="uid://cpj1q44qfwsps" path="res://src/Gameplay/Metaball.cs" id="1_jfklr"]
[ext_resource type="Shader" uid="uid://8xu2s3gyxp0v" path="res://assets/shaders/metaball.gdshader" id="2_vvscw"]
[sub_resource type="ViewportTexture" id="ViewportTexture_vvscw"]
viewport_path = NodePath("SubViewport")
[sub_resource type="ShaderMaterial" id="ShaderMaterial_vvscw"]
resource_local_to_scene = true
shader = ExtResource("2_vvscw")
shader_parameter/metaball_tex = SubResource("ViewportTexture_vvscw")
shader_parameter/threshold = 0.9250000439375
shader_parameter/color = Color(0.3, 0.5, 0.2, 1)
[node name="Metaball" type="Control" unique_id=1102466349]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_jfklr")
[node name="ColorRect" type="ColorRect" parent="." unique_id=1080383949]
material = SubResource("ShaderMaterial_vvscw")
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2

View File

@@ -5,5 +5,5 @@ namespace Slimepire.Levels;
[GlobalClass]
public abstract partial class BaseLevel : Node
{
protected abstract Camera2D Camera { get; }
public abstract Camera2D Camera { get; }
}

View File

@@ -5,5 +5,5 @@ namespace Slimepire.Levels;
[SceneTree]
public partial class DemoLevel : BaseLevel
{
protected override Camera2D Camera => _.Camera;
public override Camera2D Camera => _.Camera;
}

View File

@@ -3,6 +3,7 @@
[ext_resource type="Script" uid="uid://c8l12dlpexosv" path="res://src/Levels/DemoLevel.cs" id="1_l2cn0"]
[ext_resource type="Script" uid="uid://cdgi3y0sm3rt1" path="res://src/Gameplay/Camera.cs" id="2_mxa1b"]
[ext_resource type="PackedScene" uid="uid://j15868gtem0y" path="res://src/Gameplay/Nodules/RootNodule/RootNodule.tscn" id="3_knlma"]
[ext_resource type="PackedScene" uid="uid://ce2frpb21ct5l" path="res://src/Gameplay/GooSubViewPort.tscn" id="4_goo"]
[sub_resource type="LabelSettings" id="LabelSettings_l2cn0"]
font_size = 107
@@ -29,3 +30,15 @@ text = "DEMO LEVEL"
label_settings = SubResource("LabelSettings_l2cn0")
[node name="RootNodule" parent="." unique_id=1797926942 instance=ExtResource("3_knlma")]
[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=648836414]
[node name="ColorRect" type="ColorRect" parent="CanvasLayer" unique_id=1287004749]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="GooSubViewPort" parent="." unique_id=348721885 instance=ExtResource("4_goo")]