Made fight fightable

This commit is contained in:
jonathan
2025-09-30 16:23:05 +02:00
parent daabcdc5ee
commit 8055381478
43 changed files with 925 additions and 278 deletions
@@ -1,4 +1,6 @@
using System;
using System.Linq;
using Babushka.scripts.CSharp.Common.Util;
using Godot;
namespace Babushka.scripts.CSharp.Common.CharacterControls;
@@ -10,80 +12,89 @@ public partial class InteractionArea2D : Node2D
[Export] private bool _active = true;
[Export] private bool _useOutline = true;
[Export] private ShaderMaterial _outlineMaterial;
[Export] private bool _useSprite = true;
[Export] private CanvasItem _spriteToOutline;
//[Export] private bool _useSprite = true;
[Export(PropertyHint.ArrayType)] private CanvasItem[] _spriteToOutline = [];
[Export] private bool _showLabel = true;
[Export] private int _id = -1; // TODO: remove
private Material _backupMaterial;
private Material[] _backupMaterial;
[Signal] public delegate void InteractedToolEventHandler(int id); // TODO: remove
[Signal] public delegate void InteractedEventHandler();
public bool IsActive
{
get => _active;
set => _active = value;
set => _active = value;
}
public override void _Ready()
{
if (_useSprite && _useOutline)
if (_useOutline)
{
try
{
_backupMaterial = _spriteToOutline.Material;
_backupMaterial = _spriteToOutline.Select(s => s.Material).ToArray();
}
catch(Exception exception)
catch (Exception exception)
{
GD.PrintErr($"No sprite to outline found on: {GetParent().Name}" + exception.Message);
}
}
}
public void OnPlayerEntered(Node2D player)
{
if (!_active)
return;
if(_showLabel)
if (_showLabel)
_label.Show();
if (!_useSprite || !_useOutline)
if (!_useOutline)
return;
_spriteToOutline.Material = _outlineMaterial;
_spriteToOutline.ForEach(s => s.Material = _outlineMaterial);
}
public void OnPlayerExited(Node2D player)
{
if (!_active)
return;
_label.Hide();
if (!_useSprite || !_useOutline)
if (!_useOutline)
return;
_spriteToOutline.Material = _backupMaterial;
//_spriteToOutline.Material = _backupMaterial;
for (var i = 0; i < _spriteToOutline.Length; i++)
{
_spriteToOutline[i].Material = _backupMaterial[i];
}
}
public override void _Input(InputEvent @event)
{
if (!_active)
return;
if (@event.IsAction("interact") && @event.IsPressed())
{
if (_area.HasOverlappingBodies())
{
_label.Hide();
if (_useSprite && _useOutline)
_spriteToOutline.Material = _backupMaterial;
if (_useOutline)
{
for (var i = 0; i < _spriteToOutline.Length; i++)
{
_spriteToOutline[i].Material = _backupMaterial[i];
}
}
EmitSignal(SignalName.InteractedTool, _id);
EmitSignal(SignalName.Interacted);
}
@@ -93,7 +104,7 @@ public partial class InteractionArea2D : Node2D
public void SetSpriteActiveState(bool success, int id) // TODO: remove
{
GD.PrintErr("SetSpriteActiveState is being called.");
if(!_active)
if (!_active)
return;
}
@@ -0,0 +1,39 @@
using System;
namespace Babushka.scripts.CSharp.Common.Fight.ActionDetails;
public class TargetSelectActionDetail : FighterAction.FighterActionDetail
{
public enum VisualRange
{
Single
}
// settings
public required bool selectEnemy;
public required bool selectAlly;
public VisualRange visualRange = VisualRange.Single;
// result
private FightWorld.Fighter? target;
public override bool DetailComplete()
{
return target != null;
}
public void ResetResult()
{
target = null;
}
public void SetTarget(FightWorld.Fighter fighter)
{
target = fighter;
}
public FightWorld.Fighter GetTarget()
{
return target ?? throw new InvalidOperationException("No target selected");
}
}
@@ -0,0 +1 @@
uid://e8c8ym0fyprn
@@ -0,0 +1,46 @@
using System;
using Babushka.scripts.CSharp.Common.Fight.ActionDetails;
using Babushka.scripts.CSharp.Common.Util;
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight.Actions;
public class AllyAttackAction : FighterAction
{
// details
public TargetSelectActionDetail targetSelect = new()
{
selectEnemy = true,
selectAlly = false
};
public override Variant<float, Func<bool>> GetAnimationEnd()
{
return 1;
}
public override bool NextDetail()
{
return !targetSelect.DetailComplete();
}
public override FighterActionDetail CurrentDetail()
{
return targetSelect;
}
public override AllyActionButton BindToActionButton()
{
return AllyActionButton.Attack;
}
public override void Reset()
{
targetSelect.ResetResult();
}
public override void ExecuteAction()
{
targetSelect.GetTarget().AddHealth(-5);
}
}
@@ -0,0 +1 @@
uid://c8c4t80bqsja5
+114 -23
View File
@@ -1,34 +1,125 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using Babushka.scripts.CSharp.Common.Fight;
using Babushka.scripts.CSharp.Common.Fight.ActionDetails;
using Babushka.scripts.CSharp.Common.Util;
public partial class AllFightersVisual : Node
{
[Export] private Node2D _allyFighters;
[Export] private Node2D _enemyFighters;
[ExportCategory("References")] [Export]
private Node2D _allyFighters = null!;
[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)
[Export] private Node2D _enemyFighters = null!;
[ExportCategory("Fighter Visual Scenes")]
[Export] private PackedScene _blobFighterVisual = null!;
[Export] private PackedScene _bigBlobFighterVisual = null!;
[Export] private PackedScene _mavkaFighterVisual = null!;
[Export] private PackedScene _yourMomFighterVisual = null!;
[Export] private PackedScene _vesnaFighterVisual = null!;
[ExportCategory("Settings")]
[Export(PropertyHint.ArrayType)] private float[] _positionDistanceFromCenter = [10, 20, 30];
private Dictionary<FightWorld.Fighter, FighterVisual> _fighterVisuals = new();
#region Shortcuts
private FightWorld.FightHappeningData HappeningData =>
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
#endregion
#region State Reactions
public void FightHappeningStateChange(FightHappening.FightState from, FightHappening.FightState to)
{
var parent = isEnemy ? _enemyFighters : _allyFighters;
var packedScene = fighter.type switch
if (to == FightHappening.FightState.FightersEnterAnim)
{
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()
};
EnterFighter();
}
var fighterVisual = packedScene.Instantiate<FighterVisual>();
fighterVisual.Initialize(fighter);
parent.AddChild(fighterVisual);
if (to == FightHappening.FightState.InputActionDetail)
{
if (HappeningData.actionStaging!.CurrentDetail() is TargetSelectActionDetail targetDetail)
{
ShowTargetSelect(targetDetail);
}
}
if (from == FightHappening.FightState.InputActionDetail)
{
HideTargetSelect();
}
}
}
public void EnterFighter()
{
if (HappeningData.fightersEnterStaging == null)
return;
if (!HappeningData.fightersEnterStaging.HasAnyToExecute())
return;
foreach (var fighter in HappeningData.fightersEnterStaging.enteringEnemyFighters)
{
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);
_enemyFighters.AddChild(fighterVisual);
fighterVisual.Position = new Vector2(_positionDistanceFromCenter[_enemyFighters.GetChildCount() - 1], 0);
_fighterVisuals.Add(fighter, fighterVisual);
}
foreach (var fighter in HappeningData.fightersEnterStaging.enteringAllyFighters)
{
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);
_allyFighters.AddChild(fighterVisual);
fighterVisual.Position = new Vector2(-_positionDistanceFromCenter[_allyFighters.GetChildCount() - 1], 0);
_fighterVisuals.Add(fighter, fighterVisual);
}
}
private void ShowTargetSelect(TargetSelectActionDetail targetDetail)
{
if (targetDetail.selectEnemy)
_fighterVisuals.Where(kv => kv.Key.isEnemy).ForEach(kv => kv.Value.SetTargetSelectionActive(true));
if (targetDetail.selectAlly)
_fighterVisuals.Where(kv => !kv.Key.isEnemy).ForEach(kv => kv.Value.SetTargetSelectionActive(true));
}
private void HideTargetSelect()
{
foreach (var visual in _fighterVisuals.Values)
{
visual.SetTargetSelectionActive(false);
}
}
#endregion
}
@@ -0,0 +1,32 @@
using Babushka.scripts.CSharp.Common.Fight.Actions;
namespace Babushka.scripts.CSharp.Common.Fight;
public class AllyFighters
{
public FightWorld.Fighter vesnaFighter = new()
{
type = FightWorld.Fighter.Type.Vesna,
maxHealth = 20,
isEnemy = false,
availableActions =
[
new AllyAttackAction()
]
};
public FightWorld.Fighter chuhaFighter = new()
{
type = FightWorld.Fighter.Type.Chuha,
maxHealth = 15,
isEnemy = false,
availableActions =
[
new FighterAction.Skip()
]
};
public bool IsAlive()
{
return vesnaFighter.IsAlive();
}
}
@@ -0,0 +1 @@
uid://dst8xcyiw18uc
+101 -54
View File
@@ -9,7 +9,7 @@ using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public class FightHappening
public partial class FightHappening : Node
{
/*
To get a visual overview of the FightHappening state machine, refer to the graph on miro:
@@ -36,14 +36,14 @@ public class FightHappening
EnemyWin,
}
private class FightersEnterStaging
public class FightersEnterStaging
{
public required List<FightWorld.Fighter> enteringAllyFighters;
public required List<FightWorld.Fighter> enteringEnemyFighters;
public bool HasAnyToExecute()
{
return enteringAllyFighters.Count != 0 || enteringEnemyFighters.Count != 0;
return enteringAllyFighters.Any() || enteringEnemyFighters.Any();
}
}
@@ -56,7 +56,7 @@ public class FightHappening
#endregion
#region ShortCuts
#region Shortcuts
private static FightWorld.FightHappeningData HappeningData =>
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
@@ -67,18 +67,33 @@ public class FightHappening
#region Events
public event Action<FightState>? transitionFromState;
public event Action<FightState, FightState>? transitionState;
public event Action<FightState>? transitionToState;
[Signal]
public delegate void SignalTransitionFromStateEventHandler(FightState state);
[Signal]
public delegate void SignalTransitionStateEventHandler(FightState from, FightState to);
[Signal]
public delegate void SignalTransitionToStateEventHandler(FightState state);
#endregion
#region Staging
#region Singleton
private FightersEnterStaging? _fightersEnterStaging;
private FighterAction? _actionStaging;
public static FightHappening Instance = null!;
private void SetupInstance()
{
Instance = this;
}
#endregion
public override void _Ready()
{
SetupInstance();
StartFight();
}
#region Public Methods
@@ -88,17 +103,51 @@ public class FightHappening
ChangeState(FightState.FightStartAnim);
}
public void ActionSelect(FighterAction action)
{
RequireState(FightState.InputActionSelect);
HappeningData.actionStaging = action;
action.Reset();
ChangeState(FightState.ActionCheckDetails);
}
public void DetailFilled()
{
RequireState(FightState.InputActionDetail);
ChangeState(FightState.ActionCheckDetails);
}
#endregion
#region State Machine
private bool _inTransition = false;
private FightState? _changeToAfterTransition = null;
private void ChangeState(FightState nextState)
{
_changeToAfterTransition = null;
if (_inTransition)
{
_changeToAfterTransition = nextState;
return;
}
_inTransition = true;
TransitionFromState();
var lastState = HappeningData.fightState;
HappeningData.fightState = nextState;
TransitionFromToState(nextState, lastState);
TransitionToState(nextState);
EmitSignalSignalTransitionFromState(lastState);
EmitSignalSignalTransitionState(lastState, nextState);
EmitSignalSignalTransitionToState(nextState);
_inTransition = false;
if (_changeToAfterTransition.HasValue)
{
ChangeState(_changeToAfterTransition.Value);
}
}
private void TransitionFromState()
@@ -108,21 +157,10 @@ public class FightHappening
{
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)
{
@@ -130,8 +168,8 @@ public class FightHappening
AdvanceToStateInSeconds(FightState.FightersEnter, StartAnimationTime);
break;
case FightState.FightersEnter:
_fightersEnterStaging = StageFightersEnter();
if (_fightersEnterStaging.HasAnyToExecute())
HappeningData.fightersEnterStaging = StageFightersEnter();
if (HappeningData.fightersEnterStaging.HasAnyToExecute())
{
ExecuteFightersEnter();
ChangeState(FightState.FightersEnterAnim);
@@ -150,10 +188,11 @@ public class FightHappening
ChangeState(FightState.StateCheck);
break;
case FightState.StateCheck:
// restest action staging
_actionStaging = null;
// restest action staging and fighter enter staging
HappeningData.actionStaging = null;
HappeningData.fightersEnterStaging = null;
if ( /*TODO: are all allys dead*/ false)
if (!FightWorld.Instance.allyFighters.IsAlive())
{
ChangeState(FightState.EnemyWin);
}
@@ -161,7 +200,7 @@ public class FightHappening
{
ChangeState(FightState.PlayerWin);
}
else if (CurrentFighter.actionsLeft <= 0)
else if (CurrentFighter.actionPointsLeft <= 0)
{
ChangeState(FightState.FightersEnter);
}
@@ -179,9 +218,11 @@ public class FightHappening
// wait for player input
break;
case FightState.ActionCheckDetails:
RequireNotNull(HappeningData.actionStaging);
if (ActionAbort())
ChangeState(FightState.InputActionSelect);
else if (ActionNeededDetail() != null)
else if (ActionNeededDetail())
ChangeState(FightState.InputActionDetail);
else
ChangeState(FightState.ActionExecute);
@@ -190,7 +231,7 @@ public class FightHappening
// wait for player input
break;
case FightState.EnemyActionSelect:
_actionStaging = CurrentFighter.AutoSelectAction();
HappeningData.actionStaging = CurrentFighter.AutoSelectAction();
ChangeState(FightState.ActionExecute);
break;
case FightState.ActionExecute:
@@ -212,7 +253,6 @@ public class FightHappening
default: break;
}
}
#endregion
#region Game Logic
@@ -221,38 +261,33 @@ public class FightHappening
{
// ally
var enteringAllyFighters = new List<FightWorld.Fighter>();
//TODO
var allyFighters = FightWorld.Instance.allyFighters;
if (!allyFighters.vesnaFighter.entered)
{
enteringAllyFighters.Add(allyFighters.vesnaFighter);
}
// 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
enteringEnemyFighters = HappeningData.enemyGroup.GetUptoUnenteredFighters(enemySpaceLeft).ToList()
};
}
private void ExecuteFightersEnter()
{
Debug.Assert(_fightersEnterStaging != null);
foreach (var fighter in _fightersEnterStaging.enteringAllyFighters)
Debug.Assert(HappeningData.fightersEnterStaging != null);
foreach (var fighter in HappeningData.fightersEnterStaging.enteringAllyFighters)
{
fighter.entered = true;
HappeningData.fighterStack.AddAsLast(fighter);
}
foreach (var fighter in _fightersEnterStaging.enteringEnemyFighters)
foreach (var fighter in HappeningData.fightersEnterStaging.enteringEnemyFighters)
{
fighter.entered = true;
HappeningData.fighterStack.AddAsLast(fighter);
@@ -262,30 +297,32 @@ public class FightHappening
private void ExecuteNextFighter()
{
HappeningData.fighterStack.Next();
CurrentFighter.actionPointsLeft = CurrentFighter.maxActionPoints;
}
private void ExecuteAction()
{
Debug.Assert(_actionStaging != null);
_actionStaging.ExecuteAction();
Debug.Assert(HappeningData.actionStaging != null);
HappeningData.actionStaging.ExecuteAction();
CurrentFighter.actionPointsLeft -= HappeningData.actionStaging.GetActionPointCost();
}
private Variant<float, Func<bool>> GetActionAnimationEnd()
{
Debug.Assert(_actionStaging != null);
return _actionStaging.GetAnimationEnd();
Debug.Assert(HappeningData.actionStaging != null);
return HappeningData.actionStaging.GetAnimationEnd();
}
private bool ActionAbort()
{
Debug.Assert(_actionStaging != null);
return _actionStaging.MarkedForAbort();
Debug.Assert(HappeningData.actionStaging != null);
return HappeningData.actionStaging.MarkedForAbort();
}
private FighterAction.FighterActionDetail? ActionNeededDetail()
private bool ActionNeededDetail()
{
Debug.Assert(_actionStaging != null);
return _actionStaging.NeededDetail();
Debug.Assert(HappeningData.actionStaging != null);
return HappeningData.actionStaging.NextDetail();
}
#endregion // Game Logic
@@ -300,6 +337,14 @@ public class FightHappening
throw new Exception(
$"Can not call this Method while in state {HappeningData.fightState}. Only available in {string.Join(" ,", states)}");
}
private void RequireNotNull(Object? o)
{
if (o != null)
return;
throw new Exception("Object must not be null to call this method");
}
private void AdvanceToStateInSeconds(FightState nextState, float seconds)
{
@@ -317,4 +362,6 @@ public class FightHappening
}
#endregion
}
@@ -0,0 +1,53 @@
using Godot;
using System;
using System.Linq;
using Babushka.scripts.CSharp.Common.Fight;
public partial class FightHappeningStateDebugger : Node
{
[Export] private Label _label;
private FightWorld.FightHappeningData Data => FightWorld.Instance.fightHappeningData!;
public void StateChange(FightHappening.FightState from, FightHappening.FightState to)
{
_label.Text += $"State changed from {from} to {to}\n";
switch (to)
{
case FightHappening.FightState.None:
break;
case FightHappening.FightState.FightStartAnim:
break;
case FightHappening.FightState.FightersEnter:
break;
case FightHappening.FightState.FightersEnterAnim:
_label.Text +=
$" {Data.fightersEnterStaging!.enteringAllyFighters.Count} allies " +
$"and {Data.fightersEnterStaging.enteringEnemyFighters.Count} enemies are entering the fight.\n";
break;
case FightHappening.FightState.NextFighter:
break;
case FightHappening.FightState.StateCheck:
break;
case FightHappening.FightState.InputActionSelect:
break;
case FightHappening.FightState.ActionCheckDetails:
break;
case FightHappening.FightState.InputActionDetail:
break;
case FightHappening.FightState.ActionExecute:
_label.Text += $" Executing action: {Data.actionStaging!.GetType()}\n";
break;
case FightHappening.FightState.ActionAnim:
break;
case FightHappening.FightState.EnemyActionSelect:
break;
case FightHappening.FightState.PlayerWin:
break;
case FightHappening.FightState.EnemyWin:
break;
default:
throw new ArgumentOutOfRangeException(nameof(to), to, null);
}
}
}
@@ -0,0 +1 @@
uid://d2ugtb3dalrg3
@@ -0,0 +1,27 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightHappeningStateReaction : Node
{
[Export] private FightHappening.FightState _fightState;
[Signal]
public delegate void OnStateEnteredEventHandler();
[Signal]
public delegate void OnStateExitedEventHandler();
public void FightHappeningStateTransitioned(FightHappening.FightState fromState, FightHappening.FightState toState)
{
if (fromState == _fightState)
{
EmitSignalOnStateExited();
}
if (toState == _fightState)
{
EmitSignalOnStateEntered();
}
}
}
@@ -0,0 +1 @@
uid://buiwuf7pjfq8
@@ -1,21 +1,29 @@
using System.Collections.Generic;
using Babushka.scripts.CSharp.Common.Util;
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class FightRoomSceneSetup : Node
{
[Export] private Label debugLabel;
[Export(PropertyHint.ArrayType)] private Node2D[] _enemyGroupSpawns;
[Export] private PackedScene _roamingEnemyGroupPrefab;
[Export] private FightSceneSwitcher _fightSceneSwitcher;
public override void _Ready()
{
var room = FightWorld.Instance.currentRoom!;
debugLabel.Text = $"Room Debug:\n{room.paths.Count} paths out of this room\n{room.enemyGroups.Count} enemy groups:\n";
foreach (var enemyGroup in room.enemyGroups)
var i = 0;
foreach (var availableParent in _enemyGroupSpawns.Shuffle())
{
debugLabel.Text += $" {enemyGroup.enemies.Count} enemies:\n";
foreach (var enemy in enemyGroup.enemies)
{
debugLabel.Text += $" {enemy.type}\n";
}
var enemyGroup = room.enemyGroups[i];
var roamingEnemyGroup = _roamingEnemyGroupPrefab.Instantiate<RoamingEnemyGroup>();
roamingEnemyGroup.Initialize(enemyGroup, _fightSceneSwitcher);
availableParent.AddChild(roamingEnemyGroup);
if (i >= room.enemyGroups.Count - 1) break;
i++;
}
}
}
@@ -25,7 +25,7 @@ public partial class FightSceneSwitcher : Node
private async void UnloadAfterDelay()
{
await ToSignal(GetTree().CreateTimer(1.0f), "timeout"); // 1.0f seconds
sceneRoot.QueueFree();
//sceneRoot.QueueFree();
}
public void SwitchRoom(int pathIndex)
@@ -38,4 +38,16 @@ public partial class FightSceneSwitcher : Node
FightWorld.Instance.currentRoom = nextRoom;
LoadNext();
}
public void SwitchToFight(FightWorld.FighterGroup enemyGroup)
{
if (FightWorld.Instance.fightHappeningData != null)
throw new Exception("Trying to start a fight while already in a fight");
FightWorld.Instance.fightHappeningData = new FightWorld.FightHappeningData
{
enemyGroup = enemyGroup,
};
LoadNext();
}
}
+18 -13
View File
@@ -5,41 +5,46 @@ namespace Babushka.scripts.CSharp.Common.Fight;
public static class FightUtils
{
public static int GetEnteredAmount(this FightWorld.EnemyGroup self)
public static int GetEnteredAmount(this FightWorld.FighterGroup self)
{
return self.enemies.Count(e => e.IsAlive() && e.entered);
}
public static bool TryGetFirstUnenteredFighter(this FightWorld.EnemyGroup self, out FightWorld.Fighter fighter)
public static IEnumerable<FightWorld.Fighter> GetUptoUnenteredFighters(
this FightWorld.FighterGroup self,
int maxFighters)
{
foreach (var f in self.enemies.Where(e=>!e.entered && e.IsAlive()))
{
fighter = f;
return true;
}
if (maxFighters <= self.enemies.Count)
return self.enemies
.Where(e => !e.entered && e.IsAlive());
fighter = null!;
return false;
return self.enemies
.Where(e => !e.entered && e.IsAlive())
.Take(maxFighters);
}
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)
public static void AddHealth(this FightWorld.Fighter self, int addHealth)
{
self.health = self.GetHealth() + addHealth;
}
public static bool AreAllDead(this FightWorld.FighterGroup self)
{
return self.enemies.All(e => e.IsDead());
}
}
+24 -17
View File
@@ -14,10 +14,10 @@ public partial class FightWorld : Node
public class Room
{
public required Dictionary<int, Room> paths;
public required List<EnemyGroup> enemyGroups;
public required List<FighterGroup> enemyGroups;
}
public class EnemyGroup
public class FighterGroup
{
public required List<Fighter> enemies;
}
@@ -26,7 +26,9 @@ public partial class FightWorld : Node
{
public FightHappening.FightState fightState = FightHappening.FightState.None;
public FighterStack fighterStack = new();
public required EnemyGroup enemyGroup;
public required FighterGroup enemyGroup;
public FightHappening.FightersEnterStaging? fightersEnterStaging;
public FighterAction? actionStaging;
}
public class Fighter
@@ -37,16 +39,18 @@ public partial class FightWorld : Node
BigBlob,
Mavka,
YourMom,
Vesna
Vesna,
Chuha
}
public required Type type;
public required int maxHealth;
public required bool isEnemy;
public required List<FighterAction> availableActions;
public int maxActionPoints = 1;
public int? health = null; // null => initialize to full health on spawn
public bool entered = false;
public int actionsLeft;
public int actionPointsLeft;
public FighterAction AutoSelectAction()
{
@@ -69,6 +73,7 @@ public partial class FightWorld : Node
public World? world = null;
public Room? currentRoom = null;
public FightHappeningData? fightHappeningData = null;
public AllyFighters allyFighters = new();
public void MyEnterTree()
{
@@ -123,9 +128,9 @@ public partial class FightWorld : Node
return room;
}
private List<EnemyGroup> GenerateEnemyGroups()
private List<FighterGroup> GenerateEnemyGroups()
{
var enemyGroups = new List<EnemyGroup>();
var enemyGroups = new List<FighterGroup>();
var enemyGroupCount = GD.RandRange(1, 3);
@@ -137,9 +142,9 @@ public partial class FightWorld : Node
return enemyGroups;
}
private EnemyGroup GenerateSingleEnemyGroup()
private FighterGroup GenerateSingleEnemyGroup()
{
var enemyGroup = new EnemyGroup
var enemyGroup = new FighterGroup
{
enemies = []
};
@@ -158,13 +163,14 @@ public partial class FightWorld : Node
{
var typeRoll = GD.RandRange(0, 99);
var type = typeRoll switch
{
< 50 => Fighter.Type.Blob,
< 75 => Fighter.Type.BigBlob,
< 90 => Fighter.Type.Mavka,
_ => Fighter.Type.YourMom
};
//var type = typeRoll switch
//{
// < 50 => Fighter.Type.Blob,
// < 75 => Fighter.Type.BigBlob,
// < 90 => Fighter.Type.Mavka,
// _ => Fighter.Type.YourMom
//};
var type = Fighter.Type.Blob;
var enemy = new Fighter
{
@@ -172,7 +178,8 @@ public partial class FightWorld : Node
health = null,
isEnemy = true,
maxHealth = 12,
availableActions = [
availableActions =
[
new FighterAction.Skip()
]
};
+56 -5
View File
@@ -7,13 +7,24 @@ namespace Babushka.scripts.CSharp.Common.Fight;
public abstract class FighterAction
{
// enum has explicit values, because they are set in godot signals as integers
// e.g. here: BabushkaSceneFightHappening => ActionSelect/BottomPanel/VBoxContainer/MarginContainer/HBoxContainer/MarginContainer/AttackButton
public enum AllyActionButton
{
None,
Attack = 1,
Summon = 2,
Talk = 3,
Flee = 4,
}
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();
@@ -63,7 +74,46 @@ public abstract class FighterAction
return _abort;
}
public abstract FighterActionDetail? NeededDetail();
/// <summary>
/// Returns the FighterActionDetail, that is currently handled.
/// </summary>
/// <returns></returns>
public virtual FighterActionDetail CurrentDetail()
{
throw new Exception("Action has no details to handle");
}
/// <summary>
/// Sets the next Detail to be handled. Returns false, when there are no more details to handle.
/// </summary>
/// <returns></returns>
public abstract bool NextDetail();
/// <summary>
/// Returns the action point cost of this action.
/// Right now, only the values 1 and 0 make sense.
/// </summary>
/// <returns></returns>
public virtual int GetActionPointCost()
{
return 1;
}
/// <summary>
/// Will be called right after the action is selected by the player. Can be used to reset the state of the details
/// </summary>
public virtual void Reset()
{
}
/// <summary>
/// If this action should be bound to an action button in the UI, return the corresponding enum value here.
/// </summary>
/// <returns></returns>
public virtual AllyActionButton BindToActionButton()
{
return AllyActionButton.None;
}
public class Skip : FighterAction
{
@@ -72,9 +122,10 @@ public abstract class FighterAction
return 0f;
}
public override FighterActionDetail? NeededDetail()
public override bool NextDetail()
{
return null;
return false;
}
}
}
}
@@ -0,0 +1 @@
uid://c60jugfee0bpv
@@ -0,0 +1 @@
uid://bahm4ukspymm2
+46 -106
View File
@@ -1,131 +1,72 @@
using Godot;
using System;
using Babushka.scripts.CSharp.Common.Fight.ActionDetails;
using Godot;
using Godot.Collections;
namespace Babushka.scripts.CSharp.Common.Fight;
[Tool]
public partial class FighterVisual : Node2D
{
//[Export] public string name;
//[Export] public int maxHealth;
//[Export] public int attackStrength;
//[Export] public int maxActions = 1;
[Export] public FightWorld.Fighter.Type type;
#region Shortcuts
[ExportCategory("References")]
[Export] private Node2D _attackButtons;
[Export] private Node2D _targetButtons;
[Export] private Node2D _targetMarker;
[Export] private Label _healthText;
[Export] private Node2D _visualSprite;
private FightWorld.FightHappeningData HappeningData =>
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
[Signal] public delegate void DamageTakenEventHandler();
[Signal] public delegate void AttackingEventHandler();
[Signal] public delegate void DyingEventHandler();
[Signal] public delegate void HealedEventHandler();
#endregion
[ExportCategory("References")]
[Export] private Node2D _visualParent;
[Export] private Node2D _targetSelectionParent;
[Signal]
public delegate void DamageTakenEventHandler();
[Signal]
public delegate void AttackingEventHandler();
[Signal]
public delegate void DyingEventHandler();
[Signal]
public delegate void HealedEventHandler();
private FightWorld.Fighter _boundFighter;
//private void Die()
//{
// _visualSprite.Scale = new Vector2(1, 0.3f);
// EmitSignalDying();
//}
//public override void _Ready()
//{
// UpdateHealthVisual();
// ResetActions();
//}
public void Initialize(FightWorld.Fighter fighter)
{
_boundFighter = fighter;
UpdateHealthVisual();
UpdateMirrorState();
}
public void Attack()
/// <summary>
/// fighter visuals should always look to the right in the scene.
/// This function flips the sprites horizontally, when the fighter is an enemy.
/// </summary>
private void UpdateMirrorState()
{
//FightHappening.SelectAttack(this);
_visualParent.Scale = new Vector2(_boundFighter.isEnemy ? -1 : 1, 1);
}
public void HideAttackButton()
public void SetTargetSelectionActive(bool value)
{
_attackButtons.Hide();
_targetSelectionParent.Visible = value;
_targetSelectionParent.ProcessMode = value ? ProcessModeEnum.Inherit : ProcessModeEnum.Disabled;
}
public void ShowAttackButton()
// listen from inside
public void ClickedTarget()
{
_attackButtons.Show();
}
if (HappeningData.actionStaging!.CurrentDetail() is not TargetSelectActionDetail targetDetail)
throw new InvalidOperationException("No target selection needed right now");
public void HideTargetButtons()
{
_targetButtons.Hide();
targetDetail.SetTarget(_boundFighter);
FightHappening.Instance.DetailFilled();
}
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();
}
public void HealMouseEvent(Node viewport, InputEvent inputEvent, int shapeIdx)
{
if (inputEvent.IsPressed())
ClickedHeal();
}
private void ClickedAttack()
{
//FightHappening.SelectAttack(this);
}
private void ClickedHeal()
{
//FightHappening.SelectHeal(this);
}
private void ClickedTarget()
{
//FightHappening.SelectTargetAndAttack(this);
}
public void StartHoverTarget()
{
_targetMarker.Visible = true;
}
public void EndHoverTarget()
{
_targetMarker.Visible = false;
}
public void UpdateHealthVisual()
{
_healthText.Text = $"{_boundFighter.health}";
}
public bool IsDead()
{
//return Health <= 0;
return true;
}
public void ResetActions()
{
//_actions = maxActions;
}
// Animations
public void AttackAnimation(FightAttack attack)
{
EmitSignalAttacking();
@@ -134,7 +75,6 @@ public partial class FighterVisual : Node2D
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)
@@ -154,4 +94,4 @@ public partial class FighterVisual : Node2D
tween.TweenProperty(this, "scale", new Vector2(1, 1), 0.4)
.SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
}
}
}
@@ -0,0 +1 @@
uid://b2n37glcxm8wv
@@ -0,0 +1,20 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight;
public partial class RoamingEnemyGroup : Node2D
{
private FightWorld.FighterGroup _boundEnemyGroup;
private FightSceneSwitcher _fightSceneSwitcher;
public void Initialize(FightWorld.FighterGroup enemyGroup, FightSceneSwitcher fightSceneSwitcher)
{
_boundEnemyGroup = enemyGroup;
_fightSceneSwitcher = fightSceneSwitcher;
}
public void StartFight()
{
_fightSceneSwitcher.SwitchToFight(_boundEnemyGroup);
}
}
@@ -0,0 +1 @@
uid://lequnojtar76
@@ -0,0 +1,35 @@
using System.Linq;
using Godot;
namespace Babushka.scripts.CSharp.Common.Fight.UI;
public partial class ActionSelectUiSetup : CanvasLayer
{
// shortcuts
private FightWorld.FightHappeningData HappeningData =>
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
private FightWorld.Fighter CurrentFighter => HappeningData.fighterStack.Current;
// references
[Export] private Button _attackActionButton = null!;
[Export] private Button _summonActionButton = null!;
[Export] private Button _talkActionButton = null!;
[Export] private Button _fleeActionButton = null!;
// gets called from a state reaction enter (InputActionSelect)
public void StateEntered()
{
var actions = CurrentFighter.availableActions;
_attackActionButton.Visible = actions.Any(a => a.BindToActionButton() == FighterAction.AllyActionButton.Attack);
_summonActionButton.Visible = actions.Any(a => a.BindToActionButton() == FighterAction.AllyActionButton.Summon);
_talkActionButton.Visible = actions.Any(a => a.BindToActionButton() == FighterAction.AllyActionButton.Talk);
_fleeActionButton.Visible = actions.Any(a => a.BindToActionButton() == FighterAction.AllyActionButton.Flee);
}
public void SelectAction(FighterAction.AllyActionButton actionButton)
{
var action = CurrentFighter.availableActions.First(a => a.BindToActionButton() == actionButton);
FightHappening.Instance.ActionSelect(action);
}
}
@@ -0,0 +1 @@
uid://byf2ywov34g0x
@@ -0,0 +1,16 @@
using Godot;
using System;
public partial class TargetSelectionClick : Area2D
{
[Signal]
public delegate void TargetSelectedEventHandler();
public override void _InputEvent(Viewport viewport, InputEvent @event, int shapeIdx)
{
if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left })
{
EmitSignalTargetSelected();
}
}
}
@@ -0,0 +1 @@
uid://boprnfciqgixf
+22
View File
@@ -16,6 +16,16 @@ public static class LinqExtras
}
}
public static void ForEach<T>(this IEnumerable<T> self, Action<T, int> action)
{
var i = 0;
foreach (var t in self)
{
action.Invoke(t, i);
i++;
}
}
public static T? Random<T>(this IEnumerable<T> self)
{
var selfList = self.ToList();
@@ -24,4 +34,16 @@ public static class LinqExtras
var randomIndex = new Random().Next(0, selfList.Count);
return selfList[randomIndex];
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> self)
{
var selfList = self.ToList();
var random = new Random();
for (var i = 0; i < selfList.Count; i++)
{
var j = random.Next(i, selfList.Count);
(selfList[i], selfList[j]) = (selfList[j], selfList[i]);
}
return selfList;
}
}
@@ -0,0 +1 @@
uid://bxs7sn7j3vd0n