Fight happening base setup

This commit is contained in:
jonathan
2025-09-21 14:54:55 +02:00
parent fd0e631b1f
commit f27dd199b8
38 changed files with 1022 additions and 681 deletions
@@ -18,10 +18,13 @@ public partial class CameraController : Camera2D
[Export] private Node2D _followNode;
public FightInstance? fightToShow;
public FightHappening? fightToShow;
public override void _Process(double delta)
{
this.GlobalPosition = fightToShow?.camPositionNode.GlobalPosition ?? _followNode.GlobalPosition;
this.GlobalPosition = /*fightToShow?.camPositionNode.GlobalPosition ??*/ _followNode.GlobalPosition;
}
}
@@ -0,0 +1,34 @@
using Godot;
using System;
using Babushka.scripts.CSharp.Common.Fight;
public partial class AllFightersVisual : Node
{
[Export] private Node2D _allyFighters;
[Export] private Node2D _enemyFighters;
[Export] private PackedScene _blobFighterVisual;
[Export] private PackedScene _bigBlobFighterVisual;
[Export] private PackedScene _mavkaFighterVisual;
[Export] private PackedScene _yourMomFighterVisual;
[Export] private PackedScene _vesnaFighterVisual;
public void EnterFighter(FightWorld.Fighter fighter, bool isEnemy)
{
var parent = isEnemy ? _enemyFighters : _allyFighters;
var packedScene = fighter.type switch
{
FightWorld.Fighter.Type.Blob => _blobFighterVisual,
FightWorld.Fighter.Type.BigBlob => _bigBlobFighterVisual,
FightWorld.Fighter.Type.Mavka => _mavkaFighterVisual,
FightWorld.Fighter.Type.YourMom => _yourMomFighterVisual,
FightWorld.Fighter.Type.Vesna => _vesnaFighterVisual,
_ => throw new ArgumentOutOfRangeException()
};
var fighterVisual = packedScene.Instantiate<FighterVisual>();
fighterVisual.Initialize(fighter);
parent.AddChild(fighterVisual);
}
}
@@ -0,0 +1 @@
uid://dwsqst8fhhqlc
@@ -0,0 +1,320 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Babushka.scripts.CSharp.Common.Util;
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public class FightHappening
{
/*
To get a visual overview of the FightHappening state machine, refer to the graph on miro:
https://miro.com/app/board/uXjVK8YEprM=/?moveToWidget=3458764640805655262&cot=14
*/
#region Internal Types
public enum FightState
{
None,
FightStartAnim,
FightersEnter,
FightersEnterAnim,
NextFighter,
StateCheck,
InputActionSelect,
ActionCheckDetails,
InputActionDetail,
ActionExecute,
ActionAnim,
EnemyActionSelect,
PlayerWin,
EnemyWin,
}
private class FightersEnterStaging
{
public required List<FightWorld.Fighter> enteringAllyFighters;
public required List<FightWorld.Fighter> enteringEnemyFighters;
public bool HasAnyToExecute()
{
return enteringAllyFighters.Count != 0 || enteringEnemyFighters.Count != 0;
}
}
#endregion
#region Settings
private const float StartAnimationTime = 1;
private const float FightersEnterAnimationTime = 1;
#endregion
#region ShortCuts
private static FightWorld.FightHappeningData HappeningData =>
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
private static FightWorld.Fighter CurrentFighter => HappeningData.fighterStack.Current;
#endregion
#region Events
public event Action<FightState>? transitionFromState;
public event Action<FightState, FightState>? transitionState;
public event Action<FightState>? transitionToState;
#endregion
#region Staging
private FightersEnterStaging? _fightersEnterStaging;
private FighterAction? _actionStaging;
#endregion
#region Public Methods
public void StartFight()
{
RequireState(FightState.None);
ChangeState(FightState.FightStartAnim);
}
#endregion
#region State Machine
private void ChangeState(FightState nextState)
{
TransitionFromState();
var lastState = HappeningData.fightState;
HappeningData.fightState = nextState;
TransitionFromToState(nextState, lastState);
TransitionToState(nextState);
}
private void TransitionFromState()
{
// fixed behaviour
switch (HappeningData.fightState)
{
default: break;
}
// notify everyone else
transitionFromState?.Invoke(HappeningData.fightState);
}
private void TransitionFromToState(FightState nextState, FightState lastState)
{
transitionState?.Invoke(lastState, nextState);
}
private void TransitionToState(FightState nextState)
{
// notify everyone else
transitionToState?.Invoke(nextState);
// fixed behaviour
switch (HappeningData.fightState)
{
case FightState.FightStartAnim:
AdvanceToStateInSeconds(FightState.FightersEnter, StartAnimationTime);
break;
case FightState.FightersEnter:
_fightersEnterStaging = StageFightersEnter();
if (_fightersEnterStaging.HasAnyToExecute())
{
ExecuteFightersEnter();
ChangeState(FightState.FightersEnterAnim);
}
else
{
ChangeState(FightState.NextFighter);
}
break;
case FightState.FightersEnterAnim:
AdvanceToStateInSeconds(FightState.NextFighter, FightersEnterAnimationTime);
break;
case FightState.NextFighter:
ExecuteNextFighter();
ChangeState(FightState.StateCheck);
break;
case FightState.StateCheck:
// restest action staging
_actionStaging = null;
if ( /*TODO: are all allys dead*/ false)
{
ChangeState(FightState.EnemyWin);
}
else if (HappeningData.enemyGroup.AreAllDead())
{
ChangeState(FightState.PlayerWin);
}
else if (CurrentFighter.actionsLeft <= 0)
{
ChangeState(FightState.FightersEnter);
}
else if (CurrentFighter.isEnemy)
{
ChangeState(FightState.EnemyActionSelect);
}
else
{
ChangeState(FightState.InputActionSelect);
}
break;
case FightState.InputActionSelect:
// wait for player input
break;
case FightState.ActionCheckDetails:
if (ActionAbort())
ChangeState(FightState.InputActionSelect);
else if (ActionNeededDetail() != null)
ChangeState(FightState.InputActionDetail);
else
ChangeState(FightState.ActionExecute);
break;
case FightState.InputActionDetail:
// wait for player input
break;
case FightState.EnemyActionSelect:
_actionStaging = CurrentFighter.AutoSelectAction();
ChangeState(FightState.ActionExecute);
break;
case FightState.ActionExecute:
ExecuteAction();
ChangeState(FightState.ActionAnim);
break;
case FightState.ActionAnim:
var actionTime = GetActionAnimationEnd();
if (actionTime.IsType<float>())
{
AdvanceToStateInSeconds(FightState.StateCheck, actionTime);
}
else
{
_ = AdvanceToStateWhenDone(FightState.StateCheck, actionTime);
}
break;
default: break;
}
}
#endregion
#region Game Logic
private FightersEnterStaging StageFightersEnter()
{
// ally
var enteringAllyFighters = new List<FightWorld.Fighter>();
//TODO
// enemy
const int totalEnemySpace = 3;
var enemySpaceLeft = totalEnemySpace - HappeningData.enemyGroup.GetEnteredAmount();
var enterEnemyFighters = new List<FightWorld.Fighter>();
for (var i = 0; i < enemySpaceLeft; i++)
{
if (HappeningData.enemyGroup.TryGetFirstUnenteredFighter(out var fighter))
{
enterEnemyFighters.Add(fighter);
}
}
return new FightersEnterStaging
{
enteringAllyFighters = enteringAllyFighters,
enteringEnemyFighters = enterEnemyFighters
};
}
private void ExecuteFightersEnter()
{
Debug.Assert(_fightersEnterStaging != null);
foreach (var fighter in _fightersEnterStaging.enteringAllyFighters)
{
fighter.entered = true;
HappeningData.fighterStack.AddAsLast(fighter);
}
foreach (var fighter in _fightersEnterStaging.enteringEnemyFighters)
{
fighter.entered = true;
HappeningData.fighterStack.AddAsLast(fighter);
}
}
private void ExecuteNextFighter()
{
HappeningData.fighterStack.Next();
}
private void ExecuteAction()
{
Debug.Assert(_actionStaging != null);
_actionStaging.ExecuteAction();
}
private Variant<float, Func<bool>> GetActionAnimationEnd()
{
Debug.Assert(_actionStaging != null);
return _actionStaging.GetAnimationEnd();
}
private bool ActionAbort()
{
Debug.Assert(_actionStaging != null);
return _actionStaging.MarkedForAbort();
}
private FighterAction.FighterActionDetail? ActionNeededDetail()
{
Debug.Assert(_actionStaging != null);
return _actionStaging.NeededDetail();
}
#endregion // Game Logic
#region Utility
private void RequireState(params FightState[] states)
{
if (states.Contains(HappeningData.fightState))
return;
throw new Exception(
$"Can not call this Method while in state {HappeningData.fightState}. Only available in {string.Join(" ,", states)}");
}
private void AdvanceToStateInSeconds(FightState nextState, float seconds)
{
FightWorld.Instance.GetTree().CreateTimer(seconds).Timeout += () => ChangeState(nextState);
}
private async Task AdvanceToStateWhenDone(FightState nextState, Func<bool> isDone)
{
while (!isDone())
{
await FightWorld.Instance.ToSignal(FightWorld.Instance.GetTree(), SceneTree.SignalName.ProcessFrame);
}
ChangeState(nextState);
}
#endregion
}
@@ -0,0 +1,15 @@
using Godot;
using System;
using System.Diagnostics;
using Babushka.scripts.CSharp.Common.Fight;
public partial class FightHappeningSceneSetup : Node2D
{
public override void _Ready()
{
var fightHappening = FightWorld.Instance.fightHappeningData;
Debug.Assert(fightHappening != null, "Fight happening scene loaded, without a fight happening");
}
}
@@ -0,0 +1 @@
uid://cnhpnn8o0gybd
@@ -2,7 +2,7 @@ using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightSceneSetup : Node
public partial class FightRoomSceneSetup : Node
{
[Export] private Label debugLabel;
public override void _Ready()
@@ -9,15 +9,15 @@ public partial class FightSceneSwitcher : Node
{
[Export] private Node sceneRoot;
[Export] private string fightRoomScenePath;
[Export] private string fightingGroupScene;
[Export] private string fightHappeningScene;
private void LoadNext()
{
var nextRoom = FightWorld.Instance.currentRoom;
Debug.Assert(nextRoom != null, "nextRoom!=null");
var nextEnemyGroup = FightWorld.Instance.inFightWith;
SceneTransitionThreaded.Instance.ChangeSceneToFile(nextEnemyGroup != null
? fightingGroupScene
var nextFightHappening = FightWorld.Instance.fightHappeningData;
SceneTransitionThreaded.Instance.ChangeSceneToFile(nextFightHappening != null
? fightHappeningScene
: fightRoomScenePath);
UnloadAfterDelay();
}
+45
View File
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
namespace Babushka.scripts.CSharp.Common.Fight;
public static class FightUtils
{
public static int GetEnteredAmount(this FightWorld.EnemyGroup self)
{
return self.enemies.Count(e => e.IsAlive() && e.entered);
}
public static bool TryGetFirstUnenteredFighter(this FightWorld.EnemyGroup self, out FightWorld.Fighter fighter)
{
foreach (var f in self.enemies.Where(e=>!e.entered && e.IsAlive()))
{
fighter = f;
return true;
}
fighter = null!;
return false;
}
public static bool IsAlive(this FightWorld.Fighter self)
{
return self.GetHealth() >= 0;
}
public static bool IsDead(this FightWorld.Fighter self)
{
return !self.IsAlive();
}
public static int GetHealth(this FightWorld.Fighter self)
{
return self.health ?? self.maxHealth;
}
public static bool AreAllDead(this FightWorld.EnemyGroup self)
{
return self.enemies.All(e => e.IsDead());
}
}
@@ -0,0 +1 @@
uid://beuhpltb84pf
+37 -13
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Babushka.scripts.CSharp.Common.Util;
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
@@ -18,21 +19,39 @@ public partial class FightWorld : Node
public class EnemyGroup
{
public required List<Enemy> enemies;
public required List<Fighter> enemies;
}
public class Enemy
public class FightHappeningData
{
public FightHappening.FightState fightState = FightHappening.FightState.None;
public FighterStack fighterStack = new();
public required EnemyGroup enemyGroup;
}
public class Fighter
{
public enum Type
{
Blob,
BigBlob,
Mavka,
YourMom
YourMom,
Vesna
}
public required Type type;
public required int? health = null; // null => initialize to full health on spawn
public required int maxHealth;
public required bool isEnemy;
public required List<FighterAction> availableActions;
public int? health = null; // null => initialize to full health on spawn
public bool entered = false;
public int actionsLeft;
public FighterAction AutoSelectAction()
{
return availableActions.Random() ?? new FighterAction.Skip();
}
}
#region AutoLoad ( Contains _EnterTree() )
@@ -46,10 +65,10 @@ public partial class FightWorld : Node
}
#endregion
public World? world = null;
public Room? currentRoom = null;
public EnemyGroup? inFightWith = null;
public FightHappeningData? fightHappeningData = null;
public void MyEnterTree()
{
@@ -135,22 +154,27 @@ public partial class FightWorld : Node
return enemyGroup;
}
private Enemy GenerateSingleEnemy()
private Fighter GenerateSingleEnemy()
{
var typeRoll = GD.RandRange(0, 99);
var type = typeRoll switch
{
< 50 => Enemy.Type.Blob,
< 75 => Enemy.Type.BigBlob,
< 90 => Enemy.Type.Mavka,
_ => Enemy.Type.YourMom
< 50 => Fighter.Type.Blob,
< 75 => Fighter.Type.BigBlob,
< 90 => Fighter.Type.Mavka,
_ => Fighter.Type.YourMom
};
var enemy = new Enemy
var enemy = new Fighter
{
type = type,
health = null
health = null,
isEnemy = true,
maxHealth = 12,
availableActions = [
new FighterAction.Skip()
]
};
return enemy;
@@ -0,0 +1,80 @@
using System;
using System.Threading.Tasks;
using Babushka.scripts.CSharp.Common.Util;
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public abstract class FighterAction
{
public class TargetSelection
{
// ReSharper disable once MemberHidesStaticFromOuterClass
public static readonly TargetSelection Skip = new() { skipTargetSelection = () => true };
public Func<bool> skipTargetSelection = () => false;
}
public abstract class FighterActionDetail
{
public abstract bool DetailComplete();
}
private bool _abort = false;
#region Shortcuts
protected static FightWorld.FightHappeningData HappeningData =>
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
#endregion
/// <summary>
/// Executes the data modification for the action. This must happen instantly and not via a coroutines or callbacks.
/// To get a multiple hit effect for one action, use appropriate Visual Manipulation functions in AnimateAction.
/// </summary>
public virtual void ExecuteAction()
{
}
/// <summary>
/// Returns a way to determine, when an action animation is done
/// </summary>
/// <returns>
/// A variant that can be <c>float</c> or <c>Func&lt;bool&gt;</c>.<br/>
/// When the return type is <c>float</c>, the animation will take the return value amount of seconds.<br/>
/// When the return type is <c>Func&lt;bool&gt;</c>, the animation will be done, when the function returns true.
/// </returns>
public abstract Variant<float, Func<bool>> GetAnimationEnd();
/// <summary>
/// Animates the action.
/// </summary>
public virtual async Task AnimateAction()
{
}
public void MarkAbort()
{
_abort = true;
}
public bool MarkedForAbort()
{
return _abort;
}
public abstract FighterActionDetail? NeededDetail();
public class Skip : FighterAction
{
public override Variant<float, Func<bool>> GetAnimationEnd()
{
return 0f;
}
public override FighterActionDetail? NeededDetail()
{
return null;
}
}
}
@@ -0,0 +1,95 @@
using System.Collections.Generic;
using Godot.NativeInterop;
namespace Babushka.scripts.CSharp.Common.Fight;
public class FighterStack
{
private class Node
{
public Node next;
public FightWorld.Fighter fighter;
}
private Node? currentNode;
public FightWorld.Fighter Current => currentNode.fighter;
public void Next()
{
currentNode = currentNode.next;
}
public FightWorld.Fighter PeekNext()
{
return currentNode.next.fighter;
}
public void AddAsLast(FightWorld.Fighter value)
{
// if first node
if (currentNode == null)
{
currentNode = new Node { fighter = value };
currentNode.next = currentNode;
return;
}
var newNode = new Node { fighter = value, next = currentNode };
var node = currentNode;
while (node.next != currentNode)
{
node = node.next;
}
node.next = newNode;
}
public void AddAsNext(FightWorld.Fighter value)
{
// if first node
if (currentNode == null)
{
AddAsLast(value);
return;
}
var newNode = new Node { fighter = value, next = currentNode.next };
currentNode.next = newNode;
}
public bool Remove(FightWorld.Fighter value)
{
if (currentNode == null) return false;
// if only one node
if (currentNode.next == currentNode)
{
if (currentNode.fighter == value)
{
currentNode = null;
return true;
}
return false;
}
var node = currentNode;
do
{
// next is the fighter to remove
if (node.next.fighter == value)
{
// if removing current, keep current
// it will be implicitly deleted on the next Next() call
node.next = node.next.next;
return true;
}
node = node.next;
} while (node != currentNode);
return false;
}
}
@@ -1,12 +1,13 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class Fighter : Node2D
public partial class FighterVisual : Node2D
{
[Export] public string name;
[Export] public int maxHealth;
[Export] public int attackStrength;
[Export] public int maxActions = 1;
//[Export] public string name;
//[Export] public int maxHealth;
//[Export] public int attackStrength;
//[Export] public int maxActions = 1;
[Export] public FightWorld.Fighter.Type type;
[ExportCategory("References")]
[Export] private Node2D _attackButtons;
@@ -21,45 +22,29 @@ public partial class Fighter : Node2D
[Signal] public delegate void HealedEventHandler();
private int _health;
private int _actions;
private FightWorld.Fighter _boundFighter;
//private void Die()
//{
// _visualSprite.Scale = new Vector2(1, 0.3f);
// EmitSignalDying();
//}
public FightInstance fightInstance;
public int Health
//public override void _Ready()
//{
// UpdateHealthVisual();
// ResetActions();
//}
public void Initialize(FightWorld.Fighter fighter)
{
get => _health;
set
{
_health = value;
if (_health <= 0)
{
_health = 0;
Die();
}
if (_health > maxHealth)
{
_health = maxHealth;
}
}
}
private void Die()
{
_visualSprite.Scale = new Vector2(1, 0.3f);
EmitSignalDying();
}
public override void _Ready()
{
Health = maxHealth;
_boundFighter = fighter;
UpdateHealthVisual();
ResetActions();
}
public void Attack()
{
fightInstance.SelectAttack(this);
//FightHappening.SelectAttack(this);
}
public void HideAttackButton()
@@ -102,17 +87,17 @@ public partial class Fighter : Node2D
private void ClickedAttack()
{
fightInstance.SelectAttack(this);
//FightHappening.SelectAttack(this);
}
private void ClickedHeal()
{
fightInstance.SelectHeal(this);
//FightHappening.SelectHeal(this);
}
private void ClickedTarget()
{
fightInstance.SelectTargetAndAttack(this);
//FightHappening.SelectTargetAndAttack(this);
}
public void StartHoverTarget()
@@ -127,7 +112,18 @@ public partial class Fighter : Node2D
public void UpdateHealthVisual()
{
_healthText.Text = $"{Health}/{maxHealth}";
_healthText.Text = $"{_boundFighter.health}";
}
public bool IsDead()
{
//return Health <= 0;
return true;
}
public void ResetActions()
{
//_actions = maxActions;
}
public void AttackAnimation(FightAttack attack)
@@ -150,26 +146,6 @@ public partial class Fighter : Node2D
.SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
}
public bool IsDead()
{
return Health <= 0;
}
public void ResetActions()
{
_actions = maxActions;
}
public bool HasActionsLeft()
{
return _actions > 0;
}
public void DecrementActions()
{
_actions--;
}
public void HealAnimation()
{
EmitSignalHealed();
@@ -0,0 +1,7 @@
using System;
namespace Babushka.scripts.CSharp.Common.Fight;
public class NoFightHappeningException() : Exception("No fight happening right now")
{
}
@@ -4,6 +4,6 @@ public class FightAttack
{
public int damage;
public bool needsSelectedTarget;
public Fighter? target;
public Fighter attacker;
public FighterVisual? target;
public FighterVisual attacker;
}
@@ -1,351 +0,0 @@
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 (CheckWinAndSetState())
break;
if (CheckFriendlyActionsLeftAndSetState())
break;
ShowAttackButtons();
break;
case FightStateManager.FightState.InputTargetSelect:
ShowTargetButtons();
break;
case FightStateManager.FightState.FriendAttackAnim:
ExecuteAttack();
GetTree().CreateTimer(1).Timeout += () => _fightStateManager.CurrentFightState = FightStateManager.FightState.Input;
break;
case FightStateManager.FightState.Enemy:
if (CheckWinAndSetState())
break;
if (CheckEnemyActionsLeftAndSetState())
break;
DecideEnemyAttack();
_fightStateManager.CurrentFightState = FightStateManager.FightState.EnemyAttackAnim;
break;
case FightStateManager.FightState.EnemyAttackAnim:
ExecuteAttack();
GetTree().CreateTimer(1).Timeout += () => _fightStateManager.CurrentFightState = FightStateManager.FightState.Enemy;
break;
case FightStateManager.FightState.PlayerWinAnim:
_fightEndText.Text = "You Win!";
GetTree().CreateTimer(1.5).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;
case FightStateManager.FightState.ChangeSideToEnemy:
ResetEnemyActions();
_fightStateManager.CurrentFightState = FightStateManager.FightState.Enemy;
break;
case FightStateManager.FightState.ChangeSideToFriendly:
ResetFriendlyActions();
_fightStateManager.CurrentFightState = FightStateManager.FightState.Input;
break;
case FightStateManager.FightState.Heal:
Heal();
GetTree().CreateTimer(1).Timeout += () => _fightStateManager.CurrentFightState = FightStateManager.FightState.Input;
break;
}
};
}
private void Heal()
{
// TODO: make heal staging system
_friendlyFighters.Where(f => !f.IsDead()).ForEach(f =>
{
f.Health += 50;
f.HealAnimation();
f.DecrementActions();
});
UpdateHealthVisual();
}
private void ResetEnemyActions()
{
_enemyFighters.ForEach(f => f.ResetActions());
}
private void ResetFriendlyActions()
{
_friendlyFighters.ForEach(f => f.ResetActions());
}
/**
* <returns>
* <c>true</c> if the state was changed
* </returns>
*/
private bool CheckFriendlyActionsLeftAndSetState()
{
var hasActionsLeft = _friendlyFighters.Where(f => !f.IsDead()).Any(f => f.HasActionsLeft());
if (hasActionsLeft)
{
return false;
} // else
_fightStateManager.CurrentFightState = FightStateManager.FightState.ChangeSideToEnemy;
return true;
}
/**
* <returns>
* <c>true</c> if the state was changed
* </returns>
*/
private bool CheckEnemyActionsLeftAndSetState()
{
var hasActionsLeft = _enemyFighters.Where(f => !f.IsDead()).Any(f => f.HasActionsLeft());
if (hasActionsLeft)
{
return false;
} // else
_fightStateManager.CurrentFightState = FightStateManager.FightState.ChangeSideToFriendly;
return true;
}
private void CleanUp()
{
_enemyFighters.ForEach(f => f.QueueFree());
_friendlyFighters.ForEach(f => f.QueueFree());
_enemyFighters = new();
_friendlyFighters = new();
}
private void DecideEnemyAttack()
{
var availableEnemyFighters =
_enemyFighters
.Where(f => !f.IsDead())
.Where(f=>f.HasActionsLeft())
.ToList();
var aliveFriendlyFighters =
_friendlyFighters
.Where(f => !f.IsDead())
.ToList();
if (availableEnemyFighters.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 = availableEnemyFighters.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.DecrementActions();
_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.Where(f => !f.IsDead()).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 void SelectHeal(Fighter fighter)
{
_fightStateManager.CurrentFightState = FightStateManager.FightState.Heal;
}
public bool CheckWinAndSetState()
{
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;
}
}
@@ -19,10 +19,10 @@ public partial class FightManager : Node
public FightParty fightParty = new();
public void StartFight(PackedScene[] enemies, FightInstance instance)
public void StartFight(PackedScene[] enemies, FightHappening happening)
{
GD.Print("Starting Fight");
instance.Start(fightParty, enemies);
//happening.Start(fightParty, enemies);
}
}
@@ -4,7 +4,7 @@ namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightStarter : Node
{
[Export(PropertyHint.ArrayType)] private PackedScene[] enemies;
[Export] private FightInstance _fightInstance;
//[Export] private FightHappening _fightHappening;
[Export] private bool _once = true;
private bool hasBeenStarted = false;
@@ -14,6 +14,6 @@ public partial class FightStarter : Node
return;
hasBeenStarted = true;
FightManager.Instance.StartFight(enemies, _fightInstance);
//FightManager.Instance.StartFight(enemies, _fightHappening);
}
}
@@ -1,76 +0,0 @@
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,
ChangeSideToEnemy,
ChangeSideToFriendly,
Heal,
}
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;
}
}
@@ -1 +0,0 @@
uid://oe1uypehqvr7
+63
View File
@@ -0,0 +1,63 @@
using System;
using System.Diagnostics;
namespace Babushka.scripts.CSharp.Common.Util;
public class Variant<T1, T2>
{
private Type _type;
private Object _value;
public Type GetVariantType()
{
return _type;
}
public T GetValue<T>()
{
if (_type != typeof(T))
throw new ArgumentOutOfRangeException(
$"The Variant does not store a {typeof(T)}. The current type is {_type}");
return (T)_value;
}
public void SetValue<T>(T value)
{
if (typeof(T1) != typeof(T) && typeof(T2) != typeof(T))
throw new ArgumentOutOfRangeException(
$"The Variant does not support type {typeof(T)}. Supported types are {typeof(T1)} and {typeof(T2)}");
_type = typeof(T);
_value = value!;
}
public static implicit operator T1(Variant<T1, T2> v)
{
return v.GetValue<T1>();
}
public static implicit operator T2(Variant<T1, T2> v)
{
return v.GetValue<T2>();
}
public static implicit operator Variant<T1, T2>(T1 t)
{
return new Variant<T1, T2> { _type = typeof(T1), _value = t! };
}
public static implicit operator Variant<T1, T2>(T2 t)
{
return new Variant<T1, T2> { _type = typeof(T2), _value = t! };
}
public bool IsType<T>()
{
if (typeof(T1) != typeof(T) && typeof(T2) != typeof(T))
throw new ArgumentOutOfRangeException(
$"The Variant does not support type {typeof(T)}. Supported types are {typeof(T1)} and {typeof(T2)}");
return _type == typeof(T);
}
}