feat: initial import

This commit is contained in:
redglow
2026-07-11 10:54:02 +02:00
commit e560a743fb
55 changed files with 993 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
using Godot;
using GodotHostTest.GodotDI;
using GodotHostTest.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace GodotHostTest.Game;
public partial class GameRootScope : RootScope
{
[Export] private TargetSpawner _targetSpawner = null!;
protected override void Configure(IServiceCollection serviceCollection)
{
serviceCollection
.AddSingleton<ITargetCollector>(_targetSpawner)
.AddSingleton<ITargetSpawner>(_targetSpawner);
}
}
+1
View File
@@ -0,0 +1 @@
uid://dk578xnlxjwrr
+21
View File
@@ -0,0 +1,21 @@
using Godot;
using GodotHostTest.GodotDI;
using GodotHostTest.Helpers;
using GodotHostTest.Interfaces;
using Microsoft.Extensions.Logging;
using R3;
namespace GodotHostTest.Game;
public partial class Projectile : Node2D
{
[Inject] private readonly ILogger<Projectile> _logger = null!;
[Inject] private readonly ITargetSpawner _targetSpawner = null!;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
_targetSpawner.TargetSpawned
.Subscribe(_ => _logger.LogInformation("Projectile got informed of target spawning")).AddTo(this);
}
}
+1
View File
@@ -0,0 +1 @@
uid://dc5mvle8lc40j
+37
View File
@@ -0,0 +1,37 @@
using R3;
using Godot;
using GodotHostTest.GodotDI;
using GodotHostTest.Helpers;
using GodotHostTest.Interfaces;
namespace GodotHostTest.Game;
public partial class Score : Control
{
[Export] private AnimationPlayer _animationPlayer = null!;
[Export] private Label _label = null!;
[Inject] private readonly ITargetSpawner _targetSpawner = null!;
private int _score;
public override void _Ready()
{
_targetSpawner.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this);
base._Ready();
}
private void OnTargetSpawned(ITarget target)
{
_animationPlayer.Play("wiggle");
target.Hit.Select(_ => 20).Merge(target.TimedOut.Select(_ => -5)).SubscribeOnce(delta =>
{
_score += delta;
UpdateScore();
}).AddTo(this);
}
private void UpdateScore()
{
_label.Text = $"Score: {_score}";
}
}
+1
View File
@@ -0,0 +1 @@
uid://bgrjeadxhgmvi
+61
View File
@@ -0,0 +1,61 @@
using Godot;
using GodotHostTest.GodotDI;
using GodotHostTest.Interfaces;
using R3;
namespace GodotHostTest.Game;
public partial class Target : Node2D, ITarget
{
[Export] public double TimeOutDurationInSeconds;
[Export] public required Timer TimeOutTimer;
[Export] public required Sprite2D Image;
[Export] public required Area2D CollisionArea;
private readonly Subject<Unit> _timedOut = new();
private readonly Subject<Unit> _hit = new();
[Inject] private readonly ITargetCollector _targetCollector = null!;
public override void _Ready()
{
_targetCollector.NewTarget(this);
}
public override void _ExitTree()
{
_timedOut.Dispose();
_hit.Dispose();
}
public Observable<Unit> TimedOut => _timedOut.AsObservable();
public Observable<Unit> Hit => _hit.AsObservable();
// todo: hook to collision
private void OnHit()
{
Disable();
TimeOutTimer.Stop();
_hit.OnNext(Unit.Default);
}
public void Enable()
{
Image.Visible = true;
CollisionArea.Monitoring = true;
TimeOutTimer.Start(TimeOutDurationInSeconds);
}
private void Disable()
{
Image.Visible = false;
CollisionArea.Monitoring = false;
}
private void OnTimeout()
{
Disable();
_timedOut.OnNext(Unit.Default);
}
}
+1
View File
@@ -0,0 +1 @@
uid://bv2hat41dyxiv
+61
View File
@@ -0,0 +1,61 @@
using System.Collections.Generic;
using Godot;
using GodotHostTest.Helpers;
using GodotHostTest.Interfaces;
using R3;
namespace GodotHostTest.Game;
public partial class TargetSpawner : Node2D, ITargetCollector, ITargetSpawner
{
[Export] public double TimeBetweenSpawns;
[Export] public required Timer Timer;
private readonly List<ITarget> _availableTargets = [];
public override void _Ready()
{
Spawn();
Timer.Start(TimeBetweenSpawns);
}
private void MakeTargetAvailable(ITarget target)
{
_availableTargets.Add(target);
}
private void OnTimer()
{
Spawn();
}
private void Spawn()
{
// pick a random target
var randomIndex = GD.RandRange(0, _availableTargets.Count - 1);
var target = _availableTargets[randomIndex];
_availableTargets.RemoveAt(randomIndex);
// enable it
target.Enable();
// notify that a target has been spawned
_targetSpawned.OnNext(target);
}
public void NewTarget(ITarget target)
{
_availableTargets.Add(target);
target.Hit.Merge(target.TimedOut).Select(_ => target).Subscribe(MakeTargetAvailable).AddTo(this);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_targetSpawned.Dispose();
}
private readonly Subject<ITarget> _targetSpawned = new();
public Observable<ITarget> TargetSpawned => _targetSpawned.AsObservable();
}
+1
View File
@@ -0,0 +1 @@
uid://cxfrtglku2gfa
+15
View File
@@ -0,0 +1,15 @@
using Godot;
using GodotHostTest.GodotDI;
namespace GodotHostTest.Game;
public partial class Turret : Sprite2D
{
[Inject] private readonly ISceneInstantiator _sceneInstantiator = null!;
[Export] private PackedScene _projectilePackedScene = null!;
public override void _Ready()
{
GetTree().CreateTimer(1).Timeout += () => AddChild(_sceneInstantiator.Instantiate(_projectilePackedScene));
}
}
+1
View File
@@ -0,0 +1 @@
uid://ce7gnp1lcrrdv
+18
View File
@@ -0,0 +1,18 @@
[gd_scene format=3 uid="uid://brbufibn7hi3o"]
[ext_resource type="Script" uid="uid://dc5mvle8lc40j" path="res://Game/Projectile.cs" id="1_80yud"]
[ext_resource type="Script" uid="uid://qjonl8stia46" path="res://GodotDI/Scope.cs" id="2_3kmdq"]
[ext_resource type="Texture2D" uid="uid://y1ros3y7hag2" path="res://icon.svg" id="3_1i54g"]
[node name="Projectile" type="Node2D" unique_id=1686616504]
script = ExtResource("1_80yud")
[node name="Scope" type="Node" parent="." unique_id=513224818 node_paths=PackedStringArray("InjectedNodes")]
script = ExtResource("2_3kmdq")
InjectedNodes = [NodePath("..")]
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1021740171]
modulate = Color(1, 1, 0, 1)
rotation = 0.7853982
scale = Vector2(0.3, 0.3)
texture = ExtResource("3_1i54g")
+31
View File
@@ -0,0 +1,31 @@
[gd_scene format=3 uid="uid://3v8kq1rfwhmr"]
[ext_resource type="Texture2D" uid="uid://y1ros3y7hag2" path="res://icon.svg" id="1_032qf"]
[ext_resource type="Script" uid="uid://bv2hat41dyxiv" path="res://Game/Target.cs" id="1_c0a8e"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_u8d0v"]
size = Vector2(128, 128)
[node name="Target" type="Node2D" unique_id=2101558052 node_paths=PackedStringArray("TimeOutTimer", "Image", "CollisionArea")]
script = ExtResource("1_c0a8e")
TimeOutDurationInSeconds = 7.0
TimeOutTimer = NodePath("TimeOutTimer")
Image = NodePath("Sprite2D")
CollisionArea = NodePath("CollisionArea")
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=31059524]
visible = false
modulate = Color(1, 0.20392157, 0.015686275, 1)
texture = ExtResource("1_032qf")
[node name="CollisionArea" type="Area2D" parent="." unique_id=116681968]
monitoring = false
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="CollisionArea" unique_id=1558252366]
shape = SubResource("RectangleShape2D_u8d0v")
[node name="TimeOutTimer" type="Timer" parent="." unique_id=1079777109]
one_shot = true
[connection signal="timeout" from="TimeOutTimer" to="." method="OnTimeout"]