Basic fighting system

This commit is contained in:
cblech
2025-07-10 03:38:48 +02:00
parent 7e6163ed68
commit b6fd6292e3
26 changed files with 3065 additions and 8 deletions
@@ -1,13 +1,27 @@
using Babushka.scripts.CSharp.Common.Fight;
using Godot;
namespace Babushka.scripts.CSharp.Common.Camera;
public partial class CameraController : Camera2D
{
[Export] private Node2D _followNode;
public override void _Process(double delta)
{
this.GlobalPosition = _followNode.GlobalPosition;
}
}
#region Singleton ( Contains _EnterTree() ) // TODO: use autoload or other solution
public static CameraController Instance { get; private set; } = null!;
public override void _EnterTree()
{
Instance = this;
}
#endregion
[Export] private Node2D _followNode;
public FightInstance? fightToShow;
public override void _Process(double delta)
{
this.GlobalPosition = fightToShow?.camPositionNode.GlobalPosition ?? _followNode.GlobalPosition;
}
}
@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Babushka.scripts.CSharp.Common.Camera;
using Babushka.scripts.CSharp.Common.Util;
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightInstance : Node2D //TODO: remake
{
[Export(PropertyHint.ArrayType)] private Node2D[] _friendlyFightSpots;
[Export(PropertyHint.ArrayType)] private Node2D[] _enemyFightSpots;
[Export] public Node2D camPositionNode;
[Export] private FightStateManager _fightStateManager;
[Export] private Label _fightEndText;
[Signal]
public delegate void FightStartedEventHandler();
[Signal]
public delegate void FightEndedEventHandler();
private List<Fighter> _friendlyFighters = new();
private List<Fighter> _enemyFighters = new();
private FightAttack? _stagedAttack = null;
public override void _Ready()
{
//_fightStateManager.CurrentFightState = FightStateManager.FightState.FightStartAnim;
_fightStateManager.ExitingTransition += from =>
{
switch (from)
{
case FightStateManager.FightState.None:
CaptureCamera();
Show();
EmitSignalFightStarted();
break;
case FightStateManager.FightState.Input:
HideAttackButtons();
break;
case FightStateManager.FightState.InputTargetSelect:
HideTargetButtons();
break;
case FightStateManager.FightState.FriendAttackAnim:
_stagedAttack = null;
break;
case FightStateManager.FightState.PlayerWinAnim:
case FightStateManager.FightState.EnemyWinAnim:
_fightEndText.Text = "";
break;
}
};
_fightStateManager.EnteringTransition += to =>
{
switch (to)
{
case FightStateManager.FightState.None:
EmitSignalFightEnded();
CleanUp();
Hide();
ReleaseCamera();
break;
case FightStateManager.FightState.Input:
if (CheckWin())
{
break;
}
ShowAttackButtons();
break;
case FightStateManager.FightState.InputTargetSelect:
ShowTargetButtons();
break;
case FightStateManager.FightState.FriendAttackAnim:
ExecuteAttack();
GetTree().CreateTimer(1).Timeout += () => _fightStateManager.CurrentFightState = FightStateManager.FightState.Enemy;
break;
case FightStateManager.FightState.Enemy:
if (CheckWin())
{
break;
}
DecideEnemyAttack();
_fightStateManager.CurrentFightState = FightStateManager.FightState.EnemyAttackAnim;
break;
case FightStateManager.FightState.EnemyAttackAnim:
ExecuteAttack();
GetTree().CreateTimer(1).Timeout += () => _fightStateManager.CurrentFightState = FightStateManager.FightState.Input;
break;
case FightStateManager.FightState.PlayerWinAnim:
_fightEndText.Text = "You Win!";
GetTree().CreateTimer(3).Timeout += () => _fightStateManager.CurrentFightState = FightStateManager.FightState.None;
break;
case FightStateManager.FightState.EnemyWinAnim:
_fightEndText.Text = "You Died :(";
GetTree().CreateTimer(3).Timeout += () => _fightStateManager.CurrentFightState = FightStateManager.FightState.None;
break;
}
};
}
private void CleanUp()
{
_enemyFighters.ForEach(f => f.QueueFree());
_friendlyFighters.ForEach(f => f.QueueFree());
_enemyFighters = new();
_friendlyFighters = new();
}
private void DecideEnemyAttack()
{
var aliveEnemyFighters = _enemyFighters.Where(f => !f.IsDead()).ToList();
var aliveFriendlyFighters = _friendlyFighters.Where(f => !f.IsDead()).ToList();
if (aliveEnemyFighters.Count <= 0)
throw new InvalidOperationException("No enemy fighters available for attack.");
if (aliveFriendlyFighters.Count <= 0)
throw new InvalidOperationException("No friendly fighters available to target.");
var fighter = aliveEnemyFighters.Random();
var target = aliveFriendlyFighters.Random();
_stagedAttack = new FightAttack
{
attacker = fighter!,
needsSelectedTarget = true,
damage = fighter!.attackStrength,
target = target!
};
}
private void ExecuteAttack()
{
if (_stagedAttack == null)
throw new InvalidOperationException("No staged attack to execute.");
if (!_stagedAttack.needsSelectedTarget)
throw new NotImplementedException("Non-targeted attacks are not implemented yet.");
if (_stagedAttack.needsSelectedTarget && _stagedAttack.target == null)
throw new InvalidOperationException("No target selected for the staged attack.");
_stagedAttack.target!.Health -= _stagedAttack.damage;
_stagedAttack.attacker.AttackAnimation(_stagedAttack);
UpdateHealthVisual();
}
private void UpdateHealthVisual()
{
_friendlyFighters
.Concat(_enemyFighters)
.ForEach(f => f.UpdateHealthVisual());
}
private void ReleaseCamera()
{
CameraController.Instance.fightToShow = null;
}
private void CaptureCamera()
{
CameraController.Instance.fightToShow = this;
}
public void Start(FightParty fightParty, PackedScene?[] enemies)
{
if (_fightStateManager.IsRunning())
{
GD.PushWarning("Can not start a running fight");
return;
}
if (fightParty.vesna)
{
InstantiateFighter(_friendlyFightSpots[1], FightManager.Instance.fightingVesnaScene);
}
for (var i = 0; i < Math.Min(_enemyFightSpots.Length, enemies.Length); i++)
{
var enemy = enemies[i];
if (enemy == null)
continue;
InstantiateFighter(_enemyFightSpots[i], enemy, true);
}
_fightStateManager.ToStartAnim();
}
private void InstantiateFighter(Node2D parent, PackedScene fighterScene, bool isEnemy = false)
{
var fighter = fighterScene.Instantiate<Fighter>();
fighter.fightInstance = this;
parent.AddChild(fighter);
if (isEnemy)
{
_enemyFighters.Add(fighter);
}
else
{
_friendlyFighters.Add(fighter);
}
}
public void SelectAttack(Fighter fighter)
{
_stagedAttack = new FightAttack
{
attacker = fighter,
damage = fighter.attackStrength,
needsSelectedTarget = true
};
if (_stagedAttack.needsSelectedTarget)
{
_fightStateManager.CurrentFightState = FightStateManager.FightState.InputTargetSelect;
}
else
{
_fightStateManager.CurrentFightState = FightStateManager.FightState.FriendAttackAnim;
}
}
private void HideAttackButtons()
{
_friendlyFighters.ForEach(f => f.HideAttackButton());
}
private void ShowAttackButtons()
{
_friendlyFighters.ForEach(f => f.ShowAttackButton());
}
private void HideTargetButtons()
{
_enemyFighters.ForEach(f => f.HideTargetButtons());
}
private void ShowTargetButtons()
{
_enemyFighters.ForEach(f => f.ShowTargetButtons());
}
public void SelectTargetAndAttack(Fighter fighter)
{
if (_stagedAttack == null)
throw new InvalidOperationException("No staged attack to select target for.");
_stagedAttack.target = fighter;
_fightStateManager.CurrentFightState = FightStateManager.FightState.FriendAttackAnim;
}
public bool CheckWin()
{
if (_enemyFighters.All(f => f.IsDead()))
{
_fightStateManager.CurrentFightState = FightStateManager.FightState.PlayerWinAnim;
return true;
}
if (_friendlyFighters.All(f => f.IsDead()))
{
_fightStateManager.CurrentFightState = FightStateManager.FightState.EnemyWinAnim;
return true;
}
return false;
}
}
public class FightAttack
{
public int damage;
public bool needsSelectedTarget;
public Fighter? target;
public Fighter attacker;
}
@@ -0,0 +1 @@
uid://c76mhhqyk4lgh
@@ -0,0 +1,28 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightManager : Node
{
#region AutoLoad ( Contains _EnterTree() )
public static FightManager Instance { get; private set; } = null!;
public override void _EnterTree()
{
Instance = this;
}
#endregion
[Export]
public PackedScene fightingVesnaScene;
public FightParty fightParty = new();
public void StartFight(PackedScene[] enemies, FightInstance instance)
{
GD.Print("Starting Fight");
instance.Start(fightParty, enemies);
}
}
@@ -0,0 +1 @@
uid://j5ge24rk25wm
@@ -0,0 +1,6 @@
namespace Babushka.scripts.CSharp.Common.Fight;
public class FightParty
{
public bool vesna = true;
}
@@ -0,0 +1 @@
uid://cvhgnboybc4cm
@@ -0,0 +1,19 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightStarter : Node
{
[Export(PropertyHint.ArrayType)] private PackedScene[] enemies;
[Export] private FightInstance _fightInstance;
[Export] private bool _once = true;
private bool hasBeenStarted = false;
public void Start(Node2D _)
{
if (_once && hasBeenStarted)
return;
hasBeenStarted = true;
FightManager.Instance.StartFight(enemies, _fightInstance);
}
}
@@ -0,0 +1 @@
uid://di0xxwfw43m0i
@@ -0,0 +1,73 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightStateManager : Node
{
[Signal]
public delegate void ExitingTransitionEventHandler(FightState exitingState);
[Signal]
public delegate void EnteringTransitionEventHandler(FightState enteringState);
public enum FightState
{
None,
FightStartAnim,
Input,
InputTargetSelect,
FriendAttackAnim,
Enemy,
EnemyAttackAnim,
PlayerWinAnim,
EnemyWinAnim
}
private FightState _fightStateBacking = FightState.None;
public FightState CurrentFightState
{
set => Transition(_fightStateBacking, value);
get => _fightStateBacking;
}
private void Transition(FightState from, FightState to)
{
if(from == to)
return;
GD.Print($"Transitioning from {from} to {to}");
ExitTransition(from);
_fightStateBacking = to;
EnterTransition(to);
}
private void ExitTransition(FightState from)
{
EmitSignalExitingTransition(from);
}
private void EnterTransition(FightState to)
{
EmitSignalEnteringTransition(to);
switch (to)
{
case FightState.FightStartAnim:
EnterFightStartAnim();
break;
}
}
private void EnterFightStartAnim()
{
GetTree().CreateTimer(1).Timeout += () => CurrentFightState = FightState.Input;
}
public void ToStartAnim()
{
CurrentFightState = FightState.FightStartAnim;
}
public bool IsRunning()
{
return CurrentFightState != FightState.None;
}
}
@@ -0,0 +1 @@
uid://oe1uypehqvr7
+131
View File
@@ -0,0 +1,131 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class Fighter : Node2D
{
[Export] public string name;
[Export] public int maxHealth;
[Export] public int attackStrength;
[ExportCategory("References")]
[Export] private Node2D _attackButtons;
[Export] private Node2D _targetButtons;
[Export] private Node2D _targetMarker;
[Export] private Label _healthText;
[Export] private Node2D _visualSprite;
private int _health;
public FightInstance fightInstance;
public int Health
{
get => _health;
set
{
_health = value;
if (_health <= 0)
{
_health = 0;
Die();
}
}
}
private void Die()
{
_visualSprite.Scale = new Vector2(1, 0.3f);
}
public override void _Ready()
{
Health = maxHealth;
UpdateHealthVisual();
}
public void Attack()
{
fightInstance.SelectAttack(this);
}
public void HideAttackButton()
{
_attackButtons.Hide();
}
public void ShowAttackButton()
{
_attackButtons.Show();
}
public void HideTargetButtons()
{
_targetButtons.Hide();
}
public void ShowTargetButtons()
{
_targetButtons.Show();
}
public void TargetMouseEvent(Node viewport, InputEvent inputEvent, int shapeIdx)
{
if (inputEvent.IsPressed())
ClickedTarget();
}
public void AttackMouseEvent(Node viewport, InputEvent inputEvent, int shapeIdx)
{
if (inputEvent.IsPressed())
ClickedAttack();
}
private void ClickedAttack()
{
fightInstance.SelectAttack(this);
}
private void ClickedTarget()
{
fightInstance.SelectTargetAndAttack(this);
}
public void StartHoverTarget()
{
_targetMarker.Visible = true;
}
public void EndHoverTarget()
{
_targetMarker.Visible = false;
}
public void UpdateHealthVisual()
{
_healthText.Text = $"{Health}/{maxHealth}";
}
public void AttackAnimation(FightAttack attack)
{
var tween = GetTree().CreateTween();
tween.TweenProperty(this, "global_position", attack.target.GlobalPosition, 0.15);
tween.TweenCallback(Callable.From(() => attack.target?.HitAnimation(attack)));
tween.TweenProperty(this, "position", new Vector2(0, 0), 0.7)
.SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
}
private void HitAnimation(FightAttack attack)
{
var tween = GetTree().CreateTween();
tween.TweenProperty(this, "scale", new Vector2(1.4f, 0.6f), 0.15);
tween.TweenProperty(this, "scale", new Vector2(1,1), 0.4)
.SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
}
public bool IsDead()
{
return Health <= 0;
}
}
@@ -0,0 +1 @@
uid://by88f32fou7lh
+27
View File
@@ -0,0 +1,27 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Linq;
using System.Xml.Schema;
namespace Babushka.scripts.CSharp.Common.Util;
public static class LinqExtras
{
public static void ForEach<T>(this IEnumerable<T> self, Action<T> action)
{
foreach (var t in self)
{
action.Invoke(t);
}
}
public static T? Random<T>(this IEnumerable<T> self)
{
var selfList = self.ToList();
if (selfList.Count == 0) return default;
if (selfList.Count == 1) return selfList[0];
var randomIndex = new Random().Next(0, selfList.Count);
return selfList[randomIndex];
}
}